@boostdev/design-system-components 1.2.2 → 1.2.4

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 (215) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +50 -5
  3. package/dist/client.cjs +50 -50
  4. package/dist/client.css +506 -503
  5. package/dist/client.js +50 -50
  6. package/dist/index.cjs +50 -50
  7. package/dist/index.css +506 -503
  8. package/dist/index.js +50 -50
  9. package/dist/native/index.cjs +3692 -352
  10. package/dist/native/index.d.cts +359 -3
  11. package/dist/native/index.d.ts +359 -3
  12. package/dist/native/index.js +3811 -364
  13. package/package.json +3 -3
  14. package/src/components/interaction/Button/Button.module.css +1 -4
  15. package/src/components/interaction/Button/Button.native.stories.tsx +10 -12
  16. package/src/components/interaction/Button/Button.native.tsx +3 -3
  17. package/src/components/interaction/Dialog/Dialog.module.css +1 -1
  18. package/src/components/interaction/Dialog/Dialog.native.mdx +61 -0
  19. package/src/components/interaction/Dialog/Dialog.native.spec.tsx +73 -0
  20. package/src/components/interaction/Dialog/Dialog.native.stories.tsx +53 -0
  21. package/src/components/interaction/Dialog/Dialog.native.tsx +128 -0
  22. package/src/components/interaction/Drawer/Drawer.module.css +1 -1
  23. package/src/components/interaction/Drawer/Drawer.native.mdx +58 -0
  24. package/src/components/interaction/Drawer/Drawer.native.spec.tsx +81 -0
  25. package/src/components/interaction/Drawer/Drawer.native.stories.tsx +33 -0
  26. package/src/components/interaction/Drawer/Drawer.native.tsx +175 -0
  27. package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +4 -0
  28. package/src/components/interaction/DropdownMenu/DropdownMenu.native.mdx +74 -0
  29. package/src/components/interaction/DropdownMenu/DropdownMenu.native.spec.tsx +78 -0
  30. package/src/components/interaction/DropdownMenu/DropdownMenu.native.stories.tsx +51 -0
  31. package/src/components/interaction/DropdownMenu/DropdownMenu.native.tsx +254 -0
  32. package/src/components/interaction/DropdownMenu/DropdownMenu.stories.tsx +4 -4
  33. package/src/components/interaction/Popover/Popover.native.mdx +61 -0
  34. package/src/components/interaction/Popover/Popover.native.spec.tsx +73 -0
  35. package/src/components/interaction/Popover/Popover.native.stories.tsx +44 -0
  36. package/src/components/interaction/Popover/Popover.native.tsx +87 -0
  37. package/src/components/interaction/Rating/Rating.native.mdx +55 -0
  38. package/src/components/interaction/Rating/Rating.native.spec.tsx +38 -0
  39. package/src/components/interaction/Rating/Rating.native.stories.tsx +37 -0
  40. package/src/components/interaction/Rating/Rating.native.tsx +50 -0
  41. package/src/components/interaction/Toast/Toast.native.mdx +81 -0
  42. package/src/components/interaction/Toast/Toast.native.spec.tsx +80 -0
  43. package/src/components/interaction/Toast/Toast.native.stories.tsx +47 -0
  44. package/src/components/interaction/Toast/Toast.native.tsx +202 -0
  45. package/src/components/interaction/form/Checkbox/Checkbox.native.stories.tsx +2 -4
  46. package/src/components/interaction/form/Checkbox/Checkbox.native.tsx +1 -1
  47. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.mdx +78 -0
  48. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.spec.tsx +57 -0
  49. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.stories.tsx +96 -0
  50. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.tsx +61 -0
  51. package/src/components/interaction/form/Combobox/Combobox.module.css +1 -1
  52. package/src/components/interaction/form/Combobox/Combobox.native.mdx +86 -0
  53. package/src/components/interaction/form/Combobox/Combobox.native.spec.tsx +60 -0
  54. package/src/components/interaction/form/Combobox/Combobox.native.stories.tsx +99 -0
  55. package/src/components/interaction/form/Combobox/Combobox.native.tsx +211 -0
  56. package/src/components/interaction/form/FileInput/FileInput.native.mdx +86 -0
  57. package/src/components/interaction/form/FileInput/FileInput.native.spec.tsx +73 -0
  58. package/src/components/interaction/form/FileInput/FileInput.native.stories.tsx +69 -0
  59. package/src/components/interaction/form/FileInput/FileInput.native.tsx +129 -0
  60. package/src/components/interaction/form/FormInput/FormInput.module.css +1 -1
  61. package/src/components/interaction/form/FormInput/FormInput.native.mdx +81 -0
  62. package/src/components/interaction/form/FormInput/FormInput.native.spec.tsx +51 -0
  63. package/src/components/interaction/form/FormInput/FormInput.native.stories.tsx +54 -0
  64. package/src/components/interaction/form/FormInput/FormInput.native.tsx +122 -0
  65. package/src/components/interaction/form/NumberInput/NumberInput.module.css +1 -1
  66. package/src/components/interaction/form/NumberInput/NumberInput.native.mdx +87 -0
  67. package/src/components/interaction/form/NumberInput/NumberInput.native.spec.tsx +63 -0
  68. package/src/components/interaction/form/NumberInput/NumberInput.native.stories.tsx +63 -0
  69. package/src/components/interaction/form/NumberInput/NumberInput.native.tsx +198 -0
  70. package/src/components/interaction/form/Radio/Radio.native.spec.tsx +79 -0
  71. package/src/components/interaction/form/Radio/Radio.native.stories.tsx +12 -14
  72. package/src/components/interaction/form/Radio/Radio.native.tsx +1 -1
  73. package/src/components/interaction/form/RadioGroup/RadioGroup.native.mdx +79 -0
  74. package/src/components/interaction/form/RadioGroup/RadioGroup.native.spec.tsx +66 -0
  75. package/src/components/interaction/form/RadioGroup/RadioGroup.native.stories.tsx +96 -0
  76. package/src/components/interaction/form/RadioGroup/RadioGroup.native.tsx +61 -0
  77. package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +5 -5
  78. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.mdx +78 -0
  79. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.spec.tsx +82 -0
  80. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.stories.tsx +99 -0
  81. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.tsx +121 -0
  82. package/src/components/interaction/form/Select/Select.module.css +1 -1
  83. package/src/components/interaction/form/Select/Select.native.mdx +85 -0
  84. package/src/components/interaction/form/Select/Select.native.spec.tsx +64 -0
  85. package/src/components/interaction/form/Select/Select.native.stories.tsx +75 -0
  86. package/src/components/interaction/form/Select/Select.native.tsx +252 -0
  87. package/src/components/interaction/form/Slider/Slider.module.css +4 -4
  88. package/src/components/interaction/form/Slider/Slider.native.mdx +83 -0
  89. package/src/components/interaction/form/Slider/Slider.native.spec.tsx +51 -0
  90. package/src/components/interaction/form/Slider/Slider.native.stories.tsx +58 -0
  91. package/src/components/interaction/form/Slider/Slider.native.tsx +188 -0
  92. package/src/components/interaction/form/Switch/Switch.module.css +3 -3
  93. package/src/components/interaction/form/Switch/Switch.native.stories.tsx +2 -4
  94. package/src/components/interaction/form/Textarea/Textarea.native.mdx +76 -0
  95. package/src/components/interaction/form/Textarea/Textarea.native.spec.tsx +50 -0
  96. package/src/components/interaction/form/Textarea/Textarea.native.stories.tsx +50 -0
  97. package/src/components/interaction/form/Textarea/Textarea.native.tsx +108 -0
  98. package/src/components/interaction/form/atoms/Label.native.spec.tsx +39 -0
  99. package/src/components/interaction/form/atoms/Label.native.stories.tsx +1 -1
  100. package/src/components/interaction/form/atoms/Message.native.stories.tsx +1 -1
  101. package/src/components/layout/ButtonGroup/ButtonGroup.native.spec.tsx +83 -0
  102. package/src/components/layout/ButtonGroup/ButtonGroup.native.stories.tsx +13 -19
  103. package/src/components/layout/Card/Card.native.spec.tsx +102 -0
  104. package/src/components/layout/Card/Card.native.stories.tsx +9 -11
  105. package/src/components/layout/Card/Card.native.tsx +1 -1
  106. package/src/components/layout/IconWrapper/IconWrapper.module.css +1 -1
  107. package/src/components/layout/IconWrapper/IconWrapper.native.stories.tsx +7 -11
  108. package/src/components/layout/SectionHeader/SectionHeader.native.spec.tsx +51 -0
  109. package/src/components/layout/SectionHeader/SectionHeader.native.stories.tsx +6 -8
  110. package/src/components/ui/Accordion/Accordion.native.mdx +65 -0
  111. package/src/components/ui/Accordion/Accordion.native.spec.tsx +69 -0
  112. package/src/components/ui/Accordion/Accordion.native.stories.tsx +52 -0
  113. package/src/components/ui/Accordion/Accordion.native.tsx +141 -0
  114. package/src/components/ui/Alert/Alert.module.css +1 -1
  115. package/src/components/ui/Alert/Alert.native.spec.tsx +76 -0
  116. package/src/components/ui/Alert/Alert.native.stories.tsx +8 -10
  117. package/src/components/ui/Alert/Alert.native.tsx +7 -7
  118. package/src/components/ui/Avatar/Avatar.module.css +1 -1
  119. package/src/components/ui/Avatar/Avatar.native.stories.tsx +6 -8
  120. package/src/components/ui/Avatar/Avatar.native.tsx +2 -2
  121. package/src/components/ui/Badge/Badge.module.css +1 -1
  122. package/src/components/ui/Badge/Badge.native.spec.tsx +45 -0
  123. package/src/components/ui/Badge/Badge.native.stories.tsx +6 -8
  124. package/src/components/ui/Badge/Badge.native.tsx +2 -2
  125. package/src/components/ui/Breadcrumb/Breadcrumb.native.mdx +52 -0
  126. package/src/components/ui/Breadcrumb/Breadcrumb.native.spec.tsx +49 -0
  127. package/src/components/ui/Breadcrumb/Breadcrumb.native.stories.tsx +46 -0
  128. package/src/components/ui/Breadcrumb/Breadcrumb.native.tsx +83 -0
  129. package/src/components/ui/Calendar/Calendar.module.css +2 -2
  130. package/src/components/ui/Calendar/Calendar.native.mdx +58 -0
  131. package/src/components/ui/Calendar/Calendar.native.spec.tsx +92 -0
  132. package/src/components/ui/Calendar/Calendar.native.stories.tsx +45 -0
  133. package/src/components/ui/Calendar/Calendar.native.tsx +294 -0
  134. package/src/components/ui/Carousel/Carousel.module.css +1 -1
  135. package/src/components/ui/Carousel/Carousel.native.mdx +55 -0
  136. package/src/components/ui/Carousel/Carousel.native.spec.tsx +55 -0
  137. package/src/components/ui/Carousel/Carousel.native.stories.tsx +59 -0
  138. package/src/components/ui/Carousel/Carousel.native.tsx +184 -0
  139. package/src/components/ui/Collapsible/Collapsible.native.mdx +53 -0
  140. package/src/components/ui/Collapsible/Collapsible.native.spec.tsx +68 -0
  141. package/src/components/ui/Collapsible/Collapsible.native.stories.tsx +48 -0
  142. package/src/components/ui/Collapsible/Collapsible.native.tsx +139 -0
  143. package/src/components/ui/DescriptionList/DescriptionList.native.mdx +50 -0
  144. package/src/components/ui/DescriptionList/DescriptionList.native.spec.tsx +37 -0
  145. package/src/components/ui/DescriptionList/DescriptionList.native.stories.tsx +48 -0
  146. package/src/components/ui/DescriptionList/DescriptionList.native.tsx +54 -0
  147. package/src/components/ui/Link/Link.native.mdx +53 -0
  148. package/src/components/ui/Link/Link.native.spec.tsx +35 -0
  149. package/src/components/ui/Link/Link.native.stories.tsx +46 -0
  150. package/src/components/ui/Link/Link.native.tsx +48 -0
  151. package/src/components/ui/Loading/Loading.module.css +1 -1
  152. package/src/components/ui/Loading/Loading.native.stories.tsx +6 -8
  153. package/src/components/ui/NotificationBanner/NotificationBanner.module.css +1 -1
  154. package/src/components/ui/NotificationBanner/NotificationBanner.native.spec.tsx +85 -0
  155. package/src/components/ui/NotificationBanner/NotificationBanner.native.stories.tsx +8 -10
  156. package/src/components/ui/NotificationBanner/NotificationBanner.native.tsx +7 -7
  157. package/src/components/ui/Pagination/Pagination.module.css +5 -1
  158. package/src/components/ui/Pagination/Pagination.native.mdx +62 -0
  159. package/src/components/ui/Pagination/Pagination.native.spec.tsx +86 -0
  160. package/src/components/ui/Pagination/Pagination.native.stories.tsx +47 -0
  161. package/src/components/ui/Pagination/Pagination.native.tsx +157 -0
  162. package/src/components/ui/Progress/Progress.module.css +2 -2
  163. package/src/components/ui/Progress/Progress.native.spec.tsx +56 -0
  164. package/src/components/ui/Progress/Progress.native.stories.tsx +1 -1
  165. package/src/components/ui/Progress/Progress.native.tsx +2 -2
  166. package/src/components/ui/ProgressCircle/ProgressCircle.native.mdx +63 -0
  167. package/src/components/ui/ProgressCircle/ProgressCircle.native.spec.tsx +39 -0
  168. package/src/components/ui/ProgressCircle/ProgressCircle.native.stories.tsx +36 -0
  169. package/src/components/ui/ProgressCircle/ProgressCircle.native.tsx +176 -0
  170. package/src/components/ui/Separator/Separator.native.spec.tsx +39 -0
  171. package/src/components/ui/Separator/Separator.native.stories.tsx +10 -14
  172. package/src/components/ui/Skeleton/Skeleton.native.spec.tsx +39 -0
  173. package/src/components/ui/Skeleton/Skeleton.native.stories.tsx +6 -8
  174. package/src/components/ui/Skeleton/Skeleton.native.tsx +2 -1
  175. package/src/components/ui/Table/Table.native.mdx +72 -0
  176. package/src/components/ui/Table/Table.native.spec.tsx +83 -0
  177. package/src/components/ui/Table/Table.native.stories.tsx +55 -0
  178. package/src/components/ui/Table/Table.native.tsx +196 -0
  179. package/src/components/ui/Tabs/Tabs.native.mdx +65 -0
  180. package/src/components/ui/Tabs/Tabs.native.spec.tsx +65 -0
  181. package/src/components/ui/Tabs/Tabs.native.stories.tsx +57 -0
  182. package/src/components/ui/Tabs/Tabs.native.tsx +133 -0
  183. package/src/components/ui/Tooltip/Tooltip.module.css +1 -1
  184. package/src/components/ui/Tooltip/Tooltip.native.mdx +47 -0
  185. package/src/components/ui/Tooltip/Tooltip.native.spec.tsx +37 -0
  186. package/src/components/ui/Tooltip/Tooltip.native.stories.tsx +38 -0
  187. package/src/components/ui/Tooltip/Tooltip.native.tsx +57 -0
  188. package/src/components/ui/Typography/Typography.native.spec.tsx +49 -0
  189. package/src/components/ui/Typography/Typography.native.stories.tsx +8 -10
  190. package/src/native/ThemeContext.tsx +3 -3
  191. package/src/native.ts +37 -0
  192. package/src/stories/ReactNative.mdx +48 -13
  193. package/src/web-components/interaction/BdsAccordion.stories.tsx +1 -1
  194. package/src/web-components/interaction/BdsCollapsible.stories.tsx +1 -1
  195. package/src/web-components/interaction/BdsSkipLink.stories.tsx +1 -1
  196. package/src/web-components/interaction/BdsTabs.stories.tsx +1 -1
  197. package/src/web-components/interaction/BdsTooltip.stories.tsx +1 -1
  198. package/src/web-components/interaction/form/BdsCheckbox.stories.tsx +1 -1
  199. package/src/web-components/interaction/form/BdsCheckboxGroup.stories.tsx +1 -1
  200. package/src/web-components/interaction/form/BdsCombobox.stories.tsx +1 -1
  201. package/src/web-components/interaction/form/BdsFileInput.stories.tsx +1 -1
  202. package/src/web-components/interaction/form/BdsFormInput.stories.tsx +1 -1
  203. package/src/web-components/interaction/form/BdsNumberInput.stories.tsx +1 -1
  204. package/src/web-components/interaction/form/BdsRadio.stories.tsx +1 -1
  205. package/src/web-components/interaction/form/BdsRadioGroup.stories.tsx +1 -1
  206. package/src/web-components/interaction/form/BdsSegmentedControl.stories.tsx +1 -1
  207. package/src/web-components/interaction/form/BdsSelect.stories.tsx +1 -1
  208. package/src/web-components/interaction/form/BdsSlider.stories.tsx +1 -1
  209. package/src/web-components/interaction/form/BdsSwitch.stories.tsx +1 -1
  210. package/src/web-components/interaction/form/BdsTextarea.stories.tsx +1 -1
  211. package/src/web-components/ui/BdsButtonGroup.stories.tsx +1 -1
  212. package/src/web-components/ui/BdsCard.stories.tsx +1 -1
  213. package/src/web-components/ui/BdsIconWrapper.stories.tsx +1 -1
  214. package/src/web-components/ui/BdsRating.stories.tsx +1 -1
  215. package/src/web-components/ui/BdsSectionHeader.stories.tsx +1 -1
@@ -0,0 +1,78 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './SegmentedControl.native.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # SegmentedControl (React Native)
7
+
8
+ A horizontal control with multiple segments, where each segment acts as a tab. Supports `outline` and `filled` variants with three size options.
9
+
10
+ > **Status: alpha** -- API may change before stable release.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { SegmentedControl } from '@boostdev/components';
16
+ import { useState } from 'react';
17
+
18
+ function Example() {
19
+ const [index, setIndex] = useState(0);
20
+
21
+ return (
22
+ <SegmentedControl
23
+ selectedIndex={index}
24
+ onValueChange={setIndex}
25
+ accessibilityLabel="View mode"
26
+ >
27
+ {'Day'}
28
+ {'Week'}
29
+ {'Month'}
30
+ </SegmentedControl>
31
+ );
32
+ }
33
+ ```
34
+
35
+ ## Examples
36
+
37
+ ### Outline (default)
38
+ <Canvas of={Stories.Outline} />
39
+
40
+ ### Filled
41
+ <Canvas of={Stories.Filled} />
42
+
43
+ ### Small
44
+ <Canvas of={Stories.Small} />
45
+
46
+ ### Large
47
+ <Canvas of={Stories.Large} />
48
+
49
+ ### Disabled
50
+ <Canvas of={Stories.Disabled} />
51
+
52
+ ### Interactive
53
+ <Canvas of={Stories.Interactive} />
54
+
55
+ ### Interactive filled
56
+ <Canvas of={Stories.InteractiveFilled} />
57
+
58
+ ## Props
59
+
60
+ <ArgTypes of={Stories} />
61
+
62
+ ## Props reference
63
+
64
+ | Prop | Type | Default | Description |
65
+ |------|------|---------|-------------|
66
+ | `selectedIndex` | number | `0` | Zero-based index of the active segment |
67
+ | `disabled` | boolean | `false` | Disables all segments |
68
+ | `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Controls height and font size |
69
+ | `variant` | `'outline' \| 'filled'` | `'outline'` | Visual style of the control |
70
+ | `children` | ReactNode | -- | Segment labels (strings or elements) |
71
+ | `accessibilityLabel` | string | -- | Label for the tab group |
72
+ | `onValueChange` | `(index: number) => void` | -- | Called with the tapped segment index |
73
+
74
+ ## Accessibility
75
+
76
+ - Uses `accessibilityRole="tablist"` on the container
77
+ - Each segment uses `accessibilityRole="tab"` with `accessibilityState.selected`
78
+ - Disabled state is communicated via `accessibilityState`
@@ -0,0 +1,82 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../../native/ThemeContext';
3
+ import { SegmentedControl } from './SegmentedControl.native';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ describe('SegmentedControl (native)', () => {
10
+ it('renders all segments', () => {
11
+ renderWithTheme(
12
+ <SegmentedControl accessibilityLabel="View">
13
+ {'Day'}
14
+ {'Week'}
15
+ {'Month'}
16
+ </SegmentedControl>,
17
+ );
18
+ expect(screen.getByText('Day')).toBeInTheDocument();
19
+ expect(screen.getByText('Week')).toBeInTheDocument();
20
+ expect(screen.getByText('Month')).toBeInTheDocument();
21
+ });
22
+
23
+ it('has tablist role', () => {
24
+ renderWithTheme(
25
+ <SegmentedControl accessibilityLabel="View">
26
+ {'Day'}
27
+ {'Week'}
28
+ </SegmentedControl>,
29
+ );
30
+ expect(screen.getByRole('tablist')).toBeInTheDocument();
31
+ });
32
+
33
+ it('renders tab roles for each segment', () => {
34
+ renderWithTheme(
35
+ <SegmentedControl accessibilityLabel="View">
36
+ {'Day'}
37
+ {'Week'}
38
+ </SegmentedControl>,
39
+ );
40
+ const tabs = screen.getAllByRole('tab');
41
+ expect(tabs).toHaveLength(2);
42
+ });
43
+
44
+ it('calls onValueChange with the clicked index', () => {
45
+ const onValueChange = vi.fn();
46
+ renderWithTheme(
47
+ <SegmentedControl accessibilityLabel="View" onValueChange={onValueChange}>
48
+ {'Day'}
49
+ {'Week'}
50
+ {'Month'}
51
+ </SegmentedControl>,
52
+ );
53
+ fireEvent.click(screen.getByText('Week'));
54
+ expect(onValueChange).toHaveBeenCalledWith(1);
55
+ });
56
+
57
+ it('does not call onValueChange when disabled', () => {
58
+ const onValueChange = vi.fn();
59
+ renderWithTheme(
60
+ <SegmentedControl accessibilityLabel="View" onValueChange={onValueChange} disabled>
61
+ {'Day'}
62
+ {'Week'}
63
+ </SegmentedControl>,
64
+ );
65
+ fireEvent.click(screen.getByText('Week'));
66
+ expect(onValueChange).not.toHaveBeenCalled();
67
+ });
68
+
69
+ it('renders the selected segment with distinct styling', () => {
70
+ renderWithTheme(
71
+ <SegmentedControl accessibilityLabel="View" selectedIndex={1}>
72
+ {'Day'}
73
+ {'Week'}
74
+ </SegmentedControl>,
75
+ );
76
+ // Verify both tabs render and the selected one exists
77
+ const tabs = screen.getAllByRole('tab');
78
+ expect(tabs).toHaveLength(2);
79
+ // The selected tab text is rendered
80
+ expect(screen.getByText('Week')).toBeInTheDocument();
81
+ });
82
+ });
@@ -0,0 +1,99 @@
1
+ import React, { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { ThemeProvider } from '../../../../native/ThemeContext';
4
+ import { SegmentedControl } from './SegmentedControl.native';
5
+
6
+ const meta = {
7
+ tags: ['!stable', 'alpha'],
8
+ title: 'React Native/Form/SegmentedControl',
9
+ component: SegmentedControl,
10
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
11
+ parameters: { layout: 'padded' },
12
+ } satisfies Meta<typeof SegmentedControl>;
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Outline: Story = {
18
+ args: {
19
+ variant: 'outline',
20
+ selectedIndex: 0,
21
+ children: ['Day', 'Week', 'Month'],
22
+ accessibilityLabel: 'View mode',
23
+ },
24
+ };
25
+
26
+ export const Filled: Story = {
27
+ args: {
28
+ variant: 'filled',
29
+ selectedIndex: 1,
30
+ children: ['Day', 'Week', 'Month'],
31
+ accessibilityLabel: 'View mode',
32
+ },
33
+ };
34
+
35
+ export const Small: Story = {
36
+ args: {
37
+ size: 'small',
38
+ selectedIndex: 0,
39
+ children: ['On', 'Off'],
40
+ accessibilityLabel: 'Toggle',
41
+ },
42
+ };
43
+
44
+ export const Large: Story = {
45
+ args: {
46
+ size: 'large',
47
+ selectedIndex: 2,
48
+ children: ['First', 'Second', 'Third'],
49
+ accessibilityLabel: 'Selection',
50
+ },
51
+ };
52
+
53
+ export const Disabled: Story = {
54
+ args: {
55
+ selectedIndex: 0,
56
+ disabled: true,
57
+ children: ['Day', 'Week', 'Month'],
58
+ accessibilityLabel: 'View mode',
59
+ },
60
+ };
61
+
62
+ function InteractiveComponent() {
63
+ const [index, setIndex] = useState(0);
64
+ return (
65
+ <SegmentedControl
66
+ selectedIndex={index}
67
+ onValueChange={setIndex}
68
+ accessibilityLabel="View mode"
69
+ >
70
+ {'Day'}
71
+ {'Week'}
72
+ {'Month'}
73
+ </SegmentedControl>
74
+ );
75
+ }
76
+
77
+ export const Interactive: Story = {
78
+ render: () => <InteractiveComponent />,
79
+ };
80
+
81
+ function InteractiveFilledComponent() {
82
+ const [index, setIndex] = useState(0);
83
+ return (
84
+ <SegmentedControl
85
+ variant="filled"
86
+ selectedIndex={index}
87
+ onValueChange={setIndex}
88
+ accessibilityLabel="Category"
89
+ >
90
+ {'All'}
91
+ {'Active'}
92
+ {'Completed'}
93
+ </SegmentedControl>
94
+ );
95
+ }
96
+
97
+ export const InteractiveFilled: Story = {
98
+ render: () => <InteractiveFilledComponent />,
99
+ };
@@ -0,0 +1,121 @@
1
+ import { Children, ReactNode } from 'react';
2
+ import { StyleSheet, View, Pressable, Text, StyleProp, ViewStyle } from 'react-native';
3
+ import { spacing, font, border } from '../../../../native/tokens';
4
+ import { useTheme } from '../../../../native/ThemeContext';
5
+
6
+ type SegmentedControlSize = 'small' | 'medium' | 'large';
7
+ type SegmentedControlVariant = 'outline' | 'filled';
8
+
9
+ interface SegmentedControlProps {
10
+ selectedIndex?: number;
11
+ disabled?: boolean;
12
+ size?: SegmentedControlSize;
13
+ variant?: SegmentedControlVariant;
14
+ children: ReactNode;
15
+ accessibilityLabel?: string;
16
+ onValueChange?: (index: number) => void;
17
+ style?: StyleProp<ViewStyle>;
18
+ }
19
+
20
+ const sizeTokens: Record<SegmentedControlSize, { height: number; fontSize: number; px: number }> = {
21
+ small: { height: 32, fontSize: font.size.bodyS, px: spacing.s },
22
+ medium: { height: 40, fontSize: font.size.body, px: spacing.m },
23
+ large: { height: 48, fontSize: font.size.body, px: spacing.l },
24
+ };
25
+
26
+ const styles = StyleSheet.create({
27
+ track: {
28
+ flexDirection: 'row',
29
+ borderRadius: border.radius.full,
30
+ overflow: 'hidden',
31
+ },
32
+ item: {
33
+ flex: 1,
34
+ alignItems: 'center',
35
+ justifyContent: 'center',
36
+ minHeight: 44,
37
+ },
38
+ label: {
39
+ fontFamily: font.family.body,
40
+ fontWeight: font.weight.semibold,
41
+ textAlign: 'center',
42
+ },
43
+ });
44
+
45
+ export function SegmentedControl({
46
+ selectedIndex = 0,
47
+ disabled = false,
48
+ size = 'medium',
49
+ variant = 'outline',
50
+ children,
51
+ accessibilityLabel,
52
+ onValueChange,
53
+ style,
54
+ }: Readonly<SegmentedControlProps>) {
55
+ const { colors } = useTheme();
56
+ const { height, fontSize, px } = sizeTokens[size];
57
+ const items = Children.toArray(children);
58
+
59
+ const trackStyle: ViewStyle = variant === 'filled'
60
+ ? { backgroundColor: colors.colorBgSubtle }
61
+ : { backgroundColor: 'transparent', borderWidth: 1, borderColor: colors.colorBgSubtle };
62
+
63
+ return (
64
+ <View
65
+ style={[styles.track, trackStyle, { borderRadius: border.radius.full }, style]}
66
+ accessibilityRole="tablist"
67
+ accessibilityLabel={accessibilityLabel}
68
+ >
69
+ {items.map((child, index) => {
70
+ const isActive = index === selectedIndex;
71
+
72
+ const activeStyle: ViewStyle = variant === 'filled'
73
+ ? { backgroundColor: colors.colorInteractive }
74
+ : { borderWidth: 2, borderColor: colors.colorInteractive };
75
+
76
+ const inactiveStyle: ViewStyle = variant === 'filled'
77
+ ? {}
78
+ : {};
79
+
80
+ const textColor = variant === 'filled' && isActive
81
+ ? colors.colorOnInteractive
82
+ : isActive
83
+ ? colors.colorInteractive
84
+ : colors.colorOnBgSubtle;
85
+
86
+ // Extract label from child: if it's a string, use it directly.
87
+ // Otherwise render the child as-is.
88
+ const label = typeof child === 'string' || typeof child === 'number'
89
+ ? String(child)
90
+ : null;
91
+
92
+ return (
93
+ <Pressable
94
+ key={index}
95
+ onPress={() => !disabled && onValueChange?.(index)}
96
+ disabled={disabled}
97
+ accessibilityRole="tab"
98
+ accessibilityLabel={label ?? undefined}
99
+ accessibilityState={{ selected: isActive, disabled }}
100
+ style={({ pressed }) => [
101
+ styles.item,
102
+ { height, paddingHorizontal: px },
103
+ isActive ? activeStyle : inactiveStyle,
104
+ variant === 'filled' && isActive && { borderRadius: border.radius.full },
105
+ pressed && !disabled && { opacity: 0.7 },
106
+ disabled && { opacity: 0.4 },
107
+ ]}
108
+ >
109
+ {label !== null ? (
110
+ <Text style={[styles.label, { fontSize, color: textColor }]}>
111
+ {label}
112
+ </Text>
113
+ ) : (
114
+ child
115
+ )}
116
+ </Pressable>
117
+ );
118
+ })}
119
+ </View>
120
+ );
121
+ }
@@ -19,7 +19,7 @@
19
19
  padding-inline-end: var(--bds-space_xxl);
20
20
  border: var(--select_border, none);
21
21
  --bdc_color: currentcolor;
22
- --bdc-outline_radius: var(--bds-border_radius--xs);
22
+ --bdc-outline_radius: var(--bds-border_radius--full);
23
23
 
24
24
  border-radius: var(--select_radius, var(--bdc-outline_radius));
25
25
  box-shadow: inset 0 0 0 var(--bdc-outline_width) var(--bdc_color), var(--select_shadow, var(--bdc-outline_shadow));
@@ -0,0 +1,85 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './Select.native.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # Select (React Native)
7
+
8
+ Dropdown-style select component. Renders a trigger button that opens a bottom sheet modal with a scrollable list of options.
9
+
10
+ > **Status: alpha** — API may change before stable release.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { Select } from '@boostdev/components';
16
+ import { useState } from 'react';
17
+
18
+ const options = [
19
+ { value: 'apple', label: 'Apple' },
20
+ { value: 'banana', label: 'Banana' },
21
+ { value: 'cherry', label: 'Cherry' },
22
+ ];
23
+
24
+ function Example() {
25
+ const [value, setValue] = useState('');
26
+ return (
27
+ <Select
28
+ label="Fruit"
29
+ name="fruit"
30
+ options={options}
31
+ placeholder="Choose a fruit..."
32
+ value={value}
33
+ onChange={setValue}
34
+ />
35
+ );
36
+ }
37
+ ```
38
+
39
+ ## Examples
40
+
41
+ ### Default
42
+ <Canvas of={Stories.Default} />
43
+
44
+ ### With placeholder
45
+ <Canvas of={Stories.WithPlaceholder} />
46
+
47
+ ### With selected value
48
+ <Canvas of={Stories.WithValue} />
49
+
50
+ ### With error
51
+ <Canvas of={Stories.WithError} />
52
+
53
+ ### With hint
54
+ <Canvas of={Stories.WithHint} />
55
+
56
+ ### Disabled
57
+ <Canvas of={Stories.Disabled} />
58
+
59
+ ### Interactive
60
+ <Canvas of={Stories.Interactive} />
61
+
62
+ ## Props
63
+
64
+ <ArgTypes of={Stories} />
65
+
66
+ ## Props reference
67
+
68
+ | Prop | Type | Default | Description |
69
+ |------|------|---------|-------------|
70
+ | `label` | ReactNode | — | Visible label text |
71
+ | `name` | string | — | Field name (used for accessibility) |
72
+ | `options` | `SelectOption[]` | — | Array of `{ value, label, disabled? }` |
73
+ | `placeholder` | string | `'Select an option'` | Placeholder when no value is selected |
74
+ | `value` | string | — | Currently selected value |
75
+ | `error` | string | — | Error message displayed below the trigger |
76
+ | `hint` | string | — | Hint text displayed below the trigger |
77
+ | `disabled` | boolean | `false` | Prevents interaction, reduces opacity |
78
+ | `onChange` | `(value: string) => void` | — | Called with the selected option value |
79
+
80
+ ## Accessibility
81
+
82
+ - Trigger announces the label and current value via `accessibilityLabel`
83
+ - `accessibilityState.expanded` reflects whether the modal is open
84
+ - Each option announces its label and selected state
85
+ - Disabled options are communicated via `accessibilityState.disabled`
@@ -0,0 +1,64 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../../native/ThemeContext';
3
+ import { Select } from './Select.native';
4
+
5
+ const options = [
6
+ { value: 'apple', label: 'Apple' },
7
+ { value: 'banana', label: 'Banana' },
8
+ { value: 'cherry', label: 'Cherry' },
9
+ ];
10
+
11
+ function renderWithTheme(ui: React.ReactElement) {
12
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
13
+ }
14
+
15
+ describe('Select (native)', () => {
16
+ it('renders the label', () => {
17
+ renderWithTheme(<Select label="Fruit" name="fruit" options={options} />);
18
+ expect(screen.getByText('Fruit')).toBeInTheDocument();
19
+ });
20
+
21
+ it('shows placeholder when no value is selected', () => {
22
+ renderWithTheme(
23
+ <Select label="Fruit" name="fruit" options={options} placeholder="Choose..." />,
24
+ );
25
+ expect(screen.getByText('Choose...')).toBeInTheDocument();
26
+ });
27
+
28
+ it('shows the selected option label', () => {
29
+ renderWithTheme(
30
+ <Select label="Fruit" name="fruit" options={options} value="cherry" />,
31
+ );
32
+ expect(screen.getByText('Cherry')).toBeInTheDocument();
33
+ });
34
+
35
+ it('shows error message', () => {
36
+ renderWithTheme(
37
+ <Select label="Fruit" name="fruit" options={options} error="Required." />,
38
+ );
39
+ expect(screen.getByRole('alert')).toHaveTextContent('Required.');
40
+ });
41
+
42
+ it('shows hint message', () => {
43
+ renderWithTheme(
44
+ <Select label="Fruit" name="fruit" options={options} hint="Pick one." />,
45
+ );
46
+ expect(screen.getByText('Pick one.')).toBeInTheDocument();
47
+ });
48
+
49
+ it('renders the trigger button with correct accessibility', () => {
50
+ renderWithTheme(
51
+ <Select label="Fruit" name="fruit" options={options} />,
52
+ );
53
+ const trigger = screen.getByRole('button', { name: /Fruit/ });
54
+ expect(trigger).toBeInTheDocument();
55
+ });
56
+
57
+ it('applies disabled opacity', () => {
58
+ renderWithTheme(
59
+ <Select label="Fruit" name="fruit" options={options} disabled value="banana" />,
60
+ );
61
+ const trigger = screen.getByRole('button', { name: /Fruit/ });
62
+ expect(trigger).toBeDisabled();
63
+ });
64
+ });
@@ -0,0 +1,75 @@
1
+ import React, { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { ThemeProvider } from '../../../../native/ThemeContext';
4
+ import { Select } from './Select.native';
5
+
6
+ const fruitOptions = [
7
+ { value: 'apple', label: 'Apple' },
8
+ { value: 'banana', label: 'Banana' },
9
+ { value: 'cherry', label: 'Cherry' },
10
+ { value: 'dragonfruit', label: 'Dragonfruit' },
11
+ { value: 'elderberry', label: 'Elderberry' },
12
+ ];
13
+
14
+ const meta = {
15
+ tags: ['!stable', 'alpha'],
16
+ title: 'React Native/Form/Select',
17
+ component: Select,
18
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
19
+ parameters: { layout: 'padded' },
20
+ } satisfies Meta<typeof Select>;
21
+
22
+ export default meta;
23
+ type Story = StoryObj<typeof meta>;
24
+
25
+ export const Default: Story = {
26
+ args: { label: 'Fruit', name: 'fruit', options: fruitOptions },
27
+ };
28
+
29
+ export const WithPlaceholder: Story = {
30
+ args: { label: 'Fruit', name: 'fruit', options: fruitOptions, placeholder: 'Choose a fruit...' },
31
+ };
32
+
33
+ export const WithValue: Story = {
34
+ args: { label: 'Fruit', name: 'fruit', options: fruitOptions, value: 'cherry' },
35
+ };
36
+
37
+ export const WithError: Story = {
38
+ args: { label: 'Fruit', name: 'fruit', options: fruitOptions, error: 'Please select a fruit.' },
39
+ };
40
+
41
+ export const WithHint: Story = {
42
+ args: { label: 'Fruit', name: 'fruit', options: fruitOptions, hint: 'Pick your favourite.' },
43
+ };
44
+
45
+ export const Disabled: Story = {
46
+ args: { label: 'Fruit', name: 'fruit', options: fruitOptions, disabled: true, value: 'banana' },
47
+ };
48
+
49
+ export const WithDisabledOption: Story = {
50
+ args: {
51
+ label: 'Fruit',
52
+ name: 'fruit',
53
+ options: [
54
+ { value: 'apple', label: 'Apple' },
55
+ { value: 'banana', label: 'Banana', disabled: true },
56
+ { value: 'cherry', label: 'Cherry' },
57
+ ],
58
+ },
59
+ };
60
+
61
+ export const Interactive: Story = {
62
+ render: () => {
63
+ const [value, setValue] = useState('');
64
+ return (
65
+ <Select
66
+ label="Favourite fruit"
67
+ name="fruit"
68
+ options={fruitOptions}
69
+ placeholder="Choose a fruit..."
70
+ value={value}
71
+ onChange={setValue}
72
+ />
73
+ );
74
+ },
75
+ };