@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
@@ -12,7 +12,10 @@ import ThreeDView from "./tab-views/3d-view"
12
12
  import PCBView from "./tab-views/pcb-view"
13
13
  import SchematicView from "./tab-views/schematic-view"
14
14
  import BOMView from "./tab-views/bom-view"
15
- import { isPackageFileImportant } from "../utils/is-package-file-important"
15
+ import {
16
+ isPackageFileImportant,
17
+ scorePackageFileImportance,
18
+ } from "../utils/is-package-file-important"
16
19
  import Header from "@/components/Header"
17
20
  import Footer from "@/components/Footer"
18
21
  import PackageHeader from "./package-header"
@@ -21,6 +24,9 @@ import { useLocation } from "wouter"
21
24
  import { Package } from "fake-snippets-api/lib/db/schema"
22
25
  import { useCurrentPackageCircuitJson } from "../hooks/use-current-package-circuit-json"
23
26
  import { useRequestAiReviewMutation } from "@/hooks/use-request-ai-review-mutation"
27
+ import { useAiReview } from "@/hooks/use-ai-review"
28
+ import { useQueryClient } from "react-query"
29
+ import SidebarReleasesSection from "./sidebar-releases-section"
24
30
 
25
31
  interface PackageFile {
26
32
  package_file_id: string
@@ -48,14 +54,38 @@ export default function RepoPageContent({
48
54
  onEditClicked,
49
55
  }: RepoPageContentProps) {
50
56
  const [activeView, setActiveView] = useState<string>("files")
57
+ const [pendingAiReviewId, setPendingAiReviewId] = useState<string | null>(
58
+ null,
59
+ )
60
+ const queryClient = useQueryClient()
61
+ const { data: aiReview } = useAiReview(pendingAiReviewId, {
62
+ refetchInterval: (data) => (data && !data.ai_review_text ? 2000 : false),
63
+ })
64
+ useEffect(() => {
65
+ if (aiReview?.ai_review_text) {
66
+ queryClient.invalidateQueries(["packageRelease"])
67
+ setPendingAiReviewId(null)
68
+ }
69
+ }, [aiReview?.ai_review_text, queryClient])
51
70
  const session = useGlobalStore((s) => s.session)
52
71
  const { circuitJson, isLoading: isCircuitJsonLoading } =
53
72
  useCurrentPackageCircuitJson()
54
- const { mutate: requestAiReview } = useRequestAiReviewMutation()
73
+ const { mutate: requestAiReview, isLoading: isRequestingAiReview } =
74
+ useRequestAiReviewMutation({
75
+ onSuccess: (_packageRelease, aiReview) => {
76
+ setPendingAiReviewId(aiReview.ai_review_id)
77
+ },
78
+ })
79
+
80
+ const aiReviewRequested =
81
+ Boolean(packageRelease?.ai_review_requested) ||
82
+ Boolean(pendingAiReviewId) ||
83
+ isRequestingAiReview
55
84
 
56
85
  // Handle initial view selection and hash-based view changes
57
86
  useEffect(() => {
58
87
  if (isCircuitJsonLoading) return
88
+ if (!packageInfo) return
59
89
  const hash = window.location.hash.slice(1)
60
90
  const validViews = ["files", "3d", "pcb", "schematic", "bom"]
61
91
  const circuitDependentViews = ["3d", "pcb", "schematic", "bom"]
@@ -90,39 +120,18 @@ export default function RepoPageContent({
90
120
  const importantFiles = useMemo(() => {
91
121
  if (!packageFiles || !importantFilePaths) return []
92
122
 
93
- return packageFiles.filter((file) =>
94
- importantFilePaths.some((path) => file.file_path.endsWith(path)),
95
- )
123
+ return packageFiles
124
+ .filter((file) =>
125
+ importantFilePaths.some((path) => file.file_path.endsWith(path)),
126
+ )
127
+ .sort((a, b) => {
128
+ const aImportance = scorePackageFileImportance(a.file_path)
129
+ const bImportance = scorePackageFileImportance(b.file_path)
130
+ return aImportance - bImportance
131
+ })
132
+ .reverse()
96
133
  }, [packageFiles, importantFilePaths])
97
134
 
98
- // Generate package name with version for file lookups
99
- const packageNameWithVersion = useMemo(() => {
100
- if (!packageInfo) return ""
101
-
102
- // Format: @scope/packageName@version or packageName@version
103
- const name = packageInfo.name
104
-
105
- // Extract the latest version from the files (assuming version information is available)
106
- const versionFile = packageFiles?.find(
107
- (file) => file.file_path === "package.json",
108
- )
109
- let version = "latest"
110
-
111
- if (versionFile) {
112
- try {
113
- const content =
114
- versionFile.file_content || versionFile.content_text || "{}"
115
- const packageJson = JSON.parse(content)
116
- if (packageJson.version) {
117
- version = packageJson.version
118
- }
119
- } catch (e) {
120
- // If package.json can't be parsed, use "latest"
121
- }
122
- }
123
-
124
- return `${name}@${version}`
125
- }, [packageInfo, packageFiles])
126
135
  // Render the appropriate content based on the active view
127
136
  const renderContent = () => {
128
137
  switch (activeView) {
@@ -184,6 +193,7 @@ export default function RepoPageContent({
184
193
  {/* Main Content Header with Tabs */}
185
194
  <MainContentHeader
186
195
  activeView={activeView}
196
+ packageFiles={packageFiles ?? []}
187
197
  onViewChange={(view) => {
188
198
  setActiveView(view)
189
199
  // Update URL hash when view changes
@@ -200,10 +210,11 @@ export default function RepoPageContent({
200
210
  importantFiles={importantFiles}
201
211
  isLoading={!packageFiles}
202
212
  onEditClicked={onEditClicked}
213
+ packageAuthorOwner={packageInfo?.owner_github_username}
203
214
  aiDescription={packageInfo?.ai_description ?? ""}
204
215
  aiUsageInstructions={packageInfo?.ai_usage_instructions ?? ""}
205
216
  aiReviewText={packageRelease?.ai_review_text ?? null}
206
- aiReviewRequested={packageRelease?.ai_review_requested ?? false}
217
+ aiReviewRequested={aiReviewRequested}
207
218
  onRequestAiReview={() => {
208
219
  if (packageRelease) {
209
220
  requestAiReview({
@@ -226,6 +237,10 @@ export default function RepoPageContent({
226
237
  }}
227
238
  />
228
239
  </div>
240
+ {/* Releases section - Only visible on small screens */}
241
+ <div className="block md:hidden w-full px-5">
242
+ <SidebarReleasesSection />
243
+ </div>
229
244
  </div>
230
245
  </div>
231
246
  <Footer />
@@ -39,8 +39,8 @@ export default function SidebarAboutSection() {
39
39
  const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
40
40
  const isOwner =
41
41
  isLoggedIn &&
42
- packageInfo?.creator_account_id ===
43
- useGlobalStore((s) => s.session?.account_id)
42
+ packageInfo?.owner_github_username ===
43
+ useGlobalStore((s) => s.session?.github_username)
44
44
 
45
45
  // Local state to store updated values before the query refetches
46
46
  const [localDescription, setLocalDescription] = useState<string>("")
@@ -9,21 +9,29 @@ import type { PackageRelease } from "fake-snippets-api/lib/db/schema"
9
9
  function getTranspilationStatus(
10
10
  pr?: PackageRelease | null,
11
11
  ): BuildStep["status"] {
12
- if (!pr) return "pending"
13
- if (pr.transpilation_error) return "error"
14
- if (pr.transpilation_in_progress) return "running"
15
- if (pr.transpilation_completed_at) return "success"
16
- if (pr.transpilation_started_at) return "running"
17
- return "pending"
12
+ switch (pr?.transpilation_display_status) {
13
+ case "complete":
14
+ return "success"
15
+ case "error":
16
+ return "error"
17
+ case "building":
18
+ return "running"
19
+ default:
20
+ return "pending"
21
+ }
18
22
  }
19
23
 
20
24
  function getCircuitJsonStatus(pr?: PackageRelease | null): BuildStep["status"] {
21
- if (!pr) return "pending"
22
- if (pr.circuit_json_build_error) return "error"
23
- if (pr.circuit_json_build_in_progress) return "running"
24
- if (pr.circuit_json_build_completed_at) return "success"
25
- if (pr.circuit_json_build_started_at) return "running"
26
- return "pending"
25
+ switch (pr?.circuit_json_build_display_status) {
26
+ case "complete":
27
+ return "success"
28
+ case "error":
29
+ return "error"
30
+ case "building":
31
+ return "running"
32
+ default:
33
+ return "pending"
34
+ }
27
35
  }
28
36
 
29
37
  export default function SidebarReleasesSection() {
@@ -5,13 +5,6 @@ import { FileText, Folder } from "lucide-react"
5
5
  import { useMemo, useState } from "react"
6
6
  import { isHiddenFile } from "../../utils/is-hidden-file"
7
7
  import { isWithinDirectory } from "../../utils/is-within-directory"
8
- import {
9
- DropdownMenu,
10
- DropdownMenuContent,
11
- DropdownMenuItem,
12
- DropdownMenuTrigger,
13
- } from "@/components/ui/dropdown-menu"
14
- import { Settings } from "lucide-react"
15
8
  import HiddenFilesDropdown from "@/components/HiddenFilesDropdown"
16
9
 
17
10
  interface Directory {
@@ -0,0 +1,121 @@
1
+ export const fuzzyMatch = (
2
+ pattern: string,
3
+ text: string,
4
+ ): { score: number; matches: number[] } => {
5
+ if (!pattern) return { score: 0, matches: [] }
6
+
7
+ const normalizePattern = (pat: string) => {
8
+ return pat.toLowerCase().split(" ").join("")
9
+ }
10
+
11
+ const patternLower = normalizePattern(pattern)
12
+ const textLower = text.toLowerCase()
13
+ const matches: number[] = []
14
+
15
+ let patternIdx = 0
16
+ let score = 0
17
+ let consecutiveMatches = 0
18
+ let spaceBonus = 0
19
+
20
+ const spaceSegments = pattern.toLowerCase().trim().split(/\s+/)
21
+ const isSpaceSeparated = spaceSegments.length > 1
22
+
23
+ if (isSpaceSeparated) {
24
+ let segmentIdx = 0
25
+ let currentSegment = spaceSegments[0]
26
+ let segmentCharIdx = 0
27
+
28
+ for (
29
+ let i = 0;
30
+ i < textLower.length && segmentIdx < spaceSegments.length;
31
+ i++
32
+ ) {
33
+ const char = textLower[i]
34
+ const targetChar = currentSegment[segmentCharIdx]
35
+
36
+ if (char === targetChar) {
37
+ matches.push(i)
38
+ segmentCharIdx++
39
+ consecutiveMatches++
40
+ score += 1 + consecutiveMatches * 0.5
41
+
42
+ if (i === 0 || /[/\-_.]/.test(text[i - 1])) {
43
+ score += 2
44
+ }
45
+
46
+ if (segmentCharIdx >= currentSegment.length) {
47
+ segmentIdx++
48
+ spaceBonus += 3
49
+
50
+ if (segmentIdx < spaceSegments.length) {
51
+ currentSegment = spaceSegments[segmentIdx]
52
+ segmentCharIdx = 0
53
+ consecutiveMatches = 0
54
+ }
55
+ }
56
+ } else {
57
+ if (
58
+ segmentCharIdx > 0 &&
59
+ /[/\-_.]/.test(char) &&
60
+ segmentIdx < spaceSegments.length - 1
61
+ ) {
62
+ segmentIdx++
63
+ if (segmentIdx < spaceSegments.length) {
64
+ currentSegment = spaceSegments[segmentIdx]
65
+ segmentCharIdx = 0
66
+ consecutiveMatches = 0
67
+ if (char === currentSegment[0]) {
68
+ matches.push(i)
69
+ segmentCharIdx = 1
70
+ score += 2
71
+ }
72
+ }
73
+ } else {
74
+ consecutiveMatches = 0
75
+ }
76
+ }
77
+ }
78
+
79
+ if (
80
+ segmentIdx < spaceSegments.length ||
81
+ (segmentIdx === spaceSegments.length - 1 &&
82
+ segmentCharIdx < currentSegment.length)
83
+ ) {
84
+ return { score: -1, matches: [] }
85
+ }
86
+
87
+ score += spaceBonus
88
+ } else {
89
+ for (
90
+ let i = 0;
91
+ i < textLower.length && patternIdx < patternLower.length;
92
+ i++
93
+ ) {
94
+ if (textLower[i] === patternLower[patternIdx]) {
95
+ matches.push(i)
96
+ patternIdx++
97
+ consecutiveMatches++
98
+
99
+ score += 1 + consecutiveMatches * 0.5
100
+
101
+ if (i === 0 || /[/\-_.]/.test(text[i - 1])) {
102
+ score += 2
103
+ }
104
+ } else {
105
+ consecutiveMatches = 0
106
+ }
107
+ }
108
+
109
+ if (patternIdx !== patternLower.length) return { score: -1, matches: [] }
110
+ }
111
+
112
+ score += Math.max(0, 100 - text.length) * 0.1
113
+
114
+ const fileName = text.split("/").pop() || text
115
+ const queryFileName = pattern.replace(/\s+/g, "")
116
+ if (fileName.toLowerCase().includes(queryFileName.toLowerCase())) {
117
+ score += 5
118
+ }
119
+
120
+ return { score, matches }
121
+ }
@@ -1,4 +1,8 @@
1
1
  export const isHiddenFile = (filePath: string): boolean => {
2
+ if (filePath.startsWith("/")) {
3
+ filePath = filePath.slice(1)
4
+ }
5
+
2
6
  // Normalize the path to handle both Unix and Windows paths
3
7
  const normalizedPath = filePath.replace(/\\/g, "/")
4
8
 
@@ -1,5 +1,6 @@
1
1
  const importanceMap = {
2
2
  "readme.md": 200,
3
+ readme: 200,
3
4
  license: 100,
4
5
  "license.md": 100,
5
6
  "index.ts": 90,
@@ -7,7 +8,23 @@ const importanceMap = {
7
8
  "circuit.tsx": 90,
8
9
  }
9
10
 
10
- export const scorePackageFileImportance = (filePath: string) => {
11
+ /**
12
+ * Determines if a file is considered "important" for display in the
13
+ * `ImportantFilesView` component.
14
+ *
15
+ * A file is deemed important if it resides in the root directory of the package
16
+ * and has a positive importance score. Nested paths are not considered important.
17
+ */
18
+ export const isPackageFileImportant = (filePath: string): boolean => {
19
+ const normalized = filePath.replace(/^\.\/?/, "").toLowerCase()
20
+ if (normalized.split("/").length > 1) {
21
+ return false
22
+ }
23
+ return scorePackageFileImportance(filePath) > 0
24
+ }
25
+
26
+ // Kept for backward compatibility with older imports
27
+ export const scorePackageFileImportance = (filePath: string): number => {
11
28
  const lowerCaseFilePath = filePath.toLowerCase()
12
29
  for (const [key, value] of Object.entries(importanceMap)) {
13
30
  if (lowerCaseFilePath.endsWith(key)) {
@@ -16,7 +33,3 @@ export const scorePackageFileImportance = (filePath: string) => {
16
33
  }
17
34
  return 0
18
35
  }
19
-
20
- export const isPackageFileImportant = (filePath: string) => {
21
- return scorePackageFileImportance(filePath) > 0
22
- }
@@ -33,7 +33,7 @@ export const ConfirmDeletePackageDialog = ({
33
33
 
34
34
  return (
35
35
  <Dialog open={open} onOpenChange={onOpenChange}>
36
- <DialogContent>
36
+ <DialogContent className="w-[90vw]">
37
37
  <DialogHeader>
38
38
  <DialogTitle>Confirm Delete Package</DialogTitle>
39
39
  </DialogHeader>
@@ -0,0 +1,73 @@
1
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog"
2
+ import { Button } from "../ui/button"
3
+ import { createUseDialog } from "./create-use-dialog"
4
+ import { useState } from "react"
5
+
6
+ export const ConfirmDiscardChangesDialog = ({
7
+ open,
8
+ onOpenChange,
9
+ onConfirm,
10
+ }: {
11
+ open: boolean
12
+ onOpenChange: (open: boolean) => void
13
+ onConfirm: () => void
14
+ }) => {
15
+ const [isConfirming, setIsConfirming] = useState(false)
16
+
17
+ const handleDiscard = () => {
18
+ onConfirm()
19
+ onOpenChange(false)
20
+ setIsConfirming(false)
21
+ }
22
+
23
+ const handleCancel = () => {
24
+ onOpenChange(false)
25
+ setIsConfirming(false)
26
+ }
27
+
28
+ return (
29
+ <Dialog
30
+ open={open}
31
+ onOpenChange={(open) => {
32
+ onOpenChange(open)
33
+ if (!open) setIsConfirming(false)
34
+ }}
35
+ >
36
+ <DialogContent className="w-[90vw]">
37
+ <DialogHeader>
38
+ <DialogTitle>Discard Changes</DialogTitle>
39
+ </DialogHeader>
40
+ <p>Are you sure you want to discard all unsaved changes?</p>
41
+ <p className="text-red-600 font-medium">
42
+ This action cannot be undone.
43
+ </p>
44
+ <div className="flex justify-end space-x-2 mt-4">
45
+ <Button variant="outline" onClick={handleCancel}>
46
+ Cancel
47
+ </Button>
48
+ {!isConfirming ? (
49
+ <Button
50
+ variant="destructive"
51
+ className="bg-red-600 hover:bg-red-700"
52
+ onClick={() => setIsConfirming(true)}
53
+ >
54
+ Discard Changes
55
+ </Button>
56
+ ) : (
57
+ <Button
58
+ variant="destructive"
59
+ onClick={handleDiscard}
60
+ className="bg-red-600 hover:bg-red-700"
61
+ >
62
+ Yes, Discard All Changes
63
+ </Button>
64
+ )}
65
+ </div>
66
+ </DialogContent>
67
+ </Dialog>
68
+ )
69
+ }
70
+
71
+ export const useConfirmDiscardChangesDialog = createUseDialog(
72
+ ConfirmDiscardChangesDialog,
73
+ )
@@ -207,7 +207,7 @@ export const EditPackageDetailsDialog = ({
207
207
  return (
208
208
  <div>
209
209
  <Dialog open={showConfirmDelete} onOpenChange={setShowConfirmDelete}>
210
- <DialogContent className="max-w-md p-6 rounded-2xl shadow-lg">
210
+ <DialogContent className="w-[90vw] p-6 rounded-2xl shadow-lg">
211
211
  <DialogHeader>
212
212
  <DialogTitle className="text-left">Confirm Deletion</DialogTitle>
213
213
  <DialogDescription className="text-left">
@@ -255,7 +255,7 @@ export const EditPackageDetailsDialog = ({
255
255
  onChange={(e) =>
256
256
  setFormData((prev) => ({
257
257
  ...prev,
258
- unscopedPackageName: e.target.value,
258
+ unscopedPackageName: e.target.value.replace(/\s+/g, ""),
259
259
  }))
260
260
  }
261
261
  placeholder="Enter package name"