[#MUI-20] Développer le composant Menu (#17)
| 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: #17 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #17.
This commit is contained in:
131
.playground/pages/composant/sidebar/sidebar.vue
Normal file
131
.playground/pages/composant/sidebar/sidebar.vue
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex gap-8" style="height: calc(100vh - 100px)">
|
||||||
|
<MalioSidebar
|
||||||
|
v-model="collapsed1"
|
||||||
|
:sections="sectionsShort"
|
||||||
|
>
|
||||||
|
<template #logo>
|
||||||
|
<img src="/LOGO_MALIO.png" alt="Malio" />
|
||||||
|
</template>
|
||||||
|
<template #logo-collapsed>
|
||||||
|
<img src="/LOGO_MALIO_COLLAPSED.png" alt="Malio" />
|
||||||
|
</template>
|
||||||
|
</MalioSidebar>
|
||||||
|
|
||||||
|
<MalioSidebar
|
||||||
|
v-model="collapsed2"
|
||||||
|
:sections="sectionsLong"
|
||||||
|
>
|
||||||
|
<template #logo>
|
||||||
|
<img src="/LOGO_MALIO.png" alt="Malio" />
|
||||||
|
</template>
|
||||||
|
<template #logo-collapsed>
|
||||||
|
<img src="/LOGO_MALIO_COLLAPSED.png" alt="Malio" />
|
||||||
|
</template>
|
||||||
|
</MalioSidebar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {ref} from 'vue'
|
||||||
|
|
||||||
|
const collapsed1 = ref(false)
|
||||||
|
const collapsed2 = ref(false)
|
||||||
|
|
||||||
|
const sectionsShort = [
|
||||||
|
{
|
||||||
|
label: 'LOGISTIQUE / TRANSPORT',
|
||||||
|
icon: 'mdi:truck-delivery',
|
||||||
|
items: [
|
||||||
|
{label: 'Réception / Expédition', to: '/reception'},
|
||||||
|
{label: 'Validation expédition', to: '/validation'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'COMMERCIAL',
|
||||||
|
icon: 'mdi:handshake',
|
||||||
|
items: [
|
||||||
|
{label: 'Répertoire fournisseurs', to: '/fournisseurs'},
|
||||||
|
{label: 'Répertoire clients', to: '/clients'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const sectionsLong = [
|
||||||
|
{
|
||||||
|
label: 'LOGISTIQUE / TRANSPORT',
|
||||||
|
icon: 'mdi:truck-delivery',
|
||||||
|
items: [
|
||||||
|
{label: 'Réception / Expédition', to: '/reception'},
|
||||||
|
{label: 'Validation expédition', to: '/validation'},
|
||||||
|
{label: 'Voyage', to: '/voyage'},
|
||||||
|
{label: 'Ticket de pesée', to: '/pesee'},
|
||||||
|
{label: 'Bon de réception', to: '/bon-reception'},
|
||||||
|
{label: "Bon d'expédition", to: '/bon-expedition'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'USINE / PRODUCTION',
|
||||||
|
icon: 'mdi:factory',
|
||||||
|
items: [
|
||||||
|
{label: 'Fabrication en cours', to: '/fabrication'},
|
||||||
|
{label: 'Liste des fabrications', to: '/fabrications'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'COMMERCIAL',
|
||||||
|
icon: 'mdi:handshake',
|
||||||
|
items: [
|
||||||
|
{label: 'Répertoire fournisseurs', to: '/fournisseurs'},
|
||||||
|
{label: 'Compagnie fournisseurs', to: '/compagnie-fournisseurs'},
|
||||||
|
{label: 'Répertoire clients', to: '/clients'},
|
||||||
|
{label: 'Contrats en cours', to: '/contrats'},
|
||||||
|
{label: 'Commissions Clients', to: '/commissions'},
|
||||||
|
{label: 'Attribution expédition', to: '/attribution'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'PRIX',
|
||||||
|
icon: 'mdi:tag',
|
||||||
|
items: [
|
||||||
|
{label: "Prix d'achat/vente", to: '/prix-achat'},
|
||||||
|
{label: "Prix d'achat spécifiques", to: '/prix-specifiques'},
|
||||||
|
{label: 'Prix de ventes clients', to: '/prix-vente'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'FACTURATION',
|
||||||
|
icon: 'mdi:receipt',
|
||||||
|
items: [
|
||||||
|
{label: 'Expéditions à facturer', to: '/expeditions-facturer'},
|
||||||
|
{label: 'Factures', to: '/factures'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'TECHNIQUE',
|
||||||
|
icon: 'mdi:cog',
|
||||||
|
items: [
|
||||||
|
{label: 'Répertoire prestataires', to: '/prestataires'},
|
||||||
|
{label: 'Répertoire transporteurs', to: '/transporteurs'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'SUIVI HEURES',
|
||||||
|
icon: 'mdi:clock-outline',
|
||||||
|
items: [
|
||||||
|
{label: 'Heure Usine', to: '/heure-usine'},
|
||||||
|
{label: 'Heure Extras', to: '/heure-extras'},
|
||||||
|
{label: 'Heure Ferme', to: '/heure-ferme'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'ADMINISTRATION',
|
||||||
|
icon: 'mdi:shield-account',
|
||||||
|
items: [
|
||||||
|
{label: 'Catalogue produits', to: '/catalogue'},
|
||||||
|
{label: 'Éditer étiquettes', to: '/etiquettes'},
|
||||||
|
{label: 'Organisation catégorie', to: '/organisation'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
@@ -19,6 +19,7 @@ Liste des évolutions de la librairie Malio layer UI
|
|||||||
* [#MUI-9] Création d'un composant upload
|
* [#MUI-9] Création d'un composant upload
|
||||||
* [#MUI-14] Création d'un composant bouton icône
|
* [#MUI-14] Création d'un composant bouton icône
|
||||||
* [#MUI-11] Création d'un composant navigation par onglets
|
* [#MUI-11] Création d'un composant navigation par onglets
|
||||||
|
* [#MUI-20] Création d'un composant sidebar
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
205
app/components/malio/sidebar/Sidebar.test.ts
Normal file
205
app/components/malio/sidebar/Sidebar.test.ts
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import {describe, expect, it} from 'vitest'
|
||||||
|
import {mount} from '@vue/test-utils'
|
||||||
|
import type {DefineComponent} from 'vue'
|
||||||
|
import {Icon as IconifyIcon} from '@iconify/vue'
|
||||||
|
import Sidebar from './Sidebar.vue'
|
||||||
|
|
||||||
|
type SidebarItem = {
|
||||||
|
label: string
|
||||||
|
to: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SidebarSection = {
|
||||||
|
label?: string
|
||||||
|
icon?: string
|
||||||
|
items: SidebarItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type SidebarProps = {
|
||||||
|
sections: SidebarSection[]
|
||||||
|
modelValue?: boolean
|
||||||
|
id?: string
|
||||||
|
sidebarClass?: string
|
||||||
|
toggleClass?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarForTest = Sidebar as DefineComponent<SidebarProps>
|
||||||
|
|
||||||
|
const sections: SidebarSection[] = [
|
||||||
|
{
|
||||||
|
label: 'LOGISTIQUE / TRANSPORT',
|
||||||
|
icon: 'mdi:truck-delivery',
|
||||||
|
items: [
|
||||||
|
{label: 'Réception / Expédition', to: '/reception'},
|
||||||
|
{label: 'Validation expédition', to: '/validation'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'COMMERCIAL',
|
||||||
|
icon: 'mdi:handshake',
|
||||||
|
items: [
|
||||||
|
{label: 'Répertoire Fournisseurs', to: '/fournisseurs'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const stubs = {
|
||||||
|
NuxtLink: {
|
||||||
|
template: '<a :href="to" v-bind="$attrs"><slot /></a>',
|
||||||
|
props: ['to'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountComponent(props: SidebarProps, slots?: Record<string, string>) {
|
||||||
|
return mount(SidebarForTest, {
|
||||||
|
props,
|
||||||
|
slots,
|
||||||
|
global: {stubs},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('MalioSidebar', () => {
|
||||||
|
it('renders expanded by default', () => {
|
||||||
|
const wrapper = mountComponent({sections})
|
||||||
|
const aside = wrapper.find('aside')
|
||||||
|
expect(aside.classes()).toContain('w-[280px]')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders section labels with icons when expanded', () => {
|
||||||
|
const wrapper = mountComponent({sections})
|
||||||
|
const sectionHeaders = wrapper.findAll('nav > div > div')
|
||||||
|
expect(sectionHeaders).toHaveLength(2)
|
||||||
|
expect(sectionHeaders[0].text()).toContain('LOGISTIQUE / TRANSPORT')
|
||||||
|
expect(sectionHeaders[1].text()).toContain('COMMERCIAL')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders all menu items with icons and labels', () => {
|
||||||
|
const wrapper = mountComponent({sections})
|
||||||
|
const links = wrapper.findAll('a')
|
||||||
|
expect(links).toHaveLength(3)
|
||||||
|
expect(links[0].text()).toContain('Réception / Expédition')
|
||||||
|
expect(links[1].text()).toContain('Validation expédition')
|
||||||
|
expect(links[2].text()).toContain('Répertoire Fournisseurs')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders NuxtLink with correct to prop', () => {
|
||||||
|
const wrapper = mountComponent({sections})
|
||||||
|
const links = wrapper.findAll('a')
|
||||||
|
expect(links[0].attributes('href')).toBe('/reception')
|
||||||
|
expect(links[2].attributes('href')).toBe('/fournisseurs')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders section icons via IconifyIcon', () => {
|
||||||
|
const wrapper = mountComponent({sections})
|
||||||
|
const icons = wrapper.findAllComponents(IconifyIcon)
|
||||||
|
// 2 section icons + 1 toggle chevron = 3
|
||||||
|
expect(icons).toHaveLength(3)
|
||||||
|
expect(icons[0].props('icon')).toBe('mdi:truck-delivery')
|
||||||
|
expect(icons[1].props('icon')).toBe('mdi:handshake')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggle button shows chevron-left when expanded', () => {
|
||||||
|
const wrapper = mountComponent({sections})
|
||||||
|
const toggleIcon = wrapper.findAllComponents(IconifyIcon).at(-1)!
|
||||||
|
expect(toggleIcon.props('icon')).toBe('mdi:chevron-left')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('collapses on toggle click in uncontrolled mode', async () => {
|
||||||
|
const wrapper = mountComponent({sections})
|
||||||
|
const toggleBtn = wrapper.find('button')
|
||||||
|
|
||||||
|
await toggleBtn.trigger('click')
|
||||||
|
|
||||||
|
const aside = wrapper.find('aside')
|
||||||
|
expect(aside.classes()).toContain('w-[72px]')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hides section label text when collapsed but keeps section icon', async () => {
|
||||||
|
const wrapper = mountComponent({sections})
|
||||||
|
await wrapper.find('button').trigger('click')
|
||||||
|
|
||||||
|
const sectionHeaders = wrapper.findAll('nav > div > div')
|
||||||
|
expect(sectionHeaders).toHaveLength(2)
|
||||||
|
// Label text spans are hidden
|
||||||
|
sectionHeaders.forEach((header) => {
|
||||||
|
expect(header.findAll('span').filter(s => s.classes().includes('text-[11px]'))).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hides item text when collapsed', async () => {
|
||||||
|
const wrapper = mountComponent({sections})
|
||||||
|
await wrapper.find('button').trigger('click')
|
||||||
|
|
||||||
|
const itemSpans = wrapper.findAll('a span')
|
||||||
|
expect(itemSpans).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggle button shows chevron-right when collapsed', async () => {
|
||||||
|
const wrapper = mountComponent({sections})
|
||||||
|
await wrapper.find('button').trigger('click')
|
||||||
|
|
||||||
|
const toggleIcon = wrapper.findAllComponents(IconifyIcon).at(-1)!
|
||||||
|
expect(toggleIcon.props('icon')).toBe('mdi:chevron-right')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits update:modelValue on toggle click', async () => {
|
||||||
|
const wrapper = mountComponent({sections, modelValue: false})
|
||||||
|
await wrapper.find('button').trigger('click')
|
||||||
|
|
||||||
|
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([true])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('respects modelValue in controlled mode', () => {
|
||||||
|
const wrapper = mountComponent({sections, modelValue: true})
|
||||||
|
const aside = wrapper.find('aside')
|
||||||
|
expect(aside.classes()).toContain('w-[72px]')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders logo slot when expanded', () => {
|
||||||
|
const wrapper = mountComponent({sections}, {
|
||||||
|
logo: '<img alt="Malio" src="/logo.svg" />',
|
||||||
|
})
|
||||||
|
expect(wrapper.find('img[alt="Malio"]').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders logo-collapsed slot when collapsed', async () => {
|
||||||
|
const wrapper = mountComponent({sections}, {
|
||||||
|
'logo-collapsed': '<img alt="M" src="/logo-m.svg" />',
|
||||||
|
})
|
||||||
|
await wrapper.find('button').trigger('click')
|
||||||
|
|
||||||
|
expect(wrapper.find('img[alt="M"]').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses custom id when provided', () => {
|
||||||
|
const wrapper = mountComponent({sections, id: 'my-sidebar'})
|
||||||
|
expect(wrapper.find('aside').attributes('id')).toBe('my-sidebar')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggle button has correct aria-label', async () => {
|
||||||
|
const wrapper = mountComponent({sections})
|
||||||
|
const btn = wrapper.find('button')
|
||||||
|
expect(btn.attributes('aria-label')).toBe('Plier le menu')
|
||||||
|
|
||||||
|
await btn.trigger('click')
|
||||||
|
expect(btn.attributes('aria-label')).toBe('Déplier le menu')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('section without label does not render a section header', () => {
|
||||||
|
const noLabelSections: SidebarSection[] = [
|
||||||
|
{items: [{label: 'Item', to: '/'}]},
|
||||||
|
]
|
||||||
|
const wrapper = mountComponent({sections: noLabelSections})
|
||||||
|
expect(wrapper.findAll('nav > div > div')).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders section icon in collapsed mode', async () => {
|
||||||
|
const wrapper = mountComponent({sections})
|
||||||
|
await wrapper.find('button').trigger('click')
|
||||||
|
|
||||||
|
const icons = wrapper.findAllComponents(IconifyIcon)
|
||||||
|
// 2 section icons + 1 toggle = 3
|
||||||
|
expect(icons[0].props('icon')).toBe('mdi:truck-delivery')
|
||||||
|
expect(icons[1].props('icon')).toBe('mdi:handshake')
|
||||||
|
})
|
||||||
|
})
|
||||||
139
app/components/malio/sidebar/Sidebar.vue
Normal file
139
app/components/malio/sidebar/Sidebar.vue
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<template>
|
||||||
|
<aside
|
||||||
|
:id="componentId"
|
||||||
|
:class="twMerge(
|
||||||
|
'relative flex h-full flex-col bg-m-bg',
|
||||||
|
collapsed ? 'w-[72px]' : 'w-[280px]',
|
||||||
|
sidebarClass,
|
||||||
|
)"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<div :class="['px-[20px] py-[14px]', collapsed ? '' : 'mx-[10px] border-b-2 border-m-primary']">
|
||||||
|
<slot
|
||||||
|
v-if="collapsed"
|
||||||
|
name="logo-collapsed"
|
||||||
|
/>
|
||||||
|
<slot
|
||||||
|
v-else
|
||||||
|
name="logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="flex-1 overflow-y-auto mb-4">
|
||||||
|
<div
|
||||||
|
v-for="(section, sectionIndex) in sections"
|
||||||
|
:key="sectionIndex"
|
||||||
|
:class="collapsed ? 'first:border-t-2 first:border-m-primary' : 'mx-[10px] border-t-2 border-m-primary first:border-t-0'"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="section.label"
|
||||||
|
:class="[
|
||||||
|
'flex items-center gap-2 px-[10px] pt-2 pb-3',
|
||||||
|
collapsed ? 'justify-center pt-[40px]' : '',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<IconifyIcon
|
||||||
|
v-if="section.icon"
|
||||||
|
:icon="section.icon"
|
||||||
|
:width="24"
|
||||||
|
class="shrink-0 text-m-primary"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="!collapsed"
|
||||||
|
class="text-[15px] font-bold uppercase text-m-primary"
|
||||||
|
>
|
||||||
|
{{ section.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
<li
|
||||||
|
v-for="item in section.items"
|
||||||
|
:key="item.to"
|
||||||
|
:class="collapsed ? '' : 'pb-2 last:pb-1'"
|
||||||
|
>
|
||||||
|
<NuxtLink
|
||||||
|
:to="item.to"
|
||||||
|
:class="twMerge(
|
||||||
|
'block truncate rounded-md text-[15px] text-m-text text-black transition-colors hover:bg-m-tertiary leading-[150%]',
|
||||||
|
collapsed ? 'px-3 text-center' : 'pl-[42px] pr-3',
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<span v-if="!collapsed">{{ item.label }}</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:aria-label="collapsed ? 'Déplier le menu' : 'Plier le menu'"
|
||||||
|
:class="twMerge(
|
||||||
|
'absolute top-1/2 -translate-y-1/2 right-0 translate-x-1/2 z-10',
|
||||||
|
'flex h-8 w-8 items-center justify-center rounded-full border border-m-border bg-white shadow-sm',
|
||||||
|
'cursor-pointer transition-colors hover:bg-m-tertiary',
|
||||||
|
toggleClass,
|
||||||
|
)"
|
||||||
|
@click="toggleCollapse"
|
||||||
|
>
|
||||||
|
<IconifyIcon
|
||||||
|
:icon="collapsed ? 'mdi:chevron-right' : 'mdi:chevron-left'"
|
||||||
|
:width="18"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</aside>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, ref, useId} from 'vue'
|
||||||
|
import {Icon as IconifyIcon} from '@iconify/vue'
|
||||||
|
import {twMerge} from 'tailwind-merge'
|
||||||
|
|
||||||
|
defineOptions({name: 'MalioSidebar', inheritAttrs: false})
|
||||||
|
|
||||||
|
export type SidebarItem = {
|
||||||
|
label: string
|
||||||
|
to: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SidebarSection = {
|
||||||
|
label?: string
|
||||||
|
icon?: string
|
||||||
|
items: SidebarItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
sections: SidebarSection[]
|
||||||
|
modelValue?: boolean
|
||||||
|
id?: string
|
||||||
|
sidebarClass?: string
|
||||||
|
toggleClass?: string
|
||||||
|
}>(), {
|
||||||
|
modelValue: undefined,
|
||||||
|
id: '',
|
||||||
|
sidebarClass: '',
|
||||||
|
toggleClass: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const generatedId = useId()
|
||||||
|
const componentId = computed(() => props.id || `malio-sidebar-${generatedId}`)
|
||||||
|
|
||||||
|
const isControlled = computed(() => props.modelValue !== undefined)
|
||||||
|
const localValue = ref(false)
|
||||||
|
|
||||||
|
const collapsed = computed(() =>
|
||||||
|
isControlled.value ? props.modelValue! : localValue.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
function toggleCollapse() {
|
||||||
|
const newValue = !collapsed.value
|
||||||
|
if (!isControlled.value) {
|
||||||
|
localValue.value = newValue
|
||||||
|
}
|
||||||
|
emit('update:modelValue', newValue)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
227
app/story/sidebar/sidebarMenu.story.vue
Normal file
227
app/story/sidebar/sidebarMenu.story.vue
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
<template>
|
||||||
|
<Story title="Navigation/Sidebar">
|
||||||
|
<Variant title="Peu de liens">
|
||||||
|
<div class="flex h-[600px] border rounded-lg overflow-hidden">
|
||||||
|
<MalioSidebar
|
||||||
|
v-model="collapsed1"
|
||||||
|
:sections="sectionsShort"
|
||||||
|
>
|
||||||
|
<template #logo>
|
||||||
|
<span class="text-2xl font-bold text-m-primary">Malio</span>
|
||||||
|
</template>
|
||||||
|
<template #logo-collapsed>
|
||||||
|
<span class="text-2xl font-bold text-m-primary">M</span>
|
||||||
|
</template>
|
||||||
|
</MalioSidebar>
|
||||||
|
|
||||||
|
<div class="flex-1 p-6 bg-white">
|
||||||
|
<p class="text-m-muted">
|
||||||
|
Sidebar avec peu de liens, pas de scroll.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="Beaucoup de liens (scroll)">
|
||||||
|
<div class="flex h-[600px] border rounded-lg overflow-hidden">
|
||||||
|
<MalioSidebar
|
||||||
|
v-model="collapsed2"
|
||||||
|
:sections="sectionsLong"
|
||||||
|
>
|
||||||
|
<template #logo>
|
||||||
|
<span class="text-2xl font-bold text-m-primary">Malio</span>
|
||||||
|
</template>
|
||||||
|
<template #logo-collapsed>
|
||||||
|
<span class="text-2xl font-bold text-m-primary">M</span>
|
||||||
|
</template>
|
||||||
|
</MalioSidebar>
|
||||||
|
|
||||||
|
<div class="flex-1 p-6 bg-white">
|
||||||
|
<p class="text-m-muted">
|
||||||
|
Sidebar avec beaucoup de liens, scroll visible.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<docs lang="md">
|
||||||
|
# MalioSidebar
|
||||||
|
|
||||||
|
Composant de navigation latérale avec support déplié/plié, sections groupées,
|
||||||
|
icônes et liens NuxtLink. Un bouton circulaire avec chevron permet de toggle
|
||||||
|
entre les deux états.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
## Props détaillées
|
||||||
|
|
||||||
|
### sections
|
||||||
|
|
||||||
|
- Type: `Array<{ label?: string; items: Array<{ label: string; icon: string; to: string }> }>`
|
||||||
|
- Description: Liste des sections du menu. Chaque section a un label optionnel et une liste d'items.
|
||||||
|
|
||||||
|
### modelValue
|
||||||
|
|
||||||
|
- Type: `boolean`
|
||||||
|
- Description: Contrôle l'état plié/déplié (`true` = plié). Supporte `v-model`.
|
||||||
|
|
||||||
|
### id
|
||||||
|
|
||||||
|
- Type: `string`
|
||||||
|
- Description: ID custom pour le composant.
|
||||||
|
|
||||||
|
### sidebarClass
|
||||||
|
|
||||||
|
- Type: `string`
|
||||||
|
- Description: Classes Tailwind additionnelles pour le conteneur sidebar.
|
||||||
|
|
||||||
|
### toggleClass
|
||||||
|
|
||||||
|
- Type: `string`
|
||||||
|
- Description: Classes Tailwind additionnelles pour le bouton toggle.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
## Slots
|
||||||
|
|
||||||
|
### logo
|
||||||
|
|
||||||
|
- Contenu affiché en haut quand la sidebar est dépliée.
|
||||||
|
|
||||||
|
### logo-collapsed
|
||||||
|
|
||||||
|
- Contenu affiché en haut quand la sidebar est pliée.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
## Comportement
|
||||||
|
|
||||||
|
- **Déplié** : affiche le logo, les labels de section et les items avec texte + icône.
|
||||||
|
- **Plié** : affiche le logo réduit et les icônes seules.
|
||||||
|
- **Toggle** : bouton circulaire positionné au centre du bord droit, chevron gauche/droite.
|
||||||
|
- **Contrôlé / non-contrôlé** : fonctionne avec ou sans `v-model`.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
## Accessibilité
|
||||||
|
|
||||||
|
- `aria-label` sur le bouton toggle ("Plier le menu" / "Déplier le menu").
|
||||||
|
- Navigation sémantique avec `<nav>` et `<ul>/<li>`.
|
||||||
|
- Liens via `NuxtLink` pour le routing côté client.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
### update:modelValue
|
||||||
|
|
||||||
|
- Émis à chaque toggle.
|
||||||
|
- Retourne `true` (plié) ou `false` (déplié).
|
||||||
|
</docs>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {ref} from 'vue'
|
||||||
|
import MalioSidebar from '../../components/malio/sidebar/Sidebar.vue'
|
||||||
|
|
||||||
|
const collapsed1 = ref(false)
|
||||||
|
const collapsed2 = ref(false)
|
||||||
|
|
||||||
|
const sectionsShort = [
|
||||||
|
{
|
||||||
|
label: 'LOGISTIQUE / TRANSPORT',
|
||||||
|
icon: 'mdi:truck-delivery',
|
||||||
|
items: [
|
||||||
|
{label: 'Réception / Expédition', to: '/reception'},
|
||||||
|
{label: 'Validation expédition', to: '/validation'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'COMMERCIAL',
|
||||||
|
icon: 'mdi:handshake',
|
||||||
|
items: [
|
||||||
|
{label: 'Répertoire fournisseurs', to: '/fournisseurs'},
|
||||||
|
{label: 'Répertoire clients', to: '/clients'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const sectionsLong = [
|
||||||
|
{
|
||||||
|
label: 'LOGISTIQUE / TRANSPORT',
|
||||||
|
icon: 'mdi:truck-delivery',
|
||||||
|
items: [
|
||||||
|
{label: 'Réception / Expédition', to: '/reception'},
|
||||||
|
{label: 'Validation expédition', to: '/validation'},
|
||||||
|
{label: 'Voyage', to: '/voyage'},
|
||||||
|
{label: 'Ticket de pesée', to: '/pesee'},
|
||||||
|
{label: 'Bon de réception', to: '/bon-reception'},
|
||||||
|
{label: "Bon d'expédition", to: '/bon-expedition'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'USINE / PRODUCTION',
|
||||||
|
icon: 'mdi:factory',
|
||||||
|
items: [
|
||||||
|
{label: 'Fabrication en cours', to: '/fabrication'},
|
||||||
|
{label: 'Liste des fabrications', to: '/fabrications'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'COMMERCIAL',
|
||||||
|
icon: 'mdi:handshake',
|
||||||
|
items: [
|
||||||
|
{label: 'Répertoire fournisseurs', to: '/fournisseurs'},
|
||||||
|
{label: 'Compagnie fournisseurs', to: '/compagnie-fournisseurs'},
|
||||||
|
{label: 'Répertoire clients', to: '/clients'},
|
||||||
|
{label: 'Contrats en cours', to: '/contrats'},
|
||||||
|
{label: 'Commissions Clients', to: '/commissions'},
|
||||||
|
{label: 'Attribution expédition', to: '/attribution'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'PRIX',
|
||||||
|
icon: 'mdi:tag',
|
||||||
|
items: [
|
||||||
|
{label: "Prix d'achat/vente", to: '/prix-achat'},
|
||||||
|
{label: "Prix d'achat spécifiques", to: '/prix-specifiques'},
|
||||||
|
{label: 'Prix de ventes clients', to: '/prix-vente'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'FACTURATION',
|
||||||
|
icon: 'mdi:receipt',
|
||||||
|
items: [
|
||||||
|
{label: 'Expéditions à facturer', to: '/expeditions-facturer'},
|
||||||
|
{label: 'Factures', to: '/factures'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'TECHNIQUE',
|
||||||
|
icon: 'mdi:cog',
|
||||||
|
items: [
|
||||||
|
{label: 'Répertoire prestataires', to: '/prestataires'},
|
||||||
|
{label: 'Répertoire transporteurs', to: '/transporteurs'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'SUIVI HEURES',
|
||||||
|
icon: 'mdi:clock-outline',
|
||||||
|
items: [
|
||||||
|
{label: 'Heure Usine', to: '/heure-usine'},
|
||||||
|
{label: 'Heure Extras', to: '/heure-extras'},
|
||||||
|
{label: 'Heure Ferme', to: '/heure-ferme'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'ADMINISTRATION',
|
||||||
|
icon: 'mdi:shield-account',
|
||||||
|
items: [
|
||||||
|
{label: 'Catalogue produits', to: '/catalogue'},
|
||||||
|
{label: 'Éditer étiquettes', to: '/etiquettes'},
|
||||||
|
{label: 'Organisation catégorie', to: '/organisation'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
BIN
public/LOGO_MALIO.png
Normal file
BIN
public/LOGO_MALIO.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/LOGO_MALIO_COLLAPSED.png
Normal file
BIN
public/LOGO_MALIO_COLLAPSED.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
Reference in New Issue
Block a user