@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,167 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
import { Skeleton } from "./skeleton";
|
|
9
|
+
|
|
10
|
+
// Per docs/emara-ui-phase-3-components.md §3. Brought forward into v0.1 per
|
|
11
|
+
// docs/emara-ui-implementation-plan.md §5 so Combobox / DatePicker / Label's
|
|
12
|
+
// hint prop have the dependency they need.
|
|
13
|
+
|
|
14
|
+
const Popover = PopoverPrimitive.Root;
|
|
15
|
+
const PopoverTrigger = PopoverPrimitive.Trigger;
|
|
16
|
+
const PopoverAnchor = PopoverPrimitive.Anchor;
|
|
17
|
+
const PopoverClose = PopoverPrimitive.Close;
|
|
18
|
+
|
|
19
|
+
// ----------------------------------------------------------------------------
|
|
20
|
+
// PopoverContent
|
|
21
|
+
// ----------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
const popoverContentVariants = cva(
|
|
24
|
+
[
|
|
25
|
+
"z-popover outline-none",
|
|
26
|
+
"rounded-md border border-border bg-popover text-popover-foreground shadow-md",
|
|
27
|
+
// Enter/exit animations driven by Radix data-state. Slide direction
|
|
28
|
+
// depends on the resolved `side` so the panel "drops" from its trigger.
|
|
29
|
+
"data-[state=open]:animate-[scale-in_var(--duration-fast)_var(--ease-out)]",
|
|
30
|
+
"data-[state=closed]:animate-[scale-out_var(--duration-fast)_var(--ease-in)]",
|
|
31
|
+
"data-[side=top]:data-[state=open]:animate-[slide-in-from-bottom_var(--duration-fast)_var(--ease-out)]",
|
|
32
|
+
"data-[side=bottom]:data-[state=open]:animate-[slide-in-from-top_var(--duration-fast)_var(--ease-out)]",
|
|
33
|
+
"data-[side=left]:data-[state=open]:animate-[slide-in-from-end_var(--duration-fast)_var(--ease-out)]",
|
|
34
|
+
"data-[side=right]:data-[state=open]:animate-[slide-in-from-start_var(--duration-fast)_var(--ease-out)]",
|
|
35
|
+
].join(" "),
|
|
36
|
+
{
|
|
37
|
+
variants: {
|
|
38
|
+
padding: {
|
|
39
|
+
none: "p-0",
|
|
40
|
+
sm: "p-2",
|
|
41
|
+
md: "p-3",
|
|
42
|
+
lg: "p-4",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
defaultVariants: { padding: "md" },
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
type PopoverWidth = "fit" | "trigger" | "sm" | "md" | "lg" | "xl" | number;
|
|
50
|
+
|
|
51
|
+
const WIDTH_TO_PX: Record<Exclude<PopoverWidth, "fit" | "trigger" | number>, number> = {
|
|
52
|
+
sm: 192,
|
|
53
|
+
md: 256,
|
|
54
|
+
lg: 320,
|
|
55
|
+
xl: 448,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function resolveWidthStyle(width: PopoverWidth | undefined): React.CSSProperties | undefined {
|
|
59
|
+
if (width === undefined) return undefined;
|
|
60
|
+
if (width === "fit") return { width: "max-content" };
|
|
61
|
+
if (width === "trigger") {
|
|
62
|
+
return { width: "var(--radix-popover-trigger-width)" };
|
|
63
|
+
}
|
|
64
|
+
if (typeof width === "number") return { width: width };
|
|
65
|
+
return { width: WIDTH_TO_PX[width] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveMaxHeight(maxHeight: number | string | undefined): React.CSSProperties | undefined {
|
|
69
|
+
if (maxHeight === undefined) return undefined;
|
|
70
|
+
return { maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type PopoverContentProps = React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> &
|
|
74
|
+
VariantProps<typeof popoverContentVariants> & {
|
|
75
|
+
width?: PopoverWidth;
|
|
76
|
+
maxHeight?: number | string;
|
|
77
|
+
arrow?: boolean;
|
|
78
|
+
loading?: boolean;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const PopoverContent = forwardRef<
|
|
82
|
+
React.ElementRef<typeof PopoverPrimitive.Content>,
|
|
83
|
+
PopoverContentProps
|
|
84
|
+
>(function PopoverContent(
|
|
85
|
+
{
|
|
86
|
+
className,
|
|
87
|
+
align = "center",
|
|
88
|
+
sideOffset = 8,
|
|
89
|
+
width,
|
|
90
|
+
maxHeight,
|
|
91
|
+
arrow = false,
|
|
92
|
+
loading = false,
|
|
93
|
+
padding,
|
|
94
|
+
style,
|
|
95
|
+
children,
|
|
96
|
+
...props
|
|
97
|
+
},
|
|
98
|
+
ref,
|
|
99
|
+
) {
|
|
100
|
+
// If maxHeight is set the content scrolls internally.
|
|
101
|
+
const overflowStyle = maxHeight !== undefined ? { overflowY: "auto" as const } : undefined;
|
|
102
|
+
const merged: React.CSSProperties = {
|
|
103
|
+
...resolveWidthStyle(width),
|
|
104
|
+
...resolveMaxHeight(maxHeight),
|
|
105
|
+
...overflowStyle,
|
|
106
|
+
...style,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Loading skeleton mimics a small panel of text rows.
|
|
110
|
+
const loadingNode = (
|
|
111
|
+
<div className="space-y-2">
|
|
112
|
+
<Skeleton className="h-3 w-3/4" />
|
|
113
|
+
<Skeleton className="h-3 w-full" />
|
|
114
|
+
<Skeleton className="h-3 w-5/6" />
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<PopoverPrimitive.Portal>
|
|
120
|
+
<PopoverPrimitive.Content
|
|
121
|
+
ref={ref}
|
|
122
|
+
align={align}
|
|
123
|
+
sideOffset={sideOffset}
|
|
124
|
+
style={merged}
|
|
125
|
+
aria-busy={loading || undefined}
|
|
126
|
+
aria-live={loading ? "polite" : undefined}
|
|
127
|
+
className={cn(popoverContentVariants({ padding }), className)}
|
|
128
|
+
{...props}
|
|
129
|
+
>
|
|
130
|
+
{loading ? loadingNode : children}
|
|
131
|
+
{arrow ? <PopoverArrow /> : null}
|
|
132
|
+
</PopoverPrimitive.Content>
|
|
133
|
+
</PopoverPrimitive.Portal>
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
PopoverContent.displayName = "PopoverContent";
|
|
137
|
+
|
|
138
|
+
// ----------------------------------------------------------------------------
|
|
139
|
+
// PopoverArrow — standalone export so consumers can place it manually.
|
|
140
|
+
// ----------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
const PopoverArrow = forwardRef<
|
|
143
|
+
React.ElementRef<typeof PopoverPrimitive.Arrow>,
|
|
144
|
+
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Arrow>
|
|
145
|
+
>(function PopoverArrow({ className, width = 12, height = 6, ...props }, ref) {
|
|
146
|
+
return (
|
|
147
|
+
<PopoverPrimitive.Arrow
|
|
148
|
+
ref={ref}
|
|
149
|
+
width={width}
|
|
150
|
+
height={height}
|
|
151
|
+
className={cn("fill-popover stroke-border", className)}
|
|
152
|
+
{...props}
|
|
153
|
+
/>
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
PopoverArrow.displayName = "PopoverArrow";
|
|
157
|
+
|
|
158
|
+
export {
|
|
159
|
+
Popover,
|
|
160
|
+
PopoverTrigger,
|
|
161
|
+
PopoverContent,
|
|
162
|
+
PopoverArrow,
|
|
163
|
+
PopoverAnchor,
|
|
164
|
+
PopoverClose,
|
|
165
|
+
popoverContentVariants,
|
|
166
|
+
};
|
|
167
|
+
export type { PopoverContentProps };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { Radio, RadioGroup } from "./radio-group";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof RadioGroup> = {
|
|
6
|
+
title: "Forms/RadioGroup",
|
|
7
|
+
component: RadioGroup,
|
|
8
|
+
parameters: { layout: "centered" },
|
|
9
|
+
argTypes: {
|
|
10
|
+
size: { control: "select", options: ["sm", "md", "lg"] },
|
|
11
|
+
orientation: { control: "select", options: ["vertical", "horizontal"] },
|
|
12
|
+
variant: { control: "select", options: ["default", "card"] },
|
|
13
|
+
invalid: { control: "boolean" },
|
|
14
|
+
disabled: { control: "boolean" },
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default meta;
|
|
19
|
+
type Story = StoryObj<typeof RadioGroup>;
|
|
20
|
+
|
|
21
|
+
export const Default: Story = {
|
|
22
|
+
render: (args) => (
|
|
23
|
+
<RadioGroup {...args} defaultValue="email" className="w-72">
|
|
24
|
+
<Radio value="email" label="Email" />
|
|
25
|
+
<Radio value="sms" label="SMS" />
|
|
26
|
+
<Radio value="push" label="Push notification" />
|
|
27
|
+
</RadioGroup>
|
|
28
|
+
),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const Sizes: Story = {
|
|
32
|
+
render: () => (
|
|
33
|
+
<div className="grid grid-cols-3 gap-6">
|
|
34
|
+
{(["sm", "md", "lg"] as const).map((s) => (
|
|
35
|
+
<RadioGroup key={s} size={s} defaultValue="a">
|
|
36
|
+
<Radio value="a" label={`${s} option A`} />
|
|
37
|
+
<Radio value="b" label={`${s} option B`} />
|
|
38
|
+
</RadioGroup>
|
|
39
|
+
))}
|
|
40
|
+
</div>
|
|
41
|
+
),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const Horizontal: Story = {
|
|
45
|
+
render: () => (
|
|
46
|
+
<RadioGroup orientation="horizontal" defaultValue="ltr">
|
|
47
|
+
<Radio value="ltr" label="LTR" />
|
|
48
|
+
<Radio value="rtl" label="RTL" />
|
|
49
|
+
<Radio value="auto" label="Auto" />
|
|
50
|
+
</RadioGroup>
|
|
51
|
+
),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const WithDescriptions: Story = {
|
|
55
|
+
render: () => (
|
|
56
|
+
<RadioGroup defaultValue="standard" className="w-80">
|
|
57
|
+
<Radio
|
|
58
|
+
value="standard"
|
|
59
|
+
label="Standard shipping"
|
|
60
|
+
description="Arrives in 5-7 business days. Free."
|
|
61
|
+
/>
|
|
62
|
+
<Radio value="express" label="Express" description="Arrives in 2-3 business days. $9.99." />
|
|
63
|
+
<Radio value="overnight" label="Overnight" description="Next business day. $19.99." />
|
|
64
|
+
</RadioGroup>
|
|
65
|
+
),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const CardVariant: Story = {
|
|
69
|
+
render: () => (
|
|
70
|
+
<RadioGroup variant="card" defaultValue="pro" className="w-80">
|
|
71
|
+
<Radio value="starter" label="Starter" description="Up to 3 projects. Free forever." />
|
|
72
|
+
<Radio value="pro" label="Pro" description="Unlimited projects. $12/mo." />
|
|
73
|
+
<Radio value="team" label="Team" description="Seats and roles. $48/mo." />
|
|
74
|
+
</RadioGroup>
|
|
75
|
+
),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const Invalid: Story = {
|
|
79
|
+
args: { invalid: true },
|
|
80
|
+
render: (args) => (
|
|
81
|
+
<RadioGroup {...args} className="w-72">
|
|
82
|
+
<Radio value="a" label="Option A" />
|
|
83
|
+
<Radio value="b" label="Option B" />
|
|
84
|
+
</RadioGroup>
|
|
85
|
+
),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const Disabled: Story = {
|
|
89
|
+
render: () => (
|
|
90
|
+
<RadioGroup defaultValue="b" className="w-72">
|
|
91
|
+
<Radio value="a" label="Option A" disabled />
|
|
92
|
+
<Radio value="b" label="Option B" />
|
|
93
|
+
<Radio value="c" label="Option C" disabled />
|
|
94
|
+
</RadioGroup>
|
|
95
|
+
),
|
|
96
|
+
};
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, forwardRef, useContext, useId } from "react";
|
|
4
|
+
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
|
5
|
+
import { cva } from "class-variance-authority";
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
|
|
9
|
+
// Per docs/emara-ui-phase-2-components.md §2.
|
|
10
|
+
|
|
11
|
+
// ----------------------------------------------------------------------------
|
|
12
|
+
// Group-level context — propagates size/variant/invalid to children.
|
|
13
|
+
// ----------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
type RadioSize = "sm" | "md" | "lg";
|
|
16
|
+
type RadioVariant = "default" | "card";
|
|
17
|
+
|
|
18
|
+
interface RadioGroupContextValue {
|
|
19
|
+
size: RadioSize;
|
|
20
|
+
variant: RadioVariant;
|
|
21
|
+
invalid: boolean;
|
|
22
|
+
}
|
|
23
|
+
const RadioGroupContext = createContext<RadioGroupContextValue>({
|
|
24
|
+
size: "md",
|
|
25
|
+
variant: "default",
|
|
26
|
+
invalid: false,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function useRadioGroupContext(): RadioGroupContextValue {
|
|
30
|
+
return useContext(RadioGroupContext);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ----------------------------------------------------------------------------
|
|
34
|
+
// RadioGroup
|
|
35
|
+
// ----------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
const radioGroupRootVariants = cva("grid gap-2", {
|
|
38
|
+
variants: {
|
|
39
|
+
orientation: {
|
|
40
|
+
vertical: "grid-flow-row",
|
|
41
|
+
horizontal: "grid-flow-col auto-cols-max gap-3",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
defaultVariants: { orientation: "vertical" },
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
type RadioGroupProps = Omit<
|
|
48
|
+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>,
|
|
49
|
+
"orientation"
|
|
50
|
+
> & {
|
|
51
|
+
orientation?: "vertical" | "horizontal";
|
|
52
|
+
variant?: RadioVariant;
|
|
53
|
+
invalid?: boolean;
|
|
54
|
+
size?: RadioSize;
|
|
55
|
+
loop?: boolean;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const RadioGroup = forwardRef<React.ElementRef<typeof RadioGroupPrimitive.Root>, RadioGroupProps>(
|
|
59
|
+
function RadioGroup(
|
|
60
|
+
{
|
|
61
|
+
className,
|
|
62
|
+
orientation = "vertical",
|
|
63
|
+
variant = "default",
|
|
64
|
+
invalid = false,
|
|
65
|
+
size = "md",
|
|
66
|
+
loop,
|
|
67
|
+
...props
|
|
68
|
+
},
|
|
69
|
+
ref,
|
|
70
|
+
) {
|
|
71
|
+
return (
|
|
72
|
+
<RadioGroupContext.Provider value={{ size, variant, invalid }}>
|
|
73
|
+
<RadioGroupPrimitive.Root
|
|
74
|
+
ref={ref}
|
|
75
|
+
orientation={orientation}
|
|
76
|
+
loop={loop}
|
|
77
|
+
aria-invalid={invalid || undefined}
|
|
78
|
+
className={cn(radioGroupRootVariants({ orientation }), className)}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
</RadioGroupContext.Provider>
|
|
82
|
+
);
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
RadioGroup.displayName = "RadioGroup";
|
|
86
|
+
|
|
87
|
+
// ----------------------------------------------------------------------------
|
|
88
|
+
// Radio
|
|
89
|
+
// ----------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
const radioCircleVariants = cva(
|
|
92
|
+
[
|
|
93
|
+
"aspect-square shrink-0 rounded-full border border-input bg-background text-primary",
|
|
94
|
+
"transition-colors",
|
|
95
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
96
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
97
|
+
"data-[state=checked]:border-primary",
|
|
98
|
+
].join(" "),
|
|
99
|
+
{
|
|
100
|
+
variants: {
|
|
101
|
+
size: {
|
|
102
|
+
sm: "size-4",
|
|
103
|
+
md: "size-5",
|
|
104
|
+
lg: "size-6",
|
|
105
|
+
},
|
|
106
|
+
invalid: {
|
|
107
|
+
true: "border-destructive focus-visible:ring-destructive data-[state=checked]:border-destructive",
|
|
108
|
+
false: "",
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
defaultVariants: { size: "md", invalid: false },
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const indicatorDot: Record<RadioSize, string> = {
|
|
116
|
+
sm: "size-1.5",
|
|
117
|
+
md: "size-2",
|
|
118
|
+
lg: "size-2.5",
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const labelSize: Record<RadioSize, string> = {
|
|
122
|
+
sm: "text-xs",
|
|
123
|
+
md: "text-sm",
|
|
124
|
+
lg: "text-base",
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const descriptionSize: Record<RadioSize, string> = {
|
|
128
|
+
sm: "text-2xs",
|
|
129
|
+
md: "text-xs",
|
|
130
|
+
lg: "text-sm",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const radioCardVariants = cva(
|
|
134
|
+
[
|
|
135
|
+
"relative flex w-full items-start gap-3 rounded-md border bg-background p-3",
|
|
136
|
+
"cursor-pointer select-none",
|
|
137
|
+
"transition-colors transition-shadow",
|
|
138
|
+
"hover:bg-accent/30",
|
|
139
|
+
"focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background",
|
|
140
|
+
// Selected ring driven by the inner radio's data-state via the `in-` modifier.
|
|
141
|
+
"in-data-[state=checked]:border-primary in-data-[state=checked]:ring-2 in-data-[state=checked]:ring-primary",
|
|
142
|
+
"has-[button:disabled]:cursor-not-allowed has-[button:disabled]:opacity-50",
|
|
143
|
+
].join(" "),
|
|
144
|
+
{
|
|
145
|
+
variants: {
|
|
146
|
+
invalid: {
|
|
147
|
+
true: "border-destructive in-data-[state=checked]:border-destructive in-data-[state=checked]:ring-destructive focus-within:ring-destructive",
|
|
148
|
+
false: "border-input",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
defaultVariants: { invalid: false },
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
type RadioProps = Omit<
|
|
156
|
+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>,
|
|
157
|
+
"children"
|
|
158
|
+
> & {
|
|
159
|
+
label?: React.ReactNode;
|
|
160
|
+
description?: React.ReactNode;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const Radio = forwardRef<React.ElementRef<typeof RadioGroupPrimitive.Item>, RadioProps>(
|
|
164
|
+
function Radio({ className, label, description, id, ...props }, ref) {
|
|
165
|
+
const ctx = useRadioGroupContext();
|
|
166
|
+
const reactId = useId();
|
|
167
|
+
const radioId = id ?? reactId;
|
|
168
|
+
const descriptionId = description ? `${radioId}-description` : undefined;
|
|
169
|
+
|
|
170
|
+
const circle = (
|
|
171
|
+
<RadioGroupPrimitive.Item
|
|
172
|
+
ref={ref}
|
|
173
|
+
id={radioId}
|
|
174
|
+
aria-describedby={descriptionId}
|
|
175
|
+
className={cn(radioCircleVariants({ size: ctx.size, invalid: ctx.invalid }), className)}
|
|
176
|
+
{...props}
|
|
177
|
+
>
|
|
178
|
+
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
|
179
|
+
<span
|
|
180
|
+
className={cn(
|
|
181
|
+
"bg-primary block rounded-full",
|
|
182
|
+
indicatorDot[ctx.size],
|
|
183
|
+
ctx.invalid && "bg-destructive",
|
|
184
|
+
)}
|
|
185
|
+
/>
|
|
186
|
+
</RadioGroupPrimitive.Indicator>
|
|
187
|
+
</RadioGroupPrimitive.Item>
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
if (ctx.variant === "card") {
|
|
191
|
+
return (
|
|
192
|
+
<label htmlFor={radioId} className={cn(radioCardVariants({ invalid: ctx.invalid }))}>
|
|
193
|
+
{circle}
|
|
194
|
+
<span className="flex flex-1 flex-col gap-1 leading-none">
|
|
195
|
+
{label ? (
|
|
196
|
+
<span className={cn("font-medium select-none", labelSize[ctx.size])}>{label}</span>
|
|
197
|
+
) : null}
|
|
198
|
+
{description ? (
|
|
199
|
+
<span
|
|
200
|
+
id={descriptionId}
|
|
201
|
+
className={cn("text-muted-foreground", descriptionSize[ctx.size])}
|
|
202
|
+
>
|
|
203
|
+
{description}
|
|
204
|
+
</span>
|
|
205
|
+
) : null}
|
|
206
|
+
</span>
|
|
207
|
+
</label>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Default variant — circle + label + description in a row.
|
|
212
|
+
if (!label && !description) return circle;
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<div className="inline-flex items-start gap-2">
|
|
216
|
+
{circle}
|
|
217
|
+
<div className="space-y-0.5 leading-none">
|
|
218
|
+
{label ? (
|
|
219
|
+
<label
|
|
220
|
+
htmlFor={radioId}
|
|
221
|
+
className={cn(
|
|
222
|
+
"cursor-pointer font-medium select-none",
|
|
223
|
+
labelSize[ctx.size],
|
|
224
|
+
props.disabled && "cursor-not-allowed opacity-50",
|
|
225
|
+
)}
|
|
226
|
+
>
|
|
227
|
+
{label}
|
|
228
|
+
</label>
|
|
229
|
+
) : null}
|
|
230
|
+
{description ? (
|
|
231
|
+
<p
|
|
232
|
+
id={descriptionId}
|
|
233
|
+
className={cn(
|
|
234
|
+
"text-muted-foreground",
|
|
235
|
+
descriptionSize[ctx.size],
|
|
236
|
+
props.disabled && "opacity-50",
|
|
237
|
+
)}
|
|
238
|
+
>
|
|
239
|
+
{description}
|
|
240
|
+
</p>
|
|
241
|
+
) : null}
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
},
|
|
246
|
+
);
|
|
247
|
+
Radio.displayName = "Radio";
|
|
248
|
+
|
|
249
|
+
export { RadioGroup, Radio, radioGroupRootVariants, radioCircleVariants };
|
|
250
|
+
export type { RadioGroupProps, RadioProps };
|