mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-16 22:59:04 +02:00
* feat(editor): support text highlight (==text==) in description & comments Adds a single-color (yellow) text highlight mark to the shared rich-text editor, round-tripped through stored Markdown as ==text==. - HighlightExtension: @tiptap/extension-highlight + @tiptap/markdown hooks (markdownTokenizer/parseMarkdown/renderMarkdown) so ==text== <-> <mark> round-trips; inner inline formatting preserved via inlineTokens. - Bubble menu: highlight toggle button (Mod-Shift-H), i18n in 4 locales. - Read-only renderer: highlightToHtml lowers ==text== -> <mark> (skips code and math); rehype-sanitize schema whitelists <mark>. Nested Markdown inside a highlight still parses via the existing rehype-raw step. - prose.css: single yellow <mark> style, legible in light/dark. Pinned @tiptap/extension-highlight to exact 3.22.1 to match @tiptap/core (>=3.23 expects a getStyleProperty export core 3.22.1 doesn't have). Web/desktop only. Mobile (native md4c, no == syntax, no custom renderers) is tracked as a follow-up. MUL-2934. Tests: editor round-trip (cross-process serialization protocol), readonly <mark> rendering + sanitize, and the ==->mark transform incl. code-skip. Co-authored-by: multica-agent <github@multica.ai> * fix(editor): align highlight boundary rules across editor & readonly Addresses two boundary bugs from review (PR #3661): 1. A == inside inline code/math could close a highlight when the opening == was outside the literal span (e.g. ==a `b==c` d== wrongly became <mark>a `b</mark>c` d==). Both the editor tokenizer's lazy regex and the readonly transform only guarded the opening fence, not the closing one. 2. The readonly transform matched across blank lines (==a\n\nb==) while the editor lexes those as two literal paragraphs — a storage↔editor↔readonly mismatch. Fix: extract one shared matcher (utils/highlight-match.ts) used by BOTH the editor tokenizer and the readonly lowering, so the rules can't drift. It skips fences that fall inside code/math literal ranges (open or close) and caps the inner span at the first blank line. Tests: shared-matcher unit tests + both repros covered on the editor (round-trip/HTML) and readonly (transform + rendered DOM) sides. Co-authored-by: multica-agent <github@multica.ai> * fix(editor): handle CRLF in highlight blank-line boundary BLANK_LINE_RE only matched LF, so a CRLF blank line (==a\r\n\r\nb==) was not recognized as a block boundary and got highlighted. Widen to \r?\n[ \t]*\r?\n. Tests: CRLF blank-line (no highlight) + CRLF soft-break (still highlights) on the matcher, readonly transform, and editor sides. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
300 lines
6.6 KiB
CSS
300 lines
6.6 KiB
CSS
/*
|
|
* Rich text prose typography.
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
/* Headings — compact but with clear visual hierarchy */
|
|
.rich-text-editor h1 {
|
|
font-size: 1.375rem;
|
|
font-weight: 700;
|
|
margin-top: 1.5rem;
|
|
margin-bottom: 0.5rem;
|
|
line-height: 1.3;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
.rich-text-editor h2 {
|
|
font-size: 1.125rem;
|
|
font-weight: 600;
|
|
margin-top: 1.5rem;
|
|
margin-bottom: 0.5rem;
|
|
line-height: 1.35;
|
|
}
|
|
|
|
.rich-text-editor h3 {
|
|
font-size: 0.9375rem;
|
|
font-weight: 600;
|
|
margin-top: 1rem;
|
|
margin-bottom: 0.5rem;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
/* Paragraphs */
|
|
.rich-text-editor p {
|
|
margin-top: 0.625rem;
|
|
margin-bottom: 0.625rem;
|
|
line-height: 1.625;
|
|
}
|
|
|
|
/* First child should not have top margin */
|
|
.rich-text-editor > *:first-child {
|
|
margin-top: 0;
|
|
}
|
|
|
|
/* Last child should not have bottom margin */
|
|
.rich-text-editor > *:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
/* Lists */
|
|
.rich-text-editor ul {
|
|
list-style-type: disc;
|
|
padding-inline-start: 1.25rem;
|
|
padding-inline-end: 0.5rem;
|
|
margin: 0.5rem 0;
|
|
}
|
|
|
|
.rich-text-editor ol {
|
|
list-style-type: decimal;
|
|
padding-inline-start: 1.5rem;
|
|
margin: 0.5rem 0;
|
|
}
|
|
|
|
.rich-text-editor li {
|
|
margin: 0.25rem 0;
|
|
line-height: 1.625;
|
|
}
|
|
|
|
.rich-text-editor li + li {
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.rich-text-editor li::marker {
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
/* Remove paragraph margins inside list items (Tiptap wraps li content in <p>) */
|
|
.rich-text-editor li > p {
|
|
margin: 0;
|
|
}
|
|
|
|
.rich-text-editor li > p + p {
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.rich-text-editor .math-node {
|
|
display: inline-flex;
|
|
max-width: 100%;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.rich-text-editor .math-node.inline {
|
|
align-items: center;
|
|
}
|
|
|
|
.rich-text-editor .math-node.block {
|
|
display: block;
|
|
margin: 0.75rem 0;
|
|
overflow-x: auto;
|
|
overflow-y: hidden;
|
|
}
|
|
|
|
.rich-text-editor .math-node.block .katex-display {
|
|
margin: 0;
|
|
}
|
|
|
|
.rich-text-editor .math-node .katex {
|
|
max-width: 100%;
|
|
}
|
|
|
|
/* Nested lists — bullet style progression and tighter spacing */
|
|
.rich-text-editor ul ul {
|
|
list-style-type: circle;
|
|
margin: 0.25rem 0;
|
|
}
|
|
|
|
.rich-text-editor ul ul ul {
|
|
list-style-type: square;
|
|
}
|
|
|
|
.rich-text-editor ol ol {
|
|
list-style-type: lower-alpha;
|
|
margin: 0.25rem 0;
|
|
}
|
|
|
|
.rich-text-editor ol ol ol {
|
|
list-style-type: lower-roman;
|
|
}
|
|
|
|
/* Tables */
|
|
.rich-text-editor .tableWrapper {
|
|
overflow-x: auto;
|
|
margin: 1rem 0;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
}
|
|
|
|
.rich-text-editor table {
|
|
min-width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.rich-text-editor colgroup {
|
|
display: none;
|
|
}
|
|
|
|
.rich-text-editor thead {
|
|
background: color-mix(in srgb, var(--muted) 50%, transparent);
|
|
}
|
|
|
|
.rich-text-editor tbody tr {
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.rich-text-editor tr:hover td {
|
|
background: color-mix(in srgb, var(--muted) 30%, transparent);
|
|
transition: background 0.15s;
|
|
}
|
|
|
|
.rich-text-editor th,
|
|
.rich-text-editor td {
|
|
text-align: left;
|
|
padding: 0.625rem 1rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.rich-text-editor th {
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Remove paragraph margin inside table cells */
|
|
.rich-text-editor th p,
|
|
.rich-text-editor td p {
|
|
margin: 0;
|
|
}
|
|
|
|
/* Blockquotes */
|
|
.rich-text-editor blockquote {
|
|
border-left: 3px solid color-mix(in srgb, var(--muted-foreground) 30%, transparent);
|
|
padding-left: 0.75rem;
|
|
margin: 0.625rem 0;
|
|
color: var(--muted-foreground);
|
|
font-style: italic;
|
|
}
|
|
|
|
.rich-text-editor blockquote p {
|
|
margin-top: 0.25rem;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.rich-text-editor blockquote > *:first-child {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.rich-text-editor blockquote > *:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.rich-text-editor blockquote blockquote {
|
|
margin-top: 0.25rem;
|
|
margin-bottom: 0.25rem;
|
|
border-left-color: color-mix(in srgb, var(--muted-foreground) 15%, transparent);
|
|
}
|
|
|
|
/* Horizontal rules */
|
|
.rich-text-editor hr {
|
|
border: none;
|
|
border-top: 1px solid var(--border);
|
|
margin: 1rem 0;
|
|
}
|
|
|
|
/* Links */
|
|
.rich-text-editor a {
|
|
color: var(--brand);
|
|
text-decoration: underline;
|
|
text-decoration-color: color-mix(in srgb, var(--brand) 40%, transparent);
|
|
text-underline-offset: 2px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.rich-text-editor a:hover {
|
|
text-decoration-color: var(--brand);
|
|
}
|
|
|
|
/* Issue mention cards — inline cards that sit within text flow */
|
|
.rich-text-editor a.issue-mention {
|
|
color: inherit;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.rich-text-editor a.issue-mention:hover {
|
|
text-decoration: none;
|
|
}
|
|
|
|
/* Mentions */
|
|
.rich-text-editor .mention {
|
|
color: var(--primary);
|
|
font-weight: 600;
|
|
text-decoration: none;
|
|
margin: 0 0.125rem;
|
|
}
|
|
|
|
/* Slash command pills */
|
|
.rich-text-editor .slash-command {
|
|
color: var(--primary);
|
|
font-weight: 600;
|
|
text-decoration: none;
|
|
margin: 0 0.125rem;
|
|
}
|
|
|
|
/* Strong / emphasis */
|
|
.rich-text-editor strong {
|
|
font-weight: 600;
|
|
}
|
|
|
|
.rich-text-editor em {
|
|
font-style: italic;
|
|
}
|
|
|
|
.rich-text-editor s,
|
|
.rich-text-editor del {
|
|
text-decoration: line-through;
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
/* Text highlight (`==text==` → <mark>). Single yellow tint that stays legible
|
|
in both light and dark themes; `color: inherit` keeps the underlying text
|
|
color so highlighted links/code remain readable. box-decoration-break clones
|
|
the background across wrapped lines (same approach as inline code above). */
|
|
.rich-text-editor mark {
|
|
background-color: color-mix(in srgb, #facc15 45%, transparent);
|
|
color: inherit;
|
|
border-radius: 0.2em;
|
|
padding: 0 0.15em;
|
|
box-decoration-break: clone;
|
|
-webkit-box-decoration-break: clone;
|
|
}
|