npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@speakai/ui

v0.13.0

Published

Speak AI design system — React + Tailwind UI components with dark/light mode, WCAG AA, and full CSS variable customization

Readme

@speakai/ui

npm version npm downloads license

Speak AI's design system — 45+ React + Tailwind components with dark/light mode, WCAG AA accessibility, and full CSS variable customization.

LLM NOTE: This README is the authoritative reference for every component, prop, type, and theming rule. When generating code with @speakai/ui, consult the prop tables below — do not guess props. All color styling must use CSS variable tokens (e.g. text-foreground, bg-background). Never use hardcoded Tailwind color classes (text-gray-500, bg-white, bg-zinc-900, etc.) — these break dark mode.


Table of Contents


Install

npm install @speakai/ui

Setup

1. Import styles

/* globals.css */
@import "@speakai/ui/styles.css";

2. Configure Tailwind

// tailwind.config.ts
export default {
  content: [
    "./src/**/*.{ts,tsx}",
    "./node_modules/@speakai/ui/dist/**/*.{js,mjs}",
  ],
};

3. Dark mode

The library uses class-based dark mode (.dark on <html>). Works with next-themes:

// app/layout.tsx
import { ThemeProvider } from "next-themes";

export default function RootLayout({ children }) {
  return (
    <html suppressHydrationWarning>
      <body>
        <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
          <ToastProvider>{children}</ToastProvider>
        </ThemeProvider>
      </body>
    </html>
  );
}

4. Sub-path imports (recommended)

Each component is available as a standalone sub-path export. This avoids pulling the entire barrel and significantly improves cold compilation in bundlers like Turbopack.

// Instead of: import { Button, Card } from "@speakai/ui"
import { Button } from "@speakai/ui/button";
import { Card } from "@speakai/ui/card";
import { Dialog, DialogHeader } from "@speakai/ui/dialog";
import { cn } from "@speakai/ui/cn";

The barrel import (from "@speakai/ui") still works for backward compatibility.

| Sub-path | Components | |---|---| | @speakai/ui/accordion | Accordion, AccordionItem | | @speakai/ui/auth-card | AuthCard | | @speakai/ui/auth-divider | AuthDivider | | @speakai/ui/avatar | Avatar | | @speakai/ui/badge | Badge, StatusBadge | | @speakai/ui/bottom-sheet | BottomSheet | | @speakai/ui/breadcrumb | Breadcrumb | | @speakai/ui/button | Button | | @speakai/ui/card | Card | | @speakai/ui/checkbox | Checkbox | | @speakai/ui/chips | Chips | | @speakai/ui/cn | cn (utility) | | @speakai/ui/color-picker | ColorPicker | | @speakai/ui/confirm-dialog | ConfirmDialog | | @speakai/ui/date-picker | DatePicker | | @speakai/ui/dialog | Dialog, DialogHeader, DialogBody, DialogFooter, DialogCloseButton | | @speakai/ui/dropdown-menu | DropdownMenu, DropdownMenuItem, DropdownMenuHeader, DropdownMenuDivider, MoreButton | | @speakai/ui/empty-state | EmptyState | | @speakai/ui/error-state | ErrorState | | @speakai/ui/file-dropzone | FileDropzone | | @speakai/ui/image-uploader | ImageUploader | | @speakai/ui/info-card | InfoCard | | @speakai/ui/input | Input, SearchInput, Select, Textarea | | @speakai/ui/language-selector | LanguageSelector | | @speakai/ui/media | MediaPlayer, useMediaSync | | @speakai/ui/otp-input | OTPInput | | @speakai/ui/page-header | PageHeader, SectionHeader | | @speakai/ui/password-input | PasswordInput | | @speakai/ui/phone-input | PhoneInput | | @speakai/ui/popover | Popover | | @speakai/ui/progress | Progress | | @speakai/ui/radio-group | RadioGroup | | @speakai/ui/side-panel | SidePanel | | @speakai/ui/sidebar | Sidebar, SidebarProvider, SidebarLayout, SidebarUser, useSidebar | | @speakai/ui/skeleton | Skeleton, SkeletonText, PageHeaderSkeleton, StatCardSkeleton, StatCardsSkeletonGrid, PageSkeleton, CardSkeleton, GridSkeleton, FormSkeleton | | @speakai/ui/slider | Slider | | @speakai/ui/sso-button | SSOButton, SSOButtons | | @speakai/ui/stat-card | StatCard, StatCardGrid | | @speakai/ui/stepper | Stepper | | @speakai/ui/switch | Switch | | @speakai/ui/table | Table, TableHeader, TableBody, TableRow, TableHead, TableCell, TableSkeleton, TableActions, TableActionButton, TableSortHead, TablePagination, TableEmpty, useSort | | @speakai/ui/tabs | Tabs, TabsList, TabsTrigger, TabsContent | | @speakai/ui/theme-toggle | ThemeToggle, ThemeSelector | | @speakai/ui/time-picker | TimePicker | | @speakai/ui/toast | ToastContainer, ToastProvider, useToast | | @speakai/ui/tooltip | Tooltip | | @speakai/ui/transcript | TranscriptView |


Theming Rules

CRITICAL — Read before writing any JSX with this library.

CSS Variables — Full Reference

All color and shape tokens are CSS variables. They flip automatically between light and dark mode.

:root {
  /* Brand */
  --primary: 271 91% 65%;              /* purple */
  --primary-foreground: 0 0% 100%;

  /* Gradient */
  --gradient-from: 271 91% 65%;        /* purple */
  --gradient-to: 330 81% 60%;          /* pink */
  --gradient-accent: 24 95% 53%;       /* orange */

  /* Status */
  --danger: 0 84% 60%;
  --danger-foreground: 0 0% 100%;
  --success: 160 84% 39%;
  --success-foreground: 0 0% 100%;
  --warning: 38 92% 50%;
  --warning-foreground: 0 0% 0%;
  --info: 217 91% 60%;
  --info-foreground: 0 0% 100%;

  /* Surfaces */
  --background: 0 0% 100%;             /* page background */
  --foreground: 222 84% 5%;            /* default text */
  --card: 0 0% 100%;                   /* card surface */
  --card-foreground: 222 84% 5%;
  --popover: 0 0% 100%;                /* dropdown/tooltip bg */
  --popover-foreground: 222 84% 5%;
  --secondary: 210 40% 96%;
  --secondary-foreground: 222 47% 11%;
  --muted: 210 40% 96%;                /* subtle backgrounds */
  --muted-foreground: 215 16% 47%;     /* placeholder / help text */
  --accent: 210 40% 96%;               /* hover highlight */
  --accent-foreground: 222 47% 11%;
  --border: 214 32% 91%;               /* borders */
  --input: 214 32% 91%;                /* input borders */
  --ring: 271 91% 65%;                 /* focus ring */

  /* Skeleton */
  --skeleton-bg: 220 14% 91%;
  --skeleton-highlight: 220 14% 96%;

  /* Typography */
  --font-sans: "Inter", sans-serif;
  --font-mono: "JetBrains Mono", monospace;

  /* Shape */
  --radius: 0.75rem;                   /* border-radius base */
}

Tailwind Token → Class Mapping

| Token | Tailwind class | |-------|---------------| | --background | bg-background / text-background | | --foreground | text-foreground | | --primary | bg-primary / text-primary / border-primary | | --primary-foreground | text-primary-foreground | | --card | bg-card | | --card-foreground | text-card-foreground | | --popover | bg-popover | | --popover-foreground | text-popover-foreground | | --muted | bg-muted | | --muted-foreground | text-muted-foreground | | --accent | bg-accent | | --accent-foreground | text-accent-foreground | | --border | border-border | | --input | border-input | | --ring | ring-ring | | --danger | text-danger / bg-danger / border-danger | | --success | text-success / bg-success | | --warning | text-warning / bg-warning | | --info | text-info / bg-info |

Correct vs Incorrect Color Usage

// ❌ WRONG — hardcoded colors break dark mode
<div className="bg-white text-gray-900 border-gray-200">
<p className="text-gray-500">Help text</p>
<button className="bg-zinc-800 text-white">Submit</button>

// ✅ CORRECT — CSS variables flip automatically
<div className="bg-background text-foreground border-border">
<p className="text-muted-foreground">Help text</p>
<button className="bg-foreground text-background">Submit</button>

Override Your Brand

:root {
  --primary: 210 80% 50%;        /* your brand color (HSL) */
  --gradient-from: 210 80% 50%;
  --gradient-to: 170 80% 50%;
  --radius: 0.5rem;              /* sharper corners */
  --font-sans: "Geist", sans-serif;
}

Quick Reference — All Exports

Sub-path imports (recommended) — each import pulls only that component's code:

// Auth
import { AuthCard } from "@speakai/ui/auth-card";
import { SSOButton, SSOButtons } from "@speakai/ui/sso-button";
import { PasswordInput } from "@speakai/ui/password-input";
import { OTPInput } from "@speakai/ui/otp-input";
import { AuthDivider } from "@speakai/ui/auth-divider";

// Forms
import { Button } from "@speakai/ui/button";
import { Input, SearchInput, Select, Textarea } from "@speakai/ui/input";
import { Switch } from "@speakai/ui/switch";
import { Checkbox } from "@speakai/ui/checkbox";
import { RadioGroup } from "@speakai/ui/radio-group";
import { ColorPicker } from "@speakai/ui/color-picker";
import { ImageUploader } from "@speakai/ui/image-uploader";
import { FileDropzone } from "@speakai/ui/file-dropzone";
import { Chips } from "@speakai/ui/chips";
import { Slider } from "@speakai/ui/slider";

// Layout
import { Card } from "@speakai/ui/card";
import { Sidebar, SidebarProvider, SidebarLayout, SidebarUser } from "@speakai/ui/sidebar";
import { SidePanel } from "@speakai/ui/side-panel";
import { PageHeader, SectionHeader } from "@speakai/ui/page-header";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@speakai/ui/tabs";
import { Accordion, AccordionItem } from "@speakai/ui/accordion";
import { Stepper } from "@speakai/ui/stepper";

// Data Display
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell, TableSortHead, TableActions, TableActionButton, TableEmpty, TablePagination, TableSkeleton } from "@speakai/ui/table";
import { StatCard, StatCardGrid } from "@speakai/ui/stat-card";
import { Badge, StatusBadge } from "@speakai/ui/badge";
import { InfoCard } from "@speakai/ui/info-card";
import { Breadcrumb } from "@speakai/ui/breadcrumb";

// Feedback
import { ToastProvider, useToast } from "@speakai/ui/toast";
import { Dialog, DialogHeader, DialogBody, DialogFooter, DialogCloseButton } from "@speakai/ui/dialog";
import { ConfirmDialog } from "@speakai/ui/confirm-dialog";
import { BottomSheet } from "@speakai/ui/bottom-sheet";
import { EmptyState } from "@speakai/ui/empty-state";
import { ErrorState } from "@speakai/ui/error-state";

// Skeleton
import { Skeleton, SkeletonText, PageHeaderSkeleton, StatCardSkeleton, StatCardsSkeletonGrid, PageSkeleton, CardSkeleton, GridSkeleton, FormSkeleton } from "@speakai/ui/skeleton";

// Selectors
import { LanguageSelector } from "@speakai/ui/language-selector";
import { PhoneInput } from "@speakai/ui/phone-input";
import { DatePicker } from "@speakai/ui/date-picker";
import { TimePicker } from "@speakai/ui/time-picker";

// Utilities
import { Tooltip } from "@speakai/ui/tooltip";
import { Popover } from "@speakai/ui/popover";
import { DropdownMenu, DropdownMenuItem, DropdownMenuDivider, DropdownMenuHeader, MoreButton } from "@speakai/ui/dropdown-menu";
import { Avatar } from "@speakai/ui/avatar";
import { Progress } from "@speakai/ui/progress";
import { ThemeToggle, ThemeSelector } from "@speakai/ui/theme-toggle";
import { cn } from "@speakai/ui/cn";

// Media (separate entry — avoids prosemirror dependency)
import { MediaPlayer, useMediaSync } from "@speakai/ui/media";
import {
  Button, Card, Badge, Input, Table, Dialog, // ... all 75+ exports
} from "@speakai/ui";

Component Reference

All components support the className prop for Tailwind overrides unless noted. All accept ref via forwardRef.


Auth Components

AuthCard

Centered card layout for login, register, and verify pages.

<AuthCard logo={<Logo />} title="Welcome back" subtitle="Sign in to continue">
  <Input placeholder="Email" />
  <Button variant="primary">Sign In</Button>
</AuthCard>

| Prop | Type | Default | Required | |------|------|---------|----------| | title | string | — | Yes | | subtitle | string | — | No | | logo | ReactNode | — | No | | children | ReactNode | — | No | | className | string | — | No |


SSOButton

Single SSO provider button with built-in icon.

<SSOButton provider="google" action="login" onClick={handleGoogle} />

| Prop | Type | Default | Required | |------|------|---------|----------| | provider | "google" \| "apple" \| "microsoft" | — | Yes | | action | "login" \| "register" | "login" | No | | isLoading | boolean | false | No | | disabled | boolean | — | No |


SSOButtons

Renders all three SSO buttons (Google, Apple, Microsoft) with a shared click handler.

<SSOButtons action="register" onProviderClick={(p) => handleSSO(p)} />

| Prop | Type | Default | Required | |------|------|---------|----------| | action | "login" \| "register" | "login" | No | | onProviderClick | (provider: "google" \| "apple" \| "microsoft") => void | — | No | | isLoading | boolean | false | No | | disabled | boolean | — | No | | className | string | — | No |


PasswordInput

Input with show/hide password toggle.

<PasswordInput
  label="Password"
  placeholder="Enter password"
  error="Password is too short"
/>

| Prop | Type | Default | Required | |------|------|---------|----------| | error | boolean \| string | — | No | | label | string | — | No | | disabled | boolean | — | No |

Accepts all native <input> props.


OTPInput

Multi-digit code input with auto-focus advance and paste support.

<OTPInput length={6} value={code} onChange={setCode} autoFocus />
<OTPInput length={4} value={code} onChange={setCode} error="Invalid code" />

| Prop | Type | Default | Required | |------|------|---------|----------| | value | string | — | Yes | | onChange | (value: string) => void | — | Yes | | length | number | 6 | No | | error | boolean \| string | — | No | | disabled | boolean | false | No | | autoFocus | boolean | false | No |


AuthDivider

Horizontal rule with centered label for separating auth options.

<AuthDivider />                  {/* renders "or" */}
<AuthDivider text="continue with" />

| Prop | Type | Default | Required | |------|------|---------|----------| | text | string | "or" | No | | className | string | — | No |


Form Components

Button

Primary action element. Extend all native <button> props.

<Button variant="primary" size="md">Save Changes</Button>
<Button variant="gradient" isLoading={saving}>Submit</Button>
<Button variant="danger" size="sm">Delete</Button>
<Button variant="ghost" size="icon"><TrashIcon /></Button>

| Prop | Type | Default | Required | |------|------|---------|----------| | variant | "primary" \| "secondary" \| "danger" \| "ghost" \| "outline" \| "gradient" \| "glass" \| "solid" | "primary" | No | | size | "sm" \| "md" \| "lg" \| "icon" | "md" | No | | isLoading | boolean | false | No | | disabled | boolean | — | No | | children | ReactNode | — | No |

Size reference: sm = h-9, md = h-10, lg = h-11, icon = h-10 w-10


Input

Standard text input. Extends all native <input> props.

<Input placeholder="Email address" />
<Input error={true} />                          {/* red border only */}
<Input error="Email is required" />             {/* red border + message */}
<Input label="Full name" value={name} onChange={e => setName(e.target.value)} />

| Prop | Type | Default | Required | |------|------|---------|----------| | error | boolean \| string | — | No | | label | string | — | No | | disabled | boolean | — | No |


SearchInput

Input pre-configured with a search icon on the left.

<SearchInput
  placeholder="Search agents..."
  value={query}
  onChange={e => setQuery(e.target.value)}
  containerClassName="w-64"
/>

| Prop | Type | Default | Required | |------|------|---------|----------| | containerClassName | string | — | No |

Accepts all native <input> props.


Select

Dropdown selector. Supports both options array and children syntax.

{/* Options array */}
<Select
  options={[{ value: "en", label: "English" }, { value: "es", label: "Spanish" }]}
  placeholder="Select language"
  value={lang}
  onChange={e => setLang(e.target.value)}
/>

{/* Children syntax */}
<Select value={role} onChange={e => setRole(e.target.value)}>
  <option value="admin">Admin</option>
  <option value="member">Member</option>
</Select>

| Prop | Type | Default | Required | |------|------|---------|----------| | options | Array<{ value: string; label: string }> | — | No | | placeholder | string | — | No | | error | boolean \| string | — | No | | label | string | — | No | | disabled | boolean | — | No | | children | ReactNode | — | No |


Textarea

Multi-line text input. Extends all native <textarea> props.

<Textarea placeholder="Describe your agent..." rows={4} />
<Textarea error="Description is required" label="Bio" />

| Prop | Type | Default | Required | |------|------|---------|----------| | error | boolean \| string | — | No | | label | string | — | No | | rows | number | — | No | | disabled | boolean | — | No |


Switch

Toggle control.

<Switch checked={enabled} onChange={setEnabled} label="Enable notifications" />
<Switch checked={active} onChange={setActive} size="sm" />

| Prop | Type | Default | Required | |------|------|---------|----------| | checked | boolean | — | Yes | | onChange | (checked: boolean) => void | — | Yes | | size | "sm" \| "default" | "default" | No | | label | string | — | No | | disabled | boolean | false | No | | id | string | — | No |


Checkbox

Checkbox with optional label and description.

<Checkbox
  checked={agreed}
  onChange={e => setAgreed(e.target.checked)}
  label="I agree to the terms"
  description="By checking this box you agree..."
/>
<Checkbox size="sm" error="You must agree to continue" />

| Prop | Type | Default | Required | |------|------|---------|----------| | checked | boolean | — | No | | onChange | (e: ChangeEvent<HTMLInputElement>) => void | — | No | | label | string | — | No | | description | string | — | No | | size | "sm" \| "default" \| "lg" | "default" | No | | error | boolean \| string | — | No | | disabled | boolean | — | No | | id | string | — | No |


RadioGroup

Group of radio buttons with keyboard navigation. Supports default (flat list) and card (bordered selection) variants.

{/* Default — flat list */}
<RadioGroup
  value={plan}
  onChange={setPlan}
  options={[
    { value: "free", label: "Free", description: "For personal use" },
    { value: "pro", label: "Pro", description: "For teams" },
    { value: "enterprise", label: "Enterprise", disabled: true },
  ]}
/>

{/* Card variant — bordered containers with selection highlighting */}
<RadioGroup
  variant="card"
  value={timing}
  onChange={setTiming}
  options={[
    { value: "start", label: "At Start", description: "Collect before conversation begins" },
    { value: "during", label: "During", description: "Collect naturally during conversation" },
    { value: "end", label: "At End", description: "Collect before ending" },
  ]}
/>

| Prop | Type | Default | Required | |------|------|---------|----------| | value | string | — | Yes | | onChange | (value: string) => void | — | Yes | | options | RadioOption[] | — | Yes | | orientation | "horizontal" \| "vertical" | "vertical" | No | | variant | "default" \| "card" | "default" | No | | name | string | — | No | | disabled | boolean | false | No |

RadioOption interface:

interface RadioOption {
  value: string;
  label: string;
  description?: string;
  disabled?: boolean;
}

Card variant: Each option renders inside a bordered container. Selected option gets border-primary bg-primary/5. Unselected options get border-border with hover effect. Use for settings/config where each option needs visual separation.


ColorPicker

Native color picker with hex input and optional preset swatches.

<ColorPicker
  value={color}
  onChange={setColor}
  label="Brand color"
  presetColors={["#a855f7", "#3b82f6", "#ef4444", "#10b981"]}
  showInput={true}
/>

| Prop | Type | Default | Required | |------|------|---------|----------| | value | string | — | Yes | | onChange | (color: string) => void | — | Yes | | presetColors | string[] | — | No | | showInput | boolean | true | No | | label | string | — | No | | disabled | boolean | false | No |


ImageUploader

Drag-and-drop image upload with preview and remove button.

<ImageUploader
  value={imageUrl}
  onChange={(file) => uploadAndSetUrl(file)}
  onRemove={() => setImageUrl(undefined)}
  maxSize={5 * 1024 * 1024}   // 5 MB
  variant="dropzone"
/>
<ImageUploader variant="avatar" value={avatarUrl} onChange={handleUpload} />

| Prop | Type | Default | Required | |------|------|---------|----------| | onChange | (file: File) => void | — | Yes | | value | string | — | No | | onRemove | () => void | — | No | | accept | string | "image/*" | No | | maxSize | number (bytes) | 10485760 (10 MB) | No | | placeholder | string | "Drop an image here or click to browse" | No | | disabled | boolean | false | No | | variant | "dropzone" \| "avatar" | "dropzone" | No |


FileDropzone

Multi-file drag-and-drop upload zone.

<FileDropzone
  onFiles={(files) => handleUpload(files)}
  accept=".pdf,.doc,.docx"
  multiple={true}
  maxSize={20 * 1024 * 1024}
/>

| Prop | Type | Default | Required | |------|------|---------|----------| | onFiles | (files: File[]) => void | — | Yes | | accept | string | — | No | | multiple | boolean | true | No | | maxSize | number (bytes) | — | No | | disabled | boolean | false | No | | children | ReactNode | — | No |


Chips

Tag/chip input with autocomplete dropdown.

<Chips
  value={tags}
  onChange={setTags}
  placeholder="Add tags..."
  suggestions={["react", "typescript", "tailwind", "nextjs"]}
  maxItems={10}
/>

| Prop | Type | Default | Required | |------|------|---------|----------| | value | string[] | — | Yes | | onChange | (value: string[]) => void | — | Yes | | placeholder | string | "Type and press Enter" | No | | suggestions | string[] | — | No | | maxItems | number | — | No | | disabled | boolean | false | No |

Keyboard: Enter/comma to add, Backspace to remove last, Escape to close suggestions.


Slider

Range input with optional label and live value display. Supports inline mode for compact layouts and track overlays for media timelines.

{/* Full-width with label */}
<Slider value={volume} onChange={setVolume} min={0} max={100} label="Volume" />

{/* Without value display */}
<Slider value={temperature} onChange={setTemperature} step={0.1} min={0} max={2} showValue={false} />

{/* Inline — compact, no header, for use in flex rows */}
<div className="flex items-center gap-2">
  <span>Weight: {weight}</span>
  <Slider inline value={weight} onChange={setWeight} min={1} max={10} showValue={false} className="w-20" />
</div>

{/* With track overlay content (markers, annotations) */}
<Slider
  value={position}
  onChange={setPosition}
  min={0}
  max={duration}
  trackChildren={
    <>
      {markers.map(m => (
        <div key={m.time} className="absolute top-0 h-full w-0.5 bg-primary" style={{ left: `${(m.time / duration) * 100}%` }} />
      ))}
    </>
  }
/>

| Prop | Type | Default | Required | |------|------|---------|----------| | value | number | — | Yes | | onChange | (value: number) => void | — | Yes | | min | number | 0 | No | | max | number | 100 | No | | step | number | 1 | No | | label | string | — | No | | showValue | boolean | true | No | | disabled | boolean | false | No | | inline | boolean | false | No | | trackChildren | ReactNode | — | No |

Inline mode: Renders as inline-flex without the label/value header row. The track fills the container width. Set a fixed width via className="w-20" for compact layouts.

Track children: Content rendered inside the track <div> (positioned relative). Use absolute positioning for markers, annotations, or segment indicators.


Layout Components

Card

Surface container with multiple visual styles.

<Card variant="default">Content</Card>
<Card variant="elevated" showGradientAccent>Featured card</Card>
<Card variant="glass">Glassmorphism effect</Card>

| Prop | Type | Default | Required | |------|------|---------|----------| | variant | "default" \| "outline" \| "elevated" \| "glass" | "default" | No | | showGradientAccent | boolean | false | No | | children | ReactNode | — | No |


SidebarProvider + Sidebar + SidebarLayout

Collapsible sidebar with mobile drawer support.

// Wrap the entire layout
<SidebarProvider>
  <div className="flex h-screen">
    <Sidebar
      sections={[
        {
          id: "main",
          items: [
            { id: "dashboard", label: "Dashboard", icon: <HomeIcon />, href: "/", active: true },
            { id: "agents", label: "Agents", icon: <BotIcon />, href: "/agents" },
          ],
        },
      ]}
      header={<Logo />}
      footer={<SidebarUser name="John Doe" email="[email protected]" />}
      renderLink={({ href, children, className }) => (
        <Link href={href} className={className}>{children}</Link>
      )}
    />
    <SidebarLayout>
      <main>{/* page content */}</main>
    </SidebarLayout>
  </div>
</SidebarProvider>

SidebarProvider props:

| Prop | Type | Default | Required | |------|------|---------|----------| | children | ReactNode | — | Yes | | defaultCollapsed | boolean | false | No | | storageKey | string | "sidebar-collapsed" | No |

Sidebar props:

| Prop | Type | Default | Required | |------|------|---------|----------| | sections | SidebarSection[] | — | Yes | | header | ReactNode | — | No | | footer | ReactNode | — | No | | renderLink | (props: { href: string; children: ReactNode; className?: string }) => ReactNode | — | No | | className | string | — | No |

SidebarSection interface:

interface SidebarSection {
  id: string;
  label?: string;
  items: SidebarItem[];
}

SidebarItem interface:

interface SidebarItem {
  id: string;
  label: string;
  icon?: ReactNode;
  href?: string;
  onClick?: () => void;
  badge?: ReactNode;
  active?: boolean;
  disabled?: boolean;
  children?: SidebarItem[];  // nested items
}

SidebarUser props:

| Prop | Type | Default | Required | |------|------|---------|----------| | name | string | — | Yes | | email | string | — | No | | avatar | ReactNode | — | No | | actions | ReactNode | — | No |

SidebarLayout props:

| Prop | Type | Default | Required | |------|------|---------|----------| | children | ReactNode | — | Yes | | className | string | — | No |


SidePanel

Slide-over drawer from left or right edge.

<SidePanel open={open} onClose={() => setOpen(false)} title="Edit Agent" size="lg">
  <form>...</form>
</SidePanel>

<SidePanel
  open={open}
  onClose={close}
  title="Step 2"
  showBack
  onBack={goBack}
  side="right"
  footer={<div className="flex gap-2"><Button>Save</Button></div>}
>
  ...
</SidePanel>

| Prop | Type | Default | Required | |------|------|---------|----------| | open | boolean | — | Yes | | onClose | () => void | — | Yes | | title | string | — | No | | side | "left" \| "right" | "right" | No | | size | "sm" \| "default" \| "lg" \| "xl" \| "full" | "default" | No | | showBack | boolean | false | No | | onBack | () => void | — | No | | backdrop | boolean | true | No | | header | ReactNode | — | No | | footer | ReactNode | — | No | | children | ReactNode | — | No |


PageHeader

Page-level heading with optional gradient text accent and action slot.

<PageHeader
  title="Voice Agents"
  gradientText="AI-Powered"
  description="Build and manage your AI voice agents."
  action={<Button variant="primary">Create Agent</Button>}
/>

| Prop | Type | Default | Required | |------|------|---------|----------| | title | string | — | Yes | | gradientText | string | — | No | | description | string | — | No | | action | ReactNode | — | No |


SectionHeader

Section-level heading, smaller than PageHeader.

<SectionHeader
  title="Team Members"
  description="Manage who has access."
  action={<Button size="sm">Invite</Button>}
/>

| Prop | Type | Default | Required | |------|------|---------|----------| | title | string | — | Yes | | description | string | — | No | | action | ReactNode | — | No |


Tabs + TabsList + TabsTrigger + TabsContent

Tabbed navigation with keyboard arrow support.

<Tabs defaultTab="overview" onTabChange={setTab}>
  <TabsList variant="underline">
    <TabsTrigger value="overview">Overview</TabsTrigger>
    <TabsTrigger value="settings" icon={<SettingsIcon />}>Settings</TabsTrigger>
    <TabsTrigger value="logs" badge={<Badge variant="info">12</Badge>}>Logs</TabsTrigger>
  </TabsList>
  <TabsContent value="overview"><OverviewPanel /></TabsContent>
  <TabsContent value="settings"><SettingsPanel /></TabsContent>
  <TabsContent value="logs"><LogsPanel /></TabsContent>
</Tabs>

Tabs props:

| Prop | Type | Default | Required | |------|------|---------|----------| | defaultTab | string | — | Yes | | onTabChange | (tab: string) => void | — | No | | children | ReactNode | — | No |

TabsList props:

| Prop | Type | Default | Required | |------|------|---------|----------| | variant | "default" \| "underline" \| "pills" | "default" | No | | children | ReactNode | — | No |

TabsTrigger props:

| Prop | Type | Default | Required | |------|------|---------|----------| | value | string | — | Yes | | variant | "default" \| "underline" \| "pills" | "default" | No | | icon | ReactNode | — | No | | badge | ReactNode | — | No | | disabled | boolean | false | No | | children | ReactNode | — | No |

TabsContent props:

| Prop | Type | Default | Required | |------|------|---------|----------| | value | string | — | Yes | | children | ReactNode | — | No |


Accordion + AccordionItem

Expandable content sections.

<Accordion type="single">
  <AccordionItem title="What is a voice agent?" value="q1">
    A voice agent is an AI-powered...
  </AccordionItem>
  <AccordionItem title="How does billing work?" value="q2">
    Billing is monthly...
  </AccordionItem>
</Accordion>

<Accordion type="multiple" defaultOpen={["q1"]}>
  ...
</Accordion>

Accordion props:

| Prop | Type | Default | Required | |------|------|---------|----------| | type | "single" \| "multiple" | "single" | No | | defaultOpen | string[] | [] | No | | children | ReactNode | — | No |

AccordionItem props:

| Prop | Type | Default | Required | |------|------|---------|----------| | title | string | — | Yes | | children | ReactNode | — | Yes | | value | string | — | No | | defaultOpen | boolean | — | No | | disabled | boolean | false | No |


Stepper

Step progress indicator.

<Stepper
  currentStep={1}
  orientation="horizontal"
  steps={[
    { label: "Account", description: "Basic info" },
    { label: "Profile", description: "Customize" },
    { label: "Done" },
  ]}
/>

| Prop | Type | Default | Required | |------|------|---------|----------| | steps | StepperStep[] | — | Yes | | currentStep | number (0-indexed) | — | Yes | | orientation | "horizontal" \| "vertical" | "horizontal" | No |

StepperStep interface:

interface StepperStep {
  label: string;
  description?: string;
}

Data Display Components

Table system

Full table system with sorting, pagination, actions, and empty states.

<Table scrollable>
  <TableHeader>
    <TableRow>
      <TableSortHead sortKey="name" activeSort={sort} direction={dir} onSort={handleSort}>Name</TableSortHead>
      <TableHead>Status</TableHead>
      <TableHead>Actions</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    {items.map(item => (
      <TableRow key={item.id} clickable onClick={() => openItem(item)}>
        <TableCell>{item.name}</TableCell>
        <TableCell><Badge variant="success">Active</Badge></TableCell>
        <TableCell>
          <TableActions>
            <TableActionButton label="Edit" onClick={() => edit(item)}>
              <EditIcon />
            </TableActionButton>
            <TableActionButton label="Delete" variant="danger" onClick={() => del(item)}>
              <TrashIcon />
            </TableActionButton>
          </TableActions>
        </TableCell>
      </TableRow>
    ))}
    {items.length === 0 && (
      <TableEmpty colSpan={3} title="No agents yet" description="Create your first agent." />
    )}
  </TableBody>
</Table>
<TablePagination page={page} pageSize={25} total={total} onPageChange={setPage} />

Table props:

| Prop | Type | Default | Required | |------|------|---------|----------| | scrollable | boolean | true | No | | children | ReactNode | — | No |

TableRow props:

| Prop | Type | Default | Required | |------|------|---------|----------| | clickable | boolean | false | No | | onClick | (e: MouseEvent) => void | — | No | | children | ReactNode | — | No |

TableSortHead props:

| Prop | Type | Default | Required | |------|------|---------|----------| | sortKey | string | — | Yes | | activeSort | string \| null | — | No | | direction | "asc" \| "desc" \| null | — | No | | onSort | (key: string, direction: "asc" \| "desc") => void | — | No | | children | ReactNode | — | No |

TableActionButton props:

| Prop | Type | Default | Required | |------|------|---------|----------| | label | string (aria-label) | — | Yes | | variant | "default" \| "danger" | "default" | No | | children | ReactNode | — | No |

TableEmpty props:

| Prop | Type | Default | Required | |------|------|---------|----------| | colSpan | number | — | Yes | | title | string | "No results" | No | | description | string | — | No | | icon | ReactNode | — | No | | action | ReactNode | — | No |

TablePagination props:

| Prop | Type | Default | Required | |------|------|---------|----------| | page | number | — | Yes | | pageSize | number | — | Yes | | total | number | — | Yes | | onPageChange | (page: number) => void | — | Yes | | onPageSizeChange | (size: number) => void | — | No | | pageSizeOptions | number[] | [10, 25, 50, 100] | No |

TableSkeleton props:

| Prop | Type | Default | Required | |------|------|---------|----------| | columns | number | 4 | No | | rows | number | 5 | No |


StatCard + StatCardGrid

Dashboard stat tiles.

<StatCardGrid columns={4}>
  <StatCard
    label="Total Conversations"
    value="1,234"
    icon={<ChatIcon />}
    iconColor="purple"
    variant="gradient"
  />
  <StatCard label="Active Agents" value={8} icon={<BotIcon />} iconColor="blue" />
</StatCardGrid>

StatCard props:

| Prop | Type | Default | Required | |------|------|---------|----------| | label | string | — | Yes | | value | string \| number | — | Yes | | icon | ReactNode | — | No | | iconColor | "purple" \| "pink" \| "blue" \| "green" \| "orange" \| "red" | "purple" | No | | variant | "default" \| "gradient" | "default" | No | | valueClassName | string | — | No |

StatCardGrid props:

| Prop | Type | Default | Required | |------|------|---------|----------| | columns | 2 \| 3 \| 4 | 4 | No | | children | ReactNode | — | Yes |


Badge

Inline status labels.

<Badge variant="success">Active</Badge>
<Badge variant="warning">Pending</Badge>
<Badge variant="error">Failed</Badge>
<Badge variant="info" size="sm">12 new</Badge>
<Badge color="purple">Custom</Badge>

| Prop | Type | Default | Required | |------|------|---------|----------| | variant | "default" \| "success" \| "warning" \| "error" \| "info" \| "outline" \| "secondary" | "default" | No | | color | "green" \| "yellow" \| "red" \| "blue" \| "purple" \| "pink" \| "orange" \| "gray" | — | No | | size | "sm" \| "md" | "md" | No | | children | ReactNode | — | No |

StatusBadge

Maps a status string to a badge variant automatically.

<StatusBadge status="active" />     {/* green */}
<StatusBadge status="pending" />    {/* yellow */}
<StatusBadge status="failed" />     {/* red */}

| Prop | Type | Default | Required | |------|------|---------|----------| | status | string | — | Yes |


InfoCard

Colored callout card for alerts, tips, or highlights.

<InfoCard color="purple" title="Pro Tip" description="Use structured outputs to extract data." />
<InfoCard color="red" title="Warning">Custom children content here.</InfoCard>

| Prop | Type | Default | Required | |------|------|---------|----------| | color | "purple" \| "blue" \| "green" \| "yellow" \| "red" \| "gray" | "blue" | No | | title | string | — | No | | description | string | — | No | | children | ReactNode | — | No |


Breadcrumb

Navigation breadcrumbs with auto-truncation.

<Breadcrumb
  items={[
    { label: "Home", href: "/" },
    { label: "Settings", href: "/settings" },
    { label: "Team" },
  ]}
  separator="›"
  maxItems={4}
/>

| Prop | Type | Default | Required | |------|------|---------|----------| | items | BreadcrumbItem[] | — | Yes | | separator | ReactNode | "/" | No | | maxItems | number | 0 (no limit) | No |

BreadcrumbItem interface:

interface BreadcrumbItem {
  label: string;
  href?: string;
  onClick?: () => void;
}

Feedback Components

ToastProvider + useToast

Global toast notification system. Wrap your app in <ToastProvider> once, then call useToast() anywhere.

// Setup (once, in root layout)
<ToastProvider>{children}</ToastProvider>

// Usage in any component
const { success, error, info, warning } = useToast();

success("Saved!", "Your changes have been saved.");
error("Failed", "Could not connect to server.");
info("Update available");
warning("Low credits", "You have fewer than 100 credits left.");

useToast methods:

| Method | Signature | |--------|-----------| | success | (title: string, message?: string) => void | | error | (title: string, message?: string) => void | | info | (title: string, message?: string) => void | | warning | (title: string, message?: string) => void |


Dialog

Modal dialog with focus trap, backdrop, and 5 size options.

<Dialog open={open} onClose={() => setOpen(false)} size="lg">
  <DialogHeader>
    <h2 className="text-lg font-semibold text-foreground">Edit Agent</h2>
    <DialogCloseButton onClose={() => setOpen(false)} />
  </DialogHeader>
  <DialogBody>
    <Input label="Agent name" />
  </DialogBody>
  <DialogFooter>
    <Button variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
    <Button variant="primary" onClick={handleSave}>Save</Button>
  </DialogFooter>
</Dialog>

Dialog props:

| Prop | Type | Default | Required | |------|------|---------|----------| | open | boolean | — | Yes | | onClose | () => void | — | Yes | | size | "sm" \| "default" \| "lg" \| "xl" \| "full" | "default" | No | | children | ReactNode | — | No |

DialogCloseButton props:

| Prop | Type | Default | Required | |------|------|---------|----------| | onClose | () => void | — | Yes |


ConfirmDialog

Pre-built confirmation modal for destructive or warning actions.

<ConfirmDialog
  open={open}
  onClose={() => setOpen(false)}
  onConfirm={handleDelete}
  title="Delete agent?"
  description="This action cannot be undone."
  variant="danger"
  confirmLabel="Delete"
  isLoading={deleting}
/>

Legacy prop aliases (for backward compat): isOpen = open, message = description, confirmText = confirmLabel, cancelText = cancelLabel, onCancel = onClose.

| Prop | Type | Default | Required | |------|------|---------|----------| | open | boolean | — | Yes | | onClose | () => void | — | Yes | | onConfirm | () => void | — | Yes | | title | string | — | Yes | | description | string | — | No | | variant | "danger" \| "warning" \| "info" | "danger" | No | | confirmLabel | string | "Confirm" | No | | cancelLabel | string | "Cancel" | No | | isLoading | boolean | false | No | | icon | ReactNode | — | No |


BottomSheet

Mobile bottom sheet that slides up from the bottom of the screen. Supports drag-to-dismiss, snap points, backdrop overlay, focus trap, and body scroll lock.

const [open, setOpen] = useState(false);

<BottomSheet open={open} onClose={() => setOpen(false)}>
  <div className="px-4 pb-6">
    <h2 className="text-lg font-semibold text-foreground">Sheet Title</h2>
    <p className="mt-2 text-sm text-muted-foreground">Sheet content here.</p>
  </div>
</BottomSheet>

{/* With snap points (heights in px, ascending) */}
<BottomSheet
  open={open}
  onClose={() => setOpen(false)}
  snapPoints={[200, 400, 600]}
>
  <div className="px-4 pb-6">Snappable content</div>
</BottomSheet>

BottomSheet props:

| Prop | Type | Default | Required | |------|------|---------|----------| | open | boolean | -- | Yes | | onClose | () => void | -- | Yes | | snapPoints | number[] | -- | No | | children | ReactNode | -- | Yes | | className | string | -- | No |

Behavior:

  • Drag the handle down past 30% of sheet height to dismiss
  • With snapPoints, the sheet snaps to the nearest point on drag end
  • Escape key closes the sheet
  • Clicking the backdrop closes the sheet
  • Body scroll is locked while open
  • Focus is trapped inside the sheet (role="dialog", aria-modal)

EmptyState

Illustrated empty state for lists and tables.

<EmptyState
  icon={<BotIcon />}
  title="No agents yet"
  description="Create your first AI voice agent to get started."
  action={<Button variant="primary">Create Agent</Button>}
  height="lg"
/>

| Prop | Type | Default | Required | |------|------|---------|----------| | title | string | — | Yes | | description | string | — | No | | icon | ReactNode | — | No | | action | ReactNode | — | No | | height | "sm" \| "md" \| "lg" | "md" | No |


ErrorState

Error feedback in page, card, or inline variants.

<ErrorState
  title="Failed to load agents"
  message="Check your connection and try again."
  onRetry={refetch}
  variant="card"
/>

| Prop | Type | Default | Required | |------|------|---------|----------| | variant | "page" \| "card" \| "inline" | "card" | No | | title | string | "Something went wrong" | No | | message | string | "An unexpected error occurred. Please try again." | No | | onRetry | () => void | — | No | | retryLabel | string | "Try again" | No |


Skeleton Components

Placeholder loading states. Use before data arrives.

// Full page loading state
<PageSkeleton showCards tableRows={8} />

// Stats grid
<StatCardsSkeletonGrid count={4} />

// Card grid
<GridSkeleton columns={3} count={6} variant="agent" />

// Form sections
<FormSkeleton sections={3} />

// Table only
<TableSkeleton columns={5} rows={10} />

// Raw skeleton block (compose your own)
<Skeleton className="h-4 w-32 rounded" />
<SkeletonText lines={3} />

PageSkeleton props:

| Prop | Type | Default | Required | |------|------|---------|----------| | showCards | boolean | true | No | | cardCount | number | 4 | No | | tableRows | number | 5 | No |

GridSkeleton props:

| Prop | Type | Default | Required | |------|------|---------|----------| | columns | 2 \| 3 \| 4 | 3 | No | | count | number | — | No | | rows | number | 1 | No | | variant | "default" \| "agent" | "default" | No |

FormSkeleton props:

| Prop | Type | Default | Required | |------|------|---------|----------| | fields | number | 4 | No | | sections | number | — | No | | variant | "flat" \| "sections" | "flat" | No |

SkeletonText props:

| Prop | Type | Default | Required | |------|------|---------|----------| | lines | number | 3 | No |


Selector Components

LanguageSelector

Searchable language dropdown.

<LanguageSelector
  value={lang}
  onChange={setLang}
  searchable
  label="Language"
  languages={[
    { code: "en", name: "English" },
    { code: "es", name: "Spanish" },
    { code: "fr", name: "French" },
    { code: "de", name: "German" },
  ]}
/>

| Prop | Type | Default | Required | |------|------|---------|----------| | value | string | — | Yes | | onChange | (code: string) => void | — | Yes | | languages | Array<{ code: string; name: string }> | — | Yes | | placeholder | string | "Select language" | No | | searchable | boolean | true | No | | disabled | boolean | false | No | | error | boolean \| string | — | No | | label | string | — | No |


PhoneInput

Phone number input with country code selector.

<PhoneInput
  value={phone}
  onChange={setPhone}
  defaultCountry="US"
  label="Phone Number"
  error={phoneError}
/>

Built-in countries: US, GB, CA, AU, DE, FR, IN, JP, BR, MX (with flag emojis and dial codes).

| Prop | Type | Default | Required | |------|------|---------|----------| | value | string | — | Yes | | onChange | (value: string) => void | — | Yes | | defaultCountry | string | "US" | No | | placeholder | string | "Phone number" | No | | disabled | boolean | false | No | | error | boolean \| string | — | No | | label | string | — | No |


DatePicker

Native date picker with Date object API.

<DatePicker
  value={startDate}
  onChange={setStartDate}
  label="Start Date"
  min={new Date()}
  error={dateError}
/>

| Prop | Type | Default | Required | |------|------|---------|----------| | onChange | (date: Date) => void | — | Yes | | value | Date | — | No | | min | Date | — | No | | max | Date | — | No | | label | string | — | No | | error | boolean \| string | — | No | | disabled | boolean | false | No | | placeholder | string | — | No |


TimePicker

Native time picker with "HH:mm" string API.

<TimePicker
  value={startTime}
  onChange={setStartTime}
  label="Start Time"
  min="09:00"
  max="17:00"
  step={900}   // 15-minute increments
/>

| Prop | Type | Default | Required | |------|------|---------|----------| | onChange | (time: string) => void | — | Yes | | value | string ("HH:mm") | "" | No | | min | string ("HH:mm") | — | No | | max | string ("HH:mm") | — | No | | step | number (seconds) | 60 | No | | label | string | — | No | | error | boolean \| string | — | No | | disabled | boolean | false | No | | placeholder | string | — | No |


Utility Components

Tooltip

Hover tooltip with 4 positions. Respects viewport edges.

<Tooltip content="This will permanently delete the agent." side="top">
  <Button variant="danger">Delete</Button>
</Tooltip>

| Prop | Type | Default | Required | |------|------|---------|----------| | content | ReactNode | — | Yes | | children | ReactNode | — | Yes | | side | "top" \| "bottom" \| "left" \| "right" | "top" | No | | delayMs | number | 300 | No | | disabled | boolean | false | No |


Popover

Positioned content panel. Supports controlled and uncontrolled modes.

{/* Uncontrolled */}
<Popover
  trigger={<Button variant="outline">Options</Button>}
  side="bottom"
  align="start"
>
  <div className="p-4 text-sm text-foreground">Popover content</div>
</Popover>

{/* Controlled */}
<Popover
  trigger={<Button>Open</Button>}
  open={open}
  onOpenChange={setOpen}
>
  ...
</Popover>

| Prop | Type | Default | Required | |------|------|---------|----------| | trigger | ReactNode | — | Yes | | children | ReactNode | — | Yes | | side | "top" \| "bottom" \| "left" \| "right" | "bottom" | No | | align | "start" \| "center" \| "end" | "center" | No | | open | boolean | — | No | | onOpenChange | (open: boolean) => void | — | No |


DropdownMenu + DropdownMenuItem

Full keyboard-navigable dropdown with ARIA roles.

{/* With built-in trigger */}
<DropdownMenu trigger={<Button variant="ghost" size="icon"><MoreHorizontalIcon /></Button>} align="right">
  <DropdownMenuItem icon={<EditIcon />} onClick={() => edit(item)}>Edit</DropdownMenuItem>
  <DropdownMenuDivider />
  <DropdownMenuItem icon={<TrashIcon />} variant="danger" onClick={() => del(item)}>Delete</DropdownMenuItem>
</DropdownMenu>

{/* Controlled / external trigger */}
<DropdownMenu open={open} onOpenChange={setOpen} align="left" width="w-56">
  <DropdownMenuHeader>Workspace</DropdownMenuHeader>
  <DropdownMenuItem>Account Settings</DropdownMenuItem>
</DropdownMenu>

DropdownMenu props:

| Prop | Type | Default | Required | |------|------|---------|----------| | children | ReactNode | — | Yes | | trigger | ReactNode | — | No | | open | boolean | — | No | | onOpenChange | (open: boolean) => void | — | No | | align | "left" \| "right" | "right" | No | | width | string (Tailwind class) | "w-48" | No |

DropdownMenuItem props:

| Prop | Type | Default | Required | |------|------|---------|----------| | children | ReactNode | — | No | | variant | "default" \| "danger" | "default" | No | | icon | ReactNode | — | No | | disabled | boolean | false | No | | onClick | (e: MouseEvent) => void | — | No |

DropdownMenuHeader props: children, className

DropdownMenuDivider props: className

MoreButton

Pre-styled ··· icon button for use as a dropdown trigger.

<DropdownMenu trigger={<MoreButton />}>...</DropdownMenu>

Avatar

User avatar with image and initials fallback.

<Avatar name="John Doe" src={user.avatarUrl} size="default" />
<Avatar name="Bot Alpha" size="sm" variant="rounded" />

| Prop | Type | Default | Required | |------|------|---------|----------| | name | string | — | Yes | | src | string | — | No | | size | "sm" \| "default" \| "lg" | "default" | No | | variant | "circle" \| "rounded" | "circle" | No |


Progress

Linear progress bar.

<Progress value={75} variant="gradient" showLabel />
<Progress value={progress} size="sm" />

| Prop | Type | Default | Required | |------|------|---------|----------| | value | number (0–100) | — | Yes | | variant | "default" \| "gradient" | "default" | No | | size | "sm" \| "default" | "default" | No | | showLabel | boolean | false | No |


ThemeToggle

Icon button cycling light → dark → system. Controlled or standalone.

{/* Standalone — manages own state */}
<ThemeToggle />

{/* Controlled — pair with next-themes */}
<ThemeToggle theme={resolvedTheme as "light" | "dark" | "system"} onChange={setTheme} />

| Prop | Type | Default | Required | |------|------|---------|----------| | theme | "light" \| "dark" \| "system" | — | No | | onChange | (theme: "light" \| "dark" \| "system") => void | — | No |


ThemeSelector

Segmented 3-option theme control (light / dark / system).

<ThemeSelector theme={theme} onChange={setTheme} />

| Prop | Type | Default | Required | |------|------|---------|----------| | theme | "light" \| "dark" \| "system" | — | No | | onChange | (theme: "light" \| "dark" \| "system") => void | — | No |


Media Components

MediaPlayer

HTML5 audio/video player with playback controls, seek bar, volume, captions, keyboard shortcuts. No Video.js dependency — uses native <audio> and <video> elements.

import { MediaPlayer, useMediaSync } from "@speakai/ui";

function PlayerExample() {
  const { currentTime, setCurrentTime, seekTarget, seekTo, clearSeekTarget } = useMediaSync();

  return (
    <MediaPlayer
      src="https://example.com/audio.mp3"
      mediaType="audio"
      title="Interview Recording"
      seekTarget={seekTarget}
      onSeekComplete={clearSeekTarget}
      onTimeUpdate={setCurrentTime}
      captions={[
        { src: "/captions/en.vtt", label: "English", language: "en" }
      ]}
    />
  );
}

Video mode:

<MediaPlayer
  src="https://example.com/video.mp4"
  mediaType="video"
  poster="/thumbnail.jpg"
  mode="default"
/>

Docked mode (compact bottom bar for mobile):

<MediaPlayer src={url} mediaType="audio" title="Recording" mode="dock" />

| Prop | Type | Default | Required | |------|------|---------|----------| | src | string | — | Yes | | mediaType | "audio" \| "video" | — | Yes | | title | string | — | No | | poster | string | — | No | | seekTarget | number \| null | null | No | | onSeekComplete | () => void | — | No | | onTimeUpdate | (time: number) => void | — | No | | onPlayStateChange | (playing: boolean) => void | — | No | | onDurationChange | (duration: number) => void | — | No | | captions | Array<{ src, label, language }> | [] | No | | mode | "default" \| "dock" | "default" | No | | playbackRates | number[] | [0.5, 0.75, 1, 1.25, 1.5, 2] | No | | className | string | — | No |

Keyboard shortcuts: Space (play/pause), Left Arrow (seek -10s), Right Arrow (seek +30s)

useMediaSync

Hook for syncing MediaPlayer with transcript components. Returns shared state that both player and transcript can read/write.

const {
  currentTime,     // Current playback position (seconds)
  setCurrentTime,  // Update from player's timeupdate
  duration,        // Total media duration
  setDuration,     // Update from player's durationchange
  isPlaying,       // Whether media is currently playing
  setIsPlaying,    // Update from player's play/pause
  seekTarget,      // Time to seek to (set by transcript click)
  seekTo,          // Trigger a seek (call from transcript)
  clearSeekTarget, // Clear after seek completes
} = useMediaSync();

Usage pattern — connect player and transcript:

function MediaView() {
  const sync = useMediaSync();

  return (
    <>
      <MediaPlayer
        src={mediaUrl}
        mediaType="audio"
        seekTarget={sync.seekTarget}
        onSeekComplete={sync.clearSeekTarget}
        onTimeUpdate={sync.setCurrentTime}
        onDurationChange={sync.setDuration}
        onPlayStateChange={sync.setIsPlaying}
      />
      <TranscriptViewer
        sentences={transcript}
        currentTime={sync.currentTime}
        onSeek={sync.seekTo}
      />
    </>
  );
}

Z-Index Layering

Components use a consistent z-index scale. Do not override these without understanding the stack:

| Layer | z-index | Components | |-------|---------|------------| | Desktop sidebar | z-30 | Sidebar rail | | Backdrops | z-40 | Sidebar mobile backdrop, SidePanel backdrop | | Dropdowns / Tooltips | z-50 | DropdownMenu, Popover, Tooltip, Chips suggestions | | Drawers / Panels | z-[60] | Sidebar mobile drawer, SidePanel panel | | Modals | z-[70] | Dialog overlay + panel, BottomSheet overlay + panel | | Toasts | z-[9999] | Toast container |


Accessibility

WCAG 2.1 AA compliant:

  • focus-visible ring on all interactive elements
  • aria-busy, aria-invalid, aria-live, role="alert", role="menu", role="listbox"
  • Dialog focus trap on open, restores focus on close
  • Tabs / DropdownMenu arrow-key navigation
  • RadioGroup keyboard cycling
  • 40px minimum touch targets on all interactive elements
  • 16px minimum input font size (prevents iOS zoom)
  • Decorative SVGs use aria-hidden="true"

Development

npm install
npm run build   # compile to dist/
npm run dev     # demo at localhost:5555

License

MIT