chore: button component (#81)
* chore: add button component with storybook * chore: add stories button * refactor: remove github custom logo * fix: fix build * chore: changes based on PR review
This commit is contained in:
parent
1dd06c6baf
commit
75a6de5ac7
|
|
@ -13,6 +13,10 @@ export const parameters = {
|
|||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
darkMode: {
|
||||
dark: { ...themes.dark, backgroundColor: 'black' },
|
||||
// light: { ...themes.normal, backgroundColor: 'white' },
|
||||
},
|
||||
};
|
||||
|
||||
const { darkTheme: darkThemeClassName } = dripStitches;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"@emotion/styled": "^11.10.5",
|
||||
"@ethersproject/providers": "^5.7.2",
|
||||
"@radix-ui/colors": "^0.1.8",
|
||||
"@react-icons/all-files": "^4.1.0",
|
||||
"@reduxjs/toolkit": "^1.9.1",
|
||||
"@stitches/react": "^1.2.8",
|
||||
"formik": "^2.2.9",
|
||||
|
|
@ -57,7 +58,6 @@
|
|||
"ethers": "^5.7.2",
|
||||
"postcss": "^8.4.21",
|
||||
"prettier": "^2.8.0",
|
||||
"react-icons": "^4.7.1",
|
||||
"react-query": "^3.39.2",
|
||||
"storybook-dark-mode": "^2.0.5",
|
||||
"tailwindcss": "^3.2.4",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import { dripStitches } from '../../../theme/stitches'; //TODO replace with absolute path
|
||||
import { Flex, Grid } from '../../layout';
|
||||
|
||||
const { styled } = dripStitches;
|
||||
|
||||
export const StyledButtonContentGrid = styled(Grid, {
|
||||
gap: '$0h',
|
||||
});
|
||||
|
||||
export const StyledButtonContentFlex = styled(Flex, {
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import React from 'react';
|
||||
|
||||
import { ButtonProps } from '.';
|
||||
import {
|
||||
StyledButtonContentFlex,
|
||||
StyledButtonContentGrid,
|
||||
} from './button-content.styled';
|
||||
import { ButtonIcon } from './button-icon';
|
||||
|
||||
export type ButtonContentProps = Pick<
|
||||
ButtonProps,
|
||||
| 'leftIcon'
|
||||
| 'rightIcon'
|
||||
| 'topIcon'
|
||||
| 'bottomIcon'
|
||||
| 'children'
|
||||
| 'iconSpacing'
|
||||
>;
|
||||
|
||||
export const ButtonContent: React.FC<ButtonContentProps> = (props) => {
|
||||
const {
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
topIcon,
|
||||
bottomIcon,
|
||||
children,
|
||||
iconSpacing = '1h',
|
||||
} = props;
|
||||
|
||||
const midNode = (
|
||||
<>
|
||||
{leftIcon && (
|
||||
<ButtonIcon css={{ marginRight: `$${iconSpacing}` }}>
|
||||
{leftIcon}
|
||||
</ButtonIcon>
|
||||
)}
|
||||
{children}
|
||||
{rightIcon && (
|
||||
<ButtonIcon css={{ marginLeft: `$${iconSpacing}` }}>
|
||||
{rightIcon}
|
||||
</ButtonIcon>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
if (!topIcon && !bottomIcon) {
|
||||
return midNode;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledButtonContentGrid>
|
||||
{topIcon && <ButtonIcon>{topIcon}</ButtonIcon>}
|
||||
<StyledButtonContentFlex>{midNode}</StyledButtonContentFlex>
|
||||
{bottomIcon && <ButtonIcon>{bottomIcon}</ButtonIcon>}
|
||||
</StyledButtonContentGrid>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { dripStitches } from '../../../theme/stitches'; //TODO replace with absolute path
|
||||
|
||||
const { styled } = dripStitches;
|
||||
|
||||
export const StyledButtonIconSpan = styled('span', {
|
||||
all: 'unset',
|
||||
display: 'inline-flex',
|
||||
alignSelf: 'center',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
lineHeight: 1.3,
|
||||
flexShrink: 0,
|
||||
});
|
||||
|
||||
export type ButtonIconSpanProps = React.ComponentProps<
|
||||
typeof StyledButtonIconSpan
|
||||
>;
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
ButtonIconSpanProps,
|
||||
StyledButtonIconSpan,
|
||||
} from './button-icon.styled';
|
||||
|
||||
export const ButtonIcon: React.FC<ButtonIconSpanProps> = (props) => {
|
||||
const { children, className, ...rest } = props;
|
||||
|
||||
const _children = React.isValidElement(children)
|
||||
? React.cloneElement(children, {
|
||||
'aria-hidden': true,
|
||||
focusable: false,
|
||||
})
|
||||
: children;
|
||||
|
||||
return (
|
||||
<StyledButtonIconSpan {...rest} className={className}>
|
||||
{_children}
|
||||
</StyledButtonIconSpan>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { dripStitches } from '../../../theme/stitches'; //TODO replace with absolute path
|
||||
import { Flex, Grid } from '../../layout';
|
||||
|
||||
const { styled } = dripStitches;
|
||||
|
||||
export const StyledButtonSpinnerBox = styled(Flex, {
|
||||
alignItems: 'center',
|
||||
fontSize: '$md',
|
||||
lineHeight: 'normal',
|
||||
});
|
||||
|
||||
export const StyledButtonSpinnerDotsBox = styled(Grid, {
|
||||
gap: '$1',
|
||||
gridAutoFlow: 'column',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
export const StyledButtonSpinnerDot = styled('div', {
|
||||
all: 'unset',
|
||||
width: '$2',
|
||||
height: '$2',
|
||||
borderRadius: '$full',
|
||||
backgroundColor: '$slate1',
|
||||
opacity: 0.2,
|
||||
});
|
||||
|
||||
export interface ButtonSpinnerProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
label?: string;
|
||||
spacing?: string;
|
||||
placement?: 'start' | 'end';
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import { dripStitches } from '../../../theme/stitches'; //TODO replace with absolute path
|
||||
import React, { HTMLAttributes, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
ButtonSpinnerProps,
|
||||
StyledButtonSpinnerBox,
|
||||
StyledButtonSpinnerDot,
|
||||
StyledButtonSpinnerDotsBox,
|
||||
} from './button-spinner.styled';
|
||||
|
||||
export const ButtonSpinner: React.FC<ButtonSpinnerProps> = (props) => {
|
||||
const {
|
||||
label,
|
||||
placement,
|
||||
spacing = '$2',
|
||||
children,
|
||||
className,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const marginProp = placement === 'start' ? 'marginRight' : 'marginLeft';
|
||||
|
||||
const spinnerStyles = useMemo(
|
||||
() => ({
|
||||
position: label ? 'relative' : 'absolute',
|
||||
[marginProp]: label ? spacing : 0,
|
||||
}),
|
||||
[label, marginProp, spacing]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledButtonSpinnerBox className={className} css={spinnerStyles} {...rest}>
|
||||
{children || <ButtonSpinnerDots />}
|
||||
</StyledButtonSpinnerBox>
|
||||
);
|
||||
};
|
||||
|
||||
const { keyframes } = dripStitches;
|
||||
|
||||
const blink = keyframes({
|
||||
'0%': { opacity: 0.2 },
|
||||
'50%': { opacity: 1 },
|
||||
'100%': { opacity: 0.2 },
|
||||
});
|
||||
|
||||
const ButtonSpinnerDots: React.FC<HTMLAttributes<HTMLDivElement>> = (props) => {
|
||||
return (
|
||||
<StyledButtonSpinnerDotsBox {...props}>
|
||||
<StyledButtonSpinnerDot
|
||||
css={{
|
||||
animation: `${blink} infinite linear 2s`,
|
||||
animationDelay: '0ms',
|
||||
}}
|
||||
/>
|
||||
<StyledButtonSpinnerDot
|
||||
css={{
|
||||
animation: `${blink} infinite linear 2s`,
|
||||
animationDelay: '250ms',
|
||||
}}
|
||||
/>
|
||||
<StyledButtonSpinnerDot
|
||||
css={{
|
||||
animation: `${blink} infinite linear 2s`,
|
||||
animationDelay: '500ms',
|
||||
}}
|
||||
/>
|
||||
</StyledButtonSpinnerDotsBox>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
import { Flex } from '../../layout';
|
||||
import { Button } from './button';
|
||||
import { IconButton } from './icon-button';
|
||||
import { Icon as IconComponent } from '../icon';
|
||||
import { dripStitches } from '../../../theme/stitches';
|
||||
|
||||
export default {
|
||||
title: 'Components/Button',
|
||||
component: Button,
|
||||
};
|
||||
|
||||
const { styled } = dripStitches;
|
||||
|
||||
const StoryFlex = styled(Flex, {
|
||||
display: 'flex',
|
||||
gap: '$2',
|
||||
flexWrap: 'wrap',
|
||||
});
|
||||
|
||||
export const Default = () => (
|
||||
<StoryFlex>
|
||||
<Button colorScheme="blue">Primary</Button>
|
||||
<Button>Default</Button>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
variant="outline"
|
||||
css={{ py: '$1', borderRadius: '$md' }}
|
||||
>
|
||||
Use for NFA
|
||||
</Button>
|
||||
<Button
|
||||
colorScheme="gray"
|
||||
variant="outline"
|
||||
css={{ py: '$1', borderRadius: '$md' }}
|
||||
>
|
||||
NFA Repo
|
||||
</Button>
|
||||
</StoryFlex>
|
||||
);
|
||||
export const Icon = () => (
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="Add"
|
||||
colorScheme="gray"
|
||||
variant="link"
|
||||
icon={<IconComponent name="info" />}
|
||||
/>
|
||||
<IconButton
|
||||
aria-label="Add"
|
||||
colorScheme="gray"
|
||||
variant="link"
|
||||
icon={<IconComponent name="back" />}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
export const ConnectorButtons = () => (
|
||||
<StoryFlex>
|
||||
<Button
|
||||
size="lg"
|
||||
iconSpacing="40"
|
||||
variant="ghost"
|
||||
rightIcon={<IconComponent name="github" css={{ color: 'white' }} />}
|
||||
>
|
||||
GitHub
|
||||
</Button>
|
||||
<Button
|
||||
disabled
|
||||
size="lg"
|
||||
iconSpacing="40"
|
||||
variant="ghost"
|
||||
rightIcon={<IconComponent name="github" css={{ color: 'white' }} />}
|
||||
>
|
||||
GitHub
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
iconSpacing="40"
|
||||
variant="ghost"
|
||||
rightIcon={<IconComponent name="ethereum" />}
|
||||
>
|
||||
Connect Ethereum Wallet
|
||||
</Button>
|
||||
<Button
|
||||
disabled
|
||||
size="lg"
|
||||
iconSpacing="40"
|
||||
variant="ghost"
|
||||
rightIcon={<IconComponent name="ethereum" />}
|
||||
>
|
||||
Connect Ethereum Wallet
|
||||
</Button>
|
||||
</StoryFlex>
|
||||
);
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
import { dripStitches } from '../../../theme/stitches';
|
||||
import { CSS } from '@stitches/react';
|
||||
|
||||
type StyledButtonProps = React.ComponentProps<typeof StyledButton>;
|
||||
export interface ButtonProps extends StyledButtonProps {
|
||||
/**
|
||||
* If `true`, the button will show a spinner.
|
||||
*/
|
||||
isLoading?: boolean;
|
||||
/**
|
||||
* If `true`, the button will be styled in its active state.
|
||||
*/
|
||||
isActive?: boolean;
|
||||
/**
|
||||
* If `true`, the button will be disabled.
|
||||
*/
|
||||
isDisabled?: boolean;
|
||||
/**
|
||||
* The label to show in the button when `isLoading` is true
|
||||
* If no text is passed, it only shows the spinner
|
||||
*/
|
||||
loadingText?: string;
|
||||
/**
|
||||
* If `true`, the button will take up the full width of its container.
|
||||
*/
|
||||
isFullWidth?: boolean;
|
||||
/**
|
||||
* The html button type to use.
|
||||
*/
|
||||
type?: 'button' | 'reset' | 'submit';
|
||||
/**
|
||||
* If added, the button will show an icon before the button's label.
|
||||
* @type React.ReactElement
|
||||
*/
|
||||
leftIcon?: React.ReactElement;
|
||||
/**
|
||||
* If added, the button will show an icon after the button's label.
|
||||
* @type React.ReactElement
|
||||
*/
|
||||
rightIcon?: React.ReactElement;
|
||||
/**
|
||||
* If added, the button will show an icon on top side from the button's label.
|
||||
* @type React.ReactElement
|
||||
*/
|
||||
topIcon?: React.ReactElement;
|
||||
/**
|
||||
* If added, the button will show an icon on bottom side from the button's label.
|
||||
* @type React.ReactElement
|
||||
*/
|
||||
bottomIcon?: React.ReactElement;
|
||||
/**
|
||||
* The space between the button icon and label.
|
||||
* @type SystemProps["marginRight"]
|
||||
*/
|
||||
iconSpacing?: string;
|
||||
/**
|
||||
* Replace the spinner component when `isLoading` is set to `true`
|
||||
* @type React.ReactElement
|
||||
*/
|
||||
spinner?: React.ReactElement;
|
||||
/**
|
||||
* It determines the placement of the spinner when isLoading is true
|
||||
* @default "start"
|
||||
*/
|
||||
spinnerPlacement?: 'start' | 'end';
|
||||
}
|
||||
|
||||
export type ButtonColor = 'gray' | 'blue';
|
||||
export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'link';
|
||||
|
||||
export type GetButtonCompoundVariantOptions = {
|
||||
color?: ButtonColor;
|
||||
variant?: ButtonVariant;
|
||||
};
|
||||
|
||||
const getButtonCompoundVariant = ({
|
||||
color = 'gray',
|
||||
variant = 'solid',
|
||||
}: GetButtonCompoundVariantOptions): CSS => {
|
||||
switch (variant) {
|
||||
case 'solid':
|
||||
return {
|
||||
color: 'white',
|
||||
transition: '$all-200',
|
||||
backgroundColor: `$${color}9`,
|
||||
'&:focus, &:hover': {
|
||||
backgroundColor: `$${color}10`,
|
||||
},
|
||||
'&:focus, &:active': {
|
||||
backgroundColor: `$${color}11`,
|
||||
},
|
||||
};
|
||||
case 'outline':
|
||||
return {
|
||||
color: `$${color}11`,
|
||||
transition: '$all-200',
|
||||
backgroundColor: `$${color}4`,
|
||||
'&:hover': {
|
||||
backgroundColor: `$${color}5`,
|
||||
},
|
||||
'&:focus, &:active': {
|
||||
backgroundColor: `$${color}6`,
|
||||
},
|
||||
};
|
||||
case 'link':
|
||||
return {
|
||||
color: `$${color}11`,
|
||||
transition: '$all-200',
|
||||
height: 'auto',
|
||||
px: '0',
|
||||
'&:hover, &:focus': {
|
||||
textDecoration: 'underline',
|
||||
color: `$${color}12`,
|
||||
'&:disabled': {
|
||||
textDecoration: 'none',
|
||||
},
|
||||
},
|
||||
};
|
||||
case 'ghost':
|
||||
return {
|
||||
color: `$slate11`,
|
||||
transition: '$all-200',
|
||||
|
||||
'&:hover, &:focus, &:active': {
|
||||
color: `$slate12`,
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: `$${color}4`,
|
||||
},
|
||||
'&:focus, &:active': {
|
||||
backgroundColor: `$${color}3`,
|
||||
},
|
||||
|
||||
'&:disabled': {
|
||||
backgroundColor: `initial`,
|
||||
'&:hover': {
|
||||
color: `$${color}11`,
|
||||
backgroundColor: `initial`,
|
||||
},
|
||||
'& img, & svg': {
|
||||
filter: 'grayscale(100%)',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const { styled } = dripStitches;
|
||||
|
||||
export const StyledButton = styled('button', {
|
||||
all: 'unset',
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
verticalAlign: 'middle',
|
||||
userSelect: 'none',
|
||||
fontWeight: '$medium',
|
||||
'&:disabled': {
|
||||
cursor: 'not-allowed',
|
||||
opacity: '0.4',
|
||||
},
|
||||
|
||||
variants: {
|
||||
size: {
|
||||
sm: {
|
||||
borderRadius: '$md',
|
||||
fontSize: '$xs',
|
||||
p: '$1 $3',
|
||||
},
|
||||
md: {
|
||||
borderRadius: '$lg',
|
||||
fontSize: '$sm',
|
||||
p: '$3 $3h',
|
||||
},
|
||||
lg: {
|
||||
borderRadius: '$xl',
|
||||
fontSize: '$md',
|
||||
p: '$4 $5',
|
||||
},
|
||||
},
|
||||
variant: {
|
||||
solid: {
|
||||
color: '$gray1',
|
||||
},
|
||||
ghost: {},
|
||||
outline: {},
|
||||
link: {},
|
||||
unstyled: {},
|
||||
},
|
||||
colorScheme: {
|
||||
gray: {},
|
||||
blue: {},
|
||||
red: {},
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
colorScheme: 'gray',
|
||||
variant: 'solid',
|
||||
css: getButtonCompoundVariant({ color: 'gray', variant: 'solid' }),
|
||||
},
|
||||
{
|
||||
colorScheme: 'blue',
|
||||
variant: 'solid',
|
||||
css: getButtonCompoundVariant({ color: 'blue', variant: 'solid' }),
|
||||
},
|
||||
{
|
||||
colorScheme: 'gray',
|
||||
variant: 'outline',
|
||||
css: getButtonCompoundVariant({ color: 'gray', variant: 'outline' }),
|
||||
},
|
||||
{
|
||||
colorScheme: 'blue',
|
||||
variant: 'outline',
|
||||
css: getButtonCompoundVariant({ color: 'blue', variant: 'outline' }),
|
||||
},
|
||||
{
|
||||
colorScheme: 'gray',
|
||||
variant: 'ghost',
|
||||
css: getButtonCompoundVariant({ color: 'gray', variant: 'ghost' }),
|
||||
},
|
||||
{
|
||||
colorScheme: 'blue',
|
||||
variant: 'ghost',
|
||||
css: getButtonCompoundVariant({ color: 'blue', variant: 'ghost' }),
|
||||
},
|
||||
{
|
||||
colorScheme: 'gray',
|
||||
variant: 'link',
|
||||
css: getButtonCompoundVariant({ color: 'gray', variant: 'link' }),
|
||||
},
|
||||
{
|
||||
colorScheme: 'blue',
|
||||
variant: 'link',
|
||||
css: getButtonCompoundVariant({ color: 'blue', variant: 'link' }),
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
colorScheme: 'gray',
|
||||
variant: 'solid',
|
||||
size: 'md',
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import { ButtonProps, StyledButton } from './button.styled';
|
||||
import { ButtonContent } from './button-content';
|
||||
import { ButtonSpinner } from './button-spinner';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
export const Button = forwardRef<ButtonProps, 'button'>((props, ref) => {
|
||||
const {
|
||||
isActive,
|
||||
isLoading,
|
||||
isDisabled,
|
||||
spinnerPlacement = 'start',
|
||||
spinner,
|
||||
loadingText,
|
||||
iconSpacing,
|
||||
topIcon,
|
||||
bottomIcon,
|
||||
rightIcon,
|
||||
leftIcon,
|
||||
isFullWidth,
|
||||
children,
|
||||
...ownProps
|
||||
} = props;
|
||||
|
||||
const contentProps = {
|
||||
rightIcon,
|
||||
leftIcon,
|
||||
bottomIcon,
|
||||
topIcon,
|
||||
iconSpacing,
|
||||
children,
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledButton
|
||||
ref={ref}
|
||||
disabled={isDisabled || isLoading}
|
||||
data-active={isActive}
|
||||
data-loading={isLoading}
|
||||
css={{
|
||||
width: isFullWidth ? '100%' : undefined,
|
||||
}}
|
||||
{...ownProps}
|
||||
>
|
||||
{isLoading && spinnerPlacement === 'start' && (
|
||||
<ButtonSpinner
|
||||
label={loadingText}
|
||||
placement={spinnerPlacement}
|
||||
spacing={iconSpacing}
|
||||
>
|
||||
{spinner}
|
||||
</ButtonSpinner>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
loadingText || (
|
||||
<span style={{ opacity: 0 }}>
|
||||
<ButtonContent {...contentProps} />
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<ButtonContent {...contentProps} />
|
||||
)}
|
||||
|
||||
{isLoading && spinnerPlacement === 'end' && (
|
||||
<ButtonSpinner
|
||||
label={loadingText}
|
||||
placement={spinnerPlacement}
|
||||
spacing={iconSpacing}
|
||||
>
|
||||
{spinner}
|
||||
</ButtonSpinner>
|
||||
)}
|
||||
</StyledButton>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import React, { forwardRef, useMemo } from 'react';
|
||||
|
||||
import { Button } from './button';
|
||||
import { ButtonProps } from './button.styled';
|
||||
|
||||
type OmittedProps =
|
||||
| 'leftIcon'
|
||||
| 'isFullWidth'
|
||||
| 'rightIcon'
|
||||
| 'loadingText'
|
||||
| 'iconSpacing'
|
||||
| 'spinnerPlacement';
|
||||
|
||||
type BaseButtonProps = Omit<ButtonProps, OmittedProps>;
|
||||
|
||||
export interface IconButtonProps extends BaseButtonProps {
|
||||
/**
|
||||
* The icon to be used in the button.
|
||||
* @type React.ReactElement
|
||||
*/
|
||||
icon?: React.ReactElement;
|
||||
/**
|
||||
* If `true`, the button will be perfectly round. Else, it'll be slightly round
|
||||
*/
|
||||
isRound?: boolean;
|
||||
/**
|
||||
* A11y: A label that describes the button
|
||||
*/
|
||||
'aria-label': string;
|
||||
}
|
||||
|
||||
export const IconButton = forwardRef<IconButtonProps, 'button'>(
|
||||
function IconButton(props, ref) {
|
||||
const {
|
||||
icon,
|
||||
children,
|
||||
isRound,
|
||||
'aria-label': ariaLabel,
|
||||
size = 'md',
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
/**
|
||||
* Passing the icon as prop or children should work
|
||||
*/
|
||||
const element = icon || children;
|
||||
const _children = React.isValidElement(element)
|
||||
? React.cloneElement(element, {
|
||||
'aria-hidden': true,
|
||||
focusable: false,
|
||||
})
|
||||
: null;
|
||||
|
||||
const { minWidth, fontSize } = useMemo(() => {
|
||||
const props = {
|
||||
sm: {
|
||||
minWidth: '$8',
|
||||
fontSize: '$lg',
|
||||
},
|
||||
md: {
|
||||
minWidth: '$11',
|
||||
fontSize: '$xl',
|
||||
},
|
||||
lg: {
|
||||
minWidth: '$14',
|
||||
fontSize: '$2xl',
|
||||
},
|
||||
};
|
||||
|
||||
return props[size as 'sm' | 'md' | 'lg'];
|
||||
}, [size]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
aria-label={ariaLabel}
|
||||
size={size}
|
||||
{...rest}
|
||||
css={{
|
||||
padding: 0,
|
||||
minWidth,
|
||||
fontSize,
|
||||
borderRadius: isRound ? '$full' : undefined,
|
||||
...(rest.css ?? {}),
|
||||
}}
|
||||
>
|
||||
{_children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './button.styled';
|
||||
export * from './button';
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { IconStyles as IS } from '../icon.styles';
|
||||
|
||||
export const EthereumIcon: React.FC<IS.CustomProps> = (props) => (
|
||||
<IS.Custom
|
||||
{...props}
|
||||
viewBox="0 0 22 36"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.9728 0L10.7329 0.820393V24.6242L10.9728 24.8651L21.9453 18.3339L10.9728 0Z"
|
||||
fill="#343434"
|
||||
/>
|
||||
<path d="M10.9727 0L0 18.3339L10.9727 24.8651V13.3114V0Z" fill="#8C8C8C" />
|
||||
<path
|
||||
d="M10.9727 26.9571L10.8376 27.1231V35.6024L10.9727 35.9997L21.952 20.4292L10.9727 26.9571Z"
|
||||
fill="#3C3C3B"
|
||||
/>
|
||||
<path
|
||||
d="M10.9727 35.9997V26.9571L0 20.4292L10.9727 35.9997Z"
|
||||
fill="#8C8C8C"
|
||||
/>
|
||||
<path
|
||||
d="M10.9727 24.8652L21.9452 18.3339L10.9727 13.3115V24.8652Z"
|
||||
fill="#141414"
|
||||
/>
|
||||
<path d="M0 18.3339L10.9727 24.8652V13.3115L0 18.3339Z" fill="#393939" />
|
||||
</IS.Custom>
|
||||
);
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './metamask-icon';
|
||||
export * from './ethereum-icon';
|
||||
|
|
@ -1,16 +1,12 @@
|
|||
import { Icon, IconProps } from '@chakra-ui/react';
|
||||
import { IconStyles as IS } from '../icon.styles';
|
||||
|
||||
export const MetamaskIcon: React.FC<IconProps> = (props) => {
|
||||
const { width = '1.5em', height = '1.5em' } = props;
|
||||
export const MetamaskIcon: React.FC<IS.CustomProps> = (props) => {
|
||||
return (
|
||||
<Icon
|
||||
width={width}
|
||||
height={height}
|
||||
<IS.Custom
|
||||
{...props}
|
||||
viewBox="0 0 256 240"
|
||||
version="1.1"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
>
|
||||
<title>MetaMask</title>
|
||||
<g>
|
||||
|
|
@ -131,6 +127,6 @@ export const MetamaskIcon: React.FC<IconProps> = (props) => {
|
|||
points="163.383898 33.1117385 147.744691 75.3505047 144.425852 132.411352 143.155934 150.295986 143.055195 195.983514 112.943788 195.983514 112.846176 150.381702 111.572114 132.395585 108.251786 75.3505047 92.6150854 33.1117385"
|
||||
></polygon>
|
||||
</g>
|
||||
</Icon>
|
||||
</IS.Custom>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { IoLogoGithub } from '@react-icons/all-files/io5/IoLogoGithub';
|
||||
import { IoArrowBackCircleSharp } from '@react-icons/all-files/io5/IoArrowBackCircleSharp';
|
||||
import { IoInformationCircleSharp } from '@react-icons/all-files/io5/IoInformationCircleSharp';
|
||||
import { MetamaskIcon, EthereumIcon } from './custom';
|
||||
|
||||
export const IconLibrary = Object.freeze({
|
||||
back: IoArrowBackCircleSharp,
|
||||
ethereum: EthereumIcon,
|
||||
github: IoLogoGithub,
|
||||
info: IoInformationCircleSharp,
|
||||
metamask: MetamaskIcon, //remove if not used
|
||||
});
|
||||
|
||||
export type IconName = keyof typeof IconLibrary;
|
||||
|
||||
export type IconType<Name extends IconName> = typeof IconLibrary[Name];
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { Icon } from './icon';
|
||||
import { styled } from '@stitches/react';
|
||||
import { Flex } from '../../layout';
|
||||
|
||||
export default {
|
||||
title: 'Components/Icons',
|
||||
component: Icon,
|
||||
};
|
||||
|
||||
const StoryFlex = styled(Flex, {
|
||||
display: 'flex',
|
||||
gap: '$2',
|
||||
flexWrap: 'wrap',
|
||||
color: 'white',
|
||||
});
|
||||
|
||||
export const ConnectorIcons = () => (
|
||||
<StoryFlex>
|
||||
<Icon name="metamask" />
|
||||
<Icon name="github" />
|
||||
<Icon name="ethereum" />
|
||||
</StoryFlex>
|
||||
);
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import { dripStitches } from '../../../theme/stitches'; //TODO replace for absolute path
|
||||
|
||||
const { styled } = dripStitches;
|
||||
|
||||
export abstract class IconStyles {
|
||||
static readonly Container = styled('span', {
|
||||
transition: 'transform $default',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
||||
variants: {
|
||||
size: {
|
||||
sm: {
|
||||
fontSize: '$md',
|
||||
},
|
||||
md: {
|
||||
fontSize: '$2xl',
|
||||
},
|
||||
lg: {
|
||||
fontSize: '$3xl',
|
||||
},
|
||||
},
|
||||
|
||||
rotate: {
|
||||
true: {
|
||||
transform: 'rotate(-180deg)',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
defaultVariants: {
|
||||
rotate: false,
|
||||
},
|
||||
});
|
||||
|
||||
static readonly Custom = styled('svg');
|
||||
}
|
||||
|
||||
export namespace IconStyles {
|
||||
export type CustomProps = React.ComponentProps<typeof IconStyles.Custom>;
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { forwardRef } from 'react';
|
||||
import { IconStyles } from './icon.styles';
|
||||
import { IconLibrary, IconName, IconType } from './icon-library';
|
||||
|
||||
export type IconProps = {
|
||||
name: IconName;
|
||||
} & React.ComponentProps<typeof IconStyles.Container>;
|
||||
|
||||
export const Icon: React.FC<IconProps> = forwardRef<HTMLSpanElement, IconProps>(
|
||||
(props, ref) => {
|
||||
const { name, ...rest } = props;
|
||||
const IconElement: IconType<typeof name> = IconLibrary[name];
|
||||
|
||||
return (
|
||||
<IconStyles.Container {...rest} ref={ref}>
|
||||
<IconElement style={{ width: '1em', height: '1em' }} />
|
||||
</IconStyles.Container>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Icon.displayName = 'Icon';
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './icon';
|
||||
export * from './icon-library';
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './button';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from './metamask-icon';
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { FaWallet } from 'react-icons/fa';
|
||||
import { IoExitOutline } from 'react-icons/io5';
|
||||
import { AiOutlineCopy } from 'react-icons/ai';
|
||||
import { MetamaskIcon } from './custom';
|
||||
|
||||
export const IconLibrary = Object.freeze({
|
||||
copy: AiOutlineCopy,
|
||||
'log-out': IoExitOutline,
|
||||
metamask: MetamaskIcon,
|
||||
wallet: FaWallet,
|
||||
});
|
||||
|
||||
export type IconName = keyof typeof IconLibrary;
|
||||
|
||||
export type IconType<Name extends IconName> = typeof IconLibrary[Name];
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import {
|
||||
forwardRef,
|
||||
IconProps as IconPropsChakra,
|
||||
Icon as IconChakra,
|
||||
} from '@chakra-ui/react';
|
||||
import { IconLibrary, IconName, IconType } from './icon-types';
|
||||
|
||||
export type IconComponentProps = IconPropsChakra & { name: IconName };
|
||||
|
||||
export const Icon = forwardRef<IconComponentProps, 'svg'>(
|
||||
({ name, ...iconProps }, ref) => {
|
||||
const IconElement: IconType<typeof name> = IconLibrary[name];
|
||||
return <IconChakra as={IconElement} {...iconProps} ref={ref} />;
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from './icon';
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { dripStitches } from '../../theme/stitches'; //TODO replace with absolute path
|
||||
import React from 'react';
|
||||
|
||||
const { styled } = dripStitches;
|
||||
|
||||
export const Flex = styled('div', {
|
||||
display: 'flex',
|
||||
});
|
||||
|
||||
export type FlexProps = React.ComponentProps<typeof Flex>;
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { dripStitches } from '../../theme/stitches'; //TODO replace with absolute path
|
||||
|
||||
const { styled } = dripStitches;
|
||||
|
||||
export const Grid = styled('div', {
|
||||
display: 'grid',
|
||||
});
|
||||
|
||||
export type GridProps = React.ComponentProps<typeof Grid>;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './grid.styled';
|
||||
export * from './flex.styled';
|
||||
|
|
@ -9,7 +9,7 @@ import {
|
|||
Button,
|
||||
Flex,
|
||||
} from '@chakra-ui/react';
|
||||
import { Icon } from '../icon';
|
||||
import { Icon } from '../core/icon';
|
||||
import { WalletType } from './wallet.utils';
|
||||
|
||||
const WalletMenu: React.FC = () => {
|
||||
|
|
@ -41,7 +41,6 @@ const WalletMenu: React.FC = () => {
|
|||
_hover={{ bg: 'custom.gray.100' }}
|
||||
bg={'custom.gray.200'}
|
||||
onClick={handleCopyAccount}
|
||||
icon={<Icon name="copy" />}
|
||||
>
|
||||
Copy Account
|
||||
</MenuItem>
|
||||
|
|
@ -49,7 +48,6 @@ const WalletMenu: React.FC = () => {
|
|||
_hover={{ bg: 'custom.gray.100' }}
|
||||
bg={'custom.gray.200'}
|
||||
onClick={handleDisconnect}
|
||||
icon={<Icon name="log-out" />}
|
||||
>
|
||||
Disconnect
|
||||
</MenuItem>
|
||||
|
|
@ -72,7 +70,6 @@ const ConnectionMenu: React.FC = () => {
|
|||
<Button
|
||||
borderRadius="50px"
|
||||
as={MenuButton}
|
||||
leftIcon={<Icon name="wallet" />}
|
||||
isLoading={state === 'loading'}
|
||||
disabled={state === 'loading'}
|
||||
>
|
||||
|
|
@ -83,7 +80,6 @@ const ConnectionMenu: React.FC = () => {
|
|||
_hover={{ bg: 'custom.gray.100' }}
|
||||
bg={'custom.gray.200'}
|
||||
onClick={handleConnectWallet}
|
||||
icon={<Icon name={WalletType.metamask} />}
|
||||
>
|
||||
Metamask
|
||||
</MenuItem>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { App } from './app';
|
||||
import { ChakraProvider } from '@chakra-ui/react';
|
||||
import { theme } from './theme';
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
import { store } from './store';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
|
|
@ -19,11 +17,9 @@ const root = ReactDOM.createRoot(
|
|||
root.render(
|
||||
<React.StrictMode>
|
||||
<ReduxProvider store={store}>
|
||||
<ChakraProvider theme={theme} resetCSS>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
</ChakraProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
</ReduxProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { Button } from './Button';
|
||||
|
||||
//TODO: remove example stories
|
||||
|
||||
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
||||
export default {
|
||||
title: 'Example/Button',
|
||||
component: Button,
|
||||
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
|
||||
argTypes: {
|
||||
backgroundColor: { control: 'color' },
|
||||
},
|
||||
} as ComponentMeta<typeof Button>;
|
||||
|
||||
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
|
||||
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||
Primary.args = {
|
||||
primary: true,
|
||||
label: 'Button',
|
||||
};
|
||||
|
||||
export const Secondary = Template.bind({});
|
||||
Secondary.args = {
|
||||
label: 'Button',
|
||||
};
|
||||
|
||||
export const Large = Template.bind({});
|
||||
Large.args = {
|
||||
size: 'large',
|
||||
label: 'Button',
|
||||
};
|
||||
|
||||
export const Small = Template.bind({});
|
||||
Small.args = {
|
||||
size: 'small',
|
||||
label: 'Button',
|
||||
};
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import React from 'react';
|
||||
import './button.css';
|
||||
|
||||
interface ButtonProps {
|
||||
/**
|
||||
* Is this the principal call to action on the page?
|
||||
*/
|
||||
primary?: boolean;
|
||||
/**
|
||||
* What background color to use
|
||||
*/
|
||||
backgroundColor?: string;
|
||||
/**
|
||||
* How large should the button be?
|
||||
*/
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
/**
|
||||
* Button contents
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Optional click handler
|
||||
*/
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary UI component for user interaction
|
||||
*/
|
||||
export const Button = ({
|
||||
primary = false,
|
||||
size = 'medium',
|
||||
backgroundColor,
|
||||
label,
|
||||
...props
|
||||
}: ButtonProps) => {
|
||||
const mode = primary
|
||||
? 'storybook-button--primary'
|
||||
: 'storybook-button--secondary';
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={['storybook-button', `storybook-button--${size}`, mode].join(
|
||||
' '
|
||||
)}
|
||||
style={{ backgroundColor }}
|
||||
{...props}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
.storybook-button {
|
||||
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-weight: 700;
|
||||
border: 0;
|
||||
border-radius: 3em;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
}
|
||||
.storybook-button--primary {
|
||||
color: white;
|
||||
background-color: #1ea7fd;
|
||||
}
|
||||
.storybook-button--secondary {
|
||||
color: #333;
|
||||
background-color: transparent;
|
||||
box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
|
||||
}
|
||||
.storybook-button--small {
|
||||
font-size: 12px;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
.storybook-button--medium {
|
||||
font-size: 14px;
|
||||
padding: 11px 20px;
|
||||
}
|
||||
.storybook-button--large {
|
||||
font-size: 16px;
|
||||
padding: 12px 24px;
|
||||
}
|
||||
|
|
@ -101,6 +101,10 @@ export const createDripStitches = <
|
|||
...zIndices,
|
||||
...(theme?.zIndices || {}),
|
||||
},
|
||||
transitions: {
|
||||
'all-200': 'all 200ms',
|
||||
...(theme?.transitions || {}),
|
||||
},
|
||||
...(theme || {}),
|
||||
},
|
||||
themeMap,
|
||||
|
|
|
|||
10
ui/yarn.lock
10
ui/yarn.lock
|
|
@ -2933,6 +2933,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@radix-ui/colors/-/colors-0.1.8.tgz#b08c62536fc462a87632165fb28e9b18f9bd047e"
|
||||
integrity sha512-jwRMXYwC0hUo0mv6wGpuw254Pd9p/R6Td5xsRpOmaWkUHlooNWqVcadgyzlRumMq3xfOTXwJReU0Jv+EIy4Jbw==
|
||||
|
||||
"@react-icons/all-files@^4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-icons/all-files/-/all-files-4.1.0.tgz#477284873a0821928224b6fc84c62d2534d6650b"
|
||||
integrity sha512-hxBI2UOuVaI3O/BhQfhtb4kcGn9ft12RWAFVMUeNjqqhLsHvFtzIkFaptBJpFDANTKoDfdVoHTKZDlwKCACbMQ==
|
||||
|
||||
"@reduxjs/toolkit@^1.9.1":
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.1.tgz#4c34dc4ddcec161535288c60da5c19c3ef15180e"
|
||||
|
|
@ -10808,11 +10813,6 @@ react-focus-lock@^2.9.1:
|
|||
use-callback-ref "^1.3.0"
|
||||
use-sidecar "^1.1.2"
|
||||
|
||||
react-icons@^4.7.1:
|
||||
version "4.7.1"
|
||||
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.7.1.tgz#0f4b25a5694e6972677cb189d2a72eabea7a8345"
|
||||
integrity sha512-yHd3oKGMgm7zxo3EA7H2n7vxSoiGmHk5t6Ou4bXsfcgWyhfDKMpyKfhHR6Bjnn63c+YXBLBPUql9H4wPJM6sXw==
|
||||
|
||||
react-inspector@^5.1.0:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-5.1.1.tgz#58476c78fde05d5055646ed8ec02030af42953c8"
|
||||
|
|
|
|||
Loading…
Reference in New Issue