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
@@ -0,0 +1,47 @@
// Registre extensible des types de blocs.
// Les blocs built-in s'enregistrent dans defaultBlocks.js (chargé par index.js).
// Les consommateurs peuvent appeler `registerBlock` pour ajouter leurs propres types.
//
// Forme d'une définition de bloc :
// {
// type: string, // id unique (ex: 'paragraph', 'my_custom')
// label: string, // libellé affiché dans le slash menu
// icon: string, // glyphe court (emoji ou caractère)
// keywords: string[], // termes de recherche pour le slash menu
// shortcut?: string, // préfixe markdown qui convertit (ex: '# ', '- ')
// shortcutTransform?: (block, match) => block, // optionnel : transforme un bloc existant
// create: (init?) => Block, // construit un nouveau bloc
// isText: boolean, // true si le bloc a un contentEditable de texte
// textTag?: string, // pour info / rendu en mode display
// textClassName?: string, // classes appliquées au contentEditable
// placeholder?: string, // texte fantôme quand le bloc est vide et focus
// renderPrefix?: (ctx) => ReactNode, // pour les listes (puce, numéro)
// Component?: ReactComponent, // pour blocs non-texte ; reçoit { block, onChange, disabled }
// }
const registry = new Map();
export function registerBlock(def) {
if (!def || typeof def.type !== 'string') {
throw new Error('registerBlock: `type` is required');
}
if (typeof def.create !== 'function') {
throw new Error(`registerBlock(${def.type}): \`create\` is required`);
}
registry.set(def.type, def);
}
export function getBlockDef(type) {
return registry.get(type) || null;
}
export function listBlocks() {
return Array.from(registry.values());
}
export function isBlockText(type) {
const def = registry.get(type);
return Boolean(def?.isText);
}
export const DEFAULT_BLOCK_TYPE = 'paragraph';