@boostdev/design-system-components 1.2.3 → 1.2.5

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 (198) hide show
  1. package/AGENTS.md +19 -11
  2. package/README.md +36 -1
  3. package/dist/client.cjs +50 -50
  4. package/dist/client.css +470 -470
  5. package/dist/client.js +50 -50
  6. package/dist/index.cjs +50 -50
  7. package/dist/index.css +470 -470
  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/dist/utils.cjs +30 -0
  14. package/dist/utils.d.cts +8 -0
  15. package/dist/utils.d.ts +8 -0
  16. package/dist/utils.js +5 -0
  17. package/package.json +10 -4
  18. package/src/client.ts +6 -0
  19. package/src/components/interaction/Button/Button.native.stories.tsx +10 -12
  20. package/src/components/interaction/Button/Button.native.tsx +3 -3
  21. package/src/components/interaction/Dialog/Dialog.native.mdx +61 -0
  22. package/src/components/interaction/Dialog/Dialog.native.spec.tsx +73 -0
  23. package/src/components/interaction/Dialog/Dialog.native.stories.tsx +53 -0
  24. package/src/components/interaction/Dialog/Dialog.native.tsx +128 -0
  25. package/src/components/interaction/Drawer/Drawer.native.mdx +58 -0
  26. package/src/components/interaction/Drawer/Drawer.native.spec.tsx +81 -0
  27. package/src/components/interaction/Drawer/Drawer.native.stories.tsx +33 -0
  28. package/src/components/interaction/Drawer/Drawer.native.tsx +175 -0
  29. package/src/components/interaction/DropdownMenu/DropdownMenu.native.mdx +74 -0
  30. package/src/components/interaction/DropdownMenu/DropdownMenu.native.spec.tsx +78 -0
  31. package/src/components/interaction/DropdownMenu/DropdownMenu.native.stories.tsx +51 -0
  32. package/src/components/interaction/DropdownMenu/DropdownMenu.native.tsx +254 -0
  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.native.mdx +86 -0
  52. package/src/components/interaction/form/Combobox/Combobox.native.spec.tsx +60 -0
  53. package/src/components/interaction/form/Combobox/Combobox.native.stories.tsx +99 -0
  54. package/src/components/interaction/form/Combobox/Combobox.native.tsx +211 -0
  55. package/src/components/interaction/form/FileInput/FileInput.native.mdx +86 -0
  56. package/src/components/interaction/form/FileInput/FileInput.native.spec.tsx +73 -0
  57. package/src/components/interaction/form/FileInput/FileInput.native.stories.tsx +69 -0
  58. package/src/components/interaction/form/FileInput/FileInput.native.tsx +129 -0
  59. package/src/components/interaction/form/FormInput/FormInput.native.mdx +81 -0
  60. package/src/components/interaction/form/FormInput/FormInput.native.spec.tsx +51 -0
  61. package/src/components/interaction/form/FormInput/FormInput.native.stories.tsx +54 -0
  62. package/src/components/interaction/form/FormInput/FormInput.native.tsx +122 -0
  63. package/src/components/interaction/form/NumberInput/NumberInput.native.mdx +87 -0
  64. package/src/components/interaction/form/NumberInput/NumberInput.native.spec.tsx +63 -0
  65. package/src/components/interaction/form/NumberInput/NumberInput.native.stories.tsx +63 -0
  66. package/src/components/interaction/form/NumberInput/NumberInput.native.tsx +198 -0
  67. package/src/components/interaction/form/Radio/Radio.native.spec.tsx +79 -0
  68. package/src/components/interaction/form/Radio/Radio.native.stories.tsx +12 -14
  69. package/src/components/interaction/form/Radio/Radio.native.tsx +1 -1
  70. package/src/components/interaction/form/RadioGroup/RadioGroup.native.mdx +79 -0
  71. package/src/components/interaction/form/RadioGroup/RadioGroup.native.spec.tsx +66 -0
  72. package/src/components/interaction/form/RadioGroup/RadioGroup.native.stories.tsx +96 -0
  73. package/src/components/interaction/form/RadioGroup/RadioGroup.native.tsx +61 -0
  74. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.mdx +78 -0
  75. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.spec.tsx +82 -0
  76. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.stories.tsx +99 -0
  77. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.tsx +121 -0
  78. package/src/components/interaction/form/Select/Select.native.mdx +85 -0
  79. package/src/components/interaction/form/Select/Select.native.spec.tsx +64 -0
  80. package/src/components/interaction/form/Select/Select.native.stories.tsx +75 -0
  81. package/src/components/interaction/form/Select/Select.native.tsx +252 -0
  82. package/src/components/interaction/form/Slider/Slider.native.mdx +83 -0
  83. package/src/components/interaction/form/Slider/Slider.native.spec.tsx +51 -0
  84. package/src/components/interaction/form/Slider/Slider.native.stories.tsx +58 -0
  85. package/src/components/interaction/form/Slider/Slider.native.tsx +188 -0
  86. package/src/components/interaction/form/Switch/Switch.native.stories.tsx +2 -4
  87. package/src/components/interaction/form/Textarea/Textarea.native.mdx +76 -0
  88. package/src/components/interaction/form/Textarea/Textarea.native.spec.tsx +50 -0
  89. package/src/components/interaction/form/Textarea/Textarea.native.stories.tsx +50 -0
  90. package/src/components/interaction/form/Textarea/Textarea.native.tsx +108 -0
  91. package/src/components/interaction/form/atoms/Label.native.spec.tsx +39 -0
  92. package/src/components/interaction/form/atoms/Label.native.stories.tsx +1 -1
  93. package/src/components/interaction/form/atoms/Message.native.stories.tsx +1 -1
  94. package/src/components/layout/ButtonGroup/ButtonGroup.native.spec.tsx +83 -0
  95. package/src/components/layout/ButtonGroup/ButtonGroup.native.stories.tsx +13 -19
  96. package/src/components/layout/Card/Card.native.spec.tsx +102 -0
  97. package/src/components/layout/Card/Card.native.stories.tsx +9 -11
  98. package/src/components/layout/Card/Card.native.tsx +1 -1
  99. package/src/components/layout/IconWrapper/IconWrapper.native.stories.tsx +7 -11
  100. package/src/components/layout/SectionHeader/SectionHeader.native.spec.tsx +51 -0
  101. package/src/components/layout/SectionHeader/SectionHeader.native.stories.tsx +6 -8
  102. package/src/components/ui/Accordion/Accordion.native.mdx +65 -0
  103. package/src/components/ui/Accordion/Accordion.native.spec.tsx +69 -0
  104. package/src/components/ui/Accordion/Accordion.native.stories.tsx +52 -0
  105. package/src/components/ui/Accordion/Accordion.native.tsx +141 -0
  106. package/src/components/ui/Alert/Alert.native.spec.tsx +76 -0
  107. package/src/components/ui/Alert/Alert.native.stories.tsx +8 -10
  108. package/src/components/ui/Alert/Alert.native.tsx +7 -7
  109. package/src/components/ui/Avatar/Avatar.native.stories.tsx +6 -8
  110. package/src/components/ui/Avatar/Avatar.native.tsx +2 -2
  111. package/src/components/ui/Badge/Badge.native.spec.tsx +45 -0
  112. package/src/components/ui/Badge/Badge.native.stories.tsx +6 -8
  113. package/src/components/ui/Badge/Badge.native.tsx +2 -2
  114. package/src/components/ui/Breadcrumb/Breadcrumb.native.mdx +52 -0
  115. package/src/components/ui/Breadcrumb/Breadcrumb.native.spec.tsx +49 -0
  116. package/src/components/ui/Breadcrumb/Breadcrumb.native.stories.tsx +46 -0
  117. package/src/components/ui/Breadcrumb/Breadcrumb.native.tsx +83 -0
  118. package/src/components/ui/Calendar/Calendar.native.mdx +58 -0
  119. package/src/components/ui/Calendar/Calendar.native.spec.tsx +92 -0
  120. package/src/components/ui/Calendar/Calendar.native.stories.tsx +45 -0
  121. package/src/components/ui/Calendar/Calendar.native.tsx +294 -0
  122. package/src/components/ui/Carousel/Carousel.native.mdx +55 -0
  123. package/src/components/ui/Carousel/Carousel.native.spec.tsx +55 -0
  124. package/src/components/ui/Carousel/Carousel.native.stories.tsx +59 -0
  125. package/src/components/ui/Carousel/Carousel.native.tsx +184 -0
  126. package/src/components/ui/Collapsible/Collapsible.native.mdx +53 -0
  127. package/src/components/ui/Collapsible/Collapsible.native.spec.tsx +68 -0
  128. package/src/components/ui/Collapsible/Collapsible.native.stories.tsx +48 -0
  129. package/src/components/ui/Collapsible/Collapsible.native.tsx +139 -0
  130. package/src/components/ui/DescriptionList/DescriptionList.native.mdx +50 -0
  131. package/src/components/ui/DescriptionList/DescriptionList.native.spec.tsx +37 -0
  132. package/src/components/ui/DescriptionList/DescriptionList.native.stories.tsx +48 -0
  133. package/src/components/ui/DescriptionList/DescriptionList.native.tsx +54 -0
  134. package/src/components/ui/Link/Link.native.mdx +53 -0
  135. package/src/components/ui/Link/Link.native.spec.tsx +35 -0
  136. package/src/components/ui/Link/Link.native.stories.tsx +46 -0
  137. package/src/components/ui/Link/Link.native.tsx +48 -0
  138. package/src/components/ui/Loading/Loading.native.stories.tsx +6 -8
  139. package/src/components/ui/NotificationBanner/NotificationBanner.native.spec.tsx +85 -0
  140. package/src/components/ui/NotificationBanner/NotificationBanner.native.stories.tsx +8 -10
  141. package/src/components/ui/NotificationBanner/NotificationBanner.native.tsx +7 -7
  142. package/src/components/ui/Pagination/Pagination.native.mdx +62 -0
  143. package/src/components/ui/Pagination/Pagination.native.spec.tsx +86 -0
  144. package/src/components/ui/Pagination/Pagination.native.stories.tsx +47 -0
  145. package/src/components/ui/Pagination/Pagination.native.tsx +157 -0
  146. package/src/components/ui/Progress/Progress.native.spec.tsx +56 -0
  147. package/src/components/ui/Progress/Progress.native.stories.tsx +1 -1
  148. package/src/components/ui/Progress/Progress.native.tsx +2 -2
  149. package/src/components/ui/ProgressCircle/ProgressCircle.native.mdx +63 -0
  150. package/src/components/ui/ProgressCircle/ProgressCircle.native.spec.tsx +39 -0
  151. package/src/components/ui/ProgressCircle/ProgressCircle.native.stories.tsx +36 -0
  152. package/src/components/ui/ProgressCircle/ProgressCircle.native.tsx +176 -0
  153. package/src/components/ui/Separator/Separator.native.spec.tsx +39 -0
  154. package/src/components/ui/Separator/Separator.native.stories.tsx +10 -14
  155. package/src/components/ui/Skeleton/Skeleton.native.spec.tsx +39 -0
  156. package/src/components/ui/Skeleton/Skeleton.native.stories.tsx +6 -8
  157. package/src/components/ui/Skeleton/Skeleton.native.tsx +2 -1
  158. package/src/components/ui/Table/Table.native.mdx +72 -0
  159. package/src/components/ui/Table/Table.native.spec.tsx +83 -0
  160. package/src/components/ui/Table/Table.native.stories.tsx +55 -0
  161. package/src/components/ui/Table/Table.native.tsx +196 -0
  162. package/src/components/ui/Tabs/Tabs.native.mdx +65 -0
  163. package/src/components/ui/Tabs/Tabs.native.spec.tsx +65 -0
  164. package/src/components/ui/Tabs/Tabs.native.stories.tsx +57 -0
  165. package/src/components/ui/Tabs/Tabs.native.tsx +133 -0
  166. package/src/components/ui/Tooltip/Tooltip.native.mdx +47 -0
  167. package/src/components/ui/Tooltip/Tooltip.native.spec.tsx +37 -0
  168. package/src/components/ui/Tooltip/Tooltip.native.stories.tsx +38 -0
  169. package/src/components/ui/Tooltip/Tooltip.native.tsx +57 -0
  170. package/src/components/ui/Typography/Typography.native.spec.tsx +49 -0
  171. package/src/components/ui/Typography/Typography.native.stories.tsx +8 -10
  172. package/src/native/ThemeContext.tsx +3 -3
  173. package/src/native.ts +37 -0
  174. package/src/stories/ReactNative.mdx +48 -13
  175. package/src/utils.ts +6 -0
  176. package/src/web-components/interaction/BdsAccordion.stories.tsx +1 -1
  177. package/src/web-components/interaction/BdsCollapsible.stories.tsx +1 -1
  178. package/src/web-components/interaction/BdsSkipLink.stories.tsx +1 -1
  179. package/src/web-components/interaction/BdsTabs.stories.tsx +1 -1
  180. package/src/web-components/interaction/BdsTooltip.stories.tsx +1 -1
  181. package/src/web-components/interaction/form/BdsCheckbox.stories.tsx +1 -1
  182. package/src/web-components/interaction/form/BdsCheckboxGroup.stories.tsx +1 -1
  183. package/src/web-components/interaction/form/BdsCombobox.stories.tsx +1 -1
  184. package/src/web-components/interaction/form/BdsFileInput.stories.tsx +1 -1
  185. package/src/web-components/interaction/form/BdsFormInput.stories.tsx +1 -1
  186. package/src/web-components/interaction/form/BdsNumberInput.stories.tsx +1 -1
  187. package/src/web-components/interaction/form/BdsRadio.stories.tsx +1 -1
  188. package/src/web-components/interaction/form/BdsRadioGroup.stories.tsx +1 -1
  189. package/src/web-components/interaction/form/BdsSegmentedControl.stories.tsx +1 -1
  190. package/src/web-components/interaction/form/BdsSelect.stories.tsx +1 -1
  191. package/src/web-components/interaction/form/BdsSlider.stories.tsx +1 -1
  192. package/src/web-components/interaction/form/BdsSwitch.stories.tsx +1 -1
  193. package/src/web-components/interaction/form/BdsTextarea.stories.tsx +1 -1
  194. package/src/web-components/ui/BdsButtonGroup.stories.tsx +1 -1
  195. package/src/web-components/ui/BdsCard.stories.tsx +1 -1
  196. package/src/web-components/ui/BdsIconWrapper.stories.tsx +1 -1
  197. package/src/web-components/ui/BdsRating.stories.tsx +1 -1
  198. package/src/web-components/ui/BdsSectionHeader.stories.tsx +1 -1
@@ -0,0 +1,51 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../native/ThemeContext';
3
+ import { SectionHeader } from './SectionHeader.native';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ describe('SectionHeader (native)', () => {
10
+ it('renders the title with header role', () => {
11
+ renderWithTheme(<SectionHeader title="Overview" />);
12
+ expect(screen.getByRole('heading', { name: 'Overview' })).toBeInTheDocument();
13
+ });
14
+
15
+ it('renders the subtitle when provided', () => {
16
+ renderWithTheme(<SectionHeader title="Overview" subtitle="A brief summary" />);
17
+ expect(screen.getByText('A brief summary')).toBeInTheDocument();
18
+ });
19
+
20
+ it('does not render subtitle when not provided', () => {
21
+ renderWithTheme(<SectionHeader title="Overview" />);
22
+ expect(screen.queryByText('A brief summary')).not.toBeInTheDocument();
23
+ });
24
+
25
+ it('aligns content to flex-start by default', () => {
26
+ const { container } = renderWithTheme(<SectionHeader title="Overview" />);
27
+ expect(container.firstChild).toHaveStyle({ alignItems: 'flex-start' });
28
+ });
29
+
30
+ it('aligns content to center when alignment is center', () => {
31
+ const { container } = renderWithTheme(
32
+ <SectionHeader title="Overview" alignment="center" />,
33
+ );
34
+ expect(container.firstChild).toHaveStyle({ alignItems: 'center' });
35
+ });
36
+
37
+ it('aligns content to flex-end when alignment is end', () => {
38
+ const { container } = renderWithTheme(
39
+ <SectionHeader title="Overview" alignment="end" />,
40
+ );
41
+ expect(container.firstChild).toHaveStyle({ alignItems: 'flex-end' });
42
+ });
43
+
44
+ it('renders title and subtitle together', () => {
45
+ renderWithTheme(
46
+ <SectionHeader title="Settings" subtitle="Manage your preferences" />,
47
+ );
48
+ expect(screen.getByRole('heading', { name: 'Settings' })).toBeInTheDocument();
49
+ expect(screen.getByText('Manage your preferences')).toBeInTheDocument();
50
+ });
51
+ });
@@ -7,7 +7,7 @@ const meta = {
7
7
  tags: ['!stable', 'alpha'],
8
8
  title: 'React Native/Layout/SectionHeader',
9
9
  component: SectionHeader,
10
- decorators: [(Story) => <ThemeProvider><Story /></ThemeProvider>],
10
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
11
11
  parameters: { layout: 'padded' },
12
12
  argTypes: {
13
13
  size: { control: 'select', options: ['small', 'medium', 'large'] },
@@ -27,12 +27,10 @@ export const Centered: Story = {
27
27
  };
28
28
  export const AllSizes: Story = {
29
29
  render: () => (
30
- <ThemeProvider>
31
- <div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
32
- <SectionHeader size="large" title="Large Heading" subtitle="Large section subtitle." />
33
- <SectionHeader size="medium" title="Medium Heading" subtitle="Medium section subtitle." />
34
- <SectionHeader size="small" title="Small Heading" subtitle="Small section subtitle." />
35
- </div>
36
- </ThemeProvider>
30
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
31
+ <SectionHeader size="large" title="Large Heading" subtitle="Large section subtitle." />
32
+ <SectionHeader size="medium" title="Medium Heading" subtitle="Medium section subtitle." />
33
+ <SectionHeader size="small" title="Small Heading" subtitle="Small section subtitle." />
34
+ </div>
37
35
  ),
38
36
  };
@@ -0,0 +1,65 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './Accordion.native.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # Accordion (React Native)
7
+
8
+ React Native accordion component for stacked collapsible sections. By default only one section can be open at a time (exclusive mode). Set `allowMultiple` to permit multiple open sections.
9
+
10
+ > **Status: alpha** -- API may change before stable release.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { Accordion } from '@boostdev/components';
16
+
17
+ <Accordion
18
+ items={[
19
+ { id: '1', title: 'Section one', content: <Text>First section content.</Text> },
20
+ { id: '2', title: 'Section two', content: <Text>Second section content.</Text> },
21
+ ]}
22
+ />
23
+ ```
24
+
25
+ ## Examples
26
+
27
+ ### Default
28
+ <Canvas of={Stories.Default} />
29
+
30
+ ### With default open section
31
+ <Canvas of={Stories.WithDefaultOpen} />
32
+
33
+ ### Allow multiple
34
+ <Canvas of={Stories.AllowMultiple} />
35
+
36
+ ### With disabled item
37
+ <Canvas of={Stories.WithDisabledItem} />
38
+
39
+ ## Props
40
+
41
+ <ArgTypes of={Stories} />
42
+
43
+ ## Props reference
44
+
45
+ | Prop | Type | Default | Description |
46
+ |------|------|---------|-------------|
47
+ | `items` | `AccordionItem[]` | -- | Array of accordion sections |
48
+ | `allowMultiple` | `boolean` | `false` | Allow multiple sections open simultaneously |
49
+ | `defaultOpen` | `string[]` | `[]` | IDs of sections open initially |
50
+ | `style` | `StyleProp<ViewStyle>` | -- | Custom style for the container |
51
+
52
+ ### AccordionItem
53
+
54
+ | Prop | Type | Description |
55
+ |------|------|-------------|
56
+ | `id` | `string` | Unique identifier |
57
+ | `title` | `ReactNode` | Header label |
58
+ | `content` | `ReactNode` | Panel content |
59
+ | `disabled` | `boolean` | Prevents toggling |
60
+
61
+ ## Accessibility
62
+
63
+ - Each header has `button` role with `expanded` state
64
+ - Disabled items have `disabled` accessibility state and reduced opacity
65
+ - Chevron indicators are hidden from the accessibility tree
@@ -0,0 +1,69 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../native/ThemeContext';
3
+ import { Accordion } from './Accordion.native';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ const items = [
10
+ { id: '1', title: 'Section 1', content: <span>Content 1</span> },
11
+ { id: '2', title: 'Section 2', content: <span>Content 2</span> },
12
+ { id: '3', title: 'Section 3', content: <span>Content 3</span> },
13
+ ];
14
+
15
+ describe('Accordion (native)', () => {
16
+ it('renders all section headers', () => {
17
+ renderWithTheme(<Accordion items={items} />);
18
+ expect(screen.getByText('Section 1')).toBeInTheDocument();
19
+ expect(screen.getByText('Section 2')).toBeInTheDocument();
20
+ expect(screen.getByText('Section 3')).toBeInTheDocument();
21
+ });
22
+
23
+ it('hides all content by default', () => {
24
+ renderWithTheme(<Accordion items={items} />);
25
+ expect(screen.queryByText('Content 1')).not.toBeInTheDocument();
26
+ expect(screen.queryByText('Content 2')).not.toBeInTheDocument();
27
+ expect(screen.queryByText('Content 3')).not.toBeInTheDocument();
28
+ });
29
+
30
+ it('opens a section with defaultOpen', () => {
31
+ renderWithTheme(<Accordion items={items} defaultOpen={['2']} />);
32
+ expect(screen.queryByText('Content 1')).not.toBeInTheDocument();
33
+ expect(screen.getByText('Content 2')).toBeInTheDocument();
34
+ });
35
+
36
+ it('toggles a section on press (exclusive mode)', () => {
37
+ renderWithTheme(<Accordion items={items} />);
38
+ fireEvent.click(screen.getByText('Section 1'));
39
+ expect(screen.getByText('Content 1')).toBeInTheDocument();
40
+
41
+ fireEvent.click(screen.getByText('Section 2'));
42
+ expect(screen.queryByText('Content 1')).not.toBeInTheDocument();
43
+ expect(screen.getByText('Content 2')).toBeInTheDocument();
44
+ });
45
+
46
+ it('allows multiple open sections when allowMultiple is true', () => {
47
+ renderWithTheme(<Accordion items={items} allowMultiple />);
48
+ fireEvent.click(screen.getByText('Section 1'));
49
+ fireEvent.click(screen.getByText('Section 2'));
50
+ expect(screen.getByText('Content 1')).toBeInTheDocument();
51
+ expect(screen.getByText('Content 2')).toBeInTheDocument();
52
+ });
53
+
54
+ it('does not toggle disabled items', () => {
55
+ const withDisabled = [
56
+ ...items,
57
+ { id: '4', title: 'Disabled', content: <span>Disabled content</span>, disabled: true },
58
+ ];
59
+ renderWithTheme(<Accordion items={withDisabled} />);
60
+ fireEvent.click(screen.getByText('Disabled'));
61
+ expect(screen.queryByText('Disabled content')).not.toBeInTheDocument();
62
+ });
63
+
64
+ it('shows content of open sections and has button role on headers', () => {
65
+ renderWithTheme(<Accordion items={items} defaultOpen={['1']} />);
66
+ expect(screen.getByText('Content 1')).toBeInTheDocument();
67
+ expect(screen.getByText('Section 1')).toBeInTheDocument();
68
+ });
69
+ });
@@ -0,0 +1,52 @@
1
+ import React from 'react';
2
+ import { Text } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react';
4
+ import { ThemeProvider } from '../../../native/ThemeContext';
5
+ import { Accordion } from './Accordion.native';
6
+
7
+ const meta = {
8
+ tags: ['!stable', 'alpha'],
9
+ title: 'React Native/UI/Accordion',
10
+ component: Accordion,
11
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
12
+ parameters: { layout: 'padded' },
13
+ } satisfies Meta<typeof Accordion>;
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ const sampleItems = [
19
+ { id: '1', title: 'What is this library?', content: <Text>A design system component library built for React, React Native, and Web Components.</Text> },
20
+ { id: '2', title: 'How do I install it?', content: <Text>Run pnpm add @boostdev/components to add it to your project.</Text> },
21
+ { id: '3', title: 'Is it production ready?', content: <Text>The React components are stable. React Native components are in alpha.</Text> },
22
+ ];
23
+
24
+ export const Default: Story = {
25
+ args: {
26
+ items: sampleItems,
27
+ },
28
+ };
29
+
30
+ export const WithDefaultOpen: Story = {
31
+ args: {
32
+ items: sampleItems,
33
+ defaultOpen: ['1'],
34
+ },
35
+ };
36
+
37
+ export const AllowMultiple: Story = {
38
+ args: {
39
+ items: sampleItems,
40
+ allowMultiple: true,
41
+ defaultOpen: ['1', '2'],
42
+ },
43
+ };
44
+
45
+ export const WithDisabledItem: Story = {
46
+ args: {
47
+ items: [
48
+ ...sampleItems,
49
+ { id: '4', title: 'Disabled section', content: <Text>You cannot see this.</Text>, disabled: true },
50
+ ],
51
+ },
52
+ };
@@ -0,0 +1,141 @@
1
+ import { ReactNode, useState } from 'react';
2
+ import {
3
+ StyleSheet,
4
+ View,
5
+ Text,
6
+ Pressable,
7
+ LayoutAnimation,
8
+ Platform,
9
+ UIManager,
10
+ StyleProp,
11
+ ViewStyle,
12
+ } from 'react-native';
13
+ import { spacing, font, border, lineHeight } from '../../../native/tokens';
14
+ import { useTheme } from '../../../native/ThemeContext';
15
+
16
+ if (
17
+ Platform.OS === 'android' &&
18
+ UIManager.setLayoutAnimationEnabledExperimental
19
+ ) {
20
+ UIManager.setLayoutAnimationEnabledExperimental(true);
21
+ }
22
+
23
+ export interface AccordionItem {
24
+ id: string;
25
+ title: ReactNode;
26
+ content: ReactNode;
27
+ disabled?: boolean;
28
+ }
29
+
30
+ interface AccordionProps {
31
+ items: AccordionItem[];
32
+ allowMultiple?: boolean;
33
+ defaultOpen?: string[];
34
+ style?: StyleProp<ViewStyle>;
35
+ }
36
+
37
+ const styles = StyleSheet.create({
38
+ container: {
39
+ overflow: 'hidden',
40
+ borderRadius: border.radius.s,
41
+ },
42
+ item: {
43
+ borderBottomWidth: 1,
44
+ },
45
+ header: {
46
+ flexDirection: 'row',
47
+ alignItems: 'center',
48
+ justifyContent: 'space-between',
49
+ minHeight: 44,
50
+ paddingVertical: spacing.s,
51
+ paddingHorizontal: spacing.m,
52
+ },
53
+ titleText: {
54
+ flex: 1,
55
+ fontSize: font.size.body,
56
+ fontFamily: font.family.body,
57
+ fontWeight: font.weight.semibold,
58
+ lineHeight: lineHeight.body,
59
+ },
60
+ chevron: {
61
+ fontSize: font.size.bodyS,
62
+ marginStart: spacing.xs,
63
+ },
64
+ panel: {
65
+ paddingHorizontal: spacing.m,
66
+ paddingBottom: spacing.m,
67
+ },
68
+ pressed: {
69
+ opacity: 0.75,
70
+ },
71
+ disabled: {
72
+ opacity: 0.4,
73
+ },
74
+ });
75
+
76
+ export function Accordion({
77
+ items,
78
+ allowMultiple = false,
79
+ defaultOpen = [],
80
+ style,
81
+ }: Readonly<AccordionProps>) {
82
+ const { colors } = useTheme();
83
+ const [openIds, setOpenIds] = useState<string[]>(defaultOpen);
84
+
85
+ const toggle = (id: string) => {
86
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
87
+ setOpenIds(prev => {
88
+ const isOpen = prev.includes(id);
89
+ if (isOpen) return prev.filter(i => i !== id);
90
+ return allowMultiple ? [...prev, id] : [id];
91
+ });
92
+ };
93
+
94
+ return (
95
+ <View style={[styles.container, style]}>
96
+ {items.map((item, index) => {
97
+ const isOpen = openIds.includes(item.id);
98
+ const isLast = index === items.length - 1;
99
+
100
+ return (
101
+ <View
102
+ key={item.id}
103
+ style={[
104
+ styles.item,
105
+ { borderBottomColor: colors.colorBgSubtle },
106
+ isLast && { borderBottomWidth: 0 },
107
+ ]}
108
+ >
109
+ <Pressable
110
+ onPress={() => toggle(item.id)}
111
+ disabled={item.disabled}
112
+ accessibilityRole="button"
113
+ accessibilityState={{ expanded: isOpen, disabled: item.disabled }}
114
+ style={({ pressed }) => [
115
+ styles.header,
116
+ item.disabled && styles.disabled,
117
+ pressed && !item.disabled && styles.pressed,
118
+ ]}
119
+ >
120
+ <Text style={[styles.titleText, { color: colors.colorOnBg }]}>
121
+ {typeof item.title === 'string' ? item.title : item.title}
122
+ </Text>
123
+ <Text
124
+ style={[styles.chevron, { color: colors.colorOnBgSubtle }]}
125
+ importantForAccessibility="no"
126
+ accessibilityElementsHidden={true}
127
+ >
128
+ {isOpen ? '▾' : '▸'}
129
+ </Text>
130
+ </Pressable>
131
+ {isOpen && (
132
+ <View style={styles.panel} accessibilityRole="summary">
133
+ {item.content}
134
+ </View>
135
+ )}
136
+ </View>
137
+ );
138
+ })}
139
+ </View>
140
+ );
141
+ }
@@ -0,0 +1,76 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { Text } from 'react-native';
3
+ import { ThemeProvider } from '../../../native/ThemeContext';
4
+ import { Alert } from './Alert.native';
5
+
6
+ function renderWithTheme(ui: React.ReactElement) {
7
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
8
+ }
9
+
10
+ describe('Alert (native)', () => {
11
+ it('renders children text', () => {
12
+ renderWithTheme(<Alert>Something happened</Alert>);
13
+ expect(screen.getByText('Something happened')).toBeInTheDocument();
14
+ });
15
+
16
+ it('renders the title when provided', () => {
17
+ renderWithTheme(<Alert title="Heads up">Details here</Alert>);
18
+ expect(screen.getByText('Heads up')).toBeInTheDocument();
19
+ expect(screen.getByText('Details here')).toBeInTheDocument();
20
+ });
21
+
22
+ it('does not render a title element when title is omitted', () => {
23
+ renderWithTheme(<Alert>No title alert</Alert>);
24
+ expect(screen.getByText('No title alert')).toBeInTheDocument();
25
+ });
26
+
27
+ it('renders an icon when provided', () => {
28
+ renderWithTheme(
29
+ <Alert icon={<Text>!</Text>}>With icon</Alert>,
30
+ );
31
+ expect(screen.getByText('!')).toBeInTheDocument();
32
+ expect(screen.getByText('With icon')).toBeInTheDocument();
33
+ });
34
+
35
+ it('has alert role for error variant', () => {
36
+ renderWithTheme(<Alert variant="error">Error message</Alert>);
37
+ expect(screen.getByRole('alert')).toBeInTheDocument();
38
+ });
39
+
40
+ it('has alert role for warning variant', () => {
41
+ renderWithTheme(<Alert variant="warning">Warning message</Alert>);
42
+ expect(screen.getByRole('alert')).toBeInTheDocument();
43
+ });
44
+
45
+ it('does not have alert role for info variant', () => {
46
+ renderWithTheme(<Alert variant="info">Info message</Alert>);
47
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
48
+ });
49
+
50
+ it('does not have alert role for success variant', () => {
51
+ renderWithTheme(<Alert variant="success">Success message</Alert>);
52
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
53
+ });
54
+
55
+ it('renders a dismiss button when onDismiss is provided', () => {
56
+ renderWithTheme(<Alert onDismiss={() => {}}>Dismissable</Alert>);
57
+ expect(screen.getByRole('button', { name: 'Dismiss alert' })).toBeInTheDocument();
58
+ });
59
+
60
+ it('calls onDismiss when dismiss button is pressed', () => {
61
+ const onDismiss = vi.fn();
62
+ renderWithTheme(<Alert onDismiss={onDismiss}>Dismissable</Alert>);
63
+ fireEvent.click(screen.getByRole('button', { name: 'Dismiss alert' }));
64
+ expect(onDismiss).toHaveBeenCalledTimes(1);
65
+ });
66
+
67
+ it('does not render a dismiss button when onDismiss is omitted', () => {
68
+ renderWithTheme(<Alert>No dismiss</Alert>);
69
+ expect(screen.queryByRole('button', { name: 'Dismiss alert' })).not.toBeInTheDocument();
70
+ });
71
+
72
+ it('accepts custom style', () => {
73
+ renderWithTheme(<Alert style={{ marginTop: 10 }}>Styled</Alert>);
74
+ expect(screen.getByText('Styled')).toBeInTheDocument();
75
+ });
76
+ });
@@ -7,7 +7,7 @@ const meta = {
7
7
  tags: ['!stable', 'alpha'],
8
8
  title: 'React Native/UI/Alert',
9
9
  component: Alert,
10
- decorators: [(Story) => <ThemeProvider><Story /></ThemeProvider>],
10
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
11
11
  parameters: { layout: 'padded' },
12
12
  argTypes: {
13
13
  variant: { control: 'select', options: ['info', 'success', 'warning', 'error'] },
@@ -30,14 +30,12 @@ export const Dismissible: Story = {
30
30
  export const AllVariants: Story = {
31
31
  args: { children: '' },
32
32
  render: () => (
33
- <ThemeProvider>
34
- <div style={{ display: 'flex', flexDirection: 'column', gap: '12px', maxWidth: 400 }}>
35
- {(['info', 'success', 'warning', 'error'] as const).map(v => (
36
- <Alert key={v} variant={v} title={v.charAt(0).toUpperCase() + v.slice(1)}>
37
- This is a {v} alert.
38
- </Alert>
39
- ))}
40
- </div>
41
- </ThemeProvider>
33
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px', maxWidth: 400 }}>
34
+ {(['info', 'success', 'warning', 'error'] as const).map(v => (
35
+ <Alert key={v} variant={v} title={v.charAt(0).toUpperCase() + v.slice(1)}>
36
+ This is a {v} alert.
37
+ </Alert>
38
+ ))}
39
+ </div>
42
40
  ),
43
41
  };
@@ -58,20 +58,20 @@ export function Alert({
58
58
  const { colors } = useTheme();
59
59
  const isUrgent = variant === 'error' || variant === 'warning';
60
60
 
61
- const variantColors: Record<AlertVariant, { bg: string; text: string }> = {
62
- info: { bg: colors.colorBlueSubtle, text: colors.colorOnBlueSubtle },
63
- success: { bg: colors.colorSuccessSubtle, text: colors.colorOnSuccessSubtle },
64
- warning: { bg: colors.colorOrangeSubtle, text: colors.colorOnOrangeSubtle },
65
- error: { bg: colors.colorRedSubtle, text: colors.colorOnRedSubtle },
61
+ const variantColors: Record<AlertVariant, { bg: string; text: string; border: string }> = {
62
+ info: { bg: colors.colorBlueSubtle, text: colors.colorOnBlueSubtle, border: colors.colorBlue },
63
+ success: { bg: colors.colorSuccessSubtle, text: colors.colorOnSuccessSubtle, border: colors.colorSuccess },
64
+ warning: { bg: colors.colorOrangeSubtle, text: colors.colorOnOrangeSubtle, border: colors.colorWarning },
65
+ error: { bg: colors.colorRedSubtle, text: colors.colorOnBg, border: colors.colorError },
66
66
  };
67
67
 
68
- const { bg, text } = variantColors[variant];
68
+ const { bg, text, border: borderColor } = variantColors[variant];
69
69
 
70
70
  return (
71
71
  <View
72
72
  accessibilityRole={isUrgent ? 'alert' : 'none'}
73
73
  accessibilityLiveRegion={isUrgent ? 'assertive' : 'polite'}
74
- style={[styles.alert, { backgroundColor: bg }, style]}
74
+ style={[styles.alert, { backgroundColor: bg, borderStartWidth: spacing.s, borderStartColor: borderColor }, style]}
75
75
  >
76
76
  {icon && <View style={styles.icon}>{icon}</View>}
77
77
  <View style={styles.content}>
@@ -7,7 +7,7 @@ const meta = {
7
7
  tags: ['!stable', 'alpha'],
8
8
  title: 'React Native/UI/Avatar',
9
9
  component: Avatar,
10
- decorators: [(Story) => <ThemeProvider><Story /></ThemeProvider>],
10
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
11
11
  parameters: { layout: 'centered' },
12
12
  argTypes: {
13
13
  size: { control: 'select', options: ['small', 'medium', 'large'] },
@@ -23,12 +23,10 @@ export const Large: Story = { args: { name: 'Alex Brown', size: 'large' } };
23
23
  export const SingleName: Story = { args: { name: 'Alice', size: 'medium' } };
24
24
  export const AllSizes: Story = {
25
25
  render: () => (
26
- <ThemeProvider>
27
- <div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
28
- {(['small', 'medium', 'large'] as const).map(s => (
29
- <Avatar key={s} name="John Doe" size={s} />
30
- ))}
31
- </div>
32
- </ThemeProvider>
26
+ <div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
27
+ {(['small', 'medium', 'large'] as const).map(s => (
28
+ <Avatar key={s} name="John Doe" size={s} />
29
+ ))}
30
+ </div>
33
31
  ),
34
32
  };
@@ -1,5 +1,5 @@
1
1
  import { StyleSheet, View, Text, Image, StyleProp, ViewStyle } from 'react-native';
2
- import { font } from '../../../native/tokens';
2
+ import { font, border } from '../../../native/tokens';
3
3
  import { useTheme } from '../../../native/ThemeContext';
4
4
 
5
5
  type AvatarSize = 'small' | 'medium' | 'large';
@@ -29,7 +29,7 @@ function getInitials(name: string): string {
29
29
 
30
30
  const styles = StyleSheet.create({
31
31
  container: {
32
- borderRadius: 999,
32
+ borderRadius: border.radius.full,
33
33
  overflow: 'hidden',
34
34
  alignItems: 'center',
35
35
  justifyContent: 'center',
@@ -0,0 +1,45 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../native/ThemeContext';
3
+ import { Badge } from './Badge.native';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ describe('Badge (native)', () => {
10
+ it('renders children text', () => {
11
+ renderWithTheme(<Badge>New</Badge>);
12
+ expect(screen.getByText('New')).toBeInTheDocument();
13
+ });
14
+
15
+ it('defaults to primary variant', () => {
16
+ const { container } = renderWithTheme(<Badge>Default</Badge>);
17
+ expect(screen.getByText('Default')).toBeInTheDocument();
18
+ expect(container).toBeTruthy();
19
+ });
20
+
21
+ it('renders with secondary variant', () => {
22
+ renderWithTheme(<Badge variant="secondary">Info</Badge>);
23
+ expect(screen.getByText('Info')).toBeInTheDocument();
24
+ });
25
+
26
+ it('renders with success variant', () => {
27
+ renderWithTheme(<Badge variant="success">Active</Badge>);
28
+ expect(screen.getByText('Active')).toBeInTheDocument();
29
+ });
30
+
31
+ it('renders with error variant', () => {
32
+ renderWithTheme(<Badge variant="error">Failed</Badge>);
33
+ expect(screen.getByText('Failed')).toBeInTheDocument();
34
+ });
35
+
36
+ it('renders with warning variant', () => {
37
+ renderWithTheme(<Badge variant="warning">Pending</Badge>);
38
+ expect(screen.getByText('Pending')).toBeInTheDocument();
39
+ });
40
+
41
+ it('accepts custom style', () => {
42
+ renderWithTheme(<Badge style={{ marginTop: 10 }}>Styled</Badge>);
43
+ expect(screen.getByText('Styled')).toBeInTheDocument();
44
+ });
45
+ });
@@ -7,7 +7,7 @@ const meta = {
7
7
  tags: ['!stable', 'alpha'],
8
8
  title: 'React Native/UI/Badge',
9
9
  component: Badge,
10
- decorators: [(Story) => <ThemeProvider><Story /></ThemeProvider>],
10
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
11
11
  parameters: { layout: 'centered' },
12
12
  argTypes: {
13
13
  variant: { control: 'select', options: ['primary', 'secondary', 'success', 'error', 'warning'] },
@@ -25,12 +25,10 @@ export const Warning: Story = { args: { children: 'Warning', variant: 'warning'
25
25
  export const AllVariants: Story = {
26
26
  args: { children: '' },
27
27
  render: () => (
28
- <ThemeProvider>
29
- <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
30
- {(['primary', 'secondary', 'success', 'error', 'warning'] as const).map(v => (
31
- <Badge key={v} variant={v}>{v}</Badge>
32
- ))}
33
- </div>
34
- </ThemeProvider>
28
+ <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
29
+ {(['primary', 'secondary', 'success', 'error', 'warning'] as const).map(v => (
30
+ <Badge key={v} variant={v}>{v}</Badge>
31
+ ))}
32
+ </div>
35
33
  ),
36
34
  };