@docmd/ui 0.4.9 → 0.4.11

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 {
@@ -203,9 +204,10 @@ a:any-link {
203
204
  margin: 0
204
205
  }
205
206
 
206
- .sidebar nav li a, .sidebar nav li .nav-label {
207
+ .sidebar nav li a,
208
+ .sidebar nav li .nav-label {
207
209
  display: block;
208
- padding: .5em;
210
+ padding: .4em .5em;
209
211
  margin: .15em 0;
210
212
  text-decoration: none;
211
213
  color: var(--sidebar-text);
@@ -214,6 +216,10 @@ a:any-link {
214
216
  font-size: .9em;
215
217
  }
216
218
 
219
+ .sidebar nav li a {
220
+ cursor: pointer;
221
+ }
222
+
217
223
  .sidebar nav li>a.active {
218
224
  color: var(--link-color);
219
225
  font-weight: 600;
@@ -237,11 +243,18 @@ a:any-link {
237
243
  opacity: 1;
238
244
  }
239
245
 
246
+ .sidebar nav li .nav-label {
247
+ cursor: default;
248
+ padding: 0.25em .5em;
249
+ color: var(--text-color);
250
+ font-weight: 600;
251
+ }
252
+
240
253
  .sidebar nav .submenu {
241
254
  display: none;
242
255
  padding-left: .25em;
243
256
  border-left: 1px solid var(--link-color);
244
- margin-left: .75em;
257
+ margin-left: .85em;
245
258
  }
246
259
 
247
260
  .sidebar nav li[aria-expanded="true"]>.submenu,
@@ -1604,7 +1617,7 @@ hr {
1604
1617
 
1605
1618
  .footer-complete-top {
1606
1619
  display: flex;
1607
- justify-content: space-between;
1620
+ justify-content: center;
1608
1621
  flex-wrap: wrap;
1609
1622
  gap: 2rem;
1610
1623
  max-width: 1200px;
@@ -1625,7 +1638,7 @@ hr {
1625
1638
  width: fit-content;
1626
1639
  }
1627
1640
 
1628
- .footer-brand .logo-link img{
1641
+ .footer-brand .logo-link img {
1629
1642
  max-height: 30px;
1630
1643
  width: auto;
1631
1644
  }
@@ -1725,7 +1738,8 @@ hr {
1725
1738
  color: #fb3a3a
1726
1739
  }
1727
1740
 
1728
- .branding-footer a, .page-footer a {
1741
+ .branding-footer a,
1742
+ .page-footer a {
1729
1743
  color: var(--link-color);
1730
1744
  text-decoration: none
1731
1745
  }
@@ -11,6 +11,11 @@
11
11
  * [docmd-source] - Please do not remove this header.
12
12
  * --------------------------------------------------------------------
13
13
  */
14
+ /**
15
+ * --------------------------------------------------------------------
16
+ * docmd : Client-Side Application Logic (SPA Router & UI)
17
+ * --------------------------------------------------------------------
18
+ */
14
19
 
15
20
  (function() {
16
21
  // =========================================================================
@@ -138,7 +143,7 @@
138
143
  });
139
144
  }
140
145
 
141
- // =========================================================================
146
+ // =========================================================================
142
147
  // 3. TARGETED SPA ROUTER
143
148
  // =========================================================================
144
149
  function initializeSPA() {
@@ -146,9 +151,32 @@
146
151
  if (document.body.dataset.spaEnabled !== 'true') return;
147
152
 
148
153
  let currentPath = window.location.pathname;
154
+ const pageCache = new Map();
155
+ let prefetchTimer = null;
149
156
 
150
- document.addEventListener('click', async (e) => {
157
+ // Intent-based Hover Prefetching
158
+ document.addEventListener('mouseover', (e) => {
159
+ const link = e.target.closest('.sidebar-nav a, .page-navigation a');
160
+ if (!link || link.target === '_blank' || link.hasAttribute('download')) return;
161
+
162
+ const url = new URL(link.href).href;
163
+ if (new URL(url).origin !== location.origin) return;
164
+ if (pageCache.has(url)) return;
165
+
166
+ // Wait 65ms to ensure the user actually intends to click
167
+ clearTimeout(prefetchTimer);
168
+ 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)));
173
+ }, 65);
174
+ });
175
+
176
+ // Cancel prefetch if the mouse leaves before the 65ms "intent" delay
177
+ document.addEventListener('mouseout', () => clearTimeout(prefetchTimer));
151
178
 
179
+ document.addEventListener('click', async (e) => {
152
180
  if (e.target.closest('.collapse-icon-wrapper')) return;
153
181
 
154
182
  const link = e.target.closest('.sidebar-nav a, .page-navigation a');
@@ -162,40 +190,45 @@
162
190
  await navigateTo(url.href);
163
191
  });
164
192
 
165
- // Handle Back/Forward browser buttons & TOC Hash clicks
166
193
  window.addEventListener('popstate', () => {
167
- // If the path is identical, it means ONLY the #hash changed. Do not reload!
168
- if (window.location.pathname === currentPath) return;
169
-
194
+ if (window.location.pathname === currentPath) return;
170
195
  navigateTo(window.location.href, false);
171
196
  });
172
197
 
173
198
  async function navigateTo(url, pushHistory = true) {
174
- const mainContentWrapper = document.querySelector('.main-content-wrapper');
199
+ const layout = document.querySelector('.content-layout');
175
200
 
176
201
  try {
177
- if (mainContentWrapper) mainContentWrapper.style.opacity = '0.5';
202
+ if (layout) layout.style.minHeight = layout.getBoundingClientRect().height + 'px';
203
+
204
+ let data;
205
+ if (pageCache.has(url)) {
206
+ data = await pageCache.get(url);
207
+ data.html = await data.html;
208
+ } 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));
213
+ }
214
+
215
+ const finalUrl = data.finalUrl;
216
+ const html = data.html;
178
217
 
179
- const res = await fetch(url);
180
- if (!res.ok) throw new Error('Fetch failed');
181
- const finalUrl = res.url;
182
- const html = await res.text();
183
218
  const parser = new DOMParser();
184
219
  const doc = parser.parseFromString(html, 'text/html');
185
220
 
186
- // 1. UPDATE URL FIRST
187
221
  if (pushHistory) history.pushState({}, '', finalUrl);
188
222
  currentPath = new URL(finalUrl).pathname;
189
223
  document.title = doc.title;
190
224
 
191
- // 2. SAFELY SYNC HEAD ASSETS (Favicon & CSS)
225
+ // Sync Assets
192
226
  const assetSelectors = 'link[rel="stylesheet"], link[rel="icon"], link[rel="shortcut icon"]';
193
227
  const oldAssets = Array.from(document.head.querySelectorAll(assetSelectors));
194
228
  const newAssets = Array.from(doc.head.querySelectorAll(assetSelectors));
195
229
 
196
230
  newAssets.forEach((newAsset, index) => {
197
231
  if (oldAssets[index]) {
198
- // Only update if the relative path actually changed
199
232
  if (oldAssets[index].getAttribute('href') !== newAsset.getAttribute('href')) {
200
233
  oldAssets[index].setAttribute('href', newAsset.getAttribute('href'));
201
234
  }
@@ -204,16 +237,35 @@
204
237
  }
205
238
  });
206
239
 
207
- // 3. MEMORIZE SIDEBAR STATE
208
- const openMenus = new Set();
209
- document.querySelectorAll('.sidebar-nav li.collapsible.expanded > .nav-label .nav-item-title, .sidebar-nav li.collapsible.expanded > a .nav-item-title').forEach(el => {
210
- openMenus.add(el.textContent.trim());
211
- });
240
+ // Sync Sidebar State
241
+ const oldLis = Array.from(document.querySelectorAll('.sidebar-nav li'));
242
+ const newLis = Array.from(doc.querySelectorAll('.sidebar-nav li'));
212
243
 
213
- // 4. SWAP BODY COMPONENTS
244
+ 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
+ }
261
+ }
262
+ });
263
+
214
264
  const selectorsToSwap =[
215
- '.main-content', '.toc-sidebar', '.sidebar-nav',
216
- '.page-header .header-title', '.page-footer', '.footer-complete',
265
+ '.content-layout',
266
+ '.page-header .header-title',
267
+ '.page-footer',
268
+ '.footer-complete',
217
269
  '.page-footer-actions'
218
270
  ];
219
271
 
@@ -223,33 +275,25 @@
223
275
  if (oldEl && newEl) oldEl.innerHTML = newEl.innerHTML;
224
276
  });
225
277
 
226
- // 5. RESTORE SIDEBAR STATE
227
- document.querySelectorAll('.sidebar-nav li.collapsible').forEach(li => {
228
- const title = li.querySelector('.nav-item-title')?.textContent.trim();
229
- if (openMenus.has(title)) {
230
- li.classList.add('expanded');
231
- li.setAttribute('aria-expanded', 'true');
232
- }
233
- });
234
-
235
- // 6. SCROLL & RE-INIT
236
278
  const hash = new URL(finalUrl).hash;
237
279
  if (hash) {
238
280
  document.querySelector(hash)?.scrollIntoView();
239
281
  } else {
240
- if (mainContentWrapper) mainContentWrapper.scrollTo(0, 0);
241
282
  window.scrollTo(0, 0);
242
283
  }
243
284
 
244
- if (mainContentWrapper) mainContentWrapper.style.opacity = '1';
245
285
  injectCopyButtons();
246
286
  initializeScrollSpy();
247
-
248
287
  const newMainContent = document.querySelector('.main-content');
249
288
  if (newMainContent) executeScripts(newMainContent);
250
289
 
251
290
  document.dispatchEvent(new CustomEvent('docmd:page-mounted', { detail: { url: finalUrl } }));
252
291
 
292
+ setTimeout(() => {
293
+ const newLayout = document.querySelector('.content-layout');
294
+ if (newLayout) newLayout.style.minHeight = '';
295
+ }, 100);
296
+
253
297
  } catch(e) {
254
298
  window.location.assign(url);
255
299
  }
@@ -271,7 +315,6 @@
271
315
  document.body.setAttribute('data-theme', t);
272
316
  localStorage.setItem('docmd-theme', t);
273
317
 
274
- // Highlight.js CSS swap
275
318
  const lightLink = document.getElementById('hljs-light');
276
319
  const darkLink = document.getElementById('hljs-dark');
277
320
  if (lightLink && darkLink) {
@@ -285,19 +328,14 @@
285
328
  initializeScrollSpy();
286
329
  initializeSPA();
287
330
 
288
- // Auto-scroll sidebar safely
289
331
  setTimeout(() => {
290
332
  const activeNav = document.querySelector('.sidebar-nav a.active');
291
333
  const sidebarNav = document.querySelector('.sidebar-nav');
292
334
  if (activeNav && sidebarNav) {
293
- // Calculate scroll top safely instead of scrollIntoView which causes page jump
294
335
  sidebarNav.scrollTo({ top: activeNav.offsetTop - (sidebarNav.clientHeight / 2), behavior: 'instant' });
295
336
  }
296
-
297
- // Ensure Hash anchors work on direct link visits (New Tab)
298
337
  if (window.location.hash) {
299
- const el = document.querySelector(window.location.hash);
300
- if (el) el.scrollIntoView();
338
+ document.querySelector(window.location.hash)?.scrollIntoView();
301
339
  }
302
340
  }, 100);
303
341
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docmd/ui",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
4
4
  "description": "Base UI templates and assets for docmd",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -84,7 +84,7 @@
84
84
  %>
85
85
  <li class="<%= liClasses.join(' ') %>" <% if(isInteractive) { %> aria-expanded="<%= isOpen %>" <% } %>>
86
86
  <% if (isDummyLink) { %>
87
- <span class="nav-label" style="cursor: <%= isInteractive ? 'pointer' : 'default' %>;">
87
+ <span class="nav-label">
88
88
  <% if (item.icon) { %> <%- renderIcon(item.icon) %> <% } %>
89
89
  <span class="nav-item-title"><%= item.title %></span>
90
90
  <% if (isInteractive) { %>