@tscircuit/fake-snippets 0.0.100 → 0.0.102

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 (61) hide show
  1. package/api/generated-index.js +23 -1
  2. package/bun.lock +2 -2
  3. package/dist/bundle.js +620 -412
  4. package/dist/index.d.ts +33 -4
  5. package/dist/index.js +43 -1
  6. package/dist/schema.d.ts +94 -1
  7. package/dist/schema.js +17 -1
  8. package/fake-snippets-api/lib/db/db-client.ts +38 -1
  9. package/fake-snippets-api/lib/db/schema.ts +15 -0
  10. package/fake-snippets-api/lib/public-mapping/public-map-package.ts +2 -0
  11. package/fake-snippets-api/routes/api/accounts/search.ts +20 -0
  12. package/fake-snippets-api/routes/api/github/installations/create_new_installation_redirect.ts +75 -0
  13. package/fake-snippets-api/routes/api/github/repos/list_available.ts +91 -0
  14. package/fake-snippets-api/routes/api/packages/update.ts +4 -0
  15. package/package.json +2 -2
  16. package/src/App.tsx +10 -1
  17. package/src/components/CmdKMenu.tsx +154 -19
  18. package/src/components/CreateReleaseDialog.tsx +124 -0
  19. package/src/components/FileSidebar.tsx +128 -23
  20. package/src/components/Header2.tsx +106 -25
  21. package/src/components/PackageBuildsPage/package-build-header.tsx +28 -16
  22. package/src/components/PageSearchComponent.tsx +2 -2
  23. package/src/components/SearchComponent.tsx +2 -2
  24. package/src/components/SuspenseRunFrame.tsx +2 -2
  25. package/src/components/TrendingPackagesCarousel.tsx +2 -2
  26. package/src/components/ViewPackagePage/components/important-files-view.tsx +18 -13
  27. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +1 -0
  28. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +1 -0
  29. package/src/components/dialogs/GitHubRepositorySelector.tsx +123 -0
  30. package/src/components/dialogs/create-use-dialog.tsx +8 -2
  31. package/src/components/dialogs/edit-package-details-dialog.tsx +22 -3
  32. package/src/components/dialogs/view-ts-files-dialog.tsx +178 -33
  33. package/src/components/package-port/CodeAndPreview.tsx +4 -1
  34. package/src/components/package-port/CodeEditor.tsx +42 -35
  35. package/src/components/package-port/CodeEditorHeader.tsx +26 -20
  36. package/src/components/package-port/EditorNav.tsx +94 -37
  37. package/src/components/preview/BuildsList.tsx +238 -0
  38. package/src/components/preview/ConnectedRepoDashboard.tsx +258 -0
  39. package/src/components/preview/ConnectedRepoOverview.tsx +454 -0
  40. package/src/components/preview/ConnectedRepoSettings.tsx +343 -0
  41. package/src/components/preview/ConnectedReposCards.tsx +191 -0
  42. package/src/components/preview/index.tsx +207 -0
  43. package/src/components/ui/tree-view.tsx +23 -6
  44. package/src/hooks/use-axios.ts +2 -2
  45. package/src/hooks/use-create-release-dialog.ts +160 -0
  46. package/src/hooks/use-package-details-form.ts +7 -0
  47. package/src/hooks/use-packages-base-api-url.ts +1 -1
  48. package/src/hooks/use-sign-in.ts +2 -2
  49. package/src/hooks/useFileManagement.ts +22 -2
  50. package/src/index.css +4 -0
  51. package/src/lib/utils/formatTimeAgo.ts +10 -0
  52. package/src/lib/utils/isValidFileName.ts +15 -3
  53. package/src/pages/dashboard.tsx +2 -2
  54. package/src/pages/dev-login.tsx +2 -2
  55. package/src/pages/landing.tsx +1 -1
  56. package/src/pages/latest.tsx +2 -2
  57. package/src/pages/preview-build.tsx +380 -0
  58. package/src/pages/search.tsx +2 -2
  59. package/src/pages/trending.tsx +2 -2
  60. package/src/pages/user-profile.tsx +32 -24
  61. package/src/pages/view-connected-repo.tsx +24 -0
@@ -57,16 +57,24 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
57
57
  handleRenameFile,
58
58
  isCreatingFile,
59
59
  setIsCreatingFile,
60
- pkg,
61
60
  }) => {
62
61
  const [sidebarOpen, setSidebarOpen] = fileSidebarState
63
62
  const [newFileName, setNewFileName] = useState("")
64
63
  const [errorMessage, setErrorMessage] = useState("")
65
64
  const [renamingFile, setRenamingFile] = useState<string | null>(null)
65
+ const [selectedFolderForCreation, setSelectedFolderForCreation] = useState<
66
+ string | null
67
+ >(null)
68
+ const [selectedItemId, setSelectedItemId] = React.useState<string>(
69
+ currentFile || "",
70
+ )
66
71
  const { toast } = useToast()
67
- const session = useGlobalStore((s) => s.session)
68
72
  const canModifyFiles = true
69
73
 
74
+ const onFolderSelect = (folderPath: string) => {
75
+ setSelectedFolderForCreation(folderPath)
76
+ }
77
+
70
78
  const transformFilesToTreeData = (
71
79
  files: Record<FileName, string>,
72
80
  ): TreeDataItem[] => {
@@ -90,6 +98,7 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
90
98
  : segment
91
99
  const absolutePath = hasLeadingSlash ? `/${relativePath}` : relativePath
92
100
  const itemId = absolutePath
101
+
93
102
  if (
94
103
  !currentNode[segment] &&
95
104
  (!isHiddenFile(relativePath) ||
@@ -104,21 +113,17 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
104
113
  name: segment,
105
114
  isRenaming: renamingFile === itemId,
106
115
  onRename: (newFilename: string) => {
107
- // Preserve the folder structure when renaming
108
116
  const oldPath = itemId
109
- const pathParts = oldPath.split("/").filter((part) => part !== "") // Remove empty segments
117
+ const pathParts = oldPath.split("/").filter((part) => part !== "")
110
118
  let newPath: string
111
119
 
112
120
  if (pathParts.length > 1) {
113
- // File is in a folder, preserve the folder structure
114
121
  const folderPath = pathParts.slice(0, -1).join("/")
115
122
  newPath = folderPath + "/" + newFilename
116
123
  } else {
117
- // File is in root, just use the new filename
118
124
  newPath = newFilename
119
125
  }
120
126
 
121
- // Preserve leading slash if original path had one
122
127
  if (oldPath.startsWith("/") && !newPath.startsWith("/")) {
123
128
  newPath = "/" + newPath
124
129
  }
@@ -142,7 +147,12 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
142
147
  setRenamingFile(null)
143
148
  },
144
149
  icon: isLeafNode ? File : Folder,
145
- onClick: isLeafNode ? () => onFileSelect(absolutePath) : undefined,
150
+ onClick: isLeafNode
151
+ ? () => {
152
+ onFileSelect(absolutePath)
153
+ setSelectedFolderForCreation(null)
154
+ }
155
+ : () => onFolderSelect(absolutePath),
146
156
  draggable: false,
147
157
  droppable: !isLeafNode,
148
158
  children: isLeafNode ? undefined : {},
@@ -209,7 +219,6 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
209
219
  })
210
220
  })
211
221
 
212
- // Convert the nested object structure to array structure
213
222
  const convertToArray = (
214
223
  items: Record<string, TreeNode>,
215
224
  ): TreeDataItem[] => {
@@ -226,17 +235,70 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
226
235
 
227
236
  const treeData = transformFilesToTreeData(files)
228
237
 
238
+ const getCurrentFolderPath = (): string => {
239
+ if (selectedFolderForCreation) {
240
+ return selectedFolderForCreation
241
+ }
242
+
243
+ if (!selectedItemId || selectedItemId === "") return ""
244
+
245
+ const hasLeadingSlash = selectedItemId.startsWith("/")
246
+ const normalizedPath = hasLeadingSlash
247
+ ? selectedItemId.slice(1)
248
+ : selectedItemId
249
+ const pathParts = selectedItemId.split("/")
250
+
251
+ if (pathParts.length > 1) {
252
+ const folderPath = pathParts.slice(0, -1).join("/")
253
+ return hasLeadingSlash ? `/${folderPath}` : folderPath
254
+ }
255
+
256
+ return hasLeadingSlash ? "/" : ""
257
+ }
258
+
259
+ const constructFilePath = (fileName: string): string => {
260
+ const trimmedFileName = fileName.trim()
261
+
262
+ if (!trimmedFileName) {
263
+ return ""
264
+ }
265
+
266
+ const currentFolder = getCurrentFolderPath()
267
+
268
+ if (trimmedFileName.startsWith("/")) {
269
+ return trimmedFileName
270
+ }
271
+
272
+ if (!currentFolder || currentFolder === "/") {
273
+ const result =
274
+ currentFolder === "/" ? `/${trimmedFileName}` : trimmedFileName
275
+ return result
276
+ }
277
+
278
+ const result = `${currentFolder}/${trimmedFileName}`
279
+ return result
280
+ }
229
281
  const handleCreateFileInline = () => {
282
+ const finalFileName = constructFilePath(newFileName)
283
+ if (!finalFileName) {
284
+ setErrorMessage("File name cannot be empty")
285
+ return
286
+ }
287
+
230
288
  const { newFileCreated } = handleCreateFile({
231
- newFileName,
289
+ newFileName: finalFileName,
232
290
  onError: (error) => {
233
291
  setErrorMessage(error.message)
234
292
  },
235
293
  })
294
+
236
295
  if (newFileCreated) {
237
296
  setIsCreatingFile(false)
238
297
  setNewFileName("")
239
298
  setErrorMessage("")
299
+ onFileSelect(finalFileName)
300
+ setSelectedItemId(finalFileName)
301
+ setSelectedFolderForCreation(null)
240
302
  }
241
303
  }
242
304
 
@@ -245,6 +307,7 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
245
307
  setIsCreatingFile(false)
246
308
  setNewFileName("")
247
309
  setErrorMessage("")
310
+ setSelectedFolderForCreation(null)
248
311
  return
249
312
  }
250
313
  handleCreateFileInline()
@@ -255,6 +318,7 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
255
318
  setErrorMessage("")
256
319
  setIsCreatingFile(false)
257
320
  setNewFileName("")
321
+ setSelectedFolderForCreation(null)
258
322
  }
259
323
 
260
324
  return (
@@ -267,9 +331,7 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
267
331
  >
268
332
  <button
269
333
  onClick={toggleSidebar}
270
- className={`z-[99] mt-2 ml-2 text-gray-400 scale-90 transition-opacity duration-200 ${
271
- !sidebarOpen ? "opacity-0 pointer-events-none" : "opacity-100"
272
- }`}
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"}`}
273
335
  >
274
336
  <PanelRightOpen />
275
337
  </button>
@@ -286,33 +348,76 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
286
348
  autoFocus
287
349
  value={newFileName}
288
350
  spellCheck={false}
289
- onChange={(e) => setNewFileName(e.target.value)}
351
+ onChange={(e) => {
352
+ setNewFileName(e.target.value)
353
+ if (errorMessage) {
354
+ setErrorMessage("")
355
+ }
356
+ }}
290
357
  onBlur={handleCreateFileBlur}
291
358
  onKeyDown={(e) => {
292
359
  if (e.key === "Enter") {
360
+ e.preventDefault()
293
361
  handleCreateFileInline()
294
362
  } else if (e.key === "Escape") {
363
+ e.preventDefault()
295
364
  setIsCreatingFile(false)
296
365
  setNewFileName("")
297
366
  setErrorMessage("")
367
+ setSelectedFolderForCreation(null)
368
+ } else if (e.key === "Tab") {
369
+ e.preventDefault()
370
+ const currentFolder = getCurrentFolderPath()
371
+ if (currentFolder && !newFileName.includes("/")) {
372
+ const displayPath = currentFolder.startsWith("/")
373
+ ? currentFolder.slice(1)
374
+ : currentFolder
375
+ setNewFileName(`${displayPath}/`)
376
+ }
298
377
  }
299
378
  }}
300
- placeholder="Enter file name"
379
+ placeholder={(() => {
380
+ const currentFolder = getCurrentFolderPath()
381
+ if (!currentFolder || currentFolder === "/") {
382
+ return "Enter file name (root folder)"
383
+ }
384
+ const displayPath = currentFolder.startsWith("/")
385
+ ? currentFolder.slice(1)
386
+ : currentFolder
387
+ return `Enter file name (${displayPath}/)`
388
+ })()}
389
+ className={
390
+ errorMessage ? "border-red-500 focus:border-red-500" : ""
391
+ }
301
392
  />
302
393
  {errorMessage && (
303
- <div className="text-red-500 mt-1">{errorMessage}</div>
394
+ <div className="text-red-500 text-xs mt-1 px-1">{errorMessage}</div>
304
395
  )}
396
+ <div className="text-gray-400 text-xs mt-1 px-1">
397
+ Tip: Use / for subfolders, Tab to auto-complete current folder
398
+ </div>
305
399
  </div>
306
400
  )}
307
- <TreeView
308
- data={treeData}
309
- initialSelectedItemId={currentFile || ""}
310
- onSelectChange={(item) => {
311
- if (item?.onClick) {
312
- item.onClick()
401
+ <div
402
+ onClick={(e) => {
403
+ if (e.target === e.currentTarget) {
404
+ setSelectedFolderForCreation(null)
405
+ setSelectedItemId("")
313
406
  }
314
407
  }}
315
- />
408
+ className="flex-1 border-2 h-full"
409
+ >
410
+ <TreeView
411
+ data={treeData}
412
+ setSelectedItemId={(value) => setSelectedItemId(value || "")}
413
+ selectedItemId={selectedItemId}
414
+ onSelectChange={(item) => {
415
+ if (item?.onClick) {
416
+ item.onClick()
417
+ }
418
+ }}
419
+ />
420
+ </div>
316
421
  </div>
317
422
  )
318
423
  }
@@ -1,5 +1,5 @@
1
1
  import { Button } from "@/components/ui/button"
2
- import { CircuitBoard, Search } from "lucide-react"
2
+ import { CircuitBoard, Search, Menu, X } from "lucide-react"
3
3
  import { Link } from "wouter"
4
4
  import { PrefetchPageLink } from "./PrefetchPageLink"
5
5
  import { HeaderLogin } from "./HeaderLogin"
@@ -53,10 +53,12 @@ const SearchButtonComponent = () => {
53
53
  }
54
54
 
55
55
  export const Header2 = () => {
56
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
56
57
  const isLoggedIn = useGlobalStore((state) => Boolean(state.session))
58
+
57
59
  return (
58
60
  <>
59
- <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
61
+ <header className="sticky top-0 z-50 w-full border-b bg-white md:bg-white/80 md:backdrop-blur supports-[backdrop-filter]:md:bg-white/60">
60
62
  <div className="container mx-auto flex h-16 items-center justify-between px-2 md:px-6">
61
63
  <PrefetchPageLink
62
64
  href="/"
@@ -65,23 +67,17 @@ export const Header2 = () => {
65
67
  <CircuitBoard className="h-6 w-6" />
66
68
  <span className="text-lg font-bold">tscircuit</span>
67
69
  </PrefetchPageLink>
68
- <nav className="flex md:hidden">
70
+
71
+ {/* Desktop Navigation */}
72
+ <nav className="hidden md:flex gap-6">
69
73
  {isLoggedIn && (
70
- <Link
74
+ <PrefetchPageLink
71
75
  className="text-sm font-medium hover:underline underline-offset-4"
72
76
  href="/dashboard"
73
77
  >
74
78
  Dashboard
75
- </Link>
79
+ </PrefetchPageLink>
76
80
  )}
77
- </nav>
78
- <nav className="hidden md:flex gap-6">
79
- <PrefetchPageLink
80
- className="text-sm font-medium hover:underline underline-offset-4"
81
- href="/dashboard"
82
- >
83
- Dashboard
84
- </PrefetchPageLink>
85
81
  <PrefetchPageLink
86
82
  className="text-sm font-medium hover:underline underline-offset-4"
87
83
  href="/quickstart"
@@ -94,12 +90,6 @@ export const Header2 = () => {
94
90
  >
95
91
  Datasheets
96
92
  </PrefetchPageLink>
97
- {/* <a
98
- className="text-sm font-medium hover:underline underline-offset-4"
99
- href="https://github.com/tscircuit/tscircuit"
100
- >
101
- Github
102
- </a> */}
103
93
  <a
104
94
  className="text-sm font-medium hover:underline underline-offset-4"
105
95
  href="https://docs.tscircuit.com"
@@ -119,16 +109,107 @@ export const Header2 = () => {
119
109
  Contact
120
110
  </a>
121
111
  </nav>
122
- <div className="flex items-center gap-4">
112
+
113
+ {/* Desktop Right Side */}
114
+ <div className="hidden md:flex items-center gap-4">
123
115
  <SearchButtonComponent />
124
- {isLoggedIn && (
125
- <div className="hidden sm:block">
126
- <HeaderDropdown />
127
- </div>
128
- )}
116
+ {isLoggedIn && <HeaderDropdown />}
129
117
  <HeaderLogin />
130
118
  </div>
119
+
120
+ {/* Mobile Right Side */}
121
+ <div className="flex md:hidden items-center gap-2">
122
+ <Button
123
+ variant="ghost"
124
+ size="icon"
125
+ onClick={() => (window.location.href = "/search")}
126
+ className="h-8 w-8"
127
+ aria-label="Go to search"
128
+ >
129
+ <Search className="size-4" />
130
+ </Button>
131
+ {isLoggedIn ? (
132
+ <HeaderLogin />
133
+ ) : (
134
+ <PrefetchPageLink href="/quickstart">
135
+ <Button size="sm" className="text-xs px-3 py-1">
136
+ Get Started
137
+ </Button>
138
+ </PrefetchPageLink>
139
+ )}
140
+ <button
141
+ className="p-2"
142
+ onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
143
+ aria-label={mobileMenuOpen ? "Close menu" : "Open menu"}
144
+ >
145
+ {mobileMenuOpen ? <X size={20} /> : <Menu size={20} />}
146
+ </button>
147
+ </div>
131
148
  </div>
149
+
150
+ {/* Mobile Menu */}
151
+ {mobileMenuOpen && (
152
+ <div className="md:hidden border-t bg-white ">
153
+ <div className="container mx-auto px-4 py-3">
154
+ <nav className="mb-4">
155
+ <div className="flex flex-col items-center gap-1">
156
+ {isLoggedIn && (
157
+ <PrefetchPageLink
158
+ className="text-sm font-medium hover:underline underline-offset-4 py-2 w-full text-center"
159
+ href="/dashboard"
160
+ onClick={() => setMobileMenuOpen(false)}
161
+ >
162
+ Dashboard
163
+ </PrefetchPageLink>
164
+ )}
165
+ <PrefetchPageLink
166
+ className="text-sm font-medium hover:underline underline-offset-4 py-2 w-full text-center"
167
+ href="/quickstart"
168
+ onClick={() => setMobileMenuOpen(false)}
169
+ >
170
+ Editor
171
+ </PrefetchPageLink>
172
+ <PrefetchPageLink
173
+ className="text-sm font-medium hover:underline underline-offset-4 py-2 w-full text-center"
174
+ href="/datasheets"
175
+ onClick={() => setMobileMenuOpen(false)}
176
+ >
177
+ Datasheets
178
+ </PrefetchPageLink>
179
+ <a
180
+ className="text-sm font-medium hover:underline underline-offset-4 py-2 w-full text-center"
181
+ href="https://docs.tscircuit.com"
182
+ >
183
+ Docs
184
+ </a>
185
+ <a
186
+ className="text-sm font-medium hover:underline underline-offset-4 py-2 w-full text-center"
187
+ href="https://tscircuit.com/join"
188
+ >
189
+ Discord
190
+ </a>
191
+ <a
192
+ className="text-sm font-medium hover:underline underline-offset-4 py-2 w-full text-center"
193
+ href="mailto:hello@tscircuit.com"
194
+ >
195
+ Contact
196
+ </a>
197
+ </div>
198
+ </nav>
199
+ <div className="flex flex-col items-center gap-4 pt-4 border-t border-gray-200">
200
+ {isLoggedIn ? (
201
+ <div className="w-full flex justify-center">
202
+ <HeaderDropdown />
203
+ </div>
204
+ ) : (
205
+ <div className="w-full flex justify-center">
206
+ <HeaderLogin />
207
+ </div>
208
+ )}
209
+ </div>
210
+ </div>
211
+ </div>
212
+ )}
132
213
  </header>
133
214
  <CmdKMenu />
134
215
  <Analytics />
@@ -4,15 +4,20 @@ import { useRebuildPackageReleaseMutation } from "@/hooks/use-rebuild-package-re
4
4
  import { Github, RefreshCw } from "lucide-react"
5
5
  import { useParams } from "wouter"
6
6
  import { DownloadButtonAndMenu } from "../DownloadButtonAndMenu"
7
+ import { useGlobalStore } from "@/hooks/use-global-store"
8
+ import { useCurrentPackageCircuitJson } from "../ViewPackagePage/hooks/use-current-package-circuit-json"
7
9
 
8
10
  export function PackageBuildHeader() {
9
11
  const { author, packageName } = useParams()
12
+ const session = useGlobalStore((s) => s.session)
10
13
  const { packageRelease } = useCurrentPackageRelease({
11
14
  include_logs: true,
12
15
  })
13
16
  const { mutate: rebuildPackage, isLoading } =
14
17
  useRebuildPackageReleaseMutation()
15
18
 
19
+ const { circuitJson } = useCurrentPackageCircuitJson()
20
+
16
21
  return (
17
22
  <div className="border-b border-gray-200 bg-white px-4 sm:px-6 py-4">
18
23
  <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4 container mx-auto max-w-7xl">
@@ -44,22 +49,29 @@ export function PackageBuildHeader() {
44
49
  <span className="xs:hidden">Report</span>
45
50
  </a>
46
51
  </Button>
47
- <Button
48
- variant="outline"
49
- size="sm"
50
- className="border-gray-300 bg-white hover:bg-gray-50 text-xs sm:text-sm"
51
- disabled={isLoading || !packageRelease}
52
- onClick={() =>
53
- packageRelease &&
54
- rebuildPackage({
55
- package_release_id: packageRelease.package_release_id,
56
- })
57
- }
58
- >
59
- <RefreshCw className="w-3 h-3 sm:w-4 sm:h-4 mr-1 sm:mr-2" />
60
- {isLoading ? "Rebuilding..." : "Rebuild"}
61
- </Button>
62
- <DownloadButtonAndMenu unscopedName={packageName} author={author} />
52
+ {session?.github_username == author && (
53
+ <Button
54
+ variant="outline"
55
+ size="sm"
56
+ className="border-gray-300 bg-white hover:bg-gray-50 text-xs sm:text-sm"
57
+ disabled={isLoading || !packageRelease}
58
+ onClick={() =>
59
+ packageRelease &&
60
+ rebuildPackage({
61
+ package_release_id: packageRelease.package_release_id,
62
+ })
63
+ }
64
+ >
65
+ <RefreshCw className="w-3 h-3 sm:w-4 sm:h-4 mr-1 sm:mr-2" />
66
+ {isLoading ? "Rebuilding..." : "Rebuild"}
67
+ </Button>
68
+ )}
69
+ <DownloadButtonAndMenu
70
+ offerMultipleImageFormats
71
+ circuitJson={circuitJson}
72
+ unscopedName={packageName}
73
+ author={author}
74
+ />
63
75
  </div>
64
76
  </div>
65
77
  </div>
@@ -3,7 +3,7 @@ import { useAxios } from "@/hooks/use-axios"
3
3
  import { useLocation } from "wouter"
4
4
  import React, { useState } from "react"
5
5
  import { useQuery } from "react-query"
6
- import { usePackagesBaseApiUrl } from "@/hooks/use-packages-base-api-url"
6
+ import { useApiBaseUrl } from "@/hooks/use-packages-base-api-url"
7
7
  import { Search } from "lucide-react"
8
8
  import { Button } from "./ui/button"
9
9
  import { PackageCardSkeleton } from "./PackageCardSkeleton"
@@ -20,7 +20,7 @@ const PageSearchComponent: React.FC<PageSearchComponentProps> = ({
20
20
  }) => {
21
21
  const [location, setLocation] = useLocation()
22
22
  const axios = useAxios()
23
- const snippetsBaseApiUrl = usePackagesBaseApiUrl()
23
+ const snippetsBaseApiUrl = useApiBaseUrl()
24
24
 
25
25
  // Initialize search query directly from URL
26
26
  const [searchQuery, setSearchQuery] = useState(
@@ -4,7 +4,7 @@ import { useLocation } from "wouter"
4
4
  import React, { useEffect, useRef, useState } from "react"
5
5
  import { useQuery } from "react-query"
6
6
  import { Alert } from "./ui/alert"
7
- import { usePackagesBaseApiUrl } from "@/hooks/use-packages-base-api-url"
7
+ import { useApiBaseUrl } from "@/hooks/use-packages-base-api-url"
8
8
  import { PrefetchPageLink } from "./PrefetchPageLink"
9
9
  import { CircuitBoard } from "lucide-react"
10
10
  import { cn } from "@/lib/utils"
@@ -60,7 +60,7 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
60
60
  const resultsRef = useRef<HTMLDivElement>(null)
61
61
  const inputRef = useRef<HTMLInputElement>(null)
62
62
  const [location, setLocation] = useLocation()
63
- const snippetsBaseApiUrl = usePackagesBaseApiUrl()
63
+ const snippetsBaseApiUrl = useApiBaseUrl()
64
64
 
65
65
  const { data: searchResults, isLoading } = useQuery(
66
66
  ["packageSearch", searchQuery],
@@ -6,7 +6,7 @@ const RunFrame = lazy(async () => {
6
6
  })
7
7
 
8
8
  export const SuspenseRunFrame = (
9
- props: React.ComponentProps<typeof RunFrame>,
9
+ props: React.ComponentProps<typeof RunFrame> & { className?: string },
10
10
  ) => {
11
11
  return (
12
12
  <Suspense
@@ -20,7 +20,7 @@ export const SuspenseRunFrame = (
20
20
  </div>
21
21
  }
22
22
  >
23
- <div className="h-[98vh]">
23
+ <div className={`h-[98vh] ${props.className}`}>
24
24
  <RunFrame {...props} />
25
25
  </div>
26
26
  </Suspense>
@@ -4,7 +4,7 @@ import { StarFilledIcon } from "@radix-ui/react-icons"
4
4
  import { Link } from "wouter"
5
5
  import { Package } from "fake-snippets-api/lib/db/schema"
6
6
  import { useRef, useState } from "react"
7
- import { usePackagesBaseApiUrl } from "@/hooks/use-packages-base-api-url"
7
+ import { useApiBaseUrl } from "@/hooks/use-packages-base-api-url"
8
8
 
9
9
  const CarouselItem = ({
10
10
  pkg,
@@ -36,7 +36,7 @@ export const TrendingPackagesCarousel = () => {
36
36
  const axios = useAxios()
37
37
  const scrollRef = useRef<HTMLDivElement>(null)
38
38
  const [isHovered, setIsHovered] = useState(false)
39
- const apiBaseUrl = usePackagesBaseApiUrl()
39
+ const apiBaseUrl = useApiBaseUrl()
40
40
 
41
41
  const { data: trendingPackages } = useQuery<Package[]>(
42
42
  "trendingPackages",
@@ -17,6 +17,7 @@ import { usePackageFile } from "@/hooks/use-package-files"
17
17
  import { ShikiCodeViewer } from "./ShikiCodeViewer"
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
@@ -129,14 +130,12 @@ export default function ImportantFilesView({
129
130
  const availableTabs = useMemo((): TabInfo[] => {
130
131
  const tabs: TabInfo[] = []
131
132
 
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
- }
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
+ })
140
139
 
141
140
  tabs.push({
142
141
  type: "ai-review",
@@ -266,6 +265,8 @@ export default function ImportantFilesView({
266
265
  }
267
266
  }, [activeTab, importantFiles, getDefaultTab])
268
267
 
268
+ const { circuitJson } = useCurrentPackageCircuitJson()
269
+
269
270
  // Get active file content
270
271
  const partialActiveFile = useMemo(() => {
271
272
  if (activeTab?.type !== "file" || !activeTab.filePath) return null
@@ -321,7 +322,15 @@ export default function ImportantFilesView({
321
322
  from our AI assistant.
322
323
  </p>
323
324
  </div>
324
- {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
+ ) : (
325
334
  <Button
326
335
  onClick={onRequestAiReview}
327
336
  size="sm"
@@ -330,10 +339,6 @@ export default function ImportantFilesView({
330
339
  <SparklesIcon className="h-4 w-4 mr-2" />
331
340
  Request AI Review
332
341
  </Button>
333
- ) : (
334
- <p className="text-sm text-gray-500">
335
- Only the package owner can generate an AI review
336
- </p>
337
342
  )}
338
343
  </div>
339
344
  </div>
@@ -205,6 +205,7 @@ const MobileSidebar = ({
205
205
  currentDescription={
206
206
  packageInfo.description || packageInfo?.ai_description || ""
207
207
  }
208
+ currentGithubRepoFullName={packageInfo.github_repo_full_name}
208
209
  currentLicense={currentLicense}
209
210
  currentWebsite={(packageInfo as any)?.website || ""}
210
211
  isPrivate={Boolean(packageInfo.is_private)}
@@ -171,6 +171,7 @@ export default function SidebarAboutSection({
171
171
  unscopedPackageName={packageInfo.unscoped_name}
172
172
  packageReleaseId={packageInfo.latest_package_release_id}
173
173
  packageId={packageInfo.package_id}
174
+ currentGithubRepoFullName={packageInfo.github_repo_full_name}
174
175
  currentDescription={
175
176
  packageInfo.description || packageInfo?.ai_description || ""
176
177
  }