skrypt-ai 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/auth/index.d.ts +0 -1
- package/dist/auth/index.js +3 -5
- package/dist/autofix/index.js +15 -3
- package/dist/cli.js +19 -4
- package/dist/commands/check-links.js +164 -174
- package/dist/commands/deploy.js +5 -2
- package/dist/commands/generate.js +206 -199
- package/dist/commands/i18n.js +3 -20
- package/dist/commands/init.js +47 -40
- package/dist/commands/lint.js +3 -20
- package/dist/commands/mcp.js +125 -122
- package/dist/commands/monitor.js +125 -108
- package/dist/commands/review-pr.js +1 -1
- package/dist/commands/sdk.js +1 -1
- package/dist/config/loader.js +21 -2
- package/dist/generator/organizer.d.ts +3 -0
- package/dist/generator/organizer.js +4 -9
- package/dist/generator/writer.js +2 -10
- package/dist/github/pr-comments.js +21 -8
- package/dist/plugins/index.js +1 -0
- package/dist/scanner/index.js +8 -2
- package/dist/template/docs.json +2 -1
- package/dist/template/next.config.mjs +3 -1
- package/dist/template/package.json +17 -14
- package/dist/template/public/favicon.svg +4 -0
- package/dist/template/public/search-index.json +1 -1
- package/dist/template/scripts/build-search-index.mjs +120 -25
- package/dist/template/src/app/api/chat/route.ts +11 -3
- package/dist/template/src/app/docs/README.md +28 -0
- package/dist/template/src/app/docs/[...slug]/page.tsx +141 -14
- package/dist/template/src/app/docs/auth/page.mdx +589 -0
- package/dist/template/src/app/docs/autofix/page.mdx +624 -0
- package/dist/template/src/app/docs/cli/page.mdx +217 -0
- package/dist/template/src/app/docs/config/page.mdx +428 -0
- package/dist/template/src/app/docs/configuration/page.mdx +86 -0
- package/dist/template/src/app/docs/deployment/page.mdx +112 -0
- package/dist/template/src/app/docs/error.tsx +20 -0
- package/dist/template/src/app/docs/generator/generator.md +504 -0
- package/dist/template/src/app/docs/generator/organizer.md +779 -0
- package/dist/template/src/app/docs/generator/page.mdx +613 -0
- package/dist/template/src/app/docs/github/page.mdx +502 -0
- package/dist/template/src/app/docs/llm/anthropic-client.md +549 -0
- package/dist/template/src/app/docs/llm/index.md +471 -0
- package/dist/template/src/app/docs/llm/page.mdx +428 -0
- package/dist/template/src/app/docs/llms-full.md +256 -0
- package/dist/template/src/app/docs/llms.txt +2971 -0
- package/dist/template/src/app/docs/not-found.tsx +23 -0
- package/dist/template/src/app/docs/page.mdx +0 -3
- package/dist/template/src/app/docs/plugins/page.mdx +1793 -0
- package/dist/template/src/app/docs/pro/page.mdx +121 -0
- package/dist/template/src/app/docs/quickstart/page.mdx +93 -0
- package/dist/template/src/app/docs/scanner/content-type.md +599 -0
- package/dist/template/src/app/docs/scanner/index.md +212 -0
- package/dist/template/src/app/docs/scanner/page.mdx +307 -0
- package/dist/template/src/app/docs/scanner/python.md +469 -0
- package/dist/template/src/app/docs/scanner/python_parser.md +1056 -0
- package/dist/template/src/app/docs/scanner/rust.md +325 -0
- package/dist/template/src/app/docs/scanner/typescript.md +201 -0
- package/dist/template/src/app/error.tsx +3 -3
- package/dist/template/src/app/icon.tsx +29 -0
- package/dist/template/src/app/layout.tsx +57 -7
- package/dist/template/src/app/not-found.tsx +35 -0
- package/dist/template/src/app/page.tsx +95 -11
- package/dist/template/src/components/ai-chat.tsx +26 -21
- package/dist/template/src/components/breadcrumbs.tsx +56 -12
- package/dist/template/src/components/copy-button.tsx +17 -3
- package/dist/template/src/components/docs-layout.tsx +202 -8
- package/dist/template/src/components/feedback.tsx +4 -2
- package/dist/template/src/components/footer.tsx +42 -0
- package/dist/template/src/components/header.tsx +56 -20
- package/dist/template/src/components/mdx/accordion.tsx +17 -13
- package/dist/template/src/components/mdx/callout.tsx +50 -37
- package/dist/template/src/components/mdx/card.tsx +24 -12
- package/dist/template/src/components/mdx/code-block.tsx +17 -3
- package/dist/template/src/components/mdx/code-group.tsx +78 -18
- package/dist/template/src/components/mdx/code-playground.tsx +3 -0
- package/dist/template/src/components/mdx/go-playground.tsx +3 -0
- package/dist/template/src/components/mdx/highlighted-code.tsx +178 -38
- package/dist/template/src/components/mdx/python-playground.tsx +2 -0
- package/dist/template/src/components/mdx/steps.tsx +6 -6
- package/dist/template/src/components/mdx/tabs.tsx +76 -8
- package/dist/template/src/components/page-header.tsx +19 -0
- package/dist/template/src/components/scroll-to-top.tsx +33 -0
- package/dist/template/src/components/search-dialog.tsx +251 -57
- package/dist/template/src/components/sidebar.tsx +137 -77
- package/dist/template/src/components/table-of-contents.tsx +29 -13
- package/dist/template/src/lib/highlight.ts +90 -31
- package/dist/template/src/lib/search.ts +14 -4
- package/dist/template/src/lib/theme-utils.ts +140 -0
- package/dist/template/src/styles/globals.css +397 -84
- package/dist/template/src/types/remark-gfm.d.ts +2 -0
- package/dist/utils/files.d.ts +9 -0
- package/dist/utils/files.js +33 -0
- package/dist/utils/validation.d.ts +4 -0
- package/dist/utils/validation.js +38 -0
- package/package.json +1 -4
|
@@ -1,32 +1,44 @@
|
|
|
1
1
|
import Link from 'next/link'
|
|
2
2
|
import { cn } from '@/lib/utils'
|
|
3
|
-
import
|
|
3
|
+
import {
|
|
4
|
+
BookOpen, Code, FileText, Settings, Key, Zap, Shield, Globe,
|
|
5
|
+
Terminal, Database, Cloud, Lock, Rocket, Search, Star, Heart,
|
|
6
|
+
AlertTriangle, CheckCircle, Info, HelpCircle, ArrowRight,
|
|
7
|
+
ExternalLink, Package, Puzzle, GitBranch, Cpu
|
|
8
|
+
} from 'lucide-react'
|
|
9
|
+
|
|
10
|
+
const iconMap: Record<string, React.ComponentType<{ size?: number; className?: string }>> = {
|
|
11
|
+
BookOpen, Code, FileText, Settings, Key, Zap, Shield, Globe,
|
|
12
|
+
Terminal, Database, Cloud, Lock, Rocket, Search, Star, Heart,
|
|
13
|
+
AlertTriangle, CheckCircle, Info, HelpCircle, ArrowRight,
|
|
14
|
+
ExternalLink, Package, Puzzle, GitBranch, Cpu
|
|
15
|
+
}
|
|
4
16
|
|
|
5
17
|
interface CardProps {
|
|
6
18
|
title: string
|
|
7
|
-
icon?:
|
|
19
|
+
icon?: string
|
|
8
20
|
href?: string
|
|
9
21
|
children?: React.ReactNode
|
|
10
22
|
}
|
|
11
23
|
|
|
12
24
|
export function Card({ title, icon, href, children }: CardProps) {
|
|
13
|
-
const Icon = icon ?
|
|
25
|
+
const Icon = icon ? iconMap[icon] : null
|
|
14
26
|
|
|
15
27
|
const content = (
|
|
16
28
|
<div className={cn(
|
|
17
|
-
'p-4 border border-[var(--color-border)]
|
|
18
|
-
href && 'hover
|
|
29
|
+
'group p-4 sm:px-6 sm:py-5 rounded-2xl border border-[var(--color-border)] bg-[var(--color-bg)] overflow-hidden w-full',
|
|
30
|
+
href && 'cursor-pointer hover:!border-[var(--color-primary)]'
|
|
19
31
|
)}>
|
|
20
|
-
<div className="flex items-start gap-3">
|
|
32
|
+
<div className="flex items-start gap-3.5">
|
|
21
33
|
{Icon && (
|
|
22
|
-
<div className="
|
|
23
|
-
<Icon size={
|
|
34
|
+
<div className="shrink-0 mt-0.5 text-[var(--color-text-tertiary)] group-hover:text-[var(--color-primary)]">
|
|
35
|
+
<Icon size={18} />
|
|
24
36
|
</div>
|
|
25
37
|
)}
|
|
26
38
|
<div>
|
|
27
|
-
<h3 className="font-
|
|
39
|
+
<h3 className="font-semibold text-[0.9375rem] text-[var(--color-text)] !mt-0 !mb-0">{title}</h3>
|
|
28
40
|
{children && (
|
|
29
|
-
<div className="mt-1 text-
|
|
41
|
+
<div className="mt-1.5 text-[0.8125rem] leading-relaxed text-[var(--color-text-secondary)]">
|
|
30
42
|
{children}
|
|
31
43
|
</div>
|
|
32
44
|
)}
|
|
@@ -36,7 +48,7 @@ export function Card({ title, icon, href, children }: CardProps) {
|
|
|
36
48
|
)
|
|
37
49
|
|
|
38
50
|
if (href) {
|
|
39
|
-
return <Link href={href}>{content}</Link>
|
|
51
|
+
return <Link href={href} className="hover:no-underline">{content}</Link>
|
|
40
52
|
}
|
|
41
53
|
|
|
42
54
|
return content
|
|
@@ -55,7 +67,7 @@ export function CardGroup({ cols = 2, children }: CardGroupProps) {
|
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
return (
|
|
58
|
-
<div className={cn('grid gap-4 my-
|
|
70
|
+
<div className={cn('grid gap-4 my-2', gridCols[cols])}>
|
|
59
71
|
{children}
|
|
60
72
|
</div>
|
|
61
73
|
)
|
|
@@ -20,9 +20,23 @@ export function CodeBlock({ children, className }: CodeBlockProps) {
|
|
|
20
20
|
})
|
|
21
21
|
|
|
22
22
|
async function handleCopy() {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
try {
|
|
24
|
+
await navigator.clipboard.writeText(codeText)
|
|
25
|
+
setCopied(true)
|
|
26
|
+
setTimeout(() => setCopied(false), 2000)
|
|
27
|
+
} catch {
|
|
28
|
+
// Fallback for insecure contexts
|
|
29
|
+
const textarea = document.createElement('textarea')
|
|
30
|
+
textarea.value = codeText
|
|
31
|
+
textarea.style.position = 'fixed'
|
|
32
|
+
textarea.style.opacity = '0'
|
|
33
|
+
document.body.appendChild(textarea)
|
|
34
|
+
textarea.select()
|
|
35
|
+
document.execCommand('copy')
|
|
36
|
+
document.body.removeChild(textarea)
|
|
37
|
+
setCopied(true)
|
|
38
|
+
setTimeout(() => setCopied(false), 2000)
|
|
39
|
+
}
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
return (
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState, Children, isValidElement, ReactNode, ReactElement } from 'react'
|
|
3
|
+
import { useState, useEffect, useContext, Children, isValidElement, ReactNode, ReactElement } from 'react'
|
|
4
4
|
import { cn } from '@/lib/utils'
|
|
5
5
|
import { Copy, Check } from 'lucide-react'
|
|
6
|
+
import { highlight, DEFAULT_THEME, isLightTheme, type ThemeName } from '@/lib/highlight'
|
|
7
|
+
import { SyntaxThemeContext } from '@/contexts/syntax-theme'
|
|
6
8
|
|
|
7
9
|
interface CodeGroupProps {
|
|
8
10
|
children: ReactNode
|
|
@@ -54,17 +56,54 @@ export function CodeGroup({ children }: CodeGroupProps) {
|
|
|
54
56
|
const blocks = extractCodeBlocks(children)
|
|
55
57
|
const [activeIndex, setActiveIndex] = useState(0)
|
|
56
58
|
const [copied, setCopied] = useState(false)
|
|
59
|
+
const [highlightedHtml, setHighlightedHtml] = useState<string | null>(null)
|
|
60
|
+
|
|
61
|
+
const syntaxContext = useContext(SyntaxThemeContext)
|
|
62
|
+
const theme: ThemeName = syntaxContext?.theme ?? DEFAULT_THEME
|
|
63
|
+
const isLight = isLightTheme(theme)
|
|
64
|
+
|
|
65
|
+
const activeBlock = blocks[activeIndex] || blocks[0]
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!activeBlock?.code) return
|
|
69
|
+
let cancelled = false
|
|
70
|
+
|
|
71
|
+
async function doHighlight() {
|
|
72
|
+
try {
|
|
73
|
+
const html = await highlight(activeBlock.code, activeBlock.language, theme)
|
|
74
|
+
if (!cancelled) setHighlightedHtml(html)
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error('CodeGroup highlight failed:', err)
|
|
77
|
+
if (!cancelled) setHighlightedHtml(null)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setHighlightedHtml(null)
|
|
82
|
+
doHighlight()
|
|
83
|
+
return () => { cancelled = true }
|
|
84
|
+
}, [activeBlock?.code, activeBlock?.language, theme])
|
|
57
85
|
|
|
58
86
|
if (blocks.length === 0) {
|
|
59
87
|
return <div>{children}</div>
|
|
60
88
|
}
|
|
61
89
|
|
|
62
|
-
const activeBlock = blocks[activeIndex]
|
|
63
|
-
|
|
64
90
|
const copyToClipboard = async () => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
91
|
+
try {
|
|
92
|
+
await navigator.clipboard.writeText(activeBlock.code)
|
|
93
|
+
setCopied(true)
|
|
94
|
+
setTimeout(() => setCopied(false), 2000)
|
|
95
|
+
} catch {
|
|
96
|
+
const textarea = document.createElement('textarea')
|
|
97
|
+
textarea.value = activeBlock.code
|
|
98
|
+
textarea.style.position = 'fixed'
|
|
99
|
+
textarea.style.opacity = '0'
|
|
100
|
+
document.body.appendChild(textarea)
|
|
101
|
+
textarea.select()
|
|
102
|
+
document.execCommand('copy')
|
|
103
|
+
document.body.removeChild(textarea)
|
|
104
|
+
setCopied(true)
|
|
105
|
+
setTimeout(() => setCopied(false), 2000)
|
|
106
|
+
}
|
|
68
107
|
}
|
|
69
108
|
|
|
70
109
|
const getLanguageLabel = (lang: string, filename?: string) => {
|
|
@@ -72,31 +111,46 @@ export function CodeGroup({ children }: CodeGroupProps) {
|
|
|
72
111
|
const labels: Record<string, string> = {
|
|
73
112
|
typescript: 'TypeScript',
|
|
74
113
|
javascript: 'JavaScript',
|
|
114
|
+
ts: 'TypeScript',
|
|
115
|
+
js: 'JavaScript',
|
|
116
|
+
tsx: 'TypeScript',
|
|
117
|
+
jsx: 'JavaScript',
|
|
75
118
|
python: 'Python',
|
|
119
|
+
py: 'Python',
|
|
76
120
|
bash: 'Bash',
|
|
77
121
|
shell: 'Shell',
|
|
122
|
+
sh: 'Shell',
|
|
78
123
|
json: 'JSON',
|
|
79
124
|
yaml: 'YAML',
|
|
125
|
+
yml: 'YAML',
|
|
80
126
|
go: 'Go',
|
|
81
127
|
rust: 'Rust',
|
|
128
|
+
ruby: 'Ruby',
|
|
129
|
+
php: 'PHP',
|
|
130
|
+
java: 'Java',
|
|
131
|
+
csharp: 'C#',
|
|
132
|
+
cs: 'C#',
|
|
133
|
+
css: 'CSS',
|
|
134
|
+
html: 'HTML',
|
|
135
|
+
sql: 'SQL',
|
|
82
136
|
}
|
|
83
|
-
return labels[lang] || lang
|
|
137
|
+
return labels[lang] || lang.charAt(0).toUpperCase() + lang.slice(1)
|
|
84
138
|
}
|
|
85
139
|
|
|
86
140
|
return (
|
|
87
|
-
<div className="my-6 border border-[var(--color-border)]
|
|
141
|
+
<div className="my-6 rounded-xl overflow-hidden border border-[var(--color-border)] bg-[var(--color-code-bg)]">
|
|
88
142
|
{/* Tab bar */}
|
|
89
|
-
<div className="flex items-center justify-between
|
|
90
|
-
<div className="flex">
|
|
143
|
+
<div className="flex items-center justify-between border-b border-[var(--color-code-border)]">
|
|
144
|
+
<div className="flex gap-0 px-1 pt-1">
|
|
91
145
|
{blocks.map((block, index) => (
|
|
92
146
|
<button
|
|
93
147
|
key={`${block.language}-${block.filename || index}`}
|
|
94
148
|
onClick={() => setActiveIndex(index)}
|
|
95
149
|
className={cn(
|
|
96
|
-
'px-
|
|
150
|
+
'px-3 py-1.5 text-[0.75rem] font-medium rounded-t-lg transition-colors',
|
|
97
151
|
index === activeIndex
|
|
98
|
-
? 'bg-[var(--color-
|
|
99
|
-
: 'text-[var(--color-text-
|
|
152
|
+
? 'bg-[var(--color-bg-tertiary)] text-[var(--color-code-text)]'
|
|
153
|
+
: 'text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)]'
|
|
100
154
|
)}
|
|
101
155
|
>
|
|
102
156
|
{getLanguageLabel(block.language, block.filename)}
|
|
@@ -105,21 +159,27 @@ export function CodeGroup({ children }: CodeGroupProps) {
|
|
|
105
159
|
</div>
|
|
106
160
|
<button
|
|
107
161
|
onClick={copyToClipboard}
|
|
108
|
-
className="p-2 mr-2 text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] transition-colors"
|
|
162
|
+
className="p-2 mr-2 text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] transition-colors"
|
|
109
163
|
title="Copy code"
|
|
164
|
+
aria-label={copied ? 'Copied' : 'Copy code'}
|
|
110
165
|
>
|
|
111
|
-
{copied ? <Check size={
|
|
166
|
+
{copied ? <Check size={14} /> : <Copy size={14} />}
|
|
112
167
|
</button>
|
|
113
168
|
</div>
|
|
114
169
|
|
|
115
170
|
{/* Code content */}
|
|
116
|
-
|
|
117
|
-
<
|
|
171
|
+
{highlightedHtml ? (
|
|
172
|
+
<div
|
|
173
|
+
className="[&>pre]:!rounded-none [&>pre]:!border-0 [&>pre]:!m-0 [&>pre]:py-3.5 [&>pre]:px-4 [&>pre]:overflow-x-auto"
|
|
174
|
+
dangerouslySetInnerHTML={{ __html: highlightedHtml }}
|
|
175
|
+
/>
|
|
176
|
+
) : (
|
|
177
|
+
<pre className="!m-0 !rounded-none !border-0 !bg-transparent py-3.5 px-4 overflow-x-auto">
|
|
118
178
|
<code className={`language-${activeBlock.language}`}>
|
|
119
179
|
{activeBlock.code}
|
|
120
180
|
</code>
|
|
121
181
|
</pre>
|
|
122
|
-
|
|
182
|
+
)}
|
|
123
183
|
</div>
|
|
124
184
|
)
|
|
125
185
|
}
|
|
@@ -157,6 +157,7 @@ function PlaygroundToolbar({ originalCode, filename, autoRun, onAutoRunChange }:
|
|
|
157
157
|
onClick={handleReset}
|
|
158
158
|
className="p-1.5 text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] rounded"
|
|
159
159
|
title="Reset code"
|
|
160
|
+
aria-label="Reset code"
|
|
160
161
|
>
|
|
161
162
|
<RotateCcw size={14} />
|
|
162
163
|
</button>
|
|
@@ -164,6 +165,7 @@ function PlaygroundToolbar({ originalCode, filename, autoRun, onAutoRunChange }:
|
|
|
164
165
|
onClick={handleDownload}
|
|
165
166
|
className="p-1.5 text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] rounded"
|
|
166
167
|
title="Download code"
|
|
168
|
+
aria-label="Download code"
|
|
167
169
|
>
|
|
168
170
|
<Download size={14} />
|
|
169
171
|
</button>
|
|
@@ -171,6 +173,7 @@ function PlaygroundToolbar({ originalCode, filename, autoRun, onAutoRunChange }:
|
|
|
171
173
|
onClick={() => onAutoRunChange(!autoRun)}
|
|
172
174
|
className={`p-1.5 rounded ${autoRun ? 'text-emerald-500' : 'text-[var(--color-text-tertiary)]'} hover:text-[var(--color-text)]`}
|
|
173
175
|
title={autoRun ? 'Auto-run enabled' : 'Auto-run disabled'}
|
|
176
|
+
aria-label={autoRun ? 'Auto-run enabled' : 'Auto-run disabled'}
|
|
174
177
|
>
|
|
175
178
|
{autoRun ? <Play size={14} /> : <Pause size={14} />}
|
|
176
179
|
</button>
|
|
@@ -154,6 +154,7 @@ export function GoPlayground({
|
|
|
154
154
|
onClick={handleReset}
|
|
155
155
|
className="p-1.5 text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] rounded"
|
|
156
156
|
title="Reset code"
|
|
157
|
+
aria-label="Reset code"
|
|
157
158
|
>
|
|
158
159
|
<RotateCcw size={14} />
|
|
159
160
|
</button>
|
|
@@ -161,6 +162,7 @@ export function GoPlayground({
|
|
|
161
162
|
onClick={handleDownload}
|
|
162
163
|
className="p-1.5 text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] rounded"
|
|
163
164
|
title="Download code"
|
|
165
|
+
aria-label="Download code"
|
|
164
166
|
>
|
|
165
167
|
<Download size={14} />
|
|
166
168
|
</button>
|
|
@@ -168,6 +170,7 @@ export function GoPlayground({
|
|
|
168
170
|
onClick={openInPlayground}
|
|
169
171
|
className="p-1.5 text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] rounded"
|
|
170
172
|
title="Open in Go Playground"
|
|
173
|
+
aria-label="Open in Go Playground"
|
|
171
174
|
>
|
|
172
175
|
<ExternalLink size={14} />
|
|
173
176
|
</button>
|
|
@@ -1,89 +1,229 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useContext, ReactNode, isValidElement, Children } from 'react'
|
|
3
|
+
import { useState, useEffect, useContext, ReactNode, isValidElement, Children, useRef } from 'react'
|
|
4
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
|
|
5
|
+
import { highlight, DEFAULT_THEME, isLightTheme, type ThemeName } from '@/lib/highlight'
|
|
8
6
|
import { SyntaxThemeContext } from '@/contexts/syntax-theme'
|
|
9
7
|
|
|
8
|
+
const TERMINAL_LANGUAGES = ['bash', 'shell', 'sh', 'zsh']
|
|
9
|
+
|
|
10
10
|
interface HighlightedCodeProps {
|
|
11
11
|
children: ReactNode
|
|
12
12
|
className?: string
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
const langLabels: Record<string, string> = {
|
|
16
|
+
typescript: 'TypeScript', javascript: 'JavaScript',
|
|
17
|
+
ts: 'TypeScript', js: 'JavaScript', tsx: 'TSX', jsx: 'JSX',
|
|
18
|
+
python: 'Python', py: 'Python',
|
|
19
|
+
bash: 'Bash', shell: 'Shell', sh: 'Shell',
|
|
20
|
+
json: 'JSON', yaml: 'YAML', go: 'Go', rust: 'Rust',
|
|
21
|
+
css: 'CSS', html: 'HTML', sql: 'SQL', text: '',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Parse meta string for showLineNumbers */
|
|
25
|
+
function parseShowLineNumbers(meta: string): boolean {
|
|
26
|
+
return /showLineNumbers/.test(meta)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Parse meta string for highlighted lines: {1,3-5,8} */
|
|
30
|
+
function parseHighlightedLines(meta: string): Set<number> {
|
|
31
|
+
const set = new Set<number>()
|
|
32
|
+
const match = meta.match(/\{([\d,\s-]+)\}/)
|
|
33
|
+
if (!match) return set
|
|
34
|
+
|
|
35
|
+
const parts = match[1].split(',')
|
|
36
|
+
for (const part of parts) {
|
|
37
|
+
const trimmed = part.trim()
|
|
38
|
+
const rangeMatch = trimmed.match(/^(\d+)-(\d+)$/)
|
|
39
|
+
if (rangeMatch) {
|
|
40
|
+
const start = parseInt(rangeMatch[1])
|
|
41
|
+
const end = parseInt(rangeMatch[2])
|
|
42
|
+
for (let i = start; i <= end; i++) set.add(i)
|
|
43
|
+
} else {
|
|
44
|
+
const num = parseInt(trimmed)
|
|
45
|
+
if (!isNaN(num)) set.add(num)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return set
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Parse meta string for title="filename.ts" or title=filename.ts */
|
|
52
|
+
function parseFilename(meta: string): string | undefined {
|
|
53
|
+
const match = meta.match(/title=["']?([^"'\s]+)["']?/)
|
|
54
|
+
return match ? match[1] : undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
15
57
|
export function HighlightedCode({ children, className }: HighlightedCodeProps) {
|
|
16
58
|
const [copied, setCopied] = useState(false)
|
|
59
|
+
const [showToast, setShowToast] = useState(false)
|
|
17
60
|
const [highlightedHtml, setHighlightedHtml] = useState<string | null>(null)
|
|
61
|
+
const copyButtonRef = useRef<HTMLButtonElement>(null)
|
|
18
62
|
|
|
19
|
-
// Use context safely - fallback to default if not available
|
|
20
63
|
const syntaxContext = useContext(SyntaxThemeContext)
|
|
21
64
|
const theme: ThemeName = syntaxContext?.theme ?? DEFAULT_THEME
|
|
65
|
+
const isLight = isLightTheme(theme)
|
|
22
66
|
|
|
23
|
-
// Extract code text and language from children
|
|
24
67
|
let codeText = ''
|
|
25
68
|
let language = 'text'
|
|
69
|
+
let metaString = ''
|
|
26
70
|
|
|
27
71
|
Children.forEach(children, (child) => {
|
|
28
72
|
if (isValidElement(child) && child.type === 'code') {
|
|
29
|
-
const props = child.props as { children?: string; className?: string }
|
|
73
|
+
const props = child.props as { children?: string; className?: string; 'data-meta'?: string }
|
|
30
74
|
codeText = props.children || ''
|
|
31
|
-
// Extract language from className like "language-typescript"
|
|
32
75
|
const classMatch = props.className?.match(/language-(\w+)/)
|
|
33
|
-
if (classMatch)
|
|
34
|
-
|
|
35
|
-
}
|
|
76
|
+
if (classMatch) language = classMatch[1]
|
|
77
|
+
metaString = props['data-meta'] || ''
|
|
36
78
|
}
|
|
37
79
|
})
|
|
38
80
|
|
|
81
|
+
const showLineNumbers = parseShowLineNumbers(metaString)
|
|
82
|
+
const highlightedLines = parseHighlightedLines(metaString)
|
|
83
|
+
const filename = parseFilename(metaString)
|
|
84
|
+
const hasLineFeatures = showLineNumbers || highlightedLines.size > 0
|
|
85
|
+
const isTerminal = TERMINAL_LANGUAGES.includes(language)
|
|
86
|
+
|
|
39
87
|
useEffect(() => {
|
|
40
88
|
let cancelled = false
|
|
41
|
-
|
|
42
89
|
async function doHighlight() {
|
|
43
90
|
if (!codeText) return
|
|
44
|
-
|
|
45
91
|
try {
|
|
46
92
|
const html = await highlight(codeText, language, theme)
|
|
47
|
-
if (!cancelled)
|
|
48
|
-
setHighlightedHtml(html)
|
|
49
|
-
}
|
|
93
|
+
if (!cancelled) setHighlightedHtml(html)
|
|
50
94
|
} catch (err) {
|
|
51
95
|
console.error('Highlight failed:', err)
|
|
52
96
|
}
|
|
53
97
|
}
|
|
54
|
-
|
|
55
98
|
doHighlight()
|
|
56
|
-
|
|
57
|
-
return () => {
|
|
58
|
-
cancelled = true
|
|
59
|
-
}
|
|
99
|
+
return () => { cancelled = true }
|
|
60
100
|
}, [codeText, language, theme])
|
|
61
101
|
|
|
62
102
|
async function handleCopy() {
|
|
63
|
-
|
|
103
|
+
try {
|
|
104
|
+
await navigator.clipboard.writeText(codeText)
|
|
105
|
+
} catch {
|
|
106
|
+
const textarea = document.createElement('textarea')
|
|
107
|
+
textarea.value = codeText
|
|
108
|
+
textarea.style.position = 'fixed'
|
|
109
|
+
textarea.style.opacity = '0'
|
|
110
|
+
document.body.appendChild(textarea)
|
|
111
|
+
textarea.select()
|
|
112
|
+
document.execCommand('copy')
|
|
113
|
+
document.body.removeChild(textarea)
|
|
114
|
+
}
|
|
64
115
|
setCopied(true)
|
|
116
|
+
setShowToast(true)
|
|
65
117
|
setTimeout(() => setCopied(false), 2000)
|
|
118
|
+
setTimeout(() => setShowToast(false), 1500)
|
|
66
119
|
}
|
|
67
120
|
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
121
|
+
if (!codeText.trim()) return null
|
|
122
|
+
|
|
123
|
+
const langLabel = langLabels[language] || language
|
|
124
|
+
const showLabel = langLabel && language !== 'text'
|
|
125
|
+
const lines = codeText.split('\n')
|
|
126
|
+
// Remove trailing empty line from code
|
|
127
|
+
if (lines[lines.length - 1] === '') lines.pop()
|
|
128
|
+
|
|
129
|
+
/** Render code with line numbers and/or line highlighting */
|
|
130
|
+
function renderWithLineFeatures(htmlContent: string | null) {
|
|
131
|
+
if (!hasLineFeatures) {
|
|
132
|
+
// No line features — render normally
|
|
133
|
+
if (htmlContent) {
|
|
134
|
+
return (
|
|
135
|
+
<div
|
|
136
|
+
className="[&>pre]:!rounded-none [&>pre]:!border-0 [&>pre]:!m-0 [&>pre]:py-3.5 [&>pre]:px-4 [&>pre]:overflow-x-auto"
|
|
137
|
+
dangerouslySetInnerHTML={{ __html: htmlContent }}
|
|
138
|
+
/>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
return (
|
|
142
|
+
<pre className="!rounded-none !border-0 !m-0 py-3.5 px-4 overflow-x-auto">
|
|
77
143
|
{children}
|
|
78
144
|
</pre>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// With line features, we need to render line by line
|
|
149
|
+
return (
|
|
150
|
+
<div className="overflow-x-auto">
|
|
151
|
+
<table className="w-full border-collapse" style={{ tableLayout: 'auto' }}>
|
|
152
|
+
<tbody>
|
|
153
|
+
{lines.map((line, i) => {
|
|
154
|
+
const lineNum = i + 1
|
|
155
|
+
const isHighlighted = highlightedLines.has(lineNum)
|
|
156
|
+
return (
|
|
157
|
+
<tr
|
|
158
|
+
key={i}
|
|
159
|
+
className={isHighlighted ? 'bg-[var(--color-primary)]/[0.07]' : ''}
|
|
160
|
+
>
|
|
161
|
+
{showLineNumbers && (
|
|
162
|
+
<td className="text-right pr-4 select-none text-[var(--color-text-tertiary)] text-[0.75rem] py-0 pl-4 w-[1%] whitespace-nowrap align-top leading-[1.7142857]">
|
|
163
|
+
{lineNum}
|
|
164
|
+
</td>
|
|
165
|
+
)}
|
|
166
|
+
<td className="py-0 pr-4 whitespace-pre font-mono text-[0.875em] leading-[1.7142857]">
|
|
167
|
+
{line || ' '}
|
|
168
|
+
</td>
|
|
169
|
+
</tr>
|
|
170
|
+
)
|
|
171
|
+
})}
|
|
172
|
+
</tbody>
|
|
173
|
+
</table>
|
|
174
|
+
</div>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const hasHeader = !!filename
|
|
179
|
+
const showTerminalHeader = isTerminal && !hasHeader
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<div className={`relative group mt-5 mb-8 rounded-2xl overflow-hidden border border-[var(--color-border)] ${isTerminal ? 'terminal-block' : ''}`}>
|
|
183
|
+
{/* Terminal header with macOS-style dots */}
|
|
184
|
+
{showTerminalHeader && (
|
|
185
|
+
<div className="flex items-center gap-1.5 px-4 py-2.5 bg-[var(--color-bg-tertiary)] border-b border-[var(--color-code-border)]">
|
|
186
|
+
<span className="w-2.5 h-2.5 rounded-full bg-red-500/70" />
|
|
187
|
+
<span className="w-2.5 h-2.5 rounded-full bg-yellow-500/70" />
|
|
188
|
+
<span className="w-2.5 h-2.5 rounded-full bg-green-500/70" />
|
|
189
|
+
<span className="ml-2 text-[0.7rem] text-[var(--color-text-tertiary)]">Terminal</span>
|
|
190
|
+
</div>
|
|
79
191
|
)}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
192
|
+
|
|
193
|
+
{/* Filename header */}
|
|
194
|
+
{filename && (
|
|
195
|
+
<div className="bg-[var(--color-bg-secondary)] border-b border-[var(--color-border)] px-4 py-2 text-[0.8125rem] text-[var(--color-text-secondary)] font-medium">
|
|
196
|
+
{filename}
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
|
|
200
|
+
{renderWithLineFeatures(highlightedHtml)}
|
|
201
|
+
|
|
202
|
+
{/* Floating label + copy button — always visible (dimmed), full on hover */}
|
|
203
|
+
<div className="absolute top-2 right-2 flex items-center gap-1.5 opacity-60 group-hover:opacity-100 transition-opacity">
|
|
204
|
+
{showLabel && !filename && !showTerminalHeader && (
|
|
205
|
+
<span className={`text-[0.6875rem] px-1.5 py-0.5 rounded-md ${isLight ? 'text-black/30' : 'text-white/30'}`}>
|
|
206
|
+
{langLabel}
|
|
207
|
+
</span>
|
|
208
|
+
)}
|
|
209
|
+
<div className="relative">
|
|
210
|
+
<button
|
|
211
|
+
ref={copyButtonRef}
|
|
212
|
+
onClick={handleCopy}
|
|
213
|
+
className={`p-1.5 rounded-md backdrop-blur ${isLight ? 'text-black/30 hover:text-black/60' : 'text-white/30 hover:text-white/60'}`}
|
|
214
|
+
title={copied ? 'Copied!' : 'Copy code'}
|
|
215
|
+
aria-label={copied ? 'Copied' : 'Copy code'}
|
|
216
|
+
>
|
|
217
|
+
{copied ? <Check size={14} /> : <Copy size={14} />}
|
|
218
|
+
</button>
|
|
219
|
+
{/* Copy toast */}
|
|
220
|
+
{showToast && (
|
|
221
|
+
<span className="copy-toast absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 rounded-md bg-[var(--color-bg-secondary)] border border-[var(--color-border)] text-[0.6875rem] text-[var(--color-text-secondary)] whitespace-nowrap shadow-lg">
|
|
222
|
+
Copied!
|
|
223
|
+
</span>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
87
227
|
</div>
|
|
88
228
|
)
|
|
89
229
|
}
|
|
@@ -212,6 +212,7 @@ sys.stderr = _capture.stderr
|
|
|
212
212
|
onClick={handleReset}
|
|
213
213
|
className="p-1.5 text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] rounded"
|
|
214
214
|
title="Reset code"
|
|
215
|
+
aria-label="Reset code"
|
|
215
216
|
>
|
|
216
217
|
<RotateCcw size={14} />
|
|
217
218
|
</button>
|
|
@@ -219,6 +220,7 @@ sys.stderr = _capture.stderr
|
|
|
219
220
|
onClick={handleDownload}
|
|
220
221
|
className="p-1.5 text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] rounded"
|
|
221
222
|
title="Download code"
|
|
223
|
+
aria-label="Download code"
|
|
222
224
|
>
|
|
223
225
|
<Download size={14} />
|
|
224
226
|
</button>
|
|
@@ -8,18 +8,18 @@ export function Steps({ children }: StepsProps) {
|
|
|
8
8
|
const items = Children.toArray(children).filter(isValidElement)
|
|
9
9
|
|
|
10
10
|
return (
|
|
11
|
-
<div className="my-
|
|
11
|
+
<div className="my-8 relative">
|
|
12
12
|
{items.map((child, index) => (
|
|
13
|
-
<div key={`step-${index}`} className="flex gap-4">
|
|
13
|
+
<div key={`step-${index}`} className="flex gap-4 pb-8 last:pb-0">
|
|
14
14
|
<div className="flex flex-col items-center">
|
|
15
|
-
<div className="flex items-center justify-center w-
|
|
15
|
+
<div className="flex items-center justify-center w-7 h-7 rounded-full bg-[var(--color-text)] text-[var(--color-bg)] text-xs font-semibold shrink-0">
|
|
16
16
|
{index + 1}
|
|
17
17
|
</div>
|
|
18
18
|
{index < items.length - 1 && (
|
|
19
19
|
<div className="flex-1 w-px bg-[var(--color-border)] mt-2" />
|
|
20
20
|
)}
|
|
21
21
|
</div>
|
|
22
|
-
<div className="flex-1
|
|
22
|
+
<div className="flex-1 pt-0.5">
|
|
23
23
|
{child}
|
|
24
24
|
</div>
|
|
25
25
|
</div>
|
|
@@ -36,8 +36,8 @@ interface StepProps {
|
|
|
36
36
|
export function Step({ title, children }: StepProps) {
|
|
37
37
|
return (
|
|
38
38
|
<div>
|
|
39
|
-
<h3 className="font-
|
|
40
|
-
<div className="text-[var(--color-text-secondary)]">{children}</div>
|
|
39
|
+
<h3 className="!text-[0.9375rem] font-semibold text-[var(--color-text)] !mt-0 mb-2">{title}</h3>
|
|
40
|
+
<div className="text-[0.8125rem] text-[var(--color-text-secondary)] leading-relaxed">{children}</div>
|
|
41
41
|
</div>
|
|
42
42
|
)
|
|
43
43
|
}
|