@@ -42,3 +42,3 @@ /** | ||
| /* Code Colors */ | ||
| --code-bg: #eaeaea1f; | ||
| --code-bg: #9392921f; | ||
| --code-text: #27272a; | ||
@@ -529,11 +529,2 @@ | ||
| .content-area { | ||
| padding: var(--space-8) 5%; | ||
| max-width: 1216px; | ||
| /* 1200 + 16 for some wiggle room */ | ||
| margin: 0 auto; | ||
| width: 100%; | ||
| box-sizing: border-box; | ||
| } | ||
| @media (max-width: 768px) { | ||
@@ -1271,3 +1262,3 @@ .content-area { | ||
| .content-area { | ||
| padding: 1rem 5%; | ||
| padding: 1rem 2.5rem; | ||
| max-width: 1200px; | ||
@@ -1763,6 +1754,6 @@ margin: 0 auto; | ||
| border: 1px solid var(--border-color); | ||
| padding: 4px 12px; | ||
| padding: 3px 8px; | ||
| border-radius: 20px; | ||
| font-size: .85rem; | ||
| font-weight: 600; | ||
| font-size: .75rem; | ||
| font-weight: 500; | ||
| font-family: var(--font-family-mono) | ||
@@ -1772,5 +1763,5 @@ } | ||
| .changelog-body { | ||
| border-left: 2px solid var(--border-color); | ||
| padding-left: 2rem; | ||
| padding-bottom: 1rem | ||
| border-left: 1px solid var(--border-color); | ||
| padding-left: 2.5rem; | ||
| padding-bottom: .5rem; | ||
| } | ||
@@ -2246,2 +2237,3 @@ | ||
| display: inline-block; | ||
| margin-right: 5px; | ||
| } | ||
@@ -2248,0 +2240,0 @@ |
+54
-19
@@ -21,2 +21,4 @@ /** | ||
| /* global requestAnimationFrame, cancelAnimationFrame */ | ||
| (function () { | ||
@@ -98,3 +100,4 @@ | ||
| const normTargetRoot = targetRoot.endsWith('/') ? targetRoot : targetRoot + '/'; | ||
| const targetHref = normTargetRoot + suffix + window.location.hash; | ||
| let targetHref = normTargetRoot + suffix + window.location.hash; | ||
| targetHref = targetHref.replace(/([^:])\/{2,}/g, '$1/'); // Prevent double slashes | ||
@@ -363,6 +366,10 @@ // Smart Switcher: Check if the exact page exists in the target version | ||
| prefetchTimer = setTimeout(() => { | ||
| pageCache.set(url, fetch(url).then(res => { | ||
| // Prefetch using hash-stripped URL — the fragment is never sent to the server | ||
| const prefetchUrl = new URL(link.href); | ||
| const prefetchFetchUrl = prefetchUrl.origin + prefetchUrl.pathname + prefetchUrl.search; | ||
| if (pageCache.has(prefetchFetchUrl)) return; | ||
| pageCache.set(prefetchFetchUrl, fetch(prefetchFetchUrl).then(res => { | ||
| if (!res.ok) throw new Error('Prefetch failed'); | ||
| return { html: res.text(), finalUrl: res.url }; | ||
| }).catch(() => pageCache.delete(url))); | ||
| }).catch(() => pageCache.delete(prefetchFetchUrl))); | ||
| }, 65); | ||
@@ -384,4 +391,14 @@ }); | ||
| if (url.origin !== location.origin) return; | ||
| if (url.pathname === window.location.pathname && url.hash) return; | ||
| // Same-page hash navigation: scroll to the target element smoothly | ||
| // without a full SPA fetch. The browser's native behaviour doesn't | ||
| // fire reliably after a previous pushState, so we handle it manually. | ||
| if (url.pathname === window.location.pathname && url.hash) { | ||
| e.preventDefault(); | ||
| history.pushState({}, '', url.href); | ||
| const target = document.querySelector(url.hash) || document.getElementById(url.hash.substring(1)); | ||
| if (target) target.scrollIntoView({ behavior: 'smooth' }); | ||
| return; | ||
| } | ||
| e.preventDefault(); | ||
@@ -392,3 +409,11 @@ await navigateTo(url.href); | ||
| window.addEventListener('popstate', () => { | ||
| if (window.location.pathname === currentPath) return; | ||
| // Same-page hash change via browser back/forward | ||
| if (window.location.pathname === currentPath) { | ||
| const hash = window.location.hash; | ||
| if (hash) { | ||
| const target = document.querySelector(hash) || document.getElementById(hash.substring(1)); | ||
| if (target) target.scrollIntoView({ behavior: 'smooth' }); | ||
| } | ||
| return; | ||
| } | ||
| navigateTo(window.location.href, false); | ||
@@ -403,14 +428,21 @@ }); | ||
| // Separate hash from the fetch URL — servers don't receive fragments, | ||
| // and res.url (finalUrl) will never contain the hash. We preserve it | ||
| // manually so pushState and the scroll both use the correct value. | ||
| const parsedUrl = new URL(url); | ||
| const requestedHash = parsedUrl.hash; // e.g. "#configuration" | ||
| const fetchUrl = parsedUrl.origin + parsedUrl.pathname + parsedUrl.search; | ||
| let data; | ||
| if (pageCache.has(url)) { | ||
| data = await pageCache.get(url); | ||
| if (pageCache.has(fetchUrl)) { | ||
| data = await pageCache.get(fetchUrl); | ||
| data.html = await data.html; | ||
| } else { | ||
| const res = await fetch(url); | ||
| const res = await fetch(fetchUrl); | ||
| if (!res.ok) throw new Error('Fetch failed'); | ||
| data = { html: await res.text(), finalUrl: res.url }; | ||
| pageCache.set(url, Promise.resolve(data)); | ||
| pageCache.set(fetchUrl, Promise.resolve(data)); | ||
| } | ||
| const finalUrl = data.finalUrl; | ||
| const finalUrl = data.finalUrl + requestedHash; | ||
| const html = data.html; | ||
@@ -496,12 +528,15 @@ | ||
| const hash = new URL(finalUrl).hash; | ||
| if (hash) { | ||
| try { | ||
| document.querySelector(hash)?.scrollIntoView(); | ||
| } catch { | ||
| document.getElementById(hash.substring(1))?.scrollIntoView(); | ||
| // Scroll after the browser has painted the new content. | ||
| // requestAnimationFrame ensures the swapped innerHTML is in the layout. | ||
| requestAnimationFrame(() => { | ||
| if (requestedHash) { | ||
| try { | ||
| const target = document.querySelector(requestedHash) | ||
| || document.getElementById(requestedHash.substring(1)); | ||
| if (target) target.scrollIntoView({ behavior: 'smooth' }); | ||
| } catch { /* invalid selector — ignore */ } | ||
| } else { | ||
| window.scrollTo(0, 0); | ||
| } | ||
| } else { | ||
| window.scrollTo(0, 0); | ||
| } | ||
| }); | ||
@@ -508,0 +543,0 @@ injectCopyButtons(); |
+1
-1
| { | ||
| "name": "@docmd/ui", | ||
| "version": "0.7.5", | ||
| "version": "0.7.6", | ||
| "description": "Base UI templates and assets for docmd.", | ||
@@ -5,0 +5,0 @@ "type": "module", |
@@ -90,3 +90,3 @@ <!-- | ||
| <% } else { %> | ||
| <h1><a href="<%= relativePathToRoot %>index.html"> | ||
| <h1><a href="<%= relativePathToRoot %>"> | ||
| <%= siteTitle %> | ||
@@ -93,0 +93,0 @@ </a></h1> |
+16
-15
@@ -10,5 +10,15 @@ <%# --------------------------------------------------------------- | ||
| <ul> | ||
| <% function normalizePath(p) { if (!p || p==='#' ) return '#' ; if (p.startsWith('http')) return p; let | ||
| path=p.replace(/^(\.\/|\/)+/, '' ); path=path.replace(/(\/index\.html|index\.html|\.html|\.md)$/, '' ); | ||
| path=path.replace(/\/$/, '' ); return '/' + path; } function isChildActive(item, currentPath) { if | ||
| <% | ||
| // normalizePath: used ONLY for active-state comparison (not for href generation). | ||
| // All href generation goes through buildRelativeUrl() which delegates to the | ||
| // centralized URL engine in @docmd/parser. | ||
| function normalizePath(p) { | ||
| if (!p || p === '#') return '#'; | ||
| if (p.startsWith('http')) return p; | ||
| let path = p.replace(/^(\.\/|\/)+/, ''); | ||
| path = path.replace(/(\/index\.html|index\.html|\.html|\.md)$/, ''); | ||
| path = path.replace(/\/$/, ''); | ||
| return '/' + path; | ||
| } | ||
| function isChildActive(item, currentPath) { if | ||
| (!item.children) return false; const currentNorm=normalizePath(currentPath); return item.children.some(child=> { | ||
@@ -48,14 +58,5 @@ const childNorm = normalizePath(child.path); | ||
| let href = item.path || '#'; | ||
| if (!isExternal && !isDummyLink && !href.startsWith('http')) { | ||
| let cleanPath = href.replace(/^(\.\/|\/)+/, ''); | ||
| // Apply locale + version namespace prefix safely | ||
| const prefix = (typeof outputPrefix !== 'undefined') ? outputPrefix : ''; | ||
| href = relativePathToRoot + prefix + cleanPath; | ||
| if (isOfflineMode) { | ||
| if (href.endsWith('/')) href += 'index.html'; | ||
| else if (!href.endsWith('.html')) href += '/index.html'; | ||
| } else { | ||
| if (href.endsWith('/index.html')) href = href.slice(0, -10); | ||
| } | ||
| } | ||
| if (!isExternal && !isDummyLink) { | ||
| href = buildRelativeUrl(href); | ||
| } | ||
| %> | ||
@@ -62,0 +63,0 @@ <li class="<%= liClasses.join(' ') %>" <% if(isInteractive) { %> aria-expanded="<%= isOpen %>" <% } %>> |
@@ -162,3 +162,3 @@ <!-- | ||
| <div class="sidebar-header"> | ||
| <a href="<%= logo.href || (relativePathToRoot + 'index.html') %>" class="logo-link"> | ||
| <a href="<%= logo.href || relativePathToRoot %>" class="logo-link"> | ||
| <img src="<%= relativePathToRoot %><%- logo.light.startsWith('/') ? logo.light.substring(1) : logo.light %>" | ||
@@ -165,0 +165,0 @@ alt="<%= logo.alt || siteTitle %>" class="logo-light" <% if (logo.height) { %>style="height: <%= logo.height %>;"<% } %>> |
@@ -31,3 +31,3 @@ <%# --------------------------------------------------------------- | ||
| <% col.links.forEach(link => { %> | ||
| <li><a href="<%= link.url %>" <%= link.external ? 'target="_blank" rel="noopener"' : '' %>><%= link.text %></a></li> | ||
| <li><a href="<%= link.external ? link.url : buildRelativeUrl(link.url) %>" <%= link.external ? 'target="_blank" rel="noopener"' : '' %>><%= link.text %></a></li> | ||
| <% }) %> | ||
@@ -34,0 +34,0 @@ </ul> |
@@ -55,8 +55,10 @@ <%# --------------------------------------------------------------- | ||
| const base = config.base || '/'; | ||
| const normalizedBase = base.endsWith('/') ? base : base + '/'; | ||
| // Build locale-aware URL preserving version and page path | ||
| // Use centralised URL builder — single source of truth | ||
| const locPrefix = isLocDefault ? '' : loc.id + '/'; | ||
| const absoluteHref = isDisabled ? '#' : (normalizedBase + locPrefix + versionPrefix + pagePath); | ||
| const absoluteHref = isDisabled | ||
| ? '#' | ||
| : ((typeof buildAbsoluteUrl === 'function') | ||
| ? buildAbsoluteUrl(config.base || '/', locPrefix, versionPrefix, pagePath) | ||
| : ((config.base || '/').replace(/\/?$/, '/') + locPrefix + versionPrefix + pagePath)); | ||
| %> | ||
@@ -63,0 +65,0 @@ <li> |
@@ -9,12 +9,9 @@ <%# --------------------------------------------------------------- | ||
| <% | ||
| const menubarBase = (config.base || '/').replace(/\/$/, '') + '/'; | ||
| const menuLocPrefix = (typeof localePrefix !== 'undefined' && localePrefix) ? localePrefix : ''; | ||
| function menubarHref(item) { | ||
| if (!item.url) return '#' ; | ||
| if (item.external || item.url.startsWith('http')) return item.url; | ||
| let clean=item.url.replace(/^\//, '' ); | ||
| return menubarBase + menuLocPrefix + clean; | ||
| return item.external ? item.url : buildRelativeUrl(item.url); | ||
| } | ||
| function menubarTarget(item) { | ||
| return (item.external || (item.url && item.url.startsWith('http'))) ? ' target="_blank" rel="noopener noreferrer"' : '' ; | ||
| // Only open in new tab if user explicitly adds external: prefix | ||
| // Users who want their own docs site to open in new tab should add external: prefix | ||
| return item.external ? ' target="_blank" rel="noopener noreferrer"' : '' ; | ||
| } | ||
@@ -59,3 +56,3 @@ %> | ||
| <%= sub.text %> | ||
| <% if (sub.external || (sub.url && sub.url.startsWith('http'))) { %> | ||
| <% if (sub.external) { %> | ||
| <%- renderIcon('external-link', { class: 'menubar-ext-icon' }) %> | ||
@@ -77,3 +74,3 @@ <% } %> | ||
| </span> | ||
| <% if (item.external || (item.url && item.url.startsWith('http'))) { %> | ||
| <% if (item.external) { %> | ||
| <%- renderIcon('external-link', { class: 'menubar-ext-icon' }) %> | ||
@@ -109,3 +106,3 @@ <% } %> | ||
| <%= sub.text %> | ||
| <% if (sub.external || (sub.url && sub.url.startsWith('http'))) { %> | ||
| <% if (sub.external) { %> | ||
| <%- renderIcon('external-link', { class: 'menubar-ext-icon' }) %> | ||
@@ -127,3 +124,3 @@ <% } %> | ||
| </span> | ||
| <% if (item.external || (item.url && item.url.startsWith('http'))) { %> | ||
| <% if (item.external) { %> | ||
| <%- renderIcon('external-link', { class: 'menubar-ext-icon' }) %> | ||
@@ -130,0 +127,0 @@ <% } %> |
@@ -30,9 +30,3 @@ <%# --------------------------------------------------------------- | ||
| // We use the 'config' object which is available in all templates | ||
| const base = config.base || '/'; | ||
| const normalizedBase = base.endsWith('/') ? base : base + '/'; | ||
| // Include locale prefix for non-default locales | ||
| // Include locale prefix for non-default locales | ||
| const locPrefix = (typeof localePrefix !== 'undefined' && localePrefix) ? localePrefix : ''; | ||
@@ -45,4 +39,8 @@ | ||
| const targetSuffix = isTargetRootVersion ? '' : v.id + '/'; | ||
| const absoluteHref = normalizedBase + effectiveLocPrefix + targetSuffix; | ||
| // Use centralised URL builder — single source of truth | ||
| const absoluteHref = (typeof buildAbsoluteUrl === 'function') | ||
| ? buildAbsoluteUrl(config.base || '/', effectiveLocPrefix, targetSuffix, '') | ||
| : ((config.base || '/').replace(/\/?$/, '/') + effectiveLocPrefix + targetSuffix); | ||
| // Data Root for JS Sticky Logic | ||
@@ -49,0 +47,0 @@ const dataRoot = absoluteHref; |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
187742
0.77%3921
0.64%7
40%