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
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- import React, { useState } from 'react';
3
+ import React, { useMemo, useState } from 'react';
4
4
  import { View, Text, Pressable, LayoutAnimation, Platform, UIManager } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import Icon from '../../icons/Icon';
@@ -12,34 +12,45 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
12
  if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
13
13
  UIManager.setLayoutAnimationEnabledExperimental(true);
14
14
  }
15
+ function resolveAccordionStateMode(disabled, isExpanded, isHovered, contained) {
16
+ if (disabled) return 'Disabled';
17
+ if (contained) {
18
+ return isExpanded ? 'Open Hover' : 'Hover';
19
+ }
20
+ if (isExpanded) {
21
+ return isHovered ? 'Open Hover' : 'Open';
22
+ }
23
+ return isHovered ? 'Hover' : 'Idle';
24
+ }
25
+ function toFontWeight(value, fallback) {
26
+ if (typeof value === 'number') return String(value);
27
+ if (typeof value === 'string') {
28
+ const normalized = value.trim().toLowerCase();
29
+ if (normalized === 'bold') return '700';
30
+ if (normalized === 'medium') return '500';
31
+ if (normalized === 'regular' || normalized === 'normal') return '400';
32
+ if (/^\d+$/.test(normalized)) return normalized;
33
+ return value;
34
+ }
35
+ return fallback;
36
+ }
15
37
  /**
16
38
  * Accordion component that mirrors the Figma "Accordion" component.
17
39
  *
18
- * This component supports:
19
- * - **Expandable/collapsible content** with smooth animation
20
- * - **States**: Idle, Hover, Open, Disabled
21
- * - **Slot** for custom content
22
- * - **Design-token driven styling** via `getVariableByName` and `modes`
40
+ * Supports two visual treatments via the `contained` prop:
41
+ * - **`contained={false}`** (default) transparent header at rest; filled
42
+ * background on hover / press.
43
+ * - **`contained={true}`** header always uses the filled background.
23
44
  *
24
- * Wherever the Figma layer name contains "Slot", this component exposes a
25
- * dedicated React "slot" prop:
26
- * - Slot "content" `children`
45
+ * Interaction states (Idle, Hover, Open, Disabled) are resolved automatically
46
+ * from `expanded`, `disabled`, hover, and `contained` — consumers should not
47
+ * pass `'Accordion States'` in `modes`.
27
48
  *
28
49
  * @component
29
- * @param {Object} props
30
- * @param {string} [props.title='Accordion title'] - The accordion header title
31
- * @param {boolean} [props.defaultExpanded=false] - Initial expanded state
32
- * @param {boolean} [props.expanded] - Controlled expanded state
33
- * @param {Function} [props.onExpandedChange] - Callback fired when expanded state changes
34
- * @param {boolean} [props.disabled=false] - Whether the accordion is disabled
35
- * @param {React.ReactNode} [props.children] - Content to display when expanded
36
- * @param {Object} [props.modes={}] - Modes object passed to `getVariableByName` for all design tokens
37
- * @param {Object} [props.style] - Optional container style overrides
38
- * @param {string} [props.accessibilityLabel] - Accessibility label for the accordion. If not provided, uses title
39
- * @param {string} [props.accessibilityHint] - Additional accessibility hint for screen readers
40
50
  */
41
51
  function Accordion({
42
52
  title = 'Accordion title',
53
+ contained = false,
43
54
  defaultExpanded = false,
44
55
  expanded: controlledExpanded,
45
56
  onExpandedChange,
@@ -53,21 +64,19 @@ function Accordion({
53
64
  webAccessibilityProps,
54
65
  ...rest
55
66
  }) {
56
- // Internal state for uncontrolled mode
57
67
  const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
58
-
59
- // Determine if controlled or uncontrolled
68
+ const [isHovered, setIsHovered] = useState(false);
60
69
  const isControlled = controlledExpanded !== undefined;
61
70
  const isExpanded = isControlled ? controlledExpanded : internalExpanded;
62
-
63
- // Hover state for web
64
- const [isHovered, setIsHovered] = useState(false);
65
-
66
- // Handle toggle
71
+ const resolvedModes = useMemo(() => {
72
+ const accordionState = resolveAccordionStateMode(disabled, isExpanded, isHovered, contained);
73
+ return {
74
+ ...modes,
75
+ 'Accordion States': accordionState
76
+ };
77
+ }, [contained, disabled, isExpanded, isHovered, modes]);
67
78
  const handleToggle = () => {
68
79
  if (disabled) return;
69
-
70
- // Animate the layout change
71
80
  LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
72
81
  if (isControlled) {
73
82
  onExpandedChange?.(!isExpanded);
@@ -76,23 +85,20 @@ function Accordion({
76
85
  onExpandedChange?.(!isExpanded);
77
86
  }
78
87
  };
79
-
80
- // Resolve design tokens
81
- const titleColor = disabled ? '#999999' : getVariableByName('accordion/title/color', modes) || '#0d0d0d';
82
- const titleFontSize = getVariableByName('accordion/title/fontSize', modes) || 18;
83
- const titleLineHeight = getVariableByName('accordion/title/lineHeight', modes) || 20;
84
- const titleFontFamily = getVariableByName('accordion/title/fontFamily', modes) || 'System';
85
- const iconColor = getVariableByName('accordion/icon/color', modes) || '#141414';
86
- const iconSize = getVariableByName('accordion/icon/size', modes) || 24;
87
- const headerGap = getVariableByName('accordion/header/gap', modes) || 12;
88
- const headerPaddingVertical = getVariableByName('accordion/header/padding/vertical', modes) || 24;
89
- const headerBackground = isHovered && !disabled ? '#f2f2f2' : getVariableByName('accordion/header/background', modes) || 'transparent';
90
- const contentGap = getVariableByName('accordion/content/gap', modes) || 12;
91
- const contentPaddingTop = getVariableByName('accordion/content/padding/top', modes) || 8;
92
- const contentPaddingBottom = isExpanded ? getVariableByName('accordion/content/padding/bottom', modes) ?? 24 : 8;
93
- const borderColor = getVariableByName('accordion/border/color', modes) || '#e6e6e6';
94
-
95
- // Styles
88
+ const titleColor = getVariableByName('accordion/title/color', resolvedModes) ?? '#0d0d0d';
89
+ const titleFontSize = getVariableByName('accordion/title/fontSize', resolvedModes) ?? 14;
90
+ const titleLineHeight = getVariableByName('accordion/title/lineHeight', resolvedModes) ?? 20;
91
+ const titleFontFamily = getVariableByName('accordion/title/fontFamily', resolvedModes) ?? 'System';
92
+ const titleFontWeight = toFontWeight(getVariableByName('accordion/title/fontWeight', resolvedModes), '700');
93
+ const iconColor = getVariableByName('accordion/icon/color', resolvedModes) ?? '#141414';
94
+ const iconSize = getVariableByName('accordion/icon/size', resolvedModes) ?? 24;
95
+ const headerGap = getVariableByName('accordion/header/gap', resolvedModes) ?? 12;
96
+ const headerPaddingVertical = getVariableByName('accordion/header/padding/vertical', resolvedModes) ?? 8;
97
+ const headerBackground = getVariableByName('accordion/header/background', resolvedModes) ?? 'transparent';
98
+ const contentGap = getVariableByName('accordion/content/gap', resolvedModes) ?? 12;
99
+ const contentPaddingTop = getVariableByName('accordion/content/padding/top', resolvedModes) ?? 8;
100
+ const contentPaddingBottom = getVariableByName('accordion/content/padding/bottom', resolvedModes) ?? 8;
101
+ const borderColor = getVariableByName('accordion/border/color', resolvedModes) ?? '#e6e6e6';
96
102
  const containerStyle = {
97
103
  borderBottomWidth: 1,
98
104
  borderBottomColor: borderColor
@@ -112,7 +118,7 @@ function Accordion({
112
118
  fontSize: titleFontSize,
113
119
  lineHeight: titleLineHeight,
114
120
  fontFamily: titleFontFamily,
115
- fontWeight: '700'
121
+ fontWeight: titleFontWeight
116
122
  };
117
123
  const contentStyle = {
118
124
  backgroundColor: 'transparent',
@@ -122,11 +128,7 @@ function Accordion({
122
128
  paddingHorizontal: 0,
123
129
  overflow: 'hidden'
124
130
  };
125
-
126
- // Generate default accessibility label
127
131
  const defaultAccessibilityLabel = accessibilityLabel || title;
128
-
129
- // Web platform support
130
132
  const webProps = usePressableWebSupport({
131
133
  restProps: {},
132
134
  onPress: handleToggle,
@@ -134,9 +136,7 @@ function Accordion({
134
136
  accessibilityLabel: defaultAccessibilityLabel,
135
137
  webAccessibilityProps
136
138
  });
137
-
138
- // Process children to pass modes
139
- const processedChildren = children ? cloneChildrenWithModes(React.Children.toArray(children), modes) : null;
139
+ const processedChildren = children ? cloneChildrenWithModes(React.Children.toArray(children), resolvedModes) : null;
140
140
  return /*#__PURE__*/_jsxs(View, {
141
141
  style: [containerStyle, style],
142
142
  ...rest,
@@ -166,7 +166,7 @@ function Accordion({
166
166
  }), /*#__PURE__*/_jsx(Icon, {
167
167
  name: isExpanded ? 'ic_minus' : 'ic_add',
168
168
  size: iconSize,
169
- color: disabled ? '#999999' : iconColor,
169
+ color: iconColor,
170
170
  accessibilityElementsHidden: true,
171
171
  importantForAccessibility: "no"
172
172
  })]
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
- import React, { useMemo } from 'react';
4
- import { View, Platform } from 'react-native';
3
+ import React, { useEffect, useMemo, useRef } from 'react';
4
+ import { Animated, Keyboard, View, Platform } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import { EMPTY_MODES, cloneChildrenWithModes, flattenChildren } from '../../utils/react-utils';
7
7
  import IconButton from '../IconButton/IconButton';
@@ -105,6 +105,44 @@ function ActionFooter({
105
105
  style,
106
106
  accessibilityLabel
107
107
  }) {
108
+ // -------------------------------------------------------------------------
109
+ // Keep the footer locked in place behind the software keyboard (Android).
110
+ // -------------------------------------------------------------------------
111
+ //
112
+ // The Android activity is configured with `windowSoftInputMode="adjustResize"`,
113
+ // which shrinks the app window by the keyboard height when the keyboard
114
+ // opens. A bottom-anchored footer therefore gets lifted UP by the keyboard
115
+ // height — exactly the jump the design does not want.
116
+ //
117
+ // To counteract that, we translate the footer back DOWN by the same keyboard
118
+ // height so it visually stays exactly where it was (now sitting behind the
119
+ // keyboard). iOS does not resize the window for the keyboard, so the footer
120
+ // already stays put there; we only run this on Android to avoid pushing the
121
+ // footer off-screen on platforms that don't lift it in the first place.
122
+ const keyboardOffset = useRef(new Animated.Value(0)).current;
123
+ useEffect(() => {
124
+ if (Platform.OS !== 'android') return undefined;
125
+ const animateTo = (toValue, duration) => {
126
+ Animated.timing(keyboardOffset, {
127
+ toValue,
128
+ // Match the OS keyboard animation so the resize and our counter-shift
129
+ // cancel out smoothly with no visible footer movement.
130
+ duration: typeof duration === 'number' && duration > 0 ? duration : 150,
131
+ useNativeDriver: true
132
+ }).start();
133
+ };
134
+ const showSub = Keyboard.addListener('keyboardDidShow', e => {
135
+ animateTo(e?.endCoordinates?.height ?? 0, e?.duration);
136
+ });
137
+ const hideSub = Keyboard.addListener('keyboardDidHide', e => {
138
+ animateTo(0, e?.duration);
139
+ });
140
+ return () => {
141
+ showSub.remove();
142
+ hideSub.remove();
143
+ };
144
+ }, [keyboardOffset]);
145
+
108
146
  // All token reads collapsed into a single useMemo keyed on `modes`. With
109
147
  // the shared `EMPTY_MODES` default this resolves once for the common path
110
148
  // and never re-allocates the container/slot style objects between renders.
@@ -163,8 +201,16 @@ function ActionFooter({
163
201
  });
164
202
  });
165
203
  }, [children, modes]);
166
- return /*#__PURE__*/_jsx(View, {
167
- style: [containerStyle, WEB_SHADOW, style],
204
+ return /*#__PURE__*/_jsx(Animated.View, {
205
+ style: [containerStyle, WEB_SHADOW, style,
206
+ // Counter-translate by the keyboard height on Android so `adjustResize`
207
+ // can't lift the footer above the keyboard (no-op on iOS/web where the
208
+ // value stays at 0).
209
+ {
210
+ transform: [{
211
+ translateY: keyboardOffset
212
+ }]
213
+ }],
168
214
  accessibilityRole: "toolbar",
169
215
  accessibilityLabel: accessibilityLabel,
170
216
  children: /*#__PURE__*/_jsx(View, {
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+
3
+ import React, { useCallback, useMemo, useState } from 'react';
4
+ import { View } from 'react-native';
5
+ import { useTokens } from '../../design-tokens/JFSThemeProvider';
6
+ import { cloneChildrenWithModes, EMPTY_MODES } from '../../utils/react-utils';
7
+
8
+ /**
9
+ * Anchor point on the main content where the attached `badge` is centered.
10
+ * Mirrors the nine Figma `position` variants (corners, edge midpoints, center).
11
+ */
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ const ZERO_SIZE = {
14
+ width: 0,
15
+ height: 0
16
+ };
17
+
18
+ /**
19
+ * Fraction (0 | 0.5 | 1) of the main content's width/height at which the badge
20
+ * center should sit, derived from the `position` anchor.
21
+ */
22
+ function resolveAnchorFractions(position) {
23
+ const fx = position.includes('left') ? 0 : position.includes('right') ? 1 : 0.5;
24
+ const fy = position.startsWith('top') ? 0 : position.startsWith('bottom') ? 1 : 0.5;
25
+ return {
26
+ fx,
27
+ fy
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Attached — overlays a small `badge` on top of arbitrary main content,
33
+ * centered on one of nine anchor points (corners, edge midpoints, or center).
34
+ *
35
+ * The badge straddles the chosen anchor regardless of either element's size:
36
+ * both the main content and the badge are measured via `onLayout`, then the
37
+ * badge is absolutely positioned so its center lands exactly on the anchor.
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * <Attached position="bottom-right" badge={<InstitutionBadge modes={modes} />} modes={modes}>
42
+ * <IconCapsule iconName="ic_card" modes={modes} />
43
+ * </Attached>
44
+ * ```
45
+ */
46
+ function Attached({
47
+ children,
48
+ badge,
49
+ position = 'bottom-right',
50
+ circular = true,
51
+ modes: propModes = EMPTY_MODES,
52
+ style,
53
+ ...rest
54
+ }) {
55
+ const {
56
+ modes: globalModes
57
+ } = useTokens();
58
+ const modes = useMemo(() => globalModes === EMPTY_MODES && propModes === EMPTY_MODES ? EMPTY_MODES : {
59
+ ...globalModes,
60
+ ...propModes
61
+ }, [globalModes, propModes]);
62
+ const [mainSize, setMainSize] = useState(ZERO_SIZE);
63
+ const [badgeSize, setBadgeSize] = useState(ZERO_SIZE);
64
+ const onMainLayout = useCallback(e => {
65
+ const {
66
+ width,
67
+ height
68
+ } = e.nativeEvent.layout;
69
+ setMainSize(prev => prev.width === width && prev.height === height ? prev : {
70
+ width,
71
+ height
72
+ });
73
+ }, []);
74
+ const onBadgeLayout = useCallback(e => {
75
+ const {
76
+ width,
77
+ height
78
+ } = e.nativeEvent.layout;
79
+ setBadgeSize(prev => prev.width === width && prev.height === height ? prev : {
80
+ width,
81
+ height
82
+ });
83
+ }, []);
84
+ const mainChildren = useMemo(() => children != null ? cloneChildrenWithModes(children, modes) : null, [children, modes]);
85
+ const badgeChildren = useMemo(() => badge != null ? cloneChildrenWithModes(badge, modes) : null, [badge, modes]);
86
+ const badgePlacement = useMemo(() => {
87
+ const {
88
+ fx,
89
+ fy
90
+ } = resolveAnchorFractions(position);
91
+ const measured = mainSize.width > 0 && badgeSize.width > 0;
92
+ let anchorX;
93
+ let anchorY;
94
+ if (circular) {
95
+ // Project the anchor onto the circle inscribed in the bounding box, so
96
+ // corner badges land on the circumference (45°) instead of the box corner.
97
+ const cx = mainSize.width / 2;
98
+ const cy = mainSize.height / 2;
99
+ const radius = Math.min(mainSize.width, mainSize.height) / 2;
100
+ const dx = (fx - 0.5) * 2; // -1 | 0 | 1
101
+ const dy = (fy - 0.5) * 2; // -1 | 0 | 1
102
+ const len = Math.hypot(dx, dy) || 1; // 'center' → 0, guard against /0
103
+ anchorX = cx + dx / len * radius;
104
+ anchorY = cy + dy / len * radius;
105
+ } else {
106
+ anchorX = mainSize.width * fx;
107
+ anchorY = mainSize.height * fy;
108
+ }
109
+ return {
110
+ position: 'absolute',
111
+ left: anchorX - badgeSize.width / 2,
112
+ top: anchorY - badgeSize.height / 2,
113
+ // Hide until both elements are measured to avoid a one-frame flash at (0,0).
114
+ opacity: measured ? 1 : 0
115
+ };
116
+ }, [position, circular, mainSize, badgeSize]);
117
+ return /*#__PURE__*/_jsxs(View, {
118
+ style: [styles.container, style],
119
+ ...rest,
120
+ children: [/*#__PURE__*/_jsx(View, {
121
+ onLayout: onMainLayout,
122
+ children: mainChildren
123
+ }), badgeChildren != null && /*#__PURE__*/_jsx(View, {
124
+ style: badgePlacement,
125
+ onLayout: onBadgeLayout,
126
+ pointerEvents: "box-none",
127
+ children: badgeChildren
128
+ })]
129
+ });
130
+ }
131
+ const styles = {
132
+ // alignSelf flex-start so the wrapper hugs the main content; anchors are then
133
+ // computed relative to the content size rather than a stretched parent.
134
+ container: {
135
+ position: 'relative',
136
+ alignSelf: 'flex-start'
137
+ }
138
+ };
139
+ export default /*#__PURE__*/React.memo(Attached);
@@ -16,9 +16,11 @@ const CardContext = /*#__PURE__*/createContext({});
16
16
  * Card component implementation from Figma node 765:6186.
17
17
  *
18
18
  * Supports a `media` slot (with aspect ratio) and a content area.
19
+ * Supports an optional `header` slot (e.g. a brand logo), a `media` slot
20
+ * (with aspect ratio) and a content area.
19
21
  * Usage:
20
22
  * ```tsx
21
- * <Card media={<Image source={...} />} modes={modes}>
23
+ * <Card header={<GoldLogo />} media={<Image source={...} />} modes={modes}>
22
24
  * <Card.SupportText>Support text</Card.SupportText>
23
25
  * <Card.Title>Title</Card.Title>
24
26
  * <Card.SupportText>Support text</Card.SupportText>
@@ -26,6 +28,7 @@ const CardContext = /*#__PURE__*/createContext({});
26
28
  * ```
27
29
  */
28
30
  export function Card({
31
+ header,
29
32
  media,
30
33
  children,
31
34
  modes = EMPTY_MODES,
@@ -53,6 +56,14 @@ export function Card({
53
56
  ...modes
54
57
  }
55
58
  }) : media;
59
+
60
+ // Clone header to pass modes if it's a valid element
61
+ const headerWithModes = /*#__PURE__*/isValidElement(header) ? /*#__PURE__*/cloneElement(header, {
62
+ modes: {
63
+ ...header.props.modes,
64
+ ...modes
65
+ }
66
+ }) : header;
56
67
  const containerStyle = {
57
68
  backgroundColor,
58
69
  borderColor,
@@ -63,6 +74,15 @@ export function Card({
63
74
  paddingVertical,
64
75
  overflow: 'hidden' // Ensure border radius clips content
65
76
  };
77
+
78
+ // Header wrap uses fixed padding from Figma (no dedicated tokens defined).
79
+ const headerWrapperStyle = {
80
+ width: '100%',
81
+ flexDirection: 'row',
82
+ alignItems: 'flex-start',
83
+ paddingHorizontal: 12,
84
+ paddingVertical: 16
85
+ };
66
86
  const mediaWrapperStyle = {
67
87
  width: '100%',
68
88
  aspectRatio: mediaAspectRatio,
@@ -83,7 +103,10 @@ export function Card({
83
103
  },
84
104
  children: /*#__PURE__*/_jsxs(View, {
85
105
  style: [containerStyle, style],
86
- children: [media && /*#__PURE__*/_jsx(View, {
106
+ children: [header && /*#__PURE__*/_jsx(View, {
107
+ style: headerWrapperStyle,
108
+ children: headerWithModes
109
+ }), media && /*#__PURE__*/_jsx(View, {
87
110
  style: mediaWrapperStyle,
88
111
  children: mediaWithModes
89
112
  }), /*#__PURE__*/_jsx(View, {
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  import React, { useCallback, useEffect, useRef, useState } from 'react';
4
- import { Pressable, Platform } from 'react-native';
4
+ import { Pressable, Platform, View } from 'react-native';
5
5
  import Svg, { Path } from 'react-native-svg';
6
6
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
7
7
  import { EMPTY_MODES } from '../../utils/react-utils';
@@ -46,6 +46,15 @@ function useFocusVisible() {
46
46
  }
47
47
  };
48
48
  }
49
+
50
+ /** Minimum touch target per iOS HIG / Material accessibility guidance. */
51
+ const MIN_TOUCH_TARGET = 44;
52
+ const touchTargetStyle = {
53
+ minWidth: MIN_TOUCH_TARGET,
54
+ minHeight: MIN_TOUCH_TARGET,
55
+ alignItems: 'center',
56
+ justifyContent: 'center'
57
+ };
49
58
  /**
50
59
  * Checkbox component that maps directly to the Figma design using design tokens.
51
60
  *
@@ -171,7 +180,7 @@ function Checkbox({
171
180
  };
172
181
  const markColor = disabled && isChecked ? disabledActiveMark : selectedMarkColor;
173
182
  return /*#__PURE__*/_jsx(Pressable, {
174
- style: [resolveStyle(), style],
183
+ style: [touchTargetStyle, style],
175
184
  onPress: handlePress,
176
185
  disabled: disabled,
177
186
  onHoverIn: () => setIsHovered(true),
@@ -183,14 +192,17 @@ function Checkbox({
183
192
  disabled
184
193
  },
185
194
  accessibilityLabel: accessibilityLabel,
186
- children: isChecked && /*#__PURE__*/_jsx(Svg, {
187
- width: 12,
188
- height: 9,
189
- viewBox: "0 0 12 9",
190
- fill: "none",
191
- children: /*#__PURE__*/_jsx(Path, {
192
- 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",
193
- fill: markColor
195
+ children: /*#__PURE__*/_jsx(View, {
196
+ style: resolveStyle(),
197
+ children: isChecked && /*#__PURE__*/_jsx(Svg, {
198
+ width: 12,
199
+ height: 9,
200
+ viewBox: "0 0 12 9",
201
+ fill: "none",
202
+ children: /*#__PURE__*/_jsx(Path, {
203
+ 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",
204
+ fill: markColor
205
+ })
194
206
  })
195
207
  })
196
208
  });
@@ -30,25 +30,36 @@ function useChevronTokens(modes) {
30
30
  };
31
31
  }, [modes]);
32
32
  }
33
+ function toNumber(value, fallback) {
34
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
35
+ if (typeof value === 'string') {
36
+ const parsed = parseFloat(value);
37
+ if (Number.isFinite(parsed)) return parsed;
38
+ }
39
+ return fallback;
40
+ }
33
41
  function useFormFieldTokens(modes) {
34
42
  return useMemo(() => {
35
- const labelColor = getVariableByName('formField/label/color', modes) || '#0c0d10';
43
+ const labelColor = getVariableByName('formField/label/color', modes) || '#000000';
36
44
  const labelFontFamily = getVariableByName('formField/label/fontFamily', modes) || 'JioType Var';
37
- const labelFontSize = parseInt(getVariableByName('formField/label/fontSize', modes), 10) || 14;
38
- const labelLineHeight = parseInt(getVariableByName('formField/label/lineHeight', modes), 10) || 17;
45
+ const labelFontSize = toNumber(getVariableByName('formField/label/fontSize', modes), 14);
46
+ const labelLineHeight = toNumber(getVariableByName('formField/label/lineHeight', modes), 17);
39
47
  const labelFontWeight = getVariableByName('formField/label/fontWeight', modes) || '500';
40
- const gap = parseInt(getVariableByName('formField/gap', modes), 10) || 8;
41
- const inputPaddingH = parseInt(getVariableByName('formField/input/padding/horizontal', modes), 10) || 12;
42
- const inputGap = parseInt(getVariableByName('formField/input/gap', modes), 10) || 8;
43
- const inputRadius = parseInt(getVariableByName('formField/input/radius', modes), 10) || 8;
48
+ const gap = toNumber(getVariableByName('formField/gap', modes), 8);
49
+ const inputPaddingH = toNumber(getVariableByName('formField/input/padding/horizontal', modes), 12);
50
+ const inputGap = toNumber(getVariableByName('formField/input/gap', modes), 8);
51
+ const inputRadius = toNumber(getVariableByName('formField/input/radius', modes), 8);
44
52
  const inputBackground = getVariableByName('formField/input/background', modes) || '#ffffff';
45
- const inputFontSize = parseInt(getVariableByName('formField/input/label/fontSize', modes), 10) || 16;
46
- const inputLineHeight = parseInt(getVariableByName('formField/input/label/lineHeight', modes), 10) || 45;
53
+ const inputFontSize = toNumber(getVariableByName('formField/input/label/fontSize', modes), 16);
54
+ const inputLineHeight = toNumber(getVariableByName('formField/input/label/lineHeight', modes), 45);
47
55
  const inputFontFamily = getVariableByName('formField/input/label/fontFamily', modes) || 'JioType Var';
48
56
  const inputFontWeight = getVariableByName('formField/input/label/fontWeight', modes) || '400';
49
57
  const inputTextColor = getVariableByName('states/formField/input/label/color', modes) || getVariableByName('formField/input/label/color', modes) || '#24262b';
50
58
  const inputBorderColor = getVariableByName('states/formField/input/border/color', modes) || getVariableByName('formField/input/border/color', modes) || '#b5b6b7';
51
- const inputBorderSize = parseInt(getVariableByName('formField/input/border/size', modes), 10) || 1;
59
+ // Figma spec: 1.5px. Using parseFloat (via toNumber) preserves the
60
+ // fractional value — parseInt was truncating it to 1, leaving the
61
+ // resolved row height ~1px shorter than the Figma reference.
62
+ const inputBorderSize = toNumber(getVariableByName('formField/input/border/size', modes), 1.5);
52
63
  return {
53
64
  labelColor,
54
65
  labelFontFamily,
@@ -128,7 +139,7 @@ function DropdownInput({
128
139
  supportText,
129
140
  errorMessage,
130
141
  menuMaxHeight = 240,
131
- menuOffset = 4,
142
+ menuOffset = 6,
132
143
  matchTriggerWidth = true,
133
144
  closeOnBackdropPress = true,
134
145
  modes: propModes = EMPTY_MODES,
@@ -334,19 +345,23 @@ function DropdownInput({
334
345
  };
335
346
 
336
347
  // Focus ring uses the resolved input border color from FormField States so
337
- // active/error look consistent with TextInput-based FormField. We also lift
338
- // border weight to 2 when "Active" to read as a focus ring.
348
+ // active/error look consistent with TextInput-based FormField. Only the
349
+ // color changes between states width stays constant to avoid layout
350
+ // shift when opening the menu (a shift would invalidate the measured
351
+ // trigger rect and visually shove the popup).
339
352
  const inputRowStyle = {
340
353
  flexDirection: 'row',
341
354
  alignItems: 'center',
342
355
  backgroundColor: tokens.inputBackground,
343
356
  borderColor: tokens.inputBorderColor,
344
- borderWidth: isOpen ? Math.max(tokens.inputBorderSize, 1) : tokens.inputBorderSize,
357
+ borderWidth: tokens.inputBorderSize,
358
+ borderStyle: 'solid',
345
359
  borderRadius: tokens.inputRadius,
346
360
  paddingHorizontal: tokens.inputPaddingH,
347
361
  paddingVertical: 0,
348
362
  gap: tokens.inputGap,
349
- minHeight: tokens.inputLineHeight
363
+ minHeight: tokens.inputLineHeight,
364
+ width: '100%'
350
365
  };
351
366
  const valueTextStyle = {
352
367
  flex: 1,
@@ -494,7 +509,6 @@ function DropdownInput({
494
509
  transparent: true,
495
510
  animationType: "fade",
496
511
  onRequestClose: closeMenu,
497
- statusBarTranslucent: true,
498
512
  children: /*#__PURE__*/_jsx(Pressable, {
499
513
  style: StyleSheet.absoluteFill,
500
514
  onPress: closeOnBackdropPress ? closeMenu : undefined,