diff --git a/src/components/callouts/Callout.astro b/src/components/callouts/Callout.astro
index af3ad35..0b2271a 100644
--- a/src/components/callouts/Callout.astro
+++ b/src/components/callouts/Callout.astro
@@ -98,7 +98,7 @@ const iconClass = `mt-0.5 h-4 w-4 shrink-0 ${c.text}`;
class:list={[
"min-w-0",
c.text,
- "[&_a]:border-current [&_a]:text-current [&_code]:text-current [&_p]:leading-relaxed",
+ "[&_a]:border-current [&_a]:text-current [&_code]:text-current [&_p]:leading-relaxed [&_p]:text-current [&_strong]:text-current [&_li]:text-current",
]}
>
{
@@ -106,7 +106,7 @@ const iconClass = `mt-0.5 h-4 w-4 shrink-0 ${c.text}`;
{title}
)
}
-
diff --git a/src/components/tabs/Tab.astro b/src/components/tabs/Tab.astro
new file mode 100644
index 0000000..983cb12
--- /dev/null
+++ b/src/components/tabs/Tab.astro
@@ -0,0 +1,27 @@
+---
+import type { icons } from "lucide-react";
+import Icon from "../Icon.astro";
+
+interface Props {
+ title: string;
+ icon?: keyof typeof icons;
+ class?: string;
+}
+
+const { title, icon, class: className } = Astro.props;
+---
+
+
+ {
+ icon && (
+
+
+
+ )
+ }
+
+
diff --git a/src/components/tabs/Tabs.astro b/src/components/tabs/Tabs.astro
new file mode 100644
index 0000000..7db7a87
--- /dev/null
+++ b/src/components/tabs/Tabs.astro
@@ -0,0 +1,212 @@
+---
+interface Props {
+ defaultIndex?: number;
+ class?: string;
+}
+
+const { defaultIndex = 0, class: className } = Astro.props;
+---
+
+
+
+
+
+
+
+
diff --git a/src/components/tabs/index.ts b/src/components/tabs/index.ts
new file mode 100644
index 0000000..0d5e110
--- /dev/null
+++ b/src/components/tabs/index.ts
@@ -0,0 +1,2 @@
+export { default as Tabs } from './Tabs.astro'
+export { default as Tab } from './Tab.astro'
diff --git a/src/lib/cleanup-headings.ts b/src/lib/cleanup-headings.ts
index 1c3e3cb..036b674 100644
--- a/src/lib/cleanup-headings.ts
+++ b/src/lib/cleanup-headings.ts
@@ -1,26 +1,70 @@
import type { MarkdownHeading } from 'astro'
-function extractRawHeadingTexts(body: string): string[] {
- const headingRegex = /^(#{2,3})\s+(.+)$/gm
- const texts: string[] = []
- let match
- while ((match = headingRegex.exec(body)) !== null) {
- const rawText = match[2].trim()
- // strip jsx expressions from raw text
- const text = rawText.replace(/\{[^}]*\}/g, '').trim()
- texts.push(text)
+interface ParsedHeading {
+ text: string
+ insideTabs: boolean
+}
+
+function stripFences(body: string): string {
+ const lines = body.split('\n')
+ const out: string[] = []
+ let opener: string | null = null
+ for (const line of lines) {
+ const m = line.match(/^[ \t]{0,3}(`{3,}|~{3,})/)
+ if (opener === null) {
+ if (m) {
+ opener = m[1]
+ out.push('')
+ continue
+ }
+ out.push(line)
+ } else {
+ if (m && m[1][0] === opener[0] && m[1].length >= opener.length) {
+ opener = null
+ }
+ out.push('')
+ }
}
- return texts
+ return out.join('\n')
+}
+
+const TOKEN_RE = /|<\/Tabs>|^[ \t]*#{2,3}[ \t]+.+$/gm
+
+function parseHeadings(body: string): ParsedHeading[] {
+ const cleaned = stripFences(body)
+ const out: ParsedHeading[] = []
+ let tabsDepth = 0
+ for (const match of cleaned.matchAll(TOKEN_RE)) {
+ const tok = match[0]
+ if (tok.startsWith('$/.test(tok)) tabsDepth++
+ } else {
+ const hm = tok.match(/^[ \t]*(#{2,3})[ \t]+(.+)$/)
+ if (hm) {
+ const rawText = hm[2]
+ .trim()
+ .replace(/\{[^}]*\}/g, '')
+ .trim()
+ out.push({ text: rawText, insideTabs: tabsDepth > 0 })
+ }
+ }
+ }
+ return out
}
export const cleanupHeadings = (body: string, headings: MarkdownHeading[]): MarkdownHeading[] => {
- const rawTexts = extractRawHeadingTexts(body ?? '')
+ const parsed = parseHeadings(body ?? '')
+
return headings
.filter((h) => h.depth >= 2 && h.depth <= 3)
.map((h, index) => ({
depth: h.depth,
slug: h.slug,
- // use cleaned text from raw body if available, fallback to processed text
- text: rawTexts[index] ?? h.text,
+ text: parsed[index]?.text ?? h.text,
+ insideTabs: parsed[index]?.insideTabs ?? false,
}))
+ .filter((h) => !h.insideTabs)
+ .map(({ depth, slug, text }) => ({ depth, slug, text }))
}
diff --git a/src/styles/global.css b/src/styles/global.css
index 82fc166..8689253 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -240,6 +240,60 @@
opacity: 1;
}
+@layer components {
+ .prose {
+ --tw-prose-headings: var(--color-stone-900);
+ --tw-prose-body: var(--color-stone-600);
+ --tw-prose-links: var(--color-stone-900);
+ --tw-prose-bold: var(--color-stone-800);
+ --tw-prose-code: var(--color-stone-700);
+ }
+ .dark .prose {
+ --tw-prose-invert-headings: var(--color-stone-100);
+ --tw-prose-invert-body: var(--color-stone-300);
+ --tw-prose-invert-links: var(--color-stone-100);
+ --tw-prose-invert-bold: var(--color-stone-200);
+ --tw-prose-invert-code: var(--color-stone-300);
+ }
+
+ .prose :is(h1, h2, h3, h4, h5, h6):not(:where([class~='not-prose'], [class~='not-prose'] *)) {
+ font-weight: 600;
+ }
+
+ .prose a:not(:where([class~='not-prose'], [class~='not-prose'] *)) {
+ text-decoration: underline;
+ text-underline-offset: 2px;
+ text-decoration-color: var(--color-stone-300);
+ }
+ .prose a:hover:not(:where([class~='not-prose'], [class~='not-prose'] *)) {
+ text-decoration-color: var(--color-stone-500);
+ }
+ .dark .prose a:not(:where([class~='not-prose'], [class~='not-prose'] *)) {
+ text-decoration-color: var(--color-stone-600);
+ }
+ .dark .prose a:hover:not(:where([class~='not-prose'], [class~='not-prose'] *)) {
+ text-decoration-color: var(--color-stone-400);
+ }
+
+ .prose code:not(:where([class~='not-prose'], [class~='not-prose'] *)) {
+ border-radius: var(--radius-md);
+ background-color: var(--color-stone-100);
+ padding-inline: 0.375rem;
+ padding-block: 0.125rem;
+ }
+ .prose code:not(:where([class~='not-prose'], [class~='not-prose'] *))::before,
+ .prose code:not(:where([class~='not-prose'], [class~='not-prose'] *))::after {
+ content: none;
+ }
+ .dark .prose code:not(:where([class~='not-prose'], [class~='not-prose'] *)) {
+ background-color: var(--color-stone-800);
+ }
+
+ .prose th:not(:where([class~='not-prose'], [class~='not-prose'] *)) {
+ text-align: left;
+ }
+}
+
/* Docs assistant — shimmer text for in-flight tool indicators */
@keyframes docs-assistant-shimmer {
0% {