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:
parent
eea143abbf
commit
c12f795026
5 changed files with 188 additions and 122 deletions
|
|
@ -15,10 +15,13 @@ const Brand = () => (
|
|||
);
|
||||
|
||||
const AppActions = () => (
|
||||
<>
|
||||
<SettingsModal />
|
||||
<UserMenu />
|
||||
</>
|
||||
<div className="flex flex-col gap-0.5 w-full">
|
||||
<SettingsModal
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -130,7 +130,13 @@ const SIDEBAR_SECTIONS: SidebarSection[] = [
|
|||
|
||||
type AdminSubTab = 'providers' | 'features';
|
||||
|
||||
export function SettingsModal({ className = '' }: { className?: string }) {
|
||||
export function SettingsModal({
|
||||
className = '',
|
||||
triggerLabel,
|
||||
}: {
|
||||
className?: string;
|
||||
triggerLabel?: string;
|
||||
}) {
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
const enableDestructiveDelete = runtimeConfig.enableDestructiveDeleteActions;
|
||||
const showAllProviderModels = runtimeConfig.showAllProviderModels;
|
||||
|
|
@ -506,6 +512,7 @@ export function SettingsModal({ className = '' }: { className?: string }) {
|
|||
tabIndex={0}
|
||||
>
|
||||
<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>
|
||||
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
|
|
|
|||
|
|
@ -7,8 +7,17 @@ import { useFeatureFlag } from '@/contexts/RuntimeConfigContext';
|
|||
import { useAuthSession } from '@/hooks/useAuthSession';
|
||||
import { getAuthClient } from '@/lib/client/auth-client';
|
||||
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 enableUserSignups = useFeatureFlag('enableUserSignups');
|
||||
const { data: session, isPending } = useAuthSession();
|
||||
|
|
@ -22,7 +31,27 @@ export function UserMenu({ className = '' }: { className?: string }) {
|
|||
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 (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 (
|
||||
<div className={`flex gap-2 ${className}`}>
|
||||
<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 (
|
||||
<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]">
|
||||
|
|
|
|||
|
|
@ -502,7 +502,6 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
|||
isSidebarOpen={effectiveSidebarOpen}
|
||||
isNarrow={isNarrow}
|
||||
leftSlot={brand}
|
||||
rightSlot={appActions}
|
||||
/>
|
||||
}
|
||||
sidebar={
|
||||
|
|
@ -521,6 +520,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
|||
width={sidebarWidth}
|
||||
onWidthChange={setSidebarWidth}
|
||||
topSlot={<DocumentUploader variant="compact" />}
|
||||
bottomSlot={appActions}
|
||||
/>
|
||||
}
|
||||
statusBar={
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ interface FinderSidebarProps {
|
|||
width: number;
|
||||
onWidthChange: (px: number) => void;
|
||||
topSlot?: ReactNode;
|
||||
bottomSlot?: ReactNode;
|
||||
}
|
||||
|
||||
const MIN_WIDTH = 168;
|
||||
|
|
@ -180,6 +181,7 @@ export function FinderSidebar({
|
|||
width,
|
||||
onWidthChange,
|
||||
topSlot,
|
||||
bottomSlot,
|
||||
}: FinderSidebarProps) {
|
||||
const startRef = useRef<{ x: number; w: number } | null>(null);
|
||||
|
||||
|
|
@ -201,124 +203,131 @@ export function FinderSidebar({
|
|||
return (
|
||||
<aside
|
||||
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="p-2 flex flex-col gap-0.5">
|
||||
{topSlot && (
|
||||
<div className="mb-0.5 shrink-0" onClick={(e) => e.stopPropagation()}>
|
||||
{topSlot}
|
||||
</div>
|
||||
)}
|
||||
<SectionHeader isFirst={!!topSlot}>Library</SectionHeader>
|
||||
<SidebarRow
|
||||
active={filter === 'all'}
|
||||
onClick={() => onFilterChange('all')}
|
||||
icon={<HomeIcon className="w-3.5 h-3.5" />}
|
||||
label="All Documents"
|
||||
count={counts.all}
|
||||
/>
|
||||
<SidebarRow
|
||||
active={filter === 'recents'}
|
||||
onClick={() => onFilterChange('recents')}
|
||||
icon={<ClockIcon className="w-3.5 h-3.5" />}
|
||||
label="Recently Opened"
|
||||
/>
|
||||
|
||||
<SectionHeader>Kinds</SectionHeader>
|
||||
<SidebarRow
|
||||
active={filter === 'pdf'}
|
||||
onClick={() => onFilterChange('pdf')}
|
||||
icon={<PDFIcon className="w-3.5 h-3.5" />}
|
||||
label="PDF"
|
||||
count={counts.pdf}
|
||||
/>
|
||||
<SidebarRow
|
||||
active={filter === 'epub'}
|
||||
onClick={() => onFilterChange('epub')}
|
||||
icon={<EPUBIcon className="w-3.5 h-3.5" />}
|
||||
label="EPUB"
|
||||
count={counts.epub}
|
||||
/>
|
||||
<SidebarRow
|
||||
active={filter === 'html'}
|
||||
onClick={() => onFilterChange('html')}
|
||||
icon={<FileIcon className="w-3.5 h-3.5" />}
|
||||
label="HTML / Text"
|
||||
count={counts.html}
|
||||
/>
|
||||
|
||||
<SectionHeader
|
||||
rightSlot={(
|
||||
<Menu as="div" className="relative inline-flex items-center leading-none text-left shrink-0 normal-case tracking-normal font-normal">
|
||||
<MenuButton
|
||||
className="inline-flex items-center justify-center h-3.5 w-5 rounded-sm text-muted hover:text-accent transition-colors duration-200 ease-out focus:outline-none"
|
||||
title="Folder actions"
|
||||
aria-label="Folder actions"
|
||||
>
|
||||
<DotsHorizontalIcon className="w-4 h-2.5" />
|
||||
</MenuButton>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<MenuItems
|
||||
anchor="bottom start"
|
||||
className="z-50 mt-2 min-w-[180px] rounded-md bg-base shadow-lg ring-1 ring-black/5 focus:outline-none p-1 normal-case tracking-normal font-normal"
|
||||
>
|
||||
<MenuItem>
|
||||
{({ active }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onNewFolder}
|
||||
className={`${active ? 'bg-offbase text-accent' : 'text-foreground'} group flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`}
|
||||
>
|
||||
<FolderPlusIcon className="h-4 w-4" />
|
||||
New Folder
|
||||
</button>
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem disabled={folders.length === 0}>
|
||||
{({ active, disabled }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClearFolders}
|
||||
disabled={disabled}
|
||||
className={`${disabled ? 'text-muted/60 cursor-not-allowed' : active ? 'bg-offbase text-accent' : 'text-foreground'} group flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`}
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
Remove All Folders
|
||||
</button>
|
||||
)}
|
||||
</MenuItem>
|
||||
</MenuItems>
|
||||
</Transition>
|
||||
</Menu>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||
<div className="p-2 flex flex-col gap-0.5">
|
||||
{topSlot && (
|
||||
<div className="mb-0.5 shrink-0" onClick={(e) => e.stopPropagation()}>
|
||||
{topSlot}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
Folders
|
||||
</SectionHeader>
|
||||
{folders.length === 0 ? (
|
||||
<p className="px-2 py-1 text-[11px] text-muted">No folders yet</p>
|
||||
) : (
|
||||
folders.map((folder) => (
|
||||
<FolderRow
|
||||
key={folder.id}
|
||||
folder={folder}
|
||||
active={filter === `folder:${folder.id}`}
|
||||
onClick={() => onFilterChange(`folder:${folder.id}`)}
|
||||
onDelete={() => onDeleteFolder(folder.id)}
|
||||
onDropOnFolder={onDropOnFolder}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
<SectionHeader isFirst={!!topSlot}>Library</SectionHeader>
|
||||
<SidebarRow
|
||||
active={filter === 'all'}
|
||||
onClick={() => onFilterChange('all')}
|
||||
icon={<HomeIcon className="w-3.5 h-3.5" />}
|
||||
label="All Documents"
|
||||
count={counts.all}
|
||||
/>
|
||||
<SidebarRow
|
||||
active={filter === 'recents'}
|
||||
onClick={() => onFilterChange('recents')}
|
||||
icon={<ClockIcon className="w-3.5 h-3.5" />}
|
||||
label="Recently Opened"
|
||||
/>
|
||||
|
||||
<SectionHeader>Kinds</SectionHeader>
|
||||
<SidebarRow
|
||||
active={filter === 'pdf'}
|
||||
onClick={() => onFilterChange('pdf')}
|
||||
icon={<PDFIcon className="w-3.5 h-3.5" />}
|
||||
label="PDF"
|
||||
count={counts.pdf}
|
||||
/>
|
||||
<SidebarRow
|
||||
active={filter === 'epub'}
|
||||
onClick={() => onFilterChange('epub')}
|
||||
icon={<EPUBIcon className="w-3.5 h-3.5" />}
|
||||
label="EPUB"
|
||||
count={counts.epub}
|
||||
/>
|
||||
<SidebarRow
|
||||
active={filter === 'html'}
|
||||
onClick={() => onFilterChange('html')}
|
||||
icon={<FileIcon className="w-3.5 h-3.5" />}
|
||||
label="HTML / Text"
|
||||
count={counts.html}
|
||||
/>
|
||||
|
||||
<SectionHeader
|
||||
rightSlot={(
|
||||
<Menu as="div" className="relative inline-flex items-center leading-none text-left shrink-0 normal-case tracking-normal font-normal">
|
||||
<MenuButton
|
||||
className="inline-flex items-center justify-center h-3.5 w-5 rounded-sm text-muted hover:text-accent transition-colors duration-200 ease-out focus:outline-none"
|
||||
title="Folder actions"
|
||||
aria-label="Folder actions"
|
||||
>
|
||||
<DotsHorizontalIcon className="w-4 h-2.5" />
|
||||
</MenuButton>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<MenuItems
|
||||
anchor="bottom start"
|
||||
className="z-50 mt-2 min-w-[180px] rounded-md bg-base shadow-lg ring-1 ring-black/5 focus:outline-none p-1 normal-case tracking-normal font-normal"
|
||||
>
|
||||
<MenuItem>
|
||||
{({ active }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onNewFolder}
|
||||
className={`${active ? 'bg-offbase text-accent' : 'text-foreground'} group flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`}
|
||||
>
|
||||
<FolderPlusIcon className="h-4 w-4" />
|
||||
New Folder
|
||||
</button>
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem disabled={folders.length === 0}>
|
||||
{({ active, disabled }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClearFolders}
|
||||
disabled={disabled}
|
||||
className={`${disabled ? 'text-muted/60 cursor-not-allowed' : active ? 'bg-offbase text-accent' : 'text-foreground'} group flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`}
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
Remove All Folders
|
||||
</button>
|
||||
)}
|
||||
</MenuItem>
|
||||
</MenuItems>
|
||||
</Transition>
|
||||
</Menu>
|
||||
)}
|
||||
>
|
||||
Folders
|
||||
</SectionHeader>
|
||||
{folders.length === 0 ? (
|
||||
<p className="px-2 py-1 text-[11px] text-muted">No folders yet</p>
|
||||
) : (
|
||||
folders.map((folder) => (
|
||||
<FolderRow
|
||||
key={folder.id}
|
||||
folder={folder}
|
||||
active={filter === `folder:${folder.id}`}
|
||||
onClick={() => onFilterChange(`folder:${folder.id}`)}
|
||||
onDelete={() => onDeleteFolder(folder.id)}
|
||||
onDropOnFolder={onDropOnFolder}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{bottomSlot && (
|
||||
<div className="shrink-0 border-t border-offbase p-2" onClick={(e) => e.stopPropagation()}>
|
||||
{bottomSlot}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
role="separator"
|
||||
|
|
|
|||
Loading…
Reference in a new issue