refactor(ui): unify low-level controls and expand design system
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m57s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m57s
This commit is contained in:
525
src/app/design-system/page.tsx
Normal file
525
src/app/design-system/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user