@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.
Files changed (50) hide show
  1. package/.github/workflows/bun-formatcheck.yml +26 -0
  2. package/.github/workflows/bun-pver-release.yml +25 -0
  3. package/.github/workflows/bun-typecheck.yml +26 -0
  4. package/LICENSE +21 -0
  5. package/README.md +40 -0
  6. package/biome.json +57 -0
  7. package/bun.lockb +0 -0
  8. package/cosmos.config.json +3 -0
  9. package/cosmos.decorator.tsx +3 -0
  10. package/dist/assets/index-CNL2S1zq.js +36 -0
  11. package/dist/assets/index-DDl5GwFA.js +21609 -0
  12. package/dist/assets/vision_bundle-CPRQl8Yc.js +15 -0
  13. package/dist/index.html +13 -0
  14. package/dist/standalone.js +46887 -0
  15. package/examples/resistor.fixture.tsx +12 -0
  16. package/examples/runframe-with-api-1.fixture.tsx +41 -0
  17. package/examples/runframe1-basic.fixture.tsx +19 -0
  18. package/index.html +13 -0
  19. package/lib/components/BomTable.tsx +69 -0
  20. package/lib/components/CircuitJsonPreview.tsx +348 -0
  21. package/lib/components/CircuitJsonTableViewer/CircuitJsonTableViewer.tsx +315 -0
  22. package/lib/components/CircuitJsonTableViewer/ClickableText.tsx +20 -0
  23. package/lib/components/CircuitJsonTableViewer/HeaderCell.tsx +26 -0
  24. package/lib/components/CircuitJsonTableViewer/Modal.tsx +38 -0
  25. package/lib/components/ErrorFallback.tsx +25 -0
  26. package/lib/components/ErrorTabContent.tsx +86 -0
  27. package/lib/components/PcbViewerWithContainerHeight.tsx +47 -0
  28. package/lib/components/PreviewEmptyState.tsx +14 -0
  29. package/lib/components/RunFrame.tsx +68 -0
  30. package/lib/components/RunFrameWithApi/index.tsx +41 -0
  31. package/lib/components/RunFrameWithApi/standalone.tsx +6 -0
  32. package/lib/components/RunFrameWithApi/store.ts +122 -0
  33. package/lib/components/RunFrameWithApi/types.ts +43 -0
  34. package/lib/components/ui/button.tsx +57 -0
  35. package/lib/components/ui/dropdown-menu.tsx +203 -0
  36. package/lib/components/ui/tabs.tsx +53 -0
  37. package/lib/dev/render-to-circuit-json.ts +7 -0
  38. package/lib/hooks/use-styles.ts +17 -0
  39. package/lib/preview.ts +1 -0
  40. package/lib/runner.ts +2 -0
  41. package/lib/utils/index.ts +6 -0
  42. package/lib/utils/pcbManualEditEventHandler.ts +156 -0
  43. package/package.json +52 -0
  44. package/postcss.config.js +8 -0
  45. package/renovate.json +15 -0
  46. package/scripts/build-css.ts +26 -0
  47. package/src/main.tsx +13 -0
  48. package/tailwind.config.js +3 -0
  49. package/tsconfig.json +43 -0
  50. package/vite.config.ts +43 -0
@@ -0,0 +1,122 @@
1
+ import { create } from "zustand"
2
+ import { devtools } from "zustand/middleware"
3
+ import type {
4
+ FilePath,
5
+ FileContent,
6
+ File,
7
+ FileEvent,
8
+ RunFrameState,
9
+ } from "./types"
10
+
11
+ const API_BASE = window.API_BASE_URL ?? "/api"
12
+
13
+ async function upsertFileApi(
14
+ path: FilePath,
15
+ content: FileContent,
16
+ ): Promise<File> {
17
+ const response = await fetch(`${API_BASE}/files/upsert`, {
18
+ method: "POST",
19
+ headers: { "Content-Type": "application/json" },
20
+ body: JSON.stringify({ file_path: path, text_content: content }),
21
+ })
22
+ const data = await response.json()
23
+ return data.file
24
+ }
25
+
26
+ async function getFileApi(path: FilePath): Promise<File> {
27
+ const response = await fetch(
28
+ `${API_BASE}/files/get?file_path=${encodeURIComponent(path)}`,
29
+ )
30
+ const data = await response.json()
31
+ return data.file
32
+ }
33
+
34
+ async function getEvents(since: string | null): Promise<FileEvent[]> {
35
+ const url = since
36
+ ? `${API_BASE}/events/list?since=${encodeURIComponent(since)}`
37
+ : `${API_BASE}/events/list`
38
+ const response = await fetch(url)
39
+ const data = await response.json()
40
+ return data.event_list
41
+ }
42
+
43
+ // Create store
44
+ export const useRunFrameStore = create<RunFrameState>()(
45
+ devtools(
46
+ (set, get) => ({
47
+ fsMap: new Map(),
48
+ lastEventTime: null,
49
+ isPolling: false,
50
+ error: null,
51
+
52
+ upsertFile: async (path, content) => {
53
+ try {
54
+ const file = await upsertFileApi(path, content)
55
+ set((state) => ({
56
+ fsMap: new Map(state.fsMap).set(file.file_path, file.text_content),
57
+ }))
58
+ } catch (error) {
59
+ set({ error: error as Error })
60
+ }
61
+ },
62
+
63
+ getFile: async (path) => {
64
+ try {
65
+ const file = await getFileApi(path)
66
+ set((state) => ({
67
+ fsMap: new Map(state.fsMap).set(file.file_path, file.text_content),
68
+ }))
69
+ } catch (error) {
70
+ set({ error: error as Error })
71
+ }
72
+ },
73
+
74
+ startPolling: () => {
75
+ const poll = async () => {
76
+ const state = get()
77
+ if (!state.isPolling) return
78
+
79
+ try {
80
+ const events = await getEvents(state.lastEventTime)
81
+
82
+ if (events.length > 0) {
83
+ // Update lastEventTime to most recent event
84
+ const newLastEventTime = events[events.length - 1].created_at
85
+
86
+ // Process all file updates
87
+ const updates = new Map(state.fsMap)
88
+ for (const event of events) {
89
+ if (event.event_type === "FILE_UPDATED") {
90
+ const file = await getFileApi(event.file_path)
91
+ updates.set(file.file_path, file.text_content)
92
+ }
93
+ }
94
+
95
+ set({
96
+ fsMap: updates,
97
+ lastEventTime: newLastEventTime,
98
+ })
99
+ }
100
+ } catch (error) {
101
+ set({ error: error as Error })
102
+ }
103
+
104
+ // Schedule next poll
105
+ setTimeout(poll, 1000)
106
+ }
107
+
108
+ set({ isPolling: true })
109
+ poll()
110
+ },
111
+
112
+ stopPolling: () => {
113
+ set({ isPolling: false })
114
+ },
115
+ }),
116
+ { name: "run-frame-store" },
117
+ ),
118
+ )
119
+
120
+ // Export selector for current file map
121
+ export const selectCurrentFileMap = (state: RunFrameState) =>
122
+ Object.fromEntries(state.fsMap.entries())
@@ -0,0 +1,43 @@
1
+ // Types
2
+ export type FilePath = string
3
+ export type FileContent = string
4
+ export type FileId = string
5
+
6
+ export interface File {
7
+ file_id: FileId
8
+ file_path: FilePath
9
+ text_content: FileContent
10
+ }
11
+
12
+ export interface FileEvent {
13
+ event_id: string
14
+ event_type: "FILE_UPDATED"
15
+ file_path: FilePath
16
+ created_at: string
17
+ }
18
+
19
+ export interface RunFrameState {
20
+ fsMap: Map<FilePath, FileContent>
21
+ lastEventTime: string | null
22
+ isPolling: boolean
23
+ error: Error | null
24
+
25
+ // Actions
26
+ upsertFile: (path: FilePath, content: FileContent) => Promise<void>
27
+ getFile: (path: FilePath) => Promise<void>
28
+ startPolling: () => void
29
+ stopPolling: () => void
30
+ }
31
+
32
+ export interface RunFrameWithApiProps {
33
+ /**
34
+ * Base URL for the API endpoints
35
+ */
36
+ apiBaseUrl?: string
37
+ }
38
+
39
+ declare global {
40
+ interface Window {
41
+ API_BASE_URL: string
42
+ }
43
+ }
@@ -0,0 +1,57 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "lib/utils"
5
+
6
+ const buttonVariants = cva(
7
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 disabled:pointer-events-none disabled:opacity-50 dark:focus-visible:ring-slate-300",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default:
12
+ "bg-slate-900 text-slate-50 shadow hover:bg-slate-900/90 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50/90",
13
+ destructive:
14
+ "bg-red-500 text-slate-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-slate-50 dark:hover:bg-red-900/90",
15
+ outline:
16
+ "border border-slate-200 bg-white shadow-sm hover:bg-slate-100 hover:text-slate-900 dark:border-slate-800 dark:bg-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-50",
17
+ secondary:
18
+ "bg-slate-100 text-slate-900 shadow-sm hover:bg-slate-100/80 dark:bg-slate-800 dark:text-slate-50 dark:hover:bg-slate-800/80",
19
+ ghost:
20
+ "hover:bg-slate-100 hover:text-slate-900 dark:hover:bg-slate-800 dark:hover:text-slate-50",
21
+ link: "text-slate-900 underline-offset-4 hover:underline dark:text-slate-50",
22
+ },
23
+ size: {
24
+ default: "h-9 px-4 py-2",
25
+ sm: "h-8 rounded-md px-3 text-xs",
26
+ lg: "h-10 rounded-md px-8",
27
+ icon: "h-9 w-9",
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: "default",
32
+ size: "default",
33
+ },
34
+ },
35
+ )
36
+
37
+ export interface ButtonProps
38
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
39
+ VariantProps<typeof buttonVariants> {
40
+ asChild?: boolean
41
+ }
42
+
43
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
44
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
45
+ const Comp = "button"
46
+ return (
47
+ <Comp
48
+ className={cn(buttonVariants({ variant, size, className }))}
49
+ ref={ref}
50
+ {...props}
51
+ />
52
+ )
53
+ },
54
+ )
55
+ Button.displayName = "Button"
56
+
57
+ export { Button, buttonVariants }
@@ -0,0 +1,203 @@
1
+ import * as React from "react"
2
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
3
+ import {
4
+ CheckIcon,
5
+ ChevronRightIcon,
6
+ DotFilledIcon,
7
+ } from "@radix-ui/react-icons"
8
+
9
+ import { cn } from "lib/utils"
10
+
11
+ const DropdownMenu = DropdownMenuPrimitive.Root
12
+
13
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
14
+
15
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group
16
+
17
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal
18
+
19
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub
20
+
21
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
22
+
23
+ const DropdownMenuSubTrigger = React.forwardRef<
24
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
25
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
26
+ inset?: boolean
27
+ }
28
+ >(({ className, inset, children, ...props }, ref) => (
29
+ <DropdownMenuPrimitive.SubTrigger
30
+ ref={ref}
31
+ className={cn(
32
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-slate-100 data-[state=open]:bg-slate-100 dark:focus:bg-slate-800 dark:data-[state=open]:bg-slate-800",
33
+ inset && "pl-8",
34
+ className,
35
+ )}
36
+ {...props}
37
+ >
38
+ {children}
39
+ <ChevronRightIcon className="ml-auto h-4 w-4" />
40
+ </DropdownMenuPrimitive.SubTrigger>
41
+ ))
42
+ DropdownMenuSubTrigger.displayName =
43
+ DropdownMenuPrimitive.SubTrigger.displayName
44
+
45
+ const DropdownMenuSubContent = React.forwardRef<
46
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
47
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
48
+ >(({ className, ...props }, ref) => (
49
+ <DropdownMenuPrimitive.SubContent
50
+ ref={ref}
51
+ className={cn(
52
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-200 bg-white p-1 text-slate-950 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50",
53
+ className,
54
+ )}
55
+ {...props}
56
+ />
57
+ ))
58
+ DropdownMenuSubContent.displayName =
59
+ DropdownMenuPrimitive.SubContent.displayName
60
+
61
+ const DropdownMenuContent = React.forwardRef<
62
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
63
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
64
+ >(({ className, sideOffset = 4, ...props }, ref) => (
65
+ <DropdownMenuPrimitive.Portal>
66
+ <DropdownMenuPrimitive.Content
67
+ ref={ref}
68
+ sideOffset={sideOffset}
69
+ className={cn(
70
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-200 bg-white p-1 text-slate-950 shadow-md dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50",
71
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
72
+ className,
73
+ )}
74
+ {...props}
75
+ />
76
+ </DropdownMenuPrimitive.Portal>
77
+ ))
78
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
79
+
80
+ const DropdownMenuItem = React.forwardRef<
81
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
82
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
83
+ inset?: boolean
84
+ }
85
+ >(({ className, inset, ...props }, ref) => (
86
+ <DropdownMenuPrimitive.Item
87
+ ref={ref}
88
+ className={cn(
89
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-800 dark:focus:text-slate-50",
90
+ inset && "pl-8",
91
+ className,
92
+ )}
93
+ {...props}
94
+ />
95
+ ))
96
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
97
+
98
+ const DropdownMenuCheckboxItem = React.forwardRef<
99
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
100
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
101
+ >(({ className, children, checked, ...props }, ref) => (
102
+ <DropdownMenuPrimitive.CheckboxItem
103
+ ref={ref}
104
+ className={cn(
105
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-800 dark:focus:text-slate-50",
106
+ className,
107
+ )}
108
+ checked={checked}
109
+ {...props}
110
+ >
111
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
112
+ <DropdownMenuPrimitive.ItemIndicator>
113
+ <CheckIcon className="h-4 w-4" />
114
+ </DropdownMenuPrimitive.ItemIndicator>
115
+ </span>
116
+ {children}
117
+ </DropdownMenuPrimitive.CheckboxItem>
118
+ ))
119
+ DropdownMenuCheckboxItem.displayName =
120
+ DropdownMenuPrimitive.CheckboxItem.displayName
121
+
122
+ const DropdownMenuRadioItem = React.forwardRef<
123
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
124
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
125
+ >(({ className, children, ...props }, ref) => (
126
+ <DropdownMenuPrimitive.RadioItem
127
+ ref={ref}
128
+ className={cn(
129
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-800 dark:focus:text-slate-50",
130
+ className,
131
+ )}
132
+ {...props}
133
+ >
134
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
135
+ <DropdownMenuPrimitive.ItemIndicator>
136
+ <DotFilledIcon className="h-4 w-4 fill-current" />
137
+ </DropdownMenuPrimitive.ItemIndicator>
138
+ </span>
139
+ {children}
140
+ </DropdownMenuPrimitive.RadioItem>
141
+ ))
142
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
143
+
144
+ const DropdownMenuLabel = React.forwardRef<
145
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
146
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
147
+ inset?: boolean
148
+ }
149
+ >(({ className, inset, ...props }, ref) => (
150
+ <DropdownMenuPrimitive.Label
151
+ ref={ref}
152
+ className={cn(
153
+ "px-2 py-1.5 text-sm font-semibold",
154
+ inset && "pl-8",
155
+ className,
156
+ )}
157
+ {...props}
158
+ />
159
+ ))
160
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
161
+
162
+ const DropdownMenuSeparator = React.forwardRef<
163
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
164
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
165
+ >(({ className, ...props }, ref) => (
166
+ <DropdownMenuPrimitive.Separator
167
+ ref={ref}
168
+ className={cn("-mx-1 my-1 h-px bg-slate-100 dark:bg-slate-800", className)}
169
+ {...props}
170
+ />
171
+ ))
172
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
173
+
174
+ const DropdownMenuShortcut = ({
175
+ className,
176
+ ...props
177
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
178
+ return (
179
+ <span
180
+ className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
181
+ {...props}
182
+ />
183
+ )
184
+ }
185
+ DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
186
+
187
+ export {
188
+ DropdownMenu,
189
+ DropdownMenuTrigger,
190
+ DropdownMenuContent,
191
+ DropdownMenuItem,
192
+ DropdownMenuCheckboxItem,
193
+ DropdownMenuRadioItem,
194
+ DropdownMenuLabel,
195
+ DropdownMenuSeparator,
196
+ DropdownMenuShortcut,
197
+ DropdownMenuGroup,
198
+ DropdownMenuPortal,
199
+ DropdownMenuSub,
200
+ DropdownMenuSubContent,
201
+ DropdownMenuSubTrigger,
202
+ DropdownMenuRadioGroup,
203
+ }
@@ -0,0 +1,53 @@
1
+ import * as React from "react"
2
+ import * as TabsPrimitive from "@radix-ui/react-tabs"
3
+
4
+ import { cn } from "lib/utils"
5
+
6
+ const Tabs = TabsPrimitive.Root
7
+
8
+ const TabsList = React.forwardRef<
9
+ React.ElementRef<typeof TabsPrimitive.List>,
10
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
11
+ >(({ className, ...props }, ref) => (
12
+ <TabsPrimitive.List
13
+ ref={ref}
14
+ className={cn(
15
+ "inline-flex h-9 items-center justify-center rounded-lg bg-slate-100 p-1 text-slate-500 dark:bg-slate-800 dark:text-slate-400",
16
+ className,
17
+ )}
18
+ {...props}
19
+ />
20
+ ))
21
+ TabsList.displayName = TabsPrimitive.List.displayName
22
+
23
+ const TabsTrigger = React.forwardRef<
24
+ React.ElementRef<typeof TabsPrimitive.Trigger>,
25
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
26
+ >(({ className, ...props }, ref) => (
27
+ <TabsPrimitive.Trigger
28
+ ref={ref}
29
+ className={cn(
30
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-white transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-slate-950 data-[state=active]:shadow dark:ring-offset-slate-950 dark:focus-visible:ring-slate-300 dark:data-[state=active]:bg-slate-950 dark:data-[state=active]:text-slate-50",
31
+ className,
32
+ )}
33
+ {...props}
34
+ />
35
+ ))
36
+ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
37
+
38
+ const TabsContent = React.forwardRef<
39
+ React.ElementRef<typeof TabsPrimitive.Content>,
40
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
41
+ >(({ className, ...props }, ref) => (
42
+ <TabsPrimitive.Content
43
+ ref={ref}
44
+ className={cn(
45
+ "mt-2 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 dark:ring-offset-slate-950 dark:focus-visible:ring-slate-300",
46
+ className,
47
+ )}
48
+ {...props}
49
+ />
50
+ ))
51
+ TabsContent.displayName = TabsPrimitive.Content.displayName
52
+
53
+ export { Tabs, TabsList, TabsTrigger, TabsContent }
@@ -0,0 +1,7 @@
1
+ import * as Core from "@tscircuit/core"
2
+
3
+ export const renderToCircuitJson = (board: React.ReactElement) => {
4
+ const circuit = new Core.Circuit()
5
+ circuit.add(board)
6
+ return circuit.getCircuitJson()
7
+ }
@@ -0,0 +1,17 @@
1
+ import { useEffect } from "react"
2
+ import styles from "./styles.generated"
3
+
4
+ export const useStyles = () => {
5
+ useEffect(() => {
6
+ // Check if styles are already added
7
+ const existingStyle = document.querySelector(
8
+ 'style[data-styles="tscircuit-runframe"]',
9
+ )
10
+ if (existingStyle) return
11
+
12
+ const styleElement = document.createElement("style")
13
+ styleElement.setAttribute("data-styles", "tscircuit-runframe")
14
+ styleElement.textContent = styles
15
+ document.head.appendChild(styleElement)
16
+ }, [])
17
+ }
package/lib/preview.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./components/CircuitJsonPreview"
package/lib/runner.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./preview"
2
+ export * from "./components/RunFrame"
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,156 @@
1
+ import type { ManualTraceHint } from "@tscircuit/layout"
2
+ import { getManualTraceHintFromEvent } from "@tscircuit/layout"
3
+ import type { EditEvent } from "@tscircuit/manual-edit-events"
4
+ import type {
5
+ AnyCircuitElement,
6
+ PcbComponent,
7
+ SourceComponentBase,
8
+ } from "circuit-json"
9
+
10
+ export interface PCBPlacement {
11
+ selector: string
12
+ center: { x: number; y: number }
13
+ relative_to: "group_center"
14
+ _edit_event_id?: string
15
+ }
16
+
17
+ export interface ManualEditState {
18
+ pcb_placements: PCBPlacement[]
19
+ edit_events: EditEvent[]
20
+ manual_trace_hints: ManualTraceHint[]
21
+ }
22
+
23
+ export const createInitialManualEditState = (): ManualEditState => ({
24
+ pcb_placements: [],
25
+ edit_events: [],
26
+ manual_trace_hints: [],
27
+ })
28
+
29
+ export const applyPcbEditEvents = ({
30
+ editEvents,
31
+ circuitJson,
32
+ manualEditsFileContent,
33
+ }: {
34
+ editEvents: EditEvent[]
35
+ circuitJson: AnyCircuitElement[]
36
+ manualEditsFileContent?: string
37
+ }): ManualEditState => {
38
+ try {
39
+ // Ensure we have a valid state to work with
40
+ const validatedManualEdits = ensureValidState(manualEditsFileContent)
41
+
42
+ // Create a new state object with properly initialized arrays
43
+ const newManualEditState: ManualEditState = {
44
+ pcb_placements: [...validatedManualEdits.pcb_placements],
45
+ edit_events: [...validatedManualEdits.edit_events],
46
+ manual_trace_hints: [...validatedManualEdits.manual_trace_hints],
47
+ }
48
+
49
+ // Create a set of handled event IDs
50
+ const handledEventIds = new Set<string>()
51
+
52
+ // Add existing event IDs to the set
53
+ newManualEditState.pcb_placements.forEach((placement) => {
54
+ if (placement._edit_event_id) {
55
+ handledEventIds.add(placement._edit_event_id)
56
+ }
57
+ })
58
+
59
+ newManualEditState.edit_events.forEach((event) => {
60
+ if (event.edit_event_id) {
61
+ handledEventIds.add(event.edit_event_id)
62
+ }
63
+ })
64
+
65
+ // Process new edit events
66
+ for (const editEvent of editEvents) {
67
+ if (
68
+ (editEvent.in_progress && !editEvent.edit_event_id) ||
69
+ handledEventIds.has(editEvent.edit_event_id)
70
+ )
71
+ continue
72
+
73
+ if (
74
+ editEvent.pcb_edit_event_type === "edit_component_location" &&
75
+ editEvent.pcb_component_id
76
+ ) {
77
+ // Find the component in the circuitJson
78
+ const pcbComponent = circuitJson.find(
79
+ (item: AnyCircuitElement) =>
80
+ item.type === "pcb_component" &&
81
+ item.pcb_component_id === editEvent.pcb_component_id,
82
+ ) as PcbComponent
83
+
84
+ if (!pcbComponent?.pcb_component_id) continue
85
+
86
+ const nameofComponent = circuitJson.find(
87
+ (item: AnyCircuitElement) =>
88
+ item.type === "source_component" &&
89
+ item.source_component_id === pcbComponent.source_component_id,
90
+ ) as SourceComponentBase
91
+
92
+ // Update or add placement
93
+ const existingPlacementIndex =
94
+ newManualEditState.pcb_placements.findIndex(
95
+ (p) => p.selector === nameofComponent.name,
96
+ )
97
+
98
+ const newPlacement: PCBPlacement = {
99
+ selector: nameofComponent.name,
100
+ center: editEvent.new_center,
101
+ relative_to: "group_center",
102
+ _edit_event_id: editEvent.edit_event_id,
103
+ }
104
+
105
+ if (existingPlacementIndex !== -1) {
106
+ // Update existing placement
107
+ newManualEditState.pcb_placements[existingPlacementIndex] =
108
+ newPlacement
109
+ } else {
110
+ // Add new placement
111
+ newManualEditState.pcb_placements.push(newPlacement)
112
+ }
113
+ } else if (editEvent.pcb_edit_event_type === "edit_trace_hint") {
114
+ const newTraceHint = getManualTraceHintFromEvent(circuitJson, editEvent)
115
+ if (newTraceHint) {
116
+ newManualEditState.manual_trace_hints = [
117
+ ...newManualEditState.manual_trace_hints.filter(
118
+ (th) => th.pcb_port_selector !== newTraceHint.pcb_port_selector,
119
+ ),
120
+ newTraceHint,
121
+ ]
122
+ }
123
+ } else {
124
+ // Add any other type of event to edit_events array
125
+ newManualEditState.edit_events.push(editEvent)
126
+ }
127
+
128
+ handledEventIds.add(editEvent.edit_event_id)
129
+ }
130
+
131
+ return newManualEditState
132
+ } catch (error) {
133
+ console.error("Error handling edit events:", error)
134
+ // Return a fresh state if there's an error
135
+ return createInitialManualEditState()
136
+ }
137
+ }
138
+
139
+ // Helper function to ensure we have a valid state
140
+ const ensureValidState = (manualEditsFileContent?: string): ManualEditState => {
141
+ if (!manualEditsFileContent) return createInitialManualEditState()
142
+
143
+ const manualEditState = JSON.parse(manualEditsFileContent)
144
+
145
+ return {
146
+ pcb_placements: Array.isArray(manualEditState.pcb_placements)
147
+ ? manualEditState.pcb_placements
148
+ : [],
149
+ edit_events: Array.isArray(manualEditState.edit_events)
150
+ ? manualEditState.edit_events
151
+ : [],
152
+ manual_trace_hints: Array.isArray(manualEditState.manual_trace_hints)
153
+ ? manualEditState.manual_trace_hints
154
+ : [],
155
+ }
156
+ }