@idealyst/components 1.0.83 → 1.0.85

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 (316) hide show
  1. package/CLAUDE.md +199 -232
  2. package/README.md +5 -5
  3. package/package.json +20 -2
  4. package/plugin/README.md +272 -0
  5. package/plugin/test-cases.jsx +112 -0
  6. package/plugin/web-legacy.js +320 -0
  7. package/plugin/web.js +422 -124
  8. package/src/Accordion/Accordion.native.tsx +182 -0
  9. package/src/Accordion/Accordion.styles.tsx +260 -0
  10. package/src/Accordion/Accordion.web.tsx +147 -0
  11. package/src/Accordion/index.native.tsx +3 -0
  12. package/src/Accordion/index.ts +3 -0
  13. package/src/Accordion/index.web.tsx +3 -0
  14. package/src/Accordion/types.ts +23 -0
  15. package/src/ActivityIndicator/ActivityIndicator.native.tsx +17 -12
  16. package/src/ActivityIndicator/ActivityIndicator.styles.tsx +83 -109
  17. package/src/ActivityIndicator/ActivityIndicator.web.tsx +23 -17
  18. package/src/ActivityIndicator/index.ts +5 -2
  19. package/src/ActivityIndicator/index.web.ts +5 -2
  20. package/src/ActivityIndicator/types.ts +15 -10
  21. package/src/Alert/Alert.native.tsx +113 -0
  22. package/src/Alert/Alert.styles.tsx +304 -0
  23. package/src/Alert/Alert.web.tsx +123 -0
  24. package/src/Alert/index.native.ts +5 -0
  25. package/src/Alert/index.ts +5 -0
  26. package/src/Alert/index.web.ts +5 -0
  27. package/src/Alert/types.ts +21 -0
  28. package/src/Avatar/Avatar.native.tsx +8 -6
  29. package/src/Avatar/Avatar.styles.tsx +64 -58
  30. package/src/Avatar/Avatar.web.tsx +13 -8
  31. package/src/Avatar/index.ts +5 -2
  32. package/src/Avatar/index.web.ts +5 -2
  33. package/src/Avatar/types.ts +19 -13
  34. package/src/Badge/Badge.native.tsx +59 -14
  35. package/src/Badge/Badge.styles.tsx +125 -139
  36. package/src/Badge/Badge.web.tsx +72 -16
  37. package/src/Badge/index.ts +5 -2
  38. package/src/Badge/index.web.ts +5 -2
  39. package/src/Badge/types.ts +23 -11
  40. package/src/Breadcrumb/Breadcrumb.native.tsx +225 -0
  41. package/src/Breadcrumb/Breadcrumb.styles.tsx +234 -0
  42. package/src/Breadcrumb/Breadcrumb.web.tsx +268 -0
  43. package/src/Breadcrumb/index.native.ts +5 -0
  44. package/src/Breadcrumb/index.ts +5 -0
  45. package/src/Breadcrumb/index.web.ts +5 -0
  46. package/src/Breadcrumb/types.ts +56 -0
  47. package/src/Button/Button.native.tsx +75 -24
  48. package/src/Button/Button.styles.tsx +248 -205
  49. package/src/Button/Button.web.tsx +82 -25
  50. package/src/Button/index.ts +5 -5
  51. package/src/Button/index.web.ts +5 -3
  52. package/src/Button/types.ts +32 -15
  53. package/src/Card/Card.native.tsx +14 -11
  54. package/src/Card/Card.styles.tsx +146 -220
  55. package/src/Card/Card.web.tsx +20 -21
  56. package/src/Card/index.ts +5 -5
  57. package/src/Card/index.web.ts +5 -3
  58. package/src/Card/types.ts +24 -17
  59. package/src/Checkbox/Checkbox.native.tsx +24 -34
  60. package/src/Checkbox/Checkbox.styles.tsx +223 -275
  61. package/src/Checkbox/Checkbox.web.tsx +30 -37
  62. package/src/Checkbox/index.ts +5 -5
  63. package/src/Checkbox/index.web.ts +5 -3
  64. package/src/Checkbox/types.ts +26 -20
  65. package/src/Chip/Chip.native.tsx +126 -0
  66. package/src/Chip/Chip.styles.tsx +138 -0
  67. package/src/Chip/Chip.web.tsx +154 -0
  68. package/src/Chip/index.native.ts +5 -0
  69. package/src/Chip/index.ts +5 -0
  70. package/src/Chip/index.web.ts +5 -0
  71. package/src/Chip/types.ts +51 -0
  72. package/src/Dialog/Dialog.native.tsx +65 -12
  73. package/src/Dialog/Dialog.styles.tsx +154 -136
  74. package/src/Dialog/Dialog.web.tsx +16 -11
  75. package/src/Dialog/index.ts +5 -2
  76. package/src/Dialog/index.web.ts +5 -2
  77. package/src/Dialog/types.ts +22 -16
  78. package/src/Divider/Divider.native.tsx +19 -14
  79. package/src/Divider/Divider.styles.tsx +273 -595
  80. package/src/Divider/Divider.web.tsx +19 -12
  81. package/src/Divider/index.ts +5 -5
  82. package/src/Divider/index.web.ts +5 -3
  83. package/src/Divider/types.ts +28 -19
  84. package/src/Icon/Icon.native.tsx +17 -24
  85. package/src/Icon/Icon.styles.tsx +64 -48
  86. package/src/Icon/Icon.web.tsx +14 -11
  87. package/src/Icon/IconSvg/IconSvg.native.tsx +42 -0
  88. package/src/Icon/IconSvg/IconSvg.web.tsx +40 -0
  89. package/src/Icon/IconSvg/index.native.ts +1 -0
  90. package/src/Icon/IconSvg/index.ts +1 -0
  91. package/src/Icon/icon-resolver.native.ts +27 -0
  92. package/src/Icon/icon-resolver.ts +70 -0
  93. package/src/Icon/index.ts +5 -5
  94. package/src/Icon/index.web.ts +5 -3
  95. package/src/Icon/types.ts +17 -11
  96. package/src/Image/Image.native.tsx +86 -0
  97. package/src/Image/Image.styles.tsx +57 -0
  98. package/src/Image/Image.web.tsx +92 -0
  99. package/src/Image/index.native.ts +5 -0
  100. package/src/Image/index.ts +5 -0
  101. package/src/Image/types.ts +21 -0
  102. package/src/Input/Input.native.tsx +103 -26
  103. package/src/Input/Input.styles.tsx +240 -177
  104. package/src/Input/Input.web.tsx +141 -38
  105. package/src/Input/index.ts +5 -5
  106. package/src/Input/index.web.ts +5 -3
  107. package/src/Input/types.ts +43 -20
  108. package/src/List/List.native.tsx +56 -0
  109. package/src/List/List.styles.tsx +257 -0
  110. package/src/List/List.web.tsx +43 -0
  111. package/src/List/ListContext.tsx +16 -0
  112. package/src/List/ListItem.native.tsx +111 -0
  113. package/src/List/ListItem.web.tsx +110 -0
  114. package/src/List/ListSection.native.tsx +31 -0
  115. package/src/List/ListSection.web.tsx +33 -0
  116. package/src/List/index.native.tsx +5 -0
  117. package/src/List/index.ts +5 -0
  118. package/src/List/index.web.tsx +5 -0
  119. package/src/List/types.ts +42 -0
  120. package/src/Menu/Menu.native.tsx +150 -0
  121. package/src/Menu/Menu.styles.tsx +185 -0
  122. package/src/Menu/Menu.web.tsx +99 -0
  123. package/src/Menu/MenuItem.native.tsx +66 -0
  124. package/src/Menu/MenuItem.styles.tsx +119 -0
  125. package/src/Menu/MenuItem.web.tsx +67 -0
  126. package/src/Menu/index.native.ts +3 -0
  127. package/src/Menu/index.ts +3 -0
  128. package/src/Menu/index.web.ts +3 -0
  129. package/src/Menu/types.ts +30 -0
  130. package/src/Popover/Popover.native.tsx +102 -32
  131. package/src/Popover/Popover.styles.tsx +100 -67
  132. package/src/Popover/Popover.web.tsx +36 -260
  133. package/src/Popover/index.ts +5 -2
  134. package/src/Popover/index.web.ts +5 -2
  135. package/src/Popover/types.ts +14 -13
  136. package/src/Pressable/Pressable.native.tsx +7 -6
  137. package/src/Pressable/Pressable.web.tsx +8 -6
  138. package/src/Pressable/index.ts +5 -2
  139. package/src/Pressable/index.web.ts +5 -2
  140. package/src/Pressable/types.ts +11 -10
  141. package/src/Progress/Progress.native.tsx +179 -0
  142. package/src/Progress/Progress.styles.tsx +164 -0
  143. package/src/Progress/Progress.web.tsx +144 -0
  144. package/src/Progress/index.native.ts +1 -0
  145. package/src/Progress/index.ts +5 -0
  146. package/src/Progress/index.web.ts +5 -0
  147. package/src/Progress/types.ts +21 -0
  148. package/src/RadioButton/RadioButton.native.tsx +88 -0
  149. package/src/RadioButton/RadioButton.styles.tsx +163 -0
  150. package/src/RadioButton/RadioButton.web.tsx +85 -0
  151. package/src/RadioButton/RadioGroup.native.tsx +43 -0
  152. package/src/RadioButton/RadioGroup.web.tsx +49 -0
  153. package/src/RadioButton/index.native.ts +2 -0
  154. package/src/RadioButton/index.ts +2 -0
  155. package/src/RadioButton/index.web.ts +2 -0
  156. package/src/RadioButton/types.ts +29 -0
  157. package/src/SVGImage/SVGImage.native.tsx +9 -7
  158. package/src/SVGImage/SVGImage.styles.tsx +63 -55
  159. package/src/SVGImage/SVGImage.web.tsx +16 -13
  160. package/src/SVGImage/index.ts +5 -5
  161. package/src/SVGImage/index.web.ts +5 -2
  162. package/src/SVGImage/types.ts +7 -3
  163. package/src/Screen/Screen.native.tsx +43 -17
  164. package/src/Screen/Screen.styles.tsx +58 -54
  165. package/src/Screen/Screen.web.tsx +11 -5
  166. package/src/Screen/index.ts +5 -2
  167. package/src/Screen/index.web.ts +5 -2
  168. package/src/Screen/types.ts +23 -9
  169. package/src/Select/Select.native.tsx +140 -63
  170. package/src/Select/Select.styles.tsx +312 -302
  171. package/src/Select/Select.web.tsx +156 -316
  172. package/src/Select/index.ts +5 -2
  173. package/src/Select/index.web.ts +5 -2
  174. package/src/Select/types.ts +13 -7
  175. package/src/Skeleton/Skeleton.native.tsx +139 -0
  176. package/src/Skeleton/Skeleton.styles.tsx +59 -0
  177. package/src/Skeleton/Skeleton.web.tsx +112 -0
  178. package/src/Skeleton/index.native.ts +4 -0
  179. package/src/Skeleton/index.ts +5 -0
  180. package/src/Skeleton/index.web.ts +5 -0
  181. package/src/Skeleton/types.ts +75 -0
  182. package/src/Slider/Slider.native.tsx +248 -0
  183. package/src/Slider/Slider.styles.tsx +241 -0
  184. package/src/Slider/Slider.web.tsx +226 -0
  185. package/src/Slider/index.native.ts +3 -0
  186. package/src/Slider/index.ts +5 -0
  187. package/src/Slider/index.web.ts +5 -0
  188. package/src/Slider/types.ts +31 -0
  189. package/src/Switch/Switch.native.tsx +131 -0
  190. package/src/Switch/Switch.styles.tsx +169 -0
  191. package/src/Switch/Switch.web.tsx +121 -0
  192. package/src/Switch/index.native.ts +3 -0
  193. package/src/Switch/index.ts +5 -0
  194. package/src/Switch/index.web.ts +5 -0
  195. package/src/Switch/types.ts +21 -0
  196. package/src/TabBar/TabBar.native.tsx +142 -0
  197. package/src/TabBar/TabBar.styles.tsx +399 -0
  198. package/src/TabBar/TabBar.web.tsx +205 -0
  199. package/src/TabBar/index.native.tsx +3 -0
  200. package/src/TabBar/index.ts +3 -0
  201. package/src/TabBar/index.web.tsx +3 -0
  202. package/src/TabBar/types.ts +26 -0
  203. package/src/Table/Table.native.tsx +122 -0
  204. package/src/Table/Table.styles.tsx +283 -0
  205. package/src/Table/Table.web.tsx +112 -0
  206. package/src/Table/index.native.tsx +3 -0
  207. package/src/Table/index.ts +3 -0
  208. package/src/Table/index.web.tsx +3 -0
  209. package/src/Table/types.ts +28 -0
  210. package/src/Text/Text.native.tsx +12 -11
  211. package/src/Text/Text.styles.tsx +76 -64
  212. package/src/Text/Text.web.tsx +14 -9
  213. package/src/Text/index.ts +5 -5
  214. package/src/Text/index.web.ts +5 -3
  215. package/src/Text/types.ts +20 -13
  216. package/src/TextArea/TextArea.native.tsx +134 -0
  217. package/src/TextArea/TextArea.styles.tsx +175 -0
  218. package/src/TextArea/TextArea.web.tsx +156 -0
  219. package/src/TextArea/index.native.ts +3 -0
  220. package/src/TextArea/index.ts +3 -0
  221. package/src/TextArea/index.web.ts +3 -0
  222. package/src/TextArea/types.ts +30 -0
  223. package/src/Tooltip/Tooltip.native.tsx +165 -0
  224. package/src/Tooltip/Tooltip.styles.tsx +73 -0
  225. package/src/Tooltip/Tooltip.web.tsx +87 -0
  226. package/src/Tooltip/index.native.ts +3 -0
  227. package/src/Tooltip/index.ts +3 -0
  228. package/src/Tooltip/types.ts +18 -0
  229. package/src/Video/Video.native.tsx +105 -0
  230. package/src/Video/Video.styles.tsx +39 -0
  231. package/src/Video/Video.web.tsx +115 -0
  232. package/src/Video/index.native.ts +5 -0
  233. package/src/Video/index.ts +5 -0
  234. package/src/Video/types.ts +29 -0
  235. package/src/View/View.native.tsx +9 -14
  236. package/src/View/View.styles.tsx +101 -93
  237. package/src/View/View.web.tsx +16 -17
  238. package/src/View/index.ts +5 -5
  239. package/src/View/index.web.ts +5 -3
  240. package/src/View/types.ts +29 -21
  241. package/src/examples/AccordionExamples.tsx +126 -0
  242. package/src/examples/AlertExamples.tsx +280 -0
  243. package/src/examples/AvatarExamples.tsx +23 -23
  244. package/src/examples/BadgeExamples.tsx +109 -41
  245. package/src/examples/BreadcrumbExamples.tsx +312 -0
  246. package/src/examples/ButtonExamples.tsx +160 -33
  247. package/src/examples/CardExamples.tsx +40 -40
  248. package/src/examples/CheckboxExamples.tsx +12 -12
  249. package/src/examples/ChipExamples.tsx +197 -0
  250. package/src/examples/DialogExamples.tsx +22 -22
  251. package/src/examples/DividerExamples.tsx +49 -49
  252. package/src/examples/IconExamples.tsx +270 -54
  253. package/src/examples/ImageExamples.tsx +174 -0
  254. package/src/examples/InputExamples.tsx +75 -17
  255. package/src/examples/ListExamples.tsx +288 -0
  256. package/src/examples/MenuExamples.tsx +144 -0
  257. package/src/examples/PopoverExamples.tsx +69 -73
  258. package/src/examples/ProgressExamples.tsx +137 -0
  259. package/src/examples/RadioButtonExamples.tsx +161 -0
  260. package/src/examples/SVGImageExamples.tsx +19 -17
  261. package/src/examples/ScreenExamples.tsx +31 -31
  262. package/src/examples/SelectExamples.tsx +67 -67
  263. package/src/examples/SkeletonExamples.tsx +206 -0
  264. package/src/examples/SliderExamples.tsx +200 -0
  265. package/src/examples/SwitchExamples.tsx +182 -0
  266. package/src/examples/TabBarExamples.tsx +143 -0
  267. package/src/examples/TableExamples.tsx +280 -0
  268. package/src/examples/TextAreaExamples.tsx +173 -0
  269. package/src/examples/TextExamples.tsx +28 -32
  270. package/src/examples/ThemeExtensionExamples.tsx +10 -10
  271. package/src/examples/TooltipExamples.tsx +126 -0
  272. package/src/examples/VideoExamples.tsx +144 -0
  273. package/src/examples/ViewExamples.tsx +64 -56
  274. package/src/examples/index.ts +17 -3
  275. package/src/hooks/useMergeRefs.ts +16 -0
  276. package/src/hooks/useSmartPosition.native.ts +169 -0
  277. package/src/index.native.ts +80 -9
  278. package/src/index.ts +71 -1
  279. package/src/internal/BoundedModalContent.native.tsx +58 -0
  280. package/src/internal/PositionedPortal.tsx +254 -0
  281. package/src/internal/SafeAreaDebugOverlay.native.tsx +173 -0
  282. package/src/unistyles.d.ts +6 -0
  283. package/src/utils/buildSizeVariants.ts +16 -0
  284. package/src/utils/deepMerge.ts +43 -0
  285. package/src/utils/positionUtils.native.ts +280 -0
  286. package/src/utils/styleHelpers.ts +48 -0
  287. package/LLM-ACCESS-GUIDE.md +0 -143
  288. package/src/ActivityIndicator/README.md +0 -132
  289. package/src/Avatar/README.md +0 -139
  290. package/src/Badge/README.md +0 -170
  291. package/src/Button/Button.types.ts +0 -12
  292. package/src/Button/README.md +0 -262
  293. package/src/Card/README.md +0 -258
  294. package/src/Checkbox/README.md +0 -102
  295. package/src/Dialog/README.md +0 -210
  296. package/src/Divider/README.md +0 -108
  297. package/src/Icon/README.md +0 -81
  298. package/src/Input/README.md +0 -100
  299. package/src/SVGImage/README.md +0 -209
  300. package/src/Screen/README.md +0 -86
  301. package/src/Select/README.md +0 -166
  302. package/src/Text/README.md +0 -94
  303. package/src/View/README.md +0 -107
  304. package/src/examples/AllExamples.tsx +0 -88
  305. package/src/examples/README.md +0 -136
  306. package/src/examples/ValidationExamples.tsx +0 -95
  307. package/src/examples/extendedTheme.ts +0 -329
  308. package/src/theme/breakpoints.ts +0 -8
  309. package/src/theme/colorResolver.ts +0 -218
  310. package/src/theme/colors.ts +0 -315
  311. package/src/theme/defaultThemes.ts +0 -326
  312. package/src/theme/index.ts +0 -188
  313. package/src/theme/themeBuilder.ts +0 -602
  314. package/src/theme/unistyles.d.ts +0 -6
  315. package/src/theme/variantHelpers.ts +0 -584
  316. package/src/theme/variants.ts +0 -56
@@ -1,11 +1,14 @@
1
- import React, { useState, useRef, useEffect } from 'react';
2
- import { createPortal } from 'react-dom';
1
+ import React, { useState, useRef, useEffect, forwardRef } from 'react';
3
2
  // @ts-ignore - web-specific import
4
3
  import { getWebProps } from 'react-native-unistyles/web';
5
4
  import { SelectProps, SelectOption } from './types';
6
5
  import { selectStyles } from './Select.styles';
6
+ import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
7
+ import { resolveIconPath } from '../Icon/icon-resolver';
8
+ import { PositionedPortal } from '../internal/PositionedPortal';
9
+ import useMergeRefs from '../hooks/useMergeRefs';
7
10
 
8
- const Select: React.FC<SelectProps> = ({
11
+ const Select = forwardRef<HTMLDivElement, SelectProps>(({
9
12
  options,
10
13
  value,
11
14
  onValueChange,
@@ -14,29 +17,22 @@ const Select: React.FC<SelectProps> = ({
14
17
  error = false,
15
18
  helperText,
16
19
  label,
17
- variant = 'outlined',
20
+ type = 'outlined',
18
21
  intent = 'neutral',
19
- size = 'medium',
22
+ size = 'md',
20
23
  searchable = false,
21
24
  filterOption,
22
25
  maxHeight = 240,
23
26
  style,
24
27
  testID,
25
28
  accessibilityLabel,
26
- }) => {
29
+ }, ref) => {
27
30
  const [isOpen, setIsOpen] = useState(false);
28
31
  const [searchTerm, setSearchTerm] = useState('');
29
32
  const [focusedIndex, setFocusedIndex] = useState(-1);
30
- const triggerRef = useRef<HTMLButtonElement>(null);
31
- const dropdownRef = useRef<HTMLDivElement>(null);
33
+ const triggerRef = useRef<HTMLDivElement>(null);
32
34
  const searchInputRef = useRef<HTMLInputElement>(null);
33
35
 
34
- // Debug: Log when trigger ref is set
35
- const setTriggerRef = (el: HTMLButtonElement | null) => {
36
- console.log('Setting trigger ref to:', el);
37
- triggerRef.current = el;
38
- };
39
-
40
36
  const selectedOption = options.find(option => option.value === value);
41
37
 
42
38
  // Filter options based on search term
@@ -51,166 +47,48 @@ const Select: React.FC<SelectProps> = ({
51
47
 
52
48
  // Apply styles with variants
53
49
  selectStyles.useVariants({
54
- variant: variant as any,
50
+ type,
55
51
  size,
56
- intent,
57
52
  disabled,
58
53
  error,
59
54
  focused: isOpen,
60
55
  });
61
56
 
62
- // Position dropdown when it opens
63
- useEffect(() => {
64
- if (!isOpen) return;
65
-
66
- let retryCount = 0;
67
- const maxRetries = 10;
68
-
69
- const positionDropdown = () => {
70
- if (!triggerRef.current || !dropdownRef.current) {
71
- console.log(`[Attempt ${retryCount + 1}/${maxRetries}] Refs not ready:`, {
72
- trigger: !!triggerRef.current,
73
- dropdown: !!dropdownRef.current
74
- });
75
-
76
- if (retryCount < maxRetries) {
77
- retryCount++;
78
- setTimeout(positionDropdown, 10);
79
- }
80
- return;
81
- }
82
-
83
- const trigger = triggerRef.current;
84
- const dropdown = dropdownRef.current;
85
- const triggerRect = trigger.getBoundingClientRect();
86
-
87
- console.log('Trigger button found at:', {
88
- top: triggerRect.top,
89
- left: triggerRect.left,
90
- bottom: triggerRect.bottom,
91
- right: triggerRect.right,
92
- width: triggerRect.width,
93
- height: triggerRect.height
94
- });
95
-
96
- console.log('Dropdown initial position:', {
97
- currentTop: dropdown.style.top,
98
- currentLeft: dropdown.style.left,
99
- offsetHeight: dropdown.offsetHeight,
100
- offsetWidth: dropdown.offsetWidth
101
- });
102
-
103
- // Calculate and set position
104
- const top = triggerRect.bottom + 4;
105
- const left = triggerRect.left;
106
- const width = triggerRect.width;
107
-
108
- dropdown.style.position = 'fixed';
109
- dropdown.style.top = `${top}px`;
110
- dropdown.style.left = `${left}px`;
111
- dropdown.style.width = `${width}px`;
112
- dropdown.style.maxHeight = `${maxHeight}px`;
113
-
114
- console.log('Dropdown NEW position set to:', {
115
- top: `${top}px`,
116
- left: `${left}px`,
117
- width: `${width}px`,
118
- actualTop: dropdown.style.top,
119
- actualLeft: dropdown.style.left,
120
- actualWidth: dropdown.style.width
121
- });
122
-
123
- // Verify position was applied
124
- const dropdownRect = dropdown.getBoundingClientRect();
125
- console.log('Dropdown actual position after setting:', {
126
- top: dropdownRect.top,
127
- left: dropdownRect.left,
128
- width: dropdownRect.width,
129
- height: dropdownRect.height
130
- });
131
- };
132
-
133
- // Start positioning attempts
134
- positionDropdown();
135
-
136
- // Reposition on scroll/resize
137
- const handleReposition = () => {
138
- retryCount = 0;
139
- positionDropdown();
140
- };
141
-
142
- window.addEventListener('scroll', handleReposition, true);
143
- window.addEventListener('resize', handleReposition);
144
-
145
- return () => {
146
- window.removeEventListener('scroll', handleReposition, true);
147
- window.removeEventListener('resize', handleReposition);
148
- };
149
- }, [isOpen, maxHeight]);
150
-
151
- // Close dropdown when clicking outside
152
- useEffect(() => {
153
- if (!isOpen) return;
154
-
155
- const handleClickOutside = (event: MouseEvent) => {
156
- const target = event.target as Node;
157
-
158
- // Check if click is outside both trigger and dropdown
159
- if (
160
- triggerRef.current && !triggerRef.current.contains(target) &&
161
- dropdownRef.current && !dropdownRef.current.contains(target)
162
- ) {
163
- setIsOpen(false);
164
- }
165
- };
166
-
167
- // Use capture phase for better event handling
168
- document.addEventListener('mousedown', handleClickOutside, true);
169
-
170
- return () => {
171
- document.removeEventListener('mousedown', handleClickOutside, true);
172
- };
173
- }, [isOpen]);
174
57
 
175
58
  // Handle keyboard navigation
176
- useEffect(() => {
59
+ const handleKeyDown = (event: KeyboardEvent) => {
177
60
  if (!isOpen) return;
178
61
 
179
- const handleKeyDown = (event: KeyboardEvent) => {
180
- switch (event.key) {
181
- case 'Escape':
182
- setIsOpen(false);
183
- triggerRef.current?.focus();
184
- break;
185
- case 'ArrowDown':
186
- event.preventDefault();
187
- setFocusedIndex(prev =>
188
- prev < filteredOptions.length - 1 ? prev + 1 : 0
189
- );
190
- break;
191
- case 'ArrowUp':
192
- event.preventDefault();
193
- setFocusedIndex(prev =>
194
- prev > 0 ? prev - 1 : filteredOptions.length - 1
195
- );
196
- break;
197
- case 'Enter':
198
- case ' ':
199
- event.preventDefault();
200
- if (focusedIndex >= 0 && focusedIndex < filteredOptions.length) {
201
- const option = filteredOptions[focusedIndex];
202
- if (!option.disabled) {
203
- handleOptionSelect(option);
204
- }
62
+ switch (event.key) {
63
+ case 'ArrowDown':
64
+ event.preventDefault();
65
+ setFocusedIndex(prev =>
66
+ prev < filteredOptions.length - 1 ? prev + 1 : 0
67
+ );
68
+ break;
69
+ case 'ArrowUp':
70
+ event.preventDefault();
71
+ setFocusedIndex(prev =>
72
+ prev > 0 ? prev - 1 : filteredOptions.length - 1
73
+ );
74
+ break;
75
+ case 'Enter':
76
+ case ' ':
77
+ event.preventDefault();
78
+ if (focusedIndex >= 0 && focusedIndex < filteredOptions.length) {
79
+ const option = filteredOptions[focusedIndex];
80
+ if (!option.disabled) {
81
+ handleOptionSelect(option);
205
82
  }
206
- break;
207
- }
208
- };
83
+ }
84
+ break;
85
+ }
86
+ };
209
87
 
88
+ useEffect(() => {
89
+ if (!isOpen) return;
210
90
  document.addEventListener('keydown', handleKeyDown);
211
- return () => {
212
- document.removeEventListener('keydown', handleKeyDown);
213
- };
91
+ return () => document.removeEventListener('keydown', handleKeyDown);
214
92
  }, [isOpen, focusedIndex, filteredOptions]);
215
93
 
216
94
  // Focus search input when dropdown opens
@@ -247,183 +125,143 @@ const Select: React.FC<SelectProps> = ({
247
125
 
248
126
  const containerWebProps = getWebProps([
249
127
  selectStyles.container,
250
- style
128
+ style as any
251
129
  ]);
252
130
 
253
131
  const triggerWebProps = getWebProps([
254
- selectStyles.trigger,
255
- isOpen && selectStyles.triggerOpen
132
+ selectStyles.trigger({ type, intent }),
256
133
  ]);
257
134
 
258
- // MUI-style dropdown portal
259
- const renderDropdown = () => {
260
- if (!isOpen) return null;
261
-
262
- return createPortal(
263
- <div
264
- ref={dropdownRef}
265
- style={{
266
- position: 'fixed',
267
- top: '0px', // Explicit initial position
268
- left: '0px', // Explicit initial position
269
- opacity: 1,
270
- zIndex: 1300, // MUI's z-index for select
271
- backgroundColor: 'white',
272
- borderRadius: '4px',
273
- boxShadow: '0px 5px 5px -3px rgba(0,0,0,0.2), 0px 8px 10px 1px rgba(0,0,0,0.14), 0px 3px 14px 2px rgba(0,0,0,0.12)',
274
- overflow: 'auto',
275
- minWidth: '200px', // Ensure minimum width
276
- visibility: 'visible', // Ensure it's visible
277
- }}
278
- role="listbox"
279
- >
280
- {searchable && (
281
- <div
282
- style={{
283
- padding: '8px 16px',
284
- borderBottom: '1px solid rgba(0, 0, 0, 0.12)',
285
- position: 'sticky',
286
- top: 0,
287
- backgroundColor: 'white',
288
- zIndex: 1,
289
- }}
290
- >
291
- <input
292
- ref={searchInputRef}
293
- type="text"
294
- placeholder="Search options..."
295
- value={searchTerm}
296
- onChange={handleSearchChange}
297
- style={{
298
- width: '100%',
299
- padding: '8px 12px',
300
- border: '1px solid rgba(0, 0, 0, 0.23)',
301
- borderRadius: '4px',
302
- fontSize: '14px',
303
- outline: 'none',
304
- }}
305
- onFocus={(e) => {
306
- e.target.style.borderColor = '#1976d2';
307
- }}
308
- onBlur={(e) => {
309
- e.target.style.borderColor = 'rgba(0, 0, 0, 0.23)';
310
- }}
311
- />
312
- </div>
313
- )}
314
-
315
- <div style={{ padding: '8px 0' }}>
316
- {filteredOptions.map((option, index) => {
317
- const isSelected = option.value === value;
318
- const isFocused = index === focusedIndex;
319
-
320
- return (
321
- <div
322
- key={option.value}
323
- onClick={() => handleOptionSelect(option)}
324
- role="option"
325
- aria-selected={isSelected}
326
- onMouseEnter={() => setFocusedIndex(index)}
327
- style={{
328
- padding: '6px 16px',
329
- cursor: option.disabled ? 'default' : 'pointer',
330
- backgroundColor: isFocused
331
- ? 'rgba(0, 0, 0, 0.04)'
332
- : isSelected
333
- ? 'rgba(25, 118, 210, 0.08)'
334
- : 'transparent',
335
- color: option.disabled
336
- ? 'rgba(0, 0, 0, 0.38)'
337
- : 'rgba(0, 0, 0, 0.87)',
338
- fontSize: '14px',
339
- lineHeight: '1.5',
340
- transition: 'background-color 150ms cubic-bezier(0.4, 0, 0.2, 1)',
341
- display: 'flex',
342
- alignItems: 'center',
343
- gap: '12px',
344
- }}
345
- >
346
- {option.icon && (
347
- <span style={{ display: 'flex', alignItems: 'center' }}>
348
- {option.icon}
349
- </span>
350
- )}
351
- <span>{option.label}</span>
352
- </div>
353
- );
354
- })}
355
-
356
- {filteredOptions.length === 0 && (
357
- <div
358
- style={{
359
- padding: '6px 16px',
360
- color: 'rgba(0, 0, 0, 0.54)',
361
- fontSize: '14px',
362
- }}
363
- >
364
- No options found
365
- </div>
366
- )}
367
- </div>
368
- </div>,
369
- document.body
370
- );
135
+ const handleClose = () => {
136
+ setIsOpen(false);
137
+ triggerRef.current?.focus();
371
138
  };
372
139
 
140
+ const mergedRef = useMergeRefs(ref, containerWebProps.ref);
141
+
373
142
  return (
374
- <div {...containerWebProps} data-testid={testID}>
143
+ <div {...containerWebProps} ref={mergedRef} data-testid={testID}>
375
144
  {label && (
376
145
  <label {...getWebProps([selectStyles.label])}>
377
146
  {label}
378
147
  </label>
379
148
  )}
380
149
 
381
- <button
382
- ref={setTriggerRef}
383
- {...triggerWebProps}
384
- onClick={handleTriggerClick}
385
- disabled={disabled}
386
- aria-label={accessibilityLabel || label}
387
- aria-expanded={isOpen}
388
- aria-haspopup="listbox"
389
- type="button"
390
- >
391
- <div {...getWebProps([selectStyles.triggerContent])}>
392
- {selectedOption?.icon && (
393
- <span {...getWebProps([selectStyles.icon])}>
394
- {selectedOption.icon}
150
+ <div ref={triggerRef}>
151
+ <button
152
+ {...triggerWebProps}
153
+ onClick={handleTriggerClick}
154
+ disabled={disabled}
155
+ aria-label={accessibilityLabel || label}
156
+ aria-expanded={isOpen}
157
+ aria-haspopup="listbox"
158
+ type="button"
159
+ >
160
+ <div {...getWebProps([selectStyles.triggerContent])}>
161
+ {selectedOption?.icon && (
162
+ <span {...getWebProps([selectStyles.icon])}>
163
+ {selectedOption.icon}
164
+ </span>
165
+ )}
166
+ <span
167
+ {...getWebProps([
168
+ selectedOption ? selectStyles.triggerText : selectStyles.placeholder
169
+ ])}
170
+ >
171
+ {selectedOption ? selectedOption.label : placeholder}
395
172
  </span>
396
- )}
397
- <span
173
+ </div>
174
+
175
+ <IconSvg
176
+ path={resolveIconPath('chevron-down')}
398
177
  {...getWebProps([
399
- selectedOption ? selectStyles.triggerText : selectStyles.placeholder
178
+ selectStyles.chevron,
179
+ isOpen && selectStyles.chevronOpen
400
180
  ])}
401
- >
402
- {selectedOption ? selectedOption.label : placeholder}
403
- </span>
404
- </div>
405
-
406
- <svg
407
- {...getWebProps([
408
- selectStyles.chevron,
409
- isOpen && selectStyles.chevronOpen
410
- ])}
411
- width="16"
412
- height="16"
413
- viewBox="0 0 16 16"
414
- fill="currentColor"
181
+ aria-label="chevron-down"
182
+ />
183
+ </button>
184
+ </div>
185
+ <PositionedPortal
186
+ open={isOpen}
187
+ anchor={triggerRef}
188
+ placement="bottom-start"
189
+ offset={4}
190
+ onClickOutside={handleClose}
191
+ onEscapeKey={handleClose}
192
+ matchWidth={false}
193
+ zIndex={1000}
194
+ >
195
+ <div
196
+ {...getWebProps([selectStyles.dropdown])}
197
+ style={{
198
+ maxHeight: maxHeight,
199
+ // Override positioning since PositionedPortal handles it
200
+ position: 'relative',
201
+ top: 'auto',
202
+ left: 'auto',
203
+ right: 'auto',
204
+ }}
205
+ role="listbox"
415
206
  >
416
- <path d="M4.427 9.573l3.396-3.396a.25.25 0 01.354 0l3.396 3.396a.25.25 0 01-.177.427H4.604a.25.25 0 01-.177-.427z" />
417
- </svg>
418
- </button>
419
-
420
- {renderDropdown()}
207
+ {searchable && (
208
+ <div {...getWebProps([selectStyles.searchContainer])}>
209
+ <input
210
+ ref={searchInputRef}
211
+ type="text"
212
+ placeholder="Search options..."
213
+ value={searchTerm}
214
+ onChange={handleSearchChange}
215
+ {...getWebProps([selectStyles.searchInput])}
216
+ />
217
+ </div>
218
+ )}
219
+
220
+ <div {...getWebProps([selectStyles.optionsList])}>
221
+ {filteredOptions.map((option, index) => {
222
+ const isSelected = option.value === value;
223
+
224
+ return (
225
+ <div
226
+ key={option.value}
227
+ onClick={() => handleOptionSelect(option)}
228
+ role="option"
229
+ aria-selected={isSelected}
230
+ onMouseEnter={() => setFocusedIndex(index)}
231
+ {...getWebProps([selectStyles.option])}
232
+ >
233
+ <div {...getWebProps([selectStyles.optionContent])}>
234
+ {option.icon && (
235
+ <span {...getWebProps([selectStyles.optionIcon])}>
236
+ {option.icon}
237
+ </span>
238
+ )}
239
+ <span {...getWebProps([
240
+ selectStyles.optionText,
241
+ option.disabled && selectStyles.optionTextDisabled
242
+ ])}>
243
+ {option.label}
244
+ </span>
245
+ </div>
246
+ </div>
247
+ );
248
+ })}
249
+
250
+ {filteredOptions.length === 0 && (
251
+ <div {...getWebProps([selectStyles.option])} style={{ cursor: 'default' }}>
252
+ <span {...getWebProps([selectStyles.optionText])}>
253
+ No options found
254
+ </span>
255
+ </div>
256
+ )}
257
+ </div>
258
+ </div>
259
+ </PositionedPortal>
421
260
 
422
261
  {helperText && (
423
262
  <div
424
263
  {...getWebProps([
425
264
  selectStyles.helperText,
426
- { error }
427
265
  ])}
428
266
  >
429
267
  {helperText}
@@ -431,6 +269,8 @@ const Select: React.FC<SelectProps> = ({
431
269
  )}
432
270
  </div>
433
271
  );
434
- };
272
+ });
273
+
274
+ Select.displayName = 'Select';
435
275
 
436
276
  export default Select;
@@ -1,2 +1,5 @@
1
- export { default } from './Select.web';
2
- export * from './types';
1
+ import SelectComponent from './Select.web';
2
+
3
+ export default SelectComponent;
4
+ export { SelectComponent as Select };
5
+ export * from './types';
@@ -1,2 +1,5 @@
1
- export { default } from './Select.web';
2
- export * from './types';
1
+ import SelectComponent from './Select.web';
2
+
3
+ export default SelectComponent;
4
+ export { SelectComponent as Select };
5
+ export * from './types';
@@ -1,5 +1,11 @@
1
- import { ReactNode } from 'react';
2
- import type { IntentVariant } from '../theme/variants';
1
+ import { Intent, Size } from '@idealyst/theme';
2
+ import type { ReactNode } from 'react';
3
+ import type { StyleProp, ViewStyle } from 'react-native';
4
+
5
+ // Component-specific type aliases for future extensibility
6
+ export type SelectIntentVariant = Intent;
7
+ export type SelectSizeVariant = Size;
8
+ export type SelectType = 'outlined' | 'filled';
3
9
 
4
10
  export interface SelectOption {
5
11
  /**
@@ -65,19 +71,19 @@ export interface SelectProps {
65
71
  label?: string;
66
72
 
67
73
  /**
68
- * The visual variant of the select
74
+ * The visual type of the select
69
75
  */
70
- variant?: 'outlined' | 'filled';
76
+ type?: SelectType,
71
77
 
72
78
  /**
73
79
  * The intent/color scheme of the select
74
80
  */
75
- intent?: IntentVariant;
81
+ intent?: SelectIntentVariant;
76
82
 
77
83
  /**
78
84
  * The size of the select
79
85
  */
80
- size?: 'small' | 'medium' | 'large';
86
+ size?: SelectSizeVariant;
81
87
 
82
88
  /**
83
89
  * Whether to show a search/filter input (web only)
@@ -104,7 +110,7 @@ export interface SelectProps {
104
110
  /**
105
111
  * Additional styles (platform-specific)
106
112
  */
107
- style?: any;
113
+ style?: StyleProp<ViewStyle>;
108
114
 
109
115
  /**
110
116
  * Test ID for testing