@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.
- package/bun-tests/fake-snippets-api/routes/package_files/create.test.ts +5 -6
- package/bun-tests/fake-snippets-api/routes/packages/list-1.test.ts +3 -0
- package/bun-tests/fake-snippets-api/routes/packages/list-2.test.ts +2 -0
- package/bun-tests/fake-snippets-api/routes/snippets/list_newest.test.ts +5 -3
- package/bun-tests/fake-snippets-api/routes/snippets/list_trending.test.ts +8 -5
- package/bun-tests/fake-snippets-api/routes/snippets/update.test.ts +1 -1
- package/bun.lock +110 -5
- package/dist/bundle.js +62 -11
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -2
- package/fake-snippets-api/lib/db/autoload-snippets.json +4 -0
- package/fake-snippets-api/lib/db/db-client.ts +2 -1
- package/fake-snippets-api/lib/db/schema.ts +1 -0
- package/fake-snippets-api/lib/public-mapping/public-map-package.ts +1 -0
- package/fake-snippets-api/routes/api/package_files/list.ts +6 -3
- package/fake-snippets-api/routes/api/package_releases/get.ts +67 -1
- package/fake-snippets-api/routes/api/packages/create.ts +2 -1
- package/fake-snippets-api/routes/api/snippets/create.ts +1 -0
- package/package.json +2 -1
- package/public/placeholder-logo.png +0 -0
- package/public/placeholder-logo.svg +1 -0
- package/public/placeholder-user.jpg +0 -0
- package/public/placeholder.jpg +0 -0
- package/public/placeholder.svg +1 -0
- package/src/App.tsx +6 -0
- package/src/components/ViewPackagePage/components/ShikiCodeViewer.tsx +50 -0
- package/src/components/ViewPackagePage/components/file-explorer.tsx +118 -0
- package/src/components/ViewPackagePage/components/important-files-view.tsx +231 -0
- package/src/components/ViewPackagePage/components/main-content-header.tsx +172 -0
- package/src/components/ViewPackagePage/components/main-content-view-selector.tsx +106 -0
- package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +130 -0
- package/src/components/ViewPackagePage/components/package-header.tsx +107 -0
- package/src/components/ViewPackagePage/components/preview-image-squares.tsx +63 -0
- package/src/components/ViewPackagePage/components/readme-view.tsx +58 -0
- package/src/components/ViewPackagePage/components/repo-header-button.tsx +36 -0
- package/src/components/ViewPackagePage/components/repo-header.tsx +4 -0
- package/src/components/ViewPackagePage/components/repo-page-content.tsx +213 -0
- package/src/components/ViewPackagePage/components/repo-tab-header.tsx +12 -0
- package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +103 -0
- package/src/components/ViewPackagePage/components/sidebar-contributors-section.tsx +31 -0
- package/src/components/ViewPackagePage/components/sidebar-packages-section.tsx +16 -0
- package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +44 -0
- package/src/components/ViewPackagePage/components/sidebar.tsx +40 -0
- package/src/components/ViewPackagePage/components/tab-views/3d-view.tsx +9 -0
- package/src/components/ViewPackagePage/components/tab-views/bom-view.tsx +9 -0
- package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +166 -0
- package/src/components/ViewPackagePage/components/tab-views/pcb-view.tsx +9 -0
- package/src/components/ViewPackagePage/components/tab-views/schematic-view.tsx +9 -0
- package/src/components/ViewPackagePage/components/theme-toggle.tsx +42 -0
- package/src/components/ViewPackagePage/hooks/use-mobile.tsx +19 -0
- package/src/components/ViewPackagePage/hooks/use-toast.ts +191 -0
- package/src/components/ViewPackagePage/simulate-page.tsx +120 -0
- package/src/components/ViewPackagePage/utils/is-package-file-important.ts +21 -0
- package/src/hooks/use-package-files.ts +29 -0
- package/src/hooks/use-package-release.ts +22 -0
- package/src/index.css +15 -0
- package/src/pages/beta.tsx +282 -99
- 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
|
+
}
|