refactor(editor): unify editor into features/editor with single markdown pipeline
Replace three divergent data paths (Marked HTML loading, regex post-processing saving, separate paste parsing) with one symmetric path through @tiptap/markdown. Key changes: - Create features/editor/ module with ContentEditor (unified edit+readonly) and TitleEditor, replacing components/common/ editor files - Load content via contentType: 'markdown' instead of markdownToHtml() hack - Save content via editor.getMarkdown() directly, no post-processing - Merge RichTextEditor + ReadonlyEditor into single ContentEditor with editable prop - Extract extensions into separate modules (mention, file-upload, markdown-paste, submit-shortcut, code-block-view) - Extract shared preprocessMentionShortcodes to components/markdown/mentions.ts - Add copyMarkdown utility for clipboard operations - Upgrade all @tiptap packages from 3.20.5 to 3.22.1 (lexer isolation fix, HTML entity roundtrip fix, table alignment support) - Delete markdownToHtml.ts, readonly-editor.tsx, and 10 old component files Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8eb1caa72b
commit
27e58d91af
29 changed files with 848 additions and 999 deletions
|
|
@ -1,359 +0,0 @@
|
|||
/* Rich text editor: ProseMirror styles using shadcn design tokens */
|
||||
|
||||
.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 hierarchy for issue tracker context */
|
||||
.rich-text-editor h1 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.rich-text-editor h2 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.rich-text-editor h3 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Paragraphs */
|
||||
.rich-text-editor p {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
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: 1rem;
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
/* 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.5rem 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;
|
||||
}
|
||||
|
||||
/* 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); }
|
||||
|
||||
/* 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); }
|
||||
|
||||
/* 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: 2px solid color-mix(in srgb, var(--muted-foreground) 30%, transparent);
|
||||
padding-left: 0.75rem;
|
||||
margin: 0.5rem 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(--primary);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rich-text-editor a:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Issue mention cards — override link styling */
|
||||
.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 {
|
||||
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;
|
||||
}
|
||||
|
||||
/* Images — shared styling for both editing and readonly */
|
||||
.rich-text-editor img {
|
||||
border-radius: var(--radius);
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* Uploading image placeholder — data-uploading attribute managed by ProseMirror schema */
|
||||
.rich-text-editor img[data-uploading] {
|
||||
opacity: 0.5;
|
||||
border-radius: var(--radius);
|
||||
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; }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue