Contribute rewrite Phase 2: split-pane preview with mermaid

Add a rendered preview pane and split the Contribute-mode center
column. Discuss mode is now a single rendered preview (no more
read-only Tiptap mount); Contribute mode renders the CM6 raw editor
on the left and a live-updating preview on the right at 50/50, with
the existing chat + change-card panels untouched on the right.

The new MarkdownPreview component renders markdown via marked and
lazy-loads mermaid on the first encounter of a ```mermaid fence in
any rendered doc. Mermaid (~200 KB gzipped, plus dagre/graphlib
subchunks) stays out of the main bundle and is fetched only when a
diagram actually appears in the rendered content. The marked
renderer is scoped via a per-component Marked instance so the
mermaid-fence behavior does not leak to Editor.jsx / DiffView.

XSS hardening on mermaid: securityLevel: 'strict' is set explicitly
in mermaid.initialize. Verified end-to-end via a sandbox eval — a
hostile `<script>window.X=true</script>` payload inside a mermaid
fence is neutralized (no script tags in the rendered SVG, no global
side-effect, diagram still renders with the offending node label
empty).

The §8.12 selection tooltip is now sourced from window.getSelection()
inside the preview surface rather than Tiptap PM positions. The
SelectionTooltip's existing `{text, coords}` contract is unchanged;
coords come from the selection range's bounding rect. Anchor payloads
for flag threads now carry just the quote text — the PM-position
from/to fields that the Tiptap-era code attached are dropped (they
were never meaningful as durable anchors anyway; quote is what §8.12
and §8.13 specify).

Design decisions confirmed before coding (all defaults):
- PromptBar spans both panes at the bottom of the center column; it
  operates on the document, not on either pane individually.
- Start-Contributing is a hard cut — no transition animation. Phase
  6's chat-drawer collapse will redo the layout machinery anyway.
- Mermaid lazy-loads on first ```mermaid fence detected, not on
  Contribute-mode entry. Keeps the gzipped cost off any doc that has
  no diagrams.
- Below 1280px viewport, a narrow-viewport banner surfaces in
  Contribute mode. The drawer collapse that fixes this properly is
  Phase 6.

Mermaid integration is kept loose for the future authoring tool: the
placeholder DOM node carries the source as a data attribute, the
renderer takes (source) → SVG via mermaid.render, and per-block
memoization keys on source. A future authoring pane can intercept
the placeholder before SVG render without restructuring.

SPEC §8.3 updated to reflect the split-pane Contribute layout — the
smallest edit that captures the new shape.

Verification notes:
- Vite preview on :5180 — sandbox mount of MarkdownPreview confirms
  rendering, mermaid SVG output, securityLevel:'strict' XSS
  neutralization, and the window.getSelection → {text, coords}
  bridge end to end.
- Network log confirms mermaid + sub-chunks are absent from initial
  load and fetched only after first ```mermaid encounter.
- Backend was not running this session, so the manual-debounce
  save-now / accept / decline / live-preview-mirror pathways were
  not driven against a real branch; they remain verified by
  inspection. Phase 1's verification gap therefore carries forward.
- 125 backend integration tests still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-05-25 10:24:19 -07:00
parent 5dbcac8906
commit 7c3b8fc133
6 changed files with 1426 additions and 42 deletions
+88
View File
@@ -462,6 +462,94 @@
display: flex; flex-direction: column;
overflow: hidden;
}
/* ── Split-pane Contribute layout (Phase 2) ──────────────────────────── */
.editor-split {
flex: 1; min-height: 0;
display: flex; flex-direction: row;
overflow: hidden;
}
.editor-split-pane {
flex: 1 1 50%; min-width: 0; min-height: 0;
display: flex; flex-direction: column;
overflow: hidden;
}
.editor-split-raw { border-right: 1px solid #e5e5e5; }
.editor-split-preview { background: #fff; overflow-y: auto; }
.editor-split-preview .markdown-preview { padding: 24px 32px; }
/* ── Markdown preview (Phase 2 render pane) ──────────────────────────── */
.markdown-preview {
font-size: 15px; line-height: 1.75; color: #1a1a1a;
max-width: 720px; margin: 0 auto;
outline: none;
}
.markdown-preview-solo {
flex: 1; overflow-y: auto;
padding: 32px 48px;
}
.markdown-preview h1 { font-size: 22px; font-weight: 700; margin: 24px 0 12px; }
.markdown-preview h2 { font-size: 17px; font-weight: 600; margin: 20px 0 8px; }
.markdown-preview h3 { font-size: 15px; font-weight: 600; margin: 16px 0 6px; }
.markdown-preview p { margin: 0 0 12px; }
.markdown-preview ul, .markdown-preview ol { padding-left: 24px; }
.markdown-preview code {
background: #f0f0ee; padding: 1px 5px; border-radius: 3px; font-size: 13px;
}
.markdown-preview pre {
background: #f6f6f4; padding: 12px 16px; border-radius: 6px;
overflow-x: auto; font-size: 13px; line-height: 1.5;
}
.markdown-preview pre code { background: none; padding: 0; }
.markdown-preview blockquote {
margin: 0 0 12px; padding: 4px 14px;
border-left: 3px solid #d4d4d4; color: #555;
}
.markdown-preview table { border-collapse: collapse; margin: 0 0 12px; }
.markdown-preview th, .markdown-preview td {
border: 1px solid #e5e5e5; padding: 6px 10px;
}
.markdown-preview th { background: #fafafa; font-weight: 600; }
/* Mermaid placeholder shown until the lazy-loaded module resolves.
The .mermaid-block container is preserved so future authoring tools
can decorate it; SVG output replaces only the inner contents. */
.markdown-preview .mermaid-block {
margin: 12px 0;
text-align: center;
}
.markdown-preview .mermaid-block svg { max-width: 100%; height: auto; }
.markdown-preview .mermaid-placeholder {
background: #f6f6f4; color: #777;
padding: 12px 16px; border-radius: 6px;
font-size: 12px; line-height: 1.5;
text-align: left; white-space: pre-wrap;
}
.markdown-preview .mermaid-error {
background: #fee2e2; color: #991b1b;
padding: 10px 14px; border-radius: 6px;
font-size: 12px; line-height: 1.5;
text-align: left; white-space: pre-wrap;
}
/* Browser-native selection styling inside the preview — replaces the
PM selection-highlight plugin that lived in Tiptap's surface. The
SelectionTooltip's onMouseDown preventDefault keeps the selection
visible while the tooltip captures input. */
.markdown-preview ::selection {
background: rgba(99, 102, 241, 0.22);
}
/* Narrow-viewport banner — Phase 2 punts the chat-drawer collapse to
Phase 6, so the split layout asks for ≥1280px. */
.narrow-viewport-banner {
padding: 8px 16px;
background: #fef3c7; border-bottom: 1px solid #fcd34d;
color: #78350f; font-size: 12px;
}
@media (min-width: 1280px) {
.narrow-viewport-banner { display: none; }
}
.cm-source-editor .cm-editor {
flex: 1; min-height: 0;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;