Commit 5e808067 authored by tom goriunov's avatar tom goriunov Committed by GitHub

UI toolkit package (#2691)

* first version of the pack

* single entry point

* add custom components to the package

* move form components to toolkit

* add more utils and hooks to the package

* minor fixes

* add CI workflow

* update readme and package.json

* fix ts

* fix build of env-validator

* fix docker build

* fix icons size

* test fixes

* more test fixes
parent 69e5efd3
......@@ -51,6 +51,65 @@ jobs:
- name: Compile TypeScript
run: yarn lint:tsc
toolkit_build_check:
name: Toolkit build check
needs: [ code_quality ]
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 22.11.0
cache: 'yarn'
- name: Cache node_modules
uses: actions/cache@v4
id: cache-node-modules
with:
path: |
node_modules
key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Install project dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile
- name: Install package dependencies
run: |
cd ./toolkit/package
yarn --frozen-lockfile
- name: Type check the package
run: |
cd ./toolkit/package
yarn typecheck
- name: Build the package
run: |
cd ./toolkit/package
yarn build
- name: Verify build output
run: |
cd ./toolkit/package
if [ ! -d "dist" ]; then
echo "Build failed: dist directory not found"
exit 1
fi
if [ ! -f "dist/index.js" ]; then
echo "Build failed: dist/index.js not found"
exit 1
fi
if [ ! -f "dist/index.d.ts" ]; then
echo "Build failed: dist/index.d.ts not found"
exit 1
fi
envs_validation:
name: ENV variables validation
runs-on: ubuntu-latest
......
......@@ -90,6 +90,13 @@ jobs:
needs: publish_image
secrets: inherit
publish_toolkit:
name: Publish toolkit package to NPM
uses: './.github/workflows/toolkit-npm-publisher.yml'
secrets: inherit
with:
version: ${{ github.ref_name }}
# Temporary disable this step because it is broken
# There is an issue with building web3modal deps
upload_source_maps:
......
name: Publish Chakra theme package to NPM
name: Publish toolkit package to NPM
on:
workflow_dispatch:
......@@ -34,19 +34,19 @@ jobs:
- name: Update package version
run: |
cd ./theme
cd ./toolkit/package
npm version ${{ inputs.version }}
- name: Build the package
run: |
yarn
cd ./theme
cd ./toolkit/package
yarn
yarn build
- name: Publish to NPM registry
run: |
cd ./theme
cd ./toolkit/package
npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
......@@ -31,6 +31,7 @@
*.pem
.tools
grafana
.cursor
# debug
npm-debug.log*
......@@ -60,3 +61,4 @@ yarn-error.log*
# build outputs
/tools/preset-sync/index.js
/toolkit/package/dist
\ No newline at end of file
{
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"javascript.preferences.autoImportFileExcludePatterns": [
"./toolkit/package/**",
"./toolkit/components/**/index.ts",
],
"typescript.preferences.autoImportFileExcludePatterns": [
"./toolkit/package/**",
"./toolkit/components/**/index.ts",
]
}
\ No newline at end of file
......@@ -14,7 +14,8 @@ COPY types ./types
COPY lib ./lib
COPY configs/app ./configs/app
COPY toolkit/theme ./toolkit/theme
COPY ui/shared/forms/validators/url.ts ./ui/shared/forms/validators/url.ts
COPY toolkit/utils ./toolkit/utils
COPY toolkit/components/forms/validators/url.ts ./toolkit/components/forms/validators/url.ts
RUN apk add git
RUN yarn --frozen-lockfile --network-timeout 100000
......@@ -79,6 +80,7 @@ RUN set -a && \
# ENV NEXT_TELEMETRY_DISABLED 1
# Build app for production
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN yarn build
......
import stripTrailingSlash from 'lib/stripTrailingSlash';
import { stripTrailingSlash } from 'toolkit/utils/url';
import { getEnvValue } from './utils';
......
import type { RollupType } from 'types/client/rollup';
import type { NetworkVerificationType, NetworkVerificationTypeEnvs } from 'types/networks';
import { urlValidator } from 'ui/shared/forms/validators/url';
import { urlValidator } from 'toolkit/components/forms/validators/url';
import { getEnvValue, parseEnvJson } from './utils';
......
......@@ -2,7 +2,7 @@ import type { Feature } from './types';
import type { ParentChain, RollupType } from 'types/client/rollup';
import { ROLLUP_TYPES } from 'types/client/rollup';
import stripTrailingSlash from 'lib/stripTrailingSlash';
import { stripTrailingSlash } from 'toolkit/utils/url';
import { getEnvValue, parseEnvJson } from '../utils';
......
import type { Feature } from './types';
import stripTrailingSlash from 'lib/stripTrailingSlash';
import { stripTrailingSlash } from 'toolkit/utils/url';
import { getEnvValue } from '../utils';
......
import type { Feature } from './types';
import stripTrailingSlash from 'lib/stripTrailingSlash';
import { stripTrailingSlash } from 'toolkit/utils/url';
import { getEnvValue } from '../utils';
......
......@@ -5,7 +5,7 @@ import type { NetworkExplorer } from 'types/networks';
import type { ColorThemeId } from 'types/settings';
import type { FontFamily } from 'types/ui';
import { COLOR_THEMES } from 'lib/settings/colorTheme';
import { COLOR_THEMES, type ColorTheme } from 'lib/settings/colorTheme';
import * as features from './features';
import * as views from './ui/views';
......@@ -49,7 +49,7 @@ const highlightedRoutes = (() => {
const defaultColorTheme = (() => {
const envValue = getEnvValue('NEXT_PUBLIC_COLOR_THEME_DEFAULT') as ColorThemeId | undefined;
return COLOR_THEMES.find((theme) => theme.id === envValue);
return COLOR_THEMES.find((theme) => theme.id === envValue) as ColorTheme | undefined;
})();
const UI = Object.freeze({
......
import isBrowser from 'lib/isBrowser';
import * as regexp from 'lib/regexp';
import { isBrowser } from 'toolkit/utils/isBrowser';
import * as regexp from 'toolkit/utils/regexp';
export const replaceQuotes = (value: string | undefined) => value?.replaceAll('\'', '"');
......
......@@ -46,7 +46,7 @@ import type { VerifiedContractsFilter } from '../../../types/api/contracts';
import type { TxExternalTxsConfig } from '../../../types/client/externalTxsConfig';
import { replaceQuotes } from '../../../configs/app/utils';
import * as regexp from '../../../lib/regexp';
import * as regexp from '../../../toolkit/utils/regexp';
import type { IconName } from '../../../ui/shared/IconSvg';
const protocols = [ 'http', 'https' ];
......
......@@ -29,7 +29,7 @@ const RESTRICTED_MODULES = {
name: '@chakra-ui/react',
importNames: [
'Menu', 'useToast', 'useDisclosure', 'useClipboard', 'Tooltip', 'Skeleton', 'IconButton', 'Button', 'ButtonGroup', 'Link', 'LinkBox', 'LinkOverlay',
'Dialog', 'DialogRoot', 'DialogContent', 'DialogHeader', 'DialogCloseTrigger',
'Dialog', 'DialogRoot', 'DialogContent', 'DialogHeader', 'DialogCloseTrigger', 'DialogBody',
'Tag', 'Switch', 'Image', 'Popover', 'PopoverTrigger', 'PopoverContent', 'PopoverBody', 'PopoverFooter',
'DrawerRoot', 'DrawerBody', 'DrawerContent', 'DrawerOverlay', 'DrawerBackdrop', 'DrawerTrigger', 'Drawer',
'Alert', 'AlertIcon', 'AlertTitle', 'AlertDescription',
......@@ -37,7 +37,7 @@ const RESTRICTED_MODULES = {
'Heading', 'Badge', 'Tabs', 'Show', 'Hide', 'Checkbox', 'CheckboxGroup',
'Table', 'TableRoot', 'TableBody', 'TableHeader', 'TableRow', 'TableCell',
'Menu', 'MenuRoot', 'MenuTrigger', 'MenuContent', 'MenuItem', 'MenuTriggerItem', 'MenuRadioItemGroup', 'MenuContextTrigger',
'Rating', 'RatingGroup',
'Rating', 'RatingGroup', 'Textarea',
],
message: 'Please use corresponding component or hook from "toolkit" instead',
},
......@@ -65,9 +65,7 @@ export default tseslint.config(
{ ignores: [
'deploy/tools/',
'public/',
'theme/dist/',
'.git/',
'theme/webpack.config.js',
'next.config.js',
] },
......@@ -455,6 +453,8 @@ export default tseslint.config(
{
files: [
'toolkit/chakra/**',
'toolkit/components/**',
'toolkit/package/**',
],
rules: {
// for toolkit components allow to import @chakra-ui/react directly
......
......@@ -2,7 +2,7 @@ import BigNumber from 'bignumber.js';
import type { Block } from 'types/api/block';
import { WEI, ZERO } from 'lib/consts';
import { WEI, ZERO } from 'toolkit/utils/consts';
export default function getBlockTotalReward(block: Block) {
const totalReward = block.rewards
......
......@@ -11,7 +11,6 @@ import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import { YEAR } from 'lib/consts';
import * as cookies from 'lib/cookies';
import decodeJWT from 'lib/decodeJWT';
import getErrorMessage from 'lib/errors/getErrorMessage';
......@@ -20,6 +19,7 @@ import getQueryParamString from 'lib/router/getQueryParamString';
import removeQueryParam from 'lib/router/removeQueryParam';
import useAccount from 'lib/web3/useAccount';
import { toaster } from 'toolkit/chakra/toaster';
import { YEAR } from 'toolkit/utils/consts';
import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
const feature = config.features.rewards;
......
......@@ -2,7 +2,7 @@ import { throttle, clamp } from 'es-toolkit';
import React from 'react';
const ScrollDirectionContext = React.createContext<'up' | 'down' | null>(null);
import isBrowser from 'lib/isBrowser';
import { isBrowser } from 'toolkit/utils/isBrowser';
const SCROLL_DIFF_THRESHOLD = 20;
......
import Cookies from 'js-cookie';
import isBrowser from './isBrowser';
import { isBrowser } from 'toolkit/utils/isBrowser';
export enum NAMES {
NAV_BAR_COLLAPSED = 'nav_bar_collapsed',
......
......@@ -7,7 +7,7 @@ import relativeTime from 'dayjs/plugin/relativeTime';
import updateLocale from 'dayjs/plugin/updateLocale';
import weekOfYear from 'dayjs/plugin/weekOfYear';
import { nbsp } from 'lib/html-entities';
import { nbsp } from 'toolkit/utils/htmlEntities';
const relativeTimeConfig = {
thresholds: [
......
import BigNumber from 'bignumber.js';
import { ZERO } from 'lib/consts';
import { ZERO } from 'toolkit/utils/consts';
interface Params {
value: string;
......
......@@ -2,7 +2,7 @@ import BigNumber from 'bignumber.js';
import type { Unit } from 'types/unit';
import { WEI, GWEI } from 'lib/consts';
import { WEI, GWEI } from 'toolkit/utils/consts';
export default function getValueWithUnit(value: string | number, unit: Unit = 'wei') {
let unitBn: BigNumber.Value;
......
import React from 'react';
import { SECOND } from 'lib/consts';
import { SECOND } from 'toolkit/utils/consts';
import { initGrowthBook } from './init';
......
......@@ -5,7 +5,7 @@ import type { AdBannerProviders } from 'types/client/adProviders';
import config from 'configs/app';
import { useAppContext } from 'lib/contexts/app';
import * as cookies from 'lib/cookies';
import isBrowser from 'lib/isBrowser';
import { isBrowser } from 'toolkit/utils/isBrowser';
const DEFAULT_URL = 'https://request-global.czilladx.com';
......
......@@ -4,7 +4,7 @@ import React from 'react';
import type { NavItemInternal, NavItem, NavGroupItem } from 'types/client/navigation';
import config from 'configs/app';
import { rightLineArrow } from 'lib/html-entities';
import { rightLineArrow } from 'toolkit/utils/htmlEntities';
interface ReturnType {
mainNavItems: Array<NavItem | NavGroupItem>;
......
......@@ -5,8 +5,8 @@ import type { PreSubmitTransactionResponse, PreVerifyContractResponse } from '@b
import config from 'configs/app';
import useApiFetch from 'lib/api/useApiFetch';
import useApiQuery from 'lib/api/useApiQuery';
import { MINUTE } from 'lib/consts';
import { useRewardsContext } from 'lib/contexts/rewards';
import { MINUTE } from 'toolkit/utils/consts';
import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
const feature = config.features.rewards;
......
import React from 'react';
import { DAY, HOUR, MINUTE, SECOND } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
import { DAY, HOUR, MINUTE, SECOND } from 'toolkit/utils/consts';
function getUnits(diff: number) {
if (diff < MINUTE) {
......
export default function makePrettyLink(url: string | undefined): { url: string; domain: string } | undefined {
try {
const urlObj = new URL(url ?? '');
return {
url: urlObj.href,
domain: urlObj.hostname,
};
} catch (error) {}
}
import { uniq } from 'es-toolkit';
import isBrowser from './isBrowser';
import { isBrowser } from 'toolkit/utils/isBrowser';
const RECENT_KEYWORDS_LS_KEY = 'recent_search_keywords';
const MAX_KEYWORDS_NUMBER = 10;
......
......@@ -2,7 +2,7 @@ import type { ColorThemeId } from 'types/settings';
import type { ColorMode } from 'toolkit/chakra/color-mode';
interface ColorTheme {
export interface ColorTheme {
id: ColorThemeId;
label: string;
colorMode: ColorMode;
......
const stripLeadingSlash = (str: string) => str[0] === '/' ? str.slice(1) : str;
export default stripLeadingSlash;
const stripTrailingSlash = (str: string) => str[str.length - 1] === '/' ? str.slice(0, -1) : str;
export default stripTrailingSlash;
import * as regexp from 'lib/regexp';
import * as regexp from 'toolkit/utils/regexp';
export default function urlParser(maybeUrl: string): URL | undefined {
try {
......
......@@ -2,8 +2,8 @@ import React from 'react';
import type { AddEthereumChainParameter } from 'viem';
import config from 'configs/app';
import { SECOND } from 'toolkit/utils/consts';
import { SECOND } from '../consts';
import useRewardsActivity from '../hooks/useRewardsActivity';
import useProvider from './useProvider';
import { getHexadecimalChainId } from './utils';
......
......@@ -3,7 +3,7 @@ import type { RpcBlock } from 'viem';
import type { Block, BlocksResponse, ZilliqaBlockData } from 'types/api/block';
import { ZERO_ADDRESS } from 'lib/consts';
import { ZERO_ADDRESS } from 'toolkit/utils/consts';
import * as addressMock from '../address/address';
import * as tokenMock from '../tokens/tokenInfo';
......
......@@ -5,8 +5,8 @@ import { httpLogger } from 'nextjs/utils/logger';
import { RESOURCES } from 'lib/api/resources';
import type { ResourceName, ResourcePathParams, ResourcePayload } from 'lib/api/resources';
import { SECOND } from 'lib/consts';
import metrics from 'lib/monitoring/metrics';
import { SECOND } from 'toolkit/utils/consts';
type Params<R extends ResourceName> = (
{
......
import { Accordion } from '@chakra-ui/react';
import { Accordion, Icon } from '@chakra-ui/react';
import * as React from 'react';
import IconSvg from 'ui/shared/IconSvg';
import IndicatorIcon from 'icons/arrows/east-mini.svg';
interface AccordionItemTriggerProps extends Accordion.ItemTriggerProps {
indicatorPlacement?: 'start' | 'end';
......@@ -57,7 +57,7 @@ export const AccordionItemTrigger = React.forwardRef<
</Accordion.ItemIndicator>
) : (
<Accordion.ItemIndicator rotate={{ base: '180deg', _open: '270deg' }} display="flex">
<IconSvg name="arrows/east-mini"/>
<Icon boxSize={ 5 }><IndicatorIcon/></Icon>
</Accordion.ItemIndicator>
);
......@@ -70,7 +70,7 @@ export const AccordionItemTrigger = React.forwardRef<
);
});
interface AccordionItemContentProps extends Accordion.ItemContentProps {}
export interface AccordionItemContentProps extends Accordion.ItemContentProps {}
export const AccordionItemContent = React.forwardRef<
HTMLDivElement,
......
import type { AlertDescriptionProps } from '@chakra-ui/react';
import { Alert as ChakraAlert } from '@chakra-ui/react';
import { Alert as ChakraAlert, Icon } from '@chakra-ui/react';
import * as React from 'react';
import IconSvg from 'ui/shared/IconSvg';
import IndicatorIcon from 'icons/info_filled.svg';
import { CloseButton } from './close-button';
import { Skeleton } from './skeleton';
......@@ -37,7 +37,7 @@ export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
const [ isOpen, setIsOpen ] = React.useState(true);
const defaultIcon = <IconSvg name="info_filled" w="100%" h="100%"/>;
const defaultIcon = <Icon boxSize={ 5 }><IndicatorIcon/></Icon>;
const iconElement = (() => {
if (startElement !== undefined) {
......
......@@ -2,22 +2,19 @@ import type { BadgeProps as ChakraBadgeProps } from '@chakra-ui/react';
import { chakra, Badge as ChakraBadge } from '@chakra-ui/react';
import React from 'react';
import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
import { TruncatedTextTooltip } from '../components/truncation/TruncatedTextTooltip';
import { Skeleton } from './skeleton';
export interface BadgeProps extends Omit<ChakraBadgeProps, 'colorScheme'> {
loading?: boolean;
iconStart?: IconName;
startElement?: React.ReactNode;
endElement?: React.ReactNode;
truncated?: boolean;
}
export const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
function Badge(props, ref) {
const { loading, iconStart, children, asChild = true, truncated = false, endElement, ...rest } = props;
const { loading, startElement, children, asChild = true, truncated = false, endElement, ...rest } = props;
const child = <chakra.span overflow="hidden" textOverflow="ellipsis">{ children }</chakra.span>;
......@@ -30,7 +27,7 @@ export const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
return (
<Skeleton loading={ loading } asChild={ asChild } ref={ ref }>
<ChakraBadge display="inline-flex" alignItems="center" whiteSpace="nowrap" { ...rest }>
{ iconStart && <IconSvg name={ iconStart } boxSize="10px"/> }
{ startElement }
{ childrenElement }
{ endElement }
</ChakraBadge>
......
import type { ButtonProps } from '@chakra-ui/react';
import { useRecipe } from '@chakra-ui/react';
import { Icon, useRecipe } from '@chakra-ui/react';
import * as React from 'react';
import IconSvg from 'ui/shared/IconSvg';
import CloseIcon from 'icons/close.svg';
import { recipe as closeButtonRecipe } from '../theme/recipes/close-button.recipe';
import { IconButton } from './icon-button';
......@@ -21,7 +21,7 @@ export const CloseButton = React.forwardRef<
return (
<IconButton aria-label="Close" ref={ ref } css={ styles } { ...restProps }>
{ props.children ?? <IconSvg name="close"/> }
{ props.children ?? <Icon boxSize={ 5 }><CloseIcon/></Icon> }
</IconButton>
);
});
......@@ -2,8 +2,7 @@ import { Flex, type FlexProps } from '@chakra-ui/react';
import React from 'react';
import { scroller, Element } from 'react-scroll';
import useUpdateEffect from 'lib/hooks/useUpdateEffect';
import { useUpdateEffect } from '../hooks/useUpdateEffect';
import type { LinkProps } from './link';
import { Link } from './link';
......
import { Dialog as ChakraDialog, Portal } from '@chakra-ui/react';
import * as React from 'react';
import ButtonBackTo from 'ui/shared/buttons/ButtonBackTo';
import { BackToButton } from '../components/buttons/BackToButton';
import { CloseButton } from './close-button';
interface DialogContentProps extends ChakraDialog.ContentProps {
......@@ -62,7 +61,7 @@ export const DialogHeader = React.forwardRef<
>(function DialogHeader(props, ref) {
const { startElement: startElementProp, onBackToClick, ...rest } = props;
const startElement = startElementProp ?? (onBackToClick && <ButtonBackTo onClick={ onBackToClick }/>);
const startElement = startElementProp ?? (onBackToClick && <BackToButton onClick={ onBackToClick }/>);
return (
<ChakraDialog.Header ref={ ref } { ...rest }>
......
import { Field as ChakraField } from '@chakra-ui/react';
import * as React from 'react';
import { space } from 'lib/html-entities';
import { space } from 'toolkit/utils/htmlEntities';
import getComponentDisplayName from '../utils/getComponentDisplayName';
import type { InputProps } from './input';
......
import React from 'react';
import getComponentDisplayName from '../utils/getComponentDisplayName';
import { Button, type ButtonProps } from './button';
export interface IconButtonProps extends Omit<ButtonProps, 'size'> {
......@@ -14,9 +13,7 @@ export const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
// FIXME: I have to clone the children instead of using _icon props because of style overrides
// in some pw tests for some reason the _icon style will be applied before the style of child (IconSvg component)
const child = React.Children.only<React.ReactElement>(children as React.ReactElement);
const clonedChildren = size && getComponentDisplayName(child.type) === 'IconSvg' ?
React.cloneElement(child, { boxSize: 5 }) :
child;
const clonedChildren = size ? React.cloneElement(child, { boxSize: 5 }) : child;
const sizeStyle = (() => {
switch (size) {
......
import type { LinkProps as ChakraLinkProps } from '@chakra-ui/react';
import { Link as ChakraLink, LinkBox as ChakraLinkBox, LinkOverlay as ChakraLinkOverlay } from '@chakra-ui/react';
import { Link as ChakraLink, LinkBox as ChakraLinkBox, LinkOverlay as ChakraLinkOverlay, Icon } from '@chakra-ui/react';
import NextLink from 'next/link';
import type { LinkProps as NextLinkProps } from 'next/link';
import React from 'react';
import IconSvg from 'ui/shared/IconSvg';
import ArrowIcon from 'icons/link_external.svg';
import { Skeleton } from './skeleton';
export const LinkExternalIcon = ({ color }: { color?: ChakraLinkProps['color'] }) => (
<IconSvg
name="link_external"
<Icon
boxSize={ 3 }
verticalAlign="middle"
color={ color ?? 'icon.externalLink' }
......@@ -18,7 +17,9 @@ export const LinkExternalIcon = ({ color }: { color?: ChakraLinkProps['color'] }
color: 'inherit',
}}
flexShrink={ 0 }
/>
>
<ArrowIcon/>
</Icon>
);
interface LinkPropsChakra extends ChakraLinkProps {
......
......@@ -3,7 +3,7 @@ import * as React from 'react';
import { CloseButton } from './close-button';
interface PopoverContentProps extends ChakraPopover.ContentProps {
export interface PopoverContentProps extends ChakraPopover.ContentProps {
portalled?: boolean;
portalRef?: React.RefObject<HTMLElement>;
}
......
......@@ -3,8 +3,7 @@
import { ChakraProvider } from '@chakra-ui/react';
import React from 'react';
import theme from 'toolkit/theme/theme';
import theme from '../theme/theme';
import {
ColorModeProvider,
type ColorModeProviderProps,
......
import { RatingGroup, useRatingGroup } from '@chakra-ui/react';
import { Icon, RatingGroup, useRatingGroup } from '@chakra-ui/react';
import * as React from 'react';
import IconSvg from 'ui/shared/IconSvg';
import StarFilledIcon from 'icons/star_filled.svg';
import StarOutlineIcon from 'icons/star_outline.svg';
export interface RatingProps extends Omit<RatingGroup.RootProviderProps, 'value'> {
count?: number;
......@@ -24,7 +25,9 @@ export const Rating = React.forwardRef<HTMLDivElement, RatingProps>(
<RatingGroup.HiddenInput/>
<RatingGroup.Control>
{ Array.from({ length: count }).map((_, index) => {
const icon = index < highlightedIndex ? <IconSvg name="star_filled"/> : <IconSvg name="star_outline"/>;
const icon = index < highlightedIndex ?
<Icon boxSize={ 5 }><StarFilledIcon/></Icon> :
<Icon boxSize={ 5 }><StarOutlineIcon/></Icon>;
return (
<RatingGroup.Item key={ index } index={ index + 1 }>
......
'use client';
import type { ListCollection } from '@chakra-ui/react';
import { Box, Select as ChakraSelect, createListCollection, Flex, Portal, useSelectContext } from '@chakra-ui/react';
import { Box, Select as ChakraSelect, createListCollection, Flex, Portal, Icon, useSelectContext } from '@chakra-ui/react';
import { useDebounce } from '@uidotdev/usehooks';
import * as React from 'react';
import FilterInput from 'ui/shared/filters/FilterInput';
import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg';
import ArrowIcon from 'icons/arrows/east-mini.svg';
import CheckIcon from 'icons/check.svg';
import { FilterInput } from '../components/filters/FilterInput';
import { CloseButton } from './close-button';
import { Skeleton } from './skeleton';
export interface SelectOption<Value extends string = string> {
value: Value;
label: string;
icon?: IconName | React.ReactNode;
icon?: React.ReactNode;
}
export interface SelectControlProps extends ChakraSelect.ControlProps {
......@@ -46,7 +46,7 @@ export const SelectControl = React.forwardRef<
_open={{ transform: 'rotate(90deg)' }}
flexShrink={ 0 }
>
<IconSvg name="arrows/east-mini"/>
<Icon boxSize={ 5 }><ArrowIcon/></Icon>
</ChakraSelect.Indicator>
) }
</ChakraSelect.IndicatorGroup>
......@@ -88,26 +88,24 @@ export const SelectContent = React.forwardRef<
);
});
export interface SelectItemProps extends ChakraSelect.ItemProps {
item: SelectOption;
}
export const SelectItem = React.forwardRef<
HTMLDivElement,
ChakraSelect.ItemProps
SelectItemProps
>(function SelectItem(props, ref) {
const { item, children, ...rest } = props;
const startElement = (() => {
if (item.icon) {
return typeof item.icon === 'string' ? <IconSvg name={ item.icon } boxSize={ 5 } flexShrink={ 0 }/> : item.icon;
}
return null;
})();
const startElement = item.icon;
return (
<ChakraSelect.Item key={ item.value } item={ item } { ...rest } ref={ ref }>
{ startElement }
{ children }
<ChakraSelect.ItemIndicator asChild>
<IconSvg name="check" boxSize={ 5 } flexShrink={ 0 } ml="auto"/>
<Icon boxSize={ 5 } flexShrink={ 0 } ml="auto"><CheckIcon/></Icon>
</ChakraSelect.ItemIndicator>
</ChakraSelect.Item>
);
......@@ -142,14 +140,6 @@ export const SelectValueText = React.forwardRef<
if (!item) return placeholder;
const icon = (() => {
if (item.icon) {
return typeof item.icon === 'string' ? <IconSvg name={ item.icon as IconName } boxSize={ 5 } flexShrink={ 0 }/> : item.icon;
}
return null;
})();
const label = size === 'lg' ? (
<Box
textStyle="xs"
......@@ -164,7 +154,7 @@ export const SelectValueText = React.forwardRef<
<>
{ label }
<Flex display="inline-flex" alignItems="center" flexWrap="nowrap" gap={ 1 }>
{ icon }
{ item.icon }
<span style={{
WebkitLineClamp: 1,
WebkitBoxOrient: 'vertical',
......
import { Table as ChakraTable } from '@chakra-ui/react';
import { Table as ChakraTable, Icon } from '@chakra-ui/react';
import { throttle } from 'es-toolkit';
import * as React from 'react';
import IconSvg from 'ui/shared/IconSvg';
import ArrowIcon from 'icons/arrows/east.svg';
import { Link } from './link';
......@@ -50,8 +50,7 @@ export const TableColumnHeaderSortable = <F extends string>(props: TableColumnHe
<TableColumnHeader { ...rest }>
<Link onClick={ disabled ? undefined : handleSortToggle } position="relative">
{ sortValue.includes(sortField) && (
<IconSvg
name="arrows/east"
<Icon
w={ 4 }
h="100%"
transform={ sortValue.toLowerCase().includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)' }
......@@ -59,7 +58,9 @@ export const TableColumnHeaderSortable = <F extends string>(props: TableColumnHe
left={ indicatorPosition === 'left' ? -5 : undefined }
right={ indicatorPosition === 'right' ? -5 : undefined }
top={ 0 }
/>
>
<ArrowIcon/>
</Icon>
) }
{ children }
</Link>
......
import { chakra, Tag as ChakraTag } from '@chakra-ui/react';
import * as React from 'react';
import { nbsp } from 'lib/html-entities';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
import { nbsp } from 'toolkit/utils/htmlEntities';
import { TruncatedTextTooltip } from '../components/truncation/TruncatedTextTooltip';
import { CloseButton } from './close-button';
import { Skeleton } from './skeleton';
......
......@@ -9,7 +9,7 @@ import {
createToaster,
} from '@chakra-ui/react';
import { SECOND } from 'lib/consts';
import { SECOND } from 'toolkit/utils/consts';
import { CloseButton } from './close-button';
......
import React from 'react';
import type { TabsProps } from 'toolkit/chakra/tabs';
import { TabsContent, TabsRoot } from 'toolkit/chakra/tabs';
import useViewportSize from 'toolkit/hooks/useViewportSize';
import type { TabsProps } from '../../chakra/tabs';
import { TabsContent, TabsRoot } from '../../chakra/tabs';
import { useViewportSize } from '../../hooks/useViewportSize';
import AdaptiveTabsList, { type BaseProps as AdaptiveTabsListProps } from './AdaptiveTabsList';
import { getTabValue } from './utils';
......
......@@ -6,10 +6,10 @@ import type { TabItemRegular } from './types';
import { useScrollDirection } from 'lib/contexts/scrollDirection';
import useIsMobile from 'lib/hooks/useIsMobile';
import useIsSticky from 'lib/hooks/useIsSticky';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { TabsCounter, TabsList, TabsTrigger } from 'toolkit/chakra/tabs';
import { useIsSticky } from '../..//hooks/useIsSticky';
import { Skeleton } from '../../chakra/skeleton';
import { TabsCounter, TabsList, TabsTrigger } from '../../chakra/tabs';
import AdaptiveTabsMenu from './AdaptiveTabsMenu';
import useAdaptiveTabs from './useAdaptiveTabs';
import useScrollToActiveTab from './useScrollToActiveTab';
......
import { Icon } from '@chakra-ui/react';
import React from 'react';
import type { TabItem } from './types';
import { PopoverBody, PopoverCloseTriggerWrapper, PopoverContent, PopoverRoot, PopoverTrigger } from 'toolkit/chakra/popover';
import { TabsCounter, TabsTrigger } from 'toolkit/chakra/tabs';
import IconSvg from 'ui/shared/IconSvg';
import DotsIcon from 'icons/dots.svg';
import { IconButton } from '../../chakra/icon-button';
import type { IconButtonProps } from '../../chakra/icon-button';
import { PopoverBody, PopoverCloseTriggerWrapper, PopoverContent, PopoverRoot, PopoverTrigger } from '../../chakra/popover';
import { TabsCounter, TabsTrigger } from '../../chakra/tabs';
import { getTabValue } from './utils';
interface Props extends IconButtonProps {
......@@ -38,7 +39,7 @@ const AdaptiveTabsMenu = ({ tabs, tabsCut, isActive, ...props }: Props, ref: Rea
px="18px"
{ ...props }
>
<IconSvg name="dots" boxSize={ 5 }/>
<Icon boxSize={ 5 }><DotsIcon/></Icon>
</IconButton>
</PopoverTrigger>
<PopoverContent>
......
export type { TabItemRegular, TabItemMenu, SubTabItem } from './types';
export type { Props } from './AdaptiveTabs';
export { default } from './AdaptiveTabs';
import type { TabItem, TabItemMenu } from './types';
import { middot } from 'lib/html-entities';
import { middot } from 'toolkit/utils/htmlEntities';
export const menuButton: TabItemMenu = {
id: 'menu',
......
import { chakra } from '@chakra-ui/react';
import { Icon } from '@chakra-ui/react';
import React from 'react';
import { IconButton } from 'toolkit/chakra/icon-button';
import type { TooltipProps } from 'toolkit/chakra/tooltip';
import { Tooltip } from 'toolkit/chakra/tooltip';
import IconSvg from 'ui/shared/IconSvg';
import InfoIcon from 'icons/info.svg';
interface Props {
import type { IconButtonProps } from '../../chakra/icon-button';
import { IconButton } from '../../chakra/icon-button';
import type { TooltipProps } from '../../chakra/tooltip';
import { Tooltip } from '../../chakra/tooltip';
interface Props extends IconButtonProps {
label: string | React.ReactNode;
className?: string;
tooltipProps?: Partial<TooltipProps>;
isLoading?: boolean;
as?: React.ElementType;
}
const Hint = ({ label, className, tooltipProps, isLoading, as }: Props) => {
export const Hint = React.memo(({ label, tooltipProps, isLoading, boxSize = 5, ...rest }: Props) => {
return (
<Tooltip
content={ label }
positioning={{ placement: 'top' }}
interactive
{ ...tooltipProps }
>
<IconButton
aria-label="hint"
boxSize={ 5 }
className={ className }
boxSize={ boxSize }
loadingSkeleton={ isLoading }
borderRadius="sm"
as={ as }
color="icon.info"
_hover={{ color: 'link.primary.hover' }}
{ ...rest }
>
<IconSvg name="info" w="100%" h="100%"/>
<Icon boxSize={ boxSize }>
<InfoIcon/>
</Icon>
</IconButton>
</Tooltip>
);
};
export default React.memo(chakra(Hint));
});
......@@ -23,7 +23,7 @@ const RoutedTabs = (props: Props) => {
return;
}
const queryForPathname = pickBy(router.query, (value, key) => router.pathname.includes(`[${ key }]`));
const queryForPathname = pickBy(router.query, (_, key) => router.pathname.includes(`[${ key }]`));
router.push(
{ pathname: router.pathname, query: { ...queryForPathname, tab: value } },
undefined,
......
......@@ -3,9 +3,8 @@ import React from 'react';
import type { TabItemRegular } from '../AdaptiveTabs/types';
import { Skeleton } from 'toolkit/chakra/skeleton';
import type { TabsProps } from 'toolkit/chakra/tabs';
import { Skeleton } from '../../chakra/skeleton';
import type { TabsProps } from '../../chakra/tabs';
import useActiveTabFromQuery from './useActiveTabFromQuery';
const SkeletonTabText = ({ size, title }: { size: TabsProps['size']; title: TabItemRegular['title'] }) => (
......
export { default as RoutedTabs } from './RoutedTabs';
export { default as RoutedTabsSkeleton } from './RoutedTabsSkeleton';
export { default as useActiveTabFromQuery } from './useActiveTabFromQuery';
......@@ -2,11 +2,11 @@ import { useRouter } from 'next/router';
import type { TabItem } from '../AdaptiveTabs/types';
import getQueryParamString from 'lib/router/getQueryParamString';
import { castToString } from '../../utils/guards';
export default function useActiveTabFromQuery(tabs: Array<TabItem>) {
const router = useRouter();
const tabFromQuery = getQueryParamString(router.query.tab);
const tabFromQuery = castToString(router.query.tab);
if (!tabFromQuery) {
return;
......
import { Icon } from '@chakra-ui/react';
import React from 'react';
import type { IconButtonProps } from 'toolkit/chakra/icon-button';
import { IconButton } from 'toolkit/chakra/icon-button';
import { Link } from 'toolkit/chakra/link';
import { Tooltip } from 'toolkit/chakra/tooltip';
import IconSvg from 'ui/shared/IconSvg';
import ArrowIcon from 'icons/arrows/east.svg';
interface Props extends IconButtonProps {
import type { IconButtonProps } from '../../chakra/icon-button';
import { IconButton } from '../../chakra/icon-button';
import { Link } from '../../chakra/link';
import { Tooltip } from '../../chakra/tooltip';
export interface BackToButtonProps extends IconButtonProps {
href?: string;
hint?: string;
}
const ButtonBackTo = ({ href, hint, ...rest }: Props) => {
export const BackToButton = ({ href, hint, boxSize = 6, ...rest }: BackToButtonProps) => {
const button = (
<IconButton { ...rest } boxSize={ 6 }>
<IconSvg
name="arrows/east"
<IconButton { ...rest } boxSize={ boxSize }>
<Icon
transform="rotate(180deg)"
color="icon.backTo"
_hover={{ color: 'link.primary.hover' }}
/>
boxSize={ boxSize }
>
<ArrowIcon/>
</Icon>
</IconButton>
);
......@@ -30,5 +34,3 @@ const ButtonBackTo = ({ href, hint, ...rest }: Props) => {
</Tooltip>
);
};
export default React.memo(ButtonBackTo);
import React from 'react';
import type { CloseButtonProps } from '../../chakra/close-button';
import { CloseButton } from '../../chakra/close-button';
export interface ClearButtonProps extends CloseButtonProps {
visible?: boolean;
}
export const ClearButton = ({ disabled, visible = true, ...rest }: ClearButtonProps) => {
return (
<CloseButton
disabled={ disabled || !visible }
aria-label="Clear"
title="Clear"
opacity={ visible ? 1 : 0 }
visibility={ visible ? 'visible' : 'hidden' }
{ ...rest }
/>
);
};
import { Icon } from '@chakra-ui/react';
import type { ChangeEvent } from 'react';
import React, { useCallback, useState } from 'react';
import type { InputProps } from 'toolkit/chakra/input';
import { Input } from 'toolkit/chakra/input';
import { InputGroup } from 'toolkit/chakra/input-group';
import type { SkeletonProps } from 'toolkit/chakra/skeleton';
import { Skeleton } from 'toolkit/chakra/skeleton';
import ClearButton from 'ui/shared/ClearButton';
import IconSvg from 'ui/shared/IconSvg';
import SearchIcon from 'icons/search.svg';
interface Props extends Omit<SkeletonProps, 'onChange' | 'loading'> {
import type { InputProps } from '../../chakra/input';
import { Input } from '../../chakra/input';
import { InputGroup } from '../../chakra/input-group';
import type { SkeletonProps } from '../../chakra/skeleton';
import { Skeleton } from '../../chakra/skeleton';
import { ClearButton } from '../buttons/ClearButton';
export interface FilterInputProps extends Omit<SkeletonProps, 'onChange' | 'loading'> {
onChange?: (searchTerm: string) => void;
onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
......@@ -22,7 +24,19 @@ interface Props extends Omit<SkeletonProps, 'onChange' | 'loading'> {
inputProps?: InputProps;
};
const FilterInput = ({ onChange, size = 'sm', placeholder, initialValue, type, name, loading = false, onFocus, onBlur, inputProps, ...rest }: Props) => {
export const FilterInput = ({
onChange,
size = 'sm',
placeholder,
initialValue,
type,
name,
loading = false,
onFocus,
onBlur,
inputProps,
...rest
}: FilterInputProps) => {
const [ filterQuery, setFilterQuery ] = useState(initialValue || '');
const inputRef = React.useRef<HTMLInputElement>(null);
......@@ -39,9 +53,8 @@ const FilterInput = ({ onChange, size = 'sm', placeholder, initialValue, type, n
inputRef?.current?.focus();
}, [ onChange ]);
const startElement = <IconSvg name="search" boxSize={ 5 }/>;
const endElement = <ClearButton onClick={ handleFilterQueryClear } isVisible={ filterQuery.length > 0 }/>;
const startElement = <Icon boxSize={ 5 }><SearchIcon/></Icon>;
const endElement = <ClearButton onClick={ handleFilterQueryClear } visible={ filterQuery.length > 0 }/>;
return (
<Skeleton
......@@ -75,5 +88,3 @@ const FilterInput = ({ onChange, size = 'sm', placeholder, initialValue, type, n
</Skeleton>
);
};
export default FilterInput;
......@@ -6,8 +6,6 @@ interface Props {
className?: string;
}
const FieldError = ({ message, className }: Props) => {
export const FormFieldError = chakra(({ message, className }: Props) => {
return <Box className={ className } color="text.error" textStyle="sm" mt={ 2 } wordBreak="break-word">{ message }</Box>;
};
export default chakra(FieldError);
});
export * from './FormFieldError';
......@@ -5,9 +5,9 @@ import type { FormFieldPropsBase } from './types';
import type { PartialBy } from 'types/utils';
import { addressValidator } from '../validators/address';
import FormFieldText from './FormFieldText';
import { FormFieldText } from './FormFieldText';
const FormFieldAddress = <FormFields extends FieldValues>(
const FormFieldAddressContent = <FormFields extends FieldValues>(
props: PartialBy<FormFieldPropsBase<FormFields>, 'placeholder'>,
) => {
const rules = React.useMemo(
......@@ -30,4 +30,4 @@ const FormFieldAddress = <FormFields extends FieldValues>(
);
};
export default React.memo(FormFieldAddress) as typeof FormFieldAddress;
export const FormFieldAddress = React.memo(FormFieldAddressContent) as typeof FormFieldAddressContent;
......@@ -3,8 +3,8 @@ import { useController, useFormContext, type FieldValues, type Path } from 'reac
import type { FormFieldPropsBase } from './types';
import type { CheckboxProps } from 'toolkit/chakra/checkbox';
import { Checkbox } from 'toolkit/chakra/checkbox';
import { Checkbox } from '../../../chakra/checkbox';
import type { CheckboxProps } from '../../../chakra/checkbox';
interface Props<
FormFields extends FieldValues,
......@@ -13,7 +13,7 @@ interface Props<
label: string;
}
const FormFieldCheckbox = <
const FormFieldCheckboxContent = <
FormFields extends FieldValues,
Name extends Path<FormFields> = Path<FormFields>,
>({
......@@ -52,4 +52,4 @@ const FormFieldCheckbox = <
);
};
export default React.memo(FormFieldCheckbox) as typeof FormFieldCheckbox;
export const FormFieldCheckbox = React.memo(FormFieldCheckboxContent) as typeof FormFieldCheckboxContent;
......@@ -6,13 +6,12 @@ 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 { InputGroup } from 'toolkit/chakra/input-group';
import { validator as colorValidator } from 'ui/shared/forms/validators/color';
import getFieldErrorText from '../utils/getFieldErrorText';
import { Field } from '../../../chakra/field';
import type { InputProps } from '../../../chakra/input';
import { Input } from '../../../chakra/input';
import { InputGroup } from '../../../chakra/input-group';
import { getFormFieldErrorText } from '../utils/getFormFieldErrorText';
import { colorValidator } from '../validators/color';
interface Props<
FormFields extends FieldValues,
......@@ -21,7 +20,7 @@ interface Props<
sampleDefaultBgColor?: BoxProps['bgColor'];
}
const FormFieldColor = <
const FormFieldColorContent = <
FormFields extends FieldValues,
Name extends Path<FormFields> = Path<FormFields>,
>({
......@@ -82,7 +81,7 @@ const FormFieldColor = <
return (
<Field
label={ placeholder }
errorText={ getFieldErrorText(fieldState.error) }
errorText={ getFormFieldErrorText(fieldState.error) }
invalid={ Boolean(fieldState.error) }
disabled={ formState.isSubmitting || disabled }
size={ size }
......@@ -106,4 +105,4 @@ const FormFieldColor = <
);
};
export default React.memo(FormFieldColor) as typeof FormFieldColor;
export const FormFieldColor = React.memo(FormFieldColorContent) as typeof FormFieldColorContent;
......@@ -5,9 +5,9 @@ import type { FormFieldPropsBase } from './types';
import type { PartialBy } from 'types/utils';
import { EMAIL_REGEXP } from '../validators/email';
import FormFieldText from './FormFieldText';
import { FormFieldText } from './FormFieldText';
const FormFieldEmail = <FormFields extends FieldValues>(
const FormFieldEmailContent = <FormFields extends FieldValues>(
props: PartialBy<FormFieldPropsBase<FormFields>, 'placeholder'>,
) => {
const rules = React.useMemo(
......@@ -27,4 +27,4 @@ const FormFieldEmail = <FormFields extends FieldValues>(
);
};
export default React.memo(FormFieldEmail) as typeof FormFieldEmail;
export const FormFieldEmail = React.memo(FormFieldEmailContent) as typeof FormFieldEmailContent;
......@@ -4,17 +4,16 @@ import { useController, useFormContext } from 'react-hook-form';
import type { FormFieldPropsBase } from './types';
import type { SelectProps } from 'toolkit/chakra/select';
import { Select } from 'toolkit/chakra/select';
import getFieldErrorText from '../utils/getFieldErrorText';
import type { SelectProps } from '../../../chakra/select';
import { Select } from '../../../chakra/select';
import { getFormFieldErrorText } from '../utils/getFormFieldErrorText';
type Props<
FormFields extends FieldValues,
Name extends Path<FormFields>,
> = FormFieldPropsBase<FormFields, Name> & SelectProps;
const FormFieldSelect = <
const FormFieldSelectContent = <
FormFields extends FieldValues,
Name extends Path<FormFields>,
>(props: Props<FormFields, Name>) => {
......@@ -47,7 +46,7 @@ const FormFieldSelect = <
onInteractOutside={ handleBlur }
disabled={ isDisabled }
invalid={ Boolean(fieldState.error) }
errorText={ getFieldErrorText(fieldState.error) }
errorText={ getFormFieldErrorText(fieldState.error) }
size={ size }
positioning={{ sameWidth: true }}
{ ...rest }
......@@ -55,4 +54,4 @@ const FormFieldSelect = <
);
};
export default React.memo(FormFieldSelect) as typeof FormFieldSelect;
export const FormFieldSelect = React.memo(FormFieldSelectContent) as typeof FormFieldSelectContent;
......@@ -4,17 +4,16 @@ import { useController, useFormContext } from 'react-hook-form';
import type { FormFieldPropsBase } from './types';
import type { SelectAsyncProps } from 'toolkit/chakra/select';
import { SelectAsync } from 'toolkit/chakra/select';
import getFieldErrorText from '../utils/getFieldErrorText';
import type { SelectAsyncProps } from '../../../chakra/select';
import { SelectAsync } from '../../../chakra/select';
import { getFormFieldErrorText } from '../utils/getFormFieldErrorText';
type Props<
FormFields extends FieldValues,
Name extends Path<FormFields>,
> = FormFieldPropsBase<FormFields, Name> & SelectAsyncProps;
const FormFieldSelectAsync = <
const FormFieldSelectAsyncContent = <
FormFields extends FieldValues,
Name extends Path<FormFields>,
>(props: Props<FormFields, Name>) => {
......@@ -47,7 +46,7 @@ const FormFieldSelectAsync = <
onInteractOutside={ handleBlur }
disabled={ isDisabled }
invalid={ Boolean(fieldState.error) }
errorText={ getFieldErrorText(fieldState.error) }
errorText={ getFormFieldErrorText(fieldState.error) }
size={ size }
positioning={{ sameWidth: true }}
{ ...rest }
......@@ -55,4 +54,4 @@ const FormFieldSelectAsync = <
);
};
export default React.memo(FormFieldSelectAsync) as typeof FormFieldSelectAsync;
export const FormFieldSelectAsync = React.memo(FormFieldSelectAsyncContent) as typeof FormFieldSelectAsyncContent;
......@@ -4,14 +4,13 @@ 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 { InputGroup } from 'toolkit/chakra/input-group';
import type { TextareaProps } from 'toolkit/chakra/textarea';
import { Textarea } from 'toolkit/chakra/textarea';
import getFieldErrorText from '../utils/getFieldErrorText';
import { Field } from '../../../chakra/field';
import type { InputProps } from '../../../chakra/input';
import { Input } from '../../../chakra/input';
import { InputGroup } from '../../../chakra/input-group';
import type { TextareaProps } from '../../../chakra/textarea';
import { Textarea } from '../../../chakra/textarea';
import { getFormFieldErrorText } from '../utils/getFormFieldErrorText';
interface Props<
FormFields extends FieldValues,
......@@ -20,7 +19,7 @@ interface Props<
asComponent?: 'Input' | 'Textarea';
}
const FormFieldText = <
const FormFieldTextContent = <
FormFields extends FieldValues,
Name extends Path<FormFields> = Path<FormFields>,
>({
......@@ -84,7 +83,7 @@ const FormFieldText = <
<Field
// for floating field label, we pass placeholder value to the label
label={ floating ? placeholder : undefined }
errorText={ getFieldErrorText(fieldState.error) }
errorText={ getFormFieldErrorText(fieldState.error) }
invalid={ Boolean(fieldState.error) }
disabled={ formState.isSubmitting || disabled }
size={ size }
......@@ -96,4 +95,4 @@ const FormFieldText = <
);
};
export default React.memo(FormFieldText) as typeof FormFieldText;
export const FormFieldText = React.memo(FormFieldTextContent) as typeof FormFieldTextContent;
......@@ -4,9 +4,9 @@ import type { FieldValues } from 'react-hook-form';
import type { FormFieldPropsBase } from './types';
import { urlValidator } from '../validators/url';
import FormFieldText from './FormFieldText';
import { FormFieldText } from './FormFieldText';
const FormFieldUrl = <FormFields extends FieldValues>(
const FormFieldUrlContent = <FormFields extends FieldValues>(
props: FormFieldPropsBase<FormFields>,
) => {
const rules = React.useMemo(
......@@ -23,4 +23,4 @@ const FormFieldUrl = <FormFields extends FieldValues>(
return <FormFieldText { ...props } rules={ rules }/>;
};
export default React.memo(FormFieldUrl) as typeof FormFieldUrl;
export const FormFieldUrl = React.memo(FormFieldUrlContent) as typeof FormFieldUrlContent;
import { chakra } from '@chakra-ui/react';
import React from 'react';
import type { ColorMode } from 'toolkit/chakra/color-mode';
import { Image } from 'toolkit/chakra/image';
import { Skeleton } from 'toolkit/chakra/skeleton';
import type { ColorMode } from '../../../../chakra/color-mode';
import { Image } from '../../../../chakra/image';
import { Skeleton } from '../../../../chakra/skeleton';
interface Props {
src: string | undefined;
......@@ -15,7 +15,7 @@ interface Props {
colorMode?: ColorMode;
}
const ImageUrlPreview = ({
export const FormFieldImagePreview = chakra(React.memo(({
src,
isInvalid,
onError,
......@@ -45,6 +45,4 @@ const ImageUrlPreview = ({
onLoad={ onLoad }
/>
);
};
export default chakra(React.memo(ImageUrlPreview));
}));
......@@ -2,7 +2,7 @@ import React from 'react';
import type { FieldValues, Path } from 'react-hook-form';
import { useFormContext, useWatch } from 'react-hook-form';
import { urlValidator } from '../validators/url';
import { urlValidator } from '../../validators/url';
interface Params<
FormFields extends FieldValues,
......@@ -31,7 +31,7 @@ interface ReturnType {
};
}
export default function useFieldWithImagePreview<
export function useImageField<
FormFields extends FieldValues,
Name extends Path<FormFields>,
>({
......
export * from './image/FormFieldImagePreview';
export * from './image/useImageField';
export * from './FormFieldAddress';
export * from './FormFieldCheckbox';
export * from './FormFieldColor';
export * from './FormFieldEmail';
export * from './FormFieldSelect';
export * from './FormFieldSelectAsync';
export * from './FormFieldText';
export * from './FormFieldUrl';
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 { InputGroupProps } from 'toolkit/chakra/input-group';
import type { TextareaProps } from 'toolkit/chakra/textarea';
import type { FieldProps } from '../../../chakra/field';
import type { InputProps } from '../../../chakra/input';
import type { InputGroupProps } from '../../../chakra/input-group';
import type { TextareaProps } from '../../../chakra/textarea';
export interface FormFieldPropsBase<
FormFields extends FieldValues,
......
......@@ -13,7 +13,7 @@ interface Props {
fullFilePath?: boolean;
}
const DragAndDropArea = ({ onDrop, children, className, isDisabled, fullFilePath, isInvalid }: Props) => {
export const DragAndDropArea = chakra(({ onDrop, children, className, isDisabled, fullFilePath, isInvalid }: Props) => {
const [ isDragOver, setIsDragOver ] = React.useState(false);
const handleDrop = React.useCallback(async(event: DragEvent<HTMLDivElement>) => {
......@@ -77,6 +77,4 @@ const DragAndDropArea = ({ onDrop, children, className, isDisabled, fullFilePath
{ children }
</Center>
);
};
export default React.memo(chakra(DragAndDropArea));
});
......@@ -3,7 +3,7 @@ import type { ChangeEvent } from 'react';
import React from 'react';
import type { ControllerRenderProps, FieldValues, Path } from 'react-hook-form';
import { Input } from 'toolkit/chakra/input';
import { Input } from '../../../../chakra/input';
interface InjectedProps {
onChange: (files: Array<File>) => void;
......@@ -16,7 +16,7 @@ interface Props<V extends FieldValues, N extends Path<V>> {
multiple?: boolean;
}
const FileInput = <Values extends FieldValues, Names extends Path<Values>>({ children, accept, multiple, field }: Props<Values, Names>) => {
const FileInputContent = <Values extends FieldValues, Names extends Path<Values>>({ children, accept, multiple, field }: Props<Values, Names>) => {
const ref = React.useRef<HTMLInputElement>(null);
React.useEffect(() => {
......@@ -64,4 +64,4 @@ const FileInput = <Values extends FieldValues, Names extends Path<Values>>({ chi
);
};
export default FileInput;
export const FileInput = React.memo(FileInputContent) as typeof FileInputContent;
import { Box, Flex, Text, chakra } from '@chakra-ui/react';
import { Box, Flex, Icon, Text, chakra } from '@chakra-ui/react';
import React from 'react';
import { CloseButton } from 'toolkit/chakra/close-button';
import Hint from 'ui/shared/Hint';
import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg';
import JsonFileIcon from 'icons/files/json.svg';
import PlaceholderFileIcon from 'icons/files/placeholder.svg';
import SolFileIcon from 'icons/files/sol.svg';
import YulFileIcon from 'icons/files/yul.svg';
const FILE_ICONS: Record<string, IconName> = {
'.json': 'files/json',
'.sol': 'files/sol',
'.yul': 'files/yul',
import { CloseButton } from '../../../../chakra/close-button';
import { Hint } from '../../../../components/Hint/Hint';
const FILE_ICONS: Record<string, React.ReactNode> = {
'.json': <JsonFileIcon/>,
'.sol': <SolFileIcon/>,
'.yul': <YulFileIcon/>,
};
function getFileExtension(fileName: string) {
......@@ -30,14 +33,14 @@ interface Props {
error?: string;
}
const FileSnippet = ({ file, className, index, onRemove, isDisabled, error }: Props) => {
export const FileSnippet = chakra(({ file, className, index, onRemove, isDisabled, error }: Props) => {
const handleRemove = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation();
onRemove?.(index);
}, [ index, onRemove ]);
const fileExtension = getFileExtension(file.name);
const fileIcon = FILE_ICONS[fileExtension] || 'files/placeholder';
const fileIcon = FILE_ICONS[fileExtension] || <PlaceholderFileIcon/>;
return (
<Flex
......@@ -48,11 +51,9 @@ const FileSnippet = ({ file, className, index, onRemove, isDisabled, error }: Pr
textAlign="left"
columnGap={ 2 }
>
<IconSvg
name={ fileIcon }
boxSize="48px"
color={ error ? 'text.error' : 'initial' }
/>
<Icon boxSize="48px" color={ error ? 'text.error' : 'initial' }>
{ fileIcon }
</Icon>
<Box maxW="calc(100% - 58px - 24px)">
<Flex alignItems="center">
<Text
......@@ -78,6 +79,4 @@ const FileSnippet = ({ file, className, index, onRemove, isDisabled, error }: Pr
</Box>
</Flex>
);
};
export default React.memo(chakra(FileSnippet));
});
import stripLeadingSlash from 'lib/stripLeadingSlash';
import { stripLeadingSlash } from '../../../../utils/url';
// Function to get all files in drop directory
export async function getAllFileEntries(dataTransferItemList: DataTransferItemList): Promise<Array<FileSystemFileEntry>> {
......
export * from './file/DragAndDropArea';
export * from './file/FileInput';
export * from './file/FileSnippet';
import type { FieldError } from 'react-hook-form';
export default function getFieldErrorText(error: FieldError | undefined) {
export function getFormFieldErrorText(error: FieldError | undefined) {
if (!error?.message && error?.type === 'pattern') {
return 'Invalid format';
}
......
export * from './getFormFieldErrorText';
export const COLOR_HEX_REGEXP = /^#[a-f\d]{3,6}$/i;
export const validator = (value: unknown) => {
export const colorValidator = (value: unknown) => {
if (typeof value !== 'string') {
return true;
}
......
export const EMAIL_REGEXP = /^[\w.%+-]+@[a-z\d-]+(?:\.[a-z\d-]+)+$/i;
export const validator = (value: string) => EMAIL_REGEXP.test(value) ? true : 'Invalid email';
export const emailValidator = (value: string) => EMAIL_REGEXP.test(value) ? true : 'Invalid email';
export * from './address';
export * from './color';
export * from './email';
export * from './signature';
export * from './text';
export * from './transaction';
export * from './url';
......@@ -3,17 +3,17 @@ import { debounce } from 'es-toolkit';
import React from 'react';
import useFontFaceObserver from 'use-font-face-observer';
import { Tooltip } from 'toolkit/chakra/tooltip';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import { BODY_TYPEFACE } from 'toolkit/theme/foundations/typography';
import { Tooltip } from '../../chakra/tooltip';
import { useDisclosure } from '../../hooks/useDisclosure';
import { BODY_TYPEFACE } from '../../theme/foundations/typography';
interface Props {
export interface TruncatedTextTooltipProps {
children: React.ReactNode;
label: React.ReactNode;
placement?: Placement;
}
const TruncatedTextTooltip = ({ children, label, placement }: Props) => {
export const TruncatedTextTooltip = React.memo(({ children, label, placement }: TruncatedTextTooltipProps) => {
const childRef = React.useRef<HTMLElement>(null);
const [ isTruncated, setTruncated ] = React.useState(false);
const { open, onToggle, onOpen, onClose } = useDisclosure();
......@@ -90,6 +90,4 @@ const TruncatedTextTooltip = ({ children, label, placement }: Props) => {
}
return modifiedChildren;
};
export default React.memo(TruncatedTextTooltip);
});
import { useCopyToClipboard } from '@uidotdev/usehooks';
import React from 'react';
import { SECOND } from 'lib/consts';
import useIsMobile from 'lib/hooks/useIsMobile';
import { SECOND } from 'toolkit/utils/consts';
import { useDisclosure } from './useDisclosure';
// NOTE: If you don't need the disclosure and the timeout features, please use the useCopyToClipboard hook directly
export default function useClipboard(text: string, timeout = SECOND) {
export function useClipboard(text: string, timeout = SECOND) {
const flagTimeoutRef = React.useRef<number | null>(null);
const disclosureTimeoutRef = React.useRef<number | null>(null);
const [ hasCopied, setHasCopied ] = React.useState(false);
......
import { throttle } from 'es-toolkit';
import React from 'react';
export default function useIsSticky(ref: React.RefObject<HTMLDivElement>, offset = 0, isEnabled = true) {
export function useIsSticky(ref: React.RefObject<HTMLDivElement>, offset = 0, isEnabled = true) {
const [ isSticky, setIsSticky ] = React.useState(false);
const handleScroll = React.useCallback(() => {
......
......@@ -3,7 +3,7 @@ import React from 'react';
import { useFirstMountState } from './useFirstMountState';
// React effect hook that ignores the first invocation (e.g. on mount). The signature is exactly the same as the useEffect hook.
const useUpdateEffect: typeof React.useEffect = (effect, deps) => {
export const useUpdateEffect: typeof React.useEffect = (effect, deps) => {
const isFirstMount = useFirstMountState();
React.useEffect(() => {
......@@ -13,5 +13,3 @@ const useUpdateEffect: typeof React.useEffect = (effect, deps) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
};
export default useUpdateEffect;
import { debounce } from 'es-toolkit';
import { useEffect, useState } from 'react';
export default function useViewportSize(debounceTime = 100) {
export function useViewportSize(debounceTime = 100) {
const [ viewportSize, setViewportSize ] = useState({ width: 0, height: 0 });
useEffect(() => {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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