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.
- package/CHANGELOG.md +43 -0
- package/D2C.md +82 -87
- package/lib/commonjs/components/Accordion/Accordion.js +16 -3
- package/lib/commonjs/components/AppBar/AppBar.js +118 -93
- package/lib/commonjs/components/CompareTable/CompareTable.js +98 -33
- package/lib/commonjs/components/FormField/FormField.js +9 -1
- package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +4 -14
- package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +20 -4
- package/lib/commonjs/components/PageHero/PageHero.js +3 -0
- package/lib/commonjs/components/ProductMerchandisingCard/ProductMerchandisingCard.js +50 -30
- package/lib/commonjs/components/ValueBackMetric/ValueBackMetric.js +117 -89
- package/lib/commonjs/components/index.js +7 -1
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -42204
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/Accordion/Accordion.js +16 -3
- package/lib/module/components/AppBar/AppBar.js +116 -93
- package/lib/module/components/CompareTable/CompareTable.js +99 -34
- package/lib/module/components/FormField/FormField.js +9 -1
- package/lib/module/components/FullscreenModal/FullscreenModal.js +4 -14
- package/lib/module/components/HelloJioInput/HelloJioInput.js +20 -4
- package/lib/module/components/PageHero/PageHero.js +3 -0
- package/lib/module/components/ProductMerchandisingCard/ProductMerchandisingCard.js +50 -30
- package/lib/module/components/ValueBackMetric/ValueBackMetric.js +120 -93
- package/lib/module/components/index.js +1 -1
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -42204
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -1
- package/lib/typescript/src/components/AppBar/AppBar.d.ts +46 -14
- package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +29 -12
- package/lib/typescript/src/components/HelloJioInput/HelloJioInput.d.ts +6 -4
- package/lib/typescript/src/components/PageHero/PageHero.d.ts +1 -0
- package/lib/typescript/src/components/ProductMerchandisingCard/ProductMerchandisingCard.d.ts +8 -1
- package/lib/typescript/src/components/ValueBackMetric/ValueBackMetric.d.ts +27 -16
- package/lib/typescript/src/components/index.d.ts +1 -1
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +3 -1
- package/scripts/extract-figma-modes.js +150 -121
- package/src/components/Accordion/Accordion.tsx +61 -33
- package/src/components/AppBar/AppBar.tsx +154 -124
- package/src/components/CompareTable/CompareTable.tsx +138 -52
- package/src/components/FormField/FormField.tsx +7 -0
- package/src/components/FullscreenModal/FullscreenModal.tsx +5 -14
- package/src/components/HelloJioInput/HelloJioInput.tsx +31 -12
- package/src/components/PageHero/PageHero.tsx +5 -0
- package/src/components/ProductMerchandisingCard/ProductMerchandisingCard.tsx +63 -30
- package/src/components/ValueBackMetric/ValueBackMetric.tsx +178 -104
- package/src/components/index.ts +6 -1
- package/src/design-tokens/Coin Variables-variables-full.json +1 -42204
- package/src/icons/registry.ts +1 -1
|
@@ -114,7 +114,9 @@ function CompareTable({
|
|
|
114
114
|
style,
|
|
115
115
|
disableTruncation,
|
|
116
116
|
columnWidth,
|
|
117
|
-
|
|
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
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
|
|
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
|
-
//
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
children:
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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.
|
|
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
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
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:
|
|
125
|
-
glassIntensity:
|
|
129
|
+
glassOverlayColor: tokens.specialBadgeBg,
|
|
130
|
+
glassIntensity: tokens.specialBadgeIntensity,
|
|
126
131
|
modes: modes,
|
|
127
|
-
style:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
}
|