jfs-components 0.1.23 → 0.1.25
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.
- package/lib/commonjs/components/CardCTA/CardCTA.js +32 -25
- package/lib/commonjs/components/ChipSelect/ChipSelect.js +2 -1
- package/lib/commonjs/components/ListItem/ListItem.js +11 -4
- package/lib/commonjs/components/MetricData/MetricData.js +132 -0
- package/lib/commonjs/components/Rating/Rating.js +137 -0
- package/lib/commonjs/design-tokens/figma-modes.generated.js +2 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/CardCTA/CardCTA.js +32 -25
- package/lib/module/components/ChipSelect/ChipSelect.js +2 -1
- package/lib/module/components/ListItem/ListItem.js +11 -4
- package/lib/module/components/MetricData/MetricData.js +127 -0
- package/lib/module/components/Rating/Rating.js +132 -0
- package/lib/module/design-tokens/figma-modes.generated.js +2 -0
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/ChipSelect/ChipSelect.d.ts +6 -1
- package/lib/typescript/src/components/MetricData/MetricData.d.ts +53 -0
- package/lib/typescript/src/components/Rating/Rating.d.ts +30 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/CardCTA/CardCTA.tsx +30 -15
- package/src/components/ChipSelect/ChipSelect.tsx +7 -1
- package/src/components/ListItem/ListItem.tsx +11 -4
- package/src/components/MetricData/MetricData.tsx +185 -0
- package/src/components/Rating/Rating.tsx +174 -0
- package/src/design-tokens/figma-modes.generated.ts +2 -0
- package/src/icons/registry.ts +1 -1
|
@@ -17,6 +17,11 @@ export type ChipSelectProps = {
|
|
|
17
17
|
* @default "ic_calendar_week"
|
|
18
18
|
*/
|
|
19
19
|
icon?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Whether to show the close icon when active.
|
|
22
|
+
* @default true
|
|
23
|
+
*/
|
|
24
|
+
showCloseIcon?: boolean;
|
|
20
25
|
/**
|
|
21
26
|
* Modes for design token resolution.
|
|
22
27
|
*/
|
|
@@ -38,6 +43,6 @@ export type ChipSelectProps = {
|
|
|
38
43
|
* ChipSelect component for selecting options (e.g. Date selection).
|
|
39
44
|
* Based on Figma Node 1901-4727.
|
|
40
45
|
*/
|
|
41
|
-
declare function ChipSelect({ label, active, icon, modes, style, labelSlot, onPress, }: ChipSelectProps): import("react/jsx-runtime").JSX.Element;
|
|
46
|
+
declare function ChipSelect({ label, active, icon, showCloseIcon, modes, style, labelSlot, onPress, }: ChipSelectProps): import("react/jsx-runtime").JSX.Element;
|
|
42
47
|
export default ChipSelect;
|
|
43
48
|
//# sourceMappingURL=ChipSelect.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type StyleProp, type ViewStyle, type TextStyle } from 'react-native';
|
|
3
|
+
import type { Modes } from '../../design-tokens';
|
|
4
|
+
export type MetricDataProps = {
|
|
5
|
+
/** Top label (small, 12px). Hidden when omitted. */
|
|
6
|
+
title?: string;
|
|
7
|
+
/**
|
|
8
|
+
* The prominent value (20px bold). Pass a string, or any node (e.g. a
|
|
9
|
+
* `MoneyValue`) to compose a richer value.
|
|
10
|
+
*/
|
|
11
|
+
value?: string | React.ReactNode;
|
|
12
|
+
/** Sub-label below the value (12px, muted). Hidden when omitted. */
|
|
13
|
+
caption?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Optional glyph shown to the left of the value. Pass a registry icon name
|
|
16
|
+
* (e.g. `'ic_card'`) or a custom node. Rendered at 18px, tinted to the value
|
|
17
|
+
* colour. Hidden when omitted.
|
|
18
|
+
*/
|
|
19
|
+
icon?: string | React.ReactNode;
|
|
20
|
+
/** Design-token modes for theming. */
|
|
21
|
+
modes?: Modes;
|
|
22
|
+
/** Override the container styles. */
|
|
23
|
+
style?: StyleProp<ViewStyle>;
|
|
24
|
+
/** Override the title text styles. */
|
|
25
|
+
titleStyle?: StyleProp<TextStyle>;
|
|
26
|
+
/** Override the value text styles. */
|
|
27
|
+
valueStyle?: StyleProp<TextStyle>;
|
|
28
|
+
/** Override the caption text styles. */
|
|
29
|
+
captionStyle?: StyleProp<TextStyle>;
|
|
30
|
+
/**
|
|
31
|
+
* Accessibility label. Defaults to the resolved `title`, `value` and
|
|
32
|
+
* `caption` text joined together.
|
|
33
|
+
*/
|
|
34
|
+
accessibilityLabel?: string;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* MetricData — a compact, centered metric block.
|
|
38
|
+
*
|
|
39
|
+
* Stacks a small `title`, a prominent `value` (with an optional leading
|
|
40
|
+
* `icon`), and a muted `caption`. Typography, colours, gaps and padding all
|
|
41
|
+
* resolve from the `metricdata/*` design tokens. `title`, `caption` and `icon`
|
|
42
|
+
* are optional; only `value` is shown by default.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* <MetricData title="Balance" value="₹1,20,000" caption="as of today" />
|
|
47
|
+
* <MetricData title="Cards" value="12" icon="ic_card" caption="active" />
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
declare function MetricData({ title, value, caption, icon, modes, style, titleStyle, valueStyle, captionStyle, accessibilityLabel, }: MetricDataProps): import("react/jsx-runtime").JSX.Element;
|
|
51
|
+
declare const _default: React.MemoExoticComponent<typeof MetricData>;
|
|
52
|
+
export default _default;
|
|
53
|
+
//# sourceMappingURL=MetricData.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import type { Modes } from '../../design-tokens';
|
|
4
|
+
export type RatingProps = {
|
|
5
|
+
/** Number of filled stars. Rounded to the nearest whole star for display. */
|
|
6
|
+
value?: number;
|
|
7
|
+
/** Total number of stars to render. */
|
|
8
|
+
max?: number;
|
|
9
|
+
/**
|
|
10
|
+
* Called with the new rating (1..max) when a star is pressed. Providing this
|
|
11
|
+
* makes the control interactive; omit it (or set `readOnly`) for a display.
|
|
12
|
+
*/
|
|
13
|
+
onChange?: (value: number) => void;
|
|
14
|
+
/** Force a read-only display even when `onChange` is provided. */
|
|
15
|
+
readOnly?: boolean;
|
|
16
|
+
/** Override the star edge length. Defaults to the `ratingStar/width` token. */
|
|
17
|
+
starSize?: number;
|
|
18
|
+
/** Override the spacing between stars. Defaults to the `rating/gap` token. */
|
|
19
|
+
gap?: number;
|
|
20
|
+
/** Design token modes forwarded to token lookups. */
|
|
21
|
+
modes?: Modes;
|
|
22
|
+
/** Optional container style overrides. */
|
|
23
|
+
style?: StyleProp<ViewStyle>;
|
|
24
|
+
/** Accessibility label for the whole rating. Defaults to "X of Y stars". */
|
|
25
|
+
accessibilityLabel?: string;
|
|
26
|
+
} & Omit<React.ComponentProps<typeof View>, 'style'>;
|
|
27
|
+
declare function Rating({ value, max, onChange, readOnly, starSize, gap, modes: propModes, style, accessibilityLabel, ...rest }: RatingProps): import("react/jsx-runtime").JSX.Element;
|
|
28
|
+
declare const _default: React.MemoExoticComponent<typeof Rating>;
|
|
29
|
+
export default _default;
|
|
30
|
+
//# sourceMappingURL=Rating.d.ts.map
|
|
@@ -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-07-
|
|
7
|
+
* Generated: 2026-07-03T01:43:10.333Z
|
|
8
8
|
*/
|
|
9
9
|
export declare const iconRegistry: Record<string, {
|
|
10
10
|
path: string;
|
package/package.json
CHANGED
|
@@ -281,22 +281,37 @@ function CardCTA({
|
|
|
281
281
|
zIndex: 1,
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
// NOTE: rightWrap must
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
// the
|
|
290
|
-
//
|
|
291
|
-
// this because
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
284
|
+
// NOTE: rightWrap must implement the design's TRUE `2fr` column.
|
|
285
|
+
//
|
|
286
|
+
// History of this style — two Yoga pitfalls pull in opposite directions:
|
|
287
|
+
//
|
|
288
|
+
// 1. `flex: 2` (=> flexBasis: 0, flexShrink: 1): on Android, Yoga could
|
|
289
|
+
// shrink the wrapper below its padding+IconCapsule width when the left
|
|
290
|
+
// text was long, collapsing the padding and gluing the icon to the
|
|
291
|
+
// text. Browsers hide this because they honor `min-width: auto`; Yoga
|
|
292
|
+
// always treats min-width as 0 unless told otherwise.
|
|
293
|
+
// 2. The previous workaround, `flexBasis: 'auto'` + `flexGrow: 2`, fixed
|
|
294
|
+
// #1 but broke the column ratio: Yoga then sizes the wrapper to its
|
|
295
|
+
// content FIRST (icon + padding) and distributes only the leftover
|
|
296
|
+
// space 3:2. The icon width gets baked in on top of the 2fr share, so
|
|
297
|
+
// the right column ends up much wider than the design's 2fr and the
|
|
298
|
+
// body text wraps early with dead space to its right.
|
|
299
|
+
//
|
|
300
|
+
// Fundamental fix: keep the genuine fr-unit geometry (`flexBasis: 0`,
|
|
301
|
+
// grow 3:2, no shrink) and replace Yoga's missing `min-width: auto` with
|
|
302
|
+
// an explicit computed floor — the resolved IconCapsule size plus the
|
|
303
|
+
// wrapper's own horizontal padding. The column is exactly 2fr whenever
|
|
304
|
+
// space allows and can never collapse below its content.
|
|
305
|
+
const iconCapsuleModes = { ...modes, 'IconCapsule / Size': 'M', Emphasis: 'Medium', AppearanceBrand: 'Secondary' }
|
|
306
|
+
const rightWrapMinWidth =
|
|
307
|
+
((getVariableByName('iconCapsule/size', iconCapsuleModes) as number) || 0) +
|
|
308
|
+
((rightPaddingH as number) || 0) * 2
|
|
309
|
+
|
|
296
310
|
const rightWrapStyle: ViewStyle = {
|
|
297
311
|
flexGrow: 2,
|
|
298
312
|
flexShrink: 0,
|
|
299
|
-
flexBasis:
|
|
313
|
+
flexBasis: 0,
|
|
314
|
+
minWidth: rightWrapMinWidth,
|
|
300
315
|
paddingHorizontal: rightPaddingH,
|
|
301
316
|
paddingVertical: rightPaddingV,
|
|
302
317
|
alignItems: 'flex-end',
|
|
@@ -555,9 +570,9 @@ function CardCTA({
|
|
|
555
570
|
</View>
|
|
556
571
|
<View style={rightWrapStyle}>
|
|
557
572
|
{iconSlot ? (
|
|
558
|
-
cloneChildrenWithModes(iconSlot,
|
|
573
|
+
cloneChildrenWithModes(iconSlot, iconCapsuleModes)
|
|
559
574
|
) : (
|
|
560
|
-
<IconCapsule iconName={iconName} modes={
|
|
575
|
+
<IconCapsule iconName={iconName} modes={iconCapsuleModes} />
|
|
561
576
|
)}
|
|
562
577
|
</View>
|
|
563
578
|
</View>
|
|
@@ -21,6 +21,11 @@ export type ChipSelectProps = {
|
|
|
21
21
|
* @default "ic_calendar_week"
|
|
22
22
|
*/
|
|
23
23
|
icon?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Whether to show the close icon when active.
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
showCloseIcon?: boolean;
|
|
24
29
|
/**
|
|
25
30
|
* Modes for design token resolution.
|
|
26
31
|
*/
|
|
@@ -47,6 +52,7 @@ function ChipSelect({
|
|
|
47
52
|
label = 'Date',
|
|
48
53
|
active = false,
|
|
49
54
|
icon = 'ic_calendar_week',
|
|
55
|
+
showCloseIcon = true,
|
|
50
56
|
modes = EMPTY_MODES,
|
|
51
57
|
style,
|
|
52
58
|
labelSlot,
|
|
@@ -123,7 +129,7 @@ function ChipSelect({
|
|
|
123
129
|
|
|
124
130
|
{renderLabel()}
|
|
125
131
|
|
|
126
|
-
{active && (
|
|
132
|
+
{active && showCloseIcon && (
|
|
127
133
|
<Icon name="ic_close" size={iconSize} color={textColor} />
|
|
128
134
|
)}
|
|
129
135
|
</TouchableOpacity>
|
|
@@ -322,9 +322,16 @@ function ListItemImpl({
|
|
|
322
322
|
const renderSupportContent = () => {
|
|
323
323
|
if (processedSupportSlot) return processedSupportSlot
|
|
324
324
|
|
|
325
|
-
// Default support text
|
|
326
|
-
//
|
|
327
|
-
//
|
|
325
|
+
// Default support text — ONE `<Text>` hard-clamped to 2 lines via
|
|
326
|
+
// `numberOfLines={2}`, the only mechanism React Native actually enforces
|
|
327
|
+
// on iOS/Android (native has no `white-space: nowrap` equivalent, so any
|
|
328
|
+
// style-based trick cannot stop a too-wide word from wrapping).
|
|
329
|
+
//
|
|
330
|
+
// - vertical layout: line-broken on spaces so each word gets its own line
|
|
331
|
+
// ("Split Payments" → "Split" / "Payments"). If a word is still wider
|
|
332
|
+
// than the cell (or there are 3+ words), RN cuts it off at line 2 with
|
|
333
|
+
// a trailing ellipsis instead of ever spilling onto a third line.
|
|
334
|
+
// - horizontal layout: natural wrapping, same 2-line clamp.
|
|
328
335
|
const displayText = layout === 'Vertical'
|
|
329
336
|
? supportText.replace(/ /g, '\n')
|
|
330
337
|
: supportText
|
|
@@ -336,7 +343,7 @@ function ListItemImpl({
|
|
|
336
343
|
? [tokens.supportTextStyle, verticalSupportTextOverride]
|
|
337
344
|
: tokens.supportTextStyle
|
|
338
345
|
}
|
|
339
|
-
{...resolveTruncation(disableTruncation,
|
|
346
|
+
{...resolveTruncation(disableTruncation, 2, 'tail')}
|
|
340
347
|
>
|
|
341
348
|
{displayText}
|
|
342
349
|
</Text>
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
type StyleProp,
|
|
6
|
+
type ViewStyle,
|
|
7
|
+
type TextStyle,
|
|
8
|
+
} from 'react-native'
|
|
9
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
10
|
+
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
11
|
+
import Icon from '../Icon/Icon'
|
|
12
|
+
import type { Modes } from '../../design-tokens'
|
|
13
|
+
|
|
14
|
+
const LINE_HEIGHT_RATIO = 1.2
|
|
15
|
+
const ICON_SIZE = 18
|
|
16
|
+
// Figma renders the value-row icon with no surrounding padding; the shared Icon
|
|
17
|
+
// component pulls padding from `icon/padding/*` tokens, so we zero it here.
|
|
18
|
+
const ICON_PADDING_RESET: ViewStyle = {
|
|
19
|
+
paddingLeft: 0,
|
|
20
|
+
paddingRight: 0,
|
|
21
|
+
paddingTop: 0,
|
|
22
|
+
paddingBottom: 0,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type MetricDataProps = {
|
|
26
|
+
/** Top label (small, 12px). Hidden when omitted. */
|
|
27
|
+
title?: string
|
|
28
|
+
/**
|
|
29
|
+
* The prominent value (20px bold). Pass a string, or any node (e.g. a
|
|
30
|
+
* `MoneyValue`) to compose a richer value.
|
|
31
|
+
*/
|
|
32
|
+
value?: string | React.ReactNode
|
|
33
|
+
/** Sub-label below the value (12px, muted). Hidden when omitted. */
|
|
34
|
+
caption?: string
|
|
35
|
+
/**
|
|
36
|
+
* Optional glyph shown to the left of the value. Pass a registry icon name
|
|
37
|
+
* (e.g. `'ic_card'`) or a custom node. Rendered at 18px, tinted to the value
|
|
38
|
+
* colour. Hidden when omitted.
|
|
39
|
+
*/
|
|
40
|
+
icon?: string | React.ReactNode
|
|
41
|
+
/** Design-token modes for theming. */
|
|
42
|
+
modes?: Modes
|
|
43
|
+
/** Override the container styles. */
|
|
44
|
+
style?: StyleProp<ViewStyle>
|
|
45
|
+
/** Override the title text styles. */
|
|
46
|
+
titleStyle?: StyleProp<TextStyle>
|
|
47
|
+
/** Override the value text styles. */
|
|
48
|
+
valueStyle?: StyleProp<TextStyle>
|
|
49
|
+
/** Override the caption text styles. */
|
|
50
|
+
captionStyle?: StyleProp<TextStyle>
|
|
51
|
+
/**
|
|
52
|
+
* Accessibility label. Defaults to the resolved `title`, `value` and
|
|
53
|
+
* `caption` text joined together.
|
|
54
|
+
*/
|
|
55
|
+
accessibilityLabel?: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* MetricData — a compact, centered metric block.
|
|
60
|
+
*
|
|
61
|
+
* Stacks a small `title`, a prominent `value` (with an optional leading
|
|
62
|
+
* `icon`), and a muted `caption`. Typography, colours, gaps and padding all
|
|
63
|
+
* resolve from the `metricdata/*` design tokens. `title`, `caption` and `icon`
|
|
64
|
+
* are optional; only `value` is shown by default.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```tsx
|
|
68
|
+
* <MetricData title="Balance" value="₹1,20,000" caption="as of today" />
|
|
69
|
+
* <MetricData title="Cards" value="12" icon="ic_card" caption="active" />
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
function MetricData({
|
|
73
|
+
title,
|
|
74
|
+
value,
|
|
75
|
+
caption,
|
|
76
|
+
icon,
|
|
77
|
+
modes = EMPTY_MODES,
|
|
78
|
+
style,
|
|
79
|
+
titleStyle,
|
|
80
|
+
valueStyle,
|
|
81
|
+
captionStyle,
|
|
82
|
+
accessibilityLabel,
|
|
83
|
+
}: MetricDataProps) {
|
|
84
|
+
const titleColor = (getVariableByName('metricdata/title/color', modes) as string | null) ?? '#000000'
|
|
85
|
+
const titleFontFamily = (getVariableByName('metricdata/title/fontfamily', modes) as string | null) ?? 'JioType Var'
|
|
86
|
+
const titleFontSize = (getVariableByName('metricdata/title/fontsize', modes) as number | null) ?? 12
|
|
87
|
+
const titleFontWeight = String(getVariableByName('metricdata/title/fontweight', modes) ?? 400) as TextStyle['fontWeight']
|
|
88
|
+
|
|
89
|
+
const valueColor = (getVariableByName('metricdata/value/color', modes) as string | null) ?? '#000000'
|
|
90
|
+
const valueFontFamily = (getVariableByName('metricdata/value/fontfamily', modes) as string | null) ?? 'JioType Var'
|
|
91
|
+
const valueFontSize = (getVariableByName('metricdata/value/fontsize', modes) as number | null) ?? 20
|
|
92
|
+
const valueFontWeight = String(getVariableByName('metricdata/value/fontweight', modes) ?? 700) as TextStyle['fontWeight']
|
|
93
|
+
|
|
94
|
+
const captionColor = (getVariableByName('metricdata/caption/color', modes) as string | null) ?? '#777777'
|
|
95
|
+
const captionFontFamily = (getVariableByName('metricdata/caption/fontfamily', modes) as string | null) ?? 'JioType Var'
|
|
96
|
+
const captionFontSize = (getVariableByName('metricdata/caption/fontsize', modes) as number | null) ?? 12
|
|
97
|
+
const captionFontWeight = String(getVariableByName('metricdata/caption/fontweight', modes) ?? 500) as TextStyle['fontWeight']
|
|
98
|
+
|
|
99
|
+
const gap = (getVariableByName('metricdata/gap', modes) as number | null) ?? 4
|
|
100
|
+
const valueWrapGap = (getVariableByName('metricdata/valueWrap/gap', modes) as number | null) ?? 4
|
|
101
|
+
const paddingHorizontal = (getVariableByName('metricdata/padding/horizontal', modes) as number | null) ?? 10
|
|
102
|
+
const paddingVertical = (getVariableByName('metricdata/padding/vertical', modes) as number | null) ?? 10
|
|
103
|
+
|
|
104
|
+
const containerStyle: ViewStyle = {
|
|
105
|
+
flexDirection: 'column',
|
|
106
|
+
alignItems: 'center',
|
|
107
|
+
justifyContent: 'center',
|
|
108
|
+
gap,
|
|
109
|
+
paddingHorizontal,
|
|
110
|
+
paddingVertical,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const titleTextStyle: TextStyle = {
|
|
114
|
+
color: titleColor,
|
|
115
|
+
fontFamily: titleFontFamily,
|
|
116
|
+
fontSize: titleFontSize,
|
|
117
|
+
fontWeight: titleFontWeight,
|
|
118
|
+
lineHeight: titleFontSize * LINE_HEIGHT_RATIO,
|
|
119
|
+
textAlign: 'center',
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const valueTextStyle: TextStyle = {
|
|
123
|
+
color: valueColor,
|
|
124
|
+
fontFamily: valueFontFamily,
|
|
125
|
+
fontSize: valueFontSize,
|
|
126
|
+
fontWeight: valueFontWeight,
|
|
127
|
+
lineHeight: valueFontSize * LINE_HEIGHT_RATIO,
|
|
128
|
+
textAlign: 'center',
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const captionTextStyle: TextStyle = {
|
|
132
|
+
color: captionColor,
|
|
133
|
+
fontFamily: captionFontFamily,
|
|
134
|
+
fontSize: captionFontSize,
|
|
135
|
+
fontWeight: captionFontWeight,
|
|
136
|
+
lineHeight: captionFontSize * LINE_HEIGHT_RATIO,
|
|
137
|
+
textAlign: 'center',
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const iconNode =
|
|
141
|
+
icon == null ? null : typeof icon === 'string' ? (
|
|
142
|
+
<Icon
|
|
143
|
+
iconName={icon}
|
|
144
|
+
size={ICON_SIZE}
|
|
145
|
+
color={valueColor}
|
|
146
|
+
modes={modes}
|
|
147
|
+
style={ICON_PADDING_RESET}
|
|
148
|
+
/>
|
|
149
|
+
) : (
|
|
150
|
+
icon
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
const resolvedLabel =
|
|
154
|
+
accessibilityLabel ??
|
|
155
|
+
([title, typeof value === 'string' ? value : undefined, caption]
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.join(', ') || undefined)
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<View
|
|
161
|
+
style={[containerStyle, style]}
|
|
162
|
+
accessible
|
|
163
|
+
{...(resolvedLabel !== undefined ? { accessibilityLabel: resolvedLabel } : null)}
|
|
164
|
+
>
|
|
165
|
+
{title !== undefined && title !== '' ? (
|
|
166
|
+
<Text style={[titleTextStyle, titleStyle]}>{title}</Text>
|
|
167
|
+
) : null}
|
|
168
|
+
|
|
169
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: valueWrapGap }}>
|
|
170
|
+
{iconNode}
|
|
171
|
+
{typeof value === 'string' ? (
|
|
172
|
+
<Text style={[valueTextStyle, valueStyle]}>{value}</Text>
|
|
173
|
+
) : (
|
|
174
|
+
value
|
|
175
|
+
)}
|
|
176
|
+
</View>
|
|
177
|
+
|
|
178
|
+
{caption !== undefined && caption !== '' ? (
|
|
179
|
+
<Text style={[captionTextStyle, captionStyle]}>{caption}</Text>
|
|
180
|
+
) : null}
|
|
181
|
+
</View>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export default React.memo(MetricData)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import React, { useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Pressable,
|
|
4
|
+
View,
|
|
5
|
+
type StyleProp,
|
|
6
|
+
type ViewStyle,
|
|
7
|
+
} from 'react-native'
|
|
8
|
+
import Svg, { Path } from 'react-native-svg'
|
|
9
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
10
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
11
|
+
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
12
|
+
import type { Modes } from '../../design-tokens'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Star glyph taken directly from the Figma `Rating` component. Filled and empty
|
|
16
|
+
* stars share this shape and differ only by fill color, so a single path keeps
|
|
17
|
+
* the row perfectly aligned.
|
|
18
|
+
*/
|
|
19
|
+
const STAR_PATH =
|
|
20
|
+
'M40.7187 15.3334C40.4728 14.6159 40.0426 13.9805 39.4688 13.4885C38.8951 12.9965 38.1985 12.689 37.4403 12.566L28.3018 11.1721L24.2038 2.35741C23.8759 1.66044 23.3432 1.06596 22.6875 0.635476C22.0318 0.225491 21.2737 0 20.4951 0C19.7165 0 18.9583 0.225491 18.3027 0.635476C17.647 1.04546 17.1142 1.63994 16.7864 2.35741L12.6884 11.1721L3.4679 12.566C2.73026 12.689 2.01311 12.9965 1.43939 13.4885C0.865675 13.9805 0.435385 14.6159 0.189505 15.3334C-0.0358854 16.0304 -0.0563753 16.7889 0.107545 17.5063C0.271465 18.2238 0.640285 18.9003 1.15254 19.4333L7.89374 26.362L6.29552 36.1811C6.17258 36.9396 6.27503 37.7186 6.56189 38.4156C6.84875 39.1125 7.34051 39.7275 7.9757 40.1785C8.65187 40.691 9.49196 40.978 10.3525 40.9985C11.0697 40.9985 11.7664 40.8345 12.4015 40.486L20.5771 35.9761L28.7526 40.486C29.3673 40.8345 30.0844 41.019 30.8016 40.9985C31.6621 40.9985 32.4817 40.732 33.1784 40.24C33.7931 39.789 34.2849 39.1945 34.5922 38.4771C34.8791 37.7801 34.9815 37.0011 34.8586 36.2426L33.2604 26.4235L40.0016 19.4948C40.4933 18.9413 40.8007 18.2443 40.9441 17.5063C41.0671 16.7684 40.9851 16.0099 40.7187 15.3334Z'
|
|
21
|
+
const STAR_VIEWBOX = '0 0 41 41'
|
|
22
|
+
|
|
23
|
+
// Fallbacks mirror the Figma `Rating Star / Output` + `Rating Star State` tokens.
|
|
24
|
+
const DEFAULT_STAR_SIZE = 41
|
|
25
|
+
const DEFAULT_GAP = 12
|
|
26
|
+
const SELECTED_FALLBACK = '#5D00B5'
|
|
27
|
+
const IDLE_FALLBACK = '#F6F3FF'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* `ratingStar/background` is state-driven: the same token resolves to the filled
|
|
31
|
+
* color when `Rating Star State = selected` and to the empty color when `idle`.
|
|
32
|
+
*/
|
|
33
|
+
const RATING_STATE_COLLECTION = 'Rating Star State'
|
|
34
|
+
|
|
35
|
+
export type RatingProps = {
|
|
36
|
+
/** Number of filled stars. Rounded to the nearest whole star for display. */
|
|
37
|
+
value?: number
|
|
38
|
+
/** Total number of stars to render. */
|
|
39
|
+
max?: number
|
|
40
|
+
/**
|
|
41
|
+
* Called with the new rating (1..max) when a star is pressed. Providing this
|
|
42
|
+
* makes the control interactive; omit it (or set `readOnly`) for a display.
|
|
43
|
+
*/
|
|
44
|
+
onChange?: (value: number) => void
|
|
45
|
+
/** Force a read-only display even when `onChange` is provided. */
|
|
46
|
+
readOnly?: boolean
|
|
47
|
+
/** Override the star edge length. Defaults to the `ratingStar/width` token. */
|
|
48
|
+
starSize?: number
|
|
49
|
+
/** Override the spacing between stars. Defaults to the `rating/gap` token. */
|
|
50
|
+
gap?: number
|
|
51
|
+
/** Design token modes forwarded to token lookups. */
|
|
52
|
+
modes?: Modes
|
|
53
|
+
/** Optional container style overrides. */
|
|
54
|
+
style?: StyleProp<ViewStyle>
|
|
55
|
+
/** Accessibility label for the whole rating. Defaults to "X of Y stars". */
|
|
56
|
+
accessibilityLabel?: string
|
|
57
|
+
} & Omit<React.ComponentProps<typeof View>, 'style'>
|
|
58
|
+
|
|
59
|
+
const toNumber = (value: unknown, fallback: number) => {
|
|
60
|
+
const n = typeof value === 'string' ? Number(value) : value
|
|
61
|
+
return typeof n === 'number' && Number.isFinite(n) ? n : fallback
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function Star({
|
|
65
|
+
width,
|
|
66
|
+
height,
|
|
67
|
+
color,
|
|
68
|
+
}: {
|
|
69
|
+
width: number
|
|
70
|
+
height: number
|
|
71
|
+
color: string
|
|
72
|
+
}) {
|
|
73
|
+
return (
|
|
74
|
+
<Svg width={width} height={height} viewBox={STAR_VIEWBOX}>
|
|
75
|
+
<Path d={STAR_PATH} fill={color} />
|
|
76
|
+
</Svg>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function Rating({
|
|
81
|
+
value = 0,
|
|
82
|
+
max = 5,
|
|
83
|
+
onChange,
|
|
84
|
+
readOnly = false,
|
|
85
|
+
starSize,
|
|
86
|
+
gap,
|
|
87
|
+
modes: propModes = EMPTY_MODES,
|
|
88
|
+
style,
|
|
89
|
+
accessibilityLabel,
|
|
90
|
+
...rest
|
|
91
|
+
}: RatingProps) {
|
|
92
|
+
const { modes: globalModes } = useTokens()
|
|
93
|
+
const modes = useMemo(
|
|
94
|
+
() =>
|
|
95
|
+
globalModes === EMPTY_MODES && propModes === EMPTY_MODES
|
|
96
|
+
? EMPTY_MODES
|
|
97
|
+
: { ...globalModes, ...propModes },
|
|
98
|
+
[globalModes, propModes]
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const { selectedColor, idleColor, starWidth, starHeight, resolvedGap } =
|
|
102
|
+
useMemo(() => {
|
|
103
|
+
const selected = getVariableByName('ratingStar/background', {
|
|
104
|
+
...modes,
|
|
105
|
+
[RATING_STATE_COLLECTION]: 'selected',
|
|
106
|
+
}) as string | undefined
|
|
107
|
+
const idle = getVariableByName('ratingStar/background', {
|
|
108
|
+
...modes,
|
|
109
|
+
[RATING_STATE_COLLECTION]: 'idle',
|
|
110
|
+
}) as string | undefined
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
selectedColor: selected ?? SELECTED_FALLBACK,
|
|
114
|
+
idleColor: idle ?? IDLE_FALLBACK,
|
|
115
|
+
starWidth:
|
|
116
|
+
starSize ??
|
|
117
|
+
toNumber(
|
|
118
|
+
getVariableByName('ratingStar/width', modes),
|
|
119
|
+
DEFAULT_STAR_SIZE
|
|
120
|
+
),
|
|
121
|
+
starHeight:
|
|
122
|
+
starSize ??
|
|
123
|
+
toNumber(
|
|
124
|
+
getVariableByName('ratingStar/height', modes),
|
|
125
|
+
DEFAULT_STAR_SIZE
|
|
126
|
+
),
|
|
127
|
+
resolvedGap:
|
|
128
|
+
gap ?? toNumber(getVariableByName('rating/gap', modes), DEFAULT_GAP),
|
|
129
|
+
}
|
|
130
|
+
}, [modes, starSize, gap])
|
|
131
|
+
|
|
132
|
+
const total = Math.max(0, Math.round(max))
|
|
133
|
+
const filledCount = Math.max(0, Math.min(total, Math.round(value)))
|
|
134
|
+
const interactive = !readOnly && typeof onChange === 'function'
|
|
135
|
+
const label = accessibilityLabel ?? `${filledCount} of ${total} stars`
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<View
|
|
139
|
+
{...rest}
|
|
140
|
+
style={[
|
|
141
|
+
{ flexDirection: 'row', alignItems: 'center', gap: resolvedGap },
|
|
142
|
+
style,
|
|
143
|
+
]}
|
|
144
|
+
accessibilityRole={interactive ? 'adjustable' : 'image'}
|
|
145
|
+
accessibilityLabel={label}
|
|
146
|
+
accessibilityValue={
|
|
147
|
+
interactive ? { min: 0, max: total, now: filledCount } : undefined
|
|
148
|
+
}
|
|
149
|
+
>
|
|
150
|
+
{Array.from({ length: total }, (_, index) => {
|
|
151
|
+
const color = index < filledCount ? selectedColor : idleColor
|
|
152
|
+
const star = <Star width={starWidth} height={starHeight} color={color} />
|
|
153
|
+
|
|
154
|
+
if (!interactive) {
|
|
155
|
+
return <React.Fragment key={index}>{star}</React.Fragment>
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<Pressable
|
|
160
|
+
key={index}
|
|
161
|
+
onPress={() => onChange?.(index + 1)}
|
|
162
|
+
accessibilityRole="button"
|
|
163
|
+
accessibilityLabel={`Rate ${index + 1} of ${total} stars`}
|
|
164
|
+
hitSlop={8}
|
|
165
|
+
>
|
|
166
|
+
{star}
|
|
167
|
+
</Pressable>
|
|
168
|
+
)
|
|
169
|
+
})}
|
|
170
|
+
</View>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export default React.memo(Rating)
|
|
@@ -355,6 +355,7 @@ export const FIGMA_COMPONENT_MODES: Record<string, readonly string[]> = {
|
|
|
355
355
|
"MediaCard": ["Contrast Context"],
|
|
356
356
|
"MerchantProfile": ["Avatar Size", "Badge Size", "Color Mode", "context 10", "Context4", "Profile Card Appearance"],
|
|
357
357
|
"MessageField": ["Color Mode", "FormField States"],
|
|
358
|
+
"MetricData": ["AppearanceBrand", "Badge Size", "Color Mode", "context 10", "Context4", "Emphasis", "Profile Card Appearance"],
|
|
358
359
|
"MetricLegendItem": ["Appearance / DataViz", "Color Mode", "Emphasis / DataViz"],
|
|
359
360
|
"MoneyValue": ["Color Mode", "Context3"],
|
|
360
361
|
"MonthlyStatusGrid": ["Appearance / DataViz", "Calendar Glyph State", "Color Mode", "Emphasis / DataViz"],
|
|
@@ -373,6 +374,7 @@ export const FIGMA_COMPONENT_MODES: Record<string, readonly string[]> = {
|
|
|
373
374
|
"ProjectionMarker": ["Color Mode", "context 10", "Profile Card Appearance"],
|
|
374
375
|
"Radio": ["Color Mode"],
|
|
375
376
|
"RangeTrack": ["Appearance / DataViz", "Color Mode", "Emphasis / DataViz"],
|
|
377
|
+
"Rating": ["Color Mode", "Rating Star State"],
|
|
376
378
|
"RechargeCard": ["Avatar Size", "Badge Size", "Color Mode", "Context3", "Context4"],
|
|
377
379
|
"SavingsGoalSummary": ["Appearance / DataViz", "AppearanceBrand", "AppearanceSystem", "Color Mode", "Emphasis", "Emphasis / DataViz", "LinearProgress Size", "Semantic Intent"],
|
|
378
380
|
"Screen": ["Color Mode", "Page type"],
|
package/src/icons/registry.ts
CHANGED