@vvedantb/ui 0.5.4 → 0.6.1
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/README.md +184 -5
- package/app/README.md +100 -0
- package/package.json +11 -10
- package/shell/src/AppShell.tsx +130 -43
- package/shell/src/AppSidebar.tsx +27 -125
- package/shell/src/Sidebar.tsx +78 -0
- package/shell/src/SidebarAccountMenu.tsx +169 -77
- package/shell/src/SidebarChrome.tsx +295 -0
- package/shell/src/SidebarHeader.tsx +6 -7
- package/shell/src/SidebarPanel.tsx +95 -33
- package/shell/src/SidebarRail.tsx +74 -28
- package/shell/src/SignOutDialog.tsx +24 -51
- package/shell/src/app-shell-classes.ts +6 -0
- package/shell/src/index.ts +9 -1
- package/src/index.ts +9 -1
- package/src/ui/confirm-dialog-parts.tsx +355 -0
- package/src/ui/confirm-dialog.tsx +65 -106
- package/src/ui/overlay-a11y.ts +5 -3
- package/tokens/README.md +98 -0
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
import { IconAlertTriangle
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
Dialog,
|
|
5
|
-
DialogContent,
|
|
6
|
-
DialogDescription,
|
|
7
|
-
DialogFooter,
|
|
8
|
-
DialogHeader,
|
|
9
|
-
DialogTitle,
|
|
10
|
-
} from "../../src/ui/dialog";
|
|
1
|
+
import { IconAlertTriangle } from "@tabler/icons-react";
|
|
2
|
+
import { ConfirmDialog } from "../../src/ui/confirm-dialog";
|
|
11
3
|
import { cn } from "../../src/utils/cn";
|
|
12
4
|
import { signOutCancelClass, signOutDescriptionClass } from "./app-shell-classes";
|
|
13
5
|
|
|
@@ -22,13 +14,13 @@ export type SignOutDialogProps = {
|
|
|
22
14
|
description?: string;
|
|
23
15
|
/** Merged onto the dialog content after `sm:max-w-sm`. */
|
|
24
16
|
contentClassName?: string;
|
|
25
|
-
/**
|
|
17
|
+
/** Legacy (still supported): dialog title and confirm button label. Default `"Sign out"`. */
|
|
26
18
|
title?: string;
|
|
27
|
-
/**
|
|
19
|
+
/** Legacy (still supported): default `"Sign out of your account?"`. */
|
|
28
20
|
question?: string;
|
|
29
21
|
};
|
|
30
22
|
|
|
31
|
-
/** Sign-out confirmation. The app owns the state and the sign-out call. */
|
|
23
|
+
/** Sign-out confirmation. The app owns the state and the sign-out call. Composed from `ConfirmDialog` parts. */
|
|
32
24
|
export function SignOutDialog({
|
|
33
25
|
open,
|
|
34
26
|
onOpenChange,
|
|
@@ -40,21 +32,15 @@ export function SignOutDialog({
|
|
|
40
32
|
question = "Sign out of your account?",
|
|
41
33
|
}: SignOutDialogProps) {
|
|
42
34
|
return (
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}}
|
|
49
|
-
>
|
|
50
|
-
<DialogContent className={cn("sm:max-w-sm", contentClassName)}>
|
|
51
|
-
<DialogHeader>
|
|
52
|
-
<DialogTitle className="text-foreground">{title}</DialogTitle>
|
|
53
|
-
<DialogDescription className="sr-only">
|
|
35
|
+
<ConfirmDialog.Root open={open} onOpenChange={onOpenChange} alert={false} pending={pending}>
|
|
36
|
+
<ConfirmDialog.Content className={cn("sm:max-w-sm", contentClassName)}>
|
|
37
|
+
<ConfirmDialog.Header>
|
|
38
|
+
<ConfirmDialog.Title className="text-foreground">{title}</ConfirmDialog.Title>
|
|
39
|
+
<ConfirmDialog.Description className="sr-only">
|
|
54
40
|
Confirm signing out of your account
|
|
55
|
-
</
|
|
56
|
-
</
|
|
57
|
-
<
|
|
41
|
+
</ConfirmDialog.Description>
|
|
42
|
+
</ConfirmDialog.Header>
|
|
43
|
+
<ConfirmDialog.Body className="flex items-start gap-3 py-4">
|
|
58
44
|
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-danger/10">
|
|
59
45
|
<IconAlertTriangle size={20} className="text-danger" />
|
|
60
46
|
</div>
|
|
@@ -62,32 +48,19 @@ export function SignOutDialog({
|
|
|
62
48
|
<p className="text-foreground">{question}</p>
|
|
63
49
|
{description ? <p className={signOutDescriptionClass}>{description}</p> : null}
|
|
64
50
|
</div>
|
|
65
|
-
</
|
|
66
|
-
<
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
onClick={() => onOpenChange(false)}
|
|
70
|
-
disabled={pending}
|
|
71
|
-
className={signOutCancelClass}
|
|
72
|
-
>
|
|
73
|
-
Cancel
|
|
74
|
-
</Button>
|
|
75
|
-
<Button
|
|
51
|
+
</ConfirmDialog.Body>
|
|
52
|
+
<ConfirmDialog.Footer>
|
|
53
|
+
<ConfirmDialog.Cancel className={signOutCancelClass} />
|
|
54
|
+
<ConfirmDialog.Action
|
|
76
55
|
onClick={() => void onConfirm()}
|
|
77
|
-
|
|
56
|
+
pendingLabel="Signing out..."
|
|
57
|
+
spinnerSize={16}
|
|
78
58
|
className="bg-danger text-danger-foreground"
|
|
79
59
|
>
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
) : (
|
|
86
|
-
title
|
|
87
|
-
)}
|
|
88
|
-
</Button>
|
|
89
|
-
</DialogFooter>
|
|
90
|
-
</DialogContent>
|
|
91
|
-
</Dialog>
|
|
60
|
+
{title}
|
|
61
|
+
</ConfirmDialog.Action>
|
|
62
|
+
</ConfirmDialog.Footer>
|
|
63
|
+
</ConfirmDialog.Content>
|
|
64
|
+
</ConfirmDialog.Root>
|
|
92
65
|
);
|
|
93
66
|
}
|
|
@@ -33,6 +33,9 @@ export function sidebarNavRowClass(isIconOnly: boolean): string {
|
|
|
33
33
|
return cn(isIconOnly ? "justify-center px-0 py-1.5" : "gap-3 px-3 py-1.5");
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/** Sidebar header row: the panel title and its trailing actions. */
|
|
37
|
+
export const sidebarHeaderRowClass = "flex h-11 items-center gap-1 pl-3 pr-1";
|
|
38
|
+
|
|
36
39
|
export const mobileMenuButtonClass = "-ml-1 h-11 w-11 shrink-0 rounded-lg text-muted hover:bg-surface-tertiary/50 hover:text-foreground"; // check-tokens-allow: text-muted
|
|
37
40
|
|
|
38
41
|
export const panelCloseButtonClass = "rounded-lg text-muted hover:bg-surface-tertiary/50 hover:text-foreground"; // check-tokens-allow: text-muted
|
|
@@ -42,3 +45,6 @@ export const accountEmailClass = "block truncate text-xs font-normal text-muted"
|
|
|
42
45
|
export const signOutDescriptionClass = "mt-1 text-sm text-muted"; // check-tokens-allow: text-muted
|
|
43
46
|
|
|
44
47
|
export const signOutCancelClass = "text-muted"; // check-tokens-allow: text-muted
|
|
48
|
+
|
|
49
|
+
/** Label of the type-to-confirm field (`ConfirmDialog.Field`). */
|
|
50
|
+
export const confirmFieldLabelClass = "text-sm text-muted"; // check-tokens-allow: text-muted
|
package/shell/src/index.ts
CHANGED
|
@@ -11,9 +11,17 @@ export type {
|
|
|
11
11
|
SessionsHoverCardSurface,
|
|
12
12
|
} from "./SessionsHoverCard.js";
|
|
13
13
|
export { AppShell } from "./AppShell.js";
|
|
14
|
-
export type {
|
|
14
|
+
export type {
|
|
15
|
+
AppShellContentProps,
|
|
16
|
+
AppShellMainProps,
|
|
17
|
+
AppShellProps,
|
|
18
|
+
AppShellRootProps,
|
|
19
|
+
AppShellSkipLinkProps,
|
|
20
|
+
} from "./AppShell.js";
|
|
15
21
|
export { AppSidebar } from "./AppSidebar.js";
|
|
16
22
|
export type { AppSidebarProps } from "./AppSidebar.js";
|
|
23
|
+
// Composable sidebar parts (`Sidebar.Root`, `Sidebar.Rail`, ...) and their prop types.
|
|
24
|
+
export * from "./Sidebar.js";
|
|
17
25
|
export { SidebarRail, RailTile, RailDivider } from "./SidebarRail.js";
|
|
18
26
|
export type { SidebarRailProps, RailTileProps, RailDividerProps } from "./SidebarRail.js";
|
|
19
27
|
export { SidebarResizeHandle, useAppShellResize } from "./SidebarResize.js";
|
package/src/index.ts
CHANGED
|
@@ -123,9 +123,17 @@ export type {
|
|
|
123
123
|
// App shell: sidebar and main page layout. Widths come from the
|
|
124
124
|
// `--app-shell-*` CSS variables that `AppShell` sets.
|
|
125
125
|
export { AppShell } from "../shell/src/AppShell";
|
|
126
|
-
export type {
|
|
126
|
+
export type {
|
|
127
|
+
AppShellContentProps,
|
|
128
|
+
AppShellMainProps,
|
|
129
|
+
AppShellProps,
|
|
130
|
+
AppShellRootProps,
|
|
131
|
+
AppShellSkipLinkProps,
|
|
132
|
+
} from "../shell/src/AppShell";
|
|
127
133
|
export { AppSidebar } from "../shell/src/AppSidebar";
|
|
128
134
|
export type { AppSidebarProps } from "../shell/src/AppSidebar";
|
|
135
|
+
// Composable sidebar parts (`Sidebar.Root`, `Sidebar.Rail`, ...) and their prop types.
|
|
136
|
+
export * from "../shell/src/Sidebar";
|
|
129
137
|
export { SidebarRail, RailTile, RailDivider } from "../shell/src/SidebarRail";
|
|
130
138
|
export type { SidebarRailProps, RailTileProps, RailDividerProps } from "../shell/src/SidebarRail";
|
|
131
139
|
export { SidebarResizeHandle, useAppShellResize } from "../shell/src/SidebarResize";
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { IconLoader2 } from "@tabler/icons-react";
|
|
3
|
+
import { confirmFieldLabelClass } from "../../shell/src/app-shell-classes";
|
|
4
|
+
import { cn } from "../utils/cn";
|
|
5
|
+
import {
|
|
6
|
+
AlertDialogAction,
|
|
7
|
+
AlertDialogCancel,
|
|
8
|
+
AlertDialogContent,
|
|
9
|
+
} from "./alert-dialog";
|
|
10
|
+
import { Button, type ButtonProps } from "./button";
|
|
11
|
+
import {
|
|
12
|
+
Dialog,
|
|
13
|
+
DialogContent,
|
|
14
|
+
DialogDescription,
|
|
15
|
+
DialogFooter,
|
|
16
|
+
DialogHeader,
|
|
17
|
+
DialogTitle,
|
|
18
|
+
} from "./dialog";
|
|
19
|
+
import { Input } from "./input";
|
|
20
|
+
|
|
21
|
+
export type ConfirmDialogSize = "default" | "sm" | "md";
|
|
22
|
+
|
|
23
|
+
type ConfirmDialogContextValue = {
|
|
24
|
+
size: ConfirmDialogSize;
|
|
25
|
+
alert: boolean;
|
|
26
|
+
pending: boolean;
|
|
27
|
+
focusCancel: boolean;
|
|
28
|
+
confirmPhrase: string | undefined;
|
|
29
|
+
typed: string;
|
|
30
|
+
setTyped: (value: string) => void;
|
|
31
|
+
phraseMatches: boolean;
|
|
32
|
+
/** Calls the Root's `onOpenChange(false)` unless `pending` (and not `dismissibleWhilePending`). */
|
|
33
|
+
requestClose: () => void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const ConfirmDialogContext = React.createContext<ConfirmDialogContextValue>({
|
|
37
|
+
size: "default",
|
|
38
|
+
alert: true,
|
|
39
|
+
pending: false,
|
|
40
|
+
focusCancel: false,
|
|
41
|
+
confirmPhrase: undefined,
|
|
42
|
+
typed: "",
|
|
43
|
+
setTyped: () => {},
|
|
44
|
+
phraseMatches: true,
|
|
45
|
+
requestClose: () => {},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/** State of the nearest `ConfirmDialog.Root`, for custom parts. */
|
|
49
|
+
export function useConfirmDialog(): ConfirmDialogContextValue {
|
|
50
|
+
return React.useContext(ConfirmDialogContext);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type ConfirmDialogRootProps = React.ComponentProps<typeof Dialog> & {
|
|
54
|
+
/** Layout slot for the other parts. Default `"default"`. */
|
|
55
|
+
size?: ConfirmDialogSize;
|
|
56
|
+
/** Blocking alert dialog: no outside dismiss, no close button, Cancel focused. Default `size === "default"`. */
|
|
57
|
+
alert?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* While `true`, close requests (Escape, outside click, close button, Cancel)
|
|
60
|
+
* are swallowed, Cancel is disabled and Action shows its pending state.
|
|
61
|
+
* Needs a controlled `open`.
|
|
62
|
+
*/
|
|
63
|
+
pending?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* While `pending`, Escape, outside click and the close button still close
|
|
66
|
+
* (Root `onOpenChange(false)` runs). Cancel stays disabled and Action stays
|
|
67
|
+
* pending. Default `false`.
|
|
68
|
+
*/
|
|
69
|
+
dismissibleWhilePending?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Non-alert only: Cancel renders `data-alert-cancel=""`, so a Content with
|
|
72
|
+
* `role="alertdialog"` focuses it on open. An explicit `data-alert-cancel`
|
|
73
|
+
* on Cancel wins. No-op with `alert` (its Cancel already has it). Default `false`.
|
|
74
|
+
*/
|
|
75
|
+
focusCancel?: boolean;
|
|
76
|
+
/** Type-to-confirm phrase for `ConfirmDialog.Field`. Action stays disabled until it matches (trimmed, case-insensitive). */
|
|
77
|
+
confirmPhrase?: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function Root({
|
|
81
|
+
size = "default",
|
|
82
|
+
alert = size === "default",
|
|
83
|
+
pending = false,
|
|
84
|
+
dismissibleWhilePending = false,
|
|
85
|
+
focusCancel = false,
|
|
86
|
+
confirmPhrase,
|
|
87
|
+
open,
|
|
88
|
+
onOpenChange,
|
|
89
|
+
children,
|
|
90
|
+
...props
|
|
91
|
+
}: ConfirmDialogRootProps) {
|
|
92
|
+
const [typed, setTyped] = React.useState("");
|
|
93
|
+
const [prevOpen, setPrevOpen] = React.useState(open);
|
|
94
|
+
if (open !== prevOpen) {
|
|
95
|
+
setPrevOpen(open);
|
|
96
|
+
if (open) setTyped("");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const handleOpenChange = (next: boolean) => {
|
|
100
|
+
if (!next && pending && !dismissibleWhilePending) return;
|
|
101
|
+
if (next) setTyped("");
|
|
102
|
+
onOpenChange?.(next);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const phraseMatches =
|
|
106
|
+
confirmPhrase === undefined ||
|
|
107
|
+
typed.trim().toLowerCase() === confirmPhrase.trim().toLowerCase();
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<ConfirmDialogContext.Provider
|
|
111
|
+
value={{
|
|
112
|
+
size,
|
|
113
|
+
alert,
|
|
114
|
+
pending,
|
|
115
|
+
focusCancel,
|
|
116
|
+
confirmPhrase,
|
|
117
|
+
typed,
|
|
118
|
+
setTyped,
|
|
119
|
+
phraseMatches,
|
|
120
|
+
requestClose: () => handleOpenChange(false),
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
|
|
124
|
+
{children}
|
|
125
|
+
</Dialog>
|
|
126
|
+
</ConfirmDialogContext.Provider>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
Root.displayName = "ConfirmDialog.Root";
|
|
130
|
+
|
|
131
|
+
export type ConfirmDialogContentProps = React.ComponentProps<typeof DialogContent>;
|
|
132
|
+
|
|
133
|
+
/** Alert or plain dialog content (from Root `alert`); `sm` and `md` cap the width. */
|
|
134
|
+
function Content({ className, ...props }: ConfirmDialogContentProps) {
|
|
135
|
+
const { size, alert } = useConfirmDialog();
|
|
136
|
+
const Comp = alert ? AlertDialogContent : DialogContent;
|
|
137
|
+
return <Comp {...props} className={cn(size !== "default" && "sm:max-w-96", className)} />;
|
|
138
|
+
}
|
|
139
|
+
Content.displayName = "ConfirmDialog.Content";
|
|
140
|
+
|
|
141
|
+
export type ConfirmDialogHeaderProps = React.HTMLAttributes<HTMLDivElement>;
|
|
142
|
+
|
|
143
|
+
function Header({ className, ...props }: ConfirmDialogHeaderProps) {
|
|
144
|
+
const { size } = useConfirmDialog();
|
|
145
|
+
return <DialogHeader {...props} className={cn(size === "md" && "pb-4", className)} />;
|
|
146
|
+
}
|
|
147
|
+
Header.displayName = "ConfirmDialog.Header";
|
|
148
|
+
|
|
149
|
+
export type ConfirmDialogTitleProps = React.ComponentProps<typeof DialogTitle>;
|
|
150
|
+
|
|
151
|
+
function Title({ className, ...props }: ConfirmDialogTitleProps) {
|
|
152
|
+
const { size } = useConfirmDialog();
|
|
153
|
+
return <DialogTitle {...props} className={cn(size === "sm" && "mb-2 text-sm font-medium", className)} />;
|
|
154
|
+
}
|
|
155
|
+
Title.displayName = "ConfirmDialog.Title";
|
|
156
|
+
|
|
157
|
+
export type ConfirmDialogDescriptionProps = React.ComponentProps<typeof DialogDescription>;
|
|
158
|
+
|
|
159
|
+
function Description({ className, ...props }: ConfirmDialogDescriptionProps) {
|
|
160
|
+
const { size } = useConfirmDialog();
|
|
161
|
+
return (
|
|
162
|
+
<DialogDescription
|
|
163
|
+
{...props}
|
|
164
|
+
className={cn(
|
|
165
|
+
size === "sm" && "mb-4 text-sm text-muted-foreground",
|
|
166
|
+
size === "md" && "py-6 text-muted-foreground",
|
|
167
|
+
className,
|
|
168
|
+
)}
|
|
169
|
+
/>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
// Matched by `DialogContent` when it checks an alert dialog for a description.
|
|
173
|
+
Description.displayName = "ConfirmDialog.Description";
|
|
174
|
+
|
|
175
|
+
export type ConfirmDialogBodyProps = React.ComponentProps<"div">;
|
|
176
|
+
|
|
177
|
+
/** Plain `<div>` for extra content. No default classes. */
|
|
178
|
+
function Body(props: ConfirmDialogBodyProps) {
|
|
179
|
+
return <div {...props} />;
|
|
180
|
+
}
|
|
181
|
+
Body.displayName = "ConfirmDialog.Body";
|
|
182
|
+
|
|
183
|
+
export type ConfirmDialogFooterProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
184
|
+
/**
|
|
185
|
+
* Right-aligned row of buttons at every width, laid out like a
|
|
186
|
+
* `flex justify-end gap-2` div inside the footer. Adds `justify-end flex-row!`:
|
|
187
|
+
* the `!important` beats both the footer's own `flex-col-reverse` below `sm`
|
|
188
|
+
* and the compact sheet's descendant `flex-col-reverse` rule, which has higher
|
|
189
|
+
* specificity than a plain class. Keeps `data-slot` and every other footer
|
|
190
|
+
* class. No-op for `size="sm"` (already a row). Default `false`.
|
|
191
|
+
*/
|
|
192
|
+
row?: boolean;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
function Footer({ row = false, className, ...props }: ConfirmDialogFooterProps) {
|
|
196
|
+
const { size } = useConfirmDialog();
|
|
197
|
+
if (size === "sm") return <div {...props} className={cn("flex justify-end gap-2", className)} />;
|
|
198
|
+
return (
|
|
199
|
+
<DialogFooter
|
|
200
|
+
{...props}
|
|
201
|
+
className={cn(size === "md" && "pt-4", row && "justify-end flex-row!", className)}
|
|
202
|
+
/>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
Footer.displayName = "ConfirmDialog.Footer";
|
|
206
|
+
|
|
207
|
+
export type ConfirmDialogCancelProps = ButtonProps;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Alert: a `DialogClose` outline button. Otherwise a ghost button that runs
|
|
211
|
+
* `onClick` and then, unless it called `preventDefault()`, the Root's
|
|
212
|
+
* `onOpenChange(false)`. Disabled while pending. With Root `focusCancel`
|
|
213
|
+
* it also gets `data-alert-cancel`.
|
|
214
|
+
*/
|
|
215
|
+
function Cancel({ children = "Cancel", variant, size, disabled, onClick, ...props }: ConfirmDialogCancelProps) {
|
|
216
|
+
const dialog = useConfirmDialog();
|
|
217
|
+
if (dialog.alert) {
|
|
218
|
+
return (
|
|
219
|
+
<AlertDialogCancel
|
|
220
|
+
variant={variant}
|
|
221
|
+
size={size}
|
|
222
|
+
{...props}
|
|
223
|
+
onClick={onClick}
|
|
224
|
+
disabled={disabled || dialog.pending}
|
|
225
|
+
>
|
|
226
|
+
{children}
|
|
227
|
+
</AlertDialogCancel>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
return (
|
|
231
|
+
<Button
|
|
232
|
+
variant={variant ?? "ghost"}
|
|
233
|
+
size={size ?? (dialog.size === "sm" ? "sm" : undefined)}
|
|
234
|
+
data-alert-cancel={dialog.focusCancel ? "" : undefined}
|
|
235
|
+
{...props}
|
|
236
|
+
onClick={(event) => {
|
|
237
|
+
onClick?.(event);
|
|
238
|
+
if (!event.defaultPrevented) dialog.requestClose();
|
|
239
|
+
}}
|
|
240
|
+
disabled={disabled || dialog.pending}
|
|
241
|
+
>
|
|
242
|
+
{children}
|
|
243
|
+
</Button>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
Cancel.displayName = "ConfirmDialog.Cancel";
|
|
247
|
+
|
|
248
|
+
export type ConfirmDialogActionProps = ButtonProps & {
|
|
249
|
+
/** Default from Root `pending`. Disables the button. */
|
|
250
|
+
pending?: boolean;
|
|
251
|
+
/** Shown with a spinner while pending. Without it the children stay. */
|
|
252
|
+
pendingLabel?: React.ReactNode;
|
|
253
|
+
/** Spinner size in px. Default `14`. */
|
|
254
|
+
spinnerSize?: number;
|
|
255
|
+
/** Merged into the spinner className after `animate-spin`, e.g. `"mr-2"`. */
|
|
256
|
+
spinnerClassName?: string;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/** Confirm button. Does not close the dialog. Disabled while pending or until the confirm phrase matches. */
|
|
260
|
+
function Action({
|
|
261
|
+
pending: pendingProp,
|
|
262
|
+
pendingLabel,
|
|
263
|
+
spinnerSize = 14,
|
|
264
|
+
spinnerClassName,
|
|
265
|
+
size,
|
|
266
|
+
disabled,
|
|
267
|
+
children,
|
|
268
|
+
...props
|
|
269
|
+
}: ConfirmDialogActionProps) {
|
|
270
|
+
const dialog = useConfirmDialog();
|
|
271
|
+
const pending = pendingProp ?? dialog.pending;
|
|
272
|
+
const content =
|
|
273
|
+
pending && pendingLabel !== undefined ? (
|
|
274
|
+
<>
|
|
275
|
+
<IconLoader2 size={spinnerSize} className={cn("animate-spin", spinnerClassName)} />
|
|
276
|
+
{pendingLabel}
|
|
277
|
+
</>
|
|
278
|
+
) : (
|
|
279
|
+
children
|
|
280
|
+
);
|
|
281
|
+
const isDisabled = disabled || pending || !dialog.phraseMatches;
|
|
282
|
+
if (dialog.alert) {
|
|
283
|
+
return (
|
|
284
|
+
<AlertDialogAction size={size} {...props} disabled={isDisabled}>
|
|
285
|
+
{content}
|
|
286
|
+
</AlertDialogAction>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
return (
|
|
290
|
+
<Button size={size ?? (dialog.size === "sm" ? "sm" : undefined)} {...props} disabled={isDisabled}>
|
|
291
|
+
{content}
|
|
292
|
+
</Button>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
Action.displayName = "ConfirmDialog.Action";
|
|
296
|
+
|
|
297
|
+
export type ConfirmDialogFieldProps = {
|
|
298
|
+
/** Replaces the whole label content. Default "Type <phrase> to confirm". */
|
|
299
|
+
label?: React.ReactNode;
|
|
300
|
+
/** Enter in the input, only while the phrase matches and not pending. */
|
|
301
|
+
onEnter?: () => void;
|
|
302
|
+
className?: string;
|
|
303
|
+
/** Spread onto the `Input`; `id` also sets the label `htmlFor`. */
|
|
304
|
+
inputProps?: Omit<React.ComponentProps<typeof Input>, "value" | "defaultValue" | "onChange">;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
/** Type-to-confirm input bound to the Root `confirmPhrase`. */
|
|
308
|
+
function Field({ label, onEnter, className, inputProps }: ConfirmDialogFieldProps) {
|
|
309
|
+
const { confirmPhrase = "", typed, setTyped, phraseMatches, pending } = useConfirmDialog();
|
|
310
|
+
const autoId = React.useId();
|
|
311
|
+
const { id = autoId, onKeyDown, ...rest } = inputProps ?? {};
|
|
312
|
+
return (
|
|
313
|
+
<div className={cn("space-y-2", className)}>
|
|
314
|
+
<label htmlFor={id} className={confirmFieldLabelClass}>
|
|
315
|
+
{label ?? (
|
|
316
|
+
<>
|
|
317
|
+
Type{" "}
|
|
318
|
+
<span className="font-mono text-foreground">{confirmPhrase}</span>{" "}
|
|
319
|
+
to confirm
|
|
320
|
+
</>
|
|
321
|
+
)}
|
|
322
|
+
</label>
|
|
323
|
+
<Input
|
|
324
|
+
id={id}
|
|
325
|
+
autoComplete="off"
|
|
326
|
+
autoFocus
|
|
327
|
+
value={typed}
|
|
328
|
+
onChange={(event) => setTyped(event.target.value)}
|
|
329
|
+
disabled={pending}
|
|
330
|
+
placeholder={confirmPhrase}
|
|
331
|
+
{...rest}
|
|
332
|
+
onKeyDown={(event) => {
|
|
333
|
+
onKeyDown?.(event);
|
|
334
|
+
if (event.key !== "Enter" || !phraseMatches || pending) return;
|
|
335
|
+
event.preventDefault();
|
|
336
|
+
onEnter?.();
|
|
337
|
+
}}
|
|
338
|
+
/>
|
|
339
|
+
</div>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
Field.displayName = "ConfirmDialog.Field";
|
|
343
|
+
|
|
344
|
+
export const ConfirmDialogParts = {
|
|
345
|
+
Root,
|
|
346
|
+
Content,
|
|
347
|
+
Header,
|
|
348
|
+
Title,
|
|
349
|
+
Description,
|
|
350
|
+
Body,
|
|
351
|
+
Footer,
|
|
352
|
+
Cancel,
|
|
353
|
+
Action,
|
|
354
|
+
Field,
|
|
355
|
+
};
|