@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.
Files changed (75) 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 +187 -206
  4. package/dist/bundle.js +207 -101
  5. package/fake-snippets-api/routes/api/package_releases/create.ts +1 -1
  6. package/fake-snippets-api/routes/api/proxy.ts +128 -0
  7. package/package.json +57 -50
  8. package/renovate.json +2 -1
  9. package/src/App.tsx +22 -3
  10. package/src/ContextProviders.tsx +2 -0
  11. package/src/build-watcher.ts +52 -0
  12. package/src/components/CmdKMenu.tsx +533 -197
  13. package/src/components/DownloadButtonAndMenu.tsx +104 -26
  14. package/src/components/FileSidebar.tsx +11 -1
  15. package/src/components/Header.tsx +5 -1
  16. package/src/components/Header2.tsx +7 -2
  17. package/src/components/HeaderLogin.tsx +1 -1
  18. package/src/components/PackageBuildsPage/LogContent.tsx +25 -22
  19. package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +6 -6
  20. package/src/components/PackageBuildsPage/build-preview-content.tsx +5 -5
  21. package/src/components/PackageBuildsPage/package-build-details-panel.tsx +15 -13
  22. package/src/components/PackageBuildsPage/package-build-header.tsx +17 -16
  23. package/src/components/PackageCard.tsx +66 -16
  24. package/src/components/PrefetchPageLink.tsx +66 -15
  25. package/src/components/SearchComponent.tsx +2 -2
  26. package/src/components/SuspenseRunFrame.tsx +14 -2
  27. package/src/components/ViewPackagePage/components/important-files-view.tsx +97 -22
  28. package/src/components/ViewPackagePage/components/main-content-header.tsx +27 -3
  29. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +2 -2
  30. package/src/components/ViewPackagePage/components/repo-page-content.tsx +49 -34
  31. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +2 -2
  32. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +20 -12
  33. package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +0 -7
  34. package/src/components/ViewPackagePage/utils/fuzz-search.ts +121 -0
  35. package/src/components/ViewPackagePage/utils/is-hidden-file.ts +4 -0
  36. package/src/components/ViewPackagePage/utils/is-package-file-important.ts +18 -5
  37. package/src/components/dialogs/confirm-delete-package-dialog.tsx +1 -1
  38. package/src/components/dialogs/confirm-discard-changes-dialog.tsx +73 -0
  39. package/src/components/dialogs/edit-package-details-dialog.tsx +2 -2
  40. package/src/components/dialogs/view-ts-files-dialog.tsx +478 -42
  41. package/src/components/package-port/CodeAndPreview.tsx +16 -0
  42. package/src/components/package-port/CodeEditor.tsx +113 -11
  43. package/src/components/package-port/CodeEditorHeader.tsx +39 -4
  44. package/src/components/package-port/EditorNav.tsx +41 -15
  45. package/src/components/package-port/GlobalFindReplace.tsx +681 -0
  46. package/src/components/package-port/QuickOpen.tsx +241 -0
  47. package/src/components/ui/dialog.tsx +1 -1
  48. package/src/components/ui/tree-view.tsx +1 -1
  49. package/src/global.d.ts +3 -0
  50. package/src/hooks/use-ai-review.ts +31 -0
  51. package/src/hooks/use-current-package-release.ts +5 -1
  52. package/src/hooks/use-download-zip.ts +50 -0
  53. package/src/hooks/use-hotkey.ts +116 -0
  54. package/src/hooks/use-package-by-package-id.ts +1 -0
  55. package/src/hooks/use-package-by-package-name.ts +1 -0
  56. package/src/hooks/use-package-files.ts +3 -0
  57. package/src/hooks/use-package-release.ts +5 -1
  58. package/src/hooks/use-package.ts +1 -0
  59. package/src/hooks/use-request-ai-review-mutation.ts +14 -6
  60. package/src/hooks/use-snippet.ts +1 -0
  61. package/src/hooks/useFileManagement.ts +26 -8
  62. package/src/hooks/usePackageFilesLoader.ts +3 -1
  63. package/src/index.css +11 -0
  64. package/src/lib/decodeUrlHashToFsMap.ts +17 -0
  65. package/src/lib/download-fns/download-circuit-png.ts +88 -0
  66. package/src/lib/download-fns/download-png-utils.ts +31 -0
  67. package/src/lib/encodeFsMapToUrlHash.ts +13 -0
  68. package/src/lib/populate-query-cache-with-ssr-data.ts +39 -38
  69. package/src/lib/ts-lib-cache.ts +47 -0
  70. package/src/lib/types.ts +2 -0
  71. package/src/main.tsx +7 -0
  72. package/src/pages/dashboard.tsx +8 -5
  73. package/src/pages/user-profile.tsx +1 -1
  74. package/src/pages/view-package.tsx +15 -7
  75. 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"
@@ -73,6 +74,9 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
73
74
  const { Dialog: NewPackageSaveDialog, openDialog: openNewPackageSaveDialog } =
74
75
  usePackageVisibilitySettingsDialog()
75
76
 
77
+ const { Dialog: DiscardChangesDialog, openDialog: openDiscardChangesDialog } =
78
+ useConfirmDiscardChangesDialog()
79
+
76
80
  const {
77
81
  savePackage,
78
82
  isSaving,
@@ -169,6 +173,15 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
169
173
  )
170
174
  }
171
175
 
176
+ const handleDiscardChanges = () => {
177
+ setLocalFiles([...initialFiles])
178
+ setState((prev) => ({ ...prev, lastSavedAt: Date.now() }))
179
+ toast({
180
+ title: "Changes discarded",
181
+ description: "All unsaved changes have been discarded.",
182
+ })
183
+ }
184
+
172
185
  if (urlParams.package_id && (!pkg || isLoading)) {
173
186
  return (
174
187
  <div className="flex items-center justify-center h-[60vh]">
@@ -187,9 +200,11 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
187
200
  pkg={pkg}
188
201
  packageType={packageType}
189
202
  code={String(currentFileCode)}
203
+ fsMap={fsMap}
190
204
  isSaving={isSaving}
191
205
  hasUnsavedChanges={hasUnsavedChanges}
192
206
  onSave={saveFiles}
207
+ onDiscard={() => openDiscardChangesDialog()}
193
208
  onTogglePreview={() =>
194
209
  setState((prev) => ({ ...prev, showPreview: !prev.showPreview }))
195
210
  }
@@ -254,6 +269,7 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
254
269
  )}
255
270
  </div>
256
271
  <NewPackageSaveDialog initialIsPrivate={false} onSave={savePackage} />
272
+ <DiscardChangesDialog onConfirm={handleDiscardChanges} />
257
273
  </div>
258
274
  )
259
275
  }