jfs-components 0.1.50 → 0.1.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/.cursor/D2C-FLOW.md +94 -0
  2. package/.cursor/D2C-SCREEN.md +119 -0
  3. package/.cursor/commands/D2C.md +101 -0
  4. package/CHANGELOG.md +18 -0
  5. package/lib/commonjs/components/ActionFooter/ActionFooter.js +319 -135
  6. package/lib/commonjs/components/Badge/Badge.js +20 -1
  7. package/lib/commonjs/components/Button/Button.js +139 -47
  8. package/lib/commonjs/components/ButtonGroup/ButtonGroup.js +1 -1
  9. package/lib/commonjs/components/Card/Card.js +151 -112
  10. package/lib/commonjs/components/Card/assets/gold-logo-mark.png +0 -0
  11. package/lib/commonjs/components/CategoryCard/CategoryCard.js +97 -63
  12. package/lib/commonjs/components/CategoryCard/assets/sample.png +0 -0
  13. package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +59 -10
  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/ExpandableCheckbox/ExpandableCheckbox.js +135 -52
  40. package/lib/module/components/HelloJioInput/HelloJioInput.js +56 -14
  41. package/lib/module/components/IconCapsule/IconCapsule.js +1 -1
  42. package/lib/module/components/MetricData/MetricData.js +26 -14
  43. package/lib/module/components/MoneyValue/MoneyValue.js +1 -1
  44. package/lib/module/components/Nudge/Nudge.js +1 -1
  45. package/lib/module/components/PdpCcCard/PdpCcCard.js +48 -92
  46. package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +52 -45
  47. package/lib/module/components/PortfolioHero/PortfolioHero.js +1 -1
  48. package/lib/module/components/Slider/Slider.js +3 -3
  49. package/lib/module/components/TextInput/TextInput.js +27 -9
  50. package/lib/module/components/Tooltip/Tooltip.js +3 -5
  51. package/lib/module/components/index.js +1 -1
  52. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  53. package/lib/module/design-tokens/figma-modes.generated.js +13 -13
  54. package/lib/module/icons/registry.js +1 -1
  55. package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +50 -21
  56. package/lib/typescript/src/components/Button/Button.d.ts +24 -1
  57. package/lib/typescript/src/components/ButtonGroup/ButtonGroup.d.ts +2 -2
  58. package/lib/typescript/src/components/Card/Card.d.ts +54 -25
  59. package/lib/typescript/src/components/CategoryCard/CategoryCard.d.ts +29 -12
  60. package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +27 -9
  61. package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +37 -12
  62. package/lib/typescript/src/components/IconCapsule/IconCapsule.d.ts +1 -1
  63. package/lib/typescript/src/components/MetricData/MetricData.d.ts +7 -5
  64. package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +16 -6
  65. package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +9 -2
  66. package/lib/typescript/src/components/index.d.ts +2 -2
  67. package/lib/typescript/src/icons/registry.d.ts +1 -1
  68. package/package.json +4 -2
  69. package/src/components/ActionFooter/ActionFooter.tsx +375 -141
  70. package/src/components/Badge/Badge.tsx +21 -1
  71. package/src/components/Button/Button.tsx +192 -44
  72. package/src/components/ButtonGroup/ButtonGroup.tsx +2 -2
  73. package/src/components/Card/Card.tsx +370 -212
  74. package/src/components/Card/assets/gold-logo-mark.png +0 -0
  75. package/src/components/CategoryCard/CategoryCard.tsx +130 -84
  76. package/src/components/CategoryCard/assets/sample.png +0 -0
  77. package/src/components/CheckboxItem/CheckboxItem.tsx +72 -12
  78. package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +186 -67
  79. package/src/components/HelloJioInput/HelloJioInput.tsx +58 -17
  80. package/src/components/IconCapsule/IconCapsule.tsx +1 -1
  81. package/src/components/MetricData/MetricData.tsx +39 -22
  82. package/src/components/MoneyValue/MoneyValue.tsx +1 -1
  83. package/src/components/Nudge/Nudge.tsx +1 -1
  84. package/src/components/PdpCcCard/PdpCcCard.tsx +41 -92
  85. package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +73 -58
  86. package/src/components/PortfolioHero/PortfolioHero.tsx +1 -1
  87. package/src/components/Slider/Slider.tsx +3 -3
  88. package/src/components/TextInput/TextInput.tsx +26 -9
  89. package/src/components/Tooltip/Tooltip.tsx +3 -5
  90. package/src/components/index.ts +9 -2
  91. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  92. package/src/design-tokens/figma-modes.generated.ts +13 -13
  93. package/src/icons/registry.ts +1 -1
@@ -10,13 +10,21 @@ import GlassFill from '../../utils/GlassFill/GlassFill';
10
10
  import IconButton from '../IconButton/IconButton';
11
11
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
12
12
  const IS_WEB = Platform.OS === 'web';
13
- const STATE_COLLECTION = 'HelloJioInput State';
13
+ /** Figma collection name — must match `figma-modes.generated.ts` exactly. */
14
+ const STATE_COLLECTION = 'Hello Jio Input State';
14
15
 
15
16
  /** Default IconButton modes matching the Figma send control. */
16
17
  const DEFAULT_SEND_BUTTON_MODES = Object.freeze({
17
18
  AppearanceBrand: 'Primary',
18
19
  'Button / Size': 'S'
19
20
  });
21
+
22
+ /**
23
+ * Figma `Hello Jio Input State` only exposes Idle | Active. The component-set
24
+ * variant IdleJioPlus is glass chrome on top of Idle tokens — never a mode
25
+ * value for this collection.
26
+ */
27
+
20
28
  const toNumber = (value, fallback) => {
21
29
  if (typeof value === 'number') {
22
30
  return Number.isFinite(value) ? value : fallback;
@@ -32,6 +40,29 @@ const toFontWeight = (value, fallback) => {
32
40
  if (typeof value === 'string') return value;
33
41
  return fallback;
34
42
  };
43
+
44
+ /** True when a CSS/RN color string already carries a non-opaque alpha channel. */
45
+ const hasAlphaChannel = color => {
46
+ const trimmed = color.trim();
47
+ if (/^#[0-9a-fA-F]{8}$/.test(trimmed)) {
48
+ const alpha = parseInt(trimmed.slice(7, 9), 16);
49
+ return alpha < 255;
50
+ }
51
+ if (/^#[0-9a-fA-F]{4}$/.test(trimmed)) {
52
+ const alphaNibble = parseInt(trimmed.slice(3, 4), 16);
53
+ return alphaNibble < 15;
54
+ }
55
+ const func = trimmed.match(/^(rgba|hsla)\(/i);
56
+ if (func) {
57
+ const parts = trimmed.slice(trimmed.indexOf('(') + 1, -1).split(',');
58
+ if (parts.length === 4) {
59
+ const alpha = Number(parts[3].trim());
60
+ return Number.isFinite(alpha) && alpha < 1;
61
+ }
62
+ }
63
+ return false;
64
+ };
65
+
35
66
  /**
36
67
  * Fallback values from the Figma `HelloJioInput` component when local token
37
68
  * JSON has not been synced yet. Mode-driven `getVariableByName` wins when
@@ -51,14 +82,11 @@ const STATE_FALLBACKS = {
51
82
  iconColor: '#24262b'
52
83
  },
53
84
  IdleJioPlus: {
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.
85
+ // Frosted IdleJioPlus fill. Figma paints `helloJioInput/background` over
86
+ // `glass/minimal` blur; the synced token is often opaque `#f5f5f5`, which
87
+ // would hide the blur. Match sibling GlassFill consumers (FavoriteToggle
88
+ // `#ffffff33`, NumberPagination `rgba(141,141,141,0.4)`) with alpha so the
89
+ // frost tints the blurred content.
62
90
  background: '#f5f5f566',
63
91
  borderColor: '#f5f5f5',
64
92
  labelColor: '#545961',
@@ -88,6 +116,11 @@ function resolveHelloJioInputTokens(modes, visualState) {
88
116
  const blurMinimal = toNumber(getVariableByName('blur/minimal', modes), 29);
89
117
  const blurIntensity = Math.max(0, Math.min(100, Math.round(blurMinimal)));
90
118
  const useGlass = visualState === 'IdleJioPlus';
119
+
120
+ // Figma IdleJioPlus paints `helloJioInput/background` over `glass/minimal`
121
+ // blur. The exported token is often opaque `#f5f5f5` (alpha lost in sync),
122
+ // which would fully hide the blur — keep a translucent frost when needed.
123
+ const glassOverlay = useGlass ? hasAlphaChannel(background) ? background : STATE_FALLBACKS.IdleJioPlus.background : background;
91
124
  return {
92
125
  containerStyle: {
93
126
  position: 'relative',
@@ -133,7 +166,8 @@ function resolveHelloJioInputTokens(modes, visualState) {
133
166
  iconColor,
134
167
  iconSize,
135
168
  blurIntensity,
136
- useGlass
169
+ useGlass,
170
+ glassOverlay
137
171
  };
138
172
  }
139
173
 
@@ -180,12 +214,21 @@ const HelloJioInput = /*#__PURE__*/forwardRef(function HelloJioInput({
180
214
  const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
181
215
  const isControlled = value !== undefined;
182
216
  const currentValue = isControlled ? value : uncontrolledValue;
217
+
218
+ // Component-set visual vs token mode:
219
+ // - Idle / Active map 1:1 onto `Hello Jio Input State`
220
+ // - IdleJioPlus is Figma chrome (glass) on Idle tokens — never pass
221
+ // "IdleJioPlus" into the state collection (that mode does not exist)
183
222
  const visualState = isFocused ? 'Active' : jioPlus ? 'IdleJioPlus' : 'Idle';
223
+ const tokenState = isFocused ? 'Active' : 'Idle';
184
224
  const modes = useMemo(() => ({
185
225
  ...globalModes,
186
226
  ...propModes,
187
- [STATE_COLLECTION]: visualState
188
- }), [globalModes, propModes, visualState]);
227
+ [STATE_COLLECTION]: tokenState
228
+ }), [globalModes, propModes, tokenState]);
229
+
230
+ // Defaults first; caller/`modes` may still override Size/Appearance.
231
+ // Custom chrome that must ignore parent modes belongs in `trailing`.
189
232
  const sendButtonModes = useMemo(() => ({
190
233
  ...DEFAULT_SEND_BUTTON_MODES,
191
234
  ...modes
@@ -233,12 +276,11 @@ const HelloJioInput = /*#__PURE__*/forwardRef(function HelloJioInput({
233
276
  const containerStyleArray = [tokens.containerStyle, hoverStyle, disabled ? {
234
277
  opacity: 0.5
235
278
  } : null, style];
236
- const glassOverlay = getVariableByName('helloJioInput/background', modes) ?? STATE_FALLBACKS.IdleJioPlus.background;
237
279
  const inner = /*#__PURE__*/_jsxs(_Fragment, {
238
280
  children: [tokens.useGlass ? /*#__PURE__*/_jsx(GlassFill, {
239
281
  tint: "light",
240
282
  intensity: tokens.blurIntensity,
241
- overlayColor: glassOverlay,
283
+ overlayColor: tokens.glassOverlay,
242
284
  androidTintWash: false
243
285
  }) : null, processedLeading ? /*#__PURE__*/_jsx(View, {
244
286
  accessibilityElementsHidden: true,
@@ -44,7 +44,7 @@ function resolveIconCapsuleTokens(modes) {
44
44
  * @param {Object} props - Component props
45
45
  * @param {string} [props.iconName="ic_card"] - The name of the icon to display from the icon registry
46
46
  * @param {UnifiedSource} [props.source] - Fallback source (remote URI, inline SVG XML, `require()` asset, SVG React component, or React element). Used when `iconName` is missing or unknown. Tinted with the mode-resolved icon color so it follows design tokens just like a built-in icon.
47
- * @param {Object} [props.modes={}] - Mode configuration for design tokens (e.g., {"Appearance": "Primary"})
47
+ * @param {Object} [props.modes={}] - Mode configuration for design tokens (e.g., {"AppearanceBrand": "Primary"})
48
48
  * @param {string} [props.accessibilityLabel] - Accessibility label for screen readers
49
49
  * @param {string} [props.accessibilityRole] - Accessibility role (defaults to "image" for decorative icons)
50
50
  *
@@ -1,13 +1,12 @@
1
1
  "use strict";
2
2
 
3
- import React from 'react';
3
+ import React, { useMemo } from 'react';
4
4
  import { View, Text } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
- import { EMPTY_MODES } from '../../utils/react-utils';
6
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
7
7
  import Icon from '../Icon/Icon';
8
8
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
9
  const LINE_HEIGHT_RATIO = 1.2;
10
- const ICON_SIZE = 18;
11
10
  // Figma renders the value-row icon with no surrounding padding; the shared Icon
12
11
  // component pulls padding from `icon/padding/*` tokens, so we zero it here.
13
12
  const ICON_PADDING_RESET = {
@@ -20,9 +19,10 @@ const ICON_PADDING_RESET = {
20
19
  * MetricData — a compact, centered metric block.
21
20
  *
22
21
  * Stacks a small `title`, a prominent `value` (with an optional leading
23
- * `icon`), and a muted `caption`. Typography, colours, gaps and padding all
24
- * resolve from the `metricdata/*` design tokens. `title`, `caption` and `icon`
25
- * are optional; only `value` is shown by default.
22
+ * `icon` in a `value wrap` row), and a muted `caption`. Typography, colours,
23
+ * gaps and padding resolve from the `metricdata/*` design tokens; the leading
24
+ * glyph uses `icon/*`. `title`, `caption` and `icon` are optional — omit
25
+ * `icon` to keep the legacy text-only layout.
26
26
  *
27
27
  * @example
28
28
  * ```tsx
@@ -42,10 +42,16 @@ function MetricData({
42
42
  captionStyle,
43
43
  accessibilityLabel
44
44
  }) {
45
+ // Figma's Metricdata instance seeds Secondary on the nested Icon so the
46
+ // default glyph is brand purple (`#5d00b5`). Caller `modes` still win.
47
+ const iconModes = useMemo(() => ({
48
+ AppearanceBrand: 'Secondary',
49
+ ...modes
50
+ }), [modes]);
45
51
  const titleColor = getVariableByName('metricdata/title/color', modes) ?? '#000000';
46
52
  const titleFontFamily = getVariableByName('metricdata/title/fontfamily', modes) ?? 'JioType Var';
47
53
  const titleFontSize = getVariableByName('metricdata/title/fontsize', modes) ?? 12;
48
- const titleFontWeight = String(getVariableByName('metricdata/title/fontweight', modes) ?? 400);
54
+ const titleFontWeight = String(getVariableByName('metricdata/title/fontweight', modes) ?? 500);
49
55
  const valueColor = getVariableByName('metricdata/value/color', modes) ?? '#000000';
50
56
  const valueFontFamily = getVariableByName('metricdata/value/fontfamily', modes) ?? 'JioType Var';
51
57
  const valueFontSize = getVariableByName('metricdata/value/fontsize', modes) ?? 20;
@@ -53,11 +59,16 @@ function MetricData({
53
59
  const captionColor = getVariableByName('metricdata/caption/color', modes) ?? '#777777';
54
60
  const captionFontFamily = getVariableByName('metricdata/caption/fontfamily', modes) ?? 'JioType Var';
55
61
  const captionFontSize = getVariableByName('metricdata/caption/fontsize', modes) ?? 12;
56
- const captionFontWeight = String(getVariableByName('metricdata/caption/fontweight', modes) ?? 500);
62
+ const captionFontWeight = String(getVariableByName('metricdata/caption/fontweight', modes) ?? 400);
57
63
  const gap = getVariableByName('metricdata/gap', modes) ?? 4;
58
64
  const valueWrapGap = getVariableByName('metricdata/valueWrap/gap', modes) ?? 4;
59
65
  const paddingHorizontal = getVariableByName('metricdata/padding/horizontal', modes) ?? 10;
60
66
  const paddingVertical = getVariableByName('metricdata/padding/vertical', modes) ?? 10;
67
+
68
+ // Resolve via getVariableByName so extract-tokens picks up the new bindings;
69
+ // Icon reads the same tokens when color/size props are omitted.
70
+ const iconColor = getVariableByName('icon/color', iconModes) ?? '#5d00b5';
71
+ const iconSize = getVariableByName('icon/size', iconModes) ?? 18;
61
72
  const containerStyle = {
62
73
  flexDirection: 'column',
63
74
  alignItems: 'center',
@@ -92,11 +103,12 @@ function MetricData({
92
103
  };
93
104
  const iconNode = icon == null ? null : typeof icon === 'string' ? /*#__PURE__*/_jsx(Icon, {
94
105
  iconName: icon,
95
- size: ICON_SIZE,
96
- color: valueColor,
97
- modes: modes,
106
+ size: iconSize,
107
+ color: iconColor,
108
+ modes: iconModes,
98
109
  style: ICON_PADDING_RESET
99
- }) : icon;
110
+ }) : cloneChildrenWithModes(icon, iconModes);
111
+ const showValueWrap = iconNode != null || value !== undefined;
100
112
  const resolvedLabel = accessibilityLabel ?? ([title, typeof value === 'string' ? value : undefined, caption].filter(Boolean).join(', ') || undefined);
101
113
  return /*#__PURE__*/_jsxs(View, {
102
114
  style: [containerStyle, style],
@@ -107,7 +119,7 @@ function MetricData({
107
119
  children: [title !== undefined && title !== '' ? /*#__PURE__*/_jsx(Text, {
108
120
  style: [titleTextStyle, titleStyle],
109
121
  children: title
110
- }) : null, /*#__PURE__*/_jsxs(View, {
122
+ }) : null, showValueWrap ? /*#__PURE__*/_jsxs(View, {
111
123
  style: {
112
124
  flexDirection: 'row',
113
125
  alignItems: 'center',
@@ -118,7 +130,7 @@ function MetricData({
118
130
  style: [valueTextStyle, valueStyle],
119
131
  children: value
120
132
  }) : value]
121
- }), caption !== undefined && caption !== '' ? /*#__PURE__*/_jsx(Text, {
133
+ }) : null, caption !== undefined && caption !== '' ? /*#__PURE__*/_jsx(Text, {
122
134
  style: [captionTextStyle, captionStyle],
123
135
  children: caption
124
136
  }) : null]
@@ -176,7 +176,7 @@ function MoneyValue({
176
176
  }, [focused, cursorOpacity]);
177
177
 
178
178
  // Keyboard avoidance — lift the component when the keyboard would cover it.
179
- // Follows the same pattern as ActionFooter (uses Keyboard.addListener).
179
+ // Uses Keyboard.addListener + measureInWindow overlap math (field-local).
180
180
  // Resets when editing ends or the keyboard hides.
181
181
  const rootRef = useRef(null);
182
182
  const keyboardLift = useRef(new Animated.Value(0)).current;
@@ -15,7 +15,7 @@ function toFontWeight(value, fallback) {
15
15
  }
16
16
  function resolveNudgeTokens(modes) {
17
17
  const background = getVariableByName('nudge/background', modes);
18
- const radius = getVariableByName('nudge/radius', modes);
18
+ const radius = getVariableByName('nudge/borderRadius', modes);
19
19
  const paddingH = getVariableByName('nudge/padding/horizontal', modes);
20
20
  const paddingV = getVariableByName('nudge/padding/vertical', modes);
21
21
  const gap = getVariableByName('nudge/gap', modes);
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  import React, { useMemo } from 'react';
4
- import { View, Text, Pressable, StyleSheet } from 'react-native';
4
+ import { View, Pressable, StyleSheet } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
7
7
  import Title from '../Title/Title';
@@ -9,10 +9,11 @@ import Divider from '../Divider/Divider';
9
9
  import Button from '../Button/Button';
10
10
  import Image from '../Image/Image';
11
11
  import Icon from '../Icon/Icon';
12
+ import MetricData from '../MetricData/MetricData';
12
13
 
13
14
  /**
14
15
  * A single metric column inside the card's stats row: a small title, a bold
15
- * value and an optional muted caption.
16
+ * value (optionally preceded by an icon) and an optional muted caption.
16
17
  */
17
18
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
18
19
  /**
@@ -25,8 +26,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
25
26
  * `imageSource` for the default image or `media` for a full slot override.
26
27
  * - **Title** — a centered headline + subtitle rendered through the shared
27
28
  * {@link Title} component (`title/*`, `pageSubtitle/*` tokens).
28
- * - **Metrics** — a row of {@link PdpCcCardMetric} columns (title / value /
29
- * caption) separated by vertical `Divider`s (`metricdata/*` tokens).
29
+ * - **Metrics** — a row of {@link PdpCcCardMetric} columns composed via
30
+ * {@link MetricData} (title / optional icon + value / caption) separated by
31
+ * vertical `Divider`s (`metricdata/*`, `icon/*` tokens).
30
32
  * - **CTA** — a small tonal {@link Button} (`Button / Size: S`,
31
33
  * `AppearanceBrand: Secondary`, `Emphasis: Medium`) with a leading icon.
32
34
  *
@@ -78,12 +80,16 @@ function PdpCcCard({
78
80
  const ctaNode = button ? cloneChildrenWithModes(button, ctaModes) : /*#__PURE__*/_jsx(Button, {
79
81
  label: buttonLabel,
80
82
  modes: ctaModes,
81
- onPress: onButtonPress,
82
- leading: buttonIcon ? /*#__PURE__*/_jsx(Icon, {
83
- iconName: buttonIcon,
84
- size: tokens.buttonIconSize,
85
- color: buttonForeground
86
- }) : undefined
83
+ ...(onButtonPress !== undefined ? {
84
+ onPress: onButtonPress
85
+ } : null),
86
+ ...(buttonIcon ? {
87
+ leading: /*#__PURE__*/_jsx(Icon, {
88
+ iconName: buttonIcon,
89
+ size: tokens.buttonIconSize,
90
+ color: buttonForeground
91
+ })
92
+ } : null)
87
93
  });
88
94
  const content = /*#__PURE__*/_jsxs(_Fragment, {
89
95
  children: [/*#__PURE__*/_jsx(View, {
@@ -94,9 +100,15 @@ function PdpCcCard({
94
100
  subtitle: subtitle,
95
101
  textAlign: "Center",
96
102
  modes: modes,
97
- numberOfLines: numberOfLines,
98
- disableTruncation: disableTruncation,
99
- singleLine: singleLine
103
+ ...(numberOfLines !== undefined ? {
104
+ numberOfLines
105
+ } : null),
106
+ ...(disableTruncation !== undefined ? {
107
+ disableTruncation
108
+ } : null),
109
+ ...(singleLine !== undefined ? {
110
+ singleLine
111
+ } : null)
100
112
  }), metrics.length > 0 ? /*#__PURE__*/_jsx(View, {
101
113
  style: styles.metricsRow,
102
114
  children: metrics.map((metric, index) => /*#__PURE__*/_jsxs(React.Fragment, {
@@ -104,9 +116,21 @@ function PdpCcCard({
104
116
  direction: "vertical",
105
117
  modes: modes,
106
118
  style: styles.metricDivider
107
- }) : null, /*#__PURE__*/_jsx(Metricdata, {
108
- metric: metric,
109
- tokens: tokens
119
+ }) : null, /*#__PURE__*/_jsx(MetricData, {
120
+ ...(metric.title !== undefined ? {
121
+ title: metric.title
122
+ } : null),
123
+ ...(metric.value !== undefined ? {
124
+ value: metric.value
125
+ } : null),
126
+ ...(metric.caption !== undefined ? {
127
+ caption: metric.caption
128
+ } : null),
129
+ ...(metric.icon !== undefined ? {
130
+ icon: metric.icon
131
+ } : null),
132
+ modes: modes,
133
+ style: styles.metricCell
110
134
  })]
111
135
  }, index))
112
136
  }) : null, showButton ? ctaNode : null]
@@ -133,29 +157,6 @@ function PdpCcCard({
133
157
  });
134
158
  }
135
159
 
136
- // ---------------------------------------------------------------------------
137
- // Metricdata — internal metric column (Figma node 5352:256)
138
- // ---------------------------------------------------------------------------
139
-
140
- function Metricdata({
141
- metric,
142
- tokens
143
- }) {
144
- return /*#__PURE__*/_jsxs(View, {
145
- style: tokens.metric,
146
- children: [metric.title != null ? /*#__PURE__*/_jsx(Text, {
147
- style: tokens.metricTitle,
148
- children: metric.title
149
- }) : null, metric.value != null ? /*#__PURE__*/_jsx(Text, {
150
- style: tokens.metricValue,
151
- children: metric.value
152
- }) : null, metric.caption != null ? /*#__PURE__*/_jsx(Text, {
153
- style: tokens.metricCaption,
154
- children: metric.caption
155
- }) : null]
156
- });
157
- }
158
-
159
160
  // ---------------------------------------------------------------------------
160
161
  // Tokens / static styles
161
162
  // ---------------------------------------------------------------------------
@@ -178,21 +179,6 @@ function resolveTokens(modes) {
178
179
  const gap = asNum(getVariableByName('PDPcccard/gap', modes), 12);
179
180
  const imageRadius = asNum(getVariableByName('image/radius', modes), 8);
180
181
  const buttonIconSize = asNum(getVariableByName('button/iconSize', modes), 16);
181
- const metricGap = asNum(getVariableByName('metricdata/gap', modes), 4);
182
- const metricPadH = asNum(getVariableByName('metricdata/padding/horizontal', modes), 10);
183
- const metricPadV = asNum(getVariableByName('metricdata/padding/vertical', modes), 10);
184
- const titleColor = asStr(getVariableByName('metricdata/title/color', modes), '#000000');
185
- const titleSize = asNum(getVariableByName('metricdata/title/fontsize', modes), 12);
186
- const titleFamily = asStr(getVariableByName('metricdata/title/fontfamily', modes), 'JioType Var');
187
- const titleWeight = asStr(getVariableByName('metricdata/title/fontweight', modes), '400');
188
- const valueColor = asStr(getVariableByName('metricdata/value/color', modes), '#000000');
189
- const valueSize = asNum(getVariableByName('metricdata/value/fontsize', modes), 20);
190
- const valueFamily = asStr(getVariableByName('metricdata/value/fontfamily', modes), 'JioType Var');
191
- const valueWeight = asStr(getVariableByName('metricdata/value/fontweight', modes), '700');
192
- const captionColor = asStr(getVariableByName('metricdata/caption/color', modes), '#777777');
193
- const captionSize = asNum(getVariableByName('metricdata/caption/fontsize', modes), 12);
194
- const captionFamily = asStr(getVariableByName('metricdata/caption/fontfamily', modes), 'JioType Var');
195
- const captionWeight = asStr(getVariableByName('metricdata/caption/fontweight', modes), '500');
196
182
  return {
197
183
  container: {
198
184
  backgroundColor: background,
@@ -204,52 +190,19 @@ function resolveTokens(modes) {
204
190
  justifyContent: 'center'
205
191
  },
206
192
  imageRadius,
207
- buttonIconSize,
208
- metric: {
209
- flex: 1,
210
- gap: metricGap,
211
- paddingHorizontal: metricPadH,
212
- paddingVertical: metricPadV,
213
- alignItems: 'center',
214
- justifyContent: 'center'
215
- },
216
- metricTitle: {
217
- color: titleColor,
218
- fontSize: titleSize,
219
- fontFamily: titleFamily,
220
- fontWeight: titleWeight,
221
- lineHeight: Math.round(titleSize * 1.2),
222
- textAlign: 'center',
223
- includeFontPadding: false
224
- },
225
- metricValue: {
226
- color: valueColor,
227
- fontSize: valueSize,
228
- fontFamily: valueFamily,
229
- fontWeight: valueWeight,
230
- lineHeight: Math.round(valueSize * 1.2),
231
- textAlign: 'center',
232
- includeFontPadding: false
233
- },
234
- metricCaption: {
235
- color: captionColor,
236
- fontSize: captionSize,
237
- fontFamily: captionFamily,
238
- fontWeight: captionWeight,
239
- lineHeight: Math.round(captionSize * 1.2),
240
- textAlign: 'center',
241
- includeFontPadding: false
242
- }
193
+ buttonIconSize
243
194
  };
244
195
  }
245
196
  const DEFAULT_METRICS = [{
246
197
  title: 'Title',
247
198
  value: 'Value',
248
- caption: 'caption'
199
+ caption: 'caption',
200
+ icon: 'ic_card'
249
201
  }, {
250
202
  title: 'Title',
251
203
  value: 'Value',
252
- caption: 'caption'
204
+ caption: 'caption',
205
+ icon: 'ic_card'
253
206
  }];
254
207
  const styles = StyleSheet.create({
255
208
  mediaSlot: {
@@ -262,6 +215,9 @@ const styles = StyleSheet.create({
262
215
  flexDirection: 'row',
263
216
  alignItems: 'stretch'
264
217
  },
218
+ metricCell: {
219
+ flex: 1
220
+ },
265
221
  metricDivider: {
266
222
  alignSelf: 'center',
267
223
  height: '70%'
@@ -39,6 +39,10 @@ const DEFAULT_ROWS = [{
39
39
  label: 'Bonus JioGold',
40
40
  showInfo: true,
41
41
  values: [false, '1%']
42
+ }, {
43
+ label: 'Bonus JioSilver',
44
+ showInfo: true,
45
+ values: [false, '1%']
42
46
  }];
43
47
 
44
48
  /** Keeps every text layer on a single line. */
@@ -63,13 +67,15 @@ const planColumnStyle = {
63
67
  * current plan against one or more alternative plans across a set of feature
64
68
  * rows. Implementation of Figma node `4498:2968` (`PlanComparisonCard`).
65
69
  *
66
- * Columns use a 1.8fr / 1fr flex ratio (label vs plan), matching the Figma grid.
70
+ * Structure: header (from `columns`) + Slot (default table from `rows`, or
71
+ * custom `children`). Columns use a 1.8fr / 1fr flex ratio (label vs plan).
67
72
  *
68
73
  * @component
69
74
  */
70
75
  function PlanComparisonCard({
71
76
  columns = DEFAULT_COLUMNS,
72
77
  rows = DEFAULT_ROWS,
78
+ children,
73
79
  modes = EMPTY_MODES,
74
80
  style
75
81
  }) {
@@ -166,6 +172,50 @@ function PlanComparisonCard({
166
172
  }
167
173
  return cloneChildrenWithModes(value, modes);
168
174
  };
175
+ const defaultTable = /*#__PURE__*/_jsx(View, {
176
+ style: {
177
+ width: '100%',
178
+ backgroundColor: tableBackground,
179
+ borderWidth: tableBorderSize,
180
+ borderColor: tableBorderColor,
181
+ borderRadius: tableRadius,
182
+ overflow: 'hidden'
183
+ },
184
+ children: rows.map((row, rowIndex) => {
185
+ const isLast = rowIndex === rows.length - 1;
186
+ const showInfo = row.showInfo || row.onInfoPress != null;
187
+ return /*#__PURE__*/_jsxs(View, {
188
+ style: [rowStyle, {
189
+ borderBottomWidth: isLast ? 0 : cellBorderSize,
190
+ borderBottomColor: cellBorderColor
191
+ }],
192
+ children: [/*#__PURE__*/_jsxs(View, {
193
+ style: [labelColumnStyle, labelCellStyle],
194
+ children: [/*#__PURE__*/_jsx(Text, {
195
+ style: labelTextStyle,
196
+ children: row.label
197
+ }), showInfo && (row.onInfoPress ? /*#__PURE__*/_jsx(Pressable, {
198
+ onPress: row.onInfoPress,
199
+ accessibilityRole: "button",
200
+ accessibilityLabel: `More information about ${row.label}`,
201
+ hitSlop: 8,
202
+ children: /*#__PURE__*/_jsx(Icon, {
203
+ name: "ic_info",
204
+ size: iconSize,
205
+ color: iconColor
206
+ })
207
+ }) : /*#__PURE__*/_jsx(Icon, {
208
+ name: "ic_info",
209
+ size: iconSize,
210
+ color: iconColor
211
+ }))]
212
+ }), columns.map((column, colIndex) => /*#__PURE__*/_jsx(View, {
213
+ style: [planColumnStyle, valueCellStyle],
214
+ children: renderValue(row.values?.[colIndex], `${rowIndex}-${colIndex}`)
215
+ }, column.label ?? colIndex))]
216
+ }, row.key ?? `${row.label}-${rowIndex}`);
217
+ })
218
+ });
169
219
  return /*#__PURE__*/_jsxs(View, {
170
220
  style: [{
171
221
  gap,
@@ -184,50 +234,7 @@ function PlanComparisonCard({
184
234
  children: column.label
185
235
  })
186
236
  }, column.label ?? index))]
187
- }), /*#__PURE__*/_jsx(View, {
188
- style: {
189
- width: '100%',
190
- backgroundColor: tableBackground,
191
- borderWidth: tableBorderSize,
192
- borderColor: tableBorderColor,
193
- borderRadius: tableRadius,
194
- overflow: 'hidden'
195
- },
196
- children: rows.map((row, rowIndex) => {
197
- const isLast = rowIndex === rows.length - 1;
198
- const showInfo = row.showInfo || row.onInfoPress != null;
199
- return /*#__PURE__*/_jsxs(View, {
200
- style: [rowStyle, {
201
- borderBottomWidth: isLast ? 0 : cellBorderSize,
202
- borderBottomColor: cellBorderColor
203
- }],
204
- children: [/*#__PURE__*/_jsxs(View, {
205
- style: [labelColumnStyle, labelCellStyle],
206
- children: [/*#__PURE__*/_jsx(Text, {
207
- style: labelTextStyle,
208
- children: row.label
209
- }), showInfo && (row.onInfoPress ? /*#__PURE__*/_jsx(Pressable, {
210
- onPress: row.onInfoPress,
211
- accessibilityRole: "button",
212
- accessibilityLabel: `More information about ${row.label}`,
213
- hitSlop: 8,
214
- children: /*#__PURE__*/_jsx(Icon, {
215
- name: "ic_info",
216
- size: iconSize,
217
- color: iconColor
218
- })
219
- }) : /*#__PURE__*/_jsx(Icon, {
220
- name: "ic_info",
221
- size: iconSize,
222
- color: iconColor
223
- }))]
224
- }), columns.map((column, colIndex) => /*#__PURE__*/_jsx(View, {
225
- style: [planColumnStyle, valueCellStyle],
226
- children: renderValue(row.values?.[colIndex], `${rowIndex}-${colIndex}`)
227
- }, column.label ?? colIndex))]
228
- }, row.key ?? `${row.label}-${rowIndex}`);
229
- })
230
- })]
237
+ }), children != null ? cloneChildrenWithModes(children, modes) : defaultTable]
231
238
  });
232
239
  }
233
240
  export default PlanComparisonCard;
@@ -13,7 +13,7 @@ function PortfolioHero({
13
13
  value = '12,000.11',
14
14
  currency = '₹',
15
15
  modes = {
16
- Context3: 'Balance'
16
+ Context3: 'Balance & Cards'
17
17
  },
18
18
  children,
19
19
  style
@@ -386,9 +386,9 @@ function resolveTokens(modes) {
386
386
  const tooltipBackground = asStr(getVariableByName('tooltip/background', modes), '#0f0d0a');
387
387
  const tooltipPaddingH = asNum(getVariableByName('tooltip/padding/horizontal', modes), 12);
388
388
  const tooltipPaddingV = asNum(getVariableByName('tooltip/padding/vertical', modes), 8);
389
- // Not yet in Coin Variables export (Figma still binds bare local radius/maxWidth).
390
- const tooltipRadius = asNum(getVariableByName('tooltip/radius', modes), 8);
391
- const tooltipMaxWidth = asNum(getVariableByName('tooltip/maxWidth', modes), 280);
389
+ // Figma Tooltip binds bare local `radius` / `maxWidth` (not tooltip/*).
390
+ const tooltipRadius = Number(getVariableByName('radius', modes));
391
+ const tooltipMaxWidth = Number(getVariableByName('maxWidth', modes));
392
392
  const tipWidth = asNum(getVariableByName('tooltip/tipItem/width', modes), 16);
393
393
  const tipHeight = asNum(getVariableByName('tooltip/tipItem/height', modes), 8);
394
394
  const tooltipLabelColor = asStr(getVariableByName('tooltip/label/color', modes), '#ffffff');