@tscircuit/fake-snippets 0.0.88 → 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 (69) 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 +206 -100
  5. package/fake-snippets-api/routes/api/proxy.ts +128 -0
  6. package/package.json +56 -47
  7. package/renovate.json +2 -1
  8. package/src/App.tsx +22 -3
  9. package/src/ContextProviders.tsx +2 -0
  10. package/src/build-watcher.ts +52 -0
  11. package/src/components/CmdKMenu.tsx +533 -197
  12. package/src/components/DownloadButtonAndMenu.tsx +104 -26
  13. package/src/components/FileSidebar.tsx +11 -1
  14. package/src/components/Header2.tsx +7 -2
  15. package/src/components/PackageBuildsPage/LogContent.tsx +25 -22
  16. package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +6 -6
  17. package/src/components/PackageBuildsPage/build-preview-content.tsx +5 -5
  18. package/src/components/PackageBuildsPage/package-build-details-panel.tsx +15 -13
  19. package/src/components/PackageBuildsPage/package-build-header.tsx +17 -16
  20. package/src/components/PackageCard.tsx +66 -16
  21. package/src/components/SearchComponent.tsx +2 -2
  22. package/src/components/SuspenseRunFrame.tsx +14 -2
  23. package/src/components/ViewPackagePage/components/important-files-view.tsx +90 -17
  24. package/src/components/ViewPackagePage/components/main-content-header.tsx +26 -2
  25. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +2 -2
  26. package/src/components/ViewPackagePage/components/repo-page-content.tsx +35 -30
  27. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +2 -2
  28. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +20 -12
  29. package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +0 -7
  30. package/src/components/ViewPackagePage/utils/fuzz-search.ts +121 -0
  31. package/src/components/ViewPackagePage/utils/is-hidden-file.ts +4 -0
  32. package/src/components/dialogs/confirm-delete-package-dialog.tsx +1 -1
  33. package/src/components/dialogs/confirm-discard-changes-dialog.tsx +73 -0
  34. package/src/components/dialogs/edit-package-details-dialog.tsx +2 -2
  35. package/src/components/dialogs/view-ts-files-dialog.tsx +478 -42
  36. package/src/components/package-port/CodeAndPreview.tsx +16 -0
  37. package/src/components/package-port/CodeEditor.tsx +113 -11
  38. package/src/components/package-port/CodeEditorHeader.tsx +39 -4
  39. package/src/components/package-port/EditorNav.tsx +41 -15
  40. package/src/components/package-port/GlobalFindReplace.tsx +681 -0
  41. package/src/components/package-port/QuickOpen.tsx +241 -0
  42. package/src/components/ui/dialog.tsx +1 -1
  43. package/src/components/ui/tree-view.tsx +1 -1
  44. package/src/global.d.ts +3 -0
  45. package/src/hooks/use-ai-review.ts +31 -0
  46. package/src/hooks/use-current-package-release.ts +5 -1
  47. package/src/hooks/use-download-zip.ts +50 -0
  48. package/src/hooks/use-hotkey.ts +116 -0
  49. package/src/hooks/use-package-by-package-id.ts +1 -0
  50. package/src/hooks/use-package-by-package-name.ts +1 -0
  51. package/src/hooks/use-package-files.ts +3 -0
  52. package/src/hooks/use-package-release.ts +5 -1
  53. package/src/hooks/use-package.ts +1 -0
  54. package/src/hooks/use-request-ai-review-mutation.ts +14 -6
  55. package/src/hooks/use-snippet.ts +1 -0
  56. package/src/hooks/useFileManagement.ts +26 -8
  57. package/src/hooks/usePackageFilesLoader.ts +3 -1
  58. package/src/index.css +11 -0
  59. package/src/lib/decodeUrlHashToFsMap.ts +17 -0
  60. package/src/lib/download-fns/download-circuit-png.ts +88 -0
  61. package/src/lib/download-fns/download-png-utils.ts +31 -0
  62. package/src/lib/encodeFsMapToUrlHash.ts +13 -0
  63. package/src/lib/populate-query-cache-with-ssr-data.ts +7 -0
  64. package/src/lib/ts-lib-cache.ts +47 -0
  65. package/src/lib/types.ts +2 -0
  66. package/src/main.tsx +7 -0
  67. package/src/pages/dashboard.tsx +8 -5
  68. package/src/pages/view-package.tsx +15 -7
  69. package/vite.config.ts +100 -1
@@ -0,0 +1,73 @@
1
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog"
2
+ import { Button } from "../ui/button"
3
+ import { createUseDialog } from "./create-use-dialog"
4
+ import { useState } from "react"
5
+
6
+ export const ConfirmDiscardChangesDialog = ({
7
+ open,
8
+ onOpenChange,
9
+ onConfirm,
10
+ }: {
11
+ open: boolean
12
+ onOpenChange: (open: boolean) => void
13
+ onConfirm: () => void
14
+ }) => {
15
+ const [isConfirming, setIsConfirming] = useState(false)
16
+
17
+ const handleDiscard = () => {
18
+ onConfirm()
19
+ onOpenChange(false)
20
+ setIsConfirming(false)
21
+ }
22
+
23
+ const handleCancel = () => {
24
+ onOpenChange(false)
25
+ setIsConfirming(false)
26
+ }
27
+
28
+ return (
29
+ <Dialog
30
+ open={open}
31
+ onOpenChange={(open) => {
32
+ onOpenChange(open)
33
+ if (!open) setIsConfirming(false)
34
+ }}
35
+ >
36
+ <DialogContent className="w-[90vw]">
37
+ <DialogHeader>
38
+ <DialogTitle>Discard Changes</DialogTitle>
39
+ </DialogHeader>
40
+ <p>Are you sure you want to discard all unsaved changes?</p>
41
+ <p className="text-red-600 font-medium">
42
+ This action cannot be undone.
43
+ </p>
44
+ <div className="flex justify-end space-x-2 mt-4">
45
+ <Button variant="outline" onClick={handleCancel}>
46
+ Cancel
47
+ </Button>
48
+ {!isConfirming ? (
49
+ <Button
50
+ variant="destructive"
51
+ className="bg-red-600 hover:bg-red-700"
52
+ onClick={() => setIsConfirming(true)}
53
+ >
54
+ Discard Changes
55
+ </Button>
56
+ ) : (
57
+ <Button
58
+ variant="destructive"
59
+ onClick={handleDiscard}
60
+ className="bg-red-600 hover:bg-red-700"
61
+ >
62
+ Yes, Discard All Changes
63
+ </Button>
64
+ )}
65
+ </div>
66
+ </DialogContent>
67
+ </Dialog>
68
+ )
69
+ }
70
+
71
+ export const useConfirmDiscardChangesDialog = createUseDialog(
72
+ ConfirmDiscardChangesDialog,
73
+ )
@@ -207,7 +207,7 @@ export const EditPackageDetailsDialog = ({
207
207
  return (
208
208
  <div>
209
209
  <Dialog open={showConfirmDelete} onOpenChange={setShowConfirmDelete}>
210
- <DialogContent className="max-w-md p-6 rounded-2xl shadow-lg">
210
+ <DialogContent className="w-[90vw] p-6 rounded-2xl shadow-lg">
211
211
  <DialogHeader>
212
212
  <DialogTitle className="text-left">Confirm Deletion</DialogTitle>
213
213
  <DialogDescription className="text-left">
@@ -255,7 +255,7 @@ export const EditPackageDetailsDialog = ({
255
255
  onChange={(e) =>
256
256
  setFormData((prev) => ({
257
257
  ...prev,
258
- unscopedPackageName: e.target.value,
258
+ unscopedPackageName: e.target.value.replace(/\s+/g, ""),
259
259
  }))
260
260
  }
261
261
  placeholder="Enter package name"
@@ -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>