jfs-components 0.1.28 → 0.1.32
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/CHANGELOG.md +28 -2
- package/lib/commonjs/components/CategoryCard/CategoryCard.js +264 -0
- package/lib/commonjs/components/CompareTable/CompareTable.js +247 -25
- package/lib/commonjs/components/ContentSheet/ContentSheet.js +91 -8
- package/lib/commonjs/components/CounterBadge/CounterBadge.js +78 -0
- package/lib/commonjs/components/FavoriteToggle/FavoriteToggle.js +180 -0
- package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +287 -0
- package/lib/commonjs/components/MoneyValue/MoneyValue.js +179 -54
- package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +7 -1
- package/lib/commonjs/components/Table/Table.js +27 -7
- package/lib/commonjs/components/ValueBackMetric/ValueBackMetric.js +219 -0
- package/lib/commonjs/components/index.js +35 -0
- package/lib/commonjs/design-tokens/figma-modes.generated.js +3 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/CategoryCard/CategoryCard.js +258 -0
- package/lib/module/components/CompareTable/CompareTable.js +247 -26
- package/lib/module/components/ContentSheet/ContentSheet.js +94 -11
- package/lib/module/components/CounterBadge/CounterBadge.js +73 -0
- package/lib/module/components/FavoriteToggle/FavoriteToggle.js +174 -0
- package/lib/module/components/HelloJioInput/HelloJioInput.js +281 -0
- package/lib/module/components/MoneyValue/MoneyValue.js +182 -57
- package/lib/module/components/PdpCcCard/PdpCcCard.js +7 -1
- package/lib/module/components/Table/Table.js +27 -7
- package/lib/module/components/ValueBackMetric/ValueBackMetric.js +214 -0
- package/lib/module/components/index.js +6 -1
- package/lib/module/design-tokens/figma-modes.generated.js +3 -0
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/CategoryCard/CategoryCard.d.ts +72 -0
- package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +36 -1
- package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +20 -1
- package/lib/typescript/src/components/CounterBadge/CounterBadge.d.ts +19 -0
- package/lib/typescript/src/components/FavoriteToggle/FavoriteToggle.d.ts +66 -0
- package/lib/typescript/src/components/HelloJioInput/HelloJioInput.d.ts +111 -0
- package/lib/typescript/src/components/MoneyValue/MoneyValue.d.ts +10 -1
- package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +25 -1
- package/lib/typescript/src/components/Table/Table.d.ts +9 -1
- package/lib/typescript/src/components/ValueBackMetric/ValueBackMetric.d.ts +86 -0
- package/lib/typescript/src/components/index.d.ts +5 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/CategoryCard/CategoryCard.tsx +404 -0
- package/src/components/CompareTable/CompareTable.tsx +324 -30
- package/src/components/ContentSheet/ContentSheet.tsx +120 -11
- package/src/components/CounterBadge/CounterBadge.tsx +95 -0
- package/src/components/FavoriteToggle/FavoriteToggle.tsx +243 -0
- package/src/components/HelloJioInput/HelloJioInput.tsx +513 -0
- package/src/components/MoneyValue/MoneyValue.tsx +216 -65
- package/src/components/PdpCcCard/PdpCcCard.tsx +36 -1
- package/src/components/Table/Table.tsx +29 -4
- package/src/components/ValueBackMetric/ValueBackMetric.tsx +298 -0
- package/src/components/index.ts +5 -0
- package/src/design-tokens/figma-modes.generated.ts +3 -0
- package/src/icons/registry.ts +1 -1
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React, { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { Platform, Pressable, View, TextInput as RNTextInput } from 'react-native';
|
|
5
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider';
|
|
7
|
+
import Icon from '../../icons/Icon';
|
|
8
|
+
import { EMPTY_MODES, cloneChildrenWithModes, mergeRefs } from '../../utils/react-utils';
|
|
9
|
+
import GlassFill from '../../utils/GlassFill/GlassFill';
|
|
10
|
+
import IconButton from '../IconButton/IconButton';
|
|
11
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
12
|
+
const IS_WEB = Platform.OS === 'web';
|
|
13
|
+
const STATE_COLLECTION = 'HelloJioInput State';
|
|
14
|
+
|
|
15
|
+
/** Default IconButton modes matching the Figma send control. */
|
|
16
|
+
const DEFAULT_SEND_BUTTON_MODES = Object.freeze({
|
|
17
|
+
AppearanceBrand: 'Primary',
|
|
18
|
+
'Button / Size': 'S'
|
|
19
|
+
});
|
|
20
|
+
const toNumber = (value, fallback) => {
|
|
21
|
+
if (typeof value === 'number') {
|
|
22
|
+
return Number.isFinite(value) ? value : fallback;
|
|
23
|
+
}
|
|
24
|
+
if (typeof value === 'string') {
|
|
25
|
+
const parsed = Number(value);
|
|
26
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
27
|
+
}
|
|
28
|
+
return fallback;
|
|
29
|
+
};
|
|
30
|
+
const toFontWeight = (value, fallback) => {
|
|
31
|
+
if (typeof value === 'number') return String(value);
|
|
32
|
+
if (typeof value === 'string') return value;
|
|
33
|
+
return fallback;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Fallback values from the Figma `HelloJioInput` component when local token
|
|
37
|
+
* JSON has not been synced yet. Mode-driven `getVariableByName` wins when
|
|
38
|
+
* tokens resolve.
|
|
39
|
+
*/
|
|
40
|
+
const STATE_FALLBACKS = {
|
|
41
|
+
Idle: {
|
|
42
|
+
background: '#f5f5f5',
|
|
43
|
+
borderColor: '#f5f5f5',
|
|
44
|
+
labelColor: '#545961',
|
|
45
|
+
iconColor: '#545961'
|
|
46
|
+
},
|
|
47
|
+
Active: {
|
|
48
|
+
background: '#ffffff',
|
|
49
|
+
borderColor: '#b5b5b5',
|
|
50
|
+
labelColor: '#24262b',
|
|
51
|
+
iconColor: '#24262b'
|
|
52
|
+
},
|
|
53
|
+
IdleJioPlus: {
|
|
54
|
+
background: '#f5f5f5',
|
|
55
|
+
borderColor: '#f5f5f5',
|
|
56
|
+
labelColor: '#545961',
|
|
57
|
+
iconColor: '#545961'
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
function resolveHelloJioInputTokens(modes, visualState) {
|
|
61
|
+
const fallback = STATE_FALLBACKS[visualState];
|
|
62
|
+
const background = getVariableByName('helloJioInput/background', modes) ?? fallback.background;
|
|
63
|
+
const borderColor = getVariableByName('helloJioInput/border/color', modes) ?? fallback.borderColor;
|
|
64
|
+
const strokeWidth = toNumber(getVariableByName('helloJioInput/strokeWidth', modes), 1);
|
|
65
|
+
const gap = toNumber(getVariableByName('helloJioInput/gap', modes), 8);
|
|
66
|
+
const paddingLeft = toNumber(getVariableByName('helloJioInput/padding/left', modes), 12);
|
|
67
|
+
const paddingRight = toNumber(getVariableByName('helloJioInput/padding/right', modes), 4);
|
|
68
|
+
const paddingTop = toNumber(getVariableByName('helloJioInput/padding/top', modes), 4);
|
|
69
|
+
const paddingBottom = toNumber(getVariableByName('helloJioInput/padding/bottom', modes), 4);
|
|
70
|
+
const height = toNumber(getVariableByName('helloJioInput/height', modes), 36);
|
|
71
|
+
const radiusRaw = toNumber(getVariableByName('helloJioInput/radius', modes), 99999);
|
|
72
|
+
const borderRadius = radiusRaw >= 9999 ? 99999 : radiusRaw;
|
|
73
|
+
const labelColor = getVariableByName('helloJioInput/label/color', modes) ?? fallback.labelColor;
|
|
74
|
+
const fontSize = toNumber(getVariableByName('helloJioInput/fontSize', modes), 14);
|
|
75
|
+
const lineHeight = toNumber(getVariableByName('helloJioInput/lineHeight', modes), 18);
|
|
76
|
+
const fontFamily = getVariableByName('helloJioInput/fontFamily', modes) ?? 'JioType Var';
|
|
77
|
+
const fontWeight = toFontWeight(getVariableByName('helloJioInput/fontWeight', modes), '400');
|
|
78
|
+
const iconColor = getVariableByName('helloJioInput/icon/color', modes) ?? fallback.iconColor;
|
|
79
|
+
const iconSize = toNumber(getVariableByName('textInput/icon/size', modes) ?? getVariableByName('helloJioInput/icon/size', modes), 18);
|
|
80
|
+
const blurMinimal = toNumber(getVariableByName('blur/minimal', modes), 29);
|
|
81
|
+
const blurIntensity = Math.max(0, Math.min(100, Math.round(blurMinimal)));
|
|
82
|
+
const useGlass = visualState === 'IdleJioPlus';
|
|
83
|
+
return {
|
|
84
|
+
containerStyle: {
|
|
85
|
+
position: 'relative',
|
|
86
|
+
flexDirection: 'row',
|
|
87
|
+
alignItems: 'center',
|
|
88
|
+
// Glass keeps the container transparent so GlassFill shows through.
|
|
89
|
+
backgroundColor: useGlass ? 'transparent' : background,
|
|
90
|
+
borderColor,
|
|
91
|
+
borderWidth: strokeWidth,
|
|
92
|
+
borderStyle: 'solid',
|
|
93
|
+
borderRadius,
|
|
94
|
+
paddingLeft,
|
|
95
|
+
paddingRight,
|
|
96
|
+
paddingTop,
|
|
97
|
+
paddingBottom,
|
|
98
|
+
gap,
|
|
99
|
+
height,
|
|
100
|
+
overflow: 'hidden',
|
|
101
|
+
width: '100%'
|
|
102
|
+
},
|
|
103
|
+
inputStyle: {
|
|
104
|
+
flex: 1,
|
|
105
|
+
color: labelColor,
|
|
106
|
+
fontSize,
|
|
107
|
+
fontFamily,
|
|
108
|
+
fontWeight,
|
|
109
|
+
// iOS single-line TextInput + explicit lineHeight sits below center —
|
|
110
|
+
// same quirk TextInput works around. Omit on iOS.
|
|
111
|
+
...(Platform.OS === 'ios' ? null : {
|
|
112
|
+
lineHeight
|
|
113
|
+
}),
|
|
114
|
+
padding: 0,
|
|
115
|
+
margin: 0,
|
|
116
|
+
minHeight: lineHeight,
|
|
117
|
+
outlineStyle: 'none',
|
|
118
|
+
outlineWidth: 0,
|
|
119
|
+
outlineColor: 'transparent'
|
|
120
|
+
},
|
|
121
|
+
iconColor,
|
|
122
|
+
iconSize,
|
|
123
|
+
blurIntensity,
|
|
124
|
+
useGlass
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* `HelloJioInput` is the pill-shaped HelloJio prompt field: leading brand
|
|
130
|
+
* icon, single-line text input, and a gold send `IconButton` on the right.
|
|
131
|
+
*
|
|
132
|
+
* Visual state is derived automatically:
|
|
133
|
+
* - **Idle** — gray fill, gray label (default)
|
|
134
|
+
* - **Active** — white fill + border while focused
|
|
135
|
+
* - **IdleJioPlus** — frosted glass when `jioPlus` and unfocused
|
|
136
|
+
*
|
|
137
|
+
* @component
|
|
138
|
+
*/
|
|
139
|
+
const HelloJioInput = /*#__PURE__*/forwardRef(function HelloJioInput({
|
|
140
|
+
placeholder = 'Ask me anything',
|
|
141
|
+
value,
|
|
142
|
+
defaultValue = '',
|
|
143
|
+
onChangeText,
|
|
144
|
+
onSubmit,
|
|
145
|
+
leadingIconName = 'ic_hellojio',
|
|
146
|
+
leading,
|
|
147
|
+
trailing,
|
|
148
|
+
sendIconName = 'ic_send_message',
|
|
149
|
+
jioPlus = false,
|
|
150
|
+
disabled = false,
|
|
151
|
+
modes: propModes = EMPTY_MODES,
|
|
152
|
+
style,
|
|
153
|
+
inputStyle,
|
|
154
|
+
accessibilityLabel,
|
|
155
|
+
accessibilityHint,
|
|
156
|
+
testID,
|
|
157
|
+
onFocus,
|
|
158
|
+
onBlur,
|
|
159
|
+
returnKeyType = 'send',
|
|
160
|
+
...rest
|
|
161
|
+
}, ref) {
|
|
162
|
+
const {
|
|
163
|
+
modes: globalModes
|
|
164
|
+
} = useTokens();
|
|
165
|
+
const inputRef = useRef(null);
|
|
166
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
167
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
168
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
|
|
169
|
+
const isControlled = value !== undefined;
|
|
170
|
+
const currentValue = isControlled ? value : uncontrolledValue;
|
|
171
|
+
const visualState = isFocused ? 'Active' : jioPlus ? 'IdleJioPlus' : 'Idle';
|
|
172
|
+
const modes = useMemo(() => ({
|
|
173
|
+
...globalModes,
|
|
174
|
+
...propModes,
|
|
175
|
+
[STATE_COLLECTION]: visualState
|
|
176
|
+
}), [globalModes, propModes, visualState]);
|
|
177
|
+
const sendButtonModes = useMemo(() => ({
|
|
178
|
+
...DEFAULT_SEND_BUTTON_MODES,
|
|
179
|
+
...modes
|
|
180
|
+
}), [modes]);
|
|
181
|
+
const tokens = useMemo(() => resolveHelloJioInputTokens(modes, visualState), [modes, visualState]);
|
|
182
|
+
const handleChangeText = useCallback(text => {
|
|
183
|
+
if (!isControlled) setUncontrolledValue(text);
|
|
184
|
+
onChangeText?.(text);
|
|
185
|
+
}, [isControlled, onChangeText]);
|
|
186
|
+
const handleSubmit = useCallback(() => {
|
|
187
|
+
if (disabled) return;
|
|
188
|
+
onSubmit?.(currentValue);
|
|
189
|
+
}, [disabled, onSubmit, currentValue]);
|
|
190
|
+
const handleFocus = useCallback(e => {
|
|
191
|
+
setIsFocused(true);
|
|
192
|
+
onFocus?.(e);
|
|
193
|
+
}, [onFocus]);
|
|
194
|
+
const handleBlur = useCallback(e => {
|
|
195
|
+
setIsFocused(false);
|
|
196
|
+
onBlur?.(e);
|
|
197
|
+
}, [onBlur]);
|
|
198
|
+
const leadingElement = leading ?? (leadingIconName ? /*#__PURE__*/_jsx(Icon, {
|
|
199
|
+
name: leadingIconName,
|
|
200
|
+
size: tokens.iconSize,
|
|
201
|
+
color: tokens.iconColor
|
|
202
|
+
}) : null);
|
|
203
|
+
const processedLeading = leadingElement ? cloneChildrenWithModes(React.Children.toArray(leadingElement), modes) : null;
|
|
204
|
+
const defaultTrailing = /*#__PURE__*/_jsx(IconButton, {
|
|
205
|
+
iconName: sendIconName,
|
|
206
|
+
modes: sendButtonModes,
|
|
207
|
+
onPress: handleSubmit,
|
|
208
|
+
disabled: disabled,
|
|
209
|
+
accessibilityLabel: "Send"
|
|
210
|
+
});
|
|
211
|
+
const trailingElement = trailing === undefined ? defaultTrailing : trailing === null ? null : trailing;
|
|
212
|
+
const processedTrailing = trailingElement ? cloneChildrenWithModes(React.Children.toArray(trailingElement), modes) : null;
|
|
213
|
+
const a11yLabel = accessibilityLabel ?? (placeholder || 'HelloJio input');
|
|
214
|
+
const hoverStyle = isHovered && !disabled && !isFocused ? {
|
|
215
|
+
opacity: 0.95
|
|
216
|
+
} : {};
|
|
217
|
+
const containerStyleArray = [tokens.containerStyle, hoverStyle, disabled ? {
|
|
218
|
+
opacity: 0.5
|
|
219
|
+
} : null, style];
|
|
220
|
+
const glassOverlay = getVariableByName('helloJioInput/background', modes) ?? STATE_FALLBACKS.IdleJioPlus.background;
|
|
221
|
+
const inner = /*#__PURE__*/_jsxs(_Fragment, {
|
|
222
|
+
children: [tokens.useGlass ? /*#__PURE__*/_jsx(GlassFill, {
|
|
223
|
+
tint: "light",
|
|
224
|
+
intensity: tokens.blurIntensity,
|
|
225
|
+
overlayColor: glassOverlay,
|
|
226
|
+
androidTintWash: false
|
|
227
|
+
}) : null, processedLeading ? /*#__PURE__*/_jsx(View, {
|
|
228
|
+
accessibilityElementsHidden: true,
|
|
229
|
+
importantForAccessibility: "no",
|
|
230
|
+
style: SLOT_CENTER,
|
|
231
|
+
children: processedLeading
|
|
232
|
+
}) : null, /*#__PURE__*/_jsx(RNTextInput, {
|
|
233
|
+
ref: mergeRefs(inputRef, ref),
|
|
234
|
+
accessibilityLabel: a11yLabel,
|
|
235
|
+
accessibilityHint: accessibilityHint,
|
|
236
|
+
placeholder: isFocused ? '' : placeholder,
|
|
237
|
+
placeholderTextColor: tokens.inputStyle.color,
|
|
238
|
+
value: currentValue,
|
|
239
|
+
onChangeText: handleChangeText,
|
|
240
|
+
onFocus: handleFocus,
|
|
241
|
+
onBlur: handleBlur,
|
|
242
|
+
onSubmitEditing: handleSubmit,
|
|
243
|
+
returnKeyType: returnKeyType,
|
|
244
|
+
editable: !disabled,
|
|
245
|
+
style: [tokens.inputStyle, inputStyle],
|
|
246
|
+
testID: testID,
|
|
247
|
+
...rest
|
|
248
|
+
}), processedTrailing ? /*#__PURE__*/_jsx(View
|
|
249
|
+
// Keep the send button in the a11y tree — it has its own label.
|
|
250
|
+
, {
|
|
251
|
+
style: SLOT_CENTER,
|
|
252
|
+
children: processedTrailing
|
|
253
|
+
}) : null]
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Same Android focus rule as TextInput: never wrap native TextInput in
|
|
257
|
+
// Pressable on native (steals the first tap). Web keeps Pressable for
|
|
258
|
+
// hover + click-anywhere-to-focus.
|
|
259
|
+
if (IS_WEB) {
|
|
260
|
+
return /*#__PURE__*/_jsx(Pressable, {
|
|
261
|
+
style: containerStyleArray,
|
|
262
|
+
onHoverIn: () => setIsHovered(true),
|
|
263
|
+
onHoverOut: () => setIsHovered(false),
|
|
264
|
+
onPress: () => {
|
|
265
|
+
if (!disabled) inputRef.current?.focus();
|
|
266
|
+
},
|
|
267
|
+
accessible: false,
|
|
268
|
+
disabled: disabled,
|
|
269
|
+
children: inner
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return /*#__PURE__*/_jsx(View, {
|
|
273
|
+
style: containerStyleArray,
|
|
274
|
+
children: inner
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
const SLOT_CENTER = {
|
|
278
|
+
justifyContent: 'center',
|
|
279
|
+
alignItems: 'center'
|
|
280
|
+
};
|
|
281
|
+
export default /*#__PURE__*/React.memo(HelloJioInput);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React, { useMemo, useEffect, useRef } from 'react';
|
|
4
|
-
import { View, Text, Pressable, Animated } from 'react-native';
|
|
3
|
+
import React, { useMemo, useEffect, useRef, useState, useCallback } from 'react';
|
|
4
|
+
import { View, Text, TextInput, Pressable, Animated, Platform, Keyboard } from 'react-native';
|
|
5
5
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
6
|
import { EMPTY_MODES } from '../../utils/react-utils';
|
|
7
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
8
8
|
// Map of common ISO 4217 currency codes to display symbols
|
|
9
9
|
const CURRENCY_SYMBOLS = {
|
|
10
10
|
INR: '₹',
|
|
@@ -56,6 +56,8 @@ function MoneyValue({
|
|
|
56
56
|
negative,
|
|
57
57
|
focused = false,
|
|
58
58
|
hidden = false,
|
|
59
|
+
editable = false,
|
|
60
|
+
onValueChange,
|
|
59
61
|
modes = EMPTY_MODES,
|
|
60
62
|
style,
|
|
61
63
|
valueStyle,
|
|
@@ -67,24 +69,51 @@ function MoneyValue({
|
|
|
67
69
|
...rest
|
|
68
70
|
}) {
|
|
69
71
|
// Auto-detect negative from value and compute display value
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
} = useMemo(() => {
|
|
74
|
-
const stringValue = String(value);
|
|
75
|
-
const trimmed = stringValue.trim();
|
|
76
|
-
const valueIsNegative = trimmed.startsWith('-') || typeof value === 'number' && value < 0;
|
|
72
|
+
const stringValue = String(value);
|
|
73
|
+
const trimmed = stringValue.trim();
|
|
74
|
+
const valueIsNegative = trimmed.startsWith('-') || typeof value === 'number' && value < 0;
|
|
77
75
|
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
// Strip leading minus sign for display (we show it separately)
|
|
77
|
+
const absoluteValue = trimmed.startsWith('-') ? trimmed.slice(1) : trimmed;
|
|
80
78
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
79
|
+
// Use explicit negative prop if provided, otherwise use auto-detected
|
|
80
|
+
const showNegative = negative !== undefined ? negative : valueIsNegative;
|
|
81
|
+
const isNegative = hidden ? false : showNegative;
|
|
82
|
+
const displayValue = hidden ? '•••' : absoluteValue;
|
|
83
|
+
|
|
84
|
+
// Inline editing state
|
|
85
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
86
|
+
const [editBuffer, setEditBuffer] = useState('');
|
|
87
|
+
const inputRef = useRef(null);
|
|
88
|
+
// Always-current snapshot of the display value for callbacks to reference.
|
|
89
|
+
const displayValueRef = useRef(displayValue);
|
|
90
|
+
displayValueRef.current = displayValue;
|
|
91
|
+
const enterEditMode = useCallback(() => {
|
|
92
|
+
setIsEditing(true);
|
|
93
|
+
setEditBuffer(displayValueRef.current);
|
|
94
|
+
// Focus the input on next frame so the keyboard opens
|
|
95
|
+
setTimeout(() => inputRef.current?.focus(), 0);
|
|
96
|
+
}, []);
|
|
97
|
+
const commitEdit = useCallback(() => {
|
|
98
|
+
setIsEditing(false);
|
|
99
|
+
const resolved = editBuffer || '0';
|
|
100
|
+
if (resolved !== displayValueRef.current) {
|
|
101
|
+
onValueChange?.(resolved);
|
|
102
|
+
}
|
|
103
|
+
}, [editBuffer, onValueChange]);
|
|
104
|
+
const handleChangeText = useCallback(text => {
|
|
105
|
+
// Allow only digits and at most one decimal point
|
|
106
|
+
const filtered = text.replace(/[^0-9.]/g, '');
|
|
107
|
+
const parts = filtered.split('.');
|
|
108
|
+
const clean = parts.length > 2 ? parts[0] + '.' + parts.slice(1).join('') : filtered;
|
|
109
|
+
setEditBuffer(clean);
|
|
110
|
+
}, []);
|
|
111
|
+
const handlePress = useCallback(event => {
|
|
112
|
+
if (editable) {
|
|
113
|
+
enterEditMode();
|
|
114
|
+
}
|
|
115
|
+
onPress?.(event);
|
|
116
|
+
}, [editable, enterEditMode, onPress]);
|
|
88
117
|
|
|
89
118
|
// Resolve typography and layout tokens from Figma
|
|
90
119
|
const textColor = getVariableByName('moneyValue/text/color', modes);
|
|
@@ -145,44 +174,140 @@ function MoneyValue({
|
|
|
145
174
|
cursorOpacity.setValue(0);
|
|
146
175
|
}
|
|
147
176
|
}, [focused, cursorOpacity]);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
177
|
+
|
|
178
|
+
// Keyboard avoidance — lift the component when the keyboard would cover it.
|
|
179
|
+
// Follows the same pattern as ActionFooter (uses Keyboard.addListener).
|
|
180
|
+
// Resets when editing ends or the keyboard hides.
|
|
181
|
+
const rootRef = useRef(null);
|
|
182
|
+
const keyboardLift = useRef(new Animated.Value(0)).current;
|
|
183
|
+
const rootWindowYRef = useRef(0);
|
|
184
|
+
const rootMeasuredHeightRef = useRef(0);
|
|
185
|
+
const measureRoot = useCallback(() => {
|
|
186
|
+
rootRef.current?.measureInWindow((_x, y, _w, h) => {
|
|
187
|
+
rootWindowYRef.current = y;
|
|
188
|
+
rootMeasuredHeightRef.current = h;
|
|
189
|
+
});
|
|
190
|
+
}, []);
|
|
191
|
+
const handleRootLayout = useCallback(_e => {
|
|
192
|
+
// measureInWindow gives the component's position relative to the window,
|
|
193
|
+
// which is what we need for keyboard overlap calculation. onLayout fires
|
|
194
|
+
// after the component mounts and whenever its size changes.
|
|
195
|
+
measureRoot();
|
|
196
|
+
}, [measureRoot]);
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (!editable) return;
|
|
199
|
+
const showSub = Keyboard.addListener('keyboardDidShow', e => {
|
|
200
|
+
measureRoot();
|
|
201
|
+
if (!isEditing) return;
|
|
202
|
+
const kbHeight = e.endCoordinates.height;
|
|
203
|
+
const {
|
|
204
|
+
height: screenH
|
|
205
|
+
} = require('react-native').Dimensions.get('window');
|
|
206
|
+
const componentBottom = rootWindowYRef.current + rootMeasuredHeightRef.current;
|
|
207
|
+
const visibleBottom = screenH - kbHeight;
|
|
208
|
+
const overlap = componentBottom - visibleBottom + 12;
|
|
209
|
+
if (overlap > 0) {
|
|
210
|
+
Animated.timing(keyboardLift, {
|
|
211
|
+
toValue: -overlap,
|
|
212
|
+
duration: e.duration ?? 250,
|
|
213
|
+
useNativeDriver: true
|
|
214
|
+
}).start();
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
const hideSub = Keyboard.addListener('keyboardDidHide', () => {
|
|
218
|
+
Animated.timing(keyboardLift, {
|
|
219
|
+
toValue: 0,
|
|
220
|
+
duration: 150,
|
|
221
|
+
useNativeDriver: true
|
|
222
|
+
}).start();
|
|
223
|
+
});
|
|
224
|
+
return () => {
|
|
225
|
+
showSub.remove();
|
|
226
|
+
hideSub.remove();
|
|
227
|
+
};
|
|
228
|
+
}, [editable, isEditing, keyboardLift, measureRoot]);
|
|
229
|
+
|
|
230
|
+
// Reset lift when editing ends
|
|
231
|
+
useEffect(() => {
|
|
232
|
+
if (!isEditing) {
|
|
233
|
+
keyboardLift.setValue(0);
|
|
234
|
+
}
|
|
235
|
+
}, [isEditing, keyboardLift]);
|
|
236
|
+
const handleBlur = useCallback(() => {
|
|
237
|
+
commitEdit();
|
|
238
|
+
}, [commitEdit]);
|
|
239
|
+
const handleSubmitEditing = useCallback(() => {
|
|
240
|
+
commitEdit();
|
|
241
|
+
}, [commitEdit]);
|
|
242
|
+
return /*#__PURE__*/_jsx(Animated.View, {
|
|
243
|
+
style: {
|
|
244
|
+
transform: [{
|
|
245
|
+
translateY: keyboardLift
|
|
246
|
+
}]
|
|
247
|
+
},
|
|
248
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
249
|
+
ref: rootRef,
|
|
250
|
+
onLayout: handleRootLayout,
|
|
251
|
+
children: /*#__PURE__*/_jsxs(Pressable, {
|
|
252
|
+
style: [containerStyle, style],
|
|
253
|
+
accessibilityRole: "text",
|
|
254
|
+
accessibilityLabel: undefined,
|
|
255
|
+
accessibilityHint: accessibilityHint,
|
|
256
|
+
onPress: handlePress,
|
|
257
|
+
disabled: !onPress && !editable,
|
|
258
|
+
...rest,
|
|
259
|
+
children: [isNegative && /*#__PURE__*/_jsx(Text, {
|
|
260
|
+
style: [currencyTextStyle, negativeSignStyle],
|
|
261
|
+
accessibilityElementsHidden: true,
|
|
262
|
+
importantForAccessibility: "no",
|
|
263
|
+
children: "-"
|
|
264
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
265
|
+
style: [currencyTextStyle, currencyStyle],
|
|
266
|
+
accessibilityElementsHidden: true,
|
|
267
|
+
importantForAccessibility: "no",
|
|
268
|
+
children: resolvedCurrency
|
|
269
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
270
|
+
style: {
|
|
271
|
+
flexDirection: 'row',
|
|
272
|
+
alignItems: 'center'
|
|
273
|
+
},
|
|
274
|
+
children: isEditing ? /*#__PURE__*/_jsx(TextInput, {
|
|
275
|
+
ref: inputRef,
|
|
276
|
+
value: editBuffer,
|
|
277
|
+
onChangeText: handleChangeText,
|
|
278
|
+
onBlur: handleBlur,
|
|
279
|
+
onSubmitEditing: handleSubmitEditing,
|
|
280
|
+
keyboardType: "decimal-pad",
|
|
281
|
+
returnKeyType: "done",
|
|
282
|
+
selectTextOnFocus: true,
|
|
283
|
+
style: [valueTextStyle, valueStyle, {
|
|
284
|
+
padding: 0,
|
|
285
|
+
margin: 0,
|
|
286
|
+
// On web a thin border helps the cursor appear precisely
|
|
287
|
+
...(Platform.OS === 'web' ? {
|
|
288
|
+
outlineStyle: 'none'
|
|
289
|
+
} : {})
|
|
290
|
+
}],
|
|
291
|
+
accessibilityLabel: accessibilityLabel || 'Edit value'
|
|
292
|
+
}) : /*#__PURE__*/_jsxs(_Fragment, {
|
|
293
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
294
|
+
style: [valueTextStyle, valueStyle],
|
|
295
|
+
accessibilityElementsHidden: true,
|
|
296
|
+
importantForAccessibility: "no",
|
|
297
|
+
children: displayValue
|
|
298
|
+
}), focused && /*#__PURE__*/_jsx(Animated.View, {
|
|
299
|
+
style: {
|
|
300
|
+
opacity: cursorOpacity,
|
|
301
|
+
width: 2,
|
|
302
|
+
height: valueFontSize * 1.1,
|
|
303
|
+
backgroundColor: textColor,
|
|
304
|
+
marginLeft: 2
|
|
305
|
+
}
|
|
306
|
+
})]
|
|
307
|
+
})
|
|
308
|
+
})]
|
|
309
|
+
})
|
|
310
|
+
})
|
|
186
311
|
});
|
|
187
312
|
}
|
|
188
313
|
export default MoneyValue;
|
|
@@ -39,6 +39,9 @@ function PdpCcCard({
|
|
|
39
39
|
media,
|
|
40
40
|
title = 'Title',
|
|
41
41
|
subtitle = 'Subtitle',
|
|
42
|
+
numberOfLines,
|
|
43
|
+
disableTruncation,
|
|
44
|
+
singleLine,
|
|
42
45
|
metrics = DEFAULT_METRICS,
|
|
43
46
|
buttonLabel = 'button',
|
|
44
47
|
buttonIcon = 'ic_add_circle',
|
|
@@ -90,7 +93,10 @@ function PdpCcCard({
|
|
|
90
93
|
title: title,
|
|
91
94
|
subtitle: subtitle,
|
|
92
95
|
textAlign: "Center",
|
|
93
|
-
modes: modes
|
|
96
|
+
modes: modes,
|
|
97
|
+
numberOfLines: numberOfLines,
|
|
98
|
+
disableTruncation: disableTruncation,
|
|
99
|
+
singleLine: singleLine
|
|
94
100
|
}), metrics.length > 0 ? /*#__PURE__*/_jsx(View, {
|
|
95
101
|
style: styles.metricsRow,
|
|
96
102
|
children: metrics.map((metric, index) => /*#__PURE__*/_jsxs(React.Fragment, {
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React from 'react';
|
|
3
|
+
import React, { createContext, useContext } from 'react';
|
|
4
4
|
import { View, Text, Pressable, ScrollView, Platform } from 'react-native';
|
|
5
5
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
6
|
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
|
|
7
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
8
|
+
/**
|
|
9
|
+
* Carries the root `Table`'s `columnWidth` down to every cell so that, when it
|
|
10
|
+
* is set, all columns (header + body, data-driven or composed) share the same
|
|
11
|
+
* fixed width without each cell having to repeat the prop. Cells still honour
|
|
12
|
+
* an explicit `width` prop over this context value. `undefined` (the default,
|
|
13
|
+
* when no `Table` ancestor is present, or `columnWidth` is unset) leaves cells
|
|
14
|
+
* to flex — preserving the original behaviour.
|
|
15
|
+
*/
|
|
16
|
+
const TableContext = /*#__PURE__*/createContext(undefined);
|
|
7
17
|
|
|
8
18
|
/* -------------------------------------------------------------------------------------------------
|
|
9
19
|
* Shared types & token resolution
|
|
10
20
|
* -----------------------------------------------------------------------------------------------*/
|
|
11
21
|
|
|
12
22
|
/** Resolved styling for a header cell and a body cell, derived from design tokens. */
|
|
13
|
-
|
|
23
|
+
|
|
14
24
|
const toWeight = w => typeof w === 'number' ? `${w}` : w;
|
|
15
25
|
|
|
16
26
|
/**
|
|
@@ -88,6 +98,10 @@ function TableCell({
|
|
|
88
98
|
style
|
|
89
99
|
}) {
|
|
90
100
|
const t = resolveTableTokens(modes);
|
|
101
|
+
// A cell's width resolves in priority order: an explicit `width` prop, then
|
|
102
|
+
// the root Table's `columnWidth` (via context), then fall back to flex.
|
|
103
|
+
const ctxColumnWidth = useContext(TableContext);
|
|
104
|
+
const effectiveWidth = width ?? ctxColumnWidth;
|
|
91
105
|
return /*#__PURE__*/_jsx(View, {
|
|
92
106
|
style: [{
|
|
93
107
|
backgroundColor: t.cellBg,
|
|
@@ -99,8 +113,8 @@ function TableCell({
|
|
|
99
113
|
borderColor: t.borderColor,
|
|
100
114
|
borderBottomWidth: isLastRow ? 0 : t.borderSize,
|
|
101
115
|
borderRightWidth: isLastColumn ? 0 : t.borderSize,
|
|
102
|
-
...(
|
|
103
|
-
width
|
|
116
|
+
...(effectiveWidth != null ? {
|
|
117
|
+
width: effectiveWidth
|
|
104
118
|
} : {
|
|
105
119
|
flex,
|
|
106
120
|
minWidth: 0
|
|
@@ -128,6 +142,8 @@ function TableHeaderCell({
|
|
|
128
142
|
style
|
|
129
143
|
}) {
|
|
130
144
|
const t = resolveTableTokens(modes);
|
|
145
|
+
const ctxColumnWidth = useContext(TableContext);
|
|
146
|
+
const effectiveWidth = width ?? ctxColumnWidth;
|
|
131
147
|
return /*#__PURE__*/_jsx(View, {
|
|
132
148
|
style: [{
|
|
133
149
|
backgroundColor: t.headerBg,
|
|
@@ -136,8 +152,8 @@ function TableHeaderCell({
|
|
|
136
152
|
justifyContent: alignToJustify(align),
|
|
137
153
|
paddingHorizontal: t.headerPaddingH,
|
|
138
154
|
paddingVertical: t.headerPaddingV,
|
|
139
|
-
...(
|
|
140
|
-
width
|
|
155
|
+
...(effectiveWidth != null ? {
|
|
156
|
+
width: effectiveWidth
|
|
141
157
|
} : {
|
|
142
158
|
flex,
|
|
143
159
|
minWidth: 0
|
|
@@ -252,6 +268,7 @@ function Table({
|
|
|
252
268
|
getRowKey,
|
|
253
269
|
onRowPress,
|
|
254
270
|
scrollable = false,
|
|
271
|
+
columnWidth,
|
|
255
272
|
accessibilityLabel,
|
|
256
273
|
modes = EMPTY_MODES,
|
|
257
274
|
style
|
|
@@ -295,7 +312,10 @@ function Table({
|
|
|
295
312
|
}, !scrollable && {
|
|
296
313
|
width: '100%'
|
|
297
314
|
}, style],
|
|
298
|
-
children:
|
|
315
|
+
children: /*#__PURE__*/_jsx(TableContext.Provider, {
|
|
316
|
+
value: columnWidth,
|
|
317
|
+
children: body
|
|
318
|
+
})
|
|
299
319
|
});
|
|
300
320
|
if (scrollable) {
|
|
301
321
|
return /*#__PURE__*/_jsx(ScrollView, {
|