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