feat: Ajout des composants modal, accordeon, datetime avec selecteur d'heure à la molette (#56)
Release / release (push) Successful in 2m38s

| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|                  |                 |

## Description de la PR

## Modification du .env

## Check list

- [ ] Pas de régression
- [ ] TU/TI/TF rédigée
- [ ] TU/TI/TF OK
- [ ] CHANGELOG modifié

---------

Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Co-authored-by: matthieu <matthieu@yuno.malio.fr>
Reviewed-on: #56
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #56.
This commit is contained in:
2026-05-27 12:11:51 +00:00
committed by Autin
parent 7d7b2fb720
commit acd531f69e
36 changed files with 6581 additions and 30 deletions
@@ -0,0 +1,167 @@
# Spec — Composant Accordéon `<MalioAccordion>`
**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 `<MalioAccordion>` qui enveloppe des
enfants `<MalioAccordionItem>`. 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
### `<MalioAccordion>`
`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`.
### `<MalioAccordionItem>`
`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 `<button type="button">` → focusable nativement,
Entrée/Espace pour basculer.
- `aria-expanded` sur le bouton, `aria-controls` → id du panneau.
- Panneau : `role="region"` + `aria-labelledby` → id du bouton.
- Sections désactivées : `disabled` + `aria-disabled` sur le bouton.
- **Navigation clavier ↑/↓** entre les en-têtes (déplacement du focus d'un
en-tête à l'autre), conformément au pattern WAI-ARIA. `Home`/`End`
optionnels (nice-to-have).
## Tests (Vitest + @vue/test-utils, jsdom)
Helper `mountComponent(props)` colocalisé. Couverture cible :
**Accordion.test.ts**
- Rendu des enfants (slots).
- Mode `multiple` : plusieurs sections ouvertes simultanément.
- Mode `single` : ouvrir une section ferme la précédente.
- v-model contrôlé : `modelValue` pilote l'état ; émission de `update:modelValue`.
- Non-contrôlé : `defaultOpen` sur enfants → état initial correct.
**AccordionItem.test.ts**
- Toggle au clic sur l'en-tête.
- `disabled` : clic sans effet, attributs `disabled` / `aria-disabled`.
- Attributs ARIA : `aria-expanded`, `aria-controls`, `role="region"`,
`aria-labelledby` correctement liés.
- Navigation clavier ↑/↓ entre en-têtes.
- Override de classes via `headerClass` / `panelClass`.
## Livrables documentaires (convention maison)
- Mise à jour de `COMPONENTS.md` (tableau de props + exemples).
- Mise à jour de `CHANGELOG.md`.
- Page playground (ajout à `playground.nav.ts`).
- Story Histoire (`app/story/accordion/`).
## Hors périmètre (YAGNI, V1)
- Badge / compteur de filtres actifs dans l'en-tête.
- Icône leading.
- Slot d'en-tête personnalisé.
- Persistance d'état (localStorage, URL).
Ces éléments pourront être ajoutés ultérieurement si un besoin métier concret
émerge, sans casser l'API.