feat : ajustements visuels du datepicker selon maquette (#MUI-33)
- nouveau token couleur m-primary-light (#EFEFFD) - popover en largeur du champ, shadow au lieu de bordure, collé au champ - frames semaine (35x45) et jours alignés à 45px, cercle centré, font-medium - colonne semaine étroite + marge, numéros en black/60 (semaine courante en black) - vue mois en toutes lettres sur 3 colonnes, blocs 45px - label bleu et grossissement calibré du champ à l'ouverture - header sans hover, chevrons et titre plaqués en haut Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,41 +1,55 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="max-w-md space-y-6">
|
<div class="space-y-6 p-4">
|
||||||
<h1 class="text-2xl font-bold">MalioDate</h1>
|
<h1 class="text-2xl font-bold">MalioDate</h1>
|
||||||
|
|
||||||
<MalioDate
|
<div class="flex flex-wrap items-start gap-10">
|
||||||
v-model="value"
|
<div class="w-[480px] space-y-3">
|
||||||
label="Date de naissance"
|
<h2 class="font-semibold">Large (480px)</h2>
|
||||||
hint="Clique pour ouvrir le calendrier"
|
<MalioDate
|
||||||
/>
|
v-model="value"
|
||||||
|
label="Date de naissance"
|
||||||
|
hint="Clique pour ouvrir le calendrier"
|
||||||
|
/>
|
||||||
|
<div class="rounded border p-3 text-sm">
|
||||||
|
<p>Valeur (ISO) : <code>{{ value ?? 'null' }}</code></p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded bg-m-primary px-3 py-1.5 text-white"
|
||||||
|
@click="value = '2026-12-25'"
|
||||||
|
>
|
||||||
|
Forcer le 25/12/2026
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded border px-3 py-1.5"
|
||||||
|
@click="value = null"
|
||||||
|
>
|
||||||
|
Réinitialiser
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="rounded border p-3 text-sm">
|
<div class="w-[396px] space-y-3">
|
||||||
<p>Valeur (ISO) : <code>{{ value ?? 'null' }}</code></p>
|
<h2 class="font-semibold">ERP (396px)</h2>
|
||||||
|
<MalioDate
|
||||||
|
v-model="erpValue"
|
||||||
|
label="Date du rendez-vous"
|
||||||
|
hint="Largeur cible ERP"
|
||||||
|
/>
|
||||||
|
<div class="rounded border p-3 text-sm">
|
||||||
|
<p>Valeur (ISO) : <code>{{ erpValue ?? 'null' }}</code></p>
|
||||||
|
</div>
|
||||||
|
<MalioDate
|
||||||
|
v-model="bounded"
|
||||||
|
label="Date bornée"
|
||||||
|
:min="todayIso"
|
||||||
|
:max="maxIso"
|
||||||
|
hint="Entre aujourd'hui et +30 jours"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="rounded bg-m-primary px-3 py-1.5 text-white"
|
|
||||||
@click="value = '2026-12-25'"
|
|
||||||
>
|
|
||||||
Forcer le 25/12/2026
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="rounded border px-3 py-1.5"
|
|
||||||
@click="value = null"
|
|
||||||
>
|
|
||||||
Réinitialiser
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MalioDate
|
|
||||||
v-model="bounded"
|
|
||||||
label="Date bornée"
|
|
||||||
:min="todayIso"
|
|
||||||
:max="maxIso"
|
|
||||||
hint="Entre aujourd'hui et +30 jours"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -49,5 +63,6 @@ const todayIso = toIso(now)
|
|||||||
const maxIso = toIso(new Date(now.getTime() + 30 * 86400000))
|
const maxIso = toIso(new Date(now.getTime() + 30 * 86400000))
|
||||||
|
|
||||||
const value = ref<string | null>(null)
|
const value = ref<string | null>(null)
|
||||||
|
const erpValue = ref<string | null>(null)
|
||||||
const bounded = ref<string | null>(null)
|
const bounded = ref<string | null>(null)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
:root {
|
:root {
|
||||||
/* ── Globales ── */
|
/* ── Globales ── */
|
||||||
--m-primary: 34 39 131; /* #222783 - Bleu Malio */
|
--m-primary: 34 39 131; /* #222783 - Bleu Malio */
|
||||||
|
--m-primary-light: 239 239 253; /* #EFEFFD - Teinte claire du primary (fonds doux) */
|
||||||
--m-bg: 243 244 248; /* #F3F4F8 - Fond de page */
|
--m-bg: 243 244 248; /* #F3F4F8 - Fond de page */
|
||||||
--m-surface: 243 244 248; /* #F3F4F8 - Fond hover/cartes */
|
--m-surface: 243 244 248; /* #F3F4F8 - Fond hover/cartes */
|
||||||
--m-text: 15 23 42; /* #0F172A */
|
--m-text: 15 23 42; /* #0F172A */
|
||||||
|
|||||||
@@ -49,9 +49,9 @@
|
|||||||
</button>
|
</button>
|
||||||
<Icon
|
<Icon
|
||||||
data-test="calendar-icon"
|
data-test="calendar-icon"
|
||||||
icon="mdi:calendar-outline"
|
icon="mdi:calendar-blank"
|
||||||
:width="20"
|
:width="24"
|
||||||
:height="20"
|
:height="24"
|
||||||
:class="iconStateClass"
|
:class="iconStateClass"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,8 +60,7 @@
|
|||||||
v-if="isOpen"
|
v-if="isOpen"
|
||||||
data-test="popover"
|
data-test="popover"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
class="absolute left-0 top-[calc(100%-4px)] z-20 min-w-[320px] rounded-b-md border border-t-0 bg-white"
|
class="absolute left-0 right-0 top-full z-20 box-border w-full rounded-b-md bg-white p-[10px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]"
|
||||||
:class="popoverBorderClass"
|
|
||||||
>
|
>
|
||||||
<CalendarHeader
|
<CalendarHeader
|
||||||
:view-mode="viewMode"
|
:view-mode="viewMode"
|
||||||
@@ -253,7 +252,7 @@ const mergedGroupClass = computed(() =>
|
|||||||
|
|
||||||
const mergedInputClass = computed(() =>
|
const mergedInputClass = computed(() =>
|
||||||
twMerge(
|
twMerge(
|
||||||
'floating-input peer min-h-[40px] w-full cursor-pointer rounded-md border bg-white py-1 pl-3 pr-10 text-lg outline-none placeholder:text-transparent',
|
'floating-input peer min-h-[40px] w-full cursor-pointer rounded-md border bg-white py-1 pl-3 pr-10 text-lg outline-none transition-[padding] duration-150 placeholder:text-transparent',
|
||||||
isFilled.value ? 'border-black' : 'border-m-muted',
|
isFilled.value ? 'border-black' : 'border-m-muted',
|
||||||
props.disabled ? 'cursor-not-allowed text-black/60 border-m-muted' : '',
|
props.disabled ? 'cursor-not-allowed text-black/60 border-m-muted' : '',
|
||||||
hasError.value
|
hasError.value
|
||||||
@@ -261,7 +260,7 @@ const mergedInputClass = computed(() =>
|
|||||||
: hasSuccess.value
|
: hasSuccess.value
|
||||||
? 'border-m-success'
|
? 'border-m-success'
|
||||||
: 'focus:border-m-primary',
|
: 'focus:border-m-primary',
|
||||||
isOpen.value ? '!rounded-b-none !border-b-0 border-m-primary' : '',
|
isOpen.value ? 'border-m-primary !py-[9px] !rounded-b-none' : '',
|
||||||
props.inputClass,
|
props.inputClass,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -274,7 +273,9 @@ const mergedLabelClass = computed(() =>
|
|||||||
? 'text-m-danger'
|
? 'text-m-danger'
|
||||||
: hasSuccess.value
|
: hasSuccess.value
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: 'peer-placeholder-shown:text-m-muted text-black',
|
: isOpen.value
|
||||||
|
? 'text-m-primary'
|
||||||
|
: 'peer-placeholder-shown:text-m-muted text-black',
|
||||||
props.labelClass,
|
props.labelClass,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -286,10 +287,6 @@ const iconStateClass = computed(() => {
|
|||||||
if (isFilled.value) return 'text-black'
|
if (isFilled.value) return 'text-black'
|
||||||
return 'text-m-muted'
|
return 'text-m-muted'
|
||||||
})
|
})
|
||||||
|
|
||||||
const popoverBorderClass = computed(() =>
|
|
||||||
hasError.value ? 'border-m-danger' : hasSuccess.value ? 'border-m-success' : 'border-m-primary',
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,44 +1,44 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-12 items-center justify-between border-b border-m-primary/20 px-2">
|
<div class="flex h-[36px] justify-between border-b border-black/60 mb-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-test="header-prev"
|
data-test="header-prev"
|
||||||
class="rounded p-2 hover:bg-m-primary/10"
|
class="ml-2 flex self-start rounded"
|
||||||
:aria-label="viewMode === 'days' ? 'Mois précédent' : 'Année précédente'"
|
:aria-label="viewMode === 'days' ? 'Mois précédent' : 'Année précédente'"
|
||||||
@click="emit('prev')"
|
@click="emit('prev')"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon="mdi:chevron-left"
|
icon="mdi:chevron-left"
|
||||||
:width="20"
|
:width="25"
|
||||||
:height="20"
|
:height="25"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-test="header-toggle"
|
data-test="header-toggle"
|
||||||
class="flex items-center gap-1 rounded px-2 py-1 text-base font-medium hover:bg-m-primary/10"
|
class="flex gap-1 rounded text-base font-medium"
|
||||||
@click="emit('toggle-view')"
|
@click="emit('toggle-view')"
|
||||||
>
|
>
|
||||||
{{ label }}
|
<span class="mt-[2px]">{{ label }}</span>
|
||||||
<Icon
|
<Icon
|
||||||
icon="mdi:chevron-down"
|
icon="mdi:chevron-down"
|
||||||
:width="16"
|
:width="25"
|
||||||
:height="16"
|
:height="25"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-test="header-next"
|
data-test="header-next"
|
||||||
class="rounded p-2 hover:bg-m-primary/10"
|
class="mr-2 flex self-start rounded"
|
||||||
:aria-label="viewMode === 'days' ? 'Mois suivant' : 'Année suivante'"
|
:aria-label="viewMode === 'days' ? 'Mois suivant' : 'Année suivante'"
|
||||||
@click="emit('next')"
|
@click="emit('next')"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon="mdi:chevron-right"
|
icon="mdi:chevron-right"
|
||||||
:width="20"
|
:width="25"
|
||||||
:height="20"
|
:height="25"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,24 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<div data-test="month-grid">
|
<div data-test="month-grid">
|
||||||
<div class="grid grid-cols-8">
|
<div class="grid grid-cols-[auto_repeat(7,minmax(0,1fr))]">
|
||||||
<div class="flex h-8 items-center justify-center text-xs font-medium uppercase text-m-muted">
|
<div class="mr-[12px] flex h-8 w-[35px] items-center justify-center text-[14px] font-medium opacity-[60%]">
|
||||||
Sem
|
S
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="d in dayLabels"
|
v-for="d in dayLabels"
|
||||||
:key="d"
|
:key="d"
|
||||||
class="flex h-8 items-center justify-center text-xs font-medium uppercase text-m-muted"
|
class="flex h-8 items-center justify-center text-[14px] font-medium opacity-[60%]"
|
||||||
>
|
>
|
||||||
{{ d }}
|
{{ d }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template
|
<template
|
||||||
v-for="week in weeks"
|
v-for="(week, wIndex) in weeks"
|
||||||
:key="week.days[0].isoDate"
|
:key="week.days[0].isoDate"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-test="week-number"
|
data-test="week-number"
|
||||||
class="flex h-10 items-center justify-center bg-m-primary/10 text-sm text-m-primary/70"
|
class="mr-[12px] flex h-[45px] w-[35px] shrink-0 items-center justify-center bg-m-primary-light p-[10px] text-sm"
|
||||||
|
:class="[
|
||||||
|
week.days.some(d => d.isToday) ? 'text-black' : 'text-black/60',
|
||||||
|
wIndex === 0 ? 'rounded-t-md' : '',
|
||||||
|
wIndex === weeks.length - 1 ? 'rounded-b-md' : '',
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
{{ week.weekNumber }}
|
{{ week.weekNumber }}
|
||||||
</div>
|
</div>
|
||||||
@@ -31,11 +36,16 @@
|
|||||||
:disabled="!inRange(cell.isoDate)"
|
:disabled="!inRange(cell.isoDate)"
|
||||||
:aria-label="ariaLabel(cell)"
|
:aria-label="ariaLabel(cell)"
|
||||||
:aria-disabled="!inRange(cell.isoDate)"
|
:aria-disabled="!inRange(cell.isoDate)"
|
||||||
class="flex h-10 w-10 items-center justify-center text-sm transition-colors duration-100"
|
class="flex h-[45px] w-full items-center justify-center"
|
||||||
:class="cellClass(cell)"
|
:class="inRange(cell.isoDate) ? 'cursor-pointer' : 'cursor-not-allowed'"
|
||||||
@click="onSelect(cell.isoDate)"
|
@click="onSelect(cell.isoDate)"
|
||||||
>
|
>
|
||||||
{{ cell.day }}
|
<span
|
||||||
|
class="flex h-10 w-10 items-center justify-center rounded-full text-sm font-medium transition-colors duration-100"
|
||||||
|
:class="cellClass(cell)"
|
||||||
|
>
|
||||||
|
{{ cell.day }}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,12 +87,12 @@ const ariaLabel = (cell: DayCell) => {
|
|||||||
|
|
||||||
const cellClass = (cell: DayCell) => {
|
const cellClass = (cell: DayCell) => {
|
||||||
const selected = props.selectedDate === cell.isoDate
|
const selected = props.selectedDate === cell.isoDate
|
||||||
if (!inRange(cell.isoDate)) return 'text-m-muted/30 cursor-not-allowed'
|
if (!inRange(cell.isoDate)) return 'text-m-muted/30'
|
||||||
if (selected) return 'bg-m-primary text-white font-medium rounded-full'
|
if (selected) return 'bg-m-primary text-white'
|
||||||
const parts = ['cursor-pointer hover:bg-m-primary/10 rounded-full']
|
const parts = ['hover:bg-m-primary/10']
|
||||||
if (cell.isToday) parts.push('border border-m-primary text-m-primary font-semibold')
|
if (cell.isToday) parts.push('border border-m-primary text-m-primary')
|
||||||
else if (cell.isCurrentMonth) parts.push('text-black')
|
else if (cell.isCurrentMonth) parts.push('text-black')
|
||||||
else parts.push('text-m-muted/50')
|
else parts.push('opacity-[60%]')
|
||||||
return parts.join(' ')
|
return parts.join(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
data-test="month-picker"
|
data-test="month-picker"
|
||||||
class="grid grid-cols-4 gap-2 p-3"
|
class="grid grid-cols-3 gap-3"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-for="(name, index) in monthsShort"
|
v-for="(name, index) in months"
|
||||||
:key="name"
|
:key="name"
|
||||||
type="button"
|
type="button"
|
||||||
data-test="month"
|
data-test="month"
|
||||||
:data-month="index"
|
:data-month="index"
|
||||||
class="rounded py-3 text-sm transition-colors duration-100"
|
class="flex h-[45px] w-full items-center justify-center"
|
||||||
:class="index === selectedMonth
|
|
||||||
? 'bg-m-primary text-white'
|
|
||||||
: 'text-black hover:bg-m-primary/10'"
|
|
||||||
@click="emit('select', index)"
|
@click="emit('select', index)"
|
||||||
>
|
>
|
||||||
{{ name }}
|
<span
|
||||||
|
class="flex h-[30px] w-full items-center justify-center rounded text-sm transition-colors duration-100"
|
||||||
|
:class="index === selectedMonth
|
||||||
|
? 'bg-m-primary text-white'
|
||||||
|
: 'text-black hover:bg-m-primary/10'"
|
||||||
|
>
|
||||||
|
{{ name }}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -27,6 +31,6 @@ defineProps<{selectedMonth?: number}>()
|
|||||||
|
|
||||||
const emit = defineEmits<{(e: 'select', month: number): void}>()
|
const emit = defineEmits<{(e: 'select', month: number): void}>()
|
||||||
|
|
||||||
const monthsShort = ['Janv', 'Févr', 'Mars', 'Avr', 'Mai', 'Juin',
|
const months = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
||||||
'Juil', 'Août', 'Sept', 'Oct', 'Nov', 'Déc']
|
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export default {
|
|||||||
colors: {
|
colors: {
|
||||||
m: {
|
m: {
|
||||||
primary: 'rgb(var(--m-primary) / <alpha-value>)',
|
primary: 'rgb(var(--m-primary) / <alpha-value>)',
|
||||||
|
'primary-light': 'rgb(var(--m-primary-light) / <alpha-value>)',
|
||||||
surface: 'rgb(var(--m-surface) / <alpha-value>)',
|
surface: 'rgb(var(--m-surface) / <alpha-value>)',
|
||||||
border: 'rgb(var(--m-border) / <alpha-value>)',
|
border: 'rgb(var(--m-border) / <alpha-value>)',
|
||||||
text: 'rgb(var(--m-text) / <alpha-value>)',
|
text: 'rgb(var(--m-text) / <alpha-value>)',
|
||||||
|
|||||||
Reference in New Issue
Block a user