1
0
Fork 0

style(admin): format admin narrative-flow and award components

This commit is contained in:
Jason Fraley 2026-08-16 23:27:51 -04:00
parent a21564a78d
commit 5058d9ce6c
23 changed files with 264 additions and 164 deletions

View file

@ -116,13 +116,20 @@ function MedalBody({
</defs>
{shape === "circle" && (
<circle cx={cx} cy={cy} r={r} fill={`url(#${gradId})`} stroke={finishColors[2]} strokeWidth={1.5} />
<circle
cx={cx}
cy={cy}
r={r}
fill={`url(#${gradId})`}
stroke={finishColors[2]}
strokeWidth={1.5}
/>
)}
{shape === "star" && (
<polygon
points={Array.from({ length: 10 }, (_, i) => {
const angle = (Math.PI / 2) + (i * Math.PI) / 5;
const angle = Math.PI / 2 + (i * Math.PI) / 5;
const radius = i % 2 === 0 ? r : r * 0.45;
return `${cx + radius * Math.cos(angle)},${cy - radius * Math.sin(angle)}`;
}).join(" ")}
@ -228,14 +235,7 @@ function MedalPreview({ design }: { design: MedalDesign }) {
})()}
{/* Small connector tab between ribbon and medal */}
<rect
x={medalCx - 4}
y={ribbonBottom}
width={8}
height={8}
rx={2}
fill="#9ca3af"
/>
<rect x={medalCx - 4} y={ribbonBottom} width={8} height={8} rx={2} fill="#9ca3af" />
{/* Medal body */}
<MedalBody shape={shape} finish={finish} engraving={engraving} cx={medalCx} cy={medalCy} />
@ -424,14 +424,8 @@ export default function MedalDesigner() {
(stripes: Stripe[]) => setDesign((d) => ({ ...d, ribbonStripes: stripes })),
[],
);
const setShape = useCallback(
(shape: MedalShape) => setDesign((d) => ({ ...d, shape })),
[],
);
const setFinish = useCallback(
(finish: MetalFinish) => setDesign((d) => ({ ...d, finish })),
[],
);
const setShape = useCallback((shape: MedalShape) => setDesign((d) => ({ ...d, shape })), []);
const setFinish = useCallback((finish: MetalFinish) => setDesign((d) => ({ ...d, finish })), []);
const setEngraving = useCallback(
(engraving: string) => setDesign((d) => ({ ...d, engraving })),
[],

View file

@ -79,9 +79,7 @@ function RibbonPreview({ design }: { design: RibbonDesign }) {
const { stripes, devices, mirrored } = design;
if (stripes.length === 0) return null;
const displayStripes = mirrored
? [...stripes, ...stripes.slice(0, -1).reverse()]
: stripes;
const displayStripes = mirrored ? [...stripes, ...stripes.slice(0, -1).reverse()] : stripes;
const totalWeight = displayStripes.reduce((sum, s) => sum + s.width, 0);
const viewWidth = RIBBON_WIDTH;
@ -98,16 +96,7 @@ function RibbonPreview({ design }: { design: RibbonDesign }) {
let x = 0;
return displayStripes.map((stripe, i) => {
const w = (stripe.width / totalWeight) * viewWidth;
const el = (
<rect
key={i}
x={x}
y={0}
width={w}
height={viewHeight}
fill={stripe.color}
/>
);
const el = <rect key={i} x={x} y={0} width={w} height={viewHeight} fill={stripe.color} />;
x += w;
return el;
});
@ -156,9 +145,7 @@ function StripeEditor({
return (
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-zinc-400 uppercase tracking-wider">
Stripes
</span>
<span className="text-xs font-medium text-zinc-400 uppercase tracking-wider">Stripes</span>
<button
type="button"
onClick={addStripe}
@ -222,19 +209,14 @@ function DeviceEditor({
onChange: (d: Device[]) => void;
}) {
const addDevice = (type: DeviceType) =>
onChange([
...devices,
{ id: crypto.randomUUID(), type, position: 50, positionY: 50 },
]);
onChange([...devices, { id: crypto.randomUUID(), type, position: 50, positionY: 50 }]);
const removeDevice = (id: string) => onChange(devices.filter((d) => d.id !== id));
const updateDevice = (id: string, patch: Partial<Device>) =>
onChange(devices.map((d) => (d.id === id ? { ...d, ...patch } : d)));
return (
<div className="flex flex-col gap-2">
<span className="text-xs font-medium text-zinc-400 uppercase tracking-wider">
Devices
</span>
<span className="text-xs font-medium text-zinc-400 uppercase tracking-wider">Devices</span>
<div className="flex flex-wrap gap-1.5">
{(Object.keys(DEVICE_INFO) as DeviceType[]).map((type) => {
const info = DEVICE_INFO[type];
@ -268,9 +250,7 @@ function DeviceEditor({
min={0}
max={100}
value={device.position}
onChange={(e) =>
updateDevice(device.id, { position: Number(e.target.value) })
}
onChange={(e) => updateDevice(device.id, { position: Number(e.target.value) })}
className="flex-1 h-1 accent-zinc-400"
/>
<span className="text-[10px] text-zinc-500 font-mono w-5 text-right">
@ -284,9 +264,7 @@ function DeviceEditor({
min={0}
max={100}
value={device.positionY}
onChange={(e) =>
updateDevice(device.id, { positionY: Number(e.target.value) })
}
onChange={(e) => updateDevice(device.id, { positionY: Number(e.target.value) })}
className="flex-1 h-1 accent-zinc-400"
/>
<span className="text-[10px] text-zinc-500 font-mono w-5 text-right">
@ -330,18 +308,9 @@ export default function RibbonDesigner() {
}
}, [design, setValue]);
const setStripes = useCallback(
(stripes: Stripe[]) => setDesign((d) => ({ ...d, stripes })),
[],
);
const setDevices = useCallback(
(devices: Device[]) => setDesign((d) => ({ ...d, devices })),
[],
);
const toggleMirror = useCallback(
() => setDesign((d) => ({ ...d, mirrored: !d.mirrored })),
[],
);
const setStripes = useCallback((stripes: Stripe[]) => setDesign((d) => ({ ...d, stripes })), []);
const setDevices = useCallback((devices: Device[]) => setDesign((d) => ({ ...d, devices })), []);
const toggleMirror = useCallback(() => setDesign((d) => ({ ...d, mirrored: !d.mirrored })), []);
return (
<div className="field-type json flex flex-col gap-3">

View file

@ -8,7 +8,10 @@ export function BooleanNode({ data }: NodeProps) {
const d = data as { value?: boolean; description?: string };
return (
<NodeShell nodeType="boolean" className="bg-emerald-950/80 border border-emerald-500/50 rounded-lg shadow-md min-w-28">
<NodeShell
nodeType="boolean"
className="bg-emerald-950/80 border border-emerald-500/50 rounded-lg shadow-md min-w-28"
>
<div className="flex items-center gap-2 px-3 py-2 border-b border-emerald-500/30">
<CheckCircleIcon className="size-4 text-emerald-400 shrink-0" />
<span className="text-sm font-medium text-emerald-200 flex-1 truncate">

View file

@ -6,7 +6,10 @@ import { NodeShell } from "./NodeShell";
export function BranchNode(_props: NodeProps) {
return (
<NodeShell nodeType="branch" className="bg-rose-950/80 border border-rose-500/50 rounded-lg shadow-md min-w-36">
<NodeShell
nodeType="branch"
className="bg-rose-950/80 border border-rose-500/50 rounded-lg shadow-md min-w-36"
>
<div className="flex items-center gap-2 px-3 py-2 border-b border-rose-500/30">
<GitBranchIcon className="size-4 text-rose-400 shrink-0" />
<span className="text-sm font-medium text-rose-200 flex-1">Branch</span>

View file

@ -9,7 +9,10 @@ export function ChoiceNode({ data }: NodeProps) {
const options = d.options ?? [];
return (
<NodeShell nodeType="choice" className="bg-amber-950/80 border border-amber-500/50 rounded-lg shadow-md min-w-56">
<NodeShell
nodeType="choice"
className="bg-amber-950/80 border border-amber-500/50 rounded-lg shadow-md min-w-56"
>
<div className="flex items-center gap-2 px-3 py-2 border-b border-amber-500/30">
<WaypointsIcon className="size-4 text-amber-400 shrink-0" />
<span className="text-sm font-medium text-amber-200 flex-1 truncate">

View file

@ -9,7 +9,10 @@ export function CompareNode({ data }: NodeProps) {
const op = d.operator ?? ">";
return (
<NodeShell nodeType="compare" className="bg-slate-950/80 border border-slate-500/50 rounded-lg shadow-md min-w-32">
<NodeShell
nodeType="compare"
className="bg-slate-950/80 border border-slate-500/50 rounded-lg shadow-md min-w-32"
>
<div className="flex items-center gap-2 px-3 py-2 border-b border-slate-500/30">
<EqualIcon className="size-4 text-slate-400 shrink-0" />
<span className="text-sm font-medium text-slate-200 flex-1">Compare</span>

View file

@ -8,7 +8,10 @@ export function DateNode({ data }: NodeProps) {
const d = data as { value?: string; description?: string };
return (
<NodeShell nodeType="date" className="bg-cyan-950/80 border border-cyan-500/50 rounded-lg shadow-md min-w-32">
<NodeShell
nodeType="date"
className="bg-cyan-950/80 border border-cyan-500/50 rounded-lg shadow-md min-w-32"
>
<div className="flex items-center gap-2 px-3 py-2 border-b border-cyan-500/30">
<CalendarIcon className="size-4 text-cyan-400 shrink-0" />
<span className="text-sm font-medium text-cyan-200 flex-1 truncate">

View file

@ -8,12 +8,13 @@ export function FlagNode({ data }: NodeProps) {
const d = data as { flagName?: string };
return (
<NodeShell nodeType="flag" className="bg-orange-950/80 border border-orange-500/50 rounded-lg shadow-md min-w-40">
<NodeShell
nodeType="flag"
className="bg-orange-950/80 border border-orange-500/50 rounded-lg shadow-md min-w-40"
>
<div className="flex items-center gap-2 px-3 py-2 border-b border-orange-500/30">
<FlagIcon className="size-4 text-orange-400 shrink-0" />
<span className="text-sm font-medium text-orange-200 flex-1 truncate">
Flag
</span>
<span className="text-sm font-medium text-orange-200 flex-1 truncate">Flag</span>
</div>
<div className="px-3 py-2 text-xs text-orange-300/80 truncate">
{d.flagName || "Unnamed flag"}

View file

@ -25,7 +25,11 @@ export function FormatNode({ data }: NodeProps) {
];
return (
<NodeShell nodeType="format" dynamicInputs={dynamicInputs} className="bg-sky-950/80 border border-sky-500/50 rounded-lg shadow-md min-w-40">
<NodeShell
nodeType="format"
dynamicInputs={dynamicInputs}
className="bg-sky-950/80 border border-sky-500/50 rounded-lg shadow-md min-w-40"
>
<div className="flex items-center gap-2 px-3 py-2 border-b border-sky-500/30">
<AlignLeftIcon className="size-4 text-sky-400 shrink-0" />
<span className="text-sm font-medium text-sky-200 flex-1">Format</span>

View file

@ -20,16 +20,15 @@ export function GameDataNode({ data }: NodeProps) {
const fieldLabel = GAME_DATA_FIELDS.find((f) => f.value === d.field)?.label ?? "Select field";
return (
<NodeShell nodeType="gameData" className="bg-indigo-950/80 border border-indigo-500/50 rounded-lg shadow-md min-w-44">
<NodeShell
nodeType="gameData"
className="bg-indigo-950/80 border border-indigo-500/50 rounded-lg shadow-md min-w-44"
>
<div className="flex items-center gap-2 px-3 py-2 border-b border-indigo-500/30">
<DatabaseIcon className="size-4 text-indigo-400 shrink-0" />
<span className="text-sm font-medium text-indigo-200 flex-1 truncate">
Game Data
</span>
</div>
<div className="px-3 py-2 text-xs text-indigo-300/80">
{fieldLabel}
<span className="text-sm font-medium text-indigo-200 flex-1 truncate">Game Data</span>
</div>
<div className="px-3 py-2 text-xs text-indigo-300/80">{fieldLabel}</div>
</NodeShell>
);
}

View file

@ -9,7 +9,10 @@ export function LogicNode({ data }: NodeProps) {
const op = d.operator ?? "AND";
return (
<NodeShell nodeType="logic" className="bg-lime-950/80 border border-lime-500/50 rounded-lg shadow-md min-w-32">
<NodeShell
nodeType="logic"
className="bg-lime-950/80 border border-lime-500/50 rounded-lg shadow-md min-w-32"
>
<div className="flex items-center gap-2 px-3 py-2 border-b border-lime-500/30">
<BinaryIcon className="size-4 text-lime-400 shrink-0" />
<span className="text-sm font-medium text-lime-200 flex-1">Logic</span>

View file

@ -9,7 +9,10 @@ export function MathNode({ data }: NodeProps) {
const op = d.operator ?? "+";
return (
<NodeShell nodeType="math" className="bg-violet-950/80 border border-violet-500/50 rounded-lg shadow-md min-w-32">
<NodeShell
nodeType="math"
className="bg-violet-950/80 border border-violet-500/50 rounded-lg shadow-md min-w-32"
>
<div className="flex items-center gap-2 px-3 py-2 border-b border-violet-500/30">
<CalculatorIcon className="size-4 text-violet-400 shrink-0" />
<span className="text-sm font-medium text-violet-200 flex-1">Math</span>

View file

@ -8,7 +8,10 @@ export function NarrativeBeatNode({ data }: NodeProps) {
const d = data as { title?: string; text?: string };
return (
<NodeShell nodeType="narrativeBeat" className="bg-blue-950/80 border border-blue-500/50 rounded-lg shadow-md min-w-56">
<NodeShell
nodeType="narrativeBeat"
className="bg-blue-950/80 border border-blue-500/50 rounded-lg shadow-md min-w-56"
>
<div className="flex items-center gap-2 px-3 py-2 border-b border-blue-500/30">
<BookOpenTextIcon className="size-4 text-blue-400 shrink-0" />
<span className="text-sm font-medium text-blue-200 flex-1 truncate">

View file

@ -2,16 +2,16 @@
import { useCallback, useEffect, useRef, useState } from "react";
import {
ReactFlow,
Background,
Controls,
MiniMap,
useNodesState,
useEdgesState,
addEdge,
type Node,
type Edge,
Background,
type Connection,
Controls,
type Edge,
MiniMap,
type Node,
ReactFlow,
useEdgesState,
useNodesState,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";
@ -38,7 +38,7 @@ import { NotNode } from "./NotNode";
import { BranchNode } from "./BranchNode";
import { SequenceNode } from "./SequenceNode";
import { NodeEditDialog } from "./NodeEditDialog";
import { NODE_SOCKETS, type SocketType } from "./types";
import { NODE_SOCKETS } from "./types";
const nodeTypes = {
narrativeBeat: NarrativeBeatNode,
@ -106,7 +106,12 @@ export function NarrativeFlowEditor() {
const isValidConnection = useCallback(
(connection: Edge | Connection) => {
if (!connection.source || !connection.target || !connection.sourceHandle || !connection.targetHandle) {
if (
!connection.source ||
!connection.target ||
!connection.sourceHandle ||
!connection.targetHandle
) {
return false;
}
const sourceNode = nodes.find((n) => n.id === connection.source);
@ -121,8 +126,16 @@ export function NarrativeFlowEditor() {
const targetSocket = targetSockets.inputs.find((s) => s.id === connection.targetHandle);
// Dynamic handles (arg-*, option-*) are treated as "any"
const sourceType = sourceSocket?.type ?? (connection.sourceHandle.startsWith("arg-") || connection.sourceHandle.startsWith("option-") ? "any" : null);
const targetType = targetSocket?.type ?? (connection.targetHandle.startsWith("arg-") || connection.targetHandle.startsWith("option-") ? "any" : null);
const sourceType =
sourceSocket?.type ??
(connection.sourceHandle.startsWith("arg-") || connection.sourceHandle.startsWith("option-")
? "any"
: null);
const targetType =
targetSocket?.type ??
(connection.targetHandle.startsWith("arg-") || connection.targetHandle.startsWith("option-")
? "any"
: null);
if (sourceType === null || targetType === null) return false;
if (sourceType === "any" || targetType === "any") return true;
@ -135,21 +148,15 @@ export function NarrativeFlowEditor() {
setEditingNodeId(node.id);
}, []);
const onNodeContextMenu = useCallback(
(e: React.MouseEvent, node: Node) => {
e.preventDefault();
setContextMenu({ x: e.clientX, y: e.clientY, nodeId: node.id });
},
[],
);
const onNodeContextMenu = useCallback((e: React.MouseEvent, node: Node) => {
e.preventDefault();
setContextMenu({ x: e.clientX, y: e.clientY, nodeId: node.id });
}, []);
const onEdgeContextMenu = useCallback(
(e: React.MouseEvent, edge: Edge) => {
e.preventDefault();
setContextMenu({ x: e.clientX, y: e.clientY, edgeId: edge.id });
},
[],
);
const onEdgeContextMenu = useCallback((e: React.MouseEvent, edge: Edge) => {
e.preventDefault();
setContextMenu({ x: e.clientX, y: e.clientY, edgeId: edge.id });
}, []);
const onPaneContextMenu = useCallback((e: MouseEvent | React.MouseEvent) => {
e.preventDefault();
@ -178,7 +185,7 @@ export function NarrativeFlowEditor() {
[setNodes],
);
const selectedNode = editingNodeId ? nodes.find((n) => n.id === editingNodeId) ?? null : null;
const selectedNode = editingNodeId ? (nodes.find((n) => n.id === editingNodeId) ?? null) : null;
const addNode = useCallback(
(
@ -240,32 +247,129 @@ export function NarrativeFlowEditor() {
const toolbarButtons = (
<div className="flex flex-wrap items-center gap-1 px-2 py-1.5 bg-zinc-900 rounded-t-md border border-zinc-700">
<span className="text-[9px] uppercase tracking-wider text-zinc-500 font-semibold mr-1">Story</span>
<button className={`${buttonClass} bg-blue-950 border-blue-700 text-blue-200 hover:bg-blue-900`} onClick={() => addNode("narrativeBeat")}>Beat</button>
<button className={`${buttonClass} bg-amber-950 border-amber-700 text-amber-200 hover:bg-amber-900`} onClick={() => addNode("choice")}>Choice</button>
<button className={`${buttonClass} bg-emerald-950 border-emerald-700 text-emerald-200 hover:bg-emerald-900`} onClick={() => addNode("outcome")}>Outcome</button>
<button className={`${buttonClass} bg-yellow-950 border-yellow-700 text-yellow-200 hover:bg-yellow-900`} onClick={() => addNode("wait")}>Wait</button>
<span className="text-[9px] uppercase tracking-wider text-zinc-500 font-semibold mr-1">
Story
</span>
<button
className={`${buttonClass} bg-blue-950 border-blue-700 text-blue-200 hover:bg-blue-900`}
onClick={() => addNode("narrativeBeat")}
>
Beat
</button>
<button
className={`${buttonClass} bg-amber-950 border-amber-700 text-amber-200 hover:bg-amber-900`}
onClick={() => addNode("choice")}
>
Choice
</button>
<button
className={`${buttonClass} bg-emerald-950 border-emerald-700 text-emerald-200 hover:bg-emerald-900`}
onClick={() => addNode("outcome")}
>
Outcome
</button>
<button
className={`${buttonClass} bg-yellow-950 border-yellow-700 text-yellow-200 hover:bg-yellow-900`}
onClick={() => addNode("wait")}
>
Wait
</button>
<span className="text-[9px] uppercase tracking-wider text-zinc-500 font-semibold ml-2 mr-1">Data</span>
<button className={`${buttonClass} bg-purple-950 border-purple-700 text-purple-200 hover:bg-purple-900`} onClick={() => addNode("number")}>Number</button>
<button className={`${buttonClass} bg-teal-950 border-teal-700 text-teal-200 hover:bg-teal-900`} onClick={() => addNode("string")}>String</button>
<button className={`${buttonClass} bg-emerald-950/30 border-emerald-700/50 text-emerald-200 hover:bg-emerald-900/40`} onClick={() => addNode("boolean")}>Bool</button>
<button className={`${buttonClass} bg-cyan-950 border-cyan-700 text-cyan-200 hover:bg-cyan-900`} onClick={() => addNode("date")}>Date</button>
<button className={`${buttonClass} bg-pink-950 border-pink-700 text-pink-200 hover:bg-pink-900`} onClick={() => addNode("random")}>Random</button>
<button className={`${buttonClass} bg-indigo-950 border-indigo-700 text-indigo-200 hover:bg-indigo-900`} onClick={() => addNode("gameData")}>GameData</button>
<button className={`${buttonClass} bg-orange-950 border-orange-700 text-orange-200 hover:bg-orange-900`} onClick={() => addNode("flag")}>Flag</button>
<span className="text-[9px] uppercase tracking-wider text-zinc-500 font-semibold ml-2 mr-1">
Data
</span>
<button
className={`${buttonClass} bg-purple-950 border-purple-700 text-purple-200 hover:bg-purple-900`}
onClick={() => addNode("number")}
>
Number
</button>
<button
className={`${buttonClass} bg-teal-950 border-teal-700 text-teal-200 hover:bg-teal-900`}
onClick={() => addNode("string")}
>
String
</button>
<button
className={`${buttonClass} bg-emerald-950/30 border-emerald-700/50 text-emerald-200 hover:bg-emerald-900/40`}
onClick={() => addNode("boolean")}
>
Bool
</button>
<button
className={`${buttonClass} bg-cyan-950 border-cyan-700 text-cyan-200 hover:bg-cyan-900`}
onClick={() => addNode("date")}
>
Date
</button>
<button
className={`${buttonClass} bg-pink-950 border-pink-700 text-pink-200 hover:bg-pink-900`}
onClick={() => addNode("random")}
>
Random
</button>
<button
className={`${buttonClass} bg-indigo-950 border-indigo-700 text-indigo-200 hover:bg-indigo-900`}
onClick={() => addNode("gameData")}
>
GameData
</button>
<button
className={`${buttonClass} bg-orange-950 border-orange-700 text-orange-200 hover:bg-orange-900`}
onClick={() => addNode("flag")}
>
Flag
</button>
<span className="text-[9px] uppercase tracking-wider text-zinc-500 font-semibold ml-2 mr-1">Logic</span>
<button className={`${buttonClass} bg-slate-950 border-slate-700 text-slate-200 hover:bg-slate-900`} onClick={() => addNode("compare")}>Compare</button>
<button className={`${buttonClass} bg-violet-950 border-violet-700 text-violet-200 hover:bg-violet-900`} onClick={() => addNode("math")}>Math</button>
<button className={`${buttonClass} bg-sky-950 border-sky-700 text-sky-200 hover:bg-sky-900`} onClick={() => addNode("format")}>Format</button>
<button className={`${buttonClass} bg-lime-950 border-lime-700 text-lime-200 hover:bg-lime-900`} onClick={() => addNode("logic")}>Logic</button>
<button className={`${buttonClass} bg-red-950 border-red-700 text-red-200 hover:bg-red-900`} onClick={() => addNode("not")}>NOT</button>
<button className={`${buttonClass} bg-rose-950 border-rose-700 text-rose-200 hover:bg-rose-900`} onClick={() => addNode("branch")}>Branch</button>
<button className={`${buttonClass} bg-fuchsia-950 border-fuchsia-700 text-fuchsia-200 hover:bg-fuchsia-900`} onClick={() => addNode("sequence")}>Seq</button>
<span className="text-[9px] uppercase tracking-wider text-zinc-500 font-semibold ml-2 mr-1">
Logic
</span>
<button
className={`${buttonClass} bg-slate-950 border-slate-700 text-slate-200 hover:bg-slate-900`}
onClick={() => addNode("compare")}
>
Compare
</button>
<button
className={`${buttonClass} bg-violet-950 border-violet-700 text-violet-200 hover:bg-violet-900`}
onClick={() => addNode("math")}
>
Math
</button>
<button
className={`${buttonClass} bg-sky-950 border-sky-700 text-sky-200 hover:bg-sky-900`}
onClick={() => addNode("format")}
>
Format
</button>
<button
className={`${buttonClass} bg-lime-950 border-lime-700 text-lime-200 hover:bg-lime-900`}
onClick={() => addNode("logic")}
>
Logic
</button>
<button
className={`${buttonClass} bg-red-950 border-red-700 text-red-200 hover:bg-red-900`}
onClick={() => addNode("not")}
>
NOT
</button>
<button
className={`${buttonClass} bg-rose-950 border-rose-700 text-rose-200 hover:bg-rose-900`}
onClick={() => addNode("branch")}
>
Branch
</button>
<button
className={`${buttonClass} bg-fuchsia-950 border-fuchsia-700 text-fuchsia-200 hover:bg-fuchsia-900`}
onClick={() => addNode("sequence")}
>
Seq
</button>
<span className="ml-auto text-[10px] text-zinc-500">
{nodes.length} node{nodes.length !== 1 ? "s" : ""}, {edges.length} edge{edges.length !== 1 ? "s" : ""}
{nodes.length} node{nodes.length !== 1 ? "s" : ""}, {edges.length} edge
{edges.length !== 1 ? "s" : ""}
</span>
</div>
);

View file

@ -1,12 +1,12 @@
"use client";
import { useEffect, useState, type FC } from "react";
import { type FC, useEffect, useState } from "react";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@ -22,12 +22,7 @@ interface NodeEditDialogProps {
onSave: (nodeId: string, data: Record<string, unknown>) => void;
}
export const NodeEditDialog: FC<NodeEditDialogProps> = ({
node,
open,
onOpenChange,
onSave,
}) => {
export const NodeEditDialog: FC<NodeEditDialogProps> = ({ node, open, onOpenChange, onSave }) => {
const [formData, setFormData] = useState<Record<string, unknown>>({});
useEffect(() => {
@ -326,10 +321,7 @@ export const NodeEditDialog: FC<NodeEditDialogProps> = ({
variant="outline"
size="sm"
onClick={() => {
const updated = [
...options,
{ id: crypto.randomUUID(), label: "" },
];
const updated = [...options, { id: crypto.randomUUID(), label: "" }];
updateField("options", updated);
}}
className="dark:border-zinc-700 dark:text-zinc-300"
@ -584,11 +576,7 @@ export const NodeEditDialog: FC<NodeEditDialogProps> = ({
case "not":
case "branch":
case "sequence": {
return (
<p className="text-sm text-muted-foreground">
No configurable properties.
</p>
);
return <p className="text-sm text-muted-foreground">No configurable properties.</p>;
}
default:
@ -620,7 +608,10 @@ export const NodeEditDialog: FC<NodeEditDialogProps> = ({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg dark:bg-zinc-900 dark:text-zinc-100 dark:border-zinc-700 bg-white text-zinc-900" showCloseButton={false}>
<DialogContent
className="max-w-lg dark:bg-zinc-900 dark:text-zinc-100 dark:border-zinc-700 bg-white text-zinc-900"
showCloseButton={false}
>
<DialogHeader>
<DialogTitle className="flex items-center justify-between">
<span>Edit {label}</span>
@ -642,10 +633,7 @@ export const NodeEditDialog: FC<NodeEditDialogProps> = ({
>
Cancel
</Button>
<Button
onClick={handleSave}
className="bg-blue-600 hover:bg-blue-700 text-white"
>
<Button onClick={handleSave} className="bg-blue-600 hover:bg-blue-700 text-white">
Save
</Button>
</DialogFooter>

View file

@ -1,7 +1,7 @@
"use client";
import { Handle, Position } from "@xyflow/react";
import { SOCKET_COLORS, NODE_SOCKETS, type SocketDef, type SocketType } from "./types";
import { NODE_SOCKETS, SOCKET_COLORS, type SocketDef } from "./types";
function SocketHandle({
socket,
@ -81,16 +81,12 @@ export function NodeShell({
{/* Input handles - positioned on left edge */}
{inputs.map((socket, i) => {
const top = maxSlots === 1 ? 50 : (i / (maxSlots - 1)) * 80 + 10;
return (
<SocketHandle key={socket.id} socket={socket} isInput={true} topPercent={top} />
);
return <SocketHandle key={socket.id} socket={socket} isInput={true} topPercent={top} />;
})}
{/* Output handles - positioned on right edge */}
{outputs.map((socket, i) => {
const top = maxSlots === 1 ? 50 : (i / (maxSlots - 1)) * 80 + 10;
return (
<SocketHandle key={socket.id} socket={socket} isInput={false} topPercent={top} />
);
return <SocketHandle key={socket.id} socket={socket} isInput={false} topPercent={top} />;
})}
{/* Content area with padding to avoid handle overlap */}
<div className="px-8">{children}</div>

View file

@ -6,7 +6,10 @@ import { NodeShell } from "./NodeShell";
export function NotNode(_props: NodeProps) {
return (
<NodeShell nodeType="not" className="bg-red-950/80 border border-red-500/50 rounded-lg shadow-md min-w-28">
<NodeShell
nodeType="not"
className="bg-red-950/80 border border-red-500/50 rounded-lg shadow-md min-w-28"
>
<div className="flex items-center gap-2 px-3 py-2 border-b border-red-500/30">
<ToggleLeftIcon className="size-4 text-red-400 shrink-0" />
<span className="text-sm font-medium text-red-200 flex-1">NOT</span>

View file

@ -8,7 +8,10 @@ export function NumberNode({ data }: NodeProps) {
const d = data as { value?: number; description?: string };
return (
<NodeShell nodeType="number" className="bg-purple-950/80 border border-purple-500/50 rounded-lg shadow-md min-w-32">
<NodeShell
nodeType="number"
className="bg-purple-950/80 border border-purple-500/50 rounded-lg shadow-md min-w-32"
>
<div className="flex items-center gap-2 px-3 py-2 border-b border-purple-500/30">
<HashIcon className="size-4 text-purple-400 shrink-0" />
<span className="text-sm font-medium text-purple-200 flex-1 truncate">

View file

@ -13,7 +13,10 @@ export function OutcomeNode({ data }: NodeProps) {
const effects = d.effects ?? [];
return (
<NodeShell nodeType="outcome" className="bg-emerald-950/80 border border-emerald-500/50 rounded-lg shadow-md min-w-56">
<NodeShell
nodeType="outcome"
className="bg-emerald-950/80 border border-emerald-500/50 rounded-lg shadow-md min-w-56"
>
<div className="flex items-center gap-2 px-3 py-2 border-b border-emerald-500/30">
<FlagIcon className="size-4 text-emerald-400 shrink-0" />
<span className="text-sm font-medium text-emerald-200 flex-1 truncate">

View file

@ -8,7 +8,10 @@ export function RandomNode({ data }: NodeProps) {
const d = data as { min?: number; max?: number; description?: string };
return (
<NodeShell nodeType="random" className="bg-pink-950/80 border border-pink-500/50 rounded-lg shadow-md min-w-32">
<NodeShell
nodeType="random"
className="bg-pink-950/80 border border-pink-500/50 rounded-lg shadow-md min-w-32"
>
<div className="flex items-center gap-2 px-3 py-2 border-b border-pink-500/30">
<ShuffleIcon className="size-4 text-pink-400 shrink-0" />
<span className="text-sm font-medium text-pink-200 flex-1 truncate">

View file

@ -6,7 +6,10 @@ import { NodeShell } from "./NodeShell";
export function SequenceNode(_props: NodeProps) {
return (
<NodeShell nodeType="sequence" className="bg-fuchsia-950/80 border border-fuchsia-500/50 rounded-lg shadow-md min-w-32">
<NodeShell
nodeType="sequence"
className="bg-fuchsia-950/80 border border-fuchsia-500/50 rounded-lg shadow-md min-w-32"
>
<div className="flex items-center gap-2 px-3 py-2 border-b border-fuchsia-500/30">
<ListOrderedIcon className="size-4 text-fuchsia-400 shrink-0" />
<span className="text-sm font-medium text-fuchsia-200 flex-1">Sequence</span>

View file

@ -8,7 +8,10 @@ export function StringNode({ data }: NodeProps) {
const d = data as { value?: string; description?: string };
return (
<NodeShell nodeType="string" className="bg-teal-950/80 border border-teal-500/50 rounded-lg shadow-md min-w-32">
<NodeShell
nodeType="string"
className="bg-teal-950/80 border border-teal-500/50 rounded-lg shadow-md min-w-32"
>
<div className="flex items-center gap-2 px-3 py-2 border-b border-teal-500/30">
<TypeIcon className="size-4 text-teal-400 shrink-0" />
<span className="text-sm font-medium text-teal-200 flex-1 truncate">

View file

@ -8,7 +8,10 @@ export function WaitNode({ data }: NodeProps) {
const d = data as { ticks?: number; input?: unknown };
return (
<NodeShell nodeType="wait" className="bg-yellow-950/80 border border-yellow-500/50 rounded-lg shadow-md min-w-48">
<NodeShell
nodeType="wait"
className="bg-yellow-950/80 border border-yellow-500/50 rounded-lg shadow-md min-w-48"
>
<div className="flex items-center gap-2 px-3 py-2 border-b border-yellow-500/30">
<TimerIcon className="size-4 text-yellow-400 shrink-0" />
<span className="text-sm font-medium text-yellow-200 flex-1 truncate">