From 2ffd5e2a8477ea7481455fd90a7def16bbe6d4cb Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Tue, 28 Jul 2026 18:26:13 -0400 Subject: [PATCH] refactor: LiteGraph-style node layout with NodeShell wrapper - NodeShell component wraps all nodes with handle rendering + px-8 content padding - Handles positioned at node edges with labels outside (no overlap with content) - Labels: 9px mono font, full opacity, color-matched to socket type - Removed old TypedHandle + NodeHandles (replaced by NodeShell internals) - All 18 node components updated to use NodeShell - Format node dynamically generates handles from template {0}, {1}, etc. --- .../admin/narrative-flow/BooleanNode.tsx | 7 +- .../admin/narrative-flow/BranchNode.tsx | 11 +-- .../admin/narrative-flow/ChoiceNode.tsx | 7 +- .../admin/narrative-flow/CompareNode.tsx | 15 +-- .../admin/narrative-flow/DateNode.tsx | 7 +- .../admin/narrative-flow/FlagNode.tsx | 7 +- .../admin/narrative-flow/FormatNode.tsx | 15 +-- .../admin/narrative-flow/GameDataNode.tsx | 7 +- .../admin/narrative-flow/LogicNode.tsx | 15 +-- .../admin/narrative-flow/MathNode.tsx | 15 +-- .../narrative-flow/NarrativeBeatNode.tsx | 7 +- .../admin/narrative-flow/NodeHandles.tsx | 51 ---------- .../admin/narrative-flow/NodeShell.tsx | 99 +++++++++++++++++++ .../admin/narrative-flow/NotNode.tsx | 15 +-- .../admin/narrative-flow/NumberNode.tsx | 7 +- .../admin/narrative-flow/OutcomeNode.tsx | 7 +- .../admin/narrative-flow/RandomNode.tsx | 7 +- .../admin/narrative-flow/SequenceNode.tsx | 11 +-- .../admin/narrative-flow/StringNode.tsx | 7 +- .../admin/narrative-flow/TypedHandle.tsx | 54 ---------- .../admin/narrative-flow/WaitNode.tsx | 7 +- 21 files changed, 165 insertions(+), 213 deletions(-) delete mode 100644 src/components/admin/narrative-flow/NodeHandles.tsx create mode 100644 src/components/admin/narrative-flow/NodeShell.tsx delete mode 100644 src/components/admin/narrative-flow/TypedHandle.tsx diff --git a/src/components/admin/narrative-flow/BooleanNode.tsx b/src/components/admin/narrative-flow/BooleanNode.tsx index 58aa751..aaebb6b 100644 --- a/src/components/admin/narrative-flow/BooleanNode.tsx +++ b/src/components/admin/narrative-flow/BooleanNode.tsx @@ -2,14 +2,13 @@ import { type NodeProps } from "@xyflow/react"; import { CheckCircleIcon } from "lucide-react"; -import { NodeHandles } from "./NodeHandles"; +import { NodeShell } from "./NodeShell"; export function BooleanNode({ data }: NodeProps) { const d = data as { value?: boolean; description?: string }; return ( -
- +
@@ -19,6 +18,6 @@ export function BooleanNode({ data }: NodeProps) {
{d.description || "Boolean flag"}
-
+
); } diff --git a/src/components/admin/narrative-flow/BranchNode.tsx b/src/components/admin/narrative-flow/BranchNode.tsx index f2842ed..608e59b 100644 --- a/src/components/admin/narrative-flow/BranchNode.tsx +++ b/src/components/admin/narrative-flow/BranchNode.tsx @@ -2,22 +2,19 @@ import { type NodeProps } from "@xyflow/react"; import { GitBranchIcon } from "lucide-react"; -import { NodeHandles } from "./NodeHandles"; +import { NodeShell } from "./NodeShell"; export function BranchNode(_props: NodeProps) { return ( -
- +
- - Branch - + Branch
true false
-
+ ); } diff --git a/src/components/admin/narrative-flow/ChoiceNode.tsx b/src/components/admin/narrative-flow/ChoiceNode.tsx index 8e98e32..c9d796c 100644 --- a/src/components/admin/narrative-flow/ChoiceNode.tsx +++ b/src/components/admin/narrative-flow/ChoiceNode.tsx @@ -2,15 +2,14 @@ import { type NodeProps } from "@xyflow/react"; import { PencilIcon, WaypointsIcon } from "lucide-react"; -import { NodeHandles } from "./NodeHandles"; +import { NodeShell } from "./NodeShell"; export function ChoiceNode({ data }: NodeProps) { const d = data as { question?: string; options?: Array<{ id: string; label: string }> }; const options = d.options ?? []; return ( -
- +
@@ -33,6 +32,6 @@ export function ChoiceNode({ data }: NodeProps) { )) )}
-
+ ); } diff --git a/src/components/admin/narrative-flow/CompareNode.tsx b/src/components/admin/narrative-flow/CompareNode.tsx index 42d1f75..44d6a17 100644 --- a/src/components/admin/narrative-flow/CompareNode.tsx +++ b/src/components/admin/narrative-flow/CompareNode.tsx @@ -2,24 +2,19 @@ import { type NodeProps } from "@xyflow/react"; import { EqualIcon } from "lucide-react"; -import { NodeHandles } from "./NodeHandles"; +import { NodeShell } from "./NodeShell"; export function CompareNode({ data }: NodeProps) { const d = data as { operator?: string }; const op = d.operator ?? ">"; return ( -
- +
- - Compare - + Compare
-
- {op} -
-
+
{op}
+ ); } diff --git a/src/components/admin/narrative-flow/DateNode.tsx b/src/components/admin/narrative-flow/DateNode.tsx index b268c93..e47ee83 100644 --- a/src/components/admin/narrative-flow/DateNode.tsx +++ b/src/components/admin/narrative-flow/DateNode.tsx @@ -2,14 +2,13 @@ import { type NodeProps } from "@xyflow/react"; import { CalendarIcon } from "lucide-react"; -import { NodeHandles } from "./NodeHandles"; +import { NodeShell } from "./NodeShell"; export function DateNode({ data }: NodeProps) { const d = data as { value?: string; description?: string }; return ( -
- +
@@ -19,6 +18,6 @@ export function DateNode({ data }: NodeProps) {
{d.description || "Date value"}
-
+
); } diff --git a/src/components/admin/narrative-flow/FlagNode.tsx b/src/components/admin/narrative-flow/FlagNode.tsx index 3335c36..ee9b5f5 100644 --- a/src/components/admin/narrative-flow/FlagNode.tsx +++ b/src/components/admin/narrative-flow/FlagNode.tsx @@ -2,14 +2,13 @@ import { type NodeProps } from "@xyflow/react"; import { FlagIcon } from "lucide-react"; -import { NodeHandles } from "./NodeHandles"; +import { NodeShell } from "./NodeShell"; export function FlagNode({ data }: NodeProps) { const d = data as { flagName?: string }; return ( -
- +
@@ -19,6 +18,6 @@ export function FlagNode({ data }: NodeProps) {
{d.flagName || "Unnamed flag"}
-
+
); } diff --git a/src/components/admin/narrative-flow/FormatNode.tsx b/src/components/admin/narrative-flow/FormatNode.tsx index a193d2f..c953f16 100644 --- a/src/components/admin/narrative-flow/FormatNode.tsx +++ b/src/components/admin/narrative-flow/FormatNode.tsx @@ -2,7 +2,7 @@ import { type NodeProps } from "@xyflow/react"; import { AlignLeftIcon } from "lucide-react"; -import { NodeHandles } from "./NodeHandles"; +import { NodeShell } from "./NodeShell"; import { NODE_SOCKETS, type SocketDef } from "./types"; function parseTemplateArgs(template: string): SocketDef[] { @@ -25,17 +25,12 @@ export function FormatNode({ data }: NodeProps) { ]; return ( -
- +
- - Format - + Format
-
- {template} -
-
+
{template}
+ ); } diff --git a/src/components/admin/narrative-flow/GameDataNode.tsx b/src/components/admin/narrative-flow/GameDataNode.tsx index e9b4ac4..b6ce5d2 100644 --- a/src/components/admin/narrative-flow/GameDataNode.tsx +++ b/src/components/admin/narrative-flow/GameDataNode.tsx @@ -2,7 +2,7 @@ import { type NodeProps } from "@xyflow/react"; import { DatabaseIcon } from "lucide-react"; -import { NodeHandles } from "./NodeHandles"; +import { NodeShell } from "./NodeShell"; const GAME_DATA_FIELDS = [ { value: "structure_health", label: "Structure Health" }, @@ -20,8 +20,7 @@ export function GameDataNode({ data }: NodeProps) { const fieldLabel = GAME_DATA_FIELDS.find((f) => f.value === d.field)?.label ?? "Select field"; return ( -
- +
@@ -31,6 +30,6 @@ export function GameDataNode({ data }: NodeProps) {
{fieldLabel}
-
+
); } diff --git a/src/components/admin/narrative-flow/LogicNode.tsx b/src/components/admin/narrative-flow/LogicNode.tsx index 556508f..f8f2d79 100644 --- a/src/components/admin/narrative-flow/LogicNode.tsx +++ b/src/components/admin/narrative-flow/LogicNode.tsx @@ -2,24 +2,19 @@ import { type NodeProps } from "@xyflow/react"; import { BinaryIcon } from "lucide-react"; -import { NodeHandles } from "./NodeHandles"; +import { NodeShell } from "./NodeShell"; export function LogicNode({ data }: NodeProps) { const d = data as { operator?: string }; const op = d.operator ?? "AND"; return ( -
- +
- - Logic - + Logic
-
- {op} -
-
+
{op}
+ ); } diff --git a/src/components/admin/narrative-flow/MathNode.tsx b/src/components/admin/narrative-flow/MathNode.tsx index 2eb32da..658a204 100644 --- a/src/components/admin/narrative-flow/MathNode.tsx +++ b/src/components/admin/narrative-flow/MathNode.tsx @@ -2,24 +2,19 @@ import { type NodeProps } from "@xyflow/react"; import { CalculatorIcon } from "lucide-react"; -import { NodeHandles } from "./NodeHandles"; +import { NodeShell } from "./NodeShell"; export function MathNode({ data }: NodeProps) { const d = data as { operator?: string }; const op = d.operator ?? "+"; return ( -
- +
- - Math - + Math
-
- {op} -
-
+
{op}
+ ); } diff --git a/src/components/admin/narrative-flow/NarrativeBeatNode.tsx b/src/components/admin/narrative-flow/NarrativeBeatNode.tsx index 50d6bfd..4102a8a 100644 --- a/src/components/admin/narrative-flow/NarrativeBeatNode.tsx +++ b/src/components/admin/narrative-flow/NarrativeBeatNode.tsx @@ -2,14 +2,13 @@ import { type NodeProps } from "@xyflow/react"; import { BookOpenTextIcon, PencilIcon } from "lucide-react"; -import { NodeHandles } from "./NodeHandles"; +import { NodeShell } from "./NodeShell"; export function NarrativeBeatNode({ data }: NodeProps) { const d = data as { title?: string; text?: string }; return ( -
- +
@@ -20,6 +19,6 @@ export function NarrativeBeatNode({ data }: NodeProps) {
{d.text || "No description."}
-
+
); } diff --git a/src/components/admin/narrative-flow/NodeHandles.tsx b/src/components/admin/narrative-flow/NodeHandles.tsx deleted file mode 100644 index 5cf33c6..0000000 --- a/src/components/admin/narrative-flow/NodeHandles.tsx +++ /dev/null @@ -1,51 +0,0 @@ -"use client"; - -import { TypedHandle } from "./TypedHandle"; -import { NODE_SOCKETS, type SocketDef } from "./types"; - -interface NodeHandlesProps { - nodeType: string; - dynamicInputs?: SocketDef[]; - dynamicOutputs?: SocketDef[]; -} - -export function NodeHandles({ nodeType, dynamicInputs, dynamicOutputs }: NodeHandlesProps) { - const config = NODE_SOCKETS[nodeType]; - if (!config) return null; - - const inputs = dynamicInputs ?? config.inputs; - const outputs = dynamicOutputs ?? config.outputs; - - const maxSlots = Math.max(inputs.length, outputs.length, 1); - - return ( - <> - {inputs.map((socket, i) => { - const topPercent = maxSlots === 1 ? 50 : (i / (maxSlots - 1)) * 80 + 10; - return ( - - ); - })} - {outputs.map((socket, i) => { - const topPercent = maxSlots === 1 ? 50 : (i / (maxSlots - 1)) * 80 + 10; - return ( - - ); - })} - - ); -} diff --git a/src/components/admin/narrative-flow/NodeShell.tsx b/src/components/admin/narrative-flow/NodeShell.tsx new file mode 100644 index 0000000..17fcc01 --- /dev/null +++ b/src/components/admin/narrative-flow/NodeShell.tsx @@ -0,0 +1,99 @@ +"use client"; + +import { Handle, Position } from "@xyflow/react"; +import { SOCKET_COLORS, NODE_SOCKETS, type SocketDef, type SocketType } from "./types"; + +function SocketHandle({ + socket, + isInput, + topPercent, +}: { + socket: SocketDef; + isInput: boolean; + topPercent: number; +}) { + const color = SOCKET_COLORS[socket.type]; + const isFlow = socket.type === "flow"; + + return ( +
+ + {socket.label && ( + + {socket.label} + + )} +
+ ); +} + +interface NodeShellProps { + nodeType: string; + dynamicInputs?: SocketDef[]; + dynamicOutputs?: SocketDef[]; + children: React.ReactNode; + className?: string; +} + +export function NodeShell({ + nodeType, + dynamicInputs, + dynamicOutputs, + children, + className = "", +}: NodeShellProps) { + const config = NODE_SOCKETS[nodeType]; + if (!config) return
{children}
; + + const inputs = dynamicInputs ?? config.inputs; + const outputs = dynamicOutputs ?? config.outputs; + const maxSlots = Math.max(inputs.length, outputs.length, 1); + + return ( +
+ {/* Input handles - positioned on left edge */} + {inputs.map((socket, i) => { + const top = maxSlots === 1 ? 50 : (i / (maxSlots - 1)) * 80 + 10; + return ( + + ); + })} + {/* Output handles - positioned on right edge */} + {outputs.map((socket, i) => { + const top = maxSlots === 1 ? 50 : (i / (maxSlots - 1)) * 80 + 10; + return ( + + ); + })} + {/* Content area with padding to avoid handle overlap */} +
{children}
+
+ ); +} diff --git a/src/components/admin/narrative-flow/NotNode.tsx b/src/components/admin/narrative-flow/NotNode.tsx index b0dd839..db94c99 100644 --- a/src/components/admin/narrative-flow/NotNode.tsx +++ b/src/components/admin/narrative-flow/NotNode.tsx @@ -2,21 +2,16 @@ import { type NodeProps } from "@xyflow/react"; import { ToggleLeftIcon } from "lucide-react"; -import { NodeHandles } from "./NodeHandles"; +import { NodeShell } from "./NodeShell"; export function NotNode(_props: NodeProps) { return ( -
- +
- - NOT - + NOT
-
- Inverts boolean -
-
+
Inverts boolean
+ ); } diff --git a/src/components/admin/narrative-flow/NumberNode.tsx b/src/components/admin/narrative-flow/NumberNode.tsx index 219bda2..12b7368 100644 --- a/src/components/admin/narrative-flow/NumberNode.tsx +++ b/src/components/admin/narrative-flow/NumberNode.tsx @@ -2,14 +2,13 @@ import { type NodeProps } from "@xyflow/react"; import { HashIcon } from "lucide-react"; -import { NodeHandles } from "./NodeHandles"; +import { NodeShell } from "./NodeShell"; export function NumberNode({ data }: NodeProps) { const d = data as { value?: number; description?: string }; return ( -
- +
@@ -19,6 +18,6 @@ export function NumberNode({ data }: NodeProps) {
{d.description || "Numeric value"}
-
+
); } diff --git a/src/components/admin/narrative-flow/OutcomeNode.tsx b/src/components/admin/narrative-flow/OutcomeNode.tsx index 6a13baa..65f259a 100644 --- a/src/components/admin/narrative-flow/OutcomeNode.tsx +++ b/src/components/admin/narrative-flow/OutcomeNode.tsx @@ -2,7 +2,7 @@ import { type NodeProps } from "@xyflow/react"; import { FlagIcon, PencilIcon } from "lucide-react"; -import { NodeHandles } from "./NodeHandles"; +import { NodeShell } from "./NodeShell"; export function OutcomeNode({ data }: NodeProps) { const d = data as { @@ -13,8 +13,7 @@ export function OutcomeNode({ data }: NodeProps) { const effects = d.effects ?? []; return ( -
- +
@@ -41,6 +40,6 @@ export function OutcomeNode({ data }: NodeProps) { )) )}
-
+ ); } diff --git a/src/components/admin/narrative-flow/RandomNode.tsx b/src/components/admin/narrative-flow/RandomNode.tsx index 5e37c0b..3f70ec7 100644 --- a/src/components/admin/narrative-flow/RandomNode.tsx +++ b/src/components/admin/narrative-flow/RandomNode.tsx @@ -2,14 +2,13 @@ import { type NodeProps } from "@xyflow/react"; import { ShuffleIcon } from "lucide-react"; -import { NodeHandles } from "./NodeHandles"; +import { NodeShell } from "./NodeShell"; export function RandomNode({ data }: NodeProps) { const d = data as { min?: number; max?: number; description?: string }; return ( -
- +
@@ -19,6 +18,6 @@ export function RandomNode({ data }: NodeProps) {
{d.description || "Random number generator"}
-
+
); } diff --git a/src/components/admin/narrative-flow/SequenceNode.tsx b/src/components/admin/narrative-flow/SequenceNode.tsx index ea0327a..2fb1273 100644 --- a/src/components/admin/narrative-flow/SequenceNode.tsx +++ b/src/components/admin/narrative-flow/SequenceNode.tsx @@ -2,23 +2,20 @@ import { type NodeProps } from "@xyflow/react"; import { ListOrderedIcon } from "lucide-react"; -import { NodeHandles } from "./NodeHandles"; +import { NodeShell } from "./NodeShell"; export function SequenceNode(_props: NodeProps) { return ( -
- +
- - Sequence - + Sequence
1 2 3
-
+ ); } diff --git a/src/components/admin/narrative-flow/StringNode.tsx b/src/components/admin/narrative-flow/StringNode.tsx index aa4d6d8..fd49a99 100644 --- a/src/components/admin/narrative-flow/StringNode.tsx +++ b/src/components/admin/narrative-flow/StringNode.tsx @@ -2,14 +2,13 @@ import { type NodeProps } from "@xyflow/react"; import { TypeIcon } from "lucide-react"; -import { NodeHandles } from "./NodeHandles"; +import { NodeShell } from "./NodeShell"; export function StringNode({ data }: NodeProps) { const d = data as { value?: string; description?: string }; return ( -
- +
@@ -19,6 +18,6 @@ export function StringNode({ data }: NodeProps) {
{d.description || "String value"}
-
+
); } diff --git a/src/components/admin/narrative-flow/TypedHandle.tsx b/src/components/admin/narrative-flow/TypedHandle.tsx deleted file mode 100644 index 0593e23..0000000 --- a/src/components/admin/narrative-flow/TypedHandle.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client"; - -import { Handle, Position } from "@xyflow/react"; -import { SOCKET_COLORS, type SocketType } from "./types"; - -interface TypedHandleProps { - id: string; - socketType: SocketType; - isInput: boolean; - label?: string; - style?: React.CSSProperties; -} - -export function TypedHandle({ id, socketType, isInput, label, style }: TypedHandleProps) { - const color = SOCKET_COLORS[socketType]; - const isFlow = socketType === "flow"; - - return ( -
- - {label && ( - - {label} - - )} -
- ); -} diff --git a/src/components/admin/narrative-flow/WaitNode.tsx b/src/components/admin/narrative-flow/WaitNode.tsx index 869d48b..3bd7b92 100644 --- a/src/components/admin/narrative-flow/WaitNode.tsx +++ b/src/components/admin/narrative-flow/WaitNode.tsx @@ -2,14 +2,13 @@ import { type NodeProps } from "@xyflow/react"; import { TimerIcon } from "lucide-react"; -import { NodeHandles } from "./NodeHandles"; +import { NodeShell } from "./NodeShell"; export function WaitNode({ data }: NodeProps) { const d = data as { ticks?: number; input?: unknown }; return ( -
- +
@@ -21,6 +20,6 @@ export function WaitNode({ data }: NodeProps) { ? `Input: ${String(d.input).substring(0, 50)}${String(d.input).length > 50 ? "..." : ""}` : "No input"}
-
+ ); }