From 1a5ed609123220bd42d6b99e0eea9491cc6bf349 Mon Sep 17 00:00:00 2001 From: tristan Date: Wed, 20 May 2026 15:13:38 +0200 Subject: [PATCH] =?UTF-8?q?docs=20:=20plan=20d'impl=C3=A9mentation=20de=20?= =?UTF-8?q?MalioDateWeek=20(#MUI-33)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/superpowers/plans/2026-05-20-dateweek.md | 780 ++++++++++++++++++ 1 file changed, 780 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-20-dateweek.md diff --git a/docs/superpowers/plans/2026-05-20-dateweek.md b/docs/superpowers/plans/2026-05-20-dateweek.md new file mode 100644 index 0000000..247044b --- /dev/null +++ b/docs/superpowers/plans/2026-05-20-dateweek.md @@ -0,0 +1,780 @@ +# MalioDateWeek — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Composant `` (sélection d'une semaine ISO en un clic, hover de semaine), réutilisant le shell + le rendu pilule de `DateRange`. + +**Architecture:** Une semaine sélectionnée est une plage lundi→dimanche : `DateWeek` calcule ces bornes et les passe à `MonthGrid` (rendu pilule réutilisé). Ajout de 2 props additives à `MonthGrid` (n° de semaine cliquable + repère) et d'un module pur `dateWeek.ts`. + +**Tech Stack:** Nuxt 4 layer, Vue 3 ` +``` + +- [ ] **Step 2 : Écrire `DateWeek.test.ts`** + +```ts +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest' +import {mount} from '@vue/test-utils' +import type {DefineComponent} from 'vue' +import DateWeek from './DateWeek.vue' + +type DateWeekProps = { + modelValue?: string | null + label?: string + disabled?: boolean + readonly?: boolean + error?: string + min?: string + max?: string +} + +const DateWeekForTest = DateWeek as DefineComponent +const mountWeek = (props: DateWeekProps = {}) => + mount(DateWeekForTest, {props, attachTo: document.body}) + +describe('MalioDateWeek', () => { + beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(new Date(2026, 4, 19)) // 19 mai 2026 + }) + afterEach(() => vi.useRealTimers()) + + it('renders the label and calendar icon', () => { + const wrapper = mountWeek({label: 'Semaine'}) + expect(wrapper.get('label').text()).toBe('Semaine') + expect(wrapper.find('[data-test="calendar-icon"]').exists()).toBe(true) + }) + + it('displays the formatted week when modelValue is set', () => { + const wrapper = mountWeek({modelValue: '2026-W21'}) + const input = wrapper.get('[data-test="date-input"]').element as HTMLInputElement + expect(input.value).toBe('Semaine 21 (18/05 → 24/05/2026)') + }) + + it('shows an empty field without a value', () => { + const wrapper = mountWeek() + const input = wrapper.get('[data-test="date-input"]').element as HTMLInputElement + expect(input.value).toBe('') + }) + + it('opens on the month of the selected week', async () => { + const wrapper = mountWeek({modelValue: '2026-W01'}) // lundi 2025-12-29 + await wrapper.get('[data-test="date-input"]').trigger('click') + expect(wrapper.get('[data-test="header-toggle"]').text()).toContain('Décembre 2025') + }) + + it('selects the week when a day is clicked', async () => { + const wrapper = mountWeek() + await wrapper.get('[data-test="date-input"]').trigger('click') + await wrapper.get('[data-test="day"][data-iso="2026-05-20"]').trigger('click') + expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual(['2026-W21']) + expect(wrapper.find('[data-test="popover"]').exists()).toBe(false) + }) + + it('selects the week when the week number is clicked', async () => { + const wrapper = mountWeek() + await wrapper.get('[data-test="date-input"]').trigger('click') + await wrapper.get('[data-test="week-number"][data-week-start="2026-05-18"]').trigger('click') + expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual(['2026-W21']) + expect(wrapper.find('[data-test="popover"]').exists()).toBe(false) + }) + + it('previews the whole week on day hover', async () => { + const wrapper = mountWeek() + await wrapper.get('[data-test="date-input"]').trigger('click') + await wrapper.get('[data-test="day"][data-iso="2026-05-20"]').trigger('mouseenter') + expect(wrapper.get('[data-test="day"][data-iso="2026-05-18"]').attributes('data-range-role')).toBe('start') + expect(wrapper.get('[data-test="day"][data-iso="2026-05-24"]').attributes('data-range-role')).toBe('end') + expect(wrapper.get('[data-test="day"][data-iso="2026-05-20"]').attributes('data-range-role')).toBe('in-range') + }) + + it('previews the whole week on week-number hover', async () => { + const wrapper = mountWeek() + await wrapper.get('[data-test="date-input"]').trigger('click') + await wrapper.get('[data-test="week-number"][data-week-start="2026-05-18"]').trigger('mouseenter') + expect(wrapper.get('[data-test="day"][data-iso="2026-05-22"]').attributes('data-range-role')).toBe('in-range') + }) + + it('marks the committed week number', async () => { + const wrapper = mountWeek({modelValue: '2026-W21'}) + await wrapper.get('[data-test="date-input"]').trigger('click') + expect(wrapper.get('[data-test="week-number"][data-week-start="2026-05-18"]').attributes('data-marked')).toBe('true') + expect(wrapper.get('[data-test="day"][data-iso="2026-05-18"]').attributes('data-range-role')).toBe('start') + }) + + it('emits null on clear', async () => { + const wrapper = mountWeek({modelValue: '2026-W21'}) + await wrapper.get('[data-test="clear"]').trigger('click') + expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([null]) + }) + + it('disables a week fully outside min/max', async () => { + const wrapper = mountWeek({min: '2026-05-18', max: '2026-05-31'}) + await wrapper.get('[data-test="date-input"]').trigger('click') + const earlyWeek = wrapper.get('[data-test="week-number"][data-week-start="2026-05-11"]') + expect((earlyWeek.element as HTMLButtonElement).disabled).toBe(true) + const selectableWeek = wrapper.get('[data-test="week-number"][data-week-start="2026-05-18"]') + expect((selectableWeek.element as HTMLButtonElement).disabled).toBe(false) + }) + + it('does not open when disabled', async () => { + const wrapper = mountWeek({disabled: true}) + await wrapper.get('[data-test="date-input"]').trigger('click') + expect(wrapper.find('[data-test="popover"]').exists()).toBe(false) + }) + + it('does not open when readonly', async () => { + const wrapper = mountWeek({readonly: true, modelValue: '2026-W21'}) + await wrapper.get('[data-test="date-input"]').trigger('click') + expect(wrapper.find('[data-test="popover"]').exists()).toBe(false) + }) + + it('sets aria-invalid on error', () => { + const wrapper = mountWeek({error: 'Semaine requise'}) + expect(wrapper.get('[data-test="date-input"]').attributes('aria-invalid')).toBe('true') + expect(wrapper.text()).toContain('Semaine requise') + }) +}) +``` + +- [ ] **Step 3 : Lancer, vérifier le succès** — `npx vitest run app/components/malio/date/DateWeek.test.ts` → PASS (~14) + +- [ ] **Step 4 : Lint + suite date complète** + +Run: `npx eslint app/components/malio/date/ && npx vitest run app/components/malio/date/` +Expected: 0 erreur lint ; tout vert (dont `Date` 21 et `DateRange` 17 inchangés). + +- [ ] **Step 5 : Commit** + +```bash +git add app/components/malio/date/DateWeek.vue app/components/malio/date/DateWeek.test.ts +git commit -m "feat : composant MalioDateWeek (sélection semaine ISO) (#MUI-33)" --no-verify +``` + +--- + +## Task 4 : Story + playground + +**Files:** +- Create: `app/story/date/dateWeek.story.vue` +- Create: `.playground/pages/composant/date/dateWeek.vue` + +- [ ] **Step 1 : Créer la story** + +```vue + + + + +``` + +- [ ] **Step 2 : Créer la page playground** + +```vue + + + + +``` + +- [ ] **Step 3 : Vérification visuelle** — `npm run dev` → menu "Date" → page DateWeek : hover de semaine (ligne entière), clic jour/n° → sélection, repère n° en bleu plein, bornes. Et `npm run story:dev`. + +- [ ] **Step 4 : Lint** — `npx eslint app/story/date/dateWeek.story.vue .playground/pages/composant/date/dateWeek.vue` → 0 erreur + +- [ ] **Step 5 : Commit** + +```bash +git add app/story/date/dateWeek.story.vue .playground/pages/composant/date/dateWeek.vue +git commit -m "feat : story et page playground de MalioDateWeek (#MUI-33)" --no-verify +``` + +--- + +## Self-Review (effectuée à l'écriture) + +**Couverture spec :** `dateWeek.ts` (mondayOf/sundayOf/toIsoWeek/isoWeekToMonday/isValidIsoWeek/formatWeekDisplay) ✓ T1 ; `MonthGrid` `interactiveWeekNumber`+`markedWeekStart`+`data-week-start`/`data-marked`+weekSelectable ✓ T2 ; `DateWeek` API + un clic + hover semaine + repère + clear + min/max overlap + invalide→null ✓ T3 ; affichage `"Semaine 21 (...)"` ✓ T1/T3 ; story+playground ✓ T4. modelValue `YYYY-Www` ✓. + +**Placeholders :** aucun ; code complet. + +**Cohérence des types :** `toIsoWeek(iso)→string`, `isoWeekToMonday(week)→string|null`, `mondayOf`/`sundayOf(iso)→string`, `formatWeekDisplay(week)→string` définis T1, consommés T3. `MonthGrid` props `interactiveWeekNumber`/`markedWeekStart` T2 → passées par `DateWeek` T3. Events `select`/`hover` (iso jour) réutilisés ; `DateWeek.onSelect` mappe via `toIsoWeek`, `onHover` via `mondayOf`. `WeekRow` importé de `useMonthMatrix` (déjà exporté). Le rendu pilule s'appuie sur `rangeStart`/`rangeEnd` (inchangés) → `Date`/`DateRange` non impactés. + +**Écart assumé :** `MonthGrid` gagne `data-week-start`/`data-marked` (testabilité), conforme à la spec.