feat(ui): add BlockEditor component with block types, slash menu, and drag-and-drop

- add BlockEditor orchestrator with controlled block list and keyboard navigation
- add Block client component with contentEditable sync, drag handles, and markdown shortcuts
- add SlashMenu for inserting block types via `/` command
- add blockRegistry and block type definitions (paragraph, heading, bullet list, numbered list, quote, code, divider)
- add caret and id utility helpers
- export BlockEditor from shared components index
- add BlockEditor demo to admin devkit ComponentsPage
- add README documenting usage and architecture
This commit is contained in:
2026-04-25 17:37:23 -04:00
parent 0c99bf5002
commit 54386d3fe3
18 changed files with 1401 additions and 0 deletions
@@ -13,6 +13,7 @@ import {
TagInput,
StatCard,
Loading,
BlockEditor,
} from '@zen/core/shared/components';
import { UserCircle02Icon } from '@zen/core/shared/icons';
import AdminHeader from '../components/AdminHeader.js';
@@ -207,6 +208,31 @@ export default function ComponentsPage() {
<PreviewBlock title="Loading">
<Loading />
</PreviewBlock>
<PreviewBlock title="BlockEditor">
<BlockEditorDemo />
</PreviewBlock>
</div>
);
}
function BlockEditorDemo() {
const [blocks, setBlocks] = useState([
{ id: 'demo-1', type: 'heading_1', content: 'Bienvenue dans BlockEditor' },
{ id: 'demo-2', type: 'paragraph', content: "Tapez '/' pour ouvrir le menu de commandes." },
{ id: 'demo-3', type: 'bullet_item', content: 'Glissez la poignée ⋮⋮ pour réordonner' },
{ id: 'demo-4', type: 'bullet_item', content: 'Tapez `# ` au début pour un titre, `- ` pour une puce' },
{ id: 'demo-5', 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>
);
}