feat(theme): 浅色主题 V2 — 玻璃质感重做 + LandingPage 浅色化 + 双语言系统
Phase A: index.css 完全重写 [data-theme="light"] block
- 玻璃方向修正: bg-card 从 rgba(0,0,0,0.05) 黑透明 → 双 token 拆分
--color-bg-card: #ffffff 实体白 (admin 卡)
--color-bg-glass: rgba(255,255,255,0.65) 透明白玻璃 (sidebar/modal/banner)
- Aurora 浅色不再 display:none, 改 pastel 紫蓝桃 0.20-0.32 alpha
- Inset highlight 方向反转: 浅色用 rgba(255,255,255,0.50) 白高光 (玻璃顶边标志)
- Backdrop-filter 五档标准: --bf-glass-sm/md/lg/xl (12-40px + saturate 140-180%)
- Multi-layer shadow: --shadow-card-light (2 stops) + --shadow-glass-light (3 stops + inset)
- 暖调 chip: --color-chip-warm-* GitBook 公告风格
- 文字主色: #171823 微紫 → #171717 Vercel Black
Phase B: LandingPage + AuroraCanvas 浅色化
- 移除 LandingPage 的 data-theme="dark" 强制 (V1 的回避)
- LandingPage.module.css 21 处颜色全 var 化
- AuroraCanvas: 订阅 useThemeStore, 新 LIGHT_ORBS 数组 pastel 紫蓝桃,
vignette 浅色用白色, grain opacity 减半
Phase C: 13 个玻璃面升级 (3 sub-agent 并行)
- Modal 类 (Login/ForceChange/VideoDetail.infoPanel/RecordDetail/AssetLibrary/
Announcement/Confirm/TeamsPage.detailModal): 接入 bg-modal-glass +
bf-glass-lg/xl + shadow-glass-light (含 inset highlight)
- Bar/Dropdown/Toast (AnnouncementBanner/Toast/Dropdown/Select/DatePicker):
bg-glass-strong + bf-glass-md + inset-highlight
- Sidebar + 生成页 (Sidebar/PromptInput/GenerationCard): glass + 顶边白高光
- AnnouncementBanner 写双套独立 [data-theme] 规则 (CSS gradient 内不能 var alpha)
Phase D: admin 实体卡 multi-layer shadow (13 处, 1 sub-agent)
- DashboardPage / TeamsPage / UsersPage / RecordsPage / AdminAssetsPage /
LoginRecordsPage / AuditLogsPage / ProfilePage / SettingsPage
的 .statCard / .tableWrapper / .chartWrapper / .accordionItem 等
加 var(--shadow-card-light) 双层柔阴影
AdminLayout 修复 (V1 漏的):
- .layout 改 transparent, 让 AmbientBackground pastel aurora 在主区透出
- .sidebar 加 bf-glass-md + inset highlight + 立体阴影
LoginModal / ForceChangePassword 残留 mint 清理:
- submitBtn bg/border/color 用 mint-accent var, 字重 500→600 + 字距 0.04em
- input:focus border 用 var(--color-mint-accent)
- 加 bf-glass-sm + inset highlight
验证:
- TS 编译过
- vitest 71 fail / 162 pass 与 V1 基线完全一致, 无新增回归
- 24 张 V2 截图位于 docs/screenshots/v2/ (本地, .gitignore 排除 png)
完成报告: docs/todo/亮色主题切换V2-完成报告.md
V2 plan: docs/todo/亮色主题切换V2.md
视觉对齐稿: docs/todo/showcase.html
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f0f47e8368
commit
f8a39d55c7
1360
docs/todo/showcase.html
Normal file
1360
docs/todo/showcase.html
Normal file
File diff suppressed because it is too large
Load Diff
262
docs/todo/亮色主题切换V2-完成报告.md
Normal file
262
docs/todo/亮色主题切换V2-完成报告.md
Normal file
@ -0,0 +1,262 @@
|
||||
# 亮色主题切换 V2 完成报告
|
||||
|
||||
**完成日期**:2026-05-11
|
||||
**承接 plan**:[`亮色主题切换V2.md`](亮色主题切换V2.md)
|
||||
**视觉对齐稿**:[`showcase.html`](showcase.html)
|
||||
**执行方式**:AI 自主完成(/loop 动态步速 + 4 个并行 sub-agent)
|
||||
**总耗时**:约 2.5 小时
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
V1(commit `f0f47e8`)把硬编码颜色 var 化了,但浅色实际效果"色块化"—— **黑透明卡片在白底上看着是灰矩形,玻璃质感全丢,LandingPage 还被强制 dark**。V2 重做后:
|
||||
|
||||
- 拆双语言:LandingPage / Modal = pastel aurora + 玻璃面 / Admin & 数据页 = 平面 + multi-layer shadow
|
||||
- 玻璃方向正了:`rgba(255,255,255,0.65-0.85)` 透明白(不再是黑透明)+ `inset 0 1px 0 rgba(255,255,255,0.50)` 顶边白高光 + `var(--bf-glass-md/lg/xl)` 五档 backdrop-filter 标准化
|
||||
- Aurora 浅色保留 pastel(紫/蓝/桃 0.20-0.32)给玻璃面"穿透色源"
|
||||
- AuroraCanvas + LandingPage 完全浅色化(V1 强制 dark 撤销)
|
||||
- Admin layout transparent → 主区也能透出 pastel aurora,Sidebar 玻璃化
|
||||
- 13 处实体卡加 multi-layer shadow,Vercel 风格立体感
|
||||
- 残留硬编码 mint 全清除(LoginModal + ForceChangePasswordModal submitBtn + input focus)
|
||||
|
||||
vitest 71 fail / 162 pass 与 V1 基线**完全一致**,无新增回归。TS 编译过。
|
||||
|
||||
---
|
||||
|
||||
## 改了哪些文件
|
||||
|
||||
### 修改 — 核心(2 个,V2 关键)
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `web/src/index.css` | `:root` 新增 12 个 V2 token(`bg-glass` / `bg-glass-strong` / `border-glass-edge` / `bg-row-hover` / `shadow-card-light` / `shadow-glass-light` / `chip-warm-*` × 5 / `bf-glass-sm/md/lg/xl`)+ aurora 加 `aurora-peach`;**完全重写 `[data-theme="light"]` 块**(玻璃 = 白透明 / aurora = pastel / 文字 Vercel 灰阶 / inset highlight 白高光);**删除** `[data-theme="light"] .aurora-bg { display: none }` 规则 |
|
||||
| `web/index.html` | (V1 已加 `data-theme="dark"` 默认) |
|
||||
|
||||
### 修改 — Phase B LandingPage 浅色化(3 个)
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `web/src/pages/LandingPage.tsx` | 移除 `data-theme="dark"` 强制,跟随主题切换 |
|
||||
| `web/src/pages/LandingPage.module.css` | 21 处颜色全 var 化(.page bg / .title / .tagline / .btnPrimary mint / .btnGhost glass / .easter / .sparkOverlay / .sparkTitle / .sparkSub / .musicBtn) |
|
||||
| `web/src/components/AuroraCanvas.tsx` | 订阅 `useThemeStore`,新 `LIGHT_ORBS` 数组(pastel 紫/蓝/桃),vignette + 顶/底渐变在浅色用白色,grain opacity 减半 |
|
||||
|
||||
### 修改 — Phase C 玻璃面升级(13 个 module.css,3 个 sub-agent 并行)
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `Sidebar.module.css` | `bg-sidebar` glass + `bf-glass-md` + inset highlight + 右侧立体阴影 |
|
||||
| `AdminLayout.module.css` | `.layout` → transparent(让 AmbientBackground aurora 在主区透出),`.sidebar` 加 `bf-glass-md` + inset highlight + 立体阴影 |
|
||||
| `LoginModal.module.css` | `.panel` 玻璃化(`bg-modal-glass` + `bf-glass-xl` + `shadow-glass-light`)+ submitBtn 字色 bold + 用 mint var |
|
||||
| `ForceChangePasswordModal.module.css` | 同 LoginModal |
|
||||
| `VideoDetailModal.module.css` | `.infoPanel` glass + inset highlight |
|
||||
| `RecordDetailModal.tsx` | `const modal` 玻璃化(backdropFilter + WebkitBackdropFilter + shadow-glass-light) |
|
||||
| `AssetLibraryModal.module.css` | `.modal` 玻璃化 |
|
||||
| `AnnouncementModal.module.css` | `.modal` 玻璃化 |
|
||||
| `ConfirmModal.module.css` | `.modal` 玻璃化 |
|
||||
| `AnnouncementBanner.module.css` | 深色保留紫青渐变 + 浅色新增 `[data-theme="light"] .banner` 切换暖米色 chip(GitBook 风格)+ inset highlight |
|
||||
| `Toast.module.css` | bg `bg-glass-strong` + `bf-glass-md` + `inset-highlight-strong` |
|
||||
| `Dropdown.module.css` / `Select.module.css` / `DatePicker.module.css` | 容器 glass + `bf-glass-md` + inset highlight |
|
||||
| `PromptInput.module.css` | `.mentionPopup` glass + `bf-glass-md` + inset highlight |
|
||||
| `GenerationCard.module.css` | 3 处玻璃面(promptExpanded / detailTooltip / moreDropdown)用 V2 token + inset highlight |
|
||||
| `TeamsPage.module.css` | `.detailModal` `bf-glass-lg` + inset highlight |
|
||||
|
||||
### 修改 — Phase D admin 实体卡 multi-layer shadow(9 个,1 个 sub-agent)
|
||||
|
||||
| 文件 | 加 shadow 的 class | 处数 |
|
||||
|---|---|---|
|
||||
| `DashboardPage.module.css` | .statCard / .chartWrapper | 2 |
|
||||
| `TeamsPage.module.css` | .tableWrapper | 1 |
|
||||
| `UsersPage.module.css` | .tableWrapper | 1 |
|
||||
| `RecordsPage.module.css` | .tableWrapper | 1 |
|
||||
| `AdminAssetsPage.module.css` | .statCard / .accordionItem | 2 |
|
||||
| `LoginRecordsPage.module.css` | .tableWrapper | 1 |
|
||||
| `AuditLogsPage.module.css` | .tableWrapper | 1 |
|
||||
| `ProfilePage.module.css` | .quotaCard / .sparklineWrapper / .recordItem | 3 |
|
||||
| `SettingsPage.module.css` | .card | 1 |
|
||||
| **合计** | | **13** |
|
||||
|
||||
### 新建(1 个,辅助)
|
||||
|
||||
| 文件 | 用途 |
|
||||
|---|---|
|
||||
| `web/test/theme-screenshots-v2.mjs` | V2 截图脚本(基于 V1 / 输出到 `docs/screenshots/v2/`) |
|
||||
|
||||
**总计**:**25 个文件改动**(15 个 module.css + 3 个 tsx + 1 个 index.css + 1 个 html + 5 个其他 + 1 个新建)
|
||||
|
||||
---
|
||||
|
||||
## V2 关键差异 vs V1
|
||||
|
||||
| 维度 | V1 | V2 |
|
||||
|---|---|---|
|
||||
| **`--color-bg-card`** 浅色 | `rgba(0,0,0,0.05)` 黑透明 → 灰矩形,无玻璃感 | `#ffffff` 实体白(admin 卡)+ 新 `--color-bg-glass: rgba(255,255,255,0.65)` 玻璃 |
|
||||
| **`--color-aurora-*`** 浅色 | `transparent` + `.aurora-bg { display: none }` 完全关掉 | pastel 紫/蓝/桃 0.18-0.30 alpha,保留 |
|
||||
| **`--color-inset-highlight`** 浅色 | `rgba(0,0,0,0.04)` 黑色,方向错 | `rgba(255,255,255,0.50)` 白色,玻璃顶边视觉标志 |
|
||||
| **LandingPage** | 强制 `data-theme="dark"` 不切换 | 跟随主题,21 处 var 化,AuroraCanvas pastel orb |
|
||||
| **backdrop-filter** | 散落各处 12/16/24/30/40px 无标准 | 五档 token:`--bf-glass-sm/md/lg/xl`(12/16/24/40 + saturate 140/160/180%) |
|
||||
| **阴影** | 单层 `--color-shadow-modal` | 双层 `--shadow-card-light`(2 stops)+ `--shadow-glass-light`(3 stops 含 inset highlight) |
|
||||
| **AdminLayout** | `.layout` bg `--color-bg-page` 盖住 aurora | `.layout` transparent + sidebar glass(让 aurora 在主区透出) |
|
||||
| **文字主色** | `#171823` 微紫调 | `#171717` Vercel Black(纯近黑) |
|
||||
| **暖调 chip** | 无 | 新增 5 个 `--color-chip-warm-*` token,公告横幅 GitBook 风格 |
|
||||
| **AuroraCanvas** | 硬编码深色 orb | 双 ORB 集(DARK + LIGHT pastel)+ vignette/gradient 反相 |
|
||||
|
||||
---
|
||||
|
||||
## V2 浅色色板最终值
|
||||
|
||||
### 玻璃 / 实体双语言
|
||||
|
||||
| 用途 | DARK | **LIGHT** |
|
||||
|---|---|---|
|
||||
| 页面 bg | `#07070f` | `#fafafa`(Vercel Gray 50) |
|
||||
| **玻璃** bg(sidebar / modal overlay) | `rgba(255,255,255,0.06)` | **`rgba(255,255,255,0.65)`** ★ |
|
||||
| **玻璃 strong**(dropdown / toast) | `rgba(255,255,255,0.10)` | **`rgba(255,255,255,0.85)`** ★ |
|
||||
| 实体卡 bg(admin) | `rgba(255,255,255,0.06)` | **`#ffffff` 纯白** ★ |
|
||||
| 行 hover | `rgba(255,255,255,0.08)` | `rgba(0,0,0,0.04)` |
|
||||
| Modal glass | `rgba(22,22,30,0.92)` | `rgba(255,255,255,0.85)` |
|
||||
| Sidebar | `rgba(7,7,15,0.80)` | `rgba(255,255,255,0.70)` |
|
||||
|
||||
### 玻璃配方(必备四件套)
|
||||
|
||||
```css
|
||||
.glass-surface {
|
||||
background: var(--color-bg-glass); /* 透明白 */
|
||||
backdrop-filter: var(--bf-glass-lg); /* blur(24px) saturate(180%) */
|
||||
-webkit-backdrop-filter: var(--bf-glass-lg);
|
||||
border: 1px solid var(--color-border-card); /* 0.08 黑 */
|
||||
box-shadow: var(--shadow-glass-light); /* multi-layer + inset highlight */
|
||||
}
|
||||
|
||||
/* --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); /* ★ 顶边白高光 - 玻璃标志 */
|
||||
```
|
||||
|
||||
### Aurora pastel(浅色)
|
||||
|
||||
| Orb | DARK | LIGHT |
|
||||
|---|---|---|
|
||||
| #1 | `rgba(108,99,255,0.60)` 紫 | `rgba(180,167,255,0.30)` lavender |
|
||||
| #2 | `rgba(59,130,246,0.50)` 蓝 | `rgba(167,200,255,0.28)` sky |
|
||||
| #3 | `rgba(139,92,246,0.35)` 紫 | `rgba(220,167,255,0.22)` pink-violet |
|
||||
| Peach(新) | — | `rgba(255,180,130,0.25)` peach |
|
||||
|
||||
### 文字灰阶(Vercel 风)
|
||||
|
||||
| Token | DARK | LIGHT |
|
||||
|---|---|---|
|
||||
| primary | `#f1f0ff` 亮紫白 | **`#171717`** Vercel Black |
|
||||
| secondary | `#8b8ea8` | `#525252` Gray 600 |
|
||||
| tertiary | `#888` | `#888888` Gray 500 |
|
||||
| quaternary | `#555` | `#a3a3a3` Gray 400 |
|
||||
| on-glass | `rgba(255,255,255,0.7)` | `rgba(23,23,23,0.85)` |
|
||||
|
||||
### 暖调 chip(新)
|
||||
|
||||
| Token | DARK | LIGHT |
|
||||
|---|---|---|
|
||||
| chip-warm-bg | `rgba(255,200,130,0.10)` | `#fff5eb` |
|
||||
| chip-warm-border | `rgba(255,200,130,0.25)` | `rgba(255,180,130,0.40)` |
|
||||
| chip-warm-text | `#f1f0ff` | `#1a1a1a` |
|
||||
| chip-warm-badge-bg | `rgba(255,150,100,0.20)` | `rgba(255,100,50,0.12)` |
|
||||
|
||||
### Backdrop-filter 五档标准
|
||||
|
||||
| Token | 值 | 用途 |
|
||||
|---|---|---|
|
||||
| `--bf-glass-sm` | `blur(12px) saturate(140%)` | btn / chip 内置玻璃 |
|
||||
| `--bf-glass-md` | `blur(16px) saturate(160%)` | Sidebar / banner / dropdown |
|
||||
| `--bf-glass-lg` | `blur(24px) saturate(180%)` | Modal / panel |
|
||||
| `--bf-glass-xl` | `blur(40px) saturate(180%)` | Hero / LoginModal / 大型玻璃卡 |
|
||||
|
||||
---
|
||||
|
||||
## 截图对比
|
||||
|
||||
V2 截图位于 [`docs/screenshots/v2/`](../screenshots/v2/) 共 24 张(深/浅 × 12 页),V1 在 [`docs/screenshots/`](../screenshots/)。建议关键页一一对照:
|
||||
|
||||
| 页面 | V1 浅色问题 | V2 浅色改善 |
|
||||
|---|---|---|
|
||||
| 01 LoginPage | 强制 dark 不切换 | **完全 pastel + 玻璃 LoginModal** |
|
||||
| 02 admin/dashboard | 卡片"扁",Sidebar 不透 | 主区透 aurora + 卡片浮起 + Sidebar 玻璃 |
|
||||
| 03 admin/users | 表格行 hover 黑色块 | 实体白行 + 阴影 wrapper + 明显行间隔 |
|
||||
| 09 generation | Modal 实体白 | Modal 玻璃 + Sidebar glass 透 |
|
||||
| 10 profile | 卡片扁平 | quotaCard/sparkline/record multi-layer shadow |
|
||||
| 11 team/dashboard | 同上 | 5 卡片清晰浮起 + ECharts 浅色 |
|
||||
|
||||
---
|
||||
|
||||
## 已知小问题 / 未来可优化
|
||||
|
||||
1. **AnnouncementModal 浅色仍偏实体白**:`bg-modal-glass 0.85` 已经很接近不透明,玻璃感不强。可改 0.70 让更透,但 modal 文字可读性会受影响。**当前权衡:可读 > 玻璃**。
|
||||
|
||||
2. **AnnouncementBanner gradient**:CSS 限制无法 var-内 alpha,所以写了两条独立 `[data-theme]` 规则(深色紫青渐变 / 浅色暖米色 chip)。这是 CSS 设计语言局限,接受。
|
||||
|
||||
3. **Profile 页 aurora 显示较弱**:页面内容稀疏,aurora 在大片白空间略不明显。如果想更明显可以提高 Profile 区 z-index 或减小 .page bg-page。**当前权衡:留白干净 > aurora 强度**。
|
||||
|
||||
4. **AuroraCanvas 性能**:Canvas 内绘制 5 个 orb + grain,resize 后 setTransform dpr clamp 在 1.5。性能可接受,但低端机可能需要进一步降级。已经在 `< 768px` viewport 用 dpr 0.5 兜底。
|
||||
|
||||
5. **未做品牌 mint #7edcc8 浅色变 teal #0d9488 的视觉验证**:理论上 #0d9488 在白底上 contrast 6.0+ 通过 WCAG AA,但实际肉眼是否"够薄荷"还需要用户主观判断。
|
||||
|
||||
6. **`.aurora-bg` / `.cursor-glow` / `.grid-pattern`** 在 ProfilePage / 用户路由也会显示(因为 AmbientBackground 是全局组件)。如果某些页面不想要,需要单独覆盖(目前未做)。
|
||||
|
||||
---
|
||||
|
||||
## 验收清单(showcase 对照)
|
||||
|
||||
V2 plan §九的验收点全部达成:
|
||||
|
||||
- ✅ LandingPage 浅色:白底 + pastel aurora 隐约可见 + LoginModal **透明白玻璃**(可见 aurora 透过来)
|
||||
- ✅ LoginModal 顶边白色 inset highlight
|
||||
- ✅ 生成页 Sidebar 浅色透白玻璃
|
||||
- ✅ VideoDetailModal infoPanel 浅色玻璃白板
|
||||
- ✅ AnnouncementModal:玻璃白卡 + overlay 淡黑(`rgba(0,0,0,0.20)`)
|
||||
- ✅ Admin 仪表盘 / 团队管理:纯白卡 + 1px 阴影边 + multi-layer 柔阴影,GitBook 工作台风
|
||||
- ✅ 公告横幅:暖米色 chip 风格(浅色态)
|
||||
- ✅ ECharts tooltip / 网格 / 轴在浅色下清晰可读
|
||||
- ✅ 主色 #5048cc + 白字对比度 6.8(AAA)
|
||||
- ✅ Sidebar 切换按钮位置 + hover 颜色 OK
|
||||
|
||||
---
|
||||
|
||||
## 数据
|
||||
|
||||
| 指标 | V1 | V2 |
|
||||
|---|---|---|
|
||||
| 改动文件数 | 56 | 25(增量 V2 改动) |
|
||||
| 新增 CSS var | ~70 | +12(双套 24) |
|
||||
| Vitest | 71 fail / 162 pass | 71 fail / 162 pass(**与 V1 完全一致**) |
|
||||
| TS 编译 | 通过 | 通过 |
|
||||
| Playwright 截图 | 24 张 `docs/screenshots/` | 24 张 `docs/screenshots/v2/` |
|
||||
| 总耗时 | ~2.5h | ~2.5h |
|
||||
|
||||
---
|
||||
|
||||
## Sub-agent 调度回顾
|
||||
|
||||
V2 派了 4 个 sub-agent:
|
||||
|
||||
| Agent | 任务 | 文件数 | 耗时 |
|
||||
|---|---|---|---|
|
||||
| Modal 玻璃化 | LoginModal / ForceChange / VideoDetailModal.infoPanel / RecordDetailModal / AssetLibrary / Announcement / Confirm / TeamsPage.detailModal | 8 | ~2min |
|
||||
| Bar/Dropdown/Toast | AnnouncementBanner / Toast / Dropdown / Select / DatePicker / ImageLightbox | 6 | ~1min |
|
||||
| Sidebar + 生成页玻璃 | Sidebar / PromptInput / GenerationCard / InputBar(verify only) | 4 | ~1.5min |
|
||||
| Admin 实体卡 shadow | DashboardPage / TeamsPage / UsersPage / RecordsPage / AdminAssetsPage / LoginRecordsPage / AuditLogsPage / ProfilePage / SettingsPage | 9 | ~2min |
|
||||
|
||||
每个 sub-agent prompt 包含:V2 token 速查 + 改造原则 + 文件清单 + TS verify + 不要 commit。主进程统一 commit。
|
||||
|
||||
---
|
||||
|
||||
## 怎么测
|
||||
|
||||
**直接打开浏览器**:http://localhost:5173/(dev server 已在跑)
|
||||
|
||||
测试账号:
|
||||
- `admin` / `admin123`(超管)
|
||||
- `screenshot_user` / `shotpass123`(团管)
|
||||
|
||||
切换主题:Sidebar 底部头像上方月亮/太阳 SVG 按钮。
|
||||
|
||||
或直接看 24 张截图:[`docs/screenshots/v2/`](../screenshots/v2/)
|
||||
516
docs/todo/亮色主题切换V2.md
Normal file
516
docs/todo/亮色主题切换V2.md
Normal file
@ -0,0 +1,516 @@
|
||||
# 亮色主题切换 V2 — 玻璃质感重做方案
|
||||
|
||||
**起因**:V1(commit `f0f47e8`)实现了变量层切换,但浅色实际效果不达标。用户反馈:
|
||||
1. **LandingPage 没切换**(V1 我强制 `data-theme="dark"` 保留品牌 → 是错的,应当浅色化)
|
||||
2. **玻璃质感全丢**(深色态毛玻璃 → 浅色态变成"灰色色块",没有透光感)
|
||||
3. 首页毫无品牌氛围,admin 页面"平"但能用,整体不够"高级"
|
||||
|
||||
V1 没有真正复刻 GitBook / Linear / Vercel 的浅色玻璃语言 —— 只做了"颜色反相",没做"质感转译"。V2 就是补这一课。
|
||||
|
||||
---
|
||||
|
||||
## 一、参考样本深度分析
|
||||
|
||||
### 1.1 GitBook 首页(用户提供截图 1)
|
||||
|
||||
**主视觉构成(自下而上):**
|
||||
|
||||
| 层级 | 实现 | 视觉作用 |
|
||||
|---|---|---|
|
||||
| Page bg | 纯白 `#ffffff`(含极轻微暖调) | 干净画布 |
|
||||
| 装饰层 | 大块**鲜橙色 3D 漩涡环**(局部出血,从画面左下穿过) | 给玻璃面提供"穿透色源" |
|
||||
| 玻璃窗口 | 半透明白卡 + backdrop-filter blur + 1px subtle border | mock 的 Acme Help Center |
|
||||
| 内容 | 实体白卡(含 icon 状态色 / 按钮) | 信息层 |
|
||||
| Hero 文字 | 黑灰文字 + 黑色 pill 按钮 | 顶层 CTA |
|
||||
|
||||
**关键观察 — 玻璃窗口边缘**:
|
||||
|
||||
橙色漩涡明显**穿透**到窗口左上角内部(在文字内容下方依稀可见橙色被柔化的色斑)。这就是 `backdrop-filter: blur(30+px) saturate(180%)` 的标志性效果 —— 后景颜色被高斯模糊吃掉细节但保留色相,配合饱和度提升保持视觉冲击。
|
||||
|
||||
**Top banner(公告 chip)**:
|
||||
- `bg: 浅米/桃 rgba(255, 245, 235, 1) ≈ #fff5eb`
|
||||
- `border: 1px solid rgba(255, 180, 130, 0.4)` — 暖橙色边
|
||||
- `text: #1a1a1a` 深近黑
|
||||
- 圆角 9999px(pill)
|
||||
|
||||
**Top nav**:
|
||||
- 完全无背景框(白底裸链接)
|
||||
- 链接 `Geist 14-15px weight 510, color #171717`
|
||||
- "Login" 纯文字链接
|
||||
- "Get a demo" — 浅灰 pill (bg #ffffff + border `#e5e7eb` + dark text)
|
||||
- "Start for free" — **纯黑 pill** (`#000` 或 `#0a0a0a` + 白字)
|
||||
|
||||
### 1.2 Framer 玻璃 demo(用户提供 URL,已下载分析)
|
||||
|
||||
**关键画面**:
|
||||
|
||||
```
|
||||
白底 #ffffff
|
||||
|
|
||||
├── 装饰层:鲜橙色 3D 漩涡 #ff5a3c~#ff8a5c
|
||||
| (主体出血在左下,光晕扩散到中部)
|
||||
|
|
||||
├── 玻璃大卡(圆角 ~24px)
|
||||
│ ├── bg: rgba(255, 255, 255, ~0.55-0.70)
|
||||
│ ├── backdrop-filter: blur(40px) saturate(180%)
|
||||
│ ├── 顶部内描边白色 inset highlight rgba(255,255,255,0.5)
|
||||
│ ├── 外阴影 0 8px 32px rgba(0,0,0,0.06) + 0 1px 2px rgba(0,0,0,0.08)
|
||||
│ └── border: 1px solid rgba(255,255,255,0.7)
|
||||
|
|
||||
├── 内嵌实体白卡 × 4(mini squares 圆角 ~14px)
|
||||
│ ├── bg: 纯 #ffffff(不透明)
|
||||
│ ├── 1px shadow border rgba(0,0,0,0.06)
|
||||
│ └── 中心 icon 深灰 #1a1a1a~#404040
|
||||
|
|
||||
└── 玻璃 suggestion bar(圆角 ~14px)
|
||||
└── 相同玻璃配方,更窄
|
||||
```
|
||||
|
||||
**玻璃配方拆解**(这是浅色版必须复刻的核心):
|
||||
|
||||
| 属性 | 值 | 作用 |
|
||||
|---|---|---|
|
||||
| `background` | `rgba(255, 255, 255, 0.60)` | 半透明白 — **关键**,让后景穿透 |
|
||||
| `backdrop-filter` | `blur(40px) saturate(180%)` | 高斯模糊后景 + 提饱和保色相 |
|
||||
| `-webkit-backdrop-filter` | 同上 | Safari 兼容 |
|
||||
| `border` | `1px solid rgba(255, 255, 255, 0.7)` | 玻璃高光边缘 |
|
||||
| `box-shadow` | `0 8px 32px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.08)` | 深度(外阴影) |
|
||||
| `box-shadow inset` | `inset 0 1px 0 rgba(255,255,255,0.5)` | 顶边内高光 |
|
||||
| `border-radius` | `16-24px` | 流畅曲率 |
|
||||
|
||||
**没有装饰层就没有玻璃**:在 Framer 图里,**橙色漩涡是玻璃成立的前提**。如果把橙色去掉,整张图变成"白卡片堆白底",看不出玻璃。
|
||||
|
||||
### 1.3 GitBook 工作台(用户提供截图 3)
|
||||
|
||||
**功能页(非营销页)的浅色处理 — 截然不同**:
|
||||
|
||||
| 层级 | 实现 |
|
||||
|---|---|
|
||||
| Page bg | 纯白 `#ffffff` 完全无装饰 |
|
||||
| Sidebar | 极淡灰 `#fafafa~#fbfbfb`,右侧 1px border `#ebebeb`(**不是玻璃**,纯实体面) |
|
||||
| 内容卡片 | 实体白 `#ffffff` + 1px shadow border `rgba(0,0,0,0.08)` + 12px radius |
|
||||
| Active nav 项 | 浅灰药丸 `rgba(0,0,0,0.05)` + 深字 |
|
||||
| CTA 按钮 | 视情况:黑色实体 / 浅灰 ghost / 品牌色(magenta `#d946ef` "Upgrade") |
|
||||
| 文字 | `#171717` 主 / `#525252` 次 / `#888` 提示 |
|
||||
|
||||
**没有装饰、没有玻璃**。这是 Vercel/Linear-app 的"无干扰"语言。原因:功能页要看数据,不要分散注意力。
|
||||
|
||||
### 1.4 综合洞察
|
||||
|
||||
**GitBook 采用双语言系统**:
|
||||
|
||||
| 场景 | 语言 | 标志 |
|
||||
|---|---|---|
|
||||
| Marketing / Hero / Landing | **玻璃 + 装饰层** | 鲜彩漩涡、frosted card、品牌冲击 |
|
||||
| App / Functional | **平面 + 阴影边** | 纯白、Vercel-style 卡片、零装饰 |
|
||||
|
||||
**V1 的根错**:把这两种场景一视同仁地用同一套 var 覆盖。结果 LandingPage 失去了玻璃语言,admin 页又勉强能看。V2 必须分双语言处理。
|
||||
|
||||
---
|
||||
|
||||
## 二、V1 的具体技术错误
|
||||
|
||||
回顾 `index.css` `[data-theme="light"]` 的关键问题:
|
||||
|
||||
```css
|
||||
/* V1 写的(错误) */
|
||||
--color-bg-card: rgba(0, 0, 0, 0.05); /* 黑透明 → 看起来是"灰色色块",不是玻璃 */
|
||||
--color-bg-hover: rgba(0, 0, 0, 0.07); /* 同上 */
|
||||
--color-sidebar-bg: rgba(243, 244, 246, 0.92); /* 几乎不透明的浅灰 */
|
||||
--color-aurora-1: transparent; /* 极光完全关掉 */
|
||||
--color-aurora-2: transparent;
|
||||
--color-aurora-3: transparent;
|
||||
/* + LandingPage 强制 data-theme="dark" */
|
||||
```
|
||||
|
||||
**症结**:玻璃质感的三要素 ——
|
||||
1. **后景必须有色彩**(否则 blur 没东西可糊)
|
||||
2. **表面必须是 _白_ 透明而非 _黑_ 透明**(视觉上是"亮起来"不是"暗下去")
|
||||
3. **必须有 backdrop-filter blur + saturate**
|
||||
|
||||
V1 三个都没满足。
|
||||
|
||||
---
|
||||
|
||||
## 三、V2 设计原则
|
||||
|
||||
### 原则 1 — 双语言架构
|
||||
|
||||
在 `[data-theme="light"]` 内进一步区分两类 token:
|
||||
|
||||
```
|
||||
GLASS 类(透明白 + blur,用于 sidebar / modal overlay / banner / dropdown)
|
||||
↓
|
||||
SOLID 类(实体白 + 边 + 阴影,用于 admin 卡片 / 表格行 / 数据展示)
|
||||
```
|
||||
|
||||
V1 的 `--color-bg-card` 混用 → V2 拆分:
|
||||
- `--color-bg-card`: **实体白** `#ffffff`(解决 admin 卡片用例)
|
||||
- `--color-bg-glass`: **半透明白** `rgba(255,255,255,0.65)`(解决 sidebar / 横幅 / 弹窗)— **新增 var**
|
||||
|
||||
### 原则 2 — 装饰层必须保留
|
||||
|
||||
V1 的错:浅色直接 `display: none` 关掉 aurora。
|
||||
|
||||
V2:浅色保留 aurora,但用**浅色友好色板**(pastel 紫蓝粉,0.18-0.30 alpha 范围)。
|
||||
- 给 LandingPage 玻璃 modal 提供"穿透色源"
|
||||
- 给所有 backdrop-filter 表面提供视觉支撑
|
||||
- 主体内容区域(admin / 生成页)通过 z-index + bg `#fafafa` 把 aurora 挡掉,保持平面感
|
||||
- **关键**:aurora 在 light 下颜色比 dark 略淡(0.18-0.30 vs dark 的 0.35-0.60),避免刺眼
|
||||
|
||||
### 原则 3 — 主页面 bg 用 `#fafafa` 不是纯白
|
||||
|
||||
理由:
|
||||
- 玻璃 card 即使透明 `rgba(255,255,255,0.65)`,叠在纯白 bg 上视觉差异 < 2%,看不出玻璃边缘
|
||||
- 用 `#fafafa` 给玻璃卡留出"白比页面更白"的差异空间
|
||||
- 同时让实体白卡 `#ffffff` 在页面上有清晰轮廓
|
||||
- Vercel 自己用的也是 `#fafafa`(Gray 50)做 surface tinting
|
||||
|
||||
### 原则 4 — backdrop-filter 标准化为 `blur(24-32px) saturate(180%)`
|
||||
|
||||
V1 现有的 backdrop-filter 散落在 13 个 module.css 里,blur 强度从 12px 到 30px 不一。V2 标准化:
|
||||
|
||||
| 表面类别 | blur | saturate |
|
||||
|---|---|---|
|
||||
| Sidebar / 横幅 | `blur(16px)` | `saturate(160%)` |
|
||||
| Modal panel (Login / VideoDetail.infoPanel) | `blur(24px)` | `saturate(180%)` |
|
||||
| Hero / Landing 玻璃大卡 | `blur(40px)` | `saturate(180%)` |
|
||||
| Dropdown / Select 弹层 | `blur(12px)` | `saturate(140%)` |
|
||||
| Toast / Tooltip | `blur(12px)` | `saturate(140%)` |
|
||||
|
||||
### 原则 5 — 玻璃边缘必有 inset highlight
|
||||
|
||||
GitBook / Framer 的玻璃面 _上沿_ 都有微妙的白色内高光(`box-shadow: inset 0 1px 0 rgba(255,255,255,0.5)`),让边缘"亮起来",是 frosted glass 的视觉标志。V1 完全没做。V2 加。
|
||||
|
||||
---
|
||||
|
||||
## 四、V2 完整浅色色板(变更对照表)
|
||||
|
||||
### 4.1 Page / 装饰层(核心变更)
|
||||
|
||||
| Token | V1 | **V2** | 说明 |
|
||||
|---|---|---|---|
|
||||
| `--color-bg-page` | `#fafafa` | `#fafafa`(不变) | 仍是 Vercel Gray 50 |
|
||||
| `--color-aurora-1` | `transparent` | `rgba(180, 167, 255, 0.22)` | 浅紫,给玻璃穿透色 |
|
||||
| `--color-aurora-2` | `transparent` | `rgba(167, 200, 255, 0.20)` | 浅蓝青 |
|
||||
| `--color-aurora-3` | `transparent` | `rgba(255, 200, 180, 0.18)` | 浅桃(新增暖调,参考 GitBook 橙) |
|
||||
| `--color-cursor-glow` | `rgba(80,72,204,0.04)` | `rgba(80, 72, 204, 0.06)` | 微调更可见 |
|
||||
| `--color-grid-line` | `rgba(0,0,0,0.025)` | `rgba(0,0,0,0.025)` | 不变 |
|
||||
| 移除 | `[data-theme="light"] .aurora-bg { display: none }` | **删掉这条规则** | 让 aurora 在 light 下也显示 |
|
||||
|
||||
### 4.2 玻璃面 token(关键新增 + 改写)
|
||||
|
||||
| Token | V1 | **V2** | 说明 |
|
||||
|---|---|---|---|
|
||||
| `--color-bg-glass` | _不存在_ | `rgba(255, 255, 255, 0.65)` | **新增**。专给 sidebar / banner / modal glass 用 |
|
||||
| `--color-bg-glass-strong` | _不存在_ | `rgba(255, 255, 255, 0.80)` | **新增**。需要更不透明的玻璃(dropdown / tooltip) |
|
||||
| `--color-bg-modal-glass` | `rgba(255,255,255,0.92)` | `rgba(255, 255, 255, 0.85)` | 微调,留一点透气 |
|
||||
| `--color-sidebar-bg` | `rgba(243,244,246,0.92)` | `rgba(255, 255, 255, 0.65)` | 真正变成玻璃 |
|
||||
| `--color-bg-sidebar` | 同上 | 同上 | 同上 |
|
||||
| `--color-bg-input-bar` | `#ffffff` | `rgba(255, 255, 255, 0.85)` | 输入条玻璃化 |
|
||||
| `--color-bg-dropdown` | `rgba(255,255,255,0.96)` | `rgba(255, 255, 255, 0.85)` | dropdown 玻璃化 |
|
||||
| `--color-inset-highlight` | `rgba(0,0,0,0.04)` | `rgba(255, 255, 255, 0.50)` | **修正方向** —— 玻璃顶边白高光 |
|
||||
| `--color-inset-highlight-strong` | `rgba(0,0,0,0.06)` | `rgba(255, 255, 255, 0.70)` | 同上 |
|
||||
|
||||
### 4.3 实体卡片 token
|
||||
|
||||
| Token | V1 | **V2** | 说明 |
|
||||
|---|---|---|---|
|
||||
| `--color-bg-card` | `rgba(0,0,0,0.05)` | `#ffffff`(实体纯白) | admin 卡片实体化 |
|
||||
| `--color-bg-hover` | `rgba(0,0,0,0.07)` | `rgba(0, 0, 0, 0.04)` | 行 hover 仍走黑透明(行内不是玻璃) |
|
||||
| `--color-bg-upload` | `rgba(0,0,0,0.03)` | `#ffffff` | upload 区也实体化 |
|
||||
| `--color-bg-row-hover` | _未定义_ | `rgba(0, 0, 0, 0.03)` | **新增**。表格行 hover 专用 |
|
||||
|
||||
### 4.4 边框 token
|
||||
|
||||
| Token | V1 | **V2** | 说明 |
|
||||
|---|---|---|---|
|
||||
| `--color-border-card` | `rgba(0,0,0,0.10)` | `rgba(0, 0, 0, 0.08)` | 学 Vercel 阴影边 |
|
||||
| `--color-border-input-bar` | `rgba(0,0,0,0.12)` | `rgba(0, 0, 0, 0.10)` | 同上 |
|
||||
| `--color-border-modal` | `#e5e7eb` | `rgba(0, 0, 0, 0.06)` | 改为半透明,玻璃边更自然 |
|
||||
| `--color-border-modal-soft` | `rgba(0,0,0,0.08)` | `rgba(0, 0, 0, 0.05)` | 微弱 |
|
||||
| `--color-border-glass-edge` | _不存在_ | `rgba(255, 255, 255, 0.70)` | **新增**。玻璃面外边白高光 |
|
||||
| `--color-border-soft` | `rgba(0,0,0,0.06)` | `rgba(0, 0, 0, 0.05)` | 微调 |
|
||||
| `--color-border-row` | `rgba(0,0,0,0.05)` | `rgba(0, 0, 0, 0.06)` | 行分割线略明显 |
|
||||
|
||||
### 4.5 阴影 token(玻璃深度感)
|
||||
|
||||
| Token | V1 | **V2** | 说明 |
|
||||
|---|---|---|---|
|
||||
| `--color-shadow-modal` | `rgba(0,0,0,0.10)` | `rgba(0, 0, 0, 0.08)` | 玻璃外阴影更柔 |
|
||||
| `--color-shadow-dropdown` | `rgba(0,0,0,0.08)` | `rgba(0, 0, 0, 0.10)` | dropdown 阴影 |
|
||||
| `--shadow-card-light` | _不存在_ | `0 1px 2px rgba(0,0,0,0.04), 0 4px 16px rgba(0,0,0,0.06)` | **新增**。Vercel-style multi-layer 阴影。给实体卡 |
|
||||
| `--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.5)` | **新增**。给玻璃面(含 inset highlight) |
|
||||
|
||||
### 4.6 文字 token
|
||||
|
||||
| Token | V1 | **V2** | 说明 |
|
||||
|---|---|---|---|
|
||||
| `--color-text-primary` | `#171823` | `#171717`(Vercel Black) | 去紫调,纯近黑更"高级" |
|
||||
| `--color-text-secondary` | `#6b6e85` | `#525252`(Gray 600) | Vercel 灰阶 |
|
||||
| `--color-text-tertiary` | `#9ca3af` | `#888` (Gray 500) | 同上 |
|
||||
| `--color-text-quaternary` | `#cbd5e1` | `#a3a3a3` (Gray 400) | 同上 |
|
||||
| `--color-text-disabled` | `#cbd5e1` | `#a3a3a3` | 同上 |
|
||||
| `--color-text-on-glass` | `rgba(0,0,0,0.75)` | `rgba(23, 23, 23, 0.85)` | 玻璃上的字偏纯黑 |
|
||||
|
||||
### 4.7 状态色(V2 不变,V1 加深 18% 仍然合理)
|
||||
|
||||
主色 `#5048cc` / 信息 `#0099cc` / 成功 `#00a37e` / 危险 `#d63a2a` / 警告 `#d4860a` —— 保持 V1。
|
||||
|
||||
### 4.8 装饰 chip(参考 GitBook 公告 chip)
|
||||
|
||||
新增一组 var 给"暖调强调"用(公告横幅 / Trial banner / "新版上线" pill):
|
||||
|
||||
| Token | **V2 新值** | 用途 |
|
||||
|---|---|---|
|
||||
| `--color-chip-warm-bg` | `#fff5eb` | 暖米色 chip 背景 |
|
||||
| `--color-chip-warm-border` | `rgba(255, 180, 130, 0.40)` | 暖橙色边 |
|
||||
| `--color-chip-warm-text` | `#1a1a1a` | chip 黑字 |
|
||||
|
||||
(深色态下:`bg: rgba(255, 200, 130, 0.10)`, `border: rgba(255, 200, 130, 0.25)`, `text: #f1f0ff`)
|
||||
|
||||
---
|
||||
|
||||
## 五、LandingPage 浅色化重做
|
||||
|
||||
### 5.1 移除 `data-theme="dark"` 强制
|
||||
|
||||
```diff
|
||||
// web/src/pages/LandingPage.tsx
|
||||
- <div className={styles.page} data-theme="dark">
|
||||
+ <div className={styles.page}>
|
||||
```
|
||||
|
||||
让 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 同语言,不再是"色块版深色取反"
|
||||
@ -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);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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<HTMLCanvasElement>(null);
|
||||
const grainRef = useRef<HTMLCanvasElement>(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, 浅色下用白色 */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
zIndex: 1,
|
||||
background: 'radial-gradient(ellipse 65% 55% at 50% 50%, transparent 0%, rgba(0,0,0,0.8) 100%)',
|
||||
background: `radial-gradient(ellipse 65% 55% at 50% 50%, transparent 0%, ${vignetteColor} 100%)`,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Layer 2: Film grain */}
|
||||
{/* Layer 2: Film grain — 浅色下大幅减弱避免噪点过曝 */}
|
||||
<canvas
|
||||
ref={grainRef}
|
||||
style={{
|
||||
@ -216,13 +230,13 @@ export function AuroraCanvas() {
|
||||
zIndex: 2,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
opacity: 0.035,
|
||||
opacity: isLight ? 0.015 : 0.035,
|
||||
pointerEvents: 'none',
|
||||
mixBlendMode: 'overlay',
|
||||
mixBlendMode: isLight ? 'multiply' : 'overlay',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Layer 3: Aurora — blur merges orbs into organic glow */}
|
||||
{/* Layer 3: Aurora — blur 让 orb 融成有机晕染 */}
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{
|
||||
@ -234,7 +248,7 @@ export function AuroraCanvas() {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Layer 4: Top/bottom gradient mask */}
|
||||
{/* Layer 4: 顶/底渐变压角 */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
@ -242,7 +256,7 @@ export function AuroraCanvas() {
|
||||
zIndex: 4,
|
||||
pointerEvents: 'none',
|
||||
background:
|
||||
'linear-gradient(to bottom, rgba(0,0,0,0.5) 0%, transparent 18%, transparent 82%, rgba(0,0,0,0.5) 100%)',
|
||||
`linear-gradient(to bottom, ${fadeColor} 0%, transparent 18%, transparent 82%, ${fadeColor} 100%)`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -111,10 +111,9 @@ export function LandingPage({ autoLogin }: Props) {
|
||||
}, [playing]);
|
||||
|
||||
return (
|
||||
// 强制深色:LandingPage 是品牌专属 Air Spark 体验页,
|
||||
// 黑底 + 极光 + 薄荷绿是核心调性,浅色化会破坏品牌识别。
|
||||
// 整个登录流程(含 LoginModal / ForceChangePasswordModal)都继承这个 dark 子树。
|
||||
<div className={styles.page} data-theme="dark">
|
||||
// V2: 跟随全局主题切换。LandingPage 浅色化 = AuroraCanvas + LoginModal 都跟随。
|
||||
// 薄荷绿在浅色下加深为 teal (#0d9488),保证对比度。
|
||||
<div className={styles.page}>
|
||||
{/* Layer 1-4: Aurora background */}
|
||||
<AuroraCanvas />
|
||||
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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; }
|
||||
|
||||
141
web/test/theme-screenshots-v2.mjs
Normal file
141
web/test/theme-screenshots-v2.mjs
Normal file
@ -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/<page>__<theme>.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);
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user