refactor : refonte du playground avec routage Nuxt et MalioSidebar (#MUI-34)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
24
.playground/layouts/default.vue
Normal file
24
.playground/layouts/default.vue
Normal 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>
|
||||||
@@ -1,189 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex min-h-screen">
|
<div class="mx-auto max-w-2xl py-16 text-center">
|
||||||
<aside class="w-72 bg-m-bg p-6 text-white">
|
<h1 class="text-3xl font-bold text-m-text">
|
||||||
<button
|
Playground @malio/layer-ui
|
||||||
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' : ''"
|
|
||||||
>
|
|
||||||
▶
|
|
||||||
</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>
|
</h1>
|
||||||
<p class="mt-2 text-gray-600">
|
<p class="mt-4 text-m-muted">
|
||||||
Selectionne un composant dans la liste pour afficher sa page de demo.
|
Sélectionne un composant dans la barre latérale pour afficher sa page de démonstration.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</template>
|
</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>
|
|
||||||
|
|||||||
63
.playground/playground.nav.ts
Normal file
63
.playground/playground.nav.ts
Normal 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'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user