'use client' /** * InviteMemberDialog — collects email + role, calls the supplied submit * handler (defaults to a no-op so the caller MUST wire it; in practice the * caller passes api.teamInvitations.create or useInvitations().bulkInvite). * * Self-contained modal — uses a fixed overlay, doesn't pull Radix Dialog, * so the test harness can render the open state straightforwardly. startsim-o7s. */ import * as React from 'react' import { RoleSelector } from './RoleSelector' import type { InvitationLite, TeamRole } from './types' import { INVITE_DIALOG_DEFAULTS, type InviteMemberDialogClassNames, } from './invite-member-dialog-default-class-names' export interface InviteMemberDialogProps { /** Team the invitation will be scoped to. */ teamId: string /** * Submit hook. When undefined the dialog won't send anything — useful for * Storybook-style harnesses. Apps typically pass * (input) => api.teamInvitations.create(input) */ onSubmit?: (input: { email: string; teamId: string; role: TeamRole }) => Promise /** Notification when an invitation has been created. */ onInvited?: (invitation: InvitationLite | void) => void /** Trigger button label. */ triggerLabel?: string /** Title in the modal header. */ title?: string /** Submit button label. Override when the backend adds directly rather than * emailing an invite (e.g. Foundry: "Add member"). */ submitLabel?: string /** Confirmation line after a successful submit. Receives the email so the * caller can match its own semantics ("Added ada@x to this foundry"). */ successMessage?: (email: string) => string /** Default role pre-selected. */ defaultRole?: TeamRole /** Available roles in the dropdown. Defaults to admin/member/viewer (no owner). */ availableRoles?: TeamRole[] classNames?: InviteMemberDialogClassNames /** External open-state control. When omitted the trigger button manages it. */ open?: boolean onOpenChange?: (open: boolean) => void } export function InviteMemberDialog({ teamId, onSubmit, onInvited, triggerLabel = 'Invite member', title = 'Invite a teammate', submitLabel = 'Send invitation', successMessage = (email) => `Invitation sent to ${email}`, defaultRole = 'member', availableRoles = ['admin', 'member', 'viewer'], classNames, open: controlledOpen, onOpenChange, }: InviteMemberDialogProps) { const cls = { ...INVITE_DIALOG_DEFAULTS, ...(classNames ?? {}) } const [internalOpen, setInternalOpen] = React.useState(false) const open = controlledOpen ?? internalOpen const setOpen = (next: boolean) => { if (controlledOpen === undefined) setInternalOpen(next) onOpenChange?.(next) } const [email, setEmail] = React.useState('') const [role, setRole] = React.useState(defaultRole) const [error, setError] = React.useState('') const [success, setSuccess] = React.useState('') const [submitting, setSubmitting] = React.useState(false) function reset() { setEmail('') setRole(defaultRole) setError('') setSuccess('') setSubmitting(false) } function close() { setOpen(false) reset() } async function handleSubmit(e: React.FormEvent) { e.preventDefault() setError('') setSuccess('') if (!email.trim()) { setError('Email is required') return } setSubmitting(true) try { let invited: InvitationLite | void = undefined if (onSubmit) { invited = await onSubmit({ email: email.trim(), teamId, role }) } setSuccess(successMessage(email.trim())) setEmail('') onInvited?.(invited) } catch (err) { setError(err instanceof Error ? err.message : 'Could not send invitation') } finally { setSubmitting(false) } } return ( <> {open && (
e.stopPropagation()} >

{title}

setEmail(e.target.value)} required autoComplete="email" className={cls.input} disabled={submitting} />
{error &&

{error}

} {success &&

{success}

}
)} ) }