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
package/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [0.0.79] - 2026-05-30
8
+
9
+ - Added `Attached` — positions a badge over main content at nine anchor points (corners, edges, center).
10
+ - Added `PlanComparisonCard` — plan comparison table with column headers, feature rows, brand highlight, and cell values (text / cross / custom node).
11
+ - Added `Slot` — token-driven vertical/horizontal layout container with `modes` cascade to children.
12
+ - `Card` — new `header` slot above media (e.g. brand logo).
13
+ - `ListItem` — new `trailing` slot; `endSlot` deprecated (still honored).
14
+ - `FullscreenModal` — footer action stack now uses `Slot` instead of a raw `View`.
15
+
16
+ ---
17
+
7
18
  ## [0.0.78] - 2026-05-29
8
19
 
9
20
  - Added `ExpandableCheckbox` — checkbox row with collapsible long label and Read more / Read less toggle.
@@ -12,10 +12,18 @@ var _NavArrow = _interopRequireDefault(require("../NavArrow/NavArrow"));
12
12
  var _reactUtils = require("../../utils/react-utils");
13
13
  var _jsxRuntime = require("react/jsx-runtime");
14
14
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
15
+ // SubPage "slot wrap" geometry, taken directly from the Figma design
16
+ // (node 449:7876). The middle slot is an absolutely-centered box of a fixed
17
+ // size; its inner content (node 3991:4125) is a `flex: 1 0 0; min-width: 1px`
18
+ // item so it fills / shrinks responsively within that box.
19
+ const SUBPAGE_MIDDLE_DEFAULT_WIDTH = 192;
20
+ const SUBPAGE_MIDDLE_HEIGHT = 32;
21
+ const SUBPAGE_MIDDLE_PADDING_HORIZONTAL = 21;
15
22
  function AppBar({
16
23
  type = 'MainPage',
17
24
  leadingSlot,
18
25
  middleSlot,
26
+ middleSlotWidth = SUBPAGE_MIDDLE_DEFAULT_WIDTH,
19
27
  actionsSlot,
20
28
  modes: propModes = _reactUtils.EMPTY_MODES,
21
29
  onLeadingPress,
@@ -117,13 +125,40 @@ function AppBar({
117
125
  children: (0, _reactUtils.cloneChildrenWithModes)(_react.default.Children.toArray(actionsSlot), modes)
118
126
  }) : null;
119
127
 
120
- // When there is no middleSlot we want leading & actions pinned to the
121
- // outer edges, so we apply `space-between` at the wrapper. With a middle
122
- // slot present, the middle (flex: 1) absorbs the remaining space, so
123
- // `space-between` is a no-op.
128
+ // SubPage centers its middle slot via absolute positioning (see Figma
129
+ // "slot wrap"), so it never participates in the row flow. Only MainPage
130
+ // keeps the legacy in-flow middle slot.
131
+ const hasInFlowMiddle = isMain && !!processedMiddle;
132
+
133
+ // With an in-flow middle (MainPage) the middle (flex: 1) absorbs the
134
+ // remaining space, so leading & actions sit at the edges naturally. In all
135
+ // other cases we pin leading & actions to the outer edges with
136
+ // `space-between`; the SubPage middle floats above, centered.
124
137
  const wrapperStyle = {
125
138
  ...containerStyle,
126
- justifyContent: processedMiddle ? 'flex-start' : 'space-between'
139
+ justifyContent: hasInFlowMiddle ? 'flex-start' : 'space-between'
140
+ };
141
+
142
+ // Absolutely-centered middle box for SubPage, mirroring the Figma geometry.
143
+ // `left/top: 50%` + a negative translate keeps it centered regardless of the
144
+ // bar width, while the fixed width clips overly-wide content (overflow:
145
+ // hidden) instead of letting it bleed under the leading/actions slots.
146
+ const subPageMiddleStyle = {
147
+ position: 'absolute',
148
+ top: '50%',
149
+ left: '50%',
150
+ width: middleSlotWidth,
151
+ height: SUBPAGE_MIDDLE_HEIGHT,
152
+ transform: [{
153
+ translateX: -middleSlotWidth / 2
154
+ }, {
155
+ translateY: -SUBPAGE_MIDDLE_HEIGHT / 2
156
+ }],
157
+ flexDirection: 'row',
158
+ alignItems: 'center',
159
+ justifyContent: 'center',
160
+ paddingHorizontal: SUBPAGE_MIDDLE_PADDING_HORIZONTAL,
161
+ overflow: 'hidden'
127
162
  };
128
163
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
129
164
  style: [wrapperStyle, style],
@@ -139,7 +174,7 @@ function AppBar({
139
174
  alignItems: 'center'
140
175
  },
141
176
  children: processedLeading
142
- }), processedMiddle && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
177
+ }), hasInFlowMiddle && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
143
178
  style: {
144
179
  flex: 1,
145
180
  minWidth: 0,
@@ -152,6 +187,21 @@ function AppBar({
152
187
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
153
188
  style: actionsStyle,
154
189
  children: processedActions
190
+ }), isSub && processedMiddle && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
191
+ style: subPageMiddleStyle,
192
+ pointerEvents: "box-none",
193
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
194
+ style: {
195
+ flex: 1,
196
+ minWidth: 1,
197
+ height: '100%',
198
+ flexDirection: 'row',
199
+ alignItems: 'center',
200
+ justifyContent: 'center'
201
+ },
202
+ pointerEvents: "box-none",
203
+ children: processedMiddle
204
+ })
155
205
  })]
156
206
  });
157
207
  }
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _react = _interopRequireWildcard(require("react"));
8
+ var _reactNative = require("react-native");
9
+ var _JFSThemeProvider = require("../../design-tokens/JFSThemeProvider");
10
+ var _reactUtils = require("../../utils/react-utils");
11
+ var _jsxRuntime = require("react/jsx-runtime");
12
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
13
+ /**
14
+ * Anchor point on the main content where the attached `badge` is centered.
15
+ * Mirrors the nine Figma `position` variants (corners, edge midpoints, center).
16
+ */
17
+
18
+ const ZERO_SIZE = {
19
+ width: 0,
20
+ height: 0
21
+ };
22
+
23
+ /**
24
+ * Fraction (0 | 0.5 | 1) of the main content's width/height at which the badge
25
+ * center should sit, derived from the `position` anchor.
26
+ */
27
+ function resolveAnchorFractions(position) {
28
+ const fx = position.includes('left') ? 0 : position.includes('right') ? 1 : 0.5;
29
+ const fy = position.startsWith('top') ? 0 : position.startsWith('bottom') ? 1 : 0.5;
30
+ return {
31
+ fx,
32
+ fy
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Attached — overlays a small `badge` on top of arbitrary main content,
38
+ * centered on one of nine anchor points (corners, edge midpoints, or center).
39
+ *
40
+ * The badge straddles the chosen anchor regardless of either element's size:
41
+ * both the main content and the badge are measured via `onLayout`, then the
42
+ * badge is absolutely positioned so its center lands exactly on the anchor.
43
+ *
44
+ * @example
45
+ * ```tsx
46
+ * <Attached position="bottom-right" badge={<InstitutionBadge modes={modes} />} modes={modes}>
47
+ * <IconCapsule iconName="ic_card" modes={modes} />
48
+ * </Attached>
49
+ * ```
50
+ */
51
+ /**
52
+ * Stretches the immediate badge child/children to fill the enforced badge box.
53
+ * Merges `{ width: '100%', height: '100%' }` into each top-level element's
54
+ * `style` so an arbitrary node (e.g. an `Image` with its own width/aspectRatio)
55
+ * fills the fixed `badgeSize` box instead of laying out at its intrinsic size.
56
+ * The wrapping box's `overflow: 'hidden'` clips anything that still overflows.
57
+ */
58
+ function forceBadgeFill(children) {
59
+ return _react.default.Children.map(children, child => {
60
+ if (! /*#__PURE__*/_react.default.isValidElement(child)) return child;
61
+ const childStyle = child.props?.style;
62
+ return /*#__PURE__*/_react.default.cloneElement(child, {
63
+ style: [FILL_STYLE, childStyle]
64
+ });
65
+ });
66
+ }
67
+ function Attached({
68
+ children,
69
+ badge,
70
+ badgeSize,
71
+ badgeRadius,
72
+ position = 'bottom-right',
73
+ circular = true,
74
+ modes: propModes = _reactUtils.EMPTY_MODES,
75
+ style,
76
+ ...rest
77
+ }) {
78
+ const {
79
+ modes: globalModes
80
+ } = (0, _JFSThemeProvider.useTokens)();
81
+ const modes = (0, _react.useMemo)(() => globalModes === _reactUtils.EMPTY_MODES && propModes === _reactUtils.EMPTY_MODES ? _reactUtils.EMPTY_MODES : {
82
+ ...globalModes,
83
+ ...propModes
84
+ }, [globalModes, propModes]);
85
+ const [mainSize, setMainSize] = (0, _react.useState)(ZERO_SIZE);
86
+ const [measuredBadgeSize, setMeasuredBadgeSize] = (0, _react.useState)(ZERO_SIZE);
87
+ const onMainLayout = (0, _react.useCallback)(e => {
88
+ const {
89
+ width,
90
+ height
91
+ } = e.nativeEvent.layout;
92
+ setMainSize(prev => prev.width === width && prev.height === height ? prev : {
93
+ width,
94
+ height
95
+ });
96
+ }, []);
97
+ const onBadgeLayout = (0, _react.useCallback)(e => {
98
+ const {
99
+ width,
100
+ height
101
+ } = e.nativeEvent.layout;
102
+ setMeasuredBadgeSize(prev => prev.width === width && prev.height === height ? prev : {
103
+ width,
104
+ height
105
+ });
106
+ }, []);
107
+ const mainChildren = (0, _react.useMemo)(() => children != null ? (0, _reactUtils.cloneChildrenWithModes)(children, modes) : null, [children, modes]);
108
+ const badgeChildren = (0, _react.useMemo)(() => badge != null ? (0, _reactUtils.cloneChildrenWithModes)(badge, modes) : null, [badge, modes]);
109
+
110
+ // When a fixed size is requested, the badge is wrapped in a clipped box and
111
+ // its content is force-stretched to fill it (see `forceBadgeFill`).
112
+ const badgeBoxStyle = (0, _react.useMemo)(() => {
113
+ if (badgeSize == null) return null;
114
+ return {
115
+ width: badgeSize,
116
+ height: badgeSize,
117
+ borderRadius: badgeRadius ?? badgeSize / 2,
118
+ overflow: 'hidden'
119
+ };
120
+ }, [badgeSize, badgeRadius]);
121
+ const badgePlacement = (0, _react.useMemo)(() => {
122
+ const {
123
+ fx,
124
+ fy
125
+ } = resolveAnchorFractions(position);
126
+ const measured = mainSize.width > 0 && measuredBadgeSize.width > 0;
127
+ let anchorX;
128
+ let anchorY;
129
+ if (circular) {
130
+ // Project the anchor onto the circle inscribed in the bounding box, so
131
+ // corner badges land on the circumference (45°) instead of the box corner.
132
+ const cx = mainSize.width / 2;
133
+ const cy = mainSize.height / 2;
134
+ const radius = Math.min(mainSize.width, mainSize.height) / 2;
135
+ const dx = (fx - 0.5) * 2; // -1 | 0 | 1
136
+ const dy = (fy - 0.5) * 2; // -1 | 0 | 1
137
+ const len = Math.hypot(dx, dy) || 1; // 'center' → 0, guard against /0
138
+ anchorX = cx + dx / len * radius;
139
+ anchorY = cy + dy / len * radius;
140
+ } else {
141
+ anchorX = mainSize.width * fx;
142
+ anchorY = mainSize.height * fy;
143
+ }
144
+ return {
145
+ position: 'absolute',
146
+ left: anchorX - measuredBadgeSize.width / 2,
147
+ top: anchorY - measuredBadgeSize.height / 2,
148
+ // Hide until both elements are measured to avoid a one-frame flash at (0,0).
149
+ opacity: measured ? 1 : 0
150
+ };
151
+ }, [position, circular, mainSize, measuredBadgeSize]);
152
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
153
+ style: [styles.container, style],
154
+ ...rest,
155
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
156
+ onLayout: onMainLayout,
157
+ children: mainChildren
158
+ }), badgeChildren != null && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
159
+ style: badgePlacement,
160
+ onLayout: onBadgeLayout,
161
+ pointerEvents: "box-none",
162
+ children: badgeBoxStyle != null ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
163
+ style: badgeBoxStyle,
164
+ children: forceBadgeFill(badgeChildren)
165
+ }) : badgeChildren
166
+ })]
167
+ });
168
+ }
169
+ const styles = {
170
+ // alignSelf flex-start so the wrapper hugs the main content; anchors are then
171
+ // computed relative to the content size rather than a stretched parent.
172
+ container: {
173
+ position: 'relative',
174
+ alignSelf: 'flex-start'
175
+ }
176
+ };
177
+
178
+ /** Fill style merged into badge content when `badgeSize` enforces a fixed box. */
179
+ const FILL_STYLE = {
180
+ width: '100%',
181
+ height: '100%'
182
+ };
183
+ var _default = exports.default = /*#__PURE__*/_react.default.memo(Attached);
@@ -20,9 +20,11 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
20
20
  * Card component implementation from Figma node 765:6186.
21
21
  *
22
22
  * Supports a `media` slot (with aspect ratio) and a content area.
23
+ * Supports an optional `header` slot (e.g. a brand logo), a `media` slot
24
+ * (with aspect ratio) and a content area.
23
25
  * Usage:
24
26
  * ```tsx
25
- * <Card media={<Image source={...} />} modes={modes}>
27
+ * <Card header={<GoldLogo />} media={<Image source={...} />} modes={modes}>
26
28
  * <Card.SupportText>Support text</Card.SupportText>
27
29
  * <Card.Title>Title</Card.Title>
28
30
  * <Card.SupportText>Support text</Card.SupportText>
@@ -30,6 +32,7 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
30
32
  * ```
31
33
  */
32
34
  function Card({
35
+ header,
33
36
  media,
34
37
  children,
35
38
  modes = _reactUtils.EMPTY_MODES,
@@ -57,6 +60,14 @@ function Card({
57
60
  ...modes
58
61
  }
59
62
  }) : media;
63
+
64
+ // Clone header to pass modes if it's a valid element
65
+ const headerWithModes = /*#__PURE__*/(0, _react.isValidElement)(header) ? /*#__PURE__*/(0, _react.cloneElement)(header, {
66
+ modes: {
67
+ ...header.props.modes,
68
+ ...modes
69
+ }
70
+ }) : header;
60
71
  const containerStyle = {
61
72
  backgroundColor,
62
73
  borderColor,
@@ -67,6 +78,15 @@ function Card({
67
78
  paddingVertical,
68
79
  overflow: 'hidden' // Ensure border radius clips content
69
80
  };
81
+
82
+ // Header wrap uses fixed padding from Figma (no dedicated tokens defined).
83
+ const headerWrapperStyle = {
84
+ width: '100%',
85
+ flexDirection: 'row',
86
+ alignItems: 'flex-start',
87
+ paddingHorizontal: 12,
88
+ paddingVertical: 16
89
+ };
70
90
  const mediaWrapperStyle = {
71
91
  width: '100%',
72
92
  aspectRatio: mediaAspectRatio,
@@ -87,7 +107,10 @@ function Card({
87
107
  },
88
108
  children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
89
109
  style: [containerStyle, style],
90
- children: [media && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
110
+ children: [header && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
111
+ style: headerWrapperStyle,
112
+ children: headerWithModes
113
+ }), media && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
91
114
  style: mediaWrapperStyle,
92
115
  children: mediaWithModes
93
116
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
@@ -53,11 +53,25 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
53
53
  /** Minimum touch target per iOS HIG / Material accessibility guidance. */
54
54
  const MIN_TOUCH_TARGET = 44;
55
55
  const touchTargetStyle = {
56
- minWidth: MIN_TOUCH_TARGET,
57
- minHeight: MIN_TOUCH_TARGET,
58
56
  alignItems: 'center',
59
57
  justifyContent: 'center'
60
58
  };
59
+
60
+ /**
61
+ * Expands the tappable region to the 44pt minimum without changing layout.
62
+ * `hitSlop` extends the press-responder bounds beyond the visual box on both
63
+ * native and web (react-native-web ≥ 0.19), so the Pressable keeps its natural
64
+ * checkbox-sized footprint and sibling alignment stays intact.
65
+ */
66
+ function invisibleTouchHitSlop(checkboxSize) {
67
+ const slop = Math.max(0, Math.ceil((MIN_TOUCH_TARGET - checkboxSize) / 2));
68
+ return {
69
+ top: slop,
70
+ bottom: slop,
71
+ left: slop,
72
+ right: slop
73
+ };
74
+ }
61
75
  /**
62
76
  * Checkbox component that maps directly to the Figma design using design tokens.
63
77
  *
@@ -182,8 +196,10 @@ function Checkbox({
182
196
  };
183
197
  };
184
198
  const markColor = disabled && isChecked ? disabledActiveMark : selectedMarkColor;
199
+ const hitSlop = invisibleTouchHitSlop(size);
185
200
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
186
201
  style: [touchTargetStyle, style],
202
+ hitSlop: hitSlop,
187
203
  onPress: handlePress,
188
204
  disabled: disabled,
189
205
  onHoverIn: () => setIsHovered(true),
@@ -368,7 +368,12 @@ function Drawer({
368
368
  flexDirection: 'column',
369
369
  alignItems: 'stretch'
370
370
  }, contentContainerStyle],
371
- showsVerticalScrollIndicator: showsVerticalScrollIndicator,
371
+ showsVerticalScrollIndicator: showsVerticalScrollIndicator
372
+ // Let a tap on an input inside the sheet focus it on the FIRST tap
373
+ // even while the keyboard is already open (default 'never' would
374
+ // eat that tap just to dismiss the keyboard).
375
+ ,
376
+ keyboardShouldPersistTaps: "handled",
372
377
  animatedProps: animatedScrollProps,
373
378
  alwaysBounceVertical: false,
374
379
  overScrollMode: "always",
@@ -145,7 +145,7 @@ function DropdownInput({
145
145
  supportText,
146
146
  errorMessage,
147
147
  menuMaxHeight = 240,
148
- menuOffset = 6,
148
+ menuOffset,
149
149
  matchTriggerWidth = true,
150
150
  closeOnBackdropPress = true,
151
151
  modes: propModes = _reactUtils.EMPTY_MODES,
@@ -208,10 +208,29 @@ function DropdownInput({
208
208
  const tokens = useFormFieldTokens(modes);
209
209
  const chevron = useChevronTokens(modes);
210
210
 
211
+ // Gap between the input and the popup. Falls back to the `formField/gap`
212
+ // token so the menu's offset matches the field's own internal spacing.
213
+ const effectiveMenuOffset = menuOffset ?? tokens.gap;
214
+
211
215
  // ---------------- Layout / measurement ----------------
212
216
  const triggerRef = (0, _react.useRef)(null);
213
217
  const [triggerRect, setTriggerRect] = (0, _react.useState)(null);
214
218
  const insets = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)();
219
+
220
+ // Android coordinate-space bridge.
221
+ //
222
+ // The popup lives inside a `statusBarTranslucent` Modal, whose window is
223
+ // laid out from the PHYSICAL top of the screen (behind the status bar).
224
+ // The trigger, however, is rendered inside the app's content area (Expo
225
+ // Router / react-native-screens under edge-to-edge), so its
226
+ // `measureInWindow` Y is relative to the content area — it does NOT include
227
+ // the status bar height. Feeding that Y straight into the Modal would place
228
+ // the popup one status-bar-height too high, landing it on top of the input.
229
+ //
230
+ // Adding `insets.top` converts the trigger's content-relative Y into the
231
+ // Modal's full-screen coordinate space. iOS/web share a single coordinate
232
+ // space for the Modal and the trigger, so no shift is needed there.
233
+ const windowTopOffset = _reactNative.Platform.OS === 'android' ? insets.top : 0;
215
234
  const measure = (0, _react.useCallback)(() => {
216
235
  if (!triggerRef.current) return;
217
236
  triggerRef.current.measureInWindow((x, y, width, height) => {
@@ -277,7 +296,7 @@ function DropdownInput({
277
296
  const spaceBelow = windowHeight - (triggerRect.y + triggerRect.height) - insets.bottom;
278
297
  const spaceAbove = triggerRect.y - insets.top;
279
298
  const desiredHeight = Math.min(menuSize?.height ?? menuMaxHeight, menuMaxHeight);
280
- const needed = desiredHeight + menuOffset + 8;
299
+ const needed = desiredHeight + effectiveMenuOffset + 8;
281
300
  if (placement === 'top') {
282
301
  return spaceAbove >= needed || spaceAbove >= spaceBelow ? 'top' : 'bottom';
283
302
  }
@@ -285,7 +304,7 @@ function DropdownInput({
285
304
  return spaceBelow >= needed || spaceBelow >= spaceAbove ? 'bottom' : 'top';
286
305
  }
287
306
  return spaceBelow >= needed || spaceBelow >= spaceAbove ? 'bottom' : 'top';
288
- }, [triggerRect, placement, windowHeight, menuSize?.height, menuMaxHeight, menuOffset, insets.top, insets.bottom]);
307
+ }, [triggerRect, placement, windowHeight, menuSize?.height, menuMaxHeight, effectiveMenuOffset, insets.top, insets.bottom]);
289
308
  const popupStyle = (0, _react.useMemo)(() => {
290
309
  if (!triggerRect) {
291
310
  return {
@@ -304,15 +323,18 @@ function DropdownInput({
304
323
  const minLeft = insets.left + screenPadding;
305
324
  if (leftPos > maxLeft) leftPos = maxLeft;
306
325
  if (leftPos < minLeft) leftPos = minLeft;
326
+
327
+ // Trigger top expressed in the Modal's (full-screen) coordinate space.
328
+ const triggerTop = triggerRect.y + windowTopOffset;
307
329
  let topPos;
308
330
  if (computedPlacement === 'top') {
309
331
  const desiredHeight = menuSize?.height ?? menuMaxHeight;
310
- topPos = triggerRect.y - desiredHeight - menuOffset;
332
+ topPos = triggerTop - desiredHeight - effectiveMenuOffset;
311
333
  if (topPos < insets.top + screenPadding) {
312
334
  topPos = insets.top + screenPadding;
313
335
  }
314
336
  } else {
315
- topPos = triggerRect.y + triggerRect.height + menuOffset;
337
+ topPos = triggerTop + triggerRect.height + effectiveMenuOffset;
316
338
  }
317
339
  const style = {
318
340
  position: 'absolute',
@@ -324,7 +346,7 @@ function DropdownInput({
324
346
  // the wrong place. menuSize becomes truthy after the first layout.
325
347
  if (menuSize == null) style.opacity = 0;
326
348
  return style;
327
- }, [triggerRect, computedPlacement, menuSize, menuOffset, menuMaxHeight, matchTriggerWidth, windowWidth, insets.top, insets.left, insets.right]);
349
+ }, [triggerRect, computedPlacement, menuSize, effectiveMenuOffset, windowTopOffset, menuMaxHeight, matchTriggerWidth, windowWidth, insets.top, insets.left, insets.right]);
328
350
 
329
351
  // Reset menu size when closing so the next open re-measures (handles items
330
352
  // changing while the menu was closed).
@@ -513,6 +535,8 @@ function DropdownInput({
513
535
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Modal, {
514
536
  visible: isOpen,
515
537
  transparent: true,
538
+ statusBarTranslucent: true,
539
+ navigationBarTranslucent: true,
516
540
  animationType: "fade",
517
541
  onRequestClose: closeMenu,
518
542
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
@@ -88,8 +88,6 @@ function ExpandableCheckbox({
88
88
  }, [disabled, isExpanded, isExpandedControlled, onExpandedChange]);
89
89
  const gap = (0, _figmaVariablesResolver.getVariableByName)('expandableCheckbox/gap', modes) ?? 8;
90
90
  const rowGap = (0, _figmaVariablesResolver.getVariableByName)('checkboxItem/gap', modes) ?? 8;
91
- const rowPaddingHorizontal = (0, _figmaVariablesResolver.getVariableByName)('checkboxItem/padding/horizontal', modes) ?? 0;
92
- const rowPaddingVertical = (0, _figmaVariablesResolver.getVariableByName)('checkboxItem/padding/vertical', modes) ?? 0;
93
91
  const labelColor = (0, _figmaVariablesResolver.getVariableByName)('checkboxItem/foreground', modes) ?? '#1a1c1f';
94
92
  const labelFontFamily = (0, _figmaVariablesResolver.getVariableByName)('checkboxItem/label/fontFamily', modes) ?? 'JioType Var';
95
93
  const labelFontSize = (0, _figmaVariablesResolver.getVariableByName)('checkboxItem/label/fontSize', modes) ?? 14;
@@ -110,11 +108,9 @@ function ExpandableCheckbox({
110
108
  alignSelf: isExpanded ? 'stretch' : 'auto',
111
109
  minWidth: 0,
112
110
  flexDirection: 'row',
113
- alignItems: 'flex-start',
114
- gap: rowGap,
115
- paddingHorizontal: rowPaddingHorizontal,
116
- paddingVertical: rowPaddingVertical
117
- }), [isExpanded, rowGap, rowPaddingHorizontal, rowPaddingVertical]);
111
+ alignItems: isExpanded ? 'flex-start' : 'center',
112
+ gap: rowGap
113
+ }), [isExpanded, rowGap]);
118
114
  const resolvedLabelStyle = (0, _react.useMemo)(() => ({
119
115
  flex: 1,
120
116
  minWidth: 0,
@@ -122,11 +118,21 @@ function ExpandableCheckbox({
122
118
  fontFamily: labelFontFamily,
123
119
  fontSize: labelFontSize,
124
120
  lineHeight: labelLineHeight,
125
- fontWeight: labelFontWeight
126
- }), [labelColor, labelFontFamily, labelFontSize, labelLineHeight, labelFontWeight]);
121
+ fontWeight: labelFontWeight,
122
+ // Android adds asymmetric font padding and top-aligns the glyph inside
123
+ // an inflated line box when `lineHeight` is set. That makes the centered
124
+ // checkbox look like it drops below the text. Disabling the extra
125
+ // padding + centering the glyph keeps the single-line label optically
126
+ // aligned with the checkbox. No-op on iOS / web.
127
+ includeFontPadding: false,
128
+ textAlignVertical: isExpanded ? 'top' : 'center'
129
+ }), [labelColor, labelFontFamily, labelFontSize, labelLineHeight, labelFontWeight, isExpanded]);
130
+
131
+ // Layer component modes first (e.g. Color Mode), then button defaults so
132
+ // Secondary / XS / Low always win unless a dedicated override prop is added.
127
133
  const buttonModes = (0, _react.useMemo)(() => ({
128
- ...BUTTON_DEFAULT_MODES,
129
- ...modes
134
+ ...modes,
135
+ ...BUTTON_DEFAULT_MODES
130
136
  }), [modes]);
131
137
  const a11yLabel = accessibilityLabel ?? (typeof label === 'string' ? label : undefined);
132
138
  const buttonLabel = isExpanded ? readLessLabel : readMoreLabel;
@@ -208,16 +208,6 @@ function FormField({
208
208
  const [isFocused, setIsFocused] = (0, _react.useState)(false);
209
209
  const interactive = !isDisabled && !isReadOnly;
210
210
 
211
- // Ref to the native input so tapping anywhere in the input row (padding,
212
- // leading/trailing gutters) focuses it on the FIRST tap — fixing the Android
213
- // "two taps to open the keyboard" issue caused by the row intercepting the
214
- // initial touch.
215
- const inputRef = (0, _react.useRef)(null);
216
- const focusInput = (0, _react.useCallback)(() => {
217
- if (!interactive) return;
218
- inputRef.current?.focus();
219
- }, [interactive]);
220
-
221
211
  // FormField States cascade — error > read only/disabled > active (focused) > idle.
222
212
  // Disabled maps to "Read Only" since there is no dedicated disabled mode and
223
213
  // the visual treatment is closest. This is only the DEFAULT — an explicit
@@ -350,16 +340,13 @@ function FormField({
350
340
  style: requiredIndicatorStyle,
351
341
  children: " *"
352
342
  })]
353
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
343
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
354
344
  style: [inputRowStyle, inputStyle],
355
- onPress: focusInput,
356
- accessible: false,
357
345
  children: [processedLeading != null && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
358
346
  accessibilityElementsHidden: true,
359
347
  importantForAccessibility: "no",
360
348
  children: processedLeading
361
349
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TextInput, {
362
- ref: inputRef,
363
350
  style: [inputTextStyles, inputTextStyle],
364
351
  value: value ?? '',
365
352
  onChangeText: handleChangeText,
@@ -14,6 +14,7 @@ var _Button = _interopRequireDefault(require("../Button/Button"));
14
14
  var _Disclaimer = _interopRequireDefault(require("../Disclaimer/Disclaimer"));
15
15
  var _IconButton = _interopRequireDefault(require("../IconButton/IconButton"));
16
16
  var _ActionFooter = _interopRequireDefault(require("../ActionFooter/ActionFooter"));
17
+ var _Slot = _interopRequireDefault(require("../Slot/Slot"));
17
18
  var _jsxRuntime = require("react/jsx-runtime");
18
19
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
19
20
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
@@ -274,8 +275,9 @@ function FullscreenModal({
274
275
  if (footer) {
275
276
  footerContent = footer;
276
277
  } else if (primaryActionLabel) {
277
- footerContent = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
278
- style: footerColumnStyle,
278
+ footerContent = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_Slot.default, {
279
+ layoutDirection: "vertical",
280
+ modes: modes,
279
281
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_Button.default, {
280
282
  label: primaryActionLabel,
281
283
  modes: modes,
@@ -299,7 +301,11 @@ function FullscreenModal({
299
301
  contentContainerStyle: scrollContentStyle,
300
302
  showsVerticalScrollIndicator: false,
301
303
  onScroll: onScroll,
302
- scrollEventThrottle: 16,
304
+ scrollEventThrottle: 16
305
+ // Tap an input in the body and it focuses on the FIRST tap, even when
306
+ // the keyboard is already open (default 'never' eats that tap).
307
+ ,
308
+ keyboardShouldPersistTaps: "handled",
303
309
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
304
310
  style: heroTextRegionStyle,
305
311
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(HeroText, {
@@ -340,10 +346,6 @@ const scrollViewStyle = {
340
346
  const scrollContentStyle = {
341
347
  flexGrow: 1
342
348
  };
343
- const footerColumnStyle = {
344
- width: '100%',
345
- gap: 8
346
- };
347
349
  const fullWidthStyle = {
348
350
  width: '100%'
349
351
  };