@tscircuit/fake-snippets 0.0.108 → 0.0.109

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 (57) hide show
  1. package/bun.lock +62 -19
  2. package/dist/bundle.js +3 -2
  3. package/dist/index.d.ts +5 -0
  4. package/dist/index.js +2 -1
  5. package/dist/schema.d.ts +8 -0
  6. package/dist/schema.js +2 -1
  7. package/fake-snippets-api/lib/db/schema.ts +1 -0
  8. package/package.json +7 -8
  9. package/src/App.tsx +0 -2
  10. package/src/components/DownloadButtonAndMenu.tsx +133 -35
  11. package/src/components/FileSidebar.tsx +31 -34
  12. package/src/components/Footer.tsx +0 -1
  13. package/src/components/HeaderLogin.tsx +1 -1
  14. package/src/components/HiddenFilesDropdown.tsx +0 -2
  15. package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +0 -2
  16. package/src/components/PackageBuildsPage/build-preview-content.tsx +34 -5
  17. package/src/components/PackageCard.tsx +0 -1
  18. package/src/components/ViewPackagePage/components/ShikiCodeViewer.tsx +20 -11
  19. package/src/components/ViewPackagePage/components/important-files-view.tsx +75 -59
  20. package/src/components/ViewPackagePage/components/main-content-header.tsx +4 -4
  21. package/src/components/ViewPackagePage/components/main-content-view-selector.tsx +0 -1
  22. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +0 -1
  23. package/src/components/ViewPackagePage/components/package-header.tsx +14 -17
  24. package/src/components/ViewPackagePage/components/preview-image-squares.tsx +0 -1
  25. package/src/components/ViewPackagePage/components/repo-page-content.tsx +21 -20
  26. package/src/components/ViewPackagePage/components/sidebar.tsx +0 -2
  27. package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +18 -17
  28. package/src/components/ViewPackagePage/components/theme-toggle.tsx +0 -2
  29. package/src/components/ViewPackagePage/hooks/use-toast.tsx +0 -1
  30. package/src/components/package-port/CodeAndPreview.tsx +23 -40
  31. package/src/components/package-port/CodeEditor.tsx +24 -1
  32. package/src/components/package-port/CodeEditorHeader.tsx +5 -2
  33. package/src/components/preview/PackageReleasesDashboard.tsx +30 -25
  34. package/src/hooks/use-current-package-id.ts +5 -30
  35. package/src/hooks/use-current-package-info.ts +29 -5
  36. package/src/hooks/use-global-store.ts +1 -1
  37. package/src/hooks/useFileManagement.ts +153 -34
  38. package/src/hooks/useOptimizedPackageFilesLoader.ts +149 -0
  39. package/src/hooks/useUpdatePackageFilesMutation.ts +2 -0
  40. package/src/lib/download-fns/download-circuit-png.ts +11 -3
  41. package/src/lib/download-fns/download-gltf-from-circuit-json.ts +44 -0
  42. package/src/lib/utils/isComponentExported.ts +9 -0
  43. package/src/pages/authorize.tsx +0 -2
  44. package/src/pages/landing.tsx +0 -1
  45. package/src/pages/preview-release.tsx +14 -4
  46. package/src/pages/view-package.tsx +14 -13
  47. package/src/components/Footer2.tsx +0 -100
  48. package/src/components/ShippingInformationForm.tsx +0 -423
  49. package/src/components/StaticViewSnippetHeader.tsx +0 -70
  50. package/src/components/ViewPackagePage/components/file-explorer.tsx +0 -67
  51. package/src/components/ViewPackagePage/components/readme-view.tsx +0 -58
  52. package/src/components/ViewPackagePage/components/repo-header-button.tsx +0 -36
  53. package/src/components/ViewPackagePage/components/repo-header.tsx +0 -4
  54. package/src/components/ViewPackagePage/components/sidebar-contributors-section.tsx +0 -31
  55. package/src/components/ViewSnippetHeader.tsx +0 -181
  56. package/src/components/ui/input-otp.tsx +0 -69
  57. package/src/pages/settings.tsx +0 -25
@@ -6,7 +6,7 @@ import { Package } from "fake-snippets-api/lib/db/schema"
6
6
  import { usePackageFiles } from "./use-package-files"
7
7
  import { decodeUrlHashToText } from "@/lib/decodeUrlHashToText"
8
8
  import { decodeUrlHashToFsMap } from "@/lib/decodeUrlHashToFsMap"
9
- import { usePackageFilesLoader } from "./usePackageFilesLoader"
9
+ import { useOptimizedPackageFilesLoader } from "./useOptimizedPackageFilesLoader"
10
10
  import { useGlobalStore } from "./use-global-store"
11
11
  import { useToast } from "@/components/ViewPackagePage/hooks/use-toast"
12
12
  import { useUpdatePackageFilesMutation } from "./useUpdatePackageFilesMutation"
@@ -15,6 +15,7 @@ import { useCreatePackageMutation } from "./use-create-package-mutation"
15
15
  import { findTargetFile } from "@/lib/utils/findTargetFile"
16
16
  import { encodeFsMapToUrlHash } from "@/lib/encodeFsMapToUrlHash"
17
17
  import { isHiddenFile } from "@/components/ViewPackagePage/utils/is-hidden-file"
18
+ import { isComponentExported } from "@/lib/utils/isComponentExported"
18
19
 
19
20
  export interface ICreateFileProps {
20
21
  newFileName: string
@@ -47,27 +48,37 @@ export interface IRenameFileResult {
47
48
  export function useFileManagement({
48
49
  templateCode,
49
50
  currentPackage,
50
- fileChoosen,
51
51
  openNewPackageSaveDialog,
52
52
  updateLastUpdated,
53
+ urlParams,
53
54
  }: {
54
55
  templateCode?: string
55
56
  currentPackage?: Package
56
- fileChoosen: string | null
57
57
  openNewPackageSaveDialog: () => void
58
+ urlParams: Record<string, string>
58
59
  updateLastUpdated: () => void
59
60
  }) {
61
+ const fileChosen = urlParams.file_path ?? null
60
62
  const [localFiles, setLocalFiles] = useState<PackageFile[]>([])
61
63
  const [initialFiles, setInitialFiles] = useState<PackageFile[]>([])
62
64
  const [currentFile, setCurrentFile] = useState<string | null>(null)
63
65
  const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
64
- const loggedInUser = useGlobalStore((s) => s.session)
65
66
  const { toast } = useToast()
66
67
  const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
67
68
  const {
68
- data: packageFilesWithContent,
69
- isLoading: isLoadingPackageFilesWithContent,
70
- } = usePackageFilesLoader(currentPackage)
69
+ priorityFile,
70
+ allFiles: packageFilesWithContent,
71
+ isPriorityLoading,
72
+ areAllFilesLoading: isLoadingPackageFilesWithContent,
73
+ totalFilesCount,
74
+ loadedFilesCount,
75
+ isPriorityFileFetched,
76
+ } = useOptimizedPackageFilesLoader(
77
+ currentPackage,
78
+ fileChosen,
79
+ urlParams.package_id,
80
+ )
81
+
71
82
  const { data: packageFilesMeta, isLoading: isLoadingPackageFiles } =
72
83
  usePackageFiles(currentPackage?.latest_package_release_id)
73
84
  const initialCodeContent = useMemo(() => {
@@ -127,7 +138,7 @@ export function useFileManagement({
127
138
  })
128
139
 
129
140
  useEffect(() => {
130
- if (!currentPackage || isLoadingPackageFilesWithContent) {
141
+ if (!currentPackage || isPriorityLoading) {
131
142
  const decodedFsMap = decodeUrlHashToFsMap(window.location.toString())
132
143
 
133
144
  if (decodedFsMap && Object.keys(decodedFsMap).length > 0) {
@@ -137,38 +148,114 @@ export function useFileManagement({
137
148
  content: String(content),
138
149
  }),
139
150
  )
140
- const targetFile = findTargetFile(filesFromUrl, fileChoosen)
151
+ const targetFile = findTargetFile(filesFromUrl, fileChosen)
141
152
  setLocalFiles(filesFromUrl)
142
153
  setInitialFiles([])
143
154
  setCurrentFile(targetFile?.path || filesFromUrl[0]?.path || null)
144
155
  return
145
156
  }
146
157
 
147
- setLocalFiles([
148
- {
149
- path: "index.tsx",
150
- content: initialCodeContent || "",
151
- },
152
- ])
153
- setInitialFiles([])
154
- setCurrentFile("index.tsx")
158
+ if (!urlParams.package_id) {
159
+ setLocalFiles([
160
+ {
161
+ path: "index.tsx",
162
+ content: initialCodeContent || "",
163
+ },
164
+ ])
165
+ setInitialFiles([])
166
+ setCurrentFile("index.tsx")
167
+ }
155
168
  return
156
- } else {
157
- const targetFile = findTargetFile(
158
- packageFilesWithContent || [],
159
- fileChoosen,
160
- )
161
- setLocalFiles(packageFilesWithContent || [])
162
- setInitialFiles(packageFilesWithContent || [])
163
- setCurrentFile(targetFile?.path || null)
164
169
  }
165
- }, [currentPackage, isLoadingPackageFilesWithContent])
170
+ }, [currentPackage, isPriorityLoading])
171
+
172
+ useEffect(() => {
173
+ if (priorityFile && !isPriorityLoading && currentPackage) {
174
+ setLocalFiles((prev) => {
175
+ const existingIndex = prev.findIndex(
176
+ (f) => f.path === priorityFile.path,
177
+ )
178
+ if (existingIndex >= 0) {
179
+ const updated = [...prev]
180
+ updated[existingIndex] = priorityFile
181
+ return updated
182
+ }
183
+ return [...prev, priorityFile]
184
+ })
185
+
186
+ setCurrentFile((prevCurrentFile) => {
187
+ if (fileChosen && priorityFile.path === fileChosen) {
188
+ return priorityFile.path
189
+ } else if (!prevCurrentFile) {
190
+ return priorityFile.path
191
+ } else {
192
+ // If priority file is index.tsx, always update to it
193
+ const isPriorityFileBetter = priorityFile.path === "index.tsx"
194
+ if (isPriorityFileBetter) {
195
+ return priorityFile.path
196
+ }
197
+ }
198
+ return prevCurrentFile
199
+ })
200
+ }
201
+ }, [priorityFile, isPriorityLoading, currentPackage, fileChosen])
202
+
203
+ useEffect(() => {
204
+ if (packageFilesWithContent.length > 0 && currentPackage) {
205
+ setLocalFiles(packageFilesWithContent)
206
+ setInitialFiles(packageFilesWithContent)
207
+
208
+ if (fileChosen) {
209
+ const targetFile =
210
+ packageFilesWithContent.find((f) => f.path === fileChosen) ||
211
+ findTargetFile(packageFilesWithContent, fileChosen)
212
+ if (targetFile) {
213
+ setCurrentFile((prevCurrentFile) => {
214
+ return targetFile.path !== prevCurrentFile
215
+ ? targetFile.path
216
+ : prevCurrentFile
217
+ })
218
+ }
219
+ } else {
220
+ setCurrentFile((prevCurrentFile) => {
221
+ if (!prevCurrentFile) {
222
+ // Wait for priority file to load before making selection to avoid flicker
223
+ // Only select if we have a good candidate (tsx/ts file) or priority file isn't loading
224
+ const targetFile = findTargetFile(
225
+ packageFilesWithContent,
226
+ fileChosen,
227
+ )
228
+ // Only consider it a "good enough" candidate if it's index.tsx
229
+ // Otherwise, wait for the actual priority file (index.tsx) to load
230
+ const isTopPriorityFile =
231
+ targetFile && targetFile.path === "index.tsx"
232
+ const shouldWaitForPriority =
233
+ isPriorityLoading && !isTopPriorityFile
234
+
235
+ if (shouldWaitForPriority) {
236
+ return prevCurrentFile // Keep null to avoid flicker
237
+ }
238
+
239
+ return targetFile ? targetFile.path : prevCurrentFile
240
+ }
241
+ return prevCurrentFile
242
+ })
243
+ }
244
+ }
245
+ }, [
246
+ packageFilesWithContent.length,
247
+ currentPackage,
248
+ fileChosen,
249
+ isPriorityLoading,
250
+ ])
166
251
 
167
252
  const isLoading = useMemo(() => {
168
- return (
169
- isLoadingPackageFilesWithContent || isLoadingPackageFiles || !localFiles
170
- )
171
- }, [isLoadingPackageFilesWithContent, localFiles, isLoadingPackageFiles])
253
+ return isPriorityLoading || isLoadingPackageFiles
254
+ }, [isPriorityLoading, isLoadingPackageFiles])
255
+
256
+ const isFullyLoaded = useMemo(() => {
257
+ return !isLoadingPackageFilesWithContent && !isLoadingPackageFiles
258
+ }, [isLoadingPackageFilesWithContent, isLoadingPackageFiles])
172
259
 
173
260
  const fsMap = useMemo(() => {
174
261
  const map = localFiles.reduce(
@@ -178,6 +265,7 @@ export function useFileManagement({
178
265
  },
179
266
  {} as Record<string, string>,
180
267
  )
268
+
181
269
  return map
182
270
  }, [localFiles, manualEditsFileContent])
183
271
 
@@ -215,7 +303,6 @@ export function useFileManagement({
215
303
  }
216
304
  }
217
305
 
218
- // Check if file already exists
219
306
  const fileExists = localFiles?.some((file) => file.path === newFileName)
220
307
  if (fileExists) {
221
308
  onError(new Error(`File '${newFileName}' already exists`))
@@ -224,7 +311,6 @@ export function useFileManagement({
224
311
  }
225
312
  }
226
313
 
227
- // Ensure file name is not empty after path construction
228
314
  const fileName = newFileName.split("/").pop() || ""
229
315
  if (!fileName.trim()) {
230
316
  onError(new Error("File name cannot be empty"))
@@ -282,7 +368,6 @@ export function useFileManagement({
282
368
  }
283
369
  }
284
370
 
285
- // Extract just the filename from the path for validation
286
371
  const fileNameOnly = newFilename.split("/").pop() || ""
287
372
  if (!isValidFileName(fileNameOnly)) {
288
373
  onError(new Error("Invalid file name"))
@@ -395,7 +480,6 @@ export function useFileManagement({
395
480
 
396
481
  const saveFiles = () => {
397
482
  if (!isLoggedIn) {
398
- // For non-logged-in users, trigger immediate URL save
399
483
  if (debounceTimeoutRef.current) {
400
484
  clearTimeout(debounceTimeoutRef.current)
401
485
  }
@@ -433,20 +517,55 @@ export function useFileManagement({
433
517
  isCreatingRelease,
434
518
  ])
435
519
 
520
+ const currentFileCode = useMemo(
521
+ () =>
522
+ localFiles.find((x) => x.path === currentFile)?.content ?? DEFAULT_CODE,
523
+ [localFiles, currentFile],
524
+ )
525
+ const mainComponentPath = useMemo(() => {
526
+ const isComponentExportedInCurrentFile =
527
+ isComponentExported(currentFileCode)
528
+
529
+ const selectedComponent =
530
+ (currentFile?.endsWith(".tsx") || currentFile?.endsWith(".ts")) &&
531
+ !!localFiles.some((x) => x.path === currentFile) &&
532
+ isComponentExportedInCurrentFile
533
+ ? currentFile
534
+ : localFiles.find((x) => {
535
+ const isComponentExportedInFile = isComponentExported(x.content)
536
+ return (
537
+ x.path.endsWith(".tsx") ||
538
+ (x.path.endsWith(".ts") && isComponentExportedInFile)
539
+ )
540
+ })?.path || localFiles[0]?.path
541
+
542
+ return selectedComponent
543
+ }, [currentFile, localFiles, currentFileCode, packageFilesWithContent])
544
+
545
+ const priorityFileFetched = useMemo(() => {
546
+ return urlParams.package_id && isPriorityFileFetched
547
+ }, [urlParams.package_id, isPriorityFileFetched])
548
+
436
549
  return {
437
550
  fsMap,
438
551
  createFile,
552
+ priorityFileFetched,
439
553
  deleteFile,
440
554
  renameFile,
441
555
  saveFiles,
442
556
  localFiles,
557
+ mainComponentPath,
558
+ currentFileCode,
443
559
  initialFiles,
444
560
  currentFile,
445
561
  setLocalFiles,
446
562
  onFileSelect,
447
563
  isLoading,
564
+ isFullyLoaded,
448
565
  isSaving,
449
566
  savePackage,
450
567
  packageFilesMeta,
568
+ totalFilesCount,
569
+ loadedFilesCount,
451
570
  }
452
571
  }
@@ -0,0 +1,149 @@
1
+ import { useQuery, useQueries } from "react-query"
2
+ import { useAxios } from "@/hooks/use-axios"
3
+ import { usePackageFiles } from "@/hooks/use-package-files"
4
+ import { usePackageFileById } from "@/hooks/use-package-files"
5
+ import type { Package } from "fake-snippets-api/lib/db/schema"
6
+ import { useState, useMemo } from "react"
7
+ import { findTargetFile } from "@/lib/utils/findTargetFile"
8
+
9
+ export interface PackageFile {
10
+ path: string
11
+ content: string
12
+ }
13
+
14
+ export interface OptimizedLoadingState {
15
+ priorityFile: PackageFile | null
16
+ allFiles: PackageFile[]
17
+ isPriorityLoading: boolean
18
+ areAllFilesLoading: boolean
19
+ error: any
20
+ }
21
+
22
+ export function useOptimizedPackageFilesLoader(
23
+ pkg?: Package,
24
+ priorityFilePath?: string | null,
25
+ packageId?: string | null,
26
+ ) {
27
+ const axios = useAxios()
28
+ const [loadedFiles, setLoadedFiles] = useState<Map<string, PackageFile>>(
29
+ new Map(),
30
+ )
31
+
32
+ const pkgFilesRaw = usePackageFiles(pkg?.latest_package_release_id)
33
+
34
+ // Filter out dist/ files to avoid downloading build artifacts
35
+ const pkgFiles = useMemo(
36
+ () => ({
37
+ ...pkgFilesRaw,
38
+ data: pkgFilesRaw.data?.filter(
39
+ (file) => !file.file_path.startsWith("dist/"),
40
+ ),
41
+ }),
42
+ [pkgFilesRaw.data, pkgFilesRaw.isLoading, pkgFilesRaw.error],
43
+ )
44
+
45
+ const targetFilePath = useMemo(() => {
46
+ if (!pkgFiles.data) return priorityFilePath
47
+
48
+ if (priorityFilePath) {
49
+ const exactMatch = pkgFiles.data.find(
50
+ (f) => f.file_path === priorityFilePath,
51
+ )
52
+ if (exactMatch) return exactMatch.file_path
53
+
54
+ const partialMatch = pkgFiles.data.find(
55
+ (f) =>
56
+ f.file_path.includes(priorityFilePath) ||
57
+ priorityFilePath.includes(f.file_path),
58
+ )
59
+ if (partialMatch) return partialMatch.file_path
60
+ }
61
+
62
+ // Check for index.tsx first
63
+ const indexFile = pkgFiles.data.find((f) => f.file_path === "index.tsx")
64
+ if (indexFile) return indexFile.file_path
65
+
66
+ // Fallback to first file
67
+ return pkgFiles.data[0]?.file_path || null
68
+ }, [pkgFiles.data, priorityFilePath])
69
+
70
+ const priorityFileData = pkgFiles.data?.find(
71
+ (file) => file.file_path === targetFilePath,
72
+ )
73
+
74
+ const priorityFileQuery = useQuery({
75
+ queryKey: ["priorityPackageFile", priorityFileData?.package_file_id],
76
+ queryFn: async () => {
77
+ if (!priorityFileData) return null
78
+
79
+ const response = await axios.post(`/package_files/get`, {
80
+ package_file_id: priorityFileData.package_file_id,
81
+ })
82
+ const content = response.data.package_file?.content_text
83
+ const file = { path: priorityFileData.file_path, content: content ?? "" }
84
+
85
+ setLoadedFiles((prev) => {
86
+ const newMap = new Map(prev)
87
+ newMap.set(file.path, file)
88
+ return newMap
89
+ })
90
+
91
+ return file
92
+ },
93
+ enabled: !!priorityFileData,
94
+ refetchOnWindowFocus: false,
95
+ refetchOnMount: false,
96
+ staleTime: Infinity,
97
+ cacheTime: Infinity,
98
+ })
99
+
100
+ const remainingFilesQueries = useQueries(
101
+ pkgFiles.data
102
+ ?.filter((file) => file.file_path !== targetFilePath)
103
+ ?.map((file) => ({
104
+ queryKey: ["packageFile", file.package_file_id],
105
+ queryFn: async () => {
106
+ const response = await axios.post(`/package_files/get`, {
107
+ package_file_id: file.package_file_id,
108
+ })
109
+ const content = response.data.package_file?.content_text
110
+ const fileData = { path: file.file_path, content: content ?? "" }
111
+
112
+ setLoadedFiles((prev) => {
113
+ const newMap = new Map(prev)
114
+ newMap.set(fileData.path, fileData)
115
+ return newMap
116
+ })
117
+
118
+ return fileData
119
+ },
120
+ refetchOnWindowFocus: false,
121
+ refetchOnMount: false,
122
+ staleTime: Infinity,
123
+ cacheTime: Infinity,
124
+ })) ?? [],
125
+ )
126
+
127
+ const allFiles = useMemo(() => {
128
+ return Array.from(loadedFiles.values())
129
+ }, [loadedFiles])
130
+
131
+ const areAllFilesLoading =
132
+ remainingFilesQueries.some((q) => q.isLoading) ||
133
+ priorityFileQuery.isLoading
134
+ const error =
135
+ priorityFileQuery.error || remainingFilesQueries.find((q) => q.error)?.error
136
+
137
+ return {
138
+ priorityFile: priorityFileQuery.data || null,
139
+ priorityFileFetched: priorityFileQuery.isFetched,
140
+ allFiles,
141
+ isPriorityLoading: priorityFileQuery.isLoading,
142
+ areAllFilesLoading,
143
+ error,
144
+ isMetaLoading: pkgFiles.isLoading,
145
+ totalFilesCount: pkgFiles.data?.length || 0,
146
+ loadedFilesCount: allFiles.length,
147
+ isPriorityFileFetched: priorityFileQuery.isFetched,
148
+ }
149
+ }
@@ -2,6 +2,7 @@ import { useMutation } from "react-query"
2
2
  import type { Package } from "fake-snippets-api/lib/db/schema"
3
3
  import { useAxios } from "./use-axios"
4
4
  import { useToast } from "@/components/ViewPackagePage/hooks/use-toast"
5
+ import { isHiddenFile } from "@/components/ViewPackagePage/utils/is-hidden-file"
5
6
 
6
7
  interface PackageFile {
7
8
  path: string
@@ -42,6 +43,7 @@ export function useUpdatePackageFilesMutation({
42
43
  let updatedFilesCount = 0
43
44
 
44
45
  for (const file of localFiles) {
46
+ if (isHiddenFile(file.path)) continue
45
47
  const initialFile = initialFiles.find((x) => x.path === file.path)
46
48
  if (file.content !== initialFile?.content) {
47
49
  const updatePkgFilePayload = {
@@ -1,3 +1,4 @@
1
+ import { convertCircuitJsonToSimple3dSvg } from "circuit-json-to-simple-3d"
1
2
  import { AnyCircuitElement } from "circuit-json"
2
3
  import {
3
4
  convertCircuitJsonToAssemblySvg,
@@ -6,7 +7,7 @@ import {
6
7
  } from "circuit-to-svg"
7
8
  import { saveAs } from "file-saver"
8
9
 
9
- export type ImageFormat = "schematic" | "pcb" | "assembly"
10
+ export type ImageFormat = "schematic" | "pcb" | "assembly" | "3d"
10
11
 
11
12
  interface DownloadCircuitPngOptions {
12
13
  format: ImageFormat
@@ -65,7 +66,7 @@ export const downloadCircuitPng = async (
65
66
  if (options.width) svgOptions.width = options.width
66
67
  if (options.height) svgOptions.height = options.height
67
68
 
68
- switch (options.format) {
69
+ switch (options.format.toLowerCase()) {
69
70
  case "schematic":
70
71
  svg = convertCircuitJsonToSchematicSvg(circuitJson, svgOptions)
71
72
  break
@@ -76,13 +77,20 @@ export const downloadCircuitPng = async (
76
77
  svg = convertCircuitJsonToAssemblySvg(circuitJson, svgOptions)
77
78
  break
78
79
  default:
79
- throw new Error(`Unsupported format: ${options.format}`)
80
+ svg = await convertCircuitJsonToSimple3dSvg(circuitJson, {
81
+ background: {
82
+ color: "#fff",
83
+ opacity: 0.0,
84
+ },
85
+ defaultZoomMultiplier: 1.1,
86
+ })
80
87
  }
81
88
 
82
89
  blob = await convertSvgToPng(svg)
83
90
  const downloadFileName = `${fileName}_${options.format}.png`
84
91
  saveAs(blob, downloadFileName)
85
92
  } catch (error) {
93
+ console.error(error)
86
94
  throw new Error(`Failed to download ${options.format} PNG: ${error}`)
87
95
  }
88
96
  }
@@ -0,0 +1,44 @@
1
+ import { CircuitJson } from "circuit-json"
2
+ import { saveAs } from "file-saver"
3
+ import {
4
+ convertCircuitJsonToGltf,
5
+ type ConversionOptions,
6
+ } from "circuit-json-to-gltf"
7
+ export const downloadGltfFromCircuitJson = async (
8
+ circuitJson: CircuitJson,
9
+ fileName: string,
10
+ options?: ConversionOptions,
11
+ ) => {
12
+ const result = await convertCircuitJsonToGltf(circuitJson, options)
13
+
14
+ let blob: Blob
15
+ let extension = options?.format === "glb" ? ".glb" : ".gltf"
16
+
17
+ if (result instanceof ArrayBuffer) {
18
+ blob = new Blob([result], { type: "model/gltf-binary" })
19
+ extension = options?.format
20
+ ? options.format === "glb"
21
+ ? ".glb"
22
+ : ".gltf"
23
+ : ".glb"
24
+ } else if (
25
+ typeof ArrayBuffer !== "undefined" &&
26
+ result &&
27
+ typeof (result as any).buffer === "object" &&
28
+ (result as any).byteLength !== undefined
29
+ ) {
30
+ const view = result as ArrayBufferView
31
+ blob = new Blob([view], { type: "model/gltf-binary" })
32
+ extension = options?.format
33
+ ? options.format === "glb"
34
+ ? ".glb"
35
+ : ".gltf"
36
+ : ".glb"
37
+ } else if (typeof result === "string") {
38
+ blob = new Blob([result], { type: "model/gltf+json" })
39
+ } else {
40
+ blob = new Blob([JSON.stringify(result)], { type: "model/gltf+json" })
41
+ }
42
+
43
+ saveAs(blob, fileName + extension)
44
+ }
@@ -0,0 +1,9 @@
1
+ export const isComponentExported = (code: string) => {
2
+ return (
3
+ /export function\s+\w+/.test(code) ||
4
+ /export const\s+\w+\s*=/.test(code) ||
5
+ /export default\s+\w+/.test(code) ||
6
+ /export default\s+function\s*(\w*)\s*\(/.test(code) ||
7
+ /export default\s*\(\s*\)\s*=>/.test(code)
8
+ )
9
+ }
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import Header from "@/components/Header"
4
2
  import { useGlobalStore } from "@/hooks/use-global-store"
5
3
  import { useEffect, useState } from "react"
@@ -232,7 +232,6 @@ export function LandingPage() {
232
232
  </section>
233
233
  </main>
234
234
  <Footer />
235
- {/* <Footer2 /> */}
236
235
  </div>
237
236
  )
238
237
  }
@@ -1,4 +1,4 @@
1
- import { useState } from "react"
1
+ import { useEffect, useState } from "react"
2
2
  import { useParams } from "wouter"
3
3
  import { Loader2, ChevronLeft, ChevronRight } from "lucide-react"
4
4
  import Header from "@/components/Header"
@@ -14,6 +14,7 @@ import { usePackageFilesLoader } from "@/hooks/usePackageFilesLoader"
14
14
  import { usePackageBuild } from "@/hooks/use-package-builds"
15
15
  import { PackageBuild } from "fake-snippets-api/lib/db/schema"
16
16
  import { usePackageByName } from "@/hooks/use-package-by-package-name"
17
+ import { findTargetFile } from "@/lib/utils/findTargetFile"
17
18
 
18
19
  const StatusPill = ({ status }: { status: string }) => {
19
20
  const color =
@@ -38,7 +39,7 @@ export default function PreviewBuildPage() {
38
39
  const packageName = params?.packageName || null
39
40
 
40
41
  const [sidebarCollapsed, setSidebarCollapsed] = useState(true)
41
- const [selectedFile, setSelectedFile] = useState<string | null>("index.tsx")
42
+ const [selectedFile, setSelectedFile] = useState<string | null>(null)
42
43
  const [selectedItemId, setSelectedItemId] = useState<string>("")
43
44
 
44
45
  const { data: packageRelease, isLoading: isLoadingRelease } =
@@ -56,6 +57,15 @@ export default function PreviewBuildPage() {
56
57
  buildFiles.map((f) => [f.path, f.content]),
57
58
  )
58
59
 
60
+ const targetFile = findTargetFile(buildFiles, selectedFile)
61
+ const mainComponentPath = targetFile?.path ?? null
62
+
63
+ useEffect(() => {
64
+ if (!selectedFile && mainComponentPath) {
65
+ setSelectedFile(mainComponentPath)
66
+ }
67
+ }, [mainComponentPath, selectedFile])
68
+
59
69
  if (!packageReleaseId) {
60
70
  return <NotFoundPage heading="Package Release Not Found" />
61
71
  }
@@ -74,7 +84,7 @@ export default function PreviewBuildPage() {
74
84
 
75
85
  const treeData = transformFilesToTreeData({
76
86
  files: buildFsMap,
77
- currentFile: selectedFile,
87
+ currentFile: selectedFile ?? mainComponentPath,
78
88
  renamingFile: null,
79
89
  handleRenameFile: () => ({ fileRenamed: false }),
80
90
  handleDeleteFile: () => ({ fileDeleted: false }),
@@ -221,7 +231,7 @@ export default function PreviewBuildPage() {
221
231
  ) : status === "success" && buildFiles.length > 0 ? (
222
232
  <SuspenseRunFrame
223
233
  fsMap={buildFsMap}
224
- mainComponentPath={selectedFile ?? "index.tsx"}
234
+ mainComponentPath={mainComponentPath ?? "index.tsx"}
225
235
  showRunButton={false}
226
236
  className="[&>div]:overflow-y-hidden"
227
237
  />
@@ -5,18 +5,19 @@ import { useLocation, useParams } from "wouter"
5
5
  import { Helmet } from "react-helmet-async"
6
6
  import { useEffect, useState } from "react"
7
7
  import NotFoundPage from "./404"
8
- import { useCurrentPackageId } from "@/hooks/use-current-package-id"
9
- import { usePackage } from "@/hooks/use-package"
8
+ import { usePackageByName } from "@/hooks/use-package-by-package-name"
10
9
 
11
10
  export const ViewPackagePage = () => {
12
- const {
13
- packageId,
14
- error: packageIdError,
15
- isLoading: isLoadingPackageId,
16
- } = useCurrentPackageId()
17
- const { data: packageInfo } = usePackage(packageId)
18
11
  const { author, packageName } = useParams()
12
+ const packageNameFull = `${author}/${packageName}`
19
13
  const [, setLocation] = useLocation()
14
+
15
+ // Get package data directly by name - this will also cache by ID
16
+ const {
17
+ data: packageInfo,
18
+ error: packageError,
19
+ isLoading: isLoadingPackage,
20
+ } = usePackageByName(packageNameFull)
20
21
  const {
21
22
  data: packageRelease,
22
23
  error: packageReleaseError,
@@ -33,11 +34,10 @@ export const ViewPackagePage = () => {
33
34
  },
34
35
  )
35
36
 
36
- const { data: packageFiles } = usePackageFiles(
37
- packageRelease?.package_release_id,
38
- )
37
+ const { data: packageFiles, isFetched: arePackageFilesFetched } =
38
+ usePackageFiles(packageRelease?.package_release_id)
39
39
 
40
- if (!isLoadingPackageId && packageIdError) {
40
+ if (!isLoadingPackage && packageError) {
41
41
  return <NotFoundPage heading="Package Not Found" />
42
42
  }
43
43
 
@@ -51,7 +51,8 @@ export const ViewPackagePage = () => {
51
51
  <title>{`${author}/${packageName} - tscircuit`}</title>
52
52
  </Helmet>
53
53
  <RepoPageContent
54
- packageFiles={packageFiles as any}
54
+ packageFiles={packageFiles ?? []}
55
+ arePackageFilesFetched={arePackageFilesFetched}
55
56
  packageInfo={packageInfo}
56
57
  packageRelease={packageRelease}
57
58
  importantFilePaths={["README.md", "LICENSE", "package.json"]}