@pagenary/publisher 2026.5.0

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.
Files changed (147) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +337 -0
  3. package/bin/pagenary.mjs +116 -0
  4. package/build.config.json +5 -0
  5. package/package.json +66 -0
  6. package/scripts/build-site.js +87 -0
  7. package/scripts/build-tenants.js +3569 -0
  8. package/scripts/build.js +99 -0
  9. package/scripts/generate-sections.js +41 -0
  10. package/scripts/lib/seo-generator.js +558 -0
  11. package/scripts/lint-content.js +62 -0
  12. package/scripts/seo-smoke.js +94 -0
  13. package/scripts/serve.js +142 -0
  14. package/site/app.js +1 -0
  15. package/site/index.html +57 -0
  16. package/site/lib/categories.js +1 -0
  17. package/site/lib/export.js +1 -0
  18. package/site/lib/manifest-utils.js +1 -0
  19. package/site/lib/router.js +1 -0
  20. package/site/lib/search.js +1 -0
  21. package/site/llms.txt +22 -0
  22. package/site/manifest.js +132 -0
  23. package/site/mermaid-init.js +1 -0
  24. package/site/pages/api.html +339 -0
  25. package/site/pages/architecture.html +303 -0
  26. package/site/pages/deployment.html +282 -0
  27. package/site/pages/developer-guide.html +157 -0
  28. package/site/pages/extending.html +135 -0
  29. package/site/pages/quickstart.html +318 -0
  30. package/site/pages/seo-strategy.html +121 -0
  31. package/site/pages/tenant-config.html +519 -0
  32. package/site/pages/welcome.html +116 -0
  33. package/site/robots.txt +10 -0
  34. package/site/sections/api.js +3 -0
  35. package/site/sections/architecture.js +3 -0
  36. package/site/sections/deployment.js +3 -0
  37. package/site/sections/developer-guide.js +3 -0
  38. package/site/sections/extending.js +3 -0
  39. package/site/sections/quickstart.js +3 -0
  40. package/site/sections/section-templates.js +1 -0
  41. package/site/sections/seo-strategy.js +3 -0
  42. package/site/sections/tenant-config.js +3 -0
  43. package/site/sections/welcome.js +3 -0
  44. package/site/seo.js +1 -0
  45. package/site/sitemap.xml +63 -0
  46. package/site/styles.css +1982 -0
  47. package/site/syntax-highlight.js +1 -0
  48. package/src/app.js +988 -0
  49. package/src/index.html +56 -0
  50. package/src/lib/categories.js +55 -0
  51. package/src/lib/export.js +195 -0
  52. package/src/lib/manifest-utils.js +69 -0
  53. package/src/lib/router.js +44 -0
  54. package/src/lib/search.js +151 -0
  55. package/src/manifest.js +246 -0
  56. package/src/mermaid-init.js +207 -0
  57. package/src/sections/archive-future-roadmap.js +7 -0
  58. package/src/sections/archive-initiative-alpha.js +7 -0
  59. package/src/sections/archive-milestone-records.js +7 -0
  60. package/src/sections/archive-timeline-overview.js +7 -0
  61. package/src/sections/core-technology-compliance-frameworks.js +7 -0
  62. package/src/sections/core-technology-coordination-model.js +7 -0
  63. package/src/sections/core-technology-data-definitions.js +7 -0
  64. package/src/sections/core-technology-hardware-integration.js +7 -0
  65. package/src/sections/core-technology-integrity-controls.js +7 -0
  66. package/src/sections/core-technology-network-topology.js +7 -0
  67. package/src/sections/core-technology-operator-requirements.js +7 -0
  68. package/src/sections/core-technology-overview.js +7 -0
  69. package/src/sections/core-technology-service-interfaces.js +7 -0
  70. package/src/sections/core-technology-synchronization-strategy.js +7 -0
  71. package/src/sections/core-technology-system-foundation.js +7 -0
  72. package/src/sections/developers-api-credentials.js +7 -0
  73. package/src/sections/developers-api-operations.js +7 -0
  74. package/src/sections/developers-api-reference.js +7 -0
  75. package/src/sections/developers-api-websocket.js +7 -0
  76. package/src/sections/developers-automation-blueprints.js +7 -0
  77. package/src/sections/developers-automation-modules.js +7 -0
  78. package/src/sections/developers-automation-patterns.js +7 -0
  79. package/src/sections/developers-deployment-playbook.js +7 -0
  80. package/src/sections/developers-overview.js +7 -0
  81. package/src/sections/developers-scheduling-patterns.js +7 -0
  82. package/src/sections/developers-sdk-go.js +7 -0
  83. package/src/sections/developers-sdk-javascript.js +7 -0
  84. package/src/sections/developers-sdk-python.js +7 -0
  85. package/src/sections/developers-sdk-rust.js +7 -0
  86. package/src/sections/developers-sdks.js +7 -0
  87. package/src/sections/developers-solution-examples.js +7 -0
  88. package/src/sections/developers-testing-framework.js +7 -0
  89. package/src/sections/getting-started-architecture-basics.js +7 -0
  90. package/src/sections/getting-started-introduction.js +7 -0
  91. package/src/sections/getting-started-performance-overview.js +7 -0
  92. package/src/sections/governance-community-initiatives.js +7 -0
  93. package/src/sections/governance-dao-overview.js +7 -0
  94. package/src/sections/governance-multi-token.js +7 -0
  95. package/src/sections/governance-overview.js +7 -0
  96. package/src/sections/governance-proposal-process.js +7 -0
  97. package/src/sections/governance-proposals.js +7 -0
  98. package/src/sections/governance-structure.js +7 -0
  99. package/src/sections/governance-token-distribution.js +7 -0
  100. package/src/sections/governance-treasury.js +7 -0
  101. package/src/sections/operations-environment-prep.js +7 -0
  102. package/src/sections/operations-getting-started.js +7 -0
  103. package/src/sections/operations-incentives-guide.js +7 -0
  104. package/src/sections/operations-incentives-strategies.js +7 -0
  105. package/src/sections/operations-incentives.js +7 -0
  106. package/src/sections/operations-infrastructure.js +7 -0
  107. package/src/sections/operations-monitoring.js +7 -0
  108. package/src/sections/operations-overview.js +7 -0
  109. package/src/sections/operations-performance.js +7 -0
  110. package/src/sections/operations-power-infrastructure.js +7 -0
  111. package/src/sections/operations-setup-guide.js +7 -0
  112. package/src/sections/operations-sync-setup.js +7 -0
  113. package/src/sections/products-flagship-solution.js +7 -0
  114. package/src/sections/products-solution-library.js +7 -0
  115. package/src/sections/resources-brand-assets.js +7 -0
  116. package/src/sections/resources-faq.js +7 -0
  117. package/src/sections/resources-glossary.js +7 -0
  118. package/src/sections/resources-research-papers.js +7 -0
  119. package/src/sections/section-templates.js +873 -0
  120. package/src/sections/security-audits.js +7 -0
  121. package/src/sections/security-best-practices.js +7 -0
  122. package/src/sections/security-bug-bounty.js +7 -0
  123. package/src/sections/security-incident-response.js +7 -0
  124. package/src/sections/security-overview.js +7 -0
  125. package/src/sections/technical-architecture.js +7 -0
  126. package/src/sections/technical-whitepaper.js +7 -0
  127. package/src/sections/tutorial-automation-bot.js +7 -0
  128. package/src/sections/tutorial-build-first-integration.js +7 -0
  129. package/src/sections/tutorial-deploy-automation.js +7 -0
  130. package/src/sections/tutorial-event-driven-experience.js +7 -0
  131. package/src/sections/tutorial-operations-onboarding.js +7 -0
  132. package/src/sections/tutorial-systems-integration.js +7 -0
  133. package/src/sections/tutorials-overview.js +7 -0
  134. package/src/sections/use-case-connected-devices.js +7 -0
  135. package/src/sections/use-case-digital-auctions.js +7 -0
  136. package/src/sections/use-case-financial-automation.js +7 -0
  137. package/src/sections/use-case-interactive-media.js +7 -0
  138. package/src/sections/use-case-realtime-execution.js +7 -0
  139. package/src/sections/use-case-research-analytics.js +7 -0
  140. package/src/sections/use-case-supply-operations.js +7 -0
  141. package/src/sections/use-cases-overview.js +7 -0
  142. package/src/sections/welcome-overview.js +7 -0
  143. package/src/seo.js +90 -0
  144. package/src/styles.css +1982 -0
  145. package/src/syntax-highlight.js +90 -0
  146. package/tenants.json.example +68 -0
  147. package/tenants.schema.json +231 -0
package/src/app.js ADDED
@@ -0,0 +1,988 @@
1
+ import { MANIFEST, DEFAULT_SECTION, findSection, getAdjacentSections, SITE_CONFIG, EXPORT_CONFIG } from './manifest.js';
2
+ import { updateMetaTags } from './seo.js';
3
+ import { escapeRegExp, searchContent, flattenManifest, findPreferredIndex } from './lib/search.js';
4
+ import { resolveTarget as resolveTargetFn, resolveEntry as resolveEntryFn } from './lib/router.js';
5
+ import { composeExportDocument, collectExportableSections } from './lib/export.js';
6
+ import { renderMermaidBlocks } from './mermaid-init.js';
7
+ import { highlightCodeBlocks } from './syntax-highlight.js';
8
+
9
+ const app = document.getElementById('app');
10
+ const nav = document.getElementById('nav');
11
+ const yearMarker = document.getElementById('year');
12
+ const exportBtn = document.getElementById('exportBtn');
13
+ const commandToggle = document.getElementById('commandToggle');
14
+ const commandPalette = document.getElementById('commandPalette');
15
+ const commandInput = document.getElementById('commandInput');
16
+ const commandList = document.getElementById('commandList');
17
+ const mobileMenuToggle = document.getElementById('mobileMenuToggle');
18
+ const sidebar = document.querySelector('.sidebar');
19
+
20
+ const COMMAND_QUERY_KEY = 'docs-toolkit-command-query';
21
+
22
+ const rendered = new Map();
23
+ const navButtons = new Map();
24
+ const navGroups = new Map();
25
+ const expandedGroups = new Set();
26
+ let commandEntries = [];
27
+ let commandIndex = 0;
28
+ let paletteOpen = false;
29
+ let highlightQuery = (localStorage.getItem(COMMAND_QUERY_KEY) || '').trim();
30
+ let pendingHighlightScroll = false;
31
+
32
+ function createExternalLink(item, className) {
33
+ const link = document.createElement('a');
34
+ link.href = item.url;
35
+ link.target = '_blank';
36
+ link.rel = 'noopener noreferrer';
37
+ link.className = `${className} nav-external`;
38
+ link.title = item.summary || item.title;
39
+ link.innerHTML = `
40
+ <span class="nav-title">${item.title}<span class="nav-external-icon" aria-label="(opens in new tab)">↗</span></span>
41
+ ${item.summary ? `<span class="nav-summary">${item.summary}</span>` : ''}
42
+ `;
43
+ return link;
44
+ }
45
+
46
+ function initNav() {
47
+ nav.innerHTML = '';
48
+ navButtons.clear();
49
+ navGroups.clear();
50
+ let groupApplied = expandedGroups.size > 0;
51
+ MANIFEST.forEach((section, index) => {
52
+ // Handle external links at top level
53
+ if (section.url) {
54
+ const link = createExternalLink(section, 'nav-leaf');
55
+ nav.appendChild(link);
56
+ return;
57
+ }
58
+
59
+ if (section.subsections && section.subsections.length) {
60
+ const group = document.createElement('div');
61
+ group.className = 'nav-group';
62
+
63
+ // Check if section has content (module) in addition to subsections
64
+ const hasContent = Boolean(section.module);
65
+
66
+ const parentBtn = document.createElement('button');
67
+ parentBtn.type = 'button';
68
+ parentBtn.className = 'nav-parent' + (hasContent ? ' nav-parent-with-content' : '');
69
+ parentBtn.dataset.section = section.id;
70
+ parentBtn.title = section.summary;
71
+
72
+ if (hasContent) {
73
+ // Section has content - title navigates, arrow toggles
74
+ parentBtn.innerHTML = `
75
+ <span class="nav-title-link">${section.title}</span>
76
+ <span class="nav-expand-toggle" aria-label="Expand"></span>
77
+ ${section.summary ? `<span class="nav-summary">${section.summary}</span>` : ''}
78
+ `;
79
+ // Title click navigates to content
80
+ parentBtn.querySelector('.nav-title-link').addEventListener('click', (e) => {
81
+ e.stopPropagation();
82
+ navigate(section.id, { scrollToHighlight: Boolean(highlightQuery) });
83
+ });
84
+ // Arrow click toggles expansion
85
+ parentBtn.querySelector('.nav-expand-toggle').addEventListener('click', (e) => {
86
+ e.stopPropagation();
87
+ const next = !expandedGroups.has(section.id);
88
+ setGroupExpanded(section.id, next);
89
+ });
90
+ // Button itself does nothing (handled by children)
91
+ parentBtn.addEventListener('click', (e) => {
92
+ // Only toggle if clicking the button background, not title or arrow
93
+ if (e.target === parentBtn) {
94
+ const next = !expandedGroups.has(section.id);
95
+ setGroupExpanded(section.id, next);
96
+ }
97
+ });
98
+ } else {
99
+ // No content - whole button toggles expansion
100
+ parentBtn.innerHTML = `
101
+ <span class="nav-title">${section.title}</span>
102
+ ${section.summary ? `<span class="nav-summary">${section.summary}</span>` : ''}
103
+ `;
104
+ parentBtn.addEventListener('click', () => {
105
+ const next = !expandedGroups.has(section.id);
106
+ setGroupExpanded(section.id, next);
107
+ });
108
+ }
109
+
110
+ const list = document.createElement('div');
111
+ list.className = 'nav-sublist';
112
+ section.subsections.forEach((sub) => {
113
+ // Handle external links in subsections
114
+ if (sub.url) {
115
+ const link = createExternalLink(sub, 'nav-item');
116
+ list.appendChild(link);
117
+ return;
118
+ }
119
+
120
+ // Handle nested subsections (3-level nav)
121
+ if (sub.subsections && sub.subsections.length) {
122
+ const nestedGroup = document.createElement('div');
123
+ nestedGroup.className = 'nav-group nav-group-nested';
124
+
125
+ const nestedParentBtn = document.createElement('button');
126
+ nestedParentBtn.type = 'button';
127
+ nestedParentBtn.className = 'nav-parent nav-parent-nested';
128
+ nestedParentBtn.dataset.section = sub.id;
129
+ nestedParentBtn.title = sub.summary || sub.title;
130
+ nestedParentBtn.innerHTML = `<span class="nav-title">${sub.title}</span>`;
131
+ nestedParentBtn.addEventListener('click', () => {
132
+ const next = !expandedGroups.has(sub.id);
133
+ setGroupExpanded(sub.id, next);
134
+ });
135
+
136
+ const nestedList = document.createElement('div');
137
+ nestedList.className = 'nav-sublist nav-sublist-nested';
138
+ sub.subsections.forEach((nested) => {
139
+ if (nested.url) {
140
+ const link = createExternalLink(nested, 'nav-item');
141
+ nestedList.appendChild(link);
142
+ return;
143
+ }
144
+
145
+ // Handle 4th level of nesting (deeply nested subsections)
146
+ if (nested.subsections && nested.subsections.length) {
147
+ const deepGroup = document.createElement('div');
148
+ deepGroup.className = 'nav-group nav-group-deep';
149
+
150
+ const deepParentBtn = document.createElement('button');
151
+ deepParentBtn.type = 'button';
152
+ deepParentBtn.className = 'nav-parent nav-parent-deep';
153
+ deepParentBtn.dataset.section = nested.id;
154
+ deepParentBtn.title = nested.summary || nested.title;
155
+ deepParentBtn.innerHTML = `<span class="nav-title">${nested.title}</span>`;
156
+ deepParentBtn.addEventListener('click', () => {
157
+ const next = !expandedGroups.has(nested.id);
158
+ setGroupExpanded(nested.id, next);
159
+ });
160
+
161
+ const deepList = document.createElement('div');
162
+ deepList.className = 'nav-sublist nav-sublist-deep';
163
+ nested.subsections.forEach((deep) => {
164
+ if (deep.url) {
165
+ const link = createExternalLink(deep, 'nav-item');
166
+ deepList.appendChild(link);
167
+ return;
168
+ }
169
+
170
+ // Handle 5th level of nesting (ultra-deep subsections)
171
+ if (deep.subsections && deep.subsections.length) {
172
+ const ultraGroup = document.createElement('div');
173
+ ultraGroup.className = 'nav-group nav-group-ultra';
174
+
175
+ const ultraParentBtn = document.createElement('button');
176
+ ultraParentBtn.type = 'button';
177
+ ultraParentBtn.className = 'nav-parent nav-parent-ultra';
178
+ ultraParentBtn.dataset.section = deep.id;
179
+ ultraParentBtn.title = deep.summary || deep.title;
180
+ ultraParentBtn.innerHTML = `<span class="nav-title">${deep.title}</span>`;
181
+ ultraParentBtn.addEventListener('click', () => {
182
+ const next = !expandedGroups.has(deep.id);
183
+ setGroupExpanded(deep.id, next);
184
+ });
185
+
186
+ const ultraList = document.createElement('div');
187
+ ultraList.className = 'nav-sublist nav-sublist-ultra';
188
+ deep.subsections.forEach((ultra) => {
189
+ if (ultra.url) {
190
+ const link = createExternalLink(ultra, 'nav-item');
191
+ ultraList.appendChild(link);
192
+ return;
193
+ }
194
+ const ultraBtn = document.createElement('button');
195
+ ultraBtn.type = 'button';
196
+ ultraBtn.className = 'nav-item nav-item-ultra' + (ultra.type ? ` nav-type-${ultra.type}` : '');
197
+ ultraBtn.dataset.section = ultra.id;
198
+ ultraBtn.title = ultra.summary || ultra.title;
199
+ ultraBtn.innerHTML = `
200
+ <span class="nav-title">${ultra.title}</span>
201
+ <span class="nav-summary">${ultra.summary || ''}</span>
202
+ `;
203
+ ultraBtn.addEventListener('click', () => navigate(ultra.id, { scrollToHighlight: Boolean(highlightQuery) }));
204
+ ultraList.appendChild(ultraBtn);
205
+ navButtons.set(ultra.id, ultraBtn);
206
+ });
207
+
208
+ ultraGroup.append(ultraParentBtn, ultraList);
209
+ deepList.appendChild(ultraGroup);
210
+ navButtons.set(deep.id, ultraParentBtn);
211
+ navGroups.set(deep.id, { group: ultraGroup, button: ultraParentBtn, list: ultraList });
212
+ const ultraShouldExpand = expandedGroups.has(deep.id) && !deep.collapsed;
213
+ setGroupExpanded(deep.id, ultraShouldExpand);
214
+ return;
215
+ }
216
+
217
+ const deepBtn = document.createElement('button');
218
+ deepBtn.type = 'button';
219
+ deepBtn.className = 'nav-item nav-item-deep' + (deep.type ? ` nav-type-${deep.type}` : '');
220
+ deepBtn.dataset.section = deep.id;
221
+ deepBtn.title = deep.summary || deep.title;
222
+ deepBtn.innerHTML = `
223
+ <span class="nav-title">${deep.title}${deep.type === 'press-release' ? '<span class="nav-type-icon" aria-label="Press Release"></span>' : ''}</span>
224
+ <span class="nav-summary">${deep.summary || ''}</span>
225
+ `;
226
+ deepBtn.addEventListener('click', () => navigate(deep.id, { scrollToHighlight: Boolean(highlightQuery) }));
227
+ deepList.appendChild(deepBtn);
228
+ navButtons.set(deep.id, deepBtn);
229
+ });
230
+
231
+ deepGroup.append(deepParentBtn, deepList);
232
+ nestedList.appendChild(deepGroup);
233
+ navButtons.set(nested.id, deepParentBtn);
234
+ navGroups.set(nested.id, { group: deepGroup, button: deepParentBtn, list: deepList });
235
+ const deepShouldExpand = expandedGroups.has(nested.id) && !nested.collapsed;
236
+ setGroupExpanded(nested.id, deepShouldExpand);
237
+ return;
238
+ }
239
+
240
+ const nestedBtn = document.createElement('button');
241
+ nestedBtn.type = 'button';
242
+ nestedBtn.className = 'nav-item nav-item-nested' + (nested.type ? ` nav-type-${nested.type}` : '');
243
+ nestedBtn.dataset.section = nested.id;
244
+ nestedBtn.title = nested.summary || nested.title;
245
+ nestedBtn.innerHTML = `
246
+ <span class="nav-title">${nested.title}${nested.type === 'press-release' ? '<span class="nav-type-icon" aria-label="Press Release"></span>' : ''}</span>
247
+ <span class="nav-summary">${nested.summary || ''}</span>
248
+ `;
249
+ nestedBtn.addEventListener('click', () => navigate(nested.id, { scrollToHighlight: Boolean(highlightQuery) }));
250
+ nestedList.appendChild(nestedBtn);
251
+ navButtons.set(nested.id, nestedBtn);
252
+ });
253
+
254
+ nestedGroup.append(nestedParentBtn, nestedList);
255
+ list.appendChild(nestedGroup);
256
+ navButtons.set(sub.id, nestedParentBtn);
257
+ navGroups.set(sub.id, { group: nestedGroup, button: nestedParentBtn, list: nestedList });
258
+ // Initialize nested group expansion state (collapsed by default, or if marked collapsed)
259
+ const nestedShouldExpand = expandedGroups.has(sub.id) && !sub.collapsed;
260
+ setGroupExpanded(sub.id, nestedShouldExpand);
261
+ return;
262
+ }
263
+
264
+ const childBtn = document.createElement('button');
265
+ childBtn.type = 'button';
266
+ childBtn.className = 'nav-item' + (sub.type ? ` nav-type-${sub.type}` : '');
267
+ childBtn.dataset.section = sub.id;
268
+ childBtn.title = sub.summary;
269
+ childBtn.innerHTML = `
270
+ <span class="nav-title">${sub.title}${sub.type === 'press-release' ? '<span class="nav-type-icon" aria-label="Press Release"></span>' : ''}</span>
271
+ <span class="nav-summary">${sub.summary}</span>
272
+ `;
273
+ childBtn.addEventListener('click', () => navigate(sub.id, { scrollToHighlight: Boolean(highlightQuery) }));
274
+ list.appendChild(childBtn);
275
+ navButtons.set(sub.id, childBtn);
276
+ });
277
+
278
+ group.append(parentBtn, list);
279
+ nav.appendChild(group);
280
+ navButtons.set(section.id, parentBtn);
281
+ navGroups.set(section.id, { group, button: parentBtn, list });
282
+ // Respect collapsed property - only expand if not marked collapsed AND (previously expanded OR first group)
283
+ const shouldExpand = !section.collapsed && (expandedGroups.has(section.id) || (!groupApplied && !expandedGroups.size));
284
+ setGroupExpanded(section.id, shouldExpand);
285
+ if (shouldExpand) groupApplied = true;
286
+ } else {
287
+ const button = document.createElement('button');
288
+ button.type = 'button';
289
+ button.className = 'nav-leaf' + (section.type ? ` nav-type-${section.type}` : '');
290
+ button.dataset.section = section.id;
291
+ button.title = section.summary;
292
+ button.innerHTML = `
293
+ <span class="nav-title">${section.title}${section.type === 'press-release' ? '<span class="nav-type-icon" aria-label="Press Release"></span>' : ''}</span>
294
+ <span class="nav-summary">${section.summary}</span>
295
+ `;
296
+ button.addEventListener('click', () => navigate(section.id, { scrollToHighlight: Boolean(highlightQuery) }));
297
+ nav.appendChild(button);
298
+ navButtons.set(section.id, button);
299
+ }
300
+ });
301
+ }
302
+
303
+ function navigate(id, options = {}) {
304
+ const { scrollToHighlight = false } = options;
305
+ const { targetId, parentId } = resolveTarget(id);
306
+ if (paletteOpen) closeCommandPalette();
307
+ if (parentId) {
308
+ setGroupExpanded(parentId, true);
309
+ }
310
+ pendingHighlightScroll = scrollToHighlight || Boolean(highlightQuery);
311
+ if (location.hash.replace('#', '') === targetId) {
312
+ handleRoute();
313
+ } else {
314
+ location.hash = `#${targetId}`;
315
+ }
316
+ }
317
+
318
+ function currentSectionId() {
319
+ return location.hash.replace('#', '') || DEFAULT_SECTION;
320
+ }
321
+
322
+ async function handleRoute() {
323
+ const currentId = currentSectionId();
324
+ const resolved = resolveEntry(currentId);
325
+ if (!resolved) return;
326
+ const { entry, targetId, parentId } = resolved;
327
+ if (targetId !== currentId) {
328
+ location.replace(`#${targetId}`);
329
+ return;
330
+ }
331
+ if (parentId) {
332
+ setGroupExpanded(parentId, true);
333
+ }
334
+ setActiveNav(entry.id, parentId);
335
+ await loadSection(entry, parentId);
336
+ }
337
+
338
+ function setActiveNav(activeId, parentId = null) {
339
+ navButtons.forEach((btn) => {
340
+ btn.setAttribute('aria-current', 'false');
341
+ });
342
+ const activeBtn = navButtons.get(activeId);
343
+ if (activeBtn) {
344
+ activeBtn.setAttribute('aria-current', 'page');
345
+ }
346
+ if (parentId) {
347
+ const parentBtn = navButtons.get(parentId);
348
+ if (parentBtn) parentBtn.setAttribute('aria-current', 'page');
349
+ }
350
+ }
351
+
352
+ function setGroupExpanded(id, expanded) {
353
+ if (!id) return;
354
+ const info = navGroups.get(id);
355
+ if (expanded) {
356
+ expandedGroups.add(id);
357
+ if (info) info.group.classList.add('expanded');
358
+ } else {
359
+ expandedGroups.delete(id);
360
+ if (info) info.group.classList.remove('expanded');
361
+ }
362
+ }
363
+
364
+ // Wrapper functions that bind findSection to lib functions
365
+ function resolveTarget(id) {
366
+ return resolveTargetFn(id, findSection);
367
+ }
368
+
369
+ function resolveEntry(id) {
370
+ return resolveEntryFn(id, findSection);
371
+ }
372
+
373
+ async function loadSection(entry) {
374
+ if (!entry) return;
375
+ const module = await import(entry.module);
376
+ const loader = module.load || module.default;
377
+ if (typeof loader !== 'function') {
378
+ app.innerHTML = `<article class="section"><p>Section failed to load.</p></article>`;
379
+ return;
380
+ }
381
+ const payload = await loader();
382
+ app.innerHTML = payload.html || '';
383
+
384
+ // Render any mermaid diagrams in the content
385
+ await renderMermaidBlocks(app);
386
+
387
+ // Apply syntax highlighting to code blocks
388
+ await highlightCodeBlocks(app);
389
+
390
+ // Add bottom page navigation
391
+ renderBottomNav(entry.id);
392
+
393
+ // Scroll to top of content area
394
+ app.scrollTop = 0;
395
+ window.scrollTo(0, 0);
396
+
397
+ if (typeof payload.afterRender === 'function') {
398
+ payload.afterRender(app);
399
+ }
400
+ updateMetaTags({
401
+ title: entry.title,
402
+ description: entry.summary,
403
+ siteTitle: SITE_CONFIG.siteTitle,
404
+ siteUrl: SITE_CONFIG.siteUrl,
405
+ sectionId: entry.id
406
+ });
407
+ rendered.set(entry.id, Date.now());
408
+ const shouldScrollToHighlight = pendingHighlightScroll;
409
+ pendingHighlightScroll = false;
410
+ applyHighlight(shouldScrollToHighlight);
411
+ focusCanvas();
412
+ }
413
+
414
+ /**
415
+ * Render bottom page navigation (prev/next links)
416
+ * Visibility controlled by SITE_CONFIG.bottomNav: 'always' | 'mobile' | 'never'
417
+ * Per-section override via SITE_CONFIG.bottomNavSections array
418
+ */
419
+ function renderBottomNav(currentId) {
420
+ // Remove existing bottom nav if present
421
+ const existing = app.querySelector('.bottom-nav');
422
+ if (existing) existing.remove();
423
+
424
+ // Check if bottom nav is disabled globally
425
+ if (SITE_CONFIG.bottomNav === 'never') return;
426
+
427
+ // Check for per-section override
428
+ const sectionOverrides = SITE_CONFIG.bottomNavSections || [];
429
+ const isSectionEnabled = sectionOverrides.some(prefix => currentId.startsWith(prefix));
430
+ const isMobileOnly = SITE_CONFIG.bottomNav === 'mobile' && !isSectionEnabled;
431
+
432
+ const { prev, next } = getAdjacentSections(currentId);
433
+ if (!prev && !next) return;
434
+
435
+ const nav = document.createElement('nav');
436
+ nav.className = 'bottom-nav';
437
+ if (isMobileOnly) {
438
+ nav.classList.add('mobile-only');
439
+ }
440
+
441
+ if (prev) {
442
+ const prevWrapper = document.createElement('div');
443
+ prevWrapper.className = 'bottom-nav-item bottom-nav-prev';
444
+ prevWrapper.innerHTML = `<span class="bottom-nav-chevron">\u2039</span>`;
445
+ const prevLink = document.createElement('a');
446
+ prevLink.href = `#${prev.id}`;
447
+ prevLink.className = 'bottom-nav-link';
448
+ prevLink.title = `Previous: ${prev.title}`;
449
+ prevLink.textContent = prev.title;
450
+ prevLink.addEventListener('click', (e) => {
451
+ e.preventDefault();
452
+ navigate(prev.id);
453
+ });
454
+ prevWrapper.appendChild(prevLink);
455
+ nav.appendChild(prevWrapper);
456
+ } else {
457
+ // Empty spacer for alignment
458
+ const spacer = document.createElement('div');
459
+ spacer.className = 'bottom-nav-spacer';
460
+ nav.appendChild(spacer);
461
+ }
462
+
463
+ if (next) {
464
+ const nextWrapper = document.createElement('div');
465
+ nextWrapper.className = 'bottom-nav-item bottom-nav-next';
466
+ const nextLink = document.createElement('a');
467
+ nextLink.href = `#${next.id}`;
468
+ nextLink.className = 'bottom-nav-link';
469
+ nextLink.title = `Next: ${next.title}`;
470
+ nextLink.textContent = next.title;
471
+ nextLink.addEventListener('click', (e) => {
472
+ e.preventDefault();
473
+ navigate(next.id);
474
+ });
475
+ nextWrapper.appendChild(nextLink);
476
+ nextWrapper.innerHTML += `<span class="bottom-nav-chevron">\u203a</span>`;
477
+ nav.appendChild(nextWrapper);
478
+ }
479
+
480
+ // Append to the section content
481
+ const section = app.querySelector('section') || app;
482
+ section.appendChild(nav);
483
+ }
484
+
485
+ function focusCanvas() {
486
+ requestAnimationFrame(() => app.focus());
487
+ }
488
+
489
+ function boot() {
490
+ initNav();
491
+ if (highlightQuery) {
492
+ pendingHighlightScroll = true;
493
+ }
494
+ window.addEventListener('hashchange', () => {
495
+ if (highlightQuery) {
496
+ pendingHighlightScroll = true;
497
+ }
498
+ handleRoute();
499
+ });
500
+ yearMarker.textContent = new Date().getFullYear();
501
+ handleRoute();
502
+ }
503
+
504
+ boot();
505
+
506
+ // Command palette
507
+ if (commandToggle && commandPalette && commandInput && commandList) {
508
+ commandToggle.addEventListener('click', () => {
509
+ if (paletteOpen) closeCommandPalette();
510
+ else openCommandPalette();
511
+ });
512
+
513
+ commandInput.addEventListener('input', () => {
514
+ const value = commandInput.value;
515
+ setHighlightQuery(value, true);
516
+ updateCommandEntries(value);
517
+ });
518
+
519
+ commandInput.addEventListener('keydown', (event) => {
520
+ const max = commandEntries.length - 1;
521
+ if (event.key === 'ArrowDown') {
522
+ event.preventDefault();
523
+ commandIndex = Math.min(max, commandIndex + 1);
524
+ reflectCommandSelection();
525
+ } else if (event.key === 'ArrowUp') {
526
+ event.preventDefault();
527
+ commandIndex = Math.max(0, commandIndex - 1);
528
+ reflectCommandSelection();
529
+ } else if (event.key === 'Enter') {
530
+ event.preventDefault();
531
+ const target = commandEntries[commandIndex];
532
+ if (target) {
533
+ setHighlightQuery(commandInput.value, true);
534
+ navigate(target.id, { scrollToHighlight: true });
535
+ closeCommandPalette();
536
+ }
537
+ } else if (event.key === 'Escape') {
538
+ event.preventDefault();
539
+ closeCommandPalette();
540
+ }
541
+ });
542
+
543
+ commandList.addEventListener('click', (event) => {
544
+ const item = event.target.closest('[data-section]');
545
+ if (!item) return;
546
+ const targetId = item.dataset.section;
547
+ if (!targetId) return;
548
+ setHighlightQuery(commandInput.value, true);
549
+ navigate(targetId, { scrollToHighlight: true });
550
+ closeCommandPalette();
551
+ });
552
+
553
+ commandPalette.addEventListener('click', (event) => {
554
+ if (event.target === commandPalette) {
555
+ closeCommandPalette();
556
+ }
557
+ });
558
+
559
+ window.addEventListener('keydown', (event) => {
560
+ const target = event.target;
561
+ const isTypingContext = target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable);
562
+ const isModifier = event.metaKey || event.ctrlKey;
563
+ if ((event.key.toLowerCase() === 'k' && isModifier) || (event.key === '/' && !isTypingContext)) {
564
+ event.preventDefault();
565
+ if (paletteOpen) closeCommandPalette();
566
+ else openCommandPalette();
567
+ } else if (event.key === 'Escape' && paletteOpen) {
568
+ event.preventDefault();
569
+ closeCommandPalette();
570
+ }
571
+ });
572
+ }
573
+
574
+ function openCommandPalette() {
575
+ if (!commandPalette || !commandInput) return;
576
+ paletteOpen = true;
577
+ commandPalette.hidden = false;
578
+ const initial = highlightQuery;
579
+ commandInput.value = initial;
580
+ updateCommandEntries(initial);
581
+ requestAnimationFrame(() => {
582
+ commandInput.focus();
583
+ if (initial) {
584
+ commandInput.select();
585
+ }
586
+ });
587
+ }
588
+
589
+ function closeCommandPalette() {
590
+ if (!commandPalette || !commandInput) return;
591
+ paletteOpen = false;
592
+ commandPalette.hidden = true;
593
+ commandInput.blur();
594
+ }
595
+
596
+ let searchDebounce = null;
597
+ let isSearching = false;
598
+
599
+ async function updateCommandEntries(query) {
600
+ if (!commandList) return;
601
+
602
+ // Show loading state on first search
603
+ if (!isSearching && query.trim()) {
604
+ isSearching = true;
605
+ commandList.innerHTML = '<li class="cmd-item cmd-loading">Indexing content...</li>';
606
+ }
607
+
608
+ // Debounce to avoid excessive searches while typing
609
+ clearTimeout(searchDebounce);
610
+ searchDebounce = setTimeout(async () => {
611
+ commandEntries = await searchContent(MANIFEST, query);
612
+ const currentId = currentSectionId();
613
+ commandIndex = findPreferredIndex(commandEntries, currentId);
614
+ renderCommandList();
615
+ reflectCommandSelection();
616
+ isSearching = false;
617
+ }, query.trim() ? 150 : 0);
618
+ }
619
+
620
+ function renderCommandList() {
621
+ if (!commandList) return;
622
+ commandList.innerHTML = '';
623
+ if (!commandEntries.length) {
624
+ const empty = document.createElement('li');
625
+ empty.className = 'cmd-item';
626
+ empty.setAttribute('aria-selected', 'false');
627
+ empty.textContent = 'No matches.';
628
+ commandList.appendChild(empty);
629
+ return;
630
+ }
631
+ commandEntries.forEach((section) => {
632
+ const item = document.createElement('li');
633
+ item.className = 'cmd-item';
634
+ item.dataset.section = section.id;
635
+ item.setAttribute('role', 'option');
636
+ const title = document.createElement('span');
637
+ title.className = 'cmd-item-title';
638
+ title.textContent = section.title;
639
+ if (section.group) {
640
+ const group = document.createElement('span');
641
+ group.className = 'cmd-item-group';
642
+ group.textContent = section.group;
643
+ title.prepend(group);
644
+ }
645
+ const summary = document.createElement('span');
646
+ summary.className = 'cmd-item-summary';
647
+ summary.textContent = section.summary || '';
648
+ item.append(title, summary);
649
+ commandList.appendChild(item);
650
+ });
651
+ }
652
+
653
+ function reflectCommandSelection() {
654
+ if (!commandList) return;
655
+ Array.from(commandList.children).forEach((li, index) => {
656
+ const isActive = index === commandIndex && commandEntries.length;
657
+ li.setAttribute('aria-selected', isActive ? 'true' : 'false');
658
+ if (isActive) {
659
+ li.scrollIntoView({ block: 'nearest' });
660
+ }
661
+ });
662
+ }
663
+
664
+ // Export handler
665
+ if (exportBtn) {
666
+ exportBtn.addEventListener('click', showExportOptions);
667
+ }
668
+
669
+ function showExportOptions() {
670
+ const overlay = document.createElement('div');
671
+ overlay.className = 'export-options-overlay';
672
+ overlay.innerHTML = `
673
+ <div class="export-options-modal">
674
+ <div class="export-options-header">EXPORT OPTIONS</div>
675
+ <div class="export-options-buttons">
676
+ <button type="button" class="export-option-btn" data-scope="page">
677
+ <span class="export-option-title">Current Page</span>
678
+ <span class="export-option-desc">Export only this section</span>
679
+ </button>
680
+ <button type="button" class="export-option-btn" data-scope="site">
681
+ <span class="export-option-title">Entire Site</span>
682
+ <span class="export-option-desc">Export all documentation</span>
683
+ </button>
684
+ </div>
685
+ <button type="button" class="export-cancel-btn">Cancel</button>
686
+ </div>
687
+ `;
688
+ document.body.appendChild(overlay);
689
+ setTimeout(() => overlay.classList.add('active'), 10);
690
+
691
+ const close = () => {
692
+ overlay.classList.remove('active');
693
+ setTimeout(() => overlay.remove(), 200);
694
+ };
695
+
696
+ overlay.querySelector('.export-cancel-btn').addEventListener('click', close);
697
+ overlay.addEventListener('click', (e) => {
698
+ if (e.target === overlay) close();
699
+ });
700
+
701
+ overlay.querySelectorAll('.export-option-btn').forEach(btn => {
702
+ btn.addEventListener('click', () => {
703
+ const scope = btn.dataset.scope;
704
+ close();
705
+ handleExport(scope);
706
+ });
707
+ });
708
+ }
709
+
710
+ async function handleExport(scope = 'site') {
711
+ if (!exportBtn) return;
712
+ const originalMarkup = exportBtn.innerHTML;
713
+
714
+ // Test for popup blocking first
715
+ const testWindow = window.open('', '_blank', 'width=1,height=1,left=0,top=0');
716
+ if (!testWindow || testWindow.closed || typeof testWindow.closed === 'undefined') {
717
+ if (confirm('Pop-ups are blocked. Please allow pop-ups for this site to export the document.\n\nWould you like to try again after enabling pop-ups?')) {
718
+ return; // User will try again after enabling popups
719
+ } else {
720
+ return; // User declined, cannot proceed
721
+ }
722
+ } else {
723
+ testWindow.close(); // Close test window
724
+ }
725
+
726
+ exportBtn.disabled = true;
727
+
728
+ // Create loading overlay
729
+ const loadingOverlay = document.createElement('div');
730
+ loadingOverlay.className = 'export-loading-overlay';
731
+ loadingOverlay.innerHTML = `
732
+ <div class="export-loading-modal">
733
+ <div class="export-loading-header">
734
+ <div class="export-loading-title">COMPILING DOCUMENTATION</div>
735
+ <div class="export-loading-subtitle">Assembling all sections into unified document</div>
736
+ </div>
737
+ <div class="export-loading-progress">
738
+ <div class="export-loading-bar">
739
+ <div class="export-loading-fill"></div>
740
+ </div>
741
+ <div class="export-loading-status-container">
742
+ <div class="export-loading-status">Initializing...</div>
743
+ </div>
744
+ </div>
745
+ <div class="export-loading-scanner">
746
+ <div class="scanner-line"></div>
747
+ </div>
748
+ </div>
749
+ `;
750
+ document.body.appendChild(loadingOverlay);
751
+
752
+ // Force layout and trigger animation
753
+ setTimeout(() => loadingOverlay.classList.add('active'), 10);
754
+
755
+ const progressFill = loadingOverlay.querySelector('.export-loading-fill');
756
+ const statusText = loadingOverlay.querySelector('.export-loading-status');
757
+
758
+ try {
759
+ // Collect sections based on scope
760
+ let allSections;
761
+ if (scope === 'page') {
762
+ // Export only current page
763
+ const currentId = currentSectionId();
764
+ const allAvailable = collectExportableSections(MANIFEST);
765
+ const current = allAvailable.find(s => s.id === currentId);
766
+ allSections = current ? [current] : [];
767
+ } else {
768
+ // Export entire site
769
+ allSections = collectExportableSections(MANIFEST);
770
+ }
771
+
772
+ if (allSections.length === 0) {
773
+ alert('No content available to export.');
774
+ loadingOverlay.remove();
775
+ exportBtn.disabled = false;
776
+ return;
777
+ }
778
+
779
+ const bundle = [];
780
+ const totalSections = allSections.length;
781
+ let processedSections = 0;
782
+
783
+ for (const section of allSections) {
784
+ // Update progress
785
+ processedSections++;
786
+ const progress = (processedSections / totalSections) * 100;
787
+ progressFill.style.width = `${progress}%`;
788
+ statusText.textContent = scope === 'page'
789
+ ? `Exporting: ${section.title}`
790
+ : `Processing section ${processedSections} of ${totalSections}: ${section.title}`;
791
+
792
+ // Small delay to show progress animation
793
+ await new Promise(resolve => setTimeout(resolve, 50));
794
+
795
+ try {
796
+ const mod = await import(section.module);
797
+ const loader = mod.load || mod.default;
798
+ if (typeof loader !== 'function') continue;
799
+ const payload = await loader();
800
+ const parsed = sanitizeExportMarkup(payload.html || '');
801
+ bundle.push({ section, html: parsed });
802
+ } catch (err) {
803
+ console.error('Failed to include section in export', section.id, err);
804
+ }
805
+ }
806
+
807
+ statusText.textContent = 'Generating document...';
808
+ await new Promise(resolve => setTimeout(resolve, 200));
809
+
810
+ const htmlDoc = composeExportDocument(bundle, EXPORT_CONFIG);
811
+
812
+ statusText.textContent = 'Opening document viewer...';
813
+ await new Promise(resolve => setTimeout(resolve, 100));
814
+
815
+ // Open window only after content is ready
816
+ const printWindow = window.open('', '_blank', 'width=900,height=860,scrollbars=yes,resizable=yes');
817
+ if (!printWindow) {
818
+ alert('Please allow pop-ups to export the document.');
819
+ loadingOverlay.remove();
820
+ return;
821
+ }
822
+
823
+ printWindow.document.open();
824
+ printWindow.document.write(htmlDoc);
825
+ printWindow.document.close();
826
+ printWindow.focus();
827
+
828
+ // Fade out loading overlay
829
+ loadingOverlay.classList.remove('active');
830
+ setTimeout(() => loadingOverlay.remove(), 300);
831
+
832
+ } catch (err) {
833
+ console.error('Export failed', err);
834
+ alert('Export failed. Check console for details.');
835
+ loadingOverlay.remove();
836
+ } finally {
837
+ exportBtn.disabled = false;
838
+ exportBtn.innerHTML = originalMarkup;
839
+ }
840
+ }
841
+
842
+ function sanitizeExportMarkup(markup) {
843
+ const wrapper = document.createElement('div');
844
+ wrapper.innerHTML = markup;
845
+ wrapper.querySelectorAll('script').forEach((script) => script.remove());
846
+ wrapper.querySelectorAll('button').forEach((button) => button.removeAttribute('onclick'));
847
+ wrapper.querySelectorAll('mark.hl').forEach((mark) => {
848
+ const text = document.createTextNode(mark.textContent || '');
849
+ mark.replaceWith(text);
850
+ });
851
+ const firstSection = wrapper.querySelector('section');
852
+ return firstSection ? firstSection.innerHTML : wrapper.innerHTML;
853
+ }
854
+
855
+ function setHighlightQuery(value, persist = false) {
856
+ highlightQuery = value.trim();
857
+ if (persist) {
858
+ if (highlightQuery) {
859
+ localStorage.setItem(COMMAND_QUERY_KEY, highlightQuery);
860
+ } else {
861
+ localStorage.removeItem(COMMAND_QUERY_KEY);
862
+ }
863
+ }
864
+ applyHighlight();
865
+ }
866
+
867
+ function applyHighlight(scrollToFirst = false) {
868
+ if (!app) return;
869
+ highlightContent(app, highlightQuery, { scrollToFirst });
870
+ }
871
+
872
+ function clearHighlights(root) {
873
+ if (!root) return;
874
+ root.querySelectorAll('mark.hl').forEach((mark) => {
875
+ const text = document.createTextNode(mark.textContent || '');
876
+ mark.replaceWith(text);
877
+ });
878
+ }
879
+
880
+ function highlightContent(root, query, { scrollToFirst = false } = {}) {
881
+ if (!root) return;
882
+ clearHighlights(root);
883
+ if (!query) return;
884
+ const terms = query.split(/\s+/).map((term) => term.trim()).filter(Boolean);
885
+ if (!terms.length) return;
886
+
887
+ const lowerTerms = terms.map((term) => term.toLowerCase());
888
+ const skipTags = new Set(['SCRIPT', 'STYLE', 'CODE', 'PRE']);
889
+ const walker = document.createTreeWalker(
890
+ root,
891
+ NodeFilter.SHOW_TEXT,
892
+ {
893
+ acceptNode(node) {
894
+ if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
895
+ const parent = node.parentNode;
896
+ if (parent && skipTags.has(parent.tagName)) return NodeFilter.FILTER_REJECT;
897
+ return NodeFilter.FILTER_ACCEPT;
898
+ }
899
+ }
900
+ );
901
+
902
+ const matches = [];
903
+ let current;
904
+ while ((current = walker.nextNode())) {
905
+ const value = current.nodeValue.toLowerCase();
906
+ if (lowerTerms.some((term) => value.includes(term))) {
907
+ matches.push(current);
908
+ }
909
+ }
910
+
911
+ const termPattern = new RegExp(`(${terms.map(escapeRegExp).join('|')})`, 'gi');
912
+ matches.forEach((textNode) => {
913
+ const text = textNode.nodeValue;
914
+ const fragments = [];
915
+ let lastIndex = 0;
916
+ text.replace(termPattern, (match, _group, offset) => {
917
+ if (offset > lastIndex) {
918
+ fragments.push(document.createTextNode(text.slice(lastIndex, offset)));
919
+ }
920
+ const mark = document.createElement('mark');
921
+ mark.className = 'hl';
922
+ mark.textContent = match;
923
+ fragments.push(mark);
924
+ lastIndex = offset + match.length;
925
+ return match;
926
+ });
927
+ if (lastIndex < text.length) {
928
+ fragments.push(document.createTextNode(text.slice(lastIndex)));
929
+ }
930
+ const replacement = document.createDocumentFragment();
931
+ fragments.forEach((fragment) => replacement.appendChild(fragment));
932
+ textNode.parentNode.replaceChild(replacement, textNode);
933
+ });
934
+
935
+ if (scrollToFirst) {
936
+ requestAnimationFrame(() => {
937
+ const first = root.querySelector('mark.hl');
938
+ if (first) {
939
+ first.scrollIntoView({ behavior: 'smooth', block: 'center' });
940
+ }
941
+ });
942
+ }
943
+ }
944
+
945
+ // Mobile menu toggle
946
+ if (mobileMenuToggle && sidebar) {
947
+ mobileMenuToggle.addEventListener('click', () => {
948
+ const isOpen = sidebar.classList.contains('mobile-open');
949
+ if (isOpen) {
950
+ sidebar.classList.remove('mobile-open');
951
+ document.body.classList.remove('menu-open');
952
+ mobileMenuToggle.setAttribute('aria-expanded', 'false');
953
+ } else {
954
+ sidebar.classList.add('mobile-open');
955
+ document.body.classList.add('menu-open');
956
+ mobileMenuToggle.setAttribute('aria-expanded', 'true');
957
+ }
958
+ });
959
+
960
+ // Close menu when clicking on a nav item (but not parent sections)
961
+ nav.addEventListener('click', (e) => {
962
+ if (window.innerWidth <= 960) {
963
+ const clickedElement = e.target.closest('.nav-item, .nav-leaf, .nav-parent');
964
+ if (clickedElement) {
965
+ // Only close if it's a leaf node or nav-item (actual navigation)
966
+ // Don't close for nav-parent (section headers that expand/collapse)
967
+ if (clickedElement.classList.contains('nav-item') ||
968
+ clickedElement.classList.contains('nav-leaf')) {
969
+ sidebar.classList.remove('mobile-open');
970
+ document.body.classList.remove('menu-open');
971
+ mobileMenuToggle.setAttribute('aria-expanded', 'false');
972
+ }
973
+ }
974
+ }
975
+ });
976
+
977
+ // Close menu when clicking outside
978
+ document.addEventListener('click', (e) => {
979
+ if (window.innerWidth <= 960 &&
980
+ sidebar.classList.contains('mobile-open') &&
981
+ !sidebar.contains(e.target) &&
982
+ !mobileMenuToggle.contains(e.target)) {
983
+ sidebar.classList.remove('mobile-open');
984
+ document.body.classList.remove('menu-open');
985
+ mobileMenuToggle.setAttribute('aria-expanded', 'false');
986
+ }
987
+ });
988
+ }