Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
export { Toggle, toggleVariants } from "./toggle.js";
export type { ToggleProps, ToggleVariantProps } from "./toggle.types.js";

View File

@@ -0,0 +1,145 @@
# Toggle
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - toggle.variants — `toggleVariants` is the cva (clas…
- [toggle.variants](./toggle.variants.md) — `toggleVariants` is the cva (class-variance-authority) configuration powering every `<Toggle>` render. It is split from…
- toggle.variants — `toggleVariants` is the cva (clas…
- [toggle.variants](./toggle.variants.md) — `toggleVariants` is the cva (class-variance-authority) configuration powering every `<Toggle>` render. It is split from…
- toggle.variants — `toggleVariants` is the cva (clas…
- [toggle.variants](./toggle.variants.md) — `toggleVariants` is the cva (class-variance-authority) configuration powering every `<Toggle>` render. It is split from…
- toggle.variants — `toggleVariants` is the cva (clas…
- [toggle.variants](./toggle.variants.md) — `toggleVariants` is the cva (class-variance-authority) configuration powering every `<Toggle>` render. It is split from…
- [toggle.variants](./toggle.variants.md) — `toggleVariants` is the cva (class-variance-authority) configuration powering every `<Toggle>` render. It is split from…
## Purpose
`<Toggle>` is the chassis two-state toggle button built on `@radix-ui/react-toggle`. Radix supplies the a11y semantics (`aria-pressed="true|false"`, native `Space` / `Enter` activation, `disabled` propagation); the chassis layers theme-driven variant resolution and the `data-icon` slot so child icons inherit sizing without consumer boilerplate.
Per the shadcn rule, multi-option pickers (2–7 choices) compose `<ToggleGroup>` rather than looping over standalone `<Toggle>` with manual `pressed` state — `<ToggleGroup>` provides roving-focus semantics that a hand-rolled toggle row does not.
Refs are forwarded to the Radix Root which renders a styled `<button>`.
## Features
- Two visual variants (`default`, `outline`) covering the upstream shadcn surface.
- Three sizes (`sm`, `default`, `lg`) for use in dense toolbars or roomy headers.
- Controlled and uncontrolled use via Radix `pressed` / `defaultPressed` / `onPressedChange`.
- `disabled` blocks pointer + keyboard input and decays opacity.
- `data-state="on|off"` mirrored on the rendered button so consumer cva configs / CSS can react.
- `data-icon` slot rules (`[&_svg]:pointer-events-none size-4 shrink-0`) so child icons inherit sizing.
- Refs forwarded to the Radix Root.
- `motion-reduce:transition-none` collapses transitions for `prefers-reduced-motion: reduce`.
- Theme-driven variant overrides via `useThemedVariants("Toggle", toggleVariants)`.
## Usage: Basic Example
```tsx
import { Toggle } from "@suaveplan/react-ui";
import { BoldIcon } from "lucide-react";
export function BoldToggle() {
return (
<Toggle aria-label="Toggle bold">
<BoldIcon />
</Toggle>
);
}
```
## Usage: Advanced Example
```tsx
import { Toggle } from "@suaveplan/react-ui";
import { BoldIcon, ItalicIcon, UnderlineIcon } from "lucide-react";
import { useState } from "react";
interface MarkState {
readonly bold: boolean;
readonly italic: boolean;
readonly underline: boolean;
}
export function MarkToolbar() {
const [marks, setMarks] = useState<MarkState>({
bold: false,
italic: false,
underline: false,
});
const set = (key: keyof MarkState, value: boolean) =>
setMarks((prev) => ({ ...prev, [key]: value }));
return (
<div className="flex items-center gap-1 p-2 border rounded">
<Toggle
aria-label="Toggle bold"
size="sm"
pressed={marks.bold}
onPressedChange={(v) => set("bold", v)}
>
<BoldIcon />
</Toggle>
<Toggle
aria-label="Toggle italic"
size="sm"
pressed={marks.italic}
onPressedChange={(v) => set("italic", v)}
>
<ItalicIcon />
</Toggle>
<Toggle
aria-label="Toggle underline"
size="sm"
pressed={marks.underline}
onPressedChange={(v) => set("underline", v)}
>
<UnderlineIcon />
</Toggle>
</div>
);
}
```
> **Note:** The example above is appropriate when each toggle is independent. For mutually-exclusive option sets (text alignment, view density), use `<ToggleGroup>` instead — it provides roving-focus and unified `value` propagation.
## API Reference
### `<Toggle>`
`forwardRef<ComponentRef<typeof TogglePrimitive.Root>, ToggleProps>` — wraps Radix Root, renders a `<button>` with `aria-pressed` reflecting the current state.
#### `ToggleProps`
Inherits every prop from Radix `TogglePrimitive.Root` except `asChild`. Adds:
| Prop | Type | Default | Description |
|---|---|---|---|
| `variant` | `"default" \| "outline"` | `"default"` | Visual variant. |
| `size` | `"sm" \| "default" \| "lg"` | `"default"` | Padding / height. |
Common Radix props consumers will use:
| Prop | Type | Default | Description |
|---|---|---|---|
| `pressed` | `boolean` | _uncontrolled_ | Controlled state. |
| `defaultPressed` | `boolean` | `false` | Initial state for uncontrolled use. |
| `onPressedChange` | `(pressed: boolean) => void` | _none_ | Fires on state change. |
| `disabled` | `boolean` | `false` | Blocks input + decays opacity. |
#### `toggleVariants`
The bare cva resolver. Exported so external consumers can re-use the variants directly.
## Implementation Notes
- The variants module is intentionally separate (`toggle.variants.ts`) so an ejected source copy keeps the cva config in a dedicated file.
- For option sets (2–7 mutually-exclusive choices), use `<ToggleGroup>` — the chassis enforces this convention via the shadcn skill rule.
- The `data-icon` slot rules let consumers drop a Lucide icon as a child without specifying icon size; the cva config handles `size-4 shrink-0` automatically.
- Refs target the Radix Root (the `<button>` element) — consumers can `.focus()` the toggle programmatically through the forwarded ref.
- Icon-only toggles MUST supply `aria-label` — the chassis does not auto-inject a fallback because the toggle's purpose is domain-specific.

View File

@@ -0,0 +1,55 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { renderComponent } from "@suaveplan/testing-dom";
import * as Stories from "./toggle.stories.js";
const STORY_KEYS = [
"Default",
"Playground",
"Variants",
"States",
"WithI18n",
"WithRTL",
"WithTheme",
"A11y",
"Composition",
] as const;
type StoryKey = (typeof STORY_KEYS)[number];
interface StoryRecord {
readonly args?: Record<string, unknown>;
readonly render?: (args: Record<string, unknown>) => React.ReactNode;
}
describe("toggle.stories", () => {
it("exports every Form story", { timeout: 5000 }, () => {
for (const key of STORY_KEYS) {
expect(
(Stories as unknown as Record<string, unknown>)[key],
`missing story export: ${key}`
).toBeDefined();
}
});
for (const key of STORY_KEYS) {
it(`renders the ${key} story without throwing`, {
timeout: 10000,
}, () => {
const story = (Stories as unknown as Record<StoryKey, StoryRecord>)[
key
];
expect(story).toBeDefined();
const render = story.render;
expect(typeof render).toBe("function");
if (render === undefined) return;
const tree = render(story.args ?? {});
const ui = (
<div data-testid="story-host">{tree as React.ReactNode}</div>
);
const { container } = renderComponent(ui);
expect(
container.querySelector("[data-testid='story-host']")
).not.toBeNull();
});
}
});

View File

@@ -0,0 +1,282 @@
/**
* Storybook stories for `<Toggle>`.
*
* `@storybook/react` is NOT a dep of `@suaveplan/react-ui`; story-meta
* shapes are inlined.
*/
import {
createI18n as createI18nNarrow,
type I18nInstance,
} from "@suaveplan/i18n/plugin";
import { TranslationProvider } from "@suaveplan/react-i18n";
import type { ReactElement, ReactNode } from "react";
import type { SupportedLocale } from "../../locales/locales.types.js";
import type { ThemePlugin } from "../../theme/theme.types.js";
import { ThemeProvider } from "../../theme/theme-provider/theme-provider.js";
import { darkTheme } from "../../theme/themes/dark.js";
import { highContrastTheme } from "../../theme/themes/high-contrast.js";
import { lightTheme } from "../../theme/themes/light.js";
import { Toggle } from "./toggle.js";
import type { ToggleProps } from "./toggle.types.js";
function createI18n(
options: Parameters<typeof createI18nNarrow>[0]
): I18nInstance {
return createI18nNarrow(options) as unknown as I18nInstance;
}
interface Meta<T> {
readonly component: T;
readonly title: string;
readonly tags?: readonly string[];
readonly parameters?: Record<string, unknown>;
readonly argTypes?: Record<string, unknown>;
}
interface StoryObj<TArgs> {
readonly args?: Partial<TArgs>;
readonly render?: (args: Partial<TArgs>) => ReactNode;
readonly name?: string;
readonly parameters?: Record<string, unknown>;
}
const meta: Meta<unknown> = {
component: undefined as unknown as never,
title: "react-ui/Form/Toggle",
tags: ["autodocs"],
argTypes: {
variant: {
control: { type: "select" },
options: ["default", "outline"],
},
size: { control: { type: "select" }, options: ["sm", "default", "lg"] },
disabled: { control: "boolean" },
},
};
export default meta;
type Story = StoryObj<ToggleProps>;
interface ProviderShellProps {
readonly locale: SupportedLocale;
readonly themes: ReadonlyArray<ThemePlugin>;
readonly defaultTheme: string;
readonly children: ReactNode;
}
function ProviderShellSync(props: ProviderShellProps): ReactElement {
const i18n = createI18n({
defaultLocale: props.locale,
fallbackLocale: "en-GB",
});
return (
<TranslationProvider i18n={i18n}>
<ThemeProvider
themes={props.themes}
defaultTheme={props.defaultTheme}
>
{props.children}
</ThemeProvider>
</TranslationProvider>
);
}
export const Default: Story = {
render: (_args) => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<Toggle aria-label="bold">B</Toggle>
</ProviderShellSync>
),
args: { variant: "default", size: "default" },
};
export const Playground: Story = {
render: (args) => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<Toggle aria-label="play" {...args}>
{(args.children as React.ReactNode) ?? "Toggle"}
</Toggle>
</ProviderShellSync>
),
args: { variant: "default", size: "default" },
};
export const Variants: Story = {
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<div className="flex flex-col gap-3">
{(["default", "outline"] as const).map((v) => (
<div key={v} className="flex items-center gap-2">
{(["sm", "default", "lg"] as const).map((s) => (
<Toggle
key={`${v}-${s}`}
variant={v}
size={s}
aria-label={`${v}-${s}`}
>
{`${v}/${s}`}
</Toggle>
))}
</div>
))}
</div>
</ProviderShellSync>
),
};
export const States: Story = {
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<div className="flex items-center gap-2">
<Toggle aria-label="off">Idle</Toggle>
<Toggle aria-label="on" defaultPressed>
Pressed
</Toggle>
<Toggle aria-label="dis" disabled>
Disabled
</Toggle>
<Toggle aria-label="dis-on" disabled defaultPressed>
Disabled pressed
</Toggle>
</div>
</ProviderShellSync>
),
};
const LOCALES: ReadonlyArray<SupportedLocale> = [
"en-GB",
"zh-CN",
"ar-AE",
"cy-GB",
"ru-RU",
];
export const WithI18n: Story = {
render: () => (
<div className="flex flex-col gap-3">
{LOCALES.map((locale) => (
<div
key={locale}
dir={locale === "ar-AE" ? "rtl" : "ltr"}
className="flex items-center gap-2"
>
<span style={{ width: 80 }}>{locale}</span>
<ProviderShellSync
locale={locale}
themes={[lightTheme]}
defaultTheme="light"
>
<Toggle aria-label={`tg-${locale}`}>{locale}</Toggle>
</ProviderShellSync>
</div>
))}
</div>
),
};
export const WithRTL: Story = {
render: () => (
<div dir="rtl">
<ProviderShellSync
locale="ar-AE"
themes={[lightTheme]}
defaultTheme="light"
>
<Toggle aria-label="rtl">RTL</Toggle>
</ProviderShellSync>
</div>
),
};
const THEMES: ReadonlyArray<{
readonly label: string;
readonly theme: ThemePlugin;
}> = [
{ label: "light", theme: lightTheme },
{ label: "dark", theme: darkTheme },
{ label: "highContrast", theme: highContrastTheme },
];
export const WithTheme: Story = {
render: () => (
<div className="flex flex-col gap-3">
{THEMES.map(({ label, theme }) => (
<div key={label} className="flex items-center gap-2 p-3 border">
<span style={{ width: 120 }}>{label}</span>
<ProviderShellSync
locale="en-GB"
themes={[theme]}
defaultTheme={theme.name}
>
<Toggle aria-label={label} defaultPressed>
{label}
</Toggle>
</ProviderShellSync>
</div>
))}
</div>
),
};
export const A11y: Story = {
parameters: {
a11y: {
element: "#storybook-root",
config: {
rules: [
{ id: "color-contrast", enabled: true },
{ id: "label", enabled: true },
],
},
},
},
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<Toggle aria-label="Toggle italic">I</Toggle>
</ProviderShellSync>
),
};
export const Composition: Story = {
name: "Composition (toolbar)",
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<div className="flex items-center gap-1 p-2 border rounded">
<Toggle aria-label="bold" size="sm">
B
</Toggle>
<Toggle aria-label="italic" size="sm">
I
</Toggle>
<Toggle aria-label="underline" size="sm">
U
</Toggle>
</div>
</ProviderShellSync>
),
};

View File

@@ -0,0 +1,154 @@
import { TranslationProvider } from "@suaveplan/react-i18n";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { renderComponent } from "@suaveplan/testing-dom";
import { createRef, useState } from "react";
import { createTestI18n } from "../../../tests/setup/test-i18n.js";
import { ThemeProvider } from "../../theme/theme-provider/theme-provider.js";
import { lightTheme } from "../../theme/themes/light.js";
import { Toggle } from "./toggle.js";
import { toggleVariants } from "./toggle.variants.js";
async function withProviders(
node: React.ReactElement
): Promise<React.ReactElement> {
const i18n = await createTestI18n({ locale: "en-GB" });
return (
<TranslationProvider i18n={i18n}>
<ThemeProvider themes={[lightTheme]} defaultTheme="light">
{node}
</ThemeProvider>
</TranslationProvider>
);
}
describe("<Toggle>", () => {
it("renders a button with default classes", { timeout: 5000 }, async () => {
const tree = await withProviders(<Toggle aria-label="bold">B</Toggle>);
const { getByRole } = renderComponent(tree);
const t = getByRole("button");
expect(t.tagName).toBe("BUTTON");
expect(t.className).toContain("rounded-md");
});
it("forwards refs to the radix root", { timeout: 5000 }, async () => {
const ref = createRef<HTMLButtonElement>();
const tree = await withProviders(
<Toggle ref={ref} aria-label="ref">
R
</Toggle>
);
renderComponent(tree);
expect(ref.current).not.toBeNull();
expect(ref.current?.tagName).toBe("BUTTON");
});
it("toggles pressed state when clicked (controlled)", {
timeout: 5000,
}, async () => {
function Controlled(): React.ReactElement {
const [pressed, setPressed] = useState(false);
return (
<Toggle
aria-label="t"
pressed={pressed}
onPressedChange={setPressed}
>
T
</Toggle>
);
}
const tree = await withProviders(<Controlled />);
const { getByRole, user } = renderComponent(tree);
const t = getByRole("button");
expect(t.getAttribute("aria-pressed")).toBe("false");
await user.click(t);
expect(t.getAttribute("aria-pressed")).toBe("true");
});
it("activates via Enter key", { timeout: 5000 }, async () => {
let count = 0;
const tree = await withProviders(
<Toggle
aria-label="enter"
onPressedChange={() => {
count += 1;
}}
>
E
</Toggle>
);
const { getByRole, user } = renderComponent(tree);
const t = getByRole("button");
t.focus();
await user.keyboard("[Enter]");
expect(count).toBe(1);
});
it("respects the disabled prop", { timeout: 5000 }, async () => {
let count = 0;
const tree = await withProviders(
<Toggle
aria-label="dis"
disabled
onPressedChange={() => {
count += 1;
}}
>
D
</Toggle>
);
const { getByRole, user } = renderComponent(tree);
const t = getByRole("button");
expect(t.hasAttribute("disabled")).toBe(true);
await user.click(t);
expect(count).toBe(0);
});
it("renders every variant × size combination from cva", {
timeout: 5000,
}, () => {
const variants = ["default", "outline"] as const;
const sizes = ["sm", "default", "lg"] as const;
for (const v of variants) {
for (const s of sizes) {
const cls = toggleVariants({ variant: v, size: s });
expect(typeof cls).toBe("string");
expect(cls.length).toBeGreaterThan(0);
}
}
});
it("applies the outline variant", { timeout: 5000 }, async () => {
const tree = await withProviders(
<Toggle aria-label="o" variant="outline">
O
</Toggle>
);
const { getByRole } = renderComponent(tree);
const t = getByRole("button");
expect(t.className).toContain("border");
expect(t.className).toContain("border-input");
});
it("merges custom className alongside variant classes", {
timeout: 5000,
}, async () => {
const tree = await withProviders(
<Toggle aria-label="cls" className="custom-extra-class">
C
</Toggle>
);
const { getByRole } = renderComponent(tree);
const t = getByRole("button");
expect(t.className).toContain("custom-extra-class");
expect(t.className).toContain("rounded-md");
});
it("exposes toggleVariants as a callable resolver", {
timeout: 5000,
}, () => {
const cls = toggleVariants({ variant: "default", size: "default" });
expect(typeof cls).toBe("string");
expect(cls.length).toBeGreaterThan(0);
});
});

View File

@@ -0,0 +1,33 @@
import * as TogglePrimitive from "@radix-ui/react-toggle";
import { type ComponentRef, forwardRef, type ReactElement } from "react";
import { useThemedVariants } from "../../theme/use-themed-variants/use-themed-variants.js";
import { cn } from "../../utils/cn/cn.js";
import type { ToggleProps } from "./toggle.types.js";
import { toggleVariants } from "./toggle.variants.js";
/**
* `<Toggle>` — chassis two-state toggle button built on
* `@radix-ui/react-toggle`.
*
* Radix supplies the a11y semantics (`role="button"`,
* `aria-pressed="true|false"`, native `Space` / `Enter` activation,
* `disabled` propagation). The chassis layers theme-driven variant
* resolution and the `data-icon` slot so child icons inherit sizing.
*
* Refs are forwarded to the Radix Root (the rendered `<button>`).
*/
export const Toggle = forwardRef<
ComponentRef<typeof TogglePrimitive.Root>,
ToggleProps
>(function Toggle(props, ref): ReactElement {
const { className, variant, size, ...rest } = props;
const variants = useThemedVariants("Toggle", toggleVariants);
const classes = cn(variants({ variant, size }), className);
return <TogglePrimitive.Root ref={ref} className={classes} {...rest} />;
});
Toggle.displayName = "Toggle";
export { toggleVariants };

View File

@@ -0,0 +1,35 @@
import type * as TogglePrimitive from "@radix-ui/react-toggle";
import type { ComponentPropsWithoutRef } from "react";
import type { VariantProps } from "../../variants/variants.js";
import type { toggleVariants } from "./toggle.variants.js";
/**
* Module augmentation registering `Toggle` into `ThemableVariants` so
* `useThemedVariants("Toggle")` is type-narrowed and a `ThemePlugin`
* may declare `variantOverrides: { Toggle: <override> }`.
*/
declare module "../../theme/theme.types.js" {
interface ThemableVariants {
readonly Toggle: typeof toggleVariants;
}
}
export type ToggleVariantProps = VariantProps<typeof toggleVariants>;
/**
* Public props accepted by `<Toggle>`.
*
* Wraps `@radix-ui/react-toggle` Root: `pressed`/`onPressedChange`
* for controlled use, `defaultPressed` for uncontrolled, `disabled`
* for the decay state. The chassis layers `variant` + `size`.
*
* Per the shadcn rule, multi-option pickers (2–7 choices) compose
* `<ToggleGroup>` rather than looping over standalone `<Toggle>`
* with manual `pressed` state.
*/
export interface ToggleProps
extends Omit<
ComponentPropsWithoutRef<typeof TogglePrimitive.Root>,
"asChild"
>,
ToggleVariantProps {}

View File

@@ -0,0 +1,59 @@
# Toggle Variants (cva config)
## Purpose
`toggleVariants` is the cva (class-variance-authority) configuration powering every `<Toggle>` render. It is split from `toggle.tsx` into its own module so an ejected source copy under `npx shadcn@latest add @suaveplan/toggle` keeps the cva configuration in a dedicated file. Theme overrides flow through `useThemedVariants("Toggle", toggleVariants)`.
## Features
- Two visual variants (`default`, `outline`) covering the upstream shadcn surface.
- Three sizes (`sm`, `default`, `lg`) for use in dense toolbars or roomy headers.
- `data-[state=on]:bg-accent data-[state=on]:text-accent-foreground` flips the colour when pressed.
- `hover:bg-muted hover:text-muted-foreground` for the pointer-hover affordance.
- `data-icon` slot rules (`[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0`) so child icons inherit sizing.
- `disabled:pointer-events-none disabled:opacity-50` for the native disabled state.
- `motion-reduce:transition-none` for users with `prefers-reduced-motion: reduce`.
- `focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2` for keyboard-only focus styling.
## Usage: Basic Example
```ts
import { toggleVariants } from "@suaveplan/react-ui";
const className = toggleVariants({ variant: "default", size: "default" });
```
## Usage: Advanced Example
```ts
import { toggleVariants, cn } from "@suaveplan/react-ui";
export function CustomToggle({ active }: { active: boolean }) {
return (
<button
type="button"
aria-pressed={active}
data-state={active ? "on" : "off"}
className={cn(
toggleVariants({ variant: "outline", size: "lg" }),
active ? "ring-2 ring-accent" : ""
)}
>
Toggle
</button>
);
}
```
## API Reference
### `toggleVariants(props?: { variant?: "default" | "outline"; size?: "sm" | "default" | "lg" }): string`
Returns the cva-merged class string for the supplied variant + size combination. Both fields are optional; omitted fields fall back to the cva `defaultVariants` (`variant: "default"`, `size: "default"`).
## Implementation Notes
- The resolver is tagged with `componentName: "Toggle"` (non-enumerable, non-writable) via `defineVariants`.
- The cva config mirrors the visual surface of `<Button>` so toolbars combining `<Button>` + `<Toggle>` look coherent without per-component overrides.
- The `data-icon` slot rules are duplicated between `<Button>` and `<Toggle>` because both surfaces accept the same icon-as-child pattern; consumers benefit from the consistent sizing.
- Tokens reference theme-driven CSS custom properties (`bg-accent`, `text-accent-foreground`, `bg-muted`, `text-muted-foreground`) so the colour palette comes from the active `ThemePlugin.cssVariables` payload.

View File

@@ -0,0 +1,46 @@
import { defineVariants } from "../../variants/variants.js";
/**
* `toggleVariants` — base cva config for `<Toggle>`.
*
* Mirrors the upstream shadcn `toggle` recipe: rounded button-shaped
* surface that flips its background to `accent` when pressed. Two
* visual `variant`s (`default`, `outline`) and three sizes for use in
* dense toolbars.
*
* The cva exposes `data-icon` slot rules so child icons inherit
* sizing automatically (mirrors `<Button>`).
*/
export const toggleVariants = defineVariants(
"Toggle",
[
"inline-flex items-center justify-center gap-2 whitespace-nowrap",
"rounded-md text-sm font-medium",
"transition-colors duration-150 ease-out",
"outline-none focus-visible:ring-2 focus-visible:ring-ring",
"focus-visible:ring-offset-2 focus-visible:ring-offset-background",
"hover:bg-muted hover:text-muted-foreground",
"disabled:pointer-events-none disabled:opacity-50",
"data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
"[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"motion-reduce:transition-none",
].join(" "),
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
},
size: {
sm: "h-8 px-2 text-xs",
default: "h-9 px-3",
lg: "h-10 px-4",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);