Files
malio-layer-ui/app/components/malio/date/composables/dateFormat.ts
T
tristan 41010060ff feat : sélecteur d'année dans le calendrier (3ᵉ niveau) (#83)
## Sélecteur d'année dans le calendrier (3ᵉ niveau de navigation)

Ajoute un 3ᵉ niveau de navigation à la famille de composants date, et corrige le bornage min/max du sélecteur de mois.

### Comportement
- Clic sur le champ → calendrier (vue **jours**)
- Clic sur l'en-tête → **sélecteur de mois**
- **Re-clic sur l'en-tête → sélecteur d'année** (grille de 12 ans, chevrons paginant par pas de 12 ans, fenêtre centrée sur l'année courante − 5)
- Clic sur une année → retour au sélecteur de mois ; clic sur un mois → retour à la grille de jours
- Les props `min`/`max` **grisent les mois ET les années** hors plage (corrige l'asymétrie : le `MonthPicker` affichait jusqu'ici tous les mois)

En-tête contextuel : « Mai 2026 » (jours) / « 2026 » (mois) / « 2020 – 2031 » (années).

### Périmètre
- Shell partagé `internal/CalendarField.vue` → bénéficie aux 4 composants publics `Date`, `DateRange`, `DateTime`, `DateWeek`
- **Aucune API publique modifiée**
- Nouveau composant `internal/YearPicker.vue` (calqué sur `MonthPicker`)
- Helpers purs `isMonthInRange` / `isYearInRange` (comparaison par préfixe ISO, bornes inclusives)
- State machine `viewMode` à 3 niveaux (`useCalendarPopover` / `useCalendarView`)

### Tests
- Suite date **246/246 verte**, ESLint propre
- Unitaires : helpers, `YearPicker`, `MonthPicker` (grisage), composables (pagination ±12, recentrage, `selectYear`)
- e2e `Date.test.ts` : flux complet jours→mois→années→mois→jours + grisage min/max

### Process
Développé en brainstorming → spec → plan → exécution TDD (un commit par étape). Spec et plan inclus sous `docs/superpowers/`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: #83
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-06-22 09:28:55 +00:00

40 lines
1.3 KiB
TypeScript

export function isValidIso(iso: string): boolean {
if (!/^\d{4}-\d{2}-\d{2}$/.test(iso)) return false
const [y, m, d] = iso.split('-').map(Number)
const date = new Date(y, m - 1, d)
return date.getFullYear() === y && date.getMonth() === m - 1 && date.getDate() === d
}
export function formatIsoToDisplay(iso: string | null): string {
if (!iso || !isValidIso(iso)) return ''
const [y, m, d] = iso.split('-')
return `${d}/${m}/${y}`
}
export function parseDisplayToIso(display: string): string | null {
const match = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(display.trim())
if (!match) return null
const [, dd, mm, yyyy] = match
const iso = `${yyyy}-${mm}-${dd}`
return isValidIso(iso) ? iso : null
}
export function isDateInRange(iso: string, min?: string, max?: string): boolean {
if (min && iso < min) return false
if (max && iso > max) return false
return true
}
export function isMonthInRange(year: number, month: number, min?: string, max?: string): boolean {
const ym = `${year}-${String(month + 1).padStart(2, '0')}`
if (min && ym < min.slice(0, 7)) return false
if (max && ym > max.slice(0, 7)) return false
return true
}
export function isYearInRange(year: number, min?: string, max?: string): boolean {
if (min && year < Number(min.slice(0, 4))) return false
if (max && year > Number(max.slice(0, 4))) return false
return true
}