diff --git a/docs/superpowers/plans/2026-06-22-date-year-picker.md b/docs/superpowers/plans/2026-06-22-date-year-picker.md
new file mode 100644
index 0000000..06f38bf
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-22-date-year-picker.md
@@ -0,0 +1,934 @@
+# Sélecteur d'année dans le calendrier — 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:** Ajouter un 3ᵉ niveau de navigation au calendrier de la famille `date/` : depuis la vue mois, recliquer sur le header ouvre un sélecteur d'année calqué sur le sélecteur de mois, avec respect des bornes `min`/`max`.
+
+**Architecture:** Le shell partagé `internal/CalendarField.vue` orchestre l'input, le popover, le header (`CalendarHeader`) et la commutation entre les vues `days` / `months` / `years`. `MonthPicker` et le nouveau `YearPicker` sont rendus dans `CalendarField` ; la grille de jours reste fournie par chaque consommateur via slot scoped. La logique d'état vit dans deux composables (`useCalendarPopover`, `useCalendarView`) et les bornes dans des helpers purs (`dateFormat.ts`).
+
+**Tech Stack:** Nuxt 4 layer, Vue 3 `
+```
+
+> Note : `@click` émet toujours, mais un `` ne déclenche pas l'événement clic dans jsdom/navigateur — le test « does not emit » le valide.
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `npx vitest run app/components/malio/date/internal/YearPicker.test.ts`
+Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add app/components/malio/date/internal/YearPicker.vue app/components/malio/date/internal/YearPicker.test.ts
+git commit -m "feat : composant YearPicker avec bornage min/max (#date-year-picker)
+
+Co-Authored-By: Claude Opus 4.8 (1M context) "
+```
+
+---
+
+### Task 6: Bornage min/max du `MonthPicker` + tests
+
+**Files:**
+- Modify: `app/components/malio/date/internal/MonthPicker.vue`
+- Test (create): `app/components/malio/date/internal/MonthPicker.test.ts`
+
+**Interfaces:**
+- Consumes: `isMonthInRange` (Task 1).
+- Produces: `…" />`. `currentYear` devient **requis** (nécessaire pour borner les mois).
+
+- [ ] **Step 1: Write the failing test**
+
+Créer `app/components/malio/date/internal/MonthPicker.test.ts` :
+```ts
+import {describe, expect, it} from 'vitest'
+import {mount} from '@vue/test-utils'
+import MonthPicker from './MonthPicker.vue'
+
+const mountPicker = (props: {currentYear: number, selectedMonth?: number, min?: string, max?: string}) =>
+ mount(MonthPicker, {props})
+
+describe('MalioDateMonthPicker', () => {
+ it('renders 12 months', () => {
+ const wrapper = mountPicker({currentYear: 2026})
+ expect(wrapper.findAll('[data-test="month"]')).toHaveLength(12)
+ })
+
+ it('emits select with the clicked month index', async () => {
+ const wrapper = mountPicker({currentYear: 2026})
+ await wrapper.get('[data-test="month"][data-month="0"]').trigger('click')
+ expect(wrapper.emitted('select')?.[0]).toEqual([0])
+ })
+
+ it('disables months before min in the current year and does not emit', async () => {
+ const wrapper = mountPicker({currentYear: 2026, min: '2026-05-01'})
+ const april = wrapper.get('[data-test="month"][data-month="3"]')
+ expect(april.attributes('disabled')).toBeDefined()
+ await april.trigger('click')
+ expect(wrapper.emitted('select')).toBeUndefined()
+ })
+
+ it('disables months after max in the current year', () => {
+ const wrapper = mountPicker({currentYear: 2026, max: '2026-05-31'})
+ expect(wrapper.get('[data-test="month"][data-month="5"]').attributes('disabled')).toBeDefined()
+ expect(wrapper.get('[data-test="month"][data-month="4"]').attributes('disabled')).toBeUndefined()
+ })
+})
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `npx vitest run app/components/malio/date/internal/MonthPicker.test.ts`
+Expected: FAIL — le mois avril n'est pas désactivé (`disabled` undefined).
+
+- [ ] **Step 3: Update the component**
+
+Remplacer `MonthPicker.vue` par :
+```vue
+
+
+
+
+ {{ name }}
+
+
+
+
+
+
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `npx vitest run app/components/malio/date/internal/MonthPicker.test.ts`
+Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add app/components/malio/date/internal/MonthPicker.vue app/components/malio/date/internal/MonthPicker.test.ts
+git commit -m "feat : bornage min/max du MonthPicker (#date-year-picker)
+
+Co-Authored-By: Claude Opus 4.8 (1M context) "
+```
+
+---
+
+### Task 7: Câblage `CalendarField.vue`
+
+**Files:**
+- Modify: `app/components/malio/date/internal/CalendarField.vue`
+
+**Interfaces:**
+- Consumes: `useCalendarPopover` (`goToHigherView`), `useCalendarView` (`yearPageStart`, `selectYear`), `CalendarHeader` (prop `yearPageStart`), `MonthPicker` (props `currentYear`/`min`/`max`), `YearPicker` (Task 5).
+- Produces: `CalendarField` accepte les props `min?: string` et `max?: string` ; rend les 3 vues.
+
+> Vérifié via l'e2e en Task 8 (pas de test colocalisé propre à `CalendarField`). Steps = implémentation + lint, le cycle test rouge/vert vit en Task 8.
+
+- [ ] **Step 1: Add min/max props**
+
+Dans le bloc `defineProps`, ajouter `min` et `max` (après `reserveMessageSpace?: boolean`) :
+```ts
+ reserveMessageSpace?: boolean
+ min?: string
+ max?: string
+```
+Et dans `withDefaults({...})`, après `reserveMessageSpace: true,` :
+```ts
+ reserveMessageSpace: true,
+ min: undefined,
+ max: undefined,
+```
+
+- [ ] **Step 2: Import YearPicker and update composable destructuring**
+
+Après `import MonthPicker from './MonthPicker.vue'` :
+```ts
+import YearPicker from './YearPicker.vue'
+```
+Remplacer les deux lignes de destructuration (≈218-219) :
+```ts
+const {isOpen, viewMode, open, close: closePopover, goToHigherView} = useCalendarPopover(root)
+const {currentMonth, currentYear, yearPageStart, goToPrev, goToNext, selectMonth, selectYear, syncToIso} = useCalendarView(viewMode)
+```
+
+- [ ] **Step 3: Update the popover template**
+
+Remplacer le bloc `` + `` + `` (≈85-103) par :
+```vue
+
+
+
+
+```
+
+- [ ] **Step 4: Update handlers**
+
+`goToHigherView` ne sert qu'au header (zoom arrière). À la **sélection**, on redescend d'un niveau. `useCalendarPopover` n'expose pas de setter de vue, mais `viewMode` est un `ref` déstructuré et donc directement mutable. Remplacer `onSelectMonth` (≈324-327) par les deux handlers suivants :
+```ts
+const onSelectMonth = (m: number) => {
+ selectMonth(m)
+ viewMode.value = 'days'
+}
+
+const onSelectYear = (y: number) => {
+ selectYear(y)
+ viewMode.value = 'months'
+}
+```
+
+- [ ] **Step 5: Lint**
+
+Run: `npx eslint app/components/malio/date/internal/CalendarField.vue`
+Expected: aucune erreur (notamment, `viewMode` est bien mutable car c'est un `ref`).
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add app/components/malio/date/internal/CalendarField.vue
+git commit -m "feat : intégration du sélecteur d'année dans CalendarField (#date-year-picker)
+
+Co-Authored-By: Claude Opus 4.8 (1M context) "
+```
+
+---
+
+### Task 8: Binder min/max chez les consommateurs + e2e
+
+**Files:**
+- Modify: `app/components/malio/date/Date.vue`
+- Modify: `app/components/malio/date/DateRange.vue`
+- Modify: `app/components/malio/date/DateTime.vue`
+- Modify: `app/components/malio/date/DateWeek.vue`
+- Test: `app/components/malio/date/Date.test.ts`
+
+**Interfaces:**
+- Consumes: `CalendarField` props `min`/`max` (Task 7).
+- Produces: les 4 composants propagent leurs bornes au popover. API publique inchangée.
+
+- [ ] **Step 1: Write the failing e2e test**
+
+Dans `Date.test.ts`, à l'intérieur du `describe('vue mois', …)` (ou un nouveau `describe('vue années')`), ajouter :
+```ts
+ describe('vue années', () => {
+ it('opens the year picker on second header toggle', async () => {
+ const wrapper = mountDate()
+ await wrapper.get('[data-test="date-input"]').trigger('click')
+ await wrapper.get('[data-test="header-toggle"]').trigger('click') // -> mois
+ await wrapper.get('[data-test="header-toggle"]').trigger('click') // -> années
+ expect(wrapper.find('[data-test="year-picker"]').exists()).toBe(true)
+ expect(wrapper.get('[data-test="header-toggle"]').text()).toContain('2021 – 2032')
+ })
+
+ it('navigates days -> months -> years -> months -> days', async () => {
+ const wrapper = mountDate()
+ await wrapper.get('[data-test="date-input"]').trigger('click')
+ await wrapper.get('[data-test="header-toggle"]').trigger('click')
+ await wrapper.get('[data-test="header-toggle"]').trigger('click')
+ await wrapper.get('[data-test="year"][data-year="2024"]').trigger('click') // -> mois 2024
+ expect(wrapper.find('[data-test="month-picker"]').exists()).toBe(true)
+ expect(wrapper.get('[data-test="header-toggle"]').text()).toContain('2024')
+ await wrapper.get('[data-test="month"][data-month="0"]').trigger('click') // -> jours
+ expect(wrapper.find('[data-test="month-picker"]').exists()).toBe(false)
+ expect(wrapper.get('[data-test="header-toggle"]').text()).toContain('Janvier 2024')
+ expect(wrapper.emitted('update:modelValue')).toBeUndefined()
+ })
+
+ it('paginates the year window with chevrons', async () => {
+ const wrapper = mountDate()
+ await wrapper.get('[data-test="date-input"]').trigger('click')
+ await wrapper.get('[data-test="header-toggle"]').trigger('click')
+ await wrapper.get('[data-test="header-toggle"]').trigger('click')
+ await wrapper.get('[data-test="header-next"]').trigger('click')
+ expect(wrapper.get('[data-test="header-toggle"]').text()).toContain('2033 – 2044')
+ })
+
+ it('greys out years outside [min, max]', async () => {
+ const wrapper = mountDate({min: '2025-01-01', max: '2027-12-31'})
+ await wrapper.get('[data-test="date-input"]').trigger('click')
+ await wrapper.get('[data-test="header-toggle"]').trigger('click')
+ await wrapper.get('[data-test="header-toggle"]').trigger('click')
+ expect(wrapper.get('[data-test="year"][data-year="2024"]').attributes('disabled')).toBeDefined()
+ expect(wrapper.get('[data-test="year"][data-year="2026"]').attributes('disabled')).toBeUndefined()
+ })
+ })
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `npx vitest run app/components/malio/date/Date.test.ts`
+Expected: FAIL — `year-picker` introuvable (Date.vue ne passe pas encore `min`/`max` à `CalendarField`).
+
+- [ ] **Step 3: Bind min/max on each consumer's ``**
+
+Dans `Date.vue`, `DateRange.vue`, `DateWeek.vue` : ajouter sur la balise racine `` (au même niveau que les autres props, ex. après `:readonly="readonly"`) :
+```vue
+ :min="min"
+ :max="max"
+```
+Dans `DateTime.vue` : ajouter (les bornes y sont des `YYYY-MM-DDTHH:MM`, tronquer à la date) :
+```vue
+ :min="min?.slice(0, 10)"
+ :max="max?.slice(0, 10)"
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `npx vitest run app/components/malio/date/Date.test.ts`
+Expected: PASS (anciens + nouveaux).
+
+- [ ] **Step 5: Run the whole date suite (non-régression)**
+
+Run: `npx vitest run app/components/malio/date/`
+Expected: PASS — `Date`, `DateRange`, `DateTime`, `DateWeek`, composables, pickers. (En cas de timeout flaky WSL2, relancer ; voir Global Constraints.)
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add app/components/malio/date/Date.vue app/components/malio/date/DateRange.vue app/components/malio/date/DateTime.vue app/components/malio/date/DateWeek.vue app/components/malio/date/Date.test.ts
+git commit -m "feat : propage min/max au popover + e2e sélecteur d'année (#date-year-picker)
+
+Co-Authored-By: Claude Opus 4.8 (1M context) "
+```
+
+---
+
+### Task 9: Documentation (`COMPONENTS.md`, `CHANGELOG.md`)
+
+**Files:**
+- Modify: `COMPONENTS.md`
+- Modify: `CHANGELOG.md`
+
+**Interfaces:**
+- Consumes: comportement livré aux tasks 1-8.
+- Produces: doc à jour (convention projet : maj manuelle).
+
+- [ ] **Step 1: Locate the date section in COMPONENTS.md**
+
+Run: `grep -nE "MalioDate|Date|calendrier|sélecteur" COMPONENTS.md | head -20`
+Expected: repère la section calendrier/date. Si aucune entrée, l'ajouter à la suite des autres composants date.
+
+- [ ] **Step 2: Document the 3-level navigation**
+
+Ajouter (dans la section du calendrier date) une description du 3ᵉ niveau :
+```markdown
+Le calendrier propose trois niveaux de navigation : jours → clic sur l'en-tête →
+sélecteur de mois → nouveau clic sur l'en-tête → sélecteur d'année (grille de 12 ans,
+chevrons par pas de 12 ans). Les props `min`/`max` grisent les mois et les années hors
+plage. Sélectionner une année revient au sélecteur de mois, sélectionner un mois revient
+à la grille de jours.
+```
+
+- [ ] **Step 3: Add a CHANGELOG entry**
+
+Sous la section non publiée (ou en tête, selon le format existant — vérifier `head -20 CHANGELOG.md`) :
+```markdown
+- Calendrier (Date/DateRange/DateTime/DateWeek) : sélecteur d'année (3ᵉ niveau) et
+ grisage des mois/années hors `min`/`max`.
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add COMPONENTS.md CHANGELOG.md
+git commit -m "docs : sélecteur d'année dans le calendrier (#date-year-picker)
+
+Co-Authored-By: Claude Opus 4.8 (1M context) "
+```
+
+---
+
+## Notes de vérification finale
+
+- Suite ciblée complète : `npx vitest run app/components/malio/date/`
+- Lint global : `npm run lint`
+- Vérif manuelle navigateur : **proposer** à l'utilisateur (ne pas lancer le MCP Chrome sans accord — coût tokens). Parcours : ouvrir un `MalioDate`, cliquer le header 2× → grille d'années, paginer, choisir une année puis un mois, et tester un champ avec `min`/`max` pour voir le grisage.