fix: refonte du composant Drawer (#51)
All checks were successful
Release / release (push) Successful in 1m25s
All checks were successful
Release / release (push) Successful in 1m25s
| 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: matthieu <matthieu@yuno.malio.fr> Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr> Reviewed-on: #51 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #51.
This commit is contained in:
146
docs/superpowers/specs/2026-05-21-drawer-redesign-design.md
Normal file
146
docs/superpowers/specs/2026-05-21-drawer-redesign-design.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# Refonte du composant `<MalioDrawer>` — Design
|
||||
|
||||
> Ticket : MUI-35 — Revoir le design du composant Drawer
|
||||
> Date : 2026-05-21
|
||||
> Statut : design validé, à implémenter
|
||||
|
||||
## Contexte & problème
|
||||
|
||||
Le `<MalioDrawer>` 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** `<MalioDrawer>` (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 `<Teleport to="body">` + `<Transition>` +
|
||||
`isRendered` (démontage après l'animation de sortie).
|
||||
|
||||
## Migration (breaking)
|
||||
|
||||
| Avant | Après |
|
||||
|-------|-------|
|
||||
| `title="Titre"` | `<template #header><h2>Titre</h2></template>` (ou composant de titre Malio) |
|
||||
| `<MalioDrawer>contenu</MalioDrawer>` | 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).
|
||||
124
docs/superpowers/specs/2026-05-21-refonte-playground-design.md
Normal file
124
docs/superpowers/specs/2026-05-21-refonte-playground-design.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Refonte du système de playground
|
||||
|
||||
Date : 2026-05-21
|
||||
Branche : `feature/MUI-34-revoir-le-systeme-de-playground`
|
||||
|
||||
## Contexte
|
||||
|
||||
Le playground actuel (`.playground/`) est une **fausse SPA** : une unique page
|
||||
`index.vue` contient une sidebar codée à la main et charge dynamiquement les
|
||||
pages de démo via `import.meta.glob` + `<component :is>`. Il n'y a ni vrai
|
||||
routage, ni layout, et la sidebar ne réutilise pas le composant `MalioSidebar`
|
||||
de la bibliothèque.
|
||||
|
||||
Les pages de démo existent déjà dans `.playground/pages/composant/<catégorie>/<nom>.vue`
|
||||
mais ne sont pas exploitées comme de vraies routes.
|
||||
|
||||
## Objectif
|
||||
|
||||
Refondre le playground autour du **vrai routage fichier de Nuxt** et d'un
|
||||
**layout par défaut** qui embarque le composant `MalioSidebar` de production
|
||||
(dogfooding du composant).
|
||||
|
||||
## Décisions validées
|
||||
|
||||
| Sujet | Décision |
|
||||
|-------|----------|
|
||||
| Navigation | Vrai routage Nuxt + layout dédié |
|
||||
| Construction de la sidebar | Liste manuelle centralisée |
|
||||
| Habillage du layout | Sidebar + contenu seul (épuré, chaque page gère son titre) |
|
||||
| Page d'accueil | Page de bienvenue simple |
|
||||
| Surbrillance lien actif | Hors périmètre (MalioSidebar inchangé) |
|
||||
|
||||
## Architecture
|
||||
|
||||
### 1. Config de navigation centralisée
|
||||
|
||||
Nouveau fichier `.playground/playground.nav.ts` exportant un tableau
|
||||
`SidebarSection[]` (type exporté par `MalioSidebar`). Les sections sont
|
||||
définies manuellement ; chaque item est un `{ label, to }` pointant vers la
|
||||
route de démo.
|
||||
|
||||
```ts
|
||||
import type { SidebarSection } from '../app/components/malio/sidebar/Sidebar.vue'
|
||||
|
||||
export const navSections: SidebarSection[] = [
|
||||
{
|
||||
label: 'BOUTONS',
|
||||
icon: 'mdi:gesture-tap-button',
|
||||
items: [
|
||||
{ label: 'Button', to: '/composant/button/button' },
|
||||
{ label: 'Button Icon', to: '/composant/button/buttonIcon' },
|
||||
],
|
||||
},
|
||||
// … autres sections (Champs, Sélections, Navigation, Données, Divers)
|
||||
]
|
||||
```
|
||||
|
||||
Les routes correspondent exactement aux fichiers existants dans
|
||||
`.playground/pages/composant/`. Liste à couvrir :
|
||||
|
||||
- **button/** : `button`, `buttonIcon`
|
||||
- **checkbox/** : `checkbox`
|
||||
- **radio/** : `radioButton`
|
||||
- **input/** : `inputText`, `inputNumber`, `inputAmount`, `inputEmail`,
|
||||
`inputPassword`, `inputPhone`, `inputTextArea`, `inputAutocomplete`,
|
||||
`inputUpload`, `inputRichText`
|
||||
- **select/** : `select`, `selectCheckbox`
|
||||
- **time/** : `time`
|
||||
- **tab/** : `tabList`
|
||||
- **sidebar/** : `sidebar`
|
||||
- **drawer/** : `drawer`
|
||||
- **datatable/** : `datatable`
|
||||
- **site/** : `siteSelector`
|
||||
- **form/** : `client`
|
||||
|
||||
Le regroupement en sections et les libellés affichés sont au choix du
|
||||
développeur (manuel). Les routes, elles, sont imposées par les fichiers.
|
||||
|
||||
### 2. Layout par défaut
|
||||
|
||||
Nouveau fichier `.playground/layouts/default.vue` :
|
||||
|
||||
- Conteneur `flex` pleine hauteur (`h-screen`).
|
||||
- `<MalioSidebar :sections="navSections">` à gauche.
|
||||
- Slots `logo` / `logo-collapsed` : logos `LOGO_MALIO.png` /
|
||||
`LOGO_MALIO_COLLAPSED.png` (servis depuis le `public/` du layer),
|
||||
enveloppés dans un `<NuxtLink to="/">` pour revenir à l'accueil.
|
||||
- Collapse géré en interne par le composant (mode non-contrôlé).
|
||||
- `<main class="flex-1 overflow-y-auto p-6"><slot /></main>` à droite.
|
||||
|
||||
Le layout `default` s'applique automatiquement à toutes les pages du
|
||||
playground — aucune page n'a besoin de `definePageMeta({ layout })`.
|
||||
|
||||
### 3. Page d'accueil
|
||||
|
||||
`.playground/pages/index.vue` réécrite en page de bienvenue simple :
|
||||
titre + invitation à choisir un composant dans la sidebar. Toute la logique
|
||||
de glob / chargement dynamique / sidebar maison est supprimée.
|
||||
|
||||
### 4. Pages de démo
|
||||
|
||||
**Inchangées.** Elles sont déjà des routes `/composant/<cat>/<nom>` et
|
||||
hériteront automatiquement du layout `default`.
|
||||
|
||||
### 5. Mise à jour du skill `creating-malio-component`
|
||||
|
||||
Ajouter une étape au skill : lors de la création d'un nouveau composant,
|
||||
ajouter son entrée dans `.playground/playground.nav.ts` pour qu'il apparaisse
|
||||
dans la sidebar.
|
||||
|
||||
## Hors périmètre
|
||||
|
||||
- Surbrillance de l'item actif dans `MalioSidebar` (ticket dédié si besoin).
|
||||
- Toute autre évolution de `MalioSidebar`.
|
||||
- Refonte du contenu des pages de démo existantes.
|
||||
|
||||
## Critères de réussite
|
||||
|
||||
- `npm run dev` lance le playground avec `MalioSidebar` dans un layout.
|
||||
- Cliquer sur un item de la sidebar change l'URL et affiche la bonne démo.
|
||||
- Le logo ramène à l'accueil ; l'accueil affiche le message de bienvenue.
|
||||
- Plus aucune trace de la sidebar maison ni du chargement dynamique dans
|
||||
`index.vue`.
|
||||
- `npm run lint` et `npm run test` passent.
|
||||
Reference in New Issue
Block a user