Browse Source
feat(theme): add SongUI token system with light/dark palette and LXGW WenKai font
main
feat(theme): add SongUI token system with light/dark palette and LXGW WenKai font
main
5 changed files with 258 additions and 0 deletions
-
7package-lock.json
-
1package.json
-
36src/composables/useTheme.ts
-
6src/main.ts
-
208src/styles/song-theme.css
@ -0,0 +1,36 @@ |
|||||
|
import { ref } from 'vue' |
||||
|
|
||||
|
const STORAGE_KEY = 'mach-theme' |
||||
|
const isDark = ref(false) |
||||
|
let initialized = false |
||||
|
|
||||
|
export function applyTheme(v: boolean): void { |
||||
|
isDark.value = v |
||||
|
document.documentElement.setAttribute('data-theme', v ? 'dark' : 'light') |
||||
|
try { |
||||
|
localStorage.setItem(STORAGE_KEY, v ? 'dark' : 'light') |
||||
|
} catch { |
||||
|
/* ignore */ |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function initTheme(): void { |
||||
|
if (initialized) return |
||||
|
initialized = true |
||||
|
let stored: string | null = null |
||||
|
try { |
||||
|
stored = localStorage.getItem(STORAGE_KEY) |
||||
|
} catch { |
||||
|
/* ignore */ |
||||
|
} |
||||
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches |
||||
|
applyTheme(stored ? stored === 'dark' : prefersDark) |
||||
|
} |
||||
|
|
||||
|
export function useTheme() { |
||||
|
initTheme() |
||||
|
return { |
||||
|
isDark, |
||||
|
toggle: () => applyTheme(!isDark.value) |
||||
|
} |
||||
|
} |
||||
@ -1,7 +1,13 @@ |
|||||
import { createApp } from 'vue' |
import { createApp } from 'vue' |
||||
import { createPinia } from 'pinia' |
import { createPinia } from 'pinia' |
||||
|
import 'lxgw-wenkai-webfont/lxgwwenkai-regular.css' |
||||
|
import 'lxgw-wenkai-webfont/lxgwwenkai-bold.css' |
||||
|
import './styles/song-theme.css' |
||||
import App from './App.vue' |
import App from './App.vue' |
||||
import router from './router' |
import router from './router' |
||||
|
import { initTheme } from './composables/useTheme' |
||||
|
|
||||
|
initTheme() |
||||
|
|
||||
const app = createApp(App) |
const app = createApp(App) |
||||
app.use(createPinia()) |
app.use(createPinia()) |
||||
|
|||||
@ -0,0 +1,208 @@ |
|||||
|
/* SongUI · 檐下 — 设计契约实现 |
||||
|
* 参照 frontendui/SongUI研究报告.md §2-§9 与 src/styles/tokens/{light,dark}.css |
||||
|
* :attr=light 天青昼 / :attr=dark 黛墨夜 |
||||
|
*/ |
||||
|
|
||||
|
:root, |
||||
|
:root[data-theme='light'] { |
||||
|
color-scheme: light; |
||||
|
--song-bg: #f7f4ec; |
||||
|
--song-bg-elevated: #fffdf8; |
||||
|
--song-surface-3: #ece6d8; |
||||
|
|
||||
|
--song-text: #3a4a52; |
||||
|
--song-text-secondary: #5a6b74; |
||||
|
--song-text-disabled: #9aa7ad; |
||||
|
|
||||
|
--song-primary: #4f78b8; |
||||
|
--song-primary-bg: #e7eef9; |
||||
|
--song-success: #4e7a45; |
||||
|
--song-success-bg: #e9f0e4; |
||||
|
--song-warning: #8f7a1e; |
||||
|
--song-warning-bg: #f6f1df; |
||||
|
--song-danger: #a84635; |
||||
|
--song-danger-bg: #f6e9e2; |
||||
|
|
||||
|
--song-border: rgba(58, 74, 82, 0.14); |
||||
|
--song-border-strong: rgba(58, 74, 82, 0.28); |
||||
|
--song-hover: rgba(79, 120, 184, 0.08); |
||||
|
--song-active: rgba(79, 120, 184, 0.14); |
||||
|
|
||||
|
--song-btn-bg: #4f78b8; |
||||
|
--song-btn-text: #fff; |
||||
|
|
||||
|
--song-seal: #a84635; |
||||
|
|
||||
|
--song-shadow-1: 0 1px 2px rgba(20, 24, 27, 0.06); |
||||
|
--song-shadow-2: 0 2px 8px rgba(20, 24, 27, 0.08); |
||||
|
--song-shadow-3: 0 8px 24px rgba(20, 24, 27, 0.12); |
||||
|
|
||||
|
--song-font-display: 'LXGW WenKai', 'Noto Serif SC', 'Songti SC', serif; |
||||
|
--song-font-serif: 'Noto Serif SC', 'Songti SC', serif; |
||||
|
--song-font-sans: 'Noto Sans SC', -apple-system, 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif; |
||||
|
--song-font-body: var(--song-font-sans); |
||||
|
|
||||
|
--song-radius-s: 2px; |
||||
|
--song-radius-m: 4px; |
||||
|
--song-radius-l: 6px; |
||||
|
|
||||
|
--song-space-1: 4px; |
||||
|
--song-space-2: 8px; |
||||
|
--song-space-3: 12px; |
||||
|
--song-space-4: 16px; |
||||
|
--song-space-5: 24px; |
||||
|
--song-space-6: 32px; |
||||
|
--song-space-8: 64px; |
||||
|
|
||||
|
--song-ease: cubic-bezier(0.33, 1, 0.68, 1); |
||||
|
--song-dur-1: 200ms; |
||||
|
--song-dur-2: 320ms; |
||||
|
} |
||||
|
|
||||
|
:root[data-theme='dark'] { |
||||
|
color-scheme: dark; |
||||
|
--song-bg: #14181b; |
||||
|
--song-bg-elevated: #1d2326; |
||||
|
--song-surface-3: #232b2f; |
||||
|
|
||||
|
--song-text: #e6ecef; |
||||
|
--song-text-secondary: #a9b6be; |
||||
|
--song-text-disabled: #5e6a71; |
||||
|
|
||||
|
--song-primary: #9dbeee; |
||||
|
--song-primary-bg: rgba(113, 150, 203, 0.16); |
||||
|
--song-success: #94b989; |
||||
|
--song-success-bg: rgba(148, 185, 137, 0.14); |
||||
|
--song-warning: #d3bc6a; |
||||
|
--song-warning-bg: rgba(211, 188, 106, 0.14); |
||||
|
--song-danger: #d08a79; |
||||
|
--song-danger-bg: rgba(208, 138, 121, 0.14); |
||||
|
|
||||
|
--song-border: rgba(230, 236, 239, 0.12); |
||||
|
--song-border-strong: rgba(230, 236, 239, 0.26); |
||||
|
--song-hover: rgba(157, 190, 238, 0.1); |
||||
|
--song-active: rgba(157, 190, 238, 0.16); |
||||
|
|
||||
|
--song-btn-bg: #48689e; |
||||
|
--song-btn-text: #fff; |
||||
|
|
||||
|
--song-seal: #c76a4f; |
||||
|
|
||||
|
--song-shadow-1: 0 1px 2px rgba(0, 0, 0, 0.3); |
||||
|
--song-shadow-2: 0 2px 8px rgba(0, 0, 0, 0.35); |
||||
|
--song-shadow-3: 0 8px 24px rgba(0, 0, 0, 0.45); |
||||
|
} |
||||
|
|
||||
|
/* ── 应用语义别名(两主题自适应) ───────────────────────────── */ |
||||
|
:root { |
||||
|
--ac-bg: var(--song-bg); |
||||
|
--ac-bg-card: var(--song-bg-elevated); |
||||
|
--ac-surface: var(--song-surface-3); |
||||
|
|
||||
|
--ac-text: var(--song-text); |
||||
|
--ac-text-secondary: var(--song-text-secondary); |
||||
|
--ac-text-disabled: var(--song-text-disabled); |
||||
|
|
||||
|
--ac-primary: var(--song-primary); |
||||
|
--ac-primary-strong: var(--song-btn-bg); |
||||
|
--ac-primary-text: var(--song-btn-text); |
||||
|
--ac-primary-bg: var(--song-primary-bg); |
||||
|
|
||||
|
--ac-success: var(--song-success); |
||||
|
--ac-success-bg: var(--song-success-bg); |
||||
|
--ac-warning: var(--song-warning); |
||||
|
--ac-warning-bg: var(--song-warning-bg); |
||||
|
--ac-danger: var(--song-danger); |
||||
|
--ac-danger-bg: var(--song-danger-bg); |
||||
|
|
||||
|
--ac-border: var(--song-border); |
||||
|
--ac-border-strong: var(--song-border-strong); |
||||
|
--ac-input-border: var(--song-border-strong); |
||||
|
--ac-hover: var(--song-hover); |
||||
|
--ac-active: var(--song-active); |
||||
|
|
||||
|
--ac-shadow-1: var(--song-shadow-1); |
||||
|
--ac-shadow-2: var(--song-shadow-2); |
||||
|
--ac-shadow-3: var(--song-shadow-3); |
||||
|
|
||||
|
--ac-radius-s: 2px; |
||||
|
--ac-radius-m: 4px; |
||||
|
--ac-radius-l: 6px; |
||||
|
|
||||
|
--ac-header-bg: #232d34; |
||||
|
--ac-header-text: #eef2f4; |
||||
|
--ac-header-muted: #a9b6be; |
||||
|
--ac-header-active-bg: #31404a; |
||||
|
} |
||||
|
|
||||
|
:root[data-theme='dark'] { |
||||
|
--ac-header-bg: var(--song-bg-elevated); |
||||
|
--ac-header-text: var(--song-text); |
||||
|
--ac-header-muted: var(--song-text-secondary); |
||||
|
--ac-header-active-bg: var(--song-hover); |
||||
|
} |
||||
|
|
||||
|
/* ── 基础 §3 ──────────────────────────────────────────────── */ |
||||
|
*, |
||||
|
*::before, |
||||
|
*::after { |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
|
||||
|
html, |
||||
|
body { |
||||
|
margin: 0; |
||||
|
padding: 0; |
||||
|
} |
||||
|
|
||||
|
body { |
||||
|
background: var(--song-bg); |
||||
|
color: var(--song-text); |
||||
|
font-family: var(--song-font-body); |
||||
|
font-size: 14px; |
||||
|
line-height: 22px; |
||||
|
-webkit-font-smoothing: antialiased; |
||||
|
transition: background-color var(--song-dur-2) var(--song-ease), color var(--song-dur-2) var(--song-ease); |
||||
|
} |
||||
|
|
||||
|
a { |
||||
|
color: var(--ac-primary); |
||||
|
text-decoration: none; |
||||
|
} |
||||
|
a:hover { |
||||
|
text-decoration: underline; |
||||
|
} |
||||
|
|
||||
|
/* 焦点态 §9 — 主色 2px 轮廓 */ |
||||
|
:focus-visible { |
||||
|
outline: 2px solid var(--song-primary); |
||||
|
outline-offset: 2px; |
||||
|
border-radius: var(--song-radius-s); |
||||
|
} |
||||
|
|
||||
|
/* 数字等宽 */ |
||||
|
.tnum { |
||||
|
font-variant-numeric: tabular-nums; |
||||
|
} |
||||
|
|
||||
|
/* 页面转场 — 雾隐淡入 §7 */ |
||||
|
.page-enter-active, |
||||
|
.page-leave-active { |
||||
|
transition: opacity var(--song-dur-2) var(--song-ease), transform var(--song-dur-2) var(--song-ease); |
||||
|
} |
||||
|
.page-enter-from, |
||||
|
.page-leave-to { |
||||
|
opacity: 0; |
||||
|
transform: translateY(8px); |
||||
|
} |
||||
|
|
||||
|
/* 动效可关 §7 */ |
||||
|
@media (prefers-reduced-motion: reduce) { |
||||
|
*, |
||||
|
*::before, |
||||
|
*::after { |
||||
|
animation-duration: 0.01ms !important; |
||||
|
animation-iteration-count: 1 !important; |
||||
|
transition-duration: 0.01ms !important; |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue