Skip to content

Commit fd4aa0c

Browse files
jaysin586claude
andcommitted
fix(docs): close all deltas with svelte-markdown docs site
1. enhanceCodeBlocks β€” add to examples layout for copy buttons 2. GitHub stats fetch β€” add fetch-github-stats.ts script, wire into build pipeline so stars update on each deploy 3. CSP headers β€” add Content-Security-Policy directives to svelte.config.js (hash mode, strict defaults) 4. Prettier config β€” add .prettierrc and .prettierignore to docs/ 5. llms-full.txt β€” comprehensive LLM reference document with all props, types, methods, usage patterns, and doc links 6. Social cards route β€” add /social-cards with OG.svelte for dynamic Open Graph image generation 7. Vite chunking β€” isolate shiki and marked into separate chunks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 859b4d4 commit fd4aa0c

10 files changed

Lines changed: 291 additions & 4 deletions

File tree

β€Ždocs/.prettierignoreβ€Ž

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Ignore files for PNPM, NPM and YARN
2+
pnpm-lock.yaml
3+
package-lock.json
4+
yarn.lock
5+
6+
# Generated outputs
7+
node_modules/
8+
.svelte-kit/
9+
build/

β€Ždocs/.prettierrcβ€Ž

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"overrides": [
3+
{
4+
"files": "*.svelte",
5+
"options": {
6+
"parser": "svelte"
7+
}
8+
}
9+
],
10+
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
11+
"printWidth": 100,
12+
"semi": false,
13+
"singleQuote": true,
14+
"tabWidth": 4,
15+
"trailingComma": "none",
16+
"useTabs": false
17+
}

β€Ždocs/package.jsonβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"type": "module",
66
"scripts": {
7-
"build": "node ./scripts/generate-sitemap-manifest.mjs && tsx ./scripts/generate-social-cards.ts && vite build",
7+
"build": "tsx ./scripts/fetch-github-stats.ts && node ./scripts/generate-sitemap-manifest.mjs && tsx ./scripts/generate-social-cards.ts && vite build",
88
"cf-typegen": "wrangler types && mv worker-configuration.d.ts src/",
99
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
1010
"deploy": "npm run build && wrangler deploy",
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { fetchGitHubStats } from '@humanspeak/docs-kit/scripts/fetch-github-stats'
2+
import path from 'path'
3+
import { fileURLToPath } from 'url'
4+
import { docsConfig } from '../src/lib/docs-config'
5+
6+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
7+
8+
await fetchGitHubStats({
9+
repo: docsConfig.repo,
10+
fallbackStars: docsConfig.fallbackStars,
11+
outputPath: path.resolve(__dirname, '..', 'src', 'lib', 'github-stats.json')
12+
})

β€Ždocs/src/routes/examples/+layout.svelteβ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script lang="ts">
22
import { page } from '$app/state'
33
import { afterNavigate } from '$app/navigation'
4-
import { Header, Footer, getBreadcrumbContext } from '@humanspeak/docs-kit'
4+
import { Header, Footer, getBreadcrumbContext, enhanceCodeBlocks } from '@humanspeak/docs-kit'
55
import { docsConfig } from '$lib/docs-config'
66
import favicon from '$lib/assets/logo.svg'
77
import { buildBreadcrumbs } from '$lib/docsNav'
@@ -22,7 +22,7 @@
2222

2323
<div class="bg-background relative flex min-h-screen flex-col">
2424
<Header config={docsConfig} {favicon} />
25-
<div class="flex flex-1 flex-col">
25+
<div class="flex flex-1 flex-col" use:enhanceCodeBlocks>
2626
{@render children?.()}
2727
</div>
2828
<Footer />
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script lang="ts">
2+
const { children } = $props()
3+
</script>
4+
5+
{@render children?.()}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script lang="ts">
2+
import { page } from '$app/state'
3+
import OG from '$lib/components/shared-link/OG.svelte'
4+
5+
const cardType: 'og' | 'twitter' = $derived(
6+
(page.url.searchParams.get('type') as 'og' | 'twitter') || 'og'
7+
)
8+
</script>
9+
10+
<OG type={cardType} url={page.url.origin} />

β€Ždocs/static/llms-full.txtβ€Ž

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# Svelte Virtual Chat - Complete Reference
2+
3+
> A high-performance virtual chat viewport for Svelte 5. Purpose-built for LLM conversations, support chat, and any message-based UI with follow-bottom, streaming stability, and history prepend with anchor preservation.
4+
5+
## Package Information
6+
7+
- Package: `@humanspeak/svelte-virtual-chat`
8+
- License: MIT
9+
- Requires: Svelte 5
10+
- Dependencies: esm-env (SSR detection only)
11+
- Maintained by: Humanspeak, Inc.
12+
- Homepage: https://virtualchat.svelte.page
13+
- Repository: https://github.com/humanspeak/svelte-virtual-chat
14+
15+
## Installation
16+
17+
```bash
18+
pnpm add @humanspeak/svelte-virtual-chat
19+
# or
20+
npm install @humanspeak/svelte-virtual-chat
21+
```
22+
23+
## SvelteVirtualChat Component Props
24+
25+
| Prop | Type | Default | Description |
26+
|------|------|---------|-------------|
27+
| messages | TMessage[] | Required | Array of messages in chronological order (oldest first) |
28+
| getMessageId | (msg: TMessage) => string | Required | Extract a unique, stable ID from a message |
29+
| renderMessage | Snippet<[TMessage, number]> | Required | Svelte 5 snippet that renders a single message |
30+
| estimatedMessageHeight | number | 72 | Height estimate in pixels for unmeasured messages |
31+
| followBottomThresholdPx | number | 48 | Distance from bottom to consider "at bottom" |
32+
| overscan | number | 6 | Extra messages rendered above/below the viewport |
33+
| onNeedHistory | () => void \| Promise<void> | - | Called when user scrolls near top |
34+
| onFollowBottomChange | (isFollowing: boolean) => void | - | Called when follow-bottom state changes |
35+
| onDebugInfo | (info: SvelteVirtualChatDebugInfo) => void | - | Called with live stats on every scroll/render |
36+
| containerClass | string | '' | CSS class for outermost container |
37+
| viewportClass | string | '' | CSS class for scrollable viewport |
38+
| debug | boolean | false | Reserved for future use |
39+
| testId | string | - | Base test ID for data-testid attributes |
40+
41+
## Basic Usage
42+
43+
```svelte
44+
<script lang="ts">
45+
import SvelteVirtualChat from '@humanspeak/svelte-virtual-chat'
46+
47+
type Message = { id: string; role: string; content: string }
48+
49+
let messages: Message[] = $state([
50+
{ id: '1', role: 'assistant', content: 'Hello!' },
51+
{ id: '2', role: 'user', content: 'Hi there.' }
52+
])
53+
</script>
54+
55+
<div class="h-[600px]">
56+
<SvelteVirtualChat
57+
{messages}
58+
getMessageId={(msg) => msg.id}
59+
estimatedMessageHeight={72}
60+
containerClass="h-full"
61+
viewportClass="h-full"
62+
>
63+
{#snippet renderMessage(message, index)}
64+
<div class="p-4 border-b">
65+
<strong>{message.role}</strong>
66+
<p>{message.content}</p>
67+
</div>
68+
{/snippet}
69+
</SvelteVirtualChat>
70+
</div>
71+
```
72+
73+
## Imperative API (via bind:this)
74+
75+
```typescript
76+
let chat: ReturnType<typeof SvelteVirtualChat>
77+
```
78+
79+
| Method | Signature | Description |
80+
|--------|-----------|-------------|
81+
| scrollToBottom | (options?: { smooth?: boolean }) => void | Scroll viewport to bottom |
82+
| scrollToMessage | (id: string, options?: { smooth?: boolean }) => void | Scroll to specific message |
83+
| isAtBottom | () => boolean | Check if following bottom |
84+
| getDebugInfo | () => SvelteVirtualChatDebugInfo | Get current state snapshot |
85+
86+
## SvelteVirtualChatDebugInfo
87+
88+
| Field | Type | Description |
89+
|-------|------|-------------|
90+
| totalMessages | number | Total messages in array |
91+
| renderedCount | number | Messages currently in DOM |
92+
| measuredCount | number | Messages with measured heights |
93+
| startIndex | number | First rendered index |
94+
| endIndex | number | Last rendered index |
95+
| totalHeight | number | Calculated content height (px) |
96+
| scrollTop | number | Current scroll position (px) |
97+
| viewportHeight | number | Viewport height (px) |
98+
| isFollowingBottom | boolean | Whether viewport is pinned to bottom |
99+
| averageHeight | number | Average measured message height (px) |
100+
101+
## How Virtualization Works
102+
103+
1. Height caching β€” each message height measured via ResizeObserver, cached by ID
104+
2. Visible range β€” on every scroll, calculates which messages fall within viewport + overscan
105+
3. Absolute positioning β€” only visible messages rendered via transform: translateY()
106+
4. Follow-bottom β€” when at bottom, new messages and height changes snap to scrollHeight
107+
5. Bottom gravity β€” when messages don't fill viewport, flex justify-content: flex-end pushes to bottom
108+
109+
With 10,000 messages, the DOM contains ~15-25 elements.
110+
111+
## LLM Streaming
112+
113+
As a message grows token by token, ResizeObserver detects height changes. Height corrections are batched per requestAnimationFrame. The viewport stays pinned to bottom automatically.
114+
115+
```typescript
116+
// Just mutate the message content β€” ResizeObserver handles the rest
117+
const msg = messages.find(m => m.id === streamingId)
118+
if (msg) msg.content += chunk
119+
```
120+
121+
Pair with @humanspeak/svelte-markdown for rich markdown rendering:
122+
123+
```svelte
124+
<SvelteMarkdown source={message.content} streaming={message.isStreaming ?? false} />
125+
```
126+
127+
## History Loading
128+
129+
Use onNeedHistory to load older messages when the user scrolls near the top:
130+
131+
```svelte
132+
<SvelteVirtualChat
133+
{messages}
134+
getMessageId={(msg) => msg.id}
135+
onNeedHistory={loadOlderMessages}
136+
...
137+
/>
138+
```
139+
140+
Prepend older messages: `messages = [...older, ...messages]`
141+
The component preserves scroll position during prepend operations.
142+
143+
## Scroll Behavior
144+
145+
Two states: following bottom and scrolled away.
146+
147+
- Following: viewport within followBottomThresholdPx of bottom. New messages auto-scroll.
148+
- Scrolled away: user scrolled up past threshold. New messages do not move viewport.
149+
150+
Use onFollowBottomChange to show "new messages" indicators:
151+
152+
```svelte
153+
<SvelteVirtualChat
154+
onFollowBottomChange={(following) => {
155+
isFollowing = following
156+
if (following) unreadCount = 0
157+
}}
158+
...
159+
/>
160+
```
161+
162+
## Height Constraint
163+
164+
The component MUST have a defined height. The parent needs a fixed or flex-derived height:
165+
166+
```svelte
167+
<div class="h-screen flex flex-col">
168+
<header>...</header>
169+
<div class="flex-1 min-h-0">
170+
<SvelteVirtualChat containerClass="h-full" viewportClass="h-full" ... />
171+
</div>
172+
</div>
173+
```
174+
175+
## Exported Types
176+
177+
- SvelteVirtualChatProps β€” component props type (generic over TMessage)
178+
- SvelteVirtualChatDebugInfo β€” debug info snapshot type
179+
- ScrollToBottomOptions β€” options for scrollToBottom
180+
- ScrollToMessageOptions β€” options for scrollToMessage
181+
- VisibleRange β€” visible range result type
182+
- ScrollAnchor β€” scroll anchor for history prepend
183+
184+
## Exported Utilities
185+
186+
- ChatHeightCache β€” reactive height cache class
187+
- captureScrollAnchor β€” capture scroll position before prepend
188+
- restoreScrollAnchor β€” restore scroll position after prepend
189+
190+
## Companion Libraries
191+
192+
- @humanspeak/svelte-markdown β€” Markdown renderer with LLM streaming mode (~1.6ms per update)
193+
- @humanspeak/svelte-virtual-list β€” General-purpose virtual list for non-chat use cases
194+
195+
## Documentation Links
196+
197+
- [Getting Started](https://virtualchat.svelte.page/docs/getting-started)
198+
- [SvelteVirtualChat API](https://virtualchat.svelte.page/docs/api/svelte-virtual-chat)
199+
- [Props Reference](https://virtualchat.svelte.page/docs/api/props)
200+
- [Imperative API](https://virtualchat.svelte.page/docs/api/imperative)
201+
- [LLM Streaming Guide](https://virtualchat.svelte.page/docs/guides/llm-streaming)
202+
- [History Loading Guide](https://virtualchat.svelte.page/docs/guides/history-loading)
203+
- [Scroll Behavior Guide](https://virtualchat.svelte.page/docs/guides/scroll-behavior)
204+
- [Interactive Examples](https://virtualchat.svelte.page/examples)

β€Ždocs/svelte.config.jsβ€Ž

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,23 @@ const config = {
4040
],
4141

4242
kit: {
43-
adapter: adapter()
43+
adapter: adapter(),
44+
csp: {
45+
mode: 'hash',
46+
directives: {
47+
'default-src': ['self'],
48+
'script-src': ['self', 'unsafe-inline', 'wasm-unsafe-eval'],
49+
'style-src': ['self', 'unsafe-inline'],
50+
'img-src': ['self', 'data:', 'https:'],
51+
'font-src': ['self', 'data:'],
52+
'worker-src': ['self', 'blob:'],
53+
'connect-src': ['self', 'https:'],
54+
'frame-ancestors': ['none'],
55+
'form-action': ['self'],
56+
'base-uri': ['self'],
57+
'upgrade-insecure-requests': true
58+
}
59+
}
4460
},
4561

4662
extensions: ['.svelte', '.svx']

β€Ždocs/vite.config.tsβ€Ž

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,19 @@ export default defineConfig({
99
fs: {
1010
allow: ['..']
1111
}
12+
},
13+
build: {
14+
rollupOptions: {
15+
output: {
16+
manualChunks(id) {
17+
if (id.includes('node_modules/shiki')) {
18+
return 'shiki'
19+
}
20+
if (id.includes('node_modules/marked')) {
21+
return 'marked'
22+
}
23+
}
24+
}
25+
}
1226
}
1327
})

0 commit comments

Comments
Β (0)