jfs-components 0.0.78 → 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 (119) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/lib/commonjs/components/AppBar/AppBar.js +56 -6
  3. package/lib/commonjs/components/Attached/Attached.js +183 -0
  4. package/lib/commonjs/components/Card/Card.js +25 -2
  5. package/lib/commonjs/components/Checkbox/Checkbox.js +18 -2
  6. package/lib/commonjs/components/Drawer/Drawer.js +6 -1
  7. package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -6
  8. package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +17 -11
  9. package/lib/commonjs/components/FormField/FormField.js +1 -14
  10. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +9 -7
  11. package/lib/commonjs/components/ListItem/ListItem.js +26 -24
  12. package/lib/commonjs/components/MessageField/MessageField.js +1 -13
  13. package/lib/commonjs/components/PaymentFeedback/PaymentFeedback.js +12 -9
  14. package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +237 -0
  15. package/lib/commonjs/components/Slot/Slot.js +73 -0
  16. package/lib/commonjs/components/Spinner/Spinner.js +217 -0
  17. package/lib/commonjs/components/TextInput/TextInput.js +33 -18
  18. package/lib/commonjs/components/index.js +28 -0
  19. package/lib/commonjs/icons/components/IconArrowdown.js +19 -0
  20. package/lib/commonjs/icons/components/IconArrowup.js +19 -0
  21. package/lib/commonjs/icons/components/IconChevrondowncircle.js +19 -0
  22. package/lib/commonjs/icons/components/IconChevronleftcircle.js +19 -0
  23. package/lib/commonjs/icons/components/IconChevronrightcircle.js +19 -0
  24. package/lib/commonjs/icons/components/IconChevronupcircle.js +19 -0
  25. package/lib/commonjs/icons/components/IconOsnavback.js +19 -0
  26. package/lib/commonjs/icons/components/IconOsnavcenter.js +19 -0
  27. package/lib/commonjs/icons/components/IconOsnavhome.js +19 -0
  28. package/lib/commonjs/icons/components/IconOsnavtask.js +19 -0
  29. package/lib/commonjs/icons/components/IconSignin.js +19 -0
  30. package/lib/commonjs/icons/components/IconSignout.js +19 -0
  31. package/lib/commonjs/icons/components/index.js +132 -0
  32. package/lib/commonjs/icons/registry.js +2 -2
  33. package/lib/module/components/AppBar/AppBar.js +56 -6
  34. package/lib/module/components/Attached/Attached.js +178 -0
  35. package/lib/module/components/Card/Card.js +25 -2
  36. package/lib/module/components/Checkbox/Checkbox.js +18 -2
  37. package/lib/module/components/Drawer/Drawer.js +6 -1
  38. package/lib/module/components/DropdownInput/DropdownInput.js +30 -6
  39. package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +17 -11
  40. package/lib/module/components/FormField/FormField.js +3 -16
  41. package/lib/module/components/FullscreenModal/FullscreenModal.js +9 -7
  42. package/lib/module/components/ListItem/ListItem.js +26 -24
  43. package/lib/module/components/MessageField/MessageField.js +3 -15
  44. package/lib/module/components/PaymentFeedback/PaymentFeedback.js +13 -9
  45. package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +234 -0
  46. package/lib/module/components/Slot/Slot.js +68 -0
  47. package/lib/module/components/Spinner/Spinner.js +212 -0
  48. package/lib/module/components/TextInput/TextInput.js +34 -19
  49. package/lib/module/components/index.js +4 -0
  50. package/lib/module/icons/components/IconArrowdown.js +12 -0
  51. package/lib/module/icons/components/IconArrowup.js +12 -0
  52. package/lib/module/icons/components/IconChevrondowncircle.js +12 -0
  53. package/lib/module/icons/components/IconChevronleftcircle.js +12 -0
  54. package/lib/module/icons/components/IconChevronrightcircle.js +12 -0
  55. package/lib/module/icons/components/IconChevronupcircle.js +12 -0
  56. package/lib/module/icons/components/IconOsnavback.js +12 -0
  57. package/lib/module/icons/components/IconOsnavcenter.js +12 -0
  58. package/lib/module/icons/components/IconOsnavhome.js +12 -0
  59. package/lib/module/icons/components/IconOsnavtask.js +12 -0
  60. package/lib/module/icons/components/IconSignin.js +12 -0
  61. package/lib/module/icons/components/IconSignout.js +12 -0
  62. package/lib/module/icons/components/index.js +12 -0
  63. package/lib/module/icons/registry.js +2 -2
  64. package/lib/typescript/src/components/AppBar/AppBar.d.ts +12 -1
  65. package/lib/typescript/src/components/Attached/Attached.d.ts +64 -0
  66. package/lib/typescript/src/components/Card/Card.d.ts +9 -2
  67. package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +3 -2
  68. package/lib/typescript/src/components/ListItem/ListItem.d.ts +16 -6
  69. package/lib/typescript/src/components/PaymentFeedback/PaymentFeedback.d.ts +5 -1
  70. package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +66 -0
  71. package/lib/typescript/src/components/Slot/Slot.d.ts +52 -0
  72. package/lib/typescript/src/components/Spinner/Spinner.d.ts +45 -0
  73. package/lib/typescript/src/components/index.d.ts +4 -0
  74. package/lib/typescript/src/icons/components/IconArrowdown.d.ts +3 -0
  75. package/lib/typescript/src/icons/components/IconArrowup.d.ts +3 -0
  76. package/lib/typescript/src/icons/components/IconChevrondowncircle.d.ts +3 -0
  77. package/lib/typescript/src/icons/components/IconChevronleftcircle.d.ts +3 -0
  78. package/lib/typescript/src/icons/components/IconChevronrightcircle.d.ts +3 -0
  79. package/lib/typescript/src/icons/components/IconChevronupcircle.d.ts +3 -0
  80. package/lib/typescript/src/icons/components/IconOsnavback.d.ts +3 -0
  81. package/lib/typescript/src/icons/components/IconOsnavcenter.d.ts +3 -0
  82. package/lib/typescript/src/icons/components/IconOsnavhome.d.ts +3 -0
  83. package/lib/typescript/src/icons/components/IconOsnavtask.d.ts +3 -0
  84. package/lib/typescript/src/icons/components/IconSignin.d.ts +3 -0
  85. package/lib/typescript/src/icons/components/IconSignout.d.ts +3 -0
  86. package/lib/typescript/src/icons/components/index.d.ts +12 -0
  87. package/lib/typescript/src/icons/registry.d.ts +1 -1
  88. package/package.json +3 -2
  89. package/src/components/AppBar/AppBar.tsx +79 -12
  90. package/src/components/Attached/Attached.tsx +237 -0
  91. package/src/components/Card/Card.tsx +28 -1
  92. package/src/components/Checkbox/Checkbox.tsx +14 -2
  93. package/src/components/Drawer/Drawer.tsx +4 -0
  94. package/src/components/DropdownInput/DropdownInput.tsx +54 -20
  95. package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +13 -9
  96. package/src/components/FormField/FormField.tsx +3 -19
  97. package/src/components/FullscreenModal/FullscreenModal.tsx +6 -3
  98. package/src/components/ListItem/ListItem.tsx +42 -25
  99. package/src/components/MessageField/MessageField.tsx +3 -18
  100. package/src/components/PaymentFeedback/PaymentFeedback.tsx +15 -8
  101. package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +316 -0
  102. package/src/components/Slot/Slot.tsx +91 -0
  103. package/src/components/Spinner/Spinner.tsx +273 -0
  104. package/src/components/TextInput/TextInput.tsx +37 -19
  105. package/src/components/index.ts +4 -0
  106. package/src/icons/components/IconArrowdown.tsx +11 -0
  107. package/src/icons/components/IconArrowup.tsx +11 -0
  108. package/src/icons/components/IconChevrondowncircle.tsx +11 -0
  109. package/src/icons/components/IconChevronleftcircle.tsx +11 -0
  110. package/src/icons/components/IconChevronrightcircle.tsx +11 -0
  111. package/src/icons/components/IconChevronupcircle.tsx +11 -0
  112. package/src/icons/components/IconOsnavback.tsx +11 -0
  113. package/src/icons/components/IconOsnavcenter.tsx +11 -0
  114. package/src/icons/components/IconOsnavhome.tsx +11 -0
  115. package/src/icons/components/IconOsnavtask.tsx +11 -0
  116. package/src/icons/components/IconSignin.tsx +11 -0
  117. package/src/icons/components/IconSignout.tsx +11 -0
  118. package/src/icons/components/index.ts +12 -0
  119. 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
  }
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+
3
+ import React, { useCallback, useMemo, useState } from 'react';
4
+ import { View } from 'react-native';
5
+ import { useTokens } from '../../design-tokens/JFSThemeProvider';
6
+ import { cloneChildrenWithModes, EMPTY_MODES } from '../../utils/react-utils';
7
+
8
+ /**
9
+ * Anchor point on the main content where the attached `badge` is centered.
10
+ * Mirrors the nine Figma `position` variants (corners, edge midpoints, center).
11
+ */
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ const ZERO_SIZE = {
14
+ width: 0,
15
+ height: 0
16
+ };
17
+
18
+ /**
19
+ * Fraction (0 | 0.5 | 1) of the main content's width/height at which the badge
20
+ * center should sit, derived from the `position` anchor.
21
+ */
22
+ function resolveAnchorFractions(position) {
23
+ const fx = position.includes('left') ? 0 : position.includes('right') ? 1 : 0.5;
24
+ const fy = position.startsWith('top') ? 0 : position.startsWith('bottom') ? 1 : 0.5;
25
+ return {
26
+ fx,
27
+ fy
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Attached — overlays a small `badge` on top of arbitrary main content,
33
+ * centered on one of nine anchor points (corners, edge midpoints, or center).
34
+ *
35
+ * The badge straddles the chosen anchor regardless of either element's size:
36
+ * both the main content and the badge are measured via `onLayout`, then the
37
+ * badge is absolutely positioned so its center lands exactly on the anchor.
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * <Attached position="bottom-right" badge={<InstitutionBadge modes={modes} />} modes={modes}>
42
+ * <IconCapsule iconName="ic_card" modes={modes} />
43
+ * </Attached>
44
+ * ```
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
+ }
62
+ function Attached({
63
+ children,
64
+ badge,
65
+ badgeSize,
66
+ badgeRadius,
67
+ position = 'bottom-right',
68
+ circular = true,
69
+ modes: propModes = EMPTY_MODES,
70
+ style,
71
+ ...rest
72
+ }) {
73
+ const {
74
+ modes: globalModes
75
+ } = useTokens();
76
+ const modes = useMemo(() => globalModes === EMPTY_MODES && propModes === EMPTY_MODES ? EMPTY_MODES : {
77
+ ...globalModes,
78
+ ...propModes
79
+ }, [globalModes, propModes]);
80
+ const [mainSize, setMainSize] = useState(ZERO_SIZE);
81
+ const [measuredBadgeSize, setMeasuredBadgeSize] = useState(ZERO_SIZE);
82
+ const onMainLayout = useCallback(e => {
83
+ const {
84
+ width,
85
+ height
86
+ } = e.nativeEvent.layout;
87
+ setMainSize(prev => prev.width === width && prev.height === height ? prev : {
88
+ width,
89
+ height
90
+ });
91
+ }, []);
92
+ const onBadgeLayout = useCallback(e => {
93
+ const {
94
+ width,
95
+ height
96
+ } = e.nativeEvent.layout;
97
+ setMeasuredBadgeSize(prev => prev.width === width && prev.height === height ? prev : {
98
+ width,
99
+ height
100
+ });
101
+ }, []);
102
+ const mainChildren = useMemo(() => children != null ? cloneChildrenWithModes(children, modes) : null, [children, modes]);
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]);
116
+ const badgePlacement = useMemo(() => {
117
+ const {
118
+ fx,
119
+ fy
120
+ } = resolveAnchorFractions(position);
121
+ const measured = mainSize.width > 0 && measuredBadgeSize.width > 0;
122
+ let anchorX;
123
+ let anchorY;
124
+ if (circular) {
125
+ // Project the anchor onto the circle inscribed in the bounding box, so
126
+ // corner badges land on the circumference (45°) instead of the box corner.
127
+ const cx = mainSize.width / 2;
128
+ const cy = mainSize.height / 2;
129
+ const radius = Math.min(mainSize.width, mainSize.height) / 2;
130
+ const dx = (fx - 0.5) * 2; // -1 | 0 | 1
131
+ const dy = (fy - 0.5) * 2; // -1 | 0 | 1
132
+ const len = Math.hypot(dx, dy) || 1; // 'center' → 0, guard against /0
133
+ anchorX = cx + dx / len * radius;
134
+ anchorY = cy + dy / len * radius;
135
+ } else {
136
+ anchorX = mainSize.width * fx;
137
+ anchorY = mainSize.height * fy;
138
+ }
139
+ return {
140
+ position: 'absolute',
141
+ left: anchorX - measuredBadgeSize.width / 2,
142
+ top: anchorY - measuredBadgeSize.height / 2,
143
+ // Hide until both elements are measured to avoid a one-frame flash at (0,0).
144
+ opacity: measured ? 1 : 0
145
+ };
146
+ }, [position, circular, mainSize, measuredBadgeSize]);
147
+ return /*#__PURE__*/_jsxs(View, {
148
+ style: [styles.container, style],
149
+ ...rest,
150
+ children: [/*#__PURE__*/_jsx(View, {
151
+ onLayout: onMainLayout,
152
+ children: mainChildren
153
+ }), badgeChildren != null && /*#__PURE__*/_jsx(View, {
154
+ style: badgePlacement,
155
+ onLayout: onBadgeLayout,
156
+ pointerEvents: "box-none",
157
+ children: badgeBoxStyle != null ? /*#__PURE__*/_jsx(View, {
158
+ style: badgeBoxStyle,
159
+ children: forceBadgeFill(badgeChildren)
160
+ }) : badgeChildren
161
+ })]
162
+ });
163
+ }
164
+ const styles = {
165
+ // alignSelf flex-start so the wrapper hugs the main content; anchors are then
166
+ // computed relative to the content size rather than a stretched parent.
167
+ container: {
168
+ position: 'relative',
169
+ alignSelf: 'flex-start'
170
+ }
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
+ };
178
+ export default /*#__PURE__*/React.memo(Attached);
@@ -16,9 +16,11 @@ const CardContext = /*#__PURE__*/createContext({});
16
16
  * Card component implementation from Figma node 765:6186.
17
17
  *
18
18
  * Supports a `media` slot (with aspect ratio) and a content area.
19
+ * Supports an optional `header` slot (e.g. a brand logo), a `media` slot
20
+ * (with aspect ratio) and a content area.
19
21
  * Usage:
20
22
  * ```tsx
21
- * <Card media={<Image source={...} />} modes={modes}>
23
+ * <Card header={<GoldLogo />} media={<Image source={...} />} modes={modes}>
22
24
  * <Card.SupportText>Support text</Card.SupportText>
23
25
  * <Card.Title>Title</Card.Title>
24
26
  * <Card.SupportText>Support text</Card.SupportText>
@@ -26,6 +28,7 @@ const CardContext = /*#__PURE__*/createContext({});
26
28
  * ```
27
29
  */
28
30
  export function Card({
31
+ header,
29
32
  media,
30
33
  children,
31
34
  modes = EMPTY_MODES,
@@ -53,6 +56,14 @@ export function Card({
53
56
  ...modes
54
57
  }
55
58
  }) : media;
59
+
60
+ // Clone header to pass modes if it's a valid element
61
+ const headerWithModes = /*#__PURE__*/isValidElement(header) ? /*#__PURE__*/cloneElement(header, {
62
+ modes: {
63
+ ...header.props.modes,
64
+ ...modes
65
+ }
66
+ }) : header;
56
67
  const containerStyle = {
57
68
  backgroundColor,
58
69
  borderColor,
@@ -63,6 +74,15 @@ export function Card({
63
74
  paddingVertical,
64
75
  overflow: 'hidden' // Ensure border radius clips content
65
76
  };
77
+
78
+ // Header wrap uses fixed padding from Figma (no dedicated tokens defined).
79
+ const headerWrapperStyle = {
80
+ width: '100%',
81
+ flexDirection: 'row',
82
+ alignItems: 'flex-start',
83
+ paddingHorizontal: 12,
84
+ paddingVertical: 16
85
+ };
66
86
  const mediaWrapperStyle = {
67
87
  width: '100%',
68
88
  aspectRatio: mediaAspectRatio,
@@ -83,7 +103,10 @@ export function Card({
83
103
  },
84
104
  children: /*#__PURE__*/_jsxs(View, {
85
105
  style: [containerStyle, style],
86
- children: [media && /*#__PURE__*/_jsx(View, {
106
+ children: [header && /*#__PURE__*/_jsx(View, {
107
+ style: headerWrapperStyle,
108
+ children: headerWithModes
109
+ }), media && /*#__PURE__*/_jsx(View, {
87
110
  style: mediaWrapperStyle,
88
111
  children: mediaWithModes
89
112
  }), /*#__PURE__*/_jsx(View, {
@@ -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,
@@ -10,6 +10,7 @@ import Button from '../Button/Button';
10
10
  import Disclaimer from '../Disclaimer/Disclaimer';
11
11
  import IconButton from '../IconButton/IconButton';
12
12
  import ActionFooter from '../ActionFooter/ActionFooter';
13
+ import Slot from '../Slot/Slot';
13
14
 
14
15
  // ---------------------------------------------------------------------------
15
16
  // Forced modes
@@ -269,8 +270,9 @@ function FullscreenModal({
269
270
  if (footer) {
270
271
  footerContent = footer;
271
272
  } else if (primaryActionLabel) {
272
- footerContent = /*#__PURE__*/_jsxs(View, {
273
- style: footerColumnStyle,
273
+ footerContent = /*#__PURE__*/_jsxs(Slot, {
274
+ layoutDirection: "vertical",
275
+ modes: modes,
274
276
  children: [/*#__PURE__*/_jsx(Button, {
275
277
  label: primaryActionLabel,
276
278
  modes: modes,
@@ -294,7 +296,11 @@ function FullscreenModal({
294
296
  contentContainerStyle: scrollContentStyle,
295
297
  showsVerticalScrollIndicator: false,
296
298
  onScroll: onScroll,
297
- 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",
298
304
  children: [/*#__PURE__*/_jsx(View, {
299
305
  style: heroTextRegionStyle,
300
306
  children: /*#__PURE__*/_jsx(HeroText, {
@@ -335,10 +341,6 @@ const scrollViewStyle = {
335
341
  const scrollContentStyle = {
336
342
  flexGrow: 1
337
343
  };
338
- const footerColumnStyle = {
339
- width: '100%',
340
- gap: 8
341
- };
342
344
  const fullWidthStyle = {
343
345
  width: '100%'
344
346
  };