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
@@ -62,6 +62,8 @@ function Accordion({
62
62
  accessibilityState,
63
63
  webAccessibilityProps,
64
64
  disableTruncation,
65
+ showHeader = true,
66
+ showContent = true,
65
67
  ...rest
66
68
  }) {
67
69
  const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
@@ -99,8 +101,11 @@ function Accordion({
99
101
  const contentPaddingTop = getVariableByName('accordion/content/padding/top', resolvedModes) ?? 8;
100
102
  const contentPaddingBottom = getVariableByName('accordion/content/padding/bottom', resolvedModes) ?? 8;
101
103
  const borderColor = getVariableByName('accordion/border/color', resolvedModes) ?? '#e6e6e6';
104
+
105
+ // Header-only sticky pieces drop the divider while expanded so the paired
106
+ // content Accordion owns the section border.
102
107
  const containerStyle = {
103
- borderBottomWidth: 1,
108
+ borderBottomWidth: !showContent && isExpanded ? 0 : 1,
104
109
  borderBottomColor: borderColor
105
110
  };
106
111
  const headerStyle = {
@@ -137,10 +142,18 @@ function Accordion({
137
142
  webAccessibilityProps
138
143
  });
139
144
  const processedChildren = children ? cloneChildrenWithModes(React.Children.toArray(children), resolvedModes) : null;
145
+
146
+ // Content-only + collapsed: nothing to render (header lives in the sticky peer).
147
+ if (!showHeader && !isExpanded) {
148
+ return null;
149
+ }
150
+ if (!showHeader && !showContent) {
151
+ return null;
152
+ }
140
153
  return /*#__PURE__*/_jsxs(View, {
141
154
  style: [containerStyle, style],
142
155
  ...rest,
143
- children: [/*#__PURE__*/_jsxs(Pressable, {
156
+ children: [showHeader && /*#__PURE__*/_jsxs(Pressable, {
144
157
  accessibilityRole: "button",
145
158
  accessibilityLabel: undefined,
146
159
  accessibilityHint: accessibilityHint || (isExpanded ? 'Collapse accordion' : 'Expand accordion'),
@@ -170,7 +183,7 @@ function Accordion({
170
183
  accessibilityElementsHidden: true,
171
184
  importantForAccessibility: "no"
172
185
  })]
173
- }), isExpanded && processedChildren && /*#__PURE__*/_jsx(View, {
186
+ }), showContent && isExpanded && processedChildren && /*#__PURE__*/_jsx(View, {
174
187
  style: contentStyle,
175
188
  children: processedChildren
176
189
  })]
@@ -1,20 +1,94 @@
1
1
  "use strict";
2
2
 
3
- import React from 'react';
3
+ import React, { useMemo } from 'react';
4
4
  import { View, Pressable } from 'react-native';
5
+ import Svg, { Path, Defs, LinearGradient, Stop, ClipPath, Rect, G } from 'react-native-svg';
5
6
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
7
  import { useTokens } from '../../design-tokens/JFSThemeProvider';
7
8
  import NavArrow from '../NavArrow/NavArrow';
8
9
  import { cloneChildrenWithModes, EMPTY_MODES } from '../../utils/react-utils';
9
10
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
- // SubPage "slot wrap" geometry, taken directly from the Figma design
11
- // (node 449:7876). The middle slot is an absolutely-centered box of a fixed
12
- // size; its inner content (node 3991:4125) is a `flex: 1 0 0; min-width: 1px`
13
- // item so it fills / shrinks responsively within that box.
11
+ // SubPage "slot wrap" geometry from Figma (node 449:7876). The middle slot is
12
+ // an absolutely-centered fixed-size box; its inner content (node 3991:4125)
13
+ // fills / shrinks responsively within that box.
14
14
  const SUBPAGE_MIDDLE_DEFAULT_WIDTH = 192;
15
15
  const SUBPAGE_MIDDLE_HEIGHT = 32;
16
16
  const SUBPAGE_MIDDLE_PADDING_HORIZONTAL = 21;
17
- export default function AppBar({
17
+ const JIODOT_FALLBACK_SIZE = 32;
18
+ /**
19
+ * Figma "jiodot" mark (node 1117:1908). Opt-in via `leadingSlot` — MainPage
20
+ * does NOT render this by default so existing consumers stay empty-leading.
21
+ */
22
+ export function JioDot({
23
+ size,
24
+ modes = EMPTY_MODES
25
+ }) {
26
+ const resolvedSize = size ?? (Number(getVariableByName('appBar/mainPage/jiodot/size', modes)) || JIODOT_FALLBACK_SIZE);
27
+ return /*#__PURE__*/_jsxs(Svg, {
28
+ width: resolvedSize,
29
+ height: resolvedSize,
30
+ viewBox: "0 0 32 32",
31
+ fill: "none",
32
+ accessibilityRole: "image",
33
+ accessibilityLabel: "Jio",
34
+ children: [/*#__PURE__*/_jsxs(G, {
35
+ clipPath: "url(#jiodotClip)",
36
+ children: [/*#__PURE__*/_jsx(Path, {
37
+ 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",
38
+ fill: "url(#jiodotGrad)"
39
+ }), /*#__PURE__*/_jsx(Path, {
40
+ 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",
41
+ fill: "white"
42
+ })]
43
+ }), /*#__PURE__*/_jsxs(Defs, {
44
+ children: [/*#__PURE__*/_jsxs(LinearGradient, {
45
+ id: "jiodotGrad",
46
+ x1: "11.085",
47
+ y1: "9.25442",
48
+ x2: "22.3469",
49
+ y2: "15.7638",
50
+ gradientUnits: "userSpaceOnUse",
51
+ children: [/*#__PURE__*/_jsx(Stop, {
52
+ stopColor: "#FBDCAF"
53
+ }), /*#__PURE__*/_jsx(Stop, {
54
+ offset: "1",
55
+ stopColor: "#AD8444"
56
+ })]
57
+ }), /*#__PURE__*/_jsx(ClipPath, {
58
+ id: "jiodotClip",
59
+ children: /*#__PURE__*/_jsx(Rect, {
60
+ width: "32",
61
+ height: "32",
62
+ fill: "white"
63
+ })
64
+ })]
65
+ })]
66
+ });
67
+ }
68
+ /**
69
+ * AppBar — top navigation bar for MainPage and SubPage layouts
70
+ * (Figma node 1070:18571).
71
+ *
72
+ * Visual values resolve from design tokens via `getVariableByName`. Slots
73
+ * cascade `modes` to children through `cloneChildrenWithModes`.
74
+ *
75
+ * @component
76
+ * @example
77
+ * ```tsx
78
+ * <AppBar
79
+ * type="MainPage"
80
+ * leadingSlot={<JioDot />}
81
+ * actionsSlot={
82
+ * <>
83
+ * <IconButton iconName="ic_add" />
84
+ * <IconButton iconName="ic_add" />
85
+ * <Avatar />
86
+ * </>
87
+ * }
88
+ * />
89
+ * ```
90
+ */
91
+ function AppBar({
18
92
  type = 'MainPage',
19
93
  leadingSlot,
20
94
  middleSlot,
@@ -30,118 +104,70 @@ export default function AppBar({
30
104
  const {
31
105
  modes: globalModes
32
106
  } = useTokens();
33
- const modes = {
107
+ const modes = useMemo(() => ({
34
108
  Context2: 'AppBar',
35
109
  ...globalModes,
36
110
  ...propModes
37
- };
111
+ }), [globalModes, propModes]);
38
112
  const isMain = type === 'MainPage';
39
113
  const isSub = type === 'SubPage';
40
-
41
- // --- Tokens ---
42
- // Construct token keys based on type. keys must be camelCase as per instruction.
43
- // We use explicit string literals so that the extraction script can find them.
44
114
  const paddingHorizontal = isMain ? getVariableByName('appBar/mainPage/padding/horizontal', modes) : getVariableByName('appBar/subPage/padding/horizontal', modes);
45
115
  const paddingVertical = isMain ? getVariableByName('appBar/mainPage/padding/vertical', modes) : getVariableByName('appBar/subPage/padding/vertical', modes);
46
116
  const backgroundColor = isMain ? getVariableByName('appBar/mainPage/background', modes) : getVariableByName('appBar/subPage/background', modes);
47
117
  const actionsGap = isMain ? getVariableByName('appBar/mainPage/actions/gap', modes) : getVariableByName('appBar/subPage/actions/gap', modes);
48
-
49
- // Layout styles
50
118
  const containerStyle = {
51
119
  flexDirection: 'row',
52
120
  alignItems: 'center',
53
- // No `justifyContent` here: with the inline middle slot using `flex: 1`
54
- // the three sections lay out naturally (leading | middle | actions).
55
- // When middleSlot is absent we fall back to `space-between` at the wrapper
56
- // level so leading & actions still anchor to the edges.
57
121
  paddingHorizontal: paddingHorizontal ?? 16,
58
122
  paddingVertical: paddingVertical ?? (isMain ? 16 : 10),
59
123
  backgroundColor: backgroundColor ?? '#FFFFFF'
60
- // MainPage: h=68 (16 top/bot padding? 36 height content?)
61
- // SubPage: h=52
62
124
  };
63
125
 
64
- // --- Leading Slot Default ---
65
- // SubPage default: Back Arrow
66
- // MainPage default: Jio Logo (We'll use a placeholder or basic text if no asset,
67
- // but looking at "AppBarHome" before, it had "ic_hellojio". Let's stick to generic defaults or empty if not provided,
68
- // but usually a component library provides sensible defaults.)
69
-
70
- // Actually, let's implement the specific defaults mentioned in the plan/analysis
71
- // SubPage: NavArrow props (icon, etc).
72
-
126
+ // MainPage: no default leading (backward compatible — consumers opt in via
127
+ // leadingSlot, e.g. <JioDot />). SubPage: default back NavArrow.
73
128
  let defaultLeading = null;
74
- if (leadingSlot === undefined) {
75
- if (isSub) {
76
- defaultLeading = /*#__PURE__*/_jsx(Pressable, {
77
- ...(onLeadingPress ? {
78
- onPress: onLeadingPress
79
- } : {}),
80
- hitSlop: 8,
81
- accessibilityRole: "button",
82
- accessibilityLabel: undefined,
83
- style: ({
84
- pressed
85
- }) => ({
86
- opacity: pressed ? 0.7 : 1,
87
- alignItems: 'center',
88
- justifyContent: 'center'
89
- }),
90
- children: /*#__PURE__*/_jsx(NavArrow, {
91
- direction: "Back",
92
- modes: modes
93
- })
94
- });
95
- } else {
96
- // MainPage default leading: often the Brand Logo.
97
- // We'll leave it empty or render a placeholder if desired, but standard AppBar often lets consumer pass logo.
98
- // Previous code had `leadingIconName='ic_hellojio'` for Home.
99
- // We will render nothing by default for MainPage leading to keep it flexible, OR a specialized Logo component if we had one.
100
- // For now, let's assume consumer passes it for MainPage, or we leave empty.
101
- // Wait, Figma screenshot shows "Jio" logo.
102
- // The user said: "mostly token namings are 'camelCase'. Maximize existing component usage."
103
- // I'll leave defaultLeading as null for MainPage unless I have a "BrandLogo" component.
104
- }
129
+ if (leadingSlot === undefined && isSub) {
130
+ defaultLeading = /*#__PURE__*/_jsx(Pressable, {
131
+ ...(onLeadingPress ? {
132
+ onPress: onLeadingPress
133
+ } : {}),
134
+ hitSlop: 8,
135
+ accessibilityRole: "button",
136
+ accessibilityLabel: undefined,
137
+ style: ({
138
+ pressed
139
+ }) => ({
140
+ opacity: pressed ? 0.7 : 1,
141
+ alignItems: 'center',
142
+ justifyContent: 'center'
143
+ }),
144
+ children: /*#__PURE__*/_jsx(NavArrow, {
145
+ direction: "Back",
146
+ modes: modes
147
+ })
148
+ });
105
149
  }
106
-
107
- // --- Process Slots ---
108
150
  const processedLeading = leadingSlot ? cloneChildrenWithModes([leadingSlot], modes)[0] : defaultLeading;
109
151
  const processedMiddle = middleSlot ? cloneChildrenWithModes(React.Children.toArray(middleSlot), modes) : null;
110
-
111
- // Actions Gap wrapper
112
- // The Figma has "Actions" slot with a gap.
113
152
  const actionsStyle = {
114
153
  flexDirection: 'row',
115
154
  alignItems: 'center',
116
155
  gap: actionsGap ?? 0
117
156
  };
118
- const processedActions = actionsSlot ? /*#__PURE__*/_jsx(View, {
119
- style: actionsStyle,
120
- children: cloneChildrenWithModes(React.Children.toArray(actionsSlot), modes)
121
- }) : null;
157
+ const processedActions = actionsSlot ? cloneChildrenWithModes(React.Children.toArray(actionsSlot), modes) : null;
122
158
 
123
- // SubPage centers its middle slot via absolute positioning (see Figma
124
- // "slot wrap"), so it never participates in the row flow. Only MainPage
125
- // keeps the legacy in-flow middle slot.
159
+ // SubPage middle is absolute (Figma "slot wrap"); MainPage middle stays in-flow.
126
160
  const hasInFlowMiddle = isMain && !!processedMiddle;
127
161
 
128
- // With an in-flow middle (MainPage) the middle (flex: 1) absorbs the
129
- // remaining space, so leading & actions sit at the edges naturally. In all
130
- // other cases we pin leading & actions to the outer edges with
131
- // `space-between`; the SubPage middle floats above, centered.
162
+ // With an in-flow middle the flex:1 middle absorbs leftover space. Otherwise
163
+ // pin leading & actions to the edges; SubPage middle floats centered above.
132
164
  const wrapperStyle = {
133
165
  ...containerStyle,
134
166
  justifyContent: hasInFlowMiddle ? 'flex-start' : 'space-between'
135
167
  };
136
168
 
137
- // Absolutely-positioned overlay for the SubPage middle slot. Instead of
138
- // centering a fixed-height box with `top/left: 50%` + transform (which on
139
- // Android resolves the percentage against an intermediate, content-driven
140
- // parent height and lands a few px too high), the overlay STRETCHES to fill
141
- // the whole bar (`top/bottom/left/right: 0`) and centers its content with
142
- // flexbox. This uses the exact same full-height vertical reference as the
143
- // leading/actions row (`alignItems: 'center'`), so the middle content always
144
- // sits on the same line as the back arrow and end slot on every platform.
169
+ // Overlay stretches to the full bar and centers content with flexbox so the
170
+ // middle aligns with leading/actions on every platform.
145
171
  const subPageMiddleOverlayStyle = {
146
172
  position: 'absolute',
147
173
  top: 0,
@@ -152,10 +178,6 @@ export default function AppBar({
152
178
  alignItems: 'center',
153
179
  justifyContent: 'center'
154
180
  };
155
-
156
- // Fixed-width clipping box (mirrors the Figma "slot wrap" geometry): keeps the
157
- // middle content from bleeding under the leading/actions slots while staying
158
- // centered within the overlay above.
159
181
  const subPageMiddleBoxStyle = {
160
182
  width: middleSlotWidth,
161
183
  height: SUBPAGE_MIDDLE_HEIGHT,
@@ -179,7 +201,7 @@ export default function AppBar({
179
201
  alignItems: 'center'
180
202
  },
181
203
  children: processedLeading
182
- }), hasInFlowMiddle && /*#__PURE__*/_jsx(View, {
204
+ }), hasInFlowMiddle ? /*#__PURE__*/_jsx(View, {
183
205
  style: {
184
206
  flex: 1,
185
207
  minWidth: 0,
@@ -189,10 +211,10 @@ export default function AppBar({
189
211
  },
190
212
  pointerEvents: "box-none",
191
213
  children: processedMiddle
192
- }), /*#__PURE__*/_jsx(View, {
214
+ }) : null, /*#__PURE__*/_jsx(View, {
193
215
  style: actionsStyle,
194
216
  children: processedActions
195
- }), isSub && processedMiddle && /*#__PURE__*/_jsx(View, {
217
+ }), isSub && processedMiddle ? /*#__PURE__*/_jsx(View, {
196
218
  style: subPageMiddleOverlayStyle,
197
219
  pointerEvents: "box-none",
198
220
  children: /*#__PURE__*/_jsx(View, {
@@ -211,6 +233,7 @@ export default function AppBar({
211
233
  children: processedMiddle
212
234
  })
213
235
  })
214
- })]
236
+ }) : null]
215
237
  });
216
- }
238
+ }
239
+ export default AppBar;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- import React, { useCallback, useRef } from 'react';
3
+ import React, { useCallback, useRef, useState } from 'react';
4
4
  import { View, Text, Pressable, Platform, ScrollView } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import { EMPTY_MODES, cloneChildrenWithModes, resolveTruncation } from '../../utils/react-utils';
@@ -108,7 +108,9 @@ function CompareTable({
108
108
  style,
109
109
  disableTruncation,
110
110
  columnWidth,
111
- stickyHeaders = true,
111
+ stickyCards = true,
112
+ stickyAccordionTitles = true,
113
+ stickyHeaders = false,
112
114
  stickyHeaderLabel = true
113
115
  }) {
114
116
  // --- selection card tokens ------------------------------------------------
@@ -123,11 +125,10 @@ function CompareTable({
123
125
  const cardLabelFontFamily = getVariableByName('selectionCard/label/fontfamily', modes) ?? 'JioType Var';
124
126
  const cardLabelFontWeight = getVariableByName('selectionCard/label/fontweight', modes) ?? 700;
125
127
  const imageRadius = getVariableByName('image/radius', modes) ?? 8;
126
- // `selectionCard/radius` (Figma node 5568:3487) rounds the card corners.
127
- // The published token store is still out of sync (the variable resolves to
128
- // `0` in the design), so the read falls back to `0` and will pick up a
129
- // rounded value automatically once the store syncs.
130
- const cardRadius = getVariableByName('selectionCard/radius', modes) ?? 0;
128
+ // Selection cards are square no corner radius (Figma compare-table
129
+ // treatment). Ignore `selectionCard/radius` so a non-zero token value
130
+ // cannot round the card edges.
131
+ const cardRadius = 0;
131
132
 
132
133
  // --- add-a-card icon button tokens ---------------------------------------
133
134
  // padding/size/border come from the shared iconButton tokens; the filled
@@ -186,6 +187,16 @@ function CompareTable({
186
187
  * scrollTo echoes from synced peers are ignored, eliminating jitter. */
187
188
  const draggingSource = useRef(null);
188
189
  const lastSyncX = useRef(0);
190
+
191
+ // Controlled expand state for sticky accordion titles (header + body are
192
+ // split across two Accordion instances that must stay in sync).
193
+ const [expandedByIndex, setExpandedByIndex] = useState(() => {
194
+ const initial = {};
195
+ sections.forEach((section, index) => {
196
+ initial[index] = section.defaultExpanded ?? index === 0;
197
+ });
198
+ return initial;
199
+ });
189
200
  const syncOthers = useCallback((source, x) => {
190
201
  if (source !== 'cards' && cardsScrollRef.current) {
191
202
  cardsScrollRef.current.scrollTo({
@@ -294,6 +305,10 @@ function CompareTable({
294
305
  minHeight: 147,
295
306
  backgroundColor: cardBg,
296
307
  borderRadius: cardRadius,
308
+ borderTopLeftRadius: cardRadius,
309
+ borderTopRightRadius: cardRadius,
310
+ borderBottomLeftRadius: cardRadius,
311
+ borderBottomRightRadius: cardRadius,
297
312
  alignItems: 'center',
298
313
  justifyContent: 'center',
299
314
  gap: cardGap,
@@ -507,6 +522,7 @@ function CompareTable({
507
522
  backgroundColor: cardBg,
508
523
  borderWidth: 1,
509
524
  borderColor: '#e8e8e8',
525
+ borderRadius: 0,
510
526
  overflow: 'hidden'
511
527
  };
512
528
 
@@ -535,14 +551,18 @@ function CompareTable({
535
551
  }
536
552
 
537
553
  // --- fixed-width scroll mode: chained horizontal sync + sticky cards -----
538
- // Build children and stickyHeaderIndices for the vertical ScrollView.
539
- // When stickyHeaders is on, section headers are injected as direct children
540
- // so React Native's stickyHeaderIndices can pin them with stacking.
541
- const scrollChildren = [];
542
- const stickyIndices = [];
543
- if (showCardsRow) {
544
- stickyIndices.push(scrollChildren.length);
545
- scrollChildren.push(/*#__PURE__*/_jsx(ScrollView, {
554
+ // Sticky cards are pinned *outside* the vertical ScrollView (sibling above
555
+ // it) so they cannot scroll away `stickyHeaderIndices` is unreliable for
556
+ // a nested horizontal ScrollView on Fabric. Sticky accordion titles /
557
+ // table headers still use stickyHeaderIndices inside the scroll surface
558
+ // and therefore stick directly under the pinned cards.
559
+ const renderCardsRow = () => /*#__PURE__*/_jsx(View, {
560
+ style: {
561
+ width: '100%',
562
+ backgroundColor: cardBg,
563
+ zIndex: 2
564
+ },
565
+ children: /*#__PURE__*/_jsx(ScrollView, {
546
566
  ref: cardsScrollRef,
547
567
  horizontal: true,
548
568
  showsHorizontalScrollIndicator: false,
@@ -558,11 +578,51 @@ function CompareTable({
558
578
  },
559
579
  children: [columns.map(renderCard), showAddCard && renderAddCard()]
560
580
  })
561
- }, "cards-row"));
581
+ })
582
+ }, "cards-row");
583
+ const scrollChildren = [];
584
+ const stickyIndices = [];
585
+
586
+ // Non-sticky cards scroll away with the sections (inside the ScrollView).
587
+ if (showCardsRow && !stickyCards) {
588
+ scrollChildren.push(renderCardsRow());
562
589
  }
563
590
  sections.forEach((section, index) => {
564
591
  const sectionKey = section.key ?? section.title ?? index;
565
592
  const headerContent = renderHeaderContent(section);
593
+ const expanded = expandedByIndex[index] ?? section.defaultExpanded ?? index === 0;
594
+ const onExpandedChange = next => {
595
+ setExpandedByIndex(prev => ({
596
+ ...prev,
597
+ [index]: next
598
+ }));
599
+ };
600
+ const inlineHeader = !stickyHeaders && headerContent != null ? stickyHeaderLabel ? headerContent : /*#__PURE__*/_jsx(ScrollView, {
601
+ horizontal: true,
602
+ showsHorizontalScrollIndicator: false,
603
+ children: /*#__PURE__*/_jsx(View, {
604
+ style: {
605
+ width: contentWidth
606
+ },
607
+ children: headerContent
608
+ })
609
+ }) : null;
610
+ if (stickyAccordionTitles) {
611
+ stickyIndices.push(scrollChildren.length);
612
+ scrollChildren.push(/*#__PURE__*/_jsx(View, {
613
+ style: {
614
+ width: '100%',
615
+ backgroundColor: cardBg
616
+ },
617
+ children: /*#__PURE__*/_jsx(Accordion, {
618
+ title: section.title,
619
+ expanded: expanded,
620
+ onExpandedChange: onExpandedChange,
621
+ modes: modes,
622
+ showContent: false
623
+ })
624
+ }, `sticky-accordion-title-${sectionKey}`));
625
+ }
566
626
  if (stickyHeaders && headerContent != null) {
567
627
  stickyIndices.push(scrollChildren.length);
568
628
  scrollChildren.push(/*#__PURE__*/_jsx(View, {
@@ -581,37 +641,42 @@ function CompareTable({
581
641
  })
582
642
  }, `sticky-header-${sectionKey}`));
583
643
  }
584
- scrollChildren.push(/*#__PURE__*/_jsxs(Accordion, {
585
- title: section.title,
586
- defaultExpanded: section.defaultExpanded ?? index === 0,
587
- modes: modes,
588
- children: [!stickyHeaders && headerContent != null && (stickyHeaderLabel ? headerContent : /*#__PURE__*/_jsx(ScrollView, {
589
- horizontal: true,
590
- showsHorizontalScrollIndicator: false,
591
- children: /*#__PURE__*/_jsx(View, {
592
- style: {
593
- width: contentWidth
594
- },
595
- children: headerContent
596
- })
597
- })), renderBodyContent(section, index)]
598
- }, sectionKey));
644
+ if (stickyAccordionTitles) {
645
+ scrollChildren.push(/*#__PURE__*/_jsxs(Accordion, {
646
+ title: section.title,
647
+ expanded: expanded,
648
+ onExpandedChange: onExpandedChange,
649
+ modes: modes,
650
+ showHeader: false,
651
+ children: [inlineHeader, renderBodyContent(section, index)]
652
+ }, sectionKey));
653
+ } else {
654
+ scrollChildren.push(/*#__PURE__*/_jsxs(Accordion, {
655
+ title: section.title,
656
+ defaultExpanded: section.defaultExpanded ?? index === 0,
657
+ modes: modes,
658
+ children: [inlineHeader, renderBodyContent(section, index)]
659
+ }, sectionKey));
660
+ }
599
661
  });
600
- return /*#__PURE__*/_jsx(View, {
662
+ return /*#__PURE__*/_jsxs(View, {
601
663
  style: [outerStyle, style, {
602
664
  flexDirection: 'column',
603
665
  flex: 1
604
666
  }],
605
- children: /*#__PURE__*/_jsx(ScrollView, {
667
+ children: [showCardsRow && stickyCards ? renderCardsRow() : null, /*#__PURE__*/_jsx(ScrollView, {
606
668
  style: {
607
669
  width: '100%',
608
670
  flex: 1
609
671
  },
672
+ contentContainerStyle: {
673
+ flexGrow: 1
674
+ },
610
675
  stickyHeaderIndices: stickyIndices,
611
676
  showsVerticalScrollIndicator: false,
612
677
  directionalLockEnabled: true,
613
678
  children: scrollChildren
614
- })
679
+ })]
615
680
  });
616
681
  }
617
682
  export default CompareTable;
@@ -327,6 +327,9 @@ const FormField = /*#__PURE__*/forwardRef(function FormField({
327
327
  fontWeight: tokens.inputFontWeight,
328
328
  padding: 0,
329
329
  margin: 0,
330
+ // Match the caret to the tokenized input text color on web (native uses
331
+ // cursorColor / selectionColor on the TextInput below).
332
+ caretColor: tokens.inputTextColor,
330
333
  // Remove the default web focus ring; the input row's border acts as the
331
334
  // focus indicator via the FormField States cascade.
332
335
  outlineStyle: 'none',
@@ -389,7 +392,12 @@ const FormField = /*#__PURE__*/forwardRef(function FormField({
389
392
  onBlur: handleBlur,
390
393
  onSubmitEditing: onSubmitEditing,
391
394
  placeholder: placeholder ?? '',
392
- placeholderTextColor: placeholderColor,
395
+ placeholderTextColor: placeholderColor
396
+ // Kill platform accent cursors (Android Material green, iOS system
397
+ // blue) — match NoteInput/MoneyValue and use the tokenized text color.
398
+ ,
399
+ cursorColor: tokens.inputTextColor,
400
+ selectionColor: tokens.inputTextColor,
393
401
  editable: interactive,
394
402
  maxLength: maxLength,
395
403
  autoFocus: autoFocus,
@@ -213,23 +213,14 @@ function FullscreenModal({
213
213
  }), [globalModes, propModes]);
214
214
  const rootGap = Number(getVariableByName('fullScreenModal/gap', modes));
215
215
 
216
- // Safe-area insets so the floating chrome clears the system bars: the close
217
- // button drops below the status bar / notch, and the sticky footer keeps its
218
- // designed bottom padding ON TOP of the bottom inset (home indicator /
219
- // Android gesture or nav bar). On web — and anywhere without a
220
- // SafeAreaProvider — every inset is 0, so the layout is unchanged.
216
+ // Safe-area inset so the floating close button clears the status bar / notch.
217
+ // Footer padding stays at the design-token value from ActionFooter no
218
+ // extra inset is layered on top (the token already accounts for bottom chrome).
219
+ // On web — and anywhere without a SafeAreaProvider — insets.top is 0.
221
220
  const insets = useSafeAreaInsets();
222
221
  const closeButtonInsetStyle = useMemo(() => ({
223
222
  top: 12 + insets.top + closeOffsetY
224
223
  }), [insets.top, closeOffsetY]);
225
- // Extend (not replace) the footer's token bottom padding by the bottom inset
226
- // so the action button never sits under the system navigation area.
227
- const footerInsetStyle = useMemo(() => {
228
- const base = Number(getVariableByName('actionFooter/padding/bottom', modes));
229
- return {
230
- paddingBottom: base + insets.bottom
231
- };
232
- }, [modes, insets.bottom]);
233
224
 
234
225
  // Drives the background's parallax-free sync with the scroll. The hero media
235
226
  // lives at the ROOT (so it is never clipped to the content height and sits
@@ -390,7 +381,6 @@ function FullscreenModal({
390
381
  })
391
382
  }), footerContent ? /*#__PURE__*/_jsx(ActionFooter, {
392
383
  modes: modes,
393
- style: footerInsetStyle,
394
384
  children: footerContent
395
385
  }) : null, showClose ? /*#__PURE__*/_jsx(IconButton, {
396
386
  iconName: "ic_close",
@@ -51,7 +51,15 @@ const STATE_FALLBACKS = {
51
51
  iconColor: '#24262b'
52
52
  },
53
53
  IdleJioPlus: {
54
- background: '#f5f5f5',
54
+ // Glass fill. In Figma the IdleJioPlus surface resolves
55
+ // `helloJioInput/background` under the `Page type = JioPlus` variable mode,
56
+ // which yields a TRANSLUCENT light fill (not the opaque `#f5f5f5` the
57
+ // MainPage mode gives). Painted over the backdrop blur, an opaque value
58
+ // would read as a solid bar and hide the blur entirely — so, matching the
59
+ // translucent glass fallbacks the sibling GlassFill consumers use
60
+ // (FavoriteToggle `#ffffff33`, NumberPagination `rgba(141,141,141,0.4)`),
61
+ // this fallback carries alpha so the frost tints the blurred content.
62
+ background: '#f5f5f566',
55
63
  borderColor: '#f5f5f5',
56
64
  labelColor: '#545961',
57
65
  iconColor: '#545961'
@@ -88,7 +96,11 @@ function resolveHelloJioInputTokens(modes, visualState) {
88
96
  // Glass keeps the container transparent so GlassFill shows through.
89
97
  backgroundColor: useGlass ? 'transparent' : background,
90
98
  borderColor,
91
- borderWidth: strokeWidth,
99
+ // The Figma IdleJioPlus variant is a borderless frosted pill — it has no
100
+ // `border/color`/`strokeWidth` tokens. Drawing the Idle border color here
101
+ // would leave a faint light ring the design doesn't have, so drop the
102
+ // border entirely in the glass state.
103
+ borderWidth: useGlass ? 0 : strokeWidth,
92
104
  borderStyle: 'solid',
93
105
  borderRadius,
94
106
  paddingLeft,
@@ -195,11 +207,15 @@ const HelloJioInput = /*#__PURE__*/forwardRef(function HelloJioInput({
195
207
  setIsFocused(false);
196
208
  onBlur?.(e);
197
209
  }, [onBlur]);
198
- const leadingElement = leading ?? (leadingIconName ? /*#__PURE__*/_jsx(Icon, {
210
+
211
+ // Symmetric with `trailing`: an explicit `null` hides the leading icon
212
+ // (the design's `startSlot={false}`); `undefined` falls back to the
213
+ // default brand icon.
214
+ const leadingElement = leading === undefined ? leadingIconName ? /*#__PURE__*/_jsx(Icon, {
199
215
  name: leadingIconName,
200
216
  size: tokens.iconSize,
201
217
  color: tokens.iconColor
202
- }) : null);
218
+ }) : null : leading;
203
219
  const processedLeading = leadingElement ? cloneChildrenWithModes(React.Children.toArray(leadingElement), modes) : null;
204
220
  const defaultTrailing = /*#__PURE__*/_jsx(IconButton, {
205
221
  iconName: sendIconName,