import type { SettingsNavGroups, SettingsNavScope } from '@documenso/lib/utils/settings-nav'; import { cn } from '@documenso/ui/lib/utils'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import { useMemo } from 'react'; import { useLocation, useNavigate } from 'react-router'; import { SettingsOrgSwitcher } from './settings-org-switcher'; import { SettingsTeamSwitcher } from './settings-team-switcher'; type MobileScope = SettingsNavScope | 'account'; export type UnifiedSettingsSidebarMobileProps = { groups: SettingsNavGroups; activeScope: MobileScope; currentOrgUrl: string | null; currentTeamUrl: string | null; }; export const UnifiedSettingsSidebarMobile = ({ groups, activeScope, currentOrgUrl, currentTeamUrl, }: UnifiedSettingsSidebarMobileProps) => { const { _ } = useLingui(); const { pathname } = useLocation(); const navigate = useNavigate(); const visibleScopes = useMemo(() => { const scopes: MobileScope[] = []; if (groups.organisation) { scopes.push('organisation'); } if (groups.team) { scopes.push('team'); } scopes.push('account'); return scopes; }, [groups]); // Falls back to the Account group rather than rendering nothing — an empty scope group // would leave the mobile viewport with no settings navigation at all. const activeGroup = (activeScope === 'organisation' ? groups.organisation : activeScope === 'team' ? groups.team : null) ?? groups.account; const selectableItems = useMemo(() => activeGroup.items.filter((item) => !item.isSubNavParent), [activeGroup.items]); // The select matches on exact value, but plenty of settings pages are sub-routes of a // nav item (`/settings/security/passkeys`, `/t/x/settings/webhooks/:id`, …). Resolving // to the longest matching item path keeps the trigger populated on those pages instead // of rendering an empty box — mirrors the desktop sidebar's prefix highlighting. const selectedPath = useMemo(() => { let bestMatch: string | undefined; for (const item of selectableItems) { if (pathname !== item.path && !pathname.startsWith(`${item.path}/`)) { continue; } if (!bestMatch || item.path.length > bestMatch.length) { bestMatch = item.path; } } return bestMatch; }, [selectableItems, pathname]); const handleScopeChange = (scope: MobileScope) => { if (scope === activeScope) { return; } if (scope === 'organisation' && groups.organisation) { void navigate(groups.organisation.items[0].path); } else if (scope === 'team' && groups.team) { void navigate(groups.team.items[0].path); } else if (scope === 'account') { void navigate(groups.account.items[0].path); } }; // The tab row stays full-bleed (its border-b acts as a full-width divider); the // sections under it get `px-4` to line up with the content pane's own `px-4` // inset, and `pb-4` keeps the last control off the aside's bottom border. return (
{visibleScopes.length > 1 ? (
{visibleScopes.map((scope) => { const isActive = scope === activeScope; const accent = scope === 'organisation' ? 'border-emerald-500 text-emerald-700 dark:text-emerald-300' : scope === 'team' ? 'border-blue-500 text-blue-700 dark:text-blue-300' : 'border-foreground text-foreground'; return ( ); })}
) : (
{activeScope === 'organisation' && Organisation} {activeScope === 'team' && Team} {activeScope === 'account' && Account}
)} {/* The organisation switcher is always present in a scoped view — at team scope it's the only way for a user who can't manage the organisation to move between them, since the Organisation tab is absent when they have no organisation pages. */} {activeScope !== 'account' && currentOrgUrl && (
{activeScope === 'team' && ( )}
)}
Jump to
); };