nexus-ui
uitoolsource: ok · hit
Tool
Tool call component with state-aware header and JSON input/output blocks
npx shadcn add "https://nexus-ui.dev/r/tool.json"free
Where it goes
navbarstats
Interaction
animated-counter
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · light@hugeicons/react@hugeicons/core-free-icons@react-symbols/iconsshiki
Registry dependencies
badgecollapsible
Files3 · source cached 10/8/2026, 10:51:05 PM
components/nexus-ui/tool.tsx
"use client";
import {
createContext,
useContext,
type ComponentProps,
type CSSProperties,
} from "react";
import {
ArrowDown01Icon,
CancelCircleIcon,
CheckmarkCircle01Icon,
Clock01Icon,
Loading03Icon,
ToolsIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon, type IconSvgElement } from "@hugeicons/react";
import { Badge } from "@/components/ui/badge";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import {
CodeBlock,
CodeBlockContent,
CodeblockShiki,
} from "@/components/nexus-ui/codeblock-new";
type ToolStatus = "pending" | "ready" | "running" | "completed" | "error";
type ToolMeta = {
label: string;
icon: IconSvgElement;
color: { bg: string; fg: string };
iconClassName?: string;
};
const TOOL_META: Record<ToolStatus, ToolMeta> = {
pending: {
label: "Pending",
icon: ToolsIcon,
color: { bg: "var(--color-gray-100)", fg: "var(--color-gray-500)" },
},
ready: {
label: "Ready",
icon: Clock01Icon,
color: { bg: "var(--color-orange-100)", fg: "var(--color-orange-600)" },
},
running: {
label: "Running",
icon: Loading03Icon,
color: { bg: "var(--color-blue-100)", fg: "var(--color-blue-600)" },
iconClassName: "animate-spin",
},
completed: {
label: "Completed",
icon: CheckmarkCircle01Icon,
color: { bg: "var(--color-green-100)", fg: "var(--color-green-600)" },
},
error: {
label: "Error",
icon: CancelCircleIcon,
color: { bg: "var(--color-red-100)", fg: "var(--color-red-600)" },
},
};
type ToolContextValue = {
status: ToolStatus;
meta: ToolMeta;
};
const ToolContext = createContext<ToolContextValue | null>(null);
function isToolStatus(value: unknown): value is ToolStatus {
return (
typeof value === "string" &&
Object.prototype.hasOwnProperty.call(TOOL_META, value)
);
}
function useToolContext(component: string): ToolContextValue {
const context = useContext(ToolContext);
if (!context) {
throw new Error(`${component} must be used within <Tool>`);
}
return context;
}
function stringifyToolPayload(payload: unknown): string {
if (typeof payload === "string") return payload;
if (payload === undefined) return "";
try {
return JSON.stringify(payload, null, 2);
} catch {
return String(payload);
}
}
type ToolProps = ComponentProps<typeof Collapsible> & {
status: ToolStatus;
};
function Tool({ status, className, style, ...props }: ToolProps) {
const resolvedStatus = isToolStatus(status) ? status : "pending";
const meta = TOOL_META[resolvedStatus];
return (
<ToolContext.Provider value={{ status: resolvedStatus, meta }}>
<Collapsible
data-slot="tool"
className={cn(
"not-prose w-full max-w-100 border dark:border-accent bg-card",
"data-[state=closed]:rounded-xl data-[state=open]:rounded-xl",
className,
)}
style={
{
"--tool-color": meta.color.fg,
"--tool-bg": meta.color.bg,
...style,
} as CSSProperties
}
{...props}
/>
</ToolContext.Provider>
);
}
type ToolTriggerProps = Omit<
ComponentProps<typeof CollapsibleTrigger>,
"children"
> & {
name: string;
};
function ToolTrigger({ name, className, ...props }: ToolTriggerProps) {
const { meta } = useToolContext("ToolTrigger");
return (
<CollapsibleTrigger
data-slot="tool-trigger"
className={cn(
"group flex h-10 w-full cursor-pointer items-center justify-between px-3 py-2",
className,
)}
{...props}
>
<div className="flex items-center gap-2">
<HugeiconsIcon
data-slot="tool-trigger-icon"
icon={meta.icon}
strokeWidth={2}
className={cn("size-4 text-(--tool-color)", meta.iconClassName)}
/>
<span
data-slot="tool-trigger-name"
className="text-sm leading-6 font-[450] text-primary"
>
{name}
</span>
<Badge
data-slot="tool-trigger-badge"
className="h-6 bg-(--tool-bg)/60 font-[450] text-(--tool-color) dark:bg-(--tool-color)/10 dark:text-(--tool-color)"
>
{meta.label}
</Badge>
</div>
<HugeiconsIcon
data-slot="tool-trigger-chevron"
icon={ArrowDown01Icon}
strokeWidth={1.75}
className="size-4 transition-transform duration-200 group-data-[state=open]:rotate-180"
/>
</CollapsibleTrigger>
);
}
type ToolContentProps = ComponentProps<typeof CollapsibleContent>;
function ToolContent({ className, ...props }: ToolContentProps) {
return (
<CollapsibleContent
data-slot="tool-content"
className={cn(
"flex flex-col gap-6 p-3 pt-4",
"overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down",
className,
)}
{...props}
/>
);
}
type ToolPartProps = {
kind: "input" | "output";
payload: unknown;
errorText?: string;
};
function ToolPart({ kind, payload, errorText }: ToolPartProps) {
const { status } = useToolContext("ToolPart");
const code = stringifyToolPayload(payload);
const isOutputError = kind === "output" && status === "error";
const hasPayload = payload !== undefined && payload !== null;
const shouldShowCodeblock = !isOutputError || hasPayload;
const title = kind === "input" ? "Input" : isOutputError ? "Error" : "Output";
return (
<div data-slot={`tool-${kind}`} className="flex flex-col gap-3">
<span
data-slot={`tool-${kind}-title`}
className={cn(
"text-xs leading-4 font-[450] text-muted-foreground uppercase",
isOutputError && "text-destructive",
)}
>
{title}
</span>
{isOutputError ? (
<div
data-slot="tool-output-error"
className="rounded-xl border border-destructive/20 bg-destructive/5 px-4 py-3 text-sm leading-6 text-destructive dark:bg-destructive/10"
>
{errorText ?? "Tool execution failed"}
</div>
) : null}
{shouldShowCodeblock ? (
<CodeBlock
data-slot="tool-output-error-codeblock"
className="rounded-lg"
keepBackground
>
<CodeBlockContent>
<CodeblockShiki language="json">{code}</CodeblockShiki>
</CodeBlockContent>
</CodeBlock>
) : null}
</div>
);
}
type ToolPayloadProps = {
payload: unknown;
};
function ToolInput({ payload }: ToolPayloadProps) {
return <ToolPart kind="input" payload={payload} />;
}
type ToolOutputProps = ToolPayloadProps & {
showWhen?: ToolStatus[];
errorText?: string;
};
function ToolOutput({
payload,
showWhen = ["completed"],
errorText,
}: ToolOutputProps) {
const { status } = useToolContext("ToolOutput");
if (!showWhen.includes(status)) return null;
return <ToolPart kind="output" payload={payload} errorText={errorText} />;
}
export type { ToolStatus };
export { Tool, ToolTrigger, ToolContent, ToolInput, ToolOutput };
components/nexus-ui/codeblock-new.tsx
"use client";
import { FileIcon } from "@react-symbols/icons/utils";
import { Copy01Icon, Tick02Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import {
createContext,
useEffect,
useContext,
useState,
type CSSProperties,
type ComponentProps,
type ReactNode,
} from "react";
import { cn } from "@/lib/utils";
import { highlight, Themes } from "@/lib/shiki/highlighter";
import type { BundledLanguage } from "shiki/bundle/web";
const highlighterPromise = highlight();
type DivProps = ComponentProps<"div">;
type CodeBlockProps = DivProps & {
keepBackground?: boolean;
};
type CodeBlockCopyContextValue = {
content: string;
setContent: (value: string) => void;
};
const CodeBlockCopyContext = createContext<CodeBlockCopyContextValue | null>(
null,
);
interface CodeblockClientShikiProps extends DivProps {
code?: string;
language?: string;
lineNumbers?: boolean;
children?: ReactNode;
}
type ShikiToken = {
content: string;
htmlStyle?: Record<string, string>;
};
const buildRawTokenRows = (input: string): ShikiToken[][] =>
input.split(/\r?\n/).map((line) => [{ content: line || "\u00A0" }]);
const EMPTY_TOKEN_ROW: ShikiToken[] = [{ content: "\u00A0" }];
const resolveCodeToHighlight = (children: ReactNode, code?: string): string =>
typeof children === "string"
? children
: Array.isArray(children) &&
children.length === 1 &&
typeof children[0] === "string"
? children[0]
: (code ?? "");
const CodeBlock = ({
children,
className,
keepBackground = false,
...props
}: CodeBlockProps) => {
const [copyContent, setCopyContent] = useState("");
return (
<CodeBlockCopyContext.Provider
value={{ content: copyContent, setContent: setCopyContent }}
>
<div
className={cn(
"not-prose",
"my-0 flex w-full flex-col overflow-hidden rounded-xl",
keepBackground
? "border-none bg-secondary dark:bg-background"
: "border bg-card dark:border-accent",
"text-[13px] font-[450]",
className,
)}
{...props}
>
{children}
</div>
</CodeBlockCopyContext.Provider>
);
};
type CodeBlockHeaderProps = DivProps;
const CodeBlockHeader = ({
children,
className,
...props
}: CodeBlockHeaderProps) => {
return (
<div
className={cn(
"not-prose", // Disable Markdown Styles
"flex h-9.5 items-center justify-between gap-2 px-4",
"text-fd-muted-foreground",
className,
)}
{...props}
>
{children}
</div>
);
};
interface CodeBlockIconProps extends DivProps {
language?: string;
}
const CodeBlockIcon = ({ language, className }: CodeBlockIconProps) => {
return (
<FileIcon
width={16}
height={16}
fileName={`.${language ?? ""}`}
autoAssign={true}
className={cn(className)}
/>
);
};
type CodeBlockGroupProps = DivProps;
const CodeBlockGroup = ({
children,
className,
...props
}: CodeBlockGroupProps) => {
return (
<div
className={cn(
"flex items-center gap-2",
"text-fd-muted-foreground",
className,
)}
{...props}
>
{children}
</div>
);
};
const CodeBlockContent = ({ className, children, ...props }: DivProps) => {
return (
<div
className={cn(
"no-scrollbar max-h-96 overflow-auto overscroll-x-none",
"rounded-xl px-4 text-sm leading-6",
"font-mono whitespace-pre",
className,
)}
{...props}
>
{children}
</div>
);
};
const CodeblockShiki = ({
code,
language = "tsx",
lineNumbers = false,
className,
children,
...props
}: CodeblockClientShikiProps) => {
const setCopyContent = useContext(CodeBlockCopyContext)?.setContent;
const codeToHighlight = resolveCodeToHighlight(children, code);
const [tokenRows, setTokenRows] = useState<ShikiToken[][]>(() =>
buildRawTokenRows(codeToHighlight),
);
useEffect(() => {
setCopyContent?.(codeToHighlight);
}, [codeToHighlight, setCopyContent]);
useEffect(() => {
let cancelled = false;
async function clientHighlight() {
const rawRows = buildRawTokenRows(codeToHighlight);
if (!cancelled) {
setTokenRows(rawRows);
}
if (!codeToHighlight) {
return;
}
try {
const highlighter = await highlighterPromise;
const result = await highlighter.codeToTokens(codeToHighlight, {
lang: language as BundledLanguage,
themes: {
light: Themes.light,
dark: Themes.dark,
},
});
if (!cancelled) {
setTokenRows((result.tokens ?? rawRows) as ShikiToken[][]);
}
} catch {
if (!cancelled) {
setTokenRows(rawRows);
}
}
}
void clientHighlight();
return () => {
cancelled = true;
};
}, [codeToHighlight, language]);
return (
<div
className={cn(
"no-scrollbar w-full overflow-auto overscroll-x-none py-0",
className,
)}
{...props}
>
<pre
className={cn(
"shiki",
lineNumbers ? "shiki-line-numbers" : "nd-no-line-numbers",
)}
>
<code>
{tokenRows.map((row, rowIndex) => (
<span key={`row-${rowIndex}`} className="line">
{(row.length ? row : EMPTY_TOKEN_ROW).map((token, tokenIndex) => (
<span
key={`token-${rowIndex}-${tokenIndex}`}
style={token.htmlStyle as CSSProperties | undefined}
>
{token.content || "\u00A0"}
</span>
))}
{rowIndex < tokenRows.length - 1 && "\n"}
</span>
))}
</code>
</pre>
</div>
);
};
type CodeBlockCopyButtonProps = ComponentProps<"button">;
const CodeBlockCopyButton = ({
className,
...props
}: CodeBlockCopyButtonProps) => {
const content = useContext(CodeBlockCopyContext)?.content ?? "";
const [isCopied, setIsCopied] = useState<boolean>(false);
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.error("Failed to copy text: ", err);
return false;
}
};
useEffect(() => {
if (!isCopied) return;
const timeout = setTimeout(() => {
setIsCopied(false);
}, 2000);
return () => clearTimeout(timeout);
}, [isCopied]);
const handleCopy = async () => {
if (!content) return;
await copyToClipboard(content);
setIsCopied(true);
};
return (
<button
title="Copy to clipboard"
className={cn(
"relative flex size-7 cursor-pointer items-center justify-center rounded-full text-ring hover:text-primary",
"",
className,
)}
onClick={handleCopy}
{...props}
>
{isCopied ? (
<HugeiconsIcon
icon={Tick02Icon}
strokeWidth={2}
className="size-3.5 animate-in text-green-900 duration-200 zoom-in-50 dark:text-green-400"
/>
) : (
<HugeiconsIcon
icon={Copy01Icon}
strokeWidth={2}
className="size-3.5 animate-in duration-200 zoom-in-50"
/>
)}
</button>
);
};
export {
CodeBlock,
CodeBlockHeader,
CodeBlockIcon,
CodeBlockGroup,
CodeBlockContent,
CodeblockShiki,
CodeBlockCopyButton,
};
lib/shiki/highlighter.ts
import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
import {
bundledLanguages,
type Highlighter,
type RegexEngine,
createHighlighter,
} from "shiki/bundle/web";
let jsEngine: RegexEngine | null = null;
let highlighter: Promise<Highlighter> | null = null;
// Settings for UI components
const Themes = {
light: "github-light",
dark: "github-dark",
};
const allBundledLanguageIds = Object.keys(bundledLanguages);
const getJsEngine = (): RegexEngine => {
jsEngine ??= createJavaScriptRegexEngine();
return jsEngine;
};
const highlight = async (): Promise<Highlighter> => {
highlighter ??= createHighlighter({
langs: allBundledLanguageIds,
themes: ["github-light", "github-dark"],
engine: getJsEngine(),
});
return highlighter;
};
export { highlight, Themes };