@tscircuit/fake-snippets 0.0.98 → 0.0.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/bun.lock +304 -947
  2. package/dist/bundle.js +11 -5
  3. package/fake-snippets-api/routes/api/autocomplete/create_autocomplete.ts +0 -1
  4. package/fake-snippets-api/routes/api/packages/create.ts +14 -3
  5. package/package.json +7 -4
  6. package/src/App.tsx +58 -2
  7. package/src/components/CircuitJsonImportDialog.tsx +10 -5
  8. package/src/components/DownloadButtonAndMenu.tsx +13 -0
  9. package/src/components/FileSidebar.tsx +83 -10
  10. package/src/components/PackageBuildsPage/LogContent.tsx +19 -7
  11. package/src/components/ViewPackagePage/components/important-files-view.tsx +294 -167
  12. package/src/components/ViewPackagePage/components/main-content-header.tsx +2 -2
  13. package/src/components/ViewPackagePage/components/repo-page-content.tsx +9 -0
  14. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +10 -3
  15. package/src/components/ViewPackagePage/components/sidebar.tsx +3 -1
  16. package/src/components/dialogs/edit-package-details-dialog.tsx +3 -2
  17. package/src/components/package-port/CodeAndPreview.tsx +6 -1
  18. package/src/components/package-port/CodeEditor.tsx +21 -3
  19. package/src/components/package-port/CodeEditorHeader.tsx +12 -7
  20. package/src/components/ui/tree-view.tsx +51 -2
  21. package/src/hooks/use-create-package-mutation.ts +1 -1
  22. package/src/hooks/useFileManagement.ts +71 -6
  23. package/src/lib/download-fns/download-spice-file.ts +13 -0
  24. package/src/lib/utils/package-utils.ts +0 -3
  25. package/src/pages/dashboard.tsx +1 -1
  26. package/src/pages/datasheet.tsx +157 -67
  27. package/src/pages/datasheets.tsx +2 -2
  28. package/src/pages/latest.tsx +2 -2
  29. package/src/pages/search.tsx +1 -1
  30. package/src/pages/trending.tsx +2 -2
  31. package/vite.config.ts +1 -0
@@ -1,6 +1,6 @@
1
1
  "use client"
2
2
 
3
- import { useState, useEffect } from "react"
3
+ import { useState, useEffect, useMemo, useCallback } from "react"
4
4
  import {
5
5
  Edit,
6
6
  FileText,
@@ -9,12 +9,12 @@ import {
9
9
  CopyCheck,
10
10
  Loader2,
11
11
  RefreshCcwIcon,
12
+ SparklesIcon,
12
13
  } from "lucide-react"
13
14
  import { Skeleton } from "@/components/ui/skeleton"
14
15
  import { Button } from "@/components/ui/button"
15
- import { usePackageFile, usePackageFileByPath } from "@/hooks/use-package-files"
16
+ import { usePackageFile } from "@/hooks/use-package-files"
16
17
  import { ShikiCodeViewer } from "./ShikiCodeViewer"
17
- import { SparklesIcon } from "lucide-react"
18
18
  import MarkdownViewer from "./markdown-viewer"
19
19
  import { useGlobalStore } from "@/hooks/use-global-store"
20
20
 
@@ -36,6 +36,16 @@ interface ImportantFilesViewProps {
36
36
  aiReviewText?: string | null
37
37
  aiReviewRequested?: boolean
38
38
  onRequestAiReview?: () => void
39
+ onLicenseFileRequested?: boolean
40
+ }
41
+
42
+ type TabType = "ai" | "ai-review" | "file"
43
+
44
+ interface TabInfo {
45
+ type: TabType
46
+ filePath?: string | null
47
+ label: string
48
+ icon: React.ReactNode
39
49
  }
40
50
 
41
51
  export default function ImportantFilesView({
@@ -48,89 +58,235 @@ export default function ImportantFilesView({
48
58
  isLoading = false,
49
59
  onEditClicked,
50
60
  packageAuthorOwner,
61
+ onLicenseFileRequested,
51
62
  }: ImportantFilesViewProps) {
52
- const [activeFilePath, setActiveFilePath] = useState<string | null>(null)
53
- const [activeTab, setActiveTab] = useState<string | null>(null)
63
+ const [activeTab, setActiveTab] = useState<TabInfo | null>(null)
54
64
  const [copyState, setCopyState] = useState<"copy" | "copied">("copy")
55
65
  const { session: user } = useGlobalStore()
56
66
 
57
- const handleCopy = () => {
58
- if (activeTab === "ai-review" && aiReviewText) {
59
- navigator.clipboard.writeText(aiReviewText || "")
67
+ // Memoized computed values
68
+ const hasAiContent = useMemo(
69
+ () => Boolean(aiDescription || aiUsageInstructions),
70
+ [aiDescription, aiUsageInstructions],
71
+ )
72
+ const hasAiReview = useMemo(() => Boolean(aiReviewText), [aiReviewText])
73
+ const isOwner = useMemo(
74
+ () => user?.github_username === packageAuthorOwner,
75
+ [user?.github_username, packageAuthorOwner],
76
+ )
77
+
78
+ // File type utilities
79
+ const isLicenseFile = useCallback((filePath: string) => {
80
+ const lowerPath = filePath.toLowerCase()
81
+ return (
82
+ lowerPath === "license" ||
83
+ lowerPath.endsWith("/license") ||
84
+ lowerPath === "license.txt" ||
85
+ lowerPath.endsWith("/license.txt") ||
86
+ lowerPath === "license.md" ||
87
+ lowerPath.endsWith("/license.md")
88
+ )
89
+ }, [])
90
+
91
+ const isReadmeFile = useCallback((filePath: string) => {
92
+ const lowerPath = filePath.toLowerCase()
93
+ return lowerPath.endsWith("readme.md") || lowerPath.endsWith("readme")
94
+ }, [])
95
+
96
+ const isCodeFile = useCallback((filePath: string) => {
97
+ return (
98
+ filePath.endsWith(".js") ||
99
+ filePath.endsWith(".jsx") ||
100
+ filePath.endsWith(".ts") ||
101
+ filePath.endsWith(".tsx")
102
+ )
103
+ }, [])
104
+
105
+ const isMarkdownFile = useCallback(
106
+ (filePath: string) => {
107
+ return filePath.endsWith(".md") || isReadmeFile(filePath)
108
+ },
109
+ [isReadmeFile],
110
+ )
111
+
112
+ const getFileName = useCallback((path: string) => {
113
+ const parts = path.split("/")
114
+ return parts[parts.length - 1]
115
+ }, [])
116
+
117
+ const getFileIcon = useCallback(
118
+ (path: string) => {
119
+ return isCodeFile(path) ? (
120
+ <Code className="h-3.5 w-3.5 mr-1.5" />
121
+ ) : (
122
+ <FileText className="h-3.5 w-3.5 mr-1.5" />
123
+ )
124
+ },
125
+ [isCodeFile],
126
+ )
127
+
128
+ // Available tabs computation
129
+ const availableTabs = useMemo((): TabInfo[] => {
130
+ const tabs: TabInfo[] = []
131
+
132
+ if (hasAiContent) {
133
+ tabs.push({
134
+ type: "ai",
135
+ filePath: null,
136
+ label: "Description",
137
+ icon: <SparklesIcon className="h-3.5 w-3.5 mr-1.5" />,
138
+ })
139
+ }
140
+
141
+ tabs.push({
142
+ type: "ai-review",
143
+ filePath: null,
144
+ label: "AI Review",
145
+ icon: <SparklesIcon className="h-3.5 w-3.5 mr-1.5" />,
146
+ })
147
+
148
+ importantFiles.forEach((file) => {
149
+ tabs.push({
150
+ type: "file",
151
+ filePath: file.file_path,
152
+ label: getFileName(file.file_path),
153
+ icon: getFileIcon(file.file_path),
154
+ })
155
+ })
156
+
157
+ return tabs
158
+ }, [hasAiContent, importantFiles, getFileName, getFileIcon])
159
+
160
+ // Find default tab with fallback logic
161
+ const getDefaultTab = useCallback((): TabInfo | null => {
162
+ if (isLoading || availableTabs.length === 0) return null
163
+
164
+ // Priority 1: README file
165
+ const readmeTab = availableTabs.find(
166
+ (tab) =>
167
+ tab.type === "file" && tab.filePath && isReadmeFile(tab.filePath),
168
+ )
169
+ if (readmeTab) return readmeTab
170
+
171
+ // Priority 2: AI content
172
+ const aiTab = availableTabs.find((tab) => tab.type === "ai")
173
+ if (aiTab) return aiTab
174
+
175
+ // Priority 3: AI review
176
+ const aiReviewTab = availableTabs.find((tab) => tab.type === "ai-review")
177
+ if (aiReviewTab) return aiReviewTab
178
+
179
+ // Priority 4: First file
180
+ const firstFileTab = availableTabs.find((tab) => tab.type === "file")
181
+ if (firstFileTab) return firstFileTab
182
+
183
+ return null
184
+ }, [isLoading, availableTabs, isReadmeFile])
185
+
186
+ // Handle copy functionality
187
+ const handleCopy = useCallback(() => {
188
+ let textToCopy = ""
189
+
190
+ if (activeTab?.type === "ai-review" && aiReviewText) {
191
+ textToCopy = aiReviewText
192
+ } else if (activeTab?.type === "file" && activeFileContent) {
193
+ textToCopy = activeFileContent
194
+ }
195
+
196
+ if (textToCopy) {
197
+ navigator.clipboard.writeText(textToCopy)
60
198
  setCopyState("copied")
61
199
  setTimeout(() => setCopyState("copy"), 500)
62
- return
63
200
  }
64
- navigator.clipboard.writeText(activeFileContent)
65
- setCopyState("copied")
66
- setTimeout(() => setCopyState("copy"), 500)
67
- }
68
- // Determine if we have AI content
69
- const hasAiContent = Boolean(aiDescription || aiUsageInstructions)
70
- const hasAiReview = Boolean(aiReviewText)
201
+ }, [activeTab, aiReviewText])
202
+
203
+ // Handle tab selection with validation
204
+ const selectTab = useCallback(
205
+ (tab: TabInfo) => {
206
+ // Validate that the tab still exists (for file tabs)
207
+ if (tab.type === "file" && tab.filePath) {
208
+ const fileExists = importantFiles.some(
209
+ (file) => file.file_path === tab.filePath,
210
+ )
211
+ if (!fileExists) {
212
+ // File was deleted, fallback to default tab
213
+ const defaultTab = getDefaultTab()
214
+ setActiveTab(defaultTab)
215
+ return
216
+ }
217
+ }
218
+ setActiveTab(tab)
219
+ },
220
+ [importantFiles, getDefaultTab],
221
+ )
71
222
 
72
- // Select the appropriate tab/file when content changes. Once the user has
73
- // interacted with the tabs we keep their selection and only run this logic
74
- // if no tab has been chosen yet.
223
+ // Handle license file request
75
224
  useEffect(() => {
76
- if (activeTab !== null) return
77
- if (isLoading) return
78
-
79
- // First priority: README file if it exists
80
- const readmeFile = importantFiles.find(
81
- (file) =>
82
- file.file_path.toLowerCase().endsWith("readme.md") ||
83
- file.file_path.toLowerCase().endsWith("readme"),
84
- )
225
+ if (onLicenseFileRequested && importantFiles.length > 0) {
226
+ const licenseTab = availableTabs.find(
227
+ (tab) =>
228
+ tab.type === "file" && tab.filePath && isLicenseFile(tab.filePath),
229
+ )
85
230
 
86
- if (readmeFile) {
87
- setActiveFilePath(readmeFile.file_path)
88
- setActiveTab("file")
89
- } else if (hasAiContent) {
90
- // Second priority: AI content if available
91
- setActiveTab("ai")
92
- setActiveFilePath(null)
93
- } else if (hasAiReview) {
94
- setActiveTab("ai-review")
95
- setActiveFilePath(null)
96
- } else if (importantFiles.length > 0) {
97
- // Third priority: First important file
98
- setActiveFilePath(importantFiles[0].file_path)
99
- setActiveTab("file")
231
+ if (licenseTab) {
232
+ setActiveTab(licenseTab)
233
+ } else {
234
+ // License file not found, fallback to default
235
+ const defaultTab = getDefaultTab()
236
+ setActiveTab(defaultTab)
237
+ }
100
238
  }
101
239
  }, [
102
- aiDescription,
103
- aiUsageInstructions,
104
- aiReviewText,
105
- hasAiContent,
106
- hasAiReview,
240
+ onLicenseFileRequested,
107
241
  importantFiles,
108
- activeTab,
109
- isLoading,
242
+ availableTabs,
243
+ isLicenseFile,
244
+ getDefaultTab,
110
245
  ])
111
246
 
112
- // Get file name from path
113
- const getFileName = (path: string) => {
114
- const parts = path.split("/")
115
- return parts[parts.length - 1]
116
- }
247
+ // Set default tab when no tab is active
248
+ useEffect(() => {
249
+ if (activeTab === null && !isLoading) {
250
+ const defaultTab = getDefaultTab()
251
+ setActiveTab(defaultTab)
252
+ }
253
+ }, [activeTab, isLoading, getDefaultTab])
117
254
 
118
- // Get file icon based on extension
119
- const getFileIcon = (path: string) => {
120
- if (
121
- path.endsWith(".js") ||
122
- path.endsWith(".jsx") ||
123
- path.endsWith(".ts") ||
124
- path.endsWith(".tsx")
125
- ) {
126
- return <Code className="h-3.5 w-3.5 mr-1.5" />
255
+ // Validate active tab still exists (handles file deletion)
256
+ useEffect(() => {
257
+ if (activeTab?.type === "file" && activeTab.filePath) {
258
+ const fileExists = importantFiles.some(
259
+ (file) => file.file_path === activeTab.filePath,
260
+ )
261
+ if (!fileExists) {
262
+ // Active file was deleted, fallback to default
263
+ const defaultTab = getDefaultTab()
264
+ setActiveTab(defaultTab)
265
+ }
127
266
  }
128
- return <FileText className="h-3.5 w-3.5 mr-1.5" />
129
- }
267
+ }, [activeTab, importantFiles, getDefaultTab])
130
268
 
131
- // Render AI content
132
- const renderAiContent = () => {
133
- return (
269
+ // Get active file content
270
+ const partialActiveFile = useMemo(() => {
271
+ if (activeTab?.type !== "file" || !activeTab.filePath) return null
272
+ return importantFiles.find((file) => file.file_path === activeTab.filePath)
273
+ }, [activeTab, importantFiles])
274
+
275
+ const { data: activeFileFull } = usePackageFile(
276
+ partialActiveFile
277
+ ? {
278
+ file_path: partialActiveFile.file_path,
279
+ package_release_id: partialActiveFile.package_release_id,
280
+ }
281
+ : null,
282
+ { keepPreviousData: true },
283
+ )
284
+
285
+ const activeFileContent = activeFileFull?.content_text || ""
286
+
287
+ // Render content based on active tab
288
+ const renderAiContent = useCallback(
289
+ () => (
134
290
  <div className="markdown-content">
135
291
  {aiDescription && (
136
292
  <div className="mb-6">
@@ -145,12 +301,11 @@ export default function ImportantFilesView({
145
301
  </div>
146
302
  )}
147
303
  </div>
148
- )
149
- }
150
-
151
- const renderAiReviewContent = () => {
152
- const isOwner = user?.github_username === packageAuthorOwner
304
+ ),
305
+ [aiDescription, aiUsageInstructions],
306
+ )
153
307
 
308
+ const renderAiReviewContent = useCallback(() => {
154
309
  if (!aiReviewText && !aiReviewRequested) {
155
310
  return (
156
311
  <div className="flex flex-col items-center justify-center py-8 px-4">
@@ -206,22 +361,61 @@ export default function ImportantFilesView({
206
361
  }
207
362
 
208
363
  return <MarkdownViewer markdownContent={aiReviewText || ""} />
209
- }
364
+ }, [aiReviewText, aiReviewRequested, isOwner, onRequestAiReview])
210
365
 
211
- // Get active file content
212
- const partialActiveFile = importantFiles.find(
213
- (file) => file.file_path === activeFilePath,
214
- )
215
- const { data: activeFileFull } = usePackageFile(
216
- partialActiveFile
217
- ? {
218
- file_path: partialActiveFile.file_path,
219
- package_release_id: partialActiveFile.package_release_id,
220
- }
221
- : null,
222
- { keepPreviousData: true },
366
+ const renderFileContent = useCallback(() => {
367
+ if (!activeTab?.filePath || !activeFileContent) {
368
+ return <pre className="whitespace-pre-wrap">{activeFileContent}</pre>
369
+ }
370
+
371
+ if (isMarkdownFile(activeTab.filePath)) {
372
+ return <MarkdownViewer markdownContent={activeFileContent} />
373
+ }
374
+
375
+ if (isCodeFile(activeTab.filePath)) {
376
+ return (
377
+ <div className="overflow-x-auto">
378
+ <ShikiCodeViewer
379
+ code={activeFileContent}
380
+ filePath={activeTab.filePath}
381
+ />
382
+ </div>
383
+ )
384
+ }
385
+
386
+ return <pre className="whitespace-pre-wrap">{activeFileContent}</pre>
387
+ }, [activeTab, activeFileContent, isMarkdownFile, isCodeFile])
388
+
389
+ const renderTabContent = useCallback(() => {
390
+ if (!activeTab) return null
391
+
392
+ switch (activeTab.type) {
393
+ case "ai":
394
+ return renderAiContent()
395
+ case "ai-review":
396
+ return renderAiReviewContent()
397
+ case "file":
398
+ return renderFileContent()
399
+ default:
400
+ return null
401
+ }
402
+ }, [activeTab, renderAiContent, renderAiReviewContent, renderFileContent])
403
+
404
+ // Tab styling helper
405
+ const getTabClassName = useCallback(
406
+ (tab: TabInfo) => {
407
+ const isActive =
408
+ activeTab?.type === tab.type &&
409
+ (tab.type !== "file" || activeTab?.filePath === tab.filePath)
410
+
411
+ return `flex items-center px-3 py-1.5 rounded-md text-xs flex-shrink-0 whitespace-nowrap ${
412
+ isActive
413
+ ? "bg-gray-200 dark:bg-[#30363d] font-medium"
414
+ : "text-gray-500 dark:text-[#8b949e] hover:bg-gray-200 dark:hover:bg-[#30363d]"
415
+ }`
416
+ },
417
+ [activeTab],
223
418
  )
224
- const activeFileContent = activeFileFull?.content_text || ""
225
419
 
226
420
  if (isLoading) {
227
421
  return (
@@ -249,7 +443,7 @@ export default function ImportantFilesView({
249
443
  )
250
444
  }
251
445
 
252
- if (importantFiles.length === 0) {
446
+ if (importantFiles.length === 0 && !hasAiContent && !hasAiReview) {
253
447
  return (
254
448
  <div className="mt-4 border border-gray-200 dark:border-[#30363d] rounded-md overflow-hidden">
255
449
  <div className="flex items-center pl-2 pr-4 py-2 bg-gray-100 dark:bg-[#161b22] border-b border-gray-200 dark:border-[#30363d]">
@@ -268,68 +462,24 @@ export default function ImportantFilesView({
268
462
  )
269
463
  }
270
464
 
271
- const isOwner = user?.github_username === packageAuthorOwner
272
-
273
465
  return (
274
466
  <div className="mt-4 border border-gray-200 dark:border-[#30363d] rounded-md overflow-hidden">
275
467
  <div className="flex items-center pl-2 pr-4 py-2 bg-gray-100 dark:bg-[#161b22] border-b border-gray-200 dark:border-[#30363d]">
276
468
  <div className="flex items-center space-x-2 overflow-x-auto no-scrollbar flex-1 min-w-0">
277
- {/* AI Description Tab */}
278
- {hasAiContent && (
469
+ {availableTabs.map((tab, index) => (
279
470
  <button
280
- className={`flex items-center px-3 py-1.5 rounded-md text-xs flex-shrink-0 whitespace-nowrap ${
281
- activeTab === "ai"
282
- ? "bg-gray-200 dark:bg-[#30363d] font-medium"
283
- : "text-gray-500 dark:text-[#8b949e] hover:bg-gray-200 dark:hover:bg-[#30363d]"
284
- }`}
285
- onClick={() => {
286
- setActiveTab("ai")
287
- setActiveFilePath(null)
288
- }}
471
+ key={`${tab.type}-${tab.filePath || index}`}
472
+ className={getTabClassName(tab)}
473
+ onClick={() => selectTab(tab)}
289
474
  >
290
- <SparklesIcon className="h-3.5 w-3.5 mr-1.5" />
291
- <span>Description</span>
292
- </button>
293
- )}
294
-
295
- {/* AI Review Tab */}
296
- <button
297
- className={`flex items-center px-3 py-1.5 rounded-md text-xs flex-shrink-0 whitespace-nowrap ${
298
- activeTab === "ai-review"
299
- ? "bg-gray-200 dark:bg-[#30363d] font-medium"
300
- : "text-gray-500 dark:text-[#8b949e] hover:bg-gray-200 dark:hover:bg-[#30363d]"
301
- }`}
302
- onClick={() => {
303
- setActiveTab("ai-review")
304
- setActiveFilePath(null)
305
- }}
306
- >
307
- <SparklesIcon className="h-3.5 w-3.5 mr-1.5" />
308
- <span>AI Review</span>
309
- </button>
310
-
311
- {/* File Tabs */}
312
- {importantFiles.map((file) => (
313
- <button
314
- key={file.package_file_id}
315
- className={`flex items-center px-3 py-1.5 rounded-md text-xs flex-shrink-0 whitespace-nowrap ${
316
- activeTab === "file" && activeFilePath === file.file_path
317
- ? "bg-gray-200 dark:bg-[#30363d] font-medium"
318
- : "text-gray-500 dark:text-[#8b949e] hover:bg-gray-200 dark:hover:bg-[#30363d]"
319
- }`}
320
- onClick={() => {
321
- setActiveTab("file")
322
- setActiveFilePath(file.file_path)
323
- }}
324
- >
325
- {getFileIcon(file.file_path)}
326
- <span>{getFileName(file.file_path)}</span>
475
+ {tab.icon}
476
+ <span>{tab.label}</span>
327
477
  </button>
328
478
  ))}
329
479
  </div>
330
480
  <div className="ml-auto flex items-center">
331
- {((activeTab === "file" && activeFileContent) ||
332
- (activeTab === "ai-review" && aiReviewText)) && (
481
+ {((activeTab?.type === "file" && activeFileContent) ||
482
+ (activeTab?.type === "ai-review" && aiReviewText)) && (
333
483
  <button
334
484
  className="hover:bg-gray-200 dark:hover:bg-[#30363d] p-1 rounded-md transition-all duration-300"
335
485
  onClick={handleCopy}
@@ -342,7 +492,7 @@ export default function ImportantFilesView({
342
492
  <span className="sr-only">Copy</span>
343
493
  </button>
344
494
  )}
345
- {activeTab === "ai-review" && aiReviewText && isOwner && (
495
+ {activeTab?.type === "ai-review" && aiReviewText && isOwner && (
346
496
  <button
347
497
  className="hover:bg-gray-200 dark:hover:bg-[#30363d] p-1 rounded-md ml-1"
348
498
  onClick={onRequestAiReview}
@@ -352,10 +502,10 @@ export default function ImportantFilesView({
352
502
  <span className="sr-only">Re-request AI Review</span>
353
503
  </button>
354
504
  )}
355
- {activeTab === "file" && (
505
+ {activeTab?.type === "file" && (
356
506
  <button
357
507
  className="hover:bg-gray-200 dark:hover:bg-[#30363d] p-1 rounded-md"
358
- onClick={() => onEditClicked?.(activeFilePath)}
508
+ onClick={() => onEditClicked?.(activeTab.filePath)}
359
509
  >
360
510
  <Edit className="h-4 w-4" />
361
511
  <span className="sr-only">Edit</span>
@@ -363,30 +513,7 @@ export default function ImportantFilesView({
363
513
  )}
364
514
  </div>
365
515
  </div>
366
- <div className="p-4 bg-white dark:bg-[#0d1117]">
367
- {activeTab === "ai" ? (
368
- renderAiContent()
369
- ) : activeTab === "ai-review" ? (
370
- renderAiReviewContent()
371
- ) : activeFilePath &&
372
- (activeFilePath.endsWith(".md") ||
373
- activeFilePath?.toLowerCase().endsWith("readme")) ? (
374
- <MarkdownViewer markdownContent={activeFileContent} />
375
- ) : activeFilePath &&
376
- (activeFilePath.endsWith(".js") ||
377
- activeFilePath.endsWith(".jsx") ||
378
- activeFilePath.endsWith(".ts") ||
379
- activeFilePath.endsWith(".tsx")) ? (
380
- <div className="overflow-x-auto">
381
- <ShikiCodeViewer
382
- code={activeFileContent}
383
- filePath={activeFilePath}
384
- />
385
- </div>
386
- ) : (
387
- <pre className="whitespace-pre-wrap">{activeFileContent}</pre>
388
- )}
389
- </div>
516
+ <div className="p-4 bg-white dark:bg-[#0d1117]">{renderTabContent()}</div>
390
517
  </div>
391
518
  )
392
519
  }
@@ -105,7 +105,7 @@ export default function MainContentHeader({
105
105
  <DropdownMenuItem disabled={!Boolean(packageInfo)} asChild>
106
106
  <a
107
107
  href={`/editor?package_id=${packageInfo?.package_id}`}
108
- className="cursor-pointer p-2 py-4"
108
+ className="cursor-pointer px-2 py-3"
109
109
  >
110
110
  <Pencil className="h-4 w-4 mx-3" />
111
111
  Edit Online
@@ -115,7 +115,7 @@ export default function MainContentHeader({
115
115
  <DropdownMenuItem
116
116
  disabled={!Boolean(packageInfo)}
117
117
  onClick={handleDownloadZip}
118
- className="cursor-pointer p-2 py-4"
118
+ className="cursor-pointer px-2 py-3"
119
119
  >
120
120
  <Package2 className="h-4 w-4 mx-3" />
121
121
  Download ZIP
@@ -57,6 +57,8 @@ export default function RepoPageContent({
57
57
  const [pendingAiReviewId, setPendingAiReviewId] = useState<string | null>(
58
58
  null,
59
59
  )
60
+ const [licenseFileRequested, setLicenseFileRequested] =
61
+ useState<boolean>(false)
60
62
  const queryClient = useQueryClient()
61
63
  const { data: aiReview } = useAiReview(pendingAiReviewId, {
62
64
  refetchInterval: (data) => (data && !data.ai_review_text ? 2000 : false),
@@ -82,6 +84,11 @@ export default function RepoPageContent({
82
84
  Boolean(pendingAiReviewId) ||
83
85
  isRequestingAiReview
84
86
 
87
+ const handleLicenseFileRequest = () => {
88
+ setLicenseFileRequested(true)
89
+ setTimeout(() => setLicenseFileRequested(false), 100)
90
+ }
91
+
85
92
  // Handle initial view selection and hash-based view changes
86
93
  useEffect(() => {
87
94
  if (isCircuitJsonLoading) return
@@ -222,6 +229,7 @@ export default function RepoPageContent({
222
229
  })
223
230
  }
224
231
  }}
232
+ onLicenseFileRequested={licenseFileRequested}
225
233
  />
226
234
  </div>
227
235
 
@@ -235,6 +243,7 @@ export default function RepoPageContent({
235
243
  // Update URL hash when view changes
236
244
  window.location.hash = view
237
245
  }}
246
+ onLicenseClick={handleLicenseFileRequest}
238
247
  />
239
248
  </div>
240
249
  {/* Releases section - Only visible on small screens */}
@@ -13,9 +13,12 @@ import { PackageInfo } from "@/lib/types"
13
13
  interface SidebarAboutSectionProps {
14
14
  packageInfo?: PackageInfo
15
15
  isLoading?: boolean
16
+ onLicenseClick?: () => void
16
17
  }
17
18
 
18
- export default function SidebarAboutSection() {
19
+ export default function SidebarAboutSection({
20
+ onLicenseClick,
21
+ }: SidebarAboutSectionProps = {}) {
19
22
  const { packageInfo, refetch: refetchPackageInfo } = useCurrentPackageInfo()
20
23
  const { data: packageRelease } = usePackageReleaseById(
21
24
  packageInfo?.latest_package_release_id,
@@ -137,7 +140,11 @@ export default function SidebarAboutSection() {
137
140
  ))}
138
141
  </div>
139
142
  <div className="space-y-2 text-sm">
140
- <div className="flex items-center">
143
+ <button
144
+ className="flex items-center hover:underline hover:underline-offset-2 cursor-pointer hover:decoration-gray-500"
145
+ onClick={onLicenseClick}
146
+ disabled={!onLicenseClick}
147
+ >
141
148
  <svg
142
149
  className="h-4 w-4 mr-2 text-gray-500 dark:text-[#8b949e]"
143
150
  viewBox="0 0 16 16"
@@ -146,7 +153,7 @@ export default function SidebarAboutSection() {
146
153
  <path d="M8.75.75V2h.985c.304 0 .603.08.867.231l1.29.736c.038.022.08.033.124.033h2.234a.75.75 0 0 1 0 1.5h-.427l2.111 4.692a.75.75 0 0 1-.154.838l-.53-.53.53.53-.001.002-.002.002-.006.006-.006.005-.01.01-.045.04c-.21.176-.441.327-.686.45C14.556 10.78 13.88 11 13 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L12.178 4.5h-.162c-.305 0-.604-.079-.868-.231l-1.29-.736a.245.245 0 0 0-.124-.033H8.75V13h2.5a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5h2.5V3.5h-.984a.245.245 0 0 0-.124.033l-1.29.736c-.264.152-.563.231-.868.231h-.162l2.112 4.692a.75.75 0 0 1-.154.838l-.53-.53.53.53-.001.002-.002.002-.006.006-.016.015-.045.04c-.21.176-.441.327-.686.45C4.556 10.78 3.88 11 3 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L2.178 4.5H1.75a.75.75 0 0 1 0-1.5h2.234a.249.249 0 0 0 .125-.033l1.29-.736c.263-.15.561-.231.865-.231H7.25V.75a.75.75 0 0 1 1.5 0Zm2.945 8.477c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L13 6.327Zm-10 0c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L3 6.327Z"></path>
147
154
  </svg>
148
155
  <span>{currentLicense ?? "Unset"} license</span>
149
- </div>
156
+ </button>
150
157
 
151
158
  <div className="flex items-center">
152
159
  <Star className="h-4 w-4 mr-2 text-gray-500 dark:text-[#8b949e]" />
@@ -9,16 +9,18 @@ interface SidebarProps {
9
9
  packageInfo?: Package
10
10
  isLoading?: boolean
11
11
  onViewChange?: (view: "3d" | "pcb" | "schematic") => void
12
+ onLicenseClick?: () => void
12
13
  }
13
14
 
14
15
  export default function Sidebar({
15
16
  packageInfo,
16
17
  isLoading = false,
17
18
  onViewChange,
19
+ onLicenseClick,
18
20
  }: SidebarProps) {
19
21
  return (
20
22
  <div className="h-full p-4 bg-white dark:bg-[#0d1117] overflow-y-auto">
21
- <SidebarAboutSection />
23
+ <SidebarAboutSection onLicenseClick={onLicenseClick} />
22
24
  <PreviewImageSquares
23
25
  packageInfo={packageInfo}
24
26
  onViewChange={onViewChange}