tscircuit/ti
ti tscircuit library
- Version
- 1.0.195
- License
- unset
- Stars
- 0
system-block-ui/src/components/SubcircuitPickerModal.tsx
import { useEffect, useMemo, useState } from "react";
import type { SubcircuitDefinition } from "../model";
import {
getTiRecommendations,
matchTiRecommendedDefinitionIds,
type TiRecommendedPart,
} from "../ti-recommendations";
export function getSelectableSubcircuitCandidates(
definitions: readonly SubcircuitDefinition[],
currentDefinition: SubcircuitDefinition,
): readonly SubcircuitDefinition[] {
return definitions
.filter(
(definition) =>
definition.id !== currentDefinition.id &&
definition.canInstantiate !== false &&
definition.sourcePath.startsWith("lib/subcircuits/") &&
definition.category === currentDefinition.category,
)
.sort((a, b) => a.title.localeCompare(b.title, "en"));
}
interface SubcircuitPickerModalProps {
currentDefinition: SubcircuitDefinition;
definitions: readonly SubcircuitDefinition[];
onClose: () => void;
onSelect: (definition: SubcircuitDefinition) => void;
}
export function SubcircuitPickerModal({
currentDefinition,
definitions,
onClose,
onSelect,
}: SubcircuitPickerModalProps) {
const recommendationDefinitions = useMemo(
() =>
definitions.filter(
(definition) =>
definition.canInstantiate !== false &&
definition.sourcePath.startsWith("lib/subcircuits/") &&
definition.category === currentDefinition.category,
),
[currentDefinition.category, definitions],
);
const candidates = useMemo(() => {
const selectable = getSelectableSubcircuitCandidates(
definitions,
currentDefinition,
);
return [currentDefinition, ...selectable].sort((a, b) =>
a.title.localeCompare(b.title, "en"),
);
}, [currentDefinition, definitions]);
const [recommendedIds, setRecommendedIds] = useState<ReadonlySet<string>>(
() => new Set(),
);
const [recommendedParts, setRecommendedParts] = useState<
readonly TiRecommendedPart[]
>([]);
const [expandedPartNumber, setExpandedPartNumber] = useState<string | null>(
null,
);
const [isFetchingRecommendations, setIsFetchingRecommendations] =
useState(false);
const widerPortfolioParts = useMemo(
() =>
recommendedParts.filter(
(part) =>
matchTiRecommendedDefinitionIds(
[part.partNumber],
recommendationDefinitions,
).size === 0,
),
[recommendationDefinitions, recommendedParts],
);
useEffect(() => {
let active = true;
setRecommendedIds(new Set());
setRecommendedParts([]);
setExpandedPartNumber(null);
if (candidates.length === 0) {
setIsFetchingRecommendations(false);
return;
}
setIsFetchingRecommendations(true);
void getTiRecommendations(
currentDefinition.category,
recommendationDefinitions,
).then(
(recommendations) => {
if (!active) return;
setRecommendedIds(recommendations.definitionIds);
setRecommendedParts(recommendations.parts);
setIsFetchingRecommendations(false);
},
() => {
if (active) setIsFetchingRecommendations(false);
},
);
return () => {
active = false;
};
}, [
candidates.length,
currentDefinition.category,
recommendationDefinitions,
]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") onClose();
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [onClose]);
return (
<div
className="subcircuit-picker-backdrop"
onMouseDown={(event) => {
if (event.currentTarget === event.target) onClose();
}}
role="presentation"
>
<section
aria-labelledby="subcircuit-picker-title"
aria-modal="true"
className="subcircuit-picker-modal"
role="dialog"
>
<header className="subcircuit-picker-header">
<h2 id="subcircuit-picker-title">Choose a TI subcircuit</h2>
<button
aria-label="Close subcircuit picker"
className="subcircuit-picker-close"
onClick={onClose}
type="button"
>
Ă—
</button>
</header>
<div className="subcircuit-picker-catalog">
<div className="subcircuit-picker-results-heading">
<span>Available subcircuits</span>
<div className="subcircuit-picker-results-summary">
{isFetchingRecommendations && (
<span
aria-live="polite"
className="ti-recommendation-fetching"
role="status"
>
<i aria-hidden="true" />
Fetching TI recommendations…
</span>
)}
<small>{candidates.length}</small>
</div>
</div>
<div className="subcircuit-picker-results">
{widerPortfolioParts.length > 0 && (
<section
aria-label="Recommendations from the wider TI portfolio"
className="ti-portfolio-recommendations"
>
<div className="ti-portfolio-recommendations-heading">
<strong>TI portfolio recommendations</strong>
<small>{widerPortfolioParts.length}</small>
</div>
{widerPortfolioParts.map((part) => {
const isExpanded = expandedPartNumber === part.partNumber;
return (
<button
aria-expanded={isExpanded}
className="ti-portfolio-part"
key={part.partNumber}
onClick={() =>
setExpandedPartNumber(
isExpanded ? null : part.partNumber,
)
}
type="button"
>
<div>
<strong>{part.name}</strong>
<small>Recommended</small>
</div>
{isExpanded && part.description && (
<span>{part.description}</span>
)}
</button>
);
})}
</section>
)}
{candidates.map((definition) => {
const isCurrent = definition.id === currentDefinition.id;
return (
<button
aria-label={
isCurrent
? `Current subcircuit: ${definition.title}`
: `Select ${definition.title}`
}
className="subcircuit-candidate"
disabled={isCurrent}
key={definition.id}
onClick={() => onSelect(definition)}
type="button"
>
<div className="subcircuit-candidate-heading">
<strong>{definition.title}</strong>
<div className="subcircuit-candidate-badges">
{isCurrent && <small data-tone="current">Current</small>}
{recommendedIds.has(definition.id) && (
<small title="Recommended by TI Support Intelligence">
Recommended
</small>
)}
</div>
</div>
{definition.description && (
<span>{definition.description}</span>
)}
</button>
);
})}
{candidates.length === 0 && (
<div className="subcircuit-picker-empty">
No other TI subcircuits are available.
</div>
)}
</div>
</div>
</section>
</div>
);
}