[#MUI-34] Revoir le système de playground (#48)

| 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é

Reviewed-on: #48
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #48.
This commit is contained in:
2026-05-21 08:30:23 +00:00
committed by Autin
parent ac06ed9ae6
commit e2dabb0a26
6 changed files with 521 additions and 186 deletions

View File

@@ -0,0 +1,24 @@
<template>
<div class="flex h-screen">
<MalioSidebar :sections="navSections">
<template #logo>
<NuxtLink to="/">
<img src="/LOGO_MALIO.png" alt="Malio">
</NuxtLink>
</template>
<template #logo-collapsed>
<NuxtLink to="/">
<img src="/LOGO_MALIO_COLLAPSED.png" alt="Malio">
</NuxtLink>
</template>
</MalioSidebar>
<main class="flex-1 overflow-y-auto p-6">
<slot />
</main>
</div>
</template>
<script setup lang="ts">
import {navSections} from '../playground.nav'
</script>

View File

@@ -1,189 +1,10 @@
<template>
<div class="flex min-h-screen">
<aside class="w-72 bg-m-bg p-6 text-white">
<button
type="button"
class="text-xl text-black font-semibold"
@click="clearSelection"
>
Liste des composants
</button>
<nav class="mt-6 flex flex-col gap-1">
<div
v-for="group in groups"
:key="group.category"
>
<button
type="button"
class="flex w-full items-center justify-between rounded px-3 py-2 text-left text-black font-bold hover:bg-m-primary/10"
@click="toggleCategory(group.category)"
>
{{ group.category }}
<span
class="text-xs transition-transform duration-200"
:class="openCategories.has(group.category) ? 'rotate-90' : ''"
>
&#9654;
</span>
</button>
<div
v-if="openCategories.has(group.category)"
class="ml-3 flex flex-col gap-1 border-l border-gray-300 pl-2"
>
<button
v-for="item in group.items"
:key="item.name"
type="button"
class="rounded px-3 py-1.5 text-left text-sm text-black hover:bg-m-primary hover:text-white"
:class="selectedName === item.name ? 'bg-m-primary/50 text-white' : ''"
@click="selectItem(item.name)"
>
{{ item.label }}
</button>
</div>
</div>
</nav>
</aside>
<main class="flex-1 p-6">
<component
:is="selectedDemoComponent"
v-if="selectedDemoComponent"
/>
<p
v-else-if="selectedName"
class="text-gray-700"
>
Page de demo introuvable:
<code>.playground/pages/composant/{{ selectedDemoFileName }}.vue</code>
</p>
<div v-else>
<h1 class="text-2xl font-semibold text-gray-900">
Playground composants
</h1>
<p class="mt-2 text-gray-600">
Selectionne un composant dans la liste pour afficher sa page de demo.
</p>
</div>
</main>
<div class="mx-auto max-w-2xl py-16 text-center">
<h1 class="text-3xl font-bold text-m-text">
Playground @malio/layer-ui
</h1>
<p class="mt-4 text-m-muted">
Sélectionne un composant dans la barre latérale pour afficher sa page de démonstration.
</p>
</div>
</template>
<script setup lang="ts">
import {computed, reactive, ref, watch, shallowRef} from 'vue'
type LoadedModule = {
default: unknown
}
type Item = {
name: string
label: string
}
type Group = {
category: string
items: Item[]
}
const componentModules = import.meta.glob('../../app/components/malio/**/*.vue')
const demoModules = import.meta.glob('./composant/**/*.vue')
const demoByName: Record<string, () => Promise<LoadedModule>> =
Object.fromEntries(
Object.entries(demoModules).map(([file, loader]) => {
const name = file.split('/').pop()?.replace('.vue', '') ?? ''
return [name.toLowerCase(), loader as () => Promise<LoadedModule>]
}),
)
const groups = computed<Group[]>(() => {
const categoryMap = new Map<string, Item[]>()
Object.keys(componentModules).forEach((file) => {
const parts = file.split('/')
const name = parts.pop()?.replace('.vue', '') ?? ''
const category = parts.pop() ?? ''
if (!categoryMap.has(category)) {
categoryMap.set(category, [])
}
categoryMap.get(category)!.push({name, label: name})
})
const componentGroups = Array.from(categoryMap.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([category, items]) => ({
category: category.charAt(0).toUpperCase() + category.slice(1),
items: items.sort((a, b) => a.label.localeCompare(b.label)),
}))
return [
...componentGroups,
{
category: 'Form',
items: [{name: 'client', label: 'Client'}],
},
]
})
const openCategories = reactive(new Set<string>())
const selectedName = ref('')
const hasInitializedSelection = ref(false)
watch(
groups,
(val) => {
if (!hasInitializedSelection.value && val.length > 0) {
openCategories.add(val[0].category)
if (val[0].items.length > 0) {
selectedName.value = val[0].items[0].name
}
hasInitializedSelection.value = true
}
},
{immediate: true},
)
function toggleCategory(category: string) {
if (openCategories.has(category)) {
openCategories.delete(category)
} else {
openCategories.add(category)
}
}
function selectItem(name: string) {
selectedName.value = selectedName.value === name ? '' : name
}
function clearSelection() {
selectedName.value = ''
}
const selectedDemoComponent = shallowRef<unknown>(null)
watch(selectedName, async (name) => {
if (!name) {
selectedDemoComponent.value = null
return
}
const loader = demoByName[name.toLowerCase()]
if (!loader) {
selectedDemoComponent.value = null
return
}
const mod = await loader()
selectedDemoComponent.value = mod.default
})
const selectedDemoFileName = computed(() => {
const name = selectedName.value
if (!name) return ''
return name.charAt(0).toLowerCase() + name.slice(1)
})
</script>

View File

@@ -0,0 +1,63 @@
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'},
],
},
{
label: 'CHAMPS',
icon: 'mdi:form-textbox',
items: [
{label: 'Texte', to: '/composant/input/inputText'},
{label: 'Nombre', to: '/composant/input/inputNumber'},
{label: 'Montant', to: '/composant/input/inputAmount'},
{label: 'Email', to: '/composant/input/inputEmail'},
{label: 'Mot de passe', to: '/composant/input/inputPassword'},
{label: 'Téléphone', to: '/composant/input/inputPhone'},
{label: 'Zone de texte', to: '/composant/input/inputTextArea'},
{label: 'Saisie assistée', to: '/composant/input/inputAutocomplete'},
{label: 'Upload', to: '/composant/input/inputUpload'},
{label: 'Éditeur riche', to: '/composant/input/inputRichText'},
],
},
{
label: 'SÉLECTIONS',
icon: 'mdi:form-dropdown',
items: [
{label: 'Select', to: '/composant/select/select'},
{label: 'Select Checkbox', to: '/composant/select/selectCheckbox'},
{label: 'Checkbox', to: '/composant/checkbox/checkbox'},
{label: 'Radio', to: '/composant/radio/radioButton'},
],
},
{
label: 'NAVIGATION',
icon: 'mdi:navigation-variant',
items: [
{label: 'Sidebar', to: '/composant/sidebar/sidebar'},
{label: 'Drawer', to: '/composant/drawer/drawer'},
{label: 'Onglets', to: '/composant/tab/tabList'},
],
},
{
label: 'DONNÉES',
icon: 'mdi:table',
items: [
{label: 'DataTable', to: '/composant/datatable/datatable'},
],
},
{
label: 'DIVERS',
icon: 'mdi:dots-horizontal',
items: [
{label: 'Heure', to: '/composant/time/time'},
{label: 'Sélecteur de site', to: '/composant/site/siteSelector'},
{label: 'Formulaire client', to: '/composant/form/client'},
],
},
]

View File

@@ -30,6 +30,7 @@ Liste des évolutions de la librairie Malio layer UI
* [#MUI-30] Création d'un composant email
* [#MUI-31] Création d'un composant téléphone
* [#MUI-32] Création d'un composant saisie assistée (autocomplete)
* [#MUI-34] Revoir le système de playground
### Changed

View File

@@ -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 `<MalioSidebar>` + `<slot />`. 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 (`<NuxtLayout><NuxtPage /></NuxtLayout>`), hérité via `extends`, applique le layout automatiquement.
**Tech Stack:** Nuxt 4 (layer + playground via `extends`), Vue 3 `<script setup>`, Tailwind (tokens `m-*`), composant `MalioSidebar` (auto-importé).
**Note sur les tests :** Le playground est un harnais de dev, non livré. Vitest est scopé à `app/**/*.test.ts` (la bibliothèque) et aucune page playground n'a de test. Cette refonte n'introduit donc pas de tests unitaires : les portes de vérification sont `npm run dev:prepare` (compilation/types), `npm run lint`, et un contrôle manuel via `npm run dev`.
**Convention de commit (projet) :** Conventional Commits **avec espace avant les deux-points**, type en minuscules, pas de préfixe `[#...]`, suffixe ticket `(#MUI-34)`. Terminer par le trailer `Co-Authored-By`. Le hook pre-commit lance toute la suite et **time out de façon flaky** sous WSL2 : réessayer, puis après 2 échecs flaky committer avec `--no-verify`.
---
## File Structure
| Fichier | Rôle | Action |
|---------|------|--------|
| `.playground/playground.nav.ts` | Source unique des sections/liens de la sidebar (typé `SidebarSection[]`) | Créer |
| `.playground/layouts/default.vue` | Layout par défaut : `MalioSidebar` + zone de contenu `<slot />` | Créer |
| `.playground/pages/index.vue` | Page d'accueil simple (remplace la fausse-SPA) | Réécrire |
| `.claude/skills/creating-malio-component/SKILL.md` | Doc process création de composant | Modifier (étape playground + Common Mistakes) |
| `.playground/pages/composant/**/*.vue` | Pages de démo | **Inchangées** (déjà des routes) |
---
## Task 1 : Config de navigation centralisée
**Files:**
- Create: `.playground/playground.nav.ts`
- [ ] **Step 1 : Créer le fichier de navigation**
Créer `.playground/playground.nav.ts` avec ce contenu exact. Chaque `to` correspond exactement à un fichier existant sous `.playground/pages/composant/`. Le type est importé du SFC `MalioSidebar`.
```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'},
],
},
{
label: 'CHAMPS',
icon: 'mdi:form-textbox',
items: [
{label: 'Texte', to: '/composant/input/inputText'},
{label: 'Nombre', to: '/composant/input/inputNumber'},
{label: 'Montant', to: '/composant/input/inputAmount'},
{label: 'Email', to: '/composant/input/inputEmail'},
{label: 'Mot de passe', to: '/composant/input/inputPassword'},
{label: 'Téléphone', to: '/composant/input/inputPhone'},
{label: 'Zone de texte', to: '/composant/input/inputTextArea'},
{label: 'Saisie assistée', to: '/composant/input/inputAutocomplete'},
{label: 'Upload', to: '/composant/input/inputUpload'},
{label: 'Éditeur riche', to: '/composant/input/inputRichText'},
],
},
{
label: 'SÉLECTIONS',
icon: 'mdi:form-dropdown',
items: [
{label: 'Select', to: '/composant/select/select'},
{label: 'Select Checkbox', to: '/composant/select/selectCheckbox'},
{label: 'Checkbox', to: '/composant/checkbox/checkbox'},
{label: 'Radio', to: '/composant/radio/radioButton'},
],
},
{
label: 'NAVIGATION',
icon: 'mdi:navigation-variant',
items: [
{label: 'Sidebar', to: '/composant/sidebar/sidebar'},
{label: 'Drawer', to: '/composant/drawer/drawer'},
{label: 'Onglets', to: '/composant/tab/tabList'},
],
},
{
label: 'DONNÉES',
icon: 'mdi:table',
items: [
{label: 'DataTable', to: '/composant/datatable/datatable'},
],
},
{
label: 'DIVERS',
icon: 'mdi:dots-horizontal',
items: [
{label: 'Heure', to: '/composant/time/time'},
{label: 'Sélecteur de site', to: '/composant/site/siteSelector'},
{label: 'Formulaire client', to: '/composant/form/client'},
],
},
]
```
- [ ] **Step 2 : Vérifier le lint du fichier**
Run: `npx eslint .playground/playground.nav.ts`
Expected: aucune erreur (0 problems). Si ESLint signale un import de type non résolu depuis le `.vue`, c'est un faux positif de résolution ; il ne bloque pas (warnings only). En cas d'**erreur** bloquante sur l'import du type, fallback : remplacer la ligne d'import par une définition locale équivalente :
```ts
type SidebarItem = {label: string; to: string}
type SidebarSection = {label?: string; icon?: string; items: SidebarItem[]}
```
*(Pas de commit ici — les 3 fichiers de la refonte seront committés ensemble en Task 4, car retirer l'ancien `index.vue` casse temporairement le glob.)*
---
## Task 2 : Layout par défaut
**Files:**
- Create: `.playground/layouts/default.vue`
**Pré-requis vérifiés :** `MalioSidebar` est auto-importé (préfixe `Malio`, `pathPrefix: false`). Ses slots sont `logo` et `logo-collapsed`. Sa prop requise est `sections: SidebarSection[]`. Les logos `LOGO_MALIO.png` / `LOGO_MALIO_COLLAPSED.png` sont servis depuis le `public/` du layer (donc accessibles à la racine `/`).
- [ ] **Step 1 : Créer le layout**
Créer `.playground/layouts/default.vue`. Noter : balises `<img>` **sans** auto-fermeture (sinon warning ESLint `vue/html-self-closing`).
```vue
<template>
<div class="flex h-screen">
<MalioSidebar :sections="navSections">
<template #logo>
<NuxtLink to="/">
<img src="/LOGO_MALIO.png" alt="Malio">
</NuxtLink>
</template>
<template #logo-collapsed>
<NuxtLink to="/">
<img src="/LOGO_MALIO_COLLAPSED.png" alt="Malio">
</NuxtLink>
</template>
</MalioSidebar>
<main class="flex-1 overflow-y-auto p-6">
<slot />
</main>
</div>
</template>
<script setup lang="ts">
import {navSections} from '../playground.nav'
</script>
```
- [ ] **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
<template>
<div class="mx-auto max-w-2xl py-16 text-center">
<h1 class="text-3xl font-bold text-m-text">
Playground @malio/layer-ui
</h1>
<p class="mt-4 text-m-muted">
Sélectionne un composant dans la barre latérale pour afficher sa page de démonstration.
</p>
</div>
</template>
```
*(Page sans `<script>` : contenu purement statique. Elle hérite du layout `default` automatiquement.)*
- [ ] **Step 2 : Vérifier le lint de la page**
Run: `npx eslint .playground/pages/index.vue`
Expected: aucune erreur bloquante (0 errors).
---
## Task 4 : Vérification end-to-end + commit de la refonte
**Files:** (commit groupé)
- `.playground/playground.nav.ts`
- `.playground/layouts/default.vue`
- `.playground/pages/index.vue`
- [ ] **Step 1 : Régénérer les types Nuxt (compilation)**
Run: `npm run dev:prepare`
Expected: « Types generated in .playground/.nuxt. » sans erreur de compilation. Valide que le layout, le nav et `index.vue` compilent et que l'import du type `SidebarSection` se résout.
- [ ] **Step 2 : Lint global**
Run: `npm run lint`
Expected: 0 errors (des warnings préexistants sur d'autres fichiers sont tolérés ; aucun nouvel **error** sur les 3 fichiers créés/modifiés).
- [ ] **Step 3 : Contrôle manuel dans le navigateur**
Run: `npm run dev` puis ouvrir l'URL affichée.
Vérifier :
- L'accueil (`/`) affiche le message de bienvenue, avec la `MalioSidebar` à gauche.
- La sidebar liste les 6 sections et tous les liens.
- Cliquer un item (ex. « Texte ») change l'URL en `/composant/input/inputText` et affiche la démo correspondante dans la zone de contenu.
- Le bouton collapse de la sidebar fonctionne (plier/déplier).
- Cliquer le logo ramène à `/`.
Arrêter le serveur (Ctrl+C) une fois vérifié.
- [ ] **Step 4 : Commit de la refonte**
```bash
git add .playground/playground.nav.ts .playground/layouts/default.vue .playground/pages/index.vue
git commit -m "refactor : refonte du playground avec routage Nuxt et MalioSidebar (#MUI-34)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```
Si le hook pre-commit échoue en timeout flaky 2 fois de suite (échecs non reproductibles sur des tests triviaux), recommencer avec `--no-verify` (les fichiers modifiés ne sont pas testés par Vitest, scopé à `app/`).
---
## Task 5 : Mettre à jour le skill `creating-malio-component`
Le skill décrit encore l'ancien fonctionnement (auto-découverte par `index.vue` via glob). Il faut documenter l'ajout dans la nav centralisée et corriger le chemin de la page playground (qui est sous un sous-dossier de catégorie).
**Files:**
- Modify: `.claude/skills/creating-malio-component/SKILL.md`
- [ ] **Step 1 : Réécrire l'étape 5 (page playground)**
Remplacer le bloc de l'étape « ### 5. Créer la page playground » — du titre jusqu'à la ligne `**Variantes typiques :**` exclue — par :
```markdown
### 5. Créer la page playground
**Fichier :** `.playground/pages/composant/<categorie>/<nomComposant>.vue` (camelCase, dans le sous-dossier de catégorie)
La page devient automatiquement une route Nuxt (`/composant/<categorie>/<nomComposant>`) et hérite du layout `default` (qui affiche la `MalioSidebar`). **Ajouter ensuite le lien dans la nav centralisée** `.playground/playground.nav.ts` : insérer un `{label, to}` dans la section appropriée (ou créer une nouvelle section), où `to` = `/composant/<categorie>/<nomComposant>`.
Inclure des variantes représentatives dans une grille :
\`\`\`html
<div class="grid grid-cols-1 items-start gap-6 md:grid-cols-2">
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Titre variante</h2>
<MalioMonComposant ... />
</div>
</div>
\`\`\`
```
- [ ] **Step 2 : Mettre à jour la table « Common Mistakes »**
Remplacer la ligne :
```markdown
| Page playground non détectée | Vérifier le nom du fichier en camelCase dans `.playground/pages/composant/` |
```
par :
```markdown
| Composant absent de la sidebar du playground | Ajouter son entrée `{label, to}` dans `.playground/playground.nav.ts` (la page n'est plus auto-découverte) |
```
- [ ] **Step 3 : Vérifier la cohérence du diagramme workflow**
Lire le bloc `digraph` en tête du skill. L'étape « 5. Créer la page playground » reste valable telle quelle (le titre n'a pas changé). Aucune modification du diagramme nécessaire — confirmer visuellement puis passer à l'étape suivante.
- [ ] **Step 4 : Commit de la mise à jour du skill**
```bash
git add .claude/skills/creating-malio-component/SKILL.md
git commit -m "docs : maj skill creating-malio-component pour la nav playground (#MUI-34)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```
(Ce fichier n'est pas concerné par le hook de tests ; en cas de timeout flaky, `--no-verify`.)
---
## Vérification finale (après toutes les tâches)
- [ ] `npm run lint` → 0 errors.
- [ ] `npm run dev` → accueil + navigation entre composants OK, logo → accueil, collapse OK.
- [ ] `git log --oneline -3` → 2 nouveaux commits au format `type : … (#MUI-34)`.
- [ ] Plus aucune trace de sidebar maison / `import.meta.glob` dans `.playground/pages/index.vue`.
## Note post-exécution (pour l'agent)
Mettre à jour la mémoire `malio-datepicker-conventions.md` : la note « Playground : pages auto-découvertes par glob ; pas d'édition d'`index.vue` » est désormais fausse. Nouvelle réalité : routage Nuxt fichier + layout `default` + nav centralisée dans `.playground/playground.nav.ts` à éditer pour chaque nouveau composant.

View 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.