@tscircuit/fake-snippets 0.0.71 → 0.0.73

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 (43) hide show
  1. package/bun-tests/fake-snippets-api/routes/package_files/create_or_update.test.ts +26 -0
  2. package/bun.lock +58 -58
  3. package/dist/bundle.js +56 -5
  4. package/dist/index.d.ts +15 -2
  5. package/dist/index.js +47 -1
  6. package/dist/schema.d.ts +8 -0
  7. package/dist/schema.js +1 -0
  8. package/fake-snippets-api/lib/db/db-client.ts +47 -0
  9. package/fake-snippets-api/lib/db/schema.ts +1 -0
  10. package/fake-snippets-api/lib/package_file/generate-fs-sha.ts +20 -0
  11. package/fake-snippets-api/lib/public-mapping/public-map-package.ts +1 -0
  12. package/fake-snippets-api/routes/api/package_files/create.ts +3 -0
  13. package/fake-snippets-api/routes/api/package_files/create_or_update.ts +9 -3
  14. package/fake-snippets-api/routes/api/package_files/delete.ts +3 -0
  15. package/fake-snippets-api/routes/api/packages/create.ts +1 -0
  16. package/package.json +11 -11
  17. package/src/App.tsx +5 -0
  18. package/src/components/FileSidebar.tsx +111 -37
  19. package/src/components/JLCPCBImportDialog.tsx +1 -1
  20. package/src/components/PackageBuildsPage/DeploymentDetailsPage.tsx +56 -0
  21. package/src/components/PackageBuildsPage/build-preview-content.tsx +11 -0
  22. package/src/components/PackageBuildsPage/collapsible-section.tsx +70 -0
  23. package/src/components/PackageBuildsPage/deployment-details-panel.tsx +84 -0
  24. package/src/components/PackageBuildsPage/deployment-header.tsx +75 -0
  25. package/src/components/PackageCard.tsx +1 -10
  26. package/src/components/TrendingPackagesCarousel.tsx +5 -16
  27. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +1 -1
  28. package/src/components/ViewPackagePage/components/preview-image-squares.tsx +2 -6
  29. package/src/components/package-port/CodeAndPreview.tsx +78 -267
  30. package/src/components/package-port/CodeEditor.tsx +30 -19
  31. package/src/components/package-port/CodeEditorHeader.tsx +7 -6
  32. package/src/components/package-port/EditorNav.tsx +17 -13
  33. package/src/components/ui/tree-view.tsx +3 -3
  34. package/src/hooks/use-current-package-id.ts +2 -8
  35. package/src/hooks/use-preview-images.ts +3 -15
  36. package/src/hooks/useFileManagement.ts +257 -38
  37. package/src/hooks/usePackageFilesLoader.ts +2 -2
  38. package/src/hooks/useUpdatePackageFilesMutation.ts +50 -24
  39. package/src/lib/utils/checkIfManualEditsImported.ts +9 -0
  40. package/src/pages/editor.tsx +2 -10
  41. package/src/pages/package-builds.tsx +33 -0
  42. package/src/pages/package-editor.tsx +2 -14
  43. package/src/hooks/use-get-fsmap-hash-for-package.ts +0 -19
@@ -1,28 +1,18 @@
1
1
  import { CodeEditor } from "@/components/package-port/CodeEditor"
2
2
  import { usePackageVisibilitySettingsDialog } from "@/components/dialogs/package-visibility-settings-dialog"
3
- import { useAxios } from "@/hooks/use-axios"
4
- import { useGlobalStore } from "@/hooks/use-global-store"
5
3
  import { useToast } from "@/hooks/use-toast"
6
4
  import { useUrlParams } from "@/hooks/use-url-params"
7
5
  import useWarnUserOnPageChange from "@/hooks/use-warn-user-on-page-change"
8
- import { decodeUrlHashToText } from "@/lib/decodeUrlHashToText"
9
6
  import { getSnippetTemplate } from "@/lib/get-snippet-template"
10
7
  import { cn } from "@/lib/utils"
11
8
  import type { Package } from "fake-snippets-api/lib/db/schema"
12
9
  import { Loader2 } from "lucide-react"
13
- import { useEffect, useMemo, useState } from "react"
10
+ import { useMemo, useState } from "react"
14
11
  import EditorNav from "@/components/package-port/EditorNav"
15
12
  import { SuspenseRunFrame } from "../SuspenseRunFrame"
16
13
  import { applyEditEventsToManualEditsFile } from "@tscircuit/core"
17
- import { usePackageFileById, usePackageFiles } from "@/hooks/use-package-files"
18
- import { useCreatePackageMutation } from "@/hooks/use-create-package-mutation"
19
- import { useCreatePackageReleaseMutation } from "@/hooks/use-create-package-release-mutation"
20
- import { useUpdatePackageFilesMutation } from "@/hooks/useUpdatePackageFilesMutation"
21
- import { usePackageFilesLoader } from "@/hooks/usePackageFilesLoader"
22
- import { findTargetFile } from "@/lib/utils/findTargetFile"
23
14
  import { toastManualEditConflicts } from "@/lib/utils/toastManualEditConflicts"
24
15
  import { ManualEditEvent } from "@tscircuit/props"
25
- import { isValidFileName } from "@/lib/utils/isValidFileName"
26
16
  import { useFileManagement } from "@/hooks/useFileManagement"
27
17
 
28
18
  interface Props {
@@ -34,29 +24,17 @@ export interface PackageFile {
34
24
  content: string
35
25
  }
36
26
 
37
- export interface CreateFileProps {
38
- newFileName: string
39
- setErrorMessage: (message: string) => void
40
- onFileSelect: (fileName: string) => void
41
- setNewFileName: (fileName: string) => void
42
- setIsCreatingFile: (isCreatingFile: boolean) => void
43
- }
44
-
45
27
  export interface CodeAndPreviewState {
46
- pkgFilesWithContent: PackageFile[]
47
- initialFilesLoad: PackageFile[]
48
28
  showPreview: boolean
49
29
  fullScreen: boolean
50
30
  lastSavedAt: number
51
31
  circuitJson: null | any
52
32
  isPrivate: boolean
53
33
  lastRunCode: string
54
- pkgFilesLoaded: boolean
55
- currentFile: string
56
34
  defaultComponentFile?: string
57
35
  }
58
36
 
59
- const DEFAULT_CODE = `
37
+ export const DEFAULT_CODE = `
60
38
  export default () => (
61
39
  <board width="10mm" height="10mm">
62
40
  {/* write your code here! */}
@@ -65,236 +43,76 @@ export default () => (
65
43
  `.trim()
66
44
 
67
45
  export const generateRandomPackageName = () =>
68
- `untitled-package-${Math.floor(Math.random() * 90) + 10}`
46
+ `untitled-package-${Math.floor(Math.random() * 900) + 100}`
69
47
 
70
48
  export function CodeAndPreview({ pkg }: Props) {
71
49
  const { toast } = useToast()
72
- const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
73
- const loggedInUser = useGlobalStore((s) => s.session)
74
50
  const urlParams = useUrlParams()
75
51
 
76
52
  const templateFromUrl = useMemo(
77
53
  () => (urlParams.template ? getSnippetTemplate(urlParams.template) : null),
78
54
  [urlParams.template],
79
55
  )
80
-
81
- const pkgFiles = usePackageFiles(pkg?.latest_package_release_id)
82
- const indexFileFromHook = usePackageFileById(
83
- pkgFiles.data?.find((x) => x.file_path === "index.tsx")?.package_file_id ??
84
- null,
85
- )
86
-
87
- const defaultCode = useMemo(() => {
88
- if (indexFileFromHook.data?.content_text)
89
- return indexFileFromHook.data.content_text
90
- return (
91
- templateFromUrl?.code ??
92
- decodeUrlHashToText(window.location.toString()) ??
93
- (urlParams.package_id ? "" : DEFAULT_CODE)
94
- )
95
- }, [indexFileFromHook.data, templateFromUrl, urlParams.package_id])
96
-
97
56
  const [state, setState] = useState<CodeAndPreviewState>({
98
- pkgFilesWithContent: !pkg
99
- ? [{ path: "index.tsx", content: defaultCode }]
100
- : [],
101
- initialFilesLoad: [],
102
57
  showPreview: true,
103
58
  fullScreen: false,
104
59
  lastSavedAt: Date.now(),
105
60
  circuitJson: null,
106
61
  isPrivate: false,
107
- lastRunCode: defaultCode,
108
- pkgFilesLoaded: !urlParams.package_id,
109
- currentFile: "",
62
+ lastRunCode: "",
110
63
  })
111
64
 
112
- const manualEditsFileContent = useMemo(() => {
113
- return (
114
- state.pkgFilesWithContent.find((x) => x.path === "manual-edits.json")
115
- ?.content ?? ""
116
- )
117
- }, [state.pkgFilesWithContent])
118
-
119
- const entryPointCode = useMemo(() => {
120
- const defaultComponentFile = findTargetFile(state.pkgFilesWithContent, null)
121
- if (defaultComponentFile?.content) {
122
- setState((prev) => ({
123
- ...prev,
124
- defaultComponentFile: defaultComponentFile.path,
125
- }))
126
- return defaultComponentFile.content
127
- }
128
- return (
129
- state.pkgFilesWithContent.find((x) => x.path === "index.tsx")?.content ??
130
- defaultCode
131
- )
132
- }, [state.pkgFilesWithContent, defaultCode])
133
-
134
65
  const packageType =
135
66
  pkg?.snippet_type ?? templateFromUrl?.type ?? urlParams.snippet_type
136
67
 
137
68
  const { Dialog: NewPackageSaveDialog, openDialog: openNewPackageSaveDialog } =
138
69
  usePackageVisibilitySettingsDialog()
139
70
 
140
- const { data: loadedFiles, isLoading: isLoadingFiles } =
141
- usePackageFilesLoader(pkg)
142
-
143
- useEffect(() => {
144
- if (!pkgFiles.data?.length) {
145
- if (pkg && state.pkgFilesWithContent.length === 0) {
146
- const defaultFiles = [{ path: "index.tsx", content: defaultCode }]
147
- setState((prev) => ({
148
- ...prev,
149
- pkgFilesWithContent: defaultFiles,
150
- initialFilesLoad: defaultFiles,
151
- lastRunCode: defaultCode,
152
- }))
153
- }
154
- return
155
- }
156
-
157
- if (loadedFiles && !isLoadingFiles) {
158
- const processedResults = [...loadedFiles]
159
- setState((prev) => ({
160
- ...prev,
161
- pkgFilesWithContent: processedResults,
162
- pkgFilesLoaded: true,
163
- initialFilesLoad: processedResults,
164
- lastRunCode:
165
- processedResults.find((x) => x.path === "index.tsx")?.content ??
166
- defaultCode,
167
- }))
168
- }
169
- }, [isLoadingFiles, pkg, pkgFiles.data, defaultCode])
170
-
171
- const createPackageMutation = useCreatePackageMutation()
172
- const { mutate: createRelease } = useCreatePackageReleaseMutation({
173
- onSuccess: () => {
174
- toast({
175
- title: "Package released",
176
- description: "Your package has been released successfully.",
177
- })
71
+ const {
72
+ savePackage,
73
+ isSaving,
74
+ currentFile,
75
+ fsMap,
76
+ isLoading,
77
+ createFile,
78
+ deleteFile,
79
+ onFileSelect,
80
+ saveFiles,
81
+ setLocalFiles,
82
+ localFiles,
83
+ initialFiles,
84
+ } = useFileManagement({
85
+ templateCode: templateFromUrl?.code,
86
+ currentPackage: pkg,
87
+ fileChoosen: urlParams.file_path ?? null,
88
+ openNewPackageSaveDialog,
89
+ updateLastUpdated: () => {
90
+ setState((prev) => ({ ...prev, lastSavedAt: Date.now() }))
178
91
  },
179
92
  })
180
93
 
181
- const axios = useAxios()
182
-
183
- const updatePackageFilesMutation = useUpdatePackageFilesMutation({
184
- pkg,
185
- pkgFilesWithContent: state.pkgFilesWithContent,
186
- initialFilesLoad: state.initialFilesLoad,
187
- pkgFiles,
188
- axios,
189
- toast,
190
- })
191
-
192
94
  const hasUnsavedChanges = useMemo(
193
95
  () =>
194
- !updatePackageFilesMutation.isLoading &&
195
- Date.now() - state.lastSavedAt > 1000 &&
196
- state.pkgFilesWithContent.some((file) => {
197
- const initialFile = state.initialFilesLoad.find(
198
- (x) => x.path === file.path,
199
- )
200
- return initialFile?.content !== file.content
201
- }),
202
- [
203
- state.pkgFilesWithContent,
204
- state.initialFilesLoad,
205
- updatePackageFilesMutation.isLoading,
206
- state.lastSavedAt,
207
- ],
96
+ (!isSaving &&
97
+ Date.now() - state.lastSavedAt > 1000 &&
98
+ localFiles.some((file) => {
99
+ const initialFile = initialFiles.find((x) => x.path === file.path)
100
+ return initialFile?.content !== file.content
101
+ })) ||
102
+ localFiles.length !== initialFiles.length,
103
+ [localFiles, initialFiles, isSaving, state.lastSavedAt],
208
104
  )
209
105
 
210
106
  useWarnUserOnPageChange({ hasUnsavedChanges })
211
107
 
212
- const handleNewPackageSaveRequest = async (isPrivate: boolean) => {
213
- setState((prev) => ({ ...prev, lastSavedAt: Date.now() }))
214
- const newPackage = await createPackageMutation.mutateAsync({
215
- name: `${loggedInUser?.github_username}/${generateRandomPackageName()}`,
216
- is_private: isPrivate,
217
- })
218
-
219
- if (newPackage) {
220
- createRelease(
221
- {
222
- package_name_with_version: `${newPackage.name}@latest`,
223
- },
224
- {
225
- onSuccess: () => {
226
- updatePackageFilesMutation.mutate({
227
- package_name_with_version: `${newPackage.name}@latest`,
228
- ...newPackage,
229
- })
230
- },
231
- },
232
- )
233
- }
234
- }
235
-
236
- const handleSave = async () => {
237
- if (!isLoggedIn) {
238
- toast({
239
- title: "Not Logged In",
240
- description: "You must be logged in to save your package.",
241
- variant: "destructive",
242
- })
243
- return
244
- }
245
-
246
- if (!pkg) {
247
- openNewPackageSaveDialog()
248
- return
249
- }
250
-
251
- setState((prev) => ({ ...prev, lastSavedAt: Date.now() }))
252
-
253
- if (pkg) {
254
- updatePackageFilesMutation.mutate(
255
- {
256
- package_name_with_version: `${pkg.name}@latest`,
257
- ...pkg,
258
- },
259
- {
260
- onSuccess: () => {
261
- setState((prev) => ({
262
- ...prev,
263
- initialFilesLoad: [...prev.pkgFilesWithContent],
264
- }))
265
- pkgFiles.refetch()
266
- },
267
- },
268
- )
269
- }
270
- }
271
-
272
108
  const currentFileCode = useMemo(
273
109
  () =>
274
- state.pkgFilesWithContent.find((x) => x.path === state.currentFile)
275
- ?.content ??
110
+ localFiles.find((x) => x.path === currentFile)?.content ??
276
111
  state.defaultComponentFile ??
277
112
  DEFAULT_CODE,
278
- [state.pkgFilesWithContent, state.currentFile],
113
+ [localFiles, currentFile],
279
114
  )
280
115
 
281
- const fsMap = useMemo(() => {
282
- return {
283
- "manual-edits.json": manualEditsFileContent || "{}",
284
- ...state.pkgFilesWithContent.reduce(
285
- (acc, file) => {
286
- acc[file.path] = file.content
287
- return acc
288
- },
289
- {} as Record<string, string>,
290
- ),
291
- }
292
- }, [
293
- manualEditsFileContent,
294
- entryPointCode,
295
- packageType,
296
- state.pkgFilesWithContent,
297
- ])
298
116
  const mainComponentPath = useMemo(() => {
299
117
  const isReactComponentExported =
300
118
  /export function\s+\w+/.test(currentFileCode) ||
@@ -303,52 +121,50 @@ export function CodeAndPreview({ pkg }: Props) {
303
121
  /export default\s+function\s*(\w*)\s*\(/.test(currentFileCode) ||
304
122
  /export default\s*\(\s*\)\s*=>/.test(currentFileCode)
305
123
 
306
- return (state.currentFile?.endsWith(".tsx") ||
307
- state.currentFile?.endsWith(".ts")) &&
308
- !!state.pkgFilesWithContent.some((x) => x.path == state.currentFile) &&
124
+ return (currentFile?.endsWith(".tsx") || currentFile?.endsWith(".ts")) &&
125
+ !!localFiles.some((x) => x.path == currentFile) &&
309
126
  isReactComponentExported
310
- ? state.currentFile
127
+ ? currentFile
311
128
  : state.defaultComponentFile
312
- }, [state.currentFile, state.pkgFilesWithContent, currentFileCode])
129
+ }, [currentFile, localFiles, currentFileCode])
313
130
 
314
131
  const handleEditEvent = (event: ManualEditEvent) => {
315
- const parsedManualEdits = JSON.parse(manualEditsFileContent || "{}")
132
+ const parsedManualEdits = JSON.parse(
133
+ localFiles.find((x) => x.path === "manual-edits.json")?.content || "{}",
134
+ )
316
135
  const newManualEditsFileContent = applyEditEventsToManualEditsFile({
317
136
  circuitJson: state.circuitJson,
318
137
  editEvents: [event],
319
138
  manualEditsFile: parsedManualEdits,
320
139
  })
321
140
 
322
- setState((prev) => {
323
- const manualEditsIndex = prev.pkgFilesWithContent.findIndex(
324
- (file) => file.path === "manual-edits.json",
325
- )
326
-
327
- const updatedFiles = [...prev.pkgFilesWithContent]
141
+ setLocalFiles(
142
+ (() => {
143
+ const manualEditsIndex = localFiles.findIndex(
144
+ (file) => file.path === "manual-edits.json",
145
+ )
328
146
 
329
- if (manualEditsIndex !== -1) {
330
- // Update existing manual-edits.json
331
- updatedFiles[manualEditsIndex] = {
332
- ...updatedFiles[manualEditsIndex],
333
- content: JSON.stringify(newManualEditsFileContent, null, 2),
147
+ const updatedFiles = [...localFiles]
148
+
149
+ if (manualEditsIndex !== -1) {
150
+ // Update existing manual-edits.json
151
+ updatedFiles[manualEditsIndex] = {
152
+ ...updatedFiles[manualEditsIndex],
153
+ content: JSON.stringify(newManualEditsFileContent, null, 2),
154
+ }
155
+ } else {
156
+ // Add new manual-edits.json
157
+ updatedFiles.push({
158
+ path: "manual-edits.json",
159
+ content: JSON.stringify(newManualEditsFileContent, null, 2),
160
+ })
334
161
  }
335
- } else {
336
- // Add new manual-edits.json
337
- updatedFiles.push({
338
- path: "manual-edits.json",
339
- content: JSON.stringify(newManualEditsFileContent, null, 2),
340
- })
341
- }
342
- return {
343
- ...prev,
344
- pkgFilesWithContent: updatedFiles,
345
- }
346
- })
162
+ return updatedFiles
163
+ })(),
164
+ )
347
165
  }
348
166
 
349
- const { handleCreateFile } = useFileManagement(state, setState)
350
-
351
- if ((!pkg && urlParams.package_id) || pkgFiles.isLoading || isLoadingFiles) {
167
+ if ((!pkg && urlParams.package_id) || isLoading) {
352
168
  return (
353
169
  <div className="flex items-center justify-center h-64">
354
170
  <div className="flex flex-col items-center justify-center">
@@ -366,9 +182,9 @@ export function CodeAndPreview({ pkg }: Props) {
366
182
  pkg={pkg}
367
183
  packageType={packageType}
368
184
  code={String(currentFileCode)}
369
- isSaving={updatePackageFilesMutation.isLoading}
185
+ isSaving={isSaving}
370
186
  hasUnsavedChanges={hasUnsavedChanges}
371
- onSave={handleSave}
187
+ onSave={saveFiles}
372
188
  onTogglePreview={() =>
373
189
  setState((prev) => ({ ...prev, showPreview: !prev.showPreview }))
374
190
  }
@@ -384,24 +200,22 @@ export function CodeAndPreview({ pkg }: Props) {
384
200
  )}
385
201
  >
386
202
  <CodeEditor
387
- handleCreateFile={handleCreateFile}
388
- currentFile={state.currentFile}
389
- setCurrentFile={(file) =>
390
- setState((prev) => ({ ...prev, currentFile: file }))
391
- }
392
- files={state.pkgFilesWithContent}
203
+ handleCreateFile={createFile}
204
+ handleDeleteFile={deleteFile}
205
+ currentFile={currentFile}
206
+ onFileSelect={onFileSelect}
207
+ files={localFiles}
393
208
  onCodeChange={(newCode, filename) => {
394
- const targetFilename = filename ?? state.currentFile
395
- setState((prev) => ({
396
- ...prev,
397
- pkgFilesWithContent: prev.pkgFilesWithContent.map((file) =>
209
+ const targetFilename = filename ?? currentFile
210
+ setLocalFiles((prev) =>
211
+ prev.map((file) =>
398
212
  file.path === targetFilename
399
213
  ? { ...file, content: newCode }
400
214
  : file,
401
215
  ),
402
- }))
216
+ )
403
217
  }}
404
- pkgFilesLoaded={state.pkgFilesLoaded}
218
+ pkgFilesLoaded={!isLoading}
405
219
  />
406
220
  </div>
407
221
  {state.showPreview && (
@@ -427,15 +241,12 @@ export function CodeAndPreview({ pkg }: Props) {
427
241
  onEditEvent={(event) => {
428
242
  handleEditEvent(event)
429
243
  }}
430
- fsMap={fsMap}
244
+ fsMap={fsMap ?? {}}
431
245
  />
432
246
  </div>
433
247
  )}
434
248
  </div>
435
- <NewPackageSaveDialog
436
- initialIsPrivate={false}
437
- onSave={handleNewPackageSaveRequest}
438
- />
249
+ <NewPackageSaveDialog initialIsPrivate={false} onSave={savePackage} />
439
250
  </div>
440
251
  )
441
252
  }
@@ -24,12 +24,20 @@ import {
24
24
  import { EditorView } from "codemirror"
25
25
  import { useEffect, useMemo, useRef, useState } from "react"
26
26
  import tsModule from "typescript"
27
- import CodeEditorHeader from "@/components/package-port/CodeEditorHeader"
27
+ import CodeEditorHeader, {
28
+ FileName,
29
+ } from "@/components/package-port/CodeEditorHeader"
28
30
  import { useCodeCompletionApi } from "@/hooks/use-code-completion-ai-api"
29
31
  import FileSidebar from "../FileSidebar"
30
32
  import { findTargetFile } from "@/lib/utils/findTargetFile"
31
- import type { CreateFileProps, PackageFile } from "./CodeAndPreview"
33
+ import type { PackageFile } from "./CodeAndPreview"
32
34
  import { useShikiHighlighter } from "@/hooks/use-shiki-highlighter"
35
+ import {
36
+ ICreateFileProps,
37
+ ICreateFileResult,
38
+ IDeleteFileProps,
39
+ IDeleteFileResult,
40
+ } from "@/hooks/useFileManagement"
33
41
 
34
42
  const defaultImports = `
35
43
  import React from "@types/react/jsx-runtime"
@@ -46,19 +54,21 @@ export const CodeEditor = ({
46
54
  onFileContentChanged,
47
55
  pkgFilesLoaded,
48
56
  currentFile,
49
- setCurrentFile,
57
+ onFileSelect,
50
58
  handleCreateFile,
59
+ handleDeleteFile,
51
60
  }: {
52
61
  onCodeChange: (code: string, filename?: string) => void
53
62
  files: PackageFile[]
54
- handleCreateFile: (props: CreateFileProps) => void
63
+ handleCreateFile: (props: ICreateFileProps) => ICreateFileResult
64
+ handleDeleteFile: (props: IDeleteFileProps) => IDeleteFileResult
55
65
  readOnly?: boolean
56
66
  isStreaming?: boolean
57
67
  pkgFilesLoaded?: boolean
58
68
  showImportAndFormatButtons?: boolean
59
69
  onFileContentChanged?: (path: string, content: string) => void
60
- currentFile: string
61
- setCurrentFile: (path: string) => void
70
+ currentFile: string | null
71
+ onFileSelect: (path: string) => void
62
72
  }) => {
63
73
  const editorRef = useRef<HTMLDivElement>(null)
64
74
  const viewRef = useRef<EditorView | null>(null)
@@ -85,7 +95,6 @@ export const CodeEditor = ({
85
95
  if (files.length === 0 || !pkgFilesLoaded || currentFile) return
86
96
 
87
97
  const targetFile = findTargetFile(files, filePathFromUrl)
88
-
89
98
  if (targetFile) {
90
99
  handleFileChange(targetFile.path)
91
100
  setCode(targetFile.content)
@@ -229,7 +238,7 @@ export const CodeEditor = ({
229
238
  // Set up base extensions
230
239
  const baseExtensions = [
231
240
  basicSetup,
232
- currentFile.endsWith(".json")
241
+ currentFile?.endsWith(".json")
233
242
  ? json()
234
243
  : javascript({ typescript: true, jsx: true }),
235
244
  keymap.of([indentWithTab]),
@@ -237,7 +246,7 @@ export const CodeEditor = ({
237
246
  EditorView.updateListener.of((update) => {
238
247
  if (update.docChanged) {
239
248
  const newContent = update.state.doc.toString()
240
-
249
+ if (!currentFile) return
241
250
  if (newContent === lastFilesEventContent[currentFile]) return
242
251
  lastFilesEventContent[currentFile] = newContent
243
252
 
@@ -272,12 +281,12 @@ export const CodeEditor = ({
272
281
 
273
282
  // Add TypeScript-specific extensions and handlers
274
283
  const tsExtensions =
275
- currentFile.endsWith(".tsx") || currentFile.endsWith(".ts")
284
+ currentFile?.endsWith(".tsx") || currentFile?.endsWith(".ts")
276
285
  ? [
277
286
  tsFacet.of({
278
287
  env,
279
- path: currentFile.endsWith(".ts")
280
- ? currentFile.replace(/\.ts$/, ".tsx")
288
+ path: currentFile?.endsWith(".ts")
289
+ ? currentFile?.replace(/\.ts$/, ".tsx")
281
290
  : currentFile,
282
291
  }),
283
292
  tsSync(),
@@ -407,7 +416,7 @@ export const CodeEditor = ({
407
416
  const end = start + match[0].length
408
417
  decorations.push(
409
418
  Decoration.mark({
410
- class: "cm-underline",
419
+ class: "cm-underline cursor-pointer",
411
420
  }).range(start, end),
412
421
  )
413
422
  }
@@ -421,7 +430,7 @@ export const CodeEditor = ({
421
430
  : []
422
431
 
423
432
  const state = EditorState.create({
424
- doc: fileMap[currentFile] || "",
433
+ doc: fileMap[currentFile || ""] || "",
425
434
  extensions: [...baseExtensions, ...tsExtensions],
426
435
  })
427
436
 
@@ -432,7 +441,7 @@ export const CodeEditor = ({
432
441
 
433
442
  viewRef.current = view
434
443
 
435
- if (currentFile.endsWith(".tsx") || currentFile.endsWith(".ts")) {
444
+ if (currentFile?.endsWith(".tsx") || currentFile?.endsWith(".ts")) {
436
445
  ata(`${defaultImports}${code}`)
437
446
  }
438
447
 
@@ -459,7 +468,7 @@ export const CodeEditor = ({
459
468
  }
460
469
 
461
470
  const updateEditorToMatchCurrentFile = () => {
462
- const currentContent = fileMap[currentFile] || ""
471
+ const currentContent = fileMap[currentFile || ""] || ""
463
472
  updateCurrentEditorContent(currentContent)
464
473
  }
465
474
 
@@ -468,14 +477,14 @@ export const CodeEditor = ({
468
477
  useEffect(() => {
469
478
  if (
470
479
  ataRef.current &&
471
- (currentFile.endsWith(".tsx") || currentFile.endsWith(".ts"))
480
+ (currentFile?.endsWith(".tsx") || currentFile?.endsWith(".ts"))
472
481
  ) {
473
482
  ataRef.current(`${defaultImports}${code}`)
474
483
  }
475
484
  }, [codeImports])
476
485
 
477
486
  const handleFileChange = (path: string) => {
478
- setCurrentFile(path)
487
+ onFileSelect(path)
479
488
  try {
480
489
  // Set url query to file path
481
490
  const urlParams = new URLSearchParams(window.location.search)
@@ -484,7 +493,8 @@ export const CodeEditor = ({
484
493
  } catch {}
485
494
  }
486
495
 
487
- const updateFileContent = (path: string, newContent: string) => {
496
+ const updateFileContent = (path: FileName | null, newContent: string) => {
497
+ if (!path) return
488
498
  if (currentFile === path) {
489
499
  setCode(newContent)
490
500
  onCodeChange(newContent, path)
@@ -523,6 +533,7 @@ export const CodeEditor = ({
523
533
  }
524
534
  onFileSelect={handleFileChange}
525
535
  handleCreateFile={handleCreateFile}
536
+ handleDeleteFile={handleDeleteFile}
526
537
  />
527
538
  <div className="flex flex-col flex-1 w-full min-w-0 h-full">
528
539
  {showImportAndFormatButtons && (
@@ -23,9 +23,9 @@ import { isHiddenFile } from "../ViewPackagePage/utils/is-hidden-file"
23
23
  export type FileName = string
24
24
 
25
25
  interface CodeEditorHeaderProps {
26
- currentFile: FileName
26
+ currentFile: FileName | null
27
27
  files: Record<FileName, string>
28
- updateFileContent: (filename: FileName, content: string) => void
28
+ updateFileContent: (filename: FileName | null, content: string) => void
29
29
  fileSidebarState: ReturnType<typeof useState<boolean>>
30
30
  handleFileChange: (filename: FileName) => void
31
31
  entrypointFileName?: string
@@ -46,6 +46,7 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
46
46
 
47
47
  const handleFormatFile = useCallback(() => {
48
48
  if (!window.prettier || !window.prettierPlugins) return
49
+ if (!currentFile) return
49
50
  try {
50
51
  const currentContent = files[currentFile]
51
52
  let fileExtension = currentFile.split(".").pop()?.toLowerCase()
@@ -148,9 +149,9 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
148
149
  </div>
149
150
  </button>
150
151
  <div>
151
- <Select value={currentFile} onValueChange={handleFileChange}>
152
+ <Select value={currentFile || ""} onValueChange={handleFileChange}>
152
153
  <SelectTrigger
153
- className={`h-7 px-3 bg-white select-none transition-[margin] duration-300 ease-in-out ${
154
+ className={`h-7 w-24 px-3 bg-white select-none transition-[margin] duration-300 ease-in-out ${
154
155
  sidebarOpen ? "-ml-2" : "-ml-1"
155
156
  }`}
156
157
  >
@@ -197,7 +198,7 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
197
198
  </div>
198
199
 
199
200
  <div className="flex items-center overflow-x-hidden gap-2 px-2 py-1 ml-auto">
200
- {checkIfManualEditsImported(files, currentFile) && (
201
+ {checkIfManualEditsImported(files, currentFile || "") && (
201
202
  <DropdownMenu>
202
203
  <DropdownMenuTrigger asChild>
203
204
  <Button
@@ -235,7 +236,7 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
235
236
  </div>
236
237
  <ImportSnippetDialog
237
238
  onSnippetSelected={(snippet: any) => {
238
- const newContent = `import {} from "@tsci/${snippet.owner_name}.${snippet.unscoped_name}"\n${files[currentFile]}`
239
+ const newContent = `import {} from "@tsci/${snippet.owner_name}.${snippet.unscoped_name}"\n${files[currentFile || ""]}`
239
240
  updateFileContent(currentFile, newContent)
240
241
  }}
241
242
  />