diff --git a/packages/elements/__tests__/prompt-input.test.tsx b/packages/elements/__tests__/prompt-input.test.tsx
index d961a3ba..0b73e824 100644
--- a/packages/elements/__tests__/prompt-input.test.tsx
+++ b/packages/elements/__tests__/prompt-input.test.tsx
@@ -4,6 +4,7 @@ import { userEvent } from "@testing-library/user-event";
import React from "react";
import type { AttachmentData } from "../src/attachments";
+
import {
Attachment,
AttachmentInfo,
@@ -785,7 +786,80 @@ describe("promptInputButton", () => {
);
- expect(screen.getByRole("button", { name: "Action" })).toBeInTheDocument();
+ const button = screen.getByRole("button", { name: "Action" });
+ expect(button).toBeInTheDocument();
+ expect(button).toHaveAttribute("data-size", "sm");
+ expect(button.className).not.toContain("size-8");
+ });
+
+ it("sizes a single wrapped text label as a text button", () => {
+ setupPromptInputTests();
+ const onSubmit = vi.fn();
+ render(
+
+
+
+ Label
+
+
+
+ );
+ const button = screen.getByRole("button", { name: "Label" });
+ expect(button).toHaveAttribute("data-size", "sm");
+ expect(button.className).not.toContain("size-8");
+ });
+
+ it("sizes a lone icon as a square icon button", () => {
+ setupPromptInputTests();
+ const onSubmit = vi.fn();
+ render(
+
+
+
+
+
+
+
+ );
+ const button = screen.getByRole("button", { name: "Icon" });
+ expect(button).toHaveAttribute("data-size", "icon-sm");
+ expect(button.className).toContain("size-8");
+ });
+
+ it("sizes icon-plus-text as a text button", () => {
+ setupPromptInputTests();
+ const onSubmit = vi.fn();
+ render(
+
+
+
+
+ Search
+
+
+
+ );
+ const button = screen.getByRole("button", { name: /search/i });
+ expect(button).toHaveAttribute("data-size", "sm");
+ });
+
+ it("respects an explicit size prop over the heuristic", () => {
+ setupPromptInputTests();
+ const onSubmit = vi.fn();
+ render(
+
+
+ Action
+
+
+ );
+ const button = screen.getByRole("button", { name: "Action" });
+ expect(button).toHaveAttribute("data-size", "icon-sm");
+ expect(button.className).toContain("size-8");
});
it("renders button with string tooltip", async () => {
diff --git a/packages/elements/src/prompt-input.tsx b/packages/elements/src/prompt-input.tsx
index 412c846d..619051e9 100644
--- a/packages/elements/src/prompt-input.tsx
+++ b/packages/elements/src/prompt-input.tsx
@@ -1,5 +1,20 @@
"use client";
+import type { ChatStatus, FileUIPart, SourceDocumentUIPart } from "ai";
+import type {
+ ChangeEvent,
+ ChangeEventHandler,
+ ClipboardEventHandler,
+ ComponentProps,
+ FormEvent,
+ FormEventHandler,
+ HTMLAttributes,
+ KeyboardEventHandler,
+ PropsWithChildren,
+ ReactNode,
+ RefObject,
+} from "react";
+
import {
Command,
CommandEmpty,
@@ -40,7 +55,6 @@ import {
TooltipTrigger,
} from "@repo/shadcn-ui/components/ui/tooltip";
import { cn } from "@repo/shadcn-ui/lib/utils";
-import type { ChatStatus, FileUIPart, SourceDocumentUIPart } from "ai";
import {
CornerDownLeftIcon,
ImageIcon,
@@ -50,22 +64,10 @@ import {
XIcon,
} from "lucide-react";
import { nanoid } from "nanoid";
-import type {
- ChangeEvent,
- ChangeEventHandler,
- ClipboardEventHandler,
- ComponentProps,
- FormEvent,
- FormEventHandler,
- HTMLAttributes,
- KeyboardEventHandler,
- PropsWithChildren,
- ReactNode,
- RefObject,
-} from "react";
import {
Children,
createContext,
+ isValidElement,
useCallback,
useContext,
useEffect,
@@ -78,6 +80,20 @@ import {
// Helpers
// ============================================================================
+const hasTextContent = (node: ReactNode): boolean => {
+ if (typeof node === "string" || typeof node === "number") {
+ return node !== "";
+ }
+ if (Array.isArray(node)) {
+ return node.some(hasTextContent);
+ }
+ if (isValidElement<{ children?: ReactNode }>(node)) {
+ // Text inside