diff --git a/packages/components/src/components/ResizableBox/ResizableBox.stories.tsx b/packages/components/src/components/ResizableBox/ResizableBox.stories.tsx index 1a530f6ddfb..61cfbca23a5 100644 --- a/packages/components/src/components/ResizableBox/ResizableBox.stories.tsx +++ b/packages/components/src/components/ResizableBox/ResizableBox.stories.tsx @@ -32,12 +32,14 @@ export const ResizableBox: StoryObj = { children: Resize me from any side!, directions: ['top', 'left', 'right', 'bottom'], isLocked: false, - width: 100, - minWidth: 50, - maxWidth: 300, + width: 240, + minWidth: 80, + maxWidth: 400, height: 100, minHeight: 50, maxHeight: 300, + disabledWidthInterval: [51, 100], + disabledHeightInterval: undefined, updateWidthOnWindowResize: false, updateHeightOnWindowResize: false, }, diff --git a/packages/components/src/components/ResizableBox/ResizableBox.tsx b/packages/components/src/components/ResizableBox/ResizableBox.tsx index 72fabc73862..dcc38349342 100644 --- a/packages/components/src/components/ResizableBox/ResizableBox.tsx +++ b/packages/components/src/components/ResizableBox/ResizableBox.tsx @@ -6,6 +6,8 @@ import { ZIndexValues, zIndices } from '@trezor/theme'; type Direction = 'top' | 'left' | 'right' | 'bottom'; type Directions = Array; +type DisabledInterval = [number, number]; + export type ResizableBoxProps = { children: React.ReactNode; directions: Directions; @@ -21,6 +23,10 @@ export type ResizableBoxProps = { zIndex?: ZIndexValues; onWidthResizeEnd?: (width: number) => void; onHeightResizeEnd?: (height: number) => void; + onWidthResizeMove?: (width: number) => void; + onHeightResizeMove?: (height: number) => void; + disabledWidthInterval?: DisabledInterval; + disabledHeightInterval?: DisabledInterval; }; type ResizerHandlersProps = { @@ -41,7 +47,6 @@ type ResizersProps = ResizerHandlersProps & { const MINIMAL_BOX_SIZE = 1; const REACTIVE_AREA_WIDTH = 16; const BORDER_WIDTH = 4; - const Resizers = styled.div( ({ $width, $minWidth, $maxWidth, $height, $minHeight, $maxHeight, $isResizing }) => ` ${$width ? `width: ${$width}px;` : 'width: auto;'}; @@ -52,11 +57,7 @@ const Resizers = styled.div( ${$maxHeight && `max-height: ${$maxHeight}px;`}; box-sizing: border-box; position: relative; - ${ - $isResizing && - `user-select: none; - -webkit-user-select: none;` - }; + ${$isResizing && `user-select: none; -webkit-user-select: none; cursor: ${$isResizing ? 'ns-resize' : 'auto'};`}; `, ); @@ -171,6 +172,36 @@ const getMinResult = (min: number, result: number) => (result > min ? result : m const getMaxResult = (max: number | undefined, result: number) => max === undefined || result < max ? result : max; +const isInDisabledInterval = (value: number, interval?: DisabledInterval) => { + return interval && value > interval[0] && value < interval[1]; +}; + +const calculateDisabledHeightInterval = ( + result: number, + disabledHeightInterval?: DisabledInterval, +) => { + if (disabledHeightInterval && isInDisabledInterval(result, disabledHeightInterval)) { + return result < (disabledHeightInterval[0] + disabledHeightInterval[1]) / 2 + ? disabledHeightInterval[0] + : disabledHeightInterval[1]; + } + + return result; +}; + +const calculateDisabledWidthInterval = ( + result: number, + disabledWidthInterval?: DisabledInterval, +) => { + if (disabledWidthInterval && isInDisabledInterval(result, disabledWidthInterval)) { + return result < (disabledWidthInterval[0] + disabledWidthInterval[1]) / 2 + ? disabledWidthInterval[0] + : disabledWidthInterval[1]; + } + + return result; +}; + export const ResizableBox = ({ children, directions, @@ -186,6 +217,10 @@ export const ResizableBox = ({ zIndex = zIndices.draggableComponent, onWidthResizeEnd, onHeightResizeEnd, + onWidthResizeMove, + onHeightResizeMove, + disabledWidthInterval, + disabledHeightInterval, }: ResizableBoxProps) => { const resizableBoxRef = useRef(null); @@ -213,6 +248,7 @@ export const ResizableBox = ({ if (direction === 'top') { result = ensureMinimalSize(-difY); + result = calculateDisabledHeightInterval(result, disabledHeightInterval); if (difY < 0) { setNewHeight(getMaxResult(maxHeight, result)); @@ -221,6 +257,7 @@ export const ResizableBox = ({ } } else if (direction === 'bottom') { result = ensureMinimalSize(newHeight + difY); + result = calculateDisabledHeightInterval(result, disabledHeightInterval); if (difY > 0) { setNewHeight(getMaxResult(maxHeight, result)); @@ -229,6 +266,7 @@ export const ResizableBox = ({ } } else if (direction === 'left') { result = ensureMinimalSize(-difX); + result = calculateDisabledWidthInterval(result, disabledWidthInterval); if (difX < 0) { setNewWidth(getMaxResult(maxWidth, result)); @@ -237,6 +275,7 @@ export const ResizableBox = ({ } } else if (direction === 'right') { result = ensureMinimalSize(newWidth + difX); + result = calculateDisabledWidthInterval(result, disabledWidthInterval); if (difX > 0) { setNewWidth(getMaxResult(maxWidth, result)); @@ -247,7 +286,19 @@ export const ResizableBox = ({ return; } }, - [direction, maxHeight, maxWidth, minHeight, minWidth, newHeight, newWidth, newX, newY], + [ + newX, + newWidth, + newY, + newHeight, + direction, + disabledHeightInterval, + maxHeight, + minHeight, + disabledWidthInterval, + maxWidth, + minWidth, + ], ); const startResizing = (direction: Direction) => { @@ -271,25 +322,23 @@ export const ResizableBox = ({ } document.onmousemove = event => { - if (isResizing && direction !== null && resizeCooldown() === true) { + if (isResizing && direction !== null && resizeCooldown()) { resize(event); + onWidthResizeMove?.(newWidth); + onHeightResizeMove?.(newHeight); } }; document.onmouseup = () => { if (isResizing) { setIsResizing(false); - if (onWidthResizeEnd) { - onWidthResizeEnd(newWidth); - } - if (onHeightResizeEnd) { - onHeightResizeEnd(newHeight); - } + onWidthResizeEnd?.(newWidth); + onHeightResizeEnd?.(newHeight); } }; window.onresize = () => { - if (resizeCooldown() === true) { + if (resizeCooldown()) { if (updateHeightOnWindowResize) { setNewHeight(getMaxResult(maxHeight, window.innerHeight)); } @@ -307,7 +356,9 @@ export const ResizableBox = ({ newHeight, newWidth, onHeightResizeEnd, + onHeightResizeMove, onWidthResizeEnd, + onWidthResizeMove, resizableBoxRef, resize, resizeCooldown, diff --git a/packages/components/src/components/ResizableBox/ResizableBoxExamples.stories.tsx b/packages/components/src/components/ResizableBox/ResizableBoxExamples.stories.tsx index 6381e50563c..c5c888b9bd6 100644 --- a/packages/components/src/components/ResizableBox/ResizableBoxExamples.stories.tsx +++ b/packages/components/src/components/ResizableBox/ResizableBoxExamples.stories.tsx @@ -35,6 +35,8 @@ export const ResizableBoxExamples: StoryObj = { height={100} maxWidth={400} maxHeight={300} + disabledWidthInterval={[100, 200]} + disabledHeightInterval={[100, 200]} > Resize me from bottom and/or right @@ -46,6 +48,8 @@ export const ResizableBoxExamples: StoryObj = { height={100} maxWidth={400} maxHeight={300} + disabledWidthInterval={[100, 200]} + disabledHeightInterval={[100, 200]} > Resize me from top and/or left diff --git a/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceSelector.tsx b/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceSelector.tsx index dc4886b62e1..87d1d30a2cd 100644 --- a/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceSelector.tsx +++ b/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceSelector.tsx @@ -9,6 +9,7 @@ import { borders, spacingsPx } from '@trezor/theme'; import { focusStyleTransition, getFocusShadowStyle } from '@trezor/components/src/utils/utils'; import { SidebarDeviceStatus } from './SidebarDeviceStatus'; import { ViewOnlyTooltip } from 'src/views/view-only/ViewOnlyTooltip'; +import { ExpandedSidebarOnly } from '../Sidebar/ExpandedSidebarOnly'; import { Icon } from '@trezor/components'; const CaretContainer = styled.div` @@ -52,6 +53,7 @@ const InnerContainer = styled.div` align-items: center; cursor: pointer; gap: ${spacingsPx.md}; + min-height: 42px; `; export const DeviceSelector = () => { @@ -113,11 +115,13 @@ export const DeviceSelector = () => { > - {selectedDevice && selectedDevice.state && ( - - - - )} + + {selectedDevice && selectedDevice.state && ( + + + + )} + diff --git a/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceStatus.tsx b/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceStatus.tsx index e7f2c4c4677..4fc56bf2e06 100644 --- a/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceStatus.tsx +++ b/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceStatus.tsx @@ -7,6 +7,8 @@ import { TrezorDevice } from 'src/types/suite'; import { spacingsPx } from '@trezor/theme'; import { RotateDeviceImage } from '@trezor/components'; import { DeviceStatusText } from 'src/views/suite/SwitchDevice/DeviceItem/DeviceStatusText'; +import { ExpandedSidebarOnly } from '../Sidebar/ExpandedSidebarOnly'; +import { isCollapsedSidebar } from '../Sidebar/consts'; type DeviceStatusProps = { deviceModel: DeviceModelInternal; @@ -21,6 +23,10 @@ const Container = styled.div` gap: ${spacingsPx.md}; flex: 1; align-items: center; + + @container ${isCollapsedSidebar} { + justify-content: center; + } `; const DeviceWrapper = styled.div<{ $isLowerOpacity: boolean }>` @@ -54,15 +60,17 @@ export const DeviceStatus = ({ /> - {device && ( - - - - )} + + {device && ( + + + + )} + ); }; diff --git a/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/CollapsedSidebarOnly.tsx b/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/CollapsedSidebarOnly.tsx new file mode 100644 index 00000000000..04ba9b4f2b1 --- /dev/null +++ b/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/CollapsedSidebarOnly.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { SIDEBAR_COLLAPSED_WIDTH } from './consts'; +import { useResponsiveContext } from '../../../../../support/suite/ResponsiveContext'; + +type Props = { + children: React.ReactNode; +}; + +export const CollapsedSidebarOnly = ({ children }: Props) => { + const { sidebarWidth } = useResponsiveContext(); + + if (sidebarWidth && sidebarWidth > SIDEBAR_COLLAPSED_WIDTH) return null; + + return children; +}; diff --git a/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/ExpandedSidebarOnly.tsx b/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/ExpandedSidebarOnly.tsx new file mode 100644 index 00000000000..6b11271dbd7 --- /dev/null +++ b/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/ExpandedSidebarOnly.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { SIDEBAR_COLLAPSED_WIDTH } from './consts'; +import { useResponsiveContext } from '../../../../../support/suite/ResponsiveContext'; + +type Props = { + children: React.ReactNode; +}; + +export const ExpandedSidebarOnly = ({ children }: Props) => { + const { sidebarWidth } = useResponsiveContext(); + + if (sidebarWidth && sidebarWidth <= SIDEBAR_COLLAPSED_WIDTH) return null; + + return children; +}; diff --git a/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/Navigation.tsx b/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/Navigation.tsx index dc81baea00d..1319990fda6 100644 --- a/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/Navigation.tsx +++ b/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/Navigation.tsx @@ -5,12 +5,18 @@ import { NavigationItem, NavigationItemProps } from './NavigationItem'; import { NotificationDropdown } from './NotificationDropdown'; import { useSelector } from 'src/hooks/suite'; import { selectHasExperimentalFeature } from 'src/reducers/suite/suiteReducer'; +import { isCollapsedSidebar } from './consts'; const Nav = styled.nav` display: flex; flex-direction: column; gap: ${spacingsPx.xxs}; margin: ${spacingsPx.xs}; + align-items: stretch; + + @container ${isCollapsedSidebar} { + align-items: center; + } `; const PasswordManagerNavItem = (props: NavigationItemProps) => { diff --git a/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/NavigationItem.tsx b/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/NavigationItem.tsx index 066a29acd24..f549e1f7e95 100644 --- a/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/NavigationItem.tsx +++ b/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/NavigationItem.tsx @@ -9,7 +9,9 @@ import { useDispatch, useSelector } from 'src/hooks/suite'; import { goto } from 'src/actions/suite/routerActions'; import { MouseEvent } from 'react'; import { selectRouteName } from 'src/reducers/suite/routerReducer'; -import { Icon, IconName, IconSize, useElevation } from '@trezor/components'; +import { Tooltip, useElevation, Icon, IconName, IconSize } from '@trezor/components'; +import { ExpandedSidebarOnly } from './ExpandedSidebarOnly'; +import { CollapsedSidebarOnly } from './CollapsedSidebarOnly'; export const NavigationItemBase = styled.div.attrs(() => ({ tabIndex: 0, @@ -96,12 +98,11 @@ export const NavigationItem = ({ dispatch(goto(goToRoute, preserveParams === true ? { preserveParams } : undefined)); } }; - - const isActiveRoute = routes?.some(route => route === activeRoute); - const theme = useTheme(); + const isActiveRoute = routes?.some(route => route === activeRoute); - return ( + const Title = () => ; + const NavItem = () => ( - + + + </ExpandedSidebarOnly> {itemsCount && <Count>{itemsCount}</Count>} </Container> ); + + return ( + <> + <ExpandedSidebarOnly> + <NavItem /> + </ExpandedSidebarOnly> + <CollapsedSidebarOnly> + <Tooltip content={<Title />} placement="right" hasArrow> + <NavItem /> + </Tooltip> + </CollapsedSidebarOnly> + </> + ); }; diff --git a/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/NotificationDropdown.tsx b/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/NotificationDropdown.tsx index bbf2988d770..3af70ce14f1 100644 --- a/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/NotificationDropdown.tsx +++ b/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/NotificationDropdown.tsx @@ -26,11 +26,6 @@ const StyledNavigationItem = styled(NavigationItem)` `} `; -// eslint-disable-next-line local-rules/no-override-ds-component -const StyledDropdown = styled(Dropdown)` - width: 100%; -`; - export const NotificationDropdown = (props: NavigationItemProps) => { const dropdownRef = useRef<DropdownRef>(); @@ -54,7 +49,7 @@ export const NotificationDropdown = (props: NavigationItemProps) => { ); return ( - <StyledDropdown + <Dropdown onToggle={handleToggleChange} ref={dropdownRef} alignMenu="right-top" @@ -66,6 +61,6 @@ export const NotificationDropdown = (props: NavigationItemProps) => { } > {isToggled => <StyledNavigationItem {...props} isActive={isToggled} />} - </StyledDropdown> + </Dropdown> ); }; diff --git a/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/QuickActions/QuickActions.tsx b/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/QuickActions/QuickActions.tsx index 24c3f6fac82..ae394939dfe 100644 --- a/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/QuickActions/QuickActions.tsx +++ b/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/QuickActions/QuickActions.tsx @@ -6,6 +6,7 @@ import { CustomBackend } from './CustomBackend'; import { DebugAndExperimental } from './DebugAndExperimental'; import { HideBalances } from './HideBalances'; import { UpdateStatusActionBarIcon } from './Update/UpdateStatusActionBarIcon'; +import { isCollapsedSidebar } from '../consts'; const ActionsContainer = styled.div` display: flex; @@ -15,6 +16,10 @@ const ActionsContainer = styled.div` padding: 0 ${spacingsPx.xs}; align-items: stretch; + @container ${isCollapsedSidebar} { + flex-direction: column; + } + > * { flex: 1; } diff --git a/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/Sidebar.tsx b/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/Sidebar.tsx index 9233d704a85..a8d37d133a7 100644 --- a/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/Sidebar.tsx +++ b/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/Sidebar.tsx @@ -7,11 +7,13 @@ import { QuickActions } from './QuickActions/QuickActions'; import { ElevationUp, ResizableBox, useElevation } from '@trezor/components'; import { Elevation, mapElevationToBackground, mapElevationToBorder, zIndices } from '@trezor/theme'; import { useActions, useSelector } from 'src/hooks/suite'; -import * as suiteActions from 'src/actions/suite/suiteActions'; import { TrafficLightOffset } from '../../../TrafficLightOffset'; +import { setSidebarWidth as setSidebarWidthInRedux } from '../../../../../actions/suite/suiteActions'; +import { useResponsiveContext } from '../../../../../support/suite/ResponsiveContext'; const Container = styled.nav<{ $elevation: Elevation }>` display: flex; + container-type: inline-size; flex-direction: column; flex: 0 0 auto; height: 100%; @@ -31,21 +33,33 @@ const Content = styled.div` export const Sidebar = () => { const { elevation } = useElevation(); - const sidebarWidth = useSelector(state => state.suite.settings.sidebarWidth); - const { setSidebarWidth } = useActions({ - setSidebarWidth: (width: number) => suiteActions.setSidebarWidth({ width }), + const sidebarWidthFromRedux = useSelector(state => state.suite.settings.sidebarWidth); + + const actions = useActions({ + setSidebarWidth: (width: number) => setSidebarWidthInRedux({ width }), }); + const { setSidebarWidth, sidebarWidth } = useResponsiveContext(); + + const handleSidebarWidthChanged = (width: number) => { + actions.setSidebarWidth(width); + }; + const handleSidebarWidthUpdate = (width: number) => { + setSidebarWidth(width); + }; + return ( <Wrapper> <ResizableBox directions={['right']} - width={sidebarWidth} - minWidth={230} - maxWidth={400} + width={sidebarWidth || sidebarWidthFromRedux} + minWidth={84} + maxWidth={600} zIndex={zIndices.draggableComponent} updateHeightOnWindowResize - onWidthResizeEnd={setSidebarWidth} + onWidthResizeEnd={handleSidebarWidthChanged} + onWidthResizeMove={handleSidebarWidthUpdate} + disabledWidthInterval={[84, 240]} > <Container $elevation={elevation}> <ElevationUp> diff --git a/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/consts.ts b/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/consts.ts new file mode 100644 index 00000000000..0eb2019fc78 --- /dev/null +++ b/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/consts.ts @@ -0,0 +1,4 @@ +export const SIDEBAR_COLLAPSED_WIDTH = 200; + +export const isCollapsedSidebar = `(max-width: ${SIDEBAR_COLLAPSED_WIDTH}px)`; +export const isExpandedSidebar = `(min-width: ${SIDEBAR_COLLAPSED_WIDTH + 1}px)`; diff --git a/packages/suite/src/components/suite/layouts/SuiteLayout/SuiteLayout.tsx b/packages/suite/src/components/suite/layouts/SuiteLayout/SuiteLayout.tsx index 1af352ac6d8..28fe1493964 100644 --- a/packages/suite/src/components/suite/layouts/SuiteLayout/SuiteLayout.tsx +++ b/packages/suite/src/components/suite/layouts/SuiteLayout/SuiteLayout.tsx @@ -1,4 +1,4 @@ -import { useRef, useState, ReactNode } from 'react'; +import { useRef, useState, ReactNode, useEffect, useCallback } from 'react'; import styled from 'styled-components'; import { @@ -26,6 +26,10 @@ import { CoinjoinBars } from './CoinjoinBars/CoinjoinBars'; import { MobileAccountsMenu } from 'src/components/wallet/WalletLayout/AccountsMenu/MobileAccountsMenu'; import { selectSelectedAccount } from 'src/reducers/wallet/selectedAccountReducer'; import { useAppShortcuts } from './useAppShortcuts'; +import { + ResponsiveContextProvider, + useResponsiveContext, +} from 'src/support/suite/ResponsiveContext'; export const SCROLL_WRAPPER_ID = 'layout-scroll'; export const Wrapper = styled.div` @@ -89,7 +93,7 @@ export const ContentWrapper = styled.div` } `; -export const MainBar = styled.div` +export const MainContentContainer = styled.div` display: flex; flex: 1; flex-direction: column; @@ -100,6 +104,40 @@ export const MainBar = styled.div` } `; +type MainContentProps = { + children: ReactNode; +}; + +export const MainContent = ({ children }: MainContentProps) => { + const { contentWidth } = useResponsiveContext(); + const ref = useRef<HTMLDivElement>(null); + console.log('___!!!', contentWidth); + const { setContentWidth, sidebarWidth } = useResponsiveContext(); + + const updateContainerWidth = useCallback(() => { + if (ref.current) { + const { current } = ref; + const boundingRect = current?.getBoundingClientRect(); + const { width } = boundingRect; + setContentWidth(width); + } + }, [setContentWidth]); + + useEffect(() => { + updateContainerWidth(); + + window.addEventListener('resize', updateContainerWidth); + window.addEventListener('orientationchange', updateContainerWidth); + + return () => { + window.removeEventListener('resize', updateContainerWidth); + window.removeEventListener('orientationchange', updateContainerWidth); + }; + }, [ref, setContentWidth, sidebarWidth, updateContainerWidth]); + + return <MainContentContainer ref={ref}>{children}</MainContentContainer>; +}; + interface SuiteLayoutProps { children: ReactNode; } @@ -123,50 +161,52 @@ export const SuiteLayout = ({ children }: SuiteLayoutProps) => { <ElevationContext baseElevation={-1}> <Wrapper ref={wrapperRef} data-testid="@suite-layout"> <PageWrapper> - <NewModal.Provider> - <ModalContextProvider> - <Metadata title={title} /> - - <ModalSwitcher /> - - <CoinjoinBars /> - - {isMobileLayout && <MobileMenu />} - - <DiscoveryProgress /> - - <LayoutContext.Provider value={setLayoutPayload}> - <Body data-testid="@suite-layout/body"> - <Columns> - {!isMobileLayout && ( - <ElevationDown> - <Sidebar /> - </ElevationDown> - )} - <MainBar> - <SuiteBanners /> - <AppWrapper - data-testid="@app" - ref={scrollRef} - id={SCROLL_WRAPPER_ID} - > - <ElevationUp> - {isMobileLayout && isAccountPage && ( - <MobileAccountsMenu /> - )} - {TopMenu && <TopMenu />} - - <ContentWrapper>{children}</ContentWrapper> - </ElevationUp> - </AppWrapper> - </MainBar> - </Columns> - </Body> - </LayoutContext.Provider> - - {!isMobileLayout && <GuideButton />} - </ModalContextProvider> - </NewModal.Provider> + <ResponsiveContextProvider> + <NewModal.Provider> + <ModalContextProvider> + <Metadata title={title} /> + + <ModalSwitcher /> + + <CoinjoinBars /> + + {isMobileLayout && <MobileMenu />} + + <DiscoveryProgress /> + + <LayoutContext.Provider value={setLayoutPayload}> + <Body data-testid="@suite-layout/body"> + <Columns> + {!isMobileLayout && ( + <ElevationDown> + <Sidebar /> + </ElevationDown> + )} + <MainContent> + <SuiteBanners /> + <AppWrapper + data-testid="@app" + ref={scrollRef} + id={SCROLL_WRAPPER_ID} + > + <ElevationUp> + {isMobileLayout && isAccountPage && ( + <MobileAccountsMenu /> + )} + {TopMenu && <TopMenu />} + + <ContentWrapper>{children}</ContentWrapper> + </ElevationUp> + </AppWrapper> + </MainContent> + </Columns> + </Body> + </LayoutContext.Provider> + + {!isMobileLayout && <GuideButton />} + </ModalContextProvider> + </NewModal.Provider> + </ResponsiveContextProvider> </PageWrapper> <GuideRouter /> diff --git a/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountItem.tsx b/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountItem.tsx index 7cc1c54b6d2..89c15b47023 100644 --- a/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountItem.tsx +++ b/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountItem.tsx @@ -5,10 +5,13 @@ import { isTestnet } from '@suite-common/wallet-utils'; import { borders, spacingsPx, typography } from '@trezor/theme'; import { CoinLogo, + Column, Icon, SkeletonRectangle, SkeletonStack, + Tooltip, TOOLTIP_DELAY_LONG, + TOOLTIP_DELAY_NORMAL, TruncateWithTooltip, } from '@trezor/components'; @@ -25,6 +28,8 @@ import { goto } from 'src/actions/suite/routerActions'; import { NavigationItemBase } from 'src/components/suite/layouts/SuiteLayout/Sidebar/NavigationItem'; import { useFormatters } from '@suite-common/formatters'; import { selectLocalCurrency } from 'src/reducers/wallet/settingsReducer'; +import { ExpandedSidebarOnly } from '../../../suite/layouts/SuiteLayout/Sidebar/ExpandedSidebarOnly'; +import { CollapsedSidebarOnly } from '../../../suite/layouts/SuiteLayout/Sidebar/CollapsedSidebarOnly'; const Wrapper = styled(NavigationItemBase)<{ $isSelected: boolean; @@ -48,6 +53,20 @@ const Wrapper = styled(NavigationItemBase)<{ } `; +export const CollapsedItem = styled(NavigationItemBase)<{ $isSelected: boolean }>` + background: ${({ theme, $isSelected }) => $isSelected && theme.backgroundSurfaceElevation1}; + margin: ${spacingsPx.xs} 0; + line-height: 0; + z-index: 0; + position: relative; + + &:hover { + z-index: 1; + position: relative; + background: ${({ theme, $isSelected }) => + !$isSelected && theme.backgroundTertiaryPressedOnElevation0}; + } +`; export const Left = styled.div` position: relative; display: flex; @@ -78,7 +97,7 @@ const Row = styled.div` white-space: nowrap; `; -const AccountName = styled.div<{ $isSelected: boolean }>` +const AccountNameContainer = styled.div<{ $isSelected: boolean }>` display: flex; gap: ${spacingsPx.xxs}; flex: 1; @@ -221,7 +240,88 @@ export const AccountItem = forwardRef( // Show skeleton instead of zero balance during coinjoin initial discovery const isBalanceShown = account.backendType !== 'coinjoin' || account.status !== 'initial'; - return ( + const AccountName = () => ( + <AccountLabelContainer> + {type === 'coin' && ( + <AccountLabel + accountLabel={accountLabel} + accountType={accountType} + symbol={symbol} + index={index} + /> + )} + {type === 'staking' && <Translation id="TR_NAV_STAKING" />} + {type === 'tokens' && <Translation id="TR_NAV_TOKENS" />} + </AccountLabelContainer> + ); + + const ItemContent = () => ( + <Right> + <Row> + <AccountNameContainer + $isSelected={isSelected} + data-testid={`${dataTestKey}/label`} + > + <AccountName /> + <FiatAmount> + {customFiatValue && !isTestnet(symbol) ? ( + <HiddenPlaceholder> + <FiatAmountFormatter + value={customFiatValue} + currency={localCurrency} + minimumFractionDigits={0} + maximumFractionDigits={0} + /> + </HiddenPlaceholder> + ) : ( + <FiatValue + amount={formattedBalance} + symbol={symbol} + fiatAmountFormatterOptions={{ + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }} + > + {({ value }) => + value ? ( + <FiatValueWrapper> + <TruncateWithTooltip delayShow={TOOLTIP_DELAY_LONG}> + {value} + </TruncateWithTooltip> + </FiatValueWrapper> + ) : null + } + </FiatValue> + )} + </FiatAmount> + </AccountNameContainer> + </Row> + {isBalanceShown && type !== 'tokens' && ( + <> + <Row> + <Balance> + <CoinBalance value={formattedBalance} symbol={symbol} /> + </Balance> + </Row> + </> + )} + {!isBalanceShown && ( + <SkeletonStack $col $margin="6px 0px 0px 0px" $childMargin="0px 0px 8px 0px"> + <SkeletonRectangle width="100px" height="16px" animate={shouldAnimate} /> + + {!isTestnet(account.symbol) && ( + <SkeletonRectangle + width="100px" + height="16px" + animate={shouldAnimate} + /> + )} + </SkeletonStack> + )} + </Right> + ); + + const AccountRow = () => ( <Wrapper $isSelected={isSelected} $isGroup={isGroup} @@ -232,88 +332,31 @@ export const AccountItem = forwardRef( tabIndex={0} > <Left>{getLeftComponent()}</Left> - <Right> - <Row> - <AccountName $isSelected={isSelected} data-testid={`${dataTestKey}/label`}> - <AccountLabelContainer> - {type === 'coin' && ( - <AccountLabel - accountLabel={accountLabel} - accountType={accountType} - symbol={symbol} - index={index} - /> - )} - {type === 'staking' && <Translation id="TR_NAV_STAKING" />} - {type === 'tokens' && <Translation id="TR_NAV_TOKENS" />} - </AccountLabelContainer> - <FiatAmount> - {customFiatValue && !isTestnet(symbol) ? ( - <HiddenPlaceholder> - <FiatAmountFormatter - value={customFiatValue} - currency={localCurrency} - minimumFractionDigits={0} - maximumFractionDigits={0} - /> - </HiddenPlaceholder> - ) : ( - <FiatValue - amount={formattedBalance} - symbol={symbol} - fiatAmountFormatterOptions={{ - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }} - > - {({ value }) => - value ? ( - <FiatValueWrapper> - <TruncateWithTooltip - delayShow={TOOLTIP_DELAY_LONG} - > - {value} - </TruncateWithTooltip> - </FiatValueWrapper> - ) : null - } - </FiatValue> - )} - </FiatAmount> - </AccountName> - </Row> - {isBalanceShown && type !== 'tokens' && ( - <> - <Row> - <Balance> - <CoinBalance value={formattedBalance} symbol={symbol} /> - </Balance> - </Row> - </> - )} - {!isBalanceShown && ( - <SkeletonStack - $col - $margin="6px 0px 0px 0px" - $childMargin="0px 0px 8px 0px" - > - <SkeletonRectangle - width="100px" - height="16px" - animate={shouldAnimate} - /> - - {!isTestnet(account.symbol) && ( - <SkeletonRectangle - width="100px" - height="16px" - animate={shouldAnimate} - /> - )} - </SkeletonStack> - )} - </Right> + <ItemContent /> </Wrapper> ); + + return ( + <> + <ExpandedSidebarOnly> + <AccountRow /> + </ExpandedSidebarOnly> + <CollapsedSidebarOnly> + <Column alignItems="center"> + <Tooltip + delayShow={TOOLTIP_DELAY_NORMAL} + cursor="pointer" + content={<ItemContent />} + placement="right" + hasArrow + > + <CollapsedItem $isSelected={isSelected} onClick={handleHeaderClick}> + {getLeftComponent()} + </CollapsedItem> + </Tooltip> + </Column> + </CollapsedSidebarOnly> + </> + ); }, ); diff --git a/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountsList.tsx b/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountsList.tsx index 99ceaa991db..2830d44f684 100644 --- a/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountsList.tsx +++ b/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountsList.tsx @@ -10,6 +10,8 @@ import { AccountsMenuNotice } from './AccountsMenuNotice'; import styled from 'styled-components'; import { spacingsPx } from '@trezor/theme'; import { AccountSection } from './AcccountSection'; +import { ExpandedSidebarOnly } from '../../../suite/layouts/SuiteLayout/Sidebar/ExpandedSidebarOnly'; +import { CollapsedSidebarOnly } from '../../../suite/layouts/SuiteLayout/Sidebar/CollapsedSidebarOnly'; const SkeletonContainer = styled.div` margin: ${spacingsPx.xs}; @@ -86,14 +88,8 @@ export const AccountsList = ({ onItemClick }: AccountListProps) => { const isSkeletonShown = discoveryInProgress || (type === 'coinjoin' && coinjoinIsPreloading); - return ( - <AccountGroup - key={`${device.state}-${type}`} - type={type} - hideLabel={hideLabel} - hasBalance={groupHasBalance} - keepOpen={keepOpen(type)} - > + const Accounts = () => ( + <> {accounts.map(account => { const selected = !!isSelected(account); @@ -108,7 +104,26 @@ export const AccountsList = ({ onItemClick }: AccountListProps) => { ); })} {isSkeletonShown && <AccountItemSkeleton />} - </AccountGroup> + </> + ); + + return ( + <> + <ExpandedSidebarOnly> + <AccountGroup + key={`${device.state}-${type}`} + type={type} + hideLabel={hideLabel} + hasBalance={groupHasBalance} + keepOpen={keepOpen(type)} + > + <Accounts /> + </AccountGroup> + </ExpandedSidebarOnly> + <CollapsedSidebarOnly> + <Accounts /> + </CollapsedSidebarOnly> + </> ); }; diff --git a/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountsMenu.tsx b/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountsMenu.tsx index 2df5ea0c100..246f8c86749 100644 --- a/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountsMenu.tsx +++ b/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountsMenu.tsx @@ -1,7 +1,7 @@ import React from 'react'; import styled from 'styled-components'; -import { spacingsPx, zIndices } from '@trezor/theme'; +import { spacings, spacingsPx, zIndices } from '@trezor/theme'; import { selectDevice } from '@suite-common/wallet-core'; import { useDiscovery, useSelector } from 'src/hooks/suite'; @@ -13,7 +13,9 @@ import { Translation } from 'src/components/suite'; import { AccountsMenuNotice } from './AccountsMenuNotice'; import { getFailedAccounts, sortByCoin } from '@suite-common/wallet-utils'; import { RefreshAfterDiscoveryNeeded } from './RefreshAfterDiscoveryNeeded'; -import { useScrollShadow } from '@trezor/components'; +import { Column, useScrollShadow } from '@trezor/components'; +import { ExpandedSidebarOnly } from '../../../suite/layouts/SuiteLayout/Sidebar/ExpandedSidebarOnly'; +import { CollapsedSidebarOnly } from '../../../suite/layouts/SuiteLayout/Sidebar/CollapsedSidebarOnly'; const Wrapper = styled.div` display: flex; @@ -68,16 +70,26 @@ export const AccountsMenu = () => { return ( <Wrapper> <MenuHeader> - <Row> - {!isEmpty && <AccountSearchBox />} - <AddAccountButton - isFullWidth={isEmpty} - data-testid="@account-menu/add-account" - device={device} - /> - </Row> - - <CoinsFilter /> + <ExpandedSidebarOnly> + <Row> + {!isEmpty && <AccountSearchBox />} + <AddAccountButton + isFullWidth={isEmpty} + data-testid="@account-menu/add-account" + device={device} + /> + </Row> + <CoinsFilter /> + </ExpandedSidebarOnly> + <CollapsedSidebarOnly> + <Column alignItems="center" margin={{ bottom: spacings.sm }}> + <AddAccountButton + isFullWidth={isEmpty} + data-testid="@account-menu/add-account" + device={device} + /> + </Column> + </CollapsedSidebarOnly> </MenuHeader> <ShadowContainer> diff --git a/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AddAccountButton.tsx b/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AddAccountButton.tsx index 67b7d4ae24b..4845779fdca 100644 --- a/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AddAccountButton.tsx +++ b/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AddAccountButton.tsx @@ -3,7 +3,7 @@ import { TrezorDevice } from 'src/types/suite'; import { useDiscovery, useDispatch } from 'src/hooks/suite'; import { openModal } from 'src/actions/suite/modalActions'; -import { Tooltip, ButtonProps, IconButton, Button } from '@trezor/components'; +import { Tooltip, ButtonProps, IconButton, Button, TOOLTIP_DELAY_NORMAL } from '@trezor/components'; import { DiscoveryStatus } from '@suite-common/wallet-constants'; const getExplanationMessage = (device: TrezorDevice | undefined, discoveryIsRunning: boolean) => { @@ -92,6 +92,7 @@ export const AddAccountButton = ({ content={tooltipMessage} placement="bottom" cursor="not-allowed" + delayShow={TOOLTIP_DELAY_NORMAL} > {ButtonComponent} </Tooltip> diff --git a/packages/suite/src/support/suite/ResponsiveContext.tsx b/packages/suite/src/support/suite/ResponsiveContext.tsx new file mode 100644 index 00000000000..29813a43278 --- /dev/null +++ b/packages/suite/src/support/suite/ResponsiveContext.tsx @@ -0,0 +1,33 @@ +import React, { createContext, useContext, useState } from 'react'; + +type ResponsiveContextType = { + sidebarWidth?: number; + setSidebarWidth: (sidebarWidth: number) => void; + contentWidth?: number; + setContentWidth: (contentWidth: number) => void; +}; + +export const ResponsiveContext = createContext<ResponsiveContextType | undefined>(undefined); + +export const ResponsiveContextProvider = ({ children }: { children: React.ReactNode }) => { + const [sidebarWidth, setSidebarWidth] = useState<number | undefined>(undefined); + const [contentWidth, setContentWidth] = useState<number | undefined>(undefined); + + const value: ResponsiveContextType = { + sidebarWidth, + setSidebarWidth, + contentWidth, + setContentWidth, + }; + + return <ResponsiveContext.Provider value={value}>{children}</ResponsiveContext.Provider>; +}; + +export const useResponsiveContext = () => { + const context = useContext(ResponsiveContext); + if (!context) { + throw new Error('useResponsiveContext must be used within a ResponsiveContextProvider'); + } + + return context; +};