@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,193 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { Button } from "./button";
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogBody,
|
|
7
|
+
DialogClose,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogDescription,
|
|
10
|
+
DialogFooter,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
DialogTrigger,
|
|
14
|
+
} from "./dialog";
|
|
15
|
+
|
|
16
|
+
const meta: Meta<typeof Dialog> = {
|
|
17
|
+
title: "Overlays/Dialog",
|
|
18
|
+
component: Dialog,
|
|
19
|
+
parameters: { layout: "centered" },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
type Story = StoryObj<typeof Dialog>;
|
|
24
|
+
|
|
25
|
+
export const Default: Story = {
|
|
26
|
+
render: () => (
|
|
27
|
+
<Dialog>
|
|
28
|
+
<DialogTrigger asChild>
|
|
29
|
+
<Button>Open dialog</Button>
|
|
30
|
+
</DialogTrigger>
|
|
31
|
+
<DialogContent>
|
|
32
|
+
<DialogHeader>
|
|
33
|
+
<DialogTitle>Delete this item?</DialogTitle>
|
|
34
|
+
<DialogDescription>
|
|
35
|
+
This action cannot be undone. The item will be permanently removed.
|
|
36
|
+
</DialogDescription>
|
|
37
|
+
</DialogHeader>
|
|
38
|
+
<DialogBody>
|
|
39
|
+
<p className="text-sm">All related references will be cleared as well.</p>
|
|
40
|
+
</DialogBody>
|
|
41
|
+
<DialogFooter>
|
|
42
|
+
<DialogClose asChild>
|
|
43
|
+
<Button variant="ghost">Cancel</Button>
|
|
44
|
+
</DialogClose>
|
|
45
|
+
<Button variant="destructive">Delete</Button>
|
|
46
|
+
</DialogFooter>
|
|
47
|
+
</DialogContent>
|
|
48
|
+
</Dialog>
|
|
49
|
+
),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const Sizes: Story = {
|
|
53
|
+
render: () => (
|
|
54
|
+
<div className="flex flex-wrap gap-2">
|
|
55
|
+
{(["xs", "sm", "md", "lg", "xl", "full"] as const).map((s) => (
|
|
56
|
+
<Dialog key={s}>
|
|
57
|
+
<DialogTrigger asChild>
|
|
58
|
+
<Button variant="outline" size="sm">
|
|
59
|
+
{s}
|
|
60
|
+
</Button>
|
|
61
|
+
</DialogTrigger>
|
|
62
|
+
<DialogContent size={s}>
|
|
63
|
+
<DialogHeader>
|
|
64
|
+
<DialogTitle>size="{s}"</DialogTitle>
|
|
65
|
+
<DialogDescription>Max width follows the size scale.</DialogDescription>
|
|
66
|
+
</DialogHeader>
|
|
67
|
+
<DialogBody>
|
|
68
|
+
<p className="text-sm">
|
|
69
|
+
Body content. xs = 320px, sm = 420px, md = 560px (default), lg = 720px, xl = 960px,
|
|
70
|
+
full = viewport-fill with margin.
|
|
71
|
+
</p>
|
|
72
|
+
</DialogBody>
|
|
73
|
+
<DialogFooter>
|
|
74
|
+
<DialogClose asChild>
|
|
75
|
+
<Button>OK</Button>
|
|
76
|
+
</DialogClose>
|
|
77
|
+
</DialogFooter>
|
|
78
|
+
</DialogContent>
|
|
79
|
+
</Dialog>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const Scrollable: Story = {
|
|
86
|
+
render: () => (
|
|
87
|
+
<Dialog>
|
|
88
|
+
<DialogTrigger asChild>
|
|
89
|
+
<Button>Long content</Button>
|
|
90
|
+
</DialogTrigger>
|
|
91
|
+
<DialogContent scrollable>
|
|
92
|
+
<DialogHeader>
|
|
93
|
+
<DialogTitle>Terms of service</DialogTitle>
|
|
94
|
+
<DialogDescription>Scroll to read everything.</DialogDescription>
|
|
95
|
+
</DialogHeader>
|
|
96
|
+
<DialogBody>
|
|
97
|
+
{Array.from({ length: 30 }, (_, i) => (
|
|
98
|
+
<p key={i} className="mb-3 text-sm">
|
|
99
|
+
Paragraph {i + 1}: lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
|
100
|
+
</p>
|
|
101
|
+
))}
|
|
102
|
+
</DialogBody>
|
|
103
|
+
<DialogFooter>
|
|
104
|
+
<DialogClose asChild>
|
|
105
|
+
<Button variant="ghost">Decline</Button>
|
|
106
|
+
</DialogClose>
|
|
107
|
+
<Button>I agree</Button>
|
|
108
|
+
</DialogFooter>
|
|
109
|
+
</DialogContent>
|
|
110
|
+
</Dialog>
|
|
111
|
+
),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const ConfirmBeforeClose: Story = {
|
|
115
|
+
render: () => (
|
|
116
|
+
<Dialog>
|
|
117
|
+
<DialogTrigger asChild>
|
|
118
|
+
<Button>Open (confirm on close)</Button>
|
|
119
|
+
</DialogTrigger>
|
|
120
|
+
<DialogContent confirmBeforeClose>
|
|
121
|
+
<DialogHeader>
|
|
122
|
+
<DialogTitle>Edit profile</DialogTitle>
|
|
123
|
+
<DialogDescription>
|
|
124
|
+
Closing this dialog will prompt to confirm — useful when fields are dirty.
|
|
125
|
+
</DialogDescription>
|
|
126
|
+
</DialogHeader>
|
|
127
|
+
<DialogBody>
|
|
128
|
+
<p className="text-sm">Try clicking outside or pressing Escape.</p>
|
|
129
|
+
</DialogBody>
|
|
130
|
+
</DialogContent>
|
|
131
|
+
</Dialog>
|
|
132
|
+
),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const Loading: Story = {
|
|
136
|
+
render: () => (
|
|
137
|
+
<Dialog>
|
|
138
|
+
<DialogTrigger asChild>
|
|
139
|
+
<Button>Open (loading)</Button>
|
|
140
|
+
</DialogTrigger>
|
|
141
|
+
<DialogContent loading />
|
|
142
|
+
</Dialog>
|
|
143
|
+
),
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const NoCloseButton: Story = {
|
|
147
|
+
render: () => (
|
|
148
|
+
<Dialog>
|
|
149
|
+
<DialogTrigger asChild>
|
|
150
|
+
<Button>Open (no X)</Button>
|
|
151
|
+
</DialogTrigger>
|
|
152
|
+
<DialogContent hideCloseButton>
|
|
153
|
+
<DialogHeader>
|
|
154
|
+
<DialogTitle>Are you sure?</DialogTitle>
|
|
155
|
+
<DialogDescription>
|
|
156
|
+
Use the explicit footer buttons to dismiss this dialog.
|
|
157
|
+
</DialogDescription>
|
|
158
|
+
</DialogHeader>
|
|
159
|
+
<DialogFooter>
|
|
160
|
+
<DialogClose asChild>
|
|
161
|
+
<Button variant="ghost">Cancel</Button>
|
|
162
|
+
</DialogClose>
|
|
163
|
+
<DialogClose asChild>
|
|
164
|
+
<Button>Confirm</Button>
|
|
165
|
+
</DialogClose>
|
|
166
|
+
</DialogFooter>
|
|
167
|
+
</DialogContent>
|
|
168
|
+
</Dialog>
|
|
169
|
+
),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const DisableOverlayClick: Story = {
|
|
173
|
+
render: () => (
|
|
174
|
+
<Dialog>
|
|
175
|
+
<DialogTrigger asChild>
|
|
176
|
+
<Button>Click outside won't close</Button>
|
|
177
|
+
</DialogTrigger>
|
|
178
|
+
<DialogContent closeOnOverlayClick={false}>
|
|
179
|
+
<DialogHeader>
|
|
180
|
+
<DialogTitle>Sticky dialog</DialogTitle>
|
|
181
|
+
<DialogDescription>
|
|
182
|
+
Use the X or Cancel button to dismiss. Outside-clicks are ignored.
|
|
183
|
+
</DialogDescription>
|
|
184
|
+
</DialogHeader>
|
|
185
|
+
<DialogFooter>
|
|
186
|
+
<DialogClose asChild>
|
|
187
|
+
<Button>OK</Button>
|
|
188
|
+
</DialogClose>
|
|
189
|
+
</DialogFooter>
|
|
190
|
+
</DialogContent>
|
|
191
|
+
</Dialog>
|
|
192
|
+
),
|
|
193
|
+
};
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
5
|
+
import { RiCloseLine } from "@remixicon/react";
|
|
6
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
7
|
+
|
|
8
|
+
import { cn } from "@/lib/utils";
|
|
9
|
+
import { Skeleton } from "./skeleton";
|
|
10
|
+
|
|
11
|
+
// Per docs/emara-ui-phase-3-components.md §1.
|
|
12
|
+
|
|
13
|
+
const Dialog = DialogPrimitive.Root;
|
|
14
|
+
const DialogTrigger = DialogPrimitive.Trigger;
|
|
15
|
+
const DialogPortal = DialogPrimitive.Portal;
|
|
16
|
+
const DialogClose = DialogPrimitive.Close;
|
|
17
|
+
|
|
18
|
+
// ----------------------------------------------------------------------------
|
|
19
|
+
// DialogOverlay
|
|
20
|
+
// ----------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
const DialogOverlay = forwardRef<
|
|
23
|
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
24
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
25
|
+
>(function DialogOverlay({ className, ...props }, ref) {
|
|
26
|
+
return (
|
|
27
|
+
<DialogPrimitive.Overlay
|
|
28
|
+
ref={ref}
|
|
29
|
+
className={cn(
|
|
30
|
+
"z-modal bg-foreground/50 fixed inset-0",
|
|
31
|
+
"data-[state=open]:animate-[fade-in_var(--duration-normal)_var(--ease-out)]",
|
|
32
|
+
"data-[state=closed]:animate-[fade-out_var(--duration-fast)_var(--ease-in)]",
|
|
33
|
+
className,
|
|
34
|
+
)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
DialogOverlay.displayName = "DialogOverlay";
|
|
40
|
+
|
|
41
|
+
// ----------------------------------------------------------------------------
|
|
42
|
+
// DialogContent
|
|
43
|
+
// ----------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
const dialogContentVariants = cva(
|
|
46
|
+
[
|
|
47
|
+
"fixed start-1/2 top-1/2 z-modal grid w-full -translate-x-1/2 -translate-y-1/2 gap-0",
|
|
48
|
+
"rounded-lg border border-border bg-card text-card-foreground shadow-xl",
|
|
49
|
+
"data-[state=open]:animate-[scale-in_var(--duration-normal)_var(--ease-out)]",
|
|
50
|
+
"data-[state=closed]:animate-[scale-out_var(--duration-fast)_var(--ease-in)]",
|
|
51
|
+
// The start/-translate-x trick centers via logical start in LTR. RTL flips:
|
|
52
|
+
"rtl:start-auto rtl:end-1/2 rtl:translate-x-1/2",
|
|
53
|
+
].join(" "),
|
|
54
|
+
{
|
|
55
|
+
variants: {
|
|
56
|
+
size: {
|
|
57
|
+
xs: "max-w-dialog-xs",
|
|
58
|
+
sm: "max-w-dialog-sm",
|
|
59
|
+
md: "max-w-dialog-md",
|
|
60
|
+
lg: "max-w-dialog-lg",
|
|
61
|
+
xl: "max-w-dialog-xl",
|
|
62
|
+
full: "max-w-[calc(100vw-2rem)] max-h-[calc(100vh-2rem)]",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
defaultVariants: { size: "md" },
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
type DialogContentVariants = VariantProps<typeof dialogContentVariants>;
|
|
70
|
+
|
|
71
|
+
type DialogContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> &
|
|
72
|
+
DialogContentVariants & {
|
|
73
|
+
closeOnOverlayClick?: boolean;
|
|
74
|
+
closeOnEscape?: boolean;
|
|
75
|
+
confirmBeforeClose?: boolean | (() => boolean | Promise<boolean>);
|
|
76
|
+
loading?: boolean;
|
|
77
|
+
scrollable?: boolean;
|
|
78
|
+
hideCloseButton?: boolean;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const DialogContent = forwardRef<
|
|
82
|
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
83
|
+
DialogContentProps
|
|
84
|
+
>(function DialogContent(
|
|
85
|
+
{
|
|
86
|
+
className,
|
|
87
|
+
size,
|
|
88
|
+
closeOnOverlayClick = true,
|
|
89
|
+
closeOnEscape = true,
|
|
90
|
+
confirmBeforeClose,
|
|
91
|
+
loading = false,
|
|
92
|
+
scrollable = false,
|
|
93
|
+
hideCloseButton = false,
|
|
94
|
+
onPointerDownOutside,
|
|
95
|
+
onEscapeKeyDown,
|
|
96
|
+
onInteractOutside,
|
|
97
|
+
children,
|
|
98
|
+
...props
|
|
99
|
+
},
|
|
100
|
+
ref,
|
|
101
|
+
) {
|
|
102
|
+
const guardedClose = async (e: Event): Promise<boolean> => {
|
|
103
|
+
if (!confirmBeforeClose) return false;
|
|
104
|
+
if (typeof confirmBeforeClose === "function") {
|
|
105
|
+
const ok = await confirmBeforeClose();
|
|
106
|
+
if (!ok) {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
const ok = window.confirm("Discard unsaved changes?");
|
|
113
|
+
if (!ok) {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<DialogPortal>
|
|
122
|
+
<DialogOverlay />
|
|
123
|
+
<DialogPrimitive.Content
|
|
124
|
+
ref={ref}
|
|
125
|
+
onPointerDownOutside={(e) => {
|
|
126
|
+
if (!closeOnOverlayClick) e.preventDefault();
|
|
127
|
+
onPointerDownOutside?.(e);
|
|
128
|
+
}}
|
|
129
|
+
onEscapeKeyDown={(e) => {
|
|
130
|
+
if (!closeOnEscape) e.preventDefault();
|
|
131
|
+
onEscapeKeyDown?.(e);
|
|
132
|
+
}}
|
|
133
|
+
onInteractOutside={async (e) => {
|
|
134
|
+
if (confirmBeforeClose) await guardedClose(e as unknown as Event);
|
|
135
|
+
onInteractOutside?.(e);
|
|
136
|
+
}}
|
|
137
|
+
className={cn(
|
|
138
|
+
dialogContentVariants({ size }),
|
|
139
|
+
// Optional internal scroll. When scrollable, the body slot becomes
|
|
140
|
+
// the overflow region (see DialogBody).
|
|
141
|
+
scrollable ? "max-h-[85vh]" : "",
|
|
142
|
+
className,
|
|
143
|
+
)}
|
|
144
|
+
{...props}
|
|
145
|
+
>
|
|
146
|
+
{loading ? (
|
|
147
|
+
<div className="space-y-3 p-6" aria-busy="true" aria-live="polite">
|
|
148
|
+
<Skeleton className="h-6 w-2/5" />
|
|
149
|
+
<Skeleton className="h-4 w-3/5" />
|
|
150
|
+
<Skeleton className="h-4 w-full" />
|
|
151
|
+
<Skeleton className="h-4 w-10/12" />
|
|
152
|
+
<Skeleton className="h-4 w-4/5" />
|
|
153
|
+
</div>
|
|
154
|
+
) : (
|
|
155
|
+
children
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
{!hideCloseButton && !loading ? (
|
|
159
|
+
<DialogPrimitive.Close
|
|
160
|
+
aria-label="Close"
|
|
161
|
+
className={cn(
|
|
162
|
+
"absolute end-3 top-3 inline-flex size-7 items-center justify-center rounded-md",
|
|
163
|
+
"text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
|
164
|
+
"focus-visible:ring-ring focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
|
|
165
|
+
"[&_svg]:size-4 [&_svg]:shrink-0",
|
|
166
|
+
)}
|
|
167
|
+
>
|
|
168
|
+
<RiCloseLine />
|
|
169
|
+
</DialogPrimitive.Close>
|
|
170
|
+
) : null}
|
|
171
|
+
</DialogPrimitive.Content>
|
|
172
|
+
</DialogPortal>
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
DialogContent.displayName = "DialogContent";
|
|
176
|
+
|
|
177
|
+
// ----------------------------------------------------------------------------
|
|
178
|
+
// DialogHeader / DialogTitle / DialogDescription / DialogBody / DialogFooter
|
|
179
|
+
// ----------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
const DialogHeader = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
182
|
+
function DialogHeader({ className, ...props }, ref) {
|
|
183
|
+
return (
|
|
184
|
+
<div ref={ref} className={cn("space-y-1.5 p-6 pe-12 text-start", className)} {...props} />
|
|
185
|
+
);
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
DialogHeader.displayName = "DialogHeader";
|
|
189
|
+
|
|
190
|
+
const DialogTitle = forwardRef<
|
|
191
|
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
192
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
193
|
+
>(function DialogTitle({ className, ...props }, ref) {
|
|
194
|
+
return (
|
|
195
|
+
<DialogPrimitive.Title
|
|
196
|
+
ref={ref}
|
|
197
|
+
className={cn("text-lg leading-snug font-semibold tracking-tight", className)}
|
|
198
|
+
{...props}
|
|
199
|
+
/>
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
DialogTitle.displayName = "DialogTitle";
|
|
203
|
+
|
|
204
|
+
const DialogDescription = forwardRef<
|
|
205
|
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
206
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
207
|
+
>(function DialogDescription({ className, ...props }, ref) {
|
|
208
|
+
return (
|
|
209
|
+
<DialogPrimitive.Description
|
|
210
|
+
ref={ref}
|
|
211
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
212
|
+
{...props}
|
|
213
|
+
/>
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
DialogDescription.displayName = "DialogDescription";
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Emara addition — explicit body slot that owns padding + scroll behavior.
|
|
220
|
+
* Use inside DialogContent so the header and footer stay fixed while the body
|
|
221
|
+
* scrolls when content overflows.
|
|
222
|
+
*/
|
|
223
|
+
const DialogBody = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
224
|
+
function DialogBody({ className, ...props }, ref) {
|
|
225
|
+
return (
|
|
226
|
+
<div ref={ref} className={cn("flex-1 overflow-y-auto px-6 pb-6", className)} {...props} />
|
|
227
|
+
);
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
DialogBody.displayName = "DialogBody";
|
|
231
|
+
|
|
232
|
+
const DialogFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
233
|
+
function DialogFooter({ className, ...props }, ref) {
|
|
234
|
+
return (
|
|
235
|
+
<div
|
|
236
|
+
ref={ref}
|
|
237
|
+
className={cn(
|
|
238
|
+
"border-border flex flex-col-reverse items-stretch gap-2 border-t p-4 sm:flex-row sm:items-center sm:justify-end",
|
|
239
|
+
className,
|
|
240
|
+
)}
|
|
241
|
+
{...props}
|
|
242
|
+
/>
|
|
243
|
+
);
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
DialogFooter.displayName = "DialogFooter";
|
|
247
|
+
|
|
248
|
+
export {
|
|
249
|
+
Dialog,
|
|
250
|
+
DialogTrigger,
|
|
251
|
+
DialogPortal,
|
|
252
|
+
DialogOverlay,
|
|
253
|
+
DialogContent,
|
|
254
|
+
DialogHeader,
|
|
255
|
+
DialogTitle,
|
|
256
|
+
DialogDescription,
|
|
257
|
+
DialogBody,
|
|
258
|
+
DialogFooter,
|
|
259
|
+
DialogClose,
|
|
260
|
+
dialogContentVariants,
|
|
261
|
+
};
|
|
262
|
+
export type { DialogContentProps };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { Divider } from "./divider";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Divider> = {
|
|
6
|
+
title: "Foundations/Divider",
|
|
7
|
+
component: Divider,
|
|
8
|
+
parameters: { layout: "centered" },
|
|
9
|
+
argTypes: {
|
|
10
|
+
orientation: { control: "select", options: ["horizontal", "vertical"] },
|
|
11
|
+
variant: { control: "select", options: ["solid", "dashed", "dotted"] },
|
|
12
|
+
spacing: { control: "select", options: ["none", "sm", "md", "lg"] },
|
|
13
|
+
labelPosition: { control: "select", options: ["start", "center", "end"] },
|
|
14
|
+
decorative: { control: "boolean" },
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default meta;
|
|
19
|
+
type Story = StoryObj<typeof Divider>;
|
|
20
|
+
|
|
21
|
+
export const Default: Story = {
|
|
22
|
+
render: (args) => (
|
|
23
|
+
<div className="w-80">
|
|
24
|
+
<Divider {...args} />
|
|
25
|
+
</div>
|
|
26
|
+
),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const Variants: Story = {
|
|
30
|
+
render: () => (
|
|
31
|
+
<div className="w-80 space-y-4">
|
|
32
|
+
<Divider variant="solid" />
|
|
33
|
+
<Divider variant="dashed" />
|
|
34
|
+
<Divider variant="dotted" />
|
|
35
|
+
</div>
|
|
36
|
+
),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const SpacingScale: Story = {
|
|
40
|
+
render: () => (
|
|
41
|
+
<div className="w-80 rounded-md border border-border p-2">
|
|
42
|
+
<p className="text-sm">none</p>
|
|
43
|
+
<Divider spacing="none" />
|
|
44
|
+
<p className="text-sm">sm</p>
|
|
45
|
+
<Divider spacing="sm" />
|
|
46
|
+
<p className="text-sm">md</p>
|
|
47
|
+
<Divider spacing="md" />
|
|
48
|
+
<p className="text-sm">lg</p>
|
|
49
|
+
<Divider spacing="lg" />
|
|
50
|
+
<p className="text-sm">end</p>
|
|
51
|
+
</div>
|
|
52
|
+
),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const Vertical: Story = {
|
|
56
|
+
render: () => (
|
|
57
|
+
<div className="flex h-12 items-center gap-3 text-sm">
|
|
58
|
+
<span>Section A</span>
|
|
59
|
+
<Divider orientation="vertical" />
|
|
60
|
+
<span>Section B</span>
|
|
61
|
+
<Divider orientation="vertical" variant="dashed" />
|
|
62
|
+
<span>Section C</span>
|
|
63
|
+
</div>
|
|
64
|
+
),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const WithLabel: Story = {
|
|
68
|
+
render: () => (
|
|
69
|
+
<div className="w-80 space-y-6">
|
|
70
|
+
<Divider label="OR" />
|
|
71
|
+
<Divider label="Section" labelPosition="start" />
|
|
72
|
+
<Divider label="Section" labelPosition="end" />
|
|
73
|
+
</div>
|
|
74
|
+
),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const NonDecorative: Story = {
|
|
78
|
+
args: { decorative: false },
|
|
79
|
+
render: (args) => (
|
|
80
|
+
<div className="w-80">
|
|
81
|
+
<Divider {...args} />
|
|
82
|
+
</div>
|
|
83
|
+
),
|
|
84
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
|
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 §8.
|
|
10
|
+
|
|
11
|
+
const lineVariants = cva("shrink-0 bg-transparent", {
|
|
12
|
+
variants: {
|
|
13
|
+
orientation: {
|
|
14
|
+
horizontal: "w-full h-px border-t",
|
|
15
|
+
vertical: "h-full w-px border-s",
|
|
16
|
+
},
|
|
17
|
+
variant: {
|
|
18
|
+
solid: "border-solid border-border",
|
|
19
|
+
dashed: "border-dashed border-border",
|
|
20
|
+
dotted: "border-dotted border-border",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultVariants: {
|
|
24
|
+
orientation: "horizontal",
|
|
25
|
+
variant: "solid",
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const spacingVariants = cva("", {
|
|
30
|
+
variants: {
|
|
31
|
+
orientation: {
|
|
32
|
+
horizontal: "",
|
|
33
|
+
vertical: "",
|
|
34
|
+
},
|
|
35
|
+
spacing: {
|
|
36
|
+
none: "",
|
|
37
|
+
sm: "",
|
|
38
|
+
md: "",
|
|
39
|
+
lg: "",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
compoundVariants: [
|
|
43
|
+
{ orientation: "horizontal", spacing: "sm", class: "my-2" },
|
|
44
|
+
{ orientation: "horizontal", spacing: "md", class: "my-4" },
|
|
45
|
+
{ orientation: "horizontal", spacing: "lg", class: "my-6" },
|
|
46
|
+
{ orientation: "vertical", spacing: "sm", class: "mx-2" },
|
|
47
|
+
{ orientation: "vertical", spacing: "md", class: "mx-4" },
|
|
48
|
+
{ orientation: "vertical", spacing: "lg", class: "mx-6" },
|
|
49
|
+
],
|
|
50
|
+
defaultVariants: {
|
|
51
|
+
orientation: "horizontal",
|
|
52
|
+
spacing: "none",
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
type DividerVariants = VariantProps<typeof lineVariants> & VariantProps<typeof spacingVariants>;
|
|
57
|
+
|
|
58
|
+
type DividerProps = Omit<
|
|
59
|
+
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>,
|
|
60
|
+
"orientation" | "decorative"
|
|
61
|
+
> &
|
|
62
|
+
DividerVariants & {
|
|
63
|
+
decorative?: boolean;
|
|
64
|
+
label?: React.ReactNode;
|
|
65
|
+
labelPosition?: "start" | "center" | "end";
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const Divider = forwardRef<
|
|
69
|
+
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
|
70
|
+
DividerProps
|
|
71
|
+
>(function Divider(
|
|
72
|
+
{
|
|
73
|
+
className,
|
|
74
|
+
orientation = "horizontal",
|
|
75
|
+
variant = "solid",
|
|
76
|
+
spacing = "none",
|
|
77
|
+
decorative = true,
|
|
78
|
+
label,
|
|
79
|
+
labelPosition = "center",
|
|
80
|
+
...props
|
|
81
|
+
},
|
|
82
|
+
ref,
|
|
83
|
+
) {
|
|
84
|
+
const hasLabel = label !== undefined && label !== null && orientation === "horizontal";
|
|
85
|
+
|
|
86
|
+
if (!hasLabel) {
|
|
87
|
+
return (
|
|
88
|
+
<SeparatorPrimitive.Root
|
|
89
|
+
ref={ref}
|
|
90
|
+
orientation={orientation ?? "horizontal"}
|
|
91
|
+
decorative={decorative}
|
|
92
|
+
className={cn(
|
|
93
|
+
lineVariants({ orientation, variant }),
|
|
94
|
+
spacingVariants({ orientation, spacing }),
|
|
95
|
+
className,
|
|
96
|
+
)}
|
|
97
|
+
{...props}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Labeled divider — render label between two flex-grown lines. We provide
|
|
103
|
+
// role/aria ourselves because we're no longer a single <hr>/<div role=separator>.
|
|
104
|
+
return (
|
|
105
|
+
<div
|
|
106
|
+
ref={ref as unknown as React.Ref<HTMLDivElement>}
|
|
107
|
+
role={decorative ? "presentation" : "separator"}
|
|
108
|
+
aria-orientation={decorative ? undefined : "horizontal"}
|
|
109
|
+
className={cn(
|
|
110
|
+
"flex w-full items-center gap-3 text-sm text-muted-foreground",
|
|
111
|
+
spacingVariants({ orientation: "horizontal", spacing }),
|
|
112
|
+
className,
|
|
113
|
+
)}
|
|
114
|
+
{...props}
|
|
115
|
+
>
|
|
116
|
+
{labelPosition !== "start" ? (
|
|
117
|
+
<span
|
|
118
|
+
aria-hidden="true"
|
|
119
|
+
className={cn(lineVariants({ orientation: "horizontal", variant }), "flex-1")}
|
|
120
|
+
/>
|
|
121
|
+
) : null}
|
|
122
|
+
<span>{label}</span>
|
|
123
|
+
{labelPosition !== "end" ? (
|
|
124
|
+
<span
|
|
125
|
+
aria-hidden="true"
|
|
126
|
+
className={cn(lineVariants({ orientation: "horizontal", variant }), "flex-1")}
|
|
127
|
+
/>
|
|
128
|
+
) : null}
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
Divider.displayName = "Divider";
|
|
133
|
+
|
|
134
|
+
export { Divider, lineVariants };
|
|
135
|
+
export type { DividerProps };
|