5a8d2ad02f
- migrate block content from plain strings to InlineNode[] structure - add inline toolbar (bold, italic, code, color, link) on text selection - add checklist block type with toggle support - add image block type (URL-based, phase 2) - add inline serialization helpers (inlineToDom, domToInline) - add inline types and length utilities - extend caret utils with range get/set support - update block registry and all existing block types for new content model - update demo blocks in ComponentsPage to use rich inline content - update README to reflect new architecture
248 lines
9.2 KiB
JavaScript
248 lines
9.2 KiB
JavaScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import {
|
|
Button,
|
|
Card,
|
|
Badge,
|
|
StatusBadge,
|
|
Input,
|
|
Select,
|
|
Textarea,
|
|
Switch,
|
|
TagInput,
|
|
StatCard,
|
|
Loading,
|
|
BlockEditor,
|
|
} from '@zen/core/shared/components';
|
|
import { UserCircle02Icon } from '@zen/core/shared/icons';
|
|
import AdminHeader from '../components/AdminHeader.js';
|
|
|
|
const ROLE_OPTIONS = [
|
|
{ value: 'admin', label: 'Admin', color: '#6366f1' },
|
|
{ value: 'editor', label: 'Éditeur', color: '#f59e0b' },
|
|
{ value: 'viewer', label: 'Lecteur', color: '#10b981' },
|
|
{ value: 'support', label: 'Support', color: '#3b82f6' },
|
|
{ value: 'billing', label: 'Facturation', color: '#ec4899' },
|
|
];
|
|
|
|
function TagInputDemo({ label, description, error, renderTag }) {
|
|
const [value, setValue] = useState([]);
|
|
return (
|
|
<TagInput
|
|
label={label}
|
|
description={description}
|
|
error={error}
|
|
placeholder="Ajouter un rôle..."
|
|
options={ROLE_OPTIONS}
|
|
value={value}
|
|
onChange={setValue}
|
|
renderTag={renderTag}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function PreviewBlock({ title, children }) {
|
|
return (
|
|
<div className="flex flex-col gap-3">
|
|
<h3 className="text-[11px] font-semibold uppercase tracking-widest text-neutral-500 dark:text-neutral-400">{title}</h3>
|
|
<div className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900/50 p-6 flex flex-wrap gap-3 items-center">
|
|
{children}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function ComponentsPage() {
|
|
return (
|
|
<div className="flex flex-col gap-8">
|
|
<AdminHeader title="Composants" description="Catalogue visuel des composants partagés" />
|
|
|
|
<PreviewBlock title="Button — variants">
|
|
<Button variant="primary" size="md">Primary</Button>
|
|
<Button variant="secondary" size="md">Secondary</Button>
|
|
<Button variant="success" size="md">Success</Button>
|
|
<Button variant="danger" size="md">Danger</Button>
|
|
<Button variant="warning" size="md">Warning</Button>
|
|
<Button variant="ghost" size="md">Ghost</Button>
|
|
<Button variant="fullghost" size="md">Full Ghost</Button>
|
|
</PreviewBlock>
|
|
|
|
<PreviewBlock title="Button — tailles">
|
|
<Button variant="primary" size="sm">Small</Button>
|
|
<Button variant="primary" size="md">Medium</Button>
|
|
<Button variant="primary" size="lg">Large</Button>
|
|
</PreviewBlock>
|
|
|
|
<PreviewBlock title="Button — états">
|
|
<Button variant="primary" disabled>Désactivé</Button>
|
|
<Button variant="primary" loading>Chargement</Button>
|
|
</PreviewBlock>
|
|
|
|
<PreviewBlock title="Badge — variants">
|
|
<Badge variant="default">Default</Badge>
|
|
<Badge variant="primary">Primary</Badge>
|
|
<Badge variant="success">Success</Badge>
|
|
<Badge variant="warning">Warning</Badge>
|
|
<Badge variant="danger">Danger</Badge>
|
|
<Badge variant="info">Info</Badge>
|
|
<Badge variant="purple">Purple</Badge>
|
|
<Badge variant="pink">Pink</Badge>
|
|
<Badge variant="orange">Orange</Badge>
|
|
</PreviewBlock>
|
|
|
|
<PreviewBlock title="StatusBadge">
|
|
<StatusBadge status="active" />
|
|
<StatusBadge status="inactive" />
|
|
<StatusBadge status="pending" />
|
|
<StatusBadge status="verified" />
|
|
<StatusBadge status="unverified" />
|
|
<StatusBadge status="admin" />
|
|
<StatusBadge status="user" />
|
|
</PreviewBlock>
|
|
|
|
<PreviewBlock title="Card — variants">
|
|
{['default', 'elevated', 'outline', 'success', 'info', 'warning', 'danger'].map(v => (
|
|
<Card key={v} variant={v} padding="md" className="min-w-[140px]">
|
|
<span className="text-sm font-medium text-black dark:text-white">{v}</span>
|
|
</Card>
|
|
))}
|
|
</PreviewBlock>
|
|
|
|
<PreviewBlock title="StatCard">
|
|
<StatCard
|
|
title="Utilisateurs"
|
|
value="1 234"
|
|
change="+42 ce mois"
|
|
changeType="increase"
|
|
icon={UserCircle02Icon}
|
|
color="text-blue-700"
|
|
bgColor="bg-blue-700/10"
|
|
className="w-56"
|
|
/>
|
|
<StatCard
|
|
title="Revenus"
|
|
value="8 400 $"
|
|
change="-120 ce mois"
|
|
changeType="decrease"
|
|
icon={UserCircle02Icon}
|
|
color="text-red-700"
|
|
bgColor="bg-red-700/10"
|
|
className="w-56"
|
|
/>
|
|
<StatCard
|
|
title="Chargement"
|
|
value="..."
|
|
icon={UserCircle02Icon}
|
|
color="text-neutral-400"
|
|
bgColor="bg-neutral-400/10"
|
|
loading
|
|
className="w-56"
|
|
/>
|
|
</PreviewBlock>
|
|
|
|
<PreviewBlock title="Input">
|
|
<div className="w-72 flex flex-col gap-3">
|
|
<Input label="Champ normal" placeholder="Valeur..." value="" onChange={() => {}} />
|
|
<Input label="Avec description" placeholder="Valeur..." value="" description="Texte d'aide sous le champ." onChange={() => {}} />
|
|
<Input label="Avec erreur" placeholder="Valeur..." value="" error="Ce champ est invalide." onChange={() => {}} />
|
|
<Input label="Désactivé" placeholder="Valeur..." value="Valeur fixe" disabled onChange={() => {}} />
|
|
<Input label="Requis" placeholder="Valeur..." value="" required onChange={() => {}} />
|
|
</div>
|
|
</PreviewBlock>
|
|
|
|
<PreviewBlock title="Select">
|
|
<div className="w-72 flex flex-col gap-3">
|
|
<Select
|
|
label="Sélection normale"
|
|
value="option1"
|
|
options={[{ value: 'option1', label: 'Option 1' }, { value: 'option2', label: 'Option 2' }]}
|
|
onChange={() => {}}
|
|
/>
|
|
<Select
|
|
label="Avec erreur"
|
|
value=""
|
|
options={[{ value: 'option1', label: 'Option 1' }]}
|
|
error="Veuillez choisir une option."
|
|
onChange={() => {}}
|
|
/>
|
|
<Select
|
|
label="Désactivé"
|
|
value="option1"
|
|
options={[{ value: 'option1', label: 'Option 1' }]}
|
|
disabled
|
|
onChange={() => {}}
|
|
/>
|
|
</div>
|
|
</PreviewBlock>
|
|
|
|
<PreviewBlock title="Textarea">
|
|
<div className="w-72 flex flex-col gap-3">
|
|
<Textarea label="Zone de texte" placeholder="Entrer du texte..." value="" rows={3} onChange={() => {}} />
|
|
<Textarea label="Avec erreur" placeholder="..." value="" error="Ce champ est requis." rows={2} onChange={() => {}} />
|
|
<Textarea label="Désactivé" value="Texte fixe" disabled rows={2} onChange={() => {}} />
|
|
</div>
|
|
</PreviewBlock>
|
|
|
|
<PreviewBlock title="Switch">
|
|
<div className="w-72 flex flex-col gap-1 border border-neutral-200 dark:border-neutral-800 rounded-xl overflow-hidden">
|
|
<Switch label="Désactivé" description="Ce switch est off" checked={false} onChange={() => {}} />
|
|
<Switch label="Activé" description="Ce switch est on" checked={true} onChange={() => {}} />
|
|
<Switch label="Désactivé (disabled)" checked={false} disabled onChange={() => {}} />
|
|
</div>
|
|
</PreviewBlock>
|
|
|
|
<PreviewBlock title="TagInput">
|
|
<div className="w-72 flex flex-col gap-3">
|
|
<TagInputDemo label="Rôles" description="Sélectionnez un ou plusieurs rôles." />
|
|
<TagInputDemo
|
|
label="Avec pastilles colorées"
|
|
renderTag={(opt, onRemove) => (
|
|
<Badge key={opt.value} color={opt.color} dot onRemove={onRemove}>{opt.label}</Badge>
|
|
)}
|
|
/>
|
|
<TagInputDemo label="Avec erreur" error="Ce champ est requis." />
|
|
</div>
|
|
</PreviewBlock>
|
|
|
|
<PreviewBlock title="Loading">
|
|
<Loading />
|
|
</PreviewBlock>
|
|
|
|
<PreviewBlock title="BlockEditor">
|
|
<BlockEditorDemo />
|
|
</PreviewBlock>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function BlockEditorDemo() {
|
|
const [blocks, setBlocks] = useState([
|
|
{ id: 'demo-1', type: 'heading_1', content: [{ type: 'text', text: 'Bienvenue dans BlockEditor' }] },
|
|
{ id: 'demo-2', type: 'paragraph', content: [
|
|
{ type: 'text', text: "Tapez " },
|
|
{ type: 'text', text: "'/'", marks: [{ type: 'code' }] },
|
|
{ type: 'text', text: ' pour ouvrir le menu, ou ' },
|
|
{ type: 'text', text: 'sélectionnez', marks: [{ type: 'bold' }] },
|
|
{ type: 'text', text: ' pour ' },
|
|
{ type: 'text', text: 'mettre en forme', marks: [{ type: 'italic' }, { type: 'color', color: 'blue' }] },
|
|
{ type: 'text', text: '.' },
|
|
] },
|
|
{ id: 'demo-3', type: 'checklist', checked: true, content: [{ type: 'text', text: 'Format inline (gras, italique, couleur, lien)' }] },
|
|
{ id: 'demo-4', type: 'checklist', checked: false, content: [{ type: 'text', text: 'Bloc image (URL uniquement en Phase 2)' }] },
|
|
{ id: 'demo-5', type: 'bullet_item', content: [{ type: 'text', text: 'Glissez la poignée ⋮⋮ pour réordonner' }] },
|
|
{ id: 'demo-6', type: 'paragraph', content: [] },
|
|
]);
|
|
return (
|
|
<div className="w-full flex flex-col gap-4">
|
|
<BlockEditor value={blocks} onChange={setBlocks} />
|
|
<details className="text-xs text-neutral-500 dark:text-neutral-400">
|
|
<summary className="cursor-pointer select-none">Aperçu JSON</summary>
|
|
<pre className="mt-2 p-3 rounded-lg bg-neutral-100 dark:bg-neutral-800/60 overflow-x-auto text-[11px] leading-relaxed">
|
|
{JSON.stringify(blocks, null, 2)}
|
|
</pre>
|
|
</details>
|
|
</div>
|
|
);
|
|
}
|