MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-95

2.3.10 WYSIWYG Markdown editing — edit the rendered view inline, drop the split source/preview (Yue feedback · finding #53)

Done
Description

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's onChange still emits a Markdown string.
  • Public surface: the MarkdownEditor prop 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 via next/dynamic({ ssr: false }); readOnly renders 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 full editor uses the available screen width — the edit-form route/page widens from the narrow max-w-[42rem] column to a wide editing container (e.g. max-w-[64rem], responsive: full width on smaller viewports), and the editor is width: 100% of it. Verified at desktop + a narrow breakpoint (no horizontal overflow). The min create-modal editor stays bounded by the modal.
  • descriptionMd stays Markdown text (no schema/column change); onChange emits 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 MarkdownEditor public 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; readOnly shows content with no editing affordances.
  • The rendered/edited HTML is sanitized (no raw-HTML/script injection via the WYSIWYG surface).
  • @uiw/react-md-editor removed from package.json once unused; the single-render-module grep guard (2.3.5) still holds (only lib/markdown/render.tsx imports react-markdown).
  • The /tokens/markdown-editor specimen + the STRICT shell-a11y sweep stay green: editor is keyboard-operable, the surface is labelled (the label prop), 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; editorConfigFor is replaced
  • components/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.6 EditIssueForm — the two consumers that must NOT need changes
  • Milkdown (recommended) / Tiptap+tiptap-markdown docs — confirm SSR + dark-mode + image hooks + Markdown round-trip fidelity before committing
  • 1.5.5 STRICT axe sweep + finding #35 AA-tone rules