feat(theme): 主题整套联动——界面配色跟随文档主题(docs/22)

六套文档主题各加一组同气质的界面变量(data-doc-theme 驱动),选什么
主题,标题栏/菜单/侧边栏/状态栏就穿什么配色;界面跟随当前生效主题
(含单文档临时主题)。视图「深色主题」开关撤下,明暗并入主题;
data-theme 退化为 paperTone 派生信号供 Mermaid/源码编辑器读取。
This commit is contained in:
Muse
2026-11-14 15:57:23 +08:00
parent e94034c9a8
commit d637f0a849
4 changed files with 68 additions and 17 deletions

30
docs/22-theme-linkage.md Normal file
View File

@@ -0,0 +1,30 @@
# 主题整套联动(2026-11-14)
## 需求
用户反馈「切换主题,只有文档变了」,拍板选「整套联动」:文档主题什么样,
界面配色也跟着成什么样。此前文档主题只作用于编辑区排版,界面另有独立的
浅/深开关(视图菜单),两套互不相干。
## 实现
- styles.css 为六套文档主题各加一组界面变量(html[data-doc-theme='…']):
背景/面板/边框/文字/次级文字/强调色/选中底/悬停/代码底,取自该主题文档
配色的气质(GitHub 蓝、Notion 暖灰、简约蓝、深色护眼墨绿深色、专注写作
暖纸、经典纸张米色)。默认主题沿用 :root 基础值。原 html[data-theme='dark']
界面变量块删除。
- 界面跟随的是「当前生效主题」:全局主题,或当前文档的临时主题覆盖——
切到设了临时主题的文档,整个窗口跟着换装。
- 视图菜单「深色主题」撤下:明暗已并入主题(选深色护眼即整套深色)。
ui.theme 偏好字段保留在存量数据里但不再生效。
- data-theme 属性保留为明暗派生信号(纸面为深色的主题 = dark),由文档
主题的 paperTone 派生,供 Mermaid 渲染主题等 JS 消费方读取;源码编辑器
的明暗同样改为按 paperTone 派生,不再回退 ui.theme。
## 验证
- 无头浏览器逐个切换 7 套主题,量 body/侧边栏/标题栏计算背景色:与各自
设计值逐一吻合(如深色护眼 body #1d221c、纸张 body #f3ecd7、GitHub
body #f6f8fa),文档纸面与框架同色系;深色护眼截图全窗墨绿深灰、文字
对比正常。
- tsc 0 错误;vitest 全量通过;构建通过。

View File

@@ -196,7 +196,6 @@ export default function App() {
const uiSaveTimerRef = useRef<number | undefined>(undefined)
useEffect(() => {
document.documentElement.dataset.theme = ui.theme
if (!uiLoadedRef.current) return
window.clearTimeout(uiSaveTimerRef.current)
uiSaveTimerRef.current = window.setTimeout(() => {
@@ -228,6 +227,9 @@ export default function App() {
const effectiveDocTheme = (tab ? docOverrides[tab.id] : undefined) ?? ui.docTheme
useEffect(() => {
document.documentElement.dataset.docTheme = effectiveDocTheme
// 整套联动(docs/22):界面配色由 CSS 按 data-doc-theme 接管;
// data-theme 退化为明暗派生信号(深色纸面主题 = dark),供 Mermaid 等读取。
document.documentElement.dataset.theme = docThemeById(effectiveDocTheme).paperTone === 'dark' ? 'dark' : 'light'
}, [effectiveDocTheme])
useEffect(() => {
@@ -1157,7 +1159,6 @@ export default function App() {
{ label: '文档大纲', checked: sidebarMode === 'outline', onClick: () => { setSidebarMode('outline'); setUi((u) => ({ ...u, sidebar: true })) } },
{ divider: true },
{ label: '源代码模式', checked: sourceMode, disabled: !tab, onClick: toggleSourceMode },
{ label: '深色主题', checked: ui.theme === 'dark', onClick: () => setUi((u) => ({ ...u, theme: u.theme === 'dark' ? 'light' : 'dark' })) },
{ label: `自动保存(${ui.autosaveSeconds} 秒)`, checked: ui.autosave, onClick: () => setUi((u) => ({ ...u, autosave: !u.autosave })) },
{ label: '专注模式', checked: ui.focusMode, disabled: sourceMode, onClick: () => setUi((u) => ({ ...u, focusMode: !u.focusMode })) },
{ label: '打字机模式', checked: ui.typewriter, disabled: sourceMode, onClick: () => setUi((u) => ({ ...u, typewriter: !u.typewriter })) },
@@ -1260,9 +1261,7 @@ export default function App() {
tabId={tab.id}
initialText={tab.content}
initialCursorOffset={sourceCursorOffset}
dark={docThemeById(effectiveDocTheme).paperTone
? docThemeById(effectiveDocTheme).paperTone === 'dark'
: ui.theme === 'dark'}
dark={docThemeById(effectiveDocTheme).paperTone === 'dark'}
onChange={(text) => dispatch({ type: 'updateContent', id: tab.id, content: text })}
onReady={(api) => { sourceApiRef.current = api }}
/>

View File

@@ -26,7 +26,7 @@ export function AppearanceModal({ docTheme, override, customCss, autosave, autos
<div className="modal-backdrop" onClick={onClose}>
<div className="modal" style={{ width: 560 }} onClick={(e) => e.stopPropagation()}>
<h3>外观与设置</h3>
<div className="setting-label">文档主题(作用于编辑区排版)</div>
<div className="setting-label">主题(文档排版与界面配色整套生效)</div>
<div className="theme-grid">
{DOC_THEMES.map((t) => (
<button

View File

@@ -12,17 +12,39 @@
--hover: #e9e9e6;
--code-bg: #f4f4f2;
}
html[data-theme='dark'] {
--bg: #2b2b2b;
--bg-panel: #252525;
--bg-editor: #1f1f1f;
--border: #3a3a3a;
--text: #d6d6d3;
--text-dim: #9a9a96;
--accent: #7d93b2;
--accent-weak: #333c48;
--hover: #333333;
--code-bg: #2c2c2c;
/* ——— 主题整套联动:界面配色跟随当前文档主题(docs/22) ———
文档主题什么样,窗口框架就穿什么样:每套文档主题在这里给出同气质的
界面变量(背景/面板/边框/文字/强调色)。默认主题用 :root 基础值。
data-theme 只保留为「明暗」派生信号供 Mermaid 等 JS 消费,不再定义变量。 */
html[data-doc-theme='github'] {
--bg: #f6f8fa; --bg-panel: #eef1f4; --bg-editor: #ffffff;
--border: #d8dee4; --text: #1f2328; --text-dim: #59636e;
--accent: #0969da; --accent-weak: #e8f1fc; --hover: #eaeef2; --code-bg: #f6f8fa;
}
html[data-doc-theme='notion'] {
--bg: #f7f6f3; --bg-panel: #f1f0ec; --bg-editor: #ffffff;
--border: #e6e4e0; --text: #37352f; --text-dim: #787774;
--accent: #37352f; --accent-weak: #eceae6; --hover: #edece9; --code-bg: #f7f6f3;
}
html[data-doc-theme='simple-blue'] {
--bg: #f5f8fd; --bg-panel: #ecf2fb; --bg-editor: #ffffff;
--border: #d7e3f6; --text: #1e2a38; --text-dim: #5b7186;
--accent: #2563eb; --accent-weak: #e2ebfc; --hover: #e8effc; --code-bg: #eef4fd;
}
html[data-doc-theme='dark-eye'] {
--bg: #1d221c; --bg-panel: #181c17; --bg-editor: #20261f;
--border: #39412f; --text: #c9d1c0; --text-dim: #9aa78f;
--accent: #9ec49a; --accent-weak: #2e3a2c; --hover: #2a322a; --code-bg: #2a322a;
}
html[data-doc-theme='focus'] {
--bg: #f7f4ee; --bg-panel: #f0ebdf; --bg-editor: #fffdf8;
--border: #e2dac6; --text: #3a362c; --text-dim: #857d68;
--accent: #8a6d3b; --accent-weak: #ede4cd; --hover: #eee8d8; --code-bg: #f3efe4;
}
html[data-doc-theme='paper'] {
--bg: #f3ecd7; --bg-panel: #ede3c8; --bg-editor: #f7f1e3;
--border: #ddd0a8; --text: #433422; --text-dim: #8a7a52;
--accent: #8a5a2b; --accent-weak: #e9dcb6; --hover: #ece0ba; --code-bg: #efe6d0;
}
* { box-sizing: border-box; }
html, body, #root { height: 100%; margin: 0; }