diff --git a/src/components/TextEditor/components/CodeBlockComponent.vue b/src/components/TextEditor/components/CodeBlockComponent.vue
index 067807f90..ce57663cf 100644
--- a/src/components/TextEditor/components/CodeBlockComponent.vue
+++ b/src/components/TextEditor/components/CodeBlockComponent.vue
@@ -1,7 +1,9 @@
+
-
+
{{
+ selectedLanguage || 'auto'
+ }}
@@ -30,6 +34,16 @@ export default {
NodeViewContent,
},
props: nodeViewProps,
+ data() {
+ return {
+ isEditable: this.editor.isEditable,
+ }
+ },
+ mounted() {
+ this.editor.on('update', () => {
+ this.isEditable = this.editor.isEditable
+ })
+ },
computed: {
selectedLanguage: {
get() {
@@ -71,98 +85,209 @@ export default {
right: 0.25rem;
padding-top: 0;
padding-bottom: 0;
- opacity: 0;
- z-index: 10;
- transition-property: opacity;
- transition-duration: 0.2s;
- transition-timing-function: ease-in-out;
- transition-delay: 0s;
- pointer-events: none;
}
-.code-block-container:hover .language-selector {
- opacity: 1;
- transition-delay: 0s;
- pointer-events: auto;
-}
-
-.language-selector:focus-within {
- opacity: 1;
- transition-delay: 0s;
- pointer-events: auto;
-}
-
-/* When mouse leaves the code block, delay the hiding */
-.code-block-container:not(:hover) .language-selector:not(:focus-within) {
- transition-delay: 1.5s;
+.language-label {
+ position: absolute;
+ top: 0.5rem;
+ right: 0.75rem;
+ font-size: 12px;
+ color: var(--ink-gray-4);
+ pointer-events: none;
+ user-select: none;
}
+/* ── pre shell ────────────────────────────────────────────────── */
.ProseMirror pre {
- background: #0d0d0d;
- color: #fff;
- font-family: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
+ background: #f6f8fa;
+ color: #24292e;
+ font-family:
+ ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
'Roboto Mono', 'Oxygen Mono', 'Ubuntu Mono', 'Source Code Pro', 'Fira Mono',
'Droid Sans Mono', 'Consolas', 'Courier New', monospace;
+ font-size: 12px;
+ line-height: 1.7;
padding: 0.75rem 1rem;
border-radius: 0.75rem;
- caret-color: #fff;
+ border: 1px solid var(--outline-gray-2);
+ caret-color: #24292e;
}
.ProseMirror pre code {
color: inherit;
padding: 0;
background: none;
- font-size: 12px;
+ font-size: inherit;
}
-.ProseMirror pre .hljs-comment,
-.ProseMirror pre .hljs-quote {
- color: #999;
+/* ── highlight.js — GitHub light ─────────────────────────────── */
+.ProseMirror pre .hljs-doctag,
+.ProseMirror pre .hljs-keyword,
+.ProseMirror pre .hljs-meta .hljs-keyword,
+.ProseMirror pre .hljs-template-tag,
+.ProseMirror pre .hljs-template-variable,
+.ProseMirror pre .hljs-type,
+.ProseMirror pre .hljs-variable.language_ {
+ color: #d73a49;
}
-.ProseMirror pre .hljs-variable,
-.ProseMirror pre .hljs-template-variable,
-.ProseMirror pre .hljs-attribute,
-.ProseMirror pre .hljs-tag,
-.ProseMirror pre .hljs-name,
-.ProseMirror pre .hljs-regexp,
-.ProseMirror pre .hljs-link,
-.ProseMirror pre .hljs-selector-id,
-.ProseMirror pre .hljs-selector-class {
- color: #f2777a;
+.ProseMirror pre .hljs-title,
+.ProseMirror pre .hljs-title.class_,
+.ProseMirror pre .hljs-title.class_.inherited__,
+.ProseMirror pre .hljs-title.function_ {
+ color: #6f42c1;
}
-.ProseMirror pre .hljs-number,
-.ProseMirror pre .hljs-meta,
-.ProseMirror pre .hljs-built_in,
-.ProseMirror pre .hljs-builtin-name,
+.ProseMirror pre .hljs-attr,
+.ProseMirror pre .hljs-attribute,
.ProseMirror pre .hljs-literal,
-.ProseMirror pre .hljs-type,
-.ProseMirror pre .hljs-params {
- color: #f99157;
+.ProseMirror pre .hljs-meta,
+.ProseMirror pre .hljs-number,
+.ProseMirror pre .hljs-operator,
+.ProseMirror pre .hljs-variable,
+.ProseMirror pre .hljs-selector-attr,
+.ProseMirror pre .hljs-selector-class,
+.ProseMirror pre .hljs-selector-id {
+ color: #005cc5;
}
+.ProseMirror pre .hljs-regexp,
.ProseMirror pre .hljs-string,
-.ProseMirror pre .hljs-symbol,
-.ProseMirror pre .hljs-bullet {
- color: #99cc99;
+.ProseMirror pre .hljs-meta .hljs-string {
+ color: #032f62;
}
-.ProseMirror pre .hljs-title,
-.ProseMirror pre .hljs-section {
- color: #ffcc66;
+.ProseMirror pre .hljs-built_in,
+.ProseMirror pre .hljs-symbol {
+ color: #e36209;
}
-.ProseMirror pre .hljs-keyword,
-.ProseMirror pre .hljs-selector-tag {
- color: #6196cc;
+.ProseMirror pre .hljs-comment,
+.ProseMirror pre .hljs-code,
+.ProseMirror pre .hljs-formula {
+ color: #6a737d;
}
+.ProseMirror pre .hljs-name,
+.ProseMirror pre .hljs-quote,
+.ProseMirror pre .hljs-selector-tag,
+.ProseMirror pre .hljs-selector-pseudo {
+ color: #22863a;
+}
+
+.ProseMirror pre .hljs-subst {
+ color: #24292e;
+}
+.ProseMirror pre .hljs-section {
+ color: #005cc5;
+ font-weight: bold;
+}
+.ProseMirror pre .hljs-bullet {
+ color: #735c0f;
+}
.ProseMirror pre .hljs-emphasis {
+ color: #24292e;
font-style: italic;
}
-
.ProseMirror pre .hljs-strong {
+ color: #24292e;
+ font-weight: 700;
+}
+.ProseMirror pre .hljs-addition {
+ color: #22863a;
+ background-color: #f0fff4;
+}
+.ProseMirror pre .hljs-deletion {
+ color: #b31d28;
+ background-color: #ffeef0;
+}
+
+/* ── pre shell — dark mode ───────────────────────────────────── */
+[data-theme='dark'] .ProseMirror pre {
+ background: #0d1117;
+ color: #c9d1d9;
+ caret-color: #c9d1d9;
+}
+
+/* ── highlight.js — GitHub dark ──────────────────────────────── */
+[data-theme='dark'] .ProseMirror pre .hljs-doctag,
+[data-theme='dark'] .ProseMirror pre .hljs-keyword,
+[data-theme='dark'] .ProseMirror pre .hljs-meta .hljs-keyword,
+[data-theme='dark'] .ProseMirror pre .hljs-template-tag,
+[data-theme='dark'] .ProseMirror pre .hljs-template-variable,
+[data-theme='dark'] .ProseMirror pre .hljs-type,
+[data-theme='dark'] .ProseMirror pre .hljs-variable.language_ {
+ color: #ff7b72;
+}
+
+[data-theme='dark'] .ProseMirror pre .hljs-title,
+[data-theme='dark'] .ProseMirror pre .hljs-title.class_,
+[data-theme='dark'] .ProseMirror pre .hljs-title.class_.inherited__,
+[data-theme='dark'] .ProseMirror pre .hljs-title.function_ {
+ color: #d2a8ff;
+}
+
+[data-theme='dark'] .ProseMirror pre .hljs-attr,
+[data-theme='dark'] .ProseMirror pre .hljs-attribute,
+[data-theme='dark'] .ProseMirror pre .hljs-literal,
+[data-theme='dark'] .ProseMirror pre .hljs-meta,
+[data-theme='dark'] .ProseMirror pre .hljs-number,
+[data-theme='dark'] .ProseMirror pre .hljs-operator,
+[data-theme='dark'] .ProseMirror pre .hljs-variable,
+[data-theme='dark'] .ProseMirror pre .hljs-selector-attr,
+[data-theme='dark'] .ProseMirror pre .hljs-selector-class,
+[data-theme='dark'] .ProseMirror pre .hljs-selector-id {
+ color: #79c0ff;
+}
+
+[data-theme='dark'] .ProseMirror pre .hljs-regexp,
+[data-theme='dark'] .ProseMirror pre .hljs-string,
+[data-theme='dark'] .ProseMirror pre .hljs-meta .hljs-string {
+ color: #a5d6ff;
+}
+
+[data-theme='dark'] .ProseMirror pre .hljs-built_in,
+[data-theme='dark'] .ProseMirror pre .hljs-symbol {
+ color: #ffa657;
+}
+
+[data-theme='dark'] .ProseMirror pre .hljs-comment,
+[data-theme='dark'] .ProseMirror pre .hljs-code,
+[data-theme='dark'] .ProseMirror pre .hljs-formula {
+ color: #8b949e;
+}
+
+[data-theme='dark'] .ProseMirror pre .hljs-name,
+[data-theme='dark'] .ProseMirror pre .hljs-quote,
+[data-theme='dark'] .ProseMirror pre .hljs-selector-tag,
+[data-theme='dark'] .ProseMirror pre .hljs-selector-pseudo {
+ color: #7ee787;
+}
+
+[data-theme='dark'] .ProseMirror pre .hljs-subst {
+ color: #c9d1d9;
+}
+[data-theme='dark'] .ProseMirror pre .hljs-section {
+ color: #1f6feb;
+ font-weight: bold;
+}
+[data-theme='dark'] .ProseMirror pre .hljs-bullet {
+ color: #f2cc60;
+}
+[data-theme='dark'] .ProseMirror pre .hljs-emphasis {
+ color: #c9d1d9;
+ font-style: italic;
+}
+[data-theme='dark'] .ProseMirror pre .hljs-strong {
+ color: #c9d1d9;
font-weight: 700;
}
+[data-theme='dark'] .ProseMirror pre .hljs-addition {
+ color: #aff5b4;
+ background-color: #033a16;
+}
+[data-theme='dark'] .ProseMirror pre .hljs-deletion {
+ color: #ffdcd7;
+ background-color: #67060c;
+}
diff --git a/src/components/TextEditor/style.css b/src/components/TextEditor/style.css
index 0ef89f398..6cf46cd30 100644
--- a/src/components/TextEditor/style.css
+++ b/src/components/TextEditor/style.css
@@ -171,3 +171,20 @@ img.ProseMirror-selectednode {
margin: 2.25em 0;
}
}
+
+/* Empty paragraph height in prose-v3.
+ ProseMirror always renders empty paragraphs as
+ even in readonly mode, so p:empty never matches.
+ Edit mode: no override — natural 14px × 1.7 = 23.8px, avoids jump when typing
+ Readonly mode: scoped via [contenteditable=false] + ProseMirror-trailingBreak class
+ to precisely target empty paragraphs (not inline
in content) */
+.ProseMirror[contenteditable='false'] .prose-v3 p:has(> br.ProseMirror-trailingBreak:only-child),
+.prose-v3.ProseMirror[contenteditable='false'] p:has(> br.ProseMirror-trailingBreak:only-child) {
+ line-height: 20px;
+}
+
+/* prose-v3 blockquote: 2px left border — overrides Tailwind Typography's 0.25rem
+ using element specificity (0,1,1) to beat the plugin's :where() (0,1,0) */
+.prose-v3 blockquote {
+ border-inline-start-width: 2px;
+}
diff --git a/tailwind/plugin.js b/tailwind/plugin.js
index 95b603e01..67b4847ad 100644
--- a/tailwind/plugin.js
+++ b/tailwind/plugin.js
@@ -15,7 +15,7 @@ let globalStyles = (theme) => ({
'font-optical-sizing': 'auto',
},
'html, body, button, p, span, div': {
- fontVariationSettings: "'opsz' 24",
+ fontVariationSettings: "'opsz' 24, 'cv11' 1",
WebkitFontSmoothing: 'antialiased',
MozOsxFontSmoothing: 'grayscale',
},
@@ -390,10 +390,240 @@ export default plugin(
},
},
},
+ // prose-v3: zero paragraph margins, user controls spacing with Enter
+ // all spacing on 8px grid: 4, 8, 16, 24, 32px
+ // empty = 14px × 1.7 line-height ≈ 23.8px (the user's spacing unit)
+ v3: {
+ css: [
+ {
+ fontSize: '14px',
+ fontWeight: 420,
+ lineHeight: '1.7',
+ letterSpacing: '0.02em',
+
+ // prose-v3 color tokens — calmer, softer than defaults
+ '--tw-prose-body': 'var(--ink-gray-7)',
+ '--tw-prose-bold': 'var(--ink-gray-8)',
+ '--tw-prose-quotes': 'var(--ink-gray-7)',
+ '--tw-prose-quote-borders': 'var(--ink-gray-3)',
+ '--tw-prose-kbd': 'var(--ink-gray-8)',
+ '--tw-prose-code': 'var(--ink-gray-8)',
+
+ // links: subtle bottom border, darkens on hover
+ a: {
+ textDecoration: 'none',
+ borderBottom: '1px solid var(--ink-gray-3)',
+ transition: 'border-color 0.08s ease',
+ },
+ 'a:hover': {
+ borderBottom: '1px solid var(--ink-gray-6)',
+ },
+
+ // inline code: subtle pill — strip Tailwind's added quotes
+ 'code::before': { content: 'none' },
+ 'code::after': { content: 'none' },
+ code: {
+ backgroundColor: 'var(--surface-gray-2)',
+ borderRadius: '4px',
+ paddingTop: '1px',
+ paddingBottom: '1px',
+ paddingInlineStart: '5px',
+ paddingInlineEnd: '5px',
+ fontWeight: 420,
+ fontSize: em(12, 14),
+ },
+ // code inside pre should not get the pill styles
+ 'pre code': {
+ backgroundColor: 'transparent',
+ borderRadius: '0',
+ padding: '0',
+ fontWeight: 'inherit',
+ fontSize: 'inherit',
+ },
+
+ // blockquote: left border, receded color, no italic, no quote marks
+ blockquote: {
+ 'border-inline-start-width': '2px',
+ borderInlineStartColor: 'var(--ink-gray-3)',
+ borderInlineStartStyle: 'solid',
+ fontStyle: 'normal',
+ color: 'var(--ink-gray-6)',
+ },
+ 'blockquote p:first-of-type::before': { content: 'none' },
+ 'blockquote p:last-of-type::after': { content: 'none' },
+
+ // paragraphs: zero margin — user controls spacing with empty paragraphs
+ p: {
+ marginTop: '0',
+ marginBottom: '0',
+ },
+
+ // headings: marginTop creates section break (32/24px),
+ // marginBottom keeps heading close to its content (8px, proximity)
+ // h1/h2: full weight + darkest; h3-h5: softer weight + stepped-back color
+ h1: {
+ fontSize: em(20, 14),
+ marginTop: '32px',
+ marginBottom: '8px',
+ lineHeight: '1.3',
+ },
+ h2: {
+ fontSize: em(18, 14),
+ marginTop: '32px',
+ marginBottom: '8px',
+ lineHeight: '1.35',
+ },
+ h3: {
+ fontSize: em(16, 14),
+ marginTop: '24px',
+ marginBottom: '8px',
+ lineHeight: '1.4',
+ },
+ h4: {
+ fontSize: em(14, 14),
+ marginTop: '24px',
+ marginBottom: '8px',
+ lineHeight: '1.45',
+ },
+ h5: {
+ fontSize: em(13, 14),
+ marginTop: '24px',
+ marginBottom: '8px',
+ lineHeight: '1.45',
+ },
+
+ // element after heading gets no extra top margin
+ 'h1 + *': { marginTop: '0' },
+ 'h2 + *': { marginTop: '0' },
+ 'h3 + *': { marginTop: '0' },
+ 'h4 + *': { marginTop: '0' },
+ 'h5 + *': { marginTop: '0' },
+
+ // lists: small outer margin (4px), tight internal spacing
+ ul: {
+ marginTop: '4px',
+ marginBottom: '4px',
+ paddingInlineStart: '1.5em',
+ },
+ ol: {
+ marginTop: '4px',
+ marginBottom: '4px',
+ paddingInlineStart: '1.5em',
+ },
+ li: {
+ marginTop: '4px',
+ marginBottom: '4px',
+ },
+ 'li p': {
+ marginTop: '4px',
+ marginBottom: '4px',
+ },
+ 'ul ul, ul ol, ol ul, ol ol': {
+ marginTop: '4px',
+ marginBottom: '4px',
+ },
+
+ // blockquote: breathing room (16px), no italic, no quotes
+ blockquote: {
+ marginTop: '16px',
+ marginBottom: '16px',
+ paddingInlineStart: '1em',
+ fontStyle: 'normal',
+ quotes: 'none',
+ },
+ 'blockquote p': {
+ marginTop: '0',
+ marginBottom: '0',
+ },
+
+ // code blocks: breathing room (16px)
+ pre: {
+ fontSize: em(12, 14),
+ lineHeight: '1.6',
+ marginTop: '16px',
+ marginBottom: '16px',
+ borderRadius: '0.375rem',
+ paddingTop: '0.75em',
+ paddingInlineEnd: '1em',
+ paddingBottom: '0.75em',
+ paddingInlineStart: '1em',
+ },
+
+ // tables: breathing room (16px)
+ table: {
+ fontSize: em(12, 14),
+ lineHeight: '1.5',
+ marginTop: '16px',
+ marginBottom: '16px',
+ },
+
+ // images, video, figures: breathing room (16px)
+ img: {
+ marginTop: '16px',
+ marginBottom: '16px',
+ },
+ picture: {
+ marginTop: '16px',
+ marginBottom: '16px',
+ },
+ 'picture > img': {
+ marginTop: '0',
+ marginBottom: '0',
+ },
+ video: {
+ marginTop: '16px',
+ marginBottom: '16px',
+ },
+ figure: {
+ marginTop: '16px',
+ marginBottom: '16px',
+ },
+ 'figure > *': {
+ marginTop: '0',
+ marginBottom: '0',
+ },
+
+ // hr: short centered line, not edge-to-edge
+ hr: {
+ marginTop: '24px',
+ marginBottom: '24px',
+ marginLeft: 'auto',
+ marginRight: 'auto',
+ width: '20%',
+ },
+ 'hr + *': {
+ marginTop: '0',
+ },
+ },
+ {
+ // first/last child: no extra margin
+ '> :first-child': {
+ marginTop: '0',
+ },
+ '> :last-child': {
+ marginBottom: '0',
+ },
+ },
+ ],
+ },
+ // prose-p-spacing: restores paragraph margins for content authored before prose-v3.
+ // Apply alongside prose-v3 for pre-migration content:
+ // new content: "prose prose-v3"
+ // old content: "prose prose-v3 prose-p-spacing"
+ // Must be defined after v3 in this config so its rules come later in
+ // the generated CSS and override v3's zero paragraph margins.
+ 'p-spacing': {
+ css: {
+ p: {
+ marginTop: '0.5rem',
+ marginBottom: '0.5rem',
+ },
+ },
+ },
}),
},
},
- }
+ },
)
function em(pixels, base = 16) {