@idealyst/components 1.0.82 → 1.0.84

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 +25 -7
  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 +347 -0
  170. package/src/Select/Select.styles.tsx +335 -0
  171. package/src/Select/Select.web.tsx +276 -0
  172. package/src/Select/index.native.ts +2 -0
  173. package/src/Select/index.ts +5 -0
  174. package/src/Select/index.web.ts +5 -0
  175. package/src/Select/types.ts +124 -0
  176. package/src/Skeleton/Skeleton.native.tsx +139 -0
  177. package/src/Skeleton/Skeleton.styles.tsx +59 -0
  178. package/src/Skeleton/Skeleton.web.tsx +112 -0
  179. package/src/Skeleton/index.native.ts +4 -0
  180. package/src/Skeleton/index.ts +5 -0
  181. package/src/Skeleton/index.web.ts +5 -0
  182. package/src/Skeleton/types.ts +75 -0
  183. package/src/Slider/Slider.native.tsx +248 -0
  184. package/src/Slider/Slider.styles.tsx +241 -0
  185. package/src/Slider/Slider.web.tsx +226 -0
  186. package/src/Slider/index.native.ts +3 -0
  187. package/src/Slider/index.ts +5 -0
  188. package/src/Slider/index.web.ts +5 -0
  189. package/src/Slider/types.ts +31 -0
  190. package/src/Switch/Switch.native.tsx +131 -0
  191. package/src/Switch/Switch.styles.tsx +169 -0
  192. package/src/Switch/Switch.web.tsx +121 -0
  193. package/src/Switch/index.native.ts +3 -0
  194. package/src/Switch/index.ts +5 -0
  195. package/src/Switch/index.web.ts +5 -0
  196. package/src/Switch/types.ts +21 -0
  197. package/src/TabBar/TabBar.native.tsx +142 -0
  198. package/src/TabBar/TabBar.styles.tsx +399 -0
  199. package/src/TabBar/TabBar.web.tsx +205 -0
  200. package/src/TabBar/index.native.tsx +3 -0
  201. package/src/TabBar/index.ts +3 -0
  202. package/src/TabBar/index.web.tsx +3 -0
  203. package/src/TabBar/types.ts +26 -0
  204. package/src/Table/Table.native.tsx +122 -0
  205. package/src/Table/Table.styles.tsx +283 -0
  206. package/src/Table/Table.web.tsx +112 -0
  207. package/src/Table/index.native.tsx +3 -0
  208. package/src/Table/index.ts +3 -0
  209. package/src/Table/index.web.tsx +3 -0
  210. package/src/Table/types.ts +28 -0
  211. package/src/Text/Text.native.tsx +12 -11
  212. package/src/Text/Text.styles.tsx +76 -64
  213. package/src/Text/Text.web.tsx +14 -9
  214. package/src/Text/index.ts +5 -5
  215. package/src/Text/index.web.ts +5 -3
  216. package/src/Text/types.ts +20 -13
  217. package/src/TextArea/TextArea.native.tsx +134 -0
  218. package/src/TextArea/TextArea.styles.tsx +175 -0
  219. package/src/TextArea/TextArea.web.tsx +156 -0
  220. package/src/TextArea/index.native.ts +3 -0
  221. package/src/TextArea/index.ts +3 -0
  222. package/src/TextArea/index.web.ts +3 -0
  223. package/src/TextArea/types.ts +30 -0
  224. package/src/Tooltip/Tooltip.native.tsx +165 -0
  225. package/src/Tooltip/Tooltip.styles.tsx +73 -0
  226. package/src/Tooltip/Tooltip.web.tsx +87 -0
  227. package/src/Tooltip/index.native.ts +3 -0
  228. package/src/Tooltip/index.ts +3 -0
  229. package/src/Tooltip/types.ts +18 -0
  230. package/src/Video/Video.native.tsx +105 -0
  231. package/src/Video/Video.styles.tsx +39 -0
  232. package/src/Video/Video.web.tsx +115 -0
  233. package/src/Video/index.native.ts +5 -0
  234. package/src/Video/index.ts +5 -0
  235. package/src/Video/types.ts +29 -0
  236. package/src/View/View.native.tsx +9 -14
  237. package/src/View/View.styles.tsx +101 -93
  238. package/src/View/View.web.tsx +16 -17
  239. package/src/View/index.ts +5 -5
  240. package/src/View/index.web.ts +5 -3
  241. package/src/View/types.ts +29 -21
  242. package/src/examples/AccordionExamples.tsx +126 -0
  243. package/src/examples/AlertExamples.tsx +280 -0
  244. package/src/examples/AvatarExamples.tsx +23 -23
  245. package/src/examples/BadgeExamples.tsx +109 -41
  246. package/src/examples/BreadcrumbExamples.tsx +312 -0
  247. package/src/examples/ButtonExamples.tsx +160 -33
  248. package/src/examples/CardExamples.tsx +40 -40
  249. package/src/examples/CheckboxExamples.tsx +12 -12
  250. package/src/examples/ChipExamples.tsx +197 -0
  251. package/src/examples/DialogExamples.tsx +22 -22
  252. package/src/examples/DividerExamples.tsx +49 -49
  253. package/src/examples/IconExamples.tsx +270 -54
  254. package/src/examples/ImageExamples.tsx +174 -0
  255. package/src/examples/InputExamples.tsx +75 -17
  256. package/src/examples/ListExamples.tsx +288 -0
  257. package/src/examples/MenuExamples.tsx +144 -0
  258. package/src/examples/PopoverExamples.tsx +69 -73
  259. package/src/examples/ProgressExamples.tsx +137 -0
  260. package/src/examples/RadioButtonExamples.tsx +161 -0
  261. package/src/examples/SVGImageExamples.tsx +19 -17
  262. package/src/examples/ScreenExamples.tsx +31 -31
  263. package/src/examples/SelectExamples.tsx +423 -0
  264. package/src/examples/SkeletonExamples.tsx +206 -0
  265. package/src/examples/SliderExamples.tsx +200 -0
  266. package/src/examples/SwitchExamples.tsx +182 -0
  267. package/src/examples/TabBarExamples.tsx +143 -0
  268. package/src/examples/TableExamples.tsx +280 -0
  269. package/src/examples/TextAreaExamples.tsx +173 -0
  270. package/src/examples/TextExamples.tsx +28 -32
  271. package/src/examples/ThemeExtensionExamples.tsx +10 -10
  272. package/src/examples/TooltipExamples.tsx +126 -0
  273. package/src/examples/VideoExamples.tsx +144 -0
  274. package/src/examples/ViewExamples.tsx +64 -56
  275. package/src/examples/index.ts +18 -3
  276. package/src/hooks/useMergeRefs.ts +16 -0
  277. package/src/hooks/useSmartPosition.native.ts +169 -0
  278. package/src/index.native.ts +80 -9
  279. package/src/index.ts +75 -1
  280. package/src/internal/BoundedModalContent.native.tsx +58 -0
  281. package/src/internal/PositionedPortal.tsx +254 -0
  282. package/src/internal/SafeAreaDebugOverlay.native.tsx +173 -0
  283. package/src/unistyles.d.ts +6 -0
  284. package/src/utils/buildSizeVariants.ts +16 -0
  285. package/src/utils/deepMerge.ts +43 -0
  286. package/src/utils/positionUtils.native.ts +280 -0
  287. package/src/utils/styleHelpers.ts +48 -0
  288. package/LLM-ACCESS-GUIDE.md +0 -143
  289. package/src/ActivityIndicator/README.md +0 -132
  290. package/src/Avatar/README.md +0 -139
  291. package/src/Badge/README.md +0 -170
  292. package/src/Button/Button.types.ts +0 -12
  293. package/src/Button/README.md +0 -262
  294. package/src/Card/README.md +0 -258
  295. package/src/Checkbox/README.md +0 -102
  296. package/src/Dialog/README.md +0 -210
  297. package/src/Divider/README.md +0 -108
  298. package/src/Icon/README.md +0 -81
  299. package/src/Input/README.md +0 -100
  300. package/src/SVGImage/README.md +0 -209
  301. package/src/Screen/README.md +0 -86
  302. package/src/Text/README.md +0 -94
  303. package/src/View/README.md +0 -107
  304. package/src/examples/AllExamples.tsx +0 -84
  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
@@ -0,0 +1,66 @@
1
+ import React, { isValidElement, forwardRef, ComponentRef } from 'react';
2
+ import { Pressable, Text, View } from 'react-native';
3
+ import { Icon } from '../Icon';
4
+ import { menuItemStyles } from './MenuItem.styles';
5
+ import type { MenuItem as MenuItemType, MenuSizeVariant } from './types';
6
+ import MaterialDesignIcons from '@react-native-vector-icons/material-design-icons';
7
+
8
+ interface MenuItemProps {
9
+ item: MenuItemType;
10
+ onPress: (item: MenuItemType) => void;
11
+ size?: MenuSizeVariant;
12
+ testID?: string;
13
+ }
14
+
15
+ const MenuItem = forwardRef<ComponentRef<typeof Pressable>, MenuItemProps>(({ item, onPress, size = 'md', testID }, ref) => {
16
+ // Initialize styles with useVariants
17
+ menuItemStyles.useVariants({
18
+ size,
19
+ disabled: Boolean(item.disabled),
20
+ intent: item.intent || 'neutral',
21
+ });
22
+
23
+ const renderIcon = () => {
24
+ if (!item.icon) return null;
25
+
26
+ if (typeof item.icon === 'string') {
27
+ return (
28
+ <MaterialDesignIcons
29
+ name={item.icon as any}
30
+ style={menuItemStyles.icon}
31
+ />
32
+ );
33
+ } else if (isValidElement(item.icon)) {
34
+ return item.icon;
35
+ }
36
+ return null;
37
+ };
38
+
39
+ return (
40
+ <Pressable
41
+ ref={ref}
42
+ style={menuItemStyles.item}
43
+ onPress={() => onPress(item)}
44
+ disabled={item.disabled}
45
+ accessibilityRole="menuitem"
46
+ accessibilityState={{
47
+ disabled: item.disabled,
48
+ }}
49
+ android_ripple={{ color: 'rgba(0, 0, 0, 0.1)' }}
50
+ testID={testID}
51
+ >
52
+ {item.icon && (
53
+ <View>
54
+ {renderIcon()}
55
+ </View>
56
+ )}
57
+ <Text style={menuItemStyles.label}>
58
+ {item.label}
59
+ </Text>
60
+ </Pressable>
61
+ );
62
+ });
63
+
64
+ MenuItem.displayName = 'MenuItem';
65
+
66
+ export default MenuItem;
@@ -0,0 +1,119 @@
1
+ import { StyleSheet } from 'react-native-unistyles';
2
+ import { Theme } from '@idealyst/theme';
3
+ import { buildSizeVariants } from '../utils/buildSizeVariants';
4
+
5
+ // Styles are inlined here instead of in @idealyst/theme because Unistyles' Babel transform on native cannot resolve function calls to extract variant structures.
6
+ // @ts-ignore - TS language server needs restart to pick up theme structure changes
7
+ export const menuItemStyles = StyleSheet.create((theme: Theme) => {
8
+ return {
9
+ item: {
10
+ flexDirection: 'row',
11
+ alignItems: 'center',
12
+ backgroundColor: 'transparent',
13
+ borderRadius: 4,
14
+ minHeight: 44,
15
+ _web: {
16
+ cursor: 'pointer',
17
+ border: 'none',
18
+ outline: 'none',
19
+ transition: 'background-color 0.2s ease',
20
+ textAlign: 'left',
21
+ _hover: {
22
+ backgroundColor: theme.colors.surface.secondary,
23
+ },
24
+ },
25
+ variants: {
26
+ size: buildSizeVariants(theme, 'menu', (size) => ({
27
+ paddingVertical: size.paddingVertical,
28
+ paddingHorizontal: size.paddingHorizontal,
29
+ })),
30
+ disabled: {
31
+ true: {
32
+ opacity: 0.5,
33
+ _web: {
34
+ cursor: 'not-allowed',
35
+ },
36
+ },
37
+ false: {},
38
+ },
39
+ intent: {
40
+ primary: {
41
+ _web: {
42
+ _hover: {
43
+ backgroundColor: theme.intents.primary.light,
44
+ color: theme.intents.primary.primary,
45
+ },
46
+ },
47
+ },
48
+ neutral: {},
49
+ success: {
50
+ _web: {
51
+ _hover: {
52
+ backgroundColor: theme.intents.success.light,
53
+ color: theme.intents.success.primary,
54
+ },
55
+ },
56
+ },
57
+ error: {
58
+ _web: {
59
+ _hover: {
60
+ backgroundColor: theme.intents.error.light,
61
+ color: theme.intents.error.primary,
62
+ },
63
+ },
64
+ },
65
+ warning: {
66
+ _web: {
67
+ _hover: {
68
+ backgroundColor: theme.intents.warning.light,
69
+ color: theme.intents.warning.primary,
70
+ },
71
+ },
72
+ },
73
+ info: {
74
+ _web: {
75
+ _hover: {
76
+ backgroundColor: theme.intents.info.light,
77
+ color: theme.intents.info.primary,
78
+ },
79
+ },
80
+ },
81
+ },
82
+ },
83
+ compoundVariants: [
84
+ {
85
+ disabled: true,
86
+ styles: {
87
+ _web: {
88
+ _hover: {
89
+ backgroundColor: 'transparent',
90
+ },
91
+ },
92
+ },
93
+ },
94
+ ],
95
+ },
96
+ icon: {
97
+ alignItems: 'center',
98
+ justifyContent: 'center',
99
+ flexShrink: 0,
100
+ marginRight: 12,
101
+ variants: {
102
+ size: buildSizeVariants(theme, 'menu', (size) => ({
103
+ width: size.iconSize,
104
+ height: size.iconSize,
105
+ fontSize: size.iconSize,
106
+ }))
107
+ },
108
+ },
109
+ label: {
110
+ flex: 1,
111
+ color: theme.colors.text.primary,
112
+ variants: {
113
+ size: buildSizeVariants(theme, 'menu', (size) => ({
114
+ fontSize: size.labelFontSize,
115
+ })),
116
+ },
117
+ },
118
+ };
119
+ });
@@ -0,0 +1,67 @@
1
+ import React, { isValidElement } from 'react';
2
+ import { getWebProps } from 'react-native-unistyles/web';
3
+ import { menuItemStyles } from './MenuItem.styles';
4
+ import type { MenuItem as MenuItemType, MenuSizeVariant } from './types';
5
+ import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
6
+ import { resolveIconPath, isIconName } from '../Icon/icon-resolver';
7
+
8
+ interface MenuItemProps {
9
+ item: MenuItemType;
10
+ onPress: (item: MenuItemType) => void;
11
+ size?: MenuSizeVariant;
12
+ testID?: string;
13
+ }
14
+
15
+ const MenuItem: React.FC<MenuItemProps> = ({ item, onPress, size = 'md', testID }) => {
16
+ // Initialize styles with useVariants
17
+ menuItemStyles.useVariants({
18
+ size,
19
+ disabled: Boolean(item.disabled),
20
+ intent: item.intent || 'neutral',
21
+ });
22
+
23
+ const itemProps = getWebProps([menuItemStyles.item]);
24
+ const iconProps = getWebProps([menuItemStyles.icon]);
25
+ const labelProps = getWebProps([menuItemStyles.label]);
26
+
27
+ const renderIcon = () => {
28
+ if (!item.icon) return null;
29
+
30
+ if (isIconName(item.icon)) {
31
+ // Resolve icon name to path and render with IconSvg
32
+ const iconPath = resolveIconPath(item.icon);
33
+ return (
34
+ <IconSvg
35
+ path={iconPath}
36
+ aria-label={item.icon}
37
+ />
38
+ );
39
+ } else if (isValidElement(item.icon)) {
40
+ // Render custom component as-is
41
+ return item.icon;
42
+ }
43
+
44
+ return null;
45
+ };
46
+
47
+ return (
48
+ <button
49
+ {...itemProps}
50
+ onClick={() => onPress(item)}
51
+ disabled={item.disabled}
52
+ role="menuitem"
53
+ data-testid={testID}
54
+ >
55
+ {item.icon && (
56
+ <span {...iconProps}>
57
+ {renderIcon()}
58
+ </span>
59
+ )}
60
+ <span {...labelProps}>
61
+ {item.label}
62
+ </span>
63
+ </button>
64
+ );
65
+ };
66
+
67
+ export default MenuItem;
@@ -0,0 +1,3 @@
1
+ export { default } from './Menu.native';
2
+ export { default as Menu } from './Menu.native';
3
+ export * from './types';
@@ -0,0 +1,3 @@
1
+ export { default } from './Menu.web';
2
+ export { default as Menu } from './Menu.web';
3
+ export * from './types';
@@ -0,0 +1,3 @@
1
+ export { default } from './Menu.web';
2
+ export { default as Menu } from './Menu.web';
3
+ export * from './types';
@@ -0,0 +1,30 @@
1
+ import type { StyleProp, ViewStyle } from 'react-native';
2
+ import type { IconName } from '../Icon/icon-types';
3
+ import { Intent, Size } from '@idealyst/theme';
4
+
5
+ // Component-specific type aliases for future extensibility
6
+ export type MenuIntentVariant = Intent;
7
+ export type MenuSizeVariant = Size;
8
+ export type MenuPlacement = 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'right';
9
+
10
+ export interface MenuItem {
11
+ id: string;
12
+ label: string;
13
+ onClick?: () => void;
14
+ disabled?: boolean;
15
+ icon?: IconName | React.ReactNode;
16
+ intent?: MenuIntentVariant;
17
+ separator?: boolean;
18
+ }
19
+
20
+ export interface MenuProps {
21
+ children: React.ReactNode;
22
+ items: MenuItem[];
23
+ open?: boolean;
24
+ onOpenChange?: (open: boolean) => void;
25
+ placement?: MenuPlacement;
26
+ closeOnSelection?: boolean;
27
+ size?: MenuSizeVariant;
28
+ style?: StyleProp<ViewStyle>;
29
+ testID?: string;
30
+ }
@@ -1,9 +1,12 @@
1
- import React, { useEffect, useRef } from 'react';
1
+ import React, { useEffect, useRef, useState, forwardRef } from 'react';
2
2
  import { Modal, View, TouchableWithoutFeedback, BackHandler, Dimensions } from 'react-native';
3
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
3
4
  import { PopoverProps } from './types';
4
5
  import { popoverStyles } from './Popover.styles';
6
+ import { calculateSmartPosition, calculateAvailableHeight } from '../utils/positionUtils.native';
7
+ import { BoundedModalContent } from '../internal/BoundedModalContent.native';
5
8
 
6
- const Popover: React.FC<PopoverProps> = ({
9
+ const Popover = forwardRef<View, PopoverProps>(({
7
10
  open,
8
11
  onOpenChange,
9
12
  anchor,
@@ -11,11 +14,28 @@ const Popover: React.FC<PopoverProps> = ({
11
14
  placement = 'bottom',
12
15
  offset = 8,
13
16
  closeOnClickOutside = true,
14
- showArrow = false, // Arrows are complex on native, disabled by default
17
+ showArrow = false,
15
18
  style,
16
19
  testID,
17
- }) => {
20
+ }, ref) => {
18
21
  const popoverRef = useRef<View>(null);
22
+ const [popoverPosition, setPopoverPosition] = useState({ top: 0, left: 0, width: 0 });
23
+ const [popoverSize, setPopoverSize] = useState({ width: 0, height: 0 });
24
+ const anchorMeasurements = useRef({ x: 0, y: 0, width: 0, height: 0 });
25
+ const insets = useSafeAreaInsets();
26
+
27
+ // Apply variants
28
+ popoverStyles.useVariants({});
29
+
30
+ // Determine if anchor is a ref object
31
+ const anchorRefToUse = React.useMemo(() => {
32
+ if (!anchor || typeof anchor !== 'object') return null;
33
+ // Check if it has 'current' property (ref object)
34
+ if ('current' in anchor) {
35
+ return anchor as React.RefObject<any>;
36
+ }
37
+ return null;
38
+ }, [anchor]);
19
39
 
20
40
  // Handle Android back button
21
41
  useEffect(() => {
@@ -30,30 +50,76 @@ const Popover: React.FC<PopoverProps> = ({
30
50
  return () => backHandler.remove();
31
51
  }, [open, onOpenChange]);
32
52
 
53
+ // Measure anchor position when opening
54
+ useEffect(() => {
55
+ if (open && anchorRefToUse?.current) {
56
+ anchorRefToUse.current?.measureInWindow((x: number, y: number, width: number, height: number) => {
57
+ anchorMeasurements.current = { x, y, width, height };
58
+ calculatePopoverPosition(x, y, width, height);
59
+ });
60
+ }
61
+ }, [open, placement, anchorRefToUse]);
62
+
63
+ // Recalculate position when popover size changes
64
+ useEffect(() => {
65
+ if (open && popoverSize.width > 0 && popoverSize.height > 0) {
66
+ const { x, y, width, height } = anchorMeasurements.current;
67
+ if (x > 0 || y > 0) {
68
+ calculatePopoverPosition(x, y, width, height);
69
+ }
70
+ }
71
+ }, [popoverSize, open]);
72
+
73
+ const calculatePopoverPosition = (x: number, y: number, width: number, height: number) => {
74
+ // Use measured size if available, otherwise use estimates
75
+ const popoverWidth = popoverSize.width || 200;
76
+ const desiredMaxHeight = 500; // Maximum height we want for popovers
77
+
78
+ // For flip detection, use maxHeight so it properly detects when there's not enough space
79
+ // But if we have a measured size that's SMALLER than maxHeight, use that for final positioning
80
+ // to avoid unnecessary gaps (this happens when content naturally fits)
81
+ const heightForPositioning = popoverSize.height > 0 && popoverSize.height < desiredMaxHeight
82
+ ? popoverSize.height
83
+ : desiredMaxHeight;
84
+
85
+ const desiredSize = {
86
+ width: popoverWidth,
87
+ height: heightForPositioning
88
+ };
89
+
90
+ // Use smart positioning with boundary detection and flipping
91
+ const position = calculateSmartPosition(
92
+ { x, y, width, height },
93
+ desiredSize,
94
+ placement,
95
+ offset,
96
+ false,
97
+ insets
98
+ );
99
+
100
+ setPopoverPosition({ top: position.top, left: position.left, width });
101
+ };
102
+
33
103
  const handleBackdropPress = () => {
34
104
  if (closeOnClickOutside) {
35
105
  onOpenChange(false);
36
106
  }
37
107
  };
38
108
 
39
- if (!open) return null;
40
-
41
- // For React Native, we simplify positioning - center the popover
42
- // More complex anchor positioning would require measuring anchor positions
43
- // which is challenging cross-platform
44
- const screenDimensions = Dimensions.get('window');
45
- const popoverStyle = [
46
- popoverStyles.container,
47
- {
48
- // Center on screen as a simplified approach
49
- position: 'absolute',
50
- top: screenDimensions.height * 0.4,
51
- left: 20,
52
- right: 20,
53
- maxWidth: screenDimensions.width - 40,
54
- },
55
- style,
56
- ];
109
+ const handlePopoverLayout = (event: any) => {
110
+ const { width, height } = event.nativeEvent.layout;
111
+ // Only update if size has changed significantly (to avoid infinite loops)
112
+ if (Math.abs(width - popoverSize.width) > 1 || Math.abs(height - popoverSize.height) > 1) {
113
+ setPopoverSize({ width, height });
114
+ }
115
+ };
116
+
117
+ const { width: screenWidth } = Dimensions.get('window');
118
+ const maxPopoverWidth = screenWidth - 24; // 12px padding on each side
119
+
120
+ if (!open || popoverPosition.top === 0) {
121
+ return null;
122
+ }
57
123
 
58
124
  return (
59
125
  <Modal
@@ -65,23 +131,27 @@ const Popover: React.FC<PopoverProps> = ({
65
131
  >
66
132
  <TouchableWithoutFeedback onPress={handleBackdropPress}>
67
133
  <View style={popoverStyles.backdrop}>
68
- <TouchableWithoutFeedback onPress={(e) => e.stopPropagation()}>
69
- <View ref={popoverRef} style={popoverStyle}>
70
- {showArrow && (
71
- <View style={[
72
- popoverStyles.arrow,
73
- // Apply placement-based arrow positioning
74
- ]} />
75
- )}
134
+ <TouchableWithoutFeedback>
135
+ <BoundedModalContent
136
+ top={popoverPosition.top}
137
+ left={popoverPosition.left}
138
+ width={Math.min(popoverPosition.width || 200, maxPopoverWidth)}
139
+ maxHeight={500}
140
+ style={[popoverStyles.container, style]}
141
+ onLayout={handlePopoverLayout}
142
+ >
143
+ {showArrow && <View style={popoverStyles.arrow} />}
76
144
  <View style={popoverStyles.content}>
77
145
  {children}
78
146
  </View>
79
- </View>
147
+ </BoundedModalContent>
80
148
  </TouchableWithoutFeedback>
81
149
  </View>
82
150
  </TouchableWithoutFeedback>
83
151
  </Modal>
84
152
  );
85
- };
153
+ });
154
+
155
+ Popover.displayName = 'Popover';
86
156
 
87
157
  export default Popover;
@@ -1,95 +1,128 @@
1
1
  import { StyleSheet } from 'react-native-unistyles';
2
+ import { Theme, StylesheetStyles} from '@idealyst/theme';
2
3
 
3
- export const popoverStyles = StyleSheet.create((theme) => ({
4
- container: {
5
- backgroundColor: theme.colors?.surface?.primary || '#ffffff',
6
- borderRadius: theme.borderRadius?.md || 8,
7
- border: `1px solid ${theme.colors?.border?.primary || '#e5e7eb'}`,
8
- boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
9
- transition: 'opacity 150ms ease-out, transform 150ms ease-out',
10
- transformOrigin: 'center center',
11
- },
12
-
13
- content: {
14
- padding: theme.spacing?.md || 12,
15
- },
16
-
17
- arrow: {
18
- position: 'absolute',
19
- width: 12,
20
- height: 12,
21
- backgroundColor: theme.colors?.surface?.primary || '#ffffff',
22
- transform: 'rotate(45deg)',
23
-
24
- variants: {
25
- placement: {
4
+ type PopoverPlacement = 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end' | 'right' | 'right-start' | 'right-end';
5
+
6
+ type PopoverVariants = {
7
+ placement: PopoverPlacement;
8
+ }
9
+
10
+ export type ExpandedPopoverStyles = StylesheetStyles<keyof PopoverVariants>;
11
+
12
+ export type PopoverStylesheet = {
13
+ container: ExpandedPopoverStyles;
14
+ content: ExpandedPopoverStyles;
15
+ arrow: ExpandedPopoverStyles;
16
+ backdrop: ExpandedPopoverStyles;
17
+ }
18
+
19
+ function createArrowPlacementVariants(theme: Theme) {
20
+ return {
26
21
  top: {
27
- bottom: -6,
28
- left: '50%',
29
- marginLeft: -6,
22
+ bottom: -6,
23
+ _web: {
24
+ left: '50%',
25
+ marginLeft: -6,
26
+ },
30
27
  },
31
28
  'top-start': {
32
- bottom: -6,
33
- left: 16,
29
+ bottom: -6,
30
+ left: 16,
34
31
  },
35
32
  'top-end': {
36
- bottom: -6,
37
- right: 16,
33
+ bottom: -6,
34
+ right: 16,
38
35
  },
39
36
  bottom: {
40
- top: -6,
41
- left: '50%',
42
- marginLeft: -6,
37
+ top: -6,
38
+ _web: {
39
+ left: '50%',
40
+ marginLeft: -6,
41
+ },
43
42
  },
44
43
  'bottom-start': {
45
- top: -6,
46
- left: 16,
44
+ top: -6,
45
+ left: 16,
47
46
  },
48
47
  'bottom-end': {
49
- top: -6,
50
- right: 16,
48
+ top: -6,
49
+ right: 16,
51
50
  },
52
51
  left: {
53
- right: -6,
54
- top: '50%',
55
- marginTop: -6,
52
+ right: -6,
53
+ _web: {
54
+ top: '50%',
55
+ marginTop: -6,
56
+ },
56
57
  },
57
58
  'left-start': {
58
- right: -6,
59
- top: 16,
59
+ right: -6,
60
+ top: 16,
60
61
  },
61
62
  'left-end': {
62
- right: -6,
63
- bottom: 16,
63
+ right: -6,
64
+ bottom: 16,
64
65
  },
65
66
  right: {
66
- left: -6,
67
- top: '50%',
68
- marginTop: -6,
67
+ left: -6,
68
+ _web: {
69
+ top: '50%',
70
+ marginTop: -6,
71
+ },
69
72
  },
70
73
  'right-start': {
71
- left: -6,
72
- top: 16,
74
+ left: -6,
75
+ top: 16,
73
76
  },
74
77
  'right-end': {
75
- left: -6,
76
- bottom: 16,
78
+ left: -6,
79
+ bottom: 16,
80
+ },
81
+ };
82
+ }
83
+
84
+ // Styles are inlined here instead of in @idealyst/theme because Unistyles' Babel
85
+ // transform on native cannot resolve function calls to extract variant structures.
86
+ // @ts-ignore - TS language server needs restart to pick up theme structure changes
87
+ export const popoverStyles = StyleSheet.create((theme: Theme) => {
88
+ return {
89
+ container: {
90
+ backgroundColor: theme.colors.surface.primary,
91
+ borderRadius: 8,
92
+ borderWidth: 1,
93
+ borderColor: theme.colors.border.primary,
94
+ borderStyle: 'solid',
95
+ shadowColor: '#000',
96
+ shadowOffset: { width: 0, height: 4 },
97
+ shadowOpacity: 0.1,
98
+ shadowRadius: 12,
99
+ elevation: 8,
100
+ _web: {
101
+ border: `1px solid ${theme.colors.border.primary}`,
102
+ boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
103
+ transition: 'opacity 150ms ease-out, transform 150ms ease-out',
104
+ transformOrigin: 'center center',
105
+ },
106
+ },
107
+ content: {
108
+ padding: 16,
109
+ },
110
+ arrow: {
111
+ position: 'absolute',
112
+ width: 12,
113
+ height: 12,
114
+ backgroundColor: theme.colors.surface.primary,
115
+ variants: {
116
+ placement: createArrowPlacementVariants(theme),
117
+ },
118
+ _web: {
119
+ transform: 'rotate(45deg)',
120
+ boxShadow: '-2px 2px 4px rgba(0, 0, 0, 0.1)',
77
121
  },
78
- },
79
122
  },
80
-
81
- _web: {
82
- boxShadow: '-2px 2px 4px rgba(0, 0, 0, 0.1)',
123
+ backdrop: {
124
+ flex: 1,
125
+ backgroundColor: 'transparent',
83
126
  },
84
- },
85
-
86
- // Native-specific backdrop
87
- backdrop: {
88
- position: 'absolute',
89
- top: 0,
90
- left: 0,
91
- right: 0,
92
- bottom: 0,
93
- backgroundColor: 'transparent',
94
- },
95
- }));
127
+ };
128
+ });