From 06c90f7fb2322ef05b7fb7340f59729f8b2ba930 Mon Sep 17 00:00:00 2001 From: tristan Date: Tue, 9 Jun 2026 11:36:00 +0200 Subject: [PATCH 01/38] docs(date) : spec saisie manuelle MalioDate --- ...-06-09-maliodate-saisie-manuelle-design.md | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-09-maliodate-saisie-manuelle-design.md 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). -- 2.39.5 From 36940139b91eb5e8d51958b32c0fad5abc66d200 Mon Sep 17 00:00:00 2001 From: tristan Date: Tue, 9 Jun 2026 11:46:25 +0200 Subject: [PATCH 02/38] =?UTF-8?q?docs(date)=20:=20plan=20impl=C3=A9mentati?= =?UTF-8?q?on=20saisie=20manuelle=20MalioDate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-06-09-maliodate-saisie-manuelle.md | 635 ++++++++++++++++++ 1 file changed, 635 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-09-maliodate-saisie-manuelle.md diff --git a/docs/superpowers/plans/2026-06-09-maliodate-saisie-manuelle.md b/docs/superpowers/plans/2026-06-09-maliodate-saisie-manuelle.md new file mode 100644 index 0000000..6c1dfa7 --- /dev/null +++ b/docs/superpowers/plans/2026-06-09-maliodate-saisie-manuelle.md @@ -0,0 +1,635 @@ +# MalioDate — saisie manuelle au clavier — 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:** Permettre la saisie clavier `JJ/MM/AAAA` dans `MalioDate` (opt-in via prop `editable`), en plus de la sélection au calendrier, avec validation au blur et état d'erreur visuel. + +**Architecture:** `CalendarField` (interne, partagé) gagne un mode `editable` : input non `readonly`, masque `maska`, buffer local `draft` synchronisé sur `displayValue`, émission d'un event `commit(text)` au blur / à Entrée. `MalioDate` conserve toute la logique date : parse (`parseDisplayToIso`), validation bornes (`isDateInRange`), état d'erreur interne fusionné avec la prop `error` du consommateur. `CalendarField` reste agnostique au format. + +**Tech Stack:** Nuxt 4 layer, Vue 3 ` diff --git a/app/story/date/datePicker.story.vue b/app/story/date/datePicker.story.vue index 5c6248f..936b56a 100644 --- a/app/story/date/datePicker.story.vue +++ b/app/story/date/datePicker.story.vue @@ -28,6 +28,16 @@ /> +
+

Saisie clavier (editable)

+ +
+

Non effaçable

(null) const initialValue = ref(todayIso) const boundedValue = ref(null) const errorValue = ref(null) +const editableValue = ref(null) -- 2.39.5 From f926ae830c729e2a2d81a7d4ee678e3f0eaccf31 Mon Sep 17 00:00:00 2001 From: tristan Date: Tue, 9 Jun 2026 12:38:18 +0200 Subject: [PATCH 09/38] docs(email) : spec bouton + d'ajout sur MalioInputEmail --- ...26-06-09-inputemail-bouton-ajout-design.md | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-09-inputemail-bouton-ajout-design.md diff --git a/docs/superpowers/specs/2026-06-09-inputemail-bouton-ajout-design.md b/docs/superpowers/specs/2026-06-09-inputemail-bouton-ajout-design.md new file mode 100644 index 0000000..49fb14f --- /dev/null +++ b/docs/superpowers/specs/2026-06-09-inputemail-bouton-ajout-design.md @@ -0,0 +1,157 @@ +# MalioInputEmail — bouton « + » d'ajout (event `add`) + +**Date :** 2026-06-09 +**Statut :** Validé, prêt pour plan d'implémentation +**Périmètre :** `MalioInputEmail` uniquement. + +## Objectif + +Ajouter à `MalioInputEmail` le même bouton « + » que `MalioInputPhone` : un bouton optionnel à droite du champ qui émet un event `add`, permettant au consommateur d'ajouter dynamiquement un autre champ email (ou toute autre action). + +La logique email existante (ajoutée en MUI-41) est **conservée intégralement** : `sanitizeEmail` (suppression des espaces), prop `lowercase`, gestion du caret dans `onInput`, `type="email"` / `inputmode="email"`. Ce travail n'y touche pas. + +## Décisions validées + +| Sujet | Décision | +|-------|----------| +| API | Calquée sur `MalioInputPhone` : props `addable` / `addIconName` / `addButtonLabel`, event `add`, `data-test="add-button"`. | +| Collision icône/bouton | L'icône email étant à droite par défaut, quand `addable` est actif l'icône passe **automatiquement à gauche** et le « + » occupe la droite (disposition éprouvée de Phone). | +| Libellé par défaut | `addButtonLabel` défaut `'Ajouter une adresse email'`. | +| Garde désactivé | Le bouton n'émet pas `add` si `disabled` ou `readonly` (comme Phone). | +| Approche | Recopier le pattern de Phone dans Email (pas d'extraction d'un composant partagé — noté comme cleanup futur possible, hors scope). | + +## Conception détaillée + +### 1. Props ajoutées (interface + défauts) + +```ts + addable?: boolean // défaut: false + addIconName?: string // défaut: 'mdi:plus' + addButtonLabel?: string // défaut: 'Ajouter une adresse email' +``` + +### 2. Event ajouté + +L'`defineEmits` passe de : +```ts +const emit = defineEmits<{ + (event: 'update:modelValue', value: string): void +}>() +``` +à : +```ts +const emit = defineEmits<{ + (event: 'update:modelValue', value: string): void + (event: 'add'): void +}>() +``` + +### 3. Position d'icône « effective » + +Nouvelle règle, unique source du repositionnement : +```ts +const effectiveIconPosition = computed(() => + props.addable && props.iconName ? 'left' : props.iconPosition, +) +``` + +Les quatre computeds qui dépendent aujourd'hui de `props.iconPosition` utilisent désormais `effectiveIconPosition.value` : +- `iconInputPaddingClass` +- `iconPositionClass` +- `labelPositionClass` +- `focusPaddingClass` + +Conséquence : +- `addable=false` → `effectiveIconPosition === props.iconPosition` → comportement **strictement identique** à aujourd'hui. +- `addable=true` (avec une icône) → icône à gauche + espace réservé à droite pour le bouton. + +### 4. `iconInputPaddingClass` aligné sur Phone + +Remplacement de l'implémentation actuelle d'Email par la forme éprouvée de Phone (rendu identique dans le cas non-addable car la classe de base `pl-3 pr-3` est commune aux deux composants) : +```ts +const iconInputPaddingClass = computed(() => { + const leftIcon = props.iconName && effectiveIconPosition.value === 'left' + const rightIcon = props.iconName && effectiveIconPosition.value === 'right' + const parts: string[] = [] + if (leftIcon) parts.push('!pl-11') + if (rightIcon || props.addable) parts.push('!pr-10') + return parts.join(' ') +}) +``` + +### 5. Bouton dans le template + +Inséré après le bloc ``, à l'identique de Phone : +```html + +``` + +### 6. `mergedAddButtonClass` (copie de Phone) + +```ts +const mergedAddButtonClass = computed(() => + twMerge( + 'absolute right-[10px] top-1/2 -translate-y-1/2 cursor-pointer transition-opacity hover:opacity-70', + iconStateClass.value, + props.disabled ? 'cursor-not-allowed opacity-40 hover:opacity-40' : '', + ), +) +``` +(`iconStateClass` existe déjà dans Email.) + +### 7. Handler `onAdd` (copie de Phone) + +```ts +const onAdd = () => { + if (props.disabled || props.readonly) return + emit('add') +} +``` + +## Quatre computeds à modifier — détail + +Aujourd'hui dans `InputEmail.vue` ils référencent `props.iconPosition` ; ils doivent référencer `effectiveIconPosition.value`. Les corps restent identiques par ailleurs : +- `iconPositionClass` : `effectiveIconPosition.value === 'left' ? 'left-[10px]' : 'right-[10px]'` +- `labelPositionClass` : `props.iconName && effectiveIconPosition.value === 'left' ? 'left-11' : 'left-3'` +- `focusPaddingClass` : `props.iconName && effectiveIconPosition.value === 'left' ? 'focus:!pl-11' : 'focus:pl-[11px]'` +- `iconInputPaddingClass` : remplacé par la version §4. + +## Tests (`InputEmail.test.ts`) + +Ajouts (mirroring `InputPhone.test.ts`), sans toucher aux tests existants de sanitisation/lowercase : +- `addable=false` (défaut) → pas de `[data-test="add-button"]`. +- `addable=true` → `[data-test="add-button"]` présent. +- Clic sur le bouton → un event `add` émis (longueur 1). +- `addable + disabled` → clic n'émet pas `add` ; le bouton a l'attribut `disabled`. +- `addable + readonly` → clic n'émet pas `add` ; le bouton n'a PAS l'attribut natif `disabled` (la garde `onAdd` bloque). +- `addable=true` avec icône → l'icône email est positionnée à gauche (`left-[10px]` présent / `right-[10px]` absent sur l'icône `[data-test="icon"]`). +- Non-régression : avec `addable=false`, l'icône reste à droite (`right-[10px]`). +- `addButtonLabel` personnalisé → `aria-label` respecté ; défaut → `'Ajouter une adresse email'`. + +## Livrables documentaires + +- `COMPONENTS.md` : ajouter les lignes `addable` / `addIconName` / `addButtonLabel` et l'event `add()` dans la section `## MalioInputEmail`, plus un exemple ``. +- `CHANGELOG.md` : entrée sous `### Added`. +- Story `app/story/input/inputEmail.story.vue` : une carte « Addable » avec `@add` (calquée sur `inputPhone.story.vue`). +- Playground `.playground/pages/composant/input/inputEmail.vue` : un exemple `addable` avec un handler qui illustre l'ajout d'un champ. + +## Hors périmètre + +- Extraction d'un composant/bouton partagé entre Phone et Email (refactor dédié futur). +- Gestion réelle de la liste de champs email côté composant (c'est au consommateur de réagir à l'event `add`, comme pour Phone). +- Toute modification de la logique de sanitisation / `lowercase` / caret existante. -- 2.39.5 From 1296f62ccfcec9b970eb48a6e4515beb7b313fd4 Mon Sep 17 00:00:00 2001 From: tristan Date: Tue, 9 Jun 2026 12:46:17 +0200 Subject: [PATCH 10/38] =?UTF-8?q?docs(email)=20:=20plan=20impl=C3=A9mentat?= =?UTF-8?q?ion=20bouton=20+=20d'ajout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-06-09-inputemail-bouton-ajout.md | 458 ++++++++++++++++++ 1 file changed, 458 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-09-inputemail-bouton-ajout.md diff --git a/docs/superpowers/plans/2026-06-09-inputemail-bouton-ajout.md b/docs/superpowers/plans/2026-06-09-inputemail-bouton-ajout.md new file mode 100644 index 0000000..4f344d1 --- /dev/null +++ b/docs/superpowers/plans/2026-06-09-inputemail-bouton-ajout.md @@ -0,0 +1,458 @@ +# MalioInputEmail — bouton « + » d'ajout — 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 à `MalioInputEmail` un bouton « + » optionnel (prop `addable`) qui émet un event `add`, calqué sur `MalioInputPhone`, sans toucher à la logique de sanitisation email existante. + +**Architecture:** Recopie du pattern `addable` de `InputPhone.vue` dans `InputEmail.vue` (props `addable`/`addIconName`/`addButtonLabel`, event `add`, bouton `data-test="add-button"`). L'icône email étant à droite par défaut, une nouvelle computed `effectiveIconPosition` la force à gauche quand `addable` est actif, libérant la droite pour le bouton. Aucune modification de `onInput`/`sanitizeEmail`/`lowercase`. + +**Tech Stack:** Nuxt 4 layer, Vue 3 ` diff --git a/app/story/input/inputAmount.story.vue b/app/story/input/inputAmount.story.vue index 97defe3..6783fcc 100644 --- a/app/story/input/inputAmount.story.vue +++ b/app/story/input/inputAmount.story.vue @@ -9,6 +9,17 @@ />
+
+

Grand montant (séparateurs)

+ +

+ modelValue émis : {{ bigValue || 'vide' }} +

+
+

Avec hint

Date: Tue, 9 Jun 2026 14:17:36 +0200 Subject: [PATCH 23/38] =?UTF-8?q?fix(datatable)=20:=20libell=C3=A9s=20pagi?= =?UTF-8?q?nation=20en=20fran=C3=A7ais=20(Pr=C3=A9c.=20/=20Suiv.)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + app/components/malio/datatable/DataTable.vue | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b848491..5a8d57b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ Liste des évolutions de la librairie Malio layer UI * InputAmount : affichage groupé des milliers à la française (`1 234 567,89`) en temps réel ; `modelValue` reste propre (`'1234567.89'`) ; `maxLength` borne la longueur du modèle ### Changed +* DataTable : libellés de pagination en français — `Préc.` / `Suiv.` (étaient `Prev` / `Next`) ; aria-labels déjà en français inchangés. * MalioButton : dimensions par défaut `w-[180px]` / `h-[38px]` (étaient `w-[200px]` / `h-[40px]`). * DataTable : tailles par défaut revues — texte header `16px` (était `20px`), texte body `14px` (était `18px`), sélecteur de lignes et boutons de pagination (Prev / numéros / Next) alignés à `30px` de haut, padding de `12px` entre le bas du tableau et la barre de pagination, texte header et body passés en noir (`text-black`, étaient `text-m-primary`). * Select : nouvelle prop `fieldClass` pour surcharger les classes du field (notamment la hauteur `h-[40px]` jusqu'ici codée en dur) ; utilisée par le DataTable pour passer le sélecteur de perPage à `30px`. diff --git a/app/components/malio/datatable/DataTable.vue b/app/components/malio/datatable/DataTable.vue index 00817ee..f2575d1 100644 --- a/app/components/malio/datatable/DataTable.vue +++ b/app/components/malio/datatable/DataTable.vue @@ -81,7 +81,7 @@
+
+

Clearable (croix pour vider)

+ +

Valeur : {{ clearableUpload || '(aucun)' }}

+
+

Avec accept (PDF)

{ + clearableUpload.value = '' +} const dynamicError = computed(() => { if (!dynamicUpload.value) return '' diff --git a/app/components/malio/input/InputUpload.vue b/app/components/malio/input/InputUpload.vue index 2923ae9..0c7861d 100644 --- a/app/components/malio/input/InputUpload.vue +++ b/app/components/malio/input/InputUpload.vue @@ -26,8 +26,10 @@ placeholder="_" type="text" @click="openFilePicker" - @focus="isFocused = true" - @blur="isFocused = false" + @keydown.enter.prevent="openFilePicker" + @keydown.space.prevent="openFilePicker" + @focus="isFocused = true; onKbdFocus()" + @blur="isFocused = false; onKbdBlur()" > - +
+ + +

(), { @@ -111,6 +133,7 @@ const props = withDefaults( displayIcon: true, accept: '', required: false, + clearable: false, reserveMessageSpace: true, }, ) @@ -143,6 +166,7 @@ const mergedGroupClass = computed(() => const mergedInputClass = computed(() => twMerge( 'floating-input peer min-h-[40px] w-full border bg-white pl-3 pr-3 py-1 outline-none placeholder:text-transparent text-lg rounded-md cursor-pointer', + keyboardFocused.value ? 'm-focus-ring-kbd' : '', isReadonly.value ? '' : 'grow-height', isReadonly.value ? 'border-black' @@ -153,7 +177,9 @@ const mergedInputClass = computed(() => : hasSuccess.value ? 'border-m-success focus:border-m-success [&:not(:placeholder-shown)]:border-m-success' : isReadonly.value ? '' : 'focus:border-m-primary', - props.displayIcon ? '!pr-10' : '', + showClear.value + ? (props.displayIcon ? '!pr-16' : '!pr-10') + : (props.displayIcon ? '!pr-10' : ''), isReadonly.value ? '' : 'focus:pl-[11px]', isReadonly.value ? 'cursor-default' : '', disabled.value ? 'cursor-not-allowed' : '', @@ -191,8 +217,21 @@ const describedBy = computed(() => { const emit = defineEmits<{ (event: 'update:modelValue', value: string): void (event: 'file-selected', file: File): void + (event: 'clear'): void }>() +const showClear = computed(() => + props.clearable && isFilled.value && !props.disabled && !isReadonly.value, +) + +const onClear = () => { + if (props.disabled || isReadonly.value) return + if (!isControlled.value) localValue.value = '' + if (fileInputRef.value) fileInputRef.value.value = '' + emit('update:modelValue', '') + emit('clear') +} + const openFilePicker = () => { if (props.disabled || props.readonly) return fileInputRef.value?.click() -- 2.39.5 From 88c2a00d8957a91c9bc0581d0525a4d5eb18d1a0 Mon Sep 17 00:00:00 2001 From: tristan Date: Tue, 9 Jun 2026 16:38:32 +0200 Subject: [PATCH 31/38] feat(input) : anneau de focus clavier sur InputTextArea Co-Authored-By: Claude Opus 4.8 (1M context) --- app/components/malio/input/InputTextArea.vue | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/components/malio/input/InputTextArea.vue b/app/components/malio/input/InputTextArea.vue index d8a25a5..a3f2c85 100644 --- a/app/components/malio/input/InputTextArea.vue +++ b/app/components/malio/input/InputTextArea.vue @@ -19,6 +19,7 @@ textInput, showCounterComputed ? 'pb-6' : '', rounded, + keyboardFocused ? 'm-focus-ring-kbd' : '', ]" :required="required" :maxlength="maxLength" @@ -32,8 +33,8 @@ v-bind="attrs" placeholder="_" @input="onInput" - @focus="isFocused = true" - @blur="isFocused = false" + @focus="isFocused = true; onKbdFocus()" + @blur="isFocused = false; onKbdBlur()" />