@boostdev/design-system-components 2.4.2 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/AGENTS.md +4 -20
  2. package/README.md +3 -23
  3. package/dist/client.cjs +115 -61
  4. package/dist/client.css +537 -537
  5. package/dist/client.d.cts +11 -1
  6. package/dist/client.d.ts +11 -1
  7. package/dist/client.js +115 -61
  8. package/dist/index.cjs +115 -61
  9. package/dist/index.css +537 -537
  10. package/dist/index.d.cts +11 -1
  11. package/dist/index.d.ts +11 -1
  12. package/dist/index.js +115 -61
  13. package/dist/web-components/index.js +23 -3
  14. package/package.json +2 -10
  15. package/src/components/layout/Grid/Grid.mdx +20 -2
  16. package/src/components/layout/Grid/Grid.spec.tsx +194 -4
  17. package/src/components/layout/Grid/Grid.tsx +15 -1
  18. package/src/components/layout/Grid/masonry.ts +127 -9
  19. package/dist/native/index.cjs +0 -4337
  20. package/dist/native/index.d.cts +0 -574
  21. package/dist/native/index.d.ts +0 -574
  22. package/dist/native/index.js +0 -4421
  23. package/src/components/interaction/Button/Button.native.mdx +0 -91
  24. package/src/components/interaction/Button/Button.native.spec.tsx +0 -55
  25. package/src/components/interaction/Button/Button.native.stories.tsx +0 -141
  26. package/src/components/interaction/Button/Button.native.tsx +0 -105
  27. package/src/components/interaction/Dialog/Dialog.native.mdx +0 -61
  28. package/src/components/interaction/Dialog/Dialog.native.spec.tsx +0 -73
  29. package/src/components/interaction/Dialog/Dialog.native.stories.tsx +0 -53
  30. package/src/components/interaction/Dialog/Dialog.native.tsx +0 -128
  31. package/src/components/interaction/Drawer/Drawer.native.mdx +0 -58
  32. package/src/components/interaction/Drawer/Drawer.native.spec.tsx +0 -81
  33. package/src/components/interaction/Drawer/Drawer.native.stories.tsx +0 -33
  34. package/src/components/interaction/Drawer/Drawer.native.tsx +0 -175
  35. package/src/components/interaction/DropdownMenu/DropdownMenu.native.mdx +0 -74
  36. package/src/components/interaction/DropdownMenu/DropdownMenu.native.spec.tsx +0 -78
  37. package/src/components/interaction/DropdownMenu/DropdownMenu.native.stories.tsx +0 -51
  38. package/src/components/interaction/DropdownMenu/DropdownMenu.native.tsx +0 -254
  39. package/src/components/interaction/Popover/Popover.native.mdx +0 -61
  40. package/src/components/interaction/Popover/Popover.native.spec.tsx +0 -73
  41. package/src/components/interaction/Popover/Popover.native.stories.tsx +0 -44
  42. package/src/components/interaction/Popover/Popover.native.tsx +0 -87
  43. package/src/components/interaction/Rating/Rating.native.mdx +0 -55
  44. package/src/components/interaction/Rating/Rating.native.spec.tsx +0 -38
  45. package/src/components/interaction/Rating/Rating.native.stories.tsx +0 -37
  46. package/src/components/interaction/Rating/Rating.native.tsx +0 -50
  47. package/src/components/interaction/Toast/Toast.native.mdx +0 -81
  48. package/src/components/interaction/Toast/Toast.native.spec.tsx +0 -80
  49. package/src/components/interaction/Toast/Toast.native.stories.tsx +0 -47
  50. package/src/components/interaction/Toast/Toast.native.tsx +0 -202
  51. package/src/components/interaction/form/Checkbox/Checkbox.native.mdx +0 -74
  52. package/src/components/interaction/form/Checkbox/Checkbox.native.spec.tsx +0 -42
  53. package/src/components/interaction/form/Checkbox/Checkbox.native.stories.tsx +0 -34
  54. package/src/components/interaction/form/Checkbox/Checkbox.native.tsx +0 -80
  55. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.mdx +0 -78
  56. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.spec.tsx +0 -57
  57. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.stories.tsx +0 -96
  58. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.tsx +0 -61
  59. package/src/components/interaction/form/Combobox/Combobox.native.mdx +0 -86
  60. package/src/components/interaction/form/Combobox/Combobox.native.spec.tsx +0 -60
  61. package/src/components/interaction/form/Combobox/Combobox.native.stories.tsx +0 -99
  62. package/src/components/interaction/form/Combobox/Combobox.native.tsx +0 -211
  63. package/src/components/interaction/form/FileInput/FileInput.native.mdx +0 -86
  64. package/src/components/interaction/form/FileInput/FileInput.native.spec.tsx +0 -73
  65. package/src/components/interaction/form/FileInput/FileInput.native.stories.tsx +0 -69
  66. package/src/components/interaction/form/FileInput/FileInput.native.tsx +0 -129
  67. package/src/components/interaction/form/FormInput/FormInput.native.mdx +0 -81
  68. package/src/components/interaction/form/FormInput/FormInput.native.spec.tsx +0 -51
  69. package/src/components/interaction/form/FormInput/FormInput.native.stories.tsx +0 -54
  70. package/src/components/interaction/form/FormInput/FormInput.native.tsx +0 -122
  71. package/src/components/interaction/form/NumberInput/NumberInput.native.mdx +0 -87
  72. package/src/components/interaction/form/NumberInput/NumberInput.native.spec.tsx +0 -63
  73. package/src/components/interaction/form/NumberInput/NumberInput.native.stories.tsx +0 -63
  74. package/src/components/interaction/form/NumberInput/NumberInput.native.tsx +0 -198
  75. package/src/components/interaction/form/Radio/Radio.native.spec.tsx +0 -79
  76. package/src/components/interaction/form/Radio/Radio.native.stories.tsx +0 -44
  77. package/src/components/interaction/form/Radio/Radio.native.tsx +0 -79
  78. package/src/components/interaction/form/RadioGroup/RadioGroup.native.mdx +0 -79
  79. package/src/components/interaction/form/RadioGroup/RadioGroup.native.spec.tsx +0 -66
  80. package/src/components/interaction/form/RadioGroup/RadioGroup.native.stories.tsx +0 -96
  81. package/src/components/interaction/form/RadioGroup/RadioGroup.native.tsx +0 -61
  82. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.mdx +0 -78
  83. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.spec.tsx +0 -82
  84. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.stories.tsx +0 -99
  85. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.tsx +0 -121
  86. package/src/components/interaction/form/Select/Select.native.mdx +0 -85
  87. package/src/components/interaction/form/Select/Select.native.spec.tsx +0 -64
  88. package/src/components/interaction/form/Select/Select.native.stories.tsx +0 -75
  89. package/src/components/interaction/form/Select/Select.native.tsx +0 -252
  90. package/src/components/interaction/form/Slider/Slider.native.mdx +0 -83
  91. package/src/components/interaction/form/Slider/Slider.native.spec.tsx +0 -51
  92. package/src/components/interaction/form/Slider/Slider.native.stories.tsx +0 -58
  93. package/src/components/interaction/form/Slider/Slider.native.tsx +0 -188
  94. package/src/components/interaction/form/Switch/Switch.native.spec.tsx +0 -60
  95. package/src/components/interaction/form/Switch/Switch.native.stories.tsx +0 -33
  96. package/src/components/interaction/form/Switch/Switch.native.tsx +0 -59
  97. package/src/components/interaction/form/Textarea/Textarea.native.mdx +0 -76
  98. package/src/components/interaction/form/Textarea/Textarea.native.spec.tsx +0 -50
  99. package/src/components/interaction/form/Textarea/Textarea.native.stories.tsx +0 -50
  100. package/src/components/interaction/form/Textarea/Textarea.native.tsx +0 -108
  101. package/src/components/interaction/form/atoms/Label.native.spec.tsx +0 -39
  102. package/src/components/interaction/form/atoms/Label.native.stories.tsx +0 -18
  103. package/src/components/interaction/form/atoms/Label.native.tsx +0 -29
  104. package/src/components/interaction/form/atoms/Message.native.spec.tsx +0 -30
  105. package/src/components/interaction/form/atoms/Message.native.stories.tsx +0 -22
  106. package/src/components/interaction/form/atoms/Message.native.tsx +0 -34
  107. package/src/components/layout/ButtonGroup/ButtonGroup.native.spec.tsx +0 -83
  108. package/src/components/layout/ButtonGroup/ButtonGroup.native.stories.tsx +0 -44
  109. package/src/components/layout/ButtonGroup/ButtonGroup.native.tsx +0 -44
  110. package/src/components/layout/Card/Card.native.spec.tsx +0 -102
  111. package/src/components/layout/Card/Card.native.stories.tsx +0 -51
  112. package/src/components/layout/Card/Card.native.tsx +0 -89
  113. package/src/components/layout/IconWrapper/IconWrapper.native.spec.tsx +0 -39
  114. package/src/components/layout/IconWrapper/IconWrapper.native.stories.tsx +0 -37
  115. package/src/components/layout/IconWrapper/IconWrapper.native.tsx +0 -20
  116. package/src/components/layout/SectionHeader/SectionHeader.native.spec.tsx +0 -51
  117. package/src/components/layout/SectionHeader/SectionHeader.native.stories.tsx +0 -36
  118. package/src/components/layout/SectionHeader/SectionHeader.native.tsx +0 -79
  119. package/src/components/ui/Accordion/Accordion.native.mdx +0 -65
  120. package/src/components/ui/Accordion/Accordion.native.spec.tsx +0 -69
  121. package/src/components/ui/Accordion/Accordion.native.stories.tsx +0 -52
  122. package/src/components/ui/Accordion/Accordion.native.tsx +0 -141
  123. package/src/components/ui/Alert/Alert.native.mdx +0 -62
  124. package/src/components/ui/Alert/Alert.native.spec.tsx +0 -76
  125. package/src/components/ui/Alert/Alert.native.stories.tsx +0 -41
  126. package/src/components/ui/Alert/Alert.native.tsx +0 -94
  127. package/src/components/ui/Avatar/Avatar.native.mdx +0 -50
  128. package/src/components/ui/Avatar/Avatar.native.spec.tsx +0 -47
  129. package/src/components/ui/Avatar/Avatar.native.stories.tsx +0 -32
  130. package/src/components/ui/Avatar/Avatar.native.tsx +0 -85
  131. package/src/components/ui/Badge/Badge.native.mdx +0 -54
  132. package/src/components/ui/Badge/Badge.native.spec.tsx +0 -45
  133. package/src/components/ui/Badge/Badge.native.stories.tsx +0 -34
  134. package/src/components/ui/Badge/Badge.native.tsx +0 -50
  135. package/src/components/ui/Breadcrumb/Breadcrumb.native.mdx +0 -52
  136. package/src/components/ui/Breadcrumb/Breadcrumb.native.spec.tsx +0 -49
  137. package/src/components/ui/Breadcrumb/Breadcrumb.native.stories.tsx +0 -46
  138. package/src/components/ui/Breadcrumb/Breadcrumb.native.tsx +0 -83
  139. package/src/components/ui/Calendar/Calendar.native.mdx +0 -58
  140. package/src/components/ui/Calendar/Calendar.native.spec.tsx +0 -92
  141. package/src/components/ui/Calendar/Calendar.native.stories.tsx +0 -45
  142. package/src/components/ui/Calendar/Calendar.native.tsx +0 -294
  143. package/src/components/ui/Carousel/Carousel.native.mdx +0 -55
  144. package/src/components/ui/Carousel/Carousel.native.spec.tsx +0 -55
  145. package/src/components/ui/Carousel/Carousel.native.stories.tsx +0 -59
  146. package/src/components/ui/Carousel/Carousel.native.tsx +0 -184
  147. package/src/components/ui/Collapsible/Collapsible.native.mdx +0 -53
  148. package/src/components/ui/Collapsible/Collapsible.native.spec.tsx +0 -68
  149. package/src/components/ui/Collapsible/Collapsible.native.stories.tsx +0 -48
  150. package/src/components/ui/Collapsible/Collapsible.native.tsx +0 -139
  151. package/src/components/ui/DescriptionList/DescriptionList.native.mdx +0 -50
  152. package/src/components/ui/DescriptionList/DescriptionList.native.spec.tsx +0 -37
  153. package/src/components/ui/DescriptionList/DescriptionList.native.stories.tsx +0 -48
  154. package/src/components/ui/DescriptionList/DescriptionList.native.tsx +0 -54
  155. package/src/components/ui/Link/Link.native.mdx +0 -53
  156. package/src/components/ui/Link/Link.native.spec.tsx +0 -35
  157. package/src/components/ui/Link/Link.native.stories.tsx +0 -46
  158. package/src/components/ui/Link/Link.native.tsx +0 -48
  159. package/src/components/ui/Loading/Loading.native.spec.tsx +0 -24
  160. package/src/components/ui/Loading/Loading.native.stories.tsx +0 -31
  161. package/src/components/ui/Loading/Loading.native.tsx +0 -29
  162. package/src/components/ui/NotificationBanner/NotificationBanner.native.spec.tsx +0 -85
  163. package/src/components/ui/NotificationBanner/NotificationBanner.native.stories.tsx +0 -37
  164. package/src/components/ui/NotificationBanner/NotificationBanner.native.tsx +0 -76
  165. package/src/components/ui/Pagination/Pagination.native.mdx +0 -62
  166. package/src/components/ui/Pagination/Pagination.native.spec.tsx +0 -86
  167. package/src/components/ui/Pagination/Pagination.native.stories.tsx +0 -47
  168. package/src/components/ui/Pagination/Pagination.native.tsx +0 -157
  169. package/src/components/ui/Progress/Progress.native.spec.tsx +0 -56
  170. package/src/components/ui/Progress/Progress.native.stories.tsx +0 -34
  171. package/src/components/ui/Progress/Progress.native.tsx +0 -84
  172. package/src/components/ui/ProgressCircle/ProgressCircle.native.mdx +0 -63
  173. package/src/components/ui/ProgressCircle/ProgressCircle.native.spec.tsx +0 -39
  174. package/src/components/ui/ProgressCircle/ProgressCircle.native.stories.tsx +0 -36
  175. package/src/components/ui/ProgressCircle/ProgressCircle.native.tsx +0 -176
  176. package/src/components/ui/Separator/Separator.native.spec.tsx +0 -39
  177. package/src/components/ui/Separator/Separator.native.stories.tsx +0 -38
  178. package/src/components/ui/Separator/Separator.native.tsx +0 -32
  179. package/src/components/ui/Skeleton/Skeleton.native.spec.tsx +0 -39
  180. package/src/components/ui/Skeleton/Skeleton.native.stories.tsx +0 -31
  181. package/src/components/ui/Skeleton/Skeleton.native.tsx +0 -42
  182. package/src/components/ui/Table/Table.native.mdx +0 -72
  183. package/src/components/ui/Table/Table.native.spec.tsx +0 -83
  184. package/src/components/ui/Table/Table.native.stories.tsx +0 -55
  185. package/src/components/ui/Table/Table.native.tsx +0 -196
  186. package/src/components/ui/Tabs/Tabs.native.mdx +0 -65
  187. package/src/components/ui/Tabs/Tabs.native.spec.tsx +0 -65
  188. package/src/components/ui/Tabs/Tabs.native.stories.tsx +0 -57
  189. package/src/components/ui/Tabs/Tabs.native.tsx +0 -133
  190. package/src/components/ui/Tooltip/Tooltip.native.mdx +0 -47
  191. package/src/components/ui/Tooltip/Tooltip.native.spec.tsx +0 -37
  192. package/src/components/ui/Tooltip/Tooltip.native.stories.tsx +0 -38
  193. package/src/components/ui/Tooltip/Tooltip.native.tsx +0 -57
  194. package/src/components/ui/Typography/Typography.native.mdx +0 -56
  195. package/src/components/ui/Typography/Typography.native.spec.tsx +0 -49
  196. package/src/components/ui/Typography/Typography.native.stories.tsx +0 -36
  197. package/src/components/ui/Typography/Typography.native.tsx +0 -65
  198. package/src/native/ThemeContext.tsx +0 -28
  199. package/src/native/tokens.ts +0 -13
  200. package/src/native.ts +0 -76
  201. package/src/stories/ReactNative.mdx +0 -156
@@ -1,211 +0,0 @@
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
- }
@@ -1,86 +0,0 @@
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/design-system-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`
@@ -1,73 +0,0 @@
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
- });
@@ -1,69 +0,0 @@
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
- };
@@ -1,129 +0,0 @@
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
- }
@@ -1,81 +0,0 @@
1
- import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
- import * as Stories from './FormInput.native.stories';
3
-
4
- <Meta of={Stories} />
5
-
6
- # FormInput (React Native)
7
-
8
- Text input field with label, optional hint, and error state. Maps the `type` prop to the appropriate keyboard type and secure text entry mode.
9
-
10
- > **Status: alpha** — API may change before stable release.
11
-
12
- ## Usage
13
-
14
- ```tsx
15
- import { FormInput } from '@boostdev/design-system-components';
16
- import { useState } from 'react';
17
-
18
- function Example() {
19
- const [email, setEmail] = useState('');
20
- return (
21
- <FormInput
22
- label="Email"
23
- name="email"
24
- type="email"
25
- placeholder="you@example.com"
26
- value={email}
27
- onChange={setEmail}
28
- />
29
- );
30
- }
31
- ```
32
-
33
- ## Examples
34
-
35
- ### Default
36
- <Canvas of={Stories.Default} />
37
-
38
- ### With placeholder
39
- <Canvas of={Stories.WithPlaceholder} />
40
-
41
- ### Password
42
- <Canvas of={Stories.Password} />
43
-
44
- ### With error
45
- <Canvas of={Stories.WithError} />
46
-
47
- ### With hint
48
- <Canvas of={Stories.WithHint} />
49
-
50
- ### Disabled
51
- <Canvas of={Stories.Disabled} />
52
-
53
- ### Interactive
54
- <Canvas of={Stories.Interactive} />
55
-
56
- ## Props
57
-
58
- <ArgTypes of={Stories} />
59
-
60
- ## Props reference
61
-
62
- | Prop | Type | Default | Description |
63
- |------|------|---------|-------------|
64
- | `label` | ReactNode | — | Visible label text |
65
- | `name` | string | — | Field name (used for accessibility) |
66
- | `type` | `'text' \| 'email' \| 'password' \| 'tel' \| 'url' \| 'number' \| 'search'` | `'text'` | Input type, maps to keyboard type |
67
- | `value` | string | — | Controlled value |
68
- | `defaultValue` | string | — | Uncontrolled default value |
69
- | `placeholder` | string | — | Placeholder text |
70
- | `error` | string | — | Error message displayed below the input |
71
- | `hint` | string | — | Hint text displayed below the input |
72
- | `required` | boolean | `false` | Marks the field as required |
73
- | `disabled` | boolean | `false` | Prevents interaction, reduces opacity |
74
- | `onChange` | `(value: string) => void` | — | Called with the new text value |
75
-
76
- ## Accessibility
77
-
78
- - The label is announced by screen readers via `accessibilityLabel`
79
- - Hint text is announced via `accessibilityHint`
80
- - `disabled` communicates the disabled state via `accessibilityState`
81
- - Password type uses `secureTextEntry` for input masking
@@ -1,51 +0,0 @@
1
- import { render, screen } from '@testing-library/react';
2
- import { ThemeProvider } from '../../../../native/ThemeContext';
3
- import { FormInput } from './FormInput.native';
4
-
5
- function renderWithTheme(ui: React.ReactElement) {
6
- return render(<ThemeProvider>{ui}</ThemeProvider>);
7
- }
8
-
9
- describe('FormInput (native)', () => {
10
- it('renders the label', () => {
11
- renderWithTheme(<FormInput label="Full name" name="fullName" />);
12
- expect(screen.getByText('Full name')).toBeInTheDocument();
13
- });
14
-
15
- it('renders with placeholder text', () => {
16
- renderWithTheme(
17
- <FormInput label="Email" name="email" placeholder="you@example.com" />,
18
- );
19
- expect(screen.getByPlaceholderText('you@example.com')).toBeInTheDocument();
20
- });
21
-
22
- it('renders with a value', () => {
23
- renderWithTheme(
24
- <FormInput label="Name" name="name" value="Jane" />,
25
- );
26
- expect(screen.getByDisplayValue('Jane')).toBeInTheDocument();
27
- });
28
-
29
- it('shows error message', () => {
30
- renderWithTheme(
31
- <FormInput label="Email" name="email" error="Invalid email." />,
32
- );
33
- expect(screen.getByRole('alert')).toHaveTextContent('Invalid email.');
34
- });
35
-
36
- it('shows hint message', () => {
37
- renderWithTheme(
38
- <FormInput label="Username" name="username" hint="At least 3 characters." />,
39
- );
40
- expect(screen.getByText('At least 3 characters.')).toBeInTheDocument();
41
- });
42
-
43
- it('applies disabled opacity', () => {
44
- const { container } = renderWithTheme(
45
- <FormInput label="Name" name="name" disabled />,
46
- );
47
- // The TextInput should have opacity 0.4 when disabled
48
- const inputs = container.querySelectorAll('input, [style*="opacity"]');
49
- expect(inputs.length).toBeGreaterThan(0);
50
- });
51
- });