41010060ff
## Sélecteur d'année dans le calendrier (3ᵉ niveau de navigation) Ajoute un 3ᵉ niveau de navigation à la famille de composants date, et corrige le bornage min/max du sélecteur de mois. ### Comportement - Clic sur le champ → calendrier (vue **jours**) - Clic sur l'en-tête → **sélecteur de mois** - **Re-clic sur l'en-tête → sélecteur d'année** (grille de 12 ans, chevrons paginant par pas de 12 ans, fenêtre centrée sur l'année courante − 5) - Clic sur une année → retour au sélecteur de mois ; clic sur un mois → retour à la grille de jours - Les props `min`/`max` **grisent les mois ET les années** hors plage (corrige l'asymétrie : le `MonthPicker` affichait jusqu'ici tous les mois) En-tête contextuel : « Mai 2026 » (jours) / « 2026 » (mois) / « 2020 – 2031 » (années). ### Périmètre - Shell partagé `internal/CalendarField.vue` → bénéficie aux 4 composants publics `Date`, `DateRange`, `DateTime`, `DateWeek` - **Aucune API publique modifiée** - Nouveau composant `internal/YearPicker.vue` (calqué sur `MonthPicker`) - Helpers purs `isMonthInRange` / `isYearInRange` (comparaison par préfixe ISO, bornes inclusives) - State machine `viewMode` à 3 niveaux (`useCalendarPopover` / `useCalendarView`) ### Tests - Suite date **246/246 verte**, ESLint propre - Unitaires : helpers, `YearPicker`, `MonthPicker` (grisage), composables (pagination ±12, recentrage, `selectYear`) - e2e `Date.test.ts` : flux complet jours→mois→années→mois→jours + grisage min/max ### Process Développé en brainstorming → spec → plan → exécution TDD (un commit par étape). Spec et plan inclus sous `docs/superpowers/`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #83 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
126 lines
3.0 KiB
Vue
126 lines
3.0 KiB
Vue
<template>
|
|
<CalendarField
|
|
:id="id"
|
|
:display-value="displayValue"
|
|
:sync-to="validWeek?.monday ?? null"
|
|
:name="name"
|
|
:label="label"
|
|
:placeholder="placeholder"
|
|
:required="required"
|
|
:disabled="disabled"
|
|
:readonly="readonly"
|
|
:min="min"
|
|
:max="max"
|
|
:hint="hint"
|
|
:error="error"
|
|
:success="success"
|
|
:clearable="clearable"
|
|
:input-class="inputClass"
|
|
:label-class="labelClass"
|
|
:group-class="groupClass"
|
|
v-bind="$attrs"
|
|
@clear="onClear"
|
|
@close="onClose"
|
|
>
|
|
<template #default="{ currentMonth, currentYear, close }">
|
|
<MonthGrid
|
|
:month="currentMonth"
|
|
:year="currentYear"
|
|
:range-start="activeMonday"
|
|
:range-end="activeSunday"
|
|
:marked-week-start="validWeek?.monday ?? null"
|
|
interactive-week-number
|
|
:min="min"
|
|
:max="max"
|
|
@select="(iso) => onSelect(iso, close)"
|
|
@hover="onHover"
|
|
/>
|
|
</template>
|
|
</CalendarField>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import {computed, ref} from 'vue'
|
|
import CalendarField from './internal/CalendarField.vue'
|
|
import MonthGrid from './internal/MonthGrid.vue'
|
|
import {formatWeekDisplay, isValidIsoWeek, isoWeekToMonday, mondayOf, sundayOf, toIsoWeek} from './composables/dateWeek'
|
|
|
|
defineOptions({name: 'MalioDateWeek', inheritAttrs: false})
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
id?: string
|
|
name?: string
|
|
label?: string
|
|
modelValue?: string | null
|
|
placeholder?: string
|
|
required?: boolean
|
|
disabled?: boolean
|
|
readonly?: boolean
|
|
hint?: string
|
|
error?: string
|
|
success?: string
|
|
min?: string
|
|
max?: string
|
|
clearable?: boolean
|
|
inputClass?: string
|
|
labelClass?: string
|
|
groupClass?: string
|
|
}>(),
|
|
{
|
|
id: '',
|
|
name: '',
|
|
label: '',
|
|
modelValue: undefined,
|
|
placeholder: 'JJ/MM/AAAA',
|
|
required: false,
|
|
disabled: false,
|
|
readonly: false,
|
|
hint: '',
|
|
error: '',
|
|
success: '',
|
|
min: undefined,
|
|
max: undefined,
|
|
clearable: true,
|
|
inputClass: '',
|
|
labelClass: '',
|
|
groupClass: '',
|
|
},
|
|
)
|
|
|
|
const emit = defineEmits<{(e: 'update:modelValue', value: string | null): void}>()
|
|
|
|
const hoverWeekStart = ref<string | null>(null)
|
|
|
|
const validWeek = computed(() => {
|
|
if (props.modelValue && isValidIsoWeek(props.modelValue)) {
|
|
return {monday: isoWeekToMonday(props.modelValue) as string}
|
|
}
|
|
return null
|
|
})
|
|
|
|
const activeMonday = computed(() => hoverWeekStart.value ?? validWeek.value?.monday ?? null)
|
|
const activeSunday = computed(() => (activeMonday.value ? sundayOf(activeMonday.value) : null))
|
|
|
|
const displayValue = computed(() => (validWeek.value ? formatWeekDisplay(props.modelValue as string) : ''))
|
|
|
|
const onSelect = (iso: string, close: () => void) => {
|
|
emit('update:modelValue', toIsoWeek(iso))
|
|
hoverWeekStart.value = null
|
|
close()
|
|
}
|
|
|
|
const onHover = (iso: string | null) => {
|
|
hoverWeekStart.value = iso ? mondayOf(iso) : null
|
|
}
|
|
|
|
const onClose = () => {
|
|
hoverWeekStart.value = null
|
|
}
|
|
|
|
const onClear = () => {
|
|
emit('update:modelValue', null)
|
|
hoverWeekStart.value = null
|
|
}
|
|
</script>
|