@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.
- package/bun.lock +304 -947
- package/dist/bundle.js +11 -5
- package/fake-snippets-api/routes/api/autocomplete/create_autocomplete.ts +0 -1
- package/fake-snippets-api/routes/api/packages/create.ts +14 -3
- package/package.json +7 -4
- package/src/App.tsx +58 -2
- package/src/components/CircuitJsonImportDialog.tsx +10 -5
- package/src/components/DownloadButtonAndMenu.tsx +13 -0
- package/src/components/FileSidebar.tsx +83 -10
- package/src/components/PackageBuildsPage/LogContent.tsx +19 -7
- package/src/components/ViewPackagePage/components/important-files-view.tsx +294 -167
- package/src/components/ViewPackagePage/components/main-content-header.tsx +2 -2
- package/src/components/ViewPackagePage/components/repo-page-content.tsx +9 -0
- package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +10 -3
- package/src/components/ViewPackagePage/components/sidebar.tsx +3 -1
- package/src/components/dialogs/edit-package-details-dialog.tsx +3 -2
- package/src/components/package-port/CodeAndPreview.tsx +6 -1
- package/src/components/package-port/CodeEditor.tsx +21 -3
- package/src/components/package-port/CodeEditorHeader.tsx +12 -7
- package/src/components/ui/tree-view.tsx +51 -2
- package/src/hooks/use-create-package-mutation.ts +1 -1
- package/src/hooks/useFileManagement.ts +71 -6
- package/src/lib/download-fns/download-spice-file.ts +13 -0
- package/src/lib/utils/package-utils.ts +0 -3
- package/src/pages/dashboard.tsx +1 -1
- package/src/pages/datasheet.tsx +157 -67
- package/src/pages/datasheets.tsx +2 -2
- package/src/pages/latest.tsx +2 -2
- package/src/pages/search.tsx +1 -1
- package/src/pages/trending.tsx +2 -2
- package/vite.config.ts +1 -0
|
@@ -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
|
|
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 [
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
//
|
|
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 (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
103
|
-
aiUsageInstructions,
|
|
104
|
-
aiReviewText,
|
|
105
|
-
hasAiContent,
|
|
106
|
-
hasAiReview,
|
|
240
|
+
onLicenseFileRequested,
|
|
107
241
|
importantFiles,
|
|
108
|
-
|
|
109
|
-
|
|
242
|
+
availableTabs,
|
|
243
|
+
isLicenseFile,
|
|
244
|
+
getDefaultTab,
|
|
110
245
|
])
|
|
111
246
|
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
//
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
129
|
-
}
|
|
267
|
+
}, [activeTab, importantFiles, getDefaultTab])
|
|
130
268
|
|
|
131
|
-
//
|
|
132
|
-
const
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
{
|
|
278
|
-
{hasAiContent && (
|
|
469
|
+
{availableTabs.map((tab, index) => (
|
|
279
470
|
<button
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
291
|
-
<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?.(
|
|
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
|
|
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
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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}
|