From a0e14d9a48b3ed042efdaa8ac8bb503c6343b569 Mon Sep 17 00:00:00 2001 From: dcodes05 Date: Wed, 23 Aug 2023 13:32:47 +0200 Subject: [PATCH] feat: shared components library --- .../oeth/src/components/Swap/BestRoutes.tsx | 33 +++ .../components/Swap/GasPopover.stories.tsx | 36 +++ .../oeth/src/components/Swap/GasPopover.tsx | 199 +++++++++++++++ .../src/components/Swap/RedeemMix.stories.tsx | 94 +++++++ .../oeth/src/components/Swap/RedeemMix.tsx | 191 +++++++++++++++ .../oeth/src/components/Swap/Swap.stories.tsx | 25 ++ libs/defi/oeth/src/components/Swap/Swap.tsx | 170 +++++++++++++ .../oeth/src/components/Swap/SwapInfo.tsx | 44 ++++ .../src/components/Swap/SwapRoute.stories.tsx | 56 +++++ .../oeth/src/components/Swap/SwapRoute.tsx | 112 +++++++++ .../Swap/SwapRouteAccordion.stories.tsx | 33 +++ .../components/Swap/SwapRouteAccordion.tsx | 112 +++++++++ .../Swap/SwapRouteAccordionItem.stories.tsx | 61 +++++ .../Swap/SwapRouteAccordionItem.tsx | 133 ++++++++++ .../components/Swap/SwapRouteCard.stories.tsx | 86 +++++++ .../src/components/Swap/SwapRouteCard.tsx | 196 +++++++++++++++ .../defi/oeth/src/components/Swap/fixtures.ts | 61 +++++ libs/defi/oeth/src/components/Swap/index.tsx | 2 + .../oeth/src/components/Wrap/SwapWrap.tsx | 62 +++++ libs/defi/oeth/src/components/Wrap/index.tsx | 1 + .../defi/oeth/src/components/shared/index.tsx | 2 + libs/defi/oeth/src/views/History.tsx | 16 ++ libs/defi/oeth/src/views/Swap.tsx | 21 ++ libs/defi/oeth/src/views/Wrap.tsx | 55 +++++ libs/defi/oeth/src/views/index.tsx | 3 + .../ousd/src/components/defi-ousd.stories.tsx | 12 - libs/shared/components/.babelrc | 12 + libs/shared/components/.eslintrc.json | 18 ++ libs/shared/components/.storybook/main.ts | 32 +++ libs/shared/components/.storybook/preview.ts | 3 + libs/shared/components/README.md | 7 + libs/shared/components/project.json | 61 +++++ libs/shared/components/src/Cards/Card.tsx | 62 +++++ .../Cards/SwapCard/ActionButton.stories.tsx | 44 ++++ .../src/Cards/SwapCard/ActionButton.tsx | 40 +++ .../src/Cards/SwapCard/Input.stories.tsx | 45 ++++ .../components/src/Cards/SwapCard/Input.tsx | 177 ++++++++++++++ .../src/Cards/SwapCard/Output.stories.tsx | 42 ++++ .../components/src/Cards/SwapCard/Output.tsx | 111 +++++++++ .../src/Cards/SwapCard/SwapButton.stories.tsx | 29 +++ .../src/Cards/SwapCard/SwapButton.tsx | 48 ++++ .../src/Cards/SwapCard/SwapCard.stories.tsx | 81 ++++++ .../src/Cards/SwapCard/SwapCard.tsx | 104 ++++++++ .../src/Cards/SwapCard/SwapItem.stories.tsx | 43 ++++ .../src/Cards/SwapCard/SwapItem.tsx | 74 ++++++ .../src/Cards/SwapCard/SwapItemArrow.tsx | 0 .../Cards/SwapCard/TokenListItem.stories.tsx | 87 +++++++ .../src/Cards/SwapCard/TokenListItem.tsx | 84 +++++++ .../Cards/SwapCard/TokenListModal.stories.tsx | 92 +++++++ .../src/Cards/SwapCard/TokenListModal.tsx | 75 ++++++ .../src/Cards/SwapCard/dropdown.svg | 12 + .../components/src/Cards/SwapCard/index.tsx | 4 + .../components/src/Cards/SwapCard/utils.ts | 8 + libs/shared/components/src/Cards/index.tsx | 2 + .../components/src/LinkIcon/LinkIcon.tsx | 18 ++ .../components/src/Loader/Loader.stories.tsx | 16 ++ libs/shared/components/src/Loader/Loader.tsx | 16 ++ libs/shared/components/src/Loader/index.tsx | 1 + .../shared/components/src/Mix/Mix.stories.tsx | 20 ++ libs/shared/components/src/Mix/Mix.tsx | 37 +++ libs/shared/components/src/Mix/index.tsx | 1 + libs/shared/components/src/index.ts | 3 + .../src/top-nav/Activity.stories.tsx | 98 ++++++++ .../components/src/top-nav/Activity.tsx | 111 +++++++++ .../src/top-nav/ConnectedButton.stories.tsx | 93 +++++++ .../src/top-nav/ConnectedButton.tsx | 214 ++++++++++++++++ libs/shared/components/src/top-nav/Icon.tsx | 18 ++ .../components/src/top-nav/TopNav.stories.tsx | 161 ++++++++++++ libs/shared/components/src/top-nav/TopNav.tsx | 230 ++++++++++++++++++ .../src/top-nav/Transaction.stories.tsx | 92 +++++++ .../components/src/top-nav/Transaction.tsx | 127 ++++++++++ libs/shared/components/src/top-nav/UserId.tsx | 16 ++ .../shared/components/src/top-nav/fixtures.ts | 115 +++++++++ libs/shared/components/src/top-nav/index.tsx | 2 + libs/shared/components/src/top-nav/types.ts | 45 ++++ libs/shared/components/src/top-nav/utils.ts | 41 ++++ libs/shared/components/tsconfig.json | 24 ++ libs/shared/components/tsconfig.lib.json | 26 ++ libs/shared/components/tsconfig.spec.json | 19 ++ .../shared/components/tsconfig.storybook.json | 31 +++ libs/shared/components/vite.config.ts | 36 +++ libs/shared/storybook/.storybook/main.ts | 7 +- .../storybook/.storybook/preview-head.html | 8 + libs/shared/storybook/src/decorators.tsx | 30 ++- libs/shared/storybook/vite.config.ts | 5 +- libs/shared/theme/src/Palette.stories.tsx | 195 +++++++++++++++ libs/shared/theme/src/theme.tsx | 229 ++++++++++++++--- tsconfig.base.json | 1 + 88 files changed, 5238 insertions(+), 61 deletions(-) create mode 100644 libs/defi/oeth/src/components/Swap/BestRoutes.tsx create mode 100644 libs/defi/oeth/src/components/Swap/GasPopover.stories.tsx create mode 100644 libs/defi/oeth/src/components/Swap/GasPopover.tsx create mode 100644 libs/defi/oeth/src/components/Swap/RedeemMix.stories.tsx create mode 100644 libs/defi/oeth/src/components/Swap/RedeemMix.tsx create mode 100644 libs/defi/oeth/src/components/Swap/Swap.stories.tsx create mode 100644 libs/defi/oeth/src/components/Swap/Swap.tsx create mode 100644 libs/defi/oeth/src/components/Swap/SwapInfo.tsx create mode 100644 libs/defi/oeth/src/components/Swap/SwapRoute.stories.tsx create mode 100644 libs/defi/oeth/src/components/Swap/SwapRoute.tsx create mode 100644 libs/defi/oeth/src/components/Swap/SwapRouteAccordion.stories.tsx create mode 100644 libs/defi/oeth/src/components/Swap/SwapRouteAccordion.tsx create mode 100644 libs/defi/oeth/src/components/Swap/SwapRouteAccordionItem.stories.tsx create mode 100644 libs/defi/oeth/src/components/Swap/SwapRouteAccordionItem.tsx create mode 100644 libs/defi/oeth/src/components/Swap/SwapRouteCard.stories.tsx create mode 100644 libs/defi/oeth/src/components/Swap/SwapRouteCard.tsx create mode 100644 libs/defi/oeth/src/components/Swap/fixtures.ts create mode 100644 libs/defi/oeth/src/components/Swap/index.tsx create mode 100644 libs/defi/oeth/src/components/Wrap/SwapWrap.tsx create mode 100644 libs/defi/oeth/src/components/Wrap/index.tsx create mode 100644 libs/defi/oeth/src/components/shared/index.tsx create mode 100644 libs/defi/oeth/src/views/History.tsx create mode 100644 libs/defi/oeth/src/views/Swap.tsx create mode 100644 libs/defi/oeth/src/views/Wrap.tsx create mode 100644 libs/defi/oeth/src/views/index.tsx delete mode 100644 libs/defi/ousd/src/components/defi-ousd.stories.tsx create mode 100644 libs/shared/components/.babelrc create mode 100644 libs/shared/components/.eslintrc.json create mode 100644 libs/shared/components/.storybook/main.ts create mode 100644 libs/shared/components/.storybook/preview.ts create mode 100644 libs/shared/components/README.md create mode 100644 libs/shared/components/project.json create mode 100644 libs/shared/components/src/Cards/Card.tsx create mode 100644 libs/shared/components/src/Cards/SwapCard/ActionButton.stories.tsx create mode 100644 libs/shared/components/src/Cards/SwapCard/ActionButton.tsx create mode 100644 libs/shared/components/src/Cards/SwapCard/Input.stories.tsx create mode 100644 libs/shared/components/src/Cards/SwapCard/Input.tsx create mode 100644 libs/shared/components/src/Cards/SwapCard/Output.stories.tsx create mode 100644 libs/shared/components/src/Cards/SwapCard/Output.tsx create mode 100644 libs/shared/components/src/Cards/SwapCard/SwapButton.stories.tsx create mode 100644 libs/shared/components/src/Cards/SwapCard/SwapButton.tsx create mode 100644 libs/shared/components/src/Cards/SwapCard/SwapCard.stories.tsx create mode 100644 libs/shared/components/src/Cards/SwapCard/SwapCard.tsx create mode 100644 libs/shared/components/src/Cards/SwapCard/SwapItem.stories.tsx create mode 100644 libs/shared/components/src/Cards/SwapCard/SwapItem.tsx create mode 100644 libs/shared/components/src/Cards/SwapCard/SwapItemArrow.tsx create mode 100644 libs/shared/components/src/Cards/SwapCard/TokenListItem.stories.tsx create mode 100644 libs/shared/components/src/Cards/SwapCard/TokenListItem.tsx create mode 100644 libs/shared/components/src/Cards/SwapCard/TokenListModal.stories.tsx create mode 100644 libs/shared/components/src/Cards/SwapCard/TokenListModal.tsx create mode 100644 libs/shared/components/src/Cards/SwapCard/dropdown.svg create mode 100644 libs/shared/components/src/Cards/SwapCard/index.tsx create mode 100644 libs/shared/components/src/Cards/SwapCard/utils.ts create mode 100644 libs/shared/components/src/Cards/index.tsx create mode 100644 libs/shared/components/src/LinkIcon/LinkIcon.tsx create mode 100644 libs/shared/components/src/Loader/Loader.stories.tsx create mode 100644 libs/shared/components/src/Loader/Loader.tsx create mode 100644 libs/shared/components/src/Loader/index.tsx create mode 100644 libs/shared/components/src/Mix/Mix.stories.tsx create mode 100644 libs/shared/components/src/Mix/Mix.tsx create mode 100644 libs/shared/components/src/Mix/index.tsx create mode 100644 libs/shared/components/src/index.ts create mode 100644 libs/shared/components/src/top-nav/Activity.stories.tsx create mode 100644 libs/shared/components/src/top-nav/Activity.tsx create mode 100644 libs/shared/components/src/top-nav/ConnectedButton.stories.tsx create mode 100644 libs/shared/components/src/top-nav/ConnectedButton.tsx create mode 100644 libs/shared/components/src/top-nav/Icon.tsx create mode 100644 libs/shared/components/src/top-nav/TopNav.stories.tsx create mode 100644 libs/shared/components/src/top-nav/TopNav.tsx create mode 100644 libs/shared/components/src/top-nav/Transaction.stories.tsx create mode 100644 libs/shared/components/src/top-nav/Transaction.tsx create mode 100644 libs/shared/components/src/top-nav/UserId.tsx create mode 100644 libs/shared/components/src/top-nav/fixtures.ts create mode 100644 libs/shared/components/src/top-nav/index.tsx create mode 100644 libs/shared/components/src/top-nav/types.ts create mode 100644 libs/shared/components/src/top-nav/utils.ts create mode 100644 libs/shared/components/tsconfig.json create mode 100644 libs/shared/components/tsconfig.lib.json create mode 100644 libs/shared/components/tsconfig.spec.json create mode 100644 libs/shared/components/tsconfig.storybook.json create mode 100644 libs/shared/components/vite.config.ts create mode 100644 libs/shared/storybook/.storybook/preview-head.html create mode 100644 libs/shared/theme/src/Palette.stories.tsx diff --git a/libs/defi/oeth/src/components/Swap/BestRoutes.tsx b/libs/defi/oeth/src/components/Swap/BestRoutes.tsx new file mode 100644 index 000000000..2484e3889 --- /dev/null +++ b/libs/defi/oeth/src/components/Swap/BestRoutes.tsx @@ -0,0 +1,33 @@ +import { Box } from '@mui/material'; + +import { SwapRouteCard } from './SwapRouteCard'; + +import type { Route } from './SwapRoute'; + +interface Props { + routes: Route[]; + selected: number; + onSelect: (index: number) => void; +} + +export function BestRoutes({ routes, selected, onSelect }: Props) { + return ( + + {routes.slice(0, 2).map((route, index) => ( + onSelect(index)} + route={route} + /> + ))} + + ); +} diff --git a/libs/defi/oeth/src/components/Swap/GasPopover.stories.tsx b/libs/defi/oeth/src/components/Swap/GasPopover.stories.tsx new file mode 100644 index 000000000..13b9340f1 --- /dev/null +++ b/libs/defi/oeth/src/components/Swap/GasPopover.stories.tsx @@ -0,0 +1,36 @@ +import { screen, userEvent, within } from '@storybook/testing-library'; + +import { GasPopover } from './GasPopover'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + component: GasPopover, + title: 'Swap/GasPopover', + args: { + gasPrice: 21, + onPriceToleranceChange: (val) => null, + }, +}; + +export default meta; + +export const Default: StoryObj = {}; + +export const Expanded: StoryObj = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.getByTestId('gas-popover-button').click(); + }, +}; + +export const HighTolerance: StoryObj = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await canvas.getByTestId('gas-popover-button').click(); + const input = await screen.findByLabelText('Price tolerance'); + await userEvent.clear(input); + await userEvent.type(input, '1.2'); + }, +}; diff --git a/libs/defi/oeth/src/components/Swap/GasPopover.tsx b/libs/defi/oeth/src/components/Swap/GasPopover.tsx new file mode 100644 index 000000000..6df2f071f --- /dev/null +++ b/libs/defi/oeth/src/components/Swap/GasPopover.tsx @@ -0,0 +1,199 @@ +import { useEffect, useState } from 'react'; + +import { + alpha, + Box, + Button, + debounce, + FormControl, + FormHelperText, + IconButton, + InputAdornment, + InputBase, + InputLabel, + Popover, + Stack, + useTheme, +} from '@mui/material'; +import { isNumber } from 'lodash'; +import { useIntl } from 'react-intl'; + +import type { Theme } from '@mui/material'; + +const defaultPriceTolerance = 0.01; + +const gridStyles = { + display: 'grid', + gridTemplateColumns: (theme: Theme) => `1.5fr 1fr`, + gap: 1, + justifyContent: 'space-between', + alignItems: 'center', +}; + +interface Props { + gasPrice: number; + onPriceToleranceChange: (value: number) => void; +} + +export function GasPopover({ gasPrice, onPriceToleranceChange }: Props) { + const theme = useTheme(); + const intl = useIntl(); + const [anchorEl, setAnchorEl] = useState(null); + const [priceTolerance, setPriceTolerance] = useState(defaultPriceTolerance); + + useEffect(() => { + onPriceToleranceChange(priceTolerance); + }, [priceTolerance, onPriceToleranceChange]); + return ( + <> + setAnchorEl(e.currentTarget)} + data-testid="gas-popover-button" + > + + + setAnchorEl(null)} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'center', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + sx={{ + '& .MuiPaper-root.MuiPopover-paper': { + padding: 3, + boxSizing: 'border-box', + maxWidth: { + xs: '90vw', + md: '16.5rem', + }, + width: '100%', + border: '1px solid', + borderColor: 'grey.700', + [theme.breakpoints.down('md')]: { + left: '0 !important', + right: 0, + marginInline: 'auto', + }, + }, + }} + > + + + + {intl.formatMessage({ defaultMessage: 'Price tolerance' })} + + + { + if (isNumber(parseFloat(e.target.value))) { + setPriceTolerance(e.target.value); + } + }, 300)} + endAdornment={ + + {intl.formatMessage({ defaultMessage: '%' })} + + } + /> + + + + {priceTolerance > 1 ? ( + theme.typography.pxToRem(12), + color: (theme) => theme.palette.warning.main, + fontWeight: 400, + fontStyle: 'normal', + }} + > + {intl.formatMessage({ + defaultMessage: 'Your transaction may be frontrun', + })} + + ) : null} + + + + {intl.formatMessage({ defaultMessage: 'Gas Price' })} + + + + {intl.formatMessage({ defaultMessage: 'GWEI' })} + + } + /> + + + + + + ); +} diff --git a/libs/defi/oeth/src/components/Swap/RedeemMix.stories.tsx b/libs/defi/oeth/src/components/Swap/RedeemMix.stories.tsx new file mode 100644 index 000000000..2ac0534d4 --- /dev/null +++ b/libs/defi/oeth/src/components/Swap/RedeemMix.stories.tsx @@ -0,0 +1,94 @@ +import { Box } from '@mui/material'; + +import { RedeemMix } from './RedeemMix'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + component: RedeemMix, + title: 'Swap/Redeem Mix', + args: { + selected: 4, + index: 2, + route: { + value: 282967.55, + quantity: 149.55, + name: 'Redeem for mix via OETH vault', + waitTime: '1 min', + transactionCost: 135.83, + rate: 0.995, + type: 'redeem', + tokenAbbreviation: '', + icon: [ + 'https://app.oeth.com/images/currency/weth-icon-small.png', + 'https://app.oeth.com/images/currency/reth-icon-small.png', + 'https://app.oeth.com/images/currency/steth-icon-small.svg', + 'https://app.oeth.com/images/currency/frxeth-icon-small.svg', + ], + }, + composition: [ + { + name: 'wETH', + quantity: 117.0437, + value: 238378.36, + icon: 'https://app.oeth.com/images/currency/weth-icon-small.png', + }, + { + name: 'frxETH', + quantity: 13.1245, + value: 17643.75, + icon: 'https://app.oeth.com/images/currency/frxeth-icon-small.svg', + }, + { + name: 'rETH', + quantity: 13.1144, + value: 13138.96, + icon: 'https://app.oeth.com/images/currency/reth-icon-small.png', + }, + { + name: 'sETH', + quantity: 4.8354, + value: 13138.96, + icon: 'https://app.oeth.com/images/currency/steth-icon-small.svg', + }, + ], + }, + render: (args) => ( + + + + ), +}; + +export default meta; + +export const Default: StoryObj = {}; + +export const Hover: StoryObj = { + parameters: { + pseudo: { + hover: true, + }, + }, +}; +export const Selected: StoryObj = { + args: { + index: 4, + }, +}; + +export const SmallMobile: StoryObj = { + parameters: { + viewport: { + defaultViewport: 'mobile1', + }, + }, +}; + +export const LargeMobile: StoryObj = { + parameters: { + viewport: { + defaultViewport: 'mobile2', + }, + }, +}; diff --git a/libs/defi/oeth/src/components/Swap/RedeemMix.tsx b/libs/defi/oeth/src/components/Swap/RedeemMix.tsx new file mode 100644 index 000000000..eaa64da7e --- /dev/null +++ b/libs/defi/oeth/src/components/Swap/RedeemMix.tsx @@ -0,0 +1,191 @@ +import { Stack, Card, alpha, Box, Typography, Divider } from '@mui/material'; +import { Mix } from 'libs/shared/components/src/Mix'; +import { Redeem, Route } from './SwapRoute'; +import { useIntl } from 'react-intl'; +import { currencyFormat, quantityFormat } from '@origin/shared/components'; +import { SwapInfo } from './SwapInfo'; +import { Icon } from 'libs/shared/components/src/top-nav/Icon'; + +interface Props { + onSelect: (index: number) => void; + index: number; + selected: number; + route: Route; + // no idea if this prop makes sense 🤪 -> prob it needs to be refactored + composition: { + icon: string; + name: string; + quantity: number; + value: number; + }[]; +} + +export function RedeemMix({ + route, + index, + selected, + onSelect, + composition, +}: Props) { + const intl = useIntl(); + return ( + `linear-gradient(var(--mui-palette-grey-800), var(--mui-palette-grey-800)) padding-box, + linear-gradient(90deg, ${alpha( + theme.palette.primary.main, + 0.4, + )} 0%, ${alpha( + theme.palette.primary.dark, + 0.4, + )} 100%) border-box;`, + }, + }), + }} + role="button" + onClick={() => onSelect(index)} + > + + + + + + {intl.formatNumber(route.quantity, quantityFormat)}  + theme.typography.pxToRem(12), + fontWeight: 400, + fontStyle: 'normal', + lineHeight: (theme) => theme.typography.pxToRem(20), + }} + > + ({intl.formatNumber(route.value, currencyFormat)}) + + + + {route.name} + + + + + + {intl.formatMessage({ defaultMessage: 'Gas:' })}  + + ~{intl.formatNumber(route.transactionCost, currencyFormat)} + + + + {intl.formatMessage({ defaultMessage: 'Waiting time:' })}  + + {/* TODO better handling of this duration */}~ + {(route as Redeem).waitTime} + + + + + + + {intl.formatMessage({ defaultMessage: 'Rate' })}  + + 1:{route.rate} + +   + + + + + {composition.map((item) => ( + + + + + {item.name} + + + + + {intl.formatNumber(item.quantity, quantityFormat)} + + + {intl.formatNumber(item.value, currencyFormat)} + + + + ))} + + ); +} diff --git a/libs/defi/oeth/src/components/Swap/Swap.stories.tsx b/libs/defi/oeth/src/components/Swap/Swap.stories.tsx new file mode 100644 index 000000000..3ab1e1c72 --- /dev/null +++ b/libs/defi/oeth/src/components/Swap/Swap.stories.tsx @@ -0,0 +1,25 @@ +import { Container } from '@mui/material'; + +import { Swap } from './Swap'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + component: Swap, + title: 'Swap', + args: { + isLoading: false, + routes: [], + }, + render: () => ( + + + + ), +}; + +export default meta; + +export const SwapComponent: StoryObj = { + name: 'Swap Component', +}; diff --git a/libs/defi/oeth/src/components/Swap/Swap.tsx b/libs/defi/oeth/src/components/Swap/Swap.tsx new file mode 100644 index 000000000..5d1b6583d --- /dev/null +++ b/libs/defi/oeth/src/components/Swap/Swap.tsx @@ -0,0 +1,170 @@ +import { useState } from 'react'; + +import { Stack } from '@mui/material'; +import { + ActionButton, + DropdownIcon, + SwapCard, + TokenListModal, +} from '@origin/shared/components'; +import random from 'lodash/random'; +import { useIntl } from 'react-intl'; + +import { GasPopover } from './GasPopover'; +import { SwapRoute } from './SwapRoute'; + +import type { Option } from '@origin/shared/components'; + +export function Swap() { + const intl = useIntl(); + + const [isSelectionModalOpen, setSelectionModal] = useState(false); + const [values, setValues] = useState<{ + baseToken: Omit; + exchangeCurrency: Omit; + }>({ + baseToken: { + abbreviation: 'OETH', + imgSrc: 'https://app.oeth.com/images/currency/oeth-icon-small.svg', + quantity: 0, + value: 0, + }, + exchangeCurrency: { + abbreviation: 'ETH', + imgSrc: 'https://app.oeth.com/images/currency/eth-icon-small.svg', + quantity: 0, + value: 0, + }, + }); + + function handleCloseSelectionModal() { + setSelectionModal(false); + } + + function handleValueChange(value: string) { + const number = parseInt(value) || 0; + setValues((prev) => ({ + baseToken: { + ...prev.baseToken, + quantity: number, + value: number * random(18500, 19000, true), + }, + exchangeCurrency: { + ...prev.exchangeCurrency, + quantity: number * random(number - 0.5, number, true), + value: number * random(18500, 19000, true), + }, + })); + } + + function swapTokens() { + setValues((prev) => ({ + baseToken: { + ...prev.exchangeCurrency, + quantity: prev.baseToken.quantity, + value: prev.baseToken.quantity * random(18500, 19000), + }, + exchangeCurrency: { + ...prev.baseToken, + quantity: + prev.baseToken.quantity * + random(prev.baseToken.quantity - 0.5, prev.baseToken.quantity, true), + value: prev.baseToken.quantity * random(18500, 19000, true), + }, + })); + } + + return ( + <> + + {intl.formatMessage({ defaultMessage: 'Swap' })} + null} + /> + + } + onSwap={swapTokens} + onValueChange={handleValueChange} + baseTokenIcon={values.baseToken.imgSrc} + baseTokenName={values.baseToken.abbreviation as string} + baseTokenValue={values.baseToken.value} + exchangeTokenName={values.exchangeCurrency.abbreviation as string} + exchangeTokenIcon={values.exchangeCurrency.imgSrc} + exchangeTokenQuantity={values.exchangeCurrency.quantity} + exchangeTokenValue={values.exchangeCurrency.value} + exchangeTokenNode={ + setSelectionModal(true)} /> + } + > + + console.log('test')}>Swap + + + setValues((prev) => ({ + ...prev, + exchangeCurrency: { + value: prev.baseToken.quantity * random(18500, 19000, true), + quantity: random( + prev.baseToken.quantity - 0.5, + prev.baseToken.quantity, + ), + abbreviation: option.name, + imgSrc: option.imgSrc, + }, + })) + } + selected={values.exchangeCurrency.abbreviation} + options={[ + { + name: intl.formatMessage({ defaultMessage: 'Wrapped Ether' }), + abbreviation: intl.formatMessage({ defaultMessage: 'WETH' }), + imgSrc: 'https://app.oeth.com/images/currency/weth-icon-small.png', + value: 0, + quantity: 0, + }, + { + name: intl.formatMessage({ + defaultMessage: 'Liquid Staked Ether 2.0', + }), + abbreviation: intl.formatMessage({ defaultMessage: 'stETH' }), + imgSrc: 'https://app.oeth.com/images/currency/steth-icon-small.svg', + value: 0, + quantity: 0, + }, + { + name: intl.formatMessage({ defaultMessage: 'Rocket Pool ETH' }), + abbreviation: intl.formatMessage({ defaultMessage: 'rETH' }), + imgSrc: 'https://app.oeth.com/images/currency/reth-icon-small.png', + value: 0, + quantity: 0, + }, + { + name: intl.formatMessage({ defaultMessage: 'Frax Ether' }), + abbreviation: intl.formatMessage({ defaultMessage: 'frxETH' }), + imgSrc: + 'https://app.oeth.com/images/currency/frxeth-icon-small.svg', + value: 0, + quantity: 0, + }, + { + name: intl.formatMessage({ defaultMessage: 'ETH' }), + abbreviation: intl.formatMessage({ defaultMessage: 'ETH' }), + imgSrc: 'https://app.oeth.com/images/currency/eth-icon-small.svg', + value: 0, + quantity: 0, + }, + ]} + /> + + ); +} diff --git a/libs/defi/oeth/src/components/Swap/SwapInfo.tsx b/libs/defi/oeth/src/components/Swap/SwapInfo.tsx new file mode 100644 index 000000000..fd081c28e --- /dev/null +++ b/libs/defi/oeth/src/components/Swap/SwapInfo.tsx @@ -0,0 +1,44 @@ +import { Box, Tooltip, Typography } from '@mui/material'; +import { useIntl } from 'react-intl'; + +import type { Theme } from '@mui/material'; + +export function SwapInfo() { + const intl = useIntl(); + return ( + + {intl.formatMessage({ + defaultMessage: + 'The best swap route factors in the best price after transaction costs', + })} + + } + componentsProps={{ + tooltip: { + // @ts-expect-error type error in MUI + sx: { + paddingInline: 2, + paddingBlock: 1.5, + borderRadius: 2, + border: '1px solid', + borderColor: (theme) => theme.palette.grey[500], + boxShadow: (theme: Theme) => theme.shadows[23], + }, + }, + }} + > + theme.typography.pxToRem(12), + height: (theme) => theme.typography.pxToRem(12), + color: (theme) => theme.palette.text.secondary, + }} + > + + ); +} diff --git a/libs/defi/oeth/src/components/Swap/SwapRoute.stories.tsx b/libs/defi/oeth/src/components/Swap/SwapRoute.stories.tsx new file mode 100644 index 000000000..bb17d7f2d --- /dev/null +++ b/libs/defi/oeth/src/components/Swap/SwapRoute.stories.tsx @@ -0,0 +1,56 @@ +import { Container } from '@mui/material'; +import { userEvent, within } from '@storybook/testing-library'; + +import { routes } from './fixtures'; +import { SwapRoute } from './SwapRoute'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + component: SwapRoute, + title: 'Swap/Swap Route', + args: { + isLoading: false, + routes: [], + }, + parameters: { + backgrounds: { + default: 'dark', + values: [ + { + name: 'dark', + value: '#282A32', + }, + ], + }, + }, + render: (args) => ( + + + + ), +}; + +export default meta; + +export const Default: StoryObj = {}; + +export const HoverRouteInfo: StoryObj = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const element = await canvas.findByTestId('swap-route-info'); + userEvent.hover(element); + }, +}; +export const Loading: StoryObj = { + args: { + isLoading: true, + }, +}; + +export const SwapContent: StoryObj = { + args: { + // @ts-expect-error fixtures and type mismatch + routes, + }, +}; diff --git a/libs/defi/oeth/src/components/Swap/SwapRoute.tsx b/libs/defi/oeth/src/components/Swap/SwapRoute.tsx new file mode 100644 index 000000000..853e896b7 --- /dev/null +++ b/libs/defi/oeth/src/components/Swap/SwapRoute.tsx @@ -0,0 +1,112 @@ +import { useState } from 'react'; + +import { Skeleton, Stack, Typography } from '@mui/material'; +import { Card, cardStyles } from '@origin/shared/components'; +import { useIntl } from 'react-intl'; + +import { BestRoutes } from './BestRoutes'; +import { SwapInfo } from './SwapInfo'; +import { SwapRouteAccordion } from './SwapRouteAccordion'; + +interface Swap { + type: 'swap'; +} +export interface Redeem { + type: 'redeem'; + tokenAbbreviation: string; + waitTime: string; +} + +export type Route = { + name: string; + icon: string | string[]; + quantity: number; + value: number; + rate: number; + transactionCost: number; +} & (Swap | Redeem); + +interface Props { + isLoading: boolean; + routes: Route[]; +} + +export function SwapRoute({ isLoading = false, routes }: Props) { + const intl = useIntl(); + const [selectedRoute, setSelectedRoute] = useState(0); + + const hasContent = routes.length > 0; + return ( + theme.palette.background.default, + backgroundColor: 'grey.900', + }} + title={ + isLoading ? ( + theme.typography.pxToRem(12), + display: 'flex', + alignItems: 'center', + }} + > + theme.palette.primary.contrastText, + }} + /> + {intl.formatMessage({ + defaultMessage: 'Finding the best route...', + })} + + ) : ( + + {intl.formatMessage({ defaultMessage: 'Route' })} + + + ) + } + sxCardTitle={{ borderBottom: 'none', paddingBlock: 1, paddingInline: 2 }} + sxCardContent={{ + ...(hasContent + ? cardStyles + : { + p: 0, + paddingBlock: 0, + paddingInline: 0, + '&:last-child': { pb: 0 }, + }), + }} + > + {hasContent ? ( + <> + setSelectedRoute(index)} + /> + setSelectedRoute(index)} + sx={{ mt: 2 }} + /> + + ) : undefined} + + ); +} diff --git a/libs/defi/oeth/src/components/Swap/SwapRouteAccordion.stories.tsx b/libs/defi/oeth/src/components/Swap/SwapRouteAccordion.stories.tsx new file mode 100644 index 000000000..b17007c2c --- /dev/null +++ b/libs/defi/oeth/src/components/Swap/SwapRouteAccordion.stories.tsx @@ -0,0 +1,33 @@ +import { Box } from '@mui/material'; +import { within } from '@storybook/testing-library'; + +import { routes } from './fixtures'; +import { SwapRouteAccordion } from './SwapRouteAccordion'; + +import type { Meta, StoryObj } from '@storybook/react'; + +import type { Route } from './SwapRoute'; + +const meta: Meta = { + component: SwapRouteAccordion, + title: 'Swap/Swap Route Accordion', + args: { + selected: 4, + routes: routes as Route[], + }, + render: (args) => ( + + + + ), +}; + +export default meta; + +export const Default: StoryObj = {}; +export const Expanded: StoryObj = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + (await canvas.findByText('Show more')).click(); + }, +}; diff --git a/libs/defi/oeth/src/components/Swap/SwapRouteAccordion.tsx b/libs/defi/oeth/src/components/Swap/SwapRouteAccordion.tsx new file mode 100644 index 000000000..96d6816cc --- /dev/null +++ b/libs/defi/oeth/src/components/Swap/SwapRouteAccordion.tsx @@ -0,0 +1,112 @@ +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + Stack, + Typography, +} from '@mui/material'; +import { useIntl } from 'react-intl'; + +import { SwapRouteAccordionItem } from './SwapRouteAccordionItem'; + +import type { SxProps } from '@mui/material'; + +import type { Route } from './SwapRoute'; + +interface Props { + routes: Route[]; + selected: number; + onSelect: (index: number) => void; + sx?: SxProps; +} + +export function SwapRouteAccordion({ routes, selected, onSelect, sx }: Props) { + const intl = useIntl(); + return ( + + theme.typography.pxToRem(14), + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + '& .MuiAccordionSummary-content': { + margin: 0, + }, + padding: 0, + '&.Mui-expanded': { + minHeight: 0, + '& img': { + transform: 'rotate(-180deg)', + }, + }, + }} + > + + {intl.formatMessage({ defaultMessage: 'Show more' })} + + + theme.typography.pxToRem(12), + width: (theme) => theme.typography.pxToRem(12), + alignSelf: 'center', + }} + > + + theme.spacing(-1), + '&:before': { + content: '""', + width: (theme) => `calc(100% + ${theme.spacing(2)})`, + position: 'absolute', + left: (theme) => theme.spacing(-1), + height: '1px', + borderBlockEnd: '1px solid', + display: 'block', + borderColor: 'grey.800', + }, + }} + > + + {routes.slice(2).map((route, index) => ( + + ))} + + + + ); +} diff --git a/libs/defi/oeth/src/components/Swap/SwapRouteAccordionItem.stories.tsx b/libs/defi/oeth/src/components/Swap/SwapRouteAccordionItem.stories.tsx new file mode 100644 index 000000000..b8cd2eb3b --- /dev/null +++ b/libs/defi/oeth/src/components/Swap/SwapRouteAccordionItem.stories.tsx @@ -0,0 +1,61 @@ +import { Box } from '@mui/material'; + +import { routes } from './fixtures'; +import { SwapRouteAccordionItem } from './SwapRouteAccordionItem'; + +import type { Meta, StoryObj } from '@storybook/react'; + +import type { Route } from './SwapRoute'; + +const meta: Meta = { + component: SwapRouteAccordionItem, + title: 'Swap/Swap Route Accordion Item', + args: { + selected: 4, + index: 2, + route: routes[2] as Route, + }, + render: (args) => ( + + + + ), +}; + +export default meta; + +export const Default: StoryObj = {}; +export const Hover: StoryObj = { + parameters: { + pseudo: { + hover: true, + }, + }, +}; +export const Selected: StoryObj = { + args: { + selected: 2, + }, +}; + +export const SmallMobile: StoryObj = { + args: { + selected: 2, + }, + parameters: { + viewport: { + defaultViewport: 'mobile1', + }, + }, +}; + +export const LargeMobile: StoryObj = { + args: { + selected: 2, + }, + parameters: { + viewport: { + defaultViewport: 'mobile2', + }, + }, +}; diff --git a/libs/defi/oeth/src/components/Swap/SwapRouteAccordionItem.tsx b/libs/defi/oeth/src/components/Swap/SwapRouteAccordionItem.tsx new file mode 100644 index 000000000..2ee3f1c41 --- /dev/null +++ b/libs/defi/oeth/src/components/Swap/SwapRouteAccordionItem.tsx @@ -0,0 +1,133 @@ +import { alpha, Box, Stack, Typography, useTheme } from '@mui/material'; +import { currencyFormat, quantityFormat } from '@origin/shared/components'; +import { useIntl } from 'react-intl'; + +import { SwapInfo } from './SwapInfo'; + +import type { Route } from './SwapRoute'; + +interface Props { + route: Route; + selected: number; + index: number; + onSelect: (index: number) => void; +} + +export function SwapRouteAccordionItem({ + route, + selected, + index, + onSelect, +}: Props) { + const theme = useTheme(); + const intl = useIntl(); + return ( + + `linear-gradient(${theme.palette.grey[800]}, ${ + theme.palette.grey[800] + }) padding-box, + linear-gradient(90deg, ${alpha( + theme.palette.primary.main, + 0.4, + )} 0%, ${alpha( + theme.palette.primary.dark, + 0.4, + )} 100%) border-box;`, + }, + ...(selected === index + ? { + borderColor: 'transparent', + background: (theme) => + `linear-gradient(${theme.palette.grey[800]}, ${theme.palette.grey[800]}) padding-box, + linear-gradient(90deg, ${theme.palette.primary.main} 0%, ${theme.palette.primary.dark} 100%) border-box;`, + } + : {}), + }} + onClick={() => onSelect(index)} + role="button" + > + + + theme.typography.pxToRem(24), + width: (theme) => theme.typography.pxToRem(24), + }} + /> + + + {intl.formatNumber(route.quantity, quantityFormat)} +   + + ({intl.formatNumber(route.value, currencyFormat)}) + + + + {route.type === 'swap' + ? intl.formatMessage( + { defaultMessage: 'Swap via {name}' }, + { name: route.name }, + ) + : intl.formatMessage( + { defaultMessage: 'Swap for {name}' }, + { name: route.name }, + )} + + + + + + Rate  + +   + + 1:{intl.formatNumber(route.rate, quantityFormat)} + + + + + Est gas:  + + ~{intl.formatNumber(route.transactionCost, currencyFormat)} + + + + + + ); +} diff --git a/libs/defi/oeth/src/components/Swap/SwapRouteCard.stories.tsx b/libs/defi/oeth/src/components/Swap/SwapRouteCard.stories.tsx new file mode 100644 index 000000000..67daa867e --- /dev/null +++ b/libs/defi/oeth/src/components/Swap/SwapRouteCard.stories.tsx @@ -0,0 +1,86 @@ +import { Box } from '@mui/material'; + +import { redeemRoutes, routes } from './fixtures'; +import { SwapRouteCard } from './SwapRouteCard'; + +import type { Meta, StoryObj } from '@storybook/react'; + +import type { Route } from './SwapRoute'; + +const meta: Meta = { + component: SwapRouteCard, + title: 'Swap/SwapRouteCard', + args: { + route: routes[0] as Route, + selected: 3, + index: 4, + }, + render: (args) => ( + + + + ), +}; + +export default meta; + +export const SwapComponent: StoryObj = {}; + +export const Hover: StoryObj = { + parameters: { + pseudo: { + hover: true, + }, + }, +}; +export const Selected: StoryObj = { + args: { + selected: 4, + }, +}; + +export const Best: StoryObj = { + args: { + selected: 0, + index: 0, + }, +}; + +export const RedeemShort: StoryObj = { + args: { + selected: 0, + index: 0, + route: redeemRoutes[1] as Route, + }, +}; +export const RedeemLong: StoryObj = { + args: { + selected: 0, + index: 0, + route: redeemRoutes[0] as Route, + }, +}; + +export const SmallMobile: StoryObj = { + args: { + selected: 0, + index: 0, + }, + parameters: { + viewport: { + defaultViewport: 'mobile1', + }, + }, +}; + +export const LargeMobile: StoryObj = { + args: { + selected: 0, + index: 0, + }, + parameters: { + viewport: { + defaultViewport: 'mobile2', + }, + }, +}; diff --git a/libs/defi/oeth/src/components/Swap/SwapRouteCard.tsx b/libs/defi/oeth/src/components/Swap/SwapRouteCard.tsx new file mode 100644 index 000000000..e5f5ab6e4 --- /dev/null +++ b/libs/defi/oeth/src/components/Swap/SwapRouteCard.tsx @@ -0,0 +1,196 @@ +import { alpha, Box, Card, CardHeader, Stack, Typography } from '@mui/material'; +import { currencyFormat } from '@origin/shared/components'; +import { useIntl } from 'react-intl'; + +import type { Route } from './SwapRoute'; + +interface Props { + index: number; + selected: number; + onSelect: (index: number) => void; + route: Route; +} + +export function SwapRouteCard({ index, selected, onSelect, route }: Props) { + const intl = useIntl(); + return ( + theme.palette.grey[800], + ...(selected === index + ? { + background: `linear-gradient(var(--mui-palette-grey-800), var(--mui-palette-grey-800)) padding-box, + linear-gradient(90deg, var(--mui-palette-primary-main) 0%, var(--mui-palette-primary-dark) 100%) border-box;`, + borderColor: 'transparent', + } + : { + '&:hover': { + borderColor: 'transparent', + background: ( + theme, + ) => `linear-gradient(var(--mui-palette-grey-800), var(--mui-palette-grey-800)) padding-box, + linear-gradient(90deg, ${alpha( + theme.palette.primary.main, + 0.4, + )} 0%, ${alpha( + theme.palette.primary.dark, + 0.4, + )} 100%) border-box;`, + }, + }), + }} + role="button" + onClick={() => onSelect(index)} + > + + + + + {route.quantity}  + + ({intl.formatNumber(route.value, currencyFormat)}) + + + + {index === 0 ? ( + theme.shape.borderRadius, + background: (theme) => theme.palette.background.gradient1, + color: 'primary.contrastText', + fontSize: (theme) => theme.typography.pxToRem(12), + top: (theme) => theme.spacing(-3), + right: (theme) => theme.spacing(-2), + paddingInline: 1, + }} + > + {intl.formatMessage({ defaultMessage: 'Best' })} + + ) : undefined} + + + + ({intl.formatNumber(route.value, currencyFormat)}) + + + } + > + + + {route.name} + + + + {intl.formatMessage({ defaultMessage: 'Rate:' })} + + 1:{route.rate} + + + + + {intl.formatMessage({ + defaultMessage: 'Gas:', + })} + + + ~{intl.formatNumber(route.transactionCost, currencyFormat)} + + + {route.type === 'redeem' ? ( + + + {intl.formatMessage({ + defaultMessage: 'Wait time:', + })} + + {/* TODO better logic for coloring -> prob time should come as a ms duration and getting it formated */} + + ~{route.waitTime} + + + ) : undefined} + + + ); +} diff --git a/libs/defi/oeth/src/components/Swap/fixtures.ts b/libs/defi/oeth/src/components/Swap/fixtures.ts new file mode 100644 index 000000000..7103e966c --- /dev/null +++ b/libs/defi/oeth/src/components/Swap/fixtures.ts @@ -0,0 +1,61 @@ +export const routes = [ + { + type: 'swap', + name: 'Mint via Oeth zapper', + quantity: 150, + value: 284389.5, + rate: 1, + transactionCost: 89.25, + icon: 'https://app.oeth.com/images/oeth.svg', + }, + { + type: 'swap', + name: 'Curve', + quantity: 149.2832, + value: 282128.93, + rate: 0.9995, + transactionCost: 12.35, + icon: 'https://app.oeth.com/images/currency/eth-icon-small.svg', + }, + { + type: 'swap', + name: 'Maverick', + rate: 0.9796, + quantity: 148.74226, + value: 277106.84, + transactionCost: 46.47, + icon: '/images/maverick.png', + }, + { + type: 'swap', + name: 'Uniswap', + rate: 0.9796, + value: 277106.84, + quantity: 148.74226, + transactionCost: 46.47, + icon: '/images/uniswap.png', + }, +]; + +export const redeemRoutes = [ + { + type: 'redeem', + name: 'Redeem via OETH vault', + rate: 1, + value: 284389.5, + quantity: 150, + transactionCost: 89.25, + waitTime: '3 days', + icon: 'https://app.oeth.com/images/currency/eth-icon-small.svg', + }, + { + type: 'redeem', + name: 'Redeem via OETH vault', + rate: 1, + value: 284389.5, + quantity: 150, + transactionCost: 89.25, + waitTime: '1 min', + icon: 'https://app.oeth.com/images/currency/eth-icon-small.svg', + }, +]; diff --git a/libs/defi/oeth/src/components/Swap/index.tsx b/libs/defi/oeth/src/components/Swap/index.tsx new file mode 100644 index 000000000..d0eaa39ca --- /dev/null +++ b/libs/defi/oeth/src/components/Swap/index.tsx @@ -0,0 +1,2 @@ +export * from './Swap'; +export * from './SwapRoute'; diff --git a/libs/defi/oeth/src/components/Wrap/SwapWrap.tsx b/libs/defi/oeth/src/components/Wrap/SwapWrap.tsx new file mode 100644 index 000000000..f5405e49a --- /dev/null +++ b/libs/defi/oeth/src/components/Wrap/SwapWrap.tsx @@ -0,0 +1,62 @@ +import { useState } from 'react'; + +import { SwapCard } from '@origin/shared/components'; +import random from 'lodash/random'; +import { useIntl } from 'react-intl'; + +export function PortfolioSwap() { + const intl = useIntl(); + const [values, setValues] = useState({ + baseToken: { + abbreviation: 'OETH', + imgSrc: 'https://app.oeth.com/images/currency/oeth-icon-small.svg', + quantity: 0, + }, + exchangeCurrency: { + abbreviation: 'wOETH', + imgSrc: 'https://app.oeth.com/images/currency/woeth-icon-small.svg', + quantity: 0, + }, + }); + + function handleValueChange(value: string) { + const number = parseInt(value) || 0; + setValues((prev) => ({ + baseToken: { + ...prev.baseToken, + quantity: number, + }, + exchangeCurrency: { + ...prev.exchangeCurrency, + quantity: number * random(number - 0.5, number, true), + }, + })); + } + + function swapTokens() { + setValues((prev) => ({ + baseToken: { + ...prev.exchangeCurrency, + quantity: prev.baseToken.quantity, + }, + exchangeCurrency: { + ...prev.baseToken, + quantity: + prev.baseToken.quantity * + random(prev.baseToken.quantity - 0.5, prev.baseToken.quantity, true), + }, + })); + } + return ( + + ); +} diff --git a/libs/defi/oeth/src/components/Wrap/index.tsx b/libs/defi/oeth/src/components/Wrap/index.tsx new file mode 100644 index 000000000..bdd0921e5 --- /dev/null +++ b/libs/defi/oeth/src/components/Wrap/index.tsx @@ -0,0 +1 @@ +export * from './SwapWrap'; diff --git a/libs/defi/oeth/src/components/shared/index.tsx b/libs/defi/oeth/src/components/shared/index.tsx new file mode 100644 index 000000000..e8593a84b --- /dev/null +++ b/libs/defi/oeth/src/components/shared/index.tsx @@ -0,0 +1,2 @@ +export * from './ConnectButton'; +export * from './APY'; diff --git a/libs/defi/oeth/src/views/History.tsx b/libs/defi/oeth/src/views/History.tsx new file mode 100644 index 000000000..b0e915121 --- /dev/null +++ b/libs/defi/oeth/src/views/History.tsx @@ -0,0 +1,16 @@ +import { APY, HistoryCard } from '../components'; + +export function HistoryView() { + return ( + <> + + + + ); +} diff --git a/libs/defi/oeth/src/views/Swap.tsx b/libs/defi/oeth/src/views/Swap.tsx new file mode 100644 index 000000000..3f06fc0ca --- /dev/null +++ b/libs/defi/oeth/src/views/Swap.tsx @@ -0,0 +1,21 @@ +import { Stack } from '@mui/material'; + +import { APY, Swap } from '../components'; + +export function SwapView() { + return ( + <> + + + + + + + ); +} diff --git a/libs/defi/oeth/src/views/Wrap.tsx b/libs/defi/oeth/src/views/Wrap.tsx new file mode 100644 index 000000000..3adce7712 --- /dev/null +++ b/libs/defi/oeth/src/views/Wrap.tsx @@ -0,0 +1,55 @@ +import { Button, Stack } from '@mui/material'; +import { Card } from '@origin/shared/components'; +import { useIntl } from 'react-intl'; + +import { APY, PortfolioSwap } from '../components'; +import { ConnectWallet } from '../components/shared'; + +export function WrapView() { + const intl = useIntl(); + return ( + <> + + + + + + + + + + + ); +} diff --git a/libs/defi/oeth/src/views/index.tsx b/libs/defi/oeth/src/views/index.tsx new file mode 100644 index 000000000..68a2f7e59 --- /dev/null +++ b/libs/defi/oeth/src/views/index.tsx @@ -0,0 +1,3 @@ +export * from './Swap'; +export * from './Wrap'; +export * from './History'; diff --git a/libs/defi/ousd/src/components/defi-ousd.stories.tsx b/libs/defi/ousd/src/components/defi-ousd.stories.tsx deleted file mode 100644 index 470245a06..000000000 --- a/libs/defi/ousd/src/components/defi-ousd.stories.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { Meta } from '@storybook/react'; -import { DefiOusd } from './defi-ousd'; - -const Story: Meta = { - component: DefiOusd, - title: 'DefiOusd', -}; -export default Story; - -export const Primary = { - args: {}, -}; diff --git a/libs/shared/components/.babelrc b/libs/shared/components/.babelrc new file mode 100644 index 000000000..1ea870ead --- /dev/null +++ b/libs/shared/components/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/shared/components/.eslintrc.json b/libs/shared/components/.eslintrc.json new file mode 100644 index 000000000..75b85077d --- /dev/null +++ b/libs/shared/components/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/shared/components/.storybook/main.ts b/libs/shared/components/.storybook/main.ts new file mode 100644 index 000000000..5995b1e82 --- /dev/null +++ b/libs/shared/components/.storybook/main.ts @@ -0,0 +1,32 @@ +import type { StorybookConfig } from '@storybook/react-vite'; + +const config: StorybookConfig = { + stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'], + addons: [ + '@storybook/addon-essentials', + 'storybook-addon-pseudo-states', + '@storybook/addon-interactions', + ], + framework: { + name: '@storybook/react-vite', + options: { + builder: { + viteConfigPath: `${__dirname}../../../storybook/vite.config.ts`, + }, + }, + }, + typescript: { + check: true, + checkOptions: { + typescript: { + configFile: `${__dirname}/../tsconfig.storybook.json`, + }, + }, + }, +}; + +export default config; + +// To customize your Vite configuration you can use the viteFinal field. +// Check https://storybook.js.org/docs/react/builders/vite#configuration +// and https://nx.dev/packages/storybook/documents/custom-builder-configs diff --git a/libs/shared/components/.storybook/preview.ts b/libs/shared/components/.storybook/preview.ts new file mode 100644 index 000000000..d7cc85f52 --- /dev/null +++ b/libs/shared/components/.storybook/preview.ts @@ -0,0 +1,3 @@ +import preview from '@origin/shared/storybook'; + +export default preview; diff --git a/libs/shared/components/README.md b/libs/shared/components/README.md new file mode 100644 index 000000000..22240b2a0 --- /dev/null +++ b/libs/shared/components/README.md @@ -0,0 +1,7 @@ +# shared-components + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test shared-components` to execute the unit tests via [Vitest](https://vitest.dev/). diff --git a/libs/shared/components/project.json b/libs/shared/components/project.json new file mode 100644 index 000000000..54bede57b --- /dev/null +++ b/libs/shared/components/project.json @@ -0,0 +1,61 @@ +{ + "name": "shared-components", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/shared/components/src", + "projectType": "library", + "tags": [], + "targets": { + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/shared/components/**/*.{ts,tsx,js,jsx}"] + } + }, + "test": { + "executor": "@nx/vite:test", + "outputs": ["coverage/libs/shared/components"], + "options": { + "passWithNoTests": true, + "reportsDirectory": "../../../coverage/libs/shared/components" + } + }, + "storybook": { + "executor": "@nx/storybook:storybook", + "options": { + "port": 4400, + "configDir": "libs/shared/components/.storybook" + }, + "configurations": { + "ci": { + "quiet": true + } + } + }, + "build-storybook": { + "executor": "@nx/storybook:build", + "outputs": ["{options.outputDir}"], + "options": { + "outputDir": "dist/storybook/shared-components", + "configDir": "libs/shared/components/.storybook" + }, + "configurations": { + "ci": { + "quiet": true + } + } + }, + "static-storybook": { + "executor": "@nx/web:file-server", + "options": { + "buildTarget": "shared-components:build-storybook", + "staticFilePath": "dist/storybook/shared-components" + }, + "configurations": { + "ci": { + "buildTarget": "shared-components:build-storybook:ci" + } + } + } + } +} diff --git a/libs/shared/components/src/Cards/Card.tsx b/libs/shared/components/src/Cards/Card.tsx new file mode 100644 index 000000000..0d7ad9dff --- /dev/null +++ b/libs/shared/components/src/Cards/Card.tsx @@ -0,0 +1,62 @@ +import { Card as MuiCard, CardContent, CardHeader } from '@mui/material'; + +import type { SxProps } from '@mui/material'; +import type { Theme } from '@origin/shared/theme'; + +export const cardStyles = { + paddingBlock: 2.5, + paddingInline: 2, +} as const; + +interface Props { + title: string | React.ReactNode; + children: React.ReactNode; + sxCardContent?: SxProps; + sxCardTitle?: SxProps; + sx?: SxProps; +} + +export function Card({ + title, + children, + sxCardContent, + sxCardTitle, + sx, +}: Props) { + return ( + + theme.typography.pxToRem(14), + }, + ...(sxCardTitle as SxProps), + }} + > + + {children} + + + ); +} diff --git a/libs/shared/components/src/Cards/SwapCard/ActionButton.stories.tsx b/libs/shared/components/src/Cards/SwapCard/ActionButton.stories.tsx new file mode 100644 index 000000000..6d47f48bc --- /dev/null +++ b/libs/shared/components/src/Cards/SwapCard/ActionButton.stories.tsx @@ -0,0 +1,44 @@ +import { Container } from '@mui/material'; + +import { ActionButton } from './ActionButton'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + component: ActionButton, + title: 'Swap Card/Action Button', + args: { + children: 'Swap', + }, + render: (args) => ( + + + + ), +}; + +export default meta; + +export const Default: StoryObj = {}; + +export const Hover: StoryObj = { + parameters: { + pseudo: { + hover: true, + }, + }, +}; + +export const Disabled: StoryObj = { + args: { + disabled: true, + }, +}; + +export const SmallMobile: StoryObj = { + parameters: { + viewport: { + defaultViewport: 'mobile1', + }, + }, +}; diff --git a/libs/shared/components/src/Cards/SwapCard/ActionButton.tsx b/libs/shared/components/src/Cards/SwapCard/ActionButton.tsx new file mode 100644 index 000000000..8f6d0b399 --- /dev/null +++ b/libs/shared/components/src/Cards/SwapCard/ActionButton.tsx @@ -0,0 +1,40 @@ +import { Button } from '@mui/material'; + +import type { ButtonProps } from '@mui/material'; + +interface Props extends ButtonProps {} + +export function ActionButton({ sx, children, ...rest }: Props) { + return ( + + ); +} diff --git a/libs/shared/components/src/Cards/SwapCard/Input.stories.tsx b/libs/shared/components/src/Cards/SwapCard/Input.stories.tsx new file mode 100644 index 000000000..93cb83107 --- /dev/null +++ b/libs/shared/components/src/Cards/SwapCard/Input.stories.tsx @@ -0,0 +1,45 @@ +import { Container } from '@mui/material'; +import { userEvent, within } from '@storybook/testing-library'; + +import { Input } from './Input'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + component: Input, + title: 'Swap Card/Input', + args: { + isLoading: false, + isSwapped: false, + baseTokenBalance: 250, + baseTokenValue: 0, + baseTokenName: 'stETH', + baseTokenIcon: 'https://app.oeth.com/images/currency/steth-icon-small.svg', + onValueChange: () => null, + }, + render: (args) => ( + + + + ), +}; + +export default meta; + +export const Default: StoryObj = {}; + +export const Hover: StoryObj = { + parameters: { + pseudo: { + hover: true, + }, + }, +}; + +export const WithValue: StoryObj = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const element = canvas.getByTestId('swap-input'); + userEvent.type(element, '150'); + }, +}; diff --git a/libs/shared/components/src/Cards/SwapCard/Input.tsx b/libs/shared/components/src/Cards/SwapCard/Input.tsx new file mode 100644 index 000000000..b0bc3b54b --- /dev/null +++ b/libs/shared/components/src/Cards/SwapCard/Input.tsx @@ -0,0 +1,177 @@ +import { useState } from 'react'; + +import { alpha, Box, InputBase, Stack, Typography } from '@mui/material'; +import { useDebouncedEffect } from '@react-hookz/web'; +import { useIntl } from 'react-intl'; + +import { Loader } from '../../Loader'; +import { cardStyles } from '../Card'; +import { currencyFormat } from './SwapCard'; +import { SwapItem } from './SwapItem'; +import { styles } from './utils'; + +interface Props { + baseTokenName: string; + baseTokenIcon: string | string[]; + isSwapped: boolean; + baseTokenBalance?: number; + baseTokenValue?: number; + isLoading: boolean; + exchangeTokenNode?: React.ReactNode; + onValueChange: (value: string) => void; +} + +export function Input({ + baseTokenIcon, + baseTokenName, + isSwapped, + baseTokenBalance, + baseTokenValue, + isLoading, + exchangeTokenNode, + onValueChange, +}: Props) { + const intl = useIntl(); + const [value, setValues] = useState(''); + useDebouncedEffect( + () => { + onValueChange(value); + }, + [value], + 350, + ); + return ( + theme.shape.borderRadius, + borderStartEndRadius: (theme) => theme.shape.borderRadius, + }, + '&:hover': { + background: (theme) => + `linear-gradient(${theme.palette.grey[900]}, ${ + theme.palette.grey[900] + }) padding-box, + linear-gradient(90deg, ${alpha( + theme.palette.primary.main, + 0.4, + )} 0%, ${alpha( + theme.palette.primary.dark, + 0.4, + )} 100%) border-box;`, + }, + '&:focus-within': { + background: (theme) => + `linear-gradient(${theme.palette.grey[900]}, ${theme.palette.grey[900]}) padding-box, + linear-gradient(90deg, var(--mui-palette-primary-main) 0%, var(--mui-palette-primary-dark) 100%) border-box;`, + }, + }} + > + + { + setValues(e.target.value.replace(/[^\d.,]/g, '').replace(',', '.')); + }} + data-testid="swap-input" + /> + + + + + {baseTokenValue !== undefined ? ( + isLoading ? ( + + ) : ( + + {intl.formatNumber(baseTokenValue, currencyFormat)} + + ) + ) : undefined} + + {baseTokenBalance ? ( + + {intl.formatMessage( + { defaultMessage: 'Balance: {number}' }, + { number: intl.formatNumber(baseTokenBalance, currencyFormat) }, + )} + {/* alpha(theme.palette.common.white, 0.1), + }} + > + max + */} + + ) : undefined} + + + ); +} diff --git a/libs/shared/components/src/Cards/SwapCard/Output.stories.tsx b/libs/shared/components/src/Cards/SwapCard/Output.stories.tsx new file mode 100644 index 000000000..43c8228b1 --- /dev/null +++ b/libs/shared/components/src/Cards/SwapCard/Output.stories.tsx @@ -0,0 +1,42 @@ +import { Container } from '@mui/material'; + +import { Output } from './Output'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + component: Output, + title: 'Swap Card/Output', + args: { + isLoading: false, + isSwapped: false, + exchangeTokenQuantity: 0, + exchangeTokenValue: 0, + exchangeTokenName: 'OETH', + exchangeTokenIcon: + ' https://app.oeth.com/images/currency/oeth-icon-small.svg', + exchangeTokenBalance: 0, + }, + render: (args) => ( + + + + ), +}; + +export default meta; + +export const Default: StoryObj = {}; + +export const WithValue: StoryObj = { + args: { + exchangeTokenQuantity: 150, + exchangeTokenValue: 284389.5, + }, +}; + +export const Loading: StoryObj = { + args: { + isLoading: true, + }, +}; diff --git a/libs/shared/components/src/Cards/SwapCard/Output.tsx b/libs/shared/components/src/Cards/SwapCard/Output.tsx new file mode 100644 index 000000000..e9f24a3f1 --- /dev/null +++ b/libs/shared/components/src/Cards/SwapCard/Output.tsx @@ -0,0 +1,111 @@ +import { alpha, Box, Typography } from '@mui/material'; +import { useIntl } from 'react-intl'; + +import { Loader } from '../../Loader'; +import { cardStyles } from '../Card'; +import { currencyFormat, valueFormat } from './SwapCard'; +import { SwapItem } from './SwapItem'; +import { styles } from './utils'; + +interface Props { + isLoading: boolean; + exchangeTokenQuantity: number; + exchangeTokenName: string; + exchangeTokenIcon: string | string[]; + exchangeTokenValue?: number; + isSwapped: boolean; + exchangeTokenNode?: React.ReactNode; + exchangeTokenBalance?: number; +} + +export function Output({ + isLoading, + exchangeTokenIcon, + exchangeTokenName, + exchangeTokenQuantity, + isSwapped, + exchangeTokenValue, + exchangeTokenNode, + exchangeTokenBalance, +}: Props) { + const intl = useIntl(); + return ( + alpha(theme.palette.grey[400], 0.2), + ...cardStyles, + paddingBlock: 3.0625, + }} + > + + {isLoading ? ( + + ) : ( + + exchangeTokenQuantity === 0 + ? theme.palette.text.secondary + : theme.palette.primary.contrastText, + }} + > + {intl.formatNumber(exchangeTokenQuantity, valueFormat)} + + )} + + + + {exchangeTokenValue !== undefined ? ( + isLoading ? ( + + ) : ( + + {intl.formatNumber(exchangeTokenValue, currencyFormat)} + + ) + ) : undefined} + {exchangeTokenBalance !== undefined ? ( + isLoading ? ( + + ) : ( + + {intl.formatMessage( + { defaultMessage: 'Balance: {number}' }, + { + number: intl.formatNumber( + exchangeTokenBalance, + currencyFormat, + ), + }, + )} + + ) + ) : undefined} + + + ); +} diff --git a/libs/shared/components/src/Cards/SwapCard/SwapButton.stories.tsx b/libs/shared/components/src/Cards/SwapCard/SwapButton.stories.tsx new file mode 100644 index 000000000..57ee815e1 --- /dev/null +++ b/libs/shared/components/src/Cards/SwapCard/SwapButton.stories.tsx @@ -0,0 +1,29 @@ +import { SwapButton } from './SwapButton'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + component: SwapButton, + title: 'Swap Card/Swap Button', + args: {}, +}; + +export default meta; + +export const Default: StoryObj = {}; + +export const Hover: StoryObj = { + parameters: { + pseudo: { + hover: true, + }, + }, +}; + +export const Mobile: StoryObj = { + parameters: { + viewport: { + defaultViewport: 'mobile1', + }, + }, +}; diff --git a/libs/shared/components/src/Cards/SwapCard/SwapButton.tsx b/libs/shared/components/src/Cards/SwapCard/SwapButton.tsx new file mode 100644 index 000000000..488af9e62 --- /dev/null +++ b/libs/shared/components/src/Cards/SwapCard/SwapButton.tsx @@ -0,0 +1,48 @@ +import { Box, IconButton } from '@mui/material'; + +import type { IconButtonProps } from '@mui/material'; + +interface Props extends IconButtonProps {} + +export function SwapButton({ onClick, sx, ...rest }: Props) { + return ( + theme.palette.background.paper, + strokeWidth: (theme) => theme.typography.pxToRem(2), + stroke: (theme) => theme.palette.grey[700], + transform: { xs: 'translateY(-20%)', md: 'translateY(-8%)' }, + backgroundColor: (theme) => theme.palette.divider, + '& img': { + transition: (theme) => theme.transitions.create('transform'), + }, + '&:hover': { + backgroundColor: (theme) => theme.palette.background.default, + '& img': { + transform: 'rotate(-180deg)', + }, + }, + ...sx, + }} + {...rest} + > + + + ); +} diff --git a/libs/shared/components/src/Cards/SwapCard/SwapCard.stories.tsx b/libs/shared/components/src/Cards/SwapCard/SwapCard.stories.tsx new file mode 100644 index 000000000..e663f92ac --- /dev/null +++ b/libs/shared/components/src/Cards/SwapCard/SwapCard.stories.tsx @@ -0,0 +1,81 @@ +import { Container } from '@mui/material'; + +import { SwapCard } from './SwapCard'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + component: SwapCard, + title: 'Swap Card/Card', + args: { + title: 'Swap', + baseTokenIcon: 'https://app.oeth.com/images/currency/eth-icon-small.svg', + baseTokenName: 'ETH', + baseTokenValue: 0, + exchangeTokenValue: 0, + exchangeTokenQuantity: 0, + exchangeTokenIcon: + 'https://app.oeth.com/images/currency/oeth-icon-small.svg', + exchangeTokenName: 'OETH', + }, + render: (args) => ( + + + + ), +}; +export default meta; + +export const Default: StoryObj = {}; + +export const WithMaxValue: StoryObj = { + args: { + baseTokenBalance: 250, + }, +}; + +export const WithExchangeTokenValue: StoryObj = { + args: { + exchangeTokenValue: 284389.5, + exchangeTokenQuantity: 150, + exchangeTokenBalance: 300, + }, +}; + +export const Hover: StoryObj = { + parameters: { + pseudo: { + hover: true, + }, + }, +}; + +export const IsLoading: StoryObj = { + args: { + isLoading: true, + }, +}; + +export const SmallMobile: StoryObj = { + parameters: { + viewport: { + defaultViewport: 'mobile1', + }, + }, +}; + +export const LargeMobile: StoryObj = { + parameters: { + viewport: { + defaultViewport: 'mobile2', + }, + }, +}; + +export const Tablet: StoryObj = { + parameters: { + viewport: { + defaultViewport: 'tablet', + }, + }, +}; diff --git a/libs/shared/components/src/Cards/SwapCard/SwapCard.tsx b/libs/shared/components/src/Cards/SwapCard/SwapCard.tsx new file mode 100644 index 000000000..bc2e2a65e --- /dev/null +++ b/libs/shared/components/src/Cards/SwapCard/SwapCard.tsx @@ -0,0 +1,104 @@ +import { useState } from 'react'; + +import { Box, Stack } from '@mui/material'; + +import { Card } from '../Card'; +import { Input } from './Input'; +import { Output } from './Output'; +import { SwapButton } from './SwapButton'; + +import type { FormatNumberOptions } from 'react-intl'; + +export const valueFormat: FormatNumberOptions = { + minimumFractionDigits: 2, +}; +export const currencyFormat: FormatNumberOptions = { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, +}; +export const quantityFormat: FormatNumberOptions = { + minimumFractionDigits: 0, + maximumFractionDigits: 4, +}; + +interface Props { + title: string | React.ReactNode; + baseTokenName: string; + baseTokenIcon: string | string[]; + baseTokenBalance?: number; + baseTokenValue?: number; + exchangeTokenName: string; + exchangeTokenIcon: string | string[]; + exchangeTokenQuantity: number; + exchangeTokenValue?: number; + exchangeTokenNode?: React.ReactNode; + exchangeTokenBalance?: number; + isLoading?: boolean; + onValueChange: (value: string) => void; + onSwap: () => void; + children?: React.ReactNode[]; +} + +export function SwapCard({ + title, + baseTokenIcon, + baseTokenName, + exchangeTokenIcon, + exchangeTokenNode, + exchangeTokenName, + baseTokenValue, + baseTokenBalance, + exchangeTokenQuantity, + exchangeTokenValue, + exchangeTokenBalance, + onValueChange, + onSwap, + isLoading = false, + children, +}: Props) { + const [isSwapped, setSwapState] = useState(false); + return ( + + + + + { + setSwapState((prev) => !prev); + onSwap(); + }} + /> + + + + {children} + + + ); +} diff --git a/libs/shared/components/src/Cards/SwapCard/SwapItem.stories.tsx b/libs/shared/components/src/Cards/SwapCard/SwapItem.stories.tsx new file mode 100644 index 000000000..99f9b5074 --- /dev/null +++ b/libs/shared/components/src/Cards/SwapCard/SwapItem.stories.tsx @@ -0,0 +1,43 @@ +import { DropdownIcon, SwapItem } from './SwapItem'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + component: SwapItem, + title: 'Swap Card/Swap Item', + args: { + name: 'ETH', + icon: 'https://app.oeth.com/images/currency/eth-icon-small.svg', + }, +}; + +export default meta; + +export const Default: StoryObj = {}; +export const WithAdditionalNode: StoryObj = { + args: { + additionalNode: , + }, +}; + +export const Hover: StoryObj = { + args: { + additionalNode: , + }, + parameters: { + pseudo: { + hover: true, + }, + }, +}; +export const Mix: StoryObj = { + args: { + icon: [ + 'https://app.oeth.com/images/currency/weth-icon-small.png', + 'https://app.oeth.com/images/currency/reth-icon-small.png', + 'https://app.oeth.com/images/currency/steth-icon-small.svg', + 'https://app.oeth.com/images/currency/frxeth-icon-small.svg', + ], + name: 'LST Mix', + }, +}; diff --git a/libs/shared/components/src/Cards/SwapCard/SwapItem.tsx b/libs/shared/components/src/Cards/SwapCard/SwapItem.tsx new file mode 100644 index 000000000..058298608 --- /dev/null +++ b/libs/shared/components/src/Cards/SwapCard/SwapItem.tsx @@ -0,0 +1,74 @@ +import { alpha, Box, IconButton, Stack } from '@mui/material'; + +import { Mix } from '../../Mix'; +import { ReactComponent as Dropdown } from './dropdown.svg'; + +import type { ButtonProps, SxProps } from '@mui/material'; + +interface Props { + icon: string | string[]; + name: string; + additionalNode?: React.ReactNode; + sx?: SxProps; +} + +export function SwapItem({ icon, name, additionalNode: Component, sx }: Props) { + return ( + alpha(theme.palette.common.white, 0.1), + fontStyle: 'normal', + cursor: 'pointer', + fontWeight: 500, + ':hover': { + background: (theme) => alpha(theme.palette.common.white, 0.15), + }, + ...sx, + }} + role="button" + > + {typeof icon === 'string' ? ( + + ) : ( + + )} + {name} + {Component ? Component : undefined} + + ); +} + +export function DropdownIcon({ sx, ...rest }: ButtonProps) { + return ( + + + + ); +} diff --git a/libs/shared/components/src/Cards/SwapCard/SwapItemArrow.tsx b/libs/shared/components/src/Cards/SwapCard/SwapItemArrow.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/libs/shared/components/src/Cards/SwapCard/TokenListItem.stories.tsx b/libs/shared/components/src/Cards/SwapCard/TokenListItem.stories.tsx new file mode 100644 index 000000000..aa04c196d --- /dev/null +++ b/libs/shared/components/src/Cards/SwapCard/TokenListItem.stories.tsx @@ -0,0 +1,87 @@ +import { Container } from '@mui/material'; + +import { TokenListItem } from './TokenListItem'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + component: TokenListItem, + title: 'Swap Card/Token list item', + args: { + option: { + name: 'Lido Staked Ether', + abbreviation: 'stETH', + imgSrc: ' https://app.oeth.com/images/currency/steth-icon-small.svg', + quantity: 4, + value: 8580.24, + }, + onSelection: () => null, + selected: false, + }, + render: (args) => ( + + + + ), + parameters: { + backgrounds: { + default: 'dark', + values: [ + { + name: 'dark', + value: '#282A32', + }, + ], + }, + }, +}; + +export default meta; + +export const Default: StoryObj = {}; + +export const Hover: StoryObj = { + parameters: { + pseudo: { + hover: true, + }, + }, +}; + +export const Selected: StoryObj = { + args: { + selected: true, + }, +}; + +export const Mix: StoryObj = { + args: { + option: { + imgSrc: [ + 'https://app.oeth.com/images/currency/weth-icon-small.png', + 'https://app.oeth.com/images/currency/reth-icon-small.png', + 'https://app.oeth.com/images/currency/steth-icon-small.svg', + 'https://app.oeth.com/images/currency/frxeth-icon-small.svg', + ], + name: 'LST Mix', + abbreviation: ['wETH', 'rETH', 'stETH', 'frxETH'], + value: 0, + quantity: 0, + }, + }, +}; + +export const MixTwoItems: StoryObj = { + args: { + option: { + imgSrc: [ + 'https://app.oeth.com/images/currency/weth-icon-small.png', + 'https://app.oeth.com/images/currency/reth-icon-small.png', + ], + name: 'LST Mix', + abbreviation: ['wETH', 'rETH'], + value: 0, + quantity: 0, + }, + }, +}; diff --git a/libs/shared/components/src/Cards/SwapCard/TokenListItem.tsx b/libs/shared/components/src/Cards/SwapCard/TokenListItem.tsx new file mode 100644 index 000000000..cfb54a802 --- /dev/null +++ b/libs/shared/components/src/Cards/SwapCard/TokenListItem.tsx @@ -0,0 +1,84 @@ +import { Box, MenuItem, Stack, Typography } from '@mui/material'; +import { useIntl } from 'react-intl'; + +import { Mix } from '../../Mix'; +import { currencyFormat, quantityFormat } from './SwapCard'; + +import type { Option } from './TokenListModal'; + +const gap = 1.5; +interface Props { + option: Option; + onSelection: (option: Pick) => void; + selected: boolean; +} + +export function TokenListItem({ option, onSelection, selected }: Props) { + const intl = useIntl(); + return ( + theme.palette.background.paper, + borderRadius: 1, + '&:hover': { + background: (theme) => theme.palette.grey[700], + }, + ...(selected ? { opacity: 0.5 } : {}), + }} + onClick={() => { + onSelection({ + name: + typeof option.abbreviation === 'string' + ? option.abbreviation + : option.name, + imgSrc: option.imgSrc, + }); + }} + > + + {typeof option.imgSrc === 'string' ? ( + + ) : ( + + )} + + {option.name} + span:not(:last-child):after': { + content: '", "', + }, + }} + > + {typeof option.abbreviation === 'string' + ? option.abbreviation + : option.abbreviation.map((abbr) => ( + {abbr} + ))} + + + + + + + {intl.formatNumber(option.quantity, quantityFormat)} + + + {intl.formatNumber(option.value, currencyFormat)} + + + + ); +} diff --git a/libs/shared/components/src/Cards/SwapCard/TokenListModal.stories.tsx b/libs/shared/components/src/Cards/SwapCard/TokenListModal.stories.tsx new file mode 100644 index 000000000..24b762007 --- /dev/null +++ b/libs/shared/components/src/Cards/SwapCard/TokenListModal.stories.tsx @@ -0,0 +1,92 @@ +import { TokenListModal } from './TokenListModal'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + component: TokenListModal, + title: 'Swap Card/Swap Modal', + args: { + handleClose: () => null, + isOpen: true, + onSelection: (option) => null, + options: [ + { + name: 'Ether', + abbreviation: 'ETH', + imgSrc: ' https://app.oeth.com/images/currency/eth-icon-small.svg', + quantity: 13820, + value: 0, + }, + { + name: 'Wrapped Ether', + abbreviation: 'wETH', + imgSrc: 'https://app.oeth.com/images/currency/weth-icon-small.png', + quantity: 0, + value: 0, + }, + { + name: 'Lido Staked Ether', + abbreviation: 'stETH', + imgSrc: 'https://app.oeth.com/images/currency/steth-icon-small.svg', + quantity: 0, + value: 0, + }, + { + name: 'Rocket Pool Ether', + abbreviation: 'rETH', + imgSrc: 'https://app.oeth.com/images/currency/reth-icon-small.png', + quantity: 0, + value: 0, + }, + { + name: 'Frax Ether', + abbreviation: 'frxETH', + imgSrc: 'https://app.oeth.com/images/currency/frxeth-icon-small.svg', + quantity: 0, + value: 0, + }, + { + name: 'Origin Ether', + abbreviation: 'OETH', + imgSrc: 'https://app.oeth.com/images/currency/oeth-icon-small.svg', + quantity: 0, + value: 0, + }, + { + name: 'Wrapped Ether', + abbreviation: 'wOETH', + imgSrc: 'https://app.oeth.com/images/currency/woeth-icon-small.svg', + quantity: 1, + value: 1952.38, + }, + ], + }, +}; + +export default meta; + +export const Default: StoryObj = {}; + +export const SmallMobile: StoryObj = { + parameters: { + viewport: { + defaultViewport: 'mobile1', + }, + }, +}; + +export const LargeMobile: StoryObj = { + parameters: { + viewport: { + defaultViewport: 'mobile2', + }, + }, +}; + +export const TableMobile: StoryObj = { + parameters: { + viewport: { + defaultViewport: 'tablet', + }, + }, +}; diff --git a/libs/shared/components/src/Cards/SwapCard/TokenListModal.tsx b/libs/shared/components/src/Cards/SwapCard/TokenListModal.tsx new file mode 100644 index 000000000..c025a1c79 --- /dev/null +++ b/libs/shared/components/src/Cards/SwapCard/TokenListModal.tsx @@ -0,0 +1,75 @@ +import { Dialog, MenuList } from '@mui/material'; +import { eq } from 'lodash'; + +import { TokenListItem } from './TokenListItem'; + +interface MixOption { + imgSrc: string[]; + abbreviation: string[]; +} + +export interface SwapOption { + imgSrc: string; + abbreviation: string; +} + +export type Option = { name: string; value: number; quantity: number } & ( + | SwapOption + | MixOption +); + +interface Props { + handleClose: () => void; + isOpen: boolean; + options: Option[]; + onSelection: (option: Pick) => void; + selected: string | string[]; +} + +export function TokenListModal({ + handleClose, + isOpen, + options, + onSelection, + selected, +}: Props) { + return ( + theme.palette.background.paper, + borderRadius: 2, + border: '1px solid', + borderColor: (theme) => theme.palette.grey[800], + backgroundImage: 'none', + margin: 0, + minWidth: 'min(90vw, 33rem)', + }, + }} + > + + {options.map((option) => ( + { + onSelection(option); + handleClose(); + }} + selected={eq(selected, option.abbreviation)} + /> + ))} + + + ); +} diff --git a/libs/shared/components/src/Cards/SwapCard/dropdown.svg b/libs/shared/components/src/Cards/SwapCard/dropdown.svg new file mode 100644 index 000000000..12076e8d8 --- /dev/null +++ b/libs/shared/components/src/Cards/SwapCard/dropdown.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/libs/shared/components/src/Cards/SwapCard/index.tsx b/libs/shared/components/src/Cards/SwapCard/index.tsx new file mode 100644 index 000000000..96df922b8 --- /dev/null +++ b/libs/shared/components/src/Cards/SwapCard/index.tsx @@ -0,0 +1,4 @@ +export * from './SwapCard'; +export * from './TokenListModal'; +export * from './ActionButton'; +export { DropdownIcon } from './SwapItem'; diff --git a/libs/shared/components/src/Cards/SwapCard/utils.ts b/libs/shared/components/src/Cards/SwapCard/utils.ts new file mode 100644 index 000000000..92cfa08ae --- /dev/null +++ b/libs/shared/components/src/Cards/SwapCard/utils.ts @@ -0,0 +1,8 @@ +export const styles = { + display: 'grid', + justifyContent: 'space-between', + alignContent: 'end', + gap: 2.5, + gridTemplateColumns: '1fr auto', + rowGap: 1, +}; diff --git a/libs/shared/components/src/Cards/index.tsx b/libs/shared/components/src/Cards/index.tsx new file mode 100644 index 000000000..79595aed8 --- /dev/null +++ b/libs/shared/components/src/Cards/index.tsx @@ -0,0 +1,2 @@ +export * from './Card'; +export * from './SwapCard'; diff --git a/libs/shared/components/src/LinkIcon/LinkIcon.tsx b/libs/shared/components/src/LinkIcon/LinkIcon.tsx new file mode 100644 index 000000000..02e71d154 --- /dev/null +++ b/libs/shared/components/src/LinkIcon/LinkIcon.tsx @@ -0,0 +1,18 @@ +import { Box, Link } from '@mui/material'; + +interface Props { + url: string; + size?: string; +} + +export function LinkIcon({ url, size = '0.875rem' }: Props) { + return ( + + + + ); +} diff --git a/libs/shared/components/src/Loader/Loader.stories.tsx b/libs/shared/components/src/Loader/Loader.stories.tsx new file mode 100644 index 000000000..e950d52b9 --- /dev/null +++ b/libs/shared/components/src/Loader/Loader.stories.tsx @@ -0,0 +1,16 @@ +import { Loader } from './Loader'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + component: Loader, + title: 'Shared components/Loader', + args: { + width: 345, + height: 14, + }, +}; + +export default meta; + +export const Default: StoryObj = {}; diff --git a/libs/shared/components/src/Loader/Loader.tsx b/libs/shared/components/src/Loader/Loader.tsx new file mode 100644 index 000000000..cc5dedb7c --- /dev/null +++ b/libs/shared/components/src/Loader/Loader.tsx @@ -0,0 +1,16 @@ +import { Skeleton } from '@mui/material'; + +import type { SkeletonProps } from '@mui/material'; + +interface Props extends SkeletonProps {} + +export function Loader({ sx, ...rest }: Props) { + return ( + + ); +} diff --git a/libs/shared/components/src/Loader/index.tsx b/libs/shared/components/src/Loader/index.tsx new file mode 100644 index 000000000..d5ce98115 --- /dev/null +++ b/libs/shared/components/src/Loader/index.tsx @@ -0,0 +1 @@ +export * from './Loader'; diff --git a/libs/shared/components/src/Mix/Mix.stories.tsx b/libs/shared/components/src/Mix/Mix.stories.tsx new file mode 100644 index 000000000..d41d742f7 --- /dev/null +++ b/libs/shared/components/src/Mix/Mix.stories.tsx @@ -0,0 +1,20 @@ +import { Mix } from './Mix'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + component: Mix, + title: 'Shared components/Mix', + args: { + imgSrc: [ + 'https://app.oeth.com/images/currency/weth-icon-small.png', + 'https://app.oeth.com/images/currency/reth-icon-small.png', + 'https://app.oeth.com/images/currency/steth-icon-small.svg', + 'https://app.oeth.com/images/currency/frxeth-icon-small.svg', + ], + }, +}; + +export default meta; + +export const Default: StoryObj = {}; diff --git a/libs/shared/components/src/Mix/Mix.tsx b/libs/shared/components/src/Mix/Mix.tsx new file mode 100644 index 000000000..63cac1945 --- /dev/null +++ b/libs/shared/components/src/Mix/Mix.tsx @@ -0,0 +1,37 @@ +import { Box, Stack } from '@mui/material'; + +import type { SxProps } from '@mui/material'; + +interface Props { + imgSrc: string[]; + size?: number; + sx?: SxProps; +} + +export function Mix({ imgSrc, size = 2, sx }: Props) { + return ( + + {imgSrc.map((img, index, arr) => ( + + ))} + + ); +} diff --git a/libs/shared/components/src/Mix/index.tsx b/libs/shared/components/src/Mix/index.tsx new file mode 100644 index 000000000..15ac645c1 --- /dev/null +++ b/libs/shared/components/src/Mix/index.tsx @@ -0,0 +1 @@ +export * from './Mix'; diff --git a/libs/shared/components/src/index.ts b/libs/shared/components/src/index.ts new file mode 100644 index 000000000..785656bb9 --- /dev/null +++ b/libs/shared/components/src/index.ts @@ -0,0 +1,3 @@ +export * from './Cards'; +export * from './top-nav'; +export * from './LinkIcon/LinkIcon'; diff --git a/libs/shared/components/src/top-nav/Activity.stories.tsx b/libs/shared/components/src/top-nav/Activity.stories.tsx new file mode 100644 index 000000000..11c9bad27 --- /dev/null +++ b/libs/shared/components/src/top-nav/Activity.stories.tsx @@ -0,0 +1,98 @@ +import { within } from '@storybook/testing-library'; + +import { Activity } from './Activity'; +import { transactions } from './fixtures'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + component: Activity, + title: 'Top navigation/Activity', + args: { + transactions: [], + }, +}; +export default meta; + +export const Default: StoryObj = {}; + +export const Hover: StoryObj = { + parameters: { + pseudo: { + hover: true, + }, + }, +}; + +export const Expanded: StoryObj = { + name: 'User clicked on the profile button', + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.getByTestId('activity-button').click(); + }, +}; + +export const ExpandedWithTransactions: StoryObj = { + name: 'Recent activity list', + // @ts-expect-error type mismatch + args: { transactions }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.getByTestId('activity-button').click(); + }, +}; + +export const ExpandedWithLessTransactions: StoryObj = { + name: 'Recent activity list with smaller number of transactions', + // @ts-expect-error type mismatch + args: { transactions: transactions.slice(0, 4) }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.getByTestId('activity-button').click(); + }, +}; + +export const MobileSmall: StoryObj = { + name: 'Mobile -> Recent activity list', + // @ts-expect-error type mismatch + args: { transactions: transactions.slice(0, 4) }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.getByTestId('activity-button').click(); + }, + parameters: { + viewport: { + defaultViewport: 'mobile1', + }, + }, +}; + +export const MobileLarge: StoryObj = { + name: 'Mobile large -> Recent activity list', + // @ts-expect-error type mismatch + args: { transactions: transactions.slice(0, 4) }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.getByTestId('activity-button').click(); + }, + parameters: { + viewport: { + defaultViewport: 'mobile2', + }, + }, +}; + +export const Tablet: StoryObj = { + name: 'Tablet -> Recent activity list', + // @ts-expect-error type mismatch + args: { transactions: transactions.slice(0, 4) }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.getByTestId('activity-button').click(); + }, + parameters: { + viewport: { + defaultViewport: 'tablet', + }, + }, +}; diff --git a/libs/shared/components/src/top-nav/Activity.tsx b/libs/shared/components/src/top-nav/Activity.tsx new file mode 100644 index 000000000..756b835f2 --- /dev/null +++ b/libs/shared/components/src/top-nav/Activity.tsx @@ -0,0 +1,111 @@ +import { useState } from 'react'; + +import { + Box, + Divider, + IconButton, + Popover, + Typography, + useTheme, +} from '@mui/material'; +import { useIntl } from 'react-intl'; + +import { Icon } from './Icon'; +import { Transaction } from './Transaction'; +import { styles } from './utils'; + +import type { Transaction as ITransaction } from './types'; + +interface Props { + transactions: ITransaction[]; +} + +export function Activity({ transactions }: Props) { + const theme = useTheme(); + const intl = useIntl(); + const [anchor, setAnchor] = useState(null); + return ( + <> + theme.typography.pxToRem(40), + height: (theme) => theme.typography.pxToRem(40), + padding: 1, + '&:hover': { + background: (theme) => theme.palette.background.gradientHover, + }, + }} + data-testid="activity-button" + onClick={(e) => setAnchor(e.currentTarget)} + > + + + setAnchor(null)} + anchorOrigin={{ + vertical: 50, + horizontal: 'right', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + sx={{ + '& .MuiPopover-paper': { + width: (theme) => ({ + xs: '90vw', + md: `min(${theme.typography.pxToRem(370)}, 90vw)`, + }), + maxHeight: (theme) => + `min(${theme.typography.pxToRem(450)}, calc(100vh - 150px))`, + paddingBlockEnd: 3, + display: 'flex', + flexDirection: 'column', + [theme.breakpoints.down('md')]: { + left: '0 !important', + right: 0, + marginInline: 'auto', + }, + }, + }} + transitionDuration={theme.transitions.duration.shortest} + > + + {intl.formatMessage({ defaultMessage: 'Recent activity' })} + + + {!transactions.length ? ( + + {intl.formatMessage({ + defaultMessage: 'No transaction activity', + })} + + ) : ( + + {transactions.map((transaction, index) => ( + + ))} + + )} + {transactions.length ? : undefined} + + + ); +} diff --git a/libs/shared/components/src/top-nav/ConnectedButton.stories.tsx b/libs/shared/components/src/top-nav/ConnectedButton.stories.tsx new file mode 100644 index 000000000..3aab01b33 --- /dev/null +++ b/libs/shared/components/src/top-nav/ConnectedButton.stories.tsx @@ -0,0 +1,93 @@ +import { within } from '@storybook/testing-library'; + +import { ConnectedButton } from './ConnectedButton'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + component: ConnectedButton, + title: 'Top navigation/User connected', + args: { + userId: '0x65b033bcc4d7f74255bbde2f69966e85', + walletIcon: 'https://app.oeth.com/images/walletconnect-icon.svg', + values: [ + { + token: 'eth', + quantity: 18639.418285, + tokenIcon: 'https://app.oeth.com/images/eth.svg', + }, + { + token: 'weth', + quantity: 1639.418285, + tokenIcon: 'https://app.oeth.com/images/weth.png', + }, + { + token: 'reth', + quantity: 639.418285, + tokenIcon: 'https://app.oeth.com/images/reth.png', + }, + { + token: 'frxeth', + quantity: 39.418, + tokenIcon: ' https://app.oeth.com/images/frxeth.svg', + }, + { + token: 'sfrxeth', + quantity: 23639.415, + tokenIcon: ' https://app.oeth.com/images/sfrxeth.svg', + }, + { + token: 'steth', + quantity: 2639.415, + tokenIcon: ' https://app.oeth.com/images/steth.svg', + }, + ], + }, +}; +export default meta; + +export const Default: StoryObj = {}; + +export const Expanded: StoryObj = { + name: 'User clicked on the profile button', + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.getByTestId('connect-button').click(); + }, +}; + +export const SmallMobile: StoryObj = { + parameters: { + viewport: { + defaultViewport: 'mobile1', + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.getByTestId('connect-button').click(); + }, +}; + +export const LargeMobile: StoryObj = { + parameters: { + viewport: { + defaultViewport: 'mobile2', + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.getByTestId('connect-button').click(); + }, +}; + +export const Tablet: StoryObj = { + parameters: { + viewport: { + defaultViewport: 'tablet', + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.getByTestId('connect-button').click(); + }, +}; diff --git a/libs/shared/components/src/top-nav/ConnectedButton.tsx b/libs/shared/components/src/top-nav/ConnectedButton.tsx new file mode 100644 index 000000000..e56767c98 --- /dev/null +++ b/libs/shared/components/src/top-nav/ConnectedButton.tsx @@ -0,0 +1,214 @@ +import { useState } from 'react'; + +import { + alpha, + Box, + Button, + Divider, + Popover, + Stack, + Typography, + useTheme, +} from '@mui/material'; +import { useIntl } from 'react-intl'; + +import { LinkIcon } from '../LinkIcon/LinkIcon'; +import { Icon } from './Icon'; +import { UserId } from './UserId'; +import { styles } from './utils'; + +import type { ButtonProps, SxProps, Theme } from '@mui/material'; + +import type { Connected } from './types'; + +const padding = { paddingBlock: 1.8, paddingInline: 2 }; + +interface Props extends Pick { + onDisconnect?: () => void; +} + +export function ConnectedButton({ + userId, + walletIcon, + values, + onDisconnect, +}: Props) { + const theme = useTheme(); + const intl = useIntl(); + const [anchor, setAnchor] = useState(null); + return ( + <> + setAnchor(e.currentTarget)} + sx={{ + [theme.breakpoints.down('md')]: { + '& p': { + display: 'none', + }, + }, + }} + > + theme.spacing(3), + height: (theme) => theme.spacing(3), + }} + /> + + + setAnchor(null)} + anchorOrigin={{ + vertical: 50, + horizontal: 'right', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + sx={{ + '& .MuiPopover-paper': { + width: (theme) => ({ + xs: '90vw', + md: `min(${theme.typography.pxToRem(250)}, 90vw)`, + }), + [theme.breakpoints.down('md')]: { + left: '0 !important', + right: 0, + marginInline: 'auto', + }, + }, + }} + > + + + Account + + + + + + + + + + + {values.map((value) => ( + + + {intl.formatNumber(value.quantity)} +   + {value.token} + + ))} + + + + + ); +} + +interface ConnectButtonProps extends ButtonProps { + connected: boolean; +} + +export function ConnectButton({ + onClick, + children, + connected, + sx, + ...rest +}: ConnectButtonProps) { + return ( + + ); +} diff --git a/libs/shared/components/src/top-nav/Icon.tsx b/libs/shared/components/src/top-nav/Icon.tsx new file mode 100644 index 000000000..24879cba7 --- /dev/null +++ b/libs/shared/components/src/top-nav/Icon.tsx @@ -0,0 +1,18 @@ +import { Box } from '@mui/material'; + +import type { BoxProps } from '@mui/material'; + +interface Props extends BoxProps { + src: string; +} + +export function Icon({ src, sx, ...rest }: Props) { + return ( + theme.typography.pxToRem(20), ...sx }} + {...rest} + /> + ); +} diff --git a/libs/shared/components/src/top-nav/TopNav.stories.tsx b/libs/shared/components/src/top-nav/TopNav.stories.tsx new file mode 100644 index 000000000..1563f574e --- /dev/null +++ b/libs/shared/components/src/top-nav/TopNav.stories.tsx @@ -0,0 +1,161 @@ +import { within } from '@storybook/testing-library'; + +import { TopNav } from './TopNav'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const logo = 'https://app.oeth.com/images/origin-ether-logo.svg'; +const tabs = ['Swap', 'Wrap', 'History']; + +const connected = { + logo, + tabs, + selected: 0, + connected: true, + ipfsLink: 'https://oeth.on.fleek.co/', + userId: '0x65b033bcc4d7f74255bbde2f69966e85', + walletIcon: 'https://app.oeth.com/images/walletconnect-icon.svg', + transactions: [], + values: [ + { + token: 'eth', + quantity: 18639.418285, + tokenIcon: 'https://app.oeth.com/images/eth.svg', + }, + { + token: 'weth', + quantity: 1639.418285, + tokenIcon: 'https://app.oeth.com/images/weth.png', + }, + { + token: 'reth', + quantity: 639.418285, + tokenIcon: 'https://app.oeth.com/images/reth.png', + }, + { + token: 'frxeth', + quantity: 39.418, + tokenIcon: ' https://app.oeth.com/images/frxeth.svg', + }, + { + token: 'sfrxeth', + quantity: 23639.415, + tokenIcon: ' https://app.oeth.com/images/sfrxeth.svg', + }, + { + token: 'steth', + quantity: 2639.415, + tokenIcon: ' https://app.oeth.com/images/steth.svg', + }, + ], +} as const; + +const meta: Meta = { + component: TopNav, + title: 'Top navigation/Navigation bar', + args: { + logo, + tabs, + selected: 0, + ipfsLink: 'https://oeth.on.fleek.co/', + }, +}; +export default meta; + +export const Default: StoryObj = {}; +export const HoverItem: StoryObj = { + parameters: { + pseudo: { + hover: [ + '[href="/wrap"]', + '[href="https://oeth.on.fleek.co/"]', + '[data-testid="connect-button"]', + ], + }, + }, +}; + +export const UserConnected: StoryObj = { + // @ts-expect-error values mismatch + args: connected, +}; + +export const UserConnectedPopover: StoryObj = { + name: 'Profile button clicked', + // @ts-expect-error values mismatch + args: connected, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.getByTestId('connect-button').click(); + }, +}; + +export const ActivityPopover: StoryObj = { + name: 'Transaction popover clicked', + // @ts-expect-error values mismatch + args: connected, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.getByTestId('activity-button').click(); + }, +}; + +export const SmallMobile: StoryObj = { + name: 'Small mobile with user disconnected', + parameters: { + viewport: { + defaultViewport: 'mobile1', + }, + }, +}; + +export const SmallMobileConnected: StoryObj = { + name: 'Small mobile with user connected', + parameters: { + viewport: { + defaultViewport: 'mobile1', + }, + }, + // @ts-expect-error values mismatch + args: connected, +}; + +export const LargeMobile: StoryObj = { + name: 'Large mobile with user disconnected', + parameters: { + viewport: { + defaultViewport: 'mobile2', + }, + }, +}; + +export const LargeMobileConnected: StoryObj = { + name: 'Large mobile with user connected', + parameters: { + viewport: { + defaultViewport: 'mobile2', + }, + }, + // @ts-expect-error values mismatch + args: connected, +}; + +export const Tablet: StoryObj = { + name: 'Tablet with user disconnected', + parameters: { + viewport: { + defaultViewport: 'tablet', + }, + }, +}; + +export const TabletConnected: StoryObj = { + name: 'Tablet with user connected', + parameters: { + viewport: { + defaultViewport: 'tablet', + }, + }, + // @ts-expect-error values mismatch + args: connected, +}; diff --git a/libs/shared/components/src/top-nav/TopNav.tsx b/libs/shared/components/src/top-nav/TopNav.tsx new file mode 100644 index 000000000..48b957740 --- /dev/null +++ b/libs/shared/components/src/top-nav/TopNav.tsx @@ -0,0 +1,230 @@ +import { useState } from 'react'; + +import { + alpha, + Box, + Divider, + Link as MuiLink, + Tab, + Tabs, + useTheme, +} from '@mui/material'; +import { useIntl } from 'react-intl'; +import { Link } from 'react-router-dom'; + +import { Activity } from './Activity'; +import { ConnectButton, ConnectedButton } from './ConnectedButton'; +import { styles } from './utils'; + +import type { SxProps } from '@mui/material'; + +import type { Connected } from './types'; + +type Props = { + tabs: string[]; + logo: string; + selected: number; + ipfsLink: string; + sx?: SxProps; +} & ({ connected: false } | Connected); + +export function TopNav({ + sx, + tabs, + logo, + selected, + ipfsLink, + connected = false, + ...rest +}: Props) { + const theme = useTheme(); + const intl = useIntl(); + const [value, setValue] = useState(selected); + return ( + + ({ + '& img': { + maxHeight: { + xs: '1rem', + md: '1.5rem', + }, + maxWidth: { + xs: theme.typography.pxToRem(100), + sm: theme.typography.pxToRem(120), + md: theme.typography.pxToRem(180), + }, + }, + })} + onClick={() => setValue(0)} + > + + + setValue(value)} + sx={{ + order: { + xs: 2, + md: 0, + }, + gridColumn: { + xs: 'span 2', + md: 'span 1', + }, + backgroundColor: 'transparent', + minHeight: 0, + overflow: 'visible', + '& .MuiTabs-fixed': { + overflow: 'visible !important', + }, + fontSize: { + xs: '0.875rem', + md: '1rem', + }, + '& .MuiTabs-flexContainer': { + justifyContent: { + xs: 'center', + md: 'flex-start', + }, + }, + }} + value={value} + > + {tabs.map((tab) => ( + + `linear-gradient(90deg, ${alpha( + theme.palette.primary.main, + 0.4, + )} 0%, ${alpha(theme.palette.primary.dark, 0.4)} 100%)`, + position: 'absolute', + left: 0, + bottom: 0, + zIndex: 2, + }, + }} + > + ))} + + + a': { + fontSize: { + xs: '0.75rem', + md: '1rem', + }, + color: (theme) => theme.palette.primary.contrastText, + lineHeight: (theme) => theme.spacing(3), + }, + }} + > + theme.palette.background.paper, + backgroundImage: 'none', + }, + }} + > + {theme.breakpoints.down('md') + ? intl.formatMessage({ defaultMessage: 'IPFS' }) + : intl.formatMessage({ defaultMessage: 'View on IPFS' })} + + {connected ? ( + + ) : ( + + {intl.formatMessage({ defaultMessage: 'Connect' })} + + )} + {connected ? ( + + ) : undefined} + + + + ); +} diff --git a/libs/shared/components/src/top-nav/Transaction.stories.tsx b/libs/shared/components/src/top-nav/Transaction.stories.tsx new file mode 100644 index 000000000..c08e55be7 --- /dev/null +++ b/libs/shared/components/src/top-nav/Transaction.stories.tsx @@ -0,0 +1,92 @@ +import { Box } from '@mui/material'; + +import { transactions } from './fixtures'; +import { Transaction } from './Transaction'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + component: Transaction, + title: 'Top navigation/Transaction', + render: (args) => ( + + + + ), +}; +export default meta; + +export const ApprovalSuccess: StoryObj = { + args: { + // @ts-expect-error type mismatch + transaction: transactions[0], + }, +}; +export const ApprovalPending: StoryObj = { + args: { + // @ts-expect-error type mismatch + transaction: transactions[1], + }, +}; + +export const ApprovalFailed: StoryObj = { + args: { + // @ts-expect-error type mismatch + transaction: transactions[2], + }, +}; +export const RebaseSuccess: StoryObj = { + args: { + // @ts-expect-error type mismatch + transaction: transactions[3], + }, +}; +export const RebasePending: StoryObj = { + args: { + // @ts-expect-error type mismatch + transaction: transactions[4], + }, +}; +export const RebaseFailed: StoryObj = { + args: { + // @ts-expect-error type mismatch + transaction: transactions[5], + }, +}; + +export const SwapSuccess: StoryObj = { + args: { + // @ts-expect-error type mismatch + transaction: transactions[6], + }, +}; +export const SwapFailed: StoryObj = { + args: { + // @ts-expect-error type mismatch + transaction: transactions[7], + }, +}; +export const SwapPending: StoryObj = { + args: { + // @ts-expect-error type mismatch + transaction: transactions[8], + }, +}; +export const RedeemPending: StoryObj = { + args: { + // @ts-expect-error type mismatch + transaction: transactions[9], + }, +}; +export const RedeemSuccess: StoryObj = { + args: { + // @ts-expect-error type mismatch + transaction: transactions[10], + }, +}; +export const RedeemFailed: StoryObj = { + args: { + // @ts-expect-error type mismatch + transaction: transactions[11], + }, +}; diff --git a/libs/shared/components/src/top-nav/Transaction.tsx b/libs/shared/components/src/top-nav/Transaction.tsx new file mode 100644 index 000000000..2ec8ada3d --- /dev/null +++ b/libs/shared/components/src/top-nav/Transaction.tsx @@ -0,0 +1,127 @@ +import { keyframes } from '@emotion/react'; +import { Stack, Typography } from '@mui/material'; +import { useIntl } from 'react-intl'; + +import { LinkIcon } from '../LinkIcon/LinkIcon'; +import { Icon } from './Icon'; +import { messages } from './utils'; + +import type { Approval, Rebase, Redeem, Swap, Transaction } from './types'; + +const spin = keyframes` + 0 { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +`; + +interface Props { + transaction: Transaction; +} + +export function Transaction({ transaction }: Props) { + const intl = useIntl(); + return ( + + + + theme.typography.pxToRem(14), + ...(transaction.status === 'pending' + ? { + animation: `${spin} 3s linear infinite`, + } + : {}), + }} + /> + {intl.formatMessage( + messages[`${transaction.type}-${transaction.status}`], + )} + + + + {transaction.type === 'approval' && transaction.status === 'success' + ? intl.formatMessage(messages['approval-success-message'], { + token: transaction.token, + }) + : transaction.type === 'approval' + ? intl.formatMessage(messages['approval-message'], { + token: transaction.token, + }) + : transaction.type === 'rebase' && transaction.status === 'pending' + ? intl.formatMessage(messages['rebase-pending-message']) + : transaction.type === 'rebase' + ? intl.formatMessage(messages['rebase-message']) + : intl.formatMessage(messages['swap-message'], { + baseTokenValue: intl.formatNumber( + transaction.baseTokenQuantity, + ), + baseToken: transaction.baseToken, + exchangeTokenValue: intl.formatNumber( + transaction.exchangeTokenQuantity, + ), + exchangeToken: transaction.exchangeToken, + })} + + + theme.palette.grey['900'], + }} + direction="row" + gap={1} + alignItems="center" + > + {transaction.type === 'swap' || transaction.type === 'redeem' ? ( + <> + theme.typography.pxToRem(24), + }} + /> + theme.typography.pxToRem(8) }} + /> + theme.typography.pxToRem(24), + }} + /> + + ) : ( + theme.typography.pxToRem(24) }} + /> + )} + + + ); +} diff --git a/libs/shared/components/src/top-nav/UserId.tsx b/libs/shared/components/src/top-nav/UserId.tsx new file mode 100644 index 000000000..ab547c380 --- /dev/null +++ b/libs/shared/components/src/top-nav/UserId.tsx @@ -0,0 +1,16 @@ +import { Typography } from '@mui/material'; + +import type { TypographyProps } from '@mui/material'; + +interface Props extends TypographyProps { + userId: string; +} + +export function UserId({ userId, sx, ...rest }: Props) { + return ( + + {userId.slice(0, 4)}... + {userId.slice(-4)} + + ); +} diff --git a/libs/shared/components/src/top-nav/fixtures.ts b/libs/shared/components/src/top-nav/fixtures.ts new file mode 100644 index 000000000..66f9e4e44 --- /dev/null +++ b/libs/shared/components/src/top-nav/fixtures.ts @@ -0,0 +1,115 @@ +import { faker } from '@faker-js/faker'; + +export const transactions = [ + { + token: 'stETH', + type: 'approval', + status: 'success', + url: faker.internet.url(), + tokenIcon: 'https://oeth.on.fleek.co/images/currency/steth-icon-small.svg', + }, + { + token: 'stETH', + type: 'approval', + status: 'pending', + url: faker.internet.url(), + tokenIcon: 'https://oeth.on.fleek.co/images/currency/steth-icon-small.svg', + }, + { + token: 'stETH', + type: 'approval', + status: 'failed', + url: faker.internet.url(), + tokenIcon: 'https://oeth.on.fleek.co/images/currency/steth-icon-small.svg', + }, + { + token: 'OETH', + type: 'rebase', + status: 'success', + url: faker.internet.url(), + tokenIcon: 'https://app.oeth.com/images/oeth.svg', + }, + { + token: 'OETH', + type: 'rebase', + status: 'pending', + url: faker.internet.url(), + tokenIcon: 'https://app.oeth.com/images/oeth.svg', + }, + { + token: 'OETH', + type: 'rebase', + status: 'failed', + url: faker.internet.url(), + tokenIcon: 'https://app.oeth.com/images/oeth.svg', + }, + { + baseToken: 'stETH', + baseTokenQuantity: 4.7713, + type: 'swap', + status: 'success', + url: faker.internet.url(), + exchangeToken: 'OETH', + exchangeTokenQuantity: 4.815, + exchangeTokenIcon: 'https://app.oeth.com/images/oeth.svg', + baseTokenIcon: 'https://app.oeth.com/images/currency/steth-icon-small.svg', + }, + { + baseToken: 'stETH', + baseTokenQuantity: 4.7713, + type: 'swap', + status: 'failed', + url: faker.internet.url(), + exchangeToken: 'OETH', + exchangeTokenQuantity: 4.815, + exchangeTokenIcon: 'https://app.oeth.com/images/oeth.svg', + baseTokenIcon: 'https://app.oeth.com/images/currency/steth-icon-small.svg', + }, + { + baseToken: 'stETH', + baseTokenQuantity: 4.7713, + type: 'swap', + status: 'pending', + url: faker.internet.url(), + exchangeToken: 'OETH', + exchangeTokenQuantity: 4.815, + exchangeTokenIcon: 'https://app.oeth.com/images/oeth.svg', + baseTokenIcon: 'https://app.oeth.com/images/currency/steth-icon-small.svg', + }, + { + exchangeToken: 'stETH', + exchangeTokenQuantity: 4.7713, + type: 'redeem', + status: 'pending', + url: faker.internet.url(), + baseToken: 'OETH', + baseTokenQuantity: 4.815, + baseTokenIcon: 'https://app.oeth.com/images/oeth.svg', + exchangeTokenIcon: + 'https://app.oeth.com/images/currency/steth-icon-small.svg', + }, + { + exchangeToken: 'stETH', + exchangeTokenQuantity: 4.7713, + type: 'redeem', + status: 'success', + url: faker.internet.url(), + baseToken: 'OETH', + baseTokenQuantity: 4.815, + baseTokenIcon: 'https://app.oeth.com/images/oeth.svg', + exchangeTokenIcon: + 'https://app.oeth.com/images/currency/steth-icon-small.svg', + }, + { + exchangeToken: 'stETH', + exchangeTokenQuantity: 4.7713, + type: 'redeem', + status: 'failed', + url: faker.internet.url(), + baseToken: 'OETH', + baseTokenQuantity: 4.815, + baseTokenIcon: 'https://app.oeth.com/images/oeth.svg', + exchangeTokenIcon: + 'https://app.oeth.com/images/currency/steth-icon-small.svg', + }, +]; diff --git a/libs/shared/components/src/top-nav/index.tsx b/libs/shared/components/src/top-nav/index.tsx new file mode 100644 index 000000000..0b5f66e91 --- /dev/null +++ b/libs/shared/components/src/top-nav/index.tsx @@ -0,0 +1,2 @@ +export * from './TopNav'; +export * from './Icon'; diff --git a/libs/shared/components/src/top-nav/types.ts b/libs/shared/components/src/top-nav/types.ts new file mode 100644 index 000000000..9b49005ac --- /dev/null +++ b/libs/shared/components/src/top-nav/types.ts @@ -0,0 +1,45 @@ +export interface Connected { + connected: true; + userAvatar: string; + userId: string; + walletIcon: string; + values: { token: string; tokenIcon: string; quantity: number }[]; + transactions: Transaction[]; +} + +export type Transaction = { + status: 'pending' | 'failed' | 'success'; + url: string; +} & (Approval | Swap | Redeem | Rebase); + +export interface Approval { + token: string; + tokenIcon: string; + type: 'approval'; +} + +export interface Swap { + baseToken: string; + baseTokenQuantity: number; + baseTokenIcon: string; + exchangeToken: string; + exchangeTokenQuantity: number; + exchangeTokenIcon: string; + type: 'swap'; +} + +export interface Redeem { + baseToken: string; + baseTokenQuantity: number; + baseTokenIcon: string; + exchangeToken: string; + exchangeTokenQuantity: number; + exchangeTokenIcon: string; + type: 'redeem'; +} + +export interface Rebase { + type: 'rebase'; + token: string; + tokenIcon: string; +} diff --git a/libs/shared/components/src/top-nav/utils.ts b/libs/shared/components/src/top-nav/utils.ts new file mode 100644 index 000000000..67eb42df6 --- /dev/null +++ b/libs/shared/components/src/top-nav/utils.ts @@ -0,0 +1,41 @@ +import { defineMessage } from 'react-intl'; + +import type { SxProps, Theme } from '@mui/material'; + +export const styles: SxProps = { + backgroundColor: 'background.paper', + borderRadius: 25, + paddingBlock: 1, + paddingInline: { xs: 2, md: 3 }, + color: 'primary.contrastText', + boxShadow: (theme) => theme.shadows['24'], +}; + +export const messages = { + 'approval-pending': defineMessage({ defaultMessage: 'Pending Approval' }), + 'approval-success': defineMessage({ defaultMessage: 'Approved' }), + 'approval-failed': defineMessage({ defaultMessage: 'Approval failed' }), + 'swap-pending': defineMessage({ defaultMessage: 'Pending Swap' }), + 'swap-success': defineMessage({ defaultMessage: 'Swapped' }), + 'swap-failed': defineMessage({ defaultMessage: 'Swap failed' }), + 'redeem-success': defineMessage({ defaultMessage: 'Redeemed' }), + 'redeem-pending': defineMessage({ defaultMessage: 'Pending Redeem' }), + 'redeem-failed': defineMessage({ defaultMessage: 'Redeem failed' }), + 'rebase-success': defineMessage({ defaultMessage: 'Rebased opt-in' }), + 'rebase-failed': defineMessage({ defaultMessage: 'Rebase opt-in Failed' }), + 'rebase-pending': defineMessage({ defaultMessage: 'Rebase opt-in Pending' }), + 'swap-message': defineMessage({ + defaultMessage: + '{baseTokenValue} {baseToken} for {exchangeTokenValue} {exchangeToken}', + }), + 'approval-message': defineMessage({ + defaultMessage: 'Approve {token} for swapping', + }), + 'approval-success-message': defineMessage({ + defaultMessage: 'Approved {token} for swapping', + }), + 'rebase-message': defineMessage({ defaultMessage: 'To receive yield' }), + 'rebase-pending-message': defineMessage({ + defaultMessage: 'You will receive yield', + }), +}; diff --git a/libs/shared/components/tsconfig.json b/libs/shared/components/tsconfig.json new file mode 100644 index 000000000..d29cb83ed --- /dev/null +++ b/libs/shared/components/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "types": ["vitest"] + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + }, + { + "path": "./tsconfig.storybook.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/libs/shared/components/tsconfig.lib.json b/libs/shared/components/tsconfig.lib.json new file mode 100644 index 000000000..a9c281594 --- /dev/null +++ b/libs/shared/components/tsconfig.lib.json @@ -0,0 +1,26 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["node"] + }, + "files": [ + "../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../node_modules/@nx/react/typings/image.d.ts" + ], + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx", + "**/*.stories.ts", + "**/*.stories.js", + "**/*.stories.jsx", + "**/*.stories.tsx" + ], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/libs/shared/components/tsconfig.spec.json b/libs/shared/components/tsconfig.spec.json new file mode 100644 index 000000000..6d3be7427 --- /dev/null +++ b/libs/shared/components/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"] + }, + "include": [ + "vite.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/libs/shared/components/tsconfig.storybook.json b/libs/shared/components/tsconfig.storybook.json new file mode 100644 index 000000000..3555ddd4f --- /dev/null +++ b/libs/shared/components/tsconfig.storybook.json @@ -0,0 +1,31 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "emitDecoratorMetadata": true, + "outDir": "" + }, + "files": [ + "../../../node_modules/@nx/react/typings/styled-jsx.d.ts", + "../../../node_modules/@nx/react/typings/cssmodule.d.ts", + "../../../node_modules/@nx/react/typings/image.d.ts" + ], + "exclude": [ + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.jsx", + "src/**/*.test.js" + ], + "include": [ + "src/**/*.stories.ts", + "src/**/*.stories.js", + "src/**/*.stories.jsx", + "src/**/*.stories.tsx", + "src/**/*.stories.mdx", + ".storybook/*.js", + ".storybook/*.ts" + ] +} diff --git a/libs/shared/components/vite.config.ts b/libs/shared/components/vite.config.ts new file mode 100644 index 000000000..98ff22e76 --- /dev/null +++ b/libs/shared/components/vite.config.ts @@ -0,0 +1,36 @@ +/// +/// +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; +import svgr from 'vite-plugin-svgr'; +import viteTsConfigPaths from 'vite-tsconfig-paths'; + +export default defineConfig({ + cacheDir: '../../../node_modules/.vite/shared-components', + + plugins: [ + svgr(), + react(), + viteTsConfigPaths({ + root: '../../../', + }), + ], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ + // viteTsConfigPaths({ + // root: '../../../', + // }), + // ], + // }, + + test: { + globals: true, + cache: { + dir: '../../../node_modules/.vitest', + }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + }, +}); diff --git a/libs/shared/storybook/.storybook/main.ts b/libs/shared/storybook/.storybook/main.ts index 3ab09e8d5..15142ba69 100644 --- a/libs/shared/storybook/.storybook/main.ts +++ b/libs/shared/storybook/.storybook/main.ts @@ -2,7 +2,12 @@ import type { StorybookConfig } from '@storybook/react-vite'; const config: StorybookConfig = { stories: ['../../../**/*.stories.@(js|jsx|ts|tsx|mdx)'], - addons: ['@storybook/addon-essentials'], + addons: [ + '@storybook/addon-essentials', + 'storybook-addon-pseudo-states', + '@storybook/addon-interactions', + ], + staticDirs: [{ from: '../../assets/files', to: '/images' }], framework: { name: '@storybook/react-vite', options: { diff --git a/libs/shared/storybook/.storybook/preview-head.html b/libs/shared/storybook/.storybook/preview-head.html new file mode 100644 index 000000000..293349bfa --- /dev/null +++ b/libs/shared/storybook/.storybook/preview-head.html @@ -0,0 +1,8 @@ + + + + + diff --git a/libs/shared/storybook/src/decorators.tsx b/libs/shared/storybook/src/decorators.tsx index 49e5085c7..40792ea49 100644 --- a/libs/shared/storybook/src/decorators.tsx +++ b/libs/shared/storybook/src/decorators.tsx @@ -1,23 +1,39 @@ -import type { Preview } from '@storybook/react'; import * as React from 'react'; -import { IntlProvider } from 'react-intl'; + import { Experimental_CssVarsProvider as CssVarsProvider } from '@mui/material'; import { theme } from '@origin/shared/theme'; +import { IntlProvider } from 'react-intl'; +import { MemoryRouter } from 'react-router-dom'; + +import type { Preview } from '@storybook/react'; export const decorators: Preview['decorators'] = [ (StoryComponent) => { return ( - // - - - - // + + + + + + + ); }, ]; const preview: Preview = { decorators, + parameters: { + backgrounds: { + default: 'dark', + values: [ + { + name: 'dark', + value: '#141519', + }, + ], + }, + }, }; export default preview; diff --git a/libs/shared/storybook/vite.config.ts b/libs/shared/storybook/vite.config.ts index e26084052..3692a959a 100644 --- a/libs/shared/storybook/vite.config.ts +++ b/libs/shared/storybook/vite.config.ts @@ -1,10 +1,13 @@ /// -import { defineConfig } from 'vite'; +/// import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; +import svgr from 'vite-plugin-svgr'; import viteTsConfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ plugins: [ + svgr(), react({ babel: { plugins: [ diff --git a/libs/shared/theme/src/Palette.stories.tsx b/libs/shared/theme/src/Palette.stories.tsx new file mode 100644 index 000000000..c9d3a8015 --- /dev/null +++ b/libs/shared/theme/src/Palette.stories.tsx @@ -0,0 +1,195 @@ +import { Box, capitalize, Stack, Typography, useTheme } from '@mui/material'; + +import type { + BoxProps, + Palette, + SimplePaletteColorOptions, + StackProps, +} from '@mui/material'; +import type { Meta, StoryObj } from '@storybook/react'; + +const isColor = (value: unknown) => + typeof value === 'string' && value.match(/^(#|rgba?)/); + +const isGradient = (value: unknown) => + typeof value === 'string' && value.match(/^linear-gradient/); + +const meta: Meta = { + title: 'Origin Theme/Palette', +}; + +export interface PaletteViewProps extends StackProps { + palette: Palette; +} + +interface PaletteElemProps extends BoxProps { + keyStr: string; + value: string | number; + contrastBkg?: string; +} + +const PaletteElem = ({ + keyStr, + value, + contrastBkg, + ...rest +}: PaletteElemProps) => { + const theme = useTheme(); + const bkgColor = contrastBkg + ? contrastBkg + : isColor(value) || isGradient(value) + ? value + : '#FFF'; + let valueStr = ['string', 'number'].includes(typeof value) ? value : ''; + if ( + typeof value === 'number' || + typeof value === 'function' || + keyStr.endsWith('Channel') + ) + return <>; + if (value.split(' ').length === 3) + valueStr = `rbg(${value.split(' ').join(',')})`; + + return ( + + + + {keyStr} + + + {valueStr} + + + + {contrastBkg && ( + + T + + )} + {/* {!value && <>} */} + {!isColor(value) && !isGradient(value) && ( + + {capitalize(typeof value)} value + + )} + + + ); +}; + +const PaletteView = ({ palette, ...rest }: PaletteViewProps) => ( + + {Object.keys(palette) + .filter( + (key) => + ![ + 'mode', + 'contrastThreshold', + 'getContrastText', + 'augmentColor', + 'tonalOffset', + 'Alert', + 'AppBar', + 'Avatar', + 'Button', + 'Chip', + 'FilledInput', + 'LinearProgress', + 'Skeleton', + 'Slider', + 'SnackbarContent', + 'SpeedDialAction', + 'StepConnector', + 'StepContent', + 'Switch', + 'TableCell', + 'Tooltip', + 'dividerChannel', + 'colorScheme', + ].includes(key), + ) + .map((key: string) => ( + + + {capitalize(key)} + + {typeof palette[key] === 'object' ? ( + Object.keys(palette[key]).map((k) => ( + + )) + ) : ( + + )} + + ))} + +); + +export default meta; + +const Container = () => { + const { palette } = useTheme(); + return ; +}; + +export const Default: StoryObj = { + render: (args) => , + args: { defaultMode: 'dark' }, +}; diff --git a/libs/shared/theme/src/theme.tsx b/libs/shared/theme/src/theme.tsx index 7ea8b17cc..9b90ffdd0 100644 --- a/libs/shared/theme/src/theme.tsx +++ b/libs/shared/theme/src/theme.tsx @@ -1,14 +1,26 @@ -import { experimental_extendTheme as extendTheme } from '@mui/material/styles'; +import { + alpha, + experimental_extendTheme as extendTheme, +} from '@mui/material/styles'; +import shadows from '@mui/material/styles/shadows'; declare module '@mui/material/styles' { interface TypeBackground { gradient1: string; gradient2: string; + gradient3: string; + gradientSuccess: string; + gradientHover: string; + gradientHoverActionButton: string; } interface TypeBackgroundOptions { gradient1: string; gradient2: string; + gradient3: string; + gradientSuccess: string; + gradientHover: string; + gradientHoverActionButton: string; } interface Shape { @@ -25,25 +37,74 @@ export const theme = extendTheme({ dark: { palette: { primary: { - main: 'rgb(130, 134, 153)', - contrastText: '#fff', + main: '#8c66fc', + dark: '#0274f1', + light: '#b361e6', + contrastText: '#FAFBFB', }, - divider: '#141519', + divider: '#101113', background: { - paper: 'rgb(30, 31, 37)', - default: '#141519', + paper: '#1E1F25', + default: '#101113', + // TODO cleanup these gradients after theme is properly configured -> gradients can be generated based on css vars gradient1: 'linear-gradient(90deg,#8c66fc -28.99%,#0274f1 144.97%)', - gradient2: 'linear-gradient(90deg,#b361e6 -28.99%,#6a36fc 144.97%)', + gradient2: 'linear-gradient(90deg, #8C66FC 0%, #0274F1 100%)', + gradient3: + 'linear-gradient(90deg, rgb(179, 97, 230) 20.29%, rgb(106, 54, 252) 79.06%)', + gradientSuccess: + 'linear-gradient(97.67deg, rgb(102, 254, 144) -10.09%, rgb(102, 217, 254) 120.99%)', + gradientHover: + 'linear-gradient(0deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.05) 100%), #1E1F25', + gradientHoverActionButton: + 'linear-gradient(0deg, rgba(0, 0, 0, 0.20) 0%, rgba(0, 0, 0, 0.20) 100%), linear-gradient(90deg, #8C66FC 0%, #0274F1 100%)', + }, + action: { + hoverOpacity: 0.1, + disabledOpacity: 0.3, + }, + text: { + primary: '#828699', + secondary: '#BABDCC', + }, + grey: { + 200: '#B5BECA', + 400: '#515466', + 500: '#252833', + 700: '#141519', + 800: '#282A32', + 900: '#18191C', + }, + warning: { + main: '#FFDC86', + }, + error: { + main: '#FF8686', }, }, }, }, typography: { - fontFamily: 'Inter, Helvetica, Arial, sans-serif', + fontFamily: 'Inter, Sailec, Helvetica, Arial, sans-serif', + body1: { + fontSize: '0.875rem', + lineHeight: '1.4375rem', + }, + body2: { + fontSize: '0.75rem', + fontWeight: 400, + lineHeight: '1.25rem', + }, }, shape: { - borderRadius: 11, + borderRadius: 4, }, + shadows: [ + // @ts-expect-error remove one box shadow + ...shadows.slice(0, -2), + '0px 6px 12px 0px rgba(0, 0, 0, 0.20)', + 'rgba(0, 0, 0, 0.25) 0px 4px 4px 0px', + '0px 1.7955275774002075px 5.32008171081543px 0px rgba(0, 0, 0, 0.03), 0px 6.030803203582764px 17.869047164916992px 0px rgba(0, 0, 0, 0.04), 0px 27px 80px 0px rgba(0, 0, 0, 0.07)', + ], components: { MuiButton: { styleOverrides: { @@ -54,29 +115,51 @@ export const theme = extendTheme({ color: theme.palette.primary.contrastText, background: theme.palette.background.gradient1, }), + containedSecondary: ({ theme }) => ({ + color: theme.palette.primary.contrastText, + background: alpha(theme.palette.common.white, 0.1), + boxShadow: 'none', + '&:hover': { + background: theme.palette.background.paper, + }, + }), + }, + defaultProps: { + disableTouchRipple: true, + }, + }, + MuiIconButton: { + defaultProps: { + disableTouchRipple: true, + }, + }, + MuiCard: { + styleOverrides: { + root: { + backgroundImage: 'none', + }, }, }, MuiTab: { styleOverrides: { root: ({ theme }) => ({ + minHeight: 0, + paddingBlock: theme.spacing(1), + paddingInline: theme.spacing(2.5), '&.Mui-selected': { - zIndex: 2, color: theme.palette.primary.contrastText, }, }), }, + defaultProps: { + disableRipple: true, + disableTouchRipple: true, + }, }, MuiTabs: { styleOverrides: { - root: ({ theme }) => ({ - backgroundColor: theme.palette.background.paper, - borderRadius: theme.shape.borderRadius * 5, - }), indicator: ({ theme }) => ({ - height: '100%', background: theme.palette.background.gradient2, - zIndex: 1, - borderRadius: theme.shape.borderRadius * 5, }), }, }, @@ -93,7 +176,7 @@ export const theme = extendTheme({ root: ({ theme }) => ({ '&.Mui-selected': { backgroundColor: 'transparent', - color: theme.palette.primary.main, + color: theme.palette.text.secondary, '&:hover': { backgroundColor: theme.palette.grey[800], }, @@ -118,37 +201,103 @@ export const theme = extendTheme({ MuiFormControl: { styleOverrides: { root: { - position: 'static' - } - } + position: 'static', + }, + }, }, MuiFormLabel: { styleOverrides: { - root: { - '&.MuiInputLabel-root':{ + root: ({ theme }) => ({ + '&.MuiInputLabel-root': { position: 'static', transform: 'none', - transformOrigin: 'none', - fontSize: '0.75rem', - marginBlockEnd: '0.25rem' - } - } - } + transformOrigin: 'initial', + fontSize: theme.typography.pxToRem(14), + marginBlockEnd: '0.5rem', + color: `${theme.palette.text.primary}`, + }, + }), + }, }, MuiInputBase: { styleOverrides: { - root: ({theme})=>({ - borderRadius: 40, + root: ({ theme }) => ({ + borderRadius: 20, border: '1px solid', - borderColor:theme.palette.info.main, - fontSize: '1rem', + borderColor: theme.palette.info.main, backgroundColor: theme.palette.background.default, width: 'auto', - padding: '7px 12px', - - - }) - } - } + paddingBlock: theme.spacing(0.5), + paddingInline: theme.spacing(1.5), + + '& .MuiInputBase-input': { + color: theme.palette.text.primary, + fontSize: theme.typography.pxToRem(14), + }, + }), + input: ({ theme }) => ({ + padding: theme.spacing(0.5), + }), + }, + }, + MuiAccordion: { + styleOverrides: { + root: ({ theme }) => ({ + borderRadius: theme.shape.borderRadius, + backgroundColor: theme.palette.grey[900], + border: '1px solid', + borderColor: alpha(theme.palette.grey[200], 0.2), + boxShadow: 'none', + '&:before': { + height: 0, + }, + }), + }, + }, + MuiTableCell: { + styleOverrides: { + root: ({ theme }) => ({ + paddingInline: theme.spacing(5), + paddingBlock: theme.spacing(3), + color: theme.palette.primary.contrastText, + }), + head: ({ theme }) => ({ + color: theme.palette.text.secondary, + }), + }, + }, + MuiPaginationItem: { + styleOverrides: { + outlined: ({ theme }) => ({ + borderColor: theme.palette.divider, + '&.Mui-selected': { + color: theme.palette.primary.contrastText, + background: theme.palette.background.paper, + }, + }), + }, + }, + MuiPopover: { + styleOverrides: { + root: ({ theme }) => ({ + boxShadow: theme.shadows[23], + backgroundImage: 'none', + borderRadius: theme.shape.borderRadius * 2.5, + background: 'transparent', + }), + paper: { + backgroundImage: 'none', + }, + }, + }, + MuiSkeleton: { + styleOverrides: { + text: ({ theme }) => ({ + borderRadius: theme.shape.borderRadius * 22, + }), + }, + }, }, -}); \ No newline at end of file +}); + +export type Theme = typeof theme; diff --git a/tsconfig.base.json b/tsconfig.base.json index 21b63f18a..fa7f24d88 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -18,6 +18,7 @@ "paths": { "@origin/defi/oeth": ["libs/defi/oeth/src/index.ts"], "@origin/defi/ousd": ["libs/defi/ousd/src/index.ts"], + "@origin/shared/components": ["libs/shared/components/src/index.ts"], "@origin/shared/data-access": ["libs/shared/data-access/src/index.ts"], "@origin/shared/storybook": ["libs/shared/storybook/src/index.ts"], "@origin/shared/theme": ["libs/shared/theme/src/index.ts"]