@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
@@ -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
  </>
@@ -1,5 +1,5 @@
1
1
  import { Button } from "@/components/ui/button"
2
- import { GitFork, Star } from "lucide-react"
2
+ import { GitFork, Loader2, Star } from "lucide-react"
3
3
  import {
4
4
  DropdownMenu,
5
5
  DropdownMenuContent,
@@ -42,7 +42,6 @@ import { useViewTsFilesDialog } from "@/components/dialogs/view-ts-files-dialog"
42
42
  import { DownloadButtonAndMenu } from "@/components/DownloadButtonAndMenu"
43
43
  import { TypeBadge } from "@/components/TypeBadge"
44
44
  import { useForkPackageMutation } from "@/hooks/useForkPackageMutation"
45
- import tscircuitCorePkg from "@tscircuit/core/package.json"
46
45
  import { useRenamePackageDialog } from "../dialogs/rename-package-dialog"
47
46
  import { useUpdatePackageDescriptionDialog } from "../dialogs/update-package-description-dialog"
48
47
  import { useCreateReleaseDialog } from "@/hooks/use-create-release-dialog"
@@ -295,52 +294,37 @@ export default function EditorNav({
295
294
  Not logged in, can't save
296
295
  </div>
297
296
  )}
298
- <Button
299
- variant="outline"
300
- size="sm"
301
- className={"ml-1 h-6 px-2 text-xs save-button"}
302
- disabled={canSavePackage && pkg ? !hasUnsavedChanges : !isLoggedIn}
303
- onClick={canSavePackage ? onSave : () => forkSnippet()}
304
- >
305
- {canSavePackage ? (
306
- <>
307
- <Save className="mr-1 h-3 w-3" />
308
- Save
309
- </>
310
- ) : (
311
- <>
312
- <GitFork className="mr-1 h-3 w-3" />
313
- Fork
314
- </>
315
- )}
316
- </Button>
297
+ {(canSavePackage || (isLoggedIn && pkg)) && (
298
+ <Button
299
+ variant="outline"
300
+ size="sm"
301
+ className={"ml-1 h-6 px-2 text-xs save-button"}
302
+ disabled={
303
+ canSavePackage && pkg ? !hasUnsavedChanges : !isLoggedIn
304
+ }
305
+ onClick={canSavePackage ? onSave : () => forkSnippet()}
306
+ >
307
+ {canSavePackage ? (
308
+ <>
309
+ <Save className="mr-1 h-3 w-3" />
310
+ Save
311
+ </>
312
+ ) : (
313
+ <>
314
+ <GitFork className="mr-1 h-3 w-3" />
315
+ Fork
316
+ </>
317
+ )}
318
+ </Button>
319
+ )}
317
320
  {isSaving && (
318
- <div className="animate-fadeIn bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
319
- <svg
320
- className="animate-spin h-3 w-3 mr-2 text-blue-600"
321
- xmlns="http://www.w3.org/2000/svg"
322
- fill="none"
323
- viewBox="0 0 24 24"
324
- >
325
- <circle
326
- className="opacity-25"
327
- cx="12"
328
- cy="12"
329
- r="10"
330
- stroke="currentColor"
331
- strokeWidth="4"
332
- ></circle>
333
- <path
334
- className="opacity-75"
335
- fill="currentColor"
336
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
337
- ></path>
338
- </svg>
321
+ <div className="animate-fadeIn bg-blue-100 select-none text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
322
+ <Loader2 className="animate-spin h-3 w-3 mr-2 text-blue-600" />
339
323
  Saving...
340
324
  </div>
341
325
  )}
342
326
  {hasUnsavedChanges && !isSaving && isLoggedIn && (
343
- <div className="animate-fadeIn bg-yellow-100 text-yellow-800 text-xs font-medium px-2.5 py-0.5 rounded">
327
+ <div className="animate-fadeIn bg-yellow-100 select-none text-yellow-800 text-xs font-medium px-2.5 py-0.5 rounded">
344
328
  {pkg ? "unsaved changes" : "unsaved"}
345
329
  </div>
346
330
  )}
@@ -359,7 +343,6 @@ export default function EditorNav({
359
343
  </div>
360
344
  <div className="flex items-center justify-end -space-x-1">
361
345
  <div className="flex mx-2 items-center space-x-1">
362
- {pkg && <TypeBadge type={`${packageType ?? pkg.snippet_type}`} />}
363
346
  {/* <Button
364
347
  variant="ghost"
365
348
  size="sm"
@@ -481,9 +464,6 @@ export default function EditorNav({
481
464
  </DropdownMenuItem>
482
465
  </>
483
466
  )}
484
- <DropdownMenuItem className="text-xs text-gray-500" disabled>
485
- @tscircuit/core@{tscircuitCorePkg.version}
486
- </DropdownMenuItem>
487
467
  </DropdownMenuContent>
488
468
  </DropdownMenu>
489
469
 
@@ -505,7 +485,7 @@ export default function EditorNav({
505
485
  )}
506
486
  </Button>
507
487
  </div>
508
- <div className="flex items-center ">
488
+ <div className="flex items-center">
509
489
  <DropdownMenu>
510
490
  <DropdownMenuTrigger asChild>
511
491
  <div className="md:hidden rounded-full p-1 hover:bg-gray-100 cursor-pointer">
@@ -514,7 +494,7 @@ export default function EditorNav({
514
494
  </Button>
515
495
  </div>
516
496
  </DropdownMenuTrigger>
517
- <DropdownMenuContent>
497
+ <DropdownMenuContent className="z-[101]">
518
498
  {hasUnsavedChanges && onDiscard && (
519
499
  <DropdownMenuItem
520
500
  className="text-xs text-red-600"
@@ -601,6 +581,9 @@ export default function EditorNav({
601
581
  <RenameDialog
602
582
  packageId={pkg?.package_id ?? ""}
603
583
  currentName={pkg?.unscoped_name ?? ""}
584
+ onRename={() => {
585
+ qc.invalidateQueries({ queryKey: ["package", pkg?.package_id] })
586
+ }}
604
587
  />
605
588
  <DeleteDialog
606
589
  packageId={pkg?.package_id ?? ""}