jfs-components 0.1.17 → 0.1.18

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,7 +3,6 @@
3
3
  import React, { useId } from 'react';
4
4
  import { View, StyleSheet, Platform, UIManager } from 'react-native';
5
5
  import { BlurView } from '@react-native-community/blur';
6
- import MaskedView from '@react-native-masked-view/masked-view';
7
6
  import Svg, { Defs, LinearGradient, Stop, Rect } from 'react-native-svg';
8
7
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
8
  const DEFAULT_FALLBACK_DARK = '#1414174a';
@@ -14,32 +13,30 @@ const DEFAULT_FALLBACK_LIGHT = '#ffffff66';
14
13
  const NATIVE_BLUR_NAME = Platform.OS === 'android' ? 'AndroidBlurView' : 'BlurView';
15
14
 
16
15
  /**
17
- * Alpha stop on a layer's vertical reveal mask. `offset` is the vertical
18
- * position (0 = top of the surface, 1 = bottom); `opacity` is the mask alpha
19
- * there (0 = layer hidden / fully clear, 1 = layer fully applied).
16
+ * Alpha stop on the no-native-blur fallback gradient. `offset` is the vertical
17
+ * position (0 = top of the surface, 1 = bottom); `opacity` is the tint alpha
18
+ * there (0 = fully clear, 1 = full fallback color).
20
19
  *
21
- * Using several stops (rather than a single linear 0 → 1 ramp) lets each layer
22
- * EASE in, so the blur swells smoothly toward the bottom instead of appearing
23
- * along a hard horizontal seam. That soft S-curve is what gives the surface its
24
- * "glass" feel rather than a flat translucent panel.
20
+ * Using several stops (rather than a single linear 0 → 1 ramp) eases the tint
21
+ * in, so it swells smoothly toward the bottom instead of along a hard seam —
22
+ * the soft S-curve that gives the surface its "glass" feel.
25
23
  */
26
24
 
27
25
  /**
28
- * A single layer of the progressive ramp.
29
- * - `stops` describe how this layer is revealed from top to bottom.
30
- * - `amount` is this layer's share (0–1) of the max `blurAmount`.
26
+ * A single bottom-anchored band of the progressive blur stack.
27
+ * - `start` is where the band begins, as a fraction from the top (0 = covers
28
+ * the whole surface, 0.8 = only the bottom 20%). Each band runs from `start`
29
+ * to the bottom.
30
+ * - `amount` is the band's own blur strength as a share (0–1) of the peak.
31
31
  *
32
- * We stack just TWO layers on both platforms: a faint base blur that covers
33
- * most of the footer and a slightly stronger accent concentrated near the
34
- * bottom. Keeping the overlap shallow avoids compounding the dark tint of
35
- * multiple `BlurView`s (which is what made the earlier 3-layer ramp read as a
36
- * heavy block), while still giving a genuine variable-radius result — the blur
37
- * radius grows toward the bottom where the two layers overlap.
32
+ * Because each `BlurView` re-blurs everything rendered behind it, the bands
33
+ * COMPOUND where they overlap (toward the bottom), so the effective radius grows
34
+ * downward without any masking. The top region is covered only by the first,
35
+ * faint band; the bottom is covered by all of them.
38
36
  */
39
37
 
40
- // Base reveal: a faint trace of blur begins near the very top and grows
41
- // steadily downward, so the upper half still carries visible glass rather than
42
- // snapping clear. Also reused for the no-native-blur fallback scrim.
38
+ // Eased tint ramp for the no-native-blur fallback scrim. A faint trace begins
39
+ // near the very top and grows steadily downward.
43
40
  const BASE_MASK_STOPS = [{
44
41
  offset: 0.0,
45
42
  opacity: 0
@@ -56,26 +53,26 @@ const BASE_MASK_STOPS = [{
56
53
  offset: 1.0,
57
54
  opacity: 1
58
55
  }];
59
- const PROGRESSIVE_LAYERS = [{
60
- amount: 0.65,
61
- stops: BASE_MASK_STOPS
62
- },
63
- // Accent: the strongest blur, gathering over the lower portion for depth.
64
- {
65
- amount: 1.0,
66
- stops: [{
67
- offset: 0.0,
68
- opacity: 0
69
- }, {
70
- offset: 0.3,
71
- opacity: 0.15
72
- }, {
73
- offset: 0.65,
74
- opacity: 0.65
75
- }, {
76
- offset: 1.0,
77
- opacity: 1
78
- }]
56
+
57
+ // Bottom-anchored blur bands (top → bottom). The first band is faint and covers
58
+ // the whole surface so the top reads as barely-there glass; each subsequent band
59
+ // starts lower and adds strength, compounding toward the bottom for a smooth
60
+ // variable-radius result. Quadrature of these shares lands the bottom at ~peak.
61
+ const PROGRESSIVE_BANDS = [{
62
+ start: 0.0,
63
+ amount: 0.18
64
+ }, {
65
+ start: 0.3,
66
+ amount: 0.28
67
+ }, {
68
+ start: 0.52,
69
+ amount: 0.38
70
+ }, {
71
+ start: 0.7,
72
+ amount: 0.5
73
+ }, {
74
+ start: 0.84,
75
+ amount: 0.6
79
76
  }];
80
77
 
81
78
  /**
@@ -108,13 +105,14 @@ const NATIVE_BLUR_SUPPORTED = (() => {
108
105
  })();
109
106
 
110
107
  /**
111
- * Vertical alpha-gradient mask drawn with `react-native-svg`. `MaskedView`
112
- * reveals its child in proportion to this mask's alpha, so feeding it an eased
113
- * multi-stop gradient makes the layer's blur swell in smoothly from top to
114
- * bottom instead of along a hard seam.
108
+ * Vertical color-gradient scrim drawn with `react-native-svg` — used as the
109
+ * no-native-blur fallback. Paints `color` at the eased per-stop alpha so the
110
+ * tint swells smoothly from clear (top) to full (bottom), exactly mirroring the
111
+ * old `MaskedView`-over-solid-color fallback but with no native-view masking.
115
112
  */
116
- function GradientMask({
113
+ function GradientScrim({
117
114
  id,
115
+ color,
118
116
  stops
119
117
  }) {
120
118
  return /*#__PURE__*/_jsxs(Svg, {
@@ -129,7 +127,7 @@ function GradientMask({
129
127
  y2: "1",
130
128
  children: stops.map((s, i) => /*#__PURE__*/_jsx(Stop, {
131
129
  offset: s.offset,
132
- stopColor: "#000000",
130
+ stopColor: color,
133
131
  stopOpacity: s.opacity
134
132
  }, i))
135
133
  })
@@ -194,38 +192,37 @@ function GlassFill({
194
192
  return /*#__PURE__*/_jsx(View, {
195
193
  style: [StyleSheet.absoluteFill, style],
196
194
  pointerEvents: "none",
197
- children: /*#__PURE__*/_jsx(MaskedView, {
198
- style: StyleSheet.absoluteFill,
199
- maskElement: /*#__PURE__*/_jsx(GradientMask, {
200
- id: `${maskId}-fb`,
201
- stops: BASE_MASK_STOPS
202
- }),
203
- children: /*#__PURE__*/_jsx(View, {
204
- style: [StyleSheet.absoluteFill, {
205
- backgroundColor: fallbackColor
206
- }]
207
- })
195
+ children: /*#__PURE__*/_jsx(GradientScrim, {
196
+ id: `${maskId}-fb`,
197
+ color: fallbackColor,
198
+ stops: BASE_MASK_STOPS
208
199
  })
209
200
  });
210
201
  }
211
202
  return /*#__PURE__*/_jsx(View, {
212
203
  style: [StyleSheet.absoluteFill, style],
213
204
  pointerEvents: "none",
214
- children: PROGRESSIVE_LAYERS.map((layer, i) => {
215
- const amount = Math.max(1, Math.round(peakBlur * layer.amount));
216
- return /*#__PURE__*/_jsx(MaskedView, {
217
- style: StyleSheet.absoluteFill,
218
- maskElement: /*#__PURE__*/_jsx(GradientMask, {
219
- id: `${maskId}-${i}`,
220
- stops: layer.stops
221
- }),
222
- children: /*#__PURE__*/_jsx(BlurView, {
223
- style: StyleSheet.absoluteFill,
224
- blurType: blurType,
225
- blurAmount: amount,
226
- reducedTransparencyFallbackColor: fallbackColor
227
- })
228
- }, i);
205
+ children: PROGRESSIVE_BANDS.map((band, i) => {
206
+ const amount = Math.max(1, Math.round(peakBlur * band.amount));
207
+ return (
208
+ /*#__PURE__*/
209
+ // Each band is clipped to start at `band.start` and run to
210
+ // the bottom; its `BlurView` blurs the content (and any
211
+ // lower bands) behind it, so the radius compounds downward.
212
+ _jsx(View, {
213
+ style: [StyleSheet.absoluteFill, {
214
+ top: `${band.start * 100}%`,
215
+ overflow: 'hidden'
216
+ }],
217
+ pointerEvents: "none",
218
+ children: /*#__PURE__*/_jsx(BlurView, {
219
+ style: StyleSheet.absoluteFill,
220
+ blurType: blurType,
221
+ blurAmount: amount,
222
+ reducedTransparencyFallbackColor: fallbackColor
223
+ })
224
+ }, i)
225
+ );
229
226
  })
230
227
  });
231
228
  }
@@ -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-26T16:43:17.033Z
7
+ * Generated: 2026-06-29T08:47:00.741Z
8
8
  */
9
9
  export declare const iconRegistry: Record<string, {
10
10
  path: string;
@@ -25,10 +25,17 @@ export interface GlassFillProps {
25
25
  /**
26
26
  * Render a *progressive* (variable) blur instead of a uniform one: fully
27
27
  * clear at the top, easing into a soft blur toward the bottom. Implemented
28
- * by stacking two `MaskedView` + `BlurView` layers (a faint base + a
29
- * slightly stronger accent near the bottom), each revealed via an eased
30
- * multi-stop SVG gradient mask so the blur swells smoothly rather than
31
- * along a hard seam. Works on iOS and Android with no extra native module.
28
+ * mask-free by stacking several bottom-anchored `BlurView` "bands": each
29
+ * band is clipped to start a little lower than the last, and because a
30
+ * `BlurView` blurs whatever is rendered behind it, the bands COMPOUND toward
31
+ * the bottom (a lower band re-blurs the already-blurred output above it) so
32
+ * the effective radius swells smoothly downward. This deliberately avoids
33
+ * masking a native view — the old implementation wrapped each `BlurView` in
34
+ * `@react-native-masked-view/masked-view`, a legacy Paper component that
35
+ * resolves to a plain object (crashing "Element type is invalid") under the
36
+ * New Architecture interop. Works on iOS and Android with no extra native
37
+ * module. When the native blur isn't linked at all, falls back to a pure
38
+ * `react-native-svg` gradient scrim (see below).
32
39
  */
33
40
  progressive?: boolean;
34
41
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jfs-components",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "React Native Jio Finance Components Library",
5
5
  "author": "sunshuaiqi@gmail.com",
6
6
  "license": "MIT",
@@ -83,7 +83,6 @@
83
83
  "@expo/metro-runtime": "^6.1.2",
84
84
  "@gorhom/bottom-sheet": "5.2.6",
85
85
  "@react-native-async-storage/async-storage": "^2.2.0",
86
- "@react-native-masked-view/masked-view": "^0.3.2",
87
86
  "@shopify/flash-list": "^2.2.0",
88
87
  "ajv": "^8.17.1",
89
88
  "ajv-keywords": "^5.1.0",
@@ -1,7 +1,6 @@
1
- import React from 'react';
1
+ import React, { useId } from 'react';
2
2
  import { View, Platform, type ViewStyle } from 'react-native';
3
- import MaskedView from '@react-native-masked-view/masked-view';
4
- import Svg, { Path } from 'react-native-svg';
3
+ import Svg, { Defs, Mask, Path, ForeignObject } from 'react-native-svg';
5
4
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
5
  import { EMPTY_MODES, flattenChildren } from '../../utils/react-utils';
7
6
  import type { Modes } from '../../design-tokens'
@@ -13,6 +12,11 @@ type AvatarGroupProps = {
13
12
  } & React.ComponentProps<typeof View>;
14
13
 
15
14
  function AvatarGroup({ modes = EMPTY_MODES, children, style, ...rest }: AvatarGroupProps) {
15
+ // Stable, collision-free base for the per-avatar SVG mask ids so multiple
16
+ // groups on one screen (and multiple avatars in a group) never clash.
17
+ const rawId = useId();
18
+ const maskBaseId = `avatargroup-mask-${rawId.replace(/[^a-zA-Z0-9_-]/g, '')}`;
19
+
16
20
  const gap = getVariableByName('avatarGroup/gap', modes);
17
21
 
18
22
 
@@ -43,7 +47,11 @@ function AvatarGroup({ modes = EMPTY_MODES, children, style, ...rest }: AvatarGr
43
47
  const holeRadius = effectiveRadius + borderSizeInt;
44
48
  const holeCenter = { x: nextAvatarX + r, y: r };
45
49
 
46
- // Native Mask Path
50
+ // Native mask path: the full avatar square MINUS a circular hole where the
51
+ // next (overlapping) avatar sits. Drawn as a single `evenodd` path so the
52
+ // hole is cut out of the square. Filled WHITE because react-native-svg's
53
+ // `<Mask>` is a *luminance* mask (white = visible, black/absent = hidden) —
54
+ // the inverse of the old `@react-native-masked-view` alpha convention.
47
55
  const rectPath = `M 0 0 h ${avatarSizeInt} v ${avatarSizeInt} h -${avatarSizeInt} z`;
48
56
  const circlePath = `M ${holeCenter.x},${holeCenter.y} m -${holeRadius},0 a ${holeRadius},${holeRadius} 0 1,0 ${holeRadius * 2},0 a ${holeRadius},${holeRadius} 0 1,0 -${holeRadius * 2},0`;
49
57
  const maskPath = `${rectPath} ${circlePath}`;
@@ -101,23 +109,41 @@ function AvatarGroup({ modes = EMPTY_MODES, children, style, ...rest }: AvatarGr
101
109
  );
102
110
  }
103
111
 
104
- // Native MaskedView
112
+ // Native: mask the avatar with react-native-svg instead of the
113
+ // unmaintained `@react-native-masked-view/masked-view`, whose
114
+ // legacy `requireNativeComponent('RNCMaskedView')` resolves to a
115
+ // plain object (crashing "Element type is invalid") under the New
116
+ // Architecture interop. react-native-svg ships a Fabric/codegen
117
+ // `<Mask>` + `<ForeignObject>` that resolves on both architectures;
118
+ // the avatar is hosted inside the `<ForeignObject>` and revealed
119
+ // through the luminance mask.
120
+ const maskId = `${maskBaseId}-${index}`;
105
121
  return (
106
122
  <View key={index} style={wrapperStyle}>
107
- <MaskedView
108
- style={{ flex: 1, height: '100%' }}
109
- maskElement={
110
- <Svg height="100%" width="100%" viewBox={`0 0 ${avatarSizeInt} ${avatarSizeInt}`}>
111
- <Path
112
- d={maskPath}
113
- fill="black"
114
- fillRule="evenodd"
115
- />
116
- </Svg>
117
- }
123
+ <Svg
124
+ height="100%"
125
+ width="100%"
126
+ viewBox={`0 0 ${avatarSizeInt} ${avatarSizeInt}`}
118
127
  >
119
- {clonedChild}
120
- </MaskedView>
128
+ <Defs>
129
+ <Mask
130
+ id={maskId}
131
+ maskUnits="userSpaceOnUse"
132
+ maskContentUnits="userSpaceOnUse"
133
+ >
134
+ <Path d={maskPath} fill="white" fillRule="evenodd" />
135
+ </Mask>
136
+ </Defs>
137
+ <ForeignObject
138
+ x="0"
139
+ y="0"
140
+ width={avatarSizeInt}
141
+ height={avatarSizeInt}
142
+ mask={`url(#${maskId})`}
143
+ >
144
+ {clonedChild}
145
+ </ForeignObject>
146
+ </Svg>
121
147
  </View>
122
148
  );
123
149
  })}
@@ -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',
@@ -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-26T16:43:17.033Z
7
+ * Generated: 2026-06-29T08:47:00.741Z
8
8
  */
9
9
 
10
10
  // Icon name to SVG data mapping