@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,78 +1,514 @@
1
- import React, { useState, useEffect } from "react"
1
+ import React, { useState, useEffect, useMemo, useRef } from "react"
2
2
  import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog"
3
3
  import { ScrollArea } from "../ui/scroll-area"
4
4
  import { cn } from "@/lib/utils"
5
5
  import { createUseDialog } from "./create-use-dialog"
6
6
  import { Button } from "../ui/button"
7
- import { Download } from "lucide-react"
7
+ import { Input } from "../ui/input"
8
+ import { Badge } from "../ui/badge"
9
+ import {
10
+ Download,
11
+ Search,
12
+ File,
13
+ Folder,
14
+ Copy,
15
+ Check,
16
+ FileText,
17
+ Code2,
18
+ Menu,
19
+ } from "lucide-react"
8
20
  import JSZip from "jszip"
9
21
  import { saveAs } from "file-saver"
22
+ import { EditorView } from "codemirror"
23
+ import { EditorState } from "@codemirror/state"
24
+ import { basicSetup } from "@/lib/codemirror/basic-setup"
25
+ import { javascript } from "@codemirror/lang-javascript"
26
+ import { json } from "@codemirror/lang-json"
10
27
 
11
28
  interface ViewTsFilesDialogProps {
12
29
  open: boolean
13
30
  onOpenChange: (open: boolean) => void
14
31
  }
15
32
 
33
+ interface FileNode {
34
+ name: string
35
+ path: string
36
+ type: "file" | "folder"
37
+ children?: FileNode[]
38
+ content?: string
39
+ }
40
+
16
41
  export const ViewTsFilesDialog: React.FC<ViewTsFilesDialogProps> = ({
17
42
  open,
18
43
  onOpenChange,
19
44
  }) => {
20
45
  const [files, setFiles] = useState<Map<string, string>>(new Map())
21
46
  const [selectedFile, setSelectedFile] = useState<string | null>(null)
47
+ const [searchTerm, setSearchTerm] = useState("")
48
+ const [copiedFile, setCopiedFile] = useState<string | null>(null)
49
+ const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set())
50
+ const [sidebarOpen, setSidebarOpen] = useState(true)
51
+ const editorRef = useRef<HTMLDivElement>(null)
52
+ const viewRef = useRef<EditorView | null>(null)
53
+
54
+ const fileTree = useMemo(() => {
55
+ const tree: FileNode[] = []
56
+ const pathMap: Map<string, FileNode> = new Map()
57
+
58
+ Array.from(files.keys()).forEach((originalFilePath) => {
59
+ let filePath = originalFilePath
60
+ if (filePath.startsWith("/")) {
61
+ filePath = filePath.slice(1)
62
+ }
63
+ const parts = filePath.split("/")
64
+ let currentPath = ""
65
+
66
+ parts.forEach((part, index) => {
67
+ const isFile = index === parts.length - 1
68
+ currentPath = currentPath ? `${currentPath}/${part}` : part
69
+
70
+ if (!pathMap.has(currentPath)) {
71
+ const node: FileNode = {
72
+ name: part,
73
+ path: isFile ? originalFilePath : currentPath,
74
+ type: isFile ? "file" : "folder",
75
+ children: isFile ? undefined : [],
76
+ content: isFile ? files.get(originalFilePath) : undefined,
77
+ }
78
+
79
+ pathMap.set(currentPath, node)
80
+
81
+ if (index === 0) {
82
+ tree.push(node)
83
+ } else {
84
+ const parentPath = parts.slice(0, index).join("/")
85
+ const parent = pathMap.get(parentPath)
86
+ if (parent?.children) {
87
+ parent.children.push(node)
88
+ }
89
+ }
90
+ }
91
+ })
92
+ })
93
+
94
+ return tree
95
+ }, [files])
96
+
97
+ const filteredFiles = useMemo(() => {
98
+ if (!searchTerm) return Array.from(files.keys())
99
+ return Array.from(files.keys()).filter(
100
+ (path) =>
101
+ path.toLowerCase().includes(searchTerm.toLowerCase()) ||
102
+ files.get(path)?.toLowerCase().includes(searchTerm.toLowerCase()),
103
+ )
104
+ }, [files, searchTerm])
105
+
106
+ const getFileIcon = (filename: string) => {
107
+ const ext = filename.split(".").pop()?.toLowerCase()
108
+ switch (ext) {
109
+ case "ts":
110
+ case "tsx":
111
+ return <Code2 className="w-4 h-4 text-blue-500" />
112
+ case "json":
113
+ return <FileText className="w-4 h-4 text-yellow-500" />
114
+ default:
115
+ return <File className="w-4 h-4 text-gray-500" />
116
+ }
117
+ }
118
+
119
+ const fileStats = useMemo(() => {
120
+ const stats = {
121
+ total: files.size,
122
+ ts: 0,
123
+ tsx: 0,
124
+ json: 0,
125
+ other: 0,
126
+ totalSize: 0,
127
+ }
128
+
129
+ Array.from(files.entries()).forEach(([path, content]) => {
130
+ const ext = path.split(".").pop()?.toLowerCase()
131
+ stats.totalSize += content.length
132
+
133
+ switch (ext) {
134
+ case "ts":
135
+ stats.ts++
136
+ break
137
+ case "tsx":
138
+ stats.tsx++
139
+ break
140
+ case "json":
141
+ stats.json++
142
+ break
143
+ default:
144
+ stats.other++
145
+ break
146
+ }
147
+ })
148
+
149
+ return stats
150
+ }, [files])
151
+
152
+ useEffect(() => {
153
+ if (!editorRef.current || !selectedFile) return
154
+
155
+ const content = files.get(selectedFile) || ""
156
+ const isJson = selectedFile.endsWith(".json")
157
+
158
+ if (viewRef.current) {
159
+ viewRef.current.destroy()
160
+ }
161
+
162
+ const state = EditorState.create({
163
+ doc: content,
164
+ extensions: [
165
+ basicSetup,
166
+ isJson ? json() : javascript({ typescript: true, jsx: true }),
167
+ EditorState.readOnly.of(true),
168
+ EditorView.theme({
169
+ "&": {
170
+ height: "100%",
171
+ fontSize: "14px",
172
+ },
173
+ ".cm-content": {
174
+ padding: "16px",
175
+ minHeight: "100%",
176
+ },
177
+ ".cm-focused": {
178
+ outline: "none",
179
+ },
180
+ ".cm-editor": {
181
+ height: "100%",
182
+ },
183
+ ".cm-scroller": {
184
+ fontFamily:
185
+ "ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace",
186
+ },
187
+ }),
188
+ ],
189
+ })
190
+
191
+ viewRef.current = new EditorView({
192
+ state,
193
+ parent: editorRef.current,
194
+ })
195
+
196
+ return () => {
197
+ if (viewRef.current) {
198
+ viewRef.current.destroy()
199
+ viewRef.current = null
200
+ }
201
+ }
202
+ }, [selectedFile, files])
22
203
 
23
204
  useEffect(() => {
24
205
  if (open && window.__DEBUG_CODE_EDITOR_FS_MAP) {
25
206
  setFiles(window.__DEBUG_CODE_EDITOR_FS_MAP)
207
+
26
208
  if (window.__DEBUG_CODE_EDITOR_FS_MAP.size > 0) {
27
- setSelectedFile(Array.from(window.__DEBUG_CODE_EDITOR_FS_MAP.keys())[0])
209
+ const firstFile = Array.from(
210
+ window.__DEBUG_CODE_EDITOR_FS_MAP.keys(),
211
+ )[0]
212
+ setSelectedFile(firstFile)
213
+
214
+ let normalizedPath = firstFile
215
+ if (normalizedPath.startsWith("/")) {
216
+ normalizedPath = normalizedPath.slice(1)
217
+ }
218
+ const pathParts = normalizedPath.split("/")
219
+ const foldersToExpand = new Set<string>()
220
+ let currentPath = ""
221
+ pathParts.slice(0, -1).forEach((part) => {
222
+ currentPath = currentPath ? `${currentPath}/${part}` : part
223
+ foldersToExpand.add(currentPath)
224
+ })
225
+ setExpandedFolders(foldersToExpand)
28
226
  }
29
227
  }
30
228
  }, [open])
31
229
 
230
+ useEffect(() => {
231
+ const handleResize = () => {
232
+ if (window.innerWidth < 768 && selectedFile) {
233
+ setSidebarOpen(false)
234
+ } else if (window.innerWidth >= 768) {
235
+ setSidebarOpen(true)
236
+ }
237
+ }
238
+
239
+ handleResize()
240
+ window.addEventListener("resize", handleResize)
241
+ return () => window.removeEventListener("resize", handleResize)
242
+ }, [selectedFile])
243
+
244
+ const copyToClipboard = async (content: string, filename: string) => {
245
+ try {
246
+ await navigator.clipboard.writeText(content)
247
+ setCopiedFile(filename)
248
+ setTimeout(() => setCopiedFile(null), 2000)
249
+ } catch (err) {
250
+ console.error("Failed to copy:", err)
251
+ }
252
+ }
253
+
254
+ const downloadAllFiles = async () => {
255
+ const zip = new JSZip()
256
+
257
+ files.forEach((content, filename) => {
258
+ let normalizedFilename = filename
259
+ if (normalizedFilename.startsWith("/")) {
260
+ normalizedFilename = normalizedFilename.slice(1)
261
+ }
262
+
263
+ zip.file(normalizedFilename, content)
264
+ })
265
+
266
+ try {
267
+ const blob = await zip.generateAsync({ type: "blob" })
268
+ saveAs(blob, "typescript-files.zip")
269
+ } catch (error) {
270
+ console.error("Error generating ZIP:", error)
271
+ }
272
+ }
273
+
274
+ const toggleFolder = (folderPath: string) => {
275
+ setExpandedFolders((prev) => {
276
+ const newSet = new Set(prev)
277
+ if (newSet.has(folderPath)) {
278
+ newSet.delete(folderPath)
279
+ } else {
280
+ newSet.add(folderPath)
281
+ }
282
+ return newSet
283
+ })
284
+ }
285
+
286
+ const selectFile = (filePath: string) => {
287
+ setSelectedFile(filePath)
288
+ if (window.innerWidth < 768) {
289
+ setSidebarOpen(false)
290
+ }
291
+ }
292
+
293
+ const renderFileTree = (nodes: FileNode[], level = 0) => {
294
+ return nodes.map((node) => (
295
+ <div key={node.path}>
296
+ <div
297
+ className={cn(
298
+ "flex items-center gap-2 px-2 py-1.5 cursor-pointer hover:bg-gray-100 rounded-sm transition-colors",
299
+ selectedFile === node.path &&
300
+ "bg-blue-50 border-l-2 border-l-blue-500",
301
+ level > 0 && "ml-4",
302
+ )}
303
+ style={{ paddingLeft: `${8 + level * 16}px` }}
304
+ onClick={() => {
305
+ if (node.type === "folder") {
306
+ toggleFolder(node.path)
307
+ } else {
308
+ selectFile(node.path)
309
+ }
310
+ }}
311
+ >
312
+ {node.type === "folder" ? (
313
+ <>
314
+ <Folder
315
+ className={cn(
316
+ "w-4 h-4 text-gray-600",
317
+ expandedFolders.has(node.path) && "text-blue-600",
318
+ )}
319
+ />
320
+ <span className="text-sm font-medium text-gray-700">
321
+ {node.name}
322
+ </span>
323
+ <Badge variant="secondary" className="ml-auto text-xs">
324
+ {node.children?.length || 0}
325
+ </Badge>
326
+ </>
327
+ ) : (
328
+ <>
329
+ {getFileIcon(node.name)}
330
+ <span className="text-sm text-gray-800 flex-1 truncate">
331
+ {node.name}
332
+ </span>
333
+ <span className="text-xs text-gray-500 ml-auto">
334
+ {node.content
335
+ ? `${Math.round(node.content.length / 1024)}KB`
336
+ : "0KB"}
337
+ </span>
338
+ </>
339
+ )}
340
+ </div>
341
+ {node.type === "folder" &&
342
+ expandedFolders.has(node.path) &&
343
+ node.children && (
344
+ <div>{renderFileTree(node.children, level + 1)}</div>
345
+ )}
346
+ </div>
347
+ ))
348
+ }
349
+
32
350
  return (
33
351
  <Dialog open={open} onOpenChange={onOpenChange}>
34
- <DialogContent className="max-w-4xl w-full h-[80vh] flex flex-col">
35
- <DialogHeader className="flex flex-row items-center justify-between">
36
- <DialogTitle>TypeScript Files</DialogTitle>
37
- <Button
38
- variant="outline"
39
- size="sm"
40
- onClick={async () => {
41
- const zip = new JSZip()
42
- files.forEach((content, filename) => {
43
- zip.file(filename, content)
44
- })
45
- const blob = await zip.generateAsync({ type: "blob" })
46
- saveAs(blob, "typescript-files.zip")
47
- }}
48
- >
49
- <Download className="w-4 h-4 mr-2" />
50
- Download All
51
- </Button>
352
+ <DialogContent
353
+ className={cn(
354
+ "flex !w-full flex-col transition-all duration-300",
355
+ "!max-w-6xl !w-[80vw] h-[85vh] max-h-[90vh]",
356
+ )}
357
+ >
358
+ <DialogHeader className="flex flex-row items-center justify-between space-y-0 pb-4 border-b">
359
+ <div className="flex items-center gap-4">
360
+ <DialogTitle className="text-xl font-semibold">
361
+ TypeScript Files
362
+ </DialogTitle>
363
+ <div className="hidden sm:flex items-center gap-2 text-sm text-gray-600">
364
+ <Badge variant="outline">{fileStats.total} files</Badge>
365
+ <Badge variant="outline">
366
+ {Math.round(fileStats.totalSize / 1024)}KB total
367
+ </Badge>
368
+ </div>
369
+ </div>
370
+ <div className="flex items-center gap-2">
371
+ <Button
372
+ variant="outline"
373
+ size="sm"
374
+ onClick={() => setSidebarOpen(!sidebarOpen)}
375
+ className="md:hidden"
376
+ >
377
+ <Menu className="w-4 h-4" />
378
+ </Button>
379
+ <Button variant="outline" size="sm" onClick={downloadAllFiles}>
380
+ <Download className="w-4 h-4 sm:mr-2" />
381
+ <span className="hidden sm:inline">Download All</span>
382
+ </Button>
383
+ </div>
52
384
  </DialogHeader>
53
- <div className="flex flex-grow overflow-hidden">
54
- <div className="w-1/4 border-r">
55
- <ScrollArea className="h-full">
56
- {Array.from(files.keys()).map((filePath) => (
57
- <div
58
- key={filePath}
59
- className={cn(
60
- "px-4 py-2 cursor-pointer hover:bg-gray-100 text-xs break-all",
61
- selectedFile === filePath && "bg-gray-200",
62
- )}
63
- onClick={() => setSelectedFile(filePath)}
64
- >
65
- {filePath}
66
- </div>
67
- ))}
385
+
386
+ <div className="flex flex-1 overflow-hidden gap-4">
387
+ <div
388
+ className={cn(
389
+ "flex flex-col border-r bg-gray-50/50 transition-all duration-200",
390
+ sidebarOpen ? "w-80 md:w-80 sm:w-64" : "w-0 overflow-hidden",
391
+ "md:relative absolute md:z-0 z-10 md:bg-gray-50/50 bg-white md:shadow-none shadow-lg",
392
+ )}
393
+ >
394
+ <div className="p-3 border-b">
395
+ <div className="relative">
396
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
397
+ <Input
398
+ placeholder="Search files..."
399
+ value={searchTerm}
400
+ onChange={(e) => setSearchTerm(e.target.value)}
401
+ className="pl-10 text-sm"
402
+ />
403
+ </div>
404
+ </div>
405
+
406
+ <div className="p-3 border-b">
407
+ <div className="flex flex-wrap gap-1">
408
+ {fileStats.ts > 0 && (
409
+ <Badge variant="secondary">{fileStats.ts} .ts</Badge>
410
+ )}
411
+ {fileStats.tsx > 0 && (
412
+ <Badge variant="secondary">{fileStats.tsx} .tsx</Badge>
413
+ )}
414
+ {fileStats.json > 0 && (
415
+ <Badge variant="secondary">{fileStats.json} .json</Badge>
416
+ )}
417
+ {fileStats.other > 0 && (
418
+ <Badge variant="secondary">{fileStats.other} other</Badge>
419
+ )}
420
+ </div>
421
+ </div>
422
+
423
+ <ScrollArea className="flex-1">
424
+ <div className="p-2">
425
+ {searchTerm ? (
426
+ <div className="space-y-1">
427
+ {filteredFiles.map((filePath) => (
428
+ <div
429
+ key={filePath}
430
+ className={cn(
431
+ "flex items-center gap-2 px-2 py-1.5 cursor-pointer hover:bg-gray-100 rounded-sm transition-colors",
432
+ selectedFile === filePath &&
433
+ "bg-blue-50 border-l-2 border-l-blue-500",
434
+ )}
435
+ onClick={() => selectFile(filePath)}
436
+ >
437
+ {getFileIcon(filePath)}
438
+ <span className="text-sm text-gray-800 flex-1 truncate">
439
+ {filePath}
440
+ </span>
441
+ </div>
442
+ ))}
443
+ </div>
444
+ ) : (
445
+ <div>{renderFileTree(fileTree)}</div>
446
+ )}
447
+ </div>
68
448
  </ScrollArea>
69
449
  </div>
70
- <div className="w-3/4 overflow-hidden">
71
- <ScrollArea className="h-full">
72
- <pre className="p-4 text-xs whitespace-pre-wrap">
73
- {selectedFile ? files.get(selectedFile) : "Select a file"}
74
- </pre>
75
- </ScrollArea>
450
+
451
+ <div className="flex-1 flex flex-col overflow-hidden">
452
+ {selectedFile ? (
453
+ <>
454
+ <div className="flex items-center justify-between p-3 border-b bg-white">
455
+ <div className="flex items-center gap-2 flex-1 min-w-0">
456
+ {getFileIcon(selectedFile)}
457
+ <span className="font-medium text-gray-900 truncate">
458
+ {selectedFile}
459
+ </span>
460
+ <Badge variant="outline" className="text-xs shrink-0">
461
+ {files.get(selectedFile)?.length || 0} chars
462
+ </Badge>
463
+ </div>
464
+ <Button
465
+ variant="ghost"
466
+ size="sm"
467
+ onClick={() =>
468
+ copyToClipboard(
469
+ files.get(selectedFile) || "",
470
+ selectedFile,
471
+ )
472
+ }
473
+ className="shrink-0"
474
+ >
475
+ {copiedFile === selectedFile ? (
476
+ <>
477
+ <Check className="w-4 h-4 sm:mr-2 text-green-600" />
478
+ <span className="hidden sm:inline">Copied!</span>
479
+ </>
480
+ ) : (
481
+ <>
482
+ <Copy className="w-4 h-4 sm:mr-2" />
483
+ <span className="hidden sm:inline">Copy</span>
484
+ </>
485
+ )}
486
+ </Button>
487
+ </div>
488
+
489
+ <div className="flex-1 overflow-hidden">
490
+ <div ref={editorRef} className="h-full" />
491
+ </div>
492
+ </>
493
+ ) : (
494
+ <div className="flex-1 flex items-center justify-center text-gray-500">
495
+ <div className="text-center">
496
+ <FileText className="w-16 h-16 mx-auto mb-4 text-gray-300" />
497
+ <p className="text-lg font-medium">Select a file to view</p>
498
+ <p className="text-sm">
499
+ Choose from {files.size} available files
500
+ </p>
501
+ <Button
502
+ variant="outline"
503
+ className="mt-4 md:hidden"
504
+ onClick={() => setSidebarOpen(true)}
505
+ >
506
+ <Menu className="w-4 h-4 mr-2" />
507
+ Show Files
508
+ </Button>
509
+ </div>
510
+ </div>
511
+ )}
76
512
  </div>
77
513
  </div>
78
514
  </DialogContent>
@@ -1,5 +1,6 @@
1
1
  import { CodeEditor } from "@/components/package-port/CodeEditor"
2
2
  import { usePackageVisibilitySettingsDialog } from "@/components/dialogs/package-visibility-settings-dialog"
3
+ import { useConfirmDiscardChangesDialog } from "@/components/dialogs/confirm-discard-changes-dialog"
3
4
  import { useToast } from "@/hooks/use-toast"
4
5
  import { useUrlParams } from "@/hooks/use-url-params"
5
6
  import useWarnUserOnPageChange from "@/hooks/use-warn-user-on-page-change"
@@ -14,6 +15,7 @@ import { applyEditEventsToManualEditsFile } from "@tscircuit/core"
14
15
  import { toastManualEditConflicts } from "@/lib/utils/toastManualEditConflicts"
15
16
  import { ManualEditEvent } from "@tscircuit/props"
16
17
  import { useFileManagement } from "@/hooks/useFileManagement"
18
+ import { DEFAULT_CODE } from "@/lib/utils/package-utils"
17
19
 
18
20
  interface Props {
19
21
  pkg?: Package
@@ -24,11 +26,6 @@ interface Props {
24
26
  projectUrl?: string
25
27
  }
26
28
 
27
- export interface PackageFile {
28
- path: string
29
- content: string
30
- }
31
-
32
29
  export interface CodeAndPreviewState {
33
30
  showPreview: boolean
34
31
  fullScreen: boolean
@@ -39,17 +36,6 @@ export interface CodeAndPreviewState {
39
36
  defaultComponentFile?: string
40
37
  }
41
38
 
42
- export const DEFAULT_CODE = `
43
- export default () => (
44
- <board width="10mm" height="10mm">
45
- {/* write your code here! */}
46
- </board>
47
- )
48
- `.trim()
49
-
50
- export const generateRandomPackageName = () =>
51
- `untitled-package-${Math.floor(Math.random() * 900) + 100}`
52
-
53
39
  export function CodeAndPreview({ pkg, projectUrl }: Props) {
54
40
  const { toast } = useToast()
55
41
  const urlParams = useUrlParams()
@@ -73,6 +59,9 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
73
59
  const { Dialog: NewPackageSaveDialog, openDialog: openNewPackageSaveDialog } =
74
60
  usePackageVisibilitySettingsDialog()
75
61
 
62
+ const { Dialog: DiscardChangesDialog, openDialog: openDiscardChangesDialog } =
63
+ useConfirmDiscardChangesDialog()
64
+
76
65
  const {
77
66
  savePackage,
78
67
  isSaving,
@@ -169,6 +158,15 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
169
158
  )
170
159
  }
171
160
 
161
+ const handleDiscardChanges = () => {
162
+ setLocalFiles([...initialFiles])
163
+ setState((prev) => ({ ...prev, lastSavedAt: Date.now() }))
164
+ toast({
165
+ title: "Changes discarded",
166
+ description: "All unsaved changes have been discarded.",
167
+ })
168
+ }
169
+
172
170
  if (urlParams.package_id && (!pkg || isLoading)) {
173
171
  return (
174
172
  <div className="flex items-center justify-center h-[60vh]">
@@ -187,9 +185,11 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
187
185
  pkg={pkg}
188
186
  packageType={packageType}
189
187
  code={String(currentFileCode)}
188
+ fsMap={fsMap}
190
189
  isSaving={isSaving}
191
190
  hasUnsavedChanges={hasUnsavedChanges}
192
191
  onSave={saveFiles}
192
+ onDiscard={() => openDiscardChangesDialog()}
193
193
  onTogglePreview={() =>
194
194
  setState((prev) => ({ ...prev, showPreview: !prev.showPreview }))
195
195
  }
@@ -254,6 +254,7 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
254
254
  )}
255
255
  </div>
256
256
  <NewPackageSaveDialog initialIsPrivate={false} onSave={savePackage} />
257
+ <DiscardChangesDialog onConfirm={handleDiscardChanges} />
257
258
  </div>
258
259
  )
259
260
  }