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