ReviewOS

also looking at this

stacks/bunpress

fix(spa): repair client-side router and polish default theme

#71
Merged glennmichael123 wants to merge fix/spa-nav-and-theme-polish into main
9 files +1037 -170
packages/bunpress/src/serve.tsmodified+264-116
Changes to packages/bunpress/src/serve.ts
@@ -39,12 +39,10 @@ async function generateSidebar(config: BunPressConfig, currentPath: string): Pro
3939 const sectionsHtml = await Promise.all(sidebarSections.map(async (section) => {
4040 const itemsHtml = section.items
4141 ? section.items.map((item: SidebarItem) => {
42 // Use the link as-is (no /docs/ prefix needed)
4342 const link = item.link || '/'
44
4543 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>`
4846 }).join('')
4947 : ''
5048
@@ -149,132 +147,300 @@ function injectSPARouter(html: string): string {
149147 * browser back/forward. No framework dependencies.
150148 */
151149function generateSPARouterScript(): string {
152 return `<script>
150 return `<script data-bp-router="1">
153151(function(){
154 var cache = {};
152 if (history.scrollRestoration) history.scrollRestoration = 'manual';
153
154 var cache = Object.create(null);
155155 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 }
156183
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);
162212 }
163213
164214 function updateActiveLinks() {
165215 var path = location.pathname;
216 document.querySelectorAll('a.is-active').forEach(function(a) {
217 a.classList.remove('is-active');
218 });
166219 document.querySelectorAll('a[href]').forEach(function(a) {
167220 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');
171229 }
172230 });
173231 }
174232
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 }
177279
178 var html = cache[url];
280 var html = cache[key];
179281 if (!html) {
180282 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 }
182289 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;
186293 return;
187294 }
188295 }
189296
190297 var doc = parser.parseFromString(html, 'text/html');
191298
192 // Update title
193299 var newTitle = doc.querySelector('title');
194300 if (newTitle) document.title = newTitle.textContent;
195301
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));
200315
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);
204318
205319 if (curLayout !== newLayout) {
206 // Layout changed — full body swap
207320 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 }
214348 } 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);
235355 }
236356 }
237357
238 // Update <style> tags (CSS may differ between pages)
239358 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(); });
243361 newStyles.forEach(function(s) { document.head.appendChild(s.cloneNode(true)); });
244362 }
245363
364 lastPathname = target.pathname;
365 lastSearch = target.search;
246366 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 }
248377 }
249378
250 // Intercept clicks on internal links
251379 document.addEventListener('click', function(e) {
380 if (e.defaultPrevented) return;
381 if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;
252382 var a = e.target.closest('a[href]');
253383 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;
254388 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
258394 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 });
261411 });
262412
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 });
266427 });
267428
268 // Prefetch on hover
269429 document.addEventListener('mouseover', function(e) {
270430 var a = e.target.closest('a[href]');
271431 if (!a) return;
272432 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(){});
276440 });
277441
442 window.addEventListener('beforeunload', saveScroll);
443
278444 updateActiveLinks();
279445})();
280446<` + `/script>`
@@ -298,25 +464,22 @@ function generateNav(config: BunPressConfig): string {
298464 }
299465
300466 const links = navConfig.map((item) => {
301 // Handle items with sub-items (dropdown)
302467 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">
307472 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
308473 </svg>
309474 </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">
311476 ${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>`,
313478 ).join('')}
314479 </div>
315480 </div>`
316481 }
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>`
320483 }).join('')
321484
322485 return links
@@ -777,25 +940,22 @@ async function generateHero(hero: any): Promise<string> {
777940 return ''
778941
779942 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>`
781944 : ''
782945 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>`
784947 : ''
785948 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>`
787950 : ''
788951
789952 let actions = ''
790953 if (hero.actions) {
791954 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>`
797957 }).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>`
799959 }
800960
801961 const image = hero.image
@@ -831,7 +991,7 @@ function getFeatureIcon(icon: string): string {
831991 // If it's an emoji or HTML, pass through
832992 if (icon.startsWith('<') || /\p{Emoji}/u.test(icon)) return icon
833993 // 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>`
835995}
836996
837997async function generateFeatures(features: any[]): Promise<string> {
@@ -840,15 +1000,16 @@ async function generateFeatures(features: any[]): Promise<string> {
8401000
8411001 const items = features.map(feature => {
8421002 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}"` : ''
8461007 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}>
8481009 ${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}>`
8521013 }).join('')
8531014
8541015 return await render('features', {
@@ -1106,24 +1267,11 @@ function processBadges(content: string): string {
11061267 const badgeRegex = /<Badge([^/>]+)\/>/gi
11071268
11081269 return content.replace(badgeRegex, (_match, attributes) => {
1109 // Extract type and text attributes
11101270 const typeMatch = attributes.match(/type="(info|tip|warning|danger)"/i)
11111271 const textMatch = attributes.match(/text="([^"]+)"/)
1112
11131272 const type = typeMatch ? typeMatch[1].toLowerCase() : 'info'
11141273 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>`
11271275 })
11281276}
11291277
@@ -1161,7 +1309,7 @@ function addExternalLinkIcons(html: string): string {
11611309 if (match.includes('external-link-icon'))
11621310 return match
11631311
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>'
11651313 return match.replace('</a>', `${externalIcon}</a>`)
11661314 })
11671315}