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: {