@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,110 @@
1
+ import React, { isValidElement } from 'react';
2
+ import { getWebProps } from 'react-native-unistyles/web';
3
+ import { listStyles } from './List.styles';
4
+ import type { ListItemProps } from './types';
5
+ import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
6
+ import { resolveIconPath, isIconName } from '../Icon/icon-resolver';
7
+ import { useListContext } from './ListContext';
8
+
9
+ const ListItem: React.FC<ListItemProps> = ({
10
+ id,
11
+ label,
12
+ children,
13
+ leading,
14
+ trailing,
15
+ active = false,
16
+ selected = false,
17
+ disabled = false,
18
+ indent = 0,
19
+ size,
20
+ onPress,
21
+ style,
22
+ testID,
23
+ }) => {
24
+ const listContext = useListContext();
25
+ const isClickable = !disabled && !!onPress;
26
+
27
+ // Use explicit size prop, fallback to context size, then default
28
+ const effectiveSize = size ?? listContext.size ?? 'md';
29
+ const effectiveVariant = listContext.type ?? 'default';
30
+
31
+ // Apply types
32
+ listStyles.useVariants({
33
+ size: effectiveSize,
34
+ active,
35
+ selected,
36
+ disabled,
37
+ clickable: isClickable,
38
+ });
39
+
40
+ const itemProps = getWebProps([listStyles.item, style]);
41
+ const labelProps = getWebProps([listStyles.label]);
42
+ const leadingProps = getWebProps([listStyles.leading]);
43
+ const trailingProps = getWebProps([listStyles.trailing]);
44
+ const trailingIconProps = getWebProps([listStyles.trailing, listStyles.trailingIcon]);
45
+
46
+ const handleClick = () => {
47
+ if (!disabled && onPress) {
48
+ onPress();
49
+ }
50
+ };
51
+
52
+ // Helper to render leading/trailing icons
53
+ const renderElement = (element: typeof leading | typeof trailing, props: any, isTrailing = false) => {
54
+ if (!element) return null;
55
+
56
+ if (isIconName(element)) {
57
+ const iconPath = resolveIconPath(element);
58
+ // Use trailingIconProps for trailing icons to apply size constraints
59
+ const iconPropsToUse = isTrailing ? trailingIconProps : props;
60
+ return (
61
+ <IconSvg
62
+ path={iconPath}
63
+ {...iconPropsToUse}
64
+ aria-label={element}
65
+ />
66
+ );
67
+ } else if (isValidElement(element)) {
68
+ return element;
69
+ }
70
+
71
+ return null;
72
+ };
73
+
74
+ const content = (
75
+ <>
76
+ {leading && renderElement(leading, leadingProps)}
77
+
78
+ <div {...getWebProps([listStyles.labelContainer])}>
79
+ {label && (
80
+ <span {...labelProps}>{label}</span>
81
+ )}
82
+ {children}
83
+ </div>
84
+
85
+ {trailing && (
86
+ <div {...trailingProps}>
87
+ {renderElement(trailing, trailingIconProps, true)}
88
+ </div>
89
+ )}
90
+ </>
91
+ );
92
+
93
+ const indentStyle = indent > 0 ? { paddingLeft: `${indent * 16}px` } : {};
94
+
95
+ return (
96
+ <div
97
+ {...itemProps}
98
+ style={{ ...indentStyle }}
99
+ onClick={isClickable ? handleClick : undefined}
100
+ role="listitem"
101
+ aria-selected={selected}
102
+ aria-disabled={disabled}
103
+ data-testid={testID}
104
+ >
105
+ {content}
106
+ </div>
107
+ );
108
+ };
109
+
110
+ export default ListItem;
@@ -0,0 +1,31 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { View, Text } from 'react-native';
3
+ import { listStyles } from './List.styles';
4
+ import type { ListSectionProps } from './types';
5
+
6
+ const ListSection = forwardRef<View, ListSectionProps>(({
7
+ title,
8
+ children,
9
+ collapsed = false,
10
+ style,
11
+ testID,
12
+ }, ref) => {
13
+ return (
14
+ <View ref={ref} style={[listStyles.section, style]} testID={testID}>
15
+ {title && (
16
+ <Text style={listStyles.sectionTitle}>
17
+ {title}
18
+ </Text>
19
+ )}
20
+ {!collapsed && (
21
+ <View style={listStyles.sectionContent}>
22
+ {children}
23
+ </View>
24
+ )}
25
+ </View>
26
+ );
27
+ });
28
+
29
+ ListSection.displayName = 'ListSection';
30
+
31
+ export default ListSection;
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import { getWebProps } from 'react-native-unistyles/web';
3
+ import { listStyles } from './List.styles';
4
+ import type { ListSectionProps } from './types';
5
+
6
+ const ListSection: React.FC<ListSectionProps> = ({
7
+ title,
8
+ children,
9
+ collapsed = false,
10
+ style,
11
+ testID,
12
+ }) => {
13
+ const sectionProps = getWebProps([listStyles.section, style as any]);
14
+ const titleProps = getWebProps([listStyles.sectionTitle]);
15
+ const contentProps = getWebProps([listStyles.sectionContent]);
16
+
17
+ return (
18
+ <div {...sectionProps} data-testid={testID}>
19
+ {title && (
20
+ <div {...titleProps}>
21
+ {title}
22
+ </div>
23
+ )}
24
+ {!collapsed && (
25
+ <div {...contentProps}>
26
+ {children}
27
+ </div>
28
+ )}
29
+ </div>
30
+ );
31
+ };
32
+
33
+ export default ListSection;
@@ -0,0 +1,5 @@
1
+ export { default } from './List.native';
2
+ export { default as List } from './List.native';
3
+ export { default as ListItem } from './ListItem.native';
4
+ export { default as ListSection } from './ListSection.native';
5
+ export * from './types';
@@ -0,0 +1,5 @@
1
+ export { default } from './List.web';
2
+ export { default as List } from './List.web';
3
+ export { default as ListItem } from './ListItem.web';
4
+ export { default as ListSection } from './ListSection.web';
5
+ export * from './types';
@@ -0,0 +1,5 @@
1
+ export { default } from './List.web';
2
+ export { default as List } from './List.web';
3
+ export { default as ListItem } from './ListItem.web';
4
+ export { default as ListSection } from './ListSection.web';
5
+ export * from './types';
@@ -0,0 +1,42 @@
1
+ import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
2
+ import type { ReactNode } from 'react';
3
+ import type { IconName } from '../Icon/icon-types';
4
+ import { Size } from '@idealyst/theme';
5
+
6
+ // Component-specific type aliases for future extensibility
7
+ export type ListSizeVariant = Size;
8
+ export type ListType = 'default' | 'bordered' | 'divided';
9
+
10
+ export interface ListItemProps {
11
+ id?: string;
12
+ label?: string;
13
+ children?: ReactNode;
14
+ leading?: IconName | ReactNode;
15
+ trailing?: IconName | ReactNode;
16
+ active?: boolean;
17
+ selected?: boolean;
18
+ disabled?: boolean;
19
+ indent?: number;
20
+ size?: ListSizeVariant;
21
+ onPress?: () => void;
22
+ style?: StyleProp<ViewStyle>;
23
+ testID?: string;
24
+ }
25
+
26
+ export interface ListProps {
27
+ children: ReactNode;
28
+ type?: ListType;
29
+ size?: ListSizeVariant;
30
+ style?: StyleProp<ViewStyle>;
31
+ testID?: string;
32
+ scrollable?: boolean;
33
+ maxHeight?: number | string;
34
+ }
35
+
36
+ export interface ListSectionProps {
37
+ title?: string;
38
+ children: ReactNode;
39
+ collapsed?: boolean;
40
+ style?: StyleProp<ViewStyle>;
41
+ testID?: string;
42
+ }
@@ -0,0 +1,150 @@
1
+ import React, { useRef, useState, isValidElement, cloneElement, forwardRef, useEffect } from 'react';
2
+ import {
3
+ View,
4
+ Modal,
5
+ Pressable,
6
+ ScrollView,
7
+ } from 'react-native';
8
+ import { menuStyles } from './Menu.styles';
9
+ import type { MenuProps, MenuItem as MenuItemType } from './types';
10
+ import MenuItem from './MenuItem.native';
11
+ import useMergeRefs from '../hooks/useMergeRefs';
12
+ import { BoundedModalContent } from '../internal/BoundedModalContent.native';
13
+ import { useSmartPosition } from '../hooks/useSmartPosition.native';
14
+
15
+ const Menu = forwardRef<View, MenuProps>(({
16
+ children,
17
+ items,
18
+ open,
19
+ onOpenChange,
20
+ placement = 'bottom-start',
21
+ style,
22
+ size,
23
+ testID,
24
+ }, ref) => {
25
+ const {
26
+ position: menuPosition,
27
+ size: menuSize,
28
+ isPositioned,
29
+ anchorRef: triggerRef,
30
+ measureAndPosition,
31
+ handleLayout: handleMenuLayout,
32
+ reset: resetPosition,
33
+ } = useSmartPosition({
34
+ placement,
35
+ offset: 8,
36
+ maxHeight: 300,
37
+ matchWidth: false,
38
+ });
39
+
40
+ const mergedTriggerRef = useMergeRefs(ref, triggerRef);
41
+
42
+ // Reset position when menu closes
43
+ useEffect(() => {
44
+ if (!open) {
45
+ resetPosition();
46
+ }
47
+ }, [open]);
48
+
49
+ const handleTriggerPress = () => {
50
+ if (!onOpenChange) return;
51
+
52
+ if (!open) {
53
+ // Measure and position menu
54
+ measureAndPosition();
55
+ onOpenChange(true);
56
+ } else {
57
+ onOpenChange(false);
58
+ }
59
+ };
60
+
61
+ const handleItemPress = (item: MenuItemType) => {
62
+ if (item.disabled) return;
63
+
64
+ item.onClick?.();
65
+ onOpenChange?.(false);
66
+ };
67
+
68
+ const handleBackdropPress = () => {
69
+ onOpenChange?.(false);
70
+ };
71
+
72
+ const renderMenu = () => {
73
+ if (!open) return null;
74
+
75
+ // Show menu only after it has been measured AND positioned
76
+ const isMeasured = menuSize.height > 0;
77
+ const shouldShow = isMeasured && isPositioned;
78
+
79
+ return (
80
+ <Modal
81
+ visible={true}
82
+ transparent
83
+ animationType="none"
84
+ onRequestClose={() => onOpenChange?.(false)}
85
+ >
86
+ <Pressable
87
+ style={{ flex: 1 }}
88
+ onPress={handleBackdropPress}
89
+ >
90
+ <BoundedModalContent
91
+ top={menuPosition.top}
92
+ left={menuPosition.left}
93
+ width={menuPosition.width}
94
+ maxHeight={300}
95
+ style={[
96
+ menuStyles.menu,
97
+ style,
98
+ { opacity: shouldShow ? 1 : 0 }
99
+ ]}
100
+ onLayout={handleMenuLayout}
101
+ >
102
+ <ScrollView
103
+ showsVerticalScrollIndicator={false}
104
+ >
105
+ {items.map((item, index) => {
106
+
107
+ return (
108
+ <MenuItem
109
+ key={item.id || index}
110
+ item={item}
111
+ onPress={handleItemPress}
112
+ size={size}
113
+ testID={testID ? `${testID}-item-${item.id || index}` : undefined}
114
+ />
115
+ );
116
+ })}
117
+ </ScrollView>
118
+ </BoundedModalContent>
119
+ </Pressable>
120
+ </Modal>
121
+ );
122
+ };
123
+
124
+ // Clone the child element and merge onPress handler
125
+ const trigger = isValidElement(children)
126
+ ? cloneElement(children as React.ReactElement<any>, {
127
+ onPress: () => {
128
+ // Call original onPress if it exists
129
+ const originalOnPress = (children as any).props?.onPress;
130
+ originalOnPress?.();
131
+ // Then handle menu toggle
132
+ handleTriggerPress();
133
+ },
134
+ })
135
+ : children;
136
+
137
+ return (
138
+ <>
139
+ <View ref={mergedTriggerRef} collapsable={false}>
140
+ {trigger}
141
+ </View>
142
+
143
+ {renderMenu()}
144
+ </>
145
+ );
146
+ });
147
+
148
+ Menu.displayName = 'Menu';
149
+
150
+ export default Menu;
@@ -0,0 +1,185 @@
1
+ import { StyleSheet } from 'react-native-unistyles';
2
+ import { Theme, StylesheetStyles, CompoundVariants, Intent, Size} from '@idealyst/theme';
3
+ import { buildSizeVariants } from '../utils/buildSizeVariants';
4
+
5
+ type MenuSize = Size;
6
+ type MenuIntent = Intent;
7
+
8
+ type MenuVariants = {
9
+ size: MenuSize;
10
+ intent: MenuIntent;
11
+ disabled: boolean;
12
+ }
13
+
14
+ export type ExpandedMenuStyles = StylesheetStyles<keyof MenuVariants>;
15
+
16
+ export type MenuStylesheet = {
17
+ overlay: ExpandedMenuStyles;
18
+ menu: ExpandedMenuStyles;
19
+ separator: ExpandedMenuStyles;
20
+ item: ExpandedMenuStyles;
21
+ icon: ExpandedMenuStyles;
22
+ label: ExpandedMenuStyles;
23
+ }
24
+
25
+ /**
26
+ * Create size variants for menu item
27
+ */
28
+ function createItemSizeVariants(theme: Theme) {
29
+ return buildSizeVariants(theme, 'menu', (size) => ({
30
+ paddingVertical: size.paddingVertical,
31
+ paddingHorizontal: size.paddingHorizontal,
32
+ }));
33
+ }
34
+
35
+ /**
36
+ * Get hover styles for menu item based on intent
37
+ */
38
+ function getItemHoverStyles(theme: Theme, intent: MenuIntent) {
39
+ if (intent === 'neutral') {
40
+ return {};
41
+ }
42
+ const intentValue = theme.intents[intent];
43
+ return {
44
+ _web: {
45
+ _hover: {
46
+ backgroundColor: intentValue.light + '20',
47
+ color: intentValue.primary,
48
+ },
49
+ },
50
+ } as const;
51
+ }
52
+
53
+ /**
54
+ * Create compound variants for menu item
55
+ */
56
+ function createItemCompoundVariants(theme: Theme): CompoundVariants<keyof MenuVariants> {
57
+ return [
58
+ {
59
+ disabled: true,
60
+ styles: {
61
+ _web: {
62
+ _hover: {
63
+ backgroundColor: 'transparent',
64
+ },
65
+ },
66
+ },
67
+ },
68
+ ] as const;
69
+ }
70
+
71
+ /**
72
+ * Create size variants for icon
73
+ */
74
+ function createIconSizeVariants(theme: Theme) {
75
+ return buildSizeVariants(theme, 'menu', (size) => ({
76
+ width: size.iconSize,
77
+ height: size.iconSize,
78
+ fontSize: size.iconSize,
79
+ }));
80
+ }
81
+
82
+ /**
83
+ * Create size variants for label
84
+ */
85
+ function createLabelSizeVariants(theme: Theme) {
86
+ return buildSizeVariants(theme, 'menu', (size) => ({
87
+ fontSize: size.labelFontSize,
88
+ }));
89
+ }
90
+
91
+ const createItemStyles = (theme: Theme) => {
92
+ return ({ intent }: MenuVariants) => {
93
+ const hoverStyles = getItemHoverStyles(theme, intent);
94
+ return {
95
+ flexDirection: 'row',
96
+ alignItems: 'center',
97
+ backgroundColor: 'transparent',
98
+ borderRadius: 4,
99
+ minHeight: 44,
100
+ variants: {
101
+ size: createItemSizeVariants(theme),
102
+ disabled: {
103
+ true: {
104
+ opacity: 0.5,
105
+ _web: {
106
+ cursor: 'not-allowed',
107
+ },
108
+ },
109
+ false: {},
110
+ },
111
+ },
112
+ compoundVariants: createItemCompoundVariants(theme),
113
+ _web: {
114
+ cursor: 'pointer',
115
+ border: 'none',
116
+ outline: 'none',
117
+ transition: 'background-color 0.2s ease',
118
+ textAlign: 'left',
119
+ _hover: {
120
+ backgroundColor: theme.colors.surface.secondary,
121
+ },
122
+ },
123
+ ...hoverStyles,
124
+ } as const;
125
+ }
126
+ }
127
+
128
+ // Styles are inlined here instead of in @idealyst/theme because Unistyles' Babel transform on native cannot resolve function calls to extract variant structures.
129
+ export const menuStyles = StyleSheet.create((theme: Theme) => {
130
+ return {
131
+ overlay: {
132
+ backgroundColor: 'transparent',
133
+ _web: {
134
+ position: 'fixed' as const,
135
+ top: 0,
136
+ left: 0,
137
+ right: 0,
138
+ bottom: 0,
139
+ zIndex: 999,
140
+ }
141
+ } as const,
142
+ menu: {
143
+ position: 'absolute',
144
+ zIndex: 1000,
145
+ backgroundColor: theme.colors.surface.primary,
146
+ borderWidth: 1,
147
+ borderStyle: 'solid',
148
+ borderColor: theme.colors.border.primary,
149
+ borderRadius: 8,
150
+ minWidth: 120,
151
+ maxWidth: 400,
152
+ padding: 4,
153
+ display: 'flex',
154
+ flexDirection: 'column',
155
+ _web: {
156
+ border: `1px solid ${theme.colors.border.primary}`,
157
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
158
+ width: 'fit-content',
159
+ },
160
+ } as const,
161
+ separator: {
162
+ height: 1,
163
+ backgroundColor: theme.colors.border.primary,
164
+ marginTop: 4,
165
+ marginBottom: 4,
166
+ },
167
+ item: createItemStyles(theme),
168
+ icon: {
169
+ alignItems: 'center',
170
+ justifyContent: 'center',
171
+ flexShrink: 0,
172
+ marginRight: 8,
173
+ variants: {
174
+ size: createIconSizeVariants(theme),
175
+ } as const,
176
+ } as const,
177
+ label: {
178
+ flex: 1,
179
+ color: theme.colors.text.primary,
180
+ variants: {
181
+ size: createLabelSizeVariants(theme),
182
+ } as const,
183
+ } as const,
184
+ } as const;
185
+ });
@@ -0,0 +1,99 @@
1
+ import React, { useRef, forwardRef } from 'react';
2
+ import { getWebProps } from 'react-native-unistyles/web';
3
+ import { menuStyles } from './Menu.styles';
4
+ import type { MenuProps } from './types';
5
+ import MenuItem from './MenuItem.web';
6
+ import useMergeRefs from '../hooks/useMergeRefs';
7
+ import { PositionedPortal } from '../internal/PositionedPortal';
8
+
9
+ const Menu = forwardRef<HTMLDivElement, MenuProps>(({
10
+ children,
11
+ items,
12
+ open = false,
13
+ onOpenChange,
14
+ placement = 'bottom-start',
15
+ closeOnSelection = true,
16
+ size = 'md',
17
+ style,
18
+ testID,
19
+ }, ref) => {
20
+ const triggerRef = useRef<HTMLDivElement>(null);
21
+ const menuRef = useRef<HTMLDivElement>(null);
22
+
23
+ menuStyles.useVariants({
24
+ size,
25
+ });
26
+
27
+ const overlayProps = getWebProps([menuStyles.overlay]);
28
+ const menuProps = getWebProps([menuStyles.menu, style as any]);
29
+ const separatorProps = getWebProps([menuStyles.separator]);
30
+
31
+ const handleTriggerClick = () => {
32
+ onOpenChange?.(!open);
33
+ };
34
+
35
+ const handleItemClick = (item: typeof items[0]) => {
36
+ if (item.disabled) return;
37
+
38
+ item.onClick?.();
39
+
40
+ if (closeOnSelection) {
41
+ onOpenChange?.(false);
42
+ }
43
+ };
44
+
45
+ const mergedMenuRef = useMergeRefs(ref, menuRef);
46
+
47
+ return (
48
+ <>
49
+ <div ref={triggerRef} onClick={handleTriggerClick} style={{ display: 'inline-block' }}>
50
+ {children}
51
+ </div>
52
+
53
+ {open && <div {...overlayProps} />}
54
+
55
+ <PositionedPortal
56
+ open={open}
57
+ anchor={triggerRef}
58
+ placement={placement}
59
+ offset={4}
60
+ onClickOutside={() => onOpenChange?.(false)}
61
+ onEscapeKey={() => onOpenChange?.(false)}
62
+ zIndex={1000}
63
+ >
64
+ <div
65
+ {...menuProps}
66
+ ref={mergedMenuRef}
67
+ role="menu"
68
+ data-testid={testID}
69
+ >
70
+ {items.map((item, index) => {
71
+ if (item.separator) {
72
+ return (
73
+ <div
74
+ key={`separator-${index}`}
75
+ {...separatorProps}
76
+ role="separator"
77
+ />
78
+ );
79
+ }
80
+
81
+ return (
82
+ <MenuItem
83
+ key={item.id || index}
84
+ item={item}
85
+ onPress={handleItemClick}
86
+ size={size}
87
+ testID={testID ? `${testID}-item-${item.id || index}` : undefined}
88
+ />
89
+ );
90
+ })}
91
+ </div>
92
+ </PositionedPortal>
93
+ </>
94
+ );
95
+ });
96
+
97
+ Menu.displayName = 'Menu';
98
+
99
+ export default Menu;