jfs-components 0.0.72 → 0.0.74

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 (158) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/lib/commonjs/components/AccordionCheckbox/AccordionCheckbox.js +239 -0
  3. package/lib/commonjs/components/AccountCard/AccountCard.js +247 -0
  4. package/lib/commonjs/components/AppBar/AppBar.js +17 -11
  5. package/lib/commonjs/components/BrandChip/BrandChip.js +149 -0
  6. package/lib/commonjs/components/CardBankAccount/CardBankAccount.js +229 -0
  7. package/lib/commonjs/components/CardInsight/CardInsight.js +166 -0
  8. package/lib/commonjs/components/CheckboxGroup/CheckboxGroup.js +67 -0
  9. package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +140 -0
  10. package/lib/commonjs/components/CircularProgressBar/CircularProgressBar.js +56 -9
  11. package/lib/commonjs/components/CoverageBarComparison/CoverageBarComparison.js +272 -0
  12. package/lib/commonjs/components/CoverageRing/CoverageRing.js +141 -0
  13. package/lib/commonjs/components/DonutChart/DonutChart.js +309 -0
  14. package/lib/commonjs/components/DonutChartSummary/DonutChartSummary.js +155 -0
  15. package/lib/commonjs/components/Dropdown/Dropdown.js +214 -0
  16. package/lib/commonjs/components/DropdownInput/DropdownInput.js +542 -0
  17. package/lib/commonjs/components/FormField/FormField.js +328 -178
  18. package/lib/commonjs/components/LinearMeter/LinearMeter.js +9 -28
  19. package/lib/commonjs/components/LinearProgress/LinearProgress.js +68 -0
  20. package/lib/commonjs/components/LottieIntroBlock/LottieIntroBlock.js +150 -0
  21. package/lib/commonjs/components/MetricLegendItem/MetricLegendItem.js +95 -0
  22. package/lib/commonjs/components/MonthlyStatusGrid/MonthlyStatusGrid.js +286 -0
  23. package/lib/commonjs/components/OTP/OTP.js +381 -37
  24. package/lib/commonjs/components/PageHero/PageHero.js +153 -0
  25. package/lib/commonjs/components/PoweredByLabel/PoweredByLabel.js +135 -0
  26. package/lib/commonjs/components/PoweredByLabel/finvu.png +0 -0
  27. package/lib/commonjs/components/ProductOverview/ProductOverview.js +147 -0
  28. package/lib/commonjs/components/RangeTrack/RangeTrack.js +269 -0
  29. package/lib/commonjs/components/SavingsGoalSummary/SavingsGoalSummary.js +181 -0
  30. package/lib/commonjs/components/SegmentedTrack/SegmentedTrack.js +171 -0
  31. package/lib/commonjs/components/StatGroup/StatGroup.js +128 -0
  32. package/lib/commonjs/components/StatItem/StatItem.js +65 -35
  33. package/lib/commonjs/components/StrengthIndicator/StrengthIndicator.js +157 -0
  34. package/lib/commonjs/components/SummaryTile/SummaryTile.js +150 -0
  35. package/lib/commonjs/components/Text/Text.js +9 -2
  36. package/lib/commonjs/components/Tooltip/Tooltip.js +34 -27
  37. package/lib/commonjs/components/index.js +231 -1
  38. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  39. package/lib/commonjs/icons/registry.js +1 -1
  40. package/lib/commonjs/utils/index.js +7 -0
  41. package/lib/commonjs/utils/number-utils.js +57 -0
  42. package/lib/module/components/AccordionCheckbox/AccordionCheckbox.js +233 -0
  43. package/lib/module/components/AccountCard/AccountCard.js +241 -0
  44. package/lib/module/components/AppBar/AppBar.js +17 -11
  45. package/lib/module/components/BrandChip/BrandChip.js +143 -0
  46. package/lib/module/components/CardBankAccount/CardBankAccount.js +223 -0
  47. package/lib/module/components/CardInsight/CardInsight.js +161 -0
  48. package/lib/module/components/CheckboxGroup/CheckboxGroup.js +62 -0
  49. package/lib/module/components/CheckboxItem/CheckboxItem.js +134 -0
  50. package/lib/module/components/CircularProgressBar/CircularProgressBar.js +56 -9
  51. package/lib/module/components/CoverageBarComparison/CoverageBarComparison.js +266 -0
  52. package/lib/module/components/CoverageRing/CoverageRing.js +136 -0
  53. package/lib/module/components/DonutChart/DonutChart.js +303 -0
  54. package/lib/module/components/DonutChartSummary/DonutChartSummary.js +150 -0
  55. package/lib/module/components/Dropdown/Dropdown.js +206 -0
  56. package/lib/module/components/DropdownInput/DropdownInput.js +536 -0
  57. package/lib/module/components/FormField/FormField.js +330 -180
  58. package/lib/module/components/LinearMeter/LinearMeter.js +9 -28
  59. package/lib/module/components/LinearProgress/LinearProgress.js +63 -0
  60. package/lib/module/components/LottieIntroBlock/LottieIntroBlock.js +144 -0
  61. package/lib/module/components/MetricLegendItem/MetricLegendItem.js +90 -0
  62. package/lib/module/components/MonthlyStatusGrid/MonthlyStatusGrid.js +281 -0
  63. package/lib/module/components/OTP/OTP.js +381 -38
  64. package/lib/module/components/PageHero/PageHero.js +147 -0
  65. package/lib/module/components/PoweredByLabel/PoweredByLabel.js +130 -0
  66. package/lib/module/components/PoweredByLabel/finvu.png +0 -0
  67. package/lib/module/components/ProductOverview/ProductOverview.js +142 -0
  68. package/lib/module/components/RangeTrack/RangeTrack.js +263 -0
  69. package/lib/module/components/SavingsGoalSummary/SavingsGoalSummary.js +175 -0
  70. package/lib/module/components/SegmentedTrack/SegmentedTrack.js +166 -0
  71. package/lib/module/components/StatGroup/StatGroup.js +123 -0
  72. package/lib/module/components/StatItem/StatItem.js +66 -36
  73. package/lib/module/components/StrengthIndicator/StrengthIndicator.js +152 -0
  74. package/lib/module/components/SummaryTile/SummaryTile.js +145 -0
  75. package/lib/module/components/Text/Text.js +9 -2
  76. package/lib/module/components/Tooltip/Tooltip.js +34 -27
  77. package/lib/module/components/index.js +28 -2
  78. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  79. package/lib/module/icons/registry.js +1 -1
  80. package/lib/module/utils/index.js +2 -1
  81. package/lib/module/utils/number-utils.js +53 -0
  82. package/lib/typescript/src/components/AccordionCheckbox/AccordionCheckbox.d.ts +71 -0
  83. package/lib/typescript/src/components/AccountCard/AccountCard.d.ts +81 -0
  84. package/lib/typescript/src/components/BrandChip/BrandChip.d.ts +43 -0
  85. package/lib/typescript/src/components/CardBankAccount/CardBankAccount.d.ts +86 -0
  86. package/lib/typescript/src/components/CardInsight/CardInsight.d.ts +48 -0
  87. package/lib/typescript/src/components/CheckboxGroup/CheckboxGroup.d.ts +41 -0
  88. package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +72 -0
  89. package/lib/typescript/src/components/CircularProgressBar/CircularProgressBar.d.ts +11 -1
  90. package/lib/typescript/src/components/CoverageBarComparison/CoverageBarComparison.d.ts +105 -0
  91. package/lib/typescript/src/components/CoverageRing/CoverageRing.d.ts +90 -0
  92. package/lib/typescript/src/components/DonutChart/DonutChart.d.ts +117 -0
  93. package/lib/typescript/src/components/DonutChartSummary/DonutChartSummary.d.ts +103 -0
  94. package/lib/typescript/src/components/Dropdown/Dropdown.d.ts +62 -0
  95. package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +107 -0
  96. package/lib/typescript/src/components/FormField/FormField.d.ts +76 -19
  97. package/lib/typescript/src/components/LinearProgress/LinearProgress.d.ts +17 -0
  98. package/lib/typescript/src/components/LottieIntroBlock/LottieIntroBlock.d.ts +58 -0
  99. package/lib/typescript/src/components/MetricLegendItem/MetricLegendItem.d.ts +37 -0
  100. package/lib/typescript/src/components/MonthlyStatusGrid/MonthlyStatusGrid.d.ts +119 -0
  101. package/lib/typescript/src/components/OTP/OTP.d.ts +88 -2
  102. package/lib/typescript/src/components/PageHero/PageHero.d.ts +53 -0
  103. package/lib/typescript/src/components/PoweredByLabel/PoweredByLabel.d.ts +70 -0
  104. package/lib/typescript/src/components/ProductOverview/ProductOverview.d.ts +39 -0
  105. package/lib/typescript/src/components/RangeTrack/RangeTrack.d.ts +173 -0
  106. package/lib/typescript/src/components/SavingsGoalSummary/SavingsGoalSummary.d.ts +95 -0
  107. package/lib/typescript/src/components/SegmentedTrack/SegmentedTrack.d.ts +108 -0
  108. package/lib/typescript/src/components/StatGroup/StatGroup.d.ts +45 -0
  109. package/lib/typescript/src/components/StatItem/StatItem.d.ts +24 -7
  110. package/lib/typescript/src/components/StrengthIndicator/StrengthIndicator.d.ts +58 -0
  111. package/lib/typescript/src/components/SummaryTile/SummaryTile.d.ts +60 -0
  112. package/lib/typescript/src/components/Text/Text.d.ts +12 -2
  113. package/lib/typescript/src/components/Tooltip/Tooltip.d.ts +13 -2
  114. package/lib/typescript/src/components/index.d.ts +29 -3
  115. package/lib/typescript/src/icons/registry.d.ts +1 -1
  116. package/lib/typescript/src/utils/index.d.ts +1 -0
  117. package/lib/typescript/src/utils/number-utils.d.ts +29 -0
  118. package/package.json +1 -3
  119. package/src/components/AccordionCheckbox/AccordionCheckbox.tsx +323 -0
  120. package/src/components/AccountCard/AccountCard.tsx +376 -0
  121. package/src/components/AppBar/AppBar.tsx +25 -14
  122. package/src/components/BrandChip/BrandChip.tsx +235 -0
  123. package/src/components/CardBankAccount/CardBankAccount.tsx +321 -0
  124. package/src/components/CardInsight/CardInsight.tsx +239 -0
  125. package/src/components/CheckboxGroup/CheckboxGroup.tsx +86 -0
  126. package/src/components/CheckboxItem/CheckboxItem.tsx +209 -0
  127. package/src/components/CircularProgressBar/CircularProgressBar.tsx +74 -9
  128. package/src/components/CoverageBarComparison/CoverageBarComparison.tsx +378 -0
  129. package/src/components/CoverageRing/CoverageRing.tsx +225 -0
  130. package/src/components/DonutChart/DonutChart.tsx +503 -0
  131. package/src/components/DonutChartSummary/DonutChartSummary.tsx +256 -0
  132. package/src/components/Dropdown/Dropdown.tsx +331 -0
  133. package/src/components/DropdownInput/DropdownInput.tsx +819 -0
  134. package/src/components/FormField/FormField.tsx +542 -215
  135. package/src/components/LinearMeter/LinearMeter.tsx +9 -39
  136. package/src/components/LinearProgress/LinearProgress.tsx +92 -0
  137. package/src/components/LottieIntroBlock/LottieIntroBlock.tsx +202 -0
  138. package/src/components/MetricLegendItem/MetricLegendItem.tsx +167 -0
  139. package/src/components/MonthlyStatusGrid/MonthlyStatusGrid.tsx +438 -0
  140. package/src/components/OTP/OTP.tsx +476 -29
  141. package/src/components/PageHero/PageHero.tsx +200 -0
  142. package/src/components/PoweredByLabel/PoweredByLabel.tsx +221 -0
  143. package/src/components/PoweredByLabel/finvu.png +0 -0
  144. package/src/components/ProductOverview/ProductOverview.tsx +236 -0
  145. package/src/components/RangeTrack/RangeTrack.tsx +394 -0
  146. package/src/components/SavingsGoalSummary/SavingsGoalSummary.tsx +269 -0
  147. package/src/components/SegmentedTrack/SegmentedTrack.tsx +268 -0
  148. package/src/components/StatGroup/StatGroup.tsx +169 -0
  149. package/src/components/StatItem/StatItem.tsx +117 -40
  150. package/src/components/StrengthIndicator/StrengthIndicator.tsx +205 -0
  151. package/src/components/SummaryTile/SummaryTile.tsx +251 -0
  152. package/src/components/Text/Text.tsx +24 -3
  153. package/src/components/Tooltip/Tooltip.tsx +50 -25
  154. package/src/components/index.ts +47 -3
  155. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  156. package/src/icons/registry.ts +1 -1
  157. package/src/utils/index.ts +1 -0
  158. package/src/utils/number-utils.ts +60 -0
@@ -0,0 +1,536 @@
1
+ "use strict";
2
+
3
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
+ import { Dimensions, Modal, Platform, Pressable, StyleSheet, Text, View } from 'react-native';
5
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
6
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
7
+ import { useTokens } from '../../design-tokens/JFSThemeProvider';
8
+ import { EMPTY_MODES, flattenChildren } from '../../utils/react-utils';
9
+ import Icon from '../../icons/Icon';
10
+ import SupportText from '../SupportText/SupportText';
11
+ import Dropdown, { DropdownItem } from '../Dropdown/Dropdown';
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ const IS_WEB = Platform.OS === 'web';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Types
17
+ // ---------------------------------------------------------------------------
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Token resolution
21
+ // ---------------------------------------------------------------------------
22
+
23
+ function useChevronTokens(modes) {
24
+ return useMemo(() => {
25
+ const iconSize = parseInt(getVariableByName('input/iconSize', modes), 10) || 32;
26
+ const iconColor = getVariableByName('iconButton/icon/color', modes) || '#0f0d0a';
27
+ return {
28
+ iconSize,
29
+ iconColor
30
+ };
31
+ }, [modes]);
32
+ }
33
+ function useFormFieldTokens(modes) {
34
+ return useMemo(() => {
35
+ const labelColor = getVariableByName('formField/label/color', modes) || '#0c0d10';
36
+ const labelFontFamily = getVariableByName('formField/label/fontFamily', modes) || 'JioType Var';
37
+ const labelFontSize = parseInt(getVariableByName('formField/label/fontSize', modes), 10) || 14;
38
+ const labelLineHeight = parseInt(getVariableByName('formField/label/lineHeight', modes), 10) || 17;
39
+ const labelFontWeight = getVariableByName('formField/label/fontWeight', modes) || '500';
40
+ const gap = parseInt(getVariableByName('formField/gap', modes), 10) || 8;
41
+ const inputPaddingH = parseInt(getVariableByName('formField/input/padding/horizontal', modes), 10) || 12;
42
+ const inputGap = parseInt(getVariableByName('formField/input/gap', modes), 10) || 8;
43
+ const inputRadius = parseInt(getVariableByName('formField/input/radius', modes), 10) || 8;
44
+ const inputBackground = getVariableByName('formField/input/background', modes) || '#ffffff';
45
+ const inputFontSize = parseInt(getVariableByName('formField/input/label/fontSize', modes), 10) || 16;
46
+ const inputLineHeight = parseInt(getVariableByName('formField/input/label/lineHeight', modes), 10) || 45;
47
+ const inputFontFamily = getVariableByName('formField/input/label/fontFamily', modes) || 'JioType Var';
48
+ const inputFontWeight = getVariableByName('formField/input/label/fontWeight', modes) || '400';
49
+ const inputTextColor = getVariableByName('states/formField/input/label/color', modes) || getVariableByName('formField/input/label/color', modes) || '#24262b';
50
+ const inputBorderColor = getVariableByName('states/formField/input/border/color', modes) || getVariableByName('formField/input/border/color', modes) || '#b5b6b7';
51
+ const inputBorderSize = parseInt(getVariableByName('formField/input/border/size', modes), 10) || 1;
52
+ return {
53
+ labelColor,
54
+ labelFontFamily,
55
+ labelFontSize,
56
+ labelLineHeight,
57
+ labelFontWeight,
58
+ gap,
59
+ inputPaddingH,
60
+ inputGap,
61
+ inputRadius,
62
+ inputBackground,
63
+ inputFontSize,
64
+ inputLineHeight,
65
+ inputFontFamily,
66
+ inputFontWeight,
67
+ inputTextColor,
68
+ inputBorderColor,
69
+ inputBorderSize
70
+ };
71
+ }, [modes]);
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Helpers
76
+ // ---------------------------------------------------------------------------
77
+
78
+ /**
79
+ * Collect every option this DropdownInput knows about, in render order, from
80
+ * both `items` and `children` slots. Used for keyboard navigation, lookups
81
+ * of the selected option, and accessibility labels.
82
+ */
83
+ function collectOptionsFromChildren(children) {
84
+ const out = [];
85
+ flattenChildren(children).forEach(child => {
86
+ if (! /*#__PURE__*/React.isValidElement(child)) return;
87
+ if (child.type !== DropdownItem) return;
88
+ const childProps = child.props;
89
+ const {
90
+ value,
91
+ label,
92
+ disabled
93
+ } = childProps;
94
+ if (value == null) return;
95
+ if (typeof value !== 'string' && typeof value !== 'number') return;
96
+ if (typeof label !== 'string') return;
97
+ const opt = {
98
+ value: value,
99
+ label
100
+ };
101
+ if (disabled != null) opt.disabled = disabled;
102
+ out.push(opt);
103
+ });
104
+ return out;
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Component
109
+ // ---------------------------------------------------------------------------
110
+
111
+ function DropdownInput({
112
+ label,
113
+ placeholder = 'Select an option',
114
+ items,
115
+ value,
116
+ defaultValue = null,
117
+ onValueChange,
118
+ children,
119
+ renderValue,
120
+ open,
121
+ defaultOpen = false,
122
+ onOpenChange,
123
+ placement = 'bottom',
124
+ isRequired = false,
125
+ isDisabled = false,
126
+ isInvalid = false,
127
+ isReadOnly = false,
128
+ supportText,
129
+ errorMessage,
130
+ menuMaxHeight = 240,
131
+ menuOffset = 4,
132
+ matchTriggerWidth = true,
133
+ closeOnBackdropPress = true,
134
+ modes: propModes = EMPTY_MODES,
135
+ style,
136
+ inputStyle,
137
+ menuStyle,
138
+ accessibilityLabel,
139
+ accessibilityHint,
140
+ onFocus,
141
+ onBlur
142
+ }) {
143
+ // ---------------- Modes ----------------
144
+ const {
145
+ modes: globalModes
146
+ } = useTokens();
147
+ const baseModes = useMemo(() => ({
148
+ ...globalModes,
149
+ ...propModes
150
+ }), [globalModes, propModes]);
151
+
152
+ // ---------------- Open state ----------------
153
+ const isControlledOpen = open !== undefined;
154
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
155
+ const isOpen = (isControlledOpen ? open : internalOpen) && !isDisabled && !isReadOnly;
156
+ const setOpenState = useCallback(next => {
157
+ if (!isControlledOpen) setInternalOpen(next);
158
+ onOpenChange?.(next);
159
+ }, [isControlledOpen, onOpenChange]);
160
+ const closeMenu = useCallback(() => setOpenState(false), [setOpenState]);
161
+ const toggleMenu = useCallback(() => setOpenState(!isOpen), [isOpen, setOpenState]);
162
+
163
+ // ---------------- Value state ----------------
164
+ const isControlledValue = value !== undefined;
165
+ const [internalValue, setInternalValue] = useState(defaultValue);
166
+ const currentValue = isControlledValue ? value : internalValue;
167
+
168
+ // Combine items + children-derived options into a single lookup table so
169
+ // selecting via either API surfaces the same option metadata.
170
+ const childOptions = useMemo(() => collectOptionsFromChildren(children), [children]);
171
+ const allOptions = useMemo(() => [...(items ?? []), ...childOptions], [items, childOptions]);
172
+ const selectedOption = useMemo(() => allOptions.find(o => o.value === currentValue), [allOptions, currentValue]);
173
+ const handleSelect = useCallback(selectedValue => {
174
+ if (typeof selectedValue !== 'string' && typeof selectedValue !== 'number') {
175
+ // Items without a meaningful value just close the menu.
176
+ closeMenu();
177
+ return;
178
+ }
179
+ const option = allOptions.find(o => o.value === selectedValue);
180
+ if (option?.disabled) return;
181
+ if (!isControlledValue) setInternalValue(selectedValue);
182
+ onValueChange?.(selectedValue, option);
183
+ closeMenu();
184
+ }, [allOptions, closeMenu, isControlledValue, onValueChange]);
185
+
186
+ // ---------------- Token modes (with state cascade) ----------------
187
+ const modes = useMemo(() => ({
188
+ ...baseModes,
189
+ 'FormField States': isInvalid ? 'Error' : isReadOnly ? 'Read Only' : isOpen ? 'Active' : baseModes['FormField States'] || 'Idle'
190
+ }), [baseModes, isInvalid, isReadOnly, isOpen]);
191
+ const tokens = useFormFieldTokens(modes);
192
+ const chevron = useChevronTokens(modes);
193
+
194
+ // ---------------- Layout / measurement ----------------
195
+ const triggerRef = useRef(null);
196
+ const [triggerRect, setTriggerRect] = useState(null);
197
+ const insets = useSafeAreaInsets();
198
+ const measure = useCallback(() => {
199
+ if (!triggerRef.current) return;
200
+ triggerRef.current.measureInWindow((x, y, width, height) => {
201
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(width) || !Number.isFinite(height)) {
202
+ return;
203
+ }
204
+ setTriggerRect(prev => {
205
+ if (!prev || Math.abs(prev.x - x) > 0.5 || Math.abs(prev.y - y) > 0.5 || prev.width !== width || prev.height !== height) {
206
+ return {
207
+ x,
208
+ y,
209
+ width,
210
+ height
211
+ };
212
+ }
213
+ return prev;
214
+ });
215
+ });
216
+ }, []);
217
+
218
+ // Keep the trigger rect in sync while the menu is open (handles scroll,
219
+ // window resize, etc.). One rAF tick per frame is enough; we bail early
220
+ // if the rect hasn't changed so React doesn't re-render unnecessarily.
221
+ useEffect(() => {
222
+ if (!isOpen) return;
223
+ let raf = 0;
224
+ const loop = () => {
225
+ measure();
226
+ raf = requestAnimationFrame(loop);
227
+ };
228
+ loop();
229
+ return () => {
230
+ if (raf) cancelAnimationFrame(raf);
231
+ };
232
+ }, [isOpen, measure]);
233
+ const handleTriggerLayout = useCallback(_e => {
234
+ measure();
235
+ }, [measure]);
236
+
237
+ // ---------------- Popup positioning ----------------
238
+ const [menuSize, setMenuSize] = useState(null);
239
+ const handleMenuLayout = useCallback(e => {
240
+ const {
241
+ width,
242
+ height
243
+ } = e.nativeEvent.layout;
244
+ setMenuSize(prev => {
245
+ if (!prev || prev.width !== width || prev.height !== height) {
246
+ return {
247
+ width,
248
+ height
249
+ };
250
+ }
251
+ return prev;
252
+ });
253
+ }, []);
254
+ const {
255
+ width: windowWidth,
256
+ height: windowHeight
257
+ } = Dimensions.get('window');
258
+ const computedPlacement = useMemo(() => {
259
+ if (!triggerRect) return placement === 'top' ? 'top' : 'bottom';
260
+ const spaceBelow = windowHeight - (triggerRect.y + triggerRect.height) - insets.bottom;
261
+ const spaceAbove = triggerRect.y - insets.top;
262
+ const desiredHeight = Math.min(menuSize?.height ?? menuMaxHeight, menuMaxHeight);
263
+ const needed = desiredHeight + menuOffset + 8;
264
+ if (placement === 'top') {
265
+ return spaceAbove >= needed || spaceAbove >= spaceBelow ? 'top' : 'bottom';
266
+ }
267
+ if (placement === 'bottom') {
268
+ return spaceBelow >= needed || spaceBelow >= spaceAbove ? 'bottom' : 'top';
269
+ }
270
+ return spaceBelow >= needed || spaceBelow >= spaceAbove ? 'bottom' : 'top';
271
+ }, [triggerRect, placement, windowHeight, menuSize?.height, menuMaxHeight, menuOffset, insets.top, insets.bottom]);
272
+ const popupStyle = useMemo(() => {
273
+ if (!triggerRect) {
274
+ return {
275
+ position: 'absolute',
276
+ opacity: 0,
277
+ top: 0,
278
+ left: 0
279
+ };
280
+ }
281
+ const screenPadding = 8;
282
+ const width = matchTriggerWidth ? triggerRect.width : undefined;
283
+ const intrinsicWidth = menuSize?.width ?? triggerRect.width;
284
+ const finalWidth = width ?? intrinsicWidth;
285
+ let leftPos = triggerRect.x;
286
+ const maxLeft = windowWidth - insets.right - finalWidth - screenPadding;
287
+ const minLeft = insets.left + screenPadding;
288
+ if (leftPos > maxLeft) leftPos = maxLeft;
289
+ if (leftPos < minLeft) leftPos = minLeft;
290
+ let topPos;
291
+ if (computedPlacement === 'top') {
292
+ const desiredHeight = menuSize?.height ?? menuMaxHeight;
293
+ topPos = triggerRect.y - desiredHeight - menuOffset;
294
+ if (topPos < insets.top + screenPadding) {
295
+ topPos = insets.top + screenPadding;
296
+ }
297
+ } else {
298
+ topPos = triggerRect.y + triggerRect.height + menuOffset;
299
+ }
300
+ const style = {
301
+ position: 'absolute',
302
+ top: topPos,
303
+ left: leftPos
304
+ };
305
+ if (width != null) style.width = width;
306
+ // Hide first frame before measurement to avoid the popup flashing in
307
+ // the wrong place. menuSize becomes truthy after the first layout.
308
+ if (menuSize == null) style.opacity = 0;
309
+ return style;
310
+ }, [triggerRect, computedPlacement, menuSize, menuOffset, menuMaxHeight, matchTriggerWidth, windowWidth, insets.top, insets.left, insets.right]);
311
+
312
+ // Reset menu size when closing so the next open re-measures (handles items
313
+ // changing while the menu was closed).
314
+ useEffect(() => {
315
+ if (!isOpen) setMenuSize(null);
316
+ }, [isOpen]);
317
+
318
+ // ---------------- Styles ----------------
319
+ const labelTextStyle = {
320
+ color: tokens.labelColor,
321
+ fontFamily: tokens.labelFontFamily,
322
+ fontSize: tokens.labelFontSize,
323
+ lineHeight: tokens.labelLineHeight,
324
+ fontWeight: tokens.labelFontWeight
325
+ };
326
+ const requiredIndicatorStyle = {
327
+ ...labelTextStyle,
328
+ color: '#d93d3d'
329
+ };
330
+ const wrapperStyle = {
331
+ gap: tokens.gap,
332
+ opacity: isDisabled ? 0.5 : 1,
333
+ width: '100%'
334
+ };
335
+
336
+ // Focus ring uses the resolved input border color from FormField States so
337
+ // active/error look consistent with TextInput-based FormField. We also lift
338
+ // border weight to 2 when "Active" to read as a focus ring.
339
+ const inputRowStyle = {
340
+ flexDirection: 'row',
341
+ alignItems: 'center',
342
+ backgroundColor: tokens.inputBackground,
343
+ borderColor: tokens.inputBorderColor,
344
+ borderWidth: isOpen ? Math.max(tokens.inputBorderSize, 1) : tokens.inputBorderSize,
345
+ borderRadius: tokens.inputRadius,
346
+ paddingHorizontal: tokens.inputPaddingH,
347
+ paddingVertical: 0,
348
+ gap: tokens.inputGap,
349
+ minHeight: tokens.inputLineHeight
350
+ };
351
+ const valueTextStyle = {
352
+ flex: 1,
353
+ color: tokens.inputTextColor,
354
+ fontFamily: tokens.inputFontFamily,
355
+ fontSize: tokens.inputFontSize,
356
+ lineHeight: tokens.inputLineHeight,
357
+ fontWeight: tokens.inputFontWeight,
358
+ paddingVertical: 0
359
+ };
360
+ const placeholderColor = '#888a8d';
361
+
362
+ // ---------------- Support text ----------------
363
+ const supportStatus = isInvalid ? 'Error' : 'Neutral';
364
+ const supportLabel = isInvalid && errorMessage ? errorMessage : supportText;
365
+
366
+ // ---------------- Accessibility ----------------
367
+ const resolvedA11yLabel = accessibilityLabel || label || placeholder || 'Dropdown';
368
+ const a11yProps = {
369
+ accessibilityRole: 'combobox',
370
+ accessibilityLabel: resolvedA11yLabel,
371
+ accessibilityState: {
372
+ disabled: isDisabled,
373
+ expanded: isOpen
374
+ }
375
+ };
376
+ if (accessibilityHint) a11yProps.accessibilityHint = accessibilityHint;
377
+
378
+ // ---------------- Items rendering ----------------
379
+ const renderItems = useCallback(() => {
380
+ const itemNodes = [];
381
+ if (items && items.length > 0) {
382
+ items.forEach(opt => {
383
+ const isSelected = opt.value === currentValue;
384
+ itemNodes.push(/*#__PURE__*/_jsx(DropdownItem, {
385
+ value: opt.value,
386
+ label: opt.label,
387
+ selected: isSelected,
388
+ disabled: opt.disabled ?? false,
389
+ leading: opt.leading,
390
+ trailing: opt.trailing,
391
+ onPress: handleSelect,
392
+ modes: modes
393
+ }, `item-${opt.value}`));
394
+ });
395
+ }
396
+ if (children) {
397
+ // Inject `selected` and `onPress` into child DropdownItems so the
398
+ // consumer doesn't have to wire selection by hand. Existing
399
+ // `onPress` handlers on a child are preserved and called after our
400
+ // selection logic runs.
401
+ flattenChildren(children).forEach((child, idx) => {
402
+ if (! /*#__PURE__*/React.isValidElement(child)) {
403
+ itemNodes.push(child);
404
+ return;
405
+ }
406
+ if (child.type === DropdownItem) {
407
+ const original = child.props;
408
+ const isSelected = original.value === currentValue;
409
+ const composedOnPress = v => {
410
+ original.onPress?.(v);
411
+ handleSelect(v);
412
+ };
413
+ itemNodes.push(/*#__PURE__*/React.cloneElement(child, {
414
+ key: child.key ?? `child-${idx}`,
415
+ selected: isSelected,
416
+ onPress: composedOnPress,
417
+ modes: {
418
+ ...modes,
419
+ ...(original.modes || {})
420
+ }
421
+ }));
422
+ } else {
423
+ itemNodes.push(child);
424
+ }
425
+ });
426
+ }
427
+ return itemNodes;
428
+ }, [items, children, currentValue, handleSelect, modes]);
429
+
430
+ // ---------------- Render ----------------
431
+ const hasValue = selectedOption != null;
432
+ const displayLabel = hasValue ? selectedOption.label : placeholder;
433
+ return /*#__PURE__*/_jsxs(View, {
434
+ style: [wrapperStyle, style],
435
+ pointerEvents: isDisabled ? 'none' : 'auto',
436
+ children: [label != null && /*#__PURE__*/_jsxs(View, {
437
+ style: styles.labelRow,
438
+ children: [/*#__PURE__*/_jsx(Text, {
439
+ style: labelTextStyle,
440
+ children: label
441
+ }), isRequired && /*#__PURE__*/_jsx(Text, {
442
+ style: requiredIndicatorStyle,
443
+ children: " *"
444
+ })]
445
+ }), /*#__PURE__*/_jsxs(Pressable, {
446
+ ref: triggerRef,
447
+ onLayout: handleTriggerLayout,
448
+ onPress: () => {
449
+ if (isDisabled || isReadOnly) return;
450
+ measure();
451
+ toggleMenu();
452
+ },
453
+ ...(onFocus ? {
454
+ onFocus
455
+ } : {}),
456
+ ...(onBlur ? {
457
+ onBlur
458
+ } : {}),
459
+ style: [inputRowStyle, inputStyle, IS_WEB && webNoOutline],
460
+ ...a11yProps,
461
+ ...(IS_WEB ? {
462
+ accessibilityRole: 'combobox',
463
+ 'aria-haspopup': 'listbox',
464
+ 'aria-expanded': isOpen
465
+ } : {}),
466
+ children: [renderValue ? /*#__PURE__*/_jsx(View, {
467
+ style: {
468
+ flex: 1,
469
+ justifyContent: 'center'
470
+ },
471
+ children: renderValue(selectedOption, hasValue)
472
+ }) : /*#__PURE__*/_jsx(Text, {
473
+ style: [valueTextStyle, !hasValue && {
474
+ color: placeholderColor
475
+ }],
476
+ numberOfLines: 1,
477
+ children: displayLabel
478
+ }), /*#__PURE__*/_jsx(View, {
479
+ accessibilityElementsHidden: true,
480
+ importantForAccessibility: "no",
481
+ pointerEvents: "none",
482
+ children: /*#__PURE__*/_jsx(Icon, {
483
+ name: isOpen ? 'ic_chevron_up' : 'ic_chevron_down',
484
+ size: chevron.iconSize,
485
+ color: chevron.iconColor
486
+ })
487
+ })]
488
+ }), supportLabel != null && /*#__PURE__*/_jsx(SupportText, {
489
+ label: supportLabel,
490
+ status: supportStatus,
491
+ modes: modes
492
+ }), /*#__PURE__*/_jsx(Modal, {
493
+ visible: isOpen,
494
+ transparent: true,
495
+ animationType: "fade",
496
+ onRequestClose: closeMenu,
497
+ statusBarTranslucent: true,
498
+ children: /*#__PURE__*/_jsx(Pressable, {
499
+ style: StyleSheet.absoluteFill,
500
+ onPress: closeOnBackdropPress ? closeMenu : undefined,
501
+ accessibilityRole: "button",
502
+ accessibilityLabel: "Close options",
503
+ accessible: false,
504
+ children: /*#__PURE__*/_jsx(View, {
505
+ style: StyleSheet.absoluteFill,
506
+ pointerEvents: "box-none",
507
+ children: /*#__PURE__*/_jsx(View, {
508
+ style: popupStyle,
509
+ onLayout: handleMenuLayout,
510
+ pointerEvents: "auto",
511
+ children: /*#__PURE__*/_jsx(Dropdown, {
512
+ modes: modes,
513
+ maxHeight: menuMaxHeight,
514
+ style: menuStyle,
515
+ accessibilityLabel: `${resolvedA11yLabel} options`,
516
+ children: renderItems()
517
+ })
518
+ })
519
+ })
520
+ })
521
+ })]
522
+ });
523
+ }
524
+ const webNoOutline = {
525
+ outlineStyle: 'none',
526
+ outlineWidth: 0,
527
+ outlineColor: 'transparent',
528
+ cursor: 'pointer'
529
+ };
530
+ const styles = StyleSheet.create({
531
+ labelRow: {
532
+ flexDirection: 'row',
533
+ alignItems: 'baseline'
534
+ }
535
+ });
536
+ export default DropdownInput;