@boostdev/design-system-components 1.2.8 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/AGENTS.md +5 -3
  2. package/README.md +57 -38
  3. package/dist/client.cjs +52 -50
  4. package/dist/client.css +537 -524
  5. package/dist/client.d.cts +31 -1
  6. package/dist/client.d.ts +31 -1
  7. package/dist/client.js +52 -50
  8. package/dist/index.cjs +52 -50
  9. package/dist/index.css +537 -524
  10. package/dist/index.d.cts +31 -1
  11. package/dist/index.d.ts +31 -1
  12. package/dist/index.js +52 -50
  13. package/dist/native/index.cjs +5 -2
  14. package/dist/native/index.d.cts +3 -1
  15. package/dist/native/index.d.ts +3 -1
  16. package/dist/native/index.js +5 -2
  17. package/dist/web-components/{chunk-DZRSJGPB.js → chunk-3S42DZ7W.js} +1 -1
  18. package/dist/web-components/{chunk-3REOIRDW.js → chunk-N3TN6WCH.js} +26 -1
  19. package/dist/web-components/globals.js +1 -1
  20. package/dist/web-components/index.js +2 -2
  21. package/dist/web-components/interaction/bds-button.d.ts +7 -0
  22. package/dist/web-components/interaction/bds-button.js +1 -1
  23. package/package.json +3 -3
  24. package/src/components/interaction/Button/Button.mdx +84 -29
  25. package/src/components/interaction/Button/Button.module.css +25 -1
  26. package/src/components/interaction/Button/Button.native.mdx +33 -14
  27. package/src/components/interaction/Button/Button.native.spec.tsx +20 -0
  28. package/src/components/interaction/Button/Button.native.stories.tsx +110 -9
  29. package/src/components/interaction/Button/Button.native.tsx +13 -4
  30. package/src/components/interaction/Button/Button.spec.tsx +16 -0
  31. package/src/components/interaction/Button/Button.stories.tsx +134 -16
  32. package/src/components/interaction/Button/Button.tsx +32 -0
  33. package/src/components/interaction/Command/Command.module.css +1 -1
  34. package/src/components/interaction/Dialog/Dialog.module.css +1 -1
  35. package/src/components/interaction/Dialog/Dialog.native.mdx +2 -2
  36. package/src/components/interaction/Drawer/Drawer.module.css +1 -1
  37. package/src/components/interaction/Drawer/Drawer.native.mdx +2 -2
  38. package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +1 -1
  39. package/src/components/interaction/DropdownMenu/DropdownMenu.native.mdx +2 -2
  40. package/src/components/interaction/Popover/Popover.module.css +1 -1
  41. package/src/components/interaction/Popover/Popover.native.mdx +2 -2
  42. package/src/components/interaction/Rating/Rating.module.css +1 -1
  43. package/src/components/interaction/Rating/Rating.native.mdx +1 -1
  44. package/src/components/interaction/Toast/Toast.module.css +1 -1
  45. package/src/components/interaction/Toast/Toast.native.mdx +3 -3
  46. package/src/components/interaction/form/Checkbox/Checkbox.module.css +1 -1
  47. package/src/components/interaction/form/Checkbox/Checkbox.native.mdx +1 -1
  48. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.module.css +1 -1
  49. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.mdx +1 -1
  50. package/src/components/interaction/form/Combobox/Combobox.module.css +1 -1
  51. package/src/components/interaction/form/Combobox/Combobox.native.mdx +1 -1
  52. package/src/components/interaction/form/FileInput/FileInput.module.css +1 -1
  53. package/src/components/interaction/form/FileInput/FileInput.native.mdx +1 -1
  54. package/src/components/interaction/form/FormInput/FormInput.module.css +1 -1
  55. package/src/components/interaction/form/FormInput/FormInput.native.mdx +1 -1
  56. package/src/components/interaction/form/NumberInput/NumberInput.module.css +1 -1
  57. package/src/components/interaction/form/NumberInput/NumberInput.native.mdx +1 -1
  58. package/src/components/interaction/form/Radio/Radio.module.css +1 -1
  59. package/src/components/interaction/form/RadioGroup/RadioGroup.module.css +1 -1
  60. package/src/components/interaction/form/RadioGroup/RadioGroup.native.mdx +1 -1
  61. package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +1 -1
  62. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.mdx +1 -1
  63. package/src/components/interaction/form/Select/Select.module.css +1 -1
  64. package/src/components/interaction/form/Select/Select.native.mdx +1 -1
  65. package/src/components/interaction/form/Slider/Slider.module.css +1 -1
  66. package/src/components/interaction/form/Slider/Slider.native.mdx +1 -1
  67. package/src/components/interaction/form/Switch/Switch.module.css +1 -1
  68. package/src/components/interaction/form/Textarea/Textarea.module.css +1 -1
  69. package/src/components/interaction/form/Textarea/Textarea.native.mdx +1 -1
  70. package/src/components/interaction/form/atoms/InputContainer.module.css +1 -1
  71. package/src/components/interaction/form/atoms/Label.module.css +1 -1
  72. package/src/components/interaction/form/atoms/Message.module.css +1 -1
  73. package/src/components/layout/ButtonGroup/ButtonGroup.module.css +1 -1
  74. package/src/components/layout/Card/Card.module.css +1 -1
  75. package/src/components/layout/IconWrapper/IconWrapper.module.css +1 -1
  76. package/src/components/layout/IconWrapper/IconWrapper.stories.tsx +46 -14
  77. package/src/components/layout/SectionHeader/SectionHeader.module.css +1 -1
  78. package/src/components/ui/Accordion/Accordion.module.css +1 -1
  79. package/src/components/ui/Accordion/Accordion.native.mdx +1 -1
  80. package/src/components/ui/Accordion/Accordion.native.stories.tsx +1 -1
  81. package/src/components/ui/Alert/Alert.module.css +1 -1
  82. package/src/components/ui/Alert/Alert.native.mdx +1 -1
  83. package/src/components/ui/Avatar/Avatar.module.css +1 -1
  84. package/src/components/ui/Avatar/Avatar.native.mdx +1 -1
  85. package/src/components/ui/Badge/Badge.module.css +1 -1
  86. package/src/components/ui/Badge/Badge.native.mdx +1 -1
  87. package/src/components/ui/Breadcrumb/Breadcrumb.module.css +1 -1
  88. package/src/components/ui/Breadcrumb/Breadcrumb.native.mdx +1 -1
  89. package/src/components/ui/Calendar/Calendar.module.css +1 -1
  90. package/src/components/ui/Calendar/Calendar.native.mdx +1 -1
  91. package/src/components/ui/Carousel/Carousel.module.css +1 -1
  92. package/src/components/ui/Carousel/Carousel.native.mdx +1 -1
  93. package/src/components/ui/Collapsible/Collapsible.module.css +1 -1
  94. package/src/components/ui/Collapsible/Collapsible.native.mdx +1 -1
  95. package/src/components/ui/DescriptionList/DescriptionList.module.css +1 -1
  96. package/src/components/ui/DescriptionList/DescriptionList.native.mdx +1 -1
  97. package/src/components/ui/Link/Link.module.css +1 -1
  98. package/src/components/ui/Link/Link.native.mdx +1 -1
  99. package/src/components/ui/Loading/Loading.module.css +1 -1
  100. package/src/components/ui/NotificationBanner/NotificationBanner.module.css +1 -1
  101. package/src/components/ui/Pagination/Pagination.module.css +1 -1
  102. package/src/components/ui/Pagination/Pagination.native.mdx +1 -1
  103. package/src/components/ui/Progress/Progress.module.css +1 -1
  104. package/src/components/ui/ProgressCircle/ProgressCircle.module.css +1 -1
  105. package/src/components/ui/ProgressCircle/ProgressCircle.native.mdx +1 -1
  106. package/src/components/ui/Separator/Separator.module.css +1 -1
  107. package/src/components/ui/Skeleton/Skeleton.module.css +1 -1
  108. package/src/components/ui/SkipLink/SkipLink.module.css +1 -1
  109. package/src/components/ui/Table/Table.module.css +1 -1
  110. package/src/components/ui/Table/Table.native.mdx +1 -1
  111. package/src/components/ui/Tabs/Tabs.module.css +1 -1
  112. package/src/components/ui/Tabs/Tabs.native.mdx +1 -1
  113. package/src/components/ui/Tooltip/Tooltip.module.css +1 -1
  114. package/src/components/ui/Tooltip/Tooltip.native.mdx +1 -1
  115. package/src/components/ui/Typography/Typography.module.css +1 -1
  116. package/src/components/ui/Typography/Typography.native.mdx +1 -1
  117. package/src/css/bdc.css +1 -1
  118. package/src/css/index.css +1 -1
  119. package/src/stories/DesignSystem/Borders.mdx +3 -3
  120. package/src/stories/DesignSystem/Colors.mdx +1 -1
  121. package/src/stories/DesignSystem/DarkMode.mdx +3 -3
  122. package/src/stories/DesignSystem/Overview.mdx +16 -5
  123. package/src/stories/DesignSystem/Typography.mdx +1 -1
  124. package/src/stories/Introduction.mdx +4 -4
  125. package/src/stories/ReactNative.mdx +6 -6
  126. package/src/web-components/globals.ts +1 -1
  127. package/src/web-components/interaction/BdsAccordion.mdx +3 -3
  128. package/src/web-components/interaction/BdsAccordion.stories.tsx +1 -1
  129. package/src/web-components/interaction/BdsButton.mdx +48 -16
  130. package/src/web-components/interaction/BdsButton.stories.tsx +171 -19
  131. package/src/web-components/interaction/BdsCollapsible.mdx +2 -2
  132. package/src/web-components/interaction/BdsDialog.mdx +2 -2
  133. package/src/web-components/interaction/BdsDrawer.mdx +2 -2
  134. package/src/web-components/interaction/BdsDropdownMenu.mdx +2 -2
  135. package/src/web-components/interaction/BdsPopover.mdx +1 -1
  136. package/src/web-components/interaction/BdsSkipLink.mdx +2 -2
  137. package/src/web-components/interaction/BdsTabs.mdx +2 -2
  138. package/src/web-components/interaction/BdsToastProvider.mdx +3 -3
  139. package/src/web-components/interaction/BdsTooltip.mdx +2 -2
  140. package/src/web-components/interaction/bds-button.spec.ts +35 -0
  141. package/src/web-components/interaction/bds-button.ts +28 -1
  142. package/src/web-components/interaction/form/BdsCheckbox.mdx +1 -1
  143. package/src/web-components/interaction/form/BdsCheckboxGroup.mdx +1 -1
  144. package/src/web-components/interaction/form/BdsCombobox.mdx +1 -1
  145. package/src/web-components/interaction/form/BdsFileInput.mdx +1 -1
  146. package/src/web-components/interaction/form/BdsFormInput.mdx +1 -1
  147. package/src/web-components/interaction/form/BdsNumberInput.mdx +1 -1
  148. package/src/web-components/interaction/form/BdsRadio.mdx +1 -1
  149. package/src/web-components/interaction/form/BdsRadioGroup.mdx +1 -1
  150. package/src/web-components/interaction/form/BdsSegmentedControl.mdx +1 -1
  151. package/src/web-components/interaction/form/BdsSelect.mdx +1 -1
  152. package/src/web-components/interaction/form/BdsSlider.mdx +1 -1
  153. package/src/web-components/interaction/form/BdsSwitch.mdx +1 -1
  154. package/src/web-components/interaction/form/BdsTextarea.mdx +1 -1
  155. package/src/web-components/ui/BdsAlert.mdx +1 -1
  156. package/src/web-components/ui/BdsAvatar.mdx +1 -1
  157. package/src/web-components/ui/BdsBadge.mdx +1 -1
  158. package/src/web-components/ui/BdsBreadcrumb.mdx +1 -1
  159. package/src/web-components/ui/BdsButtonGroup.mdx +1 -1
  160. package/src/web-components/ui/BdsCalendar.mdx +1 -1
  161. package/src/web-components/ui/BdsCard.mdx +1 -1
  162. package/src/web-components/ui/BdsCarousel.mdx +1 -1
  163. package/src/web-components/ui/BdsDescriptionList.mdx +1 -1
  164. package/src/web-components/ui/BdsIconWrapper.mdx +1 -1
  165. package/src/web-components/ui/BdsLink.mdx +1 -1
  166. package/src/web-components/ui/BdsLoading.mdx +1 -1
  167. package/src/web-components/ui/BdsNotificationBanner.mdx +1 -1
  168. package/src/web-components/ui/BdsPagination.mdx +1 -1
  169. package/src/web-components/ui/BdsProgress.mdx +1 -1
  170. package/src/web-components/ui/BdsProgressCircle.mdx +1 -1
  171. package/src/web-components/ui/BdsRating.mdx +1 -1
  172. package/src/web-components/ui/BdsSectionHeader.mdx +1 -1
  173. package/src/web-components/ui/BdsSeparator.mdx +2 -2
  174. package/src/web-components/ui/BdsSkeleton.mdx +1 -1
  175. package/src/web-components/ui/BdsTable.mdx +1 -1
  176. package/src/web-components/ui/BdsTypography.mdx +1 -1
@@ -1,34 +1,113 @@
1
1
  import React from 'react';
2
+ import type { ReactNode } from 'react';
2
3
  import type { Meta, StoryObj } from '@storybook/react';
3
4
  import { ThemeProvider } from '../../../native/ThemeContext';
4
- import { Button } from './Button.native';
5
+ import { Button, type ButtonProps } from './Button.native';
5
6
 
6
- const meta = {
7
+ const CheckIcon = (
8
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" aria-hidden>
9
+ <path d="M12 0a12 12 0 1 0 12 12A12 12 0 0 0 12 0Zm6.93 8.2-6.85 9.29a1 1 0 0 1-1.43.19l-4.89-3.91a1 1 0 0 1-.15-1.41A1 1 0 0 1 7 12.21l4.08 3.26L17.32 7a1 1 0 0 1 1.39-.21 1 1 0 0 1 .22 1.41Z" fill="currentColor"/>
10
+ </svg>
11
+ );
12
+
13
+ const ArrowRight = <span aria-hidden>→</span>;
14
+ const ArrowLeft = <span aria-hidden>←</span>;
15
+
16
+ const iconMap = {
17
+ none: undefined,
18
+ check: CheckIcon,
19
+ arrowRight: ArrowRight,
20
+ arrowLeft: ArrowLeft,
21
+ };
22
+
23
+ type IconKey = keyof typeof iconMap;
24
+
25
+ type ButtonStoryArgs = Omit<ButtonProps, 'iconStart' | 'iconEnd' | 'children'> & {
26
+ iconStart?: IconKey;
27
+ iconEnd?: IconKey;
28
+ children?: ReactNode;
29
+ };
30
+
31
+ const meta: Meta<ButtonStoryArgs> = {
7
32
  tags: ['!stable', 'alpha'],
8
33
  title: 'React Native/Interaction/Button',
9
34
  component: Button,
10
35
  decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
11
36
  parameters: { layout: 'centered' },
12
37
  argTypes: {
13
- variant: { control: 'select', options: ['default', 'ghost'] },
14
- size: { control: 'select', options: ['small', 'medium', 'large'] },
38
+ variant: {
39
+ control: 'select',
40
+ options: ['default', 'outline'],
41
+ description: 'Visual style. `default` is filled; `outline` is transparent with a border.',
42
+ table: { defaultValue: { summary: 'default' } },
43
+ },
44
+ size: {
45
+ control: 'select',
46
+ options: ['small', 'medium', 'large'],
47
+ description: 'Height + horizontal padding.',
48
+ table: { defaultValue: { summary: 'medium' } },
49
+ },
50
+ iconStart: {
51
+ control: 'select',
52
+ options: Object.keys(iconMap) as IconKey[],
53
+ mapping: iconMap,
54
+ description: 'Icon rendered before the label.',
55
+ },
56
+ iconEnd: {
57
+ control: 'select',
58
+ options: Object.keys(iconMap) as IconKey[],
59
+ mapping: iconMap,
60
+ description: 'Icon rendered after the label.',
61
+ },
62
+ isIconOnly: {
63
+ control: 'boolean',
64
+ description: 'Forces 1:1 aspect ratio with minimal xs padding. Composable with any `variant`. Requires `accessibilityLabel`.',
65
+ table: { defaultValue: { summary: 'false' } },
66
+ },
67
+ disabled: { control: 'boolean' },
15
68
  onPress: { action: 'pressed' },
16
69
  },
17
- } satisfies Meta<typeof Button>;
70
+ args: { variant: 'default', size: 'medium' },
71
+ };
18
72
 
19
73
  export default meta;
20
- type Story = StoryObj<typeof meta>;
74
+ type Story = StoryObj<ButtonStoryArgs>;
21
75
 
22
- export const Default: Story = { args: { children: 'Default', variant: 'default' } };
23
- export const Ghost: Story = { args: { children: 'Ghost', variant: 'ghost' } };
76
+ export const Playground: Story = {
77
+ args: { children: 'Button', iconStart: 'none', iconEnd: 'none' },
78
+ };
79
+
80
+ export const Default: Story = { args: { children: 'Default' } };
81
+ export const Outline: Story = { args: { children: 'Outline', variant: 'outline' } };
24
82
  export const Small: Story = { args: { children: 'Small', size: 'small' } };
25
83
  export const Medium: Story = { args: { children: 'Medium', size: 'medium' } };
26
84
  export const Large: Story = { args: { children: 'Large', size: 'large' } };
27
85
  export const Disabled: Story = { args: { children: 'Disabled', disabled: true } };
86
+
87
+ export const WithIconStart: Story = { args: { children: 'Confirm', iconStart: 'check' } };
88
+ export const WithIconEnd: Story = { args: { children: 'Continue', iconEnd: 'arrowRight' } };
89
+
90
+ export const IconOnly: Story = {
91
+ args: {
92
+ isIconOnly: true,
93
+ accessibilityLabel: 'Confirm',
94
+ children: CheckIcon,
95
+ },
96
+ };
97
+
98
+ export const IconOnlyOutline: Story = {
99
+ args: {
100
+ variant: 'outline',
101
+ isIconOnly: true,
102
+ accessibilityLabel: 'Confirm',
103
+ children: CheckIcon,
104
+ },
105
+ };
106
+
28
107
  export const AllVariants: Story = {
29
108
  render: () => (
30
109
  <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
31
- {(['default', 'ghost'] as const).map(v => (
110
+ {(['default', 'outline'] as const).map(v => (
32
111
  <div key={v} style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
33
112
  {(['small', 'medium', 'large'] as const).map(s => (
34
113
  <Button key={s} variant={v} size={s}>{v} {s}</Button>
@@ -38,3 +117,25 @@ export const AllVariants: Story = {
38
117
  </div>
39
118
  ),
40
119
  };
120
+
121
+ export const AllIconOnly: Story = {
122
+ render: () => (
123
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
124
+ {(['default', 'outline'] as const).map(v => (
125
+ <div key={v} style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
126
+ {(['small', 'medium', 'large'] as const).map(s => (
127
+ <Button
128
+ key={s}
129
+ variant={v}
130
+ size={s}
131
+ isIconOnly
132
+ accessibilityLabel={`${v} ${s}`}
133
+ >
134
+ {CheckIcon}
135
+ </Button>
136
+ ))}
137
+ </div>
138
+ ))}
139
+ </div>
140
+ ),
141
+ };
@@ -13,6 +13,8 @@ export interface ButtonProps {
13
13
  iconStart?: ReactNode;
14
14
  iconEnd?: ReactNode;
15
15
  disabled?: boolean;
16
+ /** Forces a 1:1 aspect ratio with minimal padding for icon-only buttons. Requires `accessibilityLabel`. */
17
+ isIconOnly?: boolean;
16
18
  onPress?: () => void;
17
19
  accessibilityLabel?: string;
18
20
  style?: StyleProp<ViewStyle>;
@@ -46,6 +48,7 @@ export function Button({
46
48
  iconStart,
47
49
  iconEnd,
48
50
  disabled = false,
51
+ isIconOnly = false,
49
52
  onPress,
50
53
  accessibilityLabel,
51
54
  style,
@@ -62,9 +65,11 @@ export function Button({
62
65
  const isOutline = variant === 'outline' || variant === 'ghost';
63
66
  const containerStyle: ViewStyle = {
64
67
  height,
65
- paddingHorizontal: px,
68
+ paddingHorizontal: isIconOnly ? spacing.xs : px,
66
69
  borderRadius: radius,
67
70
  opacity: disabled ? 0.4 : 1,
71
+ // Icon-only: square with equal xs padding on both axes.
72
+ ...(isIconOnly && { aspectRatio: 1, paddingVertical: spacing.xs }),
68
73
  ...(variant === 'default' && { backgroundColor: primaryColor }),
69
74
  ...(isOutline && {
70
75
  backgroundColor: 'transparent',
@@ -86,9 +91,13 @@ export function Button({
86
91
  >
87
92
  {iconStart && <View importantForAccessibility="no-hide-descendants" accessibilityElementsHidden={true}>{iconStart}</View>}
88
93
  {children && (
89
- <Text style={[styles.label, { fontSize, color: textColor }]}>
90
- {children}
91
- </Text>
94
+ isIconOnly
95
+ ? children
96
+ : (
97
+ <Text style={[styles.label, { fontSize, color: textColor }]}>
98
+ {children}
99
+ </Text>
100
+ )
92
101
  )}
93
102
  {iconEnd && <View importantForAccessibility="no-hide-descendants">{iconEnd}</View>}
94
103
  </Pressable>
@@ -47,4 +47,20 @@ describe('Button', () => {
47
47
  render(<Button iconEnd={<span data-testid="icon">★</span>}>Label</Button>);
48
48
  expect(screen.getByTestId('icon')).toBeInTheDocument();
49
49
  });
50
+
51
+ it('applies --iconOnly class when isIconOnly is true', () => {
52
+ render(
53
+ <Button isIconOnly aria-label="Close">
54
+ <span data-testid="icon">×</span>
55
+ </Button>
56
+ );
57
+ expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
58
+ expect(screen.getByTestId('icon')).toBeInTheDocument();
59
+ expect(screen.getByRole('button').className).toContain('--iconOnly');
60
+ });
61
+
62
+ it('does not apply --iconOnly class by default', () => {
63
+ render(<Button>Click</Button>);
64
+ expect(screen.getByRole('button').className).not.toContain('--iconOnly');
65
+ });
50
66
  });
@@ -1,42 +1,160 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react';
2
- import { Button } from './Button';
2
+ import { Button, type ButtonProps } from './Button';
3
3
 
4
- const meta = {
4
+ const CheckIcon = (
5
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="1em" height="1em" aria-hidden>
6
+ <path d="M12 0a12 12 0 1 0 12 12A12 12 0 0 0 12 0Zm6.93 8.2-6.85 9.29a1 1 0 0 1-1.43.19l-4.89-3.91a1 1 0 0 1-.15-1.41A1 1 0 0 1 7 12.21l4.08 3.26L17.32 7a1 1 0 0 1 1.39-.21 1 1 0 0 1 .22 1.41Z"/>
7
+ </svg>
8
+ );
9
+
10
+ const ArrowRight = <span aria-hidden>→</span>;
11
+ const ArrowLeft = <span aria-hidden>←</span>;
12
+
13
+ const iconMap = {
14
+ none: undefined,
15
+ check: CheckIcon,
16
+ arrowRight: ArrowRight,
17
+ arrowLeft: ArrowLeft,
18
+ };
19
+
20
+ type IconKey = keyof typeof iconMap;
21
+
22
+ /**
23
+ * Story-local args: `iconStart`/`iconEnd` are narrowed to the icon keys
24
+ * (`'check' | 'arrowRight' | …`) so the Controls panel stays in sync with
25
+ * the rendered icon. Storybook's `mapping` resolves the key → ReactNode at
26
+ * render time, so the prop still receives a real ReactNode downstream.
27
+ */
28
+ type ButtonStoryArgs = Omit<ButtonProps, 'iconStart' | 'iconEnd'> & {
29
+ iconStart?: IconKey;
30
+ iconEnd?: IconKey;
31
+ };
32
+
33
+ const meta: Meta<ButtonStoryArgs> = {
5
34
  title: 'React/Interaction/Button',
6
35
  component: Button,
7
36
  argTypes: {
8
- variant: { control: 'select', options: ['default', 'ghost'] },
9
- size: { control: 'select', options: ['small', 'medium', 'large'] },
37
+ variant: {
38
+ control: 'select',
39
+ options: ['default', 'outline'],
40
+ description: 'Visual style. `default` is filled; `outline` is transparent with a border.',
41
+ table: { defaultValue: { summary: 'default' } },
42
+ },
43
+ size: {
44
+ control: 'select',
45
+ options: ['small', 'medium', 'large'],
46
+ description: 'Height + padding. `small` is 2.25em, `medium` is 3em, `large` bumps font-size to the h3 scale.',
47
+ table: { defaultValue: { summary: 'medium' } },
48
+ },
49
+ iconStart: {
50
+ control: 'select',
51
+ options: Object.keys(iconMap) as IconKey[],
52
+ mapping: iconMap,
53
+ description: 'Icon rendered before the label.',
54
+ },
55
+ iconEnd: {
56
+ control: 'select',
57
+ options: Object.keys(iconMap) as IconKey[],
58
+ mapping: iconMap,
59
+ description: 'Icon rendered after the label.',
60
+ },
61
+ isIconOnly: {
62
+ control: 'boolean',
63
+ description: 'Forces a 1:1 aspect ratio with minimal padding. Composable with any `variant`. Requires `aria-label`.',
64
+ table: { defaultValue: { summary: 'false' } },
65
+ },
66
+ hasPulse: {
67
+ control: 'boolean',
68
+ description: 'Adds a pulsing animation for CTA emphasis. Respects `prefers-reduced-motion`.',
69
+ table: { defaultValue: { summary: 'false' } },
70
+ },
71
+ disabled: {
72
+ control: 'boolean',
73
+ description: 'Blocks interaction, sets `aria-disabled`, and removes the element from the tab order.',
74
+ table: { defaultValue: { summary: 'false' } },
75
+ },
76
+ href: {
77
+ control: 'text',
78
+ description: 'When set, renders as `<a>` pointing to this URL instead of `<button>`.',
79
+ },
80
+ 'aria-label': {
81
+ control: 'text',
82
+ description: 'Accessible label. Required for icon-only buttons.',
83
+ },
10
84
  onClick: { action: 'clicked' },
11
85
  },
12
- } satisfies Meta<typeof Button>;
86
+ args: { variant: 'default', size: 'medium' },
87
+ };
13
88
 
14
89
  export default meta;
15
- type Story = StoryObj<typeof meta>;
90
+ type Story = StoryObj<ButtonStoryArgs>;
91
+
92
+ export const Playground: Story = {
93
+ args: {
94
+ children: 'Button',
95
+ iconStart: 'none',
96
+ iconEnd: 'none',
97
+ },
98
+ };
16
99
 
17
- export const Default: Story = { args: { children: 'Default', variant: 'default' } };
18
- export const Ghost: Story = { args: { children: 'Ghost', variant: 'ghost' } };
100
+ export const Default: Story = { args: { children: 'Default' } };
101
+ export const Outline: Story = { args: { children: 'Outline', variant: 'outline' } };
19
102
  export const Small: Story = { args: { children: 'Small', size: 'small' } };
20
103
  export const Medium: Story = { args: { children: 'Medium', size: 'medium' } };
21
104
  export const Large: Story = { args: { children: 'Large', size: 'large' } };
22
105
  export const AsLink: Story = { args: { children: 'Link Button', href: 'https://example.com' } };
23
106
  export const WithPulse: Story = { args: { children: 'Pulsing', hasPulse: true } };
24
107
  export const Disabled: Story = { args: { children: 'Disabled', disabled: true } };
25
- export const WithIcons: Story = {
108
+
109
+ export const WithIconStart: Story = { args: { children: 'Confirm', iconStart: 'check' } };
110
+ export const WithIconEnd: Story = { args: { children: 'Continue', iconEnd: 'arrowRight' } };
111
+ export const WithBothIcons: Story = { args: { children: 'Both', iconStart: 'arrowLeft', iconEnd: 'arrowRight' } };
112
+
113
+ export const IconOnly: Story = {
26
114
  args: {
27
- children: 'With Icons',
28
- iconStart: <span>→</span>,
29
- iconEnd: <span>←</span>,
115
+ isIconOnly: true,
116
+ 'aria-label': 'Confirm',
117
+ children: CheckIcon,
30
118
  },
31
119
  };
120
+
121
+ export const IconOnlyOutline: Story = {
122
+ args: {
123
+ variant: 'outline',
124
+ isIconOnly: true,
125
+ 'aria-label': 'Confirm',
126
+ children: CheckIcon,
127
+ },
128
+ };
129
+
32
130
  export const AllVariants: Story = {
33
131
  render: () => (
34
132
  <div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', alignItems: 'center' }}>
35
- {(['default', 'ghost'] as const).map(v => (
36
- ['small', 'medium', 'large'].map(s => (
37
- <Button key={`${v}-${s}`} variant={v} size={s as 'small' | 'medium' | 'large'}>{v} {s}</Button>
133
+ {(['default', 'outline'] as const).map(v =>
134
+ (['small', 'medium', 'large'] as const).map(s => (
135
+ <Button key={`${v}-${s}`} variant={v} size={s}>{v} {s}</Button>
136
+ ))
137
+ )}
138
+ </div>
139
+ ),
140
+ };
141
+
142
+ export const AllIconOnly: Story = {
143
+ render: () => (
144
+ <div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', alignItems: 'center' }}>
145
+ {(['default', 'outline'] as const).map(v =>
146
+ (['small', 'medium', 'large'] as const).map(s => (
147
+ <Button
148
+ key={`${v}-${s}`}
149
+ variant={v}
150
+ size={s}
151
+ isIconOnly
152
+ aria-label={`${v} ${s}`}
153
+ >
154
+ {CheckIcon}
155
+ </Button>
38
156
  ))
39
- ))}
157
+ )}
40
158
  </div>
41
159
  ),
42
160
  };
@@ -3,20 +3,50 @@ import css from './Button.module.css';
3
3
  import { cn } from '@boostdev/design-system-foundation';
4
4
  import type { WithClassName } from '../../../types';
5
5
 
6
+ /**
7
+ * Props for the Button component.
8
+ *
9
+ * Button spreads all unrecognised props onto the underlying element —
10
+ * `<button>` when `href` is absent, `<a>` when `href` is provided —
11
+ * so every standard HTML attribute for that element is accepted.
12
+ *
13
+ * Notably, this means `type`, `form`, `name`, `value`, `autoFocus`, all
14
+ * `aria-*`, all `data-*`, and every DOM event handler are valid props.
15
+ */
6
16
  export interface ButtonProps extends WithClassName, ButtonHTMLAttributes<HTMLButtonElement | HTMLAnchorElement> {
17
+ /** When provided, the button renders as an `<a>` element pointing to this URL. */
7
18
  href?: string;
19
+ /** Visual style. `'default'` is filled; `'outline'` is transparent with a border. `'ghost'` is a deprecated alias for `'outline'`. */
8
20
  variant?: 'default' | 'outline' | /** @deprecated Use "outline" instead */ 'ghost';
21
+ /**
22
+ * Button behaviour within a form. Defaults to `'button'` (no form submission).
23
+ * Set to `'submit'` to trigger the parent `<form>`'s submit handler on click or Enter;
24
+ * `'reset'` to reset the form. Ignored when rendering as `<a>` (i.e. when `href` is set).
25
+ */
9
26
  type?: 'button' | 'submit' | 'reset';
27
+ /** Control height and padding. Defaults to `'medium'`. */
10
28
  size?: 'small' | 'medium' | 'large';
29
+ /** Icon rendered before the label. */
11
30
  iconStart?: ReactNode;
31
+ /** Icon rendered after the label. */
12
32
  iconEnd?: ReactNode;
33
+ /** Label content. Required unless `aria-label` is set (icon-only button). */
13
34
  children?: ReactNode;
35
+ /** When true, blocks interaction, sets `aria-disabled`, and removes the element from the tab order. */
14
36
  disabled?: boolean;
37
+ /** Adds a pulsing animation for call-to-action emphasis. Respects `prefers-reduced-motion`. */
15
38
  hasPulse?: boolean;
39
+ /** When true, forces a 1:1 aspect ratio with equal `xs` padding on both axes. Pass the icon as `children`; composable with any `variant`. Requires `aria-label`. */
40
+ isIconOnly?: boolean;
41
+ /** Click handler. Typed as `HTMLElement` because the root may be `<button>` or `<a>`. */
16
42
  onClick?: MouseEventHandler<HTMLElement>;
43
+ /** Anchor target (only applied when rendered as `<a>`). */
17
44
  target?: string;
45
+ /** Anchor rel (only applied when rendered as `<a>`). */
18
46
  rel?: string;
47
+ /** Accessible label. Required for icon-only buttons (no text children). */
19
48
  'aria-label'?: string;
49
+ /** ID(s) of element(s) that describe the button (e.g. a tooltip or helper text). */
20
50
  'aria-describedby'?: string;
21
51
  }
22
52
 
@@ -29,6 +59,7 @@ export function Button({
29
59
  iconEnd,
30
60
  size = 'medium',
31
61
  hasPulse = false,
62
+ isIconOnly = false,
32
63
  href,
33
64
  target,
34
65
  rel,
@@ -41,6 +72,7 @@ export function Button({
41
72
  css[`--${variant}`],
42
73
  css[`--size_${size}`],
43
74
  hasPulse && css['--hasPulse'],
75
+ isIconOnly && css['--iconOnly'],
44
76
  className,
45
77
  );
46
78
 
@@ -1,4 +1,4 @@
1
- @layer component {
1
+ @layer boostdev.component {
2
2
  .dialog {
3
3
  padding: 0;
4
4
  border: none;
@@ -1,4 +1,4 @@
1
- @layer component {
1
+ @layer boostdev.component {
2
2
  .dialog {
3
3
  font-size: var(--bds-font_size--body--s);
4
4
  border: var(--dialog_border, none);
@@ -12,7 +12,7 @@ A modal dialog overlay that centers content on screen with a semi-transparent ba
12
12
  ## Usage
13
13
 
14
14
  ```tsx
15
- import { Dialog } from '@boostdev/components';
15
+ import { Dialog } from '@boostdev/design-system-components';
16
16
 
17
17
  const [open, setOpen] = useState(false);
18
18
 
@@ -25,7 +25,7 @@ const [open, setOpen] = useState(false);
25
25
  Wrap your tree in `ThemeProvider` (once, at the app root):
26
26
 
27
27
  ```tsx
28
- import { ThemeProvider } from '@boostdev/components/native/ThemeContext';
28
+ import { ThemeProvider } from '@boostdev/design-system-components/native/ThemeContext';
29
29
 
30
30
  export default function App() {
31
31
  return <ThemeProvider><YourApp /></ThemeProvider>;
@@ -1,4 +1,4 @@
1
- @layer component {
1
+ @layer boostdev.component {
2
2
  .drawer {
3
3
  position: fixed;
4
4
  inset-block: 0;
@@ -12,7 +12,7 @@ A sliding panel that slides in from the bottom of the screen. Built on React Nat
12
12
  ## Usage
13
13
 
14
14
  ```tsx
15
- import { Drawer } from '@boostdev/components';
15
+ import { Drawer } from '@boostdev/design-system-components';
16
16
 
17
17
  const [open, setOpen] = useState(false);
18
18
 
@@ -25,7 +25,7 @@ const [open, setOpen] = useState(false);
25
25
  Wrap your tree in `ThemeProvider` (once, at the app root):
26
26
 
27
27
  ```tsx
28
- import { ThemeProvider } from '@boostdev/components/native/ThemeContext';
28
+ import { ThemeProvider } from '@boostdev/design-system-components/native/ThemeContext';
29
29
 
30
30
  export default function App() {
31
31
  return <ThemeProvider><YourApp /></ThemeProvider>;
@@ -1,4 +1,4 @@
1
- @layer component {
1
+ @layer boostdev.component {
2
2
  .wrapper {
3
3
  position: relative;
4
4
  display: inline-flex;
@@ -12,7 +12,7 @@ A dropdown menu that opens as a bottom sheet modal on mobile. Accepts a trigger
12
12
  ## Usage
13
13
 
14
14
  ```tsx
15
- import { DropdownMenu } from '@boostdev/components';
15
+ import { DropdownMenu } from '@boostdev/design-system-components';
16
16
 
17
17
  <DropdownMenu
18
18
  trigger={<Button>Actions</Button>}
@@ -27,7 +27,7 @@ import { DropdownMenu } from '@boostdev/components';
27
27
  Wrap your tree in `ThemeProvider` (once, at the app root):
28
28
 
29
29
  ```tsx
30
- import { ThemeProvider } from '@boostdev/components/native/ThemeContext';
30
+ import { ThemeProvider } from '@boostdev/design-system-components/native/ThemeContext';
31
31
 
32
32
  export default function App() {
33
33
  return <ThemeProvider><YourApp /></ThemeProvider>;
@@ -1,4 +1,4 @@
1
- @layer component {
1
+ @layer boostdev.component {
2
2
  .wrapper {
3
3
  display: inline-flex;
4
4
 
@@ -12,7 +12,7 @@ A popover overlay that displays content in a centered modal. The trigger element
12
12
  ## Usage
13
13
 
14
14
  ```tsx
15
- import { Popover } from '@boostdev/components';
15
+ import { Popover } from '@boostdev/design-system-components';
16
16
 
17
17
  <Popover
18
18
  content={<Text>Additional information here.</Text>}
@@ -25,7 +25,7 @@ import { Popover } from '@boostdev/components';
25
25
  Wrap your tree in `ThemeProvider` (once, at the app root):
26
26
 
27
27
  ```tsx
28
- import { ThemeProvider } from '@boostdev/components/native/ThemeContext';
28
+ import { ThemeProvider } from '@boostdev/design-system-components/native/ThemeContext';
29
29
 
30
30
  export default function App() {
31
31
  return <ThemeProvider><YourApp /></ThemeProvider>;
@@ -1,4 +1,4 @@
1
- @layer component {
1
+ @layer boostdev.component {
2
2
  .rating {
3
3
  display: inline-flex;
4
4
  gap: var(--bds-space_xxxs);
@@ -12,7 +12,7 @@ React Native read-only star rating display using text characters. Shows filled a
12
12
  ## Usage
13
13
 
14
14
  ```tsx
15
- import { Rating } from '@boostdev/components';
15
+ import { Rating } from '@boostdev/design-system-components';
16
16
 
17
17
  <Rating value={4} />
18
18
  <Rating value={7} max={10} />
@@ -1,4 +1,4 @@
1
- @layer component {
1
+ @layer boostdev.component {
2
2
  .toastContainer {
3
3
  position: fixed;
4
4
  inset: unset; /* override UA popover default (inset: 0) */
@@ -14,8 +14,8 @@ A toast notification system using React context. Provides `ToastProvider` and `u
14
14
  Wrap your app root in `ToastProvider`:
15
15
 
16
16
  ```tsx
17
- import { ToastProvider } from '@boostdev/components';
18
- import { ThemeProvider } from '@boostdev/components/native/ThemeContext';
17
+ import { ToastProvider } from '@boostdev/design-system-components';
18
+ import { ThemeProvider } from '@boostdev/design-system-components/native/ThemeContext';
19
19
 
20
20
  export default function App() {
21
21
  return (
@@ -31,7 +31,7 @@ export default function App() {
31
31
  Then use `useToast` in any descendant:
32
32
 
33
33
  ```tsx
34
- import { useToast } from '@boostdev/components';
34
+ import { useToast } from '@boostdev/design-system-components';
35
35
 
36
36
  function SaveButton() {
37
37
  const { showToast } = useToast();
@@ -1,4 +1,4 @@
1
- @layer component {
1
+ @layer boostdev.component {
2
2
  .checkboxGroup {
3
3
  --inputSize: calc(var(--bds-space_s) * 2);
4
4
 
@@ -12,7 +12,7 @@ React Native checkbox component with label, optional hint, and error state. Full
12
12
  ## Usage
13
13
 
14
14
  ```tsx
15
- import { Checkbox } from '@boostdev/components';
15
+ import { Checkbox } from '@boostdev/design-system-components';
16
16
  import { useState } from 'react';
17
17
 
18
18
  function Example() {
@@ -1,4 +1,4 @@
1
- @layer component {
1
+ @layer boostdev.component {
2
2
  .group {
3
3
  border: none;
4
4
  padding: 0;
@@ -12,7 +12,7 @@ Groups related checkboxes under a common legend with optional hint and error mes
12
12
  ## Usage
13
13
 
14
14
  ```tsx
15
- import { CheckboxGroup, Checkbox } from '@boostdev/components';
15
+ import { CheckboxGroup, Checkbox } from '@boostdev/design-system-components';
16
16
  import { useState } from 'react';
17
17
 
18
18
  function Example() {
@@ -1,4 +1,4 @@
1
- @layer component {
1
+ @layer boostdev.component {
2
2
  .formGroup {
3
3
  display: flex;
4
4
  flex-direction: column;