@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,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,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,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
|
+
}
|