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.
Files changed (95) hide show
  1. package/.cursor/D2C-SCREEN.md +2 -1
  2. package/.cursor/commands/D2C.md +2 -1
  3. package/CHANGELOG.md +17 -0
  4. package/lib/commonjs/components/ActionFooter/ActionFooter.js +319 -135
  5. package/lib/commonjs/components/Badge/Badge.js +20 -1
  6. package/lib/commonjs/components/Button/Button.js +139 -47
  7. package/lib/commonjs/components/ButtonGroup/ButtonGroup.js +1 -1
  8. package/lib/commonjs/components/Card/Card.js +151 -112
  9. package/lib/commonjs/components/Card/assets/gold-logo-mark.png +0 -0
  10. package/lib/commonjs/components/CategoryCard/CategoryCard.js +97 -63
  11. package/lib/commonjs/components/CategoryCard/assets/sample.png +0 -0
  12. package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +59 -10
  13. package/lib/commonjs/components/CircularProgressBarDoted/CircularProgressBarDoted.js +1 -1
  14. package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +132 -49
  15. package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +56 -14
  16. package/lib/commonjs/components/IconCapsule/IconCapsule.js +1 -1
  17. package/lib/commonjs/components/MetricData/MetricData.js +26 -13
  18. package/lib/commonjs/components/MoneyValue/MoneyValue.js +1 -1
  19. package/lib/commonjs/components/Nudge/Nudge.js +1 -1
  20. package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +47 -91
  21. package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +52 -45
  22. package/lib/commonjs/components/PortfolioHero/PortfolioHero.js +1 -1
  23. package/lib/commonjs/components/Slider/Slider.js +3 -3
  24. package/lib/commonjs/components/TextInput/TextInput.js +26 -8
  25. package/lib/commonjs/components/Tooltip/Tooltip.js +3 -5
  26. package/lib/commonjs/components/index.js +7 -1
  27. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  28. package/lib/commonjs/design-tokens/figma-modes.generated.js +13 -13
  29. package/lib/commonjs/icons/registry.js +1 -1
  30. package/lib/module/components/ActionFooter/ActionFooter.js +322 -138
  31. package/lib/module/components/Badge/Badge.js +20 -1
  32. package/lib/module/components/Button/Button.js +140 -48
  33. package/lib/module/components/ButtonGroup/ButtonGroup.js +1 -1
  34. package/lib/module/components/Card/Card.js +152 -114
  35. package/lib/module/components/Card/assets/gold-logo-mark.png +0 -0
  36. package/lib/module/components/CategoryCard/CategoryCard.js +97 -63
  37. package/lib/module/components/CategoryCard/assets/sample.png +0 -0
  38. package/lib/module/components/CheckboxItem/CheckboxItem.js +59 -10
  39. package/lib/module/components/CircularProgressBarDoted/CircularProgressBarDoted.js +1 -1
  40. package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +135 -52
  41. package/lib/module/components/HelloJioInput/HelloJioInput.js +56 -14
  42. package/lib/module/components/IconCapsule/IconCapsule.js +1 -1
  43. package/lib/module/components/MetricData/MetricData.js +26 -14
  44. package/lib/module/components/MoneyValue/MoneyValue.js +1 -1
  45. package/lib/module/components/Nudge/Nudge.js +1 -1
  46. package/lib/module/components/PdpCcCard/PdpCcCard.js +48 -92
  47. package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +52 -45
  48. package/lib/module/components/PortfolioHero/PortfolioHero.js +1 -1
  49. package/lib/module/components/Slider/Slider.js +3 -3
  50. package/lib/module/components/TextInput/TextInput.js +27 -9
  51. package/lib/module/components/Tooltip/Tooltip.js +3 -5
  52. package/lib/module/components/index.js +1 -1
  53. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  54. package/lib/module/design-tokens/figma-modes.generated.js +13 -13
  55. package/lib/module/icons/registry.js +1 -1
  56. package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +50 -21
  57. package/lib/typescript/src/components/Button/Button.d.ts +24 -1
  58. package/lib/typescript/src/components/ButtonGroup/ButtonGroup.d.ts +2 -2
  59. package/lib/typescript/src/components/Card/Card.d.ts +54 -25
  60. package/lib/typescript/src/components/CategoryCard/CategoryCard.d.ts +29 -12
  61. package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +27 -9
  62. package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +37 -12
  63. package/lib/typescript/src/components/IconCapsule/IconCapsule.d.ts +1 -1
  64. package/lib/typescript/src/components/MetricData/MetricData.d.ts +7 -5
  65. package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +16 -6
  66. package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +9 -2
  67. package/lib/typescript/src/components/index.d.ts +2 -2
  68. package/lib/typescript/src/icons/registry.d.ts +1 -1
  69. package/package.json +1 -1
  70. package/src/components/ActionFooter/ActionFooter.tsx +375 -141
  71. package/src/components/Badge/Badge.tsx +21 -1
  72. package/src/components/Button/Button.tsx +192 -44
  73. package/src/components/ButtonGroup/ButtonGroup.tsx +2 -2
  74. package/src/components/Card/Card.tsx +370 -212
  75. package/src/components/Card/assets/gold-logo-mark.png +0 -0
  76. package/src/components/CategoryCard/CategoryCard.tsx +130 -84
  77. package/src/components/CategoryCard/assets/sample.png +0 -0
  78. package/src/components/CheckboxItem/CheckboxItem.tsx +72 -12
  79. package/src/components/CircularProgressBarDoted/CircularProgressBarDoted.tsx +1 -1
  80. package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +186 -67
  81. package/src/components/HelloJioInput/HelloJioInput.tsx +58 -17
  82. package/src/components/IconCapsule/IconCapsule.tsx +1 -1
  83. package/src/components/MetricData/MetricData.tsx +39 -22
  84. package/src/components/MoneyValue/MoneyValue.tsx +1 -1
  85. package/src/components/Nudge/Nudge.tsx +1 -1
  86. package/src/components/PdpCcCard/PdpCcCard.tsx +41 -92
  87. package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +73 -58
  88. package/src/components/PortfolioHero/PortfolioHero.tsx +1 -1
  89. package/src/components/Slider/Slider.tsx +3 -3
  90. package/src/components/TextInput/TextInput.tsx +26 -9
  91. package/src/components/Tooltip/Tooltip.tsx +3 -5
  92. package/src/components/index.ts +9 -2
  93. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  94. package/src/design-tokens/figma-modes.generated.ts +13 -13
  95. package/src/icons/registry.ts +1 -1
@@ -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 and stop:
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.
@@ -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. Stop on unresolved 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
- // The runtime token a slot child must equal (by reference) to be treated as
51
- // an IconButton. `IconButton` is exported wrapped in `React.memo`, so the
52
- // element.type identity comparison works for both `<IconButton />` from the
53
- // same module and any `React.memo`-wrapped re-export. The fallback check
54
- // (`type.type === IconButton`) catches one extra layer of `forwardRef` /
55
- // `memo` wrapping which can happen when consumers re-export the component.
56
- function isIconButtonElement(element) {
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 === _IconButton.default) return true;
59
- if (t && typeof t === 'object' && t.type === _IconButton.default) return true;
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
- * ActionFooter a sticky bottom container for primary screen actions.
65
- *
66
- * Layout contract:
67
- * - The outer container stretches horizontally (`alignSelf: 'stretch'`) so
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 ActionFooter({
92
- children,
93
- modes = _reactUtils.EMPTY_MODES,
94
- style,
95
- accessibilityLabel
96
- }) {
97
- // -------------------------------------------------------------------------
98
- // Keep the footer locked in place behind the software keyboard (Android).
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
- // All token reads collapsed into a single useMemo keyed on `modes`. With
136
- // the shared `EMPTY_MODES` default this resolves once for the common path
137
- // and never re-allocates the container/slot style objects between renders.
138
- const {
139
- containerStyle,
140
- slotStyle
141
- } = (0, _react.useMemo)(() => {
142
- const backgroundColor = (0, _figmaVariablesResolver.getVariableByName)('actionFooter/background', modes) ?? '#ffffff';
143
- const gap = (0, _figmaVariablesResolver.getVariableByName)('actionFooter/gap', modes) ?? 8;
144
- const paddingHorizontal = (0, _figmaVariablesResolver.getVariableByName)('actionFooter/padding/horizontal', modes) ?? 16;
145
- const paddingTop = (0, _figmaVariablesResolver.getVariableByName)('actionFooter/padding/top', modes) ?? 10;
146
- const paddingBottom = (0, _figmaVariablesResolver.getVariableByName)('actionFooter/padding/bottom', modes) ?? 41;
147
- const container = {
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
- const slot = {
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
- containerStyle: container,
168
- slotStyle: slot
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
- // Process children once per (children, modes) tuple:
173
- // 1. Flatten Fragments so each action is its own keyed sibling.
174
- // 2. Push `modes` down so callers don't have to thread it manually.
175
- // 3. Auto-stretch every non-IconButton with the Yoga-safe stretch style.
176
- //
177
- // The result identity is stable across re-renders when the inputs don't
178
- // change, which keeps the `React.memo`-wrapped Button/IconButton children
179
- // from re-rendering for no reason.
180
- const enhancedChildren = (0, _react.useMemo)(() => {
181
- const flat = (0, _reactUtils.flattenChildren)(children);
182
- const withModes = (0, _reactUtils.cloneChildrenWithModes)(flat, modes);
183
- return withModes.map((child, index) => {
184
- if (! /*#__PURE__*/_react.default.isValidElement(child)) return child;
185
- const element = child;
186
- if (isIconButtonElement(element)) return element;
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
- }, [children, modes]);
193
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, {
194
- style: [containerStyle, style,
195
- // Counter-translate by the keyboard height on Android so `adjustResize`
196
- // can't lift the footer above the keyboard (no-op on iOS/web where the
197
- // value stays at 0).
198
- {
199
- transform: [{
200
- translateY: keyboardOffset
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: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
206
- style: slotStyle,
207
- children: enhancedChildren
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)('badge/color/label', modes);
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);