diff --git a/.playground/pages/composant/sidebar/sidebar.vue b/.playground/pages/composant/sidebar/sidebar.vue index 533306e..96e22da 100644 --- a/.playground/pages/composant/sidebar/sidebar.vue +++ b/.playground/pages/composant/sidebar/sidebar.vue @@ -10,6 +10,18 @@ + + Malio + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a115ce..107ab34 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.) @@ -71,6 +72,7 @@ Liste des évolutions de la librairie Malio layer UI * [#MUI-42] Button / ButtonIcon : l'anneau de focus passe du halo `ring-2 ring-m-primary/50` à l'anneau standard `.m-focus-ring` (outline plein, offset 2px), pour l'homogénéité avec les autres composants. ### Fixed +* [#MUI-47] Sidebar : la **bande de ~4px en haut/bas d'un lien** (padding du `
  • ` qui porte le fond de hover) était survolée mais **non cliquable**. Le padding vertical passe du `
  • ` à l'`` (`py-1`), si bien que toute la zone survolée devient cliquable — sans changement visuel. Les côtés n'étaient pas affectés (`` en `block`, pas de padding horizontal sur le `
  • `). * Sidebar : le **lien actif** reste actif sur les **sous-routes** (match par préfixe via `useRoute().path` au lieu de l'`active-class` de NuxtLink qui dépendait de l'imbrication des routes) — ex. `/supplier` reste surligné sur `/supplier/1/edit`. Nouvelle option `exact: true` par item pour forcer le match strict. * Famille Date (CalendarField) : le **clic sur le picto calendrier** ouvre désormais le popover (le `` en overlay absolu interceptait le clic sans le traiter, et ne le laissait pas retomber sur l'input). Couvre Date, DateTime, DateRange, DateWeek. La croix d'effacement conserve son comportement (efface sans ouvrir). * Famille Date editable (MalioDate, MalioDateTime) : la saisie clavier est désormais **bornée par champ** sur le premier **et** le second chiffre (jour `01-31`, mois `01-12`, heure `00-23`, minute `00-59`) — une valeur hors plage (`99/99/9999`, un jour `33`, un mois `19`…) ne peut plus être tapée (auparavant saisissable puis rejetée a posteriori par la validation). Les impossibilités calendaires fines (`31/02`, 29/02 non bissextile, hors `min`/`max`) restent captées par la validation. Implémenté via `buildBoundedMask(template)` (CalendarField) : un `preProcess` maska valide chaque champ progressivement (un chiffre n'est accepté que s'il reste une complétion valide dans la plage) ; il distingue le mois des minutes (même lettre `M`) selon la présence d'heures dans le gabarit. 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..83bb3bd 100644 --- a/app/components/malio/sidebar/Sidebar.test.ts +++ b/app/components/malio/sidebar/Sidebar.test.ts @@ -107,19 +107,24 @@ describe('MalioSidebar', () => { expect(links[2].attributes('href')).toBe('/fournisseurs') }) - it('hover : fond + couleur + semi-bold tous portés par le
  • (texte non figé sur le )', () => { + it('hover : fond + couleur + semi-bold portés par le
  • ', () => { const wrapper = mountComponent({sections}) const li = wrapper.find('li') expect(li.classes()).toContain('hover:bg-m-primary/10') expect(li.classes()).toContain('hover:text-m-primary') expect(li.classes()).toContain('hover:font-semibold') expect(li.classes()).toContain('text-black') - expect(li.classes()).toContain('pt-1') - expect(li.classes()).toContain('pb-1') - // Le ne fige PAS sa couleur (sinon le texte resterait noir sur les bandes - // pt-1/pb-1 hors du alors que le fond du
  • est bleu). - expect(wrapper.find('a').classes()).not.toContain('text-black') - expect(wrapper.find('a').classes()).not.toContain('hover:text-m-primary') + }) + + it('zone cliquable : le padding vertical est sur le , pas sur le
  • (pas de bande morte au survol)', () => { + const wrapper = mountComponent({sections}) + // Le padding vertical doit appartenir à la cible de clic () pour que toute + // la bande survolée soit cliquable — sinon pt-1/pb-1 sur le
  • crée une + // bande colorée mais non cliquable en haut et en bas du lien. + const li = wrapper.find('li') + expect(li.classes()).not.toContain('pt-1') + expect(li.classes()).not.toContain('pb-1') + expect(wrapper.find('a').classes()).toContain('py-1') }) it('actif : route exacte → lien en primary + semi-bold, sans fond', () => { @@ -239,6 +244,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