jfs-components 0.1.17 → 0.1.19

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/lib/commonjs/components/AvatarGroup/AvatarGroup.js +41 -20
  3. package/lib/commonjs/components/Card/Card.js +29 -12
  4. package/lib/commonjs/components/CardFeedback/CardFeedback.js +16 -2
  5. package/lib/commonjs/components/CardInsight/CardInsight.js +34 -13
  6. package/lib/commonjs/components/CardProviderInfo/CardProviderInfo.js +23 -3
  7. package/lib/commonjs/components/DebitCard/DebitCard.js +23 -3
  8. package/lib/commonjs/components/MediaCard/MediaCard.js +24 -7
  9. package/lib/commonjs/components/ProductMerchandisingCard/ProductMerchandisingCard.js +64 -60
  10. package/lib/commonjs/components/RechargeCard/RechargeCard.js +34 -13
  11. package/lib/commonjs/components/TestimonialsCard/TestimonialsCard.js +23 -5
  12. package/lib/commonjs/icons/registry.js +1 -1
  13. package/lib/commonjs/utils/GlassFill/GlassFill.js +69 -73
  14. package/lib/module/components/AvatarGroup/AvatarGroup.js +43 -21
  15. package/lib/module/components/Card/Card.js +31 -14
  16. package/lib/module/components/CardFeedback/CardFeedback.js +17 -3
  17. package/lib/module/components/CardInsight/CardInsight.js +36 -15
  18. package/lib/module/components/CardProviderInfo/CardProviderInfo.js +25 -5
  19. package/lib/module/components/DebitCard/DebitCard.js +25 -5
  20. package/lib/module/components/MediaCard/MediaCard.js +26 -9
  21. package/lib/module/components/ProductMerchandisingCard/ProductMerchandisingCard.js +66 -62
  22. package/lib/module/components/RechargeCard/RechargeCard.js +36 -15
  23. package/lib/module/components/TestimonialsCard/TestimonialsCard.js +25 -7
  24. package/lib/module/icons/registry.js +1 -1
  25. package/lib/module/utils/GlassFill/GlassFill.js +69 -72
  26. package/lib/typescript/src/components/Card/Card.d.ts +10 -1
  27. package/lib/typescript/src/components/CardFeedback/CardFeedback.d.ts +10 -1
  28. package/lib/typescript/src/components/CardInsight/CardInsight.d.ts +10 -1
  29. package/lib/typescript/src/components/CardProviderInfo/CardProviderInfo.d.ts +10 -1
  30. package/lib/typescript/src/components/DebitCard/DebitCard.d.ts +10 -1
  31. package/lib/typescript/src/components/MediaCard/MediaCard.d.ts +10 -1
  32. package/lib/typescript/src/components/RechargeCard/RechargeCard.d.ts +10 -1
  33. package/lib/typescript/src/components/TestimonialsCard/TestimonialsCard.d.ts +8 -1
  34. package/lib/typescript/src/icons/registry.d.ts +1 -1
  35. package/lib/typescript/src/utils/GlassFill/GlassFill.d.ts +11 -4
  36. package/package.json +1 -2
  37. package/src/components/AvatarGroup/AvatarGroup.tsx +44 -18
  38. package/src/components/Card/Card.tsx +46 -15
  39. package/src/components/CardFeedback/CardFeedback.tsx +29 -4
  40. package/src/components/CardInsight/CardInsight.tsx +48 -17
  41. package/src/components/CardProviderInfo/CardProviderInfo.tsx +36 -3
  42. package/src/components/DebitCard/DebitCard.tsx +36 -3
  43. package/src/components/MediaCard/MediaCard.tsx +40 -9
  44. package/src/components/ProductMerchandisingCard/ProductMerchandisingCard.tsx +76 -47
  45. package/src/components/RechargeCard/RechargeCard.tsx +48 -13
  46. package/src/components/TestimonialsCard/TestimonialsCard.tsx +37 -6
  47. package/src/icons/registry.ts +1 -1
  48. package/src/utils/GlassFill/GlassFill.tsx +59 -56
@@ -1,5 +1,5 @@
1
1
  import React, { createContext, useContext, isValidElement, cloneElement } from 'react';
2
- import { View, Text, StyleSheet, type ViewStyle, type TextStyle, type StyleProp, Image } from 'react-native';
2
+ import { View, Text, Pressable, type ViewStyle, type TextStyle, type StyleProp, Image } from 'react-native';
3
3
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
4
4
  import IconComponent from '../../icons/Icon';
5
5
  import type { Modes } from '../../design-tokens'
@@ -22,6 +22,15 @@ export interface CardFeedbackProps {
22
22
  * Style overrides for the card container.
23
23
  */
24
24
  style?: StyleProp<ViewStyle>;
25
+ /**
26
+ * Press handler for the whole card. When set, the card becomes pressable
27
+ * (rendered through a `Pressable` with `accessibilityRole="button"`).
28
+ */
29
+ onPress?: () => void;
30
+ /** Disable interaction when `onPress` is set. */
31
+ disabled?: boolean;
32
+ /** Accessibility label forwarded to the pressable wrapper. */
33
+ accessibilityLabel?: string;
25
34
  }
26
35
 
27
36
  /**
@@ -33,6 +42,9 @@ export function CardFeedback({
33
42
  children,
34
43
  modes: propModes,
35
44
  style,
45
+ onPress,
46
+ disabled,
47
+ accessibilityLabel,
36
48
  }: CardFeedbackProps) {
37
49
  const modes = {
38
50
  'Appearance.System': 'positive',
@@ -61,9 +73,22 @@ export function CardFeedback({
61
73
 
62
74
  return (
63
75
  <CardFeedbackContext.Provider value={{ modes }}>
64
- <View style={[containerStyle, style]}>
65
- {children}
66
- </View>
76
+ {onPress ? (
77
+ <Pressable
78
+ style={[containerStyle, style]}
79
+ onPress={onPress}
80
+ disabled={disabled}
81
+ accessibilityRole="button"
82
+ accessibilityLabel={accessibilityLabel}
83
+ accessibilityState={{ disabled: !!disabled }}
84
+ >
85
+ {children}
86
+ </Pressable>
87
+ ) : (
88
+ <View style={[containerStyle, style]} accessibilityLabel={accessibilityLabel}>
89
+ {children}
90
+ </View>
91
+ )}
67
92
  </CardFeedbackContext.Provider>
68
93
  );
69
94
  }
@@ -2,6 +2,7 @@ import React from 'react'
2
2
  import {
3
3
  View,
4
4
  Text,
5
+ Pressable,
5
6
  type StyleProp,
6
7
  type ViewStyle,
7
8
  type TextStyle,
@@ -45,6 +46,15 @@ export type CardInsightProps = {
45
46
  modes?: Modes
46
47
  /** Override container styles. */
47
48
  style?: StyleProp<ViewStyle>
49
+ /**
50
+ * Press handler for the whole card. When set, the card becomes pressable
51
+ * (rendered through a `Pressable` with `accessibilityRole="button"`).
52
+ */
53
+ onPress?: () => void
54
+ /** Disable interaction when `onPress` is set. */
55
+ disabled?: boolean
56
+ /** Accessibility label forwarded to the pressable wrapper (defaults to `title`). */
57
+ accessibilityLabel?: string
48
58
  }
49
59
 
50
60
  /**
@@ -67,6 +77,9 @@ function CardInsight({
67
77
  divider,
68
78
  modes = EMPTY_MODES,
69
79
  style,
80
+ onPress,
81
+ disabled,
82
+ accessibilityLabel,
70
83
  }: CardInsightProps) {
71
84
  const background =
72
85
  (getVariableByName('cardInsight/background', modes) as string | null) ?? '#ffffff'
@@ -160,23 +173,20 @@ function CardInsight({
160
173
  const footerNode = renderFooter()
161
174
  const showDivider = footerNode !== null && divider !== false && divider !== null
162
175
 
163
- return (
164
- <View
165
- style={[
166
- {
167
- backgroundColor: background,
168
- borderRadius: radius,
169
- borderWidth,
170
- borderColor,
171
- paddingHorizontal,
172
- paddingVertical,
173
- gap,
174
- overflow: 'hidden',
175
- alignItems: 'stretch',
176
- },
177
- style,
178
- ]}
179
- >
176
+ const containerStyle: ViewStyle = {
177
+ backgroundColor: background,
178
+ borderRadius: radius,
179
+ borderWidth,
180
+ borderColor,
181
+ paddingHorizontal,
182
+ paddingVertical,
183
+ gap,
184
+ overflow: 'hidden',
185
+ alignItems: 'stretch',
186
+ }
187
+
188
+ const content = (
189
+ <>
180
190
  <View
181
191
  style={{
182
192
  flexDirection: 'row',
@@ -237,6 +247,27 @@ function CardInsight({
237
247
  {showDivider ? renderDivider() : null}
238
248
 
239
249
  {footerNode}
250
+ </>
251
+ )
252
+
253
+ if (onPress) {
254
+ return (
255
+ <Pressable
256
+ style={[containerStyle, style]}
257
+ onPress={onPress}
258
+ disabled={disabled}
259
+ accessibilityRole="button"
260
+ accessibilityLabel={accessibilityLabel ?? title}
261
+ accessibilityState={{ disabled: !!disabled }}
262
+ >
263
+ {content}
264
+ </Pressable>
265
+ )
266
+ }
267
+
268
+ return (
269
+ <View style={[containerStyle, style]} accessibilityLabel={accessibilityLabel}>
270
+ {content}
240
271
  </View>
241
272
  )
242
273
  }
@@ -1,5 +1,5 @@
1
1
  import React from 'react'
2
- import { View, type ViewStyle, type StyleProp, type ImageSourcePropType } from 'react-native'
2
+ import { View, Pressable, type ViewStyle, type StyleProp, type ImageSourcePropType } from 'react-native'
3
3
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
4
4
  import ProductLabel from '../ProductLabel/ProductLabel'
5
5
  import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
@@ -16,6 +16,15 @@ export type CardProviderInfoProps = {
16
16
  modes?: Modes
17
17
  /** Override container styles. */
18
18
  style?: StyleProp<ViewStyle>
19
+ /**
20
+ * Press handler for the whole card. When set, the card becomes pressable
21
+ * (rendered through a `Pressable` with `accessibilityRole="button"`).
22
+ */
23
+ onPress?: () => void
24
+ /** Disable interaction when `onPress` is set. */
25
+ disabled?: boolean
26
+ /** Accessibility label forwarded to the pressable wrapper (defaults to `label`). */
27
+ accessibilityLabel?: string
19
28
  }
20
29
 
21
30
  /**
@@ -31,6 +40,9 @@ function CardProviderInfo({
31
40
  children,
32
41
  modes = EMPTY_MODES,
33
42
  style,
43
+ onPress,
44
+ disabled,
45
+ accessibilityLabel,
34
46
  }: CardProviderInfoProps) {
35
47
  const background = getVariableByName('card/providerInfo/background', modes) ?? '#fef4e5'
36
48
  const border = getVariableByName('card/providerInfo/border', modes) ?? '#fef4e5'
@@ -59,8 +71,8 @@ function CardProviderInfo({
59
71
  rows.push(childArray.slice(i, i + 2))
60
72
  }
61
73
 
62
- return (
63
- <View style={[containerStyle, style]}>
74
+ const content = (
75
+ <>
64
76
  <ProductLabel label={label} imageSource={imageSource} modes={modes} />
65
77
  {childArray.length > 0 && (
66
78
  <View style={{ rowGap: gridGapNum }}>
@@ -75,6 +87,27 @@ function CardProviderInfo({
75
87
  ))}
76
88
  </View>
77
89
  )}
90
+ </>
91
+ )
92
+
93
+ if (onPress) {
94
+ return (
95
+ <Pressable
96
+ style={[containerStyle, style]}
97
+ onPress={onPress}
98
+ disabled={disabled}
99
+ accessibilityRole="button"
100
+ accessibilityLabel={accessibilityLabel ?? label}
101
+ accessibilityState={{ disabled: !!disabled }}
102
+ >
103
+ {content}
104
+ </Pressable>
105
+ )
106
+ }
107
+
108
+ return (
109
+ <View style={[containerStyle, style]} accessibilityLabel={accessibilityLabel}>
110
+ {content}
78
111
  </View>
79
112
  )
80
113
  }
@@ -1,5 +1,5 @@
1
1
  import React from 'react'
2
- import { View, Text, Image, type ViewStyle, type TextStyle, type ImageStyle, type StyleProp, type ImageSourcePropType } from 'react-native'
2
+ import { View, Text, Image, Pressable, type ViewStyle, type TextStyle, type ImageStyle, type StyleProp, type ImageSourcePropType } from 'react-native'
3
3
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
4
4
  import { useTokens } from '../../design-tokens/JFSThemeProvider'
5
5
  import { cloneChildrenWithModes, EMPTY_MODES } from '../../utils/react-utils'
@@ -28,6 +28,15 @@ export type DebitCardProps = {
28
28
  modes?: Modes;
29
29
  /** Container style overrides */
30
30
  style?: StyleProp<ViewStyle>;
31
+ /**
32
+ * Press handler for the whole card. When set, the card becomes pressable
33
+ * (rendered through a `Pressable` with `accessibilityRole="button"`).
34
+ */
35
+ onPress?: () => void;
36
+ /** Disable interaction when `onPress` is set. */
37
+ disabled?: boolean;
38
+ /** Accessibility label forwarded to the pressable wrapper. */
39
+ accessibilityLabel?: string;
31
40
  };
32
41
 
33
42
  function DebitCard({
@@ -41,6 +50,9 @@ function DebitCard({
41
50
  providerLogoSlot,
42
51
  modes: propModes = EMPTY_MODES,
43
52
  style,
53
+ onPress,
54
+ disabled,
55
+ accessibilityLabel,
44
56
  }: DebitCardProps) {
45
57
  const { modes: globalModes } = useTokens()
46
58
  const modes = { ...globalModes, ...propModes }
@@ -149,8 +161,8 @@ function DebitCard({
149
161
  return cardArtSource
150
162
  }, [cardArtSource])
151
163
 
152
- return (
153
- <View style={[containerStyle, style]}>
164
+ const content = (
165
+ <>
154
166
  <View style={headerStyle}>
155
167
  <View style={headerLeftStyle}>
156
168
  {bankLogoSlot ? cloneChildrenWithModes(bankLogoSlot, modes) : null}
@@ -179,6 +191,27 @@ function DebitCard({
179
191
  <Text style={textStyle}>{expireDate}</Text>
180
192
  </View>
181
193
  </View>
194
+ </>
195
+ )
196
+
197
+ if (onPress) {
198
+ return (
199
+ <Pressable
200
+ style={[containerStyle, style]}
201
+ onPress={onPress}
202
+ disabled={disabled}
203
+ accessibilityRole="button"
204
+ accessibilityLabel={accessibilityLabel ?? `${bankName} ${cardType} ${cardNumber}`}
205
+ accessibilityState={{ disabled: !!disabled }}
206
+ >
207
+ {content}
208
+ </Pressable>
209
+ )
210
+ }
211
+
212
+ return (
213
+ <View style={[containerStyle, style]} accessibilityLabel={accessibilityLabel}>
214
+ {content}
182
215
  </View>
183
216
  )
184
217
  }
@@ -1,5 +1,5 @@
1
1
  import React, { createContext, useContext } from 'react'
2
- import { View, Text, StyleSheet, type ViewStyle, type TextStyle, type StyleProp, type ImageSourcePropType } from 'react-native'
2
+ import { View, Text, Pressable, StyleSheet, type ViewStyle, type TextStyle, type StyleProp, type ImageSourcePropType } from 'react-native'
3
3
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
4
4
  import Image from '../Image/Image'
5
5
  import GlassFill, { type GlassTint } from '../../utils/GlassFill/GlassFill'
@@ -40,6 +40,15 @@ export interface MediaCardProps {
40
40
  * Style overrides for the card container.
41
41
  */
42
42
  style?: StyleProp<ViewStyle>
43
+ /**
44
+ * Press handler for the whole card. When set, the card becomes pressable
45
+ * (rendered through a `Pressable` with `accessibilityRole="button"`).
46
+ */
47
+ onPress?: () => void
48
+ /** Disable interaction when `onPress` is set. */
49
+ disabled?: boolean
50
+ /** Accessibility label forwarded to the pressable wrapper. */
51
+ accessibilityLabel?: string
43
52
  }
44
53
 
45
54
  /**
@@ -67,6 +76,9 @@ export function MediaCard({
67
76
  children,
68
77
  modes = EMPTY_MODES,
69
78
  style,
79
+ onPress,
80
+ disabled,
81
+ accessibilityLabel,
70
82
  }: MediaCardProps) {
71
83
  const radius = parseFloat(getVariableByName('cardMedia/radius', modes))
72
84
 
@@ -88,16 +100,35 @@ export function MediaCard({
88
100
  ) : null
89
101
  )
90
102
 
103
+ const content = (
104
+ <>
105
+ {background}
106
+ {children != null ? (
107
+ <View style={StyleSheet.absoluteFill} pointerEvents="box-none">
108
+ {children}
109
+ </View>
110
+ ) : null}
111
+ </>
112
+ )
113
+
91
114
  return (
92
115
  <MediaCardContext.Provider value={{ modes }}>
93
- <View style={[containerStyle, style]}>
94
- {background}
95
- {children != null ? (
96
- <View style={StyleSheet.absoluteFill} pointerEvents="box-none">
97
- {children}
98
- </View>
99
- ) : null}
100
- </View>
116
+ {onPress ? (
117
+ <Pressable
118
+ style={[containerStyle, style]}
119
+ onPress={onPress}
120
+ disabled={disabled}
121
+ accessibilityRole="button"
122
+ accessibilityLabel={accessibilityLabel}
123
+ accessibilityState={{ disabled: !!disabled }}
124
+ >
125
+ {content}
126
+ </Pressable>
127
+ ) : (
128
+ <View style={[containerStyle, style]} accessibilityLabel={accessibilityLabel}>
129
+ {content}
130
+ </View>
131
+ )}
101
132
  </MediaCardContext.Provider>
102
133
  )
103
134
  }
@@ -4,15 +4,21 @@ import {
4
4
  Text,
5
5
  Pressable,
6
6
  StyleSheet,
7
- Platform,
8
- Image as RNImage,
9
7
  type ViewStyle,
10
8
  type TextStyle,
11
9
  type StyleProp,
12
10
  type ImageSourcePropType,
13
11
  } from 'react-native'
14
- import MaskedView from '@react-native-masked-view/masked-view'
15
- import Svg, { Defs, LinearGradient, Stop, Rect } from 'react-native-svg'
12
+ import Svg, {
13
+ Defs,
14
+ LinearGradient,
15
+ Stop,
16
+ Rect,
17
+ Mask,
18
+ Filter,
19
+ FeGaussianBlur,
20
+ Image as SvgImage,
21
+ } from 'react-native-svg'
16
22
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
17
23
  import { EMPTY_MODES, resolveTruncation } from '../../utils/react-utils'
18
24
  import Avatar from '../Avatar/Avatar'
@@ -138,7 +144,9 @@ function ProductMerchandisingCard({
138
144
  const rawId = useId()
139
145
  const safeId = rawId.replace(/[^a-zA-Z0-9_-]/g, '')
140
146
  const scrimId = `pmc-footer-scrim-${safeId}`
147
+ const blurGradientId = `pmc-footer-blurgrad-${safeId}`
141
148
  const blurMaskId = `pmc-footer-blurmask-${safeId}`
149
+ const blurFilterId = `pmc-footer-blurfilter-${safeId}`
142
150
 
143
151
  const normalizedImageSource = useMemo(() => normalizeImageSource(imageSource), [imageSource])
144
152
 
@@ -234,41 +242,65 @@ function ProductMerchandisingCard({
234
242
 
235
243
  <View style={tokens.footer} pointerEvents="box-none">
236
244
  {/* Progressive blur — clear at the top of the footer, ramping to
237
- a soft blur at the bottom. Rather than the unmaintained native
238
- BlurView (which renders as a flat black tint on Android +
239
- Fabric and can hide the image on iOS), we reveal a *blurred
240
- copy of the card's own image* through a vertical gradient
241
- mask. `Image.blurRadius` is a built-in RN prop with a real
242
- Gaussian blur on BOTH iOS and Android and needs no native
243
- module. The blurred copy is pinned to the card's bottom at the
244
- full card height so it lines up pixel-for-pixel with the
245
- full-bleed background, and the mask makes the blur swell in
246
- smoothly toward the bottom (true progressive blur). */}
245
+ a soft blur at the bottom. We reveal a *blurred copy of the
246
+ card's own image* through a vertical gradient mask, rendered
247
+ entirely with `react-native-svg` (a Fabric/codegen-correct
248
+ native module that resolves on both the Old and New
249
+ architectures). The SVG `<Image>` is blurred by an
250
+ `<FeGaussianBlur>` filter (identical, resolution-independent
251
+ Gaussian on iOS, Android AND web) and revealed by a luminance
252
+ `<Mask>` fed a vertical gradient, so the blur swells in
253
+ smoothly toward the bottom. This deliberately avoids the
254
+ unmaintained `@react-native-masked-view/masked-view` (a legacy
255
+ Paper component that resolves to a plain object — and crashes
256
+ "Element type is invalid" — under the New Architecture interop)
257
+ and the flaky native `BlurView`. Because the mask is fully
258
+ transparent at the top, any minor crop mismatch between this
259
+ cover-fitted copy and the full-bleed background is invisible:
260
+ where they could differ, the blurred layer isn't drawn. */}
247
261
  {normalizedImageSource != null ? (
248
262
  <View style={StyleSheet.absoluteFill} pointerEvents="none">
249
- <MaskedView
250
- style={StyleSheet.absoluteFill}
251
- maskElement={
252
- <Svg width="100%" height="100%">
253
- <Defs>
254
- <LinearGradient id={blurMaskId} x1="0" y1="0" x2="0" y2="1">
255
- <Stop offset="0" stopColor="#000000" stopOpacity={0} />
256
- <Stop offset="0.35" stopColor="#000000" stopOpacity={0.5} />
257
- <Stop offset="0.7" stopColor="#000000" stopOpacity={0.92} />
258
- <Stop offset="1" stopColor="#000000" stopOpacity={1} />
259
- </LinearGradient>
260
- </Defs>
261
- <Rect x="0" y="0" width="100%" height="100%" fill={`url(#${blurMaskId})`} />
262
- </Svg>
263
- }
264
- >
265
- <RNImage
266
- source={normalizedImageSource}
267
- blurRadius={tokens.imageBlurRadius}
268
- resizeMode="cover"
269
- style={{ position: 'absolute', left: 0, right: 0, bottom: 0, width: '100%', height }}
263
+ <Svg width="100%" height="100%">
264
+ <Defs>
265
+ <Filter
266
+ id={blurFilterId}
267
+ x="-15%"
268
+ y="-15%"
269
+ width="130%"
270
+ height="130%"
271
+ >
272
+ <FeGaussianBlur
273
+ stdDeviation={tokens.imageBlurStdDeviation}
274
+ edgeMode="duplicate"
275
+ />
276
+ </Filter>
277
+ <LinearGradient id={blurGradientId} x1="0" y1="0" x2="0" y2="1">
278
+ <Stop offset="0" stopColor="#ffffff" stopOpacity={0} />
279
+ <Stop offset="0.35" stopColor="#ffffff" stopOpacity={0.5} />
280
+ <Stop offset="0.7" stopColor="#ffffff" stopOpacity={0.92} />
281
+ <Stop offset="1" stopColor="#ffffff" stopOpacity={1} />
282
+ </LinearGradient>
283
+ <Mask id={blurMaskId}>
284
+ <Rect
285
+ x="0"
286
+ y="0"
287
+ width="100%"
288
+ height="100%"
289
+ fill={`url(#${blurGradientId})`}
290
+ />
291
+ </Mask>
292
+ </Defs>
293
+ <SvgImage
294
+ href={normalizedImageSource as any}
295
+ x="0"
296
+ y="0"
297
+ width="100%"
298
+ height="100%"
299
+ preserveAspectRatio="xMidYMid slice"
300
+ filter={`url(#${blurFilterId})`}
301
+ mask={`url(#${blurMaskId})`}
270
302
  />
271
- </MaskedView>
303
+ </Svg>
272
304
  </View>
273
305
  ) : (
274
306
  <GlassFill tint="dark" intensity={tokens.blurIntensity} progressive />
@@ -348,7 +380,7 @@ const SPECIAL_BADGE_INTENSITY = 17 // ~5px CSS blur / ~2 native blurAmount
348
380
  interface ResolvedTokens {
349
381
  radius: number
350
382
  blurIntensity: number
351
- imageBlurRadius: number
383
+ imageBlurStdDeviation: number
352
384
  header: ViewStyle
353
385
  footer: ViewStyle
354
386
  textWrap: ViewStyle
@@ -394,20 +426,17 @@ function resolveTokens(modes: Modes): ResolvedTokens {
394
426
  const sbFamily = asStr(getVariableByName('productMerchandisingcard/specialbadge/text/fontfamily', modes), 'JioType Var')
395
427
  const sbWeight = asStr(getVariableByName('productMerchandisingcard/specialbadge/text/fontweight', modes), '500')
396
428
 
397
- // `Image.blurRadius` is interpreted differently per platform — iOS applies a
398
- // strong Gaussian while Android's IterativeBoxBlur is gentler at the same
399
- // numeric radius so we tune each platform to land on the same perceived
400
- // "minimal" frosted strength as the Figma design.
401
- const imageBlurRadius = Platform.select({
402
- ios: Math.max(6, Math.round(blurRadius * 0.5)),
403
- android: Math.max(12, Math.round(blurRadius * 0.95)),
404
- default: Math.max(8, Math.round(blurRadius * 0.6)),
405
- }) as number
429
+ // SVG `<FeGaussianBlur stdDeviation>` is resolution-independent and renders
430
+ // an identical Gaussian on iOS, Android and web so unlike the old
431
+ // `Image.blurRadius` path it needs no per-platform tuning. `stdDeviation` is
432
+ // roughly half a perceptual "blur radius", so we scale the `blur/minimal`
433
+ // token down to land on the same subtle frosted strength as the design.
434
+ const imageBlurStdDeviation = Math.max(4, Math.round(blurRadius * 0.35))
406
435
 
407
436
  return {
408
437
  radius,
409
438
  blurIntensity: Math.max(0, Math.min(100, Math.round(blurRadius * BLUR_INTENSITY_FACTOR))),
410
- imageBlurRadius,
439
+ imageBlurStdDeviation,
411
440
  header: {
412
441
  flexDirection: 'row',
413
442
  alignItems: 'flex-start',
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { View, Text, type ViewStyle } from 'react-native';
2
+ import { View, Text, Pressable, type ViewStyle } from 'react-native';
3
3
  import ButtonGroup from '../ButtonGroup/ButtonGroup';
4
4
  import AvatarGroup from '../AvatarGroup/AvatarGroup';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
@@ -65,6 +65,15 @@ export type RechargeCardProps = {
65
65
  */
66
66
  modes?: Modes;
67
67
  style?: ViewStyle;
68
+ /**
69
+ * Press handler for the whole card. When set, the card becomes pressable
70
+ * (rendered through a `Pressable` with `accessibilityRole="button"`).
71
+ */
72
+ onPress?: () => void;
73
+ /** Disable interaction when `onPress` is set. */
74
+ disabled?: boolean;
75
+ /** Accessibility label forwarded to the pressable wrapper (defaults to `title`). */
76
+ accessibilityLabel?: string;
68
77
  };
69
78
 
70
79
  /**
@@ -81,6 +90,9 @@ export default function RechargeCard({
81
90
  actions,
82
91
  modes = EMPTY_MODES,
83
92
  style,
93
+ onPress,
94
+ disabled,
95
+ accessibilityLabel,
84
96
  }: RechargeCardProps) {
85
97
  // Container Tokens (defaults mirror Figma node 2235:937).
86
98
  const backgroundColor = getVariableByName('rechargeCard/background', modes);
@@ -132,18 +144,20 @@ export default function RechargeCard({
132
144
  const hasSubscriptions = React.Children.count(subscriptionContent) > 0;
133
145
 
134
146
 
135
- return (
136
- <View style={[{
137
- backgroundColor,
138
- paddingHorizontal,
139
- paddingVertical,
140
- gap,
141
- borderRadius: radius,
142
- borderWidth: strokeWidth,
143
- borderColor: strokeColor,
144
- minWidth,
145
- alignItems: 'flex-start',
146
- }, style]}>
147
+ const containerStyle: ViewStyle = {
148
+ backgroundColor,
149
+ paddingHorizontal,
150
+ paddingVertical,
151
+ gap,
152
+ borderRadius: radius,
153
+ borderWidth: strokeWidth,
154
+ borderColor: strokeColor,
155
+ minWidth,
156
+ alignItems: 'flex-start',
157
+ };
158
+
159
+ const content = (
160
+ <>
147
161
  {/* Header */}
148
162
  <View style={{ gap: headerGap, alignItems: 'flex-start' }}>
149
163
  <Text style={{
@@ -244,6 +258,27 @@ export default function RechargeCard({
244
258
  <ButtonGroup modes={{ ...DEFAULT_BUTTON_GROUP_MODES, ...modes }}>
245
259
  {actions}
246
260
  </ButtonGroup>
261
+ </>
262
+ );
263
+
264
+ if (onPress) {
265
+ return (
266
+ <Pressable
267
+ style={[containerStyle, style]}
268
+ onPress={onPress}
269
+ disabled={disabled}
270
+ accessibilityRole="button"
271
+ accessibilityLabel={accessibilityLabel ?? title}
272
+ accessibilityState={{ disabled: !!disabled }}
273
+ >
274
+ {content}
275
+ </Pressable>
276
+ );
277
+ }
278
+
279
+ return (
280
+ <View style={[containerStyle, style]} accessibilityLabel={accessibilityLabel}>
281
+ {content}
247
282
  </View>
248
283
  );
249
284
  }