@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.
- package/api/generated-index.js +96 -14
- package/bun-tests/fake-snippets-api/routes/proxy.test.ts +42 -0
- package/bun.lock +196 -215
- package/dist/bundle.js +596 -370
- package/fake-snippets-api/routes/api/autocomplete/create_autocomplete.ts +134 -0
- package/fake-snippets-api/routes/api/proxy.ts +128 -0
- package/package.json +59 -48
- package/renovate.json +2 -1
- package/src/App.tsx +67 -3
- package/src/ContextProviders.tsx +2 -0
- package/src/build-watcher.ts +52 -0
- package/src/components/CircuitJsonImportDialog.tsx +1 -1
- 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 +19 -28
- 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 +17 -16
- package/src/components/package-port/CodeEditor.tsx +138 -17
- package/src/components/package-port/CodeEditorHeader.tsx +44 -4
- package/src/components/package-port/EditorNav.tsx +42 -29
- 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-code-completion-ai-api.ts +3 -3
- package/src/hooks/use-current-package-release.ts +5 -1
- package/src/hooks/use-delete-package.ts +6 -2
- 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 +28 -10
- 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/lib/utils/findTargetFile.ts +1 -1
- package/src/lib/utils/package-utils.ts +10 -0
- package/src/main.tsx +7 -0
- package/src/pages/dashboard.tsx +18 -5
- package/src/pages/view-package.tsx +15 -7
- package/src/types/package.ts +4 -0
- 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 {
|
|
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>
|
|
@@ -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
|
}
|