@tscircuit/fake-snippets 0.0.99 → 0.0.101

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.
@@ -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,14 +9,15 @@ 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
+ import { useCurrentPackageCircuitJson } from "../hooks/use-current-package-circuit-json"
20
21
 
21
22
  interface PackageFile {
22
23
  package_file_id: string
@@ -36,6 +37,16 @@ interface ImportantFilesViewProps {
36
37
  aiReviewText?: string | null
37
38
  aiReviewRequested?: boolean
38
39
  onRequestAiReview?: () => void
40
+ onLicenseFileRequested?: boolean
41
+ }
42
+
43
+ type TabType = "ai" | "ai-review" | "file"
44
+
45
+ interface TabInfo {
46
+ type: TabType
47
+ filePath?: string | null
48
+ label: string
49
+ icon: React.ReactNode
39
50
  }
40
51
 
41
52
  export default function ImportantFilesView({
@@ -48,89 +59,235 @@ export default function ImportantFilesView({
48
59
  isLoading = false,
49
60
  onEditClicked,
50
61
  packageAuthorOwner,
62
+ onLicenseFileRequested,
51
63
  }: ImportantFilesViewProps) {
52
- const [activeFilePath, setActiveFilePath] = useState<string | null>(null)
53
- const [activeTab, setActiveTab] = useState<string | null>(null)
64
+ const [activeTab, setActiveTab] = useState<TabInfo | null>(null)
54
65
  const [copyState, setCopyState] = useState<"copy" | "copied">("copy")
55
66
  const { session: user } = useGlobalStore()
56
67
 
57
- const handleCopy = () => {
58
- if (activeTab === "ai-review" && aiReviewText) {
59
- navigator.clipboard.writeText(aiReviewText || "")
68
+ // Memoized computed values
69
+ const hasAiContent = useMemo(
70
+ () => Boolean(aiDescription || aiUsageInstructions),
71
+ [aiDescription, aiUsageInstructions],
72
+ )
73
+ const hasAiReview = useMemo(() => Boolean(aiReviewText), [aiReviewText])
74
+ const isOwner = useMemo(
75
+ () => user?.github_username === packageAuthorOwner,
76
+ [user?.github_username, packageAuthorOwner],
77
+ )
78
+
79
+ // File type utilities
80
+ const isLicenseFile = useCallback((filePath: string) => {
81
+ const lowerPath = filePath.toLowerCase()
82
+ return (
83
+ lowerPath === "license" ||
84
+ lowerPath.endsWith("/license") ||
85
+ lowerPath === "license.txt" ||
86
+ lowerPath.endsWith("/license.txt") ||
87
+ lowerPath === "license.md" ||
88
+ lowerPath.endsWith("/license.md")
89
+ )
90
+ }, [])
91
+
92
+ const isReadmeFile = useCallback((filePath: string) => {
93
+ const lowerPath = filePath.toLowerCase()
94
+ return lowerPath.endsWith("readme.md") || lowerPath.endsWith("readme")
95
+ }, [])
96
+
97
+ const isCodeFile = useCallback((filePath: string) => {
98
+ return (
99
+ filePath.endsWith(".js") ||
100
+ filePath.endsWith(".jsx") ||
101
+ filePath.endsWith(".ts") ||
102
+ filePath.endsWith(".tsx")
103
+ )
104
+ }, [])
105
+
106
+ const isMarkdownFile = useCallback(
107
+ (filePath: string) => {
108
+ return filePath.endsWith(".md") || isReadmeFile(filePath)
109
+ },
110
+ [isReadmeFile],
111
+ )
112
+
113
+ const getFileName = useCallback((path: string) => {
114
+ const parts = path.split("/")
115
+ return parts[parts.length - 1]
116
+ }, [])
117
+
118
+ const getFileIcon = useCallback(
119
+ (path: string) => {
120
+ return isCodeFile(path) ? (
121
+ <Code className="h-3.5 w-3.5 mr-1.5" />
122
+ ) : (
123
+ <FileText className="h-3.5 w-3.5 mr-1.5" />
124
+ )
125
+ },
126
+ [isCodeFile],
127
+ )
128
+
129
+ // Available tabs computation
130
+ const availableTabs = useMemo((): TabInfo[] => {
131
+ const tabs: TabInfo[] = []
132
+
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
+ tabs.push({
141
+ type: "ai-review",
142
+ filePath: null,
143
+ label: "AI Review",
144
+ icon: <SparklesIcon className="h-3.5 w-3.5 mr-1.5" />,
145
+ })
146
+
147
+ importantFiles.forEach((file) => {
148
+ tabs.push({
149
+ type: "file",
150
+ filePath: file.file_path,
151
+ label: getFileName(file.file_path),
152
+ icon: getFileIcon(file.file_path),
153
+ })
154
+ })
155
+
156
+ return tabs
157
+ }, [hasAiContent, importantFiles, getFileName, getFileIcon])
158
+
159
+ // Find default tab with fallback logic
160
+ const getDefaultTab = useCallback((): TabInfo | null => {
161
+ if (isLoading || availableTabs.length === 0) return null
162
+
163
+ // Priority 1: README file
164
+ const readmeTab = availableTabs.find(
165
+ (tab) =>
166
+ tab.type === "file" && tab.filePath && isReadmeFile(tab.filePath),
167
+ )
168
+ if (readmeTab) return readmeTab
169
+
170
+ // Priority 2: AI content
171
+ const aiTab = availableTabs.find((tab) => tab.type === "ai")
172
+ if (aiTab) return aiTab
173
+
174
+ // Priority 3: AI review
175
+ const aiReviewTab = availableTabs.find((tab) => tab.type === "ai-review")
176
+ if (aiReviewTab) return aiReviewTab
177
+
178
+ // Priority 4: First file
179
+ const firstFileTab = availableTabs.find((tab) => tab.type === "file")
180
+ if (firstFileTab) return firstFileTab
181
+
182
+ return null
183
+ }, [isLoading, availableTabs, isReadmeFile])
184
+
185
+ // Handle copy functionality
186
+ const handleCopy = useCallback(() => {
187
+ let textToCopy = ""
188
+
189
+ if (activeTab?.type === "ai-review" && aiReviewText) {
190
+ textToCopy = aiReviewText
191
+ } else if (activeTab?.type === "file" && activeFileContent) {
192
+ textToCopy = activeFileContent
193
+ }
194
+
195
+ if (textToCopy) {
196
+ navigator.clipboard.writeText(textToCopy)
60
197
  setCopyState("copied")
61
198
  setTimeout(() => setCopyState("copy"), 500)
62
- return
63
199
  }
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)
200
+ }, [activeTab, aiReviewText])
71
201
 
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.
202
+ // Handle tab selection with validation
203
+ const selectTab = useCallback(
204
+ (tab: TabInfo) => {
205
+ // Validate that the tab still exists (for file tabs)
206
+ if (tab.type === "file" && tab.filePath) {
207
+ const fileExists = importantFiles.some(
208
+ (file) => file.file_path === tab.filePath,
209
+ )
210
+ if (!fileExists) {
211
+ // File was deleted, fallback to default tab
212
+ const defaultTab = getDefaultTab()
213
+ setActiveTab(defaultTab)
214
+ return
215
+ }
216
+ }
217
+ setActiveTab(tab)
218
+ },
219
+ [importantFiles, getDefaultTab],
220
+ )
221
+
222
+ // Handle license file request
75
223
  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
- )
224
+ if (onLicenseFileRequested && importantFiles.length > 0) {
225
+ const licenseTab = availableTabs.find(
226
+ (tab) =>
227
+ tab.type === "file" && tab.filePath && isLicenseFile(tab.filePath),
228
+ )
85
229
 
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")
230
+ if (licenseTab) {
231
+ setActiveTab(licenseTab)
232
+ } else {
233
+ // License file not found, fallback to default
234
+ const defaultTab = getDefaultTab()
235
+ setActiveTab(defaultTab)
236
+ }
100
237
  }
101
238
  }, [
102
- aiDescription,
103
- aiUsageInstructions,
104
- aiReviewText,
105
- hasAiContent,
106
- hasAiReview,
239
+ onLicenseFileRequested,
107
240
  importantFiles,
108
- activeTab,
109
- isLoading,
241
+ availableTabs,
242
+ isLicenseFile,
243
+ getDefaultTab,
110
244
  ])
111
245
 
112
- // Get file name from path
113
- const getFileName = (path: string) => {
114
- const parts = path.split("/")
115
- return parts[parts.length - 1]
116
- }
246
+ // Set default tab when no tab is active
247
+ useEffect(() => {
248
+ if (activeTab === null && !isLoading) {
249
+ const defaultTab = getDefaultTab()
250
+ setActiveTab(defaultTab)
251
+ }
252
+ }, [activeTab, isLoading, getDefaultTab])
117
253
 
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" />
254
+ // Validate active tab still exists (handles file deletion)
255
+ useEffect(() => {
256
+ if (activeTab?.type === "file" && activeTab.filePath) {
257
+ const fileExists = importantFiles.some(
258
+ (file) => file.file_path === activeTab.filePath,
259
+ )
260
+ if (!fileExists) {
261
+ // Active file was deleted, fallback to default
262
+ const defaultTab = getDefaultTab()
263
+ setActiveTab(defaultTab)
264
+ }
127
265
  }
128
- return <FileText className="h-3.5 w-3.5 mr-1.5" />
129
- }
266
+ }, [activeTab, importantFiles, getDefaultTab])
130
267
 
131
- // Render AI content
132
- const renderAiContent = () => {
133
- return (
268
+ const { circuitJson } = useCurrentPackageCircuitJson()
269
+
270
+ // Get active file content
271
+ const partialActiveFile = useMemo(() => {
272
+ if (activeTab?.type !== "file" || !activeTab.filePath) return null
273
+ return importantFiles.find((file) => file.file_path === activeTab.filePath)
274
+ }, [activeTab, importantFiles])
275
+
276
+ const { data: activeFileFull } = usePackageFile(
277
+ partialActiveFile
278
+ ? {
279
+ file_path: partialActiveFile.file_path,
280
+ package_release_id: partialActiveFile.package_release_id,
281
+ }
282
+ : null,
283
+ { keepPreviousData: true },
284
+ )
285
+
286
+ const activeFileContent = activeFileFull?.content_text || ""
287
+
288
+ // Render content based on active tab
289
+ const renderAiContent = useCallback(
290
+ () => (
134
291
  <div className="markdown-content">
135
292
  {aiDescription && (
136
293
  <div className="mb-6">
@@ -145,12 +302,11 @@ export default function ImportantFilesView({
145
302
  </div>
146
303
  )}
147
304
  </div>
148
- )
149
- }
150
-
151
- const renderAiReviewContent = () => {
152
- const isOwner = user?.github_username === packageAuthorOwner
305
+ ),
306
+ [aiDescription, aiUsageInstructions],
307
+ )
153
308
 
309
+ const renderAiReviewContent = useCallback(() => {
154
310
  if (!aiReviewText && !aiReviewRequested) {
155
311
  return (
156
312
  <div className="flex flex-col items-center justify-center py-8 px-4">
@@ -166,7 +322,15 @@ export default function ImportantFilesView({
166
322
  from our AI assistant.
167
323
  </p>
168
324
  </div>
169
- {isOwner ? (
325
+ {!isOwner ? (
326
+ <p className="text-sm text-gray-500">
327
+ Only the package owner can generate an AI review
328
+ </p>
329
+ ) : !Boolean(circuitJson) ? (
330
+ <p className="text-sm text-gray-500">
331
+ Circuit JSON is required for AI review.
332
+ </p>
333
+ ) : (
170
334
  <Button
171
335
  onClick={onRequestAiReview}
172
336
  size="sm"
@@ -175,10 +339,6 @@ export default function ImportantFilesView({
175
339
  <SparklesIcon className="h-4 w-4 mr-2" />
176
340
  Request AI Review
177
341
  </Button>
178
- ) : (
179
- <p className="text-sm text-gray-500">
180
- Only the package owner can generate an AI review
181
- </p>
182
342
  )}
183
343
  </div>
184
344
  </div>
@@ -206,22 +366,61 @@ export default function ImportantFilesView({
206
366
  }
207
367
 
208
368
  return <MarkdownViewer markdownContent={aiReviewText || ""} />
209
- }
369
+ }, [aiReviewText, aiReviewRequested, isOwner, onRequestAiReview])
210
370
 
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 },
371
+ const renderFileContent = useCallback(() => {
372
+ if (!activeTab?.filePath || !activeFileContent) {
373
+ return <pre className="whitespace-pre-wrap">{activeFileContent}</pre>
374
+ }
375
+
376
+ if (isMarkdownFile(activeTab.filePath)) {
377
+ return <MarkdownViewer markdownContent={activeFileContent} />
378
+ }
379
+
380
+ if (isCodeFile(activeTab.filePath)) {
381
+ return (
382
+ <div className="overflow-x-auto">
383
+ <ShikiCodeViewer
384
+ code={activeFileContent}
385
+ filePath={activeTab.filePath}
386
+ />
387
+ </div>
388
+ )
389
+ }
390
+
391
+ return <pre className="whitespace-pre-wrap">{activeFileContent}</pre>
392
+ }, [activeTab, activeFileContent, isMarkdownFile, isCodeFile])
393
+
394
+ const renderTabContent = useCallback(() => {
395
+ if (!activeTab) return null
396
+
397
+ switch (activeTab.type) {
398
+ case "ai":
399
+ return renderAiContent()
400
+ case "ai-review":
401
+ return renderAiReviewContent()
402
+ case "file":
403
+ return renderFileContent()
404
+ default:
405
+ return null
406
+ }
407
+ }, [activeTab, renderAiContent, renderAiReviewContent, renderFileContent])
408
+
409
+ // Tab styling helper
410
+ const getTabClassName = useCallback(
411
+ (tab: TabInfo) => {
412
+ const isActive =
413
+ activeTab?.type === tab.type &&
414
+ (tab.type !== "file" || activeTab?.filePath === tab.filePath)
415
+
416
+ return `flex items-center px-3 py-1.5 rounded-md text-xs flex-shrink-0 whitespace-nowrap ${
417
+ isActive
418
+ ? "bg-gray-200 dark:bg-[#30363d] font-medium"
419
+ : "text-gray-500 dark:text-[#8b949e] hover:bg-gray-200 dark:hover:bg-[#30363d]"
420
+ }`
421
+ },
422
+ [activeTab],
223
423
  )
224
- const activeFileContent = activeFileFull?.content_text || ""
225
424
 
226
425
  if (isLoading) {
227
426
  return (
@@ -249,7 +448,7 @@ export default function ImportantFilesView({
249
448
  )
250
449
  }
251
450
 
252
- if (importantFiles.length === 0) {
451
+ if (importantFiles.length === 0 && !hasAiContent && !hasAiReview) {
253
452
  return (
254
453
  <div className="mt-4 border border-gray-200 dark:border-[#30363d] rounded-md overflow-hidden">
255
454
  <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 +467,24 @@ export default function ImportantFilesView({
268
467
  )
269
468
  }
270
469
 
271
- const isOwner = user?.github_username === packageAuthorOwner
272
-
273
470
  return (
274
471
  <div className="mt-4 border border-gray-200 dark:border-[#30363d] rounded-md overflow-hidden">
275
472
  <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
473
  <div className="flex items-center space-x-2 overflow-x-auto no-scrollbar flex-1 min-w-0">
277
- {/* AI Description Tab */}
278
- {hasAiContent && (
279
- <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
- }}
289
- >
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) => (
474
+ {availableTabs.map((tab, index) => (
313
475
  <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
- }}
476
+ key={`${tab.type}-${tab.filePath || index}`}
477
+ className={getTabClassName(tab)}
478
+ onClick={() => selectTab(tab)}
324
479
  >
325
- {getFileIcon(file.file_path)}
326
- <span>{getFileName(file.file_path)}</span>
480
+ {tab.icon}
481
+ <span>{tab.label}</span>
327
482
  </button>
328
483
  ))}
329
484
  </div>
330
485
  <div className="ml-auto flex items-center">
331
- {((activeTab === "file" && activeFileContent) ||
332
- (activeTab === "ai-review" && aiReviewText)) && (
486
+ {((activeTab?.type === "file" && activeFileContent) ||
487
+ (activeTab?.type === "ai-review" && aiReviewText)) && (
333
488
  <button
334
489
  className="hover:bg-gray-200 dark:hover:bg-[#30363d] p-1 rounded-md transition-all duration-300"
335
490
  onClick={handleCopy}
@@ -342,7 +497,7 @@ export default function ImportantFilesView({
342
497
  <span className="sr-only">Copy</span>
343
498
  </button>
344
499
  )}
345
- {activeTab === "ai-review" && aiReviewText && isOwner && (
500
+ {activeTab?.type === "ai-review" && aiReviewText && isOwner && (
346
501
  <button
347
502
  className="hover:bg-gray-200 dark:hover:bg-[#30363d] p-1 rounded-md ml-1"
348
503
  onClick={onRequestAiReview}
@@ -352,10 +507,10 @@ export default function ImportantFilesView({
352
507
  <span className="sr-only">Re-request AI Review</span>
353
508
  </button>
354
509
  )}
355
- {activeTab === "file" && (
510
+ {activeTab?.type === "file" && (
356
511
  <button
357
512
  className="hover:bg-gray-200 dark:hover:bg-[#30363d] p-1 rounded-md"
358
- onClick={() => onEditClicked?.(activeFilePath)}
513
+ onClick={() => onEditClicked?.(activeTab.filePath)}
359
514
  >
360
515
  <Edit className="h-4 w-4" />
361
516
  <span className="sr-only">Edit</span>
@@ -363,30 +518,7 @@ export default function ImportantFilesView({
363
518
  )}
364
519
  </div>
365
520
  </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>
521
+ <div className="p-4 bg-white dark:bg-[#0d1117]">{renderTabContent()}</div>
390
522
  </div>
391
523
  )
392
524
  }
@@ -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 */}