also looking at this
fix(spa): repair client-side router and polish default theme
#71
9 files
+1037
-170
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -39,12 +39,10 @@ async function generateSidebar(config: BunPressConfig, currentPath: string): Pro | ||
| 39 | 39 | const sectionsHtml = await Promise.all(sidebarSections.map(async (section) => { |
| 40 | 40 | const itemsHtml = section.items |
| 41 | 41 | ? section.items.map((item: SidebarItem) => { |
| 42 | // Use the link as-is (no /docs/ prefix needed) | |
| 43 | 42 | const link = item.link || '/' |
| 44 | ||
| 45 | 43 | const isActive = link === currentPath || item.link === currentPath |
| 46 | const activeStyle = isActive ? 'color: var(--bp-c-brand-1); font-weight: 500;' : '' | |
| 47 | return `<li><a href="${link}" style="display: block; padding: 6px 24px; color: var(--bp-c-text-2); text-decoration: none; font-size: 14px; transition: color 0.25s; ${activeStyle}" onmouseover="this.style.color='var(--bp-c-brand-1)'" onmouseout="this.style.color='${isActive ? 'var(--bp-c-brand-1)' : 'var(--bp-c-text-2)'}'">${item.text}</a></li>` | |
| 44 | const cls = isActive ? 'VPSidebarItem-link is-active' : 'VPSidebarItem-link' | |
| 45 | return `<li><a class="${cls}" href="${link}">${item.text}</a></li>` | |
| 48 | 46 | }).join('') |
| 49 | 47 | : '' |
| 50 | 48 | |
| @@ -149,132 +147,300 @@ function injectSPARouter(html: string): string { | ||
| 149 | 147 | * browser back/forward. No framework dependencies. |
| 150 | 148 | */ |
| 151 | 149 | function generateSPARouterScript(): string { |
| 152 | return `<script> | |
| 150 | return `<script data-bp-router="1"> | |
| 153 | 151 | (function(){ |
| 154 | var cache = {}; | |
| 152 | if (history.scrollRestoration) history.scrollRestoration = 'manual'; | |
| 153 | ||
| 154 | var cache = Object.create(null); | |
| 155 | 155 | var parser = new DOMParser(); |
| 156 | var scrollPositions = Object.create(null); | |
| 157 | var historyIndex = (history.state && typeof history.state.idx === 'number') ? history.state.idx : 0; | |
| 158 | var lastPathname = location.pathname; | |
| 159 | var lastSearch = location.search; | |
| 160 | ||
| 161 | // Wrap pushState/replaceState so any caller (incl. inline scripts) gets a tracked idx | |
| 162 | var origPush = history.pushState.bind(history); | |
| 163 | var origReplace = history.replaceState.bind(history); | |
| 164 | history.pushState = function(state, title, url) { | |
| 165 | state = state || {}; | |
| 166 | if (typeof state.idx !== 'number') { | |
| 167 | historyIndex++; | |
| 168 | state.idx = historyIndex; | |
| 169 | } else { | |
| 170 | historyIndex = state.idx; | |
| 171 | } | |
| 172 | return origPush(state, title, url); | |
| 173 | }; | |
| 174 | history.replaceState = function(state, title, url) { | |
| 175 | state = state || {}; | |
| 176 | if (typeof state.idx !== 'number') state.idx = historyIndex; | |
| 177 | else historyIndex = state.idx; | |
| 178 | return origReplace(state, title, url); | |
| 179 | }; | |
| 180 | if (!history.state || typeof history.state.idx !== 'number') { | |
| 181 | origReplace({ idx: historyIndex }, '', location.href); | |
| 182 | } | |
| 156 | 183 | |
| 157 | function isInternal(url) { | |
| 158 | try { | |
| 159 | var u = new URL(url, location.origin); | |
| 160 | return u.origin === location.origin && !u.pathname.match(/\\.[a-z]+$/i); | |
| 161 | } catch(e) { return false; } | |
| 184 | function getScroller() { | |
| 185 | var el = document.querySelector('.VPContent'); | |
| 186 | if (el) { | |
| 187 | var cs = getComputedStyle(el); | |
| 188 | if (cs.position === 'fixed' && (cs.overflowY === 'auto' || cs.overflowY === 'scroll')) { | |
| 189 | return el; | |
| 190 | } | |
| 191 | } | |
| 192 | return window; | |
| 193 | } | |
| 194 | ||
| 195 | function readScroll() { | |
| 196 | var s = getScroller(); | |
| 197 | return s === window ? window.scrollY : s.scrollTop; | |
| 198 | } | |
| 199 | ||
| 200 | function applyScroll(y) { | |
| 201 | var s = getScroller(); | |
| 202 | if (s === window) window.scrollTo(0, y); | |
| 203 | else s.scrollTop = y; | |
| 204 | } | |
| 205 | ||
| 206 | function saveScroll() { | |
| 207 | scrollPositions[historyIndex] = readScroll(); | |
| 208 | } | |
| 209 | ||
| 210 | function isInternalNavigable(u) { | |
| 211 | return u.origin === location.origin && !u.pathname.match(/\\.[a-z]+$/i); | |
| 162 | 212 | } |
| 163 | 213 | |
| 164 | 214 | function updateActiveLinks() { |
| 165 | 215 | var path = location.pathname; |
| 216 | document.querySelectorAll('a.is-active').forEach(function(a) { | |
| 217 | a.classList.remove('is-active'); | |
| 218 | }); | |
| 166 | 219 | document.querySelectorAll('a[href]').forEach(function(a) { |
| 167 | 220 | var href = a.getAttribute('href'); |
| 168 | if (href === path || (href !== '/' && path.startsWith(href))) { | |
| 169 | a.style.color = 'var(--bp-c-brand-1)'; | |
| 170 | a.style.fontWeight = '500'; | |
| 221 | if (!href || href.startsWith('#')) return; | |
| 222 | var u; | |
| 223 | try { u = new URL(href, location.origin); } catch(_) { return; } | |
| 224 | if (u.origin !== location.origin) return; | |
| 225 | var p = u.pathname; | |
| 226 | // eslint-disable-next-line general/prefer-template -- inner JS string; template literals would clash with the outer TS template | |
| 227 | if (p === path || (p !== '/' && (path === p || path.startsWith(p + '/')))) { | |
| 228 | a.classList.add('is-active'); | |
| 171 | 229 | } |
| 172 | 230 | }); |
| 173 | 231 | } |
| 174 | 232 | |
| 175 | async function navigate(url, push) { | |
| 176 | if (push !== false) history.pushState(null, '', url); | |
| 233 | function executeScripts(root) { | |
| 234 | if (!root) return; | |
| 235 | root.querySelectorAll('script').forEach(function(s) { | |
| 236 | if (s.dataset && s.dataset.bpRouter) return; | |
| 237 | var ns = document.createElement('script'); | |
| 238 | for (var i = 0; i < s.attributes.length; i++) { | |
| 239 | var attr = s.attributes[i]; | |
| 240 | ns.setAttribute(attr.name, attr.value); | |
| 241 | } | |
| 242 | if (!s.src) ns.textContent = s.textContent; | |
| 243 | s.parentNode.replaceChild(ns, s); | |
| 244 | }); | |
| 245 | } | |
| 246 | ||
| 247 | function scrollToHash(hash) { | |
| 248 | if (!hash) return false; | |
| 249 | var id; | |
| 250 | try { id = decodeURIComponent(hash.slice(1)); } catch(_) { id = hash.slice(1); } | |
| 251 | if (!id) return false; | |
| 252 | var el = document.getElementById(id); | |
| 253 | if (!el) { | |
| 254 | try { el = document.querySelector('[name="' + (window.CSS && CSS.escape ? CSS.escape(id) : id) + '"]'); } catch(_) {} | |
| 255 | } | |
| 256 | if (el) { | |
| 257 | el.scrollIntoView({ behavior: 'auto', block: 'start' }); | |
| 258 | return true; | |
| 259 | } | |
| 260 | return false; | |
| 261 | } | |
| 262 | ||
| 263 | function detectLayout(root) { | |
| 264 | if (root.querySelector('.VPHome')) return 'home'; | |
| 265 | if (root.querySelector('.VPContent--doc') || root.querySelector('.VPSidebar')) return 'doc'; | |
| 266 | if (root.querySelector('.VPContent--page') || root.querySelector('.VPPage')) return 'page'; | |
| 267 | return 'page'; | |
| 268 | } | |
| 269 | ||
| 270 | async function navigate(href, opts) { | |
| 271 | opts = opts || {}; | |
| 272 | var target = new URL(href, location.origin); | |
| 273 | var key = target.pathname + target.search; | |
| 274 | ||
| 275 | if (opts.push) { | |
| 276 | saveScroll(); | |
| 277 | history.pushState({}, '', target.pathname + target.search + target.hash); | |
| 278 | } | |
| 177 | 279 | |
| 178 | var html = cache[url]; | |
| 280 | var html = cache[key]; | |
| 179 | 281 | if (!html) { |
| 180 | 282 | try { |
| 181 | var res = await fetch(url); | |
| 283 | var res = await fetch(key, { headers: { 'X-BP-SPA': '1' } }); | |
| 284 | if (!res.ok) { | |
| 285 | // 404 / 5xx — fall through to a real navigation so the browser shows the actual error page | |
| 286 | location.href = target.href; | |
| 287 | return; | |
| 288 | } | |
| 182 | 289 | html = await res.text(); |
| 183 | cache[url] = html; | |
| 184 | } catch(e) { | |
| 185 | location.href = url; | |
| 290 | cache[key] = html; | |
| 291 | } catch(_) { | |
| 292 | location.href = target.href; | |
| 186 | 293 | return; |
| 187 | 294 | } |
| 188 | 295 | } |
| 189 | 296 | |
| 190 | 297 | var doc = parser.parseFromString(html, 'text/html'); |
| 191 | 298 | |
| 192 | // Update title | |
| 193 | 299 | var newTitle = doc.querySelector('title'); |
| 194 | 300 | if (newTitle) document.title = newTitle.textContent; |
| 195 | 301 | |
| 196 | // Update meta description | |
| 197 | var newDesc = doc.querySelector('meta[name="description"]'); | |
| 198 | var curDesc = document.querySelector('meta[name="description"]'); | |
| 199 | if (newDesc && curDesc) curDesc.setAttribute('content', newDesc.getAttribute('content')); | |
| 302 | // Sync description, og:*, twitter:* meta tags | |
| 303 | doc.querySelectorAll('meta[name="description"], meta[property^="og:"], meta[name^="twitter:"]').forEach(function(nm) { | |
| 304 | var sel; | |
| 305 | if (nm.getAttribute('property')) sel = 'meta[property="' + nm.getAttribute('property') + '"]'; | |
| 306 | else sel = 'meta[name="' + nm.getAttribute('name') + '"]'; | |
| 307 | var existing = document.querySelector(sel); | |
| 308 | if (existing) existing.setAttribute('content', nm.getAttribute('content') || ''); | |
| 309 | else document.head.appendChild(nm.cloneNode(true)); | |
| 310 | }); | |
| 311 | var newCanonical = doc.querySelector('link[rel="canonical"]'); | |
| 312 | var curCanonical = document.querySelector('link[rel="canonical"]'); | |
| 313 | if (newCanonical && curCanonical) curCanonical.setAttribute('href', newCanonical.getAttribute('href') || ''); | |
| 314 | else if (newCanonical && !curCanonical) document.head.appendChild(newCanonical.cloneNode(true)); | |
| 200 | 315 | |
| 201 | // Detect layout type: home vs doc vs page | |
| 202 | var curLayout = document.querySelector('.VPHome') ? 'home' : (document.querySelector('.VPContent') ? 'doc' : 'page'); | |
| 203 | var newLayout = doc.querySelector('.VPHome') ? 'home' : (doc.querySelector('.VPContent') ? 'doc' : 'page'); | |
| 316 | var curLayout = detectLayout(document); | |
| 317 | var newLayout = detectLayout(doc); | |
| 204 | 318 | |
| 205 | 319 | if (curLayout !== newLayout) { |
| 206 | // Layout changed — full body swap | |
| 207 | 320 | document.body.innerHTML = doc.body.innerHTML; |
| 208 | // Re-execute scripts so sidebar toggle, theme toggle etc. work | |
| 209 | doc.body.querySelectorAll('script').forEach(function(s) { | |
| 210 | var ns = document.createElement('script'); | |
| 211 | if (s.src) ns.src = s.src; else ns.textContent = s.textContent; | |
| 212 | document.body.appendChild(ns); | |
| 213 | }); | |
| 321 | executeScripts(document.body); | |
| 322 | } else if (curLayout === 'home') { | |
| 323 | var newHome = doc.querySelector('.VPHome'); | |
| 324 | var curHome = document.querySelector('.VPHome'); | |
| 325 | if (newHome && curHome) { | |
| 326 | curHome.innerHTML = newHome.innerHTML; | |
| 327 | executeScripts(curHome); | |
| 328 | } | |
| 329 | } else if (curLayout === 'doc') { | |
| 330 | var newMain = doc.querySelector('.VPDoc'); | |
| 331 | var curMain = document.querySelector('.VPDoc'); | |
| 332 | if (newMain && curMain) { | |
| 333 | curMain.innerHTML = newMain.innerHTML; | |
| 334 | executeScripts(curMain); | |
| 335 | } | |
| 336 | var newSidebar = doc.querySelector('.VPSidebar'); | |
| 337 | var curSidebar = document.querySelector('.VPSidebar'); | |
| 338 | if (newSidebar && curSidebar) { | |
| 339 | curSidebar.innerHTML = newSidebar.innerHTML; | |
| 340 | executeScripts(curSidebar); | |
| 341 | } | |
| 342 | var newAside = doc.querySelector('.VPDocAside'); | |
| 343 | var curAside = document.querySelector('.VPDocAside'); | |
| 344 | if (newAside && curAside) { | |
| 345 | curAside.innerHTML = newAside.innerHTML; | |
| 346 | executeScripts(curAside); | |
| 347 | } | |
| 214 | 348 | } else { |
| 215 | // Same layout — swap content + sidebar + TOC | |
| 216 | if (curLayout === 'home') { | |
| 217 | var newHome = doc.querySelector('.VPHome'); | |
| 218 | var curHome = document.querySelector('.VPHome'); | |
| 219 | if (newHome && curHome) curHome.innerHTML = newHome.innerHTML; | |
| 220 | } else { | |
| 221 | // Doc/page layout: swap content area | |
| 222 | var newMain = doc.querySelector('.VPDoc') || doc.querySelector('main'); | |
| 223 | var curMain = document.querySelector('.VPDoc') || document.querySelector('main'); | |
| 224 | if (newMain && curMain) curMain.innerHTML = newMain.innerHTML; | |
| 225 | ||
| 226 | // Update sidebar | |
| 227 | var newSidebar = doc.querySelector('.VPSidebar'); | |
| 228 | var curSidebar = document.querySelector('.VPSidebar'); | |
| 229 | if (newSidebar && curSidebar) curSidebar.innerHTML = newSidebar.innerHTML; | |
| 230 | ||
| 231 | // Update TOC | |
| 232 | var newToc = doc.querySelector('.VPDocAside'); | |
| 233 | var curToc = document.querySelector('.VPDocAside'); | |
| 234 | if (newToc && curToc) curToc.innerHTML = newToc.innerHTML; | |
| 349 | // page layout | |
| 350 | var newPage = doc.querySelector('.VPPage') || doc.querySelector('.VPContent'); | |
| 351 | var curPage = document.querySelector('.VPPage') || document.querySelector('.VPContent'); | |
| 352 | if (newPage && curPage) { | |
| 353 | curPage.innerHTML = newPage.innerHTML; | |
| 354 | executeScripts(curPage); | |
| 235 | 355 | } |
| 236 | 356 | } |
| 237 | 357 | |
| 238 | // Update <style> tags (CSS may differ between pages) | |
| 239 | 358 | var newStyles = doc.querySelectorAll('style[data-crosswind]'); |
| 240 | var curStyles = document.querySelectorAll('style[data-crosswind]'); | |
| 241 | if (newStyles.length && curStyles.length) { | |
| 242 | curStyles.forEach(function(s) { s.remove(); }); | |
| 359 | if (newStyles.length) { | |
| 360 | document.querySelectorAll('style[data-crosswind]').forEach(function(s) { s.remove(); }); | |
| 243 | 361 | newStyles.forEach(function(s) { document.head.appendChild(s.cloneNode(true)); }); |
| 244 | 362 | } |
| 245 | 363 | |
| 364 | lastPathname = target.pathname; | |
| 365 | lastSearch = target.search; | |
| 246 | 366 | updateActiveLinks(); |
| 247 | window.scrollTo(0, 0); | |
| 367 | ||
| 368 | if (typeof opts.restoreScroll === 'number') { | |
| 369 | applyScroll(opts.restoreScroll); | |
| 370 | } else if (target.hash) { | |
| 371 | if (!scrollToHash(target.hash)) { | |
| 372 | requestAnimationFrame(function(){ scrollToHash(target.hash); }); | |
| 373 | } | |
| 374 | } else { | |
| 375 | applyScroll(0); | |
| 376 | } | |
| 248 | 377 | } |
| 249 | 378 | |
| 250 | // Intercept clicks on internal links | |
| 251 | 379 | document.addEventListener('click', function(e) { |
| 380 | if (e.defaultPrevented) return; | |
| 381 | if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return; | |
| 252 | 382 | var a = e.target.closest('a[href]'); |
| 253 | 383 | if (!a) return; |
| 384 | if (a.target && a.target !== '_self') return; | |
| 385 | if (a.hasAttribute('download')) return; | |
| 386 | var rel = a.getAttribute('rel') || ''; | |
| 387 | if (/\\bexternal\\b/.test(rel)) return; | |
| 254 | 388 | var href = a.getAttribute('href'); |
| 255 | if (!href || href.startsWith('#') || a.target === '_blank') return; | |
| 256 | var url = new URL(href, location.origin); | |
| 257 | if (!isInternal(href)) return; | |
| 389 | if (!href || href.startsWith('#')) return; | |
| 390 | var u; | |
| 391 | try { u = new URL(href, location.origin); } catch(_) { return; } | |
| 392 | if (!isInternalNavigable(u)) return; | |
| 393 | ||
| 258 | 394 | e.preventDefault(); |
| 259 | if (url.pathname === location.pathname) return; | |
| 260 | navigate(url.pathname); | |
| 395 | ||
| 396 | if (u.pathname === location.pathname && u.search === location.search) { | |
| 397 | // Same page; update hash + scroll without re-render | |
| 398 | saveScroll(); | |
| 399 | if (u.hash) { | |
| 400 | history.pushState({}, '', u.pathname + u.search + u.hash); | |
| 401 | scrollToHash(u.hash); | |
| 402 | } else { | |
| 403 | history.pushState({}, '', u.pathname + u.search); | |
| 404 | applyScroll(0); | |
| 405 | } | |
| 406 | lastPathname = u.pathname; | |
| 407 | lastSearch = u.search; | |
| 408 | return; | |
| 409 | } | |
| 410 | navigate(u.href, { push: true }); | |
| 261 | 411 | }); |
| 262 | 412 | |
| 263 | // Handle back/forward | |
| 264 | window.addEventListener('popstate', function() { | |
| 265 | navigate(location.pathname, false); | |
| 413 | window.addEventListener('popstate', function(e) { | |
| 414 | var newIdx = (e.state && typeof e.state.idx === 'number') ? e.state.idx : historyIndex; | |
| 415 | var savedScroll = scrollPositions[newIdx]; | |
| 416 | saveScroll(); // capture scroll for the page being left, under the *current* historyIndex | |
| 417 | historyIndex = newIdx; | |
| 418 | ||
| 419 | if (location.pathname === lastPathname && location.search === lastSearch) { | |
| 420 | // Hash-only or no-op change — skip re-render | |
| 421 | if (typeof savedScroll === 'number') applyScroll(savedScroll); | |
| 422 | else if (location.hash) { if (!scrollToHash(location.hash)) applyScroll(0); } | |
| 423 | else applyScroll(0); | |
| 424 | return; | |
| 425 | } | |
| 426 | navigate(location.href, { push: false, restoreScroll: typeof savedScroll === 'number' ? savedScroll : 0 }); | |
| 266 | 427 | }); |
| 267 | 428 | |
| 268 | // Prefetch on hover | |
| 269 | 429 | document.addEventListener('mouseover', function(e) { |
| 270 | 430 | var a = e.target.closest('a[href]'); |
| 271 | 431 | if (!a) return; |
| 272 | 432 | var href = a.getAttribute('href'); |
| 273 | if (href && isInternal(href) && !cache[href]) { | |
| 274 | fetch(href).then(function(r) { return r.text(); }).then(function(h) { cache[href] = h; }).catch(function(){}); | |
| 275 | } | |
| 433 | if (!href || href.startsWith('#')) return; | |
| 434 | var u; | |
| 435 | try { u = new URL(href, location.origin); } catch(_) { return; } | |
| 436 | if (!isInternalNavigable(u)) return; | |
| 437 | var key = u.pathname + u.search; | |
| 438 | if (cache[key]) return; | |
| 439 | fetch(key).then(function(r){ return r.ok ? r.text() : null; }).then(function(h){ if (h) cache[key] = h; }).catch(function(){}); | |
| 276 | 440 | }); |
| 277 | 441 | |
| 442 | window.addEventListener('beforeunload', saveScroll); | |
| 443 | ||
| 278 | 444 | updateActiveLinks(); |
| 279 | 445 | })(); |
| 280 | 446 | <` + `/script>` |
| @@ -298,25 +464,22 @@ function generateNav(config: BunPressConfig): string { | ||
| 298 | 464 | } |
| 299 | 465 | |
| 300 | 466 | const links = navConfig.map((item) => { |
| 301 | // Handle items with sub-items (dropdown) | |
| 302 | 467 | if (item.items && item.items.length > 0) { |
| 303 | return `<div class="relative group"> | |
| 304 | <button class="flex gap-1 items-center font-medium text-[#213547] text-[14px] hover:text-[#5672cd] transition-colors cursor-pointer"> | |
| 305 | ${item.text} | |
| 306 | <svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| 468 | return `<div class="VPNavBarMenu-group"> | |
| 469 | <button class="VPNavBarMenu-group-button" type="button"> | |
| 470 | <span>${item.text}</span> | |
| 471 | <svg class="chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| 307 | 472 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> |
| 308 | 473 | </svg> |
| 309 | 474 | </button> |
| 310 | <div class="hidden group-hover:block absolute right-0 top-full mt-2 py-2 min-w-[160px] bg-white border border-[#e2e2e3] rounded-lg shadow-lg"> | |
| 475 | <div class="VPNavBarMenu-group-items"> | |
| 311 | 476 | ${item.items.map(subItem => |
| 312 | `<a href="${fixNavLink(subItem.link)}" class="block px-4 py-2 text-[#213547] text-[13px] hover:text-[#5672cd] hover:bg-[#f6f6f7] transition-colors">${subItem.text}</a>`, | |
| 477 | `<a href="${fixNavLink(subItem.link)}">${subItem.text}</a>`, | |
| 313 | 478 | ).join('')} |
| 314 | 479 | </div> |
| 315 | 480 | </div>` |
| 316 | 481 | } |
| 317 | else { | |
| 318 | return `<a href="${fixNavLink(item.link)}" class="font-medium text-[#213547] text-[14px] hover:text-[#5672cd] transition-colors">${item.text}</a>` | |
| 319 | } | |
| 482 | return `<a class="VPNavBarMenu-link" href="${fixNavLink(item.link)}">${item.text}</a>` | |
| 320 | 483 | }).join('') |
| 321 | 484 | |
| 322 | 485 | return links |
| @@ -777,25 +940,22 @@ async function generateHero(hero: any): Promise<string> { | ||
| 777 | 940 | return '' |
| 778 | 941 | |
| 779 | 942 | const name = hero.name |
| 780 | ? `<p style="font-size: 20px; font-weight: 700; letter-spacing: -0.02em; line-height: 1.2; background: linear-gradient(135deg, #5672cd 0%, #8b9cf7 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; margin: 0 0 8px;">${hero.name}</p>` | |
| 943 | ? `<p class="VPHero-name">${hero.name}</p>` | |
| 781 | 944 | : '' |
| 782 | 945 | const text = hero.text |
| 783 | ? `<h1 style="font-size: 40px; font-weight: 800; letter-spacing: -0.02em; line-height: 1.1; color: var(--bp-c-text-1); margin: 0 0 8px;">${hero.text}</h1>` | |
| 946 | ? `<h1 class="VPHero-text">${hero.text}</h1>` | |
| 784 | 947 | : '' |
| 785 | 948 | const tagline = hero.tagline |
| 786 | ? `<p style="font-size: 18px; font-weight: 400; line-height: 1.6; color: var(--bp-c-text-2); margin: 12px 0 0;">${hero.tagline}</p>` | |
| 949 | ? `<p class="VPHero-tagline">${hero.tagline}</p>` | |
| 787 | 950 | : '' |
| 788 | 951 | |
| 789 | 952 | let actions = '' |
| 790 | 953 | if (hero.actions) { |
| 791 | 954 | const actionButtons = hero.actions.map((action: any) => { |
| 792 | const isPrimary = action.theme === 'brand' | |
| 793 | if (isPrimary) { | |
| 794 | return `<a href="${action.link}" style="display: inline-block; padding: 10px 24px; font-size: 14px; font-weight: 600; color: #fff; background: #5672cd; border-radius: 20px; text-decoration: none; transition: background 0.25s; border: 1px solid transparent;" onmouseover="this.style.background='#4558b8'" onmouseout="this.style.background='#5672cd'">${action.text}</a>` | |
| 795 | } | |
| 796 | return `<a href="${action.link}" style="display: inline-block; padding: 10px 24px; font-size: 14px; font-weight: 600; color: var(--bp-c-text-1); background: transparent; border: 1px solid var(--bp-c-divider); border-radius: 20px; text-decoration: none; transition: border-color 0.25s;" onmouseover="this.style.borderColor='var(--bp-c-text-2)'" onmouseout="this.style.borderColor='var(--bp-c-divider)'">${action.text}</a>` | |
| 955 | const cls = action.theme === 'brand' ? 'VPButton VPButton-brand' : 'VPButton VPButton-alt' | |
| 956 | return `<a class="${cls}" href="${action.link}">${action.text}</a>` | |
| 797 | 957 | }).join('\n ') |
| 798 | actions = `<div style="display: flex; flex-wrap: wrap; gap: 12px; margin-top: 28px;">${actionButtons}</div>` | |
| 958 | actions = `<div class="VPHero-actions">${actionButtons}</div>` | |
| 799 | 959 | } |
| 800 | 960 | |
| 801 | 961 | const image = hero.image |
| @@ -831,7 +991,7 @@ function getFeatureIcon(icon: string): string { | ||
| 831 | 991 | // If it's an emoji or HTML, pass through |
| 832 | 992 | if (icon.startsWith('<') || /\p{Emoji}/u.test(icon)) return icon |
| 833 | 993 | // Check icon map |
| 834 | return featureIconMap[icon] || `<span style="font-size: 24px; font-weight: 700;">${icon.charAt(0)}</span>` | |
| 994 | return featureIconMap[icon] || `<span class="VPFeature-icon-text">${icon.charAt(0)}</span>` | |
| 835 | 995 | } |
| 836 | 996 | |
| 837 | 997 | async function generateFeatures(features: any[]): Promise<string> { |
| @@ -840,15 +1000,16 @@ async function generateFeatures(features: any[]): Promise<string> { | ||
| 840 | 1000 | |
| 841 | 1001 | const items = features.map(feature => { |
| 842 | 1002 | const icon = feature.icon ? getFeatureIcon(feature.icon) : '' |
| 843 | const iconHtml = icon | |
| 844 | ? `<div style="width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; border-radius: 8px; background: rgba(86, 114, 205, 0.1); color: #5672cd; margin-bottom: 12px;">${icon}</div>` | |
| 845 | : '' | |
| 1003 | const iconHtml = icon ? `<div class="VPFeature-icon">${icon}</div>` : '' | |
| 1004 | const link = feature.link | |
| 1005 | const tag = link ? 'a' : 'div' | |
| 1006 | const linkAttr = link ? ` href="${link}"` : '' | |
| 846 | 1007 | return ` |
| 847 | <div style="padding: 24px; background: var(--bp-c-bg-soft, var(--bp-c-bg-alt)); border: 1px solid var(--bp-c-divider); border-radius: 12px; transition: border-color 0.25s, box-shadow 0.25s;" onmouseover="this.style.borderColor='#5672cd';this.style.boxShadow='0 2px 12px rgba(86,114,205,0.08)'" onmouseout="this.style.borderColor='var(--bp-c-divider)';this.style.boxShadow='none'"> | |
| 1008 | <${tag} class="VPFeature"${linkAttr}> | |
| 848 | 1009 | ${iconHtml} |
| 849 | <h3 style="margin: 0 0 8px; font-size: 16px; font-weight: 600; color: var(--bp-c-text-1);">${feature.title || ''}</h3> | |
| 850 | <p style="margin: 0; font-size: 14px; line-height: 1.6; color: var(--bp-c-text-2);">${feature.details || ''}</p> | |
| 851 | </div>` | |
| 1010 | <h3 class="VPFeature-title">${feature.title || ''}</h3> | |
| 1011 | <p class="VPFeature-details">${feature.details || ''}</p> | |
| 1012 | </${tag}>` | |
| 852 | 1013 | }).join('') |
| 853 | 1014 | |
| 854 | 1015 | return await render('features', { |
| @@ -1106,24 +1267,11 @@ function processBadges(content: string): string { | ||
| 1106 | 1267 | const badgeRegex = /<Badge([^/>]+)\/>/gi |
| 1107 | 1268 | |
| 1108 | 1269 | return content.replace(badgeRegex, (_match, attributes) => { |
| 1109 | // Extract type and text attributes | |
| 1110 | 1270 | const typeMatch = attributes.match(/type="(info|tip|warning|danger)"/i) |
| 1111 | 1271 | const textMatch = attributes.match(/text="([^"]+)"/) |
| 1112 | ||
| 1113 | 1272 | const type = typeMatch ? typeMatch[1].toLowerCase() : 'info' |
| 1114 | 1273 | const text = textMatch ? textMatch[1] : '' |
| 1115 | ||
| 1116 | // Badge color schemes matching VitePress | |
| 1117 | const colors: Record<string, { bg: string, text: string, border: string }> = { | |
| 1118 | info: { bg: '#e0f2fe', text: '#0c4a6e', border: '#0ea5e9' }, | |
| 1119 | tip: { bg: '#dcfce7', text: '#14532d', border: '#22c55e' }, | |
| 1120 | warning: { bg: '#fef3c7', text: '#78350f', border: '#f59e0b' }, | |
| 1121 | danger: { bg: '#fee2e2', text: '#7f1d1d', border: '#ef4444' }, | |
| 1122 | } | |
| 1123 | ||
| 1124 | const color = colors[type] || colors.info | |
| 1125 | ||
| 1126 | return `<span class="badge badge-${type}" style="display: inline-block; padding: 2px 8px; font-size: 0.85em; font-weight: 600; border-radius: 4px; background: ${color.bg}; color: ${color.text}; border: 1px solid ${color.border}; margin: 0 4px; vertical-align: middle;">${text}</span>` | |
| 1274 | return `<span class="bp-badge bp-badge-${type}">${text}</span>` | |
| 1127 | 1275 | }) |
| 1128 | 1276 | } |
| 1129 | 1277 | |
| @@ -1161,7 +1309,7 @@ function addExternalLinkIcons(html: string): string { | ||
| 1161 | 1309 | if (match.includes('external-link-icon')) |
| 1162 | 1310 | return match |
| 1163 | 1311 | |
| 1164 | const externalIcon = '<svg class="external-link-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: inline-block; margin-left: 4px; vertical-align: middle;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>' | |
| 1312 | const externalIcon = '<svg class="external-link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>' | |
| 1165 | 1313 | return match.replace('</a>', `${externalIcon}</a>`) |
| 1166 | 1314 | }) |
| 1167 | 1315 | } |
| @@ -234,6 +234,11 @@ function initPageTOC() { | ||
| 234 | 234 | }); |
| 235 | 235 | |
| 236 | 236 | // Listen to scroll events on the actual scrolling container |
| 237 | // Remove any prior listener first so SPA navigation doesn't stack duplicates | |
| 238 | if (scrollContainer.__bpTocScroll) { | |
| 239 | scrollContainer.removeEventListener('scroll', scrollContainer.__bpTocScroll); | |
| 240 | } | |
| 241 | scrollContainer.__bpTocScroll = onScroll; | |
| 237 | 242 | scrollContainer.addEventListener('scroll', onScroll); |
| 238 | 243 | |
| 239 | 244 | // Handle URL hash on page load |