@cronus-ui/ui 0.6.8 → 0.6.11
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.
|
@@ -10,7 +10,18 @@ export interface InviteDialogLabels {
|
|
|
10
10
|
cancel?: string;
|
|
11
11
|
sending?: string;
|
|
12
12
|
errorFallback?: string;
|
|
13
|
+
sentTitle?: string;
|
|
14
|
+
sentDescription?: string;
|
|
15
|
+
link?: string;
|
|
16
|
+
copy?: string;
|
|
17
|
+
copied?: string;
|
|
18
|
+
done?: string;
|
|
13
19
|
}
|
|
20
|
+
/** Returned from `onInvite` to keep the dialog open with a copyable invite URL. */
|
|
21
|
+
export interface InviteResult {
|
|
22
|
+
url?: string;
|
|
23
|
+
}
|
|
24
|
+
type InviteHandlerReturn = void | InviteResult | Promise<void | InviteResult>;
|
|
14
25
|
export interface InviteRole {
|
|
15
26
|
value: string;
|
|
16
27
|
label: string;
|
|
@@ -30,13 +41,14 @@ export interface InviteDialogProps extends Omit<ComponentPropsWithoutRef<typeof
|
|
|
30
41
|
defaultRole?: string;
|
|
31
42
|
/**
|
|
32
43
|
* Invoked with the submitted email and role. If it returns a promise, the send
|
|
33
|
-
* button shows a spinner and actions are disabled until it settles
|
|
34
|
-
* the dialog
|
|
44
|
+
* button shows a spinner and actions are disabled until it settles. On reject
|
|
45
|
+
* the dialog stays open and surfaces the error. On resolve: a `{ url }` keeps
|
|
46
|
+
* the dialog open with a copyable link; otherwise the dialog closes.
|
|
35
47
|
*/
|
|
36
48
|
onInvite?: (payload: {
|
|
37
49
|
email: string;
|
|
38
50
|
role: string;
|
|
39
|
-
}) =>
|
|
51
|
+
}) => InviteHandlerReturn;
|
|
40
52
|
/** Override any subset of the built-in English strings. */
|
|
41
53
|
labels?: InviteDialogLabels;
|
|
42
54
|
/** Forwarded to the dialog content surface. */
|
|
@@ -49,9 +61,10 @@ export interface InviteDialogProps extends Omit<ComponentPropsWithoutRef<typeof
|
|
|
49
61
|
* Behavior: `onInvite` may be async — while its promise is pending the send
|
|
50
62
|
* button shows a `Spinner`, both actions are disabled, and the dialog cannot be
|
|
51
63
|
* dismissed so the operation can't be interrupted mid-flight. On resolve the
|
|
52
|
-
* dialog closes
|
|
53
|
-
*
|
|
54
|
-
*
|
|
64
|
+
* dialog closes, unless the result is `{ url }` — then it stays open on a
|
|
65
|
+
* success surface with a copyable link. On reject it stays open and renders
|
|
66
|
+
* the error in a `role="alert"` region. Works controlled (`open`/`onOpenChange`)
|
|
67
|
+
* or uncontrolled (`defaultOpen`).
|
|
55
68
|
*
|
|
56
69
|
* Performance/a11y: late promise settlements are ignored after unmount via a
|
|
57
70
|
* mounted ref. The spinner is decorative (`aria-hidden`) with loading state
|
|
@@ -59,4 +72,5 @@ export interface InviteDialogProps extends Omit<ComponentPropsWithoutRef<typeof
|
|
|
59
72
|
* underlying primitive and degrades under `prefers-reduced-motion`.
|
|
60
73
|
*/
|
|
61
74
|
export declare function InviteDialog({ trigger, open, defaultOpen, onOpenChange, roles, defaultRole, onInvite, labels, className, ref, ...contentProps }: InviteDialogProps): import("react").JSX.Element;
|
|
75
|
+
export {};
|
|
62
76
|
//# sourceMappingURL=invite-dialog.d.ts.map
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { CircleAlert } from "lucide-react";
|
|
4
4
|
import { useCallback, useEffect, useId, useRef, useState, } from "react";
|
|
5
5
|
import { cn } from "../lib/cn.js";
|
|
6
6
|
import { Button } from "./button.js";
|
|
7
|
+
import { CopyButton } from "./copy-button.js";
|
|
7
8
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "./dialog.js";
|
|
8
9
|
import { Field, FieldLabel } from "./field.js";
|
|
9
10
|
import { Input } from "./input.js";
|
|
@@ -19,6 +20,12 @@ const DEFAULT_LABELS = {
|
|
|
19
20
|
cancel: "Cancel",
|
|
20
21
|
sending: "Sending",
|
|
21
22
|
errorFallback: "Something went wrong. Please try again.",
|
|
23
|
+
sentTitle: "Invite sent",
|
|
24
|
+
sentDescription: "Copy the link and send it to your teammate.",
|
|
25
|
+
link: "Invite link",
|
|
26
|
+
copy: "Copy invite link",
|
|
27
|
+
copied: "Copied",
|
|
28
|
+
done: "Done",
|
|
22
29
|
};
|
|
23
30
|
const DEFAULT_ROLES = [
|
|
24
31
|
{ value: "member", label: "Member" },
|
|
@@ -42,6 +49,15 @@ function resolveDefaultRole(roles, defaultRole) {
|
|
|
42
49
|
}
|
|
43
50
|
return roles[0]?.value ?? "";
|
|
44
51
|
}
|
|
52
|
+
function inviteUrlOf(result) {
|
|
53
|
+
if (!result || typeof result !== "object" || !("url" in result))
|
|
54
|
+
return undefined;
|
|
55
|
+
const url = result.url;
|
|
56
|
+
if (typeof url !== "string")
|
|
57
|
+
return undefined;
|
|
58
|
+
const trimmed = url.trim();
|
|
59
|
+
return trimmed ? trimmed : undefined;
|
|
60
|
+
}
|
|
45
61
|
/**
|
|
46
62
|
* An ergonomic invite dialog for adding a member by email and role, composed
|
|
47
63
|
* on top of the accessible `Dialog` primitive (focus trap, overlay, Escape).
|
|
@@ -49,9 +65,10 @@ function resolveDefaultRole(roles, defaultRole) {
|
|
|
49
65
|
* Behavior: `onInvite` may be async — while its promise is pending the send
|
|
50
66
|
* button shows a `Spinner`, both actions are disabled, and the dialog cannot be
|
|
51
67
|
* dismissed so the operation can't be interrupted mid-flight. On resolve the
|
|
52
|
-
* dialog closes
|
|
53
|
-
*
|
|
54
|
-
*
|
|
68
|
+
* dialog closes, unless the result is `{ url }` — then it stays open on a
|
|
69
|
+
* success surface with a copyable link. On reject it stays open and renders
|
|
70
|
+
* the error in a `role="alert"` region. Works controlled (`open`/`onOpenChange`)
|
|
71
|
+
* or uncontrolled (`defaultOpen`).
|
|
55
72
|
*
|
|
56
73
|
* Performance/a11y: late promise settlements are ignored after unmount via a
|
|
57
74
|
* mounted ref. The spinner is decorative (`aria-hidden`) with loading state
|
|
@@ -69,10 +86,12 @@ export function InviteDialog({ trigger, open, defaultOpen, onOpenChange, roles,
|
|
|
69
86
|
const [role, setRole] = useState(initialRole);
|
|
70
87
|
const [loading, setLoading] = useState(false);
|
|
71
88
|
const [error, setError] = useState(null);
|
|
89
|
+
const [sentUrl, setSentUrl] = useState(null);
|
|
72
90
|
const loadingRef = useRef(false);
|
|
73
91
|
const mountedRef = useRef(true);
|
|
74
92
|
const emailId = useId();
|
|
75
93
|
const roleId = useId();
|
|
94
|
+
const linkId = useId();
|
|
76
95
|
useEffect(() => {
|
|
77
96
|
mountedRef.current = true;
|
|
78
97
|
return () => {
|
|
@@ -93,6 +112,7 @@ export function InviteDialog({ trigger, open, defaultOpen, onOpenChange, roles,
|
|
|
93
112
|
setError(null);
|
|
94
113
|
setEmail("");
|
|
95
114
|
setRole(initialRole);
|
|
115
|
+
setSentUrl(null);
|
|
96
116
|
}
|
|
97
117
|
setOpen(next);
|
|
98
118
|
}, [initialRole, setOpen]);
|
|
@@ -116,17 +136,25 @@ export function InviteDialog({ trigger, open, defaultOpen, onOpenChange, roles,
|
|
|
116
136
|
}
|
|
117
137
|
const result = onInvite({ email: trimmed, role });
|
|
118
138
|
if (!(result instanceof Promise)) {
|
|
119
|
-
|
|
139
|
+
const url = inviteUrlOf(result);
|
|
140
|
+
if (url)
|
|
141
|
+
setSentUrl(url);
|
|
142
|
+
else
|
|
143
|
+
setOpen(false);
|
|
120
144
|
return;
|
|
121
145
|
}
|
|
122
146
|
beginLoading(true);
|
|
123
147
|
try {
|
|
124
|
-
await result;
|
|
148
|
+
const settled = await result;
|
|
125
149
|
if (!mountedRef.current) {
|
|
126
150
|
return;
|
|
127
151
|
}
|
|
128
152
|
beginLoading(false);
|
|
129
|
-
|
|
153
|
+
const url = inviteUrlOf(settled);
|
|
154
|
+
if (url)
|
|
155
|
+
setSentUrl(url);
|
|
156
|
+
else
|
|
157
|
+
setOpen(false);
|
|
130
158
|
}
|
|
131
159
|
catch (err) {
|
|
132
160
|
if (!mountedRef.current) {
|
|
@@ -136,6 +164,6 @@ export function InviteDialog({ trigger, open, defaultOpen, onOpenChange, roles,
|
|
|
136
164
|
setError(resolveErrorMessage(err, resolvedLabels.errorFallback));
|
|
137
165
|
}
|
|
138
166
|
}, [beginLoading, email, onInvite, resolvedLabels.errorFallback, role, setOpen]);
|
|
139
|
-
return (_jsxs(Dialog, { open: isOpen, onOpenChange: handleOpenChange, children: [trigger ? _jsx(DialogTrigger, { asChild: true, children: trigger }) : null,
|
|
167
|
+
return (_jsxs(Dialog, { open: isOpen, onOpenChange: handleOpenChange, children: [trigger ? _jsx(DialogTrigger, { asChild: true, children: trigger }) : null, _jsx(DialogContent, { ref: ref, "data-slot": "invite-dialog", className: cn("max-w-md", className), ...contentProps, children: sentUrl ? (_jsxs("div", { "data-slot": "invite-dialog-success", className: "flex flex-col gap-4", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: resolvedLabels.sentTitle }), resolvedLabels.sentDescription ? (_jsx(DialogDescription, { children: resolvedLabels.sentDescription })) : null] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: linkId, children: resolvedLabels.link }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { id: linkId, readOnly: true, value: sentUrl, autoComplete: "off" }), _jsx(CopyButton, { value: sentUrl, copyLabel: resolvedLabels.copy, copiedLabel: resolvedLabels.copied, variant: "outline", size: "icon" })] })] }), _jsx(DialogFooter, { children: _jsx(Button, { type: "button", onClick: () => handleOpenChange(false), children: resolvedLabels.done }) })] })) : (_jsxs(_Fragment, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: resolvedLabels.title }), resolvedLabels.description ? (_jsx(DialogDescription, { children: resolvedLabels.description })) : null] }), _jsxs("form", { className: "flex flex-col gap-4", onSubmit: handleSubmit, children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: emailId, children: resolvedLabels.email }), _jsx(Input, { id: emailId, type: "email", name: "email", autoComplete: "email", required: true, value: email, onChange: (event) => setEmail(event.target.value), placeholder: resolvedLabels.emailPlaceholder, disabled: loading })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: roleId, children: resolvedLabels.role }), _jsxs(Select, { value: role, onValueChange: setRole, disabled: loading, children: [_jsx(SelectTrigger, { id: roleId, children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: resolvedRoles.map((item) => (_jsx(SelectItem, { value: item.value, children: item.label }, item.value))) })] })] }), error ? (_jsxs("div", { role: "alert", "data-slot": "invite-dialog-error", className: "flex items-start gap-2 rounded-lg border border-error/30 bg-error/10 px-3 py-2 text-start text-sm text-error-strong", children: [_jsx(CircleAlert, { "aria-hidden": true, className: "mt-0.5 size-4 shrink-0" }), _jsx("span", { children: error })] })) : null, _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", disabled: loading, onClick: () => handleOpenChange(false), children: resolvedLabels.cancel }), _jsxs(Button, { type: "submit", "data-slot": "invite-dialog-send", disabled: loading, "aria-busy": loading, className: "min-w-24", children: [loading ? _jsx(Spinner, { size: "sm", "aria-hidden": true }) : null, loading ? resolvedLabels.sending : resolvedLabels.send] })] })] })] })) })] }));
|
|
140
168
|
}
|
|
141
169
|
//# sourceMappingURL=invite-dialog.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -142,7 +142,7 @@ export { Input } from "./components/input.js";
|
|
|
142
142
|
export type { InputGroupAddonProps, InputGroupProps } from "./components/input-group.js";
|
|
143
143
|
export { InputGroup, InputGroupAddon } from "./components/input-group.js";
|
|
144
144
|
export { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, } from "./components/input-otp.js";
|
|
145
|
-
export type { InviteDialogLabels, InviteDialogProps, InviteRole, } from "./components/invite-dialog.js";
|
|
145
|
+
export type { InviteDialogLabels, InviteDialogProps, InviteResult, InviteRole, } from "./components/invite-dialog.js";
|
|
146
146
|
export { InviteDialog } from "./components/invite-dialog.js";
|
|
147
147
|
export type { JsonViewerProps } from "./components/json-viewer.js";
|
|
148
148
|
export { JsonViewer } from "./components/json-viewer.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cronus-ui/ui",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.11",
|
|
4
4
|
"description": "Cronus UI — themeable, accessible React components (Radix + CVA + Tailwind v4).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -759,7 +759,7 @@
|
|
|
759
759
|
"vaul": "^1.1.2"
|
|
760
760
|
},
|
|
761
761
|
"peerDependencies": {
|
|
762
|
-
"@cronus-ui/theme": "0.6.
|
|
762
|
+
"@cronus-ui/theme": "0.6.11",
|
|
763
763
|
"@dnd-kit/core": "^6.3.1",
|
|
764
764
|
"@dnd-kit/sortable": "^10.0.0",
|
|
765
765
|
"@dnd-kit/utilities": "^3.2.2",
|
|
@@ -903,7 +903,7 @@
|
|
|
903
903
|
}
|
|
904
904
|
},
|
|
905
905
|
"devDependencies": {
|
|
906
|
-
"@cronus-ui/theme": "0.6.
|
|
906
|
+
"@cronus-ui/theme": "0.6.11",
|
|
907
907
|
"@dnd-kit/core": "^6.3.1",
|
|
908
908
|
"@dnd-kit/sortable": "^10.0.0",
|
|
909
909
|
"@dnd-kit/utilities": "^3.2.2",
|