@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,85 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { Text } from 'react-native';
3
+ import { ThemeProvider } from '../../../native/ThemeContext';
4
+ import { NotificationBanner } from './NotificationBanner.native';
5
+
6
+ function renderWithTheme(ui: React.ReactElement) {
7
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
8
+ }
9
+
10
+ describe('NotificationBanner (native)', () => {
11
+ it('renders children text', () => {
12
+ renderWithTheme(<NotificationBanner>Update available</NotificationBanner>);
13
+ expect(screen.getByText('Update available')).toBeInTheDocument();
14
+ });
15
+
16
+ it('has alert role for error variant', () => {
17
+ renderWithTheme(
18
+ <NotificationBanner variant="error">Something went wrong</NotificationBanner>,
19
+ );
20
+ expect(screen.getByRole('alert')).toBeInTheDocument();
21
+ });
22
+
23
+ it('has alert role for warning variant', () => {
24
+ renderWithTheme(
25
+ <NotificationBanner variant="warning">Attention needed</NotificationBanner>,
26
+ );
27
+ expect(screen.getByRole('alert')).toBeInTheDocument();
28
+ });
29
+
30
+ it('does not have alert role for info variant', () => {
31
+ renderWithTheme(
32
+ <NotificationBanner variant="info">FYI</NotificationBanner>,
33
+ );
34
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
35
+ });
36
+
37
+ it('does not have alert role for success variant', () => {
38
+ renderWithTheme(
39
+ <NotificationBanner variant="success">All good</NotificationBanner>,
40
+ );
41
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
42
+ });
43
+
44
+ it('renders an action element when provided', () => {
45
+ renderWithTheme(
46
+ <NotificationBanner action={<Text>Retry</Text>}>
47
+ Failed to save
48
+ </NotificationBanner>,
49
+ );
50
+ expect(screen.getByText('Retry')).toBeInTheDocument();
51
+ expect(screen.getByText('Failed to save')).toBeInTheDocument();
52
+ });
53
+
54
+ it('renders a dismiss button when onDismiss is provided', () => {
55
+ renderWithTheme(
56
+ <NotificationBanner onDismiss={() => {}}>Dismissable</NotificationBanner>,
57
+ );
58
+ expect(
59
+ screen.getByRole('button', { name: 'Dismiss notification' }),
60
+ ).toBeInTheDocument();
61
+ });
62
+
63
+ it('calls onDismiss when dismiss button is pressed', () => {
64
+ const onDismiss = vi.fn();
65
+ renderWithTheme(
66
+ <NotificationBanner onDismiss={onDismiss}>Dismissable</NotificationBanner>,
67
+ );
68
+ fireEvent.click(screen.getByRole('button', { name: 'Dismiss notification' }));
69
+ expect(onDismiss).toHaveBeenCalledTimes(1);
70
+ });
71
+
72
+ it('does not render a dismiss button when onDismiss is omitted', () => {
73
+ renderWithTheme(<NotificationBanner>No dismiss</NotificationBanner>);
74
+ expect(
75
+ screen.queryByRole('button', { name: 'Dismiss notification' }),
76
+ ).not.toBeInTheDocument();
77
+ });
78
+
79
+ it('accepts custom style', () => {
80
+ renderWithTheme(
81
+ <NotificationBanner style={{ marginTop: 10 }}>Styled</NotificationBanner>,
82
+ );
83
+ expect(screen.getByText('Styled')).toBeInTheDocument();
84
+ });
85
+ });
@@ -7,7 +7,7 @@ const meta = {
7
7
  tags: ['!stable', 'alpha'],
8
8
  title: 'React Native/UI/NotificationBanner',
9
9
  component: NotificationBanner,
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'] },
@@ -26,14 +26,12 @@ export const Dismissible: Story = {
26
26
  };
27
27
  export const AllVariants: Story = {
28
28
  render: () => (
29
- <ThemeProvider>
30
- <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
31
- {(['info', 'success', 'warning', 'error'] as const).map(v => (
32
- <NotificationBanner key={v} variant={v}>
33
- {v.charAt(0).toUpperCase() + v.slice(1)} notification
34
- </NotificationBanner>
35
- ))}
36
- </div>
37
- </ThemeProvider>
29
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
30
+ {(['info', 'success', 'warning', 'error'] as const).map(v => (
31
+ <NotificationBanner key={v} variant={v}>
32
+ {v.charAt(0).toUpperCase() + v.slice(1)} notification
33
+ </NotificationBanner>
34
+ ))}
35
+ </div>
38
36
  ),
39
37
  };
@@ -43,20 +43,20 @@ export function NotificationBanner({
43
43
  const { colors } = useTheme();
44
44
  const isUrgent = variant === 'error' || variant === 'warning';
45
45
 
46
- const variantColors: Record<BannerVariant, { bg: string; text: string }> = {
47
- info: { bg: colors.colorBlueSubtle, text: colors.colorOnBlueSubtle },
48
- success: { bg: colors.colorSuccessSubtle, text: colors.colorOnSuccessSubtle },
49
- warning: { bg: colors.colorOrangeSubtle, text: colors.colorOnOrangeSubtle },
50
- error: { bg: colors.colorRedSubtle, text: colors.colorOnRedSubtle },
46
+ const variantColors: Record<BannerVariant, { bg: string; text: string; border: string }> = {
47
+ info: { bg: colors.colorBlueSubtle, text: colors.colorOnBlueSubtle, border: colors.colorBlue },
48
+ success: { bg: colors.colorSuccessSubtle, text: colors.colorOnSuccessSubtle, border: colors.colorSuccess },
49
+ warning: { bg: colors.colorOrangeSubtle, text: colors.colorOnOrangeSubtle, border: colors.colorWarning },
50
+ error: { bg: colors.colorRedSubtle, text: colors.colorOnBg, border: colors.colorError },
51
51
  };
52
52
 
53
- const { bg, text } = variantColors[variant];
53
+ const { bg, text, border: borderColor } = variantColors[variant];
54
54
 
55
55
  return (
56
56
  <View
57
57
  accessibilityRole={isUrgent ? 'alert' : 'none'}
58
58
  accessibilityLiveRegion={isUrgent ? 'assertive' : 'polite'}
59
- style={[styles.banner, { backgroundColor: bg }, style]}
59
+ style={[styles.banner, { backgroundColor: bg, borderBottomWidth: 3, borderBottomColor: borderColor }, style]}
60
60
  >
61
61
  <Text style={[styles.content, { color: text }]}>{children}</Text>
62
62
  {action}
@@ -0,0 +1,62 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './Pagination.native.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # Pagination (React Native)
7
+
8
+ React Native pagination component for navigating between pages of content. Shows page numbers with ellipsis for large ranges, with Previous/Next navigation buttons.
9
+
10
+ > **Status: alpha** -- API may change before stable release.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { Pagination } from '@boostdev/components';
16
+
17
+ <Pagination
18
+ currentPage={3}
19
+ totalPages={10}
20
+ onPageChange={(page) => setPage(page)}
21
+ />
22
+ ```
23
+
24
+ ## Examples
25
+
26
+ ### Default (first page)
27
+ <Canvas of={Stories.Default} />
28
+
29
+ ### Middle page
30
+ <Canvas of={Stories.MiddlePage} />
31
+
32
+ ### Last page
33
+ <Canvas of={Stories.LastPage} />
34
+
35
+ ### Many pages (with ellipsis)
36
+ <Canvas of={Stories.ManyPages} />
37
+
38
+ ### Single page
39
+ <Canvas of={Stories.SinglePage} />
40
+
41
+ ### Interactive
42
+ <Canvas of={Stories.Interactive} />
43
+
44
+ ## Props
45
+
46
+ <ArgTypes of={Stories} />
47
+
48
+ ## Props reference
49
+
50
+ | Prop | Type | Default | Description |
51
+ |------|------|---------|-------------|
52
+ | `currentPage` | `number` | -- | The currently active page (1-based) |
53
+ | `totalPages` | `number` | -- | Total number of pages |
54
+ | `onPageChange` | `(page: number) => void` | -- | Called when a page button is pressed |
55
+ | `style` | `StyleProp<ViewStyle>` | -- | Custom style for the container |
56
+
57
+ ## Accessibility
58
+
59
+ - Container has `navigation` role with "Pagination" label
60
+ - Previous/Next buttons are disabled at bounds
61
+ - Active page is indicated with `selected` accessibility state
62
+ - Each page button has a descriptive label (e.g. "Page 3")
@@ -0,0 +1,86 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../native/ThemeContext';
3
+ import { Pagination } from './Pagination.native';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ describe('Pagination (native)', () => {
10
+ it('renders the navigation container', () => {
11
+ renderWithTheme(
12
+ <Pagination currentPage={1} totalPages={5} onPageChange={() => {}} />,
13
+ );
14
+ expect(screen.getByRole('navigation', { name: 'Pagination' })).toBeInTheDocument();
15
+ });
16
+
17
+ it('renders page buttons', () => {
18
+ renderWithTheme(
19
+ <Pagination currentPage={1} totalPages={3} onPageChange={() => {}} />,
20
+ );
21
+ expect(screen.getByRole('button', { name: 'Page 1' })).toBeInTheDocument();
22
+ expect(screen.getByRole('button', { name: 'Page 2' })).toBeInTheDocument();
23
+ expect(screen.getByRole('button', { name: 'Page 3' })).toBeInTheDocument();
24
+ });
25
+
26
+ it('renders the current page button', () => {
27
+ renderWithTheme(
28
+ <Pagination currentPage={2} totalPages={3} onPageChange={() => {}} />,
29
+ );
30
+ expect(screen.getByRole('button', { name: 'Page 2' })).toBeInTheDocument();
31
+ });
32
+
33
+ it('disables Previous on first page', () => {
34
+ renderWithTheme(
35
+ <Pagination currentPage={1} totalPages={5} onPageChange={() => {}} />,
36
+ );
37
+ expect(screen.getByRole('button', { name: 'Previous page' })).toHaveAttribute(
38
+ 'aria-disabled',
39
+ 'true',
40
+ );
41
+ });
42
+
43
+ it('disables Next on last page', () => {
44
+ renderWithTheme(
45
+ <Pagination currentPage={5} totalPages={5} onPageChange={() => {}} />,
46
+ );
47
+ expect(screen.getByRole('button', { name: 'Next page' })).toHaveAttribute(
48
+ 'aria-disabled',
49
+ 'true',
50
+ );
51
+ });
52
+
53
+ it('calls onPageChange with the next page when Next is pressed', () => {
54
+ const onPageChange = vi.fn();
55
+ renderWithTheme(
56
+ <Pagination currentPage={2} totalPages={5} onPageChange={onPageChange} />,
57
+ );
58
+ fireEvent.click(screen.getByRole('button', { name: 'Next page' }));
59
+ expect(onPageChange).toHaveBeenCalledWith(3);
60
+ });
61
+
62
+ it('calls onPageChange with the previous page when Previous is pressed', () => {
63
+ const onPageChange = vi.fn();
64
+ renderWithTheme(
65
+ <Pagination currentPage={3} totalPages={5} onPageChange={onPageChange} />,
66
+ );
67
+ fireEvent.click(screen.getByRole('button', { name: 'Previous page' }));
68
+ expect(onPageChange).toHaveBeenCalledWith(2);
69
+ });
70
+
71
+ it('calls onPageChange when a page number is pressed', () => {
72
+ const onPageChange = vi.fn();
73
+ renderWithTheme(
74
+ <Pagination currentPage={1} totalPages={5} onPageChange={onPageChange} />,
75
+ );
76
+ fireEvent.click(screen.getByRole('button', { name: 'Page 2' }));
77
+ expect(onPageChange).toHaveBeenCalledWith(2);
78
+ });
79
+
80
+ it('shows ellipsis for large page ranges', () => {
81
+ renderWithTheme(
82
+ <Pagination currentPage={5} totalPages={20} onPageChange={() => {}} />,
83
+ );
84
+ expect(screen.getAllByText('...').length).toBeGreaterThanOrEqual(1);
85
+ });
86
+ });
@@ -0,0 +1,47 @@
1
+ import React, { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { ThemeProvider } from '../../../native/ThemeContext';
4
+ import { Pagination } from './Pagination.native';
5
+
6
+ const meta = {
7
+ tags: ['!stable', 'alpha'],
8
+ title: 'React Native/UI/Pagination',
9
+ component: Pagination,
10
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
11
+ parameters: { layout: 'padded' },
12
+ } satisfies Meta<typeof Pagination>;
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Default: Story = {
18
+ args: { currentPage: 1, totalPages: 5 },
19
+ };
20
+
21
+ export const MiddlePage: Story = {
22
+ args: { currentPage: 3, totalPages: 5 },
23
+ };
24
+
25
+ export const LastPage: Story = {
26
+ args: { currentPage: 5, totalPages: 5 },
27
+ };
28
+
29
+ export const ManyPages: Story = {
30
+ args: { currentPage: 5, totalPages: 20 },
31
+ };
32
+
33
+ export const SinglePage: Story = {
34
+ args: { currentPage: 1, totalPages: 1 },
35
+ };
36
+
37
+ function InteractiveDemo() {
38
+ const [page, setPage] = useState(1);
39
+ return (
40
+ <Pagination currentPage={page} totalPages={10} onPageChange={setPage} />
41
+ );
42
+ }
43
+
44
+ export const Interactive: Story = {
45
+ args: { currentPage: 1, totalPages: 10 },
46
+ render: () => <InteractiveDemo />,
47
+ };
@@ -0,0 +1,157 @@
1
+ import { AccessibilityRole, StyleSheet, View, Text, Pressable, StyleProp, ViewStyle } from 'react-native';
2
+ import { spacing, font, border, lineHeight } from '../../../native/tokens';
3
+ import { useTheme } from '../../../native/ThemeContext';
4
+
5
+ interface PaginationProps {
6
+ currentPage: number;
7
+ totalPages: number;
8
+ onPageChange: (page: number) => void;
9
+ style?: StyleProp<ViewStyle>;
10
+ }
11
+
12
+ function getPageRange(current: number, total: number): (number | '...')[] {
13
+ const delta = 1;
14
+ const range: (number | '...')[] = [];
15
+
16
+ for (let i = 1; i <= total; i++) {
17
+ if (i === 1 || i === total || (i >= current - delta && i <= current + delta)) {
18
+ range.push(i);
19
+ } else if (range[range.length - 1] !== '...') {
20
+ range.push('...');
21
+ }
22
+ }
23
+
24
+ return range;
25
+ }
26
+
27
+ const styles = StyleSheet.create({
28
+ container: {
29
+ flexDirection: 'row',
30
+ alignItems: 'center',
31
+ gap: spacing.xxs,
32
+ },
33
+ button: {
34
+ minWidth: 44,
35
+ minHeight: 44,
36
+ alignItems: 'center',
37
+ justifyContent: 'center',
38
+ borderRadius: border.radius.full,
39
+ paddingHorizontal: spacing.xs,
40
+ paddingVertical: spacing.xxs,
41
+ },
42
+ buttonText: {
43
+ fontSize: font.size.body,
44
+ fontFamily: font.family.body,
45
+ lineHeight: lineHeight.body,
46
+ fontWeight: font.weight.body,
47
+ },
48
+ navText: {
49
+ fontSize: font.size.body,
50
+ fontFamily: font.family.body,
51
+ lineHeight: lineHeight.body,
52
+ fontWeight: font.weight.semibold,
53
+ },
54
+ ellipsis: {
55
+ fontSize: font.size.body,
56
+ fontFamily: font.family.body,
57
+ lineHeight: lineHeight.body,
58
+ minWidth: 44,
59
+ minHeight: 44,
60
+ textAlign: 'center',
61
+ textAlignVertical: 'center',
62
+ },
63
+ pressed: {
64
+ opacity: 0.75,
65
+ },
66
+ disabled: {
67
+ opacity: 0.4,
68
+ },
69
+ });
70
+
71
+ export function Pagination({
72
+ currentPage,
73
+ totalPages,
74
+ onPageChange,
75
+ style,
76
+ }: Readonly<PaginationProps>) {
77
+ const { colors } = useTheme();
78
+ const pages = getPageRange(currentPage, totalPages);
79
+
80
+ return (
81
+ <View
82
+ style={[styles.container, style]}
83
+ accessibilityRole={'navigation' as AccessibilityRole}
84
+ accessibilityLabel="Pagination"
85
+ >
86
+ <Pressable
87
+ onPress={() => onPageChange(currentPage - 1)}
88
+ disabled={currentPage <= 1}
89
+ accessibilityRole="button"
90
+ accessibilityLabel="Previous page"
91
+ accessibilityState={{ disabled: currentPage <= 1 }}
92
+ style={({ pressed }) => [
93
+ styles.button,
94
+ currentPage <= 1 && styles.disabled,
95
+ pressed && !currentPage && styles.pressed,
96
+ ]}
97
+ >
98
+ <Text style={[styles.navText, { color: colors.colorOnBg }]}>‹</Text>
99
+ </Pressable>
100
+
101
+ {pages.map((page, index) =>
102
+ page === '...' ? (
103
+ <Text
104
+ key={`ellipsis-${index}`}
105
+ style={[styles.ellipsis, { color: colors.colorOnBgSubtle }]}
106
+ importantForAccessibility="no"
107
+ accessibilityElementsHidden={true}
108
+ >
109
+ ...
110
+ </Text>
111
+ ) : (
112
+ <Pressable
113
+ key={page}
114
+ onPress={() => onPageChange(page)}
115
+ accessibilityRole="button"
116
+ accessibilityLabel={`Page ${page}`}
117
+ accessibilityState={{ selected: currentPage === page }}
118
+ style={({ pressed }) => [
119
+ styles.button,
120
+ currentPage === page && { backgroundColor: colors.colorInteractive },
121
+ pressed && styles.pressed,
122
+ ]}
123
+ >
124
+ <Text
125
+ style={[
126
+ styles.buttonText,
127
+ {
128
+ color: currentPage === page
129
+ ? colors.colorOnInteractive
130
+ : colors.colorOnBg,
131
+ },
132
+ currentPage === page && { fontWeight: font.weight.semibold },
133
+ ]}
134
+ >
135
+ {page}
136
+ </Text>
137
+ </Pressable>
138
+ ),
139
+ )}
140
+
141
+ <Pressable
142
+ onPress={() => onPageChange(currentPage + 1)}
143
+ disabled={currentPage >= totalPages}
144
+ accessibilityRole="button"
145
+ accessibilityLabel="Next page"
146
+ accessibilityState={{ disabled: currentPage >= totalPages }}
147
+ style={({ pressed }) => [
148
+ styles.button,
149
+ currentPage >= totalPages && styles.disabled,
150
+ pressed && styles.pressed,
151
+ ]}
152
+ >
153
+ <Text style={[styles.navText, { color: colors.colorOnBg }]}>›</Text>
154
+ </Pressable>
155
+ </View>
156
+ );
157
+ }
@@ -0,0 +1,56 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../native/ThemeContext';
3
+ import { Progress } from './Progress.native';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ describe('Progress (native)', () => {
10
+ it('has progressbar role with accessible label', () => {
11
+ renderWithTheme(<Progress value={50} label="Upload progress" />);
12
+ expect(screen.getByRole('progressbar', { name: 'Upload progress' })).toBeInTheDocument();
13
+ });
14
+
15
+ it('renders progressbar for various values', () => {
16
+ renderWithTheme(<Progress value={30} max={100} label="Loading" />);
17
+ expect(screen.getByRole('progressbar', { name: 'Loading' })).toBeInTheDocument();
18
+ });
19
+
20
+ it('respects a custom max value in percentage display', () => {
21
+ renderWithTheme(<Progress value={5} max={10} label="Steps" showLabel />);
22
+ expect(screen.getByText('50%')).toBeInTheDocument();
23
+ });
24
+
25
+ it('does not show label by default', () => {
26
+ renderWithTheme(<Progress value={60} label="Progress" />);
27
+ expect(screen.queryByText('Progress')).not.toBeInTheDocument();
28
+ expect(screen.queryByText('60%')).not.toBeInTheDocument();
29
+ });
30
+
31
+ it('shows label text and percentage when showLabel is true', () => {
32
+ renderWithTheme(<Progress value={60} label="Progress" showLabel />);
33
+ expect(screen.getByText('Progress')).toBeInTheDocument();
34
+ expect(screen.getByText('60%')).toBeInTheDocument();
35
+ });
36
+
37
+ it('clamps percentage display at 100%', () => {
38
+ renderWithTheme(<Progress value={150} max={100} label="Overflow" showLabel />);
39
+ expect(screen.getByText('100%')).toBeInTheDocument();
40
+ });
41
+
42
+ it('clamps percentage display at 0%', () => {
43
+ renderWithTheme(<Progress value={-10} max={100} label="Underflow" showLabel />);
44
+ expect(screen.getByText('0%')).toBeInTheDocument();
45
+ });
46
+
47
+ it('rounds the percentage to the nearest integer', () => {
48
+ renderWithTheme(<Progress value={33} max={100} label="Loading" showLabel />);
49
+ expect(screen.getByText('33%')).toBeInTheDocument();
50
+ });
51
+
52
+ it('defaults to medium size', () => {
53
+ renderWithTheme(<Progress value={50} label="Loading" />);
54
+ expect(screen.getByRole('progressbar')).toBeInTheDocument();
55
+ });
56
+ });
@@ -9,7 +9,7 @@ const meta = {
9
9
  component: Progress,
10
10
  decorators: [
11
11
  (Story) => (
12
- <ThemeProvider>
12
+ <ThemeProvider colorScheme="light">
13
13
  <div style={{ width: 300 }}>
14
14
  <Story />
15
15
  </div>
@@ -35,12 +35,12 @@ const styles = StyleSheet.create({
35
35
  },
36
36
  track: {
37
37
  width: '100%',
38
- borderRadius: border.radius.xs,
38
+ borderRadius: border.radius.full,
39
39
  overflow: 'hidden',
40
40
  },
41
41
  fill: {
42
42
  height: '100%',
43
- borderRadius: border.radius.xs,
43
+ borderRadius: border.radius.full,
44
44
  },
45
45
  });
46
46
 
@@ -0,0 +1,63 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './ProgressCircle.native.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # ProgressCircle (React Native)
7
+
8
+ React Native circular progress indicator. Displays progress as a ring with an optional percentage label in the center.
9
+
10
+ > **Status: alpha** — API may change before stable release.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { ProgressCircle } from '@boostdev/components';
16
+
17
+ <ProgressCircle value={75} label="Upload progress" showValue />
18
+ <ProgressCircle value={50} label="Loading" size="large" />
19
+ ```
20
+
21
+ ## Examples
22
+
23
+ ### Default
24
+ <Canvas of={Stories.Default} />
25
+
26
+ ### With value label
27
+ <Canvas of={Stories.WithValue} />
28
+
29
+ ### Small
30
+ <Canvas of={Stories.Small} />
31
+
32
+ ### Large
33
+ <Canvas of={Stories.Large} />
34
+
35
+ ### Complete
36
+ <Canvas of={Stories.Complete} />
37
+
38
+ ### Empty
39
+ <Canvas of={Stories.Empty} />
40
+
41
+ ### All sizes
42
+ <Canvas of={Stories.AllSizes} />
43
+
44
+ ## Props
45
+
46
+ <ArgTypes of={Stories} />
47
+
48
+ ## Props reference
49
+
50
+ | Prop | Type | Default | Description |
51
+ |------|------|---------|-------------|
52
+ | `value` | `number` | — | Current progress value |
53
+ | `max` | `number` | `100` | Maximum value |
54
+ | `size` | `"small" \| "medium" \| "large"` | `"medium"` | Circle dimensions (40, 64, 96px) |
55
+ | `label` | `string` | — | Accessible label for the progress indicator |
56
+ | `showValue` | `boolean` | `false` | Show percentage text in the center |
57
+ | `style` | `StyleProp<ViewStyle>` | — | Override container styles |
58
+
59
+ ## Accessibility
60
+
61
+ - Uses `accessibilityRole="progressbar"` with `accessibilityValue` for min, max, and current
62
+ - `label` prop provides the screen reader description
63
+ - Percentage text is hidden from assistive technologies when `showValue` is true
@@ -0,0 +1,39 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../native/ThemeContext';
3
+ import { ProgressCircle } from './ProgressCircle.native';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ describe('ProgressCircle (native)', () => {
10
+ it('has progressbar role with accessible label', () => {
11
+ renderWithTheme(<ProgressCircle value={60} label="Upload progress" />);
12
+ expect(screen.getByRole('progressbar', { name: 'Upload progress' })).toBeInTheDocument();
13
+ });
14
+
15
+ it('renders the progressbar element', () => {
16
+ renderWithTheme(<ProgressCircle value={75} max={100} label="Loading" />);
17
+ expect(screen.getByRole('progressbar')).toBeInTheDocument();
18
+ });
19
+
20
+ it('shows percentage text when showValue is true', () => {
21
+ renderWithTheme(<ProgressCircle value={60} label="Progress" showValue />);
22
+ expect(screen.getByText('60%')).toBeInTheDocument();
23
+ });
24
+
25
+ it('does not show percentage text by default', () => {
26
+ renderWithTheme(<ProgressCircle value={60} label="Progress" />);
27
+ expect(screen.queryByText('60%')).not.toBeInTheDocument();
28
+ });
29
+
30
+ it('clamps value at 100%', () => {
31
+ renderWithTheme(<ProgressCircle value={150} max={100} label="Overflow" showValue />);
32
+ expect(screen.getByText('100%')).toBeInTheDocument();
33
+ });
34
+
35
+ it('clamps value at 0%', () => {
36
+ renderWithTheme(<ProgressCircle value={-10} max={100} label="Underflow" showValue />);
37
+ expect(screen.getByText('0%')).toBeInTheDocument();
38
+ });
39
+ });