jfs-components 0.1.51 → 0.1.54
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/ActionFooter/ActionFooter.js +319 -135
- package/lib/commonjs/components/Badge/Badge.js +20 -1
- package/lib/commonjs/components/Button/Button.js +139 -47
- package/lib/commonjs/components/ButtonGroup/ButtonGroup.js +1 -1
- package/lib/commonjs/components/Card/Card.js +151 -112
- package/lib/commonjs/components/Card/assets/gold-logo-mark.png +0 -0
- package/lib/commonjs/components/CategoryCard/CategoryCard.js +97 -63
- package/lib/commonjs/components/CategoryCard/assets/sample.png +0 -0
- package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +59 -10
- package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +132 -49
- package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +56 -14
- package/lib/commonjs/components/IconCapsule/IconCapsule.js +1 -1
- package/lib/commonjs/components/MetricData/MetricData.js +26 -13
- package/lib/commonjs/components/MoneyValue/MoneyValue.js +1 -1
- package/lib/commonjs/components/Nudge/Nudge.js +1 -1
- package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +47 -91
- package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +52 -45
- package/lib/commonjs/components/PortfolioHero/PortfolioHero.js +1 -1
- package/lib/commonjs/components/Slider/Slider.js +3 -3
- package/lib/commonjs/components/TextInput/TextInput.js +26 -8
- package/lib/commonjs/components/Tooltip/Tooltip.js +3 -5
- package/lib/commonjs/components/index.js +7 -1
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/design-tokens/figma-modes.generated.js +13 -13
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/ActionFooter/ActionFooter.js +322 -138
- package/lib/module/components/Badge/Badge.js +20 -1
- package/lib/module/components/Button/Button.js +140 -48
- package/lib/module/components/ButtonGroup/ButtonGroup.js +1 -1
- package/lib/module/components/Card/Card.js +152 -114
- package/lib/module/components/Card/assets/gold-logo-mark.png +0 -0
- package/lib/module/components/CategoryCard/CategoryCard.js +97 -63
- package/lib/module/components/CategoryCard/assets/sample.png +0 -0
- package/lib/module/components/CheckboxItem/CheckboxItem.js +59 -10
- package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +135 -52
- package/lib/module/components/HelloJioInput/HelloJioInput.js +56 -14
- package/lib/module/components/IconCapsule/IconCapsule.js +1 -1
- package/lib/module/components/MetricData/MetricData.js +26 -14
- package/lib/module/components/MoneyValue/MoneyValue.js +1 -1
- package/lib/module/components/Nudge/Nudge.js +1 -1
- package/lib/module/components/PdpCcCard/PdpCcCard.js +48 -92
- package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +52 -45
- package/lib/module/components/PortfolioHero/PortfolioHero.js +1 -1
- package/lib/module/components/Slider/Slider.js +3 -3
- package/lib/module/components/TextInput/TextInput.js +27 -9
- package/lib/module/components/Tooltip/Tooltip.js +3 -5
- package/lib/module/components/index.js +1 -1
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/design-tokens/figma-modes.generated.js +13 -13
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +50 -21
- package/lib/typescript/src/components/Button/Button.d.ts +24 -1
- package/lib/typescript/src/components/ButtonGroup/ButtonGroup.d.ts +2 -2
- package/lib/typescript/src/components/Card/Card.d.ts +54 -25
- package/lib/typescript/src/components/CategoryCard/CategoryCard.d.ts +29 -12
- package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +27 -9
- package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +37 -12
- package/lib/typescript/src/components/IconCapsule/IconCapsule.d.ts +1 -1
- package/lib/typescript/src/components/MetricData/MetricData.d.ts +7 -5
- package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +16 -6
- package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +9 -2
- package/lib/typescript/src/components/index.d.ts +2 -2
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/ActionFooter/ActionFooter.tsx +375 -141
- package/src/components/Badge/Badge.tsx +21 -1
- package/src/components/Button/Button.tsx +192 -44
- package/src/components/ButtonGroup/ButtonGroup.tsx +2 -2
- package/src/components/Card/Card.tsx +370 -212
- package/src/components/Card/assets/gold-logo-mark.png +0 -0
- package/src/components/CategoryCard/CategoryCard.tsx +130 -84
- package/src/components/CategoryCard/assets/sample.png +0 -0
- package/src/components/CheckboxItem/CheckboxItem.tsx +72 -12
- package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +186 -67
- package/src/components/HelloJioInput/HelloJioInput.tsx +58 -17
- package/src/components/IconCapsule/IconCapsule.tsx +1 -1
- package/src/components/MetricData/MetricData.tsx +39 -22
- package/src/components/MoneyValue/MoneyValue.tsx +1 -1
- package/src/components/Nudge/Nudge.tsx +1 -1
- package/src/components/PdpCcCard/PdpCcCard.tsx +41 -92
- package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +73 -58
- package/src/components/PortfolioHero/PortfolioHero.tsx +1 -1
- package/src/components/Slider/Slider.tsx +3 -3
- package/src/components/TextInput/TextInput.tsx +26 -9
- package/src/components/Tooltip/Tooltip.tsx +3 -5
- package/src/components/index.ts +9 -2
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/design-tokens/figma-modes.generated.ts +13 -13
- package/src/icons/registry.ts +1 -1
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React, { useEffect, useMemo,
|
|
4
|
-
import {
|
|
3
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { View, Text, Platform, useWindowDimensions } from 'react-native';
|
|
5
|
+
import Animated, { useAnimatedKeyboard, useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
|
|
5
6
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
7
|
import { EMPTY_MODES, cloneChildrenWithModes, flattenChildren } from '../../utils/react-utils';
|
|
7
8
|
import { boxShadow } from '../../utils/shadow';
|
|
9
|
+
import ButtonGroup from '../ButtonGroup/ButtonGroup';
|
|
8
10
|
import IconButton from '../IconButton/IconButton';
|
|
9
|
-
import
|
|
11
|
+
import Slot from '../Slot/Slot';
|
|
12
|
+
import Stack from '../Stack/Stack';
|
|
13
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
14
|
+
const IS_WEB = Platform.OS === 'web';
|
|
15
|
+
// Native sticky pinning is the product contract. Web/Storybook stays in normal
|
|
16
|
+
// document flow so stories and docs layouts are not yanked out of their cards.
|
|
17
|
+
const STICKY_BY_DEFAULT = !IS_WEB;
|
|
18
|
+
|
|
19
|
+
// Fallback row height used only until the first onLayout measurement lands.
|
|
20
|
+
// Matches the common Button / IconButton intrinsic height in this library.
|
|
21
|
+
const FALLBACK_ACTION_ROW_HEIGHT = 48;
|
|
22
|
+
|
|
10
23
|
// ---------------------------------------------------------------------------
|
|
11
24
|
// Yoga-safe stretch
|
|
12
25
|
// ---------------------------------------------------------------------------
|
|
@@ -34,172 +47,343 @@ const STRETCH_STYLE = {
|
|
|
34
47
|
flexBasis: 0,
|
|
35
48
|
minWidth: 0
|
|
36
49
|
};
|
|
50
|
+
const COMPOSITE_STRETCH_STYLE = {
|
|
51
|
+
alignSelf: 'stretch',
|
|
52
|
+
width: '100%'
|
|
53
|
+
};
|
|
37
54
|
|
|
38
55
|
// Upward drop shadow routed through the unified helper: native `boxShadow` on
|
|
39
56
|
// web + RN 0.76+, legacy `shadow*` (iOS) / `elevation` (Android) on older RN.
|
|
57
|
+
// Values match `actionFooter/shadow/primary|secondary/*` tokens.
|
|
40
58
|
const SHADOW = boxShadow('0px -12px 24px 0px rgba(12, 13, 16, 0.12), 0px -16px 48px 0px rgba(12, 13, 16, 0.16)', {
|
|
41
59
|
elevation: 16
|
|
42
60
|
});
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
61
|
+
const SHELL_STYLE = {
|
|
62
|
+
// Fill the parent's cross axis without forcing callers to pass width: '100%'.
|
|
63
|
+
alignSelf: 'stretch',
|
|
64
|
+
// In a column flex parent, absorb free space above us so the footer sits on
|
|
65
|
+
// the bottom edge even when siblings above it are short. Harmless when a
|
|
66
|
+
// sibling already owns `flex: 1` (e.g. FullscreenModal's ScrollView).
|
|
67
|
+
marginTop: 'auto'
|
|
68
|
+
};
|
|
69
|
+
const PINNED_STYLE = {
|
|
70
|
+
position: 'absolute',
|
|
71
|
+
left: 0,
|
|
72
|
+
right: 0,
|
|
73
|
+
bottom: 0
|
|
74
|
+
};
|
|
75
|
+
function isElementOfType(element, component) {
|
|
51
76
|
const t = element.type;
|
|
52
|
-
if (t ===
|
|
53
|
-
|
|
77
|
+
if (t === component) return true;
|
|
78
|
+
// `React.memo` / one extra `forwardRef` layer (re-exports).
|
|
79
|
+
if (t && typeof t === 'object' && t.type === component) return true;
|
|
54
80
|
return false;
|
|
55
81
|
}
|
|
82
|
+
function isIconButtonElement(element) {
|
|
83
|
+
return isElementOfType(element, IconButton);
|
|
84
|
+
}
|
|
56
85
|
|
|
57
86
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* it fills the parent regardless of whether the parent is a flex column,
|
|
63
|
-
* a ScrollView contentContainer, or a plain View.
|
|
64
|
-
* - The inner slot is a single row sized by its tallest child. It does NOT
|
|
65
|
-
* use `flex: 1` — that previously caused the row to collapse to zero on
|
|
66
|
-
* the first Yoga pass on native, taking the button labels with it.
|
|
67
|
-
* - `IconButton` children keep their intrinsic square size.
|
|
68
|
-
* - Every other child is auto-stretched with the Yoga-safe stretch style
|
|
69
|
-
* above so two `<Button>` siblings render at equal width on iOS, Android,
|
|
70
|
-
* and Web.
|
|
71
|
-
*
|
|
72
|
-
* The `modes` prop is automatically pushed down to every slot child via
|
|
73
|
-
* {@link cloneChildrenWithModes}; explicit child-level modes win over the
|
|
74
|
-
* parent's modes.
|
|
75
|
-
*
|
|
76
|
-
* @example
|
|
77
|
-
* ```tsx
|
|
78
|
-
* <ActionFooter modes={modes}>
|
|
79
|
-
* <IconButton iconName="ic_split" />
|
|
80
|
-
* <Button label="Request" modes={{ AppearanceBrand: 'Secondary' }} />
|
|
81
|
-
* <Button label="Pay" modes={{ AppearanceBrand: 'Primary' }} />
|
|
82
|
-
* </ActionFooter>
|
|
83
|
-
* ```
|
|
87
|
+
* Figma nests `Stack → Slot → ButtonGroup` under ActionFooter. In code, Slot
|
|
88
|
+
* still exists as a parallel primitive and Stack is the preferred successor —
|
|
89
|
+
* treat all three as "composites that own their own layout" so we never
|
|
90
|
+
* double-apply the loose-button stretch math and break their internals.
|
|
84
91
|
*/
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// The Android activity is configured with `windowSoftInputMode="adjustResize"`,
|
|
96
|
-
// which shrinks the app window by the keyboard height when the keyboard
|
|
97
|
-
// opens. A bottom-anchored footer therefore gets lifted UP by the keyboard
|
|
98
|
-
// height — exactly the jump the design does not want.
|
|
99
|
-
//
|
|
100
|
-
// To counteract that, we translate the footer back DOWN by the same keyboard
|
|
101
|
-
// height so it visually stays exactly where it was (now sitting behind the
|
|
102
|
-
// keyboard). iOS does not resize the window for the keyboard, so the footer
|
|
103
|
-
// already stays put there; we only run this on Android to avoid pushing the
|
|
104
|
-
// footer off-screen on platforms that don't lift it in the first place.
|
|
105
|
-
const keyboardOffset = useRef(new Animated.Value(0)).current;
|
|
106
|
-
useEffect(() => {
|
|
107
|
-
if (Platform.OS !== 'android') return undefined;
|
|
108
|
-
const animateTo = (toValue, duration) => {
|
|
109
|
-
Animated.timing(keyboardOffset, {
|
|
110
|
-
toValue,
|
|
111
|
-
// Match the OS keyboard animation so the resize and our counter-shift
|
|
112
|
-
// cancel out smoothly with no visible footer movement.
|
|
113
|
-
duration: typeof duration === 'number' && duration > 0 ? duration : 150,
|
|
114
|
-
useNativeDriver: true
|
|
115
|
-
}).start();
|
|
116
|
-
};
|
|
117
|
-
const showSub = Keyboard.addListener('keyboardDidShow', e => {
|
|
118
|
-
animateTo(e?.endCoordinates?.height ?? 0, e?.duration);
|
|
119
|
-
});
|
|
120
|
-
const hideSub = Keyboard.addListener('keyboardDidHide', e => {
|
|
121
|
-
animateTo(0, e?.duration);
|
|
122
|
-
});
|
|
123
|
-
return () => {
|
|
124
|
-
showSub.remove();
|
|
125
|
-
hideSub.remove();
|
|
126
|
-
};
|
|
127
|
-
}, [keyboardOffset]);
|
|
92
|
+
function isCompositeActionElement(element) {
|
|
93
|
+
return isElementOfType(element, ButtonGroup) || isElementOfType(element, Stack) || isElementOfType(element, Slot);
|
|
94
|
+
}
|
|
95
|
+
function resolveChrome(modes) {
|
|
96
|
+
const backgroundColor = getVariableByName('actionFooter/background', modes) ?? '#ffffff';
|
|
97
|
+
const gap = getVariableByName('actionFooter/gap', modes) ?? 8;
|
|
98
|
+
const paddingHorizontal = getVariableByName('actionFooter/padding/horizontal', modes) ?? 16;
|
|
99
|
+
const paddingTop = getVariableByName('actionFooter/padding/top', modes) ?? 10;
|
|
100
|
+
const paddingBottom = getVariableByName('actionFooter/padding/bottom', modes) ?? 41;
|
|
101
|
+
const borderRadius = getVariableByName('actionFooter/radius', modes) ?? getVariableByName('radius/actionFooter', modes) ?? 0;
|
|
128
102
|
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// `alignSelf: 'stretch'` is the cross-platform way to ask "fill the
|
|
143
|
-
// parent's cross axis" — in the common case (column parent) this gives
|
|
144
|
-
// us full-width without the caller needing to pass `width: '100%'`.
|
|
103
|
+
// Title wrap reuses the Drawer title tokens in Figma (same binding ContentSheet
|
|
104
|
+
// uses) so the three bottom surfaces stay typographically aligned.
|
|
105
|
+
const titleColor = getVariableByName('drawer/title/color', modes) ?? '#0d0d0f';
|
|
106
|
+
const titleSize = getVariableByName('drawer/title/fontSize', modes) ?? 14;
|
|
107
|
+
const titleLineHeight = getVariableByName('drawer/title/lineHeight', modes) ?? 17;
|
|
108
|
+
const titleFontFamily = getVariableByName('drawer/title/fontFamily', modes) ?? 'JioType Var';
|
|
109
|
+
const titleFontWeightRaw = getVariableByName('drawer/title/fontWeight', modes) ?? 700;
|
|
110
|
+
const titleFontWeight = typeof titleFontWeightRaw === 'number' ? String(titleFontWeightRaw) : titleFontWeightRaw;
|
|
111
|
+
const titleWrapPaddingTop = getVariableByName('drawer/titleWrap/padding/top', modes) ?? 16;
|
|
112
|
+
const titleWrapPaddingBottom = getVariableByName('drawer/titleWrap/padding/bottom', modes) ?? 8;
|
|
113
|
+
const titleWrapPaddingHorizontal = getVariableByName('drawer/titleWrap/padding/horizontal', modes) ?? 0;
|
|
114
|
+
return {
|
|
115
|
+
containerStyle: {
|
|
145
116
|
alignSelf: 'stretch',
|
|
146
117
|
backgroundColor,
|
|
147
118
|
paddingLeft: paddingHorizontal,
|
|
148
119
|
paddingRight: paddingHorizontal,
|
|
149
120
|
paddingTop,
|
|
150
121
|
paddingBottom,
|
|
122
|
+
borderRadius,
|
|
151
123
|
...SHADOW
|
|
152
|
-
}
|
|
153
|
-
|
|
124
|
+
},
|
|
125
|
+
slotStyle: {
|
|
154
126
|
flexDirection: 'row',
|
|
155
127
|
// Vertically center the IconButton against the slightly taller Buttons
|
|
156
128
|
// so the row reads as a single optical baseline.
|
|
157
129
|
alignItems: 'center',
|
|
130
|
+
alignSelf: 'stretch',
|
|
158
131
|
gap
|
|
159
|
-
}
|
|
132
|
+
},
|
|
133
|
+
titleWrapStyle: {
|
|
134
|
+
alignSelf: 'stretch',
|
|
135
|
+
alignItems: 'center',
|
|
136
|
+
justifyContent: 'center',
|
|
137
|
+
paddingTop: titleWrapPaddingTop,
|
|
138
|
+
paddingBottom: titleWrapPaddingBottom,
|
|
139
|
+
paddingHorizontal: titleWrapPaddingHorizontal
|
|
140
|
+
},
|
|
141
|
+
titleStyle: {
|
|
142
|
+
color: titleColor,
|
|
143
|
+
fontSize: titleSize,
|
|
144
|
+
lineHeight: titleLineHeight,
|
|
145
|
+
fontFamily: titleFontFamily,
|
|
146
|
+
fontWeight: titleFontWeight,
|
|
147
|
+
textAlign: 'center'
|
|
148
|
+
},
|
|
149
|
+
// Seed the in-flow spacer so the first frame already reserves room and
|
|
150
|
+
// scroll siblings never paint under an unmeasured footer.
|
|
151
|
+
estimatedHeight: paddingTop + paddingBottom + FALLBACK_ACTION_ROW_HEIGHT
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Legacy loose-children path — identical stretch semantics to the original
|
|
157
|
+
* ActionFooter so existing call sites keep pixel parity.
|
|
158
|
+
*/
|
|
159
|
+
function enhanceLooseChildren(children, modes) {
|
|
160
|
+
const flat = flattenChildren(children);
|
|
161
|
+
const withModes = cloneChildrenWithModes(flat, modes);
|
|
162
|
+
return withModes.map((child, index) => {
|
|
163
|
+
if (! /*#__PURE__*/React.isValidElement(child)) return child;
|
|
164
|
+
const element = child;
|
|
165
|
+
if (isIconButtonElement(element)) return element;
|
|
166
|
+
return /*#__PURE__*/React.cloneElement(element, {
|
|
167
|
+
key: element.key ?? `action-footer-item-${index}`,
|
|
168
|
+
style: [STRETCH_STYLE, element.props.style]
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function enhanceCompositeChild(element, modes) {
|
|
173
|
+
const childModes = element.props.modes ?? EMPTY_MODES;
|
|
174
|
+
return /*#__PURE__*/React.cloneElement(element, {
|
|
175
|
+
modes: {
|
|
176
|
+
...modes,
|
|
177
|
+
...childModes
|
|
178
|
+
},
|
|
179
|
+
style: [COMPOSITE_STRETCH_STYLE, element.props.style]
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Shared chrome for web + native. On native (`sticky`), an in-flow shell locks
|
|
184
|
+
* the footer's height into the parent layout while the visible chrome is
|
|
185
|
+
* absolutely pinned to that shell's bottom edge — so the footer cannot scroll
|
|
186
|
+
* away or float mid-screen, and keyboard translation never collapses siblings.
|
|
187
|
+
*/
|
|
188
|
+
function ActionFooterChrome({
|
|
189
|
+
children,
|
|
190
|
+
title,
|
|
191
|
+
modes = EMPTY_MODES,
|
|
192
|
+
style,
|
|
193
|
+
accessibilityLabel,
|
|
194
|
+
sticky,
|
|
195
|
+
animatedStyle
|
|
196
|
+
}) {
|
|
197
|
+
const {
|
|
198
|
+
containerStyle,
|
|
199
|
+
slotStyle,
|
|
200
|
+
titleWrapStyle,
|
|
201
|
+
titleStyle,
|
|
202
|
+
estimatedHeight
|
|
203
|
+
} = useMemo(() => resolveChrome(modes), [modes]);
|
|
204
|
+
const showTitle = typeof title === 'string' && title.length > 0;
|
|
205
|
+
|
|
206
|
+
// Decide once whether the caller passed the Figma composite shape
|
|
207
|
+
// (ButtonGroup / Stack / Slot) or the legacy loose-button list. This keeps
|
|
208
|
+
// both APIs working without forcing a migration.
|
|
209
|
+
const {
|
|
210
|
+
actionsBody,
|
|
211
|
+
estimatedActionsHeight
|
|
212
|
+
} = useMemo(() => {
|
|
213
|
+
const flat = flattenChildren(children);
|
|
214
|
+
const sole = flat.length === 1 && /*#__PURE__*/React.isValidElement(flat[0]) ? flat[0] : null;
|
|
215
|
+
if (sole && isCompositeActionElement(sole)) {
|
|
216
|
+
return {
|
|
217
|
+
actionsBody: enhanceCompositeChild(sole, modes),
|
|
218
|
+
estimatedActionsHeight: FALLBACK_ACTION_ROW_HEIGHT
|
|
219
|
+
};
|
|
220
|
+
}
|
|
160
221
|
return {
|
|
161
|
-
|
|
162
|
-
|
|
222
|
+
actionsBody: /*#__PURE__*/_jsx(View, {
|
|
223
|
+
style: slotStyle,
|
|
224
|
+
children: enhanceLooseChildren(children, modes)
|
|
225
|
+
}),
|
|
226
|
+
estimatedActionsHeight: FALLBACK_ACTION_ROW_HEIGHT
|
|
163
227
|
};
|
|
164
|
-
}, [modes]);
|
|
228
|
+
}, [children, modes, slotStyle]);
|
|
229
|
+
const body = useMemo(() => {
|
|
230
|
+
if (!showTitle) {
|
|
231
|
+
// No title → identical tree shape to the pre-title ActionFooter for the
|
|
232
|
+
// loose-children path (just the actions row / composite). No extra Stack
|
|
233
|
+
// wrapper, so existing layouts cannot regress from an added gap node.
|
|
234
|
+
return actionsBody;
|
|
235
|
+
}
|
|
165
236
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return /*#__PURE__*/React.cloneElement(element, {
|
|
182
|
-
key: element.key ?? `action-footer-item-${index}`,
|
|
183
|
-
style: [STRETCH_STYLE, element.props.style]
|
|
184
|
-
});
|
|
237
|
+
// Figma: title wrap (sibling) + Stack → Slot → ButtonGroup. We mount a
|
|
238
|
+
// vertical Stack only when the title is present so the gap between title
|
|
239
|
+
// and actions is token-driven (`slot/gap` via Stack, cascaded modes).
|
|
240
|
+
return /*#__PURE__*/_jsxs(Stack, {
|
|
241
|
+
layoutDirection: "vertical",
|
|
242
|
+
fillWidth: true,
|
|
243
|
+
modes: modes,
|
|
244
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
245
|
+
style: titleWrapStyle,
|
|
246
|
+
accessibilityRole: "header",
|
|
247
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
248
|
+
style: titleStyle,
|
|
249
|
+
children: title
|
|
250
|
+
})
|
|
251
|
+
}), actionsBody]
|
|
185
252
|
});
|
|
186
|
-
}, [
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
253
|
+
}, [showTitle, actionsBody, modes, title, titleWrapStyle, titleStyle]);
|
|
254
|
+
const [footerHeight, setFooterHeight] = useState(estimatedHeight + (showTitle ? 41 : 0));
|
|
255
|
+
|
|
256
|
+
// Token / title presence changes should re-seed the spacer immediately;
|
|
257
|
+
// the next onLayout then corrects to the true measured height.
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
setFooterHeight(estimatedHeight + (showTitle ? 41 : 0));
|
|
260
|
+
}, [estimatedHeight, showTitle, estimatedActionsHeight]);
|
|
261
|
+
const onFooterLayout = event => {
|
|
262
|
+
const next = Math.ceil(event.nativeEvent.layout.height);
|
|
263
|
+
if (next > 0 && next !== footerHeight) {
|
|
264
|
+
setFooterHeight(next);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
const chrome = /*#__PURE__*/_jsx(Animated.View, {
|
|
268
|
+
style: [sticky ? PINNED_STYLE : null, containerStyle, style, animatedStyle],
|
|
269
|
+
onLayout: sticky ? onFooterLayout : undefined,
|
|
197
270
|
accessibilityRole: "toolbar",
|
|
198
271
|
accessibilityLabel: accessibilityLabel,
|
|
199
|
-
children:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
272
|
+
children: body
|
|
273
|
+
});
|
|
274
|
+
if (!sticky) {
|
|
275
|
+
return chrome;
|
|
276
|
+
}
|
|
277
|
+
return /*#__PURE__*/_jsx(View, {
|
|
278
|
+
style: [SHELL_STYLE, {
|
|
279
|
+
height: footerHeight
|
|
280
|
+
}],
|
|
281
|
+
pointerEvents: "box-none",
|
|
282
|
+
children: chrome
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* UI-thread keyboard tracking. Lifts by `keyboard.height - layoutShrink` so:
|
|
288
|
+
* - iOS / Android `adjustPan` / `adjustNothing` → full keyboard height
|
|
289
|
+
* - Android `adjustResize` (this app's default) → ~0 (window already shrank)
|
|
290
|
+
* Never double-counts, never depends on callers wiring KeyboardAvoidingView.
|
|
291
|
+
*/
|
|
292
|
+
function KeyboardAwareActionFooter(props) {
|
|
293
|
+
const {
|
|
294
|
+
height: windowHeight,
|
|
295
|
+
width: windowWidth
|
|
296
|
+
} = useWindowDimensions();
|
|
297
|
+
const maxWindowHeight = useSharedValue(windowHeight);
|
|
298
|
+
const layoutShrink = useSharedValue(0);
|
|
299
|
+
const keyboard = useAnimatedKeyboard();
|
|
300
|
+
|
|
301
|
+
// Orientation / split-screen width changes invalidate the height baseline.
|
|
302
|
+
// Intentionally NOT keyed on `windowHeight` — Android `adjustResize` shrinks
|
|
303
|
+
// height when the keyboard opens, and resetting the baseline there would
|
|
304
|
+
// collapse `layoutShrink` to 0 and double-lift the footer.
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
maxWindowHeight.value = windowHeight;
|
|
307
|
+
layoutShrink.value = 0;
|
|
308
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- width-only baseline
|
|
309
|
+
}, [windowWidth]);
|
|
310
|
+
|
|
311
|
+
// Track how much of the keyboard inset the window has already absorbed.
|
|
312
|
+
// Shared values keep the keyboard worklet free of React re-render lag.
|
|
313
|
+
useEffect(() => {
|
|
314
|
+
if (windowHeight > maxWindowHeight.value) {
|
|
315
|
+
maxWindowHeight.value = windowHeight;
|
|
316
|
+
}
|
|
317
|
+
layoutShrink.value = Math.max(0, maxWindowHeight.value - windowHeight);
|
|
318
|
+
}, [windowHeight, maxWindowHeight, layoutShrink]);
|
|
319
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
320
|
+
const lift = Math.max(0, keyboard.height.value - layoutShrink.value);
|
|
321
|
+
return {
|
|
322
|
+
transform: [{
|
|
323
|
+
translateY: -lift
|
|
324
|
+
}]
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
return /*#__PURE__*/_jsx(ActionFooterChrome, {
|
|
328
|
+
...props,
|
|
329
|
+
sticky: true,
|
|
330
|
+
animatedStyle: animatedStyle
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* ActionFooter — sticky bottom container for primary screen actions.
|
|
336
|
+
*
|
|
337
|
+
* Layout contract (native):
|
|
338
|
+
* - Self-pins to the bottom of its flex-column screen via `marginTop: 'auto'`
|
|
339
|
+
* plus an absolutely positioned chrome inside a height-locked shell.
|
|
340
|
+
* - The shell keeps scroll/flex siblings from painting under the footer —
|
|
341
|
+
* callers do not need to hand-maintain `paddingBottom`.
|
|
342
|
+
* - Keyboard avoidance is owned here: the footer moves up by exactly the
|
|
343
|
+
* keyboard height that layout has not already absorbed. Host
|
|
344
|
+
* `KeyboardAvoidingView` is unnecessary and should not wrap this.
|
|
345
|
+
*
|
|
346
|
+
* Layout contract (web):
|
|
347
|
+
* - In-flow stretch container (Storybook / docs friendly). No keyboard inset.
|
|
348
|
+
*
|
|
349
|
+
* Content contract:
|
|
350
|
+
* - Optional centered `title` (Figma title wrap; Drawer title tokens).
|
|
351
|
+
* - Loose `IconButton`/`Button` children keep the original equal-stretch row.
|
|
352
|
+
* - A sole `ButtonGroup` / `Stack` / `Slot` child is treated as the Figma
|
|
353
|
+
* composite and is not re-stretched internally.
|
|
354
|
+
*
|
|
355
|
+
* @example
|
|
356
|
+
* ```tsx
|
|
357
|
+
* // Legacy — still the recommended simple path
|
|
358
|
+
* <ActionFooter>
|
|
359
|
+
* <IconButton iconName="ic_split" />
|
|
360
|
+
* <Button label="Request" modes={{ AppearanceBrand: 'Secondary' }} />
|
|
361
|
+
* <Button label="Pay" modes={{ AppearanceBrand: 'Primary' }} />
|
|
362
|
+
* </ActionFooter>
|
|
363
|
+
*
|
|
364
|
+
* // Figma shape + title
|
|
365
|
+
* <ActionFooter title="Confirm payment">
|
|
366
|
+
* <ButtonGroup>
|
|
367
|
+
* <Button label="Request" modes={{ AppearanceBrand: 'Secondary' }} />
|
|
368
|
+
* <Button label="Pay" modes={{ AppearanceBrand: 'Primary' }} />
|
|
369
|
+
* </ButtonGroup>
|
|
370
|
+
* </ActionFooter>
|
|
371
|
+
* ```
|
|
372
|
+
*/
|
|
373
|
+
function ActionFooter({
|
|
374
|
+
avoidKeyboard = true,
|
|
375
|
+
...props
|
|
376
|
+
}) {
|
|
377
|
+
// Distinct component types keep `useAnimatedKeyboard` hook order stable
|
|
378
|
+
// (same pattern as ContentSheet). Web never mounts the keyboard path.
|
|
379
|
+
if (STICKY_BY_DEFAULT && avoidKeyboard) {
|
|
380
|
+
return /*#__PURE__*/_jsx(KeyboardAwareActionFooter, {
|
|
381
|
+
...props
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
return /*#__PURE__*/_jsx(ActionFooterChrome, {
|
|
385
|
+
...props,
|
|
386
|
+
sticky: STICKY_BY_DEFAULT
|
|
203
387
|
});
|
|
204
388
|
}
|
|
205
389
|
export default /*#__PURE__*/React.memo(ActionFooter);
|
|
@@ -8,6 +8,25 @@ import Skeleton from '../../skeleton/Skeleton';
|
|
|
8
8
|
import { useSkeleton } from '../../skeleton/SkeletonGroup';
|
|
9
9
|
import GlassFill from '../../utils/GlassFill/GlassFill';
|
|
10
10
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
11
|
+
/**
|
|
12
|
+
* Figma still binds `badge/color/label`, but that alias is broken for
|
|
13
|
+
* `Semantic Intent=System` + `Emphasis=High` (missing VariableID:2811:2401).
|
|
14
|
+
* Use the published tokens that the chain is supposed to land on:
|
|
15
|
+
* - Brand → `Emphasis/badge/brand/fg`
|
|
16
|
+
* - System + High → `Emphasis/badge/high/fg Copy` (AppearanceSystem)
|
|
17
|
+
* - System + Medium/Low → `Emphasis/badge/system/fg`
|
|
18
|
+
*/
|
|
19
|
+
function badgeLabelColorToken(modes) {
|
|
20
|
+
const intent = modes['Semantic Intent'] ?? 'Brand';
|
|
21
|
+
const emphasis = modes.Emphasis ?? 'High';
|
|
22
|
+
if (intent === 'System' && emphasis === 'High') {
|
|
23
|
+
return 'Emphasis/badge/high/fg Copy';
|
|
24
|
+
}
|
|
25
|
+
if (intent === 'System') {
|
|
26
|
+
return 'Emphasis/badge/system/fg';
|
|
27
|
+
}
|
|
28
|
+
return 'Emphasis/badge/brand/fg';
|
|
29
|
+
}
|
|
11
30
|
function Badge({
|
|
12
31
|
label = 'Label',
|
|
13
32
|
type = 'default',
|
|
@@ -32,7 +51,7 @@ function Badge({
|
|
|
32
51
|
const backgroundColor = getVariableByName('badge/color/background', modes);
|
|
33
52
|
const glassBackground = getVariableByName('badge/glass/color/background', modes);
|
|
34
53
|
const glassLabelColor = getVariableByName('badge/glass/color/label', modes);
|
|
35
|
-
const labelColor = isGlass ? glassLabelColor : getVariableByName(
|
|
54
|
+
const labelColor = isGlass ? glassLabelColor : getVariableByName(badgeLabelColorToken(modes), modes);
|
|
36
55
|
const fontSize = Number(getVariableByName('badge/label/fontSize', modes));
|
|
37
56
|
const fontFamily = getVariableByName('badge/fontFamily', modes);
|
|
38
57
|
const fontWeight = getVariableByName('badge/label/fontWeight', modes);
|