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:
@@ -0,0 +1,114 @@
|
||||
# BlockEditor
|
||||
|
||||
Éditeur WYSIWYG par blocs, style Notion. Construit en interne (pas de
|
||||
ProseMirror/Lexical/Tiptap). Un `contentEditable` par bloc, pas un seul
|
||||
contentEditable global — c'est plus robuste et plus simple à étendre.
|
||||
|
||||
## Utilisation
|
||||
|
||||
```jsx
|
||||
import { BlockEditor } from '@zen/core/shared/components';
|
||||
|
||||
const [blocks, setBlocks] = useState([]);
|
||||
<BlockEditor value={blocks} onChange={setBlocks} label="Contenu" />
|
||||
```
|
||||
|
||||
`value` est un **tableau de blocs JSON**. C'est la source de vérité.
|
||||
`MarkdownEditor` reste disponible en parallèle pour les usages markdown.
|
||||
|
||||
## Format des blocs (Phase 1)
|
||||
|
||||
Chaque bloc a un `id` (UUID) et un `type`. Selon le type :
|
||||
|
||||
| type | champs | description |
|
||||
|----------------|---------------|----------------------------|
|
||||
| `paragraph` | `content` | texte brut |
|
||||
| `heading_1..6` | `content` | titre niveau 1 à 6 |
|
||||
| `bullet_item` | `content` | élément de liste à puces |
|
||||
| `numbered_item`| `content` | élément de liste numérotée |
|
||||
| `quote` | `content` | citation |
|
||||
| `code` | `content` | bloc de code (monospace) |
|
||||
| `divider` | — | séparateur horizontal |
|
||||
|
||||
Phase 2 ajoutera : `checklist`, `image`, et le format de `content` passera de
|
||||
`string` à `InlineNode[]` pour supporter le formatting inline (gras, italique,
|
||||
couleur, lien). Phase 3 : `table`.
|
||||
|
||||
## Props
|
||||
|
||||
```jsx
|
||||
<BlockEditor
|
||||
value={blocks} // Block[]
|
||||
onChange={setBlocks} // (Block[]) => void
|
||||
label, error, placeholder, disabled, className
|
||||
enabledBlocks={[...]} // optionnel : restreindre les types disponibles
|
||||
/>
|
||||
```
|
||||
|
||||
## Interactions clavier
|
||||
|
||||
- `/` → ouvre le menu de commandes (filtrable au clavier, ↑ ↓ Entrée pour valider, Échap pour fermer)
|
||||
- `# ` → titre 1, `## ` → 2, …, `###### ` → 6
|
||||
- `- ` → liste à puces
|
||||
- `1. ` → liste numérotée
|
||||
- `> ` → citation
|
||||
- ` ``` ` → bloc de code
|
||||
- `---` → séparateur
|
||||
- `Backspace` au début d'un bloc typé → repasse en paragraphe ; au début d'un paragraphe, fusionne avec le bloc précédent
|
||||
- `Entrée` sur un item de liste vide → sort de la liste
|
||||
- `Ctrl/Cmd + Z` / `Ctrl/Cmd + Shift + Z` → undo / redo
|
||||
|
||||
## Drag and drop
|
||||
|
||||
Chaque bloc affiche au survol :
|
||||
- une poignée `+` pour insérer un bloc en dessous (ouvre le slash menu)
|
||||
- une poignée `⋮⋮` pour glisser-déposer (réordonner)
|
||||
|
||||
Le drag-and-drop utilise l'API HTML5 native, pas de dépendance externe.
|
||||
|
||||
## Étendre — enregistrer un bloc custom
|
||||
|
||||
```js
|
||||
import { registerBlock, newBlockId } from '@zen/core/shared/components/BlockEditor';
|
||||
|
||||
registerBlock({
|
||||
type: 'kpi',
|
||||
label: 'KPI',
|
||||
icon: '📊',
|
||||
keywords: ['kpi', 'metric', 'stat'],
|
||||
isText: false,
|
||||
create: () => ({ id: newBlockId(), type: 'kpi', value: 0 }),
|
||||
Component: ({ block, onChange, disabled }) => (
|
||||
<input
|
||||
type="number"
|
||||
value={block.value}
|
||||
disabled={disabled}
|
||||
onChange={(e) => onChange({ value: Number(e.target.value) })}
|
||||
/>
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
Le nouveau type apparaît automatiquement dans le slash menu de tout
|
||||
`<BlockEditor />` rendu après l'enregistrement. Pour les blocs **texte**, on
|
||||
fournit à la place `isText: true`, `textTag`, `textClassName`, et optionnellement
|
||||
`renderPrefix({ block, index, numberedIndex })` pour un préfixe (puce, numéro).
|
||||
|
||||
## Architecture interne
|
||||
|
||||
```
|
||||
BlockEditor.client.js orchestrateur : value/onChange, undo, slash menu, drag-drop
|
||||
Block.client.js wrapper d'un bloc : handles, contentEditable, paste sanitize
|
||||
SlashMenu.client.js menu flottant filtrable
|
||||
blockRegistry.js map type → définition, API publique d'extension
|
||||
blockTypes/ un fichier par type built-in
|
||||
utils/ids.js UUID pour les blocs
|
||||
utils/caret.js gestion du caret dans un contentEditable
|
||||
```
|
||||
|
||||
## Limitations connues (Phase 1)
|
||||
|
||||
- Inline formatting (gras, italique, couleur, lien) **pas encore** : tout est texte brut. Phase 2.
|
||||
- Pas d'imbrication de listes.
|
||||
- Paste : seul le texte brut est conservé (sanitize HTML).
|
||||
- Tables : Phase 3.
|
||||
Reference in New Issue
Block a user