@tscircuit/fake-snippets 0.0.26 → 0.0.28

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 (58) hide show
  1. package/bun-tests/fake-snippets-api/routes/package_files/create.test.ts +5 -6
  2. package/bun-tests/fake-snippets-api/routes/packages/list-1.test.ts +3 -0
  3. package/bun-tests/fake-snippets-api/routes/packages/list-2.test.ts +2 -0
  4. package/bun-tests/fake-snippets-api/routes/snippets/list_newest.test.ts +5 -3
  5. package/bun-tests/fake-snippets-api/routes/snippets/list_trending.test.ts +8 -5
  6. package/bun-tests/fake-snippets-api/routes/snippets/update.test.ts +1 -1
  7. package/bun.lock +110 -5
  8. package/dist/bundle.js +62 -11
  9. package/dist/index.d.ts +5 -0
  10. package/dist/index.js +4 -2
  11. package/fake-snippets-api/lib/db/autoload-snippets.json +4 -0
  12. package/fake-snippets-api/lib/db/db-client.ts +2 -1
  13. package/fake-snippets-api/lib/db/schema.ts +1 -0
  14. package/fake-snippets-api/lib/public-mapping/public-map-package.ts +1 -0
  15. package/fake-snippets-api/routes/api/package_files/list.ts +6 -3
  16. package/fake-snippets-api/routes/api/package_releases/get.ts +67 -1
  17. package/fake-snippets-api/routes/api/packages/create.ts +2 -1
  18. package/fake-snippets-api/routes/api/snippets/create.ts +1 -0
  19. package/package.json +2 -1
  20. package/public/placeholder-logo.png +0 -0
  21. package/public/placeholder-logo.svg +1 -0
  22. package/public/placeholder-user.jpg +0 -0
  23. package/public/placeholder.jpg +0 -0
  24. package/public/placeholder.svg +1 -0
  25. package/src/App.tsx +6 -0
  26. package/src/components/ViewPackagePage/components/ShikiCodeViewer.tsx +50 -0
  27. package/src/components/ViewPackagePage/components/file-explorer.tsx +118 -0
  28. package/src/components/ViewPackagePage/components/important-files-view.tsx +231 -0
  29. package/src/components/ViewPackagePage/components/main-content-header.tsx +172 -0
  30. package/src/components/ViewPackagePage/components/main-content-view-selector.tsx +106 -0
  31. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +130 -0
  32. package/src/components/ViewPackagePage/components/package-header.tsx +107 -0
  33. package/src/components/ViewPackagePage/components/preview-image-squares.tsx +63 -0
  34. package/src/components/ViewPackagePage/components/readme-view.tsx +58 -0
  35. package/src/components/ViewPackagePage/components/repo-header-button.tsx +36 -0
  36. package/src/components/ViewPackagePage/components/repo-header.tsx +4 -0
  37. package/src/components/ViewPackagePage/components/repo-page-content.tsx +213 -0
  38. package/src/components/ViewPackagePage/components/repo-tab-header.tsx +12 -0
  39. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +103 -0
  40. package/src/components/ViewPackagePage/components/sidebar-contributors-section.tsx +31 -0
  41. package/src/components/ViewPackagePage/components/sidebar-packages-section.tsx +16 -0
  42. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +44 -0
  43. package/src/components/ViewPackagePage/components/sidebar.tsx +40 -0
  44. package/src/components/ViewPackagePage/components/tab-views/3d-view.tsx +9 -0
  45. package/src/components/ViewPackagePage/components/tab-views/bom-view.tsx +9 -0
  46. package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +166 -0
  47. package/src/components/ViewPackagePage/components/tab-views/pcb-view.tsx +9 -0
  48. package/src/components/ViewPackagePage/components/tab-views/schematic-view.tsx +9 -0
  49. package/src/components/ViewPackagePage/components/theme-toggle.tsx +42 -0
  50. package/src/components/ViewPackagePage/hooks/use-mobile.tsx +19 -0
  51. package/src/components/ViewPackagePage/hooks/use-toast.ts +191 -0
  52. package/src/components/ViewPackagePage/simulate-page.tsx +120 -0
  53. package/src/components/ViewPackagePage/utils/is-package-file-important.ts +21 -0
  54. package/src/hooks/use-package-files.ts +29 -0
  55. package/src/hooks/use-package-release.ts +22 -0
  56. package/src/index.css +15 -0
  57. package/src/pages/beta.tsx +282 -99
  58. package/src/pages/view-package.tsx +38 -0
@@ -0,0 +1,231 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect } from "react"
4
+ import { Edit, FileText, Code } from "lucide-react"
5
+ import { Skeleton } from "@/components/ui/skeleton"
6
+ import { usePackageFile, usePackageFileByPath } from "@/hooks/use-package-files"
7
+ import { ShikiCodeViewer } from "./ShikiCodeViewer"
8
+ import { SparklesIcon } from "lucide-react"
9
+
10
+ interface PackageFile {
11
+ package_file_id: string
12
+ package_release_id: string
13
+ file_path: string
14
+ created_at: string
15
+ }
16
+
17
+ interface ImportantFilesViewProps {
18
+ importantFiles?: PackageFile[]
19
+ isLoading?: boolean
20
+ onEditClicked?: () => void
21
+
22
+ aiDescription?: string
23
+ aiUsageInstructions?: string
24
+ }
25
+
26
+ export default function ImportantFilesView({
27
+ importantFiles = [],
28
+ aiDescription,
29
+ aiUsageInstructions,
30
+ isLoading = false,
31
+ onEditClicked,
32
+ }: ImportantFilesViewProps) {
33
+ const [activeFilePath, setActiveFilePath] = useState<string | null>(null)
34
+ const [activeTab, setActiveTab] = useState<string | null>(null)
35
+
36
+ // Determine if we have AI content
37
+ const hasAiContent = Boolean(aiDescription || aiUsageInstructions)
38
+
39
+ // Select the appropriate tab/file when content changes
40
+ useEffect(() => {
41
+ // First priority: README file if it exists
42
+ const readmeFile = importantFiles.find(
43
+ (file) =>
44
+ file.file_path.toLowerCase().endsWith("readme.md") ||
45
+ file.file_path.toLowerCase().endsWith("readme"),
46
+ )
47
+
48
+ if (readmeFile) {
49
+ setActiveFilePath(readmeFile.file_path)
50
+ setActiveTab("file")
51
+ } else if (hasAiContent) {
52
+ // Second priority: AI content if available
53
+ setActiveTab("ai")
54
+ setActiveFilePath(null)
55
+ } else if (importantFiles.length > 0) {
56
+ // Third priority: First important file
57
+ setActiveFilePath(importantFiles[0].file_path)
58
+ setActiveTab("file")
59
+ }
60
+ }, [importantFiles, aiDescription, aiUsageInstructions, hasAiContent])
61
+
62
+ // Get file name from path
63
+ const getFileName = (path: string) => {
64
+ const parts = path.split("/")
65
+ return parts[parts.length - 1]
66
+ }
67
+
68
+ // Get file icon based on extension
69
+ const getFileIcon = (path: string) => {
70
+ if (
71
+ path.endsWith(".js") ||
72
+ path.endsWith(".jsx") ||
73
+ path.endsWith(".ts") ||
74
+ path.endsWith(".tsx")
75
+ ) {
76
+ return <Code className="h-3.5 w-3.5 mr-1.5" />
77
+ }
78
+ return <FileText className="h-3.5 w-3.5 mr-1.5" />
79
+ }
80
+
81
+ // Render AI content
82
+ const renderAiContent = () => {
83
+ return (
84
+ <div className="markdown-content">
85
+ {aiDescription && (
86
+ <div className="mb-6">
87
+ <h3 className="font-semibold text-lg mb-2">Description</h3>
88
+ <p className="whitespace-pre-wrap">{aiDescription}</p>
89
+ </div>
90
+ )}
91
+ {aiUsageInstructions && (
92
+ <div>
93
+ <h3 className="font-semibold text-lg mb-2">Instructions</h3>
94
+ <p className="whitespace-pre-wrap">{aiUsageInstructions}</p>
95
+ </div>
96
+ )}
97
+ </div>
98
+ )
99
+ }
100
+
101
+ // Get active file content
102
+ const partialActiveFile = importantFiles.find(
103
+ (file) => file.file_path === activeFilePath,
104
+ )
105
+ const { data: activeFileFull } = usePackageFile(
106
+ partialActiveFile
107
+ ? {
108
+ file_path: partialActiveFile.file_path,
109
+ package_release_id: partialActiveFile.package_release_id,
110
+ }
111
+ : null,
112
+ )
113
+ const activeFileContent = activeFileFull?.content_text || ""
114
+
115
+ if (isLoading) {
116
+ return (
117
+ <div className="mt-4 border border-gray-200 dark:border-[#30363d] rounded-md overflow-hidden">
118
+ <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]">
119
+ <div className="flex items-center space-x-4">
120
+ <Skeleton className="h-6 w-24" />
121
+ <Skeleton className="h-6 w-20" />
122
+ <Skeleton className="h-6 w-28" />
123
+ </div>
124
+ <div className="ml-auto flex items-center">
125
+ <Skeleton className="h-4 w-4 mr-1" />
126
+ <Skeleton className="h-4 w-4 ml-2" />
127
+ </div>
128
+ </div>
129
+ <div className="p-4 bg-white dark:bg-[#0d1117]">
130
+ <Skeleton className="h-8 w-3/4 mb-4" />
131
+ <Skeleton className="h-4 w-full mb-2" />
132
+ <Skeleton className="h-4 w-5/6 mb-2" />
133
+ <Skeleton className="h-4 w-4/5 mb-4" />
134
+ <Skeleton className="h-4 w-full mb-2" />
135
+ <Skeleton className="h-4 w-3/4" />
136
+ </div>
137
+ </div>
138
+ )
139
+ }
140
+
141
+ if (importantFiles.length === 0) {
142
+ return (
143
+ <div className="mt-4 border border-gray-200 dark:border-[#30363d] rounded-md overflow-hidden">
144
+ <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]">
145
+ <div className="flex items-center">
146
+ <FileText className="h-4 w-4 mr-2" />
147
+ <span className="font-semibold">No important files found</span>
148
+ </div>
149
+ </div>
150
+ <div className="p-4 bg-white dark:bg-[#0d1117]">
151
+ <p className="text-gray-500 dark:text-[#8b949e]">
152
+ No README, LICENSE, or other important files found in this
153
+ repository.
154
+ </p>
155
+ </div>
156
+ </div>
157
+ )
158
+ }
159
+
160
+ return (
161
+ <div className="mt-4 border border-gray-200 dark:border-[#30363d] rounded-md overflow-hidden">
162
+ <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]">
163
+ <div className="flex items-center space-x-2">
164
+ {/* AI Description Tab */}
165
+ {hasAiContent && (
166
+ <button
167
+ className={`flex items-center px-3 py-1.5 rounded-md text-xs ${
168
+ activeTab === "ai"
169
+ ? "bg-gray-200 dark:bg-[#30363d] font-medium"
170
+ : "text-gray-500 dark:text-[#8b949e] hover:bg-gray-200 dark:hover:bg-[#30363d]"
171
+ }`}
172
+ onClick={() => {
173
+ setActiveTab("ai")
174
+ setActiveFilePath(null)
175
+ }}
176
+ >
177
+ <SparklesIcon className="h-3.5 w-3.5 mr-1.5" />
178
+ <span>Description</span>
179
+ </button>
180
+ )}
181
+
182
+ {/* File Tabs */}
183
+ {importantFiles.map((file) => (
184
+ <button
185
+ key={file.package_file_id}
186
+ className={`flex items-center px-3 py-1.5 rounded-md text-xs ${
187
+ activeTab === "file" && activeFilePath === file.file_path
188
+ ? "bg-gray-200 dark:bg-[#30363d] font-medium"
189
+ : "text-gray-500 dark:text-[#8b949e] hover:bg-gray-200 dark:hover:bg-[#30363d]"
190
+ }`}
191
+ onClick={() => {
192
+ setActiveTab("file")
193
+ setActiveFilePath(file.file_path)
194
+ }}
195
+ >
196
+ {getFileIcon(file.file_path)}
197
+ <span>{getFileName(file.file_path)}</span>
198
+ </button>
199
+ ))}
200
+ </div>
201
+ <div className="ml-auto flex items-center">
202
+ <button
203
+ className="hover:bg-gray-200 dark:hover:bg-[#30363d] p-1 rounded-md"
204
+ onClick={onEditClicked}
205
+ >
206
+ <Edit className="h-4 w-4" />
207
+ <span className="sr-only">Edit</span>
208
+ </button>
209
+ </div>
210
+ </div>
211
+ <div className="p-4 bg-white dark:bg-[#0d1117]">
212
+ {activeTab === "ai" ? (
213
+ renderAiContent()
214
+ ) : activeFilePath && activeFilePath.endsWith(".md") ? (
215
+ <div className="markdown-content">
216
+ {/* In a real app, you'd use a markdown renderer here */}
217
+ <pre className="whitespace-pre-wrap">{activeFileContent}</pre>
218
+ </div>
219
+ ) : activeFilePath &&
220
+ (activeFilePath.endsWith(".js") ||
221
+ activeFilePath.endsWith(".jsx") ||
222
+ activeFilePath.endsWith(".ts") ||
223
+ activeFilePath.endsWith(".tsx")) ? (
224
+ <ShikiCodeViewer code={activeFileContent} filePath={activeFilePath} />
225
+ ) : (
226
+ <pre className="whitespace-pre-wrap">{activeFileContent}</pre>
227
+ )}
228
+ </div>
229
+ </div>
230
+ )
231
+ }
@@ -0,0 +1,172 @@
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+ import { Button } from "@/components/ui/button"
5
+ import {
6
+ ChevronDown,
7
+ CodeIcon,
8
+ Download,
9
+ Copy,
10
+ Check,
11
+ Hammer,
12
+ } from "lucide-react"
13
+ import MainContentViewSelector from "./main-content-view-selector"
14
+ import {
15
+ DropdownMenu,
16
+ DropdownMenuContent,
17
+ DropdownMenuItem,
18
+ DropdownMenuTrigger,
19
+ DropdownMenuSeparator,
20
+ } from "@/components/ui/dropdown-menu"
21
+
22
+ interface PackageInfo {
23
+ name: string
24
+ unscoped_name: string
25
+ owner_github_username: string
26
+ star_count: string
27
+ description: string
28
+ ai_description: string
29
+ creator_account_id?: string
30
+ owner_org_id?: string
31
+ }
32
+
33
+ interface MainContentHeaderProps {
34
+ activeView: string
35
+ onViewChange: (view: string) => void
36
+ onExportClicked?: (exportType: string) => void
37
+ packageInfo?: PackageInfo
38
+ }
39
+
40
+ export default function MainContentHeader({
41
+ activeView,
42
+ onViewChange,
43
+ onExportClicked,
44
+ packageInfo,
45
+ }: MainContentHeaderProps) {
46
+ const [copyInstallState, setCopyInstallState] = useState<"copy" | "copied">(
47
+ "copy",
48
+ )
49
+ const [copyCloneState, setCopyCloneState] = useState<"copy" | "copied">(
50
+ "copy",
51
+ )
52
+
53
+ const handleCopyInstall = () => {
54
+ const command = `tsci add ${packageInfo?.name || "@tscircuit/keyboard-default60"}`
55
+ navigator.clipboard.writeText(command)
56
+ setCopyInstallState("copied")
57
+ setTimeout(() => setCopyInstallState("copy"), 2000)
58
+ }
59
+
60
+ const handleCopyClone = () => {
61
+ const command = `tsci clone ${packageInfo?.name || "@tscircuit/keyboard-default60"}`
62
+ navigator.clipboard.writeText(command)
63
+ setCopyCloneState("copied")
64
+ setTimeout(() => setCopyCloneState("copy"), 2000)
65
+ }
66
+
67
+ return (
68
+ <div className="flex items-center justify-between mb-4">
69
+ <MainContentViewSelector
70
+ activeView={activeView}
71
+ onViewChange={onViewChange}
72
+ />
73
+
74
+ <div className="flex space-x-2">
75
+ {/* Export Dropdown */}
76
+ <DropdownMenu>
77
+ <DropdownMenuTrigger asChild>
78
+ <Button
79
+ variant="outline"
80
+ size="sm"
81
+ className="h-9 border-gray-300 dark:border-[#30363d] bg-gray-100 hover:bg-gray-200 dark:bg-[#21262d] dark:hover:bg-[#30363d] text-gray-700 dark:text-[#c9d1d9]"
82
+ >
83
+ <Download className="h-4 w-4 mr-0.5" />
84
+ Export
85
+ <ChevronDown className="h-4 w-4 ml-0.5" />
86
+ </Button>
87
+ </DropdownMenuTrigger>
88
+ <DropdownMenuContent align="end" className="w-48">
89
+ <DropdownMenuItem
90
+ className="cursor-pointer"
91
+ onClick={() => onExportClicked && onExportClicked("circuit_json")}
92
+ >
93
+ <Download className="h-4 w-4 mr-1.5 text-gray-500 dark:text-[#8b949e]" />
94
+ Circuit JSON
95
+ </DropdownMenuItem>
96
+ <DropdownMenuItem
97
+ className="cursor-pointer"
98
+ onClick={() =>
99
+ onExportClicked && onExportClicked("fabrication_files")
100
+ }
101
+ >
102
+ <Hammer className="h-4 w-4 mr-1.5 text-gray-500 dark:text-[#8b949e]" />
103
+ Fabrication Files
104
+ </DropdownMenuItem>
105
+ </DropdownMenuContent>
106
+ </DropdownMenu>
107
+
108
+ {/* Code Dropdown */}
109
+ <DropdownMenu>
110
+ <DropdownMenuTrigger asChild>
111
+ <Button
112
+ size="sm"
113
+ className="h-9 bg-green-600 hover:bg-green-700 dark:bg-[#238636] dark:hover:bg-[#2ea043] text-white"
114
+ >
115
+ <CodeIcon className="h-4 w-4 mr-0.5" />
116
+ Code
117
+ <ChevronDown className="h-4 w-4 ml-0.5" />
118
+ </Button>
119
+ </DropdownMenuTrigger>
120
+ <DropdownMenuContent align="end" className="w-72">
121
+ <DropdownMenuItem className="cursor-pointer">
122
+ Edit Online
123
+ </DropdownMenuItem>
124
+ <DropdownMenuSeparator />
125
+
126
+ {/* Install Option */}
127
+ <div className="p-2">
128
+ <div className="text-sm font-medium mb-1">Install</div>
129
+ <div className="flex items-center bg-gray-100 dark:bg-[#161b22] rounded-md p-2 text-sm font-mono">
130
+ <code className="flex-1 overflow-x-auto">
131
+ tsci add{" "}
132
+ {packageInfo?.name || "@tscircuit/keyboard-default60"}
133
+ </code>
134
+ <button
135
+ className="ml-2 p-1 hover:bg-gray-200 dark:hover:bg-[#30363d] rounded"
136
+ onClick={handleCopyInstall}
137
+ >
138
+ {copyInstallState === "copy" ? (
139
+ <Copy className="h-4 w-4" />
140
+ ) : (
141
+ <Check className="h-4 w-4 text-green-500" />
142
+ )}
143
+ </button>
144
+ </div>
145
+ </div>
146
+
147
+ {/* Clone Option */}
148
+ <div className="p-2">
149
+ <div className="text-sm font-medium mb-1">Clone</div>
150
+ <div className="flex items-center bg-gray-100 dark:bg-[#161b22] rounded-md p-2 text-sm font-mono">
151
+ <code className="flex-1 overflow-x-auto">
152
+ tsci clone{" "}
153
+ {packageInfo?.name || "@tscircuit/keyboard-default60"}
154
+ </code>
155
+ <button
156
+ className="ml-2 p-1 hover:bg-gray-200 dark:hover:bg-[#30363d] rounded"
157
+ onClick={handleCopyClone}
158
+ >
159
+ {copyCloneState === "copy" ? (
160
+ <Copy className="h-4 w-4" />
161
+ ) : (
162
+ <Check className="h-4 w-4 text-green-500" />
163
+ )}
164
+ </button>
165
+ </div>
166
+ </div>
167
+ </DropdownMenuContent>
168
+ </DropdownMenu>
169
+ </div>
170
+ </div>
171
+ )
172
+ }
@@ -0,0 +1,106 @@
1
+ "use client"
2
+ import React from "react"
3
+ import {
4
+ Code,
5
+ CuboidIcon as Cube,
6
+ CircuitBoardIcon as Circuit,
7
+ FileTerminal,
8
+ ClipboardList,
9
+ } from "lucide-react"
10
+ import {
11
+ DropdownMenu,
12
+ DropdownMenuContent,
13
+ DropdownMenuItem,
14
+ DropdownMenuTrigger,
15
+ } from "@/components/ui/dropdown-menu"
16
+ import { Button } from "@/components/ui/button"
17
+
18
+ interface MainContentViewSelectorProps {
19
+ activeView: string
20
+ onViewChange: (view: string) => void
21
+ }
22
+
23
+ export default function MainContentViewSelector({
24
+ activeView,
25
+ onViewChange,
26
+ }: MainContentViewSelectorProps) {
27
+ const views = [
28
+ { id: "files", label: "Files", icon: <Code className="h-4 w-4 mr-1" /> },
29
+ { id: "3d", label: "3D", icon: <Cube className="h-4 w-4 mr-1" /> },
30
+ { id: "pcb", label: "PCB", icon: <Circuit className="h-4 w-4 mr-1" /> },
31
+ {
32
+ id: "schematic",
33
+ label: "Schematic",
34
+ icon: <FileTerminal className="h-4 w-4 mr-1" />,
35
+ },
36
+ {
37
+ id: "bom",
38
+ label: "BOM",
39
+ icon: <ClipboardList className="h-4 w-4 mr-1" />,
40
+ },
41
+ ]
42
+
43
+ return (
44
+ <>
45
+ {/* Desktop Tabs */}
46
+ <div className="bg-gray-100 dark:bg-[#161b22] rounded-md p-1 hidden lg:flex">
47
+ {views.map((view) => (
48
+ <button
49
+ key={view.id}
50
+ className={`px-4 py-1.5 rounded-md text-sm font-medium transition-colors flex items-center ${
51
+ activeView === view.id
52
+ ? "bg-white dark:bg-[#0d1117] text-gray-800 dark:text-white"
53
+ : "text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-white"
54
+ }`}
55
+ onClick={() => onViewChange(view.id)}
56
+ >
57
+ {React.cloneElement(view.icon, { className: "h-4 w-4 mr-1" })}
58
+ {view.label}
59
+ </button>
60
+ ))}
61
+ </div>
62
+
63
+ {/* Mobile Dropdown */}
64
+ <div className="lg:hidden">
65
+ <DropdownMenu>
66
+ <DropdownMenuTrigger asChild>
67
+ <Button
68
+ variant="outline"
69
+ size="sm"
70
+ className="h-9 border-gray-300 dark:border-[#30363d] bg-gray-100 hover:bg-gray-200 dark:bg-[#21262d] dark:hover:bg-[#30363d] text-gray-700 dark:text-[#c9d1d9]"
71
+ >
72
+ {views.find((view) => view.id === activeView)?.icon}
73
+ {views.find((view) => view.id === activeView)?.label || "View"}
74
+ <svg
75
+ className="h-4 w-4 ml-1"
76
+ fill="none"
77
+ height="24"
78
+ stroke="currentColor"
79
+ strokeLinecap="round"
80
+ strokeLinejoin="round"
81
+ strokeWidth="2"
82
+ viewBox="0 0 24 24"
83
+ width="24"
84
+ xmlns="http://www.w3.org/2000/svg"
85
+ >
86
+ <path d="m6 9 6 6 6-6" />
87
+ </svg>
88
+ </Button>
89
+ </DropdownMenuTrigger>
90
+ <DropdownMenuContent align="start">
91
+ {views.map((view) => (
92
+ <DropdownMenuItem
93
+ key={view.id}
94
+ onClick={() => onViewChange(view.id)}
95
+ className="flex items-center cursor-pointer"
96
+ >
97
+ {view.icon}
98
+ {view.label}
99
+ </DropdownMenuItem>
100
+ ))}
101
+ </DropdownMenuContent>
102
+ </DropdownMenu>
103
+ </div>
104
+ </>
105
+ )
106
+ }
@@ -0,0 +1,130 @@
1
+ import { GitFork, Star, Tag } from "lucide-react"
2
+ import { Badge } from "@/components/ui/badge"
3
+ import { Skeleton } from "@/components/ui/skeleton"
4
+
5
+ interface PackageInfo {
6
+ name: string
7
+ unscoped_name: string
8
+ owner_github_username: string
9
+ star_count: string
10
+ description: string
11
+ ai_description: string
12
+ creator_account_id?: string
13
+ owner_org_id?: string
14
+ is_package?: boolean
15
+ }
16
+
17
+ interface MobileSidebarProps {
18
+ packageInfo?: PackageInfo
19
+ isLoading?: boolean
20
+ }
21
+
22
+ export default function MobileSidebar({
23
+ packageInfo,
24
+ isLoading = false,
25
+ }: MobileSidebarProps) {
26
+ const topics = packageInfo?.is_package ? ["Package"] : ["Board"]
27
+
28
+ if (isLoading) {
29
+ return (
30
+ <div className="p-4 bg-white dark:bg-[#0d1117] border-b border-gray-200 dark:border-[#30363d] md:hidden">
31
+ {/* Description skeleton */}
32
+ <Skeleton className="h-4 w-full mb-4" />
33
+
34
+ {/* Tags/Topics skeleton */}
35
+ <div className="flex flex-wrap gap-2 mb-4">
36
+ {[1, 2, 3].map((i) => (
37
+ <Skeleton key={i} className="h-6 w-20 rounded-full" />
38
+ ))}
39
+ </div>
40
+
41
+ <div className="flex justify-between text-sm">
42
+ <Skeleton className="h-4 w-16" />
43
+ <Skeleton className="h-4 w-16" />
44
+ <Skeleton className="h-4 w-24" />
45
+ </div>
46
+
47
+ <div className="grid grid-cols-3 gap-2 mt-4">
48
+ {[1, 2, 3].map((i) => (
49
+ <Skeleton key={i} className="aspect-square rounded-lg" />
50
+ ))}
51
+ </div>
52
+ </div>
53
+ )
54
+ }
55
+
56
+ return (
57
+ <div className="p-4 bg-white dark:bg-[#0d1117] border-b border-gray-200 dark:border-[#30363d] md:hidden">
58
+ {/* Description */}
59
+ <p className="text-sm mb-4 text-gray-700 dark:text-[#c9d1d9]">
60
+ {packageInfo?.description ||
61
+ "A Default 60 keyboard created with tscircuit"}
62
+ </p>
63
+
64
+ {/* Tags/Topics */}
65
+ <div className="flex flex-wrap gap-2 mb-4">
66
+ {topics.map((topic, index) => (
67
+ <Badge
68
+ key={index}
69
+ variant="outline"
70
+ className="text-xs rounded-full px-2 py-0.5 bg-blue-100 dark:bg-[#1f6feb33] text-blue-600 dark:text-[#58a6ff] border-blue-300 dark:border-[#1f6feb66]"
71
+ >
72
+ {topic}
73
+ </Badge>
74
+ ))}
75
+ </div>
76
+
77
+ <div className="flex justify-between text-sm">
78
+ <div className="flex items-center">
79
+ <Star className="h-4 w-4 mr-1 text-gray-500 dark:text-[#8b949e]" />
80
+ <span className="mr-4">{packageInfo?.star_count || "16"} stars</span>
81
+ </div>
82
+ <div className="flex items-center">
83
+ <GitFork className="h-4 w-4 mr-1 text-gray-500 dark:text-[#8b949e]" />
84
+ <span className="mr-4">39 forks</span>
85
+ </div>
86
+ <div className="flex items-center">
87
+ <Tag className="h-4 w-4 mr-1 text-gray-500 dark:text-[#8b949e]" />
88
+ <span>12 Releases</span>
89
+ </div>
90
+ </div>
91
+
92
+ <div className="grid grid-cols-3 gap-2 mt-4">
93
+ {["3D View", "PCB View", "Schematic View"].map((view, index) => (
94
+ <PreviewButton packageInfo={packageInfo} key={index} view={view} />
95
+ ))}
96
+ </div>
97
+ </div>
98
+ )
99
+ }
100
+
101
+ function PreviewButton({
102
+ packageInfo,
103
+ view,
104
+ }: {
105
+ packageInfo?: PackageInfo
106
+ view: string
107
+ }) {
108
+ let imageUrl: string | null = null
109
+ if (packageInfo && view === "PCB View") {
110
+ imageUrl = `https://registry-api.tscircuit.com/snippets/images/${packageInfo.name}/pcb.png`
111
+ } else if (packageInfo && view === "Schematic View") {
112
+ imageUrl = `https://registry-api.tscircuit.com/snippets/images/${packageInfo.name}/schematic.png`
113
+ }
114
+
115
+ if (imageUrl) {
116
+ return (
117
+ <button className="aspect-square bg-gray-100 dark:bg-[#161b22] rounded-lg border border-gray-200 dark:border-[#30363d] hover:bg-gray-200 dark:hover:bg-[#21262d] flex items-center justify-center transition-colors">
118
+ <img src={imageUrl} alt={view} className="w-full h-full object-cover" />
119
+ </button>
120
+ )
121
+ }
122
+
123
+ return (
124
+ <button className="aspect-square bg-gray-100 dark:bg-[#161b22] rounded-lg border border-gray-200 dark:border-[#30363d] hover:bg-gray-200 dark:hover:bg-[#21262d] flex items-center justify-center transition-colors">
125
+ <span className="text-xs font-medium text-gray-700 dark:text-[#c9d1d9]">
126
+ {view}
127
+ </span>
128
+ </button>
129
+ )
130
+ }