Ce drawer n'a pas de bouton fermer. Cliquez sur le backdrop pour fermer.
-
-
-
-
-
-
-
-
-
Ce drawer utilise une largeur personnalisée via la prop drawerClass.
+
+
+
Action requise
+
+
Ni le backdrop ni Échap ne ferment ce drawer. Utilisez la croix.
-
-
-# MalioDrawer
-
-Panneau latéral (drawer) qui s'ouvre depuis la droite avec un fond semi-transparent.
-
-## Props détaillées
-
-| Prop | Type | Défaut | Description |
-|------|------|--------|-------------|
-| `id` | `string` | auto-généré | Identifiant HTML du drawer |
-| `modelValue` | `boolean` | `undefined` | État ouvert/fermé (v-model) |
-| `title` | `string` | `''` | Titre affiché dans le header |
-| `showClose` | `boolean` | `true` | Afficher le bouton de fermeture (croix) |
-| `drawerClass` | `string` | `''` | Classes CSS additionnelles sur le panneau (fusionnées via `twMerge`) |
-
-## Comportement
-
-- Le drawer s'ouvre en glissant depuis la droite avec une transition
-- Un backdrop semi-transparent couvre le reste de la page
-- Clic sur le backdrop ferme le drawer
-- Bouton de fermeture (croix) en haut à droite, masquable via `showClose`
-- Contenu scrollable si plus haut que la fenêtre
-- Teleport vers `` pour éviter les problèmes de z-index
-
-## Accessibilité
-
-- `role="dialog"` et `aria-modal="true"` sur le panneau
-- `aria-labelledby` lié au titre
-- Bouton fermer avec `aria-label="Fermer"`
-
-## Events
-
-| Event | Payload | Description |
-|-------|---------|-------------|
-| `update:modelValue` | `boolean` | Émis à la fermeture (backdrop ou bouton) |
-
-## Slots
-
-| Slot | Description |
-|------|-------------|
-| `default` | Contenu du drawer |
-
-
-
diff --git a/docs/superpowers/plans/2026-05-21-drawer-redesign.md b/docs/superpowers/plans/2026-05-21-drawer-redesign.md
new file mode 100644
index 0000000..3f7c29a
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-21-drawer-redesign.md
@@ -0,0 +1,1117 @@
+# Refonte `` — Plan d'implémentation
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Réécrire `` en composant hand-rollé avec slots `#header`/défaut/`#footer`, choix de côté (`side`), accessibilité réelle (focus-trap, restitution du focus, Échap), scroll-lock et fermeture configurable.
+
+**Architecture:** Un seul SFC `Drawer.vue` (Teleport + Transition, pattern contrôlé/non-contrôlé Malio). Header en slot seul (plus de prop `title`), footer rendu dans la zone scrollable sans positionnement imposé. Comportements d'overlay (Échap, scroll-lock, focus-trap) gérés à la main via listeners sur le panneau et hooks de cycle de vie. **Breaking change → version majeure.**
+
+**Tech Stack:** Vue 3 `
+
+
+```
+
+> Note : `isRendered` reste `true` après ouverture jusqu'à la fin de l'animation de sortie. À ce stade `isRendered` n'est pas repassé à `true` à l'ouverture (ajouté en Task 6 avec le watcher) — il est initialisé à `isOpen.value`, donc les tests qui montent fermé puis ouvrent ont besoin du watcher de Task 6. **Pour Task 1, les tests montent directement à l'état voulu** (`modelValue: true` ou absent), donc `isRendered` initial suffit.
+
+- [ ] **Step 4: Lancer les tests pour vérifier qu'ils passent**
+
+Run: `npx vitest run app/components/malio/drawer/Drawer.test.ts`
+Expected: PASS (8 tests)
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add app/components/malio/drawer/Drawer.vue app/components/malio/drawer/Drawer.test.ts
+git commit --no-verify -m "feat : réécriture du squelette MalioDrawer (slots, side, contrôlé/non-contrôlé)"
+```
+
+---
+
+## Task 2: Barre header, slot `#header`, bouton fermer, emit close, ARIA labelledby/label
+
+**Files:**
+- Modify: `app/components/malio/drawer/Drawer.vue`
+- Test: `app/components/malio/drawer/Drawer.test.ts`
+
+- [ ] **Step 1: Ajouter les tests**
+
+Ajouter ces tests dans le `describe`, après le test `applies drawerClass` :
+
+```ts
+ it('renders the #header slot inside the header bar', () => {
+ const wrapper = mountComponent(
+ { modelValue: true },
+ { header: '
Titre
' },
+ )
+ expect(wrapper.find('[data-test="header"] [data-test="title"]').text()).toBe('Titre')
+ })
+
+ it('renders the header bar when showClose is true even without #header', () => {
+ const wrapper = mountComponent({ modelValue: true })
+ expect(wrapper.find('[data-test="header"]').exists()).toBe(true)
+ })
+
+ it('does not render the header bar when no #header and showClose is false', () => {
+ const wrapper = mountComponent({ modelValue: true, showClose: false })
+ expect(wrapper.find('[data-test="header"]').exists()).toBe(false)
+ })
+
+ it('shows the close button by default', () => {
+ const wrapper = mountComponent({ modelValue: true })
+ expect(wrapper.find('[data-test="close-button"]').exists()).toBe(true)
+ })
+
+ it('hides the close button when showClose is false', () => {
+ const wrapper = mountComponent(
+ { modelValue: true, showClose: false },
+ { header: '
' },
+ )
+ const panel = wrapper.find('[data-test="panel"]')
+ expect(panel.attributes('aria-labelledby')).toBe('test-drawer-header')
+ expect(wrapper.find('[data-test="header-content"]').attributes('id')).toBe('test-drawer-header')
+ })
+
+ it('sets aria-label from ariaLabel when no #header is provided', () => {
+ const wrapper = mountComponent({ modelValue: true, ariaLabel: 'Panneau latéral' })
+ const panel = wrapper.find('[data-test="panel"]')
+ expect(panel.attributes('aria-label')).toBe('Panneau latéral')
+ expect(panel.attributes('aria-labelledby')).toBeUndefined()
+ })
+
+ it('applies headerClass to the header bar', () => {
+ const wrapper = mountComponent({ modelValue: true, headerClass: 'bg-m-primary' })
+ expect(wrapper.find('[data-test="header"]').classes()).toContain('bg-m-primary')
+ })
+```
+
+- [ ] **Step 2: Lancer les tests pour vérifier qu'ils échouent**
+
+Run: `npx vitest run app/components/malio/drawer/Drawer.test.ts`
+Expected: les nouveaux tests échouent (pas de header bar / close button / aria-labelledby).
+
+- [ ] **Step 3: Ajouter la barre header dans le template**
+
+Dans `Drawer.vue`, insérer ce bloc **juste avant** le `
` (à l'intérieur du panneau, avant le body) :
+
+```vue
+
+
+
+
+
+
+```
+
+Ajouter les attributs ARIA sur le panneau : remplacer la ligne `aria-modal="true"` par :
+
+```vue
+ aria-modal="true"
+ :aria-labelledby="hasHeader ? headerId : undefined"
+ :aria-label="hasHeader ? undefined : (ariaLabel || undefined)"
+```
+
+- [ ] **Step 4: Mettre à jour le `
+
+
+
+
+
Drawer droite (défaut)
+
+
+
+
Détails
+
+
Contenu du drawer. Échap, clic backdrop et croix le ferment.
+
+
+
+
+
Drawer gauche
+
+
+
+
Navigation
+
+
Ce drawer glisse depuis la gauche.
+
+
+
+
+
Avec footer collant
+
+
+
+
Nouveau contact
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Non dismissable (croix uniquement)
+
+
+
+
Action requise
+
+
Ni le backdrop ni Échap ne ferment ce drawer. Utilisez la croix.
+
+
+
+
+```
+
+- [ ] **Step 2: Vérifier visuellement (optionnel, si l'environnement le permet)**
+
+Run: `npm run dev` puis ouvrir `/composant/drawer/drawer`.
+Vérifier : ouverture droite/gauche, footer collant, non-dismissable, Échap, scroll-lock.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add .playground/pages/composant/drawer/drawer.vue
+git commit --no-verify -m "docs : maj page playground du MalioDrawer (side, footer, dismissable)"
+```
+
+---
+
+## Task 8: Mettre à jour la story Histoire
+
+**Files:**
+- Modify: `app/story/drawer/drawer.story.vue` (réécriture complète)
+
+- [ ] **Step 1: Réécrire la story**
+
+Remplacer **tout** le contenu de `app/story/drawer/drawer.story.vue` par :
+
+```vue
+
+
+
+
+
+
+
+
+
+
Détails
+
+
Contenu simple du drawer.
+
+
+
+
+
+
+
+
+
+
Navigation
+
+
Ce drawer glisse depuis la gauche.
+
+
+
+
+
+
+
+
+
+
Nouveau contact
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Action requise
+
+
Ni le backdrop ni Échap ne ferment ce drawer. Utilisez la croix.
+
+
+
+
+
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add app/story/drawer/drawer.story.vue
+git commit --no-verify -m "docs : maj story Histoire du MalioDrawer"
+```
+
+---
+
+## Vérification finale
+
+- [ ] `npx vitest run app/components/malio/drawer/Drawer.test.ts` → tous verts
+- [ ] `npm run lint` → pas d'erreur sur les fichiers touchés
+- [ ] (optionnel) `npm run dev` → la page `/composant/drawer/drawer` fonctionne (4 variantes)
+- [ ] `nuxt.config.ts` toujours modifié dans le working tree ? Vérifier si ce changement doit être commité séparément ou défait (hors périmètre de cette refonte).
+
+## Notes de migration pour les apps consommatrices (à communiquer)
+
+- `title="X"` → `
X
`
+- `contenu` → inchangé
+- `drawer-class`, `show-close` → inchangés
+- Nouvelles props : `side`, `dismissable`, `close-on-escape`, `aria-label`, classes `overlay/header/body/footerClass`
+- Nouveaux slots : `#header`, `#footer`
+- Nouvel emit : `close`
diff --git a/docs/superpowers/specs/2026-05-21-drawer-redesign-design.md b/docs/superpowers/specs/2026-05-21-drawer-redesign-design.md
new file mode 100644
index 0000000..435e1fd
--- /dev/null
+++ b/docs/superpowers/specs/2026-05-21-drawer-redesign-design.md
@@ -0,0 +1,146 @@
+# Refonte du composant `` — Design
+
+> Ticket : MUI-35 — Revoir le design du composant Drawer
+> Date : 2026-05-21
+> Statut : design validé, à implémenter
+
+## Contexte & problème
+
+Le `` actuel fait le strict minimum et ne tient pas la comparaison
+avec les drawers des libs modernes (shadcn/Sheet, PrimeVue, Element Plus, Nuxt UI) :
+
+- glisse **uniquement depuis la droite**, pas de choix de côté ;
+- **un seul slot** (le contenu), pas de header/footer structurés ;
+- **aucune accessibilité réelle** : pas de focus-trap, pas de restitution du focus,
+ pas de fermeture au clavier (Échap) ;
+- **pas de scroll-lock** du body quand le drawer est ouvert.
+
+Objectif : refondre le composant en gardant l'esprit du layer Malio
+(hand-rollé, 1 composant `.vue`, props communes, `twMerge`), sans introduire de
+dépendance ni refondre les autres composants.
+
+## Décisions structurantes
+
+- **Hand-rollé**, pas de dépendance type Reka UI. Cohérence avec le reste du layer.
+- **Un seul composant** `` (props + slots). Pas de primitives composables.
+- **Breaking change assumé** → bump de version **majeure** via semantic-release.
+ Les apps consommatrices migreront (cf. section Migration).
+- Périmètre : **le drawer uniquement**. Les autres composants ne bougent pas.
+
+## API
+
+### Slots
+
+| Slot | Rôle |
+|------|------|
+| `#header` | Contenu d'en-tête (titre + ce que veut le consommateur). **Aucune prop `title`.** |
+| _défaut_ | Le body, dans la zone scrollable. |
+| `#footer` | Rendu **dans la zone scrollable**, juste après le body. **Aucune classe de positionnement imposée.** |
+
+### Props
+
+| Prop | Type | Défaut | Rôle |
+|------|------|--------|------|
+| `modelValue` | `boolean` | `undefined` | v-model d'ouverture (pattern contrôlé/non-contrôlé Malio) |
+| `id` | `string` | `''` (auto) | id du composant |
+| `side` | `'right' \| 'left'` | `'right'` | côté d'apparition |
+| `showClose` | `boolean` | `true` | affiche le bouton de fermeture (croix) |
+| `dismissable` | `boolean` | `true` | clic sur le backdrop ferme le drawer |
+| `closeOnEscape` | `boolean` | `true` | touche Échap ferme le drawer |
+| `ariaLabel` | `string` | `''` | nom accessible de secours quand `#header` est absent |
+| `drawerClass` | `string` | `''` | override du panneau (largeur réglée ici, ex. `max-w-2xl`) |
+| `overlayClass` | `string` | `''` | override du backdrop |
+| `headerClass` | `string` | `''` | override de la barre header |
+| `bodyClass` | `string` | `''` | override de la zone scrollable |
+| `footerClass` | `string` | `''` | override du wrapper du `#footer` |
+
+> **Largeur/hauteur** : pas de prop `size`. Tout se règle via `drawerClass`
+> (comme aujourd'hui).
+
+### Emits
+
+| Event | Payload | Quand |
+|-------|---------|-------|
+| `update:modelValue` | `boolean` | ouverture/fermeture |
+| `close` | — | à la fermeture (pratique pour la logique appelante) |
+
+## Layout
+
+```
+┌─────────────────────────────┐
+│ [slot #header] [ ✕ ] │ ← barre header (rendue si #header OU showClose)
+├─────────────────────────────┤
+│ │
+│ slot par défaut (body) │ ← zone scrollable (flex-1, overflow-y-auto)
+│ [slot #footer] │ ← rendu juste après le body, dans le même scroll,
+│ │ SANS classe de position par défaut
+└─────────────────────────────┘
+```
+
+- La **barre header** n'est rendue que si le slot `#header` est fourni **ou** si
+ `showClose` est vrai. Le bouton croix vit dans cette barre, à droite. L'icône de
+ fermeture est **`mdi:cancel-bold`** (on conserve l'icône actuelle ; c'est le test
+ qui sera adapté).
+- La **zone scrollable** (`flex-1 overflow-y-auto`) contient le slot par défaut
+ puis, si fourni, le wrapper `#footer`.
+- Le **`#footer`** n'a **aucune** classe `sticky`/`flex-shrink-0`/position. Par défaut
+ il scrolle avec le contenu. Pour le coller en bas, le consommateur passe
+ `footer-class="sticky bottom-0 bg-white"`.
+
+## Comportements (les manques actuels corrigés)
+
+1. **Échap** ferme le drawer si `closeOnEscape` (listener `keydown` global, ajouté à
+ l'ouverture, retiré à la fermeture).
+2. **Scroll-lock du body** : `overflow: hidden` sur `document.body` à l'ouverture,
+ restauré à la fermeture.
+3. **Focus-trap** : à l'ouverture, focus sur le premier élément focusable du panneau
+ (ou le panneau lui-même) ; `Tab`/`Shift+Tab` bouclent à l'intérieur du panneau.
+4. **Restitution du focus** : mémoriser `document.activeElement` à l'ouverture, le
+ restaurer à la fermeture.
+5. **ARIA** :
+ - `role="dialog"`, `aria-modal="true"` sur le panneau ;
+ - `aria-labelledby` pointant sur l'id du wrapper `#header` **si** le slot est fourni ;
+ - sinon `aria-label` = prop `ariaLabel` (fallback accessible).
+
+## Transition
+
+- Backdrop : fondu (`opacity`).
+- Panneau : translation selon `side` —
+ - `right` : `translateX(100%)` → `0` ;
+ - `left` : `translateX(-100%)` → `0`.
+- Conserver le pattern actuel `` + `` +
+ `isRendered` (démontage après l'animation de sortie).
+
+## Migration (breaking)
+
+| Avant | Après |
+|-------|-------|
+| `title="Titre"` | `
Titre
` (ou composant de titre Malio) |
+| `contenu` | inchangé (slot par défaut = body) |
+| `drawer-class` | inchangé |
+| `show-close` | inchangé |
+| _(nouveau)_ | `side`, `dismissable`, `closeOnEscape`, `ariaLabel`, slots `#header`/`#footer` |
+
+Les défauts des nouvelles props reproduisent au plus près le comportement actuel
+(`side="right"`, `showClose=true`, `dismissable=true`).
+
+## Tests (Vitest + @vue/test-utils, jsdom)
+
+À couvrir, en plus des tests de rendu/props/emits existants :
+
+- rendu des 3 slots (`#header`, défaut, `#footer`) ;
+- `side` left/right → classes/transition attendues ;
+- `showClose` toggle la croix ; clic croix → ferme + emit ;
+- `dismissable` : clic backdrop ferme / ne ferme pas ;
+- `closeOnEscape` : Échap ferme / ne ferme pas ;
+- scroll-lock : `body` `overflow:hidden` à l'ouverture, restauré à la fermeture ;
+- focus-trap : focus initial dans le panneau ; restitution au déclencheur ;
+- ARIA : `aria-labelledby` quand `#header`, `aria-label` sinon ;
+- pattern contrôlé/non-contrôlé.
+
+## Hors périmètre (YAGNI)
+
+- côtés `top`/`bottom` (sheets) — extensible plus tard via `side` ;
+- prop `size` sémantique — `drawerClass` suffit ;
+- hook `before-close` ;
+- empilement de plusieurs drawers (un seul scroll-lock géré simplement).