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.
@@ -3,17 +3,11 @@ import {
3
3
  View,
4
4
  Text,
5
5
  ScrollView,
6
+ StyleSheet,
6
7
  type StyleProp,
7
8
  type ViewStyle,
8
9
  type TextStyle,
9
10
  } from 'react-native'
10
- import Animated, {
11
- Extrapolation,
12
- interpolate,
13
- useAnimatedScrollHandler,
14
- useAnimatedStyle,
15
- useSharedValue,
16
- } from 'react-native-reanimated'
17
11
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
18
12
  import { useTokens } from '../../design-tokens/JFSThemeProvider'
19
13
  import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
@@ -37,17 +31,6 @@ import Slot from '../Slot/Slot'
37
31
  // ---------------------------------------------------------------------------
38
32
  const FULLSCREEN_MODAL_FORCED_MODES = Object.freeze({ context5: 'Fullscreen Modal' })
39
33
 
40
- // Reanimated-driven ScrollView so the parallax handler runs on the UI thread.
41
- // Module scope so the wrapped component identity is stable across renders.
42
- const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView)
43
-
44
- // Parallax tuning. The hero collapses by HEIGHT only as the user scrolls up —
45
- // its full width is preserved and the media keeps a fixed aspect ratio (it is
46
- // cropped, never scaled or squished, like a `cover` background). When no
47
- // explicit `heroMinHeight` is given, the hero collapses to this fraction of
48
- // its resting height.
49
- const HERO_MIN_HEIGHT_RATIO = 0.45
50
-
51
34
  export type FullscreenModalProps = {
52
35
  /** Small eyebrow line above the headline. */
53
36
  eyebrow?: string
@@ -58,23 +41,20 @@ export type FullscreenModalProps = {
58
41
  /** Secondary line below the supporting paragraph (e.g. a price / timeline). */
59
42
  priceText?: string
60
43
  /**
61
- * Media rendered full-bleed behind the hero text and driven by the parallax
62
- * scroll effect. Bring any renderer — most commonly a `LottiePlayer`, but an
63
- * `Image`, `Video`, or `SvgXml` works too. Size it to fill the hero box
64
- * (`heroHeight` tall, full modal width) and let it `cover` so that as the
65
- * hero collapses in height the art is cropped, never distorted. `modes` are
66
- * cascaded into it.
44
+ * Background media rendered full-bleed behind the hero text. Bring any
45
+ * renderer — most commonly an `Image`, but a `LottiePlayer`, `Video`, or
46
+ * `SvgXml` works too. It is laid out at the full modal width; size it with an
47
+ * aspect ratio (e.g. `<Image ratio={3 / 4} />`) so its height follows the
48
+ * width naturally. The media scrolls together with the rest of the content
49
+ * (no parallax). `modes` are cascaded into it.
67
50
  */
68
51
  heroMedia?: React.ReactNode
69
- /** Resting height of the hero region. Defaults to 420. */
70
- heroHeight?: number
71
52
  /**
72
- * Collapsed height the hero shrinks to at full scroll. Defaults to
73
- * `heroHeight * 0.45`. Only the height changes the width is always full.
53
+ * Fallback height for the hero text region when no `heroMedia` is provided.
54
+ * When `heroMedia` is present, the hero height is driven entirely by the
55
+ * media's own aspect ratio and this value is ignored. Defaults to 420.
74
56
  */
75
- heroMinHeight?: number
76
- /** Enable the scroll-driven hero collapse. Defaults to true. */
77
- parallax?: boolean
57
+ heroHeight?: number
78
58
  /** Whether to render the floating close button (top-right). Defaults to true. */
79
59
  showClose?: boolean
80
60
  /** Press handler for the close button. */
@@ -109,7 +89,7 @@ export type FullscreenModalProps = {
109
89
  // Hero text — the eyebrow / headline / supporting / price block. Built inline
110
90
  // (rather than reusing <PageHero>) so we can render BOTH a supporting
111
91
  // paragraph AND a price line with the exact PageHero token gaps, and overlay
112
- // it on the parallax media without PageHero's media/button scaffolding.
92
+ // it on the hero media without PageHero's media/button scaffolding.
113
93
  // ---------------------------------------------------------------------------
114
94
  type HeroTextProps = {
115
95
  eyebrow?: string
@@ -181,8 +161,9 @@ function HeroText({ eyebrow, headline, supportingText, priceText, modes }: HeroT
181
161
  }
182
162
 
183
163
  /**
184
- * FullscreenModal — a full-screen takeover surface with a parallax media hero,
185
- * a scrollable body, a floating close button, and a sticky `ActionFooter`.
164
+ * FullscreenModal — a full-screen takeover surface with a full-bleed media
165
+ * hero, a scrollable body, a floating close button, and a sticky
166
+ * `ActionFooter`.
186
167
  *
187
168
  * The component always themes itself with `context5: 'Fullscreen Modal'`
188
169
  * (non-overridable) so every nested component (Section, ListItem, Button,
@@ -190,14 +171,12 @@ function HeroText({ eyebrow, headline, supportingText, priceText, modes }: HeroT
190
171
  * That mode is cascaded into `children`, the footer, and the hero text via
191
172
  * `cloneChildrenWithModes` / the merged `modes` object.
192
173
  *
193
- * ### Parallax
194
- * As the user scrolls up, the hero collapses by **height only** (from
195
- * `heroHeight` to `heroMinHeight`) its **full width is always preserved**.
196
- * The `heroMedia` is pinned to the top at a fixed size and `cover`-cropped by
197
- * the collapsing clip, so it keeps a perfect aspect ratio the whole time
198
- * (never scaled or squished). Because it collapses slower than the content
199
- * scrolls, the media lags behind for the parallax depth cue. Disable with
200
- * `parallax={false}`.
174
+ * ### Hero
175
+ * The `heroMedia` is rendered full modal width inside the scroll body and
176
+ * takes its height from its own aspect ratio. The hero text (eyebrow /
177
+ * headline / supporting / price) is overlaid on top, anchored to the bottom.
178
+ * The whole hero scrolls together with the rest of the content — there is no
179
+ * parallax effect.
201
180
  *
202
181
  * @component
203
182
  * @example
@@ -207,7 +186,7 @@ function HeroText({ eyebrow, headline, supportingText, priceText, modes }: HeroT
207
186
  * headline="Get more from your money."
208
187
  * supportingText="JioFinance+ is your upgraded financial experience…"
209
188
  * priceText="₹999/year · ₹0 until 2027"
210
- * heroMedia={<LottiePlayer source={hero} size={{ width: 360, height: 420 }} />}
189
+ * heroMedia={<Image imageSource={hero} ratio={3 / 4} />}
211
190
  * primaryActionLabel="Upgrade for free"
212
191
  * disclaimer="By upgrading, we'll check your eligibility with Experian."
213
192
  * onPrimaryAction={() => upgrade()}
@@ -225,8 +204,6 @@ function FullscreenModal({
225
204
  priceText = '₹999/year · ₹0 until 2027',
226
205
  heroMedia,
227
206
  heroHeight = 420,
228
- heroMinHeight,
229
- parallax = true,
230
207
  showClose = true,
231
208
  onClose,
232
209
  closeAccessibilityLabel = 'Close',
@@ -252,28 +229,6 @@ function FullscreenModal({
252
229
 
253
230
  const rootGap = Number(getVariableByName('fullScreenModal/gap', modes)) || 16
254
231
 
255
- const minHeight = heroMinHeight ?? Math.round(heroHeight * HERO_MIN_HEIGHT_RATIO)
256
-
257
- const scrollY = useSharedValue(0)
258
- const onScroll = useAnimatedScrollHandler((event) => {
259
- scrollY.value = event.contentOffset.y
260
- })
261
-
262
- // Collapse the hero by HEIGHT only as the user scrolls up. The clip's width
263
- // never changes and the media inside is pinned full-size at the top, so the
264
- // art is cropped (cover) rather than scaled or narrowed — it keeps a perfect
265
- // aspect ratio the whole time. Pull-down (negative offset) is clamped, so the
266
- // hero never grows past its resting height.
267
- const heroAnimatedStyle = useAnimatedStyle(() => {
268
- const height = interpolate(
269
- scrollY.value,
270
- [0, heroHeight],
271
- [heroHeight, minHeight],
272
- Extrapolation.CLAMP
273
- )
274
- return { height }
275
- })
276
-
277
232
  const processedHeroMedia = useMemo(
278
233
  () =>
279
234
  heroMedia ? cloneChildrenWithModes(heroMedia, modes, FULLSCREEN_MODAL_FORCED_MODES) : null,
@@ -286,37 +241,11 @@ function FullscreenModal({
286
241
  [children, modes]
287
242
  )
288
243
 
289
- // The clip is full-width and top-pinned; its height is what animates. Width
290
- // is intentionally never animated.
291
- const heroClipBaseStyle = useMemo<ViewStyle>(
292
- () => ({
293
- position: 'absolute',
294
- top: 0,
295
- left: 0,
296
- right: 0,
297
- overflow: 'hidden',
298
- }),
299
- []
300
- )
301
-
302
- // The media sits at a fixed full-size box pinned to the top of the clip, so
303
- // the collapsing clip crops it from the bottom (cover) instead of resizing
304
- // it. Full width, fixed height — a perfect, constant aspect ratio.
305
- const heroMediaWrapStyle = useMemo<ViewStyle>(
244
+ // No-media fallback: without hero media the text region needs an explicit
245
+ // resting height (driven by `heroHeight`) so the hero still has presence.
246
+ const heroTextFallbackStyle = useMemo<ViewStyle>(
306
247
  () => ({
307
- position: 'absolute',
308
- top: 0,
309
- left: 0,
310
- right: 0,
311
- height: heroHeight,
312
- alignItems: 'stretch',
313
- }),
314
- [heroHeight]
315
- )
316
-
317
- const heroTextRegionStyle = useMemo<ViewStyle>(
318
- () => ({
319
- height: heroHeight,
248
+ minHeight: heroHeight,
320
249
  justifyContent: 'flex-end',
321
250
  paddingHorizontal: 16,
322
251
  paddingBottom: 16,
@@ -337,13 +266,29 @@ function FullscreenModal({
337
266
  [backgroundColor, rootGap, contentContainerStyle]
338
267
  )
339
268
 
340
- const heroClip = (
341
- <Animated.View
342
- style={[heroClipBaseStyle, parallax ? heroAnimatedStyle : { height: heroHeight }]}
343
- pointerEvents="none"
344
- >
345
- <View style={heroMediaWrapStyle}>{processedHeroMedia}</View>
346
- </Animated.View>
269
+ const heroTextNode = (
270
+ <HeroText
271
+ eyebrow={eyebrow}
272
+ headline={headline}
273
+ supportingText={supportingText}
274
+ priceText={priceText}
275
+ modes={modes}
276
+ />
277
+ )
278
+
279
+ // The hero scrolls inline with the body (no parallax). When media is present
280
+ // it is laid out full modal width and takes its height from its own aspect
281
+ // ratio; the hero text is overlaid on top, anchored to the bottom. Without
282
+ // media the text simply renders in flow at the fallback height.
283
+ const hero = processedHeroMedia ? (
284
+ <View style={heroMediaContainerStyle}>
285
+ {processedHeroMedia}
286
+ <View style={heroTextOverlayStyle} pointerEvents="box-none">
287
+ {heroTextNode}
288
+ </View>
289
+ </View>
290
+ ) : (
291
+ <View style={heroTextFallbackStyle}>{heroTextNode}</View>
347
292
  )
348
293
 
349
294
  // Footer: a fully custom node, or the default Button + Disclaimer column.
@@ -366,29 +311,17 @@ function FullscreenModal({
366
311
 
367
312
  return (
368
313
  <View style={[rootStyle, { backgroundColor }, style]} testID={testID}>
369
- {processedHeroMedia ? heroClip : null}
370
-
371
- <AnimatedScrollView
314
+ <ScrollView
372
315
  style={scrollViewStyle}
373
316
  contentContainerStyle={scrollContentStyle}
374
317
  showsVerticalScrollIndicator={false}
375
- onScroll={onScroll}
376
- scrollEventThrottle={16}
377
318
  // Tap an input in the body and it focuses on the FIRST tap, even when
378
319
  // the keyboard is already open (default 'never' eats that tap).
379
320
  keyboardShouldPersistTaps="handled"
380
321
  >
381
- <View style={heroTextRegionStyle}>
382
- <HeroText
383
- eyebrow={eyebrow}
384
- headline={headline}
385
- supportingText={supportingText}
386
- priceText={priceText}
387
- modes={modes}
388
- />
389
- </View>
322
+ {hero}
390
323
  <View style={bodyStyle}>{processedChildren}</View>
391
- </AnimatedScrollView>
324
+ </ScrollView>
392
325
 
393
326
  {footerContent ? (
394
327
  <ActionFooter modes={modes}>{footerContent}</ActionFooter>
@@ -413,5 +346,14 @@ const scrollViewStyle: ViewStyle = { flex: 1 }
413
346
  const scrollContentStyle: ViewStyle = { flexGrow: 1 }
414
347
  const fullWidthStyle: ViewStyle = { width: '100%' }
415
348
  const closeButtonStyle: ViewStyle = { position: 'absolute', top: 12, right: 12 }
349
+ // Full-width hero wrapper; height comes from the media's own aspect ratio.
350
+ const heroMediaContainerStyle: ViewStyle = { width: '100%', position: 'relative' }
351
+ // Hero text overlaid on the media, anchored to the bottom edge.
352
+ const heroTextOverlayStyle: ViewStyle = {
353
+ ...StyleSheet.absoluteFillObject,
354
+ justifyContent: 'flex-end',
355
+ paddingHorizontal: 16,
356
+ paddingBottom: 16,
357
+ }
416
358
 
417
359
  export default FullscreenModal
@@ -38,6 +38,7 @@ export { default as CircularProgressBarDoted, type CircularProgressBarDotedProps
38
38
  export { default as CircularRating, type CircularRatingProps } from './CircularRating/CircularRating'
39
39
  export { default as CoverageRing, type CoverageRingProps } from './CoverageRing/CoverageRing'
40
40
  export { default as CoverageBarComparison, type CoverageBarComparisonProps, type CoverageBarComparisonItem } from './CoverageBarComparison/CoverageBarComparison'
41
+ export { default as AllocationComparisonChart, type AllocationComparisonChartProps, type AllocationSegment } from './AllocationComparisonChart/AllocationComparisonChart'
41
42
  export { default as MonthlyStatusGrid, CalendarGlyph, type MonthlyStatusGridProps, type MonthlyStatusGridMonth, type MonthlyStatus, type CalendarGlyphProps } from './MonthlyStatusGrid/MonthlyStatusGrid'
42
43
  export { default as Gauge, type GaugeProps } from './Gauge/Gauge';
43
44
  export { default as HoldingsCard, type HoldingsCardProps } from './HoldingsCard/HoldingsCard';
@@ -4,7 +4,7 @@
4
4
  * Auto-generated from SVG files in src/icons/
5
5
  * DO NOT EDIT MANUALLY - Run "npm run icons:generate" to regenerate
6
6
  *
7
- * Generated: 2026-06-02T15:42:11.888Z
7
+ * Generated: 2026-06-03T15:59:02.370Z
8
8
  */
9
9
 
10
10
  // Icon name to SVG data mapping