jfs-components 0.0.85 → 0.0.95
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 +15 -0
- package/lib/commonjs/assets.d.js +1 -0
- package/lib/commonjs/components/AllocationComparisonChart/AllocationComparisonChart.js +299 -0
- package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +104 -94
- package/lib/commonjs/components/Icon/Icon.js +112 -0
- package/lib/commonjs/components/index.js +14 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/assets.d.js +1 -0
- package/lib/module/components/AllocationComparisonChart/AllocationComparisonChart.js +293 -0
- package/lib/module/components/FullscreenModal/FullscreenModal.js +106 -96
- package/lib/module/components/Icon/Icon.js +106 -0
- package/lib/module/components/index.js +2 -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/AllocationComparisonChart/AllocationComparisonChart.d.ts +118 -0
- package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +39 -29
- package/lib/typescript/src/components/Icon/Icon.d.ts +75 -0
- package/lib/typescript/src/components/index.d.ts +2 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/assets.d.ts +24 -0
- package/src/components/AllocationComparisonChart/AllocationComparisonChart.tsx +450 -0
- package/src/components/FullscreenModal/FullscreenModal.tsx +131 -126
- package/src/components/Icon/Icon.tsx +167 -0
- package/src/components/index.ts +2 -0
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/icons/registry.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ All notable changes to this project are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
+
## [0.0.95] - 2026-06-04
|
|
8
|
+
|
|
9
|
+
- Added `Icon` — token-driven design-system icon primitive (`iconName`, `source`, `children` slot); exported from the package barrel.
|
|
10
|
+
- `FullscreenModal` — `heroMedia` is now a full-bleed continuous background behind hero + body; foreground scrolls over it; defaults `Page type` to `JioPlus`; transparent body (removed solid `backgroundColor`).
|
|
11
|
+
- Added `src/assets.d.ts` for TypeScript `require()` of image assets.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## [0.0.86] - 2026-06-04
|
|
16
|
+
|
|
17
|
+
- Added `AllocationComparisonChart` — vertical pill bars comparing current vs recommended allocation with optional baseline overlay and dashed marker.
|
|
18
|
+
- `FullscreenModal` — removed parallax; hero media scrolls with content and height follows media aspect ratio.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
7
22
|
## [0.0.85] - 2026-06-03
|
|
8
23
|
|
|
9
24
|
- Added `AreaLineChart` — multi-series area/line chart with grid, axes, legend, goal pin, and interactive tooltip; exports `useChart` and compound `ChartGrid` / `ChartXAxis` / `ChartYAxis` / `GoalPin` parts.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _react = _interopRequireDefault(require("react"));
|
|
8
|
+
var _reactNative = require("react-native");
|
|
9
|
+
var _reactNativeSvg = _interopRequireWildcard(require("react-native-svg"));
|
|
10
|
+
var _figmaVariablesResolver = require("../../design-tokens/figma-variables-resolver");
|
|
11
|
+
var _JFSThemeProvider = require("../../design-tokens/JFSThemeProvider");
|
|
12
|
+
var _reactUtils = require("../../utils/react-utils");
|
|
13
|
+
var _MetricLegendItem = _interopRequireDefault(require("../MetricLegendItem/MetricLegendItem"));
|
|
14
|
+
var _jsxRuntime = require("react/jsx-runtime");
|
|
15
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
16
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
17
|
+
/**
|
|
18
|
+
* One vertical pill in the {@link AllocationComparisonChartProps.data} array.
|
|
19
|
+
*
|
|
20
|
+
* Each segment renders a single bar whose **height encodes `value`** (the
|
|
21
|
+
* "current" reading) and, when supplied, a **`baseline`** overlay drawn from
|
|
22
|
+
* the bottom up with a dashed marker line (the "recommended" reading). Both
|
|
23
|
+
* are measured against the same shared scale so bars and baselines are
|
|
24
|
+
* directly comparable across segments.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const DEFAULT_DATA = [{
|
|
28
|
+
label: 'Small & Mid',
|
|
29
|
+
value: 65,
|
|
30
|
+
baseline: 35
|
|
31
|
+
}, {
|
|
32
|
+
label: 'Large',
|
|
33
|
+
value: 25
|
|
34
|
+
}, {
|
|
35
|
+
label: 'Others',
|
|
36
|
+
value: 10
|
|
37
|
+
}];
|
|
38
|
+
const toNumber = (value, fallback) => {
|
|
39
|
+
if (typeof value === 'number') {
|
|
40
|
+
return Number.isFinite(value) ? value : fallback;
|
|
41
|
+
}
|
|
42
|
+
if (typeof value === 'string') {
|
|
43
|
+
const parsed = Number(value);
|
|
44
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
45
|
+
}
|
|
46
|
+
return fallback;
|
|
47
|
+
};
|
|
48
|
+
const toFontWeight = (value, fallback) => {
|
|
49
|
+
if (typeof value === 'number') {
|
|
50
|
+
return String(value);
|
|
51
|
+
}
|
|
52
|
+
if (typeof value === 'string') {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
return fallback;
|
|
56
|
+
};
|
|
57
|
+
const isShown = node => node !== undefined && node !== null && node !== false;
|
|
58
|
+
/**
|
|
59
|
+
* Internal: one vertical pill column (the Figma "Segment Indicator"). Not
|
|
60
|
+
* exported — the ergonomic public unit is the chart driven by `data`. The
|
|
61
|
+
* `segmentIndicator/*` token names are mirrored here so design ↔ code token
|
|
62
|
+
* alignment is preserved.
|
|
63
|
+
*/
|
|
64
|
+
function SegmentBar({
|
|
65
|
+
segment,
|
|
66
|
+
barHeightPx,
|
|
67
|
+
baselineHeightPx,
|
|
68
|
+
baselineLabel,
|
|
69
|
+
showMarker,
|
|
70
|
+
theme
|
|
71
|
+
}) {
|
|
72
|
+
const {
|
|
73
|
+
barWidth,
|
|
74
|
+
pillRadius,
|
|
75
|
+
gap,
|
|
76
|
+
currentColor,
|
|
77
|
+
baselineColor,
|
|
78
|
+
lineColor,
|
|
79
|
+
lineSize,
|
|
80
|
+
labelStyle
|
|
81
|
+
} = theme;
|
|
82
|
+
const fillColor = segment.color ?? currentColor;
|
|
83
|
+
const overlayColor = segment.baselineColor ?? baselineColor;
|
|
84
|
+
const showValueLabel = isShown(segment.valueLabel);
|
|
85
|
+
const hasBaseline = baselineHeightPx !== null && baselineHeightPx > 0;
|
|
86
|
+
const overlayHeight = hasBaseline ? Math.min(baselineHeightPx, barHeightPx) : 0;
|
|
87
|
+
const overlayRadius = Math.min(pillRadius, barWidth / 2, overlayHeight / 2);
|
|
88
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
89
|
+
style: {
|
|
90
|
+
flex: 1,
|
|
91
|
+
alignItems: 'center',
|
|
92
|
+
justifyContent: 'flex-end',
|
|
93
|
+
gap
|
|
94
|
+
},
|
|
95
|
+
accessibilityLabel: segment.accessibilityLabel,
|
|
96
|
+
children: [showValueLabel ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
97
|
+
numberOfLines: 1,
|
|
98
|
+
style: labelStyle,
|
|
99
|
+
children: segment.valueLabel
|
|
100
|
+
}) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
101
|
+
style: {
|
|
102
|
+
width: barWidth,
|
|
103
|
+
height: Math.max(barHeightPx, 1),
|
|
104
|
+
borderRadius: pillRadius,
|
|
105
|
+
backgroundColor: fillColor,
|
|
106
|
+
position: 'relative'
|
|
107
|
+
},
|
|
108
|
+
children: hasBaseline ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
109
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
110
|
+
style: {
|
|
111
|
+
position: 'absolute',
|
|
112
|
+
left: 0,
|
|
113
|
+
right: 0,
|
|
114
|
+
bottom: 0,
|
|
115
|
+
height: overlayHeight,
|
|
116
|
+
backgroundColor: overlayColor,
|
|
117
|
+
borderBottomLeftRadius: overlayRadius,
|
|
118
|
+
borderBottomRightRadius: overlayRadius
|
|
119
|
+
}
|
|
120
|
+
}), showMarker ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
121
|
+
style: {
|
|
122
|
+
position: 'absolute',
|
|
123
|
+
left: 0,
|
|
124
|
+
bottom: overlayHeight,
|
|
125
|
+
height: 0,
|
|
126
|
+
flexDirection: 'row',
|
|
127
|
+
alignItems: 'center'
|
|
128
|
+
},
|
|
129
|
+
pointerEvents: "none",
|
|
130
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.default, {
|
|
131
|
+
width: barWidth,
|
|
132
|
+
height: Math.max(lineSize, 1),
|
|
133
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSvg.Line, {
|
|
134
|
+
x1: 0,
|
|
135
|
+
y1: Math.max(lineSize, 1) / 2,
|
|
136
|
+
x2: barWidth,
|
|
137
|
+
y2: Math.max(lineSize, 1) / 2,
|
|
138
|
+
stroke: lineColor,
|
|
139
|
+
strokeWidth: lineSize,
|
|
140
|
+
strokeDasharray: "2 2"
|
|
141
|
+
})
|
|
142
|
+
}), isShown(baselineLabel) ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
143
|
+
numberOfLines: 1,
|
|
144
|
+
style: [labelStyle, {
|
|
145
|
+
marginLeft: 6
|
|
146
|
+
}],
|
|
147
|
+
children: baselineLabel
|
|
148
|
+
}) : null]
|
|
149
|
+
}) : null]
|
|
150
|
+
}) : null
|
|
151
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
152
|
+
numberOfLines: 1,
|
|
153
|
+
style: labelStyle,
|
|
154
|
+
children: segment.label
|
|
155
|
+
})]
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* `AllocationComparisonChart` plots a row of vertical pill bars that compare a
|
|
161
|
+
* **current** reading (each bar's height) against an optional **recommended**
|
|
162
|
+
* baseline (a filled overlay drawn from the bottom up, marked with a dashed
|
|
163
|
+
* line). Every bar and baseline shares a single scale, so heights are directly
|
|
164
|
+
* comparable across segments — no axes required.
|
|
165
|
+
*
|
|
166
|
+
* The chart is driven entirely by the `data` array: each entry pairs a
|
|
167
|
+
* `value`, an optional `baseline` and its `label`, so a bar can never drift
|
|
168
|
+
* out of sync with its caption or its baseline marker.
|
|
169
|
+
*
|
|
170
|
+
* Colors, fonts, spacing and the pill radius resolve from the Figma
|
|
171
|
+
* `segmentIndicator/*`, `metricLegendItem/*` and `allocationComparisonChart/*`
|
|
172
|
+
* tokens via the `modes` prop.
|
|
173
|
+
*
|
|
174
|
+
* @component
|
|
175
|
+
*/
|
|
176
|
+
function AllocationComparisonChart({
|
|
177
|
+
data = DEFAULT_DATA,
|
|
178
|
+
max,
|
|
179
|
+
height = 154,
|
|
180
|
+
barWidth,
|
|
181
|
+
showLegend = true,
|
|
182
|
+
valueLegendLabel = 'Current',
|
|
183
|
+
baselineLegendLabel = 'Recommended',
|
|
184
|
+
formatValue = value => `${value}%`,
|
|
185
|
+
modes: propModes = _reactUtils.EMPTY_MODES,
|
|
186
|
+
style,
|
|
187
|
+
chartStyle,
|
|
188
|
+
legendStyle,
|
|
189
|
+
accessibilityLabel
|
|
190
|
+
}) {
|
|
191
|
+
const {
|
|
192
|
+
modes: globalModes
|
|
193
|
+
} = (0, _JFSThemeProvider.useTokens)();
|
|
194
|
+
const modes = _react.default.useMemo(() => ({
|
|
195
|
+
...globalModes,
|
|
196
|
+
...propModes
|
|
197
|
+
}), [globalModes, propModes]);
|
|
198
|
+
const trackWidth = toNumber((0, _figmaVariablesResolver.getVariableByName)('segmentIndicator/track/width', modes), 28);
|
|
199
|
+
const resolvedBarWidth = barWidth ?? trackWidth;
|
|
200
|
+
const radiusToken = toNumber((0, _figmaVariablesResolver.getVariableByName)('segmentIndicator/indicator/radius', modes), 99999);
|
|
201
|
+
const pillRadius = Math.min(radiusToken, resolvedBarWidth / 2);
|
|
202
|
+
const gap = toNumber((0, _figmaVariablesResolver.getVariableByName)('segmentIndicator/gap', modes), 4);
|
|
203
|
+
const chartGap = toNumber((0, _figmaVariablesResolver.getVariableByName)('allocationComparisonChart/gap', modes), 8);
|
|
204
|
+
const currentColor = (0, _figmaVariablesResolver.getVariableByName)('segmentIndicator/indicator/background', modes) ?? '#5d00b5';
|
|
205
|
+
const baselineColor = (0, _figmaVariablesResolver.getVariableByName)('segmentIndicator/indicator/foreground', modes) ?? '#b84fbd';
|
|
206
|
+
const lineColor = (0, _figmaVariablesResolver.getVariableByName)('segmentIndicator/indicator/line/color', modes) ?? '#ffffff';
|
|
207
|
+
const lineSize = toNumber((0, _figmaVariablesResolver.getVariableByName)('segmentIndicator/indicator/line/size', modes), 1);
|
|
208
|
+
const foreground = (0, _figmaVariablesResolver.getVariableByName)('segmentIndicator/foreground', modes) ?? '#0c0d10';
|
|
209
|
+
const fontFamily = (0, _figmaVariablesResolver.getVariableByName)('segmentIndicator/fontFamily', modes) ?? 'JioType Var';
|
|
210
|
+
const fontSize = toNumber((0, _figmaVariablesResolver.getVariableByName)('segmentIndicator/fontSize', modes), 12);
|
|
211
|
+
const lineHeight = toNumber((0, _figmaVariablesResolver.getVariableByName)('segmentIndicator/lineHeight', modes), 12);
|
|
212
|
+
const fontWeight = toFontWeight((0, _figmaVariablesResolver.getVariableByName)('segmentIndicator/fontWeight', modes), '400');
|
|
213
|
+
const labelStyle = {
|
|
214
|
+
color: foreground,
|
|
215
|
+
fontFamily,
|
|
216
|
+
fontSize,
|
|
217
|
+
lineHeight,
|
|
218
|
+
fontWeight,
|
|
219
|
+
textAlign: 'center'
|
|
220
|
+
};
|
|
221
|
+
const computedMax = max ?? data.reduce((acc, seg) => Math.max(acc, seg.value, seg.baseline ?? 0), 0);
|
|
222
|
+
const safeMax = computedMax > 0 ? computedMax : 1;
|
|
223
|
+
const firstBaselineIndex = data.findIndex(seg => typeof seg.baseline === 'number');
|
|
224
|
+
const hasAnyBaseline = firstBaselineIndex !== -1;
|
|
225
|
+
const theme = {
|
|
226
|
+
barWidth: resolvedBarWidth,
|
|
227
|
+
pillRadius,
|
|
228
|
+
gap,
|
|
229
|
+
currentColor,
|
|
230
|
+
baselineColor,
|
|
231
|
+
lineColor,
|
|
232
|
+
lineSize,
|
|
233
|
+
labelStyle
|
|
234
|
+
};
|
|
235
|
+
const defaultAccessibilityLabel = accessibilityLabel ?? `Allocation comparison of ${data.length} segment${data.length === 1 ? '' : 's'}: ` + data.map(seg => {
|
|
236
|
+
const label = typeof seg.label === 'string' ? seg.label : 'segment';
|
|
237
|
+
const base = typeof seg.baseline === 'number' ? `, recommended ${seg.baseline}` : '';
|
|
238
|
+
return `${label} ${seg.value}${base}`;
|
|
239
|
+
}).join('; ');
|
|
240
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
241
|
+
style: [{
|
|
242
|
+
width: '100%'
|
|
243
|
+
}, style],
|
|
244
|
+
accessibilityLabel: defaultAccessibilityLabel,
|
|
245
|
+
children: [showLegend ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
246
|
+
style: [{
|
|
247
|
+
flexDirection: 'row',
|
|
248
|
+
alignItems: 'center',
|
|
249
|
+
gap: 8,
|
|
250
|
+
marginBottom: chartGap
|
|
251
|
+
}, legendStyle],
|
|
252
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_MetricLegendItem.default, {
|
|
253
|
+
label: valueLegendLabel,
|
|
254
|
+
indicatorColor: currentColor,
|
|
255
|
+
modes: modes,
|
|
256
|
+
style: {
|
|
257
|
+
flexGrow: 0,
|
|
258
|
+
flexShrink: 1
|
|
259
|
+
}
|
|
260
|
+
}), hasAnyBaseline ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_MetricLegendItem.default, {
|
|
261
|
+
label: baselineLegendLabel,
|
|
262
|
+
indicatorColor: baselineColor,
|
|
263
|
+
modes: modes,
|
|
264
|
+
style: {
|
|
265
|
+
flexGrow: 0,
|
|
266
|
+
flexShrink: 1
|
|
267
|
+
}
|
|
268
|
+
}) : null]
|
|
269
|
+
}) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
270
|
+
accessibilityRole: "image",
|
|
271
|
+
style: [{
|
|
272
|
+
flexDirection: 'row',
|
|
273
|
+
alignItems: 'flex-end',
|
|
274
|
+
gap: 8,
|
|
275
|
+
width: '100%'
|
|
276
|
+
}, chartStyle],
|
|
277
|
+
children: data.map((segment, index) => {
|
|
278
|
+
const ratio = Math.max(0, Math.min(1, segment.value / safeMax));
|
|
279
|
+
const barHeightPx = Math.max(0, height * ratio);
|
|
280
|
+
const baselineHeightPx = typeof segment.baseline === 'number' ? Math.max(0, Math.min(1, segment.baseline / safeMax)) * height : null;
|
|
281
|
+
const baselineLabel = segment.baselineLabel === undefined ? typeof segment.baseline === 'number' ? formatValue(segment.baseline) : undefined : segment.baselineLabel;
|
|
282
|
+
const resolvedSegment = {
|
|
283
|
+
...segment,
|
|
284
|
+
valueLabel: segment.valueLabel === undefined ? formatValue(segment.value) : segment.valueLabel
|
|
285
|
+
};
|
|
286
|
+
const showMarker = segment.showMarker ?? index === firstBaselineIndex;
|
|
287
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(SegmentBar, {
|
|
288
|
+
segment: resolvedSegment,
|
|
289
|
+
barHeightPx: barHeightPx,
|
|
290
|
+
baselineHeightPx: baselineHeightPx,
|
|
291
|
+
baselineLabel: baselineLabel,
|
|
292
|
+
showMarker: showMarker,
|
|
293
|
+
theme: theme
|
|
294
|
+
}, segment.key ?? `segment-${index}`);
|
|
295
|
+
})
|
|
296
|
+
})]
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
var _default = exports.default = AllocationComparisonChart;
|
|
@@ -6,7 +6,6 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.default = void 0;
|
|
7
7
|
var _react = _interopRequireWildcard(require("react"));
|
|
8
8
|
var _reactNative = require("react-native");
|
|
9
|
-
var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated"));
|
|
10
9
|
var _figmaVariablesResolver = require("../../design-tokens/figma-variables-resolver");
|
|
11
10
|
var _JFSThemeProvider = require("../../design-tokens/JFSThemeProvider");
|
|
12
11
|
var _reactUtils = require("../../utils/react-utils");
|
|
@@ -34,22 +33,24 @@ const FULLSCREEN_MODAL_FORCED_MODES = Object.freeze({
|
|
|
34
33
|
context5: 'Fullscreen Modal'
|
|
35
34
|
});
|
|
36
35
|
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
const
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Default modes
|
|
38
|
+
//
|
|
39
|
+
// A FullscreenModal is a "JioPlus" surface, so it defaults the `Page type`
|
|
40
|
+
// collection to `'JioPlus'`. Unlike the forced modes above this IS
|
|
41
|
+
// overridable — it is applied before the caller's `modes`, so passing
|
|
42
|
+
// `modes={{ 'Page type': 'SubPage' }}` still wins. Frozen for stable identity
|
|
43
|
+
// (keeps the token resolver's per-modes cache hot).
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
const FULLSCREEN_MODAL_DEFAULT_MODES = Object.freeze({
|
|
46
|
+
'Page type': 'JioPlus'
|
|
47
|
+
});
|
|
47
48
|
|
|
48
49
|
// ---------------------------------------------------------------------------
|
|
49
50
|
// Hero text — the eyebrow / headline / supporting / price block. Built inline
|
|
50
51
|
// (rather than reusing <PageHero>) so we can render BOTH a supporting
|
|
51
52
|
// paragraph AND a price line with the exact PageHero token gaps, and overlay
|
|
52
|
-
// it on the
|
|
53
|
+
// it on the hero media without PageHero's media/button scaffolding.
|
|
53
54
|
// ---------------------------------------------------------------------------
|
|
54
55
|
|
|
55
56
|
function HeroText({
|
|
@@ -134,8 +135,9 @@ function HeroText({
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
/**
|
|
137
|
-
* FullscreenModal — a full-screen takeover surface with a
|
|
138
|
-
* a scrollable body, a floating close button, and a sticky
|
|
138
|
+
* FullscreenModal — a full-screen takeover surface with a full-bleed media
|
|
139
|
+
* hero, a scrollable body, a floating close button, and a sticky
|
|
140
|
+
* `ActionFooter`.
|
|
139
141
|
*
|
|
140
142
|
* The component always themes itself with `context5: 'Fullscreen Modal'`
|
|
141
143
|
* (non-overridable) so every nested component (Section, ListItem, Button,
|
|
@@ -143,14 +145,21 @@ function HeroText({
|
|
|
143
145
|
* That mode is cascaded into `children`, the footer, and the hero text via
|
|
144
146
|
* `cloneChildrenWithModes` / the merged `modes` object.
|
|
145
147
|
*
|
|
146
|
-
* ###
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
148
|
+
* ### Background media
|
|
149
|
+
* The `heroMedia` is a single full-bleed background pinned to the top of the
|
|
150
|
+
* modal at the full width and its own natural aspect ratio. It lives at the
|
|
151
|
+
* ROOT — behind both the scrolling content and the (transparent) footer — so
|
|
152
|
+
* it fills the whole surface and is NEVER clipped to the content height. It
|
|
153
|
+
* also contributes ZERO scroll height: the scroll extent is driven purely by
|
|
154
|
+
* the in-flow foreground (hero text + `children`), so the number of body
|
|
155
|
+
* elements dictates how far the surface scrolls. It still scrolls in lockstep
|
|
156
|
+
* WITH the content (the background is translated by the scroll offset), so the
|
|
157
|
+
* content reads as sitting ON one continuous image that moves with it — there
|
|
158
|
+
* is no parallax and no separate solid body box.
|
|
159
|
+
*
|
|
160
|
+
* Pass a background sized to the full width at its natural ratio
|
|
161
|
+
* (e.g. `<Image imageSource={bg} ratio={1080 / 4140} />`). Use an asset at
|
|
162
|
+
* least as tall as the surface so it covers the full modal.
|
|
154
163
|
*
|
|
155
164
|
* @component
|
|
156
165
|
* @example
|
|
@@ -160,7 +169,7 @@ function HeroText({
|
|
|
160
169
|
* headline="Get more from your money."
|
|
161
170
|
* supportingText="JioFinance+ is your upgraded financial experience…"
|
|
162
171
|
* priceText="₹999/year · ₹0 until 2027"
|
|
163
|
-
* heroMedia={<
|
|
172
|
+
* heroMedia={<Image imageSource={hero} ratio={1080 / 4140} />}
|
|
164
173
|
* primaryActionLabel="Upgrade for free"
|
|
165
174
|
* disclaimer="By upgrading, we'll check your eligibility with Experian."
|
|
166
175
|
* onPrimaryAction={() => upgrade()}
|
|
@@ -178,8 +187,6 @@ function FullscreenModal({
|
|
|
178
187
|
priceText = '₹999/year · ₹0 until 2027',
|
|
179
188
|
heroMedia,
|
|
180
189
|
heroHeight = 420,
|
|
181
|
-
heroMinHeight,
|
|
182
|
-
parallax = true,
|
|
183
190
|
showClose = true,
|
|
184
191
|
onClose,
|
|
185
192
|
closeAccessibilityLabel = 'Close',
|
|
@@ -187,7 +194,6 @@ function FullscreenModal({
|
|
|
187
194
|
primaryActionLabel = 'Upgrade for free',
|
|
188
195
|
onPrimaryAction,
|
|
189
196
|
disclaimer = "By upgrading, we'll check your eligibility with Experian.",
|
|
190
|
-
backgroundColor = '#0f0d0a',
|
|
191
197
|
children,
|
|
192
198
|
modes: propModes = _reactUtils.EMPTY_MODES,
|
|
193
199
|
style,
|
|
@@ -198,76 +204,62 @@ function FullscreenModal({
|
|
|
198
204
|
modes: globalModes
|
|
199
205
|
} = (0, _JFSThemeProvider.useTokens)();
|
|
200
206
|
|
|
201
|
-
//
|
|
202
|
-
//
|
|
207
|
+
// Merge order (low → high priority):
|
|
208
|
+
// global theme → component defaults (Page type: JioPlus) → caller modes →
|
|
209
|
+
// forced modes (context5). So `Page type` defaults to JioPlus but the
|
|
210
|
+
// caller can override it, while `context5` always wins. This single `modes`
|
|
211
|
+
// object is what cascades to the body, hero media, and the ActionFooter.
|
|
203
212
|
const modes = (0, _react.useMemo)(() => ({
|
|
204
213
|
...globalModes,
|
|
214
|
+
...FULLSCREEN_MODAL_DEFAULT_MODES,
|
|
205
215
|
...propModes,
|
|
206
216
|
...FULLSCREEN_MODAL_FORCED_MODES
|
|
207
217
|
}), [globalModes, propModes]);
|
|
208
218
|
const rootGap = Number((0, _figmaVariablesResolver.getVariableByName)('fullScreenModal/gap', modes)) || 16;
|
|
209
|
-
const minHeight = heroMinHeight ?? Math.round(heroHeight * HERO_MIN_HEIGHT_RATIO);
|
|
210
|
-
const scrollY = (0, _reactNativeReanimated.useSharedValue)(0);
|
|
211
|
-
const onScroll = (0, _reactNativeReanimated.useAnimatedScrollHandler)(event => {
|
|
212
|
-
scrollY.value = event.contentOffset.y;
|
|
213
|
-
});
|
|
214
219
|
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
220
|
+
// Drives the background's parallax-free sync with the scroll. The hero media
|
|
221
|
+
// lives at the ROOT (so it is never clipped to the content height and sits
|
|
222
|
+
// behind the transparent footer), but we translate it up by the exact scroll
|
|
223
|
+
// offset so it moves in lockstep with the content — i.e. it scrolls WITH the
|
|
224
|
+
// body without ever contributing to the scroll height.
|
|
225
|
+
const scrollY = (0, _react.useRef)(new _reactNative.Animated.Value(0)).current;
|
|
226
|
+
const onScroll = (0, _react.useMemo)(() => _reactNative.Animated.event([{
|
|
227
|
+
nativeEvent: {
|
|
228
|
+
contentOffset: {
|
|
229
|
+
y: scrollY
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}], {
|
|
233
|
+
useNativeDriver: true
|
|
234
|
+
}), [scrollY]);
|
|
235
|
+
const heroTranslateY = (0, _react.useMemo)(() => _reactNative.Animated.multiply(scrollY, -1), [scrollY]);
|
|
226
236
|
const processedHeroMedia = (0, _react.useMemo)(() => heroMedia ? (0, _reactUtils.cloneChildrenWithModes)(heroMedia, modes, FULLSCREEN_MODAL_FORCED_MODES) : null, [heroMedia, modes]);
|
|
227
237
|
const processedChildren = (0, _react.useMemo)(() => children ? (0, _reactUtils.cloneChildrenWithModes)(children, modes, FULLSCREEN_MODAL_FORCED_MODES) : null, [children, modes]);
|
|
228
238
|
|
|
229
|
-
// The
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
top: 0,
|
|
234
|
-
left: 0,
|
|
235
|
-
right: 0,
|
|
236
|
-
overflow: 'hidden'
|
|
237
|
-
}), []);
|
|
238
|
-
|
|
239
|
-
// The media sits at a fixed full-size box pinned to the top of the clip, so
|
|
240
|
-
// the collapsing clip crops it from the bottom (cover) instead of resizing
|
|
241
|
-
// it. Full width, fixed height — a perfect, constant aspect ratio.
|
|
242
|
-
const heroMediaWrapStyle = (0, _react.useMemo)(() => ({
|
|
243
|
-
position: 'absolute',
|
|
244
|
-
top: 0,
|
|
245
|
-
left: 0,
|
|
246
|
-
right: 0,
|
|
247
|
-
height: heroHeight,
|
|
248
|
-
alignItems: 'stretch'
|
|
249
|
-
}), [heroHeight]);
|
|
239
|
+
// The hero text region always reserves `heroHeight` and anchors its content
|
|
240
|
+
// to the bottom, so the eyebrow/headline block sits in the lower part of the
|
|
241
|
+
// first screenful — over the background media when present, in flow
|
|
242
|
+
// otherwise.
|
|
250
243
|
const heroTextRegionStyle = (0, _react.useMemo)(() => ({
|
|
251
|
-
|
|
244
|
+
minHeight: heroHeight,
|
|
252
245
|
justifyContent: 'flex-end',
|
|
253
246
|
paddingHorizontal: 16,
|
|
254
247
|
paddingBottom: 16
|
|
255
248
|
}), [heroHeight]);
|
|
249
|
+
|
|
250
|
+
// Body is intentionally transparent — the background media shows through
|
|
251
|
+
// behind it. There is no solid "body box" stacked on top of the image.
|
|
256
252
|
const bodyStyle = (0, _react.useMemo)(() => [{
|
|
257
|
-
backgroundColor,
|
|
258
253
|
gap: rootGap,
|
|
259
254
|
paddingTop: rootGap,
|
|
260
255
|
paddingBottom: 24
|
|
261
|
-
}, contentContainerStyle], [
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
style: heroMediaWrapStyle,
|
|
269
|
-
children: processedHeroMedia
|
|
270
|
-
})
|
|
256
|
+
}, contentContainerStyle], [rootGap, contentContainerStyle]);
|
|
257
|
+
const heroTextNode = /*#__PURE__*/(0, _jsxRuntime.jsx)(HeroText, {
|
|
258
|
+
eyebrow: eyebrow,
|
|
259
|
+
headline: headline,
|
|
260
|
+
supportingText: supportingText,
|
|
261
|
+
priceText: priceText,
|
|
262
|
+
modes: modes
|
|
271
263
|
});
|
|
272
264
|
|
|
273
265
|
// Footer: a fully custom node, or the default Button + Disclaimer column.
|
|
@@ -292,11 +284,17 @@ function FullscreenModal({
|
|
|
292
284
|
});
|
|
293
285
|
}
|
|
294
286
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
295
|
-
style: [rootStyle,
|
|
296
|
-
backgroundColor
|
|
297
|
-
}, style],
|
|
287
|
+
style: [rootStyle, style],
|
|
298
288
|
testID: testID,
|
|
299
|
-
children: [processedHeroMedia ?
|
|
289
|
+
children: [processedHeroMedia ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, {
|
|
290
|
+
style: [heroBackgroundStyle, {
|
|
291
|
+
transform: [{
|
|
292
|
+
translateY: heroTranslateY
|
|
293
|
+
}]
|
|
294
|
+
}],
|
|
295
|
+
pointerEvents: "none",
|
|
296
|
+
children: processedHeroMedia
|
|
297
|
+
}) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.ScrollView, {
|
|
300
298
|
style: scrollViewStyle,
|
|
301
299
|
contentContainerStyle: scrollContentStyle,
|
|
302
300
|
showsVerticalScrollIndicator: false,
|
|
@@ -306,19 +304,16 @@ function FullscreenModal({
|
|
|
306
304
|
// the keyboard is already open (default 'never' eats that tap).
|
|
307
305
|
,
|
|
308
306
|
keyboardShouldPersistTaps: "handled",
|
|
309
|
-
children:
|
|
310
|
-
style:
|
|
311
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
})
|
|
318
|
-
})
|
|
319
|
-
style: bodyStyle,
|
|
320
|
-
children: processedChildren
|
|
321
|
-
})]
|
|
307
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
308
|
+
style: foregroundFlowStyle,
|
|
309
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
310
|
+
style: heroTextRegionStyle,
|
|
311
|
+
children: heroTextNode
|
|
312
|
+
}), processedChildren ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
313
|
+
style: bodyStyle,
|
|
314
|
+
children: processedChildren
|
|
315
|
+
}) : null]
|
|
316
|
+
})
|
|
322
317
|
}), footerContent ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_ActionFooter.default, {
|
|
323
318
|
modes: modes,
|
|
324
319
|
children: footerContent
|
|
@@ -354,4 +349,19 @@ const closeButtonStyle = {
|
|
|
354
349
|
top: 12,
|
|
355
350
|
right: 12
|
|
356
351
|
};
|
|
352
|
+
// Root-level full-bleed background media. Pinned to the top at full modal
|
|
353
|
+
// width; the media inside keeps its own natural aspect ratio (only `top` is
|
|
354
|
+
// pinned — no `bottom`/`overflow` clip), so it is NEVER cut to the content
|
|
355
|
+
// height and fills the surface behind the scrolling content and the footer.
|
|
356
|
+
// Living outside the ScrollView, it adds nothing to the scroll height.
|
|
357
|
+
const heroBackgroundStyle = {
|
|
358
|
+
position: 'absolute',
|
|
359
|
+
top: 0,
|
|
360
|
+
left: 0,
|
|
361
|
+
right: 0
|
|
362
|
+
};
|
|
363
|
+
// The foreground always flows normally — its content drives the scroll height.
|
|
364
|
+
const foregroundFlowStyle = {
|
|
365
|
+
width: '100%'
|
|
366
|
+
};
|
|
357
367
|
var _default = exports.default = FullscreenModal;
|