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.
Files changed (97) hide show
  1. package/README.md +1 -1
  2. package/dist/auth/index.d.ts +0 -1
  3. package/dist/auth/index.js +3 -5
  4. package/dist/autofix/index.js +15 -3
  5. package/dist/cli.js +19 -4
  6. package/dist/commands/check-links.js +164 -174
  7. package/dist/commands/deploy.js +5 -2
  8. package/dist/commands/generate.js +206 -199
  9. package/dist/commands/i18n.js +3 -20
  10. package/dist/commands/init.js +47 -40
  11. package/dist/commands/lint.js +3 -20
  12. package/dist/commands/mcp.js +125 -122
  13. package/dist/commands/monitor.js +125 -108
  14. package/dist/commands/review-pr.js +1 -1
  15. package/dist/commands/sdk.js +1 -1
  16. package/dist/config/loader.js +21 -2
  17. package/dist/generator/organizer.d.ts +3 -0
  18. package/dist/generator/organizer.js +4 -9
  19. package/dist/generator/writer.js +2 -10
  20. package/dist/github/pr-comments.js +21 -8
  21. package/dist/plugins/index.js +1 -0
  22. package/dist/scanner/index.js +8 -2
  23. package/dist/template/docs.json +2 -1
  24. package/dist/template/next.config.mjs +3 -1
  25. package/dist/template/package.json +17 -14
  26. package/dist/template/public/favicon.svg +4 -0
  27. package/dist/template/public/search-index.json +1 -1
  28. package/dist/template/scripts/build-search-index.mjs +120 -25
  29. package/dist/template/src/app/api/chat/route.ts +11 -3
  30. package/dist/template/src/app/docs/README.md +28 -0
  31. package/dist/template/src/app/docs/[...slug]/page.tsx +141 -14
  32. package/dist/template/src/app/docs/auth/page.mdx +589 -0
  33. package/dist/template/src/app/docs/autofix/page.mdx +624 -0
  34. package/dist/template/src/app/docs/cli/page.mdx +217 -0
  35. package/dist/template/src/app/docs/config/page.mdx +428 -0
  36. package/dist/template/src/app/docs/configuration/page.mdx +86 -0
  37. package/dist/template/src/app/docs/deployment/page.mdx +112 -0
  38. package/dist/template/src/app/docs/error.tsx +20 -0
  39. package/dist/template/src/app/docs/generator/generator.md +504 -0
  40. package/dist/template/src/app/docs/generator/organizer.md +779 -0
  41. package/dist/template/src/app/docs/generator/page.mdx +613 -0
  42. package/dist/template/src/app/docs/github/page.mdx +502 -0
  43. package/dist/template/src/app/docs/llm/anthropic-client.md +549 -0
  44. package/dist/template/src/app/docs/llm/index.md +471 -0
  45. package/dist/template/src/app/docs/llm/page.mdx +428 -0
  46. package/dist/template/src/app/docs/llms-full.md +256 -0
  47. package/dist/template/src/app/docs/llms.txt +2971 -0
  48. package/dist/template/src/app/docs/not-found.tsx +23 -0
  49. package/dist/template/src/app/docs/page.mdx +0 -3
  50. package/dist/template/src/app/docs/plugins/page.mdx +1793 -0
  51. package/dist/template/src/app/docs/pro/page.mdx +121 -0
  52. package/dist/template/src/app/docs/quickstart/page.mdx +93 -0
  53. package/dist/template/src/app/docs/scanner/content-type.md +599 -0
  54. package/dist/template/src/app/docs/scanner/index.md +212 -0
  55. package/dist/template/src/app/docs/scanner/page.mdx +307 -0
  56. package/dist/template/src/app/docs/scanner/python.md +469 -0
  57. package/dist/template/src/app/docs/scanner/python_parser.md +1056 -0
  58. package/dist/template/src/app/docs/scanner/rust.md +325 -0
  59. package/dist/template/src/app/docs/scanner/typescript.md +201 -0
  60. package/dist/template/src/app/error.tsx +3 -3
  61. package/dist/template/src/app/icon.tsx +29 -0
  62. package/dist/template/src/app/layout.tsx +57 -7
  63. package/dist/template/src/app/not-found.tsx +35 -0
  64. package/dist/template/src/app/page.tsx +95 -11
  65. package/dist/template/src/components/ai-chat.tsx +26 -21
  66. package/dist/template/src/components/breadcrumbs.tsx +56 -12
  67. package/dist/template/src/components/copy-button.tsx +17 -3
  68. package/dist/template/src/components/docs-layout.tsx +202 -8
  69. package/dist/template/src/components/feedback.tsx +4 -2
  70. package/dist/template/src/components/footer.tsx +42 -0
  71. package/dist/template/src/components/header.tsx +56 -20
  72. package/dist/template/src/components/mdx/accordion.tsx +17 -13
  73. package/dist/template/src/components/mdx/callout.tsx +50 -37
  74. package/dist/template/src/components/mdx/card.tsx +24 -12
  75. package/dist/template/src/components/mdx/code-block.tsx +17 -3
  76. package/dist/template/src/components/mdx/code-group.tsx +78 -18
  77. package/dist/template/src/components/mdx/code-playground.tsx +3 -0
  78. package/dist/template/src/components/mdx/go-playground.tsx +3 -0
  79. package/dist/template/src/components/mdx/highlighted-code.tsx +178 -38
  80. package/dist/template/src/components/mdx/python-playground.tsx +2 -0
  81. package/dist/template/src/components/mdx/steps.tsx +6 -6
  82. package/dist/template/src/components/mdx/tabs.tsx +76 -8
  83. package/dist/template/src/components/page-header.tsx +19 -0
  84. package/dist/template/src/components/scroll-to-top.tsx +33 -0
  85. package/dist/template/src/components/search-dialog.tsx +251 -57
  86. package/dist/template/src/components/sidebar.tsx +137 -77
  87. package/dist/template/src/components/table-of-contents.tsx +29 -13
  88. package/dist/template/src/lib/highlight.ts +90 -31
  89. package/dist/template/src/lib/search.ts +14 -4
  90. package/dist/template/src/lib/theme-utils.ts +140 -0
  91. package/dist/template/src/styles/globals.css +397 -84
  92. package/dist/template/src/types/remark-gfm.d.ts +2 -0
  93. package/dist/utils/files.d.ts +9 -0
  94. package/dist/utils/files.js +33 -0
  95. package/dist/utils/validation.d.ts +4 -0
  96. package/dist/utils/validation.js +38 -0
  97. 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 * as Icons from 'lucide-react'
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?: keyof typeof Icons
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 ? Icons[icon] as React.ComponentType<{ size?: number; className?: string }> : null
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)] rounded-lg transition-colors',
18
- href && 'hover:border-[var(--color-primary)] hover:bg-[var(--color-bg-secondary)] cursor-pointer'
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="p-2 bg-[var(--color-primary)]/10 text-[var(--color-primary)] rounded-lg">
23
- <Icon size={20} />
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-medium text-[var(--color-text)]">{title}</h3>
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-sm text-[var(--color-text-secondary)]">
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-6', gridCols[cols])}>
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
- await navigator.clipboard.writeText(codeText)
24
- setCopied(true)
25
- setTimeout(() => setCopied(false), 2000)
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
- await navigator.clipboard.writeText(activeBlock.code)
66
- setCopied(true)
67
- setTimeout(() => setCopied(false), 2000)
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)] rounded-lg overflow-hidden">
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 bg-[var(--color-bg-secondary)] border-b border-[var(--color-border)]">
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-4 py-2 text-sm font-medium transition-colors',
150
+ 'px-3 py-1.5 text-[0.75rem] font-medium rounded-t-lg transition-colors',
97
151
  index === activeIndex
98
- ? 'bg-[var(--color-code-bg)] text-[var(--color-code-text)]'
99
- : 'text-[var(--color-text-secondary)] hover: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={16} /> : <Copy size={16} />}
166
+ {copied ? <Check size={14} /> : <Copy size={14} />}
112
167
  </button>
113
168
  </div>
114
169
 
115
170
  {/* Code content */}
116
- <div className="bg-[var(--color-code-bg)]">
117
- <pre className="!m-0 !rounded-none">
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
- </div>
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
- language = classMatch[1]
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
- await navigator.clipboard.writeText(codeText)
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
- <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}>
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
- <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>
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-6 space-y-6">
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-8 h-8 rounded-full bg-[var(--color-primary)] text-white text-sm font-medium">
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 pb-6">
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-medium text-[var(--color-text)] mb-2">{title}</h3>
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
  }