## Résumé
Nouveau composant `MalioInputRichText` : éditeur WYSIWYG basé sur **TipTap v3** + **StarterKit** + **tiptap-markdown**, aligné sur le thème Malio (couleurs `m-*`, icônes `mdi:*`, états error / success / hint).
## Détails
- **Toolbar** : gras, italique, barré, H2, H3, liste à puces, liste numérotée, citation, code inline, bloc de code, lien (prompt URL), undo / redo
- **Sortie** : `markdown` (par défaut) ou `html` via la prop `outputFormat`
- **Modes** : `editable`, `disabled`, `readonly` ; mode lecture seule (`editable=false`) rend le contenu en `prose` sans toolbar
- **Accessibilité** : label `for/id`, `aria-invalid`, `aria-describedby`, `aria-pressed` sur les boutons toolbar
- **Style** : floating focus border `m-primary`, error `m-danger`, success `m-success`, toolbar `bg-m-bg`
## Dépendances ajoutées (purement additives, aucun bump existant)
- `@tiptap/vue-3` ^3.22.5
- `@tiptap/starter-kit` ^3.22.5
- `@tiptap/extension-placeholder` ^3.22.5
- `@tiptap/pm` ^3.22.5
- `tiptap-markdown` ^0.9.0
> Note : `@tiptap/extension-link` n'est pas installé séparément car StarterKit v3 l'inclut nativement (configuré via `StarterKit.configure({ link: { ... } })`).
## Test plan
- [x] `npm run test` — 315/315 (12 nouveaux tests sur InputRichText)
- [x] `npm run lint` — 0 erreur sur les fichiers ajoutés
- [x] `npm run story:build` — Histoire build OK (story `Input/RichText` listée)
- [x] `npm run dev` — playground `/composant/input/inputRichText` (vérification visuelle des 8 variantes : simple, hint, erreur, succès, readonly, disabled, lecture seule, sortie HTML)
- [x] `npm run story:dev` — story `Input/RichText` avec docs
## Fichiers
- `app/components/malio/input/InputRichText.vue` — composant
- `app/components/malio/input/InputRichText.test.ts` — tests
- `.playground/pages/composant/input/inputRichText.vue` — playground
- `app/story/input/inputRichText.story.vue` — story Histoire
- `histoire.config.ts` — alias ESM + `optimizeDeps` pour `tiptap-markdown` (sinon Histoire choisit la build UMD)
- `CHANGELOG.md`, `COMPONENTS.md` — documentation
Reviewed-on: #37
Co-authored-by: matthieu <matthieu@yuno.malio.fr>
Co-committed-by: matthieu <matthieu@yuno.malio.fr>
18 KiB
@malio/layer-ui — Composants
Tous les composants sont auto-importés avec le préfixe Malio. Utiliser v-model pour le binding bidirectionnel sur les composants de formulaire.
MalioInputText
Champ texte avec label, icône optionnelle et support de masque de saisie.
| Prop | Type | Défaut | Description |
|---|---|---|---|
id |
string |
auto | Identifiant HTML |
label |
string |
'' |
Label du champ |
modelValue |
string | null |
undefined |
Valeur (v-model) |
disabled |
boolean |
false |
Désactive le champ |
readonly |
boolean |
false |
Lecture seule |
required |
boolean |
false |
Champ requis |
hint |
string |
'' |
Message d'aide |
error |
string |
'' |
Message d'erreur |
success |
string |
'' |
Message de succès |
iconName |
string |
'' |
Icône Iconify (ex: mdi:magnify) |
iconPosition |
'left' | 'right' |
'right' |
Position de l'icône |
iconSize |
string | number |
24 |
Taille icône |
iconColor |
string |
'text-m-muted' |
Classe couleur icône |
mask |
string | MaskInputOptions |
— | Masque de saisie (maska) |
maxLength |
number | string |
— | Longueur max |
minLength |
number | string |
— | Longueur min |
inputClass |
string |
'' |
Classes CSS input |
labelClass |
string |
'' |
Classes CSS label |
groupClass |
string |
'' |
Classes CSS conteneur |
Events : update:modelValue(value: string)
<MalioInputText v-model="nom" label="Nom" />
<MalioInputText v-model="search" label="Recherche" icon-name="mdi:magnify" />
<MalioInputText v-model="tel" label="Téléphone" mask="## ## ## ## ##" />
<MalioInputText v-model="email" label="Email" error="Email invalide" />
<MalioInputText v-model="info" label="Info" disabled hint="Champ désactivé" />
MalioInputPassword
Champ mot de passe avec toggle visibilité.
| Prop | Type | Défaut | Description |
|---|---|---|---|
modelValue |
string | null |
undefined |
Valeur (v-model) |
label |
string |
'' |
Label |
displayIcon |
boolean |
true |
Afficher l'icône toggle |
disabled |
boolean |
false |
Désactivé |
readonly |
boolean |
false |
Lecture seule |
hint |
string |
'' |
Message d'aide |
error |
string |
'' |
Message d'erreur |
success |
string |
'' |
Message de succès |
Events : update:modelValue(value: string)
<MalioInputPassword v-model="password" label="Mot de passe" />
<MalioInputPassword v-model="password" label="Sans icône" :display-icon="false" />
MalioInputAmount
Champ montant avec icône devise (euro par défaut).
| Prop | Type | Défaut | Description |
|---|---|---|---|
modelValue |
string | null |
undefined |
Valeur (v-model) |
label |
string |
'' |
Label |
iconName |
string |
'mdi:currency-eur' |
Icône devise |
disabled |
boolean |
false |
Désactivé |
error |
string |
'' |
Message d'erreur |
Events : update:modelValue(value: string)
<MalioInputAmount v-model="montant" label="Montant TTC" />
<MalioInputAmount v-model="prix" label="Prix" error="Montant invalide" />
MalioInputNumber
Champ numérique avec boutons +/-.
| Prop | Type | Défaut | Description |
|---|---|---|---|
modelValue |
string | null |
undefined |
Valeur (v-model) |
label |
string |
'' |
Label |
min |
number | string |
— | Valeur minimum |
max |
number | string |
— | Valeur maximum |
disabled |
boolean |
false |
Désactivé |
error |
string |
'' |
Message d'erreur |
Events : update:modelValue(value: string)
<MalioInputNumber v-model="quantite" label="Quantité" min="0" max="100" />
MalioInputTextArea
Zone de texte multiligne avec compteur et redimensionnement.
| Prop | Type | Défaut | Description |
|---|---|---|---|
modelValue |
string | null |
undefined |
Valeur (v-model) |
label |
string |
'' |
Label |
size |
number | string |
2 |
Nombre de lignes |
resize |
'none' | 'both' | 'horizontal' | 'vertical' |
'both' |
Mode redimensionnement |
maxLength |
number |
800 |
Longueur max |
showCounter |
boolean |
false |
Afficher le compteur |
disabled |
boolean |
false |
Désactivé |
error |
string |
'' |
Message d'erreur |
Events : update:modelValue(value: string)
<MalioInputTextArea v-model="commentaire" label="Commentaire" :show-counter="true" />
<MalioInputTextArea v-model="note" label="Note" resize="vertical" :size="4" />
MalioInputRichText
Éditeur de texte riche basé sur TipTap v3 + StarterKit + tiptap-markdown. Toolbar avec gras, italique, barré, titres H2/H3, listes, citation, code, code-block, lien, undo/redo. Sortie en markdown (par défaut) ou HTML.
| Prop | Type | Défaut | Description |
|---|---|---|---|
id |
string |
auto | Identifiant HTML |
label |
string |
'' |
Label affiché au-dessus de l'éditeur |
modelValue |
string | null |
undefined |
Contenu (v-model) |
placeholder |
string |
'' |
Texte affiché quand vide |
minHeight |
string |
'160px' |
Hauteur min de la zone d'édition |
editable |
boolean |
true |
false → mode affichage seul (toolbar masquée) |
disabled |
boolean |
false |
Désactive l'édition et la toolbar |
readonly |
boolean |
false |
Lecture seule (toolbar visible mais désactivée) |
hint |
string |
'' |
Message d'aide |
error |
string |
'' |
Message d'erreur |
success |
string |
'' |
Message de succès |
outputFormat |
'markdown' | 'html' |
'markdown' |
Format émis dans update:modelValue |
groupClass |
string |
'' |
Classes CSS conteneur (twMerge) |
labelClass |
string |
'' |
Classes CSS label (twMerge) |
editorClass |
string |
'' |
Classes CSS wrapper éditeur (twMerge) |
Events : update:modelValue(value: string)
<MalioInputRichText v-model="note" label="Note" placeholder="Écrire ici…" />
<MalioInputRichText v-model="cr" label="Compte-rendu" error="Trop court" />
<MalioInputRichText v-model="article" label="Article" output-format="html" min-height="240px" />
<MalioInputRichText :model-value="content" :editable="false" />
MalioInputUpload
Champ d'upload de fichier.
| Prop | Type | Défaut | Description |
|---|---|---|---|
modelValue |
string | null |
undefined |
Nom du fichier (v-model) |
label |
string |
'' |
Label |
accept |
string |
'' |
Types de fichiers acceptés |
displayIcon |
boolean |
true |
Afficher l'icône |
disabled |
boolean |
false |
Désactivé |
error |
string |
'' |
Message d'erreur |
Events : update:modelValue(value: string), file-selected(file: File)
<MalioInputUpload v-model="fileName" label="Document" accept=".pdf,.doc" @file-selected="onFile" />
MalioSelect
Liste déroulante.
| Prop | Type | Défaut | Description |
|---|---|---|---|
modelValue |
string | number | null |
requis | Valeur sélectionnée (v-model) |
options |
{ value: string | number, text: string }[] |
[] |
Options disponibles |
emptyOptionLabel |
string |
'' |
Placeholder option vide |
label |
string |
'' |
Label |
hint |
string |
'' |
Message d'aide |
error |
string |
'' |
Message d'erreur |
success |
string |
'' |
Message de succès |
disabled |
boolean |
false |
Désactivé |
groupClass |
string |
'' |
Classes CSS conteneur (twMerge) |
minWidth |
string |
'w-96' |
Classe largeur minimum |
maxWidth |
string |
'' |
Classe largeur maximum |
rounded |
string |
'rounded-md' |
Classe border-radius |
textField |
string |
'text-lg' |
Classe taille texte bouton |
textValue |
string |
'text-lg' |
Classe taille texte valeur |
textLabel |
string |
'text-sm' |
Classe taille texte label |
Events : update:modelValue(value: string | number | null)
Slots : icon (icône dropdown custom)
<MalioSelect v-model="pays" label="Pays" :options="[{ value: 'FR', text: 'France' }, { value: 'BE', text: 'Belgique' }]" />
<MalioSelect v-model="ville" label="Ville" :options="villes" empty-option-label="Choisir..." />
<MalioSelect v-model="civilite" label="Civilité" :options="civilites" group-class="mt-0" />
MalioSelectCheckbox
Liste déroulante multi-sélection avec checkboxes.
| Prop | Type | Défaut | Description |
|---|---|---|---|
modelValue |
(string | number)[] |
requis | Valeurs sélectionnées (v-model) |
options |
{ value: string | number, text: string }[] |
[] |
Options |
displayTag |
boolean |
false |
Afficher les tags sélectionnés |
displaySelectAll |
boolean |
false |
Afficher "Tout sélectionner" |
selectAllLabel |
string |
'Tout sélectionner' |
Texte du sélecteur global |
label |
string |
'' |
Label |
disabled |
boolean |
false |
Désactivé |
Events : update:modelValue(value: (string | number)[])
<MalioSelectCheckbox v-model="competences" label="Compétences" :options="skills" :display-tag="true" />
<MalioSelectCheckbox v-model="sites" label="Sites" :options="sitesList" :display-select-all="true" />
MalioCheckbox
Case à cocher.
| Prop | Type | Défaut | Description |
|---|---|---|---|
modelValue |
boolean | null |
undefined |
Valeur (v-model) |
label |
string |
'' |
Label |
disabled |
boolean |
false |
Désactivé |
readonly |
boolean |
false |
Lecture seule |
error |
string |
'' |
Message d'erreur |
Events : update:modelValue(value: boolean)
<MalioCheckbox v-model="accepte" label="J'accepte les conditions" />
<MalioCheckbox v-model="newsletter" label="Newsletter" disabled />
MalioRadioButton
Bouton radio (à utiliser en groupe avec le même name).
| Prop | Type | Défaut | Description |
|---|---|---|---|
modelValue |
string | number | boolean | null |
undefined |
Valeur du groupe (v-model) |
value |
string | number | boolean | null |
undefined |
Valeur de cette option |
label |
string |
'' |
Label |
name |
string |
'' |
Nom du groupe radio |
disabled |
boolean |
false |
Désactivé |
readonly |
boolean |
false |
Lecture seule |
Events : update:modelValue(value: string | number | boolean | null)
<MalioRadioButton v-model="civilite" name="civilite" value="M" label="Monsieur" />
<MalioRadioButton v-model="civilite" name="civilite" value="Mme" label="Madame" />
MalioTime
Sélecteur d'heure.
| Prop | Type | Défaut | Description |
|---|---|---|---|
modelValue |
string | null |
undefined |
Heure au format HH:mm (v-model) |
label |
string |
'' |
Label |
disabled |
boolean |
false |
Désactivé |
readonly |
boolean |
false |
Lecture seule |
error |
string |
'' |
Message d'erreur |
Events : update:modelValue(value: string)
<MalioTime v-model="heure" label="Heure de début" />
<MalioTime v-model="fin" label="Heure de fin" readonly />
MalioButton
Bouton d'action avec 4 variantes visuelles et icône optionnelle.
| Prop | Type | Défaut | Description |
|---|---|---|---|
label |
string |
'' |
Texte du bouton (ou slot par défaut) |
variant |
'primary' | 'secondary' | 'tertiary' | 'danger' |
'primary' |
Variante visuelle |
disabled |
boolean |
false |
Désactivé |
buttonClass |
string |
'' |
Classes CSS additionnelles (twMerge) |
iconName |
string |
'' |
Icône Iconify |
iconPosition |
'left' | 'right' |
'right' |
Position icône |
iconSize |
string | number |
16 |
Taille icône |
Events : click(e: MouseEvent)
Slots : default (contenu du bouton, remplace label)
<MalioButton label="Valider" />
<MalioButton label="Modifier" variant="secondary" icon-name="mdi:pencil" icon-position="left" />
<MalioButton label="Voir plus" variant="tertiary" />
<MalioButton label="Supprimer" variant="danger" icon-name="mdi:trash" icon-position="left" />
<MalioButton label="Pleine largeur" button-class="w-full" />
MalioButtonIcon
Bouton icône seul (sans texte).
| Prop | Type | Défaut | Description |
|---|---|---|---|
icon |
string |
requis | Icône Iconify |
ariaLabel |
string |
requis | Label accessible |
variant |
'filled' | 'ghost' |
'filled' |
Variante visuelle |
disabled |
boolean |
false |
Désactivé |
buttonClass |
string |
'' |
Classes CSS additionnelles |
iconSize |
string | number |
24 |
Taille icône |
Events : click(e: MouseEvent)
<MalioButtonIcon icon="mdi:pencil" aria-label="Modifier" />
<MalioButtonIcon icon="mdi:trash" aria-label="Supprimer" variant="ghost" />
MalioTabList
Navigation par onglets avec contenu dynamique.
| Prop | Type | Défaut | Description |
|---|---|---|---|
modelValue |
string |
undefined |
Onglet actif (v-model) |
tabs |
{ key: string, label: string, icon?: string }[] |
requis | Liste des onglets |
Events : update:modelValue(value: string)
Slots : Un slot nommé par tab.key pour le contenu de chaque onglet
<MalioTabList v-model="activeTab" :tabs="[{ key: 'infos', label: 'Informations' }, { key: 'docs', label: 'Documents', icon: 'mdi:file' }]">
<template #infos>Contenu infos</template>
<template #docs>Contenu docs</template>
</MalioTabList>
MalioSidebar
Barre latérale de navigation rétractable.
| Prop | Type | Défaut | Description |
|---|---|---|---|
modelValue |
boolean |
undefined |
État ouvert/fermé (v-model) |
sections |
SidebarSection[] |
requis | Sections de navigation |
sidebarClass |
string |
'' |
Classes CSS sidebar |
toggleClass |
string |
'' |
Classes CSS bouton toggle |
Type SidebarSection : { title?: string, items: { label: string, icon?: string, to?: string, href?: string, active?: boolean }[] }
Events : update:modelValue(value: boolean)
Slots : logo (sidebar ouverte), logo-collapsed (sidebar fermée)
<MalioSidebar v-model="isOpen" :sections="menuSections">
<template #logo><img src="/logo.png" /></template>
<template #logo-collapsed><img src="/logo-small.png" /></template>
</MalioSidebar>
MalioDrawer
Panneau latéral (drawer) qui s'ouvre depuis la droite avec backdrop semi-transparent.
| Prop | Type | Défaut | Description |
|---|---|---|---|
id |
string |
auto | Identifiant HTML |
modelValue |
boolean |
undefined |
État ouvert/fermé (v-model) |
title |
string |
'' |
Titre affiché dans le header |
showClose |
boolean |
true |
Afficher le bouton de fermeture (croix) |
drawerClass |
string |
'' |
Classes CSS panneau (twMerge) |
Events : update:modelValue(value: boolean)
Slots : default (contenu du drawer)
<MalioDrawer v-model="isOpen" title="Détails">
<p>Contenu du drawer</p>
</MalioDrawer>
<MalioDrawer v-model="isOpen" title="Sans croix" :show-close="false">
<p>Fermeture uniquement via backdrop</p>
</MalioDrawer>
<MalioDrawer v-model="isOpen" title="Large" drawer-class="max-w-2xl">
<p>Drawer plus large</p>
</MalioDrawer>
MalioDataTable
Tableau de données presentational avec pagination, filtres par slots et lignes cliquables.
| Prop | Type | Défaut | Description |
|---|---|---|---|
id |
string |
auto | Identifiant HTML |
columns |
{ key: string, label: string }[] |
requis | Définition des colonnes |
items |
Record<string, unknown>[] |
requis | Données à afficher |
totalItems |
number |
requis | Total pour la pagination |
page |
number |
1 |
Page courante (v-model) |
perPage |
number |
10 |
Lignes par page (v-model) |
perPageOptions |
number[] |
[10, 25, 50] |
Options du sélecteur de lignes |
rowClickable |
boolean |
true |
Lignes cliquables (cursor pointer + hover) |
tableClass |
string |
'' |
Classes CSS sur <table> (twMerge) |
emptyMessage |
string |
'Aucune donnée' |
Message si items vide |
Events : update:page(value: number), update:per-page(value: number), row-click(item: Record<string, unknown>)
Slots : #header-{key} (filtre dans le <th>, placeholder = label), #cell-{key} (contenu du <td>), #empty (état vide)
<!-- Avec filtres et pagination -->
<MalioDataTable
:columns="[{ key: 'nom', label: 'Nom' }, { key: 'ville', label: 'Ville' }]"
:items="data"
:total-items="total"
v-model:page="page"
v-model:per-page="perPage"
@row-click="router.push(`/contact/${$event.id}`)"
>
<template #header-nom>
<input v-model="filtreNom" placeholder="Nom" class="w-full border-0 border-b border-black bg-transparent px-0 py-1 text-sm outline-none">
</template>
<template #header-ville>
<select v-model="filtreVille" class="w-full appearance-none border-0 border-b border-black bg-transparent px-0 py-1 text-sm outline-none">
<option value="">Ville</option>
<option v-for="v in villes" :key="v" :value="v">{{ v }}</option>
</select>
</template>
<template #cell-nom="{ item }">
<strong>{{ item.nom }}</strong>
</template>
</MalioDataTable>
<!-- Simple sans filtres -->
<MalioDataTable
:columns="columns"
:items="data"
:total-items="total"
v-model:page="page"
v-model:per-page="perPage"
/>