tscircuit/ti
ti tscircuit library
- Version
- 1.0.195
- License
- unset
- Stars
- 0
system-block-ui/src/editor/SystemBlockNodeView.tsx
import { Handle, type NodeProps, Position } from "@xyflow/react";
import { GearIcon } from "../components/Icons";
import type { PortDefinition, VoltageRequirement } from "../model/types";
import {
formatProtocolLabel,
getSystemBlockHandleId,
type SemanticInterfaceSummary,
type SystemBlockNode,
} from "./types";
function formatVoltage(voltage: VoltageRequirement): string {
if (typeof voltage === "number") return `${voltage} V`;
if (voltage.nominal !== undefined) return `${voltage.nominal} V`;
return `${voltage.min}–${voltage.max} V`;
}
function rowMetadata(ports: readonly PortDefinition[]): string {
const voltages = [
...new Set(
ports
.map((port) => port.voltage)
.filter(
(voltage): voltage is VoltageRequirement => voltage !== undefined,
)
.map(formatVoltage),
),
];
const protocols = [
...new Set(
ports
.map((port) => port.protocol)
.filter((protocol): protocol is string => Boolean(protocol))
.filter((protocol) => protocol !== "power")
.map(formatProtocolLabel),
),
];
const details = [...voltages, ...protocols];
const prefix = ports.length > 1 ? `${ports.length} ports` : ports[0]?.label;
return [prefix, ...details].filter(Boolean).join(" · ");
}
function InterfaceRow({
summary,
side,
}: {
summary: SemanticInterfaceSummary;
side: "input" | "output";
}) {
const ports = side === "input" ? summary.inputPorts : summary.outputPorts;
if (ports.length === 0) return null;
const handle = (
<Handle
className="system-block-handle"
id={getSystemBlockHandleId(side, summary.kind)}
position={side === "input" ? Position.Left : Position.Right}
style={{ background: summary.color }}
title={`${summary.label} ${side}`}
type={side === "input" ? "target" : "source"}
/>
);
return (
<div className="system-block-port-row" data-side={side}>
{side === "input" && handle}
<span className="system-block-port-direction">
{side === "input" ? "IN" : "OUT"}
</span>
<span className="system-block-port-kind" style={{ color: summary.color }}>
<span
className="system-block-port-dot"
style={{ background: summary.color }}
/>
{summary.label}
</span>
<span className="system-block-port-meta" title={rowMetadata(ports)}>
{rowMetadata(ports)}
</span>
{side === "output" && handle}
</div>
);
}
export function SystemBlockNodeView({
data,
selected,
}: NodeProps<SystemBlockNode>) {
const { definition, interfaces } = data;
const placementOnly = interfaces.length === 0;
return (
<article
className={`system-block-card${selected ? " is-selected" : ""}${placementOnly ? " is-placement-only" : ""}`}
>
<header className="system-block-card-header">
<span className="system-block-card-heading">
<strong>{definition.title}</strong>
</span>
<button
aria-label={`Choose another TI subcircuit for ${definition.title}`}
className="nodrag nopan system-block-card-action"
title="Change part"
type="button"
>
<GearIcon height={16} width={16} />
</button>
</header>
{definition.description && (
<p className="system-block-description">{definition.description}</p>
)}
{placementOnly ? (
<div className="system-block-empty-interfaces">
<span>No reviewed interfaces yet</span>
<small>This block can be placed but not auto-connected.</small>
</div>
) : (
<div className="system-block-interface-list">
{interfaces.map((summary) => (
<InterfaceRow
key={`input:${summary.kind}`}
side="input"
summary={summary}
/>
))}
{interfaces.map((summary) => (
<InterfaceRow
key={`output:${summary.kind}`}
side="output"
summary={summary}
/>
))}
</div>
)}
</article>
);
}