iprov
This commit is contained in:
@@ -12,6 +12,8 @@ export type BaseNodeProps = ComponentProps<"div"> & {
|
||||
handles?: ReactNode;
|
||||
/** When provided, node uses this size (e.g. from React Flow width/height). Enables correct resize behavior. */
|
||||
dimensions?: { width: number; height: number };
|
||||
/** When true, node is selected (from React Flow NodeProps). Used for visible selected state. */
|
||||
selected?: boolean;
|
||||
};
|
||||
|
||||
export function BaseNode({
|
||||
@@ -22,6 +24,7 @@ export function BaseNode({
|
||||
nodeId,
|
||||
handles,
|
||||
children,
|
||||
selected,
|
||||
...props
|
||||
}: BaseNodeProps) {
|
||||
const hasSize =
|
||||
@@ -48,15 +51,10 @@ export function BaseNode({
|
||||
className={cn(
|
||||
"bg-card text-card-foreground relative rounded-md border",
|
||||
"hover:ring-1",
|
||||
// React Flow displays node elements inside of a `NodeWrapper` component,
|
||||
// which compiles down to a div with the class `react-flow__node`.
|
||||
// When a node is selected, the class `selected` is added to the
|
||||
// `react-flow__node` element. This allows us to style the node when it
|
||||
// is selected, using Tailwind's `&` selector.
|
||||
"[.react-flow\\_\\_node.selected_&]:border-muted-foreground",
|
||||
"[.react-flow\\_\\_node.selected_&]:shadow-lg",
|
||||
selected && "border-primary shadow-[0_0_0_2px_hsl(var(--primary)_/_0.4)]",
|
||||
className,
|
||||
)}
|
||||
data-selected={selected}
|
||||
style={appliedStyle}
|
||||
tabIndex={0}
|
||||
{...props}
|
||||
|
||||
@@ -47,7 +47,7 @@ export type ConfigNodeData = { configType?: ConfigTypeId; content?: string; titl
|
||||
|
||||
type Props = AbstractNodeProps<ConfigNodeData>
|
||||
|
||||
function ConfigNodeComponent({ id, data, width, height }: Props) {
|
||||
function ConfigNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
const configTypeId = getConfigTypeId(data ?? {})
|
||||
const configType = getConfigType(configTypeId)
|
||||
const content = getConfigContent(data ?? {})
|
||||
@@ -232,7 +232,7 @@ function ConfigNodeComponent({ id, data, width, height }: Props) {
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<BaseNode className="min-w-80 min-h-[280px]" dimensions={dimensions} resizable nodeId={id} handles={<><InputHandle id="ain" nodeId={id} /><OutputHandle id="out" /></>}>
|
||||
<BaseNode className="min-w-80 min-h-[280px]" dimensions={dimensions} resizable nodeId={id} selected={selected} handles={<><InputHandle id="ain" nodeId={id} /><OutputHandle id="out" /></>}>
|
||||
<BaseNodeHeaderRow
|
||||
icon={<ScrollText className="size-4" />}
|
||||
title={<NodeHeaderTitle nodeId={id} displayTitle={`${id}`} />}
|
||||
|
||||
@@ -27,7 +27,7 @@ export type FunctionNodeData = { body?: string }
|
||||
|
||||
type Props = AbstractNodeProps<FunctionNodeData>
|
||||
|
||||
function FunctionNodeComponent({ id, data, width, height }: Props) {
|
||||
function FunctionNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
const bodyValue = data?.body ?? ''
|
||||
const { theme } = useTheme()
|
||||
const { nodes, sourceIds, updateData } = useAbstractNode<FunctionNodeData>(id, data ?? {})
|
||||
@@ -91,7 +91,7 @@ function FunctionNodeComponent({ id, data, width, height }: Props) {
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<BaseNode className="min-w-72 min-h-[260px]" dimensions={dimensions} resizable nodeId={id} handles={<><InputHandle id="in" nodeId={id} /><OutputHandle id="out" /></>}>
|
||||
<BaseNode className="min-w-72 min-h-[260px]" dimensions={dimensions} resizable nodeId={id} selected={selected} handles={<><InputHandle id="in" nodeId={id} /><OutputHandle id="out" /></>}>
|
||||
<BaseNodeHeaderRow icon={<Code2 className="size-4" />} title={<NodeHeaderTitle nodeId={id} displayTitle={id} />} />
|
||||
|
||||
<BaseNodeContent>
|
||||
|
||||
@@ -36,7 +36,7 @@ const DEFAULT_VIEWPORT_HEIGHT = 800
|
||||
|
||||
type Props = AbstractNodeProps<RenderingNodeData>
|
||||
|
||||
function RenderingNodeComponent({ id, data, width, height }: Props) {
|
||||
function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
const [renderedContent, setRenderedContent] = useState<string | null>(null)
|
||||
const [error, setError] = useState<null | { kind: string; message: string }>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -529,6 +529,31 @@ function RenderingNodeComponent({ id, data, width, height }: Props) {
|
||||
img.src = dataUrl
|
||||
}, [id, renderedContent, isSvgOutput])
|
||||
|
||||
const copyPng = useCallback(() => {
|
||||
if (!renderedContent || !isSvgOutput) return
|
||||
const dataUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(renderedContent)
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = img.naturalWidth
|
||||
canvas.height = img.naturalHeight
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
ctx.drawImage(img, 0, 0)
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) navigator.clipboard?.write([new ClipboardItem({ 'image/png': blob })]).catch(() => {})
|
||||
}, 'image/png')
|
||||
}
|
||||
img.onerror = () => {}
|
||||
img.src = dataUrl
|
||||
}, [renderedContent, isSvgOutput])
|
||||
|
||||
const copySvg = useCallback(() => {
|
||||
if (!renderedContent || !isSvgOutput) return
|
||||
const blob = new Blob([renderedContent], { type: 'image/svg+xml' })
|
||||
navigator.clipboard?.write([new ClipboardItem({ 'image/svg+xml': blob })]).catch(() => {})
|
||||
}, [renderedContent, isSvgOutput])
|
||||
|
||||
const status = loading ? 'loading' : error ? 'error' : renderedContent ? 'success' : 'initial'
|
||||
|
||||
const [viewportDraft, setViewportDraft] = useState({ width: viewportWidth, height: viewportHeight })
|
||||
@@ -545,7 +570,7 @@ function RenderingNodeComponent({ id, data, width, height }: Props) {
|
||||
|
||||
return (
|
||||
<NodeStatusIndicator status={status} variant="border" width={dimensions?.width} height={dimensions?.height}>
|
||||
<BaseNode className="min-w-96 min-h-[320px]" dimensions={dimensions} resizable nodeId={id} handles={<InputHandle id="ain" nodeId={id} />}>
|
||||
<BaseNode className="min-w-96 min-h-[320px]" dimensions={dimensions} resizable nodeId={id} selected={selected} handles={<InputHandle id="ain" nodeId={id} />}>
|
||||
<BaseNodeHeaderRow icon={<Sparkles className="size-4" />} title={<NodeHeaderTitle nodeId={id} displayTitle={id} />} />
|
||||
|
||||
<BaseNodeContent>
|
||||
@@ -602,14 +627,20 @@ function RenderingNodeComponent({ id, data, width, height }: Props) {
|
||||
<MenubarSub>
|
||||
<MenubarSeparator />
|
||||
<MenubarSubTrigger className="text-xs" disabled={!isSvgOutput}>
|
||||
Export
|
||||
Export / Copy
|
||||
</MenubarSubTrigger>
|
||||
<MenubarSubContent className="min-w-[10rem]">
|
||||
<MenubarItem className="text-xs" onClick={downloadPng} disabled={!isSvgOutput}>
|
||||
PNG
|
||||
</MenubarItem>
|
||||
<MenubarSubContent className="min-w-[10rem]" aria-label="Export or copy diagram">
|
||||
<MenubarItem className="text-xs" onClick={downloadSvg} disabled={!isSvgOutput}>
|
||||
SVG
|
||||
Download SVG
|
||||
</MenubarItem>
|
||||
<MenubarItem className="text-xs" onClick={downloadPng} disabled={!isSvgOutput}>
|
||||
Download PNG
|
||||
</MenubarItem>
|
||||
<MenubarItem className="text-xs" onClick={copySvg} disabled={!isSvgOutput}>
|
||||
Copy SVG
|
||||
</MenubarItem>
|
||||
<MenubarItem className="text-xs" onClick={copyPng} disabled={!isSvgOutput}>
|
||||
Copy image
|
||||
</MenubarItem>
|
||||
</MenubarSubContent>
|
||||
</MenubarSub>
|
||||
|
||||
@@ -47,7 +47,7 @@ function coerceValue(raw: string, valueType: ValueType): string | number | boole
|
||||
}
|
||||
}
|
||||
|
||||
function VariableNodeComponent({ id, data }: Props) {
|
||||
function VariableNodeComponent({ id, data, selected }: Props) {
|
||||
const { updateData } = useAbstractNode<VariableNodeData>(id, data ?? {})
|
||||
|
||||
const valueType: ValueType = data?.valueType ?? 'string'
|
||||
@@ -79,7 +79,7 @@ function VariableNodeComponent({ id, data }: Props) {
|
||||
)
|
||||
|
||||
return (
|
||||
<BaseNode className="min-w-56 min-h-[180px]" handles={<OutputHandle id="out" />}>
|
||||
<BaseNode className="min-w-56 min-h-[180px]" selected={selected} handles={<OutputHandle id="out" />}>
|
||||
<BaseNodeHeaderRow icon={<Variable className="size-4" />} title={<NodeHeaderTitle nodeId={id} displayTitle={id} />} />
|
||||
|
||||
<BaseNodeContent>
|
||||
|
||||
Reference in New Issue
Block a user