jfs-components 0.0.77 → 0.0.79
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 +28 -0
- package/lib/commonjs/components/Accordion/Accordion.js +55 -55
- package/lib/commonjs/components/ActionFooter/ActionFooter.js +48 -2
- package/lib/commonjs/components/Attached/Attached.js +144 -0
- package/lib/commonjs/components/Card/Card.js +25 -2
- 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 +353 -0
- package/lib/commonjs/components/ListItem/ListItem.js +46 -24
- package/lib/commonjs/components/MessageField/MessageField.js +318 -0
- package/lib/commonjs/components/NavArrow/NavArrow.js +58 -17
- package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +328 -0
- package/lib/commonjs/components/Slot/Slot.js +73 -0
- 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/TextInput/TextInput.js +16 -1
- package/lib/commonjs/components/Title/Title.js +10 -2
- package/lib/commonjs/components/index.js +49 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/Accordion/Accordion.js +56 -56
- package/lib/module/components/ActionFooter/ActionFooter.js +50 -4
- package/lib/module/components/Attached/Attached.js +139 -0
- package/lib/module/components/Card/Card.js +25 -2
- 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 +348 -0
- package/lib/module/components/ListItem/ListItem.js +46 -24
- package/lib/module/components/MessageField/MessageField.js +313 -0
- package/lib/module/components/NavArrow/NavArrow.js +59 -18
- package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +322 -0
- package/lib/module/components/Slot/Slot.js +68 -0
- 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/TextInput/TextInput.js +17 -2
- package/lib/module/components/Title/Title.js +10 -2
- package/lib/module/components/index.js +7 -0
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -20
- package/lib/typescript/src/components/Attached/Attached.d.ts +61 -0
- package/lib/typescript/src/components/Card/Card.d.ts +9 -2
- 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/ListItem/ListItem.d.ts +15 -5
- 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/PlanComparisonCard/PlanComparisonCard.d.ts +64 -0
- package/lib/typescript/src/components/Slot/Slot.d.ts +52 -0
- 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/index.d.ts +10 -3
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/Accordion/Accordion.tsx +113 -73
- package/src/components/ActionFooter/ActionFooter.tsx +56 -4
- package/src/components/Attached/Attached.tsx +181 -0
- package/src/components/Card/Card.tsx +28 -1
- 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/ListItem/ListItem.tsx +55 -25
- package/src/components/MessageField/MessageField.tsx +543 -0
- package/src/components/NavArrow/NavArrow.tsx +81 -17
- package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +426 -0
- package/src/components/Slot/Slot.tsx +91 -0
- 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/TextInput/TextInput.tsx +14 -1
- package/src/components/Title/Title.tsx +13 -2
- package/src/components/index.ts +10 -3
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/icons/registry.ts +1 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
4
|
+
import { View, Text } from 'react-native';
|
|
5
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
|
+
import { EMPTY_MODES } from '../../utils/react-utils';
|
|
7
|
+
import Checkbox from '../Checkbox/Checkbox';
|
|
8
|
+
import Button from '../Button/Button';
|
|
9
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
10
|
+
/**
|
|
11
|
+
* Default modes applied to the inner toggle `Button`. These resolve the
|
|
12
|
+
* tertiary-style pill in the Figma reference (small, transparent background,
|
|
13
|
+
* brand purple foreground). Any value supplied via the consumer `modes` prop
|
|
14
|
+
* takes precedence over these defaults.
|
|
15
|
+
*/
|
|
16
|
+
const BUTTON_DEFAULT_MODES = {
|
|
17
|
+
'Button / Size': 'XS',
|
|
18
|
+
AppearanceBrand: 'Secondary',
|
|
19
|
+
Emphasis: 'Low'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* ExpandableCheckbox composes a `Checkbox`, a long-form label and a
|
|
24
|
+
* "Read more" / "Read less" toggle. Mirrors the Figma "Expandable Checkbox"
|
|
25
|
+
* component with two states:
|
|
26
|
+
*
|
|
27
|
+
* - **Idle (collapsed)** — checkbox + truncated label + toggle button arranged
|
|
28
|
+
* in a horizontal row (cross-axis centered).
|
|
29
|
+
* - **Open (expanded)** — checkbox + full multi-line label, with the toggle
|
|
30
|
+
* button right-aligned beneath the row.
|
|
31
|
+
*
|
|
32
|
+
* The checkbox and the toggle button have independent press handlers — pressing
|
|
33
|
+
* the toggle does not affect the checked state, and toggling the checkbox does
|
|
34
|
+
* not collapse / expand the row.
|
|
35
|
+
*
|
|
36
|
+
* @component
|
|
37
|
+
* @param {ExpandableCheckboxProps} props
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* <ExpandableCheckbox
|
|
42
|
+
* label="By checking this box, I (a) acknowledge and (b) agree to the full terms…"
|
|
43
|
+
* defaultChecked
|
|
44
|
+
* onValueChange={setAccepted}
|
|
45
|
+
* modes={{ 'Color Mode': 'Light' }}
|
|
46
|
+
* />
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
function ExpandableCheckbox({
|
|
50
|
+
label = '',
|
|
51
|
+
checked: controlledChecked,
|
|
52
|
+
defaultChecked = false,
|
|
53
|
+
onValueChange,
|
|
54
|
+
expanded: controlledExpanded,
|
|
55
|
+
defaultExpanded = false,
|
|
56
|
+
onExpandedChange,
|
|
57
|
+
disabled = false,
|
|
58
|
+
readMoreLabel = 'Read more',
|
|
59
|
+
readLessLabel = 'Read less',
|
|
60
|
+
collapsedLines = 1,
|
|
61
|
+
modes = EMPTY_MODES,
|
|
62
|
+
style,
|
|
63
|
+
labelStyle,
|
|
64
|
+
accessibilityLabel
|
|
65
|
+
}) {
|
|
66
|
+
const isCheckedControlled = controlledChecked !== undefined;
|
|
67
|
+
const [internalChecked, setInternalChecked] = useState(defaultChecked);
|
|
68
|
+
const isChecked = isCheckedControlled ? controlledChecked : internalChecked;
|
|
69
|
+
const isExpandedControlled = controlledExpanded !== undefined;
|
|
70
|
+
const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
|
|
71
|
+
const isExpanded = isExpandedControlled ? controlledExpanded : internalExpanded;
|
|
72
|
+
const handleToggleChecked = useCallback(next => {
|
|
73
|
+
if (disabled) return;
|
|
74
|
+
if (!isCheckedControlled) setInternalChecked(next);
|
|
75
|
+
onValueChange?.(next);
|
|
76
|
+
}, [disabled, isCheckedControlled, onValueChange]);
|
|
77
|
+
const handleToggleExpanded = useCallback(() => {
|
|
78
|
+
if (disabled) return;
|
|
79
|
+
const next = !isExpanded;
|
|
80
|
+
if (!isExpandedControlled) setInternalExpanded(next);
|
|
81
|
+
onExpandedChange?.(next);
|
|
82
|
+
}, [disabled, isExpanded, isExpandedControlled, onExpandedChange]);
|
|
83
|
+
const gap = getVariableByName('expandableCheckbox/gap', modes) ?? 8;
|
|
84
|
+
const rowGap = getVariableByName('checkboxItem/gap', modes) ?? 8;
|
|
85
|
+
const rowPaddingHorizontal = getVariableByName('checkboxItem/padding/horizontal', modes) ?? 0;
|
|
86
|
+
const rowPaddingVertical = getVariableByName('checkboxItem/padding/vertical', modes) ?? 0;
|
|
87
|
+
const labelColor = getVariableByName('checkboxItem/foreground', modes) ?? '#1a1c1f';
|
|
88
|
+
const labelFontFamily = getVariableByName('checkboxItem/label/fontFamily', modes) ?? 'JioType Var';
|
|
89
|
+
const labelFontSize = getVariableByName('checkboxItem/label/fontSize', modes) ?? 14;
|
|
90
|
+
const labelLineHeight = getVariableByName('checkboxItem/label/lineHeight', modes) ?? 19;
|
|
91
|
+
const labelFontWeightRaw = getVariableByName('checkboxItem/label/fontWeight', modes) ?? 400;
|
|
92
|
+
const labelFontWeight = String(labelFontWeightRaw);
|
|
93
|
+
const containerStyle = useMemo(() => ({
|
|
94
|
+
flexDirection: isExpanded ? 'column' : 'row',
|
|
95
|
+
alignItems: isExpanded ? 'flex-end' : 'center',
|
|
96
|
+
gap,
|
|
97
|
+
width: '100%',
|
|
98
|
+
...(disabled ? {
|
|
99
|
+
opacity: 0.6
|
|
100
|
+
} : null)
|
|
101
|
+
}), [isExpanded, gap, disabled]);
|
|
102
|
+
const rowStyle = useMemo(() => ({
|
|
103
|
+
flex: isExpanded ? undefined : 1,
|
|
104
|
+
alignSelf: isExpanded ? 'stretch' : 'auto',
|
|
105
|
+
minWidth: 0,
|
|
106
|
+
flexDirection: 'row',
|
|
107
|
+
alignItems: 'flex-start',
|
|
108
|
+
gap: rowGap,
|
|
109
|
+
paddingHorizontal: rowPaddingHorizontal,
|
|
110
|
+
paddingVertical: rowPaddingVertical
|
|
111
|
+
}), [isExpanded, rowGap, rowPaddingHorizontal, rowPaddingVertical]);
|
|
112
|
+
const resolvedLabelStyle = useMemo(() => ({
|
|
113
|
+
flex: 1,
|
|
114
|
+
minWidth: 0,
|
|
115
|
+
color: labelColor,
|
|
116
|
+
fontFamily: labelFontFamily,
|
|
117
|
+
fontSize: labelFontSize,
|
|
118
|
+
lineHeight: labelLineHeight,
|
|
119
|
+
fontWeight: labelFontWeight
|
|
120
|
+
}), [labelColor, labelFontFamily, labelFontSize, labelLineHeight, labelFontWeight]);
|
|
121
|
+
const buttonModes = useMemo(() => ({
|
|
122
|
+
...BUTTON_DEFAULT_MODES,
|
|
123
|
+
...modes
|
|
124
|
+
}), [modes]);
|
|
125
|
+
const a11yLabel = accessibilityLabel ?? (typeof label === 'string' ? label : undefined);
|
|
126
|
+
const buttonLabel = isExpanded ? readLessLabel : readMoreLabel;
|
|
127
|
+
const labelNumberOfLinesProps = !isExpanded && collapsedLines > 0 ? {
|
|
128
|
+
numberOfLines: collapsedLines,
|
|
129
|
+
ellipsizeMode: 'tail'
|
|
130
|
+
} : null;
|
|
131
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
132
|
+
style: [containerStyle, style],
|
|
133
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
134
|
+
style: rowStyle,
|
|
135
|
+
children: [/*#__PURE__*/_jsx(Checkbox, {
|
|
136
|
+
checked: isChecked,
|
|
137
|
+
disabled: disabled,
|
|
138
|
+
onValueChange: handleToggleChecked,
|
|
139
|
+
modes: modes,
|
|
140
|
+
...(a11yLabel !== undefined ? {
|
|
141
|
+
accessibilityLabel: a11yLabel
|
|
142
|
+
} : {})
|
|
143
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
144
|
+
style: [resolvedLabelStyle, labelStyle],
|
|
145
|
+
selectable: false,
|
|
146
|
+
...(labelNumberOfLinesProps ?? {}),
|
|
147
|
+
children: label
|
|
148
|
+
})]
|
|
149
|
+
}), /*#__PURE__*/_jsx(Button, {
|
|
150
|
+
label: buttonLabel,
|
|
151
|
+
onPress: handleToggleExpanded,
|
|
152
|
+
disabled: disabled,
|
|
153
|
+
modes: buttonModes,
|
|
154
|
+
accessibilityLabel: buttonLabel,
|
|
155
|
+
accessibilityState: {
|
|
156
|
+
expanded: isExpanded
|
|
157
|
+
}
|
|
158
|
+
})]
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
export default ExpandableCheckbox;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React, { useCallback, useMemo, useState } from 'react';
|
|
4
|
-
import { View, Text, TextInput as RNTextInput } from 'react-native';
|
|
3
|
+
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { View, Text, Pressable, TextInput as RNTextInput } from 'react-native';
|
|
5
5
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
6
|
import { useTokens } from '../../design-tokens/JFSThemeProvider';
|
|
7
7
|
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
|
|
@@ -202,6 +202,16 @@ function FormField({
|
|
|
202
202
|
const [isFocused, setIsFocused] = useState(false);
|
|
203
203
|
const interactive = !isDisabled && !isReadOnly;
|
|
204
204
|
|
|
205
|
+
// Ref to the native input so tapping anywhere in the input row (padding,
|
|
206
|
+
// leading/trailing gutters) focuses it on the FIRST tap — fixing the Android
|
|
207
|
+
// "two taps to open the keyboard" issue caused by the row intercepting the
|
|
208
|
+
// initial touch.
|
|
209
|
+
const inputRef = useRef(null);
|
|
210
|
+
const focusInput = useCallback(() => {
|
|
211
|
+
if (!interactive) return;
|
|
212
|
+
inputRef.current?.focus();
|
|
213
|
+
}, [interactive]);
|
|
214
|
+
|
|
205
215
|
// FormField States cascade — error > read only/disabled > active (focused) > idle.
|
|
206
216
|
// Disabled maps to "Read Only" since there is no dedicated disabled mode and
|
|
207
217
|
// the visual treatment is closest. This is only the DEFAULT — an explicit
|
|
@@ -334,13 +344,16 @@ function FormField({
|
|
|
334
344
|
style: requiredIndicatorStyle,
|
|
335
345
|
children: " *"
|
|
336
346
|
})]
|
|
337
|
-
}), /*#__PURE__*/_jsxs(
|
|
347
|
+
}), /*#__PURE__*/_jsxs(Pressable, {
|
|
338
348
|
style: [inputRowStyle, inputStyle],
|
|
349
|
+
onPress: focusInput,
|
|
350
|
+
accessible: false,
|
|
339
351
|
children: [processedLeading != null && /*#__PURE__*/_jsx(View, {
|
|
340
352
|
accessibilityElementsHidden: true,
|
|
341
353
|
importantForAccessibility: "no",
|
|
342
354
|
children: processedLeading
|
|
343
355
|
}), /*#__PURE__*/_jsx(RNTextInput, {
|
|
356
|
+
ref: inputRef,
|
|
344
357
|
style: [inputTextStyles, inputTextStyle],
|
|
345
358
|
value: value ?? '',
|
|
346
359
|
onChangeText: handleChangeText,
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React, { useMemo } from 'react';
|
|
4
|
+
import { View, Text, ScrollView } from 'react-native';
|
|
5
|
+
import Animated, { Extrapolation, interpolate, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
|
|
6
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
7
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider';
|
|
8
|
+
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
|
|
9
|
+
import Button from '../Button/Button';
|
|
10
|
+
import Disclaimer from '../Disclaimer/Disclaimer';
|
|
11
|
+
import IconButton from '../IconButton/IconButton';
|
|
12
|
+
import ActionFooter from '../ActionFooter/ActionFooter';
|
|
13
|
+
import Slot from '../Slot/Slot';
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Forced modes
|
|
17
|
+
//
|
|
18
|
+
// `FullscreenModal` always themes itself with the `context5: 'Fullscreen Modal'`
|
|
19
|
+
// collection mode. This is what flips the section / list-item / hero text
|
|
20
|
+
// tokens to their white-on-dark values (see the Figma "Fullscreen Modal"
|
|
21
|
+
// context). It is intentionally NON-overridable: callers can pass any other
|
|
22
|
+
// modes (Color Mode, AppearanceBrand, …) but never context5. The frozen
|
|
23
|
+
// object keeps its identity stable so the token resolver's per-modes cache
|
|
24
|
+
// stays hot, and so `cloneChildrenWithModes` can use it as the
|
|
25
|
+
// always-wins `forcedModes` argument.
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
28
|
+
const FULLSCREEN_MODAL_FORCED_MODES = Object.freeze({
|
|
29
|
+
context5: 'Fullscreen Modal'
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Reanimated-driven ScrollView so the parallax handler runs on the UI thread.
|
|
33
|
+
// Module scope so the wrapped component identity is stable across renders.
|
|
34
|
+
const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView);
|
|
35
|
+
|
|
36
|
+
// Parallax tuning. The hero collapses by HEIGHT only as the user scrolls up —
|
|
37
|
+
// its full width is preserved and the media keeps a fixed aspect ratio (it is
|
|
38
|
+
// cropped, never scaled or squished, like a `cover` background). When no
|
|
39
|
+
// explicit `heroMinHeight` is given, the hero collapses to this fraction of
|
|
40
|
+
// its resting height.
|
|
41
|
+
const HERO_MIN_HEIGHT_RATIO = 0.45;
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Hero text — the eyebrow / headline / supporting / price block. Built inline
|
|
45
|
+
// (rather than reusing <PageHero>) so we can render BOTH a supporting
|
|
46
|
+
// paragraph AND a price line with the exact PageHero token gaps, and overlay
|
|
47
|
+
// it on the parallax media without PageHero's media/button scaffolding.
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
function HeroText({
|
|
51
|
+
eyebrow,
|
|
52
|
+
headline,
|
|
53
|
+
supportingText,
|
|
54
|
+
priceText,
|
|
55
|
+
modes
|
|
56
|
+
}) {
|
|
57
|
+
const styles = useMemo(() => {
|
|
58
|
+
const gap = Number(getVariableByName('PageHero/gap', modes)) || 16;
|
|
59
|
+
const textWrapGap = Number(getVariableByName('PageHero/textWrap/gap', modes)) || 8;
|
|
60
|
+
const eyebrowStyle = {
|
|
61
|
+
color: getVariableByName('PageHero/eyebrow/color', modes) || '#ffffff',
|
|
62
|
+
fontFamily: getVariableByName('PageHero/eyebrow/fontFamily', modes) || 'System',
|
|
63
|
+
fontSize: Number(getVariableByName('PageHero/eyebrow/fontSize', modes)) || 18,
|
|
64
|
+
fontWeight: String(getVariableByName('PageHero/eyebrow/fontWeight', modes) || 700),
|
|
65
|
+
lineHeight: Number(getVariableByName('PageHero/eyebrow/lineHeight', modes)) || 20,
|
|
66
|
+
textAlign: 'center'
|
|
67
|
+
};
|
|
68
|
+
const headlineStyle = {
|
|
69
|
+
color: getVariableByName('PageHero/headline/color', modes) || '#ffffff',
|
|
70
|
+
fontFamily: getVariableByName('PageHero/headline/fontFamily', modes) || 'System',
|
|
71
|
+
fontSize: Number(getVariableByName('PageHero/headline/fontSize', modes)) || 29,
|
|
72
|
+
fontWeight: String(getVariableByName('PageHero/headline/fontWeight', modes) || 900),
|
|
73
|
+
lineHeight: Number(getVariableByName('PageHero/headline/lineHeight', modes)) || 29,
|
|
74
|
+
textAlign: 'center',
|
|
75
|
+
width: '100%'
|
|
76
|
+
};
|
|
77
|
+
const supportingTextStyle = {
|
|
78
|
+
color: getVariableByName('PageHero/supportingText/color', modes) || '#ffffff',
|
|
79
|
+
fontFamily: getVariableByName('PageHero/supportingText/fontFamily', modes) || 'System',
|
|
80
|
+
fontSize: Number(getVariableByName('PageHero/supportingText/fontSize', modes)) || 12,
|
|
81
|
+
fontWeight: String(getVariableByName('PageHero/supportingText/fontWeight', modes) || 500),
|
|
82
|
+
lineHeight: Number(getVariableByName('PageHero/supportingText/lineHeight', modes)) || 16,
|
|
83
|
+
textAlign: 'center'
|
|
84
|
+
};
|
|
85
|
+
const priceTextStyle = {
|
|
86
|
+
color: getVariableByName('PageHero/body/color', modes) || '#ffffff',
|
|
87
|
+
fontFamily: getVariableByName('PageHero/body/fontFamily', modes) || 'System',
|
|
88
|
+
fontSize: Number(getVariableByName('PageHero/body/fontSize', modes)) || 12,
|
|
89
|
+
fontWeight: String(getVariableByName('PageHero/body/fontWeight', modes) || 500),
|
|
90
|
+
lineHeight: Number(getVariableByName('PageHero/body/lineHeight', modes)) || 16,
|
|
91
|
+
textAlign: 'center'
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
container: {
|
|
95
|
+
width: '100%',
|
|
96
|
+
alignItems: 'center',
|
|
97
|
+
gap
|
|
98
|
+
},
|
|
99
|
+
textWrap: {
|
|
100
|
+
width: '100%',
|
|
101
|
+
alignItems: 'center',
|
|
102
|
+
gap: textWrapGap
|
|
103
|
+
},
|
|
104
|
+
eyebrowStyle,
|
|
105
|
+
headlineStyle,
|
|
106
|
+
supportingTextStyle,
|
|
107
|
+
priceTextStyle
|
|
108
|
+
};
|
|
109
|
+
}, [modes]);
|
|
110
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
111
|
+
style: styles.container,
|
|
112
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
113
|
+
style: styles.textWrap,
|
|
114
|
+
children: [eyebrow ? /*#__PURE__*/_jsx(Text, {
|
|
115
|
+
style: styles.eyebrowStyle,
|
|
116
|
+
children: eyebrow
|
|
117
|
+
}) : null, headline ? /*#__PURE__*/_jsx(Text, {
|
|
118
|
+
style: styles.headlineStyle,
|
|
119
|
+
children: headline
|
|
120
|
+
}) : null]
|
|
121
|
+
}), supportingText ? /*#__PURE__*/_jsx(Text, {
|
|
122
|
+
style: styles.supportingTextStyle,
|
|
123
|
+
children: supportingText
|
|
124
|
+
}) : null, priceText ? /*#__PURE__*/_jsx(Text, {
|
|
125
|
+
style: styles.priceTextStyle,
|
|
126
|
+
children: priceText
|
|
127
|
+
}) : null]
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* FullscreenModal — a full-screen takeover surface with a parallax media hero,
|
|
133
|
+
* a scrollable body, a floating close button, and a sticky `ActionFooter`.
|
|
134
|
+
*
|
|
135
|
+
* The component always themes itself with `context5: 'Fullscreen Modal'`
|
|
136
|
+
* (non-overridable) so every nested component (Section, ListItem, Button,
|
|
137
|
+
* Disclaimer, …) resolves the white-on-dark "fullscreen modal" token values.
|
|
138
|
+
* That mode is cascaded into `children`, the footer, and the hero text via
|
|
139
|
+
* `cloneChildrenWithModes` / the merged `modes` object.
|
|
140
|
+
*
|
|
141
|
+
* ### Parallax
|
|
142
|
+
* As the user scrolls up, the hero collapses by **height only** (from
|
|
143
|
+
* `heroHeight` to `heroMinHeight`) — its **full width is always preserved**.
|
|
144
|
+
* The `heroMedia` is pinned to the top at a fixed size and `cover`-cropped by
|
|
145
|
+
* the collapsing clip, so it keeps a perfect aspect ratio the whole time
|
|
146
|
+
* (never scaled or squished). Because it collapses slower than the content
|
|
147
|
+
* scrolls, the media lags behind for the parallax depth cue. Disable with
|
|
148
|
+
* `parallax={false}`.
|
|
149
|
+
*
|
|
150
|
+
* @component
|
|
151
|
+
* @example
|
|
152
|
+
* ```tsx
|
|
153
|
+
* <FullscreenModal
|
|
154
|
+
* eyebrow="Upgrade to JioFinance+"
|
|
155
|
+
* headline="Get more from your money."
|
|
156
|
+
* supportingText="JioFinance+ is your upgraded financial experience…"
|
|
157
|
+
* priceText="₹999/year · ₹0 until 2027"
|
|
158
|
+
* heroMedia={<LottiePlayer source={hero} size={{ width: 360, height: 420 }} />}
|
|
159
|
+
* primaryActionLabel="Upgrade for free"
|
|
160
|
+
* disclaimer="By upgrading, we'll check your eligibility with Experian."
|
|
161
|
+
* onPrimaryAction={() => upgrade()}
|
|
162
|
+
* onClose={() => navigation.goBack()}
|
|
163
|
+
* >
|
|
164
|
+
* <Section title="Key Benefits" slotDirection="column" slot={…} />
|
|
165
|
+
* <Section title="Compare plans" slotDirection="column" slot={…} />
|
|
166
|
+
* </FullscreenModal>
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
function FullscreenModal({
|
|
170
|
+
eyebrow = 'Upgrade to JioFinance+',
|
|
171
|
+
headline = 'Get more from your money.',
|
|
172
|
+
supportingText = 'JioFinance+ is your upgraded financial experience, designed to work harder in the background so your money works smarter in real life.',
|
|
173
|
+
priceText = '₹999/year · ₹0 until 2027',
|
|
174
|
+
heroMedia,
|
|
175
|
+
heroHeight = 420,
|
|
176
|
+
heroMinHeight,
|
|
177
|
+
parallax = true,
|
|
178
|
+
showClose = true,
|
|
179
|
+
onClose,
|
|
180
|
+
closeAccessibilityLabel = 'Close',
|
|
181
|
+
footer,
|
|
182
|
+
primaryActionLabel = 'Upgrade for free',
|
|
183
|
+
onPrimaryAction,
|
|
184
|
+
disclaimer = "By upgrading, we'll check your eligibility with Experian.",
|
|
185
|
+
backgroundColor = '#0f0d0a',
|
|
186
|
+
children,
|
|
187
|
+
modes: propModes = EMPTY_MODES,
|
|
188
|
+
style,
|
|
189
|
+
contentContainerStyle,
|
|
190
|
+
testID
|
|
191
|
+
}) {
|
|
192
|
+
const {
|
|
193
|
+
modes: globalModes
|
|
194
|
+
} = useTokens();
|
|
195
|
+
|
|
196
|
+
// context5 is appended last so it always wins, regardless of what the
|
|
197
|
+
// caller (or the global theme) passes.
|
|
198
|
+
const modes = useMemo(() => ({
|
|
199
|
+
...globalModes,
|
|
200
|
+
...propModes,
|
|
201
|
+
...FULLSCREEN_MODAL_FORCED_MODES
|
|
202
|
+
}), [globalModes, propModes]);
|
|
203
|
+
const rootGap = Number(getVariableByName('fullScreenModal/gap', modes)) || 16;
|
|
204
|
+
const minHeight = heroMinHeight ?? Math.round(heroHeight * HERO_MIN_HEIGHT_RATIO);
|
|
205
|
+
const scrollY = useSharedValue(0);
|
|
206
|
+
const onScroll = useAnimatedScrollHandler(event => {
|
|
207
|
+
scrollY.value = event.contentOffset.y;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Collapse the hero by HEIGHT only as the user scrolls up. The clip's width
|
|
211
|
+
// never changes and the media inside is pinned full-size at the top, so the
|
|
212
|
+
// art is cropped (cover) rather than scaled or narrowed — it keeps a perfect
|
|
213
|
+
// aspect ratio the whole time. Pull-down (negative offset) is clamped, so the
|
|
214
|
+
// hero never grows past its resting height.
|
|
215
|
+
const heroAnimatedStyle = useAnimatedStyle(() => {
|
|
216
|
+
const height = interpolate(scrollY.value, [0, heroHeight], [heroHeight, minHeight], Extrapolation.CLAMP);
|
|
217
|
+
return {
|
|
218
|
+
height
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
const processedHeroMedia = useMemo(() => heroMedia ? cloneChildrenWithModes(heroMedia, modes, FULLSCREEN_MODAL_FORCED_MODES) : null, [heroMedia, modes]);
|
|
222
|
+
const processedChildren = useMemo(() => children ? cloneChildrenWithModes(children, modes, FULLSCREEN_MODAL_FORCED_MODES) : null, [children, modes]);
|
|
223
|
+
|
|
224
|
+
// The clip is full-width and top-pinned; its height is what animates. Width
|
|
225
|
+
// is intentionally never animated.
|
|
226
|
+
const heroClipBaseStyle = useMemo(() => ({
|
|
227
|
+
position: 'absolute',
|
|
228
|
+
top: 0,
|
|
229
|
+
left: 0,
|
|
230
|
+
right: 0,
|
|
231
|
+
overflow: 'hidden'
|
|
232
|
+
}), []);
|
|
233
|
+
|
|
234
|
+
// The media sits at a fixed full-size box pinned to the top of the clip, so
|
|
235
|
+
// the collapsing clip crops it from the bottom (cover) instead of resizing
|
|
236
|
+
// it. Full width, fixed height — a perfect, constant aspect ratio.
|
|
237
|
+
const heroMediaWrapStyle = useMemo(() => ({
|
|
238
|
+
position: 'absolute',
|
|
239
|
+
top: 0,
|
|
240
|
+
left: 0,
|
|
241
|
+
right: 0,
|
|
242
|
+
height: heroHeight,
|
|
243
|
+
alignItems: 'stretch'
|
|
244
|
+
}), [heroHeight]);
|
|
245
|
+
const heroTextRegionStyle = useMemo(() => ({
|
|
246
|
+
height: heroHeight,
|
|
247
|
+
justifyContent: 'flex-end',
|
|
248
|
+
paddingHorizontal: 16,
|
|
249
|
+
paddingBottom: 16
|
|
250
|
+
}), [heroHeight]);
|
|
251
|
+
const bodyStyle = useMemo(() => [{
|
|
252
|
+
backgroundColor,
|
|
253
|
+
gap: rootGap,
|
|
254
|
+
paddingTop: rootGap,
|
|
255
|
+
paddingBottom: 24
|
|
256
|
+
}, contentContainerStyle], [backgroundColor, rootGap, contentContainerStyle]);
|
|
257
|
+
const heroClip = /*#__PURE__*/_jsx(Animated.View, {
|
|
258
|
+
style: [heroClipBaseStyle, parallax ? heroAnimatedStyle : {
|
|
259
|
+
height: heroHeight
|
|
260
|
+
}],
|
|
261
|
+
pointerEvents: "none",
|
|
262
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
263
|
+
style: heroMediaWrapStyle,
|
|
264
|
+
children: processedHeroMedia
|
|
265
|
+
})
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Footer: a fully custom node, or the default Button + Disclaimer column.
|
|
269
|
+
let footerContent = null;
|
|
270
|
+
if (footer) {
|
|
271
|
+
footerContent = footer;
|
|
272
|
+
} else if (primaryActionLabel) {
|
|
273
|
+
footerContent = /*#__PURE__*/_jsxs(Slot, {
|
|
274
|
+
layoutDirection: "vertical",
|
|
275
|
+
modes: modes,
|
|
276
|
+
children: [/*#__PURE__*/_jsx(Button, {
|
|
277
|
+
label: primaryActionLabel,
|
|
278
|
+
modes: modes,
|
|
279
|
+
style: fullWidthStyle,
|
|
280
|
+
...(onPrimaryAction ? {
|
|
281
|
+
onPress: onPrimaryAction
|
|
282
|
+
} : {})
|
|
283
|
+
}), disclaimer ? /*#__PURE__*/_jsx(Disclaimer, {
|
|
284
|
+
disclaimer: disclaimer,
|
|
285
|
+
modes: modes
|
|
286
|
+
}) : null]
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
290
|
+
style: [rootStyle, {
|
|
291
|
+
backgroundColor
|
|
292
|
+
}, style],
|
|
293
|
+
testID: testID,
|
|
294
|
+
children: [processedHeroMedia ? heroClip : null, /*#__PURE__*/_jsxs(AnimatedScrollView, {
|
|
295
|
+
style: scrollViewStyle,
|
|
296
|
+
contentContainerStyle: scrollContentStyle,
|
|
297
|
+
showsVerticalScrollIndicator: false,
|
|
298
|
+
onScroll: onScroll,
|
|
299
|
+
scrollEventThrottle: 16,
|
|
300
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
301
|
+
style: heroTextRegionStyle,
|
|
302
|
+
children: /*#__PURE__*/_jsx(HeroText, {
|
|
303
|
+
eyebrow: eyebrow,
|
|
304
|
+
headline: headline,
|
|
305
|
+
supportingText: supportingText,
|
|
306
|
+
priceText: priceText,
|
|
307
|
+
modes: modes
|
|
308
|
+
})
|
|
309
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
310
|
+
style: bodyStyle,
|
|
311
|
+
children: processedChildren
|
|
312
|
+
})]
|
|
313
|
+
}), footerContent ? /*#__PURE__*/_jsx(ActionFooter, {
|
|
314
|
+
modes: modes,
|
|
315
|
+
children: footerContent
|
|
316
|
+
}) : null, showClose ? /*#__PURE__*/_jsx(IconButton, {
|
|
317
|
+
iconName: "ic_close",
|
|
318
|
+
modes: modes,
|
|
319
|
+
accessibilityLabel: closeAccessibilityLabel,
|
|
320
|
+
style: closeButtonStyle,
|
|
321
|
+
...(onClose ? {
|
|
322
|
+
onPress: onClose
|
|
323
|
+
} : {})
|
|
324
|
+
}) : null]
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Module-scope style constants — never re-allocated per render.
|
|
329
|
+
const rootStyle = {
|
|
330
|
+
flex: 1,
|
|
331
|
+
width: '100%',
|
|
332
|
+
position: 'relative'
|
|
333
|
+
};
|
|
334
|
+
const scrollViewStyle = {
|
|
335
|
+
flex: 1
|
|
336
|
+
};
|
|
337
|
+
const scrollContentStyle = {
|
|
338
|
+
flexGrow: 1
|
|
339
|
+
};
|
|
340
|
+
const fullWidthStyle = {
|
|
341
|
+
width: '100%'
|
|
342
|
+
};
|
|
343
|
+
const closeButtonStyle = {
|
|
344
|
+
position: 'absolute',
|
|
345
|
+
top: 12,
|
|
346
|
+
right: 12
|
|
347
|
+
};
|
|
348
|
+
export default FullscreenModal;
|