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.
- package/CHANGELOG.md +28 -0
- package/lib/commonjs/components/Accordion/Accordion.js +55 -55
- package/lib/commonjs/components/ActionFooter/ActionFooter.js +48 -2
- package/lib/commonjs/components/Attached/Attached.js +144 -0
- package/lib/commonjs/components/Card/Card.js +25 -2
- package/lib/commonjs/components/Checkbox/Checkbox.js +21 -9
- package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -16
- package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +167 -0
- package/lib/commonjs/components/FormField/FormField.js +14 -1
- package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +353 -0
- package/lib/commonjs/components/ListItem/ListItem.js +46 -24
- package/lib/commonjs/components/MessageField/MessageField.js +318 -0
- package/lib/commonjs/components/NavArrow/NavArrow.js +58 -17
- package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +328 -0
- package/lib/commonjs/components/Slot/Slot.js +73 -0
- package/lib/commonjs/components/Stepper/Step.js +47 -60
- package/lib/commonjs/components/Stepper/StepLabel.js +40 -10
- package/lib/commonjs/components/Stepper/Stepper.js +15 -17
- package/lib/commonjs/components/SuggestiveSearch/SuggestiveSearch.js +487 -0
- package/lib/commonjs/components/TextInput/TextInput.js +16 -1
- package/lib/commonjs/components/Title/Title.js +10 -2
- package/lib/commonjs/components/index.js +49 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/Accordion/Accordion.js +56 -56
- package/lib/module/components/ActionFooter/ActionFooter.js +50 -4
- package/lib/module/components/Attached/Attached.js +139 -0
- package/lib/module/components/Card/Card.js +25 -2
- package/lib/module/components/Checkbox/Checkbox.js +22 -10
- package/lib/module/components/DropdownInput/DropdownInput.js +30 -16
- package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +161 -0
- package/lib/module/components/FormField/FormField.js +16 -3
- package/lib/module/components/FullscreenModal/FullscreenModal.js +348 -0
- package/lib/module/components/ListItem/ListItem.js +46 -24
- package/lib/module/components/MessageField/MessageField.js +313 -0
- package/lib/module/components/NavArrow/NavArrow.js +59 -18
- package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +322 -0
- package/lib/module/components/Slot/Slot.js +68 -0
- package/lib/module/components/Stepper/Step.js +48 -61
- package/lib/module/components/Stepper/StepLabel.js +40 -10
- package/lib/module/components/Stepper/Stepper.js +15 -17
- package/lib/module/components/SuggestiveSearch/SuggestiveSearch.js +481 -0
- package/lib/module/components/TextInput/TextInput.js +17 -2
- package/lib/module/components/Title/Title.js +10 -2
- package/lib/module/components/index.js +7 -0
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -20
- package/lib/typescript/src/components/Attached/Attached.d.ts +61 -0
- package/lib/typescript/src/components/Card/Card.d.ts +9 -2
- package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +63 -0
- package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +99 -0
- package/lib/typescript/src/components/ListItem/ListItem.d.ts +15 -5
- package/lib/typescript/src/components/MessageField/MessageField.d.ts +81 -0
- package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +10 -5
- package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +64 -0
- package/lib/typescript/src/components/Slot/Slot.d.ts +52 -0
- package/lib/typescript/src/components/Stepper/Step.d.ts +4 -1
- package/lib/typescript/src/components/Stepper/StepLabel.d.ts +4 -1
- package/lib/typescript/src/components/Stepper/Stepper.d.ts +3 -1
- package/lib/typescript/src/components/SuggestiveSearch/SuggestiveSearch.d.ts +123 -0
- package/lib/typescript/src/components/index.d.ts +10 -3
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/Accordion/Accordion.tsx +113 -73
- package/src/components/ActionFooter/ActionFooter.tsx +56 -4
- package/src/components/Attached/Attached.tsx +181 -0
- package/src/components/Card/Card.tsx +28 -1
- package/src/components/Checkbox/Checkbox.tsx +22 -9
- package/src/components/DropdownInput/DropdownInput.tsx +67 -39
- package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +237 -0
- package/src/components/FormField/FormField.tsx +19 -3
- package/src/components/FullscreenModal/FullscreenModal.tsx +414 -0
- package/src/components/ListItem/ListItem.tsx +55 -25
- package/src/components/MessageField/MessageField.tsx +543 -0
- package/src/components/NavArrow/NavArrow.tsx +81 -17
- package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +426 -0
- package/src/components/Slot/Slot.tsx +91 -0
- package/src/components/Stepper/Step.tsx +52 -51
- package/src/components/Stepper/StepLabel.tsx +46 -9
- package/src/components/Stepper/Stepper.tsx +20 -15
- package/src/components/SuggestiveSearch/SuggestiveSearch.tsx +756 -0
- package/src/components/TextInput/TextInput.tsx +14 -1
- package/src/components/Title/Title.tsx +13 -2
- package/src/components/index.ts +10 -3
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- 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
|
-
*
|
|
24
|
-
* -
|
|
25
|
-
*
|
|
26
|
-
* -
|
|
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
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const
|
|
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:
|
|
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:
|
|
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: [
|
|
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: [
|
|
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:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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) || '#
|
|
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 =
|
|
44
|
-
const labelLineHeight =
|
|
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 =
|
|
47
|
-
const inputPaddingH =
|
|
48
|
-
const inputGap =
|
|
49
|
-
const inputRadius =
|
|
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 =
|
|
52
|
-
const inputLineHeight =
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
344
|
-
//
|
|
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:
|
|
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,
|