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