@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,73 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { Text, Pressable } from 'react-native';
3
+ import { ThemeProvider } from '../../../native/ThemeContext';
4
+ import { Popover } from './Popover.native';
5
+
6
+ function renderWithTheme(ui: React.ReactElement) {
7
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
8
+ }
9
+
10
+ function Trigger({ children, ...rest }: { children: string; onPress?: () => void }) {
11
+ return <Pressable {...rest}><Text>{children}</Text></Pressable>;
12
+ }
13
+
14
+ describe('Popover (native)', () => {
15
+ it('renders the trigger element', () => {
16
+ renderWithTheme(
17
+ <Popover content={<Text>Popover content</Text>}>
18
+ <Trigger>Trigger</Trigger>
19
+ </Popover>,
20
+ );
21
+ expect(screen.getByText('Trigger')).toBeInTheDocument();
22
+ });
23
+
24
+ it('does not show content initially', () => {
25
+ renderWithTheme(
26
+ <Popover content={<Text>Hidden content</Text>}>
27
+ <Trigger>Trigger</Trigger>
28
+ </Popover>,
29
+ );
30
+ expect(screen.queryByText('Hidden content')).not.toBeInTheDocument();
31
+ });
32
+
33
+ it('shows content when trigger is pressed', () => {
34
+ renderWithTheme(
35
+ <Popover content={<Text>Visible content</Text>} accessibilityLabel="Info">
36
+ <Trigger>Trigger</Trigger>
37
+ </Popover>,
38
+ );
39
+ fireEvent.click(screen.getByRole('button', { name: 'Info' }));
40
+ expect(screen.getByText('Visible content')).toBeInTheDocument();
41
+ });
42
+
43
+ it('renders trigger with button role and custom accessibility label', () => {
44
+ renderWithTheme(
45
+ <Popover content={<Text>Content</Text>} accessibilityLabel="Details">
46
+ <Trigger>Trigger</Trigger>
47
+ </Popover>,
48
+ );
49
+ const trigger = screen.getByRole('button', { name: 'Details' });
50
+ expect(trigger).toBeInTheDocument();
51
+ });
52
+
53
+ it('renders backdrop with close label when open', () => {
54
+ renderWithTheme(
55
+ <Popover content={<Text>Closable</Text>} accessibilityLabel="Test">
56
+ <Trigger>Trigger</Trigger>
57
+ </Popover>,
58
+ );
59
+ fireEvent.click(screen.getByRole('button', { name: 'Test' }));
60
+ expect(screen.getByText('Closable')).toBeInTheDocument();
61
+ // Verify the close backdrop button exists
62
+ expect(screen.getByRole('button', { name: 'Close popover' })).toBeInTheDocument();
63
+ });
64
+
65
+ it('uses default accessibility label when none provided', () => {
66
+ renderWithTheme(
67
+ <Popover content={<Text>Content</Text>}>
68
+ <Trigger>Trigger</Trigger>
69
+ </Popover>,
70
+ );
71
+ expect(screen.getByRole('button', { name: 'Open popover' })).toBeInTheDocument();
72
+ });
73
+ });
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ import { Text, View } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react';
4
+ import { ThemeProvider } from '../../../native/ThemeContext';
5
+ import { Popover } from './Popover.native';
6
+ import { Button } from '../Button/Button.native';
7
+
8
+ const meta = {
9
+ tags: ['!stable', 'alpha'],
10
+ title: 'React Native/Interaction/Popover',
11
+ component: Popover,
12
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
13
+ parameters: { layout: 'centered' },
14
+ } satisfies Meta<typeof Popover>;
15
+
16
+ export default meta;
17
+ type Story = StoryObj<typeof meta>;
18
+
19
+ export const Default: Story = {
20
+ render: () => (
21
+ <Popover
22
+ content={<Text>This is popover content displayed in a modal overlay.</Text>}
23
+ accessibilityLabel="More info"
24
+ >
25
+ <Button>Show Popover</Button>
26
+ </Popover>
27
+ ),
28
+ };
29
+
30
+ export const RichContent: Story = {
31
+ render: () => (
32
+ <Popover
33
+ content={
34
+ <View style={{ gap: 8 }}>
35
+ <Text style={{ fontWeight: '600', fontSize: 16 }}>Help</Text>
36
+ <Text>Press the button below to submit your response. You can edit your answer before submitting.</Text>
37
+ </View>
38
+ }
39
+ accessibilityLabel="Help information"
40
+ >
41
+ <Button variant="outline">Help</Button>
42
+ </Popover>
43
+ ),
44
+ };
@@ -0,0 +1,87 @@
1
+ import React, { ReactElement, ReactNode, useState } from 'react';
2
+ import {
3
+ Modal,
4
+ View,
5
+ Pressable,
6
+ StyleSheet,
7
+ StyleProp,
8
+ ViewStyle,
9
+ } from 'react-native';
10
+ import { spacing, border } from '../../../native/tokens';
11
+ import { useTheme } from '../../../native/ThemeContext';
12
+
13
+ interface PopoverProps {
14
+ children: ReactElement;
15
+ content: ReactNode;
16
+ accessibilityLabel?: string;
17
+ style?: StyleProp<ViewStyle>;
18
+ }
19
+
20
+ const styles = StyleSheet.create({
21
+ wrapper: {
22
+ alignSelf: 'flex-start',
23
+ },
24
+ overlay: {
25
+ flex: 1,
26
+ justifyContent: 'center',
27
+ alignItems: 'center',
28
+ backgroundColor: 'rgba(0, 0, 0, 0.25)',
29
+ padding: spacing.m,
30
+ },
31
+ panel: {
32
+ width: '100%',
33
+ maxWidth: 360,
34
+ borderRadius: border.radius.s,
35
+ padding: spacing.m,
36
+ shadowColor: '#000',
37
+ shadowOffset: { width: 0, height: 2 },
38
+ shadowOpacity: 0.15,
39
+ shadowRadius: 8,
40
+ elevation: 4,
41
+ },
42
+ });
43
+
44
+ export function Popover({
45
+ children,
46
+ content,
47
+ accessibilityLabel,
48
+ style,
49
+ }: Readonly<PopoverProps>) {
50
+ const { colors } = useTheme();
51
+ const [isOpen, setIsOpen] = useState(false);
52
+
53
+ return (
54
+ <View style={[styles.wrapper, style]}>
55
+ {React.cloneElement(children as ReactElement<Record<string, unknown>>, {
56
+ onPress: () => setIsOpen(true),
57
+ accessibilityRole: 'button',
58
+ accessibilityLabel: accessibilityLabel ?? 'Open popover',
59
+ })}
60
+
61
+ <Modal
62
+ visible={isOpen}
63
+ transparent
64
+ animationType="fade"
65
+ onRequestClose={() => setIsOpen(false)}
66
+ >
67
+ <Pressable
68
+ style={styles.overlay}
69
+ onPress={() => setIsOpen(false)}
70
+ accessibilityRole="button"
71
+ accessibilityLabel="Close popover"
72
+ >
73
+ <Pressable
74
+ style={[styles.panel, { backgroundColor: colors.colorBg }]}
75
+ onPress={() => {
76
+ /* prevent close when pressing panel */
77
+ }}
78
+ accessibilityRole="none"
79
+ accessibilityLabel={accessibilityLabel}
80
+ >
81
+ {content}
82
+ </Pressable>
83
+ </Pressable>
84
+ </Modal>
85
+ </View>
86
+ );
87
+ }
@@ -0,0 +1,55 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './Rating.native.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # Rating (React Native)
7
+
8
+ React Native read-only star rating display using text characters. Shows filled and empty stars based on the `value` prop.
9
+
10
+ > **Status: alpha** — API may change before stable release.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { Rating } from '@boostdev/components';
16
+
17
+ <Rating value={4} />
18
+ <Rating value={7} max={10} />
19
+ ```
20
+
21
+ ## Examples
22
+
23
+ ### Default (3 of 5)
24
+ <Canvas of={Stories.Default} />
25
+
26
+ ### Full rating
27
+ <Canvas of={Stories.Full} />
28
+
29
+ ### Empty rating
30
+ <Canvas of={Stories.Empty} />
31
+
32
+ ### Custom max
33
+ <Canvas of={Stories.CustomMax} />
34
+
35
+ ### All variants
36
+ <Canvas of={Stories.AllVariants} />
37
+
38
+ ## Props
39
+
40
+ <ArgTypes of={Stories} />
41
+
42
+ ## Props reference
43
+
44
+ | Prop | Type | Default | Description |
45
+ |------|------|---------|-------------|
46
+ | `value` | `number` | — | Number of filled stars |
47
+ | `max` | `number` | `5` | Total number of stars |
48
+ | `accessibilityLabel` | `string` | `"{value} out of {max} stars"` | Screen reader label |
49
+ | `style` | `StyleProp<ViewStyle>` | — | Override container styles |
50
+
51
+ ## Accessibility
52
+
53
+ - Uses `accessibilityRole="image"` since the rating is a visual representation
54
+ - Auto-generates an accessibility label like "3 out of 5 stars" when not provided
55
+ - Individual stars are hidden from assistive technologies
@@ -0,0 +1,38 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../native/ThemeContext';
3
+ import { Rating } from './Rating.native';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ describe('Rating (native)', () => {
10
+ it('has image role with auto-generated accessible label', () => {
11
+ renderWithTheme(<Rating value={3} />);
12
+ expect(screen.getByRole('img', { name: '3 out of 5 stars' })).toBeInTheDocument();
13
+ });
14
+
15
+ it('renders the correct number of filled stars', () => {
16
+ renderWithTheme(<Rating value={3} />);
17
+ const stars = screen.getAllByText('\u2605');
18
+ expect(stars).toHaveLength(5);
19
+ });
20
+
21
+ it('renders all stars as filled characters', () => {
22
+ renderWithTheme(<Rating value={2} />);
23
+ const stars = screen.getAllByText('\u2605');
24
+ expect(stars).toHaveLength(5);
25
+ });
26
+
27
+ it('uses custom max value', () => {
28
+ renderWithTheme(<Rating value={7} max={10} />);
29
+ expect(screen.getByRole('img', { name: '7 out of 10 stars' })).toBeInTheDocument();
30
+ const stars = screen.getAllByText('\u2605');
31
+ expect(stars).toHaveLength(10);
32
+ });
33
+
34
+ it('uses custom accessibilityLabel when provided', () => {
35
+ renderWithTheme(<Rating value={4} accessibilityLabel="Excellent rating" />);
36
+ expect(screen.getByRole('img', { name: 'Excellent rating' })).toBeInTheDocument();
37
+ });
38
+ });
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { ThemeProvider } from '../../../native/ThemeContext';
4
+ import { Rating } from './Rating.native';
5
+
6
+ const meta = {
7
+ tags: ['!stable', 'alpha'],
8
+ title: 'React Native/Interaction/Rating',
9
+ component: Rating,
10
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
11
+ parameters: { layout: 'centered' },
12
+ argTypes: {
13
+ value: { control: { type: 'range', min: 0, max: 5, step: 1 } },
14
+ max: { control: { type: 'number', min: 1, max: 10 } },
15
+ },
16
+ } satisfies Meta<typeof Rating>;
17
+
18
+ export default meta;
19
+ type Story = StoryObj<typeof meta>;
20
+
21
+ export const Default: Story = { args: { value: 3 } };
22
+ export const Full: Story = { args: { value: 5 } };
23
+ export const Empty: Story = { args: { value: 0 } };
24
+ export const CustomMax: Story = { args: { value: 7, max: 10 } };
25
+ export const WithLabel: Story = {
26
+ args: { value: 4, accessibilityLabel: 'Customer rating: 4 out of 5' },
27
+ };
28
+
29
+ export const AllVariants: Story = {
30
+ render: () => (
31
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
32
+ {[0, 1, 2, 3, 4, 5].map(v => (
33
+ <Rating key={v} value={v} />
34
+ ))}
35
+ </div>
36
+ ),
37
+ };
@@ -0,0 +1,50 @@
1
+ import { StyleSheet, View, Text, StyleProp, ViewStyle } from 'react-native';
2
+ import { font } from '../../../native/tokens';
3
+ import { useTheme } from '../../../native/ThemeContext';
4
+
5
+ interface RatingProps {
6
+ value: number;
7
+ max?: number;
8
+ style?: StyleProp<ViewStyle>;
9
+ accessibilityLabel?: string;
10
+ }
11
+
12
+ const STAR_SIZE = 24;
13
+
14
+ const styles = StyleSheet.create({
15
+ container: {
16
+ flexDirection: 'row',
17
+ alignItems: 'center',
18
+ },
19
+ star: {
20
+ fontSize: STAR_SIZE,
21
+ fontFamily: font.family.body,
22
+ },
23
+ });
24
+
25
+ export function Rating({ value, max = 5, style, accessibilityLabel }: Readonly<RatingProps>) {
26
+ const { colors } = useTheme();
27
+ const label = accessibilityLabel ?? `${value} out of ${max} stars`;
28
+
29
+ return (
30
+ <View
31
+ style={[styles.container, style]}
32
+ accessibilityRole="image"
33
+ accessibilityLabel={label}
34
+ >
35
+ {Array.from({ length: max }).map((_, i) => (
36
+ <Text
37
+ key={i}
38
+ style={[
39
+ styles.star,
40
+ { color: i < value ? colors.colorWarning : colors.colorBgSubtle },
41
+ ]}
42
+ importantForAccessibility="no"
43
+ accessibilityElementsHidden={true}
44
+ >
45
+ {'\u2605'}
46
+ </Text>
47
+ ))}
48
+ </View>
49
+ );
50
+ }
@@ -0,0 +1,81 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './Toast.native.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # Toast (React Native)
7
+
8
+ A toast notification system using React context. Provides `ToastProvider` and `useToast` hook. Toasts appear at the top of the screen with slide-in animation and auto-dismiss after 5 seconds.
9
+
10
+ > **Status: alpha** — API may change before stable release.
11
+
12
+ ## Usage
13
+
14
+ Wrap your app root in `ToastProvider`:
15
+
16
+ ```tsx
17
+ import { ToastProvider } from '@boostdev/components';
18
+ import { ThemeProvider } from '@boostdev/components/native/ThemeContext';
19
+
20
+ export default function App() {
21
+ return (
22
+ <ThemeProvider>
23
+ <ToastProvider>
24
+ <YourApp />
25
+ </ToastProvider>
26
+ </ThemeProvider>
27
+ );
28
+ }
29
+ ```
30
+
31
+ Then use `useToast` in any descendant:
32
+
33
+ ```tsx
34
+ import { useToast } from '@boostdev/components';
35
+
36
+ function SaveButton() {
37
+ const { showToast } = useToast();
38
+ return (
39
+ <Button onPress={() => showToast('Saved successfully!', 'success')}>
40
+ Save
41
+ </Button>
42
+ );
43
+ }
44
+ ```
45
+
46
+ ## Variants
47
+
48
+ - `'success'` — green background
49
+ - `'error'` — red background
50
+ - `'info'` — interactive/blue background
51
+
52
+ ## Examples
53
+
54
+ ### Default
55
+ <Canvas of={Stories.Default} />
56
+
57
+ ## API
58
+
59
+ ### ToastProvider
60
+
61
+ Wraps the application and renders toast notifications.
62
+
63
+ | Prop | Type | Default | Description |
64
+ |------|------|---------|-------------|
65
+ | `children` | `ReactNode` | — | Application content |
66
+ | `style` | `StyleProp<ViewStyle>` | — | Override wrapper styles |
67
+
68
+ ### useToast
69
+
70
+ Returns an object with:
71
+
72
+ | Method | Signature | Description |
73
+ |--------|-----------|-------------|
74
+ | `showToast` | `(message: string, variant: 'success' \| 'error' \| 'info') => void` | Displays a toast notification |
75
+
76
+ ## Accessibility
77
+
78
+ - Toasts have `accessibilityRole="alert"` and `accessibilityLiveRegion="polite"`
79
+ - Dismiss button has `accessibilityLabel="Dismiss"`
80
+ - Toasts auto-dismiss after 5 seconds
81
+ - Throws if `useToast` is called outside `ToastProvider`
@@ -0,0 +1,80 @@
1
+ import { render, screen, act, fireEvent } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../native/ThemeContext';
3
+ import { ToastProvider, useToast } from './Toast.native';
4
+
5
+ function ToastTrigger({ message = 'Test toast', variant = 'info' as const }) {
6
+ const { showToast } = useToast();
7
+ return <button onClick={() => showToast(message, variant)}>Show Toast</button>;
8
+ }
9
+
10
+ function setup(props = {}) {
11
+ return render(
12
+ <ThemeProvider>
13
+ <ToastProvider>
14
+ <ToastTrigger {...props} />
15
+ </ToastProvider>
16
+ </ThemeProvider>,
17
+ );
18
+ }
19
+
20
+ describe('Toast (native)', () => {
21
+ it('shows a toast when showToast is called', () => {
22
+ setup({ message: 'Hello toast' });
23
+ fireEvent.click(screen.getByRole('button'));
24
+ expect(screen.getByText('Hello toast')).toBeInTheDocument();
25
+ });
26
+
27
+ it('removes the toast after 5 seconds', () => {
28
+ vi.useFakeTimers();
29
+ setup({ message: 'Timed toast' });
30
+ act(() => {
31
+ fireEvent.click(screen.getByRole('button'));
32
+ });
33
+ expect(screen.getByText('Timed toast')).toBeInTheDocument();
34
+ act(() => {
35
+ vi.advanceTimersByTime(5100);
36
+ });
37
+ expect(screen.queryByText('Timed toast')).not.toBeInTheDocument();
38
+ vi.useRealTimers();
39
+ });
40
+
41
+ it('renders dismiss button with accessible label', () => {
42
+ setup({ message: 'Dismissable' });
43
+ fireEvent.click(screen.getByRole('button', { name: 'Show Toast' }));
44
+ expect(screen.getByRole('button', { name: 'Dismiss' })).toBeInTheDocument();
45
+ });
46
+
47
+ it('removes the toast when dismiss button is clicked', () => {
48
+ setup({ message: 'Dismissable toast' });
49
+ fireEvent.click(screen.getByRole('button', { name: 'Show Toast' }));
50
+ expect(screen.getByText('Dismissable toast')).toBeInTheDocument();
51
+ fireEvent.click(screen.getByRole('button', { name: 'Dismiss' }));
52
+ expect(screen.queryByText('Dismissable toast')).not.toBeInTheDocument();
53
+ });
54
+
55
+ it('throws when useToast is used outside ToastProvider', () => {
56
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
57
+ const Bad = () => {
58
+ useToast();
59
+ return null;
60
+ };
61
+ expect(() =>
62
+ render(
63
+ <ThemeProvider>
64
+ <Bad />
65
+ </ThemeProvider>,
66
+ ),
67
+ ).toThrow('useToast must be used within a ToastProvider');
68
+ errorSpy.mockRestore();
69
+ });
70
+
71
+ it('renders multiple toasts simultaneously', () => {
72
+ setup({ message: 'Toast 1' });
73
+ const trigger = screen.getByRole('button', { name: 'Show Toast' });
74
+ fireEvent.click(trigger);
75
+ fireEvent.click(trigger);
76
+ // At least one toast should be visible (they share the same message text)
77
+ const toasts = screen.getAllByText('Toast 1');
78
+ expect(toasts.length).toBe(2);
79
+ });
80
+ });
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react';
4
+ import { ThemeProvider } from '../../../native/ThemeContext';
5
+ import { ToastProvider, useToast } from './Toast.native';
6
+ import { Button } from '../Button/Button.native';
7
+
8
+ const meta = {
9
+ tags: ['!stable', 'alpha'],
10
+ title: 'React Native/Interaction/Toast',
11
+ component: ToastProvider,
12
+ parameters: { layout: 'padded' },
13
+ } satisfies Meta<typeof ToastProvider>;
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ function ToastControls() {
19
+ const { showToast } = useToast();
20
+ return (
21
+ <View style={{ gap: 12, paddingTop: 80 }}>
22
+ <Button onPress={() => showToast('Item saved successfully.', 'success')}>
23
+ Show Success Toast
24
+ </Button>
25
+ <Button onPress={() => showToast('Something went wrong. Please try again.', 'error')}>
26
+ Show Error Toast
27
+ </Button>
28
+ <Button onPress={() => showToast('Your session will expire in 5 minutes.', 'info')}>
29
+ Show Info Toast
30
+ </Button>
31
+ </View>
32
+ );
33
+ }
34
+
35
+ function ToastDemo() {
36
+ return (
37
+ <ThemeProvider colorScheme="light">
38
+ <ToastProvider>
39
+ <ToastControls />
40
+ </ToastProvider>
41
+ </ThemeProvider>
42
+ );
43
+ }
44
+
45
+ export const Default: Story = {
46
+ render: () => <ToastDemo />,
47
+ };