tscircuit/ti

ti tscircuit library

Version
1.0.193
License
unset
Stars
0

system-block-ui/src/components/BlockPalette.tsx

import { type ReactNode, useMemo, useState } from "react";
import type { SubcircuitDefinition } from "../model/types";
import { SearchIcon } from "./Icons";

export const SYSTEM_BLOCK_DRAG_MIME =
  "application/x-tscircuit-system-block-definition";

interface BlockPaletteProps {
  children?: ReactNode;
  definitions: readonly SubcircuitDefinition[];
  onInsert: (definitionId: string) => void;
}

export function BlockPalette({
  children,
  definitions,
  onInsert,
}: BlockPaletteProps) {
  const [query, setQuery] = useState("");

  const groups = useMemo(() => {
    const normalizedQuery = query.trim().toLowerCase();
    const filtered = definitions.filter((definition) => {
      if (!normalizedQuery) return true;
      return [
        definition.title,
        definition.componentName,
        definition.category,
        ...(definition.tags ?? []),
      ]
        .join(" ")
        .toLowerCase()
        .includes(normalizedQuery);
    });

    const byCategory = new Map<string, SubcircuitDefinition[]>();
    for (const definition of filtered) {
      const group = byCategory.get(definition.category) ?? [];
      group.push(definition);
      byCategory.set(definition.category, group);
    }

    return [...byCategory]
      .map(([category, items]): [string, SubcircuitDefinition[]] => [
        category,
        [...items].sort((a, b) => a.title.localeCompare(b.title)),
      ])
      .sort(([a], [b]) => a.localeCompare(b));
  }, [definitions, query]);

  return (
    <aside className="palette-panel" aria-label="Subcircuit palette">
      {children}
      <div className="panel-heading">
        <h2 className="panel-title">Subcircuits</h2>
        <span className="count-badge">{definitions.length}</span>
      </div>

      <label className="palette-search">
        <SearchIcon />
        <input
          aria-label="Search subcircuits"
          onChange={(event) => setQuery(event.target.value)}
          placeholder="Search TI blocks…"
          type="search"
          value={query}
        />
      </label>

      <p className="palette-help">
        Drag a reviewed block onto the canvas. Power and Data sockets resolve to
        exact pins automatically.
      </p>

      <div className="palette-list">
        {groups.map(([category, items]) => (
          <section key={category}>
            <h3 className="category-label">{category}</h3>
            {items.map((definition) => {
              const disabled = definition.canInstantiate === false;
              const reviewedPortCount = definition.ports.length;

              return (
                <button
                  aria-disabled={disabled}
                  className="palette-card"
                  draggable={!disabled}
                  key={definition.id}
                  onClick={() => {
                    if (!disabled) onInsert(definition.id);
                  }}
                  onDragStart={(event) => {
                    if (disabled) {
                      event.preventDefault();
                      return;
                    }
                    event.dataTransfer.effectAllowed = "copy";
                    event.dataTransfer.setData(
                      SYSTEM_BLOCK_DRAG_MIME,
                      definition.id,
                    );
                    event.dataTransfer.setData("text/plain", definition.id);
                  }}
                  title={
                    definition.warning ??
                    (reviewedPortCount === 0
                      ? "Placement is available; semantic interfaces are not curated yet."
                      : `Insert ${definition.title}`)
                  }
                  type="button"
                >
                  <span className="block-copy">
                    <span className="block-title">{definition.title}</span>
                  </span>
                </button>
              );
            })}
          </section>
        ))}

        {groups.length === 0 && (
          <p className="palette-help">No subcircuits match “{query}”.</p>
        )}
      </div>
    </aside>
  );
}