@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,99 @@
1
+ import React, { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { ThemeProvider } from '../../../../native/ThemeContext';
4
+ import { Combobox } from './Combobox.native';
5
+ import type { ComboboxOption } from './Combobox.native';
6
+
7
+ const fruits: ComboboxOption[] = [
8
+ { value: 'apple', label: 'Apple' },
9
+ { value: 'banana', label: 'Banana' },
10
+ { value: 'cherry', label: 'Cherry' },
11
+ { value: 'date', label: 'Date' },
12
+ { value: 'elderberry', label: 'Elderberry' },
13
+ ];
14
+
15
+ const meta = {
16
+ tags: ['!stable', 'alpha'],
17
+ title: 'React Native/Form/Combobox',
18
+ component: Combobox,
19
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
20
+ parameters: { layout: 'padded' },
21
+ } satisfies Meta<typeof Combobox>;
22
+
23
+ export default meta;
24
+ type Story = StoryObj<typeof meta>;
25
+
26
+ export const Default: Story = {
27
+ args: {
28
+ label: 'Fruit',
29
+ name: 'fruit',
30
+ options: fruits,
31
+ placeholder: 'Search fruit...',
32
+ },
33
+ };
34
+
35
+ export const WithValue: Story = {
36
+ args: {
37
+ label: 'Fruit',
38
+ name: 'fruit',
39
+ options: fruits,
40
+ value: 'cherry',
41
+ },
42
+ };
43
+
44
+ export const Disabled: Story = {
45
+ args: {
46
+ label: 'Fruit',
47
+ name: 'fruit',
48
+ options: fruits,
49
+ disabled: true,
50
+ },
51
+ };
52
+
53
+ export const WithError: Story = {
54
+ args: {
55
+ label: 'Fruit',
56
+ name: 'fruit',
57
+ options: fruits,
58
+ error: 'Please select a fruit.',
59
+ },
60
+ };
61
+
62
+ export const WithHint: Story = {
63
+ args: {
64
+ label: 'Fruit',
65
+ name: 'fruit',
66
+ options: fruits,
67
+ hint: 'Type to filter the list.',
68
+ },
69
+ };
70
+
71
+ export const WithDisabledOption: Story = {
72
+ args: {
73
+ label: 'Fruit',
74
+ name: 'fruit',
75
+ options: [
76
+ { value: 'apple', label: 'Apple' },
77
+ { value: 'banana', label: 'Banana', disabled: true },
78
+ { value: 'cherry', label: 'Cherry' },
79
+ ],
80
+ },
81
+ };
82
+
83
+ function InteractiveComponent() {
84
+ const [value, setValue] = useState('');
85
+ return (
86
+ <Combobox
87
+ label="Fruit"
88
+ name="fruit"
89
+ options={fruits}
90
+ placeholder="Search fruit..."
91
+ value={value}
92
+ onChange={setValue}
93
+ />
94
+ );
95
+ }
96
+
97
+ export const Interactive: Story = {
98
+ render: () => <InteractiveComponent />,
99
+ };
@@ -0,0 +1,211 @@
1
+ import { useState, useMemo } from 'react';
2
+ import {
3
+ StyleSheet,
4
+ View,
5
+ TextInput,
6
+ Pressable,
7
+ Text,
8
+ ScrollView,
9
+ StyleProp,
10
+ ViewStyle,
11
+ } from 'react-native';
12
+ import { spacing, font, border, lineHeight } from '../../../../native/tokens';
13
+ import { useTheme } from '../../../../native/ThemeContext';
14
+ import { Label } from '../atoms/Label.native';
15
+ import { Message } from '../atoms/Message.native';
16
+
17
+ export interface ComboboxOption {
18
+ value: string;
19
+ label: string;
20
+ disabled?: boolean;
21
+ }
22
+
23
+ interface ComboboxProps {
24
+ label: string;
25
+ name: string;
26
+ options: ComboboxOption[];
27
+ placeholder?: string;
28
+ value?: string;
29
+ onChange?: (value: string) => void;
30
+ disabled?: boolean;
31
+ error?: string;
32
+ hint?: string;
33
+ style?: StyleProp<ViewStyle>;
34
+ }
35
+
36
+ const MAX_DROPDOWN_HEIGHT = 200;
37
+
38
+ const styles = StyleSheet.create({
39
+ container: {
40
+ gap: spacing.xxs,
41
+ },
42
+ inputWrapper: {
43
+ position: 'relative',
44
+ },
45
+ input: {
46
+ fontFamily: font.family.body,
47
+ fontSize: font.size.body,
48
+ lineHeight: lineHeight.body,
49
+ paddingVertical: spacing.xs,
50
+ paddingHorizontal: spacing.s,
51
+ borderWidth: 1,
52
+ borderRadius: border.radius.full,
53
+ minHeight: 44,
54
+ },
55
+ dropdown: {
56
+ position: 'absolute',
57
+ top: '100%',
58
+ start: 0,
59
+ end: 0,
60
+ maxHeight: MAX_DROPDOWN_HEIGHT,
61
+ borderWidth: 1,
62
+ borderTopWidth: 0,
63
+ borderBottomStartRadius: border.radius.xs,
64
+ borderBottomEndRadius: border.radius.xs,
65
+ zIndex: 10,
66
+ },
67
+ option: {
68
+ paddingVertical: spacing.xs,
69
+ paddingHorizontal: spacing.s,
70
+ minHeight: 44,
71
+ justifyContent: 'center',
72
+ },
73
+ optionText: {
74
+ fontFamily: font.family.body,
75
+ fontSize: font.size.body,
76
+ lineHeight: lineHeight.body,
77
+ },
78
+ });
79
+
80
+ export function Combobox({
81
+ label,
82
+ name,
83
+ options,
84
+ placeholder,
85
+ value,
86
+ onChange,
87
+ disabled = false,
88
+ error,
89
+ hint,
90
+ style,
91
+ }: Readonly<ComboboxProps>) {
92
+ const { colors } = useTheme();
93
+
94
+ const selectedOption = useMemo(
95
+ () => options.find((o) => o.value === value),
96
+ [options, value],
97
+ );
98
+
99
+ const [inputValue, setInputValue] = useState(selectedOption?.label ?? '');
100
+ const [isOpen, setIsOpen] = useState(false);
101
+
102
+ const filtered = useMemo(
103
+ () =>
104
+ options.filter((o) =>
105
+ o.label.toLowerCase().includes(inputValue.toLowerCase()),
106
+ ),
107
+ [options, inputValue],
108
+ );
109
+
110
+ const selectOption = (option: ComboboxOption) => {
111
+ setInputValue(option.label);
112
+ setIsOpen(false);
113
+ onChange?.(option.value);
114
+ };
115
+
116
+ const handleChangeText = (text: string) => {
117
+ setInputValue(text);
118
+ setIsOpen(true);
119
+ };
120
+
121
+ const handleFocus = () => {
122
+ setIsOpen(true);
123
+ };
124
+
125
+ const handleBlur = () => {
126
+ // Delay closing so onPress on an option can fire first
127
+ setTimeout(() => setIsOpen(false), 150);
128
+ };
129
+
130
+ const borderColor = error ? colors.colorError : colors.colorOnBg;
131
+
132
+ return (
133
+ <View style={[styles.container, style]}>
134
+ <Label label={label} />
135
+ <View style={styles.inputWrapper}>
136
+ <TextInput
137
+ value={inputValue}
138
+ onChangeText={handleChangeText}
139
+ onFocus={handleFocus}
140
+ onBlur={handleBlur}
141
+ placeholder={placeholder}
142
+ placeholderTextColor={colors.colorOnBgSubtle}
143
+ editable={!disabled}
144
+ accessibilityRole="combobox"
145
+ accessibilityLabel={typeof label === 'string' ? label : name}
146
+ accessibilityState={{ disabled, expanded: isOpen }}
147
+ style={[
148
+ styles.input,
149
+ {
150
+ borderColor,
151
+ color: colors.colorOnBg,
152
+ backgroundColor: colors.colorBg,
153
+ },
154
+ disabled && { opacity: 0.4 },
155
+ ]}
156
+ />
157
+ {isOpen && filtered.length > 0 && (
158
+ <ScrollView
159
+ style={[
160
+ styles.dropdown,
161
+ {
162
+ borderColor: colors.colorBgSubtle,
163
+ backgroundColor: colors.colorBg,
164
+ },
165
+ ]}
166
+ keyboardShouldPersistTaps="handled"
167
+ nestedScrollEnabled
168
+ >
169
+ {filtered.map((option) => {
170
+ const isSelected = option.value === value;
171
+ const isDisabled = option.disabled;
172
+
173
+ return (
174
+ <Pressable
175
+ key={option.value}
176
+ onPress={() => !isDisabled && selectOption(option)}
177
+ disabled={isDisabled}
178
+ accessibilityRole="menuitem"
179
+ accessibilityLabel={option.label}
180
+ accessibilityState={{ selected: isSelected, disabled: isDisabled }}
181
+ style={({ pressed }) => [
182
+ styles.option,
183
+ isSelected && { backgroundColor: colors.colorBlueSubtle },
184
+ pressed && !isDisabled && { backgroundColor: colors.colorBgSubtle },
185
+ isDisabled && { opacity: 0.4 },
186
+ ]}
187
+ >
188
+ <Text
189
+ style={[
190
+ styles.optionText,
191
+ {
192
+ color: isSelected
193
+ ? colors.colorInteractive
194
+ : colors.colorOnBg,
195
+ },
196
+ isSelected && { fontWeight: font.weight.semibold },
197
+ ]}
198
+ >
199
+ {option.label}
200
+ </Text>
201
+ </Pressable>
202
+ );
203
+ })}
204
+ </ScrollView>
205
+ )}
206
+ </View>
207
+ <Message type="error" message={error} />
208
+ <Message type="hint" message={hint} />
209
+ </View>
210
+ );
211
+ }
@@ -0,0 +1,86 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './FileInput.native.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # FileInput (React Native)
7
+
8
+ A file selection area that displays an upload prompt. In React Native, actual file picking requires a native module (e.g. `expo-document-picker`). This component provides the UI trigger and displays selected file names.
9
+
10
+ > **Status: alpha** -- API may change before stable release.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { FileInput } from '@boostdev/components';
16
+ import { useState } from 'react';
17
+ import * as DocumentPicker from 'expo-document-picker';
18
+
19
+ function Example() {
20
+ const [fileNames, setFileNames] = useState<string[]>([]);
21
+
22
+ const handleSelect = async () => {
23
+ const result = await DocumentPicker.getDocumentAsync({ multiple: true });
24
+ if (!result.canceled) {
25
+ setFileNames(result.assets.map(a => a.name));
26
+ }
27
+ };
28
+
29
+ return (
30
+ <FileInput
31
+ label="Upload document"
32
+ name="document"
33
+ fileNames={fileNames}
34
+ onSelect={handleSelect}
35
+ />
36
+ );
37
+ }
38
+ ```
39
+
40
+ ## Examples
41
+
42
+ ### Default
43
+ <Canvas of={Stories.Default} />
44
+
45
+ ### With accepted file types
46
+ <Canvas of={Stories.WithAccept} />
47
+
48
+ ### With selected files
49
+ <Canvas of={Stories.WithFiles} />
50
+
51
+ ### Required
52
+ <Canvas of={Stories.Required} />
53
+
54
+ ### With hint
55
+ <Canvas of={Stories.WithHint} />
56
+
57
+ ### With error
58
+ <Canvas of={Stories.WithError} />
59
+
60
+ ### Disabled
61
+ <Canvas of={Stories.Disabled} />
62
+
63
+ ## Props
64
+
65
+ <ArgTypes of={Stories} />
66
+
67
+ ## Props reference
68
+
69
+ | Prop | Type | Default | Description |
70
+ |------|------|---------|-------------|
71
+ | `label` | string | -- | Visible label text |
72
+ | `name` | string | -- | Field name |
73
+ | `accept` | string | -- | Accepted file types hint (display only) |
74
+ | `multiple` | boolean | `false` | Whether multiple files can be selected |
75
+ | `disabled` | boolean | `false` | Prevents interaction, reduces opacity |
76
+ | `required` | boolean | -- | Appends asterisk to label |
77
+ | `error` | string | -- | Error message displayed below |
78
+ | `hint` | string | -- | Helper text displayed below |
79
+ | `onSelect` | `() => void` | -- | Called when the file area is pressed |
80
+ | `fileNames` | `string[]` | `[]` | File names to display |
81
+
82
+ ## Accessibility
83
+
84
+ - Uses `accessibilityRole="button"` for the pressable area
85
+ - `accessibilityLabel` includes the label text and action prompt
86
+ - Disabled state is communicated via `accessibilityState`
@@ -0,0 +1,73 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../../native/ThemeContext';
3
+ import { FileInput } from './FileInput.native';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ describe('FileInput (native)', () => {
10
+ it('renders with label', () => {
11
+ renderWithTheme(<FileInput label="Upload document" name="document" />);
12
+ expect(screen.getByText('Upload document')).toBeInTheDocument();
13
+ });
14
+
15
+ it('renders the tap-to-select prompt when no files are selected', () => {
16
+ renderWithTheme(<FileInput label="Upload" name="upload" />);
17
+ expect(screen.getByText('Tap to select file')).toBeInTheDocument();
18
+ });
19
+
20
+ it('displays file names when provided', () => {
21
+ renderWithTheme(
22
+ <FileInput label="Upload" name="upload" fileNames={['report.pdf', 'data.csv']} />,
23
+ );
24
+ expect(screen.getByText('report.pdf')).toBeInTheDocument();
25
+ expect(screen.getByText('data.csv')).toBeInTheDocument();
26
+ });
27
+
28
+ it('displays accept hint', () => {
29
+ renderWithTheme(
30
+ <FileInput label="Upload" name="upload" accept=".png, .jpg" />,
31
+ );
32
+ expect(screen.getByText('.png, .jpg')).toBeInTheDocument();
33
+ });
34
+
35
+ it('calls onSelect when clicked', () => {
36
+ const onSelect = vi.fn();
37
+ renderWithTheme(
38
+ <FileInput label="Upload" name="upload" onSelect={onSelect} />,
39
+ );
40
+ fireEvent.click(screen.getByRole('button'));
41
+ expect(onSelect).toHaveBeenCalledOnce();
42
+ });
43
+
44
+ it('does not call onSelect when disabled', () => {
45
+ const onSelect = vi.fn();
46
+ renderWithTheme(
47
+ <FileInput label="Upload" name="upload" onSelect={onSelect} disabled />,
48
+ );
49
+ fireEvent.click(screen.getByRole('button'));
50
+ expect(onSelect).not.toHaveBeenCalled();
51
+ });
52
+
53
+ it('renders error message', () => {
54
+ renderWithTheme(
55
+ <FileInput label="Upload" name="upload" error="File required." />,
56
+ );
57
+ expect(screen.getByRole('alert')).toHaveTextContent('File required.');
58
+ });
59
+
60
+ it('renders hint message', () => {
61
+ renderWithTheme(
62
+ <FileInput label="Upload" name="upload" hint="Max 5MB." />,
63
+ );
64
+ expect(screen.getByText('Max 5MB.')).toBeInTheDocument();
65
+ });
66
+
67
+ it('appends asterisk to label when required', () => {
68
+ renderWithTheme(
69
+ <FileInput label="Resume" name="resume" required />,
70
+ );
71
+ expect(screen.getByText('Resume *')).toBeInTheDocument();
72
+ });
73
+ });
@@ -0,0 +1,69 @@
1
+ import React, { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { ThemeProvider } from '../../../../native/ThemeContext';
4
+ import { FileInput } from './FileInput.native';
5
+
6
+ const meta = {
7
+ tags: ['!stable', 'alpha'],
8
+ title: 'React Native/Form/FileInput',
9
+ component: FileInput,
10
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
11
+ parameters: { layout: 'padded' },
12
+ } satisfies Meta<typeof FileInput>;
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Default: Story = {
18
+ args: { label: 'Upload document', name: 'document' },
19
+ };
20
+
21
+ export const WithAccept: Story = {
22
+ args: { label: 'Upload image', name: 'image', accept: '.png, .jpg, .jpeg' },
23
+ };
24
+
25
+ export const WithFiles: Story = {
26
+ args: {
27
+ label: 'Upload document',
28
+ name: 'document',
29
+ fileNames: ['report.pdf', 'summary.docx'],
30
+ },
31
+ };
32
+
33
+ export const Required: Story = {
34
+ args: { label: 'Upload resume', name: 'resume', required: true },
35
+ };
36
+
37
+ export const Disabled: Story = {
38
+ args: { label: 'Upload document', name: 'document', disabled: true },
39
+ };
40
+
41
+ export const WithError: Story = {
42
+ args: {
43
+ label: 'Upload document',
44
+ name: 'document',
45
+ error: 'Please upload a file.',
46
+ },
47
+ };
48
+
49
+ export const WithHint: Story = {
50
+ args: {
51
+ label: 'Upload photo',
52
+ name: 'photo',
53
+ hint: 'Max file size: 5MB.',
54
+ },
55
+ };
56
+
57
+ export const Interactive: Story = {
58
+ render: () => {
59
+ const [files, setFiles] = useState<string[]>([]);
60
+ return (
61
+ <FileInput
62
+ label="Upload document"
63
+ name="document"
64
+ fileNames={files}
65
+ onSelect={() => setFiles(['example.pdf'])}
66
+ />
67
+ );
68
+ },
69
+ };
@@ -0,0 +1,129 @@
1
+ import { StyleSheet, View, Pressable, Text, StyleProp, ViewStyle } from 'react-native';
2
+ import { spacing, font, border, lineHeight } from '../../../../native/tokens';
3
+ import { useTheme } from '../../../../native/ThemeContext';
4
+ import { Label } from '../atoms/Label.native';
5
+ import { Message } from '../atoms/Message.native';
6
+
7
+ interface FileInputProps {
8
+ label: string;
9
+ name: string;
10
+ accept?: string;
11
+ multiple?: boolean;
12
+ disabled?: boolean;
13
+ required?: boolean;
14
+ error?: string;
15
+ hint?: string;
16
+ /** Called when the file area is pressed. Wire up a native file picker (e.g. expo-document-picker). */
17
+ onSelect?: () => void;
18
+ /** File names to display (managed externally after picker resolves). */
19
+ fileNames?: string[];
20
+ style?: StyleProp<ViewStyle>;
21
+ }
22
+
23
+ const styles = StyleSheet.create({
24
+ container: {
25
+ gap: spacing.xxs,
26
+ },
27
+ dropZone: {
28
+ alignItems: 'center',
29
+ justifyContent: 'center',
30
+ gap: spacing.xs,
31
+ paddingVertical: spacing.xl,
32
+ paddingHorizontal: spacing.m,
33
+ borderWidth: 2,
34
+ borderStyle: 'dashed',
35
+ borderRadius: border.radius.s,
36
+ minHeight: 120,
37
+ },
38
+ icon: {
39
+ fontSize: 28,
40
+ lineHeight: 32,
41
+ },
42
+ prompt: {
43
+ fontFamily: font.family.body,
44
+ fontSize: font.size.body,
45
+ lineHeight: lineHeight.body,
46
+ textAlign: 'center',
47
+ },
48
+ acceptHint: {
49
+ fontFamily: font.family.body,
50
+ fontSize: font.size.bodyS,
51
+ lineHeight: lineHeight.bodyS,
52
+ textAlign: 'center',
53
+ },
54
+ fileList: {
55
+ gap: spacing.xxs,
56
+ alignItems: 'center',
57
+ },
58
+ fileName: {
59
+ fontFamily: font.family.body,
60
+ fontSize: font.size.bodyS,
61
+ lineHeight: lineHeight.bodyS,
62
+ },
63
+ });
64
+
65
+ export function FileInput({
66
+ label,
67
+ name,
68
+ accept,
69
+ disabled = false,
70
+ required,
71
+ error,
72
+ hint,
73
+ onSelect,
74
+ fileNames = [],
75
+ style,
76
+ }: Readonly<FileInputProps>) {
77
+ const { colors } = useTheme();
78
+
79
+ const borderColor = error
80
+ ? colors.colorError
81
+ : colors.colorBgSubtle;
82
+
83
+ return (
84
+ <View style={[styles.container, style]} testID={name}>
85
+ <Label
86
+ label={required ? `${label} *` : label}
87
+ />
88
+ <Pressable
89
+ onPress={() => !disabled && onSelect?.()}
90
+ disabled={disabled}
91
+ accessibilityRole="button"
92
+ accessibilityLabel={`${label}, tap to select file`}
93
+ accessibilityState={{ disabled }}
94
+ style={({ pressed }) => [
95
+ styles.dropZone,
96
+ {
97
+ borderColor,
98
+ backgroundColor: 'transparent',
99
+ },
100
+ pressed && { opacity: 0.7 },
101
+ disabled && { opacity: 0.4 },
102
+ ]}
103
+ >
104
+ <Text style={[styles.icon, { color: colors.colorInteractive }]}>
105
+ {'\u2191'}
106
+ </Text>
107
+ <Text style={[styles.prompt, { color: colors.colorOnBgSubtle }]}>
108
+ {fileNames.length > 0 ? 'Selected files:' : 'Tap to select file'}
109
+ </Text>
110
+ {fileNames.length > 0 && (
111
+ <View style={styles.fileList}>
112
+ {fileNames.map((fileName) => (
113
+ <Text key={fileName} style={[styles.fileName, { color: colors.colorOnBg }]}>
114
+ {fileName}
115
+ </Text>
116
+ ))}
117
+ </View>
118
+ )}
119
+ {accept && (
120
+ <Text style={[styles.acceptHint, { color: colors.colorOnBgSubtle }]}>
121
+ {accept}
122
+ </Text>
123
+ )}
124
+ </Pressable>
125
+ <Message type="error" message={error} />
126
+ <Message type="hint" message={hint} />
127
+ </View>
128
+ );
129
+ }