@docmd/ui 0.4.11 → 0.5.1

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
@@ -7,7 +7,7 @@ Contains:
7
7
  - **Assets:** Core CSS and JavaScript.
8
8
  - **Icons:** SVG icon sets.
9
9
 
10
- ## The docmd Ecosystem
10
+ ## The `docmd` Ecosystem
11
11
 
12
12
  `docmd` is a modular system. Here are the official packages:
13
13
 
@@ -22,6 +22,7 @@ Contains:
22
22
 
23
23
  **Plugins**
24
24
  * [**@docmd/plugin-search**](https://www.npmjs.com/package/@docmd/plugin-search) - Offline full-text search.
25
+ * [**@docmd/plugin-pwa**](https://www.npmjs.com/package/@docmd/plugin-pwa) - Progressive Web App support.
25
26
  * [**@docmd/plugin-mermaid**](https://www.npmjs.com/package/@docmd/plugin-mermaid) - Diagrams and flowcharts.
26
27
  * [**@docmd/plugin-seo**](https://www.npmjs.com/package/@docmd/plugin-seo) - Meta tags and Open Graph data.
27
28
  * [**@docmd/plugin-sitemap**](https://www.npmjs.com/package/@docmd/plugin-sitemap) - Automatic sitemap generation.
@@ -176,18 +176,19 @@ a:any-link {
176
176
  .sidebar {
177
177
  display: flex;
178
178
  flex-direction: column;
179
- width: 260px;
179
+ width: var(--sidebar-width);
180
180
  background-color: var(--sidebar-bg);
181
181
  color: var(--sidebar-text);
182
- padding: .5em .25em .5em .5em;
183
182
  border-right: 1px solid var(--border-color);
184
183
  height: 100vh;
185
184
  position: fixed;
185
+ padding: .5em .25em .5em .5em;
186
186
  top: 0;
187
187
  left: 0;
188
- overflow-y: auto;
188
+ overflow: hidden;
189
189
  box-sizing: border-box;
190
- flex-shrink: 0
190
+ flex-shrink: 0;
191
+ z-index: 100;
191
192
  }
192
193
 
193
194
  .sidebar h1 {
@@ -527,7 +528,7 @@ body.sidebar-collapsed .main-content-wrapper {
527
528
  }
528
529
 
529
530
  .sidebar-options-wrapper {
530
- padding: .5rem 0;
531
+ padding: .25rem 0;
531
532
  }
532
533
 
533
534
  .sidebar-options-wrapper.mt-auto {
@@ -536,6 +537,118 @@ body.sidebar-collapsed .main-content-wrapper {
536
537
  border-top: 1px solid var(--border-color);
537
538
  }
538
539
 
540
+ /* --- Version Dropdown (Flat UI) --- */
541
+ .sidebar-version-wrapper {
542
+ padding: 0.25rem .5em;
543
+ }
544
+
545
+ .sidebar-bottom-group.mt-auto {
546
+ margin-top: auto;
547
+ }
548
+
549
+ .docmd-version-dropdown {
550
+ position: relative;
551
+ width: 100%;
552
+ }
553
+
554
+ .version-dropdown-toggle {
555
+ display: flex;
556
+ align-items: center;
557
+ justify-content: space-between;
558
+ width: 100%;
559
+ padding: 0.5rem 0.75rem;
560
+ background-color: var(--bg-color);
561
+ border: 1px solid var(--border-color);
562
+ border-radius: var(--ui-border-radius);
563
+ color: var(--text-heading);
564
+ font-size: 0.85rem;
565
+ font-weight: 500;
566
+ cursor: pointer;
567
+ transition: all 0.2s ease;
568
+ }
569
+
570
+ .version-dropdown-toggle:hover {
571
+ border-color: var(--text-muted);
572
+ }
573
+
574
+ .version-dropdown-menu {
575
+ position: absolute;
576
+ top: calc(100% + 4px);
577
+ left: 0;
578
+ right: 0;
579
+ margin: 0;
580
+ padding: 0.25rem;
581
+ background-color: var(--bg-color);
582
+ border: 1px solid var(--border-color);
583
+ border-radius: var(--ui-border-radius);
584
+ box-shadow: var(--shadow-md);
585
+ list-style: none;
586
+ z-index: 100;
587
+ opacity: 0;
588
+ visibility: hidden;
589
+ transform: translateY(-5px);
590
+ transition: all 0.2s ease;
591
+ }
592
+
593
+ .docmd-version-dropdown.open .version-dropdown-menu {
594
+ opacity: 1;
595
+ visibility: visible;
596
+ transform: translateY(0);
597
+ }
598
+
599
+ .version-dropdown-item {
600
+ display: flex;
601
+ align-items: center;
602
+ justify-content: space-between;
603
+ padding: 0.4rem 0.5rem;
604
+ color: var(--text-color);
605
+ font-size: 0.85rem;
606
+ text-decoration: none;
607
+ border-radius: 4px;
608
+ transition: background-color 0.2s;
609
+ }
610
+
611
+ .version-dropdown-item:hover {
612
+ background-color: var(--sidebar-link-active-bg);
613
+ color: var(--text-heading);
614
+ text-decoration: none;
615
+ }
616
+
617
+ .version-dropdown-item.active {
618
+ font-weight: 600;
619
+ color: var(--link-color);
620
+ background-color: var(--sidebar-link-active-bg);
621
+ }
622
+
623
+ .version-check {
624
+ width: 1rem;
625
+ height: 1rem;
626
+ }
627
+
628
+ .version-chevron {
629
+ width: 1rem;
630
+ height: 1rem;
631
+ transition: transform 0.2s ease;
632
+ }
633
+
634
+ .docmd-version-dropdown.open .version-chevron {
635
+ transform: rotate(180deg);
636
+ }
637
+
638
+ .sidebar-bottom-group .version-dropdown-menu {
639
+ top: auto;
640
+ bottom: calc(100% + 4px);
641
+ transform: translateY(5px);
642
+ }
643
+
644
+ .sidebar-bottom-group .docmd-version-dropdown.open .version-dropdown-menu {
645
+ transform: translateY(0);
646
+ }
647
+
648
+ .sidebar-bottom-group .version-dropdown-toggle .version-chevron {
649
+ transform: rotate(180deg);
650
+ }
651
+
539
652
  .card .card-title {
540
653
  border-bottom: 1px solid var(--border-color)
541
654
  }
@@ -645,6 +758,7 @@ html[data-theme=dark] .theme-toggle-button .icon-sun {
645
758
  .sidebar-nav {
646
759
  flex-grow: 1;
647
760
  overflow-y: auto;
761
+ overflow-x: hidden;
648
762
  min-height: 0;
649
763
  scrollbar-width: thin;
650
764
  }
@@ -11,16 +11,16 @@
11
11
  * [docmd-source] - Please do not remove this header.
12
12
  * --------------------------------------------------------------------
13
13
  */
14
+
14
15
  /**
15
16
  * --------------------------------------------------------------------
16
17
  * docmd : Client-Side Application Logic (SPA Router & UI)
17
18
  * --------------------------------------------------------------------
18
19
  */
19
20
 
20
- (function() {
21
- // =========================================================================
21
+ (function () {
22
+
22
23
  // 1. EVENT DELEGATION
23
- // =========================================================================
24
24
  document.addEventListener('click', (e) => {
25
25
  // Collapsible Navigation
26
26
  const navLabel = e.target.closest('.nav-label, .collapse-icon-wrapper');
@@ -32,7 +32,7 @@
32
32
  item.classList.toggle('expanded', !isExpanded);
33
33
  item.setAttribute('aria-expanded', !isExpanded);
34
34
  }
35
- if (navLabel.classList.contains('collapse-icon-wrapper')) return;
35
+ if (navLabel.classList.contains('collapse-icon-wrapper')) return;
36
36
  }
37
37
 
38
38
  // Toggles
@@ -54,14 +54,59 @@
54
54
  const navItems = Array.from(tabsContainer.querySelectorAll('.docmd-tabs-nav-item'));
55
55
  const tabPanes = Array.from(tabsContainer.querySelectorAll('.docmd-tab-pane'));
56
56
  const index = navItems.indexOf(tabItem);
57
-
57
+
58
58
  navItems.forEach(item => item.classList.remove('active'));
59
59
  tabPanes.forEach(pane => pane.classList.remove('active'));
60
-
60
+
61
61
  tabItem.classList.add('active');
62
62
  if (tabPanes[index]) tabPanes[index].classList.add('active');
63
63
  }
64
64
 
65
+ // Version Dropdown Toggle
66
+ const versionToggle = e.target.closest('.version-dropdown-toggle');
67
+ if (versionToggle) {
68
+ e.preventDefault();
69
+ const dropdown = versionToggle.closest('.docmd-version-dropdown');
70
+ dropdown.classList.toggle('open');
71
+ versionToggle.setAttribute('aria-expanded', dropdown.classList.contains('open'));
72
+ return;
73
+ }
74
+
75
+ // Sticky Version Switching (Path Preservation)
76
+ const versionLink = e.target.closest('.version-dropdown-item');
77
+ if (versionLink) {
78
+ const targetRoot = versionLink.dataset.versionRoot;
79
+ // Use global fallback if undefined (e.g. on 404 pages)
80
+ const currentRoot = window.DOCMD_VERSION_ROOT || '/';
81
+
82
+ if (targetRoot && window.location.pathname) {
83
+ try {
84
+ let currentPath = window.location.pathname;
85
+ const normCurrentRoot = currentRoot.endsWith('/') ? currentRoot : currentRoot + '/';
86
+
87
+ // Only try sticky if we are actually INSIDE the known version path
88
+ if (currentPath.startsWith(normCurrentRoot)) {
89
+ e.preventDefault();
90
+ const suffix = currentPath.substring(normCurrentRoot.length);
91
+ const normTargetRoot = targetRoot.endsWith('/') ? targetRoot : targetRoot + '/';
92
+ window.location.href = normTargetRoot + suffix + window.location.hash;
93
+ return;
94
+ }
95
+ } catch (e) {
96
+ // Ignore errors, let default click happen
97
+ }
98
+ }
99
+ // If sticky logic skipped (e.g. on 404 page or outside root), default <a> click handles it
100
+ }
101
+
102
+ // Close Dropdown if clicked outside
103
+ if (!e.target.closest('.docmd-version-dropdown')) {
104
+ document.querySelectorAll('.docmd-version-dropdown.open').forEach(d => {
105
+ d.classList.remove('open');
106
+ d.querySelector('.version-dropdown-toggle').setAttribute('aria-expanded', 'false');
107
+ });
108
+ }
109
+
65
110
  // Copy Code Button
66
111
  const copyBtn = e.target.closest('.copy-code-button');
67
112
  if (copyBtn) {
@@ -79,21 +124,19 @@
79
124
  }
80
125
  });
81
126
 
82
- // =========================================================================
83
127
  // 2. COMPONENT INITIALIZERS
84
- // =========================================================================
85
128
  function injectCopyButtons() {
86
129
  if (document.body.dataset.copyCodeEnabled !== 'true') return;
87
130
  const svg = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>`;
88
-
131
+
89
132
  document.querySelectorAll('pre').forEach(preElement => {
90
- if (preElement.closest('.code-wrapper')) return;
133
+ if (preElement.closest('.code-wrapper')) return;
91
134
  const wrapper = document.createElement('div');
92
135
  wrapper.className = 'code-wrapper';
93
136
  wrapper.style.position = 'relative';
94
137
  preElement.parentNode.insertBefore(wrapper, preElement);
95
138
  wrapper.appendChild(preElement);
96
-
139
+
97
140
  const copyButton = document.createElement('button');
98
141
  copyButton.className = 'copy-code-button';
99
142
  copyButton.innerHTML = svg;
@@ -107,7 +150,7 @@
107
150
  const tocLinks = document.querySelectorAll('.toc-link');
108
151
  const headings = document.querySelectorAll('.main-content h2, .main-content h3, .main-content h4');
109
152
  const tocContainer = document.querySelector('.toc-list');
110
-
153
+
111
154
  if (tocLinks.length === 0 || headings.length === 0) return;
112
155
 
113
156
  scrollObserver = new IntersectionObserver((entries) => {
@@ -116,7 +159,7 @@
116
159
  tocLinks.forEach(link => link.classList.remove('active'));
117
160
  const id = entry.target.getAttribute('id');
118
161
  const activeLink = document.querySelector(`.toc-link[href="#${id}"]`);
119
-
162
+
120
163
  if (activeLink) {
121
164
  activeLink.classList.add('active');
122
165
  if (tocContainer) {
@@ -143,9 +186,7 @@
143
186
  });
144
187
  }
145
188
 
146
- // =========================================================================
147
189
  // 3. TARGETED SPA ROUTER
148
- // =========================================================================
149
190
  function initializeSPA() {
150
191
  if (location.protocol === 'file:') return;
151
192
  if (document.body.dataset.spaEnabled !== 'true') return;
@@ -156,9 +197,9 @@
156
197
 
157
198
  // Intent-based Hover Prefetching
158
199
  document.addEventListener('mouseover', (e) => {
159
- const link = e.target.closest('.sidebar-nav a, .page-navigation a');
200
+ const link = e.target.closest('.sidebar-nav a, .page-navigation a, .page-footer a, .main-content a');
160
201
  if (!link || link.target === '_blank' || link.hasAttribute('download')) return;
161
-
202
+
162
203
  const url = new URL(link.href).href;
163
204
  if (new URL(url).origin !== location.origin) return;
164
205
  if (pageCache.has(url)) return;
@@ -166,10 +207,10 @@
166
207
  // Wait 65ms to ensure the user actually intends to click
167
208
  clearTimeout(prefetchTimer);
168
209
  prefetchTimer = setTimeout(() => {
169
- pageCache.set(url, fetch(url).then(res => {
170
- if (!res.ok) throw new Error('Prefetch failed');
171
- return { html: res.text(), finalUrl: res.url };
172
- }).catch(() => pageCache.delete(url)));
210
+ pageCache.set(url, fetch(url).then(res => {
211
+ if (!res.ok) throw new Error('Prefetch failed');
212
+ return { html: res.text(), finalUrl: res.url };
213
+ }).catch(() => pageCache.delete(url)));
173
214
  }, 65);
174
215
  });
175
216
 
@@ -179,42 +220,44 @@
179
220
  document.addEventListener('click', async (e) => {
180
221
  if (e.target.closest('.collapse-icon-wrapper')) return;
181
222
 
182
- const link = e.target.closest('.sidebar-nav a, .page-navigation a');
223
+ if (e.target.closest('[data-spa-ignore]')) return;
224
+
225
+ const link = e.target.closest('.sidebar-nav a, .page-navigation a, .page-footer a, .main-content a');
183
226
  if (!link || link.target === '_blank' || link.hasAttribute('download')) return;
184
-
227
+
185
228
  const url = new URL(link.href);
186
229
  if (url.origin !== location.origin) return;
187
230
  if (url.pathname === window.location.pathname && url.hash) return;
188
-
231
+
189
232
  e.preventDefault();
190
233
  await navigateTo(url.href);
191
234
  });
192
235
 
193
236
  window.addEventListener('popstate', () => {
194
- if (window.location.pathname === currentPath) return;
237
+ if (window.location.pathname === currentPath) return;
195
238
  navigateTo(window.location.href, false);
196
239
  });
197
240
 
198
241
  async function navigateTo(url, pushHistory = true) {
199
242
  const layout = document.querySelector('.content-layout');
200
-
243
+
201
244
  try {
202
245
  if (layout) layout.style.minHeight = layout.getBoundingClientRect().height + 'px';
203
-
246
+
204
247
  let data;
205
248
  if (pageCache.has(url)) {
206
- data = await pageCache.get(url);
207
- data.html = await data.html;
249
+ data = await pageCache.get(url);
250
+ data.html = await data.html;
208
251
  } else {
209
- const res = await fetch(url);
210
- if (!res.ok) throw new Error('Fetch failed');
211
- data = { html: await res.text(), finalUrl: res.url };
212
- pageCache.set(url, Promise.resolve(data));
252
+ const res = await fetch(url);
253
+ if (!res.ok) throw new Error('Fetch failed');
254
+ data = { html: await res.text(), finalUrl: res.url };
255
+ pageCache.set(url, Promise.resolve(data));
213
256
  }
214
257
 
215
258
  const finalUrl = data.finalUrl;
216
259
  const html = data.html;
217
-
260
+
218
261
  const parser = new DOMParser();
219
262
  const doc = parser.parseFromString(html, 'text/html');
220
263
 
@@ -228,58 +271,62 @@
228
271
  const newAssets = Array.from(doc.head.querySelectorAll(assetSelectors));
229
272
 
230
273
  newAssets.forEach((newAsset, index) => {
231
- if (oldAssets[index]) {
232
- if (oldAssets[index].getAttribute('href') !== newAsset.getAttribute('href')) {
233
- oldAssets[index].setAttribute('href', newAsset.getAttribute('href'));
234
- }
235
- } else {
236
- document.head.appendChild(newAsset.cloneNode(true));
274
+ if (oldAssets[index]) {
275
+ if (oldAssets[index].getAttribute('href') !== newAsset.getAttribute('href')) {
276
+ oldAssets[index].setAttribute('href', newAsset.getAttribute('href'));
237
277
  }
278
+ } else {
279
+ document.head.appendChild(newAsset.cloneNode(true));
280
+ }
238
281
  });
239
282
 
240
283
  // Sync Sidebar State
241
284
  const oldLis = Array.from(document.querySelectorAll('.sidebar-nav li'));
242
285
  const newLis = Array.from(doc.querySelectorAll('.sidebar-nav li'));
243
-
286
+
244
287
  oldLis.forEach((oldLi, i) => {
245
- const newLi = newLis[i];
246
- if (newLi) {
247
- oldLi.classList.toggle('active', newLi.classList.contains('active'));
248
- oldLi.classList.toggle('active-parent', newLi.classList.contains('active-parent'));
249
-
250
- if (newLi.classList.contains('expanded')) {
251
- oldLi.classList.add('expanded');
252
- oldLi.setAttribute('aria-expanded', 'true');
253
- }
254
-
255
- const oldA = oldLi.querySelector('a');
256
- const newA = newLi.querySelector('a');
257
- if (oldA && newA) {
258
- oldA.setAttribute('href', newA.getAttribute('href'));
259
- oldA.classList.toggle('active', newA.classList.contains('active'));
260
- }
288
+ const newLi = newLis[i];
289
+ if (newLi) {
290
+ oldLi.classList.toggle('active', newLi.classList.contains('active'));
291
+ oldLi.classList.toggle('active-parent', newLi.classList.contains('active-parent'));
292
+
293
+ if (newLi.classList.contains('expanded')) {
294
+ oldLi.classList.add('expanded');
295
+ oldLi.setAttribute('aria-expanded', 'true');
261
296
  }
297
+
298
+ const oldA = oldLi.querySelector('a');
299
+ const newA = newLi.querySelector('a');
300
+ if (oldA && newA) {
301
+ oldA.setAttribute('href', newA.getAttribute('href'));
302
+ oldA.classList.toggle('active', newA.classList.contains('active'));
303
+ }
304
+ }
262
305
  });
263
306
 
264
- const selectorsToSwap =[
265
- '.content-layout',
266
- '.page-header .header-title',
267
- '.page-footer',
307
+ const selectorsToSwap = [
308
+ '.content-layout',
309
+ '.page-header .header-title',
310
+ '.page-footer',
268
311
  '.footer-complete',
269
312
  '.page-footer-actions'
270
313
  ];
271
314
 
272
315
  selectorsToSwap.forEach(selector => {
273
- const oldEl = document.querySelector(selector);
274
- const newEl = doc.querySelector(selector);
275
- if (oldEl && newEl) oldEl.innerHTML = newEl.innerHTML;
316
+ const oldEl = document.querySelector(selector);
317
+ const newEl = doc.querySelector(selector);
318
+ if (oldEl && newEl) oldEl.innerHTML = newEl.innerHTML;
276
319
  });
277
320
 
278
321
  const hash = new URL(finalUrl).hash;
279
322
  if (hash) {
323
+ try {
280
324
  document.querySelector(hash)?.scrollIntoView();
325
+ } catch (e) {
326
+ document.getElementById(hash.substring(1))?.scrollIntoView();
327
+ }
281
328
  } else {
282
- window.scrollTo(0, 0);
329
+ window.scrollTo(0, 0);
283
330
  }
284
331
 
285
332
  injectCopyButtons();
@@ -290,36 +337,34 @@
290
337
  document.dispatchEvent(new CustomEvent('docmd:page-mounted', { detail: { url: finalUrl } }));
291
338
 
292
339
  setTimeout(() => {
293
- const newLayout = document.querySelector('.content-layout');
294
- if (newLayout) newLayout.style.minHeight = '';
340
+ const newLayout = document.querySelector('.content-layout');
341
+ if (newLayout) newLayout.style.minHeight = '';
295
342
  }, 100);
296
343
 
297
- } catch(e) {
344
+ } catch (e) {
298
345
  window.location.assign(url);
299
346
  }
300
347
  }
301
348
  }
302
349
 
303
- // =========================================================================
304
350
  // 4. BOOTSTRAP
305
- // =========================================================================
306
351
  document.addEventListener('DOMContentLoaded', () => {
307
352
  if (localStorage.getItem('docmd-sidebar-collapsed') === 'true') {
308
- document.body.classList.add('sidebar-collapsed');
353
+ document.body.classList.add('sidebar-collapsed');
309
354
  }
310
-
355
+
311
356
  document.querySelectorAll('.theme-toggle-button').forEach(btn => {
312
357
  btn.addEventListener('click', () => {
313
358
  const t = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
314
359
  document.documentElement.setAttribute('data-theme', t);
315
360
  document.body.setAttribute('data-theme', t);
316
361
  localStorage.setItem('docmd-theme', t);
317
-
362
+
318
363
  const lightLink = document.getElementById('hljs-light');
319
364
  const darkLink = document.getElementById('hljs-dark');
320
365
  if (lightLink && darkLink) {
321
- lightLink.disabled = t === 'dark';
322
- darkLink.disabled = t === 'light';
366
+ lightLink.disabled = t === 'dark';
367
+ darkLink.disabled = t === 'light';
323
368
  }
324
369
  });
325
370
  });
@@ -327,16 +372,29 @@
327
372
  injectCopyButtons();
328
373
  initializeScrollSpy();
329
374
  initializeSPA();
330
-
375
+
331
376
  setTimeout(() => {
332
- const activeNav = document.querySelector('.sidebar-nav a.active');
333
- const sidebarNav = document.querySelector('.sidebar-nav');
334
- if (activeNav && sidebarNav) {
335
- sidebarNav.scrollTo({ top: activeNav.offsetTop - (sidebarNav.clientHeight / 2), behavior: 'instant' });
336
- }
337
- if (window.location.hash) {
338
- document.querySelector(window.location.hash)?.scrollIntoView();
377
+ // PWA Unregistration Safety Net:
378
+ // If the PWA plugin is removed from docmd.config.js, the <link rel="manifest"> disappears.
379
+ // We explicitly unregister all ghost service workers to safely kill the offline cache.
380
+ if ('serviceWorker' in navigator && !document.querySelector('link[rel="manifest"]')) {
381
+ navigator.serviceWorker.getRegistrations().then(registrations => {
382
+ registrations.forEach(reg => reg.unregister().catch(() => { }));
383
+ });
384
+ }
385
+
386
+ const activeNav = document.querySelector('.sidebar-nav a.active');
387
+ const sidebarNav = document.querySelector('.sidebar-nav');
388
+ if (activeNav && sidebarNav) {
389
+ sidebarNav.scrollTo({ top: activeNav.offsetTop - (sidebarNav.clientHeight / 2), behavior: 'instant' });
390
+ }
391
+ if (window.location.hash) {
392
+ try {
393
+ document.querySelector(window.location.hash)?.scrollIntoView();
394
+ } catch (e) {
395
+ document.getElementById(window.location.hash.substring(1))?.scrollIntoView();
339
396
  }
397
+ }
340
398
  }, 100);
341
399
  });
342
400
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@docmd/ui",
3
- "version": "0.4.11",
4
- "description": "Base UI templates and assets for docmd",
3
+ "version": "0.5.1",
4
+ "description": "Base UI templates and assets for docmd.",
5
5
  "main": "index.js",
6
6
  "files": [
7
7
  "templates",
@@ -0,0 +1,89 @@
1
+ <!--
2
+ ---------------------------------------------------------------
3
+ docmd : the minimalist, zero-config documentation generator.
4
+ @website https://docmd.io
5
+ [docmd-source] - Please do not remove this header.
6
+ ---------------------------------------------------------------
7
+ -->
8
+
9
+ <!DOCTYPE html>
10
+ <html lang="en">
11
+ <head>
12
+ <meta charset="UTF-8">
13
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
14
+ <title><%= pageTitle %></title>
15
+
16
+ <!-- Force absolute paths for assets using relativePathToRoot (which we will set to '/') -->
17
+ <%- faviconLinkHtml || '' %>
18
+
19
+ <!-- 1. Core CSS -->
20
+ <link rel="stylesheet" href="<%= relativePathToRoot %>assets/css/docmd-main.css?v=<%= buildHash %>">
21
+
22
+ <!-- 2. Highlight.js (Theme aware) -->
23
+ <%
24
+ const isDarkDefault = defaultMode === 'dark';
25
+ %>
26
+ <link rel="stylesheet" href="<%= relativePathToRoot %>assets/css/docmd-highlight-light.css?v=<%= buildHash %>" id="hljs-light" <%= isDarkDefault ? 'disabled' : '' %>>
27
+ <link rel="stylesheet" href="<%= relativePathToRoot %>assets/css/docmd-highlight-dark.css?v=<%= buildHash %>" id="hljs-dark" <%= isDarkDefault ? '' : 'disabled' %>>
28
+
29
+ <!-- 3. Theme CSS -->
30
+ <% if (theme && theme.name !== 'default') { %>
31
+ <link rel="stylesheet" href="<%= relativePathToRoot %>assets/css/docmd-theme-<%= theme.name %>.css?v=<%= buildHash %>">
32
+ <% } %>
33
+
34
+ <!-- 4. Custom CSS -->
35
+ <% (customCssFiles || []).forEach(cssFile => {
36
+ const cleanPath = cssFile.startsWith('/') ? cssFile.substring(1) : cssFile;
37
+ %>
38
+ <link rel="stylesheet" href="<%= relativePathToRoot %><%= cleanPath %>?v=<%= buildHash %>">
39
+ <% }); %>
40
+
41
+ <!-- 5. Theme Init (Dark Mode logic) -->
42
+ <%- themeInitScript %>
43
+
44
+ <style>
45
+ body {
46
+ display: flex;
47
+ flex-direction: column;
48
+ align-items: center;
49
+ justify-content: center;
50
+ min-height: 100vh;
51
+ text-align: center;
52
+ padding: 20px;
53
+ background-color: var(--bg-color);
54
+ color: var(--text-color);
55
+ }
56
+ .error-logo { height: 60px; margin-bottom: 2rem; width: auto; display: block; }
57
+ .error-logo.light { display: var(--display-light, block); }
58
+ .error-logo.dark { display: var(--display-dark, none); }
59
+
60
+ /* Simple theme logic for logo if JS fails */
61
+ @media (prefers-color-scheme: dark) {
62
+ :root[data-theme="system"] .error-logo.light { display: none; }
63
+ :root[data-theme="system"] .error-logo.dark { display: block; }
64
+ }
65
+ :root[data-theme="dark"] .error-logo.light { display: none; }
66
+ :root[data-theme="dark"] .error-logo.dark { display: block; }
67
+
68
+ h1 { font-size: 5rem; margin: 0; line-height: 1; color: var(--link-color); font-weight: 700; }
69
+ h2 { font-size: 2rem; margin: 1rem 0; }
70
+ p { font-size: 1.2rem; color: var(--text-muted); margin-bottom: 2rem; max-width: 500px; }
71
+ .docmd-button { font-size: 1.1rem; padding: 0.8rem 1.5rem; }
72
+ </style>
73
+ </head>
74
+ <body>
75
+ <% if (logo) { %>
76
+ <% if (logo.light) { %>
77
+ <img src="<%= relativePathToRoot %><%= logo.light.replace(/^\//, '') %>" class="error-logo light" alt="Logo">
78
+ <% } %>
79
+ <% if (logo.dark) { %>
80
+ <img src="<%= relativePathToRoot %><%= logo.dark.replace(/^\//, '') %>" class="error-logo dark" alt="Logo">
81
+ <% } %>
82
+ <% } %>
83
+
84
+ <h1>404</h1>
85
+ <h2>Page Not Found</h2>
86
+ <p><%= content %></p>
87
+ <a href="<%= relativePathToRoot %>" class="docmd-button">Return Home</a>
88
+ </body>
89
+ </html>
@@ -10,14 +10,40 @@
10
10
  <html lang="en">
11
11
  <head>
12
12
  <meta charset="UTF-8">
13
- <meta name="generator" content="docmd v0.4.x">
13
+ <meta name="generator" content="docmd v0.5.x">
14
14
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
15
+ <%
16
+ let versionRoot = '/';
17
+ let siteRoot = relativePathToRoot;
18
+
19
+ if (config.versions?.current && config._activeVersion?.id) {
20
+ const isSubVersion = config.versions.current !== config._activeVersion.id;
21
+ versionRoot = isSubVersion ? '/' + config._activeVersion.id + '/' : '/';
22
+
23
+ // If we are in a sub-version (v04/), the relativePathToRoot takes us to /v04/.
24
+ // We need to go up one more level to reach the true site root.
25
+ if (isSubVersion) {
26
+ siteRoot = (relativePathToRoot === './' ? '' : relativePathToRoot) + '../';
27
+ }
28
+ }
29
+
30
+ if (locals.isOfflineMode) {
31
+ versionRoot = '';
32
+ }
33
+ %>
15
34
  <script>
16
35
  var root = "<%= relativePathToRoot %>";
17
36
  if (root && !root.endsWith('/')) root += '/';
18
37
  if (root === '') root = './';
19
- window.DOCMD_ROOT = root;
38
+ window.DOCMD_ROOT = root; // Context Root (Assets)
39
+
40
+ var siteRoot = "<%= siteRoot %>";
41
+ if (siteRoot && !siteRoot.endsWith('/')) siteRoot += '/';
42
+ if (siteRoot === '') siteRoot = './';
43
+ window.DOCMD_SITE_ROOT = siteRoot; // True Site Root (Search Index)
44
+
20
45
  window.DOCMD_DEFAULT_MODE = "<%= defaultMode %>";
46
+ window.DOCMD_VERSION_ROOT = "<%- versionRoot %>";
21
47
  </script>
22
48
  <title><%= pageTitle %></title>
23
49
  <%- faviconLinkHtml || '' %>
@@ -41,42 +67,60 @@
41
67
  <% }); %>
42
68
  <%- themeInitScript %>
43
69
  </head>
44
- <body class="<%= sidebarConfig?.collapsible ? 'sidebar-collapsible' : 'sidebar-not-collapsible' %>"
45
- data-default-collapsed="<%= sidebarConfig?.defaultCollapsed %>"
46
- data-copy-code-enabled="<%= config.copyCode === true %>"
47
- data-spa-enabled="<%= config.layout?.spa !== false %>">
70
+ <body class="<%= sidebarConfig?.enabled === false ? 'no-sidebar' : (sidebarConfig?.collapsible ? 'sidebar-collapsible' : 'sidebar-not-collapsible') %>"
71
+ data-default-collapsed="<%= sidebarConfig?.defaultCollapsed %>"
72
+ data-copy-code-enabled="<%= config.copyCode === true %>"
73
+ data-spa-enabled="<%= config.layout?.spa !== false %>">
48
74
 
49
75
  <a href="#main-content" class="skip-link">Skip to main content</a>
50
-
51
- <aside class="sidebar">
52
- <div class="sidebar-header">
53
- <% if (logo && logo.light && logo.dark) { %>
54
- <a href="<%= logo.href || relativePathToRoot %>" class="logo-link">
55
- <img src="<%= relativePathToRoot %><%- logo.light.startsWith('/') ? logo.light.substring(1) : logo.light %>" alt="<%= logo.alt || siteTitle %>" class="logo-light" <% if (logo.height) { %>style="height: <%= logo.height %>;"<% } %>>
56
- <img src="<%= relativePathToRoot %><%- logo.dark.startsWith('/') ? logo.dark.substring(1) : logo.dark %>" alt="<%= logo.alt || siteTitle %>" class="logo-dark" <% if (logo.height) { %>style="height: <%= logo.height %>;"<% } %>>
57
- </a>
58
- <% } else { %>
59
- <h1><a href="<%= relativePathToRoot %>index.html"><%= siteTitle %></a></h1>
60
- <% } %>
61
- <span class="mobile-view sidebar-menu-button float-right">
62
- <%- renderIcon("menu") %>
63
- </span>
64
- </div>
65
76
 
66
- <% if (optionsMenu?.position === 'sidebar-top') { %>
67
- <div class="sidebar-options-wrapper">
68
- <%- include('partials/options-menu', { optionsMenu }) %>
77
+ <% if (sidebarConfig?.enabled !== false) { %>
78
+ <aside class="sidebar">
79
+ <div class="sidebar-header">
80
+ <% if (logo && logo.light && logo.dark) { %>
81
+ <a href="<%= logo.href || relativePathToRoot %>" class="logo-link">
82
+ <img src="<%= relativePathToRoot %><%- logo.light.startsWith('/') ? logo.light.substring(1) : logo.light %>" alt="<%= logo.alt || siteTitle %>" class="logo-light" <% if (logo.height) { %>style="height: <%= logo.height %>;"<% } %>>
83
+ <img src="<%= relativePathToRoot %><%- logo.dark.startsWith('/') ? logo.dark.substring(1) : logo.dark %>" alt="<%= logo.alt || siteTitle %>" class="logo-dark" <% if (logo.height) { %>style="height: <%= logo.height %>;"<% } %>>
84
+ </a>
85
+ <% } else { %>
86
+ <h1><a href="<%= relativePathToRoot %>index.html"><%= siteTitle %></a></h1>
87
+ <% } %>
88
+ <span class="mobile-view sidebar-menu-button float-right">
89
+ <%- renderIcon("menu") %>
90
+ </span>
69
91
  </div>
70
- <% } %>
71
92
 
72
- <%- navigationHtml %>
93
+ <div class="sidebar-top-group">
94
+ <% if (locals.optionsMenu && optionsMenu.position === 'sidebar-top') { %>
95
+ <div class="sidebar-options-wrapper">
96
+ <%- include('partials/options-menu', { optionsMenu }) %>
97
+ </div>
98
+ <% } %>
99
+
100
+ <% if (config.versions && config.versions.position === 'sidebar-top') { %>
101
+ <div class="sidebar-version-wrapper">
102
+ <%- include('partials/version-dropdown', { versions: config.versions, activeVersion: config._activeVersion, relativePathToRoot }) %>
103
+ </div>
104
+ <% } %>
105
+ </div>
106
+
107
+ <%- navigationHtml %>
108
+
109
+ <div class="sidebar-bottom-group mt-auto">
110
+ <% if (config.versions && config.versions.position === 'sidebar-bottom') { %>
111
+ <div class="sidebar-version-wrapper">
112
+ <%- include('partials/version-dropdown', { versions: config.versions, activeVersion: config._activeVersion, relativePathToRoot }) %>
113
+ </div>
114
+ <% } %>
73
115
 
74
- <% if (optionsMenu?.position === 'sidebar-bottom') { %>
75
- <div class="sidebar-options-wrapper mt-auto">
76
- <%- include('partials/options-menu', { optionsMenu }) %>
116
+ <% if (locals.optionsMenu && optionsMenu.position === 'sidebar-bottom') { %>
117
+ <div class="sidebar-options-wrapper">
118
+ <%- include('partials/options-menu', { optionsMenu }) %>
119
+ </div>
120
+ <% } %>
77
121
  </div>
78
- <% } %>
79
- </aside>
122
+ </aside>
123
+ <% } %>
80
124
 
81
125
  <div class="main-content-wrapper">
82
126
  <% if (headerConfig?.enabled !== false) { %>
@@ -10,7 +10,7 @@
10
10
  <html lang="en">
11
11
  <head>
12
12
  <meta charset="UTF-8">
13
- <meta name="generator" content="docmd v0.4.x">
13
+ <meta name="generator" content="docmd v0.5.x">
14
14
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
15
15
 
16
16
  <!-- 1. GLOBALS -->
@@ -40,18 +40,22 @@
40
40
 
41
41
  <div class="footer-complete-bottom">
42
42
  <div class="user-footer"><%- footerHtml || (footerConfig.copyright ? footerConfig.copyright : '') %></div>
43
+ <% if (footerConfig.branding !== false) { %>
43
44
  <div class="branding-footer">
44
45
  Built with <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"></path><path d="M12 5 9.04 7.96a2.17 2.17 0 0 0 0 3.08c.82.82 2.13.85 3 .07l2.07-1.9a2.82 2.82 0 0 1 3.79 0l2.96 2.66"></path><path d="m18 15-2-2"></path><path d="m15 18-2-2"></path></svg> <a href="https://docmd.io" target="_blank" rel="noopener">docmd.</a>
45
46
  </div>
47
+ <% } %>
46
48
  </div>
47
49
  </footer>
48
50
  <% } else { %>
49
51
  <footer class="page-footer">
50
52
  <div class="footer-content">
51
53
  <div class="user-footer"><%- footerHtml || (footerConfig?.copyright ? footerConfig.copyright : '') %></div>
54
+ <% if (footerConfig?.branding !== false) { %>
52
55
  <div class="branding-footer">
53
56
  Built with <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"></path><path d="M12 5 9.04 7.96a2.17 2.17 0 0 0 0 3.08c.82.82 2.13.85 3 .07l2.07-1.9a2.82 2.82 0 0 1 3.79 0l2.96 2.66"></path><path d="m18 15-2-2"></path><path d="m15 18-2-2"></path></svg> <a href="https://docmd.io" target="_blank" rel="noopener">docmd.</a>
54
57
  </div>
58
+ <% } %>
55
59
  </div>
56
60
  </footer>
57
61
  <% } %>
@@ -0,0 +1,50 @@
1
+ <%# ---------------------------------------------------------------
2
+ # docmd : the minimalist, zero-config documentation generator.
3
+ # @website https://docmd.io
4
+ # [docmd-source] - Please do not remove this header.
5
+ # ---------------------------------------------------------------
6
+ %>
7
+
8
+ <% if (versions && versions.all.length > 0) {
9
+ const current = activeVersion || versions.all.find(v => v.id === versions.current);
10
+
11
+ // If we are in a sub-version (e.g. /v1/), we go up one level (../) to get to root.
12
+ const isCurrentSubVersion = current && current.id !== versions.current;
13
+ const upOneLevel = isCurrentSubVersion ? '../' : '';
14
+ %>
15
+ <div class="docmd-version-dropdown">
16
+ <button class="version-dropdown-toggle" aria-expanded="false" aria-label="Select Version">
17
+ <span class="version-label"><%= current ? current.label : 'Version' %></span>
18
+ <%- renderIcon('chevron-down', { class: 'version-chevron' }) %>
19
+ </button>
20
+ <ul class="version-dropdown-menu">
21
+ <% versions.all.forEach(v => {
22
+ const isCurrentActive = current && v.id === current.id;
23
+ const isTargetRootVersion = v.id === versions.current;
24
+
25
+ // We use the 'config' object which is available in all templates
26
+
27
+ const base = config.base || '/';
28
+ const normalizedBase = base.endsWith('/') ? base : base + '/';
29
+
30
+ const targetSuffix = isTargetRootVersion ? '' : v.id + '/';
31
+ const absoluteHref = normalizedBase + targetSuffix;
32
+
33
+ // Data Root for JS Sticky Logic
34
+ const dataRoot = absoluteHref;
35
+ %>
36
+ <li>
37
+ <a href="<%= absoluteHref %>"
38
+ class="version-dropdown-item <%= isCurrentActive ? 'active' : '' %>"
39
+ data-spa-ignore
40
+ data-version-root="<%= dataRoot %>">
41
+ <%= v.label %>
42
+ <% if (isCurrentActive) { %>
43
+ <%- renderIcon('check', { class: 'version-check' }) %>
44
+ <% } %>
45
+ </a>
46
+ </li>
47
+ <% }) %>
48
+ </ul>
49
+ </div>
50
+ <% } %>