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 +5 -1
- package/package.json +5 -1
- package/site/index.html +609 -0
- package/site/main.tsx +268 -0
- package/vercel.json +5 -0
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.
|
|
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.
|
|
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
|
},
|
package/site/index.html
ADDED
|
@@ -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 → 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
|
+
}
|