jfs-components 0.0.78 → 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/CHANGELOG.md +11 -0
- package/lib/commonjs/components/AppBar/AppBar.js +56 -6
- package/lib/commonjs/components/Attached/Attached.js +183 -0
- package/lib/commonjs/components/Card/Card.js +25 -2
- 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 +9 -7
- package/lib/commonjs/components/ListItem/ListItem.js +26 -24
- 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 +237 -0
- package/lib/commonjs/components/Slot/Slot.js +73 -0
- package/lib/commonjs/components/Spinner/Spinner.js +217 -0
- package/lib/commonjs/components/TextInput/TextInput.js +33 -18
- package/lib/commonjs/components/index.js +28 -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 +178 -0
- package/lib/module/components/Card/Card.js +25 -2
- 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 +9 -7
- package/lib/module/components/ListItem/ListItem.js +26 -24
- 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 +234 -0
- package/lib/module/components/Slot/Slot.js +68 -0
- package/lib/module/components/Spinner/Spinner.js +212 -0
- package/lib/module/components/TextInput/TextInput.js +34 -19
- package/lib/module/components/index.js +4 -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 +64 -0
- package/lib/typescript/src/components/Card/Card.d.ts +9 -2
- package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +3 -2
- package/lib/typescript/src/components/ListItem/ListItem.d.ts +16 -6
- package/lib/typescript/src/components/PaymentFeedback/PaymentFeedback.d.ts +5 -1
- package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +66 -0
- package/lib/typescript/src/components/Slot/Slot.d.ts +52 -0
- package/lib/typescript/src/components/Spinner/Spinner.d.ts +45 -0
- package/lib/typescript/src/components/index.d.ts +4 -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 +237 -0
- package/src/components/Card/Card.tsx +28 -1
- 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 +6 -3
- package/src/components/ListItem/ListItem.tsx +42 -25
- package/src/components/MessageField/MessageField.tsx +3 -18
- package/src/components/PaymentFeedback/PaymentFeedback.tsx +15 -8
- package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +316 -0
- package/src/components/Slot/Slot.tsx +91 -0
- package/src/components/Spinner/Spinner.tsx +273 -0
- package/src/components/TextInput/TextInput.tsx +37 -19
- package/src/components/index.ts +4 -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
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
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
|
+
/**
|
|
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
|
+
}
|
|
62
|
+
function Attached({
|
|
63
|
+
children,
|
|
64
|
+
badge,
|
|
65
|
+
badgeSize,
|
|
66
|
+
badgeRadius,
|
|
67
|
+
position = 'bottom-right',
|
|
68
|
+
circular = true,
|
|
69
|
+
modes: propModes = EMPTY_MODES,
|
|
70
|
+
style,
|
|
71
|
+
...rest
|
|
72
|
+
}) {
|
|
73
|
+
const {
|
|
74
|
+
modes: globalModes
|
|
75
|
+
} = useTokens();
|
|
76
|
+
const modes = useMemo(() => globalModes === EMPTY_MODES && propModes === EMPTY_MODES ? EMPTY_MODES : {
|
|
77
|
+
...globalModes,
|
|
78
|
+
...propModes
|
|
79
|
+
}, [globalModes, propModes]);
|
|
80
|
+
const [mainSize, setMainSize] = useState(ZERO_SIZE);
|
|
81
|
+
const [measuredBadgeSize, setMeasuredBadgeSize] = useState(ZERO_SIZE);
|
|
82
|
+
const onMainLayout = useCallback(e => {
|
|
83
|
+
const {
|
|
84
|
+
width,
|
|
85
|
+
height
|
|
86
|
+
} = e.nativeEvent.layout;
|
|
87
|
+
setMainSize(prev => prev.width === width && prev.height === height ? prev : {
|
|
88
|
+
width,
|
|
89
|
+
height
|
|
90
|
+
});
|
|
91
|
+
}, []);
|
|
92
|
+
const onBadgeLayout = useCallback(e => {
|
|
93
|
+
const {
|
|
94
|
+
width,
|
|
95
|
+
height
|
|
96
|
+
} = e.nativeEvent.layout;
|
|
97
|
+
setMeasuredBadgeSize(prev => prev.width === width && prev.height === height ? prev : {
|
|
98
|
+
width,
|
|
99
|
+
height
|
|
100
|
+
});
|
|
101
|
+
}, []);
|
|
102
|
+
const mainChildren = useMemo(() => children != null ? cloneChildrenWithModes(children, modes) : null, [children, modes]);
|
|
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]);
|
|
116
|
+
const badgePlacement = useMemo(() => {
|
|
117
|
+
const {
|
|
118
|
+
fx,
|
|
119
|
+
fy
|
|
120
|
+
} = resolveAnchorFractions(position);
|
|
121
|
+
const measured = mainSize.width > 0 && measuredBadgeSize.width > 0;
|
|
122
|
+
let anchorX;
|
|
123
|
+
let anchorY;
|
|
124
|
+
if (circular) {
|
|
125
|
+
// Project the anchor onto the circle inscribed in the bounding box, so
|
|
126
|
+
// corner badges land on the circumference (45°) instead of the box corner.
|
|
127
|
+
const cx = mainSize.width / 2;
|
|
128
|
+
const cy = mainSize.height / 2;
|
|
129
|
+
const radius = Math.min(mainSize.width, mainSize.height) / 2;
|
|
130
|
+
const dx = (fx - 0.5) * 2; // -1 | 0 | 1
|
|
131
|
+
const dy = (fy - 0.5) * 2; // -1 | 0 | 1
|
|
132
|
+
const len = Math.hypot(dx, dy) || 1; // 'center' → 0, guard against /0
|
|
133
|
+
anchorX = cx + dx / len * radius;
|
|
134
|
+
anchorY = cy + dy / len * radius;
|
|
135
|
+
} else {
|
|
136
|
+
anchorX = mainSize.width * fx;
|
|
137
|
+
anchorY = mainSize.height * fy;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
position: 'absolute',
|
|
141
|
+
left: anchorX - measuredBadgeSize.width / 2,
|
|
142
|
+
top: anchorY - measuredBadgeSize.height / 2,
|
|
143
|
+
// Hide until both elements are measured to avoid a one-frame flash at (0,0).
|
|
144
|
+
opacity: measured ? 1 : 0
|
|
145
|
+
};
|
|
146
|
+
}, [position, circular, mainSize, measuredBadgeSize]);
|
|
147
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
148
|
+
style: [styles.container, style],
|
|
149
|
+
...rest,
|
|
150
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
151
|
+
onLayout: onMainLayout,
|
|
152
|
+
children: mainChildren
|
|
153
|
+
}), badgeChildren != null && /*#__PURE__*/_jsx(View, {
|
|
154
|
+
style: badgePlacement,
|
|
155
|
+
onLayout: onBadgeLayout,
|
|
156
|
+
pointerEvents: "box-none",
|
|
157
|
+
children: badgeBoxStyle != null ? /*#__PURE__*/_jsx(View, {
|
|
158
|
+
style: badgeBoxStyle,
|
|
159
|
+
children: forceBadgeFill(badgeChildren)
|
|
160
|
+
}) : badgeChildren
|
|
161
|
+
})]
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
const styles = {
|
|
165
|
+
// alignSelf flex-start so the wrapper hugs the main content; anchors are then
|
|
166
|
+
// computed relative to the content size rather than a stretched parent.
|
|
167
|
+
container: {
|
|
168
|
+
position: 'relative',
|
|
169
|
+
alignSelf: 'flex-start'
|
|
170
|
+
}
|
|
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
|
+
};
|
|
178
|
+
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: [
|
|
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, {
|
|
@@ -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,
|
|
@@ -10,6 +10,7 @@ import Button from '../Button/Button';
|
|
|
10
10
|
import Disclaimer from '../Disclaimer/Disclaimer';
|
|
11
11
|
import IconButton from '../IconButton/IconButton';
|
|
12
12
|
import ActionFooter from '../ActionFooter/ActionFooter';
|
|
13
|
+
import Slot from '../Slot/Slot';
|
|
13
14
|
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
15
16
|
// Forced modes
|
|
@@ -269,8 +270,9 @@ function FullscreenModal({
|
|
|
269
270
|
if (footer) {
|
|
270
271
|
footerContent = footer;
|
|
271
272
|
} else if (primaryActionLabel) {
|
|
272
|
-
footerContent = /*#__PURE__*/_jsxs(
|
|
273
|
-
|
|
273
|
+
footerContent = /*#__PURE__*/_jsxs(Slot, {
|
|
274
|
+
layoutDirection: "vertical",
|
|
275
|
+
modes: modes,
|
|
274
276
|
children: [/*#__PURE__*/_jsx(Button, {
|
|
275
277
|
label: primaryActionLabel,
|
|
276
278
|
modes: modes,
|
|
@@ -294,7 +296,11 @@ function FullscreenModal({
|
|
|
294
296
|
contentContainerStyle: scrollContentStyle,
|
|
295
297
|
showsVerticalScrollIndicator: false,
|
|
296
298
|
onScroll: onScroll,
|
|
297
|
-
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",
|
|
298
304
|
children: [/*#__PURE__*/_jsx(View, {
|
|
299
305
|
style: heroTextRegionStyle,
|
|
300
306
|
children: /*#__PURE__*/_jsx(HeroText, {
|
|
@@ -335,10 +341,6 @@ const scrollViewStyle = {
|
|
|
335
341
|
const scrollContentStyle = {
|
|
336
342
|
flexGrow: 1
|
|
337
343
|
};
|
|
338
|
-
const footerColumnStyle = {
|
|
339
|
-
width: '100%',
|
|
340
|
-
gap: 8
|
|
341
|
-
};
|
|
342
344
|
const fullWidthStyle = {
|
|
343
345
|
width: '100%'
|
|
344
346
|
};
|