jfs-components 0.1.36 → 0.1.49
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 +29 -0
- 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/Section/Section.js +3 -1
- 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/design-tokens/figma-modes.generated.js +8 -7
- 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/Section/Section.js +3 -1
- 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/design-tokens/figma-modes.generated.js +8 -7
- 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/Section/Section.d.ts +2 -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/design-tokens/figma-modes.generated.d.ts +2 -1
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- 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/Section/Section.tsx +121 -156
- 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/design-tokens/figma-modes.generated.ts +8 -7
- package/src/icons/registry.ts +1 -1
- package/D2C.md +0 -113
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useRef } from 'react'
|
|
1
|
+
import React, { useCallback, useRef, useState } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
@@ -151,25 +151,42 @@ export type CompareTableProps = {
|
|
|
151
151
|
* horizontal `ScrollView`s whose scroll events are **chained** — moving
|
|
152
152
|
* one moves every other in lockstep (the cards always stay aligned with
|
|
153
153
|
* the cells beneath them).
|
|
154
|
-
* -
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* vertically with the table.
|
|
154
|
+
* - Selection cards stick on the vertical axis by default (`stickyCards`).
|
|
155
|
+
* Accordion titles also stick by default (`stickyAccordionTitles`) and
|
|
156
|
+
* replace each other under the pinned cards as you scroll. Gray
|
|
157
|
+
* table-header rows can stick via `stickyHeaders`. Sticky header labels
|
|
158
|
+
* stay put horizontally when `stickyHeaderLabel` is on. Requires the
|
|
159
|
+
* host to give `CompareTable` a bounded height.
|
|
161
160
|
*
|
|
162
161
|
* Omit to keep the original flex layout (columns share the available width,
|
|
163
162
|
* no scrolling). @default undefined
|
|
164
163
|
*/
|
|
165
164
|
columnWidth?: number;
|
|
166
165
|
/**
|
|
167
|
-
* When `true
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* fixed-width scroll mode
|
|
166
|
+
* When `true`, the selection-cards row is pinned above the vertical scroll
|
|
167
|
+
* surface (not inside it), so cards never scroll away. Sticky accordion
|
|
168
|
+
* titles then stick under the pinned cards, replacing each other as each
|
|
169
|
+
* section reaches the top. Only applies in fixed-width scroll mode
|
|
170
|
+
* (`columnWidth` is set).
|
|
171
|
+
* @default true
|
|
172
|
+
*/
|
|
173
|
+
stickyCards?: boolean;
|
|
174
|
+
/**
|
|
175
|
+
* When `true`, each section's accordion title sticks to the top of the
|
|
176
|
+
* viewport when scrolling vertically (under sticky cards when
|
|
177
|
+
* `stickyCards` is also on), with titles replacing each other as each
|
|
178
|
+
* reaches the top. Only applies in fixed-width scroll mode
|
|
179
|
+
* (`columnWidth` is set).
|
|
171
180
|
* @default true
|
|
172
181
|
*/
|
|
182
|
+
stickyAccordionTitles?: boolean;
|
|
183
|
+
/**
|
|
184
|
+
* When `true`, each section's header row (the gray header label) sticks to
|
|
185
|
+
* the top of the viewport when scrolling vertically, with multiple sticky
|
|
186
|
+
* headers stacking up as each reaches the top. Only applies in fixed-width
|
|
187
|
+
* scroll mode (`columnWidth` is set).
|
|
188
|
+
* @default false
|
|
189
|
+
*/
|
|
173
190
|
stickyHeaders?: boolean;
|
|
174
191
|
/**
|
|
175
192
|
* When `true` (default), the section header label stays fixed on the left
|
|
@@ -220,7 +237,9 @@ function CompareTable({
|
|
|
220
237
|
style,
|
|
221
238
|
disableTruncation,
|
|
222
239
|
columnWidth,
|
|
223
|
-
|
|
240
|
+
stickyCards = true,
|
|
241
|
+
stickyAccordionTitles = true,
|
|
242
|
+
stickyHeaders = false,
|
|
224
243
|
stickyHeaderLabel = true,
|
|
225
244
|
}: CompareTableProps) {
|
|
226
245
|
// --- selection card tokens ------------------------------------------------
|
|
@@ -235,11 +254,10 @@ function CompareTable({
|
|
|
235
254
|
const cardLabelFontFamily = (getVariableByName('selectionCard/label/fontfamily', modes) as string) ?? 'JioType Var'
|
|
236
255
|
const cardLabelFontWeight = (getVariableByName('selectionCard/label/fontweight', modes) as number | string) ?? 700
|
|
237
256
|
const imageRadius = (getVariableByName('image/radius', modes) as number) ?? 8
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
const cardRadius = (getVariableByName('selectionCard/radius', modes) as number) ?? 0
|
|
257
|
+
// Selection cards are square — no corner radius (Figma compare-table
|
|
258
|
+
// treatment). Ignore `selectionCard/radius` so a non-zero token value
|
|
259
|
+
// cannot round the card edges.
|
|
260
|
+
const cardRadius = 0
|
|
243
261
|
|
|
244
262
|
// --- add-a-card icon button tokens ---------------------------------------
|
|
245
263
|
// padding/size/border come from the shared iconButton tokens; the filled
|
|
@@ -301,6 +319,16 @@ function CompareTable({
|
|
|
301
319
|
const draggingSource = useRef<'cards' | number | null>(null)
|
|
302
320
|
const lastSyncX = useRef(0)
|
|
303
321
|
|
|
322
|
+
// Controlled expand state for sticky accordion titles (header + body are
|
|
323
|
+
// split across two Accordion instances that must stay in sync).
|
|
324
|
+
const [expandedByIndex, setExpandedByIndex] = useState<Record<number, boolean>>(() => {
|
|
325
|
+
const initial: Record<number, boolean> = {}
|
|
326
|
+
sections.forEach((section, index) => {
|
|
327
|
+
initial[index] = section.defaultExpanded ?? index === 0
|
|
328
|
+
})
|
|
329
|
+
return initial
|
|
330
|
+
})
|
|
331
|
+
|
|
304
332
|
const syncOthers = useCallback((source: 'cards' | number, x: number) => {
|
|
305
333
|
if (source !== 'cards' && cardsScrollRef.current) {
|
|
306
334
|
cardsScrollRef.current.scrollTo({ x, animated: false })
|
|
@@ -420,6 +448,10 @@ function CompareTable({
|
|
|
420
448
|
minHeight: 147,
|
|
421
449
|
backgroundColor: cardBg,
|
|
422
450
|
borderRadius: cardRadius,
|
|
451
|
+
borderTopLeftRadius: cardRadius,
|
|
452
|
+
borderTopRightRadius: cardRadius,
|
|
453
|
+
borderBottomLeftRadius: cardRadius,
|
|
454
|
+
borderBottomRightRadius: cardRadius,
|
|
423
455
|
alignItems: 'center',
|
|
424
456
|
justifyContent: 'center',
|
|
425
457
|
gap: cardGap,
|
|
@@ -692,6 +724,7 @@ function CompareTable({
|
|
|
692
724
|
backgroundColor: cardBg,
|
|
693
725
|
borderWidth: 1,
|
|
694
726
|
borderColor: '#e8e8e8',
|
|
727
|
+
borderRadius: 0,
|
|
695
728
|
overflow: 'hidden',
|
|
696
729
|
}
|
|
697
730
|
|
|
@@ -723,17 +756,17 @@ function CompareTable({
|
|
|
723
756
|
}
|
|
724
757
|
|
|
725
758
|
// --- fixed-width scroll mode: chained horizontal sync + sticky cards -----
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
//
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
759
|
+
// Sticky cards are pinned *outside* the vertical ScrollView (sibling above
|
|
760
|
+
// it) so they cannot scroll away — `stickyHeaderIndices` is unreliable for
|
|
761
|
+
// a nested horizontal ScrollView on Fabric. Sticky accordion titles /
|
|
762
|
+
// table headers still use stickyHeaderIndices inside the scroll surface
|
|
763
|
+
// and therefore stick directly under the pinned cards.
|
|
764
|
+
const renderCardsRow = () => (
|
|
765
|
+
<View
|
|
766
|
+
key="cards-row"
|
|
767
|
+
style={{ width: '100%', backgroundColor: cardBg, zIndex: 2 }}
|
|
768
|
+
>
|
|
735
769
|
<ScrollView
|
|
736
|
-
key="cards-row"
|
|
737
770
|
ref={cardsScrollRef}
|
|
738
771
|
horizontal
|
|
739
772
|
showsHorizontalScrollIndicator={false}
|
|
@@ -747,13 +780,59 @@ function CompareTable({
|
|
|
747
780
|
{columns.map(renderCard)}
|
|
748
781
|
{showAddCard && renderAddCard()}
|
|
749
782
|
</View>
|
|
750
|
-
</ScrollView
|
|
751
|
-
|
|
783
|
+
</ScrollView>
|
|
784
|
+
</View>
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
const scrollChildren: React.ReactNode[] = []
|
|
788
|
+
const stickyIndices: number[] = []
|
|
789
|
+
|
|
790
|
+
// Non-sticky cards scroll away with the sections (inside the ScrollView).
|
|
791
|
+
if (showCardsRow && !stickyCards) {
|
|
792
|
+
scrollChildren.push(renderCardsRow())
|
|
752
793
|
}
|
|
753
794
|
|
|
754
795
|
sections.forEach((section, index) => {
|
|
755
796
|
const sectionKey = section.key ?? section.title ?? index
|
|
756
797
|
const headerContent = renderHeaderContent(section)
|
|
798
|
+
const expanded = expandedByIndex[index] ?? (section.defaultExpanded ?? index === 0)
|
|
799
|
+
const onExpandedChange = (next: boolean) => {
|
|
800
|
+
setExpandedByIndex((prev) => ({ ...prev, [index]: next }))
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const inlineHeader =
|
|
804
|
+
!stickyHeaders && headerContent != null
|
|
805
|
+
? stickyHeaderLabel
|
|
806
|
+
? headerContent
|
|
807
|
+
: (
|
|
808
|
+
<ScrollView
|
|
809
|
+
horizontal
|
|
810
|
+
showsHorizontalScrollIndicator={false}
|
|
811
|
+
>
|
|
812
|
+
<View style={{ width: contentWidth }}>
|
|
813
|
+
{headerContent}
|
|
814
|
+
</View>
|
|
815
|
+
</ScrollView>
|
|
816
|
+
)
|
|
817
|
+
: null
|
|
818
|
+
|
|
819
|
+
if (stickyAccordionTitles) {
|
|
820
|
+
stickyIndices.push(scrollChildren.length)
|
|
821
|
+
scrollChildren.push(
|
|
822
|
+
<View
|
|
823
|
+
key={`sticky-accordion-title-${sectionKey}`}
|
|
824
|
+
style={{ width: '100%', backgroundColor: cardBg }}
|
|
825
|
+
>
|
|
826
|
+
<Accordion
|
|
827
|
+
title={section.title}
|
|
828
|
+
expanded={expanded}
|
|
829
|
+
onExpandedChange={onExpandedChange}
|
|
830
|
+
modes={modes}
|
|
831
|
+
showContent={false}
|
|
832
|
+
/>
|
|
833
|
+
</View>,
|
|
834
|
+
)
|
|
835
|
+
}
|
|
757
836
|
|
|
758
837
|
if (stickyHeaders && headerContent != null) {
|
|
759
838
|
stickyIndices.push(scrollChildren.length)
|
|
@@ -778,34 +857,41 @@ function CompareTable({
|
|
|
778
857
|
)
|
|
779
858
|
}
|
|
780
859
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
860
|
+
if (stickyAccordionTitles) {
|
|
861
|
+
scrollChildren.push(
|
|
862
|
+
<Accordion
|
|
863
|
+
key={sectionKey}
|
|
864
|
+
title={section.title}
|
|
865
|
+
expanded={expanded}
|
|
866
|
+
onExpandedChange={onExpandedChange}
|
|
867
|
+
modes={modes}
|
|
868
|
+
showHeader={false}
|
|
869
|
+
>
|
|
870
|
+
{inlineHeader}
|
|
871
|
+
{renderBodyContent(section, index)}
|
|
872
|
+
</Accordion>,
|
|
873
|
+
)
|
|
874
|
+
} else {
|
|
875
|
+
scrollChildren.push(
|
|
876
|
+
<Accordion
|
|
877
|
+
key={sectionKey}
|
|
878
|
+
title={section.title}
|
|
879
|
+
defaultExpanded={section.defaultExpanded ?? index === 0}
|
|
880
|
+
modes={modes}
|
|
881
|
+
>
|
|
882
|
+
{inlineHeader}
|
|
883
|
+
{renderBodyContent(section, index)}
|
|
884
|
+
</Accordion>,
|
|
885
|
+
)
|
|
886
|
+
}
|
|
803
887
|
})
|
|
804
888
|
|
|
805
889
|
return (
|
|
806
890
|
<View style={[outerStyle, style, { flexDirection: 'column', flex: 1 }]}>
|
|
891
|
+
{showCardsRow && stickyCards ? renderCardsRow() : null}
|
|
807
892
|
<ScrollView
|
|
808
893
|
style={{ width: '100%', flex: 1 }}
|
|
894
|
+
contentContainerStyle={{ flexGrow: 1 }}
|
|
809
895
|
stickyHeaderIndices={stickyIndices}
|
|
810
896
|
showsVerticalScrollIndicator={false}
|
|
811
897
|
directionalLockEnabled
|
|
@@ -521,6 +521,9 @@ const FormField = forwardRef<RNTextInput, FormFieldProps>(function FormField({
|
|
|
521
521
|
fontWeight: tokens.inputFontWeight,
|
|
522
522
|
padding: 0,
|
|
523
523
|
margin: 0,
|
|
524
|
+
// Match the caret to the tokenized input text color on web (native uses
|
|
525
|
+
// cursorColor / selectionColor on the TextInput below).
|
|
526
|
+
caretColor: tokens.inputTextColor,
|
|
524
527
|
// Remove the default web focus ring; the input row's border acts as the
|
|
525
528
|
// focus indicator via the FormField States cascade.
|
|
526
529
|
outlineStyle: 'none' as TextStyle['outlineStyle'],
|
|
@@ -601,6 +604,10 @@ const FormField = forwardRef<RNTextInput, FormFieldProps>(function FormField({
|
|
|
601
604
|
onSubmitEditing={onSubmitEditing}
|
|
602
605
|
placeholder={placeholder ?? ''}
|
|
603
606
|
placeholderTextColor={placeholderColor}
|
|
607
|
+
// Kill platform accent cursors (Android Material green, iOS system
|
|
608
|
+
// blue) — match NoteInput/MoneyValue and use the tokenized text color.
|
|
609
|
+
cursorColor={tokens.inputTextColor}
|
|
610
|
+
selectionColor={tokens.inputTextColor}
|
|
604
611
|
editable={interactive}
|
|
605
612
|
maxLength={maxLength}
|
|
606
613
|
autoFocus={autoFocus}
|
|
@@ -270,22 +270,15 @@ function FullscreenModal({
|
|
|
270
270
|
|
|
271
271
|
const rootGap = Number(getVariableByName('fullScreenModal/gap', modes))
|
|
272
272
|
|
|
273
|
-
// Safe-area
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
// SafeAreaProvider — every inset is 0, so the layout is unchanged.
|
|
273
|
+
// Safe-area inset so the floating close button clears the status bar / notch.
|
|
274
|
+
// Footer padding stays at the design-token value from ActionFooter — no
|
|
275
|
+
// extra inset is layered on top (the token already accounts for bottom chrome).
|
|
276
|
+
// On web — and anywhere without a SafeAreaProvider — insets.top is 0.
|
|
278
277
|
const insets = useSafeAreaInsets()
|
|
279
278
|
const closeButtonInsetStyle = useMemo<ViewStyle>(
|
|
280
279
|
() => ({ top: 12 + insets.top + closeOffsetY }),
|
|
281
280
|
[insets.top, closeOffsetY]
|
|
282
281
|
)
|
|
283
|
-
// Extend (not replace) the footer's token bottom padding by the bottom inset
|
|
284
|
-
// so the action button never sits under the system navigation area.
|
|
285
|
-
const footerInsetStyle = useMemo<ViewStyle>(() => {
|
|
286
|
-
const base = Number(getVariableByName('actionFooter/padding/bottom', modes))
|
|
287
|
-
return { paddingBottom: base + insets.bottom }
|
|
288
|
-
}, [modes, insets.bottom])
|
|
289
282
|
|
|
290
283
|
// Drives the background's parallax-free sync with the scroll. The hero media
|
|
291
284
|
// lives at the ROOT (so it is never clipped to the content height and sits
|
|
@@ -471,9 +464,7 @@ function FullscreenModal({
|
|
|
471
464
|
</Animated.ScrollView>
|
|
472
465
|
|
|
473
466
|
{footerContent ? (
|
|
474
|
-
<ActionFooter modes={modes}
|
|
475
|
-
{footerContent}
|
|
476
|
-
</ActionFooter>
|
|
467
|
+
<ActionFooter modes={modes}>{footerContent}</ActionFooter>
|
|
477
468
|
) : null}
|
|
478
469
|
|
|
479
470
|
{showClose ? (
|
|
@@ -85,7 +85,15 @@ const STATE_FALLBACKS: Record<
|
|
|
85
85
|
iconColor: '#24262b',
|
|
86
86
|
},
|
|
87
87
|
IdleJioPlus: {
|
|
88
|
-
|
|
88
|
+
// Glass fill. In Figma the IdleJioPlus surface resolves
|
|
89
|
+
// `helloJioInput/background` under the `Page type = JioPlus` variable mode,
|
|
90
|
+
// which yields a TRANSLUCENT light fill (not the opaque `#f5f5f5` the
|
|
91
|
+
// MainPage mode gives). Painted over the backdrop blur, an opaque value
|
|
92
|
+
// would read as a solid bar and hide the blur entirely — so, matching the
|
|
93
|
+
// translucent glass fallbacks the sibling GlassFill consumers use
|
|
94
|
+
// (FavoriteToggle `#ffffff33`, NumberPagination `rgba(141,141,141,0.4)`),
|
|
95
|
+
// this fallback carries alpha so the frost tints the blurred content.
|
|
96
|
+
background: '#f5f5f566',
|
|
89
97
|
borderColor: '#f5f5f5',
|
|
90
98
|
labelColor: '#545961',
|
|
91
99
|
iconColor: '#545961',
|
|
@@ -113,9 +121,10 @@ export type HelloJioInputProps = {
|
|
|
113
121
|
leadingIconName?: string
|
|
114
122
|
/**
|
|
115
123
|
* Slot replacing the default leading icon (Figma Slot "start"). Receives
|
|
116
|
-
* `modes` recursively.
|
|
124
|
+
* `modes` recursively. Pass `null` to hide the leading icon entirely
|
|
125
|
+
* (the design's `startSlot={false}`), mirroring `trailing`.
|
|
117
126
|
*/
|
|
118
|
-
leading?: React.ReactNode
|
|
127
|
+
leading?: React.ReactNode | null
|
|
119
128
|
/**
|
|
120
129
|
* Slot replacing the default send `IconButton` (Figma Slot "end").
|
|
121
130
|
* Pass `null` to hide the trailing control entirely.
|
|
@@ -246,7 +255,11 @@ function resolveHelloJioInputTokens(
|
|
|
246
255
|
// Glass keeps the container transparent so GlassFill shows through.
|
|
247
256
|
backgroundColor: useGlass ? 'transparent' : background,
|
|
248
257
|
borderColor,
|
|
249
|
-
|
|
258
|
+
// The Figma IdleJioPlus variant is a borderless frosted pill — it has no
|
|
259
|
+
// `border/color`/`strokeWidth` tokens. Drawing the Idle border color here
|
|
260
|
+
// would leave a faint light ring the design doesn't have, so drop the
|
|
261
|
+
// border entirely in the glass state.
|
|
262
|
+
borderWidth: useGlass ? 0 : strokeWidth,
|
|
250
263
|
borderStyle: 'solid',
|
|
251
264
|
borderRadius,
|
|
252
265
|
paddingLeft,
|
|
@@ -382,15 +395,21 @@ const HelloJioInput = forwardRef<RNTextInput, HelloJioInputProps>(
|
|
|
382
395
|
[onBlur]
|
|
383
396
|
)
|
|
384
397
|
|
|
398
|
+
// Symmetric with `trailing`: an explicit `null` hides the leading icon
|
|
399
|
+
// (the design's `startSlot={false}`); `undefined` falls back to the
|
|
400
|
+
// default brand icon.
|
|
385
401
|
const leadingElement =
|
|
386
|
-
leading
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
402
|
+
leading === undefined
|
|
403
|
+
? leadingIconName
|
|
404
|
+
? (
|
|
405
|
+
<Icon
|
|
406
|
+
name={leadingIconName}
|
|
407
|
+
size={tokens.iconSize}
|
|
408
|
+
color={tokens.iconColor}
|
|
409
|
+
/>
|
|
410
|
+
)
|
|
411
|
+
: null
|
|
412
|
+
: leading
|
|
394
413
|
|
|
395
414
|
const processedLeading = leadingElement
|
|
396
415
|
? cloneChildrenWithModes(React.Children.toArray(leadingElement), modes)
|
|
@@ -93,6 +93,7 @@ export type PageHeroProps = {
|
|
|
93
93
|
* eyebrow="Upgrade to JioFinance+"
|
|
94
94
|
* headline="Resume earning cashback, extra points, and 1% gold"
|
|
95
95
|
* supportingText="₹999/year · ₹0 until 2027"
|
|
96
|
+
* body="₹999/year · ₹0 until 2027"
|
|
96
97
|
* buttonLabel="Renew for free"
|
|
97
98
|
* onButtonPress={() => navigate('Upgrade')}
|
|
98
99
|
* modes={{ 'Page type': 'JioPlus' }}
|
|
@@ -124,6 +125,9 @@ function PageHero({
|
|
|
124
125
|
)
|
|
125
126
|
|
|
126
127
|
const gap = Number(getVariableByName('PageHero/gap', modes))
|
|
128
|
+
const paddingHorizontal = Number(
|
|
129
|
+
getVariableByName('PageHero/padding/horizontal', modes)
|
|
130
|
+
)
|
|
127
131
|
const textWrapGap = Number(getVariableByName('PageHero/textWrap/gap', modes))
|
|
128
132
|
|
|
129
133
|
// Media slot box — Figma frame 4540:7845 ("Video"), sized by the
|
|
@@ -218,6 +222,7 @@ function PageHero({
|
|
|
218
222
|
flexDirection: 'column',
|
|
219
223
|
alignItems: 'center',
|
|
220
224
|
gap,
|
|
225
|
+
paddingHorizontal,
|
|
221
226
|
width: '100%',
|
|
222
227
|
}
|
|
223
228
|
|
|
@@ -51,6 +51,13 @@ export interface ProductMerchandisingCardProps {
|
|
|
51
51
|
* `undefined` and no `badge` override is provided.
|
|
52
52
|
*/
|
|
53
53
|
badgeLabel?: string
|
|
54
|
+
/**
|
|
55
|
+
* Optional leading node (icon/image) rendered before the header badge
|
|
56
|
+
* label, inside the badge surface. Maps to `Badge`'s `leading` slot — the
|
|
57
|
+
* same pattern as `specialBadgeIcon` for the footer badge. Ignored when a
|
|
58
|
+
* full `badge` override is supplied.
|
|
59
|
+
*/
|
|
60
|
+
badgeIcon?: React.ReactNode
|
|
54
61
|
/**
|
|
55
62
|
* Full override for the header badge. Takes precedence over `badgeLabel`.
|
|
56
63
|
*/
|
|
@@ -122,6 +129,7 @@ function ProductMerchandisingCard({
|
|
|
122
129
|
title = 'Title',
|
|
123
130
|
subtitle = 'Subtitle',
|
|
124
131
|
badgeLabel,
|
|
132
|
+
badgeIcon,
|
|
125
133
|
badge,
|
|
126
134
|
showAvatar = true,
|
|
127
135
|
avatarSource,
|
|
@@ -175,25 +183,27 @@ function ProductMerchandisingCard({
|
|
|
175
183
|
)
|
|
176
184
|
|
|
177
185
|
const headerBadge =
|
|
178
|
-
badge ??
|
|
186
|
+
badge ??
|
|
187
|
+
(badgeLabel != null ? (
|
|
188
|
+
<Badge label={badgeLabel} leading={badgeIcon} modes={badgeModes} />
|
|
189
|
+
) : null)
|
|
179
190
|
|
|
180
191
|
const headerAvatar = showAvatar
|
|
181
192
|
? avatar ?? (
|
|
182
193
|
<Avatar
|
|
183
194
|
style={avatarSource ? 'Image' : avatarMonogram ? 'Monogram' : 'Image'}
|
|
184
|
-
|
|
185
|
-
|
|
195
|
+
{...(avatarSource != null ? { imageSource: avatarSource } : {})}
|
|
196
|
+
{...(avatarMonogram != null ? { monogram: avatarMonogram } : {})}
|
|
186
197
|
modes={avatarModes}
|
|
187
198
|
/>
|
|
188
199
|
)
|
|
189
200
|
: null
|
|
190
201
|
|
|
191
|
-
// The footer "special badge" is
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
// instantiation renders byte-for-byte the same as before.
|
|
202
|
+
// The footer "special badge" is the shared <Badge type="glass">. Overlay +
|
|
203
|
+
// blur come from `badge/glass/*` + `blur/minimal`; box/gap from
|
|
204
|
+
// `productMerchandisingcard/specialbadge/*`; label color/weight from the
|
|
205
|
+
// glass badge tokens (Figma: white / 600). Optional `specialBadgeIcon`
|
|
206
|
+
// maps to Badge's `leading` slot.
|
|
197
207
|
const footerSpecialBadge =
|
|
198
208
|
specialBadge ??
|
|
199
209
|
(specialBadgeLabel != null ? (
|
|
@@ -201,10 +211,10 @@ function ProductMerchandisingCard({
|
|
|
201
211
|
type="glass"
|
|
202
212
|
label={specialBadgeLabel}
|
|
203
213
|
leading={specialBadgeIcon}
|
|
204
|
-
glassOverlayColor={
|
|
205
|
-
glassIntensity={
|
|
214
|
+
glassOverlayColor={tokens.specialBadgeBg}
|
|
215
|
+
glassIntensity={tokens.specialBadgeIntensity}
|
|
206
216
|
modes={modes}
|
|
207
|
-
style={
|
|
217
|
+
style={tokens.specialBadge}
|
|
208
218
|
labelStyle={tokens.specialBadgeText}
|
|
209
219
|
{...(disableTruncation != null ? { disableTruncation } : null)}
|
|
210
220
|
/>
|
|
@@ -219,7 +229,13 @@ function ProductMerchandisingCard({
|
|
|
219
229
|
)
|
|
220
230
|
|
|
221
231
|
const footerCta =
|
|
222
|
-
cta ??
|
|
232
|
+
cta ?? (
|
|
233
|
+
<Button
|
|
234
|
+
label={ctaLabel}
|
|
235
|
+
modes={ctaModes}
|
|
236
|
+
{...(onCtaPress != null ? { onPress: onCtaPress } : {})}
|
|
237
|
+
/>
|
|
238
|
+
)
|
|
223
239
|
|
|
224
240
|
const content = (
|
|
225
241
|
<>
|
|
@@ -372,11 +388,6 @@ function ProductMerchandisingCard({
|
|
|
372
388
|
// intentionally small to match the design's subtle "minimal" blur.
|
|
373
389
|
const BLUR_INTENSITY_FACTOR = 1.0
|
|
374
390
|
|
|
375
|
-
// Special badge ("frosted" pill) — values are literals in the Figma node
|
|
376
|
-
// (no dedicated tokens): bg rgba(255,245,229,0.15), 5px backdrop blur.
|
|
377
|
-
const SPECIAL_BADGE_BG = 'rgba(255,245,229,0.15)'
|
|
378
|
-
const SPECIAL_BADGE_INTENSITY = 17 // ~5px CSS blur / ~2 native blurAmount
|
|
379
|
-
|
|
380
391
|
interface ResolvedTokens {
|
|
381
392
|
radius: number
|
|
382
393
|
blurIntensity: number
|
|
@@ -386,7 +397,10 @@ interface ResolvedTokens {
|
|
|
386
397
|
textWrap: ViewStyle
|
|
387
398
|
title: TextStyle
|
|
388
399
|
subtitle: TextStyle
|
|
400
|
+
specialBadge: ViewStyle
|
|
389
401
|
specialBadgeText: TextStyle
|
|
402
|
+
specialBadgeBg: string
|
|
403
|
+
specialBadgeIntensity: number
|
|
390
404
|
}
|
|
391
405
|
|
|
392
406
|
function asNum(raw: unknown, fallback: number): number {
|
|
@@ -410,6 +424,7 @@ function resolveTokens(modes: Modes): ResolvedTokens {
|
|
|
410
424
|
const footerPadH = asNum(getVariableByName('productMerchandisingcard/footer/padding/horizontal', modes), 16)
|
|
411
425
|
const footerPadV = asNum(getVariableByName('productMerchandisingcard/footer/padding/vertical', modes), 16)
|
|
412
426
|
const textGap = asNum(getVariableByName('productMerchandisingcard/footer/text/gap', modes), 2)
|
|
427
|
+
const textWrapMinWidth = asNum(getVariableByName('productMerchandisingcard/footer/textWrap/minWidth', modes), 100)
|
|
413
428
|
const blurRadius = asNum(getVariableByName('blur/minimal', modes), 29)
|
|
414
429
|
|
|
415
430
|
const titleColor = asStr(getVariableByName('productMerchandisingcard/footer/title/font/color', modes), '#ffffff')
|
|
@@ -424,7 +439,20 @@ function resolveTokens(modes: Modes): ResolvedTokens {
|
|
|
424
439
|
|
|
425
440
|
const sbSize = asNum(getVariableByName('productMerchandisingcard/specialbadge/text/fontsize', modes), 12)
|
|
426
441
|
const sbFamily = asStr(getVariableByName('productMerchandisingcard/specialbadge/text/fontfamily', modes), 'JioType Var')
|
|
427
|
-
|
|
442
|
+
// No dedicated `specialbadge/text/fontweight` in the collection — Figma
|
|
443
|
+
// binds the glass badge label to `badge/label/fontWeight` (600).
|
|
444
|
+
const sbWeight = asStr(getVariableByName('badge/label/fontWeight', modes), '600')
|
|
445
|
+
const sbPadH = asNum(getVariableByName('productMerchandisingcard/specialbadge/padding/horizontal', modes), 6)
|
|
446
|
+
const sbGap = asNum(getVariableByName('productMerchandisingcard/specialbadge/gap', modes), 4)
|
|
447
|
+
const sbPadV = asNum(getVariableByName('badge/padding/vertical', modes), 4)
|
|
448
|
+
const sbRadius = asNum(getVariableByName('badge/radius', modes), 4)
|
|
449
|
+
const sbLabelColor = asStr(getVariableByName('badge/glass/color/label', modes), '#ffffff')
|
|
450
|
+
|
|
451
|
+
// The special badge is a `type="glass"` Badge: its overlay + blur come
|
|
452
|
+
// straight from the shared badge-glass tokens the design references
|
|
453
|
+
// (`badge/glass/color/background` and the `glass/minimal` → `blur/minimal`
|
|
454
|
+
// effect), so it frosts at the same strength as the footer.
|
|
455
|
+
const specialBadgeBg = asStr(getVariableByName('badge/glass/color/background', modes), 'rgba(255,255,255,0.15)')
|
|
428
456
|
|
|
429
457
|
// SVG `<FeGaussianBlur stdDeviation>` is resolution-independent and renders
|
|
430
458
|
// an identical Gaussian on iOS, Android and web — so unlike the old
|
|
@@ -437,6 +465,10 @@ function resolveTokens(modes: Modes): ResolvedTokens {
|
|
|
437
465
|
radius,
|
|
438
466
|
blurIntensity: Math.max(0, Math.min(100, Math.round(blurRadius * BLUR_INTENSITY_FACTOR))),
|
|
439
467
|
imageBlurStdDeviation,
|
|
468
|
+
specialBadgeBg,
|
|
469
|
+
// Same `blur/minimal`-derived intensity the footer glass uses, so the
|
|
470
|
+
// special badge frosts consistently with the surface behind it.
|
|
471
|
+
specialBadgeIntensity: Math.max(0, Math.min(100, Math.round(blurRadius * BLUR_INTENSITY_FACTOR))),
|
|
440
472
|
header: {
|
|
441
473
|
flexDirection: 'row',
|
|
442
474
|
alignItems: 'flex-start',
|
|
@@ -454,6 +486,9 @@ function resolveTokens(modes: Modes): ResolvedTokens {
|
|
|
454
486
|
},
|
|
455
487
|
textWrap: {
|
|
456
488
|
flex: 1,
|
|
489
|
+
// The Figma footer pins the text column to a minimum width so a long
|
|
490
|
+
// CTA can never squeeze the title/subtitle away (`footer/textWrap/minWidth`).
|
|
491
|
+
minWidth: textWrapMinWidth,
|
|
457
492
|
gap: textGap,
|
|
458
493
|
justifyContent: 'center',
|
|
459
494
|
},
|
|
@@ -473,8 +508,16 @@ function resolveTokens(modes: Modes): ResolvedTokens {
|
|
|
473
508
|
lineHeight: Math.round(subtitleSize * 1.2),
|
|
474
509
|
includeFontPadding: false as any,
|
|
475
510
|
},
|
|
511
|
+
// Pin the special-badge box to the card collection tokens so a parent
|
|
512
|
+
// `Badge Size` mode cannot inflate/deflate this pill away from Figma.
|
|
513
|
+
specialBadge: {
|
|
514
|
+
paddingHorizontal: sbPadH,
|
|
515
|
+
paddingVertical: sbPadV,
|
|
516
|
+
borderRadius: sbRadius,
|
|
517
|
+
gap: sbGap,
|
|
518
|
+
},
|
|
476
519
|
specialBadgeText: {
|
|
477
|
-
color:
|
|
520
|
+
color: sbLabelColor,
|
|
478
521
|
fontSize: sbSize,
|
|
479
522
|
fontFamily: sbFamily,
|
|
480
523
|
fontWeight: sbWeight as TextStyle['fontWeight'],
|
|
@@ -501,16 +544,6 @@ const styles = StyleSheet.create({
|
|
|
501
544
|
// the intended fade strength.
|
|
502
545
|
opacity: 0.9,
|
|
503
546
|
},
|
|
504
|
-
// Overrides layered on top of <Badge type="glass">. Badge already supplies
|
|
505
|
-
// the row layout + 4px gap (when a leading icon is present), `overflow:
|
|
506
|
-
// 'hidden'`, `alignSelf: 'flex-start'` and `alignItems: 'center'`; these
|
|
507
|
-
// pin the special badge's literal Figma box (6/2 padding, 4px radius)
|
|
508
|
-
// regardless of the badge size tokens resolved by the active modes.
|
|
509
|
-
specialBadge: {
|
|
510
|
-
paddingHorizontal: 6,
|
|
511
|
-
paddingVertical: 2,
|
|
512
|
-
borderRadius: 4,
|
|
513
|
-
},
|
|
514
547
|
pressed: {
|
|
515
548
|
opacity: 0.92,
|
|
516
549
|
},
|