Add a custom admin view over the technologies collection so staff can model prerequisites as a left-to-right tree instead of a flat list. - React Flow v12 canvas with category bands, OR-prerequisite edges, a lite side-panel editor, and a quick-create dialog. - Propagating layout: a technology with prerequisites lands one column right of its parents at their average row; uncategorized roots fill a catalog grid keyed by each tech's rank among all uncategorized technologies, so promoting one into the tree leaves an empty slot instead of re-shuffling the grid. - Stability contract: connecting or removing edges never moves a card (the connected node is pinned to its current spot), and a "Save positions" control commits every on-screen node so the arrangement survives reload; "Auto-arrange" clears manual positions and restores the ordered layout. - Progression-path edge colors and per-source lanes keep overlapping edges readable. Wire the view and its nav entry into payload.config.ts and add dark React Flow / scrollbar styling.
61 lines
2.1 KiB
TypeScript
61 lines
2.1 KiB
TypeScript
"use client";
|
|
|
|
import { Handle, Position, type NodeProps, type Node } from "@xyflow/react";
|
|
|
|
import type { CategoryBandLayout } from "./types";
|
|
|
|
export type CategoryBandData = {
|
|
band: CategoryBandLayout;
|
|
techCount: number;
|
|
};
|
|
|
|
export type CategoryBandNode = Node<CategoryBandData, "techBand">;
|
|
|
|
/**
|
|
* Full-height column band behind a category's technologies. Non-interactive:
|
|
* it is a backdrop, not a node (no handles, no dragging, no selection).
|
|
*/
|
|
export function CategoryBand({ data }: NodeProps<CategoryBandNode>) {
|
|
const { band, techCount } = data;
|
|
const accent = band.color ?? "#22d3ee";
|
|
|
|
return (
|
|
<div
|
|
className="pointer-events-none flex h-full flex-col border bg-black/40"
|
|
style={{
|
|
borderColor: `${accent}2e`,
|
|
boxShadow: "0 0 24px rgba(0,0,0,0.35)",
|
|
}}
|
|
data-testid={`tech-band-${band.key}`}
|
|
>
|
|
<div
|
|
className="flex items-center justify-between border-b bg-black/50 px-3 py-2"
|
|
style={{ borderColor: `${accent}33` }}
|
|
>
|
|
<span
|
|
className="font-mono text-[9px] tracking-widest uppercase"
|
|
style={{ color: `${accent}cc` }}
|
|
>
|
|
{band.label}
|
|
</span>
|
|
<span className="font-mono text-[9px] tracking-widest text-white/40 uppercase">
|
|
{band.position !== null ? `POS ${band.position}` : "AUTO"} · {techCount} TECH
|
|
{techCount === 1 ? "" : "S"}
|
|
</span>
|
|
</div>
|
|
<div
|
|
className="h-px w-full"
|
|
style={{ background: `linear-gradient(90deg, transparent, ${accent}59, transparent)` }}
|
|
/>
|
|
<div className="flex-1" />
|
|
<div className="border-t border-white/10 bg-black/40 px-3 py-1">
|
|
<span className="font-mono text-[9px] tracking-widest text-white/35 uppercase">
|
|
TIER BAND // {band.label.toUpperCase()}
|
|
</span>
|
|
</div>
|
|
{/* Inert handles so React Flow never treats the band as connectable */}
|
|
<Handle type="source" position={Position.Bottom} className="!hidden" isConnectable={false} />
|
|
<Handle type="target" position={Position.Top} className="!hidden" isConnectable={false} />
|
|
</div>
|
|
);
|
|
}
|