# MalioDateWeek — Design Spec Composant de sélection d'une **semaine ISO complète** (lundi→dimanche) via le shell calendrier partagé. Troisième brique de la famille temporelle. **Ticket :** MUI-33 (suite) **Branche :** `feature/MUI-33-developper-le-composant-datepicker` **Specs liées :** `2026-05-19-datepicker-design.md`, `2026-05-20-daterange-design.md` ## Périmètre Sélection d'une semaine entière en **un clic** (sur n'importe quel jour OU le numéro de semaine). Visuellement : la ligne de la semaine se surligne en pilule `bg-m-primary-light` (lundi arrondi gauche, dimanche arrondi droit), exactement comme une plage `DateRange` figée lun→dim. Survol = aperçu de la semaine. La cellule n° de la semaine sélectionnée passe en `bg-m-primary` (repère). **Inclus :** clic jour/n° → semaine, hover de semaine, surlignage pilule, repère n° semaine, bornes `min`/`max` (semaine sélectionnable si elle chevauche), effacement, vue mois conservée. **Reporté :** deux mois, saisie/navigation clavier. ## Donnée retournée `modelValue: string | null` au format **ISO 8601 semaine `YYYY-Www`** (ex. `"2026-W21"`), comme l'`` natif. L'année est l'**année ISO de numérotation** (peut différer de l'année calendaire aux bords d'année). Affichage humain dans le champ : `"Semaine 21 (18/05 → 24/05/2026)"` (le `modelValue` reste `2026-W21`). ## Architecture (Approche 1 — réutilisation du rendu plage) Une semaine sélectionnée **est** une plage lundi→dimanche : on réutilise le rendu pilule de `MonthGrid` (mode plage) en passant les bornes de la semaine active. Les events `select`/`hover` (jour) sont réutilisés ; l'enveloppe `DateWeek` mappe jour → semaine. ``` app/components/malio/date/ DateWeek.vue # NOUVEAU — enveloppe DateWeek.test.ts # nouveau internal/ MonthGrid.vue # étendu : interactiveWeekNumber + markedWeekStart (additifs) CalendarField.vue # inchangé (shell réutilisé) CalendarHeader.vue # inchangé MonthPicker.vue # inchangé composables/ dateWeek.ts # NOUVEAU — helpers semaine ISO (purs) dateWeek.test.ts # nouveau dateRange.ts # inchangé (rendu pilule réutilisé) dateFormat.ts # inchangé useCalendarView.ts # inchangé useCalendarPopover.ts # inchangé useMonthMatrix.ts # inchangé app/story/date/dateWeek.story.vue .playground/pages/composant/date/dateWeek.vue ``` Flux : ``` DateWeek.vue (enveloppe) ├─ état : hoverWeekStart (lundi de la semaine survolée) ├─ validWeek = isValidIsoWeek(modelValue) ? { monday: isoWeekToMonday(modelValue) } : null ├─ activeMonday = hoverWeekStart ?? validWeek.monday → activeSunday = sundayOf(activeMonday) ├─ displayValue = formatWeekDisplay(modelValue) └─ └─ onSelect(iso, close)" @hover="onHover" /> ``` ## `dateWeek.ts` (helpers purs) ```ts function mondayOf(iso: string): string // "2026-05-20" → "2026-05-18" function sundayOf(iso: string): string // "2026-05-20" → "2026-05-24" function toIsoWeek(iso: string): string // "2026-05-20" → "2026-W21" (année ISO + n° semaine) function isoWeekToMonday(week: string): string | null // "2026-W21" → "2026-05-18" ; invalide → null function isValidIsoWeek(week: string): boolean // "2026-W21" → true ; "2026-W54"/"2026-21" → false function formatWeekDisplay(week: string): string // "2026-W21" → "Semaine 21 (18/05 → 24/05/2026)" ; invalide → "" ``` - Algo ISO 8601 : jeudi de la semaine pour l'année de numérotation ; lundi de la semaine contenant le 4 janvier = semaine 1. - `formatWeekDisplay` : `Semaine {n° sans zéro} ({JJ/MM lundi} → {JJ/MM/AAAA dimanche})`, réutilise `formatIsoToDisplay`. - Cas pièges testés : `2025-12-31` → `2026-W01`, `2027-01-01` → `2026-W53`, `2026-01-01` → `2026-W01`. ## `MonthGrid.vue` — ajouts additifs Nouvelles props optionnelles (n'altèrent pas les modes simple/plage) : ```ts interactiveWeekNumber?: boolean // défaut false markedWeekStart?: string | null // défaut null — lundi de la semaine repère ``` Quand `interactiveWeekNumber === true` : - La cellule n° de semaine devient un `` : `@click` émet `select(week.days[0].isoDate)`, `@mouseenter` émet `hover(week.days[0].isoDate)`. `:disabled` si `!weekSelectable`, où `weekSelectable = week.days.some(d => inRange(d.isoDate))`. `cursor-pointer` si sélectionnable. - Repère : si `week.days[0].isoDate === markedWeekStart`, la cellule n° passe en `bg-m-primary text-white` (au lieu de `bg-m-primary-light`). Toujours (inoffensif hors mode semaine) : la cellule n° porte `:data-week-start="week.days[0].isoDate"` et `:data-marked="week.days[0].isoDate === markedWeekStart"`. Inchangé : rendu pilule des jours piloté par `rangeStart`/`rangeEnd` ; events `select`/`hover` jour ; quand `interactiveWeekNumber` est `false`, la cellule n° reste un `` non cliquable (aucune régression `Date`/`DateRange`). ## `DateWeek.vue` (enveloppe) **Props :** identiques à `Date` sauf `modelValue?: string | null` (`YYYY-Www`). **Emit :** `update:modelValue` → `string | null`. **État :** ```ts hoverWeekStart = ref(null) const validWeek = computed(() => (props.modelValue && isValidIsoWeek(props.modelValue)) ? {monday: isoWeekToMonday(props.modelValue) as string} : null) const activeMonday = computed(() => hoverWeekStart.value ?? validWeek.value?.monday ?? null) const activeSunday = computed(() => activeMonday.value ? sundayOf(activeMonday.value) : null) ``` **Passé à `MonthGrid` :** `range-start=activeMonday`, `range-end=activeSunday`, `marked-week-start=validWeek?.monday ?? null`, `interactive-week-number`, `min`, `max`, month/year du slot. **`displayValue`** = `validWeek ? formatWeekDisplay(modelValue) : ''`. **`syncTo`** = `validWeek?.monday ?? null`. **Comportement (1 clic) :** ``` onSelect(iso, close): # jour OU n° de semaine (= lundi) emit('update:modelValue', toIsoWeek(iso)) hoverWeekStart = null close() onHover(iso): # jour/n° survolé ; null au mouseleave hoverWeekStart = iso ? mondayOf(iso) : null onClose(): hoverWeekStart = null onClear(): emit('update:modelValue', null) ; hoverWeekStart = null ``` - Survol → toute la ligne en pilule via `activeMonday`. - Sélection en un clic → `YYYY-Www` + fermeture. - Repère de la semaine committée conservé pendant le survol d'une autre. - `modelValue` invalide → traité comme `null` + warning dev. ## Tests ### `dateWeek.test.ts` (~14) - `mondayOf`/`sundayOf` : mercredi, lundi (idempotent), dimanche - `toIsoWeek` : nominal + bords d'année (`2025-12-31`→`2026-W01`, `2027-01-01`→`2026-W53`, `2026-01-01`→`2026-W01`) - `isoWeekToMonday` : `2026-W21`→`2026-05-18` ; round-trip ; invalide → null - `isValidIsoWeek` : valide / `W00` / `W54` / format faux - `formatWeekDisplay` : `2026-W21`→`"Semaine 21 (18/05 → 24/05/2026)"` ; invalide → `""` ### `DateWeek.test.ts` (~14, `setSystemTime(2026-05-19)`) - Rendu label/icône, affichage `"Semaine ..."` si modelValue, champ vide sinon - Ouverture sur le mois de la semaine du modelValue - Clic d'un jour → émet le `YYYY-Www` de sa semaine + ferme - Clic du n° de semaine (`[data-test="week-number"][data-week-start="..."]`) → émet + ferme - Hover d'un jour → `data-range-role` start/in-range/end sur les 7 jours de la ligne ; autre ligne `none` - Hover du n° de semaine → même surlignage - Semaine committée → roles corrects + `data-marked="true"` sur la cellule n° - `clear` → émet `null` - min/max : semaine hors bornes n° désactivé + jours non cliquables ; semaine qui chevauche reste sélectionnable - `disabled`/`readonly` → pas d'ouverture - a11y : `aria-invalid` sur error `Date.test.ts` / `DateRange.test.ts` restent verts (props additives). ### Story `dateWeek.story.vue` Default vide, semaine initiale, min/max, états (disabled/readonly/error/success), non-clearable. ### Playground `.playground/pages/composant/date/dateWeek.vue` Comparatif Large (480px) / ERP (396px), affichage `modelValue` + bornes du champ, boutons set/reset, cas borné. ## Découpage d'implémentation 1. `dateWeek.ts` (purs) + tests 2. Extension `MonthGrid.vue` (interactiveWeekNumber + markedWeekStart + data attrs) 3. `DateWeek.vue` + `DateWeek.test.ts` 4. Story + playground