@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,53 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './Collapsible.native.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # Collapsible (React Native)
7
+
8
+ React Native collapsible section with an animated chevron and smooth expand/collapse transitions. Supports both controlled and uncontrolled modes.
9
+
10
+ > **Status: alpha** -- API may change before stable release.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { Collapsible } from '@boostdev/components';
16
+
17
+ <Collapsible summary="More details">
18
+ <Text>Hidden content revealed on press.</Text>
19
+ </Collapsible>
20
+ ```
21
+
22
+ ## Examples
23
+
24
+ ### Default (collapsed)
25
+ <Canvas of={Stories.Default} />
26
+
27
+ ### Default open
28
+ <Canvas of={Stories.DefaultOpen} />
29
+
30
+ ### Multiple sections
31
+ <Canvas of={Stories.Multiple} />
32
+
33
+ ## Props
34
+
35
+ <ArgTypes of={Stories} />
36
+
37
+ ## Props reference
38
+
39
+ | Prop | Type | Default | Description |
40
+ |------|------|---------|-------------|
41
+ | `summary` | `string` | -- | The trigger label, always visible |
42
+ | `children` | `ReactNode` | -- | Content revealed when expanded |
43
+ | `open` | `boolean` | -- | Controlled open state |
44
+ | `defaultOpen` | `boolean` | `false` | Uncontrolled initial open state |
45
+ | `onToggle` | `(open: boolean) => void` | -- | Called after each toggle with the new open state |
46
+ | `name` | `string` | -- | Grouping name (informational in native) |
47
+ | `style` | `StyleProp<ViewStyle>` | -- | Custom style for the container |
48
+
49
+ ## Accessibility
50
+
51
+ - Header has `button` role with `expanded` state
52
+ - Chevron icon is hidden from the accessibility tree
53
+ - Content appears/disappears with LayoutAnimation for smooth transitions
@@ -0,0 +1,68 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../native/ThemeContext';
3
+ import { Collapsible } from './Collapsible.native';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ describe('Collapsible (native)', () => {
10
+ it('renders the summary text', () => {
11
+ renderWithTheme(
12
+ <Collapsible summary="Details">
13
+ <span>Content</span>
14
+ </Collapsible>,
15
+ );
16
+ expect(screen.getByText('Details')).toBeInTheDocument();
17
+ });
18
+
19
+ it('hides content by default', () => {
20
+ renderWithTheme(
21
+ <Collapsible summary="Details">
22
+ <span>Hidden content</span>
23
+ </Collapsible>,
24
+ );
25
+ expect(screen.queryByText('Hidden content')).not.toBeInTheDocument();
26
+ });
27
+
28
+ it('shows content when defaultOpen is true', () => {
29
+ renderWithTheme(
30
+ <Collapsible summary="Details" defaultOpen>
31
+ <span>Visible content</span>
32
+ </Collapsible>,
33
+ );
34
+ expect(screen.getByText('Visible content')).toBeInTheDocument();
35
+ });
36
+
37
+ it('toggles content on press', () => {
38
+ renderWithTheme(
39
+ <Collapsible summary="Toggle me">
40
+ <span>Toggled content</span>
41
+ </Collapsible>,
42
+ );
43
+ expect(screen.queryByText('Toggled content')).not.toBeInTheDocument();
44
+ fireEvent.click(screen.getByRole('button', { name: 'Toggle me' }));
45
+ expect(screen.getByText('Toggled content')).toBeInTheDocument();
46
+ });
47
+
48
+ it('shows content when defaultOpen is set and has button role', () => {
49
+ renderWithTheme(
50
+ <Collapsible summary="Expandable" defaultOpen>
51
+ <span>Content</span>
52
+ </Collapsible>,
53
+ );
54
+ expect(screen.getByRole('button', { name: 'Expandable' })).toBeInTheDocument();
55
+ expect(screen.getByText('Content')).toBeInTheDocument();
56
+ });
57
+
58
+ it('calls onToggle with the new open state', () => {
59
+ const onToggle = vi.fn();
60
+ renderWithTheme(
61
+ <Collapsible summary="Toggle" onToggle={onToggle}>
62
+ <span>Content</span>
63
+ </Collapsible>,
64
+ );
65
+ fireEvent.click(screen.getByRole('button', { name: 'Toggle' }));
66
+ expect(onToggle).toHaveBeenCalledWith(true);
67
+ });
68
+ });
@@ -0,0 +1,48 @@
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 { Collapsible } from './Collapsible.native';
6
+
7
+ const meta = {
8
+ tags: ['!stable', 'alpha'],
9
+ title: 'React Native/UI/Collapsible',
10
+ component: Collapsible,
11
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
12
+ parameters: { layout: 'padded' },
13
+ } satisfies Meta<typeof Collapsible>;
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ export const Default: Story = {
19
+ args: {
20
+ summary: 'Click to expand',
21
+ children: <Text>This is the collapsible content that is revealed when expanded.</Text>,
22
+ },
23
+ };
24
+
25
+ export const DefaultOpen: Story = {
26
+ args: {
27
+ summary: 'Already open',
28
+ defaultOpen: true,
29
+ children: <Text>This content is visible by default.</Text>,
30
+ },
31
+ };
32
+
33
+ export const Multiple: Story = {
34
+ args: { summary: '', children: null },
35
+ render: () => (
36
+ <>
37
+ <Collapsible summary="Section 1">
38
+ <Text>Content for section 1.</Text>
39
+ </Collapsible>
40
+ <Collapsible summary="Section 2">
41
+ <Text>Content for section 2.</Text>
42
+ </Collapsible>
43
+ <Collapsible summary="Section 3" defaultOpen>
44
+ <Text>Content for section 3 (open by default).</Text>
45
+ </Collapsible>
46
+ </>
47
+ ),
48
+ };
@@ -0,0 +1,139 @@
1
+ import { ReactNode, useCallback, useRef, useState } from 'react';
2
+ import {
3
+ StyleSheet,
4
+ View,
5
+ Text,
6
+ Pressable,
7
+ Animated,
8
+ LayoutAnimation,
9
+ Platform,
10
+ UIManager,
11
+ StyleProp,
12
+ ViewStyle,
13
+ } from 'react-native';
14
+ import { spacing, font, border, lineHeight } from '../../../native/tokens';
15
+ import { useTheme } from '../../../native/ThemeContext';
16
+
17
+ if (
18
+ Platform.OS === 'android' &&
19
+ UIManager.setLayoutAnimationEnabledExperimental
20
+ ) {
21
+ UIManager.setLayoutAnimationEnabledExperimental(true);
22
+ }
23
+
24
+ export interface CollapsibleProps {
25
+ /** The trigger label -- always visible */
26
+ summary: string;
27
+ /** The content revealed when expanded */
28
+ children: ReactNode;
29
+ /** Controlled open state */
30
+ open?: boolean;
31
+ /** Uncontrolled initial open state */
32
+ defaultOpen?: boolean;
33
+ /** Called after each toggle with the new open state */
34
+ onToggle?: (open: boolean) => void;
35
+ /** Grouping name (informational in native) */
36
+ name?: string;
37
+ style?: StyleProp<ViewStyle>;
38
+ }
39
+
40
+ const styles = StyleSheet.create({
41
+ container: {
42
+ borderRadius: border.radius.m,
43
+ overflow: 'hidden',
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
+ summaryText: {
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.body,
62
+ marginStart: spacing.xs,
63
+ },
64
+ content: {
65
+ paddingHorizontal: spacing.m,
66
+ paddingBottom: spacing.m,
67
+ },
68
+ pressed: {
69
+ opacity: 0.75,
70
+ },
71
+ });
72
+
73
+ export function Collapsible({
74
+ summary,
75
+ children,
76
+ open: controlledOpen,
77
+ defaultOpen = false,
78
+ onToggle,
79
+ name: _name, // eslint-disable-line @typescript-eslint/no-unused-vars
80
+ style,
81
+ }: Readonly<CollapsibleProps>) {
82
+ const { colors } = useTheme();
83
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
84
+ const isControlled = controlledOpen !== undefined;
85
+ const isOpen = isControlled ? controlledOpen : internalOpen;
86
+
87
+ const rotation = useRef(new Animated.Value(isOpen ? 1 : 0)).current;
88
+
89
+ const toggle = useCallback(() => {
90
+ const next = !isOpen;
91
+
92
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
93
+
94
+ Animated.timing(rotation, {
95
+ toValue: next ? 1 : 0,
96
+ duration: 200,
97
+ useNativeDriver: true,
98
+ }).start();
99
+
100
+ if (!isControlled) {
101
+ setInternalOpen(next);
102
+ }
103
+ onToggle?.(next);
104
+ }, [isOpen, isControlled, onToggle, rotation]);
105
+
106
+ const rotateZ = rotation.interpolate({
107
+ inputRange: [0, 1],
108
+ outputRange: ['0deg', '90deg'],
109
+ });
110
+
111
+ return (
112
+ <View
113
+ style={[styles.container, { borderBottomWidth: 1, borderBottomColor: colors.colorBgSubtle }, style]}
114
+ accessibilityRole="summary"
115
+ >
116
+ <Pressable
117
+ onPress={toggle}
118
+ accessibilityRole="button"
119
+ accessibilityLabel={summary}
120
+ accessibilityState={{ expanded: isOpen }}
121
+ style={({ pressed }) => [styles.header, pressed && styles.pressed]}
122
+ >
123
+ <Text style={[styles.summaryText, { color: colors.colorOnBg }]}>{summary}</Text>
124
+ <Animated.Text
125
+ style={[styles.chevron, { color: colors.colorOnBgSubtle, transform: [{ rotateZ }] }]}
126
+ importantForAccessibility="no"
127
+ accessibilityElementsHidden={true}
128
+ >
129
+
130
+ </Animated.Text>
131
+ </Pressable>
132
+ {isOpen && (
133
+ <View style={styles.content}>
134
+ {children}
135
+ </View>
136
+ )}
137
+ </View>
138
+ );
139
+ }
@@ -0,0 +1,50 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './DescriptionList.native.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # DescriptionList (React Native)
7
+
8
+ React Native description list for displaying key-value pairs. Each item has a bold term and a description.
9
+
10
+ > **Status: alpha** — API may change before stable release.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { DescriptionList } from '@boostdev/components';
16
+
17
+ <DescriptionList
18
+ items={[
19
+ { term: 'Name', description: 'Jane Doe' },
20
+ { term: 'Email', description: 'jane@example.com' },
21
+ ]}
22
+ />
23
+ ```
24
+
25
+ ## Examples
26
+
27
+ ### Default
28
+ <Canvas of={Stories.Default} />
29
+
30
+ ### Two items
31
+ <Canvas of={Stories.TwoItems} />
32
+
33
+ ### Single item
34
+ <Canvas of={Stories.SingleItem} />
35
+
36
+ ## Props
37
+
38
+ <ArgTypes of={Stories} />
39
+
40
+ ## Props reference
41
+
42
+ | Prop | Type | Default | Description |
43
+ |------|------|---------|-------------|
44
+ | `items` | `Array<{ term: string, description: ReactNode }>` | — | List of term-description pairs |
45
+ | `style` | `StyleProp<ViewStyle>` | — | Override container styles |
46
+
47
+ ## Accessibility
48
+
49
+ - Container uses `accessibilityRole="list"`, each group uses `accessibilityRole="listitem"`
50
+ - Terms render as bold text for visual distinction
@@ -0,0 +1,37 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../native/ThemeContext';
3
+ import { DescriptionList } from './DescriptionList.native';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ describe('DescriptionList (native)', () => {
10
+ const items = [
11
+ { term: 'Name', description: 'Jane Doe' },
12
+ { term: 'Email', description: 'jane@example.com' },
13
+ ];
14
+
15
+ it('renders all term texts', () => {
16
+ renderWithTheme(<DescriptionList items={items} />);
17
+ expect(screen.getByText('Name')).toBeInTheDocument();
18
+ expect(screen.getByText('Email')).toBeInTheDocument();
19
+ });
20
+
21
+ it('renders all description texts', () => {
22
+ renderWithTheme(<DescriptionList items={items} />);
23
+ expect(screen.getByText('Jane Doe')).toBeInTheDocument();
24
+ expect(screen.getByText('jane@example.com')).toBeInTheDocument();
25
+ });
26
+
27
+ it('has list role on the container', () => {
28
+ renderWithTheme(<DescriptionList items={items} />);
29
+ expect(screen.getByRole('list')).toBeInTheDocument();
30
+ });
31
+
32
+ it('renders a single item', () => {
33
+ renderWithTheme(<DescriptionList items={[{ term: 'Version', description: '1.0' }]} />);
34
+ expect(screen.getByText('Version')).toBeInTheDocument();
35
+ expect(screen.getByText('1.0')).toBeInTheDocument();
36
+ });
37
+ });
@@ -0,0 +1,48 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { ThemeProvider } from '../../../native/ThemeContext';
4
+ import { DescriptionList } from './DescriptionList.native';
5
+
6
+ const meta = {
7
+ tags: ['!stable', 'alpha'],
8
+ title: 'React Native/UI/DescriptionList',
9
+ component: DescriptionList,
10
+ decorators: [
11
+ (Story) => (
12
+ <ThemeProvider colorScheme="light">
13
+ <div style={{ width: 300 }}>
14
+ <Story />
15
+ </div>
16
+ </ThemeProvider>
17
+ ),
18
+ ],
19
+ parameters: { layout: 'padded' },
20
+ } satisfies Meta<typeof DescriptionList>;
21
+
22
+ export default meta;
23
+ type Story = StoryObj<typeof meta>;
24
+
25
+ export const Default: Story = {
26
+ args: {
27
+ items: [
28
+ { term: 'Name', description: 'Jane Doe' },
29
+ { term: 'Email', description: 'jane@example.com' },
30
+ { term: 'Role', description: 'Administrator' },
31
+ ],
32
+ },
33
+ };
34
+
35
+ export const TwoItems: Story = {
36
+ args: {
37
+ items: [
38
+ { term: 'Status', description: 'Active' },
39
+ { term: 'Plan', description: 'Enterprise' },
40
+ ],
41
+ },
42
+ };
43
+
44
+ export const SingleItem: Story = {
45
+ args: {
46
+ items: [{ term: 'Version', description: '1.0.0' }],
47
+ },
48
+ };
@@ -0,0 +1,54 @@
1
+ import { ReactNode } from 'react';
2
+ import { AccessibilityRole, StyleSheet, View, Text, StyleProp, ViewStyle } from 'react-native';
3
+ import { spacing, font, lineHeight } from '../../../native/tokens';
4
+ import { useTheme } from '../../../native/ThemeContext';
5
+
6
+ export interface DescriptionItem {
7
+ term: string;
8
+ description: ReactNode;
9
+ }
10
+
11
+ interface DescriptionListProps {
12
+ items: DescriptionItem[];
13
+ style?: StyleProp<ViewStyle>;
14
+ }
15
+
16
+ const styles = StyleSheet.create({
17
+ list: {
18
+ gap: spacing.m,
19
+ },
20
+ group: {
21
+ gap: spacing.xxs,
22
+ },
23
+ term: {
24
+ fontSize: font.size.bodyS,
25
+ fontFamily: font.family.body,
26
+ fontWeight: font.weight.semibold,
27
+ lineHeight: lineHeight.bodyS,
28
+ },
29
+ description: {
30
+ fontSize: font.size.body,
31
+ fontFamily: font.family.body,
32
+ fontWeight: font.weight.body,
33
+ lineHeight: lineHeight.body,
34
+ },
35
+ });
36
+
37
+ export function DescriptionList({ items, style }: Readonly<DescriptionListProps>) {
38
+ const { colors } = useTheme();
39
+
40
+ return (
41
+ <View style={[styles.list, style]} accessibilityRole="list">
42
+ {items.map((item, i) => (
43
+ <View key={i} style={styles.group} accessibilityRole={'listitem' as AccessibilityRole}>
44
+ <Text style={[styles.term, { color: colors.colorOnBgSubtle }]}>{item.term}</Text>
45
+ {typeof item.description === 'string' ? (
46
+ <Text style={[styles.description, { color: colors.colorOnBg }]}>{item.description}</Text>
47
+ ) : (
48
+ <View>{item.description}</View>
49
+ )}
50
+ </View>
51
+ ))}
52
+ </View>
53
+ );
54
+ }
@@ -0,0 +1,53 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './Link.native.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # Link (React Native)
7
+
8
+ React Native link component using `Pressable` + `Text`. Opens URLs via `Linking.openURL` or calls a custom `onPress` handler.
9
+
10
+ > **Status: alpha** — API may change before stable release.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { Link } from '@boostdev/components';
16
+
17
+ <Link href="https://example.com">Visit website</Link>
18
+ <Link onPress={() => navigation.navigate('Settings')}>Settings</Link>
19
+ ```
20
+
21
+ ## Examples
22
+
23
+ ### Default
24
+ <Canvas of={Stories.Default} />
25
+
26
+ ### With onPress callback
27
+ <Canvas of={Stories.WithOnPress} />
28
+
29
+ ### With accessibility label
30
+ <Canvas of={Stories.WithAccessibilityLabel} />
31
+
32
+ ### All variants
33
+ <Canvas of={Stories.AllVariants} />
34
+
35
+ ## Props
36
+
37
+ <ArgTypes of={Stories} />
38
+
39
+ ## Props reference
40
+
41
+ | Prop | Type | Default | Description |
42
+ |------|------|---------|-------------|
43
+ | `href` | `string` | — | URL to open via `Linking.openURL` |
44
+ | `children` | `ReactNode` | — | Link label |
45
+ | `onPress` | `() => void` | — | Custom press handler (used when `href` is not provided) |
46
+ | `accessibilityLabel` | `string` | — | Screen reader label |
47
+ | `style` | `StyleProp<TextStyle>` | — | Override text styles |
48
+
49
+ ## Accessibility
50
+
51
+ - Uses `accessibilityRole="link"` for assistive technologies
52
+ - Provide `accessibilityLabel` when the visible text is not self-descriptive
53
+ - Pressed state reduces opacity for visual feedback
@@ -0,0 +1,35 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../native/ThemeContext';
3
+ import { Link } from './Link.native';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ describe('Link (native)', () => {
10
+ it('renders children text', () => {
11
+ renderWithTheme(<Link href="https://example.com">Visit</Link>);
12
+ expect(screen.getByText('Visit')).toBeInTheDocument();
13
+ });
14
+
15
+ it('has link role', () => {
16
+ renderWithTheme(<Link href="https://example.com">Visit</Link>);
17
+ expect(screen.getByRole('link')).toBeInTheDocument();
18
+ });
19
+
20
+ it('calls onPress when provided without href', () => {
21
+ const onPress = vi.fn();
22
+ renderWithTheme(<Link onPress={onPress}>Navigate</Link>);
23
+ fireEvent.click(screen.getByRole('link'));
24
+ expect(onPress).toHaveBeenCalledTimes(1);
25
+ });
26
+
27
+ it('uses accessibilityLabel when provided', () => {
28
+ renderWithTheme(
29
+ <Link href="https://example.com" accessibilityLabel="Read more about products">
30
+ Read more
31
+ </Link>,
32
+ );
33
+ expect(screen.getByLabelText('Read more about products')).toBeInTheDocument();
34
+ });
35
+ });
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { ThemeProvider } from '../../../native/ThemeContext';
4
+ import { Link } from './Link.native';
5
+
6
+ const meta = {
7
+ tags: ['!stable', 'alpha'],
8
+ title: 'React Native/UI/Link',
9
+ component: Link,
10
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
11
+ parameters: { layout: 'centered' },
12
+ argTypes: {
13
+ onPress: { action: 'pressed' },
14
+ },
15
+ } satisfies Meta<typeof Link>;
16
+
17
+ export default meta;
18
+ type Story = StoryObj<typeof meta>;
19
+
20
+ export const Default: Story = {
21
+ args: { children: 'Visit website', href: 'https://example.com' },
22
+ };
23
+
24
+ export const WithOnPress: Story = {
25
+ args: { children: 'Navigate', onPress: () => {} },
26
+ };
27
+
28
+ export const WithAccessibilityLabel: Story = {
29
+ args: {
30
+ children: 'Read more',
31
+ href: 'https://example.com',
32
+ accessibilityLabel: 'Read more about our products',
33
+ },
34
+ };
35
+
36
+ export const AllVariants: Story = {
37
+ render: () => (
38
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
39
+ <Link href="https://example.com">Default link</Link>
40
+ <Link onPress={() => {}}>Callback link</Link>
41
+ <Link href="https://example.com" accessibilityLabel="Accessible link">
42
+ With accessibility label
43
+ </Link>
44
+ </div>
45
+ ),
46
+ };
@@ -0,0 +1,48 @@
1
+ import { ReactNode } from 'react';
2
+ import { StyleSheet, Text, Pressable, Linking, StyleProp, TextStyle } from 'react-native';
3
+ import { font } from '../../../native/tokens';
4
+ import { useTheme } from '../../../native/ThemeContext';
5
+
6
+ interface LinkProps {
7
+ href?: string;
8
+ children: ReactNode;
9
+ accessibilityLabel?: string;
10
+ style?: StyleProp<TextStyle>;
11
+ onPress?: () => void;
12
+ }
13
+
14
+ const styles = StyleSheet.create({
15
+ text: {
16
+ fontSize: font.size.body,
17
+ fontFamily: font.family.body,
18
+ textDecorationLine: 'underline',
19
+ },
20
+ pressed: {
21
+ opacity: 0.75,
22
+ },
23
+ });
24
+
25
+ export function Link({ href, children, accessibilityLabel, style, onPress }: Readonly<LinkProps>) {
26
+ const { colors } = useTheme();
27
+
28
+ const handlePress = () => {
29
+ if (onPress) {
30
+ onPress();
31
+ } else if (href) {
32
+ Linking.openURL(href);
33
+ }
34
+ };
35
+
36
+ return (
37
+ <Pressable
38
+ onPress={handlePress}
39
+ accessibilityRole="link"
40
+ accessibilityLabel={accessibilityLabel}
41
+ style={({ pressed }) => pressed && styles.pressed}
42
+ >
43
+ <Text style={[styles.text, { color: colors.colorInteractiveOnBg }, style]}>
44
+ {children}
45
+ </Text>
46
+ </Pressable>
47
+ );
48
+ }