mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-18 04:09:13 +02:00
Compare commits
1 Commits
agent/lamb
...
refactor/e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd36d60f1b |
@@ -39,6 +39,7 @@ import { useDownloadAttachment } from "./use-download-attachment";
|
||||
import { AttachmentCard } from "./attachment-card";
|
||||
import { HtmlAttachmentPreview } from "./html-attachment-preview";
|
||||
import { getPreviewKind, type PreviewKind } from "./utils/preview";
|
||||
import "./styles/attachment.css";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
@@ -216,11 +217,10 @@ export function Attachment({
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// DOM and styling are intentionally a direct port of the original
|
||||
// extensions/image-view.tsx <figure> structure. All visual styles live in
|
||||
// content-editor.css under `.image-figure / .image-content / .image-toolbar`
|
||||
// — the unification step de-scoped those rules from `.rich-text-editor` so
|
||||
// standalone surfaces (chat messages, AttachmentList) get identical visuals
|
||||
// without each component carrying its own Tailwind tax.
|
||||
// extensions/image-view.tsx <figure> structure. Shared visual styles live in
|
||||
// styles/attachment.css under `.image-figure / .image-content / .image-toolbar`
|
||||
// so standalone surfaces (chat messages, AttachmentList) get identical visuals
|
||||
// without depending on the editor stylesheet being imported elsewhere.
|
||||
|
||||
interface ImageAttachmentViewProps {
|
||||
src: string;
|
||||
|
||||
25
packages/views/editor/code-block-static.test.tsx
Normal file
25
packages/views/editor/code-block-static.test.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { CodeBlockStatic } from "./code-block-static";
|
||||
|
||||
describe("CodeBlockStatic", () => {
|
||||
it("uses the standalone rich-text-editor pre shape covered by code.css", () => {
|
||||
const { container } = render(
|
||||
<CodeBlockStatic language="bash" body="uv run --extra dev pytest -q" />,
|
||||
);
|
||||
|
||||
const pre = container.querySelector("pre.rich-text-editor");
|
||||
const code = container.querySelector("pre.rich-text-editor code");
|
||||
|
||||
expect(pre).not.toBeNull();
|
||||
expect(code?.textContent).toBe("uv run --extra dev pytest -q");
|
||||
});
|
||||
|
||||
it("keeps standalone static code blocks under the block-code CSS selectors", () => {
|
||||
const codeCss = readFileSync("editor/styles/code.css", "utf8");
|
||||
|
||||
expect(codeCss).toContain("pre.rich-text-editor");
|
||||
expect(codeCss).toContain("pre.rich-text-editor code");
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,7 @@ import { useMemo } from "react";
|
||||
import { createLowlight, common } from "lowlight";
|
||||
import { toHtml } from "hast-util-to-html";
|
||||
import { cn } from "@multica/ui/lib/utils";
|
||||
import "./styles/code.css";
|
||||
|
||||
const lowlight = createLowlight(common);
|
||||
|
||||
|
||||
@@ -1,631 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
caret-color: var(--foreground);
|
||||
}
|
||||
|
||||
.rich-text-editor.ProseMirror:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Placeholder */
|
||||
.rich-text-editor .is-editor-empty:first-child::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
color: var(--muted-foreground);
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
.rich-text-editor code {
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
font-size: 0.875rem;
|
||||
background: color-mix(in srgb, var(--foreground) 3%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 5%, transparent);
|
||||
color: color-mix(in srgb, var(--foreground) 75%, transparent);
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: var(--radius-sm);
|
||||
box-decoration-break: clone;
|
||||
-webkit-box-decoration-break: clone;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
.rich-text-editor pre {
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
background: var(--muted);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.75rem 1rem;
|
||||
margin: 0.75rem 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.rich-text-editor pre code {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--foreground);
|
||||
padding: 0;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Mermaid diagrams */
|
||||
.rich-text-editor .mermaid-diagram {
|
||||
background: var(--muted);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
margin: 0.75rem 0;
|
||||
overflow-x: auto;
|
||||
padding: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rich-text-editor .mermaid-diagram-frame {
|
||||
border: 0;
|
||||
display: block;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rich-text-editor .mermaid-diagram-loading,
|
||||
.rich-text-editor .mermaid-diagram-error p {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.8125rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rich-text-editor .mermaid-diagram-error pre {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Mermaid toolbar — dark pill, top-right corner, appears on hover */
|
||||
.rich-text-editor .mermaid-diagram-toolbar {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
display: flex;
|
||||
gap: 1px;
|
||||
padding: 0.25rem;
|
||||
background: color-mix(in srgb, black 75%, transparent);
|
||||
backdrop-filter: blur(8px);
|
||||
border-radius: var(--radius);
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.rich-text-editor .mermaid-diagram:hover .mermaid-diagram-toolbar,
|
||||
.rich-text-editor .mermaid-diagram-toolbar:focus-within {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.rich-text-editor .mermaid-diagram-toolbar button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
border-radius: calc(var(--radius) - 2px);
|
||||
color: white;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.rich-text-editor .mermaid-diagram-toolbar button:hover {
|
||||
background: color-mix(in srgb, white 15%, transparent);
|
||||
}
|
||||
|
||||
/* Mermaid lightbox — full-screen preview (ESC or click backdrop to close) */
|
||||
.mermaid-diagram-lightbox {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: color-mix(in srgb, black 80%, transparent);
|
||||
cursor: zoom-out;
|
||||
}
|
||||
|
||||
.mermaid-diagram-lightbox-frame {
|
||||
border: 0;
|
||||
width: 90vw;
|
||||
height: 90vh;
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Syntax highlighting — lowlight (hljs) */
|
||||
.rich-text-editor .hljs-keyword,
|
||||
.rich-text-editor .hljs-selector-tag,
|
||||
.rich-text-editor .hljs-built_in { color: oklch(0.55 0.16 255); }
|
||||
|
||||
.rich-text-editor .hljs-string,
|
||||
.rich-text-editor .hljs-addition { color: oklch(0.55 0.14 155); }
|
||||
|
||||
.rich-text-editor .hljs-comment,
|
||||
.rich-text-editor .hljs-quote { color: var(--muted-foreground); font-style: italic; }
|
||||
|
||||
.rich-text-editor .hljs-number,
|
||||
.rich-text-editor .hljs-literal { color: oklch(0.58 0.16 30); }
|
||||
|
||||
.rich-text-editor .hljs-title,
|
||||
.rich-text-editor .hljs-section,
|
||||
.rich-text-editor .hljs-title\.function_ { color: oklch(0.55 0.14 280); }
|
||||
|
||||
.rich-text-editor .hljs-attr,
|
||||
.rich-text-editor .hljs-attribute { color: oklch(0.58 0.12 60); }
|
||||
|
||||
.rich-text-editor .hljs-variable,
|
||||
.rich-text-editor .hljs-template-variable { color: oklch(0.58 0.14 20); }
|
||||
|
||||
.rich-text-editor .hljs-type,
|
||||
.rich-text-editor .hljs-title\.class_ { color: oklch(0.55 0.14 200); }
|
||||
|
||||
.rich-text-editor .hljs-deletion { color: oklch(0.55 0.2 25); }
|
||||
|
||||
.rich-text-editor .hljs-meta { color: var(--muted-foreground); }
|
||||
|
||||
/* XML / HTML — lowlight emits .hljs-tag for `<` `>` brackets and .hljs-name
|
||||
for the element name. Without these rules, HTML source renders mostly in
|
||||
the default text color and looks unhighlighted. */
|
||||
.rich-text-editor .hljs-tag { color: var(--muted-foreground); }
|
||||
.rich-text-editor .hljs-name { color: oklch(0.55 0.16 255); }
|
||||
|
||||
/* Dark mode overrides */
|
||||
.dark .rich-text-editor .hljs-keyword,
|
||||
.dark .rich-text-editor .hljs-selector-tag,
|
||||
.dark .rich-text-editor .hljs-built_in { color: oklch(0.7 0.14 255); }
|
||||
|
||||
.dark .rich-text-editor .hljs-string,
|
||||
.dark .rich-text-editor .hljs-addition { color: oklch(0.7 0.14 155); }
|
||||
|
||||
.dark .rich-text-editor .hljs-number,
|
||||
.dark .rich-text-editor .hljs-literal { color: oklch(0.72 0.14 30); }
|
||||
|
||||
.dark .rich-text-editor .hljs-title,
|
||||
.dark .rich-text-editor .hljs-section,
|
||||
.dark .rich-text-editor .hljs-title\.function_ { color: oklch(0.72 0.12 280); }
|
||||
|
||||
.dark .rich-text-editor .hljs-attr,
|
||||
.dark .rich-text-editor .hljs-attribute { color: oklch(0.72 0.1 60); }
|
||||
|
||||
.dark .rich-text-editor .hljs-variable,
|
||||
.dark .rich-text-editor .hljs-template-variable { color: oklch(0.72 0.12 20); }
|
||||
|
||||
.dark .rich-text-editor .hljs-type,
|
||||
.dark .rich-text-editor .hljs-title\.class_ { color: oklch(0.72 0.12 200); }
|
||||
|
||||
.dark .rich-text-editor .hljs-deletion { color: oklch(0.7 0.18 25); }
|
||||
|
||||
.dark .rich-text-editor .hljs-name { color: oklch(0.72 0.14 255); }
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
/* Readonly mode overrides */
|
||||
.rich-text-editor.readonly.ProseMirror {
|
||||
caret-color: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Mention NodeView inline layout fix */
|
||||
.rich-text-editor [data-node-view-wrapper] {
|
||||
display: inline;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Block-level NodeViews (fileCard) need to override the inline default above */
|
||||
.rich-text-editor .file-card-node {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Images — generic fallback (non-NodeView contexts) */
|
||||
.rich-text-editor img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: var(--radius);
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* Image — shared by the editor NodeView AND standalone surfaces (chat
|
||||
* messages, comment-card AttachmentList). De-scoped from `.rich-text-editor`
|
||||
* during the <Attachment> unification so every call site renders identical
|
||||
* visuals without each component carrying its own Tailwind override. */
|
||||
|
||||
.image-figure {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
|
||||
.image-figure[data-clickable="true"] {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.image-figure.image-selected .image-content {
|
||||
outline: 2px solid var(--brand);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.image-content {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.image-uploading {
|
||||
opacity: 0.5;
|
||||
animation: rte-upload-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes rte-upload-pulse {
|
||||
0%, 100% { opacity: 0.5; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.image-toolbar {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
display: flex;
|
||||
gap: 1px;
|
||||
padding: 0.25rem;
|
||||
background: color-mix(in srgb, black 75%, transparent);
|
||||
backdrop-filter: blur(8px);
|
||||
border-radius: var(--radius);
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.image-figure:hover .image-toolbar {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.image-toolbar button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
border-radius: calc(var(--radius) - 2px);
|
||||
color: white;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.image-toolbar button:hover {
|
||||
background: color-mix(in srgb, white 15%, transparent);
|
||||
}
|
||||
|
||||
/* Editor-only overrides: center the figure inside the NodeView and cap
|
||||
* width so an oversize paste doesn't blow out the editor column. */
|
||||
|
||||
.rich-text-editor .image-node {
|
||||
display: block !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rich-text-editor .image-figure {
|
||||
max-width: min(100%, 640px);
|
||||
}
|
||||
|
||||
/* Bubble menu — floating toolbar pill */
|
||||
.bubble-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
padding: 0.25rem;
|
||||
background: var(--popover);
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
|
||||
border-radius: var(--radius);
|
||||
box-shadow:
|
||||
0 4px 12px color-mix(in srgb, black 12%, transparent),
|
||||
0 0 0 1px color-mix(in srgb, black 4%, transparent);
|
||||
}
|
||||
|
||||
/* Link edit mode — inline URL input */
|
||||
.bubble-menu-link-edit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
background: var(--popover);
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
|
||||
border-radius: var(--radius);
|
||||
box-shadow:
|
||||
0 4px 12px color-mix(in srgb, black 12%, transparent),
|
||||
0 0 0 1px color-mix(in srgb, black 4%, transparent);
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
/* Link hover card — shows URL + actions on link hover */
|
||||
.link-hover-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.25rem 0.25rem 0.5rem;
|
||||
background: var(--popover);
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
|
||||
border-radius: var(--radius);
|
||||
box-shadow:
|
||||
0 4px 12px color-mix(in srgb, black 12%, transparent),
|
||||
0 0 0 1px color-mix(in srgb, black 4%, transparent);
|
||||
max-width: min(360px, calc(100vw - 2rem));
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -51,7 +51,7 @@ import { EditorBubbleMenu } from "./bubble-menu";
|
||||
import { useLinkHover, LinkHoverCard } from "./link-hover-card";
|
||||
import { AttachmentDownloadProvider } from "./attachment-download-context";
|
||||
import "katex/dist/katex.min.css";
|
||||
import "./content-editor.css";
|
||||
import "./styles/index.css";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* 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),
|
||||
* All link styling is controlled by styles/prose.css (var(--brand) color),
|
||||
* not Tailwind HTMLAttributes, to keep a single source of truth.
|
||||
*/
|
||||
import type { RefObject } from "react";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, waitFor } from "@testing-library/react";
|
||||
import type { ReactElement } from "react";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
const { getAttachmentTextContentMock } = vi.hoisted(() => ({
|
||||
@@ -125,6 +126,42 @@ describe("ReadonlyContent line breaks", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ReadonlyContent code styling", () => {
|
||||
const literalCode = "uv run --extra dev pytest -q";
|
||||
|
||||
it("renders inline and fenced code through rich-text-editor code selectors", () => {
|
||||
const { container } = render(
|
||||
<ReadonlyContent
|
||||
content={[
|
||||
`<code>${literalCode}</code>`,
|
||||
"",
|
||||
"```",
|
||||
literalCode,
|
||||
"```",
|
||||
].join("\n")}
|
||||
/>,
|
||||
);
|
||||
|
||||
const inlineCode = Array.from(container.querySelectorAll("code")).find(
|
||||
(code) => !code.closest("pre"),
|
||||
);
|
||||
const blockCode = container.querySelector("pre code");
|
||||
|
||||
expect(inlineCode?.textContent).toBe(literalCode);
|
||||
expect(blockCode?.textContent).toBe(literalCode);
|
||||
});
|
||||
|
||||
it("keeps editor code literal by disabling font ligatures", () => {
|
||||
const codeCss = readFileSync("editor/styles/code.css", "utf8");
|
||||
|
||||
expect(codeCss).toContain(".rich-text-editor code");
|
||||
expect(codeCss).toContain(".rich-text-editor pre");
|
||||
expect(codeCss).toContain(".rich-text-editor pre code");
|
||||
expect(codeCss).toContain("font-variant-ligatures: none;");
|
||||
expect(codeCss).toContain('font-feature-settings: "liga" 0;');
|
||||
});
|
||||
});
|
||||
|
||||
describe("ReadonlyContent Mermaid rendering", () => {
|
||||
it("renders mermaid code fences in a sized sandbox iframe with legacy rgb colors", async () => {
|
||||
const originalGetComputedStyle = window.getComputedStyle;
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
*
|
||||
* Visual parity with ContentEditor is achieved by:
|
||||
* - Wrapping output in <div class="rich-text-editor readonly"> so the same
|
||||
* content-editor.css rules apply to standard HTML tags
|
||||
* styles/index.css rules apply to standard HTML tags
|
||||
* - Using the same preprocessMarkdown pipeline (mention shortcodes + linkify)
|
||||
* - Using lowlight for code highlighting (same engine as Tiptap's CodeBlockLowlight)
|
||||
* so .hljs-* CSS rules from content-editor.css produce identical colors
|
||||
* so .hljs-* CSS rules from styles/code.css produce identical colors
|
||||
* - Rendering mentions with the same IssueMentionCard component and .mention class
|
||||
*/
|
||||
|
||||
@@ -43,7 +43,7 @@ import { HtmlBlockPreview } from "./html-block-preview";
|
||||
import { AttachmentDownloadProvider } from "./attachment-download-context";
|
||||
import { Attachment as AttachmentRenderer } from "./attachment";
|
||||
import "katex/dist/katex.min.css";
|
||||
import "./content-editor.css";
|
||||
import "./styles/index.css";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lowlight — same engine + language set as Tiptap's CodeBlockLowlight
|
||||
|
||||
74
packages/views/editor/styles/attachment.css
Normal file
74
packages/views/editor/styles/attachment.css
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Attachment image figure.
|
||||
*
|
||||
* Shared by the editor NodeView and standalone attachment surfaces such as
|
||||
* chat messages, comment attachment lists, and markdown image renderers.
|
||||
* The Attachment component imports this file directly so its visual contract
|
||||
* does not depend on ContentEditor or ReadonlyContent being mounted first.
|
||||
*/
|
||||
|
||||
.image-figure {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
|
||||
.image-figure[data-clickable="true"] {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.image-figure.image-selected .image-content {
|
||||
outline: 2px solid var(--brand);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.image-content {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.image-uploading {
|
||||
opacity: 0.5;
|
||||
animation: rte-upload-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes rte-upload-pulse {
|
||||
0%, 100% { opacity: 0.5; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.image-toolbar {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
display: flex;
|
||||
gap: 1px;
|
||||
padding: 0.25rem;
|
||||
background: color-mix(in srgb, black 75%, transparent);
|
||||
backdrop-filter: blur(8px);
|
||||
border-radius: var(--radius);
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.image-figure:hover .image-toolbar {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.image-toolbar button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
border-radius: calc(var(--radius) - 2px);
|
||||
color: white;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.image-toolbar button:hover {
|
||||
background: color-mix(in srgb, white 15%, transparent);
|
||||
}
|
||||
113
packages/views/editor/styles/code.css
Normal file
113
packages/views/editor/styles/code.css
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Code typography and lowlight highlighting.
|
||||
*
|
||||
* The literal-code rules intentionally disable font ligatures for both inline
|
||||
* code and code blocks. Command flags such as `--extra` must render as the
|
||||
* exact characters the user can copy.
|
||||
*/
|
||||
|
||||
/* Inline code */
|
||||
.rich-text-editor code {
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
font-variant-ligatures: none;
|
||||
font-feature-settings: "liga" 0;
|
||||
font-size: 0.875rem;
|
||||
background: color-mix(in srgb, var(--foreground) 3%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 5%, transparent);
|
||||
color: color-mix(in srgb, var(--foreground) 75%, transparent);
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: var(--radius-sm);
|
||||
box-decoration-break: clone;
|
||||
-webkit-box-decoration-break: clone;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
.rich-text-editor pre,
|
||||
pre.rich-text-editor {
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
font-variant-ligatures: none;
|
||||
font-feature-settings: "liga" 0;
|
||||
background: var(--muted);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.75rem 1rem;
|
||||
margin: 0.75rem 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.rich-text-editor pre code,
|
||||
pre.rich-text-editor code {
|
||||
font-variant-ligatures: none;
|
||||
font-feature-settings: "liga" 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--foreground);
|
||||
padding: 0;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Syntax highlighting — lowlight (hljs) */
|
||||
.rich-text-editor .hljs-keyword,
|
||||
.rich-text-editor .hljs-selector-tag,
|
||||
.rich-text-editor .hljs-built_in { color: oklch(0.55 0.16 255); }
|
||||
|
||||
.rich-text-editor .hljs-string,
|
||||
.rich-text-editor .hljs-addition { color: oklch(0.55 0.14 155); }
|
||||
|
||||
.rich-text-editor .hljs-comment,
|
||||
.rich-text-editor .hljs-quote { color: var(--muted-foreground); font-style: italic; }
|
||||
|
||||
.rich-text-editor .hljs-number,
|
||||
.rich-text-editor .hljs-literal { color: oklch(0.58 0.16 30); }
|
||||
|
||||
.rich-text-editor .hljs-title,
|
||||
.rich-text-editor .hljs-section,
|
||||
.rich-text-editor .hljs-title\.function_ { color: oklch(0.55 0.14 280); }
|
||||
|
||||
.rich-text-editor .hljs-attr,
|
||||
.rich-text-editor .hljs-attribute { color: oklch(0.58 0.12 60); }
|
||||
|
||||
.rich-text-editor .hljs-variable,
|
||||
.rich-text-editor .hljs-template-variable { color: oklch(0.58 0.14 20); }
|
||||
|
||||
.rich-text-editor .hljs-type,
|
||||
.rich-text-editor .hljs-title\.class_ { color: oklch(0.55 0.14 200); }
|
||||
|
||||
.rich-text-editor .hljs-deletion { color: oklch(0.55 0.2 25); }
|
||||
|
||||
.rich-text-editor .hljs-meta { color: var(--muted-foreground); }
|
||||
|
||||
/* XML / HTML — lowlight emits .hljs-tag for `<` `>` brackets and .hljs-name
|
||||
for the element name. Without these rules, HTML source renders mostly in
|
||||
the default text color and looks unhighlighted. */
|
||||
.rich-text-editor .hljs-tag { color: var(--muted-foreground); }
|
||||
.rich-text-editor .hljs-name { color: oklch(0.55 0.16 255); }
|
||||
|
||||
/* Dark mode overrides */
|
||||
.dark .rich-text-editor .hljs-keyword,
|
||||
.dark .rich-text-editor .hljs-selector-tag,
|
||||
.dark .rich-text-editor .hljs-built_in { color: oklch(0.7 0.14 255); }
|
||||
|
||||
.dark .rich-text-editor .hljs-string,
|
||||
.dark .rich-text-editor .hljs-addition { color: oklch(0.7 0.14 155); }
|
||||
|
||||
.dark .rich-text-editor .hljs-number,
|
||||
.dark .rich-text-editor .hljs-literal { color: oklch(0.72 0.14 30); }
|
||||
|
||||
.dark .rich-text-editor .hljs-title,
|
||||
.dark .rich-text-editor .hljs-section,
|
||||
.dark .rich-text-editor .hljs-title\.function_ { color: oklch(0.72 0.12 280); }
|
||||
|
||||
.dark .rich-text-editor .hljs-attr,
|
||||
.dark .rich-text-editor .hljs-attribute { color: oklch(0.72 0.1 60); }
|
||||
|
||||
.dark .rich-text-editor .hljs-variable,
|
||||
.dark .rich-text-editor .hljs-template-variable { color: oklch(0.72 0.12 20); }
|
||||
|
||||
.dark .rich-text-editor .hljs-type,
|
||||
.dark .rich-text-editor .hljs-title\.class_ { color: oklch(0.72 0.12 200); }
|
||||
|
||||
.dark .rich-text-editor .hljs-deletion { color: oklch(0.7 0.18 25); }
|
||||
|
||||
.dark .rich-text-editor .hljs-name { color: oklch(0.72 0.14 255); }
|
||||
13
packages/views/editor/styles/index.css
Normal file
13
packages/views/editor/styles/index.css
Normal file
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Editor content styles.
|
||||
*
|
||||
* ContentEditor and ReadonlyContent intentionally share this stylesheet so
|
||||
* editable Tiptap content and readonly markdown content render with the same
|
||||
* typography, code highlighting, embeds, and inline affordances.
|
||||
*/
|
||||
|
||||
@import "./shell.css";
|
||||
@import "./prose.css";
|
||||
@import "./code.css";
|
||||
@import "./mermaid.css";
|
||||
@import "./media.css";
|
||||
26
packages/views/editor/styles/media.css
Normal file
26
packages/views/editor/styles/media.css
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Media rules that are specific to rich text editor surfaces.
|
||||
*
|
||||
* The reusable Attachment component owns the generic .image-* figure and
|
||||
* toolbar styles in attachment.css. This file only contains rich-text-editor
|
||||
* fallbacks and editor-specific constraints.
|
||||
*/
|
||||
|
||||
/* Images — generic fallback (non-NodeView contexts) */
|
||||
.rich-text-editor img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: var(--radius);
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* Editor-only overrides: center the figure inside the NodeView and cap
|
||||
* width so an oversize paste doesn't blow out the editor column. */
|
||||
.rich-text-editor .image-node {
|
||||
display: block !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rich-text-editor .image-figure {
|
||||
max-width: min(100%, 640px);
|
||||
}
|
||||
84
packages/views/editor/styles/mermaid.css
Normal file
84
packages/views/editor/styles/mermaid.css
Normal file
@@ -0,0 +1,84 @@
|
||||
/* Mermaid diagrams */
|
||||
.rich-text-editor .mermaid-diagram {
|
||||
background: var(--muted);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
margin: 0.75rem 0;
|
||||
overflow-x: auto;
|
||||
padding: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rich-text-editor .mermaid-diagram-frame {
|
||||
border: 0;
|
||||
display: block;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rich-text-editor .mermaid-diagram-loading,
|
||||
.rich-text-editor .mermaid-diagram-error p {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.8125rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rich-text-editor .mermaid-diagram-error pre {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Mermaid toolbar — dark pill, top-right corner, appears on hover */
|
||||
.rich-text-editor .mermaid-diagram-toolbar {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
display: flex;
|
||||
gap: 1px;
|
||||
padding: 0.25rem;
|
||||
background: color-mix(in srgb, black 75%, transparent);
|
||||
backdrop-filter: blur(8px);
|
||||
border-radius: var(--radius);
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.rich-text-editor .mermaid-diagram:hover .mermaid-diagram-toolbar,
|
||||
.rich-text-editor .mermaid-diagram-toolbar:focus-within {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.rich-text-editor .mermaid-diagram-toolbar button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
border-radius: calc(var(--radius) - 2px);
|
||||
color: white;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.rich-text-editor .mermaid-diagram-toolbar button:hover {
|
||||
background: color-mix(in srgb, white 15%, transparent);
|
||||
}
|
||||
|
||||
/* Mermaid lightbox — full-screen preview (ESC or click backdrop to close) */
|
||||
.mermaid-diagram-lightbox {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: color-mix(in srgb, black 80%, transparent);
|
||||
cursor: zoom-out;
|
||||
}
|
||||
|
||||
.mermaid-diagram-lightbox-frame {
|
||||
border: 0;
|
||||
width: 90vw;
|
||||
height: 90vh;
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
278
packages/views/editor/styles/prose.css
Normal file
278
packages/views/editor/styles/prose.css
Normal file
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
86
packages/views/editor/styles/shell.css
Normal file
86
packages/views/editor/styles/shell.css
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Editor shell and floating chrome.
|
||||
*
|
||||
* This file owns ProseMirror root behavior, placeholder text, NodeView layout,
|
||||
* and editor-only floating controls. Rich prose typography lives in prose.css.
|
||||
*/
|
||||
|
||||
.rich-text-editor.ProseMirror {
|
||||
color: var(--foreground);
|
||||
caret-color: var(--foreground);
|
||||
}
|
||||
|
||||
.rich-text-editor.ProseMirror:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Placeholder */
|
||||
.rich-text-editor .is-editor-empty:first-child::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
color: var(--muted-foreground);
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Readonly mode overrides */
|
||||
.rich-text-editor.readonly.ProseMirror {
|
||||
caret-color: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Mention NodeView inline layout fix */
|
||||
.rich-text-editor [data-node-view-wrapper] {
|
||||
display: inline;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Block-level NodeViews (fileCard) need to override the inline default above */
|
||||
.rich-text-editor .file-card-node {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Bubble menu — floating toolbar pill */
|
||||
.bubble-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
padding: 0.25rem;
|
||||
background: var(--popover);
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
|
||||
border-radius: var(--radius);
|
||||
box-shadow:
|
||||
0 4px 12px color-mix(in srgb, black 12%, transparent),
|
||||
0 0 0 1px color-mix(in srgb, black 4%, transparent);
|
||||
}
|
||||
|
||||
/* Link edit mode — inline URL input */
|
||||
.bubble-menu-link-edit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
background: var(--popover);
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
|
||||
border-radius: var(--radius);
|
||||
box-shadow:
|
||||
0 4px 12px color-mix(in srgb, black 12%, transparent),
|
||||
0 0 0 1px color-mix(in srgb, black 4%, transparent);
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
/* Link hover card — shows URL + actions on link hover */
|
||||
.link-hover-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.25rem 0.25rem 0.5rem;
|
||||
background: var(--popover);
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
|
||||
border-radius: var(--radius);
|
||||
box-shadow:
|
||||
0 4px 12px color-mix(in srgb, black 12%, transparent),
|
||||
0 0 0 1px color-mix(in srgb, black 4%, transparent);
|
||||
max-width: min(360px, calc(100vw - 2rem));
|
||||
white-space: nowrap;
|
||||
}
|
||||
Reference in New Issue
Block a user