jfs-components 0.1.25 → 0.1.30

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 (97) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/lib/commonjs/components/AutoplayControl/AutoplayControl.js +66 -0
  3. package/lib/commonjs/components/Carousel/Carousel.js +112 -19
  4. package/lib/commonjs/components/CompareTable/CompareTable.js +191 -25
  5. package/lib/commonjs/components/ContentSheet/ContentSheet.js +104 -10
  6. package/lib/commonjs/components/FormUpload/Additem.js +168 -0
  7. package/lib/commonjs/components/FormUpload/FormUpload.js +198 -0
  8. package/lib/commonjs/components/Grid/Grid.js +206 -0
  9. package/lib/commonjs/components/MoneyValue/MoneyValue.js +179 -54
  10. package/lib/commonjs/components/NumberPagination/NumberPagination.js +144 -0
  11. package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +7 -1
  12. package/lib/commonjs/components/ProfileCard/ProfileCard.js +208 -0
  13. package/lib/commonjs/components/Table/Table.js +27 -7
  14. package/lib/commonjs/components/UpiHandle/UpiHandle.js +23 -11
  15. package/lib/commonjs/components/index.js +42 -0
  16. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  17. package/lib/commonjs/design-tokens/figma-modes.generated.js +21 -10
  18. package/lib/commonjs/icons/components/IconBrokenimage.js +19 -0
  19. package/lib/commonjs/icons/components/IconImage.js +19 -0
  20. package/lib/commonjs/icons/components/IconMic.js +19 -0
  21. package/lib/commonjs/icons/components/IconMicoff.js +19 -0
  22. package/lib/commonjs/icons/components/IconPause.js +19 -0
  23. package/lib/commonjs/icons/components/IconPlay.js +19 -0
  24. package/lib/commonjs/icons/components/index.js +66 -0
  25. package/lib/commonjs/icons/registry.js +2 -2
  26. package/lib/module/components/AutoplayControl/AutoplayControl.js +60 -0
  27. package/lib/module/components/Carousel/Carousel.js +111 -19
  28. package/lib/module/components/CompareTable/CompareTable.js +191 -26
  29. package/lib/module/components/ContentSheet/ContentSheet.js +108 -14
  30. package/lib/module/components/FormUpload/Additem.js +162 -0
  31. package/lib/module/components/FormUpload/FormUpload.js +192 -0
  32. package/lib/module/components/Grid/Grid.js +201 -0
  33. package/lib/module/components/MoneyValue/MoneyValue.js +182 -57
  34. package/lib/module/components/NumberPagination/NumberPagination.js +138 -0
  35. package/lib/module/components/PdpCcCard/PdpCcCard.js +7 -1
  36. package/lib/module/components/ProfileCard/ProfileCard.js +202 -0
  37. package/lib/module/components/Table/Table.js +27 -7
  38. package/lib/module/components/UpiHandle/UpiHandle.js +23 -11
  39. package/lib/module/components/index.js +7 -1
  40. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  41. package/lib/module/design-tokens/figma-modes.generated.js +21 -10
  42. package/lib/module/icons/components/IconBrokenimage.js +12 -0
  43. package/lib/module/icons/components/IconImage.js +12 -0
  44. package/lib/module/icons/components/IconMic.js +12 -0
  45. package/lib/module/icons/components/IconMicoff.js +12 -0
  46. package/lib/module/icons/components/IconPause.js +12 -0
  47. package/lib/module/icons/components/IconPlay.js +12 -0
  48. package/lib/module/icons/components/index.js +6 -0
  49. package/lib/module/icons/registry.js +2 -2
  50. package/lib/typescript/src/components/AutoplayControl/AutoplayControl.d.ts +15 -0
  51. package/lib/typescript/src/components/Carousel/Carousel.d.ts +8 -1
  52. package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +21 -1
  53. package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +20 -1
  54. package/lib/typescript/src/components/FormUpload/Additem.d.ts +96 -0
  55. package/lib/typescript/src/components/FormUpload/FormUpload.d.ts +85 -0
  56. package/lib/typescript/src/components/Grid/Grid.d.ts +87 -0
  57. package/lib/typescript/src/components/MoneyValue/MoneyValue.d.ts +10 -1
  58. package/lib/typescript/src/components/NumberPagination/NumberPagination.d.ts +21 -0
  59. package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +25 -1
  60. package/lib/typescript/src/components/ProfileCard/ProfileCard.d.ts +89 -0
  61. package/lib/typescript/src/components/Table/Table.d.ts +9 -1
  62. package/lib/typescript/src/components/UpiHandle/UpiHandle.d.ts +7 -1
  63. package/lib/typescript/src/components/index.d.ts +34 -28
  64. package/lib/typescript/src/design-tokens/figma-modes.generated.d.ts +10 -4
  65. package/lib/typescript/src/icons/components/IconBrokenimage.d.ts +3 -0
  66. package/lib/typescript/src/icons/components/IconImage.d.ts +3 -0
  67. package/lib/typescript/src/icons/components/IconMic.d.ts +3 -0
  68. package/lib/typescript/src/icons/components/IconMicoff.d.ts +3 -0
  69. package/lib/typescript/src/icons/components/IconPause.d.ts +3 -0
  70. package/lib/typescript/src/icons/components/IconPlay.d.ts +3 -0
  71. package/lib/typescript/src/icons/components/index.d.ts +6 -0
  72. package/lib/typescript/src/icons/registry.d.ts +1 -1
  73. package/package.json +1 -1
  74. package/src/components/AutoplayControl/AutoplayControl.tsx +77 -0
  75. package/src/components/Carousel/Carousel.tsx +133 -23
  76. package/src/components/CompareTable/CompareTable.tsx +224 -30
  77. package/src/components/ContentSheet/ContentSheet.tsx +138 -8
  78. package/src/components/FormUpload/Additem.tsx +262 -0
  79. package/src/components/FormUpload/FormUpload.tsx +313 -0
  80. package/src/components/Grid/Grid.tsx +368 -0
  81. package/src/components/MoneyValue/MoneyValue.tsx +216 -65
  82. package/src/components/NumberPagination/NumberPagination.tsx +170 -0
  83. package/src/components/PdpCcCard/PdpCcCard.tsx +36 -1
  84. package/src/components/ProfileCard/ProfileCard.tsx +268 -0
  85. package/src/components/Table/Table.tsx +29 -4
  86. package/src/components/UpiHandle/UpiHandle.tsx +35 -24
  87. package/src/components/index.ts +329 -182
  88. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  89. package/src/design-tokens/figma-modes.generated.ts +21 -10
  90. package/src/icons/components/IconBrokenimage.tsx +11 -0
  91. package/src/icons/components/IconImage.tsx +11 -0
  92. package/src/icons/components/IconMic.tsx +11 -0
  93. package/src/icons/components/IconMicoff.tsx +11 -0
  94. package/src/icons/components/IconPause.tsx +11 -0
  95. package/src/icons/components/IconPlay.tsx +11 -0
  96. package/src/icons/components/index.ts +6 -0
  97. package/src/icons/registry.ts +25 -1
@@ -1,9 +1,14 @@
1
- import React, { useMemo, useEffect, useRef } from 'react'
1
+ import React, { useMemo, useEffect, useRef, useState, useCallback } from 'react'
2
2
  import {
3
3
  View,
4
4
  Text,
5
+ TextInput,
5
6
  Pressable,
6
7
  Animated,
8
+ Platform,
9
+ Keyboard,
10
+ type GestureResponderEvent,
11
+ type LayoutChangeEvent,
7
12
  type StyleProp,
8
13
  type ViewStyle,
9
14
  type TextStyle,
@@ -58,6 +63,15 @@ export type MoneyValueProps = {
58
63
  hidden?: boolean
59
64
  /** When true, a blinking vertical cursor is shown at the end of the value text. */
60
65
  focused?: boolean
66
+ /**
67
+ * When true, tapping the component enters an inline edit mode where the value
68
+ * becomes a numeric TextInput. The user can type digits or delete the existing
69
+ * amount. Only numeric characters (including one decimal point) are accepted.
70
+ * On blur or submit the edit is committed and the display restores.
71
+ */
72
+ editable?: boolean
73
+ /** Called when the value changes during editing (after commit on blur). */
74
+ onValueChange?: (newValue: string | number) => void
61
75
  /** Modes configuration mapped to Figma tokens. */
62
76
  modes?: Modes
63
77
  style?: StyleProp<ViewStyle>
@@ -85,6 +99,8 @@ function MoneyValue({
85
99
  negative,
86
100
  focused = false,
87
101
  hidden = false,
102
+ editable = false,
103
+ onValueChange,
88
104
  modes = EMPTY_MODES,
89
105
  style,
90
106
  valueStyle,
@@ -96,23 +112,60 @@ function MoneyValue({
96
112
  ...rest
97
113
  }: MoneyValueProps) {
98
114
  // Auto-detect negative from value and compute display value
99
- const { isNegative, displayValue } = useMemo(() => {
100
- const stringValue = String(value)
101
- const trimmed = stringValue.trim()
102
- const valueIsNegative =
103
- trimmed.startsWith('-') || (typeof value === 'number' && value < 0)
115
+ const stringValue = String(value)
116
+ const trimmed = stringValue.trim()
117
+ const valueIsNegative =
118
+ trimmed.startsWith('-') || (typeof value === 'number' && value < 0)
104
119
 
105
- // Strip leading minus sign for display (we show it separately)
106
- const absoluteValue = trimmed.startsWith('-') ? trimmed.slice(1) : trimmed
120
+ // Strip leading minus sign for display (we show it separately)
121
+ const absoluteValue = trimmed.startsWith('-') ? trimmed.slice(1) : trimmed
107
122
 
108
- // Use explicit negative prop if provided, otherwise use auto-detected
109
- const showNegative = negative !== undefined ? negative : valueIsNegative
123
+ // Use explicit negative prop if provided, otherwise use auto-detected
124
+ const showNegative = negative !== undefined ? negative : valueIsNegative
110
125
 
111
- return {
112
- isNegative: hidden ? false : showNegative,
113
- displayValue: hidden ? '•••' : absoluteValue,
126
+ const isNegative = hidden ? false : showNegative
127
+ const displayValue = hidden ? '•••' : absoluteValue
128
+
129
+ // Inline editing state
130
+ const [isEditing, setIsEditing] = useState(false)
131
+ const [editBuffer, setEditBuffer] = useState('')
132
+ const inputRef = useRef<TextInput>(null)
133
+ // Always-current snapshot of the display value for callbacks to reference.
134
+ const displayValueRef = useRef(displayValue)
135
+ displayValueRef.current = displayValue
136
+
137
+ const enterEditMode = useCallback(() => {
138
+ setIsEditing(true)
139
+ setEditBuffer(displayValueRef.current)
140
+ // Focus the input on next frame so the keyboard opens
141
+ setTimeout(() => inputRef.current?.focus(), 0)
142
+ }, [])
143
+
144
+ const commitEdit = useCallback(() => {
145
+ setIsEditing(false)
146
+ const resolved = editBuffer || '0'
147
+ if (resolved !== displayValueRef.current) {
148
+ onValueChange?.(resolved)
149
+ }
150
+ }, [editBuffer, onValueChange])
151
+
152
+ const handleChangeText = useCallback(
153
+ (text: string) => {
154
+ // Allow only digits and at most one decimal point
155
+ const filtered = text.replace(/[^0-9.]/g, '')
156
+ const parts = filtered.split('.')
157
+ const clean = parts.length > 2 ? parts[0] + '.' + parts.slice(1).join('') : filtered
158
+ setEditBuffer(clean)
159
+ },
160
+ [],
161
+ )
162
+
163
+ const handlePress = useCallback((event: GestureResponderEvent) => {
164
+ if (editable) {
165
+ enterEditMode()
114
166
  }
115
- }, [value, negative, hidden])
167
+ onPress?.(event)
168
+ }, [editable, enterEditMode, onPress])
116
169
 
117
170
  // Resolve typography and layout tokens from Figma
118
171
  const textColor = getVariableByName('moneyValue/text/color', modes)
@@ -191,64 +244,162 @@ function MoneyValue({
191
244
  }
192
245
  }, [focused, cursorOpacity])
193
246
 
194
- return (
195
- <Pressable
196
- style={[containerStyle, style]}
197
- accessibilityRole="text"
198
- accessibilityLabel={undefined}
199
- accessibilityHint={accessibilityHint}
200
- onPress={onPress}
201
- disabled={!onPress}
202
- {...rest}
203
- >
204
- {isNegative && (
205
- <Text
206
- style={[currencyTextStyle, negativeSignStyle]}
207
- accessibilityElementsHidden={true}
208
- importantForAccessibility="no"
209
- >
210
- -
211
- </Text>
212
- )}
213
- <Text
214
- style={[currencyTextStyle, currencyStyle]}
215
- accessibilityElementsHidden={true}
216
- importantForAccessibility="no"
217
- >
218
- {resolvedCurrency}
219
- </Text>
220
-
221
- {/* Group value and cursor in their own view to bypass the parent's generic gap constraint */}
222
- <View style={{ flexDirection: 'row', alignItems: 'center' }}>
223
- <Text
224
- style={[valueTextStyle, valueStyle]}
225
- accessibilityElementsHidden={true}
226
- importantForAccessibility="no"
227
- >
228
- {displayValue}
229
- </Text>
230
- {focused && (
231
- <Animated.View
232
- style={{
233
- opacity: cursorOpacity,
234
- width: 2,
235
- height: (valueFontSize as number) * 1.1,
236
- backgroundColor: textColor,
237
- marginLeft: 2,
238
- }}
239
- />
240
- )}
241
- </View>
242
- </Pressable>
247
+ // Keyboard avoidance — lift the component when the keyboard would cover it.
248
+ // Follows the same pattern as ActionFooter (uses Keyboard.addListener).
249
+ // Resets when editing ends or the keyboard hides.
250
+ const rootRef = useRef<View>(null)
251
+ const keyboardLift = useRef(new Animated.Value(0)).current
252
+ const rootWindowYRef = useRef(0)
253
+ const rootMeasuredHeightRef = useRef(0)
254
+
255
+ const measureRoot = useCallback(() => {
256
+ rootRef.current?.measureInWindow((_x, y, _w, h) => {
257
+ rootWindowYRef.current = y
258
+ rootMeasuredHeightRef.current = h
259
+ })
260
+ }, [])
261
+
262
+ const handleRootLayout = useCallback(
263
+ (_e: LayoutChangeEvent) => {
264
+ // measureInWindow gives the component's position relative to the window,
265
+ // which is what we need for keyboard overlap calculation. onLayout fires
266
+ // after the component mounts and whenever its size changes.
267
+ measureRoot()
268
+ },
269
+ [measureRoot],
243
270
  )
244
- }
245
271
 
246
- export default MoneyValue
272
+ useEffect(() => {
273
+ if (!editable) return
274
+
275
+ const showSub = Keyboard.addListener('keyboardDidShow', (e) => {
276
+ measureRoot()
277
+ if (!isEditing) return
247
278
 
279
+ const kbHeight = e.endCoordinates.height
280
+ const { height: screenH } = require('react-native').Dimensions.get('window')
281
+ const componentBottom = rootWindowYRef.current + rootMeasuredHeightRef.current
282
+ const visibleBottom = screenH - kbHeight
283
+ const overlap = componentBottom - visibleBottom + 12
248
284
 
285
+ if (overlap > 0) {
286
+ Animated.timing(keyboardLift, {
287
+ toValue: -overlap,
288
+ duration: e.duration ?? 250,
289
+ useNativeDriver: true,
290
+ }).start()
291
+ }
292
+ })
249
293
 
294
+ const hideSub = Keyboard.addListener('keyboardDidHide', () => {
295
+ Animated.timing(keyboardLift, {
296
+ toValue: 0,
297
+ duration: 150,
298
+ useNativeDriver: true,
299
+ }).start()
300
+ })
250
301
 
302
+ return () => {
303
+ showSub.remove()
304
+ hideSub.remove()
305
+ }
306
+ }, [editable, isEditing, keyboardLift, measureRoot])
307
+
308
+ // Reset lift when editing ends
309
+ useEffect(() => {
310
+ if (!isEditing) {
311
+ keyboardLift.setValue(0)
312
+ }
313
+ }, [isEditing, keyboardLift])
251
314
 
315
+ const handleBlur = useCallback(() => {
316
+ commitEdit()
317
+ }, [commitEdit])
252
318
 
319
+ const handleSubmitEditing = useCallback(() => {
320
+ commitEdit()
321
+ }, [commitEdit])
253
322
 
323
+ return (
324
+ <Animated.View style={{ transform: [{ translateY: keyboardLift }] }}>
325
+ <View ref={rootRef} onLayout={handleRootLayout}>
326
+ <Pressable
327
+ style={[containerStyle, style]}
328
+ accessibilityRole="text"
329
+ accessibilityLabel={undefined}
330
+ accessibilityHint={accessibilityHint}
331
+ onPress={handlePress}
332
+ disabled={!onPress && !editable}
333
+ {...rest}
334
+ >
335
+ {isNegative && (
336
+ <Text
337
+ style={[currencyTextStyle, negativeSignStyle]}
338
+ accessibilityElementsHidden={true}
339
+ importantForAccessibility="no"
340
+ >
341
+ -
342
+ </Text>
343
+ )}
344
+ <Text
345
+ style={[currencyTextStyle, currencyStyle]}
346
+ accessibilityElementsHidden={true}
347
+ importantForAccessibility="no"
348
+ >
349
+ {resolvedCurrency}
350
+ </Text>
351
+
352
+ {/* Group value and cursor in their own view to bypass the parent's generic gap constraint */}
353
+ <View style={{ flexDirection: 'row', alignItems: 'center' }}>
354
+ {isEditing ? (
355
+ <TextInput
356
+ ref={inputRef}
357
+ value={editBuffer}
358
+ onChangeText={handleChangeText}
359
+ onBlur={handleBlur}
360
+ onSubmitEditing={handleSubmitEditing}
361
+ keyboardType="decimal-pad"
362
+ returnKeyType="done"
363
+ selectTextOnFocus
364
+ style={[
365
+ valueTextStyle,
366
+ valueStyle,
367
+ {
368
+ padding: 0,
369
+ margin: 0,
370
+ // On web a thin border helps the cursor appear precisely
371
+ ...(Platform.OS === 'web' ? { outlineStyle: 'none' as any } : {}),
372
+ },
373
+ ]}
374
+ accessibilityLabel={accessibilityLabel || 'Edit value'}
375
+ />
376
+ ) : (
377
+ <>
378
+ <Text
379
+ style={[valueTextStyle, valueStyle]}
380
+ accessibilityElementsHidden={true}
381
+ importantForAccessibility="no"
382
+ >
383
+ {displayValue}
384
+ </Text>
385
+ {focused && (
386
+ <Animated.View
387
+ style={{
388
+ opacity: cursorOpacity,
389
+ width: 2,
390
+ height: (valueFontSize as number) * 1.1,
391
+ backgroundColor: textColor,
392
+ marginLeft: 2,
393
+ }}
394
+ />
395
+ )}
396
+ </>
397
+ )}
398
+ </View>
399
+ </Pressable>
400
+ </View>
401
+ </Animated.View>
402
+ )
403
+ }
254
404
 
405
+ export default MoneyValue
@@ -0,0 +1,170 @@
1
+ import React, { useMemo } from 'react'
2
+ import {
3
+ Pressable,
4
+ Text,
5
+ View,
6
+ type StyleProp,
7
+ type TextStyle,
8
+ type ViewStyle,
9
+ } from 'react-native'
10
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
11
+ import GlassFill from '../../utils/GlassFill/GlassFill'
12
+ import type { Modes } from '../../design-tokens'
13
+
14
+ export type NumberPaginationProps = {
15
+ /** The 1-based active page index. Ignored if `children` is supplied. Defaults to 1. */
16
+ activePage?: number;
17
+ /** The total number of pages. Ignored if `children` is supplied. Defaults to 1. */
18
+ totalPages?: number;
19
+ /** Callback fired when a page button is pressed. Ignored if `children` is supplied. */
20
+ onPageChange?: (page: number) => void;
21
+ /** Custom children to override the default page buttons. */
22
+ children?: React.ReactNode;
23
+ modes?: Modes;
24
+ style?: StyleProp<ViewStyle>;
25
+ }
26
+
27
+ interface NumberPaginationTokens {
28
+ containerStyle: ViewStyle;
29
+ activeItemStyle: ViewStyle;
30
+ inactiveItemStyle: ViewStyle;
31
+ activeTextStyle: TextStyle;
32
+ inactiveTextStyle: TextStyle;
33
+ blurIntensity: number;
34
+ backgroundColor: string;
35
+ }
36
+
37
+ function resolveNumberPaginationTokens(modes: Modes): NumberPaginationTokens {
38
+ const horizontalPadding = (getVariableByName('numberPagination/padding/horizontal', modes) ?? 4) as number
39
+ const verticalPadding = (getVariableByName('numberPagination/padding/vertical', modes) ?? 4) as number
40
+ const radiusRaw = (getVariableByName('numberPagination/radius', modes) ?? 9999) as number
41
+ const backgroundColor = (getVariableByName('numberPagination/background/color', modes) ?? 'rgba(141, 141, 141, 0.4)') as string
42
+ const borderColor = (getVariableByName('numberPagination/border/color', modes) ?? 'rgba(255, 255, 255, 0.15)') as string
43
+
44
+ // control number variables
45
+ const itemWidth = (getVariableByName('controlNumber/width', modes) ?? 32) as number
46
+ const itemHeight = (getVariableByName('controlNumber/height', modes) ?? 32) as number
47
+ const itemRadiusRaw = (getVariableByName('controlNumber/radius', modes) ?? 9999) as number
48
+ const activeBgColor = (getVariableByName('control/active/background/color', modes) ?? '#ffffff') as string
49
+ const activeTextColor = (getVariableByName('control/active/text/color', modes) ?? '#000000') as string
50
+ const inactiveTextColor = (getVariableByName('control/text/default', modes) ?? '#ffffff') as string
51
+
52
+ // typography variables
53
+ const fontFamily = (getVariableByName('numberPagination/fontFamily', modes) ?? 'JioType') as string
54
+ const fontSize = (getVariableByName('numberPagination/fontSize', modes) ?? 16) as number
55
+ const fontWeight = String(getVariableByName('numberPagination/fontWeight', modes) ?? '500') as TextStyle['fontWeight']
56
+ const lineHeight = (getVariableByName('numberPagination/lineHeight', modes) ?? 18) as number
57
+
58
+ const blurMinimal = (getVariableByName('blur/minimal', modes) ?? 29) as number
59
+ const blurIntensity = Math.max(0, Math.min(100, Math.round(Number(blurMinimal))))
60
+
61
+ const borderRadius = radiusRaw === 9999 ? 39 / 2 : radiusRaw
62
+ const itemRadius = itemRadiusRaw === 9999 ? itemWidth / 2 : itemRadiusRaw
63
+
64
+ return {
65
+ containerStyle: {
66
+ height: 39,
67
+ borderRadius,
68
+ borderWidth: 1,
69
+ borderColor,
70
+ backgroundColor: 'transparent',
71
+ overflow: 'hidden',
72
+ flexDirection: 'row',
73
+ alignItems: 'center',
74
+ paddingHorizontal: horizontalPadding,
75
+ paddingVertical: verticalPadding,
76
+ },
77
+ activeItemStyle: {
78
+ width: itemWidth,
79
+ height: itemHeight,
80
+ borderRadius: itemRadius,
81
+ backgroundColor: activeBgColor,
82
+ alignItems: 'center',
83
+ justifyContent: 'center',
84
+ },
85
+ inactiveItemStyle: {
86
+ width: itemWidth,
87
+ height: itemHeight,
88
+ borderRadius: itemRadius,
89
+ backgroundColor: 'transparent',
90
+ alignItems: 'center',
91
+ justifyContent: 'center',
92
+ },
93
+ activeTextStyle: {
94
+ fontFamily,
95
+ fontSize,
96
+ fontWeight,
97
+ lineHeight,
98
+ color: activeTextColor,
99
+ textAlign: 'center',
100
+ },
101
+ inactiveTextStyle: {
102
+ fontFamily,
103
+ fontSize,
104
+ fontWeight,
105
+ lineHeight,
106
+ color: inactiveTextColor,
107
+ textAlign: 'center',
108
+ },
109
+ blurIntensity,
110
+ backgroundColor,
111
+ }
112
+ }
113
+
114
+ /**
115
+ * NumberPagination component displays a horizontal list of page numbers on a frosted glass surface.
116
+ * All styling is driven by Figma design tokens with support for custom children slot.
117
+ */
118
+ export default function NumberPagination({
119
+ activePage = 1,
120
+ totalPages = 1,
121
+ onPageChange,
122
+ children,
123
+ modes,
124
+ style,
125
+ }: NumberPaginationProps) {
126
+ const tokens = useMemo(() => resolveNumberPaginationTokens(modes ?? {}), [modes])
127
+
128
+ const renderPages = () => {
129
+ if (children) {
130
+ return children
131
+ }
132
+
133
+ const pages = []
134
+ for (let i = 1; i <= totalPages; i++) {
135
+ const active = i === activePage
136
+ pages.push(
137
+ <Pressable
138
+ key={i}
139
+ onPress={() => onPageChange?.(i)}
140
+ style={({ pressed }) => [
141
+ active ? tokens.activeItemStyle : tokens.inactiveItemStyle,
142
+ pressed && { opacity: 0.7 },
143
+ ]}
144
+ accessibilityRole="button"
145
+ accessibilityState={{ selected: active }}
146
+ accessibilityLabel={`Page ${i}`}
147
+ >
148
+ <Text style={active ? tokens.activeTextStyle : tokens.inactiveTextStyle}>
149
+ {i}
150
+ </Text>
151
+ </Pressable>
152
+ )
153
+ }
154
+ return pages
155
+ }
156
+
157
+ return (
158
+ <View style={[tokens.containerStyle, style]}>
159
+ <GlassFill
160
+ tint="light"
161
+ intensity={tokens.blurIntensity}
162
+ overlayColor={tokens.backgroundColor}
163
+ androidTintWash={false}
164
+ />
165
+ <View style={{ flexDirection: 'row', alignItems: 'center', height: '100%' }}>
166
+ {renderPages()}
167
+ </View>
168
+ </View>
169
+ )
170
+ }
@@ -50,6 +50,30 @@ export interface PdpCcCardProps {
50
50
  title?: string
51
51
  /** Subtitle rendered below the title (14px medium). */
52
52
  subtitle?: string
53
+ /**
54
+ * Number of lines to limit the title to. The `Title` component defaults to
55
+ * `1` when this is unset, so the headline is a single line with an ellipsis
56
+ * by default. Pass a larger value (e.g. `2`) to allow the title to wrap onto
57
+ * that many lines before truncating, or set {@link disableTruncation} to
58
+ * remove the clamp entirely. The subtitle is never line-clamped.
59
+ */
60
+ numberOfLines?: number
61
+ /**
62
+ * When `true`, disables truncation for both the title and subtitle: the
63
+ * trailing ellipsis (`…`) is never shown and the `numberOfLines` clamp is
64
+ * ignored. The text is also never shrunk by a tight parent — it keeps its
65
+ * full intrinsic size and overflows (even breaking the parent layout) rather
66
+ * than ever clipping into an ellipsis. The text still wraps onto multiple
67
+ * lines.
68
+ */
69
+ disableTruncation?: boolean
70
+ /**
71
+ * When `true`, forces both the title and subtitle onto a single line with no
72
+ * wrapping (implies {@link disableTruncation}). On web the line overflows
73
+ * horizontally — brutally breaking the parent layout if it has to — instead
74
+ * of wrapping or truncating.
75
+ */
76
+ singleLine?: boolean
53
77
  /**
54
78
  * The metric columns rendered in the stats row. Vertical dividers are
55
79
  * inserted automatically between adjacent metrics. Defaults to two sample
@@ -105,6 +129,9 @@ function PdpCcCard({
105
129
  media,
106
130
  title = 'Title',
107
131
  subtitle = 'Subtitle',
132
+ numberOfLines,
133
+ disableTruncation,
134
+ singleLine,
108
135
  metrics = DEFAULT_METRICS,
109
136
  buttonLabel = 'button',
110
137
  buttonIcon = 'ic_add_circle',
@@ -165,7 +192,15 @@ function PdpCcCard({
165
192
  <>
166
193
  <View style={styles.mediaSlot}>{mediaNode}</View>
167
194
 
168
- <Title title={title} subtitle={subtitle} textAlign="Center" modes={modes} />
195
+ <Title
196
+ title={title}
197
+ subtitle={subtitle}
198
+ textAlign="Center"
199
+ modes={modes}
200
+ numberOfLines={numberOfLines}
201
+ disableTruncation={disableTruncation}
202
+ singleLine={singleLine}
203
+ />
169
204
 
170
205
  {metrics.length > 0 ? (
171
206
  <View style={styles.metricsRow}>