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,71 +2,68 @@
|
|
|
2
2
|
|
|
3
3
|
import Link from 'next/link'
|
|
4
4
|
import { usePathname } from 'next/navigation'
|
|
5
|
-
import { ChevronRight } from 'lucide-react'
|
|
6
|
-
import { useState } from 'react'
|
|
5
|
+
import { ChevronRight, BookOpen, Code, FileText, Settings, Key, Zap, Shield, Globe, Terminal, Database, Cloud, Lock, Rocket, Search, Star, Heart, Package, Puzzle, GitBranch, Cpu } from 'lucide-react'
|
|
6
|
+
import { useState, useMemo } from 'react'
|
|
7
7
|
import { cn } from '@/lib/utils'
|
|
8
8
|
|
|
9
|
-
interface
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
items?: NavItem[]
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface NavPage {
|
|
16
|
-
title: string
|
|
17
|
-
path: string
|
|
18
|
-
}
|
|
9
|
+
interface NavPage { title: string; path: string; pages?: NavPage[] }
|
|
10
|
+
interface NavGroup { group: string; icon?: string; pages: (NavPage | NavGroup)[] }
|
|
11
|
+
interface DocsConfig { navigation: NavGroup[] }
|
|
19
12
|
|
|
20
|
-
interface
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
interface SidebarProps {
|
|
14
|
+
open?: boolean
|
|
15
|
+
onClose?: () => void
|
|
16
|
+
docsConfig: DocsConfig
|
|
23
17
|
}
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
const iconMap: Record<string, React.ComponentType<{ size?: number; className?: string }>> = {
|
|
20
|
+
BookOpen, Code, FileText, Settings, Key, Zap, Shield, Globe,
|
|
21
|
+
Terminal, Database, Cloud, Lock, Rocket, Search, Star, Heart,
|
|
22
|
+
Package, Puzzle, GitBranch, Cpu,
|
|
27
23
|
}
|
|
28
24
|
|
|
29
|
-
function
|
|
30
|
-
return
|
|
31
|
-
title: group.group,
|
|
32
|
-
items: group.pages.map((page) => ({
|
|
33
|
-
title: page.title,
|
|
34
|
-
href: page.path,
|
|
35
|
-
})),
|
|
36
|
-
}))
|
|
25
|
+
function isNavGroup(item: NavPage | NavGroup): item is NavGroup {
|
|
26
|
+
return 'group' in item && !('path' in item)
|
|
37
27
|
}
|
|
38
28
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
29
|
+
/** Check if any descendant page matches the current pathname */
|
|
30
|
+
function hasActivePage(pages: (NavPage | NavGroup)[], pathname: string): boolean {
|
|
31
|
+
for (const item of pages) {
|
|
32
|
+
if (isNavGroup(item)) {
|
|
33
|
+
if (hasActivePage(item.pages, pathname)) return true
|
|
34
|
+
} else {
|
|
35
|
+
if (item.path === pathname) return true
|
|
36
|
+
if (item.pages && hasActivePage(item.pages, pathname)) return true
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return false
|
|
43
40
|
}
|
|
44
41
|
|
|
45
42
|
export function Sidebar({ open, onClose, docsConfig }: SidebarProps) {
|
|
46
43
|
const pathname = usePathname()
|
|
47
|
-
const navigation = buildNavigation(docsConfig)
|
|
48
44
|
|
|
49
45
|
return (
|
|
50
46
|
<>
|
|
51
|
-
{/* Mobile overlay */}
|
|
52
47
|
{open && (
|
|
53
48
|
<div
|
|
54
|
-
className="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm
|
|
49
|
+
className="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm lg:hidden"
|
|
50
|
+
role="button"
|
|
51
|
+
tabIndex={0}
|
|
52
|
+
aria-label="Close sidebar"
|
|
55
53
|
onClick={onClose}
|
|
54
|
+
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClose?.() } }}
|
|
56
55
|
/>
|
|
57
56
|
)}
|
|
58
|
-
|
|
59
|
-
{/* Sidebar */}
|
|
60
57
|
<aside
|
|
61
58
|
className={cn(
|
|
62
|
-
'fixed left-0 top-[var(--header-height)] bottom-0 w-[var(--sidebar-width)]
|
|
63
|
-
'
|
|
64
|
-
open ? 'translate-x-0' : '-translate-x-full
|
|
59
|
+
'fixed left-0 top-[var(--header-height)] bottom-0 w-[var(--sidebar-width)] bg-[var(--color-bg)] overflow-y-auto z-50 transition-transform duration-100 pl-2 sidebar-scroll-shadow',
|
|
60
|
+
'lg:translate-x-0',
|
|
61
|
+
open ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
|
|
65
62
|
)}
|
|
66
63
|
>
|
|
67
|
-
<nav className="
|
|
68
|
-
{navigation.map((
|
|
69
|
-
<
|
|
64
|
+
<nav className="py-4 pr-2 space-y-6 divide-y divide-[var(--color-border)]">
|
|
65
|
+
{docsConfig.navigation.map((group) => (
|
|
66
|
+
<SidebarGroup key={group.group} group={group} pathname={pathname} onNavigate={onClose} level={0} />
|
|
70
67
|
))}
|
|
71
68
|
</nav>
|
|
72
69
|
</aside>
|
|
@@ -74,55 +71,117 @@ export function Sidebar({ open, onClose, docsConfig }: SidebarProps) {
|
|
|
74
71
|
)
|
|
75
72
|
}
|
|
76
73
|
|
|
77
|
-
function
|
|
78
|
-
const
|
|
79
|
-
const [expanded, setExpanded] = useState(
|
|
74
|
+
function SidebarGroup({ group, pathname, onNavigate, level }: { group: NavGroup; pathname: string; onNavigate?: () => void; level: number }) {
|
|
75
|
+
const isActive = useMemo(() => hasActivePage(group.pages, pathname), [group.pages, pathname])
|
|
76
|
+
const [expanded, setExpanded] = useState(isActive)
|
|
80
77
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
'block px-3 py-1.5 text-sm rounded-lg transition-colors',
|
|
88
|
-
pathname === section.href
|
|
89
|
-
? 'bg-[var(--color-primary-light)] text-[var(--color-primary)] font-medium'
|
|
90
|
-
: 'text-[var(--color-text-secondary)] hover:text-[var(--color-text)] hover:bg-[var(--color-bg-secondary)]'
|
|
91
|
-
)}
|
|
92
|
-
>
|
|
93
|
-
{section.title}
|
|
94
|
-
</Link>
|
|
95
|
-
)
|
|
96
|
-
}
|
|
78
|
+
const Icon = group.icon ? iconMap[group.icon] : null
|
|
79
|
+
|
|
80
|
+
// Limit nesting to 3 levels
|
|
81
|
+
if (level > 2) return null
|
|
82
|
+
|
|
83
|
+
const paddingLeft = level === 0 ? 'pl-4' : level === 1 ? 'pl-10' : 'pl-14'
|
|
97
84
|
|
|
98
85
|
return (
|
|
99
86
|
<div>
|
|
100
87
|
<button
|
|
101
88
|
onClick={() => setExpanded(!expanded)}
|
|
102
|
-
|
|
89
|
+
aria-expanded={expanded}
|
|
90
|
+
className={cn(
|
|
91
|
+
'flex items-center gap-2.5 w-full mb-2 text-[0.8125rem] font-medium text-[var(--color-text)] hover:text-[var(--color-text)] transition-colors',
|
|
92
|
+
paddingLeft
|
|
93
|
+
)}
|
|
103
94
|
>
|
|
104
|
-
{section.title}
|
|
105
95
|
<ChevronRight
|
|
106
96
|
size={14}
|
|
107
|
-
className={cn('transition-transform duration-
|
|
97
|
+
className={cn('text-[var(--color-text-tertiary)] transition-transform duration-100', expanded && 'rotate-90')}
|
|
108
98
|
/>
|
|
99
|
+
{Icon && <Icon size={14} className="text-[var(--color-text-tertiary)]" />}
|
|
100
|
+
{group.group}
|
|
109
101
|
</button>
|
|
110
102
|
{expanded && (
|
|
111
|
-
<div className="
|
|
112
|
-
{
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
103
|
+
<div className="space-y-px">
|
|
104
|
+
{group.pages.map((item) => {
|
|
105
|
+
if (isNavGroup(item)) {
|
|
106
|
+
return (
|
|
107
|
+
<SidebarGroup
|
|
108
|
+
key={item.group}
|
|
109
|
+
group={item}
|
|
110
|
+
pathname={pathname}
|
|
111
|
+
onNavigate={onNavigate}
|
|
112
|
+
level={level + 1}
|
|
113
|
+
/>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<SidebarPageItem
|
|
119
|
+
key={item.path}
|
|
120
|
+
page={item}
|
|
121
|
+
pathname={pathname}
|
|
122
|
+
onNavigate={onNavigate}
|
|
123
|
+
level={level}
|
|
124
|
+
/>
|
|
125
|
+
)
|
|
126
|
+
})}
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function SidebarPageItem({ page, pathname, onNavigate, level }: { page: NavPage; pathname: string; onNavigate?: () => void; level: number }) {
|
|
134
|
+
// Limit nesting depth to prevent infinite recursion
|
|
135
|
+
if (level > 3) return null
|
|
136
|
+
const hasChildren = page.pages && page.pages.length > 0
|
|
137
|
+
const isActive = useMemo(() => {
|
|
138
|
+
if (page.path === pathname) return true
|
|
139
|
+
if (page.pages && hasActivePage(page.pages, pathname)) return true
|
|
140
|
+
return false
|
|
141
|
+
}, [page, pathname])
|
|
142
|
+
const [expanded, setExpanded] = useState(isActive)
|
|
143
|
+
|
|
144
|
+
const paddingClass = level === 0 ? 'pl-10' : level === 1 ? 'pl-14' : 'pl-18'
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div>
|
|
148
|
+
<div className="flex items-center">
|
|
149
|
+
<Link
|
|
150
|
+
href={page.path}
|
|
151
|
+
onClick={onNavigate}
|
|
152
|
+
className={cn(
|
|
153
|
+
'block flex-1 pr-3 py-1.5 text-[0.8125rem] rounded-lg transition-colors',
|
|
154
|
+
paddingClass,
|
|
155
|
+
pathname === page.path
|
|
156
|
+
? 'bg-[var(--color-primary)]/10 text-[var(--color-primary)]'
|
|
157
|
+
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-primary)]/10 hover:text-[var(--color-primary)]'
|
|
158
|
+
)}
|
|
159
|
+
>
|
|
160
|
+
{page.title}
|
|
161
|
+
</Link>
|
|
162
|
+
{hasChildren && (
|
|
163
|
+
<button
|
|
164
|
+
onClick={() => setExpanded(!expanded)}
|
|
165
|
+
aria-expanded={expanded}
|
|
166
|
+
className="p-1 mr-2 text-[var(--color-text-tertiary)] hover:text-[var(--color-text)]"
|
|
167
|
+
>
|
|
168
|
+
<ChevronRight
|
|
169
|
+
size={12}
|
|
170
|
+
className={cn('transition-transform duration-100', expanded && 'rotate-90')}
|
|
171
|
+
/>
|
|
172
|
+
</button>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
{hasChildren && expanded && (
|
|
176
|
+
<div className="space-y-px">
|
|
177
|
+
{page.pages!.map((child) => (
|
|
178
|
+
<SidebarPageItem
|
|
179
|
+
key={child.path}
|
|
180
|
+
page={child}
|
|
181
|
+
pathname={pathname}
|
|
182
|
+
onNavigate={onNavigate}
|
|
183
|
+
level={level + 1}
|
|
184
|
+
/>
|
|
126
185
|
))}
|
|
127
186
|
</div>
|
|
128
187
|
)}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from 'react'
|
|
4
|
+
import { usePathname } from 'next/navigation'
|
|
4
5
|
import { cn } from '@/lib/utils'
|
|
5
6
|
|
|
6
7
|
interface Heading {
|
|
@@ -12,6 +13,7 @@ interface Heading {
|
|
|
12
13
|
export function TableOfContents() {
|
|
13
14
|
const [headings, setHeadings] = useState<Heading[]>([])
|
|
14
15
|
const [activeId, setActiveId] = useState('')
|
|
16
|
+
const pathname = usePathname()
|
|
15
17
|
|
|
16
18
|
useEffect(() => {
|
|
17
19
|
const article = document.querySelector('article')
|
|
@@ -20,19 +22,33 @@ export function TableOfContents() {
|
|
|
20
22
|
const elements = article.querySelectorAll('h2, h3')
|
|
21
23
|
const items: Heading[] = []
|
|
22
24
|
|
|
25
|
+
// Generic headings that repeat in API reference pages — skip them in TOC
|
|
26
|
+
const genericHeadings = new Set([
|
|
27
|
+
'parameters', 'returns', 'return value', 'return type',
|
|
28
|
+
'returned validator function', 'requirements',
|
|
29
|
+
'when results are returned', 'when each value is returned',
|
|
30
|
+
'validationresult shape',
|
|
31
|
+
])
|
|
32
|
+
|
|
23
33
|
elements.forEach((el) => {
|
|
24
|
-
const
|
|
34
|
+
const text = el.textContent || ''
|
|
35
|
+
const normalized = text.toLowerCase().trim()
|
|
36
|
+
|
|
37
|
+
// Skip generic/repeated headings
|
|
38
|
+
if (genericHeadings.has(normalized)) return
|
|
39
|
+
|
|
40
|
+
const id = el.id || normalized.replace(/\s+/g, '-')
|
|
25
41
|
if (!el.id) el.id = id
|
|
26
42
|
|
|
27
43
|
items.push({
|
|
28
44
|
id,
|
|
29
|
-
text
|
|
45
|
+
text,
|
|
30
46
|
level: parseInt(el.tagName[1]),
|
|
31
47
|
})
|
|
32
48
|
})
|
|
33
49
|
|
|
34
50
|
setHeadings(items)
|
|
35
|
-
}, [])
|
|
51
|
+
}, [pathname])
|
|
36
52
|
|
|
37
53
|
useEffect(() => {
|
|
38
54
|
const observer = new IntersectionObserver(
|
|
@@ -43,7 +59,7 @@ export function TableOfContents() {
|
|
|
43
59
|
}
|
|
44
60
|
})
|
|
45
61
|
},
|
|
46
|
-
{ rootMargin: '-80px 0px -
|
|
62
|
+
{ rootMargin: '-80px 0px -60% 0px' }
|
|
47
63
|
)
|
|
48
64
|
|
|
49
65
|
headings.forEach(({ id }) => {
|
|
@@ -57,13 +73,13 @@ export function TableOfContents() {
|
|
|
57
73
|
if (headings.length === 0) return null
|
|
58
74
|
|
|
59
75
|
return (
|
|
60
|
-
<nav className="hidden
|
|
76
|
+
<nav className="hidden lg:block fixed right-8 top-[calc(var(--header-height)+2rem)] w-[var(--toc-width)] max-h-[calc(100vh-var(--header-height)-4rem)] overflow-y-auto">
|
|
61
77
|
<p className="text-[0.6875rem] font-semibold uppercase tracking-widest text-[var(--color-text-tertiary)] mb-3">
|
|
62
78
|
On this page
|
|
63
79
|
</p>
|
|
64
80
|
<ul className="space-y-0.5 border-l border-[var(--color-border)]">
|
|
65
|
-
{headings.map((heading) => (
|
|
66
|
-
<li key={heading.id}>
|
|
81
|
+
{headings.map((heading, index) => (
|
|
82
|
+
<li key={`${heading.id}-${index}`}>
|
|
67
83
|
<a
|
|
68
84
|
href={`#${heading.id}`}
|
|
69
85
|
className={cn(
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { createHighlighter, type Highlighter, type BundledTheme } from 'shiki'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// Cache highlighter instances per loaded theme set
|
|
4
|
+
const highlighterCache = new Map<string, Highlighter>()
|
|
4
5
|
|
|
5
|
-
export type ThemeName =
|
|
6
|
+
export type ThemeName =
|
|
7
|
+
| 'catppuccin-mocha'
|
|
8
|
+
| 'catppuccin-latte'
|
|
9
|
+
| 'github-dark'
|
|
10
|
+
| 'github-light'
|
|
11
|
+
| 'dracula'
|
|
12
|
+
| 'nord'
|
|
13
|
+
| 'one-dark-pro'
|
|
14
|
+
| 'vitesse-dark'
|
|
15
|
+
| 'vitesse-light'
|
|
6
16
|
|
|
7
17
|
export const AVAILABLE_THEMES: { name: ThemeName; label: string; isDark: boolean }[] = [
|
|
18
|
+
{ name: 'catppuccin-mocha', label: 'Catppuccin', isDark: true },
|
|
19
|
+
{ name: 'catppuccin-latte', label: 'Catppuccin Light', isDark: false },
|
|
8
20
|
{ name: 'github-dark', label: 'GitHub Dark', isDark: true },
|
|
9
21
|
{ name: 'github-light', label: 'GitHub Light', isDark: false },
|
|
10
22
|
{ name: 'dracula', label: 'Dracula', isDark: true },
|
|
@@ -14,34 +26,79 @@ export const AVAILABLE_THEMES: { name: ThemeName; label: string; isDark: boolean
|
|
|
14
26
|
{ name: 'vitesse-light', label: 'Vitesse Light', isDark: false },
|
|
15
27
|
]
|
|
16
28
|
|
|
17
|
-
export const DEFAULT_THEME: ThemeName = '
|
|
29
|
+
export const DEFAULT_THEME: ThemeName = 'catppuccin-mocha'
|
|
18
30
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
31
|
+
const SUPPORTED_LANGS = [
|
|
32
|
+
'typescript',
|
|
33
|
+
'javascript',
|
|
34
|
+
'python',
|
|
35
|
+
'go',
|
|
36
|
+
'rust',
|
|
37
|
+
'json',
|
|
38
|
+
'yaml',
|
|
39
|
+
'bash',
|
|
40
|
+
'shell',
|
|
41
|
+
'markdown',
|
|
42
|
+
'html',
|
|
43
|
+
'css',
|
|
44
|
+
'jsx',
|
|
45
|
+
'tsx',
|
|
46
|
+
'sql',
|
|
47
|
+
'graphql',
|
|
48
|
+
'diff',
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
// Start with just the default theme; load others on demand
|
|
52
|
+
let defaultHighlighter: Highlighter | null = null
|
|
53
|
+
let defaultHighlighterPromise: Promise<Highlighter> | null = null
|
|
54
|
+
|
|
55
|
+
async function getDefaultHighlighter(): Promise<Highlighter> {
|
|
56
|
+
if (defaultHighlighter) return defaultHighlighter
|
|
57
|
+
if (defaultHighlighterPromise) return defaultHighlighterPromise
|
|
58
|
+
|
|
59
|
+
defaultHighlighterPromise = createHighlighter({
|
|
60
|
+
themes: [DEFAULT_THEME] as BundledTheme[],
|
|
61
|
+
langs: SUPPORTED_LANGS,
|
|
62
|
+
}).then(hl => {
|
|
63
|
+
defaultHighlighter = hl
|
|
64
|
+
highlighterCache.set(DEFAULT_THEME, hl)
|
|
65
|
+
return hl
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return defaultHighlighterPromise
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function getHighlighterForTheme(theme: ThemeName): Promise<Highlighter> {
|
|
72
|
+
if (theme === DEFAULT_THEME || highlighterCache.has(DEFAULT_THEME)) {
|
|
73
|
+
// Try to load the theme into the existing default highlighter
|
|
74
|
+
const hl = await getDefaultHighlighter()
|
|
75
|
+
try {
|
|
76
|
+
// Check if the theme is already loaded by attempting to use it
|
|
77
|
+
const loadedThemes = hl.getLoadedThemes()
|
|
78
|
+
if (!loadedThemes.includes(theme as BundledTheme)) {
|
|
79
|
+
await hl.loadTheme(theme as BundledTheme)
|
|
80
|
+
}
|
|
81
|
+
return hl
|
|
82
|
+
} catch {
|
|
83
|
+
// If loading into existing highlighter fails, create a new one
|
|
84
|
+
}
|
|
43
85
|
}
|
|
44
|
-
|
|
86
|
+
|
|
87
|
+
// Check cache
|
|
88
|
+
const cached = highlighterCache.get(theme)
|
|
89
|
+
if (cached) return cached
|
|
90
|
+
|
|
91
|
+
// Create a new highlighter with this theme
|
|
92
|
+
const hl = await createHighlighter({
|
|
93
|
+
themes: [theme] as BundledTheme[],
|
|
94
|
+
langs: SUPPORTED_LANGS,
|
|
95
|
+
})
|
|
96
|
+
highlighterCache.set(theme, hl)
|
|
97
|
+
return hl
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function getHighlighter(): Promise<Highlighter> {
|
|
101
|
+
return getDefaultHighlighter()
|
|
45
102
|
}
|
|
46
103
|
|
|
47
104
|
export async function highlight(
|
|
@@ -49,9 +106,8 @@ export async function highlight(
|
|
|
49
106
|
language: string,
|
|
50
107
|
theme: ThemeName = DEFAULT_THEME
|
|
51
108
|
): Promise<string> {
|
|
52
|
-
const hl = await
|
|
109
|
+
const hl = await getHighlighterForTheme(theme)
|
|
53
110
|
|
|
54
|
-
// Normalize language names
|
|
55
111
|
const lang = normalizeLanguage(language)
|
|
56
112
|
|
|
57
113
|
try {
|
|
@@ -60,7 +116,6 @@ export async function highlight(
|
|
|
60
116
|
theme: theme as BundledTheme,
|
|
61
117
|
})
|
|
62
118
|
} catch {
|
|
63
|
-
// Fallback to plain text if language not supported
|
|
64
119
|
return hl.codeToHtml(code, {
|
|
65
120
|
lang: 'text',
|
|
66
121
|
theme: theme as BundledTheme,
|
|
@@ -68,6 +123,10 @@ export async function highlight(
|
|
|
68
123
|
}
|
|
69
124
|
}
|
|
70
125
|
|
|
126
|
+
export function isLightTheme(theme: ThemeName): boolean {
|
|
127
|
+
return AVAILABLE_THEMES.find(t => t.name === theme)?.isDark === false
|
|
128
|
+
}
|
|
129
|
+
|
|
71
130
|
function normalizeLanguage(lang: string): string {
|
|
72
131
|
const aliases: Record<string, string> = {
|
|
73
132
|
js: 'javascript',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { create, load, search as oramaSearch } from '@orama/orama'
|
|
1
|
+
import { create, load, search as oramaSearch, type RawData } from '@orama/orama'
|
|
2
2
|
import type { SearchDatabase, SearchHit } from './search-types'
|
|
3
3
|
|
|
4
4
|
let db: SearchDatabase | null = null
|
|
@@ -27,12 +27,22 @@ async function loadSearchIndex(): Promise<void> {
|
|
|
27
27
|
try {
|
|
28
28
|
const response = await fetch('/search-index.json')
|
|
29
29
|
if (!response.ok) {
|
|
30
|
-
|
|
30
|
+
console.warn('Search index not available (HTTP ' + response.status + '). Search will be disabled.')
|
|
31
|
+
db = null
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let data: unknown
|
|
36
|
+
try {
|
|
37
|
+
data = await response.json()
|
|
38
|
+
} catch (parseErr) {
|
|
39
|
+
console.error('Search index contains invalid JSON. Search will be disabled.')
|
|
40
|
+
db = null
|
|
41
|
+
return
|
|
31
42
|
}
|
|
32
|
-
const data = await response.json()
|
|
33
43
|
|
|
34
44
|
const newDb = await create({ schema }) as SearchDatabase
|
|
35
|
-
await load(newDb, data)
|
|
45
|
+
await load(newDb, data as RawData)
|
|
36
46
|
db = newDb
|
|
37
47
|
} catch (err) {
|
|
38
48
|
const message = err instanceof Error ? err.message : String(err)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme color derivation utilities.
|
|
3
|
+
* Pure functions — no external dependencies.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Convert a hex color string to HSL components.
|
|
8
|
+
* Accepts 3-digit (#abc) or 6-digit (#aabbcc) hex, with or without #.
|
|
9
|
+
*/
|
|
10
|
+
export function hexToHsl(hex: string): { h: number; s: number; l: number } {
|
|
11
|
+
// Strip # prefix if present
|
|
12
|
+
hex = hex.replace(/^#/, '')
|
|
13
|
+
|
|
14
|
+
// Expand 3-digit shorthand to 6-digit
|
|
15
|
+
if (hex.length === 3) {
|
|
16
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const r = parseInt(hex.substring(0, 2), 16) / 255
|
|
20
|
+
const g = parseInt(hex.substring(2, 4), 16) / 255
|
|
21
|
+
const b = parseInt(hex.substring(4, 6), 16) / 255
|
|
22
|
+
|
|
23
|
+
const max = Math.max(r, g, b)
|
|
24
|
+
const min = Math.min(r, g, b)
|
|
25
|
+
const l = (max + min) / 2
|
|
26
|
+
|
|
27
|
+
if (max === min) {
|
|
28
|
+
// Achromatic
|
|
29
|
+
return { h: 0, s: 0, l }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const d = max - min
|
|
33
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
|
34
|
+
|
|
35
|
+
let h: number
|
|
36
|
+
switch (max) {
|
|
37
|
+
case r:
|
|
38
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) / 6
|
|
39
|
+
break
|
|
40
|
+
case g:
|
|
41
|
+
h = ((b - r) / d + 2) / 6
|
|
42
|
+
break
|
|
43
|
+
default:
|
|
44
|
+
h = ((r - g) / d + 4) / 6
|
|
45
|
+
break
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { h: h * 360, s, l }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Convert HSL components back to a hex color string.
|
|
53
|
+
* h: 0-360, s: 0-1, l: 0-1
|
|
54
|
+
*/
|
|
55
|
+
export function hslToHex(h: number, s: number, l: number): string {
|
|
56
|
+
// Clamp values
|
|
57
|
+
h = ((h % 360) + 360) % 360
|
|
58
|
+
s = Math.max(0, Math.min(1, s))
|
|
59
|
+
l = Math.max(0, Math.min(1, l))
|
|
60
|
+
|
|
61
|
+
const c = (1 - Math.abs(2 * l - 1)) * s
|
|
62
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
|
|
63
|
+
const m = l - c / 2
|
|
64
|
+
|
|
65
|
+
let r: number, g: number, b: number
|
|
66
|
+
|
|
67
|
+
if (h < 60) {
|
|
68
|
+
;[r, g, b] = [c, x, 0]
|
|
69
|
+
} else if (h < 120) {
|
|
70
|
+
;[r, g, b] = [x, c, 0]
|
|
71
|
+
} else if (h < 180) {
|
|
72
|
+
;[r, g, b] = [0, c, x]
|
|
73
|
+
} else if (h < 240) {
|
|
74
|
+
;[r, g, b] = [0, x, c]
|
|
75
|
+
} else if (h < 300) {
|
|
76
|
+
;[r, g, b] = [x, 0, c]
|
|
77
|
+
} else {
|
|
78
|
+
;[r, g, b] = [c, 0, x]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const toHex = (v: number) =>
|
|
82
|
+
Math.round((v + m) * 255)
|
|
83
|
+
.toString(16)
|
|
84
|
+
.padStart(2, '0')
|
|
85
|
+
|
|
86
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Derive primary, dark, and light variants from a single hex color.
|
|
91
|
+
* Dark variant: lightness reduced by ~15%
|
|
92
|
+
* Light variant: lightness increased by ~15%
|
|
93
|
+
*/
|
|
94
|
+
/**
|
|
95
|
+
* Compute WCAG relative luminance from a hex color.
|
|
96
|
+
* Returns 0 (black) to 1 (white).
|
|
97
|
+
*/
|
|
98
|
+
export function relativeLuminance(hex: string): number {
|
|
99
|
+
hex = hex.replace(/^#/, '')
|
|
100
|
+
if (hex.length === 3) {
|
|
101
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
|
|
102
|
+
}
|
|
103
|
+
const r = parseInt(hex.substring(0, 2), 16) / 255
|
|
104
|
+
const g = parseInt(hex.substring(2, 4), 16) / 255
|
|
105
|
+
const b = parseInt(hex.substring(4, 6), 16) / 255
|
|
106
|
+
|
|
107
|
+
const toLinear = (c: number) => (c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4))
|
|
108
|
+
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Return the best contrasting foreground color (white or dark) for a given background.
|
|
113
|
+
* Uses WCAG contrast ratio.
|
|
114
|
+
*/
|
|
115
|
+
export function contrastForeground(bgHex: string): string {
|
|
116
|
+
const lum = relativeLuminance(bgHex)
|
|
117
|
+
// WCAG contrast ratio: (L1 + 0.05) / (L2 + 0.05)
|
|
118
|
+
// White on bg: (1 + 0.05) / (lum + 0.05)
|
|
119
|
+
// Dark on bg: (lum + 0.05) / (0 + 0.05)
|
|
120
|
+
const contrastWithWhite = (1.05) / (lum + 0.05)
|
|
121
|
+
const contrastWithBlack = (lum + 0.05) / 0.05
|
|
122
|
+
return contrastWithWhite >= contrastWithBlack ? '#ffffff' : '#0f1117'
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function derivePrimaryColors(hex: string): {
|
|
126
|
+
primary: string
|
|
127
|
+
primaryDark: string
|
|
128
|
+
primaryLight: string
|
|
129
|
+
primaryForeground: string
|
|
130
|
+
} {
|
|
131
|
+
const { h, s, l } = hexToHsl(hex)
|
|
132
|
+
const normalizedHex = hex.startsWith('#') ? hex : `#${hex}`
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
primary: normalizedHex,
|
|
136
|
+
primaryDark: hslToHex(h, s, Math.max(0, l - 0.15)),
|
|
137
|
+
primaryLight: hslToHex(h, s, Math.min(1, l + 0.15)),
|
|
138
|
+
primaryForeground: contrastForeground(normalizedHex),
|
|
139
|
+
}
|
|
140
|
+
}
|