Commit 2b22bfc2 authored by tom's avatar tom

links refactoring and showcases

parent b740a53f
......@@ -26,6 +26,8 @@ const RESTRICTED_MODULES = {
{ name: '@metamask/post-message-stream', message: 'Please lazy-load @metamask/post-message-stream or use useProvider hook instead' },
{ name: 'playwright/TestApp', message: 'Please use render() fixture from test() function of playwright/lib module' },
{ name: 'ui/shared/chakra/Skeleton', message: 'Please use Skeleton component from toolkit/chakra instead' },
{ name: 'ui/shared/links/LinkInternal', message: 'Please use Link component from toolkit/chakra instead' },
{ name: 'ui/shared/links/LinkExternal', message: 'Please use Link component from toolkit/chakra instead' },
{
name: '@chakra-ui/react',
importNames: [
......
import type { LinkProps as ChakraLinkProps } from '@chakra-ui/react';
import { Link as ChakraLink } from '@chakra-ui/react';
import NextLink from 'next/link';
import type { LinkProps as NextLinkProps } from 'next/link';
import React from 'react';
import IconSvg from 'ui/shared/IconSvg';
import { Skeleton } from './skeleton';
export interface LinkProps extends ChakraLinkProps {
loading?: boolean;
external?: boolean;
scroll?: boolean;
iconColor?: ChakraLinkProps['color'];
}
export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
function Link(props, ref) {
const { external, loading, href, children, scroll = true, iconColor, ...rest } = props;
if (external) {
return (
<Skeleton loading={ loading } asChild>
<ChakraLink
ref={ ref }
href={ href }
className="group"
target="_blank"
rel="noopener noreferrer" { ...rest }
>
{ children }
<IconSvg
name="link_external"
boxSize={ 3 }
verticalAlign="middle"
color={ iconColor ?? 'icon.externalLink' }
_groupHover={{
color: 'inherit',
}}
flexShrink={ 0 }
/>
</ChakraLink>
</Skeleton>
);
}
return (
<Skeleton loading={ loading } asChild>
<ChakraLink asChild ref={ ref } { ...rest }>
{ href ? <NextLink href={ href as NextLinkProps['href'] } scroll={ scroll }>{ children }</NextLink> : <span>{ children }</span> }
</ChakraLink>
</Skeleton>
);
},
);
......@@ -47,6 +47,6 @@ export const SkeletonText = React.forwardRef<HTMLDivElement, SkeletonTextProps>(
export const Skeleton = React.forwardRef<HTMLDivElement, ChakraSkeletonProps>(
function Skeleton(props, ref) {
const { loading = false, ...rest } = props;
return <ChakraSkeleton loading={ loading } { ...rest } ref={ ref }/>;
return <ChakraSkeleton loading={ loading } { ...(loading ? { 'data-loading': true } : {}) } { ...rest } ref={ ref }/>;
},
);
import React from 'react';
import { scroller, Element } from 'react-scroll';
import { Link } from 'toolkit/chakra/link';
interface Props {
children: React.ReactNode;
id?: string;
onClick?: () => void;
isLoading?: boolean;
}
const ID = 'CutLink';
const CutLink = (props: Props) => {
const { children, id = ID, onClick, isLoading } = props;
const [ isExpanded, setIsExpanded ] = React.useState(false);
const handleClick = React.useCallback(() => {
setIsExpanded((flag) => !flag);
scroller.scrollTo(id, {
duration: 500,
smooth: true,
});
onClick?.();
}, [ id, onClick ]);
return (
<>
<Element name={ id }>
<Link
textStyle="sm"
textDecorationLine="underline"
textDecorationStyle="dashed"
onClick={ handleClick }
loading={ isLoading }
>
{ isExpanded ? 'Hide details' : 'View details' }
</Link>
</Element>
{ isExpanded && children }
</>
);
};
export default React.memo(CutLink);
......@@ -89,6 +89,9 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
secondary: {
DEFAULT: { value: { _light: '{colors.gray.400}', _dark: '{colors.gray.500}' } },
},
underlaid: {
bg: { value: { _light: '{colors.gray.100}', _dark: '{colors.gray.800}' } },
},
subtle: {
DEFAULT: { value: { _light: '{colors.blackAlpha.800}', _dark: '{colors.gray.400}' } },
hover: { value: { _light: '{colors.blackAlpha.800}', _dark: '{colors.gray.400}' } },
......@@ -306,6 +309,7 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
},
icon: {
backTo: { value: '{colors.gray.400}' },
externalLink: { value: { _light: '{colors.gray.400}', _dark: '{colors.gray.400}' } },
},
global: {
body: {
......
......@@ -5,7 +5,7 @@ export const recipe = defineRecipe({
gap: 0,
},
variants: {
visual: {
variant: {
primary: {
color: 'link.primary',
_hover: {
......@@ -24,9 +24,27 @@ export const recipe = defineRecipe({
color: 'link.subtle',
_hover: {
color: 'link.subtle.hover',
textDecorationLine: 'underline',
textDecorationColor: 'link.subtle.hover',
},
},
underlaid: {
color: 'link.primary',
// css-var to override bg property on loaded skeleton
'--layer-bg': '{colors.link.underlaid.bg}',
bgColor: 'link.underlaid.bg',
px: '8px',
py: '6px',
borderRadius: 'base',
textStyle: 'sm',
_hover: {
color: 'link.primary.hover',
textDecoration: 'none',
},
_loading: {
bgColor: 'transparent',
},
},
navigation: {
color: 'link.navigation.fg',
bg: 'link.navigation.bg',
......@@ -47,6 +65,6 @@ export const recipe = defineRecipe({
},
},
defaultVariants: {
visual: 'primary',
variant: 'primary',
},
});
......@@ -19,7 +19,7 @@ export const recipe = defineRecipe({
},
},
'false': {
background: 'unset',
background: 'var(--layer-bg)',
animation: 'fade-in var(--fade-duration, 0.1s) ease-out !important',
},
},
......
......@@ -72,6 +72,8 @@ export const recipe = defineSlotRecipe({
boxShadow: 'popover',
boxShadowColor: 'popover.shadow',
borderRadius: 'md',
textAlign: 'left',
fontWeight: 'normal',
},
},
},
......
......@@ -31,6 +31,12 @@ const customConfig = defineConfig({
fonts,
shadows,
zIndex,
fontWeights: {
normal: { value: '400' },
medium: { value: '500' },
semibold: { value: '600' },
bold: { value: '700' },
},
},
},
// components,
......
......@@ -25,6 +25,7 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import AlertsShowcase from 'ui/showcases/Alerts';
import BadgesShowcase from 'ui/showcases/Badges';
import ButtonShowcase from 'ui/showcases/Button';
import LinksShowcase from 'ui/showcases/Links';
import PaginationShowcase from 'ui/showcases/Pagination';
import TabsShowcase from 'ui/showcases/Tabs';
......@@ -54,6 +55,7 @@ const ChakraShowcases = () => {
<TabsTrigger value="alerts">Alerts</TabsTrigger>
<TabsTrigger value="badges">Badges</TabsTrigger>
<TabsTrigger value="buttons">Buttons</TabsTrigger>
<TabsTrigger value="links">Links</TabsTrigger>
<TabsTrigger value="pagination">Pagination</TabsTrigger>
<TabsTrigger value="tabs">Tabs</TabsTrigger>
<TabsTrigger value="unsorted">Unsorted</TabsTrigger>
......@@ -61,6 +63,7 @@ const ChakraShowcases = () => {
<AlertsShowcase/>
<BadgesShowcase/>
<ButtonShowcase/>
<LinksShowcase/>
<TabsShowcase/>
<PaginationShowcase/>
......
......@@ -48,7 +48,7 @@ const CopyToClipboard = ({ text, className, isLoading, onClick, size = 5, type,
}, [ onClick, copy ]);
if (isLoading) {
return <Skeleton boxSize={ size } className={ className } borderRadius="sm" flexShrink={ 0 } ml={ 2 } display="inline-block"/>;
return <Skeleton boxSize={ size } className={ className } borderRadius="sm" flexShrink={ 0 } ml={ 2 } display="inline-block" loading/>;
}
return (
......
......@@ -48,7 +48,7 @@ const Icon = (props: IconProps) => {
};
if (props.isLoading) {
return <Skeleton { ...styles } borderRadius="full" flexShrink={ 0 }/>;
return <Skeleton { ...styles } loading borderRadius="full" flexShrink={ 0 }/>;
}
if (props.address.is_contract) {
......
import { Box } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react';
import dynamic from 'next/dynamic';
import React from 'react';
......@@ -53,7 +53,7 @@ const Icon = dynamic(
return (props: IconProps) => {
const svg = GradientAvatar(props.hash, props.size, 'circle');
return <div dangerouslySetInnerHTML={{ __html: svg }}/>;
return <Flex dangerouslySetInnerHTML={{ __html: svg }}/>;
};
}
......
......@@ -31,6 +31,7 @@ export interface EntityBaseProps {
tailLength?: number;
target?: React.HTMLAttributeAnchorTarget;
truncation?: Truncation;
size?: 'md' | 'lg';
}
export interface ContainerBaseProps extends Pick<EntityBaseProps, 'className'> {
......@@ -165,9 +166,9 @@ const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dyna
);
});
export type CopyBaseProps = Pick<CopyToClipboardProps, 'isLoading' | 'text'> & Pick<EntityBaseProps, 'noCopy'>;
export type CopyBaseProps = Pick<CopyToClipboardProps, 'isLoading' | 'text'> & Pick<EntityBaseProps, 'noCopy' | 'size'>;
const Copy = (props: CopyBaseProps) => {
const Copy = ({ size, ...props }: CopyBaseProps) => {
if (props.noCopy) {
return null;
}
......
import type { As } from '@chakra-ui/react';
import { chakra } from '@chakra-ui/react';
import React from 'react';
......@@ -76,7 +75,7 @@ const BlobEntity = (props: EntityProps) => {
);
};
export default React.memo(chakra<As, EntityProps>(BlobEntity));
export default React.memo(chakra(BlobEntity));
export {
Container,
......
import type { As } from '@chakra-ui/react';
import { Box, chakra, Flex, Image, PopoverBody, PopoverContent, PopoverTrigger, Portal, Text } from '@chakra-ui/react';
import { chakra, Flex, Text } from '@chakra-ui/react';
import React from 'react';
import type * as bens from '@blockscout/bens-types';
import { route } from 'nextjs-routes';
import Popover from 'ui/shared/chakra/Popover';
import Skeleton from 'ui/shared/chakra/Skeleton';
import { Image } from 'toolkit/chakra/image';
import { Link as LinkToolkit } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { Tooltip } from 'toolkit/chakra/tooltip';
import * as EntityBase from 'ui/shared/entities/base/components';
import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/links/LinkExternal';
import TruncatedValue from 'ui/shared/TruncatedValue';
import { distributeEntityProps, getIconProps } from '../base/utils';
......@@ -39,58 +39,71 @@ const Icon = (props: IconProps) => {
const styles = getIconProps(props.size);
if (props.isLoading) {
return <Skeleton boxSize={ styles.boxSize } borderRadius="sm" mr={ 2 }/>;
return <Skeleton loading boxSize={ styles.boxSize } borderRadius="sm" mr={ 2 }/>;
}
const content = (
<>
<Flex alignItems="center" textStyle="md">
<Image
src={ props.protocol.icon_url }
boxSize={ 5 }
borderRadius="sm"
mr={ 2 }
alt={ `${ props.protocol.title } protocol icon` }
fallback={ icon }
// fallbackStrategy={ props.protocol.icon_url ? 'onError' : 'beforeLoadOrError' }
/>
<div>
<span>{ props.protocol.short_name }</span>
<chakra.span color="text_secondary" whiteSpace="pre"> { props.protocol.tld_list.map((tld) => `.${ tld }`).join((' ')) }</chakra.span>
</div>
</Flex>
<Text>{ props.protocol.description }</Text>
{ props.protocol.docs_url && (
<LinkToolkit
href={ props.protocol.docs_url }
display="inline-flex"
alignItems="center"
external
>
{ /* TODO @tom2drum maybe refactor this to pass icon as a prop */ }
<IconSvg name="docs" boxSize={ 5 } color="text.secondary" mr={ 2 }/>
<span>Documentation</span>
</LinkToolkit>
) }
</>
);
return (
<Popover trigger="hover" isLazy placement="bottom-start">
<PopoverTrigger>
<Box flexShrink={ 0 }>
<Image
src={ props.protocol.icon_url }
boxSize={ styles.boxSize }
borderRadius="sm"
mr={ 2 }
alt={ `${ props.protocol.title } protocol icon` }
fallback={ icon }
fallbackStrategy={ props.protocol.icon_url ? 'onError' : 'beforeLoadOrError' }
/>
</Box>
</PopoverTrigger>
<Portal>
<PopoverContent maxW={{ base: '100vw', lg: '440px' }} minW="250px" w="fit-content">
<PopoverBody display="flex" flexDir="column" rowGap={ 3 }>
<Flex alignItems="center">
<Image
src={ props.protocol.icon_url }
boxSize={ 5 }
borderRadius="sm"
mr={ 2 }
alt={ `${ props.protocol.title } protocol icon` }
fallback={ icon }
fallbackStrategy={ props.protocol.icon_url ? 'onError' : 'beforeLoadOrError' }
/>
<div>
<span>{ props.protocol.short_name }</span>
<chakra.span color="text_secondary" whiteSpace="pre"> { props.protocol.tld_list.map((tld) => `.${ tld }`).join((' ')) }</chakra.span>
</div>
</Flex>
<Text fontSize="sm">{ props.protocol.description }</Text>
{ props.protocol.docs_url && (
<LinkExternal
href={ props.protocol.docs_url }
display="inline-flex"
alignItems="center"
fontSize="sm"
>
<IconSvg name="docs" boxSize={ 5 } color="text_secondary" mr={ 2 }/>
<span>Documentation</span>
</LinkExternal>
) }
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
<Tooltip
content={ content }
visual="popover"
positioning={{
placement: 'bottom-start',
}}
contentProps={{
maxW: { base: '100vw', lg: '440px' },
minW: '250px',
w: 'fit-content',
display: 'flex',
flexDir: 'column',
rowGap: 3,
alignItems: 'flex-start',
}}
interactive
>
<Image
src={ props.protocol.icon_url }
boxSize={ styles.boxSize }
borderRadius="sm"
mr={ 2 }
flexShrink={ 0 }
alt={ `${ props.protocol.title } protocol icon` }
fallback={ icon }
// fallbackStrategy={ props.protocol.icon_url ? 'onError' : 'beforeLoadOrError' }
/>
</Tooltip>
);
}
......@@ -140,7 +153,7 @@ const EnsEntity = (props: EntityProps) => {
);
};
export default React.memo(chakra<As, EntityProps>(EnsEntity));
export default React.memo(chakra(EnsEntity));
export {
Container,
......
import type { As } from '@chakra-ui/react';
import { chakra } from '@chakra-ui/react';
import React from 'react';
......@@ -69,7 +68,7 @@ const NftEntity = (props: EntityProps) => {
);
};
export default React.memo(chakra<As, EntityProps>(NftEntity));
export default React.memo(chakra(NftEntity));
export {
Container,
......
import type { As } from '@chakra-ui/react';
import { Flex, chakra, useColorModeValue } from '@chakra-ui/react';
import { Flex, chakra } from '@chakra-ui/react';
import React from 'react';
import type { Pool } from 'types/api/pools';
......@@ -7,7 +6,7 @@ import type { Pool } from 'types/api/pools';
import { route } from 'nextjs-routes';
import { getPoolTitle } from 'lib/pools/getPoolTitle';
import Skeleton from 'ui/shared/chakra/Skeleton';
import { Skeleton } from 'toolkit/chakra/skeleton';
import * as EntityBase from 'ui/shared/entities/base/components';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
......@@ -32,8 +31,8 @@ const Link = chakra((props: LinkProps) => {
type IconProps = Pick<EntityProps, 'pool' | 'className'> & EntityBase.IconBaseProps;
const Icon = (props: IconProps) => {
const bgColor = useColorModeValue('white', 'black');
const borderColor = useColorModeValue('whiteAlpha.800', 'blackAlpha.800');
const bgColor = { _light: 'white', _dark: 'black' };
const borderColor = { _light: 'whiteAlpha.800', _dark: 'blackAlpha.800' };
return (
<Flex>
<Flex
......@@ -87,7 +86,7 @@ const Content = chakra((props: ContentProps) => {
return (
<TruncatedTextTooltip label={ nameString }>
<Skeleton
isLoaded={ !props.isLoading }
loading={ props.isLoading }
display="inline-block"
whiteSpace="nowrap"
overflow="hidden"
......@@ -119,7 +118,7 @@ const PoolEntity = (props: EntityProps) => {
);
};
export default React.memo(chakra<As, EntityProps>(PoolEntity));
export default React.memo(chakra(PoolEntity));
export {
Container,
......
......@@ -43,7 +43,7 @@ const Icon = (props: IconProps) => {
};
if (props.isLoading) {
return <Skeleton { ...styles } className={ props.className }/>;
return <Skeleton { ...styles } loading className={ props.className }/>;
}
return (
......
import type { As } from '@chakra-ui/react';
import { chakra } from '@chakra-ui/react';
import React from 'react';
......@@ -76,7 +75,7 @@ const UserOpEntity = (props: EntityProps) => {
);
};
export default React.memo(chakra<As, EntityProps>(UserOpEntity));
export default React.memo(chakra(UserOpEntity));
export {
Container,
......
......@@ -14,12 +14,11 @@ interface Props {
children: React.ReactNode;
isLoading?: boolean;
variant?: Variants;
visual?: LinkProps['visual'];
iconColor?: LinkProps['color'];
onClick?: LinkProps['onClick'];
}
const LinkExternal = ({ href, children, className, isLoading, variant, visual, iconColor, onClick }: Props) => {
const LinkExternal = ({ href, children, className, isLoading, variant, iconColor, onClick }: Props) => {
const commonProps = {
display: 'inline-block',
alignItems: 'center',
......@@ -46,7 +45,7 @@ const LinkExternal = ({ href, children, className, isLoading, variant, visual, i
}
return (
<Link className={ className } { ...styleProps } target="_blank" href={ href } onClick={ onClick } visual={ visual }>
<Link className={ className } { ...styleProps } target="_blank" href={ href } onClick={ onClick } variant={ variant }>
{ children }
<IconSvg name="link_external" boxSize={ 3 } verticalAlign="middle" color={ iconColor ?? 'icon_link_external' } flexShrink={ 0 }/>
</Link>
......
This diff is collapsed.
import type { StackProps, TabsContentProps } from '@chakra-ui/react';
import { Code, Grid, HStack } from '@chakra-ui/react';
import { Code, Grid, HStack, VStack } from '@chakra-ui/react';
import React from 'react';
import { Heading } from 'toolkit/chakra/heading';
......@@ -13,16 +13,28 @@ export const SamplesStack = ({ children }: { children: React.ReactNode }) => (
<Grid
rowGap={ 4 }
columnGap={ 8 }
gridTemplateColumns="fit-content(100%) fit-content(100%)"
gridTemplateColumns="fit-content(100%) 1fr"
justifyItems="flex-start"
alignItems="flex-start"
>
{ children }
</Grid>
);
export const Sample = ({ children, label, ...props }: { children: React.ReactNode; label?: string } & StackProps) => (
<>
{ label && <Code w="fit-content">{ label }</Code> }
<HStack gap={ 3 } whiteSpace="pre-wrap" flexWrap="wrap" columnSpan={ label ? '1' : '2' } { ...props }>{ children }</HStack>
</>
);
export const Sample = ({ children, label, vertical, ...props }: { children: React.ReactNode; vertical?: boolean; label?: string } & StackProps) => {
const Stack = vertical ? VStack : HStack;
return (
<>
{ label && <Code w="fit-content">{ label }</Code> }
<Stack
gap={ 3 }
whiteSpace="pre-wrap"
flexWrap="wrap"
columnSpan={ label ? '1' : '2' }
alignItems={ vertical ? 'flex-start' : 'center' }
{ ...props }
>
{ children }
</Stack>
</>
);
};
......@@ -19,7 +19,7 @@ const FooterLinkItem = ({ icon, iconSize, text, url, isLoading }: Props) => {
}
return (
<Link href={ url } display="flex" alignItems="center" h="30px" visual="subtle" target="_blank" textStyle="xs">
<Link href={ url } display="flex" alignItems="center" h="30px" variant="subtle" target="_blank" textStyle="xs">
{ icon && (
<Center minW={ 6 } mr={ 2 }>
<IconSvg boxSize={ iconSize || 5 } name={ icon }/>
......
......@@ -37,7 +37,7 @@ const NavLink = ({ className, item, noIcon }: Props) => {
href={ href }
display="flex"
alignItems="center"
visual="navigation"
variant="navigation"
{ ...(isActive ? { 'data-selected': true } : {}) }
w="224px"
px={ 2 }
......
......@@ -50,7 +50,7 @@ const NavLinkGroup = ({ item }: Props) => {
visual="popover"
content={ content }
onOpenChange={ onOpenChange }
lazyMount
lazyMount={ false }
positioning={{
placement: 'bottom',
offset: { mainAxis: 8 },
......@@ -66,7 +66,7 @@ const NavLinkGroup = ({ item }: Props) => {
py={ 1.5 }
textStyle="sm"
fontWeight={ 500 }
visual="navigation"
variant="navigation"
{ ...(item.isActive ? { 'data-selected': true } : {}) }
{ ...(open ? { 'data-active': true } : {}) }
borderRadius="base"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment