skrypt-ai 0.4.2 → 0.6.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 (159) hide show
  1. package/dist/auth/index.d.ts +13 -3
  2. package/dist/auth/index.js +101 -9
  3. package/dist/auth/keychain.d.ts +5 -0
  4. package/dist/auth/keychain.js +82 -0
  5. package/dist/auth/notices.d.ts +3 -0
  6. package/dist/auth/notices.js +42 -0
  7. package/dist/autofix/index.d.ts +0 -4
  8. package/dist/autofix/index.js +10 -24
  9. package/dist/capture/browser.d.ts +11 -0
  10. package/dist/capture/browser.js +173 -0
  11. package/dist/capture/diff.d.ts +23 -0
  12. package/dist/capture/diff.js +52 -0
  13. package/dist/capture/index.d.ts +23 -0
  14. package/dist/capture/index.js +210 -0
  15. package/dist/capture/naming.d.ts +17 -0
  16. package/dist/capture/naming.js +45 -0
  17. package/dist/capture/parser.d.ts +15 -0
  18. package/dist/capture/parser.js +80 -0
  19. package/dist/capture/types.d.ts +57 -0
  20. package/dist/capture/types.js +1 -0
  21. package/dist/cli.js +20 -3
  22. package/dist/commands/autofix.js +136 -120
  23. package/dist/commands/cron.js +58 -47
  24. package/dist/commands/deploy.js +123 -102
  25. package/dist/commands/generate.js +125 -7
  26. package/dist/commands/heal.d.ts +10 -0
  27. package/dist/commands/heal.js +201 -0
  28. package/dist/commands/i18n.js +146 -111
  29. package/dist/commands/import.d.ts +2 -0
  30. package/dist/commands/import.js +157 -0
  31. package/dist/commands/init.js +19 -7
  32. package/dist/commands/lint.js +50 -44
  33. package/dist/commands/llms-txt.js +59 -49
  34. package/dist/commands/login.js +63 -34
  35. package/dist/commands/mcp.js +6 -0
  36. package/dist/commands/monitor.js +13 -8
  37. package/dist/commands/qa.d.ts +2 -0
  38. package/dist/commands/qa.js +43 -0
  39. package/dist/commands/review-pr.js +108 -92
  40. package/dist/commands/sdk.js +128 -122
  41. package/dist/commands/security.d.ts +2 -0
  42. package/dist/commands/security.js +109 -0
  43. package/dist/commands/test.js +91 -92
  44. package/dist/commands/version.js +104 -75
  45. package/dist/commands/watch.js +130 -114
  46. package/dist/config/types.js +2 -2
  47. package/dist/context-hub/index.d.ts +23 -0
  48. package/dist/context-hub/index.js +179 -0
  49. package/dist/context-hub/mappings.d.ts +8 -0
  50. package/dist/context-hub/mappings.js +55 -0
  51. package/dist/context-hub/types.d.ts +33 -0
  52. package/dist/context-hub/types.js +1 -0
  53. package/dist/generator/generator.js +39 -6
  54. package/dist/generator/types.d.ts +7 -0
  55. package/dist/generator/writer.d.ts +3 -1
  56. package/dist/generator/writer.js +36 -7
  57. package/dist/importers/confluence.d.ts +5 -0
  58. package/dist/importers/confluence.js +137 -0
  59. package/dist/importers/detect.d.ts +20 -0
  60. package/dist/importers/detect.js +121 -0
  61. package/dist/importers/docusaurus.d.ts +5 -0
  62. package/dist/importers/docusaurus.js +279 -0
  63. package/dist/importers/gitbook.d.ts +5 -0
  64. package/dist/importers/gitbook.js +189 -0
  65. package/dist/importers/github.d.ts +8 -0
  66. package/dist/importers/github.js +99 -0
  67. package/dist/importers/index.d.ts +15 -0
  68. package/dist/importers/index.js +30 -0
  69. package/dist/importers/markdown.d.ts +6 -0
  70. package/dist/importers/markdown.js +105 -0
  71. package/dist/importers/mintlify.d.ts +5 -0
  72. package/dist/importers/mintlify.js +172 -0
  73. package/dist/importers/notion.d.ts +5 -0
  74. package/dist/importers/notion.js +174 -0
  75. package/dist/importers/readme.d.ts +5 -0
  76. package/dist/importers/readme.js +184 -0
  77. package/dist/importers/transform.d.ts +90 -0
  78. package/dist/importers/transform.js +457 -0
  79. package/dist/importers/types.d.ts +37 -0
  80. package/dist/importers/types.js +1 -0
  81. package/dist/llm/anthropic-client.d.ts +1 -0
  82. package/dist/llm/anthropic-client.js +3 -1
  83. package/dist/llm/index.d.ts +6 -4
  84. package/dist/llm/index.js +76 -261
  85. package/dist/llm/openai-client.d.ts +1 -0
  86. package/dist/llm/openai-client.js +7 -2
  87. package/dist/plugins/index.js +7 -0
  88. package/dist/qa/checks.d.ts +10 -0
  89. package/dist/qa/checks.js +492 -0
  90. package/dist/qa/fixes.d.ts +30 -0
  91. package/dist/qa/fixes.js +277 -0
  92. package/dist/qa/index.d.ts +29 -0
  93. package/dist/qa/index.js +187 -0
  94. package/dist/qa/types.d.ts +24 -0
  95. package/dist/qa/types.js +1 -0
  96. package/dist/scanner/csharp.d.ts +23 -0
  97. package/dist/scanner/csharp.js +421 -0
  98. package/dist/scanner/index.js +53 -26
  99. package/dist/scanner/java.d.ts +39 -0
  100. package/dist/scanner/java.js +318 -0
  101. package/dist/scanner/kotlin.d.ts +23 -0
  102. package/dist/scanner/kotlin.js +389 -0
  103. package/dist/scanner/php.d.ts +57 -0
  104. package/dist/scanner/php.js +351 -0
  105. package/dist/scanner/python.js +17 -0
  106. package/dist/scanner/ruby.d.ts +36 -0
  107. package/dist/scanner/ruby.js +431 -0
  108. package/dist/scanner/swift.d.ts +25 -0
  109. package/dist/scanner/swift.js +392 -0
  110. package/dist/scanner/types.d.ts +1 -1
  111. package/dist/template/content/docs/_navigation.json +46 -0
  112. package/dist/template/content/docs/_sidebars.json +684 -0
  113. package/dist/template/content/docs/core.md +4544 -0
  114. package/dist/template/content/docs/index.mdx +89 -0
  115. package/dist/template/content/docs/integrations.md +1158 -0
  116. package/dist/template/content/docs/llms-full.md +403 -0
  117. package/dist/template/content/docs/llms.txt +4588 -0
  118. package/dist/template/content/docs/other.md +10379 -0
  119. package/dist/template/content/docs/tools.md +746 -0
  120. package/dist/template/content/docs/types.md +531 -0
  121. package/dist/template/docs.json +13 -11
  122. package/dist/template/mdx-components.tsx +27 -2
  123. package/dist/template/package.json +6 -0
  124. package/dist/template/public/search-index.json +1 -1
  125. package/dist/template/scripts/build-search-index.mjs +149 -13
  126. package/dist/template/src/app/api/chat/route.ts +83 -128
  127. package/dist/template/src/app/docs/[...slug]/page.tsx +75 -20
  128. package/dist/template/src/app/docs/llms-full.md +151 -4
  129. package/dist/template/src/app/docs/llms.txt +2464 -847
  130. package/dist/template/src/app/docs/page.mdx +48 -38
  131. package/dist/template/src/app/layout.tsx +3 -1
  132. package/dist/template/src/app/page.tsx +22 -8
  133. package/dist/template/src/components/ai-chat.tsx +73 -64
  134. package/dist/template/src/components/breadcrumbs.tsx +21 -23
  135. package/dist/template/src/components/copy-button.tsx +13 -9
  136. package/dist/template/src/components/copy-page-button.tsx +54 -0
  137. package/dist/template/src/components/docs-layout.tsx +37 -25
  138. package/dist/template/src/components/header.tsx +51 -10
  139. package/dist/template/src/components/mdx/card.tsx +17 -3
  140. package/dist/template/src/components/mdx/code-block.tsx +13 -9
  141. package/dist/template/src/components/mdx/code-group.tsx +13 -8
  142. package/dist/template/src/components/mdx/heading.tsx +15 -2
  143. package/dist/template/src/components/mdx/highlighted-code.tsx +13 -8
  144. package/dist/template/src/components/mdx/index.tsx +2 -0
  145. package/dist/template/src/components/mdx/mermaid.tsx +110 -0
  146. package/dist/template/src/components/mdx/screenshot.tsx +150 -0
  147. package/dist/template/src/components/scroll-to-hash.tsx +48 -0
  148. package/dist/template/src/components/sidebar.tsx +12 -18
  149. package/dist/template/src/components/table-of-contents.tsx +9 -0
  150. package/dist/template/src/lib/highlight.ts +3 -88
  151. package/dist/template/src/lib/navigation.ts +159 -0
  152. package/dist/template/src/lib/search-types.ts +4 -1
  153. package/dist/template/src/lib/search.ts +30 -7
  154. package/dist/template/src/styles/globals.css +17 -6
  155. package/dist/utils/files.d.ts +9 -1
  156. package/dist/utils/files.js +59 -10
  157. package/dist/utils/validation.d.ts +0 -3
  158. package/dist/utils/validation.js +0 -26
  159. package/package.json +5 -1
@@ -1,60 +1,70 @@
1
+ ---
2
+ title: "Skrypt Documentation"
3
+ description: "AI-powered documentation generation for your codebase"
4
+ ---
5
+
6
+ # Skrypt Documentation
7
+
8
+ Skrypt scans your source code and generates beautiful, comprehensive documentation automatically. One command, zero boilerplate.
9
+
10
+ <Info>
11
+ **130** documented API elements across 5 categories — all generated by Skrypt itself.
12
+ </Info>
1
13
 
2
14
  <CardGroup cols={2}>
3
- <Card title="Quick Start" icon="Zap" href="/docs/quickstart">
4
- Get up and running in under 5 minutes
15
+ <Card title="Core API" icon="Code" href="/docs/core">
16
+ Main exports, plugin system, auth, and documentation generation (31 items)
17
+ </Card>
18
+ <Card title="Tools & Utilities" icon="Zap" href="/docs/tools">
19
+ File discovery, frontmatter parsing, validation helpers (7 items)
20
+ </Card>
21
+ <Card title="Types & Interfaces" icon="FileText" href="/docs/types">
22
+ TypeScript scanner and type definitions (3 items)
23
+ </Card>
24
+ <Card title="Integrations" icon="Globe" href="/docs/integrations">
25
+ Anthropic and OpenAI-compatible LLM clients (8 items)
5
26
  </Card>
6
- <Card title="Reference" icon="Code" href="/docs/api">
7
- Explore all available APIs
27
+ <Card title="Other" icon="Package" href="/docs/other">
28
+ Language scanners, PR analysis, and utilities (81 items)
8
29
  </Card>
9
30
  </CardGroup>
10
31
 
11
- ## Getting Started
32
+ ## Quick Start
12
33
 
13
34
  <Steps>
14
- <Step title="Installation">
15
- Install the package using your preferred package manager.
35
+ <Step title="Install">
36
+ ```bash
37
+ npx skrypt-ai generate ./src
38
+ ```
16
39
  </Step>
17
- <Step title="Configuration">
18
- Set up your configuration and credentials.
40
+ <Step title="Configure">
41
+ Set your API key for AI-powered documentation:
42
+ ```bash
43
+ skrypt auth login
44
+ ```
19
45
  </Step>
20
- <Step title="First Steps">
21
- Start using the library in your project.
46
+ <Step title="Generate">
47
+ Skrypt scans your codebase, extracts exports, and generates MDX documentation grouped by topic.
22
48
  </Step>
23
49
  </Steps>
24
50
 
25
- ## Code Examples
51
+ ## Install Methods
26
52
 
27
53
  <CodeGroup>
28
54
 
29
- ```typescript
30
- // TypeScript example
31
- import { Client } from 'your-package'
32
-
33
- const client = new Client({
34
- apiKey: process.env.API_KEY
35
- })
36
-
37
- const result = await client.getData()
55
+ ```bash npx (no install)
56
+ npx skrypt-ai generate ./src
38
57
  ```
39
58
 
40
- ```python
41
- # Python example
42
- from your_package import Client
43
-
44
- client = Client(api_key=os.environ["API_KEY"])
59
+ ```bash npm
60
+ npm install -g skrypt-ai
61
+ skrypt generate ./src
62
+ ```
45
63
 
46
- result = client.get_data()
64
+ ```bash Homebrew
65
+ brew tap debgotwired/skrypt
66
+ brew install skrypt
67
+ skrypt generate ./src
47
68
  ```
48
69
 
49
70
  </CodeGroup>
50
-
51
- ## Need Help?
52
-
53
- <AccordionGroup>
54
- <Accordion title="Where can I get support?">
55
- Check the GitHub repository for issues and discussions.
56
- </Accordion>
57
- <Accordion title="How do I report bugs?">
58
- Open an issue on GitHub with reproduction steps.
59
- </Accordion>
60
- </AccordionGroup>
@@ -58,11 +58,13 @@ export const metadata: Metadata = {
58
58
  siteName,
59
59
  title: siteName,
60
60
  description: siteDescription,
61
+ images: ['/og-image.png'],
61
62
  },
62
63
  twitter: {
63
64
  card: 'summary_large_image',
64
65
  title: siteName,
65
66
  description: siteDescription,
67
+ images: ['/og-image.png'],
66
68
  },
67
69
  icons: {
68
70
  icon: '/favicon.svg',
@@ -110,7 +112,7 @@ export default function RootLayout({
110
112
  </>
111
113
  )}
112
114
  </head>
113
- <body className={inter.className} suppressHydrationWarning>
115
+ <body className={inter.className}>
114
116
  <a href="#main-content" className="skip-to-content">Skip to content</a>
115
117
  <SyntaxThemeProvider>
116
118
  {children}
@@ -2,6 +2,7 @@ import Link from 'next/link'
2
2
  import { readFileSync } from 'fs'
3
3
  import { join } from 'path'
4
4
  import { ArrowRight, BookOpen, Zap, Code } from 'lucide-react'
5
+ import { hasTabs, getAllPagesFlat, type Navigation } from '@/lib/navigation'
5
6
 
6
7
  function getDocsConfig() {
7
8
  const configPath = join(process.cwd(), 'docs.json')
@@ -9,17 +10,30 @@ function getDocsConfig() {
9
10
  return JSON.parse(content)
10
11
  }
11
12
 
13
+ /** Get flat groups for display on the home page (works with both formats) */
14
+ function getDisplayGroups(navigation: Navigation): Array<{ group: string; pages: Array<{ title: string; path: string }> }> {
15
+ if (hasTabs(navigation)) {
16
+ const groups: Array<{ group: string; pages: Array<{ title: string; path: string }> }> = []
17
+ for (const tab of navigation) {
18
+ if (tab.groups) {
19
+ for (const g of tab.groups) {
20
+ groups.push(g as any)
21
+ }
22
+ }
23
+ }
24
+ return groups
25
+ }
26
+ return navigation as any
27
+ }
28
+
12
29
  export default function Home() {
13
30
  const docsConfig = getDocsConfig()
14
- const firstPage = docsConfig.navigation?.[0]?.pages?.[0]?.path || '/docs'
31
+ const allPagesFlat = getAllPagesFlat(docsConfig.navigation || [])
32
+ const firstPage = allPagesFlat[0]?.path || '/docs'
15
33
 
16
34
  // 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
- }
35
+ const allPages = allPagesFlat.slice(0, 6)
36
+ const displayGroups = getDisplayGroups(docsConfig.navigation || [])
23
37
 
24
38
  return (
25
39
  <main id="main-content" className="min-h-screen flex flex-col">
@@ -64,7 +78,7 @@ export default function Home() {
64
78
 
65
79
  {/* Feature cards — show first page title + description from each group */}
66
80
  <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) => {
81
+ {displayGroups.slice(0, 3).map((group: { group: string; pages: Array<{ title: string; path: string }> }, i: number) => {
68
82
  const icons = [BookOpen, Code, Zap]
69
83
  const Icon = icons[i % icons.length]
70
84
  const firstGroupPage = group.pages?.[0]
@@ -1,13 +1,14 @@
1
1
  'use client'
2
2
 
3
3
  import { useState, useRef, useEffect } from 'react'
4
- import { MessageSquare, X, Send, Loader2, ExternalLink } from 'lucide-react'
5
-
6
- interface Message {
7
- id: string
8
- role: 'user' | 'assistant'
9
- content: string
10
- citations?: Array<{ title: string; path: string; snippet: string }>
4
+ import { useChat } from '@ai-sdk/react'
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
11
12
  }
12
13
 
13
14
  interface AIChatProps {
@@ -22,60 +23,49 @@ export function AIChat({
22
23
  placeholder = 'Ask a question...',
23
24
  }: AIChatProps) {
24
25
  const [isOpen, setIsOpen] = useState(false)
25
- const [messages, setMessages] = useState<Message[]>([])
26
- const [input, setInput] = useState('')
27
- const [isLoading, setIsLoading] = useState(false)
26
+ const [citations, setCitations] = useState<Map<string, Citation[]>>(new Map())
28
27
  const messagesEndRef = useRef<HTMLDivElement>(null)
29
28
 
30
- useEffect(() => {
31
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
32
- }, [messages])
33
-
34
- async function handleSubmit(e: React.FormEvent) {
35
- e.preventDefault()
36
- if (!input.trim() || isLoading) return
37
-
38
- const userMessage = input.trim()
39
- setInput('')
40
- setMessages(prev => [...prev, { id: Date.now().toString(), role: 'user', content: userMessage }])
41
- setIsLoading(true)
42
-
43
- try {
44
- const response = await fetch(apiEndpoint, {
45
- method: 'POST',
46
- headers: { 'Content-Type': 'application/json' },
47
- body: JSON.stringify({
48
- messages: [...messages, { role: 'user', content: userMessage }],
49
- }),
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
50
59
  })
60
+ },
61
+ })
51
62
 
52
- if (!response.ok) {
53
- throw new Error('Failed to get response')
54
- }
63
+ const isStreaming = status === 'streaming'
64
+ const isLoading = status === 'submitted'
55
65
 
56
- const data = await response.json()
57
- setMessages(prev => [
58
- ...prev,
59
- {
60
- id: Date.now().toString(),
61
- role: 'assistant',
62
- content: data.content,
63
- citations: data.citations,
64
- },
65
- ])
66
- } catch (err) {
67
- setMessages(prev => [
68
- ...prev,
69
- {
70
- id: Date.now().toString(),
71
- role: 'assistant',
72
- content: 'Sorry, I encountered an error. Please try again.',
73
- },
74
- ])
75
- } finally {
76
- setIsLoading(false)
77
- }
78
- }
66
+ useEffect(() => {
67
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
68
+ }, [messages, isStreaming])
79
69
 
80
70
  if (!isOpen) {
81
71
  return (
@@ -116,7 +106,7 @@ export function AIChat({
116
106
  {messages.length === 0 && (
117
107
  <div className="text-center text-sm text-[var(--color-text-tertiary)] py-8">
118
108
  <p>Ask me anything about {projectName}.</p>
119
- <p className="mt-2 text-xs">I'll search the docs and give you an answer with sources.</p>
109
+ <p className="mt-2 text-xs">I&apos;ll search the docs and give you an answer with sources.</p>
120
110
  </div>
121
111
  )}
122
112
 
@@ -132,14 +122,25 @@ export function AIChat({
132
122
  : 'bg-[var(--color-bg-tertiary)] text-[var(--color-text)]'
133
123
  }`}
134
124
  >
135
- <p className="whitespace-pre-wrap">{message.content}</p>
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
+ )}
136
137
 
137
138
  {/* Citations */}
138
- {message.citations && message.citations.length > 0 && (
139
+ {citations.get(message.id) && (
139
140
  <div className="mt-3 border-t border-[var(--color-border)] pt-2">
140
141
  <p className="text-xs font-medium text-[var(--color-text-tertiary)] mb-1">Sources:</p>
141
142
  <div className="space-y-1">
142
- {message.citations.map((citation, j) => (
143
+ {citations.get(message.id)!.map((citation, j) => (
143
144
  <a
144
145
  key={j}
145
146
  href={citation.path}
@@ -159,12 +160,20 @@ export function AIChat({
159
160
  {isLoading && (
160
161
  <div className="flex justify-start">
161
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)]">
162
- <Loader2 className="h-4 w-4 animate-spin" />
163
+ <span className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
163
164
  Searching docs...
164
165
  </div>
165
166
  </div>
166
167
  )}
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
+ )}
176
+
168
177
  <div ref={messagesEndRef} />
169
178
  </div>
170
179
 
@@ -177,14 +186,14 @@ export function AIChat({
177
186
  <input
178
187
  type="text"
179
188
  value={input}
180
- onChange={e => setInput(e.target.value)}
189
+ onChange={handleInputChange}
181
190
  placeholder={placeholder}
182
- disabled={isLoading}
191
+ disabled={isStreaming || isLoading}
183
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)]"
184
193
  />
185
194
  <button
186
195
  type="submit"
187
- disabled={!input.trim() || isLoading}
196
+ disabled={!input.trim() || isStreaming || isLoading}
188
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"
189
198
  aria-label="Send message"
190
199
  >
@@ -3,41 +3,39 @@
3
3
  import Link from 'next/link'
4
4
  import { usePathname } from 'next/navigation'
5
5
  import { ChevronRight } from 'lucide-react'
6
+ import { hasTabs, isNavGroup, type Navigation, type NavPage, type NavGroup } from '@/lib/navigation'
6
7
 
7
- interface NavPage { title: string; path: string; pages?: NavPage[] }
8
- interface NavGroup { group: string; pages: (NavPage | NavGroup)[] }
9
- interface DocsConfig { navigation: NavGroup[] }
8
+ interface DocsConfig { navigation: Navigation }
10
9
 
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> {
10
+ function buildTitleMap(navigation: Navigation): Map<string, string> {
16
11
  const map = new Map<string, string>()
17
- if (!docsConfig) return map
18
12
 
19
- for (const group of docsConfig.navigation) {
20
- for (const item of group.pages) {
13
+ function fillFromPages(pages: (NavPage | NavGroup)[]) {
14
+ for (const item of pages) {
21
15
  if (isNavGroup(item)) {
22
- fillTitleMap(item.pages, map)
16
+ fillFromPages(item.pages)
23
17
  } else {
24
18
  map.set(item.path, item.title)
25
- if (item.pages) fillTitleMap(item.pages, map)
19
+ if (item.pages) fillFromPages(item.pages)
26
20
  }
27
21
  }
28
22
  }
29
- return map
30
- }
31
23
 
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)
24
+ if (hasTabs(navigation)) {
25
+ for (const tab of navigation) {
26
+ if (tab.groups) {
27
+ for (const group of tab.groups) {
28
+ fillFromPages(group.pages)
29
+ }
30
+ }
31
+ }
32
+ } else {
33
+ for (const group of navigation) {
34
+ fillFromPages(group.pages)
39
35
  }
40
36
  }
37
+
38
+ return map
41
39
  }
42
40
 
43
41
  interface BreadcrumbsProps {
@@ -50,7 +48,7 @@ export function Breadcrumbs({ docsConfig }: BreadcrumbsProps) {
50
48
 
51
49
  if (segments.length <= 1) return null
52
50
 
53
- const titleMap = buildTitleMap(docsConfig)
51
+ const titleMap = docsConfig ? buildTitleMap(docsConfig.navigation) : new Map<string, string>()
54
52
 
55
53
  const crumbs = segments.map((segment, index) => {
56
54
  const href = '/' + segments.slice(0, index + 1).join('/')
@@ -17,15 +17,19 @@ export function CopyButton({ text, className = '' }: CopyButtonProps) {
17
17
  setCopied(true)
18
18
  setTimeout(() => setCopied(false), 2000)
19
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)
20
+ // Deprecated fallback for older browsers that don't support navigator.clipboard
21
+ try {
22
+ const textarea = document.createElement('textarea')
23
+ textarea.value = text
24
+ textarea.style.position = 'fixed'
25
+ textarea.style.opacity = '0'
26
+ document.body.appendChild(textarea)
27
+ textarea.select()
28
+ document.execCommand('copy') // deprecated but kept for old browser compat
29
+ document.body.removeChild(textarea)
30
+ } catch {
31
+ // Copy failed in both methods — silently ignore
32
+ }
29
33
  setCopied(true)
30
34
  setTimeout(() => setCopied(false), 2000)
31
35
  }
@@ -0,0 +1,54 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { Copy, Check } from 'lucide-react'
5
+
6
+ interface CopyPageButtonProps {
7
+ content: string
8
+ }
9
+
10
+ export function CopyPageButton({ content }: CopyPageButtonProps) {
11
+ const [copied, setCopied] = useState(false)
12
+
13
+ async function handleCopy() {
14
+ try {
15
+ await navigator.clipboard.writeText(content)
16
+ } catch {
17
+ // Deprecated fallback for older browsers that don't support navigator.clipboard
18
+ try {
19
+ const textarea = document.createElement('textarea')
20
+ textarea.value = content
21
+ textarea.style.position = 'fixed'
22
+ textarea.style.opacity = '0'
23
+ document.body.appendChild(textarea)
24
+ textarea.select()
25
+ document.execCommand('copy') // deprecated but kept for old browser compat
26
+ document.body.removeChild(textarea)
27
+ } catch {
28
+ // Copy failed in both methods — silently ignore
29
+ }
30
+ }
31
+ setCopied(true)
32
+ setTimeout(() => setCopied(false), 2000)
33
+ }
34
+
35
+ return (
36
+ <button
37
+ onClick={handleCopy}
38
+ className="flex items-center gap-1.5 shrink-0 mt-1 px-2.5 py-1.5 text-[0.75rem] text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] border border-[var(--color-border)] rounded-lg hover:bg-[var(--color-bg-secondary)] transition-colors"
39
+ title={copied ? 'Copied!' : 'Copy page content'}
40
+ >
41
+ {copied ? (
42
+ <>
43
+ <Check size={12} />
44
+ Copied
45
+ </>
46
+ ) : (
47
+ <>
48
+ <Copy size={12} />
49
+ Copy page
50
+ </>
51
+ )}
52
+ </button>
53
+ )
54
+ }