jfs-components 0.0.77 → 0.0.78

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 (70) hide show
  1. package/CHANGELOG.md +17 -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/Checkbox/Checkbox.js +21 -9
  5. package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -16
  6. package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +167 -0
  7. package/lib/commonjs/components/FormField/FormField.js +14 -1
  8. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +355 -0
  9. package/lib/commonjs/components/ListItem/ListItem.js +25 -10
  10. package/lib/commonjs/components/MessageField/MessageField.js +318 -0
  11. package/lib/commonjs/components/NavArrow/NavArrow.js +58 -17
  12. package/lib/commonjs/components/Stepper/Step.js +47 -60
  13. package/lib/commonjs/components/Stepper/StepLabel.js +40 -10
  14. package/lib/commonjs/components/Stepper/Stepper.js +15 -17
  15. package/lib/commonjs/components/SuggestiveSearch/SuggestiveSearch.js +487 -0
  16. package/lib/commonjs/components/TextInput/TextInput.js +16 -1
  17. package/lib/commonjs/components/Title/Title.js +10 -2
  18. package/lib/commonjs/components/index.js +28 -0
  19. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  20. package/lib/commonjs/icons/registry.js +1 -1
  21. package/lib/module/components/Accordion/Accordion.js +56 -56
  22. package/lib/module/components/ActionFooter/ActionFooter.js +50 -4
  23. package/lib/module/components/Checkbox/Checkbox.js +22 -10
  24. package/lib/module/components/DropdownInput/DropdownInput.js +30 -16
  25. package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +161 -0
  26. package/lib/module/components/FormField/FormField.js +16 -3
  27. package/lib/module/components/FullscreenModal/FullscreenModal.js +350 -0
  28. package/lib/module/components/ListItem/ListItem.js +25 -10
  29. package/lib/module/components/MessageField/MessageField.js +313 -0
  30. package/lib/module/components/NavArrow/NavArrow.js +59 -18
  31. package/lib/module/components/Stepper/Step.js +48 -61
  32. package/lib/module/components/Stepper/StepLabel.js +40 -10
  33. package/lib/module/components/Stepper/Stepper.js +15 -17
  34. package/lib/module/components/SuggestiveSearch/SuggestiveSearch.js +481 -0
  35. package/lib/module/components/TextInput/TextInput.js +17 -2
  36. package/lib/module/components/Title/Title.js +10 -2
  37. package/lib/module/components/index.js +4 -0
  38. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  39. package/lib/module/icons/registry.js +1 -1
  40. package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -20
  41. package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +63 -0
  42. package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +99 -0
  43. package/lib/typescript/src/components/MessageField/MessageField.d.ts +81 -0
  44. package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +10 -5
  45. package/lib/typescript/src/components/Stepper/Step.d.ts +4 -1
  46. package/lib/typescript/src/components/Stepper/StepLabel.d.ts +4 -1
  47. package/lib/typescript/src/components/Stepper/Stepper.d.ts +3 -1
  48. package/lib/typescript/src/components/SuggestiveSearch/SuggestiveSearch.d.ts +123 -0
  49. package/lib/typescript/src/components/index.d.ts +7 -3
  50. package/lib/typescript/src/icons/registry.d.ts +1 -1
  51. package/package.json +1 -1
  52. package/src/components/Accordion/Accordion.tsx +113 -73
  53. package/src/components/ActionFooter/ActionFooter.tsx +56 -4
  54. package/src/components/Checkbox/Checkbox.tsx +22 -9
  55. package/src/components/DropdownInput/DropdownInput.tsx +67 -39
  56. package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +237 -0
  57. package/src/components/FormField/FormField.tsx +19 -3
  58. package/src/components/FullscreenModal/FullscreenModal.tsx +414 -0
  59. package/src/components/ListItem/ListItem.tsx +21 -10
  60. package/src/components/MessageField/MessageField.tsx +543 -0
  61. package/src/components/NavArrow/NavArrow.tsx +81 -17
  62. package/src/components/Stepper/Step.tsx +52 -51
  63. package/src/components/Stepper/StepLabel.tsx +46 -9
  64. package/src/components/Stepper/Stepper.tsx +20 -15
  65. package/src/components/SuggestiveSearch/SuggestiveSearch.tsx +756 -0
  66. package/src/components/TextInput/TextInput.tsx +14 -1
  67. package/src/components/Title/Title.tsx +13 -2
  68. package/src/components/index.ts +7 -3
  69. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  70. package/src/icons/registry.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@ 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.78] - 2026-05-29
8
+
9
+ - Added `ExpandableCheckbox` — checkbox row with collapsible long label and Read more / Read less toggle.
10
+ - Added `FullscreenModal` — full-screen modal with parallax hero, close button, disclaimer, and action footer slots.
11
+ - Added `MessageField` — multi-line textarea with FormField States (Idle / Active / Read Only / Error / Disabled) and Form context integration.
12
+ - Added `SuggestiveSearch` — search input with inline suggestion dropdown.
13
+ - `Accordion` — new `contained` variant and hover state handling.
14
+ - `NavArrow` — pressable when `onPress` is provided; 44 pt touch target.
15
+ - `Checkbox` — 44 pt touch target; checkmark rendered inside the box view.
16
+ - `FormField` / `TextInput` — tap anywhere in the input row to focus on first tap (Android double-tap fix).
17
+ - `ActionFooter` — Android keyboard counter-shift so the footer stays behind the keyboard instead of jumping up.
18
+ - `ListItem` — title/support text use `AppearanceBrand: 'Neutral'` without forcing it onto slot children.
19
+ - `Stepper` / `Step` / `StepLabel` — `showLine`, `metaText`, `meta` props; larger indicator sizing; public type exports.
20
+ - `Title` — line-height clamp to prevent descender clipping on Android.
21
+
22
+ ---
23
+
7
24
  ## [0.0.77] - 2026-05-25
8
25
 
9
26
  ### 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, {
@@ -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,
@@ -0,0 +1,167 @@
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 _figmaVariablesResolver = require("../../design-tokens/figma-variables-resolver");
10
+ var _reactUtils = require("../../utils/react-utils");
11
+ var _Checkbox = _interopRequireDefault(require("../Checkbox/Checkbox"));
12
+ var _Button = _interopRequireDefault(require("../Button/Button"));
13
+ var _jsxRuntime = require("react/jsx-runtime");
14
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
15
+ 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); }
16
+ /**
17
+ * Default modes applied to the inner toggle `Button`. These resolve the
18
+ * tertiary-style pill in the Figma reference (small, transparent background,
19
+ * brand purple foreground). Any value supplied via the consumer `modes` prop
20
+ * takes precedence over these defaults.
21
+ */
22
+ const BUTTON_DEFAULT_MODES = {
23
+ 'Button / Size': 'XS',
24
+ AppearanceBrand: 'Secondary',
25
+ Emphasis: 'Low'
26
+ };
27
+
28
+ /**
29
+ * ExpandableCheckbox composes a `Checkbox`, a long-form label and a
30
+ * "Read more" / "Read less" toggle. Mirrors the Figma "Expandable Checkbox"
31
+ * component with two states:
32
+ *
33
+ * - **Idle (collapsed)** — checkbox + truncated label + toggle button arranged
34
+ * in a horizontal row (cross-axis centered).
35
+ * - **Open (expanded)** — checkbox + full multi-line label, with the toggle
36
+ * button right-aligned beneath the row.
37
+ *
38
+ * The checkbox and the toggle button have independent press handlers — pressing
39
+ * the toggle does not affect the checked state, and toggling the checkbox does
40
+ * not collapse / expand the row.
41
+ *
42
+ * @component
43
+ * @param {ExpandableCheckboxProps} props
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * <ExpandableCheckbox
48
+ * label="By checking this box, I (a) acknowledge and (b) agree to the full terms…"
49
+ * defaultChecked
50
+ * onValueChange={setAccepted}
51
+ * modes={{ 'Color Mode': 'Light' }}
52
+ * />
53
+ * ```
54
+ */
55
+ function ExpandableCheckbox({
56
+ label = '',
57
+ checked: controlledChecked,
58
+ defaultChecked = false,
59
+ onValueChange,
60
+ expanded: controlledExpanded,
61
+ defaultExpanded = false,
62
+ onExpandedChange,
63
+ disabled = false,
64
+ readMoreLabel = 'Read more',
65
+ readLessLabel = 'Read less',
66
+ collapsedLines = 1,
67
+ modes = _reactUtils.EMPTY_MODES,
68
+ style,
69
+ labelStyle,
70
+ accessibilityLabel
71
+ }) {
72
+ const isCheckedControlled = controlledChecked !== undefined;
73
+ const [internalChecked, setInternalChecked] = (0, _react.useState)(defaultChecked);
74
+ const isChecked = isCheckedControlled ? controlledChecked : internalChecked;
75
+ const isExpandedControlled = controlledExpanded !== undefined;
76
+ const [internalExpanded, setInternalExpanded] = (0, _react.useState)(defaultExpanded);
77
+ const isExpanded = isExpandedControlled ? controlledExpanded : internalExpanded;
78
+ const handleToggleChecked = (0, _react.useCallback)(next => {
79
+ if (disabled) return;
80
+ if (!isCheckedControlled) setInternalChecked(next);
81
+ onValueChange?.(next);
82
+ }, [disabled, isCheckedControlled, onValueChange]);
83
+ const handleToggleExpanded = (0, _react.useCallback)(() => {
84
+ if (disabled) return;
85
+ const next = !isExpanded;
86
+ if (!isExpandedControlled) setInternalExpanded(next);
87
+ onExpandedChange?.(next);
88
+ }, [disabled, isExpanded, isExpandedControlled, onExpandedChange]);
89
+ const gap = (0, _figmaVariablesResolver.getVariableByName)('expandableCheckbox/gap', modes) ?? 8;
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
+ const labelColor = (0, _figmaVariablesResolver.getVariableByName)('checkboxItem/foreground', modes) ?? '#1a1c1f';
94
+ const labelFontFamily = (0, _figmaVariablesResolver.getVariableByName)('checkboxItem/label/fontFamily', modes) ?? 'JioType Var';
95
+ const labelFontSize = (0, _figmaVariablesResolver.getVariableByName)('checkboxItem/label/fontSize', modes) ?? 14;
96
+ const labelLineHeight = (0, _figmaVariablesResolver.getVariableByName)('checkboxItem/label/lineHeight', modes) ?? 19;
97
+ const labelFontWeightRaw = (0, _figmaVariablesResolver.getVariableByName)('checkboxItem/label/fontWeight', modes) ?? 400;
98
+ const labelFontWeight = String(labelFontWeightRaw);
99
+ const containerStyle = (0, _react.useMemo)(() => ({
100
+ flexDirection: isExpanded ? 'column' : 'row',
101
+ alignItems: isExpanded ? 'flex-end' : 'center',
102
+ gap,
103
+ width: '100%',
104
+ ...(disabled ? {
105
+ opacity: 0.6
106
+ } : null)
107
+ }), [isExpanded, gap, disabled]);
108
+ const rowStyle = (0, _react.useMemo)(() => ({
109
+ flex: isExpanded ? undefined : 1,
110
+ alignSelf: isExpanded ? 'stretch' : 'auto',
111
+ minWidth: 0,
112
+ flexDirection: 'row',
113
+ alignItems: 'flex-start',
114
+ gap: rowGap,
115
+ paddingHorizontal: rowPaddingHorizontal,
116
+ paddingVertical: rowPaddingVertical
117
+ }), [isExpanded, rowGap, rowPaddingHorizontal, rowPaddingVertical]);
118
+ const resolvedLabelStyle = (0, _react.useMemo)(() => ({
119
+ flex: 1,
120
+ minWidth: 0,
121
+ color: labelColor,
122
+ fontFamily: labelFontFamily,
123
+ fontSize: labelFontSize,
124
+ lineHeight: labelLineHeight,
125
+ fontWeight: labelFontWeight
126
+ }), [labelColor, labelFontFamily, labelFontSize, labelLineHeight, labelFontWeight]);
127
+ const buttonModes = (0, _react.useMemo)(() => ({
128
+ ...BUTTON_DEFAULT_MODES,
129
+ ...modes
130
+ }), [modes]);
131
+ const a11yLabel = accessibilityLabel ?? (typeof label === 'string' ? label : undefined);
132
+ const buttonLabel = isExpanded ? readLessLabel : readMoreLabel;
133
+ const labelNumberOfLinesProps = !isExpanded && collapsedLines > 0 ? {
134
+ numberOfLines: collapsedLines,
135
+ ellipsizeMode: 'tail'
136
+ } : null;
137
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
138
+ style: [containerStyle, style],
139
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
140
+ style: rowStyle,
141
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_Checkbox.default, {
142
+ checked: isChecked,
143
+ disabled: disabled,
144
+ onValueChange: handleToggleChecked,
145
+ modes: modes,
146
+ ...(a11yLabel !== undefined ? {
147
+ accessibilityLabel: a11yLabel
148
+ } : {})
149
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
150
+ style: [resolvedLabelStyle, labelStyle],
151
+ selectable: false,
152
+ ...(labelNumberOfLinesProps ?? {}),
153
+ children: label
154
+ })]
155
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_Button.default, {
156
+ label: buttonLabel,
157
+ onPress: handleToggleExpanded,
158
+ disabled: disabled,
159
+ modes: buttonModes,
160
+ accessibilityLabel: buttonLabel,
161
+ accessibilityState: {
162
+ expanded: isExpanded
163
+ }
164
+ })]
165
+ });
166
+ }
167
+ var _default = exports.default = ExpandableCheckbox;
@@ -208,6 +208,16 @@ 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
+
211
221
  // FormField States cascade — error > read only/disabled > active (focused) > idle.
212
222
  // Disabled maps to "Read Only" since there is no dedicated disabled mode and
213
223
  // the visual treatment is closest. This is only the DEFAULT — an explicit
@@ -340,13 +350,16 @@ function FormField({
340
350
  style: requiredIndicatorStyle,
341
351
  children: " *"
342
352
  })]
343
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
353
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
344
354
  style: [inputRowStyle, inputStyle],
355
+ onPress: focusInput,
356
+ accessible: false,
345
357
  children: [processedLeading != null && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
346
358
  accessibilityElementsHidden: true,
347
359
  importantForAccessibility: "no",
348
360
  children: processedLeading
349
361
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TextInput, {
362
+ ref: inputRef,
350
363
  style: [inputTextStyles, inputTextStyle],
351
364
  value: value ?? '',
352
365
  onChangeText: handleChangeText,