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.
Files changed (95) hide show
  1. package/README.md +1 -1
  2. package/dist/auth/index.d.ts +0 -1
  3. package/dist/auth/index.js +3 -5
  4. package/dist/autofix/index.js +15 -3
  5. package/dist/cli.js +19 -4
  6. package/dist/commands/check-links.js +164 -174
  7. package/dist/commands/deploy.js +5 -2
  8. package/dist/commands/generate.js +206 -199
  9. package/dist/commands/i18n.js +3 -20
  10. package/dist/commands/init.js +47 -40
  11. package/dist/commands/lint.js +3 -20
  12. package/dist/commands/mcp.js +125 -122
  13. package/dist/commands/monitor.js +125 -108
  14. package/dist/commands/review-pr.js +1 -1
  15. package/dist/commands/sdk.js +1 -1
  16. package/dist/config/loader.js +21 -2
  17. package/dist/generator/organizer.d.ts +3 -0
  18. package/dist/generator/organizer.js +4 -9
  19. package/dist/generator/writer.js +2 -10
  20. package/dist/github/pr-comments.js +21 -8
  21. package/dist/plugins/index.js +1 -0
  22. package/dist/scanner/index.js +8 -2
  23. package/dist/template/docs.json +2 -1
  24. package/dist/template/next.config.mjs +2 -1
  25. package/dist/template/package.json +17 -15
  26. package/dist/template/public/favicon.svg +4 -0
  27. package/dist/template/public/search-index.json +1 -1
  28. package/dist/template/scripts/build-search-index.mjs +120 -25
  29. package/dist/template/src/app/api/chat/route.ts +11 -3
  30. package/dist/template/src/app/docs/README.md +28 -0
  31. package/dist/template/src/app/docs/[...slug]/page.tsx +139 -16
  32. package/dist/template/src/app/docs/auth/page.mdx +589 -0
  33. package/dist/template/src/app/docs/autofix/page.mdx +624 -0
  34. package/dist/template/src/app/docs/cli/page.mdx +217 -0
  35. package/dist/template/src/app/docs/config/page.mdx +428 -0
  36. package/dist/template/src/app/docs/configuration/page.mdx +86 -0
  37. package/dist/template/src/app/docs/deployment/page.mdx +112 -0
  38. package/dist/template/src/app/docs/error.tsx +20 -0
  39. package/dist/template/src/app/docs/generator/generator.md +504 -0
  40. package/dist/template/src/app/docs/generator/organizer.md +779 -0
  41. package/dist/template/src/app/docs/generator/page.mdx +613 -0
  42. package/dist/template/src/app/docs/github/page.mdx +502 -0
  43. package/dist/template/src/app/docs/llm/anthropic-client.md +549 -0
  44. package/dist/template/src/app/docs/llm/index.md +471 -0
  45. package/dist/template/src/app/docs/llm/page.mdx +428 -0
  46. package/dist/template/src/app/docs/llms-full.md +256 -0
  47. package/dist/template/src/app/docs/llms.txt +2971 -0
  48. package/dist/template/src/app/docs/not-found.tsx +23 -0
  49. package/dist/template/src/app/docs/page.mdx +0 -3
  50. package/dist/template/src/app/docs/plugins/page.mdx +1793 -0
  51. package/dist/template/src/app/docs/pro/page.mdx +121 -0
  52. package/dist/template/src/app/docs/quickstart/page.mdx +93 -0
  53. package/dist/template/src/app/docs/scanner/content-type.md +599 -0
  54. package/dist/template/src/app/docs/scanner/index.md +212 -0
  55. package/dist/template/src/app/docs/scanner/page.mdx +307 -0
  56. package/dist/template/src/app/docs/scanner/python.md +469 -0
  57. package/dist/template/src/app/docs/scanner/python_parser.md +1056 -0
  58. package/dist/template/src/app/docs/scanner/rust.md +325 -0
  59. package/dist/template/src/app/docs/scanner/typescript.md +201 -0
  60. package/dist/template/src/app/error.tsx +3 -3
  61. package/dist/template/src/app/icon.tsx +29 -0
  62. package/dist/template/src/app/layout.tsx +42 -0
  63. package/dist/template/src/app/not-found.tsx +35 -0
  64. package/dist/template/src/app/page.tsx +62 -28
  65. package/dist/template/src/components/ai-chat.tsx +26 -21
  66. package/dist/template/src/components/breadcrumbs.tsx +46 -2
  67. package/dist/template/src/components/copy-button.tsx +17 -3
  68. package/dist/template/src/components/docs-layout.tsx +142 -8
  69. package/dist/template/src/components/feedback.tsx +4 -2
  70. package/dist/template/src/components/footer.tsx +42 -0
  71. package/dist/template/src/components/header.tsx +29 -5
  72. package/dist/template/src/components/mdx/accordion.tsx +7 -6
  73. package/dist/template/src/components/mdx/card.tsx +19 -7
  74. package/dist/template/src/components/mdx/code-block.tsx +17 -3
  75. package/dist/template/src/components/mdx/code-group.tsx +65 -18
  76. package/dist/template/src/components/mdx/code-playground.tsx +3 -0
  77. package/dist/template/src/components/mdx/go-playground.tsx +3 -0
  78. package/dist/template/src/components/mdx/highlighted-code.tsx +171 -76
  79. package/dist/template/src/components/mdx/python-playground.tsx +2 -0
  80. package/dist/template/src/components/mdx/tabs.tsx +74 -6
  81. package/dist/template/src/components/page-header.tsx +19 -0
  82. package/dist/template/src/components/scroll-to-top.tsx +33 -0
  83. package/dist/template/src/components/search-dialog.tsx +206 -52
  84. package/dist/template/src/components/sidebar.tsx +136 -77
  85. package/dist/template/src/components/table-of-contents.tsx +23 -7
  86. package/dist/template/src/lib/highlight.ts +90 -31
  87. package/dist/template/src/lib/search.ts +14 -4
  88. package/dist/template/src/lib/theme-utils.ts +140 -0
  89. package/dist/template/src/styles/globals.css +307 -166
  90. package/dist/template/src/types/remark-gfm.d.ts +2 -0
  91. package/dist/utils/files.d.ts +9 -0
  92. package/dist/utils/files.js +33 -0
  93. package/dist/utils/validation.d.ts +4 -0
  94. package/dist/utils/validation.js +38 -0
  95. 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
- <h1 className="text-4xl sm:text-5xl font-bold tracking-tight text-[var(--color-text)] mb-4">
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-text)] text-[var(--color-bg)] rounded-lg font-medium text-[0.875rem] hover:opacity-90 hover:no-underline transition-opacity"
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="grid grid-cols-1 sm:grid-cols-3 gap-4 max-w-2xl mx-auto mt-16 w-full">
53
- <div className="p-5 rounded-xl border border-[var(--color-border)] hover:border-[var(--color-border-strong)] transition-colors">
54
- <BookOpen size={18} className="text-[var(--color-text-tertiary)] mb-3" />
55
- <h3 className="text-[0.875rem] font-semibold text-[var(--color-text)] mb-1">Guides</h3>
56
- <p className="text-[0.8125rem] text-[var(--color-text-tertiary)] leading-relaxed">
57
- Step-by-step tutorials and explanations.
58
- </p>
59
- </div>
60
- <div className="p-5 rounded-xl border border-[var(--color-border)] hover:border-[var(--color-border-strong)] transition-colors">
61
- <Code size={18} className="text-[var(--color-text-tertiary)] mb-3" />
62
- <h3 className="text-[0.875rem] font-semibold text-[var(--color-text)] mb-1">API Reference</h3>
63
- <p className="text-[0.8125rem] text-[var(--color-text-tertiary)] leading-relaxed">
64
- Complete API documentation with examples.
65
- </p>
66
- </div>
67
- <div className="p-5 rounded-xl border border-[var(--color-border)] hover:border-[var(--color-border-strong)] transition-colors">
68
- <Zap size={18} className="text-[var(--color-text-tertiary)] mb-3" />
69
- <h3 className="text-[0.875rem] font-semibold text-[var(--color-text)] mb-1">Quick Start</h3>
70
- <p className="text-[0.8125rem] text-[var(--color-text-tertiary)] leading-relaxed">
71
- Get up and running in minutes.
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
- </div>
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-zinc-900 text-white shadow-lg transition-transform hover:scale-105 dark:bg-white dark:text-zinc-900"
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-zinc-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-900">
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-zinc-200 px-4 py-3 dark:border-zinc-800">
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-zinc-900 dark:bg-white">
95
- <MessageSquare className="h-4 w-4 text-white dark:text-zinc-900" />
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-zinc-900 dark:text-white">Ask AI</p>
99
- <p className="text-xs text-zinc-500">About {projectName}</p>
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-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800"
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-zinc-500 py-8">
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, i) => (
123
+ {messages.map((message) => (
120
124
  <div
121
- key={i}
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-zinc-900 text-white dark:bg-white dark:text-zinc-900'
128
- : 'bg-zinc-100 text-zinc-900 dark:bg-zinc-800 dark:text-white'
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-zinc-200 pt-2 dark:border-zinc-700">
136
- <p className="text-xs font-medium text-zinc-500 mb-1">Sources:</p>
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-blue-600 hover:underline dark:text-blue-400"
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-zinc-100 px-4 py-2 text-sm text-zinc-500 dark:bg-zinc-800">
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-zinc-200 p-3 dark:border-zinc-800"
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-zinc-200 bg-zinc-50 px-4 py-2 text-sm outline-none transition-colors placeholder:text-zinc-400 focus:border-zinc-400 dark:border-zinc-700 dark:bg-zinc-800 dark:focus:border-zinc-600"
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-zinc-900 text-white transition-colors hover:bg-zinc-800 disabled:opacity-50 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-100"
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
- export function Breadcrumbs() {
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
- const label = segment
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
- await navigator.clipboard.writeText(text)
16
- setCopied(true)
17
- setTimeout(() => setCopied(false), 2000)
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
- pages: Array<{ title: string; path: string }>
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
- export function DocsLayout({ children, docsConfig }: { children: React.ReactNode; docsConfig: DocsConfig }) {
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 md:ml-[var(--sidebar-width)] xl:mr-[var(--toc-width)]">
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
+ }