diff --git a/src/components/Status/Status.props.ts b/src/components/Status/Status.props.ts new file mode 100644 index 00000000..885e0a86 --- /dev/null +++ b/src/components/Status/Status.props.ts @@ -0,0 +1,10 @@ +import { StatusConfig } from './Status.styles'; +import type { StatusAppearance } from './StatusAppearance.type'; +import type { StatusEmphasis } from './StatusEmphasis.type'; + +export type StatusProps = { + appearance?: StatusAppearance; + custom?: StatusConfig; + emphasis?: StatusEmphasis; + label: string; +}; diff --git a/src/components/Status/Status.stories.tsx b/src/components/Status/Status.stories.tsx new file mode 100644 index 00000000..8b3dfc50 --- /dev/null +++ b/src/components/Status/Status.stories.tsx @@ -0,0 +1,49 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { Status } from './Status'; +import { StatusEmphasis } from './StatusEmphasis.type'; + +import { StatusDocs } from '@/docs-components/StatusDocs'; +import { TetDocs } from '@/docs-components/TetDocs'; + +const meta = { + title: 'Status', + component: Status, + tags: ['autodocs'], + argTypes: { + emphasis: { + control: { + type: 'select', + options: ['high', 'medium', 'low'] satisfies StatusEmphasis[], + }, + }, + }, + args: { + appearance: 'grey', + emphasis: 'high', + }, + parameters: { + docs: { + description: { + component: + 'An indicator that conveys the current state or condition of a process, item, or user. Status indicators often use colors, icons, or text to communicate information, such as online presence, approval, or completion.', + }, + page: () => ( + + + + ), + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + appearance: 'grey', + emphasis: 'high', + label: 'status', + }, +}; diff --git a/src/components/Status/Status.styles.ts b/src/components/Status/Status.styles.ts new file mode 100644 index 00000000..1f6927ad --- /dev/null +++ b/src/components/Status/Status.styles.ts @@ -0,0 +1,214 @@ +import type { StatusAppearance } from './StatusAppearance.type'; + +import { BaseProps } from '@/types'; + +export type StatusConfig = { + appearance?: Partial< + Record< + StatusAppearance, + { emphasis?: Partial> } + > + >; + dot: { + appearance: Partial< + Record< + StatusAppearance, + { emphasis?: Partial> } + > + >; + } & BaseProps<'appearance'>; + hasLabel?: BaseProps; + innerElements: { + label?: BaseProps; + }; +} & BaseProps<'appearance'>; + +export const defaultConfig = { + display: 'flex', + w: 'fit-content', + h: '$size-xSmall', + gap: '$space-component-gap-small', + alignItems: 'center', + text: '$typo-body-medium', + dot: { + w: '8px', + h: '8px', + borderRadius: '$border-radius-full', + appearance: { + grey: { + emphasis: { + high: { + backgroundColor: '$color-nonSemantic-white-content-primary', + }, + medium: { + backgroundColor: '$color-nonSemantic-grey-background-strong', + }, + low: { + backgroundColor: '$color-nonSemantic-grey-background-strong', + }, + }, + }, + blue: { + emphasis: { + high: { + backgroundColor: '$color-nonSemantic-white-content-primary', + }, + medium: { + backgroundColor: '$color-nonSemantic-blue-background-strong', + }, + low: { + backgroundColor: '$color-nonSemantic-blue-background-strong', + }, + }, + }, + green: { + emphasis: { + high: { + backgroundColor: '$color-nonSemantic-white-content-primary', + }, + medium: { + backgroundColor: '$color-nonSemantic-green-background-strong', + }, + low: { + backgroundColor: '$color-nonSemantic-green-background-strong', + }, + }, + }, + red: { + emphasis: { + high: { + backgroundColor: '$color-nonSemantic-white-content-primary', + }, + medium: { + backgroundColor: '$color-nonSemantic-red-background-strong', + }, + low: { + backgroundColor: '$color-nonSemantic-red-background-strong', + }, + }, + }, + orange: { + emphasis: { + high: { + backgroundColor: '$color-nonSemantic-grey-content-primary', + }, + medium: { + backgroundColor: '$color-nonSemantic-orange-background-strong', + }, + low: { + backgroundColor: '$color-nonSemantic-orange-background-strong', + }, + }, + }, + }, + }, + appearance: { + grey: { + emphasis: { + high: { + backgroundColor: '$color-nonSemantic-grey-background-strong', + color: '$color-nonSemantic-white-content-primary', + p: '$space-component-padding-null $space-component-padding-small', + borderRadius: '$border-radius-medium', + }, + medium: { + backgroundColor: '$color-nonSemantic-grey-background-muted', + color: '$color-nonSemantic-grey-content-primary', + p: '$space-component-padding-null $space-component-padding-small', + borderRadius: '$border-radius-medium', + }, + low: { + backgroundColor: '$color-transparent', + color: '$color-nonSemantic-grey-content-primary', + borderRadius: '$border-radius-medium', + }, + }, + }, + blue: { + emphasis: { + high: { + backgroundColor: '$color-nonSemantic-blue-background-strong', + color: '$color-nonSemantic-white-content-primary', + p: '$space-component-padding-null $space-component-padding-small', + borderRadius: '$border-radius-medium', + }, + medium: { + backgroundColor: '$color-nonSemantic-blue-background-muted', + color: '$color-nonSemantic-blue-content-primary', + p: '$space-component-padding-null $space-component-padding-small', + borderRadius: '$border-radius-medium', + }, + low: { + backgroundColor: '$color-transparent', + color: '$color-nonSemantic-blue-content-primary', + }, + }, + }, + green: { + emphasis: { + high: { + color: '$color-nonSemantic-white-content-primary', + backgroundColor: '$color-nonSemantic-green-background-strong', + p: '$space-component-padding-null $space-component-padding-small', + borderRadius: '$border-radius-medium', + }, + medium: { + color: '$color-nonSemantic-green-content-primary', + backgroundColor: '$color-nonSemantic-green-background-muted', + p: '$space-component-padding-null $space-component-padding-small', + borderRadius: '$border-radius-medium', + }, + low: { + color: '$color-nonSemantic-green-content-primary', + }, + }, + }, + red: { + emphasis: { + high: { + color: '$color-nonSemantic-white-content-primary', + backgroundColor: '$color-nonSemantic-red-background-strong', + p: '$space-component-padding-null $space-component-padding-small', + borderRadius: '$border-radius-medium', + }, + medium: { + color: '$color-nonSemantic-red-content-primary', + backgroundColor: '$color-nonSemantic-red-background-muted', + p: '$space-component-padding-null $space-component-padding-small', + borderRadius: '$border-radius-medium', + }, + low: { + color: '$color-nonSemantic-red-content-primary', + backgroundColor: '$color-transparent', + }, + }, + }, + orange: { + emphasis: { + high: { + backgroundColor: '$color-nonSemantic-orange-background-strong', + color: '$color-nonSemantic-grey-content-primary', + p: '$space-component-padding-null $space-component-padding-small', + borderRadius: '$border-radius-medium', + }, + medium: { + color: '$color-nonSemantic-orange-content-primary', + backgroundColor: '$color-nonSemantic-orange-background-muted', + p: '$space-component-padding-null $space-component-padding-small', + borderRadius: '$border-radius-medium', + }, + low: { + color: '$color-nonSemantic-orange-content-primary', + backgroundColor: '$color-transparent', + }, + }, + }, + }, + innerElements: { + label: {}, + }, +} satisfies StatusConfig; + +export const statusStyles = { + defaultConfig, +}; diff --git a/src/components/Status/Status.test.tsx b/src/components/Status/Status.test.tsx new file mode 100644 index 00000000..074e7b4d --- /dev/null +++ b/src/components/Status/Status.test.tsx @@ -0,0 +1,117 @@ +import { Status } from './Status'; +import { render } from '../../tests/render'; + +import { customPropTester } from '@/tests/customPropTester'; + +const getStatus = (jsx: JSX.Element) => { + const { getByTestId } = render(jsx); + return { + status: getByTestId('status'), + dot: getByTestId('status-dot'), + }; +}; + +describe('Status', () => { + it('should render the status', () => { + const { status } = getStatus(); + expect(status).toBeInTheDocument(); + }); + + it('should render a correct color (grey, high)', () => { + const { status, dot } = getStatus( + , + ); + expect(status).toHaveStyle( + 'background-color: rgb(85, 95, 109); color: rgb(255, 255, 255)', + ); + expect(dot).toHaveStyle('background-color: rgb(255, 255, 255)'); + }); + + it('should render a correct color (grey, medium)', () => { + const { status, dot } = getStatus( + , + ); + expect(status).toHaveStyle( + 'background-color: rgb(222, 227, 231); color: rgb(39, 46, 53)', + ); + expect(dot).toHaveStyle('background-color: rgb(85, 95, 109)'); + }); + + it('should render a correct color (grey, low)', () => { + const { status, dot } = getStatus( + , + ); + expect(status).toHaveStyle( + 'background-color: rgba(255, 255, 255, 0); color: rgb(39, 46, 53)', + ); + expect(dot).toHaveStyle('background-color: rgb(85, 95, 109)'); + }); + + it('should render a correct color (blue, high)', () => { + const { status, dot } = getStatus( + , + ); + expect(status).toHaveStyle( + 'background-color: rgb(48, 98, 212); color: rgb(255, 255, 255)', + ); + expect(dot).toHaveStyle('background-color: rgb(255, 255, 255)'); + }); + + it('should render a correct color (green, high)', () => { + const { status, dot } = getStatus( + , + ); + expect(status).toHaveStyle( + 'background-color: rgb(29, 124, 77); color: rgb(255, 255, 255)', + ); + expect(dot).toHaveStyle('background-color: rgb(255, 255, 255)'); + }); + + it('should render a correct color (red, high)', () => { + const { status, dot } = getStatus( + , + ); + expect(status).toHaveStyle( + 'background-color: rgb(197, 52, 52); color: rgb(255, 255, 255)', + ); + expect(dot).toHaveStyle('background-color: rgb(255, 255, 255)'); + }); + + it('should render a correct color (orange, high)', () => { + const { status, dot } = getStatus( + , + ); + expect(status).toHaveStyle( + 'background-color: rgb(245, 150, 56); color: rgb(39, 46, 53)', + ); + expect(dot).toHaveStyle('background-color: rgb(39, 46, 53)'); + }); + + it('should render a correct color (orange, medium)', () => { + const { status, dot } = getStatus( + , + ); + expect(status).toHaveStyle( + 'background-color: rgb(252, 222, 192); color: rgb(122, 69, 16)', + ); + expect(dot).toHaveStyle('background-color: rgb(245, 150, 56)'); + }); + + it('should render a correct color (orange, low)', () => { + const { status, dot } = getStatus( + , + ); + expect(status).toHaveStyle( + 'background-color: rgba(255, 255, 255, 0); color: rgb(122, 69, 16)', + ); + expect(dot).toHaveStyle('background-color: rgb(245, 150, 56)'); + }); + + customPropTester(, { + containerId: 'status', + props: { + appearance: ['grey', 'blue', 'green', 'red', 'orange'], + emphasis: ['high', 'medium', 'low'], + }, + }); +}); diff --git a/src/components/Status/Status.tsx b/src/components/Status/Status.tsx new file mode 100644 index 00000000..4673c850 --- /dev/null +++ b/src/components/Status/Status.tsx @@ -0,0 +1,26 @@ +import { FC, useMemo } from 'react'; + +import { StatusProps } from './Status.props'; +import { stylesBuilder } from './stylesBuilder'; + +import { tet } from '@/tetrisly'; + +export const Status: FC = ({ + appearance, + emphasis, + label, + custom, + ...restProps +}) => { + const styles = useMemo( + () => stylesBuilder(appearance, custom, emphasis), + [custom, emphasis, appearance], + ); + + return ( + + + {label} + + ); +}; diff --git a/src/components/Status/StatusAppearance.type.ts b/src/components/Status/StatusAppearance.type.ts new file mode 100644 index 00000000..c8b44b22 --- /dev/null +++ b/src/components/Status/StatusAppearance.type.ts @@ -0,0 +1 @@ +export type StatusAppearance = 'grey' | 'blue' | 'green' | 'red' | 'orange'; diff --git a/src/components/Status/StatusEmphasis.type.ts b/src/components/Status/StatusEmphasis.type.ts new file mode 100644 index 00000000..9a95eb87 --- /dev/null +++ b/src/components/Status/StatusEmphasis.type.ts @@ -0,0 +1 @@ +export type StatusEmphasis = 'high' | 'medium' | 'low'; diff --git a/src/components/Status/index.ts b/src/components/Status/index.ts new file mode 100644 index 00000000..9dd9a067 --- /dev/null +++ b/src/components/Status/index.ts @@ -0,0 +1,3 @@ +export { Status } from './Status'; +export type { StatusProps } from './Status.props'; +export { statusStyles } from './Status.styles'; diff --git a/src/components/Status/stylesBuilder.ts b/src/components/Status/stylesBuilder.ts new file mode 100644 index 00000000..b939d908 --- /dev/null +++ b/src/components/Status/stylesBuilder.ts @@ -0,0 +1,44 @@ +import { defaultConfig, StatusConfig } from './Status.styles'; +import type { StatusAppearance } from './StatusAppearance.type'; +import type { StatusEmphasis } from './StatusEmphasis.type'; + +import { mergeConfigWithCustom } from '@/services'; +import { BaseProps } from '@/types'; + +type StylesBuilderParams = { + container: BaseProps; + dot: BaseProps; +}; + +export const stylesBuilder = ( + appearance?: StatusAppearance, + custom?: StatusConfig, + emphasis?: StatusEmphasis, +): StylesBuilderParams => { + const { + innerElements, + appearance: containerAppearance, + dot, + ...container + } = mergeConfigWithCustom({ defaultConfig, custom }); + + const { appearance: dotAppearanceStyle, ...dotAppearance } = dot; + + const containerStyles = + appearance && + emphasis && + containerAppearance[appearance].emphasis[emphasis]; + const dotStyles = + appearance && emphasis && dot.appearance[appearance].emphasis[emphasis]; + + return { + container: { + ...container, + ...containerStyles, + }, + dot: { + ...dotAppearance, + ...dotStyles, + }, + }; +}; diff --git a/src/docs-components/StatusDocs.tsx b/src/docs-components/StatusDocs.tsx new file mode 100644 index 00000000..b2dcb865 --- /dev/null +++ b/src/docs-components/StatusDocs.tsx @@ -0,0 +1,62 @@ +import { capitalize } from 'lodash'; + +import { SectionHeader } from './common/SectionHeader'; +import { State } from './common/States'; + +import { Status } from '@/components/Status'; +import { tet } from '@/tetrisly'; + +const appearances = ['grey', 'blue', 'green', 'red', 'orange'] as const; +const emphases = ['high', 'medium', 'low'] as const; + +export const StatusDocs = () => ( + <> + {emphases.map((emphasis) => ( + + + {capitalize(emphasis)} Emphasis + + + + {appearances.map((appearance, i) => ( + + + + + + + ))} + + + + ))} + +);