diff --git a/src/components/NewItemButton/NewItemButton.stories.tsx b/src/components/NewItemButton/NewItemButton.stories.tsx
new file mode 100644
index 00000000..143e6615
--- /dev/null
+++ b/src/components/NewItemButton/NewItemButton.stories.tsx
@@ -0,0 +1,54 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { NewItemButton } from './NewItemButton';
+
+import { NewItemButtonDocs } from '@/docs-components/NewItemButtonDocs';
+import { TetDocs } from '@/docs-components/TetDocs';
+
+const meta = {
+ title: 'NewItemButton',
+ component: NewItemButton,
+ tags: ['autodocs'],
+ argTypes: {},
+ args: {
+ state: 'normal',
+ text: 'text',
+ },
+ parameters: {
+ docs: {
+ description: {
+ component:
+ 'A dedicated button for creating new items, such as files, events, or tasks, typically placed in a prominent location and distinguished by an icon or label.',
+ },
+ page: () => (
+
+
+
+ ),
+ },
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ state: 'normal',
+ text: 'New category',
+ },
+};
+
+export const Alert: Story = {
+ args: {
+ state: 'alert',
+ text: 'Alert!',
+ },
+};
+
+export const Disabled: Story = {
+ args: {
+ state: 'disabled',
+ text: 'Disabled',
+ },
+};
diff --git a/src/components/NewItemButton/NewItemButton.styles.ts b/src/components/NewItemButton/NewItemButton.styles.ts
new file mode 100644
index 00000000..8bddf157
--- /dev/null
+++ b/src/components/NewItemButton/NewItemButton.styles.ts
@@ -0,0 +1,69 @@
+import { NewItemButtonState } from './NewItemButtonState.type';
+
+import type { BaseProps } from '@/types/BaseProps';
+
+export type NewItemButtonConfig = {
+ state?: Partial>;
+ innerElements?: {
+ text?: BaseProps;
+ };
+} & BaseProps;
+
+export const defaultConfig = {
+ display: 'inline-flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ gap: '$space-component-gap-small',
+ minH: '120px',
+ minWidth: '120px',
+ h: '100%',
+ w: '100%',
+ borderWidth: '$border-width-small',
+ borderStyle: '$border-style-dashed',
+ borderRadius: '$border-radius-large',
+ padding: '$space-component-padding-xLarge',
+ text: '$typo-body-medium',
+ textAlign: 'center',
+ whiteSpace: 'nowrap',
+ color: '$color-action-neutral-normal',
+ backgroundColor: '$color-interaction-background-formField',
+ transition: true,
+ transitionDuration: 200,
+ outline: {
+ focus: 'solid',
+ },
+ outlineColor: {
+ focus: '$color-interaction-focus-default',
+ },
+ outlineWidth: {
+ focus: '$border-width-focus',
+ },
+ outlineOffset: 1,
+ state: {
+ normal: {
+ borderColor: {
+ _: '$color-border-neutral-subtle',
+ hover: '$color-interaction-border-hover',
+ },
+ },
+ alert: {
+ borderColor: '$color-interaction-border-alert',
+ },
+ disabled: {
+ borderColor: '$color-border-neutral-subtle',
+ opacity: '$opacity-disabled',
+ pointerEvents: 'none',
+ },
+ },
+ innerElements: {
+ text: {
+ text: '$typo-body-medium',
+ color: '$color-content-primary',
+ },
+ },
+} as const satisfies NewItemButtonConfig;
+
+export const newItemButtonStyles = {
+ defaultConfig,
+};
diff --git a/src/components/NewItemButton/NewItemButton.test.tsx b/src/components/NewItemButton/NewItemButton.test.tsx
new file mode 100644
index 00000000..5d98a0cd
--- /dev/null
+++ b/src/components/NewItemButton/NewItemButton.test.tsx
@@ -0,0 +1,53 @@
+import { vi } from 'vitest';
+
+import { NewItemButton } from './NewItemButton';
+import { NewItemButtonState } from './NewItemButtonState.type';
+import { render, screen, fireEvent } from '../../tests/render';
+
+describe('NewItemButton', () => {
+ const states: NewItemButtonState[] = ['normal', 'alert', 'disabled'];
+
+ it('should render the NewItemButton ', () => {
+ render();
+ const button = screen.getByTestId('new-item-button');
+ expect(button).toBeInTheDocument();
+ });
+
+ it('should be disabled if disabled state is passed', () => {
+ render();
+ const button = screen.getByTestId('new-item-button');
+ expect(button).toBeDisabled();
+ expect(button).toHaveStyle('pointer-events: none');
+ expect(button).toHaveStyle('opacity: 0.5');
+ });
+
+ states.forEach((state) => {
+ describe(`State: ${state}`, () => {
+ it('should render the NewItemButton', () => {
+ render();
+ const button = screen.getByTestId('new-item-button');
+ expect(button).toBeInTheDocument();
+ });
+
+ it('should render correct text', () => {
+ render();
+ const button = screen.getByTestId('new-item-button');
+ expect(button).toHaveTextContent('Hello there!');
+ });
+
+ it('should handle onClick properly when clicked', () => {
+ const onClickMock = vi.fn();
+ render();
+
+ const button = screen.getByTestId('new-item-button');
+ expect(button).toBeInTheDocument();
+ fireEvent.click(button);
+ if (state !== 'disabled') {
+ expect(onClickMock).toHaveBeenCalled();
+ } else {
+ expect(onClickMock).not.toHaveBeenCalled();
+ }
+ });
+ });
+ });
+});
diff --git a/src/components/NewItemButton/NewItemButton.tsx b/src/components/NewItemButton/NewItemButton.tsx
new file mode 100644
index 00000000..621ff56f
--- /dev/null
+++ b/src/components/NewItemButton/NewItemButton.tsx
@@ -0,0 +1,28 @@
+import { Icon } from '@virtuslab/tetrisly-icons';
+import { useMemo, type FC } from 'react';
+
+import { NewItemButtonProps } from './NewItemButtons.props';
+import { stylesBuilder } from './stylesBuilder';
+
+import { tet } from '@/tetrisly';
+
+export const NewItemButton: FC = ({
+ state = 'normal',
+ text,
+ custom,
+ ...rest
+}) => {
+ const styles = useMemo(() => stylesBuilder(state, custom), [custom, state]);
+
+ return (
+
+
+ {!!text && {text}}
+
+ );
+};
diff --git a/src/components/NewItemButton/NewItemButtonState.type.ts b/src/components/NewItemButton/NewItemButtonState.type.ts
new file mode 100644
index 00000000..34ed6c2e
--- /dev/null
+++ b/src/components/NewItemButton/NewItemButtonState.type.ts
@@ -0,0 +1 @@
+export type NewItemButtonState = 'normal' | 'alert' | 'disabled';
diff --git a/src/components/NewItemButton/NewItemButtons.props.ts b/src/components/NewItemButton/NewItemButtons.props.ts
new file mode 100644
index 00000000..4331f8c7
--- /dev/null
+++ b/src/components/NewItemButton/NewItemButtons.props.ts
@@ -0,0 +1,10 @@
+import { ButtonHTMLAttributes } from 'react';
+
+import { NewItemButtonConfig } from './NewItemButton.styles';
+import { NewItemButtonState } from './NewItemButtonState.type';
+
+export type NewItemButtonProps = {
+ state?: NewItemButtonState | undefined;
+ text?: string;
+ custom?: NewItemButtonConfig;
+} & Omit, 'disabled' | 'color'>;
diff --git a/src/components/NewItemButton/index.ts b/src/components/NewItemButton/index.ts
new file mode 100644
index 00000000..68344e98
--- /dev/null
+++ b/src/components/NewItemButton/index.ts
@@ -0,0 +1,3 @@
+export { NewItemButton } from './NewItemButton';
+export type { NewItemButtonProps } from './NewItemButtons.props';
+export { newItemButtonStyles } from './NewItemButton.styles';
diff --git a/src/components/NewItemButton/stylesBuilder.ts b/src/components/NewItemButton/stylesBuilder.ts
new file mode 100644
index 00000000..c3ee56fd
--- /dev/null
+++ b/src/components/NewItemButton/stylesBuilder.ts
@@ -0,0 +1,35 @@
+import { NewItemButtonConfig, defaultConfig } from './NewItemButton.styles';
+import { NewItemButtonState } from './NewItemButtonState.type';
+
+import { mergeConfigWithCustom } from '@/services';
+import { BaseProps } from '@/types/BaseProps';
+
+type NewItemButtonStyleBuilder = {
+ container: BaseProps;
+ text: BaseProps;
+};
+
+export const stylesBuilder = (
+ state: NewItemButtonState,
+ custom?: NewItemButtonConfig,
+): NewItemButtonStyleBuilder => {
+ const {
+ innerElements,
+ state: containerState,
+ ...container
+ } = mergeConfigWithCustom({
+ defaultConfig,
+ custom,
+ });
+
+ const { text } = innerElements;
+ const containerStyles = containerState[state];
+
+ return {
+ container: {
+ ...container,
+ ...containerStyles,
+ },
+ text,
+ };
+};
diff --git a/src/docs-components/NewItemButtonDocs.tsx b/src/docs-components/NewItemButtonDocs.tsx
new file mode 100644
index 00000000..cde0667c
--- /dev/null
+++ b/src/docs-components/NewItemButtonDocs.tsx
@@ -0,0 +1,42 @@
+import { SectionHeader } from './common/SectionHeader';
+import { States } from './common/States';
+
+import { NewItemButton } from '@/components/NewItemButton';
+import { tet } from '@/tetrisly';
+
+const states = ['normal', 'alert', 'disabled'] as const;
+
+export const NewItemButtonDocs = () => (
+
+
+ State
+
+
+
+
+ {states.map((state) => (
+
+
+
+ ))}
+
+
+
+);
diff --git a/src/index.ts b/src/index.ts
index cfb53891..492b3ff3 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -15,6 +15,7 @@ export * from './components/InlineMessage';
export * from './components/InlineSearchInput';
export * from './components/Label';
export * from './components/Loader';
+export * from './components/NewItemButton';
export * from './components/Popover';
export * from './components/RadioButton';
export * from './components/RadioButtonGroup';
diff --git a/src/theme/theme.ts b/src/theme/theme.ts
index 8d1bea2d..e512203a 100644
--- a/src/theme/theme.ts
+++ b/src/theme/theme.ts
@@ -973,7 +973,7 @@ const fixedTokens = {
borderStyles: {
'$border-style-none': 'none',
'$border-style-solid': 'solid',
- '$border-style-dashed': 'dashed solid',
+ '$border-style-dashed': 'dashed',
},
fonts: { '$font-family-primary': 'Inter' },
fontSizes: {