@tscircuit/fake-snippets 0.0.107 → 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 (80) hide show
  1. package/api/generated-index.js +82 -22
  2. package/biome.json +7 -1
  3. package/bun-tests/fake-snippets-api/routes/package_builds/get.test.ts +0 -15
  4. package/bun-tests/fake-snippets-api/routes/package_builds/list.test.ts +0 -12
  5. package/bun.lock +62 -19
  6. package/dist/bundle.js +25 -24
  7. package/dist/index.d.ts +26 -15
  8. package/dist/index.js +19 -18
  9. package/dist/schema.d.ts +32 -24
  10. package/dist/schema.js +7 -6
  11. package/fake-snippets-api/lib/db/db-client.ts +10 -1
  12. package/fake-snippets-api/lib/db/schema.ts +4 -3
  13. package/fake-snippets-api/lib/db/seed.ts +6 -9
  14. package/fake-snippets-api/lib/public-mapping/public-map-package-build.ts +0 -3
  15. package/fake-snippets-api/lib/public-mapping/public-map-package-release.ts +3 -0
  16. package/package.json +7 -8
  17. package/src/App.tsx +12 -11
  18. package/src/components/DownloadButtonAndMenu.tsx +133 -35
  19. package/src/components/FileSidebar.tsx +45 -193
  20. package/src/components/Footer.tsx +0 -1
  21. package/src/components/HeaderLogin.tsx +1 -1
  22. package/src/components/HiddenFilesDropdown.tsx +0 -2
  23. package/src/components/PackageBreadcrumb.tsx +1 -1
  24. package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +0 -2
  25. package/src/components/PackageBuildsPage/build-preview-content.tsx +34 -5
  26. package/src/components/PackageCard.tsx +0 -1
  27. package/src/components/ViewPackagePage/components/ShikiCodeViewer.tsx +20 -11
  28. package/src/components/ViewPackagePage/components/important-files-view.tsx +75 -59
  29. package/src/components/ViewPackagePage/components/main-content-header.tsx +4 -4
  30. package/src/components/ViewPackagePage/components/main-content-view-selector.tsx +0 -1
  31. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +1 -2
  32. package/src/components/ViewPackagePage/components/package-header.tsx +14 -17
  33. package/src/components/ViewPackagePage/components/preview-image-squares.tsx +0 -1
  34. package/src/components/ViewPackagePage/components/repo-page-content.tsx +21 -20
  35. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +18 -2
  36. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +1 -1
  37. package/src/components/ViewPackagePage/components/sidebar.tsx +0 -2
  38. package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +18 -17
  39. package/src/components/ViewPackagePage/components/theme-toggle.tsx +0 -2
  40. package/src/components/ViewPackagePage/hooks/use-toast.tsx +0 -1
  41. package/src/components/package-port/CodeAndPreview.tsx +23 -40
  42. package/src/components/package-port/CodeEditor.tsx +24 -1
  43. package/src/components/package-port/CodeEditorHeader.tsx +5 -2
  44. package/src/components/preview/BuildsList.tsx +20 -9
  45. package/src/components/preview/ConnectedPackagesList.tsx +73 -60
  46. package/src/components/preview/ConnectedRepoOverview.tsx +160 -154
  47. package/src/components/preview/PackageReleasesDashboard.tsx +41 -30
  48. package/src/components/preview/index.tsx +16 -153
  49. package/src/hooks/use-current-package-id.ts +5 -30
  50. package/src/hooks/use-current-package-info.ts +29 -5
  51. package/src/hooks/use-global-store.ts +1 -1
  52. package/src/hooks/useFileManagement.ts +153 -34
  53. package/src/hooks/useOptimizedPackageFilesLoader.ts +149 -0
  54. package/src/hooks/useUpdatePackageFilesMutation.ts +2 -0
  55. package/src/index.css +24 -0
  56. package/src/lib/download-fns/download-circuit-png.ts +11 -3
  57. package/src/lib/download-fns/download-gltf-from-circuit-json.ts +44 -0
  58. package/src/lib/utils/isComponentExported.ts +9 -0
  59. package/src/lib/utils/transformFilesToTreeData.tsx +195 -0
  60. package/src/pages/404.tsx +3 -5
  61. package/src/pages/authorize.tsx +0 -2
  62. package/src/pages/landing.tsx +0 -1
  63. package/src/pages/preview-release.tsx +279 -0
  64. package/src/pages/release-builds.tsx +0 -8
  65. package/src/pages/release-detail.tsx +17 -15
  66. package/src/pages/releases.tsx +5 -1
  67. package/src/pages/view-package.tsx +14 -13
  68. package/src/components/Footer2.tsx +0 -100
  69. package/src/components/ShippingInformationForm.tsx +0 -423
  70. package/src/components/StaticViewSnippetHeader.tsx +0 -70
  71. package/src/components/ViewPackagePage/components/file-explorer.tsx +0 -67
  72. package/src/components/ViewPackagePage/components/readme-view.tsx +0 -58
  73. package/src/components/ViewPackagePage/components/repo-header-button.tsx +0 -36
  74. package/src/components/ViewPackagePage/components/repo-header.tsx +0 -4
  75. package/src/components/ViewPackagePage/components/sidebar-contributors-section.tsx +0 -31
  76. package/src/components/ViewSnippetHeader.tsx +0 -181
  77. package/src/components/ui/input-otp.tsx +0 -69
  78. package/src/hooks/use-snippets-base-api-url.ts +0 -3
  79. package/src/pages/preview-build.tsx +0 -380
  80. package/src/pages/settings.tsx +0 -25
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import { useState } from "react"
4
2
  import { Button } from "@/components/ui/button"
5
3
  import {
@@ -72,7 +70,9 @@ export default function MainContentHeader({
72
70
  }
73
71
  }
74
72
 
75
- const { circuitJson } = useCurrentPackageCircuitJson()
73
+ const hasCircuitJson = packageFiles.some(
74
+ (file) => file.file_path === "dist/circuit.json",
75
+ )
76
76
 
77
77
  return (
78
78
  <div className="flex items-center justify-between mb-4">
@@ -86,7 +86,7 @@ export default function MainContentHeader({
86
86
  unscopedName={packageInfo?.unscoped_name}
87
87
  desiredImageType={activeView}
88
88
  author={packageInfo?.owner_github_username ?? undefined}
89
- circuitJson={circuitJson}
89
+ hasCircuitJson={hasCircuitJson}
90
90
  />
91
91
 
92
92
  {/* Code Dropdown */}
@@ -1,4 +1,3 @@
1
- "use client"
2
1
  import React from "react"
3
2
  import {
4
3
  Code,
@@ -1,4 +1,3 @@
1
- "use client"
2
1
  import { GitFork, Star, Tag, Settings, LinkIcon } from "lucide-react"
3
2
  import { Badge } from "@/components/ui/badge"
4
3
  import { Skeleton } from "@/components/ui/skeleton"
@@ -112,7 +111,7 @@ const MobileSidebar = ({
112
111
  {localDescription ||
113
112
  packageInfo?.description ||
114
113
  packageInfo?.ai_description ||
115
- "A Default 60 keyboard created with tscircuit"}
114
+ ""}
116
115
  </p>
117
116
  {isOwner && (
118
117
  <Button
@@ -12,10 +12,7 @@ import { Lock, Globe } from "lucide-react"
12
12
  import { GitFork, Package, Star } from "lucide-react"
13
13
 
14
14
  import { useForkPackageMutation } from "@/hooks/use-fork-package-mutation"
15
- import {
16
- usePackageStarMutationByName,
17
- usePackageStarsByName,
18
- } from "@/hooks/use-package-stars"
15
+ import { usePackageStarMutationByName } from "@/hooks/use-package-stars"
19
16
  import { useOrderDialog } from "@tscircuit/runframe"
20
17
  import { useGlobalStore } from "@/hooks/use-global-store"
21
18
  import { Package as PackageType } from "fake-snippets-api/lib/db/schema"
@@ -45,8 +42,7 @@ export default function PackageHeader({
45
42
  isLoggedIn,
46
43
  packageReleaseId: packageInfo?.latest_package_release_id ?? "",
47
44
  })
48
- const { data: starData, isLoading: isStarDataLoading } =
49
- usePackageStarsByName(packageInfo?.name ?? null)
45
+
50
46
  const { addStar, removeStar } = usePackageStarMutationByName(
51
47
  packageInfo?.name ?? "",
52
48
  )
@@ -57,7 +53,7 @@ export default function PackageHeader({
57
53
  const handleStarClick = async () => {
58
54
  if (!packageInfo?.name || !isLoggedIn) return
59
55
 
60
- if (starData?.is_starred) {
56
+ if (packageInfo?.is_starred) {
61
57
  await removeStar.mutateAsync()
62
58
  } else {
63
59
  await addStar.mutateAsync()
@@ -69,8 +65,7 @@ export default function PackageHeader({
69
65
  await forkPackage(packageInfo.package_id)
70
66
  }
71
67
 
72
- const isStarLoading =
73
- isStarDataLoading || addStar.isLoading || removeStar.isLoading
68
+ const isStarLoading = addStar.isLoading || removeStar.isLoading
74
69
 
75
70
  useEffect(() => {
76
71
  window.TSCIRCUIT_REGISTRY_API_BASE_URL =
@@ -155,15 +150,15 @@ export default function PackageHeader({
155
150
  >
156
151
  <Star
157
152
  className={`w-4 h-4 mr-2 ${
158
- starData?.is_starred
153
+ packageInfo?.is_starred
159
154
  ? "fill-yellow-500 text-yellow-500"
160
155
  : ""
161
156
  }`}
162
157
  />
163
- {starData?.is_starred ? "Starred" : "Star"}
164
- {(starData?.star_count ?? 0) > 0 && (
158
+ {packageInfo?.is_starred ? "Starred" : "Star"}
159
+ {(packageInfo?.star_count ?? 0) > 0 && (
165
160
  <span className="ml-1.5 bg-gray-100 text-gray-700 rounded-full px-1.5 py-0.5 text-xs font-medium">
166
- {starData?.star_count}
161
+ {packageInfo?.star_count}
167
162
  </span>
168
163
  )}
169
164
  </Button>
@@ -229,13 +224,15 @@ export default function PackageHeader({
229
224
  >
230
225
  <Star
231
226
  className={`w-4 h-4 mr-2 ${
232
- starData?.is_starred ? "fill-yellow-500 text-yellow-500" : ""
227
+ packageInfo?.is_starred
228
+ ? "fill-yellow-500 text-yellow-500"
229
+ : ""
233
230
  }`}
234
231
  />
235
- {starData?.is_starred ? "Starred" : "Star"}
236
- {(starData?.star_count ?? 0) > 0 && (
232
+ {packageInfo?.is_starred ? "Starred" : "Star"}
233
+ {(packageInfo?.star_count ?? 0) > 0 && (
237
234
  <span className="ml-1.5 bg-gray-100 text-gray-700 rounded-full px-1.5 py-0.5 text-xs font-medium">
238
- {starData?.star_count}
235
+ {packageInfo?.star_count}
239
236
  </span>
240
237
  )}
241
238
  </Button>
@@ -1,4 +1,3 @@
1
- "use client"
2
1
  import { Skeleton } from "@/components/ui/skeleton"
3
2
  import { usePreviewImages } from "@/hooks/use-preview-images"
4
3
  import type { Package } from "fake-snippets-api/lib/db/schema"
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import { useState, useEffect, useMemo } from "react"
4
2
  import MainContentHeader from "./main-content-header"
5
3
  import Sidebar from "./sidebar"
@@ -21,20 +19,18 @@ import Footer from "@/components/Footer"
21
19
  import PackageHeader from "./package-header"
22
20
  import { useGlobalStore } from "@/hooks/use-global-store"
23
21
  import { useLocation } from "wouter"
24
- import { Package } from "fake-snippets-api/lib/db/schema"
25
- import { useCurrentPackageCircuitJson } from "../hooks/use-current-package-circuit-json"
22
+ import type {
23
+ Package,
24
+ PackageFile as ApiPackageFile,
25
+ } from "fake-snippets-api/lib/db/schema"
26
26
  import { useRequestAiReviewMutation } from "@/hooks/use-request-ai-review-mutation"
27
27
  import { useAiReview } from "@/hooks/use-ai-review"
28
28
  import { useQueryClient } from "react-query"
29
29
  import SidebarReleasesSection from "./sidebar-releases-section"
30
30
 
31
- interface PackageFile {
32
- package_file_id: string
33
- package_release_id: string
34
- file_path: string
35
- file_content: string
36
- content_text?: string // Keep for backward compatibility
37
- created_at: string // iso-8601
31
+ interface PackageFile extends ApiPackageFile {
32
+ file_content?: string
33
+ content_text?: string | null // Keep for backward compatibility
38
34
  }
39
35
 
40
36
  interface RepoPageContentProps {
@@ -44,10 +40,12 @@ interface RepoPageContentProps {
44
40
  packageRelease?: import("fake-snippets-api/lib/db/schema").PackageRelease
45
41
  onFileClicked?: (file: PackageFile) => void
46
42
  onEditClicked?: () => void
43
+ arePackageFilesFetched?: boolean
47
44
  }
48
45
 
49
46
  export default function RepoPageContent({
50
47
  packageFiles,
48
+ arePackageFilesFetched = false,
51
49
  packageInfo,
52
50
  packageRelease,
53
51
  onFileClicked,
@@ -70,8 +68,12 @@ export default function RepoPageContent({
70
68
  }
71
69
  }, [aiReview?.ai_review_text, queryClient])
72
70
  const session = useGlobalStore((s) => s.session)
73
- const { circuitJson, isLoading: isCircuitJsonLoading } =
74
- useCurrentPackageCircuitJson()
71
+
72
+ // Check if circuit.json exists without downloading it
73
+ const circuitJsonExists = useMemo(() => {
74
+ return packageFiles?.some((file) => file.file_path === "dist/circuit.json")
75
+ }, [packageFiles])
76
+
75
77
  const { mutate: requestAiReview, isLoading: isRequestingAiReview } =
76
78
  useRequestAiReviewMutation({
77
79
  onSuccess: (_packageRelease, aiReview) => {
@@ -91,13 +93,12 @@ export default function RepoPageContent({
91
93
 
92
94
  // Handle initial view selection and hash-based view changes
93
95
  useEffect(() => {
94
- if (isCircuitJsonLoading) return
95
- if (!packageInfo) return
96
+ if (!packageInfo || !arePackageFilesFetched) return
96
97
  const hash = window.location.hash.slice(1)
97
98
  const validViews = ["files", "3d", "pcb", "schematic", "bom"]
98
99
  const circuitDependentViews = ["3d", "pcb", "schematic", "bom"]
99
100
 
100
- const availableViews = circuitJson
101
+ const availableViews = circuitJsonExists
101
102
  ? validViews
102
103
  : validViews.filter((view) => !circuitDependentViews.includes(view))
103
104
 
@@ -115,7 +116,7 @@ export default function RepoPageContent({
115
116
  window.location.hash = "files"
116
117
  }
117
118
  }
118
- }, [packageInfo?.default_view, circuitJson, isCircuitJsonLoading])
119
+ }, [packageInfo?.default_view, circuitJsonExists])
119
120
 
120
121
  const importantFilePaths = packageFiles
121
122
  ?.filter((pf) => isPackageFileImportant(pf.file_path))
@@ -146,7 +147,7 @@ export default function RepoPageContent({
146
147
  return (
147
148
  <FilesView
148
149
  packageFiles={packageFiles}
149
- isLoading={!packageFiles}
150
+ arePackageFilesFetched={arePackageFilesFetched}
150
151
  onFileClicked={onFileClicked}
151
152
  />
152
153
  )
@@ -162,7 +163,7 @@ export default function RepoPageContent({
162
163
  return (
163
164
  <FilesView
164
165
  packageFiles={packageFiles}
165
- isLoading={!packageFiles}
166
+ arePackageFilesFetched={arePackageFilesFetched}
166
167
  onFileClicked={onFileClicked}
167
168
  />
168
169
  )
@@ -215,7 +216,7 @@ export default function RepoPageContent({
215
216
  {/* Important Files View - Always shown */}
216
217
  <ImportantFilesView
217
218
  importantFiles={importantFiles}
218
- isLoading={!packageFiles}
219
+ isFetched={arePackageFilesFetched}
219
220
  onEditClicked={onEditClicked}
220
221
  packageAuthorOwner={packageInfo?.owner_github_username}
221
222
  aiDescription={packageInfo?.ai_description ?? ""}
@@ -1,5 +1,5 @@
1
1
  import { Badge } from "@/components/ui/badge"
2
- import { GitFork, Star, Settings, LinkIcon, Github } from "lucide-react"
2
+ import { GitFork, Star, Settings, LinkIcon, Github, Plus } from "lucide-react"
3
3
  import { Skeleton } from "@/components/ui/skeleton"
4
4
  import { useCurrentPackageInfo } from "@/hooks/use-current-package-info"
5
5
  import { usePackageReleaseById } from "@/hooks/use-package-release"
@@ -164,7 +164,7 @@ export default function SidebarAboutSection({
164
164
  <GitFork className="h-4 w-4 mr-2 text-gray-500 dark:text-[#8b949e]" />
165
165
  <span>{(packageInfo as any)?.fork_count ?? "0"} forks</span>
166
166
  </div>
167
- {packageInfo?.github_repo_full_name && (
167
+ {packageInfo?.github_repo_full_name ? (
168
168
  <a
169
169
  target="_blank"
170
170
  href={`https://github.com/${packageInfo.github_repo_full_name}`}
@@ -173,6 +173,22 @@ export default function SidebarAboutSection({
173
173
  <Github className="h-4 w-4 mr-2 text-gray-500 dark:text-[#8b949e]" />
174
174
  <span>{packageInfo?.github_repo_full_name.split("/")[1]}</span>
175
175
  </a>
176
+ ) : (
177
+ <>
178
+ {isOwner && (
179
+ <div
180
+ className="flex items-center hover:underline hover:underline-offset-2 cursor-pointer hover:decoration-gray-500"
181
+ onClick={openEditPackageDetailsDialog}
182
+ title="Connect GitHub"
183
+ >
184
+ <div className="relative mr-2">
185
+ <Github className="h-4 w-4 text-gray-500 dark:text-[#8b949e]" />
186
+ <Plus className="h-2 w-2 absolute -bottom-0.5 -right-0.5 text-gray-500 dark:text-[#8b949e] bg-white dark:bg-[#0d1117] rounded-full" />
187
+ </div>
188
+ <span>Connect GitHub</span>
189
+ </div>
190
+ )}
191
+ </>
176
192
  )}
177
193
  </div>
178
194
  </div>
@@ -110,7 +110,7 @@ export default function SidebarReleasesSection() {
110
110
  ))}
111
111
  {latestBuild && (
112
112
  <PrefetchPageLink
113
- href={`/build/${latestBuild.package_build_id}`}
113
+ href={`/${packageInfo?.name}/releases`}
114
114
  className="flex items-center gap-2 text-sm text-gray-500 dark:text-[#8b949e]"
115
115
  >
116
116
  <StatusIcon status={status} />
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import { Package } from "fake-snippets-api/lib/db/schema"
4
2
  import SidebarAboutSection from "./sidebar-about-section"
5
3
  import SidebarReleasesSection from "./sidebar-releases-section"
@@ -1,11 +1,10 @@
1
- "use client"
2
-
3
1
  import { Skeleton } from "@/components/ui/skeleton"
4
2
  import { FileText, Folder } from "lucide-react"
5
3
  import { useMemo, useState } from "react"
6
4
  import { isHiddenFile } from "../../utils/is-hidden-file"
7
5
  import { isWithinDirectory } from "../../utils/is-within-directory"
8
6
  import HiddenFilesDropdown from "@/components/HiddenFilesDropdown"
7
+ import type { PackageFile as ApiPackageFile } from "fake-snippets-api/lib/db/schema"
9
8
 
10
9
  interface Directory {
11
10
  type: "directory"
@@ -21,24 +20,20 @@ interface File {
21
20
  created_at: string
22
21
  }
23
22
 
24
- interface PackageFile {
25
- package_file_id: string
26
- package_release_id: string
27
- file_path: string
28
- file_content: string
29
- content_text?: string // Keep for backward compatibility
30
- created_at: string // iso-8601
23
+ interface PackageFile extends ApiPackageFile {
24
+ file_content?: string
25
+ content_text?: string | null // Keep for backward compatibility
31
26
  }
32
27
 
33
28
  interface FilesViewProps {
34
29
  packageFiles?: PackageFile[]
35
- isLoading?: boolean
36
30
  onFileClicked?: (file: PackageFile) => void
31
+ arePackageFilesFetched?: boolean
37
32
  }
38
33
 
39
34
  export default function FilesView({
40
35
  packageFiles = [],
41
- isLoading = false,
36
+ arePackageFilesFetched = false,
42
37
  onFileClicked,
43
38
  }: FilesViewProps) {
44
39
  const [activeDir, setActiveDir] = useState("")
@@ -106,12 +101,19 @@ export default function FilesView({
106
101
  }, [packageFiles, showHiddenFiles])
107
102
  // Format date for display
108
103
  const formatDate = (dateString: string) => {
109
- const date = new Date(dateString)
104
+ const parsedDate = new Date(dateString)
105
+ if (Number.isNaN(parsedDate.getTime())) return ""
106
+
110
107
  const now = new Date()
111
- const diffTime = Math.abs(now.getTime() - date.getTime())
112
- const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
108
+ const diffMs = now.getTime() - parsedDate.getTime()
109
+ const oneDayMs = 1000 * 60 * 60 * 24
110
+
111
+ // Treat future dates as today
112
+ if (diffMs <= 0) return "today"
113
113
 
114
- if (diffDays < 1) return "today"
114
+ if (diffMs < oneDayMs) return "today"
115
+
116
+ const diffDays = Math.floor(diffMs / oneDayMs)
115
117
  if (diffDays === 1) return "yesterday"
116
118
  if (diffDays < 7) return `${diffDays} days ago`
117
119
  if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`
@@ -160,7 +162,7 @@ export default function FilesView({
160
162
 
161
163
  const toggleHiddenFiles = () => setShowHiddenFiles((prev) => !prev)
162
164
 
163
- if (isLoading) {
165
+ if (!arePackageFilesFetched) {
164
166
  return (
165
167
  <div className="mb-4 border border-gray-200 dark:border-[#30363d] rounded-md overflow-hidden">
166
168
  <div className="flex items-center px-4 py-2 md:py-3 bg-gray-100 dark:bg-[#161b22] border-b border-gray-200 dark:border-[#30363d]">
@@ -225,7 +227,6 @@ export default function FilesView({
225
227
  </div>
226
228
  </div>
227
229
  </div>
228
-
229
230
  {/* Files and Directories */}
230
231
  <div className="bg-white dark:bg-[#0d1117]">
231
232
  {items.length === 0 && !activeDir ? (
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import { useTheme } from "next-themes"
4
2
  import { Moon, Sun } from "lucide-react"
5
3
  import { Button } from "@/components/ui/button"
@@ -1,4 +1,3 @@
1
- "use client"
2
1
  import toastLibrary, { Toaster, type Toast } from "react-hot-toast"
3
2
  import React from "react"
4
3
 
@@ -7,7 +7,6 @@ import useWarnUserOnPageChange from "@/hooks/use-warn-user-on-page-change"
7
7
  import { getSnippetTemplate } from "@/lib/get-snippet-template"
8
8
  import { cn } from "@/lib/utils"
9
9
  import type { Package } from "fake-snippets-api/lib/db/schema"
10
- import { Loader2 } from "lucide-react"
11
10
  import { useMemo, useState } from "react"
12
11
  import EditorNav from "@/components/package-port/EditorNav"
13
12
  import { SuspenseRunFrame } from "../SuspenseRunFrame"
@@ -15,8 +14,7 @@ import { applyEditEventsToManualEditsFile } from "@tscircuit/core"
15
14
  import { toastManualEditConflicts } from "@/lib/utils/toastManualEditConflicts"
16
15
  import { ManualEditEvent } from "@tscircuit/props"
17
16
  import { useFileManagement } from "@/hooks/useFileManagement"
18
- import { DEFAULT_CODE } from "@/lib/utils/package-utils"
19
- import { useGlobalStore } from "@/hooks/use-global-store"
17
+ import { isHiddenFile } from "../ViewPackagePage/utils/is-hidden-file"
20
18
 
21
19
  interface Props {
22
20
  pkg?: Package
@@ -39,13 +37,13 @@ export interface CodeAndPreviewState {
39
37
 
40
38
  export function CodeAndPreview({ pkg, projectUrl }: Props) {
41
39
  const { toast } = useToast()
42
- const session = useGlobalStore((s) => s.session)
43
40
  const urlParams = useUrlParams()
44
41
 
45
42
  const templateFromUrl = useMemo(
46
43
  () => (urlParams.template ? getSnippetTemplate(urlParams.template) : null),
47
44
  [urlParams.template],
48
45
  )
46
+
49
47
  const [state, setState] = useState<CodeAndPreviewState>({
50
48
  showPreview: true,
51
49
  fullScreen: false,
@@ -69,20 +67,26 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
69
67
  isSaving,
70
68
  currentFile,
71
69
  fsMap,
70
+ priorityFileFetched,
72
71
  isLoading,
73
72
  createFile,
73
+ mainComponentPath,
74
74
  deleteFile,
75
+ isFullyLoaded,
75
76
  onFileSelect,
77
+ totalFilesCount,
76
78
  saveFiles,
77
79
  setLocalFiles,
80
+ loadedFilesCount,
78
81
  localFiles,
82
+ currentFileCode,
79
83
  initialFiles,
80
84
  renameFile,
81
85
  packageFilesMeta,
82
86
  } = useFileManagement({
83
87
  templateCode: templateFromUrl?.code,
84
88
  currentPackage: pkg,
85
- fileChoosen: urlParams.file_path ?? null,
89
+ urlParams,
86
90
  openNewPackageSaveDialog,
87
91
  updateLastUpdated: () => {
88
92
  setState((prev) => ({ ...prev, lastSavedAt: Date.now() }))
@@ -94,6 +98,7 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
94
98
  (!isSaving &&
95
99
  Date.now() - state.lastSavedAt > 1000 &&
96
100
  localFiles.some((file) => {
101
+ if (isHiddenFile(file.path)) return false
97
102
  const initialFile = initialFiles.find((x) => x.path === file.path)
98
103
  return initialFile?.content !== file.content
99
104
  })) ||
@@ -103,29 +108,6 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
103
108
 
104
109
  useWarnUserOnPageChange({ hasUnsavedChanges })
105
110
 
106
- const currentFileCode = useMemo(
107
- () =>
108
- localFiles.find((x) => x.path === currentFile)?.content ??
109
- state.defaultComponentFile ??
110
- DEFAULT_CODE,
111
- [localFiles, currentFile],
112
- )
113
-
114
- const mainComponentPath = useMemo(() => {
115
- const isReactComponentExported =
116
- /export function\s+\w+/.test(currentFileCode) ||
117
- /export const\s+\w+\s*=/.test(currentFileCode) ||
118
- /export default\s+\w+/.test(currentFileCode) ||
119
- /export default\s+function\s*(\w*)\s*\(/.test(currentFileCode) ||
120
- /export default\s*\(\s*\)\s*=>/.test(currentFileCode)
121
-
122
- return (currentFile?.endsWith(".tsx") || currentFile?.endsWith(".ts")) &&
123
- !!localFiles.some((x) => x.path == currentFile) &&
124
- isReactComponentExported
125
- ? currentFile
126
- : state.defaultComponentFile
127
- }, [currentFile, localFiles, currentFileCode])
128
-
129
111
  const handleEditEvent = (event: ManualEditEvent) => {
130
112
  const parsedManualEdits = JSON.parse(
131
113
  localFiles.find((x) => x.path === "manual-edits.json")?.content || "{}",
@@ -171,17 +153,10 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
171
153
  })
172
154
  }
173
155
 
174
- if (urlParams.package_id && (!pkg || isLoading)) {
175
- return (
176
- <div className="flex items-center justify-center h-[80vh]">
177
- <div className="flex flex-col items-center justify-center">
178
- <div className="text-lg text-gray-500 mb-4">Loading</div>
179
- <Loader2 className="w-16 h-16 animate-spin text-gray-400" />
180
- </div>
181
- </div>
182
- )
183
- }
184
-
156
+ const finalfsMap = useMemo(
157
+ () => (Object.keys(fsMap).length > 0 ? fsMap : {}),
158
+ [fsMap],
159
+ )
185
160
  return (
186
161
  <div className="flex flex-col min-h-[50vh]">
187
162
  <EditorNav
@@ -213,8 +188,14 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
213
188
  <CodeEditor
214
189
  isSaving={isSaving}
215
190
  handleCreateFile={createFile}
191
+ totalFilesCount={totalFilesCount}
192
+ loadedFilesCount={loadedFilesCount}
193
+ isFullyLoaded={isFullyLoaded}
216
194
  handleDeleteFile={deleteFile}
217
195
  handleRenameFile={renameFile}
196
+ isPriorityFileFetched={
197
+ !priorityFileFetched && Boolean(urlParams.package_id)
198
+ }
218
199
  pkg={pkg}
219
200
  currentFile={currentFile}
220
201
  onFileSelect={onFileSelect}
@@ -255,7 +236,7 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
255
236
  onEditEvent={(event) => {
256
237
  handleEditEvent(event)
257
238
  }}
258
- fsMap={fsMap ?? {}}
239
+ fsMap={finalfsMap}
259
240
  projectUrl={projectUrl}
260
241
  />
261
242
  </div>
@@ -265,3 +246,5 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
265
246
  </div>
266
247
  )
267
248
  }
249
+
250
+ export default CodeAndPreview
@@ -50,6 +50,7 @@ import {
50
50
  import { isHiddenFile } from "../ViewPackagePage/utils/is-hidden-file"
51
51
  import { inlineCopilot } from "codemirror-copilot"
52
52
  import { useViewTsFilesDialog } from "@/components/dialogs/view-ts-files-dialog"
53
+ import { Loader2 } from "lucide-react"
53
54
 
54
55
  const defaultImports = `
55
56
  import React from "@types/react/jsx-runtime"
@@ -59,6 +60,7 @@ import type { CommonLayoutProps } from "@tscircuit/props"
59
60
 
60
61
  export const CodeEditor = ({
61
62
  onCodeChange,
63
+ isPriorityFileFetched,
62
64
  readOnly = false,
63
65
  files = [],
64
66
  isSaving = false,
@@ -72,9 +74,13 @@ export const CodeEditor = ({
72
74
  handleCreateFile,
73
75
  handleDeleteFile,
74
76
  pkg,
77
+ isFullyLoaded = false,
78
+ totalFilesCount = 0,
79
+ loadedFilesCount = 0,
75
80
  }: {
76
81
  onCodeChange: (code: string, filename?: string) => void
77
82
  files: PackageFile[]
83
+ isPriorityFileFetched: boolean
78
84
  isSaving?: boolean
79
85
  handleCreateFile: (props: ICreateFileProps) => ICreateFileResult
80
86
  handleDeleteFile: (props: IDeleteFileProps) => IDeleteFileResult
@@ -87,6 +93,9 @@ export const CodeEditor = ({
87
93
  onFileContentChanged?: (path: string, content: string) => void
88
94
  currentFile: string | null
89
95
  onFileSelect: (path: string, lineNumber?: number) => void
96
+ isFullyLoaded?: boolean
97
+ totalFilesCount?: number
98
+ loadedFilesCount?: number
90
99
  }) => {
91
100
  const editorRef = useRef<HTMLDivElement>(null)
92
101
  const viewRef = useRef<EditorView | null>(null)
@@ -120,10 +129,13 @@ export const CodeEditor = ({
120
129
  const [sidebarOpen, setSidebarOpen] = useState(false)
121
130
  const [isCreatingFile, setIsCreatingFile] = useState(false)
122
131
 
123
- // Set current file on component mount
132
+ // Set current file on component mount - only when explicitly requested via URL
124
133
  useEffect(() => {
125
134
  if (files.length === 0 || !pkgFilesLoaded || currentFile) return
126
135
 
136
+ // Only run this if there's an explicit file_path in URL - don't auto-select files
137
+ if (!filePathFromUrl) return
138
+
127
139
  const targetFile = findTargetFile(files, filePathFromUrl)
128
140
  if (targetFile) {
129
141
  const lineNumber = lineNumberFromUrl
@@ -825,10 +837,15 @@ export const CodeEditor = ({
825
837
  isCreatingFile={isCreatingFile}
826
838
  setIsCreatingFile={setIsCreatingFile}
827
839
  pkg={pkg}
840
+ isLoadingFiles={!isFullyLoaded}
841
+ loadingProgress={
842
+ totalFilesCount > 0 ? `${loadedFilesCount}/${totalFilesCount}` : null
843
+ }
828
844
  />
829
845
  <div className="flex flex-col flex-1 w-full min-w-0 h-full">
830
846
  {showImportAndFormatButtons && (
831
847
  <CodeEditorHeader
848
+ isLoadingFiles={!isFullyLoaded}
832
849
  entrypointFileName={entryPointFileName}
833
850
  appendNewFile={(path: string, content: string) => {
834
851
  onFileContentChanged?.(path, content)
@@ -854,7 +871,13 @@ export const CodeEditor = ({
854
871
  className={
855
872
  "flex-1 overflow-auto [&_.cm-editor]:h-full [&_.cm-scroller]:!h-full"
856
873
  }
874
+ style={{ display: isPriorityFileFetched ? "none" : "block" }}
857
875
  />
876
+ {isPriorityFileFetched && (
877
+ <div className="grid place-items-center h-full">
878
+ <Loader2 className="w-16 h-16 animate-spin text-gray-400" />
879
+ </div>
880
+ )}
858
881
  </div>
859
882
  {showQuickOpen && (
860
883
  <QuickOpen
@@ -41,6 +41,7 @@ interface CodeEditorHeaderProps {
41
41
  handleFileChange: (filename: FileName) => void
42
42
  entrypointFileName?: string
43
43
  appendNewFile: (path: string, content: string) => void
44
+ isLoadingFiles: boolean
44
45
  createFile: (props: ICreateFileProps) => ICreateFileResult
45
46
  aiAutocompleteState: [boolean, React.Dispatch<React.SetStateAction<boolean>>]
46
47
  }
@@ -49,8 +50,8 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
49
50
  currentFile,
50
51
  files,
51
52
  updateFileContent,
52
- appendNewFile,
53
53
  fileSidebarState,
54
+ isLoadingFiles = true,
54
55
  handleFileChange,
55
56
  entrypointFileName = "index.tsx",
56
57
  createFile,
@@ -230,7 +231,9 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
230
231
  (filename) => !isHiddenFile(filename),
231
232
  ).length > 0
232
233
  ? "Select file"
233
- : "No files"
234
+ : isLoadingFiles
235
+ ? "Loading files..."
236
+ : "No files"
234
237
  }
235
238
  />
236
239
  </SelectTrigger>