Goldie Blocks

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

  1. 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.
  2. 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.
  3. 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. The reference-language module remains the one parser, binder, serializer, projector and evaluator; the Tiptap reference extension is an adapter to it, not a second grammar.
  4. Canonical semantic round-trip. parse → serialize → parse preserves 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.
  5. 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.
  6. 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.
  7. 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 single modifier field.
  8. 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.

On this page