@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,7 +1,12 @@
1
1
  import { useSnippetsBaseApiUrl } from "@/hooks/use-snippets-base-api-url"
2
+ import { useHotkeyCombo } from "@/hooks/use-hotkey"
2
3
  import { basicSetup } from "@/lib/codemirror/basic-setup"
3
- import { autocompletion } from "@codemirror/autocomplete"
4
- import { indentWithTab } from "@codemirror/commands"
4
+ import {
5
+ autocompletion,
6
+ acceptCompletion,
7
+ completionStatus,
8
+ } from "@codemirror/autocomplete"
9
+ import { indentWithTab, indentMore } from "@codemirror/commands"
5
10
  import { javascript } from "@codemirror/lang-javascript"
6
11
  import { json } from "@codemirror/lang-json"
7
12
  import { EditorState, Prec } from "@codemirror/state"
@@ -12,10 +17,10 @@ import { setupTypeAcquisition } from "@typescript/ata"
12
17
  import { linter } from "@codemirror/lint"
13
18
  import { TSCI_PACKAGE_PATTERN } from "@/lib/constants"
14
19
  import {
15
- createDefaultMapFromCDN,
16
20
  createSystem,
17
21
  createVirtualTypeScriptEnvironment,
18
22
  } from "@typescript/vfs"
23
+ import { loadDefaultLibMap } from "@/lib/ts-lib-cache"
19
24
  import { tsAutocomplete, tsFacet, tsSync } from "@valtown/codemirror-ts"
20
25
  import { getLints } from "@valtown/codemirror-ts"
21
26
  import { EditorView } from "codemirror"
@@ -27,14 +32,18 @@ import CodeEditorHeader, {
27
32
  import { useCodeCompletionApi } from "@/hooks/use-code-completion-ai-api"
28
33
  import FileSidebar from "../FileSidebar"
29
34
  import { findTargetFile } from "@/lib/utils/findTargetFile"
30
- import type { PackageFile } from "./CodeAndPreview"
35
+ import type { PackageFile } from "@/types/package"
31
36
  import { useShikiHighlighter } from "@/hooks/use-shiki-highlighter"
37
+ import QuickOpen from "./QuickOpen"
38
+ import GlobalFindReplace from "./GlobalFindReplace"
32
39
  import {
33
40
  ICreateFileProps,
34
41
  ICreateFileResult,
35
42
  IDeleteFileProps,
36
43
  IDeleteFileResult,
37
44
  } from "@/hooks/useFileManagement"
45
+ import { isHiddenFile } from "../ViewPackagePage/utils/is-hidden-file"
46
+ import { inlineCopilot } from "codemirror-copilot"
38
47
 
39
48
  const defaultImports = `
40
49
  import React from "@types/react/jsx-runtime"
@@ -77,12 +86,16 @@ export const CodeEditor = ({
77
86
  const codeCompletionApi = useCodeCompletionApi()
78
87
  const [cursorPosition, setCursorPosition] = useState<number | null>(null)
79
88
  const [code, setCode] = useState(files[0]?.content || "")
89
+ const [fontSize, setFontSize] = useState(14)
90
+ const [showQuickOpen, setShowQuickOpen] = useState(false)
91
+ const [showGlobalFindReplace, setShowGlobalFindReplace] = useState(false)
80
92
 
81
93
  const { highlighter } = useShikiHighlighter()
82
94
 
83
95
  // Get URL search params for file_path
84
96
  const urlParams = new URLSearchParams(window.location.search)
85
97
  const filePathFromUrl = urlParams.get("file_path")
98
+ const [aiAutocompleteEnabled, setAiAutocompleteEnabled] = useState(false)
86
99
 
87
100
  const entryPointFileName = useMemo(() => {
88
101
  const entryPointFile = findTargetFile(files, null)
@@ -132,6 +145,14 @@ export const CodeEditor = ({
132
145
  }
133
146
  }, [isStreaming])
134
147
 
148
+ useHotkeyCombo(
149
+ "cmd+b",
150
+ () => {
151
+ setSidebarOpen((prev) => !prev)
152
+ },
153
+ { target: window },
154
+ )
155
+
135
156
  useEffect(() => {
136
157
  if (!editorRef.current) return
137
158
 
@@ -141,12 +162,7 @@ export const CodeEditor = ({
141
162
  })
142
163
  ;(window as any).__DEBUG_CODE_EDITOR_FS_MAP = fsMap
143
164
 
144
- createDefaultMapFromCDN(
145
- { target: tsModule.ScriptTarget.ES2022 },
146
- "5.6.3",
147
- true,
148
- tsModule,
149
- ).then((defaultFsMap) => {
165
+ loadDefaultLibMap().then((defaultFsMap) => {
150
166
  defaultFsMap.forEach((content, filename) => {
151
167
  fsMap.set(filename, content)
152
168
  })
@@ -243,6 +259,29 @@ export const CodeEditor = ({
243
259
  key: "Mod-Enter",
244
260
  run: () => true,
245
261
  },
262
+ {
263
+ key: "Tab",
264
+ run: (view) => {
265
+ if (completionStatus(view.state) === "active") {
266
+ return acceptCompletion(view)
267
+ }
268
+ return indentMore(view)
269
+ },
270
+ },
271
+ {
272
+ key: "Mod-p",
273
+ run: () => {
274
+ setShowQuickOpen(true)
275
+ return true
276
+ },
277
+ },
278
+ {
279
+ key: "Mod-Shift-f",
280
+ run: () => {
281
+ setShowGlobalFindReplace(true)
282
+ return true
283
+ },
284
+ },
246
285
  ]),
247
286
  ),
248
287
  keymap.of([indentWithTab]),
@@ -263,13 +302,48 @@ export const CodeEditor = ({
263
302
  setCursorPosition(pos)
264
303
  }
265
304
  }),
305
+ EditorView.theme({
306
+ ".cm-editor": {
307
+ fontSize: `${fontSize}px`,
308
+ },
309
+ ".cm-content": {
310
+ fontSize: `${fontSize}px`,
311
+ },
312
+ }),
313
+ EditorView.domEventHandlers({
314
+ wheel: (event) => {
315
+ if (event.ctrlKey || event.metaKey) {
316
+ event.preventDefault()
317
+ const delta = event.deltaY
318
+ setFontSize((prev) => {
319
+ const newSize =
320
+ delta > 0 ? Math.max(8, prev - 1) : Math.min(32, prev + 1)
321
+ return newSize
322
+ })
323
+ return true
324
+ }
325
+ return false
326
+ },
327
+ }),
266
328
  ]
267
- if (codeCompletionApi?.apiKey) {
329
+ if (aiAutocompleteEnabled) {
268
330
  baseExtensions.push(
269
- // copilotPlugin({
270
- // apiKey: codeCompletionApi.apiKey,
271
- // language: Language.TYPESCRIPT,
272
- // }),
331
+ inlineCopilot(async (prefix, suffix) => {
332
+ const res = await fetch("/api/autocomplete/create_autocomplete", {
333
+ method: "POST",
334
+ headers: {
335
+ "Content-Type": "application/json",
336
+ },
337
+ body: JSON.stringify({
338
+ prefix,
339
+ suffix,
340
+ language: "typescript",
341
+ }),
342
+ })
343
+
344
+ const { prediction } = await res.json()
345
+ return prediction
346
+ }),
273
347
  EditorView.theme({
274
348
  ".cm-ghostText, .cm-ghostText *": {
275
349
  opacity: "0.6",
@@ -470,7 +544,15 @@ export const CodeEditor = ({
470
544
  return () => {
471
545
  view.destroy()
472
546
  }
473
- }, [!isStreaming, currentFile, code !== "", Boolean(highlighter), isSaving])
547
+ }, [
548
+ !isStreaming,
549
+ currentFile,
550
+ code !== "",
551
+ Boolean(highlighter),
552
+ isSaving,
553
+ fontSize,
554
+ aiAutocompleteEnabled,
555
+ ])
474
556
 
475
557
  const updateCurrentEditorContent = (newContent: string) => {
476
558
  if (viewRef.current) {
@@ -541,12 +623,30 @@ export const CodeEditor = ({
541
623
  updateEditorToMatchCurrentFile()
542
624
  }, [currentFile])
543
625
 
626
+ // Global keyboard listeners
627
+ useHotkeyCombo("cmd+p", () => {
628
+ setShowQuickOpen(true)
629
+ })
630
+
631
+ useHotkeyCombo("cmd+shift+f", () => {
632
+ setShowGlobalFindReplace(true)
633
+ })
634
+
635
+ useHotkeyCombo("Escape", () => {
636
+ if (showQuickOpen) {
637
+ setShowQuickOpen(false)
638
+ }
639
+ if (showGlobalFindReplace) {
640
+ setShowGlobalFindReplace(false)
641
+ }
642
+ })
643
+
544
644
  if (isStreaming) {
545
645
  return <div className="font-mono whitespace-pre-wrap text-xs">{code}</div>
546
646
  }
547
647
  const [sidebarOpen, setSidebarOpen] = useState(false)
548
648
  return (
549
- <div className="flex h-screen w-full overflow-hidden">
649
+ <div className="flex h-[98vh] w-full overflow-hidden">
550
650
  <FileSidebar
551
651
  files={Object.fromEntries(files.map((f) => [f.path, f.content]))}
552
652
  currentFile={currentFile}
@@ -570,6 +670,10 @@ export const CodeEditor = ({
570
670
  files={Object.fromEntries(files.map((f) => [f.path, f.content]))}
571
671
  updateFileContent={updateFileContent}
572
672
  handleFileChange={handleFileChange}
673
+ aiAutocompleteState={[
674
+ aiAutocompleteEnabled,
675
+ setAiAutocompleteEnabled,
676
+ ]}
573
677
  />
574
678
  )}
575
679
  <div
@@ -579,6 +683,23 @@ export const CodeEditor = ({
579
683
  }
580
684
  />
581
685
  </div>
686
+ {showQuickOpen && (
687
+ <QuickOpen
688
+ files={files.filter((f) => !isHiddenFile(f.path))}
689
+ currentFile={currentFile}
690
+ onFileSelect={handleFileChange}
691
+ onClose={() => setShowQuickOpen(false)}
692
+ />
693
+ )}
694
+ {showGlobalFindReplace && (
695
+ <GlobalFindReplace
696
+ files={files.filter((f) => !isHiddenFile(f.path))}
697
+ currentFile={currentFile}
698
+ onFileSelect={handleFileChange}
699
+ onFileContentChanged={onCodeChange}
700
+ onClose={() => setShowGlobalFindReplace(false)}
701
+ />
702
+ )}
582
703
  </div>
583
704
  )
584
705
  }
@@ -9,7 +9,7 @@ import {
9
9
  DropdownMenuItem,
10
10
  DropdownMenuTrigger,
11
11
  } from "@/components/ui/dropdown-menu"
12
- import { AlertTriangle, PanelRightClose } from "lucide-react"
12
+ import { AlertTriangle, PanelRightClose, Bot } from "lucide-react"
13
13
  import { checkIfManualEditsImported } from "@/lib/utils/checkIfManualEditsImported"
14
14
  import {
15
15
  Select,
@@ -20,6 +20,13 @@ import {
20
20
  } from "../ui/select"
21
21
  import { isHiddenFile } from "../ViewPackagePage/utils/is-hidden-file"
22
22
  import { Package } from "fake-snippets-api/lib/db/schema"
23
+ import {
24
+ Tooltip,
25
+ TooltipContent,
26
+ TooltipProvider,
27
+ TooltipTrigger,
28
+ } from "@/components/ui/tooltip"
29
+ import ai from "fake-snippets-api/routes/api/ai"
23
30
 
24
31
  export type FileName = string
25
32
 
@@ -30,6 +37,7 @@ interface CodeEditorHeaderProps {
30
37
  fileSidebarState: ReturnType<typeof useState<boolean>>
31
38
  handleFileChange: (filename: FileName) => void
32
39
  entrypointFileName?: string
40
+ aiAutocompleteState: [boolean, React.Dispatch<React.SetStateAction<boolean>>]
33
41
  }
34
42
 
35
43
  export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
@@ -39,11 +47,13 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
39
47
  fileSidebarState,
40
48
  handleFileChange,
41
49
  entrypointFileName = "index.tsx",
50
+ aiAutocompleteState,
42
51
  }) => {
43
52
  const { Dialog: ImportPackageDialog, openDialog: openImportDialog } =
44
53
  useImportPackageDialog()
45
54
  const { toast } = useToast()
46
55
  const [sidebarOpen, setSidebarOpen] = fileSidebarState
56
+ const [aiAutocompleteEnabled, setAiAutocompleteEnabled] = aiAutocompleteState
47
57
 
48
58
  const handleFormatFile = useCallback(() => {
49
59
  if (!window.prettier || !window.prettierPlugins) return
@@ -152,7 +162,7 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
152
162
  <div>
153
163
  <Select value={currentFile || ""} onValueChange={handleFileChange}>
154
164
  <SelectTrigger
155
- className={`h-7 w-24 px-3 bg-white select-none transition-[margin] duration-300 ease-in-out ${
165
+ className={`h-7 w-32 sm:w-48 px-3 bg-white select-none transition-[margin] duration-300 ease-in-out ${
156
166
  sidebarOpen ? "-ml-2" : "-ml-1"
157
167
  }`}
158
168
  >
@@ -170,7 +180,9 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
170
180
  <SelectItem className="py-1" key={filename} value={filename}>
171
181
  <span
172
182
  className={`text-xs pr-1 block truncate ${
173
- sidebarOpen ? "max-w-[5rem]" : "max-w-[10rem]"
183
+ sidebarOpen
184
+ ? "max-w-[8rem] sm:max-w-[12rem]"
185
+ : "max-w-[12rem] sm:max-w-[16rem]"
174
186
  }`}
175
187
  >
176
188
  {filename}
@@ -187,7 +199,9 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
187
199
  <SelectItem className="select-none py-1" value={currentFile}>
188
200
  <span
189
201
  className={`text-xs pr-1 block truncate ${
190
- sidebarOpen ? "max-w-[5rem]" : "max-w-[10rem]"
202
+ sidebarOpen
203
+ ? "max-w-[8rem] sm:max-w-[12rem]"
204
+ : "max-w-[12rem] sm:max-w-[16rem]"
191
205
  }`}
192
206
  >
193
207
  {currentFile}
@@ -228,6 +242,32 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
228
242
  </DropdownMenuContent>
229
243
  </DropdownMenu>
230
244
  )}
245
+
246
+ <TooltipProvider>
247
+ <Tooltip>
248
+ <TooltipTrigger asChild>
249
+ <Button
250
+ size="sm"
251
+ variant="ghost"
252
+ onClick={() => {
253
+ setAiAutocompleteEnabled((prev) => !prev)
254
+ }}
255
+ className={`relative bg-transparent ${aiAutocompleteEnabled ? "text-gray-600 bg-gray-50" : "text-gray-400"}`}
256
+ >
257
+ <Bot className="h-4 w-4" />
258
+ {!aiAutocompleteEnabled && (
259
+ <div className="absolute inset-0 flex items-center justify-center">
260
+ <div className="w-5 h-0.5 bg-gray-400 rotate-45 rounded-full" />
261
+ </div>
262
+ )}
263
+ </Button>
264
+ </TooltipTrigger>
265
+ <TooltipContent>
266
+ <p>Toggle AI autocomplete for code suggestions</p>
267
+ </TooltipContent>
268
+ </Tooltip>
269
+ </TooltipProvider>
270
+
231
271
  <Button size="sm" variant="ghost" onClick={() => openImportDialog()}>
232
272
  Import
233
273
  </Button>
@@ -10,7 +10,7 @@ import {
10
10
  DropdownMenuTrigger,
11
11
  } from "@/components/ui/dropdown-menu"
12
12
  import { useGlobalStore } from "@/hooks/use-global-store"
13
- import { encodeTextToUrlHash } from "@/lib/encodeTextToUrlHash"
13
+ import { encodeFsMapToUrlHash } from "@/lib/encodeFsMapToUrlHash"
14
14
  import { cn } from "@/lib/utils"
15
15
  import { OpenInNewWindowIcon, LockClosedIcon } from "@radix-ui/react-icons"
16
16
  import { AnyCircuitElement } from "circuit-json"
@@ -18,28 +18,26 @@ import { Package } from "fake-snippets-api/lib/db/schema"
18
18
  import {
19
19
  ChevronDown,
20
20
  CodeIcon,
21
- Download,
22
21
  Edit2,
23
22
  Eye,
24
23
  EyeIcon,
25
24
  File,
26
25
  FilePenLine,
27
26
  MoreVertical,
28
- Package as PackageIcon,
29
27
  Pencil,
30
28
  Save,
31
29
  Share,
32
30
  Sidebar,
33
- Sparkles,
34
31
  Trash2,
32
+ Undo2,
35
33
  } from "lucide-react"
36
34
  import { useEffect, useMemo, useState } from "react"
37
35
  import { useQueryClient } from "react-query"
38
36
  import { Link, useLocation } from "wouter"
39
37
  import { useAxios } from "@/hooks/use-axios"
38
+ import { useHotkeyCombo } from "@/hooks/use-hotkey"
40
39
  import { useToast } from "@/hooks/use-toast"
41
40
  import { useConfirmDeletePackageDialog } from "@/components/dialogs/confirm-delete-package-dialog"
42
- import { useFilesDialog } from "@/components/dialogs/files-dialog"
43
41
  import { useViewTsFilesDialog } from "@/components/dialogs/view-ts-files-dialog"
44
42
  import { DownloadButtonAndMenu } from "@/components/DownloadButtonAndMenu"
45
43
  import { TypeBadge } from "@/components/TypeBadge"
@@ -52,22 +50,26 @@ export default function EditorNav({
52
50
  circuitJson,
53
51
  pkg,
54
52
  code,
53
+ fsMap,
55
54
  hasUnsavedChanges,
56
55
  onTogglePreview,
57
56
  previewOpen,
58
57
  onSave,
58
+ onDiscard,
59
59
  packageType,
60
60
  isSaving,
61
61
  }: {
62
62
  pkg?: Package | null
63
63
  circuitJson?: AnyCircuitElement[] | null
64
64
  code: string
65
+ fsMap: Record<string, string>
65
66
  packageType?: string
66
67
  hasUnsavedChanges: boolean
67
68
  previewOpen: boolean
68
69
  onTogglePreview: () => void
69
70
  isSaving: boolean
70
71
  onSave: () => void
72
+ onDiscard?: () => void
71
73
  }) {
72
74
  const [, navigate] = useLocation()
73
75
  const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
@@ -80,7 +82,6 @@ export default function EditorNav({
80
82
  } = useUpdatePackageDescriptionDialog()
81
83
  const { Dialog: DeleteDialog, openDialog: openDeleteDialog } =
82
84
  useConfirmDeletePackageDialog()
83
- const { Dialog: FilesDialog, openDialog: openFilesDialog } = useFilesDialog()
84
85
  const { Dialog: ViewTsFilesDialog, openDialog: openViewTsFilesDialog } =
85
86
  useViewTsFilesDialog()
86
87
 
@@ -190,17 +191,14 @@ export default function EditorNav({
190
191
  [isLoggedIn, pkg, session?.github_username],
191
192
  )
192
193
 
193
- useEffect(() => {
194
- const handleKeyDown = (e: KeyboardEvent) => {
195
- if ((e.ctrlKey || e.metaKey) && e.key === "s") {
196
- e.preventDefault()
197
- if (!hasUnsavedChanges || !canSavePackage) return
198
- onSave()
199
- }
200
- }
201
- window.addEventListener("keydown", handleKeyDown)
202
- return () => window.removeEventListener("keydown", handleKeyDown)
203
- }, [onSave, hasUnsavedChanges, canSavePackage])
194
+ useHotkeyCombo(
195
+ "cmd+s",
196
+ () => {
197
+ if (!hasUnsavedChanges || !canSavePackage) return
198
+ onSave()
199
+ },
200
+ { target: window },
201
+ )
204
202
  return (
205
203
  <nav className="lg:flex w-screen items-center justify-between px-2 py-3 border-b border-gray-200 bg-white text-sm border-t">
206
204
  <div className="lg:flex items-center my-2 ">
@@ -311,6 +309,17 @@ export default function EditorNav({
311
309
  {pkg ? "unsaved changes" : "unsaved"}
312
310
  </div>
313
311
  )}
312
+ {hasUnsavedChanges && onDiscard && Boolean(pkg?.package_id) && (
313
+ <Button
314
+ variant="ghost"
315
+ size="sm"
316
+ className="h-6 px-2 text-xs text-red-600 hover:text-red-700 hover:bg-red-50"
317
+ onClick={onDiscard}
318
+ title="Discard all unsaved changes (Cmd+Shift+Z)"
319
+ >
320
+ <Undo2 className="mr-1 h-3 w-3" />
321
+ </Button>
322
+ )}
314
323
  </div>
315
324
  </div>
316
325
  <div className="flex items-center justify-end -space-x-1">
@@ -326,16 +335,19 @@ export default function EditorNav({
326
335
  Edit with AI
327
336
  </Button> */}
328
337
  <DownloadButtonAndMenu
329
- snippetUnscopedName={pkg?.unscoped_name}
338
+ offerMultipleImageFormats
339
+ unscopedName={pkg?.unscoped_name}
330
340
  circuitJson={circuitJson}
331
341
  className="flex"
342
+ desiredImageType={pkg?.default_view ?? "pcb"}
343
+ author={pkg?.owner_github_username ?? undefined}
332
344
  />
333
345
  <Button
334
346
  variant="ghost"
335
347
  size="sm"
336
348
  className="hidden md:flex px-2 text-xs"
337
349
  onClick={() => {
338
- const url = encodeTextToUrlHash(code, packageType)
350
+ const url = encodeFsMapToUrlHash(fsMap, packageType)
339
351
  navigator.clipboard.writeText(url)
340
352
  alert("URL copied to clipboard!")
341
353
  }}
@@ -359,13 +371,6 @@ export default function EditorNav({
359
371
  </Button>
360
372
  </DropdownMenuTrigger>
361
373
  <DropdownMenuContent>
362
- <DropdownMenuItem
363
- className="text-xs"
364
- onClick={() => openFilesDialog()}
365
- >
366
- <File className="mr-2 h-3 w-3" />
367
- View Files
368
- </DropdownMenuItem>
369
374
  <DropdownMenuItem
370
375
  className="text-xs"
371
376
  onClick={() => openupdateDescriptionDialog()}
@@ -378,7 +383,7 @@ export default function EditorNav({
378
383
  onClick={() => openViewTsFilesDialog()}
379
384
  >
380
385
  <File className="mr-2 h-3 w-3" />
381
- [Debug] View TS Files
386
+ View Files
382
387
  </DropdownMenuItem>
383
388
  <DropdownMenuSub>
384
389
  <DropdownMenuSubTrigger
@@ -432,7 +437,7 @@ export default function EditorNav({
432
437
  onClick={() => openDeleteDialog()}
433
438
  >
434
439
  <Trash2 className="mr-2 h-3 w-3" />
435
- Delete Snippet
440
+ Delete Package
436
441
  </DropdownMenuItem>
437
442
  <DropdownMenuItem className="text-xs text-gray-500" disabled>
438
443
  @tscircuit/core@{tscircuitCorePkg.version}
@@ -468,6 +473,15 @@ export default function EditorNav({
468
473
  </div>
469
474
  </DropdownMenuTrigger>
470
475
  <DropdownMenuContent>
476
+ {hasUnsavedChanges && onDiscard && (
477
+ <DropdownMenuItem
478
+ className="text-xs text-red-600"
479
+ onClick={onDiscard}
480
+ >
481
+ <Undo2 className="mr-1 h-3 w-3" />
482
+ Discard Changes
483
+ </DropdownMenuItem>
484
+ )}
471
485
  <DropdownMenuItem
472
486
  className="text-xs"
473
487
  onClick={() => {
@@ -540,7 +554,6 @@ export default function EditorNav({
540
554
  packageName={pkg?.unscoped_name ?? ""}
541
555
  packageOwner={pkg?.owner_github_username ?? ""}
542
556
  />
543
- <FilesDialog snippetId={pkg?.package_id ?? ""} />
544
557
  <ViewTsFilesDialog />
545
558
  </nav>
546
559
  )