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/plans/2026-05-21-refonte-playground.md b/docs/superpowers/plans/2026-05-21-refonte-playground.md
new file mode 100644
index 0000000..9ab86f7
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-21-refonte-playground.md
@@ -0,0 +1,302 @@
+# Refonte du playground — Implementation Plan
+
+> **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:** Remplacer la fausse-SPA du playground (sidebar maison + chargement dynamique dans `index.vue`) par du vrai routage Nuxt fichier + un layout par défaut qui embarque le composant `MalioSidebar` de production.
+
+**Architecture:** Une config de navigation centralisée (`.playground/playground.nav.ts`) alimente un layout par défaut (`.playground/layouts/default.vue`) contenant `` + ``. Les pages de démo existantes sous `.playground/pages/composant/**` deviennent automatiquement des routes et héritent du layout. `index.vue` devient une simple page d'accueil. Le `app/app.vue` du layer (``), hérité via `extends`, applique le layout automatiquement.
+
+**Tech Stack:** Nuxt 4 (layer + playground via `extends`), Vue 3 `
+```
+
+- [ ] **Step 2 : Vérifier le lint du layout**
+
+Run: `npx eslint .playground/layouts/default.vue`
+Expected: aucune erreur bloquante (0 errors).
+
+---
+
+## Task 3 : Réécrire `index.vue` en page d'accueil
+
+**Files:**
+- Modify (réécriture complète): `.playground/pages/index.vue`
+
+- [ ] **Step 1 : Remplacer tout le contenu de `index.vue`**
+
+Remplacer **l'intégralité** du fichier `.playground/pages/index.vue` (supprime la sidebar maison + le chargement dynamique par glob) par :
+
+```vue
+
+
+
+ Playground @malio/layer-ui
+
+
+ Sélectionne un composant dans la barre latérale pour afficher sa page de démonstration.
+