@meetreeve/module-team 0.0.0-stage → 0.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.
@@ -0,0 +1,607 @@
1
+ "use client";
2
+
3
+ // src/config/team-members-panel.tsx
4
+ import { useCallback, useEffect as useEffect2, useId, useRef as useRef2, useState } from "react";
5
+
6
+ // src/config/modal.tsx
7
+ import { useEffect, useRef } from "react";
8
+ import { createPortal } from "react-dom";
9
+ import { jsx } from "react/jsx-runtime";
10
+ function Modal({
11
+ open,
12
+ onClose,
13
+ titleId,
14
+ descriptionId,
15
+ dismissible = true,
16
+ children
17
+ }) {
18
+ const panelRef = useRef(null);
19
+ const closeRef = useRef({ onClose, dismissible });
20
+ useEffect(() => {
21
+ closeRef.current = { onClose, dismissible };
22
+ });
23
+ useEffect(() => {
24
+ if (!open) return;
25
+ const previouslyFocused = document.activeElement;
26
+ const first = panelRef.current?.querySelector(
27
+ "input, select, textarea, button:not([disabled])"
28
+ );
29
+ first?.focus();
30
+ const onKeyDown = (e) => {
31
+ if (e.key === "Escape" && closeRef.current.dismissible) closeRef.current.onClose();
32
+ };
33
+ document.addEventListener("keydown", onKeyDown);
34
+ return () => {
35
+ document.removeEventListener("keydown", onKeyDown);
36
+ previouslyFocused?.focus?.();
37
+ };
38
+ }, [open]);
39
+ if (!open || typeof document === "undefined") return null;
40
+ return createPortal(
41
+ /* @__PURE__ */ jsx(
42
+ "div",
43
+ {
44
+ className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4",
45
+ onMouseDown: (e) => {
46
+ if (e.target === e.currentTarget && dismissible) onClose();
47
+ },
48
+ children: /* @__PURE__ */ jsx(
49
+ "div",
50
+ {
51
+ ref: panelRef,
52
+ role: "dialog",
53
+ "aria-modal": "true",
54
+ "aria-labelledby": titleId,
55
+ "aria-describedby": descriptionId,
56
+ className: "w-full max-w-md rounded-lg border border-border bg-background p-6 text-foreground shadow-lg",
57
+ children
58
+ }
59
+ )
60
+ }
61
+ ),
62
+ document.body
63
+ );
64
+ }
65
+
66
+ // src/config/team-members-panel.tsx
67
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
68
+ var DEFAULT_INVITE_ROLES = ["admin", "member", "viewer"];
69
+ var ROLE_LABELS = {
70
+ owner: "Owner",
71
+ admin: "Admin",
72
+ billing_manager: "Billing",
73
+ member: "Member",
74
+ viewer: "Viewer"
75
+ };
76
+ var INVITE_ROLE_OPTIONS = {
77
+ admin: {
78
+ label: "Admin \u2014 Can manage the team",
79
+ description: "Can manage team members, billing, and all settings"
80
+ },
81
+ member: {
82
+ label: "Member \u2014 Full agent access",
83
+ description: "Can use all agents and connectors, but can't manage team"
84
+ },
85
+ viewer: {
86
+ label: "Viewer \u2014 Read-only dashboards",
87
+ description: "Read-only access to dashboards and reports"
88
+ }
89
+ };
90
+ function roleLabel(role) {
91
+ return ROLE_LABELS[role] ?? role;
92
+ }
93
+ function nameFromEmail(email) {
94
+ if (!email) return "Team Member";
95
+ return email.split("@")[0].replace(/[._-]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
96
+ }
97
+ function formatDate(iso) {
98
+ const d = new Date(iso);
99
+ return Number.isNaN(d.getTime()) ? "\u2014" : d.toLocaleDateString();
100
+ }
101
+ function formatAgo(iso) {
102
+ const d = new Date(iso);
103
+ if (Number.isNaN(d.getTime())) return "\u2014";
104
+ const diff = Date.now() - d.getTime();
105
+ const mins = Math.floor(diff / 6e4);
106
+ const hours = Math.floor(diff / 36e5);
107
+ const days = Math.floor(diff / 864e5);
108
+ if (mins < 1) return "just now";
109
+ if (mins < 60) return `${mins}m ago`;
110
+ if (hours < 24) return `${hours}h ago`;
111
+ if (days < 7) return `${days}d ago`;
112
+ return d.toLocaleDateString();
113
+ }
114
+ function errorInfo(err, fallback) {
115
+ const e = typeof err === "object" && err !== null ? err : {};
116
+ const detail = typeof e.body?.detail === "string" ? e.body.detail : "";
117
+ const message = typeof e.message === "string" ? e.message : "";
118
+ const status = typeof e.status === "number" ? e.status : Number(/^(?:API error )?(\d{3})\b/.exec(message)?.[1]) || null;
119
+ return { status, message: detail || message || fallback };
120
+ }
121
+ function plural(n, one, many) {
122
+ return `${n} ${n === 1 ? one : many}`;
123
+ }
124
+ var BUTTON = "inline-flex items-center justify-center rounded-md px-3 py-1.5 text-sm font-medium disabled:pointer-events-none disabled:opacity-50";
125
+ var BUTTON_PRIMARY = `${BUTTON} bg-primary text-primary-foreground hover:bg-primary/90`;
126
+ var BUTTON_OUTLINE = `${BUTTON} border border-border bg-background hover:bg-muted`;
127
+ var BUTTON_DESTRUCTIVE = `${BUTTON} bg-destructive text-white hover:bg-destructive/90`;
128
+ var BUTTON_GHOST = `${BUTTON} text-muted-foreground hover:bg-muted hover:text-foreground`;
129
+ var CONTROL = "rounded-md border border-border bg-background text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50";
130
+ var FIELD = `${CONTROL} w-full px-3 py-2`;
131
+ var BADGE = "inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium";
132
+ function roleBadgeClass(role) {
133
+ return role === "owner" || role === "admin" ? `${BADGE} border-primary/30 bg-primary/15 text-foreground` : `${BADGE} border-border bg-muted text-muted-foreground`;
134
+ }
135
+ function TeamMembersPanel(props) {
136
+ return /* @__PURE__ */ jsx2(TeamMembersPanelBody, { ...props }, props.orgId);
137
+ }
138
+ function TeamMembersPanelBody({
139
+ client,
140
+ orgId,
141
+ inviteRoles = DEFAULT_INVITE_ROLES,
142
+ currentUserEmail,
143
+ renderMemberActions,
144
+ onNotice
145
+ }) {
146
+ const ids = useId();
147
+ const roleOptions = DEFAULT_INVITE_ROLES.filter((r) => inviteRoles.includes(r));
148
+ const defaultInviteRole = roleOptions.includes("member") ? "member" : roleOptions[0] ?? "member";
149
+ const [state, setState] = useState({ status: "loading" });
150
+ const [notice, setNotice] = useState(null);
151
+ const [busy, setBusy] = useState({});
152
+ const [inviteOpen, setInviteOpen] = useState(false);
153
+ const [inviteEmail, setInviteEmail] = useState("");
154
+ const [inviteRole, setInviteRole] = useState(defaultInviteRole);
155
+ const [inviteSubmitting, setInviteSubmitting] = useState(false);
156
+ const [memberToRemove, setMemberToRemove] = useState(null);
157
+ const mounted = useRef2(true);
158
+ const generation = useRef2(0);
159
+ const notify = useCallback(
160
+ (next) => {
161
+ if (!mounted.current) return;
162
+ setNotice(next);
163
+ onNotice?.(next);
164
+ },
165
+ [onNotice]
166
+ );
167
+ const load = useCallback(async () => {
168
+ const mine = ++generation.current;
169
+ try {
170
+ const [members, invitations] = await Promise.all([
171
+ client.listMembers(orgId),
172
+ client.listInvitations(orgId)
173
+ ]);
174
+ if (!mounted.current || mine !== generation.current) return;
175
+ setState({
176
+ status: "ready",
177
+ members,
178
+ invitations: invitations.filter((inv) => inv.status === "pending")
179
+ });
180
+ } catch (err) {
181
+ if (!mounted.current || mine !== generation.current) return;
182
+ setState({ status: "error", message: errorInfo(err, "Failed to load team data").message });
183
+ }
184
+ }, [client, orgId]);
185
+ useEffect2(() => {
186
+ mounted.current = true;
187
+ void load();
188
+ return () => {
189
+ mounted.current = false;
190
+ };
191
+ }, [load]);
192
+ const setBusyFor = (key, value) => {
193
+ if (mounted.current) setBusy((b) => ({ ...b, [key]: value }));
194
+ };
195
+ const updateReady = (fn) => {
196
+ if (!mounted.current) return;
197
+ setState((s) => s.status === "ready" ? fn(s) : s);
198
+ };
199
+ const handleInvite = async () => {
200
+ const email = inviteEmail.trim();
201
+ if (!email || inviteSubmitting) return;
202
+ setInviteSubmitting(true);
203
+ try {
204
+ await client.inviteMember(orgId, email, inviteRole);
205
+ if (!mounted.current) return;
206
+ notify({ kind: "success", message: `Invitation sent to ${email}` });
207
+ setInviteEmail("");
208
+ setInviteRole(defaultInviteRole);
209
+ setInviteOpen(false);
210
+ await load();
211
+ } catch (err) {
212
+ const { status, message } = errorInfo(err, "Failed to send invitation");
213
+ notify({
214
+ kind: "error",
215
+ message: status === 409 ? "A pending invitation already exists for this email" : message
216
+ });
217
+ } finally {
218
+ if (mounted.current) setInviteSubmitting(false);
219
+ }
220
+ };
221
+ const handleRevoke = async (inv) => {
222
+ const key = `inv:${inv.id}`;
223
+ setBusyFor(key, true);
224
+ try {
225
+ await client.revokeInvitation(orgId, inv.id);
226
+ notify({ kind: "success", message: "Invitation revoked" });
227
+ updateReady((s) => ({ ...s, invitations: s.invitations.filter((i) => i.id !== inv.id) }));
228
+ } catch {
229
+ notify({ kind: "error", message: "Failed to revoke invitation" });
230
+ } finally {
231
+ setBusyFor(key, false);
232
+ }
233
+ };
234
+ const handleResend = async (inv) => {
235
+ const key = `inv:${inv.id}`;
236
+ setBusyFor(key, true);
237
+ try {
238
+ await client.resendInvitation(orgId, inv.id);
239
+ notify({ kind: "success", message: "Invitation resent \u2014 7-day expiry reset" });
240
+ if (mounted.current) await load();
241
+ } catch {
242
+ notify({ kind: "error", message: "Failed to resend invitation" });
243
+ } finally {
244
+ setBusyFor(key, false);
245
+ }
246
+ };
247
+ const handleRoleChange = async (member, role) => {
248
+ if (role === member.role) return;
249
+ const key = `member:${member.id}`;
250
+ setBusyFor(key, true);
251
+ try {
252
+ await client.updateMemberRole(orgId, member.id, role);
253
+ notify({ kind: "success", message: `Role updated to ${roleLabel(role)}` });
254
+ updateReady((s) => ({
255
+ ...s,
256
+ members: s.members.map((m) => m.id === member.id ? { ...m, role } : m)
257
+ }));
258
+ } catch {
259
+ notify({ kind: "error", message: "Failed to update role" });
260
+ } finally {
261
+ setBusyFor(key, false);
262
+ }
263
+ };
264
+ const handleRemove = async () => {
265
+ const member = memberToRemove;
266
+ if (!member) return;
267
+ const key = `member:${member.id}`;
268
+ setBusyFor(key, true);
269
+ try {
270
+ await client.removeMember(orgId, member.id);
271
+ notify({ kind: "success", message: "Member removed from the team" });
272
+ updateReady((s) => ({ ...s, members: s.members.filter((m) => m.id !== member.id) }));
273
+ if (mounted.current) setMemberToRemove(null);
274
+ } catch (err) {
275
+ const { message } = errorInfo(err, "Failed to remove member");
276
+ notify({
277
+ kind: "error",
278
+ message: /last owner/i.test(message) ? "Can't remove the last owner" : message
279
+ });
280
+ } finally {
281
+ setBusyFor(key, false);
282
+ }
283
+ };
284
+ const ready = state.status === "ready" ? state : null;
285
+ const me = currentUserEmail?.toLowerCase() ?? null;
286
+ const inviteTitleId = `${ids}-invite-title`;
287
+ const inviteDescId = `${ids}-invite-desc`;
288
+ const removeTitleId = `${ids}-remove-title`;
289
+ const removeDescId = `${ids}-remove-desc`;
290
+ const removeBusy = memberToRemove ? Boolean(busy[`member:${memberToRemove.id}`]) : false;
291
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-6", "data-testid": "team-members-panel", "data-org-id": orgId, children: [
292
+ notice ? /* @__PURE__ */ jsx2(
293
+ "p",
294
+ {
295
+ role: notice.kind === "error" ? "alert" : "status",
296
+ "data-testid": "team-notice",
297
+ className: `rounded-md border px-3 py-2 text-sm ${notice.kind === "error" ? "border-destructive/30 bg-destructive/10 text-destructive" : "border-border bg-muted text-foreground"}`,
298
+ children: notice.message
299
+ }
300
+ ) : null,
301
+ /* @__PURE__ */ jsxs(
302
+ "section",
303
+ {
304
+ "aria-labelledby": `${ids}-members-title`,
305
+ className: "rounded-lg border border-border bg-card text-card-foreground",
306
+ children: [
307
+ /* @__PURE__ */ jsxs("header", { className: "flex flex-wrap items-center justify-between gap-3 border-b border-border p-4", children: [
308
+ /* @__PURE__ */ jsxs("div", { children: [
309
+ /* @__PURE__ */ jsxs("h3", { id: `${ids}-members-title`, className: "text-base font-semibold", children: [
310
+ "Team Members",
311
+ ready ? ` (${ready.members.length})` : ""
312
+ ] }),
313
+ ready ? /* @__PURE__ */ jsxs("p", { "data-testid": "team-seat-count", className: "text-xs text-muted-foreground", children: [
314
+ plural(ready.members.length, "seat", "seats"),
315
+ " in use",
316
+ ready.invitations.length > 0 ? ` \xB7 ${plural(ready.invitations.length, "invitation", "invitations")} pending` : ""
317
+ ] }) : null
318
+ ] }),
319
+ /* @__PURE__ */ jsx2(
320
+ "button",
321
+ {
322
+ type: "button",
323
+ className: BUTTON_PRIMARY,
324
+ disabled: roleOptions.length === 0,
325
+ onClick: () => setInviteOpen(true),
326
+ children: "Invite Member"
327
+ }
328
+ )
329
+ ] }),
330
+ /* @__PURE__ */ jsxs("div", { className: "p-4", children: [
331
+ state.status === "loading" ? /* @__PURE__ */ jsx2("p", { "data-testid": "team-loading", className: "py-6 text-center text-sm text-muted-foreground", children: "Loading team\u2026" }) : null,
332
+ state.status === "error" ? /* @__PURE__ */ jsxs("div", { role: "alert", "data-testid": "team-load-error", className: "space-y-3 py-4 text-center", children: [
333
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-destructive", children: [
334
+ "Couldn't load this team: ",
335
+ state.message
336
+ ] }),
337
+ /* @__PURE__ */ jsx2(
338
+ "button",
339
+ {
340
+ type: "button",
341
+ className: BUTTON_OUTLINE,
342
+ onClick: () => {
343
+ setState({ status: "loading" });
344
+ void load();
345
+ },
346
+ children: "Retry"
347
+ }
348
+ )
349
+ ] }) : null,
350
+ ready && ready.members.length === 0 ? /* @__PURE__ */ jsx2("p", { className: "py-6 text-center text-sm text-muted-foreground", children: "No members yet \u2014 invite someone to get started." }) : null,
351
+ ready && ready.members.length > 0 ? /* @__PURE__ */ jsx2("ul", { className: "divide-y divide-border", children: ready.members.map((member) => {
352
+ const name = nameFromEmail(member.email);
353
+ const isMe = me !== null && member.email?.toLowerCase() === me;
354
+ const isOwner = member.role === "owner";
355
+ const rowBusy = Boolean(busy[`member:${member.id}`]);
356
+ const pickerRoles = roleOptions.includes(member.role) ? roleOptions : [member.role, ...roleOptions];
357
+ return /* @__PURE__ */ jsxs(
358
+ "li",
359
+ {
360
+ "data-testid": `member-row-${member.id}`,
361
+ className: "flex flex-wrap items-center justify-between gap-3 py-3",
362
+ children: [
363
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [
364
+ /* @__PURE__ */ jsx2(
365
+ "span",
366
+ {
367
+ "aria-hidden": "true",
368
+ className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/15 text-xs font-medium text-foreground",
369
+ children: name.slice(0, 2).toUpperCase()
370
+ }
371
+ ),
372
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
373
+ /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 text-sm font-medium", children: [
374
+ /* @__PURE__ */ jsx2("span", { className: "truncate", children: name }),
375
+ isMe ? /* @__PURE__ */ jsx2("span", { className: "rounded-full bg-primary/15 px-1.5 py-0.5 text-[10px] text-foreground", children: "you" }) : null
376
+ ] }),
377
+ /* @__PURE__ */ jsxs("p", { className: "truncate text-xs text-muted-foreground", children: [
378
+ member.email ?? "No email on record",
379
+ " \xB7 joined ",
380
+ formatDate(member.created_at)
381
+ ] })
382
+ ] })
383
+ ] }),
384
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 flex-wrap items-center gap-2", children: [
385
+ isOwner ? /* @__PURE__ */ jsx2("span", { className: roleBadgeClass(member.role), children: roleLabel(member.role) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
386
+ /* @__PURE__ */ jsx2(
387
+ "select",
388
+ {
389
+ "aria-label": `Role for ${name}`,
390
+ className: `${CONTROL} px-2 py-1`,
391
+ value: member.role,
392
+ disabled: rowBusy,
393
+ onChange: (e) => void handleRoleChange(member, e.target.value),
394
+ children: pickerRoles.map((role) => /* @__PURE__ */ jsx2("option", { value: role, children: roleLabel(role) }, role))
395
+ }
396
+ ),
397
+ /* @__PURE__ */ jsx2(
398
+ "button",
399
+ {
400
+ type: "button",
401
+ className: BUTTON_GHOST,
402
+ disabled: rowBusy,
403
+ onClick: () => setMemberToRemove(member),
404
+ children: "Remove"
405
+ }
406
+ )
407
+ ] }),
408
+ renderMemberActions?.(member)
409
+ ] })
410
+ ]
411
+ },
412
+ member.id
413
+ );
414
+ }) }) : null
415
+ ] })
416
+ ]
417
+ }
418
+ ),
419
+ ready && ready.invitations.length > 0 ? /* @__PURE__ */ jsxs(
420
+ "section",
421
+ {
422
+ "aria-labelledby": `${ids}-invites-title`,
423
+ className: "rounded-lg border border-border bg-card text-card-foreground",
424
+ children: [
425
+ /* @__PURE__ */ jsx2("header", { className: "border-b border-border p-4", children: /* @__PURE__ */ jsxs("h3", { id: `${ids}-invites-title`, className: "text-base font-semibold", children: [
426
+ "Pending Invitations (",
427
+ ready.invitations.length,
428
+ ")"
429
+ ] }) }),
430
+ /* @__PURE__ */ jsx2("ul", { className: "divide-y divide-border p-4", children: ready.invitations.map((inv) => {
431
+ const invBusy = Boolean(busy[`inv:${inv.id}`]);
432
+ return /* @__PURE__ */ jsxs(
433
+ "li",
434
+ {
435
+ "data-testid": `invitation-row-${inv.id}`,
436
+ className: "flex flex-wrap items-center justify-between gap-3 py-3",
437
+ children: [
438
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
439
+ /* @__PURE__ */ jsx2("p", { className: "truncate text-sm font-medium", children: inv.email }),
440
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
441
+ "invited ",
442
+ formatAgo(inv.created_at),
443
+ " by ",
444
+ inv.invited_by_email ?? "a team member",
445
+ " \xB7 expires ",
446
+ formatDate(inv.expires_at)
447
+ ] })
448
+ ] }),
449
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
450
+ /* @__PURE__ */ jsx2("span", { className: roleBadgeClass(inv.role), children: roleLabel(inv.role) }),
451
+ /* @__PURE__ */ jsx2(
452
+ "button",
453
+ {
454
+ type: "button",
455
+ className: BUTTON_GHOST,
456
+ disabled: invBusy,
457
+ "aria-label": `Resend invitation to ${inv.email}`,
458
+ onClick: () => void handleResend(inv),
459
+ children: "Resend"
460
+ }
461
+ ),
462
+ /* @__PURE__ */ jsx2(
463
+ "button",
464
+ {
465
+ type: "button",
466
+ className: BUTTON_GHOST,
467
+ disabled: invBusy,
468
+ "aria-label": `Revoke invitation to ${inv.email}`,
469
+ onClick: () => void handleRevoke(inv),
470
+ children: "Revoke"
471
+ }
472
+ )
473
+ ] })
474
+ ]
475
+ },
476
+ inv.id
477
+ );
478
+ }) })
479
+ ]
480
+ }
481
+ ) : null,
482
+ /* @__PURE__ */ jsx2(
483
+ Modal,
484
+ {
485
+ open: inviteOpen,
486
+ onClose: () => setInviteOpen(false),
487
+ titleId: inviteTitleId,
488
+ descriptionId: inviteDescId,
489
+ dismissible: !inviteSubmitting,
490
+ children: /* @__PURE__ */ jsxs(
491
+ "form",
492
+ {
493
+ className: "space-y-4",
494
+ onSubmit: (e) => {
495
+ e.preventDefault();
496
+ void handleInvite();
497
+ },
498
+ children: [
499
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
500
+ /* @__PURE__ */ jsx2("h2", { id: inviteTitleId, className: "text-lg font-semibold", children: "Invite a Team Member" }),
501
+ /* @__PURE__ */ jsx2("p", { id: inviteDescId, className: "text-sm text-muted-foreground", children: "Send an invitation to join your team. They'll receive an email with a magic link." })
502
+ ] }),
503
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
504
+ /* @__PURE__ */ jsx2("label", { htmlFor: `${ids}-invite-email`, className: "text-sm font-medium", children: "Email Address" }),
505
+ /* @__PURE__ */ jsx2(
506
+ "input",
507
+ {
508
+ id: `${ids}-invite-email`,
509
+ type: "email",
510
+ placeholder: "teammate@company.com",
511
+ className: FIELD,
512
+ value: inviteEmail,
513
+ disabled: inviteSubmitting,
514
+ onChange: (e) => setInviteEmail(e.target.value)
515
+ }
516
+ )
517
+ ] }),
518
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
519
+ /* @__PURE__ */ jsx2("label", { htmlFor: `${ids}-invite-role`, className: "text-sm font-medium", children: "Role" }),
520
+ /* @__PURE__ */ jsx2(
521
+ "select",
522
+ {
523
+ id: `${ids}-invite-role`,
524
+ className: FIELD,
525
+ value: inviteRole,
526
+ disabled: inviteSubmitting,
527
+ onChange: (e) => setInviteRole(e.target.value),
528
+ children: roleOptions.map((role) => /* @__PURE__ */ jsx2("option", { value: role, children: INVITE_ROLE_OPTIONS[role].label }, role))
529
+ }
530
+ ),
531
+ /* @__PURE__ */ jsx2("p", { className: "text-xs text-muted-foreground", children: INVITE_ROLE_OPTIONS[inviteRole]?.description })
532
+ ] }),
533
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
534
+ /* @__PURE__ */ jsx2(
535
+ "button",
536
+ {
537
+ type: "button",
538
+ className: BUTTON_OUTLINE,
539
+ disabled: inviteSubmitting,
540
+ onClick: () => setInviteOpen(false),
541
+ children: "Cancel"
542
+ }
543
+ ),
544
+ /* @__PURE__ */ jsx2(
545
+ "button",
546
+ {
547
+ type: "submit",
548
+ className: BUTTON_PRIMARY,
549
+ disabled: !inviteEmail.trim() || inviteSubmitting,
550
+ children: inviteSubmitting ? "Sending\u2026" : "Send Invitation"
551
+ }
552
+ )
553
+ ] })
554
+ ]
555
+ }
556
+ )
557
+ }
558
+ ),
559
+ /* @__PURE__ */ jsx2(
560
+ Modal,
561
+ {
562
+ open: memberToRemove !== null,
563
+ onClose: () => setMemberToRemove(null),
564
+ titleId: removeTitleId,
565
+ descriptionId: removeDescId,
566
+ dismissible: !removeBusy,
567
+ children: /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
568
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
569
+ /* @__PURE__ */ jsx2("h2", { id: removeTitleId, className: "text-lg font-semibold", children: "Remove Team Member" }),
570
+ /* @__PURE__ */ jsxs("p", { id: removeDescId, className: "text-sm text-muted-foreground", children: [
571
+ "Are you sure you want to remove",
572
+ " ",
573
+ /* @__PURE__ */ jsx2("span", { className: "font-medium text-foreground", children: memberToRemove ? nameFromEmail(memberToRemove.email) : "" }),
574
+ " ",
575
+ "from the team? They will lose access to all team resources."
576
+ ] })
577
+ ] }),
578
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
579
+ /* @__PURE__ */ jsx2(
580
+ "button",
581
+ {
582
+ type: "button",
583
+ className: BUTTON_OUTLINE,
584
+ disabled: removeBusy,
585
+ onClick: () => setMemberToRemove(null),
586
+ children: "Cancel"
587
+ }
588
+ ),
589
+ /* @__PURE__ */ jsx2(
590
+ "button",
591
+ {
592
+ type: "button",
593
+ className: BUTTON_DESTRUCTIVE,
594
+ disabled: removeBusy,
595
+ onClick: () => void handleRemove(),
596
+ children: "Remove Member"
597
+ }
598
+ )
599
+ ] })
600
+ ] })
601
+ }
602
+ )
603
+ ] });
604
+ }
605
+ export {
606
+ TeamMembersPanel
607
+ };