+```
+
+让 LandingPage 跟随全局主题切换。
+
+### 5.2 LandingPage.module.css 全 var 化
+
+V1 跳过了这文件(21 处硬编码颜色保留)。V2 全部接入 var,浅色按下表:
+
+| 元素 | DARK 现状 | LIGHT 新值 |
+|---|---|---|
+| `.page` bg | `#000` | `var(--color-bg-page)` (`#fafafa`) |
+| `.title` color | `#f1f0ff` | `var(--color-text-primary)` (`#171717`) |
+| `.tagline` color | `rgba(255,255,255,0.5)` | `var(--color-text-on-glass-soft)` (浅色下:`rgba(23,23,23,0.50)`) |
+| `.btnPrimary` bg | `rgba(120,220,200,0.12)` | `var(--color-mint-accent-bg)` (浅色下 teal `rgba(13,148,136,0.10)`) |
+| `.btnPrimary` border | `rgba(120,220,200,0.3)` | `var(--color-mint-accent-border)` |
+| `.btnPrimary .btnName` color | `#7edcc8` | `var(--color-mint-accent)` (浅色下 `#0d9488` teal 深色) |
+| `.btnPrimary:hover` bg | `rgba(120,220,200,0.22)` | `var(--color-mint-accent-bg-hover)` |
+| `.btnGhost` bg | `rgba(255,255,255,0.05)` | `var(--color-bg-glass-strong)` (`rgba(255,255,255,0.80)`) |
+| `.btnGhost` border | `rgba(255,255,255,0.1)` | `var(--color-border-card)` (`rgba(0,0,0,0.08)`) |
+| `.btnGhost .btnName` color | `rgba(255,255,255,0.7)` | `var(--color-text-primary)` |
+| `.btnSub` color | `rgba(120,220,200,0.5)` | teal 浅色下 `rgba(13,148,136,0.65)` |
+| `.btnSubGhost` color | `rgba(255,255,255,0.35)` | `var(--color-text-tertiary)` |
+| `.easter` color | `rgba(255,255,255,0.06)` | `rgba(0,0,0,0.06)` |
+| `.easter:hover` color | `rgba(255,255,255,0.25)` | `rgba(0,0,0,0.25)` |
+| `.sparkOverlay` bg | `rgba(0,0,0,0.5)` | `var(--color-overlay-soft)` (浅色 `rgba(0,0,0,0.18)`) |
+| `.sparkOverlay` backdrop-filter | `blur(30px)` | 不变(深浅都好用) |
+| `.sparkTitle` color | `#ffffff` | `var(--color-text-primary)` |
+| `.sparkSub` color | `rgba(255,255,255,0.5)` | `var(--color-text-secondary)` |
+| `.musicBtn` color | `rgba(255,255,255,0.2)` | `var(--color-text-quaternary)` |
+| `.musicBtn:hover` color | `rgba(255,255,255,0.5)` | `var(--color-text-tertiary)` |
+
+### 5.3 AuroraCanvas 浅色化
+
+`web/src/components/AuroraCanvas.tsx` 当前硬编码了 5 个 orbs 的 RGB 颜色(126,220,200 青 / 108,99,255 紫 / 59,130,246 蓝 / 167,139,250 浅紫 / 34,211,238 亮青)。
+
+V2 改造:
+1. 在 `index.css` 新增 `--orb-color-1` ~ `--orb-color-5`(深色保持原色,浅色变 pastel)
+2. AuroraCanvas 改成读 CSS var(用 `c()` helper 或直接 `getComputedStyle`)
+3. 每次 theme 切换时重启动画循环(通过 `useThemeStore` 订阅 + useEffect cleanup)
+
+或者更简单:浅色下保留 AuroraCanvas,但**降低主体 alpha**(dark `0.28` → light `0.12` 之类),保留品牌氛围又不刺眼。
+
+### 5.4 LoginModal 玻璃化
+
+V1 LoginModal 在浅色下用了:
+- bg: `var(--color-bg-modal-elevated)` = `#ffffff`
+- border: `var(--color-border-modal-soft)` = `rgba(0,0,0,0.06)`
+
+V2 升级为真正的玻璃:
+- bg: `var(--color-bg-modal-glass)` = `rgba(255,255,255,0.85)`
+- backdrop-filter: `blur(24px) saturate(180%)`(已有)
+- 加 inset highlight: `box-shadow: ..., inset 0 1px 0 var(--color-inset-highlight)`
+- 加 multi-layer shadow: `box-shadow: var(--shadow-glass-light)`
+
+---
+
+## 六、其他玻璃面升级清单
+
+按"已有 backdrop-filter 但浅色没玻璃感"扫描,13 个文件需要逐一升级:
+
+| 文件 | 现状 | V2 调整 |
+|---|---|---|
+| `Sidebar.module.css` | `bg: var(--color-sidebar-bg)` + `backdrop-filter: blur(16px) saturate(160%)` | bg 变成新的 `--color-bg-glass`(浅色透白);保留 backdrop-filter |
+| `AnnouncementBanner.module.css` | linear-gradient + backdrop-filter | gradient 在浅色下改用 chip-warm-bg;keep blur |
+| `AnnouncementModal.module.css` | overlay + 内卡 | overlay 浅色变 `rgba(0,0,0,0.20)`;内卡变玻璃 |
+| `VideoDetailModal.module.css` `.infoPanel` | `bg: var(--color-bg-upload)` + `backdrop-filter: blur(24px) saturate(180%)` | bg 改 `--color-bg-glass`;加 inset highlight |
+| `VideoDetailModal.module.css` `.detailModal` (TeamsPage 类似) | `bg: var(--color-bg-modal-glass)` | 已经是玻璃 token,V2 调 alpha 即可 |
+| `GenerationCard.module.css` | 部分卡片有 backdrop-filter | 用 glass token |
+| `PromptInput.module.css` | mention dropdown | 浅色用 `--color-bg-dropdown` 玻璃 |
+| `LoginModal.module.css` | (见 5.4) | 玻璃化 |
+| `ForceChangePasswordModal.module.css` | 同 LoginModal | 玻璃化 |
+| `Toast.module.css` | 浮层 | 玻璃 `blur(12px) saturate(140%)` |
+| `Select.module.css` | 下拉 | 同上 |
+| `Dropdown.module.css` | 下拉 | 同上 |
+| `DatePicker.module.css` | 弹层 | 同上 |
+| `TeamsPage.module.css` `.detailModal` | 玻璃弹窗 | 调 alpha + 加 inset highlight |
+
+---
+
+## 七、Admin 页面"实体白 + 影边"统一
+
+V2 在 admin 页保留 Vercel 风格(不要全玻璃,那会丢失数据焦点),但调整:
+
+1. **Stat 卡片 / 表格 wrapper**:
+ - bg: `var(--color-bg-card)` = 浅色下变 `#ffffff` 纯白
+ - box-shadow: `var(--shadow-card-light)` = `0 1px 2px rgba(0,0,0,0.04), 0 4px 16px rgba(0,0,0,0.06)`
+ - border: `1px solid var(--color-border-card)` = `rgba(0,0,0,0.08)`
+
+2. **表格行 hover**:
+ - bg: `var(--color-bg-row-hover)`(新 var)= `rgba(0,0,0,0.03)`
+
+3. **active 导航项**:
+ - bg: `var(--color-sidebar-active)` = `rgba(0,0,0,0.06)` 浅灰药丸
+ - text: `var(--color-primary)` 主色(V1 浅色 `#5048cc`)
+
+---
+
+## 八、实施步骤(推荐顺序)
+
+```
+Phase A — 基础设施(不改可见效果)
+ A.1 index.css [data-theme="light"] 全部按 §4 改值
+ A.2 index.css :root 新增 --color-bg-glass / --color-bg-glass-strong / --color-bg-row-hover /
+ --color-border-glass-edge / --color-chip-warm-* / --shadow-card-light / --shadow-glass-light
+ 七组新 var(深浅各一套)
+ A.3 删掉 [data-theme="light"] .aurora-bg { display: none }
+ A.4 调 aurora RGB 值(CSS var),让浅色 aurora 是 pastel
+ A.5 AuroraCanvas.tsx 接入 CSS var(或保留硬编码 + 在浅色下额外降 alpha)
+
+Phase B — LandingPage 浅色化
+ B.1 移除 data-theme="dark"
+ B.2 LandingPage.module.css 全部硬编码颜色 → var(约 21 处)
+ B.3 跑截图:登录页应该浅色 + LoginModal 玻璃 + AuroraCanvas pastel
+
+Phase C — 玻璃面升级
+ C.1 Sidebar 用新 --color-bg-glass
+ C.2 VideoDetailModal.infoPanel 用新 glass
+ C.3 AnnouncementModal / LoginModal / ForceChangePasswordModal 用新 glass + inset highlight
+ C.4 Toast / Dropdown / Select / DatePicker 加 saturate(140%),浅色用 --color-bg-glass-strong
+ C.5 AnnouncementBanner gradient 在浅色下改 chip-warm-bg(CSS 限制无法 var-内 alpha,
+ 所以这一项要写双套独立规则:[data-theme="dark"] .banner { ... } + [data-theme="light"] .banner { ... })
+
+Phase D — Admin 实体卡升级
+ D.1 全局加 box-shadow: var(--shadow-card-light) 给 .statCard / .tableWrapper / .chartSection
+ D.2 全局检查 .table tr:hover 用 --color-bg-row-hover
+ D.3 Sidebar active 项加确认浅色下视觉
+
+Phase E — 视觉校准
+ E.1 跑 Playwright 24 张截图
+ E.2 对照 GitBook / Framer 验收
+ E.3 逐页迭代 alpha / shadow / blur 数值
+
+Phase F — 兼容性 / 回归
+ F.1 tsc + vitest
+ F.2 完成报告 → 亮色主题切换V2-完成报告.md
+ F.3 本地 commit dev(不 push)
+```
+
+---
+
+## 九、关键验收点
+
+完工后跑截图,对比这些视觉特征:
+
+- [ ] LandingPage 浅色:白底 + pastel aurora 隐约可见 + LoginModal 是**透明白玻璃**(能看见 aurora 透过来)
+- [ ] LoginModal 顶边有微妙白色 inset highlight
+- [ ] 生成页 Sidebar 浅色是**透明白玻璃**(能看见后景内容隐约透过)
+- [ ] VideoDetailModal infoPanel 浅色:玻璃白板 + 主视频区可见
+- [ ] AnnouncementModal 弹窗:玻璃白卡 + overlay 是 _淡黑_ 不是 _重黑_
+- [ ] Admin 仪表盘 / 团队管理:纯白卡片 + 1px 阴影边 + 多层柔阴影,类似 GitBook 工作台
+- [ ] 公告横幅 / Trial / "新版上线" pill:暖米色 chip 风格
+- [ ] ECharts tooltip / 网格 / 轴在浅色下清晰可读
+- [ ] 主色按钮 #5048cc 在白底上对比度通过 WCAG AA
+- [ ] 切换按钮(月亮/太阳)位置 + hover 颜色都没问题
+
+---
+
+## 十、风险点
+
+1. **backdrop-filter 性能**:Safari + Chrome 都需要 GPU 合成层。同屏 10+ 玻璃面可能掉帧。当前项目最多 5-6 个同屏玻璃,可接受。如果发现 < 60fps,把 Toast / Dropdown 这种小弹层退回实体。
+
+2. **AuroraCanvas 浅色刺眼**:pastel 色板可能在某些浅色下仍然觉得"花"。fallback 方案:浅色下整个 AuroraCanvas 用 `opacity: 0.5` 整体压一档。
+
+3. **打印 / 截图工具兼容性**:backdrop-filter 在某些 PDF/截图引擎不渲染,玻璃会变成纯实体。Playwright headless Chromium 是 OK 的。
+
+4. **半透明色叠加导致文字对比度变化**:玻璃面上的文字(如 LoginModal "AirDrama" 标题)背景从 dark 切到 light 时对比度差异巨大。已经在 §4.6 用 `--color-text-on-glass` 调整,但实际跑下来可能还要再调。
+
+5. **AnnouncementBanner gradient var 限制**:CSS gradient 不能在 var() 上加自定义 alpha。要么写两条独立 `[data-theme]` 规则,要么改成 `background-image: linear-gradient(rgb(from var(--xxx) r g b / 0.10), ...)` 用新 `rgb(from ...)` 函数(Chrome 119+ 支持,需要查兼容性)。保险起见用两条独立规则。
+
+---
+
+## 十一、工作量估算
+
+| 阶段 | 工作量 | 备注 |
+|---|---|---|
+| Phase A | 1 小时 | index.css 改写 + 新 var |
+| Phase B | 1 小时 | LandingPage + AuroraCanvas |
+| Phase C | 1.5 小时 | 13 个玻璃面挨个调(可派 2-3 个 sub-agent 并行) |
+| Phase D | 0.5 小时 | admin 实体卡 |
+| Phase E | 1 小时 | 截图 + 视觉迭代 |
+| Phase F | 0.5 小时 | tsc / vitest / commit |
+| **合计** | **5.5 小时(AI 连续)** | 比 V1 多 1.5 小时,因为多了一轮迭代 |
+
+---
+
+## 十二、与 V1 的差异总览
+
+| 维度 | V1 | V2 |
+|---|---|---|
+| 浅色 page bg | `#fafafa` | `#fafafa` (不变) |
+| 浅色 aurora | `display: none` | pastel 紫蓝桃 0.18-0.30 |
+| LandingPage | 强制 `data-theme="dark"` | 跟随主题切换 |
+| 浅色 card bg | `rgba(0,0,0,0.05)` 黑透明 | **拆分**:实体 `#fff` (admin 卡) vs 玻璃 `rgba(255,255,255,0.65)` (sidebar/modal) |
+| backdrop-filter | 散落各处,无统一 | 五档标准化(Sidebar/Modal/Hero/Dropdown/Toast) |
+| Inset highlight | 无 | 玻璃顶边白高光 `rgba(255,255,255,0.50)` |
+| 阴影 | 单层 `--color-shadow-modal` | 双层 `--shadow-card-light` + `--shadow-glass-light` |
+| 文字主色 | `#171823` 微紫 | `#171717` Vercel Black |
+| 暖调 chip | 无 | 新增 `--color-chip-warm-*` |
+| AuroraCanvas | 浅色硬编码不变(深色配色) | 接入 CSS var 或浅色态降 alpha |
+
+---
+
+## 参考资料
+
+- GitBook 主站:https://www.gitbook.com/(用户提供截图 × 3)
+- Framer 玻璃 demo 图:https://framerusercontent.com/images/FTmA5L2PDssA4gAib6edPamSM.webp(已本地分析)
+- Linear 浅色方案:https://linear.app/(design-linear-app skill 已加载)
+- Vercel Geist:https://vercel.com/design/geist(design-vercel skill 已加载)
+- WCAG 对比度:https://webaim.org/resources/contrastchecker/
+- CSS `rgb(from var() ...)` 兼容性:https://caniuse.com/css-relative-colors
+
+---
+
+## Critical Files
+
+修改:
+- `web/src/index.css` — `[data-theme="light"]` 块大改 + 新增 ~7 个 var
+- `web/src/pages/LandingPage.tsx` — 移除 `data-theme="dark"` 强制
+- `web/src/pages/LandingPage.module.css` — 21 处颜色 → var
+- `web/src/components/AuroraCanvas.tsx` — orbs RGB 接入 var 或加 theme-aware alpha
+- 13 个 module.css 文件玻璃面调整(详见 §6)
+
+不动:
+- 后端
+- TS 业务逻辑
+- 现有 var 命名(仅微调值)
+
+---
+
+**预期效果(V2 完成后)**:
+
+- 登录页:纯白 + 微妙 pastel aurora + 透明白玻璃 LoginModal + 暖橙公告 chip
+- 生成页:透明白玻璃 Sidebar + 主视频区实体白卡 + 玻璃 modal
+- 后台仪表盘:Vercel-style 纯白卡 + 多层阴影 + 主色按钮
+- 整体感受:和 GitBook / Linear / Vercel 同语言,不再是"色块版深色取反"
diff --git a/web/src/components/AnnouncementBanner.module.css b/web/src/components/AnnouncementBanner.module.css
index 0d2c894..36c1f59 100644
--- a/web/src/components/AnnouncementBanner.module.css
+++ b/web/src/components/AnnouncementBanner.module.css
@@ -3,15 +3,31 @@
align-items: center;
gap: 10px;
padding: 10px 16px;
+ /* 深色 - 紫青渐变玻璃 */
background: linear-gradient(90deg, rgba(108, 99, 255, 0.10), rgba(0, 184, 230, 0.08));
border-left: 3px solid var(--color-primary);
border-bottom: 1px solid var(--color-border-soft);
+ backdrop-filter: var(--bf-glass-md);
+ -webkit-backdrop-filter: var(--bf-glass-md);
+ box-shadow: inset 0 1px 0 var(--color-inset-highlight);
font-size: 13px;
color: var(--color-text-primary);
line-height: 1.5;
flex-shrink: 0;
}
+[data-theme="light"] .banner {
+ /* 浅色 - 暖米色 chip */
+ background: var(--color-chip-warm-bg);
+ border-left-color: var(--color-chip-warm-border);
+ border-bottom-color: var(--color-chip-warm-border);
+ color: var(--color-chip-warm-text);
+}
+
+[data-theme="light"] .icon {
+ color: var(--color-chip-warm-badge-text);
+}
+
.icon {
flex-shrink: 0;
color: var(--color-primary);
diff --git a/web/src/components/AnnouncementModal.module.css b/web/src/components/AnnouncementModal.module.css
index 742a3ba..bc8170a 100644
--- a/web/src/components/AnnouncementModal.module.css
+++ b/web/src/components/AnnouncementModal.module.css
@@ -9,7 +9,9 @@
}
.modal {
- background: var(--color-bg-modal-elevated);
+ background: var(--color-bg-modal-glass);
+ backdrop-filter: var(--bf-glass-lg);
+ -webkit-backdrop-filter: var(--bf-glass-lg);
border: 1px solid var(--color-border-card);
border-radius: var(--radius-card);
max-width: 520px;
@@ -17,6 +19,7 @@
max-height: 75vh;
display: flex;
flex-direction: column;
+ box-shadow: var(--shadow-glass-light);
}
.header {
diff --git a/web/src/components/AssetLibraryModal.module.css b/web/src/components/AssetLibraryModal.module.css
index 9053d5c..871e032 100644
--- a/web/src/components/AssetLibraryModal.module.css
+++ b/web/src/components/AssetLibraryModal.module.css
@@ -12,12 +12,15 @@
width: 90vw;
max-width: 1400px;
height: 85vh;
- background: var(--color-bg-modal-elevated);
+ background: var(--color-bg-modal-glass);
+ backdrop-filter: var(--bf-glass-lg);
+ -webkit-backdrop-filter: var(--bf-glass-lg);
border: 1px solid var(--color-border-card);
border-radius: 12px;
overflow: hidden;
display: flex;
flex-direction: column;
+ box-shadow: var(--shadow-glass-light);
}
.header {
diff --git a/web/src/components/AuroraCanvas.tsx b/web/src/components/AuroraCanvas.tsx
index 26ddd00..30fb7b5 100644
--- a/web/src/components/AuroraCanvas.tsx
+++ b/web/src/components/AuroraCanvas.tsx
@@ -1,53 +1,64 @@
import { useRef, useEffect, useCallback } from 'react';
+import { useThemeStore } from '../store/theme';
/**
* Aurora background — 5 large diffuse orbs with additive blending.
- * Orbs are deliberately offset from center to avoid uniform "blue blob" look.
- * Mouse gently pushes nearby orbs (10-20px, very subtle).
+ * V2: 接入 themeStore,浅色态用 pastel orb + 白色 vignette/gradient,深色保持原品牌色。
*/
interface Orb {
cx: number; // base center X ratio (0-1)
cy: number; // base center Y ratio (0-1)
color: [number, number, number];
- alpha: number; // peak alpha — varies per orb for brightness contrast
+ alpha: number;
phase: number;
freqX: number;
freqY: number;
ampX: number;
ampY: number;
- radius: number; // ratio of max(w,h)
- // breathing
+ radius: number;
breathFreq: number;
breathAmp: number;
}
-// Orbs deliberately NOT centered — some in corners, some offset,
-// creating uneven light distribution with dark pockets.
-const ORBS: Orb[] = [
- // Cyan — upper-left area, large and bright
+// Dark theme orbs — vivid 品牌色(青/紫/蓝)
+const DARK_ORBS: Orb[] = [
{ cx: 0.25, cy: 0.30, color: [126, 220, 200], alpha: 0.28,
phase: 0, freqX: 0.00012, freqY: 0.00010, ampX: 0.12, ampY: 0.10,
radius: 0.50, breathFreq: 0.0004, breathAmp: 0.06 },
- // Purple — lower-right, medium
{ cx: 0.72, cy: 0.65, color: [108, 99, 255], alpha: 0.22,
phase: 1.8, freqX: 0.00010, freqY: 0.00014, ampX: 0.14, ampY: 0.12,
radius: 0.45, breathFreq: 0.0003, breathAmp: 0.08 },
- // Blue — center-right, smaller and dimmer (fills gap)
{ cx: 0.58, cy: 0.38, color: [59, 130, 246], alpha: 0.18,
phase: 3.2, freqX: 0.00015, freqY: 0.00008, ampX: 0.10, ampY: 0.15,
radius: 0.38, breathFreq: 0.0005, breathAmp: 0.07 },
- // Light purple — bottom-left corner, dim accent
{ cx: 0.20, cy: 0.70, color: [167, 139, 250], alpha: 0.15,
phase: 4.5, freqX: 0.00008, freqY: 0.00012, ampX: 0.16, ampY: 0.08,
radius: 0.40, breathFreq: 0.00035, breathAmp: 0.10 },
- // Bright cyan — upper-right, small bright accent
{ cx: 0.78, cy: 0.25, color: [34, 211, 238], alpha: 0.20,
phase: 5.8, freqX: 0.00014, freqY: 0.00016, ampX: 0.08, ampY: 0.12,
radius: 0.35, breathFreq: 0.00045, breathAmp: 0.09 },
];
-// Mouse influence radius (px) and max push distance (px)
+// Light theme orbs — pastel 浅色,alpha 减半左右,给玻璃面提供穿透色源
+const LIGHT_ORBS: Orb[] = [
+ { cx: 0.25, cy: 0.30, color: [180, 167, 255], alpha: 0.32, // pastel lavender
+ phase: 0, freqX: 0.00012, freqY: 0.00010, ampX: 0.12, ampY: 0.10,
+ radius: 0.55, breathFreq: 0.0004, breathAmp: 0.06 },
+ { cx: 0.72, cy: 0.65, color: [167, 200, 255], alpha: 0.28, // pastel sky
+ phase: 1.8, freqX: 0.00010, freqY: 0.00014, ampX: 0.14, ampY: 0.12,
+ radius: 0.50, breathFreq: 0.0003, breathAmp: 0.08 },
+ { cx: 0.58, cy: 0.38, color: [255, 180, 130, ], alpha: 0.18, // pastel peach
+ phase: 3.2, freqX: 0.00015, freqY: 0.00008, ampX: 0.10, ampY: 0.15,
+ radius: 0.42, breathFreq: 0.0005, breathAmp: 0.07 },
+ { cx: 0.20, cy: 0.70, color: [220, 167, 255], alpha: 0.20, // pastel pink-violet
+ phase: 4.5, freqX: 0.00008, freqY: 0.00012, ampX: 0.16, ampY: 0.08,
+ radius: 0.45, breathFreq: 0.00035, breathAmp: 0.10 },
+ { cx: 0.78, cy: 0.25, color: [180, 220, 255], alpha: 0.22, // pastel blue
+ phase: 5.8, freqX: 0.00014, freqY: 0.00016, ampX: 0.08, ampY: 0.12,
+ radius: 0.40, breathFreq: 0.00045, breathAmp: 0.09 },
+];
+
const MOUSE_RADIUS = 400;
const MOUSE_PUSH = 28;
@@ -55,6 +66,8 @@ export function AuroraCanvas() {
const canvasRef = useRef
(null);
const grainRef = useRef(null);
const mouseRef = useRef({ x: -9999, y: -9999, active: false });
+ const theme = useThemeStore((s) => s.theme);
+ const isLight = theme === 'light';
const handleMouseMove = useCallback((e: MouseEvent) => {
mouseRef.current.x = e.clientX;
@@ -91,32 +104,30 @@ export function AuroraCanvas() {
let animId: number;
const t0 = performance.now();
- // Smoothed mouse position for gentle push
let smoothMx = -9999;
let smoothMy = -9999;
+ const orbs = isLight ? LIGHT_ORBS : DARK_ORBS;
+
function draw(now: number) {
const t = now - t0;
ctx!.clearRect(0, 0, w, h);
- ctx!.globalCompositeOperation = 'lighter';
+ // 浅色用 source-over 让 pastel 互融时不会过曝;深色继续用 lighter 加合
+ ctx!.globalCompositeOperation = isLight ? 'source-over' : 'lighter';
- // Smooth mouse tracking (lerp toward actual position)
const mouse = mouseRef.current;
if (mouse.active) {
smoothMx += (mouse.x - smoothMx) * 0.035;
smoothMy += (mouse.y - smoothMy) * 0.035;
} else {
- // Slowly drift smoothed mouse away (return to no-influence)
smoothMx += (-9999 - smoothMx) * 0.01;
smoothMy += (-9999 - smoothMy) * 0.01;
}
- for (const orb of ORBS) {
- // Base position from slow sinusoidal movement
+ for (const orb of orbs) {
let x = w * (orb.cx + Math.sin(t * orb.freqX + orb.phase) * orb.ampX);
let y = h * (orb.cy + Math.cos(t * orb.freqY + orb.phase * 0.7) * orb.ampY);
- // Mouse push — gently offset orb away from cursor
const dx = x - smoothMx;
const dy = y - smoothMy;
const dist = Math.sqrt(dx * dx + dy * dy);
@@ -126,7 +137,6 @@ export function AuroraCanvas() {
y += (dy / dist) * strength;
}
- // Breathing: radius and alpha pulse slowly
const breathT = Math.sin(t * orb.breathFreq + orb.phase * 1.3);
const r = Math.max(w, h) * orb.radius * (1 + breathT * orb.breathAmp);
const a = orb.alpha * (1 + breathT * 0.15);
@@ -160,7 +170,7 @@ export function AuroraCanvas() {
window.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseleave', handleMouseLeave);
};
- }, [handleMouseMove, handleMouseLeave]);
+ }, [handleMouseMove, handleMouseLeave, isLight]);
// ── Film grain — 4 FPS low-noise ──
useEffect(() => {
@@ -194,20 +204,24 @@ export function AuroraCanvas() {
return () => cancelAnimationFrame(animId);
}, []);
+ // 浅色态:vignette / gradient 反相 — 用白色压边,黑色压边在浅色上是错的
+ const vignetteColor = isLight ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.8)';
+ const fadeColor = isLight ? 'rgba(250,250,250,0.7)' : 'rgba(0,0,0,0.5)';
+
return (
<>
- {/* Layer 1: Vignette — radial darkening, heavy at edges */}
+ {/* Layer 1: Vignette — radial fading, 浅色下用白色 */}
- {/* Layer 2: Film grain */}
+ {/* Layer 2: Film grain — 浅色下大幅减弱避免噪点过曝 */}
- {/* Layer 3: Aurora — blur merges orbs into organic glow */}
+ {/* Layer 3: Aurora — blur 让 orb 融成有机晕染 */}
- {/* Layer 4: Top/bottom gradient mask */}
+ {/* Layer 4: 顶/底渐变压角 */}
>
diff --git a/web/src/components/ConfirmModal.module.css b/web/src/components/ConfirmModal.module.css
index 90b3502..7334ed7 100644
--- a/web/src/components/ConfirmModal.module.css
+++ b/web/src/components/ConfirmModal.module.css
@@ -1,5 +1,5 @@
.overlay { position: fixed; inset: 0; background: var(--color-modal-overlay); display: flex; align-items: center; justify-content: center; z-index: 300; }
-.modal { background: var(--color-bg-modal-elevated); border: 1px solid var(--color-border-card); border-radius: var(--radius-card); padding: 24px; width: 400px; max-width: 90vw; }
+.modal { background: var(--color-bg-modal-glass); backdrop-filter: var(--bf-glass-lg); -webkit-backdrop-filter: var(--bf-glass-lg); border: 1px solid var(--color-border-card); border-radius: var(--radius-card); padding: 24px; width: 400px; max-width: 90vw; box-shadow: var(--shadow-glass-light); }
.title { font-size: 16px; font-weight: 600; color: var(--color-text-primary); margin-bottom: 12px; }
.message { font-size: 14px; color: var(--color-text-secondary); line-height: 1.6; margin-bottom: 20px; }
.actions { display: flex; justify-content: flex-end; gap: 8px; }
diff --git a/web/src/components/DatePicker.module.css b/web/src/components/DatePicker.module.css
index 423f5d0..f9125b3 100644
--- a/web/src/components/DatePicker.module.css
+++ b/web/src/components/DatePicker.module.css
@@ -53,11 +53,14 @@
top: calc(100% + 4px);
left: 0;
z-index: 1000;
- background: var(--color-bg-modal-elevated);
+ background: var(--color-bg-dropdown);
border: 1px solid var(--color-border-card);
border-radius: 12px;
- box-shadow: 0 8px 32px var(--color-shadow-dropdown);
- backdrop-filter: blur(20px) saturate(180%);
+ box-shadow:
+ 0 8px 32px var(--color-shadow-dropdown),
+ inset 0 1px 0 var(--color-inset-highlight);
+ backdrop-filter: var(--bf-glass-md);
+ -webkit-backdrop-filter: var(--bf-glass-md);
padding: 12px;
min-width: 280px;
}
diff --git a/web/src/components/Dropdown.module.css b/web/src/components/Dropdown.module.css
index 5a33dff..de4a9fc 100644
--- a/web/src/components/Dropdown.module.css
+++ b/web/src/components/Dropdown.module.css
@@ -7,16 +7,19 @@
bottom: calc(100% + 8px);
left: 0;
background: var(--color-bg-dropdown);
- border: 1px solid var(--color-border-input-bar);
+ border: 1px solid var(--color-border-card);
border-radius: var(--radius-dropdown);
padding: 6px;
z-index: 100;
- backdrop-filter: blur(20px) saturate(180%);
+ backdrop-filter: var(--bf-glass-md);
+ -webkit-backdrop-filter: var(--bf-glass-md);
opacity: 0;
transform: translateY(8px);
pointer-events: none;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
- box-shadow: 0 8px 32px var(--color-shadow-dropdown);
+ box-shadow:
+ 0 8px 32px var(--color-shadow-dropdown),
+ inset 0 1px 0 var(--color-inset-highlight);
}
.open {
diff --git a/web/src/components/ForceChangePasswordModal.module.css b/web/src/components/ForceChangePasswordModal.module.css
index c7485a2..64087ee 100644
--- a/web/src/components/ForceChangePasswordModal.module.css
+++ b/web/src/components/ForceChangePasswordModal.module.css
@@ -21,13 +21,13 @@
width: 100%;
max-width: 420px;
margin: 0 20px;
- background: var(--color-bg-card);
- backdrop-filter: blur(24px) saturate(180%);
- -webkit-backdrop-filter: blur(24px) saturate(180%);
+ background: var(--color-bg-modal-glass);
+ backdrop-filter: var(--bf-glass-xl);
+ -webkit-backdrop-filter: var(--bf-glass-xl);
border: 1px solid var(--color-border-card);
border-radius: 16px;
padding: 36px 32px 32px;
- box-shadow: 0 8px 32px var(--color-shadow-dropdown), 0 0 0 1px rgba(255, 255, 255, 0.05) inset;
+ box-shadow: var(--shadow-glass-light);
animation: panelIn 0.3s ease-out;
}
@@ -106,7 +106,7 @@
}
.input:focus {
- border-color: rgba(126, 220, 200, 0.5);
+ border-color: var(--color-mint-accent);
}
.error {
@@ -123,22 +123,24 @@
width: 55%;
align-self: center;
margin-top: 18px;
- background: rgba(120, 220, 200, 0.08);
- border: 1px solid rgba(120, 220, 200, 0.3);
- color: #7edcc8;
+ background: var(--color-mint-accent-bg);
+ border: 1px solid var(--color-mint-accent-border);
+ color: var(--color-mint-accent);
border-radius: 10px;
font-family: 'Space Grotesk', sans-serif;
font-size: 15px;
- font-weight: 500;
+ font-weight: 600;
+ letter-spacing: 0.04em;
cursor: pointer;
transition: all 0.2s;
- backdrop-filter: blur(12px);
- -webkit-backdrop-filter: blur(12px);
+ backdrop-filter: var(--bf-glass-sm);
+ -webkit-backdrop-filter: var(--bf-glass-sm);
+ box-shadow: inset 0 1px 0 var(--color-inset-highlight);
}
.submitBtn:hover {
- background: rgba(120, 220, 200, 0.18);
- box-shadow: 0 0 24px rgba(120, 220, 200, 0.12);
+ background: var(--color-mint-accent-bg-hover);
+ box-shadow: inset 0 1px 0 var(--color-inset-highlight), 0 0 24px var(--color-mint-accent-glow);
}
.submitBtn:disabled {
diff --git a/web/src/components/GenerationCard.module.css b/web/src/components/GenerationCard.module.css
index 3e1a05a..3a1704a 100644
--- a/web/src/components/GenerationCard.module.css
+++ b/web/src/components/GenerationCard.module.css
@@ -81,7 +81,7 @@
overflow: hidden;
}
-/* hover 展开黑底:基于 .header 定位,左边距图片 4px */
+/* hover 展开 prompt 面板 — V2 玻璃面 */
.promptExpanded {
position: absolute;
top: 0;
@@ -91,12 +91,15 @@
color: var(--color-text-primary);
line-height: 1.6;
word-break: break-word;
- background: var(--color-bg-dropdown);
- backdrop-filter: blur(12px);
+ background: var(--color-bg-glass-strong);
+ backdrop-filter: var(--bf-glass-sm);
+ -webkit-backdrop-filter: var(--bf-glass-sm);
border: 1px solid var(--color-border-card);
padding: 6px 8px;
border-radius: 8px;
- box-shadow: 0 8px 24px var(--color-shadow-dropdown);
+ box-shadow:
+ 0 8px 24px var(--color-shadow-dropdown),
+ inset 0 1px 0 var(--color-inset-highlight);
}
.mentionTag {
@@ -173,13 +176,17 @@
.detailTooltip {
position: fixed;
z-index: 1000;
- background: var(--color-bg-dropdown);
- backdrop-filter: blur(12px);
+ background: var(--color-bg-glass-strong);
+ backdrop-filter: var(--bf-glass-sm);
+ -webkit-backdrop-filter: var(--bf-glass-sm);
border: 1px solid var(--color-border-card);
border-radius: 8px;
padding: 12px 20px;
min-width: 260px;
- box-shadow: 0 8px 24px var(--color-shadow-dropdown);
+ /* V2 玻璃面 — 阴影 + 顶边白高光 */
+ box-shadow:
+ 0 8px 24px var(--color-shadow-dropdown),
+ inset 0 1px 0 var(--color-inset-highlight);
animation: detailTooltipFadeIn 0.15s ease-out;
}
@@ -417,14 +424,18 @@
position: absolute;
bottom: calc(100% + 6px);
right: 0;
- background: var(--color-bg-dropdown);
- backdrop-filter: blur(20px) saturate(180%);
+ background: var(--color-bg-glass-strong);
+ backdrop-filter: var(--bf-glass-md);
+ -webkit-backdrop-filter: var(--bf-glass-md);
border: 1px solid var(--color-border-card);
border-radius: 10px;
padding: 4px;
min-width: 100px;
z-index: 10;
- box-shadow: 0 8px 24px var(--color-shadow-dropdown);
+ /* V2 玻璃面 — 阴影 + 顶边白高光 */
+ box-shadow:
+ 0 8px 24px var(--color-shadow-dropdown),
+ inset 0 1px 0 var(--color-inset-highlight-strong);
animation: dropdownFadeIn 0.12s ease-out;
}
diff --git a/web/src/components/LoginModal.module.css b/web/src/components/LoginModal.module.css
index c34feae..b9f9c98 100644
--- a/web/src/components/LoginModal.module.css
+++ b/web/src/components/LoginModal.module.css
@@ -21,13 +21,13 @@
width: 100%;
max-width: 400px;
margin: 0 20px;
- background: var(--color-bg-card);
- backdrop-filter: blur(24px) saturate(180%);
- -webkit-backdrop-filter: blur(24px) saturate(180%);
+ background: var(--color-bg-modal-glass);
+ backdrop-filter: var(--bf-glass-xl);
+ -webkit-backdrop-filter: var(--bf-glass-xl);
border: 1px solid var(--color-border-card);
border-radius: 16px;
padding: 36px 32px 32px;
- box-shadow: 0 8px 32px var(--color-shadow-dropdown), 0 0 0 1px rgba(255, 255, 255, 0.05) inset;
+ box-shadow: var(--shadow-glass-light);
animation: panelIn 0.3s ease-out;
}
@@ -120,7 +120,7 @@
}
.input:focus {
- border-color: rgba(126, 220, 200, 0.5);
+ border-color: var(--color-mint-accent);
}
.error {
@@ -137,22 +137,25 @@
width: 55%;
align-self: center;
margin-top: 18px;
- background: rgba(120, 220, 200, 0.08);
- border: 1px solid rgba(120, 220, 200, 0.3);
- color: #7edcc8;
+ background: var(--color-mint-accent-bg);
+ border: 1px solid var(--color-mint-accent-border);
+ color: var(--color-mint-accent);
border-radius: 10px;
font-family: 'Space Grotesk', sans-serif;
font-size: 15px;
- font-weight: 500;
+ font-weight: 600; /* V2: 500 → 600,浅色下提对比度 */
+ letter-spacing: 0.04em;
cursor: pointer;
transition: all 0.2s;
- backdrop-filter: blur(12px);
- -webkit-backdrop-filter: blur(12px);
+ backdrop-filter: var(--bf-glass-sm);
+ -webkit-backdrop-filter: var(--bf-glass-sm);
+ /* V2 玻璃顶边白高光 */
+ box-shadow: inset 0 1px 0 var(--color-inset-highlight);
}
.submitBtn:hover {
- background: rgba(120, 220, 200, 0.18);
- box-shadow: 0 0 24px rgba(120, 220, 200, 0.12);
+ background: var(--color-mint-accent-bg-hover);
+ box-shadow: inset 0 1px 0 var(--color-inset-highlight), 0 0 24px var(--color-mint-accent-glow);
}
.submitBtn:disabled {
diff --git a/web/src/components/PromptInput.module.css b/web/src/components/PromptInput.module.css
index 2fe125b..72c0a35 100644
--- a/web/src/components/PromptInput.module.css
+++ b/web/src/components/PromptInput.module.css
@@ -78,18 +78,22 @@
color: var(--color-mention-text-hover);
}
-/* Mention popup — appears above cursor */
+/* Mention popup — appears above cursor (V2 玻璃面) */
.mentionPopup {
position: absolute;
z-index: 100;
- background: var(--color-bg-dropdown);
+ background: var(--color-bg-glass-strong);
border: 1px solid var(--color-border-card);
border-radius: 10px;
padding: 6px;
min-width: 200px;
max-width: 280px;
- box-shadow: 0 8px 24px var(--color-shadow-dropdown);
- backdrop-filter: blur(20px) saturate(180%);
+ /* 阴影 + 玻璃顶边白高光 */
+ box-shadow:
+ 0 8px 24px var(--color-shadow-dropdown),
+ inset 0 1px 0 var(--color-inset-highlight-strong);
+ backdrop-filter: var(--bf-glass-md);
+ -webkit-backdrop-filter: var(--bf-glass-md);
transform: translateY(-100%);
animation: fadeInUp 0.12s ease;
}
diff --git a/web/src/components/RecordDetailModal.tsx b/web/src/components/RecordDetailModal.tsx
index 6343ac9..c9882a6 100644
--- a/web/src/components/RecordDetailModal.tsx
+++ b/web/src/components/RecordDetailModal.tsx
@@ -113,8 +113,13 @@ const overlay: React.CSSProperties = {
alignItems: 'center', justifyContent: 'center', zIndex: 10000,
};
const modal: React.CSSProperties = {
- background: 'var(--color-bg-modal)', border: '1px solid var(--color-border-modal)', borderRadius: 12,
+ background: 'var(--color-bg-modal-glass)',
+ backdropFilter: 'blur(24px) saturate(180%)',
+ WebkitBackdropFilter: 'blur(24px) saturate(180%)',
+ border: '1px solid var(--color-border-modal-soft)',
+ borderRadius: 12,
width: 560, maxHeight: '80vh', display: 'flex', flexDirection: 'column',
+ boxShadow: 'var(--shadow-glass-light)',
};
const header: React.CSSProperties = {
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
diff --git a/web/src/components/Select.module.css b/web/src/components/Select.module.css
index 335b03f..3534e93 100644
--- a/web/src/components/Select.module.css
+++ b/web/src/components/Select.module.css
@@ -48,13 +48,16 @@
position: absolute;
top: calc(100% + 4px);
left: 0;
- background: var(--color-bg-modal-elevated);
+ background: var(--color-bg-dropdown);
border: 1px solid var(--color-border-card);
border-radius: 8px;
padding: 4px;
z-index: 1000;
- backdrop-filter: blur(20px) saturate(180%);
- box-shadow: 0 8px 32px var(--color-shadow-dropdown);
+ backdrop-filter: var(--bf-glass-md);
+ -webkit-backdrop-filter: var(--bf-glass-md);
+ box-shadow:
+ 0 8px 32px var(--color-shadow-dropdown),
+ inset 0 1px 0 var(--color-inset-highlight);
opacity: 0;
transform: translateY(-4px);
pointer-events: none;
diff --git a/web/src/components/Sidebar.module.css b/web/src/components/Sidebar.module.css
index ac66b4e..2d089c6 100644
--- a/web/src/components/Sidebar.module.css
+++ b/web/src/components/Sidebar.module.css
@@ -2,8 +2,14 @@
width: 76px;
height: 100%;
background: var(--color-sidebar-bg);
- backdrop-filter: blur(16px) saturate(160%);
+ backdrop-filter: var(--bf-glass-md);
+ -webkit-backdrop-filter: var(--bf-glass-md);
border-right: 1px solid var(--color-border-modal-soft);
+ /* V2 玻璃顶边白高光 + 右侧立体阴影 */
+ box-shadow:
+ inset 0 1px 0 var(--color-inset-highlight),
+ 1px 0 0 var(--color-border-soft),
+ 2px 0 12px rgba(0, 0, 0, 0.04);
display: flex;
flex-direction: column;
align-items: center;
diff --git a/web/src/components/Toast.module.css b/web/src/components/Toast.module.css
index 787ecf8..f39ea78 100644
--- a/web/src/components/Toast.module.css
+++ b/web/src/components/Toast.module.css
@@ -3,15 +3,15 @@
top: 20px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
- background: var(--color-bg-card);
- backdrop-filter: blur(24px) saturate(180%);
- -webkit-backdrop-filter: blur(24px) saturate(180%);
+ background: var(--color-bg-glass-strong);
+ backdrop-filter: var(--bf-glass-md);
+ -webkit-backdrop-filter: var(--bf-glass-md);
border: 1px solid var(--color-border-card);
box-shadow:
0 0 0 1px var(--color-inset-highlight) inset,
0 8px 32px var(--color-shadow-dropdown),
- 0 1px 0 var(--color-inset-highlight-strong) inset;
- color: var(--color-on-overlay);
+ inset 0 1px 0 var(--color-inset-highlight-strong);
+ color: var(--color-text-primary);
padding: 10px 24px;
border-radius: 10px;
font-size: 13px;
diff --git a/web/src/components/VideoDetailModal.module.css b/web/src/components/VideoDetailModal.module.css
index 049f1c1..e9b6053 100644
--- a/web/src/components/VideoDetailModal.module.css
+++ b/web/src/components/VideoDetailModal.module.css
@@ -278,9 +278,10 @@
display: flex;
flex-direction: column;
border-left: 1px solid var(--color-border-modal-soft);
- background: var(--color-bg-upload);
- backdrop-filter: blur(24px) saturate(180%);
- -webkit-backdrop-filter: blur(24px) saturate(180%);
+ background: var(--color-bg-glass);
+ backdrop-filter: var(--bf-glass-lg);
+ -webkit-backdrop-filter: var(--bf-glass-lg);
+ box-shadow: inset 0 1px 0 var(--color-inset-highlight);
}
/* Header with download + icons */
diff --git a/web/src/index.css b/web/src/index.css
index 667854b..6ecf76f 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -178,51 +178,91 @@
--color-aurora-1: rgba(108, 99, 255, 0.6);
--color-aurora-2: rgba(59, 130, 246, 0.5);
--color-aurora-3: rgba(139, 92, 246, 0.35);
+ --color-aurora-peach: rgba(255, 130, 100, 0.20);
--color-cursor-glow: rgba(108, 99, 255, 0.06);
--color-grid-line: rgba(255, 255, 255, 0.02);
+
+ /* ─── V2 新增 tokens (2026-05-11) ─── */
+ /* GLASS 类(透明白 + blur,用于 sidebar/banner/modal overlay/dropdown) */
+ --color-bg-glass: rgba(255, 255, 255, 0.06);
+ --color-bg-glass-strong: rgba(255, 255, 255, 0.10);
+ --color-border-glass-edge: rgba(255, 255, 255, 0.12);
+
+ /* 行 hover(实体卡内表格 hover;与 --color-bg-hover 区分) */
+ --color-bg-row-hover: rgba(255, 255, 255, 0.02);
+
+ /* Multi-layer shadows — Vercel/GitBook 风格 */
+ --shadow-card-light: 0 1px 2px rgba(0, 0, 0, 0.30),
+ 0 4px 16px rgba(0, 0, 0, 0.40);
+ --shadow-glass-light: 0 8px 32px rgba(0, 0, 0, 0.30),
+ 0 1px 2px rgba(0, 0, 0, 0.40),
+ inset 0 1px 0 rgba(255, 255, 255, 0.08);
+
+ /* 暖调 chip (公告/Trial/"新版上线") */
+ --color-chip-warm-bg: rgba(255, 200, 130, 0.10);
+ --color-chip-warm-border: rgba(255, 200, 130, 0.25);
+ --color-chip-warm-text: #f1f0ff;
+ --color-chip-warm-badge-bg: rgba(255, 150, 100, 0.20);
+ --color-chip-warm-badge-text: #ffb589;
+
+ /* Backdrop-filter 标准五档 */
+ --bf-glass-sm: blur(12px) saturate(140%);
+ --bf-glass-md: blur(16px) saturate(160%);
+ --bf-glass-lg: blur(24px) saturate(180%);
+ --bf-glass-xl: blur(40px) saturate(180%);
}
/* ══════════════════════════════════════════════
- LIGHT THEME OVERRIDES
- 规范来源: Vercel Geist (#fafafa / #171717 / 阴影边框) + Linear (#f3f4f5 surface)
- 主色加深 18% 满足 WCAG AA 对比度
+ LIGHT THEME OVERRIDES — V2
+ 规范:Vercel Geist 灰阶 + GitBook/Framer 玻璃 + Linear pastel aurora
+ 关键变化:
+ - 玻璃面用 _白透明_ rgba(255,255,255,0.65) — V1 是黑透明,方向反了
+ - aurora 不再 display:none,保留 pastel 紫蓝桃给玻璃穿透色源
+ - 实体卡 bg = #ffffff 纯白 + multi-layer shadow + 1px shadow-border
+ - LandingPage 不再强制 dark,跟随主题切换
+ - 玻璃顶边 inset highlight 白高光是 frosted glass 视觉标志
+ 主色加深 18% 满足 WCAG AA
══════════════════════════════════════════════ */
[data-theme="light"] {
- /* Page surfaces — Vercel Gray 50 + 纯白 modal */
+ /* ── Page surfaces ── */
--color-bg-page: #fafafa;
- --color-bg-input-bar: #ffffff;
- --color-bg-dropdown: rgba(255, 255, 255, 0.96);
- /* 卡片背景加深至 0.05,配合更强的 border 在 #fafafa 上有清晰轮廓 */
- --color-bg-upload: rgba(0, 0, 0, 0.03);
- --color-bg-card: rgba(0, 0, 0, 0.05);
- --color-bg-hover: rgba(0, 0, 0, 0.07);
- /* Sidebar 加深一档,避免在浅色 page bg 上完全融入消失 */
- --color-sidebar-bg: rgba(243, 244, 246, 0.92);
- --color-bg-sidebar: rgba(243, 244, 246, 0.92);
- --color-sidebar-active: rgba(0, 0, 0, 0.08);
- --color-sidebar-hover: rgba(0, 0, 0, 0.05);
+ --color-bg-input-bar: rgba(255, 255, 255, 0.85); /* 玻璃输入条 */
+ --color-bg-dropdown: rgba(255, 255, 255, 0.85); /* 玻璃下拉 */
+ --color-bg-upload: #ffffff; /* 实体上传区 */
+ --color-bg-card: #ffffff; /* ★ V2 核心:实体白卡,不是黑透明 */
+ --color-bg-hover: rgba(0, 0, 0, 0.04); /* 行 hover 黑透明保留(不是玻璃) */
+ --color-bg-row-hover: rgba(0, 0, 0, 0.03);
- /* Borders — Vercel shadow-border 风格,整体加深 0.02 提升浅色下卡片轮廓 */
- --color-border-input-bar: rgba(0, 0, 0, 0.12);
- --color-border-card: rgba(0, 0, 0, 0.10);
- --color-border-upload: rgba(0, 0, 0, 0.08);
- --color-border-modal: #e5e7eb;
- --color-border-modal-soft: rgba(0, 0, 0, 0.08);
+ /* ── GLASS surfaces (sidebar / banner / modal overlay) ── */
+ --color-bg-glass: rgba(255, 255, 255, 0.65); /* ★ V2 核心:玻璃透白 */
+ --color-bg-glass-strong: rgba(255, 255, 255, 0.85);
+ --color-sidebar-bg: rgba(255, 255, 255, 0.70); /* 真正玻璃 */
+ --color-bg-sidebar: rgba(255, 255, 255, 0.70);
+ --color-sidebar-active: rgba(0, 0, 0, 0.06);
+ --color-sidebar-hover: rgba(0, 0, 0, 0.04);
+
+ /* ── Borders — Vercel shadow-border 风格 0.08 ── */
+ --color-border-input-bar: rgba(0, 0, 0, 0.10);
+ --color-border-card: rgba(0, 0, 0, 0.08);
+ --color-border-upload: rgba(0, 0, 0, 0.06);
+ --color-border-modal: rgba(0, 0, 0, 0.06);
+ --color-border-modal-soft: rgba(0, 0, 0, 0.05);
--color-border-modal-hover: #9ca3af;
- --color-border-soft: rgba(0, 0, 0, 0.06);
- --color-border-row: rgba(0, 0, 0, 0.05);
+ --color-border-soft: rgba(0, 0, 0, 0.05);
+ --color-border-row: rgba(0, 0, 0, 0.06);
+ --color-border-glass-edge: rgba(255, 255, 255, 0.70); /* 玻璃外边白高光 */
- /* Text — Vercel 灰阶 #171717 / #4d4d4d / #888 / #cbd5e1 */
- --color-text-primary: #171823;
- --color-text-secondary: #6b6e85;
- --color-text-tertiary: #9ca3af;
- --color-text-quaternary: #cbd5e1;
- --color-text-disabled: #cbd5e1;
+ /* ── Text — Vercel 黑白灰严格灰阶 ── */
+ --color-text-primary: #171717; /* Vercel Black 纯近黑,去紫调 */
+ --color-text-secondary: #525252; /* Gray 600 */
+ --color-text-tertiary: #888888; /* Gray 500 */
+ --color-text-quaternary: #a3a3a3; /* Gray 400 */
+ --color-text-disabled: #a3a3a3;
--color-text-light: #374151;
--color-text-monochrome: #4b5563;
- --color-text-on-glass: rgba(0, 0, 0, 0.75);
- --color-text-on-glass-soft: rgba(0, 0, 0, 0.55);
- --color-text-on-glass-faint: rgba(0, 0, 0, 0.40);
+ --color-text-on-glass: rgba(23, 23, 23, 0.85);
+ --color-text-on-glass-soft: rgba(23, 23, 23, 0.55);
+ --color-text-on-glass-faint: rgba(23, 23, 23, 0.40);
/* Brand — 主色加深 18% (#6c63ff → #5048cc) */
--color-primary: #5048cc;
@@ -256,7 +296,7 @@
--color-purple-bg: rgba(124, 58, 237, 0.10);
--color-purple-bg-hover: rgba(124, 58, 237, 0.06);
- /* Modal & overlay — 浅色下整体减弱 */
+ /* ── Modal & overlay — 浅色下整体减弱 ── */
--color-modal-overlay: rgba(0, 0, 0, 0.20);
--color-overlay-strong: rgba(0, 0, 0, 0.30);
--color-overlay-soft: rgba(0, 0, 0, 0.18);
@@ -265,14 +305,21 @@
--color-overlay-deep: rgba(0, 0, 0, 0.45);
--color-bg-modal: #ffffff;
--color-bg-modal-elevated: #ffffff;
- --color-bg-modal-glass: rgba(255, 255, 255, 0.92);
+ --color-bg-modal-glass: rgba(255, 255, 255, 0.85); /* 略透气 */
--color-bg-modal-hover: #f5f5f5;
--color-bg-elevated: #f3f4f5;
--color-bg-placeholder: #ebebeb;
- --color-bg-dropdown-elevated: #ffffff;
+ --color-bg-dropdown-elevated: rgba(255, 255, 255, 0.95);
--color-bg-video: #000;
- --color-shadow-modal: rgba(0, 0, 0, 0.10);
- --color-shadow-dropdown: rgba(0, 0, 0, 0.08);
+ --color-shadow-modal: rgba(0, 0, 0, 0.08);
+ --color-shadow-dropdown: rgba(0, 0, 0, 0.10);
+
+ /* ── V2 multi-layer shadows ── */
+ --shadow-card-light: 0 1px 2px rgba(0, 0, 0, 0.04),
+ 0 4px 16px rgba(0, 0, 0, 0.06);
+ --shadow-glass-light: 0 8px 32px rgba(0, 0, 0, 0.06),
+ 0 1px 2px rgba(0, 0, 0, 0.08),
+ inset 0 1px 0 rgba(255, 255, 255, 0.60);
/* Charts — 浅色 tooltip 用白底 */
--color-tooltip-bg: rgba(255, 255, 255, 0.98);
@@ -295,9 +342,16 @@
--color-mint-accent-border: rgba(13, 148, 136, 0.30);
--color-mint-accent-glow: rgba(13, 148, 136, 0.18);
- /* Inset highlight (浅色下用淡黑半透明做 inset) */
- --color-inset-highlight: rgba(0, 0, 0, 0.04);
- --color-inset-highlight-strong: rgba(0, 0, 0, 0.06);
+ /* Inset highlight — V2 改为白高光,玻璃顶边视觉标志 */
+ --color-inset-highlight: rgba(255, 255, 255, 0.50);
+ --color-inset-highlight-strong: rgba(255, 255, 255, 0.70);
+
+ /* 暖调 chip — GitBook 风格 */
+ --color-chip-warm-bg: #fff5eb;
+ --color-chip-warm-border: rgba(255, 180, 130, 0.40);
+ --color-chip-warm-text: #1a1a1a;
+ --color-chip-warm-badge-bg: rgba(255, 100, 50, 0.12);
+ --color-chip-warm-badge-text: #c2410c;
/* Mention pill */
--color-mention-bg: rgba(80, 72, 204, 0.10);
@@ -331,19 +385,17 @@
--color-scrollbar-thumb: rgba(0, 0, 0, 0.15);
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.30);
- /* Aurora 在浅色下隐藏(下面有规则),但 var 也置弱以防万一 */
- --color-aurora-1: transparent;
- --color-aurora-2: transparent;
- --color-aurora-3: transparent;
- --color-cursor-glow: rgba(80, 72, 204, 0.04);
+ /* Aurora — V2 保留 pastel 紫蓝桃,给玻璃面提供穿透色源(V1 错点:display:none) */
+ --color-aurora-1: rgba(180, 167, 255, 0.30); /* pastel 紫 */
+ --color-aurora-2: rgba(167, 200, 255, 0.28); /* pastel 蓝青 */
+ --color-aurora-3: rgba(220, 167, 255, 0.22); /* pastel 粉紫 */
+ --color-aurora-peach: rgba(255, 180, 130, 0.25); /* pastel 桃 */
+ --color-cursor-glow: rgba(80, 72, 204, 0.06);
--color-grid-line: rgba(0, 0, 0, 0.025);
}
-/* 浅色下隐藏 aurora 极光层(白底 + 极光会刺眼,纯净白更"高级") */
-[data-theme="light"] .aurora-bg,
-[data-theme="light"] .aurora-blob-3 {
- display: none;
-}
+/* V2 删除原 [data-theme="light"] .aurora-bg { display: none } —
+ 极光在浅色下保留 pastel,给玻璃面 backdrop-filter 提供穿透色源 */
/* ═══════════════════════════════════════════
Reset / globals
diff --git a/web/src/pages/AdminAssetsPage.module.css b/web/src/pages/AdminAssetsPage.module.css
index 68b2d1f..1ec778d 100644
--- a/web/src/pages/AdminAssetsPage.module.css
+++ b/web/src/pages/AdminAssetsPage.module.css
@@ -10,6 +10,7 @@
flex: 1; padding: 16px 20px;
background: var(--color-bg-card); border: 1px solid var(--color-border-card);
border-radius: var(--radius-card);
+ box-shadow: var(--shadow-card-light);
}
.statLabel { font-size: 12px; color: var(--color-text-secondary); margin-bottom: 4px; }
@@ -21,6 +22,7 @@
.accordionItem {
background: var(--color-bg-card); border: 1px solid var(--color-border-card);
border-radius: var(--radius-card); overflow: hidden;
+ box-shadow: var(--shadow-card-light);
}
.accordionHeader {
diff --git a/web/src/pages/AdminLayout.module.css b/web/src/pages/AdminLayout.module.css
index 42f0356..781d7e0 100644
--- a/web/src/pages/AdminLayout.module.css
+++ b/web/src/pages/AdminLayout.module.css
@@ -2,14 +2,23 @@
display: flex;
height: 100vh;
overflow: hidden;
- background: var(--color-bg-page);
+ /* V2: transparent 让全局 AmbientBackground pastel aurora 在主区也能隐约透出 */
+ background: transparent;
+ position: relative;
+ z-index: 2;
}
.sidebar {
width: var(--sidebar-width);
min-width: var(--sidebar-width);
background: var(--color-bg-sidebar);
+ /* V2 玻璃 */
+ backdrop-filter: var(--bf-glass-md);
+ -webkit-backdrop-filter: var(--bf-glass-md);
border-right: 1px solid var(--color-border-card);
+ /* V2 玻璃顶边白高光 + 右侧立体感 */
+ box-shadow: inset 0 1px 0 var(--color-inset-highlight),
+ 2px 0 12px rgba(0, 0, 0, 0.04);
display: flex;
flex-direction: column;
transition: width 0.2s ease, min-width 0.2s ease;
diff --git a/web/src/pages/AuditLogsPage.module.css b/web/src/pages/AuditLogsPage.module.css
index 3f451cd..f1932cb 100644
--- a/web/src/pages/AuditLogsPage.module.css
+++ b/web/src/pages/AuditLogsPage.module.css
@@ -19,6 +19,7 @@
.tableWrapper {
background: var(--color-bg-card); border: 1px solid var(--color-border-card);
border-radius: var(--radius-card); overflow-x: auto;
+ box-shadow: var(--shadow-card-light);
}
.table { width: 100%; border-collapse: collapse; font-size: 13px; }
.table th { padding: 12px 16px; text-align: left; color: var(--color-text-secondary); font-weight: 500; border-bottom: 1px solid var(--color-border-card); white-space: nowrap; }
diff --git a/web/src/pages/DashboardPage.module.css b/web/src/pages/DashboardPage.module.css
index baf17ee..2c86ba5 100644
--- a/web/src/pages/DashboardPage.module.css
+++ b/web/src/pages/DashboardPage.module.css
@@ -21,6 +21,7 @@
border: 1px solid var(--color-border-card);
border-radius: var(--radius-card);
padding: 20px;
+ box-shadow: var(--shadow-card-light);
}
.statLabel {
@@ -72,6 +73,7 @@
border: 1px solid var(--color-border-card);
border-radius: var(--radius-card);
padding: 16px;
+ box-shadow: var(--shadow-card-light);
}
/* Skeleton loading */
diff --git a/web/src/pages/LandingPage.module.css b/web/src/pages/LandingPage.module.css
index a84d830..af46151 100644
--- a/web/src/pages/LandingPage.module.css
+++ b/web/src/pages/LandingPage.module.css
@@ -6,7 +6,7 @@
align-items: center;
justify-content: center;
overflow: hidden;
- background: #000;
+ background: var(--color-bg-page);
z-index: 2;
}
@@ -36,7 +36,7 @@
width: 80px;
height: 80px;
margin-bottom: 28px;
- filter: drop-shadow(0 0 40px rgba(126, 220, 200, 0.25));
+ filter: drop-shadow(0 0 40px var(--color-mint-accent-glow));
animation: fadeUp 1.2s ease-out 0.1s both;
}
@@ -44,7 +44,7 @@
font-family: 'Space Grotesk', sans-serif;
font-size: 48px;
font-weight: 300;
- color: #f1f0ff;
+ color: var(--color-text-primary);
letter-spacing: 0.1em;
margin-bottom: 12px;
line-height: 1.1;
@@ -55,7 +55,7 @@
font-family: 'Space Grotesk', sans-serif;
font-size: 14px;
font-weight: 300;
- color: rgba(255, 255, 255, 0.5);
+ color: var(--color-text-on-glass-soft);
letter-spacing: 0.15em;
margin-bottom: 48px;
animation: fadeUp 1.2s ease-out 0.3s both;
@@ -85,45 +85,47 @@
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
- backdrop-filter: blur(12px);
- -webkit-backdrop-filter: blur(12px);
+ backdrop-filter: var(--bf-glass-sm);
+ -webkit-backdrop-filter: var(--bf-glass-sm);
}
.btnPrimary {
- background: rgba(120, 220, 200, 0.12);
- border: 1px solid rgba(120, 220, 200, 0.3);
+ background: var(--color-mint-accent-bg);
+ border: 1px solid var(--color-mint-accent-border);
}
.btnPrimary .btnName {
font-family: 'Space Grotesk', sans-serif;
font-size: 15px;
font-weight: 500;
- color: #7edcc8;
+ color: var(--color-mint-accent);
}
.btnPrimary:hover {
- background: rgba(120, 220, 200, 0.22);
- box-shadow: 0 0 30px rgba(120, 220, 200, 0.15);
+ background: var(--color-mint-accent-bg-hover);
+ box-shadow: 0 0 30px var(--color-mint-accent-glow);
}
.btnGhost {
- background: rgba(255, 255, 255, 0.05);
- border: 1px solid rgba(255, 255, 255, 0.1);
+ background: var(--color-bg-glass);
+ border: 1px solid var(--color-border-card);
+ /* V2: 玻璃顶边白高光 */
+ box-shadow: inset 0 1px 0 var(--color-inset-highlight);
}
.btnGhost .btnName {
font-family: 'Space Grotesk', sans-serif;
font-size: 15px;
font-weight: 500;
- color: rgba(255, 255, 255, 0.7);
+ color: var(--color-text-on-glass);
}
.btnGhost:hover {
- background: rgba(255, 255, 255, 0.1);
+ background: var(--color-bg-glass-strong);
}
.btnGhost:hover .btnName {
- color: rgba(255, 255, 255, 0.9);
+ color: var(--color-text-primary);
}
/* Sub-text below buttons */
@@ -131,14 +133,15 @@
font-family: 'Space Grotesk', sans-serif;
font-size: 12px;
font-weight: 300;
- color: rgba(120, 220, 200, 0.5);
+ color: var(--color-mint-accent);
+ opacity: 0.65;
}
.btnSubGhost {
font-family: 'Space Grotesk', sans-serif;
font-size: 12px;
font-weight: 300;
- color: rgba(255, 255, 255, 0.35);
+ color: var(--color-text-tertiary);
}
/* ── Easter egg ── */
@@ -152,17 +155,19 @@
font-size: 13px;
font-weight: 300;
font-style: italic;
- color: rgba(255, 255, 255, 0.06);
+ color: var(--color-text-quaternary);
+ opacity: 0.45;
letter-spacing: 0.05em;
cursor: default;
- transition: color 2s ease;
+ transition: color 2s ease, opacity 2s ease;
white-space: nowrap;
text-align: center;
animation: fadeUp 1.2s ease-out 0.8s both;
}
.easter:hover {
- color: rgba(255, 255, 255, 0.25);
+ color: var(--color-text-tertiary);
+ opacity: 1;
}
/* ── Air Spark full-screen overlay ── */
@@ -174,7 +179,7 @@
align-items: center;
justify-content: center;
cursor: pointer;
- background: rgba(0, 0, 0, 0.5);
+ background: var(--color-overlay-soft);
backdrop-filter: blur(30px);
-webkit-backdrop-filter: blur(30px);
animation: sparkBgIn 0.5s ease-out both;
@@ -189,7 +194,7 @@
to {
backdrop-filter: blur(30px);
-webkit-backdrop-filter: blur(30px);
- background: rgba(0, 0, 0, 0.5);
+ background: var(--color-overlay-soft);
}
}
@@ -215,7 +220,7 @@
font-family: 'Space Grotesk', sans-serif;
font-size: clamp(40px, 5vw, 64px);
font-weight: 300;
- color: #ffffff;
+ color: var(--color-text-primary);
line-height: 1.2;
text-align: center;
}
@@ -229,7 +234,7 @@
font-family: 'Space Grotesk', sans-serif;
font-size: 16px;
font-weight: 300;
- color: rgba(255, 255, 255, 0.5);
+ color: var(--color-text-secondary);
margin-top: 20px;
}
@@ -253,14 +258,14 @@
-webkit-tap-highlight-color: transparent;
user-select: none;
-webkit-user-select: none;
- color: rgba(255, 255, 255, 0.2);
+ color: var(--color-text-quaternary);
cursor: pointer;
transition: color 0.3s;
animation: fadeUp 1.2s ease-out 0.8s both;
}
.musicBtn:hover {
- color: rgba(255, 255, 255, 0.5);
+ color: var(--color-text-tertiary);
}
diff --git a/web/src/pages/LandingPage.tsx b/web/src/pages/LandingPage.tsx
index e13a7d5..cdd0758 100644
--- a/web/src/pages/LandingPage.tsx
+++ b/web/src/pages/LandingPage.tsx
@@ -111,10 +111,9 @@ export function LandingPage({ autoLogin }: Props) {
}, [playing]);
return (
- // 强制深色:LandingPage 是品牌专属 Air Spark 体验页,
- // 黑底 + 极光 + 薄荷绿是核心调性,浅色化会破坏品牌识别。
- // 整个登录流程(含 LoginModal / ForceChangePasswordModal)都继承这个 dark 子树。
-
+ // V2: 跟随全局主题切换。LandingPage 浅色化 = AuroraCanvas + LoginModal 都跟随。
+ // 薄荷绿在浅色下加深为 teal (#0d9488),保证对比度。
+
{/* Layer 1-4: Aurora background */}
diff --git a/web/src/pages/LoginRecordsPage.module.css b/web/src/pages/LoginRecordsPage.module.css
index f9dec1a..aa84b99 100644
--- a/web/src/pages/LoginRecordsPage.module.css
+++ b/web/src/pages/LoginRecordsPage.module.css
@@ -19,6 +19,7 @@
.tableWrapper {
background: var(--color-bg-card); border: 1px solid var(--color-border-card);
border-radius: var(--radius-card); overflow-x: auto;
+ box-shadow: var(--shadow-card-light);
}
.table { width: 100%; border-collapse: collapse; font-size: 13px; max-width: none; }
.table th { padding: 12px 16px; text-align: left; color: var(--color-text-secondary); font-weight: 500; border-bottom: 1px solid var(--color-border-card); white-space: nowrap; }
diff --git a/web/src/pages/ProfilePage.module.css b/web/src/pages/ProfilePage.module.css
index d1787d4..e969421 100644
--- a/web/src/pages/ProfilePage.module.css
+++ b/web/src/pages/ProfilePage.module.css
@@ -135,6 +135,7 @@
display: flex;
flex-direction: column;
justify-content: center;
+ box-shadow: var(--shadow-card-light);
}
.quotaLabel {
@@ -220,6 +221,7 @@
border: 1px solid var(--color-border-card);
border-radius: var(--radius-card);
padding: 16px;
+ box-shadow: var(--shadow-card-light);
}
/* Records */
@@ -245,6 +247,7 @@
border: 1px solid var(--color-border-card);
border-radius: var(--radius-card);
transition: border-color 0.2s;
+ box-shadow: var(--shadow-card-light);
}
.recordItem:hover {
diff --git a/web/src/pages/RecordsPage.module.css b/web/src/pages/RecordsPage.module.css
index d359d7a..a45d9d3 100644
--- a/web/src/pages/RecordsPage.module.css
+++ b/web/src/pages/RecordsPage.module.css
@@ -29,6 +29,7 @@
.tableWrapper {
background: var(--color-bg-card); border: 1px solid var(--color-border-card);
border-radius: var(--radius-card); overflow-x: auto;
+ box-shadow: var(--shadow-card-light);
}
.table { width: 100%; border-collapse: collapse; font-size: 13px; }
.table th { padding: 12px 16px; text-align: left; color: var(--color-text-secondary); font-weight: 500; border-bottom: 1px solid var(--color-border-card); white-space: nowrap; }
diff --git a/web/src/pages/SettingsPage.module.css b/web/src/pages/SettingsPage.module.css
index b845fb1..ceca815 100644
--- a/web/src/pages/SettingsPage.module.css
+++ b/web/src/pages/SettingsPage.module.css
@@ -6,6 +6,7 @@
.card {
background: var(--color-bg-card); border: 1px solid var(--color-border-card);
border-radius: var(--radius-card); padding: 24px;
+ box-shadow: var(--shadow-card-light);
}
.cardHeader { display: flex; justify-content: space-between; align-items: flex-start; }
.cardTitle { font-size: 16px; font-weight: 600; color: var(--color-text-primary); margin-bottom: 4px; }
diff --git a/web/src/pages/TeamsPage.module.css b/web/src/pages/TeamsPage.module.css
index 58e70d7..d9bf55e 100644
--- a/web/src/pages/TeamsPage.module.css
+++ b/web/src/pages/TeamsPage.module.css
@@ -19,6 +19,7 @@
.tableWrapper {
background: var(--color-bg-card); border: 1px solid var(--color-border-card);
border-radius: var(--radius-card); overflow-x: auto;
+ box-shadow: var(--shadow-card-light);
}
.table { width: 100%; border-collapse: collapse; font-size: 13px; }
.table th { padding: 12px 16px; text-align: left; color: var(--color-text-secondary); font-weight: 500; border-bottom: 1px solid var(--color-border-card); white-space: nowrap; }
@@ -86,8 +87,8 @@
.detailModal {
background: var(--color-bg-modal-glass);
- backdrop-filter: blur(24px) saturate(180%);
- -webkit-backdrop-filter: blur(24px) saturate(180%);
+ backdrop-filter: var(--bf-glass-lg);
+ -webkit-backdrop-filter: var(--bf-glass-lg);
border: 1px solid var(--color-border-modal-soft);
border-radius: 16px;
width: 1280px;
@@ -96,7 +97,7 @@
max-height: 90vh;
display: flex;
flex-direction: column;
- box-shadow: 0 24px 64px var(--color-shadow-modal), 0 0 0 1px var(--color-border-row) inset;
+ box-shadow: 0 24px 64px var(--color-shadow-modal), var(--shadow-glass-light);
animation: modalIn 0.25s ease;
}
diff --git a/web/src/pages/UsersPage.module.css b/web/src/pages/UsersPage.module.css
index d953410..2b1ee11 100644
--- a/web/src/pages/UsersPage.module.css
+++ b/web/src/pages/UsersPage.module.css
@@ -25,6 +25,7 @@
.tableWrapper {
background: var(--color-bg-card); border: 1px solid var(--color-border-card);
border-radius: var(--radius-card); overflow-x: auto;
+ box-shadow: var(--shadow-card-light);
}
.table { width: 100%; border-collapse: collapse; font-size: 13px; }
.table th { padding: 12px 16px; text-align: left; color: var(--color-text-secondary); font-weight: 500; border-bottom: 1px solid var(--color-border-card); white-space: nowrap; }
diff --git a/web/test/theme-screenshots-v2.mjs b/web/test/theme-screenshots-v2.mjs
new file mode 100644
index 0000000..c2eba4f
--- /dev/null
+++ b/web/test/theme-screenshots-v2.mjs
@@ -0,0 +1,141 @@
+/**
+ * Theme switching visual regression — captures dark + light screenshots of key pages.
+ *
+ * Run from web/ directory after starting backend (port 8000) + dev server (port 5173):
+ * node test/theme-screenshots.mjs
+ *
+ * Output: ../docs/screenshots/
__.png
+ */
+import { chromium } from '@playwright/test';
+import { mkdir } from 'node:fs/promises';
+import { resolve, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const OUT_DIR = resolve(__dirname, '../../docs/screenshots/v2');
+const BASE = 'http://localhost:5173';
+const API = 'http://localhost:8000';
+
+const ADMIN = { username: 'admin', password: 'admin123' };
+const TEAM_USER = { username: 'screenshot_user', password: 'shotpass123' };
+
+/** Set theme directly via localStorage + html attribute, no UI click needed. */
+async function setTheme(page, theme) {
+ await page.evaluate((t) => {
+ localStorage.setItem('airdrama-theme', t);
+ document.documentElement.dataset.theme = t;
+ }, theme);
+ // Reload to ensure ECharts and any once-mounted styles re-init
+ await page.reload({ waitUntil: 'domcontentloaded' });
+ await page.waitForTimeout(400);
+}
+
+/** Programmatic login: POST to API → seed tokens into localStorage → navigate. */
+async function login(page, creds) {
+ const res = await page.request.post(`${API}/api/v1/auth/login`, { data: creds });
+ if (!res.ok()) throw new Error(`login ${creds.username} failed: ${res.status()} ${await res.text()}`);
+ const body = await res.json();
+ const access = body?.tokens?.access;
+ const refresh = body?.tokens?.refresh;
+ const user = body?.user;
+ if (!access) throw new Error(`login ${creds.username}: no access token in response`);
+ await page.goto(`${BASE}/login`, { waitUntil: 'domcontentloaded' });
+ await page.evaluate(({ access, refresh, user }) => {
+ localStorage.setItem('access_token', access);
+ if (refresh) localStorage.setItem('refresh_token', refresh);
+ if (user) localStorage.setItem('user', JSON.stringify(user));
+ }, { access, refresh, user });
+}
+
+async function shot(page, slug, theme) {
+ const file = resolve(OUT_DIR, `${slug}__${theme}.png`);
+ await page.screenshot({ path: file, fullPage: false });
+ console.log(` ✓ ${slug}__${theme}.png`);
+}
+
+/** Visit URL, wait for network idle + a settle timeout, then screenshot in both themes. */
+async function visitAndCapture(page, slug, url, opts = {}) {
+ for (const theme of ['dark', 'light']) {
+ await setTheme(page, theme);
+ await page.goto(`${BASE}${url}`, { waitUntil: 'domcontentloaded' }).catch(() => {});
+ await page.waitForTimeout(opts.settle ?? 800);
+ if (opts.afterLoad) await opts.afterLoad(page);
+ await shot(page, slug, theme);
+ }
+}
+
+async function main() {
+ await mkdir(OUT_DIR, { recursive: true });
+
+ const browser = await chromium.launch({ headless: true });
+ const ctx = await browser.newContext({
+ viewport: { width: 1440, height: 900 },
+ deviceScaleFactor: 1,
+ });
+ const page = await ctx.newPage();
+ // Mute console errors (API 4xx/5xx in empty DB are noisy but expected)
+ page.on('pageerror', () => {});
+ page.on('console', () => {});
+
+ console.log(`▼ Capturing to ${OUT_DIR}`);
+
+ // 1. Login page (no auth needed) — use a fresh context so localStorage is clean
+ console.log('\n[1/12] /login');
+ for (const theme of ['dark', 'light']) {
+ await page.goto(`${BASE}/login`, { waitUntil: 'domcontentloaded' });
+ await setTheme(page, theme);
+ await page.waitForTimeout(500);
+ await shot(page, '01_login', theme);
+ }
+
+ // 2. Login as admin → admin pages
+ console.log('\n[2/12] admin login');
+ await login(page, ADMIN);
+
+ await visitAndCapture(page, '02_admin_dashboard', '/admin/dashboard', { settle: 1500 });
+ console.log('[3/12] /admin/dashboard');
+
+ await visitAndCapture(page, '03_admin_users', '/admin/users');
+ console.log('[4/12] /admin/users');
+
+ await visitAndCapture(page, '04_admin_records', '/admin/records');
+ console.log('[5/12] /admin/records');
+
+ await visitAndCapture(page, '05_admin_settings', '/admin/settings');
+ console.log('[6/12] /admin/settings');
+
+ await visitAndCapture(page, '06_admin_security', '/admin/security');
+ console.log('[7/12] /admin/security');
+
+ await visitAndCapture(page, '07_admin_logs', '/admin/logs');
+ console.log('[8/12] /admin/logs');
+
+ await visitAndCapture(page, '08_admin_assets', '/admin/assets');
+ console.log('[9/12] /admin/assets');
+
+ // 3. Switch to team_admin user → generation + profile + team pages
+ console.log('\n[10/12] team_user login');
+ await ctx.clearCookies();
+ await page.evaluate(() => localStorage.clear());
+ await login(page, TEAM_USER);
+
+ await visitAndCapture(page, '09_generation', '/app', { settle: 1200 });
+ console.log('[10/12] /app');
+
+ await visitAndCapture(page, '10_profile', '/profile', { settle: 1200 });
+ console.log('[11/12] /profile');
+
+ await visitAndCapture(page, '11_team_dashboard', '/team/dashboard', { settle: 1500 });
+ console.log('[12/12] /team/dashboard');
+
+ await visitAndCapture(page, '12_team_members', '/team/members');
+ console.log('[12/12] /team/members');
+
+ await browser.close();
+ console.log('\n✅ done');
+}
+
+main().catch((err) => {
+ console.error('❌ screenshot run failed:', err);
+ process.exit(1);
+});