Les établissements ont besoin de définir leur référentiel de matières pour pouvoir ensuite les associer aux enseignants et aux classes. Cette fonctionnalité permet aux administrateurs de créer, modifier et archiver les matières avec leurs propriétés (nom, code court, couleur). L'architecture suit le pattern DDD avec des Value Objects utilisant les property hooks PHP 8.5 pour garantir l'immutabilité et la validation. L'isolation multi-tenant est assurée par vérification dans les handlers.
843 lines
18 KiB
Svelte
843 lines
18 KiB
Svelte
<script lang="ts">
|
||
import { goto } from '$app/navigation';
|
||
import { getApiBaseUrl } from '$lib/api/config';
|
||
import { authenticatedFetch } from '$lib/auth';
|
||
|
||
// Types
|
||
interface Subject {
|
||
id: string;
|
||
name: string;
|
||
code: string;
|
||
color: string | null;
|
||
description: string | null;
|
||
status: string;
|
||
teacherCount: number | null;
|
||
classCount: number | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
// Couleurs prédéfinies (suggestions UI)
|
||
const SUGGESTED_COLORS = [
|
||
{ label: 'Bleu (Mathématiques)', value: '#3B82F6' },
|
||
{ label: 'Rouge (Français)', value: '#EF4444' },
|
||
{ label: 'Orange (Histoire-Géo)', value: '#F59E0B' },
|
||
{ label: 'Vert (Sciences)', value: '#10B981' },
|
||
{ label: 'Indigo (Anglais)', value: '#6366F1' },
|
||
{ label: 'Rose (EPS)', value: '#EC4899' },
|
||
{ label: 'Violet (Arts)', value: '#8B5CF6' },
|
||
{ label: 'Gris (Autre)', value: '#6B7280' }
|
||
];
|
||
|
||
// State
|
||
let subjects = $state<Subject[]>([]);
|
||
let isLoading = $state(true);
|
||
let error = $state<string | null>(null);
|
||
let showCreateModal = $state(false);
|
||
let showDeleteModal = $state(false);
|
||
let subjectToDelete = $state<Subject | null>(null);
|
||
|
||
// Form state
|
||
let newSubjectName = $state('');
|
||
let newSubjectCode = $state('');
|
||
let newSubjectColor = $state<string | null>(null);
|
||
let isSubmitting = $state(false);
|
||
let isDeleting = $state(false);
|
||
|
||
// Load subjects on mount
|
||
$effect(() => {
|
||
loadSubjects();
|
||
});
|
||
|
||
async function loadSubjects() {
|
||
try {
|
||
isLoading = true;
|
||
error = null;
|
||
const apiUrl = getApiBaseUrl();
|
||
const response = await authenticatedFetch(`${apiUrl}/subjects`);
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Erreur lors du chargement des matières');
|
||
}
|
||
|
||
const data = await response.json();
|
||
// API Platform peut retourner hydra:member, member, ou un tableau direct
|
||
subjects = data['hydra:member'] ?? data['member'] ?? (Array.isArray(data) ? data : []);
|
||
} catch (e) {
|
||
error = e instanceof Error ? e.message : 'Erreur inconnue';
|
||
subjects = [];
|
||
} finally {
|
||
isLoading = false;
|
||
}
|
||
}
|
||
|
||
async function handleCreateSubject() {
|
||
if (!newSubjectName.trim() || !newSubjectCode.trim()) return;
|
||
|
||
try {
|
||
isSubmitting = true;
|
||
const apiUrl = getApiBaseUrl();
|
||
const response = await authenticatedFetch(`${apiUrl}/subjects`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
name: newSubjectName.trim(),
|
||
code: newSubjectCode.trim().toUpperCase(),
|
||
color: newSubjectColor
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
let errorMessage = `Erreur lors de la création (${response.status})`;
|
||
try {
|
||
const errorData = await response.json();
|
||
if (errorData['hydra:description']) {
|
||
errorMessage = errorData['hydra:description'];
|
||
} else if (errorData.message) {
|
||
errorMessage = errorData.message;
|
||
} else if (errorData.detail) {
|
||
errorMessage = errorData.detail;
|
||
}
|
||
} catch {
|
||
// JSON parsing failed, keep default message
|
||
}
|
||
throw new Error(errorMessage);
|
||
}
|
||
|
||
// Reload subjects and close modal
|
||
await loadSubjects();
|
||
closeModal();
|
||
} catch (e) {
|
||
error = e instanceof Error ? e.message : 'Erreur lors de la création';
|
||
} finally {
|
||
isSubmitting = false;
|
||
}
|
||
}
|
||
|
||
function openDeleteModal(subject: Subject) {
|
||
subjectToDelete = subject;
|
||
showDeleteModal = true;
|
||
}
|
||
|
||
function closeDeleteModal() {
|
||
showDeleteModal = false;
|
||
subjectToDelete = null;
|
||
}
|
||
|
||
async function handleConfirmDelete() {
|
||
if (!subjectToDelete) return;
|
||
|
||
try {
|
||
isDeleting = true;
|
||
const apiUrl = getApiBaseUrl();
|
||
const response = await authenticatedFetch(`${apiUrl}/subjects/${subjectToDelete.id}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
if (!response.ok) {
|
||
let errorMessage = `Erreur lors de la suppression (${response.status})`;
|
||
try {
|
||
const errorData = await response.json();
|
||
if (errorData['hydra:description']) {
|
||
errorMessage = errorData['hydra:description'];
|
||
} else if (errorData.message) {
|
||
errorMessage = errorData.message;
|
||
} else if (errorData.detail) {
|
||
errorMessage = errorData.detail;
|
||
}
|
||
} catch {
|
||
// JSON parsing failed, keep default message
|
||
}
|
||
throw new Error(errorMessage);
|
||
}
|
||
|
||
closeDeleteModal();
|
||
await loadSubjects();
|
||
} catch (e) {
|
||
error = e instanceof Error ? e.message : 'Erreur lors de la suppression';
|
||
} finally {
|
||
isDeleting = false;
|
||
}
|
||
}
|
||
|
||
function openCreateModal() {
|
||
showCreateModal = true;
|
||
newSubjectName = '';
|
||
newSubjectCode = '';
|
||
newSubjectColor = null;
|
||
}
|
||
|
||
function closeModal() {
|
||
showCreateModal = false;
|
||
}
|
||
|
||
function navigateToEdit(subjectId: string) {
|
||
goto(`/admin/subjects/${subjectId}`);
|
||
}
|
||
|
||
function getColorStyle(color: string | null): string {
|
||
if (!color) return '';
|
||
return `background-color: ${color}; color: ${getContrastColor(color)}`;
|
||
}
|
||
|
||
function getContrastColor(hexColor: string): string {
|
||
// Simple luminance check for contrast
|
||
const r = parseInt(hexColor.slice(1, 3), 16);
|
||
const g = parseInt(hexColor.slice(3, 5), 16);
|
||
const b = parseInt(hexColor.slice(5, 7), 16);
|
||
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||
return luminance > 0.5 ? '#000000' : '#FFFFFF';
|
||
}
|
||
</script>
|
||
|
||
<svelte:head>
|
||
<title>Gestion des matières - Classeo</title>
|
||
</svelte:head>
|
||
|
||
<div class="subjects-page">
|
||
<header class="page-header">
|
||
<div class="header-content">
|
||
<h1>Gestion des matières</h1>
|
||
<p class="subtitle">Créez et gérez les matières enseignées dans votre établissement</p>
|
||
</div>
|
||
<button class="btn-primary" onclick={openCreateModal}>
|
||
<span class="btn-icon">+</span>
|
||
Nouvelle matière
|
||
</button>
|
||
</header>
|
||
|
||
{#if error}
|
||
<div class="alert alert-error">
|
||
<span class="alert-icon">⚠️</span>
|
||
{error}
|
||
<button class="alert-close" onclick={() => (error = null)}>×</button>
|
||
</div>
|
||
{/if}
|
||
|
||
{#if isLoading}
|
||
<div class="loading-state">
|
||
<div class="spinner"></div>
|
||
<p>Chargement des matières...</p>
|
||
</div>
|
||
{:else if subjects.length === 0}
|
||
<div class="empty-state">
|
||
<span class="empty-icon">📚</span>
|
||
<h2>Aucune matière</h2>
|
||
<p>Commencez par créer votre première matière</p>
|
||
<button class="btn-primary" onclick={openCreateModal}>Créer une matière</button>
|
||
</div>
|
||
{:else}
|
||
<div class="subjects-grid">
|
||
{#each subjects as subject (subject.id)}
|
||
<div class="subject-card">
|
||
<div class="subject-header">
|
||
{#if subject.color}
|
||
<span class="subject-color-badge" style={getColorStyle(subject.color)}>
|
||
{subject.code}
|
||
</span>
|
||
{:else}
|
||
<span class="subject-code">{subject.code}</span>
|
||
{/if}
|
||
<h3 class="subject-name">{subject.name}</h3>
|
||
</div>
|
||
|
||
<div class="subject-stats">
|
||
<span class="stat-item" title="Enseignants affectés">
|
||
<span class="stat-icon">👨🏫</span>
|
||
{subject.teacherCount ?? 0}
|
||
</span>
|
||
<span class="stat-item" title="Classes associées">
|
||
<span class="stat-icon">🏫</span>
|
||
{subject.classCount ?? 0}
|
||
</span>
|
||
<span class="stat-item status-{subject.status}">
|
||
{subject.status === 'active' ? 'Active' : 'Archivée'}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="subject-actions">
|
||
<button class="btn-secondary btn-sm" onclick={() => navigateToEdit(subject.id)}>
|
||
Modifier
|
||
</button>
|
||
<button class="btn-danger btn-sm" onclick={() => openDeleteModal(subject)}>
|
||
Supprimer
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
|
||
<!-- Create Modal -->
|
||
{#if showCreateModal}
|
||
<div class="modal-overlay" onclick={closeModal} role="presentation">
|
||
<div
|
||
class="modal"
|
||
onclick={(e) => e.stopPropagation()}
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="modal-title"
|
||
>
|
||
<header class="modal-header">
|
||
<h2 id="modal-title">Nouvelle matière</h2>
|
||
<button class="modal-close" onclick={closeModal} aria-label="Fermer">×</button>
|
||
</header>
|
||
|
||
<form
|
||
class="modal-body"
|
||
onsubmit={(e) => {
|
||
e.preventDefault();
|
||
handleCreateSubject();
|
||
}}
|
||
>
|
||
<div class="form-group">
|
||
<label for="subject-name">Nom de la matière *</label>
|
||
<input
|
||
type="text"
|
||
id="subject-name"
|
||
bind:value={newSubjectName}
|
||
placeholder="ex: Mathématiques"
|
||
required
|
||
minlength="2"
|
||
maxlength="100"
|
||
/>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="subject-code">Code court *</label>
|
||
<input
|
||
type="text"
|
||
id="subject-code"
|
||
bind:value={newSubjectCode}
|
||
placeholder="ex: MATH"
|
||
required
|
||
minlength="2"
|
||
maxlength="10"
|
||
/>
|
||
<small class="form-hint">2 à 10 caractères (lettres et chiffres uniquement)</small>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="subject-color">Couleur</label>
|
||
<div class="color-picker-group">
|
||
<div class="color-swatches">
|
||
{#each SUGGESTED_COLORS as colorOption}
|
||
<button
|
||
type="button"
|
||
class="color-swatch"
|
||
class:selected={newSubjectColor === colorOption.value}
|
||
style="background-color: {colorOption.value}"
|
||
onclick={() => (newSubjectColor = colorOption.value)}
|
||
title={colorOption.label}
|
||
></button>
|
||
{/each}
|
||
<button
|
||
type="button"
|
||
class="color-swatch color-none"
|
||
class:selected={newSubjectColor === null}
|
||
onclick={() => (newSubjectColor = null)}
|
||
title="Aucune couleur"
|
||
>
|
||
∅
|
||
</button>
|
||
</div>
|
||
<input
|
||
type="color"
|
||
id="subject-color"
|
||
value={newSubjectColor ?? '#6B7280'}
|
||
onchange={(e) => (newSubjectColor = e.currentTarget.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn-secondary" onclick={closeModal} disabled={isSubmitting}>
|
||
Annuler
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
class="btn-primary"
|
||
disabled={isSubmitting || !newSubjectName.trim() || !newSubjectCode.trim()}
|
||
>
|
||
{#if isSubmitting}
|
||
Création...
|
||
{:else}
|
||
Créer la matière
|
||
{/if}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
<!-- Delete Confirmation Modal -->
|
||
{#if showDeleteModal && subjectToDelete}
|
||
<div class="modal-overlay" onclick={closeDeleteModal} role="presentation">
|
||
<div
|
||
class="modal modal-confirm"
|
||
onclick={(e) => e.stopPropagation()}
|
||
role="alertdialog"
|
||
aria-modal="true"
|
||
aria-labelledby="delete-modal-title"
|
||
aria-describedby="delete-modal-description"
|
||
>
|
||
<header class="modal-header modal-header-danger">
|
||
<h2 id="delete-modal-title">Supprimer la matière</h2>
|
||
<button class="modal-close" onclick={closeDeleteModal} aria-label="Fermer">×</button>
|
||
</header>
|
||
|
||
<div class="modal-body">
|
||
<p id="delete-modal-description">
|
||
Êtes-vous sûr de vouloir supprimer la matière <strong>{subjectToDelete.name}</strong> ({subjectToDelete.code})
|
||
?
|
||
</p>
|
||
<p class="delete-warning">Cette action est irréversible.</p>
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<button
|
||
type="button"
|
||
class="btn-secondary"
|
||
onclick={closeDeleteModal}
|
||
disabled={isDeleting}
|
||
>
|
||
Annuler
|
||
</button>
|
||
<button type="button" class="btn-danger" onclick={handleConfirmDelete} disabled={isDeleting}>
|
||
{#if isDeleting}
|
||
Suppression...
|
||
{:else}
|
||
Supprimer
|
||
{/if}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
<style>
|
||
.subjects-page {
|
||
padding: 1.5rem;
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.page-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
gap: 1rem;
|
||
margin-bottom: 1.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.header-content h1 {
|
||
margin: 0;
|
||
font-size: 1.5rem;
|
||
font-weight: 700;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.subtitle {
|
||
margin: 0.25rem 0 0;
|
||
color: #6b7280;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.btn-primary {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.75rem 1.25rem;
|
||
background: #3b82f6;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 0.5rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.btn-primary:hover:not(:disabled) {
|
||
background: #2563eb;
|
||
}
|
||
|
||
.btn-primary:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.btn-secondary {
|
||
padding: 0.5rem 1rem;
|
||
background: white;
|
||
color: #374151;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 0.375rem;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-secondary:hover:not(:disabled) {
|
||
background: #f3f4f6;
|
||
}
|
||
|
||
.btn-danger {
|
||
padding: 0.5rem 1rem;
|
||
background: #fef2f2;
|
||
color: #dc2626;
|
||
border: 1px solid #fecaca;
|
||
border-radius: 0.375rem;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
background: #fee2e2;
|
||
}
|
||
|
||
.btn-sm {
|
||
padding: 0.375rem 0.75rem;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.btn-icon {
|
||
font-size: 1.25rem;
|
||
line-height: 1;
|
||
}
|
||
|
||
.alert {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
padding: 1rem;
|
||
border-radius: 0.5rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.alert-error {
|
||
background: #fef2f2;
|
||
border: 1px solid #fecaca;
|
||
color: #dc2626;
|
||
}
|
||
|
||
.alert-icon {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.alert-close {
|
||
margin-left: auto;
|
||
padding: 0.25rem 0.5rem;
|
||
background: none;
|
||
border: none;
|
||
font-size: 1.25rem;
|
||
cursor: pointer;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.alert-close:hover {
|
||
opacity: 1;
|
||
}
|
||
|
||
.loading-state,
|
||
.empty-state {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 3rem;
|
||
text-align: center;
|
||
background: white;
|
||
border-radius: 0.75rem;
|
||
border: 2px dashed #e5e7eb;
|
||
}
|
||
|
||
.spinner {
|
||
width: 2rem;
|
||
height: 2rem;
|
||
border: 3px solid #e5e7eb;
|
||
border-top-color: #3b82f6;
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 3rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.empty-state h2 {
|
||
margin: 0 0 0.5rem;
|
||
font-size: 1.25rem;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.empty-state p {
|
||
margin: 0 0 1.5rem;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.subjects-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||
gap: 1rem;
|
||
}
|
||
|
||
.subject-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
padding: 1.25rem;
|
||
background: white;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 0.75rem;
|
||
transition: box-shadow 0.2s;
|
||
}
|
||
|
||
.subject-card:hover {
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.subject-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.subject-name {
|
||
margin: 0;
|
||
font-size: 1.125rem;
|
||
font-weight: 600;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.subject-code {
|
||
padding: 0.25rem 0.5rem;
|
||
background: #f3f4f6;
|
||
color: #374151;
|
||
border-radius: 0.25rem;
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
font-family: monospace;
|
||
}
|
||
|
||
.subject-color-badge {
|
||
padding: 0.25rem 0.5rem;
|
||
border-radius: 0.25rem;
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
font-family: monospace;
|
||
}
|
||
|
||
.subject-stats {
|
||
display: flex;
|
||
gap: 1rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.stat-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.25rem;
|
||
font-size: 0.875rem;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.stat-icon {
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.status-active {
|
||
color: #059669;
|
||
}
|
||
|
||
.status-archived {
|
||
color: #6b7280;
|
||
}
|
||
|
||
.subject-actions {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
margin-top: auto;
|
||
}
|
||
|
||
/* Modal */
|
||
.modal-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 1rem;
|
||
z-index: 100;
|
||
}
|
||
|
||
.modal {
|
||
background: white;
|
||
border-radius: 0.75rem;
|
||
width: 100%;
|
||
max-width: 28rem;
|
||
max-height: 90vh;
|
||
overflow: auto;
|
||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 1.25rem 1.5rem;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.modal-header h2 {
|
||
margin: 0;
|
||
font-size: 1.125rem;
|
||
font-weight: 600;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.modal-close {
|
||
padding: 0.25rem 0.5rem;
|
||
background: none;
|
||
border: none;
|
||
font-size: 1.5rem;
|
||
line-height: 1;
|
||
color: #6b7280;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.modal-close:hover {
|
||
color: #1f2937;
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 1.5rem;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 0.5rem;
|
||
font-weight: 500;
|
||
color: #374151;
|
||
}
|
||
|
||
.form-group input[type='text'],
|
||
.form-group select {
|
||
width: 100%;
|
||
padding: 0.75rem 1rem;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 0.375rem;
|
||
font-size: 1rem;
|
||
transition: border-color 0.2s;
|
||
}
|
||
|
||
.form-group input:focus {
|
||
outline: none;
|
||
border-color: #3b82f6;
|
||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||
}
|
||
|
||
.form-hint {
|
||
display: block;
|
||
margin-top: 0.25rem;
|
||
font-size: 0.75rem;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.color-picker-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.color-swatches {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.color-swatch {
|
||
width: 2rem;
|
||
height: 2rem;
|
||
border: 2px solid transparent;
|
||
border-radius: 0.375rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.color-swatch:hover {
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.color-swatch.selected {
|
||
border-color: #1f2937;
|
||
box-shadow: 0 0 0 2px white, 0 0 0 4px #3b82f6;
|
||
}
|
||
|
||
.color-none {
|
||
background: #f3f4f6;
|
||
color: #6b7280;
|
||
font-size: 0.875rem;
|
||
font-weight: bold;
|
||
}
|
||
|
||
input[type='color'] {
|
||
width: 3rem;
|
||
height: 2rem;
|
||
padding: 0;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 0.375rem;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.modal-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 0.75rem;
|
||
padding-top: 1rem;
|
||
border-top: 1px solid #e5e7eb;
|
||
}
|
||
|
||
/* Delete confirmation modal */
|
||
.modal-confirm {
|
||
max-width: 24rem;
|
||
}
|
||
|
||
.modal-confirm .modal-actions {
|
||
padding: 1rem 1.5rem;
|
||
border-top: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.modal-header-danger {
|
||
background: #fef2f2;
|
||
border-bottom-color: #fecaca;
|
||
}
|
||
|
||
.modal-header-danger h2 {
|
||
color: #dc2626;
|
||
}
|
||
|
||
.delete-warning {
|
||
margin: 0.75rem 0 0;
|
||
font-size: 0.875rem;
|
||
color: #6b7280;
|
||
}
|
||
</style>
|