fix styles and menu

This commit is contained in:
2026-03-06 00:39:26 +01:00
parent 417999d4d8
commit 65b7cb37fe
11 changed files with 1232 additions and 101 deletions

23
components.json Normal file
View File

@@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.cjs",
"css": "src/styles.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}

817
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,14 +9,21 @@
},
"dependencies": {
"@monaco-editor/react": "^4.7.0",
"@radix-ui/react-context-menu": "^2.2.16",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"js-yaml": "^4.1.0",
"lucide-react": "^0.577.0",
"monaco-editor": "^0.55.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"reactflow": "^11.0.0",
"shadcn": "^3.8.5"
"shadcn": "^3.8.5",
"tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@types/node": "^25.3.3",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@vitejs/plugin-react": "^3.0.0",

View File

@@ -1,3 +1,9 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
module.exports = {
plugins: {
tailwindcss: {},

View File

@@ -18,7 +18,14 @@ import CustomNode from './components/CustomNode'
import ConfigNode from './components/ConfigNode'
import RenderingNode from './components/RenderingNode'
import FlowContext from './lib/flowContext'
import {
ContextMenu,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuLabel,
ContextMenuTrigger,
} from "@/components/ui/context-menu"
// Generate short unique node ids when missing
const genId = () => `node_${Math.random().toString(36).slice(2, 9)}`
@@ -45,7 +52,8 @@ export default function App() {
const [rfInstance, setRfInstance] = React.useState<any | null>(null)
const wrapperRef = React.useRef<HTMLDivElement | null>(null)
const [contextMenu, setContextMenu] = React.useState<null | { x: number; y: number; clientX: number; clientY: number }>(null)
const lastClickRef = React.useRef<{ clientX: number; clientY: number } | null>(null)
const [contextTarget, setContextTarget] = React.useState<null | { type: 'canvas' | 'node'; nodeId?: string; clientX: number; clientY: number }>(null)
const nodeTypes = React.useMemo(
() => ({ custom: CustomNode, config: ConfigNode, render: RenderingNode }),
@@ -61,26 +69,31 @@ export default function App() {
setRfInstance(instance)
}, [])
const hideMenu = React.useCallback(() => setContextMenu(null), [])
const onCanvasContextMenu = React.useCallback((ev: React.MouseEvent) => {
ev.preventDefault()
const target = ev.target as HTMLElement
// don't show menu when right-clicking nodes or handles
if (target.closest('.react-flow__node') || target.closest('.react-flow__handle')) return
const rect = wrapperRef.current?.getBoundingClientRect()
const nodeEl = target.closest('.react-flow__node') as HTMLElement | null
const handleEl = target.closest('.react-flow__handle') as HTMLElement | null
const clientX = ev.clientX
const clientY = ev.clientY
const x = rect ? clientX - rect.left : ev.clientX
const y = rect ? clientY - rect.top : ev.clientY
setContextMenu({ x, y, clientX, clientY })
lastClickRef.current = { clientX, clientY }
if (nodeEl && !handleEl) {
const nodeId = nodeEl.dataset?.id || nodeEl.getAttribute('data-id') || (nodeEl.id && nodeEl.id.startsWith('reactflow__node-') ? nodeEl.id.replace('reactflow__node-', '') : undefined)
setContextTarget({ type: 'node', nodeId, clientX, clientY })
return
}
setContextTarget({ type: 'canvas', clientX, clientY })
}, [])
const createNode = React.useCallback(
(type: string) => {
if (!rfInstance) return
if (!contextMenu) return
const { clientX, clientY } = contextMenu
const click = contextTarget ?? lastClickRef.current
const clientX = click?.clientX ?? window.innerWidth / 2
const clientY = click?.clientY ?? window.innerHeight / 2
const rect = wrapperRef.current?.getBoundingClientRect()
const point = rect ? { x: clientX - rect.left, y: clientY - rect.top } : { x: clientX, y: clientY }
let position = point
@@ -99,52 +112,60 @@ export default function App() {
data: type === 'config' ? { yaml: '# Enter YAML here\n', title: `config-${id}` } : {},
}
setNodes((nds) => nds.concat(newNode))
hideMenu()
lastClickRef.current = null
setContextTarget(null)
},
[rfInstance, contextMenu, hideMenu, setNodes]
[rfInstance, setNodes]
)
const deleteNode = React.useCallback((id: string | undefined) => {
if (!id) return
setNodes((nds) => nds.filter((n) => n.id !== id))
setEdges((eds) => eds.filter((e) => e.source !== id && e.target !== id))
setContextTarget(null)
}, [setNodes, setEdges])
return (
<div className="reactflow-wrapper" ref={wrapperRef} onContextMenu={onCanvasContextMenu} style={{ position: 'relative' }}>
<FlowContext.Provider value={{ nodes, setNodes, edges, setEdges }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
fitView
onInit={onInit}
>
<Background />
<Controls />
<MiniMap />
</ReactFlow>
{contextMenu && (
<div
className="absolute bg-white border rounded shadow-md text-sm"
style={{ left: contextMenu.x, top: contextMenu.y, zIndex: 9999 }}
onMouseLeave={hideMenu}
>
<div className="p-2">
<div className="text-xs text-gray-700 mb-2">Create node</div>
<div className="space-y-1">
<button className="w-full text-left px-2 py-1 hover:bg-gray-100" onClick={() => createNode('config')}>
Configuration Node
</button>
<button className="w-full text-left px-2 py-1 hover:bg-gray-100" onClick={() => createNode('render')}>
Rendering Node
</button>
<button className="w-full text-left px-2 py-1 hover:bg-gray-100" onClick={() => createNode('custom')}>
Custom Node
</button>
</div>
<ContextMenu>
<ContextMenuTrigger asChild>
<div style={{ width: '100%', height: '100%' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
fitView
onInit={onInit}
>
<Background />
<Controls />ß
<MiniMap />
</ReactFlow>
</div>
</div>
)}
</ContextMenuTrigger>
<ContextMenuContent>
{contextTarget?.type === 'node' ? (
<ContextMenuItem onSelect={() => deleteNode(contextTarget.nodeId)}>Delete</ContextMenuItem>
) : (
<>
<ContextMenuGroup>
<ContextMenuLabel>New Node</ContextMenuLabel>
<ContextMenuItem onSelect={() => createNode('config')}>Config</ContextMenuItem>
<ContextMenuItem onSelect={() => createNode('render')}>Renderer</ContextMenuItem>
<ContextMenuItem onSelect={() => createNode('custom')}>Custom</ContextMenuItem>
</ContextMenuGroup>
</>
)}
</ContextMenuContent>
</ContextMenu>
</FlowContext.Provider>
</div>
)
}

View File

@@ -0,0 +1,198 @@
import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const ContextMenu = ContextMenuPrimitive.Root
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
const ContextMenuGroup = ContextMenuPrimitive.Group
const ContextMenuPortal = ContextMenuPrimitive.Portal
const ContextMenuSub = ContextMenuPrimitive.Sub
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
))
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground 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 origin-[--radix-context-menu-content-transform-origin]",
className
)}
{...props}
/>
))
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 max-h-[--radix-context-menu-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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 origin-[--radix-context-menu-content-transform-origin]",
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
))
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
))
ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-4 w-4 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
))
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
))
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
const ContextMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
ContextMenuShortcut.displayName = "ContextMenuShortcut"
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -1,4 +1,4 @@
@import "/node_modules/shadcn/dist/tailwind.css";
@import "shadcn/dist/tailwind.css";
@tailwind base;
@tailwind components;
@@ -32,3 +32,60 @@ body {
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, 'Roboto Mono', 'Courier New', monospace;
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}

View File

@@ -1,10 +1,57 @@
/* Tailwind configuration for Vite + React */
module.exports = {
content: [
'./index.html',
'./src/**/*.{js,ts,jsx,tsx}'
],
darkMode: ['class'],
content: ['./index.html', './src/**/*.{ts,tsx,js,jsx}'],
theme: {
extend: {}
extend: {
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
}
}
},
plugins: []
plugins: [require('tailwindcss-animate')],
}

34
tsconfig.json Normal file
View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": [
"DOM",
"DOM.Iterable",
"ESNext"
],
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
],
"@": [
"src"
]
}
},
"include": [
"src"
]
}

View File

@@ -2,6 +2,7 @@ import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
resolve: {