# 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.