@holaboss/ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,110 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import { cn } from "../lib/utils.js";
4
+ import { LoadingState } from "./loading-state.js";
5
+ import { EmptyState } from "../primitives/empty-state.js";
6
+
7
+ export interface DataTableColumn<Row> {
8
+ /** Column key — must be unique within a table. */
9
+ id: string;
10
+ /** Header label. */
11
+ header: ReactNode;
12
+ /** Cell content; receives the full row. */
13
+ cell: (row: Row) => ReactNode;
14
+ /**
15
+ * Cell alignment. `right` for numeric / monetary columns; `center`
16
+ * for icons / status badges.
17
+ */
18
+ align?: "left" | "right" | "center";
19
+ /** Hide on small viewports (< sm). */
20
+ hideOnSmall?: boolean;
21
+ /** Width hint in CSS units (e.g. `120px`, `20%`). */
22
+ width?: string;
23
+ }
24
+
25
+ export interface DataTableProps<Row> {
26
+ columns: DataTableColumn<Row>[];
27
+ rows: Row[];
28
+ /** Stable key per row. */
29
+ rowKey: (row: Row) => string;
30
+ /** Per-row click handler. Renders rows with hover affordance. */
31
+ onRowClick?: (row: Row) => void;
32
+ /** When true, body is replaced with a <LoadingState>. */
33
+ isLoading?: boolean;
34
+ /** Empty-state title (shown when not loading and rows is empty). */
35
+ emptyTitle?: string;
36
+ emptyDescription?: ReactNode;
37
+ className?: string;
38
+ }
39
+
40
+ const alignClass: Record<NonNullable<DataTableColumn<unknown>["align"]>, string> = {
41
+ left: "text-left",
42
+ right: "text-right",
43
+ center: "text-center",
44
+ };
45
+
46
+ export function DataTable<Row>({
47
+ columns,
48
+ rows,
49
+ rowKey,
50
+ onRowClick,
51
+ isLoading,
52
+ emptyTitle = "No data yet",
53
+ emptyDescription,
54
+ className,
55
+ }: DataTableProps<Row>) {
56
+ if (isLoading) {
57
+ return <LoadingState variant="list" />;
58
+ }
59
+ if (rows.length === 0) {
60
+ return <EmptyState title={emptyTitle} description={emptyDescription} />;
61
+ }
62
+ return (
63
+ <div className={cn("w-full overflow-x-auto", className)}>
64
+ <table className="w-full table-fixed border-collapse text-sm">
65
+ <thead className="border-b border-border bg-background">
66
+ <tr>
67
+ {columns.map((col) => (
68
+ <th
69
+ key={col.id}
70
+ className={cn(
71
+ "px-3 py-2 text-xs font-medium text-muted-foreground",
72
+ alignClass[col.align ?? "left"],
73
+ col.hideOnSmall && "hidden sm:table-cell",
74
+ )}
75
+ style={col.width ? { width: col.width } : undefined}
76
+ >
77
+ {col.header}
78
+ </th>
79
+ ))}
80
+ </tr>
81
+ </thead>
82
+ <tbody>
83
+ {rows.map((row) => (
84
+ <tr
85
+ key={rowKey(row)}
86
+ onClick={onRowClick ? () => onRowClick(row) : undefined}
87
+ className={cn(
88
+ "border-b border-border last:border-b-0 transition-colors",
89
+ onRowClick && "cursor-pointer hover:bg-accent",
90
+ )}
91
+ >
92
+ {columns.map((col) => (
93
+ <td
94
+ key={col.id}
95
+ className={cn(
96
+ "truncate px-3 py-2 text-sm text-foreground",
97
+ alignClass[col.align ?? "left"],
98
+ col.hideOnSmall && "hidden sm:table-cell",
99
+ )}
100
+ >
101
+ {col.cell(row)}
102
+ </td>
103
+ ))}
104
+ </tr>
105
+ ))}
106
+ </tbody>
107
+ </table>
108
+ </div>
109
+ );
110
+ }
@@ -0,0 +1,52 @@
1
+ import { AlertTriangle } from "lucide-react";
2
+ import type { ReactNode } from "react";
3
+
4
+ import { Button } from "../primitives/button.js";
5
+ import { cn } from "../lib/utils.js";
6
+
7
+ /**
8
+ * Centered error display with an optional retry action. Use for the
9
+ * body of a pane when data fetch / mutation fails. Title is short, the
10
+ * `detail` is the developer-relevant message (truncate-friendly).
11
+ */
12
+ export interface ErrorStateProps {
13
+ title?: string;
14
+ /** Concrete error description — the API error text, etc. */
15
+ detail?: ReactNode;
16
+ /** Click handler for the retry button. Omit to skip the button. */
17
+ onRetry?: () => void;
18
+ retryLabel?: string;
19
+ className?: string;
20
+ }
21
+
22
+ export function ErrorState({
23
+ title = "Something went wrong",
24
+ detail,
25
+ onRetry,
26
+ retryLabel = "Try again",
27
+ className,
28
+ }: ErrorStateProps) {
29
+ return (
30
+ <div
31
+ className={cn(
32
+ "flex flex-col items-center justify-center gap-3 px-4 py-14 text-center",
33
+ className,
34
+ )}
35
+ >
36
+ <div className="grid size-10 place-items-center rounded-xl bg-destructive/10 text-destructive">
37
+ <AlertTriangle className="size-4" strokeWidth={1.6} />
38
+ </div>
39
+ <p className="text-sm font-medium text-foreground">{title}</p>
40
+ {detail ? (
41
+ <p className="max-w-md text-xs leading-5 text-muted-foreground">
42
+ {detail}
43
+ </p>
44
+ ) : null}
45
+ {onRetry ? (
46
+ <Button size="sm" variant="outline" onClick={onRetry}>
47
+ {retryLabel}
48
+ </Button>
49
+ ) : null}
50
+ </div>
51
+ );
52
+ }
@@ -0,0 +1,58 @@
1
+ import { Search } from "lucide-react";
2
+ import type { ReactNode } from "react";
3
+
4
+ import { Input } from "../primitives/input.js";
5
+ import { cn } from "../lib/utils.js";
6
+
7
+ /**
8
+ * Search input + filter chip slot + right-aligned actions. Sits at the
9
+ * top of a list / table to provide a consistent control row across
10
+ * apps.
11
+ */
12
+ export interface FilterBarProps {
13
+ /** Search input value (controlled). */
14
+ search?: string;
15
+ onSearchChange?: (value: string) => void;
16
+ searchPlaceholder?: string;
17
+ /** Filter chips / selects / segmented controls. */
18
+ filters?: ReactNode;
19
+ /** Right-aligned actions (e.g. "New", "Refresh"). */
20
+ actions?: ReactNode;
21
+ className?: string;
22
+ }
23
+
24
+ export function FilterBar({
25
+ search,
26
+ onSearchChange,
27
+ searchPlaceholder = "Search…",
28
+ filters,
29
+ actions,
30
+ className,
31
+ }: FilterBarProps) {
32
+ return (
33
+ <div
34
+ className={cn(
35
+ "flex flex-wrap items-center gap-2 border-b border-border px-4 py-2",
36
+ className,
37
+ )}
38
+ >
39
+ {onSearchChange ? (
40
+ <div className="relative min-w-[180px] flex-1">
41
+ <Search className="pointer-events-none absolute left-2 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
42
+ <Input
43
+ className="h-7 pl-7 text-sm"
44
+ value={search ?? ""}
45
+ placeholder={searchPlaceholder}
46
+ onChange={(event) => onSearchChange(event.target.value)}
47
+ />
48
+ </div>
49
+ ) : null}
50
+ {filters ? (
51
+ <div className="flex flex-wrap items-center gap-1.5">{filters}</div>
52
+ ) : null}
53
+ {actions ? (
54
+ <div className="ml-auto flex items-center gap-1.5">{actions}</div>
55
+ ) : null}
56
+ </div>
57
+ );
58
+ }
@@ -0,0 +1,67 @@
1
+ import { cn } from "../lib/utils.js";
2
+
3
+ /**
4
+ * Skeleton-style loading placeholder. Use for the body of a pane while
5
+ * data is fetching. The default presentation is a vertical stack of
6
+ * pulsing bars; `variant="list"` mimics a row list and `variant="card"`
7
+ * mimics card-grid loading.
8
+ *
9
+ * Solid backgrounds + subtle pulse only. No shimmer gradients.
10
+ */
11
+ export interface LoadingStateProps {
12
+ variant?: "rows" | "list" | "card";
13
+ /** How many placeholder elements to render. Default 4. */
14
+ count?: number;
15
+ className?: string;
16
+ }
17
+
18
+ export function LoadingState({
19
+ variant = "rows",
20
+ count = 4,
21
+ className,
22
+ }: LoadingStateProps) {
23
+ const items = Array.from({ length: count }, (_, i) => i);
24
+ if (variant === "card") {
25
+ return (
26
+ <div
27
+ className={cn(
28
+ "grid grid-cols-1 gap-3 p-4 sm:grid-cols-2 lg:grid-cols-3",
29
+ className,
30
+ )}
31
+ >
32
+ {items.map((i) => (
33
+ <div
34
+ key={i}
35
+ className="h-24 animate-pulse rounded-xl border border-border bg-muted"
36
+ />
37
+ ))}
38
+ </div>
39
+ );
40
+ }
41
+ if (variant === "list") {
42
+ return (
43
+ <div className={cn("flex flex-col divide-y divide-border", className)}>
44
+ {items.map((i) => (
45
+ <div key={i} className="flex items-center gap-3 px-4 py-3">
46
+ <div className="size-8 shrink-0 animate-pulse rounded-full bg-muted" />
47
+ <div className="min-w-0 flex-1 space-y-1.5">
48
+ <div className="h-3 w-1/3 animate-pulse rounded bg-muted" />
49
+ <div className="h-2.5 w-2/3 animate-pulse rounded bg-muted" />
50
+ </div>
51
+ </div>
52
+ ))}
53
+ </div>
54
+ );
55
+ }
56
+ return (
57
+ <div className={cn("flex flex-col gap-2 p-4", className)}>
58
+ {items.map((i) => (
59
+ <div
60
+ key={i}
61
+ className="h-4 animate-pulse rounded bg-muted"
62
+ style={{ width: `${100 - i * 8}%` }}
63
+ />
64
+ ))}
65
+ </div>
66
+ );
67
+ }
@@ -0,0 +1,44 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import { cn } from "../lib/utils.js";
4
+
5
+ /**
6
+ * Title + optional subtitle + optional right-aligned actions. The
7
+ * canonical first child of `<DashboardShell header={...}>`. Density and
8
+ * weight stay consistent regardless of which app drops it in.
9
+ */
10
+ export interface PageHeaderProps {
11
+ title: ReactNode;
12
+ description?: ReactNode;
13
+ /** Right-aligned action slot (typically buttons). */
14
+ actions?: ReactNode;
15
+ className?: string;
16
+ }
17
+
18
+ export function PageHeader({
19
+ title,
20
+ description,
21
+ actions,
22
+ className,
23
+ }: PageHeaderProps) {
24
+ return (
25
+ <div
26
+ className={cn(
27
+ "flex items-start justify-between gap-4 px-4 py-3",
28
+ className,
29
+ )}
30
+ >
31
+ <div className="min-w-0 flex-1">
32
+ <h1 className="truncate text-base font-semibold text-foreground">
33
+ {title}
34
+ </h1>
35
+ {description ? (
36
+ <p className="mt-0.5 text-xs text-muted-foreground">{description}</p>
37
+ ) : null}
38
+ </div>
39
+ {actions ? (
40
+ <div className="flex shrink-0 items-center gap-1.5">{actions}</div>
41
+ ) : null}
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,49 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import { cn } from "../lib/utils.js";
4
+
5
+ /**
6
+ * Title + optional description over a content block. Use to group
7
+ * related controls or stats inside a pane.
8
+ */
9
+ export interface SectionProps {
10
+ title?: ReactNode;
11
+ description?: ReactNode;
12
+ /** Right-aligned action slot next to the title. */
13
+ actions?: ReactNode;
14
+ children: ReactNode;
15
+ className?: string;
16
+ contentClassName?: string;
17
+ }
18
+
19
+ export function Section({
20
+ title,
21
+ description,
22
+ actions,
23
+ children,
24
+ className,
25
+ contentClassName,
26
+ }: SectionProps) {
27
+ return (
28
+ <section className={cn("px-4 py-3", className)}>
29
+ {title || description || actions ? (
30
+ <header className="mb-2 flex items-start justify-between gap-3">
31
+ <div className="min-w-0 flex-1">
32
+ {title ? (
33
+ <h2 className="text-sm font-medium text-foreground">{title}</h2>
34
+ ) : null}
35
+ {description ? (
36
+ <p className="mt-0.5 text-xs text-muted-foreground">
37
+ {description}
38
+ </p>
39
+ ) : null}
40
+ </div>
41
+ {actions ? (
42
+ <div className="flex shrink-0 items-center gap-1.5">{actions}</div>
43
+ ) : null}
44
+ </header>
45
+ ) : null}
46
+ <div className={contentClassName}>{children}</div>
47
+ </section>
48
+ );
49
+ }
@@ -0,0 +1,57 @@
1
+ import type { LucideIcon } from "lucide-react";
2
+ import type { ReactNode } from "react";
3
+
4
+ import { cn } from "../lib/utils.js";
5
+
6
+ /**
7
+ * Small metric display — label on top, value below, optional icon and
8
+ * trend chip. Use in a grid at the top of a dashboard. Stays tight; no
9
+ * shadows or gradients.
10
+ */
11
+ export interface StatPillProps {
12
+ label: ReactNode;
13
+ value: ReactNode;
14
+ icon?: LucideIcon;
15
+ /** Optional trend / hint chip rendered next to the value. */
16
+ trend?: ReactNode;
17
+ /** Visual tone for the value. Default `neutral` (foreground). */
18
+ tone?: "neutral" | "positive" | "negative";
19
+ className?: string;
20
+ }
21
+
22
+ const toneClass: Record<NonNullable<StatPillProps["tone"]>, string> = {
23
+ neutral: "text-foreground",
24
+ positive: "text-emerald-600 dark:text-emerald-400",
25
+ negative: "text-destructive",
26
+ };
27
+
28
+ export function StatPill({
29
+ label,
30
+ value,
31
+ icon: Icon,
32
+ trend,
33
+ tone = "neutral",
34
+ className,
35
+ }: StatPillProps) {
36
+ return (
37
+ <div
38
+ className={cn(
39
+ "flex flex-col gap-1 rounded-lg border border-border bg-card px-3 py-2",
40
+ className,
41
+ )}
42
+ >
43
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
44
+ {Icon ? <Icon className="size-3" strokeWidth={1.6} /> : null}
45
+ <span className="truncate">{label}</span>
46
+ </div>
47
+ <div className="flex items-baseline gap-2">
48
+ <span className={cn("text-lg font-semibold", toneClass[tone])}>
49
+ {value}
50
+ </span>
51
+ {trend ? (
52
+ <span className="text-xs text-muted-foreground">{trend}</span>
53
+ ) : null}
54
+ </div>
55
+ </div>
56
+ );
57
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,76 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "../lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-card text-card-foreground",
12
+ destructive:
13
+ "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ )
21
+
22
+ function Alert({
23
+ className,
24
+ variant,
25
+ ...props
26
+ }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
27
+ return (
28
+ <div
29
+ data-slot="alert"
30
+ role="alert"
31
+ className={cn(alertVariants({ variant }), className)}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38
+ return (
39
+ <div
40
+ data-slot="alert-title"
41
+ className={cn(
42
+ "font-heading font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
43
+ className
44
+ )}
45
+ {...props}
46
+ />
47
+ )
48
+ }
49
+
50
+ function AlertDescription({
51
+ className,
52
+ ...props
53
+ }: React.ComponentProps<"div">) {
54
+ return (
55
+ <div
56
+ data-slot="alert-description"
57
+ className={cn(
58
+ "text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
59
+ className
60
+ )}
61
+ {...props}
62
+ />
63
+ )
64
+ }
65
+
66
+ function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
67
+ return (
68
+ <div
69
+ data-slot="alert-action"
70
+ className={cn("absolute top-2 right-2", className)}
71
+ {...props}
72
+ />
73
+ )
74
+ }
75
+
76
+ export { Alert, AlertTitle, AlertDescription, AlertAction }
@@ -0,0 +1,52 @@
1
+ import { mergeProps } from "@base-ui/react/merge-props"
2
+ import { useRender } from "@base-ui/react/use-render"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "../lib/utils"
6
+
7
+ const badgeVariants = cva(
8
+ "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-colors focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
13
+ secondary:
14
+ "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
15
+ destructive:
16
+ "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
17
+ outline:
18
+ "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
19
+ ghost:
20
+ "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ },
24
+ defaultVariants: {
25
+ variant: "default",
26
+ },
27
+ }
28
+ )
29
+
30
+ function Badge({
31
+ className,
32
+ variant = "default",
33
+ render,
34
+ ...props
35
+ }: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
36
+ return useRender({
37
+ defaultTagName: "span",
38
+ props: mergeProps<"span">(
39
+ {
40
+ className: cn(badgeVariants({ variant }), className),
41
+ },
42
+ props
43
+ ),
44
+ render,
45
+ state: {
46
+ slot: "badge",
47
+ variant,
48
+ },
49
+ })
50
+ }
51
+
52
+ export { Badge, badgeVariants }
@@ -0,0 +1,61 @@
1
+ import { Button as ButtonPrimitive } from "@base-ui/react/button"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "../lib/utils"
5
+
6
+ const buttonVariants = cva(
7
+ "group/button inline-flex shrink-0 items-center justify-center rounded-lg text-sm font-medium whitespace-nowrap transition-colors duration-150 ease-out outline-none select-none focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default:
12
+ "bg-primary text-primary-foreground hover:bg-primary/90 [a]:hover:bg-primary/90",
13
+ bordered:
14
+ "border border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground",
15
+ outline:
16
+ "border border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
19
+ ghost:
20
+ "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted",
21
+ destructive:
22
+ "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
23
+ link: "text-primary underline-offset-4 hover:underline",
24
+ },
25
+ size: {
26
+ default:
27
+ "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
28
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
29
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
30
+ lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
31
+ icon: "size-8",
32
+ "icon-xs":
33
+ "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
34
+ "icon-sm":
35
+ "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
36
+ "icon-lg": "size-9",
37
+ },
38
+ },
39
+ defaultVariants: {
40
+ variant: "default",
41
+ size: "default",
42
+ },
43
+ }
44
+ )
45
+
46
+ function Button({
47
+ className,
48
+ variant = "default",
49
+ size = "default",
50
+ ...props
51
+ }: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
52
+ return (
53
+ <ButtonPrimitive
54
+ data-slot="button"
55
+ className={cn(buttonVariants({ variant, size, className }))}
56
+ {...props}
57
+ />
58
+ )
59
+ }
60
+
61
+ export { Button, buttonVariants }