Block editor (BlockNote)
Notion-style block editing lives alongside the older rich-text (Tiptap) editor. This is the internals reference ; the design record has the rationale, and each consuming module's README/doc has the as-built detail.
Where it lives
@monark/common/blocks(blocks.ts) ; the dependency-free, server-safe substrate.blocksToText(blocks)flattens a block document to plain text (title-derivation, search, cell previews, event diffs) ;checklistProgress(blocks)countscheckListItemdone/total (the kanban card bar) ;textToBlocks(text)builds a minimal paragraph-per-line document (so a non-editor caller : e.g. a wiki automation node ; can write a body) ;DocumentBlockis a loose structural subset of BlockNote'sBlockso nothing in the core depends on the web editor. Import it from the/blockssubpath, never the@monark/commonbarrel : the barrel pulls the pino logger, whose module-init readsprocess.stdout.isTTYand crashes in the browser.BlockEditor/BlockView(block-editor.tsx) ; the web components. Client-only (dynamic(() => …, { ssr: false })) with a layout-accurate skeleton, theme-aware (light/dark vianext-themes). Content is a BlockNoteBlock[].
Storage
Block content persists as a JSON block array, not HTML ; lossless and free of the
dangerouslySetInnerHTML XSS surface. Consumers that filter/search/derive titles keep
a plain-text projection column (WikiPage.contentText, KanbanCard.descriptionText)
in sync on write via blocksToText, so those paths stay cheap text contains rather
than walking jsonb. The Data Models DOCUMENT value lives in DataRecord.data and is
non-filterable (its title/index use blocksToText).
The editor is create-once / uncontrolled ; and why that matters on mobile
useCreateBlockNote is called with a memoized options object (empty deps) so the
editor is built exactly once from the value present at mount ; onChange streams the
document out, and we never feed value back in. To load a different document, the
element is keyed on the document id so it remounts. This is not just tidiness:
- Re-creating the editor mid-render tears down the ProseMirror view and drops the in-flight keystroke.
- On mobile, input goes through IME composition ; a React re-render mid-composition
aborts it. A per-keystroke
setState(the naïveonChange={setBlocks}) let plain characters through but swallowed Enter. The fix is to keep edits in a ref and autosave on a timer (wiki) or flip a one-shot dirty flag (kanban card form), so the editor's React subtree never re-renders while you type.
Three environment gotchas (all fixed, worth remembering)
process.stdout.isTTYcrash : a browser-reachable contracts file imported the@monark/commonbarrel, dragging the server pino logger into the client bundle. Fixed by the dependency-free@monark/common/blockssubpath. Rule: client code imports common via a subpath, never the barrel.useEffectEvent is not a function: BlockNote's default Mantine UI does a bareimport { useEffectEvent } from "react", which Next 15.5's vendored React (used for app-client) doesn't export. Fixed by using the Ariakit renderer (@blocknote/ariakit), which has no such import. (Next 16 would also fix it but is a larger upgrade.)lib0process.stdout.isTTY:lib0(pulled by@blocknote/core) reads it unguarded at module-init. Apnpm patch(patches/lib0@1.0.0-rc.22.patch) guards it (process.stdout && process.stdout.isTTY).
Styling
.block-editor overrides --bn-colors-editor-background to transparent so the editor
blends into the page ; the default 54px side gutters (which hold the drag handle + "+"
controls) are trimmed to 36px under 768px so the text column isn't squeezed on phones.
A minHeight prop sets --bn-min-h on the editable so an empty editor still has a
usable click target. See globals.css.
Consumers
- Data Models
DOCUMENTfield type : data-models README. - Wiki page body : wiki.md.
- Kanban card description (+ its checklist) : kanban.md.
- Rich text stays for compact/inline surfaces (the calendar event description, and
the
richTextData Models field type).