@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.
@@ -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
- ...options,
168
+ body: options.body,
169
+ method: options.method,
164
170
  headers: {
165
- ...options?.headers,
171
+ authority: options.headers.authority,
172
+ Authorization: `Bearer ${session?.token}`,
166
173
  "X-Target-Url": url.toString(),
167
- "X-Sender-Origin": options?.headers?.origin ?? "",
168
- "X-Sender-Host": options?.headers?.host ?? "https://easyeda.com",
169
- "X-Sender-Referer": options?.headers?.referer ?? "",
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 placeholder="Select file" />
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: string
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
- <span className="flex-grow text-sm truncate">{item.name}</span>
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 1.0.0 when it contains no digits
45
+ // Default version to 0.0.1 when it contains no digits
46
46
  if (!resolvedVersion || !/[0-9]/.test(resolvedVersion)) {
47
- resolvedVersion = "1.0.0"
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(updatedFiles[0]?.path || "")
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,
@@ -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
+ }
@@ -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">
@@ -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="container mx-auto flex-1 px-4 py-8">
23
- <h1 className="text-3xl font-bold mb-6">{chipName} Datasheet</h1>
24
- <p className="mb-4">
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
- </p>
55
+ </div>
56
+
32
57
  {datasheetQuery.isLoading ? (
33
- <p>Loading...</p>
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
- <div>
36
- {!datasheetQuery.data.pin_information &&
37
- !datasheetQuery.data.datasheet_pdf_urls && (
38
- <p>Datasheet is processing. Please check back later.</p>
39
- )}
40
-
41
- <h2 className="text-xl font-semibold mb-2">PDFs</h2>
42
- {datasheetQuery.data.datasheet_pdf_urls ? (
43
- <ul className="list-disc pl-5 mb-6">
44
- {datasheetQuery.data.datasheet_pdf_urls.map((url) => (
45
- <li key={url}>
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
- <h2 className="text-xl font-semibold mb-2">Pin Information</h2>
57
- {datasheetQuery.data.pin_information ? (
58
- <table className="table-auto border-collapse mb-6">
59
- <thead>
60
- <tr>
61
- <th className="border px-2 py-1">Pin</th>
62
- <th className="border px-2 py-1">Name</th>
63
- <th className="border px-2 py-1">Description</th>
64
- <th className="border px-2 py-1">Capabilities</th>
65
- </tr>
66
- </thead>
67
- <tbody>
68
- {datasheetQuery.data.pin_information.map((pin) => (
69
- <tr key={pin.pin_number}>
70
- <td className="border px-2 py-1">{pin.pin_number}</td>
71
- <td className="border px-2 py-1">{pin.name}</td>
72
- <td className="border px-2 py-1">{pin.description}</td>
73
- <td className="border px-2 py-1">
74
- <ExpandableText
75
- text={pin.capabilities.join(", ")}
76
- maxChars={30}
77
- />
78
- </td>
79
- </tr>
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
- </tbody>
82
- </table>
83
- ) : (
84
- <p>No pin information available.</p>
85
- )}
86
- </div>
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
- <div>
90
- <p>No datasheet found.</p>
91
- <button
92
- className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
93
- onClick={handleCreate}
94
- disabled={createDatasheet.isLoading}
95
- >
96
- {createDatasheet.isLoading ? "Creating..." : "Create Datasheet"}
97
- </button>
98
- </div>
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
- <p>Error loading datasheet.</p>
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 />
@@ -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-500 text-white rounded"
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
  }
@@ -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 bg-gray-50">
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">
@@ -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 bg-gray-50 pb-12">
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">