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 @@
// Helpers de gestion du caret pour les contentEditable mono-bloc.
// On reste sur du texte brut au MVP : un seul Text node enfant (ou aucun).
export function getCaretOffset(el) {
const sel = typeof window !== 'undefined' ? window.getSelection() : null;
if (!sel || sel.rangeCount === 0) return 0;
const range = sel.getRangeAt(0);
if (!el.contains(range.startContainer)) return 0;
const pre = range.cloneRange();
pre.selectNodeContents(el);
pre.setEnd(range.startContainer, range.startOffset);
return pre.toString().length;
}
export function setCaretOffset(el, offset) {
if (!el) return;
el.focus();
const sel = window.getSelection();
if (!sel) return;
const range = document.createRange();
const text = el.firstChild;
if (!text) {
range.setStart(el, 0);
range.collapse(true);
} else {
const max = text.textContent?.length ?? 0;
const pos = Math.max(0, Math.min(offset, max));
range.setStart(text, pos);
range.collapse(true);
}
sel.removeAllRanges();
sel.addRange(range);
}
export function focusEnd(el) {
if (!el) return;
const len = (el.textContent ?? '').length;
setCaretOffset(el, len);
}
export function isCaretAtStart(el) {
return getCaretOffset(el) === 0;
}
export function isCaretAtEnd(el) {
return getCaretOffset(el) === (el.textContent ?? '').length;
}
@@ -0,0 +1,8 @@
// Génère un ID stable pour un bloc. randomUUID est dispo dans tous les navigateurs
// modernes côté client ; en SSR on retombe sur un fallback simple.
export function newBlockId() {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
return `b_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
}