feat : ajouts du composant input text area et fix sur le input text

This commit is contained in:
2026-02-27 09:24:37 +01:00
parent 0a3cf50576
commit 594708e71c
8 changed files with 767 additions and 85 deletions

View File

@@ -2,7 +2,7 @@
<div class="grid grid-cols-1 items-start gap-6 md:grid-cols-2"> <div class="grid grid-cols-1 items-start gap-6 md:grid-cols-2">
<div class="rounded-lg border p-4"> <div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Simple</h2> <h2 class="mb-4 text-xl font-bold">Simple</h2>
<MalioInputText v-model="simpleValue"/> <MalioInputText/>
</div> </div>
<div class="rounded-lg border p-4"> <div class="rounded-lg border p-4">
@@ -16,7 +16,7 @@
</div> </div>
<div class="rounded-lg border p-4"> <div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Avec icône</h2> <h2 class="mb-4 text-xl font-bold">Avec icône à droite</h2>
<MalioInputText <MalioInputText
v-model="searchValue" v-model="searchValue"
label="Recherche" label="Recherche"
@@ -25,6 +25,16 @@
/> />
</div> </div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Avec icône à gauche</h2>
<MalioInputText
label="Recherche"
icon-name="mdi:magnify"
icon-size="20"
icon-position="left"
/>
</div>
<div class="rounded-lg border p-4"> <div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Désactivé</h2> <h2 class="mb-4 text-xl font-bold">Désactivé</h2>
<MalioInputText <MalioInputText
@@ -142,7 +152,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const simpleValue = ref('') import { computed, ref } from 'vue'
const nameValue = ref('') const nameValue = ref('')
const searchValue = ref('') const searchValue = ref('')
const codeValue = ref('') const codeValue = ref('')

View File

@@ -0,0 +1,104 @@
<template>
<div class="grid grid-cols-1 items-start gap-6 p-4 md:grid-cols-2">
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Simple</h2>
<MalioInputTextArea/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Avec label + hint</h2>
<MalioInputTextArea
v-model="hintValue"
label="Description"
hint="Ajoutez un contexte clair"
:size="4"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Avec icône</h2>
<MalioInputTextArea
v-model="iconValue"
label="Commentaire"
icon-name="mdi:comment-text-outline"
:size="3"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Erreur / Succès</h2>
<div class="space-y-4">
<MalioInputTextArea
v-model="errorValue"
label="Message"
error="Le message est trop court"
:size="3"
/>
<MalioInputTextArea
v-model="successValue"
label="Message"
success="Message valide"
:size="3"
/>
</div>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Readonly / Disabled</h2>
<div class="space-y-4">
<MalioInputTextArea
model-value="Contenu en lecture seule"
label="Readonly"
readonly
:size="3"
/>
<MalioInputTextArea
model-value="Champ indisponible"
label="Disabled"
disabled
:size="3"
/>
</div>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Resize avec limites</h2>
<MalioInputTextArea
v-model="resizeValue"
label="Notes"
resize="both"
:size="4"
:min-resize-width="300"
:max-resize-width="700"
:min-resize-height="120"
:max-resize-height="280"
hint="Resize limite en largeur et hauteur"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Compteur (interne au composant)</h2>
<MalioInputTextArea
v-model="counterValue"
label="Message"
:size="5"
:max-length="120"
:show-counter="true"
hint="Le compteur est en bas a gauche"
/>
</div>
</div>
</template>
<script setup lang="ts">
import {ref} from 'vue'
import MalioInputTextArea from '../../../app/components/malio/InputTextArea.vue'
const hintValue = ref('')
const iconValue = ref('')
const errorValue = ref('abc')
const successValue = ref('Contenu ok')
const resizeValue = ref('Vous pouvez redimensionner ce champ.')
const counterValue = ref('')
</script>

View File

@@ -15,7 +15,7 @@
:key="item.name" :key="item.name"
type="button" type="button"
class="rounded px-3 py-2 text-left text-black font-bold hover:bg-m-primary hover:text-white" class="rounded px-3 py-2 text-left text-black font-bold hover:bg-m-primary hover:text-white"
:class="selectedName === item.name ? 'bg-m-secondary text-white ' : ''" :class="selectedName === item.name ? 'bg-m-secondary text-white' : ''"
@click="selectOrToggle(item.name)" @click="selectOrToggle(item.name)"
> >
{{ item.label }} {{ item.label }}
@@ -32,10 +32,13 @@
v-else-if="selectedName" v-else-if="selectedName"
class="text-gray-700" class="text-gray-700"
> >
Page de demo introuvable: <code>.playground/pages/composant/{{ selectedDemoFileName }}.vue</code> Page de demo introuvable:
<code>.playground/pages/composant/{{ selectedDemoFileName }}.vue</code>
</p> </p>
<div v-else> <div v-else>
<h1 class="text-2xl font-semibold text-gray-900">Playground composants</h1> <h1 class="text-2xl font-semibold text-gray-900">
Playground composants
</h1>
<p class="mt-2 text-gray-600"> <p class="mt-2 text-gray-600">
Selectionne un composant dans la liste pour afficher sa page de demo. Selectionne un composant dans la liste pour afficher sa page de demo.
</p> </p>
@@ -45,6 +48,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch, shallowRef } from 'vue'
type LoadedModule = { type LoadedModule = {
default: unknown default: unknown
} }
@@ -52,40 +57,42 @@ type LoadedModule = {
type Item = { type Item = {
name: string name: string
label: string label: string
demoComponent?: unknown
} }
const componentModules = import.meta.glob('../../app/components/malio/*.vue', { eager: true }) as Record<string, LoadedModule> const componentModules = import.meta.glob('../../app/components/malio/*.vue')
const demoModules = import.meta.glob('./composant/*.vue', { eager: true }) as Record<string, LoadedModule> const demoModules = import.meta.glob('./composant/*.vue')
const demoByName = Object.fromEntries( const demoByName: Record<string, () => Promise<LoadedModule>> =
Object.entries(demoModules).map(([file, mod]) => { Object.fromEntries(
Object.entries(demoModules).map(([file, loader]) => {
const name = file.split('/').pop()?.replace('.vue', '') ?? ''
return [name.toLowerCase(), loader as () => Promise<LoadedModule>]
}),
)
const items = computed<Item[]>(() =>
Object.keys(componentModules).map((file) => {
const name = file.split('/').pop()?.replace('.vue', '') ?? '' const name = file.split('/').pop()?.replace('.vue', '') ?? ''
return [name.toLowerCase(), mod.default]
}),
)
const items = computed(() =>
Object.entries(componentModules).map(([file]) => {
const name = file.split('/').pop()?.replace('.vue', '') ?? ''
return { return {
name, name,
label: name, label: name,
demoComponent: demoByName[name.toLowerCase()],
} }
}) as Item[], }),
) )
const selectedName = ref('') const selectedName = ref('')
const hasInitializedSelection = ref(false) const hasInitializedSelection = ref(false)
watchEffect(() => { watch(
if (!hasInitializedSelection.value && items.value.length > 0) { items,
selectedName.value = items.value[0].name (val) => {
hasInitializedSelection.value = true if (!hasInitializedSelection.value && val.length > 0) {
} selectedName.value = val[0].name
}) hasInitializedSelection.value = true
}
},
{ immediate: true },
)
function selectOrToggle(name: string) { function selectOrToggle(name: string) {
selectedName.value = selectedName.value === name ? '' : name selectedName.value = selectedName.value === name ? '' : name
@@ -95,9 +102,23 @@ function clearSelection() {
selectedName.value = '' selectedName.value = ''
} }
const selectedDemoComponent = computed(() => const selectedDemoComponent = shallowRef<unknown>(null)
items.value.find((item) => item.name === selectedName.value)?.demoComponent,
) watch(selectedName, async (name) => {
if (!name) {
selectedDemoComponent.value = null
return
}
const loader = demoByName[name.toLowerCase()]
if (!loader) {
selectedDemoComponent.value = null
return
}
const mod = await loader()
selectedDemoComponent.value = mod.default
})
const selectedDemoFileName = computed(() => { const selectedDemoFileName = computed(() => {
const name = selectedName.value const name = selectedName.value

View File

@@ -8,37 +8,42 @@
v-maska="mask" v-maska="mask"
:name="name" :name="name"
:autocomplete="autocomplete" :autocomplete="autocomplete"
class="floating-input grow-height peer min-h-[40px] w-full border bg-white pl-3 pr-3 py-1 outline-none focus:border-2" class="floating-input grow-height peer min-h-[40px] w-full border bg-white pl-3 pr-3 py-1 outline-none placeholder:text-transparent focus:border-2 "
:class="[ :class="[
disabled ? 'cursor-not-allowed text-black/60 [&:not(:placeholder-shown)]:border-m-muted border-m-muted' : 'cursor-text', disabled ? 'cursor-not-allowed text-black/60 [&:not(:placeholder-shown)]:border-m-muted border-m-muted' : 'cursor-text',
hasError hasError
? 'border-m-error focus:border-m-error focus:pl-[11px] [&:not(:placeholder-shown)]:border-m-error' ? 'border-m-error focus:border-m-error [&:not(:placeholder-shown)]:border-m-error'
: hasSuccess : hasSuccess
? 'border-m-success focus:border-m-success focus:pl-[11px] [&:not(:placeholder-shown)]:border-m-success' ? 'border-m-success focus:border-m-success [&:not(:placeholder-shown)]:border-m-success'
: 'border-m-muted focus:border-m-primary focus:pl-[11px]', : 'border-m-muted focus:border-m-primary',
textInput, textInput,
iconInputPaddingClass, iconInputPaddingClass,
focusPaddingClass,
rounded, rounded,
]" ]"
:required="required" :required="required"
:maxlength="maxLength" :maxlength="maxLength"
:minlength="minLength" :minlength="minLength"
:disabled="disabled" :disabled="disabled"
:value="modelValue ?? ''" :value="currentValue"
:readonly="readonly" :readonly="readonly"
:aria-invalid="!!error" :aria-invalid="!!error"
:aria-describedby="describedBy" :aria-describedby="describedBy"
v-bind="attrs" v-bind="attrs"
placeholder=" " placeholder="_"
type="text" type="text"
@input="onInput" @input="onInput"
@focus="isFocused = true"
@blur="isFocused = false"
> >
<label <label
v-if="label" v-if="label"
:for="inputId" :for="inputId"
class="floating-label absolute left-3 top-2 mt-1 origin-left transition-transform duration-150 font-medium" class="floating-label absolute top-2 mt-1 inline-block origin-left transition-transform duration-150 font-medium"
:class="[ :class="[
labelPositionClass,
shouldFloatLabel ? '-translate-y-[1.15rem] scale-90' : '',
disabled ? 'peer-[&:not(:placeholder-shown):not(:focus)]:text-black/60' : '', disabled ? 'peer-[&:not(:placeholder-shown):not(:focus)]:text-black/60' : '',
hasError hasError
? 'text-m-error' ? 'text-m-error'
@@ -59,7 +64,7 @@
data-test="icon" data-test="icon"
:class="[ :class="[
hasError ? 'text-m-error' : hasSuccess ? 'text-m-success' : '', hasError ? 'text-m-error' : hasSuccess ? 'text-m-success' : '',
'pointer-events-none absolute right-2 top-1/2 -translate-y-1/2', iconPositionClass,
iconColor, iconColor,
]" ]"
/> />
@@ -85,7 +90,7 @@
import type {MaskInputOptions} from 'maska' import type {MaskInputOptions} from 'maska'
import {vMaska} from 'maska/vue' import {vMaska} from 'maska/vue'
import {computed, useAttrs, useId} from 'vue' import {computed, ref, useAttrs, useId} from 'vue'
import { Icon as IconifyIcon } from '@iconify/vue' import { Icon as IconifyIcon } from '@iconify/vue'
defineOptions({name: 'MalioInputText', inheritAttrs: false}) defineOptions({name: 'MalioInputText', inheritAttrs: false})
@@ -110,6 +115,7 @@ const props = withDefaults(
error?: string error?: string
success?: string success?: string
iconName?: string iconName?: string
iconPosition?: 'left' | 'right'
rounded?: string rounded?: string
iconSize?: string | number iconSize?: string | number
iconColor?: string iconColor?: string
@@ -118,9 +124,10 @@ const props = withDefaults(
{ {
id: '', id: '',
name: '', name: '',
autocomplete: '', autocomplete: 'off',
modelValue: undefined, modelValue: undefined,
iconName: '', iconName: '',
iconPosition: 'right',
label: '', label: '',
minWidth: 'w-96', minWidth: 'w-96',
maxWidth: '', maxWidth: '',
@@ -143,8 +150,13 @@ const props = withDefaults(
const attrs = useAttrs() const attrs = useAttrs()
const generatedId = useId() const generatedId = useId()
const localValue = ref('')
const isFocused = ref(false)
const inputId = computed(() => props.id?.toString() || `malio-input-text-${generatedId}`) const inputId = computed(() => props.id?.toString() || `malio-input-text-${generatedId}`)
const isControlled = computed(() => props.modelValue !== undefined)
const currentValue = computed(() => (isControlled.value ? (props.modelValue ?? '') : localValue.value))
const shouldFloatLabel = computed(() => isFocused.value || currentValue.value.length > 0)
const hasError = computed(() => !!props.error) const hasError = computed(() => !!props.error)
const hasSuccess = computed(() => !!props.success) const hasSuccess = computed(() => !!props.success)
@@ -162,20 +174,34 @@ const emit = defineEmits<{
const onInput = (event: Event) => { const onInput = (event: Event) => {
const target = event.target as HTMLInputElement const target = event.target as HTMLInputElement
if (!isControlled.value) {
localValue.value = target.value
}
emit('update:modelValue', target.value) emit('update:modelValue', target.value)
} }
const iconInputPaddingClass = computed(() => { const iconInputPaddingClass = computed(() => {
return props.iconName ? 'pr-10' : '' if (!props.iconName) return ''
return props.iconPosition === 'left' ? '!pl-11 !pr-3' : '!pl-3'
})
const labelPositionClass = computed(() => {
if (props.iconName && props.iconPosition === 'left') return 'left-8'
return 'left-3'
})
const focusPaddingClass = computed(() => {
if (props.iconName && props.iconPosition === 'left') return 'focus:!pl-11'
return 'focus:pl-[11px]'
})
const iconPositionClass = computed(() => {
const sideClass = props.iconPosition === 'left' ? 'left-2' : 'right-2'
return `pointer-events-none absolute ${sideClass} top-1/2 -translate-y-1/2`
}) })
</script> </script>
<style scoped> <style scoped>
.floating-input:focus + label,
.floating-input:not(:placeholder-shown) + label {
transform: translateY(-1.15rem) scale(0.9);
}
.floating-label { .floating-label {
background: white; background: white;
padding: 0 0.25rem; padding: 0 0.25rem;

View File

@@ -0,0 +1,185 @@
<template>
<div
class="relative mt-4 w-full"
>
<textarea
:id="inputId"
:name="name"
:autocomplete="autocomplete"
class="floating-input peer w-full border bg-white pl-3 pr-3 py-1 outline-none placeholder:text-transparent focus:border-2 overflow-auto"
:class="[
disabled ? 'cursor-not-allowed text-black/60 border-m-muted' : 'cursor-text',
hasError
? 'border-m-error focus:border-m-error focus:pl-[11px]'
: hasSuccess
? 'border-m-success focus:border-m-success focus:pl-[11px]'
: 'border-m-muted focus:border-m-primary focus:pl-[11px]',
textInput,
showCounterComputed ? 'pb-6' : '',
rounded,
]"
:required="required"
:maxlength="maxLength"
:rows="rowsCount"
:disabled="disabled"
:value="currentValue"
:readonly="readonly"
:aria-invalid="hasError"
:aria-describedby="describedBy"
:style="textareaStyle"
v-bind="attrs"
placeholder="_"
@input="onInput"
@focus="isFocused = true"
@blur="isFocused = false"
/>
<label
v-if="label"
:for="inputId"
class="floating-label absolute left-3 top-2 mt-1 inline-block origin-left transition-transform duration-150 font-medium"
:class="[
shouldFloatLabel ? '-translate-y-[1.15rem] scale-90' : '',
disabled ? 'text-black/60' : '',
hasError
? 'text-m-error'
: hasSuccess
? 'text-m-success'
: isFocused ? 'text-m-primary' : shouldFloatLabel ? 'text-black' : 'text-m-muted',
textLabel,
]"
>
{{ label }}
</label>
<span
v-if="showCounterComputed"
class="pointer-events-none absolute bottom-2 left-3 text-xs text-m-muted"
>
{{ currentLength }}/{{ maxLength }}
</span>
</div>
<div
v-if="hasError || hasSuccess || hint"
class="mt-1 flex items-center justify-between gap-2 text-xs"
>
<p
:id="`${inputId}-describedby`"
:class="[
hasError
? 'text-m-error'
: hasSuccess
? 'text-m-success'
: 'text-m-muted',
'ml-[2px]',
]"
>
{{ error || success || hint }}
</p>
</div>
</template>
<script setup lang="ts">
import {computed, ref, useAttrs, useId} from 'vue'
defineOptions({name: 'MalioInputTextArea', inheritAttrs: false})
const props = withDefaults(
defineProps<{
id?: string
label?: string
name?: string
autocomplete?: string
modelValue?: string | null | undefined
size?: number | string
textInput?: string
textLabel?: string
resize?: 'none' | 'both' | 'horizontal' | 'vertical'
minResizeWidth?: number
maxResizeWidth?: number
minResizeHeight?: number
maxResizeHeight?: number
required?: boolean
maxLength?: number
showCounter?: boolean
disabled?: boolean
readonly?: boolean
hint?: string
error?: string
success?: string
rounded?: string
}>(),
{
id: '',
name: '',
autocomplete: 'off',
modelValue: undefined,
label: '',
size: 2,
textInput: 'text-lg',
required: false,
maxLength: 800,
showCounter: false,
readonly: false,
textLabel: 'text-sm',
disabled: false,
rounded: 'rounded-md',
hint: '',
error: '',
success: '',
resize: 'both',
minResizeWidth: 280,
maxResizeWidth: 640,
minResizeHeight: 40,
maxResizeHeight: 320,
},
)
const attrs = useAttrs()
const generatedId = useId()
const localValue = ref('')
const isFocused = ref(false)
const inputId = computed(() => props.id?.toString() || `malio-input-textarea-${generatedId}`)
const isControlled = computed(() => props.modelValue !== undefined)
const currentValue = computed(() => (isControlled.value ? (props.modelValue ?? '') : localValue.value))
const shouldFloatLabel = computed(() => isFocused.value || currentValue.value.length > 0)
const hasError = computed(() => !!props.error)
const hasSuccess = computed(() => !!props.success && !hasError.value)
const rowsCount = computed(() => Math.max(1, Number(props.size || 3)))
const currentLength = computed(() => (currentValue.value ?? '').length)
const showCounterComputed = computed(() =>
props.showCounter && Number(props.maxLength) > 0
)
const toCssSize = (value: number | string) => (typeof value === 'number' ? `${value}px` : value)
const textareaStyle = computed(() => ({
resize: props.resize,
minWidth: toCssSize(props.minResizeWidth),
maxWidth: toCssSize(props.maxResizeWidth),
minHeight: toCssSize(props.minResizeHeight),
maxHeight: toCssSize(props.maxResizeHeight),
}))
const describedBy = computed(() =>
(hasError.value || hasSuccess.value || !!props.hint) ? `${inputId.value}-describedby` : undefined,
)
const emit = defineEmits<{
(event: 'update:modelValue', value: string): void
}>()
const onInput = (event: Event) => {
const target = event.target as HTMLTextAreaElement
if (!isControlled.value) {
localValue.value = target.value
}
emit('update:modelValue', target.value)
}
</script>
<style scoped>
.floating-label {
background: white;
padding: 0 0.25rem;
}
</style>

View File

@@ -1,56 +1,200 @@
<template> <template>
<Story title="Input/Text"> <Story
title="Input/Text"
>
<MalioInputText/> <MalioInputText/>
</Story> </Story>
</template> </template>
<docs lang="md"> <docs lang="md">
# Input Text # MalioInputText
## Liste des props Composant input texte avec masque optionnel (maska), label flottant,
états visuels (erreur / succès) et icône configurable.
Le composant Input Text permet de saisir du texte. Il peut afficher des messages d'erreur, de succès ou d'information. ------------------------------------------------------------------------
On peut lui passer plusieurs props pour personnaliser son comportement et son apparence.
- id: Identifiant HTML du champ. Si non fourni, un id est généré automatiquement.
- label: Texte du label affiché au-dessus du champ (floating label).
- name: Attribut name de linput.
- autocomplete: Attribut autocomplete de linput.
- modelValue: Valeur du champ (utilisée avec v-model) ou si valeur brut mettre des " ".
- minWidth: Classe utilitaire pour la largeur minimale du conteneur. Classe Tailwind de largeur (ex: w-64, w-full).
- maxWidth: Classe utilitaire pour la largeur maximale du conteneur. Classe Tailwind de largeur maximale.
- textInput: Classe(s) de style du texte de linput. Classe(s) Tailwind de couleur ou de typographie (ex : text-gray-700, text-sm).
- textLabel: Classe(s) de style du texte du label. Classe(s) Tailwind de couleur ou de typographie (ex : text-gray-700, text-sm).
- required: Rend le champ obligatoire (required).
- maxLength: Nombre de caractère maximal autorisé.
- minLength: Nombre de caractère minimal autorisé.
- disabled: Désactive le champ et applique le style désactivé.
- readonly: Met le champ en lecture seule.
- hint: Message informatif affiché sous le champ.
- error: Message derreur affiché sous le champ. Active le style erreur.
- success: Message de succès affiché sous le champ. Active le style succès.
- iconName: Nom de licône affichée à droite dans le champ.
- rounded: Classe utilitaire pour le rayon des coins ( rounded- ).
- iconSize: Taille de licône. (ex : 24, 26, 85 ,99, ... ).
- iconColor: Classe(s) personnalisée(s) de couleur pour licône ( text- ).
- mask: Masque de saisie pour formater la valeur :
- \# : chiffre
- A : lettre majuscule
- a : lettre minuscule
- \* : chiffre ou lettre
Événement émis : ## Props détaillées
- update:modelValue: Émis à chaque saisie pour mettre à jour v-model. ### id
Règles daffichage des messages : - Type: string
- Description: Identifiant HTML de linput.
- Comportement: Si non fourni, un id unique est généré
automatiquement.
Priorité daffichage : ### label
1) error
2) success
3) hint
- Type: string
- Description: Texte affiché comme label flottant.
- Comportement: Si absent, aucun label nest rendu.
### name
- Type: string
- Description: Attribut name de linput (utile pour les formulaires).
### autocomplete
- Type: string
- Description: Active ou configure lautocomplétion navigateur.
- Défaut: vide
### modelValue
- Type: string | null | undefined
- Description: Valeur contrôlée du composant.
- Comportement:
- Si défini composant contrôlé (v-model).
- Sinon gestion interne de létat.
### mask
- Type: string | undefined
- Description: Masque appliqué via la directive maska.
- Comportement: Formate la saisie selon les tokens définis.
------------------------------------------------------------------------
## Apparence & Style
### textInput
- Type: string
- Description: Classes CSS appliquées à linput (taille de texte,
etc.).
### textLabel
- Type: string
- Description: Classes CSS appliquées au label.
### rounded
- Type: string
- Description: Classe Tailwind pour le border-radius.
- Défaut: rounded-md
### minWidth / maxWidth
- Type: string
- Description: Classes utilitaires Tailwind pour contraindre la
largeur.
------------------------------------------------------------------------
## Validation & Contraintes
### required
- Type: boolean
- Description: Ajoute lattribut HTML required.
### maxLength
- Type: number | string
- Description: Longueur maximale autorisée.
### minLength
- Type: number | string
- Description: Longueur minimale autorisée.
### disabled
- Type: boolean
- Description: Désactive complètement le champ.
### readonly
- Type: boolean
- Description: Rend le champ non modifiable mais focusable.
------------------------------------------------------------------------
## États & Messages
### hint
- Type: string
- Description: Message daide affiché sous le champ.
### error
- Type: string
- Description: Message derreur.
- Effet:
- Active létat visuel erreur.
- aria-invalid=true
- Prioritaire sur success et hint.
### success
- Type: string
- Description: Message de succès.
- Effet:
- Actif uniquement si error est absent.
------------------------------------------------------------------------
## Icône
### iconName
- Type: string
- Description: Nom de licône (ex: mdi:magnify).
### iconSize
- Type: string | number
- Description: Taille de licône.
### iconColor
- Type: string
- Description: Couleur de licône.
------------------------------------------------------------------------
## Comportement de validation
- Aucune validation interne.
- Les états sont pilotés uniquement par les props.
## Priorité visuelle
1. error
2. success
3. neutre
------------------------------------------------------------------------
## Tokens de masque
- \# : chiffre
- A : lettre majuscule
- a : lettre minuscule
- \* : chiffre ou lettre
------------------------------------------------------------------------
## Accessibilité
- aria-invalid est activé si error existe.
- aria-describedby référence dynamiquement le message affiché.
- Fonctionne avec ou sans v-model.
------------------------------------------------------------------------
## Events
### update:modelValue
- Émis à chaque modification de linput.
- Permet lutilisation avec v-model.
</docs> </docs>
<script setup lang="ts"> <script setup lang="ts">
import MalioInputText from '../components/malio/InputText.vue' import MalioInputText from '../components/malio/InputText.vue'
</script> </script>

View File

@@ -0,0 +1,192 @@
<template>
<Story
title="Input/TextArea"
>
<MalioInputTextArea/>
</Story>
</template>
<docs lang="md">
# MalioInputTextArea
Composant textarea avec label flottant, états visuels (erreur / succès),
gestion du redimensionnement et compteur optionnel.
------------------------------------------------------------------------
## Props détaillées
### id
- Type: string
- Description: Identifiant HTML du textarea.
- Comportement: Si non fourni, un id unique est généré
automatiquement.
### label
- Type: string
- Description: Texte affiché comme label flottant.
- Comportement: Si absent, aucun label nest rendu.
### name
- Type: string
- Description: Attribut name du textarea (utile pour les formulaires).
### autocomplete
- Type: string
- Description: Active ou configure lautocomplétion navigateur.
### modelValue
- Type: string | null | undefined
- Description: Valeur contrôlée du composant.
- Comportement:
- Si défini composant contrôlé (v-model).
- Sinon gestion interne de létat.
------------------------------------------------------------------------
## Apparence & Style
### textInput
- Type: string
- Description: Classes CSS appliquées au textarea (taille de texte,
etc.).
- Défaut: text-lg
### textLabel
- Type: string
- Description: Classes CSS appliquées au label.
- Défaut: text-sm
### rounded
- Type: string
- Description: Classe Tailwind pour le border-radius.
- Défaut: rounded-md
------------------------------------------------------------------------
## Dimensions
### size
- Type: number | string
- Description: Nombre de lignes initiales (rows).
- Défaut: 2
### minResizeWidth / maxResizeWidth
- Type: number | string
- Description: Largeur minimale / maximale autorisée lors du
redimensionnement.
### minResizeHeight / maxResizeHeight
- Type: number | string
- Description: Hauteur minimale / maximale autorisée lors du
redimensionnement.
### resize
- Type: 'none' | 'both' | 'horizontal' | 'vertical'
- Description: Définit le comportement de redimensionnement CSS.
- Défaut: both
------------------------------------------------------------------------
## Validation & États
### required
- Type: boolean
- Description: Ajoute lattribut HTML required.
### maxLength
- Type: number | string
- Description: Longueur maximale autorisée.
- Effet: Limite la saisie et alimente le compteur.
### disabled
- Type: boolean
- Description: Désactive complètement le champ.
### readonly
- Type: boolean
- Description: Rend le champ non modifiable mais focusable.
### hint
- Type: string
- Description: Message daide affiché sous le champ.
### error
- Type: string
- Description: Message derreur.
- Effet:
- Active létat visuel erreur.
- aria-invalid=true
- Prioritaire sur success et hint.
### success
- Type: string
- Description: Message de succès.
- Effet:
- Actif uniquement si error est absent.
------------------------------------------------------------------------
## Compteur
### showCounter
- Type: boolean
- Description: Affiche le compteur x / maxLength.
- Condition:
- showCounter = true
- maxLength défini et > 0
- Position: Bas gauche du textarea.
- Mise à jour: Dynamique à chaque saisie.
------------------------------------------------------------------------
## Priorité daffichage
### Messages
1. error
2. success
3. hint
------------------------------------------------------------------------
## Accessibilité
- aria-invalid est activé si error existe.
- aria-describedby référence le message affiché.
- Fonctionne avec ou sans v-model.
------------------------------------------------------------------------
## Events
### update:modelValue
- Émis à chaque modification du textarea.
- Permet lutilisation avec v-model.
</docs>
<script setup lang="ts">
import MalioInputTextArea from '../components/malio/InputTextArea.vue'
</script>

View File

@@ -9,7 +9,7 @@ export default {
], ],
safelist: [ safelist: [
{ {
pattern: /(sm:|md:|lg:|xl:|2xl:)?(text|rounded|w|min-w|max-w)-.+/, pattern: /^(?:(?:sm|md|lg|xl|2xl):)?(?:text|rounded|w|min-w|max-w)-[^\s]+$/,
}, },
], ],
theme: { theme: {