@tscircuit/fake-snippets 0.0.87 → 0.0.89
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/api/generated-index.js +96 -14
- package/bun-tests/fake-snippets-api/routes/proxy.test.ts +42 -0
- package/bun.lock +187 -206
- package/dist/bundle.js +207 -101
- package/fake-snippets-api/routes/api/package_releases/create.ts +1 -1
- package/fake-snippets-api/routes/api/proxy.ts +128 -0
- package/package.json +57 -50
- package/renovate.json +2 -1
- package/src/App.tsx +22 -3
- package/src/ContextProviders.tsx +2 -0
- package/src/build-watcher.ts +52 -0
- package/src/components/CmdKMenu.tsx +533 -197
- package/src/components/DownloadButtonAndMenu.tsx +104 -26
- package/src/components/FileSidebar.tsx +11 -1
- package/src/components/Header.tsx +5 -1
- package/src/components/Header2.tsx +7 -2
- package/src/components/HeaderLogin.tsx +1 -1
- package/src/components/PackageBuildsPage/LogContent.tsx +25 -22
- package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +6 -6
- package/src/components/PackageBuildsPage/build-preview-content.tsx +5 -5
- package/src/components/PackageBuildsPage/package-build-details-panel.tsx +15 -13
- package/src/components/PackageBuildsPage/package-build-header.tsx +17 -16
- package/src/components/PackageCard.tsx +66 -16
- package/src/components/PrefetchPageLink.tsx +66 -15
- package/src/components/SearchComponent.tsx +2 -2
- package/src/components/SuspenseRunFrame.tsx +14 -2
- package/src/components/ViewPackagePage/components/important-files-view.tsx +97 -22
- package/src/components/ViewPackagePage/components/main-content-header.tsx +27 -3
- package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +2 -2
- package/src/components/ViewPackagePage/components/repo-page-content.tsx +49 -34
- package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +2 -2
- package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +20 -12
- package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +0 -7
- package/src/components/ViewPackagePage/utils/fuzz-search.ts +121 -0
- package/src/components/ViewPackagePage/utils/is-hidden-file.ts +4 -0
- package/src/components/ViewPackagePage/utils/is-package-file-important.ts +18 -5
- package/src/components/dialogs/confirm-delete-package-dialog.tsx +1 -1
- package/src/components/dialogs/confirm-discard-changes-dialog.tsx +73 -0
- package/src/components/dialogs/edit-package-details-dialog.tsx +2 -2
- package/src/components/dialogs/view-ts-files-dialog.tsx +478 -42
- package/src/components/package-port/CodeAndPreview.tsx +16 -0
- package/src/components/package-port/CodeEditor.tsx +113 -11
- package/src/components/package-port/CodeEditorHeader.tsx +39 -4
- package/src/components/package-port/EditorNav.tsx +41 -15
- package/src/components/package-port/GlobalFindReplace.tsx +681 -0
- package/src/components/package-port/QuickOpen.tsx +241 -0
- package/src/components/ui/dialog.tsx +1 -1
- package/src/components/ui/tree-view.tsx +1 -1
- package/src/global.d.ts +3 -0
- package/src/hooks/use-ai-review.ts +31 -0
- package/src/hooks/use-current-package-release.ts +5 -1
- package/src/hooks/use-download-zip.ts +50 -0
- package/src/hooks/use-hotkey.ts +116 -0
- package/src/hooks/use-package-by-package-id.ts +1 -0
- package/src/hooks/use-package-by-package-name.ts +1 -0
- package/src/hooks/use-package-files.ts +3 -0
- package/src/hooks/use-package-release.ts +5 -1
- package/src/hooks/use-package.ts +1 -0
- package/src/hooks/use-request-ai-review-mutation.ts +14 -6
- package/src/hooks/use-snippet.ts +1 -0
- package/src/hooks/useFileManagement.ts +26 -8
- package/src/hooks/usePackageFilesLoader.ts +3 -1
- package/src/index.css +11 -0
- package/src/lib/decodeUrlHashToFsMap.ts +17 -0
- package/src/lib/download-fns/download-circuit-png.ts +88 -0
- package/src/lib/download-fns/download-png-utils.ts +31 -0
- package/src/lib/encodeFsMapToUrlHash.ts +13 -0
- package/src/lib/populate-query-cache-with-ssr-data.ts +39 -38
- package/src/lib/ts-lib-cache.ts +47 -0
- package/src/lib/types.ts +2 -0
- package/src/main.tsx +7 -0
- package/src/pages/dashboard.tsx +8 -5
- package/src/pages/user-profile.tsx +1 -1
- package/src/pages/view-package.tsx +15 -7
- package/vite.config.ts +100 -1
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import { JLCPCBImportDialog } from "@/components/JLCPCBImportDialog"
|
|
2
2
|
import { useAxios } from "@/hooks/use-axios"
|
|
3
3
|
import { useGlobalStore } from "@/hooks/use-global-store"
|
|
4
|
+
import { useHotkeyCombo } from "@/hooks/use-hotkey"
|
|
4
5
|
import { useNotImplementedToast } from "@/hooks/use-toast"
|
|
6
|
+
import { fuzzyMatch } from "@/components/ViewPackagePage/utils/fuzz-search"
|
|
5
7
|
import { Command } from "cmdk"
|
|
6
|
-
import { Package
|
|
7
|
-
import React from "react"
|
|
8
|
+
import { Package } from "fake-snippets-api/lib/db/schema"
|
|
9
|
+
import React, { useCallback, useEffect, useMemo, useRef } from "react"
|
|
8
10
|
import { useQuery } from "react-query"
|
|
11
|
+
import {
|
|
12
|
+
Search,
|
|
13
|
+
Package2,
|
|
14
|
+
CircuitBoard,
|
|
15
|
+
Download,
|
|
16
|
+
Sparkles,
|
|
17
|
+
Clock,
|
|
18
|
+
ArrowRight,
|
|
19
|
+
} from "lucide-react"
|
|
20
|
+
import { DialogTitle, DialogDescription } from "@/components/ui/dialog"
|
|
9
21
|
|
|
10
22
|
type SnippetType = "board" | "package" | "model" | "footprint"
|
|
11
23
|
|
|
@@ -13,24 +25,91 @@ interface Template {
|
|
|
13
25
|
name: string
|
|
14
26
|
type: SnippetType
|
|
15
27
|
disabled?: boolean
|
|
28
|
+
icon?: React.ReactNode
|
|
16
29
|
}
|
|
17
30
|
|
|
18
31
|
interface ImportOption {
|
|
19
32
|
name: string
|
|
20
33
|
type: SnippetType
|
|
21
34
|
special?: boolean
|
|
35
|
+
icon?: React.ReactNode
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ScoredPackage extends Package {
|
|
39
|
+
score: number
|
|
40
|
+
matches: number[]
|
|
22
41
|
}
|
|
23
42
|
|
|
24
43
|
const CmdKMenu = () => {
|
|
25
44
|
const [open, setOpen] = React.useState(false)
|
|
26
45
|
const [searchQuery, setSearchQuery] = React.useState("")
|
|
27
46
|
const [isJLCPCBDialogOpen, setIsJLCPCBDialogOpen] = React.useState(false)
|
|
47
|
+
const [selectedIndex, setSelectedIndex] = React.useState(0)
|
|
28
48
|
const toastNotImplemented = useNotImplementedToast()
|
|
29
49
|
const axios = useAxios()
|
|
30
50
|
const currentUser = useGlobalStore((s) => s.session?.github_username)
|
|
51
|
+
const selectedItemRef = useRef<HTMLDivElement>(null)
|
|
52
|
+
|
|
53
|
+
const blankTemplates = useMemo(
|
|
54
|
+
(): Template[] => [
|
|
55
|
+
{
|
|
56
|
+
name: "Blank Circuit Board",
|
|
57
|
+
type: "board",
|
|
58
|
+
icon: <CircuitBoard className="w-4 h-4 text-green-500" />,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "Blank Circuit Module",
|
|
62
|
+
type: "package",
|
|
63
|
+
icon: <Package2 className="w-4 h-4 text-blue-500" />,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
[],
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const templates = useMemo(
|
|
70
|
+
(): Template[] => [
|
|
71
|
+
{
|
|
72
|
+
name: "Blinking LED Board",
|
|
73
|
+
type: "board",
|
|
74
|
+
icon: <Sparkles className="w-4 h-4 text-yellow-500" />,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "USB-C LED Flashlight",
|
|
78
|
+
type: "board",
|
|
79
|
+
icon: <Sparkles className="w-4 h-4 text-yellow-500" />,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
[],
|
|
83
|
+
)
|
|
31
84
|
|
|
32
|
-
|
|
33
|
-
|
|
85
|
+
const importOptions = useMemo(
|
|
86
|
+
(): ImportOption[] => [
|
|
87
|
+
{
|
|
88
|
+
name: "KiCad Footprint",
|
|
89
|
+
type: "footprint",
|
|
90
|
+
icon: <Download className="w-4 h-4 text-gray-500" />,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "KiCad Project",
|
|
94
|
+
type: "board",
|
|
95
|
+
icon: <Download className="w-4 h-4 text-gray-500" />,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "KiCad Module",
|
|
99
|
+
type: "package",
|
|
100
|
+
icon: <Download className="w-4 h-4 text-gray-500" />,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "JLCPCB Component",
|
|
104
|
+
type: "package",
|
|
105
|
+
special: true,
|
|
106
|
+
icon: <Download className="w-4 h-4 text-red-500" />,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
[],
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
const { data: allPackages = [], isLoading: isSearching } = useQuery(
|
|
34
113
|
["packageSearch", searchQuery],
|
|
35
114
|
async () => {
|
|
36
115
|
if (!searchQuery) return []
|
|
@@ -44,7 +123,19 @@ const CmdKMenu = () => {
|
|
|
44
123
|
},
|
|
45
124
|
)
|
|
46
125
|
|
|
47
|
-
|
|
126
|
+
const searchResults = useMemo((): ScoredPackage[] => {
|
|
127
|
+
if (!searchQuery || !allPackages.length) return []
|
|
128
|
+
|
|
129
|
+
return allPackages
|
|
130
|
+
.map((pkg: Package) => {
|
|
131
|
+
const { score, matches } = fuzzyMatch(searchQuery, pkg.name)
|
|
132
|
+
return { ...pkg, score, matches }
|
|
133
|
+
})
|
|
134
|
+
.filter((pkg: ScoredPackage) => pkg.score >= 0)
|
|
135
|
+
.sort((a: ScoredPackage, b: ScoredPackage) => b.score - a.score)
|
|
136
|
+
.slice(0, 8)
|
|
137
|
+
}, [allPackages, searchQuery])
|
|
138
|
+
|
|
48
139
|
const { data: recentPackages = [] } = useQuery<Package[]>(
|
|
49
140
|
["userPackages", currentUser],
|
|
50
141
|
async () => {
|
|
@@ -59,232 +150,477 @@ const CmdKMenu = () => {
|
|
|
59
150
|
},
|
|
60
151
|
)
|
|
61
152
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
153
|
+
const filteredStaticOptions = useMemo(() => {
|
|
154
|
+
if (!searchQuery) {
|
|
155
|
+
return {
|
|
156
|
+
blankTemplates: blankTemplates,
|
|
157
|
+
templates: templates,
|
|
158
|
+
importOptions: importOptions,
|
|
67
159
|
}
|
|
68
160
|
}
|
|
69
161
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
162
|
+
const searchBlankTemplates = blankTemplates
|
|
163
|
+
.map((template) => {
|
|
164
|
+
const { score, matches } = fuzzyMatch(searchQuery, template.name)
|
|
165
|
+
return { ...template, score, matches }
|
|
166
|
+
})
|
|
167
|
+
.filter((template) => template.score >= 0)
|
|
168
|
+
.sort((a, b) => b.score - a.score)
|
|
169
|
+
|
|
170
|
+
const searchTemplates = templates
|
|
171
|
+
.map((template) => {
|
|
172
|
+
const { score, matches } = fuzzyMatch(searchQuery, template.name)
|
|
173
|
+
return { ...template, score, matches }
|
|
174
|
+
})
|
|
175
|
+
.filter((template) => template.score >= 0)
|
|
176
|
+
.sort((a, b) => b.score - a.score)
|
|
177
|
+
|
|
178
|
+
const searchImportOptions = importOptions
|
|
179
|
+
.map((option) => {
|
|
180
|
+
const { score, matches } = fuzzyMatch(
|
|
181
|
+
searchQuery,
|
|
182
|
+
`Import ${option.name}`,
|
|
183
|
+
)
|
|
184
|
+
return { ...option, score, matches }
|
|
185
|
+
})
|
|
186
|
+
.filter((option) => option.score >= 0)
|
|
187
|
+
.sort((a, b) => b.score - a.score)
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
blankTemplates: searchBlankTemplates,
|
|
191
|
+
templates: searchTemplates,
|
|
192
|
+
importOptions: searchImportOptions,
|
|
193
|
+
}
|
|
194
|
+
}, [searchQuery, blankTemplates, templates, importOptions])
|
|
195
|
+
|
|
196
|
+
const allItems = useMemo(() => {
|
|
197
|
+
const items: Array<{
|
|
198
|
+
type: "package" | "recent" | "template" | "blank" | "import"
|
|
199
|
+
item: any
|
|
200
|
+
disabled?: boolean
|
|
201
|
+
}> = []
|
|
202
|
+
|
|
203
|
+
if (searchQuery && searchResults.length > 0) {
|
|
204
|
+
searchResults.forEach((pkg) => {
|
|
205
|
+
items.push({ type: "package", item: pkg })
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!searchQuery && recentPackages.length > 0) {
|
|
210
|
+
recentPackages.slice(0, 6).forEach((pkg) => {
|
|
211
|
+
items.push({ type: "recent", item: pkg })
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
filteredStaticOptions.blankTemplates.forEach((template) => {
|
|
216
|
+
items.push({ type: "blank", item: template, disabled: template.disabled })
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
filteredStaticOptions.templates.forEach((template) => {
|
|
220
|
+
items.push({ type: "template", item: template })
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
filteredStaticOptions.importOptions.forEach((option) => {
|
|
224
|
+
items.push({ type: "import", item: option })
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
return items
|
|
228
|
+
}, [searchQuery, searchResults, recentPackages, filteredStaticOptions])
|
|
229
|
+
|
|
230
|
+
useHotkeyCombo("cmd+k", () => {
|
|
231
|
+
setOpen((prev) => !prev)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
setSelectedIndex(0)
|
|
236
|
+
}, [allItems.length])
|
|
237
|
+
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
if (selectedItemRef.current) {
|
|
240
|
+
selectedItemRef.current.scrollIntoView({
|
|
241
|
+
behavior: "smooth",
|
|
242
|
+
block: "nearest",
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
}, [selectedIndex])
|
|
246
|
+
|
|
247
|
+
const handleKeyDown = useCallback(
|
|
248
|
+
(e: React.KeyboardEvent) => {
|
|
249
|
+
if (e.key === "ArrowDown") {
|
|
250
|
+
e.preventDefault()
|
|
251
|
+
e.stopPropagation()
|
|
252
|
+
setSelectedIndex((prev) => {
|
|
253
|
+
const next = Math.min(prev + 1, allItems.length - 1)
|
|
254
|
+
return next
|
|
255
|
+
})
|
|
256
|
+
} else if (e.key === "ArrowUp") {
|
|
257
|
+
e.preventDefault()
|
|
258
|
+
e.stopPropagation()
|
|
259
|
+
setSelectedIndex((prev) => {
|
|
260
|
+
const next = Math.max(prev - 1, 0)
|
|
261
|
+
return next
|
|
262
|
+
})
|
|
263
|
+
} else if (e.key === "Enter") {
|
|
264
|
+
e.preventDefault()
|
|
265
|
+
e.stopPropagation()
|
|
266
|
+
if (allItems[selectedIndex] && !allItems[selectedIndex].disabled) {
|
|
267
|
+
handleItemSelect(allItems[selectedIndex])
|
|
268
|
+
}
|
|
269
|
+
} else if (e.key === "Escape") {
|
|
270
|
+
e.preventDefault()
|
|
271
|
+
e.stopPropagation()
|
|
272
|
+
setOpen(false)
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
[selectedIndex, allItems.length],
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
const handleItemSelect = useCallback(
|
|
279
|
+
(selectedItem: any) => {
|
|
280
|
+
const { type, item } = selectedItem
|
|
281
|
+
|
|
282
|
+
switch (type) {
|
|
283
|
+
case "package":
|
|
284
|
+
case "recent":
|
|
285
|
+
window.location.href = `/editor?package_id=${item.package_id}`
|
|
286
|
+
setOpen(false)
|
|
287
|
+
break
|
|
288
|
+
case "blank":
|
|
289
|
+
case "template":
|
|
290
|
+
if (!item.disabled) {
|
|
291
|
+
window.location.href = `/editor?template=${item.name.toLowerCase().replace(/ /g, "-")}`
|
|
292
|
+
setOpen(false)
|
|
293
|
+
}
|
|
294
|
+
break
|
|
295
|
+
case "import":
|
|
296
|
+
if (item.special) {
|
|
297
|
+
setOpen(false)
|
|
298
|
+
setIsJLCPCBDialogOpen(true)
|
|
299
|
+
} else {
|
|
300
|
+
setOpen(false)
|
|
301
|
+
toastNotImplemented(`${item.name} Import`)
|
|
302
|
+
}
|
|
303
|
+
break
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
[toastNotImplemented],
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
const renderHighlighted = useCallback(
|
|
310
|
+
(item: any, text: string) => {
|
|
311
|
+
if (!searchQuery || !item.matches) return text
|
|
312
|
+
|
|
313
|
+
const chars = text.split("")
|
|
314
|
+
return chars.map((char, i) => (
|
|
315
|
+
<span key={i} className={item.matches.includes(i) ? "bg-blue-200" : ""}>
|
|
316
|
+
{char}
|
|
317
|
+
</span>
|
|
318
|
+
))
|
|
319
|
+
},
|
|
320
|
+
[searchQuery],
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
const renderItem = useCallback(
|
|
324
|
+
(item: any, index: number) => {
|
|
325
|
+
const { type, item: data, disabled } = item
|
|
326
|
+
const isSelected = index === selectedIndex
|
|
327
|
+
|
|
328
|
+
const baseClasses = `
|
|
329
|
+
group flex items-center justify-between px-3 py-2 rounded-md cursor-pointer
|
|
330
|
+
transition-all duration-150 border border-transparent text-sm
|
|
331
|
+
${isSelected ? "bg-blue-50 border-blue-200" : "hover:bg-gray-50"}
|
|
332
|
+
${disabled ? "opacity-50 cursor-not-allowed" : ""}
|
|
333
|
+
`
|
|
334
|
+
|
|
335
|
+
switch (type) {
|
|
336
|
+
case "package":
|
|
337
|
+
case "recent":
|
|
338
|
+
return (
|
|
339
|
+
<div
|
|
340
|
+
key={`${type}-${data.package_id}`}
|
|
341
|
+
ref={isSelected ? selectedItemRef : null}
|
|
342
|
+
className={baseClasses}
|
|
343
|
+
onClick={() => !disabled && handleItemSelect(item)}
|
|
344
|
+
>
|
|
345
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
346
|
+
<Package2 className="w-4 h-4 text-blue-500 flex-shrink-0" />
|
|
347
|
+
<div className="flex flex-col min-w-0">
|
|
348
|
+
<span className="font-medium text-gray-900 truncate">
|
|
349
|
+
{type === "package"
|
|
350
|
+
? renderHighlighted(data, data.name)
|
|
351
|
+
: data.name}
|
|
352
|
+
</span>
|
|
353
|
+
{data.description && (
|
|
354
|
+
<span className="text-xs text-gray-500 truncate">
|
|
355
|
+
{data.description}
|
|
356
|
+
</span>
|
|
357
|
+
)}
|
|
358
|
+
{type === "recent" && (
|
|
359
|
+
<span className="text-xs text-gray-400">
|
|
360
|
+
{new Date(data.updated_at).toLocaleDateString()}
|
|
361
|
+
</span>
|
|
362
|
+
)}
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
<div className="flex items-center gap-1 flex-shrink-0">
|
|
366
|
+
<span className="text-xs text-gray-500 bg-gray-100 px-1.5 py-0.5 rounded">
|
|
367
|
+
package
|
|
368
|
+
</span>
|
|
369
|
+
{isSelected && <ArrowRight className="w-3 h-3 text-gray-400" />}
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
case "blank":
|
|
375
|
+
case "template":
|
|
376
|
+
return (
|
|
377
|
+
<div
|
|
378
|
+
key={`${type}-${data.name}`}
|
|
379
|
+
ref={isSelected ? selectedItemRef : null}
|
|
380
|
+
className={baseClasses}
|
|
381
|
+
onClick={() => !disabled && handleItemSelect(item)}
|
|
382
|
+
>
|
|
383
|
+
<div className="flex items-center gap-2">
|
|
384
|
+
{data.icon}
|
|
385
|
+
<span className="font-medium text-gray-900">
|
|
386
|
+
{renderHighlighted(data, data.name)}
|
|
387
|
+
</span>
|
|
388
|
+
</div>
|
|
389
|
+
<div className="flex items-center gap-1">
|
|
390
|
+
<span className="text-xs text-gray-500 bg-gray-100 px-1.5 py-0.5 rounded">
|
|
391
|
+
{data.type}
|
|
392
|
+
</span>
|
|
393
|
+
{isSelected && <ArrowRight className="w-3 h-3 text-gray-400" />}
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
case "import":
|
|
399
|
+
return (
|
|
400
|
+
<div
|
|
401
|
+
key={`import-${data.name}`}
|
|
402
|
+
ref={isSelected ? selectedItemRef : null}
|
|
403
|
+
className={baseClasses}
|
|
404
|
+
onClick={() => handleItemSelect(item)}
|
|
405
|
+
>
|
|
406
|
+
<div className="flex items-center gap-2">
|
|
407
|
+
{data.icon}
|
|
408
|
+
<span className="font-medium text-gray-900">
|
|
409
|
+
Import {renderHighlighted(data, data.name)}
|
|
410
|
+
</span>
|
|
411
|
+
</div>
|
|
412
|
+
<div className="flex items-center gap-1">
|
|
413
|
+
<span className="text-xs text-gray-500 bg-gray-100 px-1.5 py-0.5 rounded">
|
|
414
|
+
{data.type}
|
|
415
|
+
</span>
|
|
416
|
+
{isSelected && <ArrowRight className="w-3 h-3 text-gray-400" />}
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
default:
|
|
422
|
+
return null
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
[selectedIndex, handleItemSelect, renderHighlighted],
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
if (!open)
|
|
429
|
+
return (
|
|
430
|
+
<JLCPCBImportDialog
|
|
431
|
+
open={isJLCPCBDialogOpen}
|
|
432
|
+
onOpenChange={setIsJLCPCBDialogOpen}
|
|
433
|
+
/>
|
|
434
|
+
)
|
|
92
435
|
|
|
93
436
|
return (
|
|
94
437
|
<>
|
|
438
|
+
<div
|
|
439
|
+
className="fixed inset-0 bg-black/20 backdrop-blur-sm z-40"
|
|
440
|
+
onClick={() => setOpen(false)}
|
|
441
|
+
/>
|
|
442
|
+
|
|
95
443
|
<Command.Dialog
|
|
96
444
|
open={open}
|
|
97
445
|
onOpenChange={setOpen}
|
|
98
446
|
label="Command Menu"
|
|
99
|
-
className="fixed top-
|
|
100
|
-
loop
|
|
447
|
+
className="fixed top-16 left-1/2 -translate-x-1/2 max-w-2xl w-[90vw] bg-white rounded-lg shadow-xl border border-gray-200 z-50"
|
|
448
|
+
loop={false}
|
|
449
|
+
onKeyDown={handleKeyDown}
|
|
450
|
+
aria-describedby="dialog-description"
|
|
101
451
|
>
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
>
|
|
109
|
-
<path
|
|
110
|
-
strokeLinecap="round"
|
|
111
|
-
strokeLinejoin="round"
|
|
112
|
-
strokeWidth={2}
|
|
113
|
-
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
114
|
-
/>
|
|
115
|
-
</svg>
|
|
452
|
+
<DialogTitle className="sr-only">Command Menu</DialogTitle>
|
|
453
|
+
<DialogDescription id="dialog-description" className="sr-only">
|
|
454
|
+
Use this menu to search packages and commands.
|
|
455
|
+
</DialogDescription>
|
|
456
|
+
<div className="flex items-center border-b border-gray-200 px-4 py-3">
|
|
457
|
+
<Search className="w-4 h-4 mr-2 text-gray-400 flex-shrink-0" />
|
|
116
458
|
<Command.Input
|
|
117
459
|
placeholder="Search packages and commands..."
|
|
118
460
|
value={searchQuery}
|
|
119
461
|
onValueChange={setSearchQuery}
|
|
120
|
-
className="w-full
|
|
462
|
+
className="w-full bg-transparent border-none outline-none text-gray-900 placeholder-gray-500"
|
|
121
463
|
/>
|
|
122
464
|
</div>
|
|
123
465
|
|
|
124
|
-
<Command.List className="max-h-
|
|
466
|
+
<Command.List className="max-h-80 overflow-y-auto p-2 space-y-4">
|
|
125
467
|
{isSearching ? (
|
|
126
|
-
<Command.Loading className="p-
|
|
127
|
-
|
|
468
|
+
<Command.Loading className="p-6 text-center text-gray-500">
|
|
469
|
+
<div className="flex items-center justify-center gap-2">
|
|
470
|
+
<div className="w-3 h-3 border-2 border-gray-300 border-t-gray-600 rounded-full animate-spin"></div>
|
|
471
|
+
Loading...
|
|
472
|
+
</div>
|
|
128
473
|
</Command.Loading>
|
|
129
474
|
) : (
|
|
130
475
|
<>
|
|
131
476
|
{searchQuery && searchResults.length > 0 && (
|
|
132
|
-
<
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
<div className="flex flex-col">
|
|
147
|
-
<span className="text-gray-900 dark:text-gray-100">
|
|
148
|
-
{pkg.name}
|
|
149
|
-
</span>
|
|
150
|
-
{pkg.description && (
|
|
151
|
-
<span className="text-sm text-gray-500">
|
|
152
|
-
{pkg.description}
|
|
153
|
-
</span>
|
|
154
|
-
)}
|
|
155
|
-
</div>
|
|
156
|
-
<span className="text-sm text-gray-500">package</span>
|
|
157
|
-
</Command.Item>
|
|
158
|
-
))}
|
|
159
|
-
</Command.Group>
|
|
477
|
+
<div>
|
|
478
|
+
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2 px-2">
|
|
479
|
+
Search Results
|
|
480
|
+
</h3>
|
|
481
|
+
<div className="space-y-1">
|
|
482
|
+
{searchResults.slice(0, 8).map((pkg, localIndex) => {
|
|
483
|
+
const globalIndex = localIndex
|
|
484
|
+
return renderItem(
|
|
485
|
+
{ type: "package", item: pkg },
|
|
486
|
+
globalIndex,
|
|
487
|
+
)
|
|
488
|
+
})}
|
|
489
|
+
</div>
|
|
490
|
+
</div>
|
|
160
491
|
)}
|
|
161
492
|
|
|
162
493
|
{!searchQuery && recentPackages.length > 0 && (
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
<span className="text-gray-900 dark:text-gray-100">
|
|
179
|
-
{pkg.name}
|
|
180
|
-
</span>
|
|
181
|
-
<span className="text-sm text-gray-500">
|
|
182
|
-
Last edited:{" "}
|
|
183
|
-
{new Date(pkg.updated_at).toLocaleDateString()}
|
|
184
|
-
</span>
|
|
185
|
-
</div>
|
|
186
|
-
<span className="text-sm text-gray-500">package</span>
|
|
187
|
-
</Command.Item>
|
|
188
|
-
))}
|
|
189
|
-
</Command.Group>
|
|
494
|
+
<div>
|
|
495
|
+
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2 px-2 flex items-center gap-1">
|
|
496
|
+
<Clock className="w-3 h-3" />
|
|
497
|
+
Recent
|
|
498
|
+
</h3>
|
|
499
|
+
<div className="space-y-1">
|
|
500
|
+
{recentPackages.slice(0, 6).map((pkg, localIndex) => {
|
|
501
|
+
const globalIndex = localIndex
|
|
502
|
+
return renderItem(
|
|
503
|
+
{ type: "recent", item: pkg },
|
|
504
|
+
globalIndex,
|
|
505
|
+
)
|
|
506
|
+
})}
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
190
509
|
)}
|
|
191
510
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
))}
|
|
217
|
-
</Command.Group>
|
|
218
|
-
|
|
219
|
-
<Command.Group
|
|
220
|
-
heading="Start from Template"
|
|
221
|
-
className="px-2 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400"
|
|
222
|
-
>
|
|
223
|
-
{templates.map((template) => (
|
|
224
|
-
<Command.Item
|
|
225
|
-
key={template.name}
|
|
226
|
-
value={template.name}
|
|
227
|
-
onSelect={() => {
|
|
228
|
-
window.location.href = `/editor?template=${template.name.toLowerCase().replace(/ /g, "-")}`
|
|
229
|
-
setOpen(false)
|
|
230
|
-
}}
|
|
231
|
-
className="flex items-center justify-between px-2 py-1.5 rounded-sm text-sm hover:bg-gray-100 dark:hover:bg-gray-700 cursor-default aria-selected:bg-gray-100 dark:aria-selected:bg-gray-700"
|
|
232
|
-
>
|
|
233
|
-
<span className="text-gray-900 dark:text-gray-100">
|
|
234
|
-
{template.name}
|
|
235
|
-
</span>
|
|
236
|
-
<span className="text-sm text-gray-500">
|
|
237
|
-
{template.type}
|
|
238
|
-
</span>
|
|
239
|
-
</Command.Item>
|
|
240
|
-
))}
|
|
241
|
-
</Command.Group>
|
|
242
|
-
|
|
243
|
-
<Command.Group
|
|
244
|
-
heading="Import"
|
|
245
|
-
className="px-2 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400"
|
|
246
|
-
>
|
|
247
|
-
{importOptions.map((option) => (
|
|
248
|
-
<Command.Item
|
|
249
|
-
key={option.name}
|
|
250
|
-
value={option.name}
|
|
251
|
-
onSelect={() => {
|
|
252
|
-
if (option.special) {
|
|
253
|
-
setOpen(false)
|
|
254
|
-
setIsJLCPCBDialogOpen(true)
|
|
255
|
-
} else {
|
|
256
|
-
setOpen(false)
|
|
257
|
-
toastNotImplemented(`${option.name} Import`)
|
|
258
|
-
}
|
|
259
|
-
}}
|
|
260
|
-
className="flex items-center justify-between px-2 py-1.5 rounded-sm text-sm hover:bg-gray-100 dark:hover:bg-gray-700 cursor-default aria-selected:bg-gray-100 dark:aria-selected:bg-gray-700"
|
|
261
|
-
>
|
|
262
|
-
<span className="text-gray-900 dark:text-gray-100">
|
|
263
|
-
Import {option.name}
|
|
264
|
-
</span>
|
|
265
|
-
<span className="text-sm text-gray-500">{option.type}</span>
|
|
266
|
-
</Command.Item>
|
|
267
|
-
))}
|
|
268
|
-
</Command.Group>
|
|
269
|
-
|
|
270
|
-
{searchQuery && !searchResults.length && !isSearching && (
|
|
271
|
-
<Command.Empty className="py-6 text-center text-sm text-gray-500">
|
|
272
|
-
No results found.
|
|
273
|
-
</Command.Empty>
|
|
511
|
+
{filteredStaticOptions.blankTemplates.length > 0 && (
|
|
512
|
+
<div>
|
|
513
|
+
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2 px-2">
|
|
514
|
+
Create
|
|
515
|
+
</h3>
|
|
516
|
+
<div className="space-y-1">
|
|
517
|
+
{filteredStaticOptions.blankTemplates.map(
|
|
518
|
+
(template, localIndex) => {
|
|
519
|
+
const globalIndex =
|
|
520
|
+
(searchQuery
|
|
521
|
+
? searchResults.length
|
|
522
|
+
: recentPackages.length) + localIndex
|
|
523
|
+
return renderItem(
|
|
524
|
+
{
|
|
525
|
+
type: "blank",
|
|
526
|
+
item: template,
|
|
527
|
+
disabled: template.disabled,
|
|
528
|
+
},
|
|
529
|
+
globalIndex,
|
|
530
|
+
)
|
|
531
|
+
},
|
|
532
|
+
)}
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
274
535
|
)}
|
|
536
|
+
|
|
537
|
+
{filteredStaticOptions.templates.length > 0 && (
|
|
538
|
+
<div>
|
|
539
|
+
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2 px-2">
|
|
540
|
+
Templates
|
|
541
|
+
</h3>
|
|
542
|
+
<div className="space-y-1">
|
|
543
|
+
{filteredStaticOptions.templates.map(
|
|
544
|
+
(template, localIndex) => {
|
|
545
|
+
const globalIndex =
|
|
546
|
+
(searchQuery
|
|
547
|
+
? searchResults.length
|
|
548
|
+
: recentPackages.length) +
|
|
549
|
+
filteredStaticOptions.blankTemplates.length +
|
|
550
|
+
localIndex
|
|
551
|
+
return renderItem(
|
|
552
|
+
{ type: "template", item: template },
|
|
553
|
+
globalIndex,
|
|
554
|
+
)
|
|
555
|
+
},
|
|
556
|
+
)}
|
|
557
|
+
</div>
|
|
558
|
+
</div>
|
|
559
|
+
)}
|
|
560
|
+
|
|
561
|
+
{filteredStaticOptions.importOptions.length > 0 && (
|
|
562
|
+
<div>
|
|
563
|
+
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2 px-2">
|
|
564
|
+
Import
|
|
565
|
+
</h3>
|
|
566
|
+
<div className="space-y-1">
|
|
567
|
+
{filteredStaticOptions.importOptions.map(
|
|
568
|
+
(option, localIndex) => {
|
|
569
|
+
const globalIndex =
|
|
570
|
+
(searchQuery
|
|
571
|
+
? searchResults.length
|
|
572
|
+
: recentPackages.length) +
|
|
573
|
+
filteredStaticOptions.blankTemplates.length +
|
|
574
|
+
filteredStaticOptions.templates.length +
|
|
575
|
+
localIndex
|
|
576
|
+
return renderItem(
|
|
577
|
+
{ type: "import", item: option },
|
|
578
|
+
globalIndex,
|
|
579
|
+
)
|
|
580
|
+
},
|
|
581
|
+
)}
|
|
582
|
+
</div>
|
|
583
|
+
</div>
|
|
584
|
+
)}
|
|
585
|
+
|
|
586
|
+
{searchQuery &&
|
|
587
|
+
!searchResults.length &&
|
|
588
|
+
!filteredStaticOptions.blankTemplates.length &&
|
|
589
|
+
!filteredStaticOptions.templates.length &&
|
|
590
|
+
!filteredStaticOptions.importOptions.length &&
|
|
591
|
+
!isSearching && (
|
|
592
|
+
<Command.Empty className="py-8 text-center">
|
|
593
|
+
<div className="text-gray-400 mb-1">No results found</div>
|
|
594
|
+
<div className="text-gray-500 text-xs">
|
|
595
|
+
Try different search terms
|
|
596
|
+
</div>
|
|
597
|
+
</Command.Empty>
|
|
598
|
+
)}
|
|
275
599
|
</>
|
|
276
600
|
)}
|
|
277
601
|
</Command.List>
|
|
278
602
|
|
|
279
|
-
<div className="border-t border-gray-200
|
|
280
|
-
<div className="flex justify-between text-xs text-gray-500">
|
|
281
|
-
<div className="flex
|
|
282
|
-
<
|
|
283
|
-
|
|
284
|
-
|
|
603
|
+
<div className="border-t border-gray-200 px-4 py-2 bg-gray-50/50 rounded-b-lg">
|
|
604
|
+
<div className="flex justify-between items-center text-xs text-gray-500">
|
|
605
|
+
<div className="flex items-center gap-4">
|
|
606
|
+
<div className="flex items-center gap-1">
|
|
607
|
+
<kbd className="px-1.5 py-0.5 font-mono bg-white border border-gray-300 rounded text-xs">
|
|
608
|
+
↑↓
|
|
609
|
+
</kbd>
|
|
610
|
+
<span>navigate</span>
|
|
611
|
+
</div>
|
|
612
|
+
<div className="flex items-center gap-1">
|
|
613
|
+
<kbd className="px-1.5 py-0.5 font-mono bg-white border border-gray-300 rounded text-xs">
|
|
614
|
+
↵
|
|
615
|
+
</kbd>
|
|
616
|
+
<span>select</span>
|
|
617
|
+
</div>
|
|
285
618
|
</div>
|
|
286
|
-
<div>
|
|
287
|
-
<
|
|
619
|
+
<div className="flex items-center gap-1">
|
|
620
|
+
<kbd className="px-1.5 py-0.5 font-mono bg-white border border-gray-300 rounded text-xs">
|
|
621
|
+
⌘K
|
|
622
|
+
</kbd>
|
|
623
|
+
<span>close</span>
|
|
288
624
|
</div>
|
|
289
625
|
</div>
|
|
290
626
|
</div>
|