@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
@@ -186,6 +186,7 @@ export const EditPackageDetailsDialog = ({
186
186
  "packageFile",
187
187
  { package_release_id: packageReleaseId },
188
188
  ])
189
+ qc.invalidateQueries(["packageFiles", packageReleaseId])
189
190
  toast({
190
191
  title: "Package details updated",
191
192
  description: "Successfully updated package details",
@@ -376,7 +377,7 @@ export const EditPackageDetailsDialog = ({
376
377
  className="mt-2 rounded-md"
377
378
  onToggle={(e) => setDangerOpen(e.currentTarget.open)}
378
379
  >
379
- <summary className="cursor-pointer p-2 font-medium text-sm text-black list-none flex justify-between items-center">
380
+ <summary className="select-none cursor-pointer p-2 font-medium text-sm text-black list-none flex justify-between items-center">
380
381
  Danger Zone
381
382
  <ChevronDown
382
383
  className={`w-4 h-4 mr-1 transition-transform ${dangerOpen ? "rotate-180" : ""}`}
@@ -405,7 +406,7 @@ export const EditPackageDetailsDialog = ({
405
406
  </div>
406
407
 
407
408
  <DialogFooter className="mt-auto">
408
- <div className="lg:px-2 flex flex-col sm:flex-row justify-end gap-2">
409
+ <div className="lg:px-2 select-none flex flex-col sm:flex-row justify-end gap-2">
409
410
  <Button
410
411
  variant="outline"
411
412
  onClick={() => onOpenChange(false)}
@@ -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,
@@ -169,7 +172,7 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
169
172
 
170
173
  if (urlParams.package_id && (!pkg || isLoading)) {
171
174
  return (
172
- <div className="flex items-center justify-center h-[60vh]">
175
+ <div className="flex items-center justify-center h-[80vh]">
173
176
  <div className="flex flex-col items-center justify-center">
174
177
  <div className="text-lg text-gray-500 mb-4">Loading</div>
175
178
  <Loader2 className="w-16 h-16 animate-spin text-gray-400" />
@@ -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
@@ -195,7 +205,7 @@ export const CodeEditor = ({
195
205
  projectName: "my-project",
196
206
  typescript: tsModule,
197
207
  logger: console,
198
- fetcher: async (input: RequestInfo | URL, init?: RequestInit) => {
208
+ fetcher: (async (input: RequestInfo | URL, init?: RequestInit) => {
199
209
  const registryPrefixes = [
200
210
  "https://data.jsdelivr.com/v1/package/resolve/npm/@tsci/",
201
211
  "https://data.jsdelivr.com/v1/package/npm/@tsci/",
@@ -221,7 +231,7 @@ export const CodeEditor = ({
221
231
  )
222
232
  }
223
233
  return fetch(input, init)
224
- },
234
+ }) as typeof fetch,
225
235
  delegate: {
226
236
  started: () => {
227
237
  const manualEditsTypeDeclaration = `
@@ -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 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
+ />
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: 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>
@@ -17,7 +17,7 @@ export const useCreatePackageMutation = ({
17
17
  is_private,
18
18
  is_unlisted,
19
19
  }: {
20
- name: string
20
+ name?: string
21
21
  description?: string
22
22
  is_private?: boolean
23
23
  is_unlisted?: boolean
@@ -1,10 +1,7 @@
1
1
  import { useEffect, useMemo, useState, useCallback, useRef } from "react"
2
2
  import { isValidFileName } from "@/lib/utils/isValidFileName"
3
3
  import { PackageFile } from "@/types/package"
4
- import {
5
- DEFAULT_CODE,
6
- generateRandomPackageName,
7
- } from "@/lib/utils/package-utils"
4
+ import { DEFAULT_CODE } from "@/lib/utils/package-utils"
8
5
  import { Package } from "fake-snippets-api/lib/db/schema"
9
6
  import { usePackageFiles } from "./use-package-files"
10
7
  import { decodeUrlHashToText } from "@/lib/decodeUrlHashToText"
@@ -17,6 +14,7 @@ import { useCreatePackageReleaseMutation } from "./use-create-package-release-mu
17
14
  import { useCreatePackageMutation } from "./use-create-package-mutation"
18
15
  import { findTargetFile } from "@/lib/utils/findTargetFile"
19
16
  import { encodeFsMapToUrlHash } from "@/lib/encodeFsMapToUrlHash"
17
+ import { isHiddenFile } from "@/components/ViewPackagePage/utils/is-hidden-file"
20
18
 
21
19
  export interface ICreateFileProps {
22
20
  newFileName: string
@@ -36,6 +34,16 @@ export interface IDeleteFileProps {
36
34
  onError: (error: Error) => void
37
35
  }
38
36
 
37
+ export interface IRenameFileProps {
38
+ oldFilename: string
39
+ newFilename: string
40
+ onError: (error: Error) => void
41
+ }
42
+
43
+ export interface IRenameFileResult {
44
+ fileRenamed: boolean
45
+ }
46
+
39
47
  export function useFileManagement({
40
48
  templateCode,
41
49
  currentPackage,
@@ -234,12 +242,69 @@ export function useFileManagement({
234
242
  }
235
243
  const updatedFiles = localFiles.filter((file) => file.path !== filename)
236
244
  setLocalFiles(updatedFiles)
237
- onFileSelect(updatedFiles[0]?.path || "")
245
+ onFileSelect(
246
+ updatedFiles.filter((file) => !isHiddenFile(file.path))[0]?.path || "",
247
+ )
238
248
  return {
239
249
  fileDeleted: true,
240
250
  }
241
251
  }
242
252
 
253
+ const renameFile = ({
254
+ oldFilename,
255
+ newFilename,
256
+ onError,
257
+ }: IRenameFileProps): IRenameFileResult => {
258
+ newFilename = newFilename.trim()
259
+ if (!newFilename) {
260
+ onError(new Error("File name cannot be empty"))
261
+ return {
262
+ fileRenamed: false,
263
+ }
264
+ }
265
+
266
+ // Extract just the filename from the path for validation
267
+ const fileNameOnly = newFilename.split("/").pop() || ""
268
+ if (!isValidFileName(fileNameOnly)) {
269
+ onError(new Error("Invalid file name"))
270
+ return {
271
+ fileRenamed: false,
272
+ }
273
+ }
274
+
275
+ const oldFileExists = localFiles?.some((file) => file.path === oldFilename)
276
+ if (!oldFileExists) {
277
+ onError(new Error("File does not exist"))
278
+ return {
279
+ fileRenamed: false,
280
+ }
281
+ }
282
+
283
+ const newFileExists = localFiles?.some((file) => file.path === newFilename)
284
+ if (newFileExists) {
285
+ onError(new Error("A file with this name already exists"))
286
+ return {
287
+ fileRenamed: false,
288
+ }
289
+ }
290
+
291
+ const updatedFiles = localFiles.map((file) => {
292
+ if (file.path === oldFilename) {
293
+ return { ...file, path: newFilename }
294
+ }
295
+ return file
296
+ })
297
+
298
+ setLocalFiles(updatedFiles)
299
+ if (currentFile === oldFilename) {
300
+ setCurrentFile(newFilename)
301
+ }
302
+
303
+ return {
304
+ fileRenamed: true,
305
+ }
306
+ }
307
+
243
308
  const savePackage = async (isPrivate: boolean) => {
244
309
  if (!isLoggedIn) {
245
310
  toast({
@@ -250,7 +315,6 @@ export function useFileManagement({
250
315
  }
251
316
 
252
317
  await createPackageMutation.mutateAsync({
253
- name: `${loggedInUser?.github_username}/${generateRandomPackageName()}`,
254
318
  is_private: isPrivate,
255
319
  })
256
320
  }
@@ -354,6 +418,7 @@ export function useFileManagement({
354
418
  fsMap,
355
419
  createFile,
356
420
  deleteFile,
421
+ renameFile,
357
422
  saveFiles,
358
423
  localFiles,
359
424
  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
+ }
@@ -5,6 +5,3 @@ export default () => (
5
5
  </board>
6
6
  )
7
7
  `.trim()
8
-
9
- export const generateRandomPackageName = () =>
10
- `untitled-package-${Math.floor(Math.random() * 900) + 100}`
@@ -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">