Merge pull request #73 from apoapostolov/feat/active-line-highlight-control

feat: Customize active-line background through themes
This commit is contained in:
Vadim Melnicuk
2026-09-06 07:22:07 +01:00
committed by GitHub
6 changed files with 56 additions and 4 deletions
+1
View File
@@ -1,6 +1,7 @@
# Markdown Editor Optimized (MEO)
---
## Unreleased
- Added the theme color `activeLineBackground` to customize the active-line highlight in Live and Source
- Added a shared Read Only toggle beside Spell Check that blocks editing in Live and Source while preserving reading, selection, copying, and external updates; the preference defaults to off and persists across documents, sessions, and workspaces
- Added visual multiline editing to live table cells while storing line breaks as `<br>` in Markdown
- Added link-reference definitions as visible linked lists in HTML and PDF exports, and restored copying selections across rendered blocks
+10
View File
@@ -57,6 +57,16 @@ Guide on how to customise the editor background, colors, syntax highlighting, fo
- Exported HTML/PDF documents use this background color.
- Color values must be valid `#hex`, `rgb()/rgba()`, `hsl()/hsla()`, or `var(--...)`.
## activeLineBackground
`activeLineBackground` controls the background highlight on the line that currently has the cursor (Live and Source modes).
- Optional. Leave empty (or omit) to use the built-in mix derived from `base03`.
- Set to a color (`#hex`, `rgb()`, `hsl()`, `var(--...)`) to restyle the highlight for your theme.
- Set to `transparent` to remove the active-line wash via the theme (without turning off CodeMirror's active-line machinery globally).
Read Only hides the active-line highlight regardless of this color.
## colors
`colors` is the 9-color base palette (`base01` to `base09`) that drives the theme.
+8 -2
View File
@@ -354,13 +354,14 @@
"h6FontWeight": "600",
"liveLineHeight": 1.5,
"sourceLineHeight": 1.5
}
},
"activeLineBackground": ""
},
"defaultSnippets": [
{
"label": "One Monokai theme payload",
"description": "Insert the full theme object with sensible defaults.",
"body": "{\n\t\"id\": \"one-monokai\",\n\t\"name\": \"One Monokai\",\n\t\"backgroundColor\": \"var(--vscode-editor-background)\",\n\t\"colors\": {\n\t\t\"base01\": \"var(--vscode-editor-foreground)\",\n\t\t\"base02\": \"#676f7d\",\n\t\t\"base03\": \"#3e444d\",\n\t\t\"base04\": \"#e06c75\",\n\t\t\"base05\": \"#61afef\",\n\t\t\"base06\": \"#66d9ef\",\n\t\t\"base07\": \"#e5c07b\",\n\t\t\"base08\": \"#c678dd\",\n\t\t\"base09\": \"#98c379\"\n\t},\n\t\"syntaxTokens\": {},\n\t\"fonts\": {\n\t\t\"liveFont\": \"\",\n\t\t\"sourceFont\": \"\",\n\t\t\"liveFontWeight\": \"\",\n\t\t\"sourceFontWeight\": \"\",\n\t\t\"liveFontSize\": null,\n\t\t\"sourceFontSize\": null,\n\t\t\"h1FontSize\": 1.6,\n\t\t\"h2FontSize\": 1.5,\n\t\t\"h3FontSize\": 1.3,\n\t\t\"h4FontSize\": 1.2,\n\t\t\"h5FontSize\": 1.1,\n\t\t\"h6FontSize\": 1,\n\t\t\"h1FontWeight\": \"600\",\n\t\t\"h2FontWeight\": \"600\",\n\t\t\"h3FontWeight\": \"600\",\n\t\t\"h4FontWeight\": \"600\",\n\t\t\"h5FontWeight\": \"600\",\n\t\t\"h6FontWeight\": \"600\",\n\t\t\"liveLineHeight\": 1.5,\n\t\t\"sourceLineHeight\": 1.5\n\t}\n}"
"body": "{\n\t\"id\": \"one-monokai\",\n\t\"name\": \"One Monokai\",\n\t\"backgroundColor\": \"var(--vscode-editor-background)\",\n\t\"activeLineBackground\": \"\",\n\t\"colors\": {\n\t\t\"base01\": \"var(--vscode-editor-foreground)\",\n\t\t\"base02\": \"#676f7d\",\n\t\t\"base03\": \"#3e444d\",\n\t\t\"base04\": \"#e06c75\",\n\t\t\"base05\": \"#61afef\",\n\t\t\"base06\": \"#66d9ef\",\n\t\t\"base07\": \"#e5c07b\",\n\t\t\"base08\": \"#c678dd\",\n\t\t\"base09\": \"#98c379\"\n\t},\n\t\"syntaxTokens\": {},\n\t\"fonts\": {\n\t\t\"liveFont\": \"\",\n\t\t\"sourceFont\": \"\",\n\t\t\"liveFontWeight\": \"\",\n\t\t\"sourceFontWeight\": \"\",\n\t\t\"liveFontSize\": null,\n\t\t\"sourceFontSize\": null,\n\t\t\"h1FontSize\": 1.6,\n\t\t\"h2FontSize\": 1.5,\n\t\t\"h3FontSize\": 1.3,\n\t\t\"h4FontSize\": 1.2,\n\t\t\"h5FontSize\": 1.1,\n\t\t\"h6FontSize\": 1,\n\t\t\"h1FontWeight\": \"600\",\n\t\t\"h2FontWeight\": \"600\",\n\t\t\"h3FontWeight\": \"600\",\n\t\t\"h4FontWeight\": \"600\",\n\t\t\"h5FontWeight\": \"600\",\n\t\t\"h6FontWeight\": \"600\",\n\t\t\"liveLineHeight\": 1.5,\n\t\t\"sourceLineHeight\": 1.5\n\t}\n}"
}
],
"description": "Theme payload containing the editor background, base palette colors, optional token overrides, and font settings.",
@@ -765,6 +766,11 @@
"sourceLineHeight"
],
"additionalProperties": false
},
"activeLineBackground": {
"type": "string",
"default": "",
"description": "Active-line background in Live and Source. Empty uses the default color; accepts theme colors or transparent. Read Only hides the highlight."
}
},
"required": [
+31 -1
View File
@@ -335,6 +335,8 @@ export type ThemeSettings = {
id: string;
name: string;
backgroundColor: string;
/** Empty uses the built-in mix from base03. */
activeLineBackground: string;
colors: ThemeColors;
syntaxTokens: ThemeSyntaxTokens;
fonts: ThemeFonts;
@@ -405,6 +407,7 @@ const createThemeFromColors = (params: {
id: string;
name: string;
backgroundColor?: string;
activeLineBackground?: string;
colors?: Partial<ThemeColors>;
syntaxTokenPaletteOverrides?: Partial<ThemeSyntaxTokenPalette>;
syntaxTokenOverrides?: Partial<ThemeSyntaxTokens>;
@@ -416,6 +419,7 @@ const createThemeFromColors = (params: {
id: params.id,
name: params.name,
backgroundColor: params.backgroundColor ?? defaultThemeBackgroundColor,
activeLineBackground: params.activeLineBackground ?? '',
colors,
syntaxTokens: {
...buildSyntaxTokenColors(colors, params.syntaxTokenPaletteOverrides),
@@ -576,7 +580,7 @@ export const defaultThemeSettings: ThemeSettings = themePresets[0] as ThemeSetti
const hexColorRegex = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
const rgbColorRegex = /^rgba?\(\s*(?:\d{1,3}\s*,\s*){2}\d{1,3}(?:\s*,\s*(?:0(?:\.\d+)?|1(?:\.0+)?|\d*\.?\d+))?\s*\)$/;
const hslColorRegex = /^hsla?\(\s*(?:[+\-]?\d+(?:\.\d+)?(?:deg|rad|grad|turn)?\s*,\s*){2}\d{1,3}%?(?:\s*,\s*(?:0(?:\.\d+)?|1(?:\.0+)?|\d*\.?\d+))?\s*\)$/;
const hslColorRegex = /^hsla?\(\s*[+\-]?\d+(?:\.\d+)?(?:deg|rad|grad|turn)?\s*,\s*\d+(?:\.\d+)?%\s*,\s*\d+(?:\.\d+)?%(?:\s*,\s*(?:0(?:\.\d+)?|1(?:\.0+)?|\d*\.?\d+))?\s*\)$/;
const cssVarColorRegex = /^var\(\s*--[A-Za-z0-9_-]+\s*(?:,\s*[^)]+)?\)$/;
const isRecord = (value: unknown): value is Record<string, unknown> =>
@@ -599,6 +603,22 @@ const isValidThemeColor = (value: string): boolean => {
return hexColorRegex.test(candidate) || rgbColorRegex.test(candidate) || hslColorRegex.test(candidate) || cssVarColorRegex.test(candidate);
};
const isValidActiveLineColor = (value: string): boolean => (
value.trim().toLowerCase() === 'transparent' || isValidThemeColor(value)
);
/** Empty string uses the built-in mix from base03. */
const resolveOptionalThemeColor = (value: unknown): string => {
if (typeof value !== 'string') {
return '';
}
const trimmed = value.trim();
if (!trimmed) {
return '';
}
return isValidActiveLineColor(trimmed) ? trimmed : '';
};
const sanitizeThemeColor = (value: unknown, fallback: string): string => {
if (typeof value !== 'string') {
return fallback;
@@ -746,6 +766,7 @@ export const serializeThemeSettings = (theme: ThemeSettings): ThemeSettingsPaylo
id: theme.id,
name: theme.name,
backgroundColor: theme.backgroundColor,
activeLineBackground: theme.activeLineBackground,
colors: { ...theme.colors },
syntaxTokens,
fonts: { ...theme.fonts }
@@ -758,6 +779,7 @@ export const resolveTheme = (themeOverride?: Partial<ThemeSettings>): ThemeSetti
id: normalizeString(themeOverride?.id, defaultThemeSettings.id),
name: normalizeString(themeOverride?.name, defaultThemeSettings.name),
backgroundColor: sanitizeThemeColor(themeOverride?.backgroundColor, defaultThemeBackgroundColor),
activeLineBackground: resolveOptionalThemeColor(themeOverride?.activeLineBackground),
colors,
syntaxTokens: resolveThemeSyntaxTokens(themeOverride?.syntaxTokens, colors),
fonts: resolveThemeFonts(themeOverride?.fonts)
@@ -793,6 +815,14 @@ export const validateThemePayload = (value: unknown): ThemeValidationResult => {
}
}
if (value.activeLineBackground !== undefined) {
if (typeof value.activeLineBackground !== 'string') {
errors.push('Theme "activeLineBackground" must be a string.');
} else if (value.activeLineBackground.trim() && !isValidActiveLineColor(value.activeLineBackground)) {
errors.push('Theme "activeLineBackground" must be empty (default), transparent, or a valid hex, rgb, hsl, or var(--...) color string.');
}
}
const rawColors = value.colors;
if (!isRecord(rawColors)) {
errors.push('Theme "colors" must be an object.');
+5 -1
View File
@@ -123,9 +123,13 @@ export const applyThemeSettings = (theme?: ThemeSettings): void => {
`color-mix(in srgb, ${resolvedTheme.colors.base05} 28%, transparent 72%)`
);
rootStyle.setProperty('--meo-caret-color', resolvedTheme.colors.base01);
const activeLineBackground = typeof resolvedTheme.activeLineBackground === 'string'
? resolvedTheme.activeLineBackground.trim()
: '';
rootStyle.setProperty(
'--meo-active-line-bg',
`color-mix(in srgb, ${resolvedTheme.colors.base03} 35%, transparent 65%)`
activeLineBackground
|| `color-mix(in srgb, ${resolvedTheme.colors.base03} 35%, transparent 65%)`
);
for (const key of themeColorKeys) {
+1
View File
@@ -70,6 +70,7 @@ interface ThemeSettings {
id: string;
name: string;
backgroundColor?: string;
activeLineBackground?: string;
colors: Record<string, string>;
syntaxTokens: Record<string, string>;
fonts: {