'use client'; import * as React from 'react'; import { cn } from '../../lib/utils'; import { getCategoryToken } from './theme/categories'; import { getCategoryIcon } from './node-icons'; import type { NodeTypeDef } from './types'; /** MIME-ish key used to carry a node-type slug across HTML5 drag-and-drop. */ export const NODE_DND_MIME = 'application/x-workflow-node'; export interface NodePaletteProps { /** The node-type catalog to list. */ nodeTypes: NodeTypeDef[]; /** Optional click handler (e.g. add node at canvas center). */ onAddNode?: (slug: string) => void; className?: string; } function matches(nt: NodeTypeDef, query: string): boolean { if (!query) return true; const q = query.toLowerCase(); return ( nt.name.toLowerCase().includes(q) || nt.slug.toLowerCase().includes(q) || nt.category.toLowerCase().includes(q) || (nt.description?.toLowerCase().includes(q) ?? false) ); } function groupByCategory( nodeTypes: NodeTypeDef[] ): { category: string; items: NodeTypeDef[] }[] { const groups = new Map(); for (const nt of nodeTypes) { const list = groups.get(nt.category) ?? []; list.push(nt); groups.set(nt.category, list); } return [...groups.entries()].map(([category, items]) => ({ category, items })); } /** * Draggable palette of {@link NodeTypeDef}s, grouped by category with a search * filter. Each item is HTML5-draggable: dragging sets the node-type slug on * `dataTransfer` under {@link NODE_DND_MIME} for the canvas to read on drop. */ export function NodePalette({ nodeTypes, onAddNode, className }: NodePaletteProps) { const [query, setQuery] = React.useState(''); const groups = React.useMemo(() => { const filtered = nodeTypes.filter((nt) => matches(nt, query)); return groupByCategory(filtered); }, [nodeTypes, query]); return (
setQuery(e.target.value)} className="w-full rounded-md border border-border bg-background px-2 py-1.5 text-sm outline-none focus:ring-2 focus:ring-primary" />
{groups.length === 0 ? (

No matching nodes.

) : ( groups.map(({ category, items }) => { const token = getCategoryToken(category); return (

{token.label}

    {items.map((nt) => { const Icon = getCategoryIcon(token.icon); return (
  • { e.dataTransfer.setData(NODE_DND_MIME, nt.slug); e.dataTransfer.effectAllowed = 'move'; }} onClick={() => onAddNode?.(nt.slug)} className="flex cursor-grab items-center gap-2 rounded-md border border-border bg-card px-2 py-1.5 text-sm transition-colors hover:bg-accent active:cursor-grabbing" title={nt.description ?? nt.name} > {nt.name}
  • ); })}
); }) )}
); }