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 { signIn } from 'next-auth/react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { RocketIcon } from '@/components/ui'; import { Button, Input, RocketIcon } from '@/components/ui';
export default function LoginPage() { export default function LoginPage() {
const router = useRouter(); const router = useRouter();
@@ -62,42 +62,32 @@ export default function LoginPage() {
)} )}
<div className="mb-4"> <div className="mb-4">
<label htmlFor="email" className="mb-2 block text-sm font-medium text-foreground"> <Input
Email
</label>
<input
id="email" id="email"
name="email" name="email"
type="email" type="email"
label="Email"
required required
autoComplete="email" 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" placeholder="vous@exemple.com"
/> />
</div> </div>
<div className="mb-6"> <div className="mb-6">
<label htmlFor="password" className="mb-2 block text-sm font-medium text-foreground"> <Input
Mot de passe
</label>
<input
id="password" id="password"
name="password" name="password"
type="password" type="password"
label="Mot de passe"
required required
autoComplete="current-password" 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="••••••••" placeholder="••••••••"
/> />
</div> </div>
<button <Button type="submit" disabled={loading} loading={loading} className="w-full">
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"
>
{loading ? 'Connexion...' : 'Se connecter'} {loading ? 'Connexion...' : 'Se connecter'}
</button> </Button>
<p className="mt-6 text-center text-sm text-muted"> <p className="mt-6 text-center text-sm text-muted">
Pas encore de compte ?{' '} Pas encore de compte ?{' '}

View File

@@ -4,7 +4,7 @@ import { useState } from 'react';
import { signIn } from 'next-auth/react'; import { signIn } from 'next-auth/react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { RocketIcon } from '@/components/ui'; import { Button, Input, RocketIcon } from '@/components/ui';
export default function RegisterPage() { export default function RegisterPage() {
const router = useRouter(); const router = useRouter();
@@ -91,74 +91,55 @@ export default function RegisterPage() {
)} )}
<div className="mb-4"> <div className="mb-4">
<label htmlFor="name" className="mb-2 block text-sm font-medium text-foreground"> <Input
Nom
</label>
<input
id="name" id="name"
name="name" name="name"
type="text" type="text"
label="Nom"
autoComplete="name" 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" placeholder="Jean Dupont"
/> />
</div> </div>
<div className="mb-4"> <div className="mb-4">
<label htmlFor="email" className="mb-2 block text-sm font-medium text-foreground"> <Input
Email
</label>
<input
id="email" id="email"
name="email" name="email"
type="email" type="email"
label="Email"
required required
autoComplete="email" 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" placeholder="vous@exemple.com"
/> />
</div> </div>
<div className="mb-4"> <div className="mb-4">
<label htmlFor="password" className="mb-2 block text-sm font-medium text-foreground"> <Input
Mot de passe
</label>
<input
id="password" id="password"
name="password" name="password"
type="password" type="password"
label="Mot de passe"
required required
autoComplete="new-password" 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="••••••••" placeholder="••••••••"
/> />
</div> </div>
<div className="mb-6"> <div className="mb-6">
<label <Input
htmlFor="confirmPassword"
className="mb-2 block text-sm font-medium text-foreground"
>
Confirmer le mot de passe
</label>
<input
id="confirmPassword" id="confirmPassword"
name="confirmPassword" name="confirmPassword"
type="password" type="password"
label="Confirmer le mot de passe"
required required
autoComplete="new-password" 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="••••••••" placeholder="••••••••"
/> />
</div> </div>
<button <Button type="submit" disabled={loading} loading={loading} className="w-full">
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"
>
{loading ? 'Création...' : 'Créer mon compte'} {loading ? 'Création...' : 'Créer mon compte'}
</button> </Button>
<p className="mt-6 text-center text-sm text-muted"> <p className="mt-6 text-center text-sm text-muted">
Déjà un compte ?{' '} 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, CardDescription,
CardContent, CardContent,
Button, Button,
DateInput,
Input, Input,
} from '@/components/ui'; } from '@/components/ui';
import { createGifMoodSession } from '@/actions/gif-mood'; import { createGifMoodSession } from '@/actions/gif-mood';
@@ -78,20 +79,14 @@ export default function NewGifMoodPage() {
required required
/> />
<div> <DateInput
<label htmlFor="date" className="block text-sm font-medium text-foreground mb-1"> id="date"
Date name="date"
</label> label="Date"
<input value={selectedDate}
id="date" onChange={(e) => setSelectedDate(e.target.value)}
name="date" required
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>
<div className="rounded-lg border border-border bg-card-hover p-4"> <div className="rounded-lg border border-border bg-card-hover p-4">
<h3 className="font-medium text-foreground mb-2">Comment ça marche ?</h3> <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 { redirect } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { getUserOKRs } from '@/services/okrs'; 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 { ObjectivesList } from '@/components/okrs/ObjectivesList';
import { comparePeriods } from '@/lib/okr-utils'; 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 Vous n&apos;avez pas encore d&apos;OKR défini. Contactez un administrateur d&apos;équipe
pour en créer. pour en créer.
</p> </p>
<Link href="/teams"> <Link
<span className="inline-block rounded-lg bg-[var(--purple)] px-4 py-2 text-white hover:opacity-90"> href="/teams"
Voir mes équipes className={getButtonClassName({ variant: 'brand' })}
</span> >
Voir mes équipes
</Link> </Link>
</Card> </Card>
) : ( ) : (

View File

@@ -1,4 +1,5 @@
import Link from 'next/link'; import Link from 'next/link';
import { getButtonClassName } from '@/components/ui';
import { WORKSHOPS, getSessionsTabUrl } from '@/lib/workshops'; import { WORKSHOPS, getSessionsTabUrl } from '@/lib/workshops';
export default function Home() { export default function Home() {
@@ -888,14 +889,16 @@ function WorkshopCard({
<div className="flex gap-3"> <div className="flex gap-3">
<Link <Link
href={newHref} 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 }} style={{ backgroundColor: accentColor }}
> >
Démarrer Démarrer
</Link> </Link>
<Link <Link
href={href} 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 Mes sessions
</Link> </Link>

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,16 @@
import { useState, useTransition } from 'react'; import { useState, useTransition } from 'react';
import Link from 'next/link'; 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 { deleteSwotSession, updateSwotSession } from '@/actions/session';
import { deleteMotivatorSession, updateMotivatorSession } from '@/actions/moving-motivators'; import { deleteMotivatorSession, updateMotivatorSession } from '@/actions/moving-motivators';
import { deleteYearReviewSession, updateYearReviewSession } from '@/actions/year-review'; import { deleteYearReviewSession, updateYearReviewSession } from '@/actions/year-review';
@@ -211,25 +220,21 @@ export function SessionCard({
<> <>
{(session.isOwner || session.role === 'EDITOR' || session.isTeamCollab) && ( {(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'}`}> <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(); }} 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" label="Modifier"
title="Modifier" icon={<IconEdit />}
> variant="primary"
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> className="bg-card shadow-sm"
<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>
{(session.isOwner || session.isTeamCollab) && ( {(session.isOwner || session.isTeamCollab) && (
<button <IconButton
onClick={(e) => { e.preventDefault(); e.stopPropagation(); setShowDeleteModal(true); }} 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" label="Supprimer"
title="Supprimer" icon={<IconTrash />}
> variant="destructive"
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> className="bg-card shadow-sm"
<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>
)} )}
</div> </div>
)} )}
@@ -247,19 +252,15 @@ export function SessionCard({
<Modal isOpen={showEditModal} onClose={() => setShowEditModal(false)} title="Modifier l'atelier" size="sm"> <Modal isOpen={showEditModal} onClose={() => setShowEditModal(false)} title="Modifier l'atelier" size="sm">
<form onSubmit={(e) => { e.preventDefault(); handleEdit(); }} className="space-y-4"> <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> <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 && ( {!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 /> placeholder={isSwot ? 'Nom du collaborateur' : 'Nom du participant'} required />
)} )}
</div> </div>
<ModalFooter> <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())}> <Button type="submit" disabled={isPending || !editTitle.trim() || (!isWeather && !isGifMood && !editParticipant.trim())}>
{isPending ? 'Enregistrement...' : 'Enregistrer'} {isPending ? 'Enregistrement...' : 'Enregistrer'}
</Button> </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-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> <p className="text-sm text-destructive">Cette action est irréversible. Toutes les données seront perdues.</p>
<ModalFooter> <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> <Button variant="destructive" onClick={handleDelete} disabled={isPending}>{isPending ? 'Suppression...' : 'Supprimer'}</Button>
</ModalFooter> </ModalFooter>
</div> </div>

View File

@@ -52,7 +52,7 @@ export default async function TeamDetailPage({ params }: TeamDetailPageProps) {
isAdmin ? ( isAdmin ? (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href={`/teams/${id}/okrs/new`}> <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 Définir un OKR
</Button> </Button>
</Link> </Link>

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,12 +2,17 @@
import { useState, useTransition } from 'react'; import { useState, useTransition } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { Modal } from '@/components/ui/Modal'; import {
import { Input } from '@/components/ui/Input'; Modal,
import { Button } from '@/components/ui/Button'; Input,
import { Badge } from '@/components/ui/Badge'; Button,
import { Avatar } from '@/components/ui/Avatar'; Badge,
import { Select } from '@/components/ui/Select'; Avatar,
Select,
SegmentedControl,
IconButton,
IconTrash,
} from '@/components/ui';
import { getTeamMembersForShare, type TeamWithMembers, type Share } from '@/lib/share-utils'; import { getTeamMembersForShare, type TeamWithMembers, type Share } from '@/lib/share-utils';
import type { ShareRole } from '@prisma/client'; import type { ShareRole } from '@prisma/client';
@@ -117,24 +122,20 @@ export function ShareModal({
{isOwner && ( {isOwner && (
<form onSubmit={handleShare} className="space-y-4"> <form onSubmit={handleShare} className="space-y-4">
<div className="flex gap-2 border-b border-border pb-3 flex-wrap"> <div className="border-b border-border pb-3">
{tabs.map((tab) => ( <SegmentedControl
<button value={shareType}
key={tab.value} onChange={(value) => {
type="button" setShareType(value);
onClick={() => { resetForm();
setShareType(tab.value); }}
resetForm(); fullWidth
}} className="flex w-full gap-2 border-0 bg-transparent p-0"
className={`flex-1 min-w-0 rounded-lg px-4 py-2 text-sm font-medium transition-colors ${ options={tabs.map((tab) => ({
shareType === tab.value value: tab.value,
? 'bg-primary text-primary-foreground' label: `${tab.icon} ${tab.label}`,
: 'bg-card-hover text-muted hover:text-foreground' }))}
}`} />
>
{tab.icon} {tab.label}
</button>
))}
</div> </div>
{shareType === 'email' && ( {shareType === 'email' && (
@@ -271,25 +272,13 @@ export function ShareModal({
{share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'} {share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'}
</Badge> </Badge>
{isOwner && ( {isOwner && (
<button <IconButton
icon={<IconTrash className="h-4 w-4" />}
label="Retirer l'accès"
variant="destructive"
onClick={() => handleRemove(share.user.id)} onClick={() => handleRemove(share.user.id)}
disabled={isPending} disabled={isPending}
className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive" />
title="Retirer l'accès"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className="h-4 w-4"
>
<path
fillRule="evenodd"
d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z"
clipRule="evenodd"
/>
</svg>
</button>
)} )}
</div> </div>
</li> </li>

View File

@@ -1,6 +1,6 @@
import Link from 'next/link'; import Link from 'next/link';
import { auth } from '@/lib/auth'; import { auth } from '@/lib/auth';
import { RocketIcon } from '@/components/ui'; import { RocketIcon, getButtonClassName } from '@/components/ui';
import { ThemeToggle } from './ThemeToggle'; import { ThemeToggle } from './ThemeToggle';
import { UserMenu } from './UserMenu'; import { UserMenu } from './UserMenu';
import { WorkshopsDropdown } from './WorkshopsDropdown'; import { WorkshopsDropdown } from './WorkshopsDropdown';
@@ -36,7 +36,7 @@ export async function Header() {
) : ( ) : (
<Link <Link
href="/login" href="/login"
className="flex h-9 items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary-hover" className={getButtonClassName({ size: 'sm' })}
> >
Connexion Connexion
</Link> </Link>

View File

@@ -5,6 +5,7 @@ import { usePathname } from 'next/navigation';
export function NavLinks() { export function NavLinks() {
const pathname = usePathname(); const pathname = usePathname();
const isDev = process.env.NODE_ENV !== 'production';
const isActiveLink = (path: string) => pathname.startsWith(path); const isActiveLink = (path: string) => pathname.startsWith(path);
@@ -38,6 +39,17 @@ export function NavLinks() {
> >
👥 Équipes 👥 Équipes
</Link> </Link>
{isDev && (
<Link
href="/design-system"
className={`text-sm font-medium transition-colors ${
isActiveLink('/design-system') ? 'text-primary' : 'text-muted hover:text-foreground'
}`}
>
🎨 UI Guide
</Link>
)}
</> </>
); );
} }

View File

@@ -2,9 +2,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { signOut } from 'next-auth/react'; import { signOut } from 'next-auth/react';
import { useState, useRef } from 'react'; import { Avatar, DropdownMenu } from '@/components/ui';
import { Avatar } from '@/components/ui';
import { useClickOutside } from '@/hooks/useClickOutside';
interface UserMenuProps { interface UserMenuProps {
userName: string | null | undefined; userName: string | null | undefined;
@@ -12,51 +10,45 @@ interface UserMenuProps {
} }
export function UserMenu({ userName, userEmail }: UserMenuProps) { export function UserMenu({ userName, userEmail }: UserMenuProps) {
const [menuOpen, setMenuOpen] = useState(false);
const userMenuRef = useRef<HTMLDivElement>(null);
useClickOutside(userMenuRef, () => setMenuOpen(false), menuOpen);
return ( return (
<div ref={userMenuRef} className="relative"> <DropdownMenu
<button panelClassName="absolute right-0 z-20 mt-2 w-48 rounded-lg border border-border bg-card py-1 shadow-lg"
onClick={() => setMenuOpen(!menuOpen)} trigger={({ open, toggle }) => (
className="flex h-9 items-center gap-2 rounded-lg border border-border bg-card pl-1.5 pr-3 transition-colors hover:bg-card-hover" <button
> onClick={toggle}
<Avatar email={userEmail} name={userName} size={24} /> className="flex h-9 items-center gap-2 rounded-lg border border-border bg-card pl-1.5 pr-3 transition-colors hover:bg-card-hover"
<span className="text-sm font-medium text-foreground">
{userName || userEmail.split('@')[0]}
</span>
<svg
className={`h-4 w-4 text-muted transition-transform ${menuOpen ? 'rotate-180' : ''}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
> >
<path <Avatar email={userEmail} name={userName} size={24} />
strokeLinecap="round" <span className="text-sm font-medium text-foreground">
strokeLinejoin="round" {userName || userEmail.split('@')[0]}
strokeWidth={2} </span>
d="M19 9l-7 7-7-7" <svg
/> className={`h-4 w-4 text-muted transition-transform ${open ? 'rotate-180' : ''}`}
</svg> fill="none"
</button> viewBox="0 0 24 24"
stroke="currentColor"
{menuOpen && ( >
<div className="absolute right-0 z-20 mt-2 w-48 rounded-lg border border-border bg-card py-1 shadow-lg"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
)}
>
{({ close }) => (
<>
<div className="border-b border-border px-4 py-2"> <div className="border-b border-border px-4 py-2">
<p className="text-xs text-muted">Connecté en tant que</p> <p className="text-xs text-muted">Connecté en tant que</p>
<p className="truncate text-sm font-medium text-foreground">{userEmail}</p> <p className="truncate text-sm font-medium text-foreground">{userEmail}</p>
</div> </div>
<Link <Link
href="/profile" href="/profile"
onClick={() => setMenuOpen(false)} onClick={close}
className="block w-full px-4 py-2 text-left text-sm text-foreground hover:bg-card-hover" className="block w-full px-4 py-2 text-left text-sm text-foreground hover:bg-card-hover"
> >
👤 Mon Profil 👤 Mon Profil
</Link> </Link>
<Link <Link
href="/users" href="/users"
onClick={() => setMenuOpen(false)} onClick={close}
className="block w-full px-4 py-2 text-left text-sm text-foreground hover:bg-card-hover" className="block w-full px-4 py-2 text-left text-sm text-foreground hover:bg-card-hover"
> >
👥 Utilisateurs 👥 Utilisateurs
@@ -67,8 +59,8 @@ export function UserMenu({ userName, userEmail }: UserMenuProps) {
> >
Se déconnecter Se déconnecter
</button> </button>
</div> </>
)} )}
</div> </DropdownMenu>
); );
} }

View File

@@ -2,52 +2,46 @@
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { useState, useRef } from 'react'; import { DropdownMenu } from '@/components/ui';
import { WORKSHOPS } from '@/lib/workshops'; import { WORKSHOPS } from '@/lib/workshops';
import { useClickOutside } from '@/hooks/useClickOutside';
export function WorkshopsDropdown() { export function WorkshopsDropdown() {
const [workshopsOpen, setWorkshopsOpen] = useState(false);
const workshopsDropdownRef = useRef<HTMLDivElement>(null);
useClickOutside(workshopsDropdownRef, () => setWorkshopsOpen(false), workshopsOpen);
const pathname = usePathname(); const pathname = usePathname();
const isActiveLink = (path: string) => pathname.startsWith(path); const isActiveLink = (path: string) => pathname.startsWith(path);
return ( return (
<div className="relative" ref={workshopsDropdownRef}> <DropdownMenu
<button panelClassName="absolute left-0 z-20 mt-2 w-56 rounded-lg border border-border bg-card py-1 shadow-lg"
onClick={() => setWorkshopsOpen(!workshopsOpen)} trigger={({ open, toggle }) => (
className={`flex items-center gap-1 text-sm font-medium transition-colors ${ <button
WORKSHOPS.some((w) => isActiveLink(w.path)) onClick={toggle}
? 'text-primary' className={`flex items-center gap-1 text-sm font-medium transition-colors ${
: 'text-muted hover:text-foreground' WORKSHOPS.some((w) => isActiveLink(w.path))
}`} ? 'text-primary'
> : 'text-muted hover:text-foreground'
Nouvel atelier }`}
<svg
className={`h-4 w-4 transition-transform ${workshopsOpen ? 'rotate-180' : ''}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
> >
<path Nouvel atelier
strokeLinecap="round" <svg
strokeLinejoin="round" className={`h-4 w-4 transition-transform ${open ? 'rotate-180' : ''}`}
strokeWidth={2} fill="none"
d="M19 9l-7 7-7-7" viewBox="0 0 24 24"
/> stroke="currentColor"
</svg> >
</button> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
{workshopsOpen && ( </button>
<div className="absolute left-0 z-20 mt-2 w-56 rounded-lg border border-border bg-card py-1 shadow-lg"> )}
>
{({ close }) => (
<>
{WORKSHOPS.map((w) => ( {WORKSHOPS.map((w) => (
<Link <Link
key={w.id} key={w.id}
href={w.newPath} href={w.newPath}
className="flex items-center gap-3 px-4 py-2.5 text-sm text-foreground hover:bg-card-hover" className="flex items-center gap-3 px-4 py-2.5 text-sm text-foreground hover:bg-card-hover"
onClick={() => setWorkshopsOpen(false)} onClick={close}
> >
<span className="text-lg">{w.icon}</span> <span className="text-lg">{w.icon}</span>
<div> <div>
@@ -56,8 +50,8 @@ export function WorkshopsDropdown() {
</div> </div>
</Link> </Link>
))} ))}
</div> </>
)} )}
</div> </DropdownMenu>
); );
} }

View File

@@ -17,6 +17,7 @@ import {
arrayMove, arrayMove,
} from '@dnd-kit/sortable'; } from '@dnd-kit/sortable';
import type { MotivatorCard as MotivatorCardType } from '@/lib/types'; import type { MotivatorCard as MotivatorCardType } from '@/lib/types';
import { Button } from '@/components/ui';
import { MotivatorCard } from './MotivatorCard'; import { MotivatorCard } from './MotivatorCard';
import { MotivatorSummary } from './MotivatorSummary'; import { MotivatorSummary } from './MotivatorSummary';
import { InfluenceZone } from './InfluenceZone'; import { InfluenceZone } from './InfluenceZone';
@@ -167,12 +168,9 @@ export function MotivatorBoard({ sessionId, cards: initialCards, canEdit }: Moti
{/* Next button */} {/* Next button */}
<div className="flex justify-end"> <div className="flex justify-end">
<button <Button onClick={nextStep} className="px-6">
onClick={nextStep}
className="px-6 py-2 bg-primary text-primary-foreground rounded-lg font-medium hover:bg-primary/90 transition-colors"
>
Suivant Suivant
</button> </Button>
</div> </div>
</div> </div>
)} )}
@@ -197,18 +195,12 @@ export function MotivatorBoard({ sessionId, cards: initialCards, canEdit }: Moti
{/* Navigation buttons */} {/* Navigation buttons */}
<div className="flex justify-between"> <div className="flex justify-between">
<button <Button onClick={prevStep} variant="outline" className="px-6">
onClick={prevStep}
className="px-6 py-2 border border-border rounded-lg font-medium hover:bg-card transition-colors"
>
Retour Retour
</button> </Button>
<button <Button onClick={nextStep} className="px-6">
onClick={nextStep}
className="px-6 py-2 bg-primary text-primary-foreground rounded-lg font-medium hover:bg-primary/90 transition-colors"
>
Voir le récapitulatif Voir le récapitulatif
</button> </Button>
</div> </div>
</div> </div>
)} )}
@@ -226,12 +218,9 @@ export function MotivatorBoard({ sessionId, cards: initialCards, canEdit }: Moti
{/* Navigation buttons */} {/* Navigation buttons */}
<div className="flex justify-start"> <div className="flex justify-start">
<button <Button onClick={prevStep} variant="outline" className="px-6">
onClick={prevStep}
className="px-6 py-2 border border-border rounded-lg font-medium hover:bg-card transition-colors"
>
Modifier Modifier
</button> </Button>
</div> </div>
</div> </div>
)} )}

View File

@@ -3,8 +3,7 @@
import { useTransition } from 'react'; import { useTransition } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui'; import { Badge, Card, CardContent, CardHeader, CardTitle, IconButton, IconTrash } from '@/components/ui';
import { Badge } from '@/components/ui';
import { getGravatarUrl } from '@/lib/gravatar'; import { getGravatarUrl } from '@/lib/gravatar';
import type { OKR, KeyResult, OKRStatus, KeyResultStatus } from '@/lib/types'; import type { OKR, KeyResult, OKRStatus, KeyResultStatus } from '@/lib/types';
import { OKR_STATUS_LABELS, KEY_RESULT_STATUS_LABELS } from '@/lib/types'; import { OKR_STATUS_LABELS, KEY_RESULT_STATUS_LABELS } from '@/lib/types';
@@ -143,32 +142,20 @@ export function OKRCard({ okr, teamId, isAdmin = false, compact = false }: OKRCa
</div> </div>
<div className="flex items-center gap-1.5 flex-shrink-0 relative z-10"> <div className="flex items-center gap-1.5 flex-shrink-0 relative z-10">
{isAdmin && ( {isAdmin && (
<button <IconButton
icon={<IconTrash className="h-3 w-3" />}
label="Supprimer l'OKR"
variant="destructive"
size="xs"
onClick={handleDelete} onClick={handleDelete}
className="h-5 w-5 p-0 flex items-center justify-center rounded hover:bg-destructive/10 transition-colors flex-shrink-0" className="flex-shrink-0"
style={{ style={{
color: 'var(--destructive)', color: 'var(--destructive)',
border: '1px solid color-mix(in srgb, var(--destructive) 40%, transparent)', border: '1px solid color-mix(in srgb, var(--destructive) 40%, transparent)',
backgroundColor: 'color-mix(in srgb, var(--destructive) 5%, transparent)', backgroundColor: 'color-mix(in srgb, var(--destructive) 5%, transparent)',
}} }}
disabled={isPending} disabled={isPending}
title="Supprimer l'OKR" />
>
<svg
className="h-3 w-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2.5}
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>
)} )}
<Badge <Badge
style={{ style={{
@@ -255,32 +242,20 @@ export function OKRCard({ okr, teamId, isAdmin = false, compact = false }: OKRCa
{/* Action Zone */} {/* Action Zone */}
<div className="flex items-center gap-2 flex-shrink-0 relative z-10"> <div className="flex items-center gap-2 flex-shrink-0 relative z-10">
{isAdmin && ( {isAdmin && (
<button <IconButton
icon={<IconTrash className="h-4 w-4" />}
label="Supprimer l'OKR"
variant="destructive"
size="sm"
onClick={handleDelete} onClick={handleDelete}
className="h-6 w-6 p-0 flex items-center justify-center rounded hover:bg-destructive/10 transition-colors flex-shrink-0" className="flex-shrink-0"
style={{ style={{
color: 'var(--destructive)', color: 'var(--destructive)',
border: '1px solid color-mix(in srgb, var(--destructive) 40%, transparent)', border: '1px solid color-mix(in srgb, var(--destructive) 40%, transparent)',
backgroundColor: 'color-mix(in srgb, var(--destructive) 5%, transparent)', backgroundColor: 'color-mix(in srgb, var(--destructive) 5%, transparent)',
}} }}
disabled={isPending} disabled={isPending}
title="Supprimer l'OKR" />
>
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2.5}
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>
)} )}
<Badge <Badge
style={{ style={{

View File

@@ -478,7 +478,7 @@ export function OKRForm({ teamMembers, onSubmit, onCancel, initialData }: OKRFor
<Button <Button
type="submit" type="submit"
disabled={submitting} disabled={submitting}
className="bg-[var(--purple)] text-white hover:opacity-90 border-transparent" variant="brand"
> >
{submitting {submitting
? initialData?.teamMemberId ? initialData?.teamMemberId

View File

@@ -2,7 +2,18 @@
import { useState, useTransition } from 'react'; import { useState, useTransition } from 'react';
import type { SwotItem, Action, ActionLink, SwotCategory } from '@prisma/client'; import type { SwotItem, Action, ActionLink, SwotCategory } from '@prisma/client';
import { Button, Badge, Modal, ModalFooter, Input, Textarea, Select } from '@/components/ui'; import {
Button,
Badge,
Modal,
ModalFooter,
Input,
Textarea,
Select,
IconButton,
IconEdit,
IconTrash,
} from '@/components/ui';
import { createAction, updateAction, deleteAction } from '@/actions/swot'; import { createAction, updateAction, deleteAction } from '@/actions/swot';
type ActionWithLinks = Action & { type ActionWithLinks = Action & {
@@ -202,44 +213,19 @@ export function ActionPanel({
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<h3 className="font-medium text-foreground line-clamp-2">{action.title}</h3> <h3 className="font-medium text-foreground line-clamp-2">{action.title}</h3>
<div className="flex shrink-0 items-center gap-1"> <div className="flex shrink-0 items-center gap-1">
<button <IconButton
icon={<IconEdit />}
label="Modifier"
onClick={() => openEditModal(action)} onClick={() => openEditModal(action)}
className="rounded p-1 text-muted opacity-0 transition-opacity hover:bg-card-hover hover:text-foreground group-hover:opacity-100" className="opacity-0 transition-opacity group-hover:opacity-100"
aria-label="Modifier" />
> <IconButton
<svg icon={<IconTrash />}
className="h-3.5 w-3.5" label="Supprimer"
fill="none" variant="destructive"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
/>
</svg>
</button>
<button
onClick={() => handleDelete(action.id)} onClick={() => handleDelete(action.id)}
className="rounded p-1 text-muted opacity-0 transition-opacity hover:bg-destructive/10 hover:text-destructive group-hover:opacity-100" className="opacity-0 transition-opacity group-hover:opacity-100"
aria-label="Supprimer" />
>
<svg
className="h-3.5 w-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>
</div> </div>
</div> </div>
@@ -377,35 +363,37 @@ export function ActionPanel({
<label className="mb-2 block text-sm font-medium text-foreground">Priorité</label> <label className="mb-2 block text-sm font-medium text-foreground">Priorité</label>
<div className="flex gap-2"> <div className="flex gap-2">
{priorityLabels.map((label, index) => ( {priorityLabels.map((label, index) => (
<button <Button
key={index} key={index}
type="button" type="button"
variant="outline"
size="sm"
onClick={() => setPriority(index)} onClick={() => setPriority(index)}
className={` className={`
flex-1 rounded-lg border px-3 py-2 text-sm font-medium transition-colors flex-1
${ ${
priority === index priority === index
? index === 2 ? index === 2
? 'border-destructive bg-destructive/10 text-destructive' ? 'border-destructive bg-destructive/10 text-destructive'
: index === 1 : index === 1
? 'border-warning bg-warning/10 text-warning' ? 'border-warning bg-warning/10 text-warning'
: 'border-primary bg-primary/10 text-primary' : 'border-primary bg-primary/10 text-primary'
: 'border-border bg-card text-muted hover:bg-card-hover' : 'border-border bg-card text-muted hover:bg-card-hover'
} }
`} `}
> >
{label} {label}
</button> </Button>
))} ))}
</div> </div>
</div> </div>
</div> </div>
<ModalFooter> <ModalFooter>
<Button type="button" variant="outline" onClick={closeModal}> <Button type="button" variant="outline" size="sm" onClick={closeModal}>
Annuler Annuler
</Button> </Button>
<Button type="submit" loading={isPending}> <Button type="submit" size="sm" loading={isPending}>
{editingAction ? 'Enregistrer' : "Créer l'action"} {editingAction ? 'Enregistrer' : "Créer l'action"}
</Button> </Button>
</ModalFooter> </ModalFooter>

View File

@@ -6,6 +6,7 @@ import type { SwotItem, Action, ActionLink, SwotCategory } from '@prisma/client'
import { SwotQuadrant } from './SwotQuadrant'; import { SwotQuadrant } from './SwotQuadrant';
import { SwotCard } from './SwotCard'; import { SwotCard } from './SwotCard';
import { ActionPanel } from './ActionPanel'; import { ActionPanel } from './ActionPanel';
import { Button } from '@/components/ui';
import { moveSwotItem } from '@/actions/swot'; import { moveSwotItem } from '@/actions/swot';
type ActionWithLinks = Action & { type ActionWithLinks = Action & {
@@ -94,12 +95,9 @@ export function SwotBoard({ sessionId, items, actions }: SwotBoardProps) {
</p> </p>
</div> </div>
</div> </div>
<button <Button onClick={exitLinkMode} variant="outline" size="sm">
onClick={exitLinkMode}
className="rounded-lg border border-border bg-card px-4 py-2 text-sm font-medium hover:bg-card-hover"
>
Annuler Annuler
</button> </Button>
</div> </div>
)} )}

View File

@@ -3,7 +3,7 @@
import { forwardRef, memo, useState, useTransition } from 'react'; import { forwardRef, memo, useState, useTransition } from 'react';
import type { SwotItem, SwotCategory } from '@prisma/client'; import type { SwotItem, SwotCategory } from '@prisma/client';
import { updateSwotItem, deleteSwotItem, duplicateSwotItem } from '@/actions/swot'; import { updateSwotItem, deleteSwotItem, duplicateSwotItem } from '@/actions/swot';
import { IconEdit, IconTrash, IconDuplicate, IconCheck } from '@/components/ui'; import { IconEdit, IconTrash, IconDuplicate, IconCheck, IconButton } from '@/components/ui';
interface SwotCardProps { interface SwotCardProps {
item: SwotItem; item: SwotItem;
@@ -114,30 +114,32 @@ export const SwotCard = memo(
{/* Actions (visible on hover) */} {/* Actions (visible on hover) */}
{!linkMode && ( {!linkMode && (
<div className="absolute right-1 top-1 flex gap-0.5 opacity-0 transition-opacity group-hover:opacity-100"> <div className="absolute right-1 top-1 flex gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
<button <IconButton
icon={<IconEdit />}
label="Modifier"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setIsEditing(true); setIsEditing(true);
}} }}
className="rounded p-1 text-muted hover:bg-card-hover hover:text-foreground" />
aria-label="Modifier" <IconButton
> icon={<IconDuplicate />}
<IconEdit /> label="Dupliquer"
</button> variant="primary"
<button onClick={(e) => {
onClick={(e) => { e.stopPropagation(); handleDuplicate(); }} e.stopPropagation();
className="rounded p-1 text-muted hover:bg-primary/10 hover:text-primary" handleDuplicate();
aria-label="Dupliquer" }}
> />
<IconDuplicate /> <IconButton
</button> icon={<IconTrash />}
<button label="Supprimer"
onClick={(e) => { e.stopPropagation(); handleDelete(); }} variant="destructive"
className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive" onClick={(e) => {
aria-label="Supprimer" e.stopPropagation();
> handleDelete();
<IconTrash /> }}
</button> />
</div> </div>
)} )}

View File

@@ -4,7 +4,7 @@ import { forwardRef, useState, useTransition, useRef, ReactNode } from 'react';
import type { SwotCategory } from '@prisma/client'; import type { SwotCategory } from '@prisma/client';
import { createSwotItem } from '@/actions/swot'; import { createSwotItem } from '@/actions/swot';
import { QuadrantHelpPanel } from './QuadrantHelp'; import { QuadrantHelpPanel } from './QuadrantHelp';
import { IconPlus, InlineFormActions } from '@/components/ui'; import { IconPlus, InlineAddItem } from '@/components/ui';
interface SwotQuadrantProps { interface SwotQuadrantProps {
category: SwotCategory; category: SwotCategory;
@@ -66,16 +66,6 @@ export const SwotQuadrant = forwardRef<HTMLDivElement, SwotQuadrantProps>(
}); });
} }
function handleKeyDown(e: React.KeyboardEvent) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleAdd();
} else if (e.key === 'Escape') {
setIsAdding(false);
setNewContent('');
}
}
return ( return (
<div <div
ref={ref} ref={ref}
@@ -129,32 +119,18 @@ export const SwotQuadrant = forwardRef<HTMLDivElement, SwotQuadrantProps>(
{/* Add Form */} {/* Add Form */}
{isAdding && ( {isAdding && (
<div className="rounded-lg border border-border bg-card p-2 shadow-sm"> <InlineAddItem
<textarea value={newContent}
autoFocus onChange={setNewContent}
value={newContent} onSubmit={handleAdd}
onChange={(e) => setNewContent(e.target.value)} onCancel={() => {
onKeyDown={handleKeyDown} setIsAdding(false);
onBlur={(e) => { setNewContent('');
// Don't trigger on blur if clicking on a button }}
if (!e.currentTarget.contains(e.relatedTarget as Node)) { isPending={isPending}
handleAdd(); placeholder="Décrivez cet élément..."
} submitColorClass={`${styles.text} hover:bg-white/50`}
}} />
placeholder="Décrivez cet élément..."
className="w-full resize-none rounded border-0 bg-transparent p-1 text-sm text-foreground placeholder:text-muted focus:outline-none focus:ring-0"
rows={2}
disabled={isPending}
/>
<InlineFormActions
onCancel={() => { setIsAdding(false); setNewContent(''); }}
onSubmit={handleAdd}
isPending={isPending}
disabled={!newContent.trim()}
submitColorClass={`${styles.text} hover:bg-white/50`}
className="mt-1"
/>
</div>
)} )}
</div> </div>
</div> </div>

View File

@@ -153,7 +153,7 @@ export function AddMemberModal({
<Button <Button
type="submit" type="submit"
disabled={!selectedUserId || loading} disabled={!selectedUserId || loading}
className="bg-[var(--purple)] text-white hover:opacity-90 border-transparent" variant="brand"
> >
{loading ? 'Ajout...' : 'Ajouter'} {loading ? 'Ajout...' : 'Ajouter'}
</Button> </Button>

View File

@@ -42,7 +42,11 @@ export function DeleteTeamButton({ teamId, teamName }: DeleteTeamButtonProps) {
<Button <Button
onClick={() => setShowModal(true)} onClick={() => setShowModal(true)}
variant="outline" variant="outline"
className="text-destructive border-destructive hover:bg-destructive/10" size="sm"
style={{
color: 'var(--destructive)',
borderColor: 'var(--destructive)',
}}
> >
Supprimer l&apos;équipe Supprimer l&apos;équipe
</Button> </Button>
@@ -63,7 +67,7 @@ export function DeleteTeamButton({ teamId, teamName }: DeleteTeamButtonProps) {
supprimés. supprimés.
</p> </p>
<ModalFooter> <ModalFooter>
<Button variant="ghost" onClick={() => setShowModal(false)} disabled={isPending}> <Button variant="outline" onClick={() => setShowModal(false)} disabled={isPending}>
Annuler Annuler
</Button> </Button>
<Button variant="destructive" onClick={handleDelete} disabled={isPending}> <Button variant="destructive" onClick={handleDelete} disabled={isPending}>

View File

@@ -75,10 +75,7 @@ export function MembersList({ members, teamId, isAdmin, onMemberUpdate }: Member
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold text-foreground">Membres ({members.length})</h3> <h3 className="text-lg font-semibold text-foreground">Membres ({members.length})</h3>
{isAdmin && ( {isAdmin && (
<Button <Button onClick={() => setAddMemberOpen(true)} variant="brand" size="sm">
onClick={() => setAddMemberOpen(true)}
className="bg-[var(--purple)] text-white hover:opacity-90 border-transparent"
>
Ajouter un membre Ajouter un membre
</Button> </Button>
)} )}

View File

@@ -1,6 +1,6 @@
import { forwardRef, ButtonHTMLAttributes } from 'react'; import { forwardRef, ButtonHTMLAttributes } from 'react';
type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive'; type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive' | 'brand';
type ButtonSize = 'sm' | 'md' | 'lg'; type ButtonSize = 'sm' | 'md' | 'lg';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
@@ -15,6 +15,7 @@ const variantStyles: Record<ButtonVariant, string> = {
outline: 'bg-transparent text-foreground hover:bg-card-hover border-border', outline: 'bg-transparent text-foreground hover:bg-card-hover border-border',
ghost: 'bg-transparent text-foreground hover:bg-card-hover border-transparent', ghost: 'bg-transparent text-foreground hover:bg-card-hover border-transparent',
destructive: 'bg-destructive text-white hover:bg-destructive/90 border-transparent', destructive: 'bg-destructive text-white hover:bg-destructive/90 border-transparent',
brand: 'bg-[var(--purple)] text-white hover:opacity-90 border-transparent',
}; };
const sizeStyles: Record<ButtonSize, string> = { const sizeStyles: Record<ButtonSize, string> = {
@@ -23,6 +24,28 @@ const sizeStyles: Record<ButtonSize, string> = {
lg: 'h-12 px-6 text-base gap-2', lg: 'h-12 px-6 text-base gap-2',
}; };
interface GetButtonClassNameOptions {
variant?: ButtonVariant;
size?: ButtonSize;
className?: string;
}
export function getButtonClassName({
variant = 'primary',
size = 'md',
className = '',
}: GetButtonClassNameOptions = {}) {
return `
inline-flex items-center justify-center rounded-lg border font-medium
transition-colors focus-visible:outline-none focus-visible:ring-2
focus-visible:ring-ring focus-visible:ring-offset-2
disabled:pointer-events-none disabled:opacity-50
${variantStyles[variant]}
${sizeStyles[size]}
${className}
`;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>( export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
( (
{ className = '', variant = 'primary', size = 'md', loading, disabled, children, ...props }, { className = '', variant = 'primary', size = 'md', loading, disabled, children, ...props },
@@ -32,15 +55,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
<button <button
ref={ref} ref={ref}
disabled={disabled || loading} disabled={disabled || loading}
className={` className={getButtonClassName({ variant, size, className })}
inline-flex items-center justify-center rounded-lg border font-medium
transition-colors focus-visible:outline-none focus-visible:ring-2
focus-visible:ring-ring focus-visible:ring-offset-2
disabled:pointer-events-none disabled:opacity-50
${variantStyles[variant]}
${sizeStyles[size]}
${className}
`}
{...props} {...props}
> >
{loading && ( {loading && (

View File

@@ -0,0 +1,52 @@
'use client';
import { ReactNode, useId, useState } from 'react';
interface DisclosureProps {
title: ReactNode;
subtitle?: ReactNode;
defaultOpen?: boolean;
className?: string;
children: ReactNode;
}
export function Disclosure({
title,
subtitle,
defaultOpen = false,
className = '',
children,
}: DisclosureProps) {
const [isOpen, setIsOpen] = useState(defaultOpen);
const contentId = useId();
return (
<div className={`rounded-lg border border-border bg-card-hover ${className}`}>
<button
type="button"
onClick={() => setIsOpen((prev) => !prev)}
aria-expanded={isOpen}
aria-controls={contentId}
className="flex w-full items-center justify-between px-4 py-2.5 text-left transition-colors hover:bg-card"
>
<div className="min-w-0">
<div className="text-sm font-semibold text-foreground">{title}</div>
{subtitle && <div className="text-xs text-muted">{subtitle}</div>}
</div>
<svg
className={`h-4 w-4 shrink-0 text-muted transition-transform ${isOpen ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{isOpen && (
<div id={contentId} className="border-t border-border px-4 py-3">
{children}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,29 @@
'use client';
import { ReactNode, useRef, useState } from 'react';
import { useClickOutside } from '@/hooks/useClickOutside';
interface DropdownMenuProps {
trigger: (options: { open: boolean; toggle: () => void }) => ReactNode;
children: (options: { close: () => void }) => ReactNode;
panelClassName?: string;
className?: string;
}
export function DropdownMenu({
trigger,
children,
panelClassName = '',
className = '',
}: DropdownMenuProps) {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useClickOutside(ref, () => setOpen(false), open);
return (
<div ref={ref} className={`relative ${className}`}>
{trigger({ open, toggle: () => setOpen((prev) => !prev) })}
{open && <div className={panelClassName}>{children({ close: () => setOpen(false) })}</div>}
</div>
);
}

View File

@@ -0,0 +1,67 @@
import { ChangeEventHandler, ReactNode } from 'react';
import { Input } from './Input';
interface FormFieldProps {
id?: string;
label?: string;
error?: string;
hint?: string;
className?: string;
children: ReactNode;
}
export function FormField({ id, label, error, hint, className = '', children }: FormFieldProps) {
return (
<div className={className || 'w-full'}>
{label && (
<label htmlFor={id} className="mb-2 block text-sm font-medium text-foreground">
{label}
</label>
)}
{children}
{hint && !error && <p className="mt-1 text-xs text-muted">{hint}</p>}
{error && <p className="mt-1.5 text-sm text-destructive">{error}</p>}
</div>
);
}
type BaseInputProps = {
id?: string;
name?: string;
value?: string | number;
defaultValue?: string | number;
onChange?: ChangeEventHandler<HTMLInputElement>;
required?: boolean;
disabled?: boolean;
min?: string | number;
max?: string | number;
step?: string | number;
className?: string;
};
interface DateInputProps extends BaseInputProps {
label?: string;
error?: string;
}
export function DateInput({ label, error, className = '', ...props }: DateInputProps) {
return (
<FormField id={props.id} label={label} error={error}>
<Input type="date" className={className} {...props} />
</FormField>
);
}
interface NumberInputProps extends BaseInputProps {
label?: string;
error?: string;
hint?: string;
}
export function NumberInput({ label, error, hint, className = '', ...props }: NumberInputProps) {
return (
<FormField id={props.id} label={label} error={error} hint={hint}>
<Input type="number" className={className} {...props} />
</FormField>
);
}

View File

@@ -0,0 +1,53 @@
import { ButtonHTMLAttributes, ReactNode } from 'react';
type IconButtonVariant = 'default' | 'primary' | 'destructive' | 'ghost';
type IconButtonSize = 'xs' | 'sm' | 'md';
interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
icon: ReactNode;
label: string;
variant?: IconButtonVariant;
size?: IconButtonSize;
}
const variantStyles: Record<IconButtonVariant, string> = {
default: 'text-muted hover:bg-card-hover hover:text-foreground border-transparent',
primary: 'text-muted hover:bg-primary/10 hover:text-primary border-transparent',
destructive:
'text-muted hover:bg-destructive/10 hover:text-destructive border-transparent',
ghost: 'text-muted hover:text-foreground border-transparent',
};
const sizeStyles: Record<IconButtonSize, string> = {
xs: 'h-6 w-6',
sm: 'h-7 w-7',
md: 'h-8 w-8',
};
export function IconButton({
icon,
label,
variant = 'default',
size = 'sm',
className = '',
type = 'button',
...props
}: IconButtonProps) {
return (
<button
type={type}
aria-label={label}
title={label}
className={`
inline-flex items-center justify-center rounded-md border
transition-colors disabled:pointer-events-none disabled:opacity-50
${sizeStyles[size]}
${variantStyles[variant]}
${className}
`}
{...props}
>
{icon}
</button>
);
}

View File

@@ -0,0 +1,59 @@
import { ReactNode } from 'react';
import { InlineFormActions } from './InlineFormActions';
interface InlineAddItemProps {
value: string;
onChange: (value: string) => void;
onSubmit: () => void;
onCancel: () => void;
isPending: boolean;
placeholder?: string;
rows?: number;
extra?: ReactNode;
submitColorClass?: string;
className?: string;
}
export function InlineAddItem({
value,
onChange,
onSubmit,
onCancel,
isPending,
placeholder,
rows = 2,
extra,
submitColorClass,
className = '',
}: InlineAddItemProps) {
return (
<div className={`rounded-lg border border-border bg-card p-2 shadow-sm ${className}`}>
<textarea
autoFocus
value={value}
onChange={(e) => onChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
onSubmit();
} else if (e.key === 'Escape') {
onCancel();
}
}}
placeholder={placeholder}
className="w-full resize-none rounded border-0 bg-transparent p-1 text-sm text-foreground placeholder:text-muted focus:outline-none focus:ring-0"
rows={rows}
disabled={isPending}
/>
{extra}
<InlineFormActions
onCancel={onCancel}
onSubmit={onSubmit}
isPending={isPending}
disabled={!value.trim()}
submitColorClass={submitColorClass}
className="mt-1"
/>
</div>
);
}

View File

@@ -0,0 +1,68 @@
import { ReactNode } from 'react';
interface InlineEditorProps {
value: string;
onChange: (value: string) => void;
onSave: () => void;
onCancel: () => void;
isPending: boolean;
placeholder?: string;
rows?: number;
submitLabel?: string;
footer?: ReactNode;
}
export function InlineEditor({
value,
onChange,
onSave,
onCancel,
isPending,
placeholder,
rows = 2,
submitLabel = 'Enregistrer',
footer,
}: InlineEditorProps) {
return (
<div className="space-y-2">
<textarea
autoFocus
value={value}
onChange={(e) => onChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
onSave();
} else if (e.key === 'Escape') {
onCancel();
}
}}
className="w-full resize-none rounded border-0 bg-transparent p-0 text-sm text-foreground focus:outline-none focus:ring-0"
rows={rows}
disabled={isPending}
placeholder={placeholder}
/>
{footer}
{!footer && (
<div className="flex justify-end gap-2">
<button
type="button"
className="rounded px-2 py-1 text-xs text-muted hover:bg-card-hover"
onClick={onCancel}
disabled={isPending}
>
Annuler
</button>
<button
type="button"
className="rounded px-2 py-1 text-xs font-medium text-primary hover:bg-primary/10"
onClick={onSave}
disabled={isPending || !value.trim()}
>
{submitLabel}
</button>
</div>
)}
</div>
);
}

View File

@@ -1,3 +1,5 @@
import { Button } from './Button';
interface InlineFormActionsProps { interface InlineFormActionsProps {
onCancel: () => void; onCancel: () => void;
onSubmit: () => void; onSubmit: () => void;
@@ -19,21 +21,27 @@ export function InlineFormActions({
}: InlineFormActionsProps) { }: InlineFormActionsProps) {
return ( return (
<div className={`flex justify-end gap-1 ${className}`}> <div className={`flex justify-end gap-1 ${className}`}>
<button <Button
type="button"
size="sm"
variant="ghost"
onClick={onCancel} onClick={onCancel}
className="rounded px-2 py-1 text-xs text-muted hover:bg-card-hover" className="h-7 px-2 text-xs text-muted"
disabled={isPending} disabled={isPending}
> >
Annuler Annuler
</button> </Button>
<button <Button
type="button"
size="sm"
variant="ghost"
onMouseDown={(e) => e.preventDefault()} onMouseDown={(e) => e.preventDefault()}
onClick={onSubmit} onClick={onSubmit}
disabled={isPending || disabled} disabled={isPending || disabled}
className={`rounded px-2 py-1 text-xs font-medium disabled:opacity-50 ${submitColorClass}`} className={`h-7 px-2 text-xs ${submitColorClass}`}
> >
{isPending ? '...' : submitLabel} {isPending ? '...' : submitLabel}
</button> </Button>
</div> </div>
); );
} }

View File

@@ -0,0 +1,59 @@
'use client';
import { ReactNode } from 'react';
export interface SegmentedOption<T extends string> {
value: T;
label: ReactNode;
disabled?: boolean;
}
interface SegmentedControlProps<T extends string> {
value: T;
options: SegmentedOption<T>[];
onChange: (value: T) => void;
size?: 'sm' | 'md';
fullWidth?: boolean;
className?: string;
}
const sizeStyles = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-sm',
} as const;
export function SegmentedControl<T extends string>({
value,
options,
onChange,
size = 'md',
fullWidth = false,
className = '',
}: SegmentedControlProps<T>) {
return (
<div
className={`inline-flex items-center gap-1 rounded-lg border border-border bg-card p-1 ${className}`}
>
{options.map((option) => (
<button
key={option.value}
type="button"
disabled={option.disabled}
onClick={() => onChange(option.value)}
className={`
rounded-md font-medium transition-colors disabled:pointer-events-none disabled:opacity-50
${sizeStyles[size]}
${fullWidth ? 'min-w-0 flex-1' : ''}
${
value === option.value
? 'bg-primary text-primary-foreground'
: 'text-muted hover:bg-card-hover hover:text-foreground'
}
`}
>
{option.label}
</button>
))}
</div>
);
}

View File

@@ -1,8 +1,10 @@
export { Avatar } from './Avatar'; export { Avatar } from './Avatar';
export { Badge } from './Badge'; export { Badge } from './Badge';
export { Button } from './Button'; export { Button, getButtonClassName } from './Button';
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card'; export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card';
export { CollaboratorDisplay } from './CollaboratorDisplay'; export { CollaboratorDisplay } from './CollaboratorDisplay';
export { Disclosure } from './Disclosure';
export { DropdownMenu } from './DropdownMenu';
export { EditableTitle } from './EditableTitle'; export { EditableTitle } from './EditableTitle';
export { export {
EditableSessionTitle, EditableSessionTitle,
@@ -13,6 +15,9 @@ export {
EditableGifMoodTitle, EditableGifMoodTitle,
} from './EditableTitles'; } from './EditableTitles';
export { IconEdit, IconTrash, IconDuplicate, IconPlus, IconCheck, IconClose } from './Icons'; export { IconEdit, IconTrash, IconDuplicate, IconPlus, IconCheck, IconClose } from './Icons';
export { IconButton } from './IconButton';
export { InlineAddItem } from './InlineAddItem';
export { InlineEditor } from './InlineEditor';
export { InlineFormActions } from './InlineFormActions'; export { InlineFormActions } from './InlineFormActions';
export { PageHeader } from './PageHeader'; export { PageHeader } from './PageHeader';
export { SessionPageHeader } from './SessionPageHeader'; export { SessionPageHeader } from './SessionPageHeader';
@@ -22,6 +27,8 @@ export { Modal, ModalFooter } from './Modal';
export { RocketIcon } from './RocketIcon'; export { RocketIcon } from './RocketIcon';
export { Select } from './Select'; export { Select } from './Select';
export type { SelectOption } from './Select'; export type { SelectOption } from './Select';
export { SegmentedControl } from './SegmentedControl';
export { Textarea } from './Textarea'; export { Textarea } from './Textarea';
export { ToggleGroup } from './ToggleGroup'; export { ToggleGroup } from './ToggleGroup';
export type { ToggleOption } from './ToggleGroup'; export type { ToggleOption } from './ToggleGroup';
export { FormField, DateInput, NumberInput } from './FormField';

View File

@@ -1,58 +1,36 @@
'use client'; 'use client';
import { useState } from 'react'; import { Disclosure } from '@/components/ui';
export function WeatherInfoPanel() { export function WeatherInfoPanel() {
const [isOpen, setIsOpen] = useState(false);
return ( return (
<div className="mb-6 rounded-lg border border-border bg-card-hover"> <Disclosure title="Les 4 axes de la météo personnelle" className="mb-6">
<button <div className="grid gap-2.5 sm:grid-cols-2 lg:grid-cols-4">
onClick={() => setIsOpen(!isOpen)} <div>
className="w-full flex items-center justify-between px-4 py-2.5 text-left transition-colors hover:bg-card" <p className="mb-0.5 text-xs font-medium text-foreground"> Performance</p>
> <p className="text-xs leading-relaxed text-muted">
<h3 className="text-sm font-semibold text-foreground"> Votre performance personnelle et l&apos;atteinte de vos objectifs
Les 4 axes de la météo personnelle </p>
</h3>
<svg
className={`h-4 w-4 text-muted transition-transform ${isOpen ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{isOpen && (
<div className="border-t border-border px-4 py-3">
<div className="grid gap-2.5 sm:grid-cols-2 lg:grid-cols-4">
<div>
<p className="text-xs font-medium text-foreground mb-0.5"> Performance</p>
<p className="text-xs text-muted leading-relaxed">
Votre performance personnelle et l&apos;atteinte de vos objectifs
</p>
</div>
<div>
<p className="text-xs font-medium text-foreground mb-0.5">😊 Moral</p>
<p className="text-xs text-muted leading-relaxed">
Votre moral actuel et votre ressenti
</p>
</div>
<div>
<p className="text-xs font-medium text-foreground mb-0.5">🌊 Flux</p>
<p className="text-xs text-muted leading-relaxed">
Votre flux de travail personnel et les blocages éventuels
</p>
</div>
<div>
<p className="text-xs font-medium text-foreground mb-0.5">💎 Création de valeur</p>
<p className="text-xs text-muted leading-relaxed">
Votre création de valeur et votre apport
</p>
</div>
</div>
</div> </div>
)} <div>
</div> <p className="mb-0.5 text-xs font-medium text-foreground">😊 Moral</p>
<p className="text-xs leading-relaxed text-muted">
Votre moral actuel et votre ressenti
</p>
</div>
<div>
<p className="mb-0.5 text-xs font-medium text-foreground">🌊 Flux</p>
<p className="text-xs leading-relaxed text-muted">
Votre flux de travail personnel et les blocages éventuels
</p>
</div>
<div>
<p className="mb-0.5 text-xs font-medium text-foreground">💎 Création de valeur</p>
<p className="text-xs leading-relaxed text-muted">
Votre création de valeur et votre apport
</p>
</div>
</div>
</Disclosure>
); );
} }

View File

@@ -1,6 +1,7 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { Disclosure } from '@/components/ui';
import type { WeatherHistoryPoint } from '@/services/weather'; import type { WeatherHistoryPoint } from '@/services/weather';
interface WeatherTrendChartProps { interface WeatherTrendChartProps {
@@ -63,7 +64,6 @@ function buildPath(
const Y_TICKS = [1, 5, 10, 14, 19]; const Y_TICKS = [1, 5, 10, 14, 19];
export function WeatherTrendChart({ data, currentSessionId }: WeatherTrendChartProps) { export function WeatherTrendChart({ data, currentSessionId }: WeatherTrendChartProps) {
const [isOpen, setIsOpen] = useState(false);
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null); const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
if (data.length < 2) return null; if (data.length < 2) return null;
@@ -71,27 +71,16 @@ export function WeatherTrendChart({ data, currentSessionId }: WeatherTrendChartP
const hoveredPt = hoveredIdx !== null ? data[hoveredIdx] : null; const hoveredPt = hoveredIdx !== null ? data[hoveredIdx] : null;
return ( return (
<div className="mb-6 rounded-lg border border-border bg-card-hover"> <Disclosure
<button className="mb-6"
onClick={() => setIsOpen(!isOpen)} title={
className="w-full flex items-center justify-between px-4 py-2.5 text-left transition-colors hover:bg-card" <>
>
<h3 className="text-sm font-semibold text-foreground">
Évolution dans le temps Évolution dans le temps
<span className="ml-2 text-xs font-normal text-muted">{data.length} sessions</span> <span className="ml-2 text-xs font-normal text-muted">{data.length} sessions</span>
</h3> </>
<svg }
className={`h-4 w-4 text-muted transition-transform ${isOpen ? 'rotate-180' : ''}`} >
fill="none" <div className="py-1">
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{isOpen && (
<div className="border-t border-border px-4 py-4">
{/* Legend */} {/* Legend */}
<div className="flex flex-wrap gap-4 mb-3"> <div className="flex flex-wrap gap-4 mb-3">
{INDICATORS.map((ind) => ( {INDICATORS.map((ind) => (
@@ -300,8 +289,7 @@ export function WeatherTrendChart({ data, currentSessionId }: WeatherTrendChartP
<p className="text-[10px] text-muted mt-1 text-right"> <p className="text-[10px] text-muted mt-1 text-right">
Score 1 = (meilleur) · Score 19 = dégradé Score 1 = (meilleur) · Score 19 = dégradé
</p> </p>
</div> </div>
)} </Disclosure>
</div>
); );
} }

View File

@@ -4,7 +4,7 @@ import { forwardRef, memo, useState, useTransition } from 'react';
import type { WeeklyCheckInItem } from '@prisma/client'; import type { WeeklyCheckInItem } from '@prisma/client';
import { updateWeeklyCheckInItem, deleteWeeklyCheckInItem } from '@/actions/weekly-checkin'; import { updateWeeklyCheckInItem, deleteWeeklyCheckInItem } from '@/actions/weekly-checkin';
import { WEEKLY_CHECK_IN_BY_CATEGORY, EMOTION_BY_TYPE } from '@/lib/types'; import { WEEKLY_CHECK_IN_BY_CATEGORY, EMOTION_BY_TYPE } from '@/lib/types';
import { IconEdit, IconTrash, InlineFormActions } from '@/components/ui'; import { IconEdit, IconTrash, IconButton, InlineFormActions } from '@/components/ui';
import { Select } from '@/components/ui/Select'; import { Select } from '@/components/ui/Select';
interface WeeklyCheckInCardProps { interface WeeklyCheckInCardProps {
@@ -144,23 +144,23 @@ export const WeeklyCheckInCard = memo(
{/* Actions (visible on hover) */} {/* Actions (visible on hover) */}
<div className="absolute right-1 top-1 flex gap-0.5 opacity-0 transition-opacity group-hover:opacity-100"> <div className="absolute right-1 top-1 flex gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
<button <IconButton
icon={<IconEdit />}
label="Modifier"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setIsEditing(true); setIsEditing(true);
}} }}
className="rounded p-1 text-muted hover:bg-card-hover hover:text-foreground" />
aria-label="Modifier" <IconButton
> icon={<IconTrash />}
<IconEdit /> label="Supprimer"
</button> variant="destructive"
<button onClick={(e) => {
onClick={(e) => { e.stopPropagation(); handleDelete(); }} e.stopPropagation();
className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive" handleDelete();
aria-label="Supprimer" }}
> />
<IconTrash />
</button>
</div> </div>
</> </>
)} )}

View File

@@ -4,7 +4,7 @@ import { forwardRef, useState, useTransition, useRef, ReactNode } from 'react';
import type { WeeklyCheckInCategory } from '@prisma/client'; import type { WeeklyCheckInCategory } from '@prisma/client';
import { createWeeklyCheckInItem } from '@/actions/weekly-checkin'; import { createWeeklyCheckInItem } from '@/actions/weekly-checkin';
import { WEEKLY_CHECK_IN_BY_CATEGORY, EMOTION_BY_TYPE } from '@/lib/types'; import { WEEKLY_CHECK_IN_BY_CATEGORY, EMOTION_BY_TYPE } from '@/lib/types';
import { IconPlus, InlineFormActions } from '@/components/ui'; import { IconPlus, InlineAddItem } from '@/components/ui';
import { Select } from '@/components/ui/Select'; import { Select } from '@/components/ui/Select';
interface WeeklyCheckInSectionProps { interface WeeklyCheckInSectionProps {
@@ -44,17 +44,6 @@ export const WeeklyCheckInSection = forwardRef<HTMLDivElement, WeeklyCheckInSect
}); });
} }
function handleKeyDown(e: React.KeyboardEvent) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleAdd();
} else if (e.key === 'Escape') {
setIsAdding(false);
setNewContent('');
setNewEmotion('NONE');
}
}
return ( return (
<div <div
ref={ref} ref={ref}
@@ -93,53 +82,31 @@ export const WeeklyCheckInSection = forwardRef<HTMLDivElement, WeeklyCheckInSect
{/* Add Form */} {/* Add Form */}
{isAdding && ( {isAdding && (
<div <InlineAddItem
className="rounded-lg border border-border bg-card p-2 shadow-sm" value={newContent}
onBlur={(e) => { onChange={setNewContent}
// Don't close if focus moves to another element in this container onSubmit={handleAdd}
const currentTarget = e.currentTarget; onCancel={() => {
const relatedTarget = e.relatedTarget as Node | null; setIsAdding(false);
if (relatedTarget && currentTarget.contains(relatedTarget)) { setNewContent('');
return; setNewEmotion('NONE');
}
// Only add on blur if content is not empty
if (newContent.trim()) {
handleAdd();
} else {
setIsAdding(false);
setNewContent('');
setNewEmotion('NONE');
}
}} }}
> isPending={isPending}
<textarea placeholder={`Décrivez ${config.title.toLowerCase()}...`}
autoFocus extra={
value={newContent} <div className="mt-2">
onChange={(e) => setNewContent(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={`Décrivez ${config.title.toLowerCase()}...`}
className="w-full resize-none rounded border-0 bg-transparent p-1 text-sm text-foreground placeholder:text-muted focus:outline-none focus:ring-0"
rows={2}
disabled={isPending}
/>
<div className="mt-2 flex items-center justify-between gap-2">
<Select <Select
value={newEmotion} value={newEmotion}
onChange={(e) => setNewEmotion(e.target.value as typeof newEmotion)} onChange={(e) => setNewEmotion(e.target.value as typeof newEmotion)}
className="text-xs flex-1" className="text-xs"
options={Object.values(EMOTION_BY_TYPE).map((em) => ({ options={Object.values(EMOTION_BY_TYPE).map((em) => ({
value: em.emotion, value: em.emotion,
label: `${em.icon} ${em.label}`, label: `${em.icon} ${em.label}`,
}))} }))}
/> />
<InlineFormActions
onCancel={() => { setIsAdding(false); setNewContent(''); setNewEmotion('NONE'); }}
onSubmit={handleAdd}
isPending={isPending}
disabled={!newContent.trim()}
/>
</div> </div>
</div> }
/>
)} )}
</div> </div>
</div> </div>

View File

@@ -4,7 +4,7 @@ import { forwardRef, memo, useState, useTransition } from 'react';
import type { YearReviewItem } from '@prisma/client'; import type { YearReviewItem } from '@prisma/client';
import { updateYearReviewItem, deleteYearReviewItem } from '@/actions/year-review'; import { updateYearReviewItem, deleteYearReviewItem } from '@/actions/year-review';
import { YEAR_REVIEW_BY_CATEGORY } from '@/lib/types'; import { YEAR_REVIEW_BY_CATEGORY } from '@/lib/types';
import { IconEdit, IconTrash } from '@/components/ui'; import { IconEdit, IconTrash, IconButton } from '@/components/ui';
interface YearReviewCardProps { interface YearReviewCardProps {
item: YearReviewItem; item: YearReviewItem;
@@ -88,23 +88,23 @@ export const YearReviewCard = memo(
{/* Actions (visible on hover) */} {/* Actions (visible on hover) */}
<div className="absolute right-1 top-1 flex gap-0.5 opacity-0 transition-opacity group-hover:opacity-100"> <div className="absolute right-1 top-1 flex gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
<button <IconButton
icon={<IconEdit />}
label="Modifier"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setIsEditing(true); setIsEditing(true);
}} }}
className="rounded p-1 text-muted hover:bg-card-hover hover:text-foreground" />
aria-label="Modifier" <IconButton
> icon={<IconTrash />}
<IconEdit /> label="Supprimer"
</button> variant="destructive"
<button onClick={(e) => {
onClick={(e) => { e.stopPropagation(); handleDelete(); }} e.stopPropagation();
className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive" handleDelete();
aria-label="Supprimer" }}
> />
<IconTrash />
</button>
</div> </div>
</> </>
)} )}

View File

@@ -4,7 +4,7 @@ import { forwardRef, useState, useTransition, useRef, ReactNode } from 'react';
import type { YearReviewCategory } from '@prisma/client'; import type { YearReviewCategory } from '@prisma/client';
import { createYearReviewItem } from '@/actions/year-review'; import { createYearReviewItem } from '@/actions/year-review';
import { YEAR_REVIEW_BY_CATEGORY } from '@/lib/types'; import { YEAR_REVIEW_BY_CATEGORY } from '@/lib/types';
import { IconPlus, InlineFormActions } from '@/components/ui'; import { IconPlus, InlineAddItem } from '@/components/ui';
interface YearReviewSectionProps { interface YearReviewSectionProps {
category: YearReviewCategory; category: YearReviewCategory;
@@ -40,16 +40,6 @@ export const YearReviewSection = forwardRef<HTMLDivElement, YearReviewSectionPro
}); });
} }
function handleKeyDown(e: React.KeyboardEvent) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleAdd();
} else if (e.key === 'Escape') {
setIsAdding(false);
setNewContent('');
}
}
return ( return (
<div <div
ref={ref} ref={ref}
@@ -88,31 +78,17 @@ export const YearReviewSection = forwardRef<HTMLDivElement, YearReviewSectionPro
{/* Add Form */} {/* Add Form */}
{isAdding && ( {isAdding && (
<div className="rounded-lg border border-border bg-card p-2 shadow-sm"> <InlineAddItem
<textarea value={newContent}
autoFocus onChange={setNewContent}
value={newContent} onSubmit={handleAdd}
onChange={(e) => setNewContent(e.target.value)} onCancel={() => {
onKeyDown={handleKeyDown} setIsAdding(false);
onBlur={(e) => { setNewContent('');
// Don't trigger on blur if clicking on a button }}
if (!e.currentTarget.contains(e.relatedTarget as Node)) { isPending={isPending}
handleAdd(); placeholder={`Décrivez ${config.title.toLowerCase()}...`}
} />
}}
placeholder={`Décrivez ${config.title.toLowerCase()}...`}
className="w-full resize-none rounded border-0 bg-transparent p-1 text-sm text-foreground placeholder:text-muted focus:outline-none focus:ring-0"
rows={2}
disabled={isPending}
/>
<InlineFormActions
onCancel={() => { setIsAdding(false); setNewContent(''); }}
onSubmit={handleAdd}
isPending={isPending}
disabled={!newContent.trim()}
className="mt-1"
/>
</div>
)} )}
</div> </div>
</div> </div>