Commit 27872bfe authored by tom's avatar tom

contract details tab

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