feat(sidebar) : slots footer / footer-collapsed collés en bas
Ajoute deux slots `footer` et `footer-collapsed` à MalioSidebar pour afficher un contenu en bas (profil, déconnexion, version…). Le footer est toujours collé en bas grâce au `flex-1` de la nav et reste visible quand la liste de liens scrolle. Bordure haute m-primary en mode déplié, à l'image du bloc logo. Tests, page playground, story et docs (COMPONENTS + CHANGELOG) mis à jour. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,18 @@
|
|||||||
<template #logo-collapsed>
|
<template #logo-collapsed>
|
||||||
<img src="/LOGO_MALIO_COLLAPSED.png" alt="Malio" />
|
<img src="/LOGO_MALIO_COLLAPSED.png" alt="Malio" />
|
||||||
</template>
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<Icon name="mdi:account-circle" size="32" class="text-m-primary" />
|
||||||
|
<div class="leading-tight">
|
||||||
|
<p class="text-[14px] font-semibold text-m-text">Tristan</p>
|
||||||
|
<p class="text-[12px] text-m-muted">Administrateur</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer-collapsed>
|
||||||
|
<Icon name="mdi:account-circle" size="28" class="mx-auto block text-m-primary" />
|
||||||
|
</template>
|
||||||
</MalioSidebar>
|
</MalioSidebar>
|
||||||
|
|
||||||
<MalioSidebar
|
<MalioSidebar
|
||||||
@@ -22,6 +34,18 @@
|
|||||||
<template #logo-collapsed>
|
<template #logo-collapsed>
|
||||||
<img src="/LOGO_MALIO_COLLAPSED.png" alt="Malio" />
|
<img src="/LOGO_MALIO_COLLAPSED.png" alt="Malio" />
|
||||||
</template>
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex w-full items-center gap-2 text-[15px] text-m-text hover:text-m-primary"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:logout" size="20" />
|
||||||
|
Déconnexion
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template #footer-collapsed>
|
||||||
|
<Icon name="mdi:logout" size="20" class="mx-auto block text-m-text" />
|
||||||
|
</template>
|
||||||
</MalioSidebar>
|
</MalioSidebar>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -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-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).
|
* [#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`.
|
* 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
|
### 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.)
|
* 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.)
|
||||||
|
|||||||
+5
-1
@@ -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.
|
**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)`
|
**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
|
```vue
|
||||||
<MalioSidebar v-model="isOpen" :sections="menuSections">
|
<MalioSidebar v-model="isOpen" :sections="menuSections">
|
||||||
<template #logo><img src="/logo.png" /></template>
|
<template #logo><img src="/logo.png" /></template>
|
||||||
<template #logo-collapsed><img src="/logo-small.png" /></template>
|
<template #logo-collapsed><img src="/logo-small.png" /></template>
|
||||||
|
<template #footer><UserProfile /></template>
|
||||||
|
<template #footer-collapsed><Icon name="mdi:account" /></template>
|
||||||
</MalioSidebar>
|
</MalioSidebar>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -239,6 +239,43 @@ describe('MalioSidebar', () => {
|
|||||||
expect(wrapper.find('img[alt="M"]').exists()).toBe(true)
|
expect(wrapper.find('img[alt="M"]').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('renders footer slot when expanded', () => {
|
||||||
|
const wrapper = mountComponent({sections}, {
|
||||||
|
footer: '<a href="/logout">Déconnexion</a>',
|
||||||
|
})
|
||||||
|
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': '<span>FC</span>',
|
||||||
|
})
|
||||||
|
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: '<span class="ft">Footer</span>',
|
||||||
|
})
|
||||||
|
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 <nav> sont des conteneurs (+ le bouton toggle).
|
||||||
|
// Aucun div de footer ne doit apparaître après le <nav>.
|
||||||
|
const children = Array.from(wrapper.find('aside').element.children)
|
||||||
|
const navIndex = children.findIndex(el => el.tagName === 'NAV')
|
||||||
|
const after = children.slice(navIndex + 1)
|
||||||
|
expect(after.some(el => el.tagName === 'DIV')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
it('uses custom id when provided', () => {
|
it('uses custom id when provided', () => {
|
||||||
const wrapper = mountComponent({sections, id: 'my-sidebar'})
|
const wrapper = mountComponent({sections, id: 'my-sidebar'})
|
||||||
expect(wrapper.find('aside').attributes('id')).toBe('my-sidebar')
|
expect(wrapper.find('aside').attributes('id')).toBe('my-sidebar')
|
||||||
|
|||||||
@@ -67,6 +67,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="$slots.footer || $slots['footer-collapsed']"
|
||||||
|
:class="['px-[20px] py-[14px]', collapsed ? '' : 'mx-[10px] border-t-2 border-m-primary']"
|
||||||
|
>
|
||||||
|
<slot
|
||||||
|
v-if="collapsed"
|
||||||
|
name="footer-collapsed"
|
||||||
|
/>
|
||||||
|
<slot
|
||||||
|
v-else
|
||||||
|
name="footer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
:aria-label="collapsed ? 'Déplier le menu' : 'Plier le menu'"
|
:aria-label="collapsed ? 'Déplier le menu' : 'Plier le menu'"
|
||||||
|
|||||||
@@ -43,6 +43,37 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Variant>
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="Avec footer (collé en bas)">
|
||||||
|
<div class="flex h-[600px] border rounded-lg overflow-hidden">
|
||||||
|
<MalioSidebar
|
||||||
|
v-model="collapsed3"
|
||||||
|
:sections="sectionsLong"
|
||||||
|
>
|
||||||
|
<template #logo>
|
||||||
|
<span class="text-2xl font-bold text-m-primary">Malio</span>
|
||||||
|
</template>
|
||||||
|
<template #logo-collapsed>
|
||||||
|
<span class="text-2xl font-bold text-m-primary">M</span>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="leading-tight">
|
||||||
|
<p class="text-[14px] font-semibold text-m-text">Tristan</p>
|
||||||
|
<p class="text-[12px] text-m-muted">Administrateur</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer-collapsed>
|
||||||
|
<span class="block text-center text-[14px] font-bold text-m-primary">T</span>
|
||||||
|
</template>
|
||||||
|
</MalioSidebar>
|
||||||
|
|
||||||
|
<div class="flex-1 p-6 bg-white">
|
||||||
|
<p class="text-m-muted">
|
||||||
|
Le footer reste collé en bas même quand la nav scrolle.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
</Story>
|
</Story>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -94,6 +125,15 @@ entre les deux états.
|
|||||||
|
|
||||||
- Contenu affiché en haut quand la sidebar est pliée.
|
- Contenu affiché en haut quand la sidebar est pliée.
|
||||||
|
|
||||||
|
### footer
|
||||||
|
|
||||||
|
- Contenu affiché en bas quand la sidebar est dépliée (profil, déconnexion, version…).
|
||||||
|
- Toujours collé en bas : la nav occupe l'espace restant (`flex-1`) et pousse le footer.
|
||||||
|
|
||||||
|
### footer-collapsed
|
||||||
|
|
||||||
|
- Contenu affiché en bas quand la sidebar est pliée.
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
## Comportement
|
## Comportement
|
||||||
@@ -127,6 +167,7 @@ import MalioSidebar from '../../components/malio/sidebar/Sidebar.vue'
|
|||||||
|
|
||||||
const collapsed1 = ref(false)
|
const collapsed1 = ref(false)
|
||||||
const collapsed2 = ref(false)
|
const collapsed2 = ref(false)
|
||||||
|
const collapsed3 = ref(false)
|
||||||
|
|
||||||
const sectionsShort = [
|
const sectionsShort = [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user