diff --git a/docs/superpowers/specs/2026-05-20-dateweek-design.md b/docs/superpowers/specs/2026-05-20-dateweek-design.md new file mode 100644 index 0000000..5d6d37a --- /dev/null +++ b/docs/superpowers/specs/2026-05-20-dateweek-design.md @@ -0,0 +1,168 @@ +# 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