@tscircuit/fake-snippets 0.0.97 → 0.0.99
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/bun.lock +9 -146
- package/dist/bundle.js +1 -2
- package/fake-snippets-api/routes/api/autocomplete/create_autocomplete.ts +0 -1
- package/package.json +5 -4
- package/src/App.tsx +10 -1
- package/src/components/DownloadButtonAndMenu.tsx +13 -0
- package/src/components/FileSidebar.tsx +83 -10
- package/src/components/PackageBuildsPage/LogContent.tsx +19 -7
- package/src/components/ViewPackagePage/components/ShikiCodeViewer.tsx +15 -2
- package/src/components/ViewPackagePage/components/tab-views/3d-view.tsx +14 -1
- package/src/components/package-port/CodeAndPreview.tsx +5 -0
- package/src/components/package-port/CodeEditor.tsx +95 -10
- package/src/components/package-port/CodeEditorHeader.tsx +24 -14
- package/src/components/ui/tree-view.tsx +51 -2
- package/src/hooks/use-create-package-release-mutation.ts +2 -2
- package/src/hooks/useFileManagement.ts +70 -1
- package/src/lib/constants.ts +11 -0
- package/src/lib/download-fns/download-spice-file.ts +13 -0
- package/src/lib/utils/resolveRelativePath.ts +40 -0
- package/src/pages/dashboard.tsx +1 -1
- package/src/pages/datasheet.tsx +157 -67
- package/src/pages/datasheets.tsx +2 -2
- package/src/pages/latest.tsx +2 -2
- package/src/pages/search.tsx +1 -1
- package/src/pages/trending.tsx +2 -2
- package/vite.config.ts +1 -0
|
@@ -29,6 +29,7 @@ import { convertRawEasyToTsx, fetchEasyEDAComponent } from "easyeda/browser"
|
|
|
29
29
|
import { ComponentSearchResult } from "@tscircuit/runframe/runner"
|
|
30
30
|
import { usePackagesBaseApiUrl } from "@/hooks/use-packages-base-api-url"
|
|
31
31
|
import { ICreateFileProps, ICreateFileResult } from "@/hooks/useFileManagement"
|
|
32
|
+
import { useGlobalStore } from "@/hooks/use-global-store"
|
|
32
33
|
|
|
33
34
|
export type FileName = string
|
|
34
35
|
|
|
@@ -61,6 +62,7 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
|
|
|
61
62
|
const [sidebarOpen, setSidebarOpen] = fileSidebarState
|
|
62
63
|
const API_BASE = usePackagesBaseApiUrl()
|
|
63
64
|
const [aiAutocompleteEnabled, setAiAutocompleteEnabled] = aiAutocompleteState
|
|
65
|
+
const session = useGlobalStore((s) => s.session)
|
|
64
66
|
|
|
65
67
|
const handleFormatFile = useCallback(() => {
|
|
66
68
|
if (!window.prettier || !window.prettierPlugins) return
|
|
@@ -157,18 +159,21 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
|
|
|
157
159
|
updateFileContent(currentFile, newContent)
|
|
158
160
|
}
|
|
159
161
|
if (component.source == "jlcpcb") {
|
|
162
|
+
if (!session?.token) {
|
|
163
|
+
throw new Error("You need to be logged in to import jlcpcb component")
|
|
164
|
+
}
|
|
160
165
|
const jlcpcbComponent = await fetchEasyEDAComponent("C1", {
|
|
161
166
|
fetch: ((url, options: any) => {
|
|
162
167
|
return fetch(`${API_BASE}/proxy`, {
|
|
163
|
-
|
|
168
|
+
body: options.body,
|
|
169
|
+
method: options.method,
|
|
164
170
|
headers: {
|
|
165
|
-
|
|
171
|
+
authority: options.headers.authority,
|
|
172
|
+
Authorization: `Bearer ${session?.token}`,
|
|
166
173
|
"X-Target-Url": url.toString(),
|
|
167
|
-
"X-Sender-
|
|
168
|
-
"X-Sender-
|
|
169
|
-
"
|
|
170
|
-
"X-Sender-User-Agent": options?.headers?.userAgent ?? "",
|
|
171
|
-
"X-Sender-Cookie": options?.headers?.cookie ?? "",
|
|
174
|
+
"X-Sender-Host": options.headers.origin,
|
|
175
|
+
"X-Sender-Origin": options.headers.origin,
|
|
176
|
+
"content-type": options.headers["content-type"],
|
|
172
177
|
},
|
|
173
178
|
})
|
|
174
179
|
}) as typeof fetch,
|
|
@@ -184,13 +189,10 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
|
|
|
184
189
|
onError: (error) => {
|
|
185
190
|
throw error
|
|
186
191
|
},
|
|
187
|
-
openFile: false,
|
|
188
192
|
})
|
|
189
193
|
if (!createFileResult.newFileCreated) {
|
|
190
|
-
throw new Error("Failed to create file")
|
|
194
|
+
throw new Error("Failed to create component file")
|
|
191
195
|
}
|
|
192
|
-
const newContent = `import ${componentName.replace(/-/g, "")} from "./${componentName}.tsx"\n${files[currentFile || ""]}`
|
|
193
|
-
updateFileContent(currentFile, newContent)
|
|
194
196
|
}
|
|
195
197
|
}
|
|
196
198
|
|
|
@@ -216,7 +218,15 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
|
|
|
216
218
|
sidebarOpen ? "-ml-2" : "-ml-1"
|
|
217
219
|
}`}
|
|
218
220
|
>
|
|
219
|
-
<SelectValue
|
|
221
|
+
<SelectValue
|
|
222
|
+
placeholder={
|
|
223
|
+
Object.keys(files).filter(
|
|
224
|
+
(filename) => !isHiddenFile(filename),
|
|
225
|
+
).length > 0
|
|
226
|
+
? "Select file"
|
|
227
|
+
: "No files"
|
|
228
|
+
}
|
|
229
|
+
/>
|
|
220
230
|
</SelectTrigger>
|
|
221
231
|
<SelectContent>
|
|
222
232
|
{Object.keys(files)
|
|
@@ -302,12 +312,12 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
|
|
|
302
312
|
onClick={() => {
|
|
303
313
|
setAiAutocompleteEnabled((prev) => !prev)
|
|
304
314
|
}}
|
|
305
|
-
className={`relative bg-transparent ${aiAutocompleteEnabled ? "text-gray-600 bg-gray-50" : "text-gray-400"}`}
|
|
315
|
+
className={`relative group bg-transparent ${aiAutocompleteEnabled ? "text-gray-600 bg-gray-50" : "text-gray-400"}`}
|
|
306
316
|
>
|
|
307
317
|
<Bot className="h-4 w-4" />
|
|
308
318
|
{!aiAutocompleteEnabled && (
|
|
309
319
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
310
|
-
<div className="w-5 h-0.5 bg-gray-400 rotate-45 rounded-full" />
|
|
320
|
+
<div className="w-5 h-0.5 group-hover:bg-slate-900 bg-gray-400 rotate-45 rounded-full" />
|
|
311
321
|
</div>
|
|
312
322
|
)}
|
|
313
323
|
</Button>
|
|
@@ -3,6 +3,7 @@ import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
|
|
3
3
|
import { ChevronRight } from "lucide-react"
|
|
4
4
|
import { cva } from "class-variance-authority"
|
|
5
5
|
import { cn } from "@/lib/utils"
|
|
6
|
+
import { Input } from "@/components/ui/input"
|
|
6
7
|
|
|
7
8
|
const treeVariants = cva(
|
|
8
9
|
"group hover:before:opacity-100 before:absolute before:rounded-lg before:left-0 before:w-full before:opacity-0 before:bg-slate-100/70 before:h-[2rem] before:-z-10' dark:before:bg-slate-800/70",
|
|
@@ -18,7 +19,7 @@ const dragOverVariants = cva(
|
|
|
18
19
|
|
|
19
20
|
interface TreeDataItem {
|
|
20
21
|
id: string
|
|
21
|
-
name:
|
|
22
|
+
name: React.ReactNode
|
|
22
23
|
icon?: any
|
|
23
24
|
selectedIcon?: any
|
|
24
25
|
openIcon?: any
|
|
@@ -27,6 +28,9 @@ interface TreeDataItem {
|
|
|
27
28
|
onClick?: () => void
|
|
28
29
|
draggable?: boolean
|
|
29
30
|
droppable?: boolean
|
|
31
|
+
isRenaming?: boolean
|
|
32
|
+
onRename?: (newName: string) => void
|
|
33
|
+
onCancelRename?: () => void
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
type TreeProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
@@ -403,7 +407,52 @@ const TreeLeaf = React.forwardRef<
|
|
|
403
407
|
isSelected={selectedItemId === item.id}
|
|
404
408
|
default={defaultLeafIcon}
|
|
405
409
|
/>
|
|
406
|
-
|
|
410
|
+
{item.isRenaming ? (
|
|
411
|
+
<Input
|
|
412
|
+
style={{
|
|
413
|
+
zIndex: 50,
|
|
414
|
+
}}
|
|
415
|
+
defaultValue={item.name as string}
|
|
416
|
+
onKeyDown={(e) => {
|
|
417
|
+
if (e.key === "Enter") {
|
|
418
|
+
e.preventDefault()
|
|
419
|
+
const value = e.currentTarget.value.trim()
|
|
420
|
+
if (value && value !== item.name) {
|
|
421
|
+
item.onRename?.(value)
|
|
422
|
+
} else {
|
|
423
|
+
item.onCancelRename?.()
|
|
424
|
+
}
|
|
425
|
+
} else if (e.key === "Escape") {
|
|
426
|
+
e.preventDefault()
|
|
427
|
+
item.onCancelRename?.()
|
|
428
|
+
}
|
|
429
|
+
}}
|
|
430
|
+
spellCheck={false}
|
|
431
|
+
autoComplete="off"
|
|
432
|
+
onBlur={(e) => {
|
|
433
|
+
const value = e.currentTarget.value.trim()
|
|
434
|
+
if (value && value !== item.name) {
|
|
435
|
+
item.onRename?.(value)
|
|
436
|
+
} else {
|
|
437
|
+
item.onCancelRename?.()
|
|
438
|
+
}
|
|
439
|
+
}}
|
|
440
|
+
autoFocus
|
|
441
|
+
onClick={(e) => e.stopPropagation()}
|
|
442
|
+
className="h-6 px-2 py-0 text-sm flex-1 mr-8 bg-white border border-blue-500 rounded-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 shadow-sm"
|
|
443
|
+
onFocus={(e) => {
|
|
444
|
+
e.currentTarget.select()
|
|
445
|
+
// Select filename without extension
|
|
446
|
+
const filename = e.currentTarget.value
|
|
447
|
+
const lastDotIndex = filename.lastIndexOf(".")
|
|
448
|
+
if (lastDotIndex > 0) {
|
|
449
|
+
e.currentTarget.setSelectionRange(0, lastDotIndex)
|
|
450
|
+
}
|
|
451
|
+
}}
|
|
452
|
+
/>
|
|
453
|
+
) : (
|
|
454
|
+
<span className="text-sm truncate">{item.name}</span>
|
|
455
|
+
)}
|
|
407
456
|
<div className="flex items-center" onClick={(e) => e.stopPropagation()}>
|
|
408
457
|
<TreeActions isSelected={true}>{item.actions}</TreeActions>
|
|
409
458
|
</div>
|
|
@@ -42,9 +42,9 @@ export const useCreatePackageReleaseMutation = ({
|
|
|
42
42
|
resolvedPkgName = pkgName
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
// Default version to
|
|
45
|
+
// Default version to 0.0.1 when it contains no digits
|
|
46
46
|
if (!resolvedVersion || !/[0-9]/.test(resolvedVersion)) {
|
|
47
|
-
resolvedVersion = "
|
|
47
|
+
resolvedVersion = "0.0.1"
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
const normalizedPackageNameWithVersion =
|
|
@@ -17,6 +17,7 @@ import { useCreatePackageReleaseMutation } from "./use-create-package-release-mu
|
|
|
17
17
|
import { useCreatePackageMutation } from "./use-create-package-mutation"
|
|
18
18
|
import { findTargetFile } from "@/lib/utils/findTargetFile"
|
|
19
19
|
import { encodeFsMapToUrlHash } from "@/lib/encodeFsMapToUrlHash"
|
|
20
|
+
import { isHiddenFile } from "@/components/ViewPackagePage/utils/is-hidden-file"
|
|
20
21
|
|
|
21
22
|
export interface ICreateFileProps {
|
|
22
23
|
newFileName: string
|
|
@@ -36,6 +37,16 @@ export interface IDeleteFileProps {
|
|
|
36
37
|
onError: (error: Error) => void
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
export interface IRenameFileProps {
|
|
41
|
+
oldFilename: string
|
|
42
|
+
newFilename: string
|
|
43
|
+
onError: (error: Error) => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface IRenameFileResult {
|
|
47
|
+
fileRenamed: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
export function useFileManagement({
|
|
40
51
|
templateCode,
|
|
41
52
|
currentPackage,
|
|
@@ -234,12 +245,69 @@ export function useFileManagement({
|
|
|
234
245
|
}
|
|
235
246
|
const updatedFiles = localFiles.filter((file) => file.path !== filename)
|
|
236
247
|
setLocalFiles(updatedFiles)
|
|
237
|
-
onFileSelect(
|
|
248
|
+
onFileSelect(
|
|
249
|
+
updatedFiles.filter((file) => !isHiddenFile(file.path))[0]?.path || "",
|
|
250
|
+
)
|
|
238
251
|
return {
|
|
239
252
|
fileDeleted: true,
|
|
240
253
|
}
|
|
241
254
|
}
|
|
242
255
|
|
|
256
|
+
const renameFile = ({
|
|
257
|
+
oldFilename,
|
|
258
|
+
newFilename,
|
|
259
|
+
onError,
|
|
260
|
+
}: IRenameFileProps): IRenameFileResult => {
|
|
261
|
+
newFilename = newFilename.trim()
|
|
262
|
+
if (!newFilename) {
|
|
263
|
+
onError(new Error("File name cannot be empty"))
|
|
264
|
+
return {
|
|
265
|
+
fileRenamed: false,
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Extract just the filename from the path for validation
|
|
270
|
+
const fileNameOnly = newFilename.split("/").pop() || ""
|
|
271
|
+
if (!isValidFileName(fileNameOnly)) {
|
|
272
|
+
onError(new Error("Invalid file name"))
|
|
273
|
+
return {
|
|
274
|
+
fileRenamed: false,
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const oldFileExists = localFiles?.some((file) => file.path === oldFilename)
|
|
279
|
+
if (!oldFileExists) {
|
|
280
|
+
onError(new Error("File does not exist"))
|
|
281
|
+
return {
|
|
282
|
+
fileRenamed: false,
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const newFileExists = localFiles?.some((file) => file.path === newFilename)
|
|
287
|
+
if (newFileExists) {
|
|
288
|
+
onError(new Error("A file with this name already exists"))
|
|
289
|
+
return {
|
|
290
|
+
fileRenamed: false,
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const updatedFiles = localFiles.map((file) => {
|
|
295
|
+
if (file.path === oldFilename) {
|
|
296
|
+
return { ...file, path: newFilename }
|
|
297
|
+
}
|
|
298
|
+
return file
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
setLocalFiles(updatedFiles)
|
|
302
|
+
if (currentFile === oldFilename) {
|
|
303
|
+
setCurrentFile(newFilename)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
fileRenamed: true,
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
243
311
|
const savePackage = async (isPrivate: boolean) => {
|
|
244
312
|
if (!isLoggedIn) {
|
|
245
313
|
toast({
|
|
@@ -354,6 +422,7 @@ export function useFileManagement({
|
|
|
354
422
|
fsMap,
|
|
355
423
|
createFile,
|
|
356
424
|
deleteFile,
|
|
425
|
+
renameFile,
|
|
357
426
|
saveFiles,
|
|
358
427
|
localFiles,
|
|
359
428
|
initialFiles,
|
package/src/lib/constants.ts
CHANGED
|
@@ -10,3 +10,14 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export const TSCI_PACKAGE_PATTERN =
|
|
12
12
|
/@tsci\/[a-zA-Z][a-zA-Z0-9]*(?:--?[a-zA-Z0-9]+)*(?:\.[a-zA-Z][a-zA-Z0-9_]*(?:--?[a-zA-Z0-9_]+)*)*/g
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Regular expression pattern for matching local file imports
|
|
16
|
+
* Rules:
|
|
17
|
+
* - Must start with ./ or ../
|
|
18
|
+
* - Can contain letters, numbers, dots, dashes, underscores, and forward slashes
|
|
19
|
+
* - Can optionally end with file extensions like .ts, .tsx, .js, .jsx, .json
|
|
20
|
+
* - Captures the full relative path
|
|
21
|
+
*/
|
|
22
|
+
export const LOCAL_FILE_IMPORT_PATTERN =
|
|
23
|
+
/(?:\.\.?\/[a-zA-Z0-9._\-\/]*(?:\.(?:ts|tsx|js|jsx|json))?)/g
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AnyCircuitElement } from "circuit-json"
|
|
2
|
+
import { saveAs } from "file-saver"
|
|
3
|
+
import { circuitJsonToSpice } from "circuit-json-to-spice"
|
|
4
|
+
|
|
5
|
+
export const downloadSpiceFile = (
|
|
6
|
+
circuitJson: AnyCircuitElement[],
|
|
7
|
+
fileName: string,
|
|
8
|
+
) => {
|
|
9
|
+
const spiceNetlist = circuitJsonToSpice(circuitJson)
|
|
10
|
+
const spiceString = spiceNetlist.toSpiceString()
|
|
11
|
+
const blob = new Blob([spiceString], { type: "text/plain" })
|
|
12
|
+
saveAs(blob, fileName + ".cir")
|
|
13
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a relative file path to an absolute path based on the current file
|
|
3
|
+
*/
|
|
4
|
+
export const resolveRelativePath = (
|
|
5
|
+
relativePath: string,
|
|
6
|
+
currentFilePath: string,
|
|
7
|
+
): string => {
|
|
8
|
+
if (!currentFilePath) return relativePath
|
|
9
|
+
|
|
10
|
+
const currentDir = currentFilePath.includes("/")
|
|
11
|
+
? currentFilePath.substring(0, currentFilePath.lastIndexOf("/"))
|
|
12
|
+
: ""
|
|
13
|
+
|
|
14
|
+
if (relativePath.startsWith("./")) {
|
|
15
|
+
return currentDir
|
|
16
|
+
? `${currentDir}/${relativePath.slice(2)}`
|
|
17
|
+
: relativePath.slice(2)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (relativePath.startsWith("../")) {
|
|
21
|
+
const parts = currentDir.split("/").filter((p) => p !== "")
|
|
22
|
+
const relativeParts = relativePath.split("/").filter((p) => p !== "")
|
|
23
|
+
|
|
24
|
+
let upCount = 0
|
|
25
|
+
for (const part of relativeParts) {
|
|
26
|
+
if (part === "..") {
|
|
27
|
+
upCount++
|
|
28
|
+
} else {
|
|
29
|
+
break
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const resultParts = parts.slice(0, Math.max(0, parts.length - upCount))
|
|
34
|
+
const remainingParts = relativeParts.slice(upCount)
|
|
35
|
+
|
|
36
|
+
return [...resultParts, ...remainingParts].join("/")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return relativePath
|
|
40
|
+
}
|
package/src/pages/dashboard.tsx
CHANGED
|
@@ -98,7 +98,7 @@ export const DashboardPage = () => {
|
|
|
98
98
|
<title>Dashboard - tscircuit</title>
|
|
99
99
|
</Helmet>
|
|
100
100
|
<Header />
|
|
101
|
-
<div className="container mx-auto px-4 py-8">
|
|
101
|
+
<div className="container mx-auto px-4 py-8 min-h-[80vh]">
|
|
102
102
|
<h1 className="text-3xl font-bold mb-6">Dashboard</h1>
|
|
103
103
|
<div className="flex md:flex-row flex-col">
|
|
104
104
|
<div className="md:w-3/4 p-0 md:pr-6">
|
package/src/pages/datasheet.tsx
CHANGED
|
@@ -5,6 +5,21 @@ import Header from "@/components/Header"
|
|
|
5
5
|
import Footer from "@/components/Footer"
|
|
6
6
|
import ExpandableText from "@/components/ExpandableText"
|
|
7
7
|
import type { Datasheet } from "fake-snippets-api/lib/db/schema"
|
|
8
|
+
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
|
|
9
|
+
import { Button } from "@/components/ui/button"
|
|
10
|
+
import { Loader2, AlertCircle, FileText } from "lucide-react"
|
|
11
|
+
|
|
12
|
+
const SectionCard = ({
|
|
13
|
+
title,
|
|
14
|
+
children,
|
|
15
|
+
}: { title: string; children: React.ReactNode }) => (
|
|
16
|
+
<Card className="mb-6">
|
|
17
|
+
<CardHeader className="pb-2">
|
|
18
|
+
<CardTitle className="text-xl font-semibold">{title}</CardTitle>
|
|
19
|
+
</CardHeader>
|
|
20
|
+
<CardContent>{children}</CardContent>
|
|
21
|
+
</Card>
|
|
22
|
+
)
|
|
8
23
|
|
|
9
24
|
export const DatasheetPage = () => {
|
|
10
25
|
const { chipName } = useParams<{ chipName: string }>()
|
|
@@ -19,85 +34,160 @@ export const DatasheetPage = () => {
|
|
|
19
34
|
return (
|
|
20
35
|
<div className="min-h-screen flex flex-col">
|
|
21
36
|
<Header />
|
|
22
|
-
<main className="
|
|
23
|
-
<
|
|
24
|
-
|
|
37
|
+
<main className="flex-grow mx-auto px-4 md:px-20 lg:px-28 py-8 w-full">
|
|
38
|
+
<div className="mb-8">
|
|
39
|
+
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 mb-2 break-words">
|
|
40
|
+
{chipName} Datasheet
|
|
41
|
+
</h1>
|
|
42
|
+
<p className="text-lg text-gray-600 mb-4">
|
|
43
|
+
View and download the datasheet for{" "}
|
|
44
|
+
<span className="font-semibold text-gray-800">{chipName}</span>. If
|
|
45
|
+
the datasheet is not available, you can request its creation.
|
|
46
|
+
</p>
|
|
25
47
|
<a
|
|
26
48
|
href={`https://api.tscircuit.com/datasheets/get?chip_name=${encodeURIComponent(chipName)}`}
|
|
27
|
-
className="text-blue-600 underline"
|
|
49
|
+
className="inline-flex items-center gap-1 text-blue-600 hover:underline text-sm font-medium"
|
|
50
|
+
target="_blank"
|
|
51
|
+
rel="noopener noreferrer"
|
|
28
52
|
>
|
|
29
|
-
Download JSON
|
|
53
|
+
<FileText className="w-4 h-4" /> Download JSON
|
|
30
54
|
</a>
|
|
31
|
-
</
|
|
55
|
+
</div>
|
|
56
|
+
|
|
32
57
|
{datasheetQuery.isLoading ? (
|
|
33
|
-
<
|
|
58
|
+
<div className="flex flex-col items-center justify-center py-16">
|
|
59
|
+
<Loader2 className="w-10 h-10 animate-spin text-blue-500 mb-4" />
|
|
60
|
+
<h3 className="text-xl font-semibold mb-2">Loading Datasheet...</h3>
|
|
61
|
+
<p className="text-gray-500 max-w-md text-center">
|
|
62
|
+
Please wait while we fetch the datasheet information for{" "}
|
|
63
|
+
<span className="font-semibold">{chipName}</span>.
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
34
66
|
) : datasheetQuery.data ? (
|
|
35
|
-
|
|
36
|
-
{!
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
<a href={url} className="text-blue-600 underline">
|
|
47
|
-
{url}
|
|
48
|
-
</a>
|
|
49
|
-
</li>
|
|
50
|
-
))}
|
|
51
|
-
</ul>
|
|
52
|
-
) : (
|
|
53
|
-
<p>No datasheet PDFs available.</p>
|
|
67
|
+
<>
|
|
68
|
+
{!(
|
|
69
|
+
datasheetQuery.data.pin_information ||
|
|
70
|
+
datasheetQuery.data.datasheet_pdf_urls
|
|
71
|
+
) && (
|
|
72
|
+
<SectionCard title="Processing">
|
|
73
|
+
<div className="flex items-center gap-3 text-yellow-700">
|
|
74
|
+
<Loader2 className="w-5 h-5 animate-spin" />
|
|
75
|
+
<span>Datasheet is processing. Please check back later.</span>
|
|
76
|
+
</div>
|
|
77
|
+
</SectionCard>
|
|
54
78
|
)}
|
|
55
79
|
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
<SectionCard title="Description">
|
|
81
|
+
{datasheetQuery.data.ai_description ? (
|
|
82
|
+
<div className="flex items-center gap-3 text-gray-500">
|
|
83
|
+
<span>{datasheetQuery.data.ai_description}</span>
|
|
84
|
+
</div>
|
|
85
|
+
) : (
|
|
86
|
+
<p className="text-gray-500">No description available.</p>
|
|
87
|
+
)}
|
|
88
|
+
</SectionCard>
|
|
89
|
+
|
|
90
|
+
<SectionCard title="PDFs">
|
|
91
|
+
{datasheetQuery.data.datasheet_pdf_urls &&
|
|
92
|
+
datasheetQuery.data.datasheet_pdf_urls.length > 0 ? (
|
|
93
|
+
<ul className="list-disc pl-5 space-y-2">
|
|
94
|
+
{datasheetQuery.data.datasheet_pdf_urls.map((url) => (
|
|
95
|
+
<li key={url}>
|
|
96
|
+
<a
|
|
97
|
+
href={url}
|
|
98
|
+
className="text-blue-600 underline break-all"
|
|
99
|
+
target="_blank"
|
|
100
|
+
rel="noopener noreferrer"
|
|
101
|
+
>
|
|
102
|
+
{url}
|
|
103
|
+
</a>
|
|
104
|
+
</li>
|
|
80
105
|
))}
|
|
81
|
-
</
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
106
|
+
</ul>
|
|
107
|
+
) : (
|
|
108
|
+
<p className="text-gray-500">No datasheet PDFs available.</p>
|
|
109
|
+
)}
|
|
110
|
+
</SectionCard>
|
|
111
|
+
|
|
112
|
+
<SectionCard title="Pin Information">
|
|
113
|
+
{datasheetQuery.data.pin_information &&
|
|
114
|
+
datasheetQuery.data.pin_information.length > 0 ? (
|
|
115
|
+
<div className="overflow-x-auto">
|
|
116
|
+
<table className="min-w-full border-collapse text-sm">
|
|
117
|
+
<thead>
|
|
118
|
+
<tr>
|
|
119
|
+
<th className="border-b px-3 py-2 text-left font-semibold">
|
|
120
|
+
Pin
|
|
121
|
+
</th>
|
|
122
|
+
<th className="border-b px-3 py-2 text-left font-semibold">
|
|
123
|
+
Name
|
|
124
|
+
</th>
|
|
125
|
+
<th className="border-b px-3 py-2 text-left font-semibold">
|
|
126
|
+
Description
|
|
127
|
+
</th>
|
|
128
|
+
<th className="border-b px-3 py-2 text-left font-semibold">
|
|
129
|
+
Capabilities
|
|
130
|
+
</th>
|
|
131
|
+
</tr>
|
|
132
|
+
</thead>
|
|
133
|
+
<tbody>
|
|
134
|
+
{datasheetQuery.data.pin_information.map((pin) => (
|
|
135
|
+
<tr key={pin.pin_number} className="hover:bg-gray-50">
|
|
136
|
+
<td className="border-b px-3 py-2 font-mono">
|
|
137
|
+
{pin.pin_number}
|
|
138
|
+
</td>
|
|
139
|
+
<td className="border-b px-3 py-2">{pin.name}</td>
|
|
140
|
+
<td className="border-b px-3 py-2">
|
|
141
|
+
{pin.description}
|
|
142
|
+
</td>
|
|
143
|
+
<td className="border-b px-3 py-2">
|
|
144
|
+
<ExpandableText
|
|
145
|
+
text={pin.capabilities.join(", ")}
|
|
146
|
+
maxChars={30}
|
|
147
|
+
/>
|
|
148
|
+
</td>
|
|
149
|
+
</tr>
|
|
150
|
+
))}
|
|
151
|
+
</tbody>
|
|
152
|
+
</table>
|
|
153
|
+
</div>
|
|
154
|
+
) : (
|
|
155
|
+
<p className="text-gray-500">No pin information available.</p>
|
|
156
|
+
)}
|
|
157
|
+
</SectionCard>
|
|
158
|
+
</>
|
|
87
159
|
) : datasheetQuery.error &&
|
|
88
160
|
(datasheetQuery.error as any).status === 404 ? (
|
|
89
|
-
<
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
className="
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
161
|
+
<SectionCard title="No Datasheet Found">
|
|
162
|
+
<div className="flex flex-col items-center gap-4">
|
|
163
|
+
<AlertCircle className="w-8 h-8 text-red-500" />
|
|
164
|
+
<p className="text-gray-700 text-center">
|
|
165
|
+
No datasheet found for{" "}
|
|
166
|
+
<span className="font-semibold">{chipName}</span>.<br />
|
|
167
|
+
You can request its creation below.
|
|
168
|
+
</p>
|
|
169
|
+
<Button
|
|
170
|
+
className="mt-2"
|
|
171
|
+
onClick={handleCreate}
|
|
172
|
+
disabled={createDatasheet.isLoading}
|
|
173
|
+
size="lg"
|
|
174
|
+
>
|
|
175
|
+
{createDatasheet.isLoading ? "Creating..." : "Create Datasheet"}
|
|
176
|
+
</Button>
|
|
177
|
+
</div>
|
|
178
|
+
</SectionCard>
|
|
99
179
|
) : (
|
|
100
|
-
<
|
|
180
|
+
<div className="flex flex-col items-center justify-center py-16">
|
|
181
|
+
<AlertCircle className="w-10 h-10 text-red-500 mb-4" />
|
|
182
|
+
<h3 className="text-xl font-semibold mb-2">
|
|
183
|
+
Error loading datasheet
|
|
184
|
+
</h3>
|
|
185
|
+
<p className="text-gray-500 max-w-md text-center">
|
|
186
|
+
There was an error loading the datasheet for{" "}
|
|
187
|
+
<span className="font-semibold">{chipName}</span>. Please try
|
|
188
|
+
again later.
|
|
189
|
+
</p>
|
|
190
|
+
</div>
|
|
101
191
|
)}
|
|
102
192
|
</main>
|
|
103
193
|
<Footer />
|
package/src/pages/datasheets.tsx
CHANGED
|
@@ -43,7 +43,7 @@ export const DatasheetsPage: React.FC = () => {
|
|
|
43
43
|
return (
|
|
44
44
|
<div className="min-h-screen flex flex-col">
|
|
45
45
|
<Header />
|
|
46
|
-
<main className="flex-grow container mx-auto px-4 py-8">
|
|
46
|
+
<main className="flex-grow container mx-auto px-4 py-8 min-h-[80vh]">
|
|
47
47
|
<div className="mb-8 max-w-3xl">
|
|
48
48
|
<div className="flex items-center gap-2 mb-3">
|
|
49
49
|
<h1 className="text-4xl font-bold text-gray-900">Datasheets</h1>
|
|
@@ -126,7 +126,7 @@ export const DatasheetsPage: React.FC = () => {
|
|
|
126
126
|
</p>
|
|
127
127
|
{searchQuery && (
|
|
128
128
|
<button
|
|
129
|
-
className="mt-2 px-4 py-2 bg-blue-
|
|
129
|
+
className="mt-2 px-4 py-2 bg-blue-600 text-white rounded"
|
|
130
130
|
onClick={() =>
|
|
131
131
|
createDatasheet.mutate({ chip_name: searchQuery })
|
|
132
132
|
}
|
package/src/pages/latest.tsx
CHANGED
|
@@ -58,9 +58,9 @@ const LatestPage: React.FC = () => {
|
|
|
58
58
|
?.sort((a, b) => b.created_at.localeCompare(a.created_at))
|
|
59
59
|
|
|
60
60
|
return (
|
|
61
|
-
<div className="min-h-screen flex flex-col
|
|
61
|
+
<div className="min-h-screen flex flex-col">
|
|
62
62
|
<Header />
|
|
63
|
-
<main className="flex-grow container mx-auto px-4 py-8">
|
|
63
|
+
<main className="flex-grow container mx-auto px-4 py-8 min-h-[80vh]">
|
|
64
64
|
<div className="mb-8 max-w-3xl">
|
|
65
65
|
<div className="flex items-center gap-2 mb-3">
|
|
66
66
|
<h1 className="text-4xl font-bold text-gray-900">
|
package/src/pages/search.tsx
CHANGED
|
@@ -91,7 +91,7 @@ export const SearchPage = () => {
|
|
|
91
91
|
return (
|
|
92
92
|
<div className="min-h-screen flex flex-col">
|
|
93
93
|
<Header />
|
|
94
|
-
<main className="flex-grow
|
|
94
|
+
<main className="flex-grow pb-12 min-h-[80vh]">
|
|
95
95
|
<div className="container mx-auto px-4 py-8">
|
|
96
96
|
<div className="max-w-8xl mx-auto">
|
|
97
97
|
<div className="mb-6">
|