skrypt-ai 0.3.4 → 0.4.1
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 +2 -1
- package/dist/template/package.json +17 -15
- 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 +139 -16
- 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 +42 -0
- package/dist/template/src/app/not-found.tsx +35 -0
- package/dist/template/src/app/page.tsx +62 -28
- package/dist/template/src/components/ai-chat.tsx +26 -21
- package/dist/template/src/components/breadcrumbs.tsx +46 -2
- package/dist/template/src/components/copy-button.tsx +17 -3
- package/dist/template/src/components/docs-layout.tsx +142 -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 +29 -5
- package/dist/template/src/components/mdx/accordion.tsx +7 -6
- package/dist/template/src/components/mdx/card.tsx +19 -7
- package/dist/template/src/components/mdx/code-block.tsx +17 -3
- package/dist/template/src/components/mdx/code-group.tsx +65 -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 +171 -76
- package/dist/template/src/components/mdx/python-playground.tsx +2 -0
- package/dist/template/src/components/mdx/tabs.tsx +74 -6
- 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 +206 -52
- package/dist/template/src/components/sidebar.tsx +136 -77
- package/dist/template/src/components/table-of-contents.tsx +23 -7
- 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 +307 -166
- 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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import Link from 'next/link'
|
|
4
4
|
import { Search, Menu, X } from 'lucide-react'
|
|
5
|
-
import { useState } from 'react'
|
|
5
|
+
import { useState, useEffect } from 'react'
|
|
6
6
|
import { SearchDialog } from './search-dialog'
|
|
7
7
|
import { ThemeToggle } from './theme-toggle'
|
|
8
8
|
import { SyntaxThemeSelector } from './syntax-theme-selector'
|
|
@@ -12,10 +12,30 @@ interface HeaderProps {
|
|
|
12
12
|
menuOpen?: boolean
|
|
13
13
|
siteName?: string
|
|
14
14
|
navLinks?: Array<{ title: string; path: string }>
|
|
15
|
+
logo?: string
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
export function Header({ onMenuToggle, menuOpen, siteName = 'Docs', navLinks }: HeaderProps) {
|
|
18
|
+
export function Header({ onMenuToggle, menuOpen, siteName = 'Docs', navLinks, logo }: HeaderProps) {
|
|
18
19
|
const [searchOpen, setSearchOpen] = useState(false)
|
|
20
|
+
const [isMac, setIsMac] = useState(true)
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
setIsMac(
|
|
24
|
+
navigator.platform?.toLowerCase().includes('mac') ||
|
|
25
|
+
navigator.userAgent?.toLowerCase().includes('mac')
|
|
26
|
+
)
|
|
27
|
+
}, [])
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
31
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
32
|
+
e.preventDefault()
|
|
33
|
+
setSearchOpen(prev => !prev)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
37
|
+
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
38
|
+
}, [])
|
|
19
39
|
|
|
20
40
|
return (
|
|
21
41
|
<>
|
|
@@ -32,8 +52,12 @@ export function Header({ onMenuToggle, menuOpen, siteName = 'Docs', navLinks }:
|
|
|
32
52
|
{menuOpen ? <X size={18} /> : <Menu size={18} />}
|
|
33
53
|
</button>
|
|
34
54
|
|
|
35
|
-
<Link href="/" className="font-semibold text-[0.9375rem] tracking-tight text-[var(--color-text)] hover:no-underline">
|
|
36
|
-
{
|
|
55
|
+
<Link href="/" className="flex items-center gap-2 font-semibold text-[0.9375rem] tracking-tight text-[var(--color-text)] hover:no-underline">
|
|
56
|
+
{logo ? (
|
|
57
|
+
<img src={logo} alt={siteName} className="h-6 w-auto" />
|
|
58
|
+
) : (
|
|
59
|
+
siteName
|
|
60
|
+
)}
|
|
37
61
|
</Link>
|
|
38
62
|
|
|
39
63
|
<nav className="hidden md:flex items-center gap-1">
|
|
@@ -63,7 +87,7 @@ export function Header({ onMenuToggle, menuOpen, siteName = 'Docs', navLinks }:
|
|
|
63
87
|
<Search size={14} />
|
|
64
88
|
<span className="hidden sm:inline">Search...</span>
|
|
65
89
|
<kbd className="hidden sm:inline ml-2 px-1.5 py-0.5 text-[0.6875rem] font-medium bg-[var(--color-bg)] border border-[var(--color-border)] rounded">
|
|
66
|
-
⌘K
|
|
90
|
+
{isMac ? '⌘K' : 'Ctrl+K'}
|
|
67
91
|
</kbd>
|
|
68
92
|
</button>
|
|
69
93
|
<SyntaxThemeSelector />
|
|
@@ -17,6 +17,7 @@ export function Accordion({ title, defaultOpen = false, children }: AccordionPro
|
|
|
17
17
|
<div className="border-b border-[var(--color-border)] last:border-b-0">
|
|
18
18
|
<button
|
|
19
19
|
onClick={() => setOpen(!open)}
|
|
20
|
+
aria-expanded={open}
|
|
20
21
|
className="flex items-center gap-2 w-full py-3.5 text-left text-[0.875rem] font-medium text-[var(--color-text)] hover:text-[var(--color-primary)] transition-colors"
|
|
21
22
|
>
|
|
22
23
|
<ChevronRight
|
|
@@ -29,13 +30,13 @@ export function Accordion({ title, defaultOpen = false, children }: AccordionPro
|
|
|
29
30
|
{title}
|
|
30
31
|
</button>
|
|
31
32
|
<div
|
|
32
|
-
className=
|
|
33
|
-
|
|
34
|
-
open ? 'max-h-[2000px] opacity-100' : 'max-h-0 opacity-0'
|
|
35
|
-
)}
|
|
33
|
+
className="grid transition-all duration-200"
|
|
34
|
+
style={{ gridTemplateRows: open ? '1fr' : '0fr' }}
|
|
36
35
|
>
|
|
37
|
-
<div className="
|
|
38
|
-
|
|
36
|
+
<div className="overflow-hidden">
|
|
37
|
+
<div className="pl-5 pb-4 text-[0.8125rem] text-[var(--color-text-secondary)] leading-relaxed">
|
|
38
|
+
{children}
|
|
39
|
+
</div>
|
|
39
40
|
</div>
|
|
40
41
|
</div>
|
|
41
42
|
</div>
|
|
@@ -1,25 +1,37 @@
|
|
|
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
|
-
'group p-5 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
32
|
<div className="flex items-start gap-3.5">
|
|
21
33
|
{Icon && (
|
|
22
|
-
<div className="shrink-0
|
|
34
|
+
<div className="shrink-0 mt-0.5 text-[var(--color-text-tertiary)] group-hover:text-[var(--color-primary)]">
|
|
23
35
|
<Icon size={18} />
|
|
24
36
|
</div>
|
|
25
37
|
)}
|
|
@@ -55,7 +67,7 @@ export function CardGroup({ cols = 2, children }: CardGroupProps) {
|
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
return (
|
|
58
|
-
<div className={cn('grid gap-
|
|
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) => {
|
|
@@ -78,9 +117,9 @@ export function CodeGroup({ children }: CodeGroupProps) {
|
|
|
78
117
|
jsx: 'JavaScript',
|
|
79
118
|
python: 'Python',
|
|
80
119
|
py: 'Python',
|
|
81
|
-
bash: '
|
|
82
|
-
shell: '
|
|
83
|
-
sh: '
|
|
120
|
+
bash: 'Bash',
|
|
121
|
+
shell: 'Shell',
|
|
122
|
+
sh: 'Shell',
|
|
84
123
|
json: 'JSON',
|
|
85
124
|
yaml: 'YAML',
|
|
86
125
|
yml: 'YAML',
|
|
@@ -101,7 +140,7 @@ export function CodeGroup({ children }: CodeGroupProps) {
|
|
|
101
140
|
return (
|
|
102
141
|
<div className="my-6 rounded-xl overflow-hidden border border-[var(--color-border)] bg-[var(--color-code-bg)]">
|
|
103
142
|
{/* Tab bar */}
|
|
104
|
-
<div className="flex items-center justify-between border-b border-
|
|
143
|
+
<div className="flex items-center justify-between border-b border-[var(--color-code-border)]">
|
|
105
144
|
<div className="flex gap-0 px-1 pt-1">
|
|
106
145
|
{blocks.map((block, index) => (
|
|
107
146
|
<button
|
|
@@ -110,8 +149,8 @@ export function CodeGroup({ children }: CodeGroupProps) {
|
|
|
110
149
|
className={cn(
|
|
111
150
|
'px-3 py-1.5 text-[0.75rem] font-medium rounded-t-lg transition-colors',
|
|
112
151
|
index === activeIndex
|
|
113
|
-
? 'bg-
|
|
114
|
-
: 'text-
|
|
152
|
+
? 'bg-[var(--color-bg-tertiary)] text-[var(--color-code-text)]'
|
|
153
|
+
: 'text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)]'
|
|
115
154
|
)}
|
|
116
155
|
>
|
|
117
156
|
{getLanguageLabel(block.language, block.filename)}
|
|
@@ -120,19 +159,27 @@ export function CodeGroup({ children }: CodeGroupProps) {
|
|
|
120
159
|
</div>
|
|
121
160
|
<button
|
|
122
161
|
onClick={copyToClipboard}
|
|
123
|
-
className="p-2 mr-2 text-
|
|
162
|
+
className="p-2 mr-2 text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] transition-colors"
|
|
124
163
|
title="Copy code"
|
|
164
|
+
aria-label={copied ? 'Copied' : 'Copy code'}
|
|
125
165
|
>
|
|
126
166
|
{copied ? <Check size={14} /> : <Copy size={14} />}
|
|
127
167
|
</button>
|
|
128
168
|
</div>
|
|
129
169
|
|
|
130
170
|
{/* Code content */}
|
|
131
|
-
|
|
132
|
-
<
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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">
|
|
178
|
+
<code className={`language-${activeBlock.language}`}>
|
|
179
|
+
{activeBlock.code}
|
|
180
|
+
</code>
|
|
181
|
+
</pre>
|
|
182
|
+
)}
|
|
136
183
|
</div>
|
|
137
184
|
)
|
|
138
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,134 +1,229 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useContext, ReactNode, isValidElement, Children } from 'react'
|
|
4
|
-
import { Copy, Check
|
|
5
|
-
import { highlight, DEFAULT_THEME, type ThemeName } from '@/lib/highlight'
|
|
3
|
+
import { useState, useEffect, useContext, ReactNode, isValidElement, Children, useRef } from 'react'
|
|
4
|
+
import { Copy, Check } from 'lucide-react'
|
|
5
|
+
import { highlight, DEFAULT_THEME, isLightTheme, type ThemeName } from '@/lib/highlight'
|
|
6
6
|
import { SyntaxThemeContext } from '@/contexts/syntax-theme'
|
|
7
7
|
|
|
8
|
+
const TERMINAL_LANGUAGES = ['bash', 'shell', 'sh', 'zsh']
|
|
9
|
+
|
|
8
10
|
interface HighlightedCodeProps {
|
|
9
11
|
children: ReactNode
|
|
10
12
|
className?: string
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
const langLabels: Record<string, string> = {
|
|
14
|
-
typescript: 'TypeScript',
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
33
55
|
}
|
|
34
56
|
|
|
35
57
|
export function HighlightedCode({ children, className }: HighlightedCodeProps) {
|
|
36
58
|
const [copied, setCopied] = useState(false)
|
|
59
|
+
const [showToast, setShowToast] = useState(false)
|
|
37
60
|
const [highlightedHtml, setHighlightedHtml] = useState<string | null>(null)
|
|
61
|
+
const copyButtonRef = useRef<HTMLButtonElement>(null)
|
|
38
62
|
|
|
39
63
|
const syntaxContext = useContext(SyntaxThemeContext)
|
|
40
64
|
const theme: ThemeName = syntaxContext?.theme ?? DEFAULT_THEME
|
|
65
|
+
const isLight = isLightTheme(theme)
|
|
41
66
|
|
|
42
67
|
let codeText = ''
|
|
43
68
|
let language = 'text'
|
|
44
|
-
let
|
|
69
|
+
let metaString = ''
|
|
45
70
|
|
|
46
71
|
Children.forEach(children, (child) => {
|
|
47
72
|
if (isValidElement(child) && child.type === 'code') {
|
|
48
|
-
const props = child.props as { children?: string; className?: string; 'data-
|
|
73
|
+
const props = child.props as { children?: string; className?: string; 'data-meta'?: string }
|
|
49
74
|
codeText = props.children || ''
|
|
50
75
|
const classMatch = props.className?.match(/language-(\w+)/)
|
|
51
|
-
if (classMatch)
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
if (props['data-filename']) {
|
|
55
|
-
filename = props['data-filename']
|
|
56
|
-
}
|
|
76
|
+
if (classMatch) language = classMatch[1]
|
|
77
|
+
metaString = props['data-meta'] || ''
|
|
57
78
|
}
|
|
58
79
|
})
|
|
59
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
|
+
|
|
60
87
|
useEffect(() => {
|
|
61
88
|
let cancelled = false
|
|
62
|
-
|
|
63
89
|
async function doHighlight() {
|
|
64
90
|
if (!codeText) return
|
|
65
|
-
|
|
66
91
|
try {
|
|
67
92
|
const html = await highlight(codeText, language, theme)
|
|
68
|
-
if (!cancelled)
|
|
69
|
-
setHighlightedHtml(html)
|
|
70
|
-
}
|
|
93
|
+
if (!cancelled) setHighlightedHtml(html)
|
|
71
94
|
} catch (err) {
|
|
72
95
|
console.error('Highlight failed:', err)
|
|
73
96
|
}
|
|
74
97
|
}
|
|
75
|
-
|
|
76
98
|
doHighlight()
|
|
77
|
-
|
|
78
|
-
return () => {
|
|
79
|
-
cancelled = true
|
|
80
|
-
}
|
|
99
|
+
return () => { cancelled = true }
|
|
81
100
|
}, [codeText, language, theme])
|
|
82
101
|
|
|
83
102
|
async function handleCopy() {
|
|
84
|
-
|
|
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
|
+
}
|
|
85
115
|
setCopied(true)
|
|
116
|
+
setShowToast(true)
|
|
86
117
|
setTimeout(() => setCopied(false), 2000)
|
|
118
|
+
setTimeout(() => setShowToast(false), 1500)
|
|
87
119
|
}
|
|
88
120
|
|
|
89
|
-
|
|
90
|
-
|
|
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">
|
|
143
|
+
{children}
|
|
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
|
|
91
180
|
|
|
92
181
|
return (
|
|
93
|
-
<div className=
|
|
94
|
-
{/*
|
|
95
|
-
{
|
|
96
|
-
<div className="flex items-center
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
</
|
|
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>
|
|
191
|
+
)}
|
|
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">
|
|
101
210
|
<button
|
|
211
|
+
ref={copyButtonRef}
|
|
102
212
|
onClick={handleCopy}
|
|
103
|
-
className=
|
|
213
|
+
className={`p-1.5 rounded-md backdrop-blur ${isLight ? 'text-black/30 hover:text-black/60' : 'text-white/30 hover:text-white/60'}`}
|
|
104
214
|
title={copied ? 'Copied!' : 'Copy code'}
|
|
215
|
+
aria-label={copied ? 'Copied' : 'Copy code'}
|
|
105
216
|
>
|
|
106
|
-
{copied ? <Check size={
|
|
217
|
+
{copied ? <Check size={14} /> : <Copy size={14} />}
|
|
107
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
|
+
)}
|
|
108
225
|
</div>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
{highlightedHtml ? (
|
|
112
|
-
<div
|
|
113
|
-
className="[&>pre]:!rounded-none [&>pre]:!border-0 [&>pre]:!m-0 [&>pre]:px-4 [&>pre]:py-3 [&>pre]:overflow-x-auto [&>pre]:!bg-transparent"
|
|
114
|
-
dangerouslySetInnerHTML={{ __html: highlightedHtml }}
|
|
115
|
-
/>
|
|
116
|
-
) : (
|
|
117
|
-
<pre className="!rounded-none !border-0 !m-0 px-4 py-3 overflow-x-auto !bg-transparent">
|
|
118
|
-
{children}
|
|
119
|
-
</pre>
|
|
120
|
-
)}
|
|
121
|
-
|
|
122
|
-
{/* Copy button fallback (no header) */}
|
|
123
|
-
{!showHeader && (
|
|
124
|
-
<button
|
|
125
|
-
onClick={handleCopy}
|
|
126
|
-
className="absolute top-2.5 right-2.5 p-1.5 rounded-md text-white/30 hover:text-white/60 opacity-0 group-hover:opacity-100 transition-all"
|
|
127
|
-
title={copied ? 'Copied!' : 'Copy code'}
|
|
128
|
-
>
|
|
129
|
-
{copied ? <Check size={13} /> : <Copy size={13} />}
|
|
130
|
-
</button>
|
|
131
|
-
)}
|
|
226
|
+
</div>
|
|
132
227
|
</div>
|
|
133
228
|
)
|
|
134
229
|
}
|