docnest 0.1.24 → 0.1.26

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.
package/README.md CHANGED
@@ -58,20 +58,31 @@ export default defineConfig({
58
58
 
59
59
  ### 文档搜索
60
60
 
61
- 普通阅读模式下,页面顶部提供文档搜索入口,也可以使用 `⌘K`(macOS)或
61
+ 整理目录时可以把文档移动到原目录下的分类文件夹。旧地址会在原目录子树中按完全相同的文件名查找,只有唯一匹配时才跳转;重名、目录不存在或读取失败时保留 404,避免打开错误的文档。直接访问现有路径始终优先。
62
+
63
+ 文档搜索默认开启,与 `restrictedMode` 独立。页面顶部提供搜索入口,也可以使用 `⌘K`(macOS)或
62
64
  `Ctrl K`(Windows/Linux)打开。搜索索引在首次打开搜索时才加载,覆盖文档标题、目录路径、标题层级、正文和代码块;中文按连续字符匹配,英文和带空格的查询按多个词匹配。结果按标题、目录和正文命中位置综合排序,并显示安全的文本摘要。
63
65
 
64
66
  搜索完全基于宿主项目自己的 Markdown 内容,不依赖 Algolia 或其他外部服务。动态服务通过同源
65
67
  `/search-index.json` 提供索引;静态站点需要把同一份响应保存为站点根目录的
66
68
  `search-index.json`,并保留页面上的静态站点标记,让结果链接指向 `/doc/<path>.html`。
67
- 受限模式不生成搜索索引,也不显示搜索入口,避免摘要绕过阅读限制。
69
+ 只有 `search.enabled: false` 才关闭搜索入口、快捷键与索引接口;省略该项时包括受限模式在内均默认开启。索引继续经过原有授权检查;当前实现将可检索的 Markdown 文本索引交给已授权浏览器处理,不索引嵌入 HTML 的正文。
70
+
71
+ ```js
72
+ export default defineConfig({
73
+ search: { enabled: true },
74
+ restrictedMode: true,
75
+ })
76
+ ```
77
+
78
+ `search.enabled` 只接受布尔值。使用 CLI 时由配置文件解析并传递给运行时;直接嵌入 `docnest/server` 时可通过 `DOCS_SEARCH_ENABLED=true|false` 设置,缺省为开启。
68
79
 
69
80
  搜索框为空时显示最近搜索:仅记录有结果的关键词,去重后保留最近 10 条,点击即可再次搜索。
70
81
  历史保存在当前浏览器,按 `site.storageKeyPrefix` 隔离;不上传服务器。
71
82
 
72
83
  多空间托管场景可设置 `DOCS_ALLOW_RAW_HTML=false`,把 Markdown 中的原始 HTML 显示为文本,避免文档内脚本取得宿主页面权限;Markdown 语法与 Mermaid 不受影响。默认保留原始 HTML 兼容行为。
73
84
 
74
- PDF、页面水印和图表外带能力由一个开关统一控制,默认使用普通阅读模式:
85
+ 复制、导出与页面水印由 `restrictedMode` 控制;搜索独立配置,图表放大查看始终可用:
75
86
 
76
87
  ```js
77
88
  export default defineConfig({
@@ -80,9 +91,9 @@ export default defineConfig({
80
91
  ```
81
92
 
82
93
  - `restrictedMode: false`:显示打印和 PDF 导出,允许 Mermaid 图下载与大图查看。导出 PDF 时会弹出本次水印设置,可选择是否添加并自定义文案;默认文案为 `site.title`,不会写回配置。
83
- - `restrictedMode: true`:隐藏打印和 PDF 导出入口且不加载导出模块;使用 `site.title` 作为固定水印,禁止文字选择、复制、剪切和拖拽,并隐藏 Mermaid 下载与大图查看入口。Mermaid 仍会在正文中正常渲染。
94
+ - `restrictedMode: true`:隐藏打印和 PDF 导出入口且不加载导出模块;使用 `site.title` 作为固定水印,禁止文字选择、复制、剪切和拖拽,并隐藏 Mermaid 复制与下载入口。Mermaid 可正常渲染、放大、缩小、重置和全屏查看,查看器内保留水印。
84
95
 
85
- 受限模式只移除 DocNest 提供的页面入口和脚本,不承诺对浏览器开发者工具或系统级截图提供绝对防护。
96
+ 图表查看没有启用/禁用配置项。受限模式只限制 DocNest 提供的复制和导出入口,不承诺对浏览器开发者工具或系统级截图提供绝对防护。
86
97
 
87
98
  页面水印使用 4% 透明度,保持可辨识而不遮挡正文和图表。Mermaid 代码围栏按独立图表渲染,不继承普通代码块的等宽字体;节点内的 HTML 换行与图表字体测量保持一致。
88
99
 
package/dist/cli.js CHANGED
@@ -63,6 +63,7 @@ function exposeConfig(config) {
63
63
  process.env.DOCS_THEME_MODE_DEFAULT = config.appearance.defaultMode;
64
64
  process.env.DOCS_THEME_ENABLED = JSON.stringify(config.appearance.enabledThemes);
65
65
  process.env.DOCS_RESTRICTED_MODE = String(config.restrictedMode);
66
+ process.env.DOCS_SEARCH_ENABLED = String(config.search.enabled);
66
67
  process.env.DOCS_HOST = config.server.host;
67
68
  process.env.DOCS_PORT_START = String(config.server.port);
68
69
  process.env.DOCS_WATCH_ENABLED = String(config.server.watch);
package/dist/config.d.ts CHANGED
@@ -14,6 +14,10 @@ export type DocNestConfig = {
14
14
  navigation?: {
15
15
  rootDirectoryOrder?: string[];
16
16
  };
17
+ search?: {
18
+ /** Search is enabled by default, independently of restrictedMode. */
19
+ enabled?: boolean;
20
+ };
17
21
  appearance?: {
18
22
  defaultTheme?: DocNestThemeName;
19
23
  defaultMode?: DocNestColorMode;
@@ -38,8 +42,8 @@ export type DocNestConfig = {
38
42
  sessionTtlMinutes?: number;
39
43
  };
40
44
  /**
41
- * Restrict the reading experience to in-browser viewing only.
42
- * Restricted mode also enables the fixed site-title watermark.
45
+ * Restrict copying and export, and enable the fixed site-title watermark.
46
+ * Search and diagram viewing are reading capabilities and remain independent.
43
47
  */
44
48
  restrictedMode?: boolean;
45
49
  };
@@ -49,6 +53,9 @@ export type ResolvedDocNestConfig = Required<DocNestConfig> & {
49
53
  docsDir: string;
50
54
  site: Required<NonNullable<DocNestConfig['site']>>;
51
55
  navigation: Required<NonNullable<DocNestConfig['navigation']>>;
56
+ search: {
57
+ enabled: boolean;
58
+ };
52
59
  appearance: Required<NonNullable<DocNestConfig['appearance']>>;
53
60
  server: Required<NonNullable<DocNestConfig['server']>>;
54
61
  auth: {
package/dist/config.js CHANGED
@@ -98,6 +98,11 @@ async function loadConfig(projectRoot = process.cwd()) {
98
98
  const projectName = await readProjectName(projectRoot);
99
99
  const site = userConfig.site ?? {};
100
100
  const navigation = userConfig.navigation ?? {};
101
+ const search = userConfig.search ?? {};
102
+ if (typeof search !== 'object' || Array.isArray(search) ||
103
+ (search.enabled !== undefined && typeof search.enabled !== 'boolean')) {
104
+ throw new Error('search.enabled must be a boolean');
105
+ }
101
106
  const appearance = userConfig.appearance ?? {};
102
107
  const server = userConfig.server ?? {};
103
108
  const auth = userConfig.auth ?? {};
@@ -117,6 +122,7 @@ async function loadConfig(projectRoot = process.cwd()) {
117
122
  ? navigation.rootDirectoryOrder.filter((item) => typeof item === 'string')
118
123
  : [],
119
124
  },
125
+ search: { enabled: search.enabled ?? true },
120
126
  appearance: resolveAppearance(appearance),
121
127
  server: {
122
128
  host: server.host?.trim() || '127.0.0.1',
package/dist/server.js CHANGED
@@ -55,6 +55,11 @@ const DOCS_THEME_ENABLED = (() => {
55
55
  }
56
56
  })();
57
57
  const DOCS_RESTRICTED_MODE = process.env.DOCS_RESTRICTED_MODE === 'true';
58
+ if (process.env.DOCS_SEARCH_ENABLED !== undefined &&
59
+ !['true', 'false'].includes(process.env.DOCS_SEARCH_ENABLED)) {
60
+ throw new Error('DOCS_SEARCH_ENABLED must be true or false');
61
+ }
62
+ const DOCS_SEARCH_ENABLED = process.env.DOCS_SEARCH_ENABLED !== 'false';
58
63
  const DOCS_PDF_ENABLED = !DOCS_RESTRICTED_MODE;
59
64
  const DOCS_PAGE_WATERMARK_TEXT = DOCS_TITLE;
60
65
  const DOCS_DIR = path.resolve(process.env.DOCS_ROOT || path.join(process.cwd(), 'docs'));
@@ -110,6 +115,7 @@ app.locals.docsThemeDefault = DOCS_THEME_DEFAULT;
110
115
  app.locals.docsThemeModeDefault = DOCS_THEME_MODE_DEFAULT;
111
116
  app.locals.docsThemeEnabled = DOCS_THEME_ENABLED;
112
117
  app.locals.docsRestrictedMode = DOCS_RESTRICTED_MODE;
118
+ app.locals.docsSearchEnabled = DOCS_SEARCH_ENABLED;
113
119
  app.locals.docsPageWatermarkText = DOCS_PAGE_WATERMARK_TEXT;
114
120
  app.locals.docsAuthEnabled = BUILT_IN_AUTH_ENABLED;
115
121
  app.locals.docsAuthStorageKey = AUTH_CONFIG.localStorageKey || `${DOCS_STORAGE_KEY_PREFIX}:auth-passphrase`;
@@ -526,7 +532,7 @@ function invalidateSearchIndex() {
526
532
  searchIndexCache = null;
527
533
  }
528
534
  app.get('/search-index.json', async (req, res) => {
529
- if (DOCS_RESTRICTED_MODE) {
535
+ if (!DOCS_SEARCH_ENABLED) {
530
536
  return res.status(404).json({ ok: false, error: '搜索功能已关闭。' });
531
537
  }
532
538
  try {
@@ -719,7 +725,10 @@ async function resolveDocPath(requestedPath) {
719
725
  if (await storageFileExists(resolvedPath)) {
720
726
  return { resolvedPath };
721
727
  }
722
- return null;
728
+ // A document may have moved into a subdirectory during catalog cleanup.
729
+ // Resolve only an unambiguous filename inside its original directory.
730
+ const movedPath = await findFileInTree(resolvedDirPath, fileName);
731
+ return movedPath ? { resolvedPath: movedPath } : null;
723
732
  }
724
733
  // 如果只是文件名,尝试在整个文档目录中查找
725
734
  try {
@@ -733,29 +742,32 @@ async function resolveDocPath(requestedPath) {
733
742
  }
734
743
  return null;
735
744
  }
736
- // 在目录树中查找文件
737
- async function findFileInTree(dir, fileName, basePath = '') {
738
- try {
739
- const effectiveBasePath = basePath || dir || '';
740
- const entries = await storageReadDir(effectiveBasePath);
745
+ // Find one exact filename; ambiguity or an unreadable subtree must not redirect.
746
+ async function findFileInTree(dir, fileName) {
747
+ const matches = [];
748
+ async function visit(basePath) {
749
+ const entries = await storageReadDir(basePath);
741
750
  for (const entry of entries) {
742
751
  if (entry.name.startsWith('.'))
743
752
  continue;
744
- const relativePath = joinDocPath(effectiveBasePath, entry.name);
753
+ const relativePath = joinDocPath(basePath, entry.name);
745
754
  if (entry.isDirectory) {
746
- const found = await findFileInTree('', fileName, relativePath);
747
- if (found)
748
- return found;
755
+ await visit(relativePath);
749
756
  }
750
757
  else if (entry.isFile && entry.name === fileName && entry.name.endsWith('.md')) {
751
- return relativePath;
758
+ matches.push(relativePath);
752
759
  }
760
+ if (matches.length > 1)
761
+ return;
753
762
  }
754
763
  }
764
+ try {
765
+ await visit(dir || '');
766
+ }
755
767
  catch (e) {
756
- // 忽略错误
768
+ return null;
757
769
  }
758
- return null;
770
+ return matches.length === 1 ? matches[0] : null;
759
771
  }
760
772
  // 构建面包屑:基于文档路径
761
773
  async function buildBreadcrumbs(docPath) {
@@ -43,7 +43,10 @@ export default defineConfig({
43
43
  localStorageKey: 'my-project:auth-passphrase',
44
44
  sessionTtlMinutes: 24 * 60,
45
45
  },
46
- // 受限阅读模式;默认关闭。开启后隐藏打印/PDF、Mermaid 下载和大图查看,
46
+ // 搜索默认开启,独立于受限模式;设为 false 时同时关闭入口和索引。
47
+ search: { enabled: true },
48
+ // 受限阅读模式;默认关闭。开启后隐藏打印/PDF 和 Mermaid 复制、下载,
47
49
  // 并使用 site.title 显示固定水印,同时禁止页面文字选择与复制。
50
+ // 图表放大查看始终可用,没有单独配置项。
48
51
  restrictedMode: false,
49
52
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docnest",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "可复用的本地 Markdown 文档中心",
5
5
  "repository": {
6
6
  "type": "git",
@@ -59,13 +59,7 @@ html.docnest-page-protected body [contenteditable="true"] {
59
59
  user-select: text !important;
60
60
  }
61
61
 
62
- /* Restricted mode keeps Mermaid inline but removes every affordance for opening
63
- * or exporting a larger copy of the diagram. */
64
- html.docnest-restricted .markdown-body .mermaid {
65
- cursor: default;
66
- }
67
-
68
- html.docnest-restricted .markdown-body .mermaid::after,
62
+ /* Diagram viewing is always available; restricted mode only hides export. */
69
63
  html.docnest-restricted .markdown-body .mermaid-download-btn {
70
64
  display: none !important;
71
65
  }
@@ -88,7 +88,10 @@
88
88
  function getAssetUrl(path) {
89
89
  var base = document.querySelector('base');
90
90
  if (base && base.getAttribute('href')) {
91
- return base.getAttribute('href').replace(/\/?$/, '/') + path.replace(/^\//, '');
91
+ var prefix = base.getAttribute('href').replace(/\/?$/, '/');
92
+ // A mounting proxy may have already rewritten a root-relative link.
93
+ if (path.indexOf(prefix) === 0) return path;
94
+ return prefix + path.replace(/^\//, '');
92
95
  }
93
96
  return path;
94
97
  }
@@ -510,7 +513,7 @@
510
513
 
511
514
  function init() {
512
515
  var config = window.DOCNEST_CONFIG || {};
513
- if (config.restrictedMode === true) return;
516
+ if (config.searchEnabled === false) return;
514
517
  var trigger = document.getElementById('doc-search-trigger');
515
518
  if (!trigger) return;
516
519
 
@@ -3,6 +3,7 @@ let currentZoom = 1;
3
3
  let defaultZoom = 1;
4
4
  let currentSvg = null;
5
5
  let isFullscreen = false;
6
+ let viewerPreviousFocus = null;
6
7
 
7
8
  function isDocNestRestrictedMode() {
8
9
  return !!(window.DOCNEST_CONFIG && window.DOCNEST_CONFIG.restrictedMode === true);
@@ -44,45 +45,42 @@ function getDiagramIntrinsicDimensions(svgElement) {
44
45
  }
45
46
 
46
47
  function setupDiagramViewer() {
47
- if (isDocNestRestrictedMode()) return;
48
48
  const diagrams = document.querySelectorAll('.markdown-body .mermaid');
49
49
  diagrams.forEach((diagram, index) => {
50
- // 添加下载按钮
51
- const downloadBtn = document.createElement('button');
52
- downloadBtn.className = 'mermaid-download-btn';
53
- downloadBtn.innerHTML = '⬇️ 下载';
54
- downloadBtn.title = '下载图表';
55
- downloadBtn.onclick = function(e) {
56
- e.stopPropagation(); // 阻止触发点击查看大图
57
- const svg = diagram.querySelector('svg');
58
- if (svg) {
59
- if (typeof showDownloadMenu === 'function') {
50
+ let downloadBtn = diagram.querySelector('.mermaid-download-btn');
51
+ if (isDocNestRestrictedMode()) {
52
+ if (downloadBtn) downloadBtn.remove();
53
+ downloadBtn = null;
54
+ } else if (!downloadBtn) {
55
+ downloadBtn = document.createElement('button');
56
+ downloadBtn.className = 'mermaid-download-btn';
57
+ downloadBtn.innerHTML = '⬇️ 下载';
58
+ downloadBtn.title = '下载图表';
59
+ downloadBtn.onclick = function(e) {
60
+ e.stopPropagation();
61
+ const svg = diagram.querySelector('svg');
62
+ if (svg && typeof showDownloadMenu === 'function') {
60
63
  showDownloadMenu(svg.cloneNode(true), `diagram-${index + 1}`);
61
64
  }
62
- }
63
- };
64
- diagram.appendChild(downloadBtn);
65
-
66
- // 点击查看大图
67
- diagram.addEventListener('click', function(e) {
68
- // 如果点击的是下载按钮,不打开查看器
69
- if (e.target === downloadBtn || downloadBtn.contains(e.target)) {
70
- return;
71
- }
65
+ };
66
+ diagram.appendChild(downloadBtn);
67
+ }
68
+
69
+ // Repeated renders must not accumulate viewer handlers or download buttons.
70
+ diagram.onclick = function(e) {
71
+ if (downloadBtn && (e.target === downloadBtn || downloadBtn.contains(e.target))) return;
72
72
  const svg = diagram.querySelector('svg');
73
- if (svg) {
74
- openDiagramViewer(svg.cloneNode(true));
75
- }
76
- });
73
+ if (svg) openDiagramViewer(svg.cloneNode(true));
74
+ };
77
75
  });
78
76
  }
79
77
 
80
78
  function openDiagramViewer(svgElement) {
81
- if (isDocNestRestrictedMode()) return;
82
79
  const viewer = document.getElementById('diagram-viewer');
83
80
  const container = document.getElementById('diagram-viewer-svg-container');
84
81
 
85
82
  if (!viewer || !container) return;
83
+ viewerPreviousFocus = document.activeElement;
86
84
 
87
85
  // viewBox 才是图表的实际尺寸;不能把 Mermaid 的 width="100%" 解析成 100。
88
86
  const { width: originalWidth, height: originalHeight } = getDiagramIntrinsicDimensions(svgElement);
@@ -159,6 +157,7 @@ function openDiagramViewer(svgElement) {
159
157
 
160
158
  viewer.classList.add('show');
161
159
  document.body.style.overflow = 'hidden';
160
+ viewer.querySelector('.diagram-viewer-close')?.focus();
162
161
 
163
162
  // 重置全屏状态
164
163
  isFullscreen = false;
@@ -207,6 +206,8 @@ function closeDiagramViewer() {
207
206
  if (typeof removeModal === 'function') {
208
207
  removeModal('diagram-viewer');
209
208
  }
209
+ viewerPreviousFocus?.focus?.();
210
+ viewerPreviousFocus = null;
210
211
  }
211
212
 
212
213
  function resetDiagramZoom() {
@@ -39,14 +39,14 @@
39
39
  </main>
40
40
  </div>
41
41
  </div>
42
+ <%- include('partials/diagram-viewer') %>
43
+ <!-- 按依赖顺序加载模块 -->
44
+ <script src="/static/js/utils/modal-stack.js?v=<%= assetVersion %>"></script>
45
+ <script src="/static/js/diagram/diagram-viewer.js?v=<%= assetVersion %>"></script>
46
+ <script src="/static/js/diagram/diagram-zoom.js?v=<%= assetVersion %>"></script>
47
+ <script src="/static/js/diagram/diagram-drag.js?v=<%= assetVersion %>"></script>
48
+ <script src="/static/js/diagram/diagram-controls.js?v=<%= assetVersion %>"></script>
42
49
  <% if (!docsRestrictedMode) { %>
43
- <%- include('partials/diagram-viewer') %>
44
- <!-- 按依赖顺序加载模块 -->
45
- <script src="/static/js/utils/modal-stack.js?v=<%= assetVersion %>"></script>
46
- <script src="/static/js/diagram/diagram-viewer.js?v=<%= assetVersion %>"></script>
47
- <script src="/static/js/diagram/diagram-zoom.js?v=<%= assetVersion %>"></script>
48
- <script src="/static/js/diagram/diagram-drag.js?v=<%= assetVersion %>"></script>
49
- <script src="/static/js/diagram/diagram-controls.js?v=<%= assetVersion %>"></script>
50
50
  <script src="/static/js/diagram/diagram-download.js?v=<%= assetVersion %>"></script>
51
51
  <% } %>
52
52
  <script src="/static/js/utils/app.js?v=<%= assetVersion %>"></script>
@@ -30,14 +30,14 @@
30
30
  </main>
31
31
  </div>
32
32
  </div>
33
+ <%- include('partials/diagram-viewer') %>
34
+ <!-- 按依赖顺序加载模块 -->
35
+ <script src="/static/js/utils/modal-stack.js?v=<%= assetVersion %>"></script>
36
+ <script src="/static/js/diagram/diagram-viewer.js?v=<%= assetVersion %>"></script>
37
+ <script src="/static/js/diagram/diagram-zoom.js?v=<%= assetVersion %>"></script>
38
+ <script src="/static/js/diagram/diagram-drag.js?v=<%= assetVersion %>"></script>
39
+ <script src="/static/js/diagram/diagram-controls.js?v=<%= assetVersion %>"></script>
33
40
  <% if (!docsRestrictedMode) { %>
34
- <%- include('partials/diagram-viewer') %>
35
- <!-- 按依赖顺序加载模块 -->
36
- <script src="/static/js/utils/modal-stack.js?v=<%= assetVersion %>"></script>
37
- <script src="/static/js/diagram/diagram-viewer.js?v=<%= assetVersion %>"></script>
38
- <script src="/static/js/diagram/diagram-zoom.js?v=<%= assetVersion %>"></script>
39
- <script src="/static/js/diagram/diagram-drag.js?v=<%= assetVersion %>"></script>
40
- <script src="/static/js/diagram/diagram-controls.js?v=<%= assetVersion %>"></script>
41
41
  <script src="/static/js/diagram/diagram-download.js?v=<%= assetVersion %>"></script>
42
42
  <% } %>
43
43
  <script src="/static/js/utils/app.js?v=<%= assetVersion %>"></script>
@@ -4,6 +4,7 @@
4
4
  authStorageKey: <%- JSON.stringify(docsAuthStorageKey) %>,
5
5
  siteTitle: <%- JSON.stringify(docsTitle) %>,
6
6
  restrictedMode: <%= docsRestrictedMode ? 'true' : 'false' %>,
7
+ searchEnabled: <%= docsSearchEnabled ? 'true' : 'false' %>,
7
8
  storageKeyPrefix: <%- JSON.stringify(docsStorageKeyPrefix) %>,
8
9
  appearance: {
9
10
  defaultTheme: <%- JSON.stringify(docsThemeDefault) %>,
@@ -1,15 +1,24 @@
1
1
  <!-- 图表查看器模态框 -->
2
- <div id="diagram-viewer" class="diagram-viewer">
2
+ <div id="diagram-viewer" class="diagram-viewer" role="dialog" aria-modal="true" aria-label="图表查看器">
3
+ <% if (docsRestrictedMode) { %>
4
+ <div class="docnest-page-watermark" aria-hidden="true">
5
+ <% for (let index = 0; index < 12; index += 1) { %>
6
+ <span class="docnest-page-watermark__item"><%= docsPageWatermarkText %></span>
7
+ <% } %>
8
+ </div>
9
+ <% } %>
3
10
  <div class="diagram-viewer-content">
4
- <button class="diagram-viewer-close" onclick="closeDiagramViewer()">×</button>
11
+ <button class="diagram-viewer-close" type="button" aria-label="关闭图表查看器" onclick="closeDiagramViewer()">×</button>
5
12
  <div class="diagram-viewer-controls">
6
13
  <button onclick="zoomDiagram('out')">缩小</button>
7
14
  <span class="zoom-info" id="zoom-info">100%</span>
8
15
  <button onclick="zoomDiagram('in')">放大</button>
9
16
  <button onclick="resetDiagramZoom()">重置</button>
10
17
  <button onclick="toggleFullscreen()" class="fullscreen-btn" id="fullscreen-btn">⛶ 全屏</button>
11
- <button onclick="copyCurrentDiagram()" class="copy-btn">📋 复制</button>
12
- <button onclick="downloadCurrentDiagram()" class="download-btn">⬇️ 下载</button>
18
+ <% if (!docsRestrictedMode) { %>
19
+ <button onclick="copyCurrentDiagram()" class="copy-btn">📋 复制</button>
20
+ <button onclick="downloadCurrentDiagram()" class="download-btn">⬇️ 下载</button>
21
+ <% } %>
13
22
  </div>
14
23
  <div id="diagram-viewer-svg-container" class="diagram-viewer-svg-container"></div>
15
24
  </div>
@@ -1,5 +1,5 @@
1
1
  <header class="header">
2
- <div class="header-content<%= !docsRestrictedMode ? ' header-content-with-search' : '' %>">
2
+ <div class="header-content<%= docsSearchEnabled ? ' header-content-with-search' : '' %>">
3
3
  <div class="header-title-group">
4
4
  <h1>
5
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><% } %>
@@ -11,7 +11,7 @@
11
11
  <p class="subtitle"><%= docsSubtitle %></p>
12
12
  <% } %>
13
13
  </div>
14
- <% if (!docsRestrictedMode) { %>
14
+ <% if (docsSearchEnabled) { %>
15
15
  <button type="button" id="doc-search-trigger" class="header-action-btn doc-search-trigger" aria-haspopup="dialog" aria-controls="doc-search-modal" title="搜索文档(⌘K / Ctrl K)">
16
16
  <span class="doc-search-icon" aria-hidden="true">⌕</span>
17
17
  <span class="header-action-text">搜索文档</span>
@@ -65,7 +65,7 @@
65
65
  </div>
66
66
  </div>
67
67
  </header>
68
- <% if (!docsRestrictedMode) { %>
68
+ <% if (docsSearchEnabled) { %>
69
69
  <script src="/static/js/core/doc-search.js?v=<%= typeof assetVersion !== 'undefined' ? assetVersion : 'dev' %>"></script>
70
70
  <% } %>
71
71
  <% if (docsAuthEnabled) { %>