diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf4702d..904ce6f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -51,7 +51,9 @@ Liste des évolutions de la librairie Malio layer UI
* [#MUI-42] InputUpload : prop `clearable` (croix `mdi:close` focusable qui vide le champ + event `clear`) et ouverture du sélecteur de fichier au clavier (Entrée / Espace)
* [#MUI-42] Famille Date : ouverture du calendrier au clavier (Entrée / Espace), fermeture par Échap
* [#MUI-43] MalioDate : event `update:valid` (booléen) exposant l'état de validité de la saisie (`false` sur date malformée ou hors `min`/`max`, qui n'émet pas `modelValue`) — permet au parent de bloquer le submit ; la validité ne couvre pas `required` (champ vide = valide)
-* [#MUI-43] MalioDateTime : saisie clavier `JJ/MM/AAAA HH:MM` optionnelle (prop `editable`, masque maska, `invalidMessage`) + même event `update:valid` que MalioDate (mêmes règles, émis dès le montage). CalendarField : masque maska configurable via prop `mask` (défaut `##/##/####` inchangé). Nouveau parseur `parseDisplayToIsoDateTime`.
+* [#MUI-43] MalioDateTime : saisie clavier `JJ/MM/AAAA HH:MM` optionnelle (prop `editable`, masque maska, `invalidMessage`) + même event `update:valid` que MalioDate (mêmes règles, émis dès le montage). Nouveau parseur `parseDisplayToIsoDateTime`.
+* [#MUI-43] Famille Date editable (MalioDate, MalioDateTime) : gabarit fantôme progressif — le format (`JJ/MM/AAAA` / `JJ/MM/AAAA HH:MM`) s'affiche en gris et se remplit au fil de la saisie (tapé en noir, reste en gris) ; séparateurs (`/`, espace, `:`) posés automatiquement dès qu'un groupe est complet (maska `eager`). CalendarField : prop `placeholderTemplate` (le masque maska en est dérivé), remplace l'ancienne mécanique de masque codé en dur.
+* [#MUI-43] CalendarField : la croix d'effacement réinitialise désormais la saisie clavier même après une date invalide (le `v-model` restant `null`, le champ se vidait pas).
### Changed
* DataTable : libellés de pagination en français — `Préc.` / `Suiv.` (étaient `Prev` / `Next`) ; aria-labels déjà en français inchangés.
diff --git a/COMPONENTS.md b/COMPONENTS.md
index 3f268d6..0df8e98 100644
--- a/COMPONENTS.md
+++ b/COMPONENTS.md
@@ -505,7 +505,7 @@ Sélecteur de date unique avec popover (grille de calendrier + vue mois/année).
La valeur est une chaîne ISO `"YYYY-MM-DD"`. Cliquer un jour émet la date et ferme le popover.
-Avec `editable`, l'utilisateur peut aussi taper la date au clavier. La valeur n'est émise qu'au blur (ou sur Entrée) si elle est valide et dans les bornes ; sinon le texte est conservé et le champ passe en erreur (`invalidMessage`).
+Avec `editable`, l'utilisateur peut aussi taper la date au clavier. La valeur n'est émise qu'au blur (ou sur Entrée) si elle est valide et dans les bornes ; sinon le texte est conservé et le champ passe en erreur (`invalidMessage`). Un **gabarit fantôme** affiche le format `JJ/MM/AAAA` en gris et se remplit au fur et à mesure de la saisie (caractères tapés en noir, reste du gabarit en gris).
L'event `update:valid` remonte l'état de validité de la saisie au parent (`true` = vide ou date valide dans les bornes ; `false` = saisie malformée ou hors `min`/`max`). Il est émis **dès le montage** (état d'un champ pré-rempli connu sans interaction) puis à chaque transition. Il permet d'agréger la validité des champs date dans la gate de submit d'un formulaire — une saisie invalide n'émettant pas `modelValue`, c'est le seul signal disponible côté parent. La validité ne couvre **pas** l'obligation `required` (un champ vide reste valide), qui reste à la charge du parent.
@@ -698,7 +698,7 @@ La valeur est une chaîne **ISO naïve sans fuseau** au format `"YYYY-MM-DDTHH:M
Flux : cliquer un jour fixe la date (heure par défaut `00:00`), régler l'heure met à jour l'heure ; le popover se ferme au clic extérieur. La valeur est émise en direct à chaque interaction.
-Avec `editable`, l'utilisateur peut aussi taper `JJ/MM/AAAA HH:MM` au clavier. La valeur n'est émise qu'au blur (ou sur Entrée) si elle est valide et dans les bornes ; sinon le texte est conservé et le champ passe en erreur (`invalidMessage`). L'event `update:valid` (booléen) — émis **dès le montage** puis à chaque transition — remonte l'état de validité au parent (`false` = saisie malformée ou hors `min`/`max`, qui n'émet pas `modelValue`), pour bloquer un submit. La validité ne couvre **pas** `required` (champ vide = valide), comme sur `MalioDate`.
+Avec `editable`, l'utilisateur peut aussi taper `JJ/MM/AAAA HH:MM` au clavier. La valeur n'est émise qu'au blur (ou sur Entrée) si elle est valide et dans les bornes ; sinon le texte est conservé et le champ passe en erreur (`invalidMessage`). Un **gabarit fantôme** affiche le format en gris et se remplit au fil de la saisie (cf. MalioDate). L'event `update:valid` (booléen) — émis **dès le montage** puis à chaque transition — remonte l'état de validité au parent (`false` = saisie malformée ou hors `min`/`max`, qui n'émet pas `modelValue`), pour bloquer un submit. La validité ne couvre **pas** `required` (champ vide = valide), comme sur `MalioDate`.
```vue
diff --git a/app/components/malio/date/Date.test.ts b/app/components/malio/date/Date.test.ts
index 4cb9cf6..1bd6676 100644
--- a/app/components/malio/date/Date.test.ts
+++ b/app/components/malio/date/Date.test.ts
@@ -354,6 +354,55 @@ describe('MalioDate', () => {
})
})
+ describe('gabarit de saisie (editable)', () => {
+ it('affiche le gabarit complet en gris quand editable + focus + vide', async () => {
+ const wrapper = mountDate({editable: true})
+ await wrapper.get('[data-test="date-input"]').trigger('focus')
+ const ghost = wrapper.get('[data-test="format-ghost"]')
+ expect(ghost.text()).toBe('JJ/MM/AAAA')
+ expect(wrapper.get('[data-test="ghost-remaining"]').classes()).toContain('text-m-muted')
+ })
+
+ it('remplit le gabarit au fur et à mesure de la saisie', async () => {
+ const wrapper = mountDate({editable: true})
+ const input = wrapper.get('[data-test="date-input"]')
+ await input.trigger('focus')
+ await input.setValue('19')
+ // eager : le séparateur se pose dès que le groupe est complet (« 19 » → « 19/ »)
+ expect(wrapper.get('[data-test="format-ghost"]').text()).toBe('19/MM/AAAA')
+ expect(wrapper.get('[data-test="ghost-typed"]').text()).toBe('19/')
+ expect(wrapper.get('[data-test="ghost-typed"]').classes()).toContain('text-black')
+ })
+
+ it('pose le séparateur automatiquement dès qu\'un groupe est complet (eager)', async () => {
+ const wrapper = mountDate({editable: true})
+ const input = wrapper.get('[data-test="date-input"]')
+ await input.setValue('1905')
+ expect((input.element as HTMLInputElement).value).toBe('19/05/')
+ })
+
+ it('n\'affiche pas de gabarit en mode non editable', async () => {
+ const wrapper = mountDate({modelValue: '2026-05-19'})
+ await wrapper.get('[data-test="date-input"]').trigger('click')
+ expect(wrapper.find('[data-test="format-ghost"]').exists()).toBe(false)
+ })
+
+ it('n\'affiche pas de gabarit quand editable mais vide et non focus', () => {
+ const wrapper = mountDate({editable: true})
+ expect(wrapper.find('[data-test="format-ghost"]').exists()).toBe(false)
+ })
+
+ it('vide le champ au clic sur la croix même après une saisie invalide (modelValue déjà null)', async () => {
+ const wrapper = mountDate({editable: true})
+ const input = wrapper.get('[data-test="date-input"]')
+ await input.setValue('32/13/2026')
+ await input.trigger('blur')
+ expect((input.element as HTMLInputElement).value).toBe('32/13/2026')
+ await wrapper.get('[data-test="clear"]').trigger('click')
+ expect((input.element as HTMLInputElement).value).toBe('')
+ })
+ })
+
describe('état de validité (update:valid)', () => {
it('émet valid=true au montage avec une valeur valide', () => {
const wrapper = mountDate({modelValue: '2026-05-19'})
diff --git a/app/components/malio/date/DateTime.test.ts b/app/components/malio/date/DateTime.test.ts
index 5bbc31e..1b0c7b5 100644
--- a/app/components/malio/date/DateTime.test.ts
+++ b/app/components/malio/date/DateTime.test.ts
@@ -200,6 +200,30 @@ describe('MalioDateTime', () => {
})
})
+ describe('gabarit de saisie (editable)', () => {
+ it('affiche le gabarit date+heure complet en gris quand editable + focus + vide', async () => {
+ const wrapper = mountDateTime({editable: true})
+ await wrapper.get('[data-test="date-input"]').trigger('focus')
+ expect(wrapper.get('[data-test="format-ghost"]').text().replace(/\xa0/g, ' ')).toBe('JJ/MM/AAAA HH:MM')
+ expect(wrapper.get('[data-test="ghost-remaining"]').classes()).toContain('text-m-muted')
+ })
+
+ it('remplit le gabarit au fur et à mesure de la saisie', async () => {
+ const wrapper = mountDateTime({editable: true})
+ const input = wrapper.get('[data-test="date-input"]')
+ await input.trigger('focus')
+ await input.setValue('190520')
+ expect(wrapper.get('[data-test="format-ghost"]').text().replace(/\xa0/g, ' ')).toBe('19/05/20AA HH:MM')
+ expect(wrapper.get('[data-test="ghost-typed"]').text()).toBe('19/05/20')
+ })
+
+ it('n\'affiche pas de gabarit en mode non editable', async () => {
+ const wrapper = mountDateTime({modelValue: '2026-05-20T14:30:00'})
+ await wrapper.get('[data-test="date-input"]').trigger('click')
+ expect(wrapper.find('[data-test="format-ghost"]').exists()).toBe(false)
+ })
+ })
+
describe('état de validité (update:valid)', () => {
it('émet valid=true au montage avec une valeur valide', () => {
const wrapper = mountDateTime({modelValue: '2026-05-20T14:30:00'})
diff --git a/app/components/malio/date/DateTime.vue b/app/components/malio/date/DateTime.vue
index 34574b3..f73259e 100644
--- a/app/components/malio/date/DateTime.vue
+++ b/app/components/malio/date/DateTime.vue
@@ -14,7 +14,7 @@
:success="success"
:clearable="clearable"
:editable="editable"
- mask="##/##/#### ##:##"
+ placeholder-template="JJ/MM/AAAA HH:MM"
:input-class="inputClass"
:label-class="labelClass"
:group-class="groupClass"
diff --git a/app/components/malio/date/internal/CalendarField.vue b/app/components/malio/date/internal/CalendarField.vue
index 9b5555e..9becf17 100644
--- a/app/components/malio/date/internal/CalendarField.vue
+++ b/app/components/malio/date/internal/CalendarField.vue
@@ -29,6 +29,19 @@
@keydown="onKeydown"
>
+