be3d88ed45
## Problème Sur la famille Date editable, le masque maska n'imposait que la *forme* (`##/##/####`). Une valeur structurellement absurde comme `99/99/9999` était donc **saisissable**, puis rejetée *a posteriori* par la validation. Le métier veut que ce soit **impossible à taper**. ## Solution (masque borné + validation en filet) - `composables/maskTemplate.ts` — `buildBoundedMask(template)` : borne le **premier chiffre de chaque champ** (jour `0-3`, mois `0-1`, heure `0-2`, minute `0-5`). Distingue le mois des minutes (même lettre `M`) selon la présence d'heures dans le gabarit, pour ne pas brider la saisie des minutes du DateTime. - `internal/CalendarField.vue` — branche le builder dans `maskaOptions` (remplace le `replace(/[A-Za-z]/g, '#')`). Les impossibilités plus fines (`31/02`, 29/02 non bissextile, hors `min`/`max`) restent captées par la **validation** (`invalidMessage` + `update:valid=false`). ## Tests - `maskTemplate.test.ts` (5) — bornes par champ, structure du masque, non-confusion mois/minutes. - `Date.test.ts` — test `invalidMessage` adapté (`32/13/2026`, typable→invalide) + garde de non-régression : `99/99/9999` ne s'inscrit jamais et n'émet aucune date. - Suite complète : **1004/1004 verte** (DateTime 36 incluse → saisie d'heure intacte). Doc : `COMPONENTS.md` (MalioDate) + `CHANGELOG.md` (Fixed) à jour. Reviewed-on: #79 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
141 lines
3.6 KiB
Vue
141 lines
3.6 KiB
Vue
<template>
|
|
<aside
|
|
:id="componentId"
|
|
:class="twMerge(
|
|
'relative flex h-full flex-col bg-m-bg',
|
|
collapsed ? 'w-[72px]' : 'w-[232px]',
|
|
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 pt-2 pb-2',
|
|
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 ? '' : 'text-black hover:bg-m-primary/10 hover:font-semibold hover:text-m-primary pt-1 pb-1'"
|
|
>
|
|
<NuxtLink
|
|
:to="item.to"
|
|
active-class="!text-m-primary font-semibold"
|
|
:class="twMerge(
|
|
'block truncate text-[15px] leading-[150%]',
|
|
collapsed ? 'px-3 text-center' : 'pl-[32px]',
|
|
)"
|
|
>
|
|
<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-surface',
|
|
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>
|