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.
Files changed (53) hide show
  1. package/CHANGELOG.md +28 -2
  2. package/lib/commonjs/components/CategoryCard/CategoryCard.js +264 -0
  3. package/lib/commonjs/components/CompareTable/CompareTable.js +247 -25
  4. package/lib/commonjs/components/ContentSheet/ContentSheet.js +91 -8
  5. package/lib/commonjs/components/CounterBadge/CounterBadge.js +78 -0
  6. package/lib/commonjs/components/FavoriteToggle/FavoriteToggle.js +180 -0
  7. package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +287 -0
  8. package/lib/commonjs/components/MoneyValue/MoneyValue.js +179 -54
  9. package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +7 -1
  10. package/lib/commonjs/components/Table/Table.js +27 -7
  11. package/lib/commonjs/components/ValueBackMetric/ValueBackMetric.js +219 -0
  12. package/lib/commonjs/components/index.js +35 -0
  13. package/lib/commonjs/design-tokens/figma-modes.generated.js +3 -0
  14. package/lib/commonjs/icons/registry.js +1 -1
  15. package/lib/module/components/CategoryCard/CategoryCard.js +258 -0
  16. package/lib/module/components/CompareTable/CompareTable.js +247 -26
  17. package/lib/module/components/ContentSheet/ContentSheet.js +94 -11
  18. package/lib/module/components/CounterBadge/CounterBadge.js +73 -0
  19. package/lib/module/components/FavoriteToggle/FavoriteToggle.js +174 -0
  20. package/lib/module/components/HelloJioInput/HelloJioInput.js +281 -0
  21. package/lib/module/components/MoneyValue/MoneyValue.js +182 -57
  22. package/lib/module/components/PdpCcCard/PdpCcCard.js +7 -1
  23. package/lib/module/components/Table/Table.js +27 -7
  24. package/lib/module/components/ValueBackMetric/ValueBackMetric.js +214 -0
  25. package/lib/module/components/index.js +6 -1
  26. package/lib/module/design-tokens/figma-modes.generated.js +3 -0
  27. package/lib/module/icons/registry.js +1 -1
  28. package/lib/typescript/src/components/CategoryCard/CategoryCard.d.ts +72 -0
  29. package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +36 -1
  30. package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +20 -1
  31. package/lib/typescript/src/components/CounterBadge/CounterBadge.d.ts +19 -0
  32. package/lib/typescript/src/components/FavoriteToggle/FavoriteToggle.d.ts +66 -0
  33. package/lib/typescript/src/components/HelloJioInput/HelloJioInput.d.ts +111 -0
  34. package/lib/typescript/src/components/MoneyValue/MoneyValue.d.ts +10 -1
  35. package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +25 -1
  36. package/lib/typescript/src/components/Table/Table.d.ts +9 -1
  37. package/lib/typescript/src/components/ValueBackMetric/ValueBackMetric.d.ts +86 -0
  38. package/lib/typescript/src/components/index.d.ts +5 -0
  39. package/lib/typescript/src/icons/registry.d.ts +1 -1
  40. package/package.json +1 -1
  41. package/src/components/CategoryCard/CategoryCard.tsx +404 -0
  42. package/src/components/CompareTable/CompareTable.tsx +324 -30
  43. package/src/components/ContentSheet/ContentSheet.tsx +120 -11
  44. package/src/components/CounterBadge/CounterBadge.tsx +95 -0
  45. package/src/components/FavoriteToggle/FavoriteToggle.tsx +243 -0
  46. package/src/components/HelloJioInput/HelloJioInput.tsx +513 -0
  47. package/src/components/MoneyValue/MoneyValue.tsx +216 -65
  48. package/src/components/PdpCcCard/PdpCcCard.tsx +36 -1
  49. package/src/components/Table/Table.tsx +29 -4
  50. package/src/components/ValueBackMetric/ValueBackMetric.tsx +298 -0
  51. package/src/components/index.ts +5 -0
  52. package/src/design-tokens/figma-modes.generated.ts +3 -0
  53. 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
- isNegative,
72
- displayValue
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
- // Strip leading minus sign for display (we show it separately)
79
- const absoluteValue = trimmed.startsWith('-') ? trimmed.slice(1) : trimmed;
76
+ // Strip leading minus sign for display (we show it separately)
77
+ const absoluteValue = trimmed.startsWith('-') ? trimmed.slice(1) : trimmed;
80
78
 
81
- // Use explicit negative prop if provided, otherwise use auto-detected
82
- const showNegative = negative !== undefined ? negative : valueIsNegative;
83
- return {
84
- isNegative: hidden ? false : showNegative,
85
- displayValue: hidden ? '•••' : absoluteValue
86
- };
87
- }, [value, negative, hidden]);
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
- return /*#__PURE__*/_jsxs(Pressable, {
149
- style: [containerStyle, style],
150
- accessibilityRole: "text",
151
- accessibilityLabel: undefined,
152
- accessibilityHint: accessibilityHint,
153
- onPress: onPress,
154
- disabled: !onPress,
155
- ...rest,
156
- children: [isNegative && /*#__PURE__*/_jsx(Text, {
157
- style: [currencyTextStyle, negativeSignStyle],
158
- accessibilityElementsHidden: true,
159
- importantForAccessibility: "no",
160
- children: "-"
161
- }), /*#__PURE__*/_jsx(Text, {
162
- style: [currencyTextStyle, currencyStyle],
163
- accessibilityElementsHidden: true,
164
- importantForAccessibility: "no",
165
- children: resolvedCurrency
166
- }), /*#__PURE__*/_jsxs(View, {
167
- style: {
168
- flexDirection: 'row',
169
- alignItems: 'center'
170
- },
171
- children: [/*#__PURE__*/_jsx(Text, {
172
- style: [valueTextStyle, valueStyle],
173
- accessibilityElementsHidden: true,
174
- importantForAccessibility: "no",
175
- children: displayValue
176
- }), focused && /*#__PURE__*/_jsx(Animated.View, {
177
- style: {
178
- opacity: cursorOpacity,
179
- width: 2,
180
- height: valueFontSize * 1.1,
181
- backgroundColor: textColor,
182
- marginLeft: 2
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
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
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
- ...(width != null ? {
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
- ...(width != null ? {
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: body
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, {