style(admin): format admin narrative-flow and award components
This commit is contained in:
parent
a21564a78d
commit
5058d9ce6c
23 changed files with 264 additions and 164 deletions
|
|
@ -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 })),
|
||||
[],
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
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) => {
|
||||
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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in a new issue