skrypt-ai 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/index.js +3 -3
- package/dist/cli.js +1 -1
- package/dist/commands/cron.js +0 -4
- package/dist/commands/generate/index.d.ts +3 -0
- package/dist/commands/generate/index.js +393 -0
- package/dist/commands/generate/scan.d.ts +41 -0
- package/dist/commands/generate/scan.js +256 -0
- package/dist/commands/generate/verify.d.ts +14 -0
- package/dist/commands/generate/verify.js +122 -0
- package/dist/commands/generate/write.d.ts +25 -0
- package/dist/commands/generate/write.js +120 -0
- package/dist/commands/import.js +4 -1
- package/dist/commands/llms-txt.js +6 -4
- package/dist/config/loader.d.ts +0 -1
- package/dist/config/loader.js +1 -1
- package/dist/generator/agents-md.d.ts +25 -0
- package/dist/generator/agents-md.js +122 -0
- package/dist/generator/index.d.ts +2 -0
- package/dist/generator/index.js +2 -0
- package/dist/generator/mdx-serializer.d.ts +11 -0
- package/dist/generator/mdx-serializer.js +135 -0
- package/dist/generator/organizer.d.ts +1 -16
- package/dist/generator/organizer.js +0 -38
- package/dist/generator/writer.js +5 -4
- package/dist/llm/proxy-client.d.ts +32 -0
- package/dist/llm/proxy-client.js +103 -0
- package/dist/scanner/csharp.d.ts +0 -4
- package/dist/scanner/csharp.js +9 -49
- package/dist/scanner/go.d.ts +0 -3
- package/dist/scanner/go.js +8 -35
- package/dist/scanner/java.d.ts +0 -4
- package/dist/scanner/java.js +9 -49
- package/dist/scanner/kotlin.d.ts +0 -3
- package/dist/scanner/kotlin.js +6 -33
- package/dist/scanner/php.d.ts +0 -10
- package/dist/scanner/php.js +11 -55
- package/dist/scanner/ruby.d.ts +0 -3
- package/dist/scanner/ruby.js +8 -38
- package/dist/scanner/rust.d.ts +0 -3
- package/dist/scanner/rust.js +10 -37
- package/dist/scanner/swift.d.ts +0 -3
- package/dist/scanner/swift.js +8 -35
- package/dist/scanner/utils.d.ts +41 -0
- package/dist/scanner/utils.js +97 -0
- package/dist/template/docs.json +5 -2
- package/dist/template/next.config.mjs +31 -0
- package/dist/template/package.json +5 -3
- package/dist/template/src/app/layout.tsx +13 -13
- package/dist/template/src/app/llms-full.md/route.ts +29 -0
- package/dist/template/src/app/llms.txt/route.ts +29 -0
- package/dist/template/src/app/md/[...slug]/route.ts +174 -0
- package/dist/template/src/app/reference/route.ts +22 -18
- package/dist/template/src/app/sitemap.ts +1 -1
- package/dist/template/src/components/ai-chat-impl.tsx +206 -0
- package/dist/template/src/components/ai-chat.tsx +20 -193
- package/dist/template/src/components/mdx/index.tsx +27 -4
- package/dist/template/src/lib/fonts.ts +135 -0
- package/dist/template/src/middleware.ts +101 -0
- package/dist/template/src/styles/globals.css +28 -20
- package/dist/utils/files.d.ts +0 -8
- package/dist/utils/files.js +0 -33
- package/package.json +1 -1
- package/dist/autofix/autofix.test.d.ts +0 -1
- package/dist/autofix/autofix.test.js +0 -487
- package/dist/commands/generate.d.ts +0 -9
- package/dist/commands/generate.js +0 -739
- package/dist/generator/generator.test.d.ts +0 -1
- package/dist/generator/generator.test.js +0 -259
- package/dist/generator/writer.test.d.ts +0 -1
- package/dist/generator/writer.test.js +0 -411
- package/dist/llm/llm.manual-test.d.ts +0 -1
- package/dist/llm/llm.manual-test.js +0 -112
- package/dist/llm/llm.mock-test.d.ts +0 -4
- package/dist/llm/llm.mock-test.js +0 -79
- package/dist/plugins/index.d.ts +0 -47
- package/dist/plugins/index.js +0 -181
- package/dist/scanner/content-type.test.d.ts +0 -1
- package/dist/scanner/content-type.test.js +0 -231
- package/dist/scanner/integration.test.d.ts +0 -4
- package/dist/scanner/integration.test.js +0 -180
- package/dist/scanner/scanner.test.d.ts +0 -1
- package/dist/scanner/scanner.test.js +0 -210
- package/dist/scanner/typescript.manual-test.d.ts +0 -1
- package/dist/scanner/typescript.manual-test.js +0 -112
- package/dist/template/src/app/docs/auth/page.mdx +0 -589
- package/dist/template/src/app/docs/autofix/page.mdx +0 -624
- package/dist/template/src/app/docs/cli/page.mdx +0 -217
- package/dist/template/src/app/docs/config/page.mdx +0 -428
- package/dist/template/src/app/docs/configuration/page.mdx +0 -86
- package/dist/template/src/app/docs/deployment/page.mdx +0 -112
- package/dist/template/src/app/docs/generator/generator.md +0 -504
- package/dist/template/src/app/docs/generator/organizer.md +0 -779
- package/dist/template/src/app/docs/generator/page.mdx +0 -613
- package/dist/template/src/app/docs/github/page.mdx +0 -502
- package/dist/template/src/app/docs/llm/anthropic-client.md +0 -549
- package/dist/template/src/app/docs/llm/index.md +0 -471
- package/dist/template/src/app/docs/llm/page.mdx +0 -428
- package/dist/template/src/app/docs/plugins/page.mdx +0 -1793
- package/dist/template/src/app/docs/pro/page.mdx +0 -121
- package/dist/template/src/app/docs/quickstart/page.mdx +0 -93
- package/dist/template/src/app/docs/scanner/content-type.md +0 -599
- package/dist/template/src/app/docs/scanner/index.md +0 -212
- package/dist/template/src/app/docs/scanner/page.mdx +0 -307
- package/dist/template/src/app/docs/scanner/python.md +0 -469
- package/dist/template/src/app/docs/scanner/python_parser.md +0 -1056
- package/dist/template/src/app/docs/scanner/rust.md +0 -325
- package/dist/template/src/app/docs/scanner/typescript.md +0 -201
- package/dist/template/src/app/icon.tsx +0 -29
- package/dist/utils/validation.d.ts +0 -1
- package/dist/utils/validation.js +0 -12
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import { Streamdown } from 'streamdown'
|
|
6
|
-
import { MessageSquare, X, Send, ExternalLink } from 'lucide-react'
|
|
7
|
-
|
|
8
|
-
interface Citation {
|
|
9
|
-
title: string
|
|
10
|
-
path: string
|
|
11
|
-
snippet: string
|
|
12
|
-
}
|
|
3
|
+
import dynamic from 'next/dynamic'
|
|
4
|
+
import { MessageSquare } from 'lucide-react'
|
|
13
5
|
|
|
14
6
|
interface AIChatProps {
|
|
15
7
|
projectName?: string
|
|
@@ -17,190 +9,25 @@ interface AIChatProps {
|
|
|
17
9
|
placeholder?: string
|
|
18
10
|
}
|
|
19
11
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
apiEndpoint = '/api/chat',
|
|
23
|
-
placeholder = 'Ask a question...',
|
|
24
|
-
}: AIChatProps) {
|
|
25
|
-
const [isOpen, setIsOpen] = useState(false)
|
|
26
|
-
const [citations, setCitations] = useState<Map<string, Citation[]>>(new Map())
|
|
27
|
-
const messagesEndRef = useRef<HTMLDivElement>(null)
|
|
28
|
-
|
|
29
|
-
const { messages, input, handleInputChange, handleSubmit, status, error } = useChat({
|
|
30
|
-
api: apiEndpoint,
|
|
31
|
-
onResponse(response: Response) {
|
|
32
|
-
// Extract citations from response header
|
|
33
|
-
const citationsHeader = response.headers.get('X-Citations')
|
|
34
|
-
if (citationsHeader) {
|
|
35
|
-
try {
|
|
36
|
-
const parsed = JSON.parse(atob(citationsHeader)) as Citation[]
|
|
37
|
-
// Associate citations with the next assistant message
|
|
38
|
-
setCitations(prev => {
|
|
39
|
-
const next = new Map(prev)
|
|
40
|
-
// Use a temporary key that will be replaced when the message arrives
|
|
41
|
-
next.set('_pending', parsed)
|
|
42
|
-
return next
|
|
43
|
-
})
|
|
44
|
-
} catch {
|
|
45
|
-
// Skip invalid citations
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
onFinish(message: { id: string }) {
|
|
50
|
-
// Move pending citations to the actual message ID
|
|
51
|
-
setCitations(prev => {
|
|
52
|
-
const next = new Map(prev)
|
|
53
|
-
const pending = next.get('_pending')
|
|
54
|
-
if (pending) {
|
|
55
|
-
next.delete('_pending')
|
|
56
|
-
next.set(message.id, pending)
|
|
57
|
-
}
|
|
58
|
-
return next
|
|
59
|
-
})
|
|
60
|
-
},
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
const isStreaming = status === 'streaming'
|
|
64
|
-
const isLoading = status === 'submitted'
|
|
65
|
-
|
|
66
|
-
useEffect(() => {
|
|
67
|
-
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
68
|
-
}, [messages, isStreaming])
|
|
69
|
-
|
|
70
|
-
if (!isOpen) {
|
|
71
|
-
return (
|
|
72
|
-
<button
|
|
73
|
-
onClick={() => setIsOpen(true)}
|
|
74
|
-
className="fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-[var(--color-text)] text-[var(--color-bg)] shadow-lg transition-transform hover:scale-105"
|
|
75
|
-
aria-label="Open AI chat"
|
|
76
|
-
>
|
|
77
|
-
<MessageSquare className="h-6 w-6" />
|
|
78
|
-
</button>
|
|
79
|
-
)
|
|
80
|
-
}
|
|
81
|
-
|
|
12
|
+
// Thin loading placeholder — just the FAB button (no heavy deps loaded yet)
|
|
13
|
+
function AIChatLoading() {
|
|
82
14
|
return (
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
<p className="text-sm font-medium text-[var(--color-text)]">Ask AI</p>
|
|
92
|
-
<p className="text-xs text-[var(--color-text-tertiary)]">About {projectName}</p>
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
<button
|
|
96
|
-
onClick={() => setIsOpen(false)}
|
|
97
|
-
className="rounded-lg p-1 text-[var(--color-text-tertiary)] transition-colors hover:bg-[var(--color-bg-secondary)] hover:text-[var(--color-text-secondary)]"
|
|
98
|
-
aria-label="Close chat"
|
|
99
|
-
>
|
|
100
|
-
<X className="h-5 w-5" />
|
|
101
|
-
</button>
|
|
102
|
-
</div>
|
|
103
|
-
|
|
104
|
-
{/* Messages */}
|
|
105
|
-
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
106
|
-
{messages.length === 0 && (
|
|
107
|
-
<div className="text-center text-sm text-[var(--color-text-tertiary)] py-8">
|
|
108
|
-
<p>Ask me anything about {projectName}.</p>
|
|
109
|
-
<p className="mt-2 text-xs">I'll search the docs and give you an answer with sources.</p>
|
|
110
|
-
</div>
|
|
111
|
-
)}
|
|
112
|
-
|
|
113
|
-
{messages.map((message) => (
|
|
114
|
-
<div
|
|
115
|
-
key={message.id}
|
|
116
|
-
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
|
117
|
-
>
|
|
118
|
-
<div
|
|
119
|
-
className={`max-w-[85%] rounded-2xl px-4 py-2 text-sm ${
|
|
120
|
-
message.role === 'user'
|
|
121
|
-
? 'bg-[var(--color-text)] text-[var(--color-bg)]'
|
|
122
|
-
: 'bg-[var(--color-bg-tertiary)] text-[var(--color-text)]'
|
|
123
|
-
}`}
|
|
124
|
-
>
|
|
125
|
-
{message.role === 'assistant' ? (
|
|
126
|
-
<div className="chat-markdown prose prose-sm max-w-none">
|
|
127
|
-
<Streamdown
|
|
128
|
-
shikiTheme={['github-light', 'github-dark']}
|
|
129
|
-
isAnimating={isStreaming && message.id === messages[messages.length - 1]?.id}
|
|
130
|
-
>
|
|
131
|
-
{message.content}
|
|
132
|
-
</Streamdown>
|
|
133
|
-
</div>
|
|
134
|
-
) : (
|
|
135
|
-
<p className="whitespace-pre-wrap">{message.content}</p>
|
|
136
|
-
)}
|
|
137
|
-
|
|
138
|
-
{/* Citations */}
|
|
139
|
-
{citations.get(message.id) && (
|
|
140
|
-
<div className="mt-3 border-t border-[var(--color-border)] pt-2">
|
|
141
|
-
<p className="text-xs font-medium text-[var(--color-text-tertiary)] mb-1">Sources:</p>
|
|
142
|
-
<div className="space-y-1">
|
|
143
|
-
{citations.get(message.id)!.map((citation, j) => (
|
|
144
|
-
<a
|
|
145
|
-
key={j}
|
|
146
|
-
href={citation.path}
|
|
147
|
-
className="flex items-center gap-1 text-xs text-[var(--color-primary)] hover:underline"
|
|
148
|
-
>
|
|
149
|
-
<ExternalLink className="h-3 w-3" />
|
|
150
|
-
{citation.title}
|
|
151
|
-
</a>
|
|
152
|
-
))}
|
|
153
|
-
</div>
|
|
154
|
-
</div>
|
|
155
|
-
)}
|
|
156
|
-
</div>
|
|
157
|
-
</div>
|
|
158
|
-
))}
|
|
159
|
-
|
|
160
|
-
{isLoading && (
|
|
161
|
-
<div className="flex justify-start">
|
|
162
|
-
<div className="flex items-center gap-2 rounded-2xl bg-[var(--color-bg-tertiary)] px-4 py-2 text-sm text-[var(--color-text-tertiary)]">
|
|
163
|
-
<span className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
|
164
|
-
Searching docs...
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
)}
|
|
168
|
-
|
|
169
|
-
{error && (
|
|
170
|
-
<div className="flex justify-start">
|
|
171
|
-
<div className="rounded-2xl bg-red-50 px-4 py-2 text-sm text-red-600">
|
|
172
|
-
Something went wrong. Please try again.
|
|
173
|
-
</div>
|
|
174
|
-
</div>
|
|
175
|
-
)}
|
|
15
|
+
<button
|
|
16
|
+
className="fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-[var(--color-text)] text-[var(--color-bg)] shadow-lg transition-transform hover:scale-105"
|
|
17
|
+
aria-label="Open AI chat"
|
|
18
|
+
>
|
|
19
|
+
<MessageSquare className="h-6 w-6" />
|
|
20
|
+
</button>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
176
23
|
|
|
177
|
-
|
|
178
|
-
|
|
24
|
+
// Dynamic import — @ai-sdk/react, streamdown, and the full chat UI
|
|
25
|
+
// are only downloaded when this component is rendered on a page
|
|
26
|
+
const AIChatImpl = dynamic(
|
|
27
|
+
() => import('./ai-chat-impl').then(m => ({ default: m.AIChat })),
|
|
28
|
+
{ ssr: false, loading: () => <AIChatLoading /> },
|
|
29
|
+
)
|
|
179
30
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
onSubmit={handleSubmit}
|
|
183
|
-
className="border-t border-[var(--color-border)] p-3"
|
|
184
|
-
>
|
|
185
|
-
<div className="flex items-center gap-2">
|
|
186
|
-
<input
|
|
187
|
-
type="text"
|
|
188
|
-
value={input}
|
|
189
|
-
onChange={handleInputChange}
|
|
190
|
-
placeholder={placeholder}
|
|
191
|
-
disabled={isStreaming || isLoading}
|
|
192
|
-
className="flex-1 rounded-xl border border-[var(--color-border)] bg-[var(--color-bg-secondary)] px-4 py-2 text-sm text-[var(--color-text)] outline-none transition-colors placeholder:text-[var(--color-text-tertiary)] focus:border-[var(--color-border-strong)]"
|
|
193
|
-
/>
|
|
194
|
-
<button
|
|
195
|
-
type="submit"
|
|
196
|
-
disabled={!input.trim() || isStreaming || isLoading}
|
|
197
|
-
className="flex h-10 w-10 items-center justify-center rounded-xl bg-[var(--color-text)] text-[var(--color-bg)] transition-colors hover:opacity-90 disabled:opacity-50"
|
|
198
|
-
aria-label="Send message"
|
|
199
|
-
>
|
|
200
|
-
<Send className="h-4 w-4" />
|
|
201
|
-
</button>
|
|
202
|
-
</div>
|
|
203
|
-
</form>
|
|
204
|
-
</div>
|
|
205
|
-
)
|
|
31
|
+
export function AIChat(props: AIChatProps) {
|
|
32
|
+
return <AIChatImpl {...props} />
|
|
206
33
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import dynamic from 'next/dynamic'
|
|
2
|
+
|
|
3
|
+
// Lightweight components — static imports (always in bundle)
|
|
1
4
|
export { Card, CardGroup } from './card'
|
|
2
5
|
export { Tabs, TabList, Tab, TabPanel } from './tabs'
|
|
3
6
|
export { CodeGroup } from './code-group'
|
|
@@ -6,9 +9,6 @@ export { HighlightedCode } from './highlighted-code'
|
|
|
6
9
|
export { Callout, Info, Warning, Success, Error, Tip, Note } from './callout'
|
|
7
10
|
export { Accordion, AccordionGroup } from './accordion'
|
|
8
11
|
export { Steps, Step } from './steps'
|
|
9
|
-
export { CodePlayground } from './code-playground'
|
|
10
|
-
export { PythonPlayground } from './python-playground'
|
|
11
|
-
export { GoPlayground } from './go-playground'
|
|
12
12
|
export { H1, H2, H3, H4 } from './heading'
|
|
13
13
|
export { ParamTable, Schema } from './param-table'
|
|
14
14
|
export { Changelog, ChangelogEntry, Change } from './changelog'
|
|
@@ -17,5 +17,28 @@ export { Tooltip } from './tooltip'
|
|
|
17
17
|
export { Frame } from './frame'
|
|
18
18
|
export { DarkImage } from './dark-image'
|
|
19
19
|
export { LinkPreview } from './link-preview'
|
|
20
|
-
export { Mermaid } from './mermaid'
|
|
21
20
|
export { Screenshot } from './screenshot'
|
|
21
|
+
|
|
22
|
+
// Heavy components — dynamic imports (code-split, loaded only when rendered)
|
|
23
|
+
// Each underlying component already has 'use client' and guards browser APIs
|
|
24
|
+
// behind useEffect/handlers, so SSR of the shell is safe.
|
|
25
|
+
|
|
26
|
+
// @codesandbox/sandpack-react (~5 MB)
|
|
27
|
+
export const CodePlayground = dynamic(
|
|
28
|
+
() => import('./code-playground').then(m => ({ default: m.CodePlayground })),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
// Pyodide loaded at runtime via CDN, but the component itself is still heavy
|
|
32
|
+
export const PythonPlayground = dynamic(
|
|
33
|
+
() => import('./python-playground').then(m => ({ default: m.PythonPlayground })),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
// Go playground (uses fetch to go.dev API)
|
|
37
|
+
export const GoPlayground = dynamic(
|
|
38
|
+
() => import('./go-playground').then(m => ({ default: m.GoPlayground })),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
// mermaid (~2.5 MB)
|
|
42
|
+
export const Mermaid = dynamic(
|
|
43
|
+
() => import('./mermaid').then(m => ({ default: m.Mermaid })),
|
|
44
|
+
)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font configuration for the documentation template.
|
|
3
|
+
*
|
|
4
|
+
* Supports customizable Google Fonts via docs.json:
|
|
5
|
+
*
|
|
6
|
+
* {
|
|
7
|
+
* "fonts": {
|
|
8
|
+
* "sans": "Inter", // default
|
|
9
|
+
* "mono": "JetBrains Mono" // default
|
|
10
|
+
* }
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* Also supports the legacy `theme.font` key for backward compatibility.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Curated font lists — only supported Google Fonts are allowed
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export const SUPPORTED_SANS_FONTS: Record<string, { weights: string }> = {
|
|
21
|
+
'Inter': { weights: '400;500;600;700;800' },
|
|
22
|
+
'Plus Jakarta Sans': { weights: '400;500;600;700;800' },
|
|
23
|
+
'DM Sans': { weights: '400;500;600;700' },
|
|
24
|
+
'Geist': { weights: '400;500;600;700;800' },
|
|
25
|
+
'Outfit': { weights: '400;500;600;700;800' },
|
|
26
|
+
'Space Grotesk': { weights: '400;500;600;700' },
|
|
27
|
+
'Manrope': { weights: '400;500;600;700;800' },
|
|
28
|
+
'Sora': { weights: '400;500;600;700;800' },
|
|
29
|
+
'Instrument Sans': { weights: '400;500;600;700' },
|
|
30
|
+
'Albert Sans': { weights: '400;500;600;700;800' },
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const SUPPORTED_MONO_FONTS: Record<string, { weights: string }> = {
|
|
34
|
+
'JetBrains Mono': { weights: '400;500;600;700' },
|
|
35
|
+
'Fira Code': { weights: '400;500;600;700' },
|
|
36
|
+
'Source Code Pro': { weights: '400;500;600;700' },
|
|
37
|
+
'IBM Plex Mono': { weights: '400;500;600;700' },
|
|
38
|
+
'Geist Mono': { weights: '400;500;600;700' },
|
|
39
|
+
'Space Mono': { weights: '400;700' },
|
|
40
|
+
'Roboto Mono': { weights: '400;500;600;700' },
|
|
41
|
+
'Inconsolata': { weights: '400;500;600;700' },
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const DEFAULT_SANS = 'Inter'
|
|
45
|
+
const DEFAULT_MONO = 'JetBrains Mono'
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Types
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
export interface ResolvedFont {
|
|
52
|
+
/** The font name as it appears in CSS font-family */
|
|
53
|
+
name: string
|
|
54
|
+
/** Whether this font needs to be loaded from Google Fonts */
|
|
55
|
+
isCustom: boolean
|
|
56
|
+
/** 'sans' or 'mono' */
|
|
57
|
+
type: 'sans' | 'mono'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ResolvedFonts {
|
|
61
|
+
sans: ResolvedFont
|
|
62
|
+
mono: ResolvedFont
|
|
63
|
+
/** True if at least one font needs Google Fonts loading */
|
|
64
|
+
needsGoogleFonts: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Resolution
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/** Sanitize a font name — strip anything that isn't alphanumeric, space, or hyphen */
|
|
72
|
+
function sanitize(name: string): string {
|
|
73
|
+
return name.replace(/[^a-zA-Z0-9\s-]/g, '').trim()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolve font configuration from docs.json.
|
|
78
|
+
*
|
|
79
|
+
* Priority:
|
|
80
|
+
* 1. `fonts.sans` / `fonts.mono` (new format)
|
|
81
|
+
* 2. `theme.font` (legacy — applied to sans only)
|
|
82
|
+
* 3. Defaults: Inter + JetBrains Mono
|
|
83
|
+
*
|
|
84
|
+
* Unrecognized font names fall back to defaults.
|
|
85
|
+
*/
|
|
86
|
+
export function resolveFonts(docsConfig: Record<string, unknown>): ResolvedFonts {
|
|
87
|
+
const fonts = docsConfig.fonts as { sans?: string; mono?: string } | undefined
|
|
88
|
+
const legacyFont = (docsConfig.theme as { font?: string } | undefined)?.font
|
|
89
|
+
|
|
90
|
+
// Resolve sans font
|
|
91
|
+
const rawSans = fonts?.sans || legacyFont || DEFAULT_SANS
|
|
92
|
+
const safeSans = sanitize(rawSans)
|
|
93
|
+
const sansIsSupported = safeSans in SUPPORTED_SANS_FONTS
|
|
94
|
+
const sansName = sansIsSupported ? safeSans : DEFAULT_SANS
|
|
95
|
+
const sansIsCustom = sansName !== DEFAULT_SANS
|
|
96
|
+
|
|
97
|
+
// Resolve mono font
|
|
98
|
+
const rawMono = fonts?.mono || DEFAULT_MONO
|
|
99
|
+
const safeMono = sanitize(rawMono)
|
|
100
|
+
const monoIsSupported = safeMono in SUPPORTED_MONO_FONTS
|
|
101
|
+
const monoName = monoIsSupported ? safeMono : DEFAULT_MONO
|
|
102
|
+
const monoIsCustom = monoName !== DEFAULT_MONO
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
sans: { name: sansName, isCustom: sansIsCustom, type: 'sans' },
|
|
106
|
+
mono: { name: monoName, isCustom: monoIsCustom, type: 'mono' },
|
|
107
|
+
needsGoogleFonts: sansIsCustom || monoIsCustom,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Google Fonts URL builder
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Build a single Google Fonts CSS URL that loads both sans and mono fonts.
|
|
117
|
+
* Only includes fonts that are non-default (custom).
|
|
118
|
+
*/
|
|
119
|
+
export function buildGoogleFontsUrl(sans: ResolvedFont, mono: ResolvedFont): string | null {
|
|
120
|
+
const families: string[] = []
|
|
121
|
+
|
|
122
|
+
if (sans.isCustom) {
|
|
123
|
+
const weights = SUPPORTED_SANS_FONTS[sans.name]?.weights || '400;500;600;700'
|
|
124
|
+
families.push(`family=${encodeURIComponent(sans.name)}:wght@${weights}`)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (mono.isCustom) {
|
|
128
|
+
const weights = SUPPORTED_MONO_FONTS[mono.name]?.weights || '400;500;600;700'
|
|
129
|
+
families.push(`family=${encodeURIComponent(mono.name)}:wght@${weights}`)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (families.length === 0) return null
|
|
133
|
+
|
|
134
|
+
return `https://fonts.googleapis.com/css2?${families.join('&')}&display=swap`
|
|
135
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Content negotiation middleware for AI agents.
|
|
5
|
+
*
|
|
6
|
+
* When agents send `Accept: text/markdown`, rewrite to the markdown route handler.
|
|
7
|
+
* Also handles `.md` URL suffix for explicit markdown requests.
|
|
8
|
+
* Adds Link + X-Llms-Txt discovery headers to all responses.
|
|
9
|
+
*/
|
|
10
|
+
export function middleware(request: NextRequest) {
|
|
11
|
+
const { pathname } = request.nextUrl
|
|
12
|
+
|
|
13
|
+
// Skip non-doc paths: API routes, static assets, internal Next.js paths
|
|
14
|
+
if (
|
|
15
|
+
pathname.startsWith('/api/') ||
|
|
16
|
+
pathname.startsWith('/_next/') ||
|
|
17
|
+
pathname.startsWith('/reference') ||
|
|
18
|
+
pathname === '/robots.txt' ||
|
|
19
|
+
pathname === '/sitemap.xml' ||
|
|
20
|
+
pathname === '/llms.txt' ||
|
|
21
|
+
pathname === '/llms-full.md' ||
|
|
22
|
+
pathname === '/favicon.ico' ||
|
|
23
|
+
/\.(ico|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|css|js|map)$/.test(pathname)
|
|
24
|
+
) {
|
|
25
|
+
return addDiscoveryHeaders(NextResponse.next(), request)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Handle .md URL suffix: /docs/getting-started.md → serve markdown for /docs/getting-started
|
|
29
|
+
if (pathname.endsWith('.md') && pathname.startsWith('/docs/')) {
|
|
30
|
+
const cleanPath = pathname.slice(0, -3) // strip .md
|
|
31
|
+
const slug = cleanPath.replace(/^\/docs\/?/, '')
|
|
32
|
+
if (slug) {
|
|
33
|
+
const url = request.nextUrl.clone()
|
|
34
|
+
url.pathname = `/md/${slug}`
|
|
35
|
+
return addDiscoveryHeaders(NextResponse.rewrite(url), request)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Content negotiation: detect Accept: text/markdown
|
|
40
|
+
const accept = request.headers.get('accept') || ''
|
|
41
|
+
if (prefersMarkdown(accept) && pathname.startsWith('/docs/')) {
|
|
42
|
+
const slug = pathname.replace(/^\/docs\/?/, '')
|
|
43
|
+
if (slug) {
|
|
44
|
+
const url = request.nextUrl.clone()
|
|
45
|
+
url.pathname = `/md/${slug}`
|
|
46
|
+
return addDiscoveryHeaders(NextResponse.rewrite(url), request)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return addDiscoveryHeaders(NextResponse.next(), request)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if the Accept header prefers text/markdown over text/html.
|
|
55
|
+
* Handles quality values: text/markdown;q=1.0, text/html;q=0.9
|
|
56
|
+
*/
|
|
57
|
+
function prefersMarkdown(accept: string): boolean {
|
|
58
|
+
if (!accept) return false
|
|
59
|
+
|
|
60
|
+
const lower = accept.toLowerCase()
|
|
61
|
+
|
|
62
|
+
// Quick check: if text/markdown appears at all
|
|
63
|
+
if (!lower.includes('text/markdown')) return false
|
|
64
|
+
|
|
65
|
+
// Parse quality values
|
|
66
|
+
let mdQuality = 0
|
|
67
|
+
let htmlQuality = 0
|
|
68
|
+
|
|
69
|
+
for (const part of lower.split(',')) {
|
|
70
|
+
const trimmed = part.trim()
|
|
71
|
+
const qMatch = trimmed.match(/;q=([0-9.]+)/)
|
|
72
|
+
const quality = qMatch ? Math.min(parseFloat(qMatch[1]) || 0, 1.0) : 1.0
|
|
73
|
+
|
|
74
|
+
if (trimmed.startsWith('text/markdown')) {
|
|
75
|
+
mdQuality = quality
|
|
76
|
+
} else if (trimmed.startsWith('text/html')) {
|
|
77
|
+
htmlQuality = quality
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return mdQuality > 0 && mdQuality >= htmlQuality
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Add agent discovery headers to every response.
|
|
86
|
+
* - Link: points agents to llms.txt
|
|
87
|
+
* - X-Llms-Txt: explicit pointer for agent tooling
|
|
88
|
+
*/
|
|
89
|
+
function addDiscoveryHeaders(response: NextResponse, request: NextRequest): NextResponse {
|
|
90
|
+
const origin = request.nextUrl.origin
|
|
91
|
+
response.headers.set('Link', `<${origin}/llms.txt>; rel="help", <${origin}/llms-full.md>; rel="help"`)
|
|
92
|
+
response.headers.set('X-Llms-Txt', `${origin}/llms.txt`)
|
|
93
|
+
return response
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const config = {
|
|
97
|
+
matcher: [
|
|
98
|
+
// Match all paths except static files
|
|
99
|
+
'/((?!_next/static|_next/image).*)',
|
|
100
|
+
],
|
|
101
|
+
}
|
|
@@ -58,10 +58,10 @@
|
|
|
58
58
|
--color-border: rgba(3, 7, 18, 0.1);
|
|
59
59
|
--color-border-strong: rgba(3, 7, 18, 0.15);
|
|
60
60
|
|
|
61
|
-
/* Code */
|
|
62
|
-
--color-code-bg: #
|
|
61
|
+
/* Code — high-contrast, matching Mintlify/Stripe/Vercel standards */
|
|
62
|
+
--color-code-bg: #fafafa;
|
|
63
63
|
--color-code-border: rgba(3, 7, 18, 0.1);
|
|
64
|
-
--color-code-text: #
|
|
64
|
+
--color-code-text: #1a1a2e;
|
|
65
65
|
|
|
66
66
|
/* Layout */
|
|
67
67
|
--sidebar-width: 264px;
|
|
@@ -80,14 +80,14 @@
|
|
|
80
80
|
--color-bg: #0f1117;
|
|
81
81
|
--color-bg-secondary: #1a1d27;
|
|
82
82
|
--color-bg-tertiary: #252833;
|
|
83
|
-
--color-text: #
|
|
84
|
-
--color-text-secondary: #
|
|
83
|
+
--color-text: #e4e5e9;
|
|
84
|
+
--color-text-secondary: #b4b8c1;
|
|
85
85
|
--color-text-tertiary: #6b7280;
|
|
86
86
|
--color-border: rgba(255, 255, 255, 0.07);
|
|
87
87
|
--color-border-strong: rgba(255, 255, 255, 0.12);
|
|
88
|
-
--color-code-bg: #
|
|
89
|
-
--color-code-border: rgba(255, 255, 255, 0.
|
|
90
|
-
--color-code-text: #
|
|
88
|
+
--color-code-bg: #0a0a0c;
|
|
89
|
+
--color-code-border: rgba(255, 255, 255, 0.08);
|
|
90
|
+
--color-code-text: #e8eaed;
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
@@ -95,14 +95,14 @@
|
|
|
95
95
|
--color-bg: #0f1117;
|
|
96
96
|
--color-bg-secondary: #1a1d27;
|
|
97
97
|
--color-bg-tertiary: #252833;
|
|
98
|
-
--color-text: #
|
|
99
|
-
--color-text-secondary: #
|
|
98
|
+
--color-text: #e4e5e9;
|
|
99
|
+
--color-text-secondary: #b4b8c1;
|
|
100
100
|
--color-text-tertiary: #6b7280;
|
|
101
101
|
--color-border: rgba(255, 255, 255, 0.07);
|
|
102
102
|
--color-border-strong: rgba(255, 255, 255, 0.12);
|
|
103
|
-
--color-code-bg: #
|
|
104
|
-
--color-code-border: rgba(255, 255, 255, 0.
|
|
105
|
-
--color-code-text: #
|
|
103
|
+
--color-code-bg: #0a0a0c;
|
|
104
|
+
--color-code-border: rgba(255, 255, 255, 0.08);
|
|
105
|
+
--color-code-text: #e8eaed;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/* Error page icon — dark mode overrides (avoids Tailwind dark: variant mismatch) */
|
|
@@ -205,33 +205,41 @@ pre {
|
|
|
205
205
|
background-color: var(--color-code-bg) !important;
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/* Ensure Shiki syntax tokens render at full opacity — never washed out */
|
|
209
|
+
.shiki code {
|
|
210
|
+
color: var(--color-code-text);
|
|
211
|
+
}
|
|
212
|
+
.shiki .line span {
|
|
213
|
+
opacity: 1 !important;
|
|
214
|
+
}
|
|
215
|
+
|
|
208
216
|
code {
|
|
209
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
217
|
+
font-family: var(--font-mono, ui-monospace), SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
210
218
|
font-size: 0.875em;
|
|
211
219
|
font-variant-ligatures: none;
|
|
212
220
|
}
|
|
213
221
|
|
|
214
|
-
/* Inline code — Mintlify style */
|
|
222
|
+
/* Inline code — high contrast, Mintlify/Stripe style */
|
|
215
223
|
:not(pre) > code {
|
|
216
224
|
background-color: var(--color-gray-100);
|
|
217
225
|
padding: 0.125rem 0.5rem;
|
|
218
226
|
border-radius: 0.375rem;
|
|
219
227
|
font-size: 0.875em;
|
|
220
228
|
line-height: 1.5;
|
|
221
|
-
color: var(--color-gray-
|
|
229
|
+
color: var(--color-gray-800);
|
|
222
230
|
overflow-wrap: break-word;
|
|
223
231
|
word-break: break-word;
|
|
224
232
|
}
|
|
225
233
|
|
|
226
234
|
:root.dark :not(pre) > code {
|
|
227
|
-
background-color: rgba(255, 255, 255, 0.
|
|
228
|
-
color:
|
|
235
|
+
background-color: rgba(255, 255, 255, 0.08);
|
|
236
|
+
color: #e0e2e6;
|
|
229
237
|
}
|
|
230
238
|
|
|
231
239
|
@media (prefers-color-scheme: dark) {
|
|
232
240
|
:root:not(.light) :not(pre) > code {
|
|
233
|
-
background-color: rgba(255, 255, 255, 0.
|
|
234
|
-
color:
|
|
241
|
+
background-color: rgba(255, 255, 255, 0.08);
|
|
242
|
+
color: #e0e2e6;
|
|
235
243
|
}
|
|
236
244
|
}
|
|
237
245
|
|
package/dist/utils/files.d.ts
CHANGED
|
@@ -7,11 +7,3 @@ export declare function findMdxFiles(dir: string): string[];
|
|
|
7
7
|
* Convert string to URL-safe slug
|
|
8
8
|
*/
|
|
9
9
|
export declare function slugify(str: string): string;
|
|
10
|
-
/**
|
|
11
|
-
* Simple YAML frontmatter parser — splits on --- markers.
|
|
12
|
-
* Returns parsed key-value data and remaining content body.
|
|
13
|
-
*/
|
|
14
|
-
export declare function parseFrontmatter(content: string): {
|
|
15
|
-
data: Record<string, unknown>;
|
|
16
|
-
content: string;
|
|
17
|
-
};
|
package/dist/utils/files.js
CHANGED
|
@@ -47,36 +47,3 @@ export function slugify(str) {
|
|
|
47
47
|
.replace(/[^a-z0-9]+/g, '-')
|
|
48
48
|
.replace(/^-|-$/g, '');
|
|
49
49
|
}
|
|
50
|
-
/**
|
|
51
|
-
* Simple YAML frontmatter parser — splits on --- markers.
|
|
52
|
-
* Returns parsed key-value data and remaining content body.
|
|
53
|
-
*/
|
|
54
|
-
export function parseFrontmatter(content) {
|
|
55
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
56
|
-
if (!match)
|
|
57
|
-
return { data: {}, content };
|
|
58
|
-
const yamlStr = match[1];
|
|
59
|
-
const body = match[2];
|
|
60
|
-
const data = {};
|
|
61
|
-
for (const line of yamlStr.split('\n')) {
|
|
62
|
-
const kvMatch = line.match(/^(\w[\w-]*)\s*:\s*(.*)$/);
|
|
63
|
-
if (!kvMatch)
|
|
64
|
-
continue;
|
|
65
|
-
const key = kvMatch[1];
|
|
66
|
-
let value = kvMatch[2].trim();
|
|
67
|
-
if (typeof value === 'string') {
|
|
68
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
69
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
70
|
-
value = value.slice(1, -1);
|
|
71
|
-
}
|
|
72
|
-
else if (value === 'true')
|
|
73
|
-
value = true;
|
|
74
|
-
else if (value === 'false')
|
|
75
|
-
value = false;
|
|
76
|
-
else if (/^\d+$/.test(value))
|
|
77
|
-
value = parseInt(value, 10);
|
|
78
|
-
}
|
|
79
|
-
data[key] = value;
|
|
80
|
-
}
|
|
81
|
-
return { data, content: body };
|
|
82
|
-
}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|