@tscircuit/fake-snippets 0.0.87 → 0.0.89

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 (75) hide show
  1. package/api/generated-index.js +96 -14
  2. package/bun-tests/fake-snippets-api/routes/proxy.test.ts +42 -0
  3. package/bun.lock +187 -206
  4. package/dist/bundle.js +207 -101
  5. package/fake-snippets-api/routes/api/package_releases/create.ts +1 -1
  6. package/fake-snippets-api/routes/api/proxy.ts +128 -0
  7. package/package.json +57 -50
  8. package/renovate.json +2 -1
  9. package/src/App.tsx +22 -3
  10. package/src/ContextProviders.tsx +2 -0
  11. package/src/build-watcher.ts +52 -0
  12. package/src/components/CmdKMenu.tsx +533 -197
  13. package/src/components/DownloadButtonAndMenu.tsx +104 -26
  14. package/src/components/FileSidebar.tsx +11 -1
  15. package/src/components/Header.tsx +5 -1
  16. package/src/components/Header2.tsx +7 -2
  17. package/src/components/HeaderLogin.tsx +1 -1
  18. package/src/components/PackageBuildsPage/LogContent.tsx +25 -22
  19. package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +6 -6
  20. package/src/components/PackageBuildsPage/build-preview-content.tsx +5 -5
  21. package/src/components/PackageBuildsPage/package-build-details-panel.tsx +15 -13
  22. package/src/components/PackageBuildsPage/package-build-header.tsx +17 -16
  23. package/src/components/PackageCard.tsx +66 -16
  24. package/src/components/PrefetchPageLink.tsx +66 -15
  25. package/src/components/SearchComponent.tsx +2 -2
  26. package/src/components/SuspenseRunFrame.tsx +14 -2
  27. package/src/components/ViewPackagePage/components/important-files-view.tsx +97 -22
  28. package/src/components/ViewPackagePage/components/main-content-header.tsx +27 -3
  29. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +2 -2
  30. package/src/components/ViewPackagePage/components/repo-page-content.tsx +49 -34
  31. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +2 -2
  32. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +20 -12
  33. package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +0 -7
  34. package/src/components/ViewPackagePage/utils/fuzz-search.ts +121 -0
  35. package/src/components/ViewPackagePage/utils/is-hidden-file.ts +4 -0
  36. package/src/components/ViewPackagePage/utils/is-package-file-important.ts +18 -5
  37. package/src/components/dialogs/confirm-delete-package-dialog.tsx +1 -1
  38. package/src/components/dialogs/confirm-discard-changes-dialog.tsx +73 -0
  39. package/src/components/dialogs/edit-package-details-dialog.tsx +2 -2
  40. package/src/components/dialogs/view-ts-files-dialog.tsx +478 -42
  41. package/src/components/package-port/CodeAndPreview.tsx +16 -0
  42. package/src/components/package-port/CodeEditor.tsx +113 -11
  43. package/src/components/package-port/CodeEditorHeader.tsx +39 -4
  44. package/src/components/package-port/EditorNav.tsx +41 -15
  45. package/src/components/package-port/GlobalFindReplace.tsx +681 -0
  46. package/src/components/package-port/QuickOpen.tsx +241 -0
  47. package/src/components/ui/dialog.tsx +1 -1
  48. package/src/components/ui/tree-view.tsx +1 -1
  49. package/src/global.d.ts +3 -0
  50. package/src/hooks/use-ai-review.ts +31 -0
  51. package/src/hooks/use-current-package-release.ts +5 -1
  52. package/src/hooks/use-download-zip.ts +50 -0
  53. package/src/hooks/use-hotkey.ts +116 -0
  54. package/src/hooks/use-package-by-package-id.ts +1 -0
  55. package/src/hooks/use-package-by-package-name.ts +1 -0
  56. package/src/hooks/use-package-files.ts +3 -0
  57. package/src/hooks/use-package-release.ts +5 -1
  58. package/src/hooks/use-package.ts +1 -0
  59. package/src/hooks/use-request-ai-review-mutation.ts +14 -6
  60. package/src/hooks/use-snippet.ts +1 -0
  61. package/src/hooks/useFileManagement.ts +26 -8
  62. package/src/hooks/usePackageFilesLoader.ts +3 -1
  63. package/src/index.css +11 -0
  64. package/src/lib/decodeUrlHashToFsMap.ts +17 -0
  65. package/src/lib/download-fns/download-circuit-png.ts +88 -0
  66. package/src/lib/download-fns/download-png-utils.ts +31 -0
  67. package/src/lib/encodeFsMapToUrlHash.ts +13 -0
  68. package/src/lib/populate-query-cache-with-ssr-data.ts +39 -38
  69. package/src/lib/ts-lib-cache.ts +47 -0
  70. package/src/lib/types.ts +2 -0
  71. package/src/main.tsx +7 -0
  72. package/src/pages/dashboard.tsx +8 -5
  73. package/src/pages/user-profile.tsx +1 -1
  74. package/src/pages/view-package.tsx +15 -7
  75. package/vite.config.ts +100 -1
@@ -2,7 +2,13 @@ import React from "react"
2
2
  import { Link } from "wouter"
3
3
  import { Package } from "fake-snippets-api/lib/db/schema"
4
4
  import { StarIcon, LockClosedIcon } from "@radix-ui/react-icons"
5
- import { GlobeIcon, MoreVertical, PencilIcon, Trash2 } from "lucide-react"
5
+ import {
6
+ GlobeIcon,
7
+ MoreVertical,
8
+ PencilIcon,
9
+ Share2,
10
+ Trash2,
11
+ } from "lucide-react"
6
12
  import { Button } from "@/components/ui/button"
7
13
  import {
8
14
  DropdownMenu,
@@ -13,6 +19,7 @@ import {
13
19
  import { SnippetType, SnippetTypeIcon } from "./SnippetTypeIcon"
14
20
  import { timeAgo } from "@/lib/utils/timeAgo"
15
21
  import { ImageWithFallback } from "./ImageWithFallback"
22
+ import { useToast } from "@/hooks/use-toast"
16
23
 
17
24
  export interface PackageCardProps {
18
25
  /** The package data to display */
@@ -49,6 +56,7 @@ export const PackageCard: React.FC<PackageCardProps> = ({
49
56
  withLink = true,
50
57
  renderActions,
51
58
  }) => {
59
+ const { toast } = useToast()
52
60
  const handleDeleteClick = (e: React.MouseEvent) => {
53
61
  e.preventDefault() // Prevent navigation
54
62
  if (onDeleteClick) {
@@ -56,6 +64,41 @@ export const PackageCard: React.FC<PackageCardProps> = ({
56
64
  }
57
65
  }
58
66
 
67
+ const handleShareClick = async (e: React.MouseEvent) => {
68
+ e.preventDefault()
69
+
70
+ const shareUrl = `${window.location.origin}/${pkg.owner_github_username}/${pkg.unscoped_name}`
71
+ const shareText =
72
+ `Explore this tscircuit package: ${pkg.unscoped_name} by ${pkg.owner_github_username}${pkg.description ? ` - ${pkg.description}` : ""}`.trim()
73
+ if (navigator.share) {
74
+ await navigator
75
+ .share({
76
+ title: shareText,
77
+ text: shareText,
78
+ url: shareUrl,
79
+ })
80
+ .catch(() => fallbackShare(shareText, shareUrl))
81
+ } else {
82
+ fallbackShare(shareText, shareUrl)
83
+ }
84
+ }
85
+
86
+ const fallbackShare = (text: string, url: string) => {
87
+ const shareContent = `${text}\n${url}`
88
+ navigator.clipboard
89
+ .writeText(shareContent)
90
+ .then(() => {
91
+ toast({
92
+ title: "Share content copied to clipboard",
93
+ })
94
+ })
95
+ .catch(() => {
96
+ toast({
97
+ title: "Unable to share or copy to clipboard",
98
+ })
99
+ })
100
+ }
101
+
59
102
  const cardContent = (
60
103
  <div
61
104
  className={`border p-4 rounded-md hover:shadow-md transition-shadow flex flex-col gap-4 ${className}`}
@@ -92,18 +135,25 @@ export const PackageCard: React.FC<PackageCardProps> = ({
92
135
  <StarIcon className="w-4 h-4 pt-[2.5px]" />
93
136
  <span className="text-[16px]">{pkg.star_count || 0}</span>
94
137
  </div>
95
- {isCurrentUserPackage && onDeleteClick && (
96
- <DropdownMenu>
97
- <DropdownMenuTrigger asChild>
98
- <Button
99
- variant="ghost"
100
- size="icon"
101
- className="h-[1.5rem] w-[1.5rem]"
102
- >
103
- <MoreVertical className="h-4 w-4" />
104
- </Button>
105
- </DropdownMenuTrigger>
106
- <DropdownMenuContent>
138
+ <DropdownMenu>
139
+ <DropdownMenuTrigger asChild>
140
+ <Button
141
+ variant="ghost"
142
+ size="icon"
143
+ className="h-[1.5rem] w-[1.5rem]"
144
+ >
145
+ <MoreVertical className="h-4 w-4" />
146
+ </Button>
147
+ </DropdownMenuTrigger>
148
+ <DropdownMenuContent>
149
+ <DropdownMenuItem
150
+ className="text-xs cursor-pointer"
151
+ onClick={handleShareClick}
152
+ >
153
+ <Share2 className="mr-2 h-3 w-3" />
154
+ Share Package
155
+ </DropdownMenuItem>{" "}
156
+ {isCurrentUserPackage && onDeleteClick && (
107
157
  <DropdownMenuItem
108
158
  className="text-xs text-red-600"
109
159
  onClick={handleDeleteClick}
@@ -111,9 +161,9 @@ export const PackageCard: React.FC<PackageCardProps> = ({
111
161
  <Trash2 className="mr-2 h-3 w-3" />
112
162
  Delete Package
113
163
  </DropdownMenuItem>
114
- </DropdownMenuContent>
115
- </DropdownMenu>
116
- )}
164
+ )}
165
+ </DropdownMenuContent>
166
+ </DropdownMenu>
117
167
  {renderActions && renderActions(pkg)}
118
168
  </div>
119
169
  </div>
@@ -1,18 +1,34 @@
1
1
  import { Link } from "wouter"
2
2
  import { useEffect } from "react"
3
3
  import { useInView } from "react-intersection-observer"
4
+ import { useQueryClient } from "react-query"
5
+ import { useAxios } from "@/hooks/use-axios"
4
6
 
5
- export /**
6
- * PrefetchPageLink component that loads page components when links become visible.
7
- * Routes are automatically mapped to their corresponding page components under @/pages.
8
- * The href path is used to determine which page to load:
9
- *
10
- * Example:
11
- * - href="/editor" -> loads "@/pages/editor.tsx"
12
- * - href="/" -> loads "@/pages/landing.tsx"
13
- * - href="/my-orders" -> loads "@/pages/my-orders.tsx"
14
- */
15
- const PrefetchPageLink = ({
7
+ // Whitelist of known pages that can be safely prefetched
8
+ const PREFETCHABLE_PAGES = new Set([
9
+ "landing",
10
+ "editor",
11
+ "search",
12
+ "trending",
13
+ "dashboard",
14
+ "latest",
15
+ "settings",
16
+ "quickstart",
17
+ ])
18
+
19
+ // Helper to check if a path is a package path (e.g. /username/package-name)
20
+ const isPackagePath = (path: string) => {
21
+ const parts = path.split("/").filter(Boolean)
22
+ return parts.length === 2
23
+ }
24
+
25
+ // Helper to check if a path is a user profile path
26
+ const isUserProfilePath = (path: string) => {
27
+ const parts = path.split("/").filter(Boolean)
28
+ return parts.length === 1 && !PREFETCHABLE_PAGES.has(parts[0])
29
+ }
30
+
31
+ export const PrefetchPageLink = ({
16
32
  href,
17
33
  children,
18
34
  className,
@@ -27,13 +43,48 @@ const PrefetchPageLink = ({
27
43
  triggerOnce: true,
28
44
  threshold: 0,
29
45
  })
46
+ const queryClient = useQueryClient()
47
+ const axios = useAxios()
30
48
 
31
49
  useEffect(() => {
32
- if (inView) {
33
- const link = href === "/" ? "landing" : href.slice(1)
34
- if (!link) return
35
- const pageName = link.split("?")[0]
50
+ if (!inView) return
51
+
52
+ const path = href === "/" ? "landing" : href.slice(1)
53
+ if (!path) return
54
+
55
+ // Handle user profile paths
56
+ if (isUserProfilePath(path)) {
57
+ const username = path.split("/")[0]
58
+ // Prefetch user profile data
59
+ queryClient.prefetchQuery(["account", username], async () => {
60
+ const { data } = await axios.post("/accounts/get", {
61
+ github_username: username,
62
+ })
63
+ return data.account
64
+ })
65
+ return
66
+ }
67
+
68
+ // Handle package paths
69
+ if (isPackagePath(path)) {
70
+ const [owner, name] = path.split("/")
71
+ const packageName = name.split("#")[0]
72
+ // Prefetch package data
73
+ queryClient.prefetchQuery(
74
+ ["package", `${owner}/${packageName}`],
75
+ async () => {
76
+ const { data } = await axios.get("/packages/get", {
77
+ params: { name: `${owner}/${packageName}` },
78
+ })
79
+ return data.package
80
+ },
81
+ )
82
+ return
83
+ }
36
84
 
85
+ // Handle regular pages
86
+ const pageName = path.split("?")[0]
87
+ if (PREFETCHABLE_PAGES.has(pageName)) {
37
88
  import(`@/pages/${pageName}.tsx`).catch((error) => {
38
89
  console.error(`Failed to prefetch page module ${pageName}:`, error)
39
90
  })
@@ -184,7 +184,7 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
184
184
  role="searchbox"
185
185
  />
186
186
  {isLoading && (
187
- <div className="absolute top-full w-lg left-0 right-0 mt-1 bg-white shadow-lg rounded-lg border w-80 grid place-items-center py-4 z-10 p-3">
187
+ <div className="absolute top-full w-lg left-0 right-0 mt-1 bg-white shadow-lg rounded-lg border w-80 grid place-items-center py-4 z-50 p-3">
188
188
  <div className="flex items-center space-x-2">
189
189
  <div className="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
190
190
  <span className="text-gray-600 text-sm">Searching...</span>
@@ -195,7 +195,7 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
195
195
  {showResults && searchResults && (
196
196
  <div
197
197
  ref={resultsRef}
198
- className="absolute top-full md:left-0 right-0 mt-2 bg-white shadow-lg rounded-md z-10 w-80 max-h-screen overflow-y-auto overflow-x-visible"
198
+ className="absolute top-full md:left-0 right-0 mt-2 bg-white shadow-lg rounded-md z-50 w-80 max-h-screen overflow-y-auto overflow-x-visible"
199
199
  >
200
200
  {searchResults.length > 0 ? (
201
201
  <ul className="divide-y divide-gray-200">
@@ -9,8 +9,20 @@ export const SuspenseRunFrame = (
9
9
  props: React.ComponentProps<typeof RunFrame>,
10
10
  ) => {
11
11
  return (
12
- <Suspense fallback={<div>Loading...</div>}>
13
- <RunFrame {...props} />
12
+ <Suspense
13
+ fallback={
14
+ <div className="flex justify-center items-center h-full">
15
+ <div className="w-48">
16
+ <div className="loading">
17
+ <div className="loading-bar"></div>
18
+ </div>
19
+ </div>
20
+ </div>
21
+ }
22
+ >
23
+ <div className="h-[98vh]">
24
+ <RunFrame {...props} />
25
+ </div>
14
26
  </Suspense>
15
27
  )
16
28
  }
@@ -1,12 +1,22 @@
1
1
  "use client"
2
2
 
3
3
  import { useState, useEffect } from "react"
4
- import { Edit, FileText, Code, Copy, CopyCheck } from "lucide-react"
4
+ import {
5
+ Edit,
6
+ FileText,
7
+ Code,
8
+ Copy,
9
+ CopyCheck,
10
+ Loader2,
11
+ RefreshCcwIcon,
12
+ } from "lucide-react"
5
13
  import { Skeleton } from "@/components/ui/skeleton"
14
+ import { Button } from "@/components/ui/button"
6
15
  import { usePackageFile, usePackageFileByPath } from "@/hooks/use-package-files"
7
16
  import { ShikiCodeViewer } from "./ShikiCodeViewer"
8
17
  import { SparklesIcon } from "lucide-react"
9
18
  import MarkdownViewer from "./markdown-viewer"
19
+ import { useGlobalStore } from "@/hooks/use-global-store"
10
20
 
11
21
  interface PackageFile {
12
22
  package_file_id: string
@@ -20,7 +30,7 @@ interface ImportantFilesViewProps {
20
30
  importantFiles?: PackageFile[]
21
31
  isLoading?: boolean
22
32
  onEditClicked?: (file_path?: string | null) => void
23
-
33
+ packageAuthorOwner?: string | null
24
34
  aiDescription?: string
25
35
  aiUsageInstructions?: string
26
36
  aiReviewText?: string | null
@@ -37,12 +47,20 @@ export default function ImportantFilesView({
37
47
  onRequestAiReview,
38
48
  isLoading = false,
39
49
  onEditClicked,
50
+ packageAuthorOwner,
40
51
  }: ImportantFilesViewProps) {
41
52
  const [activeFilePath, setActiveFilePath] = useState<string | null>(null)
42
53
  const [activeTab, setActiveTab] = useState<string | null>(null)
43
54
  const [copyState, setCopyState] = useState<"copy" | "copied">("copy")
55
+ const { session: user } = useGlobalStore()
44
56
 
45
57
  const handleCopy = () => {
58
+ if (activeTab === "ai-review" && aiReviewText) {
59
+ navigator.clipboard.writeText(aiReviewText || "")
60
+ setCopyState("copied")
61
+ setTimeout(() => setCopyState("copy"), 500)
62
+ return
63
+ }
46
64
  navigator.clipboard.writeText(activeFileContent)
47
65
  setCopyState("copied")
48
66
  setTimeout(() => setCopyState("copy"), 500)
@@ -131,20 +149,59 @@ export default function ImportantFilesView({
131
149
  }
132
150
 
133
151
  const renderAiReviewContent = () => {
152
+ const isOwner = user?.github_username === packageAuthorOwner
153
+
134
154
  if (!aiReviewText && !aiReviewRequested) {
135
155
  return (
136
- <button
137
- className="text-sm text-blue-600 dark:text-[#58a6ff] underline"
138
- onClick={onRequestAiReview}
139
- >
140
- Request AI Review
141
- </button>
156
+ <div className="flex flex-col items-center justify-center py-8 px-4">
157
+ <div className="text-center space-y-4 max-w-md">
158
+ <div className="flex justify-center">
159
+ <div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center">
160
+ <SparklesIcon className="h-6 w-6 text-blue-600" />
161
+ </div>
162
+ </div>
163
+ <div className="space-y-2">
164
+ <p className="text-sm text-gray-600">
165
+ Get detailed feedback and suggestions for improving your package
166
+ from our AI assistant.
167
+ </p>
168
+ </div>
169
+ {isOwner ? (
170
+ <Button
171
+ onClick={onRequestAiReview}
172
+ size="sm"
173
+ className="bg-blue-600 hover:bg-blue-700 text-white shadow-sm transition-all duration-200 hover:shadow-md"
174
+ >
175
+ <SparklesIcon className="h-4 w-4 mr-2" />
176
+ Request AI Review
177
+ </Button>
178
+ ) : (
179
+ <p className="text-sm text-gray-500">
180
+ Only the package owner can generate an AI review
181
+ </p>
182
+ )}
183
+ </div>
184
+ </div>
142
185
  )
143
186
  }
144
187
 
145
188
  if (!aiReviewText && aiReviewRequested) {
146
189
  return (
147
- <p className="text-sm">AI review requested. Please check back later.</p>
190
+ <div className="flex flex-col items-center justify-center py-8 px-4">
191
+ <div className="text-center space-y-4 max-w-md">
192
+ <div className="flex justify-center">
193
+ <div className="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center">
194
+ <Loader2 className="h-6 w-6 text-gray-600 animate-spin" />
195
+ </div>
196
+ </div>
197
+ <div className="space-y-2">
198
+ <p className="text-sm text-gray-600">
199
+ Our AI is analyzing your package. This usually takes a few
200
+ minutes. Please check back shortly.
201
+ </p>
202
+ </div>
203
+ </div>
204
+ </div>
148
205
  )
149
206
  }
150
207
 
@@ -162,6 +219,7 @@ export default function ImportantFilesView({
162
219
  package_release_id: partialActiveFile.package_release_id,
163
220
  }
164
221
  : null,
222
+ { keepPreviousData: true },
165
223
  )
166
224
  const activeFileContent = activeFileFull?.content_text || ""
167
225
 
@@ -210,14 +268,16 @@ export default function ImportantFilesView({
210
268
  )
211
269
  }
212
270
 
271
+ const isOwner = user?.github_username === packageAuthorOwner
272
+
213
273
  return (
214
274
  <div className="mt-4 border border-gray-200 dark:border-[#30363d] rounded-md overflow-hidden">
215
275
  <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]">
216
- <div className="flex items-center space-x-2 overflow-x-auto no-scrollbar">
276
+ <div className="flex items-center space-x-2 overflow-x-auto no-scrollbar flex-1 min-w-0">
217
277
  {/* AI Description Tab */}
218
278
  {hasAiContent && (
219
279
  <button
220
- className={`flex items-center px-3 py-1.5 rounded-md text-xs ${
280
+ className={`flex items-center px-3 py-1.5 rounded-md text-xs flex-shrink-0 whitespace-nowrap ${
221
281
  activeTab === "ai"
222
282
  ? "bg-gray-200 dark:bg-[#30363d] font-medium"
223
283
  : "text-gray-500 dark:text-[#8b949e] hover:bg-gray-200 dark:hover:bg-[#30363d]"
@@ -234,7 +294,7 @@ export default function ImportantFilesView({
234
294
 
235
295
  {/* AI Review Tab */}
236
296
  <button
237
- className={`flex items-center px-3 py-1.5 rounded-md text-xs ${
297
+ className={`flex items-center px-3 py-1.5 rounded-md text-xs flex-shrink-0 whitespace-nowrap ${
238
298
  activeTab === "ai-review"
239
299
  ? "bg-gray-200 dark:bg-[#30363d] font-medium"
240
300
  : "text-gray-500 dark:text-[#8b949e] hover:bg-gray-200 dark:hover:bg-[#30363d]"
@@ -252,7 +312,7 @@ export default function ImportantFilesView({
252
312
  {importantFiles.map((file) => (
253
313
  <button
254
314
  key={file.package_file_id}
255
- className={`flex items-center px-3 py-1.5 rounded-md text-xs ${
315
+ className={`flex items-center px-3 py-1.5 rounded-md text-xs flex-shrink-0 whitespace-nowrap ${
256
316
  activeTab === "file" && activeFilePath === file.file_path
257
317
  ? "bg-gray-200 dark:bg-[#30363d] font-medium"
258
318
  : "text-gray-500 dark:text-[#8b949e] hover:bg-gray-200 dark:hover:bg-[#30363d]"
@@ -268,7 +328,8 @@ export default function ImportantFilesView({
268
328
  ))}
269
329
  </div>
270
330
  <div className="ml-auto flex items-center">
271
- {activeFileContent && (
331
+ {((activeTab === "file" && activeFileContent) ||
332
+ (activeTab === "ai-review" && aiReviewText)) && (
272
333
  <button
273
334
  className="hover:bg-gray-200 dark:hover:bg-[#30363d] p-1 rounded-md transition-all duration-300"
274
335
  onClick={handleCopy}
@@ -281,13 +342,25 @@ export default function ImportantFilesView({
281
342
  <span className="sr-only">Copy</span>
282
343
  </button>
283
344
  )}
284
- <button
285
- className="hover:bg-gray-200 dark:hover:bg-[#30363d] p-1 rounded-md"
286
- onClick={() => onEditClicked?.(activeFilePath)}
287
- >
288
- <Edit className="h-4 w-4" />
289
- <span className="sr-only">Edit</span>
290
- </button>
345
+ {activeTab === "ai-review" && aiReviewText && isOwner && (
346
+ <button
347
+ className="hover:bg-gray-200 dark:hover:bg-[#30363d] p-1 rounded-md ml-1"
348
+ onClick={onRequestAiReview}
349
+ title="Re-request AI Review"
350
+ >
351
+ <RefreshCcwIcon className="h-4 w-4" />
352
+ <span className="sr-only">Re-request AI Review</span>
353
+ </button>
354
+ )}
355
+ {activeTab === "file" && (
356
+ <button
357
+ className="hover:bg-gray-200 dark:hover:bg-[#30363d] p-1 rounded-md"
358
+ onClick={() => onEditClicked?.(activeFilePath)}
359
+ >
360
+ <Edit className="h-4 w-4" />
361
+ <span className="sr-only">Edit</span>
362
+ </button>
363
+ )}
291
364
  </div>
292
365
  </div>
293
366
  <div className="p-4 bg-white dark:bg-[#0d1117]">
@@ -295,7 +368,9 @@ export default function ImportantFilesView({
295
368
  renderAiContent()
296
369
  ) : activeTab === "ai-review" ? (
297
370
  renderAiReviewContent()
298
- ) : activeFilePath && activeFilePath.endsWith(".md") ? (
371
+ ) : activeFilePath &&
372
+ (activeFilePath.endsWith(".md") ||
373
+ activeFilePath?.toLowerCase().endsWith("readme")) ? (
299
374
  <MarkdownViewer markdownContent={activeFileContent} />
300
375
  ) : activeFilePath &&
301
376
  (activeFilePath.endsWith(".js") ||
@@ -12,6 +12,7 @@ import {
12
12
  Pencil,
13
13
  GitForkIcon,
14
14
  DownloadIcon,
15
+ Package2,
15
16
  } from "lucide-react"
16
17
  import MainContentViewSelector from "./main-content-view-selector"
17
18
  import {
@@ -24,8 +25,11 @@ import {
24
25
  import { DownloadButtonAndMenu } from "@/components/DownloadButtonAndMenu"
25
26
  import { useCurrentPackageCircuitJson } from "../hooks/use-current-package-circuit-json"
26
27
  import { useLocation } from "wouter"
27
- import { Package } from "fake-snippets-api/lib/db/schema"
28
+ import { Package, PackageFile } from "fake-snippets-api/lib/db/schema"
29
+ import { usePackageFiles } from "@/hooks/use-package-files"
30
+ import { useDownloadZip } from "@/hooks/use-download-zip"
28
31
  interface MainContentHeaderProps {
32
+ packageFiles: PackageFile[]
29
33
  activeView: string
30
34
  onViewChange: (view: string) => void
31
35
  onExportClicked?: (exportType: string) => void
@@ -33,6 +37,7 @@ interface MainContentHeaderProps {
33
37
  }
34
38
 
35
39
  export default function MainContentHeader({
40
+ packageFiles,
36
41
  activeView,
37
42
  onViewChange,
38
43
  packageInfo,
@@ -59,6 +64,14 @@ export default function MainContentHeader({
59
64
  setTimeout(() => setCopyCloneState("copy"), 2000)
60
65
  }
61
66
 
67
+ const { downloadZip } = useDownloadZip()
68
+
69
+ const handleDownloadZip = () => {
70
+ if (packageInfo && packageFiles) {
71
+ downloadZip(packageInfo, packageFiles)
72
+ }
73
+ }
74
+
62
75
  const { circuitJson } = useCurrentPackageCircuitJson()
63
76
 
64
77
  return (
@@ -70,7 +83,9 @@ export default function MainContentHeader({
70
83
 
71
84
  <div className="flex space-x-2">
72
85
  <DownloadButtonAndMenu
73
- snippetUnscopedName={packageInfo?.unscoped_name}
86
+ unscopedName={packageInfo?.unscoped_name}
87
+ desiredImageType={activeView}
88
+ author={packageInfo?.owner_github_username ?? undefined}
74
89
  circuitJson={circuitJson}
75
90
  />
76
91
 
@@ -87,7 +102,7 @@ export default function MainContentHeader({
87
102
  </Button>
88
103
  </DropdownMenuTrigger>
89
104
  <DropdownMenuContent align="end" className="w-72">
90
- <DropdownMenuItem asChild>
105
+ <DropdownMenuItem disabled={!Boolean(packageInfo)} asChild>
91
106
  <a
92
107
  href={`/editor?package_id=${packageInfo?.package_id}`}
93
108
  className="cursor-pointer p-2 py-4"
@@ -96,6 +111,15 @@ export default function MainContentHeader({
96
111
  Edit Online
97
112
  </a>
98
113
  </DropdownMenuItem>
114
+
115
+ <DropdownMenuItem
116
+ disabled={!Boolean(packageInfo)}
117
+ onClick={handleDownloadZip}
118
+ className="cursor-pointer p-2 py-4"
119
+ >
120
+ <Package2 className="h-4 w-4 mx-3" />
121
+ Download ZIP
122
+ </DropdownMenuItem>
99
123
  <DropdownMenuSeparator />
100
124
 
101
125
  {/* Install Option */}
@@ -41,8 +41,8 @@ const MobileSidebar = ({
41
41
  const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
42
42
  const isOwner =
43
43
  isLoggedIn &&
44
- packageInfo?.creator_account_id ===
45
- useGlobalStore((s) => s.session?.account_id)
44
+ packageInfo?.owner_github_username ===
45
+ useGlobalStore((s) => s.session?.github_username)
46
46
 
47
47
  const {
48
48
  Dialog: EditPackageDetailsDialog,