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.
@@ -225,22 +225,39 @@ function CardCTA({
225
225
  zIndex: 1
226
226
  };
227
227
 
228
- // NOTE: rightWrap must NOT shrink on native. On Android (Yoga), the default
229
- // `flex: 2` shorthand expands to `{ flexGrow: 2, flexShrink: 1, flexBasis: 0 }`,
230
- // which combined with `minWidth: 0` lets Yoga shrink this wrapper below
231
- // its own padding+IconCapsule width when leftWrap's text is long. Once that
232
- // happens the horizontal padding collapses, `alignItems: 'flex-end'` pins
233
- // the IconCapsule against the inner edge, and the icon ends up visually
234
- // touching the body text (the wrapper "appears not to exist"). Web hides
235
- // this because browsers honor `min-width: auto` on flex items. Use
236
- // explicit `flexGrow`/`flexShrink: 0`/`flexBasis: 'auto'` so the wrapper
237
- // is sized to its content as a floor and only grows for the design's
238
- // 3:2 ratio when extra space is available. leftWrap already absorbs tight
239
- // space via `flexShrink: 1` + `minWidth: 0`.
228
+ // NOTE: rightWrap must implement the design's TRUE `2fr` column.
229
+ //
230
+ // History of this styletwo Yoga pitfalls pull in opposite directions:
231
+ //
232
+ // 1. `flex: 2` (=> flexBasis: 0, flexShrink: 1): on Android, Yoga could
233
+ // shrink the wrapper below its padding+IconCapsule width when the left
234
+ // text was long, collapsing the padding and gluing the icon to the
235
+ // text. Browsers hide this because they honor `min-width: auto`; Yoga
236
+ // always treats min-width as 0 unless told otherwise.
237
+ // 2. The previous workaround, `flexBasis: 'auto'` + `flexGrow: 2`, fixed
238
+ // #1 but broke the column ratio: Yoga then sizes the wrapper to its
239
+ // content FIRST (icon + padding) and distributes only the leftover
240
+ // space 3:2. The icon width gets baked in on top of the 2fr share, so
241
+ // the right column ends up much wider than the design's 2fr and the
242
+ // body text wraps early with dead space to its right.
243
+ //
244
+ // Fundamental fix: keep the genuine fr-unit geometry (`flexBasis: 0`,
245
+ // grow 3:2, no shrink) and replace Yoga's missing `min-width: auto` with
246
+ // an explicit computed floor — the resolved IconCapsule size plus the
247
+ // wrapper's own horizontal padding. The column is exactly 2fr whenever
248
+ // space allows and can never collapse below its content.
249
+ const iconCapsuleModes = {
250
+ ...modes,
251
+ 'IconCapsule / Size': 'M',
252
+ Emphasis: 'Medium',
253
+ AppearanceBrand: 'Secondary'
254
+ };
255
+ const rightWrapMinWidth = (getVariableByName('iconCapsule/size', iconCapsuleModes) || 0) + (rightPaddingH || 0) * 2;
240
256
  const rightWrapStyle = {
241
257
  flexGrow: 2,
242
258
  flexShrink: 0,
243
- flexBasis: 'auto',
259
+ flexBasis: 0,
260
+ minWidth: rightWrapMinWidth,
244
261
  paddingHorizontal: rightPaddingH,
245
262
  paddingVertical: rightPaddingV,
246
263
  alignItems: 'flex-end',
@@ -494,19 +511,9 @@ function CardCTA({
494
511
  })]
495
512
  }), /*#__PURE__*/_jsx(View, {
496
513
  style: rightWrapStyle,
497
- children: iconSlot ? cloneChildrenWithModes(iconSlot, {
498
- ...modes,
499
- 'IconCapsule / Size': 'M',
500
- Emphasis: 'Medium',
501
- AppearanceBrand: 'Secondary'
502
- }) : /*#__PURE__*/_jsx(IconCapsule, {
514
+ children: iconSlot ? cloneChildrenWithModes(iconSlot, iconCapsuleModes) : /*#__PURE__*/_jsx(IconCapsule, {
503
515
  iconName: iconName,
504
- modes: {
505
- ...modes,
506
- 'IconCapsule / Size': 'M',
507
- Emphasis: 'Medium',
508
- AppearanceBrand: 'Secondary'
509
- }
516
+ modes: iconCapsuleModes
510
517
  })
511
518
  })]
512
519
  });
@@ -14,6 +14,7 @@ function ChipSelect({
14
14
  label = 'Date',
15
15
  active = false,
16
16
  icon = 'ic_calendar_week',
17
+ showCloseIcon = true,
17
18
  modes = EMPTY_MODES,
18
19
  style,
19
20
  labelSlot,
@@ -87,7 +88,7 @@ function ChipSelect({
87
88
  name: icon,
88
89
  size: iconSize,
89
90
  color: textColor
90
- }), renderLabel(), active && /*#__PURE__*/_jsx(Icon, {
91
+ }), renderLabel(), active && showCloseIcon && /*#__PURE__*/_jsx(Icon, {
91
92
  name: "ic_close",
92
93
  size: iconSize,
93
94
  color: textColor
@@ -249,13 +249,20 @@ function ListItemImpl({
249
249
  const renderSupportContent = () => {
250
250
  if (processedSupportSlot) return processedSupportSlot;
251
251
 
252
- // Default support text:
253
- // - vertical layout: main text (line-broken on spaces)
254
- // - horizontal layout: secondary line (clamped to 2 lines)
252
+ // Default support text — ONE `<Text>` hard-clamped to 2 lines via
253
+ // `numberOfLines={2}`, the only mechanism React Native actually enforces
254
+ // on iOS/Android (native has no `white-space: nowrap` equivalent, so any
255
+ // style-based trick cannot stop a too-wide word from wrapping).
256
+ //
257
+ // - vertical layout: line-broken on spaces so each word gets its own line
258
+ // ("Split Payments" → "Split" / "Payments"). If a word is still wider
259
+ // than the cell (or there are 3+ words), RN cuts it off at line 2 with
260
+ // a trailing ellipsis instead of ever spilling onto a third line.
261
+ // - horizontal layout: natural wrapping, same 2-line clamp.
255
262
  const displayText = layout === 'Vertical' ? supportText.replace(/ /g, '\n') : supportText;
256
263
  return /*#__PURE__*/_jsx(Text, {
257
264
  style: layout === 'Vertical' ? [tokens.supportTextStyle, verticalSupportTextOverride] : tokens.supportTextStyle,
258
- ...resolveTruncation(disableTruncation, layout === 'Horizontal' ? 2 : undefined),
265
+ ...resolveTruncation(disableTruncation, 2, 'tail'),
259
266
  children: displayText
260
267
  });
261
268
  };
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+
3
+ import React from 'react';
4
+ import { View, Text } from 'react-native';
5
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
+ import { EMPTY_MODES } from '../../utils/react-utils';
7
+ import Icon from '../Icon/Icon';
8
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
+ const LINE_HEIGHT_RATIO = 1.2;
10
+ const ICON_SIZE = 18;
11
+ // Figma renders the value-row icon with no surrounding padding; the shared Icon
12
+ // component pulls padding from `icon/padding/*` tokens, so we zero it here.
13
+ const ICON_PADDING_RESET = {
14
+ paddingLeft: 0,
15
+ paddingRight: 0,
16
+ paddingTop: 0,
17
+ paddingBottom: 0
18
+ };
19
+ /**
20
+ * MetricData — a compact, centered metric block.
21
+ *
22
+ * Stacks a small `title`, a prominent `value` (with an optional leading
23
+ * `icon`), and a muted `caption`. Typography, colours, gaps and padding all
24
+ * resolve from the `metricdata/*` design tokens. `title`, `caption` and `icon`
25
+ * are optional; only `value` is shown by default.
26
+ *
27
+ * @example
28
+ * ```tsx
29
+ * <MetricData title="Balance" value="₹1,20,000" caption="as of today" />
30
+ * <MetricData title="Cards" value="12" icon="ic_card" caption="active" />
31
+ * ```
32
+ */
33
+ function MetricData({
34
+ title,
35
+ value,
36
+ caption,
37
+ icon,
38
+ modes = EMPTY_MODES,
39
+ style,
40
+ titleStyle,
41
+ valueStyle,
42
+ captionStyle,
43
+ accessibilityLabel
44
+ }) {
45
+ const titleColor = getVariableByName('metricdata/title/color', modes) ?? '#000000';
46
+ const titleFontFamily = getVariableByName('metricdata/title/fontfamily', modes) ?? 'JioType Var';
47
+ const titleFontSize = getVariableByName('metricdata/title/fontsize', modes) ?? 12;
48
+ const titleFontWeight = String(getVariableByName('metricdata/title/fontweight', modes) ?? 400);
49
+ const valueColor = getVariableByName('metricdata/value/color', modes) ?? '#000000';
50
+ const valueFontFamily = getVariableByName('metricdata/value/fontfamily', modes) ?? 'JioType Var';
51
+ const valueFontSize = getVariableByName('metricdata/value/fontsize', modes) ?? 20;
52
+ const valueFontWeight = String(getVariableByName('metricdata/value/fontweight', modes) ?? 700);
53
+ const captionColor = getVariableByName('metricdata/caption/color', modes) ?? '#777777';
54
+ const captionFontFamily = getVariableByName('metricdata/caption/fontfamily', modes) ?? 'JioType Var';
55
+ const captionFontSize = getVariableByName('metricdata/caption/fontsize', modes) ?? 12;
56
+ const captionFontWeight = String(getVariableByName('metricdata/caption/fontweight', modes) ?? 500);
57
+ const gap = getVariableByName('metricdata/gap', modes) ?? 4;
58
+ const valueWrapGap = getVariableByName('metricdata/valueWrap/gap', modes) ?? 4;
59
+ const paddingHorizontal = getVariableByName('metricdata/padding/horizontal', modes) ?? 10;
60
+ const paddingVertical = getVariableByName('metricdata/padding/vertical', modes) ?? 10;
61
+ const containerStyle = {
62
+ flexDirection: 'column',
63
+ alignItems: 'center',
64
+ justifyContent: 'center',
65
+ gap,
66
+ paddingHorizontal,
67
+ paddingVertical
68
+ };
69
+ const titleTextStyle = {
70
+ color: titleColor,
71
+ fontFamily: titleFontFamily,
72
+ fontSize: titleFontSize,
73
+ fontWeight: titleFontWeight,
74
+ lineHeight: titleFontSize * LINE_HEIGHT_RATIO,
75
+ textAlign: 'center'
76
+ };
77
+ const valueTextStyle = {
78
+ color: valueColor,
79
+ fontFamily: valueFontFamily,
80
+ fontSize: valueFontSize,
81
+ fontWeight: valueFontWeight,
82
+ lineHeight: valueFontSize * LINE_HEIGHT_RATIO,
83
+ textAlign: 'center'
84
+ };
85
+ const captionTextStyle = {
86
+ color: captionColor,
87
+ fontFamily: captionFontFamily,
88
+ fontSize: captionFontSize,
89
+ fontWeight: captionFontWeight,
90
+ lineHeight: captionFontSize * LINE_HEIGHT_RATIO,
91
+ textAlign: 'center'
92
+ };
93
+ const iconNode = icon == null ? null : typeof icon === 'string' ? /*#__PURE__*/_jsx(Icon, {
94
+ iconName: icon,
95
+ size: ICON_SIZE,
96
+ color: valueColor,
97
+ modes: modes,
98
+ style: ICON_PADDING_RESET
99
+ }) : icon;
100
+ const resolvedLabel = accessibilityLabel ?? ([title, typeof value === 'string' ? value : undefined, caption].filter(Boolean).join(', ') || undefined);
101
+ return /*#__PURE__*/_jsxs(View, {
102
+ style: [containerStyle, style],
103
+ accessible: true,
104
+ ...(resolvedLabel !== undefined ? {
105
+ accessibilityLabel: resolvedLabel
106
+ } : null),
107
+ children: [title !== undefined && title !== '' ? /*#__PURE__*/_jsx(Text, {
108
+ style: [titleTextStyle, titleStyle],
109
+ children: title
110
+ }) : null, /*#__PURE__*/_jsxs(View, {
111
+ style: {
112
+ flexDirection: 'row',
113
+ alignItems: 'center',
114
+ justifyContent: 'center',
115
+ gap: valueWrapGap
116
+ },
117
+ children: [iconNode, typeof value === 'string' ? /*#__PURE__*/_jsx(Text, {
118
+ style: [valueTextStyle, valueStyle],
119
+ children: value
120
+ }) : value]
121
+ }), caption !== undefined && caption !== '' ? /*#__PURE__*/_jsx(Text, {
122
+ style: [captionTextStyle, captionStyle],
123
+ children: caption
124
+ }) : null]
125
+ });
126
+ }
127
+ export default /*#__PURE__*/React.memo(MetricData);
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+
3
+ import React, { useMemo } from 'react';
4
+ import { Pressable, View } from 'react-native';
5
+ import Svg, { Path } from 'react-native-svg';
6
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
7
+ import { useTokens } from '../../design-tokens/JFSThemeProvider';
8
+ import { EMPTY_MODES } from '../../utils/react-utils';
9
+ import { jsx as _jsx } from "react/jsx-runtime";
10
+ /**
11
+ * Star glyph taken directly from the Figma `Rating` component. Filled and empty
12
+ * stars share this shape and differ only by fill color, so a single path keeps
13
+ * the row perfectly aligned.
14
+ */
15
+ const STAR_PATH = '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';
16
+ const STAR_VIEWBOX = '0 0 41 41';
17
+
18
+ // Fallbacks mirror the Figma `Rating Star / Output` + `Rating Star State` tokens.
19
+ const DEFAULT_STAR_SIZE = 41;
20
+ const DEFAULT_GAP = 12;
21
+ const SELECTED_FALLBACK = '#5D00B5';
22
+ const IDLE_FALLBACK = '#F6F3FF';
23
+
24
+ /**
25
+ * `ratingStar/background` is state-driven: the same token resolves to the filled
26
+ * color when `Rating Star State = selected` and to the empty color when `idle`.
27
+ */
28
+ const RATING_STATE_COLLECTION = 'Rating Star State';
29
+ const toNumber = (value, fallback) => {
30
+ const n = typeof value === 'string' ? Number(value) : value;
31
+ return typeof n === 'number' && Number.isFinite(n) ? n : fallback;
32
+ };
33
+ function Star({
34
+ width,
35
+ height,
36
+ color
37
+ }) {
38
+ return /*#__PURE__*/_jsx(Svg, {
39
+ width: width,
40
+ height: height,
41
+ viewBox: STAR_VIEWBOX,
42
+ children: /*#__PURE__*/_jsx(Path, {
43
+ d: STAR_PATH,
44
+ fill: color
45
+ })
46
+ });
47
+ }
48
+ function Rating({
49
+ value = 0,
50
+ max = 5,
51
+ onChange,
52
+ readOnly = false,
53
+ starSize,
54
+ gap,
55
+ modes: propModes = EMPTY_MODES,
56
+ style,
57
+ accessibilityLabel,
58
+ ...rest
59
+ }) {
60
+ const {
61
+ modes: globalModes
62
+ } = useTokens();
63
+ const modes = useMemo(() => globalModes === EMPTY_MODES && propModes === EMPTY_MODES ? EMPTY_MODES : {
64
+ ...globalModes,
65
+ ...propModes
66
+ }, [globalModes, propModes]);
67
+ const {
68
+ selectedColor,
69
+ idleColor,
70
+ starWidth,
71
+ starHeight,
72
+ resolvedGap
73
+ } = useMemo(() => {
74
+ const selected = getVariableByName('ratingStar/background', {
75
+ ...modes,
76
+ [RATING_STATE_COLLECTION]: 'selected'
77
+ });
78
+ const idle = getVariableByName('ratingStar/background', {
79
+ ...modes,
80
+ [RATING_STATE_COLLECTION]: 'idle'
81
+ });
82
+ return {
83
+ selectedColor: selected ?? SELECTED_FALLBACK,
84
+ idleColor: idle ?? IDLE_FALLBACK,
85
+ starWidth: starSize ?? toNumber(getVariableByName('ratingStar/width', modes), DEFAULT_STAR_SIZE),
86
+ starHeight: starSize ?? toNumber(getVariableByName('ratingStar/height', modes), DEFAULT_STAR_SIZE),
87
+ resolvedGap: gap ?? toNumber(getVariableByName('rating/gap', modes), DEFAULT_GAP)
88
+ };
89
+ }, [modes, starSize, gap]);
90
+ const total = Math.max(0, Math.round(max));
91
+ const filledCount = Math.max(0, Math.min(total, Math.round(value)));
92
+ const interactive = !readOnly && typeof onChange === 'function';
93
+ const label = accessibilityLabel ?? `${filledCount} of ${total} stars`;
94
+ return /*#__PURE__*/_jsx(View, {
95
+ ...rest,
96
+ style: [{
97
+ flexDirection: 'row',
98
+ alignItems: 'center',
99
+ gap: resolvedGap
100
+ }, style],
101
+ accessibilityRole: interactive ? 'adjustable' : 'image',
102
+ accessibilityLabel: label,
103
+ accessibilityValue: interactive ? {
104
+ min: 0,
105
+ max: total,
106
+ now: filledCount
107
+ } : undefined,
108
+ children: Array.from({
109
+ length: total
110
+ }, (_, index) => {
111
+ const color = index < filledCount ? selectedColor : idleColor;
112
+ const star = /*#__PURE__*/_jsx(Star, {
113
+ width: starWidth,
114
+ height: starHeight,
115
+ color: color
116
+ });
117
+ if (!interactive) {
118
+ return /*#__PURE__*/_jsx(React.Fragment, {
119
+ children: star
120
+ }, index);
121
+ }
122
+ return /*#__PURE__*/_jsx(Pressable, {
123
+ onPress: () => onChange?.(index + 1),
124
+ accessibilityRole: "button",
125
+ accessibilityLabel: `Rate ${index + 1} of ${total} stars`,
126
+ hitSlop: 8,
127
+ children: star
128
+ }, index);
129
+ })
130
+ });
131
+ }
132
+ export default /*#__PURE__*/React.memo(Rating);
@@ -357,6 +357,7 @@ export const FIGMA_COMPONENT_MODES = {
357
357
  "MediaCard": ["Contrast Context"],
358
358
  "MerchantProfile": ["Avatar Size", "Badge Size", "Color Mode", "context 10", "Context4", "Profile Card Appearance"],
359
359
  "MessageField": ["Color Mode", "FormField States"],
360
+ "MetricData": ["AppearanceBrand", "Badge Size", "Color Mode", "context 10", "Context4", "Emphasis", "Profile Card Appearance"],
360
361
  "MetricLegendItem": ["Appearance / DataViz", "Color Mode", "Emphasis / DataViz"],
361
362
  "MoneyValue": ["Color Mode", "Context3"],
362
363
  "MonthlyStatusGrid": ["Appearance / DataViz", "Calendar Glyph State", "Color Mode", "Emphasis / DataViz"],
@@ -375,6 +376,7 @@ export const FIGMA_COMPONENT_MODES = {
375
376
  "ProjectionMarker": ["Color Mode", "context 10", "Profile Card Appearance"],
376
377
  "Radio": ["Color Mode"],
377
378
  "RangeTrack": ["Appearance / DataViz", "Color Mode", "Emphasis / DataViz"],
379
+ "Rating": ["Color Mode", "Rating Star State"],
378
380
  "RechargeCard": ["Avatar Size", "Badge Size", "Color Mode", "Context3", "Context4"],
379
381
  "SavingsGoalSummary": ["Appearance / DataViz", "AppearanceBrand", "AppearanceSystem", "Color Mode", "Emphasis", "Emphasis / DataViz", "LinearProgress Size", "Semantic Intent"],
380
382
  "Screen": ["Color Mode", "Page type"],