feat: composant saisie assistée, composant téléphone et composant mail (#47)
All checks were successful
Release / release (push) Successful in 1m12s

| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|                  |                 |

## Description de la PR

## Modification du .env

## Check list

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

Co-authored-by: matthieu <matthieu@yuno.malio.fr>
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Reviewed-on: #47
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #47.
This commit is contained in:
2026-05-13 07:01:30 +00:00
committed by Autin
parent d9023a0ddc
commit f3a18ace1d
33 changed files with 4040 additions and 151 deletions

View File

@@ -0,0 +1,180 @@
<template>
<div class="grid grid-cols-1 items-start gap-6 md:grid-cols-2">
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Simple (statique)</h2>
<MalioInputAutocomplete
v-model="simpleValue"
label="Pays"
:options="staticOptions"
/>
<p class="mt-2 text-sm text-m-muted">
Valeur sélectionnée : <code>{{ simpleValue ?? 'null' }}</code>
</p>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Avec icône à gauche</h2>
<MalioInputAutocomplete
v-model="leftIconValue"
label="Recherche"
icon-name="mdi:magnify"
icon-position="left"
:options="staticOptions"
/>
</div>
<div class="rounded-lg border p-4 md:col-span-2">
<h2 class="mb-4 text-xl font-bold">Branché sur une API (simulé)</h2>
<p class="mb-3 text-sm text-m-muted">
Le parent écoute l'event <code>search</code> et alimente <code>options</code> + <code>loading</code>.
Tapez au moins 2 caractères.
</p>
<MalioInputAutocomplete
v-model="apiValue"
label="Client"
:options="apiOptions"
:loading="apiLoading"
:min-search-length="2"
icon-name="mdi:magnify"
icon-position="left"
@search="onSearchApi"
@select="onSelectApi"
/>
<p v-if="apiSelected" class="mt-2 text-sm text-m-muted">
Sélection : <code>{{ apiSelected.label }} (id={{ apiSelected.value }})</code>
</p>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Avec création (allowCreate)</h2>
<MalioInputAutocomplete
v-model="createValue"
label="Catégorie"
:options="staticOptions"
allow-create
hint="Taper Entrée pour créer une nouvelle valeur"
@create="onCreate"
/>
<p v-if="createdItems.length > 0" class="mt-2 text-sm text-m-muted">
Créés : {{ createdItems.join(', ') }}
</p>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Désactivé</h2>
<MalioInputAutocomplete
model-value="fr"
label="Pays"
:options="staticOptions"
disabled
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Readonly</h2>
<MalioInputAutocomplete
model-value="fr"
label="Pays"
:options="staticOptions"
readonly
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Avec hint</h2>
<MalioInputAutocomplete
v-model="hintValue"
label="Pays"
:options="staticOptions"
hint="Sélectionne un pays dans la liste"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Erreur</h2>
<MalioInputAutocomplete
model-value="fr"
label="Pays"
:options="staticOptions"
error="Sélection invalide"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Succès</h2>
<MalioInputAutocomplete
model-value="fr"
label="Pays"
:options="staticOptions"
success="Sélection valide"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Liste vide</h2>
<MalioInputAutocomplete
v-model="emptyValue"
label="Recherche"
:options="[]"
no-results-text="Aucun élément disponible"
/>
</div>
</div>
</template>
<script setup lang="ts">
import {ref} from 'vue'
type Option = {label: string; value: string | number}
const staticOptions: Option[] = [
{label: 'France', value: 'fr'},
{label: 'Belgique', value: 'be'},
{label: 'Canada', value: 'ca'},
{label: 'Suisse', value: 'ch'},
{label: 'Luxembourg', value: 'lu'},
{label: 'Allemagne', value: 'de'},
{label: 'Espagne', value: 'es'},
{label: 'Italie', value: 'it'},
]
const simpleValue = ref<string | number | null>(null)
const leftIconValue = ref<string | number | null>(null)
const createValue = ref<string | number | null>(null)
const hintValue = ref<string | number | null>(null)
const emptyValue = ref<string | number | null>(null)
const createdItems = ref<string[]>([])
const onCreate = (value: string) => {
createdItems.value.push(value)
}
const apiValue = ref<string | number | null>(null)
const apiOptions = ref<Option[]>([])
const apiLoading = ref(false)
const apiSelected = ref<Option | null>(null)
const fakeClients: Option[] = [
{label: 'Yuno Malio', value: 1},
{label: 'Yuna Corp', value: 2},
{label: 'Yum Foods', value: 3},
{label: 'Yumi Studio', value: 4},
{label: 'Acme Inc.', value: 5},
{label: 'Globex Corp', value: 6},
{label: 'Initech', value: 7},
{label: 'Soylent Corp', value: 8},
]
const onSearchApi = async (query: string) => {
apiLoading.value = true
await new Promise(resolve => setTimeout(resolve, 400))
apiOptions.value = fakeClients.filter(c =>
c.label.toLowerCase().includes(query.toLowerCase()),
)
apiLoading.value = false
}
const onSelectApi = (option: Option | null) => {
apiSelected.value = option
}
</script>

View File

@@ -0,0 +1,106 @@
<template>
<div class="grid grid-cols-1 items-start gap-6 md:grid-cols-2">
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Simple</h2>
<MalioInputEmail />
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Avec label</h2>
<MalioInputEmail
v-model="emailValue"
label="Adresse email"
name="email"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Icône à gauche</h2>
<MalioInputEmail
label="Adresse email"
icon-position="left"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Sans icône</h2>
<MalioInputEmail
label="Adresse email"
:icon-name="''"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Désactivé</h2>
<MalioInputEmail
model-value="contact@malio.fr"
disabled
label="Adresse email"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Readonly</h2>
<MalioInputEmail
model-value="readonly@malio.fr"
readonly
label="Adresse email"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Avec hint</h2>
<MalioInputEmail
label="Adresse email"
hint="ex: prenom.nom@malio.fr"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Erreur</h2>
<MalioInputEmail
model-value="pas-un-email"
label="Adresse email"
error="Adresse email invalide"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Succès</h2>
<MalioInputEmail
model-value="contact@malio.fr"
label="Adresse email"
success="Adresse email valide"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Validation dynamique</h2>
<MalioInputEmail
v-model="dynamicEmail"
label="Adresse email"
hint="Saisir une adresse au format prenom@domaine.tld"
:error="dynamicError"
:success="dynamicSuccess"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
const emailValue = ref('')
const dynamicEmail = ref('')
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const isDynamicValid = computed(() => emailRegex.test(dynamicEmail.value))
const dynamicError = computed(() => {
if (!dynamicEmail.value) return ''
return isDynamicValid.value ? '' : 'Adresse email invalide'
})
const dynamicSuccess = computed(() => {
if (!dynamicEmail.value) return ''
return isDynamicValid.value ? 'Adresse email valide' : ''
})
</script>

View File

@@ -0,0 +1,141 @@
<template>
<div class="grid grid-cols-1 items-start gap-6 md:grid-cols-2">
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Simple</h2>
<MalioInputPhone />
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Avec label</h2>
<MalioInputPhone
v-model="phoneValue"
label="Téléphone"
name="phone"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Avec bouton « ajouter »</h2>
<MalioInputPhone
v-model="phoneAddable"
label="Téléphone"
addable
@add="onAdd"
/>
<p v-if="addClicks > 0" class="mt-2 text-sm text-m-muted">
Bouton cliqué {{ addClicks }} fois
</p>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Icône à droite (sans bouton +)</h2>
<MalioInputPhone
label="Téléphone"
icon-position="right"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Sans icône</h2>
<MalioInputPhone
label="Téléphone"
:icon-name="''"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Avec masque français</h2>
<MalioInputPhone
v-model="phoneFrench"
label="Téléphone (FR)"
mask="+33 # ## ## ## ##"
hint="Saisir uniquement les chiffres"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Désactivé (avec addable)</h2>
<MalioInputPhone
model-value="+33 6 12 34 56 78"
addable
disabled
label="Téléphone"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Readonly (avec addable)</h2>
<MalioInputPhone
model-value="+33 6 12 34 56 78"
addable
readonly
label="Téléphone"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Avec hint</h2>
<MalioInputPhone
label="Téléphone"
hint="Format international recommandé"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Erreur</h2>
<MalioInputPhone
model-value="abc"
label="Téléphone"
error="Numéro de téléphone invalide"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Succès</h2>
<MalioInputPhone
model-value="+33 6 12 34 56 78"
label="Téléphone"
success="Numéro valide"
/>
</div>
<div class="rounded-lg border p-4 md:col-span-2">
<h2 class="mb-4 text-xl font-bold">Cas ERP liste de téléphones (max 2)</h2>
<p class="mb-3 text-sm text-m-muted">
Le bouton + s'affiche sur le dernier champ tant que la liste contient moins de {{ MAX_PHONES }} numéros.
</p>
<div class="flex flex-col gap-4">
<MalioInputPhone
v-for="(phone, index) in phones"
:key="index"
v-model="phones[index]"
:label="`Téléphone ${index + 1}`"
:addable="index === phones.length - 1 && phones.length < MAX_PHONES"
@add="addPhone"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const phoneValue = ref('')
const phoneAddable = ref('')
const phoneFrench = ref('')
const addClicks = ref(0)
const onAdd = () => {
addClicks.value++
}
const MAX_PHONES = 2
const phones = ref<string[]>([''])
const addPhone = () => {
if (phones.value.length < MAX_PHONES) {
phones.value.push('')
}
}
</script>