feat(desktop): desktop design catchup (#5703)

* feat(desktop): match app grid and folder design with design

Signed-off-by: Nixieboluo <me@sagirii.me>

* feat(desktop): make folder draw area larger

Signed-off-by: Nixieboluo <me@sagirii.me>

* refactor(desktop): calculate app height using the height of 2 lines app name

Signed-off-by: Nixieboluo <me@sagirii.me>

* feat(desktop): add menu for secondary links on narrow screens

Signed-off-by: Nixieboluo <me@sagirii.me>

* chore(desktop): clean up

Signed-off-by: Nixieboluo <me@sagirii.me>

* feat(desktop): tweak pin input corner radius to match the design

Signed-off-by: Nixieboluo <me@sagirii.me>

* fix(desktop): incorrect desktop folder paging calculation

Signed-off-by: Nixieboluo <me@sagirii.me>

* fix(desktop): navbar overflows on very narrow screens

Signed-off-by: Nixieboluo <me@sagirii.me>

---------

Signed-off-by: Nixieboluo <me@sagirii.me>
This commit is contained in:
Nixieboluo
2025-07-31 17:17:43 +08:00
committed by Yun Pan
parent 260de637e5
commit d3764bf4b5
6 changed files with 375 additions and 195 deletions
@@ -0,0 +1,244 @@
import { useConfigStore } from '@/stores/config';
import useAppStore from '@/stores/app';
import { useGuideModalStore } from '@/stores/guideModal';
import {
Box,
Center,
Divider,
Flex,
Text,
useBreakpointValue,
Menu,
MenuButton,
MenuList,
MenuItem
} from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { useQuery } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react';
import { getAmount } from '@/api/auth';
import Decimal from 'decimal.js';
import { CurrencySymbol } from '@sealos/ui';
import { formatMoney } from '@/utils/format';
import useSessionStore from '@/stores/session';
import { MoreHorizontal } from 'lucide-react';
const baseItemStyle = {
minW: '36px',
h: '40px',
fontSize: '14px',
fontWeight: '500',
color: 'primary',
_hover: {
background: 'secondary'
}
};
export default function SecondaryLinks() {
const { layoutConfig } = useConfigStore();
const { t } = useTranslation();
const { openGuideModal, setInitGuide } = useGuideModalStore();
const { openDesktopApp } = useAppStore();
const { session } = useSessionStore();
const user = session?.user;
const isCollapsed = useBreakpointValue({
base: true,
lg: false
});
const openWorkOrderApp = () => {
openDesktopApp({
appKey: 'system-workorder',
pathname: '/'
});
};
const openCostCenterApp = () => {
openDesktopApp({
appKey: 'system-costcenter',
pathname: '/'
});
};
const { data } = useQuery({
queryKey: ['getAmount', { userId: user?.userCrUid }],
queryFn: getAmount,
enabled: !!user,
staleTime: 60 * 1000
});
const balance = useMemo(() => {
let realBalance = new Decimal(data?.data?.balance || 0);
if (data?.data?.deductionBalance) {
realBalance = realBalance.minus(new Decimal(data.data.deductionBalance));
}
return realBalance.toNumber();
}, [data]);
const handleGuideClick = () => {
openGuideModal();
setInitGuide(false);
};
const handleDocsClick = () => {
if (layoutConfig?.common.docsUrl) {
window.open(layoutConfig.common.docsUrl);
}
};
if (!isCollapsed) {
return (
<Flex gap={'4px'} ml={'auto'}>
{layoutConfig?.version === 'cn' && (
<Center
mr={'12px'}
borderRadius={'8px'}
bg={
'linear-gradient(90deg, rgba(129, 203, 252, 0.12) 0%, rgba(81, 159, 245, 0.12) 100%)'
}
h={'36px'}
px={'12px'}
py={'8px'}
color="#2563EB"
fontSize={'14px'}
fontWeight={'500'}
cursor={'pointer'}
onClick={openCostCenterApp}
>
<Text>{t('common:balance')}</Text>
<Divider orientation="vertical" mx={'12px'} />
<CurrencySymbol />
<Text ml={'4px'}>{formatMoney(balance).toFixed(2)}</Text>
</Center>
)}
<Center
className="guide-button"
cursor={'pointer'}
{...baseItemStyle}
px={'8px'}
borderRadius={'8px'}
onClick={handleGuideClick}
>
{t('common:guide')}
</Center>
{layoutConfig?.common.docsUrl && (
<Center
{...baseItemStyle}
cursor={'pointer'}
borderRadius={'8px'}
px={'8px'}
onClick={handleDocsClick}
>
{t('common:doc')}
</Center>
)}
{layoutConfig?.version === 'cn' && (
<Center
cursor={'pointer'}
{...baseItemStyle}
px={'8px'}
borderRadius={'8px'}
border={'1px solid transparent'}
onClick={openWorkOrderApp}
>
{t('v2:support')}
</Center>
)}
</Flex>
);
}
return (
<Menu>
<MenuButton
as={Center}
{...baseItemStyle}
tabIndex={0}
boxSize={'36px'}
cursor={'pointer'}
borderRadius={'full'}
border={'1px solid rgba(0, 0, 0, 0.05)'}
px={'8px'}
>
<MoreHorizontal size={16} />
</MenuButton>
<MenuList
p="8px"
borderRadius="12px"
boxShadow="0 4px 20px rgba(0, 0, 0, 0.15)"
border="1px solid rgba(0, 0, 0, 0.05)"
minW="200px"
>
{layoutConfig?.version === 'cn' && (
<>
<MenuItem
borderRadius="8px"
_hover={{ bg: 'rgba(129, 203, 252, 0.12)' }}
onClick={openCostCenterApp}
bg={
'linear-gradient(90deg, rgba(129, 203, 252, 0.12) 0%, rgba(81, 159, 245, 0.12) 100%)'
}
color="#2563EB"
fontSize="14px"
fontWeight="500"
justifyContent="center"
>
<Flex height={'16px'} alignItems="center" my="8px" px="12px">
<Text>{t('common:balance')}</Text>
<Divider orientation="vertical" mx="8px" />
<CurrencySymbol />
<Text ml="4px">{formatMoney(balance).toFixed(2)}</Text>
</Flex>
</MenuItem>
<Divider my="8px" mx="-8px" w="calc(100% + 16px)" bg="rgba(0, 0, 0, 0.1)" />
</>
)}
<MenuItem
py="8px"
px="12px"
borderRadius="8px"
_hover={{ bg: 'rgba(0, 0, 0, 0.05)' }}
onClick={handleGuideClick}
fontSize="14px"
fontWeight="500"
>
{t('common:guide')}
</MenuItem>
{layoutConfig?.common.docsUrl && (
<MenuItem
py="8px"
px="12px"
borderRadius="8px"
_hover={{ bg: 'rgba(0, 0, 0, 0.05)' }}
onClick={handleDocsClick}
fontSize="14px"
fontWeight="500"
>
{t('common:doc')}
</MenuItem>
)}
{layoutConfig?.version === 'cn' && (
<MenuItem
py="8px"
px="12px"
borderRadius="8px"
_hover={{ bg: 'rgba(0, 0, 0, 0.05)' }}
onClick={openWorkOrderApp}
fontSize="14px"
fontWeight="500"
>
{t('v2:support')}
</MenuItem>
)}
</MenuList>
</Menu>
);
}
+10 -124
View File
@@ -18,10 +18,10 @@ import {
useColorMode,
useDisclosure
} from '@chakra-ui/react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import RegionToggle from '../region/RegionToggle';
import WorkspaceToggle from '../team/WorkspaceToggle';
import useAppStore from '@/stores/app';
@@ -38,11 +38,9 @@ import {
} from 'lucide-react';
import AccountCenter from './AccountCenter';
import { useLanguageSwitcher } from '@/hooks/useLanguageSwitcher';
import { getAmount } from '@/api/auth';
import Decimal from 'decimal.js';
import { CopyIcon, CurrencySymbol } from '@sealos/ui';
import { CopyIcon } from '@sealos/ui';
import { useGuideModalStore } from '@/stores/guideModal';
import { formatMoney } from '@/utils/format';
import SecondaryLinks from '../SecondaryLinks';
const baseItemStyle = {
minW: '36px',
@@ -72,10 +70,6 @@ export default function Account() {
const { toggleLanguage, currentLanguage } = useLanguageSwitcher();
const onAmount = useCallback((amount: number) => setNotificationAmount(amount), []);
const [showNsId, setShowNsId] = useState(false);
const linksCollapsed = useBreakpointValue({
base: true,
md: false
});
const logout = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
@@ -85,19 +79,6 @@ export default function Account() {
setToken('');
};
const openWorkOrderApp = () => {
const workorder = installedApps.find((t) => t.key === 'system-workorder');
if (!workorder) return;
openApp(workorder);
};
const openCostCenterApp = () => {
openDesktopApp({
appKey: 'system-costcenter',
pathname: '/'
});
};
const openReferralApp = () => {
openDesktopApp({
appKey: 'system-invite',
@@ -105,21 +86,6 @@ export default function Account() {
});
};
const { data } = useQuery({
queryKey: ['getAmount', { userId: user?.userCrUid }],
queryFn: getAmount,
enabled: !!user,
staleTime: 60 * 1000
});
const balance = useMemo(() => {
let realBalance = new Decimal(data?.data?.balance || 0);
if (data?.data?.deductionBalance) {
realBalance = realBalance.minus(new Decimal(data.data.deductionBalance));
}
return realBalance.toNumber();
}, [data]);
useEffect(() => {
if (initGuide) {
openGuideModal();
@@ -127,9 +93,9 @@ export default function Account() {
}, [initGuide, openGuideModal]);
return (
<Box position={'relative'} flex={1}>
<Flex alignItems={'center'} height={'100%'} zIndex={3}>
<Flex alignItems={'center'} flexGrow={1}>
<Box position={'relative'} flex={1} w={'full'}>
<Flex alignItems={'center'} height={'100%'} zIndex={3} w={'full'}>
<Flex alignItems={'center'} flex={'1 1 auto'} minW={0}>
<Center
display={{
base: 'none',
@@ -168,72 +134,6 @@ export default function Account() {
<WorkspaceToggle />
</Flex>
{!linksCollapsed && (
<Flex gap={'4px'} ml={'auto'}>
{layoutConfig?.version === 'cn' && (
<Center
mr={'12px'}
borderRadius={'8px'}
bg={
'linear-gradient(90deg, rgba(129, 203, 252, 0.12) 0%, rgba(81, 159, 245, 0.12) 100%)'
}
h={'36px'}
px={'12px'}
py={'8px'}
color="#2563EB"
fontSize={'14px'}
fontWeight={'500'}
cursor={'pointer'}
onClick={openCostCenterApp}
>
<Text>{t('common:balance')}</Text>
<Divider orientation="vertical" mx={'12px'} />
<CurrencySymbol />
<Text ml={'4px'}>{formatMoney(balance).toFixed(2)}</Text>
</Center>
)}
<Center
className="guide-button"
cursor={'pointer'}
{...baseItemStyle}
px={'8px'}
borderRadius={'8px'}
onClick={() => {
openGuideModal();
setInitGuide(false);
}}
>
{t('common:guide')}
</Center>
{layoutConfig?.common.docsUrl && (
<Center
{...baseItemStyle}
cursor={'pointer'}
borderRadius={'8px'}
px={'8px'}
onClick={() => window.open(layoutConfig?.common?.docsUrl)}
>
{t('common:doc')}
</Center>
)}
{layoutConfig?.version === 'cn' && (
<Center
cursor={'pointer'}
{...baseItemStyle}
px={'8px'}
borderRadius={'8px'}
border={'1px solid transparent'}
onClick={openWorkOrderApp}
>
{t('v2:support')}
</Center>
)}
</Flex>
)}
<Flex
ml={'20px'}
gap={'12px'}
@@ -258,6 +158,8 @@ export default function Account() {
{/* <ThemeToggle /> */}
<SecondaryLinks />
<Notification key={'notification'} disclosure={showDisclosure} onAmount={onAmount}>
<Center
{...baseItemStyle}
@@ -375,23 +277,7 @@ export default function Account() {
</Text>
</Flex>
</MenuItem> */}
<MenuItem
_focus={{ bg: 'transparent' }}
py="6px"
px="8px"
borderRadius="8px"
_hover={{ bg: '#F4F4F5' }}
onClick={openCostCenterApp}
>
<Flex alignItems="center" gap="8px">
<Center w="20px" h="20px">
<CreditCard size={16} color="#737373" />
</Center>
<Text fontSize="14px" fontWeight="400">
{t('common:billing')}
</Text>
</Flex>
</MenuItem>
{layoutConfig?.version === 'cn' && (
<MenuItem
mt="0px"
@@ -14,7 +14,8 @@ import {
ModalBody,
Center,
useBreakpointValue,
GridProps
GridProps,
FlexProps
} from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import {
@@ -57,6 +58,9 @@ const AppItem = ({
justifyContent={'flex-start'}
alignItems={'center'}
className={app.key}
w="fit-content"
h="fit-content"
maxW="100%"
>
<Center
w={{ base: '64px', sm: '78px' }}
@@ -68,6 +72,7 @@ const AppItem = ({
overflow={'hidden'}
_hover={{ transform: 'scale(1.05)' }}
cursor={'pointer'}
flexShrink={0}
style={{
touchAction: 'none',
userSelect: 'none'
@@ -97,7 +102,11 @@ const AppItem = ({
fontSize={'14px'}
fontWeight={500}
textAlign={'center'}
lineHeight={'normal'}
lineHeight={'18px'}
flexShrink={0}
maxW="100%"
wordBreak="break-word"
noOfLines={2}
style={{
touchAction: 'none',
userSelect: 'none'
@@ -112,12 +121,16 @@ const AppItem = ({
const AppGrid = ({
children,
gridGap,
rows,
appHeight,
columns,
gridProps
}: {
children: ReactNode;
gridGap?: number;
appHeight?: number;
rows: number;
appHeight: number;
columns: number;
gridProps?: Omit<
GridProps,
'templateColumns' | 'templateRows' | 'gap' | 'flexGrow' | 'flexShrink' | 'flexBasis'
@@ -129,13 +142,12 @@ const AppGrid = ({
flexShrink={0}
flexGrow={0}
flexBasis={'100%'}
gap={`${gridGap}px`}
templateColumns={{
base: 'repeat(2, 1fr)',
sm: 'repeat(3, 1fr)',
lg: 'repeat(5, 1fr)'
}}
templateRows={`repeat(auto-fill, ${appHeight}px)`}
gap={`${gridGap ?? 0}px`}
templateColumns={`repeat(${columns}, 1fr)`}
templateRows={`repeat(${rows}, minmax(${appHeight}px, auto))`}
justifyItems={'center'}
alignContent={'space-between'}
justifyContent={'space-between'}
className="apps-container"
>
{children}
@@ -147,29 +159,27 @@ const AppGridPagingContainer = ({
children,
gridGap,
appHeight,
columns,
totalPages,
currentPage,
handleNavigation,
pageGap,
onChange
onChange,
dragContainerProps
}: {
children: ReactNode;
dragContainerProps?: FlexProps;
gridGap: number;
appHeight: number;
columns: number;
totalPages: number;
currentPage: number;
handleNavigation: boolean;
pageGap: number;
onChange: (currentPage: number, pageSize: number) => void;
}) => {
const gridWrapperRef = useRef<HTMLDivElement>(null);
const columns =
useBreakpointValue({
base: 2,
sm: 3,
lg: 5
}) ?? 2;
const dragContainerRef = useRef<HTMLDivElement>(null);
const gridContainerRef = useRef<HTMLDivElement>(null);
const [pageWidth, setPageWidth] = useState(0);
const [itemsPerPage, setItemsPerPage] = useState(8);
@@ -185,18 +195,19 @@ const AppGridPagingContainer = ({
: (pageWidth + pageGap) * clampedCurrentPage - dragDelta;
const calculateItemsPerPage = useCallback(() => {
if (!gridWrapperRef.current) return 8;
if (!gridContainerRef.current) return 8;
const height = gridWrapperRef.current.clientHeight;
const rows = Math.floor((height + gridGap) / (appHeight + gridGap));
const height = gridContainerRef.current.clientHeight;
// At least 1 row in a grid.
const rows = Math.max(1, Math.floor((height + gridGap) / (appHeight + gridGap)));
return rows * columns;
}, [columns, gridGap, appHeight]);
const calculatePageWidth = useCallback(() => {
if (!gridWrapperRef.current) return 0;
if (!gridContainerRef.current) return 0;
return (gridWrapperRef.current.scrollWidth + pageGap) / totalPages - pageGap;
return (gridContainerRef.current.scrollWidth + pageGap) / totalPages - pageGap;
}, [totalPages, pageGap]);
// Calculate items per page and scroll position on initial render and on screen size changes
@@ -266,11 +277,11 @@ const AppGridPagingContainer = ({
// Pointer drag pagination
useEffect(() => {
const wrapper = gridWrapperRef.current;
const wrapper = dragContainerRef.current;
if (!wrapper) return;
const handlePointerDown = (e: PointerEvent) => {
if (!gridWrapperRef.current) return;
if (!dragContainerRef.current) return;
setDragStartX(e.screenX);
setDragStartY(e.screenY);
@@ -332,17 +343,19 @@ const AppGridPagingContainer = ({
]);
return (
<Flex
ref={gridWrapperRef}
h="full"
w="full"
transition="transform 0.2s ease-out"
gap={`${pageGap}px`}
style={{
transform: `translateX(-${scrollPosition}px)`
}}
>
{children}
<Flex ref={dragContainerRef} w="full" h="full" {...dragContainerProps}>
<Flex
ref={gridContainerRef}
h="full"
w="full"
transition="transform 0.2s ease-out"
gap={`${pageGap}px`}
style={{
transform: `translateX(-${scrollPosition}px)`
}}
>
{children}
</Flex>
</Flex>
);
};
@@ -477,12 +490,12 @@ const PageSwitcher = ({
onChange: (target: number) => void;
}) => {
return (
<Flex justifyContent="center" alignItems="center" gap="8px">
<Flex justifyContent="center" alignItems="center" gap="11px">
{Array.from({ length: pages }).map((_, index) => (
<Box
key={index}
w="12px"
h="12px"
w="10px"
h="10px"
borderRadius="50%"
bg={index === current ? 'gray.400' : 'gray.200'}
cursor="pointer"
@@ -507,11 +520,19 @@ export default function Apps() {
const folderRef = useRef<HTMLDivElement>(null);
const modalContentRef = useRef<HTMLDivElement>(null);
// min = 1 line app name
// max = 2 lines app name
const appHeight =
useBreakpointValue({
base: 114,
md: 146
}) ?? 114;
base: 112,
md: 126
}) ?? 112;
const columns =
useBreakpointValue({
base: 2,
sm: 3,
lg: 5
}) ?? 2;
const gridGap = 10;
const [itemsPerPageInGrid, setItemsPerPageInGrid] = useState(0);
@@ -801,6 +822,7 @@ export default function Apps() {
<AppGridPagingContainer
gridGap={gridGap}
appHeight={appHeight}
columns={columns}
// One page desktop, other apps are in folder
totalPages={1}
currentPage={currentPageInGrid}
@@ -812,7 +834,13 @@ export default function Apps() {
}}
>
{desktopPages.map((page, pageIndex) => (
<AppGrid key={pageIndex} gridGap={gridGap} appHeight={appHeight}>
<AppGrid
key={pageIndex}
gridGap={gridGap}
rows={itemsPerPageInGrid / columns}
appHeight={appHeight}
columns={columns}
>
{page.map((app, index) => (
<AppItem
key={index}
@@ -839,34 +867,46 @@ export default function Apps() {
<ModalContent
ref={modalContentRef}
maxH={'90vh'}
maxW={'90vw'}
height={'720px'}
borderRadius="16px"
maxW={'min(90vw, 1240px)'}
height={'min(calc(100vh - 80px), 720px)'}
borderRadius="32px"
boxShadow="0 4px 20px rgba(0, 0, 0, 0.15)"
border="1px solid rgba(0, 0, 0, 0.05)"
p="0"
>
<ModalCloseButton
top="16px"
right="24px"
top="40px"
right="40px"
padding={'12.5px'}
size={'20px'}
border={'1.25px solid #E4E4E7'}
borderRadius={'full'}
color="#18181B"
_hover={{ bg: 'rgba(0, 0, 0, 0.05)' }}
/>
<ModalBody
py={{
base: '36px',
xl: '100px'
}}
px={{
base: '12px',
xl: '100px'
}}
overflow="hidden"
className="folder-modal-body"
>
<ModalBody p="0" maxH={'full'}>
<AppGridPagingContainer
dragContainerProps={{
pt: {
base: '112.5px',
sm: '124.5px'
},
pb: {
base: '112.5px',
sm: '144.5px'
},
px: {
base: '31.5px',
sm: '60px',
md: '64px',
lg: '82px',
xl: '90px'
},
overflow: 'hidden'
}}
gridGap={gridGap}
appHeight={appHeight!}
appHeight={appHeight}
columns={columns}
totalPages={totalPagesInFolder}
currentPage={currentPageInFolder}
handleNavigation={isFolderOpen}
@@ -874,14 +914,16 @@ export default function Apps() {
onChange={(currentPage, pageSize) => {
setCurrentPageInFolder(currentPage);
setItemsPerPageInFolder(pageSize);
setTotalPagesInFolder(Math.ceil(moreApps.length / pageSize));
setTotalPagesInFolder(Math.ceil(folderApps.length / pageSize));
}}
>
{folderPages.map((page, pageIndex) => (
<AppGrid
key={pageIndex}
gridGap={gridGap}
rows={itemsPerPageInFolder / columns}
appHeight={appHeight}
columns={columns}
gridProps={{
alignContent: 'center'
}}
@@ -899,7 +941,14 @@ export default function Apps() {
))}
</AppGridPagingContainer>
<Box position="absolute" bottom="16px" left="0" right="0">
<Box
position="absolute"
w="fit-content"
bottom="32px"
left="0"
right="0"
marginInline={'auto'}
>
<PageSwitcher
pages={totalPagesInFolder}
current={currentPageInFolder}
@@ -81,10 +81,13 @@ export default function WorkspaceToggle() {
pl={'12px'}
pr={'8px'}
bg={isOpen ? 'secondary' : ''}
minW={'0'}
flex={'0 1 auto'}
tabIndex={0}
>
{namespace?.id && (
<Box
flex={'0 0 auto'}
boxSize={{
base: '20px',
sm: '24px'
@@ -102,11 +105,9 @@ export default function WorkspaceToggle() {
fontSize={'14px'}
fontWeight={'500'}
textTransform={'capitalize'}
maxW={{
base: '30px',
xs: '80px',
sm: 'fit-content'
}}
flex={'0 1 auto'}
wordBreak={'break-all'}
minW={'2ch'}
overflow={'hidden'}
textOverflow={'ellipsis'}
whiteSpace={'nowrap'}
@@ -226,7 +226,7 @@ export default function EmailCheckComponent() {
mr={{ base: '4px', lg: '8px' }}
boxSize={{ base: '40px', lg: '56px' }}
fontSize={{ base: '16px', lg: '20px' }}
borderRadius={{ base: '8px', lg: '12px' }}
borderRadius={'12px'}
/>
))}
</PinInput>
@@ -289,7 +289,7 @@ export default function PhoneCheckComponent() {
mr={{ base: '4px', lg: '8px' }}
boxSize={{ base: '40px', lg: '56px' }}
fontSize={{ base: '16px', lg: '20px' }}
borderRadius={{ base: '8px', lg: '12px' }}
borderRadius={'12px'}
/>
))}
</PinInput>