- max {{ chart.maxLabel }}
+ max {{ chart.maxLabel }}
-
diff --git a/web/src/main.ts b/web/src/main.ts
index d70f70c..7f8aef6 100644
--- a/web/src/main.ts
+++ b/web/src/main.ts
@@ -2,12 +2,12 @@ import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import { router } from './router'
+import { useThemeStore } from './stores/theme'
import './style.css'
-// 深色优先(MVP 固定深色,后续加亮色切换)
-document.documentElement.classList.add('dark')
-
const app = createApp(App)
-app.use(createPinia())
+const pinia = createPinia()
+app.use(pinia)
+useThemeStore(pinia).init()
app.use(router)
app.mount('#app')
diff --git a/web/src/stores/theme.ts b/web/src/stores/theme.ts
new file mode 100644
index 0000000..bca673f
--- /dev/null
+++ b/web/src/stores/theme.ts
@@ -0,0 +1,42 @@
+import { defineStore } from 'pinia'
+
+export type ThemeMode = 'light' | 'dark' | 'system'
+
+const STORAGE_KEY = 'ot_theme'
+const media = window.matchMedia('(prefers-color-scheme: dark)')
+
+function resolve(mode: ThemeMode): 'light' | 'dark' {
+ if (mode === 'system') return media.matches ? 'dark' : 'light'
+ return mode
+}
+
+export const useThemeStore = defineStore('theme', {
+ state: () => ({
+ mode: (localStorage.getItem(STORAGE_KEY) as ThemeMode) || 'system',
+ }),
+ getters: {
+ resolved(state): 'light' | 'dark' {
+ return resolve(state.mode)
+ },
+ },
+ actions: {
+ // 应用当前主题;监听系统切换(system 模式时跟随)
+ init() {
+ this.apply()
+ media.addEventListener('change', () => this.apply())
+ },
+ set(mode: ThemeMode) {
+ this.mode = mode
+ localStorage.setItem(STORAGE_KEY, mode)
+ this.apply()
+ },
+ cycle() {
+ const order: ThemeMode[] = ['system', 'light', 'dark']
+ const i = order.indexOf(this.mode)
+ this.set(order[(i + 1) % order.length])
+ },
+ apply() {
+ document.documentElement.setAttribute('data-theme', this.resolved)
+ },
+ },
+})
diff --git a/web/src/style.css b/web/src/style.css
index 36b8454..084e205 100644
--- a/web/src/style.css
+++ b/web/src/style.css
@@ -4,43 +4,96 @@
/* ------------------------------------------------------------------ */
/* 设计 tokens(taste-skill 产出) */
-/* 深色优先 · 单一强调色 emerald · 圆角体系:卡片 8 / 控件 6 / 徽章 pill */
-/* 密度 7:mono 数字、紧凑表格、细线分隔 */
+/* 三主题:dark / light / system(data-theme 切换,CSS 变量语义令牌) */
+/* 单一强调色 emerald · 圆角:卡片 8 / 控件 6 / 徽章 pill */
/* ------------------------------------------------------------------ */
@theme {
--font-sans: 'Geist Variable', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-mono: 'Geist Mono Variable', ui-monospace, SFMono-Regular, Menlo, monospace;
- /* 强调色(单一一处定义,全局一致) */
- --color-accent: oklch(0.72 0.17 152);
- --color-accent-strong: oklch(0.64 0.19 152);
- --color-accent-soft: oklch(0.95 0.05 152);
-
- /* 状态色(语义,克制使用) */
- --color-ok: oklch(0.72 0.17 152);
- --color-warn: oklch(0.80 0.15 75);
- --color-err: oklch(0.63 0.21 25);
+ /* 语义令牌 → CSS 变量(随 data-theme 切换) */
+ --color-bg: var(--bg);
+ --color-surface: var(--surface);
+ --color-surface2: var(--surface2);
+ --color-edge: var(--edge);
+ --color-edge2: var(--edge2);
+ --color-ink: var(--ink);
+ --color-muted: var(--muted);
+ --color-accent: var(--accent);
+ --color-accent-strong: var(--accent-strong);
+ --color-accent-ink: var(--accent-ink);
+ --color-ok: var(--ok);
+ --color-warn: var(--warn);
+ --color-err: var(--err);
}
-html {
+/* 深色(默认) */
+:root,
+[data-theme='dark'] {
color-scheme: dark;
+ --bg: #09090b;
+ --surface: #18181b;
+ --surface2: #27272a;
+ --edge: #27272a;
+ --edge2: #3f3f46;
+ --ink: #f4f4f5;
+ --muted: #a1a1aa;
+ --accent: oklch(0.72 0.17 152);
+ --accent-strong: oklch(0.64 0.19 152);
+ --accent-ink: #09090b;
+ --ok: oklch(0.72 0.17 152);
+ --warn: #fbbf24;
+ --err: #f87171;
+}
+
+/* 浅色 */
+[data-theme='light'] {
+ color-scheme: light;
+ --bg: #fafafa;
+ --surface: #ffffff;
+ --surface2: #f4f4f5;
+ --edge: #e4e4e7;
+ --edge2: #d4d4d8;
+ --ink: #18181b;
+ --muted: #52525b;
+ --accent: oklch(0.58 0.16 152);
+ --accent-strong: oklch(0.52 0.16 152);
+ --accent-ink: #ffffff;
+ --ok: oklch(0.58 0.16 152);
+ --warn: #d97706;
+ --err: #dc2626;
}
body {
- background-color: #09090b;
- color: #f4f4f5;
+ background-color: var(--bg);
+ color: var(--ink);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
}
/* 全站统一键盘焦点可见性 */
:focus-visible {
- outline: 2px solid var(--color-accent);
+ outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: 4px;
}
+/* 通用组件视觉基元 */
+@layer components {
+ .card {
+ @apply rounded-lg border border-edge bg-surface;
+ }
+
+ .mono-num {
+ @apply font-mono tabular-nums;
+ }
+
+ .table-row {
+ @apply border-b border-edge last:border-0 hover:bg-surface2/40;
+ }
+}
+
@media (prefers-reduced-motion: reduce) {
*,
*::before,
@@ -49,18 +102,3 @@ body {
transition-duration: 0.01ms !important;
}
}
-
-/* 通用组件视觉基元 */
-@layer components {
- .card {
- @apply rounded-lg border border-zinc-800 bg-zinc-900/60;
- }
-
- .mono-num {
- @apply font-mono tabular-nums;
- }
-
- .table-row {
- @apply border-b border-zinc-800/70 last:border-0 hover:bg-zinc-800/30;
- }
-}
diff --git a/web/src/views/LandingView.vue b/web/src/views/LandingView.vue
index e20457d..695220b 100644
--- a/web/src/views/LandingView.vue
+++ b/web/src/views/LandingView.vue
@@ -1,32 +1,34 @@
-
+
-
+
openteam