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.
- package/LICENSE +21 -0
- package/README.md +200 -0
- package/dist/autofix/index.d.ts +46 -0
- package/dist/autofix/index.js +240 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +40 -0
- package/dist/commands/autofix.d.ts +2 -0
- package/dist/commands/autofix.js +143 -0
- package/dist/commands/generate.d.ts +2 -0
- package/dist/commands/generate.js +320 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +56 -0
- package/dist/commands/review-pr.d.ts +2 -0
- package/dist/commands/review-pr.js +117 -0
- package/dist/commands/watch.d.ts +2 -0
- package/dist/commands/watch.js +142 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +2 -0
- package/dist/config/loader.d.ts +9 -0
- package/dist/config/loader.js +82 -0
- package/dist/config/types.d.ts +24 -0
- package/dist/config/types.js +34 -0
- package/dist/generator/generator.d.ts +15 -0
- package/dist/generator/generator.js +144 -0
- package/dist/generator/index.d.ts +4 -0
- package/dist/generator/index.js +4 -0
- package/dist/generator/organizer.d.ts +29 -0
- package/dist/generator/organizer.js +222 -0
- package/dist/generator/types.d.ts +83 -0
- package/dist/generator/types.js +1 -0
- package/dist/generator/writer.d.ts +28 -0
- package/dist/generator/writer.js +320 -0
- package/dist/github/pr-comments.d.ts +40 -0
- package/dist/github/pr-comments.js +308 -0
- package/dist/llm/anthropic-client.d.ts +16 -0
- package/dist/llm/anthropic-client.js +92 -0
- package/dist/llm/index.d.ts +53 -0
- package/dist/llm/index.js +400 -0
- package/dist/llm/llm.manual-test.d.ts +1 -0
- package/dist/llm/llm.manual-test.js +112 -0
- package/dist/llm/llm.mock-test.d.ts +4 -0
- package/dist/llm/llm.mock-test.js +79 -0
- package/dist/llm/openai-client.d.ts +17 -0
- package/dist/llm/openai-client.js +90 -0
- package/dist/llm/types.d.ts +60 -0
- package/dist/llm/types.js +20 -0
- package/dist/scanner/content-type.d.ts +39 -0
- package/dist/scanner/content-type.js +194 -0
- package/dist/scanner/content-type.test.d.ts +1 -0
- package/dist/scanner/content-type.test.js +231 -0
- package/dist/scanner/go.d.ts +20 -0
- package/dist/scanner/go.js +269 -0
- package/dist/scanner/index.d.ts +21 -0
- package/dist/scanner/index.js +137 -0
- package/dist/scanner/python.d.ts +6 -0
- package/dist/scanner/python.js +57 -0
- package/dist/scanner/python_parser.py +230 -0
- package/dist/scanner/rust.d.ts +23 -0
- package/dist/scanner/rust.js +304 -0
- package/dist/scanner/scanner.test.d.ts +1 -0
- package/dist/scanner/scanner.test.js +210 -0
- package/dist/scanner/types.d.ts +50 -0
- package/dist/scanner/types.js +1 -0
- package/dist/scanner/typescript.d.ts +34 -0
- package/dist/scanner/typescript.js +327 -0
- package/dist/scanner/typescript.manual-test.d.ts +1 -0
- package/dist/scanner/typescript.manual-test.js +112 -0
- package/dist/template/docs.json +32 -0
- package/dist/template/mdx-components.tsx +62 -0
- package/dist/template/next-env.d.ts +6 -0
- package/dist/template/next.config.mjs +17 -0
- package/dist/template/package.json +39 -0
- package/dist/template/postcss.config.mjs +5 -0
- package/dist/template/public/search-index.json +1 -0
- package/dist/template/scripts/build-search-index.mjs +120 -0
- package/dist/template/src/app/api/mock/[...path]/route.ts +224 -0
- package/dist/template/src/app/api/openapi/route.ts +48 -0
- package/dist/template/src/app/api/rate-limit/route.ts +84 -0
- package/dist/template/src/app/docs/[...slug]/page.tsx +81 -0
- package/dist/template/src/app/docs/layout.tsx +9 -0
- package/dist/template/src/app/docs/page.mdx +67 -0
- package/dist/template/src/app/error.tsx +63 -0
- package/dist/template/src/app/layout.tsx +71 -0
- package/dist/template/src/app/page.tsx +18 -0
- package/dist/template/src/app/reference/route.ts +36 -0
- package/dist/template/src/app/robots.ts +14 -0
- package/dist/template/src/app/sitemap.ts +64 -0
- package/dist/template/src/components/breadcrumbs.tsx +41 -0
- package/dist/template/src/components/copy-button.tsx +29 -0
- package/dist/template/src/components/docs-layout.tsx +35 -0
- package/dist/template/src/components/edit-link.tsx +39 -0
- package/dist/template/src/components/feedback.tsx +52 -0
- package/dist/template/src/components/header.tsx +66 -0
- package/dist/template/src/components/mdx/accordion.tsx +48 -0
- package/dist/template/src/components/mdx/api-badge.tsx +57 -0
- package/dist/template/src/components/mdx/callout.tsx +111 -0
- package/dist/template/src/components/mdx/card.tsx +62 -0
- package/dist/template/src/components/mdx/changelog.tsx +57 -0
- package/dist/template/src/components/mdx/code-block.tsx +42 -0
- package/dist/template/src/components/mdx/code-group.tsx +125 -0
- package/dist/template/src/components/mdx/code-playground.tsx +322 -0
- package/dist/template/src/components/mdx/go-playground.tsx +235 -0
- package/dist/template/src/components/mdx/heading.tsx +37 -0
- package/dist/template/src/components/mdx/highlighted-code.tsx +89 -0
- package/dist/template/src/components/mdx/index.tsx +15 -0
- package/dist/template/src/components/mdx/param-table.tsx +71 -0
- package/dist/template/src/components/mdx/python-playground.tsx +293 -0
- package/dist/template/src/components/mdx/steps.tsx +43 -0
- package/dist/template/src/components/mdx/tabs.tsx +81 -0
- package/dist/template/src/components/rate-limit-display.tsx +183 -0
- package/dist/template/src/components/search-dialog.tsx +178 -0
- package/dist/template/src/components/sidebar.tsx +129 -0
- package/dist/template/src/components/syntax-theme-selector.tsx +50 -0
- package/dist/template/src/components/table-of-contents.tsx +84 -0
- package/dist/template/src/components/theme-toggle.tsx +46 -0
- package/dist/template/src/components/version-selector.tsx +61 -0
- package/dist/template/src/contexts/syntax-theme.tsx +52 -0
- package/dist/template/src/lib/highlight.ts +83 -0
- package/dist/template/src/lib/search-types.ts +37 -0
- package/dist/template/src/lib/search.ts +125 -0
- package/dist/template/src/lib/utils.ts +6 -0
- package/dist/template/src/styles/globals.css +152 -0
- package/dist/template/tsconfig.json +25 -0
- package/dist/template/tsconfig.tsbuildinfo +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
SandpackProvider,
|
|
5
|
+
SandpackLayout,
|
|
6
|
+
SandpackCodeEditor,
|
|
7
|
+
SandpackPreview,
|
|
8
|
+
SandpackConsole,
|
|
9
|
+
useSandpack,
|
|
10
|
+
type SandpackTheme,
|
|
11
|
+
} from '@codesandbox/sandpack-react'
|
|
12
|
+
import { useState, useEffect } from 'react'
|
|
13
|
+
import { RotateCcw, Download, Play, Pause } from 'lucide-react'
|
|
14
|
+
|
|
15
|
+
// Custom themes that match our CSS variables
|
|
16
|
+
const lightTheme: SandpackTheme = {
|
|
17
|
+
colors: {
|
|
18
|
+
surface1: '#f8fafc',
|
|
19
|
+
surface2: '#f1f5f9',
|
|
20
|
+
surface3: '#e2e8f0',
|
|
21
|
+
clickable: '#64748b',
|
|
22
|
+
base: '#0f172a',
|
|
23
|
+
disabled: '#94a3b8',
|
|
24
|
+
hover: '#475569',
|
|
25
|
+
accent: '#3b82f6',
|
|
26
|
+
error: '#ef4444',
|
|
27
|
+
errorSurface: '#fef2f2',
|
|
28
|
+
},
|
|
29
|
+
syntax: {
|
|
30
|
+
plain: '#0f172a',
|
|
31
|
+
comment: { color: '#94a3b8', fontStyle: 'italic' },
|
|
32
|
+
keyword: '#8b5cf6',
|
|
33
|
+
tag: '#3b82f6',
|
|
34
|
+
punctuation: '#64748b',
|
|
35
|
+
definition: '#0f172a',
|
|
36
|
+
property: '#0891b2',
|
|
37
|
+
static: '#059669',
|
|
38
|
+
string: '#16a34a',
|
|
39
|
+
},
|
|
40
|
+
font: {
|
|
41
|
+
body: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
42
|
+
mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
|
43
|
+
size: '14px',
|
|
44
|
+
lineHeight: '1.6',
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const darkTheme: SandpackTheme = {
|
|
49
|
+
colors: {
|
|
50
|
+
surface1: '#1e293b',
|
|
51
|
+
surface2: '#334155',
|
|
52
|
+
surface3: '#475569',
|
|
53
|
+
clickable: '#94a3b8',
|
|
54
|
+
base: '#f8fafc',
|
|
55
|
+
disabled: '#64748b',
|
|
56
|
+
hover: '#cbd5e1',
|
|
57
|
+
accent: '#60a5fa',
|
|
58
|
+
error: '#f87171',
|
|
59
|
+
errorSurface: '#450a0a',
|
|
60
|
+
},
|
|
61
|
+
syntax: {
|
|
62
|
+
plain: '#e2e8f0',
|
|
63
|
+
comment: { color: '#64748b', fontStyle: 'italic' },
|
|
64
|
+
keyword: '#c4b5fd',
|
|
65
|
+
tag: '#93c5fd',
|
|
66
|
+
punctuation: '#94a3b8',
|
|
67
|
+
definition: '#f8fafc',
|
|
68
|
+
property: '#67e8f9',
|
|
69
|
+
static: '#6ee7b7',
|
|
70
|
+
string: '#86efac',
|
|
71
|
+
},
|
|
72
|
+
font: {
|
|
73
|
+
body: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
74
|
+
mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
|
75
|
+
size: '14px',
|
|
76
|
+
lineHeight: '1.6',
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Hook to detect dark mode
|
|
81
|
+
function useDarkMode(): boolean {
|
|
82
|
+
const [isDark, setIsDark] = useState(false)
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
// Check initial state
|
|
86
|
+
const checkDark = () => {
|
|
87
|
+
const root = document.documentElement
|
|
88
|
+
const hasDarkClass = root.classList.contains('dark')
|
|
89
|
+
const prefersLight = root.classList.contains('light')
|
|
90
|
+
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
91
|
+
|
|
92
|
+
return hasDarkClass || (!prefersLight && systemDark)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setIsDark(checkDark())
|
|
96
|
+
|
|
97
|
+
// Listen for class changes on document
|
|
98
|
+
const observer = new MutationObserver(() => {
|
|
99
|
+
setIsDark(checkDark())
|
|
100
|
+
})
|
|
101
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
|
|
102
|
+
|
|
103
|
+
// Listen for system preference changes
|
|
104
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
|
105
|
+
const handleChange = () => setIsDark(checkDark())
|
|
106
|
+
mediaQuery.addEventListener('change', handleChange)
|
|
107
|
+
|
|
108
|
+
return () => {
|
|
109
|
+
observer.disconnect()
|
|
110
|
+
mediaQuery.removeEventListener('change', handleChange)
|
|
111
|
+
}
|
|
112
|
+
}, [])
|
|
113
|
+
|
|
114
|
+
return isDark
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type Template = 'react' | 'react-ts' | 'vanilla' | 'vanilla-ts' | 'node'
|
|
118
|
+
|
|
119
|
+
interface CodePlaygroundProps {
|
|
120
|
+
code: string
|
|
121
|
+
template?: Template
|
|
122
|
+
showPreview?: boolean
|
|
123
|
+
showConsole?: boolean
|
|
124
|
+
editable?: boolean
|
|
125
|
+
dependencies?: Record<string, string>
|
|
126
|
+
filename?: string
|
|
127
|
+
autoRun?: boolean
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Toolbar component with reset/download
|
|
131
|
+
function PlaygroundToolbar({ originalCode, filename, autoRun, onAutoRunChange }: {
|
|
132
|
+
originalCode: string
|
|
133
|
+
filename: string
|
|
134
|
+
autoRun: boolean
|
|
135
|
+
onAutoRunChange: (v: boolean) => void
|
|
136
|
+
}) {
|
|
137
|
+
const { sandpack } = useSandpack()
|
|
138
|
+
|
|
139
|
+
const handleReset = () => {
|
|
140
|
+
sandpack.resetAllFiles()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const handleDownload = () => {
|
|
144
|
+
const code = sandpack.files[filename]?.code || originalCode
|
|
145
|
+
const blob = new Blob([code], { type: 'text/plain' })
|
|
146
|
+
const url = URL.createObjectURL(blob)
|
|
147
|
+
const a = document.createElement('a')
|
|
148
|
+
a.href = url
|
|
149
|
+
a.download = filename.replace('/', '')
|
|
150
|
+
a.click()
|
|
151
|
+
URL.revokeObjectURL(url)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<div className="flex items-center gap-1 px-2 py-1 bg-[var(--color-bg-secondary)] border-b border-[var(--color-border)]">
|
|
156
|
+
<button
|
|
157
|
+
onClick={handleReset}
|
|
158
|
+
className="p-1.5 text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] rounded"
|
|
159
|
+
title="Reset code"
|
|
160
|
+
>
|
|
161
|
+
<RotateCcw size={14} />
|
|
162
|
+
</button>
|
|
163
|
+
<button
|
|
164
|
+
onClick={handleDownload}
|
|
165
|
+
className="p-1.5 text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] rounded"
|
|
166
|
+
title="Download code"
|
|
167
|
+
>
|
|
168
|
+
<Download size={14} />
|
|
169
|
+
</button>
|
|
170
|
+
<button
|
|
171
|
+
onClick={() => onAutoRunChange(!autoRun)}
|
|
172
|
+
className={`p-1.5 rounded ${autoRun ? 'text-emerald-500' : 'text-[var(--color-text-tertiary)]'} hover:text-[var(--color-text)]`}
|
|
173
|
+
title={autoRun ? 'Auto-run enabled' : 'Auto-run disabled'}
|
|
174
|
+
>
|
|
175
|
+
{autoRun ? <Play size={14} /> : <Pause size={14} />}
|
|
176
|
+
</button>
|
|
177
|
+
</div>
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const templateConfig: Record<Template, { files: (code: string, filename?: string) => Record<string, string>, entry: string }> = {
|
|
182
|
+
'react': {
|
|
183
|
+
files: (code, filename) => ({
|
|
184
|
+
[filename || '/App.js']: code,
|
|
185
|
+
}),
|
|
186
|
+
entry: '/App.js',
|
|
187
|
+
},
|
|
188
|
+
'react-ts': {
|
|
189
|
+
files: (code, filename) => ({
|
|
190
|
+
[filename || '/App.tsx']: code,
|
|
191
|
+
}),
|
|
192
|
+
entry: '/App.tsx',
|
|
193
|
+
},
|
|
194
|
+
'vanilla': {
|
|
195
|
+
files: (code, filename) => ({
|
|
196
|
+
[filename || '/index.js']: code,
|
|
197
|
+
'/index.html': `<!DOCTYPE html>
|
|
198
|
+
<html>
|
|
199
|
+
<head><title>Preview</title></head>
|
|
200
|
+
<body>
|
|
201
|
+
<div id="app"></div>
|
|
202
|
+
<script src="${filename || '/index.js'}"></script>
|
|
203
|
+
</body>
|
|
204
|
+
</html>`,
|
|
205
|
+
}),
|
|
206
|
+
entry: '/index.js',
|
|
207
|
+
},
|
|
208
|
+
'vanilla-ts': {
|
|
209
|
+
files: (code, filename) => ({
|
|
210
|
+
[filename || '/index.ts']: code,
|
|
211
|
+
'/index.html': `<!DOCTYPE html>
|
|
212
|
+
<html>
|
|
213
|
+
<head><title>Preview</title></head>
|
|
214
|
+
<body>
|
|
215
|
+
<div id="app"></div>
|
|
216
|
+
<script src="${filename || '/index.ts'}"></script>
|
|
217
|
+
</body>
|
|
218
|
+
</html>`,
|
|
219
|
+
}),
|
|
220
|
+
entry: '/index.ts',
|
|
221
|
+
},
|
|
222
|
+
'node': {
|
|
223
|
+
files: (code, filename) => ({
|
|
224
|
+
[filename || '/index.js']: code,
|
|
225
|
+
}),
|
|
226
|
+
entry: '/index.js',
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function CodePlayground({
|
|
231
|
+
code,
|
|
232
|
+
template = 'react-ts',
|
|
233
|
+
showPreview = true,
|
|
234
|
+
showConsole = false,
|
|
235
|
+
editable = true,
|
|
236
|
+
dependencies = {},
|
|
237
|
+
filename,
|
|
238
|
+
autoRun: initialAutoRun = true,
|
|
239
|
+
}: CodePlaygroundProps) {
|
|
240
|
+
const [activeTab, setActiveTab] = useState<'preview' | 'console'>('preview')
|
|
241
|
+
const [autoRun, setAutoRun] = useState(initialAutoRun)
|
|
242
|
+
const isDark = useDarkMode()
|
|
243
|
+
const config = templateConfig[template]
|
|
244
|
+
const files = config.files(code, filename)
|
|
245
|
+
const entryFile = filename || config.entry
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<div className="my-6 rounded-lg border border-[var(--color-border)] overflow-hidden">
|
|
249
|
+
<SandpackProvider
|
|
250
|
+
template={template === 'node' ? 'node' : template}
|
|
251
|
+
theme={isDark ? darkTheme : lightTheme}
|
|
252
|
+
files={files}
|
|
253
|
+
customSetup={{
|
|
254
|
+
dependencies,
|
|
255
|
+
}}
|
|
256
|
+
options={{
|
|
257
|
+
externalResources: [],
|
|
258
|
+
recompileMode: autoRun ? 'delayed' : 'immediate',
|
|
259
|
+
recompileDelay: 500,
|
|
260
|
+
autorun: autoRun,
|
|
261
|
+
}}
|
|
262
|
+
>
|
|
263
|
+
<PlaygroundToolbar
|
|
264
|
+
originalCode={code}
|
|
265
|
+
filename={entryFile}
|
|
266
|
+
autoRun={autoRun}
|
|
267
|
+
onAutoRunChange={setAutoRun}
|
|
268
|
+
/>
|
|
269
|
+
<SandpackLayout>
|
|
270
|
+
<SandpackCodeEditor
|
|
271
|
+
showTabs
|
|
272
|
+
showLineNumbers
|
|
273
|
+
showInlineErrors
|
|
274
|
+
wrapContent
|
|
275
|
+
readOnly={!editable}
|
|
276
|
+
style={{ minHeight: 300 }}
|
|
277
|
+
/>
|
|
278
|
+
{(showPreview || showConsole) && (
|
|
279
|
+
<div className="flex flex-col w-full">
|
|
280
|
+
{showPreview && showConsole && (
|
|
281
|
+
<div className="flex border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
|
|
282
|
+
<button
|
|
283
|
+
onClick={() => setActiveTab('preview')}
|
|
284
|
+
className={`px-4 py-2 text-sm font-medium ${
|
|
285
|
+
activeTab === 'preview'
|
|
286
|
+
? 'text-blue-600 border-b-2 border-blue-600'
|
|
287
|
+
: 'text-gray-600 dark:text-gray-400'
|
|
288
|
+
}`}
|
|
289
|
+
>
|
|
290
|
+
Preview
|
|
291
|
+
</button>
|
|
292
|
+
<button
|
|
293
|
+
onClick={() => setActiveTab('console')}
|
|
294
|
+
className={`px-4 py-2 text-sm font-medium ${
|
|
295
|
+
activeTab === 'console'
|
|
296
|
+
? 'text-blue-600 border-b-2 border-blue-600'
|
|
297
|
+
: 'text-gray-600 dark:text-gray-400'
|
|
298
|
+
}`}
|
|
299
|
+
>
|
|
300
|
+
Console
|
|
301
|
+
</button>
|
|
302
|
+
</div>
|
|
303
|
+
)}
|
|
304
|
+
{showPreview && (!showConsole || activeTab === 'preview') && (
|
|
305
|
+
<SandpackPreview
|
|
306
|
+
showNavigator={false}
|
|
307
|
+
showRefreshButton
|
|
308
|
+
style={{ minHeight: 300 }}
|
|
309
|
+
/>
|
|
310
|
+
)}
|
|
311
|
+
{showConsole && (!showPreview || activeTab === 'console') && (
|
|
312
|
+
<SandpackConsole style={{ minHeight: 300 }} />
|
|
313
|
+
)}
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
316
|
+
</SandpackLayout>
|
|
317
|
+
</SandpackProvider>
|
|
318
|
+
</div>
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export default CodePlayground
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useCallback } from 'react'
|
|
4
|
+
import { Play, RotateCcw, Download, Loader2, ExternalLink } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
interface GoPlaygroundProps {
|
|
7
|
+
code: string
|
|
8
|
+
filename?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type OutputLine = {
|
|
12
|
+
type: 'stdout' | 'stderr' | 'error'
|
|
13
|
+
content: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const GO_PLAYGROUND_URL = 'https://go.dev/_/compile'
|
|
17
|
+
|
|
18
|
+
export function GoPlayground({
|
|
19
|
+
code: initialCode,
|
|
20
|
+
filename = 'main.go',
|
|
21
|
+
}: GoPlaygroundProps) {
|
|
22
|
+
const [code, setCode] = useState(initialCode)
|
|
23
|
+
const [output, setOutput] = useState<OutputLine[]>([])
|
|
24
|
+
const [isRunning, setIsRunning] = useState(false)
|
|
25
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
|
26
|
+
|
|
27
|
+
const runCode = useCallback(async () => {
|
|
28
|
+
if (isRunning) return
|
|
29
|
+
|
|
30
|
+
setIsRunning(true)
|
|
31
|
+
setOutput([])
|
|
32
|
+
|
|
33
|
+
// Create abort controller with 30s timeout
|
|
34
|
+
const controller = new AbortController()
|
|
35
|
+
const timeoutId = setTimeout(() => controller.abort(), 30000)
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Use Go Playground API
|
|
39
|
+
const response = await fetch(GO_PLAYGROUND_URL, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: {
|
|
42
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
43
|
+
},
|
|
44
|
+
body: new URLSearchParams({
|
|
45
|
+
version: '2',
|
|
46
|
+
body: code,
|
|
47
|
+
withVet: 'true',
|
|
48
|
+
}),
|
|
49
|
+
signal: controller.signal,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
throw new Error(`HTTP ${response.status}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const result = await response.json()
|
|
57
|
+
|
|
58
|
+
const lines: OutputLine[] = []
|
|
59
|
+
|
|
60
|
+
if (result.Errors) {
|
|
61
|
+
lines.push({ type: 'error', content: result.Errors })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (result.Events) {
|
|
65
|
+
for (const event of result.Events) {
|
|
66
|
+
if (event.Kind === 'stdout') {
|
|
67
|
+
lines.push({ type: 'stdout', content: event.Message })
|
|
68
|
+
} else if (event.Kind === 'stderr') {
|
|
69
|
+
lines.push({ type: 'stderr', content: event.Message })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (result.VetErrors) {
|
|
75
|
+
lines.push({ type: 'stderr', content: `Vet: ${result.VetErrors}` })
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
setOutput(lines.length > 0 ? lines : [{ type: 'stdout', content: '(no output)' }])
|
|
79
|
+
} catch (err: unknown) {
|
|
80
|
+
let message: string
|
|
81
|
+
if (err instanceof Error) {
|
|
82
|
+
if (err.name === 'AbortError') {
|
|
83
|
+
message = 'Request timed out after 30 seconds. Try the Go Playground directly.'
|
|
84
|
+
} else {
|
|
85
|
+
message = `Failed to run: ${err.message}. Try the Go Playground directly.`
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
message = `Failed to run: ${String(err)}. Try the Go Playground directly.`
|
|
89
|
+
}
|
|
90
|
+
setOutput([{ type: 'error', content: message }])
|
|
91
|
+
} finally {
|
|
92
|
+
clearTimeout(timeoutId)
|
|
93
|
+
setIsRunning(false)
|
|
94
|
+
}
|
|
95
|
+
}, [code, isRunning])
|
|
96
|
+
|
|
97
|
+
const handleReset = () => {
|
|
98
|
+
setCode(initialCode)
|
|
99
|
+
setOutput([])
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const handleDownload = () => {
|
|
103
|
+
const blob = new Blob([code], { type: 'text/plain' })
|
|
104
|
+
const url = URL.createObjectURL(blob)
|
|
105
|
+
const a = document.createElement('a')
|
|
106
|
+
a.href = url
|
|
107
|
+
a.download = filename
|
|
108
|
+
a.click()
|
|
109
|
+
URL.revokeObjectURL(url)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const openInPlayground = () => {
|
|
113
|
+
// Encode code for Go Playground URL
|
|
114
|
+
const encoded = btoa(unescape(encodeURIComponent(code)))
|
|
115
|
+
window.open(`https://go.dev/play/p/?source=${encoded}`, '_blank')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
119
|
+
// Run on Ctrl/Cmd + Enter
|
|
120
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
121
|
+
e.preventDefault()
|
|
122
|
+
runCode()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Tab handling
|
|
126
|
+
if (e.key === 'Tab') {
|
|
127
|
+
e.preventDefault()
|
|
128
|
+
const start = textareaRef.current?.selectionStart || 0
|
|
129
|
+
const end = textareaRef.current?.selectionEnd || 0
|
|
130
|
+
const newCode = code.slice(0, start) + '\t' + code.slice(end)
|
|
131
|
+
setCode(newCode)
|
|
132
|
+
setTimeout(() => {
|
|
133
|
+
if (textareaRef.current) {
|
|
134
|
+
textareaRef.current.selectionStart = textareaRef.current.selectionEnd = start + 1
|
|
135
|
+
}
|
|
136
|
+
}, 0)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div className="my-6 rounded-lg border border-[var(--color-border)] overflow-hidden">
|
|
142
|
+
{/* Toolbar */}
|
|
143
|
+
<div className="flex items-center justify-between px-3 py-2 bg-[var(--color-bg-secondary)] border-b border-[var(--color-border)]">
|
|
144
|
+
<div className="flex items-center gap-2">
|
|
145
|
+
<span className="text-xs font-medium text-[var(--color-text-secondary)]">
|
|
146
|
+
🐹 Go
|
|
147
|
+
</span>
|
|
148
|
+
<span className="text-xs text-[var(--color-text-tertiary)]">
|
|
149
|
+
{filename}
|
|
150
|
+
</span>
|
|
151
|
+
</div>
|
|
152
|
+
<div className="flex items-center gap-1">
|
|
153
|
+
<button
|
|
154
|
+
onClick={handleReset}
|
|
155
|
+
className="p-1.5 text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] rounded"
|
|
156
|
+
title="Reset code"
|
|
157
|
+
>
|
|
158
|
+
<RotateCcw size={14} />
|
|
159
|
+
</button>
|
|
160
|
+
<button
|
|
161
|
+
onClick={handleDownload}
|
|
162
|
+
className="p-1.5 text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] rounded"
|
|
163
|
+
title="Download code"
|
|
164
|
+
>
|
|
165
|
+
<Download size={14} />
|
|
166
|
+
</button>
|
|
167
|
+
<button
|
|
168
|
+
onClick={openInPlayground}
|
|
169
|
+
className="p-1.5 text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] rounded"
|
|
170
|
+
title="Open in Go Playground"
|
|
171
|
+
>
|
|
172
|
+
<ExternalLink size={14} />
|
|
173
|
+
</button>
|
|
174
|
+
<button
|
|
175
|
+
onClick={runCode}
|
|
176
|
+
disabled={isRunning}
|
|
177
|
+
className="flex items-center gap-1 px-2 py-1 text-xs font-medium bg-cyan-600 text-white rounded hover:bg-cyan-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
178
|
+
title="Run (Ctrl+Enter)"
|
|
179
|
+
>
|
|
180
|
+
{isRunning ? (
|
|
181
|
+
<>
|
|
182
|
+
<Loader2 size={12} className="animate-spin" />
|
|
183
|
+
Running
|
|
184
|
+
</>
|
|
185
|
+
) : (
|
|
186
|
+
<>
|
|
187
|
+
<Play size={12} />
|
|
188
|
+
Run
|
|
189
|
+
</>
|
|
190
|
+
)}
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{/* Code editor */}
|
|
196
|
+
<div className="relative">
|
|
197
|
+
<textarea
|
|
198
|
+
ref={textareaRef}
|
|
199
|
+
value={code}
|
|
200
|
+
onChange={(e) => setCode(e.target.value)}
|
|
201
|
+
onKeyDown={handleKeyDown}
|
|
202
|
+
spellCheck={false}
|
|
203
|
+
className="w-full min-h-[200px] p-4 font-mono text-sm bg-[var(--color-bg)] text-[var(--color-text)] resize-none focus:outline-none"
|
|
204
|
+
style={{
|
|
205
|
+
tabSize: 4,
|
|
206
|
+
}}
|
|
207
|
+
/>
|
|
208
|
+
<div className="absolute top-2 right-2 text-xs text-[var(--color-text-tertiary)]">
|
|
209
|
+
Ctrl+Enter to run
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
{/* Output */}
|
|
214
|
+
{output.length > 0 && (
|
|
215
|
+
<div className="border-t border-[var(--color-border)] bg-[var(--color-code-bg)] text-[var(--color-code-text)] p-4 font-mono text-sm max-h-[300px] overflow-auto">
|
|
216
|
+
<div className="text-xs text-[var(--color-text-tertiary)] mb-2">Output:</div>
|
|
217
|
+
{output.map((line, i) => (
|
|
218
|
+
<pre
|
|
219
|
+
key={i}
|
|
220
|
+
className={`whitespace-pre-wrap ${
|
|
221
|
+
line.type === 'stderr' || line.type === 'error'
|
|
222
|
+
? 'text-red-400'
|
|
223
|
+
: ''
|
|
224
|
+
}`}
|
|
225
|
+
>
|
|
226
|
+
{line.content}
|
|
227
|
+
</pre>
|
|
228
|
+
))}
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export default GoPlayground
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Link2 } from 'lucide-react'
|
|
4
|
+
import { ReactNode, createElement } from 'react'
|
|
5
|
+
|
|
6
|
+
interface HeadingProps {
|
|
7
|
+
level: 1 | 2 | 3 | 4 | 5 | 6
|
|
8
|
+
children: ReactNode
|
|
9
|
+
id?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Heading({ level, children, id }: HeadingProps) {
|
|
13
|
+
const Tag = `h${level}` as 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
|
|
14
|
+
const headingId = id || (typeof children === 'string'
|
|
15
|
+
? children.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '')
|
|
16
|
+
: undefined)
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Tag id={headingId} className="group relative scroll-mt-20">
|
|
20
|
+
{children}
|
|
21
|
+
{headingId && (
|
|
22
|
+
<a
|
|
23
|
+
href={`#${headingId}`}
|
|
24
|
+
className="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity p-1 text-[var(--color-text-tertiary)] hover:text-[var(--color-text)]"
|
|
25
|
+
aria-label="Link to this section"
|
|
26
|
+
>
|
|
27
|
+
<Link2 size={16} />
|
|
28
|
+
</a>
|
|
29
|
+
)}
|
|
30
|
+
</Tag>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const H1 = (props: Omit<HeadingProps, 'level'>) => <Heading level={1} {...props} />
|
|
35
|
+
export const H2 = (props: Omit<HeadingProps, 'level'>) => <Heading level={2} {...props} />
|
|
36
|
+
export const H3 = (props: Omit<HeadingProps, 'level'>) => <Heading level={3} {...props} />
|
|
37
|
+
export const H4 = (props: Omit<HeadingProps, 'level'>) => <Heading level={4} {...props} />
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useContext, ReactNode, isValidElement, Children } from 'react'
|
|
4
|
+
import { Copy, Check } from 'lucide-react'
|
|
5
|
+
import { highlight, DEFAULT_THEME, type ThemeName } from '@/lib/highlight'
|
|
6
|
+
|
|
7
|
+
// Import the context directly to use useContext safely
|
|
8
|
+
import { SyntaxThemeContext } from '@/contexts/syntax-theme'
|
|
9
|
+
|
|
10
|
+
interface HighlightedCodeProps {
|
|
11
|
+
children: ReactNode
|
|
12
|
+
className?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function HighlightedCode({ children, className }: HighlightedCodeProps) {
|
|
16
|
+
const [copied, setCopied] = useState(false)
|
|
17
|
+
const [highlightedHtml, setHighlightedHtml] = useState<string | null>(null)
|
|
18
|
+
|
|
19
|
+
// Use context safely - fallback to default if not available
|
|
20
|
+
const syntaxContext = useContext(SyntaxThemeContext)
|
|
21
|
+
const theme: ThemeName = syntaxContext?.theme ?? DEFAULT_THEME
|
|
22
|
+
|
|
23
|
+
// Extract code text and language from children
|
|
24
|
+
let codeText = ''
|
|
25
|
+
let language = 'text'
|
|
26
|
+
|
|
27
|
+
Children.forEach(children, (child) => {
|
|
28
|
+
if (isValidElement(child) && child.type === 'code') {
|
|
29
|
+
const props = child.props as { children?: string; className?: string }
|
|
30
|
+
codeText = props.children || ''
|
|
31
|
+
// Extract language from className like "language-typescript"
|
|
32
|
+
const classMatch = props.className?.match(/language-(\w+)/)
|
|
33
|
+
if (classMatch) {
|
|
34
|
+
language = classMatch[1]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
let cancelled = false
|
|
41
|
+
|
|
42
|
+
async function doHighlight() {
|
|
43
|
+
if (!codeText) return
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const html = await highlight(codeText, language, theme)
|
|
47
|
+
if (!cancelled) {
|
|
48
|
+
setHighlightedHtml(html)
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error('Highlight failed:', err)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
doHighlight()
|
|
56
|
+
|
|
57
|
+
return () => {
|
|
58
|
+
cancelled = true
|
|
59
|
+
}
|
|
60
|
+
}, [codeText, language, theme])
|
|
61
|
+
|
|
62
|
+
async function handleCopy() {
|
|
63
|
+
await navigator.clipboard.writeText(codeText)
|
|
64
|
+
setCopied(true)
|
|
65
|
+
setTimeout(() => setCopied(false), 2000)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="relative group my-4">
|
|
70
|
+
{highlightedHtml ? (
|
|
71
|
+
<div
|
|
72
|
+
className="[&>pre]:rounded-lg [&>pre]:p-4 [&>pre]:overflow-x-auto"
|
|
73
|
+
dangerouslySetInnerHTML={{ __html: highlightedHtml }}
|
|
74
|
+
/>
|
|
75
|
+
) : (
|
|
76
|
+
<pre className={className}>
|
|
77
|
+
{children}
|
|
78
|
+
</pre>
|
|
79
|
+
)}
|
|
80
|
+
<button
|
|
81
|
+
onClick={handleCopy}
|
|
82
|
+
className="absolute top-2 right-2 p-1.5 rounded bg-[var(--color-bg-tertiary)]/80 text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] opacity-0 group-hover:opacity-100 transition-opacity"
|
|
83
|
+
title={copied ? 'Copied!' : 'Copy code'}
|
|
84
|
+
>
|
|
85
|
+
{copied ? <Check size={14} /> : <Copy size={14} />}
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { Card, CardGroup } from './card'
|
|
2
|
+
export { Tabs, TabList, Tab, TabPanel } from './tabs'
|
|
3
|
+
export { CodeGroup } from './code-group'
|
|
4
|
+
export { CodeBlock } from './code-block'
|
|
5
|
+
export { HighlightedCode } from './highlighted-code'
|
|
6
|
+
export { Callout, Info, Warning, Success, Error, Tip, Note } from './callout'
|
|
7
|
+
export { Accordion, AccordionGroup } from './accordion'
|
|
8
|
+
export { Steps, Step } from './steps'
|
|
9
|
+
export { CodePlayground } from './code-playground'
|
|
10
|
+
export { PythonPlayground } from './python-playground'
|
|
11
|
+
export { GoPlayground } from './go-playground'
|
|
12
|
+
export { H1, H2, H3, H4 } from './heading'
|
|
13
|
+
export { ParamTable, Schema } from './param-table'
|
|
14
|
+
export { Changelog, ChangelogEntry, Change } from './changelog'
|
|
15
|
+
export { MethodBadge, StatusBadge, Endpoint } from './api-badge'
|