jfs-components 0.1.2 → 0.1.8

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 (107) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/lib/commonjs/components/AmountInput/AmountInput.js +8 -5
  3. package/lib/commonjs/components/BenefitCard/BenefitCard.js +231 -0
  4. package/lib/commonjs/components/CcCard/CcCard.js +470 -0
  5. package/lib/commonjs/components/Checkbox/Checkbox.js +4 -3
  6. package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +4 -3
  7. package/lib/commonjs/components/CompareTable/CompareTable.js +372 -0
  8. package/lib/commonjs/components/ComparisonBar/ComparisonBar.js +266 -0
  9. package/lib/commonjs/components/DropdownInput/DropdownInput.js +35 -3
  10. package/lib/commonjs/components/FormField/FormField.js +4 -3
  11. package/lib/commonjs/components/InputSearch/InputSearch.js +6 -4
  12. package/lib/commonjs/components/NoteInput/NoteInput.js +6 -5
  13. package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +273 -0
  14. package/lib/commonjs/components/ProductMerchandisingCard/GlassFill.js +263 -0
  15. package/lib/commonjs/components/ProductMerchandisingCard/GlassFill.web.js +116 -0
  16. package/lib/commonjs/components/ProductMerchandisingCard/ProductMerchandisingCard.js +353 -0
  17. package/lib/commonjs/components/ProjectionMarker/ProjectionMarker.js +161 -0
  18. package/lib/commonjs/components/Radio/Radio.js +5 -5
  19. package/lib/commonjs/components/Slider/Slider.js +473 -0
  20. package/lib/commonjs/components/TextInput/TextInput.js +13 -8
  21. package/lib/commonjs/components/TextSegment/TextSegment.js +118 -0
  22. package/lib/commonjs/components/index.js +63 -0
  23. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  24. package/lib/commonjs/design-tokens/figma-modes.generated.js +38 -9
  25. package/lib/commonjs/icons/registry.js +1 -1
  26. package/lib/commonjs/utils/react-utils.js +22 -0
  27. package/lib/module/components/AmountInput/AmountInput.js +6 -4
  28. package/lib/module/components/BenefitCard/BenefitCard.js +225 -0
  29. package/lib/module/components/CcCard/CcCard.js +464 -0
  30. package/lib/module/components/Checkbox/Checkbox.js +5 -4
  31. package/lib/module/components/CheckboxItem/CheckboxItem.js +5 -4
  32. package/lib/module/components/CompareTable/CompareTable.js +367 -0
  33. package/lib/module/components/ComparisonBar/ComparisonBar.js +260 -0
  34. package/lib/module/components/DropdownInput/DropdownInput.js +36 -4
  35. package/lib/module/components/FormField/FormField.js +5 -4
  36. package/lib/module/components/InputSearch/InputSearch.js +6 -4
  37. package/lib/module/components/NoteInput/NoteInput.js +7 -6
  38. package/lib/module/components/PdpCcCard/PdpCcCard.js +267 -0
  39. package/lib/module/components/ProductMerchandisingCard/GlassFill.js +257 -0
  40. package/lib/module/components/ProductMerchandisingCard/GlassFill.web.js +111 -0
  41. package/lib/module/components/ProductMerchandisingCard/ProductMerchandisingCard.js +347 -0
  42. package/lib/module/components/ProjectionMarker/ProjectionMarker.js +156 -0
  43. package/lib/module/components/Radio/Radio.js +5 -4
  44. package/lib/module/components/Slider/Slider.js +468 -0
  45. package/lib/module/components/TextInput/TextInput.js +15 -10
  46. package/lib/module/components/TextSegment/TextSegment.js +113 -0
  47. package/lib/module/components/index.js +9 -0
  48. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  49. package/lib/module/design-tokens/figma-modes.generated.js +38 -9
  50. package/lib/module/icons/registry.js +1 -1
  51. package/lib/module/utils/react-utils.js +21 -0
  52. package/lib/typescript/src/components/AmountInput/AmountInput.d.ts +3 -2
  53. package/lib/typescript/src/components/BenefitCard/BenefitCard.d.ts +93 -0
  54. package/lib/typescript/src/components/CcCard/CcCard.d.ts +137 -0
  55. package/lib/typescript/src/components/Checkbox/Checkbox.d.ts +3 -2
  56. package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +2 -2
  57. package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +88 -0
  58. package/lib/typescript/src/components/ComparisonBar/ComparisonBar.d.ts +118 -0
  59. package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +20 -1
  60. package/lib/typescript/src/components/FormField/FormField.d.ts +2 -2
  61. package/lib/typescript/src/components/InputSearch/InputSearch.d.ts +23 -2
  62. package/lib/typescript/src/components/NoteInput/NoteInput.d.ts +19 -2
  63. package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +84 -0
  64. package/lib/typescript/src/components/ProductMerchandisingCard/GlassFill.d.ts +56 -0
  65. package/lib/typescript/src/components/ProductMerchandisingCard/GlassFill.web.d.ts +27 -0
  66. package/lib/typescript/src/components/ProductMerchandisingCard/ProductMerchandisingCard.d.ts +81 -0
  67. package/lib/typescript/src/components/ProjectionMarker/ProjectionMarker.d.ts +82 -0
  68. package/lib/typescript/src/components/Radio/Radio.d.ts +3 -2
  69. package/lib/typescript/src/components/RadioButton/RadioButton.d.ts +2 -2
  70. package/lib/typescript/src/components/Slider/Slider.d.ts +99 -0
  71. package/lib/typescript/src/components/TextInput/TextInput.d.ts +9 -29
  72. package/lib/typescript/src/components/TextSegment/TextSegment.d.ts +100 -0
  73. package/lib/typescript/src/components/index.d.ts +10 -1
  74. package/lib/typescript/src/design-tokens/figma-modes.generated.d.ts +22 -2
  75. package/lib/typescript/src/icons/registry.d.ts +1 -1
  76. package/lib/typescript/src/utils/react-utils.d.ts +10 -0
  77. package/package.json +2 -1
  78. package/src/components/AmountInput/AmountInput.tsx +7 -5
  79. package/src/components/BenefitCard/BenefitCard.tsx +309 -0
  80. package/src/components/CcCard/CcCard.tsx +598 -0
  81. package/src/components/Checkbox/Checkbox.tsx +5 -4
  82. package/src/components/CheckboxItem/CheckboxItem.tsx +5 -4
  83. package/src/components/CompareTable/CompareTable.tsx +477 -0
  84. package/src/components/ComparisonBar/ComparisonBar.tsx +356 -0
  85. package/src/components/DropdownInput/DropdownInput.tsx +55 -3
  86. package/src/components/FormField/FormField.tsx +5 -4
  87. package/src/components/InputSearch/InputSearch.tsx +8 -5
  88. package/src/components/NoteInput/NoteInput.tsx +8 -6
  89. package/src/components/PdpCcCard/PdpCcCard.tsx +356 -0
  90. package/src/components/ProductMerchandisingCard/GlassFill.tsx +276 -0
  91. package/src/components/ProductMerchandisingCard/GlassFill.web.tsx +127 -0
  92. package/src/components/ProductMerchandisingCard/ProductMerchandisingCard.tsx +423 -0
  93. package/src/components/ProjectionMarker/ProjectionMarker.tsx +277 -0
  94. package/src/components/Radio/Radio.tsx +5 -4
  95. package/src/components/Slider/Slider.tsx +628 -0
  96. package/src/components/TextInput/TextInput.tsx +15 -11
  97. package/src/components/TextSegment/TextSegment.tsx +166 -0
  98. package/src/components/index.ts +10 -1
  99. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  100. package/src/design-tokens/figma-modes.generated.ts +38 -9
  101. package/src/icons/registry.ts +1 -1
  102. package/src/utils/react-utils.ts +23 -0
  103. package/lib/typescript/scripts/extract-component-tokens.d.ts +0 -9
  104. package/lib/typescript/scripts/generate-component-docs.d.ts +0 -9
  105. package/lib/typescript/scripts/generate-icon-registry.d.ts +0 -3
  106. package/lib/typescript/scripts/generate-mode-types.d.ts +0 -2
  107. package/lib/typescript/scripts/retype-modes.d.cts +0 -2
@@ -0,0 +1,468 @@
1
+ "use strict";
2
+
3
+ import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
4
+ import { View, Text, PanResponder, Platform } from 'react-native';
5
+ import Svg, { Path } from 'react-native-svg';
6
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
7
+ import { EMPTY_MODES } from '../../utils/react-utils';
8
+
9
+ /**
10
+ * Imperative handle exposed via `ref`. The primary way to read a slider's value
11
+ * is still the controlled `value` + `onChange` pair (each `<Slider />` reports
12
+ * its own value, so multiple sliders are disambiguated by their own handlers).
13
+ * The ref is a convenience for reading the latest value on demand (e.g. on a
14
+ * button press) or imperatively nudging an uncontrolled slider.
15
+ *
16
+ * @example
17
+ * const slider = useRef<SliderHandle>(null)
18
+ * slider.current?.setValue(50)
19
+ * const v = slider.current?.getValue()
20
+ */
21
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
22
+ const clamp = (n, min, max) => Math.min(Math.max(n, min), max);
23
+ const asNum = (raw, fallback) => {
24
+ const n = typeof raw === 'number' ? raw : parseFloat(raw);
25
+ return Number.isFinite(n) ? n : fallback;
26
+ };
27
+ const asStr = (raw, fallback) => raw != null ? String(raw) : fallback;
28
+
29
+ // The unfilled track is rendered at a lighter emphasis in the design.
30
+ const TRACK_EMPHASIS = {
31
+ Emphasis: 'Low'
32
+ };
33
+
34
+ /**
35
+ * Slider — Figma node 5373:446 ("Slider").
36
+ *
37
+ * A horizontal, single-thumb slider driven entirely by design tokens. It maps a
38
+ * numeric range (`minValue`–`maxValue`) onto a track with a filled indicator, a
39
+ * draggable handle, a value bubble pinned above the handle, and optional min/max
40
+ * labels beneath the track. Built on `PanResponder` so the one code path works
41
+ * on iOS, Android and the web.
42
+ *
43
+ * The design only labels the range bounds, but a slider's purpose is to bind a
44
+ * value — so the live value is surfaced through the bubble (formatted via
45
+ * `formatOptions` / `formatValue`) and reported through `onChange` /
46
+ * `onChangeEnd`. Supports controlled and uncontrolled usage, web keyboard
47
+ * control, and an imperative {@link SliderHandle} `ref` for on-demand reads.
48
+ */
49
+ const Slider = /*#__PURE__*/forwardRef(function Slider({
50
+ value: controlledValue,
51
+ defaultValue,
52
+ onChange,
53
+ onChangeEnd,
54
+ minValue = 0,
55
+ maxValue = 100,
56
+ step = 1,
57
+ isDisabled = false,
58
+ formatOptions,
59
+ locale,
60
+ formatValue,
61
+ renderTooltip,
62
+ alwaysShowTooltip = true,
63
+ showLabels = true,
64
+ minLabel,
65
+ maxLabel,
66
+ width = '100%',
67
+ modes = EMPTY_MODES,
68
+ style,
69
+ accessibilityLabel
70
+ }, ref) {
71
+ const tokens = useMemo(() => resolveTokens(modes), [modes]);
72
+ const isControlled = controlledValue !== undefined;
73
+ const [internalValue, setInternalValue] = useState(() => clamp(defaultValue ?? minValue, minValue, maxValue));
74
+ const rawValue = isControlled ? controlledValue : internalValue;
75
+ const currentValue = clamp(rawValue, minValue, maxValue);
76
+ const [trackWidth, setTrackWidth] = useState(0);
77
+ const [tooltipWidth, setTooltipWidth] = useState(0);
78
+ const [isDragging, setIsDragging] = useState(false);
79
+ const [isHovered, setIsHovered] = useState(false);
80
+ const quantize = useCallback(v => {
81
+ if (!step || step <= 0) return clamp(v, minValue, maxValue);
82
+ const stepped = Math.round((v - minValue) / step) * step + minValue;
83
+ // Round away float dust introduced by the division (e.g. 0.30000000000004).
84
+ const decimals = (String(step).split('.')[1] || '').length;
85
+ const fixed = decimals > 0 ? Number(stepped.toFixed(decimals)) : stepped;
86
+ return clamp(fixed, minValue, maxValue);
87
+ }, [minValue, maxValue, step]);
88
+
89
+ // Keep the latest interactive config in a ref so the PanResponder (created
90
+ // once) always reads fresh values without being recreated mid-gesture.
91
+ const interaction = useRef({
92
+ trackWidth,
93
+ minValue,
94
+ maxValue,
95
+ isDisabled,
96
+ isControlled,
97
+ quantize,
98
+ onChange,
99
+ onChangeEnd
100
+ });
101
+ interaction.current = {
102
+ trackWidth,
103
+ minValue,
104
+ maxValue,
105
+ isDisabled,
106
+ isControlled,
107
+ quantize,
108
+ onChange,
109
+ onChangeEnd
110
+ };
111
+ const lastValueRef = useRef(currentValue);
112
+ lastValueRef.current = currentValue;
113
+ const commit = useCallback((next, isFinal) => {
114
+ const {
115
+ isControlled: ctrl,
116
+ onChange: change,
117
+ onChangeEnd: changeEnd
118
+ } = interaction.current;
119
+ const changed = next !== lastValueRef.current;
120
+ lastValueRef.current = next;
121
+ if (changed) {
122
+ if (!ctrl) setInternalValue(next);
123
+ change?.(next);
124
+ }
125
+ if (isFinal) changeEnd?.(next);
126
+ }, []);
127
+ const valueFromX = useCallback(x => {
128
+ const {
129
+ trackWidth: w,
130
+ minValue: min,
131
+ maxValue: max,
132
+ quantize: q
133
+ } = interaction.current;
134
+ if (w <= 0) return lastValueRef.current;
135
+ const ratio = clamp(x / w, 0, 1);
136
+ return q(min + ratio * (max - min));
137
+ }, []);
138
+ const panResponder = useRef(PanResponder.create({
139
+ onStartShouldSetPanResponder: () => !interaction.current.isDisabled,
140
+ onMoveShouldSetPanResponder: () => !interaction.current.isDisabled,
141
+ onPanResponderGrant: e => {
142
+ if (interaction.current.isDisabled) return;
143
+ setIsDragging(true);
144
+ commit(valueFromX(e.nativeEvent.locationX), false);
145
+ },
146
+ onPanResponderMove: e => {
147
+ if (interaction.current.isDisabled) return;
148
+ commit(valueFromX(e.nativeEvent.locationX), false);
149
+ },
150
+ onPanResponderRelease: e => {
151
+ setIsDragging(false);
152
+ if (interaction.current.isDisabled) return;
153
+ commit(valueFromX(e.nativeEvent.locationX), true);
154
+ },
155
+ onPanResponderTerminate: () => {
156
+ setIsDragging(false);
157
+ if (interaction.current.isDisabled) return;
158
+ commit(lastValueRef.current, true);
159
+ }
160
+ })).current;
161
+
162
+ // Keyboard support (web). Arrow/Page keys nudge, Home/End jump to bounds.
163
+ const handleKeyDown = useCallback(e => {
164
+ if (isDisabled) return;
165
+ const big = Math.max(step, (maxValue - minValue) / 10);
166
+ let next = null;
167
+ switch (e.key) {
168
+ case 'ArrowRight':
169
+ case 'ArrowUp':
170
+ next = quantize(currentValue + step);
171
+ break;
172
+ case 'ArrowLeft':
173
+ case 'ArrowDown':
174
+ next = quantize(currentValue - step);
175
+ break;
176
+ case 'PageUp':
177
+ next = quantize(currentValue + big);
178
+ break;
179
+ case 'PageDown':
180
+ next = quantize(currentValue - big);
181
+ break;
182
+ case 'Home':
183
+ next = minValue;
184
+ break;
185
+ case 'End':
186
+ next = maxValue;
187
+ break;
188
+ default:
189
+ return;
190
+ }
191
+ e.preventDefault?.();
192
+ commit(next, true);
193
+ }, [isDisabled, step, minValue, maxValue, currentValue, quantize, commit]);
194
+ const handleAccessibilityAction = useCallback(event => {
195
+ if (isDisabled) return;
196
+ const name = event.nativeEvent.actionName;
197
+ if (name === 'increment') commit(quantize(currentValue + step), true);else if (name === 'decrement') commit(quantize(currentValue - step), true);
198
+ }, [isDisabled, step, currentValue, quantize, commit]);
199
+ useImperativeHandle(ref, () => ({
200
+ getValue: () => lastValueRef.current,
201
+ setValue: v => commit(quantize(v), true),
202
+ increment: by => commit(quantize(lastValueRef.current + (by ?? step)), true),
203
+ decrement: by => commit(quantize(lastValueRef.current - (by ?? step)), true)
204
+ }), [quantize, commit, step]);
205
+ const defaultFormat = useCallback(v => {
206
+ try {
207
+ return new Intl.NumberFormat(locale, formatOptions).format(v);
208
+ } catch {
209
+ return String(v);
210
+ }
211
+ }, [locale, formatOptions]);
212
+ const format = formatValue ?? defaultFormat;
213
+ const ratio = maxValue > minValue ? (currentValue - minValue) / (maxValue - minValue) : 0;
214
+ const percent = clamp(ratio, 0, 1) * 100;
215
+ const handleSize = tokens.handleSize;
216
+ // The bubble floats (absolutely positioned, no reserved layout space). When
217
+ // `alwaysShowTooltip` is false it only appears while interacting: dragging on
218
+ // any platform, or hovering on web. Lifting the finger clears `isDragging`,
219
+ // so the bubble disappears immediately on touch release.
220
+ const showTooltip = alwaysShowTooltip || isDragging || isHovered;
221
+ const onTrackLayout = useCallback(e => {
222
+ setTrackWidth(e.nativeEvent.layout.width);
223
+ }, []);
224
+ const onTooltipLayout = useCallback(e => {
225
+ setTooltipWidth(e.nativeEvent.layout.width);
226
+ }, []);
227
+ const valueText = format(currentValue);
228
+ const tooltipNode = showTooltip ? /*#__PURE__*/_jsxs(View, {
229
+ onLayout: onTooltipLayout,
230
+ style: [styles.tooltip, {
231
+ backgroundColor: tokens.tooltipBackground,
232
+ paddingHorizontal: tokens.tooltipPaddingH,
233
+ paddingVertical: tokens.tooltipPaddingV,
234
+ borderRadius: tokens.tooltipRadius,
235
+ maxWidth: tokens.tooltipMaxWidth,
236
+ bottom: handleSize + tokens.tooltipWrapGap,
237
+ // Pinned to the value point on the full-width track area (not
238
+ // the tiny handle-width thumb). An absolute child with only
239
+ // `left` set is clamped by Yoga to `parentWidth - left`, so
240
+ // parenting it to the 20px thumb collapsed the text to ~0px
241
+ // and rendered an empty bubble on device. Centre via translate.
242
+ left: `${percent}%`,
243
+ transform: [{
244
+ translateX: -tooltipWidth / 2
245
+ }]
246
+ }],
247
+ pointerEvents: "none",
248
+ children: [renderTooltip ? renderTooltip(currentValue) : /*#__PURE__*/_jsx(Text, {
249
+ style: tokens.tooltipLabel,
250
+ numberOfLines: 1,
251
+ children: valueText
252
+ }), /*#__PURE__*/_jsx(View, {
253
+ style: [styles.tip, {
254
+ bottom: -tokens.tipHeight
255
+ }],
256
+ pointerEvents: "none",
257
+ children: /*#__PURE__*/_jsx(Svg, {
258
+ width: tokens.tipWidth,
259
+ height: tokens.tipHeight,
260
+ viewBox: `0 0 ${tokens.tipWidth} ${tokens.tipHeight}`,
261
+ children: /*#__PURE__*/_jsx(Path, {
262
+ d: `M0 0 L${tokens.tipWidth / 2} ${tokens.tipHeight} L${tokens.tipWidth} 0 Z`,
263
+ fill: tokens.tooltipBackground
264
+ })
265
+ })
266
+ })]
267
+ }) : null;
268
+ return /*#__PURE__*/_jsxs(View, {
269
+ style: [{
270
+ width,
271
+ gap: tokens.gap,
272
+ paddingTop: tokens.paddingTop,
273
+ paddingBottom: tokens.paddingBottom,
274
+ paddingLeft: tokens.paddingLeft,
275
+ paddingRight: tokens.paddingRight,
276
+ opacity: isDisabled ? 0.5 : 1
277
+ }, style],
278
+ accessibilityLabel: accessibilityLabel,
279
+ children: [/*#__PURE__*/_jsxs(View, {
280
+ ...panResponder.panHandlers,
281
+ onLayout: onTrackLayout,
282
+ style: [styles.trackArea, {
283
+ height: handleSize
284
+ }],
285
+ accessible: true,
286
+ accessibilityRole: "adjustable",
287
+ accessibilityLabel: accessibilityLabel,
288
+ accessibilityValue: {
289
+ min: minValue,
290
+ max: maxValue,
291
+ now: currentValue,
292
+ text: valueText
293
+ },
294
+ accessibilityState: {
295
+ disabled: isDisabled
296
+ },
297
+ accessibilityActions: [{
298
+ name: 'increment'
299
+ }, {
300
+ name: 'decrement'
301
+ }],
302
+ onAccessibilityAction: handleAccessibilityAction,
303
+ focusable: !isDisabled,
304
+ ...(Platform.OS === 'web' ? {
305
+ onKeyDown: handleKeyDown,
306
+ tabIndex: isDisabled ? -1 : 0,
307
+ onMouseEnter: () => !isDisabled && setIsHovered(true),
308
+ onMouseLeave: () => setIsHovered(false)
309
+ } : {}),
310
+ children: [/*#__PURE__*/_jsx(View, {
311
+ style: {
312
+ height: tokens.trackHeight,
313
+ borderRadius: tokens.trackRadius,
314
+ backgroundColor: tokens.trackBackground
315
+ }
316
+ }), /*#__PURE__*/_jsx(View, {
317
+ pointerEvents: "none",
318
+ style: {
319
+ position: 'absolute',
320
+ left: 0,
321
+ top: (handleSize - tokens.indicatorHeight) / 2,
322
+ height: tokens.indicatorHeight,
323
+ width: `${percent}%`,
324
+ borderRadius: tokens.indicatorRadius,
325
+ backgroundColor: tokens.indicatorBackground
326
+ }
327
+ }), /*#__PURE__*/_jsx(View, {
328
+ pointerEvents: "none",
329
+ style: {
330
+ position: 'absolute',
331
+ top: 0,
332
+ left: `${percent}%`,
333
+ width: handleSize,
334
+ height: handleSize,
335
+ transform: [{
336
+ translateX: -handleSize / 2
337
+ }]
338
+ },
339
+ children: /*#__PURE__*/_jsx(View, {
340
+ style: {
341
+ width: handleSize,
342
+ height: handleSize,
343
+ borderRadius: tokens.handleRadius,
344
+ backgroundColor: tokens.handleBackground
345
+ }
346
+ })
347
+ }), tooltipNode]
348
+ }), showLabels ? /*#__PURE__*/_jsxs(View, {
349
+ style: styles.labelWrap,
350
+ children: [/*#__PURE__*/_jsx(Text, {
351
+ style: tokens.label,
352
+ children: minLabel ?? format(minValue)
353
+ }), /*#__PURE__*/_jsx(Text, {
354
+ style: tokens.label,
355
+ children: maxLabel ?? format(maxValue)
356
+ })]
357
+ }) : null]
358
+ });
359
+ });
360
+ function resolveTokens(modes) {
361
+ // NOTE: token names are passed as string literals DIRECTLY to
362
+ // getVariableByName so the `extract-component-tokens` script can statically
363
+ // collect them for the generated docs. Do not refactor into a helper that
364
+ // receives the name as a variable.
365
+ const gap = asNum(getVariableByName('slider/gap', modes), 16);
366
+ const paddingTop = asNum(getVariableByName('slider/padding/top', modes), 8);
367
+ const paddingBottom = asNum(getVariableByName('slider/padding/bottom', modes), 0);
368
+ const paddingLeft = asNum(getVariableByName('slider/padding/left', modes), 0);
369
+ const paddingRight = asNum(getVariableByName('slider/padding/right', modes), 0);
370
+ const trackHeight = asNum(getVariableByName('slider/track/height', modes), 4);
371
+ const trackRadius = asNum(getVariableByName('slider/track/radius', modes), 999);
372
+ // The unfilled track uses a lighter emphasis in the design; consumer modes
373
+ // still win over the baked-in `Emphasis: Low`.
374
+ const trackBackground = asStr(getVariableByName('slider/track/background', {
375
+ ...TRACK_EMPHASIS,
376
+ ...modes
377
+ }), '#fde8c9');
378
+ const indicatorHeight = asNum(getVariableByName('slider/indicator/height', modes), 4);
379
+ const indicatorRadius = asNum(getVariableByName('slider/indicator/radius', modes), 999);
380
+ const indicatorBackground = asStr(getVariableByName('slider/indicator/background', modes), '#f7ab21');
381
+ const handleSize = asNum(getVariableByName('slider/handle/size', modes), 20);
382
+ const handleRadius = asNum(getVariableByName('slider/handle/radius', modes), 999999);
383
+ const handleBackground = asStr(getVariableByName('slider/handle/background', modes), '#f7ab21');
384
+ const tooltipWrapGap = asNum(getVariableByName('slider/tooltipWrap/gap', modes), 12);
385
+ const tooltipBackground = asStr(getVariableByName('tooltip/background', modes), '#0f0d0a');
386
+ const tooltipPaddingH = asNum(getVariableByName('tooltip/padding/horizontal', modes), 12);
387
+ const tooltipPaddingV = asNum(getVariableByName('tooltip/padding/vertical', modes), 8);
388
+ const tooltipRadius = asNum(getVariableByName('radius', modes), 8);
389
+ const tooltipMaxWidth = asNum(getVariableByName('maxWidth', modes), 280);
390
+ const tipWidth = asNum(getVariableByName('tooltip/tipItem/width', modes), 16);
391
+ const tipHeight = asNum(getVariableByName('tooltip/tipItem/height', modes), 8);
392
+ const tooltipLabelColor = asStr(getVariableByName('tooltip/label/color', modes), '#ffffff');
393
+ const tooltipLabelSize = asNum(getVariableByName('tooltip/label/fontSize', modes), 14);
394
+ const tooltipLabelFamily = asStr(getVariableByName('tooltip/lable/fontFamily', modes), 'JioType Var');
395
+ const tooltipLabelLineHeight = asNum(getVariableByName('tooltip/label/lineHeight', modes), 18);
396
+ const tooltipLabelWeight = asStr(getVariableByName('tooltip/label/fontWeight', modes), '500');
397
+ const labelColor = asStr(getVariableByName('text/foreground', modes), '#000000');
398
+ const labelSize = asNum(getVariableByName('text/fontSize', modes), 14);
399
+ const labelFamily = asStr(getVariableByName('text/fontFamily', modes), 'JioType Var');
400
+ const labelLineHeight = asNum(getVariableByName('text/lineHeight', modes), 20);
401
+ const labelWeight = asStr(getVariableByName('text/fontWeight', modes), '500');
402
+ const labelLetterSpacing = asNum(getVariableByName('text/letterSpacing', modes), -0.5);
403
+ return {
404
+ gap,
405
+ paddingTop,
406
+ paddingBottom,
407
+ paddingLeft,
408
+ paddingRight,
409
+ trackHeight,
410
+ trackRadius,
411
+ trackBackground,
412
+ indicatorHeight,
413
+ indicatorRadius,
414
+ indicatorBackground,
415
+ handleSize,
416
+ handleRadius,
417
+ handleBackground,
418
+ tooltipWrapGap,
419
+ tooltipBackground,
420
+ tooltipPaddingH,
421
+ tooltipPaddingV,
422
+ tooltipRadius,
423
+ tooltipMaxWidth,
424
+ tooltipLineHeight: tooltipLabelLineHeight,
425
+ tipWidth,
426
+ tipHeight,
427
+ tooltipLabel: {
428
+ color: tooltipLabelColor,
429
+ fontSize: tooltipLabelSize,
430
+ fontFamily: tooltipLabelFamily,
431
+ lineHeight: tooltipLabelLineHeight,
432
+ fontWeight: tooltipLabelWeight,
433
+ includeFontPadding: false
434
+ },
435
+ label: {
436
+ color: labelColor,
437
+ fontSize: labelSize,
438
+ fontFamily: labelFamily,
439
+ lineHeight: labelLineHeight,
440
+ fontWeight: labelWeight,
441
+ letterSpacing: labelLetterSpacing,
442
+ includeFontPadding: false
443
+ }
444
+ };
445
+ }
446
+ const styles = {
447
+ trackArea: {
448
+ width: '100%',
449
+ justifyContent: 'center',
450
+ position: 'relative'
451
+ },
452
+ labelWrap: {
453
+ flexDirection: 'row',
454
+ justifyContent: 'space-between',
455
+ alignItems: 'flex-start',
456
+ width: '100%'
457
+ },
458
+ tooltip: {
459
+ position: 'absolute',
460
+ alignItems: 'center',
461
+ justifyContent: 'center'
462
+ },
463
+ tip: {
464
+ position: 'absolute',
465
+ alignSelf: 'center'
466
+ }
467
+ };
468
+ export default Slider;
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
 
3
- import React, { useRef, useState } from 'react';
3
+ import React, { forwardRef, useRef, useState } from 'react';
4
4
  import { Platform, Pressable, View, TextInput as RNTextInput } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import Icon from '../../icons/Icon';
7
- import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
7
+ import { EMPTY_MODES, cloneChildrenWithModes, mergeRefs } from '../../utils/react-utils';
8
8
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
9
9
  /**
10
10
  * TextInput component that mirrors the Figma "textInput" component.
@@ -95,7 +95,7 @@ function makePlaceholderColor(color, opacity = 0.5) {
95
95
  // Fallback: return original color
96
96
  return color;
97
97
  }
98
- function TextInput({
98
+ const TextInputBase = /*#__PURE__*/forwardRef(function TextInput({
99
99
  placeholder = '',
100
100
  value = '',
101
101
  onChangeText,
@@ -110,7 +110,7 @@ function TextInput({
110
110
  accessibilityLabel,
111
111
  accessibilityHint,
112
112
  ...rest
113
- }) {
113
+ }, ref) {
114
114
  // Track focus state to hide placeholder when focused
115
115
  const [isFocused, setIsFocused] = useState(false);
116
116
  const [isHovered, setIsHovered] = useState(false);
@@ -225,7 +225,7 @@ function TextInput({
225
225
  importantForAccessibility: "no",
226
226
  children: processedLeading
227
227
  }), /*#__PURE__*/_jsx(RNTextInput, {
228
- ref: inputRef,
228
+ ref: mergeRefs(inputRef, ref),
229
229
  accessibilityLabel: undefined,
230
230
  accessibilityHint: accessibilityHint,
231
231
  placeholder: displayPlaceholder,
@@ -258,7 +258,7 @@ function TextInput({
258
258
  style: containerStyleArray,
259
259
  children: inner
260
260
  });
261
- }
261
+ });
262
262
 
263
263
  /**
264
264
  * TextInput.Search component that mirrors the Figma "textInput.search" component.
@@ -280,7 +280,7 @@ function TextInput({
280
280
  * @param {Object} [props.rest] - Additional props passed to the underlying TextInput
281
281
  */
282
282
 
283
- function TextInputSearch({
283
+ const TextInputSearch = /*#__PURE__*/forwardRef(function TextInputSearch({
284
284
  placeholder = 'Search',
285
285
  value = '',
286
286
  onChangeText,
@@ -293,7 +293,7 @@ function TextInputSearch({
293
293
  onFocus,
294
294
  onBlur,
295
295
  ...rest
296
- }) {
296
+ }, ref) {
297
297
  // Resolve icon tokens for default search icon
298
298
  const iconColor = getVariableByName('textInput/icon/color', modes) || '#24262b';
299
299
  const iconSize = getVariableByName('textInput/icon/size', modes) || 18;
@@ -330,10 +330,15 @@ function TextInputSearch({
330
330
  textInputProps.accessibilityHint = accessibilityHint;
331
331
  }
332
332
  return /*#__PURE__*/_jsx(TextInput, {
333
+ ref: ref,
333
334
  ...textInputProps
334
335
  });
335
- }
336
+ });
337
+
338
+ // Attach Search as a property of TextInput using namespace pattern. The base
339
+ // component is created via forwardRef, so we compose the static onto a typed
340
+ // alias to keep `TextInput.Search` strongly typed.
336
341
 
337
- // Attach Search as a property of TextInput using namespace pattern
342
+ const TextInput = TextInputBase;
338
343
  TextInput.Search = TextInputSearch;
339
344
  export default TextInput;
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+
3
+ import React, { useMemo } from 'react';
4
+ import { Text as RNText } from 'react-native';
5
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
7
+
8
+ /**
9
+ * A single coloured run inside a {@link TextSegment}. Each run can carry its own
10
+ * `modes`, so individual fragments of the same paragraph can resolve different
11
+ * design tokens (most commonly a different `text/foreground` colour) while still
12
+ * flowing — and wrapping — inline as one continuous line of text.
13
+ */
14
+ import { jsx as _jsx } from "react/jsx-runtime";
15
+ const TEXT_ALIGN_MAP = {
16
+ Left: 'left',
17
+ Center: 'center'
18
+ };
19
+
20
+ /**
21
+ * Resolves the `text/*` design tokens for a given modes object into an RN
22
+ * `TextStyle`. Shared by the outer paragraph and every inline run so a run that
23
+ * only overrides colour still inherits the family/size/weight/spacing.
24
+ */
25
+ function resolveTextStyle(modes) {
26
+ return {
27
+ color: getVariableByName('text/foreground', modes) ?? '#000000',
28
+ fontFamily: getVariableByName('text/fontFamily', modes) ?? 'JioType',
29
+ fontSize: getVariableByName('text/fontSize', modes) ?? 14,
30
+ fontWeight: String(getVariableByName('text/fontWeight', modes) ?? '500'),
31
+ lineHeight: getVariableByName('text/lineHeight', modes) ?? 20,
32
+ letterSpacing: getVariableByName('text/letterSpacing', modes) ?? -0.5
33
+ };
34
+ }
35
+
36
+ /**
37
+ * TextSegment — composes several differently-styled text runs into a single
38
+ * paragraph that wraps as one continuous line.
39
+ *
40
+ * On the web you would reach for `<span>`s inside a `<p>`; the React Native
41
+ * equivalent is nesting `<Text>` inside a parent `<Text>`. That nesting is the
42
+ * whole trick: sibling `<View>`s would lay out as flex blocks and break onto
43
+ * rigid rows, but nested `<Text>` nodes flow inline and wrap naturally at word
44
+ * boundaries — exactly like a real paragraph — while each nested run keeps its
45
+ * own colour/weight resolved from its own `modes`.
46
+ *
47
+ * Two equivalent ways to author content:
48
+ * - **`segments` prop** — declarative array of `{ text, modes }` runs.
49
+ * - **`children`** — pass library `Text` elements (or strings); the parent
50
+ * `modes` cascade down and each child may override its own.
51
+ *
52
+ * @example Data-driven
53
+ * ```tsx
54
+ * <TextSegment
55
+ * segments={[
56
+ * { text: 'Upsell message ' },
57
+ * { text: 'JioFinance+', modes: { 'Text Appearance': 'Primary' } },
58
+ * ]}
59
+ * />
60
+ * ```
61
+ *
62
+ * @example Compositional
63
+ * ```tsx
64
+ * <TextSegment numberOfLines={2}>
65
+ * <Text>Pay with </Text>
66
+ * <Text modes={{ 'Text Appearance': 'Primary' }}>JioFinance+</Text>
67
+ * <Text> and earn rewards on every transaction.</Text>
68
+ * </TextSegment>
69
+ * ```
70
+ */
71
+ function TextSegment({
72
+ segments,
73
+ children,
74
+ textAlign = 'Left',
75
+ numberOfLines,
76
+ modes = EMPTY_MODES,
77
+ style
78
+ }) {
79
+ const baseStyle = useMemo(() => ({
80
+ ...resolveTextStyle(modes),
81
+ textAlign: TEXT_ALIGN_MAP[textAlign]
82
+ }), [modes, textAlign]);
83
+
84
+ // children win over `segments` so the compositional API can always override.
85
+ const hasChildren = children !== undefined && children !== null && children !== false;
86
+ const renderedSegments = useMemo(() => {
87
+ if (hasChildren || !segments?.length) return null;
88
+ return segments.map((run, index) => {
89
+ // Parent modes first, then the run's own modes override them — mirrors the
90
+ // merge order used by `cloneChildrenWithModes` for the children API.
91
+ const runModes = run.modes ? {
92
+ ...modes,
93
+ ...run.modes
94
+ } : modes;
95
+ const runStyle = run.modes ? resolveTextStyle(runModes) : undefined;
96
+ return /*#__PURE__*/_jsx(RNText, {
97
+ style: [runStyle, run.style],
98
+ children: run.text
99
+ }, index);
100
+ });
101
+ }, [segments, hasChildren, modes]);
102
+
103
+ // The cascade: every child (and its descendants) receives the parent modes so
104
+ // nested library `Text` runs resolve their tokens against the same baseline,
105
+ // while still honouring any per-child `modes` override.
106
+ const content = hasChildren ? cloneChildrenWithModes(children, modes) : renderedSegments;
107
+ return /*#__PURE__*/_jsx(RNText, {
108
+ style: [baseStyle, style],
109
+ numberOfLines: numberOfLines,
110
+ children: content
111
+ });
112
+ }
113
+ export default /*#__PURE__*/React.memo(TextSegment);