Rich-text (tiptap) block editor for q2-preview — Phase 1a (experiment, opt-in)#335
Open
cscheid wants to merge 9 commits into
Open
Rich-text (tiptap) block editor for q2-preview — Phase 1a (experiment, opt-in)#335cscheid wants to merge 9 commits into
cscheid wants to merge 9 commits into
Conversation
Throwaway Phase-0 spike proving qmd can round-trip through a ProseMirror document for a future rich-text block editor in q2-preview. Approach: keep all existing detection + commit machinery; replace only the EditTextarea UI. Seed the PM doc from the untransformed Pandoc AST (not markdown-it), lift opaque constructs (shortcodes, math, @CrossRef, [@cite], raw inline) into verbatim "chip" nodes, serialize PM -> markdown via prosemirror-markdown. Oracle: native pampa (-t json --json-source-location full), no WASM init. Result: 15/16 exact, 1 benign reformat (blockquote softwrap), 0 broken. Stock prosemirror-markdown serializer needed zero per-node overrides beyond the chip rule. tsc clean; full preview-renderer suite (473 tests) passes. Spike lives at ts-packages/preview-renderer/src/q2-preview/tiptap-roundtrip-spike/ (throwaway; quarantine/delete before non-experimental merge). Adds dev deps prosemirror-model + prosemirror-markdown. Plan + verdict: claude-notes/plans/2026-06-23-tiptap-rich-text-block-editor.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AST->ProseMirror->markdown bridge for the rich-text block editor, reusing the Phase-0 spike's validated approach but as production modules keyed to tiptap's node/mark names (so one bridge + serializer serve both the test schema and the live tiptap editor). - richtext/schema.ts: PM schema (tiptap-named) + atomic `chip` node - richtext/astToProseMirror.ts: astToDoc — AST subtree -> PM doc, opaque constructs (math/cite/shortcode/raw) -> verbatim chips; list tightness from AST - richtext/serializer.ts: docToMarkdown — prosemirror-markdown rules re-keyed to tiptap names; qmd-aware italic->`_` (avoids disallowed `***`) - test-utils/pampaOracle.ts: shared native-pampa oracle; inline marks compared as a flat SET (ProseMirror model) so `[**x**](u)` == `**[x](u)**`; skips when the native binary can't be built - richtext/roundtrip.test.ts: 13 fixtures, all pass; gate is SEMANTIC equivalence (no dropped/changed nodes), byte-exactness informational Adds tiptap (core/pm/react/starter-kit) + prosemirror-markdown/model as preview-renderer deps. tsc clean; round-trip suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bd-sjb4pzx8) Wires the opt-in WYSIWYG tiptap editor into the q2-preview block-edit path. - richtext/RichTextEditor.tsx: tiptap editor seeded from the block's AST subtree (astToDoc -> JSON), committing markdown via the UNCHANGED commitTextEdit path; dirtiness from doc.eq(initialDoc) (C3, true no-op on unedited close); stale- target + focus-restore guards; Esc/Mod-Enter/blur; paragraph-only (Enter swallowed, no structural splits in 1a) - richtext/chipExtension.ts: tiptap Chip node (inline atom, verbatim pill) - richtext/styles.ts: one-time CSS — strip ProseMirror chrome, zero inner-block margin (measured box owns spacing), subtle chip pills; theme styles the rest - dispatchers.tsx: Block renders RichTextEditor (vs EditTextarea) for Para when ctx.richText; same measured box - richText flag plumbed like unlockNestingCursor: PreviewContext -> PreviewRoot -> entry -> Q2PreviewIframe (UPDATE_AST payload) -> SPA ?richText=1 tsc clean (preview-renderer + q2-preview-spa); full preview-renderer suite passes (486 tests). Browser verification next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Browser verification via q2 preview --allow-edit + ?richText=1: clicking a paragraph opens a tiptap editor visually identical to the rendered block (marks styled by the theme via the same-iframe CSS cascade); a real inline-bold edit committed through the unchanged commitTextEdit path and wrote clean qmd to disk with the rest of the paragraph round-tripped byte-clean. Adds Phase 1a evidence screenshots (rendered + editing) and marks Phase 1a complete in the plan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The WYSIWYG render is faithful enough that users couldn't tell editing was live. Give the active rich-text editor a subtle blue background tint + ring so "edit mode" is obvious. Padding is offset by an equal negative margin so the text does not shift (zero reflow); marks stay theme-styled. Verified in q2 preview. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rce (bd-sjb4pzx8)
Two UX refinements + a source-mapping fix:
- "Editing…" affordance: a faint italic label parked in the LEFT MARGIN of the
active editor (pointer-events:none, user-select:none) so it signals edit mode
without hijacking text clicking/selecting. First of the left-margin affordances.
- Shortcode chip source: prefer a node's own `.l` literal location over the
compact pool entry when slicing chip text. The pool range for a shortcode Span
is mis-assigned (points at an adjacent space → empty chip); `.l` points at the
actual token, so the chip now renders the verbatim `{{< meta key >}}` monospace.
Leaf inlines (math/cite) were already correct via the pool; this fixes
container inlines. Round-trip suite still green (native pampa carries `.l` too).
Verified in q2 preview: Editing label + math/cite/shortcode chips all render.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In-place escape hatch to the textarea, parked in the left margin under "Editing…"
(absolute, off the text, so it never hijacks clicking/selecting).
- EditAffordance.tsx: shared left-margin affordance (label + rich/plain toggle),
rendered by renderMeasuredEdit so it shows for BOTH surfaces. Toggle uses
mousedown-preventDefault to keep editor focus.
- editorMode ('rich'|'plain') in PreviewRoot, session-sticky, default rich; the
dispatcher renders RichTextEditor vs EditTextarea accordingly.
- editorModeSwitchRef guard: a surface swap fires the outgoing editor's
unmount-blur, which must NOT commit/close the session — both blur handlers
(rich + textarea) check the ref. Without it, toggling closed the editor.
- rich->plain content handoff via editDraftRef (RichTextEditor.onUpdate keeps the
shared markdown draft current, dirty-aware so an untouched toggle never
reformats). plain->rich re-seeds from the original AST (in-iframe can't parse
edited markdown — Phase 2).
Verified in q2 preview: toggle rich<->plain keeps the session open, swaps
surfaces, preserves content. tsc clean; preview-renderer suite 486 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Header added to the rich-text supported types; heading node enabled in the
tiptap editor. The AST->PM bridge already mapped Header -> heading{level}, so
the round-trip was ready (added a heading-with-marks fixture; 14/14 green).
- enableInputRules/enablePasteRules false: 1b edits existing structure only —
typing "## " must not convert a paragraph or change a heading level (structural
edits are a later phase; Cmd-B/I marks still work).
- trailingNode false: a single-heading doc was getting a phantom empty trailing
paragraph (extra editor height + a stray blank block on commit). Fixed; the
heading edit box is now tight (verified in q2 preview).
tsc clean; preview-renderer suite 487 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ks (bd-sjb4pzx8)
A small toolbar floats above the top-left of the rich-text edit box:
- Mark buttons (bold/italic/strike/subscript/superscript) call toggleMark over the
selection (same command as Cmd-B/I), highlight via isActive, and use
mousedown-preventDefault so clicking never collapses the selection. Verified
end-to-end: select word + Bold -> **word** on disk.
- Subscript/superscript are now real marks (not chips): added
@tiptap/extension-{sub,super}script, schema marks, serializer (~x~ / ^x^), AST
mapping (Pandoc Subscript/Superscript -> marks). Round-trip fixture green (15/15).
- Link button opens a URL input; setLink / extendMarkRange('link') / unsetLink
(edit/remove an existing link by placing the cursor inside it). Commit was
rescoped to focusout from the whole edit box so focusing the link input doesn't
close the session.
KNOWN ISSUE (bd-3zp3z4jx, downstream): a NEW link's URL is corrupted on write-back
when the paragraph already has another link (gets the adjacent link's URL). The
rich editor commits CORRECT markdown (verified); the corruption is in the shared
text-channel write-back (apply_node_edit / incremental writer), so it affects the
textarea editor's link edits too. Single-link / no-other-link edits are fine.
tsc clean; preview-renderer suite 488 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Experimental, opt-in WYSIWYG block editor for
q2 preview/ quarto-hub, built on tiptap/ProseMirror. Strand bd-sjb4pzx8. Plan:claude-notes/plans/2026-06-23-tiptap-rich-text-block-editor.md.This is a feasibility experiment on a branch — it does not change default behavior. The rich editor only activates behind
?richText=1(+q2 preview --allow-edit). With the flag off, everything is byte-identical to today's monospaced textarea.What it does
Clicking an editable paragraph opens a tiptap rich-text editor in place of the textarea, inside the same measured box and committing through the unchanged
commitTextEdit→parse_qmd_content→ splice → write-back path. Because the editor lives in the preview iframe (which already has the Bootstrap + theme CSS), its semantic tags (<p>/<em>/<strong>/<a>) are styled by the theme automatically — so editing looks like the rendered page.@crossref,[@cite], raw inline) become verbatim "chip" atoms.doc.eq(untouched open/close is a true no-op).Scope (Phase 1a)
Tests / verification
richtext/roundtrip.test.ts, 13 fixtures) — gate is semantic AST equivalence (no dropped/changed nodes); cosmetic reformatting allowed. Skips gracefully when nativepampacan't be built.tscclean across preview-renderer, q2-preview-spa, hub-client (tsc -b+ vite + sandboxed iframe build).q2 preview --allow-editwith chrome-devtools (screenshots inclaude-notes/richtext-shots/), including a faithful real edit writing clean qmd back to disk.Notes for review
preview-rendererdeps.ts-packages/preview-renderer/src/q2-preview/tiptap-roundtrip-spike/is a throwaway Phase-0 spike (marked as such); safe to delete before any non-experimental merge.🤖 Generated with Claude Code