fix(date) : borne la saisie clavier pour empêcher les dates absurdes (99/99/9999) #79

Merged
tristan merged 9 commits from feature/date-bounded-mask into develop 2026-06-19 13:04:12 +00:00
Owner

Problème

Sur la famille Date editable, le masque maska n'imposait que la forme (##/##/####). Une valeur structurellement absurde comme 99/99/9999 était donc saisissable, puis rejetée a posteriori par la validation. Le métier veut que ce soit impossible à taper.

Solution (masque borné + validation en filet)

  • composables/maskTemplate.tsbuildBoundedMask(template) : borne le premier chiffre de chaque champ (jour 0-3, mois 0-1, heure 0-2, minute 0-5). Distingue le mois des minutes (même lettre M) selon la présence d'heures dans le gabarit, pour ne pas brider la saisie des minutes du DateTime.
  • internal/CalendarField.vue — branche le builder dans maskaOptions (remplace le replace(/[A-Za-z]/g, '#')).

Les impossibilités plus fines (31/02, 29/02 non bissextile, hors min/max) restent captées par la validation (invalidMessage + update:valid=false).

Tests

  • maskTemplate.test.ts (5) — bornes par champ, structure du masque, non-confusion mois/minutes.
  • Date.test.ts — test invalidMessage adapté (32/13/2026, typable→invalide) + garde de non-régression : 99/99/9999 ne s'inscrit jamais et n'émet aucune date.
  • Suite complète : 1004/1004 verte (DateTime 36 incluse → saisie d'heure intacte).

Doc : COMPONENTS.md (MalioDate) + CHANGELOG.md (Fixed) à jour.

## Problème Sur la famille Date editable, le masque maska n'imposait que la *forme* (`##/##/####`). Une valeur structurellement absurde comme `99/99/9999` était donc **saisissable**, puis rejetée *a posteriori* par la validation. Le métier veut que ce soit **impossible à taper**. ## Solution (masque borné + validation en filet) - `composables/maskTemplate.ts` — `buildBoundedMask(template)` : borne le **premier chiffre de chaque champ** (jour `0-3`, mois `0-1`, heure `0-2`, minute `0-5`). Distingue le mois des minutes (même lettre `M`) selon la présence d'heures dans le gabarit, pour ne pas brider la saisie des minutes du DateTime. - `internal/CalendarField.vue` — branche le builder dans `maskaOptions` (remplace le `replace(/[A-Za-z]/g, '#')`). Les impossibilités plus fines (`31/02`, 29/02 non bissextile, hors `min`/`max`) restent captées par la **validation** (`invalidMessage` + `update:valid=false`). ## Tests - `maskTemplate.test.ts` (5) — bornes par champ, structure du masque, non-confusion mois/minutes. - `Date.test.ts` — test `invalidMessage` adapté (`32/13/2026`, typable→invalide) + garde de non-régression : `99/99/9999` ne s'inscrit jamais et n'émet aucune date. - Suite complète : **1004/1004 verte** (DateTime 36 incluse → saisie d'heure intacte). Doc : `COMPONENTS.md` (MalioDate) + `CHANGELOG.md` (Fixed) à jour.
tristan added 1 commit 2026-06-18 14:23:49 +00:00
Le masque maska n'imposait que la forme (##/##/####), donc 99/99/9999 était
saisissable puis rejeté a posteriori par la validation. Le métier veut que ce
soit impossible à taper.

buildBoundedMask(template) borne le premier chiffre de chaque champ (jour 0-3,
mois 0-1, heure 0-2, minute 0-5) ; il distingue le mois des minutes (même lettre
M) selon la présence d'heures. Les impossibilités fines (31/02, 29/02 non
bissextile, hors min/max) restent captées par la validation, en filet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tristan added 1 commit 2026-06-18 14:26:07 +00:00
Le DateTime partage CalendarField, donc buildBoundedMask bornait déjà sa saisie
(heure 0-2, minute 0-5) — couvert par maskTemplate.test.ts. Ce test rend la
garantie explicite côté composant : 99/99/9999 99:99 ne peut pas être tapé et
n'émet aucun datetime.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tristan added 1 commit 2026-06-18 14:35:42 +00:00
Le bornage par tokens ne contraignait que le 1er chiffre (positionnel, sans
mémoire du chiffre précédent) : 33 (jour) ou 19 (mois) restaient tapables.

Remplacé par un preProcess maska qui valide chaque champ progressivement : un
chiffre n'est accepté que s'il existe encore une complétion dans [min, max].
Borne donc le 1er ET le 2e chiffre ; les impossibilités calendaires fines
(31/02, 29/02 non bissextile, hors min/max) restent captées par la validation.

Tests d'intégration : 32/13 (désormais non tapable) remplacé par 31/02 comme
date « champs valides mais inexistante » ; garde sur l'exemple métier 33/19.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tristan added 1 commit 2026-06-19 06:49:31 +00:00
L'icône calendrier (overlay absolu) interceptait le clic sans le traiter et ne
le laissait pas retomber sur le @click de l'input. Ajout d'un handler onFieldClick
sur l'icône (+ cursor-pointer). Couvre Date, DateTime, DateRange, DateWeek via le
CalendarField partagé. La croix d'effacement garde son comportement (efface, n'ouvre pas).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tristan added 1 commit 2026-06-19 07:25:51 +00:00
Les liens passent le texte en m-primary au survol (hover:text-m-primary),
en plus du fond hover:bg-m-surface existant. Inclut l'alignement du test de
largeur sur la valeur actuelle (w-[232px]).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tristan added 1 commit 2026-06-19 08:09:14 +00:00
- Survol : highlight pleine largeur porté par le <li> (fond m-primary/10 +
  texte m-primary + semi-bold), espacement pt-1 pb-1. Couleur de base text-black
  sur le <li> et le <a> n'impose plus sa couleur (héritage) : sinon, sur les
  bandes pt-1/pb-1 hors du <a>, le fond passait bleu mais le texte restait noir.
- Lien actif : texte m-primary + semi-bold, sans fond (active-class avec
  !important car appliqué hors twMerge).
- Pas de transition sur le hover (texte + fond basculent ensemble, sans délai).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tristan added 1 commit 2026-06-19 12:06:32 +00:00
- Le nombre d'onglets affichés en mode fenêtré s'adapte automatiquement à la
  largeur réelle (ResizeObserver + ligne de mesure cachée). Les chevrons restent
  fixés aux bords ; le nombre est choisi pour que les onglets tiennent (pas de
  chevauchement ni de rognage). Calcul isolé en fonction pure testable (tabFit.ts,
  basée sur les vraies largeurs). maxVisibleTabs devient un plafond optionnel.
- BREAKING : suppression de la prop maxWidth (la barre prend toute la largeur).
- Survol d'un onglet inactif : même style que l'actif (texte m-primary + barre).
- Playground : bac à sable interactif (nb onglets, plafond, icônes, labels longs,
  cadre redimensionnable) pour tester tous les cas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tristan added 1 commit 2026-06-19 12:53:49 +00:00
Calqué sur InputText (texte + label grisés, cursor-not-allowed, aucune affordance
interactive). Quand disabled :
- bouton « + » d'ajout masqué (InputPhone, InputEmail)
- œil de révélation masqué (InputPassword)
- chevron masqué (Select, SelectCheckbox, InputAutocomplete)
- croix d'effacement déjà masquée (date, upload, time)
- label en text-m-muted (Select, SelectCheckbox, CalendarField → famille Date, TimePicker, InputNumber aligné)
- tags du SelectCheckbox + valeur du Select grisés ; icône horloge (TimePicker) et icône calendrier (date, même rempli) grisées

Playground : page « Champs disabled » (DIVERS) en miroir de la page readonly,
avec le SelectCheckbox en version tags. Tests par composant ajoutés/adaptés.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tristan added 1 commit 2026-06-19 12:57:12 +00:00
tristan merged commit be3d88ed45 into develop 2026-06-19 13:04:12 +00:00
tristan deleted branch feature/date-bounded-mask 2026-06-19 13:04:12 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: MALIO-DEV/malio-layer-ui#79