@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
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import { useState, useEffect, useMemo, useCallback } from "react"
4
2
  import {
5
3
  Edit,
@@ -14,30 +12,31 @@ import {
14
12
  import { Skeleton } from "@/components/ui/skeleton"
15
13
  import { Button } from "@/components/ui/button"
16
14
  import { usePackageFile } from "@/hooks/use-package-files"
17
- import { ShikiCodeViewer } from "./ShikiCodeViewer"
15
+ import { ShikiCodeViewer, SKELETON_WIDTHS } from "./ShikiCodeViewer"
18
16
  import MarkdownViewer from "./markdown-viewer"
19
17
  import { useGlobalStore } from "@/hooks/use-global-store"
20
18
  import { useCurrentPackageCircuitJson } from "../hooks/use-current-package-circuit-json"
19
+ import { useOrganization } from "@/hooks/use-organization"
20
+ import { Package } from "fake-snippets-api/lib/db/schema"
21
21
 
22
22
  interface PackageFile {
23
23
  package_file_id: string
24
24
  package_release_id: string
25
25
  file_path: string
26
26
  created_at: string
27
- content_text?: string
27
+ content_text?: string | null
28
28
  }
29
29
 
30
30
  interface ImportantFilesViewProps {
31
31
  importantFiles?: PackageFile[]
32
- isLoading?: boolean
32
+ isFetched?: boolean
33
33
  onEditClicked?: (file_path?: string | null) => void
34
- packageAuthorOwner?: string | null
35
- aiDescription?: string
36
- aiUsageInstructions?: string
37
34
  aiReviewText?: string | null
38
35
  aiReviewRequested?: boolean
39
36
  onRequestAiReview?: () => void
37
+ onRequestAiDescriptionUpdate?: () => void
40
38
  onLicenseFileRequested?: boolean
39
+ pkg?: Package
41
40
  }
42
41
 
43
42
  type TabType = "ai" | "ai-review" | "file"
@@ -51,30 +50,44 @@ interface TabInfo {
51
50
 
52
51
  export default function ImportantFilesView({
53
52
  importantFiles = [],
54
- aiDescription,
55
- aiUsageInstructions,
56
53
  aiReviewText,
57
54
  aiReviewRequested,
58
55
  onRequestAiReview,
59
- isLoading = false,
56
+ onRequestAiDescriptionUpdate,
57
+ isFetched = false,
60
58
  onEditClicked,
61
- packageAuthorOwner,
62
59
  onLicenseFileRequested,
60
+ pkg,
63
61
  }: ImportantFilesViewProps) {
64
62
  const [activeTab, setActiveTab] = useState<TabInfo | null>(null)
65
63
  const [copyState, setCopyState] = useState<"copy" | "copied">("copy")
66
64
  const { session: user } = useGlobalStore()
67
65
 
66
+ const { organization } = useOrganization(
67
+ pkg?.owner_org_id
68
+ ? { orgId: String(pkg.owner_org_id) }
69
+ : pkg?.owner_github_username
70
+ ? { github_handle: pkg.owner_github_username }
71
+ : {},
72
+ )
73
+
68
74
  // Memoized computed values
69
75
  const hasAiContent = useMemo(
70
- () => Boolean(aiDescription || aiUsageInstructions),
71
- [aiDescription, aiUsageInstructions],
76
+ () => Boolean(pkg?.ai_description || pkg?.ai_usage_instructions),
77
+ [pkg?.ai_description, pkg?.ai_usage_instructions],
72
78
  )
73
79
  const hasAiReview = useMemo(() => Boolean(aiReviewText), [aiReviewText])
74
80
  const isOwner = useMemo(
75
- () => user?.github_username === packageAuthorOwner,
76
- [user?.github_username, packageAuthorOwner],
81
+ () => user?.github_username === pkg?.owner_github_username,
82
+ [user?.github_username, pkg?.owner_github_username],
77
83
  )
84
+ const canManagePackage = useMemo(() => {
85
+ if (isOwner) return isOwner
86
+ if (organization) {
87
+ return organization.user_permissions?.can_manage_package
88
+ }
89
+ return false
90
+ }, [isOwner, organization])
78
91
 
79
92
  // File type utilities
80
93
  const isLicenseFile = useCallback((filePath: string) => {
@@ -130,19 +143,25 @@ export default function ImportantFilesView({
130
143
  const availableTabs = useMemo((): TabInfo[] => {
131
144
  const tabs: TabInfo[] = []
132
145
 
133
- tabs.push({
134
- type: "ai",
135
- filePath: null,
136
- label: "Description",
137
- icon: <SparklesIcon className="h-3.5 w-3.5 mr-1.5" />,
138
- })
146
+ // Only show AI description tab if there's actual AI content
147
+ if (hasAiContent) {
148
+ tabs.push({
149
+ type: "ai",
150
+ filePath: null,
151
+ label: "Description",
152
+ icon: <SparklesIcon className="h-3.5 w-3.5 mr-1.5" />,
153
+ })
154
+ }
139
155
 
140
- tabs.push({
141
- type: "ai-review",
142
- filePath: null,
143
- label: "AI Review",
144
- icon: <SparklesIcon className="h-3.5 w-3.5 mr-1.5" />,
145
- })
156
+ // Only show AI review tab if there's actual AI review content
157
+ if (hasAiReview || canManagePackage) {
158
+ tabs.push({
159
+ type: "ai-review",
160
+ filePath: null,
161
+ label: "AI Review",
162
+ icon: <SparklesIcon className="h-3.5 w-3.5 mr-1.5" />,
163
+ })
164
+ }
146
165
 
147
166
  importantFiles.forEach((file) => {
148
167
  tabs.push({
@@ -154,11 +173,11 @@ export default function ImportantFilesView({
154
173
  })
155
174
 
156
175
  return tabs
157
- }, [hasAiContent, importantFiles, getFileName, getFileIcon])
176
+ }, [hasAiContent, hasAiReview, importantFiles, getFileName, getFileIcon])
158
177
 
159
178
  // Find default tab with fallback logic
160
179
  const getDefaultTab = useCallback((): TabInfo | null => {
161
- if (isLoading || availableTabs.length === 0) return null
180
+ if (!isFetched || availableTabs.length === 0) return null
162
181
 
163
182
  // Priority 1: README file
164
183
  const readmeTab = availableTabs.find(
@@ -167,12 +186,14 @@ export default function ImportantFilesView({
167
186
  )
168
187
  if (readmeTab) return readmeTab
169
188
 
170
- // Priority 2: AI content
171
- const aiTab = availableTabs.find((tab) => tab.type === "ai")
189
+ // Priority 2: AI content (only if available)
190
+ const aiTab = availableTabs.find((tab) => tab.type === "ai" && hasAiContent)
172
191
  if (aiTab) return aiTab
173
192
 
174
193
  // Priority 3: AI review
175
- const aiReviewTab = availableTabs.find((tab) => tab.type === "ai-review")
194
+ const aiReviewTab = availableTabs.find(
195
+ (tab) => tab.type === "ai-review" && hasAiReview,
196
+ )
176
197
  if (aiReviewTab) return aiReviewTab
177
198
 
178
199
  // Priority 4: First file
@@ -180,24 +201,7 @@ export default function ImportantFilesView({
180
201
  if (firstFileTab) return firstFileTab
181
202
 
182
203
  return null
183
- }, [isLoading, availableTabs, isReadmeFile])
184
-
185
- // Handle copy functionality
186
- const handleCopy = useCallback(() => {
187
- let textToCopy = ""
188
-
189
- if (activeTab?.type === "ai-review" && aiReviewText) {
190
- textToCopy = aiReviewText
191
- } else if (activeTab?.type === "file" && activeFileContent) {
192
- textToCopy = activeFileContent
193
- }
194
-
195
- if (textToCopy) {
196
- navigator.clipboard.writeText(textToCopy)
197
- setCopyState("copied")
198
- setTimeout(() => setCopyState("copy"), 500)
199
- }
200
- }, [activeTab, aiReviewText])
204
+ }, [isFetched, availableTabs, isReadmeFile])
201
205
 
202
206
  // Handle tab selection with validation
203
207
  const selectTab = useCallback(
@@ -245,11 +249,11 @@ export default function ImportantFilesView({
245
249
 
246
250
  // Set default tab when no tab is active
247
251
  useEffect(() => {
248
- if (activeTab === null && !isLoading) {
252
+ if (activeTab === null && isFetched) {
249
253
  const defaultTab = getDefaultTab()
250
254
  setActiveTab(defaultTab)
251
255
  }
252
- }, [activeTab, isLoading, getDefaultTab])
256
+ }, [activeTab, isFetched, getDefaultTab])
253
257
 
254
258
  // Validate active tab still exists (handles file deletion)
255
259
  useEffect(() => {
@@ -273,38 +277,95 @@ export default function ImportantFilesView({
273
277
  return importantFiles.find((file) => file.file_path === activeTab.filePath)
274
278
  }, [activeTab, importantFiles])
275
279
 
276
- const { data: activeFileFull } = usePackageFile(
277
- partialActiveFile
278
- ? {
279
- file_path: partialActiveFile.file_path,
280
- package_release_id: partialActiveFile.package_release_id,
281
- }
282
- : null,
283
- { keepPreviousData: true },
284
- )
280
+ const { data: activeFileFull, isFetched: isActiveFileFetched } =
281
+ usePackageFile(
282
+ partialActiveFile
283
+ ? {
284
+ file_path: partialActiveFile.file_path,
285
+ package_release_id: partialActiveFile.package_release_id,
286
+ }
287
+ : null,
288
+ {
289
+ keepPreviousData: true,
290
+ staleTime: Infinity,
291
+ refetchOnMount: false,
292
+ refetchOnWindowFocus: false,
293
+ refetchOnReconnect: false,
294
+ },
295
+ )
285
296
 
286
297
  const activeFileContent = activeFileFull?.content_text || ""
287
298
 
299
+ const handleCopy = () => {
300
+ let textToCopy = ""
301
+
302
+ if (activeTab?.type === "ai-review" && aiReviewText) {
303
+ textToCopy = aiReviewText
304
+ } else if (
305
+ activeTab?.type === "ai" &&
306
+ (pkg?.ai_description || pkg?.ai_usage_instructions)
307
+ ) {
308
+ const parts = []
309
+ if (pkg?.ai_description)
310
+ parts.push(`# Description\n\n${pkg?.ai_description}`)
311
+ if (pkg?.ai_usage_instructions)
312
+ parts.push(`# Instructions\n\n${pkg?.ai_usage_instructions}`)
313
+ textToCopy = parts.join("\n\n")
314
+ } else if (activeTab?.type === "file" && activeFileContent) {
315
+ textToCopy = activeFileContent
316
+ }
317
+
318
+ if (textToCopy) {
319
+ navigator.clipboard.writeText(textToCopy)
320
+ setCopyState("copied")
321
+ setTimeout(() => setCopyState("copy"), 500)
322
+ }
323
+ }
324
+
288
325
  // Render content based on active tab
289
- const renderAiContent = useCallback(
290
- () => (
326
+ const renderAiContent = useCallback(() => {
327
+ if (!pkg?.ai_description && !pkg?.ai_usage_instructions) {
328
+ return (
329
+ <div className="flex flex-col items-center justify-center py-8 px-4">
330
+ <div className="text-center space-y-4 max-w-md">
331
+ <div className="flex justify-center">
332
+ <div className="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center">
333
+ <Loader2 className="h-6 w-6 text-gray-600 animate-spin" />
334
+ </div>
335
+ </div>
336
+ <div className="space-y-2">
337
+ <p className="text-sm text-gray-600">
338
+ Our AI is generating a description for your package. This
339
+ usually takes a few minutes. Please check back shortly.
340
+ </p>
341
+ </div>
342
+ </div>
343
+ </div>
344
+ )
345
+ }
346
+
347
+ return (
291
348
  <div className="markdown-content">
292
- {aiDescription && (
349
+ {pkg?.ai_description && (
293
350
  <div className="mb-6">
294
351
  <h3 className="font-semibold text-lg mb-2">Description</h3>
295
- <MarkdownViewer markdownContent={aiDescription} />
352
+ <MarkdownViewer markdownContent={pkg?.ai_description} />
296
353
  </div>
297
354
  )}
298
- {aiUsageInstructions && (
355
+ {pkg?.ai_usage_instructions && (
299
356
  <div>
300
357
  <h3 className="font-semibold text-lg mb-2">Instructions</h3>
301
- <MarkdownViewer markdownContent={aiUsageInstructions} />
358
+ <MarkdownViewer markdownContent={pkg?.ai_usage_instructions} />
302
359
  </div>
303
360
  )}
304
361
  </div>
305
- ),
306
- [aiDescription, aiUsageInstructions],
307
- )
362
+ )
363
+ }, [
364
+ pkg?.ai_description,
365
+ pkg?.ai_usage_instructions,
366
+ canManagePackage,
367
+ onRequestAiDescriptionUpdate,
368
+ ])
308
369
 
309
370
  const renderAiReviewContent = useCallback(() => {
310
371
  if (!aiReviewText && !aiReviewRequested) {
@@ -322,7 +383,7 @@ export default function ImportantFilesView({
322
383
  from our AI assistant.
323
384
  </p>
324
385
  </div>
325
- {!isOwner ? (
386
+ {!canManagePackage ? (
326
387
  <p className="text-sm text-gray-500">
327
388
  Only the package owner can generate an AI review
328
389
  </p>
@@ -366,30 +427,42 @@ export default function ImportantFilesView({
366
427
  }
367
428
 
368
429
  return <MarkdownViewer markdownContent={aiReviewText || ""} />
369
- }, [aiReviewText, aiReviewRequested, isOwner, onRequestAiReview])
430
+ }, [aiReviewText, aiReviewRequested, canManagePackage, onRequestAiReview])
370
431
 
371
432
  const renderFileContent = useCallback(() => {
372
- if (!activeTab?.filePath || !activeFileContent) {
373
- return <pre className="whitespace-pre-wrap">{activeFileContent}</pre>
433
+ if (!isActiveFileFetched || !activeTab?.filePath || !activeFileContent) {
434
+ return (
435
+ <div className="text-sm p-4">
436
+ {SKELETON_WIDTHS.map((w, i) => (
437
+ <Skeleton key={i} className={`h-4 mb-2 ${w}`} />
438
+ ))}
439
+ </div>
440
+ )
374
441
  }
375
442
 
376
- if (isMarkdownFile(activeTab.filePath)) {
443
+ if (isMarkdownFile(String(activeTab?.filePath))) {
377
444
  return <MarkdownViewer markdownContent={activeFileContent} />
378
445
  }
379
446
 
380
- if (isCodeFile(activeTab.filePath)) {
447
+ if (isCodeFile(String(activeTab?.filePath))) {
381
448
  return (
382
- <div className="overflow-x-auto">
449
+ <div className="overflow-x-auto no-scrollbar">
383
450
  <ShikiCodeViewer
384
451
  code={activeFileContent}
385
- filePath={activeTab.filePath}
452
+ filePath={String(activeTab?.filePath)}
386
453
  />
387
454
  </div>
388
455
  )
389
456
  }
390
457
 
391
458
  return <pre className="whitespace-pre-wrap">{activeFileContent}</pre>
392
- }, [activeTab, activeFileContent, isMarkdownFile, isCodeFile])
459
+ }, [
460
+ activeTab,
461
+ activeFileContent,
462
+ isActiveFileFetched,
463
+ isMarkdownFile,
464
+ isCodeFile,
465
+ ])
393
466
 
394
467
  const renderTabContent = useCallback(() => {
395
468
  if (!activeTab) return null
@@ -422,7 +495,7 @@ export default function ImportantFilesView({
422
495
  [activeTab],
423
496
  )
424
497
 
425
- if (isLoading) {
498
+ if (!isFetched) {
426
499
  return (
427
500
  <div className="mt-4 border border-gray-200 dark:border-[#30363d] rounded-md overflow-hidden">
428
501
  <div className="flex items-center pl-2 pr-4 py-2 bg-gray-100 dark:bg-[#161b22] border-b border-gray-200 dark:border-[#30363d]">
@@ -484,7 +557,9 @@ export default function ImportantFilesView({
484
557
  </div>
485
558
  <div className="ml-auto flex items-center">
486
559
  {((activeTab?.type === "file" && activeFileContent) ||
487
- (activeTab?.type === "ai-review" && aiReviewText)) && (
560
+ (activeTab?.type === "ai-review" && aiReviewText) ||
561
+ (activeTab?.type === "ai" &&
562
+ (pkg?.ai_description || pkg?.ai_usage_instructions))) && (
488
563
  <button
489
564
  className="hover:bg-gray-200 dark:hover:bg-[#30363d] p-1 rounded-md transition-all duration-300"
490
565
  onClick={handleCopy}
@@ -497,17 +572,29 @@ export default function ImportantFilesView({
497
572
  <span className="sr-only">Copy</span>
498
573
  </button>
499
574
  )}
500
- {activeTab?.type === "ai-review" && aiReviewText && isOwner && (
575
+ {activeTab?.type === "ai-review" &&
576
+ aiReviewText &&
577
+ canManagePackage && (
578
+ <button
579
+ className="hover:bg-gray-200 dark:hover:bg-[#30363d] p-1 rounded-md ml-1"
580
+ onClick={onRequestAiReview}
581
+ title="Re-request AI Review"
582
+ >
583
+ <RefreshCcwIcon className="h-4 w-4" />
584
+ <span className="sr-only">Re-request AI Review</span>
585
+ </button>
586
+ )}
587
+ {activeTab?.type === "ai" && hasAiContent && canManagePackage && (
501
588
  <button
502
589
  className="hover:bg-gray-200 dark:hover:bg-[#30363d] p-1 rounded-md ml-1"
503
- onClick={onRequestAiReview}
504
- title="Re-request AI Review"
590
+ onClick={onRequestAiDescriptionUpdate}
591
+ title="Regenerate AI Description"
505
592
  >
506
593
  <RefreshCcwIcon className="h-4 w-4" />
507
- <span className="sr-only">Re-request AI Review</span>
594
+ <span className="sr-only">Regenerate AI Description</span>
508
595
  </button>
509
596
  )}
510
- {activeTab?.type === "file" && (
597
+ {activeTab?.type === "file" && canManagePackage && (
511
598
  <button
512
599
  className="hover:bg-gray-200 dark:hover:bg-[#30363d] p-1 rounded-md"
513
600
  onClick={() => onEditClicked?.(activeTab.filePath)}
@@ -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 {
@@ -28,6 +26,7 @@ import { useLocation } from "wouter"
28
26
  import { Package, PackageFile } from "fake-snippets-api/lib/db/schema"
29
27
  import { usePackageFiles } from "@/hooks/use-package-files"
30
28
  import { useDownloadZip } from "@/hooks/use-download-zip"
29
+ import { useToast } from "@/hooks/use-toast"
31
30
  interface MainContentHeaderProps {
32
31
  packageFiles: PackageFile[]
33
32
  activeView: string
@@ -65,10 +64,15 @@ export default function MainContentHeader({
65
64
  }
66
65
 
67
66
  const { downloadZip } = useDownloadZip()
67
+ const { toastLibrary } = useToast()
68
68
 
69
69
  const handleDownloadZip = () => {
70
70
  if (packageInfo && packageFiles) {
71
- downloadZip(packageInfo, packageFiles)
71
+ toastLibrary.promise(downloadZip(packageInfo, packageFiles), {
72
+ loading: "Downloading ZIP...",
73
+ success: "ZIP downloaded successfully!",
74
+ error: "Failed to download ZIP",
75
+ })
72
76
  }
73
77
  }
74
78
 
@@ -101,7 +105,7 @@ export default function MainContentHeader({
101
105
  <ChevronDown className="h-4 w-4 ml-0.5" />
102
106
  </Button>
103
107
  </DropdownMenuTrigger>
104
- <DropdownMenuContent align="end" className="w-72">
108
+ <DropdownMenuContent align="end" className="w-72 relative z-[101]">
105
109
  <DropdownMenuItem disabled={!Boolean(packageInfo)} asChild>
106
110
  <a
107
111
  href={`/editor?package_id=${packageInfo?.package_id}`}
@@ -1,4 +1,3 @@
1
- "use client"
2
1
  import React from "react"
3
2
  import {
4
3
  Code,
@@ -131,7 +130,7 @@ export default function MainContentViewSelector({
131
130
  </svg>
132
131
  </Button>
133
132
  </DropdownMenuTrigger>
134
- <DropdownMenuContent align="start">
133
+ <DropdownMenuContent align="start" className="z-[101]">
135
134
  <TooltipProvider>
136
135
  {views.map((view) => {
137
136
  const isDisabled = !circuitJson && view.requiresCircuitJson
@@ -1,14 +1,18 @@
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"
4
+ import { usePackageReleaseImages } from "@/hooks/use-package-release-images"
5
5
  import { usePreviewImages } from "@/hooks/use-preview-images"
6
6
  import { useGlobalStore } from "@/hooks/use-global-store"
7
7
  import { Button } from "@/components/ui/button"
8
8
  import { useEditPackageDetailsDialog } from "@/components/dialogs/edit-package-details-dialog"
9
9
  import React, { useState, useEffect, useMemo, useCallback } from "react"
10
+ import {
11
+ normalizeSvgForSquareTile,
12
+ svgToDataUrl,
13
+ } from "@/lib/normalize-svg-for-tile"
10
14
  import { useCurrentPackageInfo } from "@/hooks/use-current-package-info"
11
- import { usePackageFile } from "@/hooks/use-package-files"
15
+ import { usePackageFileById, usePackageFiles } from "@/hooks/use-package-files"
12
16
  import { getLicenseFromLicenseContent } from "@/lib/getLicenseFromLicenseContent"
13
17
 
14
18
  interface MobileSidebarProps {
@@ -21,19 +25,25 @@ const MobileSidebar = ({
21
25
  onViewChange,
22
26
  }: MobileSidebarProps) => {
23
27
  const { packageInfo, refetch: refetchPackageInfo } = useCurrentPackageInfo()
24
- const { data: licenseFileMeta } = usePackageFile({
25
- package_release_id: packageInfo?.latest_package_release_id ?? "",
26
- file_path: "LICENSE",
27
- })
28
+ const { data: releaseFiles } = usePackageFiles(
29
+ packageInfo?.latest_package_release_id,
30
+ )
31
+ const licenseFileId = useMemo(() => {
32
+ return (
33
+ releaseFiles?.find((f) => f.file_path === "LICENSE")?.package_file_id ||
34
+ null
35
+ )
36
+ }, [releaseFiles])
37
+ const { data: licenseFileMeta } = usePackageFileById(licenseFileId)
28
38
  const currentLicense = useMemo(() => {
29
39
  if (packageInfo?.latest_license) {
30
40
  return packageInfo?.latest_license
31
41
  }
32
42
  if (licenseFileMeta?.content_text) {
33
- return getLicenseFromLicenseContent(licenseFileMeta?.content_text)
43
+ return getLicenseFromLicenseContent(licenseFileMeta.content_text)
34
44
  }
35
45
  return undefined
36
- }, [licenseFileMeta?.content_text, packageInfo?.latest_license])
46
+ }, [licenseFileMeta, packageInfo?.latest_license])
37
47
  const topics = useMemo(
38
48
  () => (packageInfo?.is_package ? ["Package"] : ["Board"]),
39
49
  [packageInfo?.is_package],
@@ -70,11 +80,21 @@ const MobileSidebar = ({
70
80
  [refetchPackageInfo],
71
81
  )
72
82
 
73
- const { availableViews } = usePreviewImages({
83
+ const { availableViews: imageViews } = usePackageReleaseImages({
84
+ packageReleaseId: packageInfo?.latest_package_release_id,
85
+ })
86
+
87
+ const { availableViews: pngViews } = usePreviewImages({
74
88
  packageName: packageInfo?.name,
75
89
  fsMapHash: packageInfo?.latest_package_release_fs_sha ?? "",
76
90
  })
77
91
 
92
+ const viewsToRender =
93
+ imageViews.length === 0 ||
94
+ imageViews.every((v) => !v.isLoading && !v.imageUrl)
95
+ ? (pngViews as any)
96
+ : imageViews
97
+
78
98
  const handleViewClick = useCallback(
79
99
  (viewId: string) => {
80
100
  onViewChange?.(viewId as "3d" | "pcb" | "schematic")
@@ -186,11 +206,14 @@ const MobileSidebar = ({
186
206
  </div>
187
207
 
188
208
  <div className="grid grid-cols-3 gap-2">
189
- {availableViews.map((view) => (
209
+ {viewsToRender.map((view: any) => (
190
210
  <PreviewButton
191
211
  key={view.id}
192
212
  view={view.label}
193
213
  onClick={() => handleViewClick(view.id)}
214
+ backgroundClass={view.backgroundClass}
215
+ svg={view.svg}
216
+ isLoading={view.isLoading}
194
217
  imageUrl={view.imageUrl}
195
218
  status={view.status}
196
219
  onLoad={view.onLoad}
@@ -226,6 +249,9 @@ export default React.memo(MobileSidebar)
226
249
  function PreviewButton({
227
250
  view,
228
251
  onClick,
252
+ backgroundClass,
253
+ svg,
254
+ isLoading,
229
255
  imageUrl,
230
256
  status,
231
257
  onLoad,
@@ -233,30 +259,38 @@ function PreviewButton({
233
259
  }: {
234
260
  view: string
235
261
  onClick: () => void
262
+ backgroundClass?: string
263
+ svg?: string | null
264
+ isLoading?: boolean
236
265
  imageUrl?: string
237
- status: "loading" | "loaded" | "error"
238
- onLoad: () => void
239
- onError: () => void
266
+ status?: "loading" | "loaded" | "error"
267
+ onLoad?: () => void
268
+ onError?: () => void
240
269
  }) {
241
- if (status === "error") {
270
+ if (!svg && !isLoading && !imageUrl) {
242
271
  return null
243
272
  }
244
273
 
245
274
  return (
246
275
  <button
247
276
  onClick={onClick}
248
- className="aspect-square bg-gray-100 dark:bg-[#161b22] rounded-lg border border-gray-200 dark:border-[#30363d] hover:bg-gray-200 dark:hover:bg-[#21262d] flex items-center justify-center transition-colors mt-4"
277
+ className={`aspect-square ${backgroundClass ?? "bg-gray-100"} rounded-lg border border-gray-200 dark:border-[#30363d] flex items-center justify-center transition-colors mt-4 overflow-hidden`}
249
278
  >
250
- {status === "loading" && (
279
+ {(isLoading || status === "loading") && (
251
280
  <Skeleton className="w-full h-full rounded-lg" />
252
281
  )}
253
- {imageUrl && (
282
+ {!isLoading && !status && svg && (
283
+ <img
284
+ src={svgToDataUrl(normalizeSvgForSquareTile(svg))}
285
+ alt={view}
286
+ className="w-full h-full object-contain"
287
+ />
288
+ )}
289
+ {imageUrl && !isLoading && (
254
290
  <img
255
291
  src={imageUrl}
256
292
  alt={view}
257
- className={`w-full h-full object-cover rounded-lg ${
258
- status === "loaded" ? "block" : "hidden"
259
- }`}
293
+ className="w-full h-full object-cover rounded-lg"
260
294
  onLoad={onLoad}
261
295
  onError={onError}
262
296
  />