diff --git a/README.md b/README.md
index 147d45b7bc9f60acf2adba136fd1d326f44121d1..cdf95658c7e0edf7c29ce460fd7dd64878d5f274 100644
--- a/README.md
+++ b/README.md
@@ -41,13 +41,6 @@ statiques hautement optimisées pour la production.
 
 Vite est extensible via son API de plugin et son API JavaScript avec un support de typage complet.
 
-### [BootstrapVue](https://bootstrap-vue.org/)
-
-Version Next (l'officielle n'est pas encore compatible Vue3/BS5)
-
-Avec BootstrapVue, vous pouvez construire des projets web réactifs, axés sur les mobiles et accessibles ARIA 
-en utilisant des composants Vue.js pour générer du HTML compatible Bootstrap
-
 ### [Axios](https://axios-http.com/)
 
 Version 1
diff --git a/components/UCalendar.vue b/components/UCalendar.vue
new file mode 100644
index 0000000000000000000000000000000000000000..01f2da5d5ad768cf3d073a5309bf68cac557c86c
--- /dev/null
+++ b/components/UCalendar.vue
@@ -0,0 +1,236 @@
+<template>
+    <div class="calendar">
+        <div class="recherche">
+            <div class="recherche btn-group">
+                <button class="btn btn-light" id="prevMois" @click="prevMois" title="Mois précédant">
+                    <u-icon name="chevron-left"/>
+                </button>
+
+                <select class="form-select btn btn-light" id="otherMois" v-model="mois">
+                    <option v-for="m in listeMois()" :value="m.id">{{ m.libelle }}</option>
+                </select>
+
+                <select class="form-select btn btn-light" id="otherAnnee" v-model="annee">
+                    <option v-for="a in listeAnnees()" :value="a">{{ a }}</option>
+                </select>
+
+                <button class="btn btn-light" id="nextMois" @click="nextMois" title="Mois suivant">
+                    <u-icon name="chevron-right"/>
+                </button>
+            </div>
+        </div>
+
+        <table class="table table-bordered table-hover table-sm">
+            <tr v-for="jour in listeJours" :data-jour="jour">
+                <th class="nom-jour">
+                    {{ nomJour(jour) }}
+                </th>
+                <th class="numero-jour">
+                    <div class="num-jour badge bg-secondary rounded-circle">{{ jour < 10 ? '0' + jour.toString() : jour }}</div>
+                </th>
+                <td>
+                    <div class="event" :style="'border-color:'+event.color+';background-color:'+event.bgcolor" v-for="(event, index) in eventsByJour(jour)" :key="index">
+                        <component :is="event.component" :event="event"></component>
+                    </div>
+
+                    <div v-if="canAddEvent">
+                        <button @click="addEvent" :data-jour="jour" class="btn btn-light btn-sm">
+                            <u-icon name="plus"/>
+                            Nouvel événement
+                        </button>
+                    </div>
+                </td>
+            </tr>
+        </table>
+    </div>
+</template>
+
+<script>
+
+export default {
+    name: 'UCalendar',
+    props: {
+        date: {type: Date, required: true},
+        events: {type: Array, required: true},
+        canAddEvent: {type: Boolean, required: true, default: true},
+    },
+    data()
+    {
+        const dateObj = new Date(this.date);
+
+        return {
+            mois: dateObj.getMonth() + 1,
+            annee: dateObj.getFullYear(),
+        };
+    },
+    computed: {
+        listeJours()
+        {
+            const dateObj = new Date(this.date);
+
+            dateObj.setDate(1);
+            dateObj.setMonth(dateObj.getMonth() + 1);
+            dateObj.setDate(dateObj.getDate() - 1);
+
+            let nombreJours = dateObj.getDate();
+
+            return Array.from({length: nombreJours}, (v, k) => k + 1)
+        },
+    },
+    watch: {
+        date: function (newVal, oldVal) { // watch it
+            const dateObj = new Date(this.date);
+
+            this.mois = dateObj.getMonth() + 1;
+            this.annee = dateObj.getFullYear();
+        },
+        mois: function (newVal, oldVal) { // watch it
+            const dateObj = new Date(this.date);
+            dateObj.setMonth(newVal - 1);
+
+            this.$emit('changeDate', dateObj);
+        },
+        annee: function (newVal, oldVal) { // watch it
+            const dateObj = new Date(this.date);
+            dateObj.setFullYear(newVal);
+
+            this.$emit('changeDate', dateObj);
+        }
+    },
+    methods: {
+        nomJour(numJour)
+        {
+            const dateObj = new Date(this.date);
+            dateObj.setDate(numJour);
+            return dateObj.toLocaleString("fr-FR", {weekday: "short"});
+        },
+
+        listeMois()
+        {
+            let mois = [];
+
+            const dateObj = new Date();
+
+            for (let i = 1; i <= 12; i++) {
+                dateObj.setMonth(i - 1);
+                let nomMois = dateObj.toLocaleString("fr-FR", {month: "long"});
+                mois.push({id: i, libelle: nomMois});
+            }
+
+            return mois;
+        },
+
+        listeAnnees()
+        {
+            const dateObj = new Date();
+            const annee = dateObj.getFullYear();
+            const range = 1;
+
+            let annees = [];
+
+            for (let a = annee - range; a <= annee + range; a++) {
+                annees.push(a);
+            }
+
+            return annees;
+        },
+
+        addEvent(event)
+        {
+            const dateObj = new Date(this.date);
+            dateObj.setDate(event.currentTarget.dataset.jour);
+
+            this.$emit('addEvent', dateObj, event);
+        },
+
+        prevMois()
+        {
+            const dateObj = new Date(this.date);
+            dateObj.setMonth(dateObj.getMonth() - 1);
+
+            this.$emit('changeDate', dateObj);
+        },
+
+        nextMois()
+        {
+            const dateObj = new Date(this.date);
+            dateObj.setMonth(dateObj.getMonth() + 1);
+
+            this.$emit('changeDate', dateObj);
+        },
+
+        eventsByJour(jour)
+        {
+            const dateObj = new Date(this.date);
+
+            let res = {};
+            for (let e in this.events) {
+                let event = this.events[e];
+                if (event.date.getFullYear() === dateObj.getFullYear()
+                    && event.date.getMonth() + 1 === dateObj.getMonth() + 1
+                    && event.date.getDate() === jour
+                ) {
+                    res[e] = event;
+                }
+            }
+            return res;
+        },
+    }
+}
+</script>
+
+<style scoped>
+
+.table tr {
+    background-color: #f4f4f4;
+    border-left: 1px #ddd solid;
+    border-right: 1px #ddd solid;
+}
+
+.table-hover tr:hover {
+    background-color: #f7f7f7;
+}
+
+.recherche {
+    text-align: center;
+}
+
+.recherche .btn-group {
+    box-shadow: none;
+    margin: auto;
+}
+
+.recherche select.btn {
+    padding-right: 3em;
+}
+
+th.nom-jour {
+    width: 1%;
+    padding-left: 3px;
+}
+
+th.numero-jour {
+    width: 1%;
+    padding-right: .5em;
+}
+
+.recherche {
+    justify-content: center;
+    padding-bottom: 5px;
+}
+
+
+.event {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 3px;
+    border-left: 10px #bbb solid;
+    border-right: 10px #bbb solid;
+}
+
+.event:hover {
+    background-color: white;
+}
+
+</style>
\ No newline at end of file
diff --git a/components/UDate.vue b/components/UDate.vue
new file mode 100644
index 0000000000000000000000000000000000000000..8fb7706203c5fe9e969784aa5ecc2f723f3f6ee4
--- /dev/null
+++ b/components/UDate.vue
@@ -0,0 +1,54 @@
+<template>
+    {{ formatted }}
+</template>
+
+<script>
+export default {
+    name: "UDate",
+    props: {
+        'value': {required: false, type: [String,Date]},
+        'format': {required: false, type: String},
+    },
+    mounted(){
+        this.formatted = this.formatage(this.value);
+    },
+    data(){
+        return {
+            formatted: undefined,
+        };
+    },
+    watch: {
+        'value': function(val){
+            this.formatted = this.formatage(val);
+        },
+    },
+    methods: {
+        formatage(val){
+            if (val === undefined) {
+                return undefined;
+            }
+            let date = new Date(val);
+
+            const year = date.getFullYear();
+            const month = (date.getMonth() + 1).toString().padStart(2, '0');
+            const day = date.getDate().toString().padStart(2, '0');
+            const hour = date.getHours().toString().padStart(2, '0');
+            const min =  date.getMinutes().toString().padStart(2, '0');
+            const sec =  date.getSeconds().toString().padStart(2, '0');
+
+            switch(this.format){
+                case 'datetime':
+                    return `${day}/${month}/${year} à ${hour}:${min}`;
+                case 'time':
+                    return `${hour}:${min}:${sec}`;
+            }
+            // format 0 : DATE par défaut
+            return `${day}/${month}/${year}`;
+        }
+    },
+}
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/components/UIcon.vue b/components/UIcon.vue
new file mode 100644
index 0000000000000000000000000000000000000000..f33691bd3e4402f6d75d41e886421bf819ba703b
--- /dev/null
+++ b/components/UIcon.vue
@@ -0,0 +1,17 @@
+<template>
+    <i :class="`fas fa-${name} text-${variant}`"></i>
+</template>
+
+<script>
+export default {
+    name: "UIcon",
+    props: {
+        name: {required: true, type: String},
+        variant: {required: false, type: String},
+    },
+}
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/components/UModal.vue b/components/UModal.vue
new file mode 100644
index 0000000000000000000000000000000000000000..d6a51506cf5b9fa8c119366f55d2be2d84688c5c
--- /dev/null
+++ b/components/UModal.vue
@@ -0,0 +1,34 @@
+<template>
+    <div class="modal fade" :id="id" tabindex="-1" aria-hidden="true">
+        <div class="modal-dialog">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h5 class="modal-title">{{ title }}</h5>
+                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+                </div>
+                <div class="modal-body">
+                    <slot name="body"></slot>
+                </div>
+                <div class="modal-footer">
+                    <slot name="footer"></slot>
+                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+
+export default {
+    name: "UModal",
+    props: {
+        id: {required: true, type: String},
+        title: {required: true, type: String},
+    },
+}
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/doc/client.md b/doc/client.md
index 31780c407861c3ac71a1642693b10dd8409080db..ade9d87a70ab5bd61801a52b500ad0ca33743ad8 100644
--- a/doc/client.md
+++ b/doc/client.md
@@ -85,3 +85,44 @@ unicaenVue.axios.get(
 Lorsqu'il reçoit une réponse du serveur, si cette dernière comporte des messages issus du flashMessenger, il va les afficher sous formes de toasts.
 Les toasts sont personnalisées selon qu'il s'agit de success, warning, info ou error. Ils sont affichés pendant 3 secondes, sauf les erreurs qu'il faut fermer 
 manuellement afin d'avoir le temps de lire le message et de le traiter.
+
+## Bibliothèque de composants personnalisés
+
+UnicaenVue embarque quelques composants que vous pourrez utiliser directement, car il embarque un système d'autoloading de composants basé sur unplugin-vue-component.
+
+Voici les composants directement utilisables :
+
+- UCalendar : calendrier responsive utilisable sur smartphone avec possibilité de définir ses propres composants pour les événements
+- UDate : Affiche de manière lisible une date fournie en String au format ISO ou en objet Date JS.
+- UIcon : affiche une icône de font-awesome en ne lui donnant que le nom, sans avoir à écrire "fas fa-".
+- UModal : dessine une fenêtre modale Bootstrap, vous n'avez plus qu'à écrire le contenu et les entêtes/pied de page.
+
+En attendant une doc plus détaillée, vous pouvez voir les composants [ici](../components).
+
+## Créer votre propre resolver
+
+Si vous avez des composants génériques à utiliser un peu partout et que vous ne voulez pas faire d'import à tout bout de champ, il vous faut mettre en place votre propre résolveur.
+C'est faisable facilement, il suffit de les mettre dans un répertoire, et de déclarer le tout dans la config de Vite.
+
+```javascript
+import unicaenVue from 'unicaen-vue';
+    
+...
+
+const components = unicaenVue.findComponents('/var/www/mon/repertoire/absolu');
+
+...
+    
+// bout de config Vite à mettre dans vite.config.js
+
+export default unicaenVue.defineConfig({
+  ...
+  resolvers: [
+    (componentName) => {
+      if (components[componentName] !== undefined) {
+        return components[componentName];
+      }
+    },
+  ]
+});
+```
\ No newline at end of file
diff --git a/doc/doc.md b/doc/doc.md
index 388fc1fa666f9d2a2c4d07943efb6b487cc472c6..906d53d6ce27861adf3617072712b153922b9499 100644
--- a/doc/doc.md
+++ b/doc/doc.md
@@ -16,5 +16,4 @@ Sites officiels :
 
 - [Vue.js](https://vuejs.org/)
 - [Vite](https://vitejs.dev)
-- [BootstrapVue](https://bootstrap-vue.org/)
 - [Axios](https://axios-http.com/)
\ No newline at end of file
diff --git a/js/Server/index.js b/js/Server/index.js
index 21c77977c416dfa758002494aba4b1650d1cb0aa..452275fba3b26547232ab323b686fa4019fea0ab 100644
--- a/js/Server/index.js
+++ b/js/Server/index.js
@@ -1,23 +1,22 @@
+const path = require("path");
+
 /**
  * Simple object check.
  * @param item
  * @returns {boolean}
  */
-function isObject(item)
-{
+function isObject(item) {
     return (item && typeof item === 'object' && !Array.isArray(item));
 }
 
 
-
 /**
  * Permet de fusionner des objets JSON comprenant des sous-objets ou des tableaux
  * @param target
  * @param ...sources
  * @returns Object
  */
-function deepMerge(target, ...sources)
-{
+function deepMerge(target, ...sources) {
     if (!sources.length) return target;
     const source = sources.shift();
 
@@ -43,14 +42,12 @@ function deepMerge(target, ...sources)
 }
 
 
-
 /**
  * Retourne le répertoire racine d'UnicaenVue
  *
  * @returns String
  */
-function unicaenVueDir()
-{
+function unicaenVueDir() {
     const path = require('path');
 
     // répertoire racine d'UnicaenVue', en partant du répertoire actuel, donc src/Server
@@ -58,34 +55,80 @@ function unicaenVueDir()
 }
 
 
-
 /**
  * Retourne le répertoire racine de l'application.
  *
  * @returns String
  */
-function projectDir()
-{
+function projectDir() {
     // répertoire racine de l'application
     // Attention : le script doit être lancé depuis le bon répertoire...
     return process.cwd();
 }
 
 
+/**
+ * Retourne la liste des composants Vue dans un répertoire donné par nom de composant et fichiers en chemin absolu
+ *
+ * Exemple de retour pour les fichiers suivants (chemin absolu) :
+ * - /var/www/html/prod/monAppli/components/MonComposant.vue
+ * - /var/www/html/prod/monAppli/components/MonComposant2.vue
+ * - /var/www/html/prod/monAppli/components/Autres/MonAutre3.vue
+ *
+ * findComponents('/var/www/html/prod/monAppli/components') => {
+ *     MonComposant: /var/www/html/prod/monAppli/components/MonComposant.vue,
+ *     MonComposant2: /var/www/html/prod/monAppli/components/MonComposant2.vue,
+ *     AutresMonAutre3: /var/www/html/prod/monAppli/components/Autres/MonAutre3.vue
+ * }
+ *
+ * @param string componentsDir
+ * @returns {{}}
+ */
+function findComponents(componentsDir) {
+    const path = require("path");
+    const FS = require("fs");
+
+    let componentsFiles = [];
+
+    function parseComponents(Directory) {
+        FS.readdirSync(Directory).forEach(File => {
+            const absolute = path.join(Directory, File);
+            if (FS.statSync(absolute).isDirectory()) return parseComponents(absolute);
+            else if (absolute.endsWith('.vue')) {
+                return componentsFiles.push(absolute.slice(componentsDir.length + 1, -4));
+            }
+        });
+    }
+
+    parseComponents(componentsDir);
+
+
+    let components = {};
+
+    for (const c in componentsFiles) {
+        components[componentsFiles[c].replaceAll('/', '')] = path.resolve(componentsDir, componentsFiles[c]) + '.vue';
+    }
+
+    return components;
+}
+
 
 /**
  *
  * @param Object projectConfig
  * @returns {*}
  */
-function defineConfig(projectConfig)
-{
+function defineConfig(projectConfig) {
     const path = require('path');
     const vue = require('@vitejs/plugin-vue');
     const vite = require('vite');
     const liveReload = require('vite-plugin-live-reload');
     const Components = require('unplugin-vue-components/vite');
-    const resolvers = require('unplugin-vue-components/resolvers');
+
+
+    const componentsDir = path.resolve(__dirname, '../../components');
+    const components = findComponents(componentsDir);
+    //const resolvers = require('unplugin-vue-components/resolvers');
 
 
     const root = projectConfig.root ? projectConfig.root : 'root';
@@ -129,8 +172,11 @@ function defineConfig(projectConfig)
             }
         },
         resolvers: [
-            // Resolver pour que les composants bootstrapVue soient chargés automatiquement
-            resolvers.BootstrapVueNextResolver(),
+            (componentName) => {
+                if (components[componentName] !== undefined) {
+                    return components[componentName];
+                }
+            },
         ],
     };
 
@@ -145,17 +191,15 @@ function defineConfig(projectConfig)
 }
 
 
-
-function start()
-{
+function start() {
 
 }
 
 
-
 module.exports = {
     defineConfig,
     projectDir,
+    findComponents,
     unicaenVueDir,
     start
 };
\ No newline at end of file
diff --git a/package.json b/package.json
index 9dbc0a2c760135dda235bbf11073695485a7f45c..909bba78f60ab010e18378e1c22795a2e6ff6e8f 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,6 @@
   "license": "ISC",
   "private": true,
   "dependencies": {
-    "bootstrap-vue-next": "^0.7.3",
     "vue": "^3.2.45",
     "@vitejs/plugin-vue": "^4.0.0",
     "@vue/compiler-sfc": "^3.2.45",