diff --git a/.changeset/tidy-singers-travel.md b/.changeset/tidy-singers-travel.md new file mode 100644 index 00000000..67a88a85 --- /dev/null +++ b/.changeset/tidy-singers-travel.md @@ -0,0 +1,5 @@ +--- +'@drivenets/design-system': minor +--- + +Add the `DsButtonV3` component diff --git a/packages/design-system/src/components/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx b/packages/design-system/src/components/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx new file mode 100644 index 00000000..b529dc42 --- /dev/null +++ b/packages/design-system/src/components/ds-button-v3/__tests__/ds-button-v3.browser.test.tsx @@ -0,0 +1,204 @@ +import { createRef } from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import { page } from 'vitest/browser'; +import { DsButtonV3 } from '../index.ts'; + +describe('DsButtonV3', () => { + it('calls onClick when clicked', async () => { + const onClick = vi.fn(); + + await page.render(Click me); + + await page.getByRole('button', { name: 'Click me' }).click(); + + expect(onClick).toHaveBeenCalled(); + }); + + it('does not call onClick when disabled', async () => { + const onClick = vi.fn(); + + await page.render( + + Click me + , + ); + + const button = page.getByRole('button', { name: 'Click me', disabled: true }); + + await button.click({ force: true }); + + expect(onClick).not.toHaveBeenCalled(); + await expect.element(button).toBeDisabled(); + }); + + it('applies selected state', async () => { + await page.render(Label); + + const button = page.getByRole('button', { name: 'Label' }); + + await expect.element(button).toHaveAttribute('data-selected', 'true'); + }); + + it('sets data-color for negative palette', async () => { + await page.render(Delete); + + const button = page.getByRole('button', { name: 'Delete' }); + + await expect.element(button).toHaveAttribute('data-color', 'negative'); + }); + + it('applies iconOnly layout when icon is set without children', async () => { + await page.render(); + + const button = page.getByRole('button', { name: 'Confirm' }); + + await expect.element(button).toHaveAttribute('data-icon-only', 'true'); + }); + + it('does not apply iconOnly layout when icon is set with children', async () => { + await page.render(Save); + + const button = page.getByRole('button', { name: 'Save' }); + + await expect.element(button).not.toHaveAttribute('data-icon-only'); + }); + + it('renders native submit button type', async () => { + await page.render(Send); + + const button = page.getByRole('button', { name: 'Send' }); + + await expect.element(button).toHaveAttribute('type', 'submit'); + }); + + it('merges className', async () => { + await page.render(X); + + await expect.element(page.getByRole('button', { name: 'X' })).toHaveClass('extra'); + }); + + it('sets aria-busy and data-loading when loading', async () => { + await page.render(Save); + + const button = page.getByRole('button', { name: 'Save' }); + + await expect.element(button).toHaveAttribute('aria-busy', 'true'); + await expect.element(button).toHaveAttribute('data-loading', ''); + }); + + it('renders spinner instead of icon when loading', async () => { + await page.render(); + + const button = page.getByRole('button', { name: 'Saving' }); + + await expect.element(button).toHaveAttribute('aria-busy', 'true'); + + const el = button.element(); + expect(el.querySelector('svg')).toBeTruthy(); + expect(el.querySelector('[aria-hidden]')).toBeNull(); + }); + + it('does not call onClick when loading', async () => { + const onClick = vi.fn(); + + await page.render( + + Save + , + ); + + await page.getByRole('button', { name: 'Save' }).click({ force: true }); + + expect(onClick).not.toHaveBeenCalled(); + }); + + it('sets data-variant for each variant', async () => { + for (const variant of ['primary', 'secondary', 'tertiary'] as const) { + await page.render( + + Label + , + ); + + await expect + .element(page.getByRole('button', { name: variant })) + .toHaveAttribute('data-variant', variant); + } + }); + + it('applies default props when none are specified', async () => { + await page.render(Default); + + const button = page.getByRole('button', { name: 'Default' }); + + await expect.element(button).toHaveAttribute('data-color', 'default'); + await expect.element(button).toHaveAttribute('data-variant', 'primary'); + await expect.element(button).toHaveAttribute('data-size', 'medium'); + }); + + it('forwards ref to the button element', async () => { + const ref = createRef(); + + await page.render(Ref); + + expect(ref.current).toBeInstanceOf(HTMLButtonElement); + expect(ref.current?.textContent).toBe('Ref'); + }); + + it('loading without disabled keeps normal colors', async () => { + const onClick = vi.fn(); + + await page.render( + + Saving + , + ); + + const button = page.getByRole('button', { name: 'Saving', disabled: true }); + + await expect.element(button).toBeDisabled(); + await expect.element(button).toHaveAttribute('data-loading', ''); + await button.click({ force: true }); + expect(onClick).not.toHaveBeenCalled(); + }); + + it('loading + disabled shows spinner with disabled styling', async () => { + await page.render( + + Saving + , + ); + + const button = page.getByRole('button', { name: 'Saving', disabled: true }); + + await expect.element(button).toBeDisabled(); + await expect.element(button).not.toHaveAttribute('data-loading'); + await expect.element(button).toHaveAttribute('aria-busy', 'true'); + }); + + it('selected + disabled does not remove selected styling', async () => { + await page.render( + + Toggle + , + ); + + const button = page.getByRole('button', { name: 'Toggle', disabled: true }); + + await expect.element(button).toBeDisabled(); + await expect.element(button).toHaveAttribute('aria-pressed', 'true'); + await expect.element(button).toHaveAttribute('data-selected', 'true'); + }); + + it('spreads rest props onto the button element', async () => { + await page.render( + + Rest + , + ); + + const button = page.getByRole('button', { name: 'Spread' }); + + await expect.element(button).toHaveAttribute('data-testid', 'my-btn'); + }); +}); diff --git a/packages/design-system/src/components/ds-button-v3/ds-button-v3.module.scss b/packages/design-system/src/components/ds-button-v3/ds-button-v3.module.scss new file mode 100644 index 00000000..59973178 --- /dev/null +++ b/packages/design-system/src/components/ds-button-v3/ds-button-v3.module.scss @@ -0,0 +1,395 @@ +@use '../../styles/root_updated'; + +$height-large: 40px; +$height-medium: 36px; +$height-small: 28px; +$border-radius: 4px; +$focus-ring-width: 2px; +$focus-ring-offset: 1px; +// it looks a bit better with 0.3 than 0.2 +$transition-duration-default: 0.3s; +$transition-duration-quick: 0.15s; + +@mixin focus-ring($outer-color) { + outline: $focus-ring-width solid $outer-color; + outline-offset: $focus-ring-offset; +} + +.root { + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--3xs); + margin: 0; + border: 1px solid transparent; + border-radius: $border-radius; + background: transparent; + font-family: var(--font-family-base); + font-weight: var(--font-weight-medium); + text-align: center; + cursor: pointer; + transition: + background-color $transition-duration-default, + border-color $transition-duration-quick, + color $transition-duration-default, + outline-color $transition-duration-quick; + + &:disabled:not([data-loading]) { + cursor: not-allowed; + } + + &[data-loading] { + cursor: default; + pointer-events: none; + } + + &:focus { + outline: none; + } + + &:focus-visible { + outline-style: solid; + } + + &[data-size='large'] { + padding: var(--3xs) var(--sm); + min-height: $height-large; + font-size: var(--font-size-md); + line-height: var(--line-height-md); + } + + &[data-size='medium'] { + padding: var(--3xs) var(--sm); + min-height: $height-medium; + font-size: var(--font-size-sm); + line-height: var(--line-height-sm); + } + + &[data-size='small'] { + padding: var(--3xs) var(--sm); + min-height: $height-small; + font-size: var(--font-size-xs); + line-height: var(--line-height-xs); + } + + &[data-size='tiny'] { + min-height: var(--icon-width-tiny); + padding: 0 var(--3xs); + font-size: var(--font-size-xs); + line-height: var(--line-height-xs); + } + + &[data-icon-only][data-size='large'] { + padding: var(--3xs); + min-width: $height-large; + } + + &[data-icon-only][data-size='medium'] { + padding: var(--3xs); + min-width: $height-medium; + } + + &[data-icon-only][data-size='small'] { + padding: var(--3xs); + min-width: $height-small; + } + + &[data-icon-only][data-size='tiny'] { + padding: 0; + min-width: var(--icon-width-tiny); + } +} + +.root[data-color='default'][data-variant='primary'] { + background-color: var(--background-background-primary); + color: var(--font-font-on-action); + border-color: transparent; + + &:hover:not(:disabled):not(:active):not([data-selected='true']) { + background-color: var(--background-background-primary-hover); + } + + &:active:not(:disabled) { + background-color: var(--background-background-primary-selected); + } + + &:focus-visible { + @include focus-ring(var(--border-border-action-primary)); + + background-color: var(--background-background-primary-hover); + border-color: var(--border-border-inverse); + } + + &[data-selected='true'] { + background-color: var(--background-background-primary-selected); + } + + &:disabled:not([data-loading]) { + background-color: var(--background-background-disable); + color: var(--font-font-on-action); + } +} + +.root[data-color='default'][data-variant='secondary'] { + background-color: var(--background-background-action-secondary); + color: var(--font-font-main); + border-color: var(--border-border); + + &:hover:not(:disabled):not(:active):not([data-selected='true']) { + background-color: var(--background-background-secondary-hover); + color: var(--font-font-action-secondary); + border-color: var(--border-border-secondary-hover); + } + + &:active:not(:disabled) { + background-color: var(--background-background-action-secondary-hover); + color: var(--font-font-action-secondary); + border-color: var(--border-border-secondary-hover); + } + + &:focus-visible { + @include focus-ring(var(--border-border-action-primary-hover)); + + background-color: var(--background-background-secondary-hover); + border-color: var(--border-border-inverse); + color: var(--font-font-action-secondary); + } + + &[data-selected='true'] { + background-color: var(--background-background-action-secondary-hover); + color: var(--font-font-action-secondary); + border-color: var(--border-border-secondary-hover); + } + + &:disabled:not([data-loading]) { + background-color: var(--background-background-action-secondary); + color: var(--font-font-disabled); + border-color: var(--border-border); + } +} + +.root[data-color='default'][data-variant='tertiary'] { + background-color: transparent; + color: var(--font-font-main); + border-color: transparent; + + &:hover:not(:disabled):not(:active):not([data-selected='true']) { + background-color: var(--background-background-secondary-hover); + color: var(--font-font-action-secondary); + } + + &:active:not(:disabled) { + background-color: var(--background-background-action-secondary-hover); + color: var(--font-font-action-secondary); + } + + &:focus-visible { + @include focus-ring(var(--border-border-action-primary-hover)); + + background-color: var(--background-background-secondary-hover); + border-color: var(--border-border-inverse); + color: var(--font-font-action-secondary); + } + + &[data-selected='true'] { + background-color: var(--background-background-action-secondary-hover); + color: var(--font-font-action-secondary); + } + + &:disabled:not([data-loading]) { + background-color: transparent; + color: var(--font-font-disabled); + } +} + +.root[data-color='negative'][data-variant='primary'] { + background-color: var(--background-background-negative); + color: var(--font-font-on-action); + border-color: transparent; + + &:hover:not(:disabled):not(:active):not([data-selected='true']) { + background-color: var(--background-background-negative-hover); + } + + &:active:not(:disabled) { + background-color: var(--background-background-negative-selected); + } + + &:focus-visible { + @include focus-ring(var(--border-border-negative)); + + background-color: var(--background-background-negative-hover); + border-color: var(--border-border-inverse); + } + + &[data-selected='true'] { + background-color: var(--background-background-negative-selected); + } + + &:disabled:not([data-loading]) { + background-color: var(--background-background-disable); + color: var(--font-font-on-action); + } +} + +.root[data-color='negative'][data-variant='secondary'] { + background-color: var(--background-background-action-secondary); + color: var(--font-font-negative); + border-color: var(--border-border-negative); + + &:hover:not(:disabled):not(:active):not([data-selected='true']) { + background-color: var(--background-background-negative-secondary-hover); + border-color: var(--border-border-negative-hover); + } + + &:active:not(:disabled) { + background-color: var(--background-background-negative-secondary-selected); + border-color: var(--border-border-negative-hover); + } + + &:focus-visible { + @include focus-ring(var(--border-border-negative)); + + background-color: var(--background-background-negative-secondary-hover); + border-color: var(--border-border-inverse); + } + + &[data-selected='true'] { + background-color: var(--background-background-negative-secondary-selected); + border-color: var(--border-border-negative-hover); + } + + &:disabled:not([data-loading]) { + background-color: var(--background-background-action-secondary); + color: var(--font-font-disabled); + border-color: var(--border-border); + } +} + +.root[data-color='negative'][data-variant='tertiary'] { + background-color: transparent; + color: var(--font-font-negative); + border-color: transparent; + + &:hover:not(:disabled):not(:active):not([data-selected='true']) { + background-color: var(--background-background-negative-secondary-hover); + } + + &:active:not(:disabled) { + background-color: var(--background-background-negative-secondary-selected); + } + + &:focus-visible { + @include focus-ring(var(--border-border-negative)); + + background-color: var(--background-background-negative-secondary-hover); + border-color: var(--border-border-inverse); + } + + &[data-selected='true'] { + background-color: var(--background-background-negative-secondary-selected); + } + + &:disabled:not([data-loading]) { + background-color: transparent; + color: var(--font-font-disabled); + } +} + +.root[data-color='light'][data-variant='primary'] { + background-color: var(--background-background-ondark-primary); + color: var(--font-font-on-action); + border-color: transparent; + + &:hover:not(:disabled):not(:active):not([data-selected='true']) { + background-color: var(--background-background-ondark-primary-hover); + } + + &:active:not(:disabled) { + background-color: var(--background-background-ondark-primary-selected); + } + + &:focus-visible { + @include focus-ring(var(--outline-outline-inverse)); + + background-color: var(--background-background-ondark-primary-hover); + border-color: var(--border-border-action-secondary); + } + + &[data-selected='true'] { + background-color: var(--background-background-ondark-primary-selected); + } + + &:disabled:not([data-loading]) { + background-color: var(--background-background-ondark-disabled); + color: var(--font-font-disabled); + } +} + +.root[data-color='light'][data-variant='secondary'] { + background-color: transparent; + color: var(--font-font-on-action); + border-color: var(--border-border-ondark-secondary); + + &:hover:not(:disabled):not(:active):not([data-selected='true']) { + background-color: var(--background-background-ondark-secondary-hover); + color: var(--font-font-on-action); + } + + &:active:not(:disabled) { + background-color: var(--background-background-ondark-secondary-selected); + color: var(--font-font-on-action); + } + + &:focus-visible { + @include focus-ring(var(--outline-outline-inverse)); + + background-color: var(--background-background-ondark-secondary-hover); + border-color: var(--border-border-action-secondary); + color: var(--font-font-on-action); + } + + &[data-selected='true'] { + background-color: var(--background-background-ondark-secondary-selected); + } + + &:disabled:not([data-loading]) { + background-color: transparent; + color: var(--font-font-ondark-disabled); + border-color: var(--background-background-ondark-disabled); + } +} + +.root[data-color='light'][data-variant='tertiary'] { + background-color: transparent; + color: var(--font-font-on-action); + border-color: transparent; + + &:hover:not(:disabled):not(:active):not([data-selected='true']) { + background-color: var(--background-background-ondark-secondary-hover); + color: var(--font-font-on-action); + } + + &:active:not(:disabled) { + background-color: var(--background-background-ondark-secondary-selected); + color: var(--font-font-on-action); + } + + &:focus-visible { + @include focus-ring(var(--outline-outline-inverse)); + + background-color: var(--background-background-ondark-secondary-hover); + border-color: var(--border-border-action-secondary); + color: var(--font-font-on-action); + } + + &[data-selected='true'] { + background-color: var(--background-background-ondark-secondary-selected); + } + + &:disabled:not([data-loading]) { + background-color: transparent; + color: var(--font-font-ondark-disabled); + } +} diff --git a/packages/design-system/src/components/ds-button-v3/ds-button-v3.stories.module.scss b/packages/design-system/src/components/ds-button-v3/ds-button-v3.stories.module.scss new file mode 100644 index 00000000..9bf55707 --- /dev/null +++ b/packages/design-system/src/components/ds-button-v3/ds-button-v3.stories.module.scss @@ -0,0 +1,85 @@ +@use '../../styles/root_updated'; + +.matrix { + display: flex; + flex-direction: column; + gap: var(--lg); + padding: var(--2xl); +} + +.section { + display: flex; + flex-direction: column; + gap: var(--sm); +} + +.sectionTitle { + margin: 0 0 var(--xs); + font-family: var(--font-family-base); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-semi-bold); + color: var(--font-font-secondary); + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.columnHeaders { + display: flex; + align-items: center; + gap: var(--sm); + padding-left: 120px; +} + +.columnHeader { + flex: 1; + font-family: var(--font-family-base); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-medium); + color: var(--font-font-secondary); + text-align: center; + text-transform: capitalize; +} + +.row { + display: flex; + align-items: center; + gap: var(--sm); +} + +.rowLabel { + width: 120px; + flex-shrink: 0; + font-family: var(--font-family-base); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: var(--font-font-secondary); + text-transform: capitalize; +} + +.cell { + flex: 1; + display: flex; + justify-content: center; +} + +.onDark { + padding: var(--2xl); + background-color: var(--darks-500); + border-radius: 4px; +} + +.onDarkLabel { + color: var(--secondary-050); +} + +.onDarkSectionTitle { + color: var(--secondary-300); +} + +.sectionTitleSpaced { + margin-top: var(--lg); +} + +.onDarkColumnHeader { + color: var(--secondary-300); +} diff --git a/packages/design-system/src/components/ds-button-v3/ds-button-v3.stories.tsx b/packages/design-system/src/components/ds-button-v3/ds-button-v3.stories.tsx new file mode 100644 index 00000000..e07a09d3 --- /dev/null +++ b/packages/design-system/src/components/ds-button-v3/ds-button-v3.stories.tsx @@ -0,0 +1,233 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import classNames from 'classnames'; +import { fn } from 'storybook/test'; +import DsButtonV3 from './ds-button-v3.tsx'; +import { + type ButtonV3Color, + buttonV3Colors, + buttonV3Sizes, + type ButtonV3Variant, + buttonV3Variants, +} from './ds-button-v3.types.ts'; +import storyStyles from './ds-button-v3.stories.module.scss'; + +const meta: Meta = { + title: 'Design System/ButtonV3', + component: DsButtonV3, + parameters: { + layout: 'centered', + }, + argTypes: { + color: { control: 'select', options: buttonV3Colors }, + variant: { control: 'select', options: buttonV3Variants }, + size: { control: 'select', options: buttonV3Sizes }, + loading: { control: 'boolean' }, + disabled: { control: 'boolean' }, + className: { table: { disable: true } }, + style: { table: { disable: true } }, + ref: { table: { disable: true } }, + }, + args: { onClick: fn() }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + color: 'default', + variant: 'primary', + size: 'medium', + icon: 'check_circle', + children: 'Button', + }, +}; + +const matrixRows = [ + ...buttonV3Variants.map((v) => ({ label: v, loading: false })), + { label: 'loading', loading: true }, +]; + +const defaultIconMatrixRows = [ + { label: 'check circle', icon: 'check_circle', variant: 'primary', color: 'default', loading: false }, + { label: 'info', icon: 'info', variant: 'secondary', color: 'default', loading: false }, + { label: 'delete', icon: 'delete', variant: 'tertiary', color: 'negative', loading: false }, + { label: 'loading', icon: 'check_circle', variant: 'primary', color: 'default', loading: true }, +] as const; + +const onDarkIconMatrixRows = [ + { + label: 'arrow down', + icon: 'keyboard_arrow_down', + variant: 'primary', + color: 'light', + loading: false, + }, + { label: 'home', icon: 'home', variant: 'secondary', color: 'light', loading: false }, + { label: 'info', icon: 'info', variant: 'tertiary', color: 'light', loading: false }, + { label: 'loading', icon: 'info', variant: 'primary', color: 'light', loading: true }, +] as const; + +const MatrixGrid = ({ color }: { color?: ButtonV3Color }) => { + const isOnDark = color === 'light'; + + return ( +
+
+ {buttonV3Sizes.map((size) => ( + + {size} + + ))} +
+ + {matrixRows.map(({ label, loading }) => ( +
+ + {label} + + + {buttonV3Sizes.map((size) => ( +
+ + {size !== 'tiny' ? 'Button' : undefined} + +
+ ))} +
+ ))} +
+ ); +}; + +const IconMatrixGrid = ({ + rows, + isOnDark = false, +}: { + rows: ReadonlyArray<{ + label: string; + icon: 'check_circle' | 'info' | 'delete' | 'keyboard_arrow_down' | 'home'; + variant: ButtonV3Variant; + color: ButtonV3Color; + loading: boolean; + }>; + isOnDark?: boolean; +}) => ( +
+
+ {buttonV3Sizes.map((size) => ( + + {size} + + ))} +
+ + {rows.map(({ label, icon, loading, variant, color }) => ( +
+ + {label} + + + {buttonV3Sizes.map((size) => { + const ariaLabel = `${label} ${size}`; + + return ( +
+ +
+ ); + })} +
+ ))} +
+); + +export const MatrixDefault: Story = { + parameters: { layout: 'fullscreen' }, + render: () => ( +
+

Default

+ +
+ ), +}; + +export const MatrixNegative: Story = { + parameters: { layout: 'fullscreen' }, + render: () => ( +
+

Negative

+ +
+ ), +}; + +export const MatrixOnDark: Story = { + parameters: { layout: 'fullscreen' }, + render: () => ( +
+
+

+ On Dark — Default +

+ +
+
+ ), +}; + +export const MatrixIcons: Story = { + parameters: { layout: 'fullscreen' }, + render: () => ( +
+

Icons — Default

+ + +
+

+ Icons — On Dark +

+ +
+
+ ), +}; diff --git a/packages/design-system/src/components/ds-button-v3/ds-button-v3.tsx b/packages/design-system/src/components/ds-button-v3/ds-button-v3.tsx new file mode 100644 index 00000000..834ba23f --- /dev/null +++ b/packages/design-system/src/components/ds-button-v3/ds-button-v3.tsx @@ -0,0 +1,55 @@ +import classNames from 'classnames'; +import { DsIcon, type IconSize } from '../ds-icon'; +import { DsSpinner } from '../ds-spinner'; +import styles from './ds-button-v3.module.scss'; +import type { ButtonV3Size, DsButtonV3Props } from './ds-button-v3.types.ts'; + +const iconSizeMap: Record = Object.freeze({ + large: 'small', + medium: 'tiny', + small: 'tiny', + tiny: 'tiny', +}); + +const DsButtonV3 = ({ + ref, + className, + style, + children, + icon, + disabled, + loading = false, + color = 'default', + variant = 'primary', + size = 'medium', + selected = false, + type = 'button', + ...rest +}: DsButtonV3Props) => { + const isIconOnly = icon !== undefined && !children; + + return ( + + ); +}; + +export default DsButtonV3; diff --git a/packages/design-system/src/components/ds-button-v3/ds-button-v3.types.ts b/packages/design-system/src/components/ds-button-v3/ds-button-v3.types.ts new file mode 100644 index 00000000..50702c59 --- /dev/null +++ b/packages/design-system/src/components/ds-button-v3/ds-button-v3.types.ts @@ -0,0 +1,49 @@ +import type { ButtonHTMLAttributes, Ref } from 'react'; +import type { IconType } from '../ds-icon'; + +export const buttonV3Variants = ['primary', 'secondary', 'tertiary'] as const; +export type ButtonV3Variant = (typeof buttonV3Variants)[number]; + +export const buttonV3Colors = ['default', 'negative', 'light'] as const; +export type ButtonV3Color = (typeof buttonV3Colors)[number]; + +export const buttonV3Sizes = ['large', 'medium', 'small', 'tiny'] as const; +export type ButtonV3Size = (typeof buttonV3Sizes)[number]; + +export interface DsButtonV3Props extends ButtonHTMLAttributes { + ref?: Ref; + + /** + * - `default` — standard light-UI palette + * - `negative` — destructive / danger palette (red tones) + * - `light` — palette for dark-background surfaces (Figma **Type** onDark) + * @default 'default' + */ + color?: ButtonV3Color; + + /** + * @default 'primary' + */ + variant?: ButtonV3Variant; + + /** + * @default 'medium' + */ + size?: ButtonV3Size; + + /** + * @default false + */ + selected?: boolean; + + /** + * Leading icon. When set without children, renders as icon-only (square) layout. + */ + icon?: IconType; + + /** + * Shows a spinner as the leading element and disables interaction. + * @default false + */ + loading?: boolean; +} diff --git a/packages/design-system/src/components/ds-button-v3/index.ts b/packages/design-system/src/components/ds-button-v3/index.ts new file mode 100644 index 00000000..50f2f2ba --- /dev/null +++ b/packages/design-system/src/components/ds-button-v3/index.ts @@ -0,0 +1,2 @@ +export { default as DsButtonV3 } from './ds-button-v3.tsx'; +export * from './ds-button-v3.types.ts'; diff --git a/packages/design-system/src/index.ts b/packages/design-system/src/index.ts index ac8f389e..b25f51f8 100644 --- a/packages/design-system/src/index.ts +++ b/packages/design-system/src/index.ts @@ -4,6 +4,7 @@ export * from './components/ds-avatar'; export * from './components/ds-avatar-group'; export * from './components/ds-breadcrumb'; export * from './components/ds-button'; +export * from './components/ds-button-v3'; export * from './components/ds-card'; export * from './components/ds-checkbox'; export * from './components/ds-chip';