circuit-json-to-tscircuit 0.0.42 → 0.0.44

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/dist/cli/main.js CHANGED
@@ -16,9 +16,11 @@ var package_default = {
16
16
  "./cli": "./dist/cli/main.js"
17
17
  },
18
18
  type: "module",
19
- version: "0.0.41",
19
+ version: "0.0.43",
20
20
  scripts: {
21
21
  build: "tsup-node lib/index.ts cli/main.ts --format esm --dts",
22
+ "build:site": "bun build ./site/index.html --outdir site-build",
23
+ "dev:site": "bun --hot ./site/index.html",
22
24
  test: "bun test",
23
25
  format: "biome format --write .",
24
26
  "format:check": "biome format ."
@@ -34,6 +36,8 @@ var package_default = {
34
36
  "circuit-json": "^0.0.433",
35
37
  "circuit-to-svg": "^0.0.353",
36
38
  commander: "^13.1.0",
39
+ prettier: "^3.9.5",
40
+ shiki: "^4.3.1",
37
41
  tscircuit: "^0.0.1873",
38
42
  tsup: "^8.3.5"
39
43
  },
package/package.json CHANGED
@@ -6,9 +6,11 @@
6
6
  "./cli": "./dist/cli/main.js"
7
7
  },
8
8
  "type": "module",
9
- "version": "0.0.42",
9
+ "version": "0.0.44",
10
10
  "scripts": {
11
11
  "build": "tsup-node lib/index.ts cli/main.ts --format esm --dts",
12
+ "build:site": "bun build ./site/index.html --outdir site-build",
13
+ "dev:site": "bun --hot ./site/index.html",
12
14
  "test": "bun test",
13
15
  "format": "biome format --write .",
14
16
  "format:check": "biome format ."
@@ -24,6 +26,8 @@
24
26
  "circuit-json": "^0.0.433",
25
27
  "circuit-to-svg": "^0.0.353",
26
28
  "commander": "^13.1.0",
29
+ "prettier": "^3.9.5",
30
+ "shiki": "^4.3.1",
27
31
  "tscircuit": "^0.0.1873",
28
32
  "tsup": "^8.3.5"
29
33
  },
@@ -0,0 +1,609 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Circuit JSON → tscircuit</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ <style>
14
+ :root {
15
+ --bg: #f5f7fa;
16
+ --surface: #ffffff;
17
+ --surface-alt: #f1f5f9;
18
+ --border: #e2e8f0;
19
+ --text: #0f172a;
20
+ --text-muted: #64748b;
21
+ --accent: #2563eb;
22
+ --accent-strong: #1d4ed8;
23
+ --accent-contrast: #ffffff;
24
+ --success: #059669;
25
+ --success-bg: #ecfdf5;
26
+ --success-border: #a7f3d0;
27
+ --error: #dc2626;
28
+ --error-bg: #fef2f2;
29
+ --error-border: #fecaca;
30
+ --processing: #d97706;
31
+ --processing-bg: #fffbeb;
32
+ --processing-border: #fde68a;
33
+ --code-bg: #ffffff;
34
+ --code-header: #f6f8fa;
35
+ --code-text: #24292f;
36
+ --code-text-muted: #6e7781;
37
+ --focus-ring: #2563eb;
38
+ }
39
+
40
+ * {
41
+ margin: 0;
42
+ padding: 0;
43
+ box-sizing: border-box;
44
+ }
45
+
46
+ html,
47
+ body {
48
+ height: 100%;
49
+ }
50
+
51
+ body {
52
+ font-family:
53
+ ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
54
+ Roboto, sans-serif;
55
+ color: var(--text);
56
+ background: var(--bg);
57
+ display: flex;
58
+ flex-direction: column;
59
+ overflow: hidden;
60
+ }
61
+
62
+ code {
63
+ font-family: "Fira Code", ui-monospace, monospace;
64
+ }
65
+
66
+ /* Top bar */
67
+ .topbar {
68
+ flex: none;
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 20px;
72
+ padding: 0 20px;
73
+ height: 56px;
74
+ border-bottom: 1px solid var(--border);
75
+ background: var(--surface);
76
+ }
77
+
78
+ .brand {
79
+ display: flex;
80
+ align-items: center;
81
+ gap: 10px;
82
+ flex: none;
83
+ text-decoration: none;
84
+ }
85
+
86
+ .brand .badge {
87
+ display: inline-flex;
88
+ align-items: center;
89
+ background: var(--accent);
90
+ color: #ffffff;
91
+ font-size: 13px;
92
+ font-weight: 600;
93
+ letter-spacing: -0.01em;
94
+ padding: 4px 9px;
95
+ border-radius: 0.4rem;
96
+ }
97
+
98
+ .brand .path {
99
+ font-family: "Fira Code", ui-monospace, monospace;
100
+ font-size: 12px;
101
+ color: var(--text-muted);
102
+ }
103
+
104
+ .topbar-spacer {
105
+ flex: 1;
106
+ }
107
+
108
+ .status {
109
+ display: none;
110
+ align-items: center;
111
+ gap: 7px;
112
+ padding: 5px 12px;
113
+ border-radius: 999px;
114
+ border: 1px solid transparent;
115
+ font-size: 12.5px;
116
+ flex: none;
117
+ white-space: nowrap;
118
+ }
119
+
120
+ .status.visible {
121
+ display: flex;
122
+ }
123
+
124
+ .status .dot {
125
+ width: 6px;
126
+ height: 6px;
127
+ border-radius: 50%;
128
+ flex-shrink: 0;
129
+ }
130
+
131
+ .status.success {
132
+ background: var(--success-bg);
133
+ color: var(--success);
134
+ border-color: var(--success-border);
135
+ }
136
+ .status.success .dot {
137
+ background: var(--success);
138
+ }
139
+
140
+ .status.error {
141
+ background: var(--error-bg);
142
+ color: var(--error);
143
+ border-color: var(--error-border);
144
+ }
145
+ .status.error .dot {
146
+ background: var(--error);
147
+ }
148
+
149
+ .status.processing {
150
+ background: var(--processing-bg);
151
+ color: var(--processing);
152
+ border-color: var(--processing-border);
153
+ }
154
+ .status.processing .dot {
155
+ background: var(--processing);
156
+ animation: pulse 1.1s ease-in-out infinite;
157
+ }
158
+
159
+ @keyframes pulse {
160
+ 0%,
161
+ 100% {
162
+ opacity: 1;
163
+ }
164
+ 50% {
165
+ opacity: 0.35;
166
+ }
167
+ }
168
+
169
+ button {
170
+ font-family: inherit;
171
+ border: 1px solid transparent;
172
+ border-radius: 0.4rem;
173
+ font-weight: 600;
174
+ cursor: pointer;
175
+ transition:
176
+ transform 0.1s ease,
177
+ background-color 0.15s ease,
178
+ border-color 0.15s ease;
179
+ }
180
+
181
+ button:focus-visible {
182
+ outline: 2px solid var(--focus-ring);
183
+ outline-offset: 2px;
184
+ }
185
+
186
+ .btn-convert {
187
+ flex: none;
188
+ padding: 8px 16px;
189
+ font-size: 13px;
190
+ background: var(--accent);
191
+ color: var(--accent-contrast);
192
+ }
193
+
194
+ .btn-convert:hover:not(:disabled) {
195
+ background: var(--accent-strong);
196
+ }
197
+
198
+ .btn-convert:active:not(:disabled) {
199
+ transform: translateY(1px);
200
+ }
201
+
202
+ .btn-convert:disabled {
203
+ opacity: 0.45;
204
+ cursor: not-allowed;
205
+ }
206
+
207
+ .btn-clear {
208
+ flex: none;
209
+ padding: 8px 14px;
210
+ font-size: 13px;
211
+ background: transparent;
212
+ color: var(--text-muted);
213
+ border-color: var(--border);
214
+ }
215
+
216
+ .btn-clear:hover {
217
+ color: var(--text);
218
+ border-color: var(--text-muted);
219
+ }
220
+
221
+ /* Split layout */
222
+ .split {
223
+ flex: 1;
224
+ display: grid;
225
+ grid-template-columns: 380px 1px minmax(0, 1fr);
226
+ min-height: 0;
227
+ }
228
+
229
+ @media (max-width: 860px) {
230
+ .split {
231
+ grid-template-columns: minmax(0, 1fr);
232
+ grid-auto-rows: min-content minmax(320px, 1fr);
233
+ overflow-y: auto;
234
+ }
235
+ .split-divider {
236
+ display: none;
237
+ }
238
+ }
239
+
240
+ .split-divider {
241
+ background: var(--border);
242
+ }
243
+
244
+ .pane-input {
245
+ display: flex;
246
+ flex-direction: column;
247
+ gap: 16px;
248
+ padding: 20px;
249
+ overflow-y: auto;
250
+ min-height: 0;
251
+ }
252
+
253
+ .pane-output {
254
+ display: flex;
255
+ flex-direction: column;
256
+ min-height: 0;
257
+ background: var(--code-bg);
258
+ }
259
+
260
+ .upload-area {
261
+ flex: none;
262
+ border: 1.5px dashed var(--border);
263
+ border-radius: 0.5rem;
264
+ padding: 22px 16px;
265
+ text-align: center;
266
+ cursor: pointer;
267
+ transition:
268
+ border-color 0.15s ease,
269
+ background-color 0.15s ease;
270
+ background: var(--surface-alt);
271
+ }
272
+
273
+ .upload-area:hover,
274
+ .upload-area:focus-visible {
275
+ border-color: var(--accent);
276
+ background: color-mix(in srgb, var(--accent) 6%, var(--surface-alt));
277
+ }
278
+
279
+ .upload-area.dragover {
280
+ border-color: var(--accent);
281
+ border-style: solid;
282
+ background: color-mix(in srgb, var(--accent) 10%, var(--surface-alt));
283
+ }
284
+
285
+ .upload-area:focus-visible {
286
+ outline: 2px solid var(--focus-ring);
287
+ outline-offset: 2px;
288
+ }
289
+
290
+ .upload-row {
291
+ display: flex;
292
+ align-items: center;
293
+ justify-content: center;
294
+ gap: 10px;
295
+ }
296
+
297
+ .upload-glyph {
298
+ width: 18px;
299
+ height: 18px;
300
+ color: var(--accent);
301
+ flex: none;
302
+ }
303
+
304
+ .upload-text {
305
+ font-size: 13.5px;
306
+ font-weight: 500;
307
+ }
308
+
309
+ .upload-hint {
310
+ color: var(--text-muted);
311
+ font-size: 12px;
312
+ margin-top: 4px;
313
+ font-family: "Fira Code", ui-monospace, monospace;
314
+ }
315
+
316
+ input[type="file"] {
317
+ display: none;
318
+ }
319
+
320
+ .divider {
321
+ flex: none;
322
+ display: flex;
323
+ align-items: center;
324
+ gap: 10px;
325
+ color: var(--text-muted);
326
+ font-size: 11px;
327
+ font-weight: 500;
328
+ letter-spacing: 0.04em;
329
+ text-transform: uppercase;
330
+ }
331
+
332
+ .divider::before,
333
+ .divider::after {
334
+ content: "";
335
+ flex: 1;
336
+ height: 1px;
337
+ background: var(--border);
338
+ }
339
+
340
+ .paste-area {
341
+ flex: 1;
342
+ display: flex;
343
+ flex-direction: column;
344
+ min-height: 100px;
345
+ }
346
+
347
+ .paste-area textarea {
348
+ flex: 1;
349
+ width: 100%;
350
+ resize: none;
351
+ padding: 10px 12px;
352
+ border: 1px solid var(--border);
353
+ border-radius: 0.5rem;
354
+ font-size: 12.5px;
355
+ font-family: "Fira Code", ui-monospace, monospace;
356
+ background: var(--surface-alt);
357
+ color: var(--text);
358
+ line-height: 1.5;
359
+ }
360
+
361
+ .paste-area textarea:focus {
362
+ outline: none;
363
+ border-color: var(--accent);
364
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
365
+ }
366
+
367
+ .paste-area textarea::placeholder {
368
+ color: var(--text-muted);
369
+ }
370
+
371
+ .file-info {
372
+ flex: none;
373
+ display: none;
374
+ align-items: center;
375
+ justify-content: space-between;
376
+ gap: 12px;
377
+ padding: 9px 12px;
378
+ background: var(--surface-alt);
379
+ border: 1px solid var(--border);
380
+ border-radius: 0.5rem;
381
+ font-family: "Fira Code", ui-monospace, monospace;
382
+ font-size: 12.5px;
383
+ }
384
+
385
+ .file-info.visible {
386
+ display: flex;
387
+ }
388
+
389
+ .file-name {
390
+ color: var(--text);
391
+ font-weight: 500;
392
+ overflow: hidden;
393
+ text-overflow: ellipsis;
394
+ white-space: nowrap;
395
+ }
396
+
397
+ .file-size {
398
+ color: var(--text-muted);
399
+ flex-shrink: 0;
400
+ }
401
+
402
+ /* Output pane */
403
+ .output-header {
404
+ flex: none;
405
+ display: flex;
406
+ align-items: center;
407
+ justify-content: space-between;
408
+ padding: 0 16px;
409
+ height: 44px;
410
+ background: var(--code-header);
411
+ border-bottom: 1px solid var(--border);
412
+ font-family: "Fira Code", ui-monospace, monospace;
413
+ font-size: 12px;
414
+ color: var(--code-text-muted);
415
+ gap: 10px;
416
+ }
417
+
418
+ .output-header .output-filename {
419
+ flex: 1;
420
+ overflow: hidden;
421
+ text-overflow: ellipsis;
422
+ white-space: nowrap;
423
+ }
424
+
425
+ .output-header .header-actions {
426
+ display: flex;
427
+ gap: 8px;
428
+ flex: none;
429
+ }
430
+
431
+ .btn-copy,
432
+ .btn-download {
433
+ flex: none;
434
+ background: transparent;
435
+ border: 1px solid color-mix(in srgb, var(--code-text-muted) 45%, transparent);
436
+ color: var(--code-text-muted);
437
+ font-family: "Fira Code", ui-monospace, monospace;
438
+ font-size: 11.5px;
439
+ font-weight: 500;
440
+ padding: 4px 10px;
441
+ border-radius: 0.35rem;
442
+ }
443
+
444
+ .btn-copy:hover,
445
+ .btn-download:hover {
446
+ color: var(--code-text);
447
+ border-color: var(--code-text-muted);
448
+ }
449
+
450
+ .btn-download:disabled,
451
+ .btn-copy:disabled {
452
+ opacity: 0.4;
453
+ cursor: not-allowed;
454
+ }
455
+
456
+ .output-body {
457
+ position: relative;
458
+ flex: 1;
459
+ min-height: 0;
460
+ }
461
+
462
+ .shiki-container {
463
+ height: 100%;
464
+ overflow: auto;
465
+ }
466
+
467
+ .shiki-container .shiki {
468
+ margin: 0;
469
+ padding: 14px 0;
470
+ font-family: "Fira Code", ui-monospace, monospace;
471
+ font-size: 12.5px;
472
+ line-height: 1.5;
473
+ background-color: var(--code-bg) !important;
474
+ }
475
+
476
+ .shiki-container .shiki code {
477
+ display: block;
478
+ counter-reset: line;
479
+ }
480
+
481
+ .shiki-container .shiki .line {
482
+ display: inline-block;
483
+ min-width: 100%;
484
+ padding: 0 16px;
485
+ }
486
+
487
+ .shiki-container .shiki .line::before {
488
+ counter-increment: line;
489
+ content: counter(line);
490
+ display: inline-block;
491
+ width: 1.8rem;
492
+ margin-right: 14px;
493
+ text-align: right;
494
+ color: var(--text-muted);
495
+ opacity: 0.5;
496
+ user-select: none;
497
+ }
498
+
499
+ .output-empty {
500
+ position: absolute;
501
+ inset: 0;
502
+ display: flex;
503
+ flex-direction: column;
504
+ align-items: center;
505
+ justify-content: center;
506
+ gap: 10px;
507
+ color: var(--code-text-muted);
508
+ text-align: center;
509
+ padding: 24px;
510
+ }
511
+
512
+ .output-empty svg {
513
+ width: 26px;
514
+ height: 26px;
515
+ opacity: 0.5;
516
+ }
517
+
518
+ .output-empty p {
519
+ font-size: 13px;
520
+ max-width: 32ch;
521
+ line-height: 1.5;
522
+ }
523
+
524
+ .output-body.has-code .output-empty {
525
+ display: none;
526
+ }
527
+
528
+ @media (prefers-reduced-motion: reduce) {
529
+ * {
530
+ animation-duration: 0.001ms !important;
531
+ transition-duration: 0.001ms !important;
532
+ }
533
+ }
534
+ </style>
535
+ </head>
536
+ <body>
537
+ <div class="topbar">
538
+ <a class="brand" href="https://tscircuit.com" target="_blank" rel="noopener">
539
+ <span class="badge">tscircuit</span>
540
+ <span class="path">circuit-json &rarr; tsx</span>
541
+ </a>
542
+
543
+ <div class="topbar-spacer"></div>
544
+
545
+ <div class="status" id="status" role="status"><span class="dot"></span><span id="statusText"></span></div>
546
+
547
+ <button class="btn-convert" id="convertBtn" disabled>Convert</button>
548
+ <button class="btn-clear" id="clearBtn">Clear</button>
549
+ </div>
550
+
551
+ <div class="split">
552
+ <div class="pane-input">
553
+ <div class="upload-area" id="uploadArea" tabindex="0" role="button" aria-label="Upload circuit JSON file">
554
+ <div class="upload-row">
555
+ <svg class="upload-glyph" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
556
+ <path d="M4 15v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3" />
557
+ <path d="M7 9l5-5 5 5" />
558
+ <path d="M12 4v12" />
559
+ </svg>
560
+ <span class="upload-text">Drop circuit.json or click to browse</span>
561
+ </div>
562
+ <div class="upload-hint">.json</div>
563
+ </div>
564
+
565
+ <input type="file" id="fileInput" accept=".json,application/json" />
566
+
567
+ <div class="file-info" id="fileInfo">
568
+ <span class="file-name" id="fileName"></span>
569
+ <span class="file-size" id="fileSize"></span>
570
+ </div>
571
+
572
+ <div class="divider">or paste JSON</div>
573
+
574
+ <div class="paste-area">
575
+ <textarea
576
+ id="pasteArea"
577
+ placeholder='[{"type": "pcb_board", ...}, ...]'
578
+ spellcheck="false"
579
+ ></textarea>
580
+ </div>
581
+ </div>
582
+
583
+ <div class="split-divider"></div>
584
+
585
+ <div class="pane-output">
586
+ <div class="output-header">
587
+ <span class="output-filename" id="outputFileName">output.tsx</span>
588
+ <div class="header-actions">
589
+ <button class="btn-copy" id="copyBtn" type="button" disabled>Copy</button>
590
+ <button class="btn-download" id="downloadBtn" type="button" disabled>Download</button>
591
+ </div>
592
+ </div>
593
+ <div class="output-body" id="outputBody">
594
+ <div class="shiki-container" id="outputCode"></div>
595
+ <div class="output-empty" id="outputEmpty">
596
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
597
+ <path d="M9 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h4" />
598
+ <path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
599
+ <path d="M9 9l3 3-3 3" />
600
+ </svg>
601
+ <p>Your generated TSX will appear here after you convert a circuit.</p>
602
+ </div>
603
+ </div>
604
+ </div>
605
+ </div>
606
+
607
+ <script type="module" src="main.tsx"></script>
608
+ </body>
609
+ </html>
package/site/main.tsx ADDED
@@ -0,0 +1,268 @@
1
+ import type { CircuitJson } from "circuit-json"
2
+ import parserEstree from "prettier/plugins/estree"
3
+ import parserTs from "prettier/plugins/typescript"
4
+ import * as prettier from "prettier/standalone"
5
+ import { codeToHtml } from "shiki/bundle/web"
6
+ import { convertCircuitJsonToTscircuit } from "../lib"
7
+
8
+ async function formatTsx(code: string): Promise<string> {
9
+ try {
10
+ return await prettier.format(code, {
11
+ parser: "typescript",
12
+ plugins: [parserTs, parserEstree],
13
+ semi: false,
14
+ singleQuote: false,
15
+ })
16
+ } catch (error) {
17
+ console.warn("Failed to format generated code:", error)
18
+ return code
19
+ }
20
+ }
21
+
22
+ // Get DOM elements
23
+ const uploadArea = document.getElementById("uploadArea")!
24
+ const fileInput = document.getElementById("fileInput")! as HTMLInputElement
25
+ const fileInfo = document.getElementById("fileInfo")!
26
+ const fileName = document.getElementById("fileName")!
27
+ const fileSize = document.getElementById("fileSize")!
28
+ const convertBtn = document.getElementById("convertBtn")! as HTMLButtonElement
29
+ const clearBtn = document.getElementById("clearBtn")!
30
+ const status = document.getElementById("status")!
31
+ const statusText = document.getElementById("statusText")!
32
+ const outputBody = document.getElementById("outputBody")!
33
+ const outputFileName = document.getElementById("outputFileName")!
34
+ const outputCode = document.getElementById("outputCode")!
35
+ const copyBtn = document.getElementById("copyBtn")! as HTMLButtonElement
36
+ const downloadBtn = document.getElementById("downloadBtn")! as HTMLButtonElement
37
+ const pasteArea = document.getElementById("pasteArea")! as HTMLTextAreaElement
38
+
39
+ let currentFile: File | null = null
40
+ let circuitJson: CircuitJson | null = null
41
+ let lastOutput: { fileName: string; code: string } | null = null
42
+
43
+ // Handle click / keyboard activation on upload area
44
+ uploadArea.addEventListener("click", () => {
45
+ fileInput.click()
46
+ })
47
+
48
+ uploadArea.addEventListener("keydown", (e) => {
49
+ if (e.key === "Enter" || e.key === " ") {
50
+ e.preventDefault()
51
+ fileInput.click()
52
+ }
53
+ })
54
+
55
+ // Handle file selection
56
+ fileInput.addEventListener("change", (e) => {
57
+ const target = e.target as HTMLInputElement
58
+ if (target.files && target.files.length > 0 && target.files[0]) {
59
+ handleFile(target.files[0])
60
+ }
61
+ })
62
+
63
+ // Handle drag over
64
+ uploadArea.addEventListener("dragover", (e) => {
65
+ e.preventDefault()
66
+ uploadArea.classList.add("dragover")
67
+ })
68
+
69
+ // Handle drag leave
70
+ uploadArea.addEventListener("dragleave", () => {
71
+ uploadArea.classList.remove("dragover")
72
+ })
73
+
74
+ // Handle drop
75
+ uploadArea.addEventListener("drop", (e) => {
76
+ e.preventDefault()
77
+ uploadArea.classList.remove("dragover")
78
+
79
+ if (
80
+ e.dataTransfer &&
81
+ e.dataTransfer.files.length > 0 &&
82
+ e.dataTransfer.files[0]
83
+ ) {
84
+ handleFile(e.dataTransfer.files[0])
85
+ }
86
+ })
87
+
88
+ function baseNameFromFile(file: File) {
89
+ return file.name.replace(/\.json$/i, "")
90
+ }
91
+
92
+ // Handle file
93
+ async function handleFile(file: File) {
94
+ currentFile = file
95
+
96
+ // Show file info
97
+ fileName.textContent = file.name
98
+ fileSize.textContent = `${(file.size / 1024).toFixed(2)} KB`
99
+ fileInfo.classList.add("visible")
100
+
101
+ clearOutput()
102
+
103
+ // Read and parse the file
104
+ try {
105
+ showStatus("Reading file...", "processing")
106
+ const text = await file.text()
107
+ circuitJson = JSON.parse(text)
108
+
109
+ // Enable convert button
110
+ convertBtn.disabled = false
111
+ showStatus("File loaded. Ready to convert.", "success")
112
+ } catch (error) {
113
+ showStatus(
114
+ `Error reading file: ${error instanceof Error ? error.message : String(error)}`,
115
+ "error",
116
+ )
117
+ convertBtn.disabled = true
118
+ circuitJson = null
119
+ }
120
+ }
121
+
122
+ // Handle pasted JSON: parse automatically once the user pastes or leaves the field
123
+ function loadPastedJson() {
124
+ const text = pasteArea.value.trim()
125
+
126
+ currentFile = null
127
+ fileInput.value = ""
128
+ fileInfo.classList.remove("visible")
129
+ clearOutput()
130
+
131
+ if (!text) {
132
+ convertBtn.disabled = true
133
+ circuitJson = null
134
+ hideStatus()
135
+ return
136
+ }
137
+
138
+ try {
139
+ circuitJson = JSON.parse(text)
140
+ convertBtn.disabled = false
141
+ showStatus("Pasted JSON loaded. Ready to convert.", "success")
142
+ } catch (error) {
143
+ showStatus(
144
+ `Error parsing pasted JSON: ${error instanceof Error ? error.message : String(error)}`,
145
+ "error",
146
+ )
147
+ convertBtn.disabled = true
148
+ circuitJson = null
149
+ }
150
+ }
151
+
152
+ pasteArea.addEventListener("paste", () => {
153
+ // Wait for the pasted text to land in the textarea before reading it
154
+ setTimeout(loadPastedJson, 0)
155
+ })
156
+
157
+ pasteArea.addEventListener("blur", loadPastedJson)
158
+
159
+ // Convert
160
+ convertBtn.addEventListener("click", async () => {
161
+ if (!circuitJson) return
162
+
163
+ try {
164
+ showStatus("Converting to tscircuit...", "processing")
165
+ convertBtn.disabled = true
166
+
167
+ // Allow UI to update
168
+ await new Promise((resolve) => setTimeout(resolve, 100))
169
+
170
+ const baseName = currentFile ? baseNameFromFile(currentFile) : "Circuit"
171
+ const outFileName = `${baseName}.tsx`
172
+
173
+ const tsxContent = convertCircuitJsonToTscircuit(circuitJson, {
174
+ componentName: "Circuit",
175
+ })
176
+ const formatted = await formatTsx(tsxContent)
177
+
178
+ await setOutput(outFileName, formatted)
179
+
180
+ showStatus("Conversion complete.", "success")
181
+ convertBtn.disabled = false
182
+ } catch (error) {
183
+ showStatus(
184
+ `Error during conversion: ${error instanceof Error ? error.message : String(error)}`,
185
+ "error",
186
+ )
187
+ convertBtn.disabled = false
188
+ console.error(error)
189
+ }
190
+ })
191
+
192
+ // Copy output to clipboard
193
+ copyBtn.addEventListener("click", async () => {
194
+ if (!lastOutput) return
195
+ try {
196
+ await navigator.clipboard.writeText(lastOutput.code)
197
+ const original = copyBtn.textContent
198
+ copyBtn.textContent = "Copied"
199
+ setTimeout(() => {
200
+ copyBtn.textContent = original
201
+ }, 1500)
202
+ } catch (error) {
203
+ console.error("Failed to copy:", error)
204
+ }
205
+ })
206
+
207
+ // Download output as a .tsx file
208
+ downloadBtn.addEventListener("click", () => {
209
+ if (!lastOutput) return
210
+ downloadFile(lastOutput.fileName, lastOutput.code)
211
+ })
212
+
213
+ // Clear button
214
+ clearBtn.addEventListener("click", () => {
215
+ currentFile = null
216
+ circuitJson = null
217
+ fileInput.value = ""
218
+ pasteArea.value = ""
219
+ fileInfo.classList.remove("visible")
220
+ clearOutput()
221
+ convertBtn.disabled = true
222
+ hideStatus()
223
+ })
224
+
225
+ async function setOutput(fileNameStr: string, code: string) {
226
+ lastOutput = { fileName: fileNameStr, code }
227
+ outputFileName.textContent = fileNameStr
228
+ outputCode.innerHTML = await codeToHtml(code, {
229
+ lang: "tsx",
230
+ theme: "github-light",
231
+ })
232
+ outputBody.classList.add("has-code")
233
+ copyBtn.disabled = false
234
+ downloadBtn.disabled = false
235
+ }
236
+
237
+ function clearOutput() {
238
+ lastOutput = null
239
+ outputFileName.textContent = "output.tsx"
240
+ outputCode.innerHTML = ""
241
+ outputBody.classList.remove("has-code")
242
+ copyBtn.disabled = true
243
+ downloadBtn.disabled = true
244
+ }
245
+
246
+ // Helper function to download a file
247
+ function downloadFile(filename: string, content: string) {
248
+ const blob = new Blob([content], { type: "text/plain" })
249
+ const url = URL.createObjectURL(blob)
250
+ const a = document.createElement("a")
251
+ a.href = url
252
+ a.download = filename
253
+ document.body.appendChild(a)
254
+ a.click()
255
+ document.body.removeChild(a)
256
+ URL.revokeObjectURL(url)
257
+ }
258
+
259
+ // Helper function to show status
260
+ function showStatus(message: string, type: "success" | "error" | "processing") {
261
+ statusText.textContent = message
262
+ status.className = `status visible ${type}`
263
+ }
264
+
265
+ // Helper function to hide status
266
+ function hideStatus() {
267
+ status.classList.remove("visible")
268
+ }
package/vercel.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "buildCommand": "bun run build:site",
3
+ "outputDirectory": "site-build",
4
+ "installCommand": "bun install"
5
+ }