mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-07-09 17:18:51 +00:00
Fix settings organization flows (#1159)
## Summary - Fix delete-organization dialog focus when switching orgs inside Settings. - Restyle delete-organization modal to match the app modal theme and remove extra organization icons. - Make the organization switcher list scrollable when many orgs exist. - Send Create organization directly to onboarding instead of opening the create-org modal. - Stop onboarding from completing when org creation fails, show an error toast, and return existing users to the dashboard.
This commit is contained in:
parent
f1ff7beb0f
commit
3a9310778a
5 changed files with 217 additions and 196 deletions
|
|
@ -39,6 +39,16 @@ const STORAGE_KEY = "supermemory-brain-onboarding-v1"
|
|||
const countsAsConnectedSource = (state: unknown) =>
|
||||
state === "connected" || state === "waitlist"
|
||||
|
||||
const getErrorMessage = (error: unknown, fallback: string) => {
|
||||
if (error instanceof Error && error.message) return error.message
|
||||
if (typeof error === "string" && error.trim()) return error
|
||||
if (typeof error === "object" && error !== null && "message" in error) {
|
||||
const message = (error as { message?: unknown }).message
|
||||
if (typeof message === "string" && message.trim()) return message
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
export default function BrainOnboardingPage() {
|
||||
const router = useRouter()
|
||||
const params = useSearchParams()
|
||||
|
|
@ -240,7 +250,12 @@ export default function BrainOnboardingPage() {
|
|||
slug,
|
||||
metadata,
|
||||
})
|
||||
await setActiveOrg(result.data?.slug ?? slug)
|
||||
if (result.error || !result.data?.slug) {
|
||||
throw new Error(
|
||||
getErrorMessage(result.error, "Organization was not created."),
|
||||
)
|
||||
}
|
||||
await setActiveOrg(result.data.slug)
|
||||
if (about.name.trim()) {
|
||||
await authClient.updateUser({
|
||||
name: about.name.trim(),
|
||||
|
|
@ -283,16 +298,22 @@ export default function BrainOnboardingPage() {
|
|||
await ensureOrg()
|
||||
goNext()
|
||||
} catch (e) {
|
||||
const message = getErrorMessage(e, "Organization was not created.")
|
||||
console.error("Failed to create organization:", e)
|
||||
analytics.onboardingWorkspaceCreateFailed({
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
error: message,
|
||||
})
|
||||
toast.error("Couldn't create your workspace. Please try again.")
|
||||
toast.error("Organization was not created", {
|
||||
description: "Please try again from Settings.",
|
||||
})
|
||||
if (forceCreate && (organizations?.length ?? 0) > 0) {
|
||||
router.replace("/")
|
||||
}
|
||||
} finally {
|
||||
creatingOrgRef.current = false
|
||||
setCreatingOrg(false)
|
||||
}
|
||||
}, [ensureOrg, goNext])
|
||||
}, [ensureOrg, goNext, forceCreate, organizations, router])
|
||||
|
||||
const [sendingInvites, setSendingInvites] = useState(false)
|
||||
const sendingInvitesRef = useRef(false)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { Logo } from "@ui/assets/Logo"
|
||||
import { useAuth } from "@lib/auth-context"
|
||||
import NovaOrb from "@/components/nova/nova-orb"
|
||||
import { useRef, useState } from "react"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { cn } from "@lib/utils"
|
||||
import { dmSansClassName, dmSans125ClassName } from "@/lib/fonts"
|
||||
import Account from "@/components/settings/account"
|
||||
|
|
@ -31,9 +31,15 @@ import {
|
|||
ChevronRight,
|
||||
ArrowUpRight,
|
||||
Building2,
|
||||
X,
|
||||
} from "lucide-react"
|
||||
import { authClient } from "@lib/auth"
|
||||
import { Dialog, DialogContent, DialogClose } from "@ui/components/dialog"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogClose,
|
||||
DialogTitle,
|
||||
} from "@ui/components/dialog"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover"
|
||||
import { useResetOrganization } from "@/hooks/use-reset-organization"
|
||||
import { useDeleteUserAccount } from "@/hooks/use-account-settings"
|
||||
|
|
@ -96,6 +102,11 @@ const NAV_ITEMS: NavItem[] = [
|
|||
},
|
||||
]
|
||||
|
||||
const MODAL_SURFACE_SHADOW =
|
||||
"0 2.842px 14.211px 0 rgba(0,0,0,0.25), 0.711px 0.711px 0.711px 0 rgba(255,255,255,0.10) inset"
|
||||
|
||||
const INSET_SHADOW = "inset 1.313px 1.313px 3.938px rgba(0,0,0,0.7)"
|
||||
|
||||
export function parseHashToTab(hash: string): SettingsTab {
|
||||
const cleaned = hash.replace("#", "").toLowerCase()
|
||||
return TABS.includes(cleaned as SettingsTab)
|
||||
|
|
@ -132,11 +143,13 @@ function IdentityCard({ displayName }: { displayName: string }) {
|
|||
export function SettingsContent({
|
||||
activeTab,
|
||||
onTabChange,
|
||||
dialogPortalContainer,
|
||||
className,
|
||||
showIdentity = true,
|
||||
}: {
|
||||
activeTab: SettingsTab
|
||||
onTabChange: (tab: SettingsTab) => void
|
||||
dialogPortalContainer?: HTMLElement | null
|
||||
className?: string
|
||||
showIdentity?: boolean
|
||||
}) {
|
||||
|
|
@ -156,7 +169,12 @@ export function SettingsContent({
|
|||
const [isDeleteOrgDialogOpen, setIsDeleteOrgDialogOpen] = useState(false)
|
||||
const [deleteOrgConfirm, setDeleteOrgConfirm] = useState("")
|
||||
const deleteOrgInputRef = useRef<HTMLInputElement>(null)
|
||||
const deleteOrgDialogTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null,
|
||||
)
|
||||
const deleteOrganization = useDeleteOrganization()
|
||||
const activeOrgId = org?.id
|
||||
const previousOrgIdRef = useRef(activeOrgId)
|
||||
|
||||
// Only owners can delete the organization.
|
||||
const activeMemberRoleQuery = useQuery({
|
||||
|
|
@ -175,11 +193,44 @@ export function SettingsContent({
|
|||
|
||||
const [dangerMenuOpen, setDangerMenuOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (previousOrgIdRef.current === activeOrgId) return
|
||||
previousOrgIdRef.current = activeOrgId
|
||||
setDangerMenuOpen(false)
|
||||
setIsDeleteOrgDialogOpen(false)
|
||||
setDeleteOrgConfirm("")
|
||||
}, [activeOrgId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDeleteOrgDialogOpen) return
|
||||
|
||||
document.body.style.pointerEvents = ""
|
||||
const focusTimer = setTimeout(() => {
|
||||
deleteOrgInputRef.current?.focus()
|
||||
}, 0)
|
||||
|
||||
return () => clearTimeout(focusTimer)
|
||||
}, [isDeleteOrgDialogOpen])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (deleteOrgDialogTimerRef.current) {
|
||||
clearTimeout(deleteOrgDialogTimerRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const openDeleteOrganizationDialog = () => {
|
||||
setDangerMenuOpen(false)
|
||||
window.requestAnimationFrame(() => {
|
||||
setDeleteOrgConfirm("")
|
||||
if (deleteOrgDialogTimerRef.current) {
|
||||
clearTimeout(deleteOrgDialogTimerRef.current)
|
||||
}
|
||||
deleteOrgDialogTimerRef.current = setTimeout(() => {
|
||||
document.body.style.pointerEvents = ""
|
||||
setIsDeleteOrgDialogOpen(true)
|
||||
})
|
||||
deleteOrgDialogTimerRef.current = null
|
||||
}, 120)
|
||||
}
|
||||
|
||||
const displayName =
|
||||
|
|
@ -610,6 +661,7 @@ export function SettingsContent({
|
|||
|
||||
{/* Delete organization dialog */}
|
||||
<Dialog
|
||||
key={activeOrgId ?? "delete-organization"}
|
||||
open={isDeleteOrgDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setIsDeleteOrgDialogOpen(open)
|
||||
|
|
@ -617,33 +669,58 @@ export function SettingsContent({
|
|||
}}
|
||||
>
|
||||
<DialogContent
|
||||
className="sm:max-w-md"
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"sm:max-w-[560px] border border-white/[0.12] bg-[#1B1F24] p-0 px-4 pt-4 pb-5 gap-0 rounded-[22px] overflow-hidden text-[#FAFAFA]",
|
||||
)}
|
||||
portalContainer={dialogPortalContainer}
|
||||
showCloseButton={false}
|
||||
style={{ boxShadow: MODAL_SURFACE_SHADOW }}
|
||||
onOpenAutoFocus={(event) => {
|
||||
event.preventDefault()
|
||||
deleteOrgInputRef.current?.focus()
|
||||
}}
|
||||
>
|
||||
<div className={cn("flex flex-col gap-5 p-1", dmSans125ClassName())}>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<h2 className="text-[18px] font-semibold text-[#FAFAFA]">
|
||||
<div className="flex shrink-0 items-center gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<DialogTitle className="text-[18px] font-semibold leading-tight text-[#FAFAFA]">
|
||||
Delete this organization?
|
||||
</h2>
|
||||
<p className="text-sm text-[#8B8B8B]">
|
||||
Permanently deletes{" "}
|
||||
<strong className="text-[#FAFAFA]">
|
||||
{org?.name || "this organization"}
|
||||
</strong>{" "}
|
||||
— its documents, spaces, connections, and members.{" "}
|
||||
<strong className="text-[#FAFAFA]">
|
||||
This cannot be undone.
|
||||
</strong>
|
||||
</DialogTitle>
|
||||
<p className="mt-0.5 truncate text-[13px] text-[#A1A1AA]">
|
||||
This action permanently removes the selected workspace.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm text-[#8B8B8B]">
|
||||
Type <strong className="text-[#FAFAFA]">{org?.name}</strong> to
|
||||
confirm:
|
||||
</p>
|
||||
<DialogClose asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close"
|
||||
className="flex size-9 shrink-0 items-center justify-center rounded-full bg-[#0D121A] transition-opacity hover:opacity-80 focus:outline-none"
|
||||
style={{ boxShadow: INSET_SHADOW }}
|
||||
>
|
||||
<X className="size-5 text-[#737373]" />
|
||||
</button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-col gap-4 rounded-[14px] bg-[#14161A] p-4 sm:p-5 shadow-[inset_1.313px_1.313px_3.938px_rgba(0,0,0,0.7)]">
|
||||
<p className="text-[13.5px] leading-relaxed text-[#A1A1AA]">
|
||||
Permanently deletes{" "}
|
||||
<strong className="font-semibold text-[#FAFAFA]">
|
||||
{org?.name || "this organization"}
|
||||
</strong>{" "}
|
||||
and all of its documents, spaces, connections, and members.{" "}
|
||||
<strong className="font-semibold text-[#FAFAFA]">
|
||||
This cannot be undone.
|
||||
</strong>
|
||||
</p>
|
||||
<label className="flex flex-col gap-2">
|
||||
<span className="text-[13px] text-[#A1A1AA]">
|
||||
Type{" "}
|
||||
<strong className="font-semibold text-[#FAFAFA]">
|
||||
{org?.name}
|
||||
</strong>{" "}
|
||||
to confirm
|
||||
</span>
|
||||
<input
|
||||
ref={deleteOrgInputRef}
|
||||
type="text"
|
||||
|
|
@ -651,43 +728,47 @@ export function SettingsContent({
|
|||
onChange={(e) => setDeleteOrgConfirm(e.target.value)}
|
||||
placeholder={org?.name ?? "Organization name"}
|
||||
autoComplete="off"
|
||||
className="w-full rounded-xl border border-[#2A2D35] bg-[#0D0F14] px-4 py-2.5 text-sm text-white placeholder:text-[#525D6E] focus:outline-none focus:border-[#C73B1B]/50 transition-colors"
|
||||
className="h-11 w-full rounded-[12px] border border-white/[0.08] bg-[#0D121A] px-4 text-[14px] text-white placeholder:text-[#525D6E] transition-colors focus:border-[#C73B1B]/55 focus:outline-none focus:ring-2 focus:ring-[#C73B1B]/15"
|
||||
style={{ boxShadow: INSET_SHADOW }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-3 justify-end">
|
||||
<DialogClose asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="px-4 py-2 rounded-full border border-[#2A2D35] text-sm text-[#8B8B8B] hover:text-white hover:border-[#3A3D45] transition-colors cursor-pointer"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</DialogClose>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex shrink-0 flex-wrap items-center justify-end gap-2">
|
||||
<DialogClose asChild>
|
||||
<button
|
||||
type="button"
|
||||
disabled={
|
||||
!org?.name ||
|
||||
deleteOrgConfirm !== org.name ||
|
||||
deleteOrganization.isPending
|
||||
}
|
||||
title={
|
||||
!org?.name || deleteOrgConfirm !== org.name
|
||||
? `Type "${org?.name ?? "the organization name"}" exactly to confirm`
|
||||
: undefined
|
||||
}
|
||||
onClick={handleDeleteOrganization}
|
||||
className="relative flex items-center gap-1.5 px-4 py-2 rounded-full text-sm font-medium cursor-pointer transition-opacity bg-[#290F0A] text-[#C73B1B] disabled:opacity-40 disabled:cursor-not-allowed hover:opacity-90"
|
||||
>
|
||||
{deleteOrganization.isPending ? (
|
||||
<LoaderIcon className="size-[15px] animate-spin" />
|
||||
) : (
|
||||
<Building2 className="size-[15px]" />
|
||||
className={cn(
|
||||
"px-3 py-2 text-[13px] font-medium text-[#737373] transition-colors hover:text-[#FAFAFA]",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
{deleteOrganization.isPending
|
||||
? "Deleting…"
|
||||
: "Delete organization"}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</DialogClose>
|
||||
<button
|
||||
type="button"
|
||||
disabled={
|
||||
!org?.name ||
|
||||
deleteOrgConfirm !== org.name ||
|
||||
deleteOrganization.isPending
|
||||
}
|
||||
title={
|
||||
!org?.name || deleteOrgConfirm !== org.name
|
||||
? `Type "${org?.name ?? "the organization name"}" exactly to confirm`
|
||||
: undefined
|
||||
}
|
||||
onClick={handleDeleteOrganization}
|
||||
className="flex h-10 shrink-0 items-center justify-center gap-2 rounded-full bg-[#0D121A] px-5 text-[14px] font-semibold text-[#FAFAFA] transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-45"
|
||||
style={{ boxShadow: INSET_SHADOW }}
|
||||
>
|
||||
{deleteOrganization.isPending ? (
|
||||
<LoaderIcon className="size-4 animate-spin text-[#C73B1B]" />
|
||||
) : null}
|
||||
{deleteOrganization.isPending
|
||||
? "Deleting..."
|
||||
: "Delete organization"}
|
||||
</button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react"
|
||||
import { useQueryState } from "nuqs"
|
||||
|
|
@ -50,6 +51,8 @@ const SettingsModalContext = createContext<SettingsModalContextValue | null>(
|
|||
|
||||
export function SettingsModalProvider({ children }: { children: ReactNode }) {
|
||||
const [param, setParam] = useQueryState(SETTINGS_PARAM)
|
||||
const [settingsDialogContent, setSettingsDialogContent] =
|
||||
useState<HTMLDivElement | null>(null)
|
||||
const open = param !== null
|
||||
const tab = parseTab(param)
|
||||
|
||||
|
|
@ -85,6 +88,7 @@ export function SettingsModalProvider({ children }: { children: ReactNode }) {
|
|||
}}
|
||||
>
|
||||
<DialogContent
|
||||
ref={setSettingsDialogContent}
|
||||
showCloseButton={false}
|
||||
style={{
|
||||
display: "flex",
|
||||
|
|
@ -122,6 +126,7 @@ export function SettingsModalProvider({ children }: { children: ReactNode }) {
|
|||
<SettingsContent
|
||||
activeTab={tab}
|
||||
onTabChange={handleTabChange}
|
||||
dialogPortalContainer={settingsDialogContent}
|
||||
showIdentity={false}
|
||||
className="flex-1 min-h-0 w-full overflow-y-auto md:overflow-hidden px-5 md:px-4 pt-4 pb-6"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useCustomer } from "autumn-js/react"
|
||||
import { toast } from "sonner"
|
||||
|
|
@ -12,17 +12,13 @@ import {
|
|||
Plus,
|
||||
} from "lucide-react"
|
||||
import { cn } from "@lib/utils"
|
||||
import { dmSansClassName, dmSans125ClassName } from "@/lib/fonts"
|
||||
import { dmSansClassName } from "@/lib/fonts"
|
||||
import { useAuth } from "@lib/auth-context"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover"
|
||||
import { Dialog, DialogContent, DialogTitle } from "@ui/components/dialog"
|
||||
import { OrgPlanBadge, resolveOrgPlan } from "@/components/org-plan-badge"
|
||||
import { useOrgSummaries } from "@/hooks/use-org-summaries"
|
||||
import { useTokenUsage, type PlanType } from "@/hooks/use-token-usage"
|
||||
|
||||
const SURFACE_SHADOW =
|
||||
"0 2.842px 14.211px 0 rgba(0,0,0,0.25), 0.711px 0.711px 0.711px 0 rgba(255,255,255,0.10) inset"
|
||||
|
||||
export function SettingsOrgSwitcher() {
|
||||
const { org, organizations, setActiveOrg } = useAuth()
|
||||
const router = useRouter()
|
||||
|
|
@ -32,15 +28,6 @@ export function SettingsOrgSwitcher() {
|
|||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [switchingId, setSwitchingId] = useState<string | null>(null)
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [createName, setCreateName] = useState("")
|
||||
const [creating, setCreating] = useState(false)
|
||||
|
||||
// Safety net: a prior Radix overlay can leave `pointer-events: none` stuck on
|
||||
// <body>, making the dialog appear but ignore clicks. Clear it when it opens.
|
||||
useEffect(() => {
|
||||
if (createOpen) document.body.style.pointerEvents = ""
|
||||
}, [createOpen])
|
||||
|
||||
const planByOrgId = useMemo(() => {
|
||||
const map = new Map<string, PlanType>()
|
||||
|
|
@ -77,50 +64,45 @@ export function SettingsOrgSwitcher() {
|
|||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
const name = createName.trim()
|
||||
if (!name || creating) return
|
||||
setCreating(true)
|
||||
// Org creation now happens through the onboarding flow (team/personal, name, invites).
|
||||
setCreateOpen(false)
|
||||
setOpen(false)
|
||||
router.push(`/onboarding?new=1&name=${encodeURIComponent(name)}`)
|
||||
router.push("/onboarding?new=1")
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"flex w-full items-center gap-2 rounded-[12px] border border-white/[0.06] bg-white/[0.03] px-2.5 py-2 transition-colors cursor-pointer hover:bg-white/[0.06]",
|
||||
)}
|
||||
>
|
||||
<span className="flex size-6 shrink-0 items-center justify-center rounded-full bg-white/[0.05] text-white/55">
|
||||
<Building2 className="size-[13px]" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate text-left text-[13px] font-medium text-white">
|
||||
{org?.name ?? "Personal"}
|
||||
</span>
|
||||
<OrgPlanBadge plan={activeOrgPlan} />
|
||||
<ChevronsUpDown className="size-3.5 shrink-0 text-white/40" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
side="bottom"
|
||||
sideOffset={6}
|
||||
style={{
|
||||
maxHeight:
|
||||
"min(360px, var(--radix-popover-content-available-height))",
|
||||
}}
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"w-[var(--radix-popover-trigger-width)] min-w-[220px] overflow-y-auto p-1.5",
|
||||
"bg-[#14161A] border-white/10 rounded-[14px]",
|
||||
"shadow-[0px_8px_28px_rgba(0,0,0,0.5)]",
|
||||
dmSansClassName(),
|
||||
"flex w-full items-center gap-2 rounded-[12px] border border-white/[0.06] bg-white/[0.03] px-2.5 py-2 transition-colors cursor-pointer hover:bg-white/[0.06]",
|
||||
)}
|
||||
>
|
||||
<span className="flex size-6 shrink-0 items-center justify-center rounded-full bg-white/[0.05] text-white/55">
|
||||
<Building2 className="size-[13px]" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate text-left text-[13px] font-medium text-white">
|
||||
{org?.name ?? "Personal"}
|
||||
</span>
|
||||
<OrgPlanBadge plan={activeOrgPlan} />
|
||||
<ChevronsUpDown className="size-3.5 shrink-0 text-white/40" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
side="bottom"
|
||||
sideOffset={6}
|
||||
className={cn(
|
||||
"w-[var(--radix-popover-trigger-width)] min-w-[220px] p-1.5",
|
||||
"bg-[#14161A] border-white/10 rounded-[14px]",
|
||||
"shadow-[0px_8px_28px_rgba(0,0,0,0.5)]",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="max-h-[360px] overflow-y-auto overscroll-contain pr-1 -mr-1 scrollbar-thin [scrollbar-gutter:stable]"
|
||||
onWheelCapture={(event) => event.stopPropagation()}
|
||||
onTouchMoveCapture={(event) => event.stopPropagation()}
|
||||
>
|
||||
{sortedOrgs.map((organization) => {
|
||||
const isCurrent = organization.id === org?.id
|
||||
|
|
@ -156,92 +138,19 @@ export function SettingsOrgSwitcher() {
|
|||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="my-1 h-px bg-white/[0.06]" />
|
||||
<div className="my-1 h-px bg-white/[0.06]" />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// Open the dialog only after the popover has closed — opening both in
|
||||
// the same tick can leave Radix's `pointer-events: none` stuck on <body>.
|
||||
setOpen(false)
|
||||
setTimeout(() => setCreateOpen(true), 120)
|
||||
}}
|
||||
className="w-full flex items-center gap-2.5 rounded-[10px] px-3 py-2 text-left text-[#A3A3A3] transition-colors hover:bg-white/5 hover:text-white cursor-pointer"
|
||||
>
|
||||
<Plus className="size-4 shrink-0" />
|
||||
<span className="text-[13.5px] font-medium">
|
||||
Create organization
|
||||
</span>
|
||||
</button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Dialog
|
||||
open={createOpen}
|
||||
onOpenChange={(next) => {
|
||||
setCreateOpen(next)
|
||||
if (!next) setCreateName("")
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
style={{ boxShadow: SURFACE_SHADOW }}
|
||||
className={cn(
|
||||
"sm:max-w-[420px] border border-white/[0.12] bg-[#1B1F24] p-5 gap-0 rounded-[22px]",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCreate}
|
||||
className="w-full flex items-center gap-2.5 rounded-[10px] px-3 py-2 text-left text-[#A3A3A3] transition-colors hover:bg-white/5 hover:text-white cursor-pointer"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<DialogTitle
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[18px] font-semibold tracking-[-0.18px] text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
Create organization
|
||||
</DialogTitle>
|
||||
<p className="text-[13px] tracking-[-0.13px] leading-relaxed text-[#737373]">
|
||||
A separate workspace with its own memories, connections, and
|
||||
members.
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
value={createName}
|
||||
onChange={(e) => setCreateName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleCreate()
|
||||
}}
|
||||
placeholder="Organization name"
|
||||
maxLength={80}
|
||||
className="w-full rounded-xl border border-[#2A2D35] bg-[#0D0F14] px-4 py-2.5 text-sm text-white placeholder:text-[#525D6E] focus:outline-none focus:border-[#4BA0FA]/50 transition-colors"
|
||||
/>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCreateOpen(false)}
|
||||
className="px-4 py-2 rounded-full border border-[#2A2D35] text-sm text-[#8B8B8B] hover:text-white hover:border-[#3A3D45] transition-colors cursor-pointer"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!createName.trim() || creating}
|
||||
onClick={handleCreate}
|
||||
className="flex items-center gap-1.5 px-4 py-2 rounded-full text-sm font-medium cursor-pointer transition-opacity bg-[#0D121A] text-[#FAFAFA] shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)] disabled:opacity-40 disabled:cursor-not-allowed hover:opacity-90"
|
||||
>
|
||||
{creating ? (
|
||||
<LoaderIcon className="size-[15px] animate-spin" />
|
||||
) : (
|
||||
<Plus className="size-[15px]" />
|
||||
)}
|
||||
{creating ? "Creating…" : "Create"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
<Plus className="size-4 shrink-0" />
|
||||
<span className="text-[13.5px] font-medium">Create organization</span>
|
||||
</button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,13 +48,18 @@ function DialogOverlay({
|
|||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
portalContainer,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
portalContainer?: HTMLElement | null
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogPortal
|
||||
container={portalContainer ?? undefined}
|
||||
data-slot="dialog-portal"
|
||||
>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
className={cn(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue