@docmd/ui 0.4.10 → 0.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.
@@ -155,7 +155,8 @@ pre {
155
155
  }
156
156
 
157
157
  a:any-link {
158
- color: var(--link-color)
158
+ color: var(--link-color);
159
+ text-decoration: none;
159
160
  }
160
161
 
161
162
  .skip-link {
@@ -175,18 +176,19 @@ a:any-link {
175
176
  .sidebar {
176
177
  display: flex;
177
178
  flex-direction: column;
178
- width: 260px;
179
+ width: var(--sidebar-width);
179
180
  background-color: var(--sidebar-bg);
180
181
  color: var(--sidebar-text);
181
- padding: .5em .25em .5em .5em;
182
182
  border-right: 1px solid var(--border-color);
183
183
  height: 100vh;
184
184
  position: fixed;
185
+ padding: .5em .25em .5em .5em;
185
186
  top: 0;
186
187
  left: 0;
187
- overflow-y: auto;
188
+ overflow: hidden;
188
189
  box-sizing: border-box;
189
- flex-shrink: 0
190
+ flex-shrink: 0;
191
+ z-index: 100;
190
192
  }
191
193
 
192
194
  .sidebar h1 {
@@ -526,7 +528,7 @@ body.sidebar-collapsed .main-content-wrapper {
526
528
  }
527
529
 
528
530
  .sidebar-options-wrapper {
529
- padding: .5rem 0;
531
+ padding: .25rem 0;
530
532
  }
531
533
 
532
534
  .sidebar-options-wrapper.mt-auto {
@@ -535,6 +537,123 @@ body.sidebar-collapsed .main-content-wrapper {
535
537
  border-top: 1px solid var(--border-color);
536
538
  }
537
539
 
540
+ /* --- Version Dropdown (Flat UI) --- */
541
+ .sidebar-version-wrapper {
542
+ padding: 0.25rem .5em;
543
+ border-bottom: 1px solid var(--border-color);
544
+ }
545
+
546
+ .sidebar-bottom-group.mt-auto {
547
+ margin-top: auto;
548
+ }
549
+
550
+ .sidebar-bottom-group .sidebar-version-wrapper {
551
+ border-bottom: none;
552
+ }
553
+
554
+ .docmd-version-dropdown {
555
+ position: relative;
556
+ width: 100%;
557
+ }
558
+
559
+ .version-dropdown-toggle {
560
+ display: flex;
561
+ align-items: center;
562
+ justify-content: space-between;
563
+ width: 100%;
564
+ padding: 0.5rem 0.75rem;
565
+ background-color: var(--bg-color);
566
+ border: 1px solid var(--border-color);
567
+ border-radius: var(--ui-border-radius);
568
+ color: var(--text-heading);
569
+ font-size: 0.85rem;
570
+ font-weight: 500;
571
+ cursor: pointer;
572
+ transition: all 0.2s ease;
573
+ }
574
+
575
+ .version-dropdown-toggle:hover {
576
+ border-color: var(--text-muted);
577
+ }
578
+
579
+ .version-dropdown-menu {
580
+ position: absolute;
581
+ top: calc(100% + 4px);
582
+ left: 0;
583
+ right: 0;
584
+ margin: 0;
585
+ padding: 0.25rem;
586
+ background-color: var(--bg-color);
587
+ border: 1px solid var(--border-color);
588
+ border-radius: var(--ui-border-radius);
589
+ box-shadow: var(--shadow-md);
590
+ list-style: none;
591
+ z-index: 100;
592
+ opacity: 0;
593
+ visibility: hidden;
594
+ transform: translateY(-5px);
595
+ transition: all 0.2s ease;
596
+ }
597
+
598
+ .docmd-version-dropdown.open .version-dropdown-menu {
599
+ opacity: 1;
600
+ visibility: visible;
601
+ transform: translateY(0);
602
+ }
603
+
604
+ .version-dropdown-item {
605
+ display: flex;
606
+ align-items: center;
607
+ justify-content: space-between;
608
+ padding: 0.4rem 0.5rem;
609
+ color: var(--text-color);
610
+ font-size: 0.85rem;
611
+ text-decoration: none;
612
+ border-radius: 4px;
613
+ transition: background-color 0.2s;
614
+ }
615
+
616
+ .version-dropdown-item:hover {
617
+ background-color: var(--sidebar-link-active-bg);
618
+ color: var(--text-heading);
619
+ text-decoration: none;
620
+ }
621
+
622
+ .version-dropdown-item.active {
623
+ font-weight: 600;
624
+ color: var(--link-color);
625
+ background-color: var(--sidebar-link-active-bg);
626
+ }
627
+
628
+ .version-check {
629
+ width: 1rem;
630
+ height: 1rem;
631
+ }
632
+
633
+ .version-chevron {
634
+ width: 1rem;
635
+ height: 1rem;
636
+ transition: transform 0.2s ease;
637
+ }
638
+
639
+ .docmd-version-dropdown.open .version-chevron {
640
+ transform: rotate(180deg);
641
+ }
642
+
643
+ .sidebar-bottom-group .version-dropdown-menu {
644
+ top: auto;
645
+ bottom: calc(100% + 4px);
646
+ transform: translateY(5px);
647
+ }
648
+
649
+ .sidebar-bottom-group .docmd-version-dropdown.open .version-dropdown-menu {
650
+ transform: translateY(0);
651
+ }
652
+
653
+ .sidebar-bottom-group .version-dropdown-toggle .version-chevron {
654
+ transform: rotate(180deg);
655
+ }
656
+
538
657
  .card .card-title {
539
658
  border-bottom: 1px solid var(--border-color)
540
659
  }
@@ -644,6 +763,7 @@ html[data-theme=dark] .theme-toggle-button .icon-sun {
644
763
  .sidebar-nav {
645
764
  flex-grow: 1;
646
765
  overflow-y: auto;
766
+ overflow-x: hidden;
647
767
  min-height: 0;
648
768
  scrollbar-width: thin;
649
769
  }
@@ -11,6 +11,7 @@
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)
@@ -18,9 +19,8 @@
18
19
  */
19
20
 
20
21
  (function() {
21
- // =========================================================================
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');
@@ -61,6 +61,51 @@
61
61
  tabItem.classList.add('active');
62
62
  if (tabPanes[index]) tabPanes[index].classList.add('active');
63
63
  }
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
+ }
64
109
 
65
110
  // Copy Code Button
66
111
  const copyBtn = e.target.closest('.copy-code-button');
@@ -79,9 +124,7 @@
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>`;
@@ -143,19 +186,42 @@
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;
152
193
 
153
194
  let currentPath = window.location.pathname;
195
+ const pageCache = new Map();
196
+ let prefetchTimer = null;
197
+
198
+ // Intent-based Hover Prefetching
199
+ document.addEventListener('mouseover', (e) => {
200
+ const link = e.target.closest('.sidebar-nav a, .page-navigation a');
201
+ if (!link || link.target === '_blank' || link.hasAttribute('download')) return;
202
+
203
+ const url = new URL(link.href).href;
204
+ if (new URL(url).origin !== location.origin) return;
205
+ if (pageCache.has(url)) return;
206
+
207
+ // Wait 65ms to ensure the user actually intends to click
208
+ clearTimeout(prefetchTimer);
209
+ prefetchTimer = setTimeout(() => {
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)));
214
+ }, 65);
215
+ });
216
+
217
+ // Cancel prefetch if the mouse leaves before the 65ms "intent" delay
218
+ document.addEventListener('mouseout', () => clearTimeout(prefetchTimer));
154
219
 
155
220
  document.addEventListener('click', async (e) => {
156
- // Ignore clicks on expand/collapse arrows so they don't trigger navigation
157
221
  if (e.target.closest('.collapse-icon-wrapper')) return;
158
222
 
223
+ if (e.target.closest('[data-spa-ignore]')) return;
224
+
159
225
  const link = e.target.closest('.sidebar-nav a, .page-navigation a');
160
226
  if (!link || link.target === '_blank' || link.hasAttribute('download')) return;
161
227
 
@@ -168,7 +234,7 @@
168
234
  });
169
235
 
170
236
  window.addEventListener('popstate', () => {
171
- if (window.location.pathname === currentPath) return; // Ignore hash-only changes
237
+ if (window.location.pathname === currentPath) return;
172
238
  navigateTo(window.location.href, false);
173
239
  });
174
240
 
@@ -176,13 +242,22 @@
176
242
  const layout = document.querySelector('.content-layout');
177
243
 
178
244
  try {
179
- // Lock height to prevent scrollbar jitter/dragging during DOM swap
180
245
  if (layout) layout.style.minHeight = layout.getBoundingClientRect().height + 'px';
181
246
 
182
- const res = await fetch(url);
183
- if (!res.ok) throw new Error('Fetch failed');
184
- const finalUrl = res.url;
185
- const html = await res.text();
247
+ let data;
248
+ if (pageCache.has(url)) {
249
+ data = await pageCache.get(url);
250
+ data.html = await data.html;
251
+ } else {
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));
256
+ }
257
+
258
+ const finalUrl = data.finalUrl;
259
+ const html = data.html;
260
+
186
261
  const parser = new DOMParser();
187
262
  const doc = parser.parseFromString(html, 'text/html');
188
263
 
@@ -205,24 +280,21 @@
205
280
  }
206
281
  });
207
282
 
208
- // Memorize Sidebar
283
+ // Sync Sidebar State
209
284
  const oldLis = Array.from(document.querySelectorAll('.sidebar-nav li'));
210
285
  const newLis = Array.from(doc.querySelectorAll('.sidebar-nav li'));
211
286
 
212
287
  oldLis.forEach((oldLi, i) => {
213
288
  const newLi = newLis[i];
214
289
  if (newLi) {
215
- // Sync active classes
216
290
  oldLi.classList.toggle('active', newLi.classList.contains('active'));
217
291
  oldLi.classList.toggle('active-parent', newLi.classList.contains('active-parent'));
218
292
 
219
- // Add expanded class if the new page requires it, but NEVER remove it
220
293
  if (newLi.classList.contains('expanded')) {
221
294
  oldLi.classList.add('expanded');
222
295
  oldLi.setAttribute('aria-expanded', 'true');
223
296
  }
224
297
 
225
- // Sync relative hrefs
226
298
  const oldA = oldLi.querySelector('a');
227
299
  const newA = newLi.querySelector('a');
228
300
  if (oldA && newA) {
@@ -232,7 +304,6 @@
232
304
  }
233
305
  });
234
306
 
235
- // 3. Swap Body Components (Removed .sidebar-nav from this list)
236
307
  const selectorsToSwap =[
237
308
  '.content-layout',
238
309
  '.page-header .header-title',
@@ -247,7 +318,6 @@
247
318
  if (oldEl && newEl) oldEl.innerHTML = newEl.innerHTML;
248
319
  });
249
320
 
250
- // Scroll & Init
251
321
  const hash = new URL(finalUrl).hash;
252
322
  if (hash) {
253
323
  document.querySelector(hash)?.scrollIntoView();
@@ -262,7 +332,6 @@
262
332
 
263
333
  document.dispatchEvent(new CustomEvent('docmd:page-mounted', { detail: { url: finalUrl } }));
264
334
 
265
- // Unlock height smoothly
266
335
  setTimeout(() => {
267
336
  const newLayout = document.querySelector('.content-layout');
268
337
  if (newLayout) newLayout.style.minHeight = '';
@@ -274,9 +343,7 @@
274
343
  }
275
344
  }
276
345
 
277
- // =========================================================================
278
346
  // 4. BOOTSTRAP
279
- // =========================================================================
280
347
  document.addEventListener('DOMContentLoaded', () => {
281
348
  if (localStorage.getItem('docmd-sidebar-collapsed') === 'true') {
282
349
  document.body.classList.add('sidebar-collapsed');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docmd/ui",
3
- "version": "0.4.10",
3
+ "version": "0.5.0",
4
4
  "description": "Base UI templates and assets for docmd",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -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
+ <% } %>