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

8.8 KiB

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)

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-312026-W01, 2027-01-012026-W53, 2026-01-012026-W01.

MonthGrid.vue — ajouts additifs

Nouvelles props optionnelles (n'altèrent pas les modes simple/plage) :

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:modelValuestring | null.

État :

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-312026-W01, 2027-01-012026-W53, 2026-01-012026-W01)
  • isoWeekToMonday : 2026-W212026-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