@boostdev/design-system-components 1.2.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +50 -5
  3. package/dist/client.cjs +50 -50
  4. package/dist/client.css +506 -503
  5. package/dist/client.js +50 -50
  6. package/dist/index.cjs +50 -50
  7. package/dist/index.css +506 -503
  8. package/dist/index.js +50 -50
  9. package/dist/native/index.cjs +3692 -352
  10. package/dist/native/index.d.cts +359 -3
  11. package/dist/native/index.d.ts +359 -3
  12. package/dist/native/index.js +3811 -364
  13. package/package.json +3 -3
  14. package/src/components/interaction/Button/Button.module.css +1 -4
  15. package/src/components/interaction/Button/Button.native.stories.tsx +10 -12
  16. package/src/components/interaction/Button/Button.native.tsx +3 -3
  17. package/src/components/interaction/Dialog/Dialog.module.css +1 -1
  18. package/src/components/interaction/Dialog/Dialog.native.mdx +61 -0
  19. package/src/components/interaction/Dialog/Dialog.native.spec.tsx +73 -0
  20. package/src/components/interaction/Dialog/Dialog.native.stories.tsx +53 -0
  21. package/src/components/interaction/Dialog/Dialog.native.tsx +128 -0
  22. package/src/components/interaction/Drawer/Drawer.module.css +1 -1
  23. package/src/components/interaction/Drawer/Drawer.native.mdx +58 -0
  24. package/src/components/interaction/Drawer/Drawer.native.spec.tsx +81 -0
  25. package/src/components/interaction/Drawer/Drawer.native.stories.tsx +33 -0
  26. package/src/components/interaction/Drawer/Drawer.native.tsx +175 -0
  27. package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +4 -0
  28. package/src/components/interaction/DropdownMenu/DropdownMenu.native.mdx +74 -0
  29. package/src/components/interaction/DropdownMenu/DropdownMenu.native.spec.tsx +78 -0
  30. package/src/components/interaction/DropdownMenu/DropdownMenu.native.stories.tsx +51 -0
  31. package/src/components/interaction/DropdownMenu/DropdownMenu.native.tsx +254 -0
  32. package/src/components/interaction/DropdownMenu/DropdownMenu.stories.tsx +4 -4
  33. package/src/components/interaction/Popover/Popover.native.mdx +61 -0
  34. package/src/components/interaction/Popover/Popover.native.spec.tsx +73 -0
  35. package/src/components/interaction/Popover/Popover.native.stories.tsx +44 -0
  36. package/src/components/interaction/Popover/Popover.native.tsx +87 -0
  37. package/src/components/interaction/Rating/Rating.native.mdx +55 -0
  38. package/src/components/interaction/Rating/Rating.native.spec.tsx +38 -0
  39. package/src/components/interaction/Rating/Rating.native.stories.tsx +37 -0
  40. package/src/components/interaction/Rating/Rating.native.tsx +50 -0
  41. package/src/components/interaction/Toast/Toast.native.mdx +81 -0
  42. package/src/components/interaction/Toast/Toast.native.spec.tsx +80 -0
  43. package/src/components/interaction/Toast/Toast.native.stories.tsx +47 -0
  44. package/src/components/interaction/Toast/Toast.native.tsx +202 -0
  45. package/src/components/interaction/form/Checkbox/Checkbox.native.stories.tsx +2 -4
  46. package/src/components/interaction/form/Checkbox/Checkbox.native.tsx +1 -1
  47. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.mdx +78 -0
  48. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.spec.tsx +57 -0
  49. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.stories.tsx +96 -0
  50. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.tsx +61 -0
  51. package/src/components/interaction/form/Combobox/Combobox.module.css +1 -1
  52. package/src/components/interaction/form/Combobox/Combobox.native.mdx +86 -0
  53. package/src/components/interaction/form/Combobox/Combobox.native.spec.tsx +60 -0
  54. package/src/components/interaction/form/Combobox/Combobox.native.stories.tsx +99 -0
  55. package/src/components/interaction/form/Combobox/Combobox.native.tsx +211 -0
  56. package/src/components/interaction/form/FileInput/FileInput.native.mdx +86 -0
  57. package/src/components/interaction/form/FileInput/FileInput.native.spec.tsx +73 -0
  58. package/src/components/interaction/form/FileInput/FileInput.native.stories.tsx +69 -0
  59. package/src/components/interaction/form/FileInput/FileInput.native.tsx +129 -0
  60. package/src/components/interaction/form/FormInput/FormInput.module.css +1 -1
  61. package/src/components/interaction/form/FormInput/FormInput.native.mdx +81 -0
  62. package/src/components/interaction/form/FormInput/FormInput.native.spec.tsx +51 -0
  63. package/src/components/interaction/form/FormInput/FormInput.native.stories.tsx +54 -0
  64. package/src/components/interaction/form/FormInput/FormInput.native.tsx +122 -0
  65. package/src/components/interaction/form/NumberInput/NumberInput.module.css +1 -1
  66. package/src/components/interaction/form/NumberInput/NumberInput.native.mdx +87 -0
  67. package/src/components/interaction/form/NumberInput/NumberInput.native.spec.tsx +63 -0
  68. package/src/components/interaction/form/NumberInput/NumberInput.native.stories.tsx +63 -0
  69. package/src/components/interaction/form/NumberInput/NumberInput.native.tsx +198 -0
  70. package/src/components/interaction/form/Radio/Radio.native.spec.tsx +79 -0
  71. package/src/components/interaction/form/Radio/Radio.native.stories.tsx +12 -14
  72. package/src/components/interaction/form/Radio/Radio.native.tsx +1 -1
  73. package/src/components/interaction/form/RadioGroup/RadioGroup.native.mdx +79 -0
  74. package/src/components/interaction/form/RadioGroup/RadioGroup.native.spec.tsx +66 -0
  75. package/src/components/interaction/form/RadioGroup/RadioGroup.native.stories.tsx +96 -0
  76. package/src/components/interaction/form/RadioGroup/RadioGroup.native.tsx +61 -0
  77. package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +5 -5
  78. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.mdx +78 -0
  79. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.spec.tsx +82 -0
  80. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.stories.tsx +99 -0
  81. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.tsx +121 -0
  82. package/src/components/interaction/form/Select/Select.module.css +1 -1
  83. package/src/components/interaction/form/Select/Select.native.mdx +85 -0
  84. package/src/components/interaction/form/Select/Select.native.spec.tsx +64 -0
  85. package/src/components/interaction/form/Select/Select.native.stories.tsx +75 -0
  86. package/src/components/interaction/form/Select/Select.native.tsx +252 -0
  87. package/src/components/interaction/form/Slider/Slider.module.css +4 -4
  88. package/src/components/interaction/form/Slider/Slider.native.mdx +83 -0
  89. package/src/components/interaction/form/Slider/Slider.native.spec.tsx +51 -0
  90. package/src/components/interaction/form/Slider/Slider.native.stories.tsx +58 -0
  91. package/src/components/interaction/form/Slider/Slider.native.tsx +188 -0
  92. package/src/components/interaction/form/Switch/Switch.module.css +3 -3
  93. package/src/components/interaction/form/Switch/Switch.native.stories.tsx +2 -4
  94. package/src/components/interaction/form/Textarea/Textarea.native.mdx +76 -0
  95. package/src/components/interaction/form/Textarea/Textarea.native.spec.tsx +50 -0
  96. package/src/components/interaction/form/Textarea/Textarea.native.stories.tsx +50 -0
  97. package/src/components/interaction/form/Textarea/Textarea.native.tsx +108 -0
  98. package/src/components/interaction/form/atoms/Label.native.spec.tsx +39 -0
  99. package/src/components/interaction/form/atoms/Label.native.stories.tsx +1 -1
  100. package/src/components/interaction/form/atoms/Message.native.stories.tsx +1 -1
  101. package/src/components/layout/ButtonGroup/ButtonGroup.native.spec.tsx +83 -0
  102. package/src/components/layout/ButtonGroup/ButtonGroup.native.stories.tsx +13 -19
  103. package/src/components/layout/Card/Card.native.spec.tsx +102 -0
  104. package/src/components/layout/Card/Card.native.stories.tsx +9 -11
  105. package/src/components/layout/Card/Card.native.tsx +1 -1
  106. package/src/components/layout/IconWrapper/IconWrapper.module.css +1 -1
  107. package/src/components/layout/IconWrapper/IconWrapper.native.stories.tsx +7 -11
  108. package/src/components/layout/SectionHeader/SectionHeader.native.spec.tsx +51 -0
  109. package/src/components/layout/SectionHeader/SectionHeader.native.stories.tsx +6 -8
  110. package/src/components/ui/Accordion/Accordion.native.mdx +65 -0
  111. package/src/components/ui/Accordion/Accordion.native.spec.tsx +69 -0
  112. package/src/components/ui/Accordion/Accordion.native.stories.tsx +52 -0
  113. package/src/components/ui/Accordion/Accordion.native.tsx +141 -0
  114. package/src/components/ui/Alert/Alert.module.css +1 -1
  115. package/src/components/ui/Alert/Alert.native.spec.tsx +76 -0
  116. package/src/components/ui/Alert/Alert.native.stories.tsx +8 -10
  117. package/src/components/ui/Alert/Alert.native.tsx +7 -7
  118. package/src/components/ui/Avatar/Avatar.module.css +1 -1
  119. package/src/components/ui/Avatar/Avatar.native.stories.tsx +6 -8
  120. package/src/components/ui/Avatar/Avatar.native.tsx +2 -2
  121. package/src/components/ui/Badge/Badge.module.css +1 -1
  122. package/src/components/ui/Badge/Badge.native.spec.tsx +45 -0
  123. package/src/components/ui/Badge/Badge.native.stories.tsx +6 -8
  124. package/src/components/ui/Badge/Badge.native.tsx +2 -2
  125. package/src/components/ui/Breadcrumb/Breadcrumb.native.mdx +52 -0
  126. package/src/components/ui/Breadcrumb/Breadcrumb.native.spec.tsx +49 -0
  127. package/src/components/ui/Breadcrumb/Breadcrumb.native.stories.tsx +46 -0
  128. package/src/components/ui/Breadcrumb/Breadcrumb.native.tsx +83 -0
  129. package/src/components/ui/Calendar/Calendar.module.css +2 -2
  130. package/src/components/ui/Calendar/Calendar.native.mdx +58 -0
  131. package/src/components/ui/Calendar/Calendar.native.spec.tsx +92 -0
  132. package/src/components/ui/Calendar/Calendar.native.stories.tsx +45 -0
  133. package/src/components/ui/Calendar/Calendar.native.tsx +294 -0
  134. package/src/components/ui/Carousel/Carousel.module.css +1 -1
  135. package/src/components/ui/Carousel/Carousel.native.mdx +55 -0
  136. package/src/components/ui/Carousel/Carousel.native.spec.tsx +55 -0
  137. package/src/components/ui/Carousel/Carousel.native.stories.tsx +59 -0
  138. package/src/components/ui/Carousel/Carousel.native.tsx +184 -0
  139. package/src/components/ui/Collapsible/Collapsible.native.mdx +53 -0
  140. package/src/components/ui/Collapsible/Collapsible.native.spec.tsx +68 -0
  141. package/src/components/ui/Collapsible/Collapsible.native.stories.tsx +48 -0
  142. package/src/components/ui/Collapsible/Collapsible.native.tsx +139 -0
  143. package/src/components/ui/DescriptionList/DescriptionList.native.mdx +50 -0
  144. package/src/components/ui/DescriptionList/DescriptionList.native.spec.tsx +37 -0
  145. package/src/components/ui/DescriptionList/DescriptionList.native.stories.tsx +48 -0
  146. package/src/components/ui/DescriptionList/DescriptionList.native.tsx +54 -0
  147. package/src/components/ui/Link/Link.native.mdx +53 -0
  148. package/src/components/ui/Link/Link.native.spec.tsx +35 -0
  149. package/src/components/ui/Link/Link.native.stories.tsx +46 -0
  150. package/src/components/ui/Link/Link.native.tsx +48 -0
  151. package/src/components/ui/Loading/Loading.module.css +1 -1
  152. package/src/components/ui/Loading/Loading.native.stories.tsx +6 -8
  153. package/src/components/ui/NotificationBanner/NotificationBanner.module.css +1 -1
  154. package/src/components/ui/NotificationBanner/NotificationBanner.native.spec.tsx +85 -0
  155. package/src/components/ui/NotificationBanner/NotificationBanner.native.stories.tsx +8 -10
  156. package/src/components/ui/NotificationBanner/NotificationBanner.native.tsx +7 -7
  157. package/src/components/ui/Pagination/Pagination.module.css +5 -1
  158. package/src/components/ui/Pagination/Pagination.native.mdx +62 -0
  159. package/src/components/ui/Pagination/Pagination.native.spec.tsx +86 -0
  160. package/src/components/ui/Pagination/Pagination.native.stories.tsx +47 -0
  161. package/src/components/ui/Pagination/Pagination.native.tsx +157 -0
  162. package/src/components/ui/Progress/Progress.module.css +2 -2
  163. package/src/components/ui/Progress/Progress.native.spec.tsx +56 -0
  164. package/src/components/ui/Progress/Progress.native.stories.tsx +1 -1
  165. package/src/components/ui/Progress/Progress.native.tsx +2 -2
  166. package/src/components/ui/ProgressCircle/ProgressCircle.native.mdx +63 -0
  167. package/src/components/ui/ProgressCircle/ProgressCircle.native.spec.tsx +39 -0
  168. package/src/components/ui/ProgressCircle/ProgressCircle.native.stories.tsx +36 -0
  169. package/src/components/ui/ProgressCircle/ProgressCircle.native.tsx +176 -0
  170. package/src/components/ui/Separator/Separator.native.spec.tsx +39 -0
  171. package/src/components/ui/Separator/Separator.native.stories.tsx +10 -14
  172. package/src/components/ui/Skeleton/Skeleton.native.spec.tsx +39 -0
  173. package/src/components/ui/Skeleton/Skeleton.native.stories.tsx +6 -8
  174. package/src/components/ui/Skeleton/Skeleton.native.tsx +2 -1
  175. package/src/components/ui/Table/Table.native.mdx +72 -0
  176. package/src/components/ui/Table/Table.native.spec.tsx +83 -0
  177. package/src/components/ui/Table/Table.native.stories.tsx +55 -0
  178. package/src/components/ui/Table/Table.native.tsx +196 -0
  179. package/src/components/ui/Tabs/Tabs.native.mdx +65 -0
  180. package/src/components/ui/Tabs/Tabs.native.spec.tsx +65 -0
  181. package/src/components/ui/Tabs/Tabs.native.stories.tsx +57 -0
  182. package/src/components/ui/Tabs/Tabs.native.tsx +133 -0
  183. package/src/components/ui/Tooltip/Tooltip.module.css +1 -1
  184. package/src/components/ui/Tooltip/Tooltip.native.mdx +47 -0
  185. package/src/components/ui/Tooltip/Tooltip.native.spec.tsx +37 -0
  186. package/src/components/ui/Tooltip/Tooltip.native.stories.tsx +38 -0
  187. package/src/components/ui/Tooltip/Tooltip.native.tsx +57 -0
  188. package/src/components/ui/Typography/Typography.native.spec.tsx +49 -0
  189. package/src/components/ui/Typography/Typography.native.stories.tsx +8 -10
  190. package/src/native/ThemeContext.tsx +3 -3
  191. package/src/native.ts +37 -0
  192. package/src/stories/ReactNative.mdx +48 -13
  193. package/src/web-components/interaction/BdsAccordion.stories.tsx +1 -1
  194. package/src/web-components/interaction/BdsCollapsible.stories.tsx +1 -1
  195. package/src/web-components/interaction/BdsSkipLink.stories.tsx +1 -1
  196. package/src/web-components/interaction/BdsTabs.stories.tsx +1 -1
  197. package/src/web-components/interaction/BdsTooltip.stories.tsx +1 -1
  198. package/src/web-components/interaction/form/BdsCheckbox.stories.tsx +1 -1
  199. package/src/web-components/interaction/form/BdsCheckboxGroup.stories.tsx +1 -1
  200. package/src/web-components/interaction/form/BdsCombobox.stories.tsx +1 -1
  201. package/src/web-components/interaction/form/BdsFileInput.stories.tsx +1 -1
  202. package/src/web-components/interaction/form/BdsFormInput.stories.tsx +1 -1
  203. package/src/web-components/interaction/form/BdsNumberInput.stories.tsx +1 -1
  204. package/src/web-components/interaction/form/BdsRadio.stories.tsx +1 -1
  205. package/src/web-components/interaction/form/BdsRadioGroup.stories.tsx +1 -1
  206. package/src/web-components/interaction/form/BdsSegmentedControl.stories.tsx +1 -1
  207. package/src/web-components/interaction/form/BdsSelect.stories.tsx +1 -1
  208. package/src/web-components/interaction/form/BdsSlider.stories.tsx +1 -1
  209. package/src/web-components/interaction/form/BdsSwitch.stories.tsx +1 -1
  210. package/src/web-components/interaction/form/BdsTextarea.stories.tsx +1 -1
  211. package/src/web-components/ui/BdsButtonGroup.stories.tsx +1 -1
  212. package/src/web-components/ui/BdsCard.stories.tsx +1 -1
  213. package/src/web-components/ui/BdsIconWrapper.stories.tsx +1 -1
  214. package/src/web-components/ui/BdsRating.stories.tsx +1 -1
  215. package/src/web-components/ui/BdsSectionHeader.stories.tsx +1 -1
@@ -0,0 +1,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
+ }
@@ -26,7 +26,7 @@
26
26
  appearance: none;
27
27
  inline-size: 100%;
28
28
  block-size: var(--slider_track-height);
29
- border-radius: 999px;
29
+ border-radius: var(--slider_track-radius, var(--bds-border_radius--full));
30
30
  background: linear-gradient(
31
31
  to right,
32
32
  var(--slider_color-active, var(--bdc-color_active)) 0%,
@@ -41,7 +41,7 @@
41
41
  .slider:focus-visible {
42
42
  outline: var(--bds-outline_default);
43
43
  outline-offset: var(--bds-outline_offset);
44
- border-radius: 999px;
44
+ border-radius: var(--slider_track-radius, var(--bds-border_radius--full));
45
45
  }
46
46
 
47
47
  .slider:disabled {
@@ -54,7 +54,7 @@
54
54
  appearance: none;
55
55
  inline-size: var(--slider_thumb-size);
56
56
  block-size: var(--slider_thumb-size);
57
- border-radius: 50%;
57
+ border-radius: var(--slider_thumb-radius, var(--bds-border_radius--circle));
58
58
  background-color: var(--slider_color-active, var(--bdc-color_active));
59
59
  box-shadow: var(--bds-shadow_s);
60
60
  transition: var(--bds-animation_transition);
@@ -66,7 +66,7 @@
66
66
  inline-size: var(--slider_thumb-size);
67
67
  block-size: var(--slider_thumb-size);
68
68
  border: none;
69
- border-radius: 50%;
69
+ border-radius: var(--slider_thumb-radius, var(--bds-border_radius--circle));
70
70
  background-color: var(--slider_color-active, var(--bdc-color_active));
71
71
  box-shadow: var(--bds-shadow_s);
72
72
  cursor: pointer;
@@ -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
+ }
@@ -52,7 +52,7 @@
52
52
  align-items: center;
53
53
  inline-size: var(--switch_track-width);
54
54
  block-size: var(--switch_track-height);
55
- border-radius: 999px;
55
+ border-radius: var(--switch_track-radius, var(--bds-border_radius--full));
56
56
  background-color: var(--switch_track-bg, var(--bds-color_grey--subtle));
57
57
  padding-inline: var(--switch_track-pad);
58
58
  pointer-events: none;
@@ -69,7 +69,7 @@
69
69
  display: block;
70
70
  inline-size: var(--switch_thumb-size);
71
71
  block-size: var(--switch_thumb-size);
72
- border-radius: 50%;
72
+ border-radius: var(--switch_thumb-radius, var(--bds-border_radius--circle));
73
73
  background-color: var(--switch_thumb-bg, var(--bds-color_interactive));
74
74
  box-shadow: var(--bds-shadow_s);
75
75
  transition: var(--bds-animation_transition);
@@ -97,7 +97,7 @@
97
97
  .switch:focus-visible + .track {
98
98
  outline: var(--bds-outline_default);
99
99
  outline-offset: var(--bds-outline_offset);
100
- border-radius: 999px;
100
+ border-radius: var(--switch_track-radius, var(--bds-border_radius--full));
101
101
  }
102
102
 
103
103
  /* Disabled state */