diff --git a/packages/page-controller/src/mask/checkDarkMode.ts b/packages/page-controller/src/mask/checkDarkMode.ts
index 17fa5a4..2395fe8 100644
--- a/packages/page-controller/src/mask/checkDarkMode.ts
+++ b/packages/page-controller/src/mask/checkDarkMode.ts
@@ -1,6 +1,25 @@
+/**
+ * A comprehensive function to determine if the page is currently in a dark theme.
+ * Heuristic check. Only work for common patterns. Return false by default.
+ */
+export function isPageDark() {
+ try {
+ if (hasDarkModeClass()) return true
+ if (hasDarkModeDataAttribute()) return true
+ if (isColorSchemeDark()) return true
+ if (isBackgroundDark()) return true
+ if (isMainContentBackgroundDark()) return true
+ if (isTextColorLight()) return true
+
+ return false
+ } catch (error) {
+ console.warn('Error determining if page is dark:', error)
+ return false
+ }
+}
+
/**
* Checks for common dark mode CSS classes on the html or body elements.
- * @returns {boolean} - True if a common dark mode class is found.
*/
function hasDarkModeClass() {
const DEFAULT_DARK_MODE_CLASSES = ['dark', 'dark-mode', 'theme-dark', 'night', 'night-mode']
@@ -15,13 +34,22 @@ function hasDarkModeClass() {
}
}
- // Some sites use data attributes (data-theme, data-color-mode, data-bs-theme, etc.)
- const dataAttrs = ['data-theme', 'data-color-mode', 'data-bs-theme', 'data-color-scheme']
+ return false
+}
+
+/**
+ * Some UI frameworks use data attributes to indicate theme
+ */
+function hasDarkModeDataAttribute() {
+ const htmlElement = document.documentElement
+ const bodyElement = document.body || document.documentElement // can be null in some cases
+
+ const dataAttrs = ['data-theme', 'data-color-mode', 'data-bs-theme', 'data-mui-color-scheme']
for (const attr of dataAttrs) {
const bodyValue = bodyElement?.getAttribute(attr)
const htmlValue = htmlElement.getAttribute(attr)
- if (bodyValue?.toLowerCase().includes('dark') || htmlValue?.toLowerCase().includes('dark')) {
+ if (bodyValue?.toLowerCase() === 'dark' || htmlValue?.toLowerCase() === 'dark') {
return true
}
}
@@ -30,47 +58,23 @@ function hasDarkModeClass() {
}
/**
- * Parses an RGB or RGBA color string and returns an object with r, g, b properties.
- * @param {string} colorString - e.g., "rgb(34, 34, 34)" or "rgba(0, 0, 0, 0.5)"
- * @returns {{r: number, g: number, b: number}|null}
+ * Checks the CSS `color-scheme` property and `` tag.
+ * Only "dark"/"only dark" counts as dark; "light dark" is ambiguous and ignored.
*/
-function parseRgbColor(colorString: string) {
- const rgbMatch = /rgba?\((\d+),\s*(\d+),\s*(\d+)/.exec(colorString)
- if (!rgbMatch) {
- return null // Not a valid rgb/rgba string
- }
- return {
- r: parseInt(rgbMatch[1]),
- g: parseInt(rgbMatch[2]),
- b: parseInt(rgbMatch[3]),
- }
-}
+function isColorSchemeDark() {
+ // Check
+ const meta = document.querySelector('meta[name="color-scheme"]')
+ const metaContent = meta?.content.toLowerCase()
+ if (metaContent === 'dark' || metaContent === 'only dark') return true
-/**
- * Determines if a color is "dark" based on its calculated luminance.
- * @param {string} colorString - The CSS color string (e.g., "rgb(50, 50, 50)").
- * @param {number} threshold - A value between 0 and 255. Colors with luminance below this will be considered dark. Default is 128.
- * @returns {boolean} - True if the color is considered dark.
- */
-function isColorDark(colorString: string, threshold = 128) {
- if (!colorString || colorString === 'transparent' || colorString.startsWith('rgba(0, 0, 0, 0)')) {
- return false // Transparent is not dark
- }
-
- const rgb = parseRgbColor(colorString)
- if (!rgb) {
- return false // Could not parse color
- }
-
- // Calculate perceived luminance using the standard formula
- const luminance = 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b
-
- return luminance < threshold
+ // Check the computed color-scheme CSS property on :root
+ const rootStyle = window.getComputedStyle(document.documentElement)
+ const colorScheme = rootStyle.getPropertyValue('color-scheme').trim().toLowerCase()
+ return colorScheme === 'dark' || colorScheme === 'only dark'
}
/**
* Checks the background color of the body element to determine if the page is dark.
- * @returns {boolean}
*/
function isBackgroundDark() {
// We check both and because some pages set the color on
@@ -92,56 +96,30 @@ function isBackgroundDark() {
return false
}
-/**
- * Checks the CSS `color-scheme` property and `` tag.
- * @returns {boolean | null} - True/false if deterministic, null if inconclusive.
- */
-function getColorSchemePreference(): boolean | null {
- // Check
- const meta = document.querySelector('meta[name="color-scheme"]')
- if (meta) {
- const content = meta.content.toLowerCase()
- // "dark" or "only dark" → dark; "light dark" is ambiguous so skip
- if (content === 'dark' || content === 'only dark') return true
- if (content === 'light' || content === 'only light') return false
- }
-
- // Check the computed color-scheme CSS property on :root
- const rootStyle = window.getComputedStyle(document.documentElement)
- const colorScheme = rootStyle.getPropertyValue('color-scheme').trim().toLowerCase()
- if (colorScheme === 'dark' || colorScheme === 'only dark') return true
- if (colorScheme === 'light' || colorScheme === 'only light') return false
-
- return null
-}
-
/**
* Checks if the text color on the body is light, which implies a dark background.
- * @returns {boolean}
*/
function isTextColorLight() {
- const bodyStyle = window.getComputedStyle(document.body || document.documentElement)
- const textColor = bodyStyle.color
+ /** Luminance (0-255) above which body text is considered light */
+ const LIGHT_TEXT_LUMINANCE = 200
- const rgb = parseRgbColor(textColor)
- if (!rgb) return false
+ const bodyStyle = window.getComputedStyle(document.body || document.documentElement)
+ const luminance = getLuminance(bodyStyle.color)
// Light text has high luminance (e.g. white text on dark bg)
- const luminance = 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b
- return luminance > 180
+ return luminance !== null && luminance > LIGHT_TEXT_LUMINANCE
}
/**
- * Checks the background color of major layout elements (, #app, #root, etc.).
+ * Checks the background color of major layout elements (#app, #root, etc.).
* Many SPAs render into a container that may have its own dark background while
* remains transparent.
- * @returns {boolean}
*/
-function isMainContentDark() {
+function isMainContentBackgroundDark() {
const { innerWidth: vw, innerHeight: vh } = window
const minArea = vw * vh * 0.5
- const selectors = ['main', '#app', '#root', '#__next', '[role="main"]']
+ const selectors = ['#app', '#root', '#__next']
for (const selector of selectors) {
const el = document.querySelector(selector)
if (!el) continue
@@ -154,42 +132,50 @@ function isMainContentDark() {
return false
}
+// --- utils ---
+
/**
- * A comprehensive function to determine if the page is currently in a dark theme.
- * It combines class checking and background color analysis.
- * @returns {boolean} - True if the page is likely dark.
+ * Parses an RGB or RGBA color string and returns an object with r, g, b properties.
+ * @param {string} colorString - e.g., "rgb(34, 34, 34)" or "rgba(0, 0, 0, 0.5)"
+ * @returns {{r: number, g: number, b: number}|null}
*/
-export function isPageDark() {
- try {
- // Strategy 1: Check for common dark mode classes and data attributes
- if (hasDarkModeClass()) {
- return true
- }
-
- // Strategy 2: Check CSS color-scheme property and meta tag
- const colorScheme = getColorSchemePreference()
- if (colorScheme !== null) {
- return colorScheme
- }
-
- // Strategy 3: Analyze the computed background color of /
- if (isBackgroundDark()) {
- return true
- }
-
- // Strategy 4: Check background of major layout containers (, #app, etc.)
- if (isMainContentDark()) {
- return true
- }
-
- // Strategy 5: Check if text color is light (implies dark background)
- if (isTextColorLight()) {
- return true
- }
-
- return false
- } catch (error) {
- console.warn('Error determining if page is dark:', error)
- return false
+function parseRgbColor(colorString: string) {
+ const rgbMatch = /rgba?\((\d+),\s*(\d+),\s*(\d+)/.exec(colorString)
+ if (!rgbMatch) {
+ return null // Not a valid rgb/rgba string
+ }
+ return {
+ r: parseInt(rgbMatch[1]),
+ g: parseInt(rgbMatch[2]),
+ b: parseInt(rgbMatch[3]),
}
}
+
+/**
+ * Calculates the perceived luminance (0-255) of a CSS color string.
+ * @param {string} colorString - e.g., "rgb(50, 50, 50)" or "rgba(0, 0, 0, 0.5)"
+ * @returns {number|null} - The luminance, or null if the color is transparent or unparseable.
+ */
+function getLuminance(colorString: string): number | null {
+ if (!colorString || colorString === 'transparent' || colorString.startsWith('rgba(0, 0, 0, 0)')) {
+ return null // Transparent has no meaningful luminance
+ }
+
+ const rgb = parseRgbColor(colorString)
+ if (!rgb) {
+ return null // Could not parse color
+ }
+
+ // Standard perceived luminance formula
+ return 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b
+}
+
+/**
+ * Determines if a color is "dark" based on its calculated luminance.
+ * @param {string} colorString - The CSS color string (e.g., "rgb(50, 50, 50)").
+ * @param {number} threshold - A value between 0 and 255. Colors with luminance below this will be considered dark. Default is 128.
+ */
+function isColorDark(colorString: string, threshold = 128) {
+ const luminance = getLuminance(colorString)
+ return luminance !== null && luminance < threshold
+}