jfs-components 0.0.79 → 0.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 (110) hide show
  1. package/lib/commonjs/components/AppBar/AppBar.js +56 -6
  2. package/lib/commonjs/components/Attached/Attached.js +46 -7
  3. package/lib/commonjs/components/Checkbox/Checkbox.js +18 -2
  4. package/lib/commonjs/components/Drawer/Drawer.js +6 -1
  5. package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -6
  6. package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +17 -11
  7. package/lib/commonjs/components/FormField/FormField.js +1 -14
  8. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +5 -1
  9. package/lib/commonjs/components/ListItem/ListItem.js +6 -11
  10. package/lib/commonjs/components/MessageField/MessageField.js +1 -13
  11. package/lib/commonjs/components/PaymentFeedback/PaymentFeedback.js +12 -9
  12. package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +69 -160
  13. package/lib/commonjs/components/Spinner/Spinner.js +217 -0
  14. package/lib/commonjs/components/TextInput/TextInput.js +33 -18
  15. package/lib/commonjs/components/index.js +7 -0
  16. package/lib/commonjs/icons/components/IconArrowdown.js +19 -0
  17. package/lib/commonjs/icons/components/IconArrowup.js +19 -0
  18. package/lib/commonjs/icons/components/IconChevrondowncircle.js +19 -0
  19. package/lib/commonjs/icons/components/IconChevronleftcircle.js +19 -0
  20. package/lib/commonjs/icons/components/IconChevronrightcircle.js +19 -0
  21. package/lib/commonjs/icons/components/IconChevronupcircle.js +19 -0
  22. package/lib/commonjs/icons/components/IconOsnavback.js +19 -0
  23. package/lib/commonjs/icons/components/IconOsnavcenter.js +19 -0
  24. package/lib/commonjs/icons/components/IconOsnavhome.js +19 -0
  25. package/lib/commonjs/icons/components/IconOsnavtask.js +19 -0
  26. package/lib/commonjs/icons/components/IconSignin.js +19 -0
  27. package/lib/commonjs/icons/components/IconSignout.js +19 -0
  28. package/lib/commonjs/icons/components/index.js +132 -0
  29. package/lib/commonjs/icons/registry.js +2 -2
  30. package/lib/module/components/AppBar/AppBar.js +56 -6
  31. package/lib/module/components/Attached/Attached.js +46 -7
  32. package/lib/module/components/Checkbox/Checkbox.js +18 -2
  33. package/lib/module/components/Drawer/Drawer.js +6 -1
  34. package/lib/module/components/DropdownInput/DropdownInput.js +30 -6
  35. package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +17 -11
  36. package/lib/module/components/FormField/FormField.js +3 -16
  37. package/lib/module/components/FullscreenModal/FullscreenModal.js +5 -1
  38. package/lib/module/components/ListItem/ListItem.js +6 -11
  39. package/lib/module/components/MessageField/MessageField.js +3 -15
  40. package/lib/module/components/PaymentFeedback/PaymentFeedback.js +13 -9
  41. package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +72 -160
  42. package/lib/module/components/Spinner/Spinner.js +212 -0
  43. package/lib/module/components/TextInput/TextInput.js +34 -19
  44. package/lib/module/components/index.js +1 -0
  45. package/lib/module/icons/components/IconArrowdown.js +12 -0
  46. package/lib/module/icons/components/IconArrowup.js +12 -0
  47. package/lib/module/icons/components/IconChevrondowncircle.js +12 -0
  48. package/lib/module/icons/components/IconChevronleftcircle.js +12 -0
  49. package/lib/module/icons/components/IconChevronrightcircle.js +12 -0
  50. package/lib/module/icons/components/IconChevronupcircle.js +12 -0
  51. package/lib/module/icons/components/IconOsnavback.js +12 -0
  52. package/lib/module/icons/components/IconOsnavcenter.js +12 -0
  53. package/lib/module/icons/components/IconOsnavhome.js +12 -0
  54. package/lib/module/icons/components/IconOsnavtask.js +12 -0
  55. package/lib/module/icons/components/IconSignin.js +12 -0
  56. package/lib/module/icons/components/IconSignout.js +12 -0
  57. package/lib/module/icons/components/index.js +12 -0
  58. package/lib/module/icons/registry.js +2 -2
  59. package/lib/typescript/src/components/AppBar/AppBar.d.ts +12 -1
  60. package/lib/typescript/src/components/Attached/Attached.d.ts +19 -16
  61. package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +3 -2
  62. package/lib/typescript/src/components/ListItem/ListItem.d.ts +3 -3
  63. package/lib/typescript/src/components/PaymentFeedback/PaymentFeedback.d.ts +5 -1
  64. package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +10 -8
  65. package/lib/typescript/src/components/Spinner/Spinner.d.ts +45 -0
  66. package/lib/typescript/src/components/index.d.ts +1 -0
  67. package/lib/typescript/src/icons/components/IconArrowdown.d.ts +3 -0
  68. package/lib/typescript/src/icons/components/IconArrowup.d.ts +3 -0
  69. package/lib/typescript/src/icons/components/IconChevrondowncircle.d.ts +3 -0
  70. package/lib/typescript/src/icons/components/IconChevronleftcircle.d.ts +3 -0
  71. package/lib/typescript/src/icons/components/IconChevronrightcircle.d.ts +3 -0
  72. package/lib/typescript/src/icons/components/IconChevronupcircle.d.ts +3 -0
  73. package/lib/typescript/src/icons/components/IconOsnavback.d.ts +3 -0
  74. package/lib/typescript/src/icons/components/IconOsnavcenter.d.ts +3 -0
  75. package/lib/typescript/src/icons/components/IconOsnavhome.d.ts +3 -0
  76. package/lib/typescript/src/icons/components/IconOsnavtask.d.ts +3 -0
  77. package/lib/typescript/src/icons/components/IconSignin.d.ts +3 -0
  78. package/lib/typescript/src/icons/components/IconSignout.d.ts +3 -0
  79. package/lib/typescript/src/icons/components/index.d.ts +12 -0
  80. package/lib/typescript/src/icons/registry.d.ts +1 -1
  81. package/package.json +3 -2
  82. package/src/components/AppBar/AppBar.tsx +79 -12
  83. package/src/components/Attached/Attached.tsx +63 -7
  84. package/src/components/Checkbox/Checkbox.tsx +14 -2
  85. package/src/components/Drawer/Drawer.tsx +4 -0
  86. package/src/components/DropdownInput/DropdownInput.tsx +54 -20
  87. package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +13 -9
  88. package/src/components/FormField/FormField.tsx +3 -19
  89. package/src/components/FullscreenModal/FullscreenModal.tsx +3 -0
  90. package/src/components/ListItem/ListItem.tsx +14 -16
  91. package/src/components/MessageField/MessageField.tsx +3 -18
  92. package/src/components/PaymentFeedback/PaymentFeedback.tsx +15 -8
  93. package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +82 -192
  94. package/src/components/Spinner/Spinner.tsx +273 -0
  95. package/src/components/TextInput/TextInput.tsx +37 -19
  96. package/src/components/index.ts +1 -0
  97. package/src/icons/components/IconArrowdown.tsx +11 -0
  98. package/src/icons/components/IconArrowup.tsx +11 -0
  99. package/src/icons/components/IconChevrondowncircle.tsx +11 -0
  100. package/src/icons/components/IconChevronleftcircle.tsx +11 -0
  101. package/src/icons/components/IconChevronrightcircle.tsx +11 -0
  102. package/src/icons/components/IconChevronupcircle.tsx +11 -0
  103. package/src/icons/components/IconOsnavback.tsx +11 -0
  104. package/src/icons/components/IconOsnavcenter.tsx +11 -0
  105. package/src/icons/components/IconOsnavhome.tsx +11 -0
  106. package/src/icons/components/IconOsnavtask.tsx +11 -0
  107. package/src/icons/components/IconSignin.tsx +11 -0
  108. package/src/icons/components/IconSignout.tsx +11 -0
  109. package/src/icons/components/index.ts +12 -0
  110. package/src/icons/registry.ts +49 -1
@@ -7,10 +7,18 @@ import { useTokens } from '../../design-tokens/JFSThemeProvider';
7
7
  import NavArrow from '../NavArrow/NavArrow';
8
8
  import { cloneChildrenWithModes, EMPTY_MODES } from '../../utils/react-utils';
9
9
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
+ // SubPage "slot wrap" geometry, taken directly from the Figma design
11
+ // (node 449:7876). The middle slot is an absolutely-centered box of a fixed
12
+ // size; its inner content (node 3991:4125) is a `flex: 1 0 0; min-width: 1px`
13
+ // item so it fills / shrinks responsively within that box.
14
+ const SUBPAGE_MIDDLE_DEFAULT_WIDTH = 192;
15
+ const SUBPAGE_MIDDLE_HEIGHT = 32;
16
+ const SUBPAGE_MIDDLE_PADDING_HORIZONTAL = 21;
10
17
  export default function AppBar({
11
18
  type = 'MainPage',
12
19
  leadingSlot,
13
20
  middleSlot,
21
+ middleSlotWidth = SUBPAGE_MIDDLE_DEFAULT_WIDTH,
14
22
  actionsSlot,
15
23
  modes: propModes = EMPTY_MODES,
16
24
  onLeadingPress,
@@ -112,13 +120,40 @@ export default function AppBar({
112
120
  children: cloneChildrenWithModes(React.Children.toArray(actionsSlot), modes)
113
121
  }) : null;
114
122
 
115
- // When there is no middleSlot we want leading & actions pinned to the
116
- // outer edges, so we apply `space-between` at the wrapper. With a middle
117
- // slot present, the middle (flex: 1) absorbs the remaining space, so
118
- // `space-between` is a no-op.
123
+ // SubPage centers its middle slot via absolute positioning (see Figma
124
+ // "slot wrap"), so it never participates in the row flow. Only MainPage
125
+ // keeps the legacy in-flow middle slot.
126
+ const hasInFlowMiddle = isMain && !!processedMiddle;
127
+
128
+ // With an in-flow middle (MainPage) the middle (flex: 1) absorbs the
129
+ // remaining space, so leading & actions sit at the edges naturally. In all
130
+ // other cases we pin leading & actions to the outer edges with
131
+ // `space-between`; the SubPage middle floats above, centered.
119
132
  const wrapperStyle = {
120
133
  ...containerStyle,
121
- justifyContent: processedMiddle ? 'flex-start' : 'space-between'
134
+ justifyContent: hasInFlowMiddle ? 'flex-start' : 'space-between'
135
+ };
136
+
137
+ // Absolutely-centered middle box for SubPage, mirroring the Figma geometry.
138
+ // `left/top: 50%` + a negative translate keeps it centered regardless of the
139
+ // bar width, while the fixed width clips overly-wide content (overflow:
140
+ // hidden) instead of letting it bleed under the leading/actions slots.
141
+ const subPageMiddleStyle = {
142
+ position: 'absolute',
143
+ top: '50%',
144
+ left: '50%',
145
+ width: middleSlotWidth,
146
+ height: SUBPAGE_MIDDLE_HEIGHT,
147
+ transform: [{
148
+ translateX: -middleSlotWidth / 2
149
+ }, {
150
+ translateY: -SUBPAGE_MIDDLE_HEIGHT / 2
151
+ }],
152
+ flexDirection: 'row',
153
+ alignItems: 'center',
154
+ justifyContent: 'center',
155
+ paddingHorizontal: SUBPAGE_MIDDLE_PADDING_HORIZONTAL,
156
+ overflow: 'hidden'
122
157
  };
123
158
  return /*#__PURE__*/_jsxs(View, {
124
159
  style: [wrapperStyle, style],
@@ -134,7 +169,7 @@ export default function AppBar({
134
169
  alignItems: 'center'
135
170
  },
136
171
  children: processedLeading
137
- }), processedMiddle && /*#__PURE__*/_jsx(View, {
172
+ }), hasInFlowMiddle && /*#__PURE__*/_jsx(View, {
138
173
  style: {
139
174
  flex: 1,
140
175
  minWidth: 0,
@@ -147,6 +182,21 @@ export default function AppBar({
147
182
  }), /*#__PURE__*/_jsx(View, {
148
183
  style: actionsStyle,
149
184
  children: processedActions
185
+ }), isSub && processedMiddle && /*#__PURE__*/_jsx(View, {
186
+ style: subPageMiddleStyle,
187
+ pointerEvents: "box-none",
188
+ children: /*#__PURE__*/_jsx(View, {
189
+ style: {
190
+ flex: 1,
191
+ minWidth: 1,
192
+ height: '100%',
193
+ flexDirection: 'row',
194
+ alignItems: 'center',
195
+ justifyContent: 'center'
196
+ },
197
+ pointerEvents: "box-none",
198
+ children: processedMiddle
199
+ })
150
200
  })]
151
201
  });
152
202
  }
@@ -43,9 +43,27 @@ function resolveAnchorFractions(position) {
43
43
  * </Attached>
44
44
  * ```
45
45
  */
46
+ /**
47
+ * Stretches the immediate badge child/children to fill the enforced badge box.
48
+ * Merges `{ width: '100%', height: '100%' }` into each top-level element's
49
+ * `style` so an arbitrary node (e.g. an `Image` with its own width/aspectRatio)
50
+ * fills the fixed `badgeSize` box instead of laying out at its intrinsic size.
51
+ * The wrapping box's `overflow: 'hidden'` clips anything that still overflows.
52
+ */
53
+ function forceBadgeFill(children) {
54
+ return React.Children.map(children, child => {
55
+ if (! /*#__PURE__*/React.isValidElement(child)) return child;
56
+ const childStyle = child.props?.style;
57
+ return /*#__PURE__*/React.cloneElement(child, {
58
+ style: [FILL_STYLE, childStyle]
59
+ });
60
+ });
61
+ }
46
62
  function Attached({
47
63
  children,
48
64
  badge,
65
+ badgeSize,
66
+ badgeRadius,
49
67
  position = 'bottom-right',
50
68
  circular = true,
51
69
  modes: propModes = EMPTY_MODES,
@@ -60,7 +78,7 @@ function Attached({
60
78
  ...propModes
61
79
  }, [globalModes, propModes]);
62
80
  const [mainSize, setMainSize] = useState(ZERO_SIZE);
63
- const [badgeSize, setBadgeSize] = useState(ZERO_SIZE);
81
+ const [measuredBadgeSize, setMeasuredBadgeSize] = useState(ZERO_SIZE);
64
82
  const onMainLayout = useCallback(e => {
65
83
  const {
66
84
  width,
@@ -76,19 +94,31 @@ function Attached({
76
94
  width,
77
95
  height
78
96
  } = e.nativeEvent.layout;
79
- setBadgeSize(prev => prev.width === width && prev.height === height ? prev : {
97
+ setMeasuredBadgeSize(prev => prev.width === width && prev.height === height ? prev : {
80
98
  width,
81
99
  height
82
100
  });
83
101
  }, []);
84
102
  const mainChildren = useMemo(() => children != null ? cloneChildrenWithModes(children, modes) : null, [children, modes]);
85
103
  const badgeChildren = useMemo(() => badge != null ? cloneChildrenWithModes(badge, modes) : null, [badge, modes]);
104
+
105
+ // When a fixed size is requested, the badge is wrapped in a clipped box and
106
+ // its content is force-stretched to fill it (see `forceBadgeFill`).
107
+ const badgeBoxStyle = useMemo(() => {
108
+ if (badgeSize == null) return null;
109
+ return {
110
+ width: badgeSize,
111
+ height: badgeSize,
112
+ borderRadius: badgeRadius ?? badgeSize / 2,
113
+ overflow: 'hidden'
114
+ };
115
+ }, [badgeSize, badgeRadius]);
86
116
  const badgePlacement = useMemo(() => {
87
117
  const {
88
118
  fx,
89
119
  fy
90
120
  } = resolveAnchorFractions(position);
91
- const measured = mainSize.width > 0 && badgeSize.width > 0;
121
+ const measured = mainSize.width > 0 && measuredBadgeSize.width > 0;
92
122
  let anchorX;
93
123
  let anchorY;
94
124
  if (circular) {
@@ -108,12 +138,12 @@ function Attached({
108
138
  }
109
139
  return {
110
140
  position: 'absolute',
111
- left: anchorX - badgeSize.width / 2,
112
- top: anchorY - badgeSize.height / 2,
141
+ left: anchorX - measuredBadgeSize.width / 2,
142
+ top: anchorY - measuredBadgeSize.height / 2,
113
143
  // Hide until both elements are measured to avoid a one-frame flash at (0,0).
114
144
  opacity: measured ? 1 : 0
115
145
  };
116
- }, [position, circular, mainSize, badgeSize]);
146
+ }, [position, circular, mainSize, measuredBadgeSize]);
117
147
  return /*#__PURE__*/_jsxs(View, {
118
148
  style: [styles.container, style],
119
149
  ...rest,
@@ -124,7 +154,10 @@ function Attached({
124
154
  style: badgePlacement,
125
155
  onLayout: onBadgeLayout,
126
156
  pointerEvents: "box-none",
127
- children: badgeChildren
157
+ children: badgeBoxStyle != null ? /*#__PURE__*/_jsx(View, {
158
+ style: badgeBoxStyle,
159
+ children: forceBadgeFill(badgeChildren)
160
+ }) : badgeChildren
128
161
  })]
129
162
  });
130
163
  }
@@ -136,4 +169,10 @@ const styles = {
136
169
  alignSelf: 'flex-start'
137
170
  }
138
171
  };
172
+
173
+ /** Fill style merged into badge content when `badgeSize` enforces a fixed box. */
174
+ const FILL_STYLE = {
175
+ width: '100%',
176
+ height: '100%'
177
+ };
139
178
  export default /*#__PURE__*/React.memo(Attached);
@@ -50,11 +50,25 @@ function useFocusVisible() {
50
50
  /** Minimum touch target per iOS HIG / Material accessibility guidance. */
51
51
  const MIN_TOUCH_TARGET = 44;
52
52
  const touchTargetStyle = {
53
- minWidth: MIN_TOUCH_TARGET,
54
- minHeight: MIN_TOUCH_TARGET,
55
53
  alignItems: 'center',
56
54
  justifyContent: 'center'
57
55
  };
56
+
57
+ /**
58
+ * Expands the tappable region to the 44pt minimum without changing layout.
59
+ * `hitSlop` extends the press-responder bounds beyond the visual box on both
60
+ * native and web (react-native-web ≥ 0.19), so the Pressable keeps its natural
61
+ * checkbox-sized footprint and sibling alignment stays intact.
62
+ */
63
+ function invisibleTouchHitSlop(checkboxSize) {
64
+ const slop = Math.max(0, Math.ceil((MIN_TOUCH_TARGET - checkboxSize) / 2));
65
+ return {
66
+ top: slop,
67
+ bottom: slop,
68
+ left: slop,
69
+ right: slop
70
+ };
71
+ }
58
72
  /**
59
73
  * Checkbox component that maps directly to the Figma design using design tokens.
60
74
  *
@@ -179,8 +193,10 @@ function Checkbox({
179
193
  };
180
194
  };
181
195
  const markColor = disabled && isChecked ? disabledActiveMark : selectedMarkColor;
196
+ const hitSlop = invisibleTouchHitSlop(size);
182
197
  return /*#__PURE__*/_jsx(Pressable, {
183
198
  style: [touchTargetStyle, style],
199
+ hitSlop: hitSlop,
184
200
  onPress: handlePress,
185
201
  disabled: disabled,
186
202
  onHoverIn: () => setIsHovered(true),
@@ -363,7 +363,12 @@ function Drawer({
363
363
  flexDirection: 'column',
364
364
  alignItems: 'stretch'
365
365
  }, contentContainerStyle],
366
- showsVerticalScrollIndicator: showsVerticalScrollIndicator,
366
+ showsVerticalScrollIndicator: showsVerticalScrollIndicator
367
+ // Let a tap on an input inside the sheet focus it on the FIRST tap
368
+ // even while the keyboard is already open (default 'never' would
369
+ // eat that tap just to dismiss the keyboard).
370
+ ,
371
+ keyboardShouldPersistTaps: "handled",
367
372
  animatedProps: animatedScrollProps,
368
373
  alwaysBounceVertical: false,
369
374
  overScrollMode: "always",
@@ -139,7 +139,7 @@ function DropdownInput({
139
139
  supportText,
140
140
  errorMessage,
141
141
  menuMaxHeight = 240,
142
- menuOffset = 6,
142
+ menuOffset,
143
143
  matchTriggerWidth = true,
144
144
  closeOnBackdropPress = true,
145
145
  modes: propModes = EMPTY_MODES,
@@ -202,10 +202,29 @@ function DropdownInput({
202
202
  const tokens = useFormFieldTokens(modes);
203
203
  const chevron = useChevronTokens(modes);
204
204
 
205
+ // Gap between the input and the popup. Falls back to the `formField/gap`
206
+ // token so the menu's offset matches the field's own internal spacing.
207
+ const effectiveMenuOffset = menuOffset ?? tokens.gap;
208
+
205
209
  // ---------------- Layout / measurement ----------------
206
210
  const triggerRef = useRef(null);
207
211
  const [triggerRect, setTriggerRect] = useState(null);
208
212
  const insets = useSafeAreaInsets();
213
+
214
+ // Android coordinate-space bridge.
215
+ //
216
+ // The popup lives inside a `statusBarTranslucent` Modal, whose window is
217
+ // laid out from the PHYSICAL top of the screen (behind the status bar).
218
+ // The trigger, however, is rendered inside the app's content area (Expo
219
+ // Router / react-native-screens under edge-to-edge), so its
220
+ // `measureInWindow` Y is relative to the content area — it does NOT include
221
+ // the status bar height. Feeding that Y straight into the Modal would place
222
+ // the popup one status-bar-height too high, landing it on top of the input.
223
+ //
224
+ // Adding `insets.top` converts the trigger's content-relative Y into the
225
+ // Modal's full-screen coordinate space. iOS/web share a single coordinate
226
+ // space for the Modal and the trigger, so no shift is needed there.
227
+ const windowTopOffset = Platform.OS === 'android' ? insets.top : 0;
209
228
  const measure = useCallback(() => {
210
229
  if (!triggerRef.current) return;
211
230
  triggerRef.current.measureInWindow((x, y, width, height) => {
@@ -271,7 +290,7 @@ function DropdownInput({
271
290
  const spaceBelow = windowHeight - (triggerRect.y + triggerRect.height) - insets.bottom;
272
291
  const spaceAbove = triggerRect.y - insets.top;
273
292
  const desiredHeight = Math.min(menuSize?.height ?? menuMaxHeight, menuMaxHeight);
274
- const needed = desiredHeight + menuOffset + 8;
293
+ const needed = desiredHeight + effectiveMenuOffset + 8;
275
294
  if (placement === 'top') {
276
295
  return spaceAbove >= needed || spaceAbove >= spaceBelow ? 'top' : 'bottom';
277
296
  }
@@ -279,7 +298,7 @@ function DropdownInput({
279
298
  return spaceBelow >= needed || spaceBelow >= spaceAbove ? 'bottom' : 'top';
280
299
  }
281
300
  return spaceBelow >= needed || spaceBelow >= spaceAbove ? 'bottom' : 'top';
282
- }, [triggerRect, placement, windowHeight, menuSize?.height, menuMaxHeight, menuOffset, insets.top, insets.bottom]);
301
+ }, [triggerRect, placement, windowHeight, menuSize?.height, menuMaxHeight, effectiveMenuOffset, insets.top, insets.bottom]);
283
302
  const popupStyle = useMemo(() => {
284
303
  if (!triggerRect) {
285
304
  return {
@@ -298,15 +317,18 @@ function DropdownInput({
298
317
  const minLeft = insets.left + screenPadding;
299
318
  if (leftPos > maxLeft) leftPos = maxLeft;
300
319
  if (leftPos < minLeft) leftPos = minLeft;
320
+
321
+ // Trigger top expressed in the Modal's (full-screen) coordinate space.
322
+ const triggerTop = triggerRect.y + windowTopOffset;
301
323
  let topPos;
302
324
  if (computedPlacement === 'top') {
303
325
  const desiredHeight = menuSize?.height ?? menuMaxHeight;
304
- topPos = triggerRect.y - desiredHeight - menuOffset;
326
+ topPos = triggerTop - desiredHeight - effectiveMenuOffset;
305
327
  if (topPos < insets.top + screenPadding) {
306
328
  topPos = insets.top + screenPadding;
307
329
  }
308
330
  } else {
309
- topPos = triggerRect.y + triggerRect.height + menuOffset;
331
+ topPos = triggerTop + triggerRect.height + effectiveMenuOffset;
310
332
  }
311
333
  const style = {
312
334
  position: 'absolute',
@@ -318,7 +340,7 @@ function DropdownInput({
318
340
  // the wrong place. menuSize becomes truthy after the first layout.
319
341
  if (menuSize == null) style.opacity = 0;
320
342
  return style;
321
- }, [triggerRect, computedPlacement, menuSize, menuOffset, menuMaxHeight, matchTriggerWidth, windowWidth, insets.top, insets.left, insets.right]);
343
+ }, [triggerRect, computedPlacement, menuSize, effectiveMenuOffset, windowTopOffset, menuMaxHeight, matchTriggerWidth, windowWidth, insets.top, insets.left, insets.right]);
322
344
 
323
345
  // Reset menu size when closing so the next open re-measures (handles items
324
346
  // changing while the menu was closed).
@@ -507,6 +529,8 @@ function DropdownInput({
507
529
  }), /*#__PURE__*/_jsx(Modal, {
508
530
  visible: isOpen,
509
531
  transparent: true,
532
+ statusBarTranslucent: true,
533
+ navigationBarTranslucent: true,
510
534
  animationType: "fade",
511
535
  onRequestClose: closeMenu,
512
536
  children: /*#__PURE__*/_jsx(Pressable, {
@@ -82,8 +82,6 @@ function ExpandableCheckbox({
82
82
  }, [disabled, isExpanded, isExpandedControlled, onExpandedChange]);
83
83
  const gap = getVariableByName('expandableCheckbox/gap', modes) ?? 8;
84
84
  const rowGap = getVariableByName('checkboxItem/gap', modes) ?? 8;
85
- const rowPaddingHorizontal = getVariableByName('checkboxItem/padding/horizontal', modes) ?? 0;
86
- const rowPaddingVertical = getVariableByName('checkboxItem/padding/vertical', modes) ?? 0;
87
85
  const labelColor = getVariableByName('checkboxItem/foreground', modes) ?? '#1a1c1f';
88
86
  const labelFontFamily = getVariableByName('checkboxItem/label/fontFamily', modes) ?? 'JioType Var';
89
87
  const labelFontSize = getVariableByName('checkboxItem/label/fontSize', modes) ?? 14;
@@ -104,11 +102,9 @@ function ExpandableCheckbox({
104
102
  alignSelf: isExpanded ? 'stretch' : 'auto',
105
103
  minWidth: 0,
106
104
  flexDirection: 'row',
107
- alignItems: 'flex-start',
108
- gap: rowGap,
109
- paddingHorizontal: rowPaddingHorizontal,
110
- paddingVertical: rowPaddingVertical
111
- }), [isExpanded, rowGap, rowPaddingHorizontal, rowPaddingVertical]);
105
+ alignItems: isExpanded ? 'flex-start' : 'center',
106
+ gap: rowGap
107
+ }), [isExpanded, rowGap]);
112
108
  const resolvedLabelStyle = useMemo(() => ({
113
109
  flex: 1,
114
110
  minWidth: 0,
@@ -116,11 +112,21 @@ function ExpandableCheckbox({
116
112
  fontFamily: labelFontFamily,
117
113
  fontSize: labelFontSize,
118
114
  lineHeight: labelLineHeight,
119
- fontWeight: labelFontWeight
120
- }), [labelColor, labelFontFamily, labelFontSize, labelLineHeight, labelFontWeight]);
115
+ fontWeight: labelFontWeight,
116
+ // Android adds asymmetric font padding and top-aligns the glyph inside
117
+ // an inflated line box when `lineHeight` is set. That makes the centered
118
+ // checkbox look like it drops below the text. Disabling the extra
119
+ // padding + centering the glyph keeps the single-line label optically
120
+ // aligned with the checkbox. No-op on iOS / web.
121
+ includeFontPadding: false,
122
+ textAlignVertical: isExpanded ? 'top' : 'center'
123
+ }), [labelColor, labelFontFamily, labelFontSize, labelLineHeight, labelFontWeight, isExpanded]);
124
+
125
+ // Layer component modes first (e.g. Color Mode), then button defaults so
126
+ // Secondary / XS / Low always win unless a dedicated override prop is added.
121
127
  const buttonModes = useMemo(() => ({
122
- ...BUTTON_DEFAULT_MODES,
123
- ...modes
128
+ ...modes,
129
+ ...BUTTON_DEFAULT_MODES
124
130
  }), [modes]);
125
131
  const a11yLabel = accessibilityLabel ?? (typeof label === 'string' ? label : undefined);
126
132
  const buttonLabel = isExpanded ? readLessLabel : readMoreLabel;
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
- import React, { useCallback, useMemo, useRef, useState } from 'react';
4
- import { View, Text, Pressable, TextInput as RNTextInput } from 'react-native';
3
+ import React, { useCallback, useMemo, useState } from 'react';
4
+ import { View, Text, TextInput as RNTextInput } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import { useTokens } from '../../design-tokens/JFSThemeProvider';
7
7
  import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
@@ -202,16 +202,6 @@ function FormField({
202
202
  const [isFocused, setIsFocused] = useState(false);
203
203
  const interactive = !isDisabled && !isReadOnly;
204
204
 
205
- // Ref to the native input so tapping anywhere in the input row (padding,
206
- // leading/trailing gutters) focuses it on the FIRST tap — fixing the Android
207
- // "two taps to open the keyboard" issue caused by the row intercepting the
208
- // initial touch.
209
- const inputRef = useRef(null);
210
- const focusInput = useCallback(() => {
211
- if (!interactive) return;
212
- inputRef.current?.focus();
213
- }, [interactive]);
214
-
215
205
  // FormField States cascade — error > read only/disabled > active (focused) > idle.
216
206
  // Disabled maps to "Read Only" since there is no dedicated disabled mode and
217
207
  // the visual treatment is closest. This is only the DEFAULT — an explicit
@@ -344,16 +334,13 @@ function FormField({
344
334
  style: requiredIndicatorStyle,
345
335
  children: " *"
346
336
  })]
347
- }), /*#__PURE__*/_jsxs(Pressable, {
337
+ }), /*#__PURE__*/_jsxs(View, {
348
338
  style: [inputRowStyle, inputStyle],
349
- onPress: focusInput,
350
- accessible: false,
351
339
  children: [processedLeading != null && /*#__PURE__*/_jsx(View, {
352
340
  accessibilityElementsHidden: true,
353
341
  importantForAccessibility: "no",
354
342
  children: processedLeading
355
343
  }), /*#__PURE__*/_jsx(RNTextInput, {
356
- ref: inputRef,
357
344
  style: [inputTextStyles, inputTextStyle],
358
345
  value: value ?? '',
359
346
  onChangeText: handleChangeText,
@@ -296,7 +296,11 @@ function FullscreenModal({
296
296
  contentContainerStyle: scrollContentStyle,
297
297
  showsVerticalScrollIndicator: false,
298
298
  onScroll: onScroll,
299
- scrollEventThrottle: 16,
299
+ scrollEventThrottle: 16
300
+ // Tap an input in the body and it focuses on the FIRST tap, even when
301
+ // the keyboard is already open (default 'never' eats that tap).
302
+ ,
303
+ keyboardShouldPersistTaps: "handled",
300
304
  children: [/*#__PURE__*/_jsx(View, {
301
305
  style: heroTextRegionStyle,
302
306
  children: /*#__PURE__*/_jsx(HeroText, {
@@ -3,7 +3,6 @@
3
3
  import React, { useCallback, useMemo, useRef } from 'react';
4
4
  import { View, Text, Pressable, Platform } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
- import IconCapsule from '../IconCapsule/IconCapsule';
7
6
  import NavArrow from '../NavArrow/NavArrow';
8
7
  import { usePressableWebSupport } from '../../utils/web-platform-utils';
9
8
  import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
@@ -144,7 +143,7 @@ const verticalSupportTextOverride = {
144
143
  * @param {string} [props.title='Title'] - Primary title used in the horizontal layout.
145
144
  * @param {string} [props.supportText='Support Text'] - Support text used in both layouts when `supportSlot` is not provided.
146
145
  * @param {boolean} [props.showSupportText=true] - Toggles rendering of the support text in Horizontal layout.
147
- * @param {React.ReactNode} [props.leading] - Optional leading slot. Defaults to `IconCapsule`.
146
+ * @param {React.ReactNode|null} [props.leading] - Optional leading slot. Omitted or `null` renders nothing.
148
147
  * @param {React.ReactNode} [props.supportSlot] - Optional custom slot used instead of the default support text block.
149
148
  * @param {React.ReactNode} [props.trailing] - Optional trailing slot (Figma Slot "trailing"). Horizontal layout only.
150
149
  * @param {boolean} [props.navArrow=true] - Whether to show NavArrow on the far right (Horizontal layout only).
@@ -213,13 +212,9 @@ function ListItemImpl({
213
212
  // Process leading slot to pass modes to children. Memoized on
214
213
  // (leading, resolvedModes) so a parent re-render doesn't re-walk the tree.
215
214
  const leadingElement = useMemo(() => {
216
- const processed = leading ? cloneChildrenWithModes(React.Children.toArray(leading), tokens.resolvedModes, SLOT_FORCED_MODES) : [];
217
- if (processed.length === 0) {
218
- return /*#__PURE__*/_jsx(IconCapsule, {
219
- modes: tokens.resolvedModes,
220
- accessibilityLabel: undefined
221
- });
222
- }
215
+ if (leading == null) return null;
216
+ const processed = cloneChildrenWithModes(React.Children.toArray(leading), tokens.resolvedModes, SLOT_FORCED_MODES);
217
+ if (processed.length === 0) return null;
223
218
  return processed.length === 1 ? processed[0] : processed;
224
219
  }, [leading, tokens.resolvedModes]);
225
220
  const processedSupportSlot = useMemo(() => {
@@ -269,7 +264,7 @@ function ListItemImpl({
269
264
  if (layout === 'Horizontal') {
270
265
  const innerContent = /*#__PURE__*/_jsxs(View, {
271
266
  style: innerContentStyleArray,
272
- children: [leadingElement, /*#__PURE__*/_jsxs(View, {
267
+ children: [leadingElement ?? null, /*#__PURE__*/_jsxs(View, {
273
268
  style: {
274
269
  flex: 1,
275
270
  minWidth: 1,
@@ -316,7 +311,7 @@ function ListItemImpl({
316
311
  // Vertical layout — icon on top, support text/slot below
317
312
  const verticalContent = /*#__PURE__*/_jsxs(View, {
318
313
  style: verticalContentStyleArray,
319
- children: [leadingElement, renderSupportContent()]
314
+ children: [leadingElement ?? null, renderSupportContent()]
320
315
  });
321
316
  if (onPress) {
322
317
  return /*#__PURE__*/_jsx(Pressable, {
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
- import React, { useCallback, useMemo, useRef, useState } from 'react';
4
- import { View, Text, Pressable, TextInput as RNTextInput } from 'react-native';
3
+ import React, { useCallback, useMemo, useState } from 'react';
4
+ import { View, Text, TextInput as RNTextInput } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import { useTokens } from '../../design-tokens/JFSThemeProvider';
7
7
  import { EMPTY_MODES } from '../../utils/react-utils';
@@ -140,15 +140,6 @@ function MessageField({
140
140
  const currentValue = isControlled ? value : uncontrolledValue;
141
141
  const [isFocused, setIsFocused] = useState(false);
142
142
  const interactive = !isDisabled && !isReadOnly;
143
-
144
- // Ref to the native textarea so tapping anywhere in the (padded) textarea
145
- // container focuses it on the FIRST tap, fixing the Android "two taps to
146
- // open the keyboard" issue.
147
- const inputRef = useRef(null);
148
- const focusInput = useCallback(() => {
149
- if (!interactive) return;
150
- inputRef.current?.focus();
151
- }, [interactive]);
152
143
  const {
153
144
  modes: globalModes
154
145
  } = useTokens();
@@ -283,12 +274,9 @@ function MessageField({
283
274
  style: requiredIndicatorStyle,
284
275
  children: " *"
285
276
  })]
286
- }), /*#__PURE__*/_jsx(Pressable, {
277
+ }), /*#__PURE__*/_jsx(View, {
287
278
  style: [textareaContainerStyle, textareaStyle],
288
- onPress: focusInput,
289
- accessible: false,
290
279
  children: /*#__PURE__*/_jsx(RNTextInput, {
291
- ref: inputRef,
292
280
  multiline: true,
293
281
  value: currentValue,
294
282
  onChangeText: handleChangeText,
@@ -1,16 +1,16 @@
1
1
  "use strict";
2
2
 
3
- import React, { isValidElement, cloneElement } from 'react';
3
+ import React from 'react';
4
4
  import { View, Text } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import { useTokens } from '../../design-tokens/JFSThemeProvider';
7
7
  import IconCapsule from '../IconCapsule/IconCapsule';
8
- import { EMPTY_MODES } from '../../utils/react-utils';
8
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
9
9
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
10
  export default function PaymentFeedback({
11
11
  title = '₹50,000',
12
12
  subtitle = 'Payment successful',
13
- body = 'Your payment has been\nsuccessfully processed.',
13
+ body,
14
14
  details = '18 March 2025, 4:15 pm\nTransaction ID: TXN121466784',
15
15
  showDetails = true,
16
16
  iconName = 'ic_confirm',
@@ -97,17 +97,21 @@ export default function PaymentFeedback({
97
97
  fontWeight: String(detailsFontWeight),
98
98
  textAlign: 'center'
99
99
  };
100
- const mediaContent = /*#__PURE__*/isValidElement(renderMedia) ? /*#__PURE__*/cloneElement(renderMedia, {
101
- modes
102
- }) : renderMedia;
100
+
101
+ // Cascade modes into a custom media slot (per the modes-cascade convention);
102
+ // any modes the consumer set on the slot child still take precedence.
103
+ const mediaContent = renderMedia != null ? cloneChildrenWithModes(renderMedia, modes) : null;
103
104
  const defaultMedia = /*#__PURE__*/_jsx(IconCapsule, {
104
- iconName: iconName,
105
+ iconName: iconName
106
+ // `positive` is the default; consumers override the capsule color by
107
+ // passing `AppearanceSystem` (or any other mode) via the `modes` prop.
108
+ ,
105
109
  modes: {
110
+ AppearanceSystem: 'positive',
106
111
  ...modes,
107
112
  'Icon Capsule Size': 'L',
108
113
  Emphasis: 'High',
109
- 'Semantic Intent': 'System',
110
- AppearanceSystem: 'positive'
114
+ 'Semantic Intent': 'System'
111
115
  }
112
116
  });
113
117
  const detailLines = details?.split('\n') ?? [];