jfs-components 0.0.74 → 0.0.78
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 +109 -0
- package/lib/commonjs/components/Accordion/Accordion.js +55 -55
- package/lib/commonjs/components/ActionFooter/ActionFooter.js +193 -82
- package/lib/commonjs/components/Avatar/Avatar.js +20 -0
- package/lib/commonjs/components/Badge/Badge.js +23 -0
- package/lib/commonjs/components/Button/Button.js +37 -0
- package/lib/commonjs/components/Checkbox/Checkbox.js +21 -9
- package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -16
- package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +167 -0
- package/lib/commonjs/components/FormField/FormField.js +14 -1
- package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +355 -0
- package/lib/commonjs/components/IconButton/IconButton.js +20 -0
- package/lib/commonjs/components/Image/Image.js +26 -1
- package/lib/commonjs/components/ListItem/ListItem.js +25 -10
- package/lib/commonjs/components/LottiePlayer/LottiePlayer.js +116 -0
- package/lib/commonjs/components/LottiePlayer/LottiePlayer.web.js +82 -0
- package/lib/commonjs/components/LottiePlayer/loadNativeLottieView.js +74 -0
- package/lib/commonjs/components/LottiePlayer/loadWebLottieView.js +50 -0
- package/lib/commonjs/components/MessageField/MessageField.js +318 -0
- package/lib/commonjs/components/NavArrow/NavArrow.js +58 -17
- package/lib/commonjs/components/PageHero/PageHero.js +41 -5
- package/lib/commonjs/components/RechargeCard/RechargeCard.js +32 -17
- package/lib/commonjs/components/Stepper/Step.js +47 -60
- package/lib/commonjs/components/Stepper/StepLabel.js +40 -10
- package/lib/commonjs/components/Stepper/Stepper.js +15 -17
- package/lib/commonjs/components/SuggestiveSearch/SuggestiveSearch.js +487 -0
- package/lib/commonjs/components/Text/Text.js +31 -1
- package/lib/commonjs/components/TextInput/TextInput.js +16 -1
- package/lib/commonjs/components/Title/Title.js +10 -2
- package/lib/commonjs/components/index.js +35 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/Icon.js +16 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/commonjs/index.js +12 -0
- package/lib/commonjs/skeleton/Skeleton.js +234 -0
- package/lib/commonjs/skeleton/SkeletonGroup.js +140 -0
- package/lib/commonjs/skeleton/index.js +58 -0
- package/lib/commonjs/skeleton/shimmer-tokens.js +189 -0
- package/lib/commonjs/skeleton/useReducedMotion.js +64 -0
- package/lib/module/components/Accordion/Accordion.js +56 -56
- package/lib/module/components/ActionFooter/ActionFooter.js +193 -83
- package/lib/module/components/Avatar/Avatar.js +19 -0
- package/lib/module/components/Badge/Badge.js +23 -0
- package/lib/module/components/Button/Button.js +37 -0
- package/lib/module/components/Checkbox/Checkbox.js +22 -10
- package/lib/module/components/DropdownInput/DropdownInput.js +30 -16
- package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +161 -0
- package/lib/module/components/FormField/FormField.js +16 -3
- package/lib/module/components/FullscreenModal/FullscreenModal.js +350 -0
- package/lib/module/components/IconButton/IconButton.js +20 -0
- package/lib/module/components/Image/Image.js +25 -1
- package/lib/module/components/ListItem/ListItem.js +25 -10
- package/lib/module/components/LottiePlayer/LottiePlayer.js +111 -0
- package/lib/module/components/LottiePlayer/LottiePlayer.web.js +77 -0
- package/lib/module/components/LottiePlayer/loadNativeLottieView.js +69 -0
- package/lib/module/components/LottiePlayer/loadWebLottieView.js +45 -0
- package/lib/module/components/MessageField/MessageField.js +313 -0
- package/lib/module/components/NavArrow/NavArrow.js +59 -18
- package/lib/module/components/PageHero/PageHero.js +41 -5
- package/lib/module/components/RechargeCard/RechargeCard.js +33 -17
- package/lib/module/components/Stepper/Step.js +48 -61
- package/lib/module/components/Stepper/StepLabel.js +40 -10
- package/lib/module/components/Stepper/Stepper.js +15 -17
- package/lib/module/components/SuggestiveSearch/SuggestiveSearch.js +481 -0
- package/lib/module/components/Text/Text.js +31 -1
- package/lib/module/components/TextInput/TextInput.js +17 -2
- package/lib/module/components/Title/Title.js +10 -2
- package/lib/module/components/index.js +5 -0
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/Icon.js +16 -0
- package/lib/module/icons/registry.js +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/skeleton/Skeleton.js +229 -0
- package/lib/module/skeleton/SkeletonGroup.js +133 -0
- package/lib/module/skeleton/index.js +6 -0
- package/lib/module/skeleton/shimmer-tokens.js +181 -0
- package/lib/module/skeleton/useReducedMotion.js +61 -0
- package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -20
- package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +26 -21
- package/lib/typescript/src/components/Avatar/Avatar.d.ts +7 -1
- package/lib/typescript/src/components/Badge/Badge.d.ts +7 -1
- package/lib/typescript/src/components/Button/Button.d.ts +8 -1
- package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +63 -0
- package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +99 -0
- package/lib/typescript/src/components/IconButton/IconButton.d.ts +7 -1
- package/lib/typescript/src/components/Image/Image.d.ts +8 -1
- package/lib/typescript/src/components/LottiePlayer/LottiePlayer.d.ts +85 -0
- package/lib/typescript/src/components/LottiePlayer/LottiePlayer.web.d.ts +28 -0
- package/lib/typescript/src/components/LottiePlayer/loadNativeLottieView.d.ts +11 -0
- package/lib/typescript/src/components/LottiePlayer/loadWebLottieView.d.ts +11 -0
- package/lib/typescript/src/components/MessageField/MessageField.d.ts +81 -0
- package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +10 -5
- package/lib/typescript/src/components/PageHero/PageHero.d.ts +31 -5
- package/lib/typescript/src/components/Stepper/Step.d.ts +4 -1
- package/lib/typescript/src/components/Stepper/StepLabel.d.ts +4 -1
- package/lib/typescript/src/components/Stepper/Stepper.d.ts +3 -1
- package/lib/typescript/src/components/SuggestiveSearch/SuggestiveSearch.d.ts +123 -0
- package/lib/typescript/src/components/Text/Text.d.ts +20 -1
- package/lib/typescript/src/components/index.d.ts +8 -3
- package/lib/typescript/src/icons/Icon.d.ts +7 -1
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/skeleton/Skeleton.d.ts +60 -0
- package/lib/typescript/src/skeleton/SkeletonGroup.d.ts +78 -0
- package/lib/typescript/src/skeleton/index.d.ts +5 -0
- package/lib/typescript/src/skeleton/shimmer-tokens.d.ts +160 -0
- package/lib/typescript/src/skeleton/useReducedMotion.d.ts +15 -0
- package/package.json +11 -1
- package/src/components/Accordion/Accordion.tsx +113 -73
- package/src/components/ActionFooter/ActionFooter.tsx +210 -92
- package/src/components/Avatar/Avatar.tsx +26 -0
- package/src/components/Badge/Badge.tsx +27 -0
- package/src/components/Button/Button.tsx +40 -0
- package/src/components/Checkbox/Checkbox.tsx +22 -9
- package/src/components/DropdownInput/DropdownInput.tsx +67 -39
- package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +237 -0
- package/src/components/FormField/FormField.tsx +19 -3
- package/src/components/FullscreenModal/FullscreenModal.tsx +414 -0
- package/src/components/IconButton/IconButton.tsx +27 -0
- package/src/components/Image/Image.tsx +25 -0
- package/src/components/ListItem/ListItem.tsx +21 -10
- package/src/components/LottiePlayer/LottiePlayer.tsx +145 -0
- package/src/components/LottiePlayer/LottiePlayer.web.tsx +94 -0
- package/src/components/LottiePlayer/loadNativeLottieView.tsx +87 -0
- package/src/components/LottiePlayer/loadWebLottieView.tsx +64 -0
- package/src/components/MessageField/MessageField.tsx +543 -0
- package/src/components/NavArrow/NavArrow.tsx +81 -17
- package/src/components/PageHero/PageHero.tsx +61 -4
- package/src/components/RechargeCard/RechargeCard.tsx +32 -24
- package/src/components/Stepper/Step.tsx +52 -51
- package/src/components/Stepper/StepLabel.tsx +46 -9
- package/src/components/Stepper/Stepper.tsx +20 -15
- package/src/components/SuggestiveSearch/SuggestiveSearch.tsx +756 -0
- package/src/components/Text/Text.tsx +54 -0
- package/src/components/TextInput/TextInput.tsx +14 -1
- package/src/components/Title/Title.tsx +13 -2
- package/src/components/index.ts +8 -3
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/icons/Icon.tsx +17 -0
- package/src/icons/registry.ts +1 -1
- package/src/index.ts +1 -0
- package/src/skeleton/Skeleton.tsx +298 -0
- package/src/skeleton/SkeletonGroup.tsx +193 -0
- package/src/skeleton/index.ts +10 -0
- package/src/skeleton/shimmer-tokens.ts +221 -0
- package/src/skeleton/useReducedMotion.ts +72 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import React, { useCallback, useMemo, useState } from 'react'
|
|
1
|
+
import React, { useCallback, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
5
|
+
Pressable,
|
|
5
6
|
TextInput as RNTextInput,
|
|
6
7
|
type StyleProp,
|
|
7
8
|
type TextInputProps as RNTextInputProps,
|
|
@@ -346,6 +347,16 @@ function FormField({
|
|
|
346
347
|
const [isFocused, setIsFocused] = useState(false)
|
|
347
348
|
const interactive = !isDisabled && !isReadOnly
|
|
348
349
|
|
|
350
|
+
// Ref to the native input so tapping anywhere in the input row (padding,
|
|
351
|
+
// leading/trailing gutters) focuses it on the FIRST tap — fixing the Android
|
|
352
|
+
// "two taps to open the keyboard" issue caused by the row intercepting the
|
|
353
|
+
// initial touch.
|
|
354
|
+
const inputRef = useRef<RNTextInput>(null)
|
|
355
|
+
const focusInput = useCallback(() => {
|
|
356
|
+
if (!interactive) return
|
|
357
|
+
inputRef.current?.focus()
|
|
358
|
+
}, [interactive])
|
|
359
|
+
|
|
349
360
|
// FormField States cascade — error > read only/disabled > active (focused) > idle.
|
|
350
361
|
// Disabled maps to "Read Only" since there is no dedicated disabled mode and
|
|
351
362
|
// the visual treatment is closest. This is only the DEFAULT — an explicit
|
|
@@ -541,7 +552,11 @@ function FormField({
|
|
|
541
552
|
</View>
|
|
542
553
|
)}
|
|
543
554
|
|
|
544
|
-
<
|
|
555
|
+
<Pressable
|
|
556
|
+
style={[inputRowStyle, inputStyle]}
|
|
557
|
+
onPress={focusInput}
|
|
558
|
+
accessible={false}
|
|
559
|
+
>
|
|
545
560
|
{processedLeading != null && (
|
|
546
561
|
<View
|
|
547
562
|
accessibilityElementsHidden
|
|
@@ -551,6 +566,7 @@ function FormField({
|
|
|
551
566
|
</View>
|
|
552
567
|
)}
|
|
553
568
|
<RNTextInput
|
|
569
|
+
ref={inputRef}
|
|
554
570
|
style={[inputTextStyles, inputTextStyle]}
|
|
555
571
|
value={value ?? ''}
|
|
556
572
|
onChangeText={handleChangeText}
|
|
@@ -578,7 +594,7 @@ function FormField({
|
|
|
578
594
|
{processedTrailing}
|
|
579
595
|
</View>
|
|
580
596
|
)}
|
|
581
|
-
</
|
|
597
|
+
</Pressable>
|
|
582
598
|
|
|
583
599
|
{supportLabel != null && supportLabel !== '' && (
|
|
584
600
|
<SupportText
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import React, { useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
ScrollView,
|
|
6
|
+
type StyleProp,
|
|
7
|
+
type ViewStyle,
|
|
8
|
+
type TextStyle,
|
|
9
|
+
} from 'react-native'
|
|
10
|
+
import Animated, {
|
|
11
|
+
Extrapolation,
|
|
12
|
+
interpolate,
|
|
13
|
+
useAnimatedScrollHandler,
|
|
14
|
+
useAnimatedStyle,
|
|
15
|
+
useSharedValue,
|
|
16
|
+
} from 'react-native-reanimated'
|
|
17
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
18
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
19
|
+
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
|
|
20
|
+
import Button from '../Button/Button'
|
|
21
|
+
import Disclaimer from '../Disclaimer/Disclaimer'
|
|
22
|
+
import IconButton from '../IconButton/IconButton'
|
|
23
|
+
import ActionFooter from '../ActionFooter/ActionFooter'
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Forced modes
|
|
27
|
+
//
|
|
28
|
+
// `FullscreenModal` always themes itself with the `context5: 'Fullscreen Modal'`
|
|
29
|
+
// collection mode. This is what flips the section / list-item / hero text
|
|
30
|
+
// tokens to their white-on-dark values (see the Figma "Fullscreen Modal"
|
|
31
|
+
// context). It is intentionally NON-overridable: callers can pass any other
|
|
32
|
+
// modes (Color Mode, AppearanceBrand, …) but never context5. The frozen
|
|
33
|
+
// object keeps its identity stable so the token resolver's per-modes cache
|
|
34
|
+
// stays hot, and so `cloneChildrenWithModes` can use it as the
|
|
35
|
+
// always-wins `forcedModes` argument.
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
const FULLSCREEN_MODAL_FORCED_MODES = Object.freeze({ context5: 'Fullscreen Modal' })
|
|
38
|
+
|
|
39
|
+
// Reanimated-driven ScrollView so the parallax handler runs on the UI thread.
|
|
40
|
+
// Module scope so the wrapped component identity is stable across renders.
|
|
41
|
+
const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView)
|
|
42
|
+
|
|
43
|
+
// Parallax tuning. The hero collapses by HEIGHT only as the user scrolls up —
|
|
44
|
+
// its full width is preserved and the media keeps a fixed aspect ratio (it is
|
|
45
|
+
// cropped, never scaled or squished, like a `cover` background). When no
|
|
46
|
+
// explicit `heroMinHeight` is given, the hero collapses to this fraction of
|
|
47
|
+
// its resting height.
|
|
48
|
+
const HERO_MIN_HEIGHT_RATIO = 0.45
|
|
49
|
+
|
|
50
|
+
export type FullscreenModalProps = {
|
|
51
|
+
/** Small eyebrow line above the headline. */
|
|
52
|
+
eyebrow?: string
|
|
53
|
+
/** Large hero headline. */
|
|
54
|
+
headline?: string
|
|
55
|
+
/** Supporting paragraph shown below the headline. */
|
|
56
|
+
supportingText?: string
|
|
57
|
+
/** Secondary line below the supporting paragraph (e.g. a price / timeline). */
|
|
58
|
+
priceText?: string
|
|
59
|
+
/**
|
|
60
|
+
* Media rendered full-bleed behind the hero text and driven by the parallax
|
|
61
|
+
* scroll effect. Bring any renderer — most commonly a `LottiePlayer`, but an
|
|
62
|
+
* `Image`, `Video`, or `SvgXml` works too. Size it to fill the hero box
|
|
63
|
+
* (`heroHeight` tall, full modal width) and let it `cover` so that as the
|
|
64
|
+
* hero collapses in height the art is cropped, never distorted. `modes` are
|
|
65
|
+
* cascaded into it.
|
|
66
|
+
*/
|
|
67
|
+
heroMedia?: React.ReactNode
|
|
68
|
+
/** Resting height of the hero region. Defaults to 420. */
|
|
69
|
+
heroHeight?: number
|
|
70
|
+
/**
|
|
71
|
+
* Collapsed height the hero shrinks to at full scroll. Defaults to
|
|
72
|
+
* `heroHeight * 0.45`. Only the height changes — the width is always full.
|
|
73
|
+
*/
|
|
74
|
+
heroMinHeight?: number
|
|
75
|
+
/** Enable the scroll-driven hero collapse. Defaults to true. */
|
|
76
|
+
parallax?: boolean
|
|
77
|
+
/** Whether to render the floating close button (top-right). Defaults to true. */
|
|
78
|
+
showClose?: boolean
|
|
79
|
+
/** Press handler for the close button. */
|
|
80
|
+
onClose?: () => void
|
|
81
|
+
/** Accessibility label for the close button. */
|
|
82
|
+
closeAccessibilityLabel?: string
|
|
83
|
+
/**
|
|
84
|
+
* Fully custom footer content rendered inside the sticky `ActionFooter`.
|
|
85
|
+
* When provided, `primaryActionLabel` / `disclaimer` are ignored.
|
|
86
|
+
*/
|
|
87
|
+
footer?: React.ReactNode
|
|
88
|
+
/** Label for the default primary action button in the footer. */
|
|
89
|
+
primaryActionLabel?: string
|
|
90
|
+
/** Press handler for the default primary action button. */
|
|
91
|
+
onPrimaryAction?: () => void
|
|
92
|
+
/** Disclaimer text shown below the default primary action button. */
|
|
93
|
+
disclaimer?: string
|
|
94
|
+
/** Solid backdrop color for the scrollable body. Defaults to a near-black. */
|
|
95
|
+
backgroundColor?: string
|
|
96
|
+
/** Body content (typically `Section`s). `modes` are cascaded automatically. */
|
|
97
|
+
children?: React.ReactNode
|
|
98
|
+
/** Mode configuration. `context5` is always forced to `'Fullscreen Modal'`. */
|
|
99
|
+
modes?: Record<string, any>
|
|
100
|
+
/** Style overrides for the outer container. */
|
|
101
|
+
style?: StyleProp<ViewStyle>
|
|
102
|
+
/** Style overrides for the scroll body wrapper (the dark content area). */
|
|
103
|
+
contentContainerStyle?: StyleProp<ViewStyle>
|
|
104
|
+
testID?: string
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Hero text — the eyebrow / headline / supporting / price block. Built inline
|
|
109
|
+
// (rather than reusing <PageHero>) so we can render BOTH a supporting
|
|
110
|
+
// paragraph AND a price line with the exact PageHero token gaps, and overlay
|
|
111
|
+
// it on the parallax media without PageHero's media/button scaffolding.
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
type HeroTextProps = {
|
|
114
|
+
eyebrow?: string
|
|
115
|
+
headline?: string
|
|
116
|
+
supportingText?: string
|
|
117
|
+
priceText?: string
|
|
118
|
+
modes: Record<string, any>
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function HeroText({ eyebrow, headline, supportingText, priceText, modes }: HeroTextProps) {
|
|
122
|
+
const styles = useMemo(() => {
|
|
123
|
+
const gap = Number(getVariableByName('PageHero/gap', modes)) || 16
|
|
124
|
+
const textWrapGap = Number(getVariableByName('PageHero/textWrap/gap', modes)) || 8
|
|
125
|
+
|
|
126
|
+
const eyebrowStyle: TextStyle = {
|
|
127
|
+
color: (getVariableByName('PageHero/eyebrow/color', modes) as string) || '#ffffff',
|
|
128
|
+
fontFamily: (getVariableByName('PageHero/eyebrow/fontFamily', modes) as string) || 'System',
|
|
129
|
+
fontSize: Number(getVariableByName('PageHero/eyebrow/fontSize', modes)) || 18,
|
|
130
|
+
fontWeight: String(getVariableByName('PageHero/eyebrow/fontWeight', modes) || 700) as TextStyle['fontWeight'],
|
|
131
|
+
lineHeight: Number(getVariableByName('PageHero/eyebrow/lineHeight', modes)) || 20,
|
|
132
|
+
textAlign: 'center',
|
|
133
|
+
}
|
|
134
|
+
const headlineStyle: TextStyle = {
|
|
135
|
+
color: (getVariableByName('PageHero/headline/color', modes) as string) || '#ffffff',
|
|
136
|
+
fontFamily: (getVariableByName('PageHero/headline/fontFamily', modes) as string) || 'System',
|
|
137
|
+
fontSize: Number(getVariableByName('PageHero/headline/fontSize', modes)) || 29,
|
|
138
|
+
fontWeight: String(getVariableByName('PageHero/headline/fontWeight', modes) || 900) as TextStyle['fontWeight'],
|
|
139
|
+
lineHeight: Number(getVariableByName('PageHero/headline/lineHeight', modes)) || 29,
|
|
140
|
+
textAlign: 'center',
|
|
141
|
+
width: '100%',
|
|
142
|
+
}
|
|
143
|
+
const supportingTextStyle: TextStyle = {
|
|
144
|
+
color: (getVariableByName('PageHero/supportingText/color', modes) as string) || '#ffffff',
|
|
145
|
+
fontFamily: (getVariableByName('PageHero/supportingText/fontFamily', modes) as string) || 'System',
|
|
146
|
+
fontSize: Number(getVariableByName('PageHero/supportingText/fontSize', modes)) || 12,
|
|
147
|
+
fontWeight: String(getVariableByName('PageHero/supportingText/fontWeight', modes) || 500) as TextStyle['fontWeight'],
|
|
148
|
+
lineHeight: Number(getVariableByName('PageHero/supportingText/lineHeight', modes)) || 16,
|
|
149
|
+
textAlign: 'center',
|
|
150
|
+
}
|
|
151
|
+
const priceTextStyle: TextStyle = {
|
|
152
|
+
color: (getVariableByName('PageHero/body/color', modes) as string) || '#ffffff',
|
|
153
|
+
fontFamily: (getVariableByName('PageHero/body/fontFamily', modes) as string) || 'System',
|
|
154
|
+
fontSize: Number(getVariableByName('PageHero/body/fontSize', modes)) || 12,
|
|
155
|
+
fontWeight: String(getVariableByName('PageHero/body/fontWeight', modes) || 500) as TextStyle['fontWeight'],
|
|
156
|
+
lineHeight: Number(getVariableByName('PageHero/body/lineHeight', modes)) || 16,
|
|
157
|
+
textAlign: 'center',
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
container: { width: '100%', alignItems: 'center', gap } as ViewStyle,
|
|
162
|
+
textWrap: { width: '100%', alignItems: 'center', gap: textWrapGap } as ViewStyle,
|
|
163
|
+
eyebrowStyle,
|
|
164
|
+
headlineStyle,
|
|
165
|
+
supportingTextStyle,
|
|
166
|
+
priceTextStyle,
|
|
167
|
+
}
|
|
168
|
+
}, [modes])
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<View style={styles.container}>
|
|
172
|
+
<View style={styles.textWrap}>
|
|
173
|
+
{eyebrow ? <Text style={styles.eyebrowStyle}>{eyebrow}</Text> : null}
|
|
174
|
+
{headline ? <Text style={styles.headlineStyle}>{headline}</Text> : null}
|
|
175
|
+
</View>
|
|
176
|
+
{supportingText ? <Text style={styles.supportingTextStyle}>{supportingText}</Text> : null}
|
|
177
|
+
{priceText ? <Text style={styles.priceTextStyle}>{priceText}</Text> : null}
|
|
178
|
+
</View>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* FullscreenModal — a full-screen takeover surface with a parallax media hero,
|
|
184
|
+
* a scrollable body, a floating close button, and a sticky `ActionFooter`.
|
|
185
|
+
*
|
|
186
|
+
* The component always themes itself with `context5: 'Fullscreen Modal'`
|
|
187
|
+
* (non-overridable) so every nested component (Section, ListItem, Button,
|
|
188
|
+
* Disclaimer, …) resolves the white-on-dark "fullscreen modal" token values.
|
|
189
|
+
* That mode is cascaded into `children`, the footer, and the hero text via
|
|
190
|
+
* `cloneChildrenWithModes` / the merged `modes` object.
|
|
191
|
+
*
|
|
192
|
+
* ### Parallax
|
|
193
|
+
* As the user scrolls up, the hero collapses by **height only** (from
|
|
194
|
+
* `heroHeight` to `heroMinHeight`) — its **full width is always preserved**.
|
|
195
|
+
* The `heroMedia` is pinned to the top at a fixed size and `cover`-cropped by
|
|
196
|
+
* the collapsing clip, so it keeps a perfect aspect ratio the whole time
|
|
197
|
+
* (never scaled or squished). Because it collapses slower than the content
|
|
198
|
+
* scrolls, the media lags behind for the parallax depth cue. Disable with
|
|
199
|
+
* `parallax={false}`.
|
|
200
|
+
*
|
|
201
|
+
* @component
|
|
202
|
+
* @example
|
|
203
|
+
* ```tsx
|
|
204
|
+
* <FullscreenModal
|
|
205
|
+
* eyebrow="Upgrade to JioFinance+"
|
|
206
|
+
* headline="Get more from your money."
|
|
207
|
+
* supportingText="JioFinance+ is your upgraded financial experience…"
|
|
208
|
+
* priceText="₹999/year · ₹0 until 2027"
|
|
209
|
+
* heroMedia={<LottiePlayer source={hero} size={{ width: 360, height: 420 }} />}
|
|
210
|
+
* primaryActionLabel="Upgrade for free"
|
|
211
|
+
* disclaimer="By upgrading, we'll check your eligibility with Experian."
|
|
212
|
+
* onPrimaryAction={() => upgrade()}
|
|
213
|
+
* onClose={() => navigation.goBack()}
|
|
214
|
+
* >
|
|
215
|
+
* <Section title="Key Benefits" slotDirection="column" slot={…} />
|
|
216
|
+
* <Section title="Compare plans" slotDirection="column" slot={…} />
|
|
217
|
+
* </FullscreenModal>
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
function FullscreenModal({
|
|
221
|
+
eyebrow = 'Upgrade to JioFinance+',
|
|
222
|
+
headline = 'Get more from your money.',
|
|
223
|
+
supportingText = 'JioFinance+ is your upgraded financial experience, designed to work harder in the background so your money works smarter in real life.',
|
|
224
|
+
priceText = '₹999/year · ₹0 until 2027',
|
|
225
|
+
heroMedia,
|
|
226
|
+
heroHeight = 420,
|
|
227
|
+
heroMinHeight,
|
|
228
|
+
parallax = true,
|
|
229
|
+
showClose = true,
|
|
230
|
+
onClose,
|
|
231
|
+
closeAccessibilityLabel = 'Close',
|
|
232
|
+
footer,
|
|
233
|
+
primaryActionLabel = 'Upgrade for free',
|
|
234
|
+
onPrimaryAction,
|
|
235
|
+
disclaimer = "By upgrading, we'll check your eligibility with Experian.",
|
|
236
|
+
backgroundColor = '#0f0d0a',
|
|
237
|
+
children,
|
|
238
|
+
modes: propModes = EMPTY_MODES,
|
|
239
|
+
style,
|
|
240
|
+
contentContainerStyle,
|
|
241
|
+
testID,
|
|
242
|
+
}: FullscreenModalProps) {
|
|
243
|
+
const { modes: globalModes } = useTokens()
|
|
244
|
+
|
|
245
|
+
// context5 is appended last so it always wins, regardless of what the
|
|
246
|
+
// caller (or the global theme) passes.
|
|
247
|
+
const modes = useMemo(
|
|
248
|
+
() => ({ ...globalModes, ...propModes, ...FULLSCREEN_MODAL_FORCED_MODES }),
|
|
249
|
+
[globalModes, propModes]
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
const rootGap = Number(getVariableByName('fullScreenModal/gap', modes)) || 16
|
|
253
|
+
|
|
254
|
+
const minHeight = heroMinHeight ?? Math.round(heroHeight * HERO_MIN_HEIGHT_RATIO)
|
|
255
|
+
|
|
256
|
+
const scrollY = useSharedValue(0)
|
|
257
|
+
const onScroll = useAnimatedScrollHandler((event) => {
|
|
258
|
+
scrollY.value = event.contentOffset.y
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// Collapse the hero by HEIGHT only as the user scrolls up. The clip's width
|
|
262
|
+
// never changes and the media inside is pinned full-size at the top, so the
|
|
263
|
+
// art is cropped (cover) rather than scaled or narrowed — it keeps a perfect
|
|
264
|
+
// aspect ratio the whole time. Pull-down (negative offset) is clamped, so the
|
|
265
|
+
// hero never grows past its resting height.
|
|
266
|
+
const heroAnimatedStyle = useAnimatedStyle(() => {
|
|
267
|
+
const height = interpolate(
|
|
268
|
+
scrollY.value,
|
|
269
|
+
[0, heroHeight],
|
|
270
|
+
[heroHeight, minHeight],
|
|
271
|
+
Extrapolation.CLAMP
|
|
272
|
+
)
|
|
273
|
+
return { height }
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
const processedHeroMedia = useMemo(
|
|
277
|
+
() =>
|
|
278
|
+
heroMedia ? cloneChildrenWithModes(heroMedia, modes, FULLSCREEN_MODAL_FORCED_MODES) : null,
|
|
279
|
+
[heroMedia, modes]
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
const processedChildren = useMemo(
|
|
283
|
+
() =>
|
|
284
|
+
children ? cloneChildrenWithModes(children, modes, FULLSCREEN_MODAL_FORCED_MODES) : null,
|
|
285
|
+
[children, modes]
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
// The clip is full-width and top-pinned; its height is what animates. Width
|
|
289
|
+
// is intentionally never animated.
|
|
290
|
+
const heroClipBaseStyle = useMemo<ViewStyle>(
|
|
291
|
+
() => ({
|
|
292
|
+
position: 'absolute',
|
|
293
|
+
top: 0,
|
|
294
|
+
left: 0,
|
|
295
|
+
right: 0,
|
|
296
|
+
overflow: 'hidden',
|
|
297
|
+
}),
|
|
298
|
+
[]
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
// The media sits at a fixed full-size box pinned to the top of the clip, so
|
|
302
|
+
// the collapsing clip crops it from the bottom (cover) instead of resizing
|
|
303
|
+
// it. Full width, fixed height — a perfect, constant aspect ratio.
|
|
304
|
+
const heroMediaWrapStyle = useMemo<ViewStyle>(
|
|
305
|
+
() => ({
|
|
306
|
+
position: 'absolute',
|
|
307
|
+
top: 0,
|
|
308
|
+
left: 0,
|
|
309
|
+
right: 0,
|
|
310
|
+
height: heroHeight,
|
|
311
|
+
alignItems: 'stretch',
|
|
312
|
+
}),
|
|
313
|
+
[heroHeight]
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
const heroTextRegionStyle = useMemo<ViewStyle>(
|
|
317
|
+
() => ({
|
|
318
|
+
height: heroHeight,
|
|
319
|
+
justifyContent: 'flex-end',
|
|
320
|
+
paddingHorizontal: 16,
|
|
321
|
+
paddingBottom: 16,
|
|
322
|
+
}),
|
|
323
|
+
[heroHeight]
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
const bodyStyle = useMemo<StyleProp<ViewStyle>>(
|
|
327
|
+
() => [
|
|
328
|
+
{
|
|
329
|
+
backgroundColor,
|
|
330
|
+
gap: rootGap,
|
|
331
|
+
paddingTop: rootGap,
|
|
332
|
+
paddingBottom: 24,
|
|
333
|
+
},
|
|
334
|
+
contentContainerStyle,
|
|
335
|
+
],
|
|
336
|
+
[backgroundColor, rootGap, contentContainerStyle]
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
const heroClip = (
|
|
340
|
+
<Animated.View
|
|
341
|
+
style={[heroClipBaseStyle, parallax ? heroAnimatedStyle : { height: heroHeight }]}
|
|
342
|
+
pointerEvents="none"
|
|
343
|
+
>
|
|
344
|
+
<View style={heroMediaWrapStyle}>{processedHeroMedia}</View>
|
|
345
|
+
</Animated.View>
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
// Footer: a fully custom node, or the default Button + Disclaimer column.
|
|
349
|
+
let footerContent: React.ReactNode = null
|
|
350
|
+
if (footer) {
|
|
351
|
+
footerContent = footer
|
|
352
|
+
} else if (primaryActionLabel) {
|
|
353
|
+
footerContent = (
|
|
354
|
+
<View style={footerColumnStyle}>
|
|
355
|
+
<Button
|
|
356
|
+
label={primaryActionLabel}
|
|
357
|
+
modes={modes}
|
|
358
|
+
style={fullWidthStyle}
|
|
359
|
+
{...(onPrimaryAction ? { onPress: onPrimaryAction } : {})}
|
|
360
|
+
/>
|
|
361
|
+
{disclaimer ? <Disclaimer disclaimer={disclaimer} modes={modes} /> : null}
|
|
362
|
+
</View>
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return (
|
|
367
|
+
<View style={[rootStyle, { backgroundColor }, style]} testID={testID}>
|
|
368
|
+
{processedHeroMedia ? heroClip : null}
|
|
369
|
+
|
|
370
|
+
<AnimatedScrollView
|
|
371
|
+
style={scrollViewStyle}
|
|
372
|
+
contentContainerStyle={scrollContentStyle}
|
|
373
|
+
showsVerticalScrollIndicator={false}
|
|
374
|
+
onScroll={onScroll}
|
|
375
|
+
scrollEventThrottle={16}
|
|
376
|
+
>
|
|
377
|
+
<View style={heroTextRegionStyle}>
|
|
378
|
+
<HeroText
|
|
379
|
+
eyebrow={eyebrow}
|
|
380
|
+
headline={headline}
|
|
381
|
+
supportingText={supportingText}
|
|
382
|
+
priceText={priceText}
|
|
383
|
+
modes={modes}
|
|
384
|
+
/>
|
|
385
|
+
</View>
|
|
386
|
+
<View style={bodyStyle}>{processedChildren}</View>
|
|
387
|
+
</AnimatedScrollView>
|
|
388
|
+
|
|
389
|
+
{footerContent ? (
|
|
390
|
+
<ActionFooter modes={modes}>{footerContent}</ActionFooter>
|
|
391
|
+
) : null}
|
|
392
|
+
|
|
393
|
+
{showClose ? (
|
|
394
|
+
<IconButton
|
|
395
|
+
iconName="ic_close"
|
|
396
|
+
modes={modes}
|
|
397
|
+
accessibilityLabel={closeAccessibilityLabel}
|
|
398
|
+
style={closeButtonStyle}
|
|
399
|
+
{...(onClose ? { onPress: onClose } : {})}
|
|
400
|
+
/>
|
|
401
|
+
) : null}
|
|
402
|
+
</View>
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Module-scope style constants — never re-allocated per render.
|
|
407
|
+
const rootStyle: ViewStyle = { flex: 1, width: '100%', position: 'relative' }
|
|
408
|
+
const scrollViewStyle: ViewStyle = { flex: 1 }
|
|
409
|
+
const scrollContentStyle: ViewStyle = { flexGrow: 1 }
|
|
410
|
+
const footerColumnStyle: ViewStyle = { width: '100%', gap: 8 }
|
|
411
|
+
const fullWidthStyle: ViewStyle = { width: '100%' }
|
|
412
|
+
const closeButtonStyle: ViewStyle = { position: 'absolute', top: 12, right: 12 }
|
|
413
|
+
|
|
414
|
+
export default FullscreenModal
|
|
@@ -12,6 +12,8 @@ import Icon from '../../icons/Icon'
|
|
|
12
12
|
import { usePressableWebSupport, type SafePressableProps, type WebAccessibilityProps } from '../../utils/web-platform-utils'
|
|
13
13
|
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
14
14
|
import type { UnifiedSource } from '../../utils/MediaSource'
|
|
15
|
+
import Skeleton from '../../skeleton/Skeleton'
|
|
16
|
+
import { useSkeleton } from '../../skeleton/SkeletonGroup'
|
|
15
17
|
|
|
16
18
|
type IconButtonProps = SafePressableProps & {
|
|
17
19
|
/** Built-in icon name from the registry (default state). */
|
|
@@ -65,6 +67,12 @@ type IconButtonProps = SafePressableProps & {
|
|
|
65
67
|
* Web-specific accessibility props (only used on web platform)
|
|
66
68
|
*/
|
|
67
69
|
webAccessibilityProps?: WebAccessibilityProps;
|
|
70
|
+
/**
|
|
71
|
+
* Explicit per-instance loading override. When `true`, renders a
|
|
72
|
+
* same-size pill-shaped skeleton instead of the button. Defaults to
|
|
73
|
+
* inheriting from the surrounding `<SkeletonGroup>`.
|
|
74
|
+
*/
|
|
75
|
+
loading?: boolean;
|
|
68
76
|
};
|
|
69
77
|
|
|
70
78
|
// ---------------------------------------------------------------------------
|
|
@@ -155,6 +163,7 @@ function IconButton({
|
|
|
155
163
|
inactiveIcon,
|
|
156
164
|
inactiveSource,
|
|
157
165
|
isActive = false,
|
|
166
|
+
loading,
|
|
158
167
|
...rest
|
|
159
168
|
}: IconButtonProps) {
|
|
160
169
|
// Merge explicit props with modes for token resolution. Memoize the merged
|
|
@@ -174,6 +183,11 @@ function IconButton({
|
|
|
174
183
|
[componentModes, disabled]
|
|
175
184
|
)
|
|
176
185
|
|
|
186
|
+
// Hook called unconditionally — short-circuit below comes AFTER all hooks
|
|
187
|
+
// to keep React's hook order stable across renders.
|
|
188
|
+
const { active: groupActive } = useSkeleton()
|
|
189
|
+
const isLoading = loading ?? groupActive
|
|
190
|
+
|
|
177
191
|
const [isFocused, setIsFocused] = useState(false)
|
|
178
192
|
const [isHovered, setIsHovered] = useState(false)
|
|
179
193
|
|
|
@@ -271,6 +285,19 @@ function IconButton({
|
|
|
271
285
|
[tokens.baseContainerStyle, style, isHovered, isFocused, disabled]
|
|
272
286
|
)
|
|
273
287
|
|
|
288
|
+
if (isLoading) {
|
|
289
|
+
const size = tokens.baseContainerStyle.width as number
|
|
290
|
+
return (
|
|
291
|
+
<Skeleton
|
|
292
|
+
kind="other"
|
|
293
|
+
width={size}
|
|
294
|
+
height={size}
|
|
295
|
+
style={style as any}
|
|
296
|
+
modes={componentModes}
|
|
297
|
+
/>
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
274
301
|
return (
|
|
275
302
|
<Pressable
|
|
276
303
|
accessibilityRole="button"
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
type ViewStyle,
|
|
9
9
|
type ImageResizeMode,
|
|
10
10
|
} from 'react-native'
|
|
11
|
+
import Skeleton from '../../skeleton/Skeleton'
|
|
12
|
+
import { useSkeleton } from '../../skeleton/SkeletonGroup'
|
|
11
13
|
|
|
12
14
|
export type ImageProps = {
|
|
13
15
|
/**
|
|
@@ -52,6 +54,13 @@ export type ImageProps = {
|
|
|
52
54
|
| 'no'
|
|
53
55
|
| 'no-hide-descendants'
|
|
54
56
|
| undefined
|
|
57
|
+
/**
|
|
58
|
+
* Explicit per-instance loading override. When `true`, the image renders as
|
|
59
|
+
* a skeleton placeholder at the same box size; when `false`, the
|
|
60
|
+
* surrounding `<SkeletonGroup>` is ignored. Defaults to inheriting from
|
|
61
|
+
* the group.
|
|
62
|
+
*/
|
|
63
|
+
loading?: boolean | undefined
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
function normalizeSource(
|
|
@@ -94,6 +103,7 @@ function Image({
|
|
|
94
103
|
accessibilityLabel,
|
|
95
104
|
accessibilityElementsHidden,
|
|
96
105
|
importantForAccessibility,
|
|
106
|
+
loading,
|
|
97
107
|
}: ImageProps) {
|
|
98
108
|
const source = useMemo(() => normalizeSource(imageSource), [imageSource])
|
|
99
109
|
|
|
@@ -112,6 +122,21 @@ function Image({
|
|
|
112
122
|
return s
|
|
113
123
|
}, [ratio, width, height, borderRadius])
|
|
114
124
|
|
|
125
|
+
const { active: groupActive } = useSkeleton()
|
|
126
|
+
const isLoading = loading ?? groupActive
|
|
127
|
+
if (isLoading) {
|
|
128
|
+
// Match the loaded image's exact box. If height is unknown but a ratio
|
|
129
|
+
// is set, the skeleton uses `aspectRatio` the same way the loaded image
|
|
130
|
+
// would, so layout never jumps when the load resolves.
|
|
131
|
+
const skeletonStyle: ViewStyle = {
|
|
132
|
+
width: (width ?? '100%') as ViewStyle['width'],
|
|
133
|
+
...(height != null
|
|
134
|
+
? { height: height as number }
|
|
135
|
+
: { aspectRatio: ratio }),
|
|
136
|
+
}
|
|
137
|
+
return <Skeleton kind="image" style={skeletonStyle} />
|
|
138
|
+
}
|
|
139
|
+
|
|
115
140
|
if (!source) {
|
|
116
141
|
return <View style={[layoutStyle, style as StyleProp<ViewStyle>]} />
|
|
117
142
|
}
|
|
@@ -72,8 +72,19 @@ interface ListItemTokens {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
function resolveListItemTokens(modes: Record<string, any>): ListItemTokens {
|
|
75
|
+
// Modes used to cascade into slot children (leading / supportSlot / endSlot).
|
|
76
|
+
// We do NOT inject an `AppearanceBrand` default here: slot content such as
|
|
77
|
+
// Buttons or Badges carry their own intended appearance, so forcing one onto
|
|
78
|
+
// them would be surprising.
|
|
75
79
|
const resolvedModes = { ...modes, Context: 'ListItem' }
|
|
76
80
|
|
|
81
|
+
// Modes used to resolve the ListItem's OWN title + support text. Within this
|
|
82
|
+
// component, `AppearanceBrand` only affects `listItem/title/color` and
|
|
83
|
+
// `listItem/supportText/color`, so the text defaults to the "Neutral"
|
|
84
|
+
// appearance (in both Vertical and Horizontal layouts). A caller-supplied
|
|
85
|
+
// `AppearanceBrand` still wins; `Context` is always forced to 'ListItem'.
|
|
86
|
+
const textModes = { AppearanceBrand: 'Neutral', ...modes, Context: 'ListItem' }
|
|
87
|
+
|
|
77
88
|
const gap = (getVariableByName('listItem/gap', resolvedModes) ?? 8) as number
|
|
78
89
|
const paddingTop = getVariableByName('listItem/padding/top', resolvedModes) ?? 0
|
|
79
90
|
const paddingBottom = getVariableByName('listItem/padding/bottom', resolvedModes) ?? 0
|
|
@@ -81,19 +92,19 @@ function resolveListItemTokens(modes: Record<string, any>): ListItemTokens {
|
|
|
81
92
|
const paddingRight = getVariableByName('listItem/padding/right', resolvedModes) ?? 0
|
|
82
93
|
const textWrapGap = (getVariableByName('listItem/text wrap', resolvedModes) ?? 0) as number
|
|
83
94
|
|
|
84
|
-
const titleColor = getVariableByName('listItem/title/color',
|
|
85
|
-
const titleFontSize = getVariableByName('listItem/title/fontSize',
|
|
86
|
-
const titleLineHeight = getVariableByName('listItem/title/lineHeight',
|
|
87
|
-
const titleFontFamily = getVariableByName('listItem/title/fontFamily',
|
|
88
|
-
const titleFontWeightRaw = getVariableByName('listItem/title/fontWeight',
|
|
95
|
+
const titleColor = getVariableByName('listItem/title/color', textModes) || '#0f0d0a'
|
|
96
|
+
const titleFontSize = getVariableByName('listItem/title/fontSize', textModes) || 14
|
|
97
|
+
const titleLineHeight = getVariableByName('listItem/title/lineHeight', textModes) || 16
|
|
98
|
+
const titleFontFamily = getVariableByName('listItem/title/fontFamily', textModes) || 'System'
|
|
99
|
+
const titleFontWeightRaw = getVariableByName('listItem/title/fontWeight', textModes) || 700
|
|
89
100
|
const titleFontWeight =
|
|
90
101
|
typeof titleFontWeightRaw === 'number' ? titleFontWeightRaw.toString() : titleFontWeightRaw
|
|
91
102
|
|
|
92
|
-
const supportColor = getVariableByName('listItem/supportText/color',
|
|
93
|
-
const supportFontSize = getVariableByName('listItem/supportText/fontSize',
|
|
94
|
-
const supportLineHeight = getVariableByName('listItem/supportText/lineHeight',
|
|
95
|
-
const supportFontFamily = getVariableByName('listItem/supportText/fontFamily',
|
|
96
|
-
const supportFontWeightRaw = getVariableByName('listItem/supportText/fontWeight',
|
|
103
|
+
const supportColor = getVariableByName('listItem/supportText/color', textModes) || '#1f1a14'
|
|
104
|
+
const supportFontSize = getVariableByName('listItem/supportText/fontSize', textModes) || 12
|
|
105
|
+
const supportLineHeight = getVariableByName('listItem/supportText/lineHeight', textModes) || 14
|
|
106
|
+
const supportFontFamily = getVariableByName('listItem/supportText/fontFamily', textModes) || 'System'
|
|
107
|
+
const supportFontWeightRaw = getVariableByName('listItem/supportText/fontWeight', textModes) || 500
|
|
97
108
|
const supportFontWeight =
|
|
98
109
|
typeof supportFontWeightRaw === 'number' ? supportFontWeightRaw.toString() : supportFontWeightRaw
|
|
99
110
|
|