refactor(ui): unify low-level controls and expand design system
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m57s

This commit is contained in:
2026-03-03 15:50:15 +01:00
parent 9a43980412
commit db7a0cef96
47 changed files with 1404 additions and 711 deletions

View File

@@ -1,46 +1,38 @@
'use client';
import { useState, useRef } from 'react';
import Link from 'next/link';
import { Button } from '@/components/ui';
import { Button, DropdownMenu } from '@/components/ui';
import { WORKSHOPS } from '@/lib/workshops';
import { useClickOutside } from '@/hooks/useClickOutside';
export function NewWorkshopDropdown() {
const [open, setOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
useClickOutside(containerRef, () => setOpen(false), open);
return (
<div ref={containerRef} className="relative">
<Button
type="button"
variant="primary"
size="sm"
onClick={() => setOpen(!open)}
className="gap-1.5"
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Nouvel atelier
<svg
className={`h-3.5 w-3.5 transition-transform ${open ? 'rotate-180' : ''}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</Button>
{open && (
<div className="absolute right-0 z-20 mt-2 w-60 rounded-xl border border-border bg-card py-1.5 shadow-lg">
<DropdownMenu
panelClassName="absolute right-0 z-20 mt-2 w-60 rounded-xl border border-border bg-card py-1.5 shadow-lg"
trigger={({ open, toggle }) => (
<Button type="button" variant="primary" size="sm" onClick={toggle} className="gap-1.5">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Nouvel atelier
<svg
className={`h-3.5 w-3.5 transition-transform ${open ? 'rotate-180' : ''}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</Button>
)}
>
{({ close }) => (
<>
{WORKSHOPS.map((w) => (
<Link
key={w.id}
href={w.newPath}
className="flex items-center gap-3 px-4 py-2.5 text-sm text-foreground hover:bg-card-hover transition-colors"
onClick={() => setOpen(false)}
onClick={close}
>
<span
className="flex h-8 w-8 items-center justify-center rounded-lg text-base flex-shrink-0"
@@ -54,8 +46,8 @@ export function NewWorkshopDropdown() {
</div>
</Link>
))}
</div>
</>
)}
</div>
</DropdownMenu>
);
}

View File

@@ -2,7 +2,16 @@
import { useState, useTransition } from 'react';
import Link from 'next/link';
import { Button, Modal, ModalFooter, Input, CollaboratorDisplay } from '@/components/ui';
import {
Button,
Modal,
ModalFooter,
Input,
CollaboratorDisplay,
IconButton,
IconEdit,
IconTrash,
} from '@/components/ui';
import { deleteSwotSession, updateSwotSession } from '@/actions/session';
import { deleteMotivatorSession, updateMotivatorSession } from '@/actions/moving-motivators';
import { deleteYearReviewSession, updateYearReviewSession } from '@/actions/year-review';
@@ -211,25 +220,21 @@ export function SessionCard({
<>
{(session.isOwner || session.role === 'EDITOR' || session.isTeamCollab) && (
<div className={`absolute flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity z-20 ${view === 'table' ? 'top-1/2 -translate-y-1/2 right-3' : 'top-2.5 right-2.5'}`}>
<button
<IconButton
onClick={(e) => { e.preventDefault(); e.stopPropagation(); openEditModal(); }}
className="p-1.5 rounded-lg bg-card border border-border text-muted hover:text-primary hover:bg-primary/5 shadow-sm"
title="Modifier"
>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
label="Modifier"
icon={<IconEdit />}
variant="primary"
className="bg-card shadow-sm"
/>
{(session.isOwner || session.isTeamCollab) && (
<button
<IconButton
onClick={(e) => { e.preventDefault(); e.stopPropagation(); setShowDeleteModal(true); }}
className="p-1.5 rounded-lg bg-card border border-border text-muted hover:text-destructive hover:bg-destructive/5 shadow-sm"
title="Supprimer"
>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
label="Supprimer"
icon={<IconTrash />}
variant="destructive"
className="bg-card shadow-sm"
/>
)}
</div>
)}
@@ -247,19 +252,15 @@ export function SessionCard({
<Modal isOpen={showEditModal} onClose={() => setShowEditModal(false)} title="Modifier l'atelier" size="sm">
<form onSubmit={(e) => { e.preventDefault(); handleEdit(); }} className="space-y-4">
<Input id="edit-title" label="Titre" value={editTitle} onChange={(e) => setEditTitle(e.target.value)} placeholder="Titre de l'atelier" required />
<div>
<label htmlFor="edit-title" className="block text-sm font-medium text-foreground mb-1">Titre</label>
<Input id="edit-title" value={editTitle} onChange={(e) => setEditTitle(e.target.value)} placeholder="Titre de l'atelier" required />
</div>
<div>
<label htmlFor="edit-participant" className="block text-sm font-medium text-foreground mb-1">{workshop.participantLabel}</label>
{!isWeather && !isGifMood && (
<Input id="edit-participant" value={editParticipant} onChange={(e) => setEditParticipant(e.target.value)}
<Input id="edit-participant" label={workshop.participantLabel} value={editParticipant} onChange={(e) => setEditParticipant(e.target.value)}
placeholder={isSwot ? 'Nom du collaborateur' : 'Nom du participant'} required />
)}
</div>
<ModalFooter>
<Button type="button" variant="ghost" onClick={() => setShowEditModal(false)} disabled={isPending}>Annuler</Button>
<Button type="button" variant="outline" onClick={() => setShowEditModal(false)} disabled={isPending}>Annuler</Button>
<Button type="submit" disabled={isPending || !editTitle.trim() || (!isWeather && !isGifMood && !editParticipant.trim())}>
{isPending ? 'Enregistrement...' : 'Enregistrer'}
</Button>
@@ -272,7 +273,7 @@ export function SessionCard({
<p className="text-muted">Êtes-vous sûr de vouloir supprimer <strong className="text-foreground">&quot;{session.title}&quot;</strong> ?</p>
<p className="text-sm text-destructive">Cette action est irréversible. Toutes les données seront perdues.</p>
<ModalFooter>
<Button variant="ghost" onClick={() => setShowDeleteModal(false)} disabled={isPending}>Annuler</Button>
<Button variant="outline" onClick={() => setShowDeleteModal(false)} disabled={isPending}>Annuler</Button>
<Button variant="destructive" onClick={handleDelete} disabled={isPending}>{isPending ? 'Suppression...' : 'Supprimer'}</Button>
</ModalFooter>
</div>