jfs-components 0.1.36 → 0.1.48

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 (47) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/lib/commonjs/components/Accordion/Accordion.js +16 -3
  3. package/lib/commonjs/components/AppBar/AppBar.js +118 -93
  4. package/lib/commonjs/components/CompareTable/CompareTable.js +98 -33
  5. package/lib/commonjs/components/FormField/FormField.js +9 -1
  6. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +4 -14
  7. package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +20 -4
  8. package/lib/commonjs/components/PageHero/PageHero.js +3 -0
  9. package/lib/commonjs/components/ProductMerchandisingCard/ProductMerchandisingCard.js +50 -30
  10. package/lib/commonjs/components/ValueBackMetric/ValueBackMetric.js +117 -89
  11. package/lib/commonjs/components/index.js +7 -1
  12. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -42204
  13. package/lib/commonjs/icons/registry.js +1 -1
  14. package/lib/module/components/Accordion/Accordion.js +16 -3
  15. package/lib/module/components/AppBar/AppBar.js +116 -93
  16. package/lib/module/components/CompareTable/CompareTable.js +99 -34
  17. package/lib/module/components/FormField/FormField.js +9 -1
  18. package/lib/module/components/FullscreenModal/FullscreenModal.js +4 -14
  19. package/lib/module/components/HelloJioInput/HelloJioInput.js +20 -4
  20. package/lib/module/components/PageHero/PageHero.js +3 -0
  21. package/lib/module/components/ProductMerchandisingCard/ProductMerchandisingCard.js +50 -30
  22. package/lib/module/components/ValueBackMetric/ValueBackMetric.js +120 -93
  23. package/lib/module/components/index.js +1 -1
  24. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -42204
  25. package/lib/module/icons/registry.js +1 -1
  26. package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -1
  27. package/lib/typescript/src/components/AppBar/AppBar.d.ts +46 -14
  28. package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +29 -12
  29. package/lib/typescript/src/components/HelloJioInput/HelloJioInput.d.ts +6 -4
  30. package/lib/typescript/src/components/PageHero/PageHero.d.ts +1 -0
  31. package/lib/typescript/src/components/ProductMerchandisingCard/ProductMerchandisingCard.d.ts +8 -1
  32. package/lib/typescript/src/components/ValueBackMetric/ValueBackMetric.d.ts +27 -16
  33. package/lib/typescript/src/components/index.d.ts +1 -1
  34. package/lib/typescript/src/icons/registry.d.ts +1 -1
  35. package/package.json +1 -1
  36. package/src/components/Accordion/Accordion.tsx +61 -33
  37. package/src/components/AppBar/AppBar.tsx +154 -124
  38. package/src/components/CompareTable/CompareTable.tsx +138 -52
  39. package/src/components/FormField/FormField.tsx +7 -0
  40. package/src/components/FullscreenModal/FullscreenModal.tsx +5 -14
  41. package/src/components/HelloJioInput/HelloJioInput.tsx +31 -12
  42. package/src/components/PageHero/PageHero.tsx +5 -0
  43. package/src/components/ProductMerchandisingCard/ProductMerchandisingCard.tsx +63 -30
  44. package/src/components/ValueBackMetric/ValueBackMetric.tsx +178 -104
  45. package/src/components/index.ts +6 -1
  46. package/src/design-tokens/Coin Variables-variables-full.json +1 -42204
  47. package/src/icons/registry.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,35 @@ 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.48] - 2026-07-22
8
+
9
+ - `CompareTable` (fixed-width scroll mode) — selection cards pin above the scroll surface (`stickyCards`, default `true`); accordion section titles stick under the cards and replace each other as you scroll (`stickyAccordionTitles`, default `true`); gray table-header sticky is now opt-in (`stickyHeaders` default `false`). Selection cards are square (corner radius forced to `0`).
10
+ - `Accordion` — new `showHeader` / `showContent` props so hosts can split sticky titles from bodies in scroll layouts (used by `CompareTable`).
11
+ - `HelloJioInput` — `leading={null}` hides the leading icon; IdleJioPlus is borderless frosted glass so blur shows through.
12
+ - `ProductMerchandisingCard` — new `badgeIcon`; footer special badge fully token-driven; footer text column keeps a min-width so CTAs don’t crush title/subtitle.
13
+ - `ValueBackMetric` — CTA is purple non-underlined `Text` (not `Link`), with optional `link` override slot; layout/typography resolve via `valueBackMetric/*`.
14
+ - Token sync — refreshed Coin Variables / `.token-metadata.json` / icon registry for the above components.
15
+
16
+ ---
17
+
18
+ ## [0.1.38] - 2026-07-22
19
+
20
+ - `FormField` — caret / cursor / selection color now follows the tokenized input text color (no platform blue/green accent).
21
+ - `FullscreenModal` — sticky footer no longer adds safe-area bottom padding on top of `ActionFooter` tokens (avoids double inset).
22
+
23
+ ---
24
+
25
+ ## [0.1.36] - 2026-07-21
26
+
27
+ - `AppBar` — exported `JioDot` brand mark for MainPage `leadingSlot` (no default logo; opt in with `<JioDot />`). Also exports `AppBarProps` / `JioDotProps`.
28
+ - `PageHero` — new `body` / `showBody` (and `showEyebrow`) for a second text line; media sizing moves to `video/*` tokens + `Video / Output` mode (117px fallback).
29
+ - `LottiePlayer` — default size tokens renamed `media/*` → `video/*`; mode key is now `Video / Output` (**breaking** for callers still passing `Media / Output`).
30
+ - Library-wide mode/docs refresh (`figma-modes.generated.ts`, Coin Variables, component MDX).
31
+
32
+ > Note: `0.1.35` was published on npm as an intermediate build; the next documented release is `0.1.36`.
33
+
34
+ ---
35
+
7
36
  ## [0.1.34] - 2026-07-21
8
37
 
9
38
  - `jfs-extract-modes` now automatically loads `FIGMA_ACCESS_TOKEN` from the
@@ -68,6 +68,8 @@ function Accordion({
68
68
  accessibilityState,
69
69
  webAccessibilityProps,
70
70
  disableTruncation,
71
+ showHeader = true,
72
+ showContent = true,
71
73
  ...rest
72
74
  }) {
73
75
  const [internalExpanded, setInternalExpanded] = (0, _react.useState)(defaultExpanded);
@@ -105,8 +107,11 @@ function Accordion({
105
107
  const contentPaddingTop = (0, _figmaVariablesResolver.getVariableByName)('accordion/content/padding/top', resolvedModes) ?? 8;
106
108
  const contentPaddingBottom = (0, _figmaVariablesResolver.getVariableByName)('accordion/content/padding/bottom', resolvedModes) ?? 8;
107
109
  const borderColor = (0, _figmaVariablesResolver.getVariableByName)('accordion/border/color', resolvedModes) ?? '#e6e6e6';
110
+
111
+ // Header-only sticky pieces drop the divider while expanded so the paired
112
+ // content Accordion owns the section border.
108
113
  const containerStyle = {
109
- borderBottomWidth: 1,
114
+ borderBottomWidth: !showContent && isExpanded ? 0 : 1,
110
115
  borderBottomColor: borderColor
111
116
  };
112
117
  const headerStyle = {
@@ -143,10 +148,18 @@ function Accordion({
143
148
  webAccessibilityProps
144
149
  });
145
150
  const processedChildren = children ? (0, _reactUtils.cloneChildrenWithModes)(_react.default.Children.toArray(children), resolvedModes) : null;
151
+
152
+ // Content-only + collapsed: nothing to render (header lives in the sticky peer).
153
+ if (!showHeader && !isExpanded) {
154
+ return null;
155
+ }
156
+ if (!showHeader && !showContent) {
157
+ return null;
158
+ }
146
159
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
147
160
  style: [containerStyle, style],
148
161
  ...rest,
149
- children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
162
+ children: [showHeader && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
150
163
  accessibilityRole: "button",
151
164
  accessibilityLabel: undefined,
152
165
  accessibilityHint: accessibilityHint || (isExpanded ? 'Collapse accordion' : 'Expand accordion'),
@@ -176,7 +189,7 @@ function Accordion({
176
189
  accessibilityElementsHidden: true,
177
190
  importantForAccessibility: "no"
178
191
  })]
179
- }), isExpanded && processedChildren && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
192
+ }), showContent && isExpanded && processedChildren && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
180
193
  style: contentStyle,
181
194
  children: processedChildren
182
195
  })]
@@ -3,22 +3,98 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = AppBar;
7
- var _react = _interopRequireDefault(require("react"));
6
+ exports.JioDot = JioDot;
7
+ exports.default = void 0;
8
+ var _react = _interopRequireWildcard(require("react"));
8
9
  var _reactNative = require("react-native");
10
+ var _reactNativeSvg = _interopRequireWildcard(require("react-native-svg"));
9
11
  var _figmaVariablesResolver = require("../../design-tokens/figma-variables-resolver");
10
12
  var _JFSThemeProvider = require("../../design-tokens/JFSThemeProvider");
11
13
  var _NavArrow = _interopRequireDefault(require("../NavArrow/NavArrow"));
12
14
  var _reactUtils = require("../../utils/react-utils");
13
15
  var _jsxRuntime = require("react/jsx-runtime");
14
16
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
15
- // SubPage "slot wrap" geometry, taken directly from the Figma design
16
- // (node 449:7876). The middle slot is an absolutely-centered box of a fixed
17
- // size; its inner content (node 3991:4125) is a `flex: 1 0 0; min-width: 1px`
18
- // item so it fills / shrinks responsively within that box.
17
+ 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); }
18
+ // SubPage "slot wrap" geometry from Figma (node 449:7876). The middle slot is
19
+ // an absolutely-centered fixed-size box; its inner content (node 3991:4125)
20
+ // fills / shrinks responsively within that box.
19
21
  const SUBPAGE_MIDDLE_DEFAULT_WIDTH = 192;
20
22
  const SUBPAGE_MIDDLE_HEIGHT = 32;
21
23
  const SUBPAGE_MIDDLE_PADDING_HORIZONTAL = 21;
24
+ const JIODOT_FALLBACK_SIZE = 32;
25
+ /**
26
+ * Figma "jiodot" mark (node 1117:1908). Opt-in via `leadingSlot` — MainPage
27
+ * does NOT render this by default so existing consumers stay empty-leading.
28
+ */
29
+ function JioDot({
30
+ size,
31
+ modes = _reactUtils.EMPTY_MODES
32
+ }) {
33
+ const resolvedSize = size ?? (Number((0, _figmaVariablesResolver.getVariableByName)('appBar/mainPage/jiodot/size', modes)) || JIODOT_FALLBACK_SIZE);
34
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeSvg.default, {
35
+ width: resolvedSize,
36
+ height: resolvedSize,
37
+ viewBox: "0 0 32 32",
38
+ fill: "none",
39
+ accessibilityRole: "image",
40
+ accessibilityLabel: "Jio",
41
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeSvg.G, {
42
+ clipPath: "url(#jiodotClip)",
43
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.Path, {
44
+ d: "M0 16C0 7.16344 7.16344 0 16 0C24.8366 0 32 7.16344 32 16C32 24.8366 24.8366 32 16 32C7.16344 32 0 24.8366 0 16Z",
45
+ fill: "url(#jiodotGrad)"
46
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.Path, {
47
+ d: "M11.3039 9.64982H10.7705C9.75837 9.64982 9.20548 10.2205 9.20548 11.3621V16.8679C9.20548 18.2854 8.72667 18.7829 7.60341 18.7829C6.71985 18.7829 6.00192 18.3959 5.43096 17.6963C5.37555 17.6229 4.21615 18.1749 4.21615 19.5378C4.21615 21.0107 5.59629 21.9132 8.15689 21.9132C11.268 21.9132 12.9077 20.3479 12.9077 16.9233V11.3621C12.9059 10.222 12.3536 9.64982 11.3039 9.64982ZM23.7039 12.3381C20.6837 12.3381 18.6769 14.2531 18.6769 17.1085C18.6769 20.0365 20.6096 21.915 23.6482 21.915C26.6674 21.915 28.6556 20.0365 28.6556 17.1274C28.6573 14.2531 26.6867 12.3381 23.7039 12.3381ZM23.6668 19.206C22.4879 19.206 21.6781 18.3408 21.6781 17.107C21.6781 15.8922 22.5077 15.0264 23.6668 15.0264C24.8259 15.0264 25.6553 15.8922 25.6553 17.1257C25.6556 18.3221 24.8084 19.206 23.6668 19.206ZM16.1252 12.43H15.7563C14.8547 12.43 14.1735 12.8531 14.1735 14.1426V20.0347C14.1735 21.3423 14.8366 21.7476 15.7936 21.7476H16.1622C17.0644 21.7476 17.7086 21.3052 17.7086 20.0347V14.1426C17.7086 12.8169 17.0831 12.43 16.1252 12.43ZM15.9228 8.15885C14.7809 8.15885 14.063 8.80359 14.063 9.81634C14.063 10.8469 14.7999 11.4913 15.9779 11.4913C17.1196 11.4913 17.8378 10.8469 17.8378 9.81634C17.8378 8.78582 17.1012 8.15885 15.9228 8.15885Z",
48
+ fill: "white"
49
+ })]
50
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeSvg.Defs, {
51
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeSvg.LinearGradient, {
52
+ id: "jiodotGrad",
53
+ x1: "11.085",
54
+ y1: "9.25442",
55
+ x2: "22.3469",
56
+ y2: "15.7638",
57
+ gradientUnits: "userSpaceOnUse",
58
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.Stop, {
59
+ stopColor: "#FBDCAF"
60
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.Stop, {
61
+ offset: "1",
62
+ stopColor: "#AD8444"
63
+ })]
64
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.ClipPath, {
65
+ id: "jiodotClip",
66
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.Rect, {
67
+ width: "32",
68
+ height: "32",
69
+ fill: "white"
70
+ })
71
+ })]
72
+ })]
73
+ });
74
+ }
75
+ /**
76
+ * AppBar — top navigation bar for MainPage and SubPage layouts
77
+ * (Figma node 1070:18571).
78
+ *
79
+ * Visual values resolve from design tokens via `getVariableByName`. Slots
80
+ * cascade `modes` to children through `cloneChildrenWithModes`.
81
+ *
82
+ * @component
83
+ * @example
84
+ * ```tsx
85
+ * <AppBar
86
+ * type="MainPage"
87
+ * leadingSlot={<JioDot />}
88
+ * actionsSlot={
89
+ * <>
90
+ * <IconButton iconName="ic_add" />
91
+ * <IconButton iconName="ic_add" />
92
+ * <Avatar />
93
+ * </>
94
+ * }
95
+ * />
96
+ * ```
97
+ */
22
98
  function AppBar({
23
99
  type = 'MainPage',
24
100
  leadingSlot,
@@ -35,118 +111,70 @@ function AppBar({
35
111
  const {
36
112
  modes: globalModes
37
113
  } = (0, _JFSThemeProvider.useTokens)();
38
- const modes = {
114
+ const modes = (0, _react.useMemo)(() => ({
39
115
  Context2: 'AppBar',
40
116
  ...globalModes,
41
117
  ...propModes
42
- };
118
+ }), [globalModes, propModes]);
43
119
  const isMain = type === 'MainPage';
44
120
  const isSub = type === 'SubPage';
45
-
46
- // --- Tokens ---
47
- // Construct token keys based on type. keys must be camelCase as per instruction.
48
- // We use explicit string literals so that the extraction script can find them.
49
121
  const paddingHorizontal = isMain ? (0, _figmaVariablesResolver.getVariableByName)('appBar/mainPage/padding/horizontal', modes) : (0, _figmaVariablesResolver.getVariableByName)('appBar/subPage/padding/horizontal', modes);
50
122
  const paddingVertical = isMain ? (0, _figmaVariablesResolver.getVariableByName)('appBar/mainPage/padding/vertical', modes) : (0, _figmaVariablesResolver.getVariableByName)('appBar/subPage/padding/vertical', modes);
51
123
  const backgroundColor = isMain ? (0, _figmaVariablesResolver.getVariableByName)('appBar/mainPage/background', modes) : (0, _figmaVariablesResolver.getVariableByName)('appBar/subPage/background', modes);
52
124
  const actionsGap = isMain ? (0, _figmaVariablesResolver.getVariableByName)('appBar/mainPage/actions/gap', modes) : (0, _figmaVariablesResolver.getVariableByName)('appBar/subPage/actions/gap', modes);
53
-
54
- // Layout styles
55
125
  const containerStyle = {
56
126
  flexDirection: 'row',
57
127
  alignItems: 'center',
58
- // No `justifyContent` here: with the inline middle slot using `flex: 1`
59
- // the three sections lay out naturally (leading | middle | actions).
60
- // When middleSlot is absent we fall back to `space-between` at the wrapper
61
- // level so leading & actions still anchor to the edges.
62
128
  paddingHorizontal: paddingHorizontal ?? 16,
63
129
  paddingVertical: paddingVertical ?? (isMain ? 16 : 10),
64
130
  backgroundColor: backgroundColor ?? '#FFFFFF'
65
- // MainPage: h=68 (16 top/bot padding? 36 height content?)
66
- // SubPage: h=52
67
131
  };
68
132
 
69
- // --- Leading Slot Default ---
70
- // SubPage default: Back Arrow
71
- // MainPage default: Jio Logo (We'll use a placeholder or basic text if no asset,
72
- // but looking at "AppBarHome" before, it had "ic_hellojio". Let's stick to generic defaults or empty if not provided,
73
- // but usually a component library provides sensible defaults.)
74
-
75
- // Actually, let's implement the specific defaults mentioned in the plan/analysis
76
- // SubPage: NavArrow props (icon, etc).
77
-
133
+ // MainPage: no default leading (backward compatible — consumers opt in via
134
+ // leadingSlot, e.g. <JioDot />). SubPage: default back NavArrow.
78
135
  let defaultLeading = null;
79
- if (leadingSlot === undefined) {
80
- if (isSub) {
81
- defaultLeading = /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
82
- ...(onLeadingPress ? {
83
- onPress: onLeadingPress
84
- } : {}),
85
- hitSlop: 8,
86
- accessibilityRole: "button",
87
- accessibilityLabel: undefined,
88
- style: ({
89
- pressed
90
- }) => ({
91
- opacity: pressed ? 0.7 : 1,
92
- alignItems: 'center',
93
- justifyContent: 'center'
94
- }),
95
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_NavArrow.default, {
96
- direction: "Back",
97
- modes: modes
98
- })
99
- });
100
- } else {
101
- // MainPage default leading: often the Brand Logo.
102
- // We'll leave it empty or render a placeholder if desired, but standard AppBar often lets consumer pass logo.
103
- // Previous code had `leadingIconName='ic_hellojio'` for Home.
104
- // We will render nothing by default for MainPage leading to keep it flexible, OR a specialized Logo component if we had one.
105
- // For now, let's assume consumer passes it for MainPage, or we leave empty.
106
- // Wait, Figma screenshot shows "Jio" logo.
107
- // The user said: "mostly token namings are 'camelCase'. Maximize existing component usage."
108
- // I'll leave defaultLeading as null for MainPage unless I have a "BrandLogo" component.
109
- }
136
+ if (leadingSlot === undefined && isSub) {
137
+ defaultLeading = /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
138
+ ...(onLeadingPress ? {
139
+ onPress: onLeadingPress
140
+ } : {}),
141
+ hitSlop: 8,
142
+ accessibilityRole: "button",
143
+ accessibilityLabel: undefined,
144
+ style: ({
145
+ pressed
146
+ }) => ({
147
+ opacity: pressed ? 0.7 : 1,
148
+ alignItems: 'center',
149
+ justifyContent: 'center'
150
+ }),
151
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_NavArrow.default, {
152
+ direction: "Back",
153
+ modes: modes
154
+ })
155
+ });
110
156
  }
111
-
112
- // --- Process Slots ---
113
157
  const processedLeading = leadingSlot ? (0, _reactUtils.cloneChildrenWithModes)([leadingSlot], modes)[0] : defaultLeading;
114
158
  const processedMiddle = middleSlot ? (0, _reactUtils.cloneChildrenWithModes)(_react.default.Children.toArray(middleSlot), modes) : null;
115
-
116
- // Actions Gap wrapper
117
- // The Figma has "Actions" slot with a gap.
118
159
  const actionsStyle = {
119
160
  flexDirection: 'row',
120
161
  alignItems: 'center',
121
162
  gap: actionsGap ?? 0
122
163
  };
123
- const processedActions = actionsSlot ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
124
- style: actionsStyle,
125
- children: (0, _reactUtils.cloneChildrenWithModes)(_react.default.Children.toArray(actionsSlot), modes)
126
- }) : null;
164
+ const processedActions = actionsSlot ? (0, _reactUtils.cloneChildrenWithModes)(_react.default.Children.toArray(actionsSlot), modes) : null;
127
165
 
128
- // SubPage centers its middle slot via absolute positioning (see Figma
129
- // "slot wrap"), so it never participates in the row flow. Only MainPage
130
- // keeps the legacy in-flow middle slot.
166
+ // SubPage middle is absolute (Figma "slot wrap"); MainPage middle stays in-flow.
131
167
  const hasInFlowMiddle = isMain && !!processedMiddle;
132
168
 
133
- // With an in-flow middle (MainPage) the middle (flex: 1) absorbs the
134
- // remaining space, so leading & actions sit at the edges naturally. In all
135
- // other cases we pin leading & actions to the outer edges with
136
- // `space-between`; the SubPage middle floats above, centered.
169
+ // With an in-flow middle the flex:1 middle absorbs leftover space. Otherwise
170
+ // pin leading & actions to the edges; SubPage middle floats centered above.
137
171
  const wrapperStyle = {
138
172
  ...containerStyle,
139
173
  justifyContent: hasInFlowMiddle ? 'flex-start' : 'space-between'
140
174
  };
141
175
 
142
- // Absolutely-positioned overlay for the SubPage middle slot. Instead of
143
- // centering a fixed-height box with `top/left: 50%` + transform (which on
144
- // Android resolves the percentage against an intermediate, content-driven
145
- // parent height and lands a few px too high), the overlay STRETCHES to fill
146
- // the whole bar (`top/bottom/left/right: 0`) and centers its content with
147
- // flexbox. This uses the exact same full-height vertical reference as the
148
- // leading/actions row (`alignItems: 'center'`), so the middle content always
149
- // sits on the same line as the back arrow and end slot on every platform.
176
+ // Overlay stretches to the full bar and centers content with flexbox so the
177
+ // middle aligns with leading/actions on every platform.
150
178
  const subPageMiddleOverlayStyle = {
151
179
  position: 'absolute',
152
180
  top: 0,
@@ -157,10 +185,6 @@ function AppBar({
157
185
  alignItems: 'center',
158
186
  justifyContent: 'center'
159
187
  };
160
-
161
- // Fixed-width clipping box (mirrors the Figma "slot wrap" geometry): keeps the
162
- // middle content from bleeding under the leading/actions slots while staying
163
- // centered within the overlay above.
164
188
  const subPageMiddleBoxStyle = {
165
189
  width: middleSlotWidth,
166
190
  height: SUBPAGE_MIDDLE_HEIGHT,
@@ -184,7 +208,7 @@ function AppBar({
184
208
  alignItems: 'center'
185
209
  },
186
210
  children: processedLeading
187
- }), hasInFlowMiddle && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
211
+ }), hasInFlowMiddle ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
188
212
  style: {
189
213
  flex: 1,
190
214
  minWidth: 0,
@@ -194,10 +218,10 @@ function AppBar({
194
218
  },
195
219
  pointerEvents: "box-none",
196
220
  children: processedMiddle
197
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
221
+ }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
198
222
  style: actionsStyle,
199
223
  children: processedActions
200
- }), isSub && processedMiddle && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
224
+ }), isSub && processedMiddle ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
201
225
  style: subPageMiddleOverlayStyle,
202
226
  pointerEvents: "box-none",
203
227
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
@@ -216,6 +240,7 @@ function AppBar({
216
240
  children: processedMiddle
217
241
  })
218
242
  })
219
- })]
243
+ }) : null]
220
244
  });
221
- }
245
+ }
246
+ var _default = exports.default = AppBar;
@@ -114,7 +114,9 @@ function CompareTable({
114
114
  style,
115
115
  disableTruncation,
116
116
  columnWidth,
117
- stickyHeaders = true,
117
+ stickyCards = true,
118
+ stickyAccordionTitles = true,
119
+ stickyHeaders = false,
118
120
  stickyHeaderLabel = true
119
121
  }) {
120
122
  // --- selection card tokens ------------------------------------------------
@@ -129,11 +131,10 @@ function CompareTable({
129
131
  const cardLabelFontFamily = (0, _figmaVariablesResolver.getVariableByName)('selectionCard/label/fontfamily', modes) ?? 'JioType Var';
130
132
  const cardLabelFontWeight = (0, _figmaVariablesResolver.getVariableByName)('selectionCard/label/fontweight', modes) ?? 700;
131
133
  const imageRadius = (0, _figmaVariablesResolver.getVariableByName)('image/radius', modes) ?? 8;
132
- // `selectionCard/radius` (Figma node 5568:3487) rounds the card corners.
133
- // The published token store is still out of sync (the variable resolves to
134
- // `0` in the design), so the read falls back to `0` and will pick up a
135
- // rounded value automatically once the store syncs.
136
- const cardRadius = (0, _figmaVariablesResolver.getVariableByName)('selectionCard/radius', modes) ?? 0;
134
+ // Selection cards are square no corner radius (Figma compare-table
135
+ // treatment). Ignore `selectionCard/radius` so a non-zero token value
136
+ // cannot round the card edges.
137
+ const cardRadius = 0;
137
138
 
138
139
  // --- add-a-card icon button tokens ---------------------------------------
139
140
  // padding/size/border come from the shared iconButton tokens; the filled
@@ -192,6 +193,16 @@ function CompareTable({
192
193
  * scrollTo echoes from synced peers are ignored, eliminating jitter. */
193
194
  const draggingSource = (0, _react.useRef)(null);
194
195
  const lastSyncX = (0, _react.useRef)(0);
196
+
197
+ // Controlled expand state for sticky accordion titles (header + body are
198
+ // split across two Accordion instances that must stay in sync).
199
+ const [expandedByIndex, setExpandedByIndex] = (0, _react.useState)(() => {
200
+ const initial = {};
201
+ sections.forEach((section, index) => {
202
+ initial[index] = section.defaultExpanded ?? index === 0;
203
+ });
204
+ return initial;
205
+ });
195
206
  const syncOthers = (0, _react.useCallback)((source, x) => {
196
207
  if (source !== 'cards' && cardsScrollRef.current) {
197
208
  cardsScrollRef.current.scrollTo({
@@ -300,6 +311,10 @@ function CompareTable({
300
311
  minHeight: 147,
301
312
  backgroundColor: cardBg,
302
313
  borderRadius: cardRadius,
314
+ borderTopLeftRadius: cardRadius,
315
+ borderTopRightRadius: cardRadius,
316
+ borderBottomLeftRadius: cardRadius,
317
+ borderBottomRightRadius: cardRadius,
303
318
  alignItems: 'center',
304
319
  justifyContent: 'center',
305
320
  gap: cardGap,
@@ -513,6 +528,7 @@ function CompareTable({
513
528
  backgroundColor: cardBg,
514
529
  borderWidth: 1,
515
530
  borderColor: '#e8e8e8',
531
+ borderRadius: 0,
516
532
  overflow: 'hidden'
517
533
  };
518
534
 
@@ -541,14 +557,18 @@ function CompareTable({
541
557
  }
542
558
 
543
559
  // --- fixed-width scroll mode: chained horizontal sync + sticky cards -----
544
- // Build children and stickyHeaderIndices for the vertical ScrollView.
545
- // When stickyHeaders is on, section headers are injected as direct children
546
- // so React Native's stickyHeaderIndices can pin them with stacking.
547
- const scrollChildren = [];
548
- const stickyIndices = [];
549
- if (showCardsRow) {
550
- stickyIndices.push(scrollChildren.length);
551
- scrollChildren.push(/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
560
+ // Sticky cards are pinned *outside* the vertical ScrollView (sibling above
561
+ // it) so they cannot scroll away `stickyHeaderIndices` is unreliable for
562
+ // a nested horizontal ScrollView on Fabric. Sticky accordion titles /
563
+ // table headers still use stickyHeaderIndices inside the scroll surface
564
+ // and therefore stick directly under the pinned cards.
565
+ const renderCardsRow = () => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
566
+ style: {
567
+ width: '100%',
568
+ backgroundColor: cardBg,
569
+ zIndex: 2
570
+ },
571
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
552
572
  ref: cardsScrollRef,
553
573
  horizontal: true,
554
574
  showsHorizontalScrollIndicator: false,
@@ -564,11 +584,51 @@ function CompareTable({
564
584
  },
565
585
  children: [columns.map(renderCard), showAddCard && renderAddCard()]
566
586
  })
567
- }, "cards-row"));
587
+ })
588
+ }, "cards-row");
589
+ const scrollChildren = [];
590
+ const stickyIndices = [];
591
+
592
+ // Non-sticky cards scroll away with the sections (inside the ScrollView).
593
+ if (showCardsRow && !stickyCards) {
594
+ scrollChildren.push(renderCardsRow());
568
595
  }
569
596
  sections.forEach((section, index) => {
570
597
  const sectionKey = section.key ?? section.title ?? index;
571
598
  const headerContent = renderHeaderContent(section);
599
+ const expanded = expandedByIndex[index] ?? section.defaultExpanded ?? index === 0;
600
+ const onExpandedChange = next => {
601
+ setExpandedByIndex(prev => ({
602
+ ...prev,
603
+ [index]: next
604
+ }));
605
+ };
606
+ const inlineHeader = !stickyHeaders && headerContent != null ? stickyHeaderLabel ? headerContent : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
607
+ horizontal: true,
608
+ showsHorizontalScrollIndicator: false,
609
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
610
+ style: {
611
+ width: contentWidth
612
+ },
613
+ children: headerContent
614
+ })
615
+ }) : null;
616
+ if (stickyAccordionTitles) {
617
+ stickyIndices.push(scrollChildren.length);
618
+ scrollChildren.push(/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
619
+ style: {
620
+ width: '100%',
621
+ backgroundColor: cardBg
622
+ },
623
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Accordion.default, {
624
+ title: section.title,
625
+ expanded: expanded,
626
+ onExpandedChange: onExpandedChange,
627
+ modes: modes,
628
+ showContent: false
629
+ })
630
+ }, `sticky-accordion-title-${sectionKey}`));
631
+ }
572
632
  if (stickyHeaders && headerContent != null) {
573
633
  stickyIndices.push(scrollChildren.length);
574
634
  scrollChildren.push(/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
@@ -587,37 +647,42 @@ function CompareTable({
587
647
  })
588
648
  }, `sticky-header-${sectionKey}`));
589
649
  }
590
- scrollChildren.push(/*#__PURE__*/(0, _jsxRuntime.jsxs)(_Accordion.default, {
591
- title: section.title,
592
- defaultExpanded: section.defaultExpanded ?? index === 0,
593
- modes: modes,
594
- children: [!stickyHeaders && headerContent != null && (stickyHeaderLabel ? headerContent : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
595
- horizontal: true,
596
- showsHorizontalScrollIndicator: false,
597
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
598
- style: {
599
- width: contentWidth
600
- },
601
- children: headerContent
602
- })
603
- })), renderBodyContent(section, index)]
604
- }, sectionKey));
650
+ if (stickyAccordionTitles) {
651
+ scrollChildren.push(/*#__PURE__*/(0, _jsxRuntime.jsxs)(_Accordion.default, {
652
+ title: section.title,
653
+ expanded: expanded,
654
+ onExpandedChange: onExpandedChange,
655
+ modes: modes,
656
+ showHeader: false,
657
+ children: [inlineHeader, renderBodyContent(section, index)]
658
+ }, sectionKey));
659
+ } else {
660
+ scrollChildren.push(/*#__PURE__*/(0, _jsxRuntime.jsxs)(_Accordion.default, {
661
+ title: section.title,
662
+ defaultExpanded: section.defaultExpanded ?? index === 0,
663
+ modes: modes,
664
+ children: [inlineHeader, renderBodyContent(section, index)]
665
+ }, sectionKey));
666
+ }
605
667
  });
606
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
668
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
607
669
  style: [outerStyle, style, {
608
670
  flexDirection: 'column',
609
671
  flex: 1
610
672
  }],
611
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
673
+ children: [showCardsRow && stickyCards ? renderCardsRow() : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
612
674
  style: {
613
675
  width: '100%',
614
676
  flex: 1
615
677
  },
678
+ contentContainerStyle: {
679
+ flexGrow: 1
680
+ },
616
681
  stickyHeaderIndices: stickyIndices,
617
682
  showsVerticalScrollIndicator: false,
618
683
  directionalLockEnabled: true,
619
684
  children: scrollChildren
620
- })
685
+ })]
621
686
  });
622
687
  }
623
688
  var _default = exports.default = CompareTable;
@@ -333,6 +333,9 @@ const FormField = /*#__PURE__*/(0, _react.forwardRef)(function FormField({
333
333
  fontWeight: tokens.inputFontWeight,
334
334
  padding: 0,
335
335
  margin: 0,
336
+ // Match the caret to the tokenized input text color on web (native uses
337
+ // cursorColor / selectionColor on the TextInput below).
338
+ caretColor: tokens.inputTextColor,
336
339
  // Remove the default web focus ring; the input row's border acts as the
337
340
  // focus indicator via the FormField States cascade.
338
341
  outlineStyle: 'none',
@@ -395,7 +398,12 @@ const FormField = /*#__PURE__*/(0, _react.forwardRef)(function FormField({
395
398
  onBlur: handleBlur,
396
399
  onSubmitEditing: onSubmitEditing,
397
400
  placeholder: placeholder ?? '',
398
- placeholderTextColor: placeholderColor,
401
+ placeholderTextColor: placeholderColor
402
+ // Kill platform accent cursors (Android Material green, iOS system
403
+ // blue) — match NoteInput/MoneyValue and use the tokenized text color.
404
+ ,
405
+ cursorColor: tokens.inputTextColor,
406
+ selectionColor: tokens.inputTextColor,
399
407
  editable: interactive,
400
408
  maxLength: maxLength,
401
409
  autoFocus: autoFocus,
@@ -219,23 +219,14 @@ function FullscreenModal({
219
219
  }), [globalModes, propModes]);
220
220
  const rootGap = Number((0, _figmaVariablesResolver.getVariableByName)('fullScreenModal/gap', modes));
221
221
 
222
- // Safe-area insets so the floating chrome clears the system bars: the close
223
- // button drops below the status bar / notch, and the sticky footer keeps its
224
- // designed bottom padding ON TOP of the bottom inset (home indicator /
225
- // Android gesture or nav bar). On web — and anywhere without a
226
- // SafeAreaProvider — every inset is 0, so the layout is unchanged.
222
+ // Safe-area inset so the floating close button clears the status bar / notch.
223
+ // Footer padding stays at the design-token value from ActionFooter no
224
+ // extra inset is layered on top (the token already accounts for bottom chrome).
225
+ // On web — and anywhere without a SafeAreaProvider — insets.top is 0.
227
226
  const insets = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)();
228
227
  const closeButtonInsetStyle = (0, _react.useMemo)(() => ({
229
228
  top: 12 + insets.top + closeOffsetY
230
229
  }), [insets.top, closeOffsetY]);
231
- // Extend (not replace) the footer's token bottom padding by the bottom inset
232
- // so the action button never sits under the system navigation area.
233
- const footerInsetStyle = (0, _react.useMemo)(() => {
234
- const base = Number((0, _figmaVariablesResolver.getVariableByName)('actionFooter/padding/bottom', modes));
235
- return {
236
- paddingBottom: base + insets.bottom
237
- };
238
- }, [modes, insets.bottom]);
239
230
 
240
231
  // Drives the background's parallax-free sync with the scroll. The hero media
241
232
  // lives at the ROOT (so it is never clipped to the content height and sits
@@ -396,7 +387,6 @@ function FullscreenModal({
396
387
  })
397
388
  }), footerContent ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_ActionFooter.default, {
398
389
  modes: modes,
399
- style: footerInsetStyle,
400
390
  children: footerContent
401
391
  }) : null, showClose ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_IconButton.default, {
402
392
  iconName: "ic_close",