Consolidate WebsiteNavSection into WebsiteNav and fix scrollbar flash.

Move sidebar rendering logic into WebsiteNav with isCollapsed prop so
SideNav uses WebsiteNav directly instead of duplicating code. Add
overflow hidden to prevent horizontal scrollbar during expand transition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mike Cao 2026-02-06 04:27:37 -08:00
parent dea3c41ecc
commit 3467184b8f
2 changed files with 91 additions and 114 deletions

View file

@ -1,5 +1,4 @@
import {
Box,
Button,
type ButtonProps,
Column,
@ -13,24 +12,17 @@ import {
} from '@umami/react-zen';
import Link from 'next/link';
import type { Key } from 'react';
import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
import { IconLabel } from '@/components/common/IconLabel';
import { useGlobalState, useMessages, useNavigation, useWebsiteNavItems } from '@/components/hooks';
import {
ArrowLeft,
Globe,
Grid2x2,
LayoutDashboard,
LinkIcon,
PanelLeft,
} from '@/components/icons';
import { useGlobalState, useMessages, useNavigation } from '@/components/hooks';
import { Globe, Grid2x2, LayoutDashboard, LinkIcon, PanelLeft } from '@/components/icons';
import { LanguageButton } from '@/components/input/LanguageButton';
import { NavButton } from '@/components/input/NavButton';
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
import { Logo } from '@/components/svg';
export function SideNav(props: any) {
const { formatMessage, labels } = useMessages();
const { pathname, renderUrl, websiteId, teamId, router } = useNavigation();
const { pathname, renderUrl, websiteId, router } = useNavigation();
const [isCollapsed, setIsCollapsed] = useGlobalState('sidenav-collapsed');
const hasNav = !!(websiteId || pathname.startsWith('/admin') || pathname.includes('/settings'));
@ -66,10 +58,6 @@ export function SideNav(props: any) {
router.push(id === 'user' ? '/websites' : `/teams/${id}/websites`);
};
const handleWebsiteChange = (value: string) => {
router.push(renderUrl(`/websites/${value}`));
};
return (
<Column
{...props}
@ -83,9 +71,10 @@ export function SideNav(props: any) {
style={{
width: isCollapsed ? '55px' : '240px',
transition: 'width 0.2s ease-in-out',
overflow: 'hidden',
}}
>
<Column overflowY="auto" style={{ minHeight: 0 }}>
<Column style={{ minHeight: 0, overflowY: 'auto', overflowX: 'hidden' }}>
<Row
alignItems="center"
justifyContent="space-between"
@ -105,12 +94,7 @@ export function SideNav(props: any) {
<NavButton showText={!isCollapsed} onAction={handleSelect} />
</Row>
{websiteId ? (
<WebsiteNavSection
websiteId={websiteId}
teamId={teamId}
isCollapsed={isCollapsed}
onWebsiteChange={handleWebsiteChange}
/>
<WebsiteNav websiteId={websiteId} isCollapsed={isCollapsed} />
) : (
<Column gap="2">
{links.map(({ id, path, label, icon }) => {
@ -143,95 +127,6 @@ export function SideNav(props: any) {
);
}
function WebsiteNavSection({
websiteId,
teamId,
isCollapsed,
onWebsiteChange,
}: {
websiteId: string;
teamId: string;
isCollapsed: boolean;
onWebsiteChange: (value: string) => void;
}) {
const { formatMessage, labels } = useMessages();
const { renderUrl } = useNavigation();
const { items, selectedKey } = useWebsiteNavItems(websiteId);
const renderValue = (value: any) => {
return (
<Text truncate style={{ maxWidth: 160, lineHeight: 1 }}>
{value?.selectedItem?.name}
</Text>
);
};
return (
<Column gap="2">
<Link href={renderUrl('/websites', false)} role="button">
<TooltipTrigger isDisabled={!isCollapsed} delay={0}>
<Focusable>
<Row
alignItems="center"
hover={{ backgroundColor: 'surface-sunken' }}
borderRadius
minHeight="40px"
>
<IconLabel
icon={<ArrowLeft />}
label={isCollapsed ? '' : formatMessage(labels.back)}
padding
/>
</Row>
</Focusable>
<Tooltip placement="right">{formatMessage(labels.back)}</Tooltip>
</TooltipTrigger>
</Link>
{!isCollapsed && (
<Box marginBottom="2">
<WebsiteSelect
websiteId={websiteId}
teamId={teamId}
onChange={onWebsiteChange}
renderValue={renderValue}
buttonProps={{ style: { outline: 'none' } }}
/>
</Box>
)}
{items.map(({ label: sectionLabel, items: sectionItems }, index) => (
<Column key={`${sectionLabel}${index}`} gap="1" marginBottom="1">
{!isCollapsed && (
<Row padding>
<Text weight="bold">{sectionLabel}</Text>
</Row>
)}
{sectionItems.map(({ id, path, label, icon }) => {
const isSelected = selectedKey === id;
return (
<Link key={id} href={path} role="button">
<TooltipTrigger isDisabled={!isCollapsed} delay={0}>
<Focusable>
<Row
alignItems="center"
hover={{ backgroundColor: 'surface-sunken' }}
backgroundColor={isSelected ? 'surface-sunken' : undefined}
borderRadius
minHeight="40px"
>
<IconLabel icon={icon} label={isCollapsed ? '' : label} padding />
</Row>
</Focusable>
<Tooltip placement="right">{label}</Tooltip>
</TooltipTrigger>
</Link>
);
})}
</Column>
))}
</Column>
);
}
const PanelButton = (props: ButtonProps) => {
const [isCollapsed, setIsCollapsed] = useGlobalState('sidenav-collapsed');
return (

View file

@ -1,15 +1,30 @@
import { Column, Text } from '@umami/react-zen';
import {
Box,
Column,
Focusable,
Label,
Row,
Text,
Tooltip,
TooltipTrigger,
} from '@umami/react-zen';
import Link from 'next/link';
import { IconLabel } from '@/components/common/IconLabel';
import { NavMenu } from '@/components/common/NavMenu';
import { useNavigation, useWebsiteNavItems } from '@/components/hooks';
import { useMessages, useNavigation, useWebsiteNavItems } from '@/components/hooks';
import { ArrowLeft } from '@/components/icons';
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
export function WebsiteNav({
websiteId,
isCollapsed,
onItemClick,
}: {
websiteId: string;
isCollapsed?: boolean;
onItemClick?: () => void;
}) {
const { formatMessage, labels } = useMessages();
const { teamId, router, renderUrl } = useNavigation();
const { items, selectedKey } = useWebsiteNavItems(websiteId);
@ -25,6 +40,73 @@ export function WebsiteNav({
);
};
if (isCollapsed !== undefined) {
return (
<Column gap="2">
<Link href={renderUrl('/websites', false)} role="button">
<TooltipTrigger isDisabled={!isCollapsed} delay={0}>
<Focusable>
<Row
alignItems="center"
hover={{ backgroundColor: 'surface-sunken' }}
borderRadius
minHeight="40px"
>
<IconLabel
icon={<ArrowLeft />}
label={isCollapsed ? '' : formatMessage(labels.back)}
padding
/>
</Row>
</Focusable>
<Tooltip placement="right">{formatMessage(labels.back)}</Tooltip>
</TooltipTrigger>
</Link>
{!isCollapsed && (
<Box marginBottom="2">
<WebsiteSelect
websiteId={websiteId}
teamId={teamId}
onChange={handleChange}
renderValue={renderValue}
buttonProps={{ style: { outline: 'none' } }}
/>
</Box>
)}
{items.map(({ label: sectionLabel, items: sectionItems }, index) => (
<Column key={`${sectionLabel}${index}`} gap="1" marginBottom="1">
{!isCollapsed && (
<Row padding>
<Text weight="bold">{sectionLabel}</Text>
</Row>
)}
{sectionItems.map(({ id, path, label, icon }) => {
const isSelected = selectedKey === id;
return (
<Link key={id} href={path} role="button">
<TooltipTrigger isDisabled={!isCollapsed} delay={0}>
<Focusable>
<Row
alignItems="center"
hover={{ backgroundColor: 'surface-sunken' }}
backgroundColor={isSelected ? 'surface-sunken' : undefined}
borderRadius
minHeight="40px"
>
<IconLabel icon={icon} label={isCollapsed ? '' : label} padding />
</Row>
</Focusable>
<Tooltip placement="right">{label}</Tooltip>
</TooltipTrigger>
</Link>
);
})}
</Column>
))}
</Column>
);
}
return (
<Column padding="2" position="sticky" top="0" gap>
<WebsiteSelect