skrypt-ai 0.3.4 → 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.
- 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
|
@@ -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>
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { createContext, useContext, useState, ReactNode } from 'react'
|
|
3
|
+
import { createContext, useContext, useState, useRef, useCallback, useId, ReactNode } from 'react'
|
|
4
4
|
import { cn } from '@/lib/utils'
|
|
5
5
|
|
|
6
6
|
interface TabsContextValue {
|
|
7
7
|
activeTab: string
|
|
8
8
|
setActiveTab: (tab: string) => void
|
|
9
|
+
instanceId: string
|
|
10
|
+
tabRefs: React.MutableRefObject<Map<string, HTMLButtonElement>>
|
|
11
|
+
tabValues: string[]
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
const TabsContext = createContext<TabsContextValue | null>(null)
|
|
@@ -17,9 +20,13 @@ interface TabsProps {
|
|
|
17
20
|
|
|
18
21
|
export function Tabs({ defaultValue, children }: TabsProps) {
|
|
19
22
|
const [activeTab, setActiveTab] = useState(defaultValue)
|
|
23
|
+
const instanceId = useId()
|
|
24
|
+
const tabRefs = useRef<Map<string, HTMLButtonElement>>(new Map())
|
|
25
|
+
// Collect tab values from children to enable arrow key navigation
|
|
26
|
+
const tabValues = useRef<string[]>([])
|
|
20
27
|
|
|
21
28
|
return (
|
|
22
|
-
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
|
|
29
|
+
<TabsContext.Provider value={{ activeTab, setActiveTab, instanceId, tabRefs, tabValues: tabValues.current }}>
|
|
23
30
|
<div className="my-6">{children}</div>
|
|
24
31
|
</TabsContext.Provider>
|
|
25
32
|
)
|
|
@@ -30,8 +37,39 @@ interface TabListProps {
|
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
export function TabList({ children }: TabListProps) {
|
|
40
|
+
const context = useContext(TabsContext)
|
|
41
|
+
if (!context) throw new Error('TabList must be used within Tabs')
|
|
42
|
+
|
|
43
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
44
|
+
const { tabValues, tabRefs, activeTab, setActiveTab } = context
|
|
45
|
+
if (!tabValues.length) return
|
|
46
|
+
|
|
47
|
+
const currentIndex = tabValues.indexOf(activeTab)
|
|
48
|
+
let nextIndex = -1
|
|
49
|
+
|
|
50
|
+
if (e.key === 'ArrowRight') {
|
|
51
|
+
e.preventDefault()
|
|
52
|
+
nextIndex = (currentIndex + 1) % tabValues.length
|
|
53
|
+
} else if (e.key === 'ArrowLeft') {
|
|
54
|
+
e.preventDefault()
|
|
55
|
+
nextIndex = (currentIndex - 1 + tabValues.length) % tabValues.length
|
|
56
|
+
} else if (e.key === 'Home') {
|
|
57
|
+
e.preventDefault()
|
|
58
|
+
nextIndex = 0
|
|
59
|
+
} else if (e.key === 'End') {
|
|
60
|
+
e.preventDefault()
|
|
61
|
+
nextIndex = tabValues.length - 1
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (nextIndex >= 0) {
|
|
65
|
+
const nextValue = tabValues[nextIndex]
|
|
66
|
+
setActiveTab(nextValue)
|
|
67
|
+
tabRefs.current.get(nextValue)?.focus()
|
|
68
|
+
}
|
|
69
|
+
}, [context])
|
|
70
|
+
|
|
33
71
|
return (
|
|
34
|
-
<div className="flex gap-0.5 border-b border-[var(--color-border)]">
|
|
72
|
+
<div role="tablist" className="flex gap-0.5 border-b border-[var(--color-border)]" onKeyDown={handleKeyDown}>
|
|
35
73
|
{children}
|
|
36
74
|
</div>
|
|
37
75
|
)
|
|
@@ -46,11 +84,30 @@ export function Tab({ value, children }: TabProps) {
|
|
|
46
84
|
const context = useContext(TabsContext)
|
|
47
85
|
if (!context) throw new Error('Tab must be used within Tabs')
|
|
48
86
|
|
|
49
|
-
const { activeTab, setActiveTab } = context
|
|
87
|
+
const { activeTab, setActiveTab, instanceId, tabRefs, tabValues } = context
|
|
50
88
|
const isActive = activeTab === value
|
|
51
89
|
|
|
90
|
+
// Register this tab value for arrow key navigation
|
|
91
|
+
const refCallback = useCallback((el: HTMLButtonElement | null) => {
|
|
92
|
+
if (el) {
|
|
93
|
+
tabRefs.current.set(value, el)
|
|
94
|
+
if (!tabValues.includes(value)) {
|
|
95
|
+
tabValues.push(value)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}, [value, tabRefs, tabValues])
|
|
99
|
+
|
|
100
|
+
const tabId = `tab-${instanceId}-${value}`
|
|
101
|
+
const panelId = `panel-${instanceId}-${value}`
|
|
102
|
+
|
|
52
103
|
return (
|
|
53
104
|
<button
|
|
105
|
+
ref={refCallback}
|
|
106
|
+
id={tabId}
|
|
107
|
+
role="tab"
|
|
108
|
+
aria-selected={isActive}
|
|
109
|
+
aria-controls={panelId}
|
|
110
|
+
tabIndex={isActive ? 0 : -1}
|
|
54
111
|
onClick={() => setActiveTab(value)}
|
|
55
112
|
className={cn(
|
|
56
113
|
'px-3 py-2 text-[0.8125rem] font-medium transition-colors border-b-2 -mb-px',
|
|
@@ -73,9 +130,20 @@ export function TabPanel({ value, children }: TabPanelProps) {
|
|
|
73
130
|
const context = useContext(TabsContext)
|
|
74
131
|
if (!context) throw new Error('TabPanel must be used within Tabs')
|
|
75
132
|
|
|
76
|
-
const { activeTab } = context
|
|
133
|
+
const { activeTab, instanceId } = context
|
|
134
|
+
const tabId = `tab-${instanceId}-${value}`
|
|
135
|
+
const panelId = `panel-${instanceId}-${value}`
|
|
77
136
|
|
|
78
137
|
if (activeTab !== value) return null
|
|
79
138
|
|
|
80
|
-
return
|
|
139
|
+
return (
|
|
140
|
+
<div
|
|
141
|
+
id={panelId}
|
|
142
|
+
role="tabpanel"
|
|
143
|
+
aria-labelledby={tabId}
|
|
144
|
+
className="pt-4"
|
|
145
|
+
>
|
|
146
|
+
{children}
|
|
147
|
+
</div>
|
|
148
|
+
)
|
|
81
149
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface PageHeaderProps {
|
|
2
|
+
title: string
|
|
3
|
+
description?: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function PageHeader({ title, description }: PageHeaderProps) {
|
|
7
|
+
return (
|
|
8
|
+
<div className="mb-8 pb-6 border-b border-[var(--color-border)]">
|
|
9
|
+
<h1 className="text-3xl sm:text-4xl font-bold tracking-tight text-[var(--color-text)]">
|
|
10
|
+
{title}
|
|
11
|
+
</h1>
|
|
12
|
+
{description && (
|
|
13
|
+
<p className="text-lg text-[var(--color-text-secondary)] mt-3 leading-relaxed max-w-2xl">
|
|
14
|
+
{description}
|
|
15
|
+
</p>
|
|
16
|
+
)}
|
|
17
|
+
</div>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react'
|
|
4
|
+
import { ChevronUp } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
export function ScrollToTop() {
|
|
7
|
+
const [visible, setVisible] = useState(false)
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
function handleScroll() {
|
|
11
|
+
setVisible(window.scrollY > 300)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
window.addEventListener('scroll', handleScroll, { passive: true })
|
|
15
|
+
return () => window.removeEventListener('scroll', handleScroll)
|
|
16
|
+
}, [])
|
|
17
|
+
|
|
18
|
+
function scrollToTop() {
|
|
19
|
+
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!visible) return null
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<button
|
|
26
|
+
onClick={scrollToTop}
|
|
27
|
+
className="fixed bottom-6 right-24 z-40 flex h-10 w-10 items-center justify-center rounded-full bg-[var(--color-primary)] text-[var(--color-primary-foreground)] shadow-lg transition-all hover:bg-[var(--color-primary-dark)] hover:scale-105"
|
|
28
|
+
aria-label="Scroll to top"
|
|
29
|
+
>
|
|
30
|
+
<ChevronUp size={20} />
|
|
31
|
+
</button>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -1,11 +1,61 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import React, { useEffect, useState, useCallback, useRef } from 'react'
|
|
4
|
-
import { Search, FileText, ArrowRight } from 'lucide-react'
|
|
3
|
+
import React, { useEffect, useState, useCallback, useRef, useMemo } from 'react'
|
|
4
|
+
import { Search, FileText, ArrowRight, Clock, X } from 'lucide-react'
|
|
5
5
|
import Link from 'next/link'
|
|
6
|
+
import { useRouter } from 'next/navigation'
|
|
6
7
|
import { cn } from '@/lib/utils'
|
|
7
8
|
import { search as performSearch, SearchResultWithHighlight } from '@/lib/search'
|
|
8
9
|
|
|
10
|
+
const RECENT_SEARCHES_KEY = 'recent-searches'
|
|
11
|
+
const MAX_RECENT_SEARCHES = 5
|
|
12
|
+
|
|
13
|
+
function getRecentSearches(): string[] {
|
|
14
|
+
try {
|
|
15
|
+
const stored = localStorage.getItem(RECENT_SEARCHES_KEY)
|
|
16
|
+
return stored ? JSON.parse(stored) : []
|
|
17
|
+
} catch {
|
|
18
|
+
return []
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function saveRecentSearch(query: string) {
|
|
23
|
+
try {
|
|
24
|
+
const trimmed = query.trim()
|
|
25
|
+
if (!trimmed) return
|
|
26
|
+
const existing = getRecentSearches()
|
|
27
|
+
const filtered = existing.filter((s) => s !== trimmed)
|
|
28
|
+
const updated = [trimmed, ...filtered].slice(0, MAX_RECENT_SEARCHES)
|
|
29
|
+
localStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(updated))
|
|
30
|
+
} catch {
|
|
31
|
+
// localStorage unavailable
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function clearRecentSearches() {
|
|
36
|
+
try {
|
|
37
|
+
localStorage.removeItem(RECENT_SEARCHES_KEY)
|
|
38
|
+
} catch {
|
|
39
|
+
// localStorage unavailable
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function SearchSkeletons() {
|
|
44
|
+
return (
|
|
45
|
+
<div className="p-4 space-y-4">
|
|
46
|
+
{[0, 1, 2, 3].map((i) => (
|
|
47
|
+
<div key={i} className="flex items-center gap-3">
|
|
48
|
+
<div className="skeleton w-8 h-8 rounded-full shrink-0" />
|
|
49
|
+
<div className="flex-1 space-y-2">
|
|
50
|
+
<div className="skeleton h-3.5 w-3/4" />
|
|
51
|
+
<div className="skeleton h-3 w-1/2" />
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
9
59
|
interface SearchDialogProps {
|
|
10
60
|
open: boolean
|
|
11
61
|
onClose: () => void
|
|
@@ -30,13 +80,39 @@ function highlightTerms(text: string, query: string): React.ReactNode {
|
|
|
30
80
|
)
|
|
31
81
|
}
|
|
32
82
|
|
|
83
|
+
interface GroupedResults {
|
|
84
|
+
section: string
|
|
85
|
+
results: SearchResultWithHighlight[]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function groupResultsBySection(results: SearchResultWithHighlight[]): GroupedResults[] {
|
|
89
|
+
const map = new Map<string, SearchResultWithHighlight[]>()
|
|
90
|
+
const order: string[] = []
|
|
91
|
+
|
|
92
|
+
for (const result of results) {
|
|
93
|
+
const section = result.section || 'Results'
|
|
94
|
+
if (!map.has(section)) {
|
|
95
|
+
map.set(section, [])
|
|
96
|
+
order.push(section)
|
|
97
|
+
}
|
|
98
|
+
map.get(section)!.push(result)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return order.map(section => ({ section, results: map.get(section)! }))
|
|
102
|
+
}
|
|
103
|
+
|
|
33
104
|
export function SearchDialog({ open, onClose }: SearchDialogProps) {
|
|
105
|
+
const router = useRouter()
|
|
34
106
|
const [query, setQuery] = useState('')
|
|
35
107
|
const [results, setResults] = useState<SearchResultWithHighlight[]>([])
|
|
36
108
|
const [isLoading, setIsLoading] = useState(false)
|
|
37
109
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
110
|
+
const [recentSearches, setRecentSearches] = useState<string[]>([])
|
|
38
111
|
const dialogRef = useRef<HTMLDivElement>(null)
|
|
39
112
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
113
|
+
const selectedRef = useRef<HTMLLIElement>(null)
|
|
114
|
+
|
|
115
|
+
const grouped = useMemo(() => groupResultsBySection(results), [results])
|
|
40
116
|
|
|
41
117
|
useEffect(() => {
|
|
42
118
|
if (open) {
|
|
@@ -45,6 +121,7 @@ export function SearchDialog({ open, onClose }: SearchDialogProps) {
|
|
|
45
121
|
setQuery('')
|
|
46
122
|
setResults([])
|
|
47
123
|
setSelectedIndex(0)
|
|
124
|
+
setRecentSearches(getRecentSearches())
|
|
48
125
|
|
|
49
126
|
return () => {
|
|
50
127
|
previouslyFocused?.focus()
|
|
@@ -78,19 +155,35 @@ export function SearchDialog({ open, onClose }: SearchDialogProps) {
|
|
|
78
155
|
|
|
79
156
|
useEffect(() => {
|
|
80
157
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
81
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
82
|
-
e.preventDefault()
|
|
83
|
-
if (open) onClose()
|
|
84
|
-
}
|
|
85
158
|
if (e.key === 'Escape' && open) {
|
|
86
159
|
onClose()
|
|
87
160
|
}
|
|
161
|
+
// Focus trapping within the dialog
|
|
162
|
+
if (e.key === 'Tab' && open && dialogRef.current) {
|
|
163
|
+
const focusable = dialogRef.current.querySelectorAll<HTMLElement>(
|
|
164
|
+
'input, button, a[href], [tabindex]:not([tabindex="-1"])'
|
|
165
|
+
)
|
|
166
|
+
if (focusable.length === 0) return
|
|
167
|
+
const first = focusable[0]
|
|
168
|
+
const last = focusable[focusable.length - 1]
|
|
169
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
170
|
+
e.preventDefault()
|
|
171
|
+
last.focus()
|
|
172
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
173
|
+
e.preventDefault()
|
|
174
|
+
first.focus()
|
|
175
|
+
}
|
|
176
|
+
}
|
|
88
177
|
}
|
|
89
178
|
|
|
90
179
|
document.addEventListener('keydown', handleKeyDown)
|
|
91
180
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
92
181
|
}, [open, onClose])
|
|
93
182
|
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
selectedRef.current?.scrollIntoView({ block: 'nearest' })
|
|
185
|
+
}, [selectedIndex])
|
|
186
|
+
|
|
94
187
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
95
188
|
if (e.key === 'ArrowDown') {
|
|
96
189
|
e.preventDefault()
|
|
@@ -99,18 +192,31 @@ export function SearchDialog({ open, onClose }: SearchDialogProps) {
|
|
|
99
192
|
e.preventDefault()
|
|
100
193
|
setSelectedIndex((prev) => Math.max(prev - 1, 0))
|
|
101
194
|
} else if (e.key === 'Enter' && results[selectedIndex]) {
|
|
195
|
+
saveRecentSearch(query)
|
|
102
196
|
onClose()
|
|
103
|
-
|
|
197
|
+
router.push(results[selectedIndex].href)
|
|
104
198
|
}
|
|
105
199
|
}
|
|
106
200
|
|
|
201
|
+
// Build a flat index map for grouped results
|
|
202
|
+
const flatIndexMap = useMemo(() => {
|
|
203
|
+
const map = new Map<string, number>()
|
|
204
|
+
let idx = 0
|
|
205
|
+
for (const group of grouped) {
|
|
206
|
+
for (const result of group.results) {
|
|
207
|
+
map.set(result.href, idx++)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return map
|
|
211
|
+
}, [grouped])
|
|
212
|
+
|
|
107
213
|
if (!open) return null
|
|
108
214
|
|
|
109
215
|
return (
|
|
110
216
|
<div className="fixed inset-0 z-50" role="presentation">
|
|
111
217
|
{/* Backdrop */}
|
|
112
218
|
<div
|
|
113
|
-
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
|
219
|
+
className="absolute inset-0 bg-black/60 backdrop-blur-sm transition-opacity duration-150"
|
|
114
220
|
onClick={onClose}
|
|
115
221
|
aria-hidden="true"
|
|
116
222
|
/>
|
|
@@ -121,7 +227,7 @@ export function SearchDialog({ open, onClose }: SearchDialogProps) {
|
|
|
121
227
|
role="dialog"
|
|
122
228
|
aria-modal="true"
|
|
123
229
|
aria-label="Search documentation"
|
|
124
|
-
className="absolute top-[15%] left-1/2 -translate-x-1/2 w-full max-w-lg px-4"
|
|
230
|
+
className="absolute top-[15%] left-1/2 -translate-x-1/2 w-full max-w-lg px-4 dialog-animate-in"
|
|
125
231
|
>
|
|
126
232
|
<div className="bg-[var(--color-bg)] border border-[var(--color-border)] rounded-xl shadow-2xl overflow-hidden">
|
|
127
233
|
{/* Search input */}
|
|
@@ -145,55 +251,103 @@ export function SearchDialog({ open, onClose }: SearchDialogProps) {
|
|
|
145
251
|
{/* Results */}
|
|
146
252
|
<div className="max-h-[60vh] overflow-y-auto">
|
|
147
253
|
{results.length > 0 ? (
|
|
148
|
-
<
|
|
149
|
-
{
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
254
|
+
<div className="p-1.5">
|
|
255
|
+
{grouped.map((group) => (
|
|
256
|
+
<div key={group.section}>
|
|
257
|
+
{/* Section header */}
|
|
258
|
+
<div className="px-3 pt-3 pb-1.5">
|
|
259
|
+
<span className="text-[0.6875rem] font-bold uppercase tracking-wider text-[var(--color-text-tertiary)]">
|
|
260
|
+
{group.section}
|
|
261
|
+
</span>
|
|
262
|
+
</div>
|
|
263
|
+
<ul>
|
|
264
|
+
{group.results.map((result) => {
|
|
265
|
+
const index = flatIndexMap.get(result.href) ?? 0
|
|
266
|
+
return (
|
|
267
|
+
<li key={result.href} ref={index === selectedIndex ? selectedRef : undefined}>
|
|
268
|
+
<Link
|
|
269
|
+
href={result.href}
|
|
270
|
+
onClick={() => { saveRecentSearch(query); onClose() }}
|
|
271
|
+
className={cn(
|
|
272
|
+
'flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors hover:no-underline group',
|
|
273
|
+
index === selectedIndex
|
|
274
|
+
? 'bg-[var(--color-primary)] text-[var(--color-primary-foreground)]'
|
|
275
|
+
: 'hover:bg-[var(--color-bg-secondary)]'
|
|
276
|
+
)}
|
|
277
|
+
>
|
|
278
|
+
<FileText size={16} className={cn(
|
|
279
|
+
'shrink-0',
|
|
280
|
+
index === selectedIndex ? 'text-white/70' : 'text-[var(--color-text-tertiary)]'
|
|
281
|
+
)} />
|
|
282
|
+
<div className="flex-1 min-w-0">
|
|
283
|
+
<div className={cn(
|
|
284
|
+
'text-[0.8125rem] font-medium truncate',
|
|
285
|
+
index === selectedIndex ? 'text-white' : 'text-[var(--color-text)]'
|
|
286
|
+
)}>
|
|
287
|
+
{result.title}
|
|
288
|
+
</div>
|
|
289
|
+
{result.snippet && (
|
|
290
|
+
<div className={cn(
|
|
291
|
+
'text-[0.75rem] truncate mt-0.5',
|
|
292
|
+
index === selectedIndex ? 'text-white/70' : 'text-[var(--color-text-tertiary)]'
|
|
293
|
+
)}>
|
|
294
|
+
{index === selectedIndex ? result.snippet : highlightTerms(result.snippet, query)}
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
297
|
+
</div>
|
|
298
|
+
<ArrowRight size={14} className={cn(
|
|
299
|
+
'shrink-0 opacity-0 group-hover:opacity-100 transition-opacity',
|
|
300
|
+
index === selectedIndex ? 'text-white/70 opacity-100' : 'text-[var(--color-text-tertiary)]'
|
|
301
|
+
)} />
|
|
302
|
+
</Link>
|
|
303
|
+
</li>
|
|
304
|
+
)
|
|
305
|
+
})}
|
|
306
|
+
</ul>
|
|
307
|
+
</div>
|
|
187
308
|
))}
|
|
188
|
-
</
|
|
309
|
+
</div>
|
|
189
310
|
) : query && !isLoading ? (
|
|
190
311
|
<div className="p-8 text-center text-[0.8125rem] text-[var(--color-text-tertiary)]">
|
|
191
312
|
No results for “{query}”
|
|
192
313
|
</div>
|
|
314
|
+
) : isLoading ? (
|
|
315
|
+
<SearchSkeletons />
|
|
193
316
|
) : !query ? (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
317
|
+
recentSearches.length > 0 ? (
|
|
318
|
+
<div className="p-1.5">
|
|
319
|
+
<div className="px-3 pt-3 pb-1.5 flex items-center justify-between">
|
|
320
|
+
<span className="text-[0.6875rem] font-bold uppercase tracking-wider text-[var(--color-text-tertiary)]">
|
|
321
|
+
Recent
|
|
322
|
+
</span>
|
|
323
|
+
<button
|
|
324
|
+
onClick={() => { clearRecentSearches(); setRecentSearches([]) }}
|
|
325
|
+
className="text-[0.6875rem] text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] transition-colors"
|
|
326
|
+
>
|
|
327
|
+
Clear
|
|
328
|
+
</button>
|
|
329
|
+
</div>
|
|
330
|
+
<ul>
|
|
331
|
+
{recentSearches.map((recentQuery) => (
|
|
332
|
+
<li key={recentQuery}>
|
|
333
|
+
<button
|
|
334
|
+
onClick={() => setQuery(recentQuery)}
|
|
335
|
+
className="flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors hover:bg-[var(--color-bg-secondary)] w-full text-left"
|
|
336
|
+
>
|
|
337
|
+
<Clock size={14} className="text-[var(--color-text-tertiary)] shrink-0" />
|
|
338
|
+
<span className="text-[0.8125rem] text-[var(--color-text-secondary)] truncate">
|
|
339
|
+
{recentQuery}
|
|
340
|
+
</span>
|
|
341
|
+
</button>
|
|
342
|
+
</li>
|
|
343
|
+
))}
|
|
344
|
+
</ul>
|
|
345
|
+
</div>
|
|
346
|
+
) : (
|
|
347
|
+
<div className="p-6 text-center text-[0.8125rem] text-[var(--color-text-tertiary)]">
|
|
348
|
+
Type to search the docs
|
|
349
|
+
</div>
|
|
350
|
+
)
|
|
197
351
|
) : null}
|
|
198
352
|
</div>
|
|
199
353
|
|