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:
@@ -1 +1 @@
|
||||
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>
|
||||
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-700 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white'])
|
||||
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white dark:bg-gray-800'])
|
||||
|
||||
@php
|
||||
$alignmentClasses = match ($align) {
|
||||
@@ -28,7 +28,7 @@ $width = match ($width) {
|
||||
class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }}"
|
||||
style="display: none;"
|
||||
@click="open = false">
|
||||
<div class="rounded-md ring-1 ring-black ring-opacity-5 {{ $contentClasses }}">
|
||||
<div class="rounded-md ring-1 ring-black ring-opacity-5 dark:ring-gray-700 {{ $contentClasses }}">
|
||||
{{ $content }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@props(['value'])
|
||||
|
||||
<label {{ $attributes->merge(['class' => 'block font-medium text-sm text-gray-700']) }}>
|
||||
<label {{ $attributes->merge(['class' => 'block font-medium text-sm text-gray-700 dark:text-gray-300']) }}>
|
||||
{{ $value ?? $slot }}
|
||||
</label>
|
||||
|
||||
@@ -18,10 +18,8 @@ $maxWidth = [
|
||||
x-data="{
|
||||
show: @js($show),
|
||||
focusables() {
|
||||
// All focusable element types...
|
||||
let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
|
||||
return [...$el.querySelectorAll(selector)]
|
||||
// All non-disabled elements...
|
||||
.filter(el => ! el.hasAttribute('disabled'))
|
||||
},
|
||||
firstFocusable() { return this.focusables()[0] },
|
||||
@@ -60,12 +58,12 @@ $maxWidth = [
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
>
|
||||
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
|
||||
<div class="absolute inset-0 bg-gray-500 dark:bg-gray-900 opacity-75"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
x-show="show"
|
||||
class="mb-6 bg-white rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full {{ $maxWidth }} sm:mx-auto"
|
||||
class="mb-6 bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-xl dark:shadow-gray-900/50 transform transition-all sm:w-full {{ $maxWidth }} sm:mx-auto"
|
||||
x-transition:enter="ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
@php
|
||||
$classes = ($active ?? false)
|
||||
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out'
|
||||
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out';
|
||||
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out'
|
||||
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:border-gray-300 dark:hover:border-gray-600 focus:outline-none focus:text-gray-700 dark:focus:text-gray-200 focus:border-gray-300 dark:focus:border-gray-600 transition duration-150 ease-in-out';
|
||||
@endphp
|
||||
|
||||
<a {{ $attributes->merge(['class' => $classes]) }}>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}>
|
||||
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-gray-300 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150']) }}>
|
||||
{{ $slot }}
|
||||
</button>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
@php
|
||||
$classes = ($active ?? false)
|
||||
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 text-start text-base font-medium text-indigo-700 bg-indigo-50 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 transition duration-150 ease-in-out'
|
||||
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out';
|
||||
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 text-start text-base font-medium text-indigo-700 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/30 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 transition duration-150 ease-in-out'
|
||||
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-600 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out';
|
||||
@endphp
|
||||
|
||||
<a {{ $attributes->merge(['class' => $classes]) }}>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center px-4 py-2 bg-white border border-gray-300 rounded-md font-semibold text-xs text-gray-700 uppercase tracking-widest shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-25 transition ease-in-out duration-150']) }}>
|
||||
<button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center px-4 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150']) }}>
|
||||
{{ $slot }}
|
||||
</button>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
@props(['disabled' => false])
|
||||
|
||||
<input @disabled($disabled) {{ $attributes->merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) }}>
|
||||
<input @disabled($disabled) {{ $attributes->merge(['class' => 'border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-400 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400 rounded-md shadow-sm']) }}>
|
||||
|
||||
@@ -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