Files
malio-layer-ui/docs/superpowers/specs/2026-05-20-dateweek-design.md
2026-05-20 15:10:25 +02:00

169 lines
8.8 KiB
Markdown

# 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'`<input type="week">` 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)
└─ <CalendarField :display-value :sync-to=validWeek.monday @clear @close>
└─ <MonthGrid
:range-start=activeMonday :range-end=activeSunday ← pilule lun→dim réutilisée
:marked-week-start=validWeek.monday ← repère n° semaine
interactive-week-number ← n° semaine cliquable/hoverable
:min :max
@select="(iso)=>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 `<button>` : `@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 `<div>` 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<string | null>(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