jfs-components 0.0.85 → 0.0.86
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 +7 -0
- package/lib/commonjs/components/AllocationComparisonChart/AllocationComparisonChart.js +299 -0
- package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +52 -89
- package/lib/commonjs/components/index.js +7 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/AllocationComparisonChart/AllocationComparisonChart.js +293 -0
- package/lib/module/components/FullscreenModal/FullscreenModal.js +53 -90
- package/lib/module/components/index.js +1 -0
- 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 +21 -25
- package/lib/typescript/src/components/index.d.ts +1 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/AllocationComparisonChart/AllocationComparisonChart.tsx +450 -0
- package/src/components/FullscreenModal/FullscreenModal.tsx +61 -119
- package/src/components/index.ts +1 -0
- package/src/icons/registry.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ 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.86] - 2026-06-04
|
|
8
|
+
|
|
9
|
+
- Added `AllocationComparisonChart` — vertical pill bars comparing current vs recommended allocation with optional baseline overlay and dashed marker.
|
|
10
|
+
- `FullscreenModal` — removed parallax; hero media scrolls with content and height follows media aspect ratio.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
7
14
|
## [0.0.85] - 2026-06-03
|
|
8
15
|
|
|
9
16
|
- 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,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,11 @@ const FULLSCREEN_MODAL_FORCED_MODES = Object.freeze({
|
|
|
34
33
|
context5: 'Fullscreen Modal'
|
|
35
34
|
});
|
|
36
35
|
|
|
37
|
-
// Reanimated-driven ScrollView so the parallax handler runs on the UI thread.
|
|
38
|
-
// Module scope so the wrapped component identity is stable across renders.
|
|
39
|
-
const AnimatedScrollView = _reactNativeReanimated.default.createAnimatedComponent(_reactNative.ScrollView);
|
|
40
|
-
|
|
41
|
-
// Parallax tuning. The hero collapses by HEIGHT only as the user scrolls up —
|
|
42
|
-
// its full width is preserved and the media keeps a fixed aspect ratio (it is
|
|
43
|
-
// cropped, never scaled or squished, like a `cover` background). When no
|
|
44
|
-
// explicit `heroMinHeight` is given, the hero collapses to this fraction of
|
|
45
|
-
// its resting height.
|
|
46
|
-
const HERO_MIN_HEIGHT_RATIO = 0.45;
|
|
47
|
-
|
|
48
36
|
// ---------------------------------------------------------------------------
|
|
49
37
|
// Hero text — the eyebrow / headline / supporting / price block. Built inline
|
|
50
38
|
// (rather than reusing <PageHero>) so we can render BOTH a supporting
|
|
51
39
|
// paragraph AND a price line with the exact PageHero token gaps, and overlay
|
|
52
|
-
// it on the
|
|
40
|
+
// it on the hero media without PageHero's media/button scaffolding.
|
|
53
41
|
// ---------------------------------------------------------------------------
|
|
54
42
|
|
|
55
43
|
function HeroText({
|
|
@@ -134,8 +122,9 @@ function HeroText({
|
|
|
134
122
|
}
|
|
135
123
|
|
|
136
124
|
/**
|
|
137
|
-
* FullscreenModal — a full-screen takeover surface with a
|
|
138
|
-
* a scrollable body, a floating close button, and a sticky
|
|
125
|
+
* FullscreenModal — a full-screen takeover surface with a full-bleed media
|
|
126
|
+
* hero, a scrollable body, a floating close button, and a sticky
|
|
127
|
+
* `ActionFooter`.
|
|
139
128
|
*
|
|
140
129
|
* The component always themes itself with `context5: 'Fullscreen Modal'`
|
|
141
130
|
* (non-overridable) so every nested component (Section, ListItem, Button,
|
|
@@ -143,14 +132,12 @@ function HeroText({
|
|
|
143
132
|
* That mode is cascaded into `children`, the footer, and the hero text via
|
|
144
133
|
* `cloneChildrenWithModes` / the merged `modes` object.
|
|
145
134
|
*
|
|
146
|
-
* ###
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* scrolls, the media lags behind for the parallax depth cue. Disable with
|
|
153
|
-
* `parallax={false}`.
|
|
135
|
+
* ### Hero
|
|
136
|
+
* The `heroMedia` is rendered full modal width inside the scroll body and
|
|
137
|
+
* takes its height from its own aspect ratio. The hero text (eyebrow /
|
|
138
|
+
* headline / supporting / price) is overlaid on top, anchored to the bottom.
|
|
139
|
+
* The whole hero scrolls together with the rest of the content — there is no
|
|
140
|
+
* parallax effect.
|
|
154
141
|
*
|
|
155
142
|
* @component
|
|
156
143
|
* @example
|
|
@@ -160,7 +147,7 @@ function HeroText({
|
|
|
160
147
|
* headline="Get more from your money."
|
|
161
148
|
* supportingText="JioFinance+ is your upgraded financial experience…"
|
|
162
149
|
* priceText="₹999/year · ₹0 until 2027"
|
|
163
|
-
* heroMedia={<
|
|
150
|
+
* heroMedia={<Image imageSource={hero} ratio={3 / 4} />}
|
|
164
151
|
* primaryActionLabel="Upgrade for free"
|
|
165
152
|
* disclaimer="By upgrading, we'll check your eligibility with Experian."
|
|
166
153
|
* onPrimaryAction={() => upgrade()}
|
|
@@ -178,8 +165,6 @@ function FullscreenModal({
|
|
|
178
165
|
priceText = '₹999/year · ₹0 until 2027',
|
|
179
166
|
heroMedia,
|
|
180
167
|
heroHeight = 420,
|
|
181
|
-
heroMinHeight,
|
|
182
|
-
parallax = true,
|
|
183
168
|
showClose = true,
|
|
184
169
|
onClose,
|
|
185
170
|
closeAccessibilityLabel = 'Close',
|
|
@@ -206,49 +191,13 @@ function FullscreenModal({
|
|
|
206
191
|
...FULLSCREEN_MODAL_FORCED_MODES
|
|
207
192
|
}), [globalModes, propModes]);
|
|
208
193
|
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
|
-
|
|
215
|
-
// Collapse the hero by HEIGHT only as the user scrolls up. The clip's width
|
|
216
|
-
// never changes and the media inside is pinned full-size at the top, so the
|
|
217
|
-
// art is cropped (cover) rather than scaled or narrowed — it keeps a perfect
|
|
218
|
-
// aspect ratio the whole time. Pull-down (negative offset) is clamped, so the
|
|
219
|
-
// hero never grows past its resting height.
|
|
220
|
-
const heroAnimatedStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => {
|
|
221
|
-
const height = (0, _reactNativeReanimated.interpolate)(scrollY.value, [0, heroHeight], [heroHeight, minHeight], _reactNativeReanimated.Extrapolation.CLAMP);
|
|
222
|
-
return {
|
|
223
|
-
height
|
|
224
|
-
};
|
|
225
|
-
});
|
|
226
194
|
const processedHeroMedia = (0, _react.useMemo)(() => heroMedia ? (0, _reactUtils.cloneChildrenWithModes)(heroMedia, modes, FULLSCREEN_MODAL_FORCED_MODES) : null, [heroMedia, modes]);
|
|
227
195
|
const processedChildren = (0, _react.useMemo)(() => children ? (0, _reactUtils.cloneChildrenWithModes)(children, modes, FULLSCREEN_MODAL_FORCED_MODES) : null, [children, modes]);
|
|
228
196
|
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
const
|
|
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]);
|
|
250
|
-
const heroTextRegionStyle = (0, _react.useMemo)(() => ({
|
|
251
|
-
height: heroHeight,
|
|
197
|
+
// No-media fallback: without hero media the text region needs an explicit
|
|
198
|
+
// resting height (driven by `heroHeight`) so the hero still has presence.
|
|
199
|
+
const heroTextFallbackStyle = (0, _react.useMemo)(() => ({
|
|
200
|
+
minHeight: heroHeight,
|
|
252
201
|
justifyContent: 'flex-end',
|
|
253
202
|
paddingHorizontal: 16,
|
|
254
203
|
paddingBottom: 16
|
|
@@ -259,15 +208,28 @@ function FullscreenModal({
|
|
|
259
208
|
paddingTop: rootGap,
|
|
260
209
|
paddingBottom: 24
|
|
261
210
|
}, contentContainerStyle], [backgroundColor, rootGap, contentContainerStyle]);
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
211
|
+
const heroTextNode = /*#__PURE__*/(0, _jsxRuntime.jsx)(HeroText, {
|
|
212
|
+
eyebrow: eyebrow,
|
|
213
|
+
headline: headline,
|
|
214
|
+
supportingText: supportingText,
|
|
215
|
+
priceText: priceText,
|
|
216
|
+
modes: modes
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// The hero scrolls inline with the body (no parallax). When media is present
|
|
220
|
+
// it is laid out full modal width and takes its height from its own aspect
|
|
221
|
+
// ratio; the hero text is overlaid on top, anchored to the bottom. Without
|
|
222
|
+
// media the text simply renders in flow at the fallback height.
|
|
223
|
+
const hero = processedHeroMedia ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
224
|
+
style: heroMediaContainerStyle,
|
|
225
|
+
children: [processedHeroMedia, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
226
|
+
style: heroTextOverlayStyle,
|
|
227
|
+
pointerEvents: "box-none",
|
|
228
|
+
children: heroTextNode
|
|
229
|
+
})]
|
|
230
|
+
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
231
|
+
style: heroTextFallbackStyle,
|
|
232
|
+
children: heroTextNode
|
|
271
233
|
});
|
|
272
234
|
|
|
273
235
|
// Footer: a fully custom node, or the default Button + Disclaimer column.
|
|
@@ -296,26 +258,15 @@ function FullscreenModal({
|
|
|
296
258
|
backgroundColor
|
|
297
259
|
}, style],
|
|
298
260
|
testID: testID,
|
|
299
|
-
children: [
|
|
261
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.ScrollView, {
|
|
300
262
|
style: scrollViewStyle,
|
|
301
263
|
contentContainerStyle: scrollContentStyle,
|
|
302
|
-
showsVerticalScrollIndicator: false
|
|
303
|
-
onScroll: onScroll,
|
|
304
|
-
scrollEventThrottle: 16
|
|
264
|
+
showsVerticalScrollIndicator: false
|
|
305
265
|
// Tap an input in the body and it focuses on the FIRST tap, even when
|
|
306
266
|
// the keyboard is already open (default 'never' eats that tap).
|
|
307
267
|
,
|
|
308
268
|
keyboardShouldPersistTaps: "handled",
|
|
309
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
310
|
-
style: heroTextRegionStyle,
|
|
311
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(HeroText, {
|
|
312
|
-
eyebrow: eyebrow,
|
|
313
|
-
headline: headline,
|
|
314
|
-
supportingText: supportingText,
|
|
315
|
-
priceText: priceText,
|
|
316
|
-
modes: modes
|
|
317
|
-
})
|
|
318
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
269
|
+
children: [hero, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
319
270
|
style: bodyStyle,
|
|
320
271
|
children: processedChildren
|
|
321
272
|
})]
|
|
@@ -354,4 +305,16 @@ const closeButtonStyle = {
|
|
|
354
305
|
top: 12,
|
|
355
306
|
right: 12
|
|
356
307
|
};
|
|
308
|
+
// Full-width hero wrapper; height comes from the media's own aspect ratio.
|
|
309
|
+
const heroMediaContainerStyle = {
|
|
310
|
+
width: '100%',
|
|
311
|
+
position: 'relative'
|
|
312
|
+
};
|
|
313
|
+
// Hero text overlaid on the media, anchored to the bottom edge.
|
|
314
|
+
const heroTextOverlayStyle = {
|
|
315
|
+
..._reactNative.StyleSheet.absoluteFillObject,
|
|
316
|
+
justifyContent: 'flex-end',
|
|
317
|
+
paddingHorizontal: 16,
|
|
318
|
+
paddingBottom: 16
|
|
319
|
+
};
|
|
357
320
|
var _default = exports.default = FullscreenModal;
|
|
@@ -33,6 +33,12 @@ Object.defineProperty(exports, "ActionTile", {
|
|
|
33
33
|
return _ActionTile.default;
|
|
34
34
|
}
|
|
35
35
|
});
|
|
36
|
+
Object.defineProperty(exports, "AllocationComparisonChart", {
|
|
37
|
+
enumerable: true,
|
|
38
|
+
get: function () {
|
|
39
|
+
return _AllocationComparisonChart.default;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
36
42
|
Object.defineProperty(exports, "AmountInput", {
|
|
37
43
|
enumerable: true,
|
|
38
44
|
get: function () {
|
|
@@ -881,6 +887,7 @@ var _CircularProgressBarDoted = _interopRequireDefault(require("./CircularProgre
|
|
|
881
887
|
var _CircularRating = _interopRequireDefault(require("./CircularRating/CircularRating"));
|
|
882
888
|
var _CoverageRing = _interopRequireDefault(require("./CoverageRing/CoverageRing"));
|
|
883
889
|
var _CoverageBarComparison = _interopRequireDefault(require("./CoverageBarComparison/CoverageBarComparison"));
|
|
890
|
+
var _AllocationComparisonChart = _interopRequireDefault(require("./AllocationComparisonChart/AllocationComparisonChart"));
|
|
884
891
|
var _MonthlyStatusGrid = _interopRequireWildcard(require("./MonthlyStatusGrid/MonthlyStatusGrid"));
|
|
885
892
|
var _Gauge = _interopRequireDefault(require("./Gauge/Gauge"));
|
|
886
893
|
var _HoldingsCard = _interopRequireDefault(require("./HoldingsCard/HoldingsCard"));
|