@tscircuit/fake-snippets 0.0.42 → 0.0.44

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 +335 -15
  2. package/dist/bundle.js +13 -7
  3. package/fake-snippets-api/routes/api/package_files/create_or_update.ts +9 -0
  4. package/fake-snippets-api/routes/api/packages/update.ts +1 -0
  5. package/package.json +5 -5
  6. package/src/App.tsx +8 -0
  7. package/src/components/CodeEditor.tsx +5 -3
  8. package/src/components/CodeEditorHeader.tsx +1 -1
  9. package/src/components/DownloadButtonAndMenu.tsx +3 -2
  10. package/src/components/EditorNav.tsx +3 -3
  11. package/src/components/FileSidebar.tsx +84 -0
  12. package/src/components/ViewPackagePage/components/important-files-view.tsx +6 -1
  13. package/src/components/ViewPackagePage/components/package-header.tsx +39 -5
  14. package/src/components/dialogs/rename-package-dialog.tsx +81 -0
  15. package/src/components/dialogs/update-package-description-dialog.tsx +96 -0
  16. package/src/components/package-port/CodeAndPreview.tsx +417 -0
  17. package/src/components/package-port/CodeEditor.tsx +510 -0
  18. package/src/components/package-port/CodeEditorHeader.tsx +153 -0
  19. package/src/components/package-port/EditorNav.tsx +518 -0
  20. package/src/components/ui/tree-view.tsx +490 -0
  21. package/src/hooks/use-fork-package-mutation.ts +7 -0
  22. package/src/hooks/use-package.ts +23 -0
  23. package/src/hooks/useForkPackageMutation.ts +49 -0
  24. package/src/hooks/usePackageFilesLoader.ts +56 -0
  25. package/src/hooks/useUpdatePackageFilesMutation.ts +86 -0
  26. package/src/hooks/useUpdatePackageMutation.ts +63 -0
  27. package/src/lib/utils/checkIfManualEditsImported.ts +1 -1
  28. package/src/pages/package-editor.tsx +44 -0
  29. /package/fake-snippets-api/routes/api/{order_quote → order_quotes}/create.ts +0 -0
  30. /package/fake-snippets-api/routes/api/{order_quote → order_quotes}/create_all_vendor_quotes.ts +0 -0
  31. /package/fake-snippets-api/routes/api/{order_quote → order_quotes}/get.ts +0 -0
@@ -0,0 +1,510 @@
1
+ import { useSnippetsBaseApiUrl } from "@/hooks/use-snippets-base-api-url"
2
+ import { basicSetup } from "@/lib/codemirror/basic-setup"
3
+ import { autocompletion } from "@codemirror/autocomplete"
4
+ import { indentWithTab } from "@codemirror/commands"
5
+ import { javascript } from "@codemirror/lang-javascript"
6
+ import { json } from "@codemirror/lang-json"
7
+ import { EditorState } from "@codemirror/state"
8
+ import { Decoration, hoverTooltip, keymap } from "@codemirror/view"
9
+ import { getImportsFromCode } from "@tscircuit/prompt-benchmarks/code-runner-utils"
10
+ import type { ATABootstrapConfig } from "@typescript/ata"
11
+ import { setupTypeAcquisition } from "@typescript/ata"
12
+ import { TSCI_PACKAGE_PATTERN } from "@/lib/constants"
13
+ import {
14
+ createDefaultMapFromCDN,
15
+ createSystem,
16
+ createVirtualTypeScriptEnvironment,
17
+ } from "@typescript/vfs"
18
+ import {
19
+ tsAutocomplete,
20
+ tsFacet,
21
+ tsHover,
22
+ tsLinter,
23
+ tsSync,
24
+ } from "@valtown/codemirror-ts"
25
+ import { EditorView } from "codemirror"
26
+ import { useEffect, useMemo, useRef, useState } from "react"
27
+ import ts from "typescript"
28
+ import CodeEditorHeader from "@/components/package-port/CodeEditorHeader"
29
+ // import { copilotPlugin, Language } from "@valtown/codemirror-codeium"
30
+ import { useCodeCompletionApi } from "@/hooks/use-code-completion-ai-api"
31
+ import FileSidebar from "../FileSidebar"
32
+
33
+ const defaultImports = `
34
+ import React from "@types/react/jsx-runtime"
35
+ import { Circuit, createUseComponent } from "@tscircuit/core"
36
+ import type { CommonLayoutProps } from "@tscircuit/props"
37
+ `
38
+ export interface FileContent {
39
+ path: string
40
+ content: string
41
+ }
42
+
43
+ export const CodeEditor = ({
44
+ onCodeChange,
45
+ onDtsChange,
46
+ readOnly = false,
47
+ files = [],
48
+ isStreaming = false,
49
+ showImportAndFormatButtons = true,
50
+ onFileContentChanged,
51
+ }: {
52
+ onCodeChange: (code: string, filename?: string) => void
53
+ onDtsChange?: (dts: string) => void
54
+ files: FileContent[]
55
+ readOnly?: boolean
56
+ isStreaming?: boolean
57
+ showImportAndFormatButtons?: boolean
58
+ onFileContentChanged?: (path: string, content: string) => void
59
+ }) => {
60
+ const editorRef = useRef<HTMLDivElement>(null)
61
+ const viewRef = useRef<EditorView | null>(null)
62
+ const ataRef = useRef<ReturnType<typeof setupTypeAcquisition> | null>(null)
63
+ const apiUrl = useSnippetsBaseApiUrl()
64
+ const codeCompletionApi = useCodeCompletionApi()
65
+
66
+ const [cursorPosition, setCursorPosition] = useState<number | null>(null)
67
+ const [code, setCode] = useState(files[0]?.content || "")
68
+ const [currentFile, setCurrentFile] = useState<string>("")
69
+
70
+ // Get URL search params for file_path
71
+ const urlParams = new URLSearchParams(window.location.search)
72
+ const filePathFromUrl = urlParams.get("file_path")
73
+
74
+ // Set current file on component mount
75
+ useEffect(() => {
76
+ if (files.length > 0 && currentFile === "") {
77
+ // Priority 1: Use file_path from URL if it exists in files
78
+ if (
79
+ filePathFromUrl &&
80
+ files.some((file) => file.path === filePathFromUrl)
81
+ ) {
82
+ setCurrentFile(filePathFromUrl)
83
+ }
84
+ // Priority 2: Use index.tsx if it exists in files
85
+ else if (files.some((file) => file.path === "index.tsx")) {
86
+ setCurrentFile("index.tsx")
87
+ }
88
+ // Priority 3: Use the first file with .tsx extension
89
+ else {
90
+ const tsxFile = files.find((file) => file.path.endsWith(".tsx"))
91
+ if (tsxFile) {
92
+ setCurrentFile(tsxFile.path)
93
+ }
94
+ // Fallback: Use the first file in the array
95
+ else if (files[0]) {
96
+ setCurrentFile(files[0].path)
97
+ }
98
+ }
99
+ return
100
+ }
101
+ }, [files])
102
+
103
+ const fileMap = useMemo(() => {
104
+ const map: Record<string, string> = {}
105
+ files.forEach((file) => {
106
+ map[file.path] = file.content
107
+ })
108
+ return map
109
+ }, [files])
110
+
111
+ useEffect(() => {
112
+ const currentFileContent =
113
+ files.find((f) => f.path === currentFile)?.content || ""
114
+ if (currentFileContent !== code) {
115
+ setCode(currentFileContent)
116
+ updateCurrentEditorContent(currentFileContent)
117
+ }
118
+ }, [files])
119
+
120
+ // Whenever streaming completes, reset the code to the initial code
121
+ useEffect(() => {
122
+ if (!isStreaming) {
123
+ const currentFileContent =
124
+ files.find((f) => f.path === currentFile)?.content || ""
125
+ if (code !== currentFileContent && currentFileContent) {
126
+ setCode(currentFileContent)
127
+ setTimeout(() => {
128
+ updateCurrentEditorContent(currentFileContent)
129
+ }, 200)
130
+ }
131
+ }
132
+ }, [isStreaming])
133
+
134
+ useEffect(() => {
135
+ if (!editorRef.current) return
136
+
137
+ const fsMap = new Map<string, string>()
138
+ files.forEach(({ path, content }) => {
139
+ fsMap.set(path, content)
140
+ })
141
+ ;(window as any).__DEBUG_CODE_EDITOR_FS_MAP = fsMap
142
+
143
+ createDefaultMapFromCDN(
144
+ { target: ts.ScriptTarget.ES2022 },
145
+ "5.6.3",
146
+ true,
147
+ ts,
148
+ ).then((defaultFsMap) => {
149
+ defaultFsMap.forEach((content, filename) => {
150
+ fsMap.set(filename, content)
151
+ })
152
+ })
153
+
154
+ const system = createSystem(fsMap)
155
+ const env = createVirtualTypeScriptEnvironment(system, [], ts, {
156
+ jsx: ts.JsxEmit.ReactJSX,
157
+ declaration: true,
158
+ allowJs: true,
159
+ target: ts.ScriptTarget.ES2022,
160
+ resolveJsonModule: true,
161
+ })
162
+
163
+ // Initialize ATA
164
+ const ataConfig: ATABootstrapConfig = {
165
+ projectName: "my-project",
166
+ typescript: ts,
167
+ logger: console,
168
+ fetcher: async (input: RequestInfo | URL, init?: RequestInit) => {
169
+ const registryPrefixes = [
170
+ "https://data.jsdelivr.com/v1/package/resolve/npm/@tsci/",
171
+ "https://data.jsdelivr.com/v1/package/npm/@tsci/",
172
+ "https://cdn.jsdelivr.net/npm/@tsci/",
173
+ ]
174
+ if (
175
+ typeof input === "string" &&
176
+ registryPrefixes.some((prefix) => input.startsWith(prefix))
177
+ ) {
178
+ const fullPackageName = input
179
+ .replace(registryPrefixes[0], "")
180
+ .replace(registryPrefixes[1], "")
181
+ .replace(registryPrefixes[2], "")
182
+ const packageName = fullPackageName.split("/")[0].replace(/\./, "/")
183
+ const pathInPackage = fullPackageName.split("/").slice(1).join("/")
184
+ const jsdelivrPath = `${packageName}${
185
+ pathInPackage ? `/${pathInPackage}` : ""
186
+ }`
187
+ return fetch(
188
+ `${apiUrl}/snippets/download?jsdelivr_resolve=${input.includes(
189
+ "/resolve/",
190
+ )}&jsdelivr_path=${encodeURIComponent(jsdelivrPath)}`,
191
+ )
192
+ }
193
+ return fetch(input, init)
194
+ },
195
+ delegate: {
196
+ started: () => {
197
+ const manualEditsTypeDeclaration = `
198
+ declare module "*.json" {
199
+ const value: {
200
+ pcb_placements?: any[],
201
+ schematic_placements?: any[],
202
+ edit_events?: any[],
203
+ manual_trace_hints?: any[],
204
+ } | undefined;
205
+ export default value;
206
+ }
207
+ `
208
+ env.createFile("manual-edits.d.ts", manualEditsTypeDeclaration)
209
+ },
210
+ receivedFile: (code: string, path: string) => {
211
+ fsMap.set(path, code)
212
+ env.createFile(path, code)
213
+ if (viewRef.current) {
214
+ viewRef.current.dispatch({
215
+ changes: {
216
+ from: 0,
217
+ to: viewRef.current.state.doc.length,
218
+ insert: viewRef.current.state.doc.toString(),
219
+ },
220
+ selection: viewRef.current.state.selection,
221
+ })
222
+ }
223
+ },
224
+ },
225
+ }
226
+
227
+ const ata = setupTypeAcquisition(ataConfig)
228
+ ataRef.current = ata
229
+
230
+ const lastFilesEventContent: Record<string, string> = {}
231
+
232
+ // Set up base extensions
233
+ const baseExtensions = [
234
+ basicSetup,
235
+ currentFile.endsWith(".json")
236
+ ? json()
237
+ : javascript({ typescript: true, jsx: true }),
238
+ keymap.of([indentWithTab]),
239
+ EditorState.readOnly.of(readOnly),
240
+ EditorView.updateListener.of((update) => {
241
+ if (update.docChanged) {
242
+ const newContent = update.state.doc.toString()
243
+
244
+ if (newContent === lastFilesEventContent[currentFile]) return
245
+ lastFilesEventContent[currentFile] = newContent
246
+
247
+ // setCode(newContent)
248
+ onCodeChange(newContent, currentFile)
249
+ onFileContentChanged?.(currentFile, newContent)
250
+
251
+ // Generate TypeScript declarations for TypeScript/TSX files
252
+ if (currentFile.endsWith(".ts") || currentFile.endsWith(".tsx")) {
253
+ const { outputFiles } = env.languageService.getEmitOutput(
254
+ currentFile,
255
+ true,
256
+ )
257
+ const dtsFile = outputFiles.find((file) =>
258
+ file.name.endsWith(".d.ts"),
259
+ )
260
+ if (dtsFile?.text && onDtsChange) {
261
+ onDtsChange(dtsFile.text)
262
+ }
263
+ }
264
+ }
265
+ if (update.selectionSet) {
266
+ const pos = update.state.selection.main.head
267
+ setCursorPosition(pos)
268
+ }
269
+ }),
270
+ ]
271
+ if (codeCompletionApi?.apiKey) {
272
+ baseExtensions.push(
273
+ // copilotPlugin({
274
+ // apiKey: codeCompletionApi.apiKey,
275
+ // language: Language.TYPESCRIPT,
276
+ // }),
277
+ EditorView.theme({
278
+ ".cm-ghostText, .cm-ghostText *": {
279
+ opacity: "0.6",
280
+ filter: "grayscale(20%)",
281
+ cursor: "pointer",
282
+ },
283
+ ".cm-ghostText:hover": {
284
+ background: "#eee",
285
+ },
286
+ }),
287
+ )
288
+ }
289
+
290
+ // Add TypeScript-specific extensions and handlers
291
+ const tsExtensions =
292
+ currentFile.endsWith(".tsx") || currentFile.endsWith(".ts")
293
+ ? [
294
+ tsFacet.of({ env, path: currentFile }),
295
+ tsSync(),
296
+ tsLinter(),
297
+ autocompletion({ override: [tsAutocomplete()] }),
298
+ tsHover(),
299
+ hoverTooltip((view, pos) => {
300
+ const line = view.state.doc.lineAt(pos)
301
+ const lineStart = line.from
302
+ const lineEnd = line.to
303
+ const lineText = view.state.sliceDoc(lineStart, lineEnd)
304
+ const matches = Array.from(
305
+ lineText.matchAll(TSCI_PACKAGE_PATTERN),
306
+ )
307
+
308
+ for (const match of matches) {
309
+ if (match.index !== undefined) {
310
+ const start = lineStart + match.index
311
+ const end = start + match[0].length
312
+ if (pos >= start && pos <= end) {
313
+ return {
314
+ pos: start,
315
+ end: end,
316
+ above: true,
317
+ create() {
318
+ const dom = document.createElement("div")
319
+ dom.textContent = "Ctrl/Cmd+Click to open snippet"
320
+ return { dom }
321
+ },
322
+ }
323
+ }
324
+ }
325
+ }
326
+ return null
327
+ }),
328
+ EditorView.domEventHandlers({
329
+ click: (event, view) => {
330
+ if (!event.ctrlKey && !event.metaKey) return false
331
+ const pos = view.posAtCoords({
332
+ x: event.clientX,
333
+ y: event.clientY,
334
+ })
335
+ if (pos === null) return false
336
+
337
+ const line = view.state.doc.lineAt(pos)
338
+ const lineStart = line.from
339
+ const lineEnd = line.to
340
+ const lineText = view.state.sliceDoc(lineStart, lineEnd)
341
+ const matches = Array.from(
342
+ lineText.matchAll(TSCI_PACKAGE_PATTERN),
343
+ )
344
+ for (const match of matches) {
345
+ if (match.index !== undefined) {
346
+ const start = lineStart + match.index
347
+ const end = start + match[0].length
348
+ if (pos >= start && pos <= end) {
349
+ const importName = match[0]
350
+ // Handle potential dots and dashes in package names
351
+ const [owner, name] = importName
352
+ .replace("@tsci/", "")
353
+ .split(".")
354
+ window.open(`/${owner}/${name}`, "_blank")
355
+ return true
356
+ }
357
+ }
358
+ }
359
+ return false
360
+ },
361
+ }),
362
+ EditorView.theme({
363
+ ".cm-content .cm-underline": {
364
+ textDecoration: "underline",
365
+ textDecorationColor: "rgba(0, 0, 255, 0.3)",
366
+ cursor: "pointer",
367
+ },
368
+ }),
369
+ EditorView.decorations.of((view) => {
370
+ const decorations = []
371
+ for (const { from, to } of view.visibleRanges) {
372
+ for (let pos = from; pos < to; ) {
373
+ const line = view.state.doc.lineAt(pos)
374
+ const lineText = line.text
375
+ const matches = lineText.matchAll(TSCI_PACKAGE_PATTERN)
376
+ for (const match of matches) {
377
+ if (match.index !== undefined) {
378
+ const start = line.from + match.index
379
+ const end = start + match[0].length
380
+ decorations.push(
381
+ Decoration.mark({
382
+ class: "cm-underline",
383
+ }).range(start, end),
384
+ )
385
+ }
386
+ }
387
+ pos = line.to + 1
388
+ }
389
+ }
390
+ return Decoration.set(decorations)
391
+ }),
392
+ ]
393
+ : []
394
+
395
+ const state = EditorState.create({
396
+ doc: fileMap[currentFile] || "",
397
+ extensions: [...baseExtensions, ...tsExtensions],
398
+ })
399
+
400
+ const view = new EditorView({
401
+ state,
402
+ parent: editorRef.current,
403
+ })
404
+
405
+ viewRef.current = view
406
+
407
+ // Initial ATA run for index.tsx
408
+ if (currentFile === "index.tsx") {
409
+ ata(`${defaultImports}${code}`)
410
+ }
411
+ // files.forEach(({path, content}) => {
412
+ // if (path.endsWith(".tsx") || path.endsWith(".ts")) {
413
+ // ata(`${defaultImports}${content}`)
414
+ // }
415
+ // })
416
+
417
+ return () => {
418
+ view.destroy()
419
+ }
420
+ }, [!isStreaming, currentFile, code !== ""])
421
+
422
+ const updateCurrentEditorContent = (newContent: string) => {
423
+ if (viewRef.current) {
424
+ const state = viewRef.current.state
425
+ if (state.doc.toString() !== newContent) {
426
+ viewRef.current.dispatch({
427
+ changes: { from: 0, to: state.doc.length, insert: newContent },
428
+ })
429
+ }
430
+ }
431
+ }
432
+
433
+ const updateEditorToMatchCurrentFile = () => {
434
+ const currentContent = fileMap[currentFile] || ""
435
+ updateCurrentEditorContent(currentContent)
436
+ }
437
+
438
+ const codeImports = getImportsFromCode(code)
439
+
440
+ useEffect(() => {
441
+ if (
442
+ ataRef.current &&
443
+ (currentFile.endsWith(".tsx") || currentFile.endsWith(".ts"))
444
+ ) {
445
+ ataRef.current(`${defaultImports}${code}`)
446
+ }
447
+ }, [codeImports])
448
+
449
+ const handleFileChange = (path: string) => {
450
+ setCurrentFile(path)
451
+ }
452
+
453
+ const updateFileContent = (path: string, newContent: string) => {
454
+ if (currentFile === path) {
455
+ setCode(newContent)
456
+ onCodeChange(newContent, path)
457
+ }
458
+ onFileContentChanged?.(path, newContent)
459
+
460
+ if (viewRef.current && currentFile === path) {
461
+ viewRef.current.dispatch({
462
+ changes: {
463
+ from: 0,
464
+ to: viewRef.current.state.doc.length,
465
+ insert: newContent,
466
+ },
467
+ })
468
+ }
469
+ }
470
+
471
+ // Whenever the current file changes, updated the editor content
472
+ useEffect(() => {
473
+ updateEditorToMatchCurrentFile()
474
+ }, [currentFile])
475
+
476
+ if (isStreaming) {
477
+ return <div className="font-mono whitespace-pre-wrap text-xs">{code}</div>
478
+ }
479
+ const [sidebarOpen, setSidebarOpen] = useState(false)
480
+ return (
481
+ <div className="flex h-full">
482
+ <FileSidebar
483
+ files={Object.fromEntries(files.map((f) => [f.path, f.content]))}
484
+ currentFile={currentFile}
485
+ fileSidebarState={
486
+ [sidebarOpen, setSidebarOpen] as ReturnType<typeof useState<boolean>>
487
+ }
488
+ onFileSelect={handleFileChange}
489
+ />
490
+ <div className="flex flex-col flex-1">
491
+ {showImportAndFormatButtons && (
492
+ <CodeEditorHeader
493
+ fileSidebarState={
494
+ [sidebarOpen, setSidebarOpen] as ReturnType<
495
+ typeof useState<boolean>
496
+ >
497
+ }
498
+ currentFile={currentFile}
499
+ files={Object.fromEntries(files.map((f) => [f.path, f.content]))}
500
+ updateFileContent={(...args) => {
501
+ return updateFileContent(...args)
502
+ }}
503
+ cursorPosition={cursorPosition}
504
+ />
505
+ )}
506
+ <div ref={editorRef} className="flex-1 overflow-auto max-w-[100%]" />
507
+ </div>
508
+ </div>
509
+ )
510
+ }
@@ -0,0 +1,153 @@
1
+ import React, { useState, useCallback } from "react"
2
+ import { Button } from "@/components/ui/button"
3
+ import { handleManualEditsImport } from "@/lib/handleManualEditsImport"
4
+ import { useImportSnippetDialog } from "@/components/dialogs/import-snippet-dialog"
5
+ import { useToast } from "@/hooks/use-toast"
6
+ import { FootprintDialog } from "@/components/FootprintDialog"
7
+ import {
8
+ DropdownMenu,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuTrigger,
12
+ } from "@/components/ui/dropdown-menu"
13
+ import { AlertTriangle, PanelRightClose } from "lucide-react"
14
+ import { checkIfManualEditsImported } from "@/lib/utils/checkIfManualEditsImported"
15
+
16
+ export type FileName = string
17
+
18
+ interface CodeEditorHeaderProps {
19
+ currentFile: FileName
20
+ files: Record<FileName, string>
21
+ updateFileContent: (filename: FileName, content: string) => void
22
+ cursorPosition: number | null
23
+ fileSidebarState: ReturnType<typeof useState<boolean>>
24
+ }
25
+
26
+ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
27
+ currentFile,
28
+ files,
29
+ updateFileContent,
30
+ cursorPosition,
31
+ fileSidebarState,
32
+ }) => {
33
+ const { Dialog: ImportSnippetDialog, openDialog: openImportDialog } =
34
+ useImportSnippetDialog()
35
+ const [footprintDialogOpen, setFootprintDialogOpen] = useState(false)
36
+ const { toast } = useToast()
37
+ const [sidebarOpen, setSidebarOpen] = fileSidebarState
38
+ const handleFormatFile = useCallback(() => {
39
+ if (!window.prettier || !window.prettierPlugins) return
40
+
41
+ try {
42
+ const currentContent = files[currentFile]
43
+
44
+ if (currentFile.endsWith(".json")) {
45
+ try {
46
+ const jsonObj = JSON.parse(currentContent)
47
+ const formattedJson = JSON.stringify(jsonObj, null, 2)
48
+ updateFileContent(currentFile, formattedJson)
49
+ } catch (jsonError) {
50
+ toast({
51
+ title: "Invalid JSON",
52
+ description: "Failed to format JSON: invalid syntax.",
53
+ variant: "destructive",
54
+ })
55
+ return
56
+ }
57
+ return
58
+ }
59
+
60
+ const formattedCode = window.prettier.format(currentContent, {
61
+ semi: false,
62
+ parser: "typescript",
63
+ plugins: window.prettierPlugins,
64
+ })
65
+
66
+ updateFileContent(currentFile, formattedCode)
67
+ } catch (error) {
68
+ console.error("Formatting error:", error)
69
+ toast({
70
+ title: "Formatting error",
71
+ description:
72
+ error instanceof Error
73
+ ? error.message
74
+ : "Failed to format the code. Please check for syntax errors.",
75
+ variant: "destructive",
76
+ })
77
+ }
78
+ }, [currentFile, files, toast, updateFileContent])
79
+
80
+ return (
81
+ <>
82
+ <div className="flex items-center gap-2 px-2 border-b border-gray-200">
83
+ <button
84
+ className={`text-black/60 scale-90 transition-opacity duration-200 ${sidebarOpen ? "opacity-0 pointer-events-none" : "opacity-100"}`}
85
+ onClick={() => setSidebarOpen(true)}
86
+ >
87
+ <PanelRightClose />
88
+ </button>
89
+
90
+ <div className="flex items-center gap-2 px-2 py-1 ml-auto">
91
+ {checkIfManualEditsImported(files) && (
92
+ <DropdownMenu>
93
+ <DropdownMenuTrigger asChild>
94
+ <Button
95
+ size="sm"
96
+ variant="ghost"
97
+ className="text-red-500 hover:bg-red-50"
98
+ >
99
+ <AlertTriangle className="mr-2 h-4 w-4" />
100
+ Error
101
+ </Button>
102
+ </DropdownMenuTrigger>
103
+ <DropdownMenuContent>
104
+ <DropdownMenuItem
105
+ className="text-red-600 cursor-pointer"
106
+ onClick={() =>
107
+ handleManualEditsImport(files, updateFileContent, toast)
108
+ }
109
+ >
110
+ Manual edits exist but have not been imported. (Click to fix)
111
+ </DropdownMenuItem>
112
+ </DropdownMenuContent>
113
+ </DropdownMenu>
114
+ )}
115
+ <DropdownMenu>
116
+ <DropdownMenuTrigger asChild>
117
+ <Button size="sm" variant="ghost">
118
+ Insert
119
+ </Button>
120
+ </DropdownMenuTrigger>
121
+ <DropdownMenuContent>
122
+ <DropdownMenuItem onClick={() => setFootprintDialogOpen(true)}>
123
+ Chip
124
+ </DropdownMenuItem>
125
+ </DropdownMenuContent>
126
+ </DropdownMenu>
127
+ <Button size="sm" variant="ghost" onClick={() => openImportDialog()}>
128
+ Import
129
+ </Button>
130
+ <Button size="sm" variant="ghost" onClick={handleFormatFile}>
131
+ Format
132
+ </Button>
133
+ </div>
134
+ <ImportSnippetDialog
135
+ onSnippetSelected={(snippet: any) => {
136
+ const newContent = `import {} from "@tsci/${snippet.owner_name}.${snippet.unscoped_name}"\n${files[currentFile]}`
137
+ updateFileContent(currentFile, newContent)
138
+ }}
139
+ />
140
+ <FootprintDialog
141
+ currentFile={currentFile as `${string}.${string}`}
142
+ open={footprintDialogOpen}
143
+ onOpenChange={setFootprintDialogOpen}
144
+ updateFileContent={updateFileContent}
145
+ files={files}
146
+ cursorPosition={cursorPosition}
147
+ />
148
+ </div>
149
+ </>
150
+ )
151
+ }
152
+
153
+ export default CodeEditorHeader