@docmd/ui 0.5.0 → 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.
@@ -540,17 +540,12 @@ body.sidebar-collapsed .main-content-wrapper {
540
540
  /* --- Version Dropdown (Flat UI) --- */
541
541
  .sidebar-version-wrapper {
542
542
  padding: 0.25rem .5em;
543
- border-bottom: 1px solid var(--border-color);
544
543
  }
545
544
 
546
545
  .sidebar-bottom-group.mt-auto {
547
546
  margin-top: auto;
548
547
  }
549
548
 
550
- .sidebar-bottom-group .sidebar-version-wrapper {
551
- border-bottom: none;
552
- }
553
-
554
549
  .docmd-version-dropdown {
555
550
  position: relative;
556
551
  width: 100%;
@@ -18,7 +18,7 @@
18
18
  * --------------------------------------------------------------------
19
19
  */
20
20
 
21
- (function() {
21
+ (function () {
22
22
 
23
23
  // 1. EVENT DELEGATION
24
24
  document.addEventListener('click', (e) => {
@@ -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,14 @@
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
65
  // Version Dropdown Toggle
66
66
  const versionToggle = e.target.closest('.version-dropdown-toggle');
67
67
  if (versionToggle) {
@@ -75,28 +75,28 @@
75
75
  // Sticky Version Switching (Path Preservation)
76
76
  const versionLink = e.target.closest('.version-dropdown-item');
77
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
- }
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
98
97
  }
99
- // If sticky logic skipped (e.g. on 404 page or outside root), default <a> click handles it
98
+ }
99
+ // If sticky logic skipped (e.g. on 404 page or outside root), default <a> click handles it
100
100
  }
101
101
 
102
102
  // Close Dropdown if clicked outside
@@ -128,15 +128,15 @@
128
128
  function injectCopyButtons() {
129
129
  if (document.body.dataset.copyCodeEnabled !== 'true') return;
130
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>`;
131
-
131
+
132
132
  document.querySelectorAll('pre').forEach(preElement => {
133
- if (preElement.closest('.code-wrapper')) return;
133
+ if (preElement.closest('.code-wrapper')) return;
134
134
  const wrapper = document.createElement('div');
135
135
  wrapper.className = 'code-wrapper';
136
136
  wrapper.style.position = 'relative';
137
137
  preElement.parentNode.insertBefore(wrapper, preElement);
138
138
  wrapper.appendChild(preElement);
139
-
139
+
140
140
  const copyButton = document.createElement('button');
141
141
  copyButton.className = 'copy-code-button';
142
142
  copyButton.innerHTML = svg;
@@ -150,7 +150,7 @@
150
150
  const tocLinks = document.querySelectorAll('.toc-link');
151
151
  const headings = document.querySelectorAll('.main-content h2, .main-content h3, .main-content h4');
152
152
  const tocContainer = document.querySelector('.toc-list');
153
-
153
+
154
154
  if (tocLinks.length === 0 || headings.length === 0) return;
155
155
 
156
156
  scrollObserver = new IntersectionObserver((entries) => {
@@ -159,7 +159,7 @@
159
159
  tocLinks.forEach(link => link.classList.remove('active'));
160
160
  const id = entry.target.getAttribute('id');
161
161
  const activeLink = document.querySelector(`.toc-link[href="#${id}"]`);
162
-
162
+
163
163
  if (activeLink) {
164
164
  activeLink.classList.add('active');
165
165
  if (tocContainer) {
@@ -197,9 +197,9 @@
197
197
 
198
198
  // Intent-based Hover Prefetching
199
199
  document.addEventListener('mouseover', (e) => {
200
- 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');
201
201
  if (!link || link.target === '_blank' || link.hasAttribute('download')) return;
202
-
202
+
203
203
  const url = new URL(link.href).href;
204
204
  if (new URL(url).origin !== location.origin) return;
205
205
  if (pageCache.has(url)) return;
@@ -207,10 +207,10 @@
207
207
  // Wait 65ms to ensure the user actually intends to click
208
208
  clearTimeout(prefetchTimer);
209
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)));
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
214
  }, 65);
215
215
  });
216
216
 
@@ -222,42 +222,42 @@
222
222
 
223
223
  if (e.target.closest('[data-spa-ignore]')) return;
224
224
 
225
- const link = e.target.closest('.sidebar-nav a, .page-navigation a');
225
+ const link = e.target.closest('.sidebar-nav a, .page-navigation a, .page-footer a, .main-content a');
226
226
  if (!link || link.target === '_blank' || link.hasAttribute('download')) return;
227
-
227
+
228
228
  const url = new URL(link.href);
229
229
  if (url.origin !== location.origin) return;
230
230
  if (url.pathname === window.location.pathname && url.hash) return;
231
-
231
+
232
232
  e.preventDefault();
233
233
  await navigateTo(url.href);
234
234
  });
235
235
 
236
236
  window.addEventListener('popstate', () => {
237
- if (window.location.pathname === currentPath) return;
237
+ if (window.location.pathname === currentPath) return;
238
238
  navigateTo(window.location.href, false);
239
239
  });
240
240
 
241
241
  async function navigateTo(url, pushHistory = true) {
242
242
  const layout = document.querySelector('.content-layout');
243
-
243
+
244
244
  try {
245
245
  if (layout) layout.style.minHeight = layout.getBoundingClientRect().height + 'px';
246
-
246
+
247
247
  let data;
248
248
  if (pageCache.has(url)) {
249
- data = await pageCache.get(url);
250
- data.html = await data.html;
249
+ data = await pageCache.get(url);
250
+ data.html = await data.html;
251
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));
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
256
  }
257
257
 
258
258
  const finalUrl = data.finalUrl;
259
259
  const html = data.html;
260
-
260
+
261
261
  const parser = new DOMParser();
262
262
  const doc = parser.parseFromString(html, 'text/html');
263
263
 
@@ -271,58 +271,62 @@
271
271
  const newAssets = Array.from(doc.head.querySelectorAll(assetSelectors));
272
272
 
273
273
  newAssets.forEach((newAsset, index) => {
274
- if (oldAssets[index]) {
275
- if (oldAssets[index].getAttribute('href') !== newAsset.getAttribute('href')) {
276
- oldAssets[index].setAttribute('href', newAsset.getAttribute('href'));
277
- }
278
- } else {
279
- 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'));
280
277
  }
278
+ } else {
279
+ document.head.appendChild(newAsset.cloneNode(true));
280
+ }
281
281
  });
282
282
 
283
283
  // Sync Sidebar State
284
284
  const oldLis = Array.from(document.querySelectorAll('.sidebar-nav li'));
285
285
  const newLis = Array.from(doc.querySelectorAll('.sidebar-nav li'));
286
-
286
+
287
287
  oldLis.forEach((oldLi, i) => {
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');
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
- }
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');
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'));
304
303
  }
304
+ }
305
305
  });
306
306
 
307
- const selectorsToSwap =[
308
- '.content-layout',
309
- '.page-header .header-title',
310
- '.page-footer',
307
+ const selectorsToSwap = [
308
+ '.content-layout',
309
+ '.page-header .header-title',
310
+ '.page-footer',
311
311
  '.footer-complete',
312
312
  '.page-footer-actions'
313
313
  ];
314
314
 
315
315
  selectorsToSwap.forEach(selector => {
316
- const oldEl = document.querySelector(selector);
317
- const newEl = doc.querySelector(selector);
318
- 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;
319
319
  });
320
320
 
321
321
  const hash = new URL(finalUrl).hash;
322
322
  if (hash) {
323
+ try {
323
324
  document.querySelector(hash)?.scrollIntoView();
325
+ } catch (e) {
326
+ document.getElementById(hash.substring(1))?.scrollIntoView();
327
+ }
324
328
  } else {
325
- window.scrollTo(0, 0);
329
+ window.scrollTo(0, 0);
326
330
  }
327
331
 
328
332
  injectCopyButtons();
@@ -333,11 +337,11 @@
333
337
  document.dispatchEvent(new CustomEvent('docmd:page-mounted', { detail: { url: finalUrl } }));
334
338
 
335
339
  setTimeout(() => {
336
- const newLayout = document.querySelector('.content-layout');
337
- if (newLayout) newLayout.style.minHeight = '';
340
+ const newLayout = document.querySelector('.content-layout');
341
+ if (newLayout) newLayout.style.minHeight = '';
338
342
  }, 100);
339
343
 
340
- } catch(e) {
344
+ } catch (e) {
341
345
  window.location.assign(url);
342
346
  }
343
347
  }
@@ -346,21 +350,21 @@
346
350
  // 4. BOOTSTRAP
347
351
  document.addEventListener('DOMContentLoaded', () => {
348
352
  if (localStorage.getItem('docmd-sidebar-collapsed') === 'true') {
349
- document.body.classList.add('sidebar-collapsed');
353
+ document.body.classList.add('sidebar-collapsed');
350
354
  }
351
-
355
+
352
356
  document.querySelectorAll('.theme-toggle-button').forEach(btn => {
353
357
  btn.addEventListener('click', () => {
354
358
  const t = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
355
359
  document.documentElement.setAttribute('data-theme', t);
356
360
  document.body.setAttribute('data-theme', t);
357
361
  localStorage.setItem('docmd-theme', t);
358
-
362
+
359
363
  const lightLink = document.getElementById('hljs-light');
360
364
  const darkLink = document.getElementById('hljs-dark');
361
365
  if (lightLink && darkLink) {
362
- lightLink.disabled = t === 'dark';
363
- darkLink.disabled = t === 'light';
366
+ lightLink.disabled = t === 'dark';
367
+ darkLink.disabled = t === 'light';
364
368
  }
365
369
  });
366
370
  });
@@ -368,16 +372,29 @@
368
372
  injectCopyButtons();
369
373
  initializeScrollSpy();
370
374
  initializeSPA();
371
-
375
+
372
376
  setTimeout(() => {
373
- const activeNav = document.querySelector('.sidebar-nav a.active');
374
- const sidebarNav = document.querySelector('.sidebar-nav');
375
- if (activeNav && sidebarNav) {
376
- sidebarNav.scrollTo({ top: activeNav.offsetTop - (sidebarNav.clientHeight / 2), behavior: 'instant' });
377
- }
378
- if (window.location.hash) {
379
- 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();
380
396
  }
397
+ }
381
398
  }, 100);
382
399
  });
383
400
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@docmd/ui",
3
- "version": "0.5.0",
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",