{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "base-node", "title": "Base Node", "description": "A themed React Flow node component with header, body, footer, and connection handles, registrable as a node type.", "dependencies": ["@xyflow/react", "lucide-react", "clsx", "tailwind-merge"], "registryDependencies": ["node-themes"], "files": [ { "path": "src/registry/thornberry/components/base-node.tsx", "content": "\"use client\";\n\nimport { Handle } from \"@xyflow/react\";\nimport { memo } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { resolveTheme } from \"@/registry/thornberry/lib/node-themes\";\n\nimport type { Node, NodeProps, Position } from \"@xyflow/react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport type {\n NodeTheme,\n NodeThemeKey,\n} from \"@/registry/thornberry/lib/node-themes\";\n\n/**\n * Standard node dimensions for consistent canvas layout. The width is an\n * even multiple of the default grid size for clean center alignment.\n */\nexport const NODE_WIDTH = 300;\nexport const NODE_MIN_HEIGHT = 120;\n\n/** Connection handle configuration */\ninterface NodeHandleConfig {\n type: \"source\" | \"target\";\n position: Position;\n id?: string;\n className?: string;\n style?: CSSProperties;\n}\n\n/**\n * Node data shape consumed by the `base` node type. React Flow supplies\n * `selected` on the node props, so it is not part of the data payload.\n * Consumers add nodes as `{ type: \"base\", data: { theme, label, ... } }`.\n */\nexport interface BaseNodeData {\n /** Theme key or fully custom theme object */\n theme: NodeThemeKey | NodeTheme;\n /** Optional icon rendered in the header badge */\n icon?: LucideIcon;\n /** Node title */\n label: string;\n /** Optional secondary description under the label */\n description?: string;\n /** Optional badge text rendered in the header */\n badge?: string;\n /** Connection handles to render */\n handles?: NodeHandleConfig[];\n /** Optional body content */\n children?: ReactNode;\n /** Optional footer content */\n footer?: ReactNode;\n /** Click handler for the node */\n onClick?: () => void;\n /** Delete handler; when set, a delete button is shown on hover */\n onDelete?: () => void;\n // Index signature so the shape satisfies React Flow's `Record`-based node data\n [key: string]: unknown;\n}\n\n/**\n * Generic base node providing a consistent header (icon, label, optional\n * description and badge), optional body and footer, selection state, and\n * connection handles. It is a proper React Flow node component: content\n * comes from `data` and selection from props, so it can be registered\n * directly as a `nodeType` without an adapter. Styling is driven by a\n * semantic {@link NodeTheme}.\n */\nexport const BaseNode = memo(\n ({ data, selected }: NodeProps>) => {\n const {\n theme,\n icon: Icon,\n label,\n description,\n badge,\n handles = [],\n children,\n footer,\n onClick,\n onDelete,\n } = data;\n const t = resolveTheme(theme);\n\n return (\n {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onClick();\n }\n }\n : undefined\n }\n >\n {onDelete && (\n {\n e.stopPropagation();\n onDelete();\n }}\n >\n x\n \n )}\n\n {/* Header */}\n
\n
\n {Icon && (\n \n \n
\n )}\n
\n {label}\n {description && (\n \n {description}\n \n )}\n
\n
\n {badge && (\n \n {badge}\n \n )}\n \n\n {/* Body */}\n {children &&
{children}
}\n\n {/* Footer */}\n {footer &&
{footer}
}\n\n {/* Handles */}\n {handles.map((handle, index) => (\n \n ))}\n \n );\n },\n);\n\nBaseNode.displayName = \"BaseNode\";\n", "type": "registry:ui" }, { "path": "src/lib/utils.ts", "content": "import { clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nimport type { ClassValue } from \"clsx\";\n\nexport const cn = (...inputs: ClassValue[]) => {\n return twMerge(clsx(inputs));\n};\n", "type": "registry:lib", "target": "" }, { "path": "src/registry/thornberry/lib/node-themes.ts", "content": "/**\n * Visual theme for a node, expressed as Tailwind utility class strings.\n *\n * Consumers either pick one of the semantic keys in {@link nodeThemes} or\n * pass a fully custom object of this shape.\n */\nexport interface NodeTheme {\n /** Border classes for the node container */\n border: string;\n /** Background classes for the node container */\n bg: string;\n /** Background classes for the icon badge */\n iconBg: string;\n /** Text/foreground classes for the icon */\n iconText: string;\n /** Accent text classes (labels, emphasis) */\n accent: string;\n /** Background classes applied to connection handles */\n handleColor: string;\n /** Optional box-shadow/glow classes */\n glow?: string;\n}\n\n/**\n * Small set of generic, semantic node themes driven by the design\n * system's CSS-variable tokens (card, primary, muted, destructive).\n *\n * `success` and `warning` are not part of the base token set, so they\n * fall back to fixed emerald/amber hues to keep their conventional\n * meaning.\n */\nexport const nodeThemes = {\n default: {\n border: \"border-border\",\n bg: \"bg-card\",\n iconBg: \"bg-secondary\",\n iconText: \"text-secondary-foreground\",\n accent: \"text-foreground\",\n handleColor: \"!bg-muted-foreground\",\n },\n accent: {\n border: \"border-primary\",\n bg: \"bg-primary/10\",\n iconBg: \"bg-primary\",\n iconText: \"text-primary-foreground\",\n accent: \"text-primary\",\n handleColor: \"!bg-primary\",\n glow: \"shadow-[0_0_15px_rgba(59,130,246,0.15)]\",\n },\n // success/warning have no design-system token; use fixed hues\n success: {\n border: \"border-emerald-300/60 dark:border-emerald-600/40\",\n bg: \"bg-emerald-50 dark:bg-emerald-950\",\n iconBg: \"bg-emerald-600 dark:bg-emerald-500\",\n iconText: \"text-white\",\n accent: \"text-emerald-600 dark:text-emerald-400\",\n handleColor: \"!bg-emerald-500\",\n glow: \"shadow-[0_0_15px_rgba(16,185,129,0.15)]\",\n },\n // success/warning have no design-system token; use fixed hues\n warning: {\n border: \"border-amber-300/60 dark:border-amber-600/40\",\n bg: \"bg-amber-50 dark:bg-amber-950\",\n iconBg: \"bg-amber-500 dark:bg-amber-600\",\n iconText: \"text-white\",\n accent: \"text-amber-600 dark:text-amber-400\",\n handleColor: \"!bg-amber-500\",\n glow: \"shadow-[0_0_15px_rgba(245,158,11,0.15)]\",\n },\n danger: {\n border: \"border-destructive\",\n bg: \"bg-destructive/10\",\n iconBg: \"bg-destructive\",\n iconText: \"text-white\",\n accent: \"text-destructive\",\n handleColor: \"!bg-destructive\",\n glow: \"shadow-[0_0_15px_rgba(239,68,68,0.15)]\",\n },\n muted: {\n border: \"border-border\",\n bg: \"bg-muted\",\n iconBg: \"bg-muted-foreground\",\n iconText: \"text-background\",\n accent: \"text-muted-foreground\",\n handleColor: \"!bg-muted-foreground\",\n },\n} as const satisfies Record;\n\n/** Union of the built-in semantic theme keys */\nexport type NodeThemeKey = keyof typeof nodeThemes;\n\n/**\n * Resolve a theme input to a concrete {@link NodeTheme}.\n *\n * A string key is looked up in {@link nodeThemes}; a custom object is\n * returned unchanged.\n */\nexport const resolveTheme = (theme: NodeThemeKey | NodeTheme): NodeTheme =>\n typeof theme === \"string\" ? nodeThemes[theme] : theme;\n", "type": "registry:lib", "target": "" } ], "type": "registry:ui" }