@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,294 @@
1
+ import { useState } from 'react';
2
+ import {
3
+ StyleSheet,
4
+ View,
5
+ Text,
6
+ Pressable,
7
+ StyleProp,
8
+ ViewStyle,
9
+ } from 'react-native';
10
+ import { spacing, font, border, lineHeight } from '../../../native/tokens';
11
+ import { useTheme } from '../../../native/ThemeContext';
12
+
13
+ const DAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
14
+ const DAY_LABELS: Record<string, string> = {
15
+ Su: 'Sunday', Mo: 'Monday', Tu: 'Tuesday', We: 'Wednesday',
16
+ Th: 'Thursday', Fr: 'Friday', Sa: 'Saturday',
17
+ };
18
+ const MONTHS = [
19
+ 'January', 'February', 'March', 'April', 'May', 'June',
20
+ 'July', 'August', 'September', 'October', 'November', 'December',
21
+ ];
22
+
23
+ interface CalendarProps {
24
+ value?: Date;
25
+ defaultValue?: Date;
26
+ min?: Date;
27
+ max?: Date;
28
+ onChange?: (date: Date) => void;
29
+ style?: StyleProp<ViewStyle>;
30
+ }
31
+
32
+ function isSameDay(a: Date, b: Date): boolean {
33
+ return (
34
+ a.getFullYear() === b.getFullYear() &&
35
+ a.getMonth() === b.getMonth() &&
36
+ a.getDate() === b.getDate()
37
+ );
38
+ }
39
+
40
+ function toMidnight(date: Date): Date {
41
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
42
+ }
43
+
44
+ function isOutOfRange(date: Date, min?: Date, max?: Date): boolean {
45
+ const d = toMidnight(date);
46
+ if (min && d < toMidnight(min)) return true;
47
+ return !!(max && d > toMidnight(max));
48
+ }
49
+
50
+ function getDaysInMonth(year: number, month: number): number {
51
+ return new Date(year, month + 1, 0).getDate();
52
+ }
53
+
54
+ function getFirstDayOfMonth(year: number, month: number): number {
55
+ return new Date(year, month, 1).getDay();
56
+ }
57
+
58
+ const DAY_SIZE = 44;
59
+
60
+ const styles = StyleSheet.create({
61
+ container: {
62
+ alignSelf: 'flex-start',
63
+ },
64
+ header: {
65
+ flexDirection: 'row',
66
+ alignItems: 'center',
67
+ justifyContent: 'space-between',
68
+ paddingVertical: spacing.xs,
69
+ paddingHorizontal: spacing.xxs,
70
+ },
71
+ navButton: {
72
+ minWidth: DAY_SIZE,
73
+ minHeight: DAY_SIZE,
74
+ alignItems: 'center',
75
+ justifyContent: 'center',
76
+ borderRadius: border.radius.full,
77
+ },
78
+ navText: {
79
+ fontSize: font.size.heading3,
80
+ fontWeight: font.weight.semibold,
81
+ },
82
+ monthYear: {
83
+ fontSize: font.size.body,
84
+ fontFamily: font.family.body,
85
+ fontWeight: font.weight.semibold,
86
+ lineHeight: lineHeight.body,
87
+ },
88
+ weekRow: {
89
+ flexDirection: 'row',
90
+ },
91
+ weekday: {
92
+ width: DAY_SIZE,
93
+ height: DAY_SIZE,
94
+ alignItems: 'center',
95
+ justifyContent: 'center',
96
+ },
97
+ weekdayText: {
98
+ fontSize: font.size.bodyS,
99
+ fontFamily: font.family.body,
100
+ fontWeight: font.weight.semibold,
101
+ lineHeight: lineHeight.bodyS,
102
+ },
103
+ grid: {
104
+ gap: spacing.xxxs,
105
+ },
106
+ dayRow: {
107
+ flexDirection: 'row',
108
+ },
109
+ dayCell: {
110
+ width: DAY_SIZE,
111
+ height: DAY_SIZE,
112
+ alignItems: 'center',
113
+ justifyContent: 'center',
114
+ borderRadius: border.radius.full,
115
+ },
116
+ dayText: {
117
+ fontSize: font.size.body,
118
+ fontFamily: font.family.body,
119
+ lineHeight: lineHeight.body,
120
+ },
121
+ today: {
122
+ borderWidth: 1,
123
+ },
124
+ pressed: {
125
+ opacity: 0.75,
126
+ },
127
+ disabled: {
128
+ opacity: 0.4,
129
+ },
130
+ });
131
+
132
+ export function Calendar({
133
+ value,
134
+ defaultValue,
135
+ min,
136
+ max,
137
+ onChange,
138
+ style,
139
+ }: Readonly<CalendarProps>) {
140
+ const { colors } = useTheme();
141
+ const today = new Date();
142
+ const controlled = value !== undefined;
143
+ const [internal, setInternal] = useState<Date | undefined>(defaultValue);
144
+ const selected = controlled ? value : internal;
145
+
146
+ const [viewYear, setViewYear] = useState(
147
+ selected?.getFullYear() ?? today.getFullYear(),
148
+ );
149
+ const [viewMonth, setViewMonth] = useState(
150
+ selected?.getMonth() ?? today.getMonth(),
151
+ );
152
+
153
+ const daysInMonth = getDaysInMonth(viewYear, viewMonth);
154
+ const firstDay = getFirstDayOfMonth(viewYear, viewMonth);
155
+
156
+ const navigate = (delta: number) => {
157
+ const d = new Date(viewYear, viewMonth + delta, 1);
158
+ setViewYear(d.getFullYear());
159
+ setViewMonth(d.getMonth());
160
+ };
161
+
162
+ const selectDay = (day: number) => {
163
+ const date = new Date(viewYear, viewMonth, day);
164
+ if (isOutOfRange(date, min, max)) return;
165
+ if (!controlled) setInternal(date);
166
+ onChange?.(date);
167
+ };
168
+
169
+ // Build grid cells
170
+ const cells: Array<{ day: number } | null> = [
171
+ ...Array.from({ length: firstDay }, () => null),
172
+ ...Array.from({ length: daysInMonth }, (_, i) => ({ day: i + 1 })),
173
+ ];
174
+ while (cells.length % 7 !== 0) cells.push(null);
175
+
176
+ // Build rows
177
+ const rows = Array.from(
178
+ { length: cells.length / 7 },
179
+ (_, i) => cells.slice(i * 7, i * 7 + 7),
180
+ );
181
+
182
+ return (
183
+ <View
184
+ style={[styles.container, style]}
185
+ accessibilityRole="none"
186
+ accessibilityLabel={`${MONTHS[viewMonth]} ${viewYear} calendar`}
187
+ >
188
+ <View style={styles.header}>
189
+ <Pressable
190
+ onPress={() => navigate(-1)}
191
+ accessibilityRole="button"
192
+ accessibilityLabel="Previous month"
193
+ style={({ pressed }) => [styles.navButton, pressed && styles.pressed]}
194
+ >
195
+ <Text style={[styles.navText, { color: colors.colorOnBg }]}>‹</Text>
196
+ </Pressable>
197
+
198
+ <Text
199
+ style={[styles.monthYear, { color: colors.colorOnBg }]}
200
+ accessibilityRole="text"
201
+ accessibilityLiveRegion="polite"
202
+ >
203
+ {MONTHS[viewMonth]} {viewYear}
204
+ </Text>
205
+
206
+ <Pressable
207
+ onPress={() => navigate(1)}
208
+ accessibilityRole="button"
209
+ accessibilityLabel="Next month"
210
+ style={({ pressed }) => [styles.navButton, pressed && styles.pressed]}
211
+ >
212
+ <Text style={[styles.navText, { color: colors.colorOnBg }]}>›</Text>
213
+ </Pressable>
214
+ </View>
215
+
216
+ <View style={styles.weekRow}>
217
+ {DAYS.map(d => (
218
+ <View key={d} style={styles.weekday}>
219
+ <Text
220
+ style={[styles.weekdayText, { color: colors.colorOnBgSubtle }]}
221
+ accessibilityLabel={DAY_LABELS[d]}
222
+ >
223
+ {d}
224
+ </Text>
225
+ </View>
226
+ ))}
227
+ </View>
228
+
229
+ <View style={styles.grid}>
230
+ {rows.map((row, rowIndex) => (
231
+ <View key={rowIndex} style={styles.dayRow}>
232
+ {row.map((cell, colIndex) => {
233
+ if (!cell) {
234
+ return <View key={colIndex} style={styles.dayCell} />;
235
+ }
236
+
237
+ const date = new Date(viewYear, viewMonth, cell.day);
238
+ const isSelected = selected ? isSameDay(date, selected) : false;
239
+ const isToday = isSameDay(date, today);
240
+ const disabled = isOutOfRange(date, min, max);
241
+
242
+ return (
243
+ <Pressable
244
+ key={colIndex}
245
+ onPress={() => selectDay(cell.day)}
246
+ disabled={disabled}
247
+ accessibilityRole="button"
248
+ accessibilityLabel={date.toLocaleDateString('en', {
249
+ month: 'long',
250
+ day: 'numeric',
251
+ year: 'numeric',
252
+ })}
253
+ accessibilityState={{
254
+ selected: isSelected,
255
+ disabled,
256
+ }}
257
+ style={({ pressed }) => [
258
+ styles.dayCell,
259
+ isSelected && {
260
+ backgroundColor: colors.colorInteractive,
261
+ },
262
+ isToday &&
263
+ !isSelected && [
264
+ styles.today,
265
+ { borderColor: colors.colorInteractive },
266
+ ],
267
+ disabled && styles.disabled,
268
+ pressed && !disabled && styles.pressed,
269
+ ]}
270
+ >
271
+ <Text
272
+ style={[
273
+ styles.dayText,
274
+ {
275
+ color: isSelected
276
+ ? colors.colorOnInteractive
277
+ : colors.colorOnBg,
278
+ fontWeight: isToday
279
+ ? font.weight.semibold
280
+ : font.weight.body,
281
+ },
282
+ ]}
283
+ >
284
+ {cell.day}
285
+ </Text>
286
+ </Pressable>
287
+ );
288
+ })}
289
+ </View>
290
+ ))}
291
+ </View>
292
+ </View>
293
+ );
294
+ }
@@ -0,0 +1,55 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './Carousel.native.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # Carousel (React Native)
7
+
8
+ React Native carousel component using a horizontally paging ScrollView with dot indicators and Previous/Next navigation. Tracks the active slide via scroll position.
9
+
10
+ > **Status: alpha** -- API may change before stable release.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { Carousel } from '@boostdev/components';
16
+ import { View, Text } from 'react-native';
17
+
18
+ <Carousel
19
+ label="Product images"
20
+ items={[
21
+ <View><Text>Slide 1</Text></View>,
22
+ <View><Text>Slide 2</Text></View>,
23
+ <View><Text>Slide 3</Text></View>,
24
+ ]}
25
+ />
26
+ ```
27
+
28
+ ## Examples
29
+
30
+ ### Default (5 slides)
31
+ <Canvas of={Stories.Default} />
32
+
33
+ ### Two slides
34
+ <Canvas of={Stories.TwoSlides} />
35
+
36
+ ### Single slide
37
+ <Canvas of={Stories.SingleSlide} />
38
+
39
+ ## Props
40
+
41
+ <ArgTypes of={Stories} />
42
+
43
+ ## Props reference
44
+
45
+ | Prop | Type | Default | Description |
46
+ |------|------|---------|-------------|
47
+ | `items` | `ReactNode[]` | -- | Array of slide content |
48
+ | `label` | `string` | -- | Accessible label for the carousel |
49
+ | `style` | `StyleProp<ViewStyle>` | -- | Custom style for the container |
50
+
51
+ ## Accessibility
52
+
53
+ - Container has an accessible label describing the carousel
54
+ - Previous/Next buttons have descriptive labels and disabled states at bounds
55
+ - Dot indicators have `button` role with `selected` state and descriptive labels (e.g. "Go to slide 2")
@@ -0,0 +1,55 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../native/ThemeContext';
3
+ import { Carousel } from './Carousel.native';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ describe('Carousel (native)', () => {
10
+ const items = [
11
+ <span key="1">Slide 1</span>,
12
+ <span key="2">Slide 2</span>,
13
+ <span key="3">Slide 3</span>,
14
+ ];
15
+
16
+ it('renders all slide items', () => {
17
+ renderWithTheme(<Carousel items={items} label="Test carousel" />);
18
+ expect(screen.getByText('Slide 1')).toBeInTheDocument();
19
+ expect(screen.getByText('Slide 2')).toBeInTheDocument();
20
+ expect(screen.getByText('Slide 3')).toBeInTheDocument();
21
+ });
22
+
23
+ it('renders Previous and Next buttons', () => {
24
+ renderWithTheme(<Carousel items={items} label="Test carousel" />);
25
+ expect(screen.getByRole('button', { name: 'Previous' })).toBeInTheDocument();
26
+ expect(screen.getByRole('button', { name: 'Next' })).toBeInTheDocument();
27
+ });
28
+
29
+ it('renders dot indicators for each slide', () => {
30
+ renderWithTheme(<Carousel items={items} label="Test carousel" />);
31
+ expect(screen.getByRole('button', { name: 'Go to slide 1' })).toBeInTheDocument();
32
+ expect(screen.getByRole('button', { name: 'Go to slide 2' })).toBeInTheDocument();
33
+ expect(screen.getByRole('button', { name: 'Go to slide 3' })).toBeInTheDocument();
34
+ });
35
+
36
+ it('has accessible label on the container', () => {
37
+ renderWithTheme(<Carousel items={items} label="Product images" />);
38
+ expect(screen.getByLabelText('Product images')).toBeInTheDocument();
39
+ });
40
+
41
+ it('disables Previous button at the first slide', () => {
42
+ renderWithTheme(<Carousel items={items} label="Test carousel" />);
43
+ expect(screen.getByRole('button', { name: 'Previous' })).toHaveAttribute(
44
+ 'aria-disabled',
45
+ 'true',
46
+ );
47
+ });
48
+
49
+ it('renders a single slide without errors', () => {
50
+ renderWithTheme(
51
+ <Carousel items={[<span key="1">Only slide</span>]} label="Single" />,
52
+ );
53
+ expect(screen.getByText('Only slide')).toBeInTheDocument();
54
+ });
55
+ });
@@ -0,0 +1,59 @@
1
+ import React from 'react';
2
+ import { View, Text, StyleSheet } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react';
4
+ import { ThemeProvider } from '../../../native/ThemeContext';
5
+ import { Carousel } from './Carousel.native';
6
+
7
+ const slideStyles = StyleSheet.create({
8
+ slide: {
9
+ height: 200,
10
+ alignItems: 'center',
11
+ justifyContent: 'center',
12
+ borderRadius: 12,
13
+ },
14
+ text: {
15
+ fontSize: 24,
16
+ fontWeight: '600',
17
+ color: '#fff',
18
+ },
19
+ });
20
+
21
+ const slideColors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'];
22
+
23
+ const sampleItems = slideColors.map((color, i) => (
24
+ <View key={i} style={[slideStyles.slide, { backgroundColor: color }]}>
25
+ <Text style={slideStyles.text}>Slide {i + 1}</Text>
26
+ </View>
27
+ ));
28
+
29
+ const meta = {
30
+ tags: ['!stable', 'alpha'],
31
+ title: 'React Native/UI/Carousel',
32
+ component: Carousel,
33
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
34
+ parameters: { layout: 'padded' },
35
+ } satisfies Meta<typeof Carousel>;
36
+
37
+ export default meta;
38
+ type Story = StoryObj<typeof meta>;
39
+
40
+ export const Default: Story = {
41
+ args: {
42
+ items: sampleItems,
43
+ label: 'Demo carousel',
44
+ },
45
+ };
46
+
47
+ export const TwoSlides: Story = {
48
+ args: {
49
+ items: sampleItems.slice(0, 2),
50
+ label: 'Two-slide carousel',
51
+ },
52
+ };
53
+
54
+ export const SingleSlide: Story = {
55
+ args: {
56
+ items: sampleItems.slice(0, 1),
57
+ label: 'Single-slide carousel',
58
+ },
59
+ };
@@ -0,0 +1,184 @@
1
+ import { ReactNode, useCallback, useRef, useState } from 'react';
2
+ import {
3
+ StyleSheet,
4
+ View,
5
+ Text,
6
+ Pressable,
7
+ ScrollView,
8
+ NativeSyntheticEvent,
9
+ NativeScrollEvent,
10
+ Dimensions,
11
+ StyleProp,
12
+ ViewStyle,
13
+ } from 'react-native';
14
+ import { spacing, border } from '../../../native/tokens';
15
+ import { useTheme } from '../../../native/ThemeContext';
16
+
17
+ interface CarouselProps {
18
+ items: ReactNode[];
19
+ label: string;
20
+ style?: StyleProp<ViewStyle>;
21
+ }
22
+
23
+ const DOT_SIZE = 8;
24
+ const DOT_HIT_SIZE = 44;
25
+
26
+ const styles = StyleSheet.create({
27
+ container: {
28
+ width: '100%',
29
+ },
30
+ scrollView: {
31
+ flexGrow: 0,
32
+ },
33
+ slide: {
34
+ justifyContent: 'center',
35
+ alignItems: 'center',
36
+ },
37
+ nav: {
38
+ flexDirection: 'row',
39
+ alignItems: 'center',
40
+ justifyContent: 'center',
41
+ gap: spacing.xs,
42
+ paddingVertical: spacing.s,
43
+ },
44
+ navButton: {
45
+ minWidth: 44,
46
+ minHeight: 44,
47
+ alignItems: 'center',
48
+ justifyContent: 'center',
49
+ borderRadius: border.radius.s,
50
+ },
51
+ navText: {
52
+ fontSize: 20,
53
+ fontWeight: '600',
54
+ },
55
+ dots: {
56
+ flexDirection: 'row',
57
+ alignItems: 'center',
58
+ gap: spacing.xs,
59
+ },
60
+ dotWrapper: {
61
+ minWidth: DOT_HIT_SIZE,
62
+ minHeight: DOT_HIT_SIZE,
63
+ alignItems: 'center',
64
+ justifyContent: 'center',
65
+ },
66
+ dot: {
67
+ width: DOT_SIZE,
68
+ height: DOT_SIZE,
69
+ borderRadius: DOT_SIZE / 2,
70
+ },
71
+ pressed: {
72
+ opacity: 0.75,
73
+ },
74
+ });
75
+
76
+ export function Carousel({
77
+ items,
78
+ label,
79
+ style,
80
+ }: Readonly<CarouselProps>) {
81
+ const { colors } = useTheme();
82
+ const scrollRef = useRef<ScrollView>(null);
83
+ const [activeIndex, setActiveIndex] = useState(0);
84
+ const [slideWidth, setSlideWidth] = useState(Dimensions.get('window').width);
85
+
86
+ const handleScroll = useCallback(
87
+ (e: NativeSyntheticEvent<NativeScrollEvent>) => {
88
+ const offset = e.nativeEvent.contentOffset.x;
89
+ const width = e.nativeEvent.layoutMeasurement.width;
90
+ if (width > 0) {
91
+ const index = Math.round(offset / width);
92
+ setActiveIndex(index);
93
+ }
94
+ },
95
+ [],
96
+ );
97
+
98
+ const scrollTo = (index: number) => {
99
+ const clamped = Math.max(0, Math.min(index, items.length - 1));
100
+ scrollRef.current?.scrollTo({ x: clamped * slideWidth, animated: true });
101
+ };
102
+
103
+ return (
104
+ <View
105
+ style={[styles.container, style]}
106
+ accessibilityRole="none"
107
+ accessibilityLabel={label}
108
+ >
109
+ <ScrollView
110
+ ref={scrollRef}
111
+ horizontal
112
+ pagingEnabled
113
+ showsHorizontalScrollIndicator={false}
114
+ onScroll={handleScroll}
115
+ scrollEventThrottle={16}
116
+ onLayout={e => setSlideWidth(e.nativeEvent.layout.width)}
117
+ style={styles.scrollView}
118
+ >
119
+ {items.map((item, index) => (
120
+ <View key={index} style={[styles.slide, { width: slideWidth }]}>
121
+ {item}
122
+ </View>
123
+ ))}
124
+ </ScrollView>
125
+
126
+ <View style={styles.nav}>
127
+ <Pressable
128
+ onPress={() => scrollTo(activeIndex - 1)}
129
+ disabled={activeIndex <= 0}
130
+ accessibilityRole="button"
131
+ accessibilityLabel="Previous"
132
+ accessibilityState={{ disabled: activeIndex <= 0 }}
133
+ style={({ pressed }) => [
134
+ styles.navButton,
135
+ activeIndex <= 0 && { opacity: 0.4 },
136
+ pressed && styles.pressed,
137
+ ]}
138
+ >
139
+ <Text style={[styles.navText, { color: colors.colorOnBg }]}>‹</Text>
140
+ </Pressable>
141
+
142
+ <View style={styles.dots}>
143
+ {items.map((_, index) => (
144
+ <Pressable
145
+ key={index}
146
+ onPress={() => scrollTo(index)}
147
+ accessibilityRole="button"
148
+ accessibilityLabel={`Go to slide ${index + 1}`}
149
+ accessibilityState={{ selected: activeIndex === index }}
150
+ style={styles.dotWrapper}
151
+ >
152
+ <View
153
+ style={[
154
+ styles.dot,
155
+ {
156
+ backgroundColor:
157
+ activeIndex === index
158
+ ? colors.colorInteractive
159
+ : colors.colorBgSubtle,
160
+ },
161
+ ]}
162
+ />
163
+ </Pressable>
164
+ ))}
165
+ </View>
166
+
167
+ <Pressable
168
+ onPress={() => scrollTo(activeIndex + 1)}
169
+ disabled={activeIndex >= items.length - 1}
170
+ accessibilityRole="button"
171
+ accessibilityLabel="Next"
172
+ accessibilityState={{ disabled: activeIndex >= items.length - 1 }}
173
+ style={({ pressed }) => [
174
+ styles.navButton,
175
+ activeIndex >= items.length - 1 && { opacity: 0.4 },
176
+ pressed && styles.pressed,
177
+ ]}
178
+ >
179
+ <Text style={[styles.navText, { color: colors.colorOnBg }]}>›</Text>
180
+ </Pressable>
181
+ </View>
182
+ </View>
183
+ );
184
+ }
@@ -0,0 +1,53 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './Collapsible.native.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # Collapsible (React Native)
7
+
8
+ React Native collapsible section with an animated chevron and smooth expand/collapse transitions. Supports both controlled and uncontrolled modes.
9
+
10
+ > **Status: alpha** -- API may change before stable release.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { Collapsible } from '@boostdev/components';
16
+
17
+ <Collapsible summary="More details">
18
+ <Text>Hidden content revealed on press.</Text>
19
+ </Collapsible>
20
+ ```
21
+
22
+ ## Examples
23
+
24
+ ### Default (collapsed)
25
+ <Canvas of={Stories.Default} />
26
+
27
+ ### Default open
28
+ <Canvas of={Stories.DefaultOpen} />
29
+
30
+ ### Multiple sections
31
+ <Canvas of={Stories.Multiple} />
32
+
33
+ ## Props
34
+
35
+ <ArgTypes of={Stories} />
36
+
37
+ ## Props reference
38
+
39
+ | Prop | Type | Default | Description |
40
+ |------|------|---------|-------------|
41
+ | `summary` | `string` | -- | The trigger label, always visible |
42
+ | `children` | `ReactNode` | -- | Content revealed when expanded |
43
+ | `open` | `boolean` | -- | Controlled open state |
44
+ | `defaultOpen` | `boolean` | `false` | Uncontrolled initial open state |
45
+ | `onToggle` | `(open: boolean) => void` | -- | Called after each toggle with the new open state |
46
+ | `name` | `string` | -- | Grouping name (informational in native) |
47
+ | `style` | `StyleProp<ViewStyle>` | -- | Custom style for the container |
48
+
49
+ ## Accessibility
50
+
51
+ - Header has `button` role with `expanded` state
52
+ - Chevron icon is hidden from the accessibility tree
53
+ - Content appears/disappears with LayoutAnimation for smooth transitions