From c81b1a6997eab528f56f6ef54e565dc947d48ce5 Mon Sep 17 00:00:00 2001
From: Karolina Szarek <74671633+karolinaszarek@users.noreply.github.com>
Date: Thu, 25 Apr 2024 10:35:57 +0200
Subject: [PATCH 1/4] feat: TET-863 add Status component (#134)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: TET-863 add Status component
* feat: TET-863 add type to an import
Co-authored-by: Mateusz Wlekliński <114148518+mwleklinskiVL@users.noreply.github.com>
* feat: TET-863 review changes
* feat: TET-863 review changes
* feat: TET-863 fix styling
* feat: TET-863 change required prop
* feat: TET-863 move stylesBuilder into main component folder
* feat: TET-863-fix-build
* feat: TET-863 fix dot styles
---------
Co-authored-by: Mateusz Wlekliński <114148518+mwleklinskiVL@users.noreply.github.com>
---
src/components/Status/Status.props.ts | 10 +
src/components/Status/Status.stories.tsx | 49 ++++
src/components/Status/Status.styles.ts | 214 ++++++++++++++++++
src/components/Status/Status.test.tsx | 117 ++++++++++
src/components/Status/Status.tsx | 26 +++
.../Status/StatusAppearance.type.ts | 1 +
src/components/Status/StatusEmphasis.type.ts | 1 +
src/components/Status/index.ts | 3 +
src/components/Status/stylesBuilder.ts | 44 ++++
src/docs-components/StatusDocs.tsx | 62 +++++
10 files changed, 527 insertions(+)
create mode 100644 src/components/Status/Status.props.ts
create mode 100644 src/components/Status/Status.stories.tsx
create mode 100644 src/components/Status/Status.styles.ts
create mode 100644 src/components/Status/Status.test.tsx
create mode 100644 src/components/Status/Status.tsx
create mode 100644 src/components/Status/StatusAppearance.type.ts
create mode 100644 src/components/Status/StatusEmphasis.type.ts
create mode 100644 src/components/Status/index.ts
create mode 100644 src/components/Status/stylesBuilder.ts
create mode 100644 src/docs-components/StatusDocs.tsx
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) => (
+
+
+
+
+
+
+ ))}
+
+
+
+ ))}
+ >
+);
From d916220f5d6f3e022ca8ec81e76ec69df2fc17e4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20Jamro=C5=BCek?=
<167776055+mjamrozekvl@users.noreply.github.com>
Date: Tue, 7 May 2024 09:52:21 +0200
Subject: [PATCH 2/4] feat: NO-JIRA Corner Dialog component (#135)
* feat: NO-JIRA basic CornerDialog scaffold
* refactor: NO-JIRA styles builder and corner dialog docs
* refactor: NO-JIRA remove unnecessary stylesBuilder directory
* test: NO-JIRA add tests to CornerDialog
* docs: NO-JIRA add missing figma scenarios
* docs: NO-JIRA add custom content story example
* style: NO-JIRA sizes according to figma
* refactor: NO-JIRA export styles
* style: NO-JIRA set react event object to optional
* refactor: NO-JIRA exclude size and hasDropdownIndicator from action button props
* refactor: NO-JIRA replace color with appropriate variable
* refactor: NO-JIRA replace rest of wrongly defined variables
* fix: NO-JIRA introduce DistributiveOmit typescript utility type
reason described here: https://github.com/VirtusLab/tetrisly-react/pull/135#discussion_r1580900884
* style: NO-JIRA disable @typescript-eslint/no-explicit-any rule in DistributiveOmit util
* refactor: NO-JIRA remove unnecessary React. type prefix
* fix: NO-JIRA export CornerDialog and Status components in src/index.ts file
* fix: NO-JIRA use -500 instead of 40px unit
---
.../CornerDialog/CornerDialog.props.ts | 18 ++
.../CornerDialog/CornerDialog.stories.tsx | 170 ++++++++++++++++++
.../CornerDialog/CornerDialog.styles.ts | 84 +++++++++
.../CornerDialog/CornerDialog.test.tsx | 96 ++++++++++
src/components/CornerDialog/CornerDialog.tsx | 69 +++++++
src/components/CornerDialog/index.ts | 3 +
src/components/CornerDialog/stylesBuilder.ts | 39 ++++
src/docs-components/CornerDialogDocs.tsx | 59 ++++++
src/index.ts | 2 +
src/utility-types/DistributiveOmit.ts | 4 +
10 files changed, 544 insertions(+)
create mode 100644 src/components/CornerDialog/CornerDialog.props.ts
create mode 100644 src/components/CornerDialog/CornerDialog.stories.tsx
create mode 100644 src/components/CornerDialog/CornerDialog.styles.ts
create mode 100644 src/components/CornerDialog/CornerDialog.test.tsx
create mode 100644 src/components/CornerDialog/CornerDialog.tsx
create mode 100644 src/components/CornerDialog/index.ts
create mode 100644 src/components/CornerDialog/stylesBuilder.ts
create mode 100644 src/docs-components/CornerDialogDocs.tsx
create mode 100644 src/utility-types/DistributiveOmit.ts
diff --git a/src/components/CornerDialog/CornerDialog.props.ts b/src/components/CornerDialog/CornerDialog.props.ts
new file mode 100644
index 00000000..ff73464a
--- /dev/null
+++ b/src/components/CornerDialog/CornerDialog.props.ts
@@ -0,0 +1,18 @@
+import type { ReactNode, MouseEvent } from 'react';
+
+import { CornerDialogConfig } from './CornerDialog.styles';
+import { DefaultButtonProps } from '../Button/Button.props';
+
+import { DistributiveOmit } from '@/utility-types/DistributiveOmit';
+
+export type CornerDialogProps = {
+ custom?: CornerDialogConfig;
+ intent?: 'none' | 'warning' | 'negative';
+ title: string;
+ content: ReactNode;
+ actions?: DistributiveOmit<
+ DefaultButtonProps,
+ 'size' | 'hasDropdownIndicator'
+ >[];
+ onCloseClick?: (e?: MouseEvent) => void;
+};
diff --git a/src/components/CornerDialog/CornerDialog.stories.tsx b/src/components/CornerDialog/CornerDialog.stories.tsx
new file mode 100644
index 00000000..b0a9f2ed
--- /dev/null
+++ b/src/components/CornerDialog/CornerDialog.stories.tsx
@@ -0,0 +1,170 @@
+import { action } from '@storybook/addon-actions';
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { CornerDialog } from './CornerDialog';
+
+import { CornerDialogDocs } from '@/docs-components/CornerDialogDocs';
+import { TetDocs } from '@/docs-components/TetDocs';
+import { tet } from '@/tetrisly';
+
+const meta = {
+ title: 'CornerDialog',
+ component: CornerDialog,
+ tags: ['autodocs'],
+ argTypes: {},
+ args: {
+ intent: 'none',
+ title: 'Corner Dialog',
+ content: 'Description',
+ actions: undefined,
+ onCloseClick: action('onCloseClick'),
+ },
+ parameters: {
+ docs: {
+ description: {
+ component:
+ 'A small, non-intrusive window that appears in the corner of the screen to convey contextual information or prompt user interaction. Often used for hints, tips, or non-essential notifications.',
+ },
+ page: () => (
+
+
+
+ ),
+ },
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ intent: 'none',
+ title: 'Corner Dialog',
+ content: 'Description',
+ actions: [
+ { label: 'Action', onClick: action('onClick') },
+ {
+ label: 'Primary Action',
+ onClick: action('onClick'),
+ appearance: 'primary',
+ },
+ ],
+ onCloseClick: action('onCloseClick'),
+ },
+};
+
+export const Decision: Story = {
+ args: {
+ intent: 'none',
+ title: 'Title',
+ content: 'Description',
+ actions: [
+ { label: 'Cancel', onClick: action('onCancelClick') },
+ {
+ label: 'Accept',
+ onClick: action('onAcceptClick'),
+ appearance: 'primary',
+ },
+ ],
+ onCloseClick: action('onCloseClick'),
+ },
+};
+
+export const Confirmation: Story = {
+ args: {
+ intent: 'none',
+ title: 'Title',
+ content: 'Description',
+ actions: [
+ {
+ label: 'Accept',
+ onClick: action('onAcceptClick'),
+ appearance: 'primary',
+ },
+ ],
+ onCloseClick: undefined,
+ },
+};
+
+export const NegativeWithDestructiveButton: Story = {
+ args: {
+ intent: 'negative',
+ title: 'Title',
+ content: 'Description',
+ actions: [
+ { label: 'Cancel', onClick: action('onCancelClick') },
+ {
+ label: 'Remove',
+ onClick: action('onRemoveClick'),
+ appearance: `primary`,
+ intent: 'destructive',
+ },
+ ],
+ onCloseClick: action('onCloseClick'),
+ },
+};
+
+export const WarningAndAdditionalAction: Story = {
+ args: {
+ intent: 'warning',
+ title: 'Title',
+ content: 'Description',
+ actions: [
+ {
+ label: 'Find out more',
+ onClick: action('onFindOutMoreClick'),
+ custom: {
+ default: {
+ position: 'absolute',
+ left: 0,
+ },
+ },
+ },
+ { label: 'Cancel', onClick: action('onCancelClick') },
+ {
+ label: 'Accept',
+ onClick: action('onAcceptClick'),
+ appearance: 'primary',
+ },
+ ],
+ onCloseClick: action('onCloseClick'),
+ custom: {
+ innerElements: {
+ footer: {
+ position: 'relative',
+ },
+ },
+ },
+ },
+};
+
+export const CustomContent: Story = {
+ args: {
+ intent: 'none',
+ title: 'Corner Dialog with custom content',
+ content: (
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit.{' '}
+
+ Morbi pellentesque elit ut sem accumsan, eget maximus erat eleifend.
+
+ Vestibulum ac tortor nunc.{' '}
+
+ Nam tincidunt nibh eget nulla aliquet, et auctor dui rhoncus. Donec
+ bibendum rhoncus lacus vel scelerisque.
+
+ Suspendisse feugiat ligula quis eros interdum varius. Ut nec ex est.
+
+ ),
+ actions: [
+ { label: 'Action', onClick: action('onClick') },
+ {
+ label: 'Primary Action',
+ onClick: action('onClick'),
+ appearance: 'primary',
+ },
+ ],
+ onCloseClick: action('onCloseClick'),
+ },
+};
diff --git a/src/components/CornerDialog/CornerDialog.styles.ts b/src/components/CornerDialog/CornerDialog.styles.ts
new file mode 100644
index 00000000..c43b69b7
--- /dev/null
+++ b/src/components/CornerDialog/CornerDialog.styles.ts
@@ -0,0 +1,84 @@
+import type { BaseProps } from '@/types/BaseProps';
+
+export type CornerDialogFooterConfig = {
+ actions?: BaseProps;
+} & BaseProps;
+
+export type CornerDialogConfig = BaseProps & {
+ innerElements?: {
+ intentIndicator?: BaseProps;
+ intentWarning?: BaseProps;
+ intentNegative?: BaseProps;
+ body?: BaseProps;
+ header?: BaseProps;
+ headerTitle?: BaseProps;
+ closeButton?: BaseProps;
+ content?: BaseProps;
+ footer?: CornerDialogFooterConfig;
+ };
+};
+
+export const defaultConfig = {
+ display: 'flex',
+ w: 'fit-content',
+ minWidth: '420px',
+ p: '$space-component-padding-2xLarge',
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ gap: '$space-component-padding-large',
+ borderRadius: '$border-radius-xLarge',
+ bg: '$color-interaction-background-modeless',
+ boxShadow: '$elevation-bottom-300',
+ borderWidth: '$border-width-small',
+ borderStyle: '$border-style-solid',
+ borderColor: '$color-border-defaultA',
+ overflow: 'hidden',
+ innerElements: {
+ intentIndicator: {
+ h: '$size-xSmall',
+ display: 'flex',
+ alignItems: 'flex-end',
+ },
+ intentWarning: {
+ color: '$color-content-warning-secondary',
+ },
+ intentNegative: {
+ color: '$color-content-negative-secondary',
+ },
+ body: {
+ display: 'flex',
+ flexGrow: 1,
+ flexDirection: 'column',
+ justifyContent: 'space-between',
+ gap: '$space-component-padding-large',
+ },
+ header: {
+ display: 'flex',
+ alignSelf: 'stretch',
+ alignItems: 'center',
+ },
+ headerTitle: {
+ display: 'flex',
+ flexGrow: 1,
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ color: '$color-content-primary',
+ text: '$typo-body-strong-large',
+ },
+ closeButton: {},
+ content: {
+ text: '$typo-body-medium',
+ color: '$color-content-secondary',
+ },
+ footer: {
+ display: 'flex',
+ alignSelf: 'stretch',
+ justifyContent: 'flex-end',
+ gap: '$space-component-padding-small',
+ },
+ },
+} as const satisfies CornerDialogConfig;
+
+export const cornerDialogStyles = {
+ defaultConfig,
+};
diff --git a/src/components/CornerDialog/CornerDialog.test.tsx b/src/components/CornerDialog/CornerDialog.test.tsx
new file mode 100644
index 00000000..c697d6a6
--- /dev/null
+++ b/src/components/CornerDialog/CornerDialog.test.tsx
@@ -0,0 +1,96 @@
+import { vi } from 'vitest';
+
+import { CornerDialog } from './CornerDialog';
+import { render, screen, fireEvent } from '../../tests/render';
+
+describe('CornerDialog', () => {
+ it('should render empty corner dialog', () => {
+ render();
+ const cornerDialog = screen.getByTestId('corner-dialog');
+ expect(cornerDialog).toBeInTheDocument();
+
+ const header = screen.getByTestId('corner-dialog-header');
+ expect(header).toBeInTheDocument();
+
+ const headerTitle = screen.getByTestId('corner-dialog-header-title');
+ expect(headerTitle).toBeInTheDocument();
+ expect(headerTitle).toHaveTextContent('Title');
+
+ const content = screen.getByTestId('corner-dialog-content');
+ expect(content).toBeInTheDocument();
+ expect(content).toHaveTextContent('Content');
+ });
+
+ it('should render warning corner dialog', () => {
+ render();
+ const warningIcon = screen.getByTestId('warning-icon');
+ expect(warningIcon).toBeInTheDocument();
+ });
+
+ it('should render negative corner dialog', () => {
+ render();
+ const negativeIcon = screen.getByTestId('negative-icon');
+ expect(negativeIcon).toBeInTheDocument();
+ });
+
+ it('should render close icon when onCloseClick handler is provided', () => {
+ render(
+ {}}
+ />,
+ );
+ const closeIcon = screen.getByTestId('close-icon');
+ expect(closeIcon).toBeInTheDocument();
+ });
+
+ it('should render footer if at least one action is provided', () => {
+ render(
+ ,
+ );
+ const footer = screen.getByTestId('corner-dialog-footer');
+ expect(footer).toBeInTheDocument();
+ });
+
+ it('should render footer with 2 actions', async () => {
+ render(
+ ,
+ );
+
+ const firstActionButton = await screen.findByText('First action');
+ expect(firstActionButton).toBeInTheDocument();
+
+ const secondActionButton = await screen.findByText('Second action');
+ expect(secondActionButton).toBeInTheDocument();
+ });
+
+ it('should call onCloseClick after click to close icon', () => {
+ const onCloseClickMock = vi.fn();
+
+ render(
+ ,
+ );
+
+ const closeIcon = screen.getByTestId('close-icon');
+ expect(closeIcon).toBeInTheDocument();
+ fireEvent.click(closeIcon);
+ expect(onCloseClickMock).toHaveBeenCalled();
+ });
+});
diff --git a/src/components/CornerDialog/CornerDialog.tsx b/src/components/CornerDialog/CornerDialog.tsx
new file mode 100644
index 00000000..770f0e24
--- /dev/null
+++ b/src/components/CornerDialog/CornerDialog.tsx
@@ -0,0 +1,69 @@
+import { Icon } from '@virtuslab/tetrisly-icons';
+import { FC } from 'react';
+
+import { CornerDialogProps } from './CornerDialog.props';
+import { stylesBuilder } from './stylesBuilder';
+
+import { Button } from '@/components/Button';
+import { IconButton } from '@/components/IconButton';
+import { tet } from '@/tetrisly';
+
+export const CornerDialog: FC = ({
+ custom,
+ intent = 'none',
+ title,
+ content,
+ actions,
+ onCloseClick,
+}) => {
+ const styles = stylesBuilder(custom);
+
+ return (
+
+ {intent === 'warning' && (
+
+
+
+ )}
+
+ {intent === 'negative' && (
+
+
+
+ )}
+
+
+
+
+ {title}
+ {!!onCloseClick && (
+
+ )}
+
+
+
+
+ {content}
+
+
+ {actions && (
+
+ {actions.map((action) => (
+
+ ))}
+
+ )}
+
+
+ );
+};
diff --git a/src/components/CornerDialog/index.ts b/src/components/CornerDialog/index.ts
new file mode 100644
index 00000000..593db68c
--- /dev/null
+++ b/src/components/CornerDialog/index.ts
@@ -0,0 +1,3 @@
+export * from './CornerDialog';
+export * from './CornerDialog.props';
+export { cornerDialogStyles } from './CornerDialog.styles';
diff --git a/src/components/CornerDialog/stylesBuilder.ts b/src/components/CornerDialog/stylesBuilder.ts
new file mode 100644
index 00000000..edec6fd1
--- /dev/null
+++ b/src/components/CornerDialog/stylesBuilder.ts
@@ -0,0 +1,39 @@
+import { CornerDialogConfig, defaultConfig } from './CornerDialog.styles';
+
+import { mergeConfigWithCustom } from '@/services/mergeConfigWithCustom/mergeConfigWithCutom';
+import { BaseProps } from '@/types/BaseProps';
+
+type CornerDialogStylesBuilder = {
+ container: BaseProps;
+ intentIndicator: BaseProps;
+ intentWarning: BaseProps;
+ intentNegative: BaseProps;
+ body: BaseProps;
+ header: BaseProps;
+ headerTitle: BaseProps;
+ closeButton: BaseProps<'appearance'>;
+ content: BaseProps;
+ footer: BaseProps;
+};
+
+export const stylesBuilder = (
+ custom?: CornerDialogConfig,
+): CornerDialogStylesBuilder => {
+ const { innerElements, ...container } = mergeConfigWithCustom({
+ defaultConfig,
+ custom,
+ });
+
+ return {
+ container,
+ intentIndicator: innerElements.intentIndicator,
+ intentWarning: innerElements.intentWarning,
+ intentNegative: innerElements.intentNegative,
+ body: innerElements.body,
+ header: innerElements.header,
+ headerTitle: innerElements.headerTitle,
+ closeButton: innerElements.closeButton,
+ content: innerElements.content,
+ footer: innerElements.footer,
+ };
+};
diff --git a/src/docs-components/CornerDialogDocs.tsx b/src/docs-components/CornerDialogDocs.tsx
new file mode 100644
index 00000000..337d0a77
--- /dev/null
+++ b/src/docs-components/CornerDialogDocs.tsx
@@ -0,0 +1,59 @@
+import { capitalize } from 'lodash';
+
+import { SectionHeader } from './common/SectionHeader';
+
+import { CornerDialog } from '@/components/CornerDialog';
+import { tet } from '@/tetrisly';
+
+const intents = ['none', 'warning', 'negative'] as const;
+
+export const CornerDialogDocs = () => (
+
+
+ Intent
+
+
+ {intents.map((intent) => (
+
+
+ {capitalize(intent)}
+
+
+
+ {}}
+ />
+
+
+ ))}
+
+
+);
diff --git a/src/index.ts b/src/index.ts
index f8d3dbfb..cfb53891 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -4,6 +4,7 @@ export * from './components/Badge';
export * from './components/Button';
export * from './components/Checkbox';
export * from './components/CheckboxGroup';
+export * from './components/CornerDialog';
export * from './components/Counter';
export * from './components/Divider';
export * from './components/HelperText';
@@ -20,6 +21,7 @@ export * from './components/RadioButtonGroup';
export * from './components/SearchInput';
export * from './components/Select';
export * from './components/SocialButton';
+export * from './components/Status';
export * from './components/StatusDot';
export * from './components/Tag';
export * from './components/TextInput';
diff --git a/src/utility-types/DistributiveOmit.ts b/src/utility-types/DistributiveOmit.ts
new file mode 100644
index 00000000..d5b0cc9e
--- /dev/null
+++ b/src/utility-types/DistributiveOmit.ts
@@ -0,0 +1,4 @@
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export type DistributiveOmit = T extends any
+ ? Omit
+ : never;
From 273513755478a07b94758e9fe93109d946a8e83d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20Jamro=C5=BCek?=
<167776055+mjamrozekvl@users.noreply.github.com>
Date: Tue, 7 May 2024 10:21:45 +0200
Subject: [PATCH 3/4] style: NO-JIRA typos found during onboarding (#137)
* style: NO-JIRA typos
* style: NO-JIRA typos in README.md
* fix: NO-JIRA typo in stylesBuilder.ts after merge
---
README.md | 6 +++---
src/components/Badge/stylesBuilder/stylesBuilder.ts | 2 +-
src/components/Button/stylesBuilder/stylesBuilder.ts | 6 +++---
src/components/Card/CardFooter/CardFooter.props.ts | 2 +-
src/components/Card/CardHeader/CardHeader.props.ts | 2 +-
src/components/CornerDialog/stylesBuilder.ts | 2 +-
src/components/Select/Select.tsx | 2 +-
src/components/TextInput/TextInput.props.ts | 4 ++--
src/components/TextInput/TextInput.stories.tsx | 6 +++---
src/components/TextInput/TextInput.styles.ts | 4 ++--
src/components/TextInput/TextInput.tsx | 2 +-
src/components/TextInput/stylesBuilder/stylesBuilder.ts | 4 ++--
src/docs-components/LoaderDocs.tsx | 4 ++--
src/services/mergeConfigWithCustom/index.ts | 2 +-
.../mergeConfigWithCustom/mergeConfigWithCustom.test.ts | 2 +-
.../{mergeConfigWithCutom.ts => mergeConfigWithCustom.ts} | 0
16 files changed, 25 insertions(+), 25 deletions(-)
rename src/services/mergeConfigWithCustom/{mergeConfigWithCutom.ts => mergeConfigWithCustom.ts} (100%)
diff --git a/README.md b/README.md
index aa9550be..9d8481c1 100644
--- a/README.md
+++ b/README.md
@@ -200,8 +200,8 @@ All Tetrisly components have a `custom` prop. It makes it possible to customize
If you want to change any of button styles, you can make it by passing custom props with object based on
specific component config.
-For instance, to change background-color of appereance="primary" intent="secondary" variant to pink, just pass
-refferenced object structure:
+For instance, to change background-color of appearance="primary" intent="secondary" variant to pink, just pass
+referenced object structure:
```jsx