2 Commits

Author SHA1 Message Date
1bbe77d391 fix: style des composants inputs + affichage des hint/error/success (#25)
All checks were successful
Release / release (push) Successful in 1m19s
| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|                  |                 |

## Description de la PR

## Modification du .env

## Check list

- [ ] Pas de régression
- [x] TU/TI/TF rédigée
- [x] TU/TI/TF OK
- [ ] CHANGELOG modifié

Co-authored-by: kevin <kevin@yuno.malio.fr>
Co-authored-by: Kevin Boudet <kevin@yuno.malio.fr>
Reviewed-on: #25
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-04-07 08:15:16 +00:00
ccc8410da0 fix: Ajout du COMPONENTS.md au livrable (#24)
All checks were successful
Release / release (push) Successful in 1m14s
| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|                  |                 |

## Description de la PR

## Modification du .env

## Check list

- [ ] Pas de régression
- [x] TU/TI/TF rédigée
- [x] TU/TI/TF OK
- [x] CHANGELOG modifié

Co-authored-by: kevin <kevin@yuno.malio.fr>
Co-authored-by: Kevin Boudet <kevin@yuno.malio.fr>
Reviewed-on: #24
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-03-26 08:15:09 +00:00
10 changed files with 374 additions and 333 deletions

View File

@@ -82,6 +82,16 @@
/> />
</div> </div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Peu d'elements (2)</h2>
<MalioSelect
v-model="shortListValue"
:options="shortOptions"
label="Civilite"
empty-option-label="Aucune selection"
/>
</div>
<div class="rounded-lg border p-4 md:col-span-2"> <div class="rounded-lg border p-4 md:col-span-2">
<h2 class="mb-4 text-xl font-bold">Liste longue</h2> <h2 class="mb-4 text-xl font-bold">Liste longue</h2>
<MalioSelect <MalioSelect
@@ -121,6 +131,11 @@ const options = [
{label: 'Portugal', value: 'pt'}, {label: 'Portugal', value: 'pt'},
] ]
const shortOptions = [
{label: 'Monsieur', value: 'M'},
{label: 'Madame', value: 'Mme'},
]
const longOptions = [ const longOptions = [
...options, ...options,
{label: 'Pays-Bas', value: 'nl'}, {label: 'Pays-Bas', value: 'nl'},
@@ -144,6 +159,7 @@ const errorValue = ref<string | number | null>(null)
const successValue = ref<string | number | null>('be') const successValue = ref<string | number | null>('be')
const disabledValue = ref<string | number | null>('ca') const disabledValue = ref<string | number | null>('ca')
const emptyValue = ref<string | number | null>(null) const emptyValue = ref<string | number | null>(null)
const shortListValue = ref<string | number | null>(null)
const longListValue = ref<string | number | null>(null) const longListValue = ref<string | number | null>(null)
const bottomValue = ref<string | number | null>(null) const bottomValue = ref<string | number | null>(null)
</script> </script>

View File

@@ -163,8 +163,17 @@ Liste déroulante.
| `options` | `{ value: string \| number, text: string }[]` | `[]` | Options disponibles | | `options` | `{ value: string \| number, text: string }[]` | `[]` | Options disponibles |
| `emptyOptionLabel` | `string` | `''` | Placeholder option vide | | `emptyOptionLabel` | `string` | `''` | Placeholder option vide |
| `label` | `string` | `''` | Label | | `label` | `string` | `''` | Label |
| `disabled` | `boolean` | `false` | Désactivé | | `hint` | `string` | `''` | Message d'aide |
| `error` | `string` | `''` | Message d'erreur | | `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)` **Events :** `update:modelValue(value: string | number | null)`
**Slots :** `icon` (icône dropdown custom) **Slots :** `icon` (icône dropdown custom)
@@ -172,6 +181,7 @@ Liste déroulante.
```vue ```vue
<MalioSelect v-model="pays" label="Pays" :options="[{ value: 'FR', text: 'France' }, { value: 'BE', text: 'Belgique' }]" /> <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="ville" label="Ville" :options="villes" empty-option-label="Choisir..." />
<MalioSelect v-model="civilite" label="Civilité" :options="civilites" group-class="mt-0" />
``` ```
--- ---

View File

@@ -1,67 +1,69 @@
<template> <template>
<div <div>
:class="mergedGroupClass" <div
> :class="mergedGroupClass"
<input
:id="inputId"
:name="name"
:autocomplete="autocomplete"
:class="mergedInputClass"
:required="required"
:maxlength="maxLength"
:minlength="minLength"
:disabled="disabled"
:value="currentValue"
:readonly="readonly"
:aria-invalid="!!error"
:aria-describedby="describedBy"
v-bind="attrs"
type="text"
inputmode="decimal"
placeholder="_"
@input="onInput"
@focus="isFocused = true"
@blur="onBlur"
> >
<input
:id="inputId"
:name="name"
:autocomplete="autocomplete"
:class="mergedInputClass"
:required="required"
:maxlength="maxLength"
:minlength="minLength"
:disabled="disabled"
:value="currentValue"
:readonly="readonly"
:aria-invalid="!!error"
:aria-describedby="describedBy"
v-bind="attrs"
type="text"
inputmode="decimal"
placeholder="_"
@input="onInput"
@focus="isFocused = true"
@blur="onBlur"
>
<label <label
v-if="label" v-if="label"
:for="inputId" :for="inputId"
:class="mergedLabelClass" :class="mergedLabelClass"
> >
{{ label }} {{ label }}
</label> </label>
<IconifyIcon <IconifyIcon
v-if="iconName" v-if="iconName"
:icon="iconName" :icon="iconName"
:width="iconSize" :width="iconSize"
:height="iconSize" :height="iconSize"
data-test="icon" data-test="icon"
:class="[ :class="[
hasError
? 'text-m-danger'
: hasSuccess
? 'text-m-success' : iconColor,
iconPositionClass,
]"
/>
</div>
<p
v-if="hint || hasError || hasSuccess"
:id="`${inputId}-describedby`"
:class="[
hasError hasError
? 'text-m-danger' ? 'text-m-danger'
: hasSuccess : hasSuccess
? 'text-m-success' ? 'text-m-success' : iconColor,
: 'text-m-muted', iconPositionClass,
'mt-1 text-xs ml-[2px] ',
]" ]"
> />
{{ hint || error || success }}
</p> </div>
<p
v-if="hint || hasError || hasSuccess"
:id="`${inputId}-describedby`"
:class="[
hasError
? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: 'text-m-muted',
'mt-1 text-xs ml-[2px] ',
]"
>
{{ hint || error || success }}
</p>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -133,7 +135,7 @@ const isFilled = computed(() => currentValue.value.trim().length > 0)
const mergedGroupClass = computed(() => const mergedGroupClass = computed(() =>
twMerge( twMerge(
'relative mt-4 flex h-12 w-full items-center', 'relative flex h-12 w-full items-center',
props.groupClass, props.groupClass,
), ),
) )

View File

@@ -1,68 +1,70 @@
<template> <template>
<div :class="mergedGroupClass" > <div>
<label <div :class="mergedGroupClass" >
v-if="label" <label
:for="inputId" v-if="label"
:class="mergedLabelClass" :for="inputId"
> :class="mergedLabelClass"
{{ label }} >
</label> {{ label }}
<button </label>
type="button" <button
:disabled="isMinusDisabled" type="button"
@click="decrement" :disabled="isMinusDisabled"
> @click="decrement"
<IconifyIcon >
icon="mdi:minus" <IconifyIcon
:class="mergedButtonMinusClass" icon="mdi:minus"
/> :class="mergedButtonMinusClass"
</button> />
<input </button>
:id="inputId" <input
:name="name" :id="inputId"
autocomplete="off" :name="name"
:class="mergedInputClass" autocomplete="off"
:style="inputWidthStyle" :class="mergedInputClass"
:value="displayedValue" :style="inputWidthStyle"
:required="required" :value="displayedValue"
:disabled="disabled" :required="required"
:readonly="readonly" :disabled="disabled"
:aria-invalid="!!error" :readonly="readonly"
:aria-describedby="describedBy" :aria-invalid="!!error"
v-bind="attrs" :aria-describedby="describedBy"
type="text" v-bind="attrs"
inputmode="numeric" type="text"
placeholder="_" inputmode="numeric"
@input="onInput" placeholder="_"
@focus="isFocused = true" @input="onInput"
@blur="isFocused = false" @focus="isFocused = true"
> @blur="isFocused = false"
<button >
type="button" <button
:disabled="isPlusDisabled" type="button"
@click="increment" :disabled="isPlusDisabled"
> @click="increment"
<IconifyIcon >
icon="mdi:plus" <IconifyIcon
:class="mergedButtonPlusClass" icon="mdi:plus"
/> :class="mergedButtonPlusClass"
</button> />
</button>
</div>
<p
v-if="hint || hasError || hasSuccess"
:id="`${inputId}-describedby`"
:class="[
hasError
? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: 'text-m-muted',
'text-xs ml-[2px] ',
]"
>
{{ hint || error || success }}
</p>
</div> </div>
<p
v-if="hint || hasError || hasSuccess"
:id="`${inputId}-describedby`"
:class="[
hasError
? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: 'text-m-muted',
'text-xs ml-[2px] ',
]"
>
{{ hint || error || success }}
</p>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -170,7 +172,7 @@ const isPlusDisabled = computed(() =>
const mergedGroupClass = computed(() => const mergedGroupClass = computed(() =>
twMerge( twMerge(
'relative mt-4 flex h-12 w-full items-center', 'relative flex h-12 w-full items-center',
props.groupClass, props.groupClass,
), ),
) )

View File

@@ -1,67 +1,69 @@
<template> <template>
<div <div>
:class="mergedGroupClass" <div
> :class="mergedGroupClass"
<input
:id="inputId"
:name="name"
:autocomplete="autocomplete"
:class="mergedInputClass"
:required="required"
:maxlength="maxLength"
:minlength="minLength"
:disabled="disabled"
:value="currentValue"
:readonly="readonly"
:aria-invalid="!!error"
:aria-describedby="describedBy"
v-bind="attrs"
placeholder="_"
:type="isPasswordVisible ? 'text' : 'password'"
@input="onInput"
@focus="isFocused = true"
@blur="isFocused = false"
> >
<input
:id="inputId"
:name="name"
:autocomplete="autocomplete"
:class="mergedInputClass"
:required="required"
:maxlength="maxLength"
:minlength="minLength"
:disabled="disabled"
:value="currentValue"
:readonly="readonly"
:aria-invalid="!!error"
:aria-describedby="describedBy"
v-bind="attrs"
placeholder="_"
:type="isPasswordVisible ? 'text' : 'password'"
@input="onInput"
@focus="isFocused = true"
@blur="isFocused = false"
>
<label <label
v-if="label" v-if="label"
:for="inputId" :for="inputId"
:class="mergedLabelClass" :class="mergedLabelClass"
> >
{{ label }} {{ label }}
</label> </label>
<IconifyIcon <IconifyIcon
v-if="displayIcon" v-if="displayIcon"
:icon="isPasswordVisible ? 'mdi:eye-outline' : 'mdi:eye-off-outline'" :icon="isPasswordVisible ? 'mdi:eye-outline' : 'mdi:eye-off-outline'"
:width="24" :width="24"
:height="24" :height="24"
data-test="icon" data-test="icon"
:class="[ :class="[
hasError
? 'text-m-danger'
: hasSuccess
? 'text-m-success' : 'text-m-muted',
'cursor-pointer absolute right-[10px] top-1/2 -translate-y-1/2',
]"
@click="toggleVisibility"
/>
</div>
<p
v-if="hint || hasError || hasSuccess"
:id="`${inputId}-describedby`"
:class="[
hasError hasError
? 'text-m-danger' ? 'text-m-danger'
: hasSuccess : hasSuccess
? 'text-m-success' ? 'text-m-success' : 'text-m-muted',
: 'text-m-muted', 'cursor-pointer absolute right-[10px] top-1/2 -translate-y-1/2',
'mt-1 text-xs ml-[2px] ',
]" ]"
> @click="toggleVisibility"
{{ hint || error || success }} />
</p>
</div>
<p
v-if="hint || hasError || hasSuccess"
:id="`${inputId}-describedby`"
:class="[
hasError
? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: 'text-m-muted',
'mt-1 text-xs ml-[2px] ',
]"
>
{{ hint || error || success }}
</p>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -132,7 +134,7 @@ const hasSuccess = computed(() => !!props.success)
const isFilled = computed(() => currentValue.value.trim().length > 0) const isFilled = computed(() => currentValue.value.trim().length > 0)
const mergedGroupClass = computed(() => const mergedGroupClass = computed(() =>
twMerge( twMerge(
'relative mt-4 flex h-12 w-full items-center', 'relative flex h-12 w-full items-center',
props.groupClass, props.groupClass,
), ),
) )

View File

@@ -1,67 +1,69 @@
<template> <template>
<div <div>
:class="mergedGroupClass" <div
> :class="mergedGroupClass"
<input
:id="inputId"
v-maska="mask"
:name="name"
:autocomplete="autocomplete"
:class="mergedInputClass"
:required="required"
:maxlength="maxLength"
:minlength="minLength"
:disabled="disabled"
:value="currentValue"
:readonly="readonly"
:aria-invalid="!!error"
:aria-describedby="describedBy"
v-bind="attrs"
placeholder="_"
type="text"
@input="onInput"
@focus="isFocused = true"
@blur="isFocused = false"
> >
<input
:id="inputId"
v-maska="mask"
:name="name"
:autocomplete="autocomplete"
:class="mergedInputClass"
:required="required"
:maxlength="maxLength"
:minlength="minLength"
:disabled="disabled"
:value="currentValue"
:readonly="readonly"
:aria-invalid="!!error"
:aria-describedby="describedBy"
v-bind="attrs"
placeholder="_"
type="text"
@input="onInput"
@focus="isFocused = true"
@blur="isFocused = false"
>
<label <label
v-if="label" v-if="label"
:for="inputId" :for="inputId"
:class="mergedLabelClass" :class="mergedLabelClass"
> >
{{ label }} {{ label }}
</label> </label>
<IconifyIcon <IconifyIcon
v-if="iconName" v-if="iconName"
:icon="iconName" :icon="iconName"
:width="iconSize" :width="iconSize"
:height="iconSize" :height="iconSize"
data-test="icon" data-test="icon"
:class="[ :class="[
hasError
? 'text-m-danger'
: hasSuccess
? 'text-m-success' : iconColor,
iconPositionClass,
]"
/>
</div>
<p
v-if="hint || hasError || hasSuccess"
:id="`${inputId}-describedby`"
:class="[
hasError hasError
? 'text-m-danger' ? 'text-m-danger'
: hasSuccess : hasSuccess
? 'text-m-success' ? 'text-m-success' : iconColor,
: 'text-m-muted', iconPositionClass,
'mt-1 text-xs ml-[2px] ',
]" ]"
> />
{{ hint || error || success }}
</p> </div>
<p
v-if="hint || hasError || hasSuccess"
:id="`${inputId}-describedby`"
:class="[
hasError
? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: 'text-m-muted',
'mt-1 text-xs ml-[2px] ',
]"
>
{{ hint || error || success }}
</p>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -138,7 +140,7 @@ const hasSuccess = computed(() => !!props.success)
const isFilled = computed(() => currentValue.value.trim().length > 0) const isFilled = computed(() => currentValue.value.trim().length > 0)
const mergedGroupClass = computed(() => const mergedGroupClass = computed(() =>
twMerge( twMerge(
'relative mt-4 flex h-12 w-full items-center', 'relative flex h-12 w-full items-center',
props.groupClass, props.groupClass,
), ),
) )

View File

@@ -1,70 +1,72 @@
<template> <template>
<div <div>
:class="mergedGroupClass" <div
> :class="mergedGroupClass"
<input
ref="fileInputRef"
type="file"
:accept="accept"
class="hidden"
:disabled="disabled"
@change="onFileChange"
> >
<input
ref="fileInputRef"
type="file"
:accept="accept"
class="hidden"
:disabled="disabled"
@change="onFileChange"
>
<input <input
:id="inputId" :id="inputId"
:class="mergedInputClass" :class="mergedInputClass"
:disabled="disabled" :disabled="disabled"
:value="currentDisplayValue" :value="currentDisplayValue"
:readonly="true" :readonly="true"
:aria-invalid="!!error" :aria-invalid="!!error"
:aria-describedby="describedBy" :aria-describedby="describedBy"
v-bind="attrs" v-bind="attrs"
placeholder="_" placeholder="_"
type="text" type="text"
@click="openFilePicker" @click="openFilePicker"
@focus="isFocused = true" @focus="isFocused = true"
@blur="isFocused = false" @blur="isFocused = false"
> >
<label <label
v-if="label" v-if="label"
:for="inputId" :for="inputId"
:class="mergedLabelClass" :class="mergedLabelClass"
> >
{{ label }} {{ label }}
</label> </label>
<IconifyIcon <IconifyIcon
v-if="displayIcon" v-if="displayIcon"
icon="mdi:cloud-arrow-up-outline" icon="mdi:cloud-arrow-up-outline"
:width="24" :width="24"
:height="24" :height="24"
data-test="icon" data-test="icon"
:class="[ :class="[
hasError
? 'text-m-danger'
: hasSuccess
? 'text-m-success' : 'text-m-muted',
'pointer-events-none absolute right-[10px] top-1/2 -translate-y-1/2',
]"
/>
</div>
<p
v-if="hint || hasError || hasSuccess"
:id="`${inputId}-describedby`"
:class="[
hasError hasError
? 'text-m-danger' ? 'text-m-danger'
: hasSuccess : hasSuccess
? 'text-m-success' ? 'text-m-success' : 'text-m-muted',
: 'text-m-muted', 'pointer-events-none absolute right-[10px] top-1/2 -translate-y-1/2',
'mt-1 text-xs ml-[2px] ',
]" ]"
> />
{{ hint || error || success }}
</p> </div>
<p
v-if="hint || hasError || hasSuccess"
:id="`${inputId}-describedby`"
:class="[
hasError
? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: 'text-m-muted',
'mt-1 text-xs ml-[2px] ',
]"
>
{{ hint || error || success }}
</p>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -121,7 +123,7 @@ const hasSuccess = computed(() => !!props.success)
const isFilled = computed(() => currentDisplayValue.value.trim().length > 0) const isFilled = computed(() => currentDisplayValue.value.trim().length > 0)
const mergedGroupClass = computed(() => const mergedGroupClass = computed(() =>
twMerge( twMerge(
'relative mt-4 flex h-12 w-full items-center', 'relative flex h-12 w-full items-center',
props.groupClass, props.groupClass,
), ),
) )

View File

@@ -1,8 +1,9 @@
<template> <template>
<div <div>
ref="root" <div
:class="mergedGroupClass" ref="root"
> :class="mergedGroupClass"
>
<button <button
:id="buttonId" :id="buttonId"
type="button" type="button"
@@ -133,21 +134,22 @@
{{ opt.label || '\u00A0' }} {{ opt.label || '\u00A0' }}
</li> </li>
</ul> </ul>
</div>
<p
v-if="hint || hasError || hasSuccess"
:id="`${buttonId}-describedby`"
:class="[
hasError
? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: 'text-m-muted',
'mt-1 ml-[2px] text-xs',
]"
>
{{ error || success || hint }}
</p>
</div> </div>
<p
v-if="hint || hasError || hasSuccess"
:id="`${buttonId}-describedby`"
:class="[
hasError
? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: 'text-m-muted',
'mt-1 ml-[2px] text-xs',
]"
>
{{ error || success || hint }}
</p>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -211,7 +213,7 @@ const normalizedOptions = computed<Option[]>(() => [
...props.options, ...props.options,
]) ])
const mergedGroupClass = computed(() => const mergedGroupClass = computed(() =>
twMerge('relative mt-4 w-full', props.minWidth, props.maxWidth, props.groupClass), twMerge('relative w-full', props.minWidth, props.maxWidth, props.groupClass),
) )
const hasError = computed(() => !!props.error) const hasError = computed(() => !!props.error)
const hasSuccess = computed(() => !!props.success && !hasError.value) const hasSuccess = computed(() => !!props.success && !hasError.value)

View File

@@ -1,9 +1,10 @@
<template> <template>
<div <div>
ref="root" <div
class="relative mt-4 w-full" ref="root"
:class="[minWidth, maxWidth]" class="relative w-full"
> :class="[minWidth, maxWidth]"
>
<button <button
:id="buttonId" :id="buttonId"
type="button" type="button"
@@ -184,21 +185,22 @@
/> />
</li> </li>
</ul> </ul>
</div>
<p
v-if="hint || hasError || hasSuccess"
:id="`${buttonId}-describedby`"
:class="[
hasError
? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: 'text-m-muted',
'mt-1 ml-[2px] text-xs',
]"
>
{{ error || success || hint }}
</p>
</div> </div>
<p
v-if="hint || hasError || hasSuccess"
:id="`${buttonId}-describedby`"
:class="[
hasError
? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: 'text-m-muted',
'mt-1 ml-[2px] text-xs',
]"
>
{{ error || success || hint }}
</p>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -6,7 +6,8 @@
"files": [ "files": [
"app/**", "app/**",
"nuxt.config.ts", "nuxt.config.ts",
"README.md" "README.md",
"COMPONENTS.md"
], ],
"scripts": { "scripts": {
"dev": "nuxi dev .playground", "dev": "nuxi dev .playground",