@idealyst/components 1.0.83 → 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 +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
@@ -0,0 +1,254 @@
1
+ import React, { useRef, useState, useEffect, useLayoutEffect, useCallback, ReactNode } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+
4
+ export type Placement =
5
+ | 'top' | 'top-start' | 'top-end'
6
+ | 'bottom' | 'bottom-start' | 'bottom-end'
7
+ | 'left' | 'left-start' | 'left-end'
8
+ | 'right' | 'right-start' | 'right-end';
9
+
10
+ interface PositionedPortalProps {
11
+ open: boolean;
12
+ anchor: React.RefObject<HTMLElement>;
13
+ children: ReactNode;
14
+ placement?: Placement;
15
+ offset?: number;
16
+ onClickOutside?: () => void;
17
+ onEscapeKey?: () => void;
18
+ matchWidth?: boolean;
19
+ zIndex?: number;
20
+ }
21
+
22
+ interface Position {
23
+ top: number;
24
+ left: number;
25
+ width?: number;
26
+ }
27
+
28
+ const calculatePosition = (
29
+ anchorRect: DOMRect,
30
+ contentSize: { width: number; height: number },
31
+ placement: Placement,
32
+ offset: number,
33
+ matchWidth: boolean
34
+ ): Position => {
35
+ const viewport = {
36
+ width: window.innerWidth,
37
+ height: window.innerHeight,
38
+ scrollX: window.scrollX,
39
+ scrollY: window.scrollY,
40
+ };
41
+
42
+ let position: Position = { top: 0, left: 0 };
43
+
44
+ // Calculate initial position based on placement
45
+ switch (placement) {
46
+ case 'top':
47
+ position = {
48
+ top: anchorRect.top + viewport.scrollY - contentSize.height - offset,
49
+ left: anchorRect.left + viewport.scrollX + anchorRect.width / 2 - contentSize.width / 2,
50
+ };
51
+ break;
52
+ case 'top-start':
53
+ position = {
54
+ top: anchorRect.top + viewport.scrollY - contentSize.height - offset,
55
+ left: anchorRect.left + viewport.scrollX,
56
+ };
57
+ break;
58
+ case 'top-end':
59
+ position = {
60
+ top: anchorRect.top + viewport.scrollY - contentSize.height - offset,
61
+ left: anchorRect.right + viewport.scrollX - contentSize.width,
62
+ };
63
+ break;
64
+ case 'bottom':
65
+ position = {
66
+ top: anchorRect.bottom + viewport.scrollY + offset,
67
+ left: anchorRect.left + viewport.scrollX + anchorRect.width / 2 - contentSize.width / 2,
68
+ };
69
+ break;
70
+ case 'bottom-start':
71
+ position = {
72
+ top: anchorRect.bottom + viewport.scrollY + offset,
73
+ left: anchorRect.left + viewport.scrollX,
74
+ };
75
+ break;
76
+ case 'bottom-end':
77
+ position = {
78
+ top: anchorRect.bottom + viewport.scrollY + offset,
79
+ left: anchorRect.right + viewport.scrollX - contentSize.width,
80
+ };
81
+ break;
82
+ case 'left':
83
+ position = {
84
+ top: anchorRect.top + viewport.scrollY + anchorRect.height / 2 - contentSize.height / 2,
85
+ left: anchorRect.left + viewport.scrollX - contentSize.width - offset,
86
+ };
87
+ break;
88
+ case 'left-start':
89
+ position = {
90
+ top: anchorRect.top + viewport.scrollY,
91
+ left: anchorRect.left + viewport.scrollX - contentSize.width - offset,
92
+ };
93
+ break;
94
+ case 'left-end':
95
+ position = {
96
+ top: anchorRect.bottom + viewport.scrollY - contentSize.height,
97
+ left: anchorRect.left + viewport.scrollX - contentSize.width - offset,
98
+ };
99
+ break;
100
+ case 'right':
101
+ position = {
102
+ top: anchorRect.top + viewport.scrollY + anchorRect.height / 2 - contentSize.height / 2,
103
+ left: anchorRect.right + viewport.scrollX + offset,
104
+ };
105
+ break;
106
+ case 'right-start':
107
+ position = {
108
+ top: anchorRect.top + viewport.scrollY,
109
+ left: anchorRect.right + viewport.scrollX + offset,
110
+ };
111
+ break;
112
+ case 'right-end':
113
+ position = {
114
+ top: anchorRect.bottom + viewport.scrollY - contentSize.height,
115
+ left: anchorRect.right + viewport.scrollX + offset,
116
+ };
117
+ break;
118
+ }
119
+
120
+ // Match anchor width if requested
121
+ if (matchWidth) {
122
+ position.width = anchorRect.width;
123
+ }
124
+
125
+ // Constrain to viewport
126
+ const padding = 8;
127
+ position.left = Math.max(padding, Math.min(position.left, viewport.width - contentSize.width - padding));
128
+ position.top = Math.max(padding, Math.min(position.top, viewport.height + viewport.scrollY - contentSize.height - padding));
129
+
130
+ return position;
131
+ };
132
+
133
+ export const PositionedPortal: React.FC<PositionedPortalProps> = ({
134
+ open,
135
+ anchor,
136
+ children,
137
+ placement = 'bottom-start',
138
+ offset = 4,
139
+ onClickOutside,
140
+ onEscapeKey,
141
+ matchWidth = false,
142
+ zIndex = 1000,
143
+ }) => {
144
+ const contentRef = useRef<HTMLDivElement>(null);
145
+ const [position, setPosition] = useState<Position>({ top: 0, left: 0 });
146
+ const [isPositioned, setIsPositioned] = useState(false);
147
+
148
+ // Calculate position
149
+ const updatePosition = useCallback(() => {
150
+ if (!contentRef.current || !anchor.current) {
151
+ return;
152
+ }
153
+
154
+ const anchorRect = anchor.current.getBoundingClientRect();
155
+ const contentRect = contentRef.current.getBoundingClientRect();
156
+
157
+ // Use actual measured size from the DOM
158
+ const newPosition = calculatePosition(
159
+ anchorRect,
160
+ { width: contentRect.width, height: contentRect.height },
161
+ placement,
162
+ offset,
163
+ matchWidth
164
+ );
165
+
166
+ setPosition(newPosition);
167
+ setIsPositioned(true);
168
+ }, [anchor, placement, offset, matchWidth]);
169
+
170
+ // Position after DOM is ready
171
+ useLayoutEffect(() => {
172
+ if (open) {
173
+ // Use requestAnimationFrame to ensure ref is attached and layout is complete
174
+ const rafId = requestAnimationFrame(() => {
175
+ if (contentRef.current && anchor.current) {
176
+ updatePosition();
177
+ }
178
+ });
179
+ return () => cancelAnimationFrame(rafId);
180
+ } else {
181
+ setIsPositioned(false);
182
+ }
183
+ }, [open, updatePosition]);
184
+
185
+ // Update position on scroll/resize
186
+ useEffect(() => {
187
+ if (!open) return;
188
+
189
+ updatePosition();
190
+
191
+ const handleUpdate = () => updatePosition();
192
+ window.addEventListener('resize', handleUpdate);
193
+ window.addEventListener('scroll', handleUpdate, true);
194
+
195
+ return () => {
196
+ window.removeEventListener('resize', handleUpdate);
197
+ window.removeEventListener('scroll', handleUpdate, true);
198
+ };
199
+ }, [open, updatePosition]);
200
+
201
+ // Handle escape key
202
+ useEffect(() => {
203
+ if (!open || !onEscapeKey) return;
204
+
205
+ const handleEscape = (event: KeyboardEvent) => {
206
+ if (event.key === 'Escape') {
207
+ onEscapeKey();
208
+ }
209
+ };
210
+
211
+ document.addEventListener('keydown', handleEscape);
212
+ return () => document.removeEventListener('keydown', handleEscape);
213
+ }, [open, onEscapeKey]);
214
+
215
+ // Handle click outside
216
+ useEffect(() => {
217
+ if (!open || !onClickOutside) return;
218
+
219
+ const handleClickOutside = (event: MouseEvent) => {
220
+ const target = event.target as Node;
221
+
222
+ if (
223
+ contentRef.current && !contentRef.current.contains(target) &&
224
+ anchor.current && !anchor.current.contains(target)
225
+ ) {
226
+ onClickOutside();
227
+ }
228
+ };
229
+
230
+ document.addEventListener('mousedown', handleClickOutside, true);
231
+ return () => document.removeEventListener('mousedown', handleClickOutside, true);
232
+ }, [open, onClickOutside, anchor]);
233
+
234
+ if (!open) return null;
235
+
236
+ const content = (
237
+ <div
238
+ ref={contentRef}
239
+ style={{
240
+ position: 'fixed',
241
+ top: position.top,
242
+ left: position.left,
243
+ width: position.width,
244
+ zIndex,
245
+ opacity: isPositioned ? 1 : 0,
246
+ pointerEvents: isPositioned ? 'auto' : 'none',
247
+ }}
248
+ >
249
+ {children}
250
+ </div>
251
+ );
252
+
253
+ return createPortal(content, document.body);
254
+ };
@@ -0,0 +1,173 @@
1
+ import React from 'react';
2
+ import { View, Text, Dimensions, StyleSheet } from 'react-native';
3
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
4
+
5
+ interface SafeAreaDebugOverlayProps {
6
+ visible?: boolean;
7
+ }
8
+
9
+ export const SafeAreaDebugOverlay: React.FC<SafeAreaDebugOverlayProps> = ({ visible = true }) => {
10
+ const insets = useSafeAreaInsets();
11
+ const { width: windowWidth, height: windowHeight } = Dimensions.get('window');
12
+ const padding = 12;
13
+
14
+ if (!visible) return null;
15
+
16
+ // Calculate safe area bounds (system-defined safe zones)
17
+ // Allow overlap with header at top (lenient), but respect system UI at bottom
18
+ const topSafeEdge = 0; // Allow header overlap
19
+ const leftSafeEdge = insets.left;
20
+ const rightSafeEdge = windowWidth - insets.right;
21
+ const bottomSafeEdge = windowHeight - insets.bottom;
22
+
23
+ // Calculate content bounds (safe area with padding for visual comfort)
24
+ const topBound = padding; // Start from top with just padding
25
+ const leftBound = leftSafeEdge + padding;
26
+ const rightBound = rightSafeEdge - padding;
27
+ const bottomBound = bottomSafeEdge - padding;
28
+
29
+ const safeWidth = rightBound - leftBound;
30
+ const safeHeight = windowHeight - insets.bottom - insets.top - padding * 2;
31
+
32
+ return (
33
+ <View style={styles.container} pointerEvents="none">
34
+ {/* Top unsafe area (status bar, notch) */}
35
+ <View
36
+ style={[
37
+ styles.safeAreaIndicator,
38
+ {
39
+ top: 0,
40
+ left: 0,
41
+ width: windowWidth,
42
+ height: topSafeEdge,
43
+ backgroundColor: 'rgba(255, 0, 0, 0.2)',
44
+ },
45
+ ]}
46
+ >
47
+ <Text style={styles.label}>System UI: {topSafeEdge.toFixed(0)}px</Text>
48
+ </View>
49
+
50
+ {/* Bottom unsafe area (gesture bar, home indicator) */}
51
+ <View
52
+ style={[
53
+ styles.safeAreaIndicator,
54
+ {
55
+ top: bottomSafeEdge,
56
+ left: 0,
57
+ width: windowWidth,
58
+ height: windowHeight - bottomSafeEdge,
59
+ backgroundColor: 'rgba(255, 0, 0, 0.2)',
60
+ },
61
+ ]}
62
+ >
63
+ <Text style={styles.label}>Gesture Bar: {(windowHeight - bottomSafeEdge).toFixed(0)}px</Text>
64
+ </View>
65
+
66
+ {/* Left unsafe area */}
67
+ <View
68
+ style={[
69
+ styles.safeAreaIndicator,
70
+ {
71
+ top: topSafeEdge,
72
+ left: 0,
73
+ width: leftSafeEdge,
74
+ height: bottomSafeEdge - topSafeEdge,
75
+ backgroundColor: 'rgba(255, 0, 0, 0.2)',
76
+ },
77
+ ]}
78
+ >
79
+ <Text style={[styles.label, styles.rotatedLabel]}>L: {leftSafeEdge.toFixed(0)}px</Text>
80
+ </View>
81
+
82
+ {/* Right unsafe area */}
83
+ <View
84
+ style={[
85
+ styles.safeAreaIndicator,
86
+ {
87
+ top: topSafeEdge,
88
+ left: rightSafeEdge,
89
+ width: windowWidth - rightSafeEdge,
90
+ height: bottomSafeEdge - topSafeEdge,
91
+ backgroundColor: 'rgba(255, 0, 0, 0.2)',
92
+ },
93
+ ]}
94
+ >
95
+ <Text style={[styles.label, styles.rotatedLabel]}>R: {(windowWidth - rightSafeEdge).toFixed(0)}px</Text>
96
+ </View>
97
+
98
+ {/* Safe area border outline */}
99
+ <View
100
+ style={[
101
+ styles.safeAreaBorder,
102
+ {
103
+ top: topBound,
104
+ left: leftBound,
105
+ width: safeWidth,
106
+ height: safeHeight,
107
+ },
108
+ ]}
109
+ />
110
+
111
+ {/* Info panel */}
112
+ <View style={styles.infoPanel}>
113
+ <Text style={styles.infoText}>Window: {windowWidth.toFixed(0)}x{windowHeight.toFixed(0)}</Text>
114
+ <Text style={styles.infoText}>Safe Area: {(rightSafeEdge - leftSafeEdge).toFixed(0)}x{(bottomSafeEdge - topSafeEdge).toFixed(0)}</Text>
115
+ <Text style={styles.infoText}>Content: {safeWidth.toFixed(0)}x{safeHeight.toFixed(0)}</Text>
116
+ <Text style={styles.infoText}>Insets: T:{insets.top} R:{insets.right} B:{insets.bottom} L:{insets.left}</Text>
117
+ <Text style={styles.infoText}>Safe: T:{topSafeEdge} B:{bottomSafeEdge.toFixed(0)}</Text>
118
+ <Text style={styles.infoText}>Bounds: T:{topBound.toFixed(0)} B:{bottomBound.toFixed(0)}</Text>
119
+ <Text style={styles.infoText}>Padding: {padding}px</Text>
120
+ </View>
121
+ </View>
122
+ );
123
+ };
124
+
125
+ const styles = StyleSheet.create({
126
+ container: {
127
+ position: 'absolute',
128
+ top: 0,
129
+ left: 0,
130
+ right: 0,
131
+ bottom: 0,
132
+ zIndex: 9999,
133
+ },
134
+ safeAreaIndicator: {
135
+ position: 'absolute',
136
+ justifyContent: 'center',
137
+ alignItems: 'center',
138
+ },
139
+ safeAreaBorder: {
140
+ position: 'absolute',
141
+ borderWidth: 2,
142
+ borderColor: 'rgba(0, 255, 0, 0.8)',
143
+ borderStyle: 'dashed',
144
+ },
145
+ label: {
146
+ color: '#fff',
147
+ fontSize: 10,
148
+ fontWeight: 'bold',
149
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
150
+ paddingHorizontal: 4,
151
+ paddingVertical: 2,
152
+ borderRadius: 4,
153
+ },
154
+ rotatedLabel: {
155
+ transform: [{ rotate: '90deg' }],
156
+ },
157
+ infoPanel: {
158
+ position: 'absolute',
159
+ top: 50,
160
+ right: 10,
161
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
162
+ padding: 8,
163
+ borderRadius: 8,
164
+ borderWidth: 1,
165
+ borderColor: 'rgba(0, 255, 0, 0.8)',
166
+ },
167
+ infoText: {
168
+ color: '#fff',
169
+ fontSize: 11,
170
+ fontFamily: 'monospace',
171
+ marginBottom: 2,
172
+ },
173
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * This file ensures that the Unistyles module augmentation from @idealyst/theme
3
+ * is properly loaded for all component files.
4
+ */
5
+
6
+ import '@idealyst/theme/unistyles';
@@ -0,0 +1,16 @@
1
+ import { AllComponentSizes, Size, Theme, Styles } from '@idealyst/theme';
2
+
3
+ /**
4
+ * Builds a generic size variant. Not really useful on its own tbh, just good to show how it can be used.
5
+ * Context really matters for sizes
6
+ * @param theme
7
+ * @param builder
8
+ * @returns
9
+ */
10
+ export function buildSizeVariants<T extends keyof AllComponentSizes>(theme: Theme, component: T, builder: (value: AllComponentSizes[T][Size]) => Styles): Record<Size, Styles> {
11
+ const variants = {} as Record<Size, Styles>;
12
+ for (const size in theme.sizes[component]) {
13
+ variants[size as Size] = builder(theme.sizes[component][size as Size]);
14
+ }
15
+ return variants;
16
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Check if a value is a plain object (not an array, null, or other special object)
3
+ */
4
+ function isPlainObject(value: unknown): value is Record<string, any> {
5
+ return (
6
+ value !== null &&
7
+ typeof value === 'object' &&
8
+ !Array.isArray(value) &&
9
+ Object.prototype.toString.call(value) === '[object Object]'
10
+ )
11
+ }
12
+
13
+ /**
14
+ * Deep merge two objects together, with the second object taking priority.
15
+ * Arrays and non-plain objects are replaced rather than merged.
16
+ *
17
+ * @param target - The base object
18
+ * @param source - The object to merge in (takes priority)
19
+ * @returns A new merged object
20
+ */
21
+ export function deepMerge<T extends Record<string, any>, S extends Record<string, any>>(
22
+ target: T,
23
+ source: S
24
+ ): T & S {
25
+ const result: Record<string, any> = { ...target }
26
+
27
+ for (const key in source) {
28
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
29
+ const sourceValue = source[key]
30
+ const targetValue = result[key]
31
+
32
+ // If both values are plain objects, merge them recursively
33
+ if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {
34
+ result[key] = deepMerge(targetValue, sourceValue)
35
+ } else {
36
+ // Otherwise, source value takes priority (including arrays, primitives, null, undefined)
37
+ result[key] = sourceValue
38
+ }
39
+ }
40
+ }
41
+
42
+ return result as T & S
43
+ }