skrypt-ai 0.3.4 → 0.4.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/README.md +1 -1
- package/dist/auth/index.d.ts +0 -1
- package/dist/auth/index.js +3 -5
- package/dist/autofix/index.js +15 -3
- package/dist/cli.js +19 -4
- package/dist/commands/check-links.js +164 -174
- package/dist/commands/deploy.js +5 -2
- package/dist/commands/generate.js +206 -199
- package/dist/commands/i18n.js +3 -20
- package/dist/commands/init.js +47 -40
- package/dist/commands/lint.js +3 -20
- package/dist/commands/mcp.js +125 -122
- package/dist/commands/monitor.js +125 -108
- package/dist/commands/review-pr.js +1 -1
- package/dist/commands/sdk.js +1 -1
- package/dist/config/loader.js +21 -2
- package/dist/generator/organizer.d.ts +3 -0
- package/dist/generator/organizer.js +4 -9
- package/dist/generator/writer.js +2 -10
- package/dist/github/pr-comments.js +21 -8
- package/dist/plugins/index.js +1 -0
- package/dist/scanner/index.js +8 -2
- package/dist/template/docs.json +2 -1
- package/dist/template/next.config.mjs +2 -1
- package/dist/template/package.json +17 -15
- package/dist/template/public/favicon.svg +4 -0
- package/dist/template/public/search-index.json +1 -1
- package/dist/template/scripts/build-search-index.mjs +120 -25
- package/dist/template/src/app/api/chat/route.ts +11 -3
- package/dist/template/src/app/docs/README.md +28 -0
- package/dist/template/src/app/docs/[...slug]/page.tsx +139 -16
- package/dist/template/src/app/docs/auth/page.mdx +589 -0
- package/dist/template/src/app/docs/autofix/page.mdx +624 -0
- package/dist/template/src/app/docs/cli/page.mdx +217 -0
- package/dist/template/src/app/docs/config/page.mdx +428 -0
- package/dist/template/src/app/docs/configuration/page.mdx +86 -0
- package/dist/template/src/app/docs/deployment/page.mdx +112 -0
- package/dist/template/src/app/docs/error.tsx +20 -0
- package/dist/template/src/app/docs/generator/generator.md +504 -0
- package/dist/template/src/app/docs/generator/organizer.md +779 -0
- package/dist/template/src/app/docs/generator/page.mdx +613 -0
- package/dist/template/src/app/docs/github/page.mdx +502 -0
- package/dist/template/src/app/docs/llm/anthropic-client.md +549 -0
- package/dist/template/src/app/docs/llm/index.md +471 -0
- package/dist/template/src/app/docs/llm/page.mdx +428 -0
- package/dist/template/src/app/docs/llms-full.md +256 -0
- package/dist/template/src/app/docs/llms.txt +2971 -0
- package/dist/template/src/app/docs/not-found.tsx +23 -0
- package/dist/template/src/app/docs/page.mdx +0 -3
- package/dist/template/src/app/docs/plugins/page.mdx +1793 -0
- package/dist/template/src/app/docs/pro/page.mdx +121 -0
- package/dist/template/src/app/docs/quickstart/page.mdx +93 -0
- package/dist/template/src/app/docs/scanner/content-type.md +599 -0
- package/dist/template/src/app/docs/scanner/index.md +212 -0
- package/dist/template/src/app/docs/scanner/page.mdx +307 -0
- package/dist/template/src/app/docs/scanner/python.md +469 -0
- package/dist/template/src/app/docs/scanner/python_parser.md +1056 -0
- package/dist/template/src/app/docs/scanner/rust.md +325 -0
- package/dist/template/src/app/docs/scanner/typescript.md +201 -0
- package/dist/template/src/app/error.tsx +3 -3
- package/dist/template/src/app/icon.tsx +29 -0
- package/dist/template/src/app/layout.tsx +42 -0
- package/dist/template/src/app/not-found.tsx +35 -0
- package/dist/template/src/app/page.tsx +62 -28
- package/dist/template/src/components/ai-chat.tsx +26 -21
- package/dist/template/src/components/breadcrumbs.tsx +46 -2
- package/dist/template/src/components/copy-button.tsx +17 -3
- package/dist/template/src/components/docs-layout.tsx +142 -8
- package/dist/template/src/components/feedback.tsx +4 -2
- package/dist/template/src/components/footer.tsx +42 -0
- package/dist/template/src/components/header.tsx +29 -5
- package/dist/template/src/components/mdx/accordion.tsx +7 -6
- package/dist/template/src/components/mdx/card.tsx +19 -7
- package/dist/template/src/components/mdx/code-block.tsx +17 -3
- package/dist/template/src/components/mdx/code-group.tsx +65 -18
- package/dist/template/src/components/mdx/code-playground.tsx +3 -0
- package/dist/template/src/components/mdx/go-playground.tsx +3 -0
- package/dist/template/src/components/mdx/highlighted-code.tsx +171 -76
- package/dist/template/src/components/mdx/python-playground.tsx +2 -0
- package/dist/template/src/components/mdx/tabs.tsx +74 -6
- package/dist/template/src/components/page-header.tsx +19 -0
- package/dist/template/src/components/scroll-to-top.tsx +33 -0
- package/dist/template/src/components/search-dialog.tsx +206 -52
- package/dist/template/src/components/sidebar.tsx +136 -77
- package/dist/template/src/components/table-of-contents.tsx +23 -7
- package/dist/template/src/lib/highlight.ts +90 -31
- package/dist/template/src/lib/search.ts +14 -4
- package/dist/template/src/lib/theme-utils.ts +140 -0
- package/dist/template/src/styles/globals.css +307 -166
- package/dist/template/src/types/remark-gfm.d.ts +2 -0
- package/dist/utils/files.d.ts +9 -0
- package/dist/utils/files.js +33 -0
- package/dist/utils/validation.d.ts +4 -0
- package/dist/utils/validation.js +38 -0
- package/package.json +1 -4
|
@@ -13,8 +13,16 @@ export default function Home() {
|
|
|
13
13
|
const docsConfig = getDocsConfig()
|
|
14
14
|
const firstPage = docsConfig.navigation?.[0]?.pages?.[0]?.path || '/docs'
|
|
15
15
|
|
|
16
|
+
// Collect first 6 pages for quick links
|
|
17
|
+
const allPages: Array<{ title: string; path: string }> = []
|
|
18
|
+
for (const group of docsConfig.navigation || []) {
|
|
19
|
+
for (const page of group.pages || []) {
|
|
20
|
+
if (allPages.length < 6) allPages.push(page)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
return (
|
|
17
|
-
<main className="min-h-screen flex flex-col">
|
|
25
|
+
<main id="main-content" className="min-h-screen flex flex-col">
|
|
18
26
|
{/* Header */}
|
|
19
27
|
<header className="h-[var(--header-height)] border-b border-[var(--color-border)] flex items-center px-6">
|
|
20
28
|
<Link href="/" className="font-semibold text-[0.9375rem] tracking-tight text-[var(--color-text)] hover:no-underline">
|
|
@@ -24,17 +32,23 @@ export default function Home() {
|
|
|
24
32
|
|
|
25
33
|
{/* Hero */}
|
|
26
34
|
<div className="flex-1 flex flex-col items-center justify-center px-6 py-20">
|
|
27
|
-
<div className="max-w-2xl mx-auto text-center">
|
|
28
|
-
|
|
35
|
+
<div className="max-w-2xl mx-auto text-center relative">
|
|
36
|
+
{/* Gradient glow behind title */}
|
|
37
|
+
<div
|
|
38
|
+
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[400px] h-[200px] rounded-full blur-[100px] opacity-20 pointer-events-none"
|
|
39
|
+
style={{ background: `radial-gradient(ellipse, var(--color-primary), transparent 70%)` }}
|
|
40
|
+
/>
|
|
41
|
+
|
|
42
|
+
<h1 className="relative text-4xl sm:text-5xl font-bold tracking-tight text-[var(--color-text)] mb-4">
|
|
29
43
|
{docsConfig.name}
|
|
30
44
|
</h1>
|
|
31
|
-
<p className="text-lg text-[var(--color-text-secondary)] mb-8 max-w-lg mx-auto leading-relaxed">
|
|
45
|
+
<p className="relative text-lg text-[var(--color-text-secondary)] mb-8 max-w-lg mx-auto leading-relaxed">
|
|
32
46
|
{docsConfig.description}
|
|
33
47
|
</p>
|
|
34
|
-
<div className="flex items-center justify-center gap-3">
|
|
48
|
+
<div className="relative flex items-center justify-center gap-3">
|
|
35
49
|
<Link
|
|
36
50
|
href={firstPage}
|
|
37
|
-
className="inline-flex items-center gap-2 px-5 py-2.5 bg-[var(--color-
|
|
51
|
+
className="inline-flex items-center gap-2 px-5 py-2.5 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-lg font-medium text-[0.875rem] hover:opacity-90 hover:no-underline transition-opacity"
|
|
38
52
|
>
|
|
39
53
|
Get Started
|
|
40
54
|
<ArrowRight size={16} />
|
|
@@ -48,30 +62,50 @@ export default function Home() {
|
|
|
48
62
|
</div>
|
|
49
63
|
</div>
|
|
50
64
|
|
|
51
|
-
{/* Feature cards */}
|
|
52
|
-
<div className="
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
{/* Feature cards — show first page title + description from each group */}
|
|
66
|
+
<div className="flex flex-wrap justify-center gap-4 max-w-2xl mx-auto mt-16 w-full">
|
|
67
|
+
{(docsConfig.navigation || []).slice(0, 3).map((group: { group: string; pages: Array<{ title: string; path: string }> }, i: number) => {
|
|
68
|
+
const icons = [BookOpen, Code, Zap]
|
|
69
|
+
const Icon = icons[i % icons.length]
|
|
70
|
+
const firstGroupPage = group.pages?.[0]
|
|
71
|
+
return (
|
|
72
|
+
<Link
|
|
73
|
+
key={group.group}
|
|
74
|
+
href={firstGroupPage?.path || '/docs'}
|
|
75
|
+
className="block p-5 rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] hover:border-[var(--color-primary)] transition-colors hover:no-underline w-full sm:w-[calc(50%-0.5rem)] min-w-[200px]"
|
|
76
|
+
>
|
|
77
|
+
<Icon size={18} className="text-[var(--color-primary)] mb-3" />
|
|
78
|
+
<h3 className="text-[0.875rem] font-semibold text-[var(--color-text)] mb-1">{group.group}</h3>
|
|
79
|
+
<p className="text-[0.8125rem] text-[var(--color-text-tertiary)] leading-relaxed">
|
|
80
|
+
{firstGroupPage ? `Start with ${firstGroupPage.title}` : `${group.pages.length} pages`}
|
|
81
|
+
</p>
|
|
82
|
+
<p className="text-[0.75rem] text-[var(--color-text-tertiary)] mt-1">
|
|
83
|
+
{group.pages.length} {group.pages.length === 1 ? 'page' : 'pages'}
|
|
84
|
+
</p>
|
|
85
|
+
</Link>
|
|
86
|
+
)
|
|
87
|
+
})}
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{/* Quick Links */}
|
|
91
|
+
{allPages.length > 0 && (
|
|
92
|
+
<div className="max-w-2xl mx-auto mt-10 w-full">
|
|
93
|
+
<p className="text-[0.6875rem] font-semibold uppercase tracking-widest text-[var(--color-text-tertiary)] mb-3 text-center">
|
|
94
|
+
Quick Links
|
|
72
95
|
</p>
|
|
96
|
+
<div className="flex flex-wrap justify-center gap-x-6 gap-y-2">
|
|
97
|
+
{allPages.map((page) => (
|
|
98
|
+
<Link
|
|
99
|
+
key={page.path}
|
|
100
|
+
href={page.path}
|
|
101
|
+
className="text-[0.8125rem] text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] hover:no-underline transition-colors"
|
|
102
|
+
>
|
|
103
|
+
{page.title}
|
|
104
|
+
</Link>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
73
107
|
</div>
|
|
74
|
-
|
|
108
|
+
)}
|
|
75
109
|
</div>
|
|
76
110
|
</main>
|
|
77
111
|
)
|
|
@@ -4,6 +4,7 @@ import { useState, useRef, useEffect } from 'react'
|
|
|
4
4
|
import { MessageSquare, X, Send, Loader2, ExternalLink } from 'lucide-react'
|
|
5
5
|
|
|
6
6
|
interface Message {
|
|
7
|
+
id: string
|
|
7
8
|
role: 'user' | 'assistant'
|
|
8
9
|
content: string
|
|
9
10
|
citations?: Array<{ title: string; path: string; snippet: string }>
|
|
@@ -36,7 +37,7 @@ export function AIChat({
|
|
|
36
37
|
|
|
37
38
|
const userMessage = input.trim()
|
|
38
39
|
setInput('')
|
|
39
|
-
setMessages(prev => [...prev, { role: 'user', content: userMessage }])
|
|
40
|
+
setMessages(prev => [...prev, { id: Date.now().toString(), role: 'user', content: userMessage }])
|
|
40
41
|
setIsLoading(true)
|
|
41
42
|
|
|
42
43
|
try {
|
|
@@ -56,6 +57,7 @@ export function AIChat({
|
|
|
56
57
|
setMessages(prev => [
|
|
57
58
|
...prev,
|
|
58
59
|
{
|
|
60
|
+
id: Date.now().toString(),
|
|
59
61
|
role: 'assistant',
|
|
60
62
|
content: data.content,
|
|
61
63
|
citations: data.citations,
|
|
@@ -65,6 +67,7 @@ export function AIChat({
|
|
|
65
67
|
setMessages(prev => [
|
|
66
68
|
...prev,
|
|
67
69
|
{
|
|
70
|
+
id: Date.now().toString(),
|
|
68
71
|
role: 'assistant',
|
|
69
72
|
content: 'Sorry, I encountered an error. Please try again.',
|
|
70
73
|
},
|
|
@@ -78,7 +81,7 @@ export function AIChat({
|
|
|
78
81
|
return (
|
|
79
82
|
<button
|
|
80
83
|
onClick={() => setIsOpen(true)}
|
|
81
|
-
className="fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-
|
|
84
|
+
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"
|
|
82
85
|
aria-label="Open AI chat"
|
|
83
86
|
>
|
|
84
87
|
<MessageSquare className="h-6 w-6" />
|
|
@@ -87,21 +90,22 @@ export function AIChat({
|
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
return (
|
|
90
|
-
<div className="fixed bottom-6 right-6 z-50 flex h-[500px] w-[380px] flex-col overflow-hidden rounded-2xl border border-
|
|
93
|
+
<div className="fixed bottom-6 right-6 z-50 flex h-[500px] w-[380px] flex-col overflow-hidden rounded-2xl border border-[var(--color-border)] bg-[var(--color-bg)] shadow-2xl">
|
|
91
94
|
{/* Header */}
|
|
92
|
-
<div className="flex items-center justify-between border-b border-
|
|
95
|
+
<div className="flex items-center justify-between border-b border-[var(--color-border)] px-4 py-3">
|
|
93
96
|
<div className="flex items-center gap-2">
|
|
94
|
-
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-
|
|
95
|
-
<MessageSquare className="h-4 w-4 text-
|
|
97
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-[var(--color-text)]">
|
|
98
|
+
<MessageSquare className="h-4 w-4 text-[var(--color-bg)]" />
|
|
96
99
|
</div>
|
|
97
100
|
<div>
|
|
98
|
-
<p className="text-sm font-medium text-
|
|
99
|
-
<p className="text-xs text-
|
|
101
|
+
<p className="text-sm font-medium text-[var(--color-text)]">Ask AI</p>
|
|
102
|
+
<p className="text-xs text-[var(--color-text-tertiary)]">About {projectName}</p>
|
|
100
103
|
</div>
|
|
101
104
|
</div>
|
|
102
105
|
<button
|
|
103
106
|
onClick={() => setIsOpen(false)}
|
|
104
|
-
className="rounded-lg p-1 text-
|
|
107
|
+
className="rounded-lg p-1 text-[var(--color-text-tertiary)] transition-colors hover:bg-[var(--color-bg-secondary)] hover:text-[var(--color-text-secondary)]"
|
|
108
|
+
aria-label="Close chat"
|
|
105
109
|
>
|
|
106
110
|
<X className="h-5 w-5" />
|
|
107
111
|
</button>
|
|
@@ -110,36 +114,36 @@ export function AIChat({
|
|
|
110
114
|
{/* Messages */}
|
|
111
115
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
112
116
|
{messages.length === 0 && (
|
|
113
|
-
<div className="text-center text-sm text-
|
|
117
|
+
<div className="text-center text-sm text-[var(--color-text-tertiary)] py-8">
|
|
114
118
|
<p>Ask me anything about {projectName}.</p>
|
|
115
119
|
<p className="mt-2 text-xs">I'll search the docs and give you an answer with sources.</p>
|
|
116
120
|
</div>
|
|
117
121
|
)}
|
|
118
122
|
|
|
119
|
-
{messages.map((message
|
|
123
|
+
{messages.map((message) => (
|
|
120
124
|
<div
|
|
121
|
-
key={
|
|
125
|
+
key={message.id}
|
|
122
126
|
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
|
123
127
|
>
|
|
124
128
|
<div
|
|
125
129
|
className={`max-w-[85%] rounded-2xl px-4 py-2 text-sm ${
|
|
126
130
|
message.role === 'user'
|
|
127
|
-
? 'bg-
|
|
128
|
-
: 'bg-
|
|
131
|
+
? 'bg-[var(--color-text)] text-[var(--color-bg)]'
|
|
132
|
+
: 'bg-[var(--color-bg-tertiary)] text-[var(--color-text)]'
|
|
129
133
|
}`}
|
|
130
134
|
>
|
|
131
135
|
<p className="whitespace-pre-wrap">{message.content}</p>
|
|
132
136
|
|
|
133
137
|
{/* Citations */}
|
|
134
138
|
{message.citations && message.citations.length > 0 && (
|
|
135
|
-
<div className="mt-3 border-t border-
|
|
136
|
-
<p className="text-xs font-medium text-
|
|
139
|
+
<div className="mt-3 border-t border-[var(--color-border)] pt-2">
|
|
140
|
+
<p className="text-xs font-medium text-[var(--color-text-tertiary)] mb-1">Sources:</p>
|
|
137
141
|
<div className="space-y-1">
|
|
138
142
|
{message.citations.map((citation, j) => (
|
|
139
143
|
<a
|
|
140
144
|
key={j}
|
|
141
145
|
href={citation.path}
|
|
142
|
-
className="flex items-center gap-1 text-xs text-
|
|
146
|
+
className="flex items-center gap-1 text-xs text-[var(--color-primary)] hover:underline"
|
|
143
147
|
>
|
|
144
148
|
<ExternalLink className="h-3 w-3" />
|
|
145
149
|
{citation.title}
|
|
@@ -154,7 +158,7 @@ export function AIChat({
|
|
|
154
158
|
|
|
155
159
|
{isLoading && (
|
|
156
160
|
<div className="flex justify-start">
|
|
157
|
-
<div className="flex items-center gap-2 rounded-2xl bg-
|
|
161
|
+
<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)]">
|
|
158
162
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
159
163
|
Searching docs...
|
|
160
164
|
</div>
|
|
@@ -167,7 +171,7 @@ export function AIChat({
|
|
|
167
171
|
{/* Input */}
|
|
168
172
|
<form
|
|
169
173
|
onSubmit={handleSubmit}
|
|
170
|
-
className="border-t border-
|
|
174
|
+
className="border-t border-[var(--color-border)] p-3"
|
|
171
175
|
>
|
|
172
176
|
<div className="flex items-center gap-2">
|
|
173
177
|
<input
|
|
@@ -176,12 +180,13 @@ export function AIChat({
|
|
|
176
180
|
onChange={e => setInput(e.target.value)}
|
|
177
181
|
placeholder={placeholder}
|
|
178
182
|
disabled={isLoading}
|
|
179
|
-
className="flex-1 rounded-xl border border-
|
|
183
|
+
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)]"
|
|
180
184
|
/>
|
|
181
185
|
<button
|
|
182
186
|
type="submit"
|
|
183
187
|
disabled={!input.trim() || isLoading}
|
|
184
|
-
className="flex h-10 w-10 items-center justify-center rounded-xl bg-
|
|
188
|
+
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"
|
|
189
|
+
aria-label="Send message"
|
|
185
190
|
>
|
|
186
191
|
<Send className="h-4 w-4" />
|
|
187
192
|
</button>
|
|
@@ -4,15 +4,59 @@ import Link from 'next/link'
|
|
|
4
4
|
import { usePathname } from 'next/navigation'
|
|
5
5
|
import { ChevronRight } from 'lucide-react'
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
interface NavPage { title: string; path: string; pages?: NavPage[] }
|
|
8
|
+
interface NavGroup { group: string; pages: (NavPage | NavGroup)[] }
|
|
9
|
+
interface DocsConfig { navigation: NavGroup[] }
|
|
10
|
+
|
|
11
|
+
function isNavGroup(item: NavPage | NavGroup): item is NavGroup {
|
|
12
|
+
return 'group' in item && !('path' in item)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildTitleMap(docsConfig?: DocsConfig): Map<string, string> {
|
|
16
|
+
const map = new Map<string, string>()
|
|
17
|
+
if (!docsConfig) return map
|
|
18
|
+
|
|
19
|
+
for (const group of docsConfig.navigation) {
|
|
20
|
+
for (const item of group.pages) {
|
|
21
|
+
if (isNavGroup(item)) {
|
|
22
|
+
fillTitleMap(item.pages, map)
|
|
23
|
+
} else {
|
|
24
|
+
map.set(item.path, item.title)
|
|
25
|
+
if (item.pages) fillTitleMap(item.pages, map)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return map
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function fillTitleMap(pages: (NavPage | NavGroup)[], map: Map<string, string>) {
|
|
33
|
+
for (const item of pages) {
|
|
34
|
+
if (isNavGroup(item)) {
|
|
35
|
+
fillTitleMap(item.pages, map)
|
|
36
|
+
} else {
|
|
37
|
+
map.set(item.path, item.title)
|
|
38
|
+
if (item.pages) fillTitleMap(item.pages, map)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface BreadcrumbsProps {
|
|
44
|
+
docsConfig?: DocsConfig
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function Breadcrumbs({ docsConfig }: BreadcrumbsProps) {
|
|
8
48
|
const pathname = usePathname()
|
|
9
49
|
const segments = pathname.split('/').filter(Boolean)
|
|
10
50
|
|
|
11
51
|
if (segments.length <= 1) return null
|
|
12
52
|
|
|
53
|
+
const titleMap = buildTitleMap(docsConfig)
|
|
54
|
+
|
|
13
55
|
const crumbs = segments.map((segment, index) => {
|
|
14
56
|
const href = '/' + segments.slice(0, index + 1).join('/')
|
|
15
|
-
|
|
57
|
+
// Look up the actual page title from navigation config
|
|
58
|
+
const configTitle = titleMap.get(href)
|
|
59
|
+
const label = configTitle || segment
|
|
16
60
|
.replace(/-/g, ' ')
|
|
17
61
|
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
18
62
|
|
|
@@ -12,9 +12,23 @@ export function CopyButton({ text, className = '' }: CopyButtonProps) {
|
|
|
12
12
|
const [copied, setCopied] = useState(false)
|
|
13
13
|
|
|
14
14
|
async function handleCopy() {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
try {
|
|
16
|
+
await navigator.clipboard.writeText(text)
|
|
17
|
+
setCopied(true)
|
|
18
|
+
setTimeout(() => setCopied(false), 2000)
|
|
19
|
+
} catch {
|
|
20
|
+
// Fallback for insecure contexts
|
|
21
|
+
const textarea = document.createElement('textarea')
|
|
22
|
+
textarea.value = text
|
|
23
|
+
textarea.style.position = 'fixed'
|
|
24
|
+
textarea.style.opacity = '0'
|
|
25
|
+
document.body.appendChild(textarea)
|
|
26
|
+
textarea.select()
|
|
27
|
+
document.execCommand('copy')
|
|
28
|
+
document.body.removeChild(textarea)
|
|
29
|
+
setCopied(true)
|
|
30
|
+
setTimeout(() => setCopied(false), 2000)
|
|
31
|
+
}
|
|
18
32
|
}
|
|
19
33
|
|
|
20
34
|
return (
|
|
@@ -1,21 +1,36 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react'
|
|
3
|
+
import { useState, useEffect } from 'react'
|
|
4
4
|
import { usePathname } from 'next/navigation'
|
|
5
5
|
import Link from 'next/link'
|
|
6
|
-
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
|
6
|
+
import { ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react'
|
|
7
7
|
import { Sidebar } from './sidebar'
|
|
8
8
|
import { Header } from './header'
|
|
9
9
|
import { TableOfContents } from './table-of-contents'
|
|
10
10
|
import { Breadcrumbs } from './breadcrumbs'
|
|
11
|
+
import { PageHeader } from './page-header'
|
|
12
|
+
import { Feedback } from './feedback'
|
|
13
|
+
import { EditLink } from './edit-link'
|
|
14
|
+
import { Footer } from './footer'
|
|
15
|
+
import { ScrollToTop } from './scroll-to-top'
|
|
11
16
|
|
|
12
17
|
interface DocsConfig {
|
|
13
18
|
name?: string
|
|
14
19
|
headerLinks?: Array<{ title: string; path: string }>
|
|
20
|
+
logo?: string
|
|
15
21
|
navigation: Array<{
|
|
16
22
|
group: string
|
|
17
|
-
|
|
23
|
+
icon?: string
|
|
24
|
+
pages: Array<{ title: string; path: string; description?: string }>
|
|
18
25
|
}>
|
|
26
|
+
footer?: {
|
|
27
|
+
links?: Array<{ title: string; url: string }>
|
|
28
|
+
}
|
|
29
|
+
editLink?: {
|
|
30
|
+
repoUrl?: string
|
|
31
|
+
branch?: string
|
|
32
|
+
docsPath?: string
|
|
33
|
+
}
|
|
19
34
|
}
|
|
20
35
|
|
|
21
36
|
function getAllPages(config: DocsConfig): Array<{ title: string; path: string }> {
|
|
@@ -70,34 +85,153 @@ function PrevNextNav({ docsConfig }: { docsConfig: DocsConfig }) {
|
|
|
70
85
|
)
|
|
71
86
|
}
|
|
72
87
|
|
|
73
|
-
|
|
88
|
+
/** Mobile TOC — collapsible "On this page" for screens < xl */
|
|
89
|
+
function MobileTOC() {
|
|
90
|
+
const [headings, setHeadings] = useState<Array<{ id: string; text: string; level: number }>>([])
|
|
91
|
+
const [open, setOpen] = useState(false)
|
|
92
|
+
const pathname = usePathname()
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const article = document.querySelector('article')
|
|
96
|
+
if (!article) return
|
|
97
|
+
|
|
98
|
+
const elements = article.querySelectorAll('h2, h3')
|
|
99
|
+
const items: Array<{ id: string; text: string; level: number }> = []
|
|
100
|
+
|
|
101
|
+
const genericHeadings = new Set([
|
|
102
|
+
'parameters', 'returns', 'return value', 'return type',
|
|
103
|
+
'returned validator function', 'requirements',
|
|
104
|
+
'when results are returned', 'when each value is returned',
|
|
105
|
+
'validationresult shape',
|
|
106
|
+
])
|
|
107
|
+
|
|
108
|
+
elements.forEach((el) => {
|
|
109
|
+
const text = el.textContent || ''
|
|
110
|
+
const normalized = text.toLowerCase().trim()
|
|
111
|
+
if (genericHeadings.has(normalized)) return
|
|
112
|
+
|
|
113
|
+
const id = el.id || normalized.replace(/\s+/g, '-')
|
|
114
|
+
if (!el.id) el.id = id
|
|
115
|
+
items.push({ id, text, level: parseInt(el.tagName[1]) })
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
setHeadings(items)
|
|
119
|
+
setOpen(false)
|
|
120
|
+
}, [pathname])
|
|
121
|
+
|
|
122
|
+
if (headings.length === 0) return null
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div className="lg:hidden mb-6 border border-[var(--color-border)] rounded-lg">
|
|
126
|
+
<button
|
|
127
|
+
onClick={() => setOpen(!open)}
|
|
128
|
+
className="flex items-center justify-between w-full px-4 py-3 text-[0.8125rem] font-medium text-[var(--color-text)]"
|
|
129
|
+
aria-expanded={open}
|
|
130
|
+
>
|
|
131
|
+
On this page
|
|
132
|
+
<ChevronDown size={14} className={`text-[var(--color-text-tertiary)] transition-transform duration-150 ${open ? 'rotate-180' : ''}`} />
|
|
133
|
+
</button>
|
|
134
|
+
{open && (
|
|
135
|
+
<div className="px-4 pb-3 border-t border-[var(--color-border)]">
|
|
136
|
+
<ul className="space-y-1 mt-2">
|
|
137
|
+
{headings.map((heading, i) => (
|
|
138
|
+
<li key={`${heading.id}-${i}`}>
|
|
139
|
+
<a
|
|
140
|
+
href={`#${heading.id}`}
|
|
141
|
+
onClick={() => setOpen(false)}
|
|
142
|
+
className={`block py-1 text-[0.8125rem] text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] hover:no-underline ${
|
|
143
|
+
heading.level === 3 ? 'pl-4' : ''
|
|
144
|
+
}`}
|
|
145
|
+
>
|
|
146
|
+
{heading.text}
|
|
147
|
+
</a>
|
|
148
|
+
</li>
|
|
149
|
+
))}
|
|
150
|
+
</ul>
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function getPageInfo(docsConfig: DocsConfig, pathname: string): { title?: string; description?: string } {
|
|
158
|
+
for (const group of docsConfig.navigation) {
|
|
159
|
+
for (const page of group.pages) {
|
|
160
|
+
if (page.path === pathname) {
|
|
161
|
+
return { title: page.title, description: page.description }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return {}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function DocsLayout({
|
|
169
|
+
children,
|
|
170
|
+
docsConfig,
|
|
171
|
+
pageTitle,
|
|
172
|
+
pageDescription,
|
|
173
|
+
}: {
|
|
174
|
+
children: React.ReactNode
|
|
175
|
+
docsConfig: DocsConfig
|
|
176
|
+
pageTitle?: string
|
|
177
|
+
pageDescription?: string
|
|
178
|
+
}) {
|
|
74
179
|
const [menuOpen, setMenuOpen] = useState(false)
|
|
180
|
+
const [frontmatterDescription, setFrontmatterDescription] = useState<string | undefined>()
|
|
181
|
+
const pathname = usePathname()
|
|
182
|
+
|
|
183
|
+
// Read frontmatter description passed from the server component via data attribute
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
const el = document.querySelector('[data-page-description]')
|
|
186
|
+
setFrontmatterDescription(el?.getAttribute('data-page-description') ?? undefined)
|
|
187
|
+
}, [pathname])
|
|
188
|
+
|
|
189
|
+
// Resolve title from props, falling back to docs.json navigation lookup
|
|
190
|
+
const pageInfo = getPageInfo(docsConfig, pathname)
|
|
191
|
+
const resolvedTitle = pageTitle ?? pageInfo.title
|
|
192
|
+
const resolvedDescription = pageDescription ?? frontmatterDescription ?? pageInfo.description
|
|
75
193
|
|
|
76
194
|
return (
|
|
77
|
-
<div className="min-h-screen">
|
|
195
|
+
<div className="min-h-screen flex flex-col">
|
|
78
196
|
<Header
|
|
79
197
|
onMenuToggle={() => setMenuOpen(!menuOpen)}
|
|
80
198
|
menuOpen={menuOpen}
|
|
81
199
|
siteName={docsConfig.name}
|
|
82
200
|
navLinks={docsConfig.headerLinks}
|
|
201
|
+
logo={docsConfig.logo}
|
|
83
202
|
/>
|
|
84
|
-
<div className="flex">
|
|
203
|
+
<div className="flex flex-1">
|
|
85
204
|
<Sidebar
|
|
86
205
|
open={menuOpen}
|
|
87
206
|
onClose={() => setMenuOpen(false)}
|
|
88
207
|
docsConfig={docsConfig}
|
|
89
208
|
/>
|
|
90
|
-
<main className="flex-1 min-w-0 px-6 md:px-10 py-8
|
|
209
|
+
<main id="main-content" className="flex-1 min-w-0 px-6 md:px-10 py-8 lg:ml-[var(--sidebar-width)] lg:mr-[var(--toc-width)]">
|
|
91
210
|
<div className="max-w-[var(--content-max-width)] mx-auto">
|
|
92
|
-
<Breadcrumbs />
|
|
211
|
+
<Breadcrumbs docsConfig={docsConfig} />
|
|
212
|
+
{resolvedTitle && (
|
|
213
|
+
<PageHeader title={resolvedTitle} description={resolvedDescription} />
|
|
214
|
+
)}
|
|
215
|
+
<MobileTOC />
|
|
93
216
|
<article className="prose">
|
|
94
217
|
{children}
|
|
95
218
|
</article>
|
|
219
|
+
{/* Feedback and Edit Link */}
|
|
220
|
+
<div className="mt-10 pt-6 border-t border-[var(--color-border)] flex items-center justify-between flex-wrap gap-4">
|
|
221
|
+
<Feedback />
|
|
222
|
+
<EditLink
|
|
223
|
+
repoUrl={docsConfig.editLink?.repoUrl}
|
|
224
|
+
branch={docsConfig.editLink?.branch}
|
|
225
|
+
docsPath={docsConfig.editLink?.docsPath}
|
|
226
|
+
/>
|
|
227
|
+
</div>
|
|
96
228
|
<PrevNextNav docsConfig={docsConfig} />
|
|
97
229
|
</div>
|
|
98
230
|
</main>
|
|
99
231
|
<TableOfContents />
|
|
100
232
|
</div>
|
|
233
|
+
<Footer docsConfig={docsConfig} />
|
|
234
|
+
<ScrollToTop />
|
|
101
235
|
</div>
|
|
102
236
|
)
|
|
103
237
|
}
|
|
@@ -28,9 +28,10 @@ export function Feedback() {
|
|
|
28
28
|
<div className="flex items-center gap-1">
|
|
29
29
|
<button
|
|
30
30
|
onClick={() => handleFeedback(true)}
|
|
31
|
+
aria-label="Helpful"
|
|
31
32
|
className={`p-1.5 rounded-md transition-colors ${
|
|
32
33
|
selection === 'yes'
|
|
33
|
-
? 'bg-emerald-100 text-emerald-600'
|
|
34
|
+
? 'bg-emerald-100 text-emerald-600 feedback-positive'
|
|
34
35
|
: 'text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)]'
|
|
35
36
|
}`}
|
|
36
37
|
>
|
|
@@ -38,9 +39,10 @@ export function Feedback() {
|
|
|
38
39
|
</button>
|
|
39
40
|
<button
|
|
40
41
|
onClick={() => handleFeedback(false)}
|
|
42
|
+
aria-label="Not helpful"
|
|
41
43
|
className={`p-1.5 rounded-md transition-colors ${
|
|
42
44
|
selection === 'no'
|
|
43
|
-
? 'bg-red-100 text-red-600'
|
|
45
|
+
? 'bg-red-100 text-red-600 feedback-negative'
|
|
44
46
|
: 'text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)]'
|
|
45
47
|
}`}
|
|
46
48
|
>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
interface FooterProps {
|
|
2
|
+
docsConfig: {
|
|
3
|
+
footer?: {
|
|
4
|
+
links?: Array<{ title: string; url: string }>
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Footer({ docsConfig }: FooterProps) {
|
|
10
|
+
const links = docsConfig.footer?.links || []
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<footer className="border-t border-[var(--color-border)] lg:ml-[var(--sidebar-width)]">
|
|
14
|
+
<div className="max-w-[var(--content-max-width)] mx-auto px-6 md:px-10 py-6 flex items-center justify-between flex-wrap gap-4">
|
|
15
|
+
<div className="flex items-center gap-6">
|
|
16
|
+
{links.map((link) => (
|
|
17
|
+
<a
|
|
18
|
+
key={link.url}
|
|
19
|
+
href={link.url}
|
|
20
|
+
target="_blank"
|
|
21
|
+
rel="noopener noreferrer"
|
|
22
|
+
className="text-[0.8125rem] text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] transition-colors"
|
|
23
|
+
>
|
|
24
|
+
{link.title}
|
|
25
|
+
</a>
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
<span className="text-[0.75rem] text-[var(--color-text-tertiary)]">
|
|
29
|
+
Built with{' '}
|
|
30
|
+
<a
|
|
31
|
+
href="https://skrypt.sh"
|
|
32
|
+
target="_blank"
|
|
33
|
+
rel="noopener noreferrer"
|
|
34
|
+
className="text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors"
|
|
35
|
+
>
|
|
36
|
+
Skrypt
|
|
37
|
+
</a>
|
|
38
|
+
</span>
|
|
39
|
+
</div>
|
|
40
|
+
</footer>
|
|
41
|
+
)
|
|
42
|
+
}
|