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>
|
</defs>
|
||||||
|
|
||||||
{shape === "circle" && (
|
{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" && (
|
{shape === "star" && (
|
||||||
<polygon
|
<polygon
|
||||||
points={Array.from({ length: 10 }, (_, i) => {
|
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;
|
const radius = i % 2 === 0 ? r : r * 0.45;
|
||||||
return `${cx + radius * Math.cos(angle)},${cy - radius * Math.sin(angle)}`;
|
return `${cx + radius * Math.cos(angle)},${cy - radius * Math.sin(angle)}`;
|
||||||
}).join(" ")}
|
}).join(" ")}
|
||||||
|
|
@ -228,14 +235,7 @@ function MedalPreview({ design }: { design: MedalDesign }) {
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
{/* Small connector tab between ribbon and medal */}
|
{/* Small connector tab between ribbon and medal */}
|
||||||
<rect
|
<rect x={medalCx - 4} y={ribbonBottom} width={8} height={8} rx={2} fill="#9ca3af" />
|
||||||
x={medalCx - 4}
|
|
||||||
y={ribbonBottom}
|
|
||||||
width={8}
|
|
||||||
height={8}
|
|
||||||
rx={2}
|
|
||||||
fill="#9ca3af"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Medal body */}
|
{/* Medal body */}
|
||||||
<MedalBody shape={shape} finish={finish} engraving={engraving} cx={medalCx} cy={medalCy} />
|
<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 })),
|
(stripes: Stripe[]) => setDesign((d) => ({ ...d, ribbonStripes: stripes })),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
const setShape = useCallback(
|
const setShape = useCallback((shape: MedalShape) => setDesign((d) => ({ ...d, shape })), []);
|
||||||
(shape: MedalShape) => setDesign((d) => ({ ...d, shape })),
|
const setFinish = useCallback((finish: MetalFinish) => setDesign((d) => ({ ...d, finish })), []);
|
||||||
[],
|
|
||||||
);
|
|
||||||
const setFinish = useCallback(
|
|
||||||
(finish: MetalFinish) => setDesign((d) => ({ ...d, finish })),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
const setEngraving = useCallback(
|
const setEngraving = useCallback(
|
||||||
(engraving: string) => setDesign((d) => ({ ...d, engraving })),
|
(engraving: string) => setDesign((d) => ({ ...d, engraving })),
|
||||||
[],
|
[],
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,7 @@ function RibbonPreview({ design }: { design: RibbonDesign }) {
|
||||||
const { stripes, devices, mirrored } = design;
|
const { stripes, devices, mirrored } = design;
|
||||||
if (stripes.length === 0) return null;
|
if (stripes.length === 0) return null;
|
||||||
|
|
||||||
const displayStripes = mirrored
|
const displayStripes = mirrored ? [...stripes, ...stripes.slice(0, -1).reverse()] : stripes;
|
||||||
? [...stripes, ...stripes.slice(0, -1).reverse()]
|
|
||||||
: stripes;
|
|
||||||
|
|
||||||
const totalWeight = displayStripes.reduce((sum, s) => sum + s.width, 0);
|
const totalWeight = displayStripes.reduce((sum, s) => sum + s.width, 0);
|
||||||
const viewWidth = RIBBON_WIDTH;
|
const viewWidth = RIBBON_WIDTH;
|
||||||
|
|
@ -98,16 +96,7 @@ function RibbonPreview({ design }: { design: RibbonDesign }) {
|
||||||
let x = 0;
|
let x = 0;
|
||||||
return displayStripes.map((stripe, i) => {
|
return displayStripes.map((stripe, i) => {
|
||||||
const w = (stripe.width / totalWeight) * viewWidth;
|
const w = (stripe.width / totalWeight) * viewWidth;
|
||||||
const el = (
|
const el = <rect key={i} x={x} y={0} width={w} height={viewHeight} fill={stripe.color} />;
|
||||||
<rect
|
|
||||||
key={i}
|
|
||||||
x={x}
|
|
||||||
y={0}
|
|
||||||
width={w}
|
|
||||||
height={viewHeight}
|
|
||||||
fill={stripe.color}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
x += w;
|
x += w;
|
||||||
return el;
|
return el;
|
||||||
});
|
});
|
||||||
|
|
@ -156,9 +145,7 @@ function StripeEditor({
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs font-medium text-zinc-400 uppercase tracking-wider">
|
<span className="text-xs font-medium text-zinc-400 uppercase tracking-wider">Stripes</span>
|
||||||
Stripes
|
|
||||||
</span>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={addStripe}
|
onClick={addStripe}
|
||||||
|
|
@ -222,19 +209,14 @@ function DeviceEditor({
|
||||||
onChange: (d: Device[]) => void;
|
onChange: (d: Device[]) => void;
|
||||||
}) {
|
}) {
|
||||||
const addDevice = (type: DeviceType) =>
|
const addDevice = (type: DeviceType) =>
|
||||||
onChange([
|
onChange([...devices, { id: crypto.randomUUID(), type, position: 50, positionY: 50 }]);
|
||||||
...devices,
|
|
||||||
{ id: crypto.randomUUID(), type, position: 50, positionY: 50 },
|
|
||||||
]);
|
|
||||||
const removeDevice = (id: string) => onChange(devices.filter((d) => d.id !== id));
|
const removeDevice = (id: string) => onChange(devices.filter((d) => d.id !== id));
|
||||||
const updateDevice = (id: string, patch: Partial<Device>) =>
|
const updateDevice = (id: string, patch: Partial<Device>) =>
|
||||||
onChange(devices.map((d) => (d.id === id ? { ...d, ...patch } : d)));
|
onChange(devices.map((d) => (d.id === id ? { ...d, ...patch } : d)));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<span className="text-xs font-medium text-zinc-400 uppercase tracking-wider">
|
<span className="text-xs font-medium text-zinc-400 uppercase tracking-wider">Devices</span>
|
||||||
Devices
|
|
||||||
</span>
|
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{(Object.keys(DEVICE_INFO) as DeviceType[]).map((type) => {
|
{(Object.keys(DEVICE_INFO) as DeviceType[]).map((type) => {
|
||||||
const info = DEVICE_INFO[type];
|
const info = DEVICE_INFO[type];
|
||||||
|
|
@ -268,9 +250,7 @@ function DeviceEditor({
|
||||||
min={0}
|
min={0}
|
||||||
max={100}
|
max={100}
|
||||||
value={device.position}
|
value={device.position}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateDevice(device.id, { position: Number(e.target.value) })}
|
||||||
updateDevice(device.id, { position: Number(e.target.value) })
|
|
||||||
}
|
|
||||||
className="flex-1 h-1 accent-zinc-400"
|
className="flex-1 h-1 accent-zinc-400"
|
||||||
/>
|
/>
|
||||||
<span className="text-[10px] text-zinc-500 font-mono w-5 text-right">
|
<span className="text-[10px] text-zinc-500 font-mono w-5 text-right">
|
||||||
|
|
@ -284,9 +264,7 @@ function DeviceEditor({
|
||||||
min={0}
|
min={0}
|
||||||
max={100}
|
max={100}
|
||||||
value={device.positionY}
|
value={device.positionY}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateDevice(device.id, { positionY: Number(e.target.value) })}
|
||||||
updateDevice(device.id, { positionY: Number(e.target.value) })
|
|
||||||
}
|
|
||||||
className="flex-1 h-1 accent-zinc-400"
|
className="flex-1 h-1 accent-zinc-400"
|
||||||
/>
|
/>
|
||||||
<span className="text-[10px] text-zinc-500 font-mono w-5 text-right">
|
<span className="text-[10px] text-zinc-500 font-mono w-5 text-right">
|
||||||
|
|
@ -330,18 +308,9 @@ export default function RibbonDesigner() {
|
||||||
}
|
}
|
||||||
}, [design, setValue]);
|
}, [design, setValue]);
|
||||||
|
|
||||||
const setStripes = useCallback(
|
const setStripes = useCallback((stripes: Stripe[]) => setDesign((d) => ({ ...d, stripes })), []);
|
||||||
(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 setDevices = useCallback(
|
|
||||||
(devices: Device[]) => setDesign((d) => ({ ...d, devices })),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
const toggleMirror = useCallback(
|
|
||||||
() => setDesign((d) => ({ ...d, mirrored: !d.mirrored })),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="field-type json flex flex-col gap-3">
|
<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 };
|
const d = data as { value?: boolean; description?: string };
|
||||||
|
|
||||||
return (
|
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">
|
<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" />
|
<CheckCircleIcon className="size-4 text-emerald-400 shrink-0" />
|
||||||
<span className="text-sm font-medium text-emerald-200 flex-1 truncate">
|
<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) {
|
export function BranchNode(_props: NodeProps) {
|
||||||
return (
|
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">
|
<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" />
|
<GitBranchIcon className="size-4 text-rose-400 shrink-0" />
|
||||||
<span className="text-sm font-medium text-rose-200 flex-1">Branch</span>
|
<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 ?? [];
|
const options = d.options ?? [];
|
||||||
|
|
||||||
return (
|
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">
|
<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" />
|
<WaypointsIcon className="size-4 text-amber-400 shrink-0" />
|
||||||
<span className="text-sm font-medium text-amber-200 flex-1 truncate">
|
<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 ?? ">";
|
const op = d.operator ?? ">";
|
||||||
|
|
||||||
return (
|
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">
|
<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" />
|
<EqualIcon className="size-4 text-slate-400 shrink-0" />
|
||||||
<span className="text-sm font-medium text-slate-200 flex-1">Compare</span>
|
<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 };
|
const d = data as { value?: string; description?: string };
|
||||||
|
|
||||||
return (
|
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">
|
<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" />
|
<CalendarIcon className="size-4 text-cyan-400 shrink-0" />
|
||||||
<span className="text-sm font-medium text-cyan-200 flex-1 truncate">
|
<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 };
|
const d = data as { flagName?: string };
|
||||||
|
|
||||||
return (
|
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">
|
<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" />
|
<FlagIcon className="size-4 text-orange-400 shrink-0" />
|
||||||
<span className="text-sm font-medium text-orange-200 flex-1 truncate">
|
<span className="text-sm font-medium text-orange-200 flex-1 truncate">Flag</span>
|
||||||
Flag
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="px-3 py-2 text-xs text-orange-300/80 truncate">
|
<div className="px-3 py-2 text-xs text-orange-300/80 truncate">
|
||||||
{d.flagName || "Unnamed flag"}
|
{d.flagName || "Unnamed flag"}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,11 @@ export function FormatNode({ data }: NodeProps) {
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
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">
|
<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" />
|
<AlignLeftIcon className="size-4 text-sky-400 shrink-0" />
|
||||||
<span className="text-sm font-medium text-sky-200 flex-1">Format</span>
|
<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";
|
const fieldLabel = GAME_DATA_FIELDS.find((f) => f.value === d.field)?.label ?? "Select field";
|
||||||
|
|
||||||
return (
|
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">
|
<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" />
|
<DatabaseIcon className="size-4 text-indigo-400 shrink-0" />
|
||||||
<span className="text-sm font-medium text-indigo-200 flex-1 truncate">
|
<span className="text-sm font-medium text-indigo-200 flex-1 truncate">Game Data</span>
|
||||||
Game Data
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="px-3 py-2 text-xs text-indigo-300/80">
|
|
||||||
{fieldLabel}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="px-3 py-2 text-xs text-indigo-300/80">{fieldLabel}</div>
|
||||||
</NodeShell>
|
</NodeShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,10 @@ export function LogicNode({ data }: NodeProps) {
|
||||||
const op = d.operator ?? "AND";
|
const op = d.operator ?? "AND";
|
||||||
|
|
||||||
return (
|
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">
|
<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" />
|
<BinaryIcon className="size-4 text-lime-400 shrink-0" />
|
||||||
<span className="text-sm font-medium text-lime-200 flex-1">Logic</span>
|
<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 ?? "+";
|
const op = d.operator ?? "+";
|
||||||
|
|
||||||
return (
|
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">
|
<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" />
|
<CalculatorIcon className="size-4 text-violet-400 shrink-0" />
|
||||||
<span className="text-sm font-medium text-violet-200 flex-1">Math</span>
|
<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 };
|
const d = data as { title?: string; text?: string };
|
||||||
|
|
||||||
return (
|
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">
|
<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" />
|
<BookOpenTextIcon className="size-4 text-blue-400 shrink-0" />
|
||||||
<span className="text-sm font-medium text-blue-200 flex-1 truncate">
|
<span className="text-sm font-medium text-blue-200 flex-1 truncate">
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,16 @@
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
ReactFlow,
|
|
||||||
Background,
|
|
||||||
Controls,
|
|
||||||
MiniMap,
|
|
||||||
useNodesState,
|
|
||||||
useEdgesState,
|
|
||||||
addEdge,
|
addEdge,
|
||||||
type Node,
|
Background,
|
||||||
type Edge,
|
|
||||||
type Connection,
|
type Connection,
|
||||||
|
Controls,
|
||||||
|
type Edge,
|
||||||
|
MiniMap,
|
||||||
|
type Node,
|
||||||
|
ReactFlow,
|
||||||
|
useEdgesState,
|
||||||
|
useNodesState,
|
||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import "@xyflow/react/dist/style.css";
|
import "@xyflow/react/dist/style.css";
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ import { NotNode } from "./NotNode";
|
||||||
import { BranchNode } from "./BranchNode";
|
import { BranchNode } from "./BranchNode";
|
||||||
import { SequenceNode } from "./SequenceNode";
|
import { SequenceNode } from "./SequenceNode";
|
||||||
import { NodeEditDialog } from "./NodeEditDialog";
|
import { NodeEditDialog } from "./NodeEditDialog";
|
||||||
import { NODE_SOCKETS, type SocketType } from "./types";
|
import { NODE_SOCKETS } from "./types";
|
||||||
|
|
||||||
const nodeTypes = {
|
const nodeTypes = {
|
||||||
narrativeBeat: NarrativeBeatNode,
|
narrativeBeat: NarrativeBeatNode,
|
||||||
|
|
@ -106,7 +106,12 @@ export function NarrativeFlowEditor() {
|
||||||
|
|
||||||
const isValidConnection = useCallback(
|
const isValidConnection = useCallback(
|
||||||
(connection: Edge | Connection) => {
|
(connection: Edge | Connection) => {
|
||||||
if (!connection.source || !connection.target || !connection.sourceHandle || !connection.targetHandle) {
|
if (
|
||||||
|
!connection.source ||
|
||||||
|
!connection.target ||
|
||||||
|
!connection.sourceHandle ||
|
||||||
|
!connection.targetHandle
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const sourceNode = nodes.find((n) => n.id === connection.source);
|
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);
|
const targetSocket = targetSockets.inputs.find((s) => s.id === connection.targetHandle);
|
||||||
|
|
||||||
// Dynamic handles (arg-*, option-*) are treated as "any"
|
// Dynamic handles (arg-*, option-*) are treated as "any"
|
||||||
const sourceType = sourceSocket?.type ?? (connection.sourceHandle.startsWith("arg-") || connection.sourceHandle.startsWith("option-") ? "any" : null);
|
const sourceType =
|
||||||
const targetType = targetSocket?.type ?? (connection.targetHandle.startsWith("arg-") || connection.targetHandle.startsWith("option-") ? "any" : null);
|
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 === null || targetType === null) return false;
|
||||||
if (sourceType === "any" || targetType === "any") return true;
|
if (sourceType === "any" || targetType === "any") return true;
|
||||||
|
|
@ -135,21 +148,15 @@ export function NarrativeFlowEditor() {
|
||||||
setEditingNodeId(node.id);
|
setEditingNodeId(node.id);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onNodeContextMenu = useCallback(
|
const onNodeContextMenu = useCallback((e: React.MouseEvent, node: Node) => {
|
||||||
(e: React.MouseEvent, node: Node) => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setContextMenu({ x: e.clientX, y: e.clientY, nodeId: node.id });
|
setContextMenu({ x: e.clientX, y: e.clientY, nodeId: node.id });
|
||||||
},
|
}, []);
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onEdgeContextMenu = useCallback(
|
const onEdgeContextMenu = useCallback((e: React.MouseEvent, edge: Edge) => {
|
||||||
(e: React.MouseEvent, edge: Edge) => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setContextMenu({ x: e.clientX, y: e.clientY, edgeId: edge.id });
|
setContextMenu({ x: e.clientX, y: e.clientY, edgeId: edge.id });
|
||||||
},
|
}, []);
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onPaneContextMenu = useCallback((e: MouseEvent | React.MouseEvent) => {
|
const onPaneContextMenu = useCallback((e: MouseEvent | React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -178,7 +185,7 @@ export function NarrativeFlowEditor() {
|
||||||
[setNodes],
|
[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(
|
const addNode = useCallback(
|
||||||
(
|
(
|
||||||
|
|
@ -240,32 +247,129 @@ export function NarrativeFlowEditor() {
|
||||||
|
|
||||||
const toolbarButtons = (
|
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">
|
<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>
|
<span className="text-[9px] uppercase tracking-wider text-zinc-500 font-semibold mr-1">
|
||||||
<button className={`${buttonClass} bg-blue-950 border-blue-700 text-blue-200 hover:bg-blue-900`} onClick={() => addNode("narrativeBeat")}>Beat</button>
|
Story
|
||||||
<button className={`${buttonClass} bg-amber-950 border-amber-700 text-amber-200 hover:bg-amber-900`} onClick={() => addNode("choice")}>Choice</button>
|
</span>
|
||||||
<button className={`${buttonClass} bg-emerald-950 border-emerald-700 text-emerald-200 hover:bg-emerald-900`} onClick={() => addNode("outcome")}>Outcome</button>
|
<button
|
||||||
<button className={`${buttonClass} bg-yellow-950 border-yellow-700 text-yellow-200 hover:bg-yellow-900`} onClick={() => addNode("wait")}>Wait</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>
|
<span className="text-[9px] uppercase tracking-wider text-zinc-500 font-semibold ml-2 mr-1">
|
||||||
<button className={`${buttonClass} bg-purple-950 border-purple-700 text-purple-200 hover:bg-purple-900`} onClick={() => addNode("number")}>Number</button>
|
Data
|
||||||
<button className={`${buttonClass} bg-teal-950 border-teal-700 text-teal-200 hover:bg-teal-900`} onClick={() => addNode("string")}>String</button>
|
</span>
|
||||||
<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
|
||||||
<button className={`${buttonClass} bg-cyan-950 border-cyan-700 text-cyan-200 hover:bg-cyan-900`} onClick={() => addNode("date")}>Date</button>
|
className={`${buttonClass} bg-purple-950 border-purple-700 text-purple-200 hover:bg-purple-900`}
|
||||||
<button className={`${buttonClass} bg-pink-950 border-pink-700 text-pink-200 hover:bg-pink-900`} onClick={() => addNode("random")}>Random</button>
|
onClick={() => addNode("number")}
|
||||||
<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>
|
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>
|
<span className="text-[9px] uppercase tracking-wider text-zinc-500 font-semibold ml-2 mr-1">
|
||||||
<button className={`${buttonClass} bg-slate-950 border-slate-700 text-slate-200 hover:bg-slate-900`} onClick={() => addNode("compare")}>Compare</button>
|
Logic
|
||||||
<button className={`${buttonClass} bg-violet-950 border-violet-700 text-violet-200 hover:bg-violet-900`} onClick={() => addNode("math")}>Math</button>
|
</span>
|
||||||
<button className={`${buttonClass} bg-sky-950 border-sky-700 text-sky-200 hover:bg-sky-900`} onClick={() => addNode("format")}>Format</button>
|
<button
|
||||||
<button className={`${buttonClass} bg-lime-950 border-lime-700 text-lime-200 hover:bg-lime-900`} onClick={() => addNode("logic")}>Logic</button>
|
className={`${buttonClass} bg-slate-950 border-slate-700 text-slate-200 hover:bg-slate-900`}
|
||||||
<button className={`${buttonClass} bg-red-950 border-red-700 text-red-200 hover:bg-red-900`} onClick={() => addNode("not")}>NOT</button>
|
onClick={() => addNode("compare")}
|
||||||
<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>
|
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">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState, type FC } from "react";
|
import { type FC, useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogFooter,
|
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
@ -22,12 +22,7 @@ interface NodeEditDialogProps {
|
||||||
onSave: (nodeId: string, data: Record<string, unknown>) => void;
|
onSave: (nodeId: string, data: Record<string, unknown>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NodeEditDialog: FC<NodeEditDialogProps> = ({
|
export const NodeEditDialog: FC<NodeEditDialogProps> = ({ node, open, onOpenChange, onSave }) => {
|
||||||
node,
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
onSave,
|
|
||||||
}) => {
|
|
||||||
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -326,10 +321,7 @@ export const NodeEditDialog: FC<NodeEditDialogProps> = ({
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const updated = [
|
const updated = [...options, { id: crypto.randomUUID(), label: "" }];
|
||||||
...options,
|
|
||||||
{ id: crypto.randomUUID(), label: "" },
|
|
||||||
];
|
|
||||||
updateField("options", updated);
|
updateField("options", updated);
|
||||||
}}
|
}}
|
||||||
className="dark:border-zinc-700 dark:text-zinc-300"
|
className="dark:border-zinc-700 dark:text-zinc-300"
|
||||||
|
|
@ -584,11 +576,7 @@ export const NodeEditDialog: FC<NodeEditDialogProps> = ({
|
||||||
case "not":
|
case "not":
|
||||||
case "branch":
|
case "branch":
|
||||||
case "sequence": {
|
case "sequence": {
|
||||||
return (
|
return <p className="text-sm text-muted-foreground">No configurable properties.</p>;
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
No configurable properties.
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
@ -620,7 +608,10 @@ export const NodeEditDialog: FC<NodeEditDialogProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<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>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center justify-between">
|
<DialogTitle className="flex items-center justify-between">
|
||||||
<span>Edit {label}</span>
|
<span>Edit {label}</span>
|
||||||
|
|
@ -642,10 +633,7 @@ export const NodeEditDialog: FC<NodeEditDialogProps> = ({
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button onClick={handleSave} className="bg-blue-600 hover:bg-blue-700 text-white">
|
||||||
onClick={handleSave}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
|
||||||
>
|
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Handle, Position } from "@xyflow/react";
|
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({
|
function SocketHandle({
|
||||||
socket,
|
socket,
|
||||||
|
|
@ -81,16 +81,12 @@ export function NodeShell({
|
||||||
{/* Input handles - positioned on left edge */}
|
{/* Input handles - positioned on left edge */}
|
||||||
{inputs.map((socket, i) => {
|
{inputs.map((socket, i) => {
|
||||||
const top = maxSlots === 1 ? 50 : (i / (maxSlots - 1)) * 80 + 10;
|
const top = maxSlots === 1 ? 50 : (i / (maxSlots - 1)) * 80 + 10;
|
||||||
return (
|
return <SocketHandle key={socket.id} socket={socket} isInput={true} topPercent={top} />;
|
||||||
<SocketHandle key={socket.id} socket={socket} isInput={true} topPercent={top} />
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
{/* Output handles - positioned on right edge */}
|
{/* Output handles - positioned on right edge */}
|
||||||
{outputs.map((socket, i) => {
|
{outputs.map((socket, i) => {
|
||||||
const top = maxSlots === 1 ? 50 : (i / (maxSlots - 1)) * 80 + 10;
|
const top = maxSlots === 1 ? 50 : (i / (maxSlots - 1)) * 80 + 10;
|
||||||
return (
|
return <SocketHandle key={socket.id} socket={socket} isInput={false} topPercent={top} />;
|
||||||
<SocketHandle key={socket.id} socket={socket} isInput={false} topPercent={top} />
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
{/* Content area with padding to avoid handle overlap */}
|
{/* Content area with padding to avoid handle overlap */}
|
||||||
<div className="px-8">{children}</div>
|
<div className="px-8">{children}</div>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,10 @@ import { NodeShell } from "./NodeShell";
|
||||||
|
|
||||||
export function NotNode(_props: NodeProps) {
|
export function NotNode(_props: NodeProps) {
|
||||||
return (
|
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">
|
<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" />
|
<ToggleLeftIcon className="size-4 text-red-400 shrink-0" />
|
||||||
<span className="text-sm font-medium text-red-200 flex-1">NOT</span>
|
<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 };
|
const d = data as { value?: number; description?: string };
|
||||||
|
|
||||||
return (
|
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">
|
<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" />
|
<HashIcon className="size-4 text-purple-400 shrink-0" />
|
||||||
<span className="text-sm font-medium text-purple-200 flex-1 truncate">
|
<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 ?? [];
|
const effects = d.effects ?? [];
|
||||||
|
|
||||||
return (
|
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">
|
<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" />
|
<FlagIcon className="size-4 text-emerald-400 shrink-0" />
|
||||||
<span className="text-sm font-medium text-emerald-200 flex-1 truncate">
|
<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 };
|
const d = data as { min?: number; max?: number; description?: string };
|
||||||
|
|
||||||
return (
|
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">
|
<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" />
|
<ShuffleIcon className="size-4 text-pink-400 shrink-0" />
|
||||||
<span className="text-sm font-medium text-pink-200 flex-1 truncate">
|
<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) {
|
export function SequenceNode(_props: NodeProps) {
|
||||||
return (
|
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">
|
<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" />
|
<ListOrderedIcon className="size-4 text-fuchsia-400 shrink-0" />
|
||||||
<span className="text-sm font-medium text-fuchsia-200 flex-1">Sequence</span>
|
<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 };
|
const d = data as { value?: string; description?: string };
|
||||||
|
|
||||||
return (
|
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">
|
<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" />
|
<TypeIcon className="size-4 text-teal-400 shrink-0" />
|
||||||
<span className="text-sm font-medium text-teal-200 flex-1 truncate">
|
<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 };
|
const d = data as { ticks?: number; input?: unknown };
|
||||||
|
|
||||||
return (
|
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">
|
<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" />
|
<TimerIcon className="size-4 text-yellow-400 shrink-0" />
|
||||||
<span className="text-sm font-medium text-yellow-200 flex-1 truncate">
|
<span className="text-sm font-medium text-yellow-200 flex-1 truncate">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue