@boostdev/design-system-react-native 0.1.0 → 0.2.1

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 (191) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/LICENSE +15 -0
  3. package/README.md +104 -2
  4. package/dist/index.cjs +106 -60
  5. package/dist/index.d.cts +61 -6
  6. package/dist/index.d.ts +61 -6
  7. package/dist/index.js +106 -60
  8. package/package.json +30 -10
  9. package/src/components/interaction/Button/Button.mdx +91 -0
  10. package/src/components/interaction/Button/Button.spec.tsx +55 -0
  11. package/src/components/interaction/Button/Button.stories.tsx +141 -0
  12. package/src/components/interaction/Button/Button.tsx +105 -0
  13. package/src/components/interaction/Dialog/Dialog.mdx +61 -0
  14. package/src/components/interaction/Dialog/Dialog.spec.tsx +73 -0
  15. package/src/components/interaction/Dialog/Dialog.stories.tsx +53 -0
  16. package/src/components/interaction/Dialog/Dialog.tsx +128 -0
  17. package/src/components/interaction/DropdownMenu/DropdownMenu.mdx +74 -0
  18. package/src/components/interaction/DropdownMenu/DropdownMenu.spec.tsx +78 -0
  19. package/src/components/interaction/DropdownMenu/DropdownMenu.stories.tsx +51 -0
  20. package/src/components/interaction/DropdownMenu/DropdownMenu.tsx +254 -0
  21. package/src/components/interaction/Popover/Popover.mdx +61 -0
  22. package/src/components/interaction/Popover/Popover.spec.tsx +73 -0
  23. package/src/components/interaction/Popover/Popover.stories.tsx +44 -0
  24. package/src/components/interaction/Popover/Popover.tsx +87 -0
  25. package/src/components/interaction/Rating/Rating.mdx +55 -0
  26. package/src/components/interaction/Rating/Rating.spec.tsx +38 -0
  27. package/src/components/interaction/Rating/Rating.stories.tsx +37 -0
  28. package/src/components/interaction/Rating/Rating.tsx +50 -0
  29. package/src/components/interaction/SideSheet/SideSheet.mdx +60 -0
  30. package/src/components/interaction/SideSheet/SideSheet.spec.tsx +80 -0
  31. package/src/components/interaction/SideSheet/SideSheet.stories.tsx +33 -0
  32. package/src/components/interaction/SideSheet/SideSheet.tsx +175 -0
  33. package/src/components/interaction/Toast/Toast.mdx +81 -0
  34. package/src/components/interaction/Toast/Toast.spec.tsx +80 -0
  35. package/src/components/interaction/Toast/Toast.stories.tsx +47 -0
  36. package/src/components/interaction/Toast/Toast.tsx +202 -0
  37. package/src/components/interaction/form/Checkbox/Checkbox.mdx +74 -0
  38. package/src/components/interaction/form/Checkbox/Checkbox.spec.tsx +42 -0
  39. package/src/components/interaction/form/Checkbox/Checkbox.stories.tsx +34 -0
  40. package/src/components/interaction/form/Checkbox/Checkbox.tsx +80 -0
  41. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.mdx +78 -0
  42. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.spec.tsx +57 -0
  43. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.stories.tsx +96 -0
  44. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.tsx +61 -0
  45. package/src/components/interaction/form/Combobox/Combobox.mdx +86 -0
  46. package/src/components/interaction/form/Combobox/Combobox.spec.tsx +60 -0
  47. package/src/components/interaction/form/Combobox/Combobox.stories.tsx +99 -0
  48. package/src/components/interaction/form/Combobox/Combobox.tsx +212 -0
  49. package/src/components/interaction/form/FileInput/FileInput.mdx +86 -0
  50. package/src/components/interaction/form/FileInput/FileInput.spec.tsx +73 -0
  51. package/src/components/interaction/form/FileInput/FileInput.stories.tsx +69 -0
  52. package/src/components/interaction/form/FileInput/FileInput.tsx +129 -0
  53. package/src/components/interaction/form/FormInput/FormInput.mdx +81 -0
  54. package/src/components/interaction/form/FormInput/FormInput.spec.tsx +81 -0
  55. package/src/components/interaction/form/FormInput/FormInput.stories.tsx +54 -0
  56. package/src/components/interaction/form/FormInput/FormInput.tsx +123 -0
  57. package/src/components/interaction/form/NumberInput/NumberInput.mdx +87 -0
  58. package/src/components/interaction/form/NumberInput/NumberInput.spec.tsx +63 -0
  59. package/src/components/interaction/form/NumberInput/NumberInput.stories.tsx +63 -0
  60. package/src/components/interaction/form/NumberInput/NumberInput.tsx +199 -0
  61. package/src/components/interaction/form/Radio/Radio.spec.tsx +79 -0
  62. package/src/components/interaction/form/Radio/Radio.stories.tsx +44 -0
  63. package/src/components/interaction/form/Radio/Radio.tsx +79 -0
  64. package/src/components/interaction/form/RadioGroup/RadioGroup.mdx +79 -0
  65. package/src/components/interaction/form/RadioGroup/RadioGroup.spec.tsx +66 -0
  66. package/src/components/interaction/form/RadioGroup/RadioGroup.stories.tsx +96 -0
  67. package/src/components/interaction/form/RadioGroup/RadioGroup.tsx +61 -0
  68. package/src/components/interaction/form/SegmentedControl/SegmentedControl.mdx +78 -0
  69. package/src/components/interaction/form/SegmentedControl/SegmentedControl.spec.tsx +82 -0
  70. package/src/components/interaction/form/SegmentedControl/SegmentedControl.stories.tsx +99 -0
  71. package/src/components/interaction/form/SegmentedControl/SegmentedControl.tsx +121 -0
  72. package/src/components/interaction/form/Select/Select.mdx +85 -0
  73. package/src/components/interaction/form/Select/Select.spec.tsx +64 -0
  74. package/src/components/interaction/form/Select/Select.stories.tsx +75 -0
  75. package/src/components/interaction/form/Select/Select.tsx +252 -0
  76. package/src/components/interaction/form/Slider/Slider.mdx +83 -0
  77. package/src/components/interaction/form/Slider/Slider.spec.tsx +51 -0
  78. package/src/components/interaction/form/Slider/Slider.stories.tsx +58 -0
  79. package/src/components/interaction/form/Slider/Slider.tsx +188 -0
  80. package/src/components/interaction/form/Switch/Switch.spec.tsx +60 -0
  81. package/src/components/interaction/form/Switch/Switch.stories.tsx +33 -0
  82. package/src/components/interaction/form/Switch/Switch.tsx +59 -0
  83. package/src/components/interaction/form/Textarea/Textarea.mdx +76 -0
  84. package/src/components/interaction/form/Textarea/Textarea.spec.tsx +50 -0
  85. package/src/components/interaction/form/Textarea/Textarea.stories.tsx +50 -0
  86. package/src/components/interaction/form/Textarea/Textarea.tsx +108 -0
  87. package/src/components/interaction/form/atoms/Label.spec.tsx +39 -0
  88. package/src/components/interaction/form/atoms/Label.stories.tsx +18 -0
  89. package/src/components/interaction/form/atoms/Label.tsx +29 -0
  90. package/src/components/interaction/form/atoms/Message.spec.tsx +30 -0
  91. package/src/components/interaction/form/atoms/Message.stories.tsx +22 -0
  92. package/src/components/interaction/form/atoms/Message.tsx +34 -0
  93. package/src/components/layout/ButtonGroup/ButtonGroup.spec.tsx +83 -0
  94. package/src/components/layout/ButtonGroup/ButtonGroup.stories.tsx +44 -0
  95. package/src/components/layout/ButtonGroup/ButtonGroup.tsx +44 -0
  96. package/src/components/layout/Card/Card.spec.tsx +102 -0
  97. package/src/components/layout/Card/Card.stories.tsx +51 -0
  98. package/src/components/layout/Card/Card.tsx +89 -0
  99. package/src/components/layout/IconWrapper/IconWrapper.spec.tsx +39 -0
  100. package/src/components/layout/IconWrapper/IconWrapper.stories.tsx +37 -0
  101. package/src/components/layout/IconWrapper/IconWrapper.tsx +20 -0
  102. package/src/components/layout/SectionHeader/SectionHeader.spec.tsx +51 -0
  103. package/src/components/layout/SectionHeader/SectionHeader.stories.tsx +36 -0
  104. package/src/components/layout/SectionHeader/SectionHeader.tsx +79 -0
  105. package/src/components/ui/Accordion/Accordion.mdx +65 -0
  106. package/src/components/ui/Accordion/Accordion.spec.tsx +69 -0
  107. package/src/components/ui/Accordion/Accordion.stories.tsx +52 -0
  108. package/src/components/ui/Accordion/Accordion.tsx +141 -0
  109. package/src/components/ui/Alert/Alert.mdx +62 -0
  110. package/src/components/ui/Alert/Alert.spec.tsx +76 -0
  111. package/src/components/ui/Alert/Alert.stories.tsx +41 -0
  112. package/src/components/ui/Alert/Alert.tsx +94 -0
  113. package/src/components/ui/Avatar/Avatar.mdx +50 -0
  114. package/src/components/ui/Avatar/Avatar.spec.tsx +47 -0
  115. package/src/components/ui/Avatar/Avatar.stories.tsx +32 -0
  116. package/src/components/ui/Avatar/Avatar.tsx +85 -0
  117. package/src/components/ui/Badge/Badge.mdx +54 -0
  118. package/src/components/ui/Badge/Badge.spec.tsx +45 -0
  119. package/src/components/ui/Badge/Badge.stories.tsx +34 -0
  120. package/src/components/ui/Badge/Badge.tsx +50 -0
  121. package/src/components/ui/Breadcrumb/Breadcrumb.mdx +52 -0
  122. package/src/components/ui/Breadcrumb/Breadcrumb.spec.tsx +49 -0
  123. package/src/components/ui/Breadcrumb/Breadcrumb.stories.tsx +46 -0
  124. package/src/components/ui/Breadcrumb/Breadcrumb.tsx +83 -0
  125. package/src/components/ui/Calendar/Calendar.mdx +58 -0
  126. package/src/components/ui/Calendar/Calendar.spec.tsx +92 -0
  127. package/src/components/ui/Calendar/Calendar.stories.tsx +45 -0
  128. package/src/components/ui/Calendar/Calendar.tsx +294 -0
  129. package/src/components/ui/Carousel/Carousel.mdx +55 -0
  130. package/src/components/ui/Carousel/Carousel.spec.tsx +55 -0
  131. package/src/components/ui/Carousel/Carousel.stories.tsx +59 -0
  132. package/src/components/ui/Carousel/Carousel.tsx +184 -0
  133. package/src/components/ui/Collapsible/Collapsible.mdx +53 -0
  134. package/src/components/ui/Collapsible/Collapsible.spec.tsx +68 -0
  135. package/src/components/ui/Collapsible/Collapsible.stories.tsx +48 -0
  136. package/src/components/ui/Collapsible/Collapsible.tsx +139 -0
  137. package/src/components/ui/DescriptionList/DescriptionList.mdx +50 -0
  138. package/src/components/ui/DescriptionList/DescriptionList.spec.tsx +37 -0
  139. package/src/components/ui/DescriptionList/DescriptionList.stories.tsx +48 -0
  140. package/src/components/ui/DescriptionList/DescriptionList.tsx +54 -0
  141. package/src/components/ui/Link/Link.mdx +53 -0
  142. package/src/components/ui/Link/Link.spec.tsx +35 -0
  143. package/src/components/ui/Link/Link.stories.tsx +46 -0
  144. package/src/components/ui/Link/Link.tsx +48 -0
  145. package/src/components/ui/Loading/Loading.spec.tsx +24 -0
  146. package/src/components/ui/Loading/Loading.stories.tsx +31 -0
  147. package/src/components/ui/Loading/Loading.tsx +29 -0
  148. package/src/components/ui/NotificationBanner/NotificationBanner.spec.tsx +85 -0
  149. package/src/components/ui/NotificationBanner/NotificationBanner.stories.tsx +37 -0
  150. package/src/components/ui/NotificationBanner/NotificationBanner.tsx +76 -0
  151. package/src/components/ui/Pagination/Pagination.mdx +62 -0
  152. package/src/components/ui/Pagination/Pagination.spec.tsx +86 -0
  153. package/src/components/ui/Pagination/Pagination.stories.tsx +47 -0
  154. package/src/components/ui/Pagination/Pagination.tsx +157 -0
  155. package/src/components/ui/Progress/Progress.spec.tsx +56 -0
  156. package/src/components/ui/Progress/Progress.stories.tsx +34 -0
  157. package/src/components/ui/Progress/Progress.tsx +84 -0
  158. package/src/components/ui/ProgressCircle/ProgressCircle.mdx +63 -0
  159. package/src/components/ui/ProgressCircle/ProgressCircle.spec.tsx +39 -0
  160. package/src/components/ui/ProgressCircle/ProgressCircle.stories.tsx +36 -0
  161. package/src/components/ui/ProgressCircle/ProgressCircle.tsx +176 -0
  162. package/src/components/ui/Separator/Separator.spec.tsx +39 -0
  163. package/src/components/ui/Separator/Separator.stories.tsx +38 -0
  164. package/src/components/ui/Separator/Separator.tsx +32 -0
  165. package/src/components/ui/Skeleton/Skeleton.spec.tsx +39 -0
  166. package/src/components/ui/Skeleton/Skeleton.stories.tsx +31 -0
  167. package/src/components/ui/Skeleton/Skeleton.tsx +42 -0
  168. package/src/components/ui/Table/Table.mdx +72 -0
  169. package/src/components/ui/Table/Table.spec.tsx +83 -0
  170. package/src/components/ui/Table/Table.stories.tsx +55 -0
  171. package/src/components/ui/Table/Table.tsx +196 -0
  172. package/src/components/ui/Tabs/Tabs.mdx +65 -0
  173. package/src/components/ui/Tabs/Tabs.spec.tsx +65 -0
  174. package/src/components/ui/Tabs/Tabs.stories.tsx +57 -0
  175. package/src/components/ui/Tabs/Tabs.tsx +133 -0
  176. package/src/components/ui/Tooltip/Tooltip.mdx +47 -0
  177. package/src/components/ui/Tooltip/Tooltip.spec.tsx +37 -0
  178. package/src/components/ui/Tooltip/Tooltip.stories.tsx +38 -0
  179. package/src/components/ui/Tooltip/Tooltip.tsx +57 -0
  180. package/src/components/ui/Typography/Typography.mdx +56 -0
  181. package/src/components/ui/Typography/Typography.spec.tsx +49 -0
  182. package/src/components/ui/Typography/Typography.stories.tsx +36 -0
  183. package/src/components/ui/Typography/Typography.tsx +65 -0
  184. package/src/index.ts +86 -0
  185. package/src/native/ReactNavigationTheme.mdx +140 -0
  186. package/src/native/ReactNavigationTheme.spec.tsx +35 -0
  187. package/src/native/ReactNavigationTheme.ts +102 -0
  188. package/src/native/ThemeContext.spec.tsx +57 -0
  189. package/src/native/ThemeContext.tsx +48 -0
  190. package/src/native/tokens.ts +13 -0
  191. package/src/test/setup.ts +1 -0
@@ -0,0 +1,83 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './Slider.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # Slider (React Native)
7
+
8
+ Range slider with a draggable track, optional value display, and error/hint messages. Uses touch responders on the track for value selection.
9
+
10
+ > **Status: alpha** — API may change before stable release.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { Slider } from '@boostdev/design-system-components';
16
+ import { useState } from 'react';
17
+
18
+ function Example() {
19
+ const [volume, setVolume] = useState(50);
20
+ return (
21
+ <Slider
22
+ label="Volume"
23
+ name="volume"
24
+ value={volume}
25
+ onChange={setVolume}
26
+ showValue
27
+ />
28
+ );
29
+ }
30
+ ```
31
+
32
+ ## Examples
33
+
34
+ ### Default
35
+ <Canvas of={Stories.Default} />
36
+
37
+ ### With value display
38
+ <Canvas of={Stories.WithValue} />
39
+
40
+ ### Custom range
41
+ <Canvas of={Stories.CustomRange} />
42
+
43
+ ### With step
44
+ <Canvas of={Stories.WithStep} />
45
+
46
+ ### With error
47
+ <Canvas of={Stories.WithError} />
48
+
49
+ ### With hint
50
+ <Canvas of={Stories.WithHint} />
51
+
52
+ ### Disabled
53
+ <Canvas of={Stories.Disabled} />
54
+
55
+ ### Interactive
56
+ <Canvas of={Stories.Interactive} />
57
+
58
+ ## Props
59
+
60
+ <ArgTypes of={Stories} />
61
+
62
+ ## Props reference
63
+
64
+ | Prop | Type | Default | Description |
65
+ |------|------|---------|-------------|
66
+ | `label` | string | — | Visible label text |
67
+ | `name` | string | — | Field name |
68
+ | `value` | number | — | Controlled value |
69
+ | `defaultValue` | number | — | Uncontrolled default value |
70
+ | `min` | number | `0` | Minimum value |
71
+ | `max` | number | `100` | Maximum value |
72
+ | `step` | number | `1` | Value increment |
73
+ | `showValue` | boolean | `false` | Show current value next to the label |
74
+ | `disabled` | boolean | `false` | Prevents interaction, reduces opacity |
75
+ | `error` | string | — | Error message displayed below the track |
76
+ | `hint` | string | — | Hint text displayed below the track |
77
+ | `onChange` | `(value: number) => void` | — | Called with the new value |
78
+
79
+ ## Accessibility
80
+
81
+ - Uses `accessibilityRole="adjustable"` for VoiceOver/TalkBack
82
+ - `accessibilityValue` provides `min`, `max`, `now`, and `text`
83
+ - `disabled` communicates the disabled state via `accessibilityState`
@@ -0,0 +1,51 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../../native/ThemeContext';
3
+ import { Slider } from './Slider';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ describe('Slider (native)', () => {
10
+ it('renders the label', () => {
11
+ renderWithTheme(<Slider label="Volume" name="volume" />);
12
+ expect(screen.getByText('Volume')).toBeInTheDocument();
13
+ });
14
+
15
+ it('shows current value when showValue is true', () => {
16
+ renderWithTheme(
17
+ <Slider label="Volume" name="volume" value={75} showValue />,
18
+ );
19
+ expect(screen.getByText('75')).toBeInTheDocument();
20
+ });
21
+
22
+ it('hides value display when showValue is false', () => {
23
+ renderWithTheme(
24
+ <Slider label="Volume" name="volume" value={50} />,
25
+ );
26
+ // Only "Volume" text should be present, not the value
27
+ expect(screen.queryByText('50')).not.toBeInTheDocument();
28
+ });
29
+
30
+ it('shows error message', () => {
31
+ renderWithTheme(
32
+ <Slider label="Volume" name="volume" error="Too low." />,
33
+ );
34
+ expect(screen.getByRole('alert')).toHaveTextContent('Too low.');
35
+ });
36
+
37
+ it('shows hint message', () => {
38
+ renderWithTheme(
39
+ <Slider label="Volume" name="volume" hint="Adjust volume." />,
40
+ );
41
+ expect(screen.getByText('Adjust volume.')).toBeInTheDocument();
42
+ });
43
+
44
+ it('uses default min value when no value is provided', () => {
45
+ renderWithTheme(
46
+ <Slider label="Volume" name="volume" showValue />,
47
+ );
48
+ // Default min is 0, which becomes the initial value
49
+ expect(screen.getByText('0')).toBeInTheDocument();
50
+ });
51
+ });
@@ -0,0 +1,58 @@
1
+ import React, { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { ThemeProvider } from '../../../../native/ThemeContext';
4
+ import { Slider } from './Slider';
5
+
6
+ const meta = {
7
+ tags: ['!stable', 'alpha'],
8
+ title: 'React Native/Form/Slider',
9
+ component: Slider,
10
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
11
+ parameters: { layout: 'padded' },
12
+ } satisfies Meta<typeof Slider>;
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Default: Story = {
18
+ args: { label: 'Volume', name: 'volume', value: 50 },
19
+ };
20
+
21
+ export const WithValue: Story = {
22
+ args: { label: 'Brightness', name: 'brightness', value: 75, showValue: true },
23
+ };
24
+
25
+ export const CustomRange: Story = {
26
+ args: { label: 'Temperature', name: 'temp', min: 16, max: 30, value: 21, showValue: true },
27
+ };
28
+
29
+ export const WithStep: Story = {
30
+ args: { label: 'Quantity', name: 'qty', min: 0, max: 50, step: 5, value: 25, showValue: true },
31
+ };
32
+
33
+ export const WithError: Story = {
34
+ args: { label: 'Volume', name: 'volume', value: 0, error: 'Volume must be at least 10.' },
35
+ };
36
+
37
+ export const WithHint: Story = {
38
+ args: { label: 'Volume', name: 'volume', value: 50, hint: 'Adjust playback volume.' },
39
+ };
40
+
41
+ export const Disabled: Story = {
42
+ args: { label: 'Volume', name: 'volume', value: 30, disabled: true, showValue: true },
43
+ };
44
+
45
+ export const Interactive: Story = {
46
+ render: () => {
47
+ const [value, setValue] = useState(50);
48
+ return (
49
+ <Slider
50
+ label="Volume"
51
+ name="volume"
52
+ value={value}
53
+ onChange={setValue}
54
+ showValue
55
+ />
56
+ );
57
+ },
58
+ };
@@ -0,0 +1,188 @@
1
+ import { useState } from 'react';
2
+ import {
3
+ StyleSheet,
4
+ View,
5
+ Text,
6
+ GestureResponderEvent,
7
+ LayoutChangeEvent,
8
+ StyleProp,
9
+ ViewStyle,
10
+ } from 'react-native';
11
+ import { spacing, font, lineHeight } from '../../../../native/tokens';
12
+ import { useTheme } from '../../../../native/ThemeContext';
13
+ import { Label } from '../atoms/Label';
14
+ import { Message } from '../atoms/Message';
15
+
16
+ interface SliderProps {
17
+ label: string;
18
+ name: string;
19
+ value?: number;
20
+ defaultValue?: number;
21
+ min?: number;
22
+ max?: number;
23
+ step?: number;
24
+ showValue?: boolean;
25
+ disabled?: boolean;
26
+ error?: string;
27
+ hint?: string;
28
+ onChange?: (value: number) => void;
29
+ style?: StyleProp<ViewStyle>;
30
+ }
31
+
32
+ const TRACK_HEIGHT = 6;
33
+ const THUMB_SIZE = 24;
34
+
35
+ const styles = StyleSheet.create({
36
+ container: {
37
+ gap: spacing.xxs,
38
+ },
39
+ labelRow: {
40
+ flexDirection: 'row',
41
+ justifyContent: 'space-between',
42
+ alignItems: 'center',
43
+ },
44
+ valueText: {
45
+ fontFamily: font.family.body,
46
+ fontSize: font.size.bodyS,
47
+ lineHeight: lineHeight.bodyS,
48
+ fontWeight: font.weight.semibold,
49
+ },
50
+ trackContainer: {
51
+ height: THUMB_SIZE + spacing.xs * 2,
52
+ justifyContent: 'center',
53
+ paddingHorizontal: THUMB_SIZE / 2,
54
+ },
55
+ track: {
56
+ height: TRACK_HEIGHT,
57
+ borderRadius: TRACK_HEIGHT / 2,
58
+ overflow: 'hidden',
59
+ },
60
+ fill: {
61
+ height: TRACK_HEIGHT,
62
+ borderRadius: TRACK_HEIGHT / 2,
63
+ },
64
+ thumb: {
65
+ position: 'absolute',
66
+ width: THUMB_SIZE,
67
+ height: THUMB_SIZE,
68
+ borderRadius: THUMB_SIZE / 2,
69
+ top: '50%',
70
+ marginTop: -(THUMB_SIZE / 2),
71
+ borderWidth: 2,
72
+ },
73
+ });
74
+
75
+ function clampValue(v: number, min: number, max: number): number {
76
+ return Math.min(max, Math.max(min, v));
77
+ }
78
+
79
+ function snapToStep(v: number, min: number, step: number): number {
80
+ return Math.round((v - min) / step) * step + min;
81
+ }
82
+
83
+ export function Slider({
84
+ label,
85
+ name: _name, // eslint-disable-line @typescript-eslint/no-unused-vars
86
+ value,
87
+ defaultValue,
88
+ min = 0,
89
+ max = 100,
90
+ step = 1,
91
+ showValue = false,
92
+ disabled = false,
93
+ error,
94
+ hint,
95
+ onChange,
96
+ style,
97
+ }: Readonly<SliderProps>) {
98
+ const { colors } = useTheme();
99
+ const isControlled = value !== undefined;
100
+ const [internalValue, setInternalValue] = useState(defaultValue ?? min);
101
+ const [trackWidth, setTrackWidth] = useState(0);
102
+
103
+ const currentValue = isControlled ? value : internalValue;
104
+ const fillPct = max > min ? ((currentValue - min) / (max - min)) * 100 : 0;
105
+
106
+ const updateValue = (v: number) => {
107
+ const clamped = clampValue(v, min, max);
108
+ const snapped = snapToStep(clamped, min, step);
109
+ if (!isControlled) setInternalValue(snapped);
110
+ onChange?.(snapped);
111
+ };
112
+
113
+ const handleTrackLayout = (e: LayoutChangeEvent) => {
114
+ setTrackWidth(e.nativeEvent.layout.width);
115
+ };
116
+
117
+ const valueFromTouch = (pageX: number, layoutX: number): number => {
118
+ const x = pageX - layoutX;
119
+ const pct = clampValue(x / trackWidth, 0, 1);
120
+ return pct * (max - min) + min;
121
+ };
122
+
123
+ const handleStartShouldSetResponder = () => !disabled;
124
+ const handleResponderGrant = (e: GestureResponderEvent) => {
125
+ const target = e.currentTarget as unknown as { measure: (cb: (x: number, y: number, w: number, h: number, px: number, py: number) => void) => void };
126
+ target.measure((_x, _y, _w, _h, pageX) => {
127
+ updateValue(valueFromTouch(e.nativeEvent.pageX, pageX));
128
+ });
129
+ };
130
+ const handleResponderMove = (e: GestureResponderEvent) => {
131
+ const target = e.currentTarget as unknown as { measure: (cb: (x: number, y: number, w: number, h: number, px: number, py: number) => void) => void };
132
+ target.measure((_x, _y, _w, _h, pageX) => {
133
+ updateValue(valueFromTouch(e.nativeEvent.pageX, pageX));
134
+ });
135
+ };
136
+
137
+ return (
138
+ <View
139
+ style={[styles.container, style]}
140
+ accessibilityRole="adjustable"
141
+ accessibilityLabel={label}
142
+ accessibilityValue={{ min, max, now: currentValue, text: String(currentValue) }}
143
+ accessibilityState={{ disabled }}
144
+ >
145
+ <View style={styles.labelRow}>
146
+ <Label label={label} />
147
+ {showValue && (
148
+ <Text style={[styles.valueText, { color: colors.colorOnBg }]}>
149
+ {currentValue}
150
+ </Text>
151
+ )}
152
+ </View>
153
+ <View
154
+ style={[styles.trackContainer, disabled && { opacity: 0.4 }]}
155
+ onLayout={handleTrackLayout}
156
+ onStartShouldSetResponder={handleStartShouldSetResponder}
157
+ onMoveShouldSetResponder={handleStartShouldSetResponder}
158
+ onResponderGrant={handleResponderGrant}
159
+ onResponderMove={handleResponderMove}
160
+ >
161
+ <View style={[styles.track, { backgroundColor: colors.colorBgSubtle }]}>
162
+ <View
163
+ style={[
164
+ styles.fill,
165
+ {
166
+ width: `${fillPct}%`,
167
+ backgroundColor: error ? colors.colorError : colors.colorInteractive,
168
+ },
169
+ ]}
170
+ />
171
+ </View>
172
+ <View
173
+ style={[
174
+ styles.thumb,
175
+ {
176
+ start: `${fillPct}%`,
177
+ marginStart: -(THUMB_SIZE / 2),
178
+ backgroundColor: colors.colorBg,
179
+ borderColor: error ? colors.colorError : colors.colorInteractive,
180
+ },
181
+ ]}
182
+ />
183
+ </View>
184
+ <Message type="error" message={error} />
185
+ <Message type="hint" message={hint} />
186
+ </View>
187
+ );
188
+ }
@@ -0,0 +1,60 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../../native/ThemeContext';
3
+ import { Switch } from './Switch';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ // react-native-web renders RNSwitch as two role="switch" elements: an outer div
10
+ // (for visual rendering) and an inner hidden <input type="checkbox"> for a11y.
11
+ // The input carries the aria-label and aria-checked state.
12
+ function getSwitchInput(container: HTMLElement): HTMLInputElement {
13
+ const input = container.querySelector<HTMLInputElement>('input[role="switch"]');
14
+ if (!input) throw new Error('Switch input not found');
15
+ return input;
16
+ }
17
+
18
+ describe('Switch (native)', () => {
19
+ it('renders a switch input with the correct label', () => {
20
+ const { container } = renderWithTheme(
21
+ <Switch label="Notifications" name="notifications" />,
22
+ );
23
+ const input = getSwitchInput(container);
24
+ expect(input).toHaveAttribute('aria-label', 'Notifications');
25
+ });
26
+
27
+ it('reflects off state', () => {
28
+ const { container } = renderWithTheme(
29
+ <Switch label="Notifications" name="notifications" value={false} />,
30
+ );
31
+ expect(getSwitchInput(container)).not.toBeChecked();
32
+ });
33
+
34
+ it('reflects on state', () => {
35
+ const { container } = renderWithTheme(
36
+ <Switch label="Notifications" name="notifications" value={true} />,
37
+ );
38
+ expect(getSwitchInput(container)).toBeChecked();
39
+ });
40
+
41
+ it('renders thumb in both on and off states without error (thumb always uses colorInteractive)', () => {
42
+ // thumbColor is always colorInteractive — interactive affordance is visible in both states.
43
+ // Track color provides the on/off visual feedback instead.
44
+ const { container: offContainer } = renderWithTheme(
45
+ <Switch label="Toggle" name="toggle" value={false} />,
46
+ );
47
+ const { container: onContainer } = renderWithTheme(
48
+ <Switch label="Toggle2" name="toggle2" value={true} />,
49
+ );
50
+ expect(offContainer.querySelector('[style*="background-color"]')).toBeInTheDocument();
51
+ expect(onContainer.querySelector('[style*="background-color"]')).toBeInTheDocument();
52
+ });
53
+
54
+ it('renders hint message', () => {
55
+ renderWithTheme(
56
+ <Switch label="Marketing" name="marketing" hint="Unsubscribe anytime." />,
57
+ );
58
+ expect(screen.getByText('Unsubscribe anytime.')).toBeInTheDocument();
59
+ });
60
+ });
@@ -0,0 +1,33 @@
1
+ import React, { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { ThemeProvider } from '../../../../native/ThemeContext';
4
+ import { Switch } from './Switch';
5
+
6
+ const meta = {
7
+ tags: ['!stable', 'alpha'],
8
+ title: 'React Native/Form/Switch',
9
+ component: Switch,
10
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
11
+ parameters: { layout: 'padded' },
12
+ } satisfies Meta<typeof Switch>;
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Off: Story = { args: { label: 'Notifications', name: 'notifications', value: false } };
18
+ export const On: Story = { args: { label: 'Notifications', name: 'notifications', value: true } };
19
+ export const Disabled: Story = { args: { label: 'Notifications', name: 'notifications', disabled: true } };
20
+ export const WithError: Story = {
21
+ args: { label: 'Accept terms', name: 'terms', error: 'You must accept the terms.' },
22
+ };
23
+ export const WithHint: Story = {
24
+ args: { label: 'Marketing emails', name: 'marketing', hint: 'You can unsubscribe at any time.' },
25
+ };
26
+ export const Interactive: Story = {
27
+ render: () => {
28
+ const [value, setValue] = useState(false);
29
+ return (
30
+ <Switch label="Enable feature" name="feature" value={value} onChange={setValue} />
31
+ );
32
+ },
33
+ };
@@ -0,0 +1,59 @@
1
+ import { Switch as RNSwitch, View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
2
+ import { spacing } from '../../../../native/tokens';
3
+ import { useTheme } from '../../../../native/ThemeContext';
4
+ import { Label } from '../atoms/Label';
5
+ import { Message } from '../atoms/Message';
6
+
7
+ interface SwitchProps {
8
+ label: string;
9
+ name: string;
10
+ value?: boolean;
11
+ onChange?: (value: boolean) => void;
12
+ disabled?: boolean;
13
+ error?: string;
14
+ hint?: string;
15
+ style?: StyleProp<ViewStyle>;
16
+ }
17
+
18
+ const styles = StyleSheet.create({
19
+ container: {
20
+ gap: spacing.xxs,
21
+ },
22
+ row: {
23
+ flexDirection: 'row',
24
+ alignItems: 'center',
25
+ gap: spacing.xs,
26
+ },
27
+ });
28
+
29
+ export function Switch({
30
+ label,
31
+ value = false,
32
+ onChange,
33
+ disabled = false,
34
+ error,
35
+ hint,
36
+ style,
37
+ }: Readonly<SwitchProps>) {
38
+ const { colors } = useTheme();
39
+
40
+ return (
41
+ <View style={[styles.container, style]}>
42
+ <View style={styles.row}>
43
+ <RNSwitch
44
+ value={value}
45
+ onValueChange={onChange}
46
+ disabled={disabled}
47
+ accessibilityLabel={label}
48
+ accessibilityRole="switch"
49
+ accessibilityState={{ checked: value, disabled }}
50
+ thumbColor={colors.colorInteractive}
51
+ trackColor={{ false: colors.colorBgSubtle, true: colors.colorBlueSubtle }}
52
+ />
53
+ <Label label={label} />
54
+ </View>
55
+ <Message type="error" message={error} />
56
+ <Message type="hint" message={hint} />
57
+ </View>
58
+ );
59
+ }
@@ -0,0 +1,76 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './Textarea.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # Textarea (React Native)
7
+
8
+ Multiline text input with label, optional hint, and error state. Uses a `TextInput` with `multiline` enabled and `textAlignVertical: 'top'`.
9
+
10
+ > **Status: alpha** — API may change before stable release.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { Textarea } from '@boostdev/design-system-components';
16
+ import { useState } from 'react';
17
+
18
+ function Example() {
19
+ const [bio, setBio] = useState('');
20
+ return (
21
+ <Textarea
22
+ label="Bio"
23
+ name="bio"
24
+ placeholder="Tell us about yourself..."
25
+ value={bio}
26
+ onChange={setBio}
27
+ />
28
+ );
29
+ }
30
+ ```
31
+
32
+ ## Examples
33
+
34
+ ### Default
35
+ <Canvas of={Stories.Default} />
36
+
37
+ ### With placeholder
38
+ <Canvas of={Stories.WithPlaceholder} />
39
+
40
+ ### With error
41
+ <Canvas of={Stories.WithError} />
42
+
43
+ ### With hint
44
+ <Canvas of={Stories.WithHint} />
45
+
46
+ ### Disabled
47
+ <Canvas of={Stories.Disabled} />
48
+
49
+ ### Interactive
50
+ <Canvas of={Stories.Interactive} />
51
+
52
+ ## Props
53
+
54
+ <ArgTypes of={Stories} />
55
+
56
+ ## Props reference
57
+
58
+ | Prop | Type | Default | Description |
59
+ |------|------|---------|-------------|
60
+ | `label` | ReactNode | — | Visible label text |
61
+ | `name` | string | — | Field name (used for accessibility) |
62
+ | `value` | string | — | Controlled value |
63
+ | `defaultValue` | string | — | Uncontrolled default value |
64
+ | `placeholder` | string | — | Placeholder text |
65
+ | `numberOfLines` | number | `4` | Number of visible lines |
66
+ | `error` | string | — | Error message displayed below the input |
67
+ | `hint` | string | — | Hint text displayed below the input |
68
+ | `required` | boolean | `false` | Marks the field as required |
69
+ | `disabled` | boolean | `false` | Prevents interaction, reduces opacity |
70
+ | `onChange` | `(value: string) => void` | — | Called with the new text value |
71
+
72
+ ## Accessibility
73
+
74
+ - The label is announced by screen readers via `accessibilityLabel`
75
+ - Hint text is announced via `accessibilityHint`
76
+ - `disabled` communicates the disabled state via `accessibilityState`
@@ -0,0 +1,50 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../../native/ThemeContext';
3
+ import { Textarea } from './Textarea';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ describe('Textarea (native)', () => {
10
+ it('renders the label', () => {
11
+ renderWithTheme(<Textarea label="Message" name="message" />);
12
+ expect(screen.getByText('Message')).toBeInTheDocument();
13
+ });
14
+
15
+ it('renders with placeholder text', () => {
16
+ renderWithTheme(
17
+ <Textarea label="Bio" name="bio" placeholder="Tell us about yourself..." />,
18
+ );
19
+ expect(screen.getByPlaceholderText('Tell us about yourself...')).toBeInTheDocument();
20
+ });
21
+
22
+ it('renders with a value', () => {
23
+ renderWithTheme(
24
+ <Textarea label="Notes" name="notes" value="Some content" />,
25
+ );
26
+ expect(screen.getByDisplayValue('Some content')).toBeInTheDocument();
27
+ });
28
+
29
+ it('shows error message', () => {
30
+ renderWithTheme(
31
+ <Textarea label="Message" name="message" error="Message is required." />,
32
+ );
33
+ expect(screen.getByRole('alert')).toHaveTextContent('Message is required.');
34
+ });
35
+
36
+ it('shows hint message', () => {
37
+ renderWithTheme(
38
+ <Textarea label="Message" name="message" hint="Minimum 20 characters." />,
39
+ );
40
+ expect(screen.getByText('Minimum 20 characters.')).toBeInTheDocument();
41
+ });
42
+
43
+ it('applies disabled opacity', () => {
44
+ const { container } = renderWithTheme(
45
+ <Textarea label="Notes" name="notes" disabled />,
46
+ );
47
+ const inputs = container.querySelectorAll('textarea, [style*="opacity"]');
48
+ expect(inputs.length).toBeGreaterThan(0);
49
+ });
50
+ });