@tscircuit/fake-snippets 0.0.109 → 0.0.111

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 (185) 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 +32 -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 +151 -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 +361 -453
  34. package/bunfig.toml +2 -1
  35. package/dist/bundle.js +1313 -639
  36. package/dist/index.d.ts +313 -6
  37. package/dist/index.js +328 -24
  38. package/dist/schema.d.ts +290 -1
  39. package/dist/schema.js +54 -1
  40. package/fake-snippets-api/lib/db/autoload-dev-packages.ts +31 -20
  41. package/fake-snippets-api/lib/db/db-client.ts +219 -4
  42. package/fake-snippets-api/lib/db/schema.ts +63 -1
  43. package/fake-snippets-api/lib/db/seed.ts +100 -0
  44. package/fake-snippets-api/lib/middleware/with-session-auth.ts +60 -8
  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 +33 -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 +48 -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 +60 -0
  54. package/fake-snippets-api/routes/api/orgs/remove_member.ts +46 -0
  55. package/fake-snippets-api/routes/api/orgs/update.ts +118 -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 +57 -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 +25 -19
  64. package/renovate.json +1 -1
  65. package/scripts/generate-sitemap.ts +1 -1
  66. package/src/App.tsx +27 -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 +17 -5
  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/SentryNotFoundReporter.tsx +44 -0
  84. package/src/components/UserCard.tsx +80 -0
  85. package/src/components/ViewPackagePage/components/build-status.tsx +1 -1
  86. package/src/components/ViewPackagePage/components/important-files-view.tsx +105 -34
  87. package/src/components/ViewPackagePage/components/main-content-header.tsx +10 -6
  88. package/src/components/ViewPackagePage/components/main-content-view-selector.tsx +1 -1
  89. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +54 -19
  90. package/src/components/ViewPackagePage/components/package-header.tsx +25 -33
  91. package/src/components/ViewPackagePage/components/preview-image-squares.tsx +11 -18
  92. package/src/components/ViewPackagePage/components/repo-page-content.tsx +12 -5
  93. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +16 -10
  94. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +11 -11
  95. package/src/components/ViewPackagePage/components/tab-views/pcb-view.tsx +1 -2
  96. package/src/components/ViewPackagePage/components/tab-views/schematic-view.tsx +2 -1
  97. package/src/components/dialogs/GitHubRepositorySelector.tsx +56 -49
  98. package/src/components/dialogs/edit-package-details-dialog.tsx +5 -6
  99. package/src/components/dialogs/import-component-dialog.tsx +16 -9
  100. package/src/components/dialogs/import-package-dialog.tsx +3 -2
  101. package/src/components/dialogs/new-package-save-prompt-dialog.tsx +190 -0
  102. package/src/components/organization/OrganizationCard.tsx +206 -0
  103. package/src/components/organization/OrganizationCardSkeleton.tsx +55 -0
  104. package/src/components/organization/OrganizationHeader.tsx +154 -0
  105. package/src/components/organization/OrganizationMembers.tsx +146 -0
  106. package/src/components/package-port/CodeAndPreview.tsx +15 -12
  107. package/src/components/package-port/CodeEditor.tsx +4 -30
  108. package/src/components/package-port/CodeEditorHeader.tsx +123 -61
  109. package/src/components/package-port/EditorNav.tsx +32 -49
  110. package/src/components/preview/ConnectedPackagesList.tsx +8 -8
  111. package/src/components/preview/ConnectedRepoOverview.tsx +102 -2
  112. package/src/components/preview/PackageReleasesDashboard.tsx +23 -11
  113. package/src/components/ui/tree-view.tsx +6 -3
  114. package/src/hooks/use-add-org-member-mutation.ts +51 -0
  115. package/src/hooks/use-create-org-mutation.ts +38 -0
  116. package/src/hooks/use-create-package-mutation.ts +3 -0
  117. package/src/hooks/use-current-package-release.ts +4 -3
  118. package/src/hooks/use-download-zip.ts +2 -2
  119. package/src/hooks/use-global-store.ts +6 -4
  120. package/src/hooks/use-hydration.ts +30 -0
  121. package/src/hooks/use-jlcpcb-component-import.tsx +164 -0
  122. package/src/hooks/use-list-org-members.ts +27 -0
  123. package/src/hooks/use-list-user-orgs.ts +25 -0
  124. package/src/hooks/use-org-by-github-handle.ts +26 -0
  125. package/src/hooks/use-org.ts +24 -0
  126. package/src/hooks/use-organization.ts +42 -0
  127. package/src/hooks/use-package-as-snippet.ts +4 -2
  128. package/src/hooks/use-package-builds.ts +6 -2
  129. package/src/hooks/use-package-files.ts +5 -3
  130. package/src/hooks/use-package-release-by-id-or-version.ts +29 -20
  131. package/src/hooks/use-package-release-images.ts +105 -0
  132. package/src/hooks/use-package-release.ts +2 -2
  133. package/src/hooks/use-package-stars.ts +80 -4
  134. package/src/hooks/use-preview-images.ts +6 -3
  135. package/src/hooks/use-remove-org-member-mutation.ts +32 -0
  136. package/src/hooks/use-update-ai-description-mutation.ts +42 -0
  137. package/src/hooks/use-update-org-mutation.ts +41 -0
  138. package/src/hooks/use-warn-user-on-page-change.ts +71 -4
  139. package/src/hooks/useFileManagement.ts +51 -22
  140. package/src/hooks/useOptimizedPackageFilesLoader.ts +11 -24
  141. package/src/hooks/usePackageFilesLoader.ts +2 -2
  142. package/src/hooks/useUpdatePackageFilesMutation.ts +13 -1
  143. package/src/lib/download-fns/download-gltf-from-circuit-json.ts +1 -1
  144. package/src/lib/download-fns/download-kicad-files.ts +22 -11
  145. package/src/lib/download-fns/download-step.ts +12 -0
  146. package/src/lib/normalize-svg-for-tile.ts +50 -0
  147. package/src/lib/posthog.ts +11 -9
  148. package/src/lib/react-query-api-failure-tracking.ts +148 -0
  149. package/src/lib/sentry.ts +14 -0
  150. package/src/lib/templates/blank-circuit-board-template.ts +0 -4
  151. package/src/lib/ts-lib-cache.ts +122 -7
  152. package/src/lib/utils/checkIfManualEditsImported.ts +4 -4
  153. package/src/lib/utils/findTargetFile.ts +45 -10
  154. package/src/lib/utils/isComponentExported.ts +2 -1
  155. package/src/main.tsx +2 -1
  156. package/src/pages/create-organization.tsx +169 -0
  157. package/src/pages/dashboard.tsx +38 -6
  158. package/src/pages/datasheet.tsx +1 -1
  159. package/src/pages/datasheets.tsx +3 -3
  160. package/src/pages/editor.tsx +4 -6
  161. package/src/pages/landing.tsx +6 -6
  162. package/src/pages/latest.tsx +3 -0
  163. package/src/pages/organization-profile.tsx +199 -0
  164. package/src/pages/organization-settings.tsx +569 -0
  165. package/src/pages/package-editor.tsx +21 -21
  166. package/src/pages/preview-release.tsx +75 -145
  167. package/src/pages/quickstart.tsx +159 -123
  168. package/src/pages/release-detail.tsx +119 -31
  169. package/src/pages/search.tsx +197 -57
  170. package/src/pages/settings-redirect.tsx +44 -0
  171. package/src/pages/trending.tsx +29 -20
  172. package/src/pages/user-profile.tsx +58 -7
  173. package/src/pages/user-settings.tsx +161 -0
  174. package/src/pages/view-package.tsx +30 -16
  175. package/vite.config.ts +9 -0
  176. package/fake-snippets-api/routes/api/autocomplete/create_autocomplete.ts +0 -133
  177. package/src/components/JLCPCBImportDialog.tsx +0 -280
  178. package/src/components/PackageBuildsPage/LogContent.tsx +0 -72
  179. package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +0 -113
  180. package/src/components/PackageBuildsPage/build-preview-content.tsx +0 -56
  181. package/src/components/PackageBuildsPage/collapsible-section.tsx +0 -63
  182. package/src/components/PackageBuildsPage/package-build-details-panel.tsx +0 -166
  183. package/src/components/PackageBuildsPage/package-build-header.tsx +0 -79
  184. package/src/components/PageSearchComponent.tsx +0 -148
  185. package/src/pages/package-builds.tsx +0 -33
@@ -0,0 +1,154 @@
1
+ import React, { useState } from "react"
2
+ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
3
+ import { Button } from "@/components/ui/button"
4
+ import { Building2, Users, Package, Lock, Globe2, Settings } from "lucide-react"
5
+ import { cn } from "@/lib/utils"
6
+ import { PublicOrgSchema } from "fake-snippets-api/lib/db/schema"
7
+ import { useGlobalStore } from "@/hooks/use-global-store"
8
+ import { useLocation } from "wouter"
9
+ import { useOrganization } from "@/hooks/use-organization"
10
+
11
+ interface OrganizationHeaderProps {
12
+ organization: PublicOrgSchema
13
+ isCurrentUserOrganization?: boolean
14
+ className?: string
15
+ showActions?: boolean
16
+ }
17
+
18
+ export const OrganizationHeader: React.FC<OrganizationHeaderProps> = ({
19
+ organization,
20
+ className,
21
+ showActions = true,
22
+ }) => {
23
+ const session = useGlobalStore((s) => s.session)
24
+ const [, navigate] = useLocation()
25
+ const canManageOrg =
26
+ organization.user_permissions?.can_manage_org ||
27
+ organization.owner_account_id === session?.account_id
28
+
29
+ const { membersCount, packagesCount, isLoading } = useOrganization({
30
+ orgId: organization.org_id,
31
+ orgName: organization.name!,
32
+ })
33
+
34
+ const handleSettingsClick = () => {
35
+ navigate(`/${organization.name}/settings`)
36
+ }
37
+ return (
38
+ <div className={cn("bg-white border-b border-gray-200", className)}>
39
+ <div className="container mx-auto px-6 py-6">
40
+ {/* Mobile Layout */}
41
+ <div className="block sm:hidden">
42
+ <div className="flex flex-col items-center text-center space-y-4">
43
+ <Avatar className="border-4 border-gray-100 shadow-sm size-16 md:size-20 lg:size-24">
44
+ <AvatarImage
45
+ src={`https://github.com/${organization.name}.png`}
46
+ alt={`${organization.name} avatar`}
47
+ className="object-cover"
48
+ />
49
+ <AvatarFallback className="bg-blue-100 text-blue-600 font-bold text-xl md:text-2xl lg:text-3xl">
50
+ {(organization.name || "")
51
+ .split(" ")
52
+ .map((word) => word[0])
53
+ .join("")
54
+ .toUpperCase()
55
+ .slice(0, 2)}
56
+ </AvatarFallback>
57
+ </Avatar>
58
+
59
+ <div>
60
+ <div className="flex flex-col items-center gap-3 mb-3">
61
+ <h1 className="font-bold text-gray-900 text-xl">
62
+ {organization.name}
63
+ </h1>
64
+ {canManageOrg && showActions && (
65
+ <Button
66
+ variant="outline"
67
+ size="sm"
68
+ onClick={handleSettingsClick}
69
+ >
70
+ <Settings className="mr-2 h-4 w-4" />
71
+ Settings
72
+ </Button>
73
+ )}
74
+ </div>
75
+
76
+ <div className="grid grid-cols-2 md:flex flex-wrap justify-center gap-4 text-sm">
77
+ <div className="flex items-center gap-1.5 text-gray-600">
78
+ <Users className="h-3.5 w-3.5" />
79
+ <span className="font-medium">
80
+ {isLoading ? "..." : membersCount}
81
+ </span>
82
+ <span>members</span>
83
+ </div>
84
+ <div className="flex items-center gap-1.5 text-gray-600">
85
+ <Package className="h-3.5 w-3.5" />
86
+ <span className="font-medium">
87
+ {isLoading ? "..." : packagesCount}
88
+ </span>
89
+ <span>packages</span>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ {/* Desktop Layout */}
97
+ <div className="hidden sm:block">
98
+ <div className="flex items-center gap-6">
99
+ <Avatar className="border-4 border-gray-100 shadow-sm size-16 md:size-20 lg:size-24 flex-shrink-0">
100
+ <AvatarImage
101
+ src={`https://github.com/${organization.name}.png`}
102
+ alt={`${organization.name} avatar`}
103
+ className="object-cover"
104
+ />
105
+ <AvatarFallback className="bg-blue-100 text-blue-600 font-bold text-xl md:text-2xl lg:text-3xl">
106
+ {(organization.name || "")
107
+ .split(" ")
108
+ .map((word) => word[0])
109
+ .join("")
110
+ .toUpperCase()
111
+ .slice(0, 2)}
112
+ </AvatarFallback>
113
+ </Avatar>
114
+
115
+ <div className="flex-1 min-w-0">
116
+ <div className="flex items-center justify-between mb-3">
117
+ <h1 className="font-bold text-gray-900 text-2xl md:text-3xl truncate">
118
+ {organization.name}
119
+ </h1>
120
+ {canManageOrg && showActions && (
121
+ <Button variant="outline" onClick={handleSettingsClick}>
122
+ <Settings className="mr-2 h-4 w-4" />
123
+ Settings
124
+ </Button>
125
+ )}
126
+ </div>
127
+
128
+ <div className="flex flex-wrap items-center gap-6 text-sm text-gray-600">
129
+ <div className="flex items-center gap-2">
130
+ <Users className="h-4 w-4" />
131
+ <span className="font-medium text-gray-900">
132
+ {isLoading ? "..." : membersCount}
133
+ </span>
134
+ <span>members</span>
135
+ </div>
136
+ <div className="flex items-center gap-2">
137
+ <Package className="h-4 w-4" />
138
+ <span className="font-medium text-gray-900">
139
+ {isLoading ? "..." : packagesCount}
140
+ </span>
141
+ <span>packages</span>
142
+ </div>
143
+ <div className="flex items-center gap-2">
144
+ <Building2 className="h-4 w-4" />
145
+ <span>Organization</span>
146
+ </div>
147
+ </div>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ )
154
+ }
@@ -0,0 +1,146 @@
1
+ import React from "react"
2
+ import { Link } from "wouter"
3
+ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
4
+ import { Badge } from "@/components/ui/badge"
5
+ import { Button } from "@/components/ui/button"
6
+ import { Users, Crown, Shield, User, Loader2 } from "lucide-react"
7
+ import { timeAgo } from "@/lib/utils/timeAgo"
8
+ import { cn } from "@/lib/utils"
9
+ import { Account } from "fake-snippets-api/lib/db/schema"
10
+ import { useListOrgMembers } from "@/hooks/use-list-org-members"
11
+
12
+ interface OrganizationMembersProps {
13
+ orgId: string
14
+ className?: string
15
+ }
16
+ type MemberRole = "owner" | "admin" | "member" //todo
17
+ const getRoleIcon = (role: MemberRole) => {
18
+ switch (role) {
19
+ case "owner":
20
+ return <Crown className="h-3 w-3" />
21
+ case "admin":
22
+ return <Shield className="h-3 w-3" />
23
+ case "member":
24
+ return <User className="h-3 w-3" />
25
+ default:
26
+ return <User className="h-3 w-3" />
27
+ }
28
+ }
29
+
30
+ const getRoleColor = (role: MemberRole) => {
31
+ switch (role) {
32
+ case "owner":
33
+ return "bg-yellow-100 text-yellow-800 border-yellow-200"
34
+ case "admin":
35
+ return "bg-purple-100 text-purple-800 border-purple-200"
36
+ case "member":
37
+ return "bg-gray-100 text-gray-800 border-gray-200"
38
+ default:
39
+ return "bg-gray-100 text-gray-800 border-gray-200"
40
+ }
41
+ }
42
+
43
+ export const OrganizationMembers: React.FC<OrganizationMembersProps> = ({
44
+ orgId,
45
+ className,
46
+ }) => {
47
+ const { data: members = [], isLoading } = useListOrgMembers({ orgId })
48
+
49
+ if (isLoading) {
50
+ return (
51
+ <div
52
+ className={cn(
53
+ "bg-white rounded-lg border border-gray-200 p-4 sm:p-6",
54
+ className,
55
+ )}
56
+ >
57
+ <div className="mb-4">
58
+ <h2 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
59
+ Members
60
+ </h2>
61
+ </div>
62
+ <div className="text-center py-20 text-gray-500 grid place-items-center">
63
+ <Loader2 className="w-8 h-8 text-blue-600 dark:text-blue-400 animate-spin" />
64
+ <p className="mt-2 text-sm select-none">Loading members...</p>
65
+ </div>
66
+ </div>
67
+ )
68
+ }
69
+ return (
70
+ <div
71
+ className={cn(
72
+ "bg-white rounded-lg border border-gray-200 p-4 py-20 sm:p-6",
73
+ className,
74
+ )}
75
+ >
76
+ <div className="mb-4">
77
+ <h2 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
78
+ Members ({members.length})
79
+ </h2>
80
+ </div>
81
+
82
+ <div className="space-y-2 sm:space-y-3">
83
+ {members.map((member) => (
84
+ <Link
85
+ key={member.account_id || member.github_username}
86
+ href={`/${member.github_username}`}
87
+ className="block"
88
+ >
89
+ <div className="flex items-center justify-between p-3 rounded-lg hover:bg-gray-50 transition-colors cursor-pointer">
90
+ <div className="flex items-center gap-3 min-w-0 flex-1">
91
+ <Avatar className="h-10 w-10 sm:h-12 sm:w-12 flex-shrink-0">
92
+ <AvatarImage
93
+ src={`https://github.com/${member.github_username}.png`}
94
+ alt={`${member.github_username} avatar`}
95
+ />
96
+ <AvatarFallback className="text-sm font-medium">
97
+ {member.github_username
98
+ .split(" ")
99
+ .map((word) => word[0])
100
+ .join("")
101
+ .toUpperCase()
102
+ .slice(0, 2)}
103
+ </AvatarFallback>
104
+ </Avatar>
105
+
106
+ <div className="min-w-0 flex-1">
107
+ <div className="flex items-center gap-2 mb-1">
108
+ <h3 className="font-medium text-gray-900 truncate">
109
+ {member.github_username}
110
+ </h3>
111
+ <Badge
112
+ variant="outline"
113
+ className={cn(
114
+ "text-xs flex items-center gap-1 flex-shrink-0",
115
+ getRoleColor("admin"),
116
+ )}
117
+ >
118
+ {getRoleIcon("admin")}
119
+ {"admin"}
120
+ </Badge>
121
+ </div>
122
+ <p className="text-sm text-gray-500 truncate">
123
+ @{member.github_username}
124
+ </p>
125
+ </div>
126
+ </div>
127
+
128
+ <div className="text-right flex-shrink-0 hidden sm:block">
129
+ <p className="text-xs text-gray-500">
130
+ Joined {timeAgo(new Date())}
131
+ </p>
132
+ </div>
133
+ </div>
134
+ </Link>
135
+ ))}
136
+ </div>
137
+
138
+ {members.length === 0 && (
139
+ <div className="text-center py-6 sm:py-8 text-gray-500">
140
+ <Users className="h-10 w-10 sm:h-12 sm:w-12 mx-auto mb-3 text-gray-300" />
141
+ <p className="text-sm sm:text-base">No members found</p>
142
+ </div>
143
+ )}
144
+ </div>
145
+ )
146
+ }
@@ -1,5 +1,4 @@
1
1
  import { CodeEditor } from "@/components/package-port/CodeEditor"
2
- import { usePackageVisibilitySettingsDialog } from "@/components/dialogs/package-visibility-settings-dialog"
3
2
  import { useConfirmDiscardChangesDialog } from "@/components/dialogs/confirm-discard-changes-dialog"
4
3
  import { useToast } from "@/hooks/use-toast"
5
4
  import { useUrlParams } from "@/hooks/use-url-params"
@@ -15,6 +14,7 @@ import { toastManualEditConflicts } from "@/lib/utils/toastManualEditConflicts"
15
14
  import { ManualEditEvent } from "@tscircuit/props"
16
15
  import { useFileManagement } from "@/hooks/useFileManagement"
17
16
  import { isHiddenFile } from "../ViewPackagePage/utils/is-hidden-file"
17
+ import { useNewPackageSavePromptDialog } from "../dialogs/new-package-save-prompt-dialog"
18
18
 
19
19
  interface Props {
20
20
  pkg?: Package
@@ -57,7 +57,7 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
57
57
  pkg?.snippet_type ?? templateFromUrl?.type ?? urlParams.snippet_type
58
58
 
59
59
  const { Dialog: NewPackageSaveDialog, openDialog: openNewPackageSaveDialog } =
60
- usePackageVisibilitySettingsDialog()
60
+ useNewPackageSavePromptDialog()
61
61
 
62
62
  const { Dialog: DiscardChangesDialog, openDialog: openDiscardChangesDialog } =
63
63
  useConfirmDiscardChangesDialog()
@@ -106,8 +106,6 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
106
106
  [localFiles, initialFiles, isSaving, state.lastSavedAt],
107
107
  )
108
108
 
109
- useWarnUserOnPageChange({ hasUnsavedChanges })
110
-
111
109
  const handleEditEvent = (event: ManualEditEvent) => {
112
110
  const parsedManualEdits = JSON.parse(
113
111
  localFiles.find((x) => x.path === "manual-edits.json")?.content || "{}",
@@ -153,12 +151,13 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
153
151
  })
154
152
  }
155
153
 
156
- const finalfsMap = useMemo(
157
- () => (Object.keys(fsMap).length > 0 ? fsMap : {}),
158
- [fsMap],
159
- )
154
+ useWarnUserOnPageChange({
155
+ hasUnsavedChanges: Boolean(hasUnsavedChanges),
156
+ isPackageThere: Boolean(pkg),
157
+ })
158
+
160
159
  return (
161
- <div className="flex flex-col min-h-[50vh]">
160
+ <div className="flex flex-col h-full">
162
161
  <EditorNav
163
162
  circuitJson={state.circuitJson}
164
163
  pkg={pkg}
@@ -177,7 +176,9 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
177
176
  packageFilesMeta={packageFilesMeta}
178
177
  />
179
178
  <div
180
- className={`flex ${state.showPreview ? "flex-col md:flex-row" : ""}`}
179
+ className={`flex flex-1 min-h-0 ${
180
+ state.showPreview ? "flex-col md:flex-row" : ""
181
+ }`}
181
182
  >
182
183
  <div
183
184
  className={cn(
@@ -215,7 +216,7 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
215
216
  </div>
216
217
  <div
217
218
  className={cn(
218
- "flex p-0 flex-col min-h-[640px] overflow-y-hidden",
219
+ "flex p-0 flex-col overflow-y-hidden",
219
220
  state.fullScreen
220
221
  ? "fixed inset-0 z-50 bg-white p-4 overflow-hidden"
221
222
  : "w-full md:w-1/2",
@@ -223,8 +224,10 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
223
224
  )}
224
225
  >
225
226
  <SuspenseRunFrame
227
+ showFileMenu={false}
226
228
  showRunButton
227
229
  forceLatestEvalVersion
230
+ isLoadingFiles={isLoading}
228
231
  onRenderStarted={() =>
229
232
  setState((prev) => ({ ...prev, lastRunCode: currentFileCode }))
230
233
  }
@@ -236,7 +239,7 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
236
239
  onEditEvent={(event) => {
237
240
  handleEditEvent(event)
238
241
  }}
239
- fsMap={finalfsMap}
242
+ fsMap={fsMap}
240
243
  projectUrl={projectUrl}
241
244
  />
242
245
  </div>
@@ -23,7 +23,7 @@ import {
23
23
  createSystem,
24
24
  createVirtualTypeScriptEnvironment,
25
25
  } from "@typescript/vfs"
26
- import { loadDefaultLibMap } from "@/lib/ts-lib-cache"
26
+ import { loadDefaultLibMap, fetchWithPackageCaching } from "@/lib/ts-lib-cache"
27
27
  import { tsAutocomplete, tsFacet, tsSync } from "@valtown/codemirror-ts"
28
28
  import { getLints } from "@valtown/codemirror-ts"
29
29
  import { EditorView } from "codemirror"
@@ -121,7 +121,7 @@ export const CodeEditor = ({
121
121
  useViewTsFilesDialog()
122
122
 
123
123
  const entryPointFileName = useMemo(() => {
124
- const entryPointFile = findTargetFile(files, null)
124
+ const entryPointFile = findTargetFile({ files, filePathFromUrl: null })
125
125
  if (entryPointFile?.path) return entryPointFile.path
126
126
  return files.find((x) => x.path === "index.tsx")?.path || "index.tsx"
127
127
  }, [files])
@@ -136,7 +136,7 @@ export const CodeEditor = ({
136
136
  // Only run this if there's an explicit file_path in URL - don't auto-select files
137
137
  if (!filePathFromUrl) return
138
138
 
139
- const targetFile = findTargetFile(files, filePathFromUrl)
139
+ const targetFile = findTargetFile({ files, filePathFromUrl })
140
140
  if (targetFile) {
141
141
  const lineNumber = lineNumberFromUrl
142
142
  ? parseInt(lineNumberFromUrl, 10)
@@ -219,33 +219,7 @@ export const CodeEditor = ({
219
219
  projectName: "my-project",
220
220
  typescript: tsModule,
221
221
  logger: console,
222
- fetcher: (async (input: RequestInfo | URL, init?: RequestInit) => {
223
- const registryPrefixes = [
224
- "https://data.jsdelivr.com/v1/package/resolve/npm/@tsci/",
225
- "https://data.jsdelivr.com/v1/package/npm/@tsci/",
226
- "https://cdn.jsdelivr.net/npm/@tsci/",
227
- ]
228
- if (
229
- typeof input === "string" &&
230
- registryPrefixes.some((prefix) => input.startsWith(prefix))
231
- ) {
232
- const fullPackageName = input
233
- .replace(registryPrefixes[0], "")
234
- .replace(registryPrefixes[1], "")
235
- .replace(registryPrefixes[2], "")
236
- const packageName = fullPackageName.split("/")[0].replace(/\./, "/")
237
- const pathInPackage = fullPackageName.split("/").slice(1).join("/")
238
- const jsdelivrPath = `${packageName}${
239
- pathInPackage ? `/${pathInPackage}` : ""
240
- }`
241
- return fetch(
242
- `${apiUrl}/snippets/download?jsdelivr_resolve=${input.includes(
243
- "/resolve/",
244
- )}&jsdelivr_path=${encodeURIComponent(jsdelivrPath)}`,
245
- )
246
- }
247
- return fetch(input, init)
248
- }) as typeof fetch,
222
+ fetcher: fetchWithPackageCaching as typeof fetch,
249
223
  delegate: {
250
224
  started: () => {
251
225
  const manualEditsTypeDeclaration = `
@@ -1,4 +1,4 @@
1
- import React, { useState, useCallback } from "react"
1
+ import React, { useState, useCallback, useMemo } from "react"
2
2
  import { Button } from "@/components/ui/button"
3
3
  import { handleManualEditsImportWithSupportForMultipleFiles } from "@/lib/handleManualEditsImportWithSupportForMultipleFiles"
4
4
  import { useImportComponentDialog } from "@/components/dialogs/import-component-dialog"
@@ -25,11 +25,14 @@ import {
25
25
  TooltipProvider,
26
26
  TooltipTrigger,
27
27
  } from "@/components/ui/tooltip"
28
- import { convertRawEasyToTsx, fetchEasyEDAComponent } from "easyeda/browser"
29
- import { ComponentSearchResult } from "@tscircuit/runframe/runner"
30
- import { useApiBaseUrl } from "@/hooks/use-packages-base-api-url"
28
+ import {
29
+ JlcpcbComponentTsxLoadedPayload,
30
+ KicadStringSelectedPayload,
31
+ TscircuitPackageSelectedPayload,
32
+ } from "@tscircuit/runframe/runner"
31
33
  import { ICreateFileProps, ICreateFileResult } from "@/hooks/useFileManagement"
32
34
  import { useGlobalStore } from "@/hooks/use-global-store"
35
+ import { openJlcpcbImportIssue } from "@/hooks/use-jlcpcb-component-import"
33
36
 
34
37
  export type FileName = string
35
38
 
@@ -59,12 +62,18 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
59
62
  }) => {
60
63
  const { Dialog: ImportComponentDialog, openDialog: openImportDialog } =
61
64
  useImportComponentDialog()
62
- const { toast, toastLibrary } = useToast()
65
+ const { toast } = useToast()
63
66
  const [sidebarOpen, setSidebarOpen] = fileSidebarState
64
- const API_BASE = useApiBaseUrl()
65
67
  const [aiAutocompleteEnabled, setAiAutocompleteEnabled] = aiAutocompleteState
66
68
  const session = useGlobalStore((s) => s.session)
67
69
 
70
+ const jlcpcbProxyRequestHeaders = useMemo(() => {
71
+ if (!session?.token) return undefined
72
+ return {
73
+ Authorization: `Bearer ${session.token}`,
74
+ }
75
+ }, [session?.token])
76
+
68
77
  const handleFormatFile = useCallback(() => {
69
78
  if (!window.prettier || !window.prettierPlugins) return
70
79
  if (!currentFile) return
@@ -154,54 +163,114 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
154
163
  }
155
164
  }, [currentFile, files, toast, updateFileContent])
156
165
 
157
- const handleComponentImport = async (component: ComponentSearchResult) => {
158
- if (component.source == "tscircuit.com") {
159
- const newContent = `import {} from "@tsci/${component.owner}.${component.name}"\n${files[currentFile || ""]}`
160
- updateFileContent(currentFile, newContent)
161
- }
162
- if (component.source == "jlcpcb") {
163
- if (!session?.token) {
164
- throw new Error("You need to be logged in to import jlcpcb component")
166
+ const handleTscircuitPackageSelected = useCallback(
167
+ async ({ fullPackageName }: TscircuitPackageSelectedPayload) => {
168
+ if (!currentFile) {
169
+ const message = "Select a file before importing a component."
170
+ toast({
171
+ title: "No file selected",
172
+ description: message,
173
+ variant: "destructive",
174
+ })
175
+ throw new Error(message)
165
176
  }
166
177
 
167
- const jlcpcbComponent = await fetchEasyEDAComponent(
168
- component.partNumber ?? component.name,
169
- {
170
- fetch: ((url, options: any) => {
171
- return fetch(`${API_BASE}/proxy`, {
172
- body: options.body,
173
- method: options.method,
174
- headers: {
175
- authority: options.headers.authority,
176
- Authorization: `Bearer ${session?.token}`,
177
- "X-Target-Url": url.toString(),
178
- "X-Sender-Host": options.headers.origin,
179
- "X-Sender-Origin": options.headers.origin,
180
- "content-type": options.headers["content-type"],
181
- },
182
- })
183
- }) as typeof fetch,
184
- },
185
- )
186
- const tsxComponent = await convertRawEasyToTsx(jlcpcbComponent)
187
- let componentName = component.name.replace(/ /g, "-")
188
- let componentPath = `imports/${componentName}.tsx`
189
- if (files[componentPath] || files[`./${componentPath}`]) {
190
- componentName = `${componentName}-1`
191
- componentPath = `imports/${componentName}.tsx`
192
- }
193
- const createFileResult = createFile({
194
- newFileName: componentPath,
195
- content: tsxComponent,
196
- onError: (error) => {
197
- throw error
198
- },
178
+ const existingContent = files[currentFile] ?? ""
179
+ const newContent = `import {} from "${fullPackageName}"\n${existingContent}`
180
+ updateFileContent(currentFile, newContent)
181
+ toast({
182
+ title: "Component imported",
183
+ description: `Added ${fullPackageName} to ${currentFile}.`,
199
184
  })
200
- if (!createFileResult.newFileCreated) {
201
- throw new Error("Failed to create component file")
185
+ },
186
+ [currentFile, files, toast, updateFileContent],
187
+ )
188
+
189
+ const handleJlcpcbComponentTsxLoaded = useCallback(
190
+ async ({ result, tsx }: JlcpcbComponentTsxLoadedPayload) => {
191
+ const partNumber = result.component.partNumber || "component"
192
+
193
+ try {
194
+ const sanitizedBaseName = partNumber
195
+ .toLowerCase()
196
+ .replace(/[^a-z0-9_-]/gi, "-")
197
+ let componentPath = `imports/${sanitizedBaseName}.tsx`
198
+ let suffix = 1
199
+ while (files[componentPath] || files[`./${componentPath}`]) {
200
+ componentPath = `imports/${sanitizedBaseName}-${suffix}.tsx`
201
+ suffix += 1
202
+ }
203
+
204
+ const createFileResult = createFile({
205
+ newFileName: componentPath,
206
+ content: tsx,
207
+ onError: (error) => {
208
+ throw error
209
+ },
210
+ })
211
+
212
+ if (!createFileResult.newFileCreated) {
213
+ throw new Error("Failed to create component file")
214
+ }
215
+
216
+ toast({
217
+ title: "Component imported",
218
+ description: `${partNumber} saved to ${componentPath}.`,
219
+ })
220
+ } catch (error) {
221
+ const message =
222
+ error instanceof Error
223
+ ? error.message
224
+ : "Failed to import component from JLCPCB"
225
+
226
+ toast({
227
+ title: "JLCPCB import failed",
228
+ description: (
229
+ <div className="space-y-2">
230
+ <p>{message}</p>
231
+ <button
232
+ className="text-sm text-blue-500 hover:underline"
233
+ onClick={(event) => {
234
+ event.preventDefault()
235
+ openJlcpcbImportIssue(partNumber, message)
236
+ }}
237
+ >
238
+ File issue on GitHub
239
+ </button>
240
+ </div>
241
+ ),
242
+ variant: "destructive",
243
+ })
244
+
245
+ throw new Error(message)
202
246
  }
203
- }
204
- }
247
+ },
248
+ [createFile, files, toast],
249
+ )
250
+
251
+ const handleKicadStringSelected = useCallback(
252
+ async ({ footprint, result }: KicadStringSelectedPayload) => {
253
+ try {
254
+ await navigator.clipboard.writeText(footprint)
255
+ toast({
256
+ title: "KiCad footprint copied",
257
+ description: `${result.footprint.qualifiedName} copied to clipboard.`,
258
+ })
259
+ } catch (error) {
260
+ const message =
261
+ error instanceof Error
262
+ ? error.message
263
+ : "Failed to copy KiCad footprint to clipboard"
264
+ toast({
265
+ title: "KiCad import failed",
266
+ description: message,
267
+ variant: "destructive",
268
+ })
269
+ throw new Error(message)
270
+ }
271
+ },
272
+ [toast],
273
+ )
205
274
 
206
275
  return (
207
276
  <>
@@ -345,17 +414,10 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
345
414
  </Button>
346
415
  </div>
347
416
  <ImportComponentDialog
348
- onComponentSelected={async (component) => {
349
- toastLibrary.promise(handleComponentImport(component), {
350
- loading: "Importing component...",
351
- success: <p>Component imported successfully!</p>,
352
- error: (error) => (
353
- <p>
354
- Error importing component: {error.message || String(error)}
355
- </p>
356
- ),
357
- })
358
- }}
417
+ onTscircuitPackageSelected={handleTscircuitPackageSelected}
418
+ onJlcpcbComponentTsxLoaded={handleJlcpcbComponentTsxLoaded}
419
+ onKicadStringSelected={handleKicadStringSelected}
420
+ jlcpcbProxyRequestHeaders={jlcpcbProxyRequestHeaders}
359
421
  />
360
422
  </div>
361
423
  </>