diff --git a/CHANGELOG.md b/CHANGELOG.md
index cc35498..c0aa836 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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 `
` in Markdown
- Added link-reference definitions as visible linked lists in HTML and PDF exports, and restored copying selections across rendered blocks
diff --git a/docs/theming.md b/docs/theming.md
index 075397e..bfc32fd 100644
--- a/docs/theming.md
+++ b/docs/theming.md
@@ -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.
diff --git a/package.json b/package.json
index 775c7d5..4b00e68 100644
--- a/package.json
+++ b/package.json
@@ -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": [
diff --git a/src/shared/themeDefaults.ts b/src/shared/themeDefaults.ts
index 06d0f97..89ebccd 100644
--- a/src/shared/themeDefaults.ts
+++ b/src/shared/themeDefaults.ts
@@ -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;
syntaxTokenPaletteOverrides?: Partial;
syntaxTokenOverrides?: Partial;
@@ -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 =>
@@ -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): 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.');
diff --git a/webview/src/helpers/theme.ts b/webview/src/helpers/theme.ts
index a8fe40e..185cea6 100644
--- a/webview/src/helpers/theme.ts
+++ b/webview/src/helpers/theme.ts
@@ -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) {
diff --git a/webview/src/types.d.ts b/webview/src/types.d.ts
index 44b84eb..42c430b 100644
--- a/webview/src/types.d.ts
+++ b/webview/src/types.d.ts
@@ -70,6 +70,7 @@ interface ThemeSettings {
id: string;
name: string;
backgroundColor?: string;
+ activeLineBackground?: string;
colors: Record;
syntaxTokens: Record;
fonts: {