@cronus-ui/ui 0.6.8 → 0.6.10

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; on resolve
34
- * the dialog closes, on reject it stays open and surfaces the error.
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
- }) => void | Promise<void>;
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; on reject it stays open and renders the error in a
53
- * `role="alert"` region. Works controlled (`open`/`onOpenChange`) or
54
- * uncontrolled (`defaultOpen`).
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; on reject it stays open and renders the error in a
53
- * `role="alert"` region. Works controlled (`open`/`onOpenChange`) or
54
- * uncontrolled (`defaultOpen`).
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
- setOpen(false);
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
- setOpen(false);
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, _jsxs(DialogContent, { ref: ref, "data-slot": "invite-dialog", className: cn("max-w-md", className), ...contentProps, 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] })] })] })] })] }));
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.8",
3
+ "version": "0.6.10",
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.8",
762
+ "@cronus-ui/theme": "0.6.10",
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.8",
906
+ "@cronus-ui/theme": "0.6.10",
907
907
  "@dnd-kit/core": "^6.3.1",
908
908
  "@dnd-kit/sortable": "^10.0.0",
909
909
  "@dnd-kit/utilities": "^3.2.2",