+
+
+
+
+
+
+
+
+
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9a115ce..24607cf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -57,6 +57,7 @@ Liste des évolutions de la librairie Malio layer UI
* [#MUI-44] MalioDate / MalioDateTime : event `update:rawValue` (string) exposant la saisie brute sur un canal séparé pour la validation back-autoritative — saisie invalide (non parsable ou hors `min`/`max`) → texte trimmé tel que tapé, saisie valide/vide + clear + sélection au calendrier → `''`. `modelValue` reste `string` ISO `| null` (la saisie invalide n'y transite jamais) ; le parent construit son payload via `valid ? modelValue : rawValue`.
* [#MUI-45] MalioDate : prop `markedDates` (`Record<"YYYY-MM-DD", 'success' | 'danger'>`) appliquant un fond tokenisé par jour dans la grille (générique, fourni par le consommateur ; précédence sélection/`today` > variante marquée > défaut) + event `month-change` (`{ month: 0-11, year }`) émis à l'ouverture du popover et à chaque navigation de mois. Sert l'écran *Heures* de SIRH (jours validés en vert, chargement du mois visible à la volée).
* Calendrier (Date/DateRange/DateTime/DateWeek) : sélecteur d'année (3ᵉ niveau de navigation — jours → mois → années) et grisage des mois et années hors `min`/`max`.
+* MalioSidebar : slots `footer` / `footer-collapsed` pour ajouter un contenu en bas de la sidebar (profil, déconnexion, version…). Toujours collé en bas (la nav `flex-1` le pousse), reste visible quand la liste de liens scrolle ; bordure haute `m-primary` en mode déplié, à l'image du bloc logo.
### Changed
* Cohérence du mode **`disabled`** sur toute la famille formulaire (calqué sur InputText : texte + label grisés, `cursor-not-allowed`, aucune affordance interactive). Concrètement, quand `disabled` : le **bouton « + »** d'ajout disparaît (InputPhone, InputEmail), l'**œil** de révélation disparaît (InputPassword), le **chevron** disparaît (Select, SelectCheckbox, InputAutocomplete), la **croix d'effacement** reste masquée (date, upload, time), le **label** passe en `text-m-muted` (Select, SelectCheckbox, famille Date via CalendarField, TimePicker), et les **tags** du SelectCheckbox + la valeur du Select passent en gris. (InputText, InputAmount, InputNumber, InputTextArea, InputRichText, Checkbox, RadioButton, InputUpload étaient déjà conformes.)
diff --git a/COMPONENTS.md b/COMPONENTS.md
index 1dbe850..2b527ce 100644
--- a/COMPONENTS.md
+++ b/COMPONENTS.md
@@ -893,12 +893,16 @@ Barre latérale de navigation rétractable.
**Lien actif :** un lien est marqué actif (texte `m-primary` + semi-bold) quand la route courante **est ce lien ou une de ses sous-routes** (match par préfixe) — ex. `/supplier` reste actif sur `/supplier/1/edit`. Mettre `exact: true` sur l'item force le match strict (actif uniquement sur la route exacte). Indépendant de l'imbrication des routes côté consommateur.
**Events :** `update:modelValue(value: boolean)`
-**Slots :** `logo` (sidebar ouverte), `logo-collapsed` (sidebar fermée)
+**Slots :** `logo` (sidebar ouverte), `logo-collapsed` (sidebar fermée), `footer` (bas, sidebar ouverte), `footer-collapsed` (bas, sidebar fermée)
+
+Le footer est **toujours collé en bas** : la nav occupe l'espace restant (`flex-1`) et pousse le footer vers le bas, qui reste visible même quand la liste de liens scrolle.
```vue
+
+
```
diff --git a/app/components/malio/sidebar/Sidebar.test.ts b/app/components/malio/sidebar/Sidebar.test.ts
index 22f4846..02cafc9 100644
--- a/app/components/malio/sidebar/Sidebar.test.ts
+++ b/app/components/malio/sidebar/Sidebar.test.ts
@@ -239,6 +239,43 @@ describe('MalioSidebar', () => {
expect(wrapper.find('img[alt="M"]').exists()).toBe(true)
})
+ it('renders footer slot when expanded', () => {
+ const wrapper = mountComponent({sections}, {
+ footer: 'Déconnexion',
+ })
+ expect(wrapper.find('a[href="/logout"]').exists()).toBe(true)
+ expect(wrapper.text()).toContain('Déconnexion')
+ })
+
+ it('renders footer-collapsed slot when collapsed', async () => {
+ const wrapper = mountComponent({sections}, {
+ 'footer-collapsed': 'FC',
+ })
+ await wrapper.find('button').trigger('click')
+ expect(wrapper.text()).toContain('FC')
+ })
+
+ it('footer is rendered after the nav (pushed to the bottom)', () => {
+ const wrapper = mountComponent({sections}, {
+ footer: 'Footer',
+ })
+ const children = wrapper.find('aside').element.children
+ const navIndex = Array.from(children).findIndex(el => el.tagName === 'NAV')
+ const footerEl = wrapper.find('.ft').element
+ const footerWrapperIndex = Array.from(children).findIndex(el => el.contains(footerEl))
+ expect(footerWrapperIndex).toBeGreaterThan(navIndex)
+ })
+
+ it('does not render a footer container when no footer slot is provided', () => {
+ const wrapper = mountComponent({sections})
+ // Seuls le bloc logo et le