From 5c5eaa4836b9df44c6f791e4f412f82a5a86d399 Mon Sep 17 00:00:00 2001 From: Tom Najdek Date: Mon, 23 Mar 2026 12:45:58 +0100 Subject: [PATCH] Add support for customizable link and visited link colors in themes --- .../components/modal-popup/theme-popup.js | 24 +++++++++++++++++++ src/common/defines.js | 12 ++++++---- src/common/types.ts | 2 ++ src/dom/common/dom-view.tsx | 10 ++++---- src/dom/epub/stylesheets/_content.scss | 9 ------- src/dom/sdt/stylesheets/sdt.scss | 8 ------- src/dom/snapshot/stylesheets/inject.scss | 8 +++++++ src/pdf/pdf-view.js | 9 ++++++- 8 files changed, 54 insertions(+), 28 deletions(-) diff --git a/src/common/components/modal-popup/theme-popup.js b/src/common/components/modal-popup/theme-popup.js index 19f10d90d..91627f827 100644 --- a/src/common/components/modal-popup/theme-popup.js +++ b/src/common/components/modal-popup/theme-popup.js @@ -88,10 +88,14 @@ function ThemePopup({ params, customThemes, colorScheme, lightTheme, darkTheme, let currentTheme = currentColorScheme === 'light' ? lightTheme : darkTheme; let bg = '#FFFFFF'; let fg = '#000000'; + let lc = '#0000EE'; + let vlc = '#551A8B'; let inv = false; if (currentTheme) { bg = currentTheme.background; fg = currentTheme.foreground; + lc = currentTheme.linkColor || ''; + vlc = currentTheme.visitedLinkColor || ''; inv = currentTheme.invertImages; } @@ -99,6 +103,8 @@ function ThemePopup({ params, customThemes, colorScheme, lightTheme, darkTheme, let [label, setLabel] = useState(params.theme?.label || ''); let [background, setBackground] = useState(params.theme?.background || bg); let [foreground, setForeground] = useState(params.theme?.foreground || fg); + let [linkColor, setLinkColor] = useState(params.theme?.linkColor || params.theme?.foreground || lc); + let [visitedLinkColor, setVisitedLinkColor] = useState(params.theme?.visitedLinkColor || params.theme?.foreground || vlc); let [invertImages, setInvertImages] = useState(params.theme?.invertImages ?? inv); let themeIsDark = isDarkTheme(background, foreground); @@ -136,6 +142,18 @@ function ThemePopup({ params, customThemes, colorScheme, lightTheme, darkTheme, theme.label = label.trim(); theme.background = background; theme.foreground = foreground; + if (linkColor && isValidHexColor(linkColor)) { + theme.linkColor = linkColor; + } + else { + delete theme.linkColor; + } + if (visitedLinkColor && isValidHexColor(visitedLinkColor)) { + theme.visitedLinkColor = visitedLinkColor; + } + else { + delete theme.visitedLinkColor; + } if (invertImages) { theme.invertImages = true; } @@ -181,6 +199,8 @@ function ThemePopup({ params, customThemes, colorScheme, lightTheme, darkTheme, !nameInvalid && isValidHexColor(background) && isValidHexColor(foreground) + && (!linkColor || isValidHexColor(linkColor)) + && (!visitedLinkColor || isValidHexColor(visitedLinkColor)) ); return ( @@ -202,6 +222,10 @@ function ThemePopup({ params, customThemes, colorScheme, lightTheme, darkTheme,
+ +
+ +
diff --git a/src/common/defines.js b/src/common/defines.js index fab092bfe..083fe4700 100644 --- a/src/common/defines.js +++ b/src/common/defines.js @@ -49,12 +49,16 @@ export const INK_ANNOTATION_WIDTH_STEPS = [ export const TEXT_ANNOTATION_FONT_SIZE_STEPS = [6, 8, 10, 12, 14, 18, 24, 36, 48, 64, 72, 96, 144, 192]; export const DEFAULT_THEMES = [ - { id: 'dark', label: 'Dark', background: "#2E3440", foreground: "#D8DEE9" }, - { id: 'black', label: 'Black', background: "#000000", foreground: "#FFFFFF", invertImages: true }, - { id: 'snow', label: 'Snow', background: "#ECEFF4", foreground: "#3B4252" }, - { id: 'sepia', label: 'Sepia', background: "#F4ECD8", foreground: "#5B4636" } + { id: 'dark', label: 'Dark', background: "#2E3440", foreground: "#D8DEE9", linkColor: "#88C0D0", visitedLinkColor: "#6E9FAB" }, + { id: 'black', label: 'Black', background: "#000000", foreground: "#FFFFFF", linkColor: "#6CB6FF", visitedLinkColor: "#5A92CC", invertImages: true }, + { id: 'snow', label: 'Snow', background: "#ECEFF4", foreground: "#3B4252", linkColor: "#4C6E96", visitedLinkColor: "#3D5975" }, + { id: 'sepia', label: 'Sepia', background: "#F4ECD8", foreground: "#5B4636", linkColor: "#8A4F3D", visitedLinkColor: "#6E3F31" } ]; +export const ORIGINAL_THEME = { + id: 'light', label: '', background: "#FFFFFF", foreground: "#121212", linkColor: "#0000EE", visitedLinkColor: "#551A8B" +}; + export const A11Y_VIRT_CURSOR_DEBOUNCE_LENGTH = 500; // ms export const READ_ALOUD_SEGMENT_MAX_LENGTH = 5000; // UTF-8 bytes diff --git a/src/common/types.ts b/src/common/types.ts index 5ad6fc688..f051c8b22 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -289,6 +289,8 @@ export type Theme = { label: string; background: string; foreground: string; + linkColor?: string; + visitedLinkColor?: string; invertImages?: boolean; }; diff --git a/src/dom/common/dom-view.tsx b/src/dom/common/dom-view.tsx index c5ee09444..adc919fe2 100644 --- a/src/dom/common/dom-view.tsx +++ b/src/dom/common/dom-view.tsx @@ -41,6 +41,7 @@ import { import { getSelectionRanges, makeDragImageForTextSelection } from "./lib/selection"; import { FindProcessor } from "./lib/find"; import { + ORIGINAL_THEME, READ_ALOUD_ACTIVE_SEGMENT_COLOR, READ_ALOUD_ACTIVE_SENTENCE_COLOR, SELECTION_COLOR @@ -944,12 +945,7 @@ abstract class DOMView { theme = this._darkTheme; } else { - theme = { - id: 'light', - label: '', - background: '#ffffff', - foreground: '#121212' - }; + theme = { ...ORIGINAL_THEME }; } let themeColorScheme = getModeBasedOnColors(theme.background, theme.foreground); @@ -960,6 +956,8 @@ abstract class DOMView { root.style.colorScheme = themeColorScheme; root.style.setProperty('--background-color', theme.background); root.style.setProperty('--text-color', theme.foreground); + root.style.setProperty('--link-color', theme.linkColor || theme.foreground); + root.style.setProperty('--visited-link-color', theme.visitedLinkColor || theme.linkColor || theme.foreground); } this._theme = theme; diff --git a/src/dom/epub/stylesheets/_content.scss b/src/dom/epub/stylesheets/_content.scss index 884342aab..6af4f4f6a 100644 --- a/src/dom/epub/stylesheets/_content.scss +++ b/src/dom/epub/stylesheets/_content.scss @@ -41,15 +41,6 @@ } } -:root { - --link-color: #0000ee; - --visited-link-color: #551a8b; - - &[data-color-scheme="dark"] { - --link-color: #63caff; - --visited-link-color: #0099e5; - } -} :root { background-color: var(--background-color) !important; diff --git a/src/dom/sdt/stylesheets/sdt.scss b/src/dom/sdt/stylesheets/sdt.scss index e34930830..4dc9aa365 100644 --- a/src/dom/sdt/stylesheets/sdt.scss +++ b/src/dom/sdt/stylesheets/sdt.scss @@ -18,14 +18,6 @@ font-size: 1.1rem; background-color: var(--background-color); color: var(--text-color); - - --link-color: #0000ee; - --visited-link-color: #551a8b; - - &[data-color-scheme="dark"] { - --link-color: #63caff; - --visited-link-color: #0099e5; - } } body { diff --git a/src/dom/snapshot/stylesheets/inject.scss b/src/dom/snapshot/stylesheets/inject.scss index 8968c305a..b97125a1e 100644 --- a/src/dom/snapshot/stylesheets/inject.scss +++ b/src/dom/snapshot/stylesheets/inject.scss @@ -19,4 +19,12 @@ body.force-static-theme { background-color: transparent !important; color: var(--text-color); } + + :link { + color: var(--link-color) !important; + } + + :visited { + color: var(--visited-link-color) !important; + } } diff --git a/src/pdf/pdf-view.js b/src/pdf/pdf-view.js index 9ef26bfa7..0a2ec5f51 100644 --- a/src/pdf/pdf-view.js +++ b/src/pdf/pdf-view.js @@ -58,6 +58,7 @@ import { A11Y_VIRT_CURSOR_DEBOUNCE_LENGTH, MIN_IMAGE_ANNOTATION_SIZE, MIN_TEXT_ANNOTATION_WIDTH, + ORIGINAL_THEME, PDF_NOTE_DIMENSIONS } from '../common/defines'; import { ReadAloudJumpButton } from '../common/read-aloud/jump-button'; @@ -549,16 +550,22 @@ class PDFView { if (this._colorScheme === 'light' && this._lightTheme) { this._iframeWindow.theme = this._lightTheme; root.style.setProperty('--background-color', this._lightTheme.background); + root.style.setProperty('--link-color', this._lightTheme.linkColor || this._lightTheme.foreground); + root.style.setProperty('--visited-link-color', this._lightTheme.visitedLinkColor || this._lightTheme.linkColor || this._lightTheme.foreground); this._themeColorScheme = getModeBasedOnColors(this._lightTheme.background, this._lightTheme.foreground); } else if (this._colorScheme === 'dark' && this._darkTheme) { this._iframeWindow.theme = this._darkTheme; root.style.setProperty('--background-color', this._darkTheme.background); + root.style.setProperty('--link-color', this._darkTheme.linkColor || this._darkTheme.foreground); + root.style.setProperty('--visited-link-color', this._darkTheme.visitedLinkColor || this._darkTheme.linkColor || this._darkTheme.foreground); this._themeColorScheme = getModeBasedOnColors(this._darkTheme.background, this._darkTheme.foreground); } else { this._iframeWindow.theme = null; - root.style.setProperty('--background-color', '#FFFFFF'); + root.style.setProperty('--background-color', ORIGINAL_THEME.background); + root.style.setProperty('--link-color', ORIGINAL_THEME.linkColor); + root.style.setProperty('--visited-link-color', ORIGINAL_THEME.visitedLinkColor); this._themeColorScheme = 'light'; }