--- name: creating-malio-component description: Use when creating a new UI component in the @malio/layer-ui Nuxt layer — covers component, tests, playground page, and Histoire story --- # Creating a Malio Component ## Overview Step-by-step process for creating a component in `@malio/layer-ui`. Each component requires 6 deliverables : le `.vue`, les tests, la page playground, la story Histoire, la mise à jour du CHANGELOG, et la mise à jour du `COMPONENTS.md`. ## When to Use - Création d'un nouveau composant dans `app/components/malio/` - Ajout d'une variante d'un composant existant (ex: InputPassword basé sur InputText) ## Workflow ```dot digraph create_component { rankdir=TB; "1. Lire les fichiers de référence" -> "2. Créer le composant .vue"; "2. Créer le composant .vue" -> "3. Créer les tests .test.ts"; "3. Créer les tests .test.ts" -> "4. npm run test + npm run lint"; "4. npm run test + npm run lint" -> "Tests OK?" [shape=diamond]; "Tests OK?" -> "5. Créer la page playground" [label="oui"]; "Tests OK?" -> "3. Créer les tests .test.ts" [label="non, corriger"]; "5. Créer la page playground" -> "6. Créer la story Histoire"; "6. Créer la story Histoire" -> "7. Mettre à jour CHANGELOG.md"; "7. Mettre à jour CHANGELOG.md" -> "8. Mettre à jour COMPONENTS.md"; } ``` ## Étapes ### 1. Lire les fichiers de référence Identifier le composant le plus proche comme base (ex: `InputText.vue` pour `InputPassword.vue`). Lire : - Le composant de référence : `app/components/malio/.vue` - Ses tests : `app/components/malio/.test.ts` ### 2. Créer le composant `.vue` **Fichier :** `app/components/malio/.vue` **Checklist obligatoire :** | Élément | Pattern | |---------|---------| | `defineOptions` | `{ name: 'Malio', inheritAttrs: false }` | | Props | `defineProps()` + `withDefaults()` — props communes : `id`, `label`, `modelValue`, `inputClass`, `labelClass`, `groupClass`, `disabled`, `readonly`, `hint`, `error`, `success` | | Contrôlé / non-contrôlé | `isControlled = computed(() => props.modelValue !== undefined)` + `localValue` en fallback | | Classes CSS | Fusionnées via `twMerge()` pour permettre l'override consommateur | | Accessibilité | `aria-invalid`, `aria-describedby`, `label[for]` lié à `input[id]` | | Icônes | `Icon as IconifyIcon` depuis `@iconify/vue` (pas `@nuxt/icon`) | | ID généré | `useId()` + prefix unique (ex: `malio-input-password-${generatedId}`) | ### 3. Créer les tests `.test.ts` **Fichier :** `app/components/malio/.test.ts` (colocalisé) **Pattern de montage :** ```ts import { mount } from '@vue/test-utils' import type { DefineComponent } from 'vue' import MonComposant from './MonComposant.vue' const ComposantForTest = MonComposant as DefineComponent const mountComponent = (props: MonComposantProps = {}) => mount(ComposantForTest, { props, global: { stubs: { IconifyIcon: { template: '', }, }, }, }) ``` **Tests minimum à couvrir :** - Rendu initial avec valeur - Rendu du label - Emit `update:modelValue` - Props `disabled`, `readonly` - États `error`, `success`, `hint` (messages + classes CSS) - Accessibilité (`aria-invalid`, `label[for]` / `input[id]`) - Comportements spécifiques au composant **Attention stub IconifyIcon :** Le stub basé sur le nom `IconifyIcon` ne remplace pas toujours le vrai composant `@iconify/vue`. Pour tester les props du composant Icon (ex: `icon`), utiliser `findComponent` avec l'import réel : ```ts import { Icon as IconifyIcon } from '@iconify/vue' // ... const iconComponent = wrapper.findComponent(IconifyIcon) expect(iconComponent.props('icon')).toBe('mdi:eye-outline') ``` ### 4. Vérification ```bash npm run test # Tous les tests passent npm run lint # Pas d'erreurs ``` ### 5. Créer la page playground **Fichier :** `.playground/pages/composant//.vue` (camelCase, dans le sous-dossier de catégorie) La page devient automatiquement une route Nuxt (`/composant//`) et hérite du layout `default` (qui affiche la `MalioSidebar`). **Ajouter ensuite le lien dans la nav centralisée** `.playground/playground.nav.ts` : insérer un `{label, to}` dans la section appropriée (ou créer une nouvelle section), où `to` = `/composant//`. Inclure des variantes représentatives dans une grille : ```html

Titre variante

``` **Variantes typiques :** simple, avec label, désactivé, readonly, hint, erreur, succès, validation dynamique. ### 6. Créer la story Histoire **Fichier :** `app/story/.story.vue` (camelCase) **Structure :** ```vue # MalioNomComposant Description courte. ## Props détaillées ## Comportement ## Accessibilité ## Events ``` **Important : initial state avec variantes.** La story doit contenir des exemples visuels directement visibles (pas un composant vide). Chaque variante a un `v-model` avec une `ref` initialisée. Variantes typiques à inclure : - Simple (avec label) - Sans icône (`display-icon="false"`) si applicable - Avec hint - Désactivé (avec valeur pré-remplie) - Readonly (avec valeur pré-remplie) - Erreur (avec valeur + message d'erreur) - Succès (avec valeur + message de succès) ### 7. Mettre à jour le CHANGELOG **Fichier :** `CHANGELOG.md` à la racine du projet. Ajouter une ligne dans la section `### Added` de la version courante. Le numéro de ticket se trouve dans le nom de la branche Git (ex: branche `feat/MUI-8-composant-password` → ticket `MUI-8`). **Format :** - Avec numéro de ticket : `* [#MUI-8] Création d'un composant mot de passe` - Sans numéro de ticket : `* Création d'un composant textarea` Pour extraire le numéro de ticket depuis la branche courante : ```bash git branch --show-current | grep -oP '(MUI-\d+|\d{3,})' | head -1 ``` ### 8. Mettre à jour COMPONENTS.md **Fichier :** `COMPONENTS.md` à la racine du projet. Ce fichier sert de documentation de référence pour les projets qui consomment `@malio/layer-ui`. Il est lu par Claude dans les projets consommateurs pour connaître les composants disponibles et leurs props. **Ajouter une section pour le nouveau composant** en suivant le format existant : ```markdown ## MalioNomComposant Description courte du composant. | Prop | Type | Défaut | Description | |------|------|--------|-------------| | ... | ... | ... | ... | **Events :** `update:modelValue(value: string)` \`\`\`vue \`\`\` ``` **Checklist :** - Toutes les props documentées avec type, défaut et description - Events listés - Slots listés si applicable - 2-5 exemples d'utilisation couvrant les cas courants (simple, avec options, disabled, erreur) - Section placée par ordre logique (inputs ensemble, boutons ensemble, etc.) ## Common Mistakes Cette section est alimentée au fur et à mesure des retours utilisateur et des problèmes rencontrés. **Si un retour ou un bug est identifié lors de la création d'un composant, ajouter une ligne dans ce tableau.** | Erreur | Solution | |--------|----------| | Stub IconifyIcon ne fonctionne pas dans les tests | Utiliser `findComponent(IconifyIcon)` avec l'import réel pour tester les props | | Oubli de `inheritAttrs: false` | Toujours dans `defineOptions` — sinon les attrs se dupliquent | | Composant absent de la sidebar du playground | Ajouter son entrée `{label, to}` dans `.playground/playground.nav.ts` (la page n'est plus auto-découverte) | | Padding input pas ajusté avec icône | Ajouter `!pr-10` (ou équivalent) quand une icône est présente à droite | | Story sans initial state | Toujours initialiser les `ref` avec des valeurs pour que les variantes soient visibles dès le chargement | | CHANGELOG oublié | Toujours ajouter la ligne dans `### Added` avant de commit | | COMPONENTS.md pas mis à jour | Ajouter la doc du composant dans `COMPONENTS.md` — c'est la référence pour les projets consommateurs |