Skip to content
pandacoderz/ui

Usage

Compose components with slots and context instead of props-for-everything.

Every component is a small family of parts that share context. You compose the parts you need and skip the rest.

Compose, don’t configure

A prompt input is a container, a textarea, and an actions row. Icons and buttons are yours:

import { ArrowUpIcon, PaperClipIcon } from "@heroicons-animated/react";
import { Button } from "@/components/ui/button";
import {
  PromptInput,
  PromptInputAction,
  PromptInputActionGroup,
  PromptInputActions,
  PromptInputTextarea,
} from "@/components/pandacoderz-ui/prompt-input";

export function ChatInput({ onSend }: { onSend: (text: string) => void }) {
  return (
    <PromptInput onSubmit={onSend}>
      <PromptInputTextarea />
      <PromptInputActions>
        <PromptInputActionGroup>
          <PromptInputAction asChild tooltip="Attach">
            <Button variant="ghost" size="icon-sm" className="rounded-full">
              <PaperClipIcon size={16} className="flex" />
            </Button>
          </PromptInputAction>
        </PromptInputActionGroup>
        <PromptInputActionGroup>
          <PromptInputAction asChild tooltip={{ content: "Send", shortcut: "↵" }}>
            <Button size="icon-sm" className="rounded-full">
              <ArrowUpIcon size={16} className="flex" />
            </Button>
          </PromptInputAction>
        </PromptInputActionGroup>
      </PromptInputActions>
    </PromptInput>
  );
}

asChild

Parts that wrap interactive elements accept asChild. The part then merges its behavior (tooltip, data attributes, click handling) into your element instead of rendering an extra wrapper.

Icons

The library uses Heroicons Animated. Each icon renders a div wrapper around an SVG and takes a numeric size, so pass size={16} and className="flex" for inline alignment. Swap in any icon set you like; nothing depends on the specific package beyond the defaults inside thread, reasoning, tool, and model-selector.

Streaming

Components that render text (MessageMarkdown, ReasoningContent) accept partial markdown and re-render as it grows. Wrap the conversation in Thread so the viewport follows new content, and drive state with a functional update:

for await (const chunk of stream) {
  setText((prev) => prev + chunk);
}

See the AI Chat block for a complete reducer over reasoning, tool, and text events.

Styling

Override any part with className. Every part sets a data-slot attribute, so you can also target them from a parent:

[data-slot="message-content"] {
  max-width: 65ch;
}