skrypt-ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -0
  3. package/dist/autofix/index.d.ts +46 -0
  4. package/dist/autofix/index.js +240 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +40 -0
  7. package/dist/commands/autofix.d.ts +2 -0
  8. package/dist/commands/autofix.js +143 -0
  9. package/dist/commands/generate.d.ts +2 -0
  10. package/dist/commands/generate.js +320 -0
  11. package/dist/commands/init.d.ts +2 -0
  12. package/dist/commands/init.js +56 -0
  13. package/dist/commands/review-pr.d.ts +2 -0
  14. package/dist/commands/review-pr.js +117 -0
  15. package/dist/commands/watch.d.ts +2 -0
  16. package/dist/commands/watch.js +142 -0
  17. package/dist/config/index.d.ts +2 -0
  18. package/dist/config/index.js +2 -0
  19. package/dist/config/loader.d.ts +9 -0
  20. package/dist/config/loader.js +82 -0
  21. package/dist/config/types.d.ts +24 -0
  22. package/dist/config/types.js +34 -0
  23. package/dist/generator/generator.d.ts +15 -0
  24. package/dist/generator/generator.js +144 -0
  25. package/dist/generator/index.d.ts +4 -0
  26. package/dist/generator/index.js +4 -0
  27. package/dist/generator/organizer.d.ts +29 -0
  28. package/dist/generator/organizer.js +222 -0
  29. package/dist/generator/types.d.ts +83 -0
  30. package/dist/generator/types.js +1 -0
  31. package/dist/generator/writer.d.ts +28 -0
  32. package/dist/generator/writer.js +320 -0
  33. package/dist/github/pr-comments.d.ts +40 -0
  34. package/dist/github/pr-comments.js +308 -0
  35. package/dist/llm/anthropic-client.d.ts +16 -0
  36. package/dist/llm/anthropic-client.js +92 -0
  37. package/dist/llm/index.d.ts +53 -0
  38. package/dist/llm/index.js +400 -0
  39. package/dist/llm/llm.manual-test.d.ts +1 -0
  40. package/dist/llm/llm.manual-test.js +112 -0
  41. package/dist/llm/llm.mock-test.d.ts +4 -0
  42. package/dist/llm/llm.mock-test.js +79 -0
  43. package/dist/llm/openai-client.d.ts +17 -0
  44. package/dist/llm/openai-client.js +90 -0
  45. package/dist/llm/types.d.ts +60 -0
  46. package/dist/llm/types.js +20 -0
  47. package/dist/scanner/content-type.d.ts +39 -0
  48. package/dist/scanner/content-type.js +194 -0
  49. package/dist/scanner/content-type.test.d.ts +1 -0
  50. package/dist/scanner/content-type.test.js +231 -0
  51. package/dist/scanner/go.d.ts +20 -0
  52. package/dist/scanner/go.js +269 -0
  53. package/dist/scanner/index.d.ts +21 -0
  54. package/dist/scanner/index.js +137 -0
  55. package/dist/scanner/python.d.ts +6 -0
  56. package/dist/scanner/python.js +57 -0
  57. package/dist/scanner/python_parser.py +230 -0
  58. package/dist/scanner/rust.d.ts +23 -0
  59. package/dist/scanner/rust.js +304 -0
  60. package/dist/scanner/scanner.test.d.ts +1 -0
  61. package/dist/scanner/scanner.test.js +210 -0
  62. package/dist/scanner/types.d.ts +50 -0
  63. package/dist/scanner/types.js +1 -0
  64. package/dist/scanner/typescript.d.ts +34 -0
  65. package/dist/scanner/typescript.js +327 -0
  66. package/dist/scanner/typescript.manual-test.d.ts +1 -0
  67. package/dist/scanner/typescript.manual-test.js +112 -0
  68. package/dist/template/docs.json +32 -0
  69. package/dist/template/mdx-components.tsx +62 -0
  70. package/dist/template/next-env.d.ts +6 -0
  71. package/dist/template/next.config.mjs +17 -0
  72. package/dist/template/package.json +39 -0
  73. package/dist/template/postcss.config.mjs +5 -0
  74. package/dist/template/public/search-index.json +1 -0
  75. package/dist/template/scripts/build-search-index.mjs +120 -0
  76. package/dist/template/src/app/api/mock/[...path]/route.ts +224 -0
  77. package/dist/template/src/app/api/openapi/route.ts +48 -0
  78. package/dist/template/src/app/api/rate-limit/route.ts +84 -0
  79. package/dist/template/src/app/docs/[...slug]/page.tsx +81 -0
  80. package/dist/template/src/app/docs/layout.tsx +9 -0
  81. package/dist/template/src/app/docs/page.mdx +67 -0
  82. package/dist/template/src/app/error.tsx +63 -0
  83. package/dist/template/src/app/layout.tsx +71 -0
  84. package/dist/template/src/app/page.tsx +18 -0
  85. package/dist/template/src/app/reference/route.ts +36 -0
  86. package/dist/template/src/app/robots.ts +14 -0
  87. package/dist/template/src/app/sitemap.ts +64 -0
  88. package/dist/template/src/components/breadcrumbs.tsx +41 -0
  89. package/dist/template/src/components/copy-button.tsx +29 -0
  90. package/dist/template/src/components/docs-layout.tsx +35 -0
  91. package/dist/template/src/components/edit-link.tsx +39 -0
  92. package/dist/template/src/components/feedback.tsx +52 -0
  93. package/dist/template/src/components/header.tsx +66 -0
  94. package/dist/template/src/components/mdx/accordion.tsx +48 -0
  95. package/dist/template/src/components/mdx/api-badge.tsx +57 -0
  96. package/dist/template/src/components/mdx/callout.tsx +111 -0
  97. package/dist/template/src/components/mdx/card.tsx +62 -0
  98. package/dist/template/src/components/mdx/changelog.tsx +57 -0
  99. package/dist/template/src/components/mdx/code-block.tsx +42 -0
  100. package/dist/template/src/components/mdx/code-group.tsx +125 -0
  101. package/dist/template/src/components/mdx/code-playground.tsx +322 -0
  102. package/dist/template/src/components/mdx/go-playground.tsx +235 -0
  103. package/dist/template/src/components/mdx/heading.tsx +37 -0
  104. package/dist/template/src/components/mdx/highlighted-code.tsx +89 -0
  105. package/dist/template/src/components/mdx/index.tsx +15 -0
  106. package/dist/template/src/components/mdx/param-table.tsx +71 -0
  107. package/dist/template/src/components/mdx/python-playground.tsx +293 -0
  108. package/dist/template/src/components/mdx/steps.tsx +43 -0
  109. package/dist/template/src/components/mdx/tabs.tsx +81 -0
  110. package/dist/template/src/components/rate-limit-display.tsx +183 -0
  111. package/dist/template/src/components/search-dialog.tsx +178 -0
  112. package/dist/template/src/components/sidebar.tsx +129 -0
  113. package/dist/template/src/components/syntax-theme-selector.tsx +50 -0
  114. package/dist/template/src/components/table-of-contents.tsx +84 -0
  115. package/dist/template/src/components/theme-toggle.tsx +46 -0
  116. package/dist/template/src/components/version-selector.tsx +61 -0
  117. package/dist/template/src/contexts/syntax-theme.tsx +52 -0
  118. package/dist/template/src/lib/highlight.ts +83 -0
  119. package/dist/template/src/lib/search-types.ts +37 -0
  120. package/dist/template/src/lib/search.ts +125 -0
  121. package/dist/template/src/lib/utils.ts +6 -0
  122. package/dist/template/src/styles/globals.css +152 -0
  123. package/dist/template/tsconfig.json +25 -0
  124. package/dist/template/tsconfig.tsbuildinfo +1 -0
  125. package/package.json +72 -0
@@ -0,0 +1,52 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { ThumbsUp, ThumbsDown, Check } from 'lucide-react'
5
+
6
+ export function Feedback() {
7
+ const [submitted, setSubmitted] = useState(false)
8
+ const [selection, setSelection] = useState<'yes' | 'no' | null>(null)
9
+
10
+ async function handleFeedback(helpful: boolean) {
11
+ setSelection(helpful ? 'yes' : 'no')
12
+ // TODO: Send to analytics
13
+ setTimeout(() => setSubmitted(true), 500)
14
+ }
15
+
16
+ if (submitted) {
17
+ return (
18
+ <div className="flex items-center gap-2 text-[13px] text-[var(--color-text-tertiary)]">
19
+ <Check size={16} className="text-emerald-500" />
20
+ Thanks for your feedback!
21
+ </div>
22
+ )
23
+ }
24
+
25
+ return (
26
+ <div className="flex items-center gap-3">
27
+ <span className="text-[13px] text-[var(--color-text-tertiary)]">Was this helpful?</span>
28
+ <div className="flex items-center gap-1">
29
+ <button
30
+ onClick={() => handleFeedback(true)}
31
+ className={`p-1.5 rounded-md transition-colors ${
32
+ selection === 'yes'
33
+ ? 'bg-emerald-100 text-emerald-600'
34
+ : 'text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)]'
35
+ }`}
36
+ >
37
+ <ThumbsUp size={16} />
38
+ </button>
39
+ <button
40
+ onClick={() => handleFeedback(false)}
41
+ className={`p-1.5 rounded-md transition-colors ${
42
+ selection === 'no'
43
+ ? 'bg-red-100 text-red-600'
44
+ : 'text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)]'
45
+ }`}
46
+ >
47
+ <ThumbsDown size={16} />
48
+ </button>
49
+ </div>
50
+ </div>
51
+ )
52
+ }
@@ -0,0 +1,66 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { Search, Menu, X } from 'lucide-react'
5
+ import { useState } from 'react'
6
+ import { SearchDialog } from './search-dialog'
7
+ import { ThemeToggle } from './theme-toggle'
8
+ import { SyntaxThemeSelector } from './syntax-theme-selector'
9
+
10
+ interface HeaderProps {
11
+ onMenuToggle?: () => void
12
+ menuOpen?: boolean
13
+ }
14
+
15
+ export function Header({ onMenuToggle, menuOpen }: HeaderProps) {
16
+ const [searchOpen, setSearchOpen] = useState(false)
17
+
18
+ return (
19
+ <>
20
+ <header className="sticky top-0 z-50 border-b border-[var(--color-border)] bg-[var(--color-bg)]/95 backdrop-blur">
21
+ <div className="flex items-center justify-between h-16 px-4 md:px-6">
22
+ <div className="flex items-center gap-4">
23
+ {/* Mobile menu button */}
24
+ <button
25
+ onClick={onMenuToggle}
26
+ className="md:hidden p-2 -ml-2 text-[var(--color-text-secondary)] hover:text-[var(--color-text)]"
27
+ aria-label={menuOpen ? 'Close menu' : 'Open menu'}
28
+ aria-expanded={menuOpen}
29
+ >
30
+ {menuOpen ? <X size={20} /> : <Menu size={20} />}
31
+ </button>
32
+
33
+ <Link href="/" className="font-semibold text-lg">
34
+ Docs
35
+ </Link>
36
+ <nav className="hidden md:flex items-center gap-4">
37
+ <Link href="/docs" className="text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text)]">
38
+ Documentation
39
+ </Link>
40
+ <Link href="/docs/api" className="text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text)]">
41
+ API Reference
42
+ </Link>
43
+ </nav>
44
+ </div>
45
+
46
+ <div className="flex items-center gap-2">
47
+ <button
48
+ onClick={() => setSearchOpen(true)}
49
+ className="flex items-center gap-2 px-3 py-1.5 text-sm text-[var(--color-text-tertiary)] bg-[var(--color-bg-secondary)] border border-[var(--color-border)] rounded-lg hover:border-[var(--color-border-strong)] transition-colors"
50
+ >
51
+ <Search size={16} />
52
+ <span className="hidden sm:inline">Search docs...</span>
53
+ <kbd className="hidden sm:inline px-1.5 py-0.5 text-xs bg-[var(--color-bg-tertiary)] rounded">
54
+ ⌘K
55
+ </kbd>
56
+ </button>
57
+ <SyntaxThemeSelector />
58
+ <ThemeToggle />
59
+ </div>
60
+ </div>
61
+ </header>
62
+
63
+ <SearchDialog open={searchOpen} onClose={() => setSearchOpen(false)} />
64
+ </>
65
+ )
66
+ }
@@ -0,0 +1,48 @@
1
+ 'use client'
2
+
3
+ import { useState, ReactNode } from 'react'
4
+ import { ChevronDown } from 'lucide-react'
5
+ import { cn } from '@/lib/utils'
6
+
7
+ interface AccordionProps {
8
+ title: string
9
+ defaultOpen?: boolean
10
+ children: ReactNode
11
+ }
12
+
13
+ export function Accordion({ title, defaultOpen = false, children }: AccordionProps) {
14
+ const [open, setOpen] = useState(defaultOpen)
15
+
16
+ return (
17
+ <div className="border border-[var(--color-border)] rounded-lg my-4">
18
+ <button
19
+ onClick={() => setOpen(!open)}
20
+ className="flex items-center justify-between w-full px-4 py-3 text-left font-medium hover:bg-[var(--color-bg-secondary)] transition-colors"
21
+ >
22
+ {title}
23
+ <ChevronDown
24
+ size={20}
25
+ className={cn(
26
+ 'text-[var(--color-text-tertiary)] transition-transform',
27
+ open && 'rotate-180'
28
+ )}
29
+ />
30
+ </button>
31
+ {open && (
32
+ <div className="px-4 pb-4 pt-0 border-t border-[var(--color-border)]">
33
+ <div className="pt-3 text-[var(--color-text-secondary)]">
34
+ {children}
35
+ </div>
36
+ </div>
37
+ )}
38
+ </div>
39
+ )
40
+ }
41
+
42
+ interface AccordionGroupProps {
43
+ children: ReactNode
44
+ }
45
+
46
+ export function AccordionGroup({ children }: AccordionGroupProps) {
47
+ return <div className="my-6 space-y-2">{children}</div>
48
+ }
@@ -0,0 +1,57 @@
1
+ type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
2
+ type Status = 'stable' | 'beta' | 'deprecated' | 'experimental'
3
+
4
+ const methodColors: Record<Method, string> = {
5
+ GET: 'bg-emerald-100 text-emerald-700 border-emerald-200',
6
+ POST: 'bg-blue-100 text-blue-700 border-blue-200',
7
+ PUT: 'bg-amber-100 text-amber-700 border-amber-200',
8
+ PATCH: 'bg-purple-100 text-purple-700 border-purple-200',
9
+ DELETE: 'bg-red-100 text-red-700 border-red-200',
10
+ }
11
+
12
+ const statusColors: Record<Status, string> = {
13
+ stable: 'bg-emerald-100 text-emerald-700',
14
+ beta: 'bg-blue-100 text-blue-700',
15
+ deprecated: 'bg-red-100 text-red-700',
16
+ experimental: 'bg-purple-100 text-purple-700',
17
+ }
18
+
19
+ interface MethodBadgeProps {
20
+ method: Method
21
+ }
22
+
23
+ export function MethodBadge({ method }: MethodBadgeProps) {
24
+ return (
25
+ <span className={`inline-block px-2 py-0.5 text-[11px] font-bold uppercase rounded border ${methodColors[method]}`}>
26
+ {method}
27
+ </span>
28
+ )
29
+ }
30
+
31
+ interface StatusBadgeProps {
32
+ status: Status
33
+ }
34
+
35
+ export function StatusBadge({ status }: StatusBadgeProps) {
36
+ return (
37
+ <span className={`inline-block px-2 py-0.5 text-[11px] font-medium uppercase rounded ${statusColors[status]}`}>
38
+ {status}
39
+ </span>
40
+ )
41
+ }
42
+
43
+ interface EndpointProps {
44
+ method: Method
45
+ path: string
46
+ status?: Status
47
+ }
48
+
49
+ export function Endpoint({ method, path, status }: EndpointProps) {
50
+ return (
51
+ <div className="flex items-center gap-3 my-4 p-3 bg-[var(--color-bg-secondary)] border border-[var(--color-border)] rounded-lg">
52
+ <MethodBadge method={method} />
53
+ <code className="text-[14px] font-semibold">{path}</code>
54
+ {status && <StatusBadge status={status} />}
55
+ </div>
56
+ )
57
+ }
@@ -0,0 +1,111 @@
1
+ import { cn } from '@/lib/utils'
2
+ import {
3
+ Info as InfoIcon,
4
+ AlertTriangle,
5
+ CheckCircle,
6
+ XCircle,
7
+ Lightbulb
8
+ } from 'lucide-react'
9
+ import { ReactNode } from 'react'
10
+
11
+ type CalloutType = 'info' | 'warning' | 'success' | 'error' | 'tip' | 'note'
12
+
13
+ interface CalloutProps {
14
+ type?: CalloutType
15
+ title?: string
16
+ children: ReactNode
17
+ }
18
+
19
+ const config: Record<CalloutType, {
20
+ icon: typeof InfoIcon
21
+ bg: string
22
+ border: string
23
+ title: string
24
+ text: string
25
+ }> = {
26
+ info: {
27
+ icon: InfoIcon,
28
+ bg: 'bg-blue-50 dark:bg-blue-950/30',
29
+ border: 'border-blue-200 dark:border-blue-800',
30
+ title: 'text-blue-800 dark:text-blue-300',
31
+ text: 'text-blue-700 dark:text-blue-400',
32
+ },
33
+ warning: {
34
+ icon: AlertTriangle,
35
+ bg: 'bg-amber-50 dark:bg-amber-950/30',
36
+ border: 'border-amber-200 dark:border-amber-800',
37
+ title: 'text-amber-800 dark:text-amber-300',
38
+ text: 'text-amber-700 dark:text-amber-400',
39
+ },
40
+ success: {
41
+ icon: CheckCircle,
42
+ bg: 'bg-green-50 dark:bg-green-950/30',
43
+ border: 'border-green-200 dark:border-green-800',
44
+ title: 'text-green-800 dark:text-green-300',
45
+ text: 'text-green-700 dark:text-green-400',
46
+ },
47
+ error: {
48
+ icon: XCircle,
49
+ bg: 'bg-red-50 dark:bg-red-950/30',
50
+ border: 'border-red-200 dark:border-red-800',
51
+ title: 'text-red-800 dark:text-red-300',
52
+ text: 'text-red-700 dark:text-red-400',
53
+ },
54
+ tip: {
55
+ icon: Lightbulb,
56
+ bg: 'bg-purple-50 dark:bg-purple-950/30',
57
+ border: 'border-purple-200 dark:border-purple-800',
58
+ title: 'text-purple-800 dark:text-purple-300',
59
+ text: 'text-purple-700 dark:text-purple-400',
60
+ },
61
+ note: {
62
+ icon: InfoIcon,
63
+ bg: 'bg-gray-50 dark:bg-gray-900/50',
64
+ border: 'border-gray-200 dark:border-gray-700',
65
+ title: 'text-gray-800 dark:text-gray-200',
66
+ text: 'text-gray-700 dark:text-gray-300',
67
+ },
68
+ }
69
+
70
+ export function Callout({ type = 'info', title, children }: CalloutProps) {
71
+ const { icon: Icon, bg, border, title: titleColor, text } = config[type]
72
+
73
+ return (
74
+ <div className={cn('my-6 p-4 border rounded-lg', bg, border)}>
75
+ <div className="flex gap-3">
76
+ <Icon className={cn('w-5 h-5 mt-0.5 flex-shrink-0', titleColor)} />
77
+ <div>
78
+ {title && (
79
+ <div className={cn('font-medium mb-1', titleColor)}>{title}</div>
80
+ )}
81
+ <div className={cn('text-sm', text)}>{children}</div>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ )
86
+ }
87
+
88
+ // Convenience components
89
+ export function Info(props: Omit<CalloutProps, 'type'>) {
90
+ return <Callout type="info" {...props} />
91
+ }
92
+
93
+ export function Warning(props: Omit<CalloutProps, 'type'>) {
94
+ return <Callout type="warning" {...props} />
95
+ }
96
+
97
+ export function Success(props: Omit<CalloutProps, 'type'>) {
98
+ return <Callout type="success" {...props} />
99
+ }
100
+
101
+ export function Error(props: Omit<CalloutProps, 'type'>) {
102
+ return <Callout type="error" {...props} />
103
+ }
104
+
105
+ export function Tip(props: Omit<CalloutProps, 'type'>) {
106
+ return <Callout type="tip" {...props} />
107
+ }
108
+
109
+ export function Note(props: Omit<CalloutProps, 'type'>) {
110
+ return <Callout type="note" {...props} />
111
+ }
@@ -0,0 +1,62 @@
1
+ import Link from 'next/link'
2
+ import { cn } from '@/lib/utils'
3
+ import * as Icons from 'lucide-react'
4
+
5
+ interface CardProps {
6
+ title: string
7
+ icon?: keyof typeof Icons
8
+ href?: string
9
+ children?: React.ReactNode
10
+ }
11
+
12
+ export function Card({ title, icon, href, children }: CardProps) {
13
+ const Icon = icon ? Icons[icon] as React.ComponentType<{ size?: number; className?: string }> : null
14
+
15
+ const content = (
16
+ <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'
19
+ )}>
20
+ <div className="flex items-start gap-3">
21
+ {Icon && (
22
+ <div className="p-2 bg-[var(--color-primary)]/10 text-[var(--color-primary)] rounded-lg">
23
+ <Icon size={20} />
24
+ </div>
25
+ )}
26
+ <div>
27
+ <h3 className="font-medium text-[var(--color-text)]">{title}</h3>
28
+ {children && (
29
+ <p className="mt-1 text-sm text-[var(--color-text-secondary)]">
30
+ {children}
31
+ </p>
32
+ )}
33
+ </div>
34
+ </div>
35
+ </div>
36
+ )
37
+
38
+ if (href) {
39
+ return <Link href={href}>{content}</Link>
40
+ }
41
+
42
+ return content
43
+ }
44
+
45
+ interface CardGroupProps {
46
+ cols?: 1 | 2 | 3
47
+ children: React.ReactNode
48
+ }
49
+
50
+ export function CardGroup({ cols = 2, children }: CardGroupProps) {
51
+ const gridCols = {
52
+ 1: 'grid-cols-1',
53
+ 2: 'grid-cols-1 md:grid-cols-2',
54
+ 3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
55
+ }
56
+
57
+ return (
58
+ <div className={cn('grid gap-4 my-6', gridCols[cols])}>
59
+ {children}
60
+ </div>
61
+ )
62
+ }
@@ -0,0 +1,57 @@
1
+ import { ReactNode } from 'react'
2
+
3
+ interface ChangelogEntryProps {
4
+ version: string
5
+ date: string
6
+ children: ReactNode
7
+ }
8
+
9
+ export function ChangelogEntry({ version, date, children }: ChangelogEntryProps) {
10
+ return (
11
+ <div className="relative pl-6 pb-8 border-l-2 border-[var(--color-border)] last:pb-0">
12
+ <div className="absolute -left-[9px] top-0 w-4 h-4 rounded-full bg-[var(--color-primary)] border-2 border-[var(--color-bg)]" />
13
+ <div className="flex items-center gap-3 mb-2">
14
+ <span className="font-semibold text-[15px]">v{version}</span>
15
+ <span className="text-[13px] text-[var(--color-text-tertiary)]">{date}</span>
16
+ </div>
17
+ <div className="text-[14px] text-[var(--color-text-secondary)]">
18
+ {children}
19
+ </div>
20
+ </div>
21
+ )
22
+ }
23
+
24
+ interface ChangelogProps {
25
+ children: ReactNode
26
+ }
27
+
28
+ export function Changelog({ children }: ChangelogProps) {
29
+ return (
30
+ <div className="my-8">
31
+ {children}
32
+ </div>
33
+ )
34
+ }
35
+
36
+ // Change type badges
37
+ type ChangeType = 'added' | 'changed' | 'deprecated' | 'removed' | 'fixed' | 'security'
38
+
39
+ const typeColors: Record<ChangeType, string> = {
40
+ added: 'bg-emerald-100 text-emerald-700',
41
+ changed: 'bg-blue-100 text-blue-700',
42
+ deprecated: 'bg-yellow-100 text-yellow-700',
43
+ removed: 'bg-red-100 text-red-700',
44
+ fixed: 'bg-purple-100 text-purple-700',
45
+ security: 'bg-orange-100 text-orange-700',
46
+ }
47
+
48
+ export function Change({ type, children }: { type: ChangeType; children: ReactNode }) {
49
+ return (
50
+ <div className="flex items-start gap-2 my-1">
51
+ <span className={`px-1.5 py-0.5 text-[11px] font-medium uppercase rounded ${typeColors[type]}`}>
52
+ {type}
53
+ </span>
54
+ <span>{children}</span>
55
+ </div>
56
+ )
57
+ }
@@ -0,0 +1,42 @@
1
+ 'use client'
2
+
3
+ import { useState, ReactNode, isValidElement, Children } from 'react'
4
+ import { Copy, Check } from 'lucide-react'
5
+
6
+ interface CodeBlockProps {
7
+ children: ReactNode
8
+ className?: string
9
+ }
10
+
11
+ export function CodeBlock({ children, className }: CodeBlockProps) {
12
+ const [copied, setCopied] = useState(false)
13
+
14
+ // Extract code text from children
15
+ let codeText = ''
16
+ Children.forEach(children, (child) => {
17
+ if (isValidElement(child) && child.type === 'code') {
18
+ codeText = (child.props as { children?: string }).children || ''
19
+ }
20
+ })
21
+
22
+ async function handleCopy() {
23
+ await navigator.clipboard.writeText(codeText)
24
+ setCopied(true)
25
+ setTimeout(() => setCopied(false), 2000)
26
+ }
27
+
28
+ return (
29
+ <div className="relative group my-4">
30
+ <pre className={className}>
31
+ {children}
32
+ </pre>
33
+ <button
34
+ onClick={handleCopy}
35
+ 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"
36
+ title={copied ? 'Copied!' : 'Copy code'}
37
+ >
38
+ {copied ? <Check size={14} /> : <Copy size={14} />}
39
+ </button>
40
+ </div>
41
+ )
42
+ }
@@ -0,0 +1,125 @@
1
+ 'use client'
2
+
3
+ import { useState, Children, isValidElement, ReactNode, ReactElement } from 'react'
4
+ import { cn } from '@/lib/utils'
5
+ import { Copy, Check } from 'lucide-react'
6
+
7
+ interface CodeGroupProps {
8
+ children: ReactNode
9
+ }
10
+
11
+ interface CodeBlockInfo {
12
+ language: string
13
+ filename?: string
14
+ code: string
15
+ }
16
+
17
+ interface PreProps {
18
+ children?: ReactElement<CodeProps>
19
+ }
20
+
21
+ interface CodeProps {
22
+ className?: string
23
+ children?: string
24
+ 'data-filename'?: string
25
+ }
26
+
27
+ function extractCodeBlocks(children: ReactNode): CodeBlockInfo[] {
28
+ const blocks: CodeBlockInfo[] = []
29
+
30
+ Children.forEach(children, (child) => {
31
+ if (isValidElement<PreProps>(child) && child.type === 'pre') {
32
+ const codeElement = child.props.children
33
+ if (isValidElement<CodeProps>(codeElement) && codeElement.type === 'code') {
34
+ const className = codeElement.props.className || ''
35
+ const match = className.match(/language-(\w+)/)
36
+ const language = match ? match[1] : 'text'
37
+
38
+ const filename = codeElement.props['data-filename'] ||
39
+ codeElement.props.children?.toString().match(/^\/\/ (.+)\n/)?.[1]
40
+
41
+ blocks.push({
42
+ language,
43
+ filename,
44
+ code: codeElement.props.children?.toString() || '',
45
+ })
46
+ }
47
+ }
48
+ })
49
+
50
+ return blocks
51
+ }
52
+
53
+ export function CodeGroup({ children }: CodeGroupProps) {
54
+ const blocks = extractCodeBlocks(children)
55
+ const [activeIndex, setActiveIndex] = useState(0)
56
+ const [copied, setCopied] = useState(false)
57
+
58
+ if (blocks.length === 0) {
59
+ return <div>{children}</div>
60
+ }
61
+
62
+ const activeBlock = blocks[activeIndex]
63
+
64
+ const copyToClipboard = async () => {
65
+ await navigator.clipboard.writeText(activeBlock.code)
66
+ setCopied(true)
67
+ setTimeout(() => setCopied(false), 2000)
68
+ }
69
+
70
+ const getLanguageLabel = (lang: string, filename?: string) => {
71
+ if (filename) return filename
72
+ const labels: Record<string, string> = {
73
+ typescript: 'TypeScript',
74
+ javascript: 'JavaScript',
75
+ python: 'Python',
76
+ bash: 'Bash',
77
+ shell: 'Shell',
78
+ json: 'JSON',
79
+ yaml: 'YAML',
80
+ go: 'Go',
81
+ rust: 'Rust',
82
+ }
83
+ return labels[lang] || lang
84
+ }
85
+
86
+ return (
87
+ <div className="my-6 border border-[var(--color-border)] rounded-lg overflow-hidden">
88
+ {/* 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">
91
+ {blocks.map((block, index) => (
92
+ <button
93
+ key={`${block.language}-${block.filename || index}`}
94
+ onClick={() => setActiveIndex(index)}
95
+ className={cn(
96
+ 'px-4 py-2 text-sm font-medium transition-colors',
97
+ index === activeIndex
98
+ ? 'bg-[var(--color-code-bg)] text-[var(--color-code-text)]'
99
+ : 'text-[var(--color-text-secondary)] hover:text-[var(--color-text)]'
100
+ )}
101
+ >
102
+ {getLanguageLabel(block.language, block.filename)}
103
+ </button>
104
+ ))}
105
+ </div>
106
+ <button
107
+ onClick={copyToClipboard}
108
+ className="p-2 mr-2 text-[var(--color-text-tertiary)] hover:text-[var(--color-text)] transition-colors"
109
+ title="Copy code"
110
+ >
111
+ {copied ? <Check size={16} /> : <Copy size={16} />}
112
+ </button>
113
+ </div>
114
+
115
+ {/* Code content */}
116
+ <div className="bg-[var(--color-code-bg)]">
117
+ <pre className="!m-0 !rounded-none">
118
+ <code className={`language-${activeBlock.language}`}>
119
+ {activeBlock.code}
120
+ </code>
121
+ </pre>
122
+ </div>
123
+ </div>
124
+ )
125
+ }