@tscircuit/fake-snippets 0.0.107 → 0.0.109

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 (80) hide show
  1. package/api/generated-index.js +82 -22
  2. package/biome.json +7 -1
  3. package/bun-tests/fake-snippets-api/routes/package_builds/get.test.ts +0 -15
  4. package/bun-tests/fake-snippets-api/routes/package_builds/list.test.ts +0 -12
  5. package/bun.lock +62 -19
  6. package/dist/bundle.js +25 -24
  7. package/dist/index.d.ts +26 -15
  8. package/dist/index.js +19 -18
  9. package/dist/schema.d.ts +32 -24
  10. package/dist/schema.js +7 -6
  11. package/fake-snippets-api/lib/db/db-client.ts +10 -1
  12. package/fake-snippets-api/lib/db/schema.ts +4 -3
  13. package/fake-snippets-api/lib/db/seed.ts +6 -9
  14. package/fake-snippets-api/lib/public-mapping/public-map-package-build.ts +0 -3
  15. package/fake-snippets-api/lib/public-mapping/public-map-package-release.ts +3 -0
  16. package/package.json +7 -8
  17. package/src/App.tsx +12 -11
  18. package/src/components/DownloadButtonAndMenu.tsx +133 -35
  19. package/src/components/FileSidebar.tsx +45 -193
  20. package/src/components/Footer.tsx +0 -1
  21. package/src/components/HeaderLogin.tsx +1 -1
  22. package/src/components/HiddenFilesDropdown.tsx +0 -2
  23. package/src/components/PackageBreadcrumb.tsx +1 -1
  24. package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +0 -2
  25. package/src/components/PackageBuildsPage/build-preview-content.tsx +34 -5
  26. package/src/components/PackageCard.tsx +0 -1
  27. package/src/components/ViewPackagePage/components/ShikiCodeViewer.tsx +20 -11
  28. package/src/components/ViewPackagePage/components/important-files-view.tsx +75 -59
  29. package/src/components/ViewPackagePage/components/main-content-header.tsx +4 -4
  30. package/src/components/ViewPackagePage/components/main-content-view-selector.tsx +0 -1
  31. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +1 -2
  32. package/src/components/ViewPackagePage/components/package-header.tsx +14 -17
  33. package/src/components/ViewPackagePage/components/preview-image-squares.tsx +0 -1
  34. package/src/components/ViewPackagePage/components/repo-page-content.tsx +21 -20
  35. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +18 -2
  36. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +1 -1
  37. package/src/components/ViewPackagePage/components/sidebar.tsx +0 -2
  38. package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +18 -17
  39. package/src/components/ViewPackagePage/components/theme-toggle.tsx +0 -2
  40. package/src/components/ViewPackagePage/hooks/use-toast.tsx +0 -1
  41. package/src/components/package-port/CodeAndPreview.tsx +23 -40
  42. package/src/components/package-port/CodeEditor.tsx +24 -1
  43. package/src/components/package-port/CodeEditorHeader.tsx +5 -2
  44. package/src/components/preview/BuildsList.tsx +20 -9
  45. package/src/components/preview/ConnectedPackagesList.tsx +73 -60
  46. package/src/components/preview/ConnectedRepoOverview.tsx +160 -154
  47. package/src/components/preview/PackageReleasesDashboard.tsx +41 -30
  48. package/src/components/preview/index.tsx +16 -153
  49. package/src/hooks/use-current-package-id.ts +5 -30
  50. package/src/hooks/use-current-package-info.ts +29 -5
  51. package/src/hooks/use-global-store.ts +1 -1
  52. package/src/hooks/useFileManagement.ts +153 -34
  53. package/src/hooks/useOptimizedPackageFilesLoader.ts +149 -0
  54. package/src/hooks/useUpdatePackageFilesMutation.ts +2 -0
  55. package/src/index.css +24 -0
  56. package/src/lib/download-fns/download-circuit-png.ts +11 -3
  57. package/src/lib/download-fns/download-gltf-from-circuit-json.ts +44 -0
  58. package/src/lib/utils/isComponentExported.ts +9 -0
  59. package/src/lib/utils/transformFilesToTreeData.tsx +195 -0
  60. package/src/pages/404.tsx +3 -5
  61. package/src/pages/authorize.tsx +0 -2
  62. package/src/pages/landing.tsx +0 -1
  63. package/src/pages/preview-release.tsx +279 -0
  64. package/src/pages/release-builds.tsx +0 -8
  65. package/src/pages/release-detail.tsx +17 -15
  66. package/src/pages/releases.tsx +5 -1
  67. package/src/pages/view-package.tsx +14 -13
  68. package/src/components/Footer2.tsx +0 -100
  69. package/src/components/ShippingInformationForm.tsx +0 -423
  70. package/src/components/StaticViewSnippetHeader.tsx +0 -70
  71. package/src/components/ViewPackagePage/components/file-explorer.tsx +0 -67
  72. package/src/components/ViewPackagePage/components/readme-view.tsx +0 -58
  73. package/src/components/ViewPackagePage/components/repo-header-button.tsx +0 -36
  74. package/src/components/ViewPackagePage/components/repo-header.tsx +0 -4
  75. package/src/components/ViewPackagePage/components/sidebar-contributors-section.tsx +0 -31
  76. package/src/components/ViewSnippetHeader.tsx +0 -181
  77. package/src/components/ui/input-otp.tsx +0 -69
  78. package/src/hooks/use-snippets-base-api-url.ts +0 -3
  79. package/src/pages/preview-build.tsx +0 -380
  80. package/src/pages/settings.tsx +0 -25
@@ -1,24 +1,9 @@
1
1
  import React, { useState } from "react"
2
2
  import { cn } from "@/lib/utils"
3
- import {
4
- File,
5
- Folder,
6
- MoreVertical,
7
- PanelRightOpen,
8
- Plus,
9
- Trash2,
10
- Pencil,
11
- } from "lucide-react"
12
- import { TreeView, TreeDataItem } from "@/components/ui/tree-view"
13
- import { isHiddenFile } from "./ViewPackagePage/utils/is-hidden-file"
3
+ import { PanelRightOpen, Plus, Loader2 } from "lucide-react"
4
+ import { TreeView } from "@/components/ui/tree-view"
14
5
  import { Input } from "@/components/ui/input"
15
- import {
16
- DropdownMenu,
17
- DropdownMenuContent,
18
- DropdownMenuGroup,
19
- DropdownMenuItem,
20
- DropdownMenuTrigger,
21
- } from "./ui/dropdown-menu"
6
+ import { transformFilesToTreeData } from "@/lib/utils/transformFilesToTreeData"
22
7
  import type {
23
8
  ICreateFileProps,
24
9
  ICreateFileResult,
@@ -27,8 +12,6 @@ import type {
27
12
  IRenameFileProps,
28
13
  IRenameFileResult,
29
14
  } from "@/hooks/useFileManagement"
30
- import { useToast } from "@/hooks/use-toast"
31
- import { useGlobalStore } from "@/hooks/use-global-store"
32
15
  import type { Package } from "fake-snippets-api/lib/db/schema"
33
16
  type FileName = string
34
17
 
@@ -44,6 +27,8 @@ interface FileSidebarProps {
44
27
  isCreatingFile: boolean
45
28
  setIsCreatingFile: React.Dispatch<React.SetStateAction<boolean>>
46
29
  pkg?: Package
30
+ isLoadingFiles?: boolean
31
+ loadingProgress?: string | null
47
32
  }
48
33
 
49
34
  const FileSidebar: React.FC<FileSidebarProps> = ({
@@ -57,6 +42,8 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
57
42
  handleRenameFile,
58
43
  isCreatingFile,
59
44
  setIsCreatingFile,
45
+ isLoadingFiles = true,
46
+ loadingProgress = null,
60
47
  }) => {
61
48
  const [sidebarOpen, setSidebarOpen] = fileSidebarState
62
49
  const [newFileName, setNewFileName] = useState("")
@@ -68,172 +55,25 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
68
55
  const [selectedItemId, setSelectedItemId] = React.useState<string>(
69
56
  currentFile || "",
70
57
  )
71
- const { toast } = useToast()
72
58
  const canModifyFiles = true
73
59
 
74
60
  const onFolderSelect = (folderPath: string) => {
75
61
  setSelectedFolderForCreation(folderPath)
76
62
  }
77
63
 
78
- const transformFilesToTreeData = (
79
- files: Record<FileName, string>,
80
- ): TreeDataItem[] => {
81
- type TreeNode = Omit<TreeDataItem, "children"> & {
82
- children?: Record<string, TreeNode>
83
- }
84
- const root: Record<string, TreeNode> = {}
85
-
86
- Object.keys(files).forEach((filePath) => {
87
- const hasLeadingSlash = filePath.startsWith("/")
88
- const pathSegments = (hasLeadingSlash ? filePath.slice(1) : filePath)
89
- .trim()
90
- .split("/")
91
- let currentNode: Record<string, TreeNode> = root
92
-
93
- pathSegments.forEach((segment, segmentIndex) => {
94
- const isLeafNode = segmentIndex === pathSegments.length - 1
95
- const ancestorPath = pathSegments.slice(0, segmentIndex).join("/")
96
- const relativePath = ancestorPath
97
- ? `${ancestorPath}/${segment}`
98
- : segment
99
- const absolutePath = hasLeadingSlash ? `/${relativePath}` : relativePath
100
- const itemId = absolutePath
101
-
102
- if (
103
- !currentNode[segment] &&
104
- (!isHiddenFile(relativePath) ||
105
- isHiddenFile(
106
- currentFile?.startsWith("/")
107
- ? currentFile.slice(1)
108
- : currentFile || "",
109
- ))
110
- ) {
111
- currentNode[segment] = {
112
- id: itemId,
113
- name: segment,
114
- isRenaming: renamingFile === itemId,
115
- onRename: (newFilename: string) => {
116
- const oldPath = itemId
117
- const pathParts = oldPath.split("/").filter((part) => part !== "")
118
- let newPath: string
119
-
120
- if (pathParts.length > 1) {
121
- const folderPath = pathParts.slice(0, -1).join("/")
122
- newPath = folderPath + "/" + newFilename
123
- } else {
124
- newPath = newFilename
125
- }
126
-
127
- if (oldPath.startsWith("/") && !newPath.startsWith("/")) {
128
- newPath = "/" + newPath
129
- }
130
-
131
- const { fileRenamed } = handleRenameFile({
132
- oldFilename: itemId,
133
- newFilename: newPath,
134
- onError: (error) => {
135
- toast({
136
- title: `Error renaming file`,
137
- description: error.message,
138
- variant: "destructive",
139
- })
140
- },
141
- })
142
- if (fileRenamed) {
143
- setRenamingFile(null)
144
- }
145
- },
146
- onCancelRename: () => {
147
- setRenamingFile(null)
148
- },
149
- icon: isLeafNode ? File : Folder,
150
- onClick: isLeafNode
151
- ? () => {
152
- onFileSelect(absolutePath)
153
- setSelectedFolderForCreation(null)
154
- }
155
- : () => onFolderSelect(absolutePath),
156
- draggable: false,
157
- droppable: !isLeafNode,
158
- children: isLeafNode ? undefined : {},
159
- actions: canModifyFiles ? (
160
- <>
161
- <DropdownMenu key={itemId}>
162
- <DropdownMenuTrigger asChild>
163
- <MoreVertical className="w-4 h-4 text-gray-500 hover:text-gray-700" />
164
- </DropdownMenuTrigger>
165
- <DropdownMenuContent
166
- className="w-fit bg-white shadow-lg rounded-md border-4 z-[100] border-white"
167
- style={{
168
- position: "absolute",
169
- top: "100%",
170
- left: "0",
171
- marginTop: "0.5rem",
172
- width: "8rem",
173
- padding: "0.01rem",
174
- }}
175
- >
176
- <DropdownMenuGroup>
177
- {isLeafNode && (
178
- <DropdownMenuItem
179
- onClick={() => {
180
- setRenamingFile(itemId)
181
- }}
182
- className="flex items-center px-3 py-1 text-xs text-black hover:bg-gray-100 cursor-pointer"
183
- >
184
- <Pencil className="mr-2 h-3 w-3" />
185
- Rename
186
- </DropdownMenuItem>
187
- )}
188
- <DropdownMenuItem
189
- onClick={() => {
190
- const { fileDeleted } = handleDeleteFile({
191
- filename: itemId,
192
- onError: (error) => {
193
- toast({
194
- title: `Error deleting file ${itemId}`,
195
- description: error.message,
196
- })
197
- },
198
- })
199
- if (fileDeleted) {
200
- setErrorMessage("")
201
- }
202
- }}
203
- className="flex items-center px-3 py-1 text-xs text-red-600 hover:bg-gray-100 cursor-pointer"
204
- >
205
- <Trash2 className="mr-2 h-3 w-3" />
206
- Delete
207
- </DropdownMenuItem>
208
- </DropdownMenuGroup>
209
- </DropdownMenuContent>
210
- </DropdownMenu>
211
- </>
212
- ) : undefined,
213
- }
214
- }
215
-
216
- if (!isLeafNode && currentNode[segment].children) {
217
- currentNode = currentNode[segment].children
218
- }
219
- })
220
- })
221
-
222
- const convertToArray = (
223
- items: Record<string, TreeNode>,
224
- ): TreeDataItem[] => {
225
- return Object.values(items).map((item) => ({
226
- ...item,
227
- children: item.children ? convertToArray(item.children) : undefined,
228
- }))
229
- }
230
- return convertToArray(root).filter((x) => {
231
- if (x.children?.length === 0) return false
232
- return true
233
- })
234
- }
235
-
236
- const treeData = transformFilesToTreeData(files)
64
+ const treeData = transformFilesToTreeData({
65
+ files,
66
+ currentFile,
67
+ renamingFile,
68
+ handleRenameFile,
69
+ handleDeleteFile,
70
+ setRenamingFile,
71
+ onFileSelect,
72
+ onFolderSelect,
73
+ canModifyFiles,
74
+ setErrorMessage,
75
+ setSelectedFolderForCreation,
76
+ })
237
77
 
238
78
  const getCurrentFolderPath = (): string => {
239
79
  if (selectedFolderForCreation) {
@@ -329,19 +169,31 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
329
169
  className,
330
170
  )}
331
171
  >
332
- <button
333
- onClick={toggleSidebar}
334
- className={`z-[99] mt-2 ml-2 text-gray-400 scale-90 transition-opacity duration-200 ${!sidebarOpen ? "opacity-0 pointer-events-none" : "opacity-100"}`}
335
- >
336
- <PanelRightOpen />
337
- </button>
338
- <button
339
- onClick={() => setIsCreatingFile(true)}
340
- className="absolute top-2 right-2 text-gray-400 hover:text-gray-600"
341
- aria-label="Create new file"
342
- >
343
- <Plus className="w-5 h-5" />
344
- </button>
172
+ <div className="flex items-center justify-between px-2 pt-2">
173
+ <button
174
+ onClick={toggleSidebar}
175
+ className={`text-gray-400 scale-90 transition-opacity duration-200 ${!sidebarOpen ? "opacity-0 pointer-events-none" : "opacity-100"}`}
176
+ >
177
+ <PanelRightOpen />
178
+ </button>
179
+ <div className="flex items-center gap-2">
180
+ {isLoadingFiles && (
181
+ <div className="flex items-center gap-1">
182
+ <Loader2 className="w-3 h-3 animate-spin text-gray-400" />
183
+ {loadingProgress && (
184
+ <span className="text-xs text-gray-400">{loadingProgress}</span>
185
+ )}
186
+ </div>
187
+ )}
188
+ <button
189
+ onClick={() => setIsCreatingFile(true)}
190
+ className="text-gray-400 hover:text-gray-600"
191
+ aria-label="Create new file"
192
+ >
193
+ <Plus className="w-5 h-5" />
194
+ </button>
195
+ </div>
196
+ </div>
345
197
  {isCreatingFile && (
346
198
  <div className="p-2">
347
199
  <Input
@@ -31,7 +31,6 @@ export default function Footer() {
31
31
  href: `/${session?.github_username}`,
32
32
  hidden: !isLoggedIn,
33
33
  },
34
- { name: "Settings", href: "/settings" },
35
34
  ]
36
35
  .filter((item) => !item.hidden)
37
36
  .map((item) => (
@@ -43,7 +43,7 @@ export const HeaderLogin = () => {
43
43
  </AvatarFallback>
44
44
  </Avatar>
45
45
  </DropdownMenuTrigger>
46
- <DropdownMenuContent className="ml-1 md:ml-0 md:mr-1">
46
+ <DropdownMenuContent className="ml-1 mr-1 md:ml-0 md:mr-1">
47
47
  <DropdownMenuItem asChild className="text-gray-500 text-xs" disabled>
48
48
  <div>
49
49
  AI Usage $
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import {
4
2
  DropdownMenu,
5
3
  DropdownMenuContent,
@@ -77,7 +77,7 @@ export function PackageBreadcrumb({
77
77
  {currentPage === "builds" ? (
78
78
  <BreadcrumbLink asChild>
79
79
  <PrefetchPageLink
80
- href={`/${packageName}/release/${releaseVersion}`}
80
+ href={`/${packageName}/releases/${releaseVersion}`}
81
81
  >
82
82
  {releaseVersion}
83
83
  </PrefetchPageLink>
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import { useCurrentPackageRelease } from "@/hooks/use-current-package-release"
4
2
  import { PackageRelease } from "fake-snippets-api/lib/db/schema"
5
3
  import { useState } from "react"
@@ -1,9 +1,13 @@
1
1
  import { useCurrentPackageInfo } from "@/hooks/use-current-package-info"
2
2
  import { useCurrentPackageRelease } from "@/hooks/use-current-package-release"
3
+ import { useState } from "react"
4
+ import { CircuitBoard } from "lucide-react"
3
5
 
4
6
  export function BuildPreviewContent() {
5
7
  const { packageRelease } = useCurrentPackageRelease({ refetchInterval: 2000 })
6
8
  const { packageInfo } = useCurrentPackageInfo()
9
+ const [imageError, setImageError] = useState(false)
10
+ const [imageLoading, setImageLoading] = useState(true)
7
11
 
8
12
  if (!packageRelease) {
9
13
  return (
@@ -16,11 +20,36 @@ export function BuildPreviewContent() {
16
20
  return (
17
21
  <div className="flex items-center justify-center w-full h-full">
18
22
  <div className="rounded overflow-hidden w-full max-w-full">
19
- <img
20
- src={`https://api.tscircuit.com/packages/images/${packageInfo?.name}/pcb.png`}
21
- alt="Package build preview"
22
- className="object-contain rounded w-full h-auto max-h-[240px] sm:max-h-[300px] lg:max-h-[360px]"
23
- />
23
+ {imageError ? (
24
+ <div className="flex flex-col items-center justify-center bg-gray-50 border border-gray-300 rounded-lg p-8 sm:p-12 lg:p-16 min-h-[240px] sm:min-h-[300px] lg:min-h-[360px]">
25
+ <CircuitBoard className="w-12 h-12 sm:w-16 sm:h-16 lg:w-20 lg:h-20 text-gray-400 mb-4" />
26
+ <h3 className="text-lg sm:text-xl font-medium text-gray-600 mb-2">
27
+ Preview Not Available
28
+ </h3>
29
+ <p className="text-sm sm:text-base text-gray-500 text-center max-w-sm">
30
+ The build preview image could not be loaded. This may be because
31
+ the build is still processing or the image is not available.
32
+ </p>
33
+ </div>
34
+ ) : (
35
+ <>
36
+ {imageLoading && (
37
+ <div className="flex items-center justify-center bg-gray-100 rounded-lg min-h-[240px] sm:min-h-[300px] lg:min-h-[360px]">
38
+ <div className="w-16 h-16 sm:w-20 sm:h-20 bg-gray-200 rounded animate-pulse"></div>
39
+ </div>
40
+ )}
41
+ <img
42
+ src={`https://api.tscircuit.com/packages/images/${packageInfo?.name}/pcb.png`}
43
+ alt="Package build preview"
44
+ className={`object-contain rounded w-full h-auto max-h-[240px] sm:max-h-[300px] lg:max-h-[360px] ${imageLoading ? "hidden" : "block"}`}
45
+ onLoad={() => setImageLoading(false)}
46
+ onError={() => {
47
+ setImageError(true)
48
+ setImageLoading(false)
49
+ }}
50
+ />
51
+ </>
52
+ )}
24
53
  </div>
25
54
  </div>
26
55
  )
@@ -19,7 +19,6 @@ import {
19
19
  } from "@/components/ui/dropdown-menu"
20
20
  import { SnippetType, SnippetTypeIcon } from "./SnippetTypeIcon"
21
21
  import { timeAgo } from "@/lib/utils/timeAgo"
22
- import { ImageWithFallback } from "./ImageWithFallback"
23
22
  import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
24
23
 
25
24
  export interface PackageCardProps {
@@ -1,11 +1,17 @@
1
1
  import { useShikiHighlighter } from "@/hooks/use-shiki-highlighter"
2
- import { useEffect, useMemo } from "react"
3
- import { useQuery } from "react-query"
2
+ import { useMemo } from "react"
4
3
  import { Skeleton } from "@/components/ui/skeleton"
5
4
 
6
5
  // Pre-randomized array to avoid flickering on re-renders
7
- const SKELETON_WIDTHS = ["w-2/3", "w-1/4", "w-5/6", "w-1/3", "w-1/2", "w-3/4"]
8
-
6
+ export const SKELETON_WIDTHS = [
7
+ "w-2/3",
8
+ "w-1/4",
9
+ "w-5/6",
10
+ "w-1/3",
11
+ "w-1/2",
12
+ "w-3/4",
13
+ ]
14
+ const PLACEHOLDER_SHIKI_HTML = `<pre class="shiki vitesse-light" style="background-color:#ffffff;color:#393a34" tabindex="0"><code><span class="line"></span></code></pre>`
9
15
  export const ShikiCodeViewer = ({
10
16
  code,
11
17
  filePath,
@@ -24,17 +30,20 @@ export const ShikiCodeViewer = ({
24
30
  [filePath, code, highlighter],
25
31
  )
26
32
 
27
- if (!html) {
33
+ if (html && html?.trim() !== PLACEHOLDER_SHIKI_HTML) {
28
34
  return (
29
- <div className="text-sm p-4">
30
- {SKELETON_WIDTHS.map((w, i) => (
31
- <Skeleton key={i} className={`h-4 mb-2 ${w}`} />
32
- ))}
33
- </div>
35
+ <div
36
+ className="text-sm shiki"
37
+ dangerouslySetInnerHTML={{ __html: html }}
38
+ />
34
39
  )
35
40
  }
36
41
 
37
42
  return (
38
- <div className="text-sm shiki" dangerouslySetInnerHTML={{ __html: html }} />
43
+ <div className="text-sm p-4">
44
+ {SKELETON_WIDTHS.map((w, i) => (
45
+ <Skeleton key={i} className={`h-4 mb-2 ${w}`} />
46
+ ))}
47
+ </div>
39
48
  )
40
49
  }
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import { useState, useEffect, useMemo, useCallback } from "react"
4
2
  import {
5
3
  Edit,
@@ -14,7 +12,7 @@ import {
14
12
  import { Skeleton } from "@/components/ui/skeleton"
15
13
  import { Button } from "@/components/ui/button"
16
14
  import { usePackageFile } from "@/hooks/use-package-files"
17
- import { ShikiCodeViewer } from "./ShikiCodeViewer"
15
+ import { ShikiCodeViewer, SKELETON_WIDTHS } from "./ShikiCodeViewer"
18
16
  import MarkdownViewer from "./markdown-viewer"
19
17
  import { useGlobalStore } from "@/hooks/use-global-store"
20
18
  import { useCurrentPackageCircuitJson } from "../hooks/use-current-package-circuit-json"
@@ -24,12 +22,12 @@ interface PackageFile {
24
22
  package_release_id: string
25
23
  file_path: string
26
24
  created_at: string
27
- content_text?: string
25
+ content_text?: string | null
28
26
  }
29
27
 
30
28
  interface ImportantFilesViewProps {
31
29
  importantFiles?: PackageFile[]
32
- isLoading?: boolean
30
+ isFetched?: boolean
33
31
  onEditClicked?: (file_path?: string | null) => void
34
32
  packageAuthorOwner?: string | null
35
33
  aiDescription?: string
@@ -56,7 +54,7 @@ export default function ImportantFilesView({
56
54
  aiReviewText,
57
55
  aiReviewRequested,
58
56
  onRequestAiReview,
59
- isLoading = false,
57
+ isFetched = false,
60
58
  onEditClicked,
61
59
  packageAuthorOwner,
62
60
  onLicenseFileRequested,
@@ -130,19 +128,25 @@ export default function ImportantFilesView({
130
128
  const availableTabs = useMemo((): TabInfo[] => {
131
129
  const tabs: TabInfo[] = []
132
130
 
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
- })
131
+ // Only show AI description tab if there's actual AI content
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
+ }
139
140
 
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
- })
141
+ // Only show AI review tab if there's actual AI review content
142
+ if (hasAiReview || isOwner) {
143
+ tabs.push({
144
+ type: "ai-review",
145
+ filePath: null,
146
+ label: "AI Review",
147
+ icon: <SparklesIcon className="h-3.5 w-3.5 mr-1.5" />,
148
+ })
149
+ }
146
150
 
147
151
  importantFiles.forEach((file) => {
148
152
  tabs.push({
@@ -154,11 +158,11 @@ export default function ImportantFilesView({
154
158
  })
155
159
 
156
160
  return tabs
157
- }, [hasAiContent, importantFiles, getFileName, getFileIcon])
161
+ }, [hasAiContent, hasAiReview, importantFiles, getFileName, getFileIcon])
158
162
 
159
163
  // Find default tab with fallback logic
160
164
  const getDefaultTab = useCallback((): TabInfo | null => {
161
- if (isLoading || availableTabs.length === 0) return null
165
+ if (!isFetched || availableTabs.length === 0) return null
162
166
 
163
167
  // Priority 1: README file
164
168
  const readmeTab = availableTabs.find(
@@ -167,12 +171,14 @@ export default function ImportantFilesView({
167
171
  )
168
172
  if (readmeTab) return readmeTab
169
173
 
170
- // Priority 2: AI content
171
- const aiTab = availableTabs.find((tab) => tab.type === "ai")
174
+ // Priority 2: AI content (only if available)
175
+ const aiTab = availableTabs.find((tab) => tab.type === "ai" && hasAiContent)
172
176
  if (aiTab) return aiTab
173
177
 
174
178
  // Priority 3: AI review
175
- const aiReviewTab = availableTabs.find((tab) => tab.type === "ai-review")
179
+ const aiReviewTab = availableTabs.find(
180
+ (tab) => tab.type === "ai-review" && hasAiReview,
181
+ )
176
182
  if (aiReviewTab) return aiReviewTab
177
183
 
178
184
  // Priority 4: First file
@@ -180,24 +186,7 @@ export default function ImportantFilesView({
180
186
  if (firstFileTab) return firstFileTab
181
187
 
182
188
  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)
197
- setCopyState("copied")
198
- setTimeout(() => setCopyState("copy"), 500)
199
- }
200
- }, [activeTab, aiReviewText])
189
+ }, [isFetched, availableTabs, isReadmeFile])
201
190
 
202
191
  // Handle tab selection with validation
203
192
  const selectTab = useCallback(
@@ -245,11 +234,11 @@ export default function ImportantFilesView({
245
234
 
246
235
  // Set default tab when no tab is active
247
236
  useEffect(() => {
248
- if (activeTab === null && !isLoading) {
237
+ if (activeTab === null && isFetched) {
249
238
  const defaultTab = getDefaultTab()
250
239
  setActiveTab(defaultTab)
251
240
  }
252
- }, [activeTab, isLoading, getDefaultTab])
241
+ }, [activeTab, isFetched, getDefaultTab])
253
242
 
254
243
  // Validate active tab still exists (handles file deletion)
255
244
  useEffect(() => {
@@ -273,18 +262,41 @@ export default function ImportantFilesView({
273
262
  return importantFiles.find((file) => file.file_path === activeTab.filePath)
274
263
  }, [activeTab, importantFiles])
275
264
 
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
- )
265
+ const { data: activeFileFull, isFetched: isActiveFileFetched } =
266
+ usePackageFile(
267
+ partialActiveFile
268
+ ? {
269
+ file_path: partialActiveFile.file_path,
270
+ package_release_id: partialActiveFile.package_release_id,
271
+ }
272
+ : null,
273
+ {
274
+ keepPreviousData: true,
275
+ staleTime: Infinity,
276
+ refetchOnMount: false,
277
+ refetchOnWindowFocus: false,
278
+ refetchOnReconnect: false,
279
+ },
280
+ )
285
281
 
286
282
  const activeFileContent = activeFileFull?.content_text || ""
287
283
 
284
+ const handleCopy = () => {
285
+ let textToCopy = ""
286
+
287
+ if (activeTab?.type === "ai-review" && aiReviewText) {
288
+ textToCopy = aiReviewText
289
+ } else if (activeTab?.type === "file" && activeFileContent) {
290
+ textToCopy = activeFileContent
291
+ }
292
+
293
+ if (textToCopy) {
294
+ navigator.clipboard.writeText(textToCopy)
295
+ setCopyState("copied")
296
+ setTimeout(() => setCopyState("copy"), 500)
297
+ }
298
+ }
299
+
288
300
  // Render content based on active tab
289
301
  const renderAiContent = useCallback(
290
302
  () => (
@@ -369,20 +381,24 @@ export default function ImportantFilesView({
369
381
  }, [aiReviewText, aiReviewRequested, isOwner, onRequestAiReview])
370
382
 
371
383
  const renderFileContent = useCallback(() => {
372
- if (!activeTab?.filePath || !activeFileContent) {
373
- return <pre className="whitespace-pre-wrap">{activeFileContent}</pre>
384
+ if (!isActiveFileFetched || !activeTab?.filePath || !activeFileContent) {
385
+ ;<div className="text-sm p-4">
386
+ {SKELETON_WIDTHS.map((w, i) => (
387
+ <Skeleton key={i} className={`h-4 mb-2 ${w}`} />
388
+ ))}
389
+ </div>
374
390
  }
375
391
 
376
- if (isMarkdownFile(activeTab.filePath)) {
392
+ if (isMarkdownFile(String(activeTab?.filePath))) {
377
393
  return <MarkdownViewer markdownContent={activeFileContent} />
378
394
  }
379
395
 
380
- if (isCodeFile(activeTab.filePath)) {
396
+ if (isCodeFile(String(activeTab?.filePath))) {
381
397
  return (
382
- <div className="overflow-x-auto">
398
+ <div className="overflow-x-auto no-scrollbar">
383
399
  <ShikiCodeViewer
384
400
  code={activeFileContent}
385
- filePath={activeTab.filePath}
401
+ filePath={String(activeTab?.filePath)}
386
402
  />
387
403
  </div>
388
404
  )
@@ -422,7 +438,7 @@ export default function ImportantFilesView({
422
438
  [activeTab],
423
439
  )
424
440
 
425
- if (isLoading) {
441
+ if (!isFetched) {
426
442
  return (
427
443
  <div className="mt-4 border border-gray-200 dark:border-[#30363d] rounded-md overflow-hidden">
428
444
  <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]">