jfs-components 0.0.77 → 0.0.79

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 (87) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/lib/commonjs/components/Accordion/Accordion.js +55 -55
  3. package/lib/commonjs/components/ActionFooter/ActionFooter.js +48 -2
  4. package/lib/commonjs/components/Attached/Attached.js +144 -0
  5. package/lib/commonjs/components/Card/Card.js +25 -2
  6. package/lib/commonjs/components/Checkbox/Checkbox.js +21 -9
  7. package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -16
  8. package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +167 -0
  9. package/lib/commonjs/components/FormField/FormField.js +14 -1
  10. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +353 -0
  11. package/lib/commonjs/components/ListItem/ListItem.js +46 -24
  12. package/lib/commonjs/components/MessageField/MessageField.js +318 -0
  13. package/lib/commonjs/components/NavArrow/NavArrow.js +58 -17
  14. package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +328 -0
  15. package/lib/commonjs/components/Slot/Slot.js +73 -0
  16. package/lib/commonjs/components/Stepper/Step.js +47 -60
  17. package/lib/commonjs/components/Stepper/StepLabel.js +40 -10
  18. package/lib/commonjs/components/Stepper/Stepper.js +15 -17
  19. package/lib/commonjs/components/SuggestiveSearch/SuggestiveSearch.js +487 -0
  20. package/lib/commonjs/components/TextInput/TextInput.js +16 -1
  21. package/lib/commonjs/components/Title/Title.js +10 -2
  22. package/lib/commonjs/components/index.js +49 -0
  23. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  24. package/lib/commonjs/icons/registry.js +1 -1
  25. package/lib/module/components/Accordion/Accordion.js +56 -56
  26. package/lib/module/components/ActionFooter/ActionFooter.js +50 -4
  27. package/lib/module/components/Attached/Attached.js +139 -0
  28. package/lib/module/components/Card/Card.js +25 -2
  29. package/lib/module/components/Checkbox/Checkbox.js +22 -10
  30. package/lib/module/components/DropdownInput/DropdownInput.js +30 -16
  31. package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +161 -0
  32. package/lib/module/components/FormField/FormField.js +16 -3
  33. package/lib/module/components/FullscreenModal/FullscreenModal.js +348 -0
  34. package/lib/module/components/ListItem/ListItem.js +46 -24
  35. package/lib/module/components/MessageField/MessageField.js +313 -0
  36. package/lib/module/components/NavArrow/NavArrow.js +59 -18
  37. package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +322 -0
  38. package/lib/module/components/Slot/Slot.js +68 -0
  39. package/lib/module/components/Stepper/Step.js +48 -61
  40. package/lib/module/components/Stepper/StepLabel.js +40 -10
  41. package/lib/module/components/Stepper/Stepper.js +15 -17
  42. package/lib/module/components/SuggestiveSearch/SuggestiveSearch.js +481 -0
  43. package/lib/module/components/TextInput/TextInput.js +17 -2
  44. package/lib/module/components/Title/Title.js +10 -2
  45. package/lib/module/components/index.js +7 -0
  46. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  47. package/lib/module/icons/registry.js +1 -1
  48. package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -20
  49. package/lib/typescript/src/components/Attached/Attached.d.ts +61 -0
  50. package/lib/typescript/src/components/Card/Card.d.ts +9 -2
  51. package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +63 -0
  52. package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +99 -0
  53. package/lib/typescript/src/components/ListItem/ListItem.d.ts +15 -5
  54. package/lib/typescript/src/components/MessageField/MessageField.d.ts +81 -0
  55. package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +10 -5
  56. package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +64 -0
  57. package/lib/typescript/src/components/Slot/Slot.d.ts +52 -0
  58. package/lib/typescript/src/components/Stepper/Step.d.ts +4 -1
  59. package/lib/typescript/src/components/Stepper/StepLabel.d.ts +4 -1
  60. package/lib/typescript/src/components/Stepper/Stepper.d.ts +3 -1
  61. package/lib/typescript/src/components/SuggestiveSearch/SuggestiveSearch.d.ts +123 -0
  62. package/lib/typescript/src/components/index.d.ts +10 -3
  63. package/lib/typescript/src/icons/registry.d.ts +1 -1
  64. package/package.json +1 -1
  65. package/src/components/Accordion/Accordion.tsx +113 -73
  66. package/src/components/ActionFooter/ActionFooter.tsx +56 -4
  67. package/src/components/Attached/Attached.tsx +181 -0
  68. package/src/components/Card/Card.tsx +28 -1
  69. package/src/components/Checkbox/Checkbox.tsx +22 -9
  70. package/src/components/DropdownInput/DropdownInput.tsx +67 -39
  71. package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +237 -0
  72. package/src/components/FormField/FormField.tsx +19 -3
  73. package/src/components/FullscreenModal/FullscreenModal.tsx +414 -0
  74. package/src/components/ListItem/ListItem.tsx +55 -25
  75. package/src/components/MessageField/MessageField.tsx +543 -0
  76. package/src/components/NavArrow/NavArrow.tsx +81 -17
  77. package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +426 -0
  78. package/src/components/Slot/Slot.tsx +91 -0
  79. package/src/components/Stepper/Step.tsx +52 -51
  80. package/src/components/Stepper/StepLabel.tsx +46 -9
  81. package/src/components/Stepper/Stepper.tsx +20 -15
  82. package/src/components/SuggestiveSearch/SuggestiveSearch.tsx +756 -0
  83. package/src/components/TextInput/TextInput.tsx +14 -1
  84. package/src/components/Title/Title.tsx +13 -2
  85. package/src/components/index.ts +10 -3
  86. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  87. package/src/icons/registry.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,34 @@ 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
+
18
+ ## [0.0.78] - 2026-05-29
19
+
20
+ - Added `ExpandableCheckbox` — checkbox row with collapsible long label and Read more / Read less toggle.
21
+ - Added `FullscreenModal` — full-screen modal with parallax hero, close button, disclaimer, and action footer slots.
22
+ - Added `MessageField` — multi-line textarea with FormField States (Idle / Active / Read Only / Error / Disabled) and Form context integration.
23
+ - Added `SuggestiveSearch` — search input with inline suggestion dropdown.
24
+ - `Accordion` — new `contained` variant and hover state handling.
25
+ - `NavArrow` — pressable when `onPress` is provided; 44 pt touch target.
26
+ - `Checkbox` — 44 pt touch target; checkmark rendered inside the box view.
27
+ - `FormField` / `TextInput` — tap anywhere in the input row to focus on first tap (Android double-tap fix).
28
+ - `ActionFooter` — Android keyboard counter-shift so the footer stays behind the keyboard instead of jumping up.
29
+ - `ListItem` — title/support text use `AppearanceBrand: 'Neutral'` without forcing it onto slot children.
30
+ - `Stepper` / `Step` / `StepLabel` — `showLine`, `metaText`, `meta` props; larger indicator sizing; public type exports.
31
+ - `Title` — line-height clamp to prevent descender clipping on Android.
32
+
33
+ ---
34
+
7
35
  ## [0.0.77] - 2026-05-25
8
36
 
9
37
  ### Added
@@ -17,34 +17,45 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
17
17
  if (_reactNative.Platform.OS === 'android' && _reactNative.UIManager.setLayoutAnimationEnabledExperimental) {
18
18
  _reactNative.UIManager.setLayoutAnimationEnabledExperimental(true);
19
19
  }
20
+ function resolveAccordionStateMode(disabled, isExpanded, isHovered, contained) {
21
+ if (disabled) return 'Disabled';
22
+ if (contained) {
23
+ return isExpanded ? 'Open Hover' : 'Hover';
24
+ }
25
+ if (isExpanded) {
26
+ return isHovered ? 'Open Hover' : 'Open';
27
+ }
28
+ return isHovered ? 'Hover' : 'Idle';
29
+ }
30
+ function toFontWeight(value, fallback) {
31
+ if (typeof value === 'number') return String(value);
32
+ if (typeof value === 'string') {
33
+ const normalized = value.trim().toLowerCase();
34
+ if (normalized === 'bold') return '700';
35
+ if (normalized === 'medium') return '500';
36
+ if (normalized === 'regular' || normalized === 'normal') return '400';
37
+ if (/^\d+$/.test(normalized)) return normalized;
38
+ return value;
39
+ }
40
+ return fallback;
41
+ }
20
42
  /**
21
43
  * Accordion component that mirrors the Figma "Accordion" component.
22
44
  *
23
- * This component supports:
24
- * - **Expandable/collapsible content** with smooth animation
25
- * - **States**: Idle, Hover, Open, Disabled
26
- * - **Slot** for custom content
27
- * - **Design-token driven styling** via `getVariableByName` and `modes`
45
+ * Supports two visual treatments via the `contained` prop:
46
+ * - **`contained={false}`** (default) transparent header at rest; filled
47
+ * background on hover / press.
48
+ * - **`contained={true}`** header always uses the filled background.
28
49
  *
29
- * Wherever the Figma layer name contains "Slot", this component exposes a
30
- * dedicated React "slot" prop:
31
- * - Slot "content" `children`
50
+ * Interaction states (Idle, Hover, Open, Disabled) are resolved automatically
51
+ * from `expanded`, `disabled`, hover, and `contained` — consumers should not
52
+ * pass `'Accordion States'` in `modes`.
32
53
  *
33
54
  * @component
34
- * @param {Object} props
35
- * @param {string} [props.title='Accordion title'] - The accordion header title
36
- * @param {boolean} [props.defaultExpanded=false] - Initial expanded state
37
- * @param {boolean} [props.expanded] - Controlled expanded state
38
- * @param {Function} [props.onExpandedChange] - Callback fired when expanded state changes
39
- * @param {boolean} [props.disabled=false] - Whether the accordion is disabled
40
- * @param {React.ReactNode} [props.children] - Content to display when expanded
41
- * @param {Object} [props.modes={}] - Modes object passed to `getVariableByName` for all design tokens
42
- * @param {Object} [props.style] - Optional container style overrides
43
- * @param {string} [props.accessibilityLabel] - Accessibility label for the accordion. If not provided, uses title
44
- * @param {string} [props.accessibilityHint] - Additional accessibility hint for screen readers
45
55
  */
46
56
  function Accordion({
47
57
  title = 'Accordion title',
58
+ contained = false,
48
59
  defaultExpanded = false,
49
60
  expanded: controlledExpanded,
50
61
  onExpandedChange,
@@ -58,21 +69,19 @@ function Accordion({
58
69
  webAccessibilityProps,
59
70
  ...rest
60
71
  }) {
61
- // Internal state for uncontrolled mode
62
72
  const [internalExpanded, setInternalExpanded] = (0, _react.useState)(defaultExpanded);
63
-
64
- // Determine if controlled or uncontrolled
73
+ const [isHovered, setIsHovered] = (0, _react.useState)(false);
65
74
  const isControlled = controlledExpanded !== undefined;
66
75
  const isExpanded = isControlled ? controlledExpanded : internalExpanded;
67
-
68
- // Hover state for web
69
- const [isHovered, setIsHovered] = (0, _react.useState)(false);
70
-
71
- // Handle toggle
76
+ const resolvedModes = (0, _react.useMemo)(() => {
77
+ const accordionState = resolveAccordionStateMode(disabled, isExpanded, isHovered, contained);
78
+ return {
79
+ ...modes,
80
+ 'Accordion States': accordionState
81
+ };
82
+ }, [contained, disabled, isExpanded, isHovered, modes]);
72
83
  const handleToggle = () => {
73
84
  if (disabled) return;
74
-
75
- // Animate the layout change
76
85
  _reactNative.LayoutAnimation.configureNext(_reactNative.LayoutAnimation.Presets.easeInEaseOut);
77
86
  if (isControlled) {
78
87
  onExpandedChange?.(!isExpanded);
@@ -81,23 +90,20 @@ function Accordion({
81
90
  onExpandedChange?.(!isExpanded);
82
91
  }
83
92
  };
84
-
85
- // Resolve design tokens
86
- const titleColor = disabled ? '#999999' : (0, _figmaVariablesResolver.getVariableByName)('accordion/title/color', modes) || '#0d0d0d';
87
- const titleFontSize = (0, _figmaVariablesResolver.getVariableByName)('accordion/title/fontSize', modes) || 18;
88
- const titleLineHeight = (0, _figmaVariablesResolver.getVariableByName)('accordion/title/lineHeight', modes) || 20;
89
- const titleFontFamily = (0, _figmaVariablesResolver.getVariableByName)('accordion/title/fontFamily', modes) || 'System';
90
- const iconColor = (0, _figmaVariablesResolver.getVariableByName)('accordion/icon/color', modes) || '#141414';
91
- const iconSize = (0, _figmaVariablesResolver.getVariableByName)('accordion/icon/size', modes) || 24;
92
- const headerGap = (0, _figmaVariablesResolver.getVariableByName)('accordion/header/gap', modes) || 12;
93
- const headerPaddingVertical = (0, _figmaVariablesResolver.getVariableByName)('accordion/header/padding/vertical', modes) || 24;
94
- const headerBackground = isHovered && !disabled ? '#f2f2f2' : (0, _figmaVariablesResolver.getVariableByName)('accordion/header/background', modes) || 'transparent';
95
- const contentGap = (0, _figmaVariablesResolver.getVariableByName)('accordion/content/gap', modes) || 12;
96
- const contentPaddingTop = (0, _figmaVariablesResolver.getVariableByName)('accordion/content/padding/top', modes) || 8;
97
- const contentPaddingBottom = isExpanded ? (0, _figmaVariablesResolver.getVariableByName)('accordion/content/padding/bottom', modes) ?? 24 : 8;
98
- const borderColor = (0, _figmaVariablesResolver.getVariableByName)('accordion/border/color', modes) || '#e6e6e6';
99
-
100
- // Styles
93
+ const titleColor = (0, _figmaVariablesResolver.getVariableByName)('accordion/title/color', resolvedModes) ?? '#0d0d0d';
94
+ const titleFontSize = (0, _figmaVariablesResolver.getVariableByName)('accordion/title/fontSize', resolvedModes) ?? 14;
95
+ const titleLineHeight = (0, _figmaVariablesResolver.getVariableByName)('accordion/title/lineHeight', resolvedModes) ?? 20;
96
+ const titleFontFamily = (0, _figmaVariablesResolver.getVariableByName)('accordion/title/fontFamily', resolvedModes) ?? 'System';
97
+ const titleFontWeight = toFontWeight((0, _figmaVariablesResolver.getVariableByName)('accordion/title/fontWeight', resolvedModes), '700');
98
+ const iconColor = (0, _figmaVariablesResolver.getVariableByName)('accordion/icon/color', resolvedModes) ?? '#141414';
99
+ const iconSize = (0, _figmaVariablesResolver.getVariableByName)('accordion/icon/size', resolvedModes) ?? 24;
100
+ const headerGap = (0, _figmaVariablesResolver.getVariableByName)('accordion/header/gap', resolvedModes) ?? 12;
101
+ const headerPaddingVertical = (0, _figmaVariablesResolver.getVariableByName)('accordion/header/padding/vertical', resolvedModes) ?? 8;
102
+ const headerBackground = (0, _figmaVariablesResolver.getVariableByName)('accordion/header/background', resolvedModes) ?? 'transparent';
103
+ const contentGap = (0, _figmaVariablesResolver.getVariableByName)('accordion/content/gap', resolvedModes) ?? 12;
104
+ const contentPaddingTop = (0, _figmaVariablesResolver.getVariableByName)('accordion/content/padding/top', resolvedModes) ?? 8;
105
+ const contentPaddingBottom = (0, _figmaVariablesResolver.getVariableByName)('accordion/content/padding/bottom', resolvedModes) ?? 8;
106
+ const borderColor = (0, _figmaVariablesResolver.getVariableByName)('accordion/border/color', resolvedModes) ?? '#e6e6e6';
101
107
  const containerStyle = {
102
108
  borderBottomWidth: 1,
103
109
  borderBottomColor: borderColor
@@ -117,7 +123,7 @@ function Accordion({
117
123
  fontSize: titleFontSize,
118
124
  lineHeight: titleLineHeight,
119
125
  fontFamily: titleFontFamily,
120
- fontWeight: '700'
126
+ fontWeight: titleFontWeight
121
127
  };
122
128
  const contentStyle = {
123
129
  backgroundColor: 'transparent',
@@ -127,11 +133,7 @@ function Accordion({
127
133
  paddingHorizontal: 0,
128
134
  overflow: 'hidden'
129
135
  };
130
-
131
- // Generate default accessibility label
132
136
  const defaultAccessibilityLabel = accessibilityLabel || title;
133
-
134
- // Web platform support
135
137
  const webProps = (0, _webPlatformUtils.usePressableWebSupport)({
136
138
  restProps: {},
137
139
  onPress: handleToggle,
@@ -139,9 +141,7 @@ function Accordion({
139
141
  accessibilityLabel: defaultAccessibilityLabel,
140
142
  webAccessibilityProps
141
143
  });
142
-
143
- // Process children to pass modes
144
- const processedChildren = children ? (0, _reactUtils.cloneChildrenWithModes)(_react.default.Children.toArray(children), modes) : null;
144
+ const processedChildren = children ? (0, _reactUtils.cloneChildrenWithModes)(_react.default.Children.toArray(children), resolvedModes) : null;
145
145
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
146
146
  style: [containerStyle, style],
147
147
  ...rest,
@@ -171,7 +171,7 @@ function Accordion({
171
171
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_Icon.default, {
172
172
  name: isExpanded ? 'ic_minus' : 'ic_add',
173
173
  size: iconSize,
174
- color: disabled ? '#999999' : iconColor,
174
+ color: iconColor,
175
175
  accessibilityElementsHidden: true,
176
176
  importantForAccessibility: "no"
177
177
  })]
@@ -111,6 +111,44 @@ function ActionFooter({
111
111
  style,
112
112
  accessibilityLabel
113
113
  }) {
114
+ // -------------------------------------------------------------------------
115
+ // Keep the footer locked in place behind the software keyboard (Android).
116
+ // -------------------------------------------------------------------------
117
+ //
118
+ // The Android activity is configured with `windowSoftInputMode="adjustResize"`,
119
+ // which shrinks the app window by the keyboard height when the keyboard
120
+ // opens. A bottom-anchored footer therefore gets lifted UP by the keyboard
121
+ // height — exactly the jump the design does not want.
122
+ //
123
+ // To counteract that, we translate the footer back DOWN by the same keyboard
124
+ // height so it visually stays exactly where it was (now sitting behind the
125
+ // keyboard). iOS does not resize the window for the keyboard, so the footer
126
+ // already stays put there; we only run this on Android to avoid pushing the
127
+ // footer off-screen on platforms that don't lift it in the first place.
128
+ const keyboardOffset = (0, _react.useRef)(new _reactNative.Animated.Value(0)).current;
129
+ (0, _react.useEffect)(() => {
130
+ if (_reactNative.Platform.OS !== 'android') return undefined;
131
+ const animateTo = (toValue, duration) => {
132
+ _reactNative.Animated.timing(keyboardOffset, {
133
+ toValue,
134
+ // Match the OS keyboard animation so the resize and our counter-shift
135
+ // cancel out smoothly with no visible footer movement.
136
+ duration: typeof duration === 'number' && duration > 0 ? duration : 150,
137
+ useNativeDriver: true
138
+ }).start();
139
+ };
140
+ const showSub = _reactNative.Keyboard.addListener('keyboardDidShow', e => {
141
+ animateTo(e?.endCoordinates?.height ?? 0, e?.duration);
142
+ });
143
+ const hideSub = _reactNative.Keyboard.addListener('keyboardDidHide', e => {
144
+ animateTo(0, e?.duration);
145
+ });
146
+ return () => {
147
+ showSub.remove();
148
+ hideSub.remove();
149
+ };
150
+ }, [keyboardOffset]);
151
+
114
152
  // All token reads collapsed into a single useMemo keyed on `modes`. With
115
153
  // the shared `EMPTY_MODES` default this resolves once for the common path
116
154
  // and never re-allocates the container/slot style objects between renders.
@@ -169,8 +207,16 @@ function ActionFooter({
169
207
  });
170
208
  });
171
209
  }, [children, modes]);
172
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
173
- style: [containerStyle, WEB_SHADOW, style],
210
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, {
211
+ style: [containerStyle, WEB_SHADOW, style,
212
+ // Counter-translate by the keyboard height on Android so `adjustResize`
213
+ // can't lift the footer above the keyboard (no-op on iOS/web where the
214
+ // value stays at 0).
215
+ {
216
+ transform: [{
217
+ translateY: keyboardOffset
218
+ }]
219
+ }],
174
220
  accessibilityRole: "toolbar",
175
221
  accessibilityLabel: accessibilityLabel,
176
222
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
@@ -0,0 +1,144 @@
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
+ function Attached({
52
+ children,
53
+ badge,
54
+ position = 'bottom-right',
55
+ circular = true,
56
+ modes: propModes = _reactUtils.EMPTY_MODES,
57
+ style,
58
+ ...rest
59
+ }) {
60
+ const {
61
+ modes: globalModes
62
+ } = (0, _JFSThemeProvider.useTokens)();
63
+ const modes = (0, _react.useMemo)(() => globalModes === _reactUtils.EMPTY_MODES && propModes === _reactUtils.EMPTY_MODES ? _reactUtils.EMPTY_MODES : {
64
+ ...globalModes,
65
+ ...propModes
66
+ }, [globalModes, propModes]);
67
+ const [mainSize, setMainSize] = (0, _react.useState)(ZERO_SIZE);
68
+ const [badgeSize, setBadgeSize] = (0, _react.useState)(ZERO_SIZE);
69
+ const onMainLayout = (0, _react.useCallback)(e => {
70
+ const {
71
+ width,
72
+ height
73
+ } = e.nativeEvent.layout;
74
+ setMainSize(prev => prev.width === width && prev.height === height ? prev : {
75
+ width,
76
+ height
77
+ });
78
+ }, []);
79
+ const onBadgeLayout = (0, _react.useCallback)(e => {
80
+ const {
81
+ width,
82
+ height
83
+ } = e.nativeEvent.layout;
84
+ setBadgeSize(prev => prev.width === width && prev.height === height ? prev : {
85
+ width,
86
+ height
87
+ });
88
+ }, []);
89
+ const mainChildren = (0, _react.useMemo)(() => children != null ? (0, _reactUtils.cloneChildrenWithModes)(children, modes) : null, [children, modes]);
90
+ const badgeChildren = (0, _react.useMemo)(() => badge != null ? (0, _reactUtils.cloneChildrenWithModes)(badge, modes) : null, [badge, modes]);
91
+ const badgePlacement = (0, _react.useMemo)(() => {
92
+ const {
93
+ fx,
94
+ fy
95
+ } = resolveAnchorFractions(position);
96
+ const measured = mainSize.width > 0 && badgeSize.width > 0;
97
+ let anchorX;
98
+ let anchorY;
99
+ if (circular) {
100
+ // Project the anchor onto the circle inscribed in the bounding box, so
101
+ // corner badges land on the circumference (45°) instead of the box corner.
102
+ const cx = mainSize.width / 2;
103
+ const cy = mainSize.height / 2;
104
+ const radius = Math.min(mainSize.width, mainSize.height) / 2;
105
+ const dx = (fx - 0.5) * 2; // -1 | 0 | 1
106
+ const dy = (fy - 0.5) * 2; // -1 | 0 | 1
107
+ const len = Math.hypot(dx, dy) || 1; // 'center' → 0, guard against /0
108
+ anchorX = cx + dx / len * radius;
109
+ anchorY = cy + dy / len * radius;
110
+ } else {
111
+ anchorX = mainSize.width * fx;
112
+ anchorY = mainSize.height * fy;
113
+ }
114
+ return {
115
+ position: 'absolute',
116
+ left: anchorX - badgeSize.width / 2,
117
+ top: anchorY - badgeSize.height / 2,
118
+ // Hide until both elements are measured to avoid a one-frame flash at (0,0).
119
+ opacity: measured ? 1 : 0
120
+ };
121
+ }, [position, circular, mainSize, badgeSize]);
122
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
123
+ style: [styles.container, style],
124
+ ...rest,
125
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
126
+ onLayout: onMainLayout,
127
+ children: mainChildren
128
+ }), badgeChildren != null && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
129
+ style: badgePlacement,
130
+ onLayout: onBadgeLayout,
131
+ pointerEvents: "box-none",
132
+ children: badgeChildren
133
+ })]
134
+ });
135
+ }
136
+ const styles = {
137
+ // alignSelf flex-start so the wrapper hugs the main content; anchors are then
138
+ // computed relative to the content size rather than a stretched parent.
139
+ container: {
140
+ position: 'relative',
141
+ alignSelf: 'flex-start'
142
+ }
143
+ };
144
+ 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, {
@@ -49,6 +49,15 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
49
49
  }
50
50
  };
51
51
  }
52
+
53
+ /** Minimum touch target per iOS HIG / Material accessibility guidance. */
54
+ const MIN_TOUCH_TARGET = 44;
55
+ const touchTargetStyle = {
56
+ minWidth: MIN_TOUCH_TARGET,
57
+ minHeight: MIN_TOUCH_TARGET,
58
+ alignItems: 'center',
59
+ justifyContent: 'center'
60
+ };
52
61
  /**
53
62
  * Checkbox component that maps directly to the Figma design using design tokens.
54
63
  *
@@ -174,7 +183,7 @@ function Checkbox({
174
183
  };
175
184
  const markColor = disabled && isChecked ? disabledActiveMark : selectedMarkColor;
176
185
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
177
- style: [resolveStyle(), style],
186
+ style: [touchTargetStyle, style],
178
187
  onPress: handlePress,
179
188
  disabled: disabled,
180
189
  onHoverIn: () => setIsHovered(true),
@@ -186,14 +195,17 @@ function Checkbox({
186
195
  disabled
187
196
  },
188
197
  accessibilityLabel: accessibilityLabel,
189
- children: isChecked && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.default, {
190
- width: 12,
191
- height: 9,
192
- viewBox: "0 0 12 9",
193
- fill: "none",
194
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.Path, {
195
- d: "M4.00091 8.66939C3.91321 8.6699 3.82628 8.65309 3.74509 8.61991C3.6639 8.58673 3.59006 8.53785 3.52779 8.47606L0.195972 5.14273C0.0704931 5.01719 -1.86978e-09 4.84693 0 4.66939C1.86978e-09 4.49186 0.0704931 4.3216 0.195972 4.19606C0.321451 4.07053 0.491636 4 0.66909 4C0.846544 4 1.01673 4.07053 1.14221 4.19606L4.00091 7.06273L10.8578 0.196061C10.9833 0.0705253 11.1535 0 11.3309 0C11.5084 0 11.6785 0.0705253 11.804 0.196061C11.9295 0.321597 12 0.49186 12 0.669394C12 0.846929 11.9295 1.01719 11.804 1.14273L4.47403 8.47606C4.41176 8.53785 4.33792 8.58673 4.25673 8.61991C4.17554 8.65309 4.08861 8.6699 4.00091 8.66939Z",
196
- fill: markColor
198
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
199
+ style: resolveStyle(),
200
+ children: isChecked && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.default, {
201
+ width: 12,
202
+ height: 9,
203
+ viewBox: "0 0 12 9",
204
+ fill: "none",
205
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.Path, {
206
+ d: "M4.00091 8.66939C3.91321 8.6699 3.82628 8.65309 3.74509 8.61991C3.6639 8.58673 3.59006 8.53785 3.52779 8.47606L0.195972 5.14273C0.0704931 5.01719 -1.86978e-09 4.84693 0 4.66939C1.86978e-09 4.49186 0.0704931 4.3216 0.195972 4.19606C0.321451 4.07053 0.491636 4 0.66909 4C0.846544 4 1.01673 4.07053 1.14221 4.19606L4.00091 7.06273L10.8578 0.196061C10.9833 0.0705253 11.1535 0 11.3309 0C11.5084 0 11.6785 0.0705253 11.804 0.196061C11.9295 0.321597 12 0.49186 12 0.669394C12 0.846929 11.9295 1.01719 11.804 1.14273L4.47403 8.47606C4.41176 8.53785 4.33792 8.58673 4.25673 8.61991C4.17554 8.65309 4.08861 8.6699 4.00091 8.66939Z",
207
+ fill: markColor
208
+ })
197
209
  })
198
210
  })
199
211
  });
@@ -36,25 +36,36 @@ function useChevronTokens(modes) {
36
36
  };
37
37
  }, [modes]);
38
38
  }
39
+ function toNumber(value, fallback) {
40
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
41
+ if (typeof value === 'string') {
42
+ const parsed = parseFloat(value);
43
+ if (Number.isFinite(parsed)) return parsed;
44
+ }
45
+ return fallback;
46
+ }
39
47
  function useFormFieldTokens(modes) {
40
48
  return (0, _react.useMemo)(() => {
41
- const labelColor = (0, _figmaVariablesResolver.getVariableByName)('formField/label/color', modes) || '#0c0d10';
49
+ const labelColor = (0, _figmaVariablesResolver.getVariableByName)('formField/label/color', modes) || '#000000';
42
50
  const labelFontFamily = (0, _figmaVariablesResolver.getVariableByName)('formField/label/fontFamily', modes) || 'JioType Var';
43
- const labelFontSize = parseInt((0, _figmaVariablesResolver.getVariableByName)('formField/label/fontSize', modes), 10) || 14;
44
- const labelLineHeight = parseInt((0, _figmaVariablesResolver.getVariableByName)('formField/label/lineHeight', modes), 10) || 17;
51
+ const labelFontSize = toNumber((0, _figmaVariablesResolver.getVariableByName)('formField/label/fontSize', modes), 14);
52
+ const labelLineHeight = toNumber((0, _figmaVariablesResolver.getVariableByName)('formField/label/lineHeight', modes), 17);
45
53
  const labelFontWeight = (0, _figmaVariablesResolver.getVariableByName)('formField/label/fontWeight', modes) || '500';
46
- const gap = parseInt((0, _figmaVariablesResolver.getVariableByName)('formField/gap', modes), 10) || 8;
47
- const inputPaddingH = parseInt((0, _figmaVariablesResolver.getVariableByName)('formField/input/padding/horizontal', modes), 10) || 12;
48
- const inputGap = parseInt((0, _figmaVariablesResolver.getVariableByName)('formField/input/gap', modes), 10) || 8;
49
- const inputRadius = parseInt((0, _figmaVariablesResolver.getVariableByName)('formField/input/radius', modes), 10) || 8;
54
+ const gap = toNumber((0, _figmaVariablesResolver.getVariableByName)('formField/gap', modes), 8);
55
+ const inputPaddingH = toNumber((0, _figmaVariablesResolver.getVariableByName)('formField/input/padding/horizontal', modes), 12);
56
+ const inputGap = toNumber((0, _figmaVariablesResolver.getVariableByName)('formField/input/gap', modes), 8);
57
+ const inputRadius = toNumber((0, _figmaVariablesResolver.getVariableByName)('formField/input/radius', modes), 8);
50
58
  const inputBackground = (0, _figmaVariablesResolver.getVariableByName)('formField/input/background', modes) || '#ffffff';
51
- const inputFontSize = parseInt((0, _figmaVariablesResolver.getVariableByName)('formField/input/label/fontSize', modes), 10) || 16;
52
- const inputLineHeight = parseInt((0, _figmaVariablesResolver.getVariableByName)('formField/input/label/lineHeight', modes), 10) || 45;
59
+ const inputFontSize = toNumber((0, _figmaVariablesResolver.getVariableByName)('formField/input/label/fontSize', modes), 16);
60
+ const inputLineHeight = toNumber((0, _figmaVariablesResolver.getVariableByName)('formField/input/label/lineHeight', modes), 45);
53
61
  const inputFontFamily = (0, _figmaVariablesResolver.getVariableByName)('formField/input/label/fontFamily', modes) || 'JioType Var';
54
62
  const inputFontWeight = (0, _figmaVariablesResolver.getVariableByName)('formField/input/label/fontWeight', modes) || '400';
55
63
  const inputTextColor = (0, _figmaVariablesResolver.getVariableByName)('states/formField/input/label/color', modes) || (0, _figmaVariablesResolver.getVariableByName)('formField/input/label/color', modes) || '#24262b';
56
64
  const inputBorderColor = (0, _figmaVariablesResolver.getVariableByName)('states/formField/input/border/color', modes) || (0, _figmaVariablesResolver.getVariableByName)('formField/input/border/color', modes) || '#b5b6b7';
57
- const inputBorderSize = parseInt((0, _figmaVariablesResolver.getVariableByName)('formField/input/border/size', modes), 10) || 1;
65
+ // Figma spec: 1.5px. Using parseFloat (via toNumber) preserves the
66
+ // fractional value — parseInt was truncating it to 1, leaving the
67
+ // resolved row height ~1px shorter than the Figma reference.
68
+ const inputBorderSize = toNumber((0, _figmaVariablesResolver.getVariableByName)('formField/input/border/size', modes), 1.5);
58
69
  return {
59
70
  labelColor,
60
71
  labelFontFamily,
@@ -134,7 +145,7 @@ function DropdownInput({
134
145
  supportText,
135
146
  errorMessage,
136
147
  menuMaxHeight = 240,
137
- menuOffset = 4,
148
+ menuOffset = 6,
138
149
  matchTriggerWidth = true,
139
150
  closeOnBackdropPress = true,
140
151
  modes: propModes = _reactUtils.EMPTY_MODES,
@@ -340,19 +351,23 @@ function DropdownInput({
340
351
  };
341
352
 
342
353
  // Focus ring uses the resolved input border color from FormField States so
343
- // active/error look consistent with TextInput-based FormField. We also lift
344
- // border weight to 2 when "Active" to read as a focus ring.
354
+ // active/error look consistent with TextInput-based FormField. Only the
355
+ // color changes between states width stays constant to avoid layout
356
+ // shift when opening the menu (a shift would invalidate the measured
357
+ // trigger rect and visually shove the popup).
345
358
  const inputRowStyle = {
346
359
  flexDirection: 'row',
347
360
  alignItems: 'center',
348
361
  backgroundColor: tokens.inputBackground,
349
362
  borderColor: tokens.inputBorderColor,
350
- borderWidth: isOpen ? Math.max(tokens.inputBorderSize, 1) : tokens.inputBorderSize,
363
+ borderWidth: tokens.inputBorderSize,
364
+ borderStyle: 'solid',
351
365
  borderRadius: tokens.inputRadius,
352
366
  paddingHorizontal: tokens.inputPaddingH,
353
367
  paddingVertical: 0,
354
368
  gap: tokens.inputGap,
355
- minHeight: tokens.inputLineHeight
369
+ minHeight: tokens.inputLineHeight,
370
+ width: '100%'
356
371
  };
357
372
  const valueTextStyle = {
358
373
  flex: 1,
@@ -500,7 +515,6 @@ function DropdownInput({
500
515
  transparent: true,
501
516
  animationType: "fade",
502
517
  onRequestClose: closeMenu,
503
- statusBarTranslucent: true,
504
518
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
505
519
  style: _reactNative.StyleSheet.absoluteFill,
506
520
  onPress: closeOnBackdropPress ? closeMenu : undefined,