docs(editor): annotate key files with design decisions and pitfalls
Add architecture comments to content-editor.tsx, markdown-paste.ts, extensions/index.ts, mention-view.tsx, content-editor.css, and preprocess.ts explaining: why single markdown pipeline, why data-pm-slice for paste detection, typography benchmarks, mention card sizing rationale, and what was removed from the old system. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b5924ffa99
commit
6c651f4be5
6 changed files with 124 additions and 3 deletions
|
|
@ -1,4 +1,29 @@
|
|||
/* Rich text editor: ProseMirror styles using shadcn design tokens */
|
||||
/*
|
||||
* ContentEditor typography — ProseMirror styles using shadcn design tokens.
|
||||
*
|
||||
* Design tier: "Compact" (same tier as Linear, Slack). Optimized for short-form
|
||||
* content (issue descriptions, comments) that users scan, not long-form reading.
|
||||
*
|
||||
* Typography values benchmarked against (April 2026):
|
||||
* - github-markdown-css (GitHub's markdown renderer)
|
||||
* - @tailwindcss/typography prose-sm preset
|
||||
* - Linear's editor (Tiptap-based, 14px body)
|
||||
*
|
||||
* Key decisions:
|
||||
* Body: 14px (text-sm), line-height 1.625 (between GitHub 1.5 and Tailwind 1.714)
|
||||
* Headings: h1=22px (1.57x), h2=18px (1.29x), h3=15px (1.07x) — compact but
|
||||
* with clear hierarchy. Previous h3 was 14px (same as body = no differentiation).
|
||||
* Paragraph spacing: 10px (was 8px; GitHub uses 10px, Tailwind prose-sm uses 16px)
|
||||
* List indent: 20px for ul (was 16px; standard is 22-32px)
|
||||
* Code block margin: 12px (was 8px; gives breathing room between code and prose)
|
||||
* Blockquote border: 3px (was 2px; GitHub/Tailwind both use 4px)
|
||||
* Links: var(--brand) blue with 40% opacity underline (was var(--primary) near-black)
|
||||
*
|
||||
* Inline elements (mention cards, inline code) that exceed line-height:
|
||||
* The browser auto-expands the line box for lines containing taller inline
|
||||
* elements. Controlled via vertical-align on [data-node-view-wrapper] and
|
||||
* box-decoration-break: clone on inline code.
|
||||
*/
|
||||
|
||||
.rich-text-editor.ProseMirror {
|
||||
color: var(--foreground);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,30 @@
|
|||
"use client";
|
||||
|
||||
/**
|
||||
* ContentEditor — the single rich-text editor for the entire application.
|
||||
*
|
||||
* Architecture decisions (April 2026 refactor):
|
||||
*
|
||||
* 1. ONE COMPONENT for both editing and readonly display. The `editable` prop
|
||||
* controls the mode. Previously we had RichTextEditor + ReadonlyEditor as
|
||||
* separate components with duplicated extension configs — this caused
|
||||
* visual inconsistency between edit and display modes.
|
||||
*
|
||||
* 2. ONE MARKDOWN PIPELINE via @tiptap/markdown. Content is loaded with
|
||||
* `contentType: 'markdown'` and saved with `editor.getMarkdown()`.
|
||||
* Previously we had a custom `markdownToHtml()` pipeline (Marked library)
|
||||
* for loading and regex post-processing for saving — two asymmetric paths
|
||||
* that caused roundtrip inconsistencies. The @tiptap/markdown extension
|
||||
* (v3.21.0+) handles table cell <p> wrapping and custom mention tokenizers
|
||||
* natively, eliminating the need for the HTML detour.
|
||||
*
|
||||
* 3. PREPROCESSING is minimal: only legacy mention shortcode migration and
|
||||
* URL linkification (preprocessMarkdown). No HTML conversion.
|
||||
*
|
||||
* Tech: Tiptap v3.22.1 (ProseMirror wrapper), @tiptap/markdown for
|
||||
* bidirectional Markdown ↔ ProseMirror JSON conversion.
|
||||
*/
|
||||
|
||||
import {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,24 @@
|
|||
/**
|
||||
* Shared extension factory for ContentEditor.
|
||||
*
|
||||
* One function builds the extension array for BOTH edit and readonly modes.
|
||||
* This ensures visual consistency — the same extensions parse and render
|
||||
* content identically regardless of mode.
|
||||
*
|
||||
* Split:
|
||||
* - Both modes: StarterKit, CodeBlock, Link, Image, Table, Markdown, Mention
|
||||
* - Edit only: Typography, Placeholder, markdownPaste, submitShortcut,
|
||||
* fileUpload, Mention suggestion popup
|
||||
*
|
||||
* Link config differs: edit mode has autolink (detects URLs while typing),
|
||||
* readonly does not (prevents false positives on display).
|
||||
*
|
||||
* Mention suggestion is only attached in edit mode — readonly doesn't need
|
||||
* the autocomplete popup.
|
||||
*
|
||||
* All link styling is controlled by content-editor.css (var(--brand) color),
|
||||
* not Tailwind HTMLAttributes, to keep a single source of truth.
|
||||
*/
|
||||
import type { RefObject } from "react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
|
||||
|
|
|
|||
|
|
@ -1,3 +1,27 @@
|
|||
/**
|
||||
* Markdown paste extension — ensures pasted text is parsed as Markdown.
|
||||
*
|
||||
* Problem: The browser clipboard can contain BOTH text/plain and text/html.
|
||||
* ProseMirror always prefers text/html when present (hardcoded in
|
||||
* parseFromClipboard: `let asText = !html`). When copying from VS Code,
|
||||
* text editors, or .md files, the OS wraps text in <pre>/<div> HTML tags.
|
||||
* ProseMirror parses these as code blocks — wrong.
|
||||
*
|
||||
* Solution: Use `handlePaste` (the only ProseMirror prop that runs for ALL
|
||||
* paste events and has access to raw ClipboardEvent). We check for
|
||||
* `data-pm-slice` in the HTML — this attribute is added by ProseMirror's
|
||||
* own clipboard serializer. If present, the source is another ProseMirror
|
||||
* editor and its HTML is structurally correct — let ProseMirror handle it.
|
||||
* Otherwise, ignore the HTML and parse text/plain as Markdown.
|
||||
*
|
||||
* Why not clipboardTextParser? It only runs when there's NO text/html on
|
||||
* the clipboard (ProseMirror source: `let asText = !!text && !html`).
|
||||
*
|
||||
* Why not heuristic detection (looksLikeMarkdown / hasRichHtml)? Unreliable.
|
||||
* VS Code's HTML contains <code> tags that fool rich-content detectors.
|
||||
* Markdown pattern matching has too many edge cases. The data-pm-slice
|
||||
* check is deterministic — no false positives.
|
||||
*/
|
||||
import { Extension } from "@tiptap/core";
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
import { Slice } from "@tiptap/pm/model";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,23 @@
|
|||
"use client";
|
||||
|
||||
/**
|
||||
* MentionView — NodeView for rendering @mentions inline in the editor.
|
||||
*
|
||||
* Member/agent mentions: plain "@Name" text with .mention class styling.
|
||||
* Issue mentions: inline card with StatusIcon + identifier + title.
|
||||
*
|
||||
* Issue card sizing: must fit within the paragraph line box (14px * 1.625
|
||||
* = 22.75px). Card uses text-xs (12px) + py-0.5 + border ≈ 22px total.
|
||||
* vertical-align: middle is set on the [data-node-view-wrapper] in CSS
|
||||
* (not on the <a> tag) because the wrapper is the outermost inline element
|
||||
* that participates in line box calculation. Setting it on the inner <a>
|
||||
* had no effect since the wrapper was already positioned.
|
||||
*
|
||||
* Fallback: when issue is not in the Zustand store (deleted or other
|
||||
* workspace), the same card style is used with just the identifier from
|
||||
* fallbackLabel — no visual degradation to a plain text link.
|
||||
*/
|
||||
|
||||
import { NodeViewWrapper } from "@tiptap/react";
|
||||
import type { NodeViewProps } from "@tiptap/react";
|
||||
import { useIssueStore } from "@/features/issues/store";
|
||||
|
|
|
|||
|
|
@ -4,9 +4,17 @@ import { preprocessMentionShortcodes } from "@/components/markdown/mentions";
|
|||
/**
|
||||
* Preprocess a markdown string before loading into Tiptap via contentType: 'markdown'.
|
||||
*
|
||||
* Two string→string transforms:
|
||||
* This is the ONLY transform applied before @tiptap/markdown parses the content.
|
||||
* It does NOT convert to HTML — that was the old markdownToHtml.ts pipeline which
|
||||
* was deleted in the April 2026 refactor.
|
||||
*
|
||||
* Two string→string transforms on raw Markdown:
|
||||
* 1. Legacy mention shortcodes [@ id="..." label="..."] → [@Label](mention://member/id)
|
||||
* 2. Raw URLs → markdown links (so they render as clickable Link nodes)
|
||||
* (old serialization format in database, migrated on read)
|
||||
* 2. Raw URLs → markdown links via linkify-it (so they render as clickable Link nodes)
|
||||
*
|
||||
* After this, @tiptap/markdown's parse() handles everything else: headings, lists,
|
||||
* tables, code blocks, and our custom mention tokenizer ([@Name](mention://type/id)).
|
||||
*/
|
||||
export function preprocessMarkdown(markdown: string): string {
|
||||
if (!markdown) return "";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue