@urbint/cl 1.0.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 (206) hide show
  1. package/.cursor/rules +313 -0
  2. package/.rnstorybook/index.ts +11 -0
  3. package/.rnstorybook/main.ts +8 -0
  4. package/.rnstorybook/preview.tsx +14 -0
  5. package/.rnstorybook/storybook.requires.ts +49 -0
  6. package/.storybook/main.ts +16 -0
  7. package/.storybook/preview.ts +32 -0
  8. package/.storybook/vitest.setup.ts +7 -0
  9. package/App.tsx +422 -0
  10. package/README.md +229 -0
  11. package/app.json +33 -0
  12. package/assets/adaptive-icon.png +0 -0
  13. package/assets/favicon.png +0 -0
  14. package/assets/icon.png +0 -0
  15. package/assets/splash-icon.png +0 -0
  16. package/babel.config.js +16 -0
  17. package/docs/components/CodeBlock.tsx +80 -0
  18. package/docs/components/PropTable.tsx +93 -0
  19. package/docs/components/Sidebar.tsx +199 -0
  20. package/docs/components/index.ts +8 -0
  21. package/docs/data/colorTokens.ts +70 -0
  22. package/docs/data/componentData.tsx +1685 -0
  23. package/docs/data/index.ts +7 -0
  24. package/docs/index.ts +19 -0
  25. package/docs/navigation.ts +94 -0
  26. package/docs/pages/ColorsPage.tsx +226 -0
  27. package/docs/pages/ComponentPage.tsx +235 -0
  28. package/docs/pages/InstallationPage.tsx +232 -0
  29. package/docs/pages/IntroductionPage.tsx +163 -0
  30. package/docs/pages/ThemingPage.tsx +251 -0
  31. package/docs/pages/index.ts +10 -0
  32. package/docs/theme.ts +64 -0
  33. package/docs/types.ts +54 -0
  34. package/index.ts +8 -0
  35. package/llms.txt +1893 -0
  36. package/mcp-config.example.json +10 -0
  37. package/mcp-server/README.md +192 -0
  38. package/mcp-server/package-lock.json +1707 -0
  39. package/mcp-server/package.json +38 -0
  40. package/mcp-server/src/index.ts +1136 -0
  41. package/mcp-server/src/registry/components.ts +1446 -0
  42. package/mcp-server/src/registry/index.ts +3 -0
  43. package/mcp-server/src/registry/tokens.ts +256 -0
  44. package/mcp-server/tsconfig.json +19 -0
  45. package/package.json +92 -0
  46. package/src/components/Accordion/Accordion.stories.tsx +226 -0
  47. package/src/components/Accordion/Accordion.tsx +255 -0
  48. package/src/components/Accordion/index.ts +12 -0
  49. package/src/components/ActionSheet/ActionSheet.stories.tsx +393 -0
  50. package/src/components/ActionSheet/ActionSheet.tsx +258 -0
  51. package/src/components/ActionSheet/index.ts +2 -0
  52. package/src/components/Alert/Alert.stories.tsx +165 -0
  53. package/src/components/Alert/Alert.tsx +164 -0
  54. package/src/components/Alert/index.ts +2 -0
  55. package/src/components/AlertDialog/AlertDialog.stories.tsx +330 -0
  56. package/src/components/AlertDialog/AlertDialog.tsx +234 -0
  57. package/src/components/AlertDialog/index.ts +2 -0
  58. package/src/components/Avatar/Avatar.stories.tsx +154 -0
  59. package/src/components/Avatar/Avatar.tsx +219 -0
  60. package/src/components/Avatar/index.ts +2 -0
  61. package/src/components/Badge/Badge.stories.tsx +146 -0
  62. package/src/components/Badge/Badge.tsx +125 -0
  63. package/src/components/Badge/index.ts +2 -0
  64. package/src/components/Box/Box.stories.tsx +192 -0
  65. package/src/components/Box/Box.tsx +184 -0
  66. package/src/components/Box/index.ts +2 -0
  67. package/src/components/Button/Button.stories.tsx +157 -0
  68. package/src/components/Button/Button.tsx +180 -0
  69. package/src/components/Button/index.ts +2 -0
  70. package/src/components/Card/Card.stories.tsx +145 -0
  71. package/src/components/Card/Card.tsx +169 -0
  72. package/src/components/Card/index.ts +11 -0
  73. package/src/components/Center/Center.stories.tsx +215 -0
  74. package/src/components/Center/Center.tsx +29 -0
  75. package/src/components/Center/index.ts +2 -0
  76. package/src/components/Checkbox/Checkbox.stories.tsx +94 -0
  77. package/src/components/Checkbox/Checkbox.tsx +242 -0
  78. package/src/components/Checkbox/index.ts +2 -0
  79. package/src/components/DatePicker/DatePicker.stories.tsx +623 -0
  80. package/src/components/DatePicker/DatePicker.tsx +1228 -0
  81. package/src/components/DatePicker/index.ts +8 -0
  82. package/src/components/Divider/Divider.stories.tsx +224 -0
  83. package/src/components/Divider/Divider.tsx +73 -0
  84. package/src/components/Divider/index.ts +2 -0
  85. package/src/components/Drawer/Drawer.stories.tsx +414 -0
  86. package/src/components/Drawer/Drawer.tsx +342 -0
  87. package/src/components/Drawer/index.ts +11 -0
  88. package/src/components/Fab/Fab.stories.tsx +360 -0
  89. package/src/components/Fab/Fab.tsx +185 -0
  90. package/src/components/Fab/index.ts +2 -0
  91. package/src/components/FormControl/FormControl.stories.tsx +276 -0
  92. package/src/components/FormControl/FormControl.tsx +185 -0
  93. package/src/components/FormControl/index.ts +12 -0
  94. package/src/components/Grid/Grid.stories.tsx +244 -0
  95. package/src/components/Grid/Grid.tsx +93 -0
  96. package/src/components/Grid/index.ts +2 -0
  97. package/src/components/HStack/HStack.stories.tsx +230 -0
  98. package/src/components/HStack/HStack.tsx +80 -0
  99. package/src/components/HStack/index.ts +2 -0
  100. package/src/components/Heading/Heading.stories.tsx +111 -0
  101. package/src/components/Heading/Heading.tsx +85 -0
  102. package/src/components/Heading/index.ts +2 -0
  103. package/src/components/Icon/Icon.stories.tsx +320 -0
  104. package/src/components/Icon/Icon.tsx +117 -0
  105. package/src/components/Icon/index.ts +2 -0
  106. package/src/components/Image/Image.stories.tsx +357 -0
  107. package/src/components/Image/Image.tsx +168 -0
  108. package/src/components/Image/index.ts +2 -0
  109. package/src/components/Input/Input.stories.tsx +164 -0
  110. package/src/components/Input/Input.tsx +274 -0
  111. package/src/components/Input/index.ts +2 -0
  112. package/src/components/Link/Link.stories.tsx +187 -0
  113. package/src/components/Link/Link.tsx +104 -0
  114. package/src/components/Link/index.ts +2 -0
  115. package/src/components/Menu/Menu.stories.tsx +363 -0
  116. package/src/components/Menu/Menu.tsx +238 -0
  117. package/src/components/Menu/index.ts +2 -0
  118. package/src/components/Modal/Modal.stories.tsx +156 -0
  119. package/src/components/Modal/Modal.tsx +280 -0
  120. package/src/components/Modal/index.ts +11 -0
  121. package/src/components/Popover/Popover.stories.tsx +330 -0
  122. package/src/components/Popover/Popover.tsx +315 -0
  123. package/src/components/Popover/index.ts +11 -0
  124. package/src/components/Portal/Portal.stories.tsx +376 -0
  125. package/src/components/Portal/Portal.tsx +100 -0
  126. package/src/components/Portal/index.ts +2 -0
  127. package/src/components/Pressable/Pressable.stories.tsx +338 -0
  128. package/src/components/Pressable/Pressable.tsx +71 -0
  129. package/src/components/Pressable/index.ts +2 -0
  130. package/src/components/Progress/Progress.stories.tsx +131 -0
  131. package/src/components/Progress/Progress.tsx +219 -0
  132. package/src/components/Progress/index.ts +2 -0
  133. package/src/components/Radio/Radio.stories.tsx +101 -0
  134. package/src/components/Radio/Radio.tsx +234 -0
  135. package/src/components/Radio/index.ts +2 -0
  136. package/src/components/Select/Select.stories.tsx +908 -0
  137. package/src/components/Select/Select.tsx +659 -0
  138. package/src/components/Select/index.ts +8 -0
  139. package/src/components/Skeleton/Skeleton.stories.tsx +154 -0
  140. package/src/components/Skeleton/Skeleton.tsx +192 -0
  141. package/src/components/Skeleton/index.ts +8 -0
  142. package/src/components/Slider/Slider.stories.tsx +363 -0
  143. package/src/components/Slider/Slider.tsx +209 -0
  144. package/src/components/Slider/index.ts +2 -0
  145. package/src/components/Spinner/Spinner.stories.tsx +108 -0
  146. package/src/components/Spinner/Spinner.tsx +121 -0
  147. package/src/components/Spinner/index.ts +2 -0
  148. package/src/components/Switch/Switch.stories.tsx +116 -0
  149. package/src/components/Switch/Switch.tsx +172 -0
  150. package/src/components/Switch/index.ts +2 -0
  151. package/src/components/Table/Table.stories.tsx +417 -0
  152. package/src/components/Table/Table.tsx +233 -0
  153. package/src/components/Table/index.ts +2 -0
  154. package/src/components/Text/Text.stories.tsx +93 -0
  155. package/src/components/Text/Text.tsx +119 -0
  156. package/src/components/Text/index.ts +2 -0
  157. package/src/components/Textarea/Textarea.stories.tsx +280 -0
  158. package/src/components/Textarea/Textarea.tsx +212 -0
  159. package/src/components/Textarea/index.ts +2 -0
  160. package/src/components/Toast/Toast.stories.tsx +446 -0
  161. package/src/components/Toast/Toast.tsx +221 -0
  162. package/src/components/Toast/index.ts +2 -0
  163. package/src/components/Tooltip/Tooltip.stories.tsx +354 -0
  164. package/src/components/Tooltip/Tooltip.tsx +261 -0
  165. package/src/components/Tooltip/index.ts +2 -0
  166. package/src/components/VStack/VStack.stories.tsx +183 -0
  167. package/src/components/VStack/VStack.tsx +76 -0
  168. package/src/components/VStack/index.ts +2 -0
  169. package/src/components/index.ts +62 -0
  170. package/src/hooks/index.ts +7 -0
  171. package/src/hooks/useControllableState.ts +41 -0
  172. package/src/hooks/useDisclosure.ts +51 -0
  173. package/src/index.ts +22 -0
  174. package/src/stories/Button.stories.tsx +53 -0
  175. package/src/stories/Button.tsx +101 -0
  176. package/src/stories/Configure.mdx +364 -0
  177. package/src/stories/Header.stories.tsx +33 -0
  178. package/src/stories/Header.tsx +75 -0
  179. package/src/stories/Page.stories.tsx +25 -0
  180. package/src/stories/Page.tsx +154 -0
  181. package/src/stories/assets/accessibility.png +0 -0
  182. package/src/stories/assets/accessibility.svg +1 -0
  183. package/src/stories/assets/addon-library.png +0 -0
  184. package/src/stories/assets/assets.png +0 -0
  185. package/src/stories/assets/avif-test-image.avif +0 -0
  186. package/src/stories/assets/context.png +0 -0
  187. package/src/stories/assets/discord.svg +1 -0
  188. package/src/stories/assets/docs.png +0 -0
  189. package/src/stories/assets/figma-plugin.png +0 -0
  190. package/src/stories/assets/github.svg +1 -0
  191. package/src/stories/assets/share.png +0 -0
  192. package/src/stories/assets/styling.png +0 -0
  193. package/src/stories/assets/testing.png +0 -0
  194. package/src/stories/assets/theming.png +0 -0
  195. package/src/stories/assets/tutorials.svg +1 -0
  196. package/src/stories/assets/youtube.svg +1 -0
  197. package/src/styles/index.ts +7 -0
  198. package/src/styles/tokens.ts +318 -0
  199. package/src/styles/unistyles.ts +254 -0
  200. package/src/utils/createContext.tsx +25 -0
  201. package/src/utils/index.ts +7 -0
  202. package/src/utils/mergeRefs.ts +21 -0
  203. package/tsconfig.json +26 -0
  204. package/urbint-cl-1.0.0.tgz +0 -0
  205. package/vitest.config.ts +37 -0
  206. package/vitest.shims.d.ts +1 -0
@@ -0,0 +1,659 @@
1
+ /**
2
+ * Select Component
3
+ * Dropdown selection component with popover-style dropdown
4
+ * Supports single and multi-select modes with hover states and search
5
+ */
6
+
7
+ import React, { forwardRef, useState, useRef, useCallback, useEffect } from 'react';
8
+ import { View, ViewProps, Pressable, Text, Modal, ScrollView, StyleSheet, Platform, LayoutRectangle, TextInput } from 'react-native';
9
+ import Svg, { Path } from 'react-native-svg';
10
+ import { colors, spacing, borderRadius, typography, elevation } from '../../styles/tokens';
11
+
12
+ export interface SelectOption {
13
+ label: string;
14
+ value: string;
15
+ disabled?: boolean;
16
+ /** Any additional data for custom rendering */
17
+ [key: string]: any;
18
+ }
19
+
20
+ export interface RenderOptionProps {
21
+ option: SelectOption;
22
+ isSelected: boolean;
23
+ isDisabled: boolean;
24
+ isHovered?: boolean;
25
+ /** Search query for highlighting matches */
26
+ searchQuery?: string;
27
+ }
28
+
29
+ export interface RenderSelectedValueProps {
30
+ /** Single selected option (when isMultiple is false) */
31
+ option: SelectOption | undefined;
32
+ /** Multiple selected options (when isMultiple is true) */
33
+ selectedOptions: SelectOption[];
34
+ placeholder: string;
35
+ isMultiple: boolean;
36
+ }
37
+
38
+ export interface SelectProps extends Omit<ViewProps, 'children'> {
39
+ /** Select options */
40
+ options: SelectOption[];
41
+ /** Selected value (string for single, string[] for multiple) */
42
+ value?: string | string[];
43
+ /** Default value (string for single, string[] for multiple) */
44
+ defaultValue?: string | string[];
45
+ /** On change handler */
46
+ onChange?: (value: string | string[]) => void;
47
+ /** Placeholder text */
48
+ placeholder?: string;
49
+ /** Label text */
50
+ label?: string;
51
+ /** Helper text */
52
+ helperText?: string;
53
+ /** Error message */
54
+ errorMessage?: string;
55
+ /** Is invalid */
56
+ isInvalid?: boolean;
57
+ /** Is disabled */
58
+ isDisabled?: boolean;
59
+ /** Is required */
60
+ isRequired?: boolean;
61
+ /** Enable multiple selection */
62
+ isMultiple?: boolean;
63
+ /** Enable search/filter functionality */
64
+ isSearchable?: boolean;
65
+ /** Placeholder text for search input */
66
+ searchPlaceholder?: string;
67
+ /** Text to display when no results match search */
68
+ noResultsText?: string;
69
+ /** Select size */
70
+ size?: 'sm' | 'md' | 'lg';
71
+ /** Select variant */
72
+ variant?: 'outline' | 'filled';
73
+ /** Custom render function for dropdown options */
74
+ renderOption?: (props: RenderOptionProps) => React.ReactNode;
75
+ /** Custom render function for the selected value display */
76
+ renderSelectedValue?: (props: RenderSelectedValueProps) => React.ReactNode;
77
+ /** Custom filter function for search */
78
+ filterOption?: (option: SelectOption, searchQuery: string) => boolean;
79
+ }
80
+
81
+ export const Select = forwardRef<View, SelectProps>(
82
+ (
83
+ {
84
+ style,
85
+ options,
86
+ value: controlledValue,
87
+ defaultValue,
88
+ onChange,
89
+ placeholder = 'Select an option',
90
+ label,
91
+ helperText,
92
+ errorMessage,
93
+ isInvalid = false,
94
+ isDisabled = false,
95
+ isRequired = false,
96
+ isMultiple = false,
97
+ isSearchable = false,
98
+ searchPlaceholder = 'Search...',
99
+ noResultsText = 'No results found',
100
+ size = 'md',
101
+ variant = 'outline',
102
+ renderOption,
103
+ renderSelectedValue,
104
+ filterOption,
105
+ ...props
106
+ },
107
+ ref
108
+ ) => {
109
+ const [isOpen, setIsOpen] = useState(false);
110
+ const [internalValue, setInternalValue] = useState<string | string[]>(
111
+ defaultValue ?? (isMultiple ? [] : '')
112
+ );
113
+ const [dropdownPosition, setDropdownPosition] = useState<LayoutRectangle | null>(null);
114
+ const [hoveredOption, setHoveredOption] = useState<string | null>(null);
115
+ const [searchQuery, setSearchQuery] = useState('');
116
+ const [searchFocused, setSearchFocused] = useState(false);
117
+ const selectRef = useRef<View>(null);
118
+ const searchInputRef = useRef<TextInput>(null);
119
+
120
+ const isControlled = controlledValue !== undefined;
121
+ const currentValue = isControlled ? controlledValue : internalValue;
122
+
123
+ // Normalize to array for easier handling
124
+ const selectedValues: string[] = isMultiple
125
+ ? (Array.isArray(currentValue) ? currentValue : [currentValue].filter(Boolean))
126
+ : (currentValue ? [currentValue as string] : []);
127
+
128
+ const selectedOptions = options.filter((opt) => selectedValues.includes(opt.value));
129
+ const selectedOption = selectedOptions[0]; // For single select backward compatibility
130
+ const hasError = isInvalid || !!errorMessage;
131
+
132
+ // Default filter function
133
+ const defaultFilterOption = (option: SelectOption, query: string): boolean => {
134
+ return option.label.toLowerCase().includes(query.toLowerCase());
135
+ };
136
+
137
+ // Filter options based on search query
138
+ const filteredOptions = isSearchable && searchQuery
139
+ ? options.filter((option) =>
140
+ filterOption
141
+ ? filterOption(option, searchQuery)
142
+ : defaultFilterOption(option, searchQuery)
143
+ )
144
+ : options;
145
+
146
+ // Reset search when dropdown closes
147
+ useEffect(() => {
148
+ if (!isOpen) {
149
+ setSearchQuery('');
150
+ }
151
+ }, [isOpen]);
152
+
153
+ // Focus search input when dropdown opens
154
+ useEffect(() => {
155
+ if (isOpen && isSearchable && Platform.OS === 'web') {
156
+ // Small delay to ensure modal is rendered
157
+ setTimeout(() => {
158
+ searchInputRef.current?.focus();
159
+ }, 100);
160
+ }
161
+ }, [isOpen, isSearchable]);
162
+
163
+ const handleOpen = useCallback(() => {
164
+ if (isDisabled) return;
165
+
166
+ selectRef.current?.measureInWindow((x, y, width, height) => {
167
+ setDropdownPosition({ x, y, width, height });
168
+ setIsOpen(true);
169
+ });
170
+ }, [isDisabled]);
171
+
172
+ const handleSelect = (optionValue: string) => {
173
+ if (isMultiple) {
174
+ const newValues = selectedValues.includes(optionValue)
175
+ ? selectedValues.filter(v => v !== optionValue)
176
+ : [...selectedValues, optionValue];
177
+
178
+ if (!isControlled) {
179
+ setInternalValue(newValues);
180
+ }
181
+ onChange?.(newValues);
182
+ // Don't close on multi-select
183
+ } else {
184
+ if (!isControlled) {
185
+ setInternalValue(optionValue);
186
+ }
187
+ onChange?.(optionValue);
188
+ setIsOpen(false);
189
+ }
190
+ };
191
+
192
+ const handleClose = () => {
193
+ setIsOpen(false);
194
+ setHoveredOption(null);
195
+ };
196
+
197
+ // Get display text for selected value(s)
198
+ const getDisplayText = () => {
199
+ if (selectedOptions.length === 0) {
200
+ return placeholder;
201
+ }
202
+ if (isMultiple) {
203
+ return selectedOptions.map(opt => opt.label).join(', ');
204
+ }
205
+ return selectedOption?.label || placeholder;
206
+ };
207
+
208
+ // Calculate dropdown max height based on number of options and search
209
+ const searchInputHeight = isSearchable ? 48 : 0;
210
+ const dropdownMaxHeight = Math.min(filteredOptions.length * 48 + 16 + searchInputHeight, 300);
211
+
212
+ // Clear button for multiselect
213
+ const handleClear = (e: any) => {
214
+ e.stopPropagation();
215
+ if (!isControlled) {
216
+ setInternalValue(isMultiple ? [] : '');
217
+ }
218
+ onChange?.(isMultiple ? [] : '');
219
+ };
220
+
221
+ const showClearButton = isMultiple && selectedValues.length > 0 && !isDisabled;
222
+
223
+ return (
224
+ <View ref={ref} style={[styles.container, style]} {...props}>
225
+ {label && (
226
+ <Text style={[styles.label, isDisabled && styles.labelDisabled]}>
227
+ {label}
228
+ {isRequired && <Text style={styles.required}> *</Text>}
229
+ </Text>
230
+ )}
231
+ <Pressable
232
+ ref={selectRef}
233
+ onPress={handleOpen}
234
+ style={({ hovered }: any) => [
235
+ styles.select,
236
+ styles[variant],
237
+ styles[size],
238
+ hasError && styles.error,
239
+ isDisabled && styles.disabled,
240
+ isOpen && styles.selectOpen,
241
+ Platform.OS === 'web' && hovered && !isDisabled && styles.selectHovered,
242
+ ]}
243
+ >
244
+ {renderSelectedValue ? (
245
+ <View style={styles.customSelectedValue}>
246
+ {renderSelectedValue({
247
+ option: selectedOption,
248
+ selectedOptions,
249
+ placeholder,
250
+ isMultiple
251
+ })}
252
+ </View>
253
+ ) : (
254
+ <Text
255
+ style={[
256
+ styles.selectText,
257
+ selectedOptions.length === 0 && styles.placeholder,
258
+ isDisabled && styles.disabledText,
259
+ ]}
260
+ numberOfLines={1}
261
+ >
262
+ {getDisplayText()}
263
+ </Text>
264
+ )}
265
+ <View style={styles.iconContainer}>
266
+ {showClearButton && (
267
+ <Pressable
268
+ onPress={handleClear}
269
+ style={({ hovered }: any) => [
270
+ styles.clearButton,
271
+ Platform.OS === 'web' && hovered && styles.clearButtonHovered,
272
+ ]}
273
+ hitSlop={8}
274
+ >
275
+ <Svg width={16} height={16} viewBox="0 0 24 24" fill="none">
276
+ <Path
277
+ d="M18 6L6 18M6 6l12 12"
278
+ stroke={colors.text.secondary}
279
+ strokeWidth={2}
280
+ strokeLinecap="round"
281
+ strokeLinejoin="round"
282
+ />
283
+ </Svg>
284
+ </Pressable>
285
+ )}
286
+ <Svg
287
+ width={20}
288
+ height={20}
289
+ viewBox="0 0 24 24"
290
+ fill="none"
291
+ style={{ transform: [{ rotate: isOpen ? '180deg' : '0deg' }] }}
292
+ >
293
+ <Path
294
+ d="M6 9l6 6 6-6"
295
+ stroke={isDisabled ? colors.text.disabled : colors.text.secondary}
296
+ strokeWidth={2}
297
+ strokeLinecap="round"
298
+ strokeLinejoin="round"
299
+ />
300
+ </Svg>
301
+ </View>
302
+ </Pressable>
303
+ {(helperText || errorMessage) && (
304
+ <Text style={[styles.helperText, hasError && styles.errorText]}>
305
+ {errorMessage || helperText}
306
+ </Text>
307
+ )}
308
+
309
+ <Modal
310
+ visible={isOpen}
311
+ transparent
312
+ animationType="fade"
313
+ onRequestClose={handleClose}
314
+ >
315
+ <Pressable style={styles.modalOverlay} onPress={handleClose}>
316
+ <View
317
+ style={[
318
+ styles.dropdown,
319
+ dropdownPosition && {
320
+ position: 'absolute',
321
+ top: dropdownPosition.y + dropdownPosition.height + 4,
322
+ left: dropdownPosition.x,
323
+ width: dropdownPosition.width,
324
+ maxHeight: dropdownMaxHeight,
325
+ },
326
+ ]}
327
+ // Prevent closing when clicking inside dropdown
328
+ onStartShouldSetResponder={() => true}
329
+ >
330
+ {isSearchable && (
331
+ <View style={styles.searchContainer}>
332
+ <View style={[
333
+ styles.searchInputWrapper,
334
+ searchFocused && styles.searchInputWrapperFocused,
335
+ ]}>
336
+ <Svg
337
+ width={16}
338
+ height={16}
339
+ viewBox="0 0 24 24"
340
+ fill="none"
341
+ style={styles.searchIcon}
342
+ >
343
+ <Path
344
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
345
+ stroke={searchFocused ? colors.brand.blue : colors.text.secondary}
346
+ strokeWidth={2}
347
+ strokeLinecap="round"
348
+ strokeLinejoin="round"
349
+ />
350
+ </Svg>
351
+ <TextInput
352
+ ref={searchInputRef}
353
+ style={styles.searchInput}
354
+ placeholder={searchPlaceholder}
355
+ placeholderTextColor={colors.text.disabled}
356
+ value={searchQuery}
357
+ onChangeText={setSearchQuery}
358
+ onFocus={() => setSearchFocused(true)}
359
+ onBlur={() => setSearchFocused(false)}
360
+ autoCapitalize="none"
361
+ autoCorrect={false}
362
+ />
363
+ {searchQuery.length > 0 && (
364
+ <Pressable
365
+ onPress={() => setSearchQuery('')}
366
+ style={({ hovered }: any) => [
367
+ styles.searchClearButton,
368
+ Platform.OS === 'web' && hovered && styles.clearButtonHovered,
369
+ ]}
370
+ hitSlop={8}
371
+ >
372
+ <Svg width={14} height={14} viewBox="0 0 24 24" fill="none">
373
+ <Path
374
+ d="M18 6L6 18M6 6l12 12"
375
+ stroke={colors.text.secondary}
376
+ strokeWidth={2}
377
+ strokeLinecap="round"
378
+ strokeLinejoin="round"
379
+ />
380
+ </Svg>
381
+ </Pressable>
382
+ )}
383
+ </View>
384
+ </View>
385
+ )}
386
+ <ScrollView
387
+ style={styles.optionsList}
388
+ showsVerticalScrollIndicator={true}
389
+ nestedScrollEnabled
390
+ keyboardShouldPersistTaps="handled"
391
+ >
392
+ {filteredOptions.length === 0 ? (
393
+ <View style={styles.noResults}>
394
+ <Text style={styles.noResultsText}>{noResultsText}</Text>
395
+ </View>
396
+ ) : (
397
+ filteredOptions.map((option) => {
398
+ const isSelected = selectedValues.includes(option.value);
399
+ const isOptionDisabled = !!option.disabled;
400
+ const isHovered = hoveredOption === option.value;
401
+
402
+ return (
403
+ <Pressable
404
+ key={option.value}
405
+ onPress={() => !isOptionDisabled && handleSelect(option.value)}
406
+ onHoverIn={() => Platform.OS === 'web' && setHoveredOption(option.value)}
407
+ onHoverOut={() => Platform.OS === 'web' && setHoveredOption(null)}
408
+ style={({ pressed }) => [
409
+ styles.option,
410
+ isSelected && styles.optionSelected,
411
+ isOptionDisabled && styles.optionDisabled,
412
+ pressed && !isOptionDisabled && styles.optionPressed,
413
+ Platform.OS === 'web' && isHovered && !isOptionDisabled && styles.optionHovered,
414
+ ]}
415
+ >
416
+ {renderOption ? (
417
+ renderOption({ option, isSelected, isDisabled: isOptionDisabled, isHovered, searchQuery })
418
+ ) : (
419
+ <>
420
+ <Text
421
+ style={[
422
+ styles.optionText,
423
+ isSelected && styles.optionTextSelected,
424
+ isOptionDisabled && styles.optionTextDisabled,
425
+ ]}
426
+ >
427
+ {option.label}
428
+ </Text>
429
+ {isSelected && (
430
+ <Svg width={16} height={16} viewBox="0 0 24 24" fill="none">
431
+ <Path
432
+ d="M20 6L9 17l-5-5"
433
+ stroke={colors.brand.blue}
434
+ strokeWidth={2}
435
+ strokeLinecap="round"
436
+ strokeLinejoin="round"
437
+ />
438
+ </Svg>
439
+ )}
440
+ </>
441
+ )}
442
+ </Pressable>
443
+ );
444
+ })
445
+ )}
446
+ </ScrollView>
447
+ </View>
448
+ </Pressable>
449
+ </Modal>
450
+ </View>
451
+ );
452
+ }
453
+ );
454
+
455
+ Select.displayName = 'Select';
456
+
457
+ const styles = StyleSheet.create({
458
+ container: {
459
+ width: '100%',
460
+ },
461
+ label: {
462
+ fontSize: typography.fontSize.componentLabel,
463
+ fontWeight: typography.fontWeight.semiBold,
464
+ color: colors.text.default,
465
+ marginBottom: spacing.base,
466
+ },
467
+ labelDisabled: {
468
+ color: colors.text.disabled,
469
+ },
470
+ required: {
471
+ color: colors.feedback.error.content,
472
+ },
473
+ select: {
474
+ flexDirection: 'row',
475
+ alignItems: 'center',
476
+ justifyContent: 'space-between',
477
+ borderRadius: borderRadius.md,
478
+ },
479
+ selectOpen: {
480
+ borderColor: colors.brand.blue,
481
+ },
482
+ selectHovered: {
483
+ borderColor: colors.brand.blue,
484
+ backgroundColor: colors.background.hover,
485
+ },
486
+ outline: {
487
+ borderWidth: 1,
488
+ borderColor: colors.border.default,
489
+ backgroundColor: colors.background.default,
490
+ },
491
+ filled: {
492
+ backgroundColor: colors.background.secondary,
493
+ },
494
+ sm: {
495
+ height: 32,
496
+ paddingHorizontal: spacing['2x'],
497
+ },
498
+ md: {
499
+ height: 40,
500
+ paddingHorizontal: spacing['3x'],
501
+ },
502
+ lg: {
503
+ height: 48,
504
+ paddingHorizontal: spacing['4x'],
505
+ },
506
+ error: {
507
+ borderColor: colors.border.danger,
508
+ },
509
+ disabled: {
510
+ backgroundColor: colors.background.secondary,
511
+ borderColor: colors.border.disabled,
512
+ },
513
+ selectText: {
514
+ flex: 1,
515
+ color: colors.text.default,
516
+ fontSize: typography.fontSize.body,
517
+ },
518
+ customSelectedValue: {
519
+ flex: 1,
520
+ flexDirection: 'row',
521
+ alignItems: 'center',
522
+ },
523
+ placeholder: {
524
+ color: colors.text.disabled,
525
+ },
526
+ disabledText: {
527
+ color: colors.text.disabled,
528
+ },
529
+ iconContainer: {
530
+ flexDirection: 'row',
531
+ alignItems: 'center',
532
+ gap: spacing.base,
533
+ },
534
+ clearButton: {
535
+ padding: spacing['0.5x'],
536
+ borderRadius: borderRadius.sm,
537
+ },
538
+ clearButtonHovered: {
539
+ backgroundColor: colors.background.secondary,
540
+ },
541
+ helperText: {
542
+ fontSize: typography.fontSize.caption,
543
+ color: colors.text.secondary,
544
+ marginTop: spacing.base,
545
+ },
546
+ errorText: {
547
+ color: colors.feedback.error.content,
548
+ },
549
+ modalOverlay: {
550
+ flex: 1,
551
+ backgroundColor: 'transparent',
552
+ },
553
+ dropdown: {
554
+ backgroundColor: colors.background.default,
555
+ borderRadius: borderRadius.md,
556
+ borderWidth: 1,
557
+ borderColor: colors.border.default,
558
+ minWidth: 200,
559
+ ...Platform.select({
560
+ ios: {
561
+ shadowColor: '#000',
562
+ shadowOffset: { width: 0, height: 4 },
563
+ shadowOpacity: 0.15,
564
+ shadowRadius: 12,
565
+ },
566
+ android: {
567
+ elevation: 8,
568
+ },
569
+ web: {
570
+ boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
571
+ } as any,
572
+ }),
573
+ },
574
+ searchContainer: {
575
+ padding: spacing['2x'],
576
+ borderBottomWidth: 1,
577
+ borderBottomColor: colors.border.disabled,
578
+ backgroundColor: colors.background.default,
579
+ },
580
+ searchInputWrapper: {
581
+ flexDirection: 'row',
582
+ alignItems: 'center',
583
+ backgroundColor: colors.background.secondary,
584
+ borderWidth: 1,
585
+ borderColor: colors.border.default,
586
+ borderRadius: borderRadius.md,
587
+ paddingHorizontal: spacing['2x'],
588
+ height: 36,
589
+ },
590
+ searchInputWrapperFocused: {
591
+ borderColor: colors.brand.blue,
592
+ backgroundColor: colors.background.default,
593
+ },
594
+ searchIcon: {
595
+ marginRight: spacing['2x'],
596
+ },
597
+ searchInput: {
598
+ flex: 1,
599
+ fontSize: typography.fontSize.body,
600
+ color: colors.text.default,
601
+ padding: 0,
602
+ margin: 0,
603
+ height: '100%',
604
+ ...Platform.select({
605
+ web: {
606
+ outlineStyle: 'none',
607
+ } as any,
608
+ }),
609
+ },
610
+ searchClearButton: {
611
+ padding: spacing.base,
612
+ borderRadius: borderRadius.sm,
613
+ marginLeft: spacing.base,
614
+ },
615
+ optionsList: {
616
+ paddingVertical: spacing.base,
617
+ paddingHorizontal: spacing['2x'],
618
+ },
619
+ noResults: {
620
+ paddingVertical: spacing['4x'],
621
+ paddingHorizontal: spacing['2x'],
622
+ alignItems: 'center',
623
+ },
624
+ noResultsText: {
625
+ fontSize: typography.fontSize.body,
626
+ color: colors.text.secondary,
627
+ },
628
+ option: {
629
+ flexDirection: 'row',
630
+ alignItems: 'center',
631
+ justifyContent: 'space-between',
632
+ paddingVertical: spacing['2x'],
633
+ paddingHorizontal: spacing['2x'],
634
+ borderRadius: borderRadius.sm,
635
+ },
636
+ optionSelected: {
637
+ backgroundColor: `${colors.brand.blue}10`,
638
+ },
639
+ optionHovered: {
640
+ backgroundColor: colors.background.hover,
641
+ },
642
+ optionPressed: {
643
+ backgroundColor: colors.background.secondary,
644
+ },
645
+ optionDisabled: {
646
+ opacity: 0.5,
647
+ },
648
+ optionText: {
649
+ fontSize: typography.fontSize.body,
650
+ color: colors.text.default,
651
+ },
652
+ optionTextSelected: {
653
+ color: colors.brand.blue,
654
+ fontWeight: typography.fontWeight.medium,
655
+ },
656
+ optionTextDisabled: {
657
+ color: colors.text.disabled,
658
+ },
659
+ });
@@ -0,0 +1,8 @@
1
+ export {
2
+ Select,
3
+ type SelectProps,
4
+ type SelectOption,
5
+ type RenderOptionProps,
6
+ type RenderSelectedValueProps
7
+ } from './Select';
8
+