@emara/ui 1.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.
- package/components/ui/.gitkeep +0 -0
- package/components/ui/accordion.stories.tsx +231 -0
- package/components/ui/accordion.tsx +250 -0
- package/components/ui/app-shell.stories.tsx +270 -0
- package/components/ui/app-shell.tsx +491 -0
- package/components/ui/avatar.stories.tsx +174 -0
- package/components/ui/avatar.tsx +257 -0
- package/components/ui/badge.stories.tsx +127 -0
- package/components/ui/badge.tsx +146 -0
- package/components/ui/breadcrumb.stories.tsx +92 -0
- package/components/ui/breadcrumb.tsx +302 -0
- package/components/ui/button.stories.tsx +186 -0
- package/components/ui/button.tsx +128 -0
- package/components/ui/card.stories.tsx +279 -0
- package/components/ui/card.tsx +250 -0
- package/components/ui/checkbox.stories.tsx +93 -0
- package/components/ui/checkbox.tsx +131 -0
- package/components/ui/combobox.stories.tsx +489 -0
- package/components/ui/combobox.tsx +874 -0
- package/components/ui/context-menu.stories.tsx +202 -0
- package/components/ui/context-menu.tsx +309 -0
- package/components/ui/data-table.stories.tsx +227 -0
- package/components/ui/data-table.tsx +539 -0
- package/components/ui/date-picker.stories.tsx +225 -0
- package/components/ui/date-picker.tsx +597 -0
- package/components/ui/dialog.stories.tsx +193 -0
- package/components/ui/dialog.tsx +262 -0
- package/components/ui/divider.stories.tsx +84 -0
- package/components/ui/divider.tsx +135 -0
- package/components/ui/drawer.stories.tsx +218 -0
- package/components/ui/drawer.tsx +329 -0
- package/components/ui/dropdown-menu.stories.tsx +270 -0
- package/components/ui/dropdown-menu.tsx +353 -0
- package/components/ui/empty-state.stories.tsx +121 -0
- package/components/ui/empty-state.tsx +289 -0
- package/components/ui/field-group.stories.tsx +201 -0
- package/components/ui/field-group.tsx +276 -0
- package/components/ui/form.stories.tsx +219 -0
- package/components/ui/form.tsx +542 -0
- package/components/ui/input.stories.tsx +154 -0
- package/components/ui/input.tsx +208 -0
- package/components/ui/label.stories.tsx +84 -0
- package/components/ui/label.tsx +98 -0
- package/components/ui/page-header.stories.tsx +136 -0
- package/components/ui/page-header.tsx +315 -0
- package/components/ui/pagination.stories.tsx +136 -0
- package/components/ui/pagination.tsx +427 -0
- package/components/ui/popover.stories.tsx +212 -0
- package/components/ui/popover.tsx +167 -0
- package/components/ui/radio-group.stories.tsx +96 -0
- package/components/ui/radio-group.tsx +250 -0
- package/components/ui/select.stories.tsx +203 -0
- package/components/ui/select.tsx +318 -0
- package/components/ui/sidebar.stories.tsx +186 -0
- package/components/ui/sidebar.tsx +623 -0
- package/components/ui/skeleton.stories.tsx +131 -0
- package/components/ui/skeleton.tsx +311 -0
- package/components/ui/switch.stories.tsx +74 -0
- package/components/ui/switch.tsx +186 -0
- package/components/ui/table.stories.tsx +107 -0
- package/components/ui/table.tsx +285 -0
- package/components/ui/tabs.stories.tsx +222 -0
- package/components/ui/tabs.tsx +287 -0
- package/components/ui/textarea.stories.tsx +96 -0
- package/components/ui/textarea.tsx +182 -0
- package/components/ui/toast.stories.tsx +169 -0
- package/components/ui/toast.tsx +250 -0
- package/components/ui/tooltip.stories.tsx +146 -0
- package/components/ui/tooltip.tsx +156 -0
- package/components/ui/top-bar.stories.tsx +182 -0
- package/components/ui/top-bar.tsx +155 -0
- package/dist/components/ui/accordion.d.ts +45 -0
- package/dist/components/ui/accordion.d.ts.map +1 -0
- package/dist/components/ui/accordion.js +99 -0
- package/dist/components/ui/accordion.js.map +1 -0
- package/dist/components/ui/app-shell.d.ts +70 -0
- package/dist/components/ui/app-shell.d.ts.map +1 -0
- package/dist/components/ui/app-shell.js +199 -0
- package/dist/components/ui/app-shell.js.map +1 -0
- package/dist/components/ui/avatar.d.ts +41 -0
- package/dist/components/ui/avatar.d.ts.map +1 -0
- package/dist/components/ui/avatar.js +104 -0
- package/dist/components/ui/avatar.js.map +1 -0
- package/dist/components/ui/badge.d.ts +27 -0
- package/dist/components/ui/badge.d.ts.map +1 -0
- package/dist/components/ui/badge.js +65 -0
- package/dist/components/ui/badge.js.map +1 -0
- package/dist/components/ui/breadcrumb.d.ts +35 -0
- package/dist/components/ui/breadcrumb.d.ts.map +1 -0
- package/dist/components/ui/breadcrumb.js +88 -0
- package/dist/components/ui/breadcrumb.js.map +1 -0
- package/dist/components/ui/button.d.ts +26 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/button.js +73 -0
- package/dist/components/ui/button.js.map +1 -0
- package/dist/components/ui/card.d.ts +52 -0
- package/dist/components/ui/card.d.ts.map +1 -0
- package/dist/components/ui/card.js +96 -0
- package/dist/components/ui/card.js.map +1 -0
- package/dist/components/ui/checkbox.d.ts +18 -0
- package/dist/components/ui/checkbox.d.ts.map +1 -0
- package/dist/components/ui/checkbox.js +59 -0
- package/dist/components/ui/checkbox.js.map +1 -0
- package/dist/components/ui/combobox.d.ts +194 -0
- package/dist/components/ui/combobox.d.ts.map +1 -0
- package/dist/components/ui/combobox.js +361 -0
- package/dist/components/ui/combobox.js.map +1 -0
- package/dist/components/ui/context-menu.d.ts +46 -0
- package/dist/components/ui/context-menu.d.ts.map +1 -0
- package/dist/components/ui/context-menu.js +95 -0
- package/dist/components/ui/context-menu.js.map +1 -0
- package/dist/components/ui/data-table.d.ts +53 -0
- package/dist/components/ui/data-table.d.ts.map +1 -0
- package/dist/components/ui/data-table.js +163 -0
- package/dist/components/ui/data-table.js.map +1 -0
- package/dist/components/ui/date-picker.d.ts +103 -0
- package/dist/components/ui/date-picker.d.ts.map +1 -0
- package/dist/components/ui/date-picker.js +306 -0
- package/dist/components/ui/date-picker.js.map +1 -0
- package/dist/components/ui/dialog.d.ts +40 -0
- package/dist/components/ui/dialog.d.ts.map +1 -0
- package/dist/components/ui/dialog.js +110 -0
- package/dist/components/ui/dialog.js.map +1 -0
- package/dist/components/ui/divider.d.ts +30 -0
- package/dist/components/ui/divider.d.ts.map +1 -0
- package/dist/components/ui/divider.js +62 -0
- package/dist/components/ui/divider.js.map +1 -0
- package/dist/components/ui/drawer.d.ts +56 -0
- package/dist/components/ui/drawer.d.ts.map +1 -0
- package/dist/components/ui/drawer.js +147 -0
- package/dist/components/ui/drawer.js.map +1 -0
- package/dist/components/ui/dropdown-menu.d.ts +63 -0
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
- package/dist/components/ui/dropdown-menu.js +116 -0
- package/dist/components/ui/dropdown-menu.js.map +1 -0
- package/dist/components/ui/empty-state.d.ts +43 -0
- package/dist/components/ui/empty-state.d.ts.map +1 -0
- package/dist/components/ui/empty-state.js +128 -0
- package/dist/components/ui/empty-state.js.map +1 -0
- package/dist/components/ui/field-group.d.ts +38 -0
- package/dist/components/ui/field-group.d.ts.map +1 -0
- package/dist/components/ui/field-group.js +107 -0
- package/dist/components/ui/field-group.js.map +1 -0
- package/dist/components/ui/form.d.ts +67 -0
- package/dist/components/ui/form.d.ts.map +1 -0
- package/dist/components/ui/form.js +286 -0
- package/dist/components/ui/form.js.map +1 -0
- package/dist/components/ui/input.d.ts +36 -0
- package/dist/components/ui/input.d.ts.map +1 -0
- package/dist/components/ui/input.js +99 -0
- package/dist/components/ui/input.js.map +1 -0
- package/dist/components/ui/label.d.ts +37 -0
- package/dist/components/ui/label.d.ts.map +1 -0
- package/dist/components/ui/label.js +34 -0
- package/dist/components/ui/label.js.map +1 -0
- package/dist/components/ui/page-header.d.ts +65 -0
- package/dist/components/ui/page-header.d.ts.map +1 -0
- package/dist/components/ui/page-header.js +140 -0
- package/dist/components/ui/page-header.js.map +1 -0
- package/dist/components/ui/pagination.d.ts +67 -0
- package/dist/components/ui/pagination.d.ts.map +1 -0
- package/dist/components/ui/pagination.js +109 -0
- package/dist/components/ui/pagination.js.map +1 -0
- package/dist/components/ui/popover.d.ts +28 -0
- package/dist/components/ui/popover.d.ts.map +1 -0
- package/dist/components/ui/popover.js +85 -0
- package/dist/components/ui/popover.js.map +1 -0
- package/dist/components/ui/radio-group.d.ts +35 -0
- package/dist/components/ui/radio-group.d.ts.map +1 -0
- package/dist/components/ui/radio-group.js +103 -0
- package/dist/components/ui/radio-group.js.map +1 -0
- package/dist/components/ui/select.d.ts +42 -0
- package/dist/components/ui/select.d.ts.map +1 -0
- package/dist/components/ui/select.js +86 -0
- package/dist/components/ui/select.js.map +1 -0
- package/dist/components/ui/sidebar.d.ts +59 -0
- package/dist/components/ui/sidebar.d.ts.map +1 -0
- package/dist/components/ui/sidebar.js +189 -0
- package/dist/components/ui/sidebar.js.map +1 -0
- package/dist/components/ui/skeleton.d.ts +77 -0
- package/dist/components/ui/skeleton.d.ts.map +1 -0
- package/dist/components/ui/skeleton.js +115 -0
- package/dist/components/ui/skeleton.js.map +1 -0
- package/dist/components/ui/switch.d.ts +26 -0
- package/dist/components/ui/switch.d.ts.map +1 -0
- package/dist/components/ui/switch.js +84 -0
- package/dist/components/ui/switch.js.map +1 -0
- package/dist/components/ui/table.d.ts +52 -0
- package/dist/components/ui/table.d.ts.map +1 -0
- package/dist/components/ui/table.js +109 -0
- package/dist/components/ui/table.js.map +1 -0
- package/dist/components/ui/tabs.d.ts +42 -0
- package/dist/components/ui/tabs.d.ts.map +1 -0
- package/dist/components/ui/tabs.js +163 -0
- package/dist/components/ui/tabs.js.map +1 -0
- package/dist/components/ui/textarea.d.ts +26 -0
- package/dist/components/ui/textarea.d.ts.map +1 -0
- package/dist/components/ui/textarea.js +96 -0
- package/dist/components/ui/textarea.js.map +1 -0
- package/dist/components/ui/toast.d.ts +77 -0
- package/dist/components/ui/toast.d.ts.map +1 -0
- package/dist/components/ui/toast.js +141 -0
- package/dist/components/ui/toast.js.map +1 -0
- package/dist/components/ui/tooltip.d.ts +31 -0
- package/dist/components/ui/tooltip.d.ts.map +1 -0
- package/dist/components/ui/tooltip.js +71 -0
- package/dist/components/ui/tooltip.js.map +1 -0
- package/dist/components/ui/top-bar.d.ts +30 -0
- package/dist/components/ui/top-bar.d.ts.map +1 -0
- package/dist/components/ui/top-bar.js +64 -0
- package/dist/components/ui/top-bar.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/lib/utils.ts +6 -0
- package/package.json +112 -0
- package/styles/globals.css +685 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Skeleton,
|
|
5
|
+
SkeletonButton,
|
|
6
|
+
SkeletonCard,
|
|
7
|
+
SkeletonCircle,
|
|
8
|
+
SkeletonInput,
|
|
9
|
+
SkeletonTable,
|
|
10
|
+
SkeletonText,
|
|
11
|
+
} from "./skeleton";
|
|
12
|
+
|
|
13
|
+
const meta: Meta<typeof Skeleton> = {
|
|
14
|
+
title: "Foundations/Skeleton",
|
|
15
|
+
component: Skeleton,
|
|
16
|
+
parameters: { layout: "centered" },
|
|
17
|
+
argTypes: {
|
|
18
|
+
animation: { control: "select", options: ["pulse", "shimmer", "none"] },
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
type Story = StoryObj<typeof Skeleton>;
|
|
24
|
+
|
|
25
|
+
export const Default: Story = {
|
|
26
|
+
args: { className: "h-4 w-48" },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const Animations: Story = {
|
|
30
|
+
render: () => (
|
|
31
|
+
<div className="w-96 space-y-4">
|
|
32
|
+
<div className="space-y-1">
|
|
33
|
+
<p className="text-muted-foreground text-xs">pulse</p>
|
|
34
|
+
<Skeleton animation="pulse" className="h-4 w-full" />
|
|
35
|
+
</div>
|
|
36
|
+
<div className="space-y-1">
|
|
37
|
+
<p className="text-muted-foreground text-xs">shimmer</p>
|
|
38
|
+
<Skeleton animation="shimmer" className="h-4 w-full" />
|
|
39
|
+
</div>
|
|
40
|
+
<div className="space-y-1">
|
|
41
|
+
<p className="text-muted-foreground text-xs">none</p>
|
|
42
|
+
<Skeleton animation="none" className="h-4 w-full" />
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const Text: Story = {
|
|
49
|
+
render: () => (
|
|
50
|
+
<div className="w-96">
|
|
51
|
+
<SkeletonText lines={4} />
|
|
52
|
+
</div>
|
|
53
|
+
),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const Circles: Story = {
|
|
57
|
+
render: () => (
|
|
58
|
+
<div className="flex items-end gap-3">
|
|
59
|
+
<SkeletonCircle size="xs" />
|
|
60
|
+
<SkeletonCircle size="sm" />
|
|
61
|
+
<SkeletonCircle size="md" />
|
|
62
|
+
<SkeletonCircle size="lg" />
|
|
63
|
+
<SkeletonCircle size="xl" />
|
|
64
|
+
<SkeletonCircle size="2xl" />
|
|
65
|
+
</div>
|
|
66
|
+
),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* SkeletonCircle with the shimmer animation. Validates that the parent
|
|
71
|
+
* Skeleton's shimmer band (via `before:` pseudo-element) correctly clips
|
|
72
|
+
* to the circle shape and is visible against the base fill.
|
|
73
|
+
*/
|
|
74
|
+
export const CirclesWithShimmer: Story = {
|
|
75
|
+
render: () => (
|
|
76
|
+
<div className="flex items-end gap-3">
|
|
77
|
+
<SkeletonCircle size="sm" animation="shimmer" />
|
|
78
|
+
<SkeletonCircle size="md" animation="shimmer" />
|
|
79
|
+
<SkeletonCircle size="lg" animation="shimmer" />
|
|
80
|
+
<SkeletonCircle size="xl" animation="shimmer" />
|
|
81
|
+
</div>
|
|
82
|
+
),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const Buttons: Story = {
|
|
86
|
+
render: () => (
|
|
87
|
+
<div className="flex items-end gap-3">
|
|
88
|
+
<SkeletonButton size="xs" />
|
|
89
|
+
<SkeletonButton size="sm" />
|
|
90
|
+
<SkeletonButton size="default" />
|
|
91
|
+
<SkeletonButton size="lg" />
|
|
92
|
+
<SkeletonButton size="xl" />
|
|
93
|
+
</div>
|
|
94
|
+
),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const Inputs: Story = {
|
|
98
|
+
render: () => (
|
|
99
|
+
<div className="w-72 space-y-2">
|
|
100
|
+
<SkeletonInput size="xs" />
|
|
101
|
+
<SkeletonInput size="sm" />
|
|
102
|
+
<SkeletonInput size="md" />
|
|
103
|
+
<SkeletonInput size="lg" />
|
|
104
|
+
<SkeletonInput size="xl" />
|
|
105
|
+
</div>
|
|
106
|
+
),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const Card: Story = {
|
|
110
|
+
render: () => (
|
|
111
|
+
<div className="w-96">
|
|
112
|
+
<SkeletonCard />
|
|
113
|
+
</div>
|
|
114
|
+
),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const Table: Story = {
|
|
118
|
+
render: () => (
|
|
119
|
+
<div className="w-[40rem]">
|
|
120
|
+
<SkeletonTable rows={5} cols={4} />
|
|
121
|
+
</div>
|
|
122
|
+
),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const ShimmerLayout: Story = {
|
|
126
|
+
render: () => (
|
|
127
|
+
<div className="w-96">
|
|
128
|
+
<SkeletonCard animation="shimmer" />
|
|
129
|
+
</div>
|
|
130
|
+
),
|
|
131
|
+
};
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
|
|
9
|
+
// Per docs/emara-ui-phase-1-components.md §9.
|
|
10
|
+
|
|
11
|
+
const skeletonVariants = cva("rounded-md bg-foreground/10", {
|
|
12
|
+
variants: {
|
|
13
|
+
animation: {
|
|
14
|
+
pulse: "animate-pulse",
|
|
15
|
+
shimmer: [
|
|
16
|
+
"relative overflow-hidden isolate",
|
|
17
|
+
// Sliding band — a contrasting tone sweeps across the base fill.
|
|
18
|
+
// Base is `bg-foreground/10`; the band is `foreground/35` so it's
|
|
19
|
+
// visibly darker in light mode and visibly lighter in dark mode.
|
|
20
|
+
// The keyframe `shimmer-slide` (in globals.css) animates translateX
|
|
21
|
+
// and the cascade flips direction for RTL.
|
|
22
|
+
"before:absolute before:inset-0 before:content-[''] before:z-10 before:pointer-events-none",
|
|
23
|
+
"before:bg-gradient-to-r before:from-transparent before:via-foreground/35 before:to-transparent",
|
|
24
|
+
"before:animate-[shimmer-slide_1.2s_linear_infinite]",
|
|
25
|
+
].join(" "),
|
|
26
|
+
none: "",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: {
|
|
30
|
+
animation: "pulse",
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
type SkeletonVariants = VariantProps<typeof skeletonVariants>;
|
|
35
|
+
|
|
36
|
+
type SkeletonProps = React.HTMLAttributes<HTMLDivElement> &
|
|
37
|
+
SkeletonVariants & {
|
|
38
|
+
asChild?: boolean;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const Skeleton = forwardRef<HTMLDivElement, SkeletonProps>(function Skeleton(
|
|
42
|
+
{ className, animation, asChild = false, ...props },
|
|
43
|
+
ref,
|
|
44
|
+
) {
|
|
45
|
+
const Comp = asChild ? Slot : "div";
|
|
46
|
+
return (
|
|
47
|
+
<Comp
|
|
48
|
+
ref={ref}
|
|
49
|
+
aria-hidden="true"
|
|
50
|
+
className={cn(skeletonVariants({ animation }), className)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
Skeleton.displayName = "Skeleton";
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Preset shapes (Emara additions). All are aria-hidden via the base Skeleton.
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
// --- SkeletonText -----------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
type SkeletonTextProps = Omit<SkeletonProps, "asChild"> & {
|
|
64
|
+
lines?: number;
|
|
65
|
+
/** Width of the last line as a fraction of full width. Default 0.6. */
|
|
66
|
+
lastLineWidth?: number;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Pre-computed "realistic" widths for paragraphs. Repeats after this many lines.
|
|
70
|
+
const TEXT_LINE_WIDTHS = ["w-full", "w-11/12", "w-full", "w-10/12", "w-full"];
|
|
71
|
+
|
|
72
|
+
const SkeletonText = forwardRef<HTMLDivElement, SkeletonTextProps>(function SkeletonText(
|
|
73
|
+
{ className, animation, lines = 3, lastLineWidth = 0.6, ...props },
|
|
74
|
+
ref,
|
|
75
|
+
) {
|
|
76
|
+
const lastWidthStyle: React.CSSProperties = { width: `${Math.round(lastLineWidth * 100)}%` };
|
|
77
|
+
return (
|
|
78
|
+
<div ref={ref} aria-hidden="true" className={cn("space-y-2", className)} {...props}>
|
|
79
|
+
{Array.from({ length: Math.max(1, lines) }, (_, i) => {
|
|
80
|
+
const isLast = i === lines - 1 && lines > 1;
|
|
81
|
+
const widthClass = isLast
|
|
82
|
+
? ""
|
|
83
|
+
: (TEXT_LINE_WIDTHS[i % TEXT_LINE_WIDTHS.length] ?? "w-full");
|
|
84
|
+
return (
|
|
85
|
+
<Skeleton
|
|
86
|
+
key={i}
|
|
87
|
+
animation={animation}
|
|
88
|
+
className={cn("h-4", widthClass)}
|
|
89
|
+
style={isLast ? lastWidthStyle : undefined}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
})}
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
SkeletonText.displayName = "SkeletonText";
|
|
97
|
+
|
|
98
|
+
// --- SkeletonCircle ---------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
type SkeletonCircleSize = "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
|
|
101
|
+
|
|
102
|
+
// Note: animation is intentionally NOT part of this variant set — the parent
|
|
103
|
+
// <Skeleton> already supplies the right classes for pulse / shimmer / none
|
|
104
|
+
// (including the relative+overflow-hidden+::before band that shimmer needs).
|
|
105
|
+
// Duplicating animation here used to strip the shimmer styling, so the audit
|
|
106
|
+
// flagged "shimmer doesn't work on SkeletonCircle".
|
|
107
|
+
const skeletonCircleVariants = cva("rounded-full", {
|
|
108
|
+
variants: {
|
|
109
|
+
size: {
|
|
110
|
+
xs: "size-6",
|
|
111
|
+
sm: "size-8",
|
|
112
|
+
md: "size-10",
|
|
113
|
+
lg: "size-12",
|
|
114
|
+
xl: "size-16",
|
|
115
|
+
"2xl": "size-24",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
defaultVariants: { size: "md" },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
type SkeletonCircleProps = Omit<React.HTMLAttributes<HTMLDivElement>, "children"> & {
|
|
122
|
+
size?: SkeletonCircleSize;
|
|
123
|
+
animation?: SkeletonVariants["animation"];
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const SkeletonCircle = forwardRef<HTMLDivElement, SkeletonCircleProps>(function SkeletonCircle(
|
|
127
|
+
{ className, size, animation = "pulse", ...props },
|
|
128
|
+
ref,
|
|
129
|
+
) {
|
|
130
|
+
return (
|
|
131
|
+
<Skeleton
|
|
132
|
+
ref={ref}
|
|
133
|
+
animation={animation}
|
|
134
|
+
className={cn(skeletonCircleVariants({ size }), className)}
|
|
135
|
+
{...props}
|
|
136
|
+
/>
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
SkeletonCircle.displayName = "SkeletonCircle";
|
|
140
|
+
|
|
141
|
+
// --- SkeletonButton ---------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
type SkeletonButtonSize = "xs" | "sm" | "default" | "lg" | "xl";
|
|
144
|
+
|
|
145
|
+
const skeletonButtonVariants = cva("rounded-md bg-foreground/10", {
|
|
146
|
+
variants: {
|
|
147
|
+
size: {
|
|
148
|
+
xs: "h-7",
|
|
149
|
+
sm: "h-8",
|
|
150
|
+
default: "h-9",
|
|
151
|
+
lg: "h-10",
|
|
152
|
+
xl: "h-12",
|
|
153
|
+
},
|
|
154
|
+
width: {
|
|
155
|
+
auto: "w-24",
|
|
156
|
+
full: "w-full",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
defaultVariants: { size: "default", width: "auto" },
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
type SkeletonButtonProps = Omit<React.HTMLAttributes<HTMLDivElement>, "children"> & {
|
|
163
|
+
size?: SkeletonButtonSize;
|
|
164
|
+
width?: "auto" | "full";
|
|
165
|
+
animation?: SkeletonVariants["animation"];
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const SkeletonButton = forwardRef<HTMLDivElement, SkeletonButtonProps>(function SkeletonButton(
|
|
169
|
+
{ className, size, width, animation = "pulse", ...props },
|
|
170
|
+
ref,
|
|
171
|
+
) {
|
|
172
|
+
return (
|
|
173
|
+
<Skeleton
|
|
174
|
+
ref={ref}
|
|
175
|
+
animation={animation}
|
|
176
|
+
className={cn(skeletonButtonVariants({ size, width }), className)}
|
|
177
|
+
{...props}
|
|
178
|
+
/>
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
SkeletonButton.displayName = "SkeletonButton";
|
|
182
|
+
|
|
183
|
+
// --- SkeletonInput ----------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
type SkeletonInputSize = "xs" | "sm" | "md" | "lg" | "xl";
|
|
186
|
+
|
|
187
|
+
const skeletonInputVariants = cva("rounded-md bg-foreground/10 w-full", {
|
|
188
|
+
variants: {
|
|
189
|
+
size: {
|
|
190
|
+
xs: "h-7",
|
|
191
|
+
sm: "h-8",
|
|
192
|
+
md: "h-9",
|
|
193
|
+
lg: "h-10",
|
|
194
|
+
xl: "h-12",
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
defaultVariants: { size: "md" },
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
type SkeletonInputProps = Omit<React.HTMLAttributes<HTMLDivElement>, "children"> & {
|
|
201
|
+
size?: SkeletonInputSize;
|
|
202
|
+
animation?: SkeletonVariants["animation"];
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const SkeletonInput = forwardRef<HTMLDivElement, SkeletonInputProps>(function SkeletonInput(
|
|
206
|
+
{ className, size, animation = "pulse", ...props },
|
|
207
|
+
ref,
|
|
208
|
+
) {
|
|
209
|
+
return (
|
|
210
|
+
<Skeleton
|
|
211
|
+
ref={ref}
|
|
212
|
+
animation={animation}
|
|
213
|
+
className={cn(skeletonInputVariants({ size }), className)}
|
|
214
|
+
{...props}
|
|
215
|
+
/>
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
SkeletonInput.displayName = "SkeletonInput";
|
|
219
|
+
|
|
220
|
+
// --- SkeletonCard -----------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
type SkeletonCardProps = Omit<React.HTMLAttributes<HTMLDivElement>, "children"> & {
|
|
223
|
+
/** Show the header section (title + description). Default true. */
|
|
224
|
+
header?: boolean;
|
|
225
|
+
/** Number of body lines. Default 3. */
|
|
226
|
+
lines?: number;
|
|
227
|
+
animation?: SkeletonVariants["animation"];
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const SkeletonCard = forwardRef<HTMLDivElement, SkeletonCardProps>(function SkeletonCard(
|
|
231
|
+
{ className, header = true, lines = 3, animation = "pulse", ...props },
|
|
232
|
+
ref,
|
|
233
|
+
) {
|
|
234
|
+
return (
|
|
235
|
+
<div
|
|
236
|
+
ref={ref}
|
|
237
|
+
aria-hidden="true"
|
|
238
|
+
className={cn("border-border space-y-4 rounded-md border p-4", className)}
|
|
239
|
+
{...props}
|
|
240
|
+
>
|
|
241
|
+
{header ? (
|
|
242
|
+
<div className="space-y-2">
|
|
243
|
+
<Skeleton animation={animation} className="h-5 w-2/5" />
|
|
244
|
+
<Skeleton animation={animation} className="h-4 w-3/5" />
|
|
245
|
+
</div>
|
|
246
|
+
) : null}
|
|
247
|
+
<SkeletonText animation={animation} lines={lines} />
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
SkeletonCard.displayName = "SkeletonCard";
|
|
252
|
+
|
|
253
|
+
// --- SkeletonTable ----------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
type SkeletonTableProps = Omit<React.HTMLAttributes<HTMLDivElement>, "children"> & {
|
|
256
|
+
rows?: number;
|
|
257
|
+
cols?: number;
|
|
258
|
+
animation?: SkeletonVariants["animation"];
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const SkeletonTable = forwardRef<HTMLDivElement, SkeletonTableProps>(function SkeletonTable(
|
|
262
|
+
{ className, rows = 5, cols = 4, animation = "pulse", ...props },
|
|
263
|
+
ref,
|
|
264
|
+
) {
|
|
265
|
+
return (
|
|
266
|
+
<div ref={ref} aria-hidden="true" className={cn("space-y-2", className)} {...props}>
|
|
267
|
+
{/* Header row */}
|
|
268
|
+
<div
|
|
269
|
+
className="grid gap-3"
|
|
270
|
+
style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }}
|
|
271
|
+
>
|
|
272
|
+
{Array.from({ length: cols }, (_, c) => (
|
|
273
|
+
<Skeleton key={`h-${c}`} animation={animation} className="h-4" />
|
|
274
|
+
))}
|
|
275
|
+
</div>
|
|
276
|
+
{/* Body rows */}
|
|
277
|
+
{Array.from({ length: rows }, (_, r) => (
|
|
278
|
+
<div
|
|
279
|
+
key={r}
|
|
280
|
+
className="grid gap-3"
|
|
281
|
+
style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }}
|
|
282
|
+
>
|
|
283
|
+
{Array.from({ length: cols }, (_, c) => (
|
|
284
|
+
<Skeleton key={`${r}-${c}`} animation={animation} className="h-4" />
|
|
285
|
+
))}
|
|
286
|
+
</div>
|
|
287
|
+
))}
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
SkeletonTable.displayName = "SkeletonTable";
|
|
292
|
+
|
|
293
|
+
export {
|
|
294
|
+
Skeleton,
|
|
295
|
+
SkeletonText,
|
|
296
|
+
SkeletonCircle,
|
|
297
|
+
SkeletonButton,
|
|
298
|
+
SkeletonInput,
|
|
299
|
+
SkeletonCard,
|
|
300
|
+
SkeletonTable,
|
|
301
|
+
skeletonVariants,
|
|
302
|
+
};
|
|
303
|
+
export type {
|
|
304
|
+
SkeletonProps,
|
|
305
|
+
SkeletonTextProps,
|
|
306
|
+
SkeletonCircleProps,
|
|
307
|
+
SkeletonButtonProps,
|
|
308
|
+
SkeletonInputProps,
|
|
309
|
+
SkeletonCardProps,
|
|
310
|
+
SkeletonTableProps,
|
|
311
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { RiMoonLine, RiSunLine } from "@remixicon/react";
|
|
3
|
+
|
|
4
|
+
import { Switch } from "./switch";
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Switch> = {
|
|
7
|
+
title: "Forms/Switch",
|
|
8
|
+
component: Switch,
|
|
9
|
+
parameters: { layout: "centered" },
|
|
10
|
+
argTypes: {
|
|
11
|
+
size: { control: "select", options: ["sm", "md", "lg"] },
|
|
12
|
+
loading: { control: "boolean" },
|
|
13
|
+
disabled: { control: "boolean" },
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof Switch>;
|
|
19
|
+
|
|
20
|
+
export const Default: Story = {};
|
|
21
|
+
|
|
22
|
+
export const Sizes: Story = {
|
|
23
|
+
render: () => (
|
|
24
|
+
<div className="flex items-center gap-4">
|
|
25
|
+
<Switch size="sm" defaultChecked />
|
|
26
|
+
<Switch size="md" defaultChecked />
|
|
27
|
+
<Switch size="lg" defaultChecked />
|
|
28
|
+
</div>
|
|
29
|
+
),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const States: Story = {
|
|
33
|
+
render: () => (
|
|
34
|
+
<div className="grid grid-cols-2 gap-3 text-sm">
|
|
35
|
+
<span>off</span>
|
|
36
|
+
<Switch />
|
|
37
|
+
<span>on</span>
|
|
38
|
+
<Switch defaultChecked />
|
|
39
|
+
<span>disabled off</span>
|
|
40
|
+
<Switch disabled />
|
|
41
|
+
<span>disabled on</span>
|
|
42
|
+
<Switch disabled defaultChecked />
|
|
43
|
+
<span>loading</span>
|
|
44
|
+
<Switch loading defaultChecked />
|
|
45
|
+
</div>
|
|
46
|
+
),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const PairedWithLabel: Story = {
|
|
50
|
+
render: () => (
|
|
51
|
+
<div className="flex items-center gap-3">
|
|
52
|
+
<Switch id="airplane" />
|
|
53
|
+
<label htmlFor="airplane" className="cursor-pointer text-sm font-medium">
|
|
54
|
+
Airplane mode
|
|
55
|
+
</label>
|
|
56
|
+
</div>
|
|
57
|
+
),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const WithIcons: Story = {
|
|
61
|
+
render: () => (
|
|
62
|
+
<Switch
|
|
63
|
+
defaultChecked
|
|
64
|
+
size="lg"
|
|
65
|
+
onIcon={<RiMoonLine />}
|
|
66
|
+
offIcon={<RiSunLine />}
|
|
67
|
+
aria-label="Theme"
|
|
68
|
+
/>
|
|
69
|
+
),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const WithSideLabels: Story = {
|
|
73
|
+
render: () => <Switch defaultChecked size="lg" onLabel="ON" offLabel="OFF" aria-label="Power" />,
|
|
74
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
|
5
|
+
import { RiLoader2Line } from "@remixicon/react";
|
|
6
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
7
|
+
|
|
8
|
+
import { cn } from "@/lib/utils";
|
|
9
|
+
|
|
10
|
+
// Per docs/emara-ui-phase-2-components.md §3.
|
|
11
|
+
|
|
12
|
+
const switchRootVariants = cva(
|
|
13
|
+
[
|
|
14
|
+
"peer inline-flex shrink-0 cursor-pointer items-center rounded-full",
|
|
15
|
+
"border border-transparent transition-colors",
|
|
16
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
17
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
18
|
+
"data-[state=checked]:bg-primary",
|
|
19
|
+
"data-[state=unchecked]:bg-input",
|
|
20
|
+
"aria-busy:cursor-progress",
|
|
21
|
+
].join(" "),
|
|
22
|
+
{
|
|
23
|
+
variants: {
|
|
24
|
+
size: {
|
|
25
|
+
// [track width × height], thumb size handled via the thumb's variants.
|
|
26
|
+
// Track widths leave enough room for short side-labels in the empty
|
|
27
|
+
// half (~22px at md, ~26px at lg).
|
|
28
|
+
sm: "h-5 w-10 px-0.5",
|
|
29
|
+
md: "h-6 w-12 px-0.5",
|
|
30
|
+
lg: "h-7 w-16 px-0.5",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
defaultVariants: { size: "md" },
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const switchThumbVariants = cva(
|
|
38
|
+
[
|
|
39
|
+
"pointer-events-none block rounded-full bg-background shadow-sm",
|
|
40
|
+
"transition-transform duration-fast",
|
|
41
|
+
// RTL: in `dir="rtl"` the "checked" position is on the visual start
|
|
42
|
+
// (LTR right == RTL left). Use a CSS variable so the transform flips.
|
|
43
|
+
"translate-x-0",
|
|
44
|
+
].join(" "),
|
|
45
|
+
{
|
|
46
|
+
variants: {
|
|
47
|
+
size: {
|
|
48
|
+
// Translation = (track width - thumb size - 2 × px-0.5).
|
|
49
|
+
// sm: 40 - 16 - 4 = 20 → translate-x-5
|
|
50
|
+
// md: 48 - 20 - 4 = 24 → translate-x-6
|
|
51
|
+
// lg: 64 - 24 - 4 = 36 → translate-x-9
|
|
52
|
+
sm: "size-4 data-[state=checked]:translate-x-5 rtl:data-[state=checked]:-translate-x-5",
|
|
53
|
+
md: "size-5 data-[state=checked]:translate-x-6 rtl:data-[state=checked]:-translate-x-6",
|
|
54
|
+
lg: "size-6 data-[state=checked]:translate-x-9 rtl:data-[state=checked]:-translate-x-9",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
defaultVariants: { size: "md" },
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const thumbIconSize: Record<"sm" | "md" | "lg", string> = {
|
|
62
|
+
sm: "size-2.5",
|
|
63
|
+
md: "size-3",
|
|
64
|
+
lg: "size-3.5",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Side-label sizes are intentionally **sub-canonical** (below the type-scale's
|
|
68
|
+
// `--text-2xs` step at 11px) — they're decorative micro-labels that sit inside
|
|
69
|
+
// the empty half of the track, not body text. Per design-tokens §4.2's note on
|
|
70
|
+
// sub-canonical micro-labels: arbitrary px values are allowed here precisely
|
|
71
|
+
// because these aren't part of the readable type scale. No `tracking-wide`
|
|
72
|
+
// — it would expand the text and clip it against the track edge.
|
|
73
|
+
const sideLabelSize: Record<"sm" | "md" | "lg", string> = {
|
|
74
|
+
sm: "text-[8px]",
|
|
75
|
+
md: "text-[9px]",
|
|
76
|
+
lg: "text-[10px]",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type SwitchVariants = VariantProps<typeof switchRootVariants>;
|
|
80
|
+
|
|
81
|
+
type SwitchProps = Omit<React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>, "size"> &
|
|
82
|
+
SwitchVariants & {
|
|
83
|
+
loading?: boolean;
|
|
84
|
+
onIcon?: React.ReactNode;
|
|
85
|
+
offIcon?: React.ReactNode;
|
|
86
|
+
onLabel?: string;
|
|
87
|
+
offLabel?: string;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const Switch = forwardRef<React.ElementRef<typeof SwitchPrimitive.Root>, SwitchProps>(
|
|
91
|
+
function Switch(
|
|
92
|
+
{
|
|
93
|
+
className,
|
|
94
|
+
size = "md",
|
|
95
|
+
loading = false,
|
|
96
|
+
onIcon,
|
|
97
|
+
offIcon,
|
|
98
|
+
onLabel,
|
|
99
|
+
offLabel,
|
|
100
|
+
disabled,
|
|
101
|
+
checked,
|
|
102
|
+
defaultChecked,
|
|
103
|
+
...props
|
|
104
|
+
},
|
|
105
|
+
ref,
|
|
106
|
+
) {
|
|
107
|
+
const resolvedSize = size ?? "md";
|
|
108
|
+
const isDisabled = disabled || loading;
|
|
109
|
+
// exactOptionalPropertyTypes: explicitly omit `checked`/`defaultChecked`
|
|
110
|
+
// when undefined rather than passing through.
|
|
111
|
+
const controlProps: Pick<
|
|
112
|
+
React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>,
|
|
113
|
+
"checked" | "defaultChecked"
|
|
114
|
+
> = {};
|
|
115
|
+
if (checked !== undefined) controlProps.checked = checked;
|
|
116
|
+
if (defaultChecked !== undefined) controlProps.defaultChecked = defaultChecked;
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<SwitchPrimitive.Root
|
|
120
|
+
ref={ref}
|
|
121
|
+
disabled={isDisabled}
|
|
122
|
+
aria-busy={loading || undefined}
|
|
123
|
+
{...controlProps}
|
|
124
|
+
className={cn(switchRootVariants({ size }), "relative", className)}
|
|
125
|
+
{...props}
|
|
126
|
+
>
|
|
127
|
+
{/* Optional inline side-labels. The Root carries `data-state`; child
|
|
128
|
+
spans react via the `in-data-[state=...]` Tailwind v4 modifier so the
|
|
129
|
+
label that's NOT covered by the thumb is the one visible:
|
|
130
|
+
- on → thumb at end → ON label appears at start
|
|
131
|
+
- off → thumb at start → OFF label appears at end
|
|
132
|
+
Pointer-events-none so the labels never block the toggle. */}
|
|
133
|
+
{onLabel ? (
|
|
134
|
+
<span
|
|
135
|
+
aria-hidden="true"
|
|
136
|
+
className={cn(
|
|
137
|
+
// Sit at the start side (empty when on), vertically centered.
|
|
138
|
+
"text-primary-foreground pointer-events-none absolute inset-y-0 start-1 inline-flex items-center leading-none font-semibold uppercase select-none",
|
|
139
|
+
sideLabelSize[resolvedSize],
|
|
140
|
+
"opacity-0 in-data-[state=checked]:opacity-100",
|
|
141
|
+
)}
|
|
142
|
+
>
|
|
143
|
+
{onLabel}
|
|
144
|
+
</span>
|
|
145
|
+
) : null}
|
|
146
|
+
{offLabel ? (
|
|
147
|
+
<span
|
|
148
|
+
aria-hidden="true"
|
|
149
|
+
className={cn(
|
|
150
|
+
// Sit at the end side (empty when off), vertically centered.
|
|
151
|
+
"text-muted-foreground pointer-events-none absolute inset-y-0 end-1 inline-flex items-center leading-none font-semibold uppercase select-none",
|
|
152
|
+
sideLabelSize[resolvedSize],
|
|
153
|
+
"opacity-100 in-data-[state=checked]:opacity-0",
|
|
154
|
+
)}
|
|
155
|
+
>
|
|
156
|
+
{offLabel}
|
|
157
|
+
</span>
|
|
158
|
+
) : null}
|
|
159
|
+
|
|
160
|
+
<SwitchPrimitive.Thumb className={cn(switchThumbVariants({ size }))}>
|
|
161
|
+
{/* Thumb-internal slot: spinner during loading, otherwise on/off icon
|
|
162
|
+
driven by the parent's data-state (peer would be cleaner but the
|
|
163
|
+
Thumb's own [data-state] does the same job at the Radix level). */}
|
|
164
|
+
<span className="text-foreground/70 flex h-full w-full items-center justify-center [&_svg]:size-3 [&_svg]:shrink-0">
|
|
165
|
+
{loading ? (
|
|
166
|
+
<RiLoader2Line className={cn(thumbIconSize[resolvedSize], "animate-spin")} />
|
|
167
|
+
) : (
|
|
168
|
+
<>
|
|
169
|
+
{onIcon ? (
|
|
170
|
+
<span className="hidden in-data-[state=checked]:inline-flex">{onIcon}</span>
|
|
171
|
+
) : null}
|
|
172
|
+
{offIcon ? (
|
|
173
|
+
<span className="inline-flex in-data-[state=checked]:hidden">{offIcon}</span>
|
|
174
|
+
) : null}
|
|
175
|
+
</>
|
|
176
|
+
)}
|
|
177
|
+
</span>
|
|
178
|
+
</SwitchPrimitive.Thumb>
|
|
179
|
+
</SwitchPrimitive.Root>
|
|
180
|
+
);
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
Switch.displayName = "Switch";
|
|
184
|
+
|
|
185
|
+
export { Switch, switchRootVariants, switchThumbVariants };
|
|
186
|
+
export type { SwitchProps };
|