@tscircuit/fake-snippets 0.0.98 → 0.0.100

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 (31) hide show
  1. package/bun.lock +304 -947
  2. package/dist/bundle.js +11 -5
  3. package/fake-snippets-api/routes/api/autocomplete/create_autocomplete.ts +0 -1
  4. package/fake-snippets-api/routes/api/packages/create.ts +14 -3
  5. package/package.json +7 -4
  6. package/src/App.tsx +58 -2
  7. package/src/components/CircuitJsonImportDialog.tsx +10 -5
  8. package/src/components/DownloadButtonAndMenu.tsx +13 -0
  9. package/src/components/FileSidebar.tsx +83 -10
  10. package/src/components/PackageBuildsPage/LogContent.tsx +19 -7
  11. package/src/components/ViewPackagePage/components/important-files-view.tsx +294 -167
  12. package/src/components/ViewPackagePage/components/main-content-header.tsx +2 -2
  13. package/src/components/ViewPackagePage/components/repo-page-content.tsx +9 -0
  14. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +10 -3
  15. package/src/components/ViewPackagePage/components/sidebar.tsx +3 -1
  16. package/src/components/dialogs/edit-package-details-dialog.tsx +3 -2
  17. package/src/components/package-port/CodeAndPreview.tsx +6 -1
  18. package/src/components/package-port/CodeEditor.tsx +21 -3
  19. package/src/components/package-port/CodeEditorHeader.tsx +12 -7
  20. package/src/components/ui/tree-view.tsx +51 -2
  21. package/src/hooks/use-create-package-mutation.ts +1 -1
  22. package/src/hooks/useFileManagement.ts +71 -6
  23. package/src/lib/download-fns/download-spice-file.ts +13 -0
  24. package/src/lib/utils/package-utils.ts +0 -3
  25. package/src/pages/dashboard.tsx +1 -1
  26. package/src/pages/datasheet.tsx +157 -67
  27. package/src/pages/datasheets.tsx +2 -2
  28. package/src/pages/latest.tsx +2 -2
  29. package/src/pages/search.tsx +1 -1
  30. package/src/pages/trending.tsx +2 -2
  31. package/vite.config.ts +1 -0
@@ -1,4 +1,3 @@
1
- import "dotenv/config"
2
1
  import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
3
2
  import { z } from "zod"
4
3
  import OpenAI from "openai"
@@ -13,7 +13,8 @@ export default withRouteSpec({
13
13
  /^[@a-zA-Z0-9-_\/]+$/,
14
14
  "Package name can only contain letters, numbers, hyphens, underscores, and forward slashes",
15
15
  )
16
- .transform((name) => name.replace(/^@/, "")),
16
+ .transform((name) => name.replace(/^@/, ""))
17
+ .optional(),
17
18
  description: z.string().optional(),
18
19
  is_private: z.boolean().optional().default(false),
19
20
  is_unlisted: z.boolean().optional().default(false),
@@ -33,10 +34,20 @@ export default withRouteSpec({
33
34
  })
34
35
  }
35
36
 
36
- const unscoped_name = name.split("/")[1]
37
+ let unscoped_name = name?.includes("/") ? name?.split("/")[1] : name
38
+ if (!unscoped_name) {
39
+ const state = ctx.db.getState()
40
+ const count = state.packages.filter(
41
+ (pkg) => pkg.creator_account_id === ctx.auth.account_id,
42
+ ).length
43
+
44
+ unscoped_name = `untitled-package-${count}`
45
+ }
37
46
 
38
47
  const newPackage = ctx.db.addPackage({
39
- name,
48
+ name: name?.includes("/")
49
+ ? name
50
+ : `${ctx.auth.github_username}/${String(unscoped_name)}`,
40
51
  description: description ?? null,
41
52
  creator_account_id: ctx.auth.account_id,
42
53
  owner_org_id: ctx.auth.personal_org_id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fake-snippets",
3
- "version": "0.0.98",
3
+ "version": "0.0.100",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -75,7 +75,7 @@
75
75
  "@radix-ui/react-toggle-group": "^1.1.0",
76
76
  "@radix-ui/react-tooltip": "^1.1.2",
77
77
  "@tailwindcss/typography": "^0.5.16",
78
- "@tscircuit/3d-viewer": "^0.0.279",
78
+ "@tscircuit/3d-viewer": "^0.0.303",
79
79
  "@tscircuit/assembly-viewer": "^0.0.1",
80
80
  "@tscircuit/create-snippet-url": "^0.0.8",
81
81
  "@tscircuit/eval": "^0.0.244",
@@ -83,7 +83,7 @@
83
83
  "@tscircuit/mm": "^0.0.8",
84
84
  "@tscircuit/pcb-viewer": "^1.11.194",
85
85
  "@tscircuit/prompt-benchmarks": "^0.0.28",
86
- "@tscircuit/runframe": "^0.0.669",
86
+ "@tscircuit/runframe": "0.0.725",
87
87
  "@tscircuit/schematic-viewer": "^2.0.21",
88
88
  "@types/babel__standalone": "^7.1.7",
89
89
  "@types/bun": "^1.1.10",
@@ -105,9 +105,10 @@
105
105
  "autoprefixer": "^10.4.20",
106
106
  "change-case": "^5.4.4",
107
107
  "circuit-json-to-bom-csv": "^0.0.7",
108
- "circuit-json-to-gerber": "^0.0.25",
108
+ "circuit-json-to-gerber": "^0.0.29",
109
109
  "circuit-json-to-pnp-csv": "^0.0.7",
110
110
  "circuit-json-to-readable-netlist": "^0.0.13",
111
+ "circuit-json-to-spice": "^0.0.6",
111
112
  "circuit-json-to-tscircuit": "^0.0.4",
112
113
  "circuit-to-svg": "^0.0.167",
113
114
  "class-variance-authority": "^0.7.1",
@@ -138,6 +139,7 @@
138
139
  "ky": "^1.7.5",
139
140
  "lucide-react": "^0.488.0",
140
141
  "lz-string": "^1.5.0",
142
+ "marked": "^16.1.1",
141
143
  "md5": "^2.3.0",
142
144
  "ms": "^2.1.3",
143
145
  "next-themes": "^0.3.0",
@@ -150,6 +152,7 @@
150
152
  "react-cookie-consent": "^9.0.0",
151
153
  "react-day-picker": "8.10.1",
152
154
  "react-dom": "^18.3.1",
155
+ "react-error-boundary": "^6.0.0",
153
156
  "react-helmet": "^6.1.0",
154
157
  "react-helmet-async": "^2.0.5",
155
158
  "react-hook-form": "^7.53.0",
package/src/App.tsx CHANGED
@@ -3,6 +3,8 @@ import { Route, Switch } from "wouter"
3
3
  import "./components/CmdKMenu"
4
4
  import { ContextProviders } from "./ContextProviders"
5
5
  import React from "react"
6
+ import { ReloadIcon } from "@radix-ui/react-icons"
7
+ import { Loader2 } from "lucide-react"
6
8
 
7
9
  const FullPageLoader = () => (
8
10
  <div className="fixed inset-0 flex items-center justify-center bg-white z-50">
@@ -163,10 +165,64 @@ class ErrorBoundary extends React.Component<
163
165
 
164
166
  render() {
165
167
  if (this.state.reloading) {
166
- return <div>There was a problem loading this page. Reloading…</div>
168
+ return (
169
+ <div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 px-4">
170
+ <div className="max-w-md w-full text-center">
171
+ <div className="mb-6">
172
+ <div className="inline-flex items-center justify-center w-16 h-16 bg-blue-100 dark:bg-blue-900 rounded-full mb-4">
173
+ <Loader2 className="w-8 h-8 text-blue-600 dark:text-blue-400 animate-spin" />
174
+ </div>
175
+ <h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
176
+ Reloading Page
177
+ </h2>
178
+ <p className="text-gray-600 dark:text-gray-400">
179
+ We encountered an issue and are refreshing the page for you.
180
+ </p>
181
+ </div>
182
+ </div>
183
+ </div>
184
+ )
167
185
  }
168
186
  if (this.state.hasError) {
169
- return <div>Something went wrong loading the page.</div>
187
+ return (
188
+ <div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 px-4">
189
+ <div className="max-w-lg w-full text-center">
190
+ <div className="mb-8">
191
+ <div className="inline-flex items-center justify-center w-20 h-20 bg-red-100 dark:bg-red-900 rounded-full mb-6">
192
+ <svg
193
+ className="w-10 h-10 text-red-600 dark:text-red-400"
194
+ fill="none"
195
+ stroke="currentColor"
196
+ viewBox="0 0 24 24"
197
+ >
198
+ <path
199
+ strokeLinecap="round"
200
+ strokeLinejoin="round"
201
+ strokeWidth={2}
202
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
203
+ />
204
+ </svg>
205
+ </div>
206
+ <h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-3">
207
+ Oops! Something went wrong
208
+ </h1>
209
+ <p className="text-gray-600 dark:text-gray-400 mb-6">
210
+ We're experiencing technical difficulties. The page will
211
+ automatically reload when you return to this tab.
212
+ </p>
213
+ </div>
214
+ <div className="space-y-3">
215
+ <button
216
+ onClick={this.performReload}
217
+ className="w-full sm:w-auto inline-flex items-center justify-center px-6 py-2 border border-transparent text-base font-medium rounded-lg text-white bg-red-600 hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors duration-200"
218
+ >
219
+ <ReloadIcon className="w-4 h-4 mr-2" />
220
+ Reload Now
221
+ </button>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ )
170
226
  }
171
227
  return this.props.children
172
228
  }
@@ -15,7 +15,6 @@ import { useLocation } from "wouter"
15
15
  import { useGlobalStore } from "@/hooks/use-global-store"
16
16
  import { convertCircuitJsonToTscircuit } from "circuit-json-to-tscircuit"
17
17
  import { useCreatePackageMutation } from "@/hooks/use-create-package-mutation"
18
- import { generateRandomPackageName } from "@/lib/utils/package-utils"
19
18
  import { useCreatePackageReleaseMutation } from "@/hooks/use-create-package-release-mutation"
20
19
  import { useCreatePackageFilesMutation } from "@/hooks/use-create-package-files-mutation"
21
20
 
@@ -67,10 +66,17 @@ export function CircuitJsonImportDialog({
67
66
 
68
67
  const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
69
68
  const selectedFile = e.target.files?.[0]
70
- if (selectedFile && selectedFile.type === "application/json") {
71
- setFile(selectedFile)
69
+ if (selectedFile) {
70
+ try {
71
+ const fileText = await selectedFile.text()
72
+ JSON.parse(fileText)
73
+ setFile(selectedFile)
74
+ setError(null)
75
+ } catch (e) {
76
+ setError("Please select a valid JSON file that can be parsed.")
77
+ }
72
78
  } else {
73
- setError("Please select a valid JSON file.")
79
+ setError("Please select a file.")
74
80
  }
75
81
  }
76
82
 
@@ -124,7 +130,6 @@ export function CircuitJsonImportDialog({
124
130
 
125
131
  await createPackageMutation.mutateAsync(
126
132
  {
127
- name: `${loggedInUser?.github_username}/${generateRandomPackageName()}`,
128
133
  description: "Imported from Circuit JSON",
129
134
  },
130
135
  {
@@ -12,6 +12,7 @@ import { downloadDsnFile } from "@/lib/download-fns/download-dsn-file-fn"
12
12
  import { downloadFabricationFiles } from "@/lib/download-fns/download-fabrication-files"
13
13
  import { downloadSchematicSvg } from "@/lib/download-fns/download-schematic-svg"
14
14
  import { downloadReadableNetlist } from "@/lib/download-fns/download-readable-netlist"
15
+ import { downloadSpiceFile } from "@/lib/download-fns/download-spice-file"
15
16
  import { downloadAssemblySvg } from "@/lib/download-fns/download-assembly-svg"
16
17
  import { usePcbDownloadDialog } from "@/components/dialogs/pcb-download-dialog"
17
18
  import { downloadKicadFiles } from "@/lib/download-fns/download-kicad-files"
@@ -214,6 +215,18 @@ export function DownloadButtonAndMenu({
214
215
  txt
215
216
  </span>
216
217
  </DropdownMenuItem>
218
+ <DropdownMenuItem
219
+ className="text-xs"
220
+ onSelect={() => {
221
+ downloadSpiceFile(circuitJson, unscopedName || "circuit")
222
+ }}
223
+ >
224
+ <Download className="mr-1 h-3 w-3" />
225
+ <span className="flex-grow mr-6">SPICE Netlist</span>
226
+ <span className="text-[0.6rem] opacity-80 bg-blue-500 text-white font-mono rounded-md px-1 text-center py-0.5 mr-1">
227
+ spice
228
+ </span>
229
+ </DropdownMenuItem>
217
230
  <DropdownMenuItem
218
231
  className="text-xs"
219
232
  onSelect={() => {
@@ -1,6 +1,14 @@
1
1
  import React, { useState } from "react"
2
2
  import { cn } from "@/lib/utils"
3
- import { File, Folder, MoreVertical, PanelRightOpen, Plus } from "lucide-react"
3
+ import {
4
+ File,
5
+ Folder,
6
+ MoreVertical,
7
+ PanelRightOpen,
8
+ Plus,
9
+ Trash2,
10
+ Pencil,
11
+ } from "lucide-react"
4
12
  import { TreeView, TreeDataItem } from "@/components/ui/tree-view"
5
13
  import { isHiddenFile } from "./ViewPackagePage/utils/is-hidden-file"
6
14
  import { Input } from "@/components/ui/input"
@@ -16,8 +24,12 @@ import type {
16
24
  ICreateFileResult,
17
25
  IDeleteFileProps,
18
26
  IDeleteFileResult,
27
+ IRenameFileProps,
28
+ IRenameFileResult,
19
29
  } from "@/hooks/useFileManagement"
20
30
  import { useToast } from "@/hooks/use-toast"
31
+ import { useGlobalStore } from "@/hooks/use-global-store"
32
+ import type { Package } from "fake-snippets-api/lib/db/schema"
21
33
  type FileName = string
22
34
 
23
35
  interface FileSidebarProps {
@@ -28,6 +40,10 @@ interface FileSidebarProps {
28
40
  fileSidebarState: ReturnType<typeof useState<boolean>>
29
41
  handleCreateFile: (props: ICreateFileProps) => ICreateFileResult
30
42
  handleDeleteFile: (props: IDeleteFileProps) => IDeleteFileResult
43
+ handleRenameFile: (props: IRenameFileProps) => IRenameFileResult
44
+ isCreatingFile: boolean
45
+ setIsCreatingFile: React.Dispatch<React.SetStateAction<boolean>>
46
+ pkg?: Package
31
47
  }
32
48
 
33
49
  const FileSidebar: React.FC<FileSidebarProps> = ({
@@ -38,12 +54,18 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
38
54
  fileSidebarState,
39
55
  handleCreateFile,
40
56
  handleDeleteFile,
57
+ handleRenameFile,
58
+ isCreatingFile,
59
+ setIsCreatingFile,
60
+ pkg,
41
61
  }) => {
42
62
  const [sidebarOpen, setSidebarOpen] = fileSidebarState
43
63
  const [newFileName, setNewFileName] = useState("")
44
- const [isCreatingFile, setIsCreatingFile] = useState(false)
45
64
  const [errorMessage, setErrorMessage] = useState("")
65
+ const [renamingFile, setRenamingFile] = useState<string | null>(null)
46
66
  const { toast } = useToast()
67
+ const session = useGlobalStore((s) => s.session)
68
+ const canModifyFiles = true
47
69
 
48
70
  const transformFilesToTreeData = (
49
71
  files: Record<FileName, string>,
@@ -79,20 +101,59 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
79
101
  ) {
80
102
  currentNode[segment] = {
81
103
  id: itemId,
82
- name: isLeafNode ? segment : segment,
104
+ name: segment,
105
+ isRenaming: renamingFile === itemId,
106
+ onRename: (newFilename: string) => {
107
+ // Preserve the folder structure when renaming
108
+ const oldPath = itemId
109
+ const pathParts = oldPath.split("/").filter((part) => part !== "") // Remove empty segments
110
+ let newPath: string
111
+
112
+ if (pathParts.length > 1) {
113
+ // File is in a folder, preserve the folder structure
114
+ const folderPath = pathParts.slice(0, -1).join("/")
115
+ newPath = folderPath + "/" + newFilename
116
+ } else {
117
+ // File is in root, just use the new filename
118
+ newPath = newFilename
119
+ }
120
+
121
+ // Preserve leading slash if original path had one
122
+ if (oldPath.startsWith("/") && !newPath.startsWith("/")) {
123
+ newPath = "/" + newPath
124
+ }
125
+
126
+ const { fileRenamed } = handleRenameFile({
127
+ oldFilename: itemId,
128
+ newFilename: newPath,
129
+ onError: (error) => {
130
+ toast({
131
+ title: `Error renaming file`,
132
+ description: error.message,
133
+ variant: "destructive",
134
+ })
135
+ },
136
+ })
137
+ if (fileRenamed) {
138
+ setRenamingFile(null)
139
+ }
140
+ },
141
+ onCancelRename: () => {
142
+ setRenamingFile(null)
143
+ },
83
144
  icon: isLeafNode ? File : Folder,
84
145
  onClick: isLeafNode ? () => onFileSelect(absolutePath) : undefined,
85
146
  draggable: false,
86
147
  droppable: !isLeafNode,
87
148
  children: isLeafNode ? undefined : {},
88
- actions: (
149
+ actions: canModifyFiles ? (
89
150
  <>
90
151
  <DropdownMenu key={itemId}>
91
152
  <DropdownMenuTrigger asChild>
92
153
  <MoreVertical className="w-4 h-4 text-gray-500 hover:text-gray-700" />
93
154
  </DropdownMenuTrigger>
94
155
  <DropdownMenuContent
95
- className="w-48 bg-white shadow-lg rounded-md border-4 z-[100] border-white"
156
+ className="w-fit bg-white shadow-lg rounded-md border-4 z-[100] border-white"
96
157
  style={{
97
158
  position: "absolute",
98
159
  top: "100%",
@@ -103,13 +164,24 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
103
164
  }}
104
165
  >
105
166
  <DropdownMenuGroup>
167
+ {isLeafNode && (
168
+ <DropdownMenuItem
169
+ onClick={() => {
170
+ setRenamingFile(itemId)
171
+ }}
172
+ className="flex items-center px-3 py-1 text-xs text-black hover:bg-gray-100 cursor-pointer"
173
+ >
174
+ <Pencil className="mr-2 h-3 w-3" />
175
+ Rename
176
+ </DropdownMenuItem>
177
+ )}
106
178
  <DropdownMenuItem
107
179
  onClick={() => {
108
180
  const { fileDeleted } = handleDeleteFile({
109
- filename: relativePath,
181
+ filename: itemId,
110
182
  onError: (error) => {
111
183
  toast({
112
- title: `Error deleting file ${relativePath}`,
184
+ title: `Error deleting file ${itemId}`,
113
185
  description: error.message,
114
186
  })
115
187
  },
@@ -118,15 +190,16 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
118
190
  setErrorMessage("")
119
191
  }
120
192
  }}
121
- className="flex items-center px-4 py-1 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer"
193
+ className="flex items-center px-3 py-1 text-xs text-red-600 hover:bg-gray-100 cursor-pointer"
122
194
  >
195
+ <Trash2 className="mr-2 h-3 w-3" />
123
196
  Delete
124
197
  </DropdownMenuItem>
125
198
  </DropdownMenuGroup>
126
199
  </DropdownMenuContent>
127
200
  </DropdownMenu>
128
201
  </>
129
- ),
202
+ ) : undefined,
130
203
  }
131
204
  }
132
205
 
@@ -152,7 +225,7 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
152
225
  }
153
226
 
154
227
  const treeData = transformFilesToTreeData(files)
155
- // console.log("treeData", files)
228
+
156
229
  const handleCreateFileInline = () => {
157
230
  const { newFileCreated } = handleCreateFile({
158
231
  newFileName,
@@ -19,34 +19,46 @@ export const LogContent = ({
19
19
  type?: "info" | "success" | "error"
20
20
  msg?: string
21
21
  message?: string
22
- timestamp?: string
22
+ timestamp?: string | number
23
+ [key: string]: unknown
23
24
  }>
24
25
  error?: ErrorObject | string | null
25
26
  }) => {
26
27
  return (
27
28
  <div className="font-mono text-xs space-y-1 min-w-0">
28
29
  {logs.map((log, i) => {
29
- const text = log.msg ?? log.message
30
+ const { type, msg, message, timestamp, ...rest } = log
31
+ const text = msg ?? message
30
32
  if (!text) return null
31
33
 
32
34
  return (
33
35
  <div
34
36
  key={i}
35
37
  className={`break-words whitespace-pre-wrap ${
36
- log.type === "error"
38
+ type === "error"
37
39
  ? "text-red-600"
38
- : log.type === "success"
40
+ : type === "success"
39
41
  ? "text-green-600"
40
42
  : "text-gray-600"
41
43
  }`}
42
44
  >
43
- {log.timestamp && (
45
+ {timestamp !== undefined && (
44
46
  <span className="text-gray-500 whitespace-nowrap">
45
- {new Date(log.timestamp).toLocaleTimeString()}
47
+ {new Date(Number(timestamp)).toLocaleTimeString()}
46
48
  </span>
47
49
  )}
48
- {log.timestamp && " "}
50
+ {timestamp !== undefined && " "}
49
51
  <span className="break-all">{text}</span>
52
+ {Object.keys(rest).filter((k) => k !== "package_release_id")
53
+ .length > 0 && (
54
+ <span className="text-gray-500">
55
+ {" "}
56
+ {Object.entries(rest)
57
+ .filter(([key]) => key !== "package_release_id")
58
+ .map(([key, value]) => `${key}: ${String(value)}`)
59
+ .join(" ")}
60
+ </span>
61
+ )}
50
62
  </div>
51
63
  )
52
64
  })}