jfs-components 0.1.35 → 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 (49) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/D2C.md +82 -87
  3. package/lib/commonjs/components/Accordion/Accordion.js +16 -3
  4. package/lib/commonjs/components/AppBar/AppBar.js +118 -93
  5. package/lib/commonjs/components/CompareTable/CompareTable.js +98 -33
  6. package/lib/commonjs/components/FormField/FormField.js +9 -1
  7. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +4 -14
  8. package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +20 -4
  9. package/lib/commonjs/components/PageHero/PageHero.js +3 -0
  10. package/lib/commonjs/components/ProductMerchandisingCard/ProductMerchandisingCard.js +50 -30
  11. package/lib/commonjs/components/ValueBackMetric/ValueBackMetric.js +117 -89
  12. package/lib/commonjs/components/index.js +7 -1
  13. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -42204
  14. package/lib/commonjs/icons/registry.js +1 -1
  15. package/lib/module/components/Accordion/Accordion.js +16 -3
  16. package/lib/module/components/AppBar/AppBar.js +116 -93
  17. package/lib/module/components/CompareTable/CompareTable.js +99 -34
  18. package/lib/module/components/FormField/FormField.js +9 -1
  19. package/lib/module/components/FullscreenModal/FullscreenModal.js +4 -14
  20. package/lib/module/components/HelloJioInput/HelloJioInput.js +20 -4
  21. package/lib/module/components/PageHero/PageHero.js +3 -0
  22. package/lib/module/components/ProductMerchandisingCard/ProductMerchandisingCard.js +50 -30
  23. package/lib/module/components/ValueBackMetric/ValueBackMetric.js +120 -93
  24. package/lib/module/components/index.js +1 -1
  25. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -42204
  26. package/lib/module/icons/registry.js +1 -1
  27. package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -1
  28. package/lib/typescript/src/components/AppBar/AppBar.d.ts +46 -14
  29. package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +29 -12
  30. package/lib/typescript/src/components/HelloJioInput/HelloJioInput.d.ts +6 -4
  31. package/lib/typescript/src/components/PageHero/PageHero.d.ts +1 -0
  32. package/lib/typescript/src/components/ProductMerchandisingCard/ProductMerchandisingCard.d.ts +8 -1
  33. package/lib/typescript/src/components/ValueBackMetric/ValueBackMetric.d.ts +27 -16
  34. package/lib/typescript/src/components/index.d.ts +1 -1
  35. package/lib/typescript/src/icons/registry.d.ts +1 -1
  36. package/package.json +3 -1
  37. package/scripts/extract-figma-modes.js +150 -121
  38. package/src/components/Accordion/Accordion.tsx +61 -33
  39. package/src/components/AppBar/AppBar.tsx +154 -124
  40. package/src/components/CompareTable/CompareTable.tsx +138 -52
  41. package/src/components/FormField/FormField.tsx +7 -0
  42. package/src/components/FullscreenModal/FullscreenModal.tsx +5 -14
  43. package/src/components/HelloJioInput/HelloJioInput.tsx +31 -12
  44. package/src/components/PageHero/PageHero.tsx +5 -0
  45. package/src/components/ProductMerchandisingCard/ProductMerchandisingCard.tsx +63 -30
  46. package/src/components/ValueBackMetric/ValueBackMetric.tsx +178 -104
  47. package/src/components/index.ts +6 -1
  48. package/src/design-tokens/Coin Variables-variables-full.json +1 -42204
  49. package/src/icons/registry.ts +1 -1
@@ -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",
@@ -57,7 +57,15 @@ const STATE_FALLBACKS = {
57
57
  iconColor: '#24262b'
58
58
  },
59
59
  IdleJioPlus: {
60
- background: '#f5f5f5',
60
+ // Glass fill. In Figma the IdleJioPlus surface resolves
61
+ // `helloJioInput/background` under the `Page type = JioPlus` variable mode,
62
+ // which yields a TRANSLUCENT light fill (not the opaque `#f5f5f5` the
63
+ // MainPage mode gives). Painted over the backdrop blur, an opaque value
64
+ // would read as a solid bar and hide the blur entirely — so, matching the
65
+ // translucent glass fallbacks the sibling GlassFill consumers use
66
+ // (FavoriteToggle `#ffffff33`, NumberPagination `rgba(141,141,141,0.4)`),
67
+ // this fallback carries alpha so the frost tints the blurred content.
68
+ background: '#f5f5f566',
61
69
  borderColor: '#f5f5f5',
62
70
  labelColor: '#545961',
63
71
  iconColor: '#545961'
@@ -94,7 +102,11 @@ function resolveHelloJioInputTokens(modes, visualState) {
94
102
  // Glass keeps the container transparent so GlassFill shows through.
95
103
  backgroundColor: useGlass ? 'transparent' : background,
96
104
  borderColor,
97
- borderWidth: strokeWidth,
105
+ // The Figma IdleJioPlus variant is a borderless frosted pill — it has no
106
+ // `border/color`/`strokeWidth` tokens. Drawing the Idle border color here
107
+ // would leave a faint light ring the design doesn't have, so drop the
108
+ // border entirely in the glass state.
109
+ borderWidth: useGlass ? 0 : strokeWidth,
98
110
  borderStyle: 'solid',
99
111
  borderRadius,
100
112
  paddingLeft,
@@ -201,11 +213,15 @@ const HelloJioInput = /*#__PURE__*/(0, _react.forwardRef)(function HelloJioInput
201
213
  setIsFocused(false);
202
214
  onBlur?.(e);
203
215
  }, [onBlur]);
204
- const leadingElement = leading ?? (leadingIconName ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Icon.default, {
216
+
217
+ // Symmetric with `trailing`: an explicit `null` hides the leading icon
218
+ // (the design's `startSlot={false}`); `undefined` falls back to the
219
+ // default brand icon.
220
+ const leadingElement = leading === undefined ? leadingIconName ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Icon.default, {
205
221
  name: leadingIconName,
206
222
  size: tokens.iconSize,
207
223
  color: tokens.iconColor
208
- }) : null);
224
+ }) : null : leading;
209
225
  const processedLeading = leadingElement ? (0, _reactUtils.cloneChildrenWithModes)(_react.default.Children.toArray(leadingElement), modes) : null;
210
226
  const defaultTrailing = /*#__PURE__*/(0, _jsxRuntime.jsx)(_IconButton.default, {
211
227
  iconName: sendIconName,
@@ -39,6 +39,7 @@ const MEDIA_FALLBACK = 117;
39
39
  * eyebrow="Upgrade to JioFinance+"
40
40
  * headline="Resume earning cashback, extra points, and 1% gold"
41
41
  * supportingText="₹999/year · ₹0 until 2027"
42
+ * body="₹999/year · ₹0 until 2027"
42
43
  * buttonLabel="Renew for free"
43
44
  * onButtonPress={() => navigate('Upgrade')}
44
45
  * modes={{ 'Page type': 'JioPlus' }}
@@ -71,6 +72,7 @@ function PageHero({
71
72
  ...propModes
72
73
  }), [globalModes, propModes]);
73
74
  const gap = Number((0, _figmaVariablesResolver.getVariableByName)('PageHero/gap', modes));
75
+ const paddingHorizontal = Number((0, _figmaVariablesResolver.getVariableByName)('PageHero/padding/horizontal', modes));
74
76
  const textWrapGap = Number((0, _figmaVariablesResolver.getVariableByName)('PageHero/textWrap/gap', modes));
75
77
 
76
78
  // Media slot box — Figma frame 4540:7845 ("Video"), sized by the
@@ -115,6 +117,7 @@ function PageHero({
115
117
  flexDirection: 'column',
116
118
  alignItems: 'center',
117
119
  gap,
120
+ paddingHorizontal,
118
121
  width: '100%'
119
122
  };
120
123
  const textWrapStyle = {
@@ -48,6 +48,7 @@ function ProductMerchandisingCard({
48
48
  title = 'Title',
49
49
  subtitle = 'Subtitle',
50
50
  badgeLabel,
51
+ badgeIcon,
51
52
  badge,
52
53
  showAvatar = true,
53
54
  avatarSource,
@@ -102,29 +103,33 @@ function ProductMerchandisingCard({
102
103
  }), [modes]);
103
104
  const headerBadge = badge ?? (badgeLabel != null ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Badge.default, {
104
105
  label: badgeLabel,
106
+ leading: badgeIcon,
105
107
  modes: badgeModes
106
108
  }) : null);
107
109
  const headerAvatar = showAvatar ? avatar ?? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Avatar.default, {
108
110
  style: avatarSource ? 'Image' : avatarMonogram ? 'Monogram' : 'Image',
109
- imageSource: avatarSource,
110
- monogram: avatarMonogram,
111
+ ...(avatarSource != null ? {
112
+ imageSource: avatarSource
113
+ } : {}),
114
+ ...(avatarMonogram != null ? {
115
+ monogram: avatarMonogram
116
+ } : {}),
111
117
  modes: avatarModes
112
118
  }) : null;
113
119
 
114
- // The footer "special badge" is now the shared <Badge type="glass">. The
115
- // hardcoded literals that defined its look (warm frosted overlay, gentle
116
- // 5px blur, 6/2 padding, 4px radius, weight-500 white label) are preserved
117
- // 1:1 via the glass overrides + `style`/`labelStyle`, and the optional
118
- // `specialBadgeIcon` maps to Badge's `leading` slot — so every existing
119
- // instantiation renders byte-for-byte the same as before.
120
+ // The footer "special badge" is the shared <Badge type="glass">. Overlay +
121
+ // blur come from `badge/glass/*` + `blur/minimal`; box/gap from
122
+ // `productMerchandisingcard/specialbadge/*`; label color/weight from the
123
+ // glass badge tokens (Figma: white / 600). Optional `specialBadgeIcon`
124
+ // maps to Badge's `leading` slot.
120
125
  const footerSpecialBadge = specialBadge ?? (specialBadgeLabel != null ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Badge.default, {
121
126
  type: "glass",
122
127
  label: specialBadgeLabel,
123
128
  leading: specialBadgeIcon,
124
- glassOverlayColor: SPECIAL_BADGE_BG,
125
- glassIntensity: SPECIAL_BADGE_INTENSITY,
129
+ glassOverlayColor: tokens.specialBadgeBg,
130
+ glassIntensity: tokens.specialBadgeIntensity,
126
131
  modes: modes,
127
- style: styles.specialBadge,
132
+ style: tokens.specialBadge,
128
133
  labelStyle: tokens.specialBadgeText,
129
134
  ...(disableTruncation != null ? {
130
135
  disableTruncation
@@ -142,7 +147,9 @@ function ProductMerchandisingCard({
142
147
  const footerCta = cta ?? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Button.default, {
143
148
  label: ctaLabel,
144
149
  modes: ctaModes,
145
- onPress: onCtaPress
150
+ ...(onCtaPress != null ? {
151
+ onPress: onCtaPress
152
+ } : {})
146
153
  });
147
154
  const content = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
148
155
  children: [imageSource != null ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Image.default, {
@@ -303,12 +310,6 @@ function ProductMerchandisingCard({
303
310
  // the ramp (the strongest point); the upper part fades to fully clear. Kept
304
311
  // intentionally small to match the design's subtle "minimal" blur.
305
312
  const BLUR_INTENSITY_FACTOR = 1.0;
306
-
307
- // Special badge ("frosted" pill) — values are literals in the Figma node
308
- // (no dedicated tokens): bg rgba(255,245,229,0.15), 5px backdrop blur.
309
- const SPECIAL_BADGE_BG = 'rgba(255,245,229,0.15)';
310
- const SPECIAL_BADGE_INTENSITY = 17; // ~5px CSS blur / ~2 native blurAmount
311
-
312
313
  function asNum(raw, fallback) {
313
314
  const n = typeof raw === 'number' ? raw : parseFloat(raw);
314
315
  return Number.isFinite(n) ? n : fallback;
@@ -328,6 +329,7 @@ function resolveTokens(modes) {
328
329
  const footerPadH = asNum((0, _figmaVariablesResolver.getVariableByName)('productMerchandisingcard/footer/padding/horizontal', modes), 16);
329
330
  const footerPadV = asNum((0, _figmaVariablesResolver.getVariableByName)('productMerchandisingcard/footer/padding/vertical', modes), 16);
330
331
  const textGap = asNum((0, _figmaVariablesResolver.getVariableByName)('productMerchandisingcard/footer/text/gap', modes), 2);
332
+ const textWrapMinWidth = asNum((0, _figmaVariablesResolver.getVariableByName)('productMerchandisingcard/footer/textWrap/minWidth', modes), 100);
331
333
  const blurRadius = asNum((0, _figmaVariablesResolver.getVariableByName)('blur/minimal', modes), 29);
332
334
  const titleColor = asStr((0, _figmaVariablesResolver.getVariableByName)('productMerchandisingcard/footer/title/font/color', modes), '#ffffff');
333
335
  const titleSize = asNum((0, _figmaVariablesResolver.getVariableByName)('productMerchandisingcard/footer/title/fontsize', modes), 14);
@@ -339,7 +341,20 @@ function resolveTokens(modes) {
339
341
  const subtitleWeight = asStr((0, _figmaVariablesResolver.getVariableByName)('productMerchandisingcard/footer/subtitle/fontweight', modes), '400');
340
342
  const sbSize = asNum((0, _figmaVariablesResolver.getVariableByName)('productMerchandisingcard/specialbadge/text/fontsize', modes), 12);
341
343
  const sbFamily = asStr((0, _figmaVariablesResolver.getVariableByName)('productMerchandisingcard/specialbadge/text/fontfamily', modes), 'JioType Var');
342
- const sbWeight = asStr((0, _figmaVariablesResolver.getVariableByName)('productMerchandisingcard/specialbadge/text/fontweight', modes), '500');
344
+ // No dedicated `specialbadge/text/fontweight` in the collection — Figma
345
+ // binds the glass badge label to `badge/label/fontWeight` (600).
346
+ const sbWeight = asStr((0, _figmaVariablesResolver.getVariableByName)('badge/label/fontWeight', modes), '600');
347
+ const sbPadH = asNum((0, _figmaVariablesResolver.getVariableByName)('productMerchandisingcard/specialbadge/padding/horizontal', modes), 6);
348
+ const sbGap = asNum((0, _figmaVariablesResolver.getVariableByName)('productMerchandisingcard/specialbadge/gap', modes), 4);
349
+ const sbPadV = asNum((0, _figmaVariablesResolver.getVariableByName)('badge/padding/vertical', modes), 4);
350
+ const sbRadius = asNum((0, _figmaVariablesResolver.getVariableByName)('badge/radius', modes), 4);
351
+ const sbLabelColor = asStr((0, _figmaVariablesResolver.getVariableByName)('badge/glass/color/label', modes), '#ffffff');
352
+
353
+ // The special badge is a `type="glass"` Badge: its overlay + blur come
354
+ // straight from the shared badge-glass tokens the design references
355
+ // (`badge/glass/color/background` and the `glass/minimal` → `blur/minimal`
356
+ // effect), so it frosts at the same strength as the footer.
357
+ const specialBadgeBg = asStr((0, _figmaVariablesResolver.getVariableByName)('badge/glass/color/background', modes), 'rgba(255,255,255,0.15)');
343
358
 
344
359
  // SVG `<FeGaussianBlur stdDeviation>` is resolution-independent and renders
345
360
  // an identical Gaussian on iOS, Android and web — so unlike the old
@@ -351,6 +366,10 @@ function resolveTokens(modes) {
351
366
  radius,
352
367
  blurIntensity: Math.max(0, Math.min(100, Math.round(blurRadius * BLUR_INTENSITY_FACTOR))),
353
368
  imageBlurStdDeviation,
369
+ specialBadgeBg,
370
+ // Same `blur/minimal`-derived intensity the footer glass uses, so the
371
+ // special badge frosts consistently with the surface behind it.
372
+ specialBadgeIntensity: Math.max(0, Math.min(100, Math.round(blurRadius * BLUR_INTENSITY_FACTOR))),
354
373
  header: {
355
374
  flexDirection: 'row',
356
375
  alignItems: 'flex-start',
@@ -368,6 +387,9 @@ function resolveTokens(modes) {
368
387
  },
369
388
  textWrap: {
370
389
  flex: 1,
390
+ // The Figma footer pins the text column to a minimum width so a long
391
+ // CTA can never squeeze the title/subtitle away (`footer/textWrap/minWidth`).
392
+ minWidth: textWrapMinWidth,
371
393
  gap: textGap,
372
394
  justifyContent: 'center'
373
395
  },
@@ -387,8 +409,16 @@ function resolveTokens(modes) {
387
409
  lineHeight: Math.round(subtitleSize * 1.2),
388
410
  includeFontPadding: false
389
411
  },
412
+ // Pin the special-badge box to the card collection tokens so a parent
413
+ // `Badge Size` mode cannot inflate/deflate this pill away from Figma.
414
+ specialBadge: {
415
+ paddingHorizontal: sbPadH,
416
+ paddingVertical: sbPadV,
417
+ borderRadius: sbRadius,
418
+ gap: sbGap
419
+ },
390
420
  specialBadgeText: {
391
- color: '#ffffff',
421
+ color: sbLabelColor,
392
422
  fontSize: sbSize,
393
423
  fontFamily: sbFamily,
394
424
  fontWeight: sbWeight,
@@ -414,16 +444,6 @@ const styles = _reactNative.StyleSheet.create({
414
444
  // the intended fade strength.
415
445
  opacity: 0.9
416
446
  },
417
- // Overrides layered on top of <Badge type="glass">. Badge already supplies
418
- // the row layout + 4px gap (when a leading icon is present), `overflow:
419
- // 'hidden'`, `alignSelf: 'flex-start'` and `alignItems: 'center'`; these
420
- // pin the special badge's literal Figma box (6/2 padding, 4px radius)
421
- // regardless of the badge size tokens resolved by the active modes.
422
- specialBadge: {
423
- paddingHorizontal: 6,
424
- paddingVertical: 2,
425
- borderRadius: 4
426
- },
427
447
  pressed: {
428
448
  opacity: 0.92
429
449
  }