From 984eace2f29461806adfe6662dfbd74ade060eea Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Thu, 21 May 2026 14:39:33 +0530 Subject: [PATCH 1/6] add team management in nova account settings --- apps/web/components/settings/account.tsx | 545 +++++++++++++++++++---- packages/lib/auth-context.tsx | 9 + 2 files changed, 469 insertions(+), 85 deletions(-) diff --git a/apps/web/components/settings/account.tsx b/apps/web/components/settings/account.tsx index 57d1264b1..aaa2319c4 100644 --- a/apps/web/components/settings/account.tsx +++ b/apps/web/components/settings/account.tsx @@ -3,6 +3,7 @@ import { dmSans125ClassName } from "@/lib/fonts" import { cn } from "@lib/utils" import { useAuth } from "@lib/auth-context" +import { authClient } from "@lib/auth" import { useOrgSummaries } from "@/hooks/use-org-summaries" import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar" import { @@ -12,9 +13,35 @@ import { type PlanType, } from "@/hooks/use-token-usage" import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@ui/components/select" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@ui/components/dropdown-menu" import { useCustomer } from "autumn-js/react" -import { Check, LoaderIcon, ChevronDown, Building2, Users } from "lucide-react" +import { useMutation } from "@tanstack/react-query" +import { + Check, + LoaderIcon, + ChevronDown, + Building2, + Users, + UserPlus, + Mail, + MoreHorizontal, + UserMinus, + X, +} from "lucide-react" import { useMemo, useState } from "react" +import { toast } from "sonner" function SectionTitle({ children }: { children: React.ReactNode }) { return ( @@ -69,6 +96,21 @@ const ROLE_LABELS: Record = { member: "Member", } +type InviteRole = "admin" | "member" + +function getErrorMessage(error: unknown, fallback: string) { + if (error instanceof Error && error.message) return error.message + if ( + error && + typeof error === "object" && + "message" in error && + typeof error.message === "string" + ) { + return error.message + } + return fallback +} + function formatRole(role: string): string { const r = role?.toLowerCase() ?? "" if (ROLE_LABELS[r]) return ROLE_LABELS[r] @@ -93,6 +135,17 @@ function RolePill({ role }: { role: string }) { ) } +function isPendingInvitation(invitation: { + status?: string + expiresAt?: Date | string +}) { + if (invitation.status && invitation.status.toLowerCase() !== "pending") { + return false + } + if (!invitation.expiresAt) return true + return new Date(invitation.expiresAt).getTime() > Date.now() +} + function resolveOrgPlan( orgId: string, isCurrent: boolean, @@ -106,10 +159,18 @@ function resolveOrgPlan( } export default function Account() { - const { user, org, organizations: allOrgs, setActiveOrg } = useAuth() + const { + user, + org, + organizations: allOrgs, + setActiveOrg, + refetchActiveOrg, + } = useAuth() const autumn = useCustomer() const [switchingOrgId, setSwitchingOrgId] = useState(null) const [orgMenuOpen, setOrgMenuOpen] = useState(false) + const [inviteEmail, setInviteEmail] = useState("") + const [inviteRole, setInviteRole] = useState("member") const canSwitchOrg = (allOrgs?.length ?? 0) > 1 const { data: orgSummaries } = useOrgSummaries() @@ -127,6 +188,123 @@ export default function Account() { const { currentPlan } = useTokenUsage(autumn) + const currentMember = useMemo( + () => org?.members?.find((member) => member.userId === user?.id) ?? null, + [org?.members, user?.id], + ) + const currentRole = currentMember?.role?.toLowerCase() ?? "member" + const canManageTeam = currentRole === "owner" || currentRole === "admin" + const isOwner = currentRole === "owner" + + const pendingInvitations = useMemo( + () => (org?.invitations ?? []).filter(isPendingInvitation), + [org?.invitations], + ) + + const inviteMemberMutation = useMutation({ + mutationFn: async () => { + if (!org?.id) throw new Error("No active organization") + const email = inviteEmail.trim().toLowerCase() + if (!email) throw new Error("Enter an email address") + const result = await authClient.organization.inviteMember({ + email, + role: inviteRole, + organizationId: org.id, + resend: true, + }) + if (result.error) { + throw new Error(result.error.message ?? "Failed to invite teammate") + } + return result.data + }, + onSuccess: async (invitation) => { + setInviteEmail("") + await refetchActiveOrg() + toast.success("Invitation sent", { + description: invitation?.email + ? `${invitation.email} can now join ${org?.name ?? "your organization"}.` + : undefined, + }) + }, + onError: (error) => { + toast.error(getErrorMessage(error, "Failed to invite teammate")) + }, + }) + + const updateMemberRoleMutation = useMutation({ + mutationFn: async ({ + memberId, + role, + }: { + memberId: string + role: InviteRole + }) => { + if (!org?.id) throw new Error("No active organization") + const result = await authClient.organization.updateMemberRole({ + memberId, + role, + organizationId: org.id, + }) + if (result.error) { + throw new Error(result.error.message ?? "Failed to update role") + } + return result.data + }, + onSuccess: async () => { + await refetchActiveOrg() + toast.success("Role updated") + }, + onError: (error) => { + toast.error(getErrorMessage(error, "Failed to update role")) + }, + }) + + const removeMemberMutation = useMutation({ + mutationFn: async (memberIdOrEmail: string) => { + if (!org?.id) throw new Error("No active organization") + const result = await authClient.organization.removeMember({ + memberIdOrEmail, + organizationId: org.id, + }) + if (result.error) { + throw new Error(result.error.message ?? "Failed to remove member") + } + return result.data + }, + onSuccess: async () => { + await refetchActiveOrg() + toast.success("Member removed") + }, + onError: (error) => { + toast.error(getErrorMessage(error, "Failed to remove member")) + }, + }) + + const cancelInvitationMutation = useMutation({ + mutationFn: async (invitationId: string) => { + const result = await authClient.organization.cancelInvitation({ + invitationId, + }) + if (result.error) { + throw new Error(result.error.message ?? "Failed to cancel invitation") + } + return result.data + }, + onSuccess: async () => { + await refetchActiveOrg() + toast.success("Invitation canceled") + }, + onError: (error) => { + toast.error(getErrorMessage(error, "Failed to cancel invitation")) + }, + }) + + const handleInviteSubmit = (event: React.FormEvent) => { + event.preventDefault() + if (!canManageTeam || inviteMemberMutation.isPending) return + inviteMemberMutation.mutate() + } + const planByOrgId = useMemo(() => { const map = new Map() for (const summary of orgSummaries ?? []) { @@ -318,8 +496,19 @@ export default function Account() {
-
- Team members +
+
+ Team members +

+ Invite people into {org?.name ?? "your organization"} and manage + their access. +

+
{(org?.members?.length ?? 0) > 0 && ( - {org?.members && org.members.length > 0 ? ( -
    - {[...org.members] - .sort((a, b) => { - const rolePriority = (r: string) => - r === "owner" ? 0 : r === "admin" ? 1 : 2 - const diff = - rolePriority(a.role.toLowerCase()) - - rolePriority(b.role.toLowerCase()) - if (diff !== 0) return diff - return (a.user?.name ?? "").localeCompare(b.user?.name ?? "") - }) - .map((m, idx) => { - const isYou = m.userId === user?.id - const name = m.user?.name ?? m.user?.email ?? "Unknown" - return ( +
    + {canManageTeam ? ( +
    + +
    + + setInviteEmail(event.target.value)} + placeholder="teammate@company.com" + autoComplete="email" + className={cn( + dmSans125ClassName(), + "h-10 w-full rounded-[10px] border border-white/[0.08] bg-[#0D0F14] pl-9 pr-3 text-[14px] text-[#FAFAFA] placeholder:text-[#525D6E] outline-none transition-colors focus:border-[#4BA0FA]/50", + )} + /> +
    + + +
    + ) : ( +
    +
    + +
    +

    + Only organization owners and admins can invite teammates or + change roles. +

    +
    + )} + + {pendingInvitations.length > 0 && ( +
    +

    + Pending invitations +

    +
      + {pendingInvitations.map((invitation) => (
    • 0 && "border-t border-white/[0.04]", - )} + key={invitation.id} + className="flex items-center gap-3 px-3 py-2.5 border-t border-white/[0.04] first:border-t-0 bg-white/[0.015]" > - - - - {(name.charAt(0) || "U").toUpperCase()} - - -
      -
      - + +
      +
      +

      + {invitation.email} +

      +

      + Invited as {formatRole(invitation.role)} +

      +
      + {canManageTeam && ( +
      + +
      + )} +
    • + ))} +
    +
    + )} + + {org?.members && org.members.length > 0 ? ( +
      + {[...org.members] + .sort((a, b) => { + const rolePriority = (r: string) => + r === "owner" ? 0 : r === "admin" ? 1 : 2 + const diff = + rolePriority(a.role.toLowerCase()) - + rolePriority(b.role.toLowerCase()) + if (diff !== 0) return diff + return (a.user?.name ?? "").localeCompare( + b.user?.name ?? "", + ) + }) + .map((m, idx) => { + const isYou = m.userId === user?.id + const memberRole = m.role.toLowerCase() + const name = m.user?.name ?? m.user?.email ?? "Unknown" + const canEditMember = + canManageTeam && !isYou && memberRole !== "owner" + return ( +
    • 0 && "border-t border-white/[0.04]", + )} + > + + + + {(name.charAt(0) || "U").toUpperCase()} + + +
      +
      + + {name} + + {isYou && ( + + You + + )} +
      + {m.user?.email && ( - You + {m.user.email} )}
      - {m.user?.email && ( - { + if (value === memberRole) return + updateMemberRoleMutation.mutate({ + memberId: m.id, + role: value as InviteRole, + }) + }} > - {m.user.email} - + + + + + Member + Admin + + + ) : ( + )} -
    - - - ) - })} -
- ) : ( -
-
- -
-
- - Just you for now - - - Invite teammates from your organization settings. - + {canEditMember && ( + + + + + + + removeMemberMutation.mutate(m.id) + } + disabled={ + removeMemberMutation.isPending || !isOwner + } + > + + Remove member + + + + )} + + ) + })} + + ) : ( +
+
+ +
+
+ + Just you for now + + + Invite teammates to start collaborating. + +
-
- )} + )} +
diff --git a/packages/lib/auth-context.tsx b/packages/lib/auth-context.tsx index 838a74e61..485628131 100644 --- a/packages/lib/auth-context.tsx +++ b/packages/lib/auth-context.tsx @@ -28,6 +28,7 @@ interface AuthContextType { setActiveOrg: (orgSlug: string) => Promise clearActiveOrg: () => void updateOrgMetadata: (partial: Record) => void + refetchActiveOrg: () => Promise refetchOrganizations: () => Promise } @@ -81,6 +82,13 @@ export function AuthProvider({ children }: { children: ReactNode }) { }) }, []) + const refetchActiveOrg = useCallback(async () => { + const full = await authClient.organization.getFullOrganization() + const nextOrg = full?.data ?? null + setOrg(nextOrg) + return nextOrg + }, []) + useEffect(() => { if (isSessionPending) return @@ -198,6 +206,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { setActiveOrg, clearActiveOrg, updateOrgMetadata, + refetchActiveOrg, refetchOrganizations, }} > From 730001cfa9168fb6c07fd3c6940be302e10f53aa Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Thu, 21 May 2026 15:39:12 +0530 Subject: [PATCH 2/6] check admin settings --- apps/web/components/settings/account.tsx | 29 ++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/apps/web/components/settings/account.tsx b/apps/web/components/settings/account.tsx index aaa2319c4..31768b9a0 100644 --- a/apps/web/components/settings/account.tsx +++ b/apps/web/components/settings/account.tsx @@ -27,7 +27,7 @@ import { DropdownMenuTrigger, } from "@ui/components/dropdown-menu" import { useCustomer } from "autumn-js/react" -import { useMutation } from "@tanstack/react-query" +import { useMutation, useQuery } from "@tanstack/react-query" import { Check, LoaderIcon, @@ -188,11 +188,36 @@ export default function Account() { const { currentPlan } = useTokenUsage(autumn) + const activeMemberRoleQuery = useQuery({ + queryKey: ["organization", org?.id, "active-member-role"], + queryFn: async () => { + if (!org?.id) return null + const result = await authClient.organization.getActiveMemberRole({ + query: { organizationId: org.id }, + }) + if (result.error) { + throw new Error(result.error.message ?? "Failed to load team role") + } + return result.data?.role ?? null + }, + enabled: !!org?.id, + retry: false, + }) + const currentMember = useMemo( () => org?.members?.find((member) => member.userId === user?.id) ?? null, [org?.members, user?.id], ) - const currentRole = currentMember?.role?.toLowerCase() ?? "member" + const isSingleMemberPersonalOrg = + (org?.members?.length ?? 0) <= 1 && + (!org?.members?.[0]?.userId || org.members[0].userId === user?.id) + const currentRole = isSingleMemberPersonalOrg + ? "owner" + : ( + activeMemberRoleQuery.data ?? + currentMember?.role ?? + "member" + ).toLowerCase() const canManageTeam = currentRole === "owner" || currentRole === "admin" const isOwner = currentRole === "owner" From ebd99b5d68e352b66c88e253619441d2d9143b6d Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Fri, 22 May 2026 13:08:45 +0530 Subject: [PATCH 3/6] implement invite modal in Nova team settings --- apps/web/components/settings/account.tsx | 300 +++++++++++++++++++---- 1 file changed, 254 insertions(+), 46 deletions(-) diff --git a/apps/web/components/settings/account.tsx b/apps/web/components/settings/account.tsx index 31768b9a0..96cf4baac 100644 --- a/apps/web/components/settings/account.tsx +++ b/apps/web/components/settings/account.tsx @@ -26,6 +26,12 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@ui/components/dropdown-menu" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@ui/components/dialog" import { useCustomer } from "autumn-js/react" import { useMutation, useQuery } from "@tanstack/react-query" import { @@ -38,6 +44,7 @@ import { Mail, MoreHorizontal, UserMinus, + ShieldCheck, X, } from "lucide-react" import { useMemo, useState } from "react" @@ -98,6 +105,27 @@ const ROLE_LABELS: Record = { type InviteRole = "admin" | "member" +const INVITE_PERMISSION_OPTIONS: Record< + InviteRole, + { title: string; description: string; permissions: string[] } +> = { + member: { + title: "Member access", + description: "Use the organization workspace with standard access.", + permissions: ["Read organization access", "Use shared memories"], + }, + admin: { + title: "Admin access", + description: "Manage teammates and organization-level team settings.", + permissions: [ + "Invite and cancel invitations", + "Change member roles", + "Remove members", + "Update organization settings", + ], + }, +} + function getErrorMessage(error: unknown, fallback: string) { if (error instanceof Error && error.message) return error.message if ( @@ -169,6 +197,7 @@ export default function Account() { const autumn = useCustomer() const [switchingOrgId, setSwitchingOrgId] = useState(null) const [orgMenuOpen, setOrgMenuOpen] = useState(false) + const [inviteDialogOpen, setInviteDialogOpen] = useState(false) const [inviteEmail, setInviteEmail] = useState("") const [inviteRole, setInviteRole] = useState("member") const canSwitchOrg = (allOrgs?.length ?? 0) > 1 @@ -244,6 +273,8 @@ export default function Account() { }, onSuccess: async (invitation) => { setInviteEmail("") + setInviteRole("member") + setInviteDialogOpen(false) await refetchActiveOrg() toast.success("Invitation sent", { description: invitation?.email @@ -549,60 +580,43 @@ export default function Account() {
{canManageTeam ? ( -
- -
- - setInviteEmail(event.target.value)} - placeholder="teammate@company.com" - autoComplete="email" - className={cn( - dmSans125ClassName(), - "h-10 w-full rounded-[10px] border border-white/[0.08] bg-[#0D0F14] pl-9 pr-3 text-[14px] text-[#FAFAFA] placeholder:text-[#525D6E] outline-none transition-colors focus:border-[#4BA0FA]/50", - )} - /> +
+
+
+ +
+
+

+ Invite teammate +

+

+ Choose role and permission preset before sending. +

+
- - +
) : (
@@ -830,6 +844,200 @@ export default function Account() {
+ + { + setInviteDialogOpen(open) + if (!open && !inviteMemberMutation.isPending) { + setInviteEmail("") + setInviteRole("member") + } + }} + > + + + + Invite teammate + + +
+
+ +
+ + setInviteEmail(event.target.value)} + placeholder="teammate@company.com" + autoComplete="email" + className={cn( + dmSans125ClassName(), + "h-10 w-full rounded-[10px] border border-white/[0.08] bg-[#0D0F14] pl-9 pr-3 text-[14px] text-[#FAFAFA] placeholder:text-[#525D6E] outline-none transition-colors focus:border-[#4BA0FA]/50", + )} + /> +
+
+ +
+

+ Role +

+
+ {(["member", "admin"] as const).map((role) => { + const selected = inviteRole === role + return ( + + ) + })} +
+
+ +
+

+ Permissions +

+
+ {(["member", "admin"] as const).map((role) => { + const option = INVITE_PERMISSION_OPTIONS[role] + const selected = inviteRole === role + return ( + + ) + })} +
+
+ +
+ + +
+
+
+
) } From bb068998ec0291f7ef8f6f186b642049aa8a30b3 Mon Sep 17 00:00:00 2001 From: ved015 Date: Sat, 23 May 2026 21:30:45 +0530 Subject: [PATCH 4/6] fix invite team member modal --- apps/web/components/settings/account.tsx | 469 +++++++++++++++-------- packages/ui/components/dialog.tsx | 2 +- 2 files changed, 316 insertions(+), 155 deletions(-) diff --git a/apps/web/components/settings/account.tsx b/apps/web/components/settings/account.tsx index 96cf4baac..b08dac713 100644 --- a/apps/web/components/settings/account.tsx +++ b/apps/web/components/settings/account.tsx @@ -44,11 +44,14 @@ import { Mail, MoreHorizontal, UserMinus, - ShieldCheck, X, + Tag, + Plus, } from "lucide-react" -import { useMemo, useState } from "react" +import { useMemo, useRef, useState } from "react" import { toast } from "sonner" +import { useContainerTags } from "@/hooks/use-container-tags" +import { PopoverAnchor } from "@ui/components/popover" function SectionTitle({ children }: { children: React.ReactNode }) { return ( @@ -105,26 +108,6 @@ const ROLE_LABELS: Record = { type InviteRole = "admin" | "member" -const INVITE_PERMISSION_OPTIONS: Record< - InviteRole, - { title: string; description: string; permissions: string[] } -> = { - member: { - title: "Member access", - description: "Use the organization workspace with standard access.", - permissions: ["Read organization access", "Use shared memories"], - }, - admin: { - title: "Admin access", - description: "Manage teammates and organization-level team settings.", - permissions: [ - "Invite and cancel invitations", - "Change member roles", - "Remove members", - "Update organization settings", - ], - }, -} function getErrorMessage(error: unknown, fallback: string) { if (error instanceof Error && error.message) return error.message @@ -200,6 +183,23 @@ export default function Account() { const [inviteDialogOpen, setInviteDialogOpen] = useState(false) const [inviteEmail, setInviteEmail] = useState("") const [inviteRole, setInviteRole] = useState("member") + const [inviteAccessType, setInviteAccessType] = useState<"full" | "restricted">("full") + const [inviteAssignments, setInviteAssignments] = useState<{ containerTag: string; permission: "read" | "write" }[]>([]) + const [tagQuery, setTagQuery] = useState("") + const [tagDropdownOpen, setTagDropdownOpen] = useState(false) + const tagInputRef = useRef(null) + const tagAnchorRef = useRef(null) + const { allProjects: allContainerTags } = useContainerTags() + + const selectedTagSet = new Set(inviteAssignments.map((a) => a.containerTag)) + const filteredTags = useMemo(() => { + const available = (allContainerTags ?? []).map((t) => t.containerTag).filter((t) => !selectedTagSet.has(t)) + if (!tagQuery) return available + return available.filter((t) => t.toLowerCase().includes(tagQuery.toLowerCase())) + }, [allContainerTags, selectedTagSet, tagQuery]) + + const showAccessType = inviteRole === "member" + const showTagPicker = inviteRole === "member" && inviteAccessType === "restricted" const canSwitchOrg = (allOrgs?.length ?? 0) > 1 const { data: orgSummaries } = useOrgSummaries() @@ -255,16 +255,28 @@ export default function Account() { [org?.invitations], ) + const resetInviteForm = () => { + setInviteEmail("") + setInviteRole("member") + setInviteAccessType("full") + setInviteAssignments([]) + setTagQuery("") + } + const inviteMemberMutation = useMutation({ mutationFn: async () => { if (!org?.id) throw new Error("No active organization") const email = inviteEmail.trim().toLowerCase() if (!email) throw new Error("Enter an email address") + const isRestricted = inviteRole === "member" && inviteAccessType === "restricted" const result = await authClient.organization.inviteMember({ email, role: inviteRole, organizationId: org.id, resend: true, + ...(isRestricted && inviteAssignments.length > 0 + ? { data: { accessType: "restricted", containerTags: inviteAssignments } } + : {}), }) if (result.error) { throw new Error(result.error.message ?? "Failed to invite teammate") @@ -272,8 +284,7 @@ export default function Account() { return result.data }, onSuccess: async (invitation) => { - setInviteEmail("") - setInviteRole("member") + resetInviteForm() setInviteDialogOpen(false) await refetchActiveOrg() toast.success("Invitation sent", { @@ -610,10 +621,10 @@ export default function Account() { disabled={!org?.id} className={cn( dmSans125ClassName(), - "inline-flex h-10 items-center justify-center gap-2 rounded-[10px] bg-[#4BA0FA] px-4 text-[14px] font-semibold text-[#00171A] transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-45", + "inline-flex h-9 items-center justify-center gap-2 rounded-[10px] border border-[#2261CA33] bg-[#00173C] px-4 text-[13px] font-semibold text-white transition-colors hover:bg-[#002654] disabled:cursor-not-allowed disabled:opacity-45", )} > - + Invite member
@@ -850,167 +861,316 @@ export default function Account() { onOpenChange={(open) => { setInviteDialogOpen(open) if (!open && !inviteMemberMutation.isPending) { - setInviteEmail("") - setInviteRole("member") + resetInviteForm() } }} > - - - - Invite teammate - - -
-
-
+ )}
-
+
+ ))} + {tagQuery.trim().length > 0 && + !selectedTagSet.has(tagQuery.trim()) && + !(allContainerTags ?? []).some( + (t) => + t.containerTag.toLowerCase() === + tagQuery.trim().toLowerCase(), + ) && ( + + )} + + - 0 || (tagQuery.trim().length > 0 && !selectedTagSet.has(tagQuery.trim())))} - onOpenChange={setTagDropdownOpen} - > - -
0 && ( +
+ {inviteAssignments.map((a) => ( +
+ + - { - setTagQuery(e.target.value) - if (!tagDropdownOpen) setTagDropdownOpen(true) - }} - onClick={() => setTagDropdownOpen(true)} - onFocus={() => setTagDropdownOpen(true)} - placeholder="Search or create spaces..." - className={cn( - dmSans125ClassName(), - "h-full w-full bg-transparent pl-3 pr-8 text-[13px] text-[#FAFAFA] placeholder:text-[#525D6E] outline-none", - )} - /> - + {a.containerTag} + +
+ {(["read", "write"] as const).map((perm) => { + const active = a.permission === perm + return ( + + ) + })}
- - e.preventDefault()} - onPointerDownOutside={(e) => { - if (tagAnchorRef.current?.contains(e.target as Node)) { - e.preventDefault() - } - }} - > - {filteredTags.map((tag) => ( - - ))} - {tagQuery.trim().length > 0 && !selectedTagSet.has(tagQuery.trim()) && !(allContainerTags ?? []).some((t) => t.containerTag.toLowerCase() === tagQuery.trim().toLowerCase()) && ( - - )} - - - - {/* Selected tags */} - {inviteAssignments.length > 0 && ( -
- {inviteAssignments.map((a) => ( -
- - - {a.containerTag} - -
- {(["read", "write"] as const).map((perm) => { - const active = a.permission === perm - return ( - - ) - })} -
-
- ))}
- )} + ))} +
+ )} - {inviteAssignments.length === 0 && ( -

- Select at least one space -

+ {inviteAssignments.length === 0 && ( +

+ Select at least one space +

+ )}
)}
From a4601f118b3841aa279467cea29bc878046d581c Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommu Date: Sun, 24 May 2026 13:16:48 -0700 Subject: [PATCH 6/6] refactor(settings): align invite teammate modal with select-space house style - match modal surface to select-spaces-modal (bg-#1B1F24, border-none, rounded-[22px], circular close) - replace role chips with the same Select used in member rows - move invite trigger to a button beside the Team members title; drop the standalone card - swap navy fills for the neutral bg-#14161A + shadow-inside-out primary style --- apps/web/components/settings/account.tsx | 172 ++++++++++------------- 1 file changed, 75 insertions(+), 97 deletions(-) diff --git a/apps/web/components/settings/account.tsx b/apps/web/components/settings/account.tsx index a27b39a53..5ec84e597 100644 --- a/apps/web/components/settings/account.tsx +++ b/apps/web/components/settings/account.tsx @@ -26,12 +26,8 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@ui/components/dropdown-menu" -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@ui/components/dialog" +import { Dialog, DialogContent, DialogTitle } from "@ui/components/dialog" +import * as DialogPrimitive from "@radix-ui/react-dialog" import { useCustomer } from "autumn-js/react" import { useMutation, useQuery } from "@tanstack/react-query" import { @@ -590,59 +586,37 @@ export default function Account() { their access.

- {(org?.members?.length ?? 0) > 0 && ( - - {org?.members?.length}{" "} - {org?.members?.length === 1 ? "member" : "members"} - - )} +
+ {(org?.members?.length ?? 0) > 0 && ( + + {org?.members?.length}{" "} + {org?.members?.length === 1 ? "member" : "members"} + + )} + {canManageTeam && ( + + )} +
- {canManageTeam ? ( -
-
-
- -
-
-

- Invite teammate -

-

- Choose role and permission preset before sending. -

-
-
- -
- ) : ( + {!canManageTeam && (
@@ -879,13 +853,16 @@ export default function Account() { } }} > - -
- + +
+
Invite teammate @@ -893,12 +870,21 @@ export default function Account() {

Send an invitation to join your organization.

- +
+ + + Close +
@@ -931,7 +917,7 @@ export default function Account() {
- {/* Role chips */} + {/* Role */}

Role

-
- {(["member", "admin"] as const).map((role) => { - const selected = inviteRole === role - return ( - - ) - })} -
+
{/* Access type (only for Member) */} @@ -996,8 +974,8 @@ export default function Account() { dmSans125ClassName(), "flex items-center justify-center h-9 rounded-[10px] border text-[13px] font-medium transition-colors cursor-pointer", selected - ? "border-[#2261CA33] bg-[#00173C] text-white" - : "border-[#161F2C] bg-[#0D121A] text-[#737373] hover:bg-[#00173C] hover:border-[#2261CA33]", + ? "border-white/10 bg-[#14161A] text-[#FAFAFA] shadow-inside-out" + : "border-[#161F2C] bg-[#0D121A] text-[#737373] hover:bg-[#14161A] hover:text-[#FAFAFA]", )} > {type === "full" ? "Full Access" : "Restricted"} @@ -1182,8 +1160,8 @@ export default function Account() { dmSans125ClassName(), "h-7 px-3 rounded-[8px] border text-[12px] font-medium transition-colors cursor-pointer capitalize", active - ? "border-[#2261CA33] bg-[#00173C] text-white" - : "border-[#161F2C] bg-[#0D121A] text-[#737373] hover:bg-[#00173C] hover:border-[#2261CA33]", + ? "border-white/10 bg-[#14161A] text-[#FAFAFA] shadow-inside-out" + : "border-[#161F2C] bg-[#0D121A] text-[#737373] hover:bg-[#14161A] hover:text-[#FAFAFA]", )} > {perm} @@ -1210,13 +1188,13 @@ export default function Account() { )}
-
+