Files
mesreleves-php/resources/views/components/lieu-picker.blade.php
T
yann64 3faa74640d Fix dark mode : formulaires et composant lieu-picker
- app.css : règle @layer base globale pour tous les <input>, <select>,
  <textarea> en mode sombre (bg-gray-700, border-gray-600, text-gray-100)
  sans toucher checkboxes, radios ni boutons
- lieu-picker : bouton déclencheur, fenêtre modale, champ de recherche,
  liste de résultats et badges entièrement adaptés au thème sombre

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 20:14:44 +02:00

214 lines
8.7 KiB
PHP

{{--
Composant de sélection d'un lieu par recherche contextuelle.
Paramètres :
$name : nom du champ hidden (ex: "lieu_id")
$label : libellé affiché au-dessus du champ
$value : id du lieu sélectionné (null si aucun)
$displayValue : texte affiché (nom_long du lieu sélectionné)
$required : bool — rend le champ obligatoire
$placeholder : texte quand rien n'est sélectionné
--}}
@props([
'name',
'label' => 'Lieu',
'value' => null,
'displayValue' => '',
'required' => false,
'placeholder' => 'Rechercher un lieu…',
])
<div
x-data="{
open: false,
search: '',
results: [],
loading: false,
selected: {
id: {{ $value ? (int)$value : 'null' }},
name: {{ json_encode($displayValue ?: '') }}
},
debounceTimer: null,
openModal() {
this.open = true;
this.search = '';
this.results = [];
this.$nextTick(() => this.$refs.searchInput?.focus());
},
onSearchInput() {
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => this.fetchResults(), 220);
},
async fetchResults() {
if (this.search.length < 1) { this.results = []; return; }
this.loading = true;
try {
const res = await fetch(
'{{ route('lieux.search') }}?q=' + encodeURIComponent(this.search),
{ headers: { 'X-Requested-With': 'XMLHttpRequest' } }
);
this.results = await res.json();
} finally {
this.loading = false;
}
},
select(lieu) {
this.selected = { id: lieu.id, name: lieu.nom_long };
this.open = false;
this.search = '';
this.results = [];
},
clear() {
this.selected = { id: null, name: '' };
}
}"
@keydown.escape.window="open = false"
>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{{ $label }}
@if($required) <span class="text-red-500">*</span> @endif
</label>
{{-- Champ hidden pour la valeur soumise --}}
<input type="hidden" name="{{ $name }}" :value="selected.id ?? ''">
{{-- Affichage du lieu sélectionné + boutons --}}
<div class="flex gap-2">
<button
type="button"
@click="openModal()"
class="flex-1 text-left px-3 py-2 border border-gray-300 dark:border-gray-600
rounded-md bg-white dark:bg-gray-700 text-sm shadow-sm
hover:border-indigo-400 dark:hover:border-indigo-500
focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-colors
@error($name) border-red-500 @enderror"
>
<span x-show="selected.id"
class="text-gray-900 dark:text-gray-100"
x-text="selected.name"></span>
<span x-show="!selected.id"
class="text-gray-400 dark:text-gray-500">{{ $placeholder }}</span>
</button>
<button
type="button"
x-show="selected.id"
@click="clear()"
title="Effacer"
class="px-2 py-2 text-gray-400 dark:text-gray-500 hover:text-red-500 dark:hover:text-red-400 transition-colors"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
@error($name)
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
{{-- Modale de recherche --}}
<div
x-show="open"
x-cloak
class="fixed inset-0 z-50 flex items-start justify-center pt-24 px-4"
@click.self="open = false"
>
{{-- Fond semi-transparent --}}
<div class="absolute inset-0 bg-black/40 dark:bg-black/60" @click="open = false"></div>
{{-- Panneau --}}
<div class="relative bg-white dark:bg-gray-800 rounded-xl shadow-2xl dark:shadow-gray-900/50
w-full max-w-lg z-10 overflow-hidden">
{{-- En-tête avec champ de recherche --}}
<div class="px-4 py-3 border-b border-gray-100 dark:border-gray-700 flex items-center gap-3">
<svg class="w-5 h-5 text-gray-400 dark:text-gray-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z"/>
</svg>
<input
x-ref="searchInput"
type="text"
x-model="search"
@input="onSearchInput()"
placeholder="Nom, code INSEE…"
class="flex-1 text-sm outline-none bg-transparent
text-gray-900 dark:text-gray-100
placeholder-gray-400 dark:placeholder-gray-500
focus:ring-0 border-none p-0"
>
<button type="button" @click="open = false"
class="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
{{-- Résultats --}}
<div class="max-h-80 overflow-y-auto">
{{-- Chargement --}}
<div x-show="loading"
class="px-4 py-6 text-center text-sm text-gray-400 dark:text-gray-500">
Recherche…
</div>
{{-- Aucun résultat --}}
<div x-show="!loading && search.length > 0 && results.length === 0"
class="px-4 py-6 text-center text-sm text-gray-400 dark:text-gray-500">
Aucun lieu trouvé pour « <span x-text="search"></span> »
</div>
{{-- Invite initiale --}}
<div x-show="!loading && search.length === 0"
class="px-4 py-6 text-center text-sm text-gray-400 dark:text-gray-500">
Saisissez au moins une lettre pour rechercher
</div>
{{-- Liste des résultats --}}
<ul x-show="results.length > 0">
<template x-for="lieu in results" :key="lieu.id">
<li>
<button
type="button"
@click="select(lieu)"
class="w-full text-left px-4 py-3 flex items-center justify-between gap-3 transition-colors
hover:bg-indigo-50 dark:hover:bg-indigo-900/30"
>
<div>
<span class="text-sm font-medium text-gray-900 dark:text-gray-100"
x-text="lieu.nom_long"></span>
<span x-show="lieu.code"
class="ml-2 text-xs text-gray-400 dark:text-gray-500"
x-text="lieu.code"></span>
</div>
<span x-show="lieu.type"
class="shrink-0 text-xs px-2 py-0.5 rounded-full
bg-gray-100 dark:bg-gray-700
text-gray-500 dark:text-gray-400"
x-text="lieu.type"></span>
</button>
</li>
</template>
</ul>
</div>
@can('create', App\Models\Lieu::class)
{{-- Pied : créer un nouveau lieu --}}
<div class="border-t border-gray-100 dark:border-gray-700 px-4 py-2.5">
<a href="{{ route('lieux.create') }}" target="_blank"
class="text-xs text-indigo-600 dark:text-indigo-400 hover:underline">
+ Créer un nouveau lieu
</a>
</div>
@endcan
</div>
</div>
</div>