@tscircuit/fake-snippets 0.0.98 → 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/components/DownloadButtonAndMenu.tsx +13 -0
- package/src/components/FileSidebar.tsx +83 -10
- package/src/components/PackageBuildsPage/LogContent.tsx +19 -7
- package/src/components/package-port/CodeAndPreview.tsx +5 -0
- package/src/components/package-port/CodeEditor.tsx +19 -1
- package/src/components/package-port/CodeEditorHeader.tsx +12 -7
- package/src/components/ui/tree-view.tsx +51 -2
- package/src/hooks/useFileManagement.ts +70 -1
- package/src/lib/download-fns/download-spice-file.ts +13 -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tscircuit/fake-snippets",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.99",
|
|
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.
|
|
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": "
|
|
86
|
+
"@tscircuit/runframe": "0.0.715",
|
|
87
87
|
"@tscircuit/schematic-viewer": "^2.0.21",
|
|
88
88
|
"@types/babel__standalone": "^7.1.7",
|
|
89
89
|
"@types/bun": "^1.1.10",
|
|
@@ -187,6 +187,7 @@
|
|
|
187
187
|
"wouter": "^3.3.5",
|
|
188
188
|
"zod": "^3.23.8",
|
|
189
189
|
"zustand": "^4.5.5",
|
|
190
|
-
"zustand-hoist": "^2.0.1"
|
|
190
|
+
"zustand-hoist": "^2.0.1",
|
|
191
|
+
"circuit-json-to-spice": "^0.0.6"
|
|
191
192
|
}
|
|
192
193
|
}
|
|
@@ -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 {
|
|
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:
|
|
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-
|
|
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:
|
|
181
|
+
filename: itemId,
|
|
110
182
|
onError: (error) => {
|
|
111
183
|
toast({
|
|
112
|
-
title: `Error deleting file ${
|
|
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-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
38
|
+
type === "error"
|
|
37
39
|
? "text-red-600"
|
|
38
|
-
:
|
|
40
|
+
: type === "success"
|
|
39
41
|
? "text-green-600"
|
|
40
42
|
: "text-gray-600"
|
|
41
43
|
}`}
|
|
42
44
|
>
|
|
43
|
-
{
|
|
45
|
+
{timestamp !== undefined && (
|
|
44
46
|
<span className="text-gray-500 whitespace-nowrap">
|
|
45
|
-
{new Date(
|
|
47
|
+
{new Date(Number(timestamp)).toLocaleTimeString()}
|
|
46
48
|
</span>
|
|
47
49
|
)}
|
|
48
|
-
{
|
|
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
|
})}
|
|
@@ -16,6 +16,7 @@ import { toastManualEditConflicts } from "@/lib/utils/toastManualEditConflicts"
|
|
|
16
16
|
import { ManualEditEvent } from "@tscircuit/props"
|
|
17
17
|
import { useFileManagement } from "@/hooks/useFileManagement"
|
|
18
18
|
import { DEFAULT_CODE } from "@/lib/utils/package-utils"
|
|
19
|
+
import { useGlobalStore } from "@/hooks/use-global-store"
|
|
19
20
|
|
|
20
21
|
interface Props {
|
|
21
22
|
pkg?: Package
|
|
@@ -38,6 +39,7 @@ export interface CodeAndPreviewState {
|
|
|
38
39
|
|
|
39
40
|
export function CodeAndPreview({ pkg, projectUrl }: Props) {
|
|
40
41
|
const { toast } = useToast()
|
|
42
|
+
const session = useGlobalStore((s) => s.session)
|
|
41
43
|
const urlParams = useUrlParams()
|
|
42
44
|
|
|
43
45
|
const templateFromUrl = useMemo(
|
|
@@ -75,6 +77,7 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
|
|
|
75
77
|
setLocalFiles,
|
|
76
78
|
localFiles,
|
|
77
79
|
initialFiles,
|
|
80
|
+
renameFile,
|
|
78
81
|
} = useFileManagement({
|
|
79
82
|
templateCode: templateFromUrl?.code,
|
|
80
83
|
currentPackage: pkg,
|
|
@@ -208,6 +211,8 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
|
|
|
208
211
|
isSaving={isSaving}
|
|
209
212
|
handleCreateFile={createFile}
|
|
210
213
|
handleDeleteFile={deleteFile}
|
|
214
|
+
handleRenameFile={renameFile}
|
|
215
|
+
pkg={pkg}
|
|
211
216
|
currentFile={currentFile}
|
|
212
217
|
onFileSelect={onFileSelect}
|
|
213
218
|
files={localFiles}
|
|
@@ -35,6 +35,7 @@ import CodeEditorHeader, {
|
|
|
35
35
|
import FileSidebar from "../FileSidebar"
|
|
36
36
|
import { findTargetFile } from "@/lib/utils/findTargetFile"
|
|
37
37
|
import type { PackageFile } from "@/types/package"
|
|
38
|
+
import type { Package } from "fake-snippets-api/lib/db/schema"
|
|
38
39
|
import { useShikiHighlighter } from "@/hooks/use-shiki-highlighter"
|
|
39
40
|
import QuickOpen from "./QuickOpen"
|
|
40
41
|
import GlobalFindReplace from "./GlobalFindReplace"
|
|
@@ -43,6 +44,8 @@ import {
|
|
|
43
44
|
ICreateFileResult,
|
|
44
45
|
IDeleteFileProps,
|
|
45
46
|
IDeleteFileResult,
|
|
47
|
+
IRenameFileProps,
|
|
48
|
+
IRenameFileResult,
|
|
46
49
|
} from "@/hooks/useFileManagement"
|
|
47
50
|
import { isHiddenFile } from "../ViewPackagePage/utils/is-hidden-file"
|
|
48
51
|
import { inlineCopilot } from "codemirror-copilot"
|
|
@@ -65,14 +68,18 @@ export const CodeEditor = ({
|
|
|
65
68
|
pkgFilesLoaded,
|
|
66
69
|
currentFile,
|
|
67
70
|
onFileSelect,
|
|
71
|
+
handleRenameFile,
|
|
68
72
|
handleCreateFile,
|
|
69
73
|
handleDeleteFile,
|
|
74
|
+
pkg,
|
|
70
75
|
}: {
|
|
71
76
|
onCodeChange: (code: string, filename?: string) => void
|
|
72
77
|
files: PackageFile[]
|
|
73
78
|
isSaving?: boolean
|
|
74
79
|
handleCreateFile: (props: ICreateFileProps) => ICreateFileResult
|
|
75
80
|
handleDeleteFile: (props: IDeleteFileProps) => IDeleteFileResult
|
|
81
|
+
handleRenameFile: (props: IRenameFileProps) => IRenameFileResult
|
|
82
|
+
pkg?: Package
|
|
76
83
|
readOnly?: boolean
|
|
77
84
|
isStreaming?: boolean
|
|
78
85
|
pkgFilesLoaded?: boolean
|
|
@@ -108,6 +115,9 @@ export const CodeEditor = ({
|
|
|
108
115
|
return files.find((x) => x.path === "index.tsx")?.path || "index.tsx"
|
|
109
116
|
}, [files])
|
|
110
117
|
|
|
118
|
+
const [sidebarOpen, setSidebarOpen] = useState(false)
|
|
119
|
+
const [isCreatingFile, setIsCreatingFile] = useState(false)
|
|
120
|
+
|
|
111
121
|
// Set current file on component mount
|
|
112
122
|
useEffect(() => {
|
|
113
123
|
if (files.length === 0 || !pkgFilesLoaded || currentFile) return
|
|
@@ -786,10 +796,14 @@ export const CodeEditor = ({
|
|
|
786
796
|
}
|
|
787
797
|
})
|
|
788
798
|
|
|
799
|
+
useHotkeyCombo("cmd+m", () => {
|
|
800
|
+
setSidebarOpen(true)
|
|
801
|
+
setIsCreatingFile(true)
|
|
802
|
+
})
|
|
803
|
+
|
|
789
804
|
if (isStreaming) {
|
|
790
805
|
return <div className="font-mono whitespace-pre-wrap text-xs">{code}</div>
|
|
791
806
|
}
|
|
792
|
-
const [sidebarOpen, setSidebarOpen] = useState(false)
|
|
793
807
|
return (
|
|
794
808
|
<div className="flex h-[98vh] w-full overflow-hidden">
|
|
795
809
|
<FileSidebar
|
|
@@ -800,7 +814,11 @@ export const CodeEditor = ({
|
|
|
800
814
|
}
|
|
801
815
|
onFileSelect={(path) => handleFileChange(path)}
|
|
802
816
|
handleCreateFile={handleCreateFile}
|
|
817
|
+
handleRenameFile={handleRenameFile}
|
|
803
818
|
handleDeleteFile={handleDeleteFile}
|
|
819
|
+
isCreatingFile={isCreatingFile}
|
|
820
|
+
setIsCreatingFile={setIsCreatingFile}
|
|
821
|
+
pkg={pkg}
|
|
804
822
|
/>
|
|
805
823
|
<div className="flex flex-col flex-1 w-full min-w-0 h-full">
|
|
806
824
|
{showImportAndFormatButtons && (
|
|
@@ -189,13 +189,10 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
|
|
|
189
189
|
onError: (error) => {
|
|
190
190
|
throw error
|
|
191
191
|
},
|
|
192
|
-
openFile: false,
|
|
193
192
|
})
|
|
194
193
|
if (!createFileResult.newFileCreated) {
|
|
195
|
-
throw new Error("Failed to create file")
|
|
194
|
+
throw new Error("Failed to create component file")
|
|
196
195
|
}
|
|
197
|
-
const newContent = `import ${componentName.replace(/-/g, "")} from "./${componentName}.tsx"\n${files[currentFile || ""]}`
|
|
198
|
-
updateFileContent(currentFile, newContent)
|
|
199
196
|
}
|
|
200
197
|
}
|
|
201
198
|
|
|
@@ -221,7 +218,15 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
|
|
|
221
218
|
sidebarOpen ? "-ml-2" : "-ml-1"
|
|
222
219
|
}`}
|
|
223
220
|
>
|
|
224
|
-
<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
|
+
/>
|
|
225
230
|
</SelectTrigger>
|
|
226
231
|
<SelectContent>
|
|
227
232
|
{Object.keys(files)
|
|
@@ -307,12 +312,12 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
|
|
|
307
312
|
onClick={() => {
|
|
308
313
|
setAiAutocompleteEnabled((prev) => !prev)
|
|
309
314
|
}}
|
|
310
|
-
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"}`}
|
|
311
316
|
>
|
|
312
317
|
<Bot className="h-4 w-4" />
|
|
313
318
|
{!aiAutocompleteEnabled && (
|
|
314
319
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
315
|
-
<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" />
|
|
316
321
|
</div>
|
|
317
322
|
)}
|
|
318
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>
|
|
@@ -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,
|
|
@@ -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
|
+
}
|
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">
|