skrypt-ai 0.1.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 (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -0
  3. package/dist/autofix/index.d.ts +46 -0
  4. package/dist/autofix/index.js +240 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +40 -0
  7. package/dist/commands/autofix.d.ts +2 -0
  8. package/dist/commands/autofix.js +143 -0
  9. package/dist/commands/generate.d.ts +2 -0
  10. package/dist/commands/generate.js +320 -0
  11. package/dist/commands/init.d.ts +2 -0
  12. package/dist/commands/init.js +56 -0
  13. package/dist/commands/review-pr.d.ts +2 -0
  14. package/dist/commands/review-pr.js +117 -0
  15. package/dist/commands/watch.d.ts +2 -0
  16. package/dist/commands/watch.js +142 -0
  17. package/dist/config/index.d.ts +2 -0
  18. package/dist/config/index.js +2 -0
  19. package/dist/config/loader.d.ts +9 -0
  20. package/dist/config/loader.js +82 -0
  21. package/dist/config/types.d.ts +24 -0
  22. package/dist/config/types.js +34 -0
  23. package/dist/generator/generator.d.ts +15 -0
  24. package/dist/generator/generator.js +144 -0
  25. package/dist/generator/index.d.ts +4 -0
  26. package/dist/generator/index.js +4 -0
  27. package/dist/generator/organizer.d.ts +29 -0
  28. package/dist/generator/organizer.js +222 -0
  29. package/dist/generator/types.d.ts +83 -0
  30. package/dist/generator/types.js +1 -0
  31. package/dist/generator/writer.d.ts +28 -0
  32. package/dist/generator/writer.js +320 -0
  33. package/dist/github/pr-comments.d.ts +40 -0
  34. package/dist/github/pr-comments.js +308 -0
  35. package/dist/llm/anthropic-client.d.ts +16 -0
  36. package/dist/llm/anthropic-client.js +92 -0
  37. package/dist/llm/index.d.ts +53 -0
  38. package/dist/llm/index.js +400 -0
  39. package/dist/llm/llm.manual-test.d.ts +1 -0
  40. package/dist/llm/llm.manual-test.js +112 -0
  41. package/dist/llm/llm.mock-test.d.ts +4 -0
  42. package/dist/llm/llm.mock-test.js +79 -0
  43. package/dist/llm/openai-client.d.ts +17 -0
  44. package/dist/llm/openai-client.js +90 -0
  45. package/dist/llm/types.d.ts +60 -0
  46. package/dist/llm/types.js +20 -0
  47. package/dist/scanner/content-type.d.ts +39 -0
  48. package/dist/scanner/content-type.js +194 -0
  49. package/dist/scanner/content-type.test.d.ts +1 -0
  50. package/dist/scanner/content-type.test.js +231 -0
  51. package/dist/scanner/go.d.ts +20 -0
  52. package/dist/scanner/go.js +269 -0
  53. package/dist/scanner/index.d.ts +21 -0
  54. package/dist/scanner/index.js +137 -0
  55. package/dist/scanner/python.d.ts +6 -0
  56. package/dist/scanner/python.js +57 -0
  57. package/dist/scanner/python_parser.py +230 -0
  58. package/dist/scanner/rust.d.ts +23 -0
  59. package/dist/scanner/rust.js +304 -0
  60. package/dist/scanner/scanner.test.d.ts +1 -0
  61. package/dist/scanner/scanner.test.js +210 -0
  62. package/dist/scanner/types.d.ts +50 -0
  63. package/dist/scanner/types.js +1 -0
  64. package/dist/scanner/typescript.d.ts +34 -0
  65. package/dist/scanner/typescript.js +327 -0
  66. package/dist/scanner/typescript.manual-test.d.ts +1 -0
  67. package/dist/scanner/typescript.manual-test.js +112 -0
  68. package/dist/template/docs.json +32 -0
  69. package/dist/template/mdx-components.tsx +62 -0
  70. package/dist/template/next-env.d.ts +6 -0
  71. package/dist/template/next.config.mjs +17 -0
  72. package/dist/template/package.json +39 -0
  73. package/dist/template/postcss.config.mjs +5 -0
  74. package/dist/template/public/search-index.json +1 -0
  75. package/dist/template/scripts/build-search-index.mjs +120 -0
  76. package/dist/template/src/app/api/mock/[...path]/route.ts +224 -0
  77. package/dist/template/src/app/api/openapi/route.ts +48 -0
  78. package/dist/template/src/app/api/rate-limit/route.ts +84 -0
  79. package/dist/template/src/app/docs/[...slug]/page.tsx +81 -0
  80. package/dist/template/src/app/docs/layout.tsx +9 -0
  81. package/dist/template/src/app/docs/page.mdx +67 -0
  82. package/dist/template/src/app/error.tsx +63 -0
  83. package/dist/template/src/app/layout.tsx +71 -0
  84. package/dist/template/src/app/page.tsx +18 -0
  85. package/dist/template/src/app/reference/route.ts +36 -0
  86. package/dist/template/src/app/robots.ts +14 -0
  87. package/dist/template/src/app/sitemap.ts +64 -0
  88. package/dist/template/src/components/breadcrumbs.tsx +41 -0
  89. package/dist/template/src/components/copy-button.tsx +29 -0
  90. package/dist/template/src/components/docs-layout.tsx +35 -0
  91. package/dist/template/src/components/edit-link.tsx +39 -0
  92. package/dist/template/src/components/feedback.tsx +52 -0
  93. package/dist/template/src/components/header.tsx +66 -0
  94. package/dist/template/src/components/mdx/accordion.tsx +48 -0
  95. package/dist/template/src/components/mdx/api-badge.tsx +57 -0
  96. package/dist/template/src/components/mdx/callout.tsx +111 -0
  97. package/dist/template/src/components/mdx/card.tsx +62 -0
  98. package/dist/template/src/components/mdx/changelog.tsx +57 -0
  99. package/dist/template/src/components/mdx/code-block.tsx +42 -0
  100. package/dist/template/src/components/mdx/code-group.tsx +125 -0
  101. package/dist/template/src/components/mdx/code-playground.tsx +322 -0
  102. package/dist/template/src/components/mdx/go-playground.tsx +235 -0
  103. package/dist/template/src/components/mdx/heading.tsx +37 -0
  104. package/dist/template/src/components/mdx/highlighted-code.tsx +89 -0
  105. package/dist/template/src/components/mdx/index.tsx +15 -0
  106. package/dist/template/src/components/mdx/param-table.tsx +71 -0
  107. package/dist/template/src/components/mdx/python-playground.tsx +293 -0
  108. package/dist/template/src/components/mdx/steps.tsx +43 -0
  109. package/dist/template/src/components/mdx/tabs.tsx +81 -0
  110. package/dist/template/src/components/rate-limit-display.tsx +183 -0
  111. package/dist/template/src/components/search-dialog.tsx +178 -0
  112. package/dist/template/src/components/sidebar.tsx +129 -0
  113. package/dist/template/src/components/syntax-theme-selector.tsx +50 -0
  114. package/dist/template/src/components/table-of-contents.tsx +84 -0
  115. package/dist/template/src/components/theme-toggle.tsx +46 -0
  116. package/dist/template/src/components/version-selector.tsx +61 -0
  117. package/dist/template/src/contexts/syntax-theme.tsx +52 -0
  118. package/dist/template/src/lib/highlight.ts +83 -0
  119. package/dist/template/src/lib/search-types.ts +37 -0
  120. package/dist/template/src/lib/search.ts +125 -0
  121. package/dist/template/src/lib/utils.ts +6 -0
  122. package/dist/template/src/styles/globals.css +152 -0
  123. package/dist/template/tsconfig.json +25 -0
  124. package/dist/template/tsconfig.tsbuildinfo +1 -0
  125. package/package.json +72 -0
@@ -0,0 +1,84 @@
1
+ import { NextResponse } from 'next/server'
2
+
3
+ /**
4
+ * Rate Limit Info Endpoint
5
+ * Returns current rate limit status in headers
6
+ *
7
+ * This is a demo endpoint - in production, connect to your actual rate limiter
8
+ */
9
+
10
+ // Simulated rate limit state (in production, use Redis/database)
11
+ const rateLimitState = new Map<string, { used: number; resetAt: number }>()
12
+
13
+ const LIMIT = 1000
14
+ const WINDOW_MS = 60 * 60 * 1000 // 1 hour
15
+
16
+ function getRateLimitInfo(clientId: string) {
17
+ const now = Date.now()
18
+ let state = rateLimitState.get(clientId)
19
+
20
+ // Reset if window expired
21
+ if (!state || state.resetAt < now) {
22
+ state = {
23
+ used: 0,
24
+ resetAt: now + WINDOW_MS,
25
+ }
26
+ rateLimitState.set(clientId, state)
27
+ }
28
+
29
+ return {
30
+ limit: LIMIT,
31
+ remaining: Math.max(0, LIMIT - state.used),
32
+ reset: Math.floor(state.resetAt / 1000),
33
+ used: state.used,
34
+ }
35
+ }
36
+
37
+ export async function GET(request: Request) {
38
+ // Use IP or auth token as client identifier
39
+ const clientId = request.headers.get('x-forwarded-for') ||
40
+ request.headers.get('authorization') ||
41
+ 'anonymous'
42
+
43
+ const info = getRateLimitInfo(clientId)
44
+
45
+ return NextResponse.json(
46
+ {
47
+ message: 'Rate limit info',
48
+ ...info,
49
+ },
50
+ {
51
+ headers: {
52
+ 'X-RateLimit-Limit': String(info.limit),
53
+ 'X-RateLimit-Remaining': String(info.remaining),
54
+ 'X-RateLimit-Reset': String(info.reset),
55
+ 'X-RateLimit-Used': String(info.used),
56
+ // Standard Rate Limit headers (draft spec)
57
+ 'RateLimit-Limit': String(info.limit),
58
+ 'RateLimit-Remaining': String(info.remaining),
59
+ 'RateLimit-Reset': String(info.reset),
60
+ },
61
+ }
62
+ )
63
+ }
64
+
65
+ export async function HEAD(request: Request) {
66
+ const clientId = request.headers.get('x-forwarded-for') ||
67
+ request.headers.get('authorization') ||
68
+ 'anonymous'
69
+
70
+ const info = getRateLimitInfo(clientId)
71
+
72
+ return new NextResponse(null, {
73
+ status: 200,
74
+ headers: {
75
+ 'X-RateLimit-Limit': String(info.limit),
76
+ 'X-RateLimit-Remaining': String(info.remaining),
77
+ 'X-RateLimit-Reset': String(info.reset),
78
+ 'X-RateLimit-Used': String(info.used),
79
+ 'RateLimit-Limit': String(info.limit),
80
+ 'RateLimit-Remaining': String(info.remaining),
81
+ 'RateLimit-Reset': String(info.reset),
82
+ },
83
+ })
84
+ }
@@ -0,0 +1,81 @@
1
+ import { notFound } from 'next/navigation'
2
+ import { readFile, readdir, stat } from 'fs/promises'
3
+ import { join } from 'path'
4
+ import { compileMDX } from 'next-mdx-remote/rsc'
5
+ import * as components from '@/components/mdx'
6
+
7
+ interface PageProps {
8
+ params: Promise<{ slug: string[] }>
9
+ }
10
+
11
+ async function getContent(slug: string[]) {
12
+ const contentDir = join(process.cwd(), 'content', 'docs')
13
+ const filePath = join(contentDir, ...slug)
14
+
15
+ // Try .mdx first, then .md
16
+ for (const ext of ['.mdx', '.md', '/index.mdx', '/index.md']) {
17
+ try {
18
+ const fullPath = filePath + ext
19
+ const content = await readFile(fullPath, 'utf-8')
20
+ return { content, path: fullPath }
21
+ } catch {
22
+ continue
23
+ }
24
+ }
25
+
26
+ return null
27
+ }
28
+
29
+ export default async function DocPage({ params }: PageProps) {
30
+ const { slug } = await params
31
+ const result = await getContent(slug)
32
+
33
+ if (!result) {
34
+ notFound()
35
+ }
36
+
37
+ const { content } = await compileMDX({
38
+ source: result.content,
39
+ components: components as any,
40
+ options: {
41
+ parseFrontmatter: true,
42
+ },
43
+ })
44
+
45
+ return (
46
+ <div className="prose max-w-none">
47
+ {content}
48
+ </div>
49
+ )
50
+ }
51
+
52
+ export async function generateStaticParams() {
53
+ const contentDir = join(process.cwd(), 'content', 'docs')
54
+ const slugs: { slug: string[] }[] = []
55
+
56
+ async function walkDir(dir: string, prefix: string[] = []) {
57
+ try {
58
+ const entries = await readdir(dir, { withFileTypes: true })
59
+
60
+ for (const entry of entries) {
61
+ if (entry.isDirectory()) {
62
+ await walkDir(join(dir, entry.name), [...prefix, entry.name])
63
+ } else if (entry.name.endsWith('.md') || entry.name.endsWith('.mdx')) {
64
+ const name = entry.name.replace(/\.(mdx?|md)$/, '')
65
+ if (name === 'index') {
66
+ if (prefix.length > 0) {
67
+ slugs.push({ slug: prefix })
68
+ }
69
+ } else {
70
+ slugs.push({ slug: [...prefix, name] })
71
+ }
72
+ }
73
+ }
74
+ } catch {
75
+ // Directory doesn't exist yet
76
+ }
77
+ }
78
+
79
+ await walkDir(contentDir)
80
+ return slugs
81
+ }
@@ -0,0 +1,9 @@
1
+ import { DocsLayout } from '@/components/docs-layout'
2
+
3
+ export default function Layout({
4
+ children,
5
+ }: {
6
+ children: React.ReactNode
7
+ }) {
8
+ return <DocsLayout>{children}</DocsLayout>
9
+ }
@@ -0,0 +1,67 @@
1
+ # Introduction
2
+
3
+ Welcome to the API documentation. This guide will help you get started with integrating our API into your applications.
4
+
5
+ <CardGroup cols={2}>
6
+ <Card title="Quick Start" icon="Zap" href="/docs/quickstart">
7
+ Get up and running in under 5 minutes
8
+ </Card>
9
+ <Card title="API Reference" icon="Code" href="/docs/api">
10
+ Explore all available endpoints
11
+ </Card>
12
+ </CardGroup>
13
+
14
+ ## Features
15
+
16
+ <Info title="Full API Access">
17
+ Our API provides complete access to all platform features with comprehensive documentation.
18
+ </Info>
19
+
20
+ <Steps>
21
+ <Step title="Install the SDK">
22
+ Install our SDK using your preferred package manager.
23
+ </Step>
24
+ <Step title="Configure authentication">
25
+ Set up your API keys and configure authentication.
26
+ </Step>
27
+ <Step title="Make your first request">
28
+ Start making API calls to our endpoints.
29
+ </Step>
30
+ </Steps>
31
+
32
+ ## Code Examples
33
+
34
+ <CodeGroup>
35
+
36
+ ```typescript
37
+ // TypeScript
38
+ import { Client } from 'your-sdk'
39
+
40
+ const client = new Client({
41
+ apiKey: process.env.API_KEY
42
+ })
43
+
44
+ const result = await client.getData()
45
+ ```
46
+
47
+ ```python
48
+ # Python
49
+ from your_sdk import Client
50
+
51
+ client = Client(api_key=os.environ["API_KEY"])
52
+
53
+ result = client.get_data()
54
+ ```
55
+
56
+ </CodeGroup>
57
+
58
+ ## FAQ
59
+
60
+ <AccordionGroup>
61
+ <Accordion title="How do I get an API key?">
62
+ You can generate an API key from your dashboard. Navigate to Settings → API Keys and click "Create New Key".
63
+ </Accordion>
64
+ <Accordion title="What are the rate limits?">
65
+ Free tier: 100 requests/minute. Pro tier: 1000 requests/minute. Enterprise: Unlimited.
66
+ </Accordion>
67
+ </AccordionGroup>
@@ -0,0 +1,63 @@
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+
5
+ interface ErrorProps {
6
+ error: Error & { digest?: string }
7
+ reset: () => void
8
+ }
9
+
10
+ export default function Error({ error, reset }: ErrorProps) {
11
+ useEffect(() => {
12
+ console.error('Application error:', error)
13
+ }, [error])
14
+
15
+ return (
16
+ <div className="min-h-screen flex items-center justify-center p-4">
17
+ <div className="max-w-md w-full text-center">
18
+ <div className="mb-6">
19
+ <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-red-100 dark:bg-red-900/30 mb-4">
20
+ <svg
21
+ className="w-8 h-8 text-red-600 dark:text-red-400"
22
+ fill="none"
23
+ viewBox="0 0 24 24"
24
+ stroke="currentColor"
25
+ >
26
+ <path
27
+ strokeLinecap="round"
28
+ strokeLinejoin="round"
29
+ strokeWidth={2}
30
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
31
+ />
32
+ </svg>
33
+ </div>
34
+ <h1 className="text-2xl font-bold text-[var(--color-text)] mb-2">
35
+ Something went wrong
36
+ </h1>
37
+ <p className="text-[var(--color-text-secondary)] mb-4">
38
+ An unexpected error occurred. Please try again.
39
+ </p>
40
+ {error.digest && (
41
+ <p className="text-xs text-[var(--color-text-tertiary)] mb-4">
42
+ Error ID: {error.digest}
43
+ </p>
44
+ )}
45
+ </div>
46
+ <div className="flex gap-3 justify-center">
47
+ <button
48
+ onClick={reset}
49
+ className="px-4 py-2 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] transition-colors"
50
+ >
51
+ Try again
52
+ </button>
53
+ <a
54
+ href="/"
55
+ className="px-4 py-2 border border-[var(--color-border)] text-[var(--color-text)] rounded-lg hover:bg-[var(--color-bg-secondary)] transition-colors"
56
+ >
57
+ Go home
58
+ </a>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ )
63
+ }
@@ -0,0 +1,71 @@
1
+ import type { Metadata } from 'next'
2
+ import { Inter } from 'next/font/google'
3
+ import '@/styles/globals.css'
4
+ import { SyntaxThemeProvider } from '@/contexts/syntax-theme'
5
+
6
+ const inter = Inter({ subsets: ['latin'] })
7
+
8
+ const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://docs.example.com'
9
+ const siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'API Documentation'
10
+
11
+ export const metadata: Metadata = {
12
+ title: {
13
+ default: siteName,
14
+ template: `%s | ${siteName}`,
15
+ },
16
+ description: 'API documentation with interactive examples',
17
+ metadataBase: new URL(siteUrl),
18
+ openGraph: {
19
+ type: 'website',
20
+ locale: 'en_US',
21
+ url: siteUrl,
22
+ siteName,
23
+ title: siteName,
24
+ description: 'API documentation with interactive examples',
25
+ },
26
+ twitter: {
27
+ card: 'summary_large_image',
28
+ title: siteName,
29
+ description: 'API documentation with interactive examples',
30
+ },
31
+ robots: {
32
+ index: true,
33
+ follow: true,
34
+ },
35
+ }
36
+
37
+ // JSON-LD structured data
38
+ const jsonLd = {
39
+ '@context': 'https://schema.org',
40
+ '@type': 'WebSite',
41
+ name: siteName,
42
+ url: siteUrl,
43
+ description: 'API documentation with interactive examples',
44
+ potentialAction: {
45
+ '@type': 'SearchAction',
46
+ target: `${siteUrl}/docs?q={search_term_string}`,
47
+ 'query-input': 'required name=search_term_string',
48
+ },
49
+ }
50
+
51
+ export default function RootLayout({
52
+ children,
53
+ }: {
54
+ children: React.ReactNode
55
+ }) {
56
+ return (
57
+ <html lang="en" suppressHydrationWarning>
58
+ <head>
59
+ <script
60
+ type="application/ld+json"
61
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
62
+ />
63
+ </head>
64
+ <body className={inter.className}>
65
+ <SyntaxThemeProvider>
66
+ {children}
67
+ </SyntaxThemeProvider>
68
+ </body>
69
+ </html>
70
+ )
71
+ }
@@ -0,0 +1,18 @@
1
+ import Link from 'next/link'
2
+
3
+ export default function Home() {
4
+ return (
5
+ <main className="min-h-screen flex flex-col items-center justify-center p-8">
6
+ <h1 className="text-4xl font-bold mb-4">API Documentation</h1>
7
+ <p className="text-[var(--color-text-secondary)] mb-8 text-center max-w-xl">
8
+ Welcome to the API documentation. Browse the docs to learn how to integrate with our API.
9
+ </p>
10
+ <Link
11
+ href="/docs"
12
+ className="px-6 py-3 bg-[var(--color-primary)] text-white rounded-lg font-medium hover:bg-[var(--color-primary-dark)] transition-colors"
13
+ >
14
+ Get Started
15
+ </Link>
16
+ </main>
17
+ )
18
+ }
@@ -0,0 +1,36 @@
1
+ import { ApiReference } from '@scalar/nextjs-api-reference'
2
+
3
+ const MOCK_SERVER_ENABLED = process.env.NEXT_PUBLIC_MOCK_SERVER === 'true'
4
+
5
+ // Build servers list
6
+ const servers = [
7
+ ...(MOCK_SERVER_ENABLED ? [{
8
+ url: '/api/mock',
9
+ description: 'Mock Server (returns example responses)',
10
+ }] : []),
11
+ {
12
+ url: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000',
13
+ description: 'Default',
14
+ },
15
+ ...(process.env.NEXT_PUBLIC_API_STAGING_URL ? [{
16
+ url: process.env.NEXT_PUBLIC_API_STAGING_URL,
17
+ description: 'Staging',
18
+ }] : []),
19
+ ]
20
+
21
+ export const GET = ApiReference({
22
+ spec: {
23
+ url: '/api/openapi',
24
+ },
25
+ // Scalar configuration options
26
+ theme: 'default',
27
+ hideModels: false,
28
+ darkMode: true,
29
+ servers,
30
+ // Enable request sharing (#76)
31
+ showSidebar: true,
32
+ searchHotKey: 'k',
33
+ metaData: {
34
+ title: 'API Reference',
35
+ },
36
+ })
@@ -0,0 +1,14 @@
1
+ import { MetadataRoute } from 'next'
2
+
3
+ export default function robots(): MetadataRoute.Robots {
4
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://docs.example.com'
5
+
6
+ return {
7
+ rules: {
8
+ userAgent: '*',
9
+ allow: '/',
10
+ disallow: ['/api/', '/_next/'],
11
+ },
12
+ sitemap: `${baseUrl}/sitemap.xml`,
13
+ }
14
+ }
@@ -0,0 +1,64 @@
1
+ import { MetadataRoute } from 'next'
2
+ import { readdir } from 'fs/promises'
3
+ import { join } from 'path'
4
+
5
+ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
6
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://docs.example.com'
7
+
8
+ const routes: MetadataRoute.Sitemap = [
9
+ {
10
+ url: baseUrl,
11
+ lastModified: new Date(),
12
+ changeFrequency: 'weekly',
13
+ priority: 1,
14
+ },
15
+ {
16
+ url: `${baseUrl}/docs`,
17
+ lastModified: new Date(),
18
+ changeFrequency: 'weekly',
19
+ priority: 0.9,
20
+ },
21
+ ]
22
+
23
+ // Scan content/docs for all MDX files
24
+ try {
25
+ const docsDir = join(process.cwd(), 'content', 'docs')
26
+ const files = await scanDir(docsDir, '')
27
+
28
+ for (const file of files) {
29
+ const slug = file.replace(/\.mdx?$/, '').replace(/\/index$/, '')
30
+ routes.push({
31
+ url: `${baseUrl}/docs/${slug}`,
32
+ lastModified: new Date(),
33
+ changeFrequency: 'weekly',
34
+ priority: 0.8,
35
+ })
36
+ }
37
+ } catch {
38
+ // No docs directory yet
39
+ }
40
+
41
+ return routes
42
+ }
43
+
44
+ async function scanDir(dir: string, prefix: string): Promise<string[]> {
45
+ const files: string[] = []
46
+
47
+ try {
48
+ const entries = await readdir(dir, { withFileTypes: true })
49
+
50
+ for (const entry of entries) {
51
+ const path = prefix ? `${prefix}/${entry.name}` : entry.name
52
+
53
+ if (entry.isDirectory()) {
54
+ files.push(...await scanDir(join(dir, entry.name), path))
55
+ } else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {
56
+ files.push(path)
57
+ }
58
+ }
59
+ } catch {
60
+ // Directory doesn't exist
61
+ }
62
+
63
+ return files
64
+ }
@@ -0,0 +1,41 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { usePathname } from 'next/navigation'
5
+ import { ChevronRight, Home } from 'lucide-react'
6
+
7
+ export function Breadcrumbs() {
8
+ const pathname = usePathname()
9
+ const segments = pathname.split('/').filter(Boolean)
10
+
11
+ if (segments.length <= 1) return null
12
+
13
+ const crumbs = segments.map((segment, index) => {
14
+ const href = '/' + segments.slice(0, index + 1).join('/')
15
+ const label = segment
16
+ .replace(/-/g, ' ')
17
+ .replace(/\b\w/g, (c) => c.toUpperCase())
18
+
19
+ return { href, label }
20
+ })
21
+
22
+ return (
23
+ <nav className="flex items-center gap-1.5 text-[13px] text-[var(--color-text-tertiary)] mb-4">
24
+ <Link href="/" className="hover:text-[var(--color-text)]">
25
+ <Home size={14} />
26
+ </Link>
27
+ {crumbs.map((crumb, i) => (
28
+ <span key={crumb.href} className="flex items-center gap-1.5">
29
+ <ChevronRight size={14} />
30
+ {i === crumbs.length - 1 ? (
31
+ <span className="text-[var(--color-text)]">{crumb.label}</span>
32
+ ) : (
33
+ <Link href={crumb.href} className="hover:text-[var(--color-text)]">
34
+ {crumb.label}
35
+ </Link>
36
+ )}
37
+ </span>
38
+ ))}
39
+ </nav>
40
+ )
41
+ }
@@ -0,0 +1,29 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { Copy, Check } from 'lucide-react'
5
+
6
+ interface CopyButtonProps {
7
+ text: string
8
+ className?: string
9
+ }
10
+
11
+ export function CopyButton({ text, className = '' }: CopyButtonProps) {
12
+ const [copied, setCopied] = useState(false)
13
+
14
+ async function handleCopy() {
15
+ await navigator.clipboard.writeText(text)
16
+ setCopied(true)
17
+ setTimeout(() => setCopied(false), 2000)
18
+ }
19
+
20
+ return (
21
+ <button
22
+ onClick={handleCopy}
23
+ className={`p-1.5 rounded text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] hover:bg-[var(--color-bg-tertiary)] transition-colors ${className}`}
24
+ title={copied ? 'Copied!' : 'Copy to clipboard'}
25
+ >
26
+ {copied ? <Check size={14} /> : <Copy size={14} />}
27
+ </button>
28
+ )
29
+ }
@@ -0,0 +1,35 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { Sidebar } from './sidebar'
5
+ import { Header } from './header'
6
+ import { TableOfContents } from './table-of-contents'
7
+ import { Breadcrumbs } from './breadcrumbs'
8
+
9
+ export function DocsLayout({ children }: { children: React.ReactNode }) {
10
+ const [menuOpen, setMenuOpen] = useState(false)
11
+
12
+ return (
13
+ <div className="min-h-screen">
14
+ <Header
15
+ onMenuToggle={() => setMenuOpen(!menuOpen)}
16
+ menuOpen={menuOpen}
17
+ />
18
+ <div className="flex">
19
+ <Sidebar
20
+ open={menuOpen}
21
+ onClose={() => setMenuOpen(false)}
22
+ />
23
+ <main className="flex-1 min-w-0 px-4 md:px-8 py-6 md:ml-[var(--sidebar-width)] xl:mr-[var(--toc-width)]">
24
+ <div className="max-w-3xl mx-auto">
25
+ <Breadcrumbs />
26
+ <article className="prose">
27
+ {children}
28
+ </article>
29
+ </div>
30
+ </main>
31
+ <TableOfContents />
32
+ </div>
33
+ </div>
34
+ )
35
+ }
@@ -0,0 +1,39 @@
1
+ 'use client'
2
+
3
+ import { usePathname } from 'next/navigation'
4
+ import { Pencil } from 'lucide-react'
5
+
6
+ interface EditLinkProps {
7
+ repoUrl?: string
8
+ branch?: string
9
+ docsPath?: string
10
+ }
11
+
12
+ export function EditLink({
13
+ repoUrl = '',
14
+ branch = 'main',
15
+ docsPath = 'content'
16
+ }: EditLinkProps) {
17
+ const pathname = usePathname()
18
+
19
+ if (!repoUrl) return null
20
+
21
+ // Convert pathname to file path
22
+ const filePath = pathname === '/docs'
23
+ ? `${docsPath}/docs/index.mdx`
24
+ : `${docsPath}${pathname}.mdx`
25
+
26
+ const editUrl = `${repoUrl}/edit/${branch}/${filePath}`
27
+
28
+ return (
29
+ <a
30
+ href={editUrl}
31
+ target="_blank"
32
+ rel="noopener noreferrer"
33
+ className="inline-flex items-center gap-1.5 text-[13px] text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] transition-colors"
34
+ >
35
+ <Pencil size={14} />
36
+ Edit this page
37
+ </a>
38
+ )
39
+ }