docnest 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -488,6 +488,65 @@
488
488
  });
489
489
  }
490
490
 
491
+ function showPdfExportError(message) {
492
+ var modalId = 'pdf-export-error';
493
+ var existing = document.querySelector('.pdf-export-error');
494
+ if (existing && existing.parentNode) {
495
+ existing.parentNode.removeChild(existing);
496
+ }
497
+ if (typeof removeModal === 'function') {
498
+ removeModal(modalId);
499
+ }
500
+
501
+ var overlay = document.createElement('div');
502
+ overlay.className = 'pdf-export-modal pdf-export-error';
503
+ overlay.innerHTML = [
504
+ '<div class="pdf-export-modal__panel" role="alertdialog" aria-modal="true" aria-labelledby="pdf-export-error-title" aria-describedby="pdf-export-error-desc">',
505
+ '<div class="pdf-export-modal__title" id="pdf-export-error-title">导出 PDF 失败</div>',
506
+ '<div class="pdf-export-modal__desc" id="pdf-export-error-desc"></div>',
507
+ '<div class="pdf-export-modal__actions">',
508
+ '<button type="button" class="pdf-export-modal__btn pdf-export-modal__btn--primary" data-action="close">知道了</button>',
509
+ '</div>',
510
+ '</div>'
511
+ ].join('');
512
+
513
+ var description = overlay.querySelector('#pdf-export-error-desc');
514
+ if (description) {
515
+ description.textContent = message || '暂时无法生成 PDF,请稍后重试。';
516
+ }
517
+
518
+ var closed = false;
519
+ function close() {
520
+ if (closed) return;
521
+ closed = true;
522
+ if (typeof removeModal === 'function') {
523
+ removeModal(modalId);
524
+ }
525
+ if (overlay.parentNode) {
526
+ overlay.parentNode.removeChild(overlay);
527
+ }
528
+ }
529
+
530
+ overlay.addEventListener('click', function(event) {
531
+ if (event.target === overlay) {
532
+ close();
533
+ }
534
+ });
535
+
536
+ var closeButton = overlay.querySelector('[data-action="close"]');
537
+ if (closeButton) {
538
+ closeButton.addEventListener('click', close);
539
+ }
540
+
541
+ document.body.appendChild(overlay);
542
+ if (typeof pushModal === 'function') {
543
+ pushModal(modalId, close);
544
+ }
545
+ if (closeButton) {
546
+ closeButton.focus();
547
+ }
548
+ }
549
+
491
550
  function setButtonLoading(button, loading) {
492
551
  if (!button) return;
493
552
  if (loading) {
@@ -512,7 +571,7 @@
512
571
  function exportCurrentDoc(button, options) {
513
572
  var article = document.querySelector('.main-content .markdown-body');
514
573
  if (!article) {
515
- window.alert('未找到当前文档内容,无法导出 PDF。');
574
+ showPdfExportError('未找到当前文档内容,无法导出 PDF。');
516
575
  return;
517
576
  }
518
577
 
@@ -552,7 +611,7 @@
552
611
  })
553
612
  .catch(function(error) {
554
613
  console.error('导出 PDF 失败:', error);
555
- window.alert('导出 PDF 失败,请稍后重试。');
614
+ showPdfExportError('暂时无法生成 PDF,请稍后重试。');
556
615
  })
557
616
  .finally(function() {
558
617
  setButtonLoading(button, false);
@@ -1,138 +1,335 @@
1
- // 主题切换功能(支持系统自动跟随)
1
+ // DocNest 外观系统:主题风格与明暗模式相互独立,并按宿主项目持久化。
2
2
  (function() {
3
- // 检测系统主题
4
- function getSystemTheme() {
5
- return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
6
- }
7
-
8
- // 从 localStorage 读取主题偏好,默认为 'auto'(跟随系统)
9
- const savedTheme = localStorage.getItem('markdown-theme') || 'auto';
10
-
11
- // 相对路径,由 resolveThemeCss 统一加上 base 或前导 /
12
- const themes = {
13
- light: {
14
- css: 'vendor/github-markdown-css/github-markdown-light.css',
15
- icon: '☀️',
16
- text: '浅色模式'
3
+ 'use strict';
4
+
5
+ var THEME_PROFILES = {
6
+ 'slate-modern': {
7
+ label: 'Slate Modern',
8
+ description: '清爽、克制的现代文档界面',
9
+ colors: ['#ffffff', '#111111', '#5e6ad2']
10
+ },
11
+ 'editorial-atlas': {
12
+ label: 'Editorial Atlas',
13
+ description: '安静、舒展的技术出版物',
14
+ colors: ['#f3f0e7', '#171815', '#1f5b57']
15
+ },
16
+ 'precision-index': {
17
+ label: 'Precision Index',
18
+ description: '高密度、可追溯的工程索引',
19
+ colors: ['#eef2f3', '#14242b', '#007486']
17
20
  },
18
- dark: {
19
- css: 'vendor/github-markdown-css/github-markdown-dark.css',
20
- icon: '🌙',
21
- text: '深色模式'
21
+ 'archive-room': {
22
+ label: 'Archive Room',
23
+ description: '温和、有秩序的档案阅览室',
24
+ colors: ['#f3efe4', '#262821', '#a5523d']
22
25
  },
23
- auto: {
24
- css: '',
25
- icon: '💻',
26
- text: '跟随系统'
26
+ 'swiss-manual': {
27
+ label: 'Swiss Manual',
28
+ description: '鲜明、直接的瑞士工程手册',
29
+ colors: ['#f4f3ef', '#111111', '#e33b27']
27
30
  }
28
31
  };
29
32
 
30
- function resolveThemeCss(path) {
31
- if (!path) return '';
32
- var base = document.querySelector('base');
33
- if (base && base.getAttribute('href')) {
34
- var baseHref = base.getAttribute('href').replace(/\/?$/, '/');
35
- return baseHref + path.replace(/^\//, '');
33
+ var COLOR_MODES = {
34
+ auto: { label: '跟随系统', shortLabel: 'A' },
35
+ light: { label: '浅色', shortLabel: 'L' },
36
+ dark: { label: '深色', shortLabel: 'D' }
37
+ };
38
+ var MODE_CYCLE = ['auto', 'light', 'dark'];
39
+ var config = window.DOCNEST_CONFIG || {};
40
+ var appearance = config.appearance || {};
41
+ var storagePrefix = String(config.storageKeyPrefix || 'docnest');
42
+ var themeStorageKey = storagePrefix + ':theme-style';
43
+ var modeStorageKey = storagePrefix + ':theme-mode';
44
+
45
+ function readStorage(key) {
46
+ try {
47
+ return window.localStorage.getItem(key);
48
+ } catch (_) {
49
+ return null;
36
50
  }
37
- return path.charAt(0) === '/' ? path : '/' + path;
38
- }
39
-
40
- // 获取实际要应用的主题(如果是 auto,则使用系统主题)
41
- function getEffectiveTheme(theme) {
42
- return theme === 'auto' ? getSystemTheme() : theme;
43
- }
44
-
45
- // 三种形态循环顺序:auto → light → dark → auto
46
- const themeCycle = ['auto', 'light', 'dark'];
47
-
48
- // 等待 DOM 加载完成
49
- function initTheme() {
50
- const themeToggle = document.getElementById('theme-toggle');
51
- const themeLink = document.getElementById('markdown-theme');
52
- const themeIcon = themeToggle?.querySelector('.theme-icon');
53
- const themeText = themeToggle?.querySelector('.theme-text');
54
-
55
- if (!themeToggle || !themeLink || !themeIcon) {
56
- if (document.readyState === 'loading') {
57
- document.addEventListener('DOMContentLoaded', initTheme);
58
- return;
59
- }
60
- console.warn('主题切换元素未找到,可能 DOM 尚未加载完成');
61
- return;
51
+ }
52
+
53
+ function writeStorage(key, value) {
54
+ try {
55
+ window.localStorage.setItem(key, value);
56
+ } catch (_) {}
57
+ }
58
+
59
+ function normalizeThemeName(value) {
60
+ return value === 'current-docs' ? 'slate-modern' : value;
61
+ }
62
+
63
+ function uniqueKnownThemes(values) {
64
+ var seen = new Set();
65
+ return (Array.isArray(values) ? values : [])
66
+ .map(normalizeThemeName)
67
+ .filter(function(value) {
68
+ if (!THEME_PROFILES[value] || seen.has(value)) return false;
69
+ seen.add(value);
70
+ return true;
71
+ });
72
+ }
73
+
74
+ var enabledThemes = uniqueKnownThemes(appearance.enabledThemes);
75
+ if (enabledThemes.length === 0) enabledThemes = Object.keys(THEME_PROFILES);
76
+
77
+ var configuredThemeName = normalizeThemeName(appearance.defaultTheme);
78
+ var configuredTheme = THEME_PROFILES[configuredThemeName]
79
+ ? configuredThemeName
80
+ : 'slate-modern';
81
+ var defaultTheme = enabledThemes.indexOf(configuredTheme) >= 0
82
+ ? configuredTheme
83
+ : enabledThemes[0];
84
+ var defaultMode = COLOR_MODES[appearance.defaultMode] ? appearance.defaultMode : 'auto';
85
+ var storedThemeValue = readStorage(themeStorageKey);
86
+ var storedTheme = normalizeThemeName(storedThemeValue);
87
+ var storedMode = readStorage(modeStorageKey);
88
+ var migratedLegacyMode = false;
89
+
90
+ // 兼容 0.1.x 的明暗模式存储键;只迁移有效值,不覆盖新的项目级偏好。
91
+ if (!storedMode) {
92
+ var legacyMode = readStorage('markdown-theme');
93
+ if (COLOR_MODES[legacyMode]) {
94
+ storedMode = legacyMode;
95
+ migratedLegacyMode = true;
62
96
  }
97
+ }
98
+
99
+ var activeTheme = enabledThemes.indexOf(storedTheme) >= 0 ? storedTheme : defaultTheme;
100
+ var activeMode = COLOR_MODES[storedMode] ? storedMode : defaultMode;
101
+ if (storedThemeValue === 'current-docs') writeStorage(themeStorageKey, activeTheme);
102
+ if (migratedLegacyMode) writeStorage(modeStorageKey, activeMode);
103
+
104
+ function getSystemMode() {
105
+ return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
106
+ ? 'dark'
107
+ : 'light';
108
+ }
63
109
 
64
- // 更新按钮显示(图标 + 文案)
65
- function updateButtonDisplay(theme) {
66
- const themeConfig = themes[theme];
67
- if (themeIcon) themeIcon.textContent = themeConfig.icon;
68
- if (themeText) themeText.textContent = themeConfig.text;
110
+ function getEffectiveMode(mode) {
111
+ return mode === 'auto' ? getSystemMode() : mode;
112
+ }
113
+
114
+ function setAppearanceAttributes(theme, mode) {
115
+ var effectiveMode = getEffectiveMode(mode);
116
+ var root = document.documentElement;
117
+ root.setAttribute('data-doc-theme', theme);
118
+ root.setAttribute('data-theme', effectiveMode);
119
+ root.setAttribute('data-theme-mode', mode);
120
+ root.style.colorScheme = effectiveMode;
121
+ if (document.body) {
122
+ document.body.setAttribute('data-doc-theme', theme);
123
+ document.body.setAttribute('data-theme', effectiveMode);
124
+ document.body.setAttribute('data-theme-mode', mode);
69
125
  }
126
+ return effectiveMode;
127
+ }
128
+
129
+ // 在 DOM 完成前先写入 html,避免主题闪烁。
130
+ setAppearanceAttributes(activeTheme, activeMode);
131
+
132
+ function resolveThemeCss(assetPath) {
133
+ var base = document.querySelector('base');
134
+ if (base && base.getAttribute('href')) {
135
+ return base.getAttribute('href').replace(/\/?$/, '/') + assetPath.replace(/^\//, '');
136
+ }
137
+ return assetPath.charAt(0) === '/' ? assetPath : '/' + assetPath;
138
+ }
139
+
140
+ function applyMarkdownTheme(effectiveMode) {
141
+ var link = document.getElementById('markdown-theme');
142
+ if (!link) return;
143
+ link.href = resolveThemeCss(
144
+ effectiveMode === 'dark'
145
+ ? 'vendor/github-markdown-css/github-markdown-dark.css'
146
+ : 'vendor/github-markdown-css/github-markdown-light.css'
147
+ );
148
+ }
149
+
150
+ function updateControls() {
151
+ var profile = THEME_PROFILES[activeTheme];
152
+ var styleText = document.querySelector('.theme-style-text');
153
+ var modeText = document.querySelector('.theme-text');
154
+ var modeIcon = document.querySelector('.theme-icon');
155
+ if (styleText) styleText.textContent = profile.label;
156
+ if (modeText) modeText.textContent = COLOR_MODES[activeMode].label;
157
+ if (modeIcon) modeIcon.textContent = COLOR_MODES[activeMode].shortLabel;
70
158
 
71
- // 应用主题
72
- function applyTheme(theme, skipSave) {
73
- const effectiveTheme = getEffectiveTheme(theme);
74
- const themeConfig = themes[theme];
75
- const effectiveConfig = themes[effectiveTheme];
159
+ document.querySelectorAll('[data-theme-option]').forEach(function(button) {
160
+ var checked = button.getAttribute('data-theme-option') === activeTheme;
161
+ button.setAttribute('aria-checked', String(checked));
162
+ button.classList.toggle('is-active', checked);
163
+ });
164
+ document.querySelectorAll('[data-mode-option]').forEach(function(button) {
165
+ var checked = button.getAttribute('data-mode-option') === activeMode;
166
+ button.setAttribute('aria-checked', String(checked));
167
+ button.classList.toggle('is-active', checked);
168
+ });
169
+ }
76
170
 
77
- themeLink.href = resolveThemeCss(effectiveConfig.css);
78
- document.body.setAttribute('data-theme', effectiveTheme);
79
- document.body.setAttribute('data-theme-mode', theme);
171
+ function refreshMermaid(effectiveMode) {
172
+ if (typeof window.mermaid === 'undefined') return;
173
+ window.mermaid.initialize({
174
+ startOnLoad: false,
175
+ theme: effectiveMode === 'dark' ? 'dark' : 'default',
176
+ securityLevel: 'loose',
177
+ flowchart: { useMaxWidth: true, htmlLabels: true }
178
+ });
179
+ }
80
180
 
81
- updateButtonDisplay(theme);
181
+ function applyAppearance(theme, mode, options) {
182
+ var nextTheme = enabledThemes.indexOf(theme) >= 0 ? theme : defaultTheme;
183
+ var nextMode = COLOR_MODES[mode] ? mode : defaultMode;
184
+ activeTheme = nextTheme;
185
+ activeMode = nextMode;
186
+ var effectiveMode = setAppearanceAttributes(activeTheme, activeMode);
187
+ applyMarkdownTheme(effectiveMode);
188
+ updateControls();
189
+ refreshMermaid(effectiveMode);
82
190
 
83
- if (!skipSave) {
84
- localStorage.setItem('markdown-theme', theme);
191
+ if (!options || options.persist !== false) {
192
+ writeStorage(themeStorageKey, activeTheme);
193
+ writeStorage(modeStorageKey, activeMode);
194
+ }
195
+
196
+ window.dispatchEvent(new CustomEvent('docnest:appearance-change', {
197
+ detail: {
198
+ theme: activeTheme,
199
+ mode: activeMode,
200
+ effectiveMode: effectiveMode
85
201
  }
202
+ }));
203
+ }
204
+
205
+ function buildThemeOptions(container) {
206
+ enabledThemes.forEach(function(themeName) {
207
+ var profile = THEME_PROFILES[themeName];
208
+ var button = document.createElement('button');
209
+ button.type = 'button';
210
+ button.className = 'theme-option';
211
+ button.setAttribute('role', 'radio');
212
+ button.setAttribute('data-theme-option', themeName);
213
+ button.setAttribute('aria-checked', 'false');
86
214
 
87
- if (typeof mermaid !== 'undefined') {
88
- mermaid.initialize({
89
- startOnLoad: false,
90
- theme: effectiveTheme === 'dark' ? 'dark' : 'default',
91
- securityLevel: 'loose',
92
- flowchart: {
93
- useMaxWidth: true,
94
- htmlLabels: true
95
- }
96
- });
97
- mermaid.run().then(() => {
98
- if (typeof setupDiagramViewer === 'function') {
99
- setupDiagramViewer();
100
- }
101
- });
215
+ var palette = document.createElement('span');
216
+ palette.className = 'theme-option-palette';
217
+ palette.setAttribute('aria-hidden', 'true');
218
+ profile.colors.forEach(function(color) {
219
+ var chip = document.createElement('i');
220
+ chip.style.backgroundColor = color;
221
+ palette.appendChild(chip);
222
+ });
223
+
224
+ var copy = document.createElement('span');
225
+ copy.className = 'theme-option-copy';
226
+ var title = document.createElement('strong');
227
+ title.textContent = profile.label;
228
+ var description = document.createElement('small');
229
+ description.textContent = profile.description;
230
+ copy.appendChild(title);
231
+ copy.appendChild(description);
232
+
233
+ var check = document.createElement('span');
234
+ check.className = 'theme-option-check';
235
+ check.setAttribute('aria-hidden', 'true');
236
+ check.textContent = '✓';
237
+
238
+ button.appendChild(palette);
239
+ button.appendChild(copy);
240
+ button.appendChild(check);
241
+ button.addEventListener('click', function() {
242
+ applyAppearance(themeName, activeMode);
243
+ });
244
+ container.appendChild(button);
245
+ });
246
+ }
247
+
248
+ function buildModeOptions(container) {
249
+ MODE_CYCLE.forEach(function(mode) {
250
+ var button = document.createElement('button');
251
+ button.type = 'button';
252
+ button.className = 'theme-mode-option';
253
+ button.setAttribute('role', 'radio');
254
+ button.setAttribute('data-mode-option', mode);
255
+ button.setAttribute('aria-checked', 'false');
256
+ button.innerHTML = '<span aria-hidden="true">' + COLOR_MODES[mode].shortLabel + '</span>' +
257
+ '<strong>' + COLOR_MODES[mode].label + '</strong>';
258
+ button.addEventListener('click', function() {
259
+ applyAppearance(activeTheme, mode);
260
+ });
261
+ container.appendChild(button);
262
+ });
263
+ }
264
+
265
+ function initAppearanceControls() {
266
+ setAppearanceAttributes(activeTheme, activeMode);
267
+ var styleToggle = document.getElementById('theme-style-toggle');
268
+ var modeToggle = document.getElementById('theme-toggle');
269
+ var popover = document.getElementById('theme-popover');
270
+ var themeOptions = document.getElementById('theme-options');
271
+ var modeOptions = document.getElementById('theme-mode-options');
272
+
273
+ if (themeOptions) buildThemeOptions(themeOptions);
274
+ if (modeOptions) buildModeOptions(modeOptions);
275
+
276
+ function setPopoverOpen(open) {
277
+ if (!styleToggle || !popover) return;
278
+ popover.hidden = !open;
279
+ styleToggle.setAttribute('aria-expanded', String(open));
280
+ if (open) {
281
+ var activeOption = popover.querySelector('.theme-option.is-active');
282
+ if (activeOption) activeOption.focus();
102
283
  }
103
284
  }
104
285
 
105
- // 一键循环到下一形态
106
- function cycleTheme() {
107
- const current = document.body.getAttribute('data-theme-mode') || savedTheme;
108
- const idx = themeCycle.indexOf(current);
109
- const nextIdx = idx >= 0 ? (idx + 1) % themeCycle.length : 0;
110
- applyTheme(themeCycle[nextIdx]);
286
+ if (styleToggle && popover) {
287
+ styleToggle.addEventListener('click', function(event) {
288
+ event.stopPropagation();
289
+ setPopoverOpen(popover.hidden);
290
+ });
291
+ popover.addEventListener('click', function(event) {
292
+ event.stopPropagation();
293
+ });
294
+ document.addEventListener('click', function() { setPopoverOpen(false); });
295
+ document.addEventListener('keydown', function(event) {
296
+ if (event.key === 'Escape') {
297
+ setPopoverOpen(false);
298
+ styleToggle.focus();
299
+ }
300
+ });
111
301
  }
112
302
 
113
- if (window.matchMedia && !window._themeMediaQueryBound) {
114
- const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
115
- mediaQuery.addEventListener('change', function() {
116
- const currentMode = document.body.getAttribute('data-theme-mode') || savedTheme;
117
- if (currentMode === 'auto') {
118
- applyTheme('auto', true);
119
- }
303
+ if (modeToggle) {
304
+ modeToggle.addEventListener('click', function() {
305
+ var index = MODE_CYCLE.indexOf(activeMode);
306
+ applyAppearance(activeTheme, MODE_CYCLE[(index + 1) % MODE_CYCLE.length]);
120
307
  });
121
- window._themeMediaQueryBound = true;
122
308
  }
123
309
 
124
- applyTheme(savedTheme);
310
+ if (window.matchMedia && !window.__docnestThemeMediaQueryBound) {
311
+ var query = window.matchMedia('(prefers-color-scheme: dark)');
312
+ query.addEventListener('change', function() {
313
+ if (activeMode === 'auto') applyAppearance(activeTheme, 'auto', { persist: false });
314
+ });
315
+ window.__docnestThemeMediaQueryBound = true;
316
+ }
125
317
 
126
- themeToggle.addEventListener('click', function(e) {
127
- e.stopPropagation();
128
- cycleTheme();
129
- });
318
+ applyAppearance(activeTheme, activeMode, { persist: false });
130
319
  }
131
-
132
- // 如果 DOM 已经加载完成,立即执行;否则等待 DOMContentLoaded
320
+
321
+ window.DocNestAppearance = {
322
+ get: function() {
323
+ return { theme: activeTheme, mode: activeMode, effectiveMode: getEffectiveMode(activeMode) };
324
+ },
325
+ setTheme: function(theme) { applyAppearance(theme, activeMode); },
326
+ setMode: function(mode) { applyAppearance(activeTheme, mode); },
327
+ themes: Object.keys(THEME_PROFILES)
328
+ };
329
+
133
330
  if (document.readyState === 'loading') {
134
- document.addEventListener('DOMContentLoaded', initTheme);
331
+ document.addEventListener('DOMContentLoaded', initAppearanceControls);
135
332
  } else {
136
- initTheme();
333
+ initAppearanceControls();
137
334
  }
138
335
  })();
@@ -8,7 +8,9 @@
8
8
  <link id="markdown-theme" rel="stylesheet" href="/vendor/github-markdown-css/github-markdown-light.css?v=<%= assetVersion %>">
9
9
  <link rel="stylesheet" href="/static/css/styles.css?v=<%= assetVersion %>">
10
10
  <link rel="stylesheet" href="/static/css/highlight.css?v=<%= assetVersion %>">
11
+ <link rel="stylesheet" href="/static/css/themes.css?v=<%= assetVersion %>">
11
12
  <script src="/mermaid/mermaid.min.js?v=<%= assetVersion %>"></script>
13
+ <%- include('partials/appearance-head') %>
12
14
  <script src="/static/js/core/theme.js?v=<%= assetVersion %>"></script>
13
15
  <script src="/static/js/core/sidebar.js?v=<%= assetVersion %>"></script>
14
16
  </head>
@@ -8,13 +8,9 @@
8
8
  <link id="markdown-theme" rel="stylesheet" href="/vendor/github-markdown-css/github-markdown-light.css?v=<%= assetVersion %>">
9
9
  <link rel="stylesheet" href="/static/css/styles.css?v=<%= assetVersion %>">
10
10
  <link rel="stylesheet" href="/static/css/highlight.css?v=<%= assetVersion %>">
11
+ <link rel="stylesheet" href="/static/css/themes.css?v=<%= assetVersion %>">
11
12
  <script src="/mermaid/mermaid.min.js?v=<%= assetVersion %>"></script>
12
- <script>
13
- window.DOCNEST_CONFIG = {
14
- watermarkEnabled: <%= docsWatermarkEnabled ? 'true' : 'false' %>,
15
- watermarkText: <%- JSON.stringify(docsWatermarkText) %>
16
- };
17
- </script>
13
+ <%- include('partials/appearance-head') %>
18
14
  <!-- 基础功能模块 -->
19
15
  <script src="/static/js/core/theme.js?v=<%= assetVersion %>"></script>
20
16
  <script src="/static/js/core/mermaid-init.js?v=<%= assetVersion %>"></script>
@@ -8,13 +8,9 @@
8
8
  <link id="markdown-theme" rel="stylesheet" href="/vendor/github-markdown-css/github-markdown-light.css?v=<%= assetVersion %>">
9
9
  <link rel="stylesheet" href="/static/css/styles.css?v=<%= assetVersion %>">
10
10
  <link rel="stylesheet" href="/static/css/highlight.css?v=<%= assetVersion %>">
11
+ <link rel="stylesheet" href="/static/css/themes.css?v=<%= assetVersion %>">
11
12
  <script src="/mermaid/mermaid.min.js?v=<%= assetVersion %>"></script>
12
- <script>
13
- window.DOCNEST_CONFIG = {
14
- watermarkEnabled: <%= docsWatermarkEnabled ? 'true' : 'false' %>,
15
- watermarkText: <%- JSON.stringify(docsWatermarkText) %>
16
- };
17
- </script>
13
+ <%- include('partials/appearance-head') %>
18
14
  <!-- 基础功能模块 -->
19
15
  <script src="/static/js/core/theme.js?v=<%= assetVersion %>"></script>
20
16
  <script src="/static/js/core/mermaid-init.js?v=<%= assetVersion %>"></script>
@@ -8,6 +8,8 @@
8
8
  <link id="markdown-theme" rel="stylesheet" href="/vendor/github-markdown-css/github-markdown-light.css?v=<%= assetVersion %>">
9
9
  <link rel="stylesheet" href="/static/css/styles.css?v=<%= assetVersion %>">
10
10
  <link rel="stylesheet" href="/static/css/highlight.css?v=<%= assetVersion %>">
11
+ <link rel="stylesheet" href="/static/css/themes.css?v=<%= assetVersion %>">
12
+ <%- include('partials/appearance-head') %>
11
13
  <script src="/static/js/core/theme.js?v=<%= assetVersion %>"></script>
12
14
  <script src="/static/js/core/sidebar.js?v=<%= assetVersion %>"></script>
13
15
  </head>
@@ -0,0 +1,12 @@
1
+ <script>
2
+ window.DOCNEST_CONFIG = {
3
+ watermarkEnabled: <%= docsWatermarkEnabled ? 'true' : 'false' %>,
4
+ watermarkText: <%- JSON.stringify(docsWatermarkText) %>,
5
+ storageKeyPrefix: <%- JSON.stringify(docsStorageKeyPrefix) %>,
6
+ appearance: {
7
+ defaultTheme: <%- JSON.stringify(docsThemeDefault) %>,
8
+ defaultMode: <%- JSON.stringify(docsThemeModeDefault) %>,
9
+ enabledThemes: <%- JSON.stringify(docsThemeEnabled) %>
10
+ }
11
+ };
12
+ </script>
@@ -2,7 +2,7 @@
2
2
  <div class="header-content">
3
3
  <div class="header-title-group">
4
4
  <h1>
5
- <% if (docsLogo) { %><img class="brand-logo" src="<%= docsLogo %>" alt=""><% } else { %>📚<% } %>
5
+ <% if (docsLogo) { %><img class="brand-logo" src="<%= docsLogo %>" alt=""><% } else { %><span class="brand-fallback" aria-hidden="true"><span class="brand-fallback-emoji">📚</span><span class="brand-fallback-shape"></span></span><% } %>
6
6
  <%= docsTitle %>
7
7
  </h1>
8
8
  <% if (typeof showBackLink !== 'undefined' && showBackLink) { %>
@@ -25,12 +25,35 @@
25
25
  <span class="header-action-text">下载 PDF</span>
26
26
  </button>
27
27
  <% } %>
28
- <div class="theme-selector">
29
- <button id="theme-toggle" class="header-action-btn theme-toggle" title="点击切换:跟随系统 / 浅色 / 深色">
30
- <span class="theme-icon">💻</span>
31
- <span class="theme-text">跟随系统</span>
28
+ <div class="theme-selector" id="theme-selector">
29
+ <button
30
+ id="theme-style-toggle"
31
+ class="header-action-btn theme-style-toggle"
32
+ type="button"
33
+ aria-haspopup="dialog"
34
+ aria-expanded="false"
35
+ aria-controls="theme-popover"
36
+ title="选择文档中心主题"
37
+ >
38
+ <span class="theme-swatch" aria-hidden="true"></span>
39
+ <span class="theme-style-text">Slate Modern</span>
40
+ <span class="theme-chevron" aria-hidden="true">⌄</span>
32
41
  </button>
42
+ <div class="theme-popover" id="theme-popover" role="dialog" aria-label="外观设置" hidden>
43
+ <div class="theme-popover-section">
44
+ <p class="theme-popover-label">主题风格</p>
45
+ <div class="theme-options" id="theme-options" role="radiogroup" aria-label="主题风格"></div>
46
+ </div>
47
+ <div class="theme-popover-section theme-mode-section">
48
+ <p class="theme-popover-label">明暗模式</p>
49
+ <div class="theme-mode-options" id="theme-mode-options" role="radiogroup" aria-label="明暗模式"></div>
50
+ </div>
51
+ </div>
33
52
  </div>
53
+ <button id="theme-toggle" class="header-action-btn theme-toggle" type="button" title="切换明暗模式">
54
+ <span class="theme-icon" aria-hidden="true">A</span>
55
+ <span class="theme-text">跟随系统</span>
56
+ </button>
34
57
  </div>
35
58
  </div>
36
59
  </div>