[#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'},
],
},
]