Commit 27872bfe authored by tom's avatar tom

contract details tab

parent 4a55cf1a
...@@ -5,6 +5,7 @@ import IconSvg from 'ui/shared/IconSvg'; ...@@ -5,6 +5,7 @@ import IconSvg from 'ui/shared/IconSvg';
interface AccordionItemTriggerProps extends Accordion.ItemTriggerProps { interface AccordionItemTriggerProps extends Accordion.ItemTriggerProps {
indicatorPlacement?: 'start' | 'end'; indicatorPlacement?: 'start' | 'end';
noIndicator?: boolean;
variant?: Accordion.RootProps['variant']; variant?: Accordion.RootProps['variant'];
} }
...@@ -12,7 +13,7 @@ export const AccordionItemTrigger = React.forwardRef< ...@@ -12,7 +13,7 @@ export const AccordionItemTrigger = React.forwardRef<
HTMLButtonElement, HTMLButtonElement,
AccordionItemTriggerProps AccordionItemTriggerProps
>(function AccordionItemTrigger(props, ref) { >(function AccordionItemTrigger(props, ref) {
const { children, indicatorPlacement: indicatorPlacementProp, variant, ...rest } = props; const { children, indicatorPlacement: indicatorPlacementProp, variant, noIndicator, ...rest } = props;
const indicatorPlacement = variant === 'faq' ? 'start' : (indicatorPlacementProp ?? 'end'); const indicatorPlacement = variant === 'faq' ? 'start' : (indicatorPlacementProp ?? 'end');
...@@ -59,9 +60,9 @@ export const AccordionItemTrigger = React.forwardRef< ...@@ -59,9 +60,9 @@ export const AccordionItemTrigger = React.forwardRef<
return ( return (
<Accordion.ItemTrigger className="group" { ...rest } ref={ ref }> <Accordion.ItemTrigger className="group" { ...rest } ref={ ref }>
{ indicatorPlacement === 'start' && indicator } { indicatorPlacement === 'start' && !noIndicator && indicator }
{ children } { children }
{ indicatorPlacement === 'end' && indicator } { indicatorPlacement === 'end' && !noIndicator && indicator }
</Accordion.ItemTrigger> </Accordion.ItemTrigger>
); );
}); });
......
import type { AlertDescriptionProps } from '@chakra-ui/react';
import { Alert as ChakraAlert } from '@chakra-ui/react'; import { Alert as ChakraAlert } from '@chakra-ui/react';
import * as React from 'react'; import * as React from 'react';
...@@ -9,6 +10,7 @@ import { Skeleton } from './skeleton'; ...@@ -9,6 +10,7 @@ import { Skeleton } from './skeleton';
export interface AlertProps extends Omit<ChakraAlert.RootProps, 'title'> { export interface AlertProps extends Omit<ChakraAlert.RootProps, 'title'> {
startElement?: React.ReactNode; startElement?: React.ReactNode;
endElement?: React.ReactNode; endElement?: React.ReactNode;
descriptionProps?: AlertDescriptionProps;
title?: React.ReactNode; title?: React.ReactNode;
icon?: React.ReactElement; icon?: React.ReactElement;
closable?: boolean; closable?: boolean;
...@@ -29,6 +31,7 @@ export const Alert = React.forwardRef<HTMLDivElement, AlertProps>( ...@@ -29,6 +31,7 @@ export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
endElement, endElement,
loading, loading,
showIcon = false, showIcon = false,
descriptionProps,
...rest ...rest
} = props; } = props;
...@@ -53,7 +56,7 @@ export const Alert = React.forwardRef<HTMLDivElement, AlertProps>( ...@@ -53,7 +56,7 @@ export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
{ children ? ( { children ? (
<ChakraAlert.Content> <ChakraAlert.Content>
{ title && <ChakraAlert.Title>{ title }</ChakraAlert.Title> } { title && <ChakraAlert.Title>{ title }</ChakraAlert.Title> }
<ChakraAlert.Description display="inline-flex">{ children }</ChakraAlert.Description> <ChakraAlert.Description display="inline-flex" flexWrap="wrap" { ...descriptionProps }>{ children }</ChakraAlert.Description>
</ChakraAlert.Content> </ChakraAlert.Content>
) : ( ) : (
<ChakraAlert.Title flex="1">{ title }</ChakraAlert.Title> <ChakraAlert.Title flex="1">{ title }</ChakraAlert.Title>
......
...@@ -12,7 +12,9 @@ export const TabsRoot = React.forwardRef<HTMLDivElement, TabsProps>( ...@@ -12,7 +12,9 @@ export const TabsRoot = React.forwardRef<HTMLDivElement, TabsProps>(
export const TabsList = ChakraTabs.List; export const TabsList = ChakraTabs.List;
export const TabsTrigger = React.forwardRef<HTMLButtonElement, ChakraTabs.TriggerProps>( export interface TabsTriggerProps extends ChakraTabs.TriggerProps {}
export const TabsTrigger = React.forwardRef<HTMLButtonElement, TabsTriggerProps>(
function TabsTrigger(props, ref) { function TabsTrigger(props, ref) {
return <ChakraTabs.Trigger ref={ ref } className="group" { ...props }/>; return <ChakraTabs.Trigger ref={ ref } className="group" { ...props }/>;
}, },
......
...@@ -21,6 +21,8 @@ const AdaptiveTabsMenu = ({ tabs, tabsCut, isActive, ...props }: Props, ref: Rea ...@@ -21,6 +21,8 @@ const AdaptiveTabsMenu = ({ tabs, tabsCut, isActive, ...props }: Props, ref: Rea
<PopoverRoot positioning={{ placement: 'bottom-end' }}> <PopoverRoot positioning={{ placement: 'bottom-end' }}>
<PopoverTrigger> <PopoverTrigger>
<Button <Button
// we use "div" so the :last-of-type pseudo-class targets the last tab and not the menu trigger
as="div"
variant="plain" variant="plain"
color="tabs.solid.fg" color="tabs.solid.fg"
_hover={{ _hover={{
......
...@@ -175,6 +175,15 @@ const semanticTokens: ThemingConfig['semanticTokens'] = { ...@@ -175,6 +175,15 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
DEFAULT: { value: { _light: '{colors.gray.200}', _dark: '{colors.gray.600}' } }, DEFAULT: { value: { _light: '{colors.gray.200}', _dark: '{colors.gray.600}' } },
}, },
}, },
segmented: {
fg: {
DEFAULT: { value: { _light: '{colors.blue.600}', _dark: '{colors.blue.300}' } },
selected: { value: { _light: '{colors.blue.700}', _dark: '{colors.gray.50}' } },
},
border: {
DEFAULT: { value: { _light: '{colors.blue.50}', _dark: '{colors.gray.800}' } },
},
},
}, },
'switch': { 'switch': {
primary: { primary: {
......
...@@ -49,6 +49,21 @@ export const recipe = defineSlotRecipe({ ...@@ -49,6 +49,21 @@ export const recipe = defineSlotRecipe({
}, },
variants: { variants: {
noAnimation: {
'true': {
itemContent: {
_open: {
animationName: 'none',
},
_closed: {
animationName: 'none',
},
},
itemIndicator: {
transition: 'none',
},
},
},
variant: { variant: {
outline: { outline: {
item: { item: {
......
...@@ -124,6 +124,7 @@ export const recipe = defineSlotRecipe({ ...@@ -124,6 +124,7 @@ export const recipe = defineSlotRecipe({
textStyle: 'md', textStyle: 'md',
}, },
}, },
free: {},
}, },
variant: { variant: {
...@@ -178,6 +179,38 @@ export const recipe = defineSlotRecipe({ ...@@ -178,6 +179,38 @@ export const recipe = defineSlotRecipe({
}, },
}, },
}, },
segmented: {
trigger: {
color: 'tabs.segmented.fg',
bg: 'transparent',
borderWidth: '2px',
borderStyle: 'solid',
borderColor: 'tabs.segmented.border',
_hover: {
color: 'link.primary.hover',
},
_selected: {
color: 'tabs.segmented.fg.selected',
bg: 'tabs.segmented.border',
borderColor: 'tabs.segmented.border',
_hover: {
color: 'tabs.segmented.fg.selected',
},
},
_notFirst: {
borderLeftWidth: '0',
},
_first: {
borderTopLeftRadius: 'base',
borderBottomLeftRadius: 'base',
},
_last: {
borderTopRightRadius: 'base',
borderBottomRightRadius: 'base',
},
},
},
unstyled: {},
}, },
}, },
......
import React from 'react'; import React from 'react';
import type { RoutedSubTab } from 'ui/shared/Tabs/types'; import type { TabItemRegular } from 'toolkit/components/AdaptiveTabs/types';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
interface Props { interface Props {
tabs: Array<RoutedSubTab>; tabs: Array<TabItemRegular>;
isLoading: boolean; isLoading: boolean;
shouldRender?: boolean; shouldRender?: boolean;
} }
...@@ -20,7 +20,7 @@ const AddressContract = ({ tabs, isLoading, shouldRender }: Props) => { ...@@ -20,7 +20,7 @@ const AddressContract = ({ tabs, isLoading, shouldRender }: Props) => {
} }
return ( return (
<RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS } isLoading={ isLoading }/> <RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" listProps={ TAB_LIST_PROPS } isLoading={ isLoading }/>
); );
}; };
......
import { import { Flex, chakra } from '@chakra-ui/react';
Flex,
Button,
chakra,
PopoverTrigger,
PopoverBody,
PopoverContent,
Image,
useDisclosure,
useColorModeValue,
} from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import Popover from 'ui/shared/chakra/Popover'; import { Button } from 'toolkit/chakra/button';
import Skeleton from 'ui/shared/chakra/Skeleton'; import { useColorModeValue } from 'toolkit/chakra/color-mode';
import { Image } from 'toolkit/chakra/image';
import { Link } from 'toolkit/chakra/link';
import { PopoverRoot, PopoverTrigger, PopoverContent, PopoverBody } from 'toolkit/chakra/popover';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/links/LinkExternal';
interface Props { interface Props {
className?: string; className?: string;
hash: string; hash: string;
isLoading?: string; isLoading?: boolean;
} }
const ContractCodeIde = ({ className, hash, isLoading }: Props) => { const ContractCodeIde = ({ className, hash, isLoading }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure(); const { open, onOpenChange } = useDisclosure();
const defaultIconColor = useColorModeValue('gray.600', 'gray.500'); const defaultIconColor = useColorModeValue('gray.600', 'gray.500');
const ideLinks = React.useMemo(() => { const ideLinks = React.useMemo(() => {
...@@ -36,16 +30,16 @@ const ContractCodeIde = ({ className, hash, isLoading }: Props) => { ...@@ -36,16 +30,16 @@ const ContractCodeIde = ({ className, hash, isLoading }: Props) => {
<IconSvg name="ABI_slim" boxSize={ 5 } color={ defaultIconColor } mr={ 2 }/>; <IconSvg name="ABI_slim" boxSize={ 5 } color={ defaultIconColor } mr={ 2 }/>;
return ( return (
<LinkExternal key={ ide.title } href={ url } display="inline-flex" alignItems="center"> <Link external key={ ide.title } href={ url } display="inline-flex" alignItems="center">
{ icon } { icon }
{ ide.title } { ide.title }
</LinkExternal> </Link>
); );
}); });
}, [ defaultIconColor, hash ]); }, [ defaultIconColor, hash ]);
if (isLoading) { if (isLoading) {
return <Skeleton h={ 8 } w="92px" borderRadius="base"/>; return <Skeleton loading h={ 8 } w="92px" borderRadius="base"/>;
} }
if (ideLinks.length === 0) { if (ideLinks.length === 0) {
...@@ -53,23 +47,20 @@ const ContractCodeIde = ({ className, hash, isLoading }: Props) => { ...@@ -53,23 +47,20 @@ const ContractCodeIde = ({ className, hash, isLoading }: Props) => {
} }
return ( return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy> <PopoverRoot open={ open } onOpenChange={ onOpenChange }>
<PopoverTrigger> <PopoverTrigger>
<Button <Button
className={ className } className={ className }
size="sm" size="sm"
variant="outline" variant="dropdown"
colorScheme="gray"
onClick={ onToggle }
isActive={ isOpen }
aria-label="Open source code in IDE" aria-label="Open source code in IDE"
fontWeight={ 500 } fontWeight={ 500 }
px={ 2 } gap={ 0 }
h="32px" h="32px"
flexShrink={ 0 } flexShrink={ 0 }
> >
<span>Open in</span> <span>Open in</span>
<IconSvg name="arrows/east-mini" transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } transitionDuration="faster" boxSize={ 5 }/> <IconSvg name="arrows/east-mini" transform={ open ? 'rotate(90deg)' : 'rotate(-90deg)' } transitionDuration="faster" boxSize={ 5 }/>
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent w="240px"> <PopoverContent w="240px">
...@@ -86,7 +77,7 @@ const ContractCodeIde = ({ className, hash, isLoading }: Props) => { ...@@ -86,7 +77,7 @@ const ContractCodeIde = ({ className, hash, isLoading }: Props) => {
</Flex> </Flex>
</PopoverBody> </PopoverBody>
</PopoverContent> </PopoverContent>
</Popover> </PopoverRoot>
); );
}; };
......
...@@ -15,8 +15,8 @@ import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; ...@@ -15,8 +15,8 @@ import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import * as stubs from 'stubs/contract'; import * as stubs from 'stubs/contract';
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import ContractDetailsAlerts from './alerts/ContractDetailsAlerts'; import ContractDetailsAlerts from './alerts/ContractDetailsAlerts';
import ContractSourceAddressSelector from './ContractSourceAddressSelector'; import ContractSourceAddressSelector from './ContractSourceAddressSelector';
...@@ -114,10 +114,10 @@ const ContractDetails = ({ addressHash, channel, mainContractQuery }: Props) => ...@@ -114,10 +114,10 @@ const ContractDetails = ({ addressHash, channel, mainContractQuery }: Props) =>
<RoutedTabs <RoutedTabs
tabs={ tabs } tabs={ tabs }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
variant="radio_group" variant="segmented"
size="sm" size="sm"
leftSlot={ addressSelector } leftSlot={ addressSelector }
tabListProps={ TAB_LIST_PROPS } listProps={ TAB_LIST_PROPS }
leftSlotProps={ LEFT_SLOT_PROPS } leftSlotProps={ LEFT_SLOT_PROPS }
/> />
) : ( ) : (
......
import { Flex, Text, Tooltip } from '@chakra-ui/react'; import { Flex, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { SmartContract } from 'types/api/contract'; import type { SmartContract } from 'types/api/contract';
...@@ -6,9 +6,10 @@ import type { SmartContract } from 'types/api/contract'; ...@@ -6,9 +6,10 @@ import type { SmartContract } from 'types/api/contract';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import formatLanguageName from 'lib/contracts/formatLanguageName'; import formatLanguageName from 'lib/contracts/formatLanguageName';
import Skeleton from 'ui/shared/chakra/Skeleton'; import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { Tooltip } from 'toolkit/chakra/tooltip';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import LinkInternal from 'ui/shared/links/LinkInternal';
import CodeEditor from 'ui/shared/monaco/CodeEditor'; import CodeEditor from 'ui/shared/monaco/CodeEditor';
import formatFilePath from 'ui/shared/monaco/utils/formatFilePath'; import formatFilePath from 'ui/shared/monaco/utils/formatFilePath';
...@@ -54,10 +55,10 @@ export const ContractSourceCode = ({ data, isLoading, sourceAddress }: Props) => ...@@ -54,10 +55,10 @@ export const ContractSourceCode = ({ data, isLoading, sourceAddress }: Props) =>
}, [ data ]); }, [ data ]);
const heading = ( const heading = (
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 }> <Skeleton loading={ isLoading } fontWeight={ 500 }>
<span>Contract source code</span> <span>Contract source code</span>
{ data?.language && { data?.language &&
<Text whiteSpace="pre" as="span" variant="secondary"> ({ formatLanguageName(data.language) })</Text> } <Text whiteSpace="pre" as="span" color="text.secondary"> ({ formatLanguageName(data.language) })</Text> }
</Skeleton> </Skeleton>
); );
...@@ -66,16 +67,16 @@ export const ContractSourceCode = ({ data, isLoading, sourceAddress }: Props) => ...@@ -66,16 +67,16 @@ export const ContractSourceCode = ({ data, isLoading, sourceAddress }: Props) =>
null; null;
const diagramLink = data?.can_be_visualized_via_sol2uml ? ( const diagramLink = data?.can_be_visualized_via_sol2uml ? (
<Tooltip label="Visualize contract code using Sol2Uml JS library"> <Tooltip content="Visualize contract code using Sol2Uml JS library">
<LinkInternal <Link
href={ route({ pathname: '/visualize/sol2uml', query: { address: sourceAddress } }) } href={ route({ pathname: '/visualize/sol2uml', query: { address: sourceAddress } }) }
ml={{ base: '0', lg: 'auto' }} ml={{ base: '0', lg: 'auto' }}
isLoading={ isLoading } loading={ isLoading }
> >
<Skeleton isLoaded={ !isLoading }> <Skeleton loading={ isLoading }>
View UML diagram View UML diagram
</Skeleton> </Skeleton>
</LinkInternal> </Link>
</Tooltip> </Tooltip>
) : null; ) : null;
...@@ -83,7 +84,7 @@ export const ContractSourceCode = ({ data, isLoading, sourceAddress }: Props) => ...@@ -83,7 +84,7 @@ export const ContractSourceCode = ({ data, isLoading, sourceAddress }: Props) =>
<ContractCodeIdes hash={ sourceAddress } isLoading={ isLoading }/> : <ContractCodeIdes hash={ sourceAddress } isLoading={ isLoading }/> :
null; null;
const copyToClipboard = data && editorData?.length === 1 ? ( const copyToClipboard = data && editorData?.length === 1 && data.source_code ? (
<CopyToClipboard <CopyToClipboard
text={ data.source_code } text={ data.source_code }
isLoading={ isLoading } isLoading={ isLoading }
...@@ -94,7 +95,7 @@ export const ContractSourceCode = ({ data, isLoading, sourceAddress }: Props) => ...@@ -94,7 +95,7 @@ export const ContractSourceCode = ({ data, isLoading, sourceAddress }: Props) =>
const content = (() => { const content = (() => {
if (isLoading) { if (isLoading) {
return <Skeleton h="557px" w="100%"/>; return <Skeleton loading h="557px" w="100%"/>;
} }
if (!editorData) { if (!editorData) {
......
import { Alert } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { SmartContractProxyType } from 'types/api/contract'; import type { SmartContractProxyType } from 'types/api/contract';
import LinkExternal from 'ui/shared/links/LinkExternal'; import { Alert } from 'toolkit/chakra/alert';
import { Link } from 'toolkit/chakra/link';
interface Props { interface Props {
type: NonNullable<SmartContractProxyType>; type: NonNullable<SmartContractProxyType>;
...@@ -70,10 +70,10 @@ const ContractCodeProxyPattern = ({ type }: Props) => { ...@@ -70,10 +70,10 @@ const ContractCodeProxyPattern = ({ type }: Props) => {
} }
return ( return (
<Alert status="warning" flexWrap="wrap" whiteSpace="pre-wrap"> <Alert status="warning" whiteSpace="pre-wrap">
{ proxyInfo.link ? ( { proxyInfo.link ? (
<> <>
This proxy smart-contract is detected via <LinkExternal href={ proxyInfo.link }>{ proxyInfo.name }</LinkExternal> This proxy smart-contract is detected via <Link href={ proxyInfo.link } external>{ proxyInfo.name }</Link>
{ proxyInfo.description && ` - ${ proxyInfo.description }` } { proxyInfo.description && ` - ${ proxyInfo.description }` }
</> </>
) : ( ) : (
......
import { Alert } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { SmartContract } from 'types/api/contract'; import type { SmartContract } from 'types/api/contract';
import LinkExternal from 'ui/shared/links/LinkExternal'; import { Alert } from 'toolkit/chakra/alert';
import { Link } from 'toolkit/chakra/link';
interface Props { interface Props {
data: SmartContract | undefined; data: SmartContract | undefined;
...@@ -12,23 +12,24 @@ interface Props { ...@@ -12,23 +12,24 @@ interface Props {
const ContractDetailsAlertVerificationSource = ({ data }: Props) => { const ContractDetailsAlertVerificationSource = ({ data }: Props) => {
if (data?.is_verified_via_eth_bytecode_db) { if (data?.is_verified_via_eth_bytecode_db) {
return ( return (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap"> <Alert status="warning" whiteSpace="pre-wrap">
<span>This contract has been { data.is_partially_verified ? 'partially ' : '' }verified using </span> <span>This contract has been { data.is_partially_verified ? 'partially ' : '' }verified using </span>
<LinkExternal <Link
href="https://docs.blockscout.com/about/features/ethereum-bytecode-database-microservice" href="https://docs.blockscout.com/about/features/ethereum-bytecode-database-microservice"
fontSize="md" textStyle="md"
external
> >
Blockscout Bytecode Database Blockscout Bytecode Database
</LinkExternal> </Link>
</Alert> </Alert>
); );
} }
if (data?.is_verified_via_sourcify) { if (data?.is_verified_via_sourcify) {
return ( return (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap"> <Alert status="warning" whiteSpace="pre-wrap">
<span>This contract has been { data.is_partially_verified ? 'partially ' : '' }verified via Sourcify. </span> <span>This contract has been { data.is_partially_verified ? 'partially ' : '' }verified via Sourcify. </span>
{ data.sourcify_repo_url && <LinkExternal href={ data.sourcify_repo_url } fontSize="md">View contract in Sourcify repository</LinkExternal> } { data.sourcify_repo_url && <Link href={ data.sourcify_repo_url } textStyle="md" external>View contract in Sourcify repository</Link> }
</Alert> </Alert>
); );
} }
......
import { chakra, Alert, Box, Flex } from '@chakra-ui/react'; import { chakra, Box, Flex } from '@chakra-ui/react';
import type { Channel } from 'phoenix'; import type { Channel } from 'phoenix';
import React from 'react'; import React from 'react';
...@@ -8,10 +8,10 @@ import type { SmartContract } from 'types/api/contract'; ...@@ -8,10 +8,10 @@ import type { SmartContract } from 'types/api/contract';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import Skeleton from 'ui/shared/chakra/Skeleton'; import { Alert } from 'toolkit/chakra/alert';
import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import LinkExternal from 'ui/shared/links/LinkExternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ContractDetailsVerificationButton from '../ContractDetailsVerificationButton'; import ContractDetailsVerificationButton from '../ContractDetailsVerificationButton';
import ContractDetailsAlertProxyPattern from './ContractDetailsAlertProxyPattern'; import ContractDetailsAlertProxyPattern from './ContractDetailsAlertProxyPattern';
...@@ -42,14 +42,14 @@ const ContractDetailsAlerts = ({ data, isLoading, addressHash, channel }: Props) ...@@ -42,14 +42,14 @@ const ContractDetailsAlerts = ({ data, isLoading, addressHash, channel }: Props)
{ data?.is_blueprint && ( { data?.is_blueprint && (
<Box> <Box>
<span>This is an </span> <span>This is an </span>
<LinkExternal href="https://eips.ethereum.org/EIPS/eip-5202"> <Link external href="https://eips.ethereum.org/EIPS/eip-5202">
ERC-5202 Blueprint contract ERC-5202 Blueprint contract
</LinkExternal> </Link>
</Box> </Box>
) } ) }
{ data?.is_verified && ( { data?.is_verified && (
<Skeleton isLoaded={ !isLoading }> <Skeleton loading={ isLoading }>
<Alert status="success" flexWrap="wrap" rowGap={ 3 } columnGap={ 5 }> <Alert status="success" descriptionProps={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 3, columnGap: 5 }}>
<span>Contract Source Code Verified ({ data.is_partially_verified ? 'Partial' : 'Exact' } Match)</span> <span>Contract Source Code Verified ({ data.is_partially_verified ? 'Partial' : 'Exact' } Match)</span>
{ {
data.is_partially_verified ? ( data.is_partially_verified ? (
...@@ -70,7 +70,7 @@ const ContractDetailsAlerts = ({ data, isLoading, addressHash, channel }: Props) ...@@ -70,7 +70,7 @@ const ContractDetailsAlerts = ({ data, isLoading, addressHash, channel }: Props)
</Alert> </Alert>
) } ) }
{ !data?.is_verified && data?.verified_twin_address_hash && (!data?.proxy_type || data.proxy_type === 'unknown') && ( { !data?.is_verified && data?.verified_twin_address_hash && (!data?.proxy_type || data.proxy_type === 'unknown') && (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap"> <Alert status="warning" whiteSpace="pre-wrap">
<span>Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB </span> <span>Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB </span>
<AddressEntity <AddressEntity
address={{ hash: data.verified_twin_address_hash, filecoin: { robust: data.verified_twin_filecoin_robust_address }, is_contract: true }} address={{ hash: data.verified_twin_address_hash, filecoin: { robust: data.verified_twin_filecoin_robust_address }, is_contract: true }}
...@@ -79,9 +79,9 @@ const ContractDetailsAlerts = ({ data, isLoading, addressHash, channel }: Props) ...@@ -79,9 +79,9 @@ const ContractDetailsAlerts = ({ data, isLoading, addressHash, channel }: Props)
fontWeight="500" fontWeight="500"
/> />
<chakra.span mt={ 1 }>All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with </chakra.span> <chakra.span mt={ 1 }>All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with </chakra.span>
<LinkInternal href={ route({ pathname: '/address/[hash]/contract-verification', query: { hash: addressHash } }) }> <Link href={ route({ pathname: '/address/[hash]/contract-verification', query: { hash: addressHash } }) }>
Verify & Publish Verify & Publish
</LinkInternal> </Link>
<span> page</span> <span> page</span>
</Alert> </Alert>
) } ) }
......
import { Box, Button, useDisclosure } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { SmartContractSecurityAuditSubmission } from 'types/api/contract'; import type { SmartContractSecurityAuditSubmission } from 'types/api/contract';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import { Button } from 'toolkit/chakra/button';
import { Link } from 'toolkit/chakra/link';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import ContainerWithScrollY from 'ui/shared/ContainerWithScrollY'; import ContainerWithScrollY from 'ui/shared/ContainerWithScrollY';
import FormModal from 'ui/shared/FormModal'; import FormModal from 'ui/shared/FormModal';
import LinkExternal from 'ui/shared/links/LinkExternal';
import ContractSubmitAuditForm from './ContractSubmitAuditForm'; import ContractSubmitAuditForm from './ContractSubmitAuditForm';
...@@ -46,16 +48,16 @@ const ContractSecurityAudits = ({ addressHash }: Props) => { ...@@ -46,16 +48,16 @@ const ContractSecurityAudits = ({ addressHash }: Props) => {
mt={ 2 } mt={ 2 }
> >
{ data.items.map(item => ( { data.items.map(item => (
<LinkExternal href={ item.audit_report_url } key={ item.audit_company_name + item.audit_publish_date } isLoading={ isPlaceholderData }> <Link external href={ item.audit_report_url } key={ item.audit_company_name + item.audit_publish_date } loading={ isPlaceholderData }>
{ `${ item.audit_company_name }, ${ dayjs(item.audit_publish_date).format('MMM DD, YYYY') }` } { `${ item.audit_company_name }, ${ dayjs(item.audit_publish_date).format('MMM DD, YYYY') }` }
</LinkExternal> </Link>
)) } )) }
</ContainerWithScrollY> </ContainerWithScrollY>
</Box> </Box>
) } ) }
<FormModal<SmartContractSecurityAuditSubmission> <FormModal<SmartContractSecurityAuditSubmission>
isOpen={ modalProps.isOpen } open={ modalProps.open }
onClose={ modalProps.onClose } onOpenChange={ modalProps.onOpenChange }
title={ formTitle } title={ formTitle }
renderForm={ renderForm } renderForm={ renderForm }
/> />
......
import { Button, VStack } from '@chakra-ui/react'; import { VStack } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
...@@ -8,7 +8,8 @@ import type { SmartContractSecurityAuditSubmission } from 'types/api/contract'; ...@@ -8,7 +8,8 @@ import type { SmartContractSecurityAuditSubmission } from 'types/api/contract';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import useToast from 'lib/hooks/useToast'; import { Button } from 'toolkit/chakra/button';
import { toaster } from 'toolkit/chakra/toaster';
import FormFieldCheckbox from 'ui/shared/forms/fields/FormFieldCheckbox'; import FormFieldCheckbox from 'ui/shared/forms/fields/FormFieldCheckbox';
import FormFieldEmail from 'ui/shared/forms/fields/FormFieldEmail'; import FormFieldEmail from 'ui/shared/forms/fields/FormFieldEmail';
import FormFieldText from 'ui/shared/forms/fields/FormFieldText'; import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
...@@ -39,7 +40,6 @@ const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => { ...@@ -39,7 +40,6 @@ const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => {
const containerRef = React.useRef<HTMLFormElement>(null); const containerRef = React.useRef<HTMLFormElement>(null);
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const toast = useToast();
const formApi = useForm<Inputs>({ const formApi = useForm<Inputs>({
mode: 'onTouched', mode: 'onTouched',
...@@ -57,13 +57,9 @@ const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => { ...@@ -57,13 +57,9 @@ const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => {
}, },
}); });
toast({ toaster.success({
position: 'top-right',
title: 'Success', title: 'Success',
description: 'Your audit report has been successfully submitted for review', description: 'Your audit report has been successfully submitted for review',
status: 'success',
variant: 'subtle',
isClosable: true,
}); });
onSuccess(); onSuccess();
...@@ -77,37 +73,32 @@ const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => { ...@@ -77,37 +73,32 @@ const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => {
setError(errorField, { type: 'custom', message: errorMap[errorField].join(', ') }); setError(errorField, { type: 'custom', message: errorMap[errorField].join(', ') });
}); });
} else { } else {
toast({ toaster.error({
position: 'top-right',
title: 'Error', title: 'Error',
description: (_error as ResourceError<{ message: string }>)?.payload?.message || 'Something went wrong. Try again later.', description: (_error as ResourceError<{ message: string }>)?.payload?.message || 'Something went wrong. Try again later.',
status: 'error',
variant: 'subtle',
isClosable: true,
}); });
} }
} }
}, [ apiFetch, address, toast, setError, onSuccess ]); }, [ apiFetch, address, setError, onSuccess ]);
return ( return (
<FormProvider { ...formApi }> <FormProvider { ...formApi }>
<form noValidate onSubmit={ handleSubmit(onFormSubmit) } autoComplete="off" ref={ containerRef }> <form noValidate onSubmit={ handleSubmit(onFormSubmit) } autoComplete="off" ref={ containerRef }>
<VStack gap={ 5 } alignItems="flex-start"> <VStack gap={ 5 } alignItems="flex-start">
<FormFieldText<Inputs> name="submitter_name" isRequired placeholder="Submitter name"/> <FormFieldText<Inputs> name="submitter_name" required placeholder="Submitter name"/>
<FormFieldEmail<Inputs> name="submitter_email" isRequired placeholder="Submitter email"/> <FormFieldEmail<Inputs> name="submitter_email" required placeholder="Submitter email"/>
<FormFieldCheckbox<Inputs, 'is_project_owner'> <FormFieldCheckbox<Inputs, 'is_project_owner'>
name="is_project_owner" name="is_project_owner"
label="I'm the contract owner" label="I'm the contract owner"
/> />
<FormFieldText<Inputs> name="project_name" isRequired placeholder="Project name"/> <FormFieldText<Inputs> name="project_name" required placeholder="Project name"/>
<FormFieldUrl<Inputs> name="project_url" isRequired placeholder="Project URL"/> <FormFieldUrl<Inputs> name="project_url" required placeholder="Project URL"/>
<FormFieldText<Inputs> name="audit_company_name" isRequired placeholder="Audit company name"/> <FormFieldText<Inputs> name="audit_company_name" required placeholder="Audit company name"/>
<FormFieldUrl<Inputs> name="audit_report_url" isRequired placeholder="Audit report URL"/> <FormFieldUrl<Inputs> name="audit_report_url" required placeholder="Audit report URL"/>
<FormFieldText<Inputs> <FormFieldText<Inputs>
name="audit_publish_date" name="audit_publish_date"
type="date" inputProps={{ type: 'date', max: dayjs().format('YYYY-MM-DD') }}
max={ dayjs().format('YYYY-MM-DD') } required
isRequired
placeholder="Audit publish date" placeholder="Audit publish date"
/> />
<FormFieldText<Inputs> <FormFieldText<Inputs>
...@@ -122,9 +113,9 @@ const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => { ...@@ -122,9 +113,9 @@ const ContractSubmitAuditForm = ({ address, onSuccess }: Props) => {
type="submit" type="submit"
size="lg" size="lg"
mt={ 8 } mt={ 8 }
isLoading={ formState.isSubmitting } loading={ formState.isSubmitting }
loadingText="Send request" loadingText="Send request"
isDisabled={ !formState.isDirty } disabled={ !formState.isDirty }
> >
Send request Send request
</Button> </Button>
......
...@@ -6,9 +6,9 @@ import type { SmartContract } from 'types/api/contract'; ...@@ -6,9 +6,9 @@ import type { SmartContract } from 'types/api/contract';
import config from 'configs/app'; import config from 'configs/app';
import { CONTRACT_LICENSES } from 'lib/contracts/licenses'; import { CONTRACT_LICENSES } from 'lib/contracts/licenses';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import { Link } from 'toolkit/chakra/link';
import { getGitHubOwnerAndRepo } from 'ui/contractVerification/utils'; import { getGitHubOwnerAndRepo } from 'ui/contractVerification/utils';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel'; import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import LinkExternal from 'ui/shared/links/LinkExternal';
import ContractSecurityAudits from '../audits/ContractSecurityAudits'; import ContractSecurityAudits from '../audits/ContractSecurityAudits';
import ContractDetailsInfoItem from './ContractDetailsInfoItem'; import ContractDetailsInfoItem from './ContractDetailsInfoItem';
...@@ -40,9 +40,9 @@ const ContractDetailsInfo = ({ data, isLoading, addressHash }: Props) => { ...@@ -40,9 +40,9 @@ const ContractDetailsInfo = ({ data, isLoading, addressHash }: Props) => {
} }
return ( return (
<LinkExternal href={ license.url }> <Link external href={ license.url }>
{ license.label } { license.label }
</LinkExternal> </Link>
); );
})(); })();
...@@ -57,9 +57,9 @@ const ContractDetailsInfo = ({ data, isLoading, addressHash }: Props) => { ...@@ -57,9 +57,9 @@ const ContractDetailsInfo = ({ data, isLoading, addressHash }: Props) => {
const commit = data.github_repository_metadata.commit; const commit = data.github_repository_metadata.commit;
const pathPrefix = data.github_repository_metadata.path_prefix; const pathPrefix = data.github_repository_metadata.path_prefix;
return ( return (
<LinkExternal href={ `${ repoUrl }/tree/${ commit }${ pathPrefix ? `/${ pathPrefix }` : '' }` }> <Link external href={ `${ repoUrl }/tree/${ commit }${ pathPrefix ? `/${ pathPrefix }` : '' }` }>
{ owner && repo ? `${ owner }/${ repo }` : data.github_repository_metadata.repository_url } { owner && repo ? `${ owner }/${ repo }` : data.github_repository_metadata.repository_url }
</LinkExternal> </Link>
); );
})(); })();
...@@ -70,90 +70,102 @@ const ContractDetailsInfo = ({ data, isLoading, addressHash }: Props) => { ...@@ -70,90 +70,102 @@ const ContractDetailsInfo = ({ data, isLoading, addressHash }: Props) => {
{ data.name && ( { data.name && (
<ContractDetailsInfoItem <ContractDetailsInfoItem
label="Contract name" label="Contract name"
content={ contractNameWithCertifiedIcon }
isLoading={ isLoading } isLoading={ isLoading }
/> >
{ contractNameWithCertifiedIcon }
</ContractDetailsInfoItem>
) } ) }
{ data.compiler_version && ( { data.compiler_version && (
<ContractDetailsInfoItem <ContractDetailsInfoItem
label="Compiler version" label="Compiler version"
content={ data.compiler_version }
isLoading={ isLoading } isLoading={ isLoading }
/> >
{ data.compiler_version }
</ContractDetailsInfoItem>
) } ) }
{ data.zk_compiler_version && ( { data.zk_compiler_version && (
<ContractDetailsInfoItem <ContractDetailsInfoItem
label="ZK compiler version" label="ZK compiler version"
content={ data.zk_compiler_version }
isLoading={ isLoading } isLoading={ isLoading }
/> >
{ data.zk_compiler_version }
</ContractDetailsInfoItem>
) } ) }
{ data.evm_version && ( { data.evm_version && (
<ContractDetailsInfoItem <ContractDetailsInfoItem
label="EVM version" label="EVM version"
content={ data.evm_version }
textTransform="capitalize" textTransform="capitalize"
isLoading={ isLoading } isLoading={ isLoading }
/> >
{ data.evm_version }
</ContractDetailsInfoItem>
) } ) }
{ licenseLink && ( { licenseLink && (
<ContractDetailsInfoItem <ContractDetailsInfoItem
label="License" label="License"
content={ licenseLink }
hint="License type is entered manually during verification. The initial source code may contain a different license type than the one displayed." hint="License type is entered manually during verification. The initial source code may contain a different license type than the one displayed."
isLoading={ isLoading } isLoading={ isLoading }
/> >
{ licenseLink }
</ContractDetailsInfoItem>
) } ) }
{ typeof data.optimization_enabled === 'boolean' && !isStylusContract && ( { typeof data.optimization_enabled === 'boolean' && !isStylusContract && (
<ContractDetailsInfoItem <ContractDetailsInfoItem
label="Optimization enabled" label="Optimization enabled"
content={ data.optimization_enabled ? 'true' : 'false' }
isLoading={ isLoading } isLoading={ isLoading }
/> >
{ data.optimization_enabled ? 'true' : 'false' }
</ContractDetailsInfoItem>
) } ) }
{ data.optimization_runs !== null && !isStylusContract && ( { data.optimization_runs !== null && !isStylusContract && (
<ContractDetailsInfoItem <ContractDetailsInfoItem
label={ rollupFeature.isEnabled && rollupFeature.type === 'zkSync' ? 'Optimization mode' : 'Optimization runs' } label={ rollupFeature.isEnabled && rollupFeature.type === 'zkSync' ? 'Optimization mode' : 'Optimization runs' }
content={ String(data.optimization_runs) }
isLoading={ isLoading } isLoading={ isLoading }
/> >
{ String(data.optimization_runs) }
</ContractDetailsInfoItem>
) } ) }
{ data.package_name && ( { data.package_name && (
<ContractDetailsInfoItem <ContractDetailsInfoItem
label="Package name" label="Package name"
content={ data.package_name }
isLoading={ isLoading } isLoading={ isLoading }
/> >
{ data.package_name }
</ContractDetailsInfoItem>
) } ) }
{ data.verified_at && ( { data.verified_at && (
<ContractDetailsInfoItem <ContractDetailsInfoItem
label="Verified at" label="Verified at"
content={ dayjs(data.verified_at).format('llll') }
wordBreak="break-word" wordBreak="break-word"
isLoading={ isLoading } isLoading={ isLoading }
/> >
{ dayjs(data.verified_at).format('llll') }
</ContractDetailsInfoItem>
) } ) }
{ data.file_path && !isStylusContract && ( { data.file_path && !isStylusContract && (
<ContractDetailsInfoItem <ContractDetailsInfoItem
label="Contract file path" label="Contract file path"
content={ data.file_path }
wordBreak="break-word" wordBreak="break-word"
isLoading={ isLoading } isLoading={ isLoading }
/> >
{ data.file_path }
</ContractDetailsInfoItem>
) } ) }
{ sourceCodeLink && ( { sourceCodeLink && (
<ContractDetailsInfoItem <ContractDetailsInfoItem
label="Source code" label="Source code"
content={ sourceCodeLink }
isLoading={ isLoading } isLoading={ isLoading }
/> >
{ sourceCodeLink }
</ContractDetailsInfoItem>
) } ) }
{ config.UI.hasContractAuditReports && ( { config.UI.hasContractAuditReports && (
<ContractDetailsInfoItem <ContractDetailsInfoItem
label="Security audit" label="Security audit"
content={ <ContractSecurityAudits addressHash={ addressHash }/> }
isLoading={ isLoading } isLoading={ isLoading }
/> >
<ContractSecurityAudits addressHash={ addressHash }/>
</ContractDetailsInfoItem>
) } ) }
</Grid> </Grid>
); );
......
import { chakra, useColorModeValue, Flex, GridItem } from '@chakra-ui/react'; import { chakra, Flex, GridItem } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import Skeleton from 'ui/shared/chakra/Skeleton'; import { Skeleton } from 'toolkit/chakra/skeleton';
import Hint from 'ui/shared/Hint'; import Hint from 'ui/shared/Hint';
interface Props { interface Props {
label: string; label: string;
content: string | React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
isLoading: boolean; isLoading: boolean;
hint?: string; hint?: string;
} }
const ContractDetailsInfoItem = ({ label, content, className, isLoading, hint }: Props) => { const ContractDetailsInfoItem = ({ label, children, className, isLoading, hint }: Props) => {
const hintIconColor = useColorModeValue('gray.600', 'gray.400');
return ( return (
<GridItem display="flex" columnGap={ 6 } wordBreak="break-all" className={ className } alignItems="baseline"> <GridItem display="flex" columnGap={ 6 } wordBreak="break-all" className={ className } alignItems="baseline">
<Skeleton isLoaded={ !isLoading } w="170px" flexShrink={ 0 } fontWeight={ 500 }> <Skeleton loading={ isLoading } w="170px" flexShrink={ 0 } fontWeight={ 500 }>
<Flex alignItems="center"> <Flex alignItems="center">
{ label } { label }
{ hint && ( { hint && (
<Hint <Hint
label={ hint } label={ hint }
ml={ 2 } ml={ 2 }
color={ hintIconColor } color={{ _light: 'gray.600', _dark: 'gray.400' }}
tooltipProps={{ placement: 'bottom' }} tooltipProps={{ positioning: { placement: 'bottom' } }}
/> />
) } ) }
</Flex> </Flex>
</Skeleton> </Skeleton>
<Skeleton isLoaded={ !isLoading }>{ content }</Skeleton> <Skeleton loading={ isLoading }>{ children }</Skeleton>
</GridItem> </GridItem>
); );
}; };
......
import { Alert, Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { SmartContract } from 'types/api/contract'; import type { SmartContract } from 'types/api/contract';
import { Alert } from 'toolkit/chakra/alert';
import CodeViewSnippet from 'ui/shared/CodeViewSnippet'; import CodeViewSnippet from 'ui/shared/CodeViewSnippet';
import RawDataSnippet from 'ui/shared/RawDataSnippet'; import RawDataSnippet from 'ui/shared/RawDataSnippet';
......
...@@ -74,12 +74,12 @@ export default function useContractTabs(data: Address | undefined, isPlaceholder ...@@ -74,12 +74,12 @@ export default function useContractTabs(data: Address | undefined, isPlaceholder
return React.useMemo(() => { return React.useMemo(() => {
return { return {
tabs: [ tabs: [
// data?.hash && { data?.hash && {
// id: 'contract_code' as const, id: 'contract_code' as const,
// title: 'Code', title: 'Code',
// component: <ContractDetails mainContractQuery={ contractQuery } channel={ channel } addressHash={ data.hash }/>, component: <ContractDetails mainContractQuery={ contractQuery } channel={ channel } addressHash={ data.hash }/>,
// subTabs: CONTRACT_DETAILS_TAB_IDS as unknown as Array<string>, subTabs: CONTRACT_DETAILS_TAB_IDS as unknown as Array<string>,
// }, },
// contractQuery.data?.abi && { // contractQuery.data?.abi && {
// id: [ 'read_write_contract' as const, 'read_contract' as const, 'write_contract' as const ], // id: [ 'read_write_contract' as const, 'read_contract' as const, 'write_contract' as const ],
// title: 'Read/Write contract', // title: 'Read/Write contract',
......
...@@ -235,29 +235,29 @@ const AddressPageContent = () => { ...@@ -235,29 +235,29 @@ const AddressPageContent = () => {
} : } :
undefined, undefined,
// addressQuery.data?.is_contract ? { addressQuery.data?.is_contract ? {
// id: 'contract', id: 'contract',
// title: () => { title: () => {
// if (addressQuery.data.is_verified) { if (addressQuery.data.is_verified) {
// return ( return (
// <> <>
// <span>Contract</span> <span>Contract</span>
// <IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 }/> <IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 }/>
// </> </>
// ); );
// } }
// return 'Contract'; return 'Contract';
// }, },
// component: ( component: (
// <AddressContract <AddressContract
// tabs={ contractTabs.tabs } tabs={ contractTabs.tabs }
// shouldRender={ !isTabsLoading } shouldRender={ !isTabsLoading }
// isLoading={ contractTabs.isLoading } isLoading={ contractTabs.isLoading }
// /> />
// ), ),
// subTabs: CONTRACT_TAB_IDS, subTabs: CONTRACT_TAB_IDS,
// } : undefined, } : undefined,
].filter(Boolean); ].filter(Boolean);
}, [ }, [
addressQuery.data, addressQuery.data,
......
import { Box, chakra, Flex } from '@chakra-ui/react'; import { Box, chakra, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import Skeleton from 'ui/shared/chakra/Skeleton'; import { Skeleton } from 'toolkit/chakra/skeleton';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import CodeEditor from 'ui/shared/monaco/CodeEditor'; import CodeEditor from 'ui/shared/monaco/CodeEditor';
...@@ -25,12 +25,12 @@ const CodeViewSnippet = ({ data, copyData, language, title, className, rightSlot ...@@ -25,12 +25,12 @@ const CodeViewSnippet = ({ data, copyData, language, title, className, rightSlot
<Box className={ className } as="section" title={ title }> <Box className={ className } as="section" title={ title }>
{ (title || rightSlot) && ( { (title || rightSlot) && (
<Flex justifyContent={ title ? 'space-between' : 'flex-end' } alignItems="center" mb={ 3 }> <Flex justifyContent={ title ? 'space-between' : 'flex-end' } alignItems="center" mb={ 3 }>
{ title && <Skeleton fontWeight={ 500 } isLoaded={ !isLoading }>{ title }</Skeleton> } { title && <Skeleton loading={ isLoading } fontWeight={ 500 }>{ title }</Skeleton> }
{ rightSlot } { rightSlot }
<CopyToClipboard text={ copyData ?? data } isLoading={ isLoading }/> <CopyToClipboard text={ copyData ?? data } isLoading={ isLoading }/>
</Flex> </Flex>
) } ) }
{ isLoading ? <Skeleton height="500px" w="100%"/> : <CodeEditor data={ editorData } language={ language }/> } { isLoading ? <Skeleton loading height="500px" w="100%"/> : <CodeEditor data={ editorData } language={ language }/> }
</Box> </Box>
); );
}; };
......
...@@ -87,7 +87,7 @@ const CoinzillaTextAd = ({ className }: { className?: string }) => { ...@@ -87,7 +87,7 @@ const CoinzillaTextAd = ({ className }: { className?: string }) => {
src={ adData.ad.thumbnail } src={ adData.ad.thumbnail }
width="20px" width="20px"
height="20px" height="20px"
mb="-4px" mb="2px"
mr={ 1 } mr={ 1 }
display="inline-block" display="inline-block"
alt="" alt=""
......
import type { HTMLChakraProps } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { FieldValues, Path } from 'react-hook-form'; import type { FieldValues, Path } from 'react-hook-form';
import { useController, useFormContext } from 'react-hook-form'; import { useController, useFormContext } from 'react-hook-form';
...@@ -6,7 +5,9 @@ import { useController, useFormContext } from 'react-hook-form'; ...@@ -6,7 +5,9 @@ import { useController, useFormContext } from 'react-hook-form';
import type { FormFieldPropsBase } from './types'; import type { FormFieldPropsBase } from './types';
import { Field } from 'toolkit/chakra/field'; import { Field } from 'toolkit/chakra/field';
import type { InputProps } from 'toolkit/chakra/input';
import { Input } from 'toolkit/chakra/input'; import { Input } from 'toolkit/chakra/input';
import type { TextareaProps } from 'toolkit/chakra/textarea';
import { Textarea } from 'toolkit/chakra/textarea'; import { Textarea } from 'toolkit/chakra/textarea';
import getFieldErrorText from '../utils/getFieldErrorText'; import getFieldErrorText from '../utils/getFieldErrorText';
...@@ -49,14 +50,14 @@ const FormFieldText = < ...@@ -49,14 +50,14 @@ const FormFieldText = <
<Textarea <Textarea
{ ...field } { ...field }
autoComplete="off" autoComplete="off"
{ ...inputProps as HTMLChakraProps<'textarea'> } { ...inputProps as TextareaProps }
onBlur={ handleBlur } onBlur={ handleBlur }
/> />
) : ( ) : (
<Input <Input
{ ...field } { ...field }
autoComplete="off" autoComplete="off"
{ ...inputProps as HTMLChakraProps<'input'> } { ...inputProps as InputProps }
onBlur={ handleBlur } onBlur={ handleBlur }
/> />
); );
......
import type { HTMLChakraProps } from '@chakra-ui/react';
import type React from 'react'; import type React from 'react';
import type { ControllerRenderProps, FieldValues, Path, RegisterOptions } from 'react-hook-form'; import type { ControllerRenderProps, FieldValues, Path, RegisterOptions } from 'react-hook-form';
import type { FieldProps } from 'toolkit/chakra/field'; import type { FieldProps } from 'toolkit/chakra/field';
import type { InputProps } from 'toolkit/chakra/input';
import type { TextareaProps } from 'toolkit/chakra/textarea';
export interface FormFieldPropsBase< export interface FormFieldPropsBase<
FormFields extends FieldValues, FormFields extends FieldValues,
...@@ -14,5 +15,5 @@ export interface FormFieldPropsBase< ...@@ -14,5 +15,5 @@ export interface FormFieldPropsBase<
onBlur?: () => void; onBlur?: () => void;
onChange?: () => void; onChange?: () => void;
rightElement?: ({ field }: { field: ControllerRenderProps<FormFields, Name> }) => React.ReactNode; rightElement?: ({ field }: { field: ControllerRenderProps<FormFields, Name> }) => React.ReactNode;
inputProps?: HTMLChakraProps<'input' | 'textarea'>; inputProps?: InputProps | TextareaProps;
} }
import type { SystemStyleObject } from '@chakra-ui/react'; import type { SystemStyleObject } from '@chakra-ui/react';
import { Box, useColorMode, Flex, useToken, Center } from '@chakra-ui/react'; import { Box, Flex, useToken, Center } from '@chakra-ui/react';
import type { EditorProps } from '@monaco-editor/react'; import type { EditorProps } from '@monaco-editor/react';
import MonacoEditor from '@monaco-editor/react'; import MonacoEditor from '@monaco-editor/react';
import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
...@@ -11,6 +11,7 @@ import type { SmartContractExternalLibrary } from 'types/api/contract'; ...@@ -11,6 +11,7 @@ import type { SmartContractExternalLibrary } from 'types/api/contract';
import useClientRect from 'lib/hooks/useClientRect'; import useClientRect from 'lib/hooks/useClientRect';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import isMetaKey from 'lib/isMetaKey'; import isMetaKey from 'lib/isMetaKey';
import { useColorMode } from 'toolkit/chakra/color-mode';
import ErrorBoundary from 'ui/shared/ErrorBoundary'; import ErrorBoundary from 'ui/shared/ErrorBoundary';
import CodeEditorBreadcrumbs from './CodeEditorBreadcrumbs'; import CodeEditorBreadcrumbs from './CodeEditorBreadcrumbs';
...@@ -56,7 +57,7 @@ const CodeEditor = ({ data, remappings, libraries, language, mainFile, contractN ...@@ -56,7 +57,7 @@ const CodeEditor = ({ data, remappings, libraries, language, mainFile, contractN
const [ containerRect, containerNodeRef ] = useClientRect<HTMLDivElement>(); const [ containerRect, containerNodeRef ] = useClientRect<HTMLDivElement>();
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
const borderRadius = useToken('radii', 'md'); const [ borderRadius ] = useToken('radii', 'md');
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const themeColors = useThemeColors(); const themeColors = useThemeColors();
...@@ -202,28 +203,28 @@ const CodeEditor = ({ data, remappings, libraries, language, mainFile, contractN ...@@ -202,28 +203,28 @@ const CodeEditor = ({ data, remappings, libraries, language, mainFile, contractN
setIsMetaPressed(false); setIsMetaPressed(false);
}, []); }, []);
const containerSx: SystemStyleObject = React.useMemo(() => ({ const containerCss: SystemStyleObject = React.useMemo(() => ({
'.editor-container': { '& .editor-container': {
position: 'absolute', position: 'absolute',
top: 0, top: 0,
left: 0, left: 0,
width: `${ editorWidth }px`, width: `${ editorWidth }px`,
height: '100%', height: '100%',
}, },
'.monaco-editor': { '& .monaco-editor': {
'border-bottom-left-radius': borderRadius, 'border-bottom-left-radius': borderRadius,
}, },
'.monaco-editor .overflow-guard': { '& .monaco-editor .overflow-guard': {
'border-bottom-left-radius': borderRadius, 'border-bottom-left-radius': borderRadius,
}, },
'.monaco-editor .core-guide': { '& .monaco-editor .core-guide': {
zIndex: 1, zIndex: 1,
}, },
// '.monaco-editor .currentFindMatch': // TODO: find a better way to style this // '.monaco-editor .currentFindMatch': // TODO: find a better way to style this
'.monaco-editor .findMatch': { '& .monaco-editor .findMatch': {
backgroundColor: themeColors['custom.findMatchHighlightBackground'], backgroundColor: themeColors['custom.findMatchHighlightBackground'],
}, },
'.highlight': { '& .highlight': {
backgroundColor: themeColors['custom.findMatchHighlightBackground'], backgroundColor: themeColors['custom.findMatchHighlightBackground'],
}, },
'&&.meta-pressed .import-link:hover, &&.meta-pressed .import-link:hover + .import-link': { '&&.meta-pressed .import-link:hover, &&.meta-pressed .import-link:hover + .import-link': {
...@@ -231,19 +232,19 @@ const CodeEditor = ({ data, remappings, libraries, language, mainFile, contractN ...@@ -231,19 +232,19 @@ const CodeEditor = ({ data, remappings, libraries, language, mainFile, contractN
textDecoration: 'underline', textDecoration: 'underline',
cursor: 'pointer', cursor: 'pointer',
}, },
'.risk-warning-primary': { '& .risk-warning-primary': {
backgroundColor: themeColors['custom.riskWarning.primaryBackground'], backgroundColor: themeColors['custom.riskWarning.primaryBackground'],
}, },
'.risk-warning': { '& .risk-warning': {
backgroundColor: themeColors['custom.riskWarning.background'], backgroundColor: themeColors['custom.riskWarning.background'],
}, },
'.main-contract-header': { '& .main-contract-header': {
backgroundColor: themeColors['custom.mainContract.header'], backgroundColor: themeColors['custom.mainContract.header'],
}, },
'.main-contract-body': { '& .main-contract-body': {
backgroundColor: themeColors['custom.mainContract.body'], backgroundColor: themeColors['custom.mainContract.body'],
}, },
'.main-contract-glyph': { '& .main-contract-glyph': {
zIndex: 1, zIndex: 1,
background: 'url(/static/contract_star.png) no-repeat center center', background: 'url(/static/contract_star.png) no-repeat center center',
backgroundSize: '12px', backgroundSize: '12px',
...@@ -256,18 +257,18 @@ const CodeEditor = ({ data, remappings, libraries, language, mainFile, contractN ...@@ -256,18 +257,18 @@ const CodeEditor = ({ data, remappings, libraries, language, mainFile, contractN
}, [ themeColors ]); }, [ themeColors ]);
if (data.length === 1) { if (data.length === 1) {
const sx = { const css = {
...containerSx, ...containerCss,
'.monaco-editor': { '& .monaco-editor': {
'border-radius': borderRadius, 'border-radius': borderRadius,
}, },
'.monaco-editor .overflow-guard': { '& .monaco-editor .overflow-guard': {
'border-radius': borderRadius, 'border-radius': borderRadius,
}, },
}; };
return ( return (
<Box height={ `${ EDITOR_HEIGHT }px` } width="100%" sx={ sx } ref={ containerNodeRef }> <Box height={ `${ EDITOR_HEIGHT }px` } width="100%" css={ css } ref={ containerNodeRef }>
<ErrorBoundary renderErrorScreen={ renderErrorScreen }> <ErrorBoundary renderErrorScreen={ renderErrorScreen }>
<MonacoEditor <MonacoEditor
className="editor-container" className="editor-container"
...@@ -290,7 +291,7 @@ const CodeEditor = ({ data, remappings, libraries, language, mainFile, contractN ...@@ -290,7 +291,7 @@ const CodeEditor = ({ data, remappings, libraries, language, mainFile, contractN
height={ `${ EDITOR_HEIGHT + TABS_HEIGHT + BREADCRUMBS_HEIGHT }px` } height={ `${ EDITOR_HEIGHT + TABS_HEIGHT + BREADCRUMBS_HEIGHT }px` }
position="relative" position="relative"
ref={ containerNodeRef } ref={ containerNodeRef }
sx={ containerSx } css={ containerCss }
overflow={{ base: 'hidden', lg: 'visible' }} overflow={{ base: 'hidden', lg: 'visible' }}
borderRadius="md" borderRadius="md"
onClick={ handleClick } onClick={ handleClick }
......
import type { ChakraProps } from '@chakra-ui/react'; import type { AccordionItemProps } from '@chakra-ui/react';
import { Box, Accordion, AccordionButton, AccordionItem, AccordionPanel, chakra } from '@chakra-ui/react'; import { Box, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { FileTree } from './types'; import type { FileTree } from './types';
import { AccordionItem, AccordionItemContent, AccordionItemTrigger, AccordionRoot } from 'toolkit/chakra/accordion';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import CodeEditorFileIcon from './CodeEditorFileIcon'; import CodeEditorFileIcon from './CodeEditorFileIcon';
...@@ -20,7 +21,13 @@ interface Props { ...@@ -20,7 +21,13 @@ interface Props {
} }
const CodeEditorFileTree = ({ tree, level = 0, onItemClick, isCollapsed, selectedFile, mainFile }: Props) => { const CodeEditorFileTree = ({ tree, level = 0, onItemClick, isCollapsed, selectedFile, mainFile }: Props) => {
const itemProps: ChakraProps = { const [ value, setValue ] = React.useState<Array<string>>(isCollapsed ? [] : tree.map((item) => item.name));
const handleValueChange = React.useCallback(({ value }: { value: Array<string> }) => {
setValue(value);
}, []);
const itemProps: Partial<AccordionItemProps> = {
borderWidth: '0px', borderWidth: '0px',
cursor: 'pointer', cursor: 'pointer',
lineHeight: '22px', lineHeight: '22px',
...@@ -31,47 +38,52 @@ const CodeEditorFileTree = ({ tree, level = 0, onItemClick, isCollapsed, selecte ...@@ -31,47 +38,52 @@ const CodeEditorFileTree = ({ tree, level = 0, onItemClick, isCollapsed, selecte
const themeColors = useThemeColors(); const themeColors = useThemeColors();
return ( return (
<Accordion allowMultiple defaultIndex={ isCollapsed ? undefined : tree.map((item, index) => index) } reduceMotion> <AccordionRoot multiple value={ value } onValueChange={ handleValueChange } noAnimation>
{ {
tree.map((leaf, index) => { tree.map((leaf, index) => {
const leafName = <chakra.span overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">{ leaf.name }</chakra.span>; const leafName = <chakra.span overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">{ leaf.name }</chakra.span>;
const isExpanded = value.includes(leaf.name);
if ('children' in leaf) { if ('children' in leaf) {
return ( return (
<AccordionItem key={ index } { ...itemProps }> <AccordionItem key={ index } value={ leaf.name } { ...itemProps }>
{ ({ isExpanded }) => ( <AccordionItemTrigger
<> pr="8px"
<AccordionButton py="0"
pr="8px" pl={ `${ 8 + 8 * level }px` }
py="0" _hover={{ bgColor: themeColors['custom.list.hoverBackground'] }}
pl={ `${ 8 + 8 * level }px` } fontSize="13px"
_hover={{ bgColor: themeColors['custom.list.hoverBackground'] }} lineHeight="22px"
fontSize="13px" h="22px"
lineHeight="22px" transitionDuration="0"
h="22px" noIndicator
transitionDuration="0" >
> <Box
<Box className="codicon codicon-tree-item-expanded"
className="codicon codicon-tree-item-expanded" transform="rotate(-90deg)"
transform={ isExpanded ? 'rotate(0deg)' : 'rotate(-90deg)' } _groupExpanded={{
boxSize="16px" transform: 'rotate(0deg)',
mr="2px" }}
/> boxSize="16px"
<IconSvg name={ isExpanded ? 'monaco/folder-open' : 'monaco/folder' } boxSize="16px" mr="4px"/> mr="2px"
{ leafName } />
</AccordionButton> <IconSvg
<AccordionPanel p="0"> name={ isExpanded ? 'monaco/folder-open' : 'monaco/folder' }
<CodeEditorFileTree boxSize="16px"
tree={ leaf.children } mr="4px"
level={ level + 1 } />
onItemClick={ onItemClick } { leafName }
isCollapsed={ isCollapsed } </AccordionItemTrigger>
selectedFile={ selectedFile } <AccordionItemContent p="0">
mainFile={ mainFile } <CodeEditorFileTree
/> tree={ leaf.children }
</AccordionPanel> level={ level + 1 }
</> onItemClick={ onItemClick }
) } isCollapsed={ isCollapsed }
selectedFile={ selectedFile }
mainFile={ mainFile }
/>
</AccordionItemContent>
</AccordionItem> </AccordionItem>
); );
} }
...@@ -79,6 +91,7 @@ const CodeEditorFileTree = ({ tree, level = 0, onItemClick, isCollapsed, selecte ...@@ -79,6 +91,7 @@ const CodeEditorFileTree = ({ tree, level = 0, onItemClick, isCollapsed, selecte
return ( return (
<AccordionItem <AccordionItem
key={ index } key={ index }
value={ leaf.name }
{ ...itemProps } { ...itemProps }
pl={ `${ 26 + (level * 8) }px` } pl={ `${ 26 + (level * 8) }px` }
pr="8px" pr="8px"
...@@ -106,7 +119,7 @@ const CodeEditorFileTree = ({ tree, level = 0, onItemClick, isCollapsed, selecte ...@@ -106,7 +119,7 @@ const CodeEditorFileTree = ({ tree, level = 0, onItemClick, isCollapsed, selecte
); );
}) })
} }
</Accordion> </AccordionRoot>
); );
}; };
......
import { Box, chakra, Tooltip } from '@chakra-ui/react'; import { Box, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { Tooltip } from 'toolkit/chakra/tooltip';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
interface Props { interface Props {
...@@ -9,7 +10,7 @@ interface Props { ...@@ -9,7 +10,7 @@ interface Props {
const CodeEditorMainFileIndicator = ({ className }: Props) => { const CodeEditorMainFileIndicator = ({ className }: Props) => {
return ( return (
<Tooltip label="The main file containing verified contract"> <Tooltip content="The main file containing verified contract">
<Box className={ className } > <Box className={ className } >
<IconSvg name="star_filled" boxSize={ 3 } display="block" color="green.500"/> <IconSvg name="star_filled" boxSize={ 3 } display="block" color="green.500"/>
</Box> </Box>
......
import type { ChakraProps } from '@chakra-ui/react'; import type { HTMLChakraProps } from '@chakra-ui/react';
import { Accordion, Box, Input, InputGroup, InputRightElement, useBoolean } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import React from 'react'; import React from 'react';
import type { File, Monaco, SearchResult } from './types'; import type { File, Monaco, SearchResult } from './types';
import useDebounce from 'lib/hooks/useDebounce'; import useDebounce from 'lib/hooks/useDebounce';
import { AccordionRoot } from 'toolkit/chakra/accordion';
import { Input } from 'toolkit/chakra/input';
import { InputGroup } from 'toolkit/chakra/input-group';
import CodeEditorSearchSection from './CodeEditorSearchSection'; import CodeEditorSearchSection from './CodeEditorSearchSection';
import CoderEditorCollapseButton from './CoderEditorCollapseButton'; import CoderEditorCollapseButton from './CoderEditorCollapseButton';
...@@ -24,16 +27,28 @@ interface Props { ...@@ -24,16 +27,28 @@ interface Props {
const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive, setActionBarRenderer, defaultValue }: Props) => { const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive, setActionBarRenderer, defaultValue }: Props) => {
const [ searchTerm, changeSearchTerm ] = React.useState(''); const [ searchTerm, changeSearchTerm ] = React.useState('');
const [ searchResults, setSearchResults ] = React.useState<Array<SearchResult>>([]); const [ searchResults, setSearchResults ] = React.useState<Array<SearchResult>>([]);
const [ expandedSections, setExpandedSections ] = React.useState<Array<number>>([]); const [ expandedSections, setExpandedSections ] = React.useState<Array<string>>([]);
const [ isMatchCase, setMatchCase ] = useBoolean(); const [ isMatchCase, setMatchCase ] = React.useState(false);
const [ isMatchWholeWord, setMatchWholeWord ] = useBoolean(); const [ isMatchWholeWord, setMatchWholeWord ] = React.useState(false);
const [ isMatchRegex, setMatchRegex ] = useBoolean(); const [ isMatchRegex, setMatchRegex ] = React.useState(false);
const decorations = React.useRef<Record<string, Array<string>>>({}); const decorations = React.useRef<Record<string, Array<string>>>({});
const themeColors = useThemeColors(); const themeColors = useThemeColors();
const debouncedSearchTerm = useDebounce(searchTerm, 300); const debouncedSearchTerm = useDebounce(searchTerm, 300);
const handleMatchCaseChange = React.useCallback(() => {
setMatchCase((prev) => !prev);
}, []);
const handleMatchWholeWordChange = React.useCallback(() => {
setMatchWholeWord((prev) => !prev);
}, []);
const handleMatchRegexChange = React.useCallback(() => {
setMatchRegex((prev) => !prev);
}, []);
React.useEffect(() => { React.useEffect(() => {
changeSearchTerm(defaultValue); changeSearchTerm(defaultValue);
}, [ defaultValue ]); }, [ defaultValue ]);
...@@ -73,7 +88,7 @@ const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive, ...@@ -73,7 +88,7 @@ const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive,
}, [ debouncedSearchTerm, isMatchCase, isMatchRegex, isMatchWholeWord, monaco ]); }, [ debouncedSearchTerm, isMatchCase, isMatchRegex, isMatchWholeWord, monaco ]);
React.useEffect(() => { React.useEffect(() => {
setExpandedSections(searchResults.map((item, index) => index)); setExpandedSections(searchResults.map((item) => item.file_path));
}, [ searchResults ]); }, [ searchResults ]);
const handleSearchTermChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => { const handleSearchTermChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
...@@ -87,13 +102,13 @@ const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive, ...@@ -87,13 +102,13 @@ const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive,
} }
}, [ data, onFileSelect ]); }, [ data, onFileSelect ]);
const handleAccordionStateChange = React.useCallback((newValue: Array<number>) => { const handleAccordionStateChange = React.useCallback(({ value }: { value: Array<string> }) => {
setExpandedSections(newValue); setExpandedSections(value);
}, []); }, []);
const handleToggleCollapseClick = React.useCallback(() => { const handleToggleCollapseClick = React.useCallback(() => {
if (expandedSections.length === 0) { if (expandedSections.length === 0) {
setExpandedSections(searchResults.map((item, index) => index)); setExpandedSections(searchResults.map((item) => item.file_path));
} else { } else {
setExpandedSections([]); setExpandedSections([]);
} }
...@@ -114,13 +129,14 @@ const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive, ...@@ -114,13 +129,14 @@ const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive,
isActive && setActionBarRenderer(() => renderActionBar); isActive && setActionBarRenderer(() => renderActionBar);
}, [ isActive, renderActionBar, setActionBarRenderer ]); }, [ isActive, renderActionBar, setActionBarRenderer ]);
const buttonProps: ChakraProps = { const buttonProps: HTMLChakraProps<'div'> = {
boxSize: '20px', boxSize: '20px',
p: '1px', p: '1px',
cursor: 'pointer', cursor: 'pointer',
borderRadius: '3px', borderRadius: '3px',
borderWidth: '1px', borderWidth: '1px',
borderColor: 'transparent', borderColor: 'transparent',
color: 'global.body.fg',
}; };
const searchResultNum = (() => { const searchResultNum = (() => {
...@@ -145,8 +161,40 @@ const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive, ...@@ -145,8 +161,40 @@ const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive,
); );
})(); })();
const inputEndElement = (
<>
<Box
{ ...buttonProps }
className="codicon codicon-case-sensitive"
onClick={ handleMatchCaseChange }
bgColor={ isMatchCase ? themeColors['custom.inputOption.activeBackground'] : 'transparent' }
_hover={{ bgColor: isMatchCase ? themeColors['custom.inputOption.activeBackground'] : themeColors['custom.inputOption.hoverBackground'] }}
title="Match Case"
aria-label="Match Case"
/>
<Box
{ ...buttonProps }
className="codicon codicon-whole-word"
bgColor={ isMatchWholeWord ? themeColors['custom.inputOption.activeBackground'] : 'transparent' }
onClick={ handleMatchWholeWordChange }
_hover={{ bgColor: isMatchWholeWord ? themeColors['custom.inputOption.activeBackground'] : themeColors['custom.inputOption.hoverBackground'] }}
title="Match Whole Word"
aria-label="Match Whole Word"
/>
<Box
{ ...buttonProps }
className="codicon codicon-regex"
bgColor={ isMatchRegex ? themeColors['custom.inputOption.activeBackground'] : 'transparent' }
onClick={ handleMatchRegexChange }
_hover={{ bgColor: isMatchRegex ? themeColors['custom.inputOption.activeBackground'] : themeColors['custom.inputOption.hoverBackground'] }}
title="Use Regular Expression"
aria-label="Use Regular Expression"
/>
</>
);
return ( return (
<Box> <>
<InputGroup <InputGroup
px="8px" px="8px"
position="sticky" position="sticky"
...@@ -155,17 +203,20 @@ const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive, ...@@ -155,17 +203,20 @@ const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive,
zIndex="2" zIndex="2"
bgColor={ themeColors['sideBar.background'] } bgColor={ themeColors['sideBar.background'] }
pb="8px" pb="8px"
boxShadow={ isInputStuck ? 'md' : 'none' } boxShadow={ isInputStuck ? '0px 6px 3px 0px rgba(0, 0, 0, 0.05)' : 'none' }
endElement={ inputEndElement }
endElementProps={{ height: '26px', pl: '0', pr: '10px', columnGap: '2px' }}
endOffset="75px"
> >
<Input <Input
size="xs" size="xs"
onChange={ handleSearchTermChange } onChange={ handleSearchTermChange }
value={ searchTerm } value={ searchTerm }
placeholder="Search" placeholder="Search"
variant="unstyled"
color={ themeColors['input.foreground'] } color={ themeColors['input.foreground'] }
bgColor={ themeColors['input.background'] } bgColor={ themeColors['input.background'] }
borderRadius="none" borderRadius="none"
height="26px"
fontSize="13px" fontSize="13px"
lineHeight="20px" lineHeight="20px"
borderWidth="1px" borderWidth="1px"
...@@ -178,47 +229,19 @@ const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive, ...@@ -178,47 +229,19 @@ const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive,
borderColor: themeColors.focusBorder, borderColor: themeColors.focusBorder,
}} }}
/> />
<InputRightElement w="auto" h="auto" right="12px" top="3px" columnGap="2px">
<Box
{ ...buttonProps }
className="codicon codicon-case-sensitive"
onClick={ setMatchCase.toggle }
bgColor={ isMatchCase ? themeColors['custom.inputOption.activeBackground'] : 'transparent' }
_hover={{ bgColor: isMatchCase ? themeColors['custom.inputOption.activeBackground'] : themeColors['custom.inputOption.hoverBackground'] }}
title="Match Case"
aria-label="Match Case"
/>
<Box
{ ...buttonProps }
className="codicon codicon-whole-word"
bgColor={ isMatchWholeWord ? themeColors['custom.inputOption.activeBackground'] : 'transparent' }
onClick={ setMatchWholeWord.toggle }
_hover={{ bgColor: isMatchWholeWord ? themeColors['custom.inputOption.activeBackground'] : themeColors['custom.inputOption.hoverBackground'] }}
title="Match Whole Word"
aria-label="Match Whole Word"
/>
<Box
{ ...buttonProps }
className="codicon codicon-regex"
bgColor={ isMatchRegex ? themeColors['custom.inputOption.activeBackground'] : 'transparent' }
onClick={ setMatchRegex.toggle }
_hover={{ bgColor: isMatchRegex ? themeColors['custom.inputOption.activeBackground'] : themeColors['custom.inputOption.hoverBackground'] }}
title="Use Regular Expression"
aria-label="Use Regular Expression"
/>
</InputRightElement>
</InputGroup> </InputGroup>
{ searchResultNum } { searchResultNum }
<Accordion <AccordionRoot
key={ debouncedSearchTerm } key={ debouncedSearchTerm }
allowMultiple multiple
index={ expandedSections } value={ expandedSections }
onChange={ handleAccordionStateChange } onValueChange={ handleAccordionStateChange }
reduceMotion noAnimation
> >
{ searchResults.map((item) => <CodeEditorSearchSection key={ item.file_path } data={ item } onItemClick={ handleResultItemClick }/>) } { searchResults.map((item) => <CodeEditorSearchSection key={ item.file_path } data={ item } onItemClick={ handleResultItemClick }/>) }
</Accordion> </AccordionRoot>
</Box> </>
); );
}; };
......
import { AccordionButton, AccordionItem, AccordionPanel, Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { SearchResult } from './types'; import type { SearchResult } from './types';
import { AccordionItem, AccordionItemContent, AccordionItemTrigger } from 'toolkit/chakra/accordion';
import CodeEditorFileIcon from './CodeEditorFileIcon'; import CodeEditorFileIcon from './CodeEditorFileIcon';
import CodeEditorSearchResultItem from './CodeEditorSearchResultItem'; import CodeEditorSearchResultItem from './CodeEditorSearchResultItem';
import getFileName from './utils/getFileName'; import getFileName from './utils/getFileName';
...@@ -26,58 +28,59 @@ const CodeEditorSearchSection = ({ data, onItemClick }: Props) => { ...@@ -26,58 +28,59 @@ const CodeEditorSearchSection = ({ data, onItemClick }: Props) => {
const themeColors = useThemeColors(); const themeColors = useThemeColors();
return ( return (
<AccordionItem borderWidth="0px" _last={{ borderBottomWidth: '0px' }} > <AccordionItem value={ data.file_path } borderWidth="0px" _last={{ borderBottomWidth: '0px' }}>
{ ({ isExpanded }) => ( <AccordionItemTrigger
<> py={ 0 }
<AccordionButton px={ 2 }
py={ 0 } _hover={{ bgColor: themeColors['custom.list.hoverBackground'] }}
px={ 2 } fontSize="13px"
_hover={{ bgColor: themeColors['custom.list.hoverBackground'] }} transitionDuration="0"
fontSize="13px" lineHeight="22px"
transitionDuration="0" alignItems="center"
lineHeight="22px" noIndicator
alignItems="center" >
> <Box
<Box className="codicon codicon-tree-item-expanded"
className="codicon codicon-tree-item-expanded" transform="rotate(-90deg)"
transform={ isExpanded ? 'rotate(0deg)' : 'rotate(-90deg)' } _groupExpanded={{
width="20px" transform: 'rotate(0deg)',
height="22px" }}
py="3px" // transform={ isExpanded ? 'rotate(0deg)' : 'rotate(-90deg)' }
flexShrink={ 0 } width="20px"
/> height="22px"
<CodeEditorFileIcon mr="4px" fileName={ fileName }/> py="3px"
<Box flexShrink={ 0 }
mr="8px" />
whiteSpace="nowrap" <CodeEditorFileIcon mr="4px" fileName={ fileName }/>
overflow="hidden" <Box
textOverflow="ellipsis" mr="8px"
textAlign="left" whiteSpace="nowrap"
> overflow="hidden"
{ fileName } textOverflow="ellipsis"
</Box> textAlign="left"
<Box >
className="monaco-count-badge" { fileName }
ml="auto" </Box>
bgColor={ themeColors['badge.background'] } <Box
flexShrink={ 0 } className="monaco-count-badge"
> ml="auto"
{ data.matches.length } bgColor={ themeColors['badge.background'] }
</Box> flexShrink={ 0 }
</AccordionButton> >
<AccordionPanel p={ 0 }> { data.matches.length }
{ data.matches.map((match) => ( </Box>
<CodeEditorSearchResultItem </AccordionItemTrigger>
key={ data.file_path + '_' + match.startLineNumber + '_' + match.startColumn } <AccordionItemContent p={ 0 }>
filePath={ data.file_path } { data.matches.map((match) => (
onClick={ handleFileLineClick } <CodeEditorSearchResultItem
{ ...match } key={ data.file_path + '_' + match.startLineNumber + '_' + match.startColumn }
/> filePath={ data.file_path }
), onClick={ handleFileLineClick }
) } { ...match }
</AccordionPanel> />
</> ),
) } ) }
</AccordionItemContent>
</AccordionItem> </AccordionItem>
); );
}; };
......
import type { HTMLChakraProps } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { Box, Tab, TabList, TabPanel, TabPanels, Tabs, useBoolean } from '@chakra-ui/react';
import { throttle } from 'es-toolkit'; import { throttle } from 'es-toolkit';
import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import React from 'react'; import React from 'react';
...@@ -7,6 +6,8 @@ import React from 'react'; ...@@ -7,6 +6,8 @@ import React from 'react';
import type { File, Monaco } from './types'; import type { File, Monaco } from './types';
import { shift, cmd } from 'lib/html-entities'; import { shift, cmd } from 'lib/html-entities';
import type { TabsTriggerProps } from 'toolkit/chakra/tabs';
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from 'toolkit/chakra/tabs';
import CodeEditorFileExplorer from './CodeEditorFileExplorer'; import CodeEditorFileExplorer from './CodeEditorFileExplorer';
import CodeEditorSearch from './CodeEditorSearch'; import CodeEditorSearch from './CodeEditorSearch';
...@@ -26,14 +27,14 @@ export const CONTAINER_WIDTH = 250; ...@@ -26,14 +27,14 @@ export const CONTAINER_WIDTH = 250;
const CodeEditorSideBar = ({ onFileSelect, data, monaco, editor, selectedFile, mainFile }: Props) => { const CodeEditorSideBar = ({ onFileSelect, data, monaco, editor, selectedFile, mainFile }: Props) => {
const [ isStuck, setIsStuck ] = React.useState(false); const [ isStuck, setIsStuck ] = React.useState(false);
const [ isDrawerOpen, setIsDrawerOpen ] = useBoolean(false); const [ isDrawerOpen, setIsDrawerOpen ] = React.useState(false);
const [ tabIndex, setTabIndex ] = React.useState(0); const [ activeTab, setActiveTab ] = React.useState('explorer');
const [ searchValue, setSearchValue ] = React.useState(''); const [ searchValue, setSearchValue ] = React.useState('');
const [ actionBarRenderer, setActionBarRenderer ] = React.useState<() => React.JSX.Element>(); const [ actionBarRenderer, setActionBarRenderer ] = React.useState<() => React.JSX.Element>();
const themeColors = useThemeColors(); const themeColors = useThemeColors();
const tabProps: HTMLChakraProps<'button'> = { const tabProps: Partial<TabsTriggerProps> = {
fontFamily: 'heading', fontFamily: 'heading',
textTransform: 'uppercase', textTransform: 'uppercase',
fontSize: '11px', fontSize: '11px',
...@@ -52,10 +53,18 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, editor, selectedFile, m ...@@ -52,10 +53,18 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, editor, selectedFile, m
}, 100)); }, 100));
const handleFileSelect = React.useCallback((index: number, lineNumber?: number) => { const handleFileSelect = React.useCallback((index: number, lineNumber?: number) => {
isDrawerOpen && setIsDrawerOpen.off(); isDrawerOpen && setIsDrawerOpen(false);
onFileSelect(index, lineNumber); onFileSelect(index, lineNumber);
}, [ isDrawerOpen, onFileSelect, setIsDrawerOpen ]); }, [ isDrawerOpen, onFileSelect, setIsDrawerOpen ]);
const handleSideBarButtonClick = React.useCallback(() => {
setIsDrawerOpen((prev) => !prev);
}, []);
const handleTabChange = React.useCallback(({ value }: { value: string }) => {
setActiveTab(value);
}, []);
React.useEffect(() => { React.useEffect(() => {
if (editor && monaco) { if (editor && monaco) {
editor.addAction({ editor.addAction({
...@@ -67,7 +76,7 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, editor, selectedFile, m ...@@ -67,7 +76,7 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, editor, selectedFile, m
contextMenuGroupId: 'navigation', contextMenuGroupId: 'navigation',
contextMenuOrder: 1.5, contextMenuOrder: 1.5,
run: function() { run: function() {
setTabIndex(0); setActiveTab('explorer');
}, },
}); });
...@@ -80,7 +89,7 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, editor, selectedFile, m ...@@ -80,7 +89,7 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, editor, selectedFile, m
contextMenuGroupId: 'navigation', contextMenuGroupId: 'navigation',
contextMenuOrder: 1.6, contextMenuOrder: 1.6,
run: function(editor) { run: function(editor) {
setTabIndex(1); setActiveTab('search');
const selection = editor.getSelection(); const selection = editor.getSelection();
const selectedValue = selection ? editor.getModel()?.getValueInRange(selection) : ''; const selectedValue = selection ? editor.getModel()?.getValueInRange(selection) : '';
setSearchValue(selectedValue || ''); setSearchValue(selectedValue || '');
...@@ -111,8 +120,8 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, editor, selectedFile, m ...@@ -111,8 +120,8 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, editor, selectedFile, m
borderTopRightRadius="md" borderTopRightRadius="md"
borderBottomRightRadius="md" borderBottomRightRadius="md"
> >
<Tabs isLazy lazyBehavior="keepMounted" variant="unstyled" size="13px" index={ tabIndex } onChange={ setTabIndex }> <TabsRoot unmountOnExit={ false } variant="unstyled" size="free" value={ activeTab } onValueChange={ handleTabChange }>
<TabList <TabsList
columnGap={ 3 } columnGap={ 3 }
position="sticky" position="sticky"
top={ 0 } top={ 0 }
...@@ -120,37 +129,37 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, editor, selectedFile, m ...@@ -120,37 +129,37 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, editor, selectedFile, m
bgColor={ themeColors['sideBar.background'] } bgColor={ themeColors['sideBar.background'] }
zIndex="1" zIndex="1"
px={ 2 } px={ 2 }
h="35px"
alignItems="center"
boxShadow={ isStuck ? 'md' : 'none' } boxShadow={ isStuck ? 'md' : 'none' }
borderTopRightRadius="md" borderTopRightRadius="md"
> >
<Tab { ...tabProps } title={ `File explorer (${ shift + cmd }E)` }>Explorer</Tab> <TabsTrigger value="explorer" { ...tabProps } title={ `File explorer (${ shift + cmd }E)` }>Explorer</TabsTrigger>
<Tab { ...tabProps } title={ `Search in files (${ shift + cmd }F)` }>Search</Tab> <TabsTrigger value="search" { ...tabProps } title={ `Search in files (${ shift + cmd }F)` }>Search</TabsTrigger>
{ actionBarRenderer?.() } { actionBarRenderer?.() }
</TabList> </TabsList>
<TabPanels> <TabsContent value="explorer" p={ 0 }>
<TabPanel p={ 0 }> <CodeEditorFileExplorer
<CodeEditorFileExplorer data={ data }
data={ data } onFileSelect={ handleFileSelect }
onFileSelect={ handleFileSelect } selectedFile={ selectedFile }
selectedFile={ selectedFile } mainFile={ mainFile }
mainFile={ mainFile } isActive={ activeTab === 'explorer' }
isActive={ tabIndex === 0 } setActionBarRenderer={ setActionBarRenderer }
setActionBarRenderer={ setActionBarRenderer } />
/> </TabsContent>
</TabPanel> <TabsContent value="search" p={ 0 }>
<TabPanel p={ 0 }> <CodeEditorSearch
<CodeEditorSearch data={ data }
data={ data } onFileSelect={ handleFileSelect }
onFileSelect={ handleFileSelect } monaco={ monaco }
monaco={ monaco } isInputStuck={ isStuck }
isInputStuck={ isStuck } isActive={ activeTab === 'search' }
isActive={ tabIndex === 1 } setActionBarRenderer={ setActionBarRenderer }
setActionBarRenderer={ setActionBarRenderer } defaultValue={ searchValue }
defaultValue={ searchValue } />
/> </TabsContent>
</TabPanel> </TabsRoot>
</TabPanels>
</Tabs>
</Box> </Box>
<Box <Box
boxSize="24px" boxSize="24px"
...@@ -163,7 +172,7 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, editor, selectedFile, m ...@@ -163,7 +172,7 @@ const CodeEditorSideBar = ({ onFileSelect, data, monaco, editor, selectedFile, m
borderTopLeftRadius="4px" borderTopLeftRadius="4px"
borderBottomLeftRadius="4px" borderBottomLeftRadius="4px"
boxShadow="md" boxShadow="md"
onClick={ setIsDrawerOpen.toggle } onClick={ handleSideBarButtonClick }
zIndex="1" zIndex="1"
transitionProperty="right" transitionProperty="right"
transitionDuration="normal" transitionDuration="normal"
......
...@@ -48,7 +48,7 @@ const CodeEditorTab = ({ isActive, isMainFile, path, onClick, onClose, isCloseDi ...@@ -48,7 +48,7 @@ const CodeEditorTab = ({ isActive, isMainFile, path, onClick, onClose, isCloseDi
cursor="pointer" cursor="pointer"
onClick={ handleClick } onClick={ handleClick }
_hover={{ _hover={{
'.codicon-close': { '& .codicon-close': {
visibility: 'visible', visibility: 'visible',
}, },
}} }}
......
export const light = { import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
export const light: monaco.editor.IStandaloneThemeData = {
base: 'vs' as const, base: 'vs' as const,
inherit: true, inherit: true,
rules: [ rules: [
...@@ -44,7 +46,7 @@ export const light = { ...@@ -44,7 +46,7 @@ export const light = {
} as const, } as const,
}; };
export const dark = { export const dark: monaco.editor.IStandaloneThemeData = {
base: 'vs-dark' as const, base: 'vs-dark' as const,
inherit: true, inherit: true,
rules: [ rules: [
......
import { useColorModeValue } from '@chakra-ui/react'; import { useColorModeValue } from 'toolkit/chakra/color-mode';
import * as themes from './themes'; import * as themes from './themes';
......
...@@ -41,6 +41,18 @@ const TabsShowcase = () => { ...@@ -41,6 +41,18 @@ const TabsShowcase = () => {
<TabsContent value="tab2">Second tab content</TabsContent> <TabsContent value="tab2">Second tab content</TabsContent>
</TabsRoot> </TabsRoot>
</Sample> </Sample>
<Sample label="variant: segmented">
<TabsRoot defaultValue="tab1" variant="segmented" size="sm">
<TabsList>
<TabsTrigger value="tab1">First tab</TabsTrigger>
<TabsTrigger value="tab2">Second tab</TabsTrigger>
<TabsTrigger value="tab3">Third tab</TabsTrigger>
</TabsList>
<TabsContent value="tab1">First tab content</TabsContent>
<TabsContent value="tab2">Second tab content</TabsContent>
<TabsContent value="tab3">Third tab content</TabsContent>
</TabsRoot>
</Sample>
</SamplesStack> </SamplesStack>
</Section> </Section>
......
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