Browse Source

feat(theme): add SongUI token system with light/dark palette and LXGW WenKai font

main
Mohan 1 month ago
parent
commit
3a2437369e
  1. 7
      package-lock.json
  2. 1
      package.json
  3. 36
      src/composables/useTheme.ts
  4. 6
      src/main.ts
  5. 208
      src/styles/song-theme.css

7
package-lock.json

@ -11,6 +11,7 @@
"axios": "^1.7.9",
"dompurify": "^3.2.4",
"highlight.js": "^11.11.1",
"lxgw-wenkai-webfont": "^1.7.0",
"marked": "^18.0.9",
"marked-highlight": "^2.2.4",
"pinia": "^2.3.0",
@ -2544,6 +2545,12 @@
"node": "20 || >=22"
}
},
"node_modules/lxgw-wenkai-webfont": {
"version": "1.7.0",
"resolved": "https://registry.npmmirror.com/lxgw-wenkai-webfont/-/lxgw-wenkai-webfont-1.7.0.tgz",
"integrity": "sha512-kVJmT4xMAyOLhG6q0w63wJkVU7RP8UMQRGoAWj9tnpzL7sWFFYiEHBdUQyW9myqBQFa5SQfWfa2VLYF4IJBL5g==",
"license": "MIT"
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",

1
package.json

@ -13,6 +13,7 @@
"axios": "^1.7.9",
"dompurify": "^3.2.4",
"highlight.js": "^11.11.1",
"lxgw-wenkai-webfont": "^1.7.0",
"marked": "^18.0.9",
"marked-highlight": "^2.2.4",
"pinia": "^2.3.0",

36
src/composables/useTheme.ts

@ -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)
}
}

6
src/main.ts

@ -1,7 +1,13 @@
import { createApp } from 'vue'
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 router from './router'
import { initTheme } from './composables/useTheme'
initTheme()
const app = createApp(App)
app.use(createPinia())

208
src/styles/song-theme.css

@ -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;
}
}
Loading…
Cancel
Save