Skip to content

Commit fd55d1e

Browse files
jaysin586claude
andcommitted
feat: add shared layout, ToC, Example, and Admonition components
Extract duplicated docs infrastructure into reusable components: - TableOfContents + extractHeadings (github-slugger) for heading tracking - DocsLayout: three-column layout with sidebar, prose content, and ToC - RootLayout: ModeWatcher, SEO context, breadcrumbs, MotionConfig, JSON-LD - Example: unified toolbar with refresh, source link, dotted grid background - Admonition: info/note/warning/error/success callout boxes - Extend DocsKitConfig with softwareRequirements and programmingLanguages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2014a51 commit fd55d1e

File tree

10 files changed

+695
-0
lines changed

10 files changed

+695
-0
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
"runed": "^0.37.1",
102102
"satori": "^0.24.1",
103103
"satori-html": "^0.3.2",
104+
"github-slugger": "^2.0.0",
104105
"tailwind-merge": "^3.5.0"
105106
}
106107
}

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<!--
2+
@component
3+
Admonition callout box for docs: info, note, warning, error, success variants.
4+
-->
5+
<script lang="ts">
6+
import type { Snippet } from 'svelte'
7+
import { CircleX, TriangleAlert, CircleCheck, StickyNote, Info } from '@lucide/svelte'
8+
9+
type Props = {
10+
type?: 'info' | 'note' | 'warning' | 'error' | 'success'
11+
title?: string
12+
children?: Snippet
13+
}
14+
15+
const { type = 'info', title = '', children }: Props = $props()
16+
17+
const ariaRole: string =
18+
type === 'error' ? 'alert' : type === 'info' || type === 'note' ? 'note' : 'status'
19+
20+
const typeLabel: string =
21+
type === 'error'
22+
? 'Error'
23+
: type === 'warning'
24+
? 'Warning'
25+
: type === 'success'
26+
? 'Success'
27+
: type === 'note'
28+
? 'Note'
29+
: 'Information'
30+
</script>
31+
32+
<div class="admonition" data-type={type} role={ariaRole}>
33+
<span class="sr-only">{typeLabel}:</span>
34+
<div class="row">
35+
<div class="icon" aria-hidden="true">
36+
{#if type === 'error'}
37+
<CircleX class="size-4" />
38+
{:else if type === 'warning'}
39+
<TriangleAlert class="size-4" />
40+
{:else if type === 'success'}
41+
<CircleCheck class="size-4" />
42+
{:else if type === 'note'}
43+
<StickyNote class="size-4" />
44+
{:else}
45+
<Info class="size-4" />
46+
{/if}
47+
</div>
48+
<div class="body">
49+
{#if title}
50+
<div class="title">{title}</div>
51+
{/if}
52+
<div class="content">
53+
{@render children?.()}
54+
</div>
55+
</div>
56+
</div>
57+
</div>
58+
59+
<style>
60+
.admonition {
61+
border: 1px solid var(--border);
62+
border-left-width: 4px;
63+
border-radius: 0.5rem;
64+
padding: 0.75rem 1rem;
65+
margin: 1rem 0;
66+
background: var(--card);
67+
}
68+
.admonition .row {
69+
display: flex;
70+
align-items: flex-start;
71+
gap: 0.5rem;
72+
}
73+
.admonition .icon {
74+
flex: none;
75+
width: 1rem;
76+
height: 1rem;
77+
margin-top: 0.125rem;
78+
font-size: 1rem;
79+
line-height: 1;
80+
}
81+
.admonition[data-type='info'] .icon {
82+
color: var(--brand-500);
83+
}
84+
.admonition[data-type='note'] .icon {
85+
color: var(--muted-foreground);
86+
}
87+
.admonition[data-type='warning'] .icon {
88+
color: var(--admonition-warning, #f59e0b);
89+
}
90+
.admonition[data-type='error'] .icon {
91+
color: var(--admonition-error, #ef4444);
92+
}
93+
.admonition[data-type='success'] .icon {
94+
color: var(--admonition-success, #22c55e);
95+
}
96+
.admonition .title {
97+
font-weight: 600;
98+
margin-bottom: 0.25rem;
99+
}
100+
.admonition[data-type='info'] {
101+
border-left-color: var(--brand-500);
102+
}
103+
.admonition[data-type='note'] {
104+
border-left-color: var(--muted-foreground);
105+
}
106+
.admonition[data-type='warning'] {
107+
border-left-color: var(--admonition-warning, #f59e0b);
108+
}
109+
.admonition[data-type='error'] {
110+
border-left-color: var(--admonition-error, #ef4444);
111+
}
112+
.admonition[data-type='success'] {
113+
border-left-color: var(--admonition-success, #22c55e);
114+
}
115+
</style>
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<!--
2+
@component
3+
Three-column docs layout: left sidebar, content with prose, right ToC.
4+
Handles heading extraction, MutationObserver, breadcrumbs, and code block enhancement.
5+
-->
6+
<script lang="ts">
7+
import { page } from '$app/state'
8+
import type { Snippet } from 'svelte'
9+
import type { DocsKitConfig } from '../config.js'
10+
import { getBreadcrumbContext, type Breadcrumb } from '../contexts/breadcrumb.js'
11+
import type { NavItem, NavSection } from '../types/nav.js'
12+
import { getDocsTitleByPath } from '../utils/nav.js'
13+
import { extractHeadings, type TocHeading } from '../utils/headings.js'
14+
import { enhanceCodeBlocks } from '../actions/enhanceCodeBlocks.js'
15+
import Footer from './Footer.svelte'
16+
import Header from './Header.svelte'
17+
import Sidebar from './Sidebar.svelte'
18+
import TableOfContents from './TableOfContents.svelte'
19+
20+
const {
21+
config,
22+
sections,
23+
otherProjects = [],
24+
loveAndRespect,
25+
favicon = '/logo.svg',
26+
breadcrumbResolver,
27+
children,
28+
head
29+
}: {
30+
config: DocsKitConfig
31+
sections: NavSection[]
32+
otherProjects?: NavItem[]
33+
loveAndRespect?: NavItem[]
34+
favicon?: string
35+
breadcrumbResolver?: (pathname: string) => Breadcrumb[]
36+
children: Snippet
37+
head?: Snippet
38+
} = $props()
39+
40+
const breadcrumbContext = getBreadcrumbContext()
41+
$effect(() => {
42+
if (breadcrumbContext) {
43+
const pathname = page.url.pathname as string
44+
if (breadcrumbResolver) {
45+
breadcrumbContext.breadcrumbs = breadcrumbResolver(pathname)
46+
} else {
47+
const title = getDocsTitleByPath(sections, pathname)
48+
breadcrumbContext.breadcrumbs =
49+
title && pathname !== '/docs'
50+
? [{ title: 'Docs', href: '/docs' }, { title }]
51+
: [{ title: 'Docs' }]
52+
}
53+
}
54+
})
55+
56+
let contentElement: HTMLElement | undefined = $state(undefined)
57+
let headings: TocHeading[] = $state([])
58+
59+
function refreshHeadings() {
60+
if (!contentElement) return
61+
headings = extractHeadings(contentElement)
62+
}
63+
64+
// Setup MutationObserver to watch for DOM changes and initial extraction
65+
$effect(() => {
66+
if (!contentElement) return
67+
68+
// Initial extraction
69+
refreshHeadings()
70+
71+
// Watch for DOM mutations (new content loaded via navigation)
72+
let rafId: number | null = null
73+
const observer = new MutationObserver(() => {
74+
if (rafId) cancelAnimationFrame(rafId)
75+
rafId = requestAnimationFrame(() => {
76+
rafId = null
77+
refreshHeadings()
78+
})
79+
})
80+
81+
observer.observe(contentElement, {
82+
childList: true,
83+
subtree: true
84+
})
85+
86+
return () => {
87+
if (rafId) cancelAnimationFrame(rafId)
88+
observer.disconnect()
89+
}
90+
})
91+
</script>
92+
93+
<svelte:head>
94+
{#if head}
95+
{@render head()}
96+
{/if}
97+
</svelte:head>
98+
99+
<div class="flex min-h-screen flex-col justify-between bg-background">
100+
<Header {config} {favicon} />
101+
102+
<div class="flex flex-1">
103+
<!-- Left sidebar - Navigation -->
104+
<aside
105+
class="hidden w-64 shrink-0 border-r border-sidebar-border bg-sidebar-background/95 shadow-sm lg:sticky lg:top-0 lg:block lg:h-screen lg:overflow-y-auto"
106+
>
107+
<Sidebar
108+
{config}
109+
{sections}
110+
currentPath={page.url.pathname}
111+
{otherProjects}
112+
{loveAndRespect}
113+
/>
114+
</aside>
115+
116+
<!-- Main content area -->
117+
<main class="flex-1">
118+
<div class="flex">
119+
<!-- Content -->
120+
<article
121+
bind:this={contentElement}
122+
use:enhanceCodeBlocks
123+
class="flex-1 px-4 py-8 sm:px-6 lg:px-8"
124+
>
125+
<div
126+
class="prose max-w-none text-text-primary prose-slate dark:prose-invert prose-headings:scroll-mt-20"
127+
>
128+
{@render children()}
129+
</div>
130+
</article>
131+
132+
<!-- Right sidebar - Table of Contents -->
133+
<aside
134+
class="hidden w-56 shrink-0 border-l border-sidebar-border bg-sidebar-background/95 shadow-sm xl:sticky xl:top-0 xl:block xl:h-screen xl:overflow-y-auto"
135+
>
136+
<TableOfContents {headings} />
137+
</aside>
138+
</div>
139+
</main>
140+
</div>
141+
<Footer />
142+
</div>

0 commit comments

Comments
 (0)