feat: full screen node
This commit is contained in:
@@ -51,6 +51,11 @@ import {
|
|||||||
EmptyTitle,
|
EmptyTitle,
|
||||||
} from '@/components/ui/empty'
|
} from '@/components/ui/empty'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
import { ClipboardPaste, FolderOpen, FileStack, CircleDotDashed } from 'lucide-react'
|
import { ClipboardPaste, FolderOpen, FileStack, CircleDotDashed } from 'lucide-react'
|
||||||
import { DEFAULT_NODE_STYLE, getDefaultDataForType, getNextNodeId } from '@/lib/flowUtils'
|
import { DEFAULT_NODE_STYLE, getDefaultDataForType, getNextNodeId } from '@/lib/flowUtils'
|
||||||
import {
|
import {
|
||||||
@@ -58,6 +63,7 @@ import {
|
|||||||
getRegisteredNodeTypesGroupedByClassification,
|
getRegisteredNodeTypesGroupedByClassification,
|
||||||
getRegisteredNodeTypeIds,
|
getRegisteredNodeTypeIds,
|
||||||
getDefaultStyle,
|
getDefaultStyle,
|
||||||
|
getNodeType,
|
||||||
isConnectionAllowed,
|
isConnectionAllowed,
|
||||||
} from '@/lib/nodeRegistry'
|
} from '@/lib/nodeRegistry'
|
||||||
import type { AppNode, AppEdge } from '@/lib/nodeTypes'
|
import type { AppNode, AppEdge } from '@/lib/nodeTypes'
|
||||||
@@ -131,6 +137,70 @@ function FlowFitViewOnLoad() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FullscreenNodeOverlayProps = {
|
||||||
|
nodeId: string
|
||||||
|
nodes: AppNode[]
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function FullscreenNodeOverlay({ nodeId, nodes, onClose }: FullscreenNodeOverlayProps) {
|
||||||
|
const open = nodeId != null
|
||||||
|
const node = nodes.find((n) => n.id === nodeId)
|
||||||
|
if (!nodeId) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={(isOpen) => { if (!isOpen) onClose() }}>
|
||||||
|
<DialogContent
|
||||||
|
className="max-w-[94vw] w-[94vw] max-h-[90vh] h-[90vh] p-0 gap-0 overflow-hidden flex flex-col border rounded-lg shadow-xl bg-card"
|
||||||
|
aria-describedby={undefined}
|
||||||
|
hideCloseButton
|
||||||
|
>
|
||||||
|
<DialogTitle className="sr-only">Node: {node?.id ?? nodeId}</DialogTitle>
|
||||||
|
{node && node.type && (
|
||||||
|
<FullscreenNodeContent node={node} />
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FullscreenNodeContent({ node }: { node: AppNode }) {
|
||||||
|
const descriptor = getNodeType(node.type ?? '')
|
||||||
|
const NodeComponent = descriptor?.component as React.ComponentType<{
|
||||||
|
id: string
|
||||||
|
data: Record<string, unknown>
|
||||||
|
type?: string
|
||||||
|
selected?: boolean
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
}> | undefined
|
||||||
|
if (!NodeComponent) return null
|
||||||
|
|
||||||
|
const w = typeof window !== 'undefined' ? Math.round(window.innerWidth * 0.94) : 1200
|
||||||
|
const h = typeof window !== 'undefined' ? Math.round(window.innerHeight * 0.9) : 800
|
||||||
|
|
||||||
|
const fullscreenNodeForStore: AppNode = {
|
||||||
|
...node,
|
||||||
|
position: node.position ?? { x: 0, y: 0 },
|
||||||
|
style: { ...(node.style as object), width: w, height: h },
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 min-h-0 overflow-hidden flex">
|
||||||
|
<ReactFlowProvider initialNodes={[fullscreenNodeForStore]} initialEdges={[]}>
|
||||||
|
<NodeComponent
|
||||||
|
id={node.id}
|
||||||
|
data={(node.data as Record<string, unknown>) ?? {}}
|
||||||
|
type={node.type}
|
||||||
|
selected={false}
|
||||||
|
width={w}
|
||||||
|
height={h}
|
||||||
|
/>
|
||||||
|
</ReactFlowProvider>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export type CanvasPageProps = {
|
export type CanvasPageProps = {
|
||||||
/** Optional project id for future per-project graph loading */
|
/** Optional project id for future per-project graph loading */
|
||||||
projectId?: string
|
projectId?: string
|
||||||
@@ -193,6 +263,7 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
|
|||||||
const [isPanning, setIsPanning] = React.useState(false)
|
const [isPanning, setIsPanning] = React.useState(false)
|
||||||
const [isSelecting, setIsSelecting] = React.useState(false)
|
const [isSelecting, setIsSelecting] = React.useState(false)
|
||||||
const [ariaAnnouncement, setAriaAnnouncement] = React.useState<string | null>(null)
|
const [ariaAnnouncement, setAriaAnnouncement] = React.useState<string | null>(null)
|
||||||
|
const [fullscreenNodeId, setFullscreenNodeId] = React.useState<string | null>(null)
|
||||||
const nodesRef = useRef(nodes)
|
const nodesRef = useRef(nodes)
|
||||||
nodesRef.current = nodes
|
nodesRef.current = nodes
|
||||||
|
|
||||||
@@ -412,6 +483,8 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
|
|||||||
setConnectionFrom,
|
setConnectionFrom,
|
||||||
isValidConnection,
|
isValidConnection,
|
||||||
flowActionsRef,
|
flowActionsRef,
|
||||||
|
fullscreenNodeId,
|
||||||
|
setFullscreenNodeId,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
nodes,
|
nodes,
|
||||||
@@ -424,6 +497,8 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
|
|||||||
setConnectionFrom,
|
setConnectionFrom,
|
||||||
isValidConnection,
|
isValidConnection,
|
||||||
flowActionsRef,
|
flowActionsRef,
|
||||||
|
fullscreenNodeId,
|
||||||
|
setFullscreenNodeId,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -707,6 +782,13 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
|
|||||||
</ContextMenuGroup>
|
</ContextMenuGroup>
|
||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
|
{fullscreenNodeId && (
|
||||||
|
<FullscreenNodeOverlay
|
||||||
|
nodeId={fullscreenNodeId}
|
||||||
|
nodes={nodes}
|
||||||
|
onClose={() => setFullscreenNodeId(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</FlowContext.Provider>
|
</FlowContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -122,19 +122,37 @@ export function BaseNodeHeaderRow({
|
|||||||
title,
|
title,
|
||||||
right,
|
right,
|
||||||
className,
|
className,
|
||||||
|
onHeaderDoubleClick,
|
||||||
}: {
|
}: {
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
title: ReactNode;
|
title: ReactNode;
|
||||||
/** Optional right-side content (e.g. type selector). */
|
/** Optional right-side content (e.g. type selector). */
|
||||||
right?: ReactNode;
|
right?: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
/** When provided, double-click on the header triggers this (e.g. fullscreen). Header gets cursor-pointer and nodrag. */
|
||||||
|
onHeaderDoubleClick?: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
|
role={onHeaderDoubleClick ? 'button' : undefined}
|
||||||
|
tabIndex={onHeaderDoubleClick ? 0 : undefined}
|
||||||
|
onDoubleClick={onHeaderDoubleClick}
|
||||||
|
onKeyDown={
|
||||||
|
onHeaderDoubleClick
|
||||||
|
? (e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
onHeaderDoubleClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 mx-0 my-0 flex flex-row items-center justify-between gap-2 px-3 py-2",
|
"shrink-0 mx-0 my-0 flex flex-row items-center justify-between gap-2 px-3 py-2",
|
||||||
|
onHeaderDoubleClick && "cursor-pointer",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
title={onHeaderDoubleClick ? 'Double-click for full screen' : undefined}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
<h3
|
<h3
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ export function FlowKeyboardShortcuts() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeyDown = (ev: KeyboardEvent) => {
|
const onKeyDown = (ev: KeyboardEvent) => {
|
||||||
if (ev.key === 'Escape') {
|
if (ev.key === 'Escape') {
|
||||||
|
const openDialog = document.querySelector('[role="dialog"]')
|
||||||
|
if (openDialog && ev.target instanceof Node && openDialog.contains(ev.target)) return
|
||||||
setConnectionFrom?.(null)
|
setConnectionFrom?.(null)
|
||||||
setNodes?.((nds) => nds.map((n) => ({ ...n, selected: false })))
|
setNodes?.((nds) => nds.map((n) => ({ ...n, selected: false })))
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useMemo, useState } from 'react'
|
import React, { useCallback, useContext, useMemo, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
AbstractNodeProps,
|
AbstractNodeProps,
|
||||||
createAbstractNodeComponent,
|
createAbstractNodeComponent,
|
||||||
@@ -15,6 +15,8 @@ import { NodeMenubar } from '@/components/base/NodeMenubar'
|
|||||||
import { NodeFooterEdgeIndicators } from '@/components/base/NodeFooterEdgeIndicators'
|
import { NodeFooterEdgeIndicators } from '@/components/base/NodeFooterEdgeIndicators'
|
||||||
import { NodeHeaderTitle } from '@/components/base/NodeHeaderTitle'
|
import { NodeHeaderTitle } from '@/components/base/NodeHeaderTitle'
|
||||||
import { InputHandle, OutputHandle } from '@/components/base/NodeHandles'
|
import { InputHandle, OutputHandle } from '@/components/base/NodeHandles'
|
||||||
|
import FlowContext from '@/lib/flowContext'
|
||||||
|
import { getNodeType } from '@/lib/nodeRegistry'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { usePlatform } from '@/app/kosmos/KosmosContext'
|
import { usePlatform } from '@/app/kosmos/KosmosContext'
|
||||||
import { Bot, Play, Loader2 } from 'lucide-react'
|
import { Bot, Play, Loader2 } from 'lucide-react'
|
||||||
@@ -51,6 +53,9 @@ function serializeNodeForContext(nodes: { id: string; type?: string; data?: unkn
|
|||||||
}
|
}
|
||||||
|
|
||||||
function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
||||||
|
const flowContext = useContext(FlowContext)
|
||||||
|
const setFullscreenNodeId = flowContext?.setFullscreenNodeId
|
||||||
|
const supportsFullscreen = getNodeType('agent')?.supportsFullscreen
|
||||||
const { nodes, sourceIds, updateData } = useAbstractNode<AgentNodeData>(id, data ?? {})
|
const { nodes, sourceIds, updateData } = useAbstractNode<AgentNodeData>(id, data ?? {})
|
||||||
const { aiConnection } = usePlatform()
|
const { aiConnection } = usePlatform()
|
||||||
const [running, setRunning] = useState(false)
|
const [running, setRunning] = useState(false)
|
||||||
@@ -137,6 +142,7 @@ function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
<BaseNodeHeaderRow
|
<BaseNodeHeaderRow
|
||||||
icon={<Bot className="size-4" />}
|
icon={<Bot className="size-4" />}
|
||||||
title={<NodeHeaderTitle nodeId={id} displayTitle={id} />}
|
title={<NodeHeaderTitle nodeId={id} displayTitle={id} />}
|
||||||
|
onHeaderDoubleClick={supportsFullscreen && setFullscreenNodeId ? () => setFullscreenNodeId(id) : undefined}
|
||||||
/>
|
/>
|
||||||
<BaseNodeContent>
|
<BaseNodeContent>
|
||||||
<div className="shrink-0 w-full">
|
<div className="shrink-0 w-full">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useMemo, useRef } from 'react'
|
import React, { useCallback, useContext, useMemo, useRef } from 'react'
|
||||||
import { autocompletion } from '@codemirror/autocomplete'
|
import { autocompletion } from '@codemirror/autocomplete'
|
||||||
import CodeMirror from '@uiw/react-codemirror'
|
import CodeMirror from '@uiw/react-codemirror'
|
||||||
import { javascript } from '@codemirror/lang-javascript'
|
import { javascript } from '@codemirror/lang-javascript'
|
||||||
@@ -42,12 +42,17 @@ import { InputHandle, OutputHandle } from '../base/NodeHandles'
|
|||||||
import { NodeFooterEdgeIndicators } from '../base/NodeFooterEdgeIndicators'
|
import { NodeFooterEdgeIndicators } from '../base/NodeFooterEdgeIndicators'
|
||||||
import { NodeHeaderTitle } from '../base/NodeHeaderTitle'
|
import { NodeHeaderTitle } from '../base/NodeHeaderTitle'
|
||||||
import { NodeMenubar } from '../base/NodeMenubar'
|
import { NodeMenubar } from '../base/NodeMenubar'
|
||||||
|
import FlowContext from '../../lib/flowContext'
|
||||||
|
import { getNodeType } from '../../lib/nodeRegistry'
|
||||||
|
|
||||||
export type ConfigNodeData = { configType?: ConfigTypeId; content?: string; title?: string; /** @deprecated use content */ plantuml?: string }
|
export type ConfigNodeData = { configType?: ConfigTypeId; content?: string; title?: string; /** @deprecated use content */ plantuml?: string }
|
||||||
|
|
||||||
type Props = AbstractNodeProps<ConfigNodeData>
|
type Props = AbstractNodeProps<ConfigNodeData>
|
||||||
|
|
||||||
function ConfigNodeComponent({ id, data, width, height, selected }: Props) {
|
function ConfigNodeComponent({ id, data, width, height, selected }: Props) {
|
||||||
|
const flowContext = useContext(FlowContext)
|
||||||
|
const setFullscreenNodeId = flowContext?.setFullscreenNodeId
|
||||||
|
const supportsFullscreen = getNodeType('config')?.supportsFullscreen
|
||||||
const configTypeId = getConfigTypeId(data ?? {})
|
const configTypeId = getConfigTypeId(data ?? {})
|
||||||
const configType = getConfigType(configTypeId)
|
const configType = getConfigType(configTypeId)
|
||||||
const content = getConfigContent(data ?? {})
|
const content = getConfigContent(data ?? {})
|
||||||
@@ -262,6 +267,7 @@ function ConfigNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
}
|
}
|
||||||
|
onHeaderDoubleClick={supportsFullscreen && setFullscreenNodeId ? () => setFullscreenNodeId(id) : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BaseNodeContent>
|
<BaseNodeContent>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useMemo, useRef } from 'react'
|
import React, { useCallback, useContext, useMemo, useRef } from 'react'
|
||||||
import CodeMirror from '@uiw/react-codemirror'
|
import CodeMirror from '@uiw/react-codemirror'
|
||||||
import { javascript } from '@codemirror/lang-javascript'
|
import { javascript } from '@codemirror/lang-javascript'
|
||||||
import {
|
import {
|
||||||
@@ -19,6 +19,8 @@ import { InputHandle, OutputHandle } from '../base/NodeHandles'
|
|||||||
import { NodeFooterEdgeIndicators } from '../base/NodeFooterEdgeIndicators'
|
import { NodeFooterEdgeIndicators } from '../base/NodeFooterEdgeIndicators'
|
||||||
import { NodeHeaderTitle } from '../base/NodeHeaderTitle'
|
import { NodeHeaderTitle } from '../base/NodeHeaderTitle'
|
||||||
import { NodeMenubar } from '../base/NodeMenubar'
|
import { NodeMenubar } from '../base/NodeMenubar'
|
||||||
|
import FlowContext from '../../lib/flowContext'
|
||||||
|
import { getNodeType } from '../../lib/nodeRegistry'
|
||||||
import { MenubarItem, MenubarShortcut } from '../ui/menubar'
|
import { MenubarItem, MenubarShortcut } from '../ui/menubar'
|
||||||
import { Kbd } from '../ui/kbd'
|
import { Kbd } from '../ui/kbd'
|
||||||
import { Code2, Variable } from 'lucide-react'
|
import { Code2, Variable } from 'lucide-react'
|
||||||
@@ -28,6 +30,9 @@ export type FunctionNodeData = { body?: string }
|
|||||||
type Props = AbstractNodeProps<FunctionNodeData>
|
type Props = AbstractNodeProps<FunctionNodeData>
|
||||||
|
|
||||||
function FunctionNodeComponent({ id, data, width, height, selected }: Props) {
|
function FunctionNodeComponent({ id, data, width, height, selected }: Props) {
|
||||||
|
const flowContext = useContext(FlowContext)
|
||||||
|
const setFullscreenNodeId = flowContext?.setFullscreenNodeId
|
||||||
|
const supportsFullscreen = getNodeType('function')?.supportsFullscreen
|
||||||
const bodyValue = data?.body ?? ''
|
const bodyValue = data?.body ?? ''
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const { nodes, sourceIds, updateData } = useAbstractNode<FunctionNodeData>(id, data ?? {})
|
const { nodes, sourceIds, updateData } = useAbstractNode<FunctionNodeData>(id, data ?? {})
|
||||||
@@ -92,7 +97,11 @@ function FunctionNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseNode className="min-w-72 min-h-[260px]" dimensions={dimensions} resizable nodeId={id} selected={selected} 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} />} />
|
<BaseNodeHeaderRow
|
||||||
|
icon={<Code2 className="size-4" />}
|
||||||
|
title={<NodeHeaderTitle nodeId={id} displayTitle={id} />}
|
||||||
|
onHeaderDoubleClick={supportsFullscreen && setFullscreenNodeId ? () => setFullscreenNodeId(id) : undefined}
|
||||||
|
/>
|
||||||
|
|
||||||
<BaseNodeContent>
|
<BaseNodeContent>
|
||||||
<div className="shrink-0 w-full">
|
<div className="shrink-0 w-full">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import nunjucks from 'nunjucks'
|
import nunjucks from 'nunjucks'
|
||||||
import CodeMirror from '@uiw/react-codemirror'
|
import CodeMirror from '@uiw/react-codemirror'
|
||||||
import { javascript } from '@codemirror/lang-javascript'
|
import { javascript } from '@codemirror/lang-javascript'
|
||||||
@@ -19,7 +19,8 @@ import {
|
|||||||
BaseNodeHeaderRow,
|
BaseNodeHeaderRow,
|
||||||
} from '../base/BaseNode'
|
} from '../base/BaseNode'
|
||||||
import { getDefaultDataForType, getNextNodeId } from '../../lib/flowUtils'
|
import { getDefaultDataForType, getNextNodeId } from '../../lib/flowUtils'
|
||||||
import { getDefaultStyle } from '../../lib/nodeRegistry'
|
import FlowContext from '../../lib/flowContext'
|
||||||
|
import { getDefaultStyle, getNodeType } from '../../lib/nodeRegistry'
|
||||||
import { NodeFooterEdgeIndicators } from '../base/NodeFooterEdgeIndicators'
|
import { NodeFooterEdgeIndicators } from '../base/NodeFooterEdgeIndicators'
|
||||||
import { NodeHeaderTitle } from '../base/NodeHeaderTitle'
|
import { NodeHeaderTitle } from '../base/NodeHeaderTitle'
|
||||||
import { NodeMenubar } from '../base/NodeMenubar'
|
import { NodeMenubar } from '../base/NodeMenubar'
|
||||||
@@ -47,6 +48,9 @@ type Props = AbstractNodeProps<RenderingNodeData>
|
|||||||
type ViewMode = 'preview' | 'raw'
|
type ViewMode = 'preview' | 'raw'
|
||||||
|
|
||||||
function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||||
|
const flowContext = useContext(FlowContext)
|
||||||
|
const setFullscreenNodeId = flowContext?.setFullscreenNodeId
|
||||||
|
const supportsFullscreen = getNodeType('render')?.supportsFullscreen
|
||||||
const [renderedContent, setRenderedContent] = useState<string | null>(null)
|
const [renderedContent, setRenderedContent] = useState<string | null>(null)
|
||||||
const [resolvedContent, setResolvedContent] = useState<string | null>(null)
|
const [resolvedContent, setResolvedContent] = useState<string | null>(null)
|
||||||
const [error, setError] = useState<null | { kind: string; message: string }>(null)
|
const [error, setError] = useState<null | { kind: string; message: string }>(null)
|
||||||
@@ -675,6 +679,7 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
<BaseNodeHeaderRow
|
<BaseNodeHeaderRow
|
||||||
icon={<Sparkles className="size-4" />}
|
icon={<Sparkles className="size-4" />}
|
||||||
title={<NodeHeaderTitle nodeId={id} displayTitle={id} />}
|
title={<NodeHeaderTitle nodeId={id} displayTitle={id} />}
|
||||||
|
onHeaderDoubleClick={supportsFullscreen && setFullscreenNodeId ? () => setFullscreenNodeId(id) : undefined}
|
||||||
right={
|
right={
|
||||||
incomingIds.length > 0 ? (
|
incomingIds.length > 0 ? (
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
|||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
const DialogContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { hideCloseButton?: boolean }
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, hideCloseButton, ...props }, ref) => (
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
<DialogOverlay />
|
<DialogOverlay />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
@@ -42,10 +42,12 @@ const DialogContent = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
{!hideCloseButton && (
|
||||||
<X className="h-4 w-4" />
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
<span className="sr-only">Close</span>
|
<X className="h-4 w-4" />
|
||||||
</DialogPrimitive.Close>
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ export type FlowContextValue = {
|
|||||||
isValidConnection: (connection: Connection) => boolean
|
isValidConnection: (connection: Connection) => boolean
|
||||||
/** Set by FlowKeyboardShortcuts so Node menubar can trigger paste / fit view. */
|
/** Set by FlowKeyboardShortcuts so Node menubar can trigger paste / fit view. */
|
||||||
flowActionsRef: React.MutableRefObject<FlowActions | null>
|
flowActionsRef: React.MutableRefObject<FlowActions | null>
|
||||||
|
/** When set, graph centers on this node and a fullscreen dialog shows the node. Cleared on close. */
|
||||||
|
fullscreenNodeId: string | null
|
||||||
|
setFullscreenNodeId: (id: string | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const FlowContext = React.createContext<FlowContextValue | null>(null)
|
const FlowContext = React.createContext<FlowContextValue | null>(null)
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ export type NodeTypeDescriptor = {
|
|||||||
getResetData?: (nodeId?: string) => Record<string, unknown>
|
getResetData?: (nodeId?: string) => Record<string, unknown>
|
||||||
/** Optional: label shown on edge when this type is the target (e.g. "render", "add input"). */
|
/** Optional: label shown on edge when this type is the target (e.g. "render", "add input"). */
|
||||||
connectionLabel?: string
|
connectionLabel?: string
|
||||||
|
/** When true, double-clicking the node header opens a fullscreen dialog for this node. */
|
||||||
|
supportsFullscreen?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const registry = new Map<string, NodeTypeDescriptor>()
|
const registry = new Map<string, NodeTypeDescriptor>()
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export function registerBuiltinNodes(): void {
|
|||||||
title: nodeId ?? '',
|
title: nodeId ?? '',
|
||||||
}),
|
}),
|
||||||
connectionLabel: 'adding input',
|
connectionLabel: 'adding input',
|
||||||
|
supportsFullscreen: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
registerNodeType({
|
registerNodeType({
|
||||||
@@ -58,6 +59,7 @@ export function registerBuiltinNodes(): void {
|
|||||||
menuLabel: 'Renderer',
|
menuLabel: 'Renderer',
|
||||||
menuIcon: <Sparkles className={ICON_CLASS} />,
|
menuIcon: <Sparkles className={ICON_CLASS} />,
|
||||||
connectionLabel: 'rendering',
|
connectionLabel: 'rendering',
|
||||||
|
supportsFullscreen: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
registerNodeType({
|
registerNodeType({
|
||||||
@@ -75,6 +77,7 @@ export function registerBuiltinNodes(): void {
|
|||||||
menuLabel: 'Agent',
|
menuLabel: 'Agent',
|
||||||
menuIcon: <Bot className={ICON_CLASS} />,
|
menuIcon: <Bot className={ICON_CLASS} />,
|
||||||
connectionLabel: 'prompt/context',
|
connectionLabel: 'prompt/context',
|
||||||
|
supportsFullscreen: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
registerNodeType({
|
registerNodeType({
|
||||||
@@ -128,5 +131,6 @@ export function registerBuiltinNodes(): void {
|
|||||||
menuIcon: <Code2 className={ICON_CLASS} />,
|
menuIcon: <Code2 className={ICON_CLASS} />,
|
||||||
getResetData: () => ({ body: '' }),
|
getResetData: () => ({ body: '' }),
|
||||||
connectionLabel: 'adding input',
|
connectionLabel: 'adding input',
|
||||||
|
supportsFullscreen: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user