1
0
Fork 0
polaris-task-force/src/components/admin/tech-tree/CategoryBand.tsx
Z8MB1E 89482798f4 feat(tech-tree): add admin tech tree editor view
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.
2026-10-01 10:03:58 -04:00

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>
);
}