@tscircuit/fake-snippets 0.0.88 → 0.0.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/api/generated-index.js +96 -14
  2. package/bun-tests/fake-snippets-api/routes/proxy.test.ts +42 -0
  3. package/bun.lock +196 -215
  4. package/dist/bundle.js +596 -370
  5. package/fake-snippets-api/routes/api/autocomplete/create_autocomplete.ts +134 -0
  6. package/fake-snippets-api/routes/api/proxy.ts +128 -0
  7. package/package.json +59 -48
  8. package/renovate.json +2 -1
  9. package/src/App.tsx +67 -3
  10. package/src/ContextProviders.tsx +2 -0
  11. package/src/build-watcher.ts +52 -0
  12. package/src/components/CircuitJsonImportDialog.tsx +1 -1
  13. package/src/components/CmdKMenu.tsx +533 -197
  14. package/src/components/DownloadButtonAndMenu.tsx +104 -26
  15. package/src/components/FileSidebar.tsx +11 -1
  16. package/src/components/Header2.tsx +7 -2
  17. package/src/components/PackageBuildsPage/LogContent.tsx +25 -22
  18. package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +6 -6
  19. package/src/components/PackageBuildsPage/build-preview-content.tsx +5 -5
  20. package/src/components/PackageBuildsPage/package-build-details-panel.tsx +15 -13
  21. package/src/components/PackageBuildsPage/package-build-header.tsx +19 -28
  22. package/src/components/PackageCard.tsx +66 -16
  23. package/src/components/SearchComponent.tsx +2 -2
  24. package/src/components/SuspenseRunFrame.tsx +14 -2
  25. package/src/components/ViewPackagePage/components/important-files-view.tsx +90 -17
  26. package/src/components/ViewPackagePage/components/main-content-header.tsx +26 -2
  27. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +2 -2
  28. package/src/components/ViewPackagePage/components/repo-page-content.tsx +35 -30
  29. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +2 -2
  30. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +20 -12
  31. package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +0 -7
  32. package/src/components/ViewPackagePage/utils/fuzz-search.ts +121 -0
  33. package/src/components/ViewPackagePage/utils/is-hidden-file.ts +4 -0
  34. package/src/components/dialogs/confirm-delete-package-dialog.tsx +1 -1
  35. package/src/components/dialogs/confirm-discard-changes-dialog.tsx +73 -0
  36. package/src/components/dialogs/edit-package-details-dialog.tsx +2 -2
  37. package/src/components/dialogs/view-ts-files-dialog.tsx +478 -42
  38. package/src/components/package-port/CodeAndPreview.tsx +17 -16
  39. package/src/components/package-port/CodeEditor.tsx +138 -17
  40. package/src/components/package-port/CodeEditorHeader.tsx +44 -4
  41. package/src/components/package-port/EditorNav.tsx +42 -29
  42. package/src/components/package-port/GlobalFindReplace.tsx +681 -0
  43. package/src/components/package-port/QuickOpen.tsx +241 -0
  44. package/src/components/ui/dialog.tsx +1 -1
  45. package/src/components/ui/tree-view.tsx +1 -1
  46. package/src/global.d.ts +3 -0
  47. package/src/hooks/use-ai-review.ts +31 -0
  48. package/src/hooks/use-code-completion-ai-api.ts +3 -3
  49. package/src/hooks/use-current-package-release.ts +5 -1
  50. package/src/hooks/use-delete-package.ts +6 -2
  51. package/src/hooks/use-download-zip.ts +50 -0
  52. package/src/hooks/use-hotkey.ts +116 -0
  53. package/src/hooks/use-package-by-package-id.ts +1 -0
  54. package/src/hooks/use-package-by-package-name.ts +1 -0
  55. package/src/hooks/use-package-files.ts +3 -0
  56. package/src/hooks/use-package-release.ts +5 -1
  57. package/src/hooks/use-package.ts +1 -0
  58. package/src/hooks/use-request-ai-review-mutation.ts +14 -6
  59. package/src/hooks/use-snippet.ts +1 -0
  60. package/src/hooks/useFileManagement.ts +28 -10
  61. package/src/hooks/usePackageFilesLoader.ts +3 -1
  62. package/src/index.css +11 -0
  63. package/src/lib/decodeUrlHashToFsMap.ts +17 -0
  64. package/src/lib/download-fns/download-circuit-png.ts +88 -0
  65. package/src/lib/download-fns/download-png-utils.ts +31 -0
  66. package/src/lib/encodeFsMapToUrlHash.ts +13 -0
  67. package/src/lib/populate-query-cache-with-ssr-data.ts +7 -0
  68. package/src/lib/ts-lib-cache.ts +47 -0
  69. package/src/lib/types.ts +2 -0
  70. package/src/lib/utils/findTargetFile.ts +1 -1
  71. package/src/lib/utils/package-utils.ts +10 -0
  72. package/src/main.tsx +7 -0
  73. package/src/pages/dashboard.tsx +18 -5
  74. package/src/pages/view-package.tsx +15 -7
  75. package/src/types/package.ts +4 -0
  76. 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, Snippet } from "fake-snippets-api/lib/db/schema"
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
- // Search results query
33
- const { data: searchResults = [], isLoading: isSearching } = useQuery(
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
- // Recent packages query
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
- React.useEffect(() => {
63
- const down = (e: KeyboardEvent) => {
64
- if ((e.metaKey || e.ctrlKey) && e.key === "k") {
65
- e.preventDefault()
66
- setOpen((prev) => !prev)
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
- document.addEventListener("keydown", down)
71
- return () => document.removeEventListener("keydown", down)
72
- }, [])
73
-
74
- const blankTemplates: Template[] = [
75
- { name: "New Circuit Board", type: "board" },
76
- { name: "New Circuit Module", type: "package" },
77
- { name: "New 3D Model", type: "model", disabled: true },
78
- { name: "New Footprint", type: "footprint", disabled: true },
79
- ]
80
-
81
- const templates: Template[] = [
82
- { name: "Blinking LED Board", type: "board" },
83
- { name: "USB-C LED Flashlight", type: "board" },
84
- ]
85
-
86
- const importOptions: ImportOption[] = [
87
- { name: "KiCad Footprint", type: "footprint" },
88
- { name: "KiCad Project", type: "board" },
89
- { name: "KiCad Module", type: "package" },
90
- { name: "JLCPCB Component", type: "package", special: true },
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-32 left-1/2 -translate-x-1/2 max-w-2xl w-full bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700"
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
- <div className="flex items-center border-b border-gray-200 dark:border-gray-700 px-3">
103
- <svg
104
- className="w-4 h-4 mr-2 text-gray-400"
105
- fill="none"
106
- stroke="currentColor"
107
- viewBox="0 0 24 24"
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 h-12 bg-transparent border-none outline-none text-gray-900 dark:text-gray-100 placeholder-gray-500"
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-96 overflow-y-auto p-2">
466
+ <Command.List className="max-h-80 overflow-y-auto p-2 space-y-4">
125
467
  {isSearching ? (
126
- <Command.Loading className="p-4 text-sm text-gray-500">
127
- Loading results...
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
- <Command.Group
133
- heading="Search Results"
134
- className="px-2 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400"
135
- >
136
- {searchResults.map((pkg: Package) => (
137
- <Command.Item
138
- key={pkg.package_id}
139
- value={pkg.name}
140
- onSelect={() => {
141
- window.location.href = `/editor?package_id=${pkg.package_id}`
142
- setOpen(false)
143
- }}
144
- 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"
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
- <Command.Group
164
- heading="Recent Packages"
165
- className="px-2 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400"
166
- >
167
- {recentPackages.slice(0, 6).map((pkg) => (
168
- <Command.Item
169
- key={pkg.package_id}
170
- value={pkg.unscoped_name}
171
- onSelect={() => {
172
- window.location.href = `/editor?package_id=${pkg.package_id}`
173
- setOpen(false)
174
- }}
175
- 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"
176
- >
177
- <div className="flex flex-col">
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
- <Command.Group
193
- heading="Start Blank Package"
194
- className="px-2 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400"
195
- >
196
- {blankTemplates.map((template) => (
197
- <Command.Item
198
- key={template.name}
199
- value={template.name}
200
- disabled={template.disabled}
201
- onSelect={() => {
202
- if (!template.disabled) {
203
- window.location.href = `/editor?template=${template.name.toLowerCase().replace(/ /g, "-")}`
204
- setOpen(false)
205
- }
206
- }}
207
- 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 disabled:opacity-50 aria-selected:bg-gray-100 dark:aria-selected:bg-gray-700"
208
- >
209
- <span className="text-gray-900 dark:text-gray-100">
210
- {template.name}
211
- </span>
212
- <span className="text-sm text-gray-500">
213
- {template.type}
214
- </span>
215
- </Command.Item>
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 dark:border-gray-700 px-3 py-2">
280
- <div className="flex justify-between text-xs text-gray-500">
281
- <div className="flex space-x-4">
282
- <span>↑↓ to navigate</span>
283
- <span>↵ to select</span>
284
- <span>esc to close</span>
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
- <span>⌘K to toggle</span>
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>