@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.
- package/api/generated-index.js +96 -14
- package/bun-tests/fake-snippets-api/routes/proxy.test.ts +42 -0
- package/bun.lock +187 -206
- package/dist/bundle.js +206 -100
- package/fake-snippets-api/routes/api/proxy.ts +128 -0
- package/package.json +56 -47
- package/renovate.json +2 -1
- package/src/App.tsx +22 -3
- package/src/ContextProviders.tsx +2 -0
- package/src/build-watcher.ts +52 -0
- package/src/components/CmdKMenu.tsx +533 -197
- package/src/components/DownloadButtonAndMenu.tsx +104 -26
- package/src/components/FileSidebar.tsx +11 -1
- package/src/components/Header2.tsx +7 -2
- package/src/components/PackageBuildsPage/LogContent.tsx +25 -22
- package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +6 -6
- package/src/components/PackageBuildsPage/build-preview-content.tsx +5 -5
- package/src/components/PackageBuildsPage/package-build-details-panel.tsx +15 -13
- package/src/components/PackageBuildsPage/package-build-header.tsx +17 -16
- package/src/components/PackageCard.tsx +66 -16
- package/src/components/SearchComponent.tsx +2 -2
- package/src/components/SuspenseRunFrame.tsx +14 -2
- package/src/components/ViewPackagePage/components/important-files-view.tsx +90 -17
- package/src/components/ViewPackagePage/components/main-content-header.tsx +26 -2
- package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +2 -2
- package/src/components/ViewPackagePage/components/repo-page-content.tsx +35 -30
- package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +2 -2
- package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +20 -12
- package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +0 -7
- package/src/components/ViewPackagePage/utils/fuzz-search.ts +121 -0
- package/src/components/ViewPackagePage/utils/is-hidden-file.ts +4 -0
- package/src/components/dialogs/confirm-delete-package-dialog.tsx +1 -1
- package/src/components/dialogs/confirm-discard-changes-dialog.tsx +73 -0
- package/src/components/dialogs/edit-package-details-dialog.tsx +2 -2
- package/src/components/dialogs/view-ts-files-dialog.tsx +478 -42
- package/src/components/package-port/CodeAndPreview.tsx +16 -0
- package/src/components/package-port/CodeEditor.tsx +113 -11
- package/src/components/package-port/CodeEditorHeader.tsx +39 -4
- package/src/components/package-port/EditorNav.tsx +41 -15
- package/src/components/package-port/GlobalFindReplace.tsx +681 -0
- package/src/components/package-port/QuickOpen.tsx +241 -0
- package/src/components/ui/dialog.tsx +1 -1
- package/src/components/ui/tree-view.tsx +1 -1
- package/src/global.d.ts +3 -0
- package/src/hooks/use-ai-review.ts +31 -0
- package/src/hooks/use-current-package-release.ts +5 -1
- package/src/hooks/use-download-zip.ts +50 -0
- package/src/hooks/use-hotkey.ts +116 -0
- package/src/hooks/use-package-by-package-id.ts +1 -0
- package/src/hooks/use-package-by-package-name.ts +1 -0
- package/src/hooks/use-package-files.ts +3 -0
- package/src/hooks/use-package-release.ts +5 -1
- package/src/hooks/use-package.ts +1 -0
- package/src/hooks/use-request-ai-review-mutation.ts +14 -6
- package/src/hooks/use-snippet.ts +1 -0
- package/src/hooks/useFileManagement.ts +26 -8
- package/src/hooks/usePackageFilesLoader.ts +3 -1
- package/src/index.css +11 -0
- package/src/lib/decodeUrlHashToFsMap.ts +17 -0
- package/src/lib/download-fns/download-circuit-png.ts +88 -0
- package/src/lib/download-fns/download-png-utils.ts +31 -0
- package/src/lib/encodeFsMapToUrlHash.ts +13 -0
- package/src/lib/populate-query-cache-with-ssr-data.ts +7 -0
- package/src/lib/ts-lib-cache.ts +47 -0
- package/src/lib/types.ts +2 -0
- package/src/main.tsx +7 -0
- package/src/pages/dashboard.tsx +8 -5
- package/src/pages/view-package.tsx +15 -7
- 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="
|
|
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 {
|
|
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
|
-
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
</
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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>
|