@docmd/ui 0.4.10 → 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 {
@@ -151,9 +151,32 @@
151
151
  if (document.body.dataset.spaEnabled !== 'true') return;
152
152
 
153
153
  let currentPath = window.location.pathname;
154
+ const pageCache = new Map();
155
+ let prefetchTimer = null;
156
+
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));
154
178
 
155
179
  document.addEventListener('click', async (e) => {
156
- // Ignore clicks on expand/collapse arrows so they don't trigger navigation
157
180
  if (e.target.closest('.collapse-icon-wrapper')) return;
158
181
 
159
182
  const link = e.target.closest('.sidebar-nav a, .page-navigation a');
@@ -168,7 +191,7 @@
168
191
  });
169
192
 
170
193
  window.addEventListener('popstate', () => {
171
- if (window.location.pathname === currentPath) return; // Ignore hash-only changes
194
+ if (window.location.pathname === currentPath) return;
172
195
  navigateTo(window.location.href, false);
173
196
  });
174
197
 
@@ -176,13 +199,22 @@
176
199
  const layout = document.querySelector('.content-layout');
177
200
 
178
201
  try {
179
- // Lock height to prevent scrollbar jitter/dragging during DOM swap
180
202
  if (layout) layout.style.minHeight = layout.getBoundingClientRect().height + 'px';
181
203
 
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();
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;
217
+
186
218
  const parser = new DOMParser();
187
219
  const doc = parser.parseFromString(html, 'text/html');
188
220
 
@@ -205,24 +237,21 @@
205
237
  }
206
238
  });
207
239
 
208
- // Memorize Sidebar
240
+ // Sync Sidebar State
209
241
  const oldLis = Array.from(document.querySelectorAll('.sidebar-nav li'));
210
242
  const newLis = Array.from(doc.querySelectorAll('.sidebar-nav li'));
211
243
 
212
244
  oldLis.forEach((oldLi, i) => {
213
245
  const newLi = newLis[i];
214
246
  if (newLi) {
215
- // Sync active classes
216
247
  oldLi.classList.toggle('active', newLi.classList.contains('active'));
217
248
  oldLi.classList.toggle('active-parent', newLi.classList.contains('active-parent'));
218
249
 
219
- // Add expanded class if the new page requires it, but NEVER remove it
220
250
  if (newLi.classList.contains('expanded')) {
221
251
  oldLi.classList.add('expanded');
222
252
  oldLi.setAttribute('aria-expanded', 'true');
223
253
  }
224
254
 
225
- // Sync relative hrefs
226
255
  const oldA = oldLi.querySelector('a');
227
256
  const newA = newLi.querySelector('a');
228
257
  if (oldA && newA) {
@@ -232,7 +261,6 @@
232
261
  }
233
262
  });
234
263
 
235
- // 3. Swap Body Components (Removed .sidebar-nav from this list)
236
264
  const selectorsToSwap =[
237
265
  '.content-layout',
238
266
  '.page-header .header-title',
@@ -247,7 +275,6 @@
247
275
  if (oldEl && newEl) oldEl.innerHTML = newEl.innerHTML;
248
276
  });
249
277
 
250
- // Scroll & Init
251
278
  const hash = new URL(finalUrl).hash;
252
279
  if (hash) {
253
280
  document.querySelector(hash)?.scrollIntoView();
@@ -262,7 +289,6 @@
262
289
 
263
290
  document.dispatchEvent(new CustomEvent('docmd:page-mounted', { detail: { url: finalUrl } }));
264
291
 
265
- // Unlock height smoothly
266
292
  setTimeout(() => {
267
293
  const newLayout = document.querySelector('.content-layout');
268
294
  if (newLayout) newLayout.style.minHeight = '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docmd/ui",
3
- "version": "0.4.10",
3
+ "version": "0.4.11",
4
4
  "description": "Base UI templates and assets for docmd",
5
5
  "main": "index.js",
6
6
  "files": [