feat: unified header bar with Lux Network branding

- Redesign TopBar as single unified header: logo + nav + controls
- Add NetworkSelector dropdown (Mainnet/Testnet/Devnet)
- Monochrome ChainSwitcher (kill blue panel)
- Connect wallet button via UserProfileDesktop in header
- Remove NavBar and SideBar (nav now in TopBar)
- Simplify HeaderDesktop to search bar only
This commit is contained in:
2026-02-27 11:18:39 -08:00
parent 5fed458a21
commit dc5479a2b1
6 changed files with 203 additions and 42 deletions
+2 -5
View File
@@ -1,6 +1,3 @@
import config from 'configs/app';
import NavigationDesktop from 'ui/snippets/navigation/horizontal/NavigationDesktop';
const NavBar = () => null;
const EmptyComponent = () => null;
export default config.UI.navigation.layout === 'horizontal' ? NavigationDesktop : EmptyComponent;
export default NavBar;
+2 -5
View File
@@ -1,6 +1,3 @@
import config from 'configs/app';
import NavigationDesktop from 'ui/snippets/navigation/vertical/NavigationDesktop';
const SideBar = () => null;
const EmptyComponent = () => null;
export default config.UI.navigation.layout === 'horizontal' ? EmptyComponent : NavigationDesktop;
export default SideBar;
-9
View File
@@ -1,10 +1,7 @@
import { HStack, Box } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import RewardsButton from 'ui/rewards/RewardsButton';
import SearchBar from 'ui/snippets/searchBar/SearchBarDesktop';
import UserProfileDesktop from 'ui/snippets/user/UserProfileDesktop';
type Props = {
renderSearchBar?: () => React.ReactNode;
@@ -26,12 +23,6 @@ const HeaderDesktop = ({ renderSearchBar }: Props) => {
<Box width="100%">
{ searchBar }
</Box>
{ config.UI.navigation.layout === 'vertical' && (
<Box display="flex" gap={ 2 } flexShrink={ 0 }>
{ config.features.rewards.isEnabled && <RewardsButton/> }
<UserProfileDesktop buttonVariant="header"/>
</Box>
) }
</HStack>
);
};
+8 -10
View File
@@ -19,7 +19,7 @@ const CHAINS: ReadonlyArray<ChainItem> = [
{ name: 'Zoo', label: 'Zoo Chain', vm: 'Subnet EVM', url: 'https://explore-zoo.lux.network', isCurrent: false },
{ name: 'Hanzo', label: 'Hanzo AI', vm: 'Subnet EVM', url: 'https://explore-hanzo.lux.network', isCurrent: false },
{ name: 'SPC', label: 'SPC Chain', vm: 'Subnet EVM', url: 'https://explore-spc.lux.network', isCurrent: false },
{ name: 'Pars', label: 'Pars Network', vm: 'Subnet EVM', url: 'https://explore.pars.network', isCurrent: false },
{ name: 'Pars', label: 'Pars Network', vm: 'Subnet EVM', url: 'https://explore-pars.lux.network', isCurrent: false },
];
const ChainSwitcher = () => {
@@ -33,6 +33,8 @@ const ChainSwitcher = () => {
setOpen((prev) => !prev);
}, []);
const current = CHAINS.find((c) => c.isCurrent);
return (
<PopoverRoot
positioning={{ placement: 'bottom-end', offset: { mainAxis: 6 } }}
@@ -47,10 +49,10 @@ const ChainSwitcher = () => {
fontWeight={ 500 }
onClick={ handleToggle }
borderColor="border.divider"
px={ 3 }
px={ 2.5 }
>
<IconSvg name="networks" boxSize="14px"/>
<span>C-Chain</span>
<span>{ current?.name ?? 'C-Chain' }</span>
<IconSvg name="arrows/east-mini" boxSize={ 4 } transform="rotate(-90deg)"/>
</Button>
</PopoverTrigger>
@@ -72,7 +74,7 @@ const ChainSwitcher = () => {
px={ 3 }
py={ 2.5 }
cursor={ chain.isCurrent ? 'default' : 'pointer' }
_hover={ chain.isCurrent ? {} : { bg: { _light: 'gray.50', _dark: 'whiteAlpha.50' } } }
_hover={ chain.isCurrent ? {} : { bg: { _light: 'blackAlpha.50', _dark: 'whiteAlpha.50' } } }
transition="background 0.15s"
borderBottom="1px solid"
borderColor="border.divider"
@@ -85,11 +87,7 @@ const ChainSwitcher = () => {
{ chain.name }
</Text>
{ chain.isCurrent && (
<Box
bgColor="green.400"
borderRadius="full"
boxSize="6px"
/>
<Box boxSize="6px" borderRadius="full" bgColor="green.400"/>
) }
</Flex>
<Text fontSize="xs" color="text.secondary">
@@ -97,7 +95,7 @@ const ChainSwitcher = () => {
</Text>
</Flex>
<Box
bgColor={{ _light: 'gray.100', _dark: 'whiteAlpha.100' }}
bgColor={{ _light: 'blackAlpha.50', _dark: 'whiteAlpha.100' }}
color="text.secondary"
borderRadius="sm"
px={ 1.5 }
+101
View File
@@ -0,0 +1,101 @@
import { Box, Flex, Text } from '@chakra-ui/react';
import React from 'react';
import { Button } from 'toolkit/chakra/button';
import { PopoverBody, PopoverContent, PopoverRoot, PopoverTrigger } from 'toolkit/chakra/popover';
import IconSvg from 'ui/shared/IconSvg';
interface NetworkItem {
readonly name: string;
readonly label: string;
readonly url: string;
readonly isCurrent: boolean;
}
const NETWORKS: ReadonlyArray<NetworkItem> = [
{ name: 'Mainnet', label: 'Production network', url: 'https://explore.lux.network', isCurrent: true },
{ name: 'Testnet', label: 'Test network', url: 'https://explore.lux-test.network', isCurrent: false },
{ name: 'Devnet', label: 'Development network', url: 'https://explore.lux-dev.network', isCurrent: false },
];
const NetworkSelector = () => {
const [ open, setOpen ] = React.useState(false);
const handleOpenChange = React.useCallback(({ open: isOpen }: { open: boolean }) => {
setOpen(isOpen);
}, []);
const handleToggle = React.useCallback(() => {
setOpen((prev) => !prev);
}, []);
const current = NETWORKS.find((n) => n.isCurrent);
return (
<PopoverRoot
positioning={{ placement: 'bottom-end', offset: { mainAxis: 6 } }}
lazyMount
open={ open }
onOpenChange={ handleOpenChange }
>
<PopoverTrigger>
<Button
variant="outline"
size="2xs"
fontWeight={ 500 }
onClick={ handleToggle }
borderColor="border.divider"
px={ 2.5 }
>
<Box boxSize="6px" borderRadius="full" bgColor="green.400" flexShrink={ 0 }/>
<span>{ current?.name ?? 'Mainnet' }</span>
<IconSvg name="arrows/east-mini" boxSize={ 4 } transform="rotate(-90deg)"/>
</Button>
</PopoverTrigger>
<PopoverContent w="240px">
<PopoverBody p={ 0 }>
<Box px={ 3 } py={ 2 } borderBottom="1px solid" borderColor="border.divider">
<Text fontSize="xs" fontWeight={ 600 } color="text.secondary" textTransform="uppercase" letterSpacing="wider">
Network
</Text>
</Box>
{ NETWORKS.map((network) => (
<Box
key={ network.name }
as={ network.isCurrent ? 'div' : 'a' }
{ ...(!network.isCurrent ? { href: network.url } : {}) }
display="flex"
alignItems="center"
justifyContent="space-between"
px={ 3 }
py={ 2.5 }
cursor={ network.isCurrent ? 'default' : 'pointer' }
_hover={ network.isCurrent ? {} : { bg: { _light: 'blackAlpha.50', _dark: 'whiteAlpha.50' } } }
transition="background 0.15s"
borderBottom="1px solid"
borderColor="border.divider"
_last={{ borderBottom: 'none' }}
textDecoration="none"
>
<Flex direction="column">
<Flex align="center" gap={ 2 }>
<Text fontSize="sm" fontWeight={ 500 } color="text.primary">
{ network.name }
</Text>
{ network.isCurrent && (
<Box boxSize="6px" borderRadius="full" bgColor="green.400"/>
) }
</Flex>
<Text fontSize="xs" color="text.secondary">
{ network.label }
</Text>
</Flex>
</Box>
)) }
</PopoverBody>
</PopoverContent>
</PopoverRoot>
);
};
export default React.memo(NetworkSelector);
+90 -13
View File
@@ -1,33 +1,110 @@
import { Flex, Box, HStack } from '@chakra-ui/react';
import { Flex, Box, HStack, chakra } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import config from 'configs/app';
import { route } from 'nextjs-routes';
import { Link } from 'toolkit/chakra/link';
import { CONTENT_MAX_WIDTH } from 'ui/shared/layout/utils';
import UserProfileDesktop from 'ui/snippets/user/UserProfileDesktop';
import ChainSwitcher from './ChainSwitcher';
import DeFiDropdown from './DeFiDropdown';
import NetworkMenu from './NetworkMenu';
import NetworkSelector from './NetworkSelector';
interface NavLinkProps {
readonly text: string;
readonly href: string;
readonly isActive: boolean;
}
const NavLinkItem = ({ text, href, isActive }: NavLinkProps) => (
<Link
href={ href }
px={ 2.5 }
py={ 1.5 }
textStyle="sm"
fontWeight={ 500 }
borderRadius="base"
variant="navigation"
{ ...(isActive ? { 'data-selected': true } : {}) }
textDecoration="none"
whiteSpace="nowrap"
>
{ text }
</Link>
);
const TopBar = () => {
const hasDeFiDropdown = Boolean(config.features.deFiDropdown.isEnabled);
const router = useRouter();
const pathname = router.pathname;
const navItems = React.useMemo(() => [
{ text: 'Home', href: route({ pathname: '/' as const }), isActive: pathname === '/' },
{ text: 'Chains', href: route({ pathname: '/chains' as const }), isActive: pathname === '/chains' || pathname.startsWith('/chain/') },
{ text: 'Validators', href: route({ pathname: '/validators' as const }), isActive: pathname === '/validators' || pathname.startsWith('/validators/') },
{ text: 'Stats', href: route({ pathname: '/stats' as const }), isActive: pathname.startsWith('/stats') },
{ text: 'Bridge', href: route({ pathname: '/bridge' as const }), isActive: pathname === '/bridge' },
{ text: 'Blocks', href: route({ pathname: '/blocks' as const }), isActive: pathname === '/blocks' || pathname.startsWith('/block/') },
{ text: 'Txns', href: route({ pathname: '/txs' as const }), isActive: pathname === '/txs' || pathname.startsWith('/tx/') },
{ text: 'Tokens', href: route({ pathname: '/tokens' as const }), isActive: pathname === '/tokens' || pathname.startsWith('/token/') },
], [ pathname ]);
return (
// not ideal if scrollbar is visible, but better than having a horizontal scroll
<Box bgColor={{ _light: 'theme.topbar.bg._light', _dark: 'theme.topbar.bg._dark' }} position="sticky" left={ 0 } width="100%" maxWidth="100vw">
<Box
bgColor={{ _light: 'rgba(255, 255, 255, 0.95)', _dark: 'rgba(16, 17, 18, 0.95)' }}
position="sticky"
top={ 0 }
left={ 0 }
width="100%"
maxWidth="100vw"
zIndex="sticky"
borderBottom="1px solid"
borderColor="border.divider"
backdropFilter="blur(12px)"
>
<Flex
py={ 2 }
px={{ base: 3, lg: 6 }}
m="0 auto"
justifyContent="space-between"
alignItems="center"
maxW={ `${ CONTENT_MAX_WIDTH }px` }
gap={ 2 }
>
<HStack gap={ 2 } fontSize="xs">
{ Boolean(config.UI.featuredNetworks.items) && <NetworkMenu/> }
<ChainSwitcher/>
{ /* Logo */ }
<chakra.a
href={ route({ pathname: '/' as const }) }
display="flex"
alignItems="center"
gap="8px"
flexShrink={ 0 }
mr={ 4 }
aria-label="Lux Network home"
textDecoration="none"
_hover={{ textDecoration: 'none' }}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="20" height="20">
<polygon points="25,46.65 50,3.35 0,3.35" fill="currentColor"/>
</svg>
<chakra.span fontWeight={ 700 } fontSize="md" letterSpacing="-0.02em" whiteSpace="nowrap">
Lux Network
</chakra.span>
</chakra.a>
{ /* Navigation */ }
<HStack as="nav" gap={ 0 } display={{ base: 'none', lg: 'flex' }}>
{ navItems.map((item) => (
<NavLinkItem key={ item.text } { ...item }/>
)) }
</HStack>
<HStack alignItems="center" gap={ 2 }>
{ hasDeFiDropdown && <DeFiDropdown/> }
{ /* Spacer */ }
<Box flex={ 1 }/>
{ /* Right controls */ }
<HStack gap={ 2 } flexShrink={ 0 }>
<NetworkSelector/>
<ChainSwitcher/>
<UserProfileDesktop buttonSize="sm"/>
</HStack>
</Flex>
</Box>