@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,252 @@
1
+ import { ReactNode, useState } from 'react';
2
+ import {
3
+ StyleSheet,
4
+ View,
5
+ Text,
6
+ Pressable,
7
+ Modal,
8
+ FlatList,
9
+ SafeAreaView,
10
+ StyleProp,
11
+ ViewStyle,
12
+ } from 'react-native';
13
+ import { spacing, font, border, lineHeight } from '../../../../native/tokens';
14
+ import { useTheme } from '../../../../native/ThemeContext';
15
+ import { Label } from '../atoms/Label.native';
16
+ import { Message } from '../atoms/Message.native';
17
+
18
+ export interface SelectOption {
19
+ value: string;
20
+ label: string;
21
+ disabled?: boolean;
22
+ }
23
+
24
+ interface SelectProps {
25
+ label: ReactNode;
26
+ name: string;
27
+ options: SelectOption[];
28
+ placeholder?: string;
29
+ value?: string;
30
+ error?: string;
31
+ hint?: string;
32
+ disabled?: boolean;
33
+ onChange?: (value: string) => void;
34
+ style?: StyleProp<ViewStyle>;
35
+ }
36
+
37
+ const styles = StyleSheet.create({
38
+ container: {
39
+ gap: spacing.xxs,
40
+ },
41
+ trigger: {
42
+ flexDirection: 'row',
43
+ alignItems: 'center',
44
+ justifyContent: 'space-between',
45
+ paddingHorizontal: spacing.s,
46
+ paddingVertical: spacing.xs,
47
+ borderWidth: 1,
48
+ borderRadius: border.radius.full,
49
+ minHeight: 44,
50
+ },
51
+ triggerText: {
52
+ fontFamily: font.family.body,
53
+ fontSize: font.size.body,
54
+ lineHeight: lineHeight.body,
55
+ flex: 1,
56
+ },
57
+ chevron: {
58
+ fontFamily: font.family.body,
59
+ fontSize: font.size.body,
60
+ marginStart: spacing.xs,
61
+ },
62
+ overlay: {
63
+ flex: 1,
64
+ justifyContent: 'flex-end',
65
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
66
+ },
67
+ sheet: {
68
+ maxHeight: '60%',
69
+ borderTopStartRadius: border.radius.m,
70
+ borderTopEndRadius: border.radius.m,
71
+ paddingTop: spacing.m,
72
+ paddingBottom: spacing.xxxl,
73
+ },
74
+ sheetHeader: {
75
+ flexDirection: 'row',
76
+ justifyContent: 'space-between',
77
+ alignItems: 'center',
78
+ paddingHorizontal: spacing.m,
79
+ paddingBottom: spacing.s,
80
+ },
81
+ sheetTitle: {
82
+ fontFamily: font.family.body,
83
+ fontSize: font.size.heading3,
84
+ fontWeight: font.weight.semibold,
85
+ },
86
+ closeText: {
87
+ fontFamily: font.family.body,
88
+ fontSize: font.size.body,
89
+ fontWeight: font.weight.semibold,
90
+ },
91
+ optionItem: {
92
+ paddingHorizontal: spacing.m,
93
+ paddingVertical: spacing.s,
94
+ minHeight: 44,
95
+ justifyContent: 'center',
96
+ },
97
+ optionText: {
98
+ fontFamily: font.family.body,
99
+ fontSize: font.size.body,
100
+ lineHeight: lineHeight.body,
101
+ },
102
+ });
103
+
104
+ export function Select({
105
+ label,
106
+ name,
107
+ options,
108
+ placeholder = 'Select an option',
109
+ value,
110
+ error,
111
+ hint,
112
+ disabled = false,
113
+ onChange,
114
+ style,
115
+ }: Readonly<SelectProps>) {
116
+ const { colors } = useTheme();
117
+ const [open, setOpen] = useState(false);
118
+
119
+ const selectedOption = options.find((o) => o.value === value);
120
+ const displayText = selectedOption?.label ?? placeholder;
121
+ const isPlaceholder = !selectedOption;
122
+
123
+ const borderColor = error
124
+ ? colors.colorError
125
+ : open
126
+ ? colors.colorInteractive
127
+ : colors.colorOnBg;
128
+
129
+ const handleSelect = (optionValue: string) => {
130
+ onChange?.(optionValue);
131
+ setOpen(false);
132
+ };
133
+
134
+ return (
135
+ <View style={[styles.container, style]}>
136
+ <Label label={label} />
137
+ <Pressable
138
+ accessibilityRole="button"
139
+ accessibilityLabel={typeof label === 'string' ? `${label}, ${displayText}` : name}
140
+ accessibilityState={{ disabled, expanded: open }}
141
+ accessibilityHint="Double tap to open options"
142
+ disabled={disabled}
143
+ onPress={() => setOpen(true)}
144
+ style={[
145
+ styles.trigger,
146
+ {
147
+ borderColor,
148
+ backgroundColor: colors.colorBg,
149
+ },
150
+ disabled && { opacity: 0.4 },
151
+ ]}
152
+ >
153
+ <Text
154
+ style={[
155
+ styles.triggerText,
156
+ {
157
+ color: isPlaceholder ? colors.colorOnBgSubtle : colors.colorOnBg,
158
+ },
159
+ ]}
160
+ >
161
+ {displayText}
162
+ </Text>
163
+ <Text style={[styles.chevron, { color: colors.colorOnBgSubtle }]} aria-hidden>
164
+ {'\u25BE'}
165
+ </Text>
166
+ </Pressable>
167
+
168
+ <Modal
169
+ visible={open}
170
+ transparent
171
+ animationType="slide"
172
+ onRequestClose={() => setOpen(false)}
173
+ >
174
+ <Pressable
175
+ style={styles.overlay}
176
+ onPress={() => setOpen(false)}
177
+ accessibilityRole="button"
178
+ accessibilityLabel="Close options"
179
+ >
180
+ <Pressable
181
+ style={[styles.sheet, { backgroundColor: colors.colorBg }]}
182
+ onPress={() => {
183
+ /* prevent close when tapping sheet */
184
+ }}
185
+ >
186
+ <SafeAreaView>
187
+ <View style={styles.sheetHeader}>
188
+ <Text style={[styles.sheetTitle, { color: colors.colorOnBg }]}>
189
+ {typeof label === 'string' ? label : 'Select'}
190
+ </Text>
191
+ <Pressable
192
+ accessibilityRole="button"
193
+ accessibilityLabel="Close"
194
+ onPress={() => setOpen(false)}
195
+ hitSlop={8}
196
+ >
197
+ <Text style={[styles.closeText, { color: colors.colorInteractive }]}>
198
+ Done
199
+ </Text>
200
+ </Pressable>
201
+ </View>
202
+ <FlatList
203
+ data={options}
204
+ keyExtractor={(item) => item.value}
205
+ renderItem={({ item }) => {
206
+ const isSelected = item.value === value;
207
+ return (
208
+ <Pressable
209
+ accessibilityRole="button"
210
+ accessibilityLabel={item.label}
211
+ accessibilityState={{
212
+ selected: isSelected,
213
+ disabled: item.disabled,
214
+ }}
215
+ disabled={item.disabled}
216
+ onPress={() => handleSelect(item.value)}
217
+ style={({ pressed }) => [
218
+ styles.optionItem,
219
+ isSelected && { backgroundColor: colors.colorBgSubtle },
220
+ pressed && { opacity: 0.7 },
221
+ item.disabled && { opacity: 0.4 },
222
+ ]}
223
+ >
224
+ <Text
225
+ style={[
226
+ styles.optionText,
227
+ {
228
+ color: isSelected
229
+ ? colors.colorInteractive
230
+ : colors.colorOnBg,
231
+ fontWeight: isSelected
232
+ ? font.weight.semibold
233
+ : font.weight.body,
234
+ },
235
+ ]}
236
+ >
237
+ {item.label}
238
+ </Text>
239
+ </Pressable>
240
+ );
241
+ }}
242
+ />
243
+ </SafeAreaView>
244
+ </Pressable>
245
+ </Pressable>
246
+ </Modal>
247
+
248
+ <Message type="error" message={error} />
249
+ <Message type="hint" message={hint} />
250
+ </View>
251
+ );
252
+ }
@@ -0,0 +1,83 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './Slider.native.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/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.native';
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.native';
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.native';
14
+ import { Message } from '../atoms/Message.native';
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
+ }
@@ -7,7 +7,7 @@ const meta = {
7
7
  tags: ['!stable', 'alpha'],
8
8
  title: 'React Native/Form/Switch',
9
9
  component: Switch,
10
- decorators: [(Story) => <ThemeProvider><Story /></ThemeProvider>],
10
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
11
11
  parameters: { layout: 'padded' },
12
12
  } satisfies Meta<typeof Switch>;
13
13
 
@@ -27,9 +27,7 @@ export const Interactive: Story = {
27
27
  render: () => {
28
28
  const [value, setValue] = useState(false);
29
29
  return (
30
- <ThemeProvider>
31
- <Switch label="Enable feature" name="feature" value={value} onChange={setValue} />
32
- </ThemeProvider>
30
+ <Switch label="Enable feature" name="feature" value={value} onChange={setValue} />
33
31
  );
34
32
  },
35
33
  };