@tscircuit/fake-snippets 0.0.6 → 0.0.7

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 (38) hide show
  1. package/bun-tests/fake-snippets-api/routes/package_files/create.test.ts +375 -0
  2. package/bun-tests/fake-snippets-api/routes/package_files/download.test.ts +248 -0
  3. package/bun-tests/fake-snippets-api/routes/package_files/get.test.ts +220 -0
  4. package/bun-tests/fake-snippets-api/routes/package_files/list.test.ts +204 -0
  5. package/bun-tests/fake-snippets-api/routes/package_releases/get.test.ts +0 -1
  6. package/bun.lock +97 -73
  7. package/dist/bundle.js +584 -212
  8. package/fake-snippets-api/lib/db/db-client.ts +13 -0
  9. package/fake-snippets-api/lib/package_file/get-package-file-id-from-file-descriptor.ts +168 -0
  10. package/fake-snippets-api/lib/package_release/find-package-release-id.ts +122 -0
  11. package/fake-snippets-api/routes/api/package_files/create.ts +132 -0
  12. package/fake-snippets-api/routes/api/package_files/download.ts +70 -153
  13. package/fake-snippets-api/routes/api/package_files/get.ts +24 -5
  14. package/fake-snippets-api/routes/api/package_files/list.ts +16 -28
  15. package/index.html +12 -1
  16. package/package.json +9 -9
  17. package/playwright-tests/profile-page.spec.ts +59 -0
  18. package/playwright-tests/snapshots/profile-page.spec.ts-profile-page-before-delete.png +0 -0
  19. package/playwright-tests/snapshots/profile-page.spec.ts-profile-page-delete-dialog.png +0 -0
  20. package/playwright-tests/snapshots/profile-page.spec.ts-profile-page-dropdown-open.png +0 -0
  21. package/scripts/generate-image-sizes.ts +0 -1
  22. package/scripts/generate_bundle_stats.js +22 -6
  23. package/src/components/AiChatInterface.tsx +8 -0
  24. package/src/components/Analytics.tsx +1 -1
  25. package/src/components/CodeAndPreview.tsx +9 -3
  26. package/src/components/CreateNewSnippetWithAiHero.tsx +6 -2
  27. package/src/components/EditorNav.tsx +4 -0
  28. package/src/components/Footer.tsx +1 -1
  29. package/src/components/Header.tsx +7 -10
  30. package/src/components/HeaderLogin.tsx +1 -1
  31. package/src/components/PreviewContent.tsx +4 -1
  32. package/src/components/SnippetList.tsx +71 -0
  33. package/src/lib/templates/blinking-led-board-template.ts +2 -1
  34. package/src/pages/dashboard.tsx +19 -44
  35. package/src/pages/editor.tsx +1 -1
  36. package/src/pages/landing.tsx +8 -16
  37. package/src/pages/quickstart.tsx +9 -9
  38. package/src/pages/user-profile.tsx +50 -3
@@ -1,24 +1,24 @@
1
1
  import { withRouteSpec } from "fake-snippets-api/lib/with-winter-spec"
2
2
  import { z } from "zod"
3
3
  import * as ZT from "fake-snippets-api/lib/db/schema"
4
- import { NotFoundError } from "winterspec/middleware"
4
+ import { getPackageFileIdFromFileDescriptor } from "fake-snippets-api/lib/package_file/get-package-file-id-from-file-descriptor"
5
5
 
6
6
  const routeSpec = {
7
7
  methods: ["POST"],
8
8
  auth: "none",
9
9
  jsonBody: z
10
10
  .object({
11
- package_file_id: z.string().uuid(),
11
+ package_file_id: z.string(),
12
12
  })
13
13
  .or(
14
14
  z.object({
15
- package_release_id: z.string().uuid(),
15
+ package_release_id: z.string(),
16
16
  file_path: z.string(),
17
17
  }),
18
18
  )
19
19
  .or(
20
20
  z.object({
21
- package_id: z.string().uuid(),
21
+ package_id: z.string(),
22
22
  version: z.string().optional(),
23
23
  file_path: z.string(),
24
24
  }),
@@ -45,8 +45,27 @@ const routeSpec = {
45
45
  } as const
46
46
 
47
47
  export default withRouteSpec(routeSpec)(async (req, ctx) => {
48
+ const packageFileId = await getPackageFileIdFromFileDescriptor(
49
+ req.jsonBody,
50
+ ctx,
51
+ )
52
+
53
+ const packageFile = ctx.db.packageFiles.find(
54
+ (pf: ZT.PackageFile) => pf.package_file_id === packageFileId,
55
+ )
56
+
57
+ if (!packageFile) {
58
+ return ctx.error(404, {
59
+ error_code: "package_file_not_found",
60
+ message: "Package file not found",
61
+ })
62
+ }
63
+
48
64
  return ctx.json({
49
65
  ok: true,
50
- package_file: undefined,
66
+ package_file: {
67
+ ...packageFile,
68
+ created_at: packageFile.created_at.toString(),
69
+ },
51
70
  })
52
71
  })
@@ -1,13 +1,14 @@
1
1
  import { withRouteSpec } from "fake-snippets-api/lib/with-winter-spec"
2
2
  import { z } from "zod"
3
3
  import * as ZT from "fake-snippets-api/lib/db/schema"
4
+ import { findPackageReleaseId } from "fake-snippets-api/lib/package_release/find-package-release-id"
4
5
 
5
6
  const routeSpec = {
6
7
  methods: ["POST"],
7
8
  auth: "none",
8
9
  jsonBody: z
9
10
  .object({
10
- package_release_id: z.string().uuid(),
11
+ package_release_id: z.string(),
11
12
  })
12
13
  .or(
13
14
  z.object({
@@ -27,34 +28,21 @@ const routeSpec = {
27
28
  } as const
28
29
 
29
30
  export default withRouteSpec(routeSpec)(async (req, ctx) => {
30
- // const package_release_id = await findPackageReleaseId(req.jsonBody, ctx)
31
- // if (!package_release_id) {
32
- // return ctx.error(404, {
33
- // error_code: "package_release_not_found",
34
- // message: "Package release not found",
35
- // })
36
- // }
37
- // const package_files = await ctx.db
38
- // .selectFrom("main.package_file")
39
- // .select([
40
- // "package_file_id",
41
- // "package_release_id",
42
- // "content_mimetype",
43
- // "file_path",
44
- // "created_at",
45
- // ])
46
- // .where("package_release_id", "=", package_release_id)
47
- // .where("file_path", "not like", ".tscircuit-internal/%")
48
- // .execute()
49
- // return ctx.json({
50
- // ok: true,
51
- // package_files: package_files.map((pf) => ({
52
- // ...pf,
53
- // created_at: pf.created_at.toISOString(),
54
- // })),
55
- // })
31
+ const packageReleaseId = await findPackageReleaseId(req.jsonBody, ctx)
32
+
33
+ if (!packageReleaseId) {
34
+ return ctx.error(404, {
35
+ error_code: "package_release_not_found",
36
+ message: "Package release not found",
37
+ })
38
+ }
39
+
40
+ const packageFiles = ctx.db.packageFiles.filter(
41
+ (file) => file.package_release_id === packageReleaseId,
42
+ )
43
+
56
44
  return ctx.json({
57
45
  ok: true,
58
- package_files: [],
46
+ package_files: packageFiles,
59
47
  })
60
48
  })
package/index.html CHANGED
@@ -84,6 +84,17 @@
84
84
  }, 300); // Match the opacity transition duration
85
85
  };
86
86
  </script>
87
+
88
+ <!-- Google tag (gtag.js) -->
89
+ <script async src="https://www.googletagmanager.com/gtag/js?id=AW-16607843883">
90
+ </script>
91
+ <script>
92
+ window.dataLayer = window.dataLayer || [];
93
+ function gtag(){dataLayer.push(arguments);}
94
+ gtag('js', new Date());
95
+
96
+ gtag('config', 'AW-16607843883');
97
+ </script>
87
98
  </head>
88
99
 
89
100
  <body>
@@ -102,4 +113,4 @@
102
113
  <script type="module" src="./src/main.tsx"></script>
103
114
  </body>
104
115
 
105
- </html>
116
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fake-snippets",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -61,13 +61,13 @@
61
61
  "@radix-ui/react-toggle": "^1.1.0",
62
62
  "@radix-ui/react-toggle-group": "^1.1.0",
63
63
  "@radix-ui/react-tooltip": "^1.1.2",
64
- "@tscircuit/3d-viewer": "^0.0.113",
65
- "@tscircuit/footprinter": "^0.0.102",
64
+ "@tscircuit/3d-viewer": "^0.0.142",
65
+ "@tscircuit/footprinter": "^0.0.124",
66
66
  "@tscircuit/layout": "^0.0.29",
67
67
  "@tscircuit/math-utils": "^0.0.10",
68
68
  "@tscircuit/mm": "^0.0.8",
69
69
  "@tscircuit/pcb-viewer": "^1.11.12",
70
- "@tscircuit/props": "^0.0.138",
70
+ "@tscircuit/props": "^0.0.143",
71
71
  "@tscircuit/schematic-viewer": "^1.4.3",
72
72
  "@types/file-saver": "^2.0.7",
73
73
  "@types/ms": "^0.7.34",
@@ -76,12 +76,12 @@
76
76
  "@valtown/codemirror-ts": "^2.2.0",
77
77
  "@vercel/analytics": "^1.4.1",
78
78
  "change-case": "^5.4.4",
79
- "circuit-json": "^0.0.135",
79
+ "circuit-json": "^0.0.136",
80
80
  "circuit-json-to-bom-csv": "^0.0.6",
81
81
  "circuit-json-to-gerber": "^0.0.16",
82
82
  "circuit-json-to-pnp-csv": "^0.0.6",
83
+ "circuit-json-to-readable-netlist": "^0.0.8",
83
84
  "circuit-json-to-tscircuit": "^0.0.4",
84
- "circuit-json-to-readable-netlist": "^0.0.7",
85
85
  "class-variance-authority": "^0.7.0",
86
86
  "clsx": "^2.1.1",
87
87
  "cmdk": "^1.0.4",
@@ -97,7 +97,7 @@
97
97
  "immer": "^10.1.1",
98
98
  "input-otp": "^1.2.4",
99
99
  "jose": "^5.9.3",
100
- "jscad-electronics": "^0.0.24",
100
+ "jscad-electronics": "^0.0.25",
101
101
  "jszip": "^3.10.1",
102
102
  "kicad-converter": "^0.0.16",
103
103
  "lucide-react": "^0.445.0",
@@ -130,9 +130,9 @@
130
130
  "@babel/standalone": "^7.26.2",
131
131
  "@biomejs/biome": "^1.9.2",
132
132
  "@playwright/test": "^1.48.0",
133
- "@tscircuit/core": "^0.0.296",
133
+ "@tscircuit/core": "^0.0.315",
134
134
  "@tscircuit/prompt-benchmarks": "^0.0.28",
135
- "@tscircuit/runframe": "^0.0.139",
135
+ "@tscircuit/runframe": "^0.0.193",
136
136
  "@types/babel__standalone": "^7.1.7",
137
137
  "@types/bun": "^1.1.10",
138
138
  "@types/country-list": "^2.1.4",
@@ -0,0 +1,59 @@
1
+ import { test, expect } from "@playwright/test"
2
+
3
+ test("test delete functionality in profile", async ({ page }) => {
4
+ // Go to profile page
5
+ await page.goto("http://localhost:5177/testuser")
6
+
7
+ // Login
8
+ await page.getByRole("button", { name: "Log in" }).click()
9
+
10
+ // Wait for snippets grid to load
11
+ await page.waitForSelector(".grid")
12
+ await page.waitForLoadState("networkidle")
13
+
14
+ // Verify initial snippet exists
15
+ const snippetTitle = page.locator(".text-md.font-semibold").first()
16
+ const snippetName = await snippetTitle.textContent()
17
+ expect(await snippetTitle.isVisible()).toBe(true)
18
+
19
+ // Take screenshot of initial state
20
+ await expect(page).toHaveScreenshot("profile-page-before-delete.png")
21
+
22
+ // Open dropdown menu
23
+ await page.locator(".lucide-ellipsis-vertical").first().click()
24
+ await page.waitForTimeout(1000)
25
+
26
+ // Take screenshot with dropdown open
27
+ await expect(page).toHaveScreenshot("profile-page-dropdown-open.png")
28
+
29
+ // Click delete option
30
+ await page.getByRole("menuitem", { name: "Delete Snippet" }).click()
31
+
32
+ // Wait for and verify confirmation dialog
33
+ const dialog = page.locator('[role="dialog"]')
34
+ await expect(dialog).toBeVisible()
35
+ const dialogText = await dialog.textContent()
36
+ expect(dialogText).toContain(
37
+ `Are you sure you want to delete the snippet "${snippetName}"?`,
38
+ )
39
+
40
+ // Take screenshot of delete dialog
41
+ await expect(page).toHaveScreenshot("profile-page-delete-dialog.png")
42
+
43
+ // Confirm delete
44
+ await page.getByRole("button", { name: "Delete" }).click()
45
+
46
+ // Verify success toast appears
47
+ await page.waitForSelector('div:has-text("Successfully deleted")', {
48
+ state: "visible",
49
+ })
50
+
51
+ // Wait for page to update
52
+ await page.waitForLoadState("networkidle")
53
+
54
+ // Verify snippet is removed
55
+ const remainingSnippets = await page
56
+ .locator(`.text-md.font-semibold:has-text("${snippetName}")`)
57
+ .count()
58
+ expect(remainingSnippets).toBe(0)
59
+ })
@@ -7,7 +7,6 @@ const INPUT_DIR = "src/assets/originals"
7
7
  const OUTPUT_DIR = "public/assets"
8
8
 
9
9
  async function generateImageSizes() {
10
- console.log("one")
11
10
  if (!fs.existsSync(INPUT_DIR)) {
12
11
  fs.mkdirSync(INPUT_DIR, { recursive: true })
13
12
  }
@@ -82,11 +82,22 @@ function compareSizes(prData, mainData, dependencies) {
82
82
  const mainSize = mainStats.depStats[dep]?.size || 0
83
83
  const diff = prSize - mainSize
84
84
 
85
+ let percentChange = "N/A"
86
+ if (prSize === 0 && mainSize > 0) {
87
+ percentChange = "Removed"
88
+ } else if (mainSize !== 0) {
89
+ percentChange = (diff / mainSize) * 100
90
+ } else if (prSize > 0 && mainSize === 0) {
91
+ percentChange = "Added"
92
+ } else {
93
+ percentChange = 0
94
+ }
95
+
85
96
  diffStats[dep] = {
86
97
  before: mainSize,
87
98
  after: prSize,
88
99
  diff,
89
- percentChange: (diff / mainSize) * 100 || 0,
100
+ percentChange,
90
101
  }
91
102
  })
92
103
 
@@ -95,7 +106,10 @@ function compareSizes(prData, mainData, dependencies) {
95
106
  totalAfter: prStats.totalSize,
96
107
  totalDiff: prStats.totalSize - mainStats.totalSize,
97
108
  totalPercentChange:
98
- ((prStats.totalSize - mainStats.totalSize) / mainStats.totalSize) * 100,
109
+ mainStats.totalSize !== 0
110
+ ? ((prStats.totalSize - mainStats.totalSize) / mainStats.totalSize) *
111
+ 100
112
+ : 0,
99
113
  diffStats,
100
114
  }
101
115
  }
@@ -107,7 +121,7 @@ function generateDiffMarkdown(prData, mainData, dependencies) {
107
121
  markdown += `## Total Bundle Size\n\n`
108
122
  markdown += `- Before: **${formatBytes(comparison.totalBefore)}**\n`
109
123
  markdown += `- After: **${formatBytes(comparison.totalAfter)}**\n`
110
- markdown += `- Change: ${totalDiffSymbol} **${formatBytes(Math.abs(comparison.totalDiff))}** (${comparison.totalPercentChange.toFixed(2)}%)\n\n`
124
+ markdown += `- Change: ${totalDiffSymbol} **${formatBytes(Math.abs(comparison.totalDiff))}** (${isNaN(comparison.totalPercentChange) ? "N/A" : comparison.totalPercentChange.toFixed(2)}%)\n\n`
111
125
 
112
126
  markdown += `## Diff\n\n`
113
127
 
@@ -117,7 +131,10 @@ function generateDiffMarkdown(prData, mainData, dependencies) {
117
131
 
118
132
  const significantChanges = sortedDiffs.filter(
119
133
  ([, stats]) =>
120
- Math.abs(stats.percentChange) > 1 || Math.abs(stats.diff) > 1024,
134
+ (typeof stats.percentChange === "number" &&
135
+ (Math.abs(stats.percentChange) > 1 || Math.abs(stats.diff) > 1024)) ||
136
+ stats.percentChange === "Added" ||
137
+ stats.percentChange === "Removed",
121
138
  )
122
139
 
123
140
  if (significantChanges.length > 0) {
@@ -127,8 +144,7 @@ function generateDiffMarkdown(prData, mainData, dependencies) {
127
144
  for (const [name, stats] of significantChanges) {
128
145
  const version = dependencies[name]
129
146
  const symbol = stats.diff > 0 ? "📈" : "📉"
130
- markdown += `| ${name}@${version} | ${formatBytes(stats.before)} | ${formatBytes(stats.after)} | ${symbol} ${formatBytes(Math.abs(stats.diff))} |
131
- ${stats.percentChange.toFixed(2)}% |\n`
147
+ markdown += `| ${name}@${version} | ${formatBytes(stats.before)} | ${formatBytes(stats.after)} | ${symbol} ${formatBytes(Math.abs(stats.diff))} | ${typeof stats.percentChange === "number" ? stats.percentChange.toFixed(2) + "%" : stats.percentChange} |\n`
132
148
  }
133
149
  } else {
134
150
  markdown += "No significant changes in bundle size.\n"
@@ -165,6 +165,14 @@ export default function AIChatInterface({
165
165
  {messages.length === 0 && isLoggedIn && (
166
166
  <div className="text-gray-500 text-xl text-center pt-[30vh] flex flex-col items-center">
167
167
  <div>Submit a prompt to {snippet ? "edit!" : "get started!"}</div>
168
+ <div className="mt-2">
169
+ This is our legacy AI chat interface. For a better experience,
170
+ please use{" "}
171
+ <PrefetchPageLink href="https://chat.tscircuit.com">
172
+ chat.tscircuit.com
173
+ </PrefetchPageLink>
174
+ .
175
+ </div>
168
176
  <div className="text-6xl mt-4">↓</div>
169
177
  </div>
170
178
  )}
@@ -3,7 +3,7 @@ import posthog from "posthog-js"
3
3
  import CookieConsent from "react-cookie-consent"
4
4
 
5
5
  posthog.init("phc_htd8AQjSfVEsFCLQMAiUooG4Q0DKBCjqYuQglc9V3Wo", {
6
- api_host: "https://us.i.posthog.com",
6
+ api_host: "https://postpig.tscircuit.com",
7
7
  person_profiles: "always",
8
8
  })
9
9
 
@@ -166,6 +166,15 @@ export function CodeAndPreview({ snippet }: Props) {
166
166
  const [lastSavedAt, setLastSavedAt] = useState(Date.now())
167
167
 
168
168
  const handleSave = async () => {
169
+ if (hasUnrunChanges) {
170
+ toast({
171
+ title: "Warning",
172
+ description: "You must run the snippet before saving your changes.",
173
+ variant: "destructive",
174
+ })
175
+ return
176
+ }
177
+
169
178
  setLastSavedAt(Date.now())
170
179
  if (snippet) {
171
180
  updateSnippetMutation.mutate()
@@ -197,8 +206,6 @@ export function CodeAndPreview({ snippet }: Props) {
197
206
  ...(code.match(/export const (\w+) ?=/)?.slice(1) ?? []),
198
207
  ]
199
208
 
200
- console.log(possibleExportNames)
201
-
202
209
  const exportName = possibleExportNames[0]
203
210
 
204
211
  let entrypointContent: string
@@ -224,7 +231,6 @@ export function CodeAndPreview({ snippet }: Props) {
224
231
  "main.tsx": entrypointContent,
225
232
  }
226
233
  }, [code, manualEditsFileContent])
227
- console.log(fsMap)
228
234
 
229
235
  if (!snippet && (urlParams.snippet_id || urlParams.should_create_snippet)) {
230
236
  return (
@@ -12,12 +12,16 @@ export function CreateNewSnippetWithAiHero() {
12
12
  const handleSubmit = (e: React.FormEvent) => {
13
13
  e.preventDefault()
14
14
  if (inputValue.trim()) {
15
- navigate(`/ai?initial_prompt=${encodeURIComponent(inputValue)}`)
15
+ navigate(
16
+ `https://chat.tscircuit.com?initial_prompt=${encodeURIComponent(inputValue)}`,
17
+ )
16
18
  }
17
19
  }
18
20
 
19
21
  const handleQuickPrompt = (prompt: string) => {
20
- navigate(`/ai?initial_prompt=${encodeURIComponent(prompt)}`)
22
+ navigate(
23
+ `https://chat.tscircuit.com?initial_prompt=${encodeURIComponent(prompt)}`,
24
+ )
21
25
  }
22
26
 
23
27
  return (
@@ -48,6 +48,7 @@ import { SnippetLink } from "./SnippetLink"
48
48
  import { TypeBadge } from "./TypeBadge"
49
49
  import { useUpdateDescriptionDialog } from "./dialogs/edit-description-dialog"
50
50
  import { useForkSnippetMutation } from "@/hooks/useForkSnippetMutation"
51
+ import tscircuitCorePkg from "@tscircuit/core/package.json"
51
52
 
52
53
  export default function EditorNav({
53
54
  circuitJson,
@@ -367,6 +368,9 @@ export default function EditorNav({
367
368
  <Trash2 className="mr-2 h-3 w-3" />
368
369
  Delete Snippet
369
370
  </DropdownMenuItem>
371
+ <DropdownMenuItem className="text-xs text-gray-500" disabled>
372
+ @tscircuit/core@{tscircuitCorePkg.version}
373
+ </DropdownMenuItem>
370
374
  </DropdownMenuContent>
371
375
  </DropdownMenu>
372
376
  <Button
@@ -26,7 +26,7 @@ export default function Footer() {
26
26
  { name: "Home", href: "/" },
27
27
  { name: "Dashboard", href: "/dashboard" },
28
28
  { name: "Editor", href: "/editor" },
29
- { name: "Create with AI", href: "/ai" },
29
+ { name: "Create with AI", href: "https://chat.tscircuit.com" },
30
30
  {
31
31
  name: "My Profile",
32
32
  href: `/${session?.github_username}`,
@@ -75,16 +75,15 @@ export default function Header() {
75
75
  <HeaderButton href="/dashboard">Dashboard</HeaderButton>
76
76
  </li>
77
77
  )}
78
- <li>
79
- <HeaderButton href="/newest">Newest</HeaderButton>
80
- </li>
81
78
  <li>
82
79
  <HeaderButton href="/quickstart" alsoHighlightForUrl="/editor">
83
80
  Editor
84
81
  </HeaderButton>
85
82
  </li>
86
83
  <li>
87
- <HeaderButton href="/ai">AI</HeaderButton>
84
+ <a href="https://chat.tscircuit.com">
85
+ <Button variant="ghost">AI</Button>
86
+ </a>
88
87
  </li>
89
88
  <li>
90
89
  <a href="https://docs.tscircuit.com">
@@ -144,11 +143,6 @@ export default function Header() {
144
143
  </HeaderButton>
145
144
  </li>
146
145
  )}
147
- <li>
148
- <HeaderButton className="w-full justify-start" href="/newest">
149
- Newest
150
- </HeaderButton>
151
- </li>
152
146
  <li>
153
147
  <HeaderButton
154
148
  className="w-full justify-start"
@@ -159,7 +153,10 @@ export default function Header() {
159
153
  </HeaderButton>
160
154
  </li>
161
155
  <li>
162
- <HeaderButton className="w-full justify-start" href="/ai">
156
+ <HeaderButton
157
+ className="w-full justify-start"
158
+ href="https://chat.tscircuit.com"
159
+ >
163
160
  AI
164
161
  </HeaderButton>
165
162
  </li>
@@ -41,7 +41,7 @@ export const HeaderLogin: React.FC<HeaderLoginProps> = () => {
41
41
  }
42
42
  }
43
43
  return (
44
- <div className="flex items-center space-x-2 justify-end">
44
+ <div className="flex items-center md:space-x-2 justify-end">
45
45
  <Button onClick={() => signIn()} variant="ghost">
46
46
  Log in
47
47
  </Button>
@@ -320,7 +320,10 @@ export const PreviewContent = ({
320
320
  >
321
321
  <ErrorBoundary FallbackComponent={ErrorFallback}>
322
322
  {circuitJson ? (
323
- <CadViewer soup={circuitJson as any} ref={threeJsObjectRef} />
323
+ <CadViewer
324
+ circuitJson={circuitJson as any}
325
+ ref={threeJsObjectRef}
326
+ />
324
327
  ) : (
325
328
  <PreviewEmptyState triggerRunTsx={triggerRunTsx} />
326
329
  )}
@@ -0,0 +1,71 @@
1
+ import { Button } from "@/components/ui/button"
2
+ import { ChevronDown, ChevronUp, Star } from "lucide-react"
3
+ import { Link } from "wouter"
4
+ import { Snippet } from "fake-snippets-api/lib/db/schema"
5
+
6
+ interface SnippetListProps {
7
+ title: string
8
+ snippets?: Snippet[]
9
+ showAll?: boolean
10
+ onToggleShowAll?: () => void
11
+ maxItems?: number
12
+ }
13
+
14
+ export const SnippetList = ({
15
+ title,
16
+ snippets = [],
17
+ showAll = false,
18
+ onToggleShowAll,
19
+ maxItems = 5,
20
+ }: SnippetListProps) => {
21
+ const displayedSnippets = showAll ? snippets : snippets.slice(0, maxItems)
22
+
23
+ return (
24
+ <div>
25
+ <div className="flex items-center justify-between">
26
+ <h2 className="text-sm font-bold text-gray-700">{title}</h2>
27
+ {snippets.length > maxItems && onToggleShowAll && (
28
+ <Button
29
+ variant="ghost"
30
+ size="sm"
31
+ onClick={onToggleShowAll}
32
+ className="text-blue-600 hover:text-blue-700 hover:bg-transparent"
33
+ >
34
+ {showAll ? (
35
+ <>
36
+ Show less <ChevronUp className="w-3 h-3 ml-1" />
37
+ </>
38
+ ) : (
39
+ <>
40
+ Show more <ChevronDown className="w-3 h-3 ml-1" />
41
+ </>
42
+ )}
43
+ </Button>
44
+ )}
45
+ </div>
46
+ <div className="border-b border-gray-200" />
47
+ {snippets && (
48
+ <ul className="space-y-1 mt-2">
49
+ {displayedSnippets.map((snippet) => (
50
+ <li key={snippet.snippet_id}>
51
+ <div className="flex items-center">
52
+ <Link
53
+ href={`/${snippet.owner_name}/${snippet.unscoped_name}`}
54
+ className="text-blue-600 hover:underline text-sm"
55
+ >
56
+ {snippet.owner_name}/{snippet.unscoped_name}
57
+ </Link>
58
+ {snippet.star_count > 0 && (
59
+ <span className="ml-2 text-gray-500 text-xs flex items-center">
60
+ <Star className="w-3 h-3 mr-1" />
61
+ {snippet.star_count}
62
+ </span>
63
+ )}
64
+ </div>
65
+ </li>
66
+ ))}
67
+ </ul>
68
+ )}
69
+ </div>
70
+ )
71
+ }
@@ -3,11 +3,12 @@ export const blinkingLedBoardTemplate = {
3
3
  code: `
4
4
  import { useUsbC } from "@tsci/seveibar.smd-usb-c"
5
5
  import { useRedLed } from "@tsci/seveibar.red-led"
6
- import { A555Timer } from "@tsci/seveibar.a555timer"
6
+ import { useNE555P } from "@tsci/seveibar.a555timer"
7
7
 
8
8
  export const MyBlinkingLedCircuit = () => {
9
9
  const USBC = useUsbC("USBC")
10
10
  const Led = useRedLed("LED")
11
+ const A555Timer = useNE555P("B1")
11
12
 
12
13
  return (
13
14
  <board width="30mm" height="30mm" schAutoLayoutEnabled>