diff --git a/.storybook/main.js b/.storybook/main.js index 1492ac61f..3f81e8662 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -1,6 +1,6 @@ module.exports = { stories: [], - addons: ['@storybook/addon-essentials'], + addons: ['@storybook/addon-essentials', '@storybook/addon-a11y'], // uncomment the property below if you want to apply some webpack config globally // webpackFinal: async (config, { configType }) => { // // Make whatever fine-grained changes you need that should apply to all storybook configs diff --git a/apps/explorer/.env b/apps/explorer/.env index 272697aca..56370f422 100644 --- a/apps/explorer/.env +++ b/apps/explorer/.env @@ -20,6 +20,15 @@ NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL # App configuration variables NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api" -NX_TENDERMINT_URL = "https://mainnet-observer-proxy01.ops.vega.xyz" -NX_TENDERMINT_WEBSOCKET_URL = "wss://mainnet-observer-proxy01.ops.vega.xyz/websocket" -NX_VEGA_URL = "http://mainnet-observer.ops.vega.xyz:3008/query" +NX_TENDERMINT_URL = "https://lb.testnet.vega.xyz/tm" +NX_TENDERMINT_WEBSOCKET_URL = "wss://lb.testnet.vega.xyz/tm/websocket" +NX_VEGA_URL = "https://lb.testnet.vega.xyz/query" + +# App flags +NX_EXPLORER_ASSETS = 1 +NX_EXPLORER_GENESIS = 1 +NX_EXPLORER_GOVERNANCE = 1 +NX_EXPLORER_MARKETS = 1 +NX_EXPLORER_NETWORK_PARAMETERS = 1 +NX_EXPLORER_PARTIES = 1 +NX_EXPLORER_VALIDATORS = 1 diff --git a/apps/explorer/src/app/lib/flags.ts b/apps/explorer/src/app/lib/flags.ts new file mode 100644 index 000000000..fed011476 --- /dev/null +++ b/apps/explorer/src/app/lib/flags.ts @@ -0,0 +1,15 @@ +const { env } = process; + +const truthy = ['1', 'true']; + +export default { + assets: truthy.includes(env['NX_EXPLORER_ASSETS'] || ''), + genesis: truthy.includes(env['NX_EXPLORER_GENESIS'] || ''), + governance: truthy.includes(env['NX_EXPLORER_GOVERNANCE'] || ''), + markets: truthy.includes(env['NX_EXPLORER_MARKETS'] || ''), + networkParameters: truthy.includes( + env['NX_EXPLORER_NETWORK_PARAMETERS'] || '' + ), + parties: truthy.includes(env['NX_EXPLORER_PARTIES'] || ''), + validators: truthy.includes(env['NX_EXPLORER_VALIDATORS'] || ''), +}; diff --git a/apps/explorer/src/app/routes/governance/index.tsx b/apps/explorer/src/app/routes/governance/index.tsx index 1af2e7b95..d9dc20ddc 100644 --- a/apps/explorer/src/app/routes/governance/index.tsx +++ b/apps/explorer/src/app/routes/governance/index.tsx @@ -1,7 +1,24 @@ import { gql, useQuery } from '@apollo/client'; import React from 'react'; import { SyntaxHighlighter } from '../../components/syntax-highlighter'; -import { ProposalsQuery } from './__generated__/ProposalsQuery'; +import { + ProposalsQuery, + ProposalsQuery_proposals_terms_change, +} from './__generated__/ProposalsQuery'; + +export function getProposalName(change: ProposalsQuery_proposals_terms_change) { + if (change.__typename === 'NewAsset') { + return `New asset: ${change.symbol}`; + } else if (change.__typename === 'NewMarket') { + return `New market: ${change.instrument.name}`; + } else if (change.__typename === 'UpdateMarket') { + return `Update market: ${change.marketId}`; + } else if (change.__typename === 'UpdateNetworkParameter') { + return `Update network: ${change.networkParameter.key}`; + } + + return 'Unknown proposal'; +} const PROPOSAL_QUERY = gql` query ProposalsQuery { @@ -90,7 +107,7 @@ const Governance = () => { {data.proposals.map((p) => ( {/* TODO get proposal name generator from console */} -

{p.id}

+

{getProposalName(p.terms.change)}

))} diff --git a/apps/explorer/src/app/routes/router-config.tsx b/apps/explorer/src/app/routes/router-config.tsx index 6362804af..af048e084 100644 --- a/apps/explorer/src/app/routes/router-config.tsx +++ b/apps/explorer/src/app/routes/router-config.tsx @@ -15,7 +15,7 @@ import { Blocks } from './blocks/home'; import { Tx } from './txs/id'; import { Txs as TxHome } from './txs/home'; import { PendingTxs } from './pending'; - +import flags from '../lib/flags'; export const Routes = { HOME: '/', TX: 'txs', @@ -29,6 +29,85 @@ export const Routes = { NETWORK_PARAMETERS: 'network-parameters', }; +const partiesRoutes = flags.parties + ? [ + { + path: Routes.PARTIES, + name: 'Parties', + element: , + children: [ + { + index: true, + element: , + }, + { + path: ':party', + element: , + }, + ], + }, + ] + : []; + +const assetsRoutes = flags.assets + ? [ + { + path: Routes.ASSETS, + name: 'Assets', + element: , + }, + ] + : []; + +const genesisRoutes = flags.genesis + ? [ + { + path: Routes.GENESIS, + name: 'Genesis', + element: , + }, + ] + : []; + +const governanceRoutes = flags.governance + ? [ + { + path: Routes.GOVERNANCE, + name: 'Governance', + element: , + }, + ] + : []; + +const marketsRoutes = flags.markets + ? [ + { + path: Routes.MARKETS, + name: 'Markets', + element: , + }, + ] + : []; + +const networkParametersRoutes = flags.networkParameters + ? [ + { + path: Routes.NETWORK_PARAMETERS, + name: 'NetworkParameters', + element: , + }, + ] + : []; +const validators = flags.validators + ? [ + { + path: Routes.VALIDATORS, + name: 'Validators', + element: , + }, + ] + : []; + const routerConfig = [ { path: Routes.HOME, @@ -70,51 +149,13 @@ const routerConfig = [ }, ], }, - { - path: Routes.PARTIES, - name: 'Parties', - element: , - children: [ - { - index: true, - element: , - }, - { - path: ':party', - element: , - }, - ], - }, - { - path: Routes.ASSETS, - name: 'Assets', - element: , - }, - { - path: Routes.GENESIS, - name: 'Genesis', - element: , - }, - { - path: Routes.GOVERNANCE, - name: 'Governance', - element: , - }, - { - path: Routes.MARKETS, - name: 'Markets', - element: , - }, - { - path: Routes.NETWORK_PARAMETERS, - name: 'NetworkParameters', - element: , - }, - { - path: Routes.VALIDATORS, - name: 'Validators', - element: , - }, + ...partiesRoutes, + ...assetsRoutes, + ...genesisRoutes, + ...governanceRoutes, + ...marketsRoutes, + ...networkParametersRoutes, + ...validators, ]; export default routerConfig; diff --git a/apps/explorer/src/main.tsx b/apps/explorer/src/main.tsx index 5877922a0..2f15bfe0d 100644 --- a/apps/explorer/src/main.tsx +++ b/apps/explorer/src/main.tsx @@ -1,3 +1,5 @@ +import * as Sentry from '@sentry/react'; +import { BrowserTracing } from '@sentry/tracing'; import { StrictMode } from 'react'; import * as ReactDOM from 'react-dom'; import { BrowserRouter } from 'react-router-dom'; @@ -5,6 +7,18 @@ import './styles.css'; import App from './app/app'; +const dsn = process.env['NX_SENTRY_DSN']; + +/* istanbul ignore next */ +if (dsn) { + Sentry.init({ + dsn, + integrations: [new BrowserTracing()], + tracesSampleRate: 0.1, + environment: process.env['NODE_ENV'], + }); +} + ReactDOM.render( diff --git a/libs/tailwindcss-config/src/theme.js b/libs/tailwindcss-config/src/theme.js index 3130e02a7..6aef6ed94 100644 --- a/libs/tailwindcss-config/src/theme.js +++ b/libs/tailwindcss-config/src/theme.js @@ -8,72 +8,47 @@ module.exports = { colors: { transparent: 'transparent', current: 'currentColor', - black: '#000', - white: '#FFF', - - neutral: { - // 250 - 23 = 227; (900-50) / 227 = 850 / 227 = 3.74449339207 - 50: '#fafafa', // FA = 250 - 100: '#ebebeb', - 150: '#dcdcdc', - 200: '#cdcdcd', - 250: '#bebebe', - 300: '#afafaf', - 350: '#a1a1a1', - 400: '#939393', - 450: '#858585', - 500: '#787878', - 550: '#6a6a6a', - 593: '#696969', // dark muted - 600: '#5d5d5d', - 650: '#515151', - 700: '#444444', - 753: '#3E3E3E', // dark -> 3F is muted - 750: '#383838', - 800: '#2d2d2d', // breakdown-background was 2C - 850: '#222222', - 900: '#171717', // 17 = 23 + white: { + DEFAULT: '#FFF', + '02': 'rgba(255, 255, 255, 0.02)', + '05': 'rgba(255, 255, 255, 0.05)', + 10: 'rgba(255, 255, 255, 0.10)', + 25: 'rgba(255, 255, 255, 0.25)', + 40: 'rgba(255, 255, 255, 0.40)', + 60: 'rgba(255, 255, 255, 0.60)', + 80: 'rgba(255, 255, 255, 0.80)', + 95: 'rgba(255, 255, 255, 0.95)', + 100: 'rgba(255, 255, 255, 1.00)', + }, + black: { + DEFAULT: '#000', + '02': 'rgba(0, 0, 0, 0.02)', + '05': 'rgba(0, 0, 0, 0.05)', + 10: 'rgba(0, 0, 0, 0.10)', + 25: 'rgba(0, 0, 0, 0.25)', + 40: 'rgba(0, 0, 0, 0.40)', + 60: 'rgba(0, 0, 0, 0.60)', + 80: 'rgba(0, 0, 0, 0.80)', + 95: 'rgba(0, 0, 0, 0.95)', + 100: 'rgba(0, 0, 0, 1)', }, - - 'light-gray-50': '#F5F8FA', //off-white - https://blueprintjs.com/docs/#core/colors - 'gray-50': '#BFCCD6', // muted - https://blueprintjs.com/docs/#core/colors coral: '#FF6057', - // below colors are not defined as atoms vega: { yellow: '#EDFF22', pink: '#FF2D5E', green: '#00F780', }, + 'vega-yellow-dark': '#474B0A', // yellow 0.3 opacity on black intent: { danger: '#FF261A', warning: '#FF7A1A', prompt: '#EDFF22', - progress: '#FFF', success: '#26FF8A', help: '#494949', - background: { - danger: '#9E0025', // for white text - }, - } /*, - data: { - red: { - white: { - 50: '#FFFFFF', - 220: '#FF6057', // overlay FFF 80% - 390: '#FF6057', // overlay FFF 60% - 560: '#FF6057', // overlay FFF 40% - 730: '#FF6057', // overlay FFF 20% - 900: '#FF6057', - }, - green: { - 50: '#30F68B', - 220: '#89DC50', - 475: '#F2BD09', - 730: '#FF8501', - 900: '#FF6057', - }, - }, - },*/, + }, + 'intent-background': { + danger: '#9E0025', // for white text + }, }, spacing: { 0: '0px', @@ -81,17 +56,37 @@ module.exports = { 4: '0.25rem', 8: '0.5rem', 12: '0.75rem', + 16: '1rem', + 20: '1.25rem', + 24: '1.5rem', 28: '1.75rem', + 32: '2rem', 44: '2.75rem', }, - backgroundColor: ({ theme }) => ({ - transparent: 'transparent', - dark: theme('colors.dark'), - black: '#000', - white: theme('colors.white'), - danger: theme('colors.intent.background.danger'), - 'neutral-200': theme('colors.neutral.200'), - }), + opacity: { + 0: '0', + 2: '0.02', + 5: '0.05', + 10: '0.1', + 15: '0.15', + 20: '0.2', + 25: '0.25', + 30: '0.3', + 35: '0.35', + 40: '0.4', + 45: '0.45', + 50: '0.5', + 55: '0.55', + 60: '0.6', + 65: '0.65', + 70: '0.7', + 75: '0.75', + 80: '0.8', + 85: '0.85', + 90: '0.9', + 95: '0.95', + 100: '1', + }, borderWidth: { DEFAULT: '1px', 1: '1px', @@ -131,12 +126,12 @@ module.exports = { ], }, fontSize: { - h1: ['72px', { lineHeight: '92px', letterSpacing: '-1%' }], - h2: ['48px', { lineHeight: '64px', letterSpacing: '-1%' }], - h3: ['32px', { lineHeight: '40px', letterSpacing: '-1%' }], + h1: ['72px', { lineHeight: '92px', letterSpacing: '-0.01em' }], + h2: ['48px', { lineHeight: '64px', letterSpacing: '-0.01em' }], + h3: ['32px', { lineHeight: '40px', letterSpacing: '-0.01em' }], - h4: ['24px', { lineHeight: '36px', letterSpacing: '-1%' }], - h5: ['18px', { lineHeight: '28px', letterSpacing: '-1%' }], + h4: ['24px', { lineHeight: '36px', letterSpacing: '-0.01em' }], + h5: ['18px', { lineHeight: '28px', letterSpacing: '-0.01em' }], 'body-large': ['16px', '24px'], body: ['14px', '20px'], @@ -147,7 +142,9 @@ module.exports = { extend: { boxShadow: { - callout: '5px 5px 0 1px rgba(0, 0, 0, 0.05)', + callout: '5px 5px 0 1px rgba(255, 255, 255, 0.05)', + focus: '0px 0px 0px 1px #FFFFFF, 0px 0px 3px 2px #FFE600', + 'focus-dark': '0px 0px 0px 1px #000000, 0px 0px 3px 2px #FFE600', }, }, }; diff --git a/libs/ui-toolkit/.storybook/preview.js b/libs/ui-toolkit/.storybook/preview.js index 259a4a7d6..6258bc80b 100644 --- a/libs/ui-toolkit/.storybook/preview.js +++ b/libs/ui-toolkit/.storybook/preview.js @@ -1,19 +1,29 @@ import '../src/styles.scss'; export const parameters = { actions: { argTypesRegex: '^on[A-Z].*' }, - themes: { + /*themes: { default: 'dark', list: [ { name: 'dark', class: ['dark', 'bg-black'], color: '#000' }, { name: 'light', class: '', color: '#FFF' }, ], - }, + },*/ }; export const decorators = [ - (Story) => ( -
- -
- ), + (Story, context) => + context.parameters.themes === false ? ( +
+ +
+ ) : ( +
+
+ +
+
+ +
+
+ ), ]; diff --git a/libs/ui-toolkit/src/components/button/Button.spec.tsx b/libs/ui-toolkit/src/components/button/Button.spec.tsx new file mode 100644 index 000000000..f5030e955 --- /dev/null +++ b/libs/ui-toolkit/src/components/button/Button.spec.tsx @@ -0,0 +1,10 @@ +import { render } from '@testing-library/react'; + +import { Button } from './button'; + +describe('Button', () => { + it('should render successfully', () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); +}); diff --git a/libs/ui-toolkit/src/components/button/Button.stories.tsx b/libs/ui-toolkit/src/components/button/Button.stories.tsx new file mode 100644 index 000000000..324759938 --- /dev/null +++ b/libs/ui-toolkit/src/components/button/Button.stories.tsx @@ -0,0 +1,101 @@ +import { Story, Meta } from '@storybook/react'; +import { Button } from './button'; + +export default { + component: Button, + title: 'Button', +} as Meta; + +const Template: Story = (args) => ( + <> +
+
+ {args['variant'] !== 'inline' && + +
+ +
+
+ +
+ +); + +export const NavInline: Story = (args) => ( + <> +
+ +
+
+ +
+
+ +
+ +); + +export const IconPrepend = Template.bind({}); +IconPrepend.args = { + children: 'Icon prepend', + prependIconName: 'search', + variant: 'accent', +}; + +export const IconAppend = Template.bind({}); +IconAppend.args = { + children: 'Icon append', + appendIconName: 'search', + variant: 'accent', +}; diff --git a/libs/ui-toolkit/src/components/button/button.tsx b/libs/ui-toolkit/src/components/button/button.tsx new file mode 100644 index 000000000..e937b49ed --- /dev/null +++ b/libs/ui-toolkit/src/components/button/button.tsx @@ -0,0 +1,161 @@ +import { AnchorHTMLAttributes, ButtonHTMLAttributes, forwardRef } from 'react'; +import classNames from 'classnames'; +import { Icon, IconName } from '../icon'; + +interface CommonProps { + children?: React.ReactNode; + variant?: 'primary' | 'secondary' | 'accent' | 'inline'; + className?: string; + prependIconName?: IconName; + appendIconName?: IconName; +} +export interface ButtonProps + extends ButtonHTMLAttributes, + CommonProps {} + +export interface AnchorButtonProps + extends AnchorHTMLAttributes, + CommonProps {} + +const getClassName = ( + className: CommonProps['className'], + variant: CommonProps['variant'] +) => { + const noPaddingLeftProvided = !( + className?.match(/(^| )p(l|x)-\d+( |$)/) || variant === 'inline' + ); + const noPaddingRightProvided = !( + className?.match(/(^| )p(r|x)-\d+( |$)/) || variant === 'inline' + ); + return classNames( + [ + 'inline-flex', + 'items-center', + 'justify-center', + 'box-border', + 'h-28', + 'border', + 'text-ui', + 'no-underline', + 'hover:underline', + 'disabled:no-underline', + 'transition-all', + ], + { + 'pl-28': noPaddingLeftProvided, + 'pr-28': noPaddingRightProvided, + + 'hover:border-black dark:hover:border-white': variant !== 'inline', + 'active:border-black dark:active:border-white': true, + + 'bg-black dark:bg-white': variant === 'primary', + 'border-black-60 dark:border-white-60': + variant === 'primary' || variant === 'secondary', + 'text-white dark:text-black': variant === 'primary', + 'hover:bg-black-80 dark:hover:bg-white-80': variant === 'primary', + 'active:bg-white dark:active:bg-black': + variant === 'primary' || variant === 'accent', + 'active:text-black dark:active:text-white': + variant === 'primary' || variant === 'accent', + + 'bg-white dark:bg-black': variant === 'secondary', + 'text-black dark:text-white': variant === 'secondary', + 'hover:bg-black-25 dark:hover:bg-white-25': variant === 'secondary', + 'hover:text-black dark:hover:text-white': + variant === 'secondary' || variant === 'accent', + 'active:bg-black dark:active:bg-white': variant === 'secondary', + 'active:text-white dark:active:text-black': variant === 'secondary', + + uppercase: variant === 'accent', + 'bg-vega-yellow dark:bg-vega-yellow': variant === 'accent', + 'border-transparent dark:border-transparent': + variant === 'accent' || variant === 'inline', + 'hover:bg-vega-yellow-dark dark:hover:bg-vega-yellow/30': + variant === 'accent', + 'hover:text-white dark:hover:text-white': variant === 'accent', + + 'pl-4': variant === 'inline', + 'pr-4': variant === 'inline', + 'border-0': variant === 'inline', + underline: variant === 'inline', + 'hover:no-underline': variant === 'inline', + 'hover:border-transparent dark:hover:border-transparent': + variant === 'inline', + 'active:border-transparent dark:active:border-transparent': + variant === 'inline', + 'active:text-black dark:active:text-vega-yellow': variant === 'inline', + 'text-black-95 dark:text-white-95': variant === 'inline', + 'hover:text-black hover:dark:text-white': variant === 'inline', + + 'disabled:bg-black-10 dark:disabled:bg-white-10': variant !== 'inline', + 'disabled:text-black-60 dark:disabled:text-white-60': + variant !== 'inline', + 'disabled:border-black-25 dark:disabled:border-white-25': + variant !== 'inline', + }, + className + ); +}; + +const getContent = ( + children: React.ReactNode, + prependIconName?: IconName, + appendIconName?: IconName +) => { + const iconName = prependIconName || appendIconName; + if (iconName === undefined) { + return children; + } + const iconClassName = classNames(['fill-current'], { + 'mr-8': prependIconName, + 'ml-8': appendIconName, + }); + const icon = ; + return ( + <> + {prependIconName && icon} + {children} + {appendIconName && icon} + + ); +}; + +export const Button = forwardRef( + ( + { + variant = 'primary', + children, + className, + prependIconName, + appendIconName, + ...props + }, + ref + ) => { + return ( + + ); + } +); + +export const AnchorButton = forwardRef( + ( + { + variant = 'primary', + children, + className, + prependIconName, + appendIconName, + ...prosp + }, + ref + ) => { + return ( + + {getContent(children, prependIconName, appendIconName)} + + ); + } +); diff --git a/libs/ui-toolkit/src/components/button/index.ts b/libs/ui-toolkit/src/components/button/index.ts new file mode 100644 index 000000000..eaf5eea7f --- /dev/null +++ b/libs/ui-toolkit/src/components/button/index.ts @@ -0,0 +1 @@ +export * from './button'; diff --git a/libs/ui-toolkit/src/components/callout/callout.module.scss b/libs/ui-toolkit/src/components/callout/callout.module.scss deleted file mode 100644 index 4f76110c3..000000000 --- a/libs/ui-toolkit/src/components/callout/callout.module.scss +++ /dev/null @@ -1,58 +0,0 @@ -@import '../../styles/colors'; - -.callout { - display: flex; - padding: 14px; - border: 1px solid $white; - box-shadow: 3px 3px 0px $white; - margin: 12px 0; - - p { - margin: 0 0 10px 0; - - &:last-child { - margin-bottom: 0; - } - } - - // VARIATIONS - &--error { - box-shadow: 5px 5px 0px $vega-red3; - } - - &--success { - box-shadow: 5px 5px 0px $vega-green3; - } - - &--warn { - box-shadow: 5px 5px 0px $vega-orange3; - } - - &--action { - box-shadow: 5px 5px 0px $vega-yellow3; - } - - &__content { - width: 100%; - - > :last-child { - margin-bottom: 0; - } - } - - &__title, - h4, - h5, - h6 { - margin-bottom: 15px; - } - - &__title { - margin-top: 0; - } - - &__icon { - margin-right: 10px; - color: $white; - } -} diff --git a/libs/ui-toolkit/src/components/callout/callout.stories.tsx b/libs/ui-toolkit/src/components/callout/callout.stories.tsx index f8b3cc854..1c5da802e 100644 --- a/libs/ui-toolkit/src/components/callout/callout.stories.tsx +++ b/libs/ui-toolkit/src/components/callout/callout.stories.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { ComponentStory, ComponentMeta } from '@storybook/react'; -import { Callout } from '.'; +import { Callout } from './callout'; +import { Button } from '../button'; export default { title: 'Callout', @@ -9,37 +10,61 @@ export default { } as ComponentMeta; const Template: ComponentStory = (args) => ( - Content + ); export const Default = Template.bind({}); +Default.args = { + children: 'Content', +}; export const Danger = Template.bind({}); Danger.args = { intent: 'danger', + children: 'Content', }; export const Warning = Template.bind({}); Warning.args = { intent: 'warning', + children: 'Content', }; export const Prompt = Template.bind({}); Prompt.args = { intent: 'prompt', + children: 'Content', }; export const Progress = Template.bind({}); Progress.args = { intent: 'progress', + children: 'Content', }; export const Success = Template.bind({}); Success.args = { intent: 'success', + children: 'Content', }; export const Help = Template.bind({}); Help.args = { intent: 'help', + children: 'Content', +}; + +export const IconAndContent = Template.bind({}); +IconAndContent.args = { + intent: 'help', + title: 'This is what this thing does', + iconName: 'endorsed', + children: ( +
+
With a longer explaination
+ +
+ ), }; diff --git a/libs/ui-toolkit/src/components/callout/callout.test.tsx b/libs/ui-toolkit/src/components/callout/callout.test.tsx index a71d3c1e0..561052ee1 100644 --- a/libs/ui-toolkit/src/components/callout/callout.test.tsx +++ b/libs/ui-toolkit/src/components/callout/callout.test.tsx @@ -8,8 +8,10 @@ test('It renders content within callout', () => { }); test('It renders title and icon', () => { - render(} title="title" />); - expect(screen.getByTestId('icon')).toBeInTheDocument(); + render(); + expect( + screen.getByTestId('callout').querySelector('svg') + ).toBeInTheDocument(); expect(screen.getByText('title')).toBeInTheDocument(); }); @@ -33,7 +35,7 @@ intents.map((intent) => test(`Applies class for progress`, () => { render(); expect(screen.getByTestId('callout')).toHaveClass( - 'shadow-intent-black', - 'dark:shadow-intent-progress' + 'shadow-black', + 'dark:shadow-white' ); }); diff --git a/libs/ui-toolkit/src/components/callout/callout.tsx b/libs/ui-toolkit/src/components/callout/callout.tsx index 1661d3287..815e44e45 100644 --- a/libs/ui-toolkit/src/components/callout/callout.tsx +++ b/libs/ui-toolkit/src/components/callout/callout.tsx @@ -1,48 +1,55 @@ -import React from 'react'; import classNames from 'classnames'; +import { Icon, IconName } from '../icon'; -export const Callout = ({ - children, - title, - intent = 'help', - icon, - headingLevel, -}: { +export interface CalloutProps { children?: React.ReactNode; title?: React.ReactElement | string; intent?: 'danger' | 'warning' | 'prompt' | 'progress' | 'success' | 'help'; - icon?: React.ReactNode; + iconName?: IconName; headingLevel?: 1 | 2 | 3 | 4 | 5 | 6; -}) => { +} + +export function Callout({ + children, + title, + intent = 'help', + iconName, + headingLevel, +}: CalloutProps) { const className = classNames( 'shadow-callout', 'border', 'border-black', 'dark:border-white', + 'text-body-large', + 'dark:text-white', 'p-8', { 'shadow-intent-danger': intent === 'danger', 'shadow-intent-warning': intent === 'warning', 'shadow-intent-prompt': intent === 'prompt', - 'shadow-intent-black dark:shadow-intent-progress': intent === 'progress', + 'shadow-black dark:shadow-white': intent === 'progress', 'shadow-intent-success': intent === 'success', 'shadow-intent-help': intent === 'help', - flex: icon, + flex: !!iconName, } ); const TitleTag: keyof JSX.IntrinsicElements = headingLevel ? `h${headingLevel}` : 'div'; + const icon = iconName && ( + + ); const body = ( -
+ <> {title && {title}} {children} -
+ ); return (
- {icon &&
{icon}
} + {icon} {icon ?
{body}
: body}
); -}; +} diff --git a/libs/ui-toolkit/src/components/etherscan-link/etherscan-link.test.tsx b/libs/ui-toolkit/src/components/etherscan-link/etherscan-link.test.tsx index 59dd73916..5bef244c7 100644 --- a/libs/ui-toolkit/src/components/etherscan-link/etherscan-link.test.tsx +++ b/libs/ui-toolkit/src/components/etherscan-link/etherscan-link.test.tsx @@ -1,6 +1,5 @@ import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; -import * as React from 'react'; import { EtherscanLink } from '.'; import { EthereumChainIds } from '../../utils/web3'; diff --git a/libs/ui-toolkit/src/components/icon/icon.spec.tsx b/libs/ui-toolkit/src/components/icon/icon.spec.tsx new file mode 100644 index 000000000..28bfc5ffe --- /dev/null +++ b/libs/ui-toolkit/src/components/icon/icon.spec.tsx @@ -0,0 +1,10 @@ +import { render } from '@testing-library/react'; + +import { Icon } from './icon'; + +describe('Icon', () => { + it('should render successfully', () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); +}); diff --git a/libs/ui-toolkit/src/components/icon/icon.stories.tsx b/libs/ui-toolkit/src/components/icon/icon.stories.tsx new file mode 100644 index 000000000..8f5843757 --- /dev/null +++ b/libs/ui-toolkit/src/components/icon/icon.stories.tsx @@ -0,0 +1,22 @@ +import { Story, Meta } from '@storybook/react'; +import { Icon } from './icon'; + +export default { + component: Icon, + title: 'Input', +} as Meta; + +const Template: Story = (args) => ; + +export const Default = Template.bind({}); +Default.args = {}; + +export const WithError = Template.bind({}); +WithError.args = { + hasError: true, +}; + +export const Disabled = Template.bind({}); +Disabled.args = { + disabled: true, +}; diff --git a/libs/ui-toolkit/src/components/icon/icon.tsx b/libs/ui-toolkit/src/components/icon/icon.tsx new file mode 100644 index 000000000..fc7e469fc --- /dev/null +++ b/libs/ui-toolkit/src/components/icon/icon.tsx @@ -0,0 +1,32 @@ +import { IconSvgPaths20, IconSvgPaths16, IconName } from '@blueprintjs/icons'; +import classNames from 'classnames'; + +export type { IconName } from '@blueprintjs/icons'; + +interface IconProps { + hasError?: boolean; + disabled?: boolean; + name: IconName; + className?: string; + size?: 16 | 20 | 24 | 32 | 48 | 64; +} + +export const Icon = ({ size = 16, name, className }: IconProps) => { + const effectiveClassName = classNames( + { + 'w-20': size === 20, + 'h-20': size === 20, + 'w-16': size === 16, + 'h-16': size === 16, + }, + className + ); + const viewbox = size <= 16 ? '0 0 16 16' : '0 0 20 20'; + return ( + + {(size <= 16 ? IconSvgPaths16 : IconSvgPaths20)[name].map((d, key) => ( + + ))} + + ); +}; diff --git a/libs/ui-toolkit/src/components/icon/index.ts b/libs/ui-toolkit/src/components/icon/index.ts new file mode 100644 index 000000000..af77d84ef --- /dev/null +++ b/libs/ui-toolkit/src/components/icon/index.ts @@ -0,0 +1 @@ +export * from './icon'; diff --git a/libs/ui-toolkit/src/components/input-error/index.ts b/libs/ui-toolkit/src/components/input-error/index.ts new file mode 100644 index 000000000..619f584f4 --- /dev/null +++ b/libs/ui-toolkit/src/components/input-error/index.ts @@ -0,0 +1 @@ +export * from './input-error'; diff --git a/libs/ui-toolkit/src/components/input-error/input-error.spec.tsx b/libs/ui-toolkit/src/components/input-error/input-error.spec.tsx new file mode 100644 index 000000000..352d66675 --- /dev/null +++ b/libs/ui-toolkit/src/components/input-error/input-error.spec.tsx @@ -0,0 +1,10 @@ +import { render } from '@testing-library/react'; + +import { InputError } from './input-error'; + +describe('InputError', () => { + it('should render successfully', () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); +}); diff --git a/libs/ui-toolkit/src/components/input-error/input-error.stories.tsx b/libs/ui-toolkit/src/components/input-error/input-error.stories.tsx new file mode 100644 index 000000000..b227a0c6c --- /dev/null +++ b/libs/ui-toolkit/src/components/input-error/input-error.stories.tsx @@ -0,0 +1,20 @@ +import { Story, Meta } from '@storybook/react'; +import { InputError } from './input-error'; + +export default { + component: InputError, + title: 'InputError', +} as Meta; + +const Template: Story = (args) => ; + +export const Danger = Template.bind({}); +Danger.args = { + children: 'An error that might have happened', +}; + +export const Warning = Template.bind({}); +Warning.args = { + intent: 'warning', + children: 'Something that might be an issue', +}; diff --git a/libs/ui-toolkit/src/components/input-error/input-error.tsx b/libs/ui-toolkit/src/components/input-error/input-error.tsx new file mode 100644 index 000000000..66e1b4c95 --- /dev/null +++ b/libs/ui-toolkit/src/components/input-error/input-error.tsx @@ -0,0 +1,41 @@ +import classNames from 'classnames'; +import { Icon } from '../icon'; + +interface InputErrorProps { + children?: React.ReactNode; + className?: string; + intent?: 'danger' | 'warning'; +} + +export const InputError = ({ + intent = 'danger', + className, + children, +}: InputErrorProps) => { + const effectiveClassName = classNames( + [ + 'inline-flex', + 'items-center', + 'box-border', + 'h-28', + 'border-l-4', + 'text-black-95 dark:text-white-95', + 'text-ui', + ], + { + 'border-intent-danger': intent === 'danger', + 'border-intent-warning': intent === 'warning', + }, + className + ); + const iconClassName = classNames(['mx-8'], { + 'fill-intent-danger': intent === 'danger', + 'fill-intent-warning': intent === 'warning', + }); + return ( +
+ + {children} +
+ ); +}; diff --git a/libs/ui-toolkit/src/components/input/index.ts b/libs/ui-toolkit/src/components/input/index.ts new file mode 100644 index 000000000..e3365cb90 --- /dev/null +++ b/libs/ui-toolkit/src/components/input/index.ts @@ -0,0 +1 @@ +export * from './input'; diff --git a/libs/ui-toolkit/src/components/input/input.spec.tsx b/libs/ui-toolkit/src/components/input/input.spec.tsx new file mode 100644 index 000000000..9a37552b5 --- /dev/null +++ b/libs/ui-toolkit/src/components/input/input.spec.tsx @@ -0,0 +1,10 @@ +import { render } from '@testing-library/react'; + +import { Input } from './input'; + +describe('Input', () => { + it('should render successfully', () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); +}); diff --git a/libs/ui-toolkit/src/components/input/input.stories.tsx b/libs/ui-toolkit/src/components/input/input.stories.tsx new file mode 100644 index 000000000..21f8a2199 --- /dev/null +++ b/libs/ui-toolkit/src/components/input/input.stories.tsx @@ -0,0 +1,29 @@ +import { Story, Meta } from '@storybook/react'; +import { Input } from './input'; +export default { + component: Input, + title: 'Input', +} as Meta; + +const Template: Story = (args) => ; + +export const Default = Template.bind({}); +Default.args = {}; + +export const WithError = Template.bind({}); +WithError.args = { + hasError: true, +}; + +export const Disabled = Template.bind({}); +Disabled.args = { + disabled: true, +}; + +export const IconPrepend: Story = () => ( + +); + +export const IconAppend: Story = () => ( + +); diff --git a/libs/ui-toolkit/src/components/input/input.tsx b/libs/ui-toolkit/src/components/input/input.tsx new file mode 100644 index 000000000..bfa70e6b1 --- /dev/null +++ b/libs/ui-toolkit/src/components/input/input.tsx @@ -0,0 +1,96 @@ +import { InputHTMLAttributes, forwardRef } from 'react'; +import classNames from 'classnames'; +import { Icon, IconName } from '../icon'; + +interface InputProps extends InputHTMLAttributes { + hasError?: boolean; + disabled?: boolean; + className?: string; + prependIconName?: IconName; + appendIconName?: IconName; +} +export const inputClassNames = ({ + hasError, + className, +}: { + hasError?: boolean; + className?: string; +}) => { + const noPaddingLeftProvided = !className?.match(/(^| )p(l|x)-\d+( |$)/); + const noPaddingRightProvided = !className?.match(/(^| )p(r|x)-\d+( |$)/); + return classNames( + [ + 'inline-flex', + 'items-center', + 'box-border', + 'border', + 'bg-clip-padding', + 'border-black-60 dark:border-white-60', + 'bg-black-25 dark:bg-white-25', + 'text-black-60 dark:text-white-60', + 'text-ui', + 'focus-visible:shadow-focus dark:focus-visible:shadow-focus-dark', + 'focus-visible:outline-0', + 'disabled:bg-black-10 disabled:dark:bg-white-10', + ], + { + 'pl-8': noPaddingLeftProvided, + 'pr-8': noPaddingRightProvided, + 'border-vega-pink dark:border-vega-pink': hasError, + }, + className + ); +}; + +export const inputStyle = ({ + style, + disabled, +}: { + style?: React.CSSProperties; + disabled?: boolean; +}) => + disabled + ? { + ...style, + backgroundImage: + 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAAXNSR0IArs4c6QAAACNJREFUGFdjtLS0/M8ABcePH2eEsRlJl4BpBdHIuuFmEi0BABqjEQVjx/LTAAAAAElFTkSuQmCC)', + } + : style; + +export const Input = forwardRef( + ({ prependIconName, appendIconName, className, ...props }, ref) => { + className = `${className} h-28`; + if (prependIconName) { + className += ' pl-28'; + } + if (appendIconName) { + className += ' pr-28'; + } + const input = ( + + ); + const iconName = prependIconName || appendIconName; + if (iconName !== undefined) { + const iconClassName = classNames( + ['fill-black-60 dark:fill-white-60', 'absolute', 'z-10'], + { + 'left-8': prependIconName, + 'right-8': appendIconName, + } + ); + const icon = ; + return ( +
+ {prependIconName && icon} + {input} + {appendIconName && icon} +
+ ); + } + return input; + } +); diff --git a/libs/ui-toolkit/src/components/select/index.ts b/libs/ui-toolkit/src/components/select/index.ts new file mode 100644 index 000000000..c7396734d --- /dev/null +++ b/libs/ui-toolkit/src/components/select/index.ts @@ -0,0 +1 @@ +export * from './select'; diff --git a/libs/ui-toolkit/src/components/select/select.spec.tsx b/libs/ui-toolkit/src/components/select/select.spec.tsx new file mode 100644 index 000000000..6f8f3453f --- /dev/null +++ b/libs/ui-toolkit/src/components/select/select.spec.tsx @@ -0,0 +1,10 @@ +import { render } from '@testing-library/react'; + +import { Select } from './select'; + +describe('Select', () => { + it('should render successfully', () => { + const { baseElement } = render( + + +); + +export const Default = Template.bind({}); +Default.args = {}; + +export const WithError = Template.bind({}); +WithError.args = { + hasError: true, +}; + +export const Disabled = Template.bind({}); +Disabled.args = { + disabled: true, +}; diff --git a/libs/ui-toolkit/src/components/select/select.tsx b/libs/ui-toolkit/src/components/select/select.tsx new file mode 100644 index 000000000..189b2e9c7 --- /dev/null +++ b/libs/ui-toolkit/src/components/select/select.tsx @@ -0,0 +1,20 @@ +import { SelectHTMLAttributes, forwardRef } from 'react'; +import classNames from 'classnames'; +import { inputClassNames } from '../input/input'; + +export interface SelectProps extends SelectHTMLAttributes { + hasError?: boolean; + className?: string; + value?: string | number; + children?: React.ReactNode; +} + +export const Select = forwardRef( + (props, ref) => ( + +); + +export const Default = Template.bind({}); +Default.args = {}; + +export const WithError = Template.bind({}); +WithError.args = { + hasError: true, +}; + +export const Disabled = Template.bind({}); +Disabled.args = { + disabled: true, +}; diff --git a/libs/ui-toolkit/src/components/text-area/text-area.tsx b/libs/ui-toolkit/src/components/text-area/text-area.tsx new file mode 100644 index 000000000..a58afaf01 --- /dev/null +++ b/libs/ui-toolkit/src/components/text-area/text-area.tsx @@ -0,0 +1,14 @@ +import { TextareaHTMLAttributes, forwardRef } from 'react'; +import { inputClassNames } from '../input/input'; + +export interface TextAreaProps + extends TextareaHTMLAttributes { + hassError?: boolean; + className?: string; +} + +export const TextArea = forwardRef( + (props, ref) => ( +