jfs-components 0.0.69 → 0.0.71

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 (56) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/lib/commonjs/components/CardAdvisory/CardAdvisory.js +203 -0
  3. package/lib/commonjs/components/CardCTA/CardCTA.js +198 -16
  4. package/lib/commonjs/components/CircularProgressBar/CircularProgressBar.js +147 -0
  5. package/lib/commonjs/components/CircularProgressBarDoted/CircularProgressBarDoted.js +258 -0
  6. package/lib/commonjs/components/CircularRating/CircularRating.js +161 -0
  7. package/lib/commonjs/components/Gauge/Gauge.js +223 -0
  8. package/lib/commonjs/components/ListGroup/ListGroup.js +3 -1
  9. package/lib/commonjs/components/MediaCard/GlassFill.js +62 -0
  10. package/lib/commonjs/components/MediaCard/GlassFill.web.js +48 -0
  11. package/lib/commonjs/components/MediaCard/MediaCard.js +28 -31
  12. package/lib/commonjs/components/Nudge/Nudge.js +179 -87
  13. package/lib/commonjs/components/index.js +35 -0
  14. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  15. package/lib/commonjs/icons/registry.js +1 -1
  16. package/lib/module/components/CardAdvisory/CardAdvisory.js +197 -0
  17. package/lib/module/components/CardCTA/CardCTA.js +199 -17
  18. package/lib/module/components/CircularProgressBar/CircularProgressBar.js +141 -0
  19. package/lib/module/components/CircularProgressBarDoted/CircularProgressBarDoted.js +253 -0
  20. package/lib/module/components/CircularRating/CircularRating.js +155 -0
  21. package/lib/module/components/Gauge/Gauge.js +217 -0
  22. package/lib/module/components/ListGroup/ListGroup.js +3 -1
  23. package/lib/module/components/MediaCard/GlassFill.js +57 -0
  24. package/lib/module/components/MediaCard/GlassFill.web.js +43 -0
  25. package/lib/module/components/MediaCard/MediaCard.js +29 -32
  26. package/lib/module/components/Nudge/Nudge.js +178 -87
  27. package/lib/module/components/index.js +5 -0
  28. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  29. package/lib/module/icons/registry.js +1 -1
  30. package/lib/typescript/src/components/CardAdvisory/CardAdvisory.d.ts +49 -0
  31. package/lib/typescript/src/components/CardCTA/CardCTA.d.ts +16 -1
  32. package/lib/typescript/src/components/CircularProgressBar/CircularProgressBar.d.ts +27 -0
  33. package/lib/typescript/src/components/CircularProgressBarDoted/CircularProgressBarDoted.d.ts +48 -0
  34. package/lib/typescript/src/components/CircularRating/CircularRating.d.ts +49 -0
  35. package/lib/typescript/src/components/Gauge/Gauge.d.ts +53 -0
  36. package/lib/typescript/src/components/MediaCard/GlassFill.d.ts +47 -0
  37. package/lib/typescript/src/components/MediaCard/GlassFill.web.d.ts +20 -0
  38. package/lib/typescript/src/components/MediaCard/MediaCard.d.ts +17 -13
  39. package/lib/typescript/src/components/Nudge/Nudge.d.ts +14 -11
  40. package/lib/typescript/src/components/index.d.ts +6 -1
  41. package/lib/typescript/src/icons/registry.d.ts +1 -1
  42. package/package.json +3 -2
  43. package/src/components/CardAdvisory/CardAdvisory.tsx +283 -0
  44. package/src/components/CardCTA/CardCTA.tsx +236 -13
  45. package/src/components/CircularProgressBar/CircularProgressBar.tsx +190 -0
  46. package/src/components/CircularProgressBarDoted/CircularProgressBarDoted.tsx +357 -0
  47. package/src/components/CircularRating/CircularRating.tsx +241 -0
  48. package/src/components/Gauge/Gauge.tsx +303 -0
  49. package/src/components/ListGroup/ListGroup.tsx +3 -1
  50. package/src/components/MediaCard/GlassFill.tsx +89 -0
  51. package/src/components/MediaCard/GlassFill.web.tsx +53 -0
  52. package/src/components/MediaCard/MediaCard.tsx +29 -48
  53. package/src/components/Nudge/Nudge.tsx +222 -82
  54. package/src/components/index.ts +6 -1
  55. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  56. package/src/icons/registry.ts +1 -1
@@ -0,0 +1,241 @@
1
+ import React, { useMemo } from 'react'
2
+ import {
3
+ Text,
4
+ View,
5
+ type StyleProp,
6
+ type TextStyle,
7
+ type ViewStyle,
8
+ } from 'react-native'
9
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
10
+ import { useTokens } from '../../design-tokens/JFSThemeProvider'
11
+ import Icon from '../../icons/Icon'
12
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
13
+ import CircularProgressBarDoted from '../CircularProgressBarDoted/CircularProgressBarDoted'
14
+ import Nudge from '../Nudge/Nudge'
15
+
16
+ type CircularRatingBaseProps = Omit<React.ComponentProps<typeof View>, 'children' | 'style'>
17
+
18
+ export type CircularRatingProps = CircularRatingBaseProps & {
19
+ /** Rating value. Clamped by CircularProgressBarDoted between 0 and 100. */
20
+ value?: number
21
+ /** Number of dots rendered around the rating ring. */
22
+ dotCount?: number
23
+ /** Small label above the score. */
24
+ label?: string
25
+ /** Text below the score. */
26
+ tierLabel?: string
27
+ /** Footer timestamp/copy shown below the rating. */
28
+ footerText?: string
29
+ /** Show the footer info icon. */
30
+ showFooterIcon?: boolean
31
+ /** Show the bottom inline nudge. */
32
+ showNudge?: boolean
33
+ /** Body text for the default bottom nudge. */
34
+ nudgeBody?: string
35
+ /** Button label for the default bottom nudge. */
36
+ nudgeButtonLabel?: string
37
+ /** Called when the nudge button is pressed. */
38
+ onPressNudgeButton?: () => void
39
+ /** Called when the rating tier row is pressed. */
40
+ onTierPress?: () => void
41
+ /** Optional footer slot. Receives `modes` recursively. */
42
+ footerSlot?: React.ReactNode
43
+ /** Optional nudge slot. Receives `modes` recursively. */
44
+ nudgeSlot?: React.ReactNode
45
+ /** Design token modes forwarded to token lookups and child components. */
46
+ modes?: Record<string, any>
47
+ /** Optional container style overrides. */
48
+ style?: StyleProp<ViewStyle>
49
+ /** Optional rating ring wrapper style overrides. */
50
+ ratingStyle?: StyleProp<ViewStyle>
51
+ /** Optional footer row style overrides. */
52
+ footerStyle?: StyleProp<ViewStyle>
53
+ /** Optional footer text style overrides. */
54
+ footerTextStyle?: StyleProp<TextStyle>
55
+ /** Optional nudge style overrides. */
56
+ nudgeStyle?: StyleProp<ViewStyle>
57
+ /** Accessibility label for the whole component. */
58
+ accessibilityLabel?: string
59
+ }
60
+
61
+ interface CircularRatingTokens {
62
+ containerStyle: ViewStyle
63
+ ratingStyle: ViewStyle
64
+ footerStyle: ViewStyle
65
+ footerTextStyle: TextStyle
66
+ footerIconColor: string
67
+ footerIconSize: number
68
+ }
69
+
70
+ const toNumber = (value: unknown, fallback: number) => {
71
+ if (typeof value === 'number') {
72
+ return Number.isFinite(value) ? value : fallback
73
+ }
74
+
75
+ if (typeof value === 'string') {
76
+ const parsed = Number(value)
77
+ return Number.isFinite(parsed) ? parsed : fallback
78
+ }
79
+
80
+ return fallback
81
+ }
82
+
83
+ const toFontWeight = (value: unknown, fallback: TextStyle['fontWeight']) => {
84
+ if (typeof value === 'number') {
85
+ return String(value) as TextStyle['fontWeight']
86
+ }
87
+
88
+ if (typeof value === 'string') {
89
+ return value as TextStyle['fontWeight']
90
+ }
91
+
92
+ return fallback
93
+ }
94
+
95
+ function resolveCircularRatingTokens(modes: Record<string, any>): CircularRatingTokens {
96
+ const gap = toNumber(getVariableByName('circularRating/gap', modes), 32)
97
+ const padding = toNumber(getVariableByName('circularRating/padding', modes), 10)
98
+
99
+ const footerGap = toNumber(getVariableByName('circularRating/footer/gap', modes), 4)
100
+ const footerHeight = toNumber(getVariableByName('circularRating/footer/height', modes), 16)
101
+ const footerTextColor = getVariableByName('circularRating/footer/text/color', modes) || '#0d0d0f'
102
+ const footerTextFontSize = toNumber(getVariableByName('circularRating/footer/text/fontSize', modes), 12)
103
+ const footerTextFontFamily = getVariableByName('circularRating/footer/text/fontFamily', modes) || 'JioType Var'
104
+ const footerTextLineHeight = toNumber(getVariableByName('circularRating/footer/text/lineHeight', modes), 16)
105
+ const footerTextFontWeight = toFontWeight(
106
+ getVariableByName('circularRating/footer/text/fontWeight', modes),
107
+ '400'
108
+ )
109
+
110
+ return {
111
+ containerStyle: {
112
+ alignItems: 'flex-start',
113
+ gap,
114
+ justifyContent: 'center',
115
+ padding,
116
+ },
117
+ ratingStyle: {
118
+ height: 320,
119
+ width: 320,
120
+ },
121
+ footerStyle: {
122
+ alignItems: 'center',
123
+ flexDirection: 'row',
124
+ gap: footerGap,
125
+ height: footerHeight,
126
+ justifyContent: 'center',
127
+ width: '100%',
128
+ },
129
+ footerTextStyle: {
130
+ color: footerTextColor as string,
131
+ fontFamily: footerTextFontFamily as string,
132
+ fontSize: footerTextFontSize,
133
+ fontWeight: footerTextFontWeight,
134
+ lineHeight: footerTextLineHeight,
135
+ textAlign: 'center',
136
+ },
137
+ footerIconColor: (getVariableByName('circularRating/footer/icon/color', modes) || '#1a1c1f') as string,
138
+ footerIconSize: toNumber(getVariableByName('circularRating/footer/icon/size', modes), 16),
139
+ }
140
+ }
141
+
142
+ function CircularRating({
143
+ value = 72,
144
+ dotCount = 24,
145
+ label = 'Rating',
146
+ tierLabel = 'Doing great',
147
+ footerText = 'Updated on 1 March',
148
+ showFooterIcon = true,
149
+ showNudge = true,
150
+ nudgeBody = 'Split this transaction into installments',
151
+ nudgeButtonLabel = 'Button',
152
+ onPressNudgeButton,
153
+ onTierPress,
154
+ footerSlot,
155
+ nudgeSlot,
156
+ modes: propModes = EMPTY_MODES,
157
+ style,
158
+ ratingStyle,
159
+ footerStyle,
160
+ footerTextStyle,
161
+ nudgeStyle,
162
+ accessibilityLabel,
163
+ ...rest
164
+ }: CircularRatingProps) {
165
+ const { modes: globalModes } = useTokens()
166
+ const modes = useMemo(
167
+ () => (globalModes === EMPTY_MODES && propModes === EMPTY_MODES
168
+ ? EMPTY_MODES
169
+ : { ...globalModes, ...propModes }),
170
+ [globalModes, propModes]
171
+ )
172
+ const tokens = useMemo(() => resolveCircularRatingTokens(modes), [modes])
173
+
174
+ const processedFooterSlot = useMemo(() => {
175
+ if (!footerSlot) return null
176
+ const processed = cloneChildrenWithModes(React.Children.toArray(footerSlot), modes)
177
+ return processed.length === 1 ? processed[0] : processed
178
+ }, [footerSlot, modes])
179
+
180
+ const processedNudgeSlot = useMemo(() => {
181
+ if (!nudgeSlot) return null
182
+ const processed = cloneChildrenWithModes(React.Children.toArray(nudgeSlot), modes)
183
+ return processed.length === 1 ? processed[0] : processed
184
+ }, [nudgeSlot, modes])
185
+
186
+ const defaultAccessibilityLabel =
187
+ accessibilityLabel ?? `${label}. ${Math.round(value)} out of 100. ${tierLabel}. ${footerText}`
188
+
189
+ return (
190
+ <View
191
+ accessibilityLabel={defaultAccessibilityLabel}
192
+ style={[tokens.containerStyle, style]}
193
+ {...rest}
194
+ >
195
+ <View style={[tokens.ratingStyle, ratingStyle]}>
196
+ <CircularProgressBarDoted
197
+ value={value}
198
+ dotCount={dotCount}
199
+ label={label}
200
+ tierLabel={tierLabel}
201
+ onTierPress={onTierPress}
202
+ modes={modes}
203
+ />
204
+ </View>
205
+
206
+ <View style={[tokens.footerStyle, footerStyle]}>
207
+ {processedFooterSlot || (
208
+ <>
209
+ <Text numberOfLines={1} style={[tokens.footerTextStyle, footerTextStyle]}>
210
+ {footerText}
211
+ </Text>
212
+ {showFooterIcon ? (
213
+ <Icon
214
+ name="ic_info"
215
+ size={tokens.footerIconSize}
216
+ color={tokens.footerIconColor}
217
+ accessibilityElementsHidden={true}
218
+ importantForAccessibility="no"
219
+ />
220
+ ) : null}
221
+ </>
222
+ )}
223
+ </View>
224
+
225
+ {showNudge ? (
226
+ processedNudgeSlot || (
227
+ <Nudge
228
+ type="inline-compact"
229
+ body={nudgeBody}
230
+ buttonLabel={nudgeButtonLabel}
231
+ onPressButton={onPressNudgeButton}
232
+ modes={modes}
233
+ style={[{ width: 312 }, nudgeStyle]}
234
+ />
235
+ )
236
+ ) : null}
237
+ </View>
238
+ )
239
+ }
240
+
241
+ export default React.memo(CircularRating)
@@ -0,0 +1,303 @@
1
+ import React from 'react'
2
+ import { StyleSheet, View, Text, type StyleProp, type TextStyle, type ViewStyle } from 'react-native'
3
+ import Svg, { Path } from 'react-native-svg'
4
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
5
+ import { useTokens } from '../../design-tokens/JFSThemeProvider'
6
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
7
+ import SupportText, { type SupportTextProps } from '../SupportText/SupportText'
8
+
9
+ type GaugeBaseProps = Omit<React.ComponentProps<typeof View>, 'children' | 'style'>
10
+
11
+ export type GaugeProps = GaugeBaseProps & {
12
+ /** Current gauge value. Interpreted against `min` and `max`. */
13
+ value?: number
14
+ /** Lower bound used to normalize progress. */
15
+ min?: number
16
+ /** Upper bound used to normalize progress. */
17
+ max?: number
18
+ /** Optional formatted value shown in the default readout. */
19
+ valueLabel?: string
20
+ /** Heading above the arc. */
21
+ title?: string
22
+ /** Caption below the arc. */
23
+ caption?: string
24
+ /** Support text shown in the default readout. */
25
+ supportText?: string
26
+ /** Status passed to the default SupportText component. */
27
+ supportTextStatus?: SupportTextProps['status']
28
+ /** Hides the heading while keeping the rest of the layout intact. */
29
+ showTitle?: boolean
30
+ /** Hides the caption while keeping the rest of the layout intact. */
31
+ showCaption?: boolean
32
+ /** Hides default support text when no custom readout slot is provided. */
33
+ showSupportText?: boolean
34
+ /** Design token modes forwarded to token lookups and slot children. */
35
+ modes?: Record<string, any>
36
+ /** Slot rendered in the center of the gauge arc. Receives `modes` recursively. */
37
+ children?: React.ReactNode
38
+ /** Container style override. */
39
+ style?: StyleProp<ViewStyle>
40
+ /** Arc wrapper style override. */
41
+ arcStyle?: StyleProp<ViewStyle>
42
+ /** Readout container style override. */
43
+ readoutStyle?: StyleProp<ViewStyle>
44
+ /** Title text style override. */
45
+ titleStyle?: StyleProp<TextStyle>
46
+ /** Value text style override. */
47
+ valueStyle?: StyleProp<TextStyle>
48
+ /** Caption text style override. */
49
+ captionStyle?: StyleProp<TextStyle>
50
+ /** Track stroke style override. */
51
+ trackStyle?: StyleProp<ViewStyle>
52
+ /** Progress stroke style override. */
53
+ progressStyle?: StyleProp<ViewStyle>
54
+ /** Accessibility label for the whole gauge. */
55
+ accessibilityLabel?: string
56
+ }
57
+
58
+ const clamp = (value: number, min: number, max: number) => {
59
+ if (max <= min) {
60
+ return 0
61
+ }
62
+
63
+ return Math.min(1, Math.max(0, (value - min) / (max - min)))
64
+ }
65
+
66
+ const toNumber = (value: unknown, fallback: number) => {
67
+ if (typeof value === 'number') {
68
+ return Number.isFinite(value) ? value : fallback
69
+ }
70
+
71
+ if (typeof value === 'string') {
72
+ const parsed = Number(value)
73
+ return Number.isFinite(parsed) ? parsed : fallback
74
+ }
75
+
76
+ return fallback
77
+ }
78
+
79
+ const toFontWeight = (value: unknown, fallback: TextStyle['fontWeight']) => {
80
+ if (typeof value === 'number') {
81
+ return String(value) as TextStyle['fontWeight']
82
+ }
83
+
84
+ if (typeof value === 'string') {
85
+ return value as TextStyle['fontWeight']
86
+ }
87
+
88
+ return fallback
89
+ }
90
+
91
+ const getStrokeColor = (style: StyleProp<ViewStyle>, fallback: string) => {
92
+ const flattened = StyleSheet.flatten(style)
93
+ return typeof flattened.backgroundColor === 'string'
94
+ ? flattened.backgroundColor
95
+ : fallback
96
+ }
97
+
98
+ function Gauge({
99
+ value = 84,
100
+ min = 0,
101
+ max = 100,
102
+ valueLabel,
103
+ title = 'Your score is based on strong data',
104
+ caption = 'Add more details for even better accuracy',
105
+ supportText = 'Support Text',
106
+ supportTextStatus = 'Success',
107
+ showTitle = true,
108
+ showCaption = true,
109
+ showSupportText = true,
110
+ modes: propModes = EMPTY_MODES,
111
+ children,
112
+ style,
113
+ arcStyle,
114
+ readoutStyle,
115
+ titleStyle,
116
+ valueStyle,
117
+ captionStyle,
118
+ trackStyle,
119
+ progressStyle,
120
+ accessibilityLabel,
121
+ ...rest
122
+ }: GaugeProps) {
123
+ const { modes: globalModes } = useTokens()
124
+ const modes = { ...globalModes, ...propModes }
125
+ const supportTextModes = { ...modes, Status: supportTextStatus }
126
+
127
+ const gap = toNumber(getVariableByName('gauge/gap', modes), 30)
128
+ const padding = toNumber(getVariableByName('gauge/padding', modes), 16)
129
+ const width = toNumber(getVariableByName('gauge/width', modes), 299)
130
+ const arcWidth = toNumber(getVariableByName('gauge/arc/width', modes), 267)
131
+ const arcHeight = arcWidth * (186 / 328)
132
+
133
+ const trackSize = toNumber(getVariableByName('gauge/track/size', modes), 244)
134
+ const trackInnerRadius = toNumber(getVariableByName('gauge/track/radius', modes), 99)
135
+ const progressInnerRadius = toNumber(getVariableByName('gauge/progress/radius', modes), 99)
136
+ const innerRadius = Math.min(trackInnerRadius, progressInnerRadius)
137
+ const strokeWidth = Math.max(1, trackSize / 2 - innerRadius)
138
+ const radius = innerRadius + strokeWidth / 2
139
+ const centerX = arcWidth / 2
140
+ const centerY = Math.min(arcHeight - strokeWidth / 2, radius + strokeWidth / 2)
141
+ const startX = centerX - radius
142
+ const endX = centerX + radius
143
+ const arcPath = `M ${startX} ${centerY} A ${radius} ${radius} 0 0 1 ${endX} ${centerY}`
144
+ const arcLength = Math.PI * radius
145
+ const progress = clamp(value, min, max)
146
+
147
+ const titleColor = getVariableByName('gauge/title/color', modes) || '#0c0d10'
148
+ const titleFontSize = toNumber(getVariableByName('gauge/title/fontSize', modes), 23)
149
+ const titleLineHeight = toNumber(getVariableByName('gauge/title/lineHeight', modes), 23)
150
+ const titleFontFamily = getVariableByName('gauge/title/fontFamily', modes) || 'JioType Var'
151
+ const titleFontWeight = toFontWeight(getVariableByName('gauge/title/fontWeight', modes), '900')
152
+ const titleHeight = toNumber(getVariableByName('gauge/title/height', modes), 46)
153
+
154
+ const readoutGap = toNumber(getVariableByName('gauge/readout/gap', modes), 8)
155
+ const readoutPadding = toNumber(getVariableByName('gauge/readout/padding', modes), 8)
156
+ const valueColor = getVariableByName('gauge/readout/value/color', modes) || '#0c0d10'
157
+ const valueFontSize = toNumber(getVariableByName('gauge/readout/value/fontSize', modes), 36)
158
+ const valueLineHeight = toNumber(getVariableByName('gauge/readout/value/lineHeight', modes), 36)
159
+ const valueFontFamily = getVariableByName('gauge/readout/value/fontFamily', modes) || 'JioType Var'
160
+ const valueFontWeight = toFontWeight(getVariableByName('gauge/readout/value/fontWeight', modes), '800')
161
+
162
+ const captionColor = getVariableByName('gauge/caption/color', modes) || '#0c0d10'
163
+ const captionFontSize = toNumber(getVariableByName('gauge/caption/fontSize', modes), 14)
164
+ const captionLineHeight = toNumber(getVariableByName('gauge/caption/lineHeight', modes), 18)
165
+ const captionFontFamily = getVariableByName('gauge/caption/fontFamily', modes) || 'JioType Var'
166
+ const captionFontWeight = toFontWeight(getVariableByName('gauge/caption/fontWeight', modes), '500')
167
+
168
+ const trackColor = getStrokeColor(
169
+ trackStyle,
170
+ getVariableByName('gauge/track/color', modes) || '#f5f5f6'
171
+ )
172
+ const progressColor = getStrokeColor(
173
+ progressStyle,
174
+ getVariableByName('gauge/progress/color', modes) || '#25ab21'
175
+ )
176
+
177
+ const containerStyle: ViewStyle = {
178
+ alignItems: 'center',
179
+ gap,
180
+ padding,
181
+ width,
182
+ }
183
+
184
+ const computedTitleStyle: TextStyle = {
185
+ color: titleColor as string,
186
+ fontFamily: titleFontFamily as string,
187
+ fontSize: titleFontSize,
188
+ fontWeight: titleFontWeight,
189
+ lineHeight: titleLineHeight,
190
+ minHeight: titleHeight,
191
+ textAlign: 'center',
192
+ width: '100%',
193
+ }
194
+
195
+ const computedArcStyle: ViewStyle = {
196
+ height: arcHeight,
197
+ overflow: 'hidden',
198
+ position: 'relative',
199
+ width: '100%',
200
+ }
201
+
202
+ const computedReadoutStyle: ViewStyle = {
203
+ alignItems: 'center',
204
+ gap: readoutGap,
205
+ left: 0,
206
+ padding: readoutPadding,
207
+ position: 'absolute',
208
+ right: 0,
209
+ top: arcHeight * 0.42,
210
+ }
211
+
212
+ const computedValueStyle: TextStyle = {
213
+ color: valueColor as string,
214
+ fontFamily: valueFontFamily as string,
215
+ fontSize: valueFontSize,
216
+ fontWeight: valueFontWeight,
217
+ lineHeight: valueLineHeight,
218
+ textAlign: 'center',
219
+ }
220
+
221
+ const computedCaptionStyle: TextStyle = {
222
+ color: captionColor as string,
223
+ fontFamily: captionFontFamily as string,
224
+ fontSize: captionFontSize,
225
+ fontWeight: captionFontWeight,
226
+ lineHeight: captionLineHeight,
227
+ textAlign: 'center',
228
+ width: '100%',
229
+ }
230
+
231
+ const defaultValueLabel = valueLabel ?? String(Math.round(value))
232
+ const defaultAccessibilityLabel =
233
+ accessibilityLabel ??
234
+ `${title}. ${defaultValueLabel} out of ${max}. ${supportText}. ${caption}`
235
+
236
+ return (
237
+ <View
238
+ accessibilityRole="progressbar"
239
+ accessibilityLabel={defaultAccessibilityLabel}
240
+ accessibilityValue={{ min, max, now: value }}
241
+ style={[containerStyle, style]}
242
+ {...rest}
243
+ >
244
+ {showTitle ? (
245
+ <Text style={[computedTitleStyle, titleStyle]}>
246
+ {title}
247
+ </Text>
248
+ ) : null}
249
+
250
+ <View style={[computedArcStyle, arcStyle]}>
251
+ <Svg
252
+ width="100%"
253
+ height={arcHeight}
254
+ viewBox={`0 0 ${arcWidth} ${arcHeight}`}
255
+ >
256
+ <Path
257
+ d={arcPath}
258
+ stroke={trackColor}
259
+ strokeWidth={strokeWidth}
260
+ strokeLinecap="butt"
261
+ fill="none"
262
+ />
263
+ <Path
264
+ d={arcPath}
265
+ stroke={progressColor}
266
+ strokeWidth={strokeWidth}
267
+ strokeLinecap="butt"
268
+ fill="none"
269
+ strokeDasharray={`${arcLength} ${arcLength}`}
270
+ strokeDashoffset={arcLength * (1 - progress)}
271
+ />
272
+ </Svg>
273
+
274
+ <View style={[computedReadoutStyle, readoutStyle]}>
275
+ {children ? (
276
+ cloneChildrenWithModes(children, modes)
277
+ ) : (
278
+ <>
279
+ <Text style={[computedValueStyle, valueStyle]}>
280
+ {defaultValueLabel}
281
+ </Text>
282
+ {showSupportText ? (
283
+ <SupportText
284
+ label={supportText}
285
+ status={supportTextStatus}
286
+ modes={supportTextModes}
287
+ />
288
+ ) : null}
289
+ </>
290
+ )}
291
+ </View>
292
+ </View>
293
+
294
+ {showCaption ? (
295
+ <Text style={[computedCaptionStyle, captionStyle]}>
296
+ {caption}
297
+ </Text>
298
+ ) : null}
299
+ </View>
300
+ )
301
+ }
302
+
303
+ export default Gauge
@@ -55,7 +55,9 @@ function ListGroup({
55
55
  }: ListGroupProps) {
56
56
  // Resolve container tokens
57
57
  const backgroundColor = getVariableByName('listGroup/background', modes) || 'rgba(255,255,255,0)'
58
- const borderColor = getVariableByName('listGroup/border/color', modes) || 'rgba(255,255,255,0)'
58
+ // The current exported token aliases a missing Figma variable. Keep the
59
+ // transparent fallback without logging the missing alias on every render.
60
+ const borderColor = 'rgba(255,255,255,0)'
59
61
  const borderWidth = getVariableByName('listGroup/borderWidth', modes) || 0
60
62
  const gap = getVariableByName('listGroup/gap', modes) || 12
61
63
 
@@ -0,0 +1,89 @@
1
+ import React from 'react'
2
+ import { View, StyleSheet, Platform, type ViewStyle, type StyleProp } from 'react-native'
3
+ import { BlurView } from '@react-native-community/blur'
4
+
5
+ export type GlassTint = 'dark' | 'light'
6
+
7
+ export interface GlassFillProps {
8
+ /**
9
+ * Visual tint of the glass surface. Maps to `BlurView`'s `blurType`
10
+ * (`'dark'` | `'light'`). The library also drives the appropriate
11
+ * `reducedTransparencyFallbackColor` from this so iOS gracefully degrades
12
+ * when "Reduce Transparency" is enabled in system accessibility settings.
13
+ */
14
+ tint?: GlassTint
15
+ /**
16
+ * Blur strength as a 0–100 "intensity" value (kept compatible with the
17
+ * previous `expo-blur` API so consumers don't need to relearn the scale).
18
+ * Internally mapped to `@react-native-community/blur`'s `blurAmount`,
19
+ * which is roughly 0–32 on iOS / Android.
20
+ */
21
+ intensity?: number
22
+ /**
23
+ * Token-derived color tint laid OVER the live blur (Figma `blur/minimal/background`).
24
+ * Painted as a translucent overlay so the glass keeps the design system
25
+ * color signature even when the platform blur quality varies.
26
+ */
27
+ overlayColor?: string
28
+ /** Container style overrides. Defaults to `StyleSheet.absoluteFill`. */
29
+ style?: StyleProp<ViewStyle>
30
+ }
31
+
32
+ const DEFAULT_FALLBACK_DARK = '#1414174a'
33
+ const DEFAULT_FALLBACK_LIGHT = '#ffffff66'
34
+
35
+ /**
36
+ * Glass / frosted surface for native (iOS + Android).
37
+ *
38
+ * Why this lives in its own platform-split file:
39
+ * - `@react-native-community/blur` is a native-only module. Importing it on
40
+ * web throws because the JS shim references native components that aren't
41
+ * registered there. By using Metro's platform-extension resolution
42
+ * (`GlassFill.tsx` for native, `GlassFill.web.tsx` for web), we keep the
43
+ * web bundle free of any native-only imports.
44
+ * - Centralizes the `intensity` (0–100) -> `blurAmount` (0–32) mapping so
45
+ * callers can keep the Figma token semantics they already know.
46
+ *
47
+ * On iOS this is a real `UIVisualEffectView` (true OS-level live blur).
48
+ * On Android this uses the community blur view (RealtimeBlurView). On devices
49
+ * where realtime blur is unavailable, `reducedTransparencyFallbackColor` (and
50
+ * the explicit `overlayColor`) ensure the surface still renders as a
51
+ * translucent tinted scrim instead of disappearing.
52
+ */
53
+ function GlassFill({
54
+ tint = 'dark',
55
+ intensity = 50,
56
+ overlayColor,
57
+ style,
58
+ }: GlassFillProps) {
59
+ const blurType: 'light' | 'dark' = tint === 'light' ? 'light' : 'dark'
60
+ const blurAmount = Math.max(0, Math.min(32, Math.round(intensity * 0.32)))
61
+ const fallbackColor = overlayColor ?? (tint === 'light' ? DEFAULT_FALLBACK_LIGHT : DEFAULT_FALLBACK_DARK)
62
+
63
+ return (
64
+ <View style={[StyleSheet.absoluteFill, style]} pointerEvents="none">
65
+ <BlurView
66
+ style={StyleSheet.absoluteFill}
67
+ blurType={blurType}
68
+ blurAmount={blurAmount}
69
+ reducedTransparencyFallbackColor={fallbackColor}
70
+ />
71
+ {overlayColor != null ? (
72
+ <View style={[StyleSheet.absoluteFill, { backgroundColor: overlayColor }]} />
73
+ ) : null}
74
+ {Platform.OS === 'android' ? (
75
+ <View
76
+ style={[
77
+ StyleSheet.absoluteFill,
78
+ {
79
+ backgroundColor: 'rgba(255,255,255,0.03)',
80
+ opacity: 0.6,
81
+ },
82
+ ]}
83
+ />
84
+ ) : null}
85
+ </View>
86
+ )
87
+ }
88
+
89
+ export default GlassFill
@@ -0,0 +1,53 @@
1
+ import React from 'react'
2
+ import { View, StyleSheet, type ViewStyle, type StyleProp } from 'react-native'
3
+
4
+ export type GlassTint = 'dark' | 'light'
5
+
6
+ export interface GlassFillProps {
7
+ tint?: GlassTint
8
+ intensity?: number
9
+ overlayColor?: string
10
+ style?: StyleProp<ViewStyle>
11
+ }
12
+
13
+ const DEFAULT_FALLBACK_DARK = '#1414174a'
14
+ const DEFAULT_FALLBACK_LIGHT = '#ffffff66'
15
+
16
+ /**
17
+ * Web counterpart of `GlassFill`.
18
+ *
19
+ * `@react-native-community/blur` does not ship a web implementation, so for
20
+ * the web bundle we render a translucent `View` with `backdrop-filter: blur()`
21
+ * — which is exactly how 0.0.67 and earlier shipped the glass effect on web.
22
+ * Native bundles pick up `GlassFill.tsx` instead via Metro's platform
23
+ * resolver; the web bundle picks up this file.
24
+ */
25
+ function GlassFill({
26
+ tint = 'dark',
27
+ intensity = 50,
28
+ overlayColor,
29
+ style,
30
+ }: GlassFillProps) {
31
+ // Approximate mapping: intensity 0-100 -> ~0-30px CSS blur. Keeps parity
32
+ // with the native blur strength so the component looks roughly the same
33
+ // across platforms.
34
+ const blurPx = Math.max(0, Math.min(30, Math.round(intensity * 0.3)))
35
+ const tintColor = overlayColor ?? (tint === 'light' ? DEFAULT_FALLBACK_LIGHT : DEFAULT_FALLBACK_DARK)
36
+
37
+ return (
38
+ <View
39
+ style={[
40
+ StyleSheet.absoluteFill,
41
+ { backgroundColor: tintColor },
42
+ // backdrop-filter is a web-only CSS property; ignored by RN
43
+ // on native (we never bundle this file there anyway).
44
+ // @ts-ignore web-only style
45
+ { backdropFilter: `blur(${blurPx}px)`, WebkitBackdropFilter: `blur(${blurPx}px)` },
46
+ style,
47
+ ]}
48
+ pointerEvents="none"
49
+ />
50
+ )
51
+ }
52
+
53
+ export default GlassFill