CodeMirror layers the active-line background above the selection layer, so the previous opaque active-line color hid selections on the current line. Bumps selection alpha and switches active-line to a subtle sky tint, with the brighter gutter line number as the primary cue. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
160 lines
5.2 KiB
TypeScript
160 lines
5.2 KiB
TypeScript
// Dashboard-matching theme + highlight style for CodeMirror.
|
|
//
|
|
// Colors are pulled from the existing slate/sky palette used across
|
|
// the dashboard (#0f172a / #1e293b / #38bdf8) so the editor blends
|
|
// into the surrounding cards instead of looking like a third-party
|
|
// transplant. Stock dark themes like "One Dark" or "VS Code Dark"
|
|
// would each clash with the slate background by a few shades.
|
|
|
|
import { EditorView } from '@codemirror/view';
|
|
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
|
import { tags as t } from '@lezer/highlight';
|
|
|
|
const palette = {
|
|
bg: '#0b1220', // matches existing <input>/<textarea> backgrounds
|
|
bgGutter: '#0f172a',
|
|
border: '#334155',
|
|
text: '#e2e8f0',
|
|
textMuted: '#94a3b8',
|
|
cursor: '#38bdf8',
|
|
// Selection alpha was originally 30 (≈19%) — bumped to 55 (≈33%)
|
|
// so it stays clearly visible when it sits on top of (or under, in
|
|
// CodeMirror's case) the active-line tint. The default CM layering
|
|
// puts the selection layer behind line backgrounds, so an opaque
|
|
// active line hides selections on the current line; this pair of
|
|
// values makes both visible at once.
|
|
selection: '#38bdf855',
|
|
// Active line: very subtle sky tint at ~6% alpha. Strong enough to
|
|
// see at a glance which line the caret is on, weak enough to leave
|
|
// the selection visible underneath. The active-line gutter (the
|
|
// brighter line number on the left) is the primary indicator.
|
|
activeLine: '#38bdf810',
|
|
activeLineGutter: '#1e293b',
|
|
matchingBracket: '#38bdf850',
|
|
searchMatch: '#38bdf850',
|
|
searchMatchSelected: '#38bdf8',
|
|
|
|
// Syntax — chosen to be readable against #0b1220 without making
|
|
// any single token feel louder than the rest.
|
|
comment: '#64748b',
|
|
str: '#86efac',
|
|
num: '#fbbf24',
|
|
bool: '#fbbf24',
|
|
keyword: '#c4b5fd',
|
|
control: '#c4b5fd',
|
|
operator: '#cbd5e1',
|
|
punct: '#cbd5e1',
|
|
function: '#38bdf8',
|
|
property: '#e2e8f0',
|
|
variable: '#e2e8f0',
|
|
ctx: '#f472b6', // `ctx` is special — pinker so users notice it
|
|
namespace: '#fbbf24', // `log::`
|
|
invalid: '#ef4444'
|
|
};
|
|
|
|
export const dashboardTheme = EditorView.theme(
|
|
{
|
|
'&': {
|
|
color: palette.text,
|
|
backgroundColor: palette.bg,
|
|
borderRadius: '0.375rem'
|
|
},
|
|
'&.cm-focused': {
|
|
outline: `1px solid ${palette.cursor}`
|
|
},
|
|
'.cm-content': {
|
|
caretColor: palette.cursor,
|
|
fontFamily:
|
|
'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace',
|
|
fontSize: '0.85rem',
|
|
padding: '0.5rem 0'
|
|
},
|
|
'.cm-cursor, .cm-dropCursor': {
|
|
borderLeftColor: palette.cursor,
|
|
borderLeftWidth: '2px'
|
|
},
|
|
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
|
|
{
|
|
backgroundColor: palette.selection
|
|
},
|
|
'.cm-activeLine': {
|
|
backgroundColor: palette.activeLine
|
|
},
|
|
'.cm-gutters': {
|
|
backgroundColor: palette.bgGutter,
|
|
color: palette.textMuted,
|
|
border: 'none',
|
|
borderRight: `1px solid ${palette.border}`
|
|
},
|
|
'.cm-activeLineGutter': {
|
|
backgroundColor: palette.activeLineGutter,
|
|
color: palette.text
|
|
},
|
|
'.cm-matchingBracket, .cm-nonmatchingBracket': {
|
|
backgroundColor: palette.matchingBracket,
|
|
outline: 'none'
|
|
},
|
|
// Search / replace panel (Ctrl+F)
|
|
'.cm-panels': {
|
|
backgroundColor: palette.bgGutter,
|
|
color: palette.text,
|
|
borderTop: `1px solid ${palette.border}`
|
|
},
|
|
'.cm-searchMatch': {
|
|
backgroundColor: palette.searchMatch
|
|
},
|
|
'.cm-searchMatch.cm-searchMatch-selected': {
|
|
backgroundColor: palette.searchMatchSelected
|
|
},
|
|
'.cm-panel input, .cm-panel button': {
|
|
backgroundColor: palette.bg,
|
|
color: palette.text,
|
|
border: `1px solid ${palette.border}`,
|
|
borderRadius: '0.25rem',
|
|
padding: '0.2rem 0.4rem'
|
|
},
|
|
'.cm-panel button:hover': {
|
|
backgroundColor: palette.activeLine
|
|
},
|
|
// Autocomplete popup
|
|
'.cm-tooltip': {
|
|
backgroundColor: palette.bgGutter,
|
|
border: `1px solid ${palette.border}`,
|
|
color: palette.text
|
|
},
|
|
'.cm-tooltip.cm-tooltip-autocomplete > ul > li[aria-selected]': {
|
|
backgroundColor: palette.activeLine,
|
|
color: palette.text
|
|
},
|
|
'.cm-completionLabel': {
|
|
color: palette.text
|
|
},
|
|
'.cm-completionDetail': {
|
|
color: palette.textMuted,
|
|
fontStyle: 'normal'
|
|
},
|
|
'.cm-completionIcon': {
|
|
color: palette.textMuted
|
|
}
|
|
},
|
|
{ dark: true }
|
|
);
|
|
|
|
export const dashboardHighlight = HighlightStyle.define([
|
|
{ tag: t.comment, color: palette.comment, fontStyle: 'italic' },
|
|
{ tag: [t.string, t.special(t.string)], color: palette.str },
|
|
{ tag: [t.number, t.bool, t.null], color: palette.num },
|
|
{ tag: [t.keyword, t.modifier], color: palette.keyword, fontWeight: '600' },
|
|
{ tag: t.controlKeyword, color: palette.control, fontWeight: '600' },
|
|
{ tag: [t.operator, t.derefOperator, t.logicOperator], color: palette.operator },
|
|
{ tag: [t.punctuation, t.bracket, t.brace, t.paren, t.squareBracket], color: palette.punct },
|
|
{ tag: [t.function(t.variableName), t.function(t.propertyName)], color: palette.function },
|
|
{ tag: [t.propertyName, t.attributeName], color: palette.property },
|
|
{ tag: t.variableName, color: palette.variable },
|
|
{ tag: t.special(t.variableName), color: palette.ctx, fontWeight: '600' },
|
|
{ tag: t.namespace, color: palette.namespace, fontWeight: '600' },
|
|
{ tag: t.invalid, color: palette.invalid, textDecoration: 'underline wavy' }
|
|
]);
|
|
|
|
export const dashboardSyntaxHighlighting = syntaxHighlighting(dashboardHighlight);
|