@tscircuit/runframe 0.0.1
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/.github/workflows/bun-formatcheck.yml +26 -0
- package/.github/workflows/bun-pver-release.yml +25 -0
- package/.github/workflows/bun-typecheck.yml +26 -0
- package/LICENSE +21 -0
- package/README.md +40 -0
- package/biome.json +57 -0
- package/bun.lockb +0 -0
- package/cosmos.config.json +3 -0
- package/cosmos.decorator.tsx +3 -0
- package/dist/assets/index-CNL2S1zq.js +36 -0
- package/dist/assets/index-DDl5GwFA.js +21609 -0
- package/dist/assets/vision_bundle-CPRQl8Yc.js +15 -0
- package/dist/index.html +13 -0
- package/dist/standalone.js +46887 -0
- package/examples/resistor.fixture.tsx +12 -0
- package/examples/runframe-with-api-1.fixture.tsx +41 -0
- package/examples/runframe1-basic.fixture.tsx +19 -0
- package/index.html +13 -0
- package/lib/components/BomTable.tsx +69 -0
- package/lib/components/CircuitJsonPreview.tsx +348 -0
- package/lib/components/CircuitJsonTableViewer/CircuitJsonTableViewer.tsx +315 -0
- package/lib/components/CircuitJsonTableViewer/ClickableText.tsx +20 -0
- package/lib/components/CircuitJsonTableViewer/HeaderCell.tsx +26 -0
- package/lib/components/CircuitJsonTableViewer/Modal.tsx +38 -0
- package/lib/components/ErrorFallback.tsx +25 -0
- package/lib/components/ErrorTabContent.tsx +86 -0
- package/lib/components/PcbViewerWithContainerHeight.tsx +47 -0
- package/lib/components/PreviewEmptyState.tsx +14 -0
- package/lib/components/RunFrame.tsx +68 -0
- package/lib/components/RunFrameWithApi/index.tsx +41 -0
- package/lib/components/RunFrameWithApi/standalone.tsx +6 -0
- package/lib/components/RunFrameWithApi/store.ts +122 -0
- package/lib/components/RunFrameWithApi/types.ts +43 -0
- package/lib/components/ui/button.tsx +57 -0
- package/lib/components/ui/dropdown-menu.tsx +203 -0
- package/lib/components/ui/tabs.tsx +53 -0
- package/lib/dev/render-to-circuit-json.ts +7 -0
- package/lib/hooks/use-styles.ts +17 -0
- package/lib/preview.ts +1 -0
- package/lib/runner.ts +2 -0
- package/lib/utils/index.ts +6 -0
- package/lib/utils/pcbManualEditEventHandler.ts +156 -0
- package/package.json +52 -0
- package/postcss.config.js +8 -0
- package/renovate.json +15 -0
- package/scripts/build-css.ts +26 -0
- package/src/main.tsx +13 -0
- package/tailwind.config.js +3 -0
- package/tsconfig.json +43 -0
- package/vite.config.ts +43 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { useReducer, useState } from "react"
|
|
2
|
+
import { ClickableText } from "./ClickableText"
|
|
3
|
+
import { HeaderCell } from "./HeaderCell"
|
|
4
|
+
import Modal from "./Modal"
|
|
5
|
+
|
|
6
|
+
type Filters = {
|
|
7
|
+
component_type_filter?:
|
|
8
|
+
| "any"
|
|
9
|
+
| "source"
|
|
10
|
+
| "source/pcb"
|
|
11
|
+
| "source/schematic"
|
|
12
|
+
| string
|
|
13
|
+
id_search?: string
|
|
14
|
+
name_search?: string
|
|
15
|
+
selector_search?: string
|
|
16
|
+
focused_id?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type CommonProps = { name?: string; type: string }
|
|
20
|
+
|
|
21
|
+
type ModalState =
|
|
22
|
+
| { open: false; element?: never }
|
|
23
|
+
| { open: true; element: Element; title: string }
|
|
24
|
+
|
|
25
|
+
interface Element {
|
|
26
|
+
[key: string]: any
|
|
27
|
+
type: string
|
|
28
|
+
name?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ProcessedElement extends CommonProps {
|
|
32
|
+
primary_id: string
|
|
33
|
+
other_ids: { [key: string]: string }
|
|
34
|
+
selector_path?: string
|
|
35
|
+
_og_elm: Element
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface Column {
|
|
39
|
+
key: string
|
|
40
|
+
name: string
|
|
41
|
+
renderCell?: (row: ProcessedElement) => React.ReactNode
|
|
42
|
+
renderHeaderCell?: (col: Column) => React.ReactNode
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const CircuitJsonTableViewer: React.FC<{ elements: Element[] }> = ({
|
|
46
|
+
elements,
|
|
47
|
+
}) => {
|
|
48
|
+
const [modal, setModal] = useState<ModalState>({ open: false })
|
|
49
|
+
const [filters, setFilter] = useReducer(
|
|
50
|
+
(s: Filters, a: Filters) => ({
|
|
51
|
+
...s,
|
|
52
|
+
...a,
|
|
53
|
+
}),
|
|
54
|
+
{},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const element_types = [...new Set(elements.map((e) => e.type))]
|
|
58
|
+
|
|
59
|
+
// Process elements to separate primary and non-primary ids
|
|
60
|
+
const elements2: ProcessedElement[] = elements.map((e) => {
|
|
61
|
+
const primary_id = e[`${e.type}_id`]
|
|
62
|
+
|
|
63
|
+
const other_ids = Object.fromEntries(
|
|
64
|
+
Object.entries(e).filter(([k]) => {
|
|
65
|
+
if (k === `${e.type}_id`) return false
|
|
66
|
+
if (!k.endsWith("_id")) return false
|
|
67
|
+
return true
|
|
68
|
+
}),
|
|
69
|
+
) as { [key: string]: string }
|
|
70
|
+
|
|
71
|
+
const other_props: CommonProps = Object.fromEntries(
|
|
72
|
+
Object.entries(e).filter(([k]) => !k.endsWith("_id")),
|
|
73
|
+
) as CommonProps
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
primary_id,
|
|
77
|
+
other_ids,
|
|
78
|
+
...other_props,
|
|
79
|
+
_og_elm: e,
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const elements3 = elements2.map((e) => {
|
|
84
|
+
let selector_path = ""
|
|
85
|
+
|
|
86
|
+
const getSelectorPath = (e2: ProcessedElement): string => {
|
|
87
|
+
const parent_key = Object.keys(e2.other_ids).find((k) =>
|
|
88
|
+
k.startsWith("source_"),
|
|
89
|
+
)
|
|
90
|
+
if (!parent_key) return `.${e2.name}`
|
|
91
|
+
const parent_type = parent_key.slice(0, -3) // trim "_id"
|
|
92
|
+
|
|
93
|
+
const parent = elements2.find(
|
|
94
|
+
(p) =>
|
|
95
|
+
p.type === parent_type && p.primary_id === e2.other_ids[parent_key],
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if (!parent) return `??? > .${e2.name}`
|
|
99
|
+
|
|
100
|
+
if (!("name" in parent)) return `#${parent.primary_id} > .${e2.name}`
|
|
101
|
+
|
|
102
|
+
return `${getSelectorPath(parent)} > .${e2.name}`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if ("name" in e) {
|
|
106
|
+
selector_path = getSelectorPath(e)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
...e,
|
|
111
|
+
selector_path,
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const columns: Column[] = [
|
|
116
|
+
{
|
|
117
|
+
key: "primary_id",
|
|
118
|
+
name: "primary_id",
|
|
119
|
+
renderCell: (row: ProcessedElement) => (
|
|
120
|
+
<div className="flex items-center">
|
|
121
|
+
<ClickableText
|
|
122
|
+
text={row.primary_id}
|
|
123
|
+
onClick={() =>
|
|
124
|
+
setFilter({
|
|
125
|
+
focused_id: row.primary_id,
|
|
126
|
+
id_search: undefined,
|
|
127
|
+
selector_search: undefined,
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
/>
|
|
131
|
+
<span className="flex-grow" />
|
|
132
|
+
<ClickableText
|
|
133
|
+
text="(JSON)"
|
|
134
|
+
onClick={() =>
|
|
135
|
+
setModal({
|
|
136
|
+
open: true,
|
|
137
|
+
element: row._og_elm,
|
|
138
|
+
title: row.primary_id,
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
),
|
|
144
|
+
renderHeaderCell: (col: Column) => (
|
|
145
|
+
<HeaderCell
|
|
146
|
+
column={col}
|
|
147
|
+
onTextChange={(v) => setFilter({ id_search: v })}
|
|
148
|
+
field={
|
|
149
|
+
!filters.focused_id
|
|
150
|
+
? undefined
|
|
151
|
+
: () => (
|
|
152
|
+
<div>
|
|
153
|
+
Focus:{" "}
|
|
154
|
+
<span className="underline">{filters.focused_id}</span>
|
|
155
|
+
<ClickableText
|
|
156
|
+
text="(unfocus)"
|
|
157
|
+
onClick={() => setFilter({ focused_id: undefined })}
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
/>
|
|
163
|
+
),
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
key: "type",
|
|
167
|
+
name: "type",
|
|
168
|
+
renderHeaderCell: (col: Column) => (
|
|
169
|
+
<HeaderCell
|
|
170
|
+
column={col}
|
|
171
|
+
field={() => (
|
|
172
|
+
<select
|
|
173
|
+
onChange={(e) =>
|
|
174
|
+
setFilter({ component_type_filter: e.target.value })
|
|
175
|
+
}
|
|
176
|
+
className="border rounded p-1 w-full"
|
|
177
|
+
>
|
|
178
|
+
<option key="any" value="any">
|
|
179
|
+
any
|
|
180
|
+
</option>
|
|
181
|
+
<option key="source" value="source">
|
|
182
|
+
source
|
|
183
|
+
</option>
|
|
184
|
+
<option key="source/pcb" value="source/pcb">
|
|
185
|
+
source/pcb
|
|
186
|
+
</option>
|
|
187
|
+
<option key="source/schematic" value="source/schematic">
|
|
188
|
+
source/schematic
|
|
189
|
+
</option>
|
|
190
|
+
{element_types.map((type) => (
|
|
191
|
+
<option key={type} value={type}>
|
|
192
|
+
{type}
|
|
193
|
+
</option>
|
|
194
|
+
))}
|
|
195
|
+
</select>
|
|
196
|
+
)}
|
|
197
|
+
/>
|
|
198
|
+
),
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
key: "name",
|
|
202
|
+
name: "name",
|
|
203
|
+
renderHeaderCell: (col: Column) => (
|
|
204
|
+
<HeaderCell
|
|
205
|
+
column={col}
|
|
206
|
+
onTextChange={(t) => setFilter({ name_search: t })}
|
|
207
|
+
/>
|
|
208
|
+
),
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
key: "selector_path",
|
|
212
|
+
name: "selector_path",
|
|
213
|
+
renderHeaderCell: (col: Column) => (
|
|
214
|
+
<HeaderCell
|
|
215
|
+
column={col}
|
|
216
|
+
onTextChange={(t) => setFilter({ selector_search: t })}
|
|
217
|
+
/>
|
|
218
|
+
),
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
key: "other_ids",
|
|
222
|
+
name: "other_ids",
|
|
223
|
+
renderCell: (row: ProcessedElement) => (
|
|
224
|
+
<div className="space-x-2">
|
|
225
|
+
{Object.entries(row.other_ids).map(([other_id, v]) => (
|
|
226
|
+
<ClickableText
|
|
227
|
+
key={v}
|
|
228
|
+
text={v}
|
|
229
|
+
onClick={() => setFilter({ focused_id: v })}
|
|
230
|
+
/>
|
|
231
|
+
))}
|
|
232
|
+
</div>
|
|
233
|
+
),
|
|
234
|
+
},
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
const elements4 = elements3
|
|
238
|
+
.filter((e) => {
|
|
239
|
+
if (!filters.name_search) return true
|
|
240
|
+
return e.name?.toLowerCase()?.includes(filters.name_search.toLowerCase())
|
|
241
|
+
})
|
|
242
|
+
.filter((e) => {
|
|
243
|
+
if (!filters.component_type_filter) return true
|
|
244
|
+
if (filters.component_type_filter === "any") return true
|
|
245
|
+
if (filters.component_type_filter === "source") {
|
|
246
|
+
return e.type.startsWith("source_")
|
|
247
|
+
}
|
|
248
|
+
if (filters.component_type_filter === "source/pcb") {
|
|
249
|
+
return e.type.startsWith("source_") || e.type.startsWith("pcb_")
|
|
250
|
+
}
|
|
251
|
+
if (filters.component_type_filter === "source/schematic") {
|
|
252
|
+
return e.type.startsWith("source_") || e.type.startsWith("schematic_")
|
|
253
|
+
}
|
|
254
|
+
return e.type?.includes(filters.component_type_filter)
|
|
255
|
+
})
|
|
256
|
+
.filter((e) => {
|
|
257
|
+
if (!filters.selector_search) return true
|
|
258
|
+
const parts = filters.selector_search
|
|
259
|
+
.split(" ")
|
|
260
|
+
.filter((p) => p.length > 0)
|
|
261
|
+
return parts.every((part) => e.selector_path?.includes(part))
|
|
262
|
+
})
|
|
263
|
+
.filter((e) => {
|
|
264
|
+
if (!filters.id_search) return true
|
|
265
|
+
return e.primary_id?.includes(filters.id_search)
|
|
266
|
+
})
|
|
267
|
+
.filter((e) => {
|
|
268
|
+
if (!filters.focused_id) return true
|
|
269
|
+
if (e.primary_id === filters.focused_id) return true
|
|
270
|
+
if (Object.values(e.other_ids).includes(filters.focused_id)) return true
|
|
271
|
+
return false
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
<div className="font-mono text-xs">
|
|
276
|
+
<div className="overflow-x-auto">
|
|
277
|
+
<table className="table-auto w-full text-left">
|
|
278
|
+
<thead>
|
|
279
|
+
<tr>
|
|
280
|
+
{columns.map((col) => (
|
|
281
|
+
<th key={col.key} className="px-4 py-2 border-b">
|
|
282
|
+
{col.renderHeaderCell ? col.renderHeaderCell(col) : col.name}
|
|
283
|
+
</th>
|
|
284
|
+
))}
|
|
285
|
+
</tr>
|
|
286
|
+
</thead>
|
|
287
|
+
<tbody>
|
|
288
|
+
{elements4.map((row, rowIndex) => (
|
|
289
|
+
<tr key={rowIndex} className="hover:bg-gray-100">
|
|
290
|
+
{columns.map((col) => (
|
|
291
|
+
<td key={col.key} className="px-4 py-2 border-b">
|
|
292
|
+
{col.renderCell
|
|
293
|
+
? col.renderCell(row)
|
|
294
|
+
: (row as any)[col.key]}
|
|
295
|
+
</td>
|
|
296
|
+
))}
|
|
297
|
+
</tr>
|
|
298
|
+
))}
|
|
299
|
+
</tbody>
|
|
300
|
+
</table>
|
|
301
|
+
</div>
|
|
302
|
+
<Modal
|
|
303
|
+
open={modal.open}
|
|
304
|
+
onClose={() => setModal({ open: false })}
|
|
305
|
+
title={modal.open ? modal.title : ""}
|
|
306
|
+
>
|
|
307
|
+
<div className="bg-gray-800 p-3 text-white rounded">
|
|
308
|
+
<pre className="whitespace-pre-wrap">
|
|
309
|
+
{modal.open ? JSON.stringify(modal.element, null, 2) : ""}
|
|
310
|
+
</pre>
|
|
311
|
+
</div>
|
|
312
|
+
</Modal>
|
|
313
|
+
</div>
|
|
314
|
+
)
|
|
315
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type React from "react"
|
|
2
|
+
|
|
3
|
+
interface ClickableTextProps {
|
|
4
|
+
text: string
|
|
5
|
+
onClick: () => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const ClickableText: React.FC<ClickableTextProps> = ({
|
|
9
|
+
text,
|
|
10
|
+
onClick,
|
|
11
|
+
}) => {
|
|
12
|
+
return (
|
|
13
|
+
<span
|
|
14
|
+
className="cursor-pointer underline text-blue-300 mx-2"
|
|
15
|
+
onClick={onClick}
|
|
16
|
+
>
|
|
17
|
+
{text}
|
|
18
|
+
</span>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type React from "react"
|
|
2
|
+
|
|
3
|
+
interface HeaderCellProps {
|
|
4
|
+
column: { name: string }
|
|
5
|
+
field?: () => React.ReactNode
|
|
6
|
+
onTextChange?: (value: string) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const HeaderCell: React.FC<HeaderCellProps> = (p) => {
|
|
10
|
+
return (
|
|
11
|
+
<div className="leading-5">
|
|
12
|
+
<div className="py-2 font-bold">{p.column.name}</div>
|
|
13
|
+
<div>
|
|
14
|
+
{p.field?.() ?? (
|
|
15
|
+
<input
|
|
16
|
+
type="text"
|
|
17
|
+
className="border rounded p-1 w-full"
|
|
18
|
+
onChange={(e) => {
|
|
19
|
+
p.onTextChange?.(e.target.value)
|
|
20
|
+
}}
|
|
21
|
+
/>
|
|
22
|
+
)}
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type React from "react"
|
|
2
|
+
|
|
3
|
+
interface ModalProps {
|
|
4
|
+
open: boolean
|
|
5
|
+
children: React.ReactNode
|
|
6
|
+
title: string
|
|
7
|
+
onClose: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const Modal: React.FC<ModalProps> = ({ open, children, title, onClose }) => {
|
|
11
|
+
if (!open) return null
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50"
|
|
16
|
+
onClick={onClose}
|
|
17
|
+
>
|
|
18
|
+
<div
|
|
19
|
+
className="bg-white p-5 rounded-lg relative w-11/12 max-w-2xl"
|
|
20
|
+
onClick={(e) => {
|
|
21
|
+
e.preventDefault()
|
|
22
|
+
e.stopPropagation()
|
|
23
|
+
}}
|
|
24
|
+
>
|
|
25
|
+
<h2 className="mt-0 text-xl">{title}</h2>
|
|
26
|
+
<button
|
|
27
|
+
className="absolute top-4 right-4 text-2xl font-bold"
|
|
28
|
+
onClick={onClose}
|
|
29
|
+
>
|
|
30
|
+
×
|
|
31
|
+
</button>
|
|
32
|
+
{children}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default Modal
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
export const ErrorFallback = ({ error }: { error: Error }) => {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
data-testid="error-container"
|
|
7
|
+
className="error-container mt-4 bg-red-50 rounded-md border border-red-200"
|
|
8
|
+
>
|
|
9
|
+
<div className="p-4">
|
|
10
|
+
<h2 className="text-lg font-semibold text-red-800 mb-3">
|
|
11
|
+
Error Loading 3D Viewer
|
|
12
|
+
</h2>
|
|
13
|
+
<p className="text-xs font-mono whitespace-pre-wrap text-red-700">
|
|
14
|
+
{error.message}
|
|
15
|
+
</p>
|
|
16
|
+
<details
|
|
17
|
+
style={{ whiteSpace: "pre-wrap" }}
|
|
18
|
+
className="text-xs font-mono text-red-600 mt-2"
|
|
19
|
+
>
|
|
20
|
+
{error.stack}
|
|
21
|
+
</details>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { GitHubLogoIcon } from "@radix-ui/react-icons"
|
|
2
|
+
import { ClipboardIcon } from "lucide-react"
|
|
3
|
+
import { Button } from "lib/components/ui/button"
|
|
4
|
+
|
|
5
|
+
export const ErrorTabContent = ({
|
|
6
|
+
code,
|
|
7
|
+
isStreaming,
|
|
8
|
+
errorMessage,
|
|
9
|
+
}: {
|
|
10
|
+
code?: string
|
|
11
|
+
isStreaming?: boolean
|
|
12
|
+
errorMessage?: string | null
|
|
13
|
+
}) => {
|
|
14
|
+
if (!errorMessage) {
|
|
15
|
+
return (
|
|
16
|
+
<div className="mt-4 bg-green-50 rounded-md border border-green-200">
|
|
17
|
+
<div className="p-4">
|
|
18
|
+
<h3 className="text-lg font-semibold text-green-800 mb-3">
|
|
19
|
+
No Errors 👌
|
|
20
|
+
</h3>
|
|
21
|
+
<p className="text-sm text-green-700">
|
|
22
|
+
Your code is running without any errors.
|
|
23
|
+
</p>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<div className="mt-4 bg-red-50 rounded-md border border-red-200 max-h-[500px] overflow-y-auto">
|
|
32
|
+
<div className="p-4">
|
|
33
|
+
<h3 className="text-lg font-semibold text-red-800 mb-3">Error</h3>
|
|
34
|
+
<p className="text-xs font-mono whitespace-pre-wrap text-red-600 mt-2">
|
|
35
|
+
{errorMessage}
|
|
36
|
+
</p>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<div className="flex gap-2 mt-4 justify-end">
|
|
40
|
+
<Button
|
|
41
|
+
variant="outline"
|
|
42
|
+
onClick={() => {
|
|
43
|
+
if (!errorMessage) return
|
|
44
|
+
navigator.clipboard.writeText(errorMessage)
|
|
45
|
+
alert("Error copied to clipboard!")
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<ClipboardIcon className="w-4 h-4 mr-2" />
|
|
49
|
+
Copy Error
|
|
50
|
+
</Button>
|
|
51
|
+
<Button
|
|
52
|
+
variant="outline"
|
|
53
|
+
onClick={() => {
|
|
54
|
+
window.alert(
|
|
55
|
+
"Not supported yet! Please report/upvote an issue on github.com/tscircuit/tscircuit",
|
|
56
|
+
)
|
|
57
|
+
// const title = `Error: ${errorMessage
|
|
58
|
+
// .replace("Render Error:", "")
|
|
59
|
+
// .replace(/\"_errors\":\[\]/g, "")
|
|
60
|
+
// .replace(/\{,/g, "{")
|
|
61
|
+
// .replace(/"_errors":\[/g, "")
|
|
62
|
+
// .replace(/[^a-zA-Z0-9 ]/g, " ")
|
|
63
|
+
// .replace(/\s+/g, " ")
|
|
64
|
+
// .replace(/ \d+ /g, " ")
|
|
65
|
+
// .slice(0, 100)}`
|
|
66
|
+
// const url = encodeTextToUrlHash(code ?? "").replace(
|
|
67
|
+
// "http://localhost:5173",
|
|
68
|
+
// "https://snippets.tscircuit.com",
|
|
69
|
+
// )
|
|
70
|
+
// let body = `[Snippet code to reproduce](${url})\n\n### Error\n\`\`\`\n${errorMessage.slice(0, 600)}\n\`\`\``
|
|
71
|
+
// if (body.length > 4000) {
|
|
72
|
+
// body = `\`\`\`tsx\n// Please paste the code here\`\`\`\n\n### Error\n\`\`\`\n${errorMessage.slice(0, 2000)}\n\`\`\``
|
|
73
|
+
// }
|
|
74
|
+
// window.open(
|
|
75
|
+
// `https://github.com/tscircuit/snippets/issues/new?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`,
|
|
76
|
+
// "_blank",
|
|
77
|
+
// )
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
<GitHubLogoIcon className="w-4 h-4 mr-2" />
|
|
81
|
+
Report Issue
|
|
82
|
+
</Button>
|
|
83
|
+
</div>
|
|
84
|
+
</>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useRef, useState, useLayoutEffect, type ComponentProps } from "react"
|
|
2
|
+
import { PCBViewer } from "@tscircuit/pcb-viewer"
|
|
3
|
+
|
|
4
|
+
export const PcbViewerWithContainerHeight = ({
|
|
5
|
+
containerClassName,
|
|
6
|
+
...props
|
|
7
|
+
}: {
|
|
8
|
+
containerClassName?: string
|
|
9
|
+
} & ComponentProps<typeof PCBViewer>) => {
|
|
10
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
11
|
+
const [computedHeight, setComputedHeight] = useState(620)
|
|
12
|
+
|
|
13
|
+
useLayoutEffect(() => {
|
|
14
|
+
const updateHeight = () => {
|
|
15
|
+
if (containerRef.current) {
|
|
16
|
+
const containerHeight = containerRef.current.clientHeight
|
|
17
|
+
const screenHeight = window.innerHeight
|
|
18
|
+
setComputedHeight(
|
|
19
|
+
Math.min(Math.max(containerHeight, 620), screenHeight),
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Immediate synchronous calculation
|
|
25
|
+
updateHeight()
|
|
26
|
+
|
|
27
|
+
// Resize listener for dynamic changes
|
|
28
|
+
const resizeObserver = new ResizeObserver(updateHeight)
|
|
29
|
+
if (containerRef.current) {
|
|
30
|
+
resizeObserver.observe(containerRef.current)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Fallback for window resize
|
|
34
|
+
window.addEventListener("resize", updateHeight)
|
|
35
|
+
|
|
36
|
+
return () => {
|
|
37
|
+
resizeObserver.disconnect()
|
|
38
|
+
window.removeEventListener("resize", updateHeight)
|
|
39
|
+
}
|
|
40
|
+
}, [])
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div ref={containerRef} className={containerClassName || "w-full h-full"}>
|
|
44
|
+
<PCBViewer {...props} height={computedHeight} />
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Button } from "lib/components/ui/button"
|
|
2
|
+
import { PlayIcon } from "lucide-react"
|
|
3
|
+
|
|
4
|
+
const PreviewEmptyState = ({ onRunClicked }: { onRunClicked: () => void }) => (
|
|
5
|
+
<div className="flex items-center gap-3 bg-gray-100 text-center justify-center py-10">
|
|
6
|
+
No circuit json loaded
|
|
7
|
+
<Button className="bg-blue-600 hover:bg-blue-500" onClick={onRunClicked}>
|
|
8
|
+
Run Code
|
|
9
|
+
<PlayIcon className="w-3 h-3 ml-2" />
|
|
10
|
+
</Button>
|
|
11
|
+
</div>
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
export default PreviewEmptyState
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createCircuitWebWorker } from "@tscircuit/eval-webworker"
|
|
2
|
+
import { CircuitJsonPreview } from "./CircuitJsonPreview"
|
|
3
|
+
import { useEffect, useState } from "react"
|
|
4
|
+
|
|
5
|
+
// @ts-ignore
|
|
6
|
+
import evalWebWorkerBlobUrl from "@tscircuit/eval-webworker/blob-url"
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
/**
|
|
10
|
+
* Map of filenames to file contents that will be available in the worker
|
|
11
|
+
*/
|
|
12
|
+
fsMap: { [filename: string]: string }
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The entry point file that will be executed first
|
|
16
|
+
*/
|
|
17
|
+
entrypoint: string
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Called when the circuit JSON changes
|
|
21
|
+
*/
|
|
22
|
+
onCircuitJsonChange?: (circuitJson: any) => void
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Called when rendering is finished
|
|
26
|
+
*/
|
|
27
|
+
onRenderingFinished?: (params: { circuitJson: any }) => void
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Called for each render event
|
|
31
|
+
*/
|
|
32
|
+
onRenderEvent?: (event: any) => void
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Called when an error occurs
|
|
36
|
+
*/
|
|
37
|
+
onError?: (error: Error) => void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const RunFrame = (props: Props) => {
|
|
41
|
+
const [circuitJson, setCircuitJson] = useState<any>(null)
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
async function runWorker() {
|
|
45
|
+
const worker = await createCircuitWebWorker({
|
|
46
|
+
webWorkerUrl: evalWebWorkerBlobUrl,
|
|
47
|
+
verbose: true,
|
|
48
|
+
})
|
|
49
|
+
const $finished = worker.executeWithFsMap({
|
|
50
|
+
entrypoint: props.entrypoint,
|
|
51
|
+
fsMap: props.fsMap,
|
|
52
|
+
})
|
|
53
|
+
console.log("waiting for execution to finish...")
|
|
54
|
+
await $finished
|
|
55
|
+
console.log("waiting for initial circuit json...")
|
|
56
|
+
setCircuitJson(await worker.getCircuitJson())
|
|
57
|
+
console.log("got initial circuit json")
|
|
58
|
+
await $finished.catch((e) => {
|
|
59
|
+
console.error(e)
|
|
60
|
+
})
|
|
61
|
+
setCircuitJson(await worker.getCircuitJson())
|
|
62
|
+
}
|
|
63
|
+
runWorker()
|
|
64
|
+
}, [props.fsMap])
|
|
65
|
+
|
|
66
|
+
console.log({ circuitJson })
|
|
67
|
+
return <CircuitJsonPreview circuitJson={circuitJson} />
|
|
68
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useEffect } from "react"
|
|
2
|
+
import { RunFrame } from "../RunFrame"
|
|
3
|
+
import { useRunFrameStore, selectCurrentFileMap } from "./store"
|
|
4
|
+
import type { RunFrameWithApiProps } from "./types"
|
|
5
|
+
|
|
6
|
+
const guessEntrypoint = (files: string[]) =>
|
|
7
|
+
files.find((file) => file.endsWith(".tsx"))
|
|
8
|
+
|
|
9
|
+
export const RunFrameWithApi = ({ apiBaseUrl }: RunFrameWithApiProps) => {
|
|
10
|
+
const { startPolling, stopPolling } = useRunFrameStore()
|
|
11
|
+
const fsMap = useRunFrameStore(selectCurrentFileMap)
|
|
12
|
+
|
|
13
|
+
// Initialize API base URL
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (apiBaseUrl) {
|
|
16
|
+
window.API_BASE_URL = apiBaseUrl
|
|
17
|
+
}
|
|
18
|
+
}, [apiBaseUrl])
|
|
19
|
+
|
|
20
|
+
// Start/stop polling
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
startPolling()
|
|
23
|
+
return () => stopPolling()
|
|
24
|
+
}, [startPolling, stopPolling])
|
|
25
|
+
|
|
26
|
+
const entrypoint = guessEntrypoint(Object.keys(fsMap))
|
|
27
|
+
|
|
28
|
+
if (!entrypoint) {
|
|
29
|
+
return <div>No entrypoint found for Run Frame!</div>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<RunFrame
|
|
34
|
+
fsMap={fsMap}
|
|
35
|
+
entrypoint={entrypoint}
|
|
36
|
+
onError={(error) => {
|
|
37
|
+
console.error("RunFrame error:", error)
|
|
38
|
+
}}
|
|
39
|
+
/>
|
|
40
|
+
)
|
|
41
|
+
}
|