@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.
Files changed (35) hide show
  1. package/.github/workflows/bun-pver-release.yml +1 -0
  2. package/bun.lock +43 -11
  3. package/dist/bundle.js +405 -335
  4. package/dist/schema.d.ts +1845 -0
  5. package/dist/schema.js +251 -0
  6. package/fake-snippets-api/routes/api/_fake/received_quotes.ts +66 -0
  7. package/fake-snippets-api/routes/api/package_releases/update.ts +25 -18
  8. package/package.json +8 -3
  9. package/src/components/CodeAndPreview.tsx +0 -1
  10. package/src/components/CodeEditor.tsx +0 -1
  11. package/src/components/CodeEditorHeader.tsx +0 -25
  12. package/src/components/EditorNav.tsx +10 -8
  13. package/src/components/ErrorOutline.tsx +35 -0
  14. package/src/components/FileSidebar.tsx +46 -16
  15. package/src/components/NotFound.tsx +37 -0
  16. package/src/components/PreviewContent.tsx +0 -6
  17. package/src/components/TrendingSnippetCarousel.tsx +1 -1
  18. package/src/components/ViewPackagePage/components/package-header.tsx +24 -3
  19. package/src/components/ViewPackagePage/utils/is-hidden-file.ts +0 -1
  20. package/src/components/dialogs/package-visibility-settings-dialog.tsx +10 -1
  21. package/src/components/dialogs/view-ts-files-dialog.tsx +0 -6
  22. package/src/components/package-port/CodeAndPreview.tsx +24 -9
  23. package/src/components/package-port/CodeEditor.tsx +26 -38
  24. package/src/components/package-port/CodeEditorHeader.tsx +117 -39
  25. package/src/components/package-port/EditorNav.tsx +10 -8
  26. package/src/components/ui/tree-view.tsx +5 -1
  27. package/src/{prettier.ts → lib/types.ts} +3 -1
  28. package/src/lib/utils/findTargetFile.ts +62 -0
  29. package/src/lib/utils/load-prettier.ts +3 -0
  30. package/src/pages/404.tsx +2 -33
  31. package/src/pages/package-editor.tsx +14 -3
  32. package/src/pages/user-profile.tsx +66 -27
  33. package/src/components/FootprintDialog.tsx +0 -339
  34. package/src/components/ParametersEditor.tsx +0 -140
  35. 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' dark:before:bg-slate-800/70",
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
+ }
@@ -6,6 +6,9 @@ export async function loadPrettier() {
6
6
  loadScript(
7
7
  "https://cdn.jsdelivr.net/npm/prettier@2.8.8/parser-typescript.js",
8
8
  ),
9
+ loadScript(
10
+ "https://cdn.jsdelivr.net/npm/prettier@2.8.8/parser-markdown.js",
11
+ ),
9
12
  ])
10
13
  }
11
14
 
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 { Button } from "@/components/ui/button"
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
- <main className="flex-1 flex items-center justify-center min-h-[90vh]">
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 && error.status === 404 && <h1>"Package Not Found"</h1>}
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="flex flex-col">
38
- Something strange happened<div>{error.message}</div>
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?.filter((snippet) => {
59
- return (
60
- !searchQuery ||
61
- snippet.unscoped_name.toLowerCase().includes(searchQuery.toLowerCase())
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
- <Input
109
- type="text"
110
- placeholder="Searching User Packages..."
111
- value={searchQuery}
112
- onChange={(e) => setSearchQuery(e.target.value)}
113
- className="mb-4"
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
- ?.sort((a, b) => b.updated_at.localeCompare(a.updated_at))
125
- ?.map((snippet) => (
126
- <SnippetCard
127
- key={snippet.snippet_id}
128
- snippet={snippet}
129
- baseUrl={baseUrl}
130
- showOwner={activeTab === "starred"}
131
- isCurrentUserSnippet={
132
- isCurrentUserProfile && activeTab === "all"
133
- }
134
- onDeleteClick={handleDeleteClick}
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
- }