diff --git a/.gitignore b/.gitignore
index b7afa48..f451a33 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,8 +10,16 @@ build/
*.iml
.DS_Store
node_modules/
+.pnpm-store/
dist/
# Playwright 测试工具 — 只跟踪脚本和配置,忽略输出/截图/缓存
workplace/*
!workplace/*.py
!workplace/*.yaml
+
+# 暗色扫描输出与会话
+scripts/output/
+scripts/.browser-profile/
+
+# Dark Reader 源码下载目录(构建依赖已 vendor 到 ui/vendor/darkreader)
+docs/darkreader/
diff --git a/CLAUDE.md b/CLAUDE.md
index 8b0ea15..542a7ce 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -54,6 +54,7 @@ halo-dark-mode-plugin/
│
├── 前端 (Vue 3 / TypeScript) ← 核心实现
│ ├── index.ts ← definePlugin() 入口,注册路由+组件
+│ ├── darkreader-engine.ts ← Dark Reader 通用暗色引擎
│ ├── injector.ts ← ThemeToggle 侧边栏注入器(MutationObserver 方案)
│ ├── composables/
│ │ ├── useDarkMode.ts ← 单例状态管理(light/dark/auto + localStorage 持久化)
@@ -80,33 +81,39 @@ halo-dark-mode-plugin/
### 主题切换机制
- **触发方式**:`document.documentElement` 上设置/移除 `data-halo-theme="dark"` 属性
+- **主引擎**:Dark Reader(本地 vendored 于 `ui/vendor/darkreader`,API 包由
+ `npm run api` 构建),监听 `useDarkMode().isDark`,深色时 `enable()`,浅色时
+ `disable()`,可自动分析 Halo 核心与第三方插件的动态 DOM
- **CSS 变量体系**:所有颜色通过 `--halo-*` 前缀的 CSS 自定义属性控制,一个语义变量对应一个视觉属性
- **颜色空间**:全部使用 OKLCH(感知均匀,暗色模式天然适配)
- **暗色配色策略**:用亮度层次区分背景(越"高"的层越亮),中性色含微量蓝色调,强调色略降饱和
+- **CSS 覆盖定位**:现有 `overrides/` 仅作为 Dark Reader 的兼容层与兜底,
+ 不再逐页新增手工转换规则;新页面残留统一交给 Dark Reader 处理
### ⚠️ 最关键的教训:Halo 2.25 用 UnoCSS,不是 Tailwind
-Halo 2.25 的 Console 实际使用 **UnoCSS**(hash 类如 `uno-*`)加 **BEM 语义类**。**不要写 `.bg-white`、`.text-gray-900`、`.v-card` 这类 Tailwind/Vuetify 原子选择器**——它们在真实 DOM 中不存在,覆盖会静默失效。
+Halo 2.25 的 Console 实际使用 **UnoCSS**(hash 类如 `uno-*`)加 **BEM 语义aliyun "curl -sI --max-time 5 http://localhost/ 2>&1 |类**。**不要写 `.bg-white`、`.text-gray-900`、`.v-card` 这类 Tailwind/Vuetify 原子选择器**——它们在真实 DOM 中不存在,覆盖会静默失效。
真实 DOM 里的容器类名(已被 `halo-core.css` 覆盖):
-| 语义 | 真实类名 |
-|------|---------|
-| 页面顶栏 | `.page-header` / `.page-header__title-text` |
-| 列表卡片容器 | `.card-wrapper` |
-| 文章/用户列表项标题 | `.entity-field-title` / `.entity-field-title-body` |
-| 分页 | `.pagination` / `.pagination__btn` |
-| 标签 | `.tag-default` / `.tag-content` |
-| 模态框 | `.modal-content` / `.modal-header` / `.modal-body` / `.modal-footer` |
-| 详情页描述项 | `.description-item-wrapper` / `.description-item__label` / `.description-item__content` |
-| Toast | `.toast-container .toast-body` |
-| 用户头像 | `.avatar-wrapper` / `.avatar-circle` |
+| 语义 | 真实类名 |
+| ------------------- | --------------------------------------------------------------------------------------------- |
+| 页面顶栏 | `.page-header` / `.page-header__title-text` |
+| 列表卡片容器 | `.card-wrapper` |
+| 文章/用户列表项标题 | `.entity-field-title` / `.entity-field-title-body` |
+| 分页 | `.pagination` / `.pagination__btn` |
+| 标签 | `.tag-default` / `.tag-content` |
+| 模态框 | `.modal-content` / `.modal-header` / `.modal-body` / `.modal-footer` |
+| 详情页描述项 | `.description-item-wrapper` / `.description-item__label` / `.description-item__content` |
+| Toast | `.toast-container .toast-body` |
+| 用户头像 | `.avatar-wrapper` / `.avatar-circle` |
新增覆盖时:**先到真实环境确认类名,不要凭经验写**。
### 覆盖策略(重写后)
`halo-core.css` 是主要覆盖文件,按语义类精准覆盖。它包含三类规则:
+
1. **BEM 语义类**(如 `.card-wrapper`)— 直接 `[data-halo-theme="dark"] .card-wrapper { background-color: var(--halo-bg-card) }`
2. **通用 UnoCSS 工具类**(如 `.bg-gray-50`、`.hover:text-gray-600`)— 批量覆盖文字/背景/边框
3. **根背景兜底** — `html, body { background-color: var(--halo-bg-body) !important }`(body 不设暗色会在溢出时露白)
@@ -134,6 +141,15 @@ Halo 扩展点系统**没有侧边栏插槽**。`injector.ts` 用 `MutationObser
后端极简 — `DarkModePlugin extends BasePlugin` 仅含 `start()`/`stop()` 生命周期钩子。所有核心逻辑在前端。插件不依赖后端 Setting API。
+## 打包约定(必须)
+
+每次执行 `./gradlew build` 打 JAR 前,**必须先递增版本标签**:
+
+1. 修改 `gradle.properties` 中的 `version`(如 `1.0.2` → `1.0.3`)
+2. 同步更新 `src/main/java/run/halo/darkmode/DarkModePlugin.java` 的 `@since`
+3. 构建完成后核对 `build/libs/plugin-dark-mode-<新版本>.jar` 存在,且 JAR 内的
+ `ui/style.css`、`ui/main.js` 包含本次改动(用关键字检查,例如新增类名)
+4. 向用户交付时明确给出新 JAR 路径和版本号,避免线上仍加载旧 bundle 的混淆
## 验证工作流(必须)
对 CSS 覆盖的任何改动,都要在**真实 Halo 环境**验证,不能只靠 `vite build` 通过:
diff --git a/README.md b/README.md
index e5c6d13..709a5fe 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,8 @@ Halo 2.25 暗色模式插件 — 为 Halo 后台管理面板提供深色/浅色
- ⚡ **瞬间切换**:CSS 变量瞬时生效,无可见闪烁
- 🧩 **侧边栏注入**:切换按钮自动出现在侧边栏底部(UserProfileBanner 上方)
- ⚙️ **设置页面**:提供详细的模式选择界面(菜单 → 偏好设置 → 深色模式)
+- 🔍 **Dark Reader 引擎**:内置成熟的开源暗色转换引擎,自动分析页面 CSS 与 DOM,
+ 第三方插件页面的黑字/白底也能自动转换,不再依赖逐页手工覆盖
- 🎨 **OKLCH 色彩空间**:感知均匀,暗色模式天然适配,WCAG AA 对比度保证
- 📦 **零后端依赖**:纯前端实现,不需要后端 API
- ✅ **覆盖已验证**:仪表盘、内容管理(文章/页面/评论)、链接、订阅、瞬间、用户、主题、设置、编辑器、模态框等页面已针对 Halo 2.25 真实 DOM 逐一验证
@@ -69,6 +71,7 @@ pnpm test:unit # 单元测试
└── ui/
└── src/
├── index.ts # definePlugin 入口
+ ├── darkreader-engine.ts # Dark Reader 通用暗色引擎
├── injector.ts # ThemeToggle 侧边栏注入器
├── composables/
│ ├── useDarkMode.ts # 核心状态管理(模块级单例)
diff --git a/gradle.properties b/gradle.properties
index 07899a3..a30291e 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,3 +1,3 @@
-version=1.0.1
+version=1.0.3
org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8
-org.gradle.java.home=D:/settings/Language/Java/jdk-25.0.1
\ No newline at end of file
+org.gradle.java.home=D:/settings/Language/Java/jdk-25.0.1
diff --git a/scripts/README.md b/scripts/README.md
new file mode 100644
index 0000000..1cc0b0c
--- /dev/null
+++ b/scripts/README.md
@@ -0,0 +1,60 @@
+# 暗色模式回归扫描工具
+
+用 Playwright 驱动 Edge 对 Halo 后台做全路由深浅色残留扫描,验证每次 CSS 改动。
+
+## 依赖
+
+- Python 3.13 环境(本机:`D:\settings\settings\uv\my_uv_env`)
+- Playwright(已安装):`pip install playwright` 或 `uv pip install playwright`
+- 本机 Edge(Playwright 通过 `channel="msedge"` 复用,无需下载浏览器内核)
+
+## 首次使用:登录
+
+```powershell
+D:\settings\settings\uv\my_uv_env\Scripts\python.exe scripts\login-wait.py
+```
+
+会弹出 Edge 窗口,登录 `https://blog.liuhangyv.top/console` 后脚本自动退出,
+会话保存在 `scripts/.browser-profile/`(已被 gitignore,不会入库)。
+
+已有登录态想复用,可显式指定 profile:
+
+```powershell
+D:\settings\settings\uv\my_uv_env\Scripts\python.exe scripts\scan-dark.py --profile workplace\pw-profile
+```
+
+## 常用命令
+
+```powershell
+# 全路由深色扫描,注入本地构建产物做预部署验证,存在残留时返回非 0
+D:\settings\settings\uv\my_uv_env\Scripts\python.exe scripts\scan-dark.py --local-css ui\build\dist\style.css --assert-zero
+
+# 只扫指定页面
+D:\settings\settings\uv\my_uv_env\Scripts\python.exe scripts\scan-dark.py --pages /console/posts,/console/users
+
+# 浅色模式回归抽查
+D:\settings\settings\uv\my_uv_env\Scripts\python.exe scripts\scan-dark.py --mode light --pages /console/dashboard,/console/posts
+```
+
+## 输出
+
+- `scripts/output/scan-results.json`:按路由分组的结果
+- `scripts/output/shots/*.png`:每页截图
+- 均已被 gitignore,不入库。
+
+## 切换行为验证
+
+```powershell
+D:\settings\settings\uv\my_uv_env\Scripts\python.exe scripts\verify-toggle.py
+```
+
+部署新版插件后运行,会检查侧边栏按钮切换、`data-halo-theme` 属性、
+localStorage 持久化以及 Monaco 日志查看器主题是否同步。
+
+## 注意事项
+
+- 扫描会忽略 `img/canvas/video/svg/iframe`,避免把文章图片、编辑器画布等内容性
+ 元素误判为界面残留。
+- 路由从 Vue Router 动态发现,Halo 或插件升级新增页面后无需维护清单。
+- `--local-css` 用 `add_style_tag` 追加本地样式(不替换线上 bundle),可安全验证
+ CSS 修复;JS 改动(如 Monaco 主题同步)需要重新构建并部署后再跑 `verify-toggle.py`。
\ No newline at end of file
diff --git a/scripts/login-wait.py b/scripts/login-wait.py
new file mode 100644
index 0000000..0b21fc2
--- /dev/null
+++ b/scripts/login-wait.py
@@ -0,0 +1,53 @@
+"""打开带持久化配置的 Edge 窗口,等待用户在 Halo 后台完成登录。
+
+登录成功后会话保存在 profile 目录,供 scan-dark.py 复用。
+"""
+import argparse
+import pathlib
+import sys
+import time
+
+from playwright.sync_api import sync_playwright
+
+DEFAULT_PROFILE = pathlib.Path(__file__).parent / ".browser-profile"
+LOGIN_URL = "https://blog.liuhangyv.top/console/login"
+TIMEOUT_S = 280
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description="登录 Halo 后台并保存会话")
+ parser.add_argument("--profile", default=str(DEFAULT_PROFILE))
+ args = parser.parse_args()
+
+ with sync_playwright() as p:
+ ctx = p.chromium.launch_persistent_context(
+ args.profile,
+ channel="msedge",
+ headless=False,
+ viewport={"width": 1600, "height": 950},
+ )
+ page = ctx.pages[0] if ctx.pages else ctx.new_page()
+ page.goto(LOGIN_URL)
+ print("浏览器窗口已打开,请在其中登录 Halo 后台...", flush=True)
+
+ deadline = time.time() + TIMEOUT_S
+ while time.time() < deadline:
+ try:
+ url = page.url
+ except Exception:
+ print("检测到窗口被关闭", flush=True)
+ return 2
+ if "/console" in url and "/login" not in url:
+ time.sleep(3)
+ print(f"检测到登录成功: {url}", flush=True)
+ ctx.close()
+ return 0
+ time.sleep(2)
+
+ print("等待超时,未检测到登录", flush=True)
+ ctx.close()
+ return 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
\ No newline at end of file
diff --git a/scripts/scan-dark.py b/scripts/scan-dark.py
new file mode 100644
index 0000000..8fffcf0
--- /dev/null
+++ b/scripts/scan-dark.py
@@ -0,0 +1,220 @@
+"""Halo 后台暗色模式残留扫描器(正式回归工具)。
+
+用法示例:
+ python scripts/scan-dark.py --local-css ui/build/dist/style.css --assert-zero
+ python scripts/scan-dark.py --mode light --pages /console/posts,/console/users
+ python scripts/scan-dark.py --profile workplace/pw-profile
+"""
+import argparse
+import json
+import pathlib
+import re
+import sys
+import time
+
+from playwright.sync_api import sync_playwright
+
+try:
+ sys.stdout.reconfigure(encoding="utf-8")
+except Exception:
+ pass
+
+BASE = "https://blog.liuhangyv.top"
+DEFAULT_PROFILE = pathlib.Path(__file__).parent / ".browser-profile"
+DEFAULT_OUTPUT = pathlib.Path(__file__).parent / "output"
+
+SCAN_JS = r"""
+() => {
+ const EXCLUDED_TAGS = new Set(['IMG', 'CANVAS', 'VIDEO', 'SVG', 'IFRAME']);
+ const results = [];
+ const seen = new Set();
+ const isVisible = (el) => {
+ const cs = getComputedStyle(el);
+ return cs.display !== 'none' && cs.visibility !== 'hidden' && +cs.opacity > 0.05;
+ };
+ const shortPath = (el) => {
+ const parts = [];
+ let cur = el;
+ for (let i = 0; i < 5 && cur && cur !== document.body; i++) {
+ let p = cur.tagName.toLowerCase();
+ if (cur.id) p += '#' + cur.id;
+ else if (typeof cur.className === 'string' && cur.className.trim()) {
+ p += '.' + cur.className.trim().split(/\s+/).slice(0, 2).join('.');
+ }
+ parts.unshift(p);
+ cur = cur.parentElement;
+ }
+ return parts.join(' > ');
+ };
+ document.querySelectorAll('body *').forEach(el => {
+ if (EXCLUDED_TAGS.has(el.tagName)) return;
+ if (!isVisible(el)) return;
+ const r = el.getBoundingClientRect();
+ if (r.width < 50 || r.height < 20) return;
+ const cs = getComputedStyle(el);
+ const issues = [];
+ const bg = cs.backgroundColor.match(/rgba?\(([\d.]+),\s*([\d.]+),\s*([\d.]+)(?:,\s*([\d.]+))?\)/);
+ if (bg && bg[4] !== '0' && +bg[1] > 235 && +bg[2] > 235 && +bg[3] > 235) {
+ issues.push('light-bg ' + cs.backgroundColor);
+ }
+ const hasText = [...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim());
+ const c = cs.color.match(/rgba?\(([\d.]+),\s*([\d.]+),\s*([\d.]+)(?:,\s*([\d.]+))?\)/);
+ if (hasText && c && (+c[4] ?? 1) > 0.05 && (+c[1] + +c[2] + +c[3]) / 3 < 95) {
+ issues.push('dark-text ' + cs.color);
+ }
+ if (!issues.length) return;
+ const cls = (typeof el.className === 'string' ? el.className : '').trim().replace(/\s+/g, ' ').slice(0, 150);
+ const key = el.tagName + '|' + cls + '|' + issues.join(',');
+ if (seen.has(key)) return;
+ seen.add(key);
+ results.push({
+ tag: el.tagName.toLowerCase(),
+ cls,
+ path: shortPath(el),
+ issues,
+ size: Math.round(r.width) + 'x' + Math.round(r.height),
+ text: (el.textContent || '').trim().slice(0, 40),
+ });
+ });
+ return results;
+}
+"""
+
+
+def slug(route: str) -> str:
+ return re.sub(r"[^a-z0-9]+", "-", route.lower()).strip("-") or "root"
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description="Halo 后台暗色模式残留扫描")
+ parser.add_argument("--base", default=BASE)
+ parser.add_argument("--profile", default=str(DEFAULT_PROFILE))
+ parser.add_argument("--output-dir", default=str(DEFAULT_OUTPUT))
+ parser.add_argument("--mode", choices=["dark", "light"], default="dark")
+ parser.add_argument("--pages", default="", help="逗号分隔的路由子集,默认扫描全部")
+ parser.add_argument("--local-css", default="", help="注入本地构建的 style.css 做预部署验证")
+ parser.add_argument("--assert-zero", action="store_true", help="存在残留时以退出码 1 结束")
+ args = parser.parse_args()
+
+ output = pathlib.Path(args.output_dir)
+ shots = output / "shots"
+ shots.mkdir(parents=True, exist_ok=True)
+
+ if args.mode == "dark":
+ init_js = """
+ try { localStorage.setItem('halo-dark-mode-theme', 'dark'); } catch (e) {}
+ document.documentElement.setAttribute('data-halo-theme', 'dark');
+ """
+ else:
+ init_js = """
+ try { localStorage.setItem('halo-dark-mode-theme', 'light'); } catch (e) {}
+ document.documentElement.removeAttribute('data-halo-theme');
+ """
+
+ with sync_playwright() as p:
+ ctx = p.chromium.launch_persistent_context(
+ args.profile,
+ channel="msedge",
+ headless=True,
+ viewport={"width": 1600, "height": 950},
+ )
+ ctx.add_init_script(init_js)
+ page = ctx.pages[0] if ctx.pages else ctx.new_page()
+ page.goto(args.base + "/console/dashboard", wait_until="domcontentloaded")
+ try:
+ page.wait_for_load_state("networkidle", timeout=10000)
+ except Exception:
+ pass
+ try:
+ page.wait_for_selector("#app > *, .sidebar, .main-content", timeout=20000)
+ except Exception:
+ pass
+ time.sleep(2)
+ if "/login" in page.url:
+ print("SESSION_EXPIRED 登录态失效,请先运行 scripts/login-wait.py 登录")
+ ctx.close()
+ return 3
+
+ routes = []
+ try:
+ routes = page.evaluate(
+ """() => {
+ const app = document.querySelector('#app').__vue_app__;
+ const router = app.config.globalProperties.$router;
+ return router.getRoutes().map(r => r.path);
+ }"""
+ )
+ except Exception:
+ routes = page.evaluate(
+ """() => [...new Set([...document.querySelectorAll('a[href]')]
+ .map(a => a.getAttribute('href'))
+ .filter(h => h && h.startsWith('/console')))]"""
+ )
+ routes = sorted(
+ {
+ r if r.startswith("/console") else "/console" + r
+ for r in routes
+ if r.startswith("/") and ":" not in r and r not in ("/", "/console")
+ }
+ )
+ editor = [r for r in routes if "editor" in r]
+ routes = [r for r in routes if "editor" not in r] + editor[:1]
+ if args.pages:
+ wanted = [r.strip() for r in args.pages.split(",") if r.strip()]
+ routes = [r for r in wanted if r.startswith("/console")]
+
+ print(f"模式={args.mode} 待扫描路由数={len(routes)}", flush=True)
+ all_results = {}
+ for route in routes:
+ try:
+ last_error = None
+ for attempt in range(3):
+ try:
+ page.goto(args.base + route, wait_until="domcontentloaded")
+ last_error = None
+ break
+ except Exception as e:
+ last_error = e
+ print(f"{route}: 第 {attempt + 1} 次导航失败,稍后重试", flush=True)
+ time.sleep(8)
+ if last_error:
+ raise last_error
+
+ try:
+ page.wait_for_load_state("networkidle", timeout=8000)
+ except Exception:
+ pass
+ # 限流时 Halo bundle 可能加载较慢,确保应用渲染完成再扫描
+ page.wait_for_selector("#app > *, .sidebar, .main-content", timeout=20000)
+ if args.local_css:
+ page.add_style_tag(path=args.local_css)
+ time.sleep(2)
+ if args.mode == "dark":
+ page.evaluate("document.documentElement.setAttribute('data-halo-theme','dark')")
+ else:
+ page.evaluate("document.documentElement.removeAttribute('data-halo-theme')")
+ time.sleep(0.3)
+ items = page.evaluate(SCAN_JS)
+ all_results[route] = items
+ page.screenshot(path=str(shots / (slug(route) + ".png")))
+ print(f"{route}: {len(items)} 处疑似残留", flush=True)
+ except Exception as e:
+ all_results[route] = {"error": str(e)[:200]}
+ print(f"{route}: 扫描失败 {e}", flush=True)
+
+ out_json = output / "scan-results.json"
+ out_json.write_text(json.dumps(all_results, ensure_ascii=False, indent=2), encoding="utf-8")
+ ctx.close()
+
+ total = sum(
+ len(v) for v in all_results.values() if isinstance(v, list)
+ )
+ dirty = sum(1 for v in all_results.values() if isinstance(v, list) and v)
+ print(f"DONE 残留条目={total} 残留路由={dirty} 结果文件={out_json}", flush=True)
+ if args.assert_zero and dirty:
+ return 1
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
\ No newline at end of file
diff --git a/scripts/verify-toggle.py b/scripts/verify-toggle.py
new file mode 100644
index 0000000..1416aec
--- /dev/null
+++ b/scripts/verify-toggle.py
@@ -0,0 +1,76 @@
+"""验证侧边栏主题切换:点击后检查 data-halo-theme、localStorage 与 Monaco 主题同步。
+
+部署新版插件后运行可获得完整结果;预部署阶段 Monaco 断言会提示跳过。
+"""
+import argparse
+import pathlib
+import sys
+import time
+
+from playwright.sync_api import sync_playwright
+
+DEFAULT_PROFILE = pathlib.Path(__file__).parent / ".browser-profile"
+BASE = "https://blog.liuhangyv.top"
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description="验证主题切换运行时行为")
+ parser.add_argument("--base", default=BASE)
+ parser.add_argument("--profile", default=str(DEFAULT_PROFILE))
+ args = parser.parse_args()
+
+ with sync_playwright() as p:
+ ctx = p.chromium.launch_persistent_context(
+ args.profile, channel="msedge", headless=True,
+ viewport={"width": 1600, "height": 950},
+ )
+ page = ctx.pages[0] if ctx.pages else ctx.new_page()
+ page.goto(args.base + "/console/dashboard", wait_until="domcontentloaded")
+ try:
+ page.wait_for_load_state("networkidle", timeout=10000)
+ except Exception:
+ pass
+ time.sleep(3)
+ if "/login" in page.url:
+ print("SESSION_EXPIRED 请先运行 scripts/login-wait.py")
+ ctx.close()
+ return 3
+
+ toggle = page.locator(".theme-toggle")
+ toggle.wait_for(state="visible", timeout=15000)
+ before = page.evaluate("() => ({ attr: document.documentElement.getAttribute('data-halo-theme'), stored: localStorage.getItem('halo-dark-mode-theme') })")
+ toggle.click()
+ time.sleep(0.5)
+ after = page.evaluate("() => ({ attr: document.documentElement.getAttribute('data-halo-theme'), stored: localStorage.getItem('halo-dark-mode-theme') })")
+ toggle.click()
+ time.sleep(0.5)
+ restored = page.evaluate("() => ({ attr: document.documentElement.getAttribute('data-halo-theme'), stored: localStorage.getItem('halo-dark-mode-theme') })")
+
+ attr_flipped = before["attr"] != after["attr"] and before["attr"] == restored["attr"]
+ stored_flipped = before["stored"] != after["stored"] and before["stored"] == restored["stored"]
+ print(f"初始: {before}")
+ print(f"切换: {after}")
+ print(f"还原: {restored}")
+ print(f"属性翻转: {'PASS' if attr_flipped else 'FAIL'} 存储翻转: {'PASS' if stored_flipped else 'FAIL'}")
+
+ page.goto(args.base + "/console/log-viewer", wait_until="domcontentloaded")
+ try:
+ page.wait_for_load_state("networkidle", timeout=6000)
+ except Exception:
+ pass
+ time.sleep(2)
+ has_monaco = page.locator(".monaco-editor").count() > 0
+ if has_monaco:
+ dark_attr = page.evaluate("document.documentElement.getAttribute('data-halo-theme')") == "dark"
+ theme_cls = page.evaluate("() => { const el = document.querySelector('.monaco-editor'); return el ? el.className : '' }")
+ monaco_dark = "vs-dark" in theme_cls
+ print(f"Monaco 实例: 存在 | 属性dark={dark_attr} | 主题类含vs-dark={monaco_dark} | class={theme_cls[:60]}")
+ print(f"Monaco 主题同步: {'PASS' if (dark_attr and monaco_dark) or (not dark_attr and not monaco_dark) else 'FAIL(可能部署的还是旧版 JS)'}")
+ else:
+ print("Monaco 实例: 未检测到,跳过主题断言(日志页面可能未加载或路由不同)")
+ ctx.close()
+ return 0 if attr_flipped and stored_flipped else 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
\ No newline at end of file
diff --git a/src/main/java/run/halo/darkmode/DarkModePlugin.java b/src/main/java/run/halo/darkmode/DarkModePlugin.java
index 9955eda..3ab1522 100644
--- a/src/main/java/run/halo/darkmode/DarkModePlugin.java
+++ b/src/main/java/run/halo/darkmode/DarkModePlugin.java
@@ -10,7 +10,7 @@ import run.halo.app.plugin.PluginContext;
*
Only one main class extending {@link BasePlugin} is allowed per plugin.
*
* @author LHY
- * @since 1.0.1
+ * @since 1.0.3
*/
@Component
public class DarkModePlugin extends BasePlugin {
diff --git a/ui/build.gradle b/ui/build.gradle
index f97bde3..1bd34ea 100644
--- a/ui/build.gradle
+++ b/ui/build.gradle
@@ -5,6 +5,11 @@ plugins {
group 'run.halo.darkmode.ui'
+node {
+ // 使用系统安装的 Node.js,避免 Gradle 下载的本地 Node 目录缺失时失败
+ download = false
+}
+
tasks.register('pnpmBuild', PnpmTask) {
group = 'build'
description = 'Build the UI project using pnpm'
diff --git a/ui/package.json b/ui/package.json
index 8827382..46ef037 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -23,10 +23,10 @@
"@halo-dev/ui-shared": "^2.25.1",
"axios": "^1.13.5",
"canvas-confetti": "^1.9.3",
+ "darkreader": "file:./vendor/darkreader",
"vue": "^3.5.28"
},
"devDependencies": {
- "vite": "^8.0.16",
"@halo-dev/ui-plugin-bundler-kit": "^2.25.1",
"@iconify-json/ri": "^1.2.10",
"@tsconfig/node20": "^20.1.6",
@@ -41,6 +41,7 @@
"eslint": "^9.29.0",
"eslint-plugin-oxlint": "^0.16.12",
"eslint-plugin-vue": "~10.0.1",
+ "jiti": "^2.7.0",
"jsdom": "^26.1.0",
"npm-run-all2": "^7.0.2",
"oxlint": "^0.16.12",
@@ -48,6 +49,7 @@
"sass": "^1.89.2",
"typescript": "~5.8.3",
"unplugin-icons": "^23.0.1",
+ "vite": "^8.0.16",
"vitest": "^4.1.0",
"vue-tsc": "^3.3.3"
},
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
index e28b0d0..302a28d 100644
--- a/ui/pnpm-lock.yaml
+++ b/ui/pnpm-lock.yaml
@@ -13,23 +13,26 @@ importers:
version: 2.25.2(axios@1.19.0)
'@halo-dev/components':
specifier: ^2.25.1
- version: 2.25.2(vue-router@5.2.0(@rspack/core@2.1.8(@swc/helpers@0.5.23))(@vue/compiler-sfc@3.5.41)(rolldown@1.2.3)(vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.8.3)))(vue@3.5.41(typescript@5.8.3))
+ version: 2.25.2(vue-router@5.2.0(@rspack/core@2.1.8(@swc/helpers@0.5.23))(@vue/compiler-sfc@3.5.41)(rolldown@1.2.3)(vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.8.3)))(vue@3.5.41(typescript@5.8.3))
'@halo-dev/ui-shared':
specifier: ^2.25.1
- version: 2.25.2(@tiptap/pm@3.29.2)(axios@1.19.0)(vue-router@5.2.0(@rspack/core@2.1.8(@swc/helpers@0.5.23))(@vue/compiler-sfc@3.5.41)(rolldown@1.2.3)(vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.8.3)))(vue@3.5.41(typescript@5.8.3))
+ version: 2.25.2(@tiptap/pm@3.29.2)(axios@1.19.0)(vue-router@5.2.0(@rspack/core@2.1.8(@swc/helpers@0.5.23))(@vue/compiler-sfc@3.5.41)(rolldown@1.2.3)(vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.8.3)))(vue@3.5.41(typescript@5.8.3))
axios:
specifier: ^1.13.5
version: 1.19.0
canvas-confetti:
specifier: ^1.9.3
version: 1.9.4
+ darkreader:
+ specifier: file:./vendor/darkreader
+ version: file:vendor/darkreader
vue:
specifier: ^3.5.28
version: 3.5.41(typescript@5.8.3)
devDependencies:
'@halo-dev/ui-plugin-bundler-kit':
specifier: ^2.25.1
- version: 2.25.2(@rsbuild/core@2.1.10)(@rsbuild/plugin-vue@2.0.1(@rsbuild/core@2.1.10)(@rspack/core@2.1.8(@swc/helpers@0.5.23))(@vue/compiler-sfc@3.5.41)(vue@3.5.41(typescript@5.8.3)))(@vitejs/plugin-vue@6.0.8(vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.8.3)))(axios@1.19.0)(vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0))
+ version: 2.25.2(@rsbuild/core@2.1.10)(@rsbuild/plugin-vue@2.0.1(@rsbuild/core@2.1.10)(@rspack/core@2.1.8(@swc/helpers@0.5.23))(@vue/compiler-sfc@3.5.41)(vue@3.5.41(typescript@5.8.3)))(@vitejs/plugin-vue@6.0.8(vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.8.3)))(axios@1.19.0)(vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0))
'@iconify-json/ri':
specifier: ^1.2.10
version: 1.2.10
@@ -47,13 +50,13 @@ importers:
version: 24.13.3
'@vitest/eslint-plugin':
specifier: ^1.2.7
- version: 1.6.26(@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5)(typescript@5.8.3))(eslint@9.39.5)(typescript@5.8.3))(eslint@9.39.5)(typescript@5.8.3)(vitest@4.1.10(@types/node@24.13.3)(jsdom@26.1.0)(vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0)))
+ version: 1.6.26(@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3)(vitest@4.1.10(@types/node@24.13.3)(jsdom@26.1.0)(vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0)))
'@vue/eslint-config-prettier':
specifier: ^10.2.0
- version: 10.2.0(eslint@9.39.5)(prettier@3.9.6)
+ version: 10.2.0(eslint@9.39.5(jiti@2.7.0))(prettier@3.9.6)
'@vue/eslint-config-typescript':
specifier: ^14.5.1
- version: 14.9.0(eslint-plugin-vue@10.0.1(eslint@9.39.5)(vue-eslint-parser@10.4.1(eslint@9.39.5)))(eslint@9.39.5)(typescript@5.8.3)
+ version: 14.9.0(eslint-plugin-vue@10.0.1(eslint@9.39.5(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@9.39.5(jiti@2.7.0))))(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3)
'@vue/test-utils':
specifier: ^2.4.6
version: 2.4.11(@vue/compiler-dom@3.5.41)(@vue/server-renderer@3.5.41)(vue@3.5.41(typescript@5.8.3))
@@ -62,13 +65,16 @@ importers:
version: 0.7.0(typescript@5.8.3)(vue@3.5.41(typescript@5.8.3))
eslint:
specifier: ^9.29.0
- version: 9.39.5
+ version: 9.39.5(jiti@2.7.0)
eslint-plugin-oxlint:
specifier: ^0.16.12
version: 0.16.12
eslint-plugin-vue:
specifier: ~10.0.1
- version: 10.0.1(eslint@9.39.5)(vue-eslint-parser@10.4.1(eslint@9.39.5))
+ version: 10.0.1(eslint@9.39.5(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@9.39.5(jiti@2.7.0)))
+ jiti:
+ specifier: ^2.7.0
+ version: 2.7.0
jsdom:
specifier: ^26.1.0
version: 26.1.0
@@ -92,10 +98,10 @@ importers:
version: 23.0.1(@vue/compiler-sfc@3.5.41)
vite:
specifier: ^8.0.16
- version: 8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0)
+ version: 8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0)
vitest:
specifier: ^4.1.0
- version: 4.1.10(@types/node@24.13.3)(jsdom@26.1.0)(vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0))
+ version: 4.1.10(@types/node@24.13.3)(jsdom@26.1.0)(vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0))
vue-tsc:
specifier: ^3.3.3
version: 3.3.9(typescript@5.8.3)
@@ -1079,6 +1085,9 @@ packages:
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+ darkreader@file:vendor/darkreader:
+ resolution: {directory: vendor/darkreader, type: directory}
+
data-urls@5.0.0:
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
engines: {node: '>=18'}
@@ -1454,6 +1463,10 @@ packages:
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
+ jiti@2.7.0:
+ resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
+ hasBin: true
+
js-beautify@1.15.4:
resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==}
engines: {node: '>=14'}
@@ -2427,9 +2440,9 @@ snapshots:
tslib: 2.8.1
optional: true
- '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5)':
+ '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(jiti@2.7.0))':
dependencies:
- eslint: 9.39.5
+ eslint: 9.39.5(jiti@2.7.0)
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
@@ -2488,33 +2501,33 @@ snapshots:
axios: 1.19.0
qs: 6.15.3
- '@halo-dev/components@2.25.2(vue-router@5.2.0(@rspack/core@2.1.8(@swc/helpers@0.5.23))(@vue/compiler-sfc@3.5.41)(rolldown@1.2.3)(vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.8.3)))(vue@3.5.41(typescript@5.8.3))':
+ '@halo-dev/components@2.25.2(vue-router@5.2.0(@rspack/core@2.1.8(@swc/helpers@0.5.23))(@vue/compiler-sfc@3.5.41)(rolldown@1.2.3)(vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.8.3)))(vue@3.5.41(typescript@5.8.3))':
dependencies:
floating-vue: 5.2.2(vue@3.5.41(typescript@5.8.3))
vue: 3.5.41(typescript@5.8.3)
- vue-router: 5.2.0(@rspack/core@2.1.8(@swc/helpers@0.5.23))(@vue/compiler-sfc@3.5.41)(rolldown@1.2.3)(vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.8.3))
+ vue-router: 5.2.0(@rspack/core@2.1.8(@swc/helpers@0.5.23))(@vue/compiler-sfc@3.5.41)(rolldown@1.2.3)(vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.8.3))
transitivePeerDependencies:
- '@nuxt/kit'
- '@halo-dev/ui-plugin-bundler-kit@2.25.2(@rsbuild/core@2.1.10)(@rsbuild/plugin-vue@2.0.1(@rsbuild/core@2.1.10)(@rspack/core@2.1.8(@swc/helpers@0.5.23))(@vue/compiler-sfc@3.5.41)(vue@3.5.41(typescript@5.8.3)))(@vitejs/plugin-vue@6.0.8(vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.8.3)))(axios@1.19.0)(vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0))':
+ '@halo-dev/ui-plugin-bundler-kit@2.25.2(@rsbuild/core@2.1.10)(@rsbuild/plugin-vue@2.0.1(@rsbuild/core@2.1.10)(@rspack/core@2.1.8(@swc/helpers@0.5.23))(@vue/compiler-sfc@3.5.41)(vue@3.5.41(typescript@5.8.3)))(@vitejs/plugin-vue@6.0.8(vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.8.3)))(axios@1.19.0)(vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0))':
dependencies:
'@halo-dev/api-client': 2.25.2(axios@1.19.0)
'@rsbuild/core': 2.1.10
'@rsbuild/plugin-vue': 2.0.1(@rsbuild/core@2.1.10)(@rspack/core@2.1.8(@swc/helpers@0.5.23))(@vue/compiler-sfc@3.5.41)(vue@3.5.41(typescript@5.8.3))
- '@vitejs/plugin-vue': 6.0.8(vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.8.3))
+ '@vitejs/plugin-vue': 6.0.8(vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.8.3))
js-yaml: 4.3.1
semver: 7.8.5
- vite: 8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0)
+ vite: 8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0)
transitivePeerDependencies:
- axios
- '@halo-dev/ui-shared@2.25.2(@tiptap/pm@3.29.2)(axios@1.19.0)(vue-router@5.2.0(@rspack/core@2.1.8(@swc/helpers@0.5.23))(@vue/compiler-sfc@3.5.41)(rolldown@1.2.3)(vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.8.3)))(vue@3.5.41(typescript@5.8.3))':
+ '@halo-dev/ui-shared@2.25.2(@tiptap/pm@3.29.2)(axios@1.19.0)(vue-router@5.2.0(@rspack/core@2.1.8(@swc/helpers@0.5.23))(@vue/compiler-sfc@3.5.41)(rolldown@1.2.3)(vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.8.3)))(vue@3.5.41(typescript@5.8.3))':
dependencies:
'@halo-dev/api-client': 2.25.2(axios@1.19.0)
'@tiptap/core': 3.29.2(@tiptap/pm@3.29.2)
mitt: 3.0.1
vue: 3.5.41(typescript@5.8.3)
- vue-router: 5.2.0(@rspack/core@2.1.8(@swc/helpers@0.5.23))(@vue/compiler-sfc@3.5.41)(rolldown@1.2.3)(vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.8.3))
+ vue-router: 5.2.0(@rspack/core@2.1.8(@swc/helpers@0.5.23))(@vue/compiler-sfc@3.5.41)(rolldown@1.2.3)(vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.8.3))
transitivePeerDependencies:
- '@tiptap/pm'
- axios
@@ -2870,15 +2883,15 @@ snapshots:
'@types/tough-cookie@4.0.5': {}
- '@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5)(typescript@5.8.3))(eslint@9.39.5)(typescript@5.8.3)':
+ '@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
- '@typescript-eslint/parser': 8.66.0(eslint@9.39.5)(typescript@5.8.3)
+ '@typescript-eslint/parser': 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3)
'@typescript-eslint/scope-manager': 8.66.0
- '@typescript-eslint/type-utils': 8.66.0(eslint@9.39.5)(typescript@5.8.3)
- '@typescript-eslint/utils': 8.66.0(eslint@9.39.5)(typescript@5.8.3)
+ '@typescript-eslint/type-utils': 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3)
+ '@typescript-eslint/utils': 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3)
'@typescript-eslint/visitor-keys': 8.66.0
- eslint: 9.39.5
+ eslint: 9.39.5(jiti@2.7.0)
ignore: 7.0.6
natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@5.8.3)
@@ -2886,14 +2899,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/parser@8.66.0(eslint@9.39.5)(typescript@5.8.3)':
+ '@typescript-eslint/parser@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.66.0
'@typescript-eslint/types': 8.66.0
'@typescript-eslint/typescript-estree': 8.66.0(typescript@5.8.3)
'@typescript-eslint/visitor-keys': 8.66.0
debug: 4.4.3
- eslint: 9.39.5
+ eslint: 9.39.5(jiti@2.7.0)
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
@@ -2916,13 +2929,13 @@ snapshots:
dependencies:
typescript: 5.8.3
- '@typescript-eslint/type-utils@8.66.0(eslint@9.39.5)(typescript@5.8.3)':
+ '@typescript-eslint/type-utils@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3)':
dependencies:
'@typescript-eslint/types': 8.66.0
'@typescript-eslint/typescript-estree': 8.66.0(typescript@5.8.3)
- '@typescript-eslint/utils': 8.66.0(eslint@9.39.5)(typescript@5.8.3)
+ '@typescript-eslint/utils': 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3)
debug: 4.4.3
- eslint: 9.39.5
+ eslint: 9.39.5(jiti@2.7.0)
ts-api-utils: 2.5.0(typescript@5.8.3)
typescript: 5.8.3
transitivePeerDependencies:
@@ -2945,13 +2958,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/utils@8.66.0(eslint@9.39.5)(typescript@5.8.3)':
+ '@typescript-eslint/utils@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3)':
dependencies:
- '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5)
+ '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0))
'@typescript-eslint/scope-manager': 8.66.0
'@typescript-eslint/types': 8.66.0
'@typescript-eslint/typescript-estree': 8.66.0(typescript@5.8.3)
- eslint: 9.39.5
+ eslint: 9.39.5(jiti@2.7.0)
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
@@ -2961,21 +2974,21 @@ snapshots:
'@typescript-eslint/types': 8.66.0
eslint-visitor-keys: 5.0.1
- '@vitejs/plugin-vue@6.0.8(vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.8.3))':
+ '@vitejs/plugin-vue@6.0.8(vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.8.3))':
dependencies:
'@rolldown/pluginutils': 1.0.1
- vite: 8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0)
+ vite: 8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0)
vue: 3.5.41(typescript@5.8.3)
- '@vitest/eslint-plugin@1.6.26(@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5)(typescript@5.8.3))(eslint@9.39.5)(typescript@5.8.3))(eslint@9.39.5)(typescript@5.8.3)(vitest@4.1.10(@types/node@24.13.3)(jsdom@26.1.0)(vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0)))':
+ '@vitest/eslint-plugin@1.6.26(@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3)(vitest@4.1.10(@types/node@24.13.3)(jsdom@26.1.0)(vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0)))':
dependencies:
'@typescript-eslint/scope-manager': 8.66.0
- '@typescript-eslint/utils': 8.66.0(eslint@9.39.5)(typescript@5.8.3)
- eslint: 9.39.5
+ '@typescript-eslint/utils': 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3)
+ eslint: 9.39.5(jiti@2.7.0)
optionalDependencies:
- '@typescript-eslint/eslint-plugin': 8.66.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5)(typescript@5.8.3))(eslint@9.39.5)(typescript@5.8.3)
+ '@typescript-eslint/eslint-plugin': 8.66.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3)
typescript: 5.8.3
- vitest: 4.1.10(@types/node@24.13.3)(jsdom@26.1.0)(vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0))
+ vitest: 4.1.10(@types/node@24.13.3)(jsdom@26.1.0)(vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0))
transitivePeerDependencies:
- supports-color
@@ -2988,13 +3001,13 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.1.1
- '@vitest/mocker@4.1.10(vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0))':
+ '@vitest/mocker@4.1.10(vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0))':
dependencies:
'@vitest/spy': 4.1.10
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
- vite: 8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0)
+ vite: 8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0)
'@vitest/pretty-format@4.1.10':
dependencies:
@@ -3085,23 +3098,23 @@ snapshots:
'@vue/devtools-shared@8.2.1': {}
- '@vue/eslint-config-prettier@10.2.0(eslint@9.39.5)(prettier@3.9.6)':
+ '@vue/eslint-config-prettier@10.2.0(eslint@9.39.5(jiti@2.7.0))(prettier@3.9.6)':
dependencies:
- eslint: 9.39.5
- eslint-config-prettier: 10.1.8(eslint@9.39.5)
- eslint-plugin-prettier: 5.5.6(eslint-config-prettier@10.1.8(eslint@9.39.5))(eslint@9.39.5)(prettier@3.9.6)
+ eslint: 9.39.5(jiti@2.7.0)
+ eslint-config-prettier: 10.1.8(eslint@9.39.5(jiti@2.7.0))
+ eslint-plugin-prettier: 5.5.6(eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0))(prettier@3.9.6)
prettier: 3.9.6
transitivePeerDependencies:
- '@types/eslint'
- '@vue/eslint-config-typescript@14.9.0(eslint-plugin-vue@10.0.1(eslint@9.39.5)(vue-eslint-parser@10.4.1(eslint@9.39.5)))(eslint@9.39.5)(typescript@5.8.3)':
+ '@vue/eslint-config-typescript@14.9.0(eslint-plugin-vue@10.0.1(eslint@9.39.5(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@9.39.5(jiti@2.7.0))))(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3)':
dependencies:
- '@typescript-eslint/utils': 8.66.0(eslint@9.39.5)(typescript@5.8.3)
- eslint: 9.39.5
- eslint-plugin-vue: 10.0.1(eslint@9.39.5)(vue-eslint-parser@10.4.1(eslint@9.39.5))
+ '@typescript-eslint/utils': 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3)
+ eslint: 9.39.5(jiti@2.7.0)
+ eslint-plugin-vue: 10.0.1(eslint@9.39.5(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@9.39.5(jiti@2.7.0)))
fast-glob: 3.3.3
- typescript-eslint: 8.66.0(eslint@9.39.5)(typescript@5.8.3)
- vue-eslint-parser: 10.4.1(eslint@9.39.5)
+ typescript-eslint: 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3)
+ vue-eslint-parser: 10.4.1(eslint@9.39.5(jiti@2.7.0))
optionalDependencies:
typescript: 5.8.3
transitivePeerDependencies:
@@ -3307,6 +3320,8 @@ snapshots:
csstype@3.2.3: {}
+ darkreader@file:vendor/darkreader: {}
+
data-urls@5.0.0:
dependencies:
whatwg-mimetype: 4.0.0
@@ -3366,32 +3381,32 @@ snapshots:
escape-string-regexp@4.0.0: {}
- eslint-config-prettier@10.1.8(eslint@9.39.5):
+ eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)):
dependencies:
- eslint: 9.39.5
+ eslint: 9.39.5(jiti@2.7.0)
eslint-plugin-oxlint@0.16.12:
dependencies:
jsonc-parser: 3.3.1
- eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@9.39.5))(eslint@9.39.5)(prettier@3.9.6):
+ eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0))(prettier@3.9.6):
dependencies:
- eslint: 9.39.5
+ eslint: 9.39.5(jiti@2.7.0)
prettier: 3.9.6
prettier-linter-helpers: 1.0.1
synckit: 0.11.13
optionalDependencies:
- eslint-config-prettier: 10.1.8(eslint@9.39.5)
+ eslint-config-prettier: 10.1.8(eslint@9.39.5(jiti@2.7.0))
- eslint-plugin-vue@10.0.1(eslint@9.39.5)(vue-eslint-parser@10.4.1(eslint@9.39.5)):
+ eslint-plugin-vue@10.0.1(eslint@9.39.5(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@9.39.5(jiti@2.7.0))):
dependencies:
- '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5)
- eslint: 9.39.5
+ '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0))
+ eslint: 9.39.5(jiti@2.7.0)
natural-compare: 1.4.0
nth-check: 2.1.1
postcss-selector-parser: 6.1.4
semver: 7.8.5
- vue-eslint-parser: 10.4.1(eslint@9.39.5)
+ vue-eslint-parser: 10.4.1(eslint@9.39.5(jiti@2.7.0))
xml-name-validator: 4.0.0
eslint-scope@8.4.0:
@@ -3412,9 +3427,9 @@ snapshots:
eslint-visitor-keys@5.0.1: {}
- eslint@9.39.5:
+ eslint@9.39.5(jiti@2.7.0):
dependencies:
- '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5)
+ '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0))
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.21.2
'@eslint/config-helpers': 0.4.2
@@ -3448,6 +3463,8 @@ snapshots:
minimatch: 3.1.5
natural-compare: 1.4.0
optionator: 0.9.4
+ optionalDependencies:
+ jiti: 2.7.0
transitivePeerDependencies:
- supports-color
@@ -3676,6 +3693,8 @@ snapshots:
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
+ jiti@2.7.0: {}
+
js-beautify@1.15.4:
dependencies:
config-chain: 1.1.13
@@ -4259,13 +4278,13 @@ snapshots:
dependencies:
prelude-ls: 1.2.1
- typescript-eslint@8.66.0(eslint@9.39.5)(typescript@5.8.3):
+ typescript-eslint@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3):
dependencies:
- '@typescript-eslint/eslint-plugin': 8.66.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5)(typescript@5.8.3))(eslint@9.39.5)(typescript@5.8.3)
- '@typescript-eslint/parser': 8.66.0(eslint@9.39.5)(typescript@5.8.3)
+ '@typescript-eslint/eslint-plugin': 8.66.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3)
+ '@typescript-eslint/parser': 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3)
'@typescript-eslint/typescript-estree': 8.66.0(typescript@5.8.3)
- '@typescript-eslint/utils': 8.66.0(eslint@9.39.5)(typescript@5.8.3)
- eslint: 9.39.5
+ '@typescript-eslint/utils': 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3)
+ eslint: 9.39.5(jiti@2.7.0)
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
@@ -4298,7 +4317,7 @@ snapshots:
picomatch: 4.0.5
webpack-virtual-modules: 0.6.2
- unplugin@3.3.0(@rspack/core@2.1.8(@swc/helpers@0.5.23))(rolldown@1.2.3)(vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0)):
+ unplugin@3.3.0(@rspack/core@2.1.8(@swc/helpers@0.5.23))(rolldown@1.2.3)(vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0)):
dependencies:
'@jridgewell/remapping': 2.3.5
picomatch: 4.0.5
@@ -4306,7 +4325,7 @@ snapshots:
optionalDependencies:
'@rspack/core': 2.1.8(@swc/helpers@0.5.23)
rolldown: 1.2.3
- vite: 8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0)
+ vite: 8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0)
uri-js@4.4.1:
dependencies:
@@ -4314,7 +4333,7 @@ snapshots:
util-deprecate@1.0.2: {}
- vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0):
+ vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0):
dependencies:
lightningcss: 1.33.0
picomatch: 4.0.5
@@ -4324,13 +4343,14 @@ snapshots:
optionalDependencies:
'@types/node': 24.13.3
fsevents: 2.3.3
+ jiti: 2.7.0
sass: 1.102.0
yaml: 2.9.0
- vitest@4.1.10(@types/node@24.13.3)(jsdom@26.1.0)(vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0)):
+ vitest@4.1.10(@types/node@24.13.3)(jsdom@26.1.0)(vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.10
- '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0))
+ '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0))
'@vitest/pretty-format': 4.1.10
'@vitest/runner': 4.1.10
'@vitest/snapshot': 4.1.10
@@ -4347,7 +4367,7 @@ snapshots:
tinyexec: 1.3.0
tinyglobby: 0.2.17
tinyrainbow: 3.1.1
- vite: 8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0)
+ vite: 8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 24.13.3
@@ -4359,10 +4379,10 @@ snapshots:
vue-component-type-helpers@3.3.9: {}
- vue-eslint-parser@10.4.1(eslint@9.39.5):
+ vue-eslint-parser@10.4.1(eslint@9.39.5(jiti@2.7.0)):
dependencies:
debug: 4.4.3
- eslint: 9.39.5
+ eslint: 9.39.5(jiti@2.7.0)
eslint-scope: 9.1.2
eslint-visitor-keys: 5.0.1
espree: 11.2.0
@@ -4375,7 +4395,7 @@ snapshots:
dependencies:
vue: 3.5.41(typescript@5.8.3)
- vue-router@5.2.0(@rspack/core@2.1.8(@swc/helpers@0.5.23))(@vue/compiler-sfc@3.5.41)(rolldown@1.2.3)(vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.8.3)):
+ vue-router@5.2.0(@rspack/core@2.1.8(@swc/helpers@0.5.23))(@vue/compiler-sfc@3.5.41)(rolldown@1.2.3)(vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0))(vue@3.5.41(typescript@5.8.3)):
dependencies:
'@babel/generator': 8.0.0
'@vue-macros/common': 3.1.4(vue@3.5.41(typescript@5.8.3))
@@ -4392,13 +4412,13 @@ snapshots:
picomatch: 4.0.5
scule: 1.3.0
tinyglobby: 0.2.17
- unplugin: 3.3.0(@rspack/core@2.1.8(@swc/helpers@0.5.23))(rolldown@1.2.3)(vite@8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0))
+ unplugin: 3.3.0(@rspack/core@2.1.8(@swc/helpers@0.5.23))(rolldown@1.2.3)(vite@8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0))
unplugin-utils: 0.3.2
vue: 3.5.41(typescript@5.8.3)
yaml: 2.9.0
optionalDependencies:
'@vue/compiler-sfc': 3.5.41
- vite: 8.2.0(@types/node@24.13.3)(sass@1.102.0)(yaml@2.9.0)
+ vite: 8.2.0(@types/node@24.13.3)(jiti@2.7.0)(sass@1.102.0)(yaml@2.9.0)
transitivePeerDependencies:
- '@farmfe/core'
- '@rspack/core'
diff --git a/ui/src/darkreader-engine.ts b/ui/src/darkreader-engine.ts
new file mode 100644
index 0000000..df36d2a
--- /dev/null
+++ b/ui/src/darkreader-engine.ts
@@ -0,0 +1,48 @@
+import { disable, enable } from 'darkreader'
+import { watch } from 'vue'
+import { useDarkMode } from './composables/useDarkMode'
+
+/**
+ * Dark Reader 引擎参数。
+ * brightness/contrast 保持默认观感,背景色与现有 Halo 暗色变量接近,
+ * 让 Dark Reader 补全第三方插件页面时不会显得突兀。
+ */
+const DARK_READER_THEME = {
+ brightness: 100,
+ contrast: 90,
+ grayscale: 0,
+ sepia: 0,
+ darkSchemeBackgroundColor: '#181b20',
+ darkSchemeTextColor: '#e8e6e3',
+ scrollbarColor: '#3a3f4a',
+ selectionColor: '#2f6f7a',
+ styleSystemControls: true,
+} as const
+
+let initialized = false
+
+/**
+ * 初始化 Dark Reader 通用暗色引擎。
+ * 监听 useDarkMode 的 isDark 状态,深色时启用,浅色时关闭。
+ */
+export function initDarkReaderEngine(): void {
+ if (initialized) return
+ initialized = true
+
+ const { isDark } = useDarkMode()
+ watch(
+ isDark,
+ (dark) => {
+ try {
+ if (dark) {
+ enable(DARK_READER_THEME)
+ } else {
+ disable()
+ }
+ } catch (error) {
+ console.error('[dark-mode] Dark Reader 引擎异常', error)
+ }
+ },
+ { immediate: true },
+ )
+}
diff --git a/ui/src/index.ts b/ui/src/index.ts
index 7e513a1..92c48a1 100644
--- a/ui/src/index.ts
+++ b/ui/src/index.ts
@@ -2,10 +2,14 @@ import { definePlugin } from '@halo-dev/ui-shared'
import { IconPalette } from '@halo-dev/components'
import { markRaw } from 'vue'
import './styles/index.css'
+import { initDarkReaderEngine } from './darkreader-engine'
import { injectThemeToggle } from './injector'
+import { initMonacoThemeSync } from './monaco-theme'
-// 在插件加载后将切换器注入到侧边栏
+// 在插件加载后将切换器注入到侧边栏,并让 Monaco 跟随主题
injectThemeToggle()
+initMonacoThemeSync()
+initDarkReaderEngine()
export default definePlugin({
components: {},
diff --git a/ui/src/monaco-theme.ts b/ui/src/monaco-theme.ts
new file mode 100644
index 0000000..0845062
--- /dev/null
+++ b/ui/src/monaco-theme.ts
@@ -0,0 +1,43 @@
+/**
+ * 让 Monaco Editor(日志查看器)跟随 Halo 暗色模式。
+ * 优先调用 Monaco 官方主题 API,CSS 兜底见 styles/overrides/editor.css。
+ */
+
+type MonacoLike = {
+ editor?: {
+ setTheme: (name: string) => void
+ }
+}
+
+const DARK_THEME = 'vs-dark'
+const LIGHT_THEME = 'vs'
+
+function getMonaco(): MonacoLike | undefined {
+ return (window as unknown as { monaco?: MonacoLike }).monaco
+}
+
+function applyMonacoTheme(): void {
+ const monaco = getMonaco()
+ if (!monaco?.editor?.setTheme) return
+ const isDark = document.documentElement.getAttribute('data-halo-theme') === 'dark'
+ monaco.editor.setTheme(isDark ? DARK_THEME : LIGHT_THEME)
+}
+
+/** 初始化 Monaco 主题同步:立即应用,并监听属性变化与实例懒加载。 */
+export function initMonacoThemeSync(): void {
+ applyMonacoTheme()
+
+ const attributeObserver = new MutationObserver(() => applyMonacoTheme())
+ attributeObserver.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ['data-halo-theme'],
+ })
+
+ // 日志页面可能懒加载 Monaco,实例渲染后再补一次
+ const instanceObserver = new MutationObserver(() => {
+ if (document.querySelector('.monaco-editor')) {
+ applyMonacoTheme()
+ }
+ })
+ instanceObserver.observe(document.body, { childList: true, subtree: true })
+}
\ No newline at end of file
diff --git a/ui/src/styles/index.css b/ui/src/styles/index.css
index e1b67bc..14ceabf 100644
--- a/ui/src/styles/index.css
+++ b/ui/src/styles/index.css
@@ -6,6 +6,7 @@
@import './variables.css';
@import './overrides/halo-core.css';
@import './overrides/plugin-pages.css';
+@import './overrides/uno-fallback.css';
@import './overrides/utilities.css';
@import './overrides/layout.css';
@import './overrides/components.css';
diff --git a/ui/src/styles/overrides/components.css b/ui/src/styles/overrides/components.css
index f7fb4c6..195869b 100644
--- a/ui/src/styles/overrides/components.css
+++ b/ui/src/styles/overrides/components.css
@@ -1,73 +1,21 @@
/* ============================================================
- Halo Dark Mode — @halo-dev/components 组件库覆盖
- 覆盖 Halo 组件库中的 VCard, VModal, VDropdown, VTag 等
+ Halo Dark Mode — Halo 组件库覆盖(已清理)
+ 已删除 Halo 2.25 中不存在的 Vuetify 类(.v-card 等),
+ 真实 DOM 类覆盖请优先维护 halo-core.css。
============================================================ */
[data-halo-theme="dark"] {
- /* ===== 卡片 VCard ===== */
- .v-card,
- [class*="v-card"] {
- background-color: var(--halo-bg-card);
- border-color: var(--halo-border-base);
- color: var(--halo-text-primary);
- }
-
- /* ===== 模态框 VModal ===== */
- .v-modal,
- .modal-container,
- [class*="modal"] {
- background-color: var(--halo-bg-card);
- color: var(--halo-text-primary);
- }
-
- /* ===== 下拉菜单 VDropdown ===== */
- .v-dropdown,
- .dropdown-menu,
- [class*="dropdown"] {
- background-color: var(--halo-bg-dropdown);
- border-color: var(--halo-border-base);
- color: var(--halo-text-primary);
- }
-
- .v-dropdown-item:hover,
- .dropdown-item:hover {
- background-color: var(--halo-bg-hover);
- }
-
- /* ===== 提示框 VTooltip ===== */
- .v-tooltip,
- [class*="tooltip"] {
- background-color: var(--halo-bg-tooltip);
- color: var(--halo-text-inverse);
- }
-
- /* ===== 标签/徽章 VTag, VBadge ===== */
- .v-tag,
- [class*="tag"],
- .v-badge,
- [class*="badge"] {
- background-color: var(--halo-tag-bg);
- color: var(--halo-tag-text);
- }
-
- /* ===== 按钮 ===== */
- .btn-default,
- .btn-secondary,
- button:not([class*="btn-primary"]):not([class*="btn-danger"]) {
- background-color: var(--halo-bg-card);
- color: var(--halo-text-primary);
- border-color: var(--halo-border-base);
- }
-
- .btn-default:hover,
- .btn-secondary:hover {
- background-color: var(--halo-bg-hover);
+ /* ===== 模态框(具体类 + !important,防被核心 scoped 样式覆盖) ===== */
+ .modal-content,
+ .modal-header,
+ .modal-body,
+ .modal-footer {
+ background-color: var(--halo-bg-card) !important;
+ color: var(--halo-text-primary) !important;
}
/* ===== 表格 VTable ===== */
- table,
- .v-table,
- [class*="table"] {
+ table {
background-color: var(--halo-bg-card);
color: var(--halo-text-primary);
}
@@ -94,49 +42,29 @@
}
/* ===== 分页 ===== */
- .pagination,
- .v-pagination {
+ .pagination {
color: var(--halo-text-secondary);
}
- .pagination .active,
- .v-pagination .active {
+ .pagination .active {
background-color: var(--halo-accent-primary);
color: var(--halo-accent-primary-text);
}
/* ===== 面包屑 ===== */
- .breadcrumb,
- .v-breadcrumb {
+ .breadcrumb {
color: var(--halo-text-secondary);
}
- .breadcrumb a,
- .v-breadcrumb a {
+ .breadcrumb a {
color: var(--halo-text-link);
}
/* ===== Toast / 通知 ===== */
- .v-toast,
.toast-notification,
[class*="toast"] {
background-color: var(--halo-bg-card);
color: var(--halo-text-primary);
border-color: var(--halo-border-base);
}
-
- /* ===== 步骤条 ===== */
- .v-steps,
- [class*="steps"] {
- color: var(--halo-text-secondary);
- }
-
- /* ===== 开关 VSwitch ===== */
- .v-switch-track {
- background-color: var(--halo-bg-disabled);
- }
-
- .v-switch-track[aria-checked="true"] {
- background-color: var(--halo-accent-primary);
- }
-}
+}
\ No newline at end of file
diff --git a/ui/src/styles/overrides/editor.css b/ui/src/styles/overrides/editor.css
index 3bf697d..49c4eaa 100644
--- a/ui/src/styles/overrides/editor.css
+++ b/ui/src/styles/overrides/editor.css
@@ -232,3 +232,32 @@
background-color: oklch(30% 0.04 160 / 50%);
}
}
+/* ===== Monaco Editor(日志查看器,JS 主题优先,此处仅兜底) ===== */
+html[data-halo-theme="dark"] .monaco-editor.vs,
+html[data-halo-theme="dark"] .monaco-editor.vs .margin,
+html[data-halo-theme="dark"] .monaco-editor.vs .lines-content,
+html[data-halo-theme="dark"] .monaco-editor.vs .monaco-scrollable-element {
+ background-color: var(--halo-bg-input) !important;
+ color: var(--halo-text-primary) !important;
+}
+html[data-halo-theme="dark"] .monaco-editor.vs .line-numbers {
+ color: var(--halo-text-tertiary) !important;
+}
+html[data-halo-theme="dark"] .monaco-editor.vs .cursor {
+ border-color: var(--halo-text-primary) !important;
+}
+html[data-halo-theme="dark"] .monaco-editor.vs .selected-text,
+html[data-halo-theme="dark"] .monaco-editor.vs .view-overlays .selected-text {
+ background-color: oklch(30% 0.04 160 / 50%) !important;
+}
+html[data-halo-theme="dark"] .monaco-editor.vs .find-widget,
+html[data-halo-theme="dark"] .monaco-editor.vs .monaco-hover {
+ background-color: var(--halo-bg-dropdown) !important;
+ color: var(--halo-text-primary) !important;
+}
+
+/* ===== 编辑器链接(覆盖通用链接色规则) ===== */
+html[data-halo-theme="dark"] .ProseMirror a,
+html[data-halo-theme="dark"] .tiptap a {
+ color: var(--halo-text-link) !important;
+}
\ No newline at end of file
diff --git a/ui/src/styles/overrides/forms.css b/ui/src/styles/overrides/forms.css
index f569f61..ec3409a 100644
--- a/ui/src/styles/overrides/forms.css
+++ b/ui/src/styles/overrides/forms.css
@@ -87,3 +87,16 @@
border-color: var(--halo-border-base);
}
}
+/* ===== 强表单覆盖(scoped 哈希类特异性更高,需 html 前缀 + !important) ===== */
+html[data-halo-theme="dark"] input:not([type="checkbox"]):not([type="radio"]):not([type="color"]),
+html[data-halo-theme="dark"] textarea,
+html[data-halo-theme="dark"] select {
+ background-color: var(--halo-bg-input) !important;
+ color: var(--halo-text-primary) !important;
+ border-color: var(--halo-border-input) !important;
+}
+html[data-halo-theme="dark"] input:not([type="checkbox"]):not([type="radio"]):not([type="color"])::placeholder,
+html[data-halo-theme="dark"] textarea::placeholder,
+html[data-halo-theme="dark"] select::placeholder {
+ color: var(--halo-text-tertiary) !important;
+}
\ No newline at end of file
diff --git a/ui/src/styles/overrides/halo-core.css b/ui/src/styles/overrides/halo-core.css
index 6a36d52..b6fa177 100644
--- a/ui/src/styles/overrides/halo-core.css
+++ b/ui/src/styles/overrides/halo-core.css
@@ -1,11 +1,14 @@
+/* ===== 根元素背景(html 自身携带属性,需独立选择器) ===== */
+html[data-halo-theme="dark"] {
+ background-color: var(--halo-bg-body) !important;
+}
/* ============================================================
Halo Dark Mode — Halo 核心 BEM 类覆盖
基于 Halo 2.25 真实 DOM 扫描(demo.halocms.site)得出的选择器
============================================================ */
[data-halo-theme="dark"] {
- /* ===== 页面根背景(body 溢出时兜底) ===== */
- html,
+ /* ===== 页面根背景(body 溢出时兜底,html 见文件顶部独立规则) ===== */
body {
background-color: var(--halo-bg-body) !important;
color: var(--halo-text-primary);
@@ -100,14 +103,6 @@
.entity-field-description {
color: var(--halo-text-secondary);
}
- .entity-field-title a,
- .entity-field-title-body a {
- color: var(--halo-text-primary);
- }
- .entity-field-title a:hover,
- .entity-field-title-body a:hover {
- color: var(--halo-text-link);
- }
/* ===== 分页 ===== */
.pagination {
@@ -131,8 +126,7 @@
/* ===== 标签/徽章(BEM) ===== */
.tag-wrapper,
.tag-default,
- .tag-pill,
- [class*="tag-"] {
+ .tag-pill {
background-color: var(--halo-bg-hover);
color: var(--halo-text-secondary);
}
@@ -206,10 +200,10 @@
border-color: var(--halo-border-light);
}
.description-item__label {
- color: var(--halo-text-secondary);
+ color: var(--halo-text-secondary) !important;
}
.description-item__content {
- color: var(--halo-text-primary);
+ color: var(--halo-text-primary) !important;
}
/* ===== 已选指示器 ===== */
@@ -265,8 +259,50 @@
border-color: var(--halo-border-base);
}
- /* ===== 通用链接 ===== */
- a:not([class]) {
- color: var(--halo-text-link);
- }
+
}
+/* ===== 侧边栏激活菜单(Halo 核心 .menu-item-title.active) ===== */
+html[data-halo-theme="dark"] .menu-item-title.active,
+html[data-halo-theme="dark"] .menu-item-title.active:hover {
+ background-color: var(--halo-menu-item-active) !important;
+ color: var(--halo-text-primary) !important;
+}
+html[data-halo-theme="dark"] .menu-item-title.active::after {
+ background: var(--halo-accent-primary) !important;
+}
+
+/* ===== 空状态标题 ===== */
+html[data-halo-theme="dark"] .empty-wrapper .empty-title {
+ color: var(--halo-text-secondary) !important;
+}
+
+/* ===== 提示条(Alert) ===== */
+html[data-halo-theme="dark"] .alert-wrapper {
+ background-color: var(--halo-bg-card) !important;
+ border-color: var(--halo-border-base) !important;
+ color: var(--halo-text-primary) !important;
+}
+html[data-halo-theme="dark"] .alert-wrapper .alert-title,
+html[data-halo-theme="dark"] .alert-wrapper .alert-description,
+html[data-halo-theme="dark"] .alert-wrapper .alert-icon {
+ color: var(--halo-text-secondary) !important;
+}
+html[data-halo-theme="dark"] .alert-wrapper.alert-warning {
+ border-color: oklch(40% 0.12 85) !important;
+}
+html[data-halo-theme="dark"] .alert-wrapper.alert-error {
+ border-color: oklch(40% 0.17 25) !important;
+}
+html[data-halo-theme="dark"] .alert-wrapper.alert-success {
+ border-color: oklch(40% 0.14 150) !important;
+}
+
+/* ===== 实体列表标题链接(覆盖通用链接色,hover 变链接色) ===== */
+html[data-halo-theme="dark"] .entity-field-title a,
+html[data-halo-theme="dark"] .entity-field-title-body a {
+ color: var(--halo-text-primary) !important;
+}
+html[data-halo-theme="dark"] .entity-field-title a:hover,
+html[data-halo-theme="dark"] .entity-field-title-body a:hover {
+ color: var(--halo-text-link) !important;
+}
\ No newline at end of file
diff --git a/ui/src/styles/overrides/layout.css b/ui/src/styles/overrides/layout.css
index a18d87d..ea9f456 100644
--- a/ui/src/styles/overrides/layout.css
+++ b/ui/src/styles/overrides/layout.css
@@ -55,8 +55,7 @@
}
/* ===== 页面标题 ===== */
- .page-title,
- .v-card-title {
+ .page-title {
color: var(--halo-text-primary);
}
}
diff --git a/ui/src/styles/overrides/plugin-pages.css b/ui/src/styles/overrides/plugin-pages.css
index c4c7980..4ce8214 100644
--- a/ui/src/styles/overrides/plugin-pages.css
+++ b/ui/src/styles/overrides/plugin-pages.css
@@ -247,3 +247,137 @@
color: var(--halo-text-primary) !important;
}
}
+/* ===== AstraHub / 心愿便签(共用 ah-* UI) ===== */
+html[data-halo-theme="dark"] .ah-card,
+html[data-halo-theme="dark"] .ah-topbar,
+html[data-halo-theme="dark"] .ah-topbar-search,
+html[data-halo-theme="dark"] .ah-float-nav {
+ background-color: var(--halo-bg-card) !important;
+ border-color: var(--halo-border-base) !important;
+ color: var(--halo-text-primary) !important;
+}
+html[data-halo-theme="dark"] .ah-topbar-search {
+ background-color: var(--halo-bg-input) !important;
+}
+html[data-halo-theme="dark"] .ah-topbar-brand,
+html[data-halo-theme="dark"] .planet-hero-title {
+ color: var(--halo-text-primary) !important;
+}
+html[data-halo-theme="dark"] .ah-topbar-search input {
+ background-color: transparent !important;
+ color: var(--halo-text-primary) !important;
+}
+html[data-halo-theme="dark"] .planet-links-more {
+ background-color: var(--halo-bg-card) !important;
+ color: var(--halo-text-secondary) !important;
+}
+
+/* ===== RSS 订阅(links/rss) ===== */
+html[data-halo-theme="dark"] .subscription-panel,
+html[data-halo-theme="dark"] .feed-main,
+html[data-halo-theme="dark"] .feed-toolbar,
+html[data-halo-theme="dark"] .feed-stream,
+html[data-halo-theme="dark"] .feed-stream__footer {
+ background-color: var(--halo-bg-card) !important;
+ border-color: var(--halo-border-base) !important;
+ color: var(--halo-text-primary) !important;
+}
+html[data-halo-theme="dark"] .feed-status-tabs {
+ background-color: var(--halo-bg-hover) !important;
+ color: var(--halo-text-secondary) !important;
+}
+html[data-halo-theme="dark"] .feed-brief__title,
+html[data-halo-theme="dark"] .feed-stream p,
+html[data-halo-theme="dark"] .feed-stream a,
+html[data-halo-theme="dark"] .subscription-panel a,
+html[data-halo-theme="dark"] .subscription-panel span {
+ color: var(--halo-text-primary) !important;
+}
+
+/* ===== 私密文章插件 ===== */
+html[data-halo-theme="dark"] .focus-card,
+html[data-halo-theme="dark"] .overview-card,
+html[data-halo-theme="dark"] .list-card,
+html[data-halo-theme="dark"] .empty-state {
+ background-color: var(--halo-bg-card) !important;
+ border-color: var(--halo-border-base) !important;
+ color: var(--halo-text-primary) !important;
+}
+html[data-halo-theme="dark"] .overview-card h2,
+html[data-halo-theme="dark"] .page-shell h2,
+html[data-halo-theme="dark"] .overview-stats dd,
+html[data-halo-theme="dark"] .overview-stats dt {
+ color: var(--halo-text-primary) !important;
+}
+
+/* ===== AI 评论自动处理插件 ===== */
+html[data-halo-theme="dark"] .settings-container,
+html[data-halo-theme="dark"] .setting-panel,
+html[data-halo-theme="dark"] .tabs-wrap,
+html[data-halo-theme="dark"] .sidebar-card,
+html[data-halo-theme="dark"] .list-empty,
+html[data-halo-theme="dark"] .reply-card {
+ background-color: var(--halo-bg-card) !important;
+ border-color: var(--halo-border-base) !important;
+ color: var(--halo-text-primary) !important;
+}
+html[data-halo-theme="dark"] .form-row,
+html[data-halo-theme="dark"] .list-col,
+html[data-halo-theme="dark"] .panel-header,
+html[data-halo-theme="dark"] .card-footer {
+ background-color: var(--halo-bg-hover) !important;
+ color: var(--halo-text-primary) !important;
+}
+html[data-halo-theme="dark"] .section-header__text h3,
+html[data-halo-theme="dark"] .sidebar-header h4,
+html[data-halo-theme="dark"] .panel-header h3,
+html[data-halo-theme="dark"] .reply-card h3 {
+ color: var(--halo-text-primary) !important;
+}
+html[data-halo-theme="dark"] .wake-word-tag {
+ background-color: var(--halo-bg-hover) !important;
+ color: var(--halo-text-secondary) !important;
+}
+
+/* ===== 数据工坊 / 代码注入器等工具页语义容器 ===== */
+html[data-halo-theme="dark"] .data-studio-card-body,
+html[data-halo-theme="dark"] .injector-view-card-body,
+html[data-halo-theme="dark"] .injector-editor-container {
+ background-color: var(--halo-bg-card) !important;
+ border-color: var(--halo-border-base) !important;
+ color: var(--halo-text-primary) !important;
+}
+
+/* ===== 示例插件页 ===== */
+html[data-halo-theme="dark"] main section#plugin-starter {
+ background-color: var(--halo-bg-body) !important;
+ color: var(--halo-text-primary) !important;
+}
+
+/* ===== 友链卡片徽章 ===== */
+html[data-halo-theme="dark"] .link-badge {
+ background-color: var(--halo-bg-hover) !important;
+ color: var(--halo-text-primary) !important;
+ border-color: var(--halo-border-base) !important;
+}
+/* ===== 日程日历插件(time-column 布局) ===== */
+html[data-halo-theme="dark"] .time-column__header,
+html[data-halo-theme="dark"] .time-column__body,
+html[data-halo-theme="dark"] [class*="time-column"] {
+ background-color: var(--halo-bg-card) !important;
+ color: var(--halo-text-primary) !important;
+ border-color: var(--halo-border-base) !important;
+}
+html[data-halo-theme="dark"] .entry-card-header__title {
+ color: var(--halo-text-primary) !important;
+}
+
+/* ===== 存储工具箱补充类 ===== */
+html[data-halo-theme="dark"] .stat-value,
+html[data-halo-theme="dark"] .stat-card .card-title,
+html[data-halo-theme="dark"] .panel-card .card-title {
+ color: var(--halo-text-primary) !important;
+}
+html[data-halo-theme="dark"] .bar-track {
+ background-color: var(--halo-bg-disabled) !important;
+}
\ No newline at end of file
diff --git a/ui/src/styles/overrides/uno-fallback.css b/ui/src/styles/overrides/uno-fallback.css
new file mode 100644
index 0000000..844a909
--- /dev/null
+++ b/ui/src/styles/overrides/uno-fallback.css
@@ -0,0 +1,34 @@
+/* ============================================================
+ Halo Dark Mode — UnoCSS 哈希类兜底
+ Halo 2.25 中 UnoCSS 同时生成 uno-* / i-* 哈希类,
+ 这些类随构建变化,只能按结构定位兜底,不写死哈希值。
+ 注意:本文件规则刻意不加 !important,便于后续语义规则覆盖。
+ ============================================================ */
+
+/* 文字兜底:只修正文本元素,豁免表单控件 */
+html[data-halo-theme="dark"] main [class*="uno-"]:not(input):not(select):not(textarea) {
+ color: var(--halo-text-primary);
+}
+
+/* 块级容器背景兜底:豁免交互元素与表单控件 */
+html[data-halo-theme="dark"] main [class*="uno-"]:not(button):not(a):not(input):not(select):not(textarea):not(label) {
+ background-color: var(--halo-bg-card);
+ border-color: var(--halo-border-base);
+}
+
+/* 页面级容器使用内容区背景 */
+html[data-halo-theme="dark"] main > [class*="uno-"] {
+ background-color: var(--halo-bg-content);
+}
+
+/* 卡片头/卡片体内的 uno-* 区块使用 hover 级背景 */
+html[data-halo-theme="dark"] .card-header [class*="uno-"],
+html[data-halo-theme="dark"] .card-body [class*="uno-"] {
+ background-color: var(--halo-bg-hover);
+ border-color: var(--halo-border-light);
+}
+
+/* 旧版 i-* 哈希:只做文字兜底,避免给图标类设置背景 */
+html[data-halo-theme="dark"] main [class*="i-"]:not(input):not(select):not(textarea) {
+ color: var(--halo-text-primary);
+}
\ No newline at end of file
diff --git a/ui/src/styles/overrides/utilities.css b/ui/src/styles/overrides/utilities.css
index 83bdfa7..fca3ee3 100644
--- a/ui/src/styles/overrides/utilities.css
+++ b/ui/src/styles/overrides/utilities.css
@@ -62,10 +62,7 @@
}
/* ===== 链接 ===== */
- a:not([class*="text-"]),
- .hover\:text-gray-900:hover,
- .hover\:text-gray-800:hover,
- .hover\:text-gray-700:hover {
+ html[data-halo-theme="dark"] a:not([class*="text-"]) {
color: var(--halo-text-link);
}
diff --git a/ui/vendor/darkreader/LICENSE b/ui/vendor/darkreader/LICENSE
new file mode 100644
index 0000000..73e15f6
--- /dev/null
+++ b/ui/vendor/darkreader/LICENSE
@@ -0,0 +1,23 @@
+MIT License
+
+Copyright (c) 2026 Dark Reader Ltd.
+
+All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/ui/vendor/darkreader/darkreader.js b/ui/vendor/darkreader/darkreader.js
new file mode 100644
index 0000000..a481ea0
--- /dev/null
+++ b/ui/vendor/darkreader/darkreader.js
@@ -0,0 +1,9366 @@
+/**
+ * Dark Reader v4.9.129
+ * https://darkreader.org/
+ */
+
+(function (global, factory) {
+ typeof exports === "object" && typeof module !== "undefined"
+ ? factory(exports)
+ : typeof define === "function" && define.amd
+ ? define(["exports"], factory)
+ : ((global =
+ typeof globalThis !== "undefined"
+ ? globalThis
+ : global || self),
+ factory((global.DarkReader = {})));
+})(this, function (exports) {
+ "use strict";
+
+ var MessageTypeUItoBG;
+ (function (MessageTypeUItoBG) {
+ MessageTypeUItoBG["GET_DATA"] = "ui-bg-get-data";
+ MessageTypeUItoBG["GET_DEVTOOLS_DATA"] = "ui-bg-get-devtools-data";
+ MessageTypeUItoBG["SUBSCRIBE_TO_CHANGES"] =
+ "ui-bg-subscribe-to-changes";
+ MessageTypeUItoBG["UNSUBSCRIBE_FROM_CHANGES"] =
+ "ui-bg-unsubscribe-from-changes";
+ MessageTypeUItoBG["CHANGE_SETTINGS"] = "ui-bg-change-settings";
+ MessageTypeUItoBG["SET_THEME"] = "ui-bg-set-theme";
+ MessageTypeUItoBG["TOGGLE_ACTIVE_TAB"] = "ui-bg-toggle-active-tab";
+ MessageTypeUItoBG["MARK_NEWS_AS_READ"] = "ui-bg-mark-news-as-read";
+ MessageTypeUItoBG["MARK_NEWS_AS_DISPLAYED"] =
+ "ui-bg-mark-news-as-displayed";
+ MessageTypeUItoBG["LOAD_CONFIG"] = "ui-bg-load-config";
+ MessageTypeUItoBG["APPLY_DEV_FIXES"] = "ui-bg-apply-dev-fixes";
+ MessageTypeUItoBG["RESET_DEV_FIXES"] = "ui-bg-reset-dev-fixes";
+ MessageTypeUItoBG["START_ACTIVATION"] = "ui-bg-start-activation";
+ MessageTypeUItoBG["RESET_ACTIVATION"] = "ui-bg-reset-activation";
+ MessageTypeUItoBG["COLOR_SCHEME_CHANGE"] = "ui-bg-color-scheme-change";
+ MessageTypeUItoBG["HIDE_HIGHLIGHTS"] = "ui-bg-hide-highlights";
+ })(MessageTypeUItoBG || (MessageTypeUItoBG = {}));
+ var MessageTypeBGtoUI;
+ (function (MessageTypeBGtoUI) {
+ MessageTypeBGtoUI["CHANGES"] = "bg-ui-changes";
+ })(MessageTypeBGtoUI || (MessageTypeBGtoUI = {}));
+ var DebugMessageTypeBGtoUI;
+ (function (DebugMessageTypeBGtoUI) {
+ DebugMessageTypeBGtoUI["CSS_UPDATE"] = "debug-bg-ui-css-update";
+ DebugMessageTypeBGtoUI["UPDATE"] = "debug-bg-ui-update";
+ })(DebugMessageTypeBGtoUI || (DebugMessageTypeBGtoUI = {}));
+ var MessageTypeBGtoCS;
+ (function (MessageTypeBGtoCS) {
+ MessageTypeBGtoCS["ADD_CSS_FILTER"] = "bg-cs-add-css-filter";
+ MessageTypeBGtoCS["ADD_DYNAMIC_THEME"] = "bg-cs-add-dynamic-theme";
+ MessageTypeBGtoCS["ADD_STATIC_THEME"] = "bg-cs-add-static-theme";
+ MessageTypeBGtoCS["ADD_SVG_FILTER"] = "bg-cs-add-svg-filter";
+ MessageTypeBGtoCS["CLEAN_UP"] = "bg-cs-clean-up";
+ MessageTypeBGtoCS["FETCH_RESPONSE"] = "bg-cs-fetch-response";
+ MessageTypeBGtoCS["UNSUPPORTED_SENDER"] = "bg-cs-unsupported-sender";
+ })(MessageTypeBGtoCS || (MessageTypeBGtoCS = {}));
+ var DebugMessageTypeBGtoCS;
+ (function (DebugMessageTypeBGtoCS) {
+ DebugMessageTypeBGtoCS["RELOAD"] = "debug-bg-cs-reload";
+ })(DebugMessageTypeBGtoCS || (DebugMessageTypeBGtoCS = {}));
+ var MessageTypeCStoBG;
+ (function (MessageTypeCStoBG) {
+ MessageTypeCStoBG["COLOR_SCHEME_CHANGE"] = "cs-bg-color-scheme-change";
+ MessageTypeCStoBG["DARK_THEME_DETECTED"] = "cs-bg-dark-theme-detected";
+ MessageTypeCStoBG["DARK_THEME_NOT_DETECTED"] =
+ "cs-bg-dark-theme-not-detected";
+ MessageTypeCStoBG["FETCH"] = "cs-bg-fetch";
+ MessageTypeCStoBG["DOCUMENT_CONNECT"] = "cs-bg-document-connect";
+ MessageTypeCStoBG["DOCUMENT_FORGET"] = "cs-bg-document-forget";
+ MessageTypeCStoBG["DOCUMENT_FREEZE"] = "cs-bg-document-freeze";
+ MessageTypeCStoBG["DOCUMENT_RESUME"] = "cs-bg-document-resume";
+ })(MessageTypeCStoBG || (MessageTypeCStoBG = {}));
+ var DebugMessageTypeCStoBG;
+ (function (DebugMessageTypeCStoBG) {
+ DebugMessageTypeCStoBG["LOG"] = "debug-cs-bg-log";
+ })(DebugMessageTypeCStoBG || (DebugMessageTypeCStoBG = {}));
+ var MessageTypeCStoUI;
+ (function (MessageTypeCStoUI) {
+ MessageTypeCStoUI["EXPORT_CSS_RESPONSE"] = "cs-ui-export-css-response";
+ })(MessageTypeCStoUI || (MessageTypeCStoUI = {}));
+ var MessageTypeUItoCS;
+ (function (MessageTypeUItoCS) {
+ MessageTypeUItoCS["EXPORT_CSS"] = "ui-cs-export-css";
+ })(MessageTypeUItoCS || (MessageTypeUItoCS = {}));
+
+ const isNavigatorDefined = typeof navigator !== "undefined";
+ const userAgent = isNavigatorDefined
+ ? navigator.userAgentData &&
+ Array.isArray(navigator.userAgentData.brands)
+ ? navigator.userAgentData.brands
+ .map(
+ (brand) => `${brand.brand.toLowerCase()} ${brand.version}`
+ )
+ .join(" ")
+ : navigator.userAgent.toLowerCase()
+ : "some useragent";
+ const platform = isNavigatorDefined
+ ? navigator.userAgentData &&
+ typeof navigator.userAgentData.platform === "string"
+ ? navigator.userAgentData.platform.toLowerCase()
+ : navigator.platform.toLowerCase()
+ : "some platform";
+ const isChromium =
+ userAgent.includes("chrome") || userAgent.includes("chromium");
+ const isFirefox =
+ userAgent.includes("firefox") ||
+ userAgent.includes("thunderbird") ||
+ userAgent.includes("librewolf");
+ const isSafari = userAgent.includes("safari") && !isChromium;
+ const isWindows = platform.startsWith("win");
+ const isMacOS = platform.startsWith("mac");
+ const isMobile =
+ isNavigatorDefined && navigator.userAgentData
+ ? navigator.userAgentData.mobile
+ : userAgent.includes("mobile") || false;
+ const isShadowDomSupported = typeof ShadowRoot === "function";
+ const isMatchMediaChangeEventListenerSupported =
+ typeof MediaQueryList === "function" &&
+ typeof MediaQueryList.prototype.addEventListener === "function";
+ const isLayerRuleSupported = typeof CSSLayerBlockRule === "function";
+ const isContainerRuleSupported = typeof CSSContainerRule === "function";
+ (() => {
+ const m = userAgent.match(/chrom(?:e|ium)(?:\/| )([^ ]+)/);
+ if (m && m[1]) {
+ return m[1];
+ }
+ return "";
+ })();
+ (() => {
+ const m = userAgent.match(/(?:firefox|librewolf)(?:\/| )([^ ]+)/);
+ if (m && m[1]) {
+ return m[1];
+ }
+ return "";
+ })();
+ const isDefinedSelectorSupported = (() => {
+ try {
+ document.querySelector(":defined");
+ return true;
+ } catch (err) {
+ return false;
+ }
+ })();
+ const isCSSColorSchemePropSupported = (() => {
+ try {
+ if (typeof document === "undefined") {
+ return false;
+ }
+ const el = document.createElement("div");
+ if (!el || typeof el.style !== "object") {
+ return false;
+ }
+ if (typeof el.style.colorScheme === "string") {
+ return true;
+ }
+ el.setAttribute("style", "color-scheme: dark");
+ return el.style.colorScheme === "dark";
+ } catch (e) {
+ return false;
+ }
+ })();
+
+ async function getOKResponse(url, mimeType, origin) {
+ const credentials =
+ origin && url.startsWith(`${origin}/`) ? undefined : "omit";
+ const redirect = mimeType === "text/css" ? undefined : "error";
+ const response = await fetch(url, {
+ cache: "force-cache",
+ credentials,
+ referrer: origin,
+ redirect
+ });
+ if (
+ isFirefox &&
+ mimeType === "text/css" &&
+ url.startsWith("moz-extension://") &&
+ url.endsWith(".css")
+ ) {
+ return response;
+ }
+ const contentType = response.headers.get("Content-Type");
+ if (
+ mimeType &&
+ !(
+ contentType === mimeType ||
+ contentType?.startsWith(`${mimeType};`)
+ )
+ ) {
+ throw new Error(`Mime type mismatch when loading ${url}`);
+ }
+ if (
+ response.redirected &&
+ response.url &&
+ shouldIgnoreCors(new URL(response.url))
+ ) {
+ throw new Error("Cross-origin limit reached");
+ }
+ if (!response.ok) {
+ throw new Error(
+ `Unable to load ${url} ${response.status} ${response.statusText}`
+ );
+ }
+ return response;
+ }
+ async function loadAsDataURL(url, mimeType) {
+ const response = await getOKResponse(url, mimeType);
+ return await readResponseAsDataURL(response);
+ }
+ async function loadAsBlob(url, mimeType) {
+ const response = await getOKResponse(url, mimeType);
+ return await response.blob();
+ }
+ async function readResponseAsDataURL(response) {
+ const blob = await response.blob();
+ const dataURL = await new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onloadend = () => resolve(reader.result);
+ reader.onerror = () => reject(reader.error);
+ reader.readAsDataURL(blob);
+ });
+ return dataURL;
+ }
+ async function loadAsText(url, mimeType, origin) {
+ const response = await getOKResponse(url, mimeType, origin);
+ return await response.text();
+ }
+ const MAX_CORS_HOSTS = 16;
+ const corsHosts = new Set();
+ const checkedHosts = new Set();
+ const localAliases = [
+ "127-0-0-1.org.uk",
+ "42foo.com",
+ "domaincontrol.com",
+ "fbi.com",
+ "fuf.me",
+ "lacolhost.com",
+ "local.sisteminha.com",
+ "localfabriek.nl",
+ "localhost",
+ "localhst.co.uk",
+ "localmachine.info",
+ "localmachine.name",
+ "localtest.me",
+ "lvh.me",
+ "mouse-potato.com",
+ "nip.io",
+ "sslip.io",
+ "vcap.me",
+ "xip.io",
+ "yoogle.com"
+ ];
+ const localSubDomains = [
+ ".corp",
+ ".direct",
+ ".home",
+ ".internal",
+ ".intranet",
+ ".lan",
+ ".local",
+ ".localdomain",
+ ".test",
+ ".zz",
+ ...localAliases.map((alias) => `.${alias}`)
+ ];
+ function isIPHost(hostname) {
+ if (hostname.startsWith("[")) {
+ return true;
+ }
+ const labels = hostname.split(".");
+ const last = labels[labels.length - 1];
+ return /^(0x[0-9a-f]+|\d+)$/i.test(last);
+ }
+ function shouldIgnoreCors(url) {
+ const {host, port, protocol} = url;
+ const hostname = url.hostname.endsWith(".")
+ ? url.hostname.slice(0, -1)
+ : url.hostname;
+ if (!corsHosts.has(host)) {
+ corsHosts.add(host);
+ }
+ if (checkedHosts.has(host)) {
+ return false;
+ }
+ if (
+ corsHosts.size >= MAX_CORS_HOSTS ||
+ protocol !== "https:" ||
+ port !== "" ||
+ localAliases.includes(hostname) ||
+ localSubDomains.some((sub) => hostname.endsWith(sub)) ||
+ isIPHost(hostname)
+ ) {
+ return true;
+ }
+ checkedHosts.add(host);
+ return false;
+ }
+
+ const throwCORSError = async (url) => {
+ return Promise.reject(
+ new Error(
+ [
+ "Embedded Dark Reader cannot access a cross-origin resource",
+ url,
+ "Overview your URLs and CORS policies or use",
+ "`DarkReader.setFetchMethod(fetch: (url) => Promise))`.",
+ "See if using `DarkReader.setFetchMethod(window.fetch)`",
+ "before `DarkReader.enable()` works."
+ ].join(" ")
+ )
+ );
+ };
+ let fetcher = throwCORSError;
+ function setFetchMethod$1(fetch) {
+ if (fetch) {
+ fetcher = fetch;
+ } else {
+ fetcher = throwCORSError;
+ }
+ }
+ async function callFetchMethod(url) {
+ return await fetcher(url);
+ }
+
+ if (!window.chrome) {
+ window.chrome = {};
+ }
+ if (!chrome.runtime) {
+ chrome.runtime = {};
+ }
+ const messageListeners = new Set();
+ async function sendMessage(...args) {
+ if (args[0] && args[0].type === MessageTypeCStoBG.FETCH) {
+ const {id} = args[0];
+ try {
+ const {url, responseType} = args[0].data;
+ const response = await callFetchMethod(url);
+ let text;
+ if (responseType === "data-url") {
+ text = await readResponseAsDataURL(response);
+ } else {
+ text = await response.text();
+ }
+ messageListeners.forEach((cb) =>
+ cb({
+ type: MessageTypeBGtoCS.FETCH_RESPONSE,
+ data: text,
+ error: null,
+ id
+ })
+ );
+ } catch (error) {
+ console.error(error);
+ messageListeners.forEach((cb) =>
+ cb({
+ type: MessageTypeBGtoCS.FETCH_RESPONSE,
+ data: null,
+ error,
+ id
+ })
+ );
+ }
+ }
+ }
+ function addMessageListener(callback) {
+ messageListeners.add(callback);
+ }
+ if (typeof chrome.runtime.sendMessage === "function") {
+ const nativeSendMessage = chrome.runtime.sendMessage;
+ chrome.runtime.sendMessage = (...args) => {
+ sendMessage(...args);
+ nativeSendMessage.apply(chrome.runtime, args);
+ };
+ } else {
+ chrome.runtime.sendMessage = sendMessage;
+ }
+ if (!chrome.runtime.onMessage) {
+ chrome.runtime.onMessage = {};
+ }
+ if (typeof chrome.runtime.onMessage.addListener === "function") {
+ const nativeAddListener = chrome.runtime.onMessage.addListener;
+ chrome.runtime.onMessage.addListener = (...args) => {
+ addMessageListener(args[0]);
+ nativeAddListener.apply(chrome.runtime.onMessage, args);
+ };
+ } else {
+ chrome.runtime.onMessage.addListener = (...args) =>
+ addMessageListener(args[0]);
+ }
+
+ var ThemeEngine;
+ (function (ThemeEngine) {
+ ThemeEngine["cssFilter"] = "cssFilter";
+ ThemeEngine["svgFilter"] = "svgFilter";
+ ThemeEngine["staticTheme"] = "staticTheme";
+ ThemeEngine["dynamicTheme"] = "dynamicTheme";
+ })(ThemeEngine || (ThemeEngine = {}));
+
+ var AutomationMode;
+ (function (AutomationMode) {
+ AutomationMode["NONE"] = "";
+ AutomationMode["TIME"] = "time";
+ AutomationMode["SYSTEM"] = "system";
+ AutomationMode["LOCATION"] = "location";
+ })(AutomationMode || (AutomationMode = {}));
+
+ const DEFAULT_COLORS = {
+ darkScheme: {
+ background: "#181a1b",
+ text: "#e8e6e3"
+ },
+ lightScheme: {
+ background: "#dcdad7",
+ text: "#181a1b"
+ }
+ };
+ const DEFAULT_THEME = {
+ mode: 1,
+ brightness: 100,
+ contrast: 100,
+ grayscale: 0,
+ sepia: 0,
+ useFont: false,
+ fontFamily: isMacOS
+ ? "Helvetica Neue"
+ : isWindows
+ ? "Segoe UI"
+ : "Open Sans",
+ textStroke: 0,
+ engine: ThemeEngine.dynamicTheme,
+ stylesheet: "",
+ darkSchemeBackgroundColor: DEFAULT_COLORS.darkScheme.background,
+ darkSchemeTextColor: DEFAULT_COLORS.darkScheme.text,
+ lightSchemeBackgroundColor: DEFAULT_COLORS.lightScheme.background,
+ lightSchemeTextColor: DEFAULT_COLORS.lightScheme.text,
+ scrollbarColor: "",
+ selectionColor: "auto",
+ styleSystemControls: !isCSSColorSchemePropSupported,
+ lightColorScheme: "Default",
+ darkColorScheme: "Default",
+ immediateModify: false
+ };
+ const filterModeSites = [
+ "*.officeapps.live.com",
+ "*.sharepoint.com",
+ "docs.google.com",
+ "onedrive.live.com"
+ ];
+ ({
+ customThemes: filterModeSites.map((url) => {
+ const engine = ThemeEngine.cssFilter;
+ return {
+ url: [url],
+ theme: {...DEFAULT_THEME, engine},
+ builtIn: true
+ };
+ }),
+ automation: {
+ mode: AutomationMode.NONE
+ }
+ });
+
+ function getMatches(regex, input, group = 0) {
+ const matches = [];
+ let m;
+ while ((m = regex.exec(input))) {
+ matches.push(m[group]);
+ }
+ return matches;
+ }
+ function getMatchesWithOffsets(regex, input, group = 0) {
+ const matches = [];
+ let m;
+ while ((m = regex.exec(input))) {
+ matches.push({text: m[group], offset: m.index});
+ }
+ return matches;
+ }
+ function getHashCode(text) {
+ const len = text.length;
+ let hash = 0;
+ for (let i = 0; i < len; i++) {
+ const c = text.charCodeAt(i);
+ hash = ((hash << 5) - hash + c) & 4294967295;
+ }
+ return hash;
+ }
+ function escapeRegExpSpecialChars(input) {
+ return input.replaceAll(/[\^$.*+?\(\)\[\]{}|\-\\]/g, "\\$&");
+ }
+ function getParenthesesRange(input, searchStartIndex = 0) {
+ return getOpenCloseRange(input, searchStartIndex, "(", ")", []);
+ }
+ function getOpenCloseRange(
+ input,
+ searchStartIndex,
+ openToken,
+ closeToken,
+ excludeRanges
+ ) {
+ let indexOf;
+ if (excludeRanges.length === 0) {
+ indexOf = (token, pos) => input.indexOf(token, pos);
+ } else {
+ indexOf = (token, pos) =>
+ indexOfExcluding(input, token, pos, excludeRanges);
+ }
+ const {length} = input;
+ let depth = 0;
+ let firstOpenIndex = -1;
+ for (let i = searchStartIndex; i < length; i++) {
+ if (depth === 0) {
+ const openIndex = indexOf(openToken, i);
+ if (openIndex < 0) {
+ break;
+ }
+ firstOpenIndex = openIndex;
+ depth++;
+ i = openIndex;
+ } else {
+ const closeIndex = indexOf(closeToken, i);
+ if (closeIndex < 0) {
+ break;
+ }
+ const openIndex = indexOf(openToken, i);
+ if (openIndex < 0 || closeIndex <= openIndex) {
+ depth--;
+ if (depth === 0) {
+ return {start: firstOpenIndex, end: closeIndex + 1};
+ }
+ i = closeIndex;
+ } else {
+ depth++;
+ i = openIndex;
+ }
+ }
+ }
+ return null;
+ }
+ function indexOfExcluding(input, search, position, excludeRanges) {
+ const i = input.indexOf(search, position);
+ const exclusion = excludeRanges.find((r) => i >= r.start && i < r.end);
+ if (exclusion) {
+ return indexOfExcluding(
+ input,
+ search,
+ exclusion.end,
+ excludeRanges
+ );
+ }
+ return i;
+ }
+ function splitExcluding(input, separator, excludeRanges) {
+ const parts = [];
+ let commaIndex = -1;
+ let currIndex = 0;
+ while (
+ (commaIndex = indexOfExcluding(
+ input,
+ separator,
+ currIndex,
+ excludeRanges
+ )) >= 0
+ ) {
+ parts.push(input.substring(currIndex, commaIndex).trim());
+ currIndex = commaIndex + 1;
+ }
+ parts.push(input.substring(currIndex).trim());
+ return parts;
+ }
+
+ let anchor;
+ const parsedURLCache = new Map();
+ function fixBaseURL($url) {
+ if (!anchor) {
+ anchor = document.createElement("a");
+ }
+ anchor.href = $url;
+ return anchor.href;
+ }
+ function parseURL($url, $base = null) {
+ const key = `${$url}${$base ? `;${$base}` : ""}`;
+ if (parsedURLCache.has(key)) {
+ return parsedURLCache.get(key);
+ }
+ if ($base) {
+ const parsedURL = new URL($url, fixBaseURL($base));
+ parsedURLCache.set(key, parsedURL);
+ return parsedURL;
+ }
+ const parsedURL = new URL(fixBaseURL($url));
+ parsedURLCache.set($url, parsedURL);
+ return parsedURL;
+ }
+ function getAbsoluteURL($base, $relative) {
+ if ($relative.match(/^data\\?\:/)) {
+ return $relative;
+ }
+ if (/^\/\//.test($relative)) {
+ return `${location.protocol}${$relative}`;
+ }
+ const b = parseURL($base);
+ const a = parseURL($relative, b.href);
+ return a.href;
+ }
+ function isRelativeHrefOnAbsolutePath(href) {
+ if (href.startsWith("data:")) {
+ return true;
+ }
+ const url = parseURL(href);
+ if (url.protocol !== location.protocol) {
+ return false;
+ }
+ if (url.hostname !== location.hostname) {
+ return false;
+ }
+ if (url.port !== location.port) {
+ return false;
+ }
+ return url.pathname === location.pathname;
+ }
+
+ const excludedSelectors = [
+ "pre",
+ "pre *",
+ "code",
+ '[aria-hidden="true"]',
+ '[class*="fa-"]',
+ ".fa",
+ ".fab",
+ ".fad",
+ ".fal",
+ ".far",
+ ".fas",
+ ".fass",
+ ".fasr",
+ ".fat",
+ ".icofont",
+ '[style*="font-"]',
+ '[class*="icon"]',
+ '[class*="Icon"]',
+ '[class*="symbol"]',
+ '[class*="Symbol"]',
+ ".glyphicon",
+ '[class*="material-symbol"]',
+ '[class*="material-icon"]',
+ "mu",
+ '[class*="mu-"]',
+ ".typcn",
+ '[class*="vjs-"]'
+ ];
+ function createTextStyle(config) {
+ const lines = [];
+ lines.push(`*:not(${excludedSelectors.join(", ")}) {`);
+ if (config.useFont && config.fontFamily) {
+ lines.push(` font-family: ${config.fontFamily} !important;`);
+ }
+ if (config.textStroke > 0) {
+ lines.push(
+ ` -webkit-text-stroke: ${config.textStroke}px !important;`
+ );
+ lines.push(` text-stroke: ${config.textStroke}px !important;`);
+ }
+ lines.push("}");
+ return lines.join("\n");
+ }
+
+ function isArrayLike(items) {
+ return items.length != null;
+ }
+ function forEach(items, iterator) {
+ if (isArrayLike(items)) {
+ for (let i = 0, len = items.length; i < len; i++) {
+ iterator(items[i]);
+ }
+ } else {
+ for (const item of items) {
+ iterator(item);
+ }
+ }
+ }
+ function push(array, addition) {
+ forEach(addition, (a) => array.push(a));
+ }
+ function toArray(items) {
+ const results = [];
+ for (let i = 0, len = items.length; i < len; i++) {
+ results.push(items[i]);
+ }
+ return results;
+ }
+
+ function scale(x, inLow, inHigh, outLow, outHigh) {
+ return ((x - inLow) * (outHigh - outLow)) / (inHigh - inLow) + outLow;
+ }
+ function clamp(x, min, max) {
+ return Math.min(max, Math.max(min, x));
+ }
+ function multiplyMatrices(m1, m2) {
+ const result = [];
+ for (let i = 0, len = m1.length; i < len; i++) {
+ result[i] = [];
+ for (let j = 0, len2 = m2[0].length; j < len2; j++) {
+ let sum = 0;
+ for (let k = 0, len3 = m1[0].length; k < len3; k++) {
+ sum += m1[i][k] * m2[k][j];
+ }
+ result[i][j] = sum;
+ }
+ }
+ return result;
+ }
+
+ function createFilterMatrix(config) {
+ let m = Matrix.identity();
+ if (config.sepia !== 0) {
+ m = multiplyMatrices(m, Matrix.sepia(config.sepia / 100));
+ }
+ if (config.grayscale !== 0) {
+ m = multiplyMatrices(m, Matrix.grayscale(config.grayscale / 100));
+ }
+ if (config.contrast !== 100) {
+ m = multiplyMatrices(m, Matrix.contrast(config.contrast / 100));
+ }
+ if (config.brightness !== 100) {
+ m = multiplyMatrices(m, Matrix.brightness(config.brightness / 100));
+ }
+ if (config.mode === 1) {
+ m = multiplyMatrices(m, Matrix.invertNHue());
+ }
+ return m;
+ }
+ function applyColorMatrix([r, g, b], matrix) {
+ const rgb = [[r / 255], [g / 255], [b / 255], [1], [1]];
+ const result = multiplyMatrices(matrix, rgb);
+ return [0, 1, 2].map((i) =>
+ clamp(Math.round(result[i][0] * 255), 0, 255)
+ );
+ }
+ const Matrix = {
+ identity() {
+ return [
+ [1, 0, 0, 0, 0],
+ [0, 1, 0, 0, 0],
+ [0, 0, 1, 0, 0],
+ [0, 0, 0, 1, 0],
+ [0, 0, 0, 0, 1]
+ ];
+ },
+ invertNHue() {
+ return [
+ [0.333, -0.667, -0.667, 0, 1],
+ [-0.667, 0.333, -0.667, 0, 1],
+ [-0.667, -0.667, 0.333, 0, 1],
+ [0, 0, 0, 1, 0],
+ [0, 0, 0, 0, 1]
+ ];
+ },
+ brightness(v) {
+ return [
+ [v, 0, 0, 0, 0],
+ [0, v, 0, 0, 0],
+ [0, 0, v, 0, 0],
+ [0, 0, 0, 1, 0],
+ [0, 0, 0, 0, 1]
+ ];
+ },
+ contrast(v) {
+ const t = (1 - v) / 2;
+ return [
+ [v, 0, 0, 0, t],
+ [0, v, 0, 0, t],
+ [0, 0, v, 0, t],
+ [0, 0, 0, 1, 0],
+ [0, 0, 0, 0, 1]
+ ];
+ },
+ sepia(v) {
+ return [
+ [
+ 0.393 + 0.607 * (1 - v),
+ 0.769 - 0.769 * (1 - v),
+ 0.189 - 0.189 * (1 - v),
+ 0,
+ 0
+ ],
+ [
+ 0.349 - 0.349 * (1 - v),
+ 0.686 + 0.314 * (1 - v),
+ 0.168 - 0.168 * (1 - v),
+ 0,
+ 0
+ ],
+ [
+ 0.272 - 0.272 * (1 - v),
+ 0.534 - 0.534 * (1 - v),
+ 0.131 + 0.869 * (1 - v),
+ 0,
+ 0
+ ],
+ [0, 0, 0, 1, 0],
+ [0, 0, 0, 0, 1]
+ ];
+ },
+ grayscale(v) {
+ return [
+ [
+ 0.2126 + 0.7874 * (1 - v),
+ 0.7152 - 0.7152 * (1 - v),
+ 0.0722 - 0.0722 * (1 - v),
+ 0,
+ 0
+ ],
+ [
+ 0.2126 - 0.2126 * (1 - v),
+ 0.7152 + 0.2848 * (1 - v),
+ 0.0722 - 0.0722 * (1 - v),
+ 0,
+ 0
+ ],
+ [
+ 0.2126 - 0.2126 * (1 - v),
+ 0.7152 - 0.7152 * (1 - v),
+ 0.0722 + 0.9278 * (1 - v),
+ 0,
+ 0
+ ],
+ [0, 0, 0, 1, 0],
+ [0, 0, 0, 0, 1]
+ ];
+ }
+ };
+
+ var FilterMode;
+ (function (FilterMode) {
+ FilterMode[(FilterMode["light"] = 0)] = "light";
+ FilterMode[(FilterMode["dark"] = 1)] = "dark";
+ })(FilterMode || (FilterMode = {}));
+ function getCSSFilterValue(config) {
+ const filters = [];
+ if (config.mode === FilterMode.dark) {
+ filters.push("invert(100%) hue-rotate(180deg)");
+ }
+ if (config.brightness !== 100) {
+ filters.push(`brightness(${config.brightness}%)`);
+ }
+ if (config.contrast !== 100) {
+ filters.push(`contrast(${config.contrast}%)`);
+ }
+ if (config.grayscale !== 0) {
+ filters.push(`grayscale(${config.grayscale}%)`);
+ }
+ if (config.sepia !== 0) {
+ filters.push(`sepia(${config.sepia}%)`);
+ }
+ if (filters.length === 0) {
+ return null;
+ }
+ return filters.join(" ");
+ }
+
+ function evalMath(expression) {
+ const rpnStack = [];
+ const workingStack = [];
+ let lastToken;
+ for (let i = 0, len = expression.length; i < len; i++) {
+ const token = expression[i];
+ if (!token || token === " ") {
+ continue;
+ }
+ if (operators.has(token)) {
+ const op = operators.get(token);
+ while (workingStack.length) {
+ const currentOp = operators.get(workingStack[0]);
+ if (!currentOp) {
+ break;
+ }
+ if (op.lessOrEqualThan(currentOp)) {
+ rpnStack.push(workingStack.shift());
+ } else {
+ break;
+ }
+ }
+ workingStack.unshift(token);
+ } else if (!lastToken || operators.has(lastToken)) {
+ rpnStack.push(token);
+ } else {
+ rpnStack[rpnStack.length - 1] += token;
+ }
+ lastToken = token;
+ }
+ rpnStack.push(...workingStack);
+ const stack = [];
+ for (let i = 0, len = rpnStack.length; i < len; i++) {
+ const op = operators.get(rpnStack[i]);
+ if (op) {
+ const args = stack.splice(0, 2);
+ stack.push(op.exec(args[1], args[0]));
+ } else {
+ stack.unshift(parseFloat(rpnStack[i]));
+ }
+ }
+ return stack[0];
+ }
+ class Operator {
+ constructor(precedence, method) {
+ this.precendce = precedence;
+ this.execMethod = method;
+ }
+ exec(left, right) {
+ return this.execMethod(left, right);
+ }
+ lessOrEqualThan(op) {
+ return this.precendce <= op.precendce;
+ }
+ }
+ const operators = new Map([
+ ["+", new Operator(1, (left, right) => left + right)],
+ ["-", new Operator(1, (left, right) => left - right)],
+ ["*", new Operator(2, (left, right) => left * right)],
+ ["/", new Operator(2, (left, right) => left / right)]
+ ]);
+
+ const isSystemDarkModeEnabled = () =>
+ matchMedia("(prefers-color-scheme: dark)").matches;
+
+ const hslaParseCache = new Map();
+ const rgbaParseCache = new Map();
+ function parseColorWithCache($color) {
+ $color = $color.trim();
+ const key = $color;
+ if (rgbaParseCache.has(key)) {
+ return rgbaParseCache.get(key);
+ }
+ if ($color.includes("calc(")) {
+ $color = lowerCalcExpression($color);
+ }
+ const color = parse($color);
+ rgbaParseCache.set(key, color);
+ return color;
+ }
+ function parseToHSLWithCache(color) {
+ if (hslaParseCache.has(color)) {
+ return hslaParseCache.get(color);
+ }
+ const rgb = parseColorWithCache(color);
+ if (!rgb) {
+ return null;
+ }
+ const hsl = rgbToHSL(rgb);
+ hslaParseCache.set(color, hsl);
+ return hsl;
+ }
+ function clearColorCache() {
+ hslaParseCache.clear();
+ rgbaParseCache.clear();
+ }
+ function hslToRGB({h, s, l, a = 1}) {
+ if (s === 0) {
+ const [r, b, g] = [l, l, l].map((x) => Math.round(x * 255));
+ return {r, g, b, a};
+ }
+ const c = (1 - Math.abs(2 * l - 1)) * s;
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
+ const m = l - c / 2;
+ const [r, g, b] = (
+ h < 60
+ ? [c, x, 0]
+ : h < 120
+ ? [x, c, 0]
+ : h < 180
+ ? [0, c, x]
+ : h < 240
+ ? [0, x, c]
+ : h < 300
+ ? [x, 0, c]
+ : [c, 0, x]
+ ).map((n) => Math.round((n + m) * 255));
+ return {r, g, b, a};
+ }
+ function rgbToHSL({r: r255, g: g255, b: b255, a = 1}) {
+ const r = r255 / 255;
+ const g = g255 / 255;
+ const b = b255 / 255;
+ const max = Math.max(r, g, b);
+ const min = Math.min(r, g, b);
+ const c = max - min;
+ const l = (max + min) / 2;
+ if (c === 0) {
+ return {h: 0, s: 0, l, a};
+ }
+ let h =
+ (max === r
+ ? ((g - b) / c) % 6
+ : max === g
+ ? (b - r) / c + 2
+ : (r - g) / c + 4) * 60;
+ if (h < 0) {
+ h += 360;
+ }
+ const s = c / (1 - Math.abs(2 * l - 1));
+ return {h, s, l, a};
+ }
+ function toFixed(n, digits = 0) {
+ const fixed = n.toFixed(digits);
+ if (digits === 0) {
+ return fixed;
+ }
+ const dot = fixed.indexOf(".");
+ if (dot >= 0) {
+ const zerosMatch = fixed.match(/0+$/);
+ if (zerosMatch) {
+ if (zerosMatch.index === dot + 1) {
+ return fixed.substring(0, dot);
+ }
+ return fixed.substring(0, zerosMatch.index);
+ }
+ }
+ return fixed;
+ }
+ function rgbToString(rgb) {
+ const {r, g, b, a} = rgb;
+ if (a != null && a < 1) {
+ return `rgba(${toFixed(r)}, ${toFixed(g)}, ${toFixed(b)}, ${toFixed(a, 2)})`;
+ }
+ return `rgb(${toFixed(r)}, ${toFixed(g)}, ${toFixed(b)})`;
+ }
+ function rgbToHexString({r, g, b, a}) {
+ return `#${(a != null && a < 1
+ ? [r, g, b, Math.round(a * 255)]
+ : [r, g, b]
+ )
+ .map((x) => {
+ return `${x < 16 ? "0" : ""}${x.toString(16)}`;
+ })
+ .join("")}`;
+ }
+ function hslToString(hsl) {
+ const {h, s, l, a} = hsl;
+ if (a != null && a < 1) {
+ return `hsla(${toFixed(h)}, ${toFixed(s * 100)}%, ${toFixed(l * 100)}%, ${toFixed(a, 2)})`;
+ }
+ return `hsl(${toFixed(h)}, ${toFixed(s * 100)}%, ${toFixed(l * 100)}%)`;
+ }
+ const rgbMatch = /^rgba?\([^\(\)]+\)$/;
+ const hslMatch = /^hsla?\([^\(\)]+\)$/;
+ const hexMatch = /^#[0-9a-f]+$/i;
+ const supportedColorFuncs = [
+ "color",
+ "color-mix",
+ "hwb",
+ "lab",
+ "lch",
+ "oklab",
+ "oklch"
+ ];
+ function parse($color) {
+ const c = $color.trim().toLowerCase();
+ if (c.includes("(from ")) {
+ if (c.indexOf("(from") !== c.lastIndexOf("(from")) {
+ return null;
+ }
+ return domParseColor(c);
+ }
+ if (c.match(rgbMatch)) {
+ if (c.startsWith("rgb(#") || c.startsWith("rgba(#")) {
+ if (c.lastIndexOf("rgb") > 0) {
+ return null;
+ }
+ return domParseColor(c);
+ }
+ return parseRGB(c);
+ }
+ if (c.match(hslMatch)) {
+ return parseHSL(c);
+ }
+ if (c.match(hexMatch)) {
+ return parseHex(c);
+ }
+ if (knownColors.has(c)) {
+ return getColorByName(c);
+ }
+ if (systemColors.has(c)) {
+ return getSystemColor(c);
+ }
+ if (c === "transparent") {
+ return {r: 0, g: 0, b: 0, a: 0};
+ }
+ if (
+ c.endsWith(")") &&
+ supportedColorFuncs.some(
+ (fn) =>
+ c.startsWith(fn) &&
+ c[fn.length] === "(" &&
+ c.lastIndexOf(fn) === 0
+ )
+ ) {
+ return domParseColor(c);
+ }
+ if (c.startsWith("light-dark(") && c.endsWith(")")) {
+ const match = c.match(
+ /^light-dark\(\s*([a-z]+(\(.*\))?),\s*([a-z]+(\(.*\))?)\s*\)$/
+ );
+ if (match) {
+ const schemeColor = isSystemDarkModeEnabled()
+ ? match[3]
+ : match[1];
+ return parse(schemeColor);
+ }
+ }
+ return null;
+ }
+ const C_0 = "0".charCodeAt(0);
+ const C_9 = "9".charCodeAt(0);
+ const C_e = "e".charCodeAt(0);
+ const C_DOT = ".".charCodeAt(0);
+ const C_PLUS = "+".charCodeAt(0);
+ const C_MINUS = "-".charCodeAt(0);
+ const C_SPACE = " ".charCodeAt(0);
+ const C_COMMA = ",".charCodeAt(0);
+ const C_SLASH = "/".charCodeAt(0);
+ const C_PERCENT = "%".charCodeAt(0);
+ function getNumbersFromString(input, range, units) {
+ const numbers = [];
+ const searchStart = input.indexOf("(") + 1;
+ const searchEnd = input.length - 1;
+ let numStart = -1;
+ let unitStart = -1;
+ const push = (matchEnd) => {
+ const numEnd = unitStart > -1 ? unitStart : matchEnd;
+ const $num = input.slice(numStart, numEnd);
+ let n = parseFloat($num);
+ const r = range[numbers.length];
+ if (unitStart > -1) {
+ const unit = input.slice(unitStart, matchEnd);
+ const u = units[unit];
+ if (u != null) {
+ n *= r / u;
+ }
+ }
+ if (r > 1) {
+ n = Math.round(n);
+ }
+ numbers.push(n);
+ numStart = -1;
+ unitStart = -1;
+ };
+ for (let i = searchStart; i < searchEnd; i++) {
+ const c = input.charCodeAt(i);
+ const isNumChar =
+ (c >= C_0 && c <= C_9) ||
+ c === C_DOT ||
+ c === C_PLUS ||
+ c === C_MINUS ||
+ c === C_e;
+ const isDelimiter = c === C_SPACE || c === C_COMMA || c === C_SLASH;
+ if (isNumChar) {
+ if (numStart === -1) {
+ numStart = i;
+ }
+ } else if (numStart > -1) {
+ if (isDelimiter) {
+ push(i);
+ } else if (unitStart === -1) {
+ unitStart = i;
+ }
+ }
+ }
+ if (numStart > -1) {
+ push(searchEnd);
+ }
+ return numbers;
+ }
+ const rgbRange = [255, 255, 255, 1];
+ const rgbUnits = {"%": 100};
+ function getRGBValues(input) {
+ const CHAR_CODE_0 = 48;
+ const length = input.length;
+ let i = 0;
+ let digitsCount = 0;
+ let digitSequence = false;
+ let floatDigitsCount = -1;
+ let delimiter = C_SPACE;
+ let channel = -1;
+ let result = null;
+ while (i < length) {
+ const c = input.charCodeAt(i);
+ if ((c >= C_0 && c <= C_9) || c === C_DOT) {
+ if (!digitSequence) {
+ digitSequence = true;
+ digitsCount = 0;
+ floatDigitsCount = -1;
+ channel++;
+ if (channel === 3 && result) {
+ result[3] = 0;
+ }
+ if (channel > 3) {
+ return null;
+ }
+ }
+ if (c === C_DOT) {
+ if (floatDigitsCount > 0) {
+ return null;
+ }
+ floatDigitsCount = 0;
+ } else {
+ const d = c - CHAR_CODE_0;
+ if (!result) {
+ result = [0, 0, 0, 1];
+ }
+ if (floatDigitsCount > -1) {
+ floatDigitsCount++;
+ result[channel] += d / 10 ** floatDigitsCount;
+ } else {
+ digitsCount++;
+ if (digitsCount > 3) {
+ return null;
+ }
+ result[channel] = result[channel] * 10 + d;
+ }
+ }
+ } else if (c === C_PERCENT) {
+ if (
+ channel < 0 ||
+ channel > 3 ||
+ delimiter !== C_SPACE ||
+ !result
+ ) {
+ return null;
+ }
+ result[channel] =
+ channel < 3
+ ? Math.round((result[channel] * 255) / 100)
+ : result[channel] / 100;
+ digitSequence = false;
+ } else {
+ digitSequence = false;
+ if (c === C_SPACE) {
+ if (channel === 0) {
+ delimiter = c;
+ }
+ } else if (c === C_COMMA) {
+ if (channel === -1) {
+ return null;
+ }
+ delimiter = C_COMMA;
+ } else if (c === C_SLASH) {
+ if (channel !== 2 || delimiter !== C_SPACE) {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+ i++;
+ }
+ if (channel < 2 || channel > 3) {
+ return null;
+ }
+ return result;
+ }
+ function parseRGB($rgb) {
+ const [r, g, b, a = 1] = getNumbersFromString($rgb, rgbRange, rgbUnits);
+ if (r == null || g == null || b == null || a == null) {
+ return null;
+ }
+ return {r, g, b, a};
+ }
+ const hslRange = [360, 1, 1, 1];
+ const hslUnits = {"%": 100, "deg": 360, "rad": 2 * Math.PI, "turn": 1};
+ function parseHSL($hsl) {
+ const [h, s, l, a = 1] = getNumbersFromString($hsl, hslRange, hslUnits);
+ if (h == null || s == null || l == null || a == null) {
+ return null;
+ }
+ return hslToRGB({h, s, l, a});
+ }
+ const C_A = "A".charCodeAt(0);
+ const C_F = "F".charCodeAt(0);
+ const C_a = "a".charCodeAt(0);
+ const C_f = "f".charCodeAt(0);
+ function parseHex($hex) {
+ const length = $hex.length;
+ const digitCount = length - 1;
+ const isShort = digitCount === 3 || digitCount === 4;
+ const isLong = digitCount === 6 || digitCount === 8;
+ if (!isShort && !isLong) {
+ return null;
+ }
+ const hex = (i) => {
+ const c = $hex.charCodeAt(i);
+ if (c >= C_A && c <= C_F) {
+ return c + 10 - C_A;
+ }
+ if (c >= C_a && c <= C_f) {
+ return c + 10 - C_a;
+ }
+ return c - C_0;
+ };
+ let r;
+ let g;
+ let b;
+ let a = 1;
+ if (isShort) {
+ r = hex(1) * 17;
+ g = hex(2) * 17;
+ b = hex(3) * 17;
+ if (digitCount === 4) {
+ a = (hex(4) * 17) / 255;
+ }
+ } else {
+ r = hex(1) * 16 + hex(2);
+ g = hex(3) * 16 + hex(4);
+ b = hex(5) * 16 + hex(6);
+ if (digitCount === 8) {
+ a = (hex(7) * 16 + hex(8)) / 255;
+ }
+ }
+ return {r, g, b, a};
+ }
+ function getColorByName($color) {
+ const n = knownColors.get($color);
+ return {
+ r: (n >> 16) & 255,
+ g: (n >> 8) & 255,
+ b: (n >> 0) & 255,
+ a: 1
+ };
+ }
+ function getSystemColor($color) {
+ const n = systemColors.get($color);
+ return {
+ r: (n >> 16) & 255,
+ g: (n >> 8) & 255,
+ b: (n >> 0) & 255,
+ a: 1
+ };
+ }
+ function lowerCalcExpression(color) {
+ let searchIndex = 0;
+ const replaceBetweenIndices = (start, end, replacement) => {
+ color =
+ color.substring(0, start) + replacement + color.substring(end);
+ };
+ while ((searchIndex = color.indexOf("calc(")) !== -1) {
+ const range = getParenthesesRange(color, searchIndex);
+ if (!range) {
+ break;
+ }
+ let slice = color.slice(range.start + 1, range.end - 1);
+ const includesPercentage = slice.includes("%");
+ slice = slice.split("%").join("");
+ const output = Math.round(evalMath(slice));
+ replaceBetweenIndices(
+ range.start - 4,
+ range.end,
+ output + (includesPercentage ? "%" : "")
+ );
+ }
+ return color;
+ }
+ const knownColors = new Map(
+ Object.entries({
+ aliceblue: 0xf0f8ff,
+ antiquewhite: 0xfaebd7,
+ aqua: 0x00ffff,
+ aquamarine: 0x7fffd4,
+ azure: 0xf0ffff,
+ beige: 0xf5f5dc,
+ bisque: 0xffe4c4,
+ black: 0x000000,
+ blanchedalmond: 0xffebcd,
+ blue: 0x0000ff,
+ blueviolet: 0x8a2be2,
+ brown: 0xa52a2a,
+ burlywood: 0xdeb887,
+ cadetblue: 0x5f9ea0,
+ chartreuse: 0x7fff00,
+ chocolate: 0xd2691e,
+ coral: 0xff7f50,
+ cornflowerblue: 0x6495ed,
+ cornsilk: 0xfff8dc,
+ crimson: 0xdc143c,
+ cyan: 0x00ffff,
+ darkblue: 0x00008b,
+ darkcyan: 0x008b8b,
+ darkgoldenrod: 0xb8860b,
+ darkgray: 0xa9a9a9,
+ darkgrey: 0xa9a9a9,
+ darkgreen: 0x006400,
+ darkkhaki: 0xbdb76b,
+ darkmagenta: 0x8b008b,
+ darkolivegreen: 0x556b2f,
+ darkorange: 0xff8c00,
+ darkorchid: 0x9932cc,
+ darkred: 0x8b0000,
+ darksalmon: 0xe9967a,
+ darkseagreen: 0x8fbc8f,
+ darkslateblue: 0x483d8b,
+ darkslategray: 0x2f4f4f,
+ darkslategrey: 0x2f4f4f,
+ darkturquoise: 0x00ced1,
+ darkviolet: 0x9400d3,
+ deeppink: 0xff1493,
+ deepskyblue: 0x00bfff,
+ dimgray: 0x696969,
+ dimgrey: 0x696969,
+ dodgerblue: 0x1e90ff,
+ firebrick: 0xb22222,
+ floralwhite: 0xfffaf0,
+ forestgreen: 0x228b22,
+ fuchsia: 0xff00ff,
+ gainsboro: 0xdcdcdc,
+ ghostwhite: 0xf8f8ff,
+ gold: 0xffd700,
+ goldenrod: 0xdaa520,
+ gray: 0x808080,
+ grey: 0x808080,
+ green: 0x008000,
+ greenyellow: 0xadff2f,
+ honeydew: 0xf0fff0,
+ hotpink: 0xff69b4,
+ indianred: 0xcd5c5c,
+ indigo: 0x4b0082,
+ ivory: 0xfffff0,
+ khaki: 0xf0e68c,
+ lavender: 0xe6e6fa,
+ lavenderblush: 0xfff0f5,
+ lawngreen: 0x7cfc00,
+ lemonchiffon: 0xfffacd,
+ lightblue: 0xadd8e6,
+ lightcoral: 0xf08080,
+ lightcyan: 0xe0ffff,
+ lightgoldenrodyellow: 0xfafad2,
+ lightgray: 0xd3d3d3,
+ lightgrey: 0xd3d3d3,
+ lightgreen: 0x90ee90,
+ lightpink: 0xffb6c1,
+ lightsalmon: 0xffa07a,
+ lightseagreen: 0x20b2aa,
+ lightskyblue: 0x87cefa,
+ lightslategray: 0x778899,
+ lightslategrey: 0x778899,
+ lightsteelblue: 0xb0c4de,
+ lightyellow: 0xffffe0,
+ lime: 0x00ff00,
+ limegreen: 0x32cd32,
+ linen: 0xfaf0e6,
+ magenta: 0xff00ff,
+ maroon: 0x800000,
+ mediumaquamarine: 0x66cdaa,
+ mediumblue: 0x0000cd,
+ mediumorchid: 0xba55d3,
+ mediumpurple: 0x9370db,
+ mediumseagreen: 0x3cb371,
+ mediumslateblue: 0x7b68ee,
+ mediumspringgreen: 0x00fa9a,
+ mediumturquoise: 0x48d1cc,
+ mediumvioletred: 0xc71585,
+ midnightblue: 0x191970,
+ mintcream: 0xf5fffa,
+ mistyrose: 0xffe4e1,
+ moccasin: 0xffe4b5,
+ navajowhite: 0xffdead,
+ navy: 0x000080,
+ oldlace: 0xfdf5e6,
+ olive: 0x808000,
+ olivedrab: 0x6b8e23,
+ orange: 0xffa500,
+ orangered: 0xff4500,
+ orchid: 0xda70d6,
+ palegoldenrod: 0xeee8aa,
+ palegreen: 0x98fb98,
+ paleturquoise: 0xafeeee,
+ palevioletred: 0xdb7093,
+ papayawhip: 0xffefd5,
+ peachpuff: 0xffdab9,
+ peru: 0xcd853f,
+ pink: 0xffc0cb,
+ plum: 0xdda0dd,
+ powderblue: 0xb0e0e6,
+ purple: 0x800080,
+ rebeccapurple: 0x663399,
+ red: 0xff0000,
+ rosybrown: 0xbc8f8f,
+ royalblue: 0x4169e1,
+ saddlebrown: 0x8b4513,
+ salmon: 0xfa8072,
+ sandybrown: 0xf4a460,
+ seagreen: 0x2e8b57,
+ seashell: 0xfff5ee,
+ sienna: 0xa0522d,
+ silver: 0xc0c0c0,
+ skyblue: 0x87ceeb,
+ slateblue: 0x6a5acd,
+ slategray: 0x708090,
+ slategrey: 0x708090,
+ snow: 0xfffafa,
+ springgreen: 0x00ff7f,
+ steelblue: 0x4682b4,
+ tan: 0xd2b48c,
+ teal: 0x008080,
+ thistle: 0xd8bfd8,
+ tomato: 0xff6347,
+ turquoise: 0x40e0d0,
+ violet: 0xee82ee,
+ wheat: 0xf5deb3,
+ white: 0xffffff,
+ whitesmoke: 0xf5f5f5,
+ yellow: 0xffff00,
+ yellowgreen: 0x9acd32
+ })
+ );
+ const systemColors = new Map(
+ Object.entries({
+ "ActiveBorder": 0x3b99fc,
+ "ActiveCaption": 0x000000,
+ "AppWorkspace": 0xaaaaaa,
+ "Background": 0x6363ce,
+ "ButtonFace": 0xffffff,
+ "ButtonHighlight": 0xe9e9e9,
+ "ButtonShadow": 0x9fa09f,
+ "ButtonText": 0x000000,
+ "CaptionText": 0x000000,
+ "GrayText": 0x7f7f7f,
+ "Highlight": 0xb2d7ff,
+ "HighlightText": 0x000000,
+ "InactiveBorder": 0xffffff,
+ "InactiveCaption": 0xffffff,
+ "InactiveCaptionText": 0x000000,
+ "InfoBackground": 0xfbfcc5,
+ "InfoText": 0x000000,
+ "Menu": 0xf6f6f6,
+ "MenuText": 0xffffff,
+ "Scrollbar": 0xaaaaaa,
+ "ThreeDDarkShadow": 0x000000,
+ "ThreeDFace": 0xc0c0c0,
+ "ThreeDHighlight": 0xffffff,
+ "ThreeDLightShadow": 0xffffff,
+ "ThreeDShadow": 0x000000,
+ "Window": 0xececec,
+ "WindowFrame": 0xaaaaaa,
+ "WindowText": 0x000000,
+ "-webkit-focus-ring-color": 0xe59700
+ }).map(([key, value]) => [key.toLowerCase(), value])
+ );
+ function getSRGBLightness(r, g, b) {
+ return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
+ }
+ let canvas$1;
+ let context$1;
+ function domParseColor($color) {
+ if (!context$1) {
+ canvas$1 = document.createElement("canvas");
+ canvas$1.width = 1;
+ canvas$1.height = 1;
+ context$1 = canvas$1.getContext("2d", {willReadFrequently: true});
+ }
+ context$1.fillStyle = $color;
+ context$1.fillRect(0, 0, 1, 1);
+ const d = context$1.getImageData(0, 0, 1, 1).data;
+ const color = `rgba(${d[0]}, ${d[1]}, ${d[2]}, ${(d[3] / 255).toFixed(2)})`;
+ return parseRGB(color);
+ }
+
+ function throttle(callback) {
+ let pending = false;
+ let frameId = null;
+ let lastArgs;
+ const throttled = (...args) => {
+ lastArgs = args;
+ if (frameId) {
+ pending = true;
+ } else {
+ callback(...lastArgs);
+ frameId = requestAnimationFrame(() => {
+ frameId = null;
+ if (pending) {
+ callback(...lastArgs);
+ pending = false;
+ }
+ });
+ }
+ };
+ const cancel = () => {
+ cancelAnimationFrame(frameId);
+ pending = false;
+ frameId = null;
+ };
+ return Object.assign(throttled, {cancel});
+ }
+ function createAsyncTasksQueue() {
+ const tasks = [];
+ let frameId = null;
+ function runTasks() {
+ let task;
+ while ((task = tasks.shift())) {
+ task();
+ }
+ frameId = null;
+ }
+ function add(task) {
+ tasks.push(task);
+ if (!frameId) {
+ frameId = requestAnimationFrame(runTasks);
+ }
+ }
+ function cancel() {
+ tasks.splice(0);
+ cancelAnimationFrame(frameId);
+ frameId = null;
+ }
+ return {add, cancel};
+ }
+
+ function hexify(number) {
+ return (number < 16 ? "0" : "") + number.toString(16);
+ }
+ function generateUID() {
+ if ("randomUUID" in crypto) {
+ const uuid = crypto.randomUUID();
+ return (
+ uuid.substring(0, 8) +
+ uuid.substring(9, 13) +
+ uuid.substring(14, 18) +
+ uuid.substring(19, 23) +
+ uuid.substring(24)
+ );
+ }
+ if ("getRandomValues" in crypto) {
+ return Array.from(crypto.getRandomValues(new Uint8Array(16)))
+ .map((x) => hexify(x))
+ .join("");
+ }
+ return Math.floor(Math.random() * 2 ** 55).toString(36);
+ }
+
+ let documentVisibilityListener = null;
+ let documentIsVisible_ = !document.hidden;
+ const listenerOptions = {
+ capture: true,
+ passive: true
+ };
+ function watchForDocumentVisibility() {
+ document.addEventListener(
+ "visibilitychange",
+ documentVisibilityListener,
+ listenerOptions
+ );
+ window.addEventListener(
+ "pageshow",
+ documentVisibilityListener,
+ listenerOptions
+ );
+ window.addEventListener(
+ "focus",
+ documentVisibilityListener,
+ listenerOptions
+ );
+ }
+ function stopWatchingForDocumentVisibility() {
+ document.removeEventListener(
+ "visibilitychange",
+ documentVisibilityListener,
+ listenerOptions
+ );
+ window.removeEventListener(
+ "pageshow",
+ documentVisibilityListener,
+ listenerOptions
+ );
+ window.removeEventListener(
+ "focus",
+ documentVisibilityListener,
+ listenerOptions
+ );
+ }
+ function setDocumentVisibilityListener(callback) {
+ const alreadyWatching = Boolean(documentVisibilityListener);
+ documentVisibilityListener = () => {
+ if (!document.hidden) {
+ removeDocumentVisibilityListener();
+ callback();
+ documentIsVisible_ = true;
+ }
+ };
+ if (!alreadyWatching) {
+ watchForDocumentVisibility();
+ }
+ }
+ function removeDocumentVisibilityListener() {
+ stopWatchingForDocumentVisibility();
+ documentVisibilityListener = null;
+ }
+ function documentIsVisible() {
+ return documentIsVisible_;
+ }
+
+ function getDuration(time) {
+ let duration = 0;
+ if (time.seconds) {
+ duration += time.seconds * 1000;
+ }
+ if (time.minutes) {
+ duration += time.minutes * 60 * 1000;
+ }
+ if (time.hours) {
+ duration += time.hours * 60 * 60 * 1000;
+ }
+ if (time.days) {
+ duration += time.days * 24 * 60 * 60 * 1000;
+ }
+ return duration;
+ }
+
+ function logInfo(...args) {}
+ function logWarn(...args) {}
+
+ function removeNode(node) {
+ node && node.parentNode && node.parentNode.removeChild(node);
+ }
+ function watchForNodePosition(node, mode, onRestore = Function.prototype) {
+ const MAX_ATTEMPTS_COUNT = 10;
+ const RETRY_TIMEOUT = getDuration({seconds: 2});
+ const ATTEMPTS_INTERVAL = getDuration({seconds: 10});
+ let prevSibling = node.previousSibling;
+ let parent = node.parentNode;
+ if (!parent) {
+ throw new Error(
+ "Unable to watch for node position: parent element not found"
+ );
+ }
+ if (mode === "prev-sibling" && !prevSibling) {
+ throw new Error(
+ "Unable to watch for node position: there is no previous sibling"
+ );
+ }
+ let attempts = 0;
+ let start = null;
+ let timeoutId = null;
+ const restore = throttle(() => {
+ if (timeoutId) {
+ return;
+ }
+ attempts++;
+ const now = Date.now();
+ if (start == null) {
+ start = now;
+ } else if (attempts >= MAX_ATTEMPTS_COUNT) {
+ if (now - start < ATTEMPTS_INTERVAL) {
+ logWarn(
+ `Node position watcher paused: retry in ${RETRY_TIMEOUT}ms`,
+ node,
+ prevSibling
+ );
+ timeoutId = setTimeout(() => {
+ start = null;
+ attempts = 0;
+ timeoutId = null;
+ restore();
+ }, RETRY_TIMEOUT);
+ return;
+ }
+ start = now;
+ attempts = 1;
+ }
+ if (mode === "head") {
+ if (prevSibling && prevSibling.parentNode !== parent) {
+ logWarn(
+ "Sibling moved, moving node to the head end",
+ node,
+ prevSibling,
+ parent
+ );
+ prevSibling = document.head.lastChild;
+ }
+ }
+ if (mode === "prev-sibling") {
+ if (prevSibling.parentNode == null) {
+ logWarn(
+ "Unable to restore node position: sibling was removed",
+ node,
+ prevSibling,
+ parent
+ );
+ stop();
+ return;
+ }
+ if (prevSibling.parentNode !== parent) {
+ logWarn(
+ "Style was moved to another parent",
+ node,
+ prevSibling,
+ parent
+ );
+ updateParent(prevSibling.parentNode);
+ }
+ }
+ if (mode === "head" && !parent.isConnected) {
+ parent = document.head;
+ }
+ logWarn("Restoring node position", node, prevSibling, parent);
+ parent.insertBefore(
+ node,
+ prevSibling && prevSibling.isConnected
+ ? prevSibling.nextSibling
+ : parent.firstChild
+ );
+ observer.takeRecords();
+ onRestore && onRestore();
+ });
+ const observer = new MutationObserver(() => {
+ if (
+ (mode === "head" &&
+ (node.parentNode !== parent ||
+ !node.parentNode.isConnected)) ||
+ (mode === "prev-sibling" &&
+ node.previousSibling !== prevSibling)
+ ) {
+ restore();
+ }
+ });
+ const run = () => {
+ observer.observe(parent, {childList: true});
+ };
+ const stop = () => {
+ clearTimeout(timeoutId);
+ observer.disconnect();
+ restore.cancel();
+ };
+ const skip = () => {
+ observer.takeRecords();
+ };
+ const updateParent = (parentNode) => {
+ parent = parentNode;
+ stop();
+ run();
+ };
+ run();
+ return {run, stop, skip};
+ }
+ function iterateShadowHosts(root, iterator) {
+ if (root == null) {
+ return;
+ }
+ const walker = document.createTreeWalker(
+ root,
+ NodeFilter.SHOW_ELEMENT,
+ {
+ acceptNode(node) {
+ return node.shadowRoot == null
+ ? NodeFilter.FILTER_SKIP
+ : NodeFilter.FILTER_ACCEPT;
+ }
+ }
+ );
+ for (
+ let node = root.shadowRoot ? walker.currentNode : walker.nextNode();
+ node != null;
+ node = walker.nextNode()
+ ) {
+ if (node.classList.contains("surfingkeys_hints_host")) {
+ continue;
+ }
+ iterator(node);
+ iterateShadowHosts(node.shadowRoot, iterator);
+ }
+ }
+ let isDOMReady = () => {
+ return (
+ document.readyState === "complete" ||
+ document.readyState === "interactive"
+ );
+ };
+ function setIsDOMReady(newFunc) {
+ isDOMReady = newFunc;
+ }
+ const readyStateListeners = new Set();
+ function addDOMReadyListener(listener) {
+ isDOMReady() ? listener() : readyStateListeners.add(listener);
+ }
+ function removeDOMReadyListener(listener) {
+ readyStateListeners.delete(listener);
+ }
+ function isReadyStateComplete() {
+ return document.readyState === "complete";
+ }
+ const readyStateCompleteListeners = new Set();
+ function addReadyStateCompleteListener(listener) {
+ isReadyStateComplete()
+ ? listener()
+ : readyStateCompleteListeners.add(listener);
+ }
+ function cleanReadyStateCompleteListeners() {
+ readyStateCompleteListeners.clear();
+ }
+ if (!isDOMReady()) {
+ const onReadyStateChange = () => {
+ if (isDOMReady()) {
+ readyStateListeners.forEach((listener) => listener());
+ readyStateListeners.clear();
+ if (isReadyStateComplete()) {
+ document.removeEventListener(
+ "readystatechange",
+ onReadyStateChange
+ );
+ readyStateCompleteListeners.forEach((listener) =>
+ listener()
+ );
+ readyStateCompleteListeners.clear();
+ }
+ }
+ };
+ document.addEventListener("readystatechange", onReadyStateChange);
+ }
+ const HUGE_MUTATIONS_COUNT = 1000;
+ function isHugeMutation(mutations) {
+ if (mutations.length > HUGE_MUTATIONS_COUNT) {
+ return true;
+ }
+ let addedNodesCount = 0;
+ for (let i = 0; i < mutations.length; i++) {
+ addedNodesCount += mutations[i].addedNodes.length;
+ if (addedNodesCount > HUGE_MUTATIONS_COUNT) {
+ return true;
+ }
+ }
+ return false;
+ }
+ function getElementsTreeOperations(mutations) {
+ const additions = new Set();
+ const deletions = new Set();
+ const moves = new Set();
+ mutations.forEach((m) => {
+ forEach(m.addedNodes, (n) => {
+ if (n instanceof Element && n.isConnected) {
+ additions.add(n);
+ }
+ });
+ forEach(m.removedNodes, (n) => {
+ if (n instanceof Element) {
+ if (n.isConnected) {
+ moves.add(n);
+ additions.delete(n);
+ } else {
+ deletions.add(n);
+ }
+ }
+ });
+ });
+ const duplicateAdditions = [];
+ const duplicateDeletions = [];
+ additions.forEach((node) => {
+ if (additions.has(node.parentElement)) {
+ duplicateAdditions.push(node);
+ }
+ });
+ deletions.forEach((node) => {
+ if (deletions.has(node.parentElement)) {
+ duplicateDeletions.push(node);
+ }
+ });
+ duplicateAdditions.forEach((node) => additions.delete(node));
+ duplicateDeletions.forEach((node) => deletions.delete(node));
+ return {additions, moves, deletions};
+ }
+ const optimizedTreeObservers = new Map();
+ const optimizedTreeCallbacks = new WeakMap();
+ function createOptimizedTreeObserver(root, callbacks) {
+ let observer;
+ let observerCallbacks;
+ let domReadyListener;
+ if (optimizedTreeObservers.has(root)) {
+ observer = optimizedTreeObservers.get(root);
+ observerCallbacks = optimizedTreeCallbacks.get(observer);
+ } else {
+ let hadHugeMutationsBefore = false;
+ let subscribedForReadyState = false;
+ observer = new MutationObserver((mutations) => {
+ if (isHugeMutation(mutations)) {
+ if (!hadHugeMutationsBefore || isDOMReady()) {
+ observerCallbacks.forEach(({onHugeMutations}) =>
+ onHugeMutations(root)
+ );
+ } else if (!subscribedForReadyState) {
+ domReadyListener = () =>
+ observerCallbacks.forEach(({onHugeMutations}) =>
+ onHugeMutations(root)
+ );
+ addDOMReadyListener(domReadyListener);
+ subscribedForReadyState = true;
+ }
+ hadHugeMutationsBefore = true;
+ } else {
+ const elementsOperations =
+ getElementsTreeOperations(mutations);
+ observerCallbacks.forEach(({onMinorMutations}) =>
+ onMinorMutations(root, elementsOperations)
+ );
+ }
+ });
+ observer.observe(root, {childList: true, subtree: true});
+ optimizedTreeObservers.set(root, observer);
+ observerCallbacks = new Set();
+ optimizedTreeCallbacks.set(observer, observerCallbacks);
+ }
+ observerCallbacks.add(callbacks);
+ return {
+ disconnect() {
+ observerCallbacks.delete(callbacks);
+ if (domReadyListener) {
+ removeDOMReadyListener(domReadyListener);
+ }
+ if (observerCallbacks.size === 0) {
+ observer.disconnect();
+ optimizedTreeCallbacks.delete(observer);
+ optimizedTreeObservers.delete(root);
+ }
+ }
+ };
+ }
+
+ function iterateCSSRules(
+ rules,
+ iterate,
+ onImportError,
+ importedSheets = new Set()
+ ) {
+ forEach(rules, (rule) => {
+ if (isStyleRule(rule)) {
+ iterate(rule);
+ if (rule.cssRules?.length > 0) {
+ iterateCSSRules(
+ rule.cssRules,
+ iterate,
+ onImportError,
+ importedSheets
+ );
+ }
+ } else if (isImportRule(rule)) {
+ try {
+ const importedSheet = rule.styleSheet;
+ if (!importedSheets.has(importedSheet)) {
+ importedSheets.add(importedSheet);
+ iterateCSSRules(
+ importedSheet.cssRules,
+ iterate,
+ onImportError,
+ importedSheets
+ );
+ }
+ } catch (err) {
+ onImportError?.();
+ }
+ } else if (isMediaRule(rule)) {
+ const media = Array.from(rule.media);
+ const isScreenOrAllOrQuery = media.some(
+ (m) =>
+ m.startsWith("screen") ||
+ m.startsWith("all") ||
+ m.startsWith("(")
+ );
+ const isNotScreen =
+ !isScreenOrAllOrQuery &&
+ media.some((m) =>
+ ignoredMedia.some((i) => m.startsWith(i))
+ );
+ if (isScreenOrAllOrQuery || !isNotScreen) {
+ iterateCSSRules(
+ rule.cssRules,
+ iterate,
+ onImportError,
+ importedSheets
+ );
+ }
+ } else if (isSupportsRule(rule)) {
+ if (CSS.supports(rule.conditionText)) {
+ iterateCSSRules(
+ rule.cssRules,
+ iterate,
+ onImportError,
+ importedSheets
+ );
+ }
+ } else if (isLayerRule(rule)) {
+ iterateCSSRules(
+ rule.cssRules,
+ iterate,
+ onImportError,
+ importedSheets
+ );
+ } else if (isContainerRule(rule)) {
+ iterateCSSRules(
+ rule.cssRules,
+ iterate,
+ onImportError,
+ importedSheets
+ );
+ } else {
+ logWarn(`CSSRule type not supported`, rule);
+ }
+ });
+ }
+ const ignoredMedia = [
+ "aural",
+ "braille",
+ "embossed",
+ "handheld",
+ "print",
+ "projection",
+ "speech",
+ "tty",
+ "tv"
+ ];
+ const shorthandVarDependantProperties = [
+ "background",
+ "border",
+ "border-color",
+ "border-bottom",
+ "border-left",
+ "border-right",
+ "border-top",
+ "outline",
+ "outline-color"
+ ];
+ const shorthandVarDepPropRegexps = isSafari
+ ? shorthandVarDependantProperties.map((prop) => {
+ const regexp = new RegExp(`${prop}:\\s*(.*?)\\s*;`);
+ return [prop, regexp];
+ })
+ : null;
+ function iterateCSSDeclarations(style, iterate) {
+ const cssText = style.cssText;
+ if (cssText.includes("var(")) {
+ if (isSafari) {
+ shorthandVarDepPropRegexps.forEach(([prop, regexp]) => {
+ const match = cssText.match(regexp);
+ if (match && match[1]) {
+ const val = match[1].trim();
+ iterate(prop, val);
+ }
+ });
+ } else {
+ shorthandVarDependantProperties.forEach((prop) => {
+ const val = style.getPropertyValue(prop);
+ if (val && val.includes("var(")) {
+ iterate(prop, val);
+ }
+ });
+ }
+ }
+ if (
+ (cssText.includes("background-color: ;") ||
+ cssText.includes("background-image: ;")) &&
+ !style.getPropertyValue("background")
+ ) {
+ handleEmptyShorthand("background", style, iterate);
+ }
+ if (
+ cssText.includes("border-") &&
+ cssText.includes("-color: ;") &&
+ !style.getPropertyValue("border")
+ ) {
+ handleEmptyShorthand("border", style, iterate);
+ }
+ forEach(style, (property) => {
+ const value = style.getPropertyValue(property).trim();
+ if (!value) {
+ return;
+ }
+ iterate(property, value);
+ });
+ }
+ function handleEmptyShorthand(shorthand, style, iterate) {
+ const parentRule = style.parentRule;
+ if (isStyleRule(parentRule)) {
+ const sourceCSSText =
+ parentRule.parentStyleSheet?.ownerNode?.textContent;
+ if (sourceCSSText) {
+ let escapedSelector = escapeRegExpSpecialChars(
+ parentRule.selectorText
+ );
+ escapedSelector = escapedSelector.replaceAll(/\s+/g, "\\s*");
+ escapedSelector = escapedSelector.replaceAll(/::/g, "::?");
+ const regexp = new RegExp(
+ `${escapedSelector}\\s*{[^}]*${shorthand}:\\s*([^;}]+)`
+ );
+ const match = sourceCSSText.match(regexp);
+ if (match) {
+ iterate(shorthand, match[1]);
+ }
+ } else if (shorthand === "background") {
+ iterate("background-color", "#ffffff");
+ iterate("background-image", "none");
+ }
+ }
+ }
+ const cssURLRegex = /url\((('.*?')|(".*?")|([^\)]*?))\)/g;
+ const cssImportRegex =
+ /@import\s*(url\()?(('.+?')|(".+?")|([^\)]*?))\)? ?(screen)?;?/gi;
+ function getCSSURLValue(cssURL) {
+ return cssURL
+ .trim()
+ .replace(/[\n\r\\]+/g, "")
+ .replace(/^url\((.*)\)$/, "$1")
+ .trim()
+ .replace(/^"(.*)"$/, "$1")
+ .replace(/^'(.*)'$/, "$1")
+ .replace(/(?:\\(.))/g, "$1");
+ }
+ function getCSSBaseBath(url) {
+ const cssURL = parseURL(url);
+ return `${cssURL.origin}${cssURL.pathname.replace(/\?.*$/, "").replace(/(\/)([^\/]+)$/i, "$1")}`;
+ }
+ function replaceCSSRelativeURLsWithAbsolute($css, cssBasePath) {
+ return $css.replace(cssURLRegex, (match) => {
+ try {
+ const url = getCSSURLValue(match);
+ const absoluteURL = getAbsoluteURL(cssBasePath, url);
+ const escapedURL = absoluteURL.replaceAll("'", "\\'");
+ return `url('${escapedURL}')`;
+ } catch (err) {
+ logWarn(
+ "Not able to replace relative URL with Absolute URL, skipping"
+ );
+ return match;
+ }
+ });
+ }
+ const fontFaceRegex = /@font-face\s*{[^}]*}/g;
+ function replaceCSSFontFace($css) {
+ return $css.replace(fontFaceRegex, "");
+ }
+ const styleRules = new WeakSet();
+ const importRules = new WeakSet();
+ const mediaRules = new WeakSet();
+ const supportsRules = new WeakSet();
+ const layerRules = new WeakSet();
+ const containerRules = new WeakSet();
+ function isStyleRule(rule) {
+ if (!rule) {
+ return false;
+ }
+ if (styleRules.has(rule)) {
+ return true;
+ }
+ if (rule.selectorText) {
+ styleRules.add(rule);
+ return true;
+ }
+ return false;
+ }
+ function isImportRule(rule) {
+ if (!rule) {
+ return false;
+ }
+ if (styleRules.has(rule)) {
+ return false;
+ }
+ if (importRules.has(rule)) {
+ return true;
+ }
+ if (rule.href) {
+ importRules.add(rule);
+ return true;
+ }
+ return false;
+ }
+ function isMediaRule(rule) {
+ if (!rule) {
+ return false;
+ }
+ if (styleRules.has(rule)) {
+ return false;
+ }
+ if (mediaRules.has(rule)) {
+ return true;
+ }
+ if (rule.media) {
+ mediaRules.add(rule);
+ return true;
+ }
+ return false;
+ }
+ function isSupportsRule(rule) {
+ if (!rule) {
+ return false;
+ }
+ if (styleRules.has(rule)) {
+ return false;
+ }
+ if (supportsRules.has(rule)) {
+ return true;
+ }
+ if (rule instanceof CSSSupportsRule) {
+ supportsRules.add(rule);
+ return true;
+ }
+ return false;
+ }
+ function isLayerRule(rule) {
+ if (!rule) {
+ return false;
+ }
+ if (styleRules.has(rule)) {
+ return false;
+ }
+ if (layerRules.has(rule)) {
+ return true;
+ }
+ if (isLayerRuleSupported && rule instanceof CSSLayerBlockRule) {
+ layerRules.add(rule);
+ return true;
+ }
+ return false;
+ }
+ function isContainerRule(rule) {
+ if (!rule) {
+ return false;
+ }
+ if (styleRules.has(rule)) {
+ return false;
+ }
+ if (containerRules.has(rule)) {
+ return true;
+ }
+ if (isContainerRuleSupported && rule instanceof CSSContainerRule) {
+ containerRules.add(rule);
+ return true;
+ }
+ return false;
+ }
+
+ const sheetsScopes = new WeakMap();
+ function defineSheetScope(sheet, node) {
+ sheetsScopes.set(sheet, node);
+ }
+ function getSheetScope(sheet) {
+ if (!sheet.ownerNode) {
+ return null;
+ }
+ if (sheetsScopes.has(sheet)) {
+ return sheetsScopes.get(sheet);
+ }
+ let node = sheet.ownerNode;
+ while (node) {
+ if (node instanceof ShadowRoot || node instanceof Document) {
+ defineSheetScope(sheet, node);
+ return node;
+ }
+ node = node.parentNode;
+ }
+ return null;
+ }
+
+ let variablesSheet;
+ const registeredColors = new Map();
+ function registerVariablesSheet(sheet) {
+ variablesSheet = sheet;
+ const types = ["background", "text", "border"];
+ registeredColors.forEach((registered) => {
+ types.forEach((type) => {
+ if (registered[type]) {
+ const {variable, value} = registered[type];
+ variablesSheet?.cssRules[0]?.style.setProperty(
+ variable,
+ value
+ );
+ }
+ });
+ });
+ }
+ function releaseVariablesSheet() {
+ variablesSheet = null;
+ clearColorPalette();
+ }
+ function getRegisteredVariableValue(type, registered) {
+ return `var(${registered[type].variable}, ${registered[type].value})`;
+ }
+ function getRegisteredColor(type, parsed) {
+ const hex = rgbToHexString(parsed);
+ const registered = registeredColors.get(hex);
+ if (registered?.[type]) {
+ return getRegisteredVariableValue(type, registered);
+ }
+ return null;
+ }
+ function registerColor(type, parsed, value) {
+ const hex = rgbToHexString(parsed);
+ let registered;
+ if (registeredColors.has(hex)) {
+ registered = registeredColors.get(hex);
+ } else {
+ const parsed = parseColorWithCache(hex);
+ registered = {parsed};
+ registeredColors.set(hex, registered);
+ }
+ const variable = `--darkreader-${type}-${hex.replace("#", "")}`;
+ registered[type] = {variable, value};
+ if (variablesSheet?.cssRules[0]?.style) {
+ (variablesSheet?.cssRules[0]).style.setProperty(variable, value);
+ }
+ return getRegisteredVariableValue(type, registered);
+ }
+ function getColorPalette() {
+ const background = [];
+ const border = [];
+ const text = [];
+ registeredColors.forEach((registered) => {
+ if (registered.background) {
+ background.push(registered.parsed);
+ }
+ if (registered.border) {
+ border.push(registered.parsed);
+ }
+ if (registered.text) {
+ text.push(registered.parsed);
+ }
+ });
+ return {background, border, text};
+ }
+ function clearColorPalette() {
+ registeredColors.clear();
+ }
+
+ function getBgPole(theme) {
+ const isDarkScheme = theme.mode === 1;
+ const prop = isDarkScheme
+ ? "darkSchemeBackgroundColor"
+ : "lightSchemeBackgroundColor";
+ return theme[prop];
+ }
+ function getFgPole(theme) {
+ const isDarkScheme = theme.mode === 1;
+ const prop = isDarkScheme
+ ? "darkSchemeTextColor"
+ : "lightSchemeTextColor";
+ return theme[prop];
+ }
+ const colorModificationCache = new Map();
+ function clearColorModificationCache() {
+ colorModificationCache.clear();
+ }
+ const rgbCacheKeys = ["r", "g", "b", "a"];
+ const themeCacheKeys = [
+ "mode",
+ "brightness",
+ "contrast",
+ "grayscale",
+ "sepia",
+ "darkSchemeBackgroundColor",
+ "darkSchemeTextColor",
+ "lightSchemeBackgroundColor",
+ "lightSchemeTextColor"
+ ];
+ function getCacheId(rgb, theme, poleA, poleB) {
+ let resultId = "";
+ rgbCacheKeys.forEach((key) => {
+ resultId += `${rgb[key]};`;
+ });
+ themeCacheKeys.forEach((key) => {
+ resultId += `${theme[key]};`;
+ });
+ resultId += `${poleA};${poleB}`;
+ return resultId;
+ }
+ function modifyColorWithCache(
+ rgb,
+ theme,
+ modifyHSL,
+ poleColor,
+ anotherPoleColor
+ ) {
+ let fnCache;
+ if (colorModificationCache.has(modifyHSL)) {
+ fnCache = colorModificationCache.get(modifyHSL);
+ } else {
+ fnCache = new Map();
+ colorModificationCache.set(modifyHSL, fnCache);
+ }
+ const id = getCacheId(rgb, theme, poleColor, anotherPoleColor);
+ if (fnCache.has(id)) {
+ return fnCache.get(id);
+ }
+ const hsl = rgbToHSL(rgb);
+ const pole = poleColor == null ? null : parseToHSLWithCache(poleColor);
+ const anotherPole =
+ anotherPoleColor == null
+ ? null
+ : parseToHSLWithCache(anotherPoleColor);
+ const modified = modifyHSL(hsl, pole, anotherPole);
+ const {r, g, b, a} = hslToRGB(modified);
+ const matrix = createFilterMatrix({...theme, mode: 0});
+ const [rf, gf, bf] = applyColorMatrix([r, g, b], matrix);
+ const color =
+ a === 1
+ ? rgbToHexString({r: rf, g: gf, b: bf})
+ : rgbToString({r: rf, g: gf, b: bf, a});
+ fnCache.set(id, color);
+ return color;
+ }
+ function modifyAndRegisterColor(type, rgb, theme, modifier) {
+ const registered = getRegisteredColor(type, rgb);
+ if (registered) {
+ return registered;
+ }
+ const value = modifier(rgb, theme);
+ return registerColor(type, rgb, value);
+ }
+ function modifyLightSchemeColor(rgb, theme) {
+ const poleBg = getBgPole(theme);
+ const poleFg = getFgPole(theme);
+ return modifyColorWithCache(
+ rgb,
+ theme,
+ modifyLightModeHSL,
+ poleFg,
+ poleBg
+ );
+ }
+ function modifyLightModeHSL({h, s, l, a}, poleFg, poleBg) {
+ const isDark = l < 0.5;
+ let isNeutral;
+ if (isDark) {
+ isNeutral = l < 0.2 || s < 0.12;
+ } else {
+ const isBlue = h > 200 && h < 280;
+ isNeutral = s < 0.24 || (l > 0.8 && isBlue);
+ }
+ let hx = h;
+ let sx = s;
+ if (isNeutral) {
+ if (isDark) {
+ hx = poleFg.h;
+ sx = poleFg.s;
+ } else {
+ hx = poleBg.h;
+ sx = poleBg.s;
+ }
+ }
+ const lx = scale(l, 0, 1, poleFg.l, poleBg.l);
+ return {h: hx, s: sx, l: lx, a};
+ }
+ const MAX_BG_LIGHTNESS = 0.4;
+ function modifyBgHSL({h, s, l, a}, pole) {
+ const isDark = l < 0.5;
+ const isBlue = h > 200 && h < 280;
+ const isNeutral = s < 0.12 || (l > 0.8 && isBlue);
+ if (isDark) {
+ const lx = scale(l, 0, 0.5, 0, MAX_BG_LIGHTNESS);
+ if (isNeutral) {
+ const hx = pole.h;
+ const sx = pole.s;
+ return {h: hx, s: sx, l: lx, a};
+ }
+ return {h, s, l: lx, a};
+ }
+ let lx = scale(l, 0.5, 1, MAX_BG_LIGHTNESS, pole.l);
+ if (isNeutral) {
+ const hx = pole.h;
+ const sx = pole.s;
+ return {h: hx, s: sx, l: lx, a};
+ }
+ let hx = h;
+ const isYellow = h > 60 && h < 180;
+ if (isYellow) {
+ const isCloserToGreen = h > 120;
+ if (isCloserToGreen) {
+ hx = scale(h, 120, 180, 135, 180);
+ } else {
+ hx = scale(h, 60, 120, 60, 105);
+ }
+ }
+ if (hx > 40 && hx < 80) {
+ lx *= 0.75;
+ }
+ return {h: hx, s, l: lx, a};
+ }
+ function _modifyBackgroundColor(rgb, theme) {
+ if (theme.mode === 0) {
+ return modifyLightSchemeColor(rgb, theme);
+ }
+ const pole = getBgPole(theme);
+ return modifyColorWithCache(rgb, theme, modifyBgHSL, pole);
+ }
+ function modifyBackgroundColor(
+ rgb,
+ theme,
+ shouldRegisterColorVariable = true
+ ) {
+ if (!shouldRegisterColorVariable) {
+ return _modifyBackgroundColor(rgb, theme);
+ }
+ return modifyAndRegisterColor(
+ "background",
+ rgb,
+ theme,
+ _modifyBackgroundColor
+ );
+ }
+ const MIN_FG_LIGHTNESS = 0.55;
+ function modifyBlueFgHue(hue) {
+ return scale(hue, 205, 245, 205, 220);
+ }
+ function modifyFgHSL({h, s, l, a}, pole) {
+ const isLight = l > 0.5;
+ const isNeutral = l < 0.2 || s < 0.24;
+ const isBlue = !isNeutral && h > 205 && h < 245;
+ if (isLight) {
+ const lx = scale(l, 0.5, 1, MIN_FG_LIGHTNESS, pole.l);
+ if (isNeutral) {
+ const hx = pole.h;
+ const sx = pole.s;
+ return {h: hx, s: sx, l: lx, a};
+ }
+ let hx = h;
+ if (isBlue) {
+ hx = modifyBlueFgHue(h);
+ }
+ return {h: hx, s, l: lx, a};
+ }
+ if (isNeutral) {
+ const hx = pole.h;
+ const sx = pole.s;
+ const lx = scale(l, 0, 0.5, pole.l, MIN_FG_LIGHTNESS);
+ return {h: hx, s: sx, l: lx, a};
+ }
+ let hx = h;
+ let lx;
+ if (isBlue) {
+ hx = modifyBlueFgHue(h);
+ lx = scale(l, 0, 0.5, pole.l, Math.min(1, MIN_FG_LIGHTNESS + 0.05));
+ } else {
+ lx = scale(l, 0, 0.5, pole.l, MIN_FG_LIGHTNESS);
+ }
+ return {h: hx, s, l: lx, a};
+ }
+ function _modifyForegroundColor(rgb, theme) {
+ if (theme.mode === 0) {
+ return modifyLightSchemeColor(rgb, theme);
+ }
+ const pole = getFgPole(theme);
+ return modifyColorWithCache(rgb, theme, modifyFgHSL, pole);
+ }
+ function modifyForegroundColor(
+ rgb,
+ theme,
+ shouldRegisterColorVariable = true
+ ) {
+ if (!shouldRegisterColorVariable) {
+ return _modifyForegroundColor(rgb, theme);
+ }
+ return modifyAndRegisterColor(
+ "text",
+ rgb,
+ theme,
+ _modifyForegroundColor
+ );
+ }
+ function modifyBorderHSL({h, s, l, a}, poleFg, poleBg) {
+ const isDark = l < 0.5;
+ const isNeutral = l < 0.2 || s < 0.24;
+ let hx = h;
+ let sx = s;
+ if (isNeutral) {
+ if (isDark) {
+ hx = poleFg.h;
+ sx = poleFg.s;
+ } else {
+ hx = poleBg.h;
+ sx = poleBg.s;
+ }
+ }
+ const lx = scale(l, 0, 1, 0.5, 0.2);
+ return {h: hx, s: sx, l: lx, a};
+ }
+ function _modifyBorderColor(rgb, theme) {
+ if (theme.mode === 0) {
+ return modifyLightSchemeColor(rgb, theme);
+ }
+ const poleFg = getFgPole(theme);
+ const poleBg = getBgPole(theme);
+ return modifyColorWithCache(
+ rgb,
+ theme,
+ modifyBorderHSL,
+ poleFg,
+ poleBg
+ );
+ }
+ function modifyBorderColor(rgb, theme, shouldRegisterColorVariable = true) {
+ if (!shouldRegisterColorVariable) {
+ return _modifyBorderColor(rgb, theme);
+ }
+ return modifyAndRegisterColor("border", rgb, theme, _modifyBorderColor);
+ }
+ function modifyShadowColor(rgb, theme) {
+ return modifyBackgroundColor(rgb, theme);
+ }
+ function modifyGradientColor(rgb, theme) {
+ return modifyBackgroundColor(rgb, theme);
+ }
+
+ const gradientLength = "gradient".length;
+ const conicGradient = "conic-";
+ const conicGradientLength = conicGradient.length;
+ const radialGradient = "radial-";
+ const linearGradient = "linear-";
+ function parseGradient(value) {
+ const result = [];
+ let index = 0;
+ let startIndex = conicGradient.length;
+ while ((index = value.indexOf("gradient", startIndex)) !== -1) {
+ let typeGradient;
+ [linearGradient, radialGradient, conicGradient].find(
+ (possibleType) => {
+ if (index - possibleType.length >= 0) {
+ const possibleGradient = value.substring(
+ index - possibleType.length,
+ index
+ );
+ if (possibleGradient === possibleType) {
+ if (
+ value.slice(
+ index - possibleType.length - 10,
+ index - possibleType.length - 1
+ ) === "repeating"
+ ) {
+ typeGradient = `repeating-${possibleType}gradient`;
+ return true;
+ }
+ if (
+ value.slice(
+ index - possibleType.length - 8,
+ index - possibleType.length - 1
+ ) === "-webkit"
+ ) {
+ typeGradient = `-webkit-${possibleType}gradient`;
+ return true;
+ }
+ typeGradient = `${possibleType}gradient`;
+ return true;
+ }
+ }
+ }
+ );
+ if (!typeGradient) {
+ break;
+ }
+ const {start, end} = getParenthesesRange(
+ value,
+ index + gradientLength
+ );
+ const match = value.substring(start + 1, end - 1);
+ startIndex = end + 1 + conicGradientLength;
+ result.push({
+ typeGradient,
+ match,
+ offset: typeGradient.length + 2,
+ index: index - typeGradient.length + gradientLength,
+ hasComma: true
+ });
+ }
+ if (result.length) {
+ result[result.length - 1].hasComma = false;
+ }
+ return result;
+ }
+
+ const STORAGE_KEY_IMAGE_DETAILS_LIST = "__darkreader__imageDetails_v2_list";
+ const STORAGE_KEY_IMAGE_DETAILS_PREFIX = "__darkreader__imageDetails_v2_";
+ const STORAGE_KEY_CSS_FETCH_PREFIX = "__darkreader__cssFetch_";
+ let imageCacheTimeout = 0;
+ const imageDetailsCacheQueue = new Map();
+ const cachedImageUrls = [];
+ function writeImageDetailsQueue() {
+ imageDetailsCacheQueue.forEach((details, url) => {
+ if (url && url.startsWith("https://")) {
+ try {
+ const json = JSON.stringify(details);
+ sessionStorage.setItem(
+ `${STORAGE_KEY_IMAGE_DETAILS_PREFIX}${url}`,
+ json
+ );
+ cachedImageUrls.push(url);
+ } catch (err) {}
+ }
+ });
+ imageDetailsCacheQueue.clear();
+ sessionStorage.setItem(
+ STORAGE_KEY_IMAGE_DETAILS_LIST,
+ JSON.stringify(cachedImageUrls)
+ );
+ }
+ function writeImageDetailsCache(url, imageDetails) {
+ if (!url || !url.startsWith("https://")) {
+ return;
+ }
+ imageDetailsCacheQueue.set(url, imageDetails);
+ clearTimeout(imageCacheTimeout);
+ imageCacheTimeout = setTimeout(writeImageDetailsQueue, 1000);
+ }
+ function readImageDetailsCache(targetMap) {
+ try {
+ const jsonList = sessionStorage.getItem(
+ STORAGE_KEY_IMAGE_DETAILS_LIST
+ );
+ if (!jsonList) {
+ return;
+ }
+ const list = JSON.parse(jsonList);
+ list.forEach((url) => {
+ const json = sessionStorage.getItem(
+ `${STORAGE_KEY_IMAGE_DETAILS_PREFIX}${url}`
+ );
+ if (json) {
+ const details = JSON.parse(json);
+ targetMap.set(url, details);
+ }
+ });
+ } catch (err) {}
+ }
+ function writeCSSFetchCache(url, cssText) {
+ const key = `${STORAGE_KEY_CSS_FETCH_PREFIX}${url}`;
+ try {
+ sessionStorage.setItem(key, cssText);
+ } catch (err) {}
+ }
+ function readCSSFetchCache(url) {
+ const key = `${STORAGE_KEY_CSS_FETCH_PREFIX}${url}`;
+ try {
+ return sessionStorage.getItem(key) ?? null;
+ } catch (err) {}
+ return null;
+ }
+
+ function toSVGMatrix(matrix) {
+ return matrix
+ .slice(0, 4)
+ .map((m) => m.map((m) => m.toFixed(3)).join(" "))
+ .join(" ");
+ }
+ function getSVGFilterMatrixValue(config) {
+ return toSVGMatrix(createFilterMatrix(config));
+ }
+
+ const MAX_FRAME_DURATION = 1000 / 60;
+ class AsyncQueue {
+ constructor() {
+ this.queue = [];
+ this.timerId = null;
+ }
+ addTask(task) {
+ this.queue.push(task);
+ this.scheduleFrame();
+ }
+ stop() {
+ if (this.timerId !== null) {
+ cancelAnimationFrame(this.timerId);
+ this.timerId = null;
+ }
+ this.queue = [];
+ }
+ scheduleFrame() {
+ if (this.timerId) {
+ return;
+ }
+ this.timerId = requestAnimationFrame(() => {
+ this.timerId = null;
+ const start = Date.now();
+ let cb;
+ while ((cb = this.queue.shift())) {
+ cb();
+ if (Date.now() - start >= MAX_FRAME_DURATION) {
+ this.scheduleFrame();
+ break;
+ }
+ }
+ });
+ }
+ }
+
+ const resolvers$1 = new Map();
+ const rejectors = new Map();
+ async function bgFetch(request) {
+ if (window.DarkReader?.Plugins?.fetch) {
+ return window.DarkReader.Plugins.fetch(request);
+ }
+ const parsedURL = new URL(request.url);
+ if (
+ parsedURL.origin !== request.origin &&
+ shouldIgnoreCors(parsedURL)
+ ) {
+ throw new Error("Cross-origin limit reached");
+ }
+ return new Promise((resolve, reject) => {
+ const id = generateUID();
+ resolvers$1.set(id, resolve);
+ rejectors.set(id, reject);
+ chrome.runtime.sendMessage({
+ type: MessageTypeCStoBG.FETCH,
+ data: request,
+ id
+ });
+ });
+ }
+ chrome.runtime.onMessage.addListener(({type, data, error, id}) => {
+ if (type === MessageTypeBGtoCS.FETCH_RESPONSE) {
+ const resolve = resolvers$1.get(id);
+ const reject = rejectors.get(id);
+ resolvers$1.delete(id);
+ rejectors.delete(id);
+ if (error) {
+ reject &&
+ reject(
+ typeof error === "string" ? new Error(error) : error
+ );
+ } else {
+ resolve && resolve(data);
+ }
+ }
+ });
+
+ const imageManager = new AsyncQueue();
+ async function getImageDetails(url) {
+ return new Promise(async (resolve, reject) => {
+ try {
+ let dataURL = url.startsWith("data:")
+ ? url
+ : await getDataURL(url);
+ const blob =
+ tryConvertDataURLToBlobSync(dataURL) ??
+ (await loadAsBlob(url));
+ let image;
+ let useViewBox = false;
+ if (dataURL.startsWith("data:image/svg+xml")) {
+ const commaIndex = dataURL.indexOf(",");
+ if (commaIndex >= 0) {
+ let svgText = dataURL.slice(commaIndex + 1);
+ const encoding = dataURL
+ .slice(0, commaIndex)
+ .split(";")[1];
+ if (encoding === "base64") {
+ if (svgText.includes("%")) {
+ svgText = decodeURIComponent(svgText);
+ }
+ svgText = atob(svgText);
+ } else if (svgText.startsWith("%3c")) {
+ svgText = decodeURIComponent(svgText);
+ }
+ if (svgText.startsWith("