Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
13 KiB
État visuel readonly cohérent — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use checkbox (
- [ ]) syntax.
Goal: Donner aux champs readonly un état visuel distinct et cohérent : bordure noire même vide, aucun grossissement/bleu au focus, label gris→noir selon rempli, icône gris→noir selon rempli.
Architecture: Pas de composant partagé (les styles sont dupliqués par composant, on suit ce pattern). Dans chaque composant on rend conditionnelles 4 zones de classes selon readonly. Le champ reste focusable (sélection/copie du texte) mais sans visuel de focus.
Tech Stack: Vue 3 <script setup>, Tailwind m-*, twMerge, Vitest + @vue/test-utils.
Branche: feature/MUI-41-props-required-asterisque-dans-le-label-sur-les-co (on continue dessus).
La recette commune (appliquée quand readonly === true)
Priorité inchangée : error puis success puis disabled passent TOUJOURS avant readonly. La recette readonly ne s'applique que dans la branche « état normal ».
- Bordure : forcer
border-black(même vide). Ne PAS inclureborder-m-mutednifocus:border-m-primaryquand readonly. - Grow + bleu : ne PAS inclure la classe
grow-height(donc pas de grossissement au focus) ni les classesfocus:*(border, paddingfocus:pl-*/focus:!pl-*). PourInputTextArea(pas degrow-height) : retirerfocus:border-m-primaryet le surlignage de focustextarea-scrollbar-primary. - Label : utiliser
isFilled ? 'text-black' : 'text-m-muted'; ne PAS inclurepeer-focus:text-m-primaryni les combospeer-placeholder-shown/peer-[&:not(:placeholder-shown):not(:focus)]. De plus, en readonly,shouldFloatLabel(ou équivalent qui pilote le float) doit ignorerisFocused→ float basé surisFilledseul (un champ readonly vide garde son label gris au repos). - Icône :
isFilled ? 'text-black' : 'text-m-muted'; sauter la brancheisFocused → text-m-primary. (error/success/disabledtoujours prioritaires.) - Interaction :
readonlybloque l'ouverture (Upload :openFilePickerno-op ; pickers : déjà bloqué). Le champ reste sélectionnable (ne pas retirer la focusabilité).
Implémentation conseillée : un petit computed isReadonly = computed(() => props.readonly && !props.disabled) (disabled prime), puis dans chaque twMerge(...) remplacer les fragments concernés par des expressions ternaires sur props.readonly. Garder le code lisible et homogène avec l'existant du fichier.
Patron de test (adapter le sélecteur input/textarea et le helper de montage du fichier)
it('readonly : bordure noire même vide, pas de grow/bleu', () => {
const wrapper = mountX({label: 'Champ', readonly: true}) // pas de modelValue → vide
const field = wrapper.get('input') // ou 'textarea'
expect(field.classes()).toContain('border-black')
expect(field.classes()).not.toContain('border-m-muted')
expect(field.classes()).not.toContain('grow-height') // sauf InputTextArea (pas de grow-height) : asserter l'absence de 'focus:border-m-primary'
expect(field.classes()).not.toContain('focus:border-m-primary')
})
it('readonly : label gris si vide, pas de bleu', () => {
const wrapper = mountX({label: 'Champ', readonly: true})
const label = wrapper.get('label')
expect(label.classes()).not.toContain('peer-focus:text-m-primary')
expect(label.classes()).toContain('text-m-muted')
})
it('readonly rempli : label noir + icône noire', () => {
const wrapper = mountX({label: 'Champ', readonly: true, modelValue: '...valeur remplie...'})
expect(wrapper.get('label').classes()).toContain('text-black')
// si le composant a une icône d'état :
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black')
})
Pour les composants à icône d'état, ajouter aussi : readonly + vide → icône text-m-muted. Adapter modelValue au type (montant, date ISO, etc.). Pour les pickers, la « valeur remplie » se passe via la prop d'affichage habituelle (voir tests voisins).
Task 1 : InputUpload (ajout de la prop readonly)
InputUpload n'a PAS de prop readonly aujourd'hui (son <input type="text"> est :readonly="true" en dur pour empêcher la saisie). On AJOUTE une vraie prop readonly.
Files: Modify app/components/malio/input/InputUpload.vue ; Test app/components/malio/input/InputUpload.test.ts
- Step 1 — tests d'abord : ajouter le patron de test ci-dessus. Champ =
wrapper.get('input[type="text"]'). Icône =[data-test="icon"](le nuage). « rempli » =modelValue: 'fichier.pdf'. - Step 2 — run, FAIL :
npm run test -- app/components/malio/input/InputUpload.test.ts - Step 3 — ajouter la prop :
readonly?: booleandansdefineProps+readonly: falsedanswithDefaults. - Step 4 — appliquer la recette dans
mergedInputClass,mergedLabelClass,iconStateClassetshouldFloatLabel(float =isFilledquand readonly). Forcercursor-default(au lieu decursor-pointer) quand readonly. - Step 5 — bloquer l'ouverture : dans
openFilePicker,if (props.disabled || props.readonly) return. - Step 6 — run, PASS : même commande. (Suite flaky connue : relancer le fichier si timeout non lié ;
--no-verifysi un timeout flaky bloque un commit déjà vérifié.) - Step 7 — commit
git add app/components/malio/input/InputUpload.vue app/components/malio/input/InputUpload.test.ts
git commit -m "feat(ui) : état readonly visuel sur InputUpload (+ prop readonly)"
(corps + ligne vide + Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>)
Task 2 : Inputs floating-label standard (lot de 6)
InputText, InputEmail, InputAmount, InputAutocomplete, InputPassword, InputTextArea ont déjà une prop readonly. Appliquer la recette à chacun.
Files: Modify les 6 .vue (app/components/malio/input/InputText.vue, InputEmail.vue, InputAmount.vue, InputAutocomplete.vue, InputPassword.vue, InputTextArea.vue) ; Test les 6 *.test.ts correspondants (Input.test.ts pour InputText, puis InputEmail/InputAmount/InputAutocomplete/InputPassword/InputTextArea.test.ts).
Spécificités par fichier :
-
InputText / InputEmail / InputAmount : structure identique (
mergedInputClassavecgrow-height+focus:border-m-primary+focus:pl-[11px];mergedLabelClassavecpeer-focus:text-m-primary;iconStateClassavec brancheisFocused). Appliquer la recette 1-4. -
InputAutocomplete : idem ; il a deux usages de
iconStateClass(icône gauche + chevron) — appliquer la recette àiconStateClass.isFilledy incluthasSelection. -
InputPassword : recette 1-4. L'icône est le toggle œil (cliquable) : garder le
@clickde bascule ; seule la couleur suit la recette (pas de bleu). NE PAS rendre l'œil non-cliquable en readonly. -
InputTextArea : classes inline dans le template (pas de
grow-height). Recette :isFilled ? border-black : border-m-muted→readonly ? border-black : (isFilled ? border-black : border-m-muted); retirerfocus:border-m-primaryet leisFocused ? 'textarea-scrollbar-primary'quand readonly ; label idem. Pas d'icône (recette 4 N/A). -
Step 1 — tests d'abord : ajouter le patron à chacun des 6 fichiers test (champ =
input, sauf TextArea =textarea; pour TextArea ne pas assertergrow-height). Pour les composants à icône, asserter aussi l'icône ([data-test="icon"]). AdaptermodelValuerempli au type (Amount : un montant valide). -
Step 2 — run, FAIL :
npm run test -- app/components/malio/input/Input.test.ts app/components/malio/input/InputEmail.test.ts app/components/malio/input/InputAmount.test.ts app/components/malio/input/InputAutocomplete.test.ts app/components/malio/input/InputPassword.test.ts app/components/malio/input/InputTextArea.test.ts -
Step 3 — appliquer la recette aux 6
.vue. -
Step 4 — run, PASS : même commande (relancer un fichier si flaky).
-
Step 5 — commit
git add app/components/malio/input/InputText.vue app/components/malio/input/InputEmail.vue app/components/malio/input/InputAmount.vue app/components/malio/input/InputAutocomplete.vue app/components/malio/input/InputPassword.vue app/components/malio/input/InputTextArea.vue app/components/malio/input/Input.test.ts app/components/malio/input/InputEmail.test.ts app/components/malio/input/InputAmount.test.ts app/components/malio/input/InputAutocomplete.test.ts app/components/malio/input/InputPassword.test.ts app/components/malio/input/InputTextArea.test.ts
git commit -m "feat(ui) : état readonly visuel sur les inputs floating-label"
Task 3 : InputPhone (découpler readonly de disabled)
Aujourd'hui InputPhone traite disabled || readonly ensemble (bouton « add » + opacity-40, look désactivé). On découple : readonly applique la recette readonly (bordure noire, pas de look disabled), tout en restant non-éditable. L'action « add » reste bloquée en readonly mais le champ ne doit plus avoir l'apparence désactivée.
Files: Modify app/components/malio/input/InputPhone.vue ; Test app/components/malio/input/InputPhone.test.ts
- Step 1 — tests d'abord : patron readonly (champ =
input, icône =[data-test="icon"]). Ajouter aussi une assertion que le champ readonly n'a PASopacity-40(plus de look disabled).modelValuerempli = un numéro. - Step 2 — run, FAIL :
npm run test -- app/components/malio/input/InputPhone.test.ts - Step 3 — appliquer la recette à
mergedInputClass/mergedLabelClass/iconStateClass(recette 1-4). Pour le bouton « add » (mergedAddButtonClass) : garder l'action bloquée en readonly (onAddretourne déjà), mais retirer l'apparenceopacity-40 cursor-not-allowedspécifique au readonly — la garder uniquement pourdisabled. (En readonly, le bouton add suit la couleur d'icône readonly.) - Step 4 — run, PASS (relancer si flaky).
- Step 5 — commit
git add app/components/malio/input/InputPhone.vue app/components/malio/input/InputPhone.test.ts
git commit -m "feat(ui) : InputPhone readonly suit les règles readonly (plus de look disabled)"
Task 4 : Pickers CalendarField (date family) + TimePicker
CalendarField (rend Date/DateTime/DateRange/DateWeek) et TimePicker ont déjà une prop readonly qui bloque l'ouverture du popover. Appliquer la recette visuelle. Leur input interne est déjà readonly natif ; le float du label suit isFilled || isOpen — en readonly, isOpen reste faux (ouverture bloquée), donc float = isFilled. Forcer bordure noire, label gris→noir, icône gris→noir sans branche focus/open.
Files: Modify app/components/malio/date/internal/CalendarField.vue, app/components/malio/time/TimePicker.vue ; Test app/components/malio/date/Date.test.ts (couvre CalendarField) et app/components/malio/time/TimePicker.test.ts
- Step 1 — tests d'abord : patron readonly. Pour
Date.test.ts, montermountDate({label, readonly: true})et une variante remplie (passer une valeur de date ISO comme les tests voisins). Champ = l'input du composant (voir sélecteur utilisé par les tests voisins). PourTimePicker.test.ts, utiliser le helper du fichier. - Step 2 — run, FAIL :
npm run test -- app/components/malio/date/Date.test.ts app/components/malio/time/TimePicker.test.ts - Step 3 — appliquer la recette à
CalendarField.vueetTimePicker.vue(mergedInputClass/mergedLabelClass/iconStateClass; float =isFilleden readonly). Vérifier que la croix « clear » reste masquée en readonly (déjà le cas — ne pas régresser). - Step 4 — run, PASS (relancer si flaky).
- Step 5 — commit
git add app/components/malio/date/internal/CalendarField.vue app/components/malio/time/TimePicker.vue app/components/malio/date/Date.test.ts app/components/malio/time/TimePicker.test.ts
git commit -m "feat(ui) : état readonly visuel sur pickers date/heure"
Task 5 : Playground + vérification finale
Files: Modify les pages playground concernées sous .playground/pages/composant/...
- Step 1 — exemples readonly : ajouter sur chaque page concernée (inputText, inputEmail, inputAmount, inputAutocomplete, inputPassword, inputTextArea, inputPhone, inputUpload [manquant], date, timePicker) un exemple readonly : une instance vide (
:readonly="true") ET une instance remplie readonly, pour visualiser bordure noire vide + label/icône noir rempli. Suivre le pattern de chaque page ; si une page rend l'ajout coûteux, le signaler et passer (mais inputUpload est demandé explicitement, le faire). - Step 2 — lint :
npm run lint→ 0 erreur (baseline 24 warnings préexistants). - Step 3 — suite complète :
npm run test→ tout vert (relancer un fichier en cas de timeout flaky). - Step 4 — commit
git add .playground
git commit -m "docs(playground) : exemples readonly"
Récapitulatif commits attendus
feat(ui) : état readonly visuel sur InputUpload (+ prop readonly)feat(ui) : état readonly visuel sur les inputs floating-labelfeat(ui) : InputPhone readonly suit les règles readonly (plus de look disabled)feat(ui) : état readonly visuel sur pickers date/heuredocs(playground) : exemples readonly
Note convention : le hook commit-msg malio impose un espace avant :.