@tscircuit/fake-snippets 0.0.109 → 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 (181) 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 +349 -453
  34. package/bunfig.toml +2 -1
  35. package/dist/bundle.js +1253 -624
  36. package/dist/index.d.ts +291 -4
  37. package/dist/index.js +323 -23
  38. package/dist/schema.d.ts +274 -1
  39. package/dist/schema.js +52 -1
  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 +61 -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 +24 -20
  64. package/renovate.json +1 -1
  65. package/scripts/generate-sitemap.ts +1 -1
  66. package/src/App.tsx +29 -8
  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 +3 -4
  71. package/src/components/FileSidebar.tsx +11 -17
  72. package/src/components/Footer.tsx +8 -9
  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 +43 -15
  77. package/src/components/NotFound.tsx +5 -5
  78. package/src/components/PackageBreadcrumb.tsx +6 -12
  79. package/src/components/PackageSearchResults.tsx +1 -1
  80. package/src/components/PrefetchPageLink.tsx +7 -1
  81. package/src/components/ProfileRouter.tsx +32 -0
  82. package/src/components/SearchComponent.tsx +12 -8
  83. package/src/components/UserCard.tsx +80 -0
  84. package/src/components/ViewPackagePage/components/build-status.tsx +1 -1
  85. package/src/components/ViewPackagePage/components/important-files-view.tsx +105 -34
  86. package/src/components/ViewPackagePage/components/main-content-header.tsx +10 -6
  87. package/src/components/ViewPackagePage/components/main-content-view-selector.tsx +1 -1
  88. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +54 -19
  89. package/src/components/ViewPackagePage/components/package-header.tsx +25 -33
  90. package/src/components/ViewPackagePage/components/preview-image-squares.tsx +11 -18
  91. package/src/components/ViewPackagePage/components/repo-page-content.tsx +12 -5
  92. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +16 -10
  93. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +11 -11
  94. package/src/components/ViewPackagePage/components/tab-views/pcb-view.tsx +1 -2
  95. package/src/components/ViewPackagePage/components/tab-views/schematic-view.tsx +2 -1
  96. package/src/components/dialogs/GitHubRepositorySelector.tsx +56 -49
  97. package/src/components/dialogs/edit-package-details-dialog.tsx +5 -6
  98. package/src/components/dialogs/import-component-dialog.tsx +16 -9
  99. package/src/components/dialogs/import-package-dialog.tsx +3 -2
  100. package/src/components/dialogs/new-package-save-prompt-dialog.tsx +190 -0
  101. package/src/components/organization/OrganizationCard.tsx +204 -0
  102. package/src/components/organization/OrganizationCardSkeleton.tsx +55 -0
  103. package/src/components/organization/OrganizationHeader.tsx +154 -0
  104. package/src/components/organization/OrganizationMembers.tsx +146 -0
  105. package/src/components/package-port/CodeAndPreview.tsx +15 -12
  106. package/src/components/package-port/CodeEditor.tsx +4 -30
  107. package/src/components/package-port/CodeEditorHeader.tsx +123 -61
  108. package/src/components/package-port/EditorNav.tsx +32 -49
  109. package/src/components/preview/ConnectedPackagesList.tsx +8 -8
  110. package/src/components/preview/ConnectedRepoOverview.tsx +102 -2
  111. package/src/components/preview/PackageReleasesDashboard.tsx +23 -11
  112. package/src/components/ui/tree-view.tsx +6 -3
  113. package/src/hooks/use-add-org-member-mutation.ts +51 -0
  114. package/src/hooks/use-create-org-mutation.ts +38 -0
  115. package/src/hooks/use-create-package-mutation.ts +3 -0
  116. package/src/hooks/use-current-package-release.ts +4 -3
  117. package/src/hooks/use-download-zip.ts +2 -2
  118. package/src/hooks/use-global-store.ts +6 -4
  119. package/src/hooks/use-jlcpcb-component-import.tsx +164 -0
  120. package/src/hooks/use-list-org-members.ts +27 -0
  121. package/src/hooks/use-list-user-orgs.ts +25 -0
  122. package/src/hooks/use-org-by-github-handle.ts +26 -0
  123. package/src/hooks/use-org.ts +24 -0
  124. package/src/hooks/use-organization.ts +42 -0
  125. package/src/hooks/use-package-as-snippet.ts +4 -2
  126. package/src/hooks/use-package-builds.ts +6 -2
  127. package/src/hooks/use-package-files.ts +5 -3
  128. package/src/hooks/use-package-release-by-id-or-version.ts +29 -20
  129. package/src/hooks/use-package-release-images.ts +105 -0
  130. package/src/hooks/use-package-release.ts +2 -2
  131. package/src/hooks/use-package-stars.ts +80 -4
  132. package/src/hooks/use-preview-images.ts +6 -3
  133. package/src/hooks/use-remove-org-member-mutation.ts +32 -0
  134. package/src/hooks/use-update-ai-description-mutation.ts +42 -0
  135. package/src/hooks/use-update-org-mutation.ts +41 -0
  136. package/src/hooks/use-warn-user-on-page-change.ts +71 -4
  137. package/src/hooks/useFileManagement.ts +51 -22
  138. package/src/hooks/useOptimizedPackageFilesLoader.ts +11 -24
  139. package/src/hooks/usePackageFilesLoader.ts +2 -2
  140. package/src/hooks/useUpdatePackageFilesMutation.ts +13 -1
  141. package/src/lib/download-fns/download-gltf-from-circuit-json.ts +1 -1
  142. package/src/lib/download-fns/download-kicad-files.ts +12 -11
  143. package/src/lib/normalize-svg-for-tile.ts +50 -0
  144. package/src/lib/posthog.ts +11 -9
  145. package/src/lib/react-query-api-failure-tracking.ts +148 -0
  146. package/src/lib/sentry.ts +14 -0
  147. package/src/lib/templates/blank-circuit-board-template.ts +0 -4
  148. package/src/lib/ts-lib-cache.ts +122 -7
  149. package/src/lib/utils/checkIfManualEditsImported.ts +4 -4
  150. package/src/lib/utils/findTargetFile.ts +45 -10
  151. package/src/lib/utils/isComponentExported.ts +2 -1
  152. package/src/main.tsx +2 -1
  153. package/src/pages/create-organization.tsx +168 -0
  154. package/src/pages/dashboard.tsx +38 -6
  155. package/src/pages/datasheet.tsx +1 -1
  156. package/src/pages/datasheets.tsx +3 -3
  157. package/src/pages/editor.tsx +4 -6
  158. package/src/pages/landing.tsx +6 -6
  159. package/src/pages/latest.tsx +3 -0
  160. package/src/pages/organization-profile.tsx +199 -0
  161. package/src/pages/organization-settings.tsx +566 -0
  162. package/src/pages/package-editor.tsx +21 -21
  163. package/src/pages/preview-release.tsx +75 -145
  164. package/src/pages/quickstart.tsx +159 -123
  165. package/src/pages/release-detail.tsx +119 -31
  166. package/src/pages/search.tsx +192 -57
  167. package/src/pages/settings-redirect.tsx +44 -0
  168. package/src/pages/trending.tsx +29 -20
  169. package/src/pages/user-profile.tsx +58 -7
  170. package/src/pages/view-package.tsx +7 -13
  171. package/vite.config.ts +9 -0
  172. package/fake-snippets-api/routes/api/autocomplete/create_autocomplete.ts +0 -133
  173. package/src/components/JLCPCBImportDialog.tsx +0 -280
  174. package/src/components/PackageBuildsPage/LogContent.tsx +0 -72
  175. package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +0 -113
  176. package/src/components/PackageBuildsPage/build-preview-content.tsx +0 -56
  177. package/src/components/PackageBuildsPage/collapsible-section.tsx +0 -63
  178. package/src/components/PackageBuildsPage/package-build-details-panel.tsx +0 -166
  179. package/src/components/PackageBuildsPage/package-build-header.tsx +0 -79
  180. package/src/components/PageSearchComponent.tsx +0 -148
  181. package/src/pages/package-builds.tsx +0 -33
@@ -5,7 +5,7 @@ import React, { useEffect, useRef, useState } from "react"
5
5
  import { useQuery } from "react-query"
6
6
  import { Alert } from "./ui/alert"
7
7
  import { useApiBaseUrl } from "@/hooks/use-packages-base-api-url"
8
- import { PrefetchPageLink } from "./PrefetchPageLink"
8
+ import { Link } from "wouter"
9
9
  import { CircuitBoard } from "lucide-react"
10
10
  import { cn } from "@/lib/utils"
11
11
 
@@ -42,9 +42,9 @@ const LinkWithNewTabHandling = ({
42
42
  )
43
43
  }
44
44
  return (
45
- <PrefetchPageLink onClick={onClick} className={className} href={href}>
45
+ <Link onClick={onClick} className={className} href={href}>
46
46
  {children}
47
- </PrefetchPageLink>
47
+ </Link>
48
48
  )
49
49
  }
50
50
 
@@ -60,7 +60,7 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
60
60
  const resultsRef = useRef<HTMLDivElement>(null)
61
61
  const inputRef = useRef<HTMLInputElement>(null)
62
62
  const [location, setLocation] = useLocation()
63
- const snippetsBaseApiUrl = useApiBaseUrl()
63
+ const apiBaseUrl = useApiBaseUrl()
64
64
 
65
65
  const { data: searchResults, isLoading } = useQuery(
66
66
  ["packageSearch", searchQuery],
@@ -137,10 +137,13 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
137
137
  <Input
138
138
  autoComplete="off"
139
139
  spellCheck={false}
140
+ autoCorrect="off"
141
+ autoCapitalize="off"
140
142
  ref={inputRef}
141
143
  type="search"
144
+ aria-autocomplete="none"
142
145
  placeholder="Search"
143
- className="pl-4 focus:border-blue-500 placeholder-gray-400 text-sm"
146
+ className="pl-4 focus:border-blue-500 placeholder-gray-400 text-sm select-none"
144
147
  value={searchQuery}
145
148
  onChange={(e) => {
146
149
  setSearchQuery(e.target.value)
@@ -195,10 +198,10 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
195
198
  {showResults && searchResults && (
196
199
  <div
197
200
  ref={resultsRef}
198
- className="absolute top-full md:left-0 right-0 mt-2 bg-white shadow-lg rounded-md z-50 w-80 max-h-screen overflow-y-auto overflow-x-visible"
201
+ className="absolute top-full md:left-0 right-0 no-scrollbar mt-2 bg-white shadow-lg rounded-md z-50 w-80 max-h-screen overflow-y-auto overflow-x-visible"
199
202
  >
200
203
  {searchResults.length > 0 ? (
201
- <ul className="divide-y divide-gray-200">
204
+ <ul className="divide-y divide-gray-200 no-scrollbar">
202
205
  {searchResults.map((pkg: any, index: number) => (
203
206
  <li
204
207
  key={pkg.package_id}
@@ -222,8 +225,9 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
222
225
  >
223
226
  <div className="w-12 h-12 overflow-hidden mr-2 flex-shrink-0 rounded-sm bg-gray-50 border flex items-center justify-center">
224
227
  <img
225
- src={`${snippetsBaseApiUrl}/snippets/images/${pkg.name}/pcb.svg`}
228
+ src={`${apiBaseUrl}/packages/images/${pkg.name}/pcb.svg`}
226
229
  alt={`PCB preview for ${pkg.name}`}
230
+ draggable={false}
227
231
  className="w-12 h-12 object-contain p-1 scale-[4] rotate-45"
228
232
  onError={(e) => {
229
233
  e.currentTarget.style.display = "none"
@@ -0,0 +1,80 @@
1
+ import React from "react"
2
+ import { Link } from "wouter"
3
+ import { Account } from "fake-snippets-api/lib/db/schema"
4
+ import { User } from "lucide-react"
5
+
6
+ export interface UserCardProps {
7
+ /** The account data to display */
8
+ account: Account | Omit<Account, "account_id">
9
+ /** Whether to render the card with a link to the user profile page */
10
+ withLink?: boolean
11
+ /** Custom class name for the card container */
12
+ className?: string
13
+ /** Custom onClick handler */
14
+ onClick?: (account: Account | Omit<Account, "account_id">) => void
15
+ }
16
+
17
+ export const UserCard: React.FC<UserCardProps> = ({
18
+ account,
19
+ withLink = true,
20
+ className = "",
21
+ onClick,
22
+ }) => {
23
+ const handleClick = () => {
24
+ if (onClick) {
25
+ onClick(account)
26
+ } else if (!withLink) {
27
+ window.location.href = `/${account.github_username}`
28
+ }
29
+ }
30
+
31
+ const cardContent = (
32
+ <div
33
+ className={`border p-4 rounded-md hover:shadow-md transition-shadow flex flex-col gap-4 cursor-pointer ${className}`}
34
+ onClick={!withLink ? handleClick : undefined}
35
+ >
36
+ <div className="flex items-start gap-4">
37
+ <div className="w-16 h-16 flex-shrink-0 rounded-md overflow-hidden bg-gray-50 border flex items-center justify-center">
38
+ <img
39
+ src={`https://github.com/${account.github_username}.png`}
40
+ alt={`${account.github_username} avatar`}
41
+ className="object-cover h-full w-full transition-transform duration-300 hover:scale-110"
42
+ onError={(e) => {
43
+ const target = e.target as HTMLImageElement
44
+ target.style.display = "none"
45
+ target.nextElementSibling?.classList.remove("hidden")
46
+ target.nextElementSibling?.classList.add("flex")
47
+ }}
48
+ />
49
+ <div className="hidden items-center justify-center h-full w-full">
50
+ <User className="w-6 h-6 text-gray-300" />
51
+ </div>
52
+ </div>
53
+ <div className="flex-1 min-w-0 flex flex-col justify-center my-auto">
54
+ <div className="flex justify-between items-start">
55
+ <h2 className="text-md font-semibold truncate pr-[30px]">
56
+ <span className="text-gray-900">{account.github_username}</span>
57
+ </h2>
58
+ </div>
59
+ <p className="text-sm text-gray-500 truncate max-w-xs">
60
+ @{account.github_username}
61
+ </p>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ )
66
+
67
+ if (withLink) {
68
+ return (
69
+ <Link
70
+ key={account.github_username}
71
+ href={`/${account.github_username}`}
72
+ onClick={onClick ? () => onClick(account) : undefined}
73
+ >
74
+ {cardContent}
75
+ </Link>
76
+ )
77
+ }
78
+
79
+ return cardContent
80
+ }
@@ -14,7 +14,7 @@ export interface BuildStatusProps {
14
14
 
15
15
  export const BuildStatus = ({ step, packageReleaseId }: BuildStatusProps) => {
16
16
  const { author, packageName } = useParams()
17
- const href = `/${author}/${packageName}/builds?package_release_id=${packageReleaseId}`
17
+ const href = `/${author}/${packageName}/releases/${packageReleaseId}`
18
18
 
19
19
  return (
20
20
  <Link href={href} className="flex items-center gap-2">
@@ -16,6 +16,8 @@ import { ShikiCodeViewer, SKELETON_WIDTHS } from "./ShikiCodeViewer"
16
16
  import MarkdownViewer from "./markdown-viewer"
17
17
  import { useGlobalStore } from "@/hooks/use-global-store"
18
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"
19
21
 
20
22
  interface PackageFile {
21
23
  package_file_id: string
@@ -29,13 +31,12 @@ interface ImportantFilesViewProps {
29
31
  importantFiles?: PackageFile[]
30
32
  isFetched?: boolean
31
33
  onEditClicked?: (file_path?: string | null) => void
32
- packageAuthorOwner?: string | null
33
- aiDescription?: string
34
- aiUsageInstructions?: string
35
34
  aiReviewText?: string | null
36
35
  aiReviewRequested?: boolean
37
36
  onRequestAiReview?: () => void
37
+ onRequestAiDescriptionUpdate?: () => void
38
38
  onLicenseFileRequested?: boolean
39
+ pkg?: Package
39
40
  }
40
41
 
41
42
  type TabType = "ai" | "ai-review" | "file"
@@ -49,30 +50,44 @@ interface TabInfo {
49
50
 
50
51
  export default function ImportantFilesView({
51
52
  importantFiles = [],
52
- aiDescription,
53
- aiUsageInstructions,
54
53
  aiReviewText,
55
54
  aiReviewRequested,
56
55
  onRequestAiReview,
56
+ onRequestAiDescriptionUpdate,
57
57
  isFetched = false,
58
58
  onEditClicked,
59
- packageAuthorOwner,
60
59
  onLicenseFileRequested,
60
+ pkg,
61
61
  }: ImportantFilesViewProps) {
62
62
  const [activeTab, setActiveTab] = useState<TabInfo | null>(null)
63
63
  const [copyState, setCopyState] = useState<"copy" | "copied">("copy")
64
64
  const { session: user } = useGlobalStore()
65
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
+
66
74
  // Memoized computed values
67
75
  const hasAiContent = useMemo(
68
- () => Boolean(aiDescription || aiUsageInstructions),
69
- [aiDescription, aiUsageInstructions],
76
+ () => Boolean(pkg?.ai_description || pkg?.ai_usage_instructions),
77
+ [pkg?.ai_description, pkg?.ai_usage_instructions],
70
78
  )
71
79
  const hasAiReview = useMemo(() => Boolean(aiReviewText), [aiReviewText])
72
80
  const isOwner = useMemo(
73
- () => user?.github_username === packageAuthorOwner,
74
- [user?.github_username, packageAuthorOwner],
81
+ () => user?.github_username === pkg?.owner_github_username,
82
+ [user?.github_username, pkg?.owner_github_username],
75
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])
76
91
 
77
92
  // File type utilities
78
93
  const isLicenseFile = useCallback((filePath: string) => {
@@ -139,7 +154,7 @@ export default function ImportantFilesView({
139
154
  }
140
155
 
141
156
  // Only show AI review tab if there's actual AI review content
142
- if (hasAiReview || isOwner) {
157
+ if (hasAiReview || canManagePackage) {
143
158
  tabs.push({
144
159
  type: "ai-review",
145
160
  filePath: null,
@@ -286,6 +301,16 @@ export default function ImportantFilesView({
286
301
 
287
302
  if (activeTab?.type === "ai-review" && aiReviewText) {
288
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")
289
314
  } else if (activeTab?.type === "file" && activeFileContent) {
290
315
  textToCopy = activeFileContent
291
316
  }
@@ -298,25 +323,49 @@ export default function ImportantFilesView({
298
323
  }
299
324
 
300
325
  // Render content based on active tab
301
- const renderAiContent = useCallback(
302
- () => (
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 (
303
348
  <div className="markdown-content">
304
- {aiDescription && (
349
+ {pkg?.ai_description && (
305
350
  <div className="mb-6">
306
351
  <h3 className="font-semibold text-lg mb-2">Description</h3>
307
- <MarkdownViewer markdownContent={aiDescription} />
352
+ <MarkdownViewer markdownContent={pkg?.ai_description} />
308
353
  </div>
309
354
  )}
310
- {aiUsageInstructions && (
355
+ {pkg?.ai_usage_instructions && (
311
356
  <div>
312
357
  <h3 className="font-semibold text-lg mb-2">Instructions</h3>
313
- <MarkdownViewer markdownContent={aiUsageInstructions} />
358
+ <MarkdownViewer markdownContent={pkg?.ai_usage_instructions} />
314
359
  </div>
315
360
  )}
316
361
  </div>
317
- ),
318
- [aiDescription, aiUsageInstructions],
319
- )
362
+ )
363
+ }, [
364
+ pkg?.ai_description,
365
+ pkg?.ai_usage_instructions,
366
+ canManagePackage,
367
+ onRequestAiDescriptionUpdate,
368
+ ])
320
369
 
321
370
  const renderAiReviewContent = useCallback(() => {
322
371
  if (!aiReviewText && !aiReviewRequested) {
@@ -334,7 +383,7 @@ export default function ImportantFilesView({
334
383
  from our AI assistant.
335
384
  </p>
336
385
  </div>
337
- {!isOwner ? (
386
+ {!canManagePackage ? (
338
387
  <p className="text-sm text-gray-500">
339
388
  Only the package owner can generate an AI review
340
389
  </p>
@@ -378,15 +427,17 @@ export default function ImportantFilesView({
378
427
  }
379
428
 
380
429
  return <MarkdownViewer markdownContent={aiReviewText || ""} />
381
- }, [aiReviewText, aiReviewRequested, isOwner, onRequestAiReview])
430
+ }, [aiReviewText, aiReviewRequested, canManagePackage, onRequestAiReview])
382
431
 
383
432
  const renderFileContent = useCallback(() => {
384
433
  if (!isActiveFileFetched || !activeTab?.filePath || !activeFileContent) {
385
- ;<div className="text-sm p-4">
386
- {SKELETON_WIDTHS.map((w, i) => (
387
- <Skeleton key={i} className={`h-4 mb-2 ${w}`} />
388
- ))}
389
- </div>
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
+ )
390
441
  }
391
442
 
392
443
  if (isMarkdownFile(String(activeTab?.filePath))) {
@@ -405,7 +456,13 @@ export default function ImportantFilesView({
405
456
  }
406
457
 
407
458
  return <pre className="whitespace-pre-wrap">{activeFileContent}</pre>
408
- }, [activeTab, activeFileContent, isMarkdownFile, isCodeFile])
459
+ }, [
460
+ activeTab,
461
+ activeFileContent,
462
+ isActiveFileFetched,
463
+ isMarkdownFile,
464
+ isCodeFile,
465
+ ])
409
466
 
410
467
  const renderTabContent = useCallback(() => {
411
468
  if (!activeTab) return null
@@ -500,7 +557,9 @@ export default function ImportantFilesView({
500
557
  </div>
501
558
  <div className="ml-auto flex items-center">
502
559
  {((activeTab?.type === "file" && activeFileContent) ||
503
- (activeTab?.type === "ai-review" && aiReviewText)) && (
560
+ (activeTab?.type === "ai-review" && aiReviewText) ||
561
+ (activeTab?.type === "ai" &&
562
+ (pkg?.ai_description || pkg?.ai_usage_instructions))) && (
504
563
  <button
505
564
  className="hover:bg-gray-200 dark:hover:bg-[#30363d] p-1 rounded-md transition-all duration-300"
506
565
  onClick={handleCopy}
@@ -513,17 +572,29 @@ export default function ImportantFilesView({
513
572
  <span className="sr-only">Copy</span>
514
573
  </button>
515
574
  )}
516
- {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 && (
517
588
  <button
518
589
  className="hover:bg-gray-200 dark:hover:bg-[#30363d] p-1 rounded-md ml-1"
519
- onClick={onRequestAiReview}
520
- title="Re-request AI Review"
590
+ onClick={onRequestAiDescriptionUpdate}
591
+ title="Regenerate AI Description"
521
592
  >
522
593
  <RefreshCcwIcon className="h-4 w-4" />
523
- <span className="sr-only">Re-request AI Review</span>
594
+ <span className="sr-only">Regenerate AI Description</span>
524
595
  </button>
525
596
  )}
526
- {activeTab?.type === "file" && (
597
+ {activeTab?.type === "file" && canManagePackage && (
527
598
  <button
528
599
  className="hover:bg-gray-200 dark:hover:bg-[#30363d] p-1 rounded-md"
529
600
  onClick={() => onEditClicked?.(activeTab.filePath)}
@@ -26,6 +26,7 @@ import { useLocation } from "wouter"
26
26
  import { Package, PackageFile } from "fake-snippets-api/lib/db/schema"
27
27
  import { usePackageFiles } from "@/hooks/use-package-files"
28
28
  import { useDownloadZip } from "@/hooks/use-download-zip"
29
+ import { useToast } from "@/hooks/use-toast"
29
30
  interface MainContentHeaderProps {
30
31
  packageFiles: PackageFile[]
31
32
  activeView: string
@@ -63,16 +64,19 @@ export default function MainContentHeader({
63
64
  }
64
65
 
65
66
  const { downloadZip } = useDownloadZip()
67
+ const { toastLibrary } = useToast()
66
68
 
67
69
  const handleDownloadZip = () => {
68
70
  if (packageInfo && packageFiles) {
69
- 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
+ })
70
76
  }
71
77
  }
72
78
 
73
- const hasCircuitJson = packageFiles.some(
74
- (file) => file.file_path === "dist/circuit.json",
75
- )
79
+ const { circuitJson } = useCurrentPackageCircuitJson()
76
80
 
77
81
  return (
78
82
  <div className="flex items-center justify-between mb-4">
@@ -86,7 +90,7 @@ export default function MainContentHeader({
86
90
  unscopedName={packageInfo?.unscoped_name}
87
91
  desiredImageType={activeView}
88
92
  author={packageInfo?.owner_github_username ?? undefined}
89
- hasCircuitJson={hasCircuitJson}
93
+ circuitJson={circuitJson}
90
94
  />
91
95
 
92
96
  {/* Code Dropdown */}
@@ -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}`}
@@ -130,7 +130,7 @@ export default function MainContentViewSelector({
130
130
  </svg>
131
131
  </Button>
132
132
  </DropdownMenuTrigger>
133
- <DropdownMenuContent align="start">
133
+ <DropdownMenuContent align="start" className="z-[101]">
134
134
  <TooltipProvider>
135
135
  {views.map((view) => {
136
136
  const isDisabled = !circuitJson && view.requiresCircuitJson
@@ -1,13 +1,18 @@
1
1
  import { GitFork, Star, Tag, Settings, LinkIcon } from "lucide-react"
2
2
  import { Badge } from "@/components/ui/badge"
3
3
  import { Skeleton } from "@/components/ui/skeleton"
4
+ import { usePackageReleaseImages } from "@/hooks/use-package-release-images"
4
5
  import { usePreviewImages } from "@/hooks/use-preview-images"
5
6
  import { useGlobalStore } from "@/hooks/use-global-store"
6
7
  import { Button } from "@/components/ui/button"
7
8
  import { useEditPackageDetailsDialog } from "@/components/dialogs/edit-package-details-dialog"
8
9
  import React, { useState, useEffect, useMemo, useCallback } from "react"
10
+ import {
11
+ normalizeSvgForSquareTile,
12
+ svgToDataUrl,
13
+ } from "@/lib/normalize-svg-for-tile"
9
14
  import { useCurrentPackageInfo } from "@/hooks/use-current-package-info"
10
- import { usePackageFile } from "@/hooks/use-package-files"
15
+ import { usePackageFileById, usePackageFiles } from "@/hooks/use-package-files"
11
16
  import { getLicenseFromLicenseContent } from "@/lib/getLicenseFromLicenseContent"
12
17
 
13
18
  interface MobileSidebarProps {
@@ -20,19 +25,25 @@ const MobileSidebar = ({
20
25
  onViewChange,
21
26
  }: MobileSidebarProps) => {
22
27
  const { packageInfo, refetch: refetchPackageInfo } = useCurrentPackageInfo()
23
- const { data: licenseFileMeta } = usePackageFile({
24
- package_release_id: packageInfo?.latest_package_release_id ?? "",
25
- file_path: "LICENSE",
26
- })
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)
27
38
  const currentLicense = useMemo(() => {
28
39
  if (packageInfo?.latest_license) {
29
40
  return packageInfo?.latest_license
30
41
  }
31
42
  if (licenseFileMeta?.content_text) {
32
- return getLicenseFromLicenseContent(licenseFileMeta?.content_text)
43
+ return getLicenseFromLicenseContent(licenseFileMeta.content_text)
33
44
  }
34
45
  return undefined
35
- }, [licenseFileMeta?.content_text, packageInfo?.latest_license])
46
+ }, [licenseFileMeta, packageInfo?.latest_license])
36
47
  const topics = useMemo(
37
48
  () => (packageInfo?.is_package ? ["Package"] : ["Board"]),
38
49
  [packageInfo?.is_package],
@@ -69,11 +80,21 @@ const MobileSidebar = ({
69
80
  [refetchPackageInfo],
70
81
  )
71
82
 
72
- const { availableViews } = usePreviewImages({
83
+ const { availableViews: imageViews } = usePackageReleaseImages({
84
+ packageReleaseId: packageInfo?.latest_package_release_id,
85
+ })
86
+
87
+ const { availableViews: pngViews } = usePreviewImages({
73
88
  packageName: packageInfo?.name,
74
89
  fsMapHash: packageInfo?.latest_package_release_fs_sha ?? "",
75
90
  })
76
91
 
92
+ const viewsToRender =
93
+ imageViews.length === 0 ||
94
+ imageViews.every((v) => !v.isLoading && !v.imageUrl)
95
+ ? (pngViews as any)
96
+ : imageViews
97
+
77
98
  const handleViewClick = useCallback(
78
99
  (viewId: string) => {
79
100
  onViewChange?.(viewId as "3d" | "pcb" | "schematic")
@@ -185,11 +206,14 @@ const MobileSidebar = ({
185
206
  </div>
186
207
 
187
208
  <div className="grid grid-cols-3 gap-2">
188
- {availableViews.map((view) => (
209
+ {viewsToRender.map((view: any) => (
189
210
  <PreviewButton
190
211
  key={view.id}
191
212
  view={view.label}
192
213
  onClick={() => handleViewClick(view.id)}
214
+ backgroundClass={view.backgroundClass}
215
+ svg={view.svg}
216
+ isLoading={view.isLoading}
193
217
  imageUrl={view.imageUrl}
194
218
  status={view.status}
195
219
  onLoad={view.onLoad}
@@ -225,6 +249,9 @@ export default React.memo(MobileSidebar)
225
249
  function PreviewButton({
226
250
  view,
227
251
  onClick,
252
+ backgroundClass,
253
+ svg,
254
+ isLoading,
228
255
  imageUrl,
229
256
  status,
230
257
  onLoad,
@@ -232,30 +259,38 @@ function PreviewButton({
232
259
  }: {
233
260
  view: string
234
261
  onClick: () => void
262
+ backgroundClass?: string
263
+ svg?: string | null
264
+ isLoading?: boolean
235
265
  imageUrl?: string
236
- status: "loading" | "loaded" | "error"
237
- onLoad: () => void
238
- onError: () => void
266
+ status?: "loading" | "loaded" | "error"
267
+ onLoad?: () => void
268
+ onError?: () => void
239
269
  }) {
240
- if (status === "error") {
270
+ if (!svg && !isLoading && !imageUrl) {
241
271
  return null
242
272
  }
243
273
 
244
274
  return (
245
275
  <button
246
276
  onClick={onClick}
247
- 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`}
248
278
  >
249
- {status === "loading" && (
279
+ {(isLoading || status === "loading") && (
250
280
  <Skeleton className="w-full h-full rounded-lg" />
251
281
  )}
252
- {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 && (
253
290
  <img
254
291
  src={imageUrl}
255
292
  alt={view}
256
- className={`w-full h-full object-cover rounded-lg ${
257
- status === "loaded" ? "block" : "hidden"
258
- }`}
293
+ className="w-full h-full object-cover rounded-lg"
259
294
  onLoad={onLoad}
260
295
  onError={onError}
261
296
  />