diff --git a/docs/superpowers/specs/2026-06-09-maliodate-saisie-manuelle-design.md b/docs/superpowers/specs/2026-06-09-maliodate-saisie-manuelle-design.md new file mode 100644 index 0000000..d9e9e10 --- /dev/null +++ b/docs/superpowers/specs/2026-06-09-maliodate-saisie-manuelle-design.md @@ -0,0 +1,118 @@ +# MalioDate — saisie manuelle au clavier + +**Date :** 2026-06-09 +**Statut :** Validé, prêt pour plan d'implémentation +**Périmètre :** `MalioDate` uniquement (la famille `DateTime`/`DateRange`/`DateWeek` n'est pas concernée pour l'instant). + +## Objectif + +Permettre à l'utilisateur de saisir une date au clavier (`JJ/MM/AAAA`) dans `MalioDate`, en plus de la sélection via le calendrier. Aujourd'hui l'`` de `CalendarField` est codé en dur en `readonly` : seule la sélection au calendrier est possible. + +## Décisions d'UX (validées) + +| Sujet | Décision | +|-------|----------| +| Ouverture du popover | Le **focus** (ou le clic) ouvre le calendrier, tout en laissant taper en même temps. | +| Masque / validation | Masque `maska` `##/##/####` pendant la frappe ; **validation au blur** (pas à chaque touche). | +| Activation | **Opt-in** via une prop `editable` (défaut `false`). Aucune régression pour les consommateurs existants. | +| Saisie invalide au blur | On **garde le texte tapé** et on affiche un **état d'erreur visuel** (bordure rouge + message). | +| Message d'erreur par défaut | « **Date invalide** » (couvre aussi le hors-bornes min/max), surchargeable via prop. | +| Touche Entrée | Déclenche le `commit` (parse immédiat) + ferme le popover. | + +## Approche retenue + +**Mode `editable` dans `CalendarField`, parsing dans `MalioDate`.** + +`CalendarField` reste agnostique au format : il expose un mode éditable (input non `readonly`, masque, buffer local) et émet du texte brut. `MalioDate` conserve toute la logique propre à la date (parse, validation `min`/`max`, état d'erreur). Cela évite de coupler `CalendarField` à un format date spécifique et garde le terrain prêt pour une éventuelle extension future à la famille Date. + +Approches écartées : +- **`CalendarField` générique avec fonction `parse` injectée** : trop générique pour le périmètre actuel (YAGNI). +- **`MalioDate` gère son propre ``** : duplication du rendu / label flottant / styles de `CalendarField`. + +## Conception détaillée + +### 1. `MalioDate` — props ajoutées + +- `editable?: boolean` — défaut `false`. Active la saisie clavier. +- `invalidMessage?: string` — défaut `'Date invalide'`. Message affiché en cas de saisie invalide/hors-bornes. + +Quand `editable === false`, le comportement est **strictement identique** à aujourd'hui (lecture seule, sélection calendrier uniquement). + +### 2. `CalendarField` — mode éditable + +Ajout d'une prop `editable?: boolean` (défaut `false`). Quand `true` : + +- L'`` perd l'attribut `readonly` et reçoit `v-maska="'##/##/####'"`. +- Un buffer local `draft` (ref) alimente l'input : `:value="editable ? draft : displayValue"`. +- `draft` est **resynchronisé** sur `displayValue` via un `watch` → couvre la sélection au calendrier, le clear, et tout changement externe de `modelValue`. Cette resynchro **efface aussi l'état d'erreur** côté `MalioDate` (via le nouveau `displayValue` émis). +- À la frappe (`@input`) : met à jour `draft` et émet `input(text)`. **Pas de validation** à ce stade. +- Au blur (`@blur`) : émet `commit(text)`. +- À la touche Entrée (`@keydown.enter`) : émet `commit(text)` + ferme le popover. +- `@focus` ouvre le popover, tout en laissant taper (input non `readonly`). + +Quand `editable === false`, aucun de ces comportements ne s'applique : le chemin de code actuel reste inchangé. + +`disabled` et `readonly` priment toujours sur `editable` (champ non éditable). + +### 3. `MalioDate` — parsing, validation, état d'erreur + +Une ref locale `internalError` est fusionnée avec la prop `error` du consommateur et transmise à `CalendarField` : +`:error="error || internalError"` (l'erreur métier du consommateur reste prioritaire). + +Sur réception de `commit(text)` : + +- **Texte vide** → `emit('update:modelValue', null)` ; `internalError = ''`. +- **Valide** (`parseDisplayToIso(text)` non `null` **et** `isDateInRange(iso, min, max)`) → `emit('update:modelValue', iso)` ; `internalError = ''`. +- **Invalide ou hors-bornes** → on **n'émet pas** de nouveau `modelValue` ; `internalError = props.invalidMessage`. Le texte tapé reste affiché. + +L'état d'erreur s'efface dès qu'une saisie valide ou une sélection calendrier ultérieure produit un nouveau `displayValue`. + +## Flux de données + +``` +Frappe clavier + └─ CalendarField: maj draft + émet input(text) (pas de validation) +Blur / Entrée + └─ CalendarField: émet commit(text) + └─ MalioDate: parseDisplayToIso + isDateInRange + ├─ valide → emit update:modelValue(iso) ; internalError='' + ├─ vide → emit update:modelValue(null) ; internalError='' + └─ invalide→ internalError = invalidMessage ; (texte conservé) + +Sélection calendrier + └─ emit update:modelValue(iso) + └─ displayValue change → CalendarField resync draft → erreur effacée +``` + +## Réutilisation de l'existant + +Les helpers nécessaires existent déjà dans `app/components/malio/date/composables/dateFormat.ts` : +- `parseDisplayToIso(display)` → `string | null` +- `isValidIso(iso)` → `boolean` +- `isDateInRange(iso, min?, max?)` → `boolean` +- `formatIsoToDisplay(iso)` → `string` + +`maska` est déjà une dépendance du projet (utilisée par `InputText`/`InputPhone` via `v-maska` + `vMaska` de `maska/vue`). + +## Tests (`Date.test.ts`) + +- Frappe valide + blur → émet l'ISO attendu. +- Saisie invalide (`32/13/2026`) au blur → texte conservé, message « Date invalide », `aria-invalid`. +- Date valide hors `min`/`max` au blur → état d'erreur. +- Saisie vide au blur → émet `null`. +- Sélection au calendrier après une saisie invalide → erreur effacée, valeur mise à jour. +- Touche Entrée → commit + fermeture popover. +- `editable=false` (défaut) → input reste `readonly`, aucun nouveau comportement (non-régression). +- `invalidMessage` personnalisé → message affiché respecté. + +## Livrables documentaires + +- Mise à jour de `COMPONENTS.md` (props `editable`, `invalidMessage`). +- Entrée dans `CHANGELOG.md`. +- Mise à jour de la story Histoire + page playground de `MalioDate` pour exposer la prop `editable`. + +## Hors périmètre + +- Extension de la saisie manuelle à `DateTime`, `DateRange`, `DateWeek`. +- Saisie partielle « intelligente » (auto-complétion d'année, etc.). +- Validation à la frappe (on reste sur validation au blur).