skrypt-ai 0.4.2 → 0.6.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/dist/auth/index.d.ts +13 -3
- package/dist/auth/index.js +101 -9
- package/dist/auth/keychain.d.ts +5 -0
- package/dist/auth/keychain.js +82 -0
- package/dist/auth/notices.d.ts +3 -0
- package/dist/auth/notices.js +42 -0
- package/dist/autofix/index.d.ts +0 -4
- package/dist/autofix/index.js +10 -24
- package/dist/capture/browser.d.ts +11 -0
- package/dist/capture/browser.js +173 -0
- package/dist/capture/diff.d.ts +23 -0
- package/dist/capture/diff.js +52 -0
- package/dist/capture/index.d.ts +23 -0
- package/dist/capture/index.js +210 -0
- package/dist/capture/naming.d.ts +17 -0
- package/dist/capture/naming.js +45 -0
- package/dist/capture/parser.d.ts +15 -0
- package/dist/capture/parser.js +80 -0
- package/dist/capture/types.d.ts +57 -0
- package/dist/capture/types.js +1 -0
- package/dist/cli.js +20 -3
- package/dist/commands/autofix.js +136 -120
- package/dist/commands/cron.js +58 -47
- package/dist/commands/deploy.js +123 -102
- package/dist/commands/generate.js +125 -7
- package/dist/commands/heal.d.ts +10 -0
- package/dist/commands/heal.js +201 -0
- package/dist/commands/i18n.js +146 -111
- package/dist/commands/import.d.ts +2 -0
- package/dist/commands/import.js +157 -0
- package/dist/commands/init.js +19 -7
- package/dist/commands/lint.js +50 -44
- package/dist/commands/llms-txt.js +59 -49
- package/dist/commands/login.js +63 -34
- package/dist/commands/mcp.js +6 -0
- package/dist/commands/monitor.js +13 -8
- package/dist/commands/qa.d.ts +2 -0
- package/dist/commands/qa.js +43 -0
- package/dist/commands/review-pr.js +108 -92
- package/dist/commands/sdk.js +128 -122
- package/dist/commands/security.d.ts +2 -0
- package/dist/commands/security.js +109 -0
- package/dist/commands/test.js +91 -92
- package/dist/commands/version.js +104 -75
- package/dist/commands/watch.js +130 -114
- package/dist/config/types.js +2 -2
- package/dist/context-hub/index.d.ts +23 -0
- package/dist/context-hub/index.js +179 -0
- package/dist/context-hub/mappings.d.ts +8 -0
- package/dist/context-hub/mappings.js +55 -0
- package/dist/context-hub/types.d.ts +33 -0
- package/dist/context-hub/types.js +1 -0
- package/dist/generator/generator.js +39 -6
- package/dist/generator/types.d.ts +7 -0
- package/dist/generator/writer.d.ts +3 -1
- package/dist/generator/writer.js +36 -7
- package/dist/importers/confluence.d.ts +5 -0
- package/dist/importers/confluence.js +137 -0
- package/dist/importers/detect.d.ts +20 -0
- package/dist/importers/detect.js +121 -0
- package/dist/importers/docusaurus.d.ts +5 -0
- package/dist/importers/docusaurus.js +279 -0
- package/dist/importers/gitbook.d.ts +5 -0
- package/dist/importers/gitbook.js +189 -0
- package/dist/importers/github.d.ts +8 -0
- package/dist/importers/github.js +99 -0
- package/dist/importers/index.d.ts +15 -0
- package/dist/importers/index.js +30 -0
- package/dist/importers/markdown.d.ts +6 -0
- package/dist/importers/markdown.js +105 -0
- package/dist/importers/mintlify.d.ts +5 -0
- package/dist/importers/mintlify.js +172 -0
- package/dist/importers/notion.d.ts +5 -0
- package/dist/importers/notion.js +174 -0
- package/dist/importers/readme.d.ts +5 -0
- package/dist/importers/readme.js +184 -0
- package/dist/importers/transform.d.ts +90 -0
- package/dist/importers/transform.js +457 -0
- package/dist/importers/types.d.ts +37 -0
- package/dist/importers/types.js +1 -0
- package/dist/llm/anthropic-client.d.ts +1 -0
- package/dist/llm/anthropic-client.js +3 -1
- package/dist/llm/index.d.ts +6 -4
- package/dist/llm/index.js +76 -261
- package/dist/llm/openai-client.d.ts +1 -0
- package/dist/llm/openai-client.js +7 -2
- package/dist/plugins/index.js +7 -0
- package/dist/qa/checks.d.ts +10 -0
- package/dist/qa/checks.js +492 -0
- package/dist/qa/fixes.d.ts +30 -0
- package/dist/qa/fixes.js +277 -0
- package/dist/qa/index.d.ts +29 -0
- package/dist/qa/index.js +187 -0
- package/dist/qa/types.d.ts +24 -0
- package/dist/qa/types.js +1 -0
- package/dist/scanner/csharp.d.ts +23 -0
- package/dist/scanner/csharp.js +421 -0
- package/dist/scanner/index.js +53 -26
- package/dist/scanner/java.d.ts +39 -0
- package/dist/scanner/java.js +318 -0
- package/dist/scanner/kotlin.d.ts +23 -0
- package/dist/scanner/kotlin.js +389 -0
- package/dist/scanner/php.d.ts +57 -0
- package/dist/scanner/php.js +351 -0
- package/dist/scanner/python.js +17 -0
- package/dist/scanner/ruby.d.ts +36 -0
- package/dist/scanner/ruby.js +431 -0
- package/dist/scanner/swift.d.ts +25 -0
- package/dist/scanner/swift.js +392 -0
- package/dist/scanner/types.d.ts +1 -1
- package/dist/template/content/docs/_navigation.json +46 -0
- package/dist/template/content/docs/_sidebars.json +684 -0
- package/dist/template/content/docs/core.md +4544 -0
- package/dist/template/content/docs/index.mdx +89 -0
- package/dist/template/content/docs/integrations.md +1158 -0
- package/dist/template/content/docs/llms-full.md +403 -0
- package/dist/template/content/docs/llms.txt +4588 -0
- package/dist/template/content/docs/other.md +10379 -0
- package/dist/template/content/docs/tools.md +746 -0
- package/dist/template/content/docs/types.md +531 -0
- package/dist/template/docs.json +13 -11
- package/dist/template/mdx-components.tsx +27 -2
- package/dist/template/package.json +6 -0
- package/dist/template/public/search-index.json +1 -1
- package/dist/template/scripts/build-search-index.mjs +149 -13
- package/dist/template/src/app/api/chat/route.ts +83 -128
- package/dist/template/src/app/docs/[...slug]/page.tsx +75 -20
- package/dist/template/src/app/docs/llms-full.md +151 -4
- package/dist/template/src/app/docs/llms.txt +2464 -847
- package/dist/template/src/app/docs/page.mdx +48 -38
- package/dist/template/src/app/layout.tsx +3 -1
- package/dist/template/src/app/page.tsx +22 -8
- package/dist/template/src/components/ai-chat.tsx +73 -64
- package/dist/template/src/components/breadcrumbs.tsx +21 -23
- package/dist/template/src/components/copy-button.tsx +13 -9
- package/dist/template/src/components/copy-page-button.tsx +54 -0
- package/dist/template/src/components/docs-layout.tsx +37 -25
- package/dist/template/src/components/header.tsx +51 -10
- package/dist/template/src/components/mdx/card.tsx +17 -3
- package/dist/template/src/components/mdx/code-block.tsx +13 -9
- package/dist/template/src/components/mdx/code-group.tsx +13 -8
- package/dist/template/src/components/mdx/heading.tsx +15 -2
- package/dist/template/src/components/mdx/highlighted-code.tsx +13 -8
- package/dist/template/src/components/mdx/index.tsx +2 -0
- package/dist/template/src/components/mdx/mermaid.tsx +110 -0
- package/dist/template/src/components/mdx/screenshot.tsx +150 -0
- package/dist/template/src/components/scroll-to-hash.tsx +48 -0
- package/dist/template/src/components/sidebar.tsx +12 -18
- package/dist/template/src/components/table-of-contents.tsx +9 -0
- package/dist/template/src/lib/highlight.ts +3 -88
- package/dist/template/src/lib/navigation.ts +159 -0
- package/dist/template/src/lib/search-types.ts +4 -1
- package/dist/template/src/lib/search.ts +30 -7
- package/dist/template/src/styles/globals.css +17 -6
- package/dist/utils/files.d.ts +9 -1
- package/dist/utils/files.js +59 -10
- package/dist/utils/validation.d.ts +0 -3
- package/dist/utils/validation.js +0 -26
- package/package.json +5 -1
|
@@ -13,16 +13,23 @@ import { Feedback } from './feedback'
|
|
|
13
13
|
import { EditLink } from './edit-link'
|
|
14
14
|
import { Footer } from './footer'
|
|
15
15
|
import { ScrollToTop } from './scroll-to-top'
|
|
16
|
+
import { ScrollToHash } from './scroll-to-hash'
|
|
17
|
+
import { CopyPageButton } from './copy-page-button'
|
|
18
|
+
import {
|
|
19
|
+
hasTabs,
|
|
20
|
+
getTabGroups,
|
|
21
|
+
getAllPagesFlat,
|
|
22
|
+
findPageInNavigation,
|
|
23
|
+
type Navigation,
|
|
24
|
+
type NavTab,
|
|
25
|
+
type NavGroup,
|
|
26
|
+
} from '@/lib/navigation'
|
|
16
27
|
|
|
17
28
|
interface DocsConfig {
|
|
18
29
|
name?: string
|
|
19
30
|
headerLinks?: Array<{ title: string; path: string }>
|
|
20
31
|
logo?: string
|
|
21
|
-
navigation:
|
|
22
|
-
group: string
|
|
23
|
-
icon?: string
|
|
24
|
-
pages: Array<{ title: string; path: string; description?: string }>
|
|
25
|
-
}>
|
|
32
|
+
navigation: Navigation
|
|
26
33
|
footer?: {
|
|
27
34
|
links?: Array<{ title: string; url: string }>
|
|
28
35
|
}
|
|
@@ -33,13 +40,9 @@ interface DocsConfig {
|
|
|
33
40
|
}
|
|
34
41
|
}
|
|
35
42
|
|
|
36
|
-
function getAllPages(config: DocsConfig): Array<{ title: string; path: string }> {
|
|
37
|
-
return config.navigation.flatMap((group) => group.pages)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
43
|
function PrevNextNav({ docsConfig }: { docsConfig: DocsConfig }) {
|
|
41
44
|
const pathname = usePathname()
|
|
42
|
-
const allPages =
|
|
45
|
+
const allPages = getAllPagesFlat(docsConfig.navigation)
|
|
43
46
|
const currentIndex = allPages.findIndex((p) => p.path === pathname)
|
|
44
47
|
|
|
45
48
|
if (currentIndex === -1) return null
|
|
@@ -103,15 +106,21 @@ function MobileTOC() {
|
|
|
103
106
|
'returned validator function', 'requirements',
|
|
104
107
|
'when results are returned', 'when each value is returned',
|
|
105
108
|
'validationresult shape',
|
|
109
|
+
'example', 'related', 'notes', 'properties',
|
|
106
110
|
])
|
|
107
111
|
|
|
112
|
+
const seenIds = new Set<string>()
|
|
113
|
+
|
|
108
114
|
elements.forEach((el) => {
|
|
109
115
|
const text = el.textContent || ''
|
|
110
116
|
const normalized = text.toLowerCase().trim()
|
|
111
117
|
if (genericHeadings.has(normalized)) return
|
|
118
|
+
if (el.closest('.card-link') || el.closest('[data-card]')) return
|
|
112
119
|
|
|
113
120
|
const id = el.id || normalized.replace(/\s+/g, '-')
|
|
114
121
|
if (!el.id) el.id = id
|
|
122
|
+
if (seenIds.has(id)) return
|
|
123
|
+
seenIds.add(id)
|
|
115
124
|
items.push({ id, text, level: parseInt(el.tagName[1]) })
|
|
116
125
|
})
|
|
117
126
|
|
|
@@ -154,17 +163,6 @@ function MobileTOC() {
|
|
|
154
163
|
)
|
|
155
164
|
}
|
|
156
165
|
|
|
157
|
-
function getPageInfo(docsConfig: DocsConfig, pathname: string): { title?: string; description?: string } {
|
|
158
|
-
for (const group of docsConfig.navigation) {
|
|
159
|
-
for (const page of group.pages) {
|
|
160
|
-
if (page.path === pathname) {
|
|
161
|
-
return { title: page.title, description: page.description }
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return {}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
166
|
export function DocsLayout({
|
|
169
167
|
children,
|
|
170
168
|
docsConfig,
|
|
@@ -178,19 +176,28 @@ export function DocsLayout({
|
|
|
178
176
|
}) {
|
|
179
177
|
const [menuOpen, setMenuOpen] = useState(false)
|
|
180
178
|
const [frontmatterDescription, setFrontmatterDescription] = useState<string | undefined>()
|
|
179
|
+
const [rawContent, setRawContent] = useState<string | undefined>()
|
|
181
180
|
const pathname = usePathname()
|
|
182
181
|
|
|
183
|
-
// Read frontmatter description passed from the server component via data
|
|
182
|
+
// Read frontmatter description and raw content passed from the server component via data attributes
|
|
184
183
|
useEffect(() => {
|
|
185
184
|
const el = document.querySelector('[data-page-description]')
|
|
186
185
|
setFrontmatterDescription(el?.getAttribute('data-page-description') ?? undefined)
|
|
186
|
+
|
|
187
|
+
const rawEl = document.getElementById('raw-page-content')
|
|
188
|
+
const b64 = rawEl?.getAttribute('data-content')
|
|
189
|
+
setRawContent(b64 ? atob(b64) : undefined)
|
|
187
190
|
}, [pathname])
|
|
188
191
|
|
|
189
192
|
// Resolve title from props, falling back to docs.json navigation lookup
|
|
190
|
-
const pageInfo =
|
|
193
|
+
const pageInfo = findPageInNavigation(docsConfig.navigation, pathname)
|
|
191
194
|
const resolvedTitle = pageTitle ?? pageInfo.title
|
|
192
195
|
const resolvedDescription = pageDescription ?? frontmatterDescription ?? pageInfo.description
|
|
193
196
|
|
|
197
|
+
// Tab support
|
|
198
|
+
const tabs: NavTab[] | undefined = hasTabs(docsConfig.navigation) ? docsConfig.navigation : undefined
|
|
199
|
+
const sidebarGroups: NavGroup[] = getTabGroups(docsConfig.navigation, pathname)
|
|
200
|
+
|
|
194
201
|
return (
|
|
195
202
|
<div className="min-h-screen flex flex-col">
|
|
196
203
|
<Header
|
|
@@ -199,18 +206,22 @@ export function DocsLayout({
|
|
|
199
206
|
siteName={docsConfig.name}
|
|
200
207
|
navLinks={docsConfig.headerLinks}
|
|
201
208
|
logo={docsConfig.logo}
|
|
209
|
+
tabs={tabs}
|
|
202
210
|
/>
|
|
203
211
|
<div className="flex flex-1">
|
|
204
212
|
<Sidebar
|
|
205
213
|
open={menuOpen}
|
|
206
214
|
onClose={() => setMenuOpen(false)}
|
|
207
|
-
|
|
215
|
+
groups={sidebarGroups}
|
|
208
216
|
/>
|
|
209
217
|
<main id="main-content" className="flex-1 min-w-0 px-6 md:px-10 py-8 lg:ml-[var(--sidebar-width)] lg:mr-[var(--toc-width)]">
|
|
210
218
|
<div className="max-w-[var(--content-max-width)] mx-auto">
|
|
211
219
|
<Breadcrumbs docsConfig={docsConfig} />
|
|
212
220
|
{resolvedTitle && (
|
|
213
|
-
<
|
|
221
|
+
<div className="flex items-start justify-between gap-4">
|
|
222
|
+
<PageHeader title={resolvedTitle} description={resolvedDescription} />
|
|
223
|
+
{rawContent && <CopyPageButton content={rawContent} />}
|
|
224
|
+
</div>
|
|
214
225
|
)}
|
|
215
226
|
<MobileTOC />
|
|
216
227
|
<article className="prose">
|
|
@@ -232,6 +243,7 @@ export function DocsLayout({
|
|
|
232
243
|
</div>
|
|
233
244
|
<Footer docsConfig={docsConfig} />
|
|
234
245
|
<ScrollToTop />
|
|
246
|
+
<ScrollToHash />
|
|
235
247
|
</div>
|
|
236
248
|
)
|
|
237
249
|
}
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import Link from 'next/link'
|
|
4
|
-
import {
|
|
4
|
+
import { usePathname } from 'next/navigation'
|
|
5
|
+
import { Search, Menu, X, BookOpen, Code, FileText, Settings, Key, Zap, Shield, Globe, Terminal, Database, Cloud, Lock, Rocket, Star, Heart, Package, Puzzle, GitBranch, Cpu, List } from 'lucide-react'
|
|
5
6
|
import { useState, useEffect } from 'react'
|
|
6
7
|
import { SearchDialog } from './search-dialog'
|
|
7
8
|
import { ThemeToggle } from './theme-toggle'
|
|
8
9
|
import { SyntaxThemeSelector } from './syntax-theme-selector'
|
|
10
|
+
import type { NavTab } from '@/lib/navigation'
|
|
11
|
+
import { getActiveTab, getTabHref } from '@/lib/navigation'
|
|
12
|
+
|
|
13
|
+
const tabIconMap: Record<string, React.ComponentType<{ size?: number; className?: string }>> = {
|
|
14
|
+
BookOpen, Code, FileText, Settings, Key, Zap, Shield, Globe,
|
|
15
|
+
Terminal, Database, Cloud, Lock, Rocket, Search, Star, Heart,
|
|
16
|
+
Package, Puzzle, GitBranch, Cpu, List,
|
|
17
|
+
}
|
|
9
18
|
|
|
10
19
|
interface HeaderProps {
|
|
11
20
|
onMenuToggle?: () => void
|
|
@@ -13,11 +22,13 @@ interface HeaderProps {
|
|
|
13
22
|
siteName?: string
|
|
14
23
|
navLinks?: Array<{ title: string; path: string }>
|
|
15
24
|
logo?: string
|
|
25
|
+
tabs?: NavTab[]
|
|
16
26
|
}
|
|
17
27
|
|
|
18
|
-
export function Header({ onMenuToggle, menuOpen, siteName = 'Docs', navLinks, logo }: HeaderProps) {
|
|
28
|
+
export function Header({ onMenuToggle, menuOpen, siteName = 'Docs', navLinks, logo, tabs }: HeaderProps) {
|
|
19
29
|
const [searchOpen, setSearchOpen] = useState(false)
|
|
20
30
|
const [isMac, setIsMac] = useState(true)
|
|
31
|
+
const pathname = usePathname()
|
|
21
32
|
|
|
22
33
|
useEffect(() => {
|
|
23
34
|
setIsMac(
|
|
@@ -37,10 +48,12 @@ export function Header({ onMenuToggle, menuOpen, siteName = 'Docs', navLinks, lo
|
|
|
37
48
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
38
49
|
}, [])
|
|
39
50
|
|
|
51
|
+
const activeTab = tabs ? getActiveTab(pathname, tabs) : null
|
|
52
|
+
|
|
40
53
|
return (
|
|
41
54
|
<>
|
|
42
|
-
<header className="sticky top-0 z-50
|
|
43
|
-
<div className="flex items-center justify-between h-
|
|
55
|
+
<header className="sticky top-0 z-50 border-b border-[var(--color-border)] bg-[var(--color-bg)]/80 backdrop-blur-xl">
|
|
56
|
+
<div className="flex items-center justify-between h-[var(--header-height)] px-4 md:px-6">
|
|
44
57
|
<div className="flex items-center gap-6">
|
|
45
58
|
{/* Mobile menu button */}
|
|
46
59
|
<button
|
|
@@ -61,12 +74,14 @@ export function Header({ onMenuToggle, menuOpen, siteName = 'Docs', navLinks, lo
|
|
|
61
74
|
</Link>
|
|
62
75
|
|
|
63
76
|
<nav className="hidden md:flex items-center gap-1">
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
77
|
+
{!tabs && (
|
|
78
|
+
<Link
|
|
79
|
+
href="/docs"
|
|
80
|
+
className="px-3 py-1.5 text-[0.8125rem] text-[var(--color-text-secondary)] hover:text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)] rounded-lg hover:no-underline"
|
|
81
|
+
>
|
|
82
|
+
Documentation
|
|
83
|
+
</Link>
|
|
84
|
+
)}
|
|
70
85
|
{navLinks?.map((link) => (
|
|
71
86
|
<Link
|
|
72
87
|
key={link.path}
|
|
@@ -94,6 +109,32 @@ export function Header({ onMenuToggle, menuOpen, siteName = 'Docs', navLinks, lo
|
|
|
94
109
|
<ThemeToggle />
|
|
95
110
|
</div>
|
|
96
111
|
</div>
|
|
112
|
+
|
|
113
|
+
{/* Tab bar */}
|
|
114
|
+
{tabs && tabs.length > 0 && (
|
|
115
|
+
<div className="flex items-center gap-0 px-4 md:px-6 overflow-x-auto scrollbar-hide border-t border-[var(--color-border)]/50">
|
|
116
|
+
{tabs.map((tab) => {
|
|
117
|
+
const isActive = activeTab?.tab === tab.tab
|
|
118
|
+
const Icon = tab.icon ? tabIconMap[tab.icon] : null
|
|
119
|
+
const href = getTabHref(tab)
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<Link
|
|
123
|
+
key={tab.tab}
|
|
124
|
+
href={href}
|
|
125
|
+
className={`flex items-center gap-1.5 px-3 py-2 text-[0.8125rem] whitespace-nowrap border-b-2 transition-colors hover:no-underline ${
|
|
126
|
+
isActive
|
|
127
|
+
? 'border-[var(--color-primary)] text-[var(--color-primary)] font-medium'
|
|
128
|
+
: 'border-transparent text-[var(--color-text-tertiary)] hover:text-[var(--color-text)]'
|
|
129
|
+
}`}
|
|
130
|
+
>
|
|
131
|
+
{Icon && <Icon size={14} />}
|
|
132
|
+
{tab.tab}
|
|
133
|
+
</Link>
|
|
134
|
+
)
|
|
135
|
+
})}
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
97
138
|
</header>
|
|
98
139
|
|
|
99
140
|
<SearchDialog open={searchOpen} onClose={() => setSearchOpen(false)} />
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
1
3
|
import Link from 'next/link'
|
|
2
4
|
import { cn } from '@/lib/utils'
|
|
3
5
|
import {
|
|
@@ -25,7 +27,7 @@ export function Card({ title, icon, href, children }: CardProps) {
|
|
|
25
27
|
const Icon = icon ? iconMap[icon] : null
|
|
26
28
|
|
|
27
29
|
const content = (
|
|
28
|
-
<div className={cn(
|
|
30
|
+
<div data-card="" className={cn(
|
|
29
31
|
'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
32
|
href && 'cursor-pointer hover:!border-[var(--color-primary)]'
|
|
31
33
|
)}>
|
|
@@ -36,7 +38,7 @@ export function Card({ title, icon, href, children }: CardProps) {
|
|
|
36
38
|
</div>
|
|
37
39
|
)}
|
|
38
40
|
<div>
|
|
39
|
-
<
|
|
41
|
+
<p className="font-semibold text-[0.9375rem] text-[var(--color-text)] !mt-0 !mb-0">{title}</p>
|
|
40
42
|
{children && (
|
|
41
43
|
<div className="mt-1.5 text-[0.8125rem] leading-relaxed text-[var(--color-text-secondary)]">
|
|
42
44
|
{children}
|
|
@@ -48,7 +50,19 @@ export function Card({ title, icon, href, children }: CardProps) {
|
|
|
48
50
|
)
|
|
49
51
|
|
|
50
52
|
if (href) {
|
|
51
|
-
|
|
53
|
+
if (href.startsWith('#')) {
|
|
54
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
55
|
+
e.preventDefault()
|
|
56
|
+
const id = href.slice(1)
|
|
57
|
+
const target = document.getElementById(id)
|
|
58
|
+
if (target) {
|
|
59
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
60
|
+
window.history.pushState(null, '', href)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return <a href={href} onClick={handleClick} className="card-link no-underline hover:no-underline">{content}</a>
|
|
64
|
+
}
|
|
65
|
+
return <Link href={href} className="card-link no-underline hover:no-underline">{content}</Link>
|
|
52
66
|
}
|
|
53
67
|
|
|
54
68
|
return content
|
|
@@ -25,15 +25,19 @@ export function CodeBlock({ children, className }: CodeBlockProps) {
|
|
|
25
25
|
setCopied(true)
|
|
26
26
|
setTimeout(() => setCopied(false), 2000)
|
|
27
27
|
} catch {
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
// Deprecated fallback for older browsers that don't support navigator.clipboard
|
|
29
|
+
try {
|
|
30
|
+
const textarea = document.createElement('textarea')
|
|
31
|
+
textarea.value = codeText
|
|
32
|
+
textarea.style.position = 'fixed'
|
|
33
|
+
textarea.style.opacity = '0'
|
|
34
|
+
document.body.appendChild(textarea)
|
|
35
|
+
textarea.select()
|
|
36
|
+
document.execCommand('copy') // deprecated but kept for old browser compat
|
|
37
|
+
document.body.removeChild(textarea)
|
|
38
|
+
} catch {
|
|
39
|
+
// Copy failed in both methods — silently ignore
|
|
40
|
+
}
|
|
37
41
|
setCopied(true)
|
|
38
42
|
setTimeout(() => setCopied(false), 2000)
|
|
39
43
|
}
|
|
@@ -93,14 +93,19 @@ export function CodeGroup({ children }: CodeGroupProps) {
|
|
|
93
93
|
setCopied(true)
|
|
94
94
|
setTimeout(() => setCopied(false), 2000)
|
|
95
95
|
} catch {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
// Deprecated fallback for older browsers that don't support navigator.clipboard
|
|
97
|
+
try {
|
|
98
|
+
const textarea = document.createElement('textarea')
|
|
99
|
+
textarea.value = activeBlock.code
|
|
100
|
+
textarea.style.position = 'fixed'
|
|
101
|
+
textarea.style.opacity = '0'
|
|
102
|
+
document.body.appendChild(textarea)
|
|
103
|
+
textarea.select()
|
|
104
|
+
document.execCommand('copy') // deprecated but kept for old browser compat
|
|
105
|
+
document.body.removeChild(textarea)
|
|
106
|
+
} catch {
|
|
107
|
+
// Copy failed in both methods — silently ignore
|
|
108
|
+
}
|
|
104
109
|
setCopied(true)
|
|
105
110
|
setTimeout(() => setCopied(false), 2000)
|
|
106
111
|
}
|
|
@@ -9,10 +9,23 @@ interface HeadingProps {
|
|
|
9
9
|
id?: string
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/** Extract plain text from React children (handles <code>, <em>, etc.) */
|
|
13
|
+
function getTextContent(node: ReactNode): string {
|
|
14
|
+
if (typeof node === 'string') return node
|
|
15
|
+
if (typeof node === 'number') return String(node)
|
|
16
|
+
if (!node) return ''
|
|
17
|
+
if (Array.isArray(node)) return node.map(getTextContent).join('')
|
|
18
|
+
if (typeof node === 'object' && 'props' in node) {
|
|
19
|
+
return getTextContent((node as any).props.children)
|
|
20
|
+
}
|
|
21
|
+
return ''
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
export function Heading({ level, children, id }: HeadingProps) {
|
|
13
25
|
const Tag = `h${level}` as 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
|
|
14
|
-
const
|
|
15
|
-
|
|
26
|
+
const text = getTextContent(children)
|
|
27
|
+
const headingId = id || (text
|
|
28
|
+
? text.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '')
|
|
16
29
|
: undefined)
|
|
17
30
|
|
|
18
31
|
return (
|
|
@@ -159,14 +159,19 @@ export function HighlightedCode({ children, className }: HighlightedCodeProps) {
|
|
|
159
159
|
try {
|
|
160
160
|
await navigator.clipboard.writeText(codeText)
|
|
161
161
|
} catch {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
162
|
+
// Deprecated fallback for older browsers that don't support navigator.clipboard
|
|
163
|
+
try {
|
|
164
|
+
const textarea = document.createElement('textarea')
|
|
165
|
+
textarea.value = codeText
|
|
166
|
+
textarea.style.position = 'fixed'
|
|
167
|
+
textarea.style.opacity = '0'
|
|
168
|
+
document.body.appendChild(textarea)
|
|
169
|
+
textarea.select()
|
|
170
|
+
document.execCommand('copy') // deprecated but kept for old browser compat
|
|
171
|
+
document.body.removeChild(textarea)
|
|
172
|
+
} catch {
|
|
173
|
+
// Copy failed in both methods — silently ignore
|
|
174
|
+
}
|
|
170
175
|
}
|
|
171
176
|
setCopied(true)
|
|
172
177
|
setShowToast(true)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef, useId } from 'react'
|
|
4
|
+
|
|
5
|
+
interface MermaidProps {
|
|
6
|
+
chart: string
|
|
7
|
+
className?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Mermaid({ chart, className }: MermaidProps) {
|
|
11
|
+
const [svg, setSvg] = useState<string | null>(null)
|
|
12
|
+
const [error, setError] = useState<string | null>(null)
|
|
13
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
14
|
+
const id = useId().replace(/:/g, '-')
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
let cancelled = false
|
|
18
|
+
|
|
19
|
+
async function renderDiagram() {
|
|
20
|
+
try {
|
|
21
|
+
const mermaid = (await import('mermaid')).default
|
|
22
|
+
|
|
23
|
+
// Detect dark mode
|
|
24
|
+
const isDark = document.documentElement.classList.contains('dark')
|
|
25
|
+
|
|
26
|
+
mermaid.initialize({
|
|
27
|
+
startOnLoad: false,
|
|
28
|
+
theme: isDark ? 'dark' : 'default',
|
|
29
|
+
securityLevel: 'strict',
|
|
30
|
+
fontFamily: 'inherit',
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const { svg: rendered } = await mermaid.render(`mermaid-${id}`, chart.trim())
|
|
34
|
+
if (!cancelled) {
|
|
35
|
+
setSvg(rendered)
|
|
36
|
+
setError(null)
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (!cancelled) {
|
|
40
|
+
setError(err instanceof Error ? err.message : 'Failed to render diagram')
|
|
41
|
+
setSvg(null)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
renderDiagram()
|
|
47
|
+
return () => { cancelled = true }
|
|
48
|
+
}, [chart, id])
|
|
49
|
+
|
|
50
|
+
// Re-render when theme changes
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const observer = new MutationObserver((mutations) => {
|
|
53
|
+
for (const mutation of mutations) {
|
|
54
|
+
if (mutation.attributeName === 'class') {
|
|
55
|
+
// Theme class changed, re-render
|
|
56
|
+
const isDark = document.documentElement.classList.contains('dark')
|
|
57
|
+
import('mermaid').then(({ default: mermaid }) => {
|
|
58
|
+
mermaid.initialize({
|
|
59
|
+
startOnLoad: false,
|
|
60
|
+
theme: isDark ? 'dark' : 'default',
|
|
61
|
+
securityLevel: 'strict',
|
|
62
|
+
fontFamily: 'inherit',
|
|
63
|
+
})
|
|
64
|
+
// Remove old rendered element if it exists
|
|
65
|
+
const oldEl = document.getElementById(`mermaid-${id}`)
|
|
66
|
+
if (oldEl) oldEl.remove()
|
|
67
|
+
|
|
68
|
+
mermaid.render(`mermaid-${id}`, chart.trim()).then(({ svg: rendered }) => {
|
|
69
|
+
setSvg(rendered)
|
|
70
|
+
setError(null)
|
|
71
|
+
}).catch(() => {
|
|
72
|
+
// Ignore re-render errors
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
|
|
80
|
+
return () => observer.disconnect()
|
|
81
|
+
}, [chart, id])
|
|
82
|
+
|
|
83
|
+
if (error) {
|
|
84
|
+
return (
|
|
85
|
+
<div className={`my-5 p-4 rounded-2xl border border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950 ${className || ''}`}>
|
|
86
|
+
<p className="text-sm text-red-600 dark:text-red-400 font-medium mb-1">Diagram error</p>
|
|
87
|
+
<pre className="text-xs text-red-500 dark:text-red-400 whitespace-pre-wrap">{error}</pre>
|
|
88
|
+
</div>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!svg) {
|
|
93
|
+
return (
|
|
94
|
+
<div className={`my-5 flex items-center justify-center p-8 rounded-2xl border border-[var(--color-border)] bg-[var(--color-bg-secondary)] ${className || ''}`}>
|
|
95
|
+
<div className="flex items-center gap-2 text-sm text-[var(--color-text-tertiary)]">
|
|
96
|
+
<div className="w-4 h-4 border-2 border-[var(--color-text-tertiary)] border-t-transparent rounded-full animate-spin" />
|
|
97
|
+
Loading diagram...
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div
|
|
105
|
+
ref={containerRef}
|
|
106
|
+
className={`my-5 flex justify-center p-4 rounded-2xl border border-[var(--color-border)] bg-[var(--color-bg)] overflow-x-auto [&_svg]:max-w-full ${className || ''}`}
|
|
107
|
+
dangerouslySetInnerHTML={{ __html: svg }}
|
|
108
|
+
/>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react'
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
|
|
6
|
+
interface ScreenshotProps {
|
|
7
|
+
url: string
|
|
8
|
+
selector?: string
|
|
9
|
+
alt: string
|
|
10
|
+
caption?: string
|
|
11
|
+
className?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Compute the expected screenshot filename from url + selector.
|
|
16
|
+
* Must match the naming logic in src/capture/naming.ts.
|
|
17
|
+
*/
|
|
18
|
+
function computeFilename(url: string, selector?: string): string {
|
|
19
|
+
let pathname: string
|
|
20
|
+
try {
|
|
21
|
+
pathname = new URL(url).pathname
|
|
22
|
+
} catch {
|
|
23
|
+
pathname = url
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let slug = pathname.replace(/^\/+/, '')
|
|
27
|
+
slug = slug.replace(/\//g, '-')
|
|
28
|
+
if (!slug) slug = 'index'
|
|
29
|
+
slug = slug.replace(/[^a-zA-Z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')
|
|
30
|
+
|
|
31
|
+
if (selector) {
|
|
32
|
+
let selectorSlug = selector.replace(/^[.#]/, '')
|
|
33
|
+
selectorSlug = selectorSlug.replace(/[^a-zA-Z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')
|
|
34
|
+
slug = `${slug}--${selectorSlug}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return `${slug}.png`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getIsDark(): boolean {
|
|
41
|
+
if (typeof document === 'undefined') return false
|
|
42
|
+
if (document.documentElement.classList.contains('dark')) return true
|
|
43
|
+
if (document.documentElement.classList.contains('light')) return false
|
|
44
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function TrafficDots() {
|
|
48
|
+
return (
|
|
49
|
+
<div className="flex items-center gap-1.5">
|
|
50
|
+
<span className="block w-2 h-2 rounded-full bg-[#ff5f57]" />
|
|
51
|
+
<span className="block w-2 h-2 rounded-full bg-[#febc2e]" />
|
|
52
|
+
<span className="block w-2 h-2 rounded-full bg-[#28c840]" />
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function Screenshot({ url, selector, alt, caption, className }: ScreenshotProps) {
|
|
58
|
+
const [isDark, setIsDark] = useState(false)
|
|
59
|
+
const [hasError, setHasError] = useState(false)
|
|
60
|
+
const [fallbackToLight, setFallbackToLight] = useState(false)
|
|
61
|
+
|
|
62
|
+
const lightFile = computeFilename(url, selector)
|
|
63
|
+
const darkFile = lightFile.replace(/\.png$/, '-dark.png')
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
setIsDark(getIsDark())
|
|
67
|
+
|
|
68
|
+
const mq = window.matchMedia('(prefers-color-scheme: dark)')
|
|
69
|
+
const onMediaChange = () => setIsDark(getIsDark())
|
|
70
|
+
mq.addEventListener('change', onMediaChange)
|
|
71
|
+
|
|
72
|
+
const observer = new MutationObserver(() => {
|
|
73
|
+
setIsDark(getIsDark())
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
observer.observe(document.documentElement, {
|
|
77
|
+
attributes: true,
|
|
78
|
+
attributeFilter: ['class'],
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
return () => {
|
|
82
|
+
mq.removeEventListener('change', onMediaChange)
|
|
83
|
+
observer.disconnect()
|
|
84
|
+
}
|
|
85
|
+
}, [])
|
|
86
|
+
|
|
87
|
+
const imageSrc = `/screenshots/${isDark && !fallbackToLight ? darkFile : lightFile}`
|
|
88
|
+
|
|
89
|
+
// Extract display URL for browser chrome
|
|
90
|
+
let displayUrl: string
|
|
91
|
+
try {
|
|
92
|
+
const parsed = new URL(url)
|
|
93
|
+
displayUrl = `${parsed.origin}${parsed.pathname}`
|
|
94
|
+
} catch {
|
|
95
|
+
displayUrl = url
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<figure
|
|
100
|
+
className={cn('my-4', className)}
|
|
101
|
+
data-screenshot-url={url}
|
|
102
|
+
data-screenshot-selector={selector}
|
|
103
|
+
>
|
|
104
|
+
<div className="rounded-xl border border-[var(--color-border)] overflow-hidden">
|
|
105
|
+
{/* Browser chrome with actual URL */}
|
|
106
|
+
<div className="flex items-center gap-3 px-3.5 py-2.5 border-b border-[var(--color-border)] bg-[var(--color-bg-secondary)]">
|
|
107
|
+
<TrafficDots />
|
|
108
|
+
<div className="flex-1 h-5 rounded-md bg-[var(--color-bg)] border border-[var(--color-border)] px-2.5 flex items-center">
|
|
109
|
+
<span className="text-[10px] text-[var(--color-text-tertiary)] select-none truncate">
|
|
110
|
+
{displayUrl}
|
|
111
|
+
</span>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
{/* Screenshot image */}
|
|
116
|
+
<div className="[&>img]:!m-0 [&>img]:!rounded-none [&>img]:block [&>img]:w-full">
|
|
117
|
+
{hasError ? (
|
|
118
|
+
<div className="flex items-center justify-center p-8 bg-[var(--color-bg-secondary)] text-[var(--color-text-tertiary)] text-sm">
|
|
119
|
+
<p>
|
|
120
|
+
Screenshot not captured yet — run{' '}
|
|
121
|
+
<code className="px-1.5 py-0.5 rounded bg-[var(--color-bg)] border border-[var(--color-border)] text-xs">
|
|
122
|
+
skrypt heal --screenshots
|
|
123
|
+
</code>
|
|
124
|
+
</p>
|
|
125
|
+
</div>
|
|
126
|
+
) : (
|
|
127
|
+
<img
|
|
128
|
+
src={imageSrc}
|
|
129
|
+
alt={alt}
|
|
130
|
+
loading="lazy"
|
|
131
|
+
onError={() => {
|
|
132
|
+
if (isDark && !fallbackToLight) {
|
|
133
|
+
setFallbackToLight(true)
|
|
134
|
+
} else {
|
|
135
|
+
setHasError(true)
|
|
136
|
+
}
|
|
137
|
+
}}
|
|
138
|
+
/>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
{caption && (
|
|
144
|
+
<figcaption className="text-sm text-[var(--color-text-tertiary)] mt-2 text-center">
|
|
145
|
+
{caption}
|
|
146
|
+
</figcaption>
|
|
147
|
+
)}
|
|
148
|
+
</figure>
|
|
149
|
+
)
|
|
150
|
+
}
|