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 @@ @@ -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) {