From 12ce7f7b4654a24c7e2d15365261bdc619dec72d Mon Sep 17 00:00:00 2001 From: WJH <3506456886@qq.com> Date: Tue, 23 Jun 2026 20:30:04 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=203=20=E5=A5=97?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E5=88=87=E6=8D=A2=E7=B3=BB=E7=BB=9F=E6=8F=90?= =?UTF-8?q?=E5=8D=87=E5=AD=A6=E4=B9=A0=E6=95=88=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将单一黑金主题升级为 4 选项主题系统: - Obsidian(默认·黑金奢华·夜间) - Parchment(米黄纸·长时间阅读) - Daylight(清亮白·强光环境) - System(跟随 prefers-color-scheme) 技术方案:CSS 变量 + data-theme 属性切换,偏好持久化到 settingsStore,main.tsx 同步初始化防 FOUC,App.tsx 监听 系统主题变化实时跟随。全站(含着陆页)内联硬编码颜色 统一替换为 var(--token)。 SDD 产物: - docs/superpowers/specs/2026-06-23-theme-system-design.md - docs/superpowers/plans/2026-06-23-theme-system-implementation.md Closes #4 Co-authored-by: traeagent --- .../2026-06-23-theme-system-implementation.md | 82 ++++++ .../specs/2026-06-23-theme-system-design.md | 155 ++++++++++++ src/App.tsx | 26 ++ src/components/AppNav.tsx | 4 + src/components/Hero.tsx | 4 +- src/components/Nav.tsx | 2 + src/components/ThemeSwitcher.tsx | 34 +++ src/index.css | 234 +++++++++++++----- src/main.tsx | 23 ++ src/pages/LandingPage.tsx | 6 +- src/pages/PlansPage.tsx | 10 +- src/pages/PracticePage.tsx | 14 +- src/pages/ResourcesPage.tsx | 16 +- src/pages/SettingsPage.tsx | 22 +- src/pages/StudyPage.tsx | 4 +- src/store/settingsStore.ts | 44 +++- src/styles/app.css | 199 +++++++++------ tailwind.config.js | 22 +- 18 files changed, 710 insertions(+), 191 deletions(-) create mode 100644 docs/superpowers/plans/2026-06-23-theme-system-implementation.md create mode 100644 docs/superpowers/specs/2026-06-23-theme-system-design.md create mode 100644 src/components/ThemeSwitcher.tsx diff --git a/docs/superpowers/plans/2026-06-23-theme-system-implementation.md b/docs/superpowers/plans/2026-06-23-theme-system-implementation.md new file mode 100644 index 0000000..82fff34 --- /dev/null +++ b/docs/superpowers/plans/2026-06-23-theme-system-implementation.md @@ -0,0 +1,82 @@ +# 主题系统实现计划(Theme System Implementation Plan) + +**日期**:2026-06-23 +**关联 Spec**:`docs/superpowers/specs/2026-06-23-theme-system-design.md` + +## 实施顺序 + +### Phase 1:CSS 变量层(基础设施) + +**文件**:`src/index.css` + +1. 在 `@layer base` 之前新增 `:root`、`[data-theme="parchment"]`、`[data-theme="daylight"]` 三个变量定义块,覆盖 spec 第 4 节全部 token。 +2. 将 `body`、`::selection`、所有 `@layer components` 内的硬编码颜色替换为 `var(--token)`。 +3. 噪点 overlay 的 `opacity` 改为 `var(--grain-opacity)`。 + +**文件**:`src/styles/app.css` + +4. 将所有 `#0a0a0f`、`#12121a`、`#1a1a26`、`#d4a853`、`#e8c878`、`#8a6d2b`、`#2dd4bf`、`#f0ece4`、`#c9c4ba`、`#9a958c`、`#5c584f`、`rgba(212, 168, 83, ...)`、`rgba(45, 212, 191, ...)` 替换为对应 `var(--token)`。 + +### Phase 2:Tailwind 配置 + +**文件**:`tailwind.config.js` + +5. `colors` 字段值改为 `'var(--bg-deep)'` 等字符串引用(保持键名不变,向后兼容)。 + +### Phase 3:状态层 + +**文件**:`src/store/settingsStore.ts` + +6. 新增 `ThemeId` 类型、`theme` / `resolvedTheme` 状态、`setTheme` action。 +7. `persist` 配置不变(`name: 'kf-settings'`),新增字段自动持久化。 +8. 新增 `resolveSystemTheme()` 工具函数:`window.matchMedia('(prefers-color-scheme: dark)').matches ? 'obsidian' : 'daylight'`。 + +### Phase 4:主题应用 + +**文件**:`src/main.tsx` + +9. 在 `createRoot().render()` 之前插入同步初始化脚本:从 `localStorage.getItem('kf-settings')` 解析 `theme`,若为 `'system'` 或不存在则调用 `resolveSystemTheme()`,将结果写入 `document.documentElement.dataset.theme`。 + +**文件**:`src/App.tsx` + +10. 新增 `useThemeEffect()` hook:订阅 `useSettingsStore` 的 `theme`,在 `useEffect` 中计算 `resolvedTheme` 并写 `data-theme`;同时监听 `matchMedia('(prefers-color-scheme: dark)')` 的 `change` 事件,当 `theme === 'system'` 时重新解析。 + +### Phase 5:切换器组件 + +**文件**:`src/components/ThemeSwitcher.tsx`(新建) + +11. 实现 4 选项 segmented control:系统 / Obsidian / Parchment / Daylight。 +12. 样式使用现有 `.btn-ghost` 派生,紧凑型布局,适配导航栏。 +13. 当前选中项高亮(`.active` 态)。 + +### Phase 6:导航栏集成 + +**文件**:`src/components/AppNav.tsx`、`src/components/Nav.tsx` + +14. 在两个导航栏右侧加入 ``。 +15. 调整 `nav-bar` / `app-nav` 的 flex 布局,确保切换器与现有链接对齐。 + +### Phase 7:内联样式清理 + +**文件**:`src/pages/SettingsPage.tsx` 及其他含内联颜色的页面 + +16. 将 `style={{ color: '#f0ece4' }}` 等内联硬编码替换为 `var(--text-primary)` 等。 +17. 扫描 `src/pages/*.tsx` 与 `src/components/*.tsx` 中的 `#` 开头颜色字面量,统一替换。 + +### Phase 8:验证 + +18. `npm run build` 通过。 +19. `npm run quality:precommit` 通过(若可用)。 +20. 启动 dev server 人工验证三套主题切换、刷新持久化、系统跟随。 + +## 风险与回滚 + +- **风险**:CSS 变量替换遗漏导致某元素颜色异常。 +- **缓解**:Obsidian 主题的 token 值与原硬编码完全一致,替换后视觉应像素级不变;若有差异即为遗漏点。 +- **回滚**:所有改动集中在 CSS 变量层,回滚只需还原 `index.css` / `app.css` 两个文件即可恢复原状。 + +## 不在本次范围 + +- 不修改 `animation` / `keyframes`。 +- 不调整字体(Playfair Display + DM Sans 三套主题共用)。 +- 不引入新的依赖包。 diff --git a/docs/superpowers/specs/2026-06-23-theme-system-design.md b/docs/superpowers/specs/2026-06-23-theme-system-design.md new file mode 100644 index 0000000..44f8164 --- /dev/null +++ b/docs/superpowers/specs/2026-06-23-theme-system-design.md @@ -0,0 +1,155 @@ +# 主题系统设计(Theme System Design) + +**日期**:2026-06-23 +**状态**:Draft +**类型**:Design System Architecture Adjustment + +## 1. 背景与动机 + +KnowledgeFlow 当前采用单一"黑金奢华编辑风"主题:深黑底 `#0a0a0f` + 金色 `#d4a853` + Playfair Display。所有颜色硬编码于 `src/index.css` 与 `src/styles/app.css`,无 CSS 变量、无主题切换能力。 + +**学习效率视角的三个问题**: + +1. **长时间阅读疲劳**:学习场景下用户会停留 30 分钟以上盯着 Markdown 正文(StudyPage chat-content),高对比黑金适合品牌着陆页的"惊艳感",但加速视疲劳。 +2. **环境光不适配**:学习者白天明亮教室与晚上昏暗宿舍共用同一界面。 +3. **缺乏个性化归属**:无法调整主题会削弱"这是我的学习空间"的持续使用意愿。 + +## 2. 目标 + +- 提供 3 套精心设计的预设主题,覆盖夜间/阅读/白天三场景。 +- 全站支持主题切换(含着陆页)。 +- 首次访问跟随系统 `prefers-color-scheme`,用户手动切换后记住偏好。 +- 保留黑金作为默认品牌主题,不稀释品牌识别。 + +## 3. 非目标 + +- 不做"调色板自由切换"(红绿蓝紫),避免稀释品牌与增加心智负担。 +- 不做用户自定义颜色导入/导出。 +- 不引入 CSS-in-JS 运行时主题引擎,保持纯 CSS 变量方案。 + +## 4. 主题定义 + +### 4.1 Obsidian(默认·现状保留) + +夜间/品牌展示场景。完全保留当前黑金配色。 + +| Token | 值 | 用途 | +|-------|-----|------| +| `--bg-deep` | `#0a0a0f` | 页面底色 | +| `--bg-surface` | `#12121a` | 卡片底 | +| `--bg-elevated` | `#1a1a26` | 悬浮元素 | +| `--bg-input` | `#0a0a0f` | 输入框底 | +| `--bg-code` | `#08080d` | 代码块底 | +| `--gold-primary` | `#d4a853` | 主强调 | +| `--gold-light` | `#e8c878` | 次强调 | +| `--gold-dim` | `#8a6d2b` | 弱化强调 | +| `--teal-accent` | `#2dd4bf` | 用户消息/已掌握 | +| `--teal-deep` | `#0d9488` | — | +| `--text-primary` | `#f0ece4` | 正文 | +| `--text-body` | `#c9c4ba` | Markdown 正文 | +| `--text-secondary` | `#9a958c` | 次要文字 | +| `--text-muted` | `#5c584f` | 弱化文字 | +| `--border-gold` | `rgba(212, 168, 83, 0.12)` | 金色描边 | +| `--border-gold-strong` | `rgba(212, 168, 83, 0.25)` | 强描边 | +| `--grain-opacity` | `0.04` | 噪点强度 | + +### 4.2 Parchment(米黄纸·阅读优化) + +长时间阅读/白天柔和场景。模拟旧书纸质感。 + +| Token | 值 | +|-------|-----| +| `--bg-deep` | `#f5f0e6` | +| `--bg-surface` | `#ede5d3` | +| `--bg-elevated` | `#e3d9c2` | +| `--bg-input` | `#fbf7ee` | +| `--bg-code` | `#e8dfc9` | +| `--gold-primary` | `#8a5a1f` | +| `--gold-light` | `#a87328` | +| `--gold-dim` | `#6b4415` | +| `--teal-accent` | `#0d7d72` | +| `--teal-deep` | `#0a5e56` | +| `--text-primary` | `#3d2f1f` | +| `--text-body` | `#5a4a35` | +| `--text-secondary` | `#7a6a55` | +| `--text-muted` | `#9a8a75` | +| `--border-gold` | `rgba(138, 90, 31, 0.18)` | +| `--border-gold-strong` | `rgba(138, 90, 31, 0.35)` | +| `--grain-opacity` | `0.06` | + +### 4.3 Daylight(清亮白·强光环境) + +强光环境/偏好浅色场景。保留金色作品牌锚点。 + +| Token | 值 | +|-------|-----| +| `--bg-deep` | `#fafafa` | +| `--bg-surface` | `#ffffff` | +| `--bg-elevated` | `#f4f4f5` | +| `--bg-input` | `#ffffff` | +| `--bg-code` | `#f4f4f5` | +| `--gold-primary` | `#a87328` | +| `--gold-light` | `#c89548` | +| `--gold-dim` | `#8a5a1f` | +| `--teal-accent` | `#0d9488` | +| `--teal-deep` | `#0a766c` | +| `--text-primary` | `#1a1a1a` | +| `--text-body` | `#3a3a3a` | +| `--text-secondary` | `#6a6a6a` | +| `--text-muted` | `#9a9a9a` | +| `--border-gold` | `rgba(168, 115, 40, 0.18)` | +| `--border-gold-strong` | `rgba(168, 115, 40, 0.35)` | +| `--grain-opacity` | `0.03` | + +## 5. 技术方案 + +### 5.1 CSS 变量架构 + +在 `src/index.css` 顶部定义 `:root`(默认 Obsidian)与 `[data-theme="parchment"]`、`[data-theme="daylight"]` 三个选择器块,每个块覆盖全部 token 变量。 + +所有现有硬编码颜色(`#0a0a0f`、`#d4a853`、`rgba(212, 168, 83, 0.12)` 等)替换为 `var(--token-name)`。 + +### 5.2 Tailwind 集成 + +`tailwind.config.js` 的 `colors` 字段从硬编码值改为 `rgb(var(--token-rgb) / )` 形式,或直接引用 CSS 变量字符串。考虑到当前 Tailwind 类使用较少(项目以自定义 CSS 类为主),采用直接字符串引用 `'var(--bg-deep)'` 即可。 + +### 5.3 状态管理 + +`settingsStore` 增加: + +```ts +type ThemeId = 'obsidian' | 'parchment' | 'daylight' | 'system' +theme: ThemeId // 'system' 表示跟随系统 +resolvedTheme: ThemeId // 实际解析后的主题(不含 'system') +setTheme: (theme: ThemeId) => void +``` + +`'system'` 在 store 内部解析为 `obsidian` 或 `daylight`(基于 `prefers-color-scheme`)。持久化 `theme` 字段。 + +### 5.4 主题应用 + +在 `App.tsx` 中通过 `useEffect` 监听 `theme` 变化,将 `data-theme` 属性写到 `document.documentElement`。同时监听 `prefers-color-scheme` 变化(当 `theme === 'system'` 时重新解析)。 + +为避免首屏闪烁(FOUC),在 `main.tsx` 中插入一段同步脚本,在 React 渲染前从 localStorage 读取 `kf-settings` 并设置 `data-theme`。 + +### 5.5 切换器 UI + +`ThemeSwitcher` 组件:3 段式 segmented control(Obsidian / Parchment / Daylight),含一个"跟随系统"选项。放置于: +- `AppNav`(应用内导航栏,右侧) +- `Nav`(着陆页导航栏,右侧) + +样式遵循现有 `.btn-ghost` 风格,使用图标 + 文字。 + +## 6. 兼容性与迁移 + +- 现有所有页面无需改动业务逻辑,仅样式 token 替换。 +- `SettingsPage` 等内联 `style={{ color: '#f0ece4' }}` 需替换为 `var(--text-primary)`。 +- `tailwind.config.js` 中 `animation` / `keyframes` 不变。 + +## 7. 验收标准 + +1. 三套主题切换时无视觉断裂,所有页面颜色一致变化。 +2. 刷新页面后主题保持,无 FOUC。 +3. 首次访问(无 localStorage)时,深色系统→Obsidian,浅色系统→Daylight。 +4. `prefers-color-scheme` 变化时(且 `theme === 'system'`),主题实时跟随。 +5. 现有黑金视觉与切换前像素级一致(Obsidian 主题)。 diff --git a/src/App.tsx b/src/App.tsx index e605494..863828b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,3 +1,4 @@ +import { useEffect } from 'react' import { HashRouter, Routes, Route } from 'react-router-dom' import LandingPage from './pages/LandingPage' import StudyPage from './pages/StudyPage' @@ -5,8 +6,33 @@ import PlansPage from './pages/PlansPage' import PracticePage from './pages/PracticePage' import ResourcesPage from './pages/ResourcesPage' import SettingsPage from './pages/SettingsPage' +import { useSettingsStore, resolveTheme } from './store/settingsStore' + +/** + * 主题副作用 hook: + * - 订阅 store 中的 theme(含 'system') + * - 将解析后的 ResolvedTheme 写入 + * - 当 theme === 'system' 时,监听 prefers-color-scheme 变化实时跟随 + */ +function useThemeEffect() { + const theme = useSettingsStore((s) => s.theme) + + useEffect(() => { + const root = document.documentElement + const apply = () => { + root.setAttribute('data-theme', resolveTheme(theme)) + } + apply() + + if (theme !== 'system') return + const mql = window.matchMedia('(prefers-color-scheme: dark)') + mql.addEventListener('change', apply) + return () => mql.removeEventListener('change', apply) + }, [theme]) +} export default function App() { + useThemeEffect() return ( diff --git a/src/components/AppNav.tsx b/src/components/AppNav.tsx index 416138e..835607e 100644 --- a/src/components/AppNav.tsx +++ b/src/components/AppNav.tsx @@ -1,5 +1,6 @@ import { NavLink } from 'react-router-dom' import { useSettingsStore } from '../store/settingsStore' +import ThemeSwitcher from './ThemeSwitcher' const NAV_ITEMS = [ { to: '/study', label: '智能学习' }, @@ -36,6 +37,9 @@ export default function AppNav() { 演示模式 )} +
  • + +
  • ) diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index bd3c9fe..bd69123 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -31,7 +31,7 @@ export default function Hero({ onStart, onLearnMore }: { onStart?: () => void; o style={{ inset: '40px', animation: 'ringRotate 25s linear infinite reverse', - borderColor: 'rgba(45, 212, 191, 0.1)', + borderColor: 'var(--teal-border)', }} >
    @@ -45,7 +45,7 @@ export default function Hero({ onStart, onLearnMore }: { onStart?: () => void; o style={{ inset: '120px', animation: 'ringRotate 35s linear infinite reverse', - borderColor: 'rgba(45, 212, 191, 0.08)', + borderColor: 'var(--teal-border)', }} />
    diff --git a/src/components/Nav.tsx b/src/components/Nav.tsx index b43bf25..69f6d1a 100644 --- a/src/components/Nav.tsx +++ b/src/components/Nav.tsx @@ -1,4 +1,5 @@ import { Link } from 'react-router-dom' +import ThemeSwitcher from './ThemeSwitcher' export default function Nav() { return ( @@ -13,6 +14,7 @@ export default function Nav() {
  • 练习
  • 资源
  • 设置
  • +
  • ) diff --git a/src/components/ThemeSwitcher.tsx b/src/components/ThemeSwitcher.tsx new file mode 100644 index 0000000..d8a77a6 --- /dev/null +++ b/src/components/ThemeSwitcher.tsx @@ -0,0 +1,34 @@ +import { THEME_OPTIONS, useSettingsStore, type ThemeId } from '../store/settingsStore' + +/** + * 主题切换器 — 4 段式 segmented control + * 系统 / 黑曜 / 羊皮纸 / 日光 + */ +export default function ThemeSwitcher() { + const theme = useSettingsStore((s) => s.theme) + const setTheme = useSettingsStore((s) => s.setTheme) + + return ( +
    + {THEME_OPTIONS.map((opt) => ( + + ))} +
    + ) +} diff --git a/src/index.css b/src/index.css index 526670c..ee60035 100644 --- a/src/index.css +++ b/src/index.css @@ -8,6 +8,112 @@ Playfair Display + DM Sans · Ambient depth ═══════════════════════════════════════════════════ */ +/* ─── THEME TOKENS ─── + Obsidian (default) — 黑金奢华,夜间/品牌展示 */ +:root { + --bg-deep: #0a0a0f; + --bg-surface: #12121a; + --bg-elevated: #1a1a26; + --bg-input: #0a0a0f; + --bg-code: #08080d; + --gold-primary: #d4a853; + --gold-light: #e8c878; + --gold-dim: #8a6d2b; + --teal-accent: #2dd4bf; + --teal-deep: #0d9488; + --text-primary: #f0ece4; + --text-body: #c9c4ba; + --text-secondary: #9a958c; + --text-muted: #5c584f; + --border-gold: rgba(212, 168, 83, 0.12); + --border-gold-strong: rgba(212, 168, 83, 0.25); + --border-gold-hover: rgba(212, 168, 83, 0.35); + --gold-glow: rgba(212, 168, 83, 0.06); + --gold-glow-strong: rgba(212, 168, 83, 0.1); + --teal-glow: rgba(45, 212, 191, 0.12); + --teal-border: rgba(45, 212, 191, 0.25); + --warn-color: #f59e0b; + --warn-border: rgba(245, 158, 11, 0.2); + --error-color: #f87171; + --error-border: rgba(239, 68, 68, 0.3); + --grain-opacity: 0.04; + --nav-bg: rgba(10, 10, 15, 0.85); + --chat-input-bg: rgba(10, 10, 15, 0.9); + --user-bubble-bg: rgba(45, 212, 191, 0.04); + --user-bubble-border: rgba(45, 212, 191, 0.12); + color-scheme: dark; +} + +/* Parchment — 米黄纸,长时间阅读/白天柔和 */ +[data-theme="parchment"] { + --bg-deep: #f5f0e6; + --bg-surface: #ede5d3; + --bg-elevated: #e3d9c2; + --bg-input: #fbf7ee; + --bg-code: #e8dfc9; + --gold-primary: #8a5a1f; + --gold-light: #a87328; + --gold-dim: #6b4415; + --teal-accent: #0d7d72; + --teal-deep: #0a5e56; + --text-primary: #3d2f1f; + --text-body: #5a4a35; + --text-secondary: #7a6a55; + --text-muted: #9a8a75; + --border-gold: rgba(138, 90, 31, 0.18); + --border-gold-strong: rgba(138, 90, 31, 0.35); + --border-gold-hover: rgba(138, 90, 31, 0.45); + --gold-glow: rgba(138, 90, 31, 0.06); + --gold-glow-strong: rgba(138, 90, 31, 0.1); + --teal-glow: rgba(13, 125, 114, 0.12); + --teal-border: rgba(13, 125, 114, 0.3); + --warn-color: #b45309; + --warn-border: rgba(180, 83, 9, 0.25); + --error-color: #b91c1c; + --error-border: rgba(185, 28, 28, 0.3); + --grain-opacity: 0.06; + --nav-bg: rgba(245, 240, 230, 0.85); + --chat-input-bg: rgba(245, 240, 230, 0.9); + --user-bubble-bg: rgba(13, 125, 114, 0.06); + --user-bubble-border: rgba(13, 125, 114, 0.18); + color-scheme: light; +} + +/* Daylight — 清亮白,强光环境/偏好浅色 */ +[data-theme="daylight"] { + --bg-deep: #fafafa; + --bg-surface: #ffffff; + --bg-elevated: #f4f4f5; + --bg-input: #ffffff; + --bg-code: #f4f4f5; + --gold-primary: #a87328; + --gold-light: #c89548; + --gold-dim: #8a5a1f; + --teal-accent: #0d9488; + --teal-deep: #0a766c; + --text-primary: #1a1a1a; + --text-body: #3a3a3a; + --text-secondary: #6a6a6a; + --text-muted: #9a9a9a; + --border-gold: rgba(168, 115, 40, 0.18); + --border-gold-strong: rgba(168, 115, 40, 0.35); + --border-gold-hover: rgba(168, 115, 40, 0.45); + --gold-glow: rgba(168, 115, 40, 0.06); + --gold-glow-strong: rgba(168, 115, 40, 0.1); + --teal-glow: rgba(13, 148, 136, 0.12); + --teal-border: rgba(13, 148, 136, 0.3); + --warn-color: #b45309; + --warn-border: rgba(180, 83, 9, 0.25); + --error-color: #b91c1c; + --error-border: rgba(185, 28, 28, 0.3); + --grain-opacity: 0.03; + --nav-bg: rgba(250, 250, 250, 0.85); + --chat-input-bg: rgba(250, 250, 250, 0.9); + --user-bubble-bg: rgba(13, 148, 136, 0.06); + --user-bubble-border: rgba(13, 148, 136, 0.18); + color-scheme: light; +} + @layer base { * { margin: 0; @@ -21,12 +127,13 @@ body { font-family: 'DM Sans', 'Noto Serif SC', sans-serif; - background: #0a0a0f; - color: #f0ece4; + background: var(--bg-deep); + color: var(--text-primary); min-height: 100vh; overflow-x: hidden; position: relative; -webkit-font-smoothing: antialiased; + transition: background 0.4s ease, color 0.4s ease; } /* Grain texture overlay */ @@ -37,6 +144,7 @@ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E"); pointer-events: none; z-index: 9999; + opacity: var(--grain-opacity); } section { @@ -45,7 +153,7 @@ ::selection { background: rgba(212, 168, 83, 0.3); - color: #f0ece4; + color: var(--text-primary); } } @@ -65,8 +173,8 @@ align-items: center; gap: 10px; padding: 16px 36px; - background: #d4a853; - color: #0a0a0f; + background: var(--gold-primary); + color: var(--bg-deep); text-decoration: none; border-radius: 999px; font-weight: 700; @@ -102,19 +210,19 @@ gap: 8px; padding: 16px 28px; background: transparent; - color: #9a958c; + color: var(--text-secondary); text-decoration: none; border-radius: 999px; font-weight: 500; font-size: 0.95rem; transition: all 0.3s ease; cursor: pointer; - border: 1px solid rgba(212, 168, 83, 0.12); + border: 1px solid var(--border-gold); } .btn-ghost:hover { - border-color: rgba(212, 168, 83, 0.35); - color: #e8c878; + border-color: var(--border-gold-hover); + color: var(--gold-light); } /* ─── TYPOGRAPHY ─── */ @@ -123,7 +231,7 @@ font-weight: 500; letter-spacing: 0.25em; text-transform: uppercase; - color: #8a6d2b; + color: var(--gold-dim); margin-bottom: 16px; } @@ -133,14 +241,14 @@ font-weight: 700; line-height: 1.15; letter-spacing: -0.02em; - color: #f0ece4; + color: var(--text-primary); margin-bottom: 20px; } .section-desc { font-size: 1.05rem; line-height: 1.7; - color: #9a958c; + color: var(--text-secondary); max-width: 500px; } @@ -150,7 +258,7 @@ align-items: center; justify-content: space-between; padding: 28px 0; - border-bottom: 1px solid rgba(212, 168, 83, 0.12); + border-bottom: 1px solid var(--border-gold); } .nav-logo { @@ -163,7 +271,7 @@ .nav-logo-mark { width: 42px; height: 42px; - border: 2px solid #d4a853; + border: 2px solid var(--gold-primary); border-radius: 12px; display: flex; align-items: center; @@ -177,7 +285,7 @@ position: absolute; width: 20px; height: 20px; - background: #d4a853; + background: var(--gold-primary); border-radius: 50%; opacity: 0.3; animation: logoPulse 3s ease-in-out infinite; @@ -187,7 +295,7 @@ font-family: 'Playfair Display', 'Noto Serif SC', serif; font-size: 1.3rem; font-weight: 700; - color: #f0ece4; + color: var(--text-primary); letter-spacing: -0.02em; } @@ -198,7 +306,7 @@ } .nav-link { - color: #9a958c; + color: var(--text-secondary); text-decoration: none; font-size: 0.88rem; font-weight: 500; @@ -209,7 +317,7 @@ } .nav-link:hover { - color: #e8c878; + color: var(--gold-light); } .nav-link::after { @@ -218,7 +326,7 @@ bottom: -4px; left: 0; height: 1px; - background: #d4a853; + background: var(--gold-primary); transition: width 0.3s ease; width: 0; } @@ -238,7 +346,7 @@ font-weight: 500; letter-spacing: 0.2em; text-transform: uppercase; - color: #d4a853; + color: var(--gold-primary); margin-bottom: 28px; display: flex; align-items: center; @@ -249,7 +357,7 @@ content: ''; width: 40px; height: 1px; - background: #d4a853; + background: var(--gold-primary); display: inline-block; } @@ -258,7 +366,7 @@ font-weight: 900; line-height: 1.05; letter-spacing: -0.03em; - color: #f0ece4; + color: var(--text-primary); margin-bottom: 32px; max-width: 800px; font-size: clamp(3rem, 7vw, 5.5rem); @@ -266,7 +374,7 @@ .hero-title em { font-style: italic; - color: #d4a853; + color: var(--gold-primary); position: relative; } @@ -277,14 +385,14 @@ left: 0; right: 0; height: 3px; - background: #d4a853; + background: var(--gold-primary); opacity: 0.4; } .hero-desc { font-size: 1.15rem; line-height: 1.7; - color: #9a958c; + color: var(--text-secondary); max-width: 520px; margin-bottom: 48px; } @@ -307,14 +415,14 @@ .hero-ring { position: absolute; border-radius: 50%; - border: 1px solid rgba(212, 168, 83, 0.12); + border: 1px solid var(--border-gold); } .hero-ring-dot { position: absolute; width: 8px; height: 8px; - background: #d4a853; + background: var(--gold-primary); border-radius: 50%; top: 50%; left: -4px; @@ -324,8 +432,8 @@ .stats-strip { display: grid; grid-template-columns: repeat(4, 1fr); - border-top: 1px solid rgba(212, 168, 83, 0.12); - border-bottom: 1px solid rgba(212, 168, 83, 0.12); + border-top: 1px solid var(--border-gold); + border-bottom: 1px solid var(--border-gold); } .stat-item { @@ -341,14 +449,14 @@ top: 20%; height: 60%; width: 1px; - background: rgba(212, 168, 83, 0.12); + background: var(--border-gold); } .stat-value { font-family: 'Playfair Display', 'Noto Serif SC', serif; font-size: 2.8rem; font-weight: 700; - color: #d4a853; + color: var(--gold-primary); line-height: 1; margin-bottom: 8px; } @@ -357,7 +465,7 @@ font-size: 0.82rem; letter-spacing: 0.1em; text-transform: uppercase; - color: #5c584f; + color: var(--text-muted); } /* ─── FEATURES ─── */ @@ -376,7 +484,7 @@ } .feature-card { - background: #12121a; + background: var(--bg-surface); padding: 48px 36px; position: relative; overflow: hidden; @@ -393,7 +501,7 @@ height: 100%; opacity: 0; transition: opacity 0.5s ease; - background: linear-gradient(135deg, rgba(212, 168, 83, 0.06), transparent 60%); + background: linear-gradient(135deg, var(--gold-glow), transparent 60%); } .feature-card:hover::before { @@ -408,20 +516,20 @@ font-family: 'Playfair Display', 'Noto Serif SC', serif; font-size: 4rem; font-weight: 900; - color: #1a1a26; + color: var(--bg-elevated); line-height: 1; margin-bottom: 24px; transition: color 0.5s ease; } .feature-card:hover .feature-num { - color: rgba(212, 168, 83, 0.15); + color: var(--gold-glow-strong); } .feature-line { width: 32px; height: 2px; - background: #d4a853; + background: var(--gold-primary); margin-bottom: 20px; transition: width 0.3s ease; } @@ -435,11 +543,11 @@ font-size: 1.35rem; font-weight: 700; margin-bottom: 14px; - color: #f0ece4; + color: var(--text-primary); } .feature-card p { - color: #9a958c; + color: var(--text-secondary); line-height: 1.7; font-size: 0.92rem; } @@ -464,11 +572,11 @@ padding: 32px 0; position: relative; transition: all 0.3s ease; - border-bottom: 1px solid rgba(212, 168, 83, 0.12); + border-bottom: 1px solid var(--border-gold); } .workflow-step:first-child { - border-top: 1px solid rgba(212, 168, 83, 0.12); + border-top: 1px solid var(--border-gold); } .workflow-step:hover { @@ -479,12 +587,12 @@ font-family: 'Playfair Display', 'Noto Serif SC', serif; font-size: 1.6rem; font-weight: 700; - color: #8a6d2b; + color: var(--gold-dim); transition: color 0.3s ease; } .workflow-step:hover .workflow-step-num { - color: #d4a853; + color: var(--gold-primary); } .workflow-step h4 { @@ -492,11 +600,11 @@ font-size: 1.15rem; font-weight: 700; margin-bottom: 6px; - color: #f0ece4; + color: var(--text-primary); } .workflow-step p { - color: #9a958c; + color: var(--text-secondary); font-size: 0.9rem; line-height: 1.6; } @@ -510,14 +618,14 @@ } .user-card { - background: #12121a; + background: var(--bg-surface); border-radius: 20px; padding: 36px 28px; text-align: center; position: relative; overflow: hidden; transition: all 0.4s ease; - border: 1px solid rgba(212, 168, 83, 0.12); + border: 1px solid var(--border-gold); } .user-card::after { @@ -527,7 +635,7 @@ left: 0; right: 0; height: 3px; - background: #d4a853; + background: var(--gold-primary); transform: scaleX(0); transition: transform 0.4s ease; } @@ -538,7 +646,7 @@ .user-card:hover { transform: translateY(-6px); - border-color: rgba(212, 168, 83, 0.35); + border-color: var(--border-gold-hover); } .user-emoji { @@ -552,11 +660,11 @@ font-size: 1.1rem; font-weight: 700; margin-bottom: 10px; - color: #f0ece4; + color: var(--text-primary); } .user-card p { - color: #9a958c; + color: var(--text-secondary); font-size: 0.85rem; line-height: 1.6; } @@ -564,8 +672,8 @@ /* ─── TECH ─── */ .tech-section { padding: 60px 0; - border-top: 1px solid rgba(212, 168, 83, 0.12); - border-bottom: 1px solid rgba(212, 168, 83, 0.12); + border-top: 1px solid var(--border-gold); + border-bottom: 1px solid var(--border-gold); } .tech-inner { @@ -579,7 +687,7 @@ font-weight: 500; letter-spacing: 0.25em; text-transform: uppercase; - color: #5c584f; + color: var(--text-muted); white-space: nowrap; } @@ -592,18 +700,18 @@ .tech-tag { padding: 8px 20px; border-radius: 40px; - color: #9a958c; + color: var(--text-secondary); font-size: 0.82rem; font-weight: 500; letter-spacing: 0.03em; transition: all 0.3s ease; - border: 1px solid rgba(212, 168, 83, 0.12); + border: 1px solid var(--border-gold); } .tech-tag:hover { - border-color: #d4a853; - color: #e8c878; - background: rgba(212, 168, 83, 0.06); + border-color: var(--gold-primary); + color: var(--gold-light); + background: var(--gold-glow); } /* ─── CTA ─── */ @@ -626,7 +734,7 @@ /* ─── FOOTER ─── */ .footer-bar { padding: 40px 0; - border-top: 1px solid rgba(212, 168, 83, 0.12); + border-top: 1px solid var(--border-gold); } .footer-inner { @@ -636,7 +744,7 @@ } .footer-text { - color: #5c584f; + color: var(--text-muted); font-size: 0.82rem; } @@ -647,14 +755,14 @@ } .footer-links a { - color: #5c584f; + color: var(--text-muted); text-decoration: none; font-size: 0.82rem; transition: color 0.3s ease; } .footer-links a:hover { - color: #e8c878; + color: var(--gold-light); } } diff --git a/src/main.tsx b/src/main.tsx index 136df4d..5c25b08 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,6 +4,29 @@ import App from './App.tsx' import './index.css' import './styles/app.css' +/** + * 主题防闪烁(FOUC prevention): + * 在 React 渲染前同步读取 localStorage 中的主题偏好并写入 。 + * 若用户选择 'system' 或无记录,则按 prefers-color-scheme 解析。 + * 必须同步执行,否则首帧会以 :root 默认(obsidian)渲染后跳变。 + */ +;(function applyInitialTheme() { + try { + const raw = localStorage.getItem('kf-settings') + const stored = raw ? (JSON.parse(raw).state?.theme as string | undefined) : undefined + const theme = stored || 'system' + const resolved = + theme === 'system' + ? window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'obsidian' + : 'daylight' + : theme + document.documentElement.setAttribute('data-theme', resolved) + } catch { + document.documentElement.setAttribute('data-theme', 'obsidian') + } +})() + createRoot(document.getElementById('root')!).render( diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx index ce548cf..ebef6a2 100644 --- a/src/pages/LandingPage.tsx +++ b/src/pages/LandingPage.tsx @@ -64,7 +64,7 @@ export default function LandingPage() {
    +
    {error}
    )} @@ -196,14 +196,14 @@ export default function PlansPage() { justifyContent: 'space-between', alignItems: 'center', padding: '16px 20px', - background: '#0a0a0f', + background: 'var(--bg-input)', borderRadius: '12px', - border: '1px solid rgba(212, 168, 83, 0.1)', + border: '1px solid var(--border-gold)', }} >
    -
    {node.topic}
    -
    +
    {node.topic}
    +
    {new Date(node.learnedAt).toLocaleString('zh-CN')}
    diff --git a/src/pages/PracticePage.tsx b/src/pages/PracticePage.tsx index bacdfb7..75b39b2 100644 --- a/src/pages/PracticePage.tsx +++ b/src/pages/PracticePage.tsx @@ -276,7 +276,7 @@ function sum(n) {
    {error && ( -
    +
    {error}
    )} @@ -317,8 +317,8 @@ function sum(n) { {/* 批改结果 */} {answers && ( -
    -
    批改结果
    +
    +
    批改结果
    )} @@ -336,14 +336,14 @@ function sum(n) { justifyContent: 'space-between', alignItems: 'center', padding: '16px 20px', - background: '#0a0a0f', + background: 'var(--bg-input)', borderRadius: '12px', - border: '1px solid rgba(212, 168, 83, 0.1)', + border: '1px solid var(--border-gold)', }} >
    -
    {p.topic}
    -
    +
    {p.topic}
    +
    {new Date(p.createdAt).toLocaleString('zh-CN')}
    diff --git a/src/pages/ResourcesPage.tsx b/src/pages/ResourcesPage.tsx index a2e8085..0635c24 100644 --- a/src/pages/ResourcesPage.tsx +++ b/src/pages/ResourcesPage.tsx @@ -77,7 +77,7 @@ export default function ResourcesPage() { className="app-card" style={{ marginBottom: '24px', - border: dragging ? '2px dashed #d4a853' : '2px dashed rgba(212, 168, 83, 0.2)', + border: dragging ? '2px dashed var(--gold-primary)' : '2px dashed var(--border-gold-strong)', textAlign: 'center', cursor: 'pointer', transition: 'all 0.3s ease', @@ -95,10 +95,10 @@ export default function ResourcesPage() { onChange={(e) => handleFiles(e.target.files)} />
    📁
    -
    +
    点击或拖拽文件到此处上传
    -
    +
    支持:TXT / MD / JSON / CSV / 代码文件(直接读取)· PDF / 图片 / 视频(模拟可读化)
    @@ -118,10 +118,10 @@ export default function ResourcesPage() {
    -
    +
    {r.name}
    -
    +
    {r.type} · {formatSize(r.size)} · {new Date(r.createdAt).toLocaleString('zh-CN')}
    @@ -152,13 +152,13 @@ export default function ResourcesPage() { {r.content && r.status === 'readable' && (
               
    当前状态
    -
    +
    {isConfigured() ? '✅ 已就绪' : '⚠️ 未配置'}
    -
    +
    {currentPreset?.label} {provider !== 'demo' && model ? ` · ${model}` : ''}
    @@ -65,18 +65,18 @@ export default function SettingsPage() { onClick={() => handlePresetChange(preset.id)} style={{ padding: '20px', - background: provider === preset.id ? 'rgba(212, 168, 83, 0.08)' : '#0a0a0f', - border: provider === preset.id ? '1px solid #d4a853' : '1px solid rgba(212, 168, 83, 0.12)', + background: provider === preset.id ? 'var(--gold-glow)' : 'var(--bg-input)', + border: provider === preset.id ? '1px solid var(--gold-primary)' : '1px solid var(--border-gold)', borderRadius: '14px', cursor: 'pointer', textAlign: 'left', transition: 'all 0.3s ease', }} > -
    +
    {preset.label}
    -
    +
    {preset.hint}
    @@ -105,7 +105,7 @@ export default function SettingsPage() { {showKey ? '隐藏' : '显示'}
    -
    +
    🔒 API Key 仅存储在你浏览器的 localStorage 中,不会上传到任何服务器。
    @@ -130,7 +130,7 @@ export default function SettingsPage() { value={model} onChange={(e) => setModel(e.target.value)} /> -
    +
    推荐模型:gpt-4o-mini(性价比高)/ gpt-4o(效果最好)/ deepseek-chat(国内友好)
    @@ -144,9 +144,9 @@ export default function SettingsPage() {
    {/* 安全提示 */} -
    -
    安全说明
    -
      +
      +
      安全说明
      +
      • 本产品为纯前端应用,所有数据存储在浏览器本地(localStorage)。
      • API Key 仅用于直接从你的浏览器调用 AI 服务,不经过任何中间服务器。
      • 请勿在公共电脑上保存 API Key。
      • diff --git a/src/pages/StudyPage.tsx b/src/pages/StudyPage.tsx index a3d6f7e..e970062 100644 --- a/src/pages/StudyPage.tsx +++ b/src/pages/StudyPage.tsx @@ -234,7 +234,7 @@ export default function StudyPage() {
      {msg.role === 'user' ? ( -

      {msg.content}

      +

      {msg.content}

      ) : ( )} @@ -264,7 +264,7 @@ export default function StudyPage() { {/* 错误提示 */} {error && ( -
      +
      {error}