From cdc08a04299f57ef68eb133c13297e528c4fac7c Mon Sep 17 00:00:00 2001 From: tristan Date: Tue, 26 May 2026 15:18:08 +0200 Subject: [PATCH] docs(accordion): spec de conception du composant MalioAccordion [#MUI-37] Co-Authored-By: Claude Opus 4.7 (1M context) --- .../specs/2026-05-26-accordion-design.md | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-26-accordion-design.md diff --git a/docs/superpowers/specs/2026-05-26-accordion-design.md b/docs/superpowers/specs/2026-05-26-accordion-design.md new file mode 100644 index 0000000..695a46b --- /dev/null +++ b/docs/superpowers/specs/2026-05-26-accordion-design.md @@ -0,0 +1,167 @@ +# 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 `