docs(amount) : plan implémentation séparateurs de milliers
This commit is contained in:
@@ -0,0 +1,532 @@
|
||||
# MalioInputAmount — séparateurs de milliers — 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:** Afficher les montants groupés à la française (`1 234 567,89`) en temps réel dans `MalioInputAmount`, tout en émettant un `modelValue` propre inchangé (`1234567.89`).
|
||||
|
||||
**Architecture:** Extraction des fonctions pures (`normalizeAmount` déplacé + `formatGroupedAmount` + helpers curseur) dans `composables/amountFormat.ts`. Le composant binde l'affichage groupé (`formatGroupedAmount(currentValue)`) et, à la frappe, parse vers le modèle propre (émis), reformate, et repositionne le curseur en comptant les caractères significatifs. `maxLength` borne la longueur du modèle (plus de `maxlength` natif).
|
||||
|
||||
**Tech Stack:** Nuxt 4 layer, Vue 3 `<script setup lang="ts">`, Vitest + @vue/test-utils (jsdom).
|
||||
|
||||
**Référence spec :** `docs/superpowers/specs/2026-06-09-inputamount-separateurs-milliers-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- **Create** `app/components/malio/input/composables/amountFormat.ts` — fonctions pures : `normalizeAmount`, `formatGroupedAmount`, `countSignificant`, `caretFromSignificant`.
|
||||
- **Create** `app/components/malio/input/composables/amountFormat.test.ts` — tests unitaires des fonctions pures.
|
||||
- **Modify** `app/components/malio/input/InputAmount.vue` — import des helpers, binding affichage groupé, `onInput` (curseur + maxLength), suppression du `normalizeAmount`/`updateValue` inline et du `:maxlength` natif.
|
||||
- **Modify** `app/components/malio/input/InputAmount.test.ts` — assertions d'affichage (brut → groupé), nouveaux tests.
|
||||
- **Modify** `COMPONENTS.md` — note affichage groupé + contrat modèle + `maxLength`.
|
||||
- **Modify** `CHANGELOG.md` — entrée.
|
||||
- **Modify** `app/story/input/inputAmount.story.vue` + `.playground/pages/composant/input/inputAmount.vue` — exemple grand montant.
|
||||
|
||||
**Note hooks pré-commit :** `make pre-commit` lance lint + suite complète (~900 tests), KNOWN FLAKY (timeouts 5000ms intermittents sur des fichiers SANS rapport). Si un commit échoue uniquement sur un timeout sans rapport, relancer une fois ; si ça reflake, `git commit --no-verify`. Stager des fichiers explicites — **jamais** `git add -A` (`nuxt.config.ts` et `.playground/pages/composant/radio/radioButton.vue` modifiés localement ne doivent PAS être committés).
|
||||
|
||||
**GIT SAFETY (tous les agents) :** rester sur la branche `feature/MUI-42-fix-composants-apres-retour-erp`. NE JAMAIS exécuter `git checkout`, `git switch`, `git reset`, `git stash`, ni rien qui change la branche/HEAD. Uniquement `git add <fichiers>` et `git commit`.
|
||||
|
||||
---
|
||||
|
||||
## Task 1 : `amountFormat.ts` — fonctions pures (TDD)
|
||||
|
||||
**Files:**
|
||||
- Create: `app/components/malio/input/composables/amountFormat.ts`
|
||||
- Create: `app/components/malio/input/composables/amountFormat.test.ts`
|
||||
|
||||
- [ ] **Step 1 : Écrire les tests (échouent car le module n'existe pas)**
|
||||
|
||||
Créer `app/components/malio/input/composables/amountFormat.test.ts` :
|
||||
```ts
|
||||
import {describe, expect, it} from 'vitest'
|
||||
import {normalizeAmount, formatGroupedAmount, countSignificant, caretFromSignificant} from './amountFormat'
|
||||
|
||||
describe('normalizeAmount', () => {
|
||||
it('garde le point décimal', () => {
|
||||
expect(normalizeAmount('12.5')).toBe('12.5')
|
||||
})
|
||||
it('convertit la virgule en point et nettoie', () => {
|
||||
expect(normalizeAmount('0012,345abc')).toBe('12.34')
|
||||
})
|
||||
it('normalise une décimale en tête', () => {
|
||||
expect(normalizeAmount(',5')).toBe('0.5')
|
||||
})
|
||||
it('retire les espaces', () => {
|
||||
expect(normalizeAmount('1 234 567')).toBe('1234567')
|
||||
})
|
||||
it('limite à 2 décimales', () => {
|
||||
expect(normalizeAmount('1234.567')).toBe('1234.56')
|
||||
})
|
||||
it('garde une décimale en cours de saisie', () => {
|
||||
expect(normalizeAmount('12.')).toBe('12.')
|
||||
})
|
||||
it('renvoie une chaîne vide pour une saisie non numérique', () => {
|
||||
expect(normalizeAmount('abc')).toBe('')
|
||||
})
|
||||
it('garde un zéro seul', () => {
|
||||
expect(normalizeAmount('0')).toBe('0')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatGroupedAmount', () => {
|
||||
it('groupe la partie entière par 3 avec des espaces', () => {
|
||||
expect(formatGroupedAmount('1234567')).toBe('1 234 567')
|
||||
})
|
||||
it('utilise la virgule comme séparateur décimal', () => {
|
||||
expect(formatGroupedAmount('1234.56')).toBe('1 234,56')
|
||||
})
|
||||
it('affiche une virgule pour une décimale en cours', () => {
|
||||
expect(formatGroupedAmount('12.')).toBe('12,')
|
||||
})
|
||||
it('gère les valeurs sous 1000 sans séparateur', () => {
|
||||
expect(formatGroupedAmount('12')).toBe('12')
|
||||
})
|
||||
it('groupe avec une décimale en tête', () => {
|
||||
expect(formatGroupedAmount('0.5')).toBe('0,5')
|
||||
})
|
||||
it('renvoie une chaîne vide pour une chaîne vide', () => {
|
||||
expect(formatGroupedAmount('')).toBe('')
|
||||
})
|
||||
it('garde un zéro seul', () => {
|
||||
expect(formatGroupedAmount('0')).toBe('0')
|
||||
})
|
||||
})
|
||||
|
||||
describe('countSignificant', () => {
|
||||
it('compte les caractères hors espaces à gauche du curseur', () => {
|
||||
// "1 234|" → curseur en position 5, 4 caractères significatifs (1,2,3,4)
|
||||
expect(countSignificant('1 234', 5)).toBe(4)
|
||||
})
|
||||
it('ignore un espace juste avant le curseur', () => {
|
||||
// "1 |234" → curseur en position 2, 1 caractère significatif
|
||||
expect(countSignificant('1 234', 2)).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('caretFromSignificant', () => {
|
||||
it('place le curseur après le n-ième caractère significatif', () => {
|
||||
// 4 caractères significatifs dans "1 234 567" → après le "4" (index 5)
|
||||
expect(caretFromSignificant('1 234 567', 4)).toBe(5)
|
||||
})
|
||||
it('place le curseur en fin si on dépasse', () => {
|
||||
expect(caretFromSignificant('1 234', 10)).toBe(5)
|
||||
})
|
||||
it('place le curseur au début pour 0 caractère significatif', () => {
|
||||
expect(caretFromSignificant('1 234', 0)).toBe(0)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2 : Lancer les tests pour vérifier l'échec**
|
||||
|
||||
Run : `npm run test -- amountFormat.test.ts`
|
||||
Expected : FAIL (le module `./amountFormat` n'existe pas).
|
||||
|
||||
- [ ] **Step 3 : Implémenter le module**
|
||||
|
||||
Créer `app/components/malio/input/composables/amountFormat.ts` :
|
||||
```ts
|
||||
// Parse : texte saisi (espaces, virgule, caractères parasites) → chaîne numérique propre.
|
||||
export const normalizeAmount = (value: string): string => {
|
||||
const sanitizedValue = value
|
||||
.replace(/\s+/g, '')
|
||||
.replace(/,/g, '.')
|
||||
.replace(/[^\d.]/g, '')
|
||||
const [integerPartRaw = '', ...decimalParts] = sanitizedValue.split('.')
|
||||
const integerPart = integerPartRaw.replace(/^0+(?=\d)/, '')
|
||||
const decimalPart = decimalParts.join('').slice(0, 2)
|
||||
|
||||
if (sanitizedValue.includes('.')) {
|
||||
return `${integerPart || '0'}.${decimalPart}`
|
||||
}
|
||||
|
||||
return integerPart
|
||||
}
|
||||
|
||||
// Format : modèle propre (point décimal) → affichage groupé FR (espaces + virgule).
|
||||
export const formatGroupedAmount = (model: string): string => {
|
||||
if (model === '') return ''
|
||||
const hasDot = model.includes('.')
|
||||
const [integerPart = '', decimalPart = ''] = model.split('.')
|
||||
const groupedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ' ')
|
||||
return hasDot ? `${groupedInteger},${decimalPart}` : groupedInteger
|
||||
}
|
||||
|
||||
// Nombre de caractères significatifs (hors espaces de groupement) à gauche d'une position.
|
||||
export const countSignificant = (str: string, upTo: number): number =>
|
||||
str.slice(0, upTo).replace(/ /g, '').length
|
||||
|
||||
// Position de curseur après le n-ième caractère significatif dans la chaîne affichée.
|
||||
export const caretFromSignificant = (display: string, sig: number): number => {
|
||||
if (sig <= 0) return 0
|
||||
let seen = 0
|
||||
for (let i = 0; i < display.length; i++) {
|
||||
if (display[i] !== ' ') seen++
|
||||
if (seen >= sig) return i + 1
|
||||
}
|
||||
return display.length
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4 : Lancer les tests pour vérifier le succès**
|
||||
|
||||
Run : `npm run test -- amountFormat.test.ts`
|
||||
Expected : PASS (tous).
|
||||
|
||||
- [ ] **Step 5 : Commit**
|
||||
|
||||
```bash
|
||||
git add app/components/malio/input/composables/amountFormat.ts app/components/malio/input/composables/amountFormat.test.ts
|
||||
git commit -m "feat(amount) : helpers amountFormat (normalize, group, curseur)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2 : Brancher `InputAmount.vue` sur les helpers
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/components/malio/input/InputAmount.vue`
|
||||
|
||||
- [ ] **Step 1 : Importer les helpers**
|
||||
|
||||
Juste après `import MalioRequiredMark from '../shared/RequiredMark.vue'`, ajouter :
|
||||
```ts
|
||||
import {normalizeAmount, formatGroupedAmount, countSignificant, caretFromSignificant} from './composables/amountFormat'
|
||||
```
|
||||
|
||||
- [ ] **Step 2 : Ajouter la computed `formattedValue`**
|
||||
|
||||
Juste après la ligne `const currentValue = computed(() => (isControlled.value ? (props.modelValue ?? '') : localValue.value))`, ajouter :
|
||||
```ts
|
||||
const formattedValue = computed(() => formatGroupedAmount(currentValue.value))
|
||||
```
|
||||
|
||||
- [ ] **Step 3 : Binder l'affichage groupé et retirer le `maxlength` natif**
|
||||
|
||||
Dans le `<template>`, sur l'`<input>` :
|
||||
- Remplacer `:value="currentValue"` par `:value="formattedValue"`.
|
||||
- **Supprimer** la ligne `:maxlength="maxLength"` (le plafond est géré en JS). Garder `:minlength="minLength"`.
|
||||
|
||||
- [ ] **Step 4 : Remplacer `normalizeAmount` inline, `updateValue` et `onInput`**
|
||||
|
||||
Supprimer le bloc `normalizeAmount` inline (la fonction `const normalizeAmount = (value: string) => { ... }`) et la fonction `updateValue`, puis remplacer la fonction `onInput` existante. Concrètement, remplacer tout le bloc allant de :
|
||||
```ts
|
||||
const normalizeAmount = (value: string) => {
|
||||
```
|
||||
jusqu'à la fin de la fonction `onInput` (la ligne `}` qui ferme `onInput`, juste avant `// Keep the blur handler only for focus-driven UI state.`), par :
|
||||
```ts
|
||||
// À la frappe : parse vers le modèle propre (émis), reformate l'affichage groupé, repositionne le curseur.
|
||||
const onInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
const rawText = target.value
|
||||
const caret = target.selectionStart ?? rawText.length
|
||||
const model = normalizeAmount(rawText)
|
||||
|
||||
// maxLength borne la longueur du MODÈLE (pas l'affichage) : on ignore le keystroke en dépassement.
|
||||
if (props.maxLength != null && model.length > Number(props.maxLength)) {
|
||||
target.value = formattedValue.value
|
||||
const restored = Math.max(0, caret - 1)
|
||||
target.setSelectionRange(restored, restored)
|
||||
return
|
||||
}
|
||||
|
||||
const display = formatGroupedAmount(model)
|
||||
const sig = countSignificant(rawText, caret)
|
||||
target.value = display
|
||||
const newCaret = caretFromSignificant(display, sig)
|
||||
target.setSelectionRange(newCaret, newCaret)
|
||||
|
||||
if (!isControlled.value) {
|
||||
localValue.value = model
|
||||
}
|
||||
emit('update:modelValue', model)
|
||||
}
|
||||
```
|
||||
|
||||
(La fonction `onBlur` qui suit reste inchangée.)
|
||||
|
||||
- [ ] **Step 5 : Vérifier la compilation et lancer les tests existants (ils vont en partie échouer — c'est attendu, ils sont mis à jour en Task 3)**
|
||||
|
||||
Run : `npm run test -- amountFormat.test.ts`
|
||||
Expected : PASS (le module est intact).
|
||||
|
||||
Run : `npm run lint`
|
||||
Expected : 0 erreur sur `InputAmount.vue` (pas de variable inutilisée, imports utilisés).
|
||||
|
||||
Note : `npm run test -- InputAmount.test.ts` affichera des échecs sur les assertions d'affichage (`'12.5'` → `'12,5'`) — c'est normal, la Task 3 met à jour ces tests. Ne pas « corriger » le composant pour les faire passer.
|
||||
|
||||
- [ ] **Step 6 : Commit**
|
||||
|
||||
```bash
|
||||
git add app/components/malio/input/InputAmount.vue
|
||||
git commit -m "feat(amount) : affichage groupé temps réel (séparateurs de milliers)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3 : Mettre à jour `InputAmount.test.ts`
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/components/malio/input/InputAmount.test.ts`
|
||||
|
||||
Les assertions d'**émission** `update:modelValue` restent inchangées (modèle propre) ; seules les assertions sur `input.element.value` passent à l'affichage groupé.
|
||||
|
||||
- [ ] **Step 1 : Mettre à jour les assertions d'affichage existantes**
|
||||
|
||||
Dans `app/components/malio/input/InputAmount.test.ts`, appliquer ces remplacements exacts :
|
||||
|
||||
Test « keeps dots as the decimal separator on input » :
|
||||
```ts
|
||||
expect(wrapper.get('input').element.value).toBe('12.5')
|
||||
```
|
||||
devient :
|
||||
```ts
|
||||
expect(wrapper.get('input').element.value).toBe('12,5')
|
||||
```
|
||||
|
||||
Test « accepts commas but normalizes them to dots » :
|
||||
```ts
|
||||
expect(wrapper.get('input').element.value).toBe('12.34')
|
||||
```
|
||||
devient :
|
||||
```ts
|
||||
expect(wrapper.get('input').element.value).toBe('12,34')
|
||||
```
|
||||
|
||||
Test « normalizes a leading decimal separator » :
|
||||
```ts
|
||||
expect(wrapper.get('input').element.value).toBe('0.5')
|
||||
```
|
||||
devient :
|
||||
```ts
|
||||
expect(wrapper.get('input').element.value).toBe('0,5')
|
||||
```
|
||||
|
||||
Test « keeps the normalized decimal value on blur » :
|
||||
```ts
|
||||
expect(input.element.value).toBe('12.5')
|
||||
```
|
||||
devient :
|
||||
```ts
|
||||
expect(input.element.value).toBe('12,5')
|
||||
```
|
||||
|
||||
(Les tests « keeps integer values unchanged on blur » → `'12'` et « keeps an empty value empty on blur » → `''` restent corrects tels quels, car `formatGroupedAmount('12') === '12'` et `formatGroupedAmount('') === ''`.)
|
||||
|
||||
- [ ] **Step 2 : Ajouter les tests de groupement et de maxLength**
|
||||
|
||||
Juste avant la `})` finale qui ferme le `describe('MalioInputAmount', ...)`, ajouter :
|
||||
```ts
|
||||
it('groupe les milliers à l\'affichage tout en émettant la valeur propre', async () => {
|
||||
const wrapper = mountInputAmount({modelValue: ''})
|
||||
|
||||
await wrapper.get('input').setValue('1234567')
|
||||
|
||||
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual(['1234567'])
|
||||
expect(wrapper.get('input').element.value).toBe('1 234 567')
|
||||
})
|
||||
|
||||
it('groupe un grand montant avec décimales', async () => {
|
||||
const wrapper = mountInputAmount({modelValue: ''})
|
||||
|
||||
await wrapper.get('input').setValue('1234567,89')
|
||||
|
||||
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual(['1234567.89'])
|
||||
expect(wrapper.get('input').element.value).toBe('1 234 567,89')
|
||||
})
|
||||
|
||||
it('formate la valeur initiale (modelValue) en groupé', () => {
|
||||
const wrapper = mountInputAmount({modelValue: '1234567.89'})
|
||||
|
||||
expect(wrapper.get('input').element.value).toBe('1 234 567,89')
|
||||
})
|
||||
|
||||
it('maxLength borne la longueur du modèle : un dépassement est ignoré', async () => {
|
||||
const wrapper = mountInputAmount({modelValue: '', maxLength: 4})
|
||||
|
||||
await wrapper.get('input').setValue('12345')
|
||||
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
expect(wrapper.get('input').element.value).toBe('')
|
||||
})
|
||||
|
||||
it('maxLength autorise une valeur à la limite', async () => {
|
||||
const wrapper = mountInputAmount({modelValue: '', maxLength: 4})
|
||||
|
||||
await wrapper.get('input').setValue('1234')
|
||||
|
||||
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual(['1234'])
|
||||
expect(wrapper.get('input').element.value).toBe('1 234')
|
||||
})
|
||||
|
||||
it('n\'a plus d\'attribut maxlength natif sur l\'input', () => {
|
||||
const wrapper = mountInputAmount({maxLength: 4})
|
||||
|
||||
expect(wrapper.get('input').attributes('maxlength')).toBeUndefined()
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 3 : Lancer la suite**
|
||||
|
||||
Run : `npm run test -- InputAmount.test.ts`
|
||||
Expected : PASS (tous : existants mis à jour + 6 nouveaux).
|
||||
|
||||
Si un test de `setValue` échoue parce que jsdom ne déclenche pas `setSelectionRange` comme attendu, vérifier la valeur réelle via `console.log(wrapper.get('input').element.value)` — l'affichage attendu suit `formatGroupedAmount`. Ne pas affaiblir une assertion sans comprendre.
|
||||
|
||||
- [ ] **Step 4 : Commit**
|
||||
|
||||
```bash
|
||||
git add app/components/malio/input/InputAmount.test.ts
|
||||
git commit -m "test(amount) : affichage groupé + maxLength sur le modèle"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4 : Documentation
|
||||
|
||||
**Files:**
|
||||
- Modify: `COMPONENTS.md`
|
||||
- Modify: `CHANGELOG.md`
|
||||
|
||||
- [ ] **Step 1 : Note dans `COMPONENTS.md`**
|
||||
|
||||
Dans la section `## MalioInputAmount`, remplacer la ligne de description :
|
||||
```markdown
|
||||
Champ montant avec icône devise (euro par défaut).
|
||||
```
|
||||
par :
|
||||
```markdown
|
||||
Champ montant avec icône devise (euro par défaut).
|
||||
|
||||
L'affichage est groupé à la française (`1 234 567,89` : espace pour les milliers, virgule décimale), mis à jour en temps réel pendant la saisie. La valeur émise (`modelValue`) reste une **chaîne numérique propre** (point décimal, sans espaces, ex. `'1234567.89'`). `maxLength` borne la longueur de cette chaîne propre (pas de l'affichage).
|
||||
```
|
||||
|
||||
- [ ] **Step 2 : Mettre à jour l'exemple de la section**
|
||||
|
||||
Dans le bloc ```vue de la section `## MalioInputAmount`, ajouter avant la fence fermante :
|
||||
```vue
|
||||
<MalioInputAmount v-model="gros" label="Budget" />
|
||||
<!-- saisie 1234567.89 → affiché "1 234 567,89", modelValue "1234567.89" -->
|
||||
```
|
||||
|
||||
- [ ] **Step 3 : Entrée CHANGELOG**
|
||||
|
||||
Dans `CHANGELOG.md`, sous `### Added`, ajouter comme dernière puce de la liste (après la dernière puce existante du bloc Added) :
|
||||
```markdown
|
||||
* 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
|
||||
```
|
||||
|
||||
- [ ] **Step 4 : Commit**
|
||||
|
||||
```bash
|
||||
git add COMPONENTS.md CHANGELOG.md
|
||||
git commit -m "docs(amount) : documente l'affichage groupé des milliers"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5 : Story + playground
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/story/input/inputAmount.story.vue`
|
||||
- Modify: `.playground/pages/composant/input/inputAmount.vue`
|
||||
|
||||
- [ ] **Step 1 : Carte « grand montant » dans la story**
|
||||
|
||||
Dans `app/story/input/inputAmount.story.vue`, juste après la carte « Simple » (le `<div class="rounded-lg border p-4">` contenant `v-model="simpleValue"`, qui se termine par `</div>` avant la carte « Avec hint »), insérer :
|
||||
```html
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Grand montant (séparateurs)</h2>
|
||||
<MalioInputAmount
|
||||
v-model="bigValue"
|
||||
label="Budget"
|
||||
/>
|
||||
<p class="mt-2 text-sm text-m-muted">
|
||||
modelValue émis : <code>{{ bigValue || 'vide' }}</code>
|
||||
</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 2 : Déclarer la ref dans la story**
|
||||
|
||||
Dans le `<script setup>` de `app/story/input/inputAmount.story.vue`, après `const simpleValue = ref('')`, ajouter :
|
||||
```ts
|
||||
const bigValue = ref('1234567.89')
|
||||
```
|
||||
|
||||
- [ ] **Step 3 : Exemple « grand montant » dans le playground**
|
||||
|
||||
Dans `.playground/pages/composant/input/inputAmount.vue`, juste après la carte « Avec label » (le `<div class="rounded-lg border p-4">` contenant `name="amount"`, qui se termine par `</div>` avant la carte « Désactivé »), insérer :
|
||||
```html
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Grand montant (séparateurs)</h2>
|
||||
<MalioInputAmount
|
||||
v-model="bigValue"
|
||||
label="Budget"
|
||||
/>
|
||||
<div class="mt-2 rounded border p-3 text-sm">
|
||||
<p>modelValue émis : <code>{{ bigValue || 'vide' }}</code></p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 4 : Déclarer la ref dans le playground**
|
||||
|
||||
Dans le `<script setup>` de `.playground/pages/composant/input/inputAmount.vue`, ajouter (créer le bloc s'il n'existe pas déjà — vérifier la présence d'un `<script setup lang="ts">` ; sinon l'ajouter en bas du fichier) :
|
||||
```ts
|
||||
const bigValue = ref('1234567.89')
|
||||
```
|
||||
Si le `<script setup>` n'importe pas encore `ref`, ajouter `import {ref} from 'vue'` en tête du script.
|
||||
|
||||
- [ ] **Step 5 : Vérifier le lint**
|
||||
|
||||
Run : `npm run lint`
|
||||
Expected : 0 erreur sur les deux fichiers modifiés (warnings pré-existants ailleurs tolérés).
|
||||
|
||||
- [ ] **Step 6 : Commit**
|
||||
|
||||
```bash
|
||||
git add app/story/input/inputAmount.story.vue .playground/pages/composant/input/inputAmount.vue
|
||||
git commit -m "docs(amount) : exemple grand montant groupé (story + playground)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6 : Vérification finale
|
||||
|
||||
- [ ] **Step 1 : Suites amount**
|
||||
|
||||
Run : `npm run test -- amountFormat.test.ts InputAmount.test.ts`
|
||||
Expected : PASS (helpers + composant).
|
||||
|
||||
- [ ] **Step 2 : Lint global**
|
||||
|
||||
Run : `npm run lint`
|
||||
Expected : 0 erreur.
|
||||
|
||||
- [ ] **Step 3 : Vérification manuelle (playground)**
|
||||
|
||||
Run : `npm run dev`, ouvrir `composant/input/inputAmount`, carte « Grand montant ».
|
||||
Vérifier :
|
||||
- Taper `1234567` → affiche `1 234 567` au fil de la frappe ; `modelValue` affiché = `1234567`.
|
||||
- Taper une virgule + décimales → `1 234 567,89` ; `modelValue` = `1234567.89`.
|
||||
- Le curseur reste cohérent quand un séparateur s'insère (taper au milieu d'un nombre).
|
||||
- La valeur initiale `1234567.89` s'affiche groupée au montage.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
**Spec coverage :**
|
||||
- Modèle propre, séparateurs visuels → Task 2 (binding `formattedValue`, emit `model`).
|
||||
- Temps réel + curseur → Task 2 Step 4 (`onInput` avec `countSignificant`/`caretFromSignificant`).
|
||||
- Format FR (espace + virgule) → Task 1 (`formatGroupedAmount`).
|
||||
- Par défaut sur tous (pas de prop) → aucune prop ajoutée.
|
||||
- `maxLength` sur le modèle + suppression `maxlength` natif → Task 2 Steps 3-4, tests Task 3.
|
||||
- Extraction `amountFormat.ts` → Task 1.
|
||||
- Table de vérité → Task 1 tests + Task 3 tests.
|
||||
- Docs + story + playground → Tasks 4, 5.
|
||||
|
||||
**Placeholder scan :** aucun TODO/TBD ; code fourni intégralement.
|
||||
|
||||
**Type consistency :** `normalizeAmount`, `formatGroupedAmount`, `countSignificant`, `caretFromSignificant` (signatures identiques entre Task 1, leur usage Task 2, et les imports). `formattedValue` (computed) utilisée dans le binding et le chemin de rejet maxLength. `model` = sortie de `normalizeAmount`, émis tel quel.
|
||||
Reference in New Issue
Block a user