diff --git a/packages/tailwindcss-config/index.js b/packages/tailwindcss-config/index.js index 009c2a742..fdce54534 100644 --- a/packages/tailwindcss-config/index.js +++ b/packages/tailwindcss-config/index.js @@ -55,7 +55,7 @@ module.exports = { 'primary-20': '#F5A549', 'primary-10': '#FFDAA7', - negative: '#FF6868', + negative: '#FF0D39', positive: '#72EADE', error: '#FF0000', @@ -186,6 +186,9 @@ module.exports = { scale: { 85: '.85', }, + opacity: { + 65: '.65', + }, }, }, variants: { diff --git a/packages/ui/src/1_atoms/Tabs/Tabs.module.css b/packages/ui/src/1_atoms/Tabs/Tabs.module.css new file mode 100644 index 000000000..143e06feb --- /dev/null +++ b/packages/ui/src/1_atoms/Tabs/Tabs.module.css @@ -0,0 +1,17 @@ +.wrapper { + @apply inline-flex flex-col; + + .tabs { + @apply inline-flex rounded-t items-center flex-nowrap overflow-auto; + + &.primary { + @apply border border-b-0 border-gray-50 bg-gray-90; + } + } + + .content { + &.primary { + @apply bg-gray-90 rounded-b text-gray-10 border-b border-x border-gray-50; + } + } +} diff --git a/packages/ui/src/1_atoms/Tabs/Tabs.stories.tsx b/packages/ui/src/1_atoms/Tabs/Tabs.stories.tsx new file mode 100644 index 000000000..bdf68c7d8 --- /dev/null +++ b/packages/ui/src/1_atoms/Tabs/Tabs.stories.tsx @@ -0,0 +1,88 @@ +import { Story } from '@storybook/react'; + +import React, { ComponentProps, useState } from 'react'; + +import { Tabs } from './Tabs'; +import { TabSize, TabType } from './Tabs.types'; + +export default { + title: 'Atoms/Tabs', + component: Tabs, +}; + +const Template: Story> = args => { + const [index, setIndex] = useState(args.index); + return ; +}; + +export const Primary = Template.bind(); +Primary.args = { + items: [ + { + label: 'Default', + content:
Default
, + activeClassName: 'border-t-primary-30', + dataActionId: 'default', + }, + { + label: 'Buy', + content:
Buy
, + activeClassName: 'border-t-positive', + dataActionId: 'buy', + }, + { + label: 'Sell', + content:
Sell
, + activeClassName: 'border-t-negative', + dataActionId: 'sell', + }, + { + label: 'Disabled', + content:
Disabled
, + disabled: true, + activeClassName: '', + dataActionId: 'disabled', + }, + ], + index: 0, + contentClassName: 'p-6', + className: '', + size: TabSize.normal, + type: TabType.primary, +}; + +export const Secondary = Template.bind(); +Secondary.args = { + items: [ + { + label: 'Default', + content:
Default
, + activeClassName: 'text-primary-20', + dataActionId: 'default', + }, + { + label: 'Buy', + content:
Buy
, + activeClassName: 'text-positive', + dataActionId: 'buy', + }, + { + label: 'Sell', + content:
Sell
, + activeClassName: 'text-negative', + dataActionId: 'sell', + }, + { + label: 'Disabled', + content:
Disabled
, + disabled: true, + activeClassName: '', + dataActionId: 'disabled', + }, + ], + index: 0, + contentClassName: 'p-4', + className: '', + size: TabSize.normal, + type: TabType.secondary, +}; diff --git a/packages/ui/src/1_atoms/Tabs/Tabs.test.tsx b/packages/ui/src/1_atoms/Tabs/Tabs.test.tsx new file mode 100644 index 000000000..cedf3f29c --- /dev/null +++ b/packages/ui/src/1_atoms/Tabs/Tabs.test.tsx @@ -0,0 +1,54 @@ +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import React from 'react'; + +import { Tabs } from './Tabs'; + +const items = [ + { + label: '0', + content: 'content-0', + }, + { + label: '1', + content: 'content-1', + }, + { + label: 'disabled', + content: 'disabled-content', + disabled: true, + }, +]; + +describe('Tabs', () => { + it('renders tabs', () => { + const { getByText } = render(); + + expect(getByText('0')).toBeInTheDocument(); + expect(getByText('1')).toBeInTheDocument(); + expect(getByText('content-1')).toBeInTheDocument(); + }); + + it('switch between tabs', () => { + const onChange = jest.fn(); + const { getByText } = render( + , + ); + + userEvent.click(getByText('0')); + + expect(onChange).toBeCalledWith(0); + }); + + it('does not switch to a disabled tab', () => { + const onChange = jest.fn(); + const { getByText } = render( + , + ); + + userEvent.click(getByText('disabled')); + + expect(onChange).not.toBeCalled(); + }); +}); diff --git a/packages/ui/src/1_atoms/Tabs/Tabs.tsx b/packages/ui/src/1_atoms/Tabs/Tabs.tsx new file mode 100644 index 000000000..4bf737072 --- /dev/null +++ b/packages/ui/src/1_atoms/Tabs/Tabs.tsx @@ -0,0 +1,73 @@ +import React, { useMemo, useCallback } from 'react'; + +import classNames from 'classnames'; + +import styles from './Tabs.module.css'; +import { TabSize, TabType } from './Tabs.types'; +import { Tab } from './components/Tab/Tab'; + +interface ITabItem { + label: React.ReactNode; + content: React.ReactNode; + disabled?: boolean; + dataActionId?: string; + activeClassName?: string; +} + +type TabsProps = { + className?: string; + contentClassName?: string; + index: number; + items: ITabItem[]; + onChange?: (index: number) => void; + type?: TabType; + size?: TabSize; +}; + +export const Tabs: React.FC = ({ + items, + index, + onChange, + className = '', + contentClassName, + type = TabType.primary, + size = TabSize.normal, +}) => { + const selectTab = useCallback( + (item: ITabItem, index: number) => { + if (!item.disabled) { + onChange?.(index); + } + }, + [onChange], + ); + + const content = useMemo(() => items[index]?.content, [index, items]); + + return ( +
+
+ {items.map((item, i) => ( + selectTab(item, i)} + content={item.label} + dataActionId={item.dataActionId} + type={type} + size={size} + activeIndex={index} + activeClassName={item.activeClassName} + /> + ))} +
+
+ {content} +
+
+ ); +}; diff --git a/packages/ui/src/1_atoms/Tabs/Tabs.types.ts b/packages/ui/src/1_atoms/Tabs/Tabs.types.ts new file mode 100644 index 000000000..50d0bd8c8 --- /dev/null +++ b/packages/ui/src/1_atoms/Tabs/Tabs.types.ts @@ -0,0 +1,9 @@ +export enum TabSize { + small = 'small', + normal = 'normal', +} + +export enum TabType { + primary = 'primary', + secondary = 'secondary', +} diff --git a/packages/ui/src/1_atoms/Tabs/components/Tab/Tab.module.css b/packages/ui/src/1_atoms/Tabs/components/Tab/Tab.module.css new file mode 100644 index 000000000..fde8cc5b3 --- /dev/null +++ b/packages/ui/src/1_atoms/Tabs/components/Tab/Tab.module.css @@ -0,0 +1,64 @@ +.button { + @apply cursor-pointer inline-flex box-border items-center justify-center text-center rounded cursor-pointer transition-colors relative; + + &.primary { + @apply border-t-2 border-t-transparent border-b text-base text-gray-10/50 rounded-b-none h-12; + min-width: 10rem; + + &.normal { + @apply font-medium; + } + + &:not(:disabled):not(.active):hover { + @apply text-gray-10; + } + + &.active { + @apply text-gray-10 bg-gray-90 border-x border-x-gray-50 pb-0.5 border-b-transparent; + } + + &:last-child { + @apply border-r-0; + } + + &:first-child { + @apply border-l-0; + } + + &:not(.active) { + @apply bg-gray-80 border-b-gray-50 border-r border-r-gray-50 rounded-none border-t-0; + + &:last-child, + &.noRightBorder { + @apply border-r-0; + } + } + } + + &.secondary { + &.normal { + @apply font-semibold text-xs px-0.5 py-2 mx-0.5; + min-width: 3.5rem; + } + + &.small { + @apply font-semibold text-tiny px-0.5 py-1 mx-0.5; + min-width: 2.5rem; + } + + &.active { + @apply bg-gray-70; + } + + &:not(.active) { + @apply text-gray-10/75; + &:not(:disabled):hover { + @apply text-gray-10; + } + } + } + + &:disabled { + @apply cursor-not-allowed; + } +} diff --git a/packages/ui/src/1_atoms/Tabs/components/Tab/Tab.tsx b/packages/ui/src/1_atoms/Tabs/components/Tab/Tab.tsx new file mode 100644 index 000000000..8771c20e3 --- /dev/null +++ b/packages/ui/src/1_atoms/Tabs/components/Tab/Tab.tsx @@ -0,0 +1,54 @@ +import React from 'react'; + +import classNames from 'classnames'; + +import { TabSize, TabType } from '../../Tabs.types'; +import styles from './Tab.module.css'; + +type TabProps = { + content: React.ReactNode; + disabled?: boolean; + active: boolean; + onClick: () => void; + className?: string; + dataActionId?: string; + type: TabType; + size: TabSize; + activeClassName?: string; + index: number; + activeIndex: number; +}; + +export const Tab: React.FC = ({ + content, + onClick, + disabled, + className, + dataActionId, + type, + size, + active, + activeClassName = '', + activeIndex, + index, +}) => ( + +);