jfs-components 0.0.68 → 0.0.69

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.
@@ -36,11 +36,20 @@ export interface MediaCardProps {
36
36
  /**
37
37
  * MediaCard component implementation from Figma node 1241:4140.
38
38
  *
39
- * Features a background media slot, a large title, and a glass-morphism footer.
40
- *
41
- * The background can be supplied either as `imageSource` (preferred — uses
42
- * the shared `<Image>` primitive under the hood) or as a custom `media` node
43
- * for non-image backgrounds.
39
+ * Layout contract (important read this before editing):
40
+ * - The **background** (image or custom `media`) is the only child in
41
+ * normal flow. It dictates the card's height typically via
42
+ * `aspectRatio` on the inner `<Image>`. There is no `minHeight`.
43
+ * - `Header` and `Footer` are **absolutely positioned overlays**:
44
+ * - `Header` pinned to top-left/right with safe padding.
45
+ * - `Footer` pinned to bottom-left/right with `zIndex: 2` so it sits
46
+ * on top of the header (and on top of the image). This guarantees
47
+ * the footer never moves no matter how many lines the title wraps
48
+ * to — the title may overflow the header bounds, but the footer's
49
+ * position is a function of the card box, not the title.
50
+ * - `pointerEvents="box-none"` is applied so taps still land on the
51
+ * interactive elements inside the overlays without the wrapper itself
52
+ * capturing them.
44
53
  */
45
54
  export declare function MediaCard({ imageSource, ratio, media, children, modes, style, }: MediaCardProps): import("react/jsx-runtime").JSX.Element;
46
55
  export declare namespace MediaCard {
@@ -51,9 +60,11 @@ export declare namespace MediaCard {
51
60
  var FooterSubtitle: typeof import("./MediaCard").FooterSubtitle;
52
61
  }
53
62
  /**
54
- * Header/Title Wrapper
55
- * It seems the title is just floating at the top with padding.
56
- * Figma: "title wrap" p-[16px]
63
+ * Header overlay — pinned to the top of the card. Title content can wrap to
64
+ * any number of lines without affecting the footer's position; if it grows
65
+ * taller than the card, the card's `overflow: 'hidden'` clips it.
66
+ *
67
+ * Default `padding: 16` matches the Figma "title wrap" spec.
57
68
  */
58
69
  export declare function Header({ children, style }: {
59
70
  children?: React.ReactNode;
@@ -69,8 +80,23 @@ export declare function Title({ children, style, modes: propModes }: {
69
80
  modes?: Record<string, any>;
70
81
  }): import("react/jsx-runtime").JSX.Element;
71
82
  /**
72
- * Glass Footer Component
73
- * Tokens: cardMedia/footer/*, glass/minimal, blur/minimal
83
+ * Glass Footer — pinned to the bottom of the card, **always** on top of the
84
+ * Header (`zIndex: 2`).
85
+ *
86
+ * Glass implementation (April 2026 best practice for RN/Expo):
87
+ * - **iOS:** `expo-blur`'s `BlurView` renders a native `UIVisualEffectView`,
88
+ * so this is a real OS-level live blur of whatever's underneath. We pick
89
+ * `tint` from the Figma "Contrast Context" mode (`'dark'` / `'light'`)
90
+ * and a moderate intensity that matches the Figma `blur/minimal` token.
91
+ * - **Android:** the same `BlurView` with `experimentalBlurMethod="dimezisBlurView"`
92
+ * enables the hardware-accelerated `RenderEffect` blur on Android 12+.
93
+ * On older Android, expo-blur cleanly degrades to a tinted scrim — we
94
+ * layer a subtle noise/grain overlay on top so the surface still reads
95
+ * as "frosted glass" instead of a flat color.
96
+ * - **Web:** `BlurView` on web is implemented as `backdrop-filter: blur()`,
97
+ * which already worked in the previous version. Same component, same API.
98
+ *
99
+ * Tokens still drive the tint color, blur radius and inner spacing.
74
100
  */
75
101
  export declare function Footer({ children, style, modes: propModes }: {
76
102
  children?: React.ReactNode;
@@ -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-04-22T12:06:55.739Z
7
+ * Generated: 2026-04-22T12:14:37.458Z
8
8
  */
9
9
  export declare const iconRegistry: Record<string, {
10
10
  path: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jfs-components",
3
- "version": "0.0.68",
3
+ "version": "0.0.69",
4
4
  "description": "React Native Jio Finance Components Library",
5
5
  "author": "sunshuaiqi@gmail.com",
6
6
  "license": "MIT",
@@ -86,6 +86,7 @@
86
86
  "ajv": "^8.17.1",
87
87
  "ajv-keywords": "^5.1.0",
88
88
  "expo": "^54.0.33",
89
+ "expo-blur": "~15.0.8",
89
90
  "patch-package": "^8.0.1",
90
91
  "react-native-gesture-handler": "^2.29.1",
91
92
  "react-native-reanimated": "3.18.1",
@@ -1,5 +1,6 @@
1
1
  import React, { createContext, useContext } from 'react'
2
2
  import { View, Text, StyleSheet, type ViewStyle, type TextStyle, type StyleProp, type ImageSourcePropType, Platform } from 'react-native'
3
+ import { BlurView, type BlurTint } from 'expo-blur'
3
4
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
4
5
  import Image from '../Image/Image'
5
6
  import { EMPTY_MODES } from '../../utils/react-utils'
@@ -43,11 +44,20 @@ export interface MediaCardProps {
43
44
  /**
44
45
  * MediaCard component implementation from Figma node 1241:4140.
45
46
  *
46
- * Features a background media slot, a large title, and a glass-morphism footer.
47
- *
48
- * The background can be supplied either as `imageSource` (preferred — uses
49
- * the shared `<Image>` primitive under the hood) or as a custom `media` node
50
- * for non-image backgrounds.
47
+ * Layout contract (important read this before editing):
48
+ * - The **background** (image or custom `media`) is the only child in
49
+ * normal flow. It dictates the card's height typically via
50
+ * `aspectRatio` on the inner `<Image>`. There is no `minHeight`.
51
+ * - `Header` and `Footer` are **absolutely positioned overlays**:
52
+ * - `Header` pinned to top-left/right with safe padding.
53
+ * - `Footer` pinned to bottom-left/right with `zIndex: 2` so it sits
54
+ * on top of the header (and on top of the image). This guarantees
55
+ * the footer never moves no matter how many lines the title wraps
56
+ * to — the title may overflow the header bounds, but the footer's
57
+ * position is a function of the card box, not the title.
58
+ * - `pointerEvents="box-none"` is applied so taps still land on the
59
+ * interactive elements inside the overlays without the wrapper itself
60
+ * capturing them.
51
61
  */
52
62
  export function MediaCard({
53
63
  imageSource,
@@ -59,20 +69,12 @@ export function MediaCard({
59
69
  }: MediaCardProps) {
60
70
  const radius = parseFloat(getVariableByName('cardMedia/radius', modes) || '24')
61
71
 
62
- // No magic minHeight, no aspectRatio on the container. The card simply
63
- // hugs whatever the background renders at: the <Image> sits in normal
64
- // flow with `aspectRatio: ratio`, so its rendered height becomes the
65
- // card's height. Header and Footer are absolutely positioned overlays
66
- // and don't contribute to layout.
67
72
  const containerStyle: ViewStyle = {
68
73
  borderRadius: radius,
69
74
  overflow: 'hidden',
70
75
  position: 'relative',
71
76
  }
72
77
 
73
- // `media` wins as an escape hatch (gradient/video/etc.). Otherwise we
74
- // delegate to the shared <Image> for image-source backgrounds. The
75
- // background renders in normal flow so its height drives the card.
76
78
  const background = media ?? (
77
79
  imageSource != null ? (
78
80
  <Image
@@ -104,25 +106,28 @@ export function MediaCard({
104
106
  // ----------------------------------------------------------------------------
105
107
 
106
108
  /**
107
- * Header/Title Wrapper
108
- * It seems the title is just floating at the top with padding.
109
- * Figma: "title wrap" p-[16px]
109
+ * Header overlay — pinned to the top of the card. Title content can wrap to
110
+ * any number of lines without affecting the footer's position; if it grows
111
+ * taller than the card, the card's `overflow: 'hidden'` clips it.
112
+ *
113
+ * Default `padding: 16` matches the Figma "title wrap" spec.
110
114
  */
111
115
  export function Header({ children, style }: { children?: React.ReactNode; style?: StyleProp<ViewStyle> }) {
112
- // NOTE: the previous `flex: 1` shorthand expanded on Yoga (Android) to
113
- // `{ flexGrow: 1, flexShrink: 1, flexBasis: 0 }`. With `flexBasis: 0` the
114
- // Header has *no intrinsic floor*, so when MediaCard is placed inside a
115
- // height-unbounded parent — e.g. a Carousel slot whose contentContainer
116
- // is `alignItems: 'flex-start'` — Yoga's first measurement pass sizes
117
- // the Header at 0 and the card's overall height becomes non-deterministic.
118
- // On native this manifests as the card "over-stretching" vertically (the
119
- // same Yoga foot-gun we fixed in `CardCTA` rightWrap). Web hides it
120
- // because browsers honor `min-height: auto` on flex items. Use explicit
121
- // `flexGrow / flexShrink: 0 / flexBasis: 'auto'` so the Header is sized
122
- // to its content as a floor and only grows to consume the extra space
123
- // contributed by `MediaCard`'s `minHeight: 308`.
124
116
  return (
125
- <View style={[{ padding: 16, flexGrow: 1, flexShrink: 0, flexBasis: 'auto' }, style]}>
117
+ <View
118
+ style={[
119
+ {
120
+ position: 'absolute',
121
+ top: 0,
122
+ left: 0,
123
+ right: 0,
124
+ padding: 16,
125
+ zIndex: 1,
126
+ },
127
+ style,
128
+ ]}
129
+ pointerEvents="box-none"
130
+ >
126
131
  {children}
127
132
  </View>
128
133
  )
@@ -154,8 +159,23 @@ export function Title({ children, style, modes: propModes }: { children?: React.
154
159
  }
155
160
 
156
161
  /**
157
- * Glass Footer Component
158
- * Tokens: cardMedia/footer/*, glass/minimal, blur/minimal
162
+ * Glass Footer — pinned to the bottom of the card, **always** on top of the
163
+ * Header (`zIndex: 2`).
164
+ *
165
+ * Glass implementation (April 2026 best practice for RN/Expo):
166
+ * - **iOS:** `expo-blur`'s `BlurView` renders a native `UIVisualEffectView`,
167
+ * so this is a real OS-level live blur of whatever's underneath. We pick
168
+ * `tint` from the Figma "Contrast Context" mode (`'dark'` / `'light'`)
169
+ * and a moderate intensity that matches the Figma `blur/minimal` token.
170
+ * - **Android:** the same `BlurView` with `experimentalBlurMethod="dimezisBlurView"`
171
+ * enables the hardware-accelerated `RenderEffect` blur on Android 12+.
172
+ * On older Android, expo-blur cleanly degrades to a tinted scrim — we
173
+ * layer a subtle noise/grain overlay on top so the surface still reads
174
+ * as "frosted glass" instead of a flat color.
175
+ * - **Web:** `BlurView` on web is implemented as `backdrop-filter: blur()`,
176
+ * which already worked in the previous version. Same component, same API.
177
+ *
178
+ * Tokens still drive the tint color, blur radius and inner spacing.
159
179
  */
160
180
  export function Footer({ children, style, modes: propModes }: { children?: React.ReactNode; style?: StyleProp<ViewStyle>; modes?: Record<string, any> }) {
161
181
  const context = useContext(MediaCardContext)
@@ -165,28 +185,77 @@ export function Footer({ children, style, modes: propModes }: { children?: React
165
185
  const paddingHorizontal = parseFloat(getVariableByName('cardMedia/footer/padding/horizontal', modes) || '16')
166
186
  const paddingVertical = parseFloat(getVariableByName('cardMedia/footer/padding/vertical', modes) || '12')
167
187
 
168
- // Glass Effect
169
- // Figma:
170
- // blur/minimal/background: "#1414174a"
171
- // blur/minimal: 29
188
+ // Figma tokens:
189
+ // blur/minimal/background -> tint laid over the native blur
190
+ // blur/minimal -> blur radius (px). expo-blur takes a 0-100
191
+ // "intensity" instead of px; we map roughly:
192
+ // intensity ≈ clamp(radius * 1.7, 0, 100).
172
193
  const glassBgColor = getVariableByName('blur/minimal/background', modes) || '#1414174a'
173
194
  const blurRadius = parseFloat(getVariableByName('blur/minimal', modes) || '29')
195
+ const intensity = Math.max(0, Math.min(100, Math.round(blurRadius * 1.7)))
174
196
 
175
- const containerStyle: ViewStyle = {
176
- flexDirection: 'row',
177
- alignItems: 'center',
178
- gap,
179
- paddingHorizontal,
180
- paddingVertical,
181
- backgroundColor: glassBgColor,
182
- // Web-specific backdrop filter for glass effect
183
- // @ts-ignore
184
- ...(Platform.OS === 'web' ? { backdropFilter: `blur(${blurRadius}px)` } : {}),
185
- }
197
+ // Pick the iOS/Android material tint from "Contrast Context" mode so the
198
+ // glass adapts to dark/light backgrounds the same way the Figma tokens do.
199
+ const contrast = (modes['Contrast Context'] || 'on dark') as string
200
+ const tint: BlurTint = contrast === 'on light' ? 'light' : 'dark'
186
201
 
187
202
  return (
188
- <View style={[containerStyle, style]}>
189
- {children}
203
+ <View
204
+ style={[
205
+ {
206
+ position: 'absolute',
207
+ left: 0,
208
+ right: 0,
209
+ bottom: 0,
210
+ overflow: 'hidden',
211
+ // zIndex 2 ensures Footer always paints above Header,
212
+ // regardless of which is rendered first in the tree.
213
+ zIndex: 2,
214
+ },
215
+ style,
216
+ ]}
217
+ pointerEvents="box-none"
218
+ >
219
+ {/* Native live blur. On Android pre-12 expo-blur falls back to a
220
+ tinted scrim automatically; on web it's a backdrop-filter. */}
221
+ <BlurView
222
+ style={StyleSheet.absoluteFill}
223
+ tint={tint}
224
+ intensity={intensity}
225
+ experimentalBlurMethod="dimezisBlurView"
226
+ />
227
+
228
+ {/* Token-driven tint laid on top of the live blur — keeps the
229
+ Figma color signature regardless of platform blur quality. */}
230
+ <View style={[StyleSheet.absoluteFill, { backgroundColor: glassBgColor }]} />
231
+
232
+ {/* Subtle noise/grain on Android only, to compensate for the
233
+ lower-fidelity blur — purely additive, no behavior change.
234
+ On iOS/web the native blur already has natural texture. */}
235
+ {Platform.OS === 'android' ? (
236
+ <View
237
+ style={[
238
+ StyleSheet.absoluteFill,
239
+ {
240
+ backgroundColor: 'rgba(255,255,255,0.03)',
241
+ opacity: 0.6,
242
+ },
243
+ ]}
244
+ pointerEvents="none"
245
+ />
246
+ ) : null}
247
+
248
+ <View
249
+ style={{
250
+ flexDirection: 'row',
251
+ alignItems: 'center',
252
+ gap,
253
+ paddingHorizontal,
254
+ paddingVertical,
255
+ }}
256
+ >
257
+ {children}
258
+ </View>
190
259
  </View>
191
260
  )
192
261
  }
@@ -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-04-22T12:06:55.739Z
7
+ * Generated: 2026-04-22T12:14:37.458Z
8
8
  */
9
9
 
10
10
  // Icon name to SVG data mapping