@docmd/ui 0.4.8 → 0.4.10

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.
@@ -203,9 +203,10 @@ a:any-link {
203
203
  margin: 0
204
204
  }
205
205
 
206
- .sidebar nav li a, .sidebar nav li .nav-label {
206
+ .sidebar nav li a,
207
+ .sidebar nav li .nav-label {
207
208
  display: block;
208
- padding: .5em;
209
+ padding: .4em .5em;
209
210
  margin: .15em 0;
210
211
  text-decoration: none;
211
212
  color: var(--sidebar-text);
@@ -214,6 +215,10 @@ a:any-link {
214
215
  font-size: .9em;
215
216
  }
216
217
 
218
+ .sidebar nav li a {
219
+ cursor: pointer;
220
+ }
221
+
217
222
  .sidebar nav li>a.active {
218
223
  color: var(--link-color);
219
224
  font-weight: 600;
@@ -237,11 +242,18 @@ a:any-link {
237
242
  opacity: 1;
238
243
  }
239
244
 
245
+ .sidebar nav li .nav-label {
246
+ cursor: default;
247
+ padding: 0.25em .5em;
248
+ color: var(--text-color);
249
+ font-weight: 600;
250
+ }
251
+
240
252
  .sidebar nav .submenu {
241
253
  display: none;
242
254
  padding-left: .25em;
243
255
  border-left: 1px solid var(--link-color);
244
- margin-left: .75em;
256
+ margin-left: .85em;
245
257
  }
246
258
 
247
259
  .sidebar nav li[aria-expanded="true"]>.submenu,
@@ -1604,7 +1616,7 @@ hr {
1604
1616
 
1605
1617
  .footer-complete-top {
1606
1618
  display: flex;
1607
- justify-content: space-between;
1619
+ justify-content: center;
1608
1620
  flex-wrap: wrap;
1609
1621
  gap: 2rem;
1610
1622
  max-width: 1200px;
@@ -1625,7 +1637,7 @@ hr {
1625
1637
  width: fit-content;
1626
1638
  }
1627
1639
 
1628
- .footer-brand .logo-link img{
1640
+ .footer-brand .logo-link img {
1629
1641
  max-height: 30px;
1630
1642
  width: auto;
1631
1643
  }
@@ -1725,7 +1737,8 @@ hr {
1725
1737
  color: #fb3a3a
1726
1738
  }
1727
1739
 
1728
- .branding-footer a, .page-footer a {
1740
+ .branding-footer a,
1741
+ .page-footer a {
1729
1742
  color: var(--link-color);
1730
1743
  text-decoration: none
1731
1744
  }
@@ -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() {
@@ -148,7 +153,7 @@
148
153
  let currentPath = window.location.pathname;
149
154
 
150
155
  document.addEventListener('click', async (e) => {
151
-
156
+ // Ignore clicks on expand/collapse arrows so they don't trigger navigation
152
157
  if (e.target.closest('.collapse-icon-wrapper')) return;
153
158
 
154
159
  const link = e.target.closest('.sidebar-nav a, .page-navigation a');
@@ -162,19 +167,17 @@
162
167
  await navigateTo(url.href);
163
168
  });
164
169
 
165
- // Handle Back/Forward browser buttons & TOC Hash clicks
166
170
  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
-
171
+ if (window.location.pathname === currentPath) return; // Ignore hash-only changes
170
172
  navigateTo(window.location.href, false);
171
173
  });
172
174
 
173
175
  async function navigateTo(url, pushHistory = true) {
174
- const mainContentWrapper = document.querySelector('.main-content-wrapper');
176
+ const layout = document.querySelector('.content-layout');
175
177
 
176
178
  try {
177
- if (mainContentWrapper) mainContentWrapper.style.opacity = '0.5';
179
+ // Lock height to prevent scrollbar jitter/dragging during DOM swap
180
+ if (layout) layout.style.minHeight = layout.getBoundingClientRect().height + 'px';
178
181
 
179
182
  const res = await fetch(url);
180
183
  if (!res.ok) throw new Error('Fetch failed');
@@ -183,19 +186,17 @@
183
186
  const parser = new DOMParser();
184
187
  const doc = parser.parseFromString(html, 'text/html');
185
188
 
186
- // 1. UPDATE URL FIRST
187
189
  if (pushHistory) history.pushState({}, '', finalUrl);
188
190
  currentPath = new URL(finalUrl).pathname;
189
191
  document.title = doc.title;
190
192
 
191
- // 2. SAFELY SYNC HEAD ASSETS (Favicon & CSS)
193
+ // Sync Assets
192
194
  const assetSelectors = 'link[rel="stylesheet"], link[rel="icon"], link[rel="shortcut icon"]';
193
195
  const oldAssets = Array.from(document.head.querySelectorAll(assetSelectors));
194
196
  const newAssets = Array.from(doc.head.querySelectorAll(assetSelectors));
195
197
 
196
198
  newAssets.forEach((newAsset, index) => {
197
199
  if (oldAssets[index]) {
198
- // Only update if the relative path actually changed
199
200
  if (oldAssets[index].getAttribute('href') !== newAsset.getAttribute('href')) {
200
201
  oldAssets[index].setAttribute('href', newAsset.getAttribute('href'));
201
202
  }
@@ -204,16 +205,39 @@
204
205
  }
205
206
  });
206
207
 
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
- });
208
+ // Memorize Sidebar
209
+ const oldLis = Array.from(document.querySelectorAll('.sidebar-nav li'));
210
+ const newLis = Array.from(doc.querySelectorAll('.sidebar-nav li'));
212
211
 
213
- // 4. SWAP BODY COMPONENTS
212
+ oldLis.forEach((oldLi, i) => {
213
+ const newLi = newLis[i];
214
+ if (newLi) {
215
+ // Sync active classes
216
+ oldLi.classList.toggle('active', newLi.classList.contains('active'));
217
+ oldLi.classList.toggle('active-parent', newLi.classList.contains('active-parent'));
218
+
219
+ // Add expanded class if the new page requires it, but NEVER remove it
220
+ if (newLi.classList.contains('expanded')) {
221
+ oldLi.classList.add('expanded');
222
+ oldLi.setAttribute('aria-expanded', 'true');
223
+ }
224
+
225
+ // Sync relative hrefs
226
+ const oldA = oldLi.querySelector('a');
227
+ const newA = newLi.querySelector('a');
228
+ if (oldA && newA) {
229
+ oldA.setAttribute('href', newA.getAttribute('href'));
230
+ oldA.classList.toggle('active', newA.classList.contains('active'));
231
+ }
232
+ }
233
+ });
234
+
235
+ // 3. Swap Body Components (Removed .sidebar-nav from this list)
214
236
  const selectorsToSwap =[
215
- '.main-content', '.toc-sidebar', '.sidebar-nav',
216
- '.page-header .header-title', '.page-footer', '.footer-complete',
237
+ '.content-layout',
238
+ '.page-header .header-title',
239
+ '.page-footer',
240
+ '.footer-complete',
217
241
  '.page-footer-actions'
218
242
  ];
219
243
 
@@ -223,33 +247,27 @@
223
247
  if (oldEl && newEl) oldEl.innerHTML = newEl.innerHTML;
224
248
  });
225
249
 
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
250
+ // Scroll & Init
236
251
  const hash = new URL(finalUrl).hash;
237
252
  if (hash) {
238
253
  document.querySelector(hash)?.scrollIntoView();
239
254
  } else {
240
- if (mainContentWrapper) mainContentWrapper.scrollTo(0, 0);
241
255
  window.scrollTo(0, 0);
242
256
  }
243
257
 
244
- if (mainContentWrapper) mainContentWrapper.style.opacity = '1';
245
258
  injectCopyButtons();
246
259
  initializeScrollSpy();
247
-
248
260
  const newMainContent = document.querySelector('.main-content');
249
261
  if (newMainContent) executeScripts(newMainContent);
250
262
 
251
263
  document.dispatchEvent(new CustomEvent('docmd:page-mounted', { detail: { url: finalUrl } }));
252
264
 
265
+ // Unlock height smoothly
266
+ setTimeout(() => {
267
+ const newLayout = document.querySelector('.content-layout');
268
+ if (newLayout) newLayout.style.minHeight = '';
269
+ }, 100);
270
+
253
271
  } catch(e) {
254
272
  window.location.assign(url);
255
273
  }
@@ -271,7 +289,6 @@
271
289
  document.body.setAttribute('data-theme', t);
272
290
  localStorage.setItem('docmd-theme', t);
273
291
 
274
- // Highlight.js CSS swap
275
292
  const lightLink = document.getElementById('hljs-light');
276
293
  const darkLink = document.getElementById('hljs-dark');
277
294
  if (lightLink && darkLink) {
@@ -285,19 +302,14 @@
285
302
  initializeScrollSpy();
286
303
  initializeSPA();
287
304
 
288
- // Auto-scroll sidebar safely
289
305
  setTimeout(() => {
290
306
  const activeNav = document.querySelector('.sidebar-nav a.active');
291
307
  const sidebarNav = document.querySelector('.sidebar-nav');
292
308
  if (activeNav && sidebarNav) {
293
- // Calculate scroll top safely instead of scrollIntoView which causes page jump
294
309
  sidebarNav.scrollTo({ top: activeNav.offsetTop - (sidebarNav.clientHeight / 2), behavior: 'instant' });
295
310
  }
296
-
297
- // Ensure Hash anchors work on direct link visits (New Tab)
298
311
  if (window.location.hash) {
299
- const el = document.querySelector(window.location.hash);
300
- if (el) el.scrollIntoView();
312
+ document.querySelector(window.location.hash)?.scrollIntoView();
301
313
  }
302
314
  }, 100);
303
315
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docmd/ui",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
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) { %>