Mode sombre, option désactivation mises à jour, user-picker avec recherche
- Dark mode complet : darkMode:'class' Tailwind, sélecteur clair/sombre/auto dans la navigation (mémorisé dans localStorage, sans flash au chargement) ; 53 vues et 8 composants Breeze mis à jour avec classes dark: - Composant user-picker : fenêtre modale avec recherche temps réel (nom/email) remplace les <select> d'ajout de membres dans sections et sources - Paramètres : option "Désactiver la vérification automatique des mises à jour" (case à cochage auto-soumise, route POST parametres/updates) - Panneau "Paramètres généraux" remonté en tête de la page de paramètres - README recentré sur l'installation manuelle hébergement PHP+MySQL - VERSION 1.0.1 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
{{--
|
||||
Sélecteur d'utilisateur avec recherche (fenêtre modale).
|
||||
Props :
|
||||
name — nom du champ hidden (défaut : user_id)
|
||||
users — collection/array d'objets avec {id, name, email}
|
||||
placeholder — texte affiché avant sélection
|
||||
required — ajoute l'attribut required sur le champ hidden (pour validation formulaire)
|
||||
--}}
|
||||
@props([
|
||||
'name' => 'user_id',
|
||||
'users' => [],
|
||||
'placeholder' => 'Sélectionner un utilisateur…',
|
||||
'required' => false,
|
||||
])
|
||||
|
||||
@php
|
||||
$usersJson = collect($users)->map(fn ($u) => [
|
||||
'id' => $u->id,
|
||||
'name' => $u->name,
|
||||
'email' => $u->email,
|
||||
])->values()->toJson();
|
||||
@endphp
|
||||
|
||||
<div x-data="{
|
||||
open: false,
|
||||
search: '',
|
||||
selectedId: null,
|
||||
selectedLabel: '',
|
||||
users: {{ $usersJson }},
|
||||
get filtered() {
|
||||
if (! this.search.trim()) return this.users;
|
||||
const q = this.search.toLowerCase();
|
||||
return this.users.filter(u =>
|
||||
u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q)
|
||||
);
|
||||
},
|
||||
select(u) {
|
||||
this.selectedId = u.id;
|
||||
this.selectedLabel = u.name + ' (' + u.email + ')';
|
||||
this.open = false;
|
||||
this.search = '';
|
||||
},
|
||||
clear() {
|
||||
this.selectedId = null;
|
||||
this.selectedLabel = '';
|
||||
}
|
||||
}" @keydown.escape.window="open = false">
|
||||
|
||||
<input type="hidden" name="{{ $name }}" :value="selectedId" {{ $required ? 'required' : '' }}>
|
||||
|
||||
{{-- Bouton déclencheur --}}
|
||||
<button type="button" @click="open = true"
|
||||
class="w-full flex items-center justify-between gap-2 px-3 py-2
|
||||
bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600
|
||||
rounded-md shadow-sm text-sm text-left
|
||||
hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors
|
||||
focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1 dark:focus:ring-offset-gray-800">
|
||||
<span :class="selectedId ? 'text-gray-900 dark:text-gray-100' : 'text-gray-400 dark:text-gray-500'"
|
||||
x-text="selectedLabel || '{{ $placeholder }}'"></span>
|
||||
<svg class="w-4 h-4 text-gray-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{{-- Fenêtre modale --}}
|
||||
<div x-show="open" x-cloak
|
||||
class="fixed inset-0 z-50 flex items-start justify-center pt-16 sm:pt-24 px-4"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0">
|
||||
|
||||
{{-- Fond semi-transparent --}}
|
||||
<div class="absolute inset-0 bg-gray-900/50 dark:bg-gray-950/70" @click="open = false"></div>
|
||||
|
||||
{{-- Panneau --}}
|
||||
<div class="relative w-full max-w-md bg-white dark:bg-gray-800 rounded-xl shadow-xl dark:shadow-gray-900/50 overflow-hidden"
|
||||
@click.stop
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2"
|
||||
x-transition:enter-end="opacity-100 translate-y-0">
|
||||
|
||||
{{-- Champ de recherche --}}
|
||||
<div class="flex items-center gap-2 px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<svg class="w-4 h-4 text-gray-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
<input type="text" x-model="search" x-ref="searchInput"
|
||||
x-init="$watch('open', v => v && $nextTick(() => $refs.searchInput && $refs.searchInput.focus()))"
|
||||
placeholder="Rechercher par nom ou email…"
|
||||
class="flex-1 bg-transparent border-none outline-none p-0 text-sm
|
||||
text-gray-900 dark:text-gray-100
|
||||
placeholder-gray-400 dark:placeholder-gray-500
|
||||
focus:ring-0">
|
||||
<button type="button" @click="open = false"
|
||||
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 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>
|
||||
|
||||
{{-- Liste des utilisateurs --}}
|
||||
<div class="overflow-y-auto" style="max-height: 18rem;">
|
||||
<template x-if="filtered.length === 0">
|
||||
<p class="px-4 py-8 text-center text-sm text-gray-400 dark:text-gray-500">
|
||||
Aucun résultat pour « <span x-text="search"></span> »
|
||||
</p>
|
||||
</template>
|
||||
<template x-for="u in filtered" :key="u.id">
|
||||
<button type="button" @click="select(u)"
|
||||
class="w-full flex items-center gap-3 px-4 py-2.5 text-left transition-colors
|
||||
hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
:class="selectedId === u.id
|
||||
? 'bg-indigo-50 dark:bg-indigo-900/30'
|
||||
: ''">
|
||||
{{-- Avatar initiale --}}
|
||||
<div class="w-8 h-8 rounded-full bg-indigo-100 dark:bg-indigo-900/50
|
||||
flex items-center justify-center shrink-0">
|
||||
<span class="text-xs font-semibold text-indigo-700 dark:text-indigo-300"
|
||||
x-text="u.name.charAt(0).toUpperCase()"></span>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate"
|
||||
x-text="u.name"></p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 truncate"
|
||||
x-text="u.email"></p>
|
||||
</div>
|
||||
<template x-if="selectedId === u.id">
|
||||
<svg class="w-4 h-4 text-indigo-600 dark:text-indigo-400 shrink-0"
|
||||
fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{{-- Pied : compteur --}}
|
||||
<div class="px-4 py-2 border-t border-gray-100 dark:border-gray-700
|
||||
text-xs text-gray-400 dark:text-gray-500 flex items-center justify-between">
|
||||
<span>
|
||||
<span x-text="filtered.length"></span> /
|
||||
<span x-text="users.length"></span> utilisateur<span x-show="users.length !== 1">s</span>
|
||||
</span>
|
||||
<button x-show="selectedId" type="button" @click="clear()"
|
||||
class="text-red-500 hover:text-red-700 dark:hover:text-red-400 transition-colors">
|
||||
Effacer la sélection
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user