Sections across the top, and a choice between the two shells
The old web UI put everything in a sidebar, and a reskin of it would still read as the old web UI. The modern shell splits navigation in two instead: the layout switcher already in the top bar is tier one, and a new bar under it carries that layout's own top-level items, each group opening its children in a menu. Nothing on the left, so a queue table or a dashboard gets the whole window. - Management's nine entries and Account's eleven suit a menu bar; the bar measures its items once per layout and folds whatever doesn't fit into "More", recomputing on resize from the cached widths. - Settings keeps the sidebar. Nineteen groups is a configuration browser, not a set of destinations, and a menu bar stops helping however wide the window; the threshold is on the count, not on the name, so a changed schema can't strand anyone. - Which shell to use is the reader's, beside the theme: user menu, Layout, Modern or Legacy. Modern is the default, and the choice is remembered in the browser rather than with the account — settings.json is shared with the webmail, which has no notion of this. - A phone is unchanged. The section bar is hidden below md and the sidebar stays as the slide-over behind the hamburger. - The tree walking both shells need moves to lib/navTree.ts, so the sidebar and the section bar agree on what is visible, what is locked and what is active.
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
const { Sun, Moon, User, LogOut, Check, Menu, Search, FileCode, Palette } = LucideIcons;
|
||||
const { Sun, Moon, User, LogOut, Check, Menu, Search, FileCode, Palette, LayoutTemplate } = LucideIcons;
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CommandPalette } from '@/components/common/CommandPalette';
|
||||
import {
|
||||
@@ -33,7 +33,7 @@ import { sourceDownloadUrl } from '@/lib/sourceDownload';
|
||||
import { visibleLayouts } from '@/lib/layout';
|
||||
import { sectionLandingLink } from '@/lib/lastVisited';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useUIStore } from '@/stores/uiStore';
|
||||
import { isAdminLayout, useUIStore } from '@/stores/uiStore';
|
||||
import { useAuthStore } from '@/stores/authStore';
|
||||
import { buildEndSessionUrl, getPostLogoutRedirectUri } from '@/services/auth/oauth';
|
||||
import { createElement, useEffect, useState } from 'react';
|
||||
@@ -57,6 +57,8 @@ export function TopBar() {
|
||||
const toggleTheme = useUIStore((s) => s.toggleTheme);
|
||||
const palette = useUIStore((s) => s.palette);
|
||||
const setPalette = useUIStore((s) => s.setPalette);
|
||||
const adminLayout = useUIStore((s) => s.adminLayout);
|
||||
const setAdminLayout = useUIStore((s) => s.setAdminLayout);
|
||||
const toggleSidebar = useUIStore((s) => s.toggleSidebar);
|
||||
const setActiveSection = useUIStore((s) => s.setActiveSection);
|
||||
const activeSection = useUIStore((s) => s.activeSection);
|
||||
@@ -159,11 +161,12 @@ export function TopBar() {
|
||||
if (firstLink) navigate(`/${layout.name}/${firstLink}`);
|
||||
}}
|
||||
className={cn(
|
||||
'flex h-8 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground',
|
||||
'flex h-8 items-center justify-center gap-1.5 rounded-lg px-2.5 text-[13px] font-medium text-muted-foreground transition-colors hover:text-foreground lg:px-3',
|
||||
isActive && 'bg-card text-primary shadow-soft',
|
||||
)}
|
||||
>
|
||||
{createElement(getIcon(layout.icon), { className: 'h-4 w-4' })}
|
||||
{createElement(getIcon(layout.icon), { className: 'h-4 w-4 shrink-0' })}
|
||||
<span className="hidden lg:inline">{layout.name}</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{layout.name}</TooltipContent>
|
||||
@@ -228,6 +231,33 @@ export function TopBar() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* INBUXA: the shell is the reader's choice, the way the palette is. */}
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<LayoutTemplate className="mr-2 h-4 w-4" />
|
||||
{t('nav.layoutMenu', 'Layout')}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="w-56">
|
||||
<DropdownMenuRadioGroup
|
||||
value={adminLayout}
|
||||
onValueChange={(v) => isAdminLayout(v) && setAdminLayout(v)}
|
||||
>
|
||||
<DropdownMenuRadioItem value="modern" className="gap-2">
|
||||
{t('nav.layoutModern', 'Modern')}
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="legacy" className="gap-2">
|
||||
{t('nav.layoutLegacy', 'Legacy')}
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<p className="px-2 py-1.5 text-[11px] leading-snug text-muted-foreground">
|
||||
{adminLayout === 'modern'
|
||||
? t('nav.layoutModernHint', 'Sections across the top; the page gets the full width.')
|
||||
: t('nav.layoutLegacyHint', 'The sidebar, as the old web UI had it.')}
|
||||
</p>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
|
||||
{/* INBUXA: the same palettes as INBUXA webmail. */}
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
|
||||
Reference in New Issue
Block a user