jfs-components 0.0.79 → 0.0.84
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/components/AppBar/AppBar.js +56 -6
- package/lib/commonjs/components/Attached/Attached.js +46 -7
- package/lib/commonjs/components/Checkbox/Checkbox.js +18 -2
- package/lib/commonjs/components/Drawer/Drawer.js +6 -1
- package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -6
- package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +17 -11
- package/lib/commonjs/components/FormField/FormField.js +1 -14
- package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +5 -1
- package/lib/commonjs/components/ListItem/ListItem.js +6 -11
- package/lib/commonjs/components/MessageField/MessageField.js +1 -13
- package/lib/commonjs/components/PaymentFeedback/PaymentFeedback.js +12 -9
- package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +69 -160
- package/lib/commonjs/components/Spinner/Spinner.js +217 -0
- package/lib/commonjs/components/TextInput/TextInput.js +33 -18
- package/lib/commonjs/components/index.js +7 -0
- package/lib/commonjs/icons/components/IconArrowdown.js +19 -0
- package/lib/commonjs/icons/components/IconArrowup.js +19 -0
- package/lib/commonjs/icons/components/IconChevrondowncircle.js +19 -0
- package/lib/commonjs/icons/components/IconChevronleftcircle.js +19 -0
- package/lib/commonjs/icons/components/IconChevronrightcircle.js +19 -0
- package/lib/commonjs/icons/components/IconChevronupcircle.js +19 -0
- package/lib/commonjs/icons/components/IconOsnavback.js +19 -0
- package/lib/commonjs/icons/components/IconOsnavcenter.js +19 -0
- package/lib/commonjs/icons/components/IconOsnavhome.js +19 -0
- package/lib/commonjs/icons/components/IconOsnavtask.js +19 -0
- package/lib/commonjs/icons/components/IconSignin.js +19 -0
- package/lib/commonjs/icons/components/IconSignout.js +19 -0
- package/lib/commonjs/icons/components/index.js +132 -0
- package/lib/commonjs/icons/registry.js +2 -2
- package/lib/module/components/AppBar/AppBar.js +56 -6
- package/lib/module/components/Attached/Attached.js +46 -7
- package/lib/module/components/Checkbox/Checkbox.js +18 -2
- package/lib/module/components/Drawer/Drawer.js +6 -1
- package/lib/module/components/DropdownInput/DropdownInput.js +30 -6
- package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +17 -11
- package/lib/module/components/FormField/FormField.js +3 -16
- package/lib/module/components/FullscreenModal/FullscreenModal.js +5 -1
- package/lib/module/components/ListItem/ListItem.js +6 -11
- package/lib/module/components/MessageField/MessageField.js +3 -15
- package/lib/module/components/PaymentFeedback/PaymentFeedback.js +13 -9
- package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +72 -160
- package/lib/module/components/Spinner/Spinner.js +212 -0
- package/lib/module/components/TextInput/TextInput.js +34 -19
- package/lib/module/components/index.js +1 -0
- package/lib/module/icons/components/IconArrowdown.js +12 -0
- package/lib/module/icons/components/IconArrowup.js +12 -0
- package/lib/module/icons/components/IconChevrondowncircle.js +12 -0
- package/lib/module/icons/components/IconChevronleftcircle.js +12 -0
- package/lib/module/icons/components/IconChevronrightcircle.js +12 -0
- package/lib/module/icons/components/IconChevronupcircle.js +12 -0
- package/lib/module/icons/components/IconOsnavback.js +12 -0
- package/lib/module/icons/components/IconOsnavcenter.js +12 -0
- package/lib/module/icons/components/IconOsnavhome.js +12 -0
- package/lib/module/icons/components/IconOsnavtask.js +12 -0
- package/lib/module/icons/components/IconSignin.js +12 -0
- package/lib/module/icons/components/IconSignout.js +12 -0
- package/lib/module/icons/components/index.js +12 -0
- package/lib/module/icons/registry.js +2 -2
- package/lib/typescript/src/components/AppBar/AppBar.d.ts +12 -1
- package/lib/typescript/src/components/Attached/Attached.d.ts +19 -16
- package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +3 -2
- package/lib/typescript/src/components/ListItem/ListItem.d.ts +3 -3
- package/lib/typescript/src/components/PaymentFeedback/PaymentFeedback.d.ts +5 -1
- package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +10 -8
- package/lib/typescript/src/components/Spinner/Spinner.d.ts +45 -0
- package/lib/typescript/src/components/index.d.ts +1 -0
- package/lib/typescript/src/icons/components/IconArrowdown.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconArrowup.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconChevrondowncircle.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconChevronleftcircle.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconChevronrightcircle.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconChevronupcircle.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconOsnavback.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconOsnavcenter.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconOsnavhome.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconOsnavtask.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconSignin.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconSignout.d.ts +3 -0
- package/lib/typescript/src/icons/components/index.d.ts +12 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +3 -2
- package/src/components/AppBar/AppBar.tsx +79 -12
- package/src/components/Attached/Attached.tsx +63 -7
- package/src/components/Checkbox/Checkbox.tsx +14 -2
- package/src/components/Drawer/Drawer.tsx +4 -0
- package/src/components/DropdownInput/DropdownInput.tsx +54 -20
- package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +13 -9
- package/src/components/FormField/FormField.tsx +3 -19
- package/src/components/FullscreenModal/FullscreenModal.tsx +3 -0
- package/src/components/ListItem/ListItem.tsx +14 -16
- package/src/components/MessageField/MessageField.tsx +3 -18
- package/src/components/PaymentFeedback/PaymentFeedback.tsx +15 -8
- package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +82 -192
- package/src/components/Spinner/Spinner.tsx +273 -0
- package/src/components/TextInput/TextInput.tsx +37 -19
- package/src/components/index.ts +1 -0
- package/src/icons/components/IconArrowdown.tsx +11 -0
- package/src/icons/components/IconArrowup.tsx +11 -0
- package/src/icons/components/IconChevrondowncircle.tsx +11 -0
- package/src/icons/components/IconChevronleftcircle.tsx +11 -0
- package/src/icons/components/IconChevronrightcircle.tsx +11 -0
- package/src/icons/components/IconChevronupcircle.tsx +11 -0
- package/src/icons/components/IconOsnavback.tsx +11 -0
- package/src/icons/components/IconOsnavcenter.tsx +11 -0
- package/src/icons/components/IconOsnavhome.tsx +11 -0
- package/src/icons/components/IconOsnavtask.tsx +11 -0
- package/src/icons/components/IconSignin.tsx +11 -0
- package/src/icons/components/IconSignout.tsx +11 -0
- package/src/icons/components/index.ts +12 -0
- package/src/icons/registry.ts +49 -1
|
@@ -7,10 +7,18 @@ import { useTokens } from '../../design-tokens/JFSThemeProvider';
|
|
|
7
7
|
import NavArrow from '../NavArrow/NavArrow';
|
|
8
8
|
import { cloneChildrenWithModes, EMPTY_MODES } from '../../utils/react-utils';
|
|
9
9
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
10
|
+
// SubPage "slot wrap" geometry, taken directly from the Figma design
|
|
11
|
+
// (node 449:7876). The middle slot is an absolutely-centered box of a fixed
|
|
12
|
+
// size; its inner content (node 3991:4125) is a `flex: 1 0 0; min-width: 1px`
|
|
13
|
+
// item so it fills / shrinks responsively within that box.
|
|
14
|
+
const SUBPAGE_MIDDLE_DEFAULT_WIDTH = 192;
|
|
15
|
+
const SUBPAGE_MIDDLE_HEIGHT = 32;
|
|
16
|
+
const SUBPAGE_MIDDLE_PADDING_HORIZONTAL = 21;
|
|
10
17
|
export default function AppBar({
|
|
11
18
|
type = 'MainPage',
|
|
12
19
|
leadingSlot,
|
|
13
20
|
middleSlot,
|
|
21
|
+
middleSlotWidth = SUBPAGE_MIDDLE_DEFAULT_WIDTH,
|
|
14
22
|
actionsSlot,
|
|
15
23
|
modes: propModes = EMPTY_MODES,
|
|
16
24
|
onLeadingPress,
|
|
@@ -112,13 +120,40 @@ export default function AppBar({
|
|
|
112
120
|
children: cloneChildrenWithModes(React.Children.toArray(actionsSlot), modes)
|
|
113
121
|
}) : null;
|
|
114
122
|
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
123
|
+
// SubPage centers its middle slot via absolute positioning (see Figma
|
|
124
|
+
// "slot wrap"), so it never participates in the row flow. Only MainPage
|
|
125
|
+
// keeps the legacy in-flow middle slot.
|
|
126
|
+
const hasInFlowMiddle = isMain && !!processedMiddle;
|
|
127
|
+
|
|
128
|
+
// With an in-flow middle (MainPage) the middle (flex: 1) absorbs the
|
|
129
|
+
// remaining space, so leading & actions sit at the edges naturally. In all
|
|
130
|
+
// other cases we pin leading & actions to the outer edges with
|
|
131
|
+
// `space-between`; the SubPage middle floats above, centered.
|
|
119
132
|
const wrapperStyle = {
|
|
120
133
|
...containerStyle,
|
|
121
|
-
justifyContent:
|
|
134
|
+
justifyContent: hasInFlowMiddle ? 'flex-start' : 'space-between'
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Absolutely-centered middle box for SubPage, mirroring the Figma geometry.
|
|
138
|
+
// `left/top: 50%` + a negative translate keeps it centered regardless of the
|
|
139
|
+
// bar width, while the fixed width clips overly-wide content (overflow:
|
|
140
|
+
// hidden) instead of letting it bleed under the leading/actions slots.
|
|
141
|
+
const subPageMiddleStyle = {
|
|
142
|
+
position: 'absolute',
|
|
143
|
+
top: '50%',
|
|
144
|
+
left: '50%',
|
|
145
|
+
width: middleSlotWidth,
|
|
146
|
+
height: SUBPAGE_MIDDLE_HEIGHT,
|
|
147
|
+
transform: [{
|
|
148
|
+
translateX: -middleSlotWidth / 2
|
|
149
|
+
}, {
|
|
150
|
+
translateY: -SUBPAGE_MIDDLE_HEIGHT / 2
|
|
151
|
+
}],
|
|
152
|
+
flexDirection: 'row',
|
|
153
|
+
alignItems: 'center',
|
|
154
|
+
justifyContent: 'center',
|
|
155
|
+
paddingHorizontal: SUBPAGE_MIDDLE_PADDING_HORIZONTAL,
|
|
156
|
+
overflow: 'hidden'
|
|
122
157
|
};
|
|
123
158
|
return /*#__PURE__*/_jsxs(View, {
|
|
124
159
|
style: [wrapperStyle, style],
|
|
@@ -134,7 +169,7 @@ export default function AppBar({
|
|
|
134
169
|
alignItems: 'center'
|
|
135
170
|
},
|
|
136
171
|
children: processedLeading
|
|
137
|
-
}),
|
|
172
|
+
}), hasInFlowMiddle && /*#__PURE__*/_jsx(View, {
|
|
138
173
|
style: {
|
|
139
174
|
flex: 1,
|
|
140
175
|
minWidth: 0,
|
|
@@ -147,6 +182,21 @@ export default function AppBar({
|
|
|
147
182
|
}), /*#__PURE__*/_jsx(View, {
|
|
148
183
|
style: actionsStyle,
|
|
149
184
|
children: processedActions
|
|
185
|
+
}), isSub && processedMiddle && /*#__PURE__*/_jsx(View, {
|
|
186
|
+
style: subPageMiddleStyle,
|
|
187
|
+
pointerEvents: "box-none",
|
|
188
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
189
|
+
style: {
|
|
190
|
+
flex: 1,
|
|
191
|
+
minWidth: 1,
|
|
192
|
+
height: '100%',
|
|
193
|
+
flexDirection: 'row',
|
|
194
|
+
alignItems: 'center',
|
|
195
|
+
justifyContent: 'center'
|
|
196
|
+
},
|
|
197
|
+
pointerEvents: "box-none",
|
|
198
|
+
children: processedMiddle
|
|
199
|
+
})
|
|
150
200
|
})]
|
|
151
201
|
});
|
|
152
202
|
}
|
|
@@ -43,9 +43,27 @@ function resolveAnchorFractions(position) {
|
|
|
43
43
|
* </Attached>
|
|
44
44
|
* ```
|
|
45
45
|
*/
|
|
46
|
+
/**
|
|
47
|
+
* Stretches the immediate badge child/children to fill the enforced badge box.
|
|
48
|
+
* Merges `{ width: '100%', height: '100%' }` into each top-level element's
|
|
49
|
+
* `style` so an arbitrary node (e.g. an `Image` with its own width/aspectRatio)
|
|
50
|
+
* fills the fixed `badgeSize` box instead of laying out at its intrinsic size.
|
|
51
|
+
* The wrapping box's `overflow: 'hidden'` clips anything that still overflows.
|
|
52
|
+
*/
|
|
53
|
+
function forceBadgeFill(children) {
|
|
54
|
+
return React.Children.map(children, child => {
|
|
55
|
+
if (! /*#__PURE__*/React.isValidElement(child)) return child;
|
|
56
|
+
const childStyle = child.props?.style;
|
|
57
|
+
return /*#__PURE__*/React.cloneElement(child, {
|
|
58
|
+
style: [FILL_STYLE, childStyle]
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
46
62
|
function Attached({
|
|
47
63
|
children,
|
|
48
64
|
badge,
|
|
65
|
+
badgeSize,
|
|
66
|
+
badgeRadius,
|
|
49
67
|
position = 'bottom-right',
|
|
50
68
|
circular = true,
|
|
51
69
|
modes: propModes = EMPTY_MODES,
|
|
@@ -60,7 +78,7 @@ function Attached({
|
|
|
60
78
|
...propModes
|
|
61
79
|
}, [globalModes, propModes]);
|
|
62
80
|
const [mainSize, setMainSize] = useState(ZERO_SIZE);
|
|
63
|
-
const [
|
|
81
|
+
const [measuredBadgeSize, setMeasuredBadgeSize] = useState(ZERO_SIZE);
|
|
64
82
|
const onMainLayout = useCallback(e => {
|
|
65
83
|
const {
|
|
66
84
|
width,
|
|
@@ -76,19 +94,31 @@ function Attached({
|
|
|
76
94
|
width,
|
|
77
95
|
height
|
|
78
96
|
} = e.nativeEvent.layout;
|
|
79
|
-
|
|
97
|
+
setMeasuredBadgeSize(prev => prev.width === width && prev.height === height ? prev : {
|
|
80
98
|
width,
|
|
81
99
|
height
|
|
82
100
|
});
|
|
83
101
|
}, []);
|
|
84
102
|
const mainChildren = useMemo(() => children != null ? cloneChildrenWithModes(children, modes) : null, [children, modes]);
|
|
85
103
|
const badgeChildren = useMemo(() => badge != null ? cloneChildrenWithModes(badge, modes) : null, [badge, modes]);
|
|
104
|
+
|
|
105
|
+
// When a fixed size is requested, the badge is wrapped in a clipped box and
|
|
106
|
+
// its content is force-stretched to fill it (see `forceBadgeFill`).
|
|
107
|
+
const badgeBoxStyle = useMemo(() => {
|
|
108
|
+
if (badgeSize == null) return null;
|
|
109
|
+
return {
|
|
110
|
+
width: badgeSize,
|
|
111
|
+
height: badgeSize,
|
|
112
|
+
borderRadius: badgeRadius ?? badgeSize / 2,
|
|
113
|
+
overflow: 'hidden'
|
|
114
|
+
};
|
|
115
|
+
}, [badgeSize, badgeRadius]);
|
|
86
116
|
const badgePlacement = useMemo(() => {
|
|
87
117
|
const {
|
|
88
118
|
fx,
|
|
89
119
|
fy
|
|
90
120
|
} = resolveAnchorFractions(position);
|
|
91
|
-
const measured = mainSize.width > 0 &&
|
|
121
|
+
const measured = mainSize.width > 0 && measuredBadgeSize.width > 0;
|
|
92
122
|
let anchorX;
|
|
93
123
|
let anchorY;
|
|
94
124
|
if (circular) {
|
|
@@ -108,12 +138,12 @@ function Attached({
|
|
|
108
138
|
}
|
|
109
139
|
return {
|
|
110
140
|
position: 'absolute',
|
|
111
|
-
left: anchorX -
|
|
112
|
-
top: anchorY -
|
|
141
|
+
left: anchorX - measuredBadgeSize.width / 2,
|
|
142
|
+
top: anchorY - measuredBadgeSize.height / 2,
|
|
113
143
|
// Hide until both elements are measured to avoid a one-frame flash at (0,0).
|
|
114
144
|
opacity: measured ? 1 : 0
|
|
115
145
|
};
|
|
116
|
-
}, [position, circular, mainSize,
|
|
146
|
+
}, [position, circular, mainSize, measuredBadgeSize]);
|
|
117
147
|
return /*#__PURE__*/_jsxs(View, {
|
|
118
148
|
style: [styles.container, style],
|
|
119
149
|
...rest,
|
|
@@ -124,7 +154,10 @@ function Attached({
|
|
|
124
154
|
style: badgePlacement,
|
|
125
155
|
onLayout: onBadgeLayout,
|
|
126
156
|
pointerEvents: "box-none",
|
|
127
|
-
children:
|
|
157
|
+
children: badgeBoxStyle != null ? /*#__PURE__*/_jsx(View, {
|
|
158
|
+
style: badgeBoxStyle,
|
|
159
|
+
children: forceBadgeFill(badgeChildren)
|
|
160
|
+
}) : badgeChildren
|
|
128
161
|
})]
|
|
129
162
|
});
|
|
130
163
|
}
|
|
@@ -136,4 +169,10 @@ const styles = {
|
|
|
136
169
|
alignSelf: 'flex-start'
|
|
137
170
|
}
|
|
138
171
|
};
|
|
172
|
+
|
|
173
|
+
/** Fill style merged into badge content when `badgeSize` enforces a fixed box. */
|
|
174
|
+
const FILL_STYLE = {
|
|
175
|
+
width: '100%',
|
|
176
|
+
height: '100%'
|
|
177
|
+
};
|
|
139
178
|
export default /*#__PURE__*/React.memo(Attached);
|
|
@@ -50,11 +50,25 @@ function useFocusVisible() {
|
|
|
50
50
|
/** Minimum touch target per iOS HIG / Material accessibility guidance. */
|
|
51
51
|
const MIN_TOUCH_TARGET = 44;
|
|
52
52
|
const touchTargetStyle = {
|
|
53
|
-
minWidth: MIN_TOUCH_TARGET,
|
|
54
|
-
minHeight: MIN_TOUCH_TARGET,
|
|
55
53
|
alignItems: 'center',
|
|
56
54
|
justifyContent: 'center'
|
|
57
55
|
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Expands the tappable region to the 44pt minimum without changing layout.
|
|
59
|
+
* `hitSlop` extends the press-responder bounds beyond the visual box on both
|
|
60
|
+
* native and web (react-native-web ≥ 0.19), so the Pressable keeps its natural
|
|
61
|
+
* checkbox-sized footprint and sibling alignment stays intact.
|
|
62
|
+
*/
|
|
63
|
+
function invisibleTouchHitSlop(checkboxSize) {
|
|
64
|
+
const slop = Math.max(0, Math.ceil((MIN_TOUCH_TARGET - checkboxSize) / 2));
|
|
65
|
+
return {
|
|
66
|
+
top: slop,
|
|
67
|
+
bottom: slop,
|
|
68
|
+
left: slop,
|
|
69
|
+
right: slop
|
|
70
|
+
};
|
|
71
|
+
}
|
|
58
72
|
/**
|
|
59
73
|
* Checkbox component that maps directly to the Figma design using design tokens.
|
|
60
74
|
*
|
|
@@ -179,8 +193,10 @@ function Checkbox({
|
|
|
179
193
|
};
|
|
180
194
|
};
|
|
181
195
|
const markColor = disabled && isChecked ? disabledActiveMark : selectedMarkColor;
|
|
196
|
+
const hitSlop = invisibleTouchHitSlop(size);
|
|
182
197
|
return /*#__PURE__*/_jsx(Pressable, {
|
|
183
198
|
style: [touchTargetStyle, style],
|
|
199
|
+
hitSlop: hitSlop,
|
|
184
200
|
onPress: handlePress,
|
|
185
201
|
disabled: disabled,
|
|
186
202
|
onHoverIn: () => setIsHovered(true),
|
|
@@ -363,7 +363,12 @@ function Drawer({
|
|
|
363
363
|
flexDirection: 'column',
|
|
364
364
|
alignItems: 'stretch'
|
|
365
365
|
}, contentContainerStyle],
|
|
366
|
-
showsVerticalScrollIndicator: showsVerticalScrollIndicator
|
|
366
|
+
showsVerticalScrollIndicator: showsVerticalScrollIndicator
|
|
367
|
+
// Let a tap on an input inside the sheet focus it on the FIRST tap
|
|
368
|
+
// even while the keyboard is already open (default 'never' would
|
|
369
|
+
// eat that tap just to dismiss the keyboard).
|
|
370
|
+
,
|
|
371
|
+
keyboardShouldPersistTaps: "handled",
|
|
367
372
|
animatedProps: animatedScrollProps,
|
|
368
373
|
alwaysBounceVertical: false,
|
|
369
374
|
overScrollMode: "always",
|
|
@@ -139,7 +139,7 @@ function DropdownInput({
|
|
|
139
139
|
supportText,
|
|
140
140
|
errorMessage,
|
|
141
141
|
menuMaxHeight = 240,
|
|
142
|
-
menuOffset
|
|
142
|
+
menuOffset,
|
|
143
143
|
matchTriggerWidth = true,
|
|
144
144
|
closeOnBackdropPress = true,
|
|
145
145
|
modes: propModes = EMPTY_MODES,
|
|
@@ -202,10 +202,29 @@ function DropdownInput({
|
|
|
202
202
|
const tokens = useFormFieldTokens(modes);
|
|
203
203
|
const chevron = useChevronTokens(modes);
|
|
204
204
|
|
|
205
|
+
// Gap between the input and the popup. Falls back to the `formField/gap`
|
|
206
|
+
// token so the menu's offset matches the field's own internal spacing.
|
|
207
|
+
const effectiveMenuOffset = menuOffset ?? tokens.gap;
|
|
208
|
+
|
|
205
209
|
// ---------------- Layout / measurement ----------------
|
|
206
210
|
const triggerRef = useRef(null);
|
|
207
211
|
const [triggerRect, setTriggerRect] = useState(null);
|
|
208
212
|
const insets = useSafeAreaInsets();
|
|
213
|
+
|
|
214
|
+
// Android coordinate-space bridge.
|
|
215
|
+
//
|
|
216
|
+
// The popup lives inside a `statusBarTranslucent` Modal, whose window is
|
|
217
|
+
// laid out from the PHYSICAL top of the screen (behind the status bar).
|
|
218
|
+
// The trigger, however, is rendered inside the app's content area (Expo
|
|
219
|
+
// Router / react-native-screens under edge-to-edge), so its
|
|
220
|
+
// `measureInWindow` Y is relative to the content area — it does NOT include
|
|
221
|
+
// the status bar height. Feeding that Y straight into the Modal would place
|
|
222
|
+
// the popup one status-bar-height too high, landing it on top of the input.
|
|
223
|
+
//
|
|
224
|
+
// Adding `insets.top` converts the trigger's content-relative Y into the
|
|
225
|
+
// Modal's full-screen coordinate space. iOS/web share a single coordinate
|
|
226
|
+
// space for the Modal and the trigger, so no shift is needed there.
|
|
227
|
+
const windowTopOffset = Platform.OS === 'android' ? insets.top : 0;
|
|
209
228
|
const measure = useCallback(() => {
|
|
210
229
|
if (!triggerRef.current) return;
|
|
211
230
|
triggerRef.current.measureInWindow((x, y, width, height) => {
|
|
@@ -271,7 +290,7 @@ function DropdownInput({
|
|
|
271
290
|
const spaceBelow = windowHeight - (triggerRect.y + triggerRect.height) - insets.bottom;
|
|
272
291
|
const spaceAbove = triggerRect.y - insets.top;
|
|
273
292
|
const desiredHeight = Math.min(menuSize?.height ?? menuMaxHeight, menuMaxHeight);
|
|
274
|
-
const needed = desiredHeight +
|
|
293
|
+
const needed = desiredHeight + effectiveMenuOffset + 8;
|
|
275
294
|
if (placement === 'top') {
|
|
276
295
|
return spaceAbove >= needed || spaceAbove >= spaceBelow ? 'top' : 'bottom';
|
|
277
296
|
}
|
|
@@ -279,7 +298,7 @@ function DropdownInput({
|
|
|
279
298
|
return spaceBelow >= needed || spaceBelow >= spaceAbove ? 'bottom' : 'top';
|
|
280
299
|
}
|
|
281
300
|
return spaceBelow >= needed || spaceBelow >= spaceAbove ? 'bottom' : 'top';
|
|
282
|
-
}, [triggerRect, placement, windowHeight, menuSize?.height, menuMaxHeight,
|
|
301
|
+
}, [triggerRect, placement, windowHeight, menuSize?.height, menuMaxHeight, effectiveMenuOffset, insets.top, insets.bottom]);
|
|
283
302
|
const popupStyle = useMemo(() => {
|
|
284
303
|
if (!triggerRect) {
|
|
285
304
|
return {
|
|
@@ -298,15 +317,18 @@ function DropdownInput({
|
|
|
298
317
|
const minLeft = insets.left + screenPadding;
|
|
299
318
|
if (leftPos > maxLeft) leftPos = maxLeft;
|
|
300
319
|
if (leftPos < minLeft) leftPos = minLeft;
|
|
320
|
+
|
|
321
|
+
// Trigger top expressed in the Modal's (full-screen) coordinate space.
|
|
322
|
+
const triggerTop = triggerRect.y + windowTopOffset;
|
|
301
323
|
let topPos;
|
|
302
324
|
if (computedPlacement === 'top') {
|
|
303
325
|
const desiredHeight = menuSize?.height ?? menuMaxHeight;
|
|
304
|
-
topPos =
|
|
326
|
+
topPos = triggerTop - desiredHeight - effectiveMenuOffset;
|
|
305
327
|
if (topPos < insets.top + screenPadding) {
|
|
306
328
|
topPos = insets.top + screenPadding;
|
|
307
329
|
}
|
|
308
330
|
} else {
|
|
309
|
-
topPos =
|
|
331
|
+
topPos = triggerTop + triggerRect.height + effectiveMenuOffset;
|
|
310
332
|
}
|
|
311
333
|
const style = {
|
|
312
334
|
position: 'absolute',
|
|
@@ -318,7 +340,7 @@ function DropdownInput({
|
|
|
318
340
|
// the wrong place. menuSize becomes truthy after the first layout.
|
|
319
341
|
if (menuSize == null) style.opacity = 0;
|
|
320
342
|
return style;
|
|
321
|
-
}, [triggerRect, computedPlacement, menuSize,
|
|
343
|
+
}, [triggerRect, computedPlacement, menuSize, effectiveMenuOffset, windowTopOffset, menuMaxHeight, matchTriggerWidth, windowWidth, insets.top, insets.left, insets.right]);
|
|
322
344
|
|
|
323
345
|
// Reset menu size when closing so the next open re-measures (handles items
|
|
324
346
|
// changing while the menu was closed).
|
|
@@ -507,6 +529,8 @@ function DropdownInput({
|
|
|
507
529
|
}), /*#__PURE__*/_jsx(Modal, {
|
|
508
530
|
visible: isOpen,
|
|
509
531
|
transparent: true,
|
|
532
|
+
statusBarTranslucent: true,
|
|
533
|
+
navigationBarTranslucent: true,
|
|
510
534
|
animationType: "fade",
|
|
511
535
|
onRequestClose: closeMenu,
|
|
512
536
|
children: /*#__PURE__*/_jsx(Pressable, {
|
|
@@ -82,8 +82,6 @@ function ExpandableCheckbox({
|
|
|
82
82
|
}, [disabled, isExpanded, isExpandedControlled, onExpandedChange]);
|
|
83
83
|
const gap = getVariableByName('expandableCheckbox/gap', modes) ?? 8;
|
|
84
84
|
const rowGap = getVariableByName('checkboxItem/gap', modes) ?? 8;
|
|
85
|
-
const rowPaddingHorizontal = getVariableByName('checkboxItem/padding/horizontal', modes) ?? 0;
|
|
86
|
-
const rowPaddingVertical = getVariableByName('checkboxItem/padding/vertical', modes) ?? 0;
|
|
87
85
|
const labelColor = getVariableByName('checkboxItem/foreground', modes) ?? '#1a1c1f';
|
|
88
86
|
const labelFontFamily = getVariableByName('checkboxItem/label/fontFamily', modes) ?? 'JioType Var';
|
|
89
87
|
const labelFontSize = getVariableByName('checkboxItem/label/fontSize', modes) ?? 14;
|
|
@@ -104,11 +102,9 @@ function ExpandableCheckbox({
|
|
|
104
102
|
alignSelf: isExpanded ? 'stretch' : 'auto',
|
|
105
103
|
minWidth: 0,
|
|
106
104
|
flexDirection: 'row',
|
|
107
|
-
alignItems: 'flex-start',
|
|
108
|
-
gap: rowGap
|
|
109
|
-
|
|
110
|
-
paddingVertical: rowPaddingVertical
|
|
111
|
-
}), [isExpanded, rowGap, rowPaddingHorizontal, rowPaddingVertical]);
|
|
105
|
+
alignItems: isExpanded ? 'flex-start' : 'center',
|
|
106
|
+
gap: rowGap
|
|
107
|
+
}), [isExpanded, rowGap]);
|
|
112
108
|
const resolvedLabelStyle = useMemo(() => ({
|
|
113
109
|
flex: 1,
|
|
114
110
|
minWidth: 0,
|
|
@@ -116,11 +112,21 @@ function ExpandableCheckbox({
|
|
|
116
112
|
fontFamily: labelFontFamily,
|
|
117
113
|
fontSize: labelFontSize,
|
|
118
114
|
lineHeight: labelLineHeight,
|
|
119
|
-
fontWeight: labelFontWeight
|
|
120
|
-
|
|
115
|
+
fontWeight: labelFontWeight,
|
|
116
|
+
// Android adds asymmetric font padding and top-aligns the glyph inside
|
|
117
|
+
// an inflated line box when `lineHeight` is set. That makes the centered
|
|
118
|
+
// checkbox look like it drops below the text. Disabling the extra
|
|
119
|
+
// padding + centering the glyph keeps the single-line label optically
|
|
120
|
+
// aligned with the checkbox. No-op on iOS / web.
|
|
121
|
+
includeFontPadding: false,
|
|
122
|
+
textAlignVertical: isExpanded ? 'top' : 'center'
|
|
123
|
+
}), [labelColor, labelFontFamily, labelFontSize, labelLineHeight, labelFontWeight, isExpanded]);
|
|
124
|
+
|
|
125
|
+
// Layer component modes first (e.g. Color Mode), then button defaults so
|
|
126
|
+
// Secondary / XS / Low always win unless a dedicated override prop is added.
|
|
121
127
|
const buttonModes = useMemo(() => ({
|
|
122
|
-
...
|
|
123
|
-
...
|
|
128
|
+
...modes,
|
|
129
|
+
...BUTTON_DEFAULT_MODES
|
|
124
130
|
}), [modes]);
|
|
125
131
|
const a11yLabel = accessibilityLabel ?? (typeof label === 'string' ? label : undefined);
|
|
126
132
|
const buttonLabel = isExpanded ? readLessLabel : readMoreLabel;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React, { useCallback, useMemo,
|
|
4
|
-
import { View, Text,
|
|
3
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
4
|
+
import { View, Text, TextInput as RNTextInput } from 'react-native';
|
|
5
5
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
6
|
import { useTokens } from '../../design-tokens/JFSThemeProvider';
|
|
7
7
|
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
|
|
@@ -202,16 +202,6 @@ function FormField({
|
|
|
202
202
|
const [isFocused, setIsFocused] = useState(false);
|
|
203
203
|
const interactive = !isDisabled && !isReadOnly;
|
|
204
204
|
|
|
205
|
-
// Ref to the native input so tapping anywhere in the input row (padding,
|
|
206
|
-
// leading/trailing gutters) focuses it on the FIRST tap — fixing the Android
|
|
207
|
-
// "two taps to open the keyboard" issue caused by the row intercepting the
|
|
208
|
-
// initial touch.
|
|
209
|
-
const inputRef = useRef(null);
|
|
210
|
-
const focusInput = useCallback(() => {
|
|
211
|
-
if (!interactive) return;
|
|
212
|
-
inputRef.current?.focus();
|
|
213
|
-
}, [interactive]);
|
|
214
|
-
|
|
215
205
|
// FormField States cascade — error > read only/disabled > active (focused) > idle.
|
|
216
206
|
// Disabled maps to "Read Only" since there is no dedicated disabled mode and
|
|
217
207
|
// the visual treatment is closest. This is only the DEFAULT — an explicit
|
|
@@ -344,16 +334,13 @@ function FormField({
|
|
|
344
334
|
style: requiredIndicatorStyle,
|
|
345
335
|
children: " *"
|
|
346
336
|
})]
|
|
347
|
-
}), /*#__PURE__*/_jsxs(
|
|
337
|
+
}), /*#__PURE__*/_jsxs(View, {
|
|
348
338
|
style: [inputRowStyle, inputStyle],
|
|
349
|
-
onPress: focusInput,
|
|
350
|
-
accessible: false,
|
|
351
339
|
children: [processedLeading != null && /*#__PURE__*/_jsx(View, {
|
|
352
340
|
accessibilityElementsHidden: true,
|
|
353
341
|
importantForAccessibility: "no",
|
|
354
342
|
children: processedLeading
|
|
355
343
|
}), /*#__PURE__*/_jsx(RNTextInput, {
|
|
356
|
-
ref: inputRef,
|
|
357
344
|
style: [inputTextStyles, inputTextStyle],
|
|
358
345
|
value: value ?? '',
|
|
359
346
|
onChangeText: handleChangeText,
|
|
@@ -296,7 +296,11 @@ function FullscreenModal({
|
|
|
296
296
|
contentContainerStyle: scrollContentStyle,
|
|
297
297
|
showsVerticalScrollIndicator: false,
|
|
298
298
|
onScroll: onScroll,
|
|
299
|
-
scrollEventThrottle: 16
|
|
299
|
+
scrollEventThrottle: 16
|
|
300
|
+
// Tap an input in the body and it focuses on the FIRST tap, even when
|
|
301
|
+
// the keyboard is already open (default 'never' eats that tap).
|
|
302
|
+
,
|
|
303
|
+
keyboardShouldPersistTaps: "handled",
|
|
300
304
|
children: [/*#__PURE__*/_jsx(View, {
|
|
301
305
|
style: heroTextRegionStyle,
|
|
302
306
|
children: /*#__PURE__*/_jsx(HeroText, {
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import React, { useCallback, useMemo, useRef } from 'react';
|
|
4
4
|
import { View, Text, Pressable, Platform } from 'react-native';
|
|
5
5
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
|
-
import IconCapsule from '../IconCapsule/IconCapsule';
|
|
7
6
|
import NavArrow from '../NavArrow/NavArrow';
|
|
8
7
|
import { usePressableWebSupport } from '../../utils/web-platform-utils';
|
|
9
8
|
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
|
|
@@ -144,7 +143,7 @@ const verticalSupportTextOverride = {
|
|
|
144
143
|
* @param {string} [props.title='Title'] - Primary title used in the horizontal layout.
|
|
145
144
|
* @param {string} [props.supportText='Support Text'] - Support text used in both layouts when `supportSlot` is not provided.
|
|
146
145
|
* @param {boolean} [props.showSupportText=true] - Toggles rendering of the support text in Horizontal layout.
|
|
147
|
-
* @param {React.ReactNode} [props.leading] - Optional leading slot.
|
|
146
|
+
* @param {React.ReactNode|null} [props.leading] - Optional leading slot. Omitted or `null` renders nothing.
|
|
148
147
|
* @param {React.ReactNode} [props.supportSlot] - Optional custom slot used instead of the default support text block.
|
|
149
148
|
* @param {React.ReactNode} [props.trailing] - Optional trailing slot (Figma Slot "trailing"). Horizontal layout only.
|
|
150
149
|
* @param {boolean} [props.navArrow=true] - Whether to show NavArrow on the far right (Horizontal layout only).
|
|
@@ -213,13 +212,9 @@ function ListItemImpl({
|
|
|
213
212
|
// Process leading slot to pass modes to children. Memoized on
|
|
214
213
|
// (leading, resolvedModes) so a parent re-render doesn't re-walk the tree.
|
|
215
214
|
const leadingElement = useMemo(() => {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
modes: tokens.resolvedModes,
|
|
220
|
-
accessibilityLabel: undefined
|
|
221
|
-
});
|
|
222
|
-
}
|
|
215
|
+
if (leading == null) return null;
|
|
216
|
+
const processed = cloneChildrenWithModes(React.Children.toArray(leading), tokens.resolvedModes, SLOT_FORCED_MODES);
|
|
217
|
+
if (processed.length === 0) return null;
|
|
223
218
|
return processed.length === 1 ? processed[0] : processed;
|
|
224
219
|
}, [leading, tokens.resolvedModes]);
|
|
225
220
|
const processedSupportSlot = useMemo(() => {
|
|
@@ -269,7 +264,7 @@ function ListItemImpl({
|
|
|
269
264
|
if (layout === 'Horizontal') {
|
|
270
265
|
const innerContent = /*#__PURE__*/_jsxs(View, {
|
|
271
266
|
style: innerContentStyleArray,
|
|
272
|
-
children: [leadingElement, /*#__PURE__*/_jsxs(View, {
|
|
267
|
+
children: [leadingElement ?? null, /*#__PURE__*/_jsxs(View, {
|
|
273
268
|
style: {
|
|
274
269
|
flex: 1,
|
|
275
270
|
minWidth: 1,
|
|
@@ -316,7 +311,7 @@ function ListItemImpl({
|
|
|
316
311
|
// Vertical layout — icon on top, support text/slot below
|
|
317
312
|
const verticalContent = /*#__PURE__*/_jsxs(View, {
|
|
318
313
|
style: verticalContentStyleArray,
|
|
319
|
-
children: [leadingElement, renderSupportContent()]
|
|
314
|
+
children: [leadingElement ?? null, renderSupportContent()]
|
|
320
315
|
});
|
|
321
316
|
if (onPress) {
|
|
322
317
|
return /*#__PURE__*/_jsx(Pressable, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React, { useCallback, useMemo,
|
|
4
|
-
import { View, Text,
|
|
3
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
4
|
+
import { View, Text, TextInput as RNTextInput } from 'react-native';
|
|
5
5
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
6
|
import { useTokens } from '../../design-tokens/JFSThemeProvider';
|
|
7
7
|
import { EMPTY_MODES } from '../../utils/react-utils';
|
|
@@ -140,15 +140,6 @@ function MessageField({
|
|
|
140
140
|
const currentValue = isControlled ? value : uncontrolledValue;
|
|
141
141
|
const [isFocused, setIsFocused] = useState(false);
|
|
142
142
|
const interactive = !isDisabled && !isReadOnly;
|
|
143
|
-
|
|
144
|
-
// Ref to the native textarea so tapping anywhere in the (padded) textarea
|
|
145
|
-
// container focuses it on the FIRST tap, fixing the Android "two taps to
|
|
146
|
-
// open the keyboard" issue.
|
|
147
|
-
const inputRef = useRef(null);
|
|
148
|
-
const focusInput = useCallback(() => {
|
|
149
|
-
if (!interactive) return;
|
|
150
|
-
inputRef.current?.focus();
|
|
151
|
-
}, [interactive]);
|
|
152
143
|
const {
|
|
153
144
|
modes: globalModes
|
|
154
145
|
} = useTokens();
|
|
@@ -283,12 +274,9 @@ function MessageField({
|
|
|
283
274
|
style: requiredIndicatorStyle,
|
|
284
275
|
children: " *"
|
|
285
276
|
})]
|
|
286
|
-
}), /*#__PURE__*/_jsx(
|
|
277
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
287
278
|
style: [textareaContainerStyle, textareaStyle],
|
|
288
|
-
onPress: focusInput,
|
|
289
|
-
accessible: false,
|
|
290
279
|
children: /*#__PURE__*/_jsx(RNTextInput, {
|
|
291
|
-
ref: inputRef,
|
|
292
280
|
multiline: true,
|
|
293
281
|
value: currentValue,
|
|
294
282
|
onChangeText: handleChangeText,
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React
|
|
3
|
+
import React from 'react';
|
|
4
4
|
import { View, Text } from 'react-native';
|
|
5
5
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
6
|
import { useTokens } from '../../design-tokens/JFSThemeProvider';
|
|
7
7
|
import IconCapsule from '../IconCapsule/IconCapsule';
|
|
8
|
-
import { EMPTY_MODES } from '../../utils/react-utils';
|
|
8
|
+
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
|
|
9
9
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
10
10
|
export default function PaymentFeedback({
|
|
11
11
|
title = '₹50,000',
|
|
12
12
|
subtitle = 'Payment successful',
|
|
13
|
-
body
|
|
13
|
+
body,
|
|
14
14
|
details = '18 March 2025, 4:15 pm\nTransaction ID: TXN121466784',
|
|
15
15
|
showDetails = true,
|
|
16
16
|
iconName = 'ic_confirm',
|
|
@@ -97,17 +97,21 @@ export default function PaymentFeedback({
|
|
|
97
97
|
fontWeight: String(detailsFontWeight),
|
|
98
98
|
textAlign: 'center'
|
|
99
99
|
};
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
|
|
101
|
+
// Cascade modes into a custom media slot (per the modes-cascade convention);
|
|
102
|
+
// any modes the consumer set on the slot child still take precedence.
|
|
103
|
+
const mediaContent = renderMedia != null ? cloneChildrenWithModes(renderMedia, modes) : null;
|
|
103
104
|
const defaultMedia = /*#__PURE__*/_jsx(IconCapsule, {
|
|
104
|
-
iconName: iconName
|
|
105
|
+
iconName: iconName
|
|
106
|
+
// `positive` is the default; consumers override the capsule color by
|
|
107
|
+
// passing `AppearanceSystem` (or any other mode) via the `modes` prop.
|
|
108
|
+
,
|
|
105
109
|
modes: {
|
|
110
|
+
AppearanceSystem: 'positive',
|
|
106
111
|
...modes,
|
|
107
112
|
'Icon Capsule Size': 'L',
|
|
108
113
|
Emphasis: 'High',
|
|
109
|
-
'Semantic Intent': 'System'
|
|
110
|
-
AppearanceSystem: 'positive'
|
|
114
|
+
'Semantic Intent': 'System'
|
|
111
115
|
}
|
|
112
116
|
});
|
|
113
117
|
const detailLines = details?.split('\n') ?? [];
|