Commit e91e9ae1 authored by tom's avatar tom

dialog styles and AuthModal first step refactoring

parent 302a4760
import type { ButtonProps } from '@chakra-ui/react';
import { IconButton as ChakraIconButton } from '@chakra-ui/react';
import { IconButton as ChakraIconButton, useRecipe } from '@chakra-ui/react';
import * as React from 'react';
import { LuX } from 'react-icons/lu';
export type CloseButtonProps = ButtonProps;
import { recipe as closeButtonRecipe } from '../theme/recipes/close-button.recipe';
export interface CloseButtonProps extends Omit<ButtonProps, 'visual' | 'size'> {
visual?: 'plain';
size?: 'md';
}
export const CloseButton = React.forwardRef<
HTMLButtonElement,
CloseButtonProps
>(function CloseButton(props, ref) {
const recipe = useRecipe({ recipe: closeButtonRecipe });
const [ recipeProps, restProps ] = recipe.splitVariantProps(props);
const styles = recipe(recipeProps);
return (
<ChakraIconButton variant="ghost" aria-label="Close" ref={ ref } { ...props }>
<ChakraIconButton aria-label="Close" ref={ ref } css={ styles } { ...restProps }>
{ props.children ?? <LuX/> }
</ChakraIconButton>
);
......
......@@ -39,22 +39,37 @@ export const DialogCloseTrigger = React.forwardRef<
>(function DialogCloseTrigger(props, ref) {
return (
<ChakraDialog.CloseTrigger
position="absolute"
top="2"
insetEnd="2"
{ ...props }
asChild
>
<CloseButton size="sm" ref={ ref }>
<CloseButton ref={ ref }>
{ props.children }
</CloseButton>
</ChakraDialog.CloseTrigger>
);
});
export interface DialogHeaderProps extends ChakraDialog.HeaderProps {
startElement?: React.ReactNode;
}
export const DialogHeader = React.forwardRef<
HTMLDivElement,
DialogHeaderProps
>(function DialogHeader(props, ref) {
const { startElement, ...rest } = props;
return (
<ChakraDialog.Header ref={ ref } { ...rest }>
{ startElement }
<ChakraDialog.Title>{ props.children }</ChakraDialog.Title>
<DialogCloseTrigger ml="auto"/>
</ChakraDialog.Header>
);
});
export const DialogRoot = ChakraDialog.Root;
export const DialogFooter = ChakraDialog.Footer;
export const DialogHeader = ChakraDialog.Header;
export const DialogBody = ChakraDialog.Body;
export const DialogBackdrop = ChakraDialog.Backdrop;
export const DialogTitle = ChakraDialog.Title;
......
......@@ -25,12 +25,13 @@ export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
placeholder: ' ',
size: props.size,
floating: props.floating,
bgColor: rest.bgColor,
});
return (
<ChakraField.Root pos="relative" w="full" ref={ ref } { ...rest }>
{ clonedChild }
<ChakraField.Label>
<ChakraField.Label bgColor={ rest.bgColor }>
{ label }
<ChakraField.RequiredIndicator fallback={ optionalText }/>
{ errorText && (
......
......@@ -12,8 +12,10 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
py="0"
height="auto"
minW="auto"
flexShrink="0"
ref={ ref }
{ ...props }
visual={ props.visual ?? 'plain' }
/>
);
},
......
......@@ -46,7 +46,7 @@ export const PopoverCloseTrigger = React.forwardRef<
asChild
ref={ ref }
>
<CloseButton size="sm"/>
<CloseButton/>
</ChakraPopover.CloseTrigger>
);
});
......
......@@ -140,7 +140,7 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
},
input: {
fg: {
DEFAULT: { value: { _light: '{colors.blue.800}', _dark: '{colors.gray.50}' } },
DEFAULT: { value: { _light: '{colors.gray.800}', _dark: '{colors.gray.50}' } },
error: { value: '{colors.text.error}' },
},
bg: {
......@@ -163,6 +163,14 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
error: { value: '{colors.red.500}' },
},
},
dialog: {
bg: {
DEFAULT: { value: { _light: '{colors.white}', _dark: '{colors.gray.900}' } },
},
fg: {
DEFAULT: { value: { _light: '{colors.blackAlpha.800}', _dark: '{colors.whiteAlpha.800}' } },
},
},
text: {
primary: { value: { _light: '{colors.blackAlpha.800}', _dark: '{colors.whiteAlpha.800}' } },
secondary: { value: { _light: '{colors.gray.500}', _dark: '{colors.gray.400}' } },
......@@ -172,6 +180,9 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
divider: { value: { _light: '{colors.blackAlpha.200}', _dark: '{colors.whiteAlpha.200}' } },
error: { value: '{colors.red.500}' },
},
icon: {
backTo: { value: '{colors.gray.400}' },
},
global: {
body: {
bg: { value: { _light: '{colors.white}', _dark: '{colors.black}' } },
......
......@@ -105,6 +105,16 @@ export const recipe = defineRecipe({
bg: 'transparent',
},
},
link: {
bg: 'transparent',
color: 'link.primary',
border: 'none',
fontWeight: '400',
_hover: {
bg: 'transparent',
color: 'link.primary.hovered',
},
},
},
size: {
xs: { px: 2, h: 6, fontSize: '12px' },
......
import { defineRecipe } from '@chakra-ui/react';
export const recipe = defineRecipe({
base: {
display: 'flex',
gap: 0,
borderRadius: 'sm',
overflow: 'hidden',
_disabled: {
opacity: 0.2,
},
minWidth: 'auto',
},
variants: {
visual: {
plain: {
bg: 'transparent',
color: 'icon.backTo',
border: 'none',
_hover: {
bg: 'transparent',
color: 'link.primary.hover',
},
},
},
size: {
md: { boxSize: 6, '& svg': { boxSize: 5 } },
},
},
defaultVariants: {
size: 'md',
visual: 'plain',
},
});
import { defineSlotRecipe } from '@chakra-ui/react';
export const recipe = defineSlotRecipe({
slots: [ 'backdrop', 'positioner', 'content', 'header', 'body', 'footer', 'title', 'description' ],
base: {
backdrop: {
bg: 'blackAlpha.500',
pos: 'fixed',
left: 0,
top: 0,
w: '100vw',
h: '100dvh',
zIndex: 'modal',
_open: {
animationName: 'fade-in',
animationDuration: 'slow',
},
_closed: {
animationName: 'fade-out',
animationDuration: 'moderate',
},
},
positioner: {
display: 'flex',
width: '100vw',
height: '100dvh',
position: 'fixed',
left: 0,
top: 0,
'--dialog-z-index': 'zIndex.modal',
zIndex: 'calc(var(--dialog-z-index) + var(--layer-index, 0))',
justifyContent: 'center',
overscrollBehaviorY: 'none',
},
content: {
display: 'flex',
flexDirection: 'column',
position: 'relative',
width: '100%',
padding: 6,
outline: 0,
textStyle: 'md',
my: 'var(--dialog-margin, var(--dialog-base-margin))',
'--dialog-z-index': 'zIndex.modal',
zIndex: 'calc(var(--dialog-z-index) + var(--layer-index, 0))',
bg: 'dialog.bg',
color: 'dialog.fg',
boxShadow: 'lg',
_open: {
animationDuration: 'moderate',
},
_closed: {
animationDuration: 'faster',
},
},
header: {
flex: 0,
p: 0,
mb: 2,
display: 'flex',
alignItems: 'center',
columnGap: 2,
},
body: {
flex: '1',
p: 0,
},
footer: {
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
gap: '3',
px: '6',
pt: '2',
pb: '4',
},
title: {
textStyle: 'heading.md',
fontWeight: '500',
},
description: {
color: 'dialog.fg',
},
},
variants: {
placement: {
center: {
positioner: {
alignItems: 'center',
},
content: {
'--dialog-base-margin': 'auto',
mx: 'auto',
},
},
top: {
positioner: {
alignItems: 'flex-start',
},
content: {
'--dialog-base-margin': 'spacing.16',
mx: 'auto',
},
},
bottom: {
positioner: {
alignItems: 'flex-end',
},
content: {
'--dialog-base-margin': 'spacing.16',
mx: 'auto',
},
},
},
scrollBehavior: {
inside: {
positioner: {
overflow: 'hidden',
},
content: {
minH: 'auto',
maxH: 'calc(100% - 7.5rem)',
borderRadius: 'xl',
},
body: {
overflow: 'auto',
},
},
outside: {
positioner: {
overflow: 'auto',
pointerEvents: 'auto',
},
},
},
size: {
sm: {
content: {
maxW: '400px',
},
},
md: {
content: {
maxW: '640px',
},
},
cover: {
positioner: {
padding: '10',
},
content: {
width: '100%',
height: '100%',
'--dialog-margin': '0',
},
},
full: {
content: {
maxW: '100vw',
minH: '100vh',
'--dialog-margin': '0',
borderRadius: '0',
},
},
},
motionPreset: {
scale: {
content: {
_open: { animationName: 'scale-in, fade-in' },
_closed: { animationName: 'scale-out, fade-out' },
},
},
'slide-in-bottom': {
content: {
_open: { animationName: 'slide-from-bottom, fade-in' },
_closed: { animationName: 'slide-to-bottom, fade-out' },
},
},
'slide-in-top': {
content: {
_open: { animationName: 'slide-from-top, fade-in' },
_closed: { animationName: 'slide-to-top, fade-out' },
},
},
'slide-in-left': {
content: {
_open: { animationName: 'slide-from-left, fade-in' },
_closed: { animationName: 'slide-to-left, fade-out' },
},
},
'slide-in-right': {
content: {
_open: { animationName: 'slide-from-right, fade-in' },
_closed: { animationName: 'slide-to-right, fade-out' },
},
},
none: {},
},
},
defaultVariants: {
size: 'md',
scrollBehavior: 'inside',
placement: 'center',
motionPreset: 'scale',
},
});
......@@ -51,6 +51,7 @@ export const recipe = defineSlotRecipe({
bg: 'bg',
top: '2px',
left: '2px',
color: 'gray.500',
width: 'calc(100% - 4px)',
borderRadius: 'base',
pointerEvents: 'none',
......@@ -83,7 +84,7 @@ export const recipe = defineSlotRecipe({
},
},
// special size for textarea
xxl: {
'2xl': {
label: {
fontSize: 'md',
},
......@@ -136,7 +137,7 @@ export const recipe = defineSlotRecipe({
},
},
{
size: 'xxl',
size: '2xl',
floating: true,
css: {
label: {
......
import { recipe as alert } from './alert.recipe';
import { recipe as button } from './button.recipe';
import { recipe as closeButton } from './close-button.recipe';
import { recipe as dialog } from './dialog.recipe';
import { recipe as field } from './field.recipe';
import { recipe as input } from './input.recipe';
import { recipe as link } from './link.recipe';
......@@ -14,6 +16,7 @@ import { recipe as tooltip } from './tooltip.recipe';
export const recipes = {
button,
closeButton,
input,
link,
skeleton,
......@@ -22,6 +25,7 @@ export const recipes = {
export const slotRecipes = {
alert,
dialog,
field,
popover,
progressCircle,
......
......@@ -20,7 +20,7 @@ export const recipe = defineRecipe({
},
variants: {
size: {
xxl: {
'2xl': {
textStyle: 'md',
px: '6',
py: '4',
......@@ -76,7 +76,7 @@ export const recipe = defineRecipe({
},
defaultVariants: {
size: 'xxl',
size: '2xl',
variant: 'outline',
},
});
......@@ -40,6 +40,7 @@ const ChakraShowcases = () => {
<Button visual="header">Header</Button>
<Button visual="header" selected>Header selected</Button>
<Button visual="header" selected highlighted>Header highlighted</Button>
<Button visual="link">Link</Button>
</HStack>
</section>
......@@ -102,10 +103,10 @@ const ChakraShowcases = () => {
<section>
<Heading textStyle="heading.md" mb={ 2 }>Textarea</Heading>
<HStack gap={ 4 }>
<Field label="Description" required floating size="xxl" w="400px">
<Field label="Description" required floating size="2xl" w="400px">
<Textarea/>
</Field>
<Field label="Description" required floating size="xxl" w="400px">
<Field label="Description" required floating size="2xl" w="400px">
<Textarea value={ TEXT }/>
</Field>
</HStack>
......
import React from 'react';
import { IconButton } from 'toolkit/chakra/icon-button';
import IconSvg from 'ui/shared/IconSvg';
interface Props {
onClick: () => void;
}
const ButtonBackTo = ({ onClick }: Props) => {
return (
<IconButton>
<IconSvg
name="arrows/east"
boxSize={ 6 }
transform="rotate(180deg)"
color="icon.backTo"
_hover={{ color: 'link.primary.hover' }}
onClick={ onClick }
/>
</IconButton>
);
};
export default React.memo(ButtonBackTo);
import type { ChakraProps } from '@chakra-ui/react';
import React from 'react';
import type { FieldValues, Path } from 'react-hook-form';
import type { FieldValues } from 'react-hook-form';
import type { FormFieldPropsBase } from './types';
import type { PartialBy } from 'types/utils';
......@@ -28,9 +27,4 @@ const FormFieldEmail = <FormFields extends FieldValues>(
);
};
export type WrappedComponent = <
FormFields extends FieldValues,
Name extends Path<FormFields> = Path<FormFields>,
>(props: PartialBy<FormFieldPropsBase<FormFields, Name>, 'placeholder'> & ChakraProps) => React.JSX.Element;
export default React.memo(FormFieldEmail) as WrappedComponent;
export default React.memo(FormFieldEmail) as typeof FormFieldEmail;
import type { ChakraProps } from '@chakra-ui/react';
import { FormControl, Input, InputGroup, InputRightElement, Textarea, chakra, shouldForwardProp } from '@chakra-ui/react';
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';
import type { FormFieldPropsBase } from './types';
import FormInputPlaceholder from '../inputs/FormInputPlaceholder';
import { Field } from 'toolkit/chakra/field';
import { Input } from 'toolkit/chakra/input';
import { Textarea } from 'toolkit/chakra/textarea';
import getFieldErrorText from '../utils/getFieldErrorText';
interface Props<
FormFields extends FieldValues,
......@@ -21,95 +24,82 @@ const FormFieldText = <
>({
name,
placeholder,
isReadOnly,
isRequired,
rules,
onBlur,
type = 'text',
rightElement,
inputProps,
asComponent,
max,
className,
size = 'md',
bgColor,
minH,
maxH,
size = 'xl',
disabled,
...restProps
}: Props<FormFields, Name>) => {
const { control } = useFormContext<FormFields>();
const { field, fieldState, formState } = useController<FormFields, typeof name>({
control,
name,
rules: { ...rules, required: isRequired },
rules: { ...rules, required: restProps.required },
});
const isDisabled = formState.isSubmitting;
const handleBlur = React.useCallback(() => {
field.onBlur();
onBlur?.();
}, [ field, onBlur ]);
const Component = asComponent === 'Textarea' ? Textarea : Input;
const input = (
<Component
const input = asComponent === 'Textarea' ? (
<Textarea
{ ...field }
autoComplete="off"
{ ...inputProps as HTMLChakraProps<'textarea'> }
onBlur={ handleBlur }
isInvalid={ Boolean(fieldState.error) }
isDisabled={ isDisabled }
isReadOnly={ isReadOnly }
/>
) : (
<Input
{ ...field }
autoComplete="off"
type={ type }
placeholder=" "
max={ max }
size={ size }
bgColor={ bgColor }
minH={ minH }
maxH={ maxH }
{ ...inputProps as HTMLChakraProps<'input'> }
onBlur={ handleBlur }
/>
);
const inputPlaceholder = size !== 'xs' && <FormInputPlaceholder text={ placeholder } error={ fieldState.error }/>;
return (
<FormControl
className={ className }
variant="floating"
isDisabled={ isDisabled }
isRequired={ isRequired }
<Field
label={ placeholder }
errorText={ getFieldErrorText(fieldState.error) }
invalid={ Boolean(fieldState.error) }
disabled={ formState.isSubmitting || disabled }
size={ size }
bgColor={ bgColor }
floating
{ ...restProps }
>
{ rightElement ? (
<InputGroup>
{ input }
{ inputPlaceholder }
<InputRightElement h="100%"> { rightElement({ field }) } </InputRightElement>
</InputGroup>
) : (
<>
{ input }
{ inputPlaceholder }
</>
) }
</FormControl>
{ input }
</Field>
);
};
const WrappedFormFieldText = chakra(FormFieldText, {
shouldForwardProp: (prop) => {
const isChakraProp = !shouldForwardProp(prop);
if (isChakraProp && ![ 'bgColor', 'size', 'minH', 'maxH' ].includes(prop)) {
return false;
}
return true;
},
});
// TODO @tom2drum add input group
export type WrappedComponent = <
FormFields extends FieldValues,
Name extends Path<FormFields> = Path<FormFields>,
>(props: Props<FormFields, Name> & ChakraProps) => React.JSX.Element;
// return (
// <FormControl
// className={ className }
// variant="floating"
// isDisabled={ isDisabled }
// isRequired={ isRequired }
// size={ size }
// bgColor={ bgColor }
// >
// { rightElement ? (
// <InputGroup>
// { input }
// { inputPlaceholder }
// <InputRightElement h="100%"> { rightElement({ field }) } </InputRightElement>
// </InputGroup>
// ) : (
// <>
// { input }
// { inputPlaceholder }
// </>
// ) }
// </FormControl>
// );
};
export default React.memo(WrappedFormFieldText) as WrappedComponent;
export default React.memo(FormFieldText) as typeof FormFieldText;
import type { FormControlProps } from '@chakra-ui/react';
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';
export interface FormFieldPropsBase<
FormFields extends FieldValues,
Name extends Path<FormFields> = Path<FormFields>,
> {
> extends Omit<FieldProps, 'children'> {
name: Name;
placeholder: string;
isReadOnly?: boolean;
isRequired?: boolean;
rules?: Omit<RegisterOptions<FormFields, Name>, 'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'>;
onBlur?: () => void;
onChange?: () => void;
type?: HTMLInputElement['type'];
rightElement?: ({ field }: { field: ControllerRenderProps<FormFields, Name> }) => React.ReactNode;
max?: HTMLInputElement['max'];
// styles
size?: FormControlProps['size'];
bgColor?: FormControlProps['bgColor'];
maxH?: FormControlProps['maxH'];
minH?: FormControlProps['minH'];
className?: string;
inputProps?: HTMLChakraProps<'input' | 'textarea'>;
}
......@@ -9,6 +9,7 @@ interface Props {
isFancy?: boolean;
}
// TODO @tom2drum remove this component
const FormInputPlaceholder = ({ text, icon, error, isFancy }: Props) => {
let errorMessage = error?.message;
......
import type { FieldError } from 'react-hook-form';
export default function getFieldErrorText(error: FieldError | undefined) {
if (!error?.message && error?.type === 'pattern') {
return 'Invalid format';
}
return error?.message;
}
......@@ -8,8 +8,8 @@ import config from 'configs/app';
import { getResourceKey } from 'lib/api/useApiQuery';
import useGetCsrfToken from 'lib/hooks/useGetCsrfToken';
import * as mixpanel from 'lib/mixpanel';
import { DialogBackdrop, DialogBody, DialogCloseTrigger, DialogContent, DialogHeader, DialogRoot } from 'toolkit/chakra/dialog';
import IconSvg from 'ui/shared/IconSvg';
import { DialogBackdrop, DialogBody, DialogContent, DialogHeader, DialogRoot } from 'toolkit/chakra/dialog';
import ButtonBackTo from 'ui/shared/buttons/ButtonBackTo';
import AuthModalScreenConnectWallet from './screens/AuthModalScreenConnectWallet';
import AuthModalScreenEmail from './screens/AuthModalScreenEmail';
......@@ -96,7 +96,7 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig, closeOnError }: Pro
}, [ isSuccess, onClose ]);
const onModalOpenChange = React.useCallback(({ open }: { open: boolean }) => {
open && onClose();
!open && onClose();
}, [ onClose ]);
const header = (() => {
......@@ -170,23 +170,13 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig, closeOnError }: Pro
return (
<DialogRoot open onOpenChange={ onModalOpenChange } size={{ base: 'full', lg: 'sm' }}>
<DialogBackdrop/>
<DialogContent p={ 6 } maxW={{ lg: '400px' }}>
<DialogHeader fontWeight="500" textStyle="h3" mb={ 2 } display="flex" alignItems="center" columnGap={ 2 }>
{ steps.length > 1 && !steps[steps.length - 1].type.startsWith('success') && (
<IconSvg
name="arrows/east"
boxSize={ 6 }
transform="rotate(180deg)"
color="gray.400"
flexShrink={ 0 }
onClick={ onPrevStep }
cursor="pointer"
/>
) }
<DialogContent>
<DialogHeader
startElement={ steps.length > 1 && !steps[steps.length - 1].type.startsWith('success') && <ButtonBackTo onClick={ onPrevStep }/> }
>
{ header }
</DialogHeader>
<DialogCloseTrigger top={ 6 } right={ 6 } color="gray.400"/>
<DialogBody mb={ 0 }>
<DialogBody>
{ content }
</DialogBody>
</DialogContent>
......
import { chakra, Button, Text } from '@chakra-ui/react';
import { chakra, Text } from '@chakra-ui/react';
import React from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { FormProvider, useForm } from 'react-hook-form';
import type { EmailFormFields, Screen } from '../types';
import { toaster } from 'toolkit/chakra/toaster';
import useApiFetch from 'lib/api/useApiFetch';
import getErrorMessage from 'lib/errors/getErrorMessage';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import * as mixpanel from 'lib/mixpanel';
import { Button } from 'toolkit/chakra/button';
import { toaster } from 'toolkit/chakra/toaster';
import FormFieldEmail from 'ui/shared/forms/fields/FormFieldEmail';
import ReCaptcha from 'ui/shared/reCaptcha/ReCaptcha';
import useReCaptcha from 'ui/shared/reCaptcha/useReCaptcha';
......@@ -79,16 +80,16 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => {
<Text>Account email, used for transaction notifications from your watchlist.</Text>
<FormFieldEmail<EmailFormFields>
name="email"
isRequired
required
placeholder="Email"
bgColor="dialog_bg"
bgColor="dialog.bg"
mt={ 6 }
/>
<Button
mt={ 6 }
type="submit"
disabled={ formApi.formState.isSubmitting }
isLoading={ formApi.formState.isSubmitting }
loading={ formApi.formState.isSubmitting }
loadingText="Send a code"
>
Send a code
......
import { useDisclosure, type ButtonProps } from '@chakra-ui/react';
import { type ButtonProps } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -8,6 +8,7 @@ import config from 'configs/app';
import * as mixpanel from 'lib/mixpanel';
import useAccount from 'lib/web3/useAccount';
import { PopoverBody, PopoverContent, PopoverRoot, PopoverTrigger } from 'toolkit/chakra/popover';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import AuthModal from 'ui/snippets/auth/AuthModal';
import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
......
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