diff --git a/.changelog/482.feature.md b/.changelog/482.feature.md new file mode 100644 index 000000000..688a0a6ce --- /dev/null +++ b/.changelog/482.feature.md @@ -0,0 +1 @@ +Mobile ParaTime picker diff --git a/src/app/components/PageLayout/Header.tsx b/src/app/components/PageLayout/Header.tsx index 99270824d..fb9825622 100644 --- a/src/app/components/PageLayout/Header.tsx +++ b/src/app/components/PageLayout/Header.tsx @@ -1,4 +1,4 @@ -import { FC } from 'react' +import { FC, useEffect, useRef } from 'react' import AppBar from '@mui/material/AppBar' import Grid from '@mui/material/Unstable_Grid2' import useScrollTrigger from '@mui/material/useScrollTrigger' @@ -8,18 +8,35 @@ import { NetworkSelector } from './NetworkSelector' import Box from '@mui/material/Box' import { useScopeParam } from '../../hooks/useScopeParam' import { useScreenSize } from '../../hooks/useScreensize' +import useResizeObserver from 'use-resize-observer' export const Header: FC = () => { const theme = useTheme() - const { isMobile } = useScreenSize() + const { isMobile, isTablet } = useScreenSize() const scope = useScopeParam() const scrollTrigger = useScrollTrigger({ disableHysteresis: true, threshold: 0, }) + const headerRef = useRef(null) + + const { height: headerHeight } = useResizeObserver({ + ref: headerRef, + }) + + useEffect(() => { + if (!isTablet) { + return + } + + if (headerRef.current !== null) { + document.body.style.setProperty('--app-header-height', `${headerHeight?.toFixed(2) || 0}px`) + } + }, [isTablet, headerHeight]) return ( > = ({ children, m const { isMobile, isTablet } = useScreenSize() const scope = useScopeParam() const isApiOffline = useIsApiOffline(scope?.network || Network.mainnet) + const bannersRef = useRef(null) + + const { height: bannersHeight } = useResizeObserver({ + ref: bannersRef, + }) + + useEffect(() => { + if (!isTablet) { + return + } + + if (bannersRef.current !== null) { + document.body.style.setProperty('--app-banners-height', `${bannersHeight?.toFixed(2) || 0}px`) + } + }, [isTablet, bannersHeight]) return ( <> - - - {scope && } + + + + {scope && } + = ({ network, selectedLayer }) => { const { t } = useTranslation() const theme = useTheme() + const { isTablet } = useScreenSize() const labels = getNetworkNames(t) const layerLabels = getLayerLabels(t) const icons = getNetworkIcons() @@ -92,7 +94,7 @@ export const LayerDetails: FC = ({ network, selectedLayer }) } return ( - + = ({ children }) => ( + + {children} + +) + export const DisabledLayerMenuItem: FC = ({ divider, layer }) => { + const { isTablet } = useScreenSize() const { t } = useTranslation() const labels = getLayerLabels(t) @@ -27,7 +38,10 @@ export const DisabledLayerMenuItem: FC = ({ divider, lay {/* Div is needed because we need an element with enabled pointer-events to make Tooltip work */}
- {labels[layer]} + + {labels[layer]} + {isTablet && {t('paraTimePicker.comingSoon')}} +
@@ -70,12 +84,9 @@ export const LayerMenuItem: FC = ({ > {labels[layer]} - - {selectedNetwork === network && activeLayer === layer && t('paraTimePicker.selected')} - + {selectedNetwork === network && activeLayer === layer && ( + {t('paraTimePicker.selected')} + )} {layer === selectedLayer && } @@ -105,7 +116,7 @@ export const LayerMenu: FC = ({ .sort(orderByLayer) return ( - + {options.map((option, index) => { if (!option.enabled) { return ( diff --git a/src/app/components/ParaTimePicker/NetworkMenu.tsx b/src/app/components/ParaTimePicker/NetworkMenu.tsx index 8d609ec84..29505531d 100644 --- a/src/app/components/ParaTimePicker/NetworkMenu.tsx +++ b/src/app/components/ParaTimePicker/NetworkMenu.tsx @@ -85,7 +85,7 @@ export const NetworkMenu: FC = ({ activeNetwork, selectedNetwo return ( <> - + {stableOptions.map((network, index) => ( void @@ -25,73 +28,176 @@ type ParaTimePickerProps = { open: boolean } +const ParaTimePickerDrawer = styled(Drawer)(() => ({ + [`.${drawerClasses.root}`]: { + height: '100vh', + }, +})) + export const ParaTimePicker: FC = ({ onClose, onConfirm, open }) => ( - + - + ) +const StyledParaTimePickerContent = styled(Box)(({ theme }) => ({ + [theme.breakpoints.down('md')]: { + display: 'flex', + flexDirection: 'column', + width: '100%', + flex: 1, + }, +})) + +const StyledContent = styled(Box)(({ theme }) => ({ + flex: 1, + [theme.breakpoints.down('md')]: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + }, +})) + +const TabletBackButton = styled(Button)({ + color: COLORS.brandDark, + width: 'fit-content', + textTransform: 'capitalize', + textDecoration: 'none', +}) + +const TabletActionBar = styled(Box)(() => ({ + minHeight: '50px', +})) + +const ActionBar = styled(Box)(({ theme }) => ({ + display: 'flex', + justifyContent: 'space-around', + [theme.breakpoints.up('md')]: { + justifyContent: 'flex-end', + gap: theme.spacing(4), + }, +})) + type ParaTimePickerContentProps = Omit +enum ParaTimePickerTabletStep { + Network, + ParaTime, + ParaTimeDetails, +} + const ParaTimePickerContent: FC = ({ onClose, onConfirm }) => { + const { isTablet } = useScreenSize() const { t } = useTranslation() const { network, layer } = useRequiredScopeParam() - const [showNetworkMenu, setShowNetworkMenu] = useState(network !== Network.mainnet) + const [showNetworkMenu, setShowNetworkMenu] = useState(isTablet || network !== Network.mainnet) const [selectedLayer, setSelectedLayer] = useState(layer) const [selectedNetwork, setSelectedNetwork] = useState(network) + const [tabletStep, setTabletStep] = useState( + ParaTimePickerTabletStep.ParaTimeDetails, + ) const selectNetwork = (newNetwork: Network) => { setSelectedNetwork(newNetwork) setSelectedLayer(RouteUtils.getEnabledLayersForNetwork(newNetwork)[0]) } return ( - - - - - setShowNetworkMenu(!showNetworkMenu)} - sx={{ - color: COLORS.brandDark, - ml: 3, - }} - > - {showNetworkMenu ? : } - + + {!isTablet && ( + <> + + + + setShowNetworkMenu(!showNetworkMenu)} + sx={{ + color: COLORS.brandDark, + ml: 3, + }} + > + {showNetworkMenu ? : } + + + )} + {isTablet && ( + <> + + {tabletStep === ParaTimePickerTabletStep.ParaTime && ( + } + onClick={() => { + setTabletStep(ParaTimePickerTabletStep.Network) + }} + > + {t('paraTimePicker.viewNetworks')} + + )} + {tabletStep === ParaTimePickerTabletStep.ParaTimeDetails && ( + } + onClick={() => { + setTabletStep(ParaTimePickerTabletStep.ParaTime) + }} + > + {t('paraTimePicker.viewParaTimes')} + + )} + + + )} - + - {!showNetworkMenu && ( + {!showNetworkMenu && !isTablet && ( )} - {showNetworkMenu && ( - + {((!isTablet && showNetworkMenu) || + (isTablet && tabletStep === ParaTimePickerTabletStep.Network)) && ( + { + selectNetwork(network) + setTabletStep(ParaTimePickerTabletStep.ParaTime) + }} + /> + + )} + {(!isTablet || (isTablet && tabletStep === ParaTimePickerTabletStep.ParaTime)) && ( + + { + setSelectedLayer(layer) + setTabletStep(ParaTimePickerTabletStep.ParaTimeDetails) + }} /> )} - - - - - - + {(!isTablet || (isTablet && tabletStep === ParaTimePickerTabletStep.ParaTimeDetails)) && ( + + + + )} - - @@ -100,11 +206,12 @@ const ParaTimePickerContent: FC = ({ onClose, onConf disabled={selectedNetwork === network && selectedLayer === layer} color="primary" variant="contained" + size="large" > {t('common.select')} - - - + + + ) } diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index fbd5aa530..fdab148df 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -244,6 +244,7 @@ "paraTimePicker": { "chainId": "Chain ID:", "decimal": "Decimal: {{ id }}", + "comingSoon": "(coming soon)", "hex": "Hex: {{ id }}", "less": "Show Less", "more": "Show More", @@ -252,6 +253,8 @@ "rpcWebSockets": "RPC WebSockets endpoint: {{ endpoint }}", "selected": "(currently active)", "toggleNetworkMenu": "Toggle networks menu", + "viewNetworks": "View Networks", + "viewParaTimes": "View ParaTimes", "mainnet": { "emerald": "The Emerald ParaTime is our official EVM Compatible ParaTime providing smart contract environment with full EVM compatibility.", "sapphire": "The Sapphire ParaTime is our official confidential EVM Compatible ParaTime providing a smart contract development environment with EVM compatibility." diff --git a/src/styles/index.css b/src/styles/index.css index 920b6c69e..1b4753f03 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -1,6 +1,11 @@ @import '../../node_modules/@fontsource-variable/figtree/index.css'; @import '../../node_modules/@fontsource-variable/roboto-mono/index.css'; +:root { + --app-header-height: 0px; + --app-banners-height: 0px; +} + html { scroll-behavior: smooth; } diff --git a/src/styles/theme/defaultTheme.ts b/src/styles/theme/defaultTheme.ts index 984fa4fbe..18381fb39 100644 --- a/src/styles/theme/defaultTheme.ts +++ b/src/styles/theme/defaultTheme.ts @@ -6,6 +6,7 @@ import { inputBaseClasses } from '@mui/material/InputBase' import { inputAdornmentClasses } from '@mui/material/InputAdornment' import { tabClasses } from '@mui/material/Tab' import { menuItemClasses } from '@mui/material/MenuItem' +import { modalClasses } from '@mui/material/Modal' declare module '@mui/material/styles' { interface Palette { @@ -503,6 +504,19 @@ export const defaultTheme = createTheme({ borderRadius: '0 0 12px 12px', padding: `${theme.spacing(4)} 5%`, }), + modal: ({ theme }) => ({ + [theme.breakpoints.down('md')]: { + [`& .${modalClasses.backdrop}`]: { + background: 'transparent', + }, + }, + }), + paper: ({ theme }) => ({ + [theme.breakpoints.down('md')]: { + height: `calc(100vh - var(--app-header-height) - var(--app-banners-height))`, + top: `calc(var(--app-header-height) + var(--app-banners-height))`, + }, + }), }, }, MuiLink: {