@umodeapporg/ui 0.31.2 → 0.32.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.
|
@@ -14,6 +14,8 @@ export interface IUserMenuItem {
|
|
|
14
14
|
interface IUserMenuProps {
|
|
15
15
|
userName?: string;
|
|
16
16
|
userEmail?: string;
|
|
17
|
+
/** The person's photo beside their name in the open menu; without one, or if it fails to load, the initials. */
|
|
18
|
+
userAvatarUrl?: string;
|
|
17
19
|
/** The active account/tenant shown in the trigger, beside the user identity. */
|
|
18
20
|
accountName?: string;
|
|
19
21
|
accountLogoUrl?: string;
|
|
@@ -39,9 +39,25 @@ const AccountRow = ({ account, isActive, switchingAccountId, onSwitchAccount })
|
|
|
39
39
|
]
|
|
40
40
|
}
|
|
41
41
|
);
|
|
42
|
+
const UserIdentityAvatar = ({ photoUrl, initials }) => {
|
|
43
|
+
const [failedUrl, setFailedUrl] = useState(null);
|
|
44
|
+
if (photoUrl && photoUrl !== failedUrl) {
|
|
45
|
+
return /* @__PURE__ */ jsx(
|
|
46
|
+
"img",
|
|
47
|
+
{
|
|
48
|
+
src: photoUrl,
|
|
49
|
+
alt: "",
|
|
50
|
+
onError: () => setFailedUrl(photoUrl),
|
|
51
|
+
className: "size-11 shrink-0 rounded-full object-cover"
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return /* @__PURE__ */ jsx("div", { className: "flex size-11 shrink-0 items-center justify-center rounded-full bg-gradient-to-tl from-brand to-brand text-sm font-bold text-white", children: initials });
|
|
56
|
+
};
|
|
42
57
|
const UserMenu = ({
|
|
43
58
|
userName,
|
|
44
59
|
userEmail,
|
|
60
|
+
userAvatarUrl,
|
|
45
61
|
accountName,
|
|
46
62
|
accountLogoUrl,
|
|
47
63
|
roleLabel,
|
|
@@ -137,7 +153,7 @@ const UserMenu = ({
|
|
|
137
153
|
) }),
|
|
138
154
|
/* @__PURE__ */ jsxs(ControlledPopoverContent, { className: "z-[9999] w-72 max-w-[calc(100vw-1.5rem)] rounded-xl border border-border p-0 shadow-xl", children: [
|
|
139
155
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-3 px-5 pb-4 pt-5", children: [
|
|
140
|
-
/* @__PURE__ */ jsx(
|
|
156
|
+
/* @__PURE__ */ jsx(UserIdentityAvatar, { photoUrl: userAvatarUrl, initials: userInitials }),
|
|
141
157
|
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col overflow-hidden", children: [
|
|
142
158
|
/* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold text-foreground", children: userName || t("userMenu.loading") }),
|
|
143
159
|
/* @__PURE__ */ jsx("span", { className: "truncate text-xs text-foreground-faint", children: userEmail || "" })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UserMenu.js","sources":["../../../src/components/UI/UserMenu.tsx"],"sourcesContent":["import { ReactNode, useEffect, useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledImage from '@/components/Form/ControlledImage';\nimport { useUiNavigation } from '@/components/Navigation/UiNavigationContext';\nimport UnifiedSearchBar from '@/components/Search/UnifiedSearchBar';\nimport OutlinedChip from '@/components/UI/Chips/OutlinedChip';\nimport {\n ControlledPopoverContent,\n ControlledPopoverHandler,\n ControlledPopoverRoot,\n} from '@/components/UI/ControlledPopover';\nimport LoadingSpinner from '@/components/UI/LoadingSpinner';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport NoDataFound from '@/components/UI/NoDataFound';\nimport { getUserAvatarLetters } from '@/helpers/stringHelpers';\n\nconst SEARCH_THRESHOLD = 5;\n\nexport interface IUserMenuAccount {\n id: string;\n name: string;\n logoUrl?: string;\n}\n\nexport interface IUserMenuItem {\n key: string;\n label: string;\n /** Internal route, rendered through the navigation port's Link. */\n to?: string;\n onClick?: () => void;\n tone?: 'default' | 'danger';\n}\n\ninterface IUserMenuProps {\n userName?: string;\n userEmail?: string;\n /** The active account/tenant shown in the trigger, beside the user identity. */\n accountName?: string;\n accountLogoUrl?: string;\n /** Role chip in the trigger; omitted when empty. */\n roleLabel?: string;\n /** Account switcher list; omit (undefined) to hide the whole section. */\n accounts?: Array<IUserMenuAccount>;\n activeAccountId?: string;\n /** Account being switched to; disables the list and shows its spinner. */\n switchingAccountId?: string | null;\n loadingAccounts?: boolean;\n onSwitchAccount?: (accountId: string) => void;\n /** Fired when the popover opens/closes, e.g. to lazily fetch the accounts. */\n onOpenChange?: (open: boolean) => void;\n open: boolean;\n /** Extra rows in the footer (profile, settings...), before the logout row. */\n menuItems?: Array<IUserMenuItem>;\n onLogout?: () => void;\n logoutLabel?: string;\n /** Renders only the identity summary, with no popover (e.g. admin mode). */\n readOnly?: boolean;\n className?: string;\n}\n\nconst AccountRow: React.FC<{\n account: IUserMenuAccount;\n isActive: boolean;\n switchingAccountId?: string | null;\n onSwitchAccount?: (accountId: string) => void;\n}> = ({ account, isActive, switchingAccountId, onSwitchAccount }) => (\n <ControlledButton\n defaultHtmlButton\n type=\"button\"\n disabled={!!switchingAccountId}\n onClick={() => onSwitchAccount?.(account.id)}\n className={classNames('flex w-full items-center gap-x-3 rounded-lg px-2 py-2.5 transition-colors', {\n 'bg-brand-soft': isActive,\n 'hover:bg-muted': !isActive,\n 'disabled:cursor-not-allowed disabled:opacity-60': !!switchingAccountId,\n })}\n >\n {account.logoUrl ? (\n <ControlledImage\n src={account.logoUrl}\n className=\"size-8 shrink-0 rounded-full border border-brand-border bg-surface object-cover\"\n />\n ) : (\n <div className=\"flex size-8 shrink-0 items-center justify-center rounded-full bg-brand-hover text-xs font-bold text-brand-fg\">\n {getUserAvatarLetters(account.name)}\n </div>\n )}\n\n <div className=\"flex min-w-0 flex-1 flex-col items-start\">\n <span className=\"w-full truncate text-left text-sm text-foreground\">{account.name}</span>\n </div>\n\n {switchingAccountId === account.id && <LoadingSpinner size={4} color=\"foreground-faint\" />}\n {isActive && !switchingAccountId && <MaterialSymbol icon=\"check\" size={18} color=\"rgb(var(--color-primary))\" />}\n </ControlledButton>\n);\n\n/**\n * The uMode account menu: identity summary as the trigger, account switcher (with search past\n * five entries), custom rows and logout in a popover. Everything app-shaped (who the user is,\n * what switching does, where profile lives) arrives as props, so any host renders the same\n * surface the dashboard does.\n */\nconst UserMenu: React.FC<IUserMenuProps> = ({\n userName,\n userEmail,\n accountName,\n accountLogoUrl,\n roleLabel,\n accounts,\n activeAccountId,\n switchingAccountId,\n loadingAccounts = false,\n onSwitchAccount,\n onOpenChange,\n open,\n menuItems,\n onLogout,\n logoutLabel,\n readOnly = false,\n className,\n}) => {\n const { t } = useTranslation('ui');\n const { Link: NavigationLink } = useUiNavigation();\n\n const userInitials = userName ? getUserAvatarLetters(userName) : '??';\n const showAccounts = Array.isArray(accounts);\n const safeAccounts = accounts ?? [];\n\n const [filtered, setFiltered] = useState<Array<IUserMenuAccount>>(safeAccounts);\n\n useEffect(() => {\n setFiltered(accounts ?? []);\n }, [accounts, open]);\n\n const accountSummary = (\n <>\n <div className=\"flex size-10 shrink-0 items-center justify-center rounded-full bg-gradient-to-tl from-brand to-brand\">\n {accountLogoUrl && <ControlledImage src={accountLogoUrl} className=\"size-9 rounded-full\" />}\n </div>\n {/* Below the tablet edge the header only has room for the avatar; the same identity is\n one tap away inside the popover, so nothing is lost. */}\n <div className=\"hidden min-w-0 flex-1 flex-col items-start gap-y-0.5 tablet:flex\">\n <span className=\"w-full truncate text-left text-sm font-semibold text-foreground\">{userName}</span>\n <div className=\"flex w-full items-center justify-between gap-x-2\">\n <span className=\"min-w-0 flex-1 truncate text-left text-xs text-foreground-subtle\">{accountName}</span>\n {roleLabel && (\n <OutlinedChip\n label={roleLabel}\n chipIcon=\"verified_user\"\n truncate\n tooltip={roleLabel}\n className=\"max-w-[15rem] shrink-0 whitespace-nowrap border border-brand-border bg-brand-soft text-brand-emphasis\"\n />\n )}\n </div>\n </div>\n </>\n );\n\n if (readOnly) {\n return (\n <div className={classNames('flex min-w-0 items-center gap-x-2.5 px-2.5 py-1.5 tablet:min-w-[300px]', className)}>\n {accountSummary}\n </div>\n );\n }\n\n const footerRow = (item: IUserMenuItem): ReactNode => {\n const rowClass = classNames(\n 'rounded-lg px-2 py-2.5 text-left text-sm transition-colors',\n item.tone === 'danger' ? 'text-danger-emphasis hover:bg-danger-soft' : 'text-foreground-muted hover:bg-muted',\n );\n\n if (item.to) {\n return (\n <NavigationLink key={item.key} to={item.to} onClick={() => onOpenChange?.(false)} className={rowClass}>\n {item.label}\n </NavigationLink>\n );\n }\n return (\n <ControlledButton\n defaultHtmlButton\n key={item.key}\n type=\"button\"\n onClick={() => {\n onOpenChange?.(false);\n item.onClick?.();\n }}\n className={classNames('cursor-pointer', rowClass)}\n >\n {item.label}\n </ControlledButton>\n );\n };\n\n return (\n <ControlledPopoverRoot placement=\"bottom-end\" open={open} handler={(next) => onOpenChange?.(next)}>\n <ControlledPopoverHandler>\n <ControlledButton\n defaultHtmlButton\n type=\"button\"\n className={classNames(\n 'flex min-w-0 cursor-pointer items-center gap-x-2.5 px-2.5 py-1.5 tablet:min-w-[300px]',\n className,\n )}\n >\n {accountSummary}\n <MaterialSymbol\n icon={open ? 'expand_less' : 'expand_more'}\n size={20}\n color=\"rgb(var(--color-foreground-subtle))\"\n />\n </ControlledButton>\n </ControlledPopoverHandler>\n\n <ControlledPopoverContent className=\"z-[9999] w-72 max-w-[calc(100vw-1.5rem)] rounded-xl border border-border p-0 shadow-xl\">\n <div className=\"flex items-center gap-x-3 px-5 pb-4 pt-5\">\n <div className=\"flex size-11 shrink-0 items-center justify-center rounded-full bg-gradient-to-tl from-brand to-brand text-sm font-bold text-white\">\n {userInitials}\n </div>\n <div className=\"flex min-w-0 flex-1 flex-col overflow-hidden\">\n <span className=\"truncate text-sm font-semibold text-foreground\">{userName || t('userMenu.loading')}</span>\n <span className=\"truncate text-xs text-foreground-faint\">{userEmail || ''}</span>\n </div>\n </div>\n\n <hr className=\"border-border-subtle\" />\n\n {showAccounts && (\n <>\n <div className=\"px-5 pb-2 pt-4\">\n <span className=\"text-xxs font-semibold uppercase tracking-wider text-foreground-faint\">\n {t('userMenu.yourAccounts')}\n </span>\n </div>\n\n {safeAccounts.length > SEARCH_THRESHOLD && (\n <div className=\"px-3 pb-2\">\n <UnifiedSearchBar\n mode=\"default\"\n searchableItems={safeAccounts}\n searchKeys={['name']}\n setFilteredItems={setFiltered}\n label={t('userMenu.searchAccount')}\n debounceTime={300}\n minSearchLength={1}\n containerClassName=\"relative flex gap-x-2\"\n />\n </div>\n )}\n\n <div className=\"max-h-48 overflow-y-auto px-3 pb-2\">\n {loadingAccounts ? (\n <div className=\"flex items-center justify-center py-4\">\n <LoadingSpinner size={5} color=\"foreground-faint\" />\n </div>\n ) : filtered.length === 0 ? (\n <NoDataFound compact message={t('userMenu.noAccounts')} />\n ) : (\n filtered.map((account) => (\n <AccountRow\n key={account.id}\n account={account}\n isActive={account.id === activeAccountId}\n switchingAccountId={switchingAccountId}\n onSwitchAccount={onSwitchAccount}\n />\n ))\n )}\n </div>\n\n <hr className=\"border-border-subtle\" />\n </>\n )}\n\n <div className=\"flex flex-col px-3 py-2\">\n {(menuItems ?? []).map(footerRow)}\n\n {onLogout && (\n <ControlledButton\n defaultHtmlButton\n type=\"button\"\n onClick={() => {\n onOpenChange?.(false);\n onLogout();\n }}\n className=\"cursor-pointer rounded-lg px-2 py-2.5 text-left text-sm text-danger-emphasis transition-colors hover:bg-danger-soft\"\n >\n {logoutLabel || t('userMenu.logout')}\n </ControlledButton>\n )}\n </div>\n </ControlledPopoverContent>\n </ControlledPopoverRoot>\n );\n};\n\nexport default UserMenu;\nexport type { IUserMenuProps };\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"UserMenu.js","sources":["../../../src/components/UI/UserMenu.tsx"],"sourcesContent":["import { ReactNode, useEffect, useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledImage from '@/components/Form/ControlledImage';\nimport { useUiNavigation } from '@/components/Navigation/UiNavigationContext';\nimport UnifiedSearchBar from '@/components/Search/UnifiedSearchBar';\nimport OutlinedChip from '@/components/UI/Chips/OutlinedChip';\nimport {\n ControlledPopoverContent,\n ControlledPopoverHandler,\n ControlledPopoverRoot,\n} from '@/components/UI/ControlledPopover';\nimport LoadingSpinner from '@/components/UI/LoadingSpinner';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport NoDataFound from '@/components/UI/NoDataFound';\nimport { getUserAvatarLetters } from '@/helpers/stringHelpers';\n\nconst SEARCH_THRESHOLD = 5;\n\nexport interface IUserMenuAccount {\n id: string;\n name: string;\n logoUrl?: string;\n}\n\nexport interface IUserMenuItem {\n key: string;\n label: string;\n /** Internal route, rendered through the navigation port's Link. */\n to?: string;\n onClick?: () => void;\n tone?: 'default' | 'danger';\n}\n\ninterface IUserMenuProps {\n userName?: string;\n userEmail?: string;\n /** The person's photo beside their name in the open menu; without one, or if it fails to load, the initials. */\n userAvatarUrl?: string;\n /** The active account/tenant shown in the trigger, beside the user identity. */\n accountName?: string;\n accountLogoUrl?: string;\n /** Role chip in the trigger; omitted when empty. */\n roleLabel?: string;\n /** Account switcher list; omit (undefined) to hide the whole section. */\n accounts?: Array<IUserMenuAccount>;\n activeAccountId?: string;\n /** Account being switched to; disables the list and shows its spinner. */\n switchingAccountId?: string | null;\n loadingAccounts?: boolean;\n onSwitchAccount?: (accountId: string) => void;\n /** Fired when the popover opens/closes, e.g. to lazily fetch the accounts. */\n onOpenChange?: (open: boolean) => void;\n open: boolean;\n /** Extra rows in the footer (profile, settings...), before the logout row. */\n menuItems?: Array<IUserMenuItem>;\n onLogout?: () => void;\n logoutLabel?: string;\n /** Renders only the identity summary, with no popover (e.g. admin mode). */\n readOnly?: boolean;\n className?: string;\n}\n\nconst AccountRow: React.FC<{\n account: IUserMenuAccount;\n isActive: boolean;\n switchingAccountId?: string | null;\n onSwitchAccount?: (accountId: string) => void;\n}> = ({ account, isActive, switchingAccountId, onSwitchAccount }) => (\n <ControlledButton\n defaultHtmlButton\n type=\"button\"\n disabled={!!switchingAccountId}\n onClick={() => onSwitchAccount?.(account.id)}\n className={classNames('flex w-full items-center gap-x-3 rounded-lg px-2 py-2.5 transition-colors', {\n 'bg-brand-soft': isActive,\n 'hover:bg-muted': !isActive,\n 'disabled:cursor-not-allowed disabled:opacity-60': !!switchingAccountId,\n })}\n >\n {account.logoUrl ? (\n <ControlledImage\n src={account.logoUrl}\n className=\"size-8 shrink-0 rounded-full border border-brand-border bg-surface object-cover\"\n />\n ) : (\n <div className=\"flex size-8 shrink-0 items-center justify-center rounded-full bg-brand-hover text-xs font-bold text-brand-fg\">\n {getUserAvatarLetters(account.name)}\n </div>\n )}\n\n <div className=\"flex min-w-0 flex-1 flex-col items-start\">\n <span className=\"w-full truncate text-left text-sm text-foreground\">{account.name}</span>\n </div>\n\n {switchingAccountId === account.id && <LoadingSpinner size={4} color=\"foreground-faint\" />}\n {isActive && !switchingAccountId && <MaterialSymbol icon=\"check\" size={18} color=\"rgb(var(--color-primary))\" />}\n </ControlledButton>\n);\n\n/** The person at the top of the open menu. A new URL gets its own chance after an old one failed. */\nconst UserIdentityAvatar: React.FC<{ photoUrl?: string; initials: string }> = ({ photoUrl, initials }) => {\n const [failedUrl, setFailedUrl] = useState<string | null>(null);\n\n if (photoUrl && photoUrl !== failedUrl) {\n return (\n <img\n src={photoUrl}\n alt=\"\"\n onError={() => setFailedUrl(photoUrl)}\n className=\"size-11 shrink-0 rounded-full object-cover\"\n />\n );\n }\n\n return (\n <div className=\"flex size-11 shrink-0 items-center justify-center rounded-full bg-gradient-to-tl from-brand to-brand text-sm font-bold text-white\">\n {initials}\n </div>\n );\n};\n\n/**\n * The uMode account menu: identity summary as the trigger, account switcher (with search past\n * five entries), custom rows and logout in a popover. Everything app-shaped (who the user is,\n * what switching does, where profile lives) arrives as props, so any host renders the same\n * surface the dashboard does.\n */\nconst UserMenu: React.FC<IUserMenuProps> = ({\n userName,\n userEmail,\n userAvatarUrl,\n accountName,\n accountLogoUrl,\n roleLabel,\n accounts,\n activeAccountId,\n switchingAccountId,\n loadingAccounts = false,\n onSwitchAccount,\n onOpenChange,\n open,\n menuItems,\n onLogout,\n logoutLabel,\n readOnly = false,\n className,\n}) => {\n const { t } = useTranslation('ui');\n const { Link: NavigationLink } = useUiNavigation();\n\n const userInitials = userName ? getUserAvatarLetters(userName) : '??';\n const showAccounts = Array.isArray(accounts);\n const safeAccounts = accounts ?? [];\n\n const [filtered, setFiltered] = useState<Array<IUserMenuAccount>>(safeAccounts);\n\n useEffect(() => {\n setFiltered(accounts ?? []);\n }, [accounts, open]);\n\n const accountSummary = (\n <>\n <div className=\"flex size-10 shrink-0 items-center justify-center rounded-full bg-gradient-to-tl from-brand to-brand\">\n {accountLogoUrl && <ControlledImage src={accountLogoUrl} className=\"size-9 rounded-full\" />}\n </div>\n {/* Below the tablet edge the header only has room for the avatar; the same identity is\n one tap away inside the popover, so nothing is lost. */}\n <div className=\"hidden min-w-0 flex-1 flex-col items-start gap-y-0.5 tablet:flex\">\n <span className=\"w-full truncate text-left text-sm font-semibold text-foreground\">{userName}</span>\n <div className=\"flex w-full items-center justify-between gap-x-2\">\n <span className=\"min-w-0 flex-1 truncate text-left text-xs text-foreground-subtle\">{accountName}</span>\n {roleLabel && (\n <OutlinedChip\n label={roleLabel}\n chipIcon=\"verified_user\"\n truncate\n tooltip={roleLabel}\n className=\"max-w-[15rem] shrink-0 whitespace-nowrap border border-brand-border bg-brand-soft text-brand-emphasis\"\n />\n )}\n </div>\n </div>\n </>\n );\n\n if (readOnly) {\n return (\n <div className={classNames('flex min-w-0 items-center gap-x-2.5 px-2.5 py-1.5 tablet:min-w-[300px]', className)}>\n {accountSummary}\n </div>\n );\n }\n\n const footerRow = (item: IUserMenuItem): ReactNode => {\n const rowClass = classNames(\n 'rounded-lg px-2 py-2.5 text-left text-sm transition-colors',\n item.tone === 'danger' ? 'text-danger-emphasis hover:bg-danger-soft' : 'text-foreground-muted hover:bg-muted',\n );\n\n if (item.to) {\n return (\n <NavigationLink key={item.key} to={item.to} onClick={() => onOpenChange?.(false)} className={rowClass}>\n {item.label}\n </NavigationLink>\n );\n }\n return (\n <ControlledButton\n defaultHtmlButton\n key={item.key}\n type=\"button\"\n onClick={() => {\n onOpenChange?.(false);\n item.onClick?.();\n }}\n className={classNames('cursor-pointer', rowClass)}\n >\n {item.label}\n </ControlledButton>\n );\n };\n\n return (\n <ControlledPopoverRoot placement=\"bottom-end\" open={open} handler={(next) => onOpenChange?.(next)}>\n <ControlledPopoverHandler>\n <ControlledButton\n defaultHtmlButton\n type=\"button\"\n className={classNames(\n 'flex min-w-0 cursor-pointer items-center gap-x-2.5 px-2.5 py-1.5 tablet:min-w-[300px]',\n className,\n )}\n >\n {accountSummary}\n <MaterialSymbol\n icon={open ? 'expand_less' : 'expand_more'}\n size={20}\n color=\"rgb(var(--color-foreground-subtle))\"\n />\n </ControlledButton>\n </ControlledPopoverHandler>\n\n <ControlledPopoverContent className=\"z-[9999] w-72 max-w-[calc(100vw-1.5rem)] rounded-xl border border-border p-0 shadow-xl\">\n <div className=\"flex items-center gap-x-3 px-5 pb-4 pt-5\">\n <UserIdentityAvatar photoUrl={userAvatarUrl} initials={userInitials} />\n <div className=\"flex min-w-0 flex-1 flex-col overflow-hidden\">\n <span className=\"truncate text-sm font-semibold text-foreground\">{userName || t('userMenu.loading')}</span>\n <span className=\"truncate text-xs text-foreground-faint\">{userEmail || ''}</span>\n </div>\n </div>\n\n <hr className=\"border-border-subtle\" />\n\n {showAccounts && (\n <>\n <div className=\"px-5 pb-2 pt-4\">\n <span className=\"text-xxs font-semibold uppercase tracking-wider text-foreground-faint\">\n {t('userMenu.yourAccounts')}\n </span>\n </div>\n\n {safeAccounts.length > SEARCH_THRESHOLD && (\n <div className=\"px-3 pb-2\">\n <UnifiedSearchBar\n mode=\"default\"\n searchableItems={safeAccounts}\n searchKeys={['name']}\n setFilteredItems={setFiltered}\n label={t('userMenu.searchAccount')}\n debounceTime={300}\n minSearchLength={1}\n containerClassName=\"relative flex gap-x-2\"\n />\n </div>\n )}\n\n <div className=\"max-h-48 overflow-y-auto px-3 pb-2\">\n {loadingAccounts ? (\n <div className=\"flex items-center justify-center py-4\">\n <LoadingSpinner size={5} color=\"foreground-faint\" />\n </div>\n ) : filtered.length === 0 ? (\n <NoDataFound compact message={t('userMenu.noAccounts')} />\n ) : (\n filtered.map((account) => (\n <AccountRow\n key={account.id}\n account={account}\n isActive={account.id === activeAccountId}\n switchingAccountId={switchingAccountId}\n onSwitchAccount={onSwitchAccount}\n />\n ))\n )}\n </div>\n\n <hr className=\"border-border-subtle\" />\n </>\n )}\n\n <div className=\"flex flex-col px-3 py-2\">\n {(menuItems ?? []).map(footerRow)}\n\n {onLogout && (\n <ControlledButton\n defaultHtmlButton\n type=\"button\"\n onClick={() => {\n onOpenChange?.(false);\n onLogout();\n }}\n className=\"cursor-pointer rounded-lg px-2 py-2.5 text-left text-sm text-danger-emphasis transition-colors hover:bg-danger-soft\"\n >\n {logoutLabel || t('userMenu.logout')}\n </ControlledButton>\n )}\n </div>\n </ControlledPopoverContent>\n </ControlledPopoverRoot>\n );\n};\n\nexport default UserMenu;\nexport type { IUserMenuProps };\n"],"names":[],"mappings":"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"}
|