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

@@ -4,7 +4,7 @@ import { useState } from 'react';
import { signIn } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { RocketIcon } from '@/components/ui';
import { Button, Input, RocketIcon } from '@/components/ui';
export default function LoginPage() {
const router = useRouter();
@@ -62,42 +62,32 @@ export default function LoginPage() {
)}
<div className="mb-4">
<label htmlFor="email" className="mb-2 block text-sm font-medium text-foreground">
Email
</label>
<input
<Input
id="email"
name="email"
type="email"
label="Email"
required
autoComplete="email"
className="w-full rounded-lg border border-input-border bg-input px-4 py-2.5 text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
placeholder="vous@exemple.com"
/>
</div>
<div className="mb-6">
<label htmlFor="password" className="mb-2 block text-sm font-medium text-foreground">
Mot de passe
</label>
<input
<Input
id="password"
name="password"
type="password"
label="Mot de passe"
required
autoComplete="current-password"
className="w-full rounded-lg border border-input-border bg-input px-4 py-2.5 text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
placeholder="••••••••"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full rounded-lg bg-primary px-4 py-2.5 font-semibold text-primary-foreground transition-colors hover:bg-primary-hover disabled:cursor-not-allowed disabled:opacity-50"
>
<Button type="submit" disabled={loading} loading={loading} className="w-full">
{loading ? 'Connexion...' : 'Se connecter'}
</button>
</Button>
<p className="mt-6 text-center text-sm text-muted">
Pas encore de compte ?{' '}

View File

@@ -4,7 +4,7 @@ import { useState } from 'react';
import { signIn } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { RocketIcon } from '@/components/ui';
import { Button, Input, RocketIcon } from '@/components/ui';
export default function RegisterPage() {
const router = useRouter();
@@ -91,74 +91,55 @@ export default function RegisterPage() {
)}
<div className="mb-4">
<label htmlFor="name" className="mb-2 block text-sm font-medium text-foreground">
Nom
</label>
<input
<Input
id="name"
name="name"
type="text"
label="Nom"
autoComplete="name"
className="w-full rounded-lg border border-input-border bg-input px-4 py-2.5 text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
placeholder="Jean Dupont"
/>
</div>
<div className="mb-4">
<label htmlFor="email" className="mb-2 block text-sm font-medium text-foreground">
Email
</label>
<input
<Input
id="email"
name="email"
type="email"
label="Email"
required
autoComplete="email"
className="w-full rounded-lg border border-input-border bg-input px-4 py-2.5 text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
placeholder="vous@exemple.com"
/>
</div>
<div className="mb-4">
<label htmlFor="password" className="mb-2 block text-sm font-medium text-foreground">
Mot de passe
</label>
<input
<Input
id="password"
name="password"
type="password"
label="Mot de passe"
required
autoComplete="new-password"
className="w-full rounded-lg border border-input-border bg-input px-4 py-2.5 text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
placeholder="••••••••"
/>
</div>
<div className="mb-6">
<label
htmlFor="confirmPassword"
className="mb-2 block text-sm font-medium text-foreground"
>
Confirmer le mot de passe
</label>
<input
<Input
id="confirmPassword"
name="confirmPassword"
type="password"
label="Confirmer le mot de passe"
required
autoComplete="new-password"
className="w-full rounded-lg border border-input-border bg-input px-4 py-2.5 text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
placeholder="••••••••"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full rounded-lg bg-primary px-4 py-2.5 font-semibold text-primary-foreground transition-colors hover:bg-primary-hover disabled:cursor-not-allowed disabled:opacity-50"
>
<Button type="submit" disabled={loading} loading={loading} className="w-full">
{loading ? 'Création...' : 'Créer mon compte'}
</button>
</Button>
<p className="mt-6 text-center text-sm text-muted">
Déjà un compte ?{' '}

View File

@@ -0,0 +1,525 @@
'use client';
import { useState } from 'react';
import {
Avatar,
Badge,
Button,
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
CollaboratorDisplay,
DateInput,
Disclosure,
DropdownMenu,
EditableGifMoodTitle,
EditableMotivatorTitle,
EditableSessionTitle,
EditableTitle,
EditableWeatherTitle,
EditableWeeklyCheckInTitle,
EditableYearReviewTitle,
InlineFormActions,
Input,
IconCheck,
IconClose,
IconDuplicate,
IconEdit,
IconButton,
IconPlus,
IconTrash,
Modal,
ModalFooter,
PageHeader,
ParticipantInput,
RocketIcon,
Select,
SegmentedControl,
SessionPageHeader,
Textarea,
ToggleGroup,
FormField,
NumberInput,
} from '@/components/ui';
const BUTTON_VARIANTS = [
'primary',
'secondary',
'outline',
'ghost',
'destructive',
'brand',
] as const;
const BUTTON_SIZES = ['sm', 'md', 'lg'] as const;
const BADGE_VARIANTS = [
'default',
'primary',
'strength',
'weakness',
'opportunity',
'threat',
'success',
'warning',
'destructive',
'accent',
] as const;
const SELECT_OPTIONS = [
{ value: 'editor', label: 'Editeur' },
{ value: 'viewer', label: 'Lecteur' },
{ value: 'admin', label: 'Admin' },
];
const SECTION_LINKS = [
{ id: 'buttons', label: 'Buttons' },
{ id: 'badges', label: 'Badges' },
{ id: 'icon-button', label: 'IconButton' },
{ id: 'form-inputs', label: 'Form Inputs' },
{ id: 'select-toggle', label: 'Select & Toggle' },
{ id: 'form-field', label: 'FormField / Date / Number' },
{ id: 'cards', label: 'Cards' },
{ id: 'avatars', label: 'Avatar & Collaborators' },
{ id: 'disclosure-dropdown', label: 'Disclosure & Dropdown' },
{ id: 'menu', label: 'Menu' },
{ id: 'editable-titles', label: 'Editable Titles' },
{ id: 'session-header', label: 'Session Header' },
{ id: 'participant-input', label: 'ParticipantInput' },
{ id: 'icons', label: 'Icons' },
{ id: 'modal', label: 'Modal' },
] as const;
export default function DesignSystemPage() {
const [modalOpen, setModalOpen] = useState(false);
const [toggleValue, setToggleValue] = useState<'cards' | 'table' | 'list'>('cards');
const [selectMd, setSelectMd] = useState('editor');
const [selectSm, setSelectSm] = useState('viewer');
const [selectXs, setSelectXs] = useState('admin');
const [selectLg, setSelectLg] = useState('editor');
const [menuCount, setMenuCount] = useState(0);
return (
<main className="mx-auto max-w-7xl px-4 py-8">
<PageHeader
emoji="🎨"
title="Design System"
subtitle="Guide visuel des composants UI et de leurs variantes"
actions={
<Button variant="brand" size="sm">
Action principale
</Button>
}
/>
<div className="grid items-start gap-8" style={{ gridTemplateColumns: '240px minmax(0, 1fr)' }}>
<aside>
<Card className="sticky top-20 p-4">
<p className="mb-3 text-sm font-medium text-foreground">Menu de la page</p>
<nav className="flex flex-col gap-1.5">
{SECTION_LINKS.map((section) => (
<a
key={section.id}
href={`#${section.id}`}
className="rounded-md px-2.5 py-1.5 text-sm text-muted transition-colors hover:bg-card-hover hover:text-foreground"
>
{section.label}
</a>
))}
</nav>
</Card>
</aside>
<div className="space-y-8">
<Card id="buttons" className="p-6">
<h2 className="mb-4 text-xl font-semibold text-foreground">Buttons</h2>
<div className="space-y-4">
{BUTTON_SIZES.map((size) => (
<div key={size} className="flex flex-wrap items-center gap-3">
<span className="w-12 text-xs uppercase tracking-wide text-muted">{size}</span>
{BUTTON_VARIANTS.map((variant) => (
<Button key={`${size}-${variant}`} variant={variant} size={size}>
{variant}
</Button>
))}
</div>
))}
<div className="flex flex-wrap items-center gap-3 border-t border-border pt-4">
<Button loading>Chargement</Button>
<Button disabled>Desactive</Button>
</div>
</div>
</Card>
<Card id="badges" className="p-6">
<h2 className="mb-4 text-xl font-semibold text-foreground">Badges</h2>
<div className="flex flex-wrap gap-2">
{BADGE_VARIANTS.map((variant) => (
<Badge key={variant} variant={variant}>
{variant}
</Badge>
))}
</div>
</Card>
<Card id="icon-button" className="p-6">
<h2 className="mb-4 text-xl font-semibold text-foreground">IconButton</h2>
<div className="flex flex-wrap items-center gap-2">
<IconButton icon={<IconEdit />} label="Edit" />
<IconButton icon={<IconDuplicate />} label="Duplicate" variant="primary" />
<IconButton icon={<IconTrash />} label="Delete" variant="destructive" />
</div>
</Card>
<Card id="form-inputs" className="p-6">
<h2 className="mb-4 text-xl font-semibold text-foreground">Form Inputs</h2>
<div className="grid gap-4 md:grid-cols-2">
<Input label="Input standard" placeholder="Votre texte" />
<Input label="Input avec erreur" defaultValue="Valeur invalide" error="Champ invalide" />
<Textarea label="Textarea standard" placeholder="Votre description" rows={3} />
<Textarea
label="Textarea avec erreur"
defaultValue="Texte"
rows={3}
error="Description trop courte"
/>
</div>
</Card>
<Card id="select-toggle" className="p-6">
<h2 className="mb-4 text-xl font-semibold text-foreground">Select & ToggleGroup</h2>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-3">
<Select
label="Select XS"
size="xs"
value={selectXs}
onChange={(e) => setSelectXs(e.target.value)}
options={SELECT_OPTIONS}
/>
<Select
label="Select SM"
size="sm"
value={selectSm}
onChange={(e) => setSelectSm(e.target.value)}
options={SELECT_OPTIONS}
/>
<Select
label="Select MD"
size="md"
value={selectMd}
onChange={(e) => setSelectMd(e.target.value)}
options={SELECT_OPTIONS}
/>
<Select
label="Select LG"
size="lg"
value={selectLg}
onChange={(e) => setSelectLg(e.target.value)}
options={SELECT_OPTIONS}
/>
</div>
<div className="space-y-3">
<p className="text-sm font-medium text-foreground">Toggle group</p>
<ToggleGroup
value={toggleValue}
onChange={setToggleValue}
options={[
{ value: 'cards', label: 'Cards' },
{ value: 'table', label: 'Table' },
{ value: 'list', label: 'List' },
]}
/>
<p className="text-sm text-muted">Valeur active: {toggleValue}</p>
<p className="pt-2 text-sm font-medium text-foreground">Segmented control</p>
<SegmentedControl
value={toggleValue}
onChange={setToggleValue}
options={[
{ value: 'cards', label: 'Cards' },
{ value: 'table', label: 'Table' },
{ value: 'list', label: 'List' },
]}
/>
</div>
</div>
</Card>
<Card id="form-field" className="p-6">
<h2 className="mb-4 text-xl font-semibold text-foreground">FormField / Date / Number</h2>
<div className="grid gap-4 md:grid-cols-2">
<FormField label="FormField">
<Input placeholder="Control custom" />
</FormField>
<DateInput label="DateInput" defaultValue="2026-03-03" />
<NumberInput label="NumberInput" defaultValue={42} />
</div>
</Card>
<Card id="cards" className="p-6">
<h2 className="mb-4 text-xl font-semibold text-foreground">Cards & Header blocks</h2>
<div className="grid gap-4 md:grid-cols-2">
<Card hover>
<CardHeader>
<CardTitle>Card title</CardTitle>
<CardDescription>Description secondaire</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted">Contenu principal de la card.</p>
</CardContent>
<CardFooter className="justify-end">
<Button size="sm" variant="outline">
Annuler
</Button>
<Button size="sm">Valider</Button>
</CardFooter>
</Card>
<Card className="p-4">
<h3 className="mb-3 font-medium text-foreground">Inline actions</h3>
<Input placeholder="Exemple inline" className="mb-2" />
<InlineFormActions
onCancel={() => {}}
onSubmit={() => {}}
isPending={false}
submitLabel="Ajouter"
/>
</Card>
</div>
</Card>
<Card id="avatars" className="p-6">
<h2 className="mb-4 text-xl font-semibold text-foreground">Avatar & Collaborators</h2>
<div className="grid gap-4 md:grid-cols-2">
<div className="flex items-center gap-3">
<Avatar email="jane.doe@example.com" name="Jane Doe" size={40} />
<Avatar email="john.smith@example.com" name="John Smith" size={32} />
<Avatar email="team@example.com" size={24} />
</div>
<div className="space-y-3">
<CollaboratorDisplay
collaborator={{
raw: 'Jane Doe',
matchedUser: {
id: '1',
email: 'jane.doe@example.com',
name: 'Jane Doe',
},
}}
showEmail
/>
<CollaboratorDisplay
collaborator={{
raw: 'Intervenant externe',
matchedUser: null,
}}
/>
</div>
</div>
</Card>
<Card id="disclosure-dropdown" className="p-6">
<h2 className="mb-4 text-xl font-semibold text-foreground">Disclosure & Dropdown</h2>
<div className="space-y-4">
<Disclosure title="Panneau pliable" subtitle="Composant Disclosure">
<p className="text-sm text-muted">Contenu du panneau.</p>
</Disclosure>
<DropdownMenu
panelClassName="mt-2 w-56 rounded-lg border border-border bg-card p-2 shadow-lg"
trigger={({ open, toggle }) => (
<Button type="button" variant="outline" onClick={toggle}>
Menu demo {open ? '▲' : '▼'}
</Button>
)}
>
{({ close }) => (
<Button
size="sm"
variant="secondary"
onClick={() => {
setMenuCount((prev) => prev + 1);
close();
}}
>
Incrementer ({menuCount})
</Button>
)}
</DropdownMenu>
</div>
</Card>
<Card id="menu" className="p-6">
<h2 className="mb-4 text-xl font-semibold text-foreground">Menu</h2>
<DropdownMenu
panelClassName="mt-2 w-64 overflow-hidden rounded-lg border border-border bg-card py-1 shadow-lg"
trigger={({ open, toggle }) => (
<Button type="button" variant="outline" onClick={toggle}>
Ouvrir le menu {open ? '▲' : '▼'}
</Button>
)}
>
{({ close }) => (
<>
<div className="border-b border-border px-4 py-2">
<p className="text-xs text-muted">MENU DE DEMO</p>
<p className="text-sm font-medium text-foreground">Navigation rapide</p>
</div>
<button
type="button"
onClick={close}
className="block w-full px-4 py-2 text-left text-sm text-foreground hover:bg-card-hover"
>
👤 Mon profil
</button>
<button
type="button"
onClick={close}
className="block w-full px-4 py-2 text-left text-sm text-foreground hover:bg-card-hover"
>
👥 Equipes
</button>
<button
type="button"
onClick={close}
className="block w-full px-4 py-2 text-left text-sm text-destructive hover:bg-card-hover"
>
Se deconnecter
</button>
</>
)}
</DropdownMenu>
</Card>
<Card id="editable-titles" className="p-6">
<h2 className="mb-4 text-xl font-semibold text-foreground">Editable Titles</h2>
<div className="space-y-6">
<div>
<p className="mb-2 text-sm font-medium text-foreground">EditableTitle (base)</p>
<EditableTitle
sessionId="demo-editable-title"
initialTitle="Titre modifiable (cliquez pour tester)"
canEdit
onUpdate={async () => ({ success: true })}
/>
</div>
<div className="grid gap-4 md:grid-cols-2">
<EditableSessionTitle
sessionId="demo-session-title"
initialTitle="Session title wrapper"
canEdit={false}
/>
<EditableMotivatorTitle
sessionId="demo-motivator-title"
initialTitle="Motivator title wrapper"
canEdit={false}
/>
<EditableYearReviewTitle
sessionId="demo-year-review-title"
initialTitle="Year review title wrapper"
canEdit={false}
/>
<EditableWeatherTitle
sessionId="demo-weather-title"
initialTitle="Weather title wrapper"
canEdit={false}
/>
<EditableWeeklyCheckInTitle
sessionId="demo-weekly-checkin-title"
initialTitle="Weekly check-in title wrapper"
canEdit={false}
/>
<EditableGifMoodTitle
sessionId="demo-gif-mood-title"
initialTitle="Gif mood title wrapper"
canEdit={false}
/>
</div>
</div>
</Card>
<Card id="session-header" className="p-6">
<h2 className="mb-4 text-xl font-semibold text-foreground">Session Header</h2>
<SessionPageHeader
workshopType="swot"
sessionId="demo-session"
sessionTitle="Atelier de demonstration"
isOwner={true}
canEdit={false}
ownerUser={{ name: 'Jane Doe', email: 'jane.doe@example.com' }}
date={new Date()}
collaborator={{
raw: 'Jane Doe',
matchedUser: {
id: '1',
email: 'jane.doe@example.com',
name: 'Jane Doe',
},
}}
badges={<Badge variant="primary">DEMO</Badge>}
/>
</Card>
<Card id="participant-input" className="p-6">
<h2 className="mb-4 text-xl font-semibold text-foreground">ParticipantInput</h2>
<ParticipantInput name="participant" />
</Card>
<Card id="icons" className="p-6">
<h2 className="mb-4 text-xl font-semibold text-foreground">Icons</h2>
<div className="flex flex-wrap items-center gap-4 text-foreground">
<div className="flex items-center gap-2">
<IconEdit />
<span className="text-sm text-muted">Edit</span>
</div>
<div className="flex items-center gap-2">
<IconTrash />
<span className="text-sm text-muted">Trash</span>
</div>
<div className="flex items-center gap-2">
<IconDuplicate />
<span className="text-sm text-muted">Duplicate</span>
</div>
<div className="flex items-center gap-2">
<IconPlus />
<span className="text-sm text-muted">Plus</span>
</div>
<div className="flex items-center gap-2">
<IconCheck />
<span className="text-sm text-muted">Check</span>
</div>
<div className="flex items-center gap-2">
<IconClose />
<span className="text-sm text-muted">Close</span>
</div>
<div className="flex items-center gap-2">
<RocketIcon className="h-5 w-5" />
<span className="text-sm text-muted">Rocket</span>
</div>
</div>
</Card>
<Card id="modal" className="p-6">
<h2 className="mb-4 text-xl font-semibold text-foreground">Modal</h2>
<Button onClick={() => setModalOpen(true)}>Ouvrir la popup</Button>
</Card>
</div>
</div>
<Modal isOpen={modalOpen} onClose={() => setModalOpen(false)} title="Exemple de popup" size="md">
<p className="text-sm text-muted">
Ceci est un exemple de modal avec ses actions standardisees.
</p>
<ModalFooter>
<Button variant="outline" onClick={() => setModalOpen(false)}>
Annuler
</Button>
<Button onClick={() => setModalOpen(false)}>Confirmer</Button>
</ModalFooter>
</Modal>
</main>
);
}

View File

@@ -9,6 +9,7 @@ import {
CardDescription,
CardContent,
Button,
DateInput,
Input,
} from '@/components/ui';
import { createGifMoodSession } from '@/actions/gif-mood';
@@ -78,20 +79,14 @@ export default function NewGifMoodPage() {
required
/>
<div>
<label htmlFor="date" className="block text-sm font-medium text-foreground mb-1">
Date
</label>
<input
id="date"
name="date"
type="date"
value={selectedDate}
onChange={(e) => setSelectedDate(e.target.value)}
required
className="w-full rounded-lg border border-border bg-input px-3 py-2 text-foreground outline-none focus:border-primary focus:ring-2 focus:ring-primary/20"
/>
</div>
<DateInput
id="date"
name="date"
label="Date"
value={selectedDate}
onChange={(e) => setSelectedDate(e.target.value)}
required
/>
<div className="rounded-lg border border-border bg-card-hover p-4">
<h3 className="font-medium text-foreground mb-2">Comment ça marche ?</h3>

View File

@@ -2,7 +2,7 @@ import { auth } from '@/lib/auth';
import { redirect } from 'next/navigation';
import Link from 'next/link';
import { getUserOKRs } from '@/services/okrs';
import { Card, PageHeader } from '@/components/ui';
import { Card, PageHeader, getButtonClassName } from '@/components/ui';
import { ObjectivesList } from '@/components/okrs/ObjectivesList';
import { comparePeriods } from '@/lib/okr-utils';
@@ -46,10 +46,11 @@ export default async function ObjectivesPage() {
Vous n&apos;avez pas encore d&apos;OKR défini. Contactez un administrateur d&apos;équipe
pour en créer.
</p>
<Link href="/teams">
<span className="inline-block rounded-lg bg-[var(--purple)] px-4 py-2 text-white hover:opacity-90">
Voir mes équipes
</span>
<Link
href="/teams"
className={getButtonClassName({ variant: 'brand' })}
>
Voir mes équipes
</Link>
</Card>
) : (

View File

@@ -1,4 +1,5 @@
import Link from 'next/link';
import { getButtonClassName } from '@/components/ui';
import { WORKSHOPS, getSessionsTabUrl } from '@/lib/workshops';
export default function Home() {
@@ -888,14 +889,16 @@ function WorkshopCard({
<div className="flex gap-3">
<Link
href={newHref}
className="flex-1 rounded-lg px-4 py-2.5 text-center font-medium text-white transition-colors"
className={getButtonClassName({
className: 'flex-1 border-transparent text-white',
})}
style={{ backgroundColor: accentColor }}
>
Démarrer
</Link>
<Link
href={href}
className="rounded-lg border border-border px-4 py-2.5 font-medium text-foreground transition-colors hover:bg-card-hover"
className={getButtonClassName({ variant: 'outline' })}
>
Mes sessions
</Link>

View File

@@ -39,29 +39,20 @@ export function PasswordForm() {
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label
htmlFor="currentPassword"
className="mb-1.5 block text-sm font-medium text-foreground"
>
Mot de passe actuel
</label>
<Input
id="currentPassword"
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
required
/>
</div>
<Input
id="currentPassword"
type="password"
label="Mot de passe actuel"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
required
/>
<div>
<label htmlFor="newPassword" className="mb-1.5 block text-sm font-medium text-foreground">
Nouveau mot de passe
</label>
<Input
id="newPassword"
type="password"
label="Nouveau mot de passe"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
@@ -71,15 +62,10 @@ export function PasswordForm() {
</div>
<div>
<label
htmlFor="confirmPassword"
className="mb-1.5 block text-sm font-medium text-foreground"
>
Confirmer le nouveau mot de passe
</label>
<Input
id="confirmPassword"
type="password"
label="Confirmer le nouveau mot de passe"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required

View File

@@ -39,31 +39,23 @@ export function ProfileForm({ initialData }: ProfileFormProps) {
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="name" className="mb-1.5 block text-sm font-medium text-foreground">
Nom
</label>
<Input
id="name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Votre nom"
/>
</div>
<Input
id="name"
type="text"
label="Nom"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Votre nom"
/>
<div>
<label htmlFor="email" className="mb-1.5 block text-sm font-medium text-foreground">
Email
</label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<Input
id="email"
type="email"
label="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
{message && (
<p

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>

View File

@@ -52,7 +52,7 @@ export default async function TeamDetailPage({ params }: TeamDetailPageProps) {
isAdmin ? (
<div className="flex items-center gap-3">
<Link href={`/teams/${id}/okrs/new`}>
<Button className="bg-[var(--purple)] text-white hover:opacity-90 border-transparent">
<Button variant="brand" size="sm">
Définir un OKR
</Button>
</Link>

View File

@@ -79,7 +79,7 @@ export default function NewTeamPage() {
<Button
type="submit"
disabled={submitting}
className="bg-[var(--purple)] text-white hover:opacity-90 border-transparent"
variant="brand"
>
{submitting ? 'Création...' : "Créer l'équipe"}
</Button>

View File

@@ -22,7 +22,7 @@ export default async function TeamsPage() {
subtitle={`${teams.length} équipe${teams.length !== 1 ? 's' : ''} · Collaborez et définissez vos OKRs`}
actions={
<Link href="/teams/new">
<Button className="bg-[var(--purple)] text-white hover:opacity-90 border-transparent">
<Button variant="brand" size="sm">
Créer une équipe
</Button>
</Link>
@@ -44,7 +44,7 @@ export default async function TeamsPage() {
Créez votre première équipe pour commencer à définir des OKRs
</div>
<Link href="/teams/new" className="mt-6">
<Button className="!bg-[var(--purple)] !text-white hover:!bg-[var(--purple)]/90">
<Button variant="brand">
Créer une équipe
</Button>
</Link>

View File

@@ -9,6 +9,7 @@ import {
CardDescription,
CardContent,
Button,
DateInput,
Input,
} from '@/components/ui';
import { createWeatherSession } from '@/actions/weather';
@@ -93,20 +94,14 @@ export default function NewWeatherPage() {
required
/>
<div>
<label htmlFor="date" className="block text-sm font-medium text-foreground mb-1">
Date de la météo
</label>
<input
id="date"
name="date"
type="date"
value={selectedDate}
onChange={handleDateChange}
required
className="w-full rounded-lg border border-border bg-input px-3 py-2 text-foreground outline-none focus:border-primary focus:ring-2 focus:ring-primary/20"
/>
</div>
<DateInput
id="date"
name="date"
label="Date de la météo"
value={selectedDate}
onChange={handleDateChange}
required
/>
<div className="rounded-lg border border-border bg-card-hover p-4">
<h3 className="font-medium text-foreground mb-2">Comment ça marche ?</h3>

View File

@@ -9,6 +9,7 @@ import {
CardDescription,
CardContent,
Button,
DateInput,
Input,
ParticipantInput,
} from '@/components/ui';
@@ -98,20 +99,14 @@ export default function NewWeeklyCheckInPage() {
<ParticipantInput name="participant" required />
<div>
<label htmlFor="date" className="block text-sm font-medium text-foreground mb-1">
Date du check-in
</label>
<input
id="date"
name="date"
type="date"
value={selectedDate}
onChange={handleDateChange}
required
className="w-full rounded-lg border border-border bg-input px-3 py-2 text-foreground outline-none focus:border-primary focus:ring-2 focus:ring-primary/20"
/>
</div>
<DateInput
id="date"
name="date"
label="Date du check-in"
value={selectedDate}
onChange={handleDateChange}
required
/>
<div className="rounded-lg border border-border bg-card-hover p-4">
<h3 className="font-medium text-foreground mb-2">Comment ça marche ?</h3>

View File

@@ -9,6 +9,7 @@ import {
CardDescription,
CardContent,
Button,
NumberInput,
Input,
ParticipantInput,
} from '@/components/ui';
@@ -79,21 +80,15 @@ export default function NewYearReviewPage() {
<ParticipantInput name="participant" required />
<div>
<label htmlFor="year" className="block text-sm font-medium text-foreground mb-1">
Année du bilan
</label>
<input
id="year"
name="year"
type="number"
min="2000"
max="2100"
defaultValue={currentYear}
required
className="w-full rounded-lg border border-border bg-input px-3 py-2 text-foreground outline-none focus:border-primary focus:ring-2 focus:ring-primary/20"
/>
</div>
<NumberInput
id="year"
name="year"
label="Année du bilan"
min="2000"
max="2100"
defaultValue={currentYear}
required
/>
<div className="rounded-lg border border-border bg-card-hover p-4">
<h3 className="font-medium text-foreground mb-2">Comment ça marche ?</h3>