# Spec — Composant Accordéon `` **Date :** 2026-05-26 **Ticket :** MUI-37 **Statut :** Validé (design), prêt pour planification ## Contexte & objectif Ajouter un composant accordéon à `@malio/layer-ui`. Cas d'usage principal : un **système de filtres dans un drawer** d'ERP, où plusieurs sections de critères (prix, catégorie, marque…) doivent pouvoir être dépliées simultanément, chaque section ayant un contenu hétérogène (checkboxes, slider, recherche…). ## Décision d'API : composants enfants (compositional) Plutôt que l'API « tableau `items` + slots » de NuxtUI (qui impose un template `#content` unique avec un switch central sur l'item courant), on adopte une **API compositionnelle** : un parent `` qui enveloppe des enfants ``. Chaque section déclare son titre **et** son contenu au même endroit, sans switch central, et s'ajoute/se retire indépendamment. Rationale : pour des filtres au contenu hétérogène, c'est nettement plus lisible et évolutif. On reste **100 % natif** (pas de dépendance Reka UI, contrairement à NuxtUI), cohérent avec le `TabList` maison et les conventions du layer (`@iconify/vue`, `twMerge`, props `*Class`). ## Architecture ``` MalioAccordion (parent : état d'ouverture, mode, coordination) └─ MalioAccordionItem (enfant : en-tête cliquable + panneau animé + slot) ``` Le parent **fournit** (`provide`) un contexte d'accordéon ; chaque enfant **l'injecte** (`inject`) pour connaître son état d'ouverture et déclencher les bascules. Communication via une clé `Symbol` (`InjectionKey`). **Contexte fourni** (forme indicative) : ```ts interface AccordionContext { mode: ComputedRef<'single' | 'multiple'> isOpen: (value: string) => boolean toggle: (value: string) => void register: (value: string, defaultOpen: boolean) => void // enfant → parent au montage unregister: (value: string) => void baseId: string // pour générer les ids ARIA registerHeader / focus nav helpers // pour la navigation flèches } ``` **Fichiers :** ``` app/components/malio/accordion/Accordion.vue app/components/malio/accordion/AccordionItem.vue app/components/malio/accordion/Accordion.test.ts app/components/malio/accordion/AccordionItem.test.ts ``` (+ page playground et story Histoire, cf. skill `creating-malio-component`.) ## API publique ### `` `defineOptions({ name: 'MalioAccordion', inheritAttrs: false })` | Prop | Type | Défaut | Rôle | |------|------|--------|------| | `mode` | `'single' \| 'multiple'` | `'multiple'` | Un seul ou plusieurs panneaux ouverts | | `modelValue` | `string \| string[]` | `undefined` | v-model des clés ouvertes (`string` en `single`, `string[]` en `multiple`) | | `id` | `string` | auto (`useId`) | Base d'id pour les attributs ARIA | | `groupClass` | `string` | `''` | Classes du conteneur (fusion `twMerge`) | **Events :** `update:modelValue(value: string | string[])` **Pattern contrôlé / non-contrôlé** (convention maison) : `isControlled = computed(() => props.modelValue !== undefined)`, avec `localValue` en fallback. En non-contrôlé, l'état initial est dérivé des enfants ayant `defaultOpen`. ### `` `defineOptions({ name: 'MalioAccordionItem', inheritAttrs: false })` | Prop | Type | Défaut | Rôle | |------|------|--------|------| | `title` | `string` | — | Texte de l'en-tête | | `value` | `string` | auto (`useId`) | Clé unique (recommandée pour piloter le v-model) | | `defaultOpen` | `boolean` | `false` | Ouvert au montage (mode non-contrôlé) | | `disabled` | `boolean` | `false` | En-tête non cliquable | | `headerClass` | `string` | `''` | Override classes de l'en-tête (`twMerge`) | | `panelClass` | `string` | `''` | Override classes du panneau (`twMerge`) | **Slot par défaut** = contenu du panneau. ## Comportement : mode `single` vs `multiple` - **`multiple`** (défaut) : `modelValue` est un `string[]`. Basculer une section ajoute/retire sa clé du tableau, sans affecter les autres. - **`single`** : `modelValue` est un `string` (clé ouverte, ou `''`/`undefined` si tout fermé). Ouvrir une section ferme la précédente. L'en-tête minimal : **titre + chevron animé** uniquement. Pas de badge, pas d'icône leading, pas de slot d'en-tête custom dans cette version (extensible plus tard si besoin métier). ## Animation & rendu - **Ouverture/fermeture** : transition de hauteur via `grid-template-rows: 0fr → 1fr` sur un wrapper en `overflow: hidden` (gère la hauteur dynamique du contenu sans mesure JS). - **Chevron** : `mdi:chevron-down` via `@iconify/vue`, rotation 180° en transition synchronisée avec l'ouverture. - **Tokens Malio** : séparateurs `border-m-border`, titre `text-m-text`, `rounded-malio` au besoin. Tout surchargeable via `headerClass` / `panelClass` fusionnés avec `twMerge()`. ## Accessibilité (WAI-ARIA Accordion Pattern) - En-tête = vrai `