@tscircuit/fake-snippets 0.0.88 → 0.0.90

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 (76) 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 +196 -215
  4. package/dist/bundle.js +596 -370
  5. package/fake-snippets-api/routes/api/autocomplete/create_autocomplete.ts +134 -0
  6. package/fake-snippets-api/routes/api/proxy.ts +128 -0
  7. package/package.json +59 -48
  8. package/renovate.json +2 -1
  9. package/src/App.tsx +67 -3
  10. package/src/ContextProviders.tsx +2 -0
  11. package/src/build-watcher.ts +52 -0
  12. package/src/components/CircuitJsonImportDialog.tsx +1 -1
  13. package/src/components/CmdKMenu.tsx +533 -197
  14. package/src/components/DownloadButtonAndMenu.tsx +104 -26
  15. package/src/components/FileSidebar.tsx +11 -1
  16. package/src/components/Header2.tsx +7 -2
  17. package/src/components/PackageBuildsPage/LogContent.tsx +25 -22
  18. package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +6 -6
  19. package/src/components/PackageBuildsPage/build-preview-content.tsx +5 -5
  20. package/src/components/PackageBuildsPage/package-build-details-panel.tsx +15 -13
  21. package/src/components/PackageBuildsPage/package-build-header.tsx +19 -28
  22. package/src/components/PackageCard.tsx +66 -16
  23. package/src/components/SearchComponent.tsx +2 -2
  24. package/src/components/SuspenseRunFrame.tsx +14 -2
  25. package/src/components/ViewPackagePage/components/important-files-view.tsx +90 -17
  26. package/src/components/ViewPackagePage/components/main-content-header.tsx +26 -2
  27. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +2 -2
  28. package/src/components/ViewPackagePage/components/repo-page-content.tsx +35 -30
  29. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +2 -2
  30. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +20 -12
  31. package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +0 -7
  32. package/src/components/ViewPackagePage/utils/fuzz-search.ts +121 -0
  33. package/src/components/ViewPackagePage/utils/is-hidden-file.ts +4 -0
  34. package/src/components/dialogs/confirm-delete-package-dialog.tsx +1 -1
  35. package/src/components/dialogs/confirm-discard-changes-dialog.tsx +73 -0
  36. package/src/components/dialogs/edit-package-details-dialog.tsx +2 -2
  37. package/src/components/dialogs/view-ts-files-dialog.tsx +478 -42
  38. package/src/components/package-port/CodeAndPreview.tsx +17 -16
  39. package/src/components/package-port/CodeEditor.tsx +138 -17
  40. package/src/components/package-port/CodeEditorHeader.tsx +44 -4
  41. package/src/components/package-port/EditorNav.tsx +42 -29
  42. package/src/components/package-port/GlobalFindReplace.tsx +681 -0
  43. package/src/components/package-port/QuickOpen.tsx +241 -0
  44. package/src/components/ui/dialog.tsx +1 -1
  45. package/src/components/ui/tree-view.tsx +1 -1
  46. package/src/global.d.ts +3 -0
  47. package/src/hooks/use-ai-review.ts +31 -0
  48. package/src/hooks/use-code-completion-ai-api.ts +3 -3
  49. package/src/hooks/use-current-package-release.ts +5 -1
  50. package/src/hooks/use-delete-package.ts +6 -2
  51. package/src/hooks/use-download-zip.ts +50 -0
  52. package/src/hooks/use-hotkey.ts +116 -0
  53. package/src/hooks/use-package-by-package-id.ts +1 -0
  54. package/src/hooks/use-package-by-package-name.ts +1 -0
  55. package/src/hooks/use-package-files.ts +3 -0
  56. package/src/hooks/use-package-release.ts +5 -1
  57. package/src/hooks/use-package.ts +1 -0
  58. package/src/hooks/use-request-ai-review-mutation.ts +14 -6
  59. package/src/hooks/use-snippet.ts +1 -0
  60. package/src/hooks/useFileManagement.ts +28 -10
  61. package/src/hooks/usePackageFilesLoader.ts +3 -1
  62. package/src/index.css +11 -0
  63. package/src/lib/decodeUrlHashToFsMap.ts +17 -0
  64. package/src/lib/download-fns/download-circuit-png.ts +88 -0
  65. package/src/lib/download-fns/download-png-utils.ts +31 -0
  66. package/src/lib/encodeFsMapToUrlHash.ts +13 -0
  67. package/src/lib/populate-query-cache-with-ssr-data.ts +7 -0
  68. package/src/lib/ts-lib-cache.ts +47 -0
  69. package/src/lib/types.ts +2 -0
  70. package/src/lib/utils/findTargetFile.ts +1 -1
  71. package/src/lib/utils/package-utils.ts +10 -0
  72. package/src/main.tsx +7 -0
  73. package/src/pages/dashboard.tsx +18 -5
  74. package/src/pages/view-package.tsx +15 -7
  75. package/src/types/package.ts +4 -0
  76. package/vite.config.ts +100 -1
@@ -0,0 +1,134 @@
1
+ import "dotenv/config"
2
+ import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
3
+ import { z } from "zod"
4
+ import OpenAI from "openai"
5
+
6
+ // Lazy-loaded client instance
7
+ let openai: OpenAI | null = null
8
+ let cachedReadme: string | null = null
9
+
10
+ function getOpenAIClient() {
11
+ const apiKey = process.env.VITE_OPENROUTER_API_KEY
12
+ if (!apiKey) {
13
+ throw new Error("Missing Api Key in env")
14
+ }
15
+
16
+ if (!openai) {
17
+ openai = new OpenAI({
18
+ apiKey,
19
+ baseURL: "https://openrouter.ai/api/v1",
20
+ defaultHeaders: {
21
+ "HTTP-Referer": "https://tscircuit.com",
22
+ "X-Title": "TSCircuit Editor",
23
+ },
24
+ })
25
+ }
26
+
27
+ return openai
28
+ }
29
+
30
+ // Cache README
31
+ async function getCachedReadme(): Promise<string> {
32
+ if (cachedReadme !== null) return cachedReadme
33
+ const res = await fetch(
34
+ "https://raw.githubusercontent.com/tscircuit/props/main/README.md",
35
+ )
36
+ if (!res.ok) {
37
+ throw new Error(`Failed to fetch README: ${res.status}`)
38
+ }
39
+ cachedReadme = await res.text()
40
+ return cachedReadme
41
+ }
42
+
43
+ async function completion(
44
+ openai: OpenAI,
45
+ readmeContent: string,
46
+ prefix: string,
47
+ suffix: string,
48
+ model = "openai/gpt-4.1-mini",
49
+ language?: string,
50
+ ) {
51
+ const systemMessage = `You are an expert ${language ? language + " " : ""}programmer working in a TSX (TypeScript + React JSX) environment.
52
+
53
+ Below is the README.md for the available components. You MUST use this to determine which components and props are valid.
54
+ Only use components explicitly documented under Available Components in the README. Never invent or guess new components. If the user partially types a component that does not exist in the README, do NOT try to complete it.
55
+
56
+ ===== README.md START =====
57
+ ${readmeContent}
58
+ ===== README.md END =====
59
+
60
+ Special instruction for the <chip> component:
61
+ - Do NOT add chip as a prop (e.g., <chip chip="..."> is invalid).
62
+ - Always use this format:
63
+ <chip name="U<number>" footprint="<valid footprint>" pinLabels={{}} pcbX={0} pcbY={0} schX={0} schY={0} />
64
+ - Determine the next sequential name automatically: e.g. U1, U2, U3.
65
+ - Only use valid footprints and pinLabels from the README.
66
+ - Some components like <netlabel> do not have a 'name' prop — do not add it for those.
67
+
68
+ STRICT rules:
69
+ - If partial like "<capa", only append remaining "citor". Never repeat letters.
70
+ - If input is "<capacitor", add only props, never repeat tag.
71
+ - Always produce exactly one JSX component, starting with "<" if needed.
72
+ - If partial doesn’t match any valid component, output nothing.
73
+ - Never output two JSX elements. Always end with exactly one "/>".
74
+ - Never add duplicate closing "/>".
75
+ - Never output the component name as a prop.
76
+ - Never add whitespace before your completion.
77
+ - If the input is exactly "<", then start with the component name directly (like "resistor ... />") without adding another "<".
78
+ - So that the final result is "<resistor ... />", not "<<resistor ... />".
79
+ - Never produce a double "<".
80
+
81
+ Examples:
82
+ - Input: "<FILL_ME>"
83
+ Output: <resistor name="R1" footprint="0603" pcbX={5} pcbY={7} schX={1} schY={2} resistance={1000} />
84
+ - Input: "<ca<FILL_ME>"
85
+ Output: pacitor name="C1" footprint="0805" pcbX={10} pcbY={15} schX={3} schY={4} />
86
+ - Input: "<chip<FILL_ME>"
87
+ Output: name="U1" footprint="SOIC-8" pinLabels={{}} pcbX={0} pcbY={0} schX={0} schY={0} />
88
+ - Input: "<netl<FILL_ME>"
89
+ Output: abel name="N1" />
90
+ - NEVER output: <capacitor capacitor ... /> or <netnet ... />
91
+ - Input: "<"
92
+ Output: resistor name="R1" footprint="0603" pcbX={5} pcbY={7} schX={1} schY={2} resistance={1000} />
93
+ - Input: "<ca"
94
+ Output: pacitor name="C1" footprint="0805" pcbX={10} pcbY={15} schX={3} schY={4} />
95
+ - Input: "<capacitor"
96
+ Output: capacitance="1000pF" footprint="0805" name="C1" pcbX={10} pcbY={15} schX={3} schY={4} />`
97
+
98
+ const chatCompletion = await openai.chat.completions.create({
99
+ messages: [
100
+ { role: "system", content: systemMessage },
101
+ { role: "user", content: `${prefix}<FILL_ME>${suffix}` },
102
+ ],
103
+ model,
104
+ })
105
+
106
+ return chatCompletion.choices[0].message?.content ?? ""
107
+ }
108
+ export default withRouteSpec({
109
+ methods: ["POST"],
110
+ auth: "session", // ✅ Require user to be signed in
111
+ jsonBody: z.object({
112
+ prefix: z.string(),
113
+ suffix: z.string(),
114
+ model: z.string().optional(),
115
+ language: z.string().optional(),
116
+ }),
117
+ jsonResponse: z.object({
118
+ prediction: z.string(),
119
+ }),
120
+ })(async (req, ctx) => {
121
+ const openai = getOpenAIClient()
122
+ const { prefix, suffix, model, language } = req.jsonBody
123
+
124
+ const readmeContent = await getCachedReadme()
125
+ const predictionResult = await completion(
126
+ openai,
127
+ readmeContent,
128
+ prefix,
129
+ suffix,
130
+ model,
131
+ language,
132
+ )
133
+ return ctx.json({ prediction: predictionResult })
134
+ })
@@ -0,0 +1,128 @@
1
+ import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
2
+ import { z } from "zod"
3
+
4
+ const PROXY_HEADERS = [
5
+ "X-Target-Url",
6
+ "X-Sender-Origin",
7
+ "X-Sender-Host",
8
+ "X-Sender-Referer",
9
+ "X-Sender-User-Agent",
10
+ "X-Sender-Cookie",
11
+ ]
12
+
13
+ const ALLOWED_DOMAINS: Array<{ domain: string; routes?: string[] }> = [
14
+ { domain: "easyeda.com" },
15
+ ]
16
+
17
+ function isAllowedDomain(url: string) {
18
+ try {
19
+ const { hostname, pathname, port } = new URL(url)
20
+ const allowedDomain = ALLOWED_DOMAINS.find(
21
+ (domain) => domain.domain === hostname,
22
+ )
23
+
24
+ if (allowedDomain) {
25
+ if (
26
+ !allowedDomain.routes ||
27
+ allowedDomain.routes.some((route) => pathname.startsWith(route))
28
+ ) {
29
+ return true
30
+ }
31
+ }
32
+
33
+ return hostname === "localhost" || (hostname === "127.0.0.1" && port)
34
+ } catch {
35
+ return false
36
+ }
37
+ }
38
+
39
+ export default withRouteSpec({
40
+ methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
41
+ jsonResponse: z.any(),
42
+ auth: "session",
43
+ })(async (req, ctx) => {
44
+ if (req.headers.get("X-proxied")) {
45
+ return ctx.json(
46
+ { error: "Recursive proxy calls are not allowed" },
47
+ { status: 403 },
48
+ )
49
+ }
50
+
51
+ const targetUrl = req.headers.get("X-Target-Url")
52
+
53
+ if (!targetUrl) {
54
+ return ctx.json(
55
+ { error: "X-Target-Url header is required" },
56
+ { status: 400 },
57
+ )
58
+ }
59
+
60
+ if (!isAllowedDomain(targetUrl)) {
61
+ return ctx.json({ error: "Domain not allowed" }, { status: 403 })
62
+ }
63
+
64
+ let body: string | undefined
65
+ if (["POST", "PUT", "PATCH"].includes(req.method)) {
66
+ body = await req.clone().text()
67
+ }
68
+
69
+ const headers = new Headers(req.headers)
70
+
71
+ const senderOrigin = req.headers.get("X-Sender-Origin")
72
+ if (senderOrigin) {
73
+ headers.set("Origin", senderOrigin)
74
+ }
75
+
76
+ const senderHost = req.headers.get("X-Sender-Host")
77
+ if (senderHost) {
78
+ const hostValue = senderHost.replace(/^https?:\/\//, "")
79
+ headers.set("Host", hostValue)
80
+ headers.set("authority", hostValue)
81
+ }
82
+
83
+ const senderReferer = req.headers.get("X-Sender-Referer")
84
+ if (senderReferer) {
85
+ headers.set("Referer", senderReferer)
86
+ }
87
+
88
+ const senderUserAgent = req.headers.get("X-Sender-User-Agent")
89
+ if (senderUserAgent) {
90
+ headers.set("User-Agent", senderUserAgent)
91
+ }
92
+
93
+ const senderCookie = req.headers.get("X-Sender-Cookie")
94
+ if (senderCookie) {
95
+ headers.set("Cookie", senderCookie)
96
+ }
97
+
98
+ for (const header of PROXY_HEADERS) {
99
+ headers.delete(header)
100
+ }
101
+
102
+ headers.delete("content-encoding")
103
+ headers.delete("accept-encoding")
104
+ headers.set("X-proxied", "true")
105
+
106
+ try {
107
+ const response = await fetch(targetUrl, {
108
+ method: req.method,
109
+ headers: headers,
110
+ body: ["GET", "HEAD"].includes(req.method) ? undefined : body,
111
+ })
112
+
113
+ const responseHeaders = new Headers(response.headers)
114
+ responseHeaders.delete("content-encoding")
115
+
116
+ return new Response(response.body, {
117
+ status: response.status,
118
+ statusText: response.statusText,
119
+ headers: responseHeaders,
120
+ })
121
+ } catch (error) {
122
+ console.error("Proxy error:", error)
123
+ return ctx.json(
124
+ { error: { message: "Failed to proxy request" } },
125
+ { status: 502 },
126
+ )
127
+ }
128
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fake-snippets",
3
- "version": "0.0.88",
3
+ "version": "0.0.90",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,8 +33,11 @@
33
33
  "generate-sitemap": "bun run scripts/generate-sitemap.ts"
34
34
  },
35
35
  "devDependencies": {
36
+ "@anthropic-ai/sdk": "^0.27.3",
36
37
  "@babel/preset-react": "^7.25.9",
37
38
  "@babel/preset-typescript": "^7.26.0",
39
+ "@babel/standalone": "^7.26.2",
40
+ "@biomejs/biome": "^1.9.2",
38
41
  "@codemirror/autocomplete": "^6.18.1",
39
42
  "@codemirror/lang-javascript": "^6.2.2",
40
43
  "@codemirror/lang-json": "^6.0.1",
@@ -42,6 +45,7 @@
42
45
  "@codemirror/state": "^6.4.1",
43
46
  "@codemirror/view": "^6.34.1",
44
47
  "@hookform/resolvers": "^3.9.0",
48
+ "@playwright/test": "^1.48.0",
45
49
  "@radix-ui/react-accordion": "^1.2.4",
46
50
  "@radix-ui/react-alert-dialog": "^1.1.1",
47
51
  "@radix-ui/react-aspect-ratio": "^1.1.0",
@@ -70,36 +74,65 @@
70
74
  "@radix-ui/react-toggle": "^1.1.0",
71
75
  "@radix-ui/react-toggle-group": "^1.1.0",
72
76
  "@radix-ui/react-tooltip": "^1.1.2",
73
- "@tscircuit/footprinter": "^0.0.176",
77
+ "@tailwindcss/typography": "^0.5.16",
78
+ "@tscircuit/3d-viewer": "^0.0.279",
79
+ "@tscircuit/assembly-viewer": "^0.0.1",
80
+ "@tscircuit/core": "^0.0.536",
81
+ "@tscircuit/create-snippet-url": "^0.0.8",
82
+ "@tscircuit/eval": "^0.0.244",
83
+ "@tscircuit/footprinter": "^0.0.186",
74
84
  "@tscircuit/layout": "^0.0.29",
75
85
  "@tscircuit/math-utils": "^0.0.10",
76
86
  "@tscircuit/mm": "^0.0.8",
77
- "@tscircuit/props": "^0.0.194",
87
+ "@tscircuit/pcb-viewer": "^1.11.194",
88
+ "@tscircuit/prompt-benchmarks": "^0.0.28",
89
+ "@tscircuit/props": "^0.0.246",
90
+ "@tscircuit/runframe": "^0.0.669",
91
+ "@tscircuit/schematic-viewer": "^2.0.21",
92
+ "@types/babel__standalone": "^7.1.7",
93
+ "@types/bun": "^1.1.10",
94
+ "@types/country-list": "^2.1.4",
78
95
  "@types/file-saver": "^2.0.7",
96
+ "@types/md5": "^2.3.5",
79
97
  "@types/ms": "^0.7.34",
98
+ "@types/node": "^22.13.0",
99
+ "@types/prismjs": "^1.26.4",
100
+ "@types/react": "^18.3.9",
101
+ "@types/react-dom": "^18.3.0",
102
+ "@types/react-helmet": "^6.1.11",
103
+ "@types/sharp": "^0.32.0",
80
104
  "@typescript/ata": "^0.9.7",
81
- "@valtown/codemirror-codeium": "^1.1.1",
105
+ "@typescript/vfs": "^1.6.0",
82
106
  "@valtown/codemirror-ts": "^2.2.0",
83
107
  "@vercel/analytics": "^1.4.1",
108
+ "@vitejs/plugin-react": "^4.3.1",
109
+ "autoprefixer": "^10.4.20",
84
110
  "change-case": "^5.4.4",
85
111
  "circuit-json": "^0.0.190",
86
- "circuit-json-to-bom-csv": "^0.0.6",
87
- "circuit-json-to-gerber": "^0.0.21",
88
- "circuit-json-to-pnp-csv": "^0.0.6",
112
+ "circuit-json-to-bom-csv": "^0.0.7",
113
+ "circuit-json-to-gerber": "^0.0.25",
114
+ "circuit-json-to-pnp-csv": "^0.0.7",
89
115
  "circuit-json-to-readable-netlist": "^0.0.13",
90
116
  "circuit-json-to-tscircuit": "^0.0.4",
117
+ "circuit-to-svg": "^0.0.163",
91
118
  "class-variance-authority": "^0.7.1",
92
119
  "clsx": "^2.1.1",
93
120
  "cmdk": "^1.0.4",
94
121
  "codemirror": "^6.0.1",
122
+ "codemirror-copilot": "^0.0.7",
95
123
  "country-list": "^2.3.0",
96
124
  "date-fns": "^4.1.0",
125
+ "dotenv": "^16.5.0",
97
126
  "dsn-converter": "^0.0.60",
98
- "easyeda": "^0.0.129",
127
+ "easyeda": "^0.0.195",
99
128
  "embla-carousel-react": "^8.3.0",
100
129
  "extract-codefence": "^0.0.4",
101
130
  "fflate": "^0.8.2",
102
131
  "file-saver": "^2.0.5",
132
+ "get-port": "^7.1.0",
133
+ "globals": "^15.9.0",
134
+ "he": "^1.2.0",
135
+ "idb-keyval": "^6.2.2",
103
136
  "immer": "^10.1.1",
104
137
  "input-otp": "^1.2.4",
105
138
  "javascript-time-ago": "^2.5.11",
@@ -107,11 +140,18 @@
107
140
  "jscad-electronics": "^0.0.25",
108
141
  "jszip": "^3.10.1",
109
142
  "kicad-converter": "^0.0.16",
143
+ "ky": "^1.7.5",
110
144
  "lucide-react": "^0.488.0",
145
+ "lz-string": "^1.5.0",
111
146
  "md5": "^2.3.0",
112
147
  "ms": "^2.1.3",
113
148
  "next-themes": "^0.3.0",
149
+ "openai": "^5.6.0",
150
+ "postcss": "^8.4.47",
114
151
  "posthog-js": "^1.203.2",
152
+ "prismjs": "^1.29.0",
153
+ "prompts": "^2.4.2",
154
+ "react": "^18.3.1",
115
155
  "react-cookie-consent": "^9.0.0",
116
156
  "react-day-picker": "8.10.1",
117
157
  "react-dom": "^18.3.1",
@@ -124,60 +164,31 @@
124
164
  "react-query": "^3.39.3",
125
165
  "react-resizable-panels": "^2.1.3",
126
166
  "recharts": "^2.12.7",
167
+ "redaxios": "^0.5.1",
127
168
  "remark-gfm": "^4.0.1",
128
169
  "rollup-plugin-visualizer": "^5.12.0",
129
170
  "schematic-symbols": "^0.0.155",
171
+ "sharp": "^0.33.5",
172
+ "shiki": "^3.2.1",
130
173
  "sitemap": "^8.0.0",
131
174
  "sonner": "^1.5.0",
132
175
  "states-us": "^1.1.1",
133
176
  "tailwind-merge": "^2.5.2",
134
- "tailwindcss-animate": "^1.0.7",
135
- "use-async-memo": "^1.2.5",
136
- "use-mouse-matrix-transform": "^1.3.0",
137
- "vaul": "^0.9.9",
138
- "vite-plugin-vercel": "^9.0.4",
139
- "wouter": "^3.3.5",
140
- "@anthropic-ai/sdk": "^0.27.3",
141
- "@babel/standalone": "^7.26.2",
142
- "@biomejs/biome": "^1.9.2",
143
- "@playwright/test": "^1.48.0",
144
- "@tailwindcss/typography": "^0.5.16",
145
- "@tscircuit/core": "^0.0.433",
146
- "@tscircuit/eval": "^0.0.227",
147
- "@tscircuit/prompt-benchmarks": "^0.0.28",
148
- "@tscircuit/runframe": "^0.0.582",
149
- "@types/babel__standalone": "^7.1.7",
150
- "@types/bun": "^1.1.10",
151
- "@types/country-list": "^2.1.4",
152
- "@types/md5": "^2.3.5",
153
- "@types/node": "^22.13.0",
154
- "@types/prismjs": "^1.26.4",
155
- "@types/react": "^18.3.9",
156
- "@types/react-dom": "^18.3.0",
157
- "@types/react-helmet": "^6.1.11",
158
- "@types/sharp": "^0.32.0",
159
- "@typescript/vfs": "^1.6.0",
160
- "@vitejs/plugin-react": "^4.3.1",
161
- "autoprefixer": "^10.4.20",
162
- "circuit-to-svg": "^0.0.152",
163
- "get-port": "^7.1.0",
164
- "globals": "^15.9.0",
165
- "he": "^1.2.0",
166
- "ky": "^1.7.5",
167
- "postcss": "^8.4.47",
168
- "prismjs": "^1.29.0",
169
- "prompts": "^2.4.2",
170
- "react": "^18.3.1",
171
- "redaxios": "^0.5.1",
172
- "sharp": "^0.33.5",
173
- "shiki": "^3.2.1",
174
177
  "tailwindcss": "^3.4.13",
178
+ "tailwindcss-animate": "^1.0.7",
175
179
  "terser": "^5.27.0",
180
+ "three": "^0.177.0",
181
+ "three-stdlib": "^2.36.0",
176
182
  "tsup": "^8.5.0",
177
183
  "typescript": "^5.6.3",
184
+ "use-async-memo": "^1.2.5",
185
+ "use-mouse-matrix-transform": "^1.3.0",
186
+ "vaul": "^0.9.9",
178
187
  "vite": "^6.3.4",
179
188
  "vite-plugin-image-optimizer": "^1.1.8",
189
+ "vite-plugin-vercel": "^9.0.4",
180
190
  "winterspec": "^0.0.107",
191
+ "wouter": "^3.3.5",
181
192
  "zod": "^3.23.8",
182
193
  "zustand": "^4.5.5",
183
194
  "zustand-hoist": "^2.0.1"
package/renovate.json CHANGED
@@ -10,7 +10,8 @@
10
10
  "jscad-electronics",
11
11
  "circuit-json",
12
12
  "dsn-converter",
13
- "circuit-json-to-readable-netlist"
13
+ "circuit-json-to-readable-netlist",
14
+ "circuit-*"
14
15
  ],
15
16
  "enabled": false
16
17
  },
package/src/App.tsx CHANGED
@@ -78,18 +78,82 @@ const PackageEditorPage = lazyImport(async () => {
78
78
 
79
79
  class ErrorBoundary extends React.Component<
80
80
  { children: React.ReactNode },
81
- { hasError: boolean }
81
+ { hasError: boolean; reloading: boolean }
82
82
  > {
83
+ private visibilityHandler?: () => void
84
+ private reloadTimeout?: number
85
+
83
86
  constructor(props: { children: React.ReactNode }) {
84
87
  super(props)
85
- this.state = { hasError: false }
88
+ this.state = { hasError: false, reloading: false }
86
89
  }
87
90
 
88
91
  static getDerivedStateFromError() {
89
- return { hasError: true }
92
+ return { hasError: true, reloading: false }
93
+ }
94
+
95
+ componentDidCatch(error: Error) {
96
+ console.error("ErrorBoundary caught", error)
97
+ const message = error.message || ""
98
+ if (
99
+ /(Loading chunk|ChunkLoadError|dynamically imported module)/i.test(
100
+ message,
101
+ )
102
+ ) {
103
+ const loadedAt = window.__APP_LOADED_AT || Date.now()
104
+ if (Date.now() - loadedAt >= 10_000) {
105
+ this.performReload()
106
+ }
107
+ }
108
+ }
109
+
110
+ componentDidUpdate(_prevProps: any, prevState: any) {
111
+ if (!prevState.hasError && this.state.hasError && !this.state.reloading) {
112
+ this.setupIdleReload()
113
+ }
114
+ }
115
+
116
+ componentWillUnmount() {
117
+ this.cleanup()
118
+ }
119
+
120
+ cleanup = () => {
121
+ if (this.visibilityHandler) {
122
+ document.removeEventListener("visibilitychange", this.visibilityHandler)
123
+ this.visibilityHandler = undefined
124
+ }
125
+ if (this.reloadTimeout) {
126
+ clearTimeout(this.reloadTimeout)
127
+ this.reloadTimeout = undefined
128
+ }
129
+ }
130
+
131
+ performReload = () => {
132
+ if (this.state.reloading) return // Prevent multiple reloads
133
+
134
+ this.cleanup() // Clean up listeners before reload
135
+ this.setState({ reloading: true })
136
+ this.reloadTimeout = window.setTimeout(() => {
137
+ window.location.reload()
138
+ }, 500)
139
+ }
140
+
141
+ setupIdleReload = () => {
142
+ this.cleanup() // Clean up any existing handlers
143
+
144
+ this.visibilityHandler = () => {
145
+ if (!document.hidden && this.state.hasError && !this.state.reloading) {
146
+ this.performReload()
147
+ }
148
+ }
149
+
150
+ document.addEventListener("visibilitychange", this.visibilityHandler)
90
151
  }
91
152
 
92
153
  render() {
154
+ if (this.state.reloading) {
155
+ return <div>There was a problem loading this page. Reloading…</div>
156
+ }
93
157
  if (this.state.hasError) {
94
158
  return <div>Something went wrong loading the page.</div>
95
159
  }
@@ -4,6 +4,7 @@ import { useEffect } from "react"
4
4
  import { useGlobalStore } from "./hooks/use-global-store"
5
5
  import { posthog } from "./lib/posthog"
6
6
  import { Toaster } from "react-hot-toast"
7
+ import { Toaster as SonnerToaster } from "@/components/ui/sonner"
7
8
  import { populateQueryCacheWithSSRData } from "./lib/populate-query-cache-with-ssr-data"
8
9
 
9
10
  const staffGithubUsernames = [
@@ -65,6 +66,7 @@ export const ContextProviders = ({ children }: any) => {
65
66
  <PostHogIdentifier />
66
67
  {children}
67
68
  <Toaster position="bottom-right" />
69
+ <SonnerToaster position="bottom-left" />
68
70
  </HelmetProvider>
69
71
  </QueryClientProvider>
70
72
  )
@@ -0,0 +1,52 @@
1
+ import { toast } from "sonner"
2
+
3
+ export function setupBuildWatcher() {
4
+ const meta = document.querySelector<HTMLMetaElement>(
5
+ 'meta[name="tscircuit-build"]',
6
+ )
7
+ const currentId = meta?.content || ""
8
+ ;(window as any).TSC_BUILD_ID = currentId
9
+
10
+ async function fetchBuildId(): Promise<string | null> {
11
+ try {
12
+ const res = await fetch("/api/generated-index", { cache: "no-store" })
13
+ const text = await res.text()
14
+ const match = text.match(
15
+ /<meta name="tscircuit-build" content="([^"]+)"/i,
16
+ )
17
+ return match ? match[1] : null
18
+ } catch (err) {
19
+ console.error("Failed to fetch build identifier", err)
20
+ return null
21
+ }
22
+ }
23
+
24
+ let updateToastShown = false
25
+
26
+ async function checkForUpdate() {
27
+ const serverId = await fetchBuildId()
28
+ if (
29
+ serverId &&
30
+ serverId !== (window as any).TSC_BUILD_ID &&
31
+ !updateToastShown
32
+ ) {
33
+ updateToastShown = true
34
+ toast("A new version of tscircuit.com is available.", {
35
+ action: {
36
+ label: "Reload",
37
+ onClick: () => window.location.reload(),
38
+ },
39
+ duration: Infinity,
40
+ position: "bottom-left",
41
+ })
42
+ }
43
+ }
44
+
45
+ document.addEventListener("visibilitychange", () => {
46
+ if (document.visibilityState === "visible") {
47
+ checkForUpdate()
48
+ }
49
+ })
50
+
51
+ setInterval(checkForUpdate, 5 * 60 * 1000)
52
+ }
@@ -15,7 +15,7 @@ import { useLocation } from "wouter"
15
15
  import { useGlobalStore } from "@/hooks/use-global-store"
16
16
  import { convertCircuitJsonToTscircuit } from "circuit-json-to-tscircuit"
17
17
  import { useCreatePackageMutation } from "@/hooks/use-create-package-mutation"
18
- import { generateRandomPackageName } from "./package-port/CodeAndPreview"
18
+ import { generateRandomPackageName } from "@/lib/utils/package-utils"
19
19
  import { useCreatePackageReleaseMutation } from "@/hooks/use-create-package-release-mutation"
20
20
  import { useCreatePackageFilesMutation } from "@/hooks/use-create-package-files-mutation"
21
21