Files
PiCloud/dashboard/src/lib/editor-theme.ts
MechaCat02 56de652f7a fix(dashboard): keep selection visible against active-line tint
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>
2026-05-24 22:58:27 +02:00

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);