feat(ui): move app actions to sidebar and add sidebar user menu variant

Shift app-level actions (settings and user menu) from the top bar to the bottom
of the sidebar for improved navigation consistency. Introduce a new "sidebar"
variant for the UserMenu component with tailored styling and behavior for
sidebar placement. Update SettingsModal to support a customizable trigger label
and styling for sidebar integration. Refactor FinderSidebar to accept an
optional bottomSlot, enabling flexible placement of sidebar controls.
This commit is contained in:
Richard R 2026-05-28 14:58:03 -06:00
parent eea143abbf
commit c12f795026
5 changed files with 188 additions and 122 deletions

View file

@ -15,10 +15,13 @@ const Brand = () => (
); );
const AppActions = () => ( const AppActions = () => (
<> <div className="flex flex-col gap-0.5 w-full">
<SettingsModal /> <SettingsModal
<UserMenu /> triggerLabel="Settings"
</> className="w-full justify-start gap-2 px-2 py-1 text-[12px] border-transparent hover:border-accent"
/>
<UserMenu variant="sidebar" />
</div>
); );
export function HomeContent() { export function HomeContent() {

View file

@ -130,7 +130,13 @@ const SIDEBAR_SECTIONS: SidebarSection[] = [
type AdminSubTab = 'providers' | 'features'; type AdminSubTab = 'providers' | 'features';
export function SettingsModal({ className = '' }: { className?: string }) { export function SettingsModal({
className = '',
triggerLabel,
}: {
className?: string;
triggerLabel?: string;
}) {
const runtimeConfig = useRuntimeConfig(); const runtimeConfig = useRuntimeConfig();
const enableDestructiveDelete = runtimeConfig.enableDestructiveDeleteActions; const enableDestructiveDelete = runtimeConfig.enableDestructiveDeleteActions;
const showAllProviderModels = runtimeConfig.showAllProviderModels; const showAllProviderModels = runtimeConfig.showAllProviderModels;
@ -506,6 +512,7 @@ export function SettingsModal({ className = '' }: { className?: string }) {
tabIndex={0} tabIndex={0}
> >
<SettingsIcon className="w-4 h-4 transition-transform duration-200 ease-out hover:scale-[1.01] hover:rotate-45" /> <SettingsIcon className="w-4 h-4 transition-transform duration-200 ease-out hover:scale-[1.01] hover:rotate-45" />
{triggerLabel && <span className="ml-2">{triggerLabel}</span>}
</Button> </Button>
<Transition appear show={isOpen} as={Fragment}> <Transition appear show={isOpen} as={Fragment}>

View file

@ -7,8 +7,17 @@ import { useFeatureFlag } from '@/contexts/RuntimeConfigContext';
import { useAuthSession } from '@/hooks/useAuthSession'; import { useAuthSession } from '@/hooks/useAuthSession';
import { getAuthClient } from '@/lib/client/auth-client'; import { getAuthClient } from '@/lib/client/auth-client';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { UserIcon } from '@/components/icons/Icons';
export function UserMenu({ className = '' }: { className?: string }) { type UserMenuVariant = 'toolbar' | 'sidebar';
export function UserMenu({
className = '',
variant = 'toolbar',
}: {
className?: string;
variant?: UserMenuVariant;
}) {
const { authEnabled, baseUrl } = useAuthConfig(); const { authEnabled, baseUrl } = useAuthConfig();
const enableUserSignups = useFeatureFlag('enableUserSignups'); const enableUserSignups = useFeatureFlag('enableUserSignups');
const { data: session, isPending } = useAuthSession(); const { data: session, isPending } = useAuthSession();
@ -22,7 +31,27 @@ export function UserMenu({ className = '' }: { className?: string }) {
router.push('/signin'); router.push('/signin');
}; };
const rowClass =
'w-full inline-flex items-center gap-2 px-2 py-1 rounded-md text-[12px] border border-transparent transition-all duration-200 ease-out text-left hover:scale-[1.01] hover:border-accent hover:bg-offbase hover:text-accent';
if (!session || session.user.isAnonymous) { if (!session || session.user.isAnonymous) {
if (variant === 'sidebar') {
return (
<div className={`flex w-full flex-col gap-0.5 ${className}`}>
<Link href="/signin" className={rowClass}>
<UserIcon className="h-3.5 w-3.5 text-muted" />
<span className="truncate">Connect</span>
</Link>
{enableUserSignups && (
<Link href="/signup" className={rowClass}>
<UserIcon className="h-3.5 w-3.5 text-muted" />
<span className="truncate">Create account</span>
</Link>
)}
</div>
);
}
return ( return (
<div className={`flex gap-2 ${className}`}> <div className={`flex gap-2 ${className}`}>
<Link href="/signin"> <Link href="/signin">
@ -41,6 +70,24 @@ export function UserMenu({ className = '' }: { className?: string }) {
); );
} }
if (variant === 'sidebar') {
return (
<button
onClick={handleDisconnectAccount}
className={`${rowClass} ${className}`}
title="Disconnect account"
>
<UserIcon className="h-3.5 w-3.5 text-muted" />
<span className="truncate flex-1">{session.user.email || 'Account'}</span>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
<polyline points="16 17 21 12 16 7"></polyline>
<line x1="21" y1="12" x2="9" y2="12"></line>
</svg>
</button>
);
}
return ( return (
<div className={`flex items-center gap-2 px-2 py-1 rounded-md border border-offbase bg-base ${className}`}> <div className={`flex items-center gap-2 px-2 py-1 rounded-md border border-offbase bg-base ${className}`}>
<span className="hidden sm:block text-xs font-medium text-foreground truncate max-w-[160px]"> <span className="hidden sm:block text-xs font-medium text-foreground truncate max-w-[160px]">

View file

@ -502,7 +502,6 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
isSidebarOpen={effectiveSidebarOpen} isSidebarOpen={effectiveSidebarOpen}
isNarrow={isNarrow} isNarrow={isNarrow}
leftSlot={brand} leftSlot={brand}
rightSlot={appActions}
/> />
} }
sidebar={ sidebar={
@ -521,6 +520,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
width={sidebarWidth} width={sidebarWidth}
onWidthChange={setSidebarWidth} onWidthChange={setSidebarWidth}
topSlot={<DocumentUploader variant="compact" />} topSlot={<DocumentUploader variant="compact" />}
bottomSlot={appActions}
/> />
} }
statusBar={ statusBar={

View file

@ -22,6 +22,7 @@ interface FinderSidebarProps {
width: number; width: number;
onWidthChange: (px: number) => void; onWidthChange: (px: number) => void;
topSlot?: ReactNode; topSlot?: ReactNode;
bottomSlot?: ReactNode;
} }
const MIN_WIDTH = 168; const MIN_WIDTH = 168;
@ -180,6 +181,7 @@ export function FinderSidebar({
width, width,
onWidthChange, onWidthChange,
topSlot, topSlot,
bottomSlot,
}: FinderSidebarProps) { }: FinderSidebarProps) {
const startRef = useRef<{ x: number; w: number } | null>(null); const startRef = useRef<{ x: number; w: number } | null>(null);
@ -201,8 +203,9 @@ export function FinderSidebar({
return ( return (
<aside <aside
style={{ '--sidebar-width': `${width}px` } as CSSProperties} style={{ '--sidebar-width': `${width}px` } as CSSProperties}
className="relative h-full w-full md:[width:var(--sidebar-width)] bg-base border-r border-offbase shrink-0 overflow-y-auto" className="relative h-full w-full md:[width:var(--sidebar-width)] bg-base border-r border-offbase shrink-0 flex flex-col"
> >
<div className="min-h-0 flex-1 overflow-y-auto">
<div className="p-2 flex flex-col gap-0.5"> <div className="p-2 flex flex-col gap-0.5">
{topSlot && ( {topSlot && (
<div className="mb-0.5 shrink-0" onClick={(e) => e.stopPropagation()}> <div className="mb-0.5 shrink-0" onClick={(e) => e.stopPropagation()}>
@ -319,6 +322,12 @@ export function FinderSidebar({
)) ))
)} )}
</div> </div>
</div>
{bottomSlot && (
<div className="shrink-0 border-t border-offbase p-2" onClick={(e) => e.stopPropagation()}>
{bottomSlot}
</div>
)}
<div <div
role="separator" role="separator"