@tscircuit/fake-snippets 0.0.44 → 0.0.46
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-pver-release.yml +1 -0
- package/bun.lock +43 -11
- package/dist/bundle.js +405 -335
- package/dist/schema.d.ts +1845 -0
- package/dist/schema.js +251 -0
- package/fake-snippets-api/routes/api/_fake/received_quotes.ts +66 -0
- package/fake-snippets-api/routes/api/package_releases/update.ts +25 -18
- package/package.json +8 -3
- package/src/components/CodeAndPreview.tsx +0 -1
- package/src/components/CodeEditor.tsx +0 -1
- package/src/components/CodeEditorHeader.tsx +0 -25
- package/src/components/EditorNav.tsx +10 -8
- package/src/components/ErrorOutline.tsx +35 -0
- package/src/components/FileSidebar.tsx +46 -16
- package/src/components/NotFound.tsx +37 -0
- package/src/components/PreviewContent.tsx +0 -6
- package/src/components/TrendingSnippetCarousel.tsx +1 -1
- package/src/components/ViewPackagePage/components/package-header.tsx +24 -3
- package/src/components/ViewPackagePage/utils/is-hidden-file.ts +0 -1
- package/src/components/dialogs/package-visibility-settings-dialog.tsx +10 -1
- package/src/components/dialogs/view-ts-files-dialog.tsx +0 -6
- package/src/components/package-port/CodeAndPreview.tsx +24 -9
- package/src/components/package-port/CodeEditor.tsx +26 -38
- package/src/components/package-port/CodeEditorHeader.tsx +117 -39
- package/src/components/package-port/EditorNav.tsx +10 -8
- package/src/components/ui/tree-view.tsx +5 -1
- package/src/{prettier.ts → lib/types.ts} +3 -1
- package/src/lib/utils/findTargetFile.ts +62 -0
- package/src/lib/utils/load-prettier.ts +3 -0
- package/src/pages/404.tsx +2 -33
- package/src/pages/package-editor.tsx +14 -3
- package/src/pages/user-profile.tsx +66 -27
- package/src/components/FootprintDialog.tsx +0 -339
- package/src/components/ParametersEditor.tsx +0 -140
- package/src/lib/utils/parseFootprintParams.ts +0 -52
|
@@ -9,7 +9,7 @@ const treeVariants = cva(
|
|
|
9
9
|
)
|
|
10
10
|
|
|
11
11
|
const selectedTreeVariants = cva(
|
|
12
|
-
"before:opacity-100 before:bg-slate-100/70 text-accent-foreground
|
|
12
|
+
"before:opacity-100 before:bg-slate-100/70 text-accent-foreground dark:before:bg-slate-800/70",
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
const dragOverVariants = cva(
|
|
@@ -58,6 +58,10 @@ const TreeView = React.forwardRef<HTMLDivElement, TreeProps>(
|
|
|
58
58
|
string | undefined
|
|
59
59
|
>(initialSelectedItemId)
|
|
60
60
|
|
|
61
|
+
React.useEffect(() => {
|
|
62
|
+
setSelectedItemId(initialSelectedItemId)
|
|
63
|
+
}, [initialSelectedItemId])
|
|
64
|
+
|
|
61
65
|
const [draggedItem, setDraggedItem] = React.useState<TreeDataItem | null>(
|
|
62
66
|
null,
|
|
63
67
|
)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
// Prettier is injected into the global scope inside index.html
|
|
2
1
|
declare global {
|
|
3
2
|
interface Window {
|
|
3
|
+
TSCIRCUIT_REGISTRY_API_BASE_URL: string
|
|
4
|
+
TSCIRCUIT_3D_OBJECT_REF: any
|
|
5
|
+
__DEBUG_CODE_EDITOR_FS_MAP: Map<string, string>
|
|
4
6
|
prettier: {
|
|
5
7
|
format: (code: string, options: any) => string
|
|
6
8
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { PackageFile } from "@/components/package-port/CodeAndPreview"
|
|
2
|
+
|
|
3
|
+
export const findMainEntrypointFileFromTscircuitConfig = (
|
|
4
|
+
files: PackageFile[],
|
|
5
|
+
): PackageFile | null => {
|
|
6
|
+
const configFile = files.find((file) => file.path === "tscircuit.config.json")
|
|
7
|
+
|
|
8
|
+
if (configFile) {
|
|
9
|
+
try {
|
|
10
|
+
const config = JSON.parse(configFile.content)
|
|
11
|
+
|
|
12
|
+
if (config && typeof config.mainEntrypoint === "string") {
|
|
13
|
+
const mainComponentPath = config.mainEntrypoint
|
|
14
|
+
|
|
15
|
+
const normalizedPath = mainComponentPath.startsWith("./")
|
|
16
|
+
? mainComponentPath.substring(2)
|
|
17
|
+
: mainComponentPath
|
|
18
|
+
|
|
19
|
+
return files.find((file) => file.path === normalizedPath) ?? null
|
|
20
|
+
}
|
|
21
|
+
} catch {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const findTargetFile = (
|
|
28
|
+
files: PackageFile[],
|
|
29
|
+
filePathFromUrl: string | null,
|
|
30
|
+
): PackageFile | null => {
|
|
31
|
+
if (files.length === 0) {
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let targetFile: PackageFile | null = null
|
|
36
|
+
|
|
37
|
+
if (filePathFromUrl) {
|
|
38
|
+
targetFile = files.find((file) => file.path === filePathFromUrl) ?? null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!targetFile) {
|
|
42
|
+
targetFile = findMainEntrypointFileFromTscircuitConfig(files)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!targetFile) {
|
|
46
|
+
targetFile = files.find((file) => file.path === "index.tsx") ?? null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!targetFile) {
|
|
50
|
+
targetFile = files.find((file) => file.path.endsWith(".tsx")) ?? null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!targetFile) {
|
|
54
|
+
targetFile = files.find((file) => file.path === "index.ts") ?? null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!targetFile && files[0]) {
|
|
58
|
+
targetFile = files[0]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return targetFile
|
|
62
|
+
}
|
package/src/pages/404.tsx
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import React from "react"
|
|
2
1
|
import { Helmet } from "react-helmet"
|
|
3
2
|
import { Header2 } from "@/components/Header2"
|
|
4
3
|
import Footer from "@/components/Footer"
|
|
5
|
-
import {
|
|
6
|
-
import { PrefetchPageLink } from "@/components/PrefetchPageLink"
|
|
4
|
+
import { NotFound } from "@/components/NotFound"
|
|
7
5
|
|
|
8
6
|
export function NotFoundPage({
|
|
9
7
|
heading = "Page Not Found",
|
|
@@ -18,36 +16,7 @@ export function NotFoundPage({
|
|
|
18
16
|
/>
|
|
19
17
|
</Helmet>
|
|
20
18
|
<Header2 />
|
|
21
|
-
<
|
|
22
|
-
<div className="container px-4 md:px-6 py-12 flex flex-col items-center text-center max-w-3xl">
|
|
23
|
-
<div className="mb-8 flex flex-col items-center justify-center">
|
|
24
|
-
<div className="mb-2">
|
|
25
|
-
<span className="text-3xl font-bold text-white bg-blue-500 px-4 py-2 rounded-md shadow-md inline-block">
|
|
26
|
-
404
|
|
27
|
-
</span>
|
|
28
|
-
</div>
|
|
29
|
-
</div>
|
|
30
|
-
<h1 className="text-4xl font-extrabold tracking-tight lg:text-5xl mb-4">
|
|
31
|
-
{heading}
|
|
32
|
-
</h1>
|
|
33
|
-
<p className="text-xl text-muted-foreground mb-8">
|
|
34
|
-
The page you're looking for doesn't exist or has been moved to
|
|
35
|
-
another address.
|
|
36
|
-
</p>
|
|
37
|
-
<div className="flex flex-col sm:flex-row gap-4">
|
|
38
|
-
<PrefetchPageLink href="/">
|
|
39
|
-
<Button size="lg" className="bg-blue-500 hover:bg-blue-600">
|
|
40
|
-
Return Home
|
|
41
|
-
</Button>
|
|
42
|
-
</PrefetchPageLink>
|
|
43
|
-
<PrefetchPageLink href="/search">
|
|
44
|
-
<Button size="lg" variant="outline">
|
|
45
|
-
Search Packages
|
|
46
|
-
</Button>
|
|
47
|
-
</PrefetchPageLink>
|
|
48
|
-
</div>
|
|
49
|
-
</div>
|
|
50
|
-
</main>
|
|
19
|
+
<NotFound heading={heading} />
|
|
51
20
|
<Footer />
|
|
52
21
|
</div>
|
|
53
22
|
)
|
|
@@ -4,10 +4,15 @@ import Header from "@/components/Header"
|
|
|
4
4
|
import { usePackage } from "@/hooks/use-package"
|
|
5
5
|
import { Helmet } from "react-helmet-async"
|
|
6
6
|
import { useCurrentPackageId } from "@/hooks/use-current-package-id"
|
|
7
|
+
import { NotFound } from "@/components/NotFound"
|
|
8
|
+
import { ErrorOutline } from "@/components/ErrorOutline"
|
|
7
9
|
|
|
8
10
|
export const EditorPage = () => {
|
|
9
11
|
const { packageId } = useCurrentPackageId()
|
|
10
12
|
const { data: pkg, isLoading, error } = usePackage(packageId)
|
|
13
|
+
const uuid4RegExp = new RegExp(
|
|
14
|
+
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/,
|
|
15
|
+
)
|
|
11
16
|
return (
|
|
12
17
|
<div className="overflow-x-hidden">
|
|
13
18
|
<Helmet>
|
|
@@ -32,10 +37,16 @@ export const EditorPage = () => {
|
|
|
32
37
|
</Helmet>
|
|
33
38
|
<Header />
|
|
34
39
|
{!error && <CodeAndPreview pkg={pkg} />}
|
|
35
|
-
{error &&
|
|
40
|
+
{error &&
|
|
41
|
+
(error.status === 404 || !uuid4RegExp.test(packageId ?? "")) && (
|
|
42
|
+
<NotFound heading="Package not found" />
|
|
43
|
+
)}
|
|
36
44
|
{error && error.status !== 404 && (
|
|
37
|
-
<div className="
|
|
38
|
-
|
|
45
|
+
<div className="min-h-screen grid place-items-center">
|
|
46
|
+
<ErrorOutline
|
|
47
|
+
error={error}
|
|
48
|
+
description={"There was an error loading the editor page"}
|
|
49
|
+
/>
|
|
39
50
|
</div>
|
|
40
51
|
)}
|
|
41
52
|
<Footer />
|
|
@@ -15,12 +15,20 @@ import type React from "react"
|
|
|
15
15
|
import { useState } from "react"
|
|
16
16
|
import { useQuery } from "react-query"
|
|
17
17
|
import { useParams } from "wouter"
|
|
18
|
+
import {
|
|
19
|
+
Select,
|
|
20
|
+
SelectContent,
|
|
21
|
+
SelectItem,
|
|
22
|
+
SelectTrigger,
|
|
23
|
+
SelectValue,
|
|
24
|
+
} from "@/components/ui/select"
|
|
18
25
|
|
|
19
26
|
export const UserProfilePage = () => {
|
|
20
27
|
const { username } = useParams()
|
|
21
28
|
const axios = useAxios()
|
|
22
29
|
const [searchQuery, setSearchQuery] = useState("")
|
|
23
30
|
const [activeTab, setActiveTab] = useState("all")
|
|
31
|
+
const [filter, setFilter] = useState("most-recent") // Changed default from "newest" to "most-recent"
|
|
24
32
|
const session = useGlobalStore((s) => s.session)
|
|
25
33
|
const isCurrentUserProfile = username === session?.github_username
|
|
26
34
|
const { Dialog: DeleteDialog, openDialog: openDeleteDialog } =
|
|
@@ -55,12 +63,31 @@ export const UserProfilePage = () => {
|
|
|
55
63
|
const isLoading =
|
|
56
64
|
activeTab === "starred" ? isLoadingStarredSnippets : isLoadingUserSnippets
|
|
57
65
|
|
|
58
|
-
const filteredSnippets = snippetsToShow
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
const filteredSnippets = snippetsToShow
|
|
67
|
+
?.filter((snippet) => {
|
|
68
|
+
return (
|
|
69
|
+
!searchQuery ||
|
|
70
|
+
snippet.unscoped_name
|
|
71
|
+
.toLowerCase()
|
|
72
|
+
.includes(searchQuery.toLowerCase().trim())
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
?.sort((a, b) => {
|
|
76
|
+
switch (filter) {
|
|
77
|
+
case "most-recent":
|
|
78
|
+
return b.updated_at.localeCompare(a.updated_at)
|
|
79
|
+
case "least-recent":
|
|
80
|
+
return a.updated_at.localeCompare(b.updated_at)
|
|
81
|
+
case "most-starred":
|
|
82
|
+
return (b.star_count || 0) - (a.star_count || 0)
|
|
83
|
+
case "a-z":
|
|
84
|
+
return a.unscoped_name.localeCompare(b.unscoped_name)
|
|
85
|
+
case "z-a":
|
|
86
|
+
return b.unscoped_name.localeCompare(a.unscoped_name)
|
|
87
|
+
default:
|
|
88
|
+
return 0
|
|
89
|
+
}
|
|
90
|
+
})
|
|
64
91
|
|
|
65
92
|
const handleDeleteClick = (e: React.MouseEvent, snippet: Snippet) => {
|
|
66
93
|
e.preventDefault() // Prevent navigation
|
|
@@ -105,13 +132,27 @@ export const UserProfilePage = () => {
|
|
|
105
132
|
<TabsTrigger value="starred">Starred Packages</TabsTrigger>
|
|
106
133
|
</TabsList>
|
|
107
134
|
</Tabs>
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
135
|
+
<div className="flex gap-4 mb-4">
|
|
136
|
+
<Input
|
|
137
|
+
type="text"
|
|
138
|
+
placeholder="Searching User Packages..."
|
|
139
|
+
value={searchQuery}
|
|
140
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
141
|
+
className="mb-4"
|
|
142
|
+
/>
|
|
143
|
+
<Select value={filter} onValueChange={setFilter}>
|
|
144
|
+
<SelectTrigger className="w-[180px]">
|
|
145
|
+
<SelectValue placeholder="Sort by" />
|
|
146
|
+
</SelectTrigger>
|
|
147
|
+
<SelectContent>
|
|
148
|
+
<SelectItem value="most-recent">Most Recent</SelectItem>
|
|
149
|
+
<SelectItem value="least-recent">Least Recent</SelectItem>
|
|
150
|
+
<SelectItem value="most-starred">Most Starred</SelectItem>
|
|
151
|
+
<SelectItem value="a-z">A-Z</SelectItem>
|
|
152
|
+
<SelectItem value="z-a">Z-A</SelectItem>
|
|
153
|
+
</SelectContent>
|
|
154
|
+
</Select>
|
|
155
|
+
</div>
|
|
115
156
|
{isLoading ? (
|
|
116
157
|
<div>
|
|
117
158
|
{activeTab === "starred"
|
|
@@ -120,20 +161,18 @@ export const UserProfilePage = () => {
|
|
|
120
161
|
</div>
|
|
121
162
|
) : (
|
|
122
163
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
123
|
-
{filteredSnippets
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
/>
|
|
136
|
-
))}
|
|
164
|
+
{filteredSnippets?.map((snippet) => (
|
|
165
|
+
<SnippetCard
|
|
166
|
+
key={snippet.snippet_id}
|
|
167
|
+
snippet={snippet}
|
|
168
|
+
baseUrl={baseUrl}
|
|
169
|
+
showOwner={activeTab === "starred"}
|
|
170
|
+
isCurrentUserSnippet={
|
|
171
|
+
isCurrentUserProfile && activeTab === "all"
|
|
172
|
+
}
|
|
173
|
+
onDeleteClick={handleDeleteClick}
|
|
174
|
+
/>
|
|
175
|
+
))}
|
|
137
176
|
</div>
|
|
138
177
|
)}
|
|
139
178
|
</div>
|
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
import { Input } from "./ui/input"
|
|
2
|
-
import { useEffect, useMemo, useState } from "react"
|
|
3
|
-
import { parseFootprintParams } from "../lib/utils/parseFootprintParams"
|
|
4
|
-
import ParametersEditor from "./ParametersEditor"
|
|
5
|
-
import { convertCircuitJsonToPcbSvg } from "circuit-to-svg"
|
|
6
|
-
import { fp, getFootprintNamesByType } from "@tscircuit/footprinter"
|
|
7
|
-
import { useToast } from "../hooks/use-toast"
|
|
8
|
-
import { Button } from "./ui/button"
|
|
9
|
-
import { FileName } from "./CodeEditorHeader"
|
|
10
|
-
import {
|
|
11
|
-
Dialog,
|
|
12
|
-
DialogContent,
|
|
13
|
-
DialogDescription,
|
|
14
|
-
DialogHeader,
|
|
15
|
-
DialogTitle,
|
|
16
|
-
} from "./ui/dialog"
|
|
17
|
-
import { Copy, Check } from "lucide-react"
|
|
18
|
-
import { Combobox } from "./ui/combobox"
|
|
19
|
-
|
|
20
|
-
interface FootprintDialogProps {
|
|
21
|
-
currentFile: FileName
|
|
22
|
-
open: boolean
|
|
23
|
-
onOpenChange: (open: boolean) => void
|
|
24
|
-
updateFileContent: (filename: FileName, content: string) => void
|
|
25
|
-
files: Record<string, string>
|
|
26
|
-
cursorPosition?: number | null
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const PARAM_NAMES: any = {
|
|
30
|
-
p: "Pitch",
|
|
31
|
-
w: "Width",
|
|
32
|
-
num_pins: "Number of Pins",
|
|
33
|
-
pl: "Pad Length",
|
|
34
|
-
pw: "Pad Width",
|
|
35
|
-
id: "Inner Diameter",
|
|
36
|
-
od: "Outer Diameter",
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const FootprintDialog = ({
|
|
40
|
-
currentFile,
|
|
41
|
-
open,
|
|
42
|
-
onOpenChange,
|
|
43
|
-
updateFileContent,
|
|
44
|
-
files,
|
|
45
|
-
cursorPosition,
|
|
46
|
-
}: FootprintDialogProps) => {
|
|
47
|
-
const [footprintString, setFootprintString] = useState("")
|
|
48
|
-
const [footprintName, setFootprintName] = useState("")
|
|
49
|
-
const [previewSvg, setPreviewSvg] = useState<string | null>(null)
|
|
50
|
-
const [chipName, setChipName] = useState("")
|
|
51
|
-
const [footprintNameError, setFootprintNameError] = useState(false)
|
|
52
|
-
const [copied, setCopied] = useState(false)
|
|
53
|
-
const [error, setError] = useState<string | null>(null)
|
|
54
|
-
const { toast } = useToast()
|
|
55
|
-
|
|
56
|
-
const { normalFootprintNames } = getFootprintNamesByType()
|
|
57
|
-
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
if (copied) {
|
|
60
|
-
const timeout = setTimeout(() => {
|
|
61
|
-
setCopied(false)
|
|
62
|
-
}, 1000)
|
|
63
|
-
return () => clearTimeout(timeout)
|
|
64
|
-
}
|
|
65
|
-
}, [copied])
|
|
66
|
-
|
|
67
|
-
const params: any = useMemo(() => {
|
|
68
|
-
try {
|
|
69
|
-
return fp.string(footprintString).json()
|
|
70
|
-
} catch (error) {
|
|
71
|
-
return null
|
|
72
|
-
}
|
|
73
|
-
}, [footprintName, footprintString])
|
|
74
|
-
|
|
75
|
-
const updateFootprintString = (baseName: string, currentParams: any) => {
|
|
76
|
-
try {
|
|
77
|
-
const parsedParams = parseFootprintParams(currentParams)
|
|
78
|
-
|
|
79
|
-
if (parsedParams.missing && Array.isArray(parsedParams.missing)) {
|
|
80
|
-
parsedParams.missing =
|
|
81
|
-
parsedParams.missing.length > 0
|
|
82
|
-
? `missing(${parsedParams.missing.join(",")})`
|
|
83
|
-
: "missing()"
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
parsedParams.grid === "0x0" ||
|
|
88
|
-
parsedParams.grid === "0x" ||
|
|
89
|
-
/^(\d+x0|0x\d+)$/.test(parsedParams.grid as string)
|
|
90
|
-
) {
|
|
91
|
-
delete parsedParams.grid
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const paramsString = Object.entries(parsedParams)
|
|
95
|
-
.filter(([key]) => key !== "fn" && key !== "num_pins")
|
|
96
|
-
.map(([key, val]) => {
|
|
97
|
-
if (typeof val === "boolean") return val ? key : ""
|
|
98
|
-
if (key === "missing") return val
|
|
99
|
-
return `${key}${val}`
|
|
100
|
-
})
|
|
101
|
-
.filter((item) => item !== "")
|
|
102
|
-
.join("_")
|
|
103
|
-
|
|
104
|
-
const newFootprintString = paramsString
|
|
105
|
-
? `${baseName}_${paramsString}`
|
|
106
|
-
: baseName
|
|
107
|
-
setFootprintString(newFootprintString)
|
|
108
|
-
handleFootprintPreview(newFootprintString)
|
|
109
|
-
} catch (error) {
|
|
110
|
-
console.error("Error updating footprint string:", error)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const updateParam = (
|
|
115
|
-
paramName: string,
|
|
116
|
-
value: string | number | boolean | string[],
|
|
117
|
-
) => {
|
|
118
|
-
try {
|
|
119
|
-
let currentParams = parseFootprintParams({ ...params })
|
|
120
|
-
if (paramName === "num_pins") {
|
|
121
|
-
if (Number(value) < 1) value = 1
|
|
122
|
-
if (Number(value) > 4000) value = 4000
|
|
123
|
-
const baseNameWithoutNumber = footprintName.replace(/\d+$/, "")
|
|
124
|
-
const newName = `${baseNameWithoutNumber}${value}`
|
|
125
|
-
updateFootprintString(newName, currentParams)
|
|
126
|
-
return
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
currentParams[paramName] = value
|
|
130
|
-
if (currentParams.missing && Array.isArray(currentParams.missing)) {
|
|
131
|
-
currentParams.missing =
|
|
132
|
-
currentParams.missing.length > 0
|
|
133
|
-
? `missing(${currentParams.missing.join(",")})`
|
|
134
|
-
: "missing()"
|
|
135
|
-
}
|
|
136
|
-
currentParams = parseFootprintParams(currentParams)
|
|
137
|
-
const pinMatch = footprintString.match(/\d+(?=(_|$))/)
|
|
138
|
-
const pinNumber = pinMatch ? pinMatch[0] : ""
|
|
139
|
-
const baseNameWithoutNumber = footprintName.replace(/\d+$/, "")
|
|
140
|
-
const nameWithNumber = pinNumber
|
|
141
|
-
? `${baseNameWithoutNumber}${pinNumber}`
|
|
142
|
-
: footprintName
|
|
143
|
-
|
|
144
|
-
updateFootprintString(nameWithNumber, currentParams)
|
|
145
|
-
} catch (error) {
|
|
146
|
-
console.error("Error updating parameter:", error)
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const handleFootprintPreview = async (str: string) => {
|
|
151
|
-
try {
|
|
152
|
-
const circuitJson = fp.string(str).circuitJson()
|
|
153
|
-
const svg = convertCircuitJsonToPcbSvg(circuitJson)
|
|
154
|
-
setFootprintNameError(false)
|
|
155
|
-
setPreviewSvg(svg)
|
|
156
|
-
setError(null)
|
|
157
|
-
} catch (error) {
|
|
158
|
-
setFootprintNameError(true)
|
|
159
|
-
setPreviewSvg(null)
|
|
160
|
-
setError(
|
|
161
|
-
error instanceof Error
|
|
162
|
-
? error.message
|
|
163
|
-
: "Invalid footprint configuration",
|
|
164
|
-
)
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const handleInsertFootprint = () => {
|
|
169
|
-
try {
|
|
170
|
-
const tsxCode = `\n
|
|
171
|
-
<chip
|
|
172
|
-
name="${chipName}"
|
|
173
|
-
footprint="${footprintString}"
|
|
174
|
-
/>\n`
|
|
175
|
-
const currentContent = files[currentFile]
|
|
176
|
-
|
|
177
|
-
if (cursorPosition !== undefined && cursorPosition !== null) {
|
|
178
|
-
const newContent =
|
|
179
|
-
currentContent.slice(0, cursorPosition) +
|
|
180
|
-
tsxCode +
|
|
181
|
-
currentContent.slice(cursorPosition)
|
|
182
|
-
updateFileContent(currentFile, newContent)
|
|
183
|
-
} else {
|
|
184
|
-
// No cursor position, look for </board> tag
|
|
185
|
-
const boardClosingTagIndex = currentContent.lastIndexOf("</board>")
|
|
186
|
-
|
|
187
|
-
if (boardClosingTagIndex !== -1) {
|
|
188
|
-
// Insert before the closing board tag
|
|
189
|
-
const newContent =
|
|
190
|
-
currentContent.slice(0, boardClosingTagIndex) +
|
|
191
|
-
tsxCode +
|
|
192
|
-
currentContent.slice(boardClosingTagIndex)
|
|
193
|
-
updateFileContent(currentFile, newContent)
|
|
194
|
-
} else {
|
|
195
|
-
const lastParenIndex = currentContent.lastIndexOf(")")
|
|
196
|
-
|
|
197
|
-
if (lastParenIndex !== -1) {
|
|
198
|
-
const newContent =
|
|
199
|
-
currentContent.slice(0, lastParenIndex) +
|
|
200
|
-
tsxCode +
|
|
201
|
-
currentContent.slice(lastParenIndex)
|
|
202
|
-
updateFileContent(currentFile, newContent)
|
|
203
|
-
} else {
|
|
204
|
-
// If no closing parenthesis found, append to end of file
|
|
205
|
-
updateFileContent(currentFile, currentContent + tsxCode)
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
setChipName("")
|
|
211
|
-
} catch (error) {
|
|
212
|
-
console.error("Error inserting footprint:", error)
|
|
213
|
-
toast({
|
|
214
|
-
title: "Error",
|
|
215
|
-
description: "Failed to insert footprint",
|
|
216
|
-
variant: "destructive",
|
|
217
|
-
})
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const handleCopyToClipboard = async () => {
|
|
222
|
-
try {
|
|
223
|
-
await navigator.clipboard.writeText(footprintString)
|
|
224
|
-
setCopied(true)
|
|
225
|
-
} catch (err) {
|
|
226
|
-
console.error("Failed to copy to clipboard:", err)
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return (
|
|
231
|
-
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
232
|
-
<DialogContent className="max-w-[1160px] h-full flex flex-col overflow-x-scroll">
|
|
233
|
-
<DialogHeader>
|
|
234
|
-
<DialogTitle>Insert Chip</DialogTitle>
|
|
235
|
-
<DialogDescription>
|
|
236
|
-
Choose a footprint type and configure its parameters. The footprint
|
|
237
|
-
will be inserted at your cursor position.
|
|
238
|
-
</DialogDescription>
|
|
239
|
-
</DialogHeader>
|
|
240
|
-
<div className="w-fit h-fit flex gap-4 pt-4">
|
|
241
|
-
<div className="space-y-4 min-w-[280px]">
|
|
242
|
-
<div>
|
|
243
|
-
<label className="text-sm font-medium">Chip Name</label>
|
|
244
|
-
<Input
|
|
245
|
-
value={chipName}
|
|
246
|
-
onChange={(e) => setChipName(e.target.value)}
|
|
247
|
-
placeholder="Enter chip name (e.g., U1)..."
|
|
248
|
-
className="mt-1"
|
|
249
|
-
/>
|
|
250
|
-
</div>
|
|
251
|
-
<div>
|
|
252
|
-
<label className="text-sm font-medium">Footprint Name</label>
|
|
253
|
-
<Combobox
|
|
254
|
-
value={footprintName}
|
|
255
|
-
onChange={(value) => {
|
|
256
|
-
setFootprintName(value)
|
|
257
|
-
try {
|
|
258
|
-
let newParams = fp.string(value).json()
|
|
259
|
-
updateFootprintString(value, newParams)
|
|
260
|
-
} catch (error) {
|
|
261
|
-
console.error("Error updating footprint string:", error)
|
|
262
|
-
setFootprintString(value)
|
|
263
|
-
handleFootprintPreview(value)
|
|
264
|
-
}
|
|
265
|
-
}}
|
|
266
|
-
options={normalFootprintNames}
|
|
267
|
-
placeholder="Select footprint..."
|
|
268
|
-
searchPlaceholder="Search footprints..."
|
|
269
|
-
emptyText="No footprints found."
|
|
270
|
-
className="mt-1"
|
|
271
|
-
/>
|
|
272
|
-
</div>
|
|
273
|
-
<div>
|
|
274
|
-
<label className="text-sm font-medium">Footprint String</label>
|
|
275
|
-
<div className="flex items-center justify-center mt-1 gap-1">
|
|
276
|
-
<Input
|
|
277
|
-
readOnly
|
|
278
|
-
value={footprintString}
|
|
279
|
-
onChange={(e) => {
|
|
280
|
-
setFootprintString(e.target.value)
|
|
281
|
-
handleFootprintPreview(e.target.value)
|
|
282
|
-
}}
|
|
283
|
-
placeholder="Complete footprint string..."
|
|
284
|
-
className={`bg-gray-50 text-gray-500 ${footprintNameError && "bg-red-50 border-red-200"}`}
|
|
285
|
-
/>
|
|
286
|
-
<Button
|
|
287
|
-
size="icon"
|
|
288
|
-
variant="outline"
|
|
289
|
-
onClick={handleCopyToClipboard}
|
|
290
|
-
className={`shrink-0 ${copied && "text-green-500 border-green-500"}`}
|
|
291
|
-
title="Copy to clipboard"
|
|
292
|
-
>
|
|
293
|
-
{copied ? (
|
|
294
|
-
<Check className="h-4 w-4" />
|
|
295
|
-
) : (
|
|
296
|
-
<Copy className="h-4 w-4" />
|
|
297
|
-
)}
|
|
298
|
-
</Button>
|
|
299
|
-
</div>
|
|
300
|
-
</div>
|
|
301
|
-
{params && (
|
|
302
|
-
<ParametersEditor
|
|
303
|
-
params={params}
|
|
304
|
-
updateParam={updateParam}
|
|
305
|
-
paramNames={PARAM_NAMES}
|
|
306
|
-
/>
|
|
307
|
-
)}
|
|
308
|
-
<Button
|
|
309
|
-
onClick={() => {
|
|
310
|
-
handleInsertFootprint()
|
|
311
|
-
onOpenChange(false)
|
|
312
|
-
}}
|
|
313
|
-
disabled={!footprintString || !chipName}
|
|
314
|
-
className="w-full"
|
|
315
|
-
>
|
|
316
|
-
Insert Footprint
|
|
317
|
-
</Button>
|
|
318
|
-
</div>
|
|
319
|
-
<div className="flex flex-col">
|
|
320
|
-
<div className="rounded-xl overflow-hidden w-[800px] h-[600px]">
|
|
321
|
-
{previewSvg && (
|
|
322
|
-
<div
|
|
323
|
-
dangerouslySetInnerHTML={{
|
|
324
|
-
__html: previewSvg,
|
|
325
|
-
}}
|
|
326
|
-
/>
|
|
327
|
-
)}
|
|
328
|
-
</div>
|
|
329
|
-
{error && (
|
|
330
|
-
<div className="mt-2 p-2 text-sm text-red-600 bg-red-50 border border-red-200 rounded">
|
|
331
|
-
{error}
|
|
332
|
-
</div>
|
|
333
|
-
)}
|
|
334
|
-
</div>
|
|
335
|
-
</div>
|
|
336
|
-
</DialogContent>
|
|
337
|
-
</Dialog>
|
|
338
|
-
)
|
|
339
|
-
}
|