Collapse sidebar nav groups into dropdowns that open on the active page
Each group label (Dashboard, Settings) toggles its items; a group starts closed unless it holds the current route, and a manual toggle resets on the next navigation. Mobile keeps every item visible. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -135,6 +135,9 @@ export function AppLayout() {
|
|||||||
const { signOut } = useClerk()
|
const { signOut } = useClerk()
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const current = NAV.find(n => pathname.startsWith(n.to))
|
const current = NAV.find(n => pathname.startsWith(n.to))
|
||||||
|
// Nav groups stay closed unless they hold the current page; a click on the label overrides that until the next route.
|
||||||
|
const [toggled, setToggled] = useState<Partial<Record<Key, boolean>>>({})
|
||||||
|
useEffect(() => setToggled({}), [pathname])
|
||||||
const name = user?.fullName || user?.primaryEmailAddress?.emailAddress || '-'
|
const name = user?.fullName || user?.primaryEmailAddress?.emailAddress || '-'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -143,14 +146,22 @@ export function AppLayout() {
|
|||||||
<NavLink to="/production"><Brand /></NavLink>
|
<NavLink to="/production"><Brand /></NavLink>
|
||||||
<OrgSwitcher />
|
<OrgSwitcher />
|
||||||
<nav aria-label={t('layout.mainNav')}>
|
<nav aria-label={t('layout.mainNav')}>
|
||||||
{NAV_GROUPS.map(g => [
|
{NAV_GROUPS.map(g => {
|
||||||
<div key={g.label} className="nav-label">{t(g.label)}</div>,
|
const open = toggled[g.label] ?? g.items.some(n => pathname.startsWith(n.to))
|
||||||
...g.items.map(n => (
|
return [
|
||||||
|
<button key={g.label} type="button" className="nav-label" aria-expanded={open} aria-controls={`nav-${g.label}`}
|
||||||
|
onClick={() => setToggled(s => ({ ...s, [g.label]: !open }))}>
|
||||||
|
{t(g.label)}<Icon name="chevron-down" />
|
||||||
|
</button>,
|
||||||
|
<div key={`${g.label}-items`} id={`nav-${g.label}`} className={open ? 'nav-items' : 'nav-items closed'}>
|
||||||
|
{g.items.map(n => (
|
||||||
<NavLink key={n.to} to={n.to} className={({ isActive }) => (isActive ? 'active' : undefined)}>
|
<NavLink key={n.to} to={n.to} className={({ isActive }) => (isActive ? 'active' : undefined)}>
|
||||||
<Icon name={n.icon} />{t(n.label)}
|
<Icon name={n.icon} />{t(n.label)}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
)),
|
))}
|
||||||
])}
|
</div>,
|
||||||
|
]
|
||||||
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
<div className="sidebar-help">
|
<div className="sidebar-help">
|
||||||
<Icon name="info" />
|
<Icon name="info" />
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const paths: Record<string, string> = {
|
|||||||
search: '<circle cx="10" cy="10" r="6"/><path d="m15 15 5 5"/>',
|
search: '<circle cx="10" cy="10" r="6"/><path d="m15 15 5 5"/>',
|
||||||
info: '<circle cx="12" cy="12" r="9"/><path d="M12 11v6M12 7v.2"/>',
|
info: '<circle cx="12" cy="12" r="9"/><path d="M12 11v6M12 7v.2"/>',
|
||||||
check: '<path d="m5 12 4 4L19 6"/>',
|
check: '<path d="m5 12 4 4L19 6"/>',
|
||||||
|
'chevron-down': '<path d="m6 9 6 6 6-6"/>',
|
||||||
'chevron-updown': '<path d="m8 9 4-4 4 4M8 15l4 4 4-4"/>',
|
'chevron-updown': '<path d="m8 9 4-4 4 4M8 15l4 4 4-4"/>',
|
||||||
logout:'<path d="M10 4H4v16h6M10 12h11m-4-4 4 4-4 4"/>',
|
logout:'<path d="M10 4H4v16h6M10 12h11m-4-4 4 4-4 4"/>',
|
||||||
plus: '<path d="M12 4v16M4 12h16"/>',
|
plus: '<path d="M12 4v16M4 12h16"/>',
|
||||||
|
|||||||
@@ -287,10 +287,15 @@ dialog::backdrop{background:rgba(16,24,40,.45);backdrop-filter:blur(4px)}
|
|||||||
@keyframes page-in{from{opacity:0;transform:translateY(6px)}}
|
@keyframes page-in{from{opacity:0;transform:translateY(6px)}}
|
||||||
@media (prefers-reduced-motion:reduce){.skel,.app main{animation:none}}
|
@media (prefers-reduced-motion:reduce){.skel,.app main{animation:none}}
|
||||||
|
|
||||||
/* Sidebar groups: Dashboard, then Settings */
|
/* Sidebar groups: Dashboard, then Settings, each a dropdown that opens on its active page */
|
||||||
.sidebar nav .nav-label{margin:0 12px 6px}
|
.sidebar nav button.nav-label{display:flex;align-items:center;justify-content:space-between;width:auto;margin:0 0 2px;padding:6px 12px;border-radius:8px;color:var(--muted);font-size:11px;font-weight:600;box-shadow:none;background:none}
|
||||||
.sidebar nav .nav-label~.nav-label{margin-top:18px}
|
.sidebar nav button.nav-label:hover{background:#f2f7ff;color:var(--ink)}
|
||||||
@media(max-width:700px){.sidebar nav .nav-label{display:none}}
|
.sidebar nav button.nav-label svg{width:14px;height:14px;color:inherit;transition:transform .15s}
|
||||||
|
.sidebar nav button.nav-label[aria-expanded=false] svg{transform:rotate(-90deg)}
|
||||||
|
.sidebar nav .nav-items~.nav-label{margin-top:12px}
|
||||||
|
.nav-items{display:contents}
|
||||||
|
@media(min-width:701px){.nav-items.closed{display:none}}
|
||||||
|
@media(max-width:700px){.sidebar nav button.nav-label{display:none}}
|
||||||
|
|
||||||
/* Production: the knowledge graph, then the metric row and the latest content */
|
/* Production: the knowledge graph, then the metric row and the latest content */
|
||||||
.prod-metrics{grid-template-columns:repeat(3,minmax(0,1fr));margin-top:24px}
|
.prod-metrics{grid-template-columns:repeat(3,minmax(0,1fr));margin-top:24px}
|
||||||
|
|||||||
Reference in New Issue
Block a user