2.3.10 WYSIWYG Markdown editing — edit the rendered view inline, drop the split source/preview (Yue feedback · finding #53)
Estimate: 18m · Depends on: 2.3.5, 2.3.7
Yue feedback (finding #53): the description editor is a side-by-side
source + preview split — you type raw Markdown (**bold**) on
the left and watch a rendered pane on the right. That's wrong for the product: the user
should edit the rendered view directly and see formatting appear inline as
they type (the Linear / Notion / GitHub-comment experience), with NO raw-syntax pane.
This supersedes 2.3.5's "source editor, not WYSIWYG" decision. 2.3.5
chose @uiw/react-md-editor and explicitly rejected WYSIWYG on the grounds that
rich editors store ProseMirror JSON and serialize-to-Markdown lossily — which would break
Story 1.4's hard invariant (source of truth = Markdown text in
descriptionMd). The resolution that satisfies BOTH the UX ask and the
invariant is a remark-native WYSIWYG editor — one whose canonical document
model IS Markdown (parsed/serialized by remark), so editing the rendered view round-trips
to Markdown losslessly. Recommended: Milkdown (ProseMirror +
remark, purpose-built "WYSIWYG markdown" — Markdown is the document model, not a lossy
export); acceptable alternative: Tiptap + tiptap-markdown (more
popular, but Markdown is a serialization layer over PM JSON — accept ONLY if a round-trip
test proves no drift on our content set). The executor confirms the library at build time
against the round-trip-fidelity criterion; the storage shape does not change.
What stays fixed (the seams 2.3.5/2.3.6 already established):
- Storage: still Markdown in
descriptionMd— no schema change, no new column, no JSON blob. The editor'sonChangestill emits a Markdown string. - Public surface: the
MarkdownEditorprop contract is UNCHANGED —value/onChange/label/size: 'min' | 'full'/onImageUpload?/readOnly?— so the two consumers (2.3.3 create modal, 2.3.6 edit form) need ZERO changes. This Subtask swaps the editor INTERNALS, not the interface. - Read path:
MarkdownView/renderMarkdown(the ONE remark+rehype-sanitize+highlight module) stays the display surface everywhere else (detail page 2.4, list previews). The editor's inline rendering is sanitized too (no raw-HTML injection through the WYSIWYG). - Carried-over behaviour: image paste/drop via
onImageUpload(placeholder→URL, polite notice on absent/failed — never silent-drop); theme via the 1.0.5 ThemeProvider (useOptionalTheme/ dark mode); SSR-safe vianext/dynamic({ ssr: false });readOnlyrenders the content with no editing affordances.
Size variants reinterpreted: min (create modal) = a compact
inline-edit surface (toolbar minimal or a selection bubble-menu, ~6 lines); full
(edit form) = the full inline-edit surface with a formatting toolbar (~16+ lines). Neither
shows a raw-source pane. @uiw/react-md-editor is removed from
package.json once no code imports it.
Full-width editing surface (Yue feedback). The full editor
must use the available screen width for a comfortable writing canvas — the edit-form route
(2.3.6) currently constrains content to a narrow max-w-[42rem] column, which
cramps the editor. Widen the edit surface: the editor (and the title/description editing
region of the edit form) should span a wide content container (e.g. max-w-[64rem]
/ the page's comfortable reading-plus-editing width, responsive — full width on smaller
viewports), and the editor itself is width: 100% of that container. The narrow
column was fine for a read view; an EDIT canvas wants room. (The min editor in
the create modal stays bounded by the modal width — the width ask is about the full editor.)
Post-build (Yue feedback): (1) E2E create→edit round-trip now types PLAIN PROSE — a WYSIWYG stores typed syntax as literal text + serializes it back escaped, so raw-Markdown-in-textarea round-trip is N/A; fidelity stays gated by the headless round-trip unit test. (2) Edit form widened to full content width (dropped the max-w-64rem cap; metadata grid → lg:grid-cols-3). (3) Removed the duplicate "Description" label at both the edit form + create modal (the editor owns its own label/aria-label).
Library shipped: Tiptap v3 + tiptap-markdown (over the card's Milkdown rec; round-trip-fidelity gate green). Scope trim: tables out of v1 (lossy round-trip).
Acceptance criteria
- Editing happens on the RENDERED view: typing
**bold**-style content shows bold inline (or via toolbar/shortcut), with no separate raw-syntax pane and no side-by-side preview. - Full-width canvas (Yue feedback): the
fulleditor uses the available screen width — the edit-form route/page widens from the narrowmax-w-[42rem]column to a wide editing container (e.g.max-w-[64rem], responsive: full width on smaller viewports), and the editor iswidth: 100%of it. Verified at desktop + a narrow breakpoint (no horizontal overflow). Themincreate-modal editor stays bounded by the modal. descriptionMdstays Markdown text (no schema/column change);onChangeemits Markdown. A round-trip test proves a representative document (headings, bold/italic, links, lists, task lists, code block, table) survives load → render → edit-noop → serialize with NO drift (this is the gate that justifies the chosen library).- The
MarkdownEditorpublic props are byte-identical to 2.3.5; 2.3.3's create modal is unchanged and 2.3.6's edit form changes ONLY its layout-container width (the full-width canvas above) — no prop/logic edits to either consumer. Existing 2.3.3/2.3.5/2.3.6 component tests pass or are updated only where they asserted the OLD split-pane internals or the old narrow width. - Image paste/drop still works (placeholder→URL, polite notice on absent/failed handler, never silent-drop); theme dark/light still applies;
readOnlyshows content with no editing affordances. - The rendered/edited HTML is sanitized (no raw-HTML/script injection via the WYSIWYG surface).
@uiw/react-md-editorremoved frompackage.jsononce unused; the single-render-module grep guard (2.3.5) still holds (onlylib/markdown/render.tsximportsreact-markdown).- The
/tokens/markdown-editorspecimen + the STRICT shell-a11y sweep stay green: editor is keyboard-operable, the surface is labelled (thelabelprop), toolbar/bubble buttons carry accessible names; third-party editor chrome may keep the documented narrow exclusions (color-contrast / per-svg titles) as in 2.3.5.
Context refs
components/ui/MarkdownEditor.tsx(2.3.5) — the prop surface to PRESERVE + the image-paste/theme/SSR behaviour to carry over;editorConfigForis replacedcomponents/ui/MarkdownView.tsx+lib/markdown/render.tsx— the read path + the sanitize/highlight chain to keep as display SoT- Story 1.4 storage invariant (
descriptionMd= Markdown source) + the AI-storage boundary note — the constraint that picks a remark-native editor - 2.3.3
CreateIssueModal+ 2.3.6EditIssueForm— the two consumers that must NOT need changes Milkdown(recommended) /Tiptap+tiptap-markdowndocs — confirm SSR + dark-mode + image hooks + Markdown round-trip fidelity before committing- 1.5.5 STRICT axe sweep + finding #35 AA-tone rules