Tiptap for Content Editing
The Content facet moves to a restricted Tiptap editor behind an authored-Markdown boundary; what the editor may and may not own
Recorded 2026-09-12 from the retired editor plan (decided 2026-08-06). The work itself is owned by Backlog tasks; this page holds the decisions those tasks build to.
In plain terms: the firm keeps its own house style for clauses and references, and hires a proven word-processing engine only for the typing, pasting, undo and selection mechanics. The engine never gets to decide what a clause or a reference is.
Current. Nothing is built. The hand-rolled editor
(src/shared/ui/atoms/editable-prose/ and the block card's editable content
row) is live; Tiptap is not in package.json. The authored-Markdown boundary
already exists in the domain package (block-content-authoring.ts).
Decisions
- Tiptap on ProseMirror, not the hand-rolled editor. Structured paste, atomic references, selection-aware controls, history, diff decorations, mobile behaviour and possible collaboration are editor-engine problems. Not raw ProseMirror (same foundation, more integration work), not BlockNote (its block-first model competes with the Folio model), not Lexical (gives up the ProseMirror path to the Convex sync component). CodeMirror stays a candidate for About/YAML only.
- One Tiptap document per content block. The app owns document, module and block structure, selection, ordering, permissions, versioning, proposals and the Control Panel. Tiptap introduces no document-level blocks, drag handles, slash commands or version model.
- Authored Markdown is the boundary. The editor reads and writes readable
authored references such as
@data.executor_name:upper. It never exposes compiled<@fld_...|hint>tokens and never makes ProseMirror JSON authoritative. Thereference-languagemodule remains the one parser, binder, serializer, projector and evaluator; the Tiptap reference extension is an adapter to it, not a second grammar. - Canonical semantic round-trip.
parse → serialize → parsepreserves text, paragraph, heading, emphasis and list structure, the blank lines that keep them distinct,---page breaks, and every authored reference's scope, path, resolution and casing modifiers. Insignificant formatting may normalise. Unsupported input is never silently saved through a lossy conversion: preserve it losslessly or refuse editing with a visible block notification and leave storage untouched. - Persistence behaviour stays as it is first. Stage one commits on blur through the existing content mutation and block edit envelope. No debounced writes merely because Tiptap emits transactions; autosave or collaboration needs its own later decision.
- Restricted schema derived from the documented vocabulary. No StarterKit. Only: document, paragraph, text; headings 1–6; bold and italic; the list structures present in the corpus and supported by export; a page-break node; a Legal Zebra reference node; history and placeholder. Nothing else lands before it is supported Content syntax in the docs and every serializer and exporter.
- References are a Legal Zebra node, not a stock mention. An inline atom
whose attributes follow the reference parse shape (separate resolution
and casing slots); shows resolved chrome while keeping authored data;
selects and deletes atomically; serialises to authored
@syntax; takes candidates from the compilation catalogue; refuses free-text targets; degrades visibly when a target no longer resolves. It waits for the@two-slot modifier shape so it never institutionalises the old singlemodifierfield. - No collaboration or ProseMirror storage. No prosemirror-sync, Hocuspocus, Yjs or Tiptap collaboration; live co-editing needs its own decision about step stores, authorisation, flush and migration.
Order of work
Before any editor code: extract every active content-block Markdown from the
compiled seed without changing output; classify every construct present;
build a pure parse/serialize harness with named fixtures; assert semantic
equivalence and exact authored-reference equivalence; assert unsupported
constructs produce an explicit non-writing result. Then a throwaway spike
outside the live block path answers, in writing: does the parser preserve the
round-trip contract; do dash lists and --- map without widening the schema;
can an inline atom serialise through the authored @ interface without a
second grammar; does Word paste reduce deterministically; does SSR and
hydration work in the TanStack Start route. A lossy supported construct stops
the work before persistence is connected. The spike is deleted when
production proceeds.
Chat rendering and composer convergence
Owner direction, 2026-09-20: use Tiptap for the later chat display and formatted composer as well. Chat and Content share an editor engine, not necessarily a schema: the restricted Content vocabulary above must not silently drop chat's GitHub-flavoured tables, task lists, strikethrough, links or code.
Current. Tiptap is absent. Read-only chat Markdown display is implemented
behind Shared UI's MarkdownText (content: string, optional className), using
react-markdown plus remark-gfm. Chat composers remain textareas, including the
reference-chip composer. Markdown remains the sent and persisted format.
Target. Prove a Tiptap display implementation behind the same boundary and a separate editing component for formatted input. Reuse supported formatting and presentation rules across sent messages and composer preview. Preserve existing messages, line breaks, safe links, incremental replies, and code/table overflow. Do not require an assistant-ui runtime or migrate chat persistence to editor JSON.
Gap / spike evidence. Extend the feasibility spike with chat-format fixtures and composer checks: typing, selection, undo/redo, paste, IME composition, reference-chip integrity, draft preservation, and current submit/newline keys. Prove the sent Markdown renders equivalently in the message list. Keep the six Content proof points and the corpus-first order; sequence production chat and composer integration separately after the relevant spike proofs pass. Unsupported constructs must remain lossless or visibly refuse editing without changing data.
Tinted Chrome, White Paper
Why the all-chrome kinds carry a tint while every row of a block — its data header included — stays white, why a content block has no header at all, and why Builder capability gates every header and card
Two Columns — Glyphs Print, Edges Do Not
Why the card keeps one glyph column for everything that prints as words and adds an edge column for screen chrome — cards, buttons, chips, the composer — whose contents take their own inset, and how this amends the 2026-08-09 print ruling