Files
core/src/features/admin/devkit/ComponentsPage.client.js
T
hykocx 5a8d2ad02f feat(BlockEditor): add inline formatting with rich content model
- 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
2026-04-25 18:27:20 -04:00

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>
);
}