@tscircuit/fake-snippets 0.0.108 → 0.0.110

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 (203) hide show
  1. package/.github/workflows/bun-formatcheck.yml +2 -2
  2. package/.github/workflows/bun-pver-release.yml +3 -3
  3. package/.github/workflows/bun-test.yml +1 -1
  4. package/.github/workflows/bun-typecheck.yml +2 -2
  5. package/.github/workflows/update-snapshots.yml +1 -1
  6. package/README.md +4 -0
  7. package/api/generated-index.js +37 -3
  8. package/biome.json +2 -1
  9. package/bun-tests/fake-snippets-api/fixtures/get-test-server.ts +31 -3
  10. package/bun-tests/fake-snippets-api/fixtures/preload.ts +18 -0
  11. package/bun-tests/fake-snippets-api/routes/orgs/add_member.test.ts +26 -0
  12. package/bun-tests/fake-snippets-api/routes/orgs/create.test.ts +37 -0
  13. package/bun-tests/fake-snippets-api/routes/orgs/get.test.ts +52 -0
  14. package/bun-tests/fake-snippets-api/routes/orgs/list.test.ts +17 -0
  15. package/bun-tests/fake-snippets-api/routes/orgs/list_members.test.ts +23 -0
  16. package/bun-tests/fake-snippets-api/routes/orgs/remove_member.test.ts +81 -0
  17. package/bun-tests/fake-snippets-api/routes/orgs/update.test.ts +99 -0
  18. package/bun-tests/fake-snippets-api/routes/package_builds/get.test.ts +1 -1
  19. package/bun-tests/fake-snippets-api/routes/package_files/create.test.ts +15 -13
  20. package/bun-tests/fake-snippets-api/routes/package_files/create_or_update.test.ts +26 -24
  21. package/bun-tests/fake-snippets-api/routes/package_files/delete.test.ts +9 -9
  22. package/bun-tests/fake-snippets-api/routes/package_files/download.test.ts +4 -4
  23. package/bun-tests/fake-snippets-api/routes/package_files/get.test.ts +38 -28
  24. package/bun-tests/fake-snippets-api/routes/package_files/list.test.ts +23 -15
  25. package/bun-tests/fake-snippets-api/routes/package_releases/create.test.ts +33 -0
  26. package/bun-tests/fake-snippets-api/routes/package_releases/get.test.ts +4 -4
  27. package/bun-tests/fake-snippets-api/routes/package_releases/get_image_generation_fields.test.ts +38 -0
  28. package/bun-tests/fake-snippets-api/routes/packages/create.test.ts +19 -0
  29. package/bun-tests/fake-snippets-api/routes/packages/fork.test.ts +3 -4
  30. package/bun-tests/fake-snippets-api/routes/packages/get.test.ts +30 -0
  31. package/bun-tests/fake-snippets-api/routes/packages/images.test.ts +4 -2
  32. package/bun-tests/fake-snippets-api/routes/packages/list-1.test.ts +34 -0
  33. package/bun.lock +389 -450
  34. package/bunfig.toml +2 -1
  35. package/dist/bundle.js +1255 -625
  36. package/dist/index.d.ts +296 -4
  37. package/dist/index.js +325 -24
  38. package/dist/schema.d.ts +282 -1
  39. package/dist/schema.js +54 -2
  40. package/fake-snippets-api/lib/db/autoload-dev-packages.ts +31 -20
  41. package/fake-snippets-api/lib/db/db-client.ts +214 -3
  42. package/fake-snippets-api/lib/db/schema.ts +62 -0
  43. package/fake-snippets-api/lib/db/seed.ts +100 -0
  44. package/fake-snippets-api/lib/middleware/with-session-auth.ts +1 -1
  45. package/fake-snippets-api/lib/package_file/get-package-file-id-from-file-descriptor.ts +2 -2
  46. package/fake-snippets-api/lib/public-mapping/public-map-org.ts +32 -0
  47. package/fake-snippets-api/lib/public-mapping/public-map-package-build.ts +10 -0
  48. package/fake-snippets-api/lib/public-mapping/public-map-package-release.ts +17 -0
  49. package/fake-snippets-api/routes/api/orgs/add_member.ts +52 -0
  50. package/fake-snippets-api/routes/api/orgs/create.ts +46 -0
  51. package/fake-snippets-api/routes/api/orgs/get.ts +39 -0
  52. package/fake-snippets-api/routes/api/orgs/list.ts +31 -0
  53. package/fake-snippets-api/routes/api/orgs/list_members.ts +67 -0
  54. package/fake-snippets-api/routes/api/orgs/remove_member.ts +46 -0
  55. package/fake-snippets-api/routes/api/orgs/update.ts +93 -0
  56. package/fake-snippets-api/routes/api/package_files/get.ts +3 -6
  57. package/fake-snippets-api/routes/api/package_files/list.ts +7 -4
  58. package/fake-snippets-api/routes/api/packages/create.ts +54 -10
  59. package/fake-snippets-api/routes/api/packages/get.ts +23 -0
  60. package/fake-snippets-api/routes/api/packages/images/[owner_github_username]/[unscoped_name]/[view_format].ts +13 -11
  61. package/fake-snippets-api/routes/api/packages/list.ts +29 -2
  62. package/fake-snippets-api/routes/api/packages/update_ai_description.ts +37 -0
  63. package/package.json +27 -24
  64. package/renovate.json +1 -1
  65. package/scripts/generate-sitemap.ts +1 -1
  66. package/src/App.tsx +29 -10
  67. package/src/ContextProviders.tsx +25 -2
  68. package/src/components/CircuitJsonImportDialog.tsx +1 -1
  69. package/src/components/CmdKMenu.tsx +281 -247
  70. package/src/components/DownloadButtonAndMenu.tsx +133 -36
  71. package/src/components/FileSidebar.tsx +41 -50
  72. package/src/components/Footer.tsx +8 -10
  73. package/src/components/Header.tsx +19 -32
  74. package/src/components/Header2.tsx +16 -32
  75. package/src/components/HeaderDropdown.tsx +13 -8
  76. package/src/components/HeaderLogin.tsx +44 -16
  77. package/src/components/HiddenFilesDropdown.tsx +0 -2
  78. package/src/components/NotFound.tsx +5 -5
  79. package/src/components/PackageBreadcrumb.tsx +6 -12
  80. package/src/components/PackageCard.tsx +0 -1
  81. package/src/components/PackageSearchResults.tsx +1 -1
  82. package/src/components/PrefetchPageLink.tsx +7 -1
  83. package/src/components/ProfileRouter.tsx +32 -0
  84. package/src/components/SearchComponent.tsx +12 -8
  85. package/src/components/UserCard.tsx +80 -0
  86. package/src/components/ViewPackagePage/components/ShikiCodeViewer.tsx +20 -11
  87. package/src/components/ViewPackagePage/components/build-status.tsx +1 -1
  88. package/src/components/ViewPackagePage/components/important-files-view.tsx +174 -87
  89. package/src/components/ViewPackagePage/components/main-content-header.tsx +8 -4
  90. package/src/components/ViewPackagePage/components/main-content-view-selector.tsx +1 -2
  91. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +54 -20
  92. package/src/components/ViewPackagePage/components/package-header.tsx +26 -37
  93. package/src/components/ViewPackagePage/components/preview-image-squares.tsx +11 -19
  94. package/src/components/ViewPackagePage/components/repo-page-content.tsx +33 -25
  95. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +16 -10
  96. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +11 -11
  97. package/src/components/ViewPackagePage/components/sidebar.tsx +0 -2
  98. package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +18 -17
  99. package/src/components/ViewPackagePage/components/tab-views/pcb-view.tsx +1 -2
  100. package/src/components/ViewPackagePage/components/tab-views/schematic-view.tsx +2 -1
  101. package/src/components/ViewPackagePage/components/theme-toggle.tsx +0 -2
  102. package/src/components/ViewPackagePage/hooks/use-toast.tsx +0 -1
  103. package/src/components/dialogs/GitHubRepositorySelector.tsx +56 -49
  104. package/src/components/dialogs/edit-package-details-dialog.tsx +5 -6
  105. package/src/components/dialogs/import-component-dialog.tsx +16 -9
  106. package/src/components/dialogs/import-package-dialog.tsx +3 -2
  107. package/src/components/dialogs/new-package-save-prompt-dialog.tsx +190 -0
  108. package/src/components/organization/OrganizationCard.tsx +204 -0
  109. package/src/components/organization/OrganizationCardSkeleton.tsx +55 -0
  110. package/src/components/organization/OrganizationHeader.tsx +154 -0
  111. package/src/components/organization/OrganizationMembers.tsx +146 -0
  112. package/src/components/package-port/CodeAndPreview.tsx +32 -46
  113. package/src/components/package-port/CodeEditor.tsx +28 -31
  114. package/src/components/package-port/CodeEditorHeader.tsx +128 -63
  115. package/src/components/package-port/EditorNav.tsx +32 -49
  116. package/src/components/preview/ConnectedPackagesList.tsx +8 -8
  117. package/src/components/preview/ConnectedRepoOverview.tsx +102 -2
  118. package/src/components/preview/PackageReleasesDashboard.tsx +53 -36
  119. package/src/components/ui/tree-view.tsx +6 -3
  120. package/src/hooks/use-add-org-member-mutation.ts +51 -0
  121. package/src/hooks/use-create-org-mutation.ts +38 -0
  122. package/src/hooks/use-create-package-mutation.ts +3 -0
  123. package/src/hooks/use-current-package-id.ts +5 -30
  124. package/src/hooks/use-current-package-info.ts +29 -5
  125. package/src/hooks/use-current-package-release.ts +4 -3
  126. package/src/hooks/use-download-zip.ts +2 -2
  127. package/src/hooks/use-global-store.ts +6 -4
  128. package/src/hooks/use-jlcpcb-component-import.tsx +164 -0
  129. package/src/hooks/use-list-org-members.ts +27 -0
  130. package/src/hooks/use-list-user-orgs.ts +25 -0
  131. package/src/hooks/use-org-by-github-handle.ts +26 -0
  132. package/src/hooks/use-org.ts +24 -0
  133. package/src/hooks/use-organization.ts +42 -0
  134. package/src/hooks/use-package-as-snippet.ts +4 -2
  135. package/src/hooks/use-package-builds.ts +6 -2
  136. package/src/hooks/use-package-files.ts +5 -3
  137. package/src/hooks/use-package-release-by-id-or-version.ts +29 -20
  138. package/src/hooks/use-package-release-images.ts +105 -0
  139. package/src/hooks/use-package-release.ts +2 -2
  140. package/src/hooks/use-package-stars.ts +80 -4
  141. package/src/hooks/use-preview-images.ts +6 -3
  142. package/src/hooks/use-remove-org-member-mutation.ts +32 -0
  143. package/src/hooks/use-update-ai-description-mutation.ts +42 -0
  144. package/src/hooks/use-update-org-mutation.ts +41 -0
  145. package/src/hooks/use-warn-user-on-page-change.ts +71 -4
  146. package/src/hooks/useFileManagement.ts +183 -35
  147. package/src/hooks/useOptimizedPackageFilesLoader.ts +136 -0
  148. package/src/hooks/usePackageFilesLoader.ts +2 -2
  149. package/src/hooks/useUpdatePackageFilesMutation.ts +15 -1
  150. package/src/lib/download-fns/download-circuit-png.ts +11 -3
  151. package/src/lib/download-fns/download-gltf-from-circuit-json.ts +44 -0
  152. package/src/lib/download-fns/download-kicad-files.ts +12 -11
  153. package/src/lib/normalize-svg-for-tile.ts +50 -0
  154. package/src/lib/posthog.ts +11 -9
  155. package/src/lib/react-query-api-failure-tracking.ts +148 -0
  156. package/src/lib/sentry.ts +14 -0
  157. package/src/lib/templates/blank-circuit-board-template.ts +0 -4
  158. package/src/lib/ts-lib-cache.ts +122 -7
  159. package/src/lib/utils/checkIfManualEditsImported.ts +4 -4
  160. package/src/lib/utils/findTargetFile.ts +45 -10
  161. package/src/lib/utils/isComponentExported.ts +10 -0
  162. package/src/main.tsx +2 -1
  163. package/src/pages/authorize.tsx +0 -2
  164. package/src/pages/create-organization.tsx +168 -0
  165. package/src/pages/dashboard.tsx +38 -6
  166. package/src/pages/datasheet.tsx +1 -1
  167. package/src/pages/datasheets.tsx +3 -3
  168. package/src/pages/editor.tsx +4 -6
  169. package/src/pages/landing.tsx +6 -7
  170. package/src/pages/latest.tsx +3 -0
  171. package/src/pages/organization-profile.tsx +199 -0
  172. package/src/pages/organization-settings.tsx +566 -0
  173. package/src/pages/package-editor.tsx +21 -21
  174. package/src/pages/preview-release.tsx +76 -136
  175. package/src/pages/quickstart.tsx +159 -123
  176. package/src/pages/release-detail.tsx +119 -31
  177. package/src/pages/search.tsx +192 -57
  178. package/src/pages/settings-redirect.tsx +44 -0
  179. package/src/pages/trending.tsx +29 -20
  180. package/src/pages/user-profile.tsx +58 -7
  181. package/src/pages/view-package.tsx +21 -26
  182. package/vite.config.ts +9 -0
  183. package/fake-snippets-api/routes/api/autocomplete/create_autocomplete.ts +0 -133
  184. package/src/components/Footer2.tsx +0 -100
  185. package/src/components/JLCPCBImportDialog.tsx +0 -280
  186. package/src/components/PackageBuildsPage/LogContent.tsx +0 -72
  187. package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +0 -115
  188. package/src/components/PackageBuildsPage/build-preview-content.tsx +0 -27
  189. package/src/components/PackageBuildsPage/collapsible-section.tsx +0 -63
  190. package/src/components/PackageBuildsPage/package-build-details-panel.tsx +0 -166
  191. package/src/components/PackageBuildsPage/package-build-header.tsx +0 -79
  192. package/src/components/PageSearchComponent.tsx +0 -148
  193. package/src/components/ShippingInformationForm.tsx +0 -423
  194. package/src/components/StaticViewSnippetHeader.tsx +0 -70
  195. package/src/components/ViewPackagePage/components/file-explorer.tsx +0 -67
  196. package/src/components/ViewPackagePage/components/readme-view.tsx +0 -58
  197. package/src/components/ViewPackagePage/components/repo-header-button.tsx +0 -36
  198. package/src/components/ViewPackagePage/components/repo-header.tsx +0 -4
  199. package/src/components/ViewPackagePage/components/sidebar-contributors-section.tsx +0 -31
  200. package/src/components/ViewSnippetHeader.tsx +0 -181
  201. package/src/components/ui/input-otp.tsx +0 -69
  202. package/src/pages/package-builds.tsx +0 -33
  203. 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,33 @@ 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(currentPackage, fileChosen)
77
+
71
78
  const { data: packageFilesMeta, isLoading: isLoadingPackageFiles } =
72
79
  usePackageFiles(currentPackage?.latest_package_release_id)
73
80
  const initialCodeContent = useMemo(() => {
@@ -127,7 +134,7 @@ export function useFileManagement({
127
134
  })
128
135
 
129
136
  useEffect(() => {
130
- if (!currentPackage || isLoadingPackageFilesWithContent) {
137
+ if (!currentPackage || isPriorityLoading) {
131
138
  const decodedFsMap = decodeUrlHashToFsMap(window.location.toString())
132
139
 
133
140
  if (decodedFsMap && Object.keys(decodedFsMap).length > 0) {
@@ -137,38 +144,134 @@ export function useFileManagement({
137
144
  content: String(content),
138
145
  }),
139
146
  )
140
- const targetFile = findTargetFile(filesFromUrl, fileChoosen)
147
+ const targetFile = findTargetFile({
148
+ files: filesFromUrl,
149
+ filePathFromUrl: fileChosen,
150
+ })
141
151
  setLocalFiles(filesFromUrl)
142
152
  setInitialFiles([])
143
153
  setCurrentFile(targetFile?.path || filesFromUrl[0]?.path || null)
144
154
  return
145
155
  }
146
156
 
147
- setLocalFiles([
148
- {
149
- path: "index.tsx",
150
- content: initialCodeContent || "",
151
- },
152
- ])
153
- setInitialFiles([])
154
- setCurrentFile("index.tsx")
157
+ if (!urlParams.package_id) {
158
+ setLocalFiles([
159
+ {
160
+ path: "index.tsx",
161
+ content: initialCodeContent || "",
162
+ },
163
+ ])
164
+ setInitialFiles([])
165
+ setCurrentFile("index.tsx")
166
+ }
155
167
  return
156
- } else {
157
- const targetFile = findTargetFile(
158
- packageFilesWithContent || [],
159
- fileChoosen,
160
- )
161
- setLocalFiles(packageFilesWithContent || [])
162
- setInitialFiles(packageFilesWithContent || [])
163
- setCurrentFile(targetFile?.path || null)
164
168
  }
165
- }, [currentPackage, isLoadingPackageFilesWithContent])
169
+ }, [currentPackage, isPriorityLoading])
170
+
171
+ useEffect(() => {
172
+ if (priorityFile && !isPriorityLoading && currentPackage) {
173
+ setLocalFiles((prev) => {
174
+ const existingIndex = prev.findIndex(
175
+ (f) => f.path === priorityFile.path,
176
+ )
177
+ if (existingIndex >= 0) {
178
+ const updated = [...prev]
179
+ updated[existingIndex] = priorityFile
180
+ return updated
181
+ }
182
+ return [...prev, priorityFile]
183
+ })
184
+
185
+ setCurrentFile((prevCurrentFile) => {
186
+ if (fileChosen && priorityFile.path === fileChosen) {
187
+ return priorityFile.path
188
+ } else if (!prevCurrentFile) {
189
+ return priorityFile.path
190
+ } else {
191
+ // If priority file is index.tsx, always update to it
192
+ const isPriorityFileBetter = priorityFile.path === "index.tsx"
193
+ if (isPriorityFileBetter) {
194
+ return priorityFile.path
195
+ }
196
+ }
197
+ return prevCurrentFile
198
+ })
199
+ }
200
+ }, [priorityFile, isPriorityLoading, currentPackage, fileChosen])
201
+
202
+ useEffect(() => {
203
+ if (packageFilesWithContent.length > 0 && currentPackage) {
204
+ setLocalFiles(packageFilesWithContent)
205
+ setInitialFiles(packageFilesWithContent)
206
+
207
+ if (fileChosen) {
208
+ const targetFile =
209
+ packageFilesWithContent.find((f) => f.path === fileChosen) ||
210
+ findTargetFile({
211
+ files: packageFilesWithContent,
212
+ filePathFromUrl: fileChosen,
213
+ })
214
+ if (targetFile) {
215
+ setCurrentFile((prevCurrentFile) => {
216
+ return targetFile.path !== prevCurrentFile
217
+ ? targetFile.path
218
+ : prevCurrentFile
219
+ })
220
+ }
221
+ } else {
222
+ setCurrentFile((prevCurrentFile) => {
223
+ if (!prevCurrentFile) {
224
+ // Wait for priority file to load before making selection to avoid flicker
225
+ // Only select if we have a good candidate (tsx/ts file) or priority file isn't loading
226
+ const targetFile = findTargetFile({
227
+ files: packageFilesWithContent,
228
+ filePathFromUrl: fileChosen,
229
+ })
230
+ // Only consider it a "good enough" candidate if it's index.tsx
231
+ // Otherwise, wait for the actual priority file (index.tsx) to load
232
+ const isTopPriorityFile =
233
+ targetFile && targetFile.path === "index.tsx"
234
+ const shouldWaitForPriority =
235
+ isPriorityLoading && !isTopPriorityFile
236
+
237
+ if (shouldWaitForPriority) {
238
+ return prevCurrentFile // Keep null to avoid flicker
239
+ }
240
+
241
+ return targetFile ? targetFile.path : prevCurrentFile
242
+ }
243
+ return prevCurrentFile
244
+ })
245
+ }
246
+ }
247
+ }, [
248
+ packageFilesWithContent.length,
249
+ currentPackage,
250
+ fileChosen,
251
+ isPriorityLoading,
252
+ ])
166
253
 
167
254
  const isLoading = useMemo(() => {
168
- return (
169
- isLoadingPackageFilesWithContent || isLoadingPackageFiles || !localFiles
170
- )
171
- }, [isLoadingPackageFilesWithContent, localFiles, isLoadingPackageFiles])
255
+ const waitingForPriorityFile =
256
+ Boolean(urlParams.package_id) && !isPriorityFileFetched
257
+
258
+ const hasPackageWithFilesButNoneLoaded =
259
+ Boolean(urlParams.package_id) &&
260
+ isPriorityFileFetched &&
261
+ totalFilesCount > 0 &&
262
+ localFiles.length === 0
263
+
264
+ return waitingForPriorityFile || hasPackageWithFilesButNoneLoaded
265
+ }, [
266
+ isPriorityFileFetched,
267
+ urlParams.package_id,
268
+ totalFilesCount,
269
+ localFiles.length,
270
+ ])
271
+
272
+ const isFullyLoaded = useMemo(() => {
273
+ return !isLoadingPackageFilesWithContent && !isLoadingPackageFiles
274
+ }, [isLoadingPackageFilesWithContent, isLoadingPackageFiles])
172
275
 
173
276
  const fsMap = useMemo(() => {
174
277
  const map = localFiles.reduce(
@@ -178,6 +281,7 @@ export function useFileManagement({
178
281
  },
179
282
  {} as Record<string, string>,
180
283
  )
284
+
181
285
  return map
182
286
  }, [localFiles, manualEditsFileContent])
183
287
 
@@ -215,7 +319,6 @@ export function useFileManagement({
215
319
  }
216
320
  }
217
321
 
218
- // Check if file already exists
219
322
  const fileExists = localFiles?.some((file) => file.path === newFileName)
220
323
  if (fileExists) {
221
324
  onError(new Error(`File '${newFileName}' already exists`))
@@ -224,7 +327,6 @@ export function useFileManagement({
224
327
  }
225
328
  }
226
329
 
227
- // Ensure file name is not empty after path construction
228
330
  const fileName = newFileName.split("/").pop() || ""
229
331
  if (!fileName.trim()) {
230
332
  onError(new Error("File name cannot be empty"))
@@ -282,7 +384,6 @@ export function useFileManagement({
282
384
  }
283
385
  }
284
386
 
285
- // Extract just the filename from the path for validation
286
387
  const fileNameOnly = newFilename.split("/").pop() || ""
287
388
  if (!isValidFileName(fileNameOnly)) {
288
389
  onError(new Error("Invalid file name"))
@@ -324,7 +425,15 @@ export function useFileManagement({
324
425
  }
325
426
  }
326
427
 
327
- const savePackage = async (isPrivate: boolean) => {
428
+ const savePackage = async ({
429
+ name,
430
+ isPrivate,
431
+ orgId,
432
+ }: {
433
+ name?: string
434
+ isPrivate: boolean
435
+ orgId: string
436
+ }) => {
328
437
  if (!isLoggedIn) {
329
438
  toast({
330
439
  title: "Not Logged In",
@@ -335,6 +444,8 @@ export function useFileManagement({
335
444
 
336
445
  await createPackageMutation.mutateAsync({
337
446
  is_private: isPrivate,
447
+ org_id: orgId,
448
+ ...(name ? { name } : {}),
338
449
  })
339
450
  }
340
451
 
@@ -395,7 +506,6 @@ export function useFileManagement({
395
506
 
396
507
  const saveFiles = () => {
397
508
  if (!isLoggedIn) {
398
- // For non-logged-in users, trigger immediate URL save
399
509
  if (debounceTimeoutRef.current) {
400
510
  clearTimeout(debounceTimeoutRef.current)
401
511
  }
@@ -433,20 +543,58 @@ export function useFileManagement({
433
543
  isCreatingRelease,
434
544
  ])
435
545
 
546
+ const currentFileCode = useMemo(
547
+ () =>
548
+ localFiles.find((x) => x.path === currentFile)?.content ?? DEFAULT_CODE,
549
+ [localFiles, currentFile],
550
+ )
551
+ const mainComponentPath = useMemo(() => {
552
+ const targetFile = findTargetFile({
553
+ files: localFiles,
554
+ filePathFromUrl: fileChosen,
555
+ fallbackToAnyFile: false,
556
+ })?.path
557
+ if (targetFile && !fileChosen && !currentFile) {
558
+ return targetFile
559
+ }
560
+
561
+ const isComponentExportedInCurrentFile =
562
+ isComponentExported(currentFileCode)
563
+
564
+ const selectedComponent =
565
+ (currentFile?.endsWith(".tsx") || currentFile?.endsWith(".ts")) &&
566
+ !!localFiles.some((x) => x.path === currentFile) &&
567
+ isComponentExportedInCurrentFile
568
+ ? currentFile
569
+ : targetFile
570
+
571
+ return selectedComponent
572
+ }, [currentFile, localFiles, currentFileCode])
573
+
574
+ const priorityFileFetched = useMemo(() => {
575
+ return urlParams.package_id && isPriorityFileFetched
576
+ }, [urlParams.package_id, isPriorityFileFetched])
577
+
436
578
  return {
437
579
  fsMap,
438
580
  createFile,
581
+ priorityFileFetched,
439
582
  deleteFile,
440
583
  renameFile,
441
584
  saveFiles,
442
585
  localFiles,
586
+ mainComponentPath,
587
+ currentFileCode,
443
588
  initialFiles,
444
589
  currentFile,
445
590
  setLocalFiles,
446
591
  onFileSelect,
447
592
  isLoading,
593
+ isFullyLoaded,
448
594
  isSaving,
449
595
  savePackage,
450
596
  packageFilesMeta,
597
+ totalFilesCount,
598
+ loadedFilesCount,
451
599
  }
452
600
  }
@@ -0,0 +1,136 @@
1
+ import { useQuery, useQueries } from "react-query"
2
+ import { useAxios } from "@/hooks/use-axios"
3
+ import { usePackageFiles } from "@/hooks/use-package-files"
4
+ import type { Package } from "fake-snippets-api/lib/db/schema"
5
+ import { useState, useMemo } from "react"
6
+
7
+ export interface PackageFile {
8
+ path: string
9
+ content: string
10
+ }
11
+
12
+ export interface OptimizedLoadingState {
13
+ priorityFile: PackageFile | null
14
+ allFiles: PackageFile[]
15
+ isPriorityLoading: boolean
16
+ areAllFilesLoading: boolean
17
+ error: any
18
+ }
19
+
20
+ export function useOptimizedPackageFilesLoader(
21
+ pkg?: Package,
22
+ priorityFilePath?: string | null,
23
+ ) {
24
+ const axios = useAxios()
25
+ const [loadedFiles, setLoadedFiles] = useState<Map<string, PackageFile>>(
26
+ new Map(),
27
+ )
28
+
29
+ const pkgFiles = usePackageFiles(pkg?.latest_package_release_id)
30
+
31
+ const targetFilePath = useMemo(() => {
32
+ if (!pkgFiles.data) return priorityFilePath
33
+
34
+ if (priorityFilePath) {
35
+ const exactMatch = pkgFiles.data.find(
36
+ (f) => f.file_path === priorityFilePath,
37
+ )
38
+ if (exactMatch) return exactMatch.file_path
39
+
40
+ const partialMatch = pkgFiles.data.find(
41
+ (f) =>
42
+ f.file_path.includes(priorityFilePath) ||
43
+ priorityFilePath.includes(f.file_path),
44
+ )
45
+ if (partialMatch) return partialMatch.file_path
46
+ }
47
+
48
+ // Check for index.tsx first
49
+ const indexFile = pkgFiles.data.find((f) => f.file_path === "index.tsx")
50
+ if (indexFile) return indexFile.file_path
51
+
52
+ // Fallback to first file
53
+ return pkgFiles.data[0]?.file_path || null
54
+ }, [pkgFiles.data, priorityFilePath])
55
+
56
+ const priorityFileData = pkgFiles.data?.find(
57
+ (file) => file.file_path === targetFilePath,
58
+ )
59
+
60
+ const priorityFileQuery = useQuery({
61
+ queryKey: ["priorityPackageFile", priorityFileData?.package_file_id],
62
+ queryFn: async () => {
63
+ if (!priorityFileData) return null
64
+
65
+ const response = await axios.get(`/package_files/get`, {
66
+ params: { package_file_id: priorityFileData.package_file_id },
67
+ })
68
+ const content = response.data.package_file?.content_text
69
+ const file = { path: priorityFileData.file_path, content: content ?? "" }
70
+
71
+ setLoadedFiles((prev) => {
72
+ const newMap = new Map(prev)
73
+ newMap.set(file.path, file)
74
+ return newMap
75
+ })
76
+
77
+ return file
78
+ },
79
+ enabled: !!priorityFileData,
80
+ refetchOnWindowFocus: false,
81
+ refetchOnMount: true,
82
+ staleTime: 0,
83
+ cacheTime: Infinity,
84
+ })
85
+
86
+ const remainingFilesQueries = useQueries(
87
+ pkgFiles.data
88
+ ?.filter((file) => file.file_path !== targetFilePath)
89
+ ?.map((file) => ({
90
+ queryKey: ["packageFile", file.package_file_id],
91
+ queryFn: async () => {
92
+ const response = await axios.get(`/package_files/get`, {
93
+ params: { package_file_id: file.package_file_id },
94
+ })
95
+ const content = response.data.package_file?.content_text
96
+ const fileData = { path: file.file_path, content: content ?? "" }
97
+
98
+ setLoadedFiles((prev) => {
99
+ const newMap = new Map(prev)
100
+ newMap.set(fileData.path, fileData)
101
+ return newMap
102
+ })
103
+
104
+ return fileData
105
+ },
106
+ refetchOnWindowFocus: false,
107
+ refetchOnMount: true,
108
+ staleTime: 0,
109
+ cacheTime: Infinity,
110
+ })) ?? [],
111
+ )
112
+
113
+ const allFiles = useMemo(() => {
114
+ const files = Array.from(loadedFiles.values())
115
+ return files
116
+ }, [loadedFiles])
117
+
118
+ const areAllFilesLoading =
119
+ remainingFilesQueries.some((q) => q.isLoading) ||
120
+ priorityFileQuery.isLoading
121
+ const error =
122
+ priorityFileQuery.error || remainingFilesQueries.find((q) => q.error)?.error
123
+
124
+ return {
125
+ priorityFile: priorityFileQuery.data || null,
126
+ priorityFileFetched: priorityFileQuery.isFetched,
127
+ allFiles,
128
+ isPriorityLoading: priorityFileQuery.isLoading,
129
+ areAllFilesLoading,
130
+ error,
131
+ isMetaLoading: pkgFiles.isLoading,
132
+ totalFilesCount: pkgFiles.data?.length || 0,
133
+ loadedFilesCount: allFiles.length,
134
+ isPriorityFileFetched: priorityFileQuery.isFetched,
135
+ }
136
+ }
@@ -30,8 +30,8 @@ export function usePackageFilesLoader(pkg?: Package) {
30
30
  }
31
31
  }
32
32
 
33
- const response = await axios.post(`/package_files/get`, {
34
- package_file_id: file.package_file_id,
33
+ const response = await axios.get(`/package_files/get`, {
34
+ params: { package_file_id: file.package_file_id },
35
35
  })
36
36
  const content = response.data.package_file?.content_text
37
37
  return { path: file.file_path, content: content ?? "" }
@@ -1,7 +1,8 @@
1
- import { useMutation } from "react-query"
1
+ import { useMutation, useQueryClient } 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
@@ -28,6 +29,7 @@ export function useUpdatePackageFilesMutation({
28
29
  }: UseUpdatePackageFilesMutationProps) {
29
30
  const axios = useAxios()
30
31
  const { toast } = useToast()
32
+ const queryClient = useQueryClient()
31
33
  return useMutation({
32
34
  mutationFn: async (
33
35
  newPackage: Pick<Package, "package_id" | "name"> & {
@@ -42,6 +44,7 @@ export function useUpdatePackageFilesMutation({
42
44
  let updatedFilesCount = 0
43
45
 
44
46
  for (const file of localFiles) {
47
+ if (isHiddenFile(file.path)) continue
45
48
  const initialFile = initialFiles.find((x) => x.path === file.path)
46
49
  if (file.content !== initialFile?.content) {
47
50
  const updatePkgFilePayload = {
@@ -93,6 +96,17 @@ export function useUpdatePackageFilesMutation({
93
96
  title: `Package's ${updatedFilesCount} files saved`,
94
97
  description: "Your changes have been saved successfully.",
95
98
  })
99
+ queryClient.invalidateQueries({
100
+ predicate: (q) => {
101
+ const key = q.queryKey as any
102
+ return (
103
+ Array.isArray(key) &&
104
+ (key[0] === "packageFiles" ||
105
+ key[0] === "packageFile" ||
106
+ key[0] === "priorityPackageFile")
107
+ )
108
+ },
109
+ })
96
110
  }
97
111
  },
98
112
  onError: (error: any) => {
@@ -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 ArrayBuffer
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
+ }
@@ -1,6 +1,8 @@
1
1
  import { saveAs } from "file-saver"
2
- import { convertCircuitJsonToKiCadPcb } from "kicad-converter"
3
- import { convertCircuitJsonToKicadPro } from "kicad-converter"
2
+ import {
3
+ CircuitJsonToKicadPcbConverter,
4
+ CircuitJsonToKicadSchConverter,
5
+ } from "circuit-json-to-kicad"
4
6
  import { AnyCircuitElement } from "circuit-json"
5
7
  import JSZip from "jszip"
6
8
 
@@ -8,18 +10,17 @@ export const downloadKicadFiles = (
8
10
  circuitJson: AnyCircuitElement[],
9
11
  fileName: string,
10
12
  ) => {
11
- const kicadPcbString = convertCircuitJsonToKiCadPcb(circuitJson as any)
12
- const pcbContent =
13
- typeof kicadPcbString === "object"
14
- ? JSON.stringify(kicadPcbString)
15
- : kicadPcbString
13
+ const pcbConverter = new CircuitJsonToKicadPcbConverter(circuitJson)
14
+ pcbConverter.runUntilFinished()
15
+ const kicadPcbContent = pcbConverter.getOutputString()
16
16
 
17
- const kicadProContent = convertCircuitJsonToKicadPro(circuitJson as any)
18
- const proContent = JSON.stringify(kicadProContent)
17
+ const schConverter = new CircuitJsonToKicadSchConverter(circuitJson)
18
+ schConverter.runUntilFinished()
19
+ const kicadSchContent = schConverter.getOutputString()
19
20
 
20
21
  const zip = new JSZip()
21
- zip.file(`${fileName}.kicad_pcb`, pcbContent)
22
- zip.file(`${fileName}.kicad_pro`, proContent)
22
+ zip.file(`${fileName}.kicad_pcb`, kicadPcbContent)
23
+ zip.file(`${fileName}.kicad_sch`, kicadSchContent)
23
24
 
24
25
  zip.generateAsync({ type: "blob" }).then((content) => {
25
26
  saveAs(content, `${fileName}_kicad.zip`)