diff --git a/apps/docs/src/components/button.stories.tsx b/apps/docs/src/components/button.stories.tsx
index fb20f9a..ab7aa65 100644
--- a/apps/docs/src/components/button.stories.tsx
+++ b/apps/docs/src/components/button.stories.tsx
@@ -1,6 +1,19 @@
import { Button } from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
+function FavoriteIcon() {
+ return (
+
+ );
+}
+
function getButtonFromCanvas(canvasElement: HTMLElement, name: string) {
const buttons = canvasElement.querySelectorAll("button, a");
@@ -104,7 +117,7 @@ export const Sizes: Story = {
)
diff --git a/apps/docs/src/components/checkbox.stories.tsx b/apps/docs/src/components/checkbox.stories.tsx
index c9a4f65..d5b5e29 100644
--- a/apps/docs/src/components/checkbox.stories.tsx
+++ b/apps/docs/src/components/checkbox.stories.tsx
@@ -5,6 +5,7 @@ const meta = {
title: "Components/Checkbox",
component: Checkbox,
args: {
+ "aria-label": "Checkbox example",
defaultChecked: true
},
argTypes: {
@@ -31,7 +32,9 @@ export default meta;
type Story = StoryObj;
-export const Playground: Story = {};
+export const Playground: Story = {
+ render: (args) =>
+};
export const States: Story = {
render: () => (
diff --git a/apps/docs/src/style-matrix.stories.tsx b/apps/docs/src/style-matrix.stories.tsx
index 71c99fb..7975192 100644
--- a/apps/docs/src/style-matrix.stories.tsx
+++ b/apps/docs/src/style-matrix.stories.tsx
@@ -39,6 +39,20 @@ const motionAccessibilityModes = [
type MotionAccessibilityMode = (typeof motionAccessibilityModes)[number]["value"];
+function ClosePreviewIcon() {
+ return (
+
+ );
+}
+
function RuntimePill({ children }: { children: React.ReactNode }) {
return (
@@ -71,7 +85,7 @@ function PanelPreview() {
diff --git a/packages/ui/src/components/checkbox.test.tsx b/packages/ui/src/components/checkbox.test.tsx
index 74a62a1..98b8448 100644
--- a/packages/ui/src/components/checkbox.test.tsx
+++ b/packages/ui/src/components/checkbox.test.tsx
@@ -21,7 +21,7 @@ describe("Checkbox", () => {
expect(checkbox).toBeChecked();
expect(checkbox).toHaveAttribute("data-state", "checked");
expect(onCheckedChange).toHaveBeenCalledWith(true);
- expect(screen.getByText("✓")).toHaveAttribute("data-slot", "icon");
+ expect(checkbox.querySelector('[data-slot="icon"] svg')).toBeInTheDocument();
});
it("supports keyboard interaction", async () => {
diff --git a/packages/ui/src/components/checkbox.tsx b/packages/ui/src/components/checkbox.tsx
index 763890f..67fc525 100644
--- a/packages/ui/src/components/checkbox.tsx
+++ b/packages/ui/src/components/checkbox.tsx
@@ -4,6 +4,7 @@ import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "reac
import { checkboxIndicatorVariants, checkboxVariants } from "./checkbox.variants";
import { cn } from "../lib/cn";
import { createDataAttributes, createSlot } from "../lib/contracts";
+import { CheckIcon } from "../lib/icons";
export type CheckboxProps = ComponentPropsWithoutRef
& {
invalid?: boolean;
@@ -30,7 +31,7 @@ export const Checkbox = forwardRef<
{...createSlot("icon")}
className={checkboxIndicatorVariants()}
>
- ✓
+
);
diff --git a/packages/ui/src/components/combobox.test.tsx b/packages/ui/src/components/combobox.test.tsx
index 64480ad..16099d7 100644
--- a/packages/ui/src/components/combobox.test.tsx
+++ b/packages/ui/src/components/combobox.test.tsx
@@ -55,6 +55,7 @@ describe("Combobox", () => {
expect(trigger).toHaveTextContent("Design review");
expect(trigger).toHaveAttribute("data-slot", "trigger");
+ expect(trigger).not.toHaveAttribute("aria-controls");
await user.click(trigger);
@@ -65,6 +66,7 @@ describe("Combobox", () => {
const option = within(listbox).getByRole("option", { name: /Legal review/i });
expect(listbox).toHaveAttribute("data-slot", "list");
+ expect(trigger).toHaveAttribute("aria-controls", listbox.id);
expect(screen.getByText("Specialist lanes")).toHaveAttribute("data-slot", "label");
await user.click(option);
diff --git a/packages/ui/src/components/combobox.tsx b/packages/ui/src/components/combobox.tsx
index cfe17a3..0016c07 100644
--- a/packages/ui/src/components/combobox.tsx
+++ b/packages/ui/src/components/combobox.tsx
@@ -22,6 +22,7 @@ import {
} from "./combobox.variants";
import { cn } from "../lib/cn";
import { createDataAttributes, createSlot } from "../lib/contracts";
+import { CheckIcon, ChevronDownIcon } from "../lib/icons";
import { useFieldContext } from "./field";
function mergeIds(...ids: Array) {
@@ -143,6 +144,7 @@ export const Combobox = forwardRef(function Co
return haystack.includes(query);
});
}, [items, resolvedSearchValue]);
+ const listboxId = filteredItems.length > 0 ? `${controlId}-listbox` : undefined;
const groupedItems = useMemo(() => {
const groups = new Map();
@@ -305,7 +307,7 @@ export const Combobox = forwardRef(function Co
invalid: resolvedInvalid,
placeholder: selectedItem ? undefined : true
})}
- aria-controls={`${controlId}-listbox`}
+ aria-controls={resolvedOpen ? listboxId : undefined}
aria-describedby={describedBy}
aria-expanded={resolvedOpen}
aria-invalid={resolvedInvalid || undefined}
@@ -326,7 +328,7 @@ export const Combobox = forwardRef(function Co
aria-hidden="true"
className="shrink-0 text-[var(--color-muted-foreground)]"
>
- ▾
+
@@ -359,7 +361,7 @@ export const Combobox = forwardRef(function Co
{groupedItems.map(([group, groupItems]) => (
@@ -405,11 +407,11 @@ export const Combobox = forwardRef
(function Co
{...createSlot("icon")}
aria-hidden="true"
className={cn(
- "mt-0.5 inline-flex size-4 shrink-0 items-center justify-center text-xs text-[var(--color-primary)]",
+ "mt-0.5 inline-flex size-4 shrink-0 items-center justify-center text-[var(--color-primary)]",
!isSelected && "opacity-0"
)}
>
- ✓
+
{item.label}
diff --git a/packages/ui/src/components/data-table.tsx b/packages/ui/src/components/data-table.tsx
index c3e63b7..1ede175 100644
--- a/packages/ui/src/components/data-table.tsx
+++ b/packages/ui/src/components/data-table.tsx
@@ -40,6 +40,11 @@ import { Spinner } from "./spinner";
import { cn } from "../lib/cn";
import type { VariantProps } from "../lib/cva";
import { createDataAttributes, createSlot } from "../lib/contracts";
+import {
+ SortAscendingIcon,
+ SortDescendingIcon,
+ SortUnsortedIcon
+} from "../lib/icons";
import {
dataTableBodyVariants,
dataTableCellVariants,
@@ -546,9 +551,15 @@ function DataTableInner(
{flexRender(header.column.columnDef.header, header.getContext())}
- {sortState === "asc" ? "↑" : sortState === "desc" ? "↓" : "↕"}
+ {sortState === "asc" ? (
+
+ ) : sortState === "desc" ? (
+
+ ) : (
+
+ )}
Sort by{" "}
diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx
index e9a82c3..49ca0f5 100644
--- a/packages/ui/src/components/dialog.tsx
+++ b/packages/ui/src/components/dialog.tsx
@@ -5,6 +5,7 @@ import { dialogContentVariants, dialogFooterVariants, dialogHeaderVariants, dial
import { cn } from "../lib/cn";
import type { VariantProps } from "../lib/cva";
import { createDataAttributes, createSlot } from "../lib/contracts";
+import { CloseIcon } from "../lib/icons";
export const Dialog = DialogPrimitive.Root;
export const DialogTrigger = DialogPrimitive.Trigger;
@@ -47,9 +48,7 @@ export const DialogContent = forwardRef<
aria-label="Close dialog"
className="absolute right-4 top-4 inline-flex size-9 items-center justify-center rounded-[var(--ui-control-radius)] border border-transparent text-[var(--color-muted-foreground)] outline-none transition-colors duration-[var(--dur-fast)] ease-[var(--ease-standard)] hover:border-[var(--ui-control-border)] hover:bg-[var(--ui-control-bg)] hover:text-[var(--color-foreground)] hover:shadow-[var(--ui-control-shadow)] focus-visible:ring-2 focus-visible:ring-[var(--color-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ui-panel-bg)]"
>
-
- ×
-
+
diff --git a/packages/ui/src/components/dropdown-menu.tsx b/packages/ui/src/components/dropdown-menu.tsx
index 604e913..fe07a59 100644
--- a/packages/ui/src/components/dropdown-menu.tsx
+++ b/packages/ui/src/components/dropdown-menu.tsx
@@ -15,6 +15,7 @@ import {
import { cn } from "../lib/cn";
import type { VariantProps } from "../lib/cva";
import { createDataAttributes, createSlot } from "../lib/contracts";
+import { CheckIcon, ChevronRightIcon, DotIcon } from "../lib/icons";
export const DropdownMenu = DropdownMenuPrimitive.Root;
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
@@ -117,9 +118,11 @@ export const DropdownMenuCheckboxItem = forwardRef<
>
- ✓
+
+
+
{children}
@@ -147,9 +150,11 @@ export const DropdownMenuRadioItem = forwardRef<
>
- •
+
+
+
{children}
@@ -209,7 +214,7 @@ export const DropdownMenuSubTrigger = forwardRef<
ref={ref}
>
{children}
- ›
+
);
});
diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/select.tsx
index 431947f..81fc3e1 100644
--- a/packages/ui/src/components/select.tsx
+++ b/packages/ui/src/components/select.tsx
@@ -11,6 +11,7 @@ import {
} from "./select.variants";
import { cn } from "../lib/cn";
import { createDataAttributes, createSlot } from "../lib/contracts";
+import { CheckIcon, ChevronDownIcon } from "../lib/icons";
import { useFieldContext } from "./field";
function mergeIds(...ids: Array) {
@@ -57,7 +58,7 @@ export const SelectTrigger = forwardRef<
{...createSlot("icon")}
className="text-[var(--color-muted-foreground)]"
>
- ▾
+
);
@@ -117,9 +118,11 @@ export const SelectItem = forwardRef<
>
- ✓
+
+
+
{children}
diff --git a/packages/ui/src/components/sheet.tsx b/packages/ui/src/components/sheet.tsx
index 799b255..001c1a0 100644
--- a/packages/ui/src/components/sheet.tsx
+++ b/packages/ui/src/components/sheet.tsx
@@ -10,6 +10,7 @@ import {
import { cn } from "../lib/cn";
import type { VariantProps } from "../lib/cva";
import { createDataAttributes, createSlot } from "../lib/contracts";
+import { CloseIcon } from "../lib/icons";
export const Sheet = DialogPrimitive.Root;
export const SheetTrigger = DialogPrimitive.Trigger;
@@ -64,9 +65,7 @@ export const SheetContent = forwardRef<
aria-label="Close sheet"
className="absolute right-4 top-4 inline-flex size-9 items-center justify-center rounded-[var(--ui-control-radius)] border border-transparent text-[var(--color-muted-foreground)] outline-none transition-colors duration-[var(--dur-fast)] ease-[var(--ease-standard)] hover:border-[var(--ui-control-border)] hover:bg-[var(--ui-control-bg)] hover:text-[var(--color-foreground)] hover:shadow-[var(--ui-control-shadow)] focus-visible:ring-2 focus-visible:ring-[var(--color-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ui-panel-bg)]"
>
-
- ×
-
+
diff --git a/packages/ui/src/components/toast.tsx b/packages/ui/src/components/toast.tsx
index 105b61c..f55fd2c 100644
--- a/packages/ui/src/components/toast.tsx
+++ b/packages/ui/src/components/toast.tsx
@@ -5,6 +5,7 @@ import { toastActionVariants, toastCloseVariants, toastVariants, toastViewportVa
import { cn } from "../lib/cn";
import type { VariantProps } from "../lib/cva";
import { createDataAttributes, createSlot } from "../lib/contracts";
+import { CloseIcon } from "../lib/icons";
export const ToastProvider = ToastPrimitive.Provider;
@@ -95,9 +96,7 @@ export const ToastClose = forwardRef<
ref={ref}
>
{children ?? (
-
- ×
-
+
)}
);
diff --git a/packages/ui/src/lib/icons.tsx b/packages/ui/src/lib/icons.tsx
new file mode 100644
index 0000000..f91202b
--- /dev/null
+++ b/packages/ui/src/lib/icons.tsx
@@ -0,0 +1,133 @@
+import type { ComponentPropsWithoutRef, ReactNode } from "react";
+
+import { cn } from "./cn";
+
+type IconProps = ComponentPropsWithoutRef<"svg">;
+
+function IconFrame({
+ children,
+ className,
+ viewBox = "0 0 16 16",
+ ...props
+}: IconProps & {
+ children: ReactNode;
+ viewBox?: string;
+}) {
+ return (
+
+ );
+}
+
+export function CheckIcon({ className, ...props }: IconProps) {
+ return (
+
+
+
+ );
+}
+
+export function ChevronDownIcon({ className, ...props }: IconProps) {
+ return (
+
+
+
+ );
+}
+
+export function ChevronRightIcon({ className, ...props }: IconProps) {
+ return (
+
+
+
+ );
+}
+
+export function CloseIcon({ className, ...props }: IconProps) {
+ return (
+
+
+
+ );
+}
+
+export function DotIcon({ className, ...props }: IconProps) {
+ return (
+
+
+
+ );
+}
+
+export function SortAscendingIcon({ className, ...props }: IconProps) {
+ return (
+
+
+
+ );
+}
+
+export function SortDescendingIcon({ className, ...props }: IconProps) {
+ return (
+
+
+
+ );
+}
+
+export function SortUnsortedIcon({ className, ...props }: IconProps) {
+ return (
+
+
+
+ );
+}