jfs-components 0.0.56 → 0.0.58

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 (26) hide show
  1. package/lib/commonjs/components/ActionFooter/ActionFooter.js +20 -14
  2. package/lib/commonjs/components/ButtonGroup/ButtonGroup.js +10 -7
  3. package/lib/commonjs/components/ListGroup/ListGroup.js +1 -1
  4. package/lib/commonjs/components/Section/Section.js +71 -8
  5. package/lib/commonjs/components/Tabs/Tabs.js +6 -3
  6. package/lib/commonjs/components/TransactionBubble/TransactionBubble.js +24 -20
  7. package/lib/commonjs/icons/registry.js +1 -1
  8. package/lib/module/components/ActionFooter/ActionFooter.js +21 -15
  9. package/lib/module/components/ButtonGroup/ButtonGroup.js +10 -7
  10. package/lib/module/components/ListGroup/ListGroup.js +1 -1
  11. package/lib/module/components/Section/Section.js +73 -9
  12. package/lib/module/components/Tabs/Tabs.js +6 -3
  13. package/lib/module/components/TransactionBubble/TransactionBubble.js +24 -20
  14. package/lib/module/icons/registry.js +1 -1
  15. package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +4 -10
  16. package/lib/typescript/src/components/ButtonGroup/ButtonGroup.d.ts +3 -1
  17. package/lib/typescript/src/components/TransactionBubble/TransactionBubble.d.ts +9 -6
  18. package/lib/typescript/src/icons/registry.d.ts +1 -1
  19. package/package.json +1 -1
  20. package/src/components/ActionFooter/ActionFooter.tsx +20 -14
  21. package/src/components/ButtonGroup/ButtonGroup.tsx +16 -10
  22. package/src/components/ListGroup/ListGroup.tsx +1 -1
  23. package/src/components/Section/Section.tsx +90 -8
  24. package/src/components/Tabs/Tabs.tsx +2 -2
  25. package/src/components/TransactionBubble/TransactionBubble.tsx +17 -14
  26. package/src/icons/registry.ts +1 -1
@@ -38,18 +38,12 @@ export type ActionFooterProps = {
38
38
  *
39
39
  * @example
40
40
  * ```tsx
41
- * // Basic usage - modes are automatically passed to all children
41
+ * // Basic usage - modes are automatically passed to all children.
42
+ * // Non-IconButton children (e.g., Button) are auto-stretched to fill.
42
43
  * <ActionFooter modes={modes}>
43
44
  * <IconButton iconName="ic_split" />
44
- * <Button label="Request" style={{ flex: 1 }} />
45
- * <Button label="Pay" style={{ flex: 1 }} />
46
- * </ActionFooter>
47
- *
48
- * // Children can override with their own modes (merged with parent)
49
- * <ActionFooter modes={modes}>
50
- * <IconButton iconName="ic_split" />
51
- * <Button label="Request" modes={{ Appearance: 'secondary' }} style={{ flex: 1 }} />
52
- * <Button label="Pay" modes={{ Appearance: 'primary' }} style={{ flex: 1 }} />
45
+ * <Button label="Request" />
46
+ * <Button label="Pay" />
53
47
  * </ActionFooter>
54
48
  * ```
55
49
  */
@@ -21,13 +21,15 @@ export type ButtonGroupProps = {
21
21
  * and handles their layout and styling/theming via design tokens.
22
22
  *
23
23
  * It passes the provided `modes` to all its immediate React Element children.
24
+ * Non-IconButton children (e.g., Button) are automatically stretched to fill
25
+ * available space. IconButton children keep their natural width.
24
26
  *
25
27
  * @component
26
28
  * @example
27
29
  * ```jsx
28
30
  * <ButtonGroup modes={{"Appearance": "light"}}>
29
31
  * <IconButton iconName="ic_qr_code" />
30
- * <IconButton iconName="ic_photo" />
32
+ * <Button label="Pay" />
31
33
  * </ButtonGroup>
32
34
  * ```
33
35
  */
@@ -20,16 +20,19 @@ export type TransactionBubbleProps = {
20
20
  /**
21
21
  * TransactionBubble — Figma node 1517:1155.
22
22
  *
23
- * Layout (single horizontal row inside a rounded bordered pill):
23
+ * Layout (vertical stack inside a rounded bordered pill):
24
24
  *
25
25
  * ┌──────────────────────────────────────────────┐
26
- * │ Description Status · Date
27
- * │ ₹56
26
+ * │ Description
27
+ * │ ₹56
28
+ * │ [slot / children content] │
29
+ * │ ⚠ Expired · 20 Mar 2025 › │
28
30
  * └──────────────────────────────────────────────┘
29
31
  *
30
- * Left column: description text + MoneyValue, stacked vertically with `transactionBubble/gap`.
31
- * Right column: TransactionStatus + NavArrow, stacked vertically with `transactionBubble/statusWrap/gap`,
32
- * right-aligned.
32
+ * moneyValueWrap: description + MoneyValue, vertical with `transactionBubble/wrap/gap`.
33
+ * slotWrap: children (optional), rendered between moneyValueWrap and statusWrap.
34
+ * statusWrap: TransactionStatus + NavArrow, horizontal row with `transactionBubble/statusWrap/gap`.
35
+ * Container gap between sections: `transactionBubble/gap`.
33
36
  */
34
37
  declare function TransactionBubble({ description, value, currency, status, date, statusSlot, children, modes, onPress, style, accessibilityLabel, accessibilityHint, webAccessibilityProps, ...rest }: TransactionBubbleProps): import("react/jsx-runtime").JSX.Element;
35
38
  export default TransactionBubble;
@@ -4,7 +4,7 @@
4
4
  * Auto-generated from SVG files in src/icons/
5
5
  * DO NOT EDIT MANUALLY - Run "npm run icons:generate" to regenerate
6
6
  *
7
- * Generated: 2026-04-14T12:07:25.514Z
7
+ * Generated: 2026-04-14T19:47:53.357Z
8
8
  */
9
9
  export declare const iconRegistry: Record<string, {
10
10
  path: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jfs-components",
3
- "version": "0.0.56",
3
+ "version": "0.0.58",
4
4
  "description": "React Native Jio Finance Components Library",
5
5
  "author": "sunshuaiqi@gmail.com",
6
6
  "license": "MIT",
@@ -6,7 +6,8 @@ import {
6
6
  Platform,
7
7
  } from 'react-native'
8
8
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
9
- import { cloneChildrenWithModes } from '../../utils/react-utils'
9
+ import { cloneChildrenWithModes, flattenChildren } from '../../utils/react-utils'
10
+ import IconButton from '../IconButton/IconButton'
10
11
 
11
12
  export type ActionFooterProps = {
12
13
  /**
@@ -47,18 +48,12 @@ export type ActionFooterProps = {
47
48
  *
48
49
  * @example
49
50
  * ```tsx
50
- * // Basic usage - modes are automatically passed to all children
51
+ * // Basic usage - modes are automatically passed to all children.
52
+ * // Non-IconButton children (e.g., Button) are auto-stretched to fill.
51
53
  * <ActionFooter modes={modes}>
52
54
  * <IconButton iconName="ic_split" />
53
- * <Button label="Request" style={{ flex: 1 }} />
54
- * <Button label="Pay" style={{ flex: 1 }} />
55
- * </ActionFooter>
56
- *
57
- * // Children can override with their own modes (merged with parent)
58
- * <ActionFooter modes={modes}>
59
- * <IconButton iconName="ic_split" />
60
- * <Button label="Request" modes={{ Appearance: 'secondary' }} style={{ flex: 1 }} />
61
- * <Button label="Pay" modes={{ Appearance: 'primary' }} style={{ flex: 1 }} />
55
+ * <Button label="Request" />
56
+ * <Button label="Pay" />
62
57
  * </ActionFooter>
63
58
  * ```
64
59
  */
@@ -113,8 +108,19 @@ function ActionFooter({
113
108
  ? { boxShadow: '0px -12px 24px 0px rgba(12, 13, 16, 0.12), 0px -16px 48px 0px rgba(12, 13, 16, 0.16)' } as any
114
109
  : {}
115
110
 
116
- // Pass modes prop to all slot children
117
- const childrenWithModes = cloneChildrenWithModes(children, modes)
111
+ const flatChildren = flattenChildren(children)
112
+ const processedChildren = cloneChildrenWithModes(flatChildren, modes)
113
+
114
+ const enhancedChildren = (processedChildren as React.ReactNode[]).map((child, index) => {
115
+ if (!React.isValidElement(child)) return child
116
+ const element = child as React.ReactElement<any>
117
+ const isIconButton = element.type === IconButton
118
+ const stretchStyle = isIconButton ? undefined : { flex: 1 }
119
+ return React.cloneElement(element, {
120
+ key: element.key ?? index,
121
+ style: [stretchStyle, element.props.style],
122
+ })
123
+ })
118
124
 
119
125
  return (
120
126
  <View
@@ -123,7 +129,7 @@ function ActionFooter({
123
129
  accessibilityLabel={undefined}
124
130
  >
125
131
  <View style={slotStyle}>
126
- {childrenWithModes}
132
+ {enhancedChildren}
127
133
  </View>
128
134
  </View>
129
135
  )
@@ -6,6 +6,7 @@ import {
6
6
  } from 'react-native'
7
7
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
8
8
  import { flattenChildren } from '../../utils/react-utils'
9
+ import IconButton from '../IconButton/IconButton'
9
10
 
10
11
  export type ButtonGroupProps = {
11
12
  /**
@@ -29,13 +30,15 @@ export type ButtonGroupProps = {
29
30
  * and handles their layout and styling/theming via design tokens.
30
31
  *
31
32
  * It passes the provided `modes` to all its immediate React Element children.
33
+ * Non-IconButton children (e.g., Button) are automatically stretched to fill
34
+ * available space. IconButton children keep their natural width.
32
35
  *
33
36
  * @component
34
37
  * @example
35
38
  * ```jsx
36
39
  * <ButtonGroup modes={{"Appearance": "light"}}>
37
40
  * <IconButton iconName="ic_qr_code" />
38
- * <IconButton iconName="ic_photo" />
41
+ * <Button label="Pay" />
39
42
  * </ButtonGroup>
40
43
  * ```
41
44
  */
@@ -59,19 +62,22 @@ function ButtonGroup({
59
62
  }
60
63
 
61
64
  // Flatten children to handle Fragments properly
62
- const flatChildren = flattenChildren(children);
65
+ const flatChildren = flattenChildren(children)
63
66
 
64
- // Clone children to pass `modes` prop
65
67
  const childrenWithModes = React.Children.map(flatChildren, (child) => {
66
68
  if (React.isValidElement(child)) {
67
- // We safely try to pass `modes` to the child.
68
- // We merge the group modes with any modes defined on the child explicitly.
69
- // Child modes take precedence.
70
- const element = child as React.ReactElement<any>;
71
- const childModes = element.props.modes || {};
72
- const mergedModes = { ...modes, ...childModes };
69
+ const element = child as React.ReactElement<any>
70
+ const childModes = element.props.modes || {}
71
+ const mergedModes = { ...modes, ...childModes }
72
+ const isIconButton = element.type === IconButton
73
+ const stretchStyle = isIconButton
74
+ ? undefined
75
+ : { flex: 1 }
73
76
 
74
- return React.cloneElement(element, { modes: mergedModes })
77
+ return React.cloneElement(element, {
78
+ modes: mergedModes,
79
+ style: [stretchStyle, element.props.style],
80
+ })
75
81
  }
76
82
  return child
77
83
  })
@@ -73,7 +73,7 @@ function ListGroup({
73
73
  borderWidth,
74
74
  borderStyle: 'solid' as const,
75
75
  flexDirection: 'column',
76
- alignItems: 'flex-start',
76
+ alignItems: 'stretch',
77
77
  width: '100%',
78
78
  }
79
79
 
@@ -1,10 +1,87 @@
1
- import React, { useState } from 'react'
1
+ import React, { useState, useRef, useCallback, useEffect } from 'react'
2
2
  import { View, Text, Pressable, Platform, type StyleProp, type ViewStyle, type PressableStateCallbackType } from 'react-native'
3
3
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
4
4
  import NavArrow from '../NavArrow/NavArrow'
5
5
  import { usePressableWebSupport, type WebAccessibilityProps } from '../../utils/web-platform-utils'
6
6
  import { cloneChildrenWithModes, flattenChildren } from '../../utils/react-utils'
7
7
 
8
+ // ---------------------------------------------------------------------------
9
+ // Shared grid layout: measures widest child, enforces uniform width,
10
+ // chunks into fixed rows of up to maxColumns, space-between per row.
11
+ // ---------------------------------------------------------------------------
12
+ const SLOT_GRID_MAX_COLUMNS = 4
13
+
14
+ type SlotGridProps = {
15
+ items: React.ReactNode[];
16
+ gap: number;
17
+ maxColumns?: number;
18
+ }
19
+
20
+ function SlotGrid({ items, gap, maxColumns = SLOT_GRID_MAX_COLUMNS }: SlotGridProps) {
21
+ const [maxItemWidth, setMaxItemWidth] = useState<number | null>(null)
22
+ const itemWidthsRef = useRef<Map<number, number>>(new Map())
23
+ const totalItems = items.length
24
+
25
+ useEffect(() => {
26
+ itemWidthsRef.current.clear()
27
+ setMaxItemWidth(null)
28
+ }, [totalItems])
29
+
30
+ const handleItemLayout = useCallback((index: number, width: number) => {
31
+ itemWidthsRef.current.set(index, width)
32
+ if (itemWidthsRef.current.size >= totalItems && totalItems > 0) {
33
+ setMaxItemWidth(Math.max(...itemWidthsRef.current.values()))
34
+ }
35
+ }, [totalItems])
36
+
37
+ const isMeasured = maxItemWidth !== null
38
+ const columns = Math.min(maxColumns, totalItems || 1)
39
+
40
+ const rows: React.ReactNode[][] = []
41
+ for (let i = 0; i < items.length; i += columns) {
42
+ rows.push(items.slice(i, i + columns))
43
+ }
44
+
45
+ return (
46
+ <View style={{ gap, ...(isMeasured ? {} : { opacity: 0 }) }}>
47
+ {rows.map((row, rowIndex) => {
48
+ const spacersNeeded = row.length < columns ? columns - row.length : 0
49
+ return (
50
+ <View
51
+ key={rowIndex}
52
+ style={{
53
+ flexDirection: 'row' as const,
54
+ justifyContent: 'space-between' as const,
55
+ }}
56
+ >
57
+ {row.map((child, colIndex) => {
58
+ const itemIndex = rowIndex * columns + colIndex
59
+ return (
60
+ <View
61
+ key={itemIndex}
62
+ onLayout={
63
+ !isMeasured
64
+ ? (e) => handleItemLayout(itemIndex, e.nativeEvent.layout.width)
65
+ : undefined
66
+ }
67
+ style={isMeasured ? { width: maxItemWidth } : undefined}
68
+ >
69
+ {child}
70
+ </View>
71
+ )
72
+ })}
73
+ {isMeasured &&
74
+ spacersNeeded > 0 &&
75
+ Array.from({ length: spacersNeeded }, (_, i) => (
76
+ <View key={`spacer-${i}`} style={{ width: maxItemWidth }} />
77
+ ))}
78
+ </View>
79
+ )
80
+ })}
81
+ </View>
82
+ )
83
+ }
84
+
8
85
  type SectionProps = {
9
86
  title?: string;
10
87
  supportText?: string;
@@ -233,8 +310,14 @@ function Section({
233
310
  {headerContent}
234
311
  </View>
235
312
  )}
236
- {slot && (
237
- <View style={{ flexDirection: slotDirection, gap: slotGap }}>
313
+ {slot && slotDirection === 'row' && (
314
+ <SlotGrid
315
+ items={cloneChildrenWithModes(flattenChildren(slot), modes)}
316
+ gap={sectionGap}
317
+ />
318
+ )}
319
+ {slot && slotDirection === 'column' && (
320
+ <View style={{ flexDirection: 'column' as const, gap: slotGap }}>
238
321
  {cloneChildrenWithModes(flattenChildren(slot), modes)}
239
322
  </View>
240
323
  )}
@@ -301,8 +384,6 @@ function SectionBento({
301
384
  gap,
302
385
  }
303
386
 
304
-
305
-
306
387
  const processedNavSlot = navSlot
307
388
  ? cloneChildrenWithModes(flattenChildren(navSlot), modes)
308
389
  : null
@@ -320,9 +401,10 @@ function SectionBento({
320
401
  {...rest}
321
402
  >
322
403
  {processedNavSlot && (
323
- <View style={{ flexDirection: 'row' as const, gap }}>
324
- {processedNavSlot}
325
- </View>
404
+ <SlotGrid
405
+ items={processedNavSlot as React.ReactNode[]}
406
+ gap={gap}
407
+ />
326
408
  )}
327
409
  {processedUpiSlot && (
328
410
  <View style={{ flexDirection: 'row' as const, gap: 8 }}>
@@ -58,12 +58,12 @@ function Tabs({
58
58
  const paddingLeft = (getVariableByName('tabs/padding/left', modes) ?? 0) as number
59
59
  const paddingRight = (getVariableByName('tabs/padding/right', modes) ?? 0) as number
60
60
 
61
- // Forward modes to all TabItem children
62
61
  const enhancedChildren = React.Children.map(children, (child) => {
63
62
  if (React.isValidElement(child) && child.type === TabItem) {
64
- const childElement = child as React.ReactElement<any>;
63
+ const childElement = child as React.ReactElement<any>
65
64
  return React.cloneElement(childElement, {
66
65
  modes: { ...modes, ...(childElement.props.modes ?? {}) },
66
+ ...(scrollable ? {} : { style: [{ flex: 1 }, childElement.props.style] }),
67
67
  })
68
68
  }
69
69
  return child
@@ -27,16 +27,19 @@ export type TransactionBubbleProps = {
27
27
  /**
28
28
  * TransactionBubble — Figma node 1517:1155.
29
29
  *
30
- * Layout (single horizontal row inside a rounded bordered pill):
30
+ * Layout (vertical stack inside a rounded bordered pill):
31
31
  *
32
32
  * ┌──────────────────────────────────────────────┐
33
- * │ Description Status · Date
34
- * │ ₹56
33
+ * │ Description
34
+ * │ ₹56
35
+ * │ [slot / children content] │
36
+ * │ ⚠ Expired · 20 Mar 2025 › │
35
37
  * └──────────────────────────────────────────────┘
36
38
  *
37
- * Left column: description text + MoneyValue, stacked vertically with `transactionBubble/gap`.
38
- * Right column: TransactionStatus + NavArrow, stacked vertically with `transactionBubble/statusWrap/gap`,
39
- * right-aligned.
39
+ * moneyValueWrap: description + MoneyValue, vertical with `transactionBubble/wrap/gap`.
40
+ * slotWrap: children (optional), rendered between moneyValueWrap and statusWrap.
41
+ * statusWrap: TransactionStatus + NavArrow, horizontal row with `transactionBubble/statusWrap/gap`.
42
+ * Container gap between sections: `transactionBubble/gap`.
40
43
  */
41
44
  function TransactionBubble({
42
45
  description = 'Payment to Uber India',
@@ -65,7 +68,7 @@ function TransactionBubble({
65
68
  const backgroundColor = getVariableByName('transactionBubble/background', resolvedModes) || '#ffffff'
66
69
  const borderColor = getVariableByName('transactionBubble/border/color', resolvedModes) || '#e5e5e5'
67
70
  const bubbleGap = Number(getVariableByName('transactionBubble/gap', resolvedModes)) || 8
68
- const wrapGap = Number(getVariableByName('transactionBubble/wrap/gap', resolvedModes)) || 8
71
+ const moneyValueWrapGap = Number(getVariableByName('transactionBubble/wrap/gap', resolvedModes)) || 8
69
72
 
70
73
  const descriptionColor = getVariableByName('transactionBubble/description/color', resolvedModes) || '#24262b'
71
74
  const descriptionFontSize = Number(getVariableByName('transactionBubble/description/fontSize', resolvedModes)) || 14
@@ -73,6 +76,7 @@ function TransactionBubble({
73
76
  const descriptionFontFamily = getVariableByName('transactionBubble/description/fontFamily', resolvedModes) || 'JioType Var'
74
77
 
75
78
  const statusWrapGap = Number(getVariableByName('transactionBubble/statusWrap/gap', resolvedModes)) || 4
79
+ const statusWrapHeight = Number(getVariableByName('transactionBubble/statusWrap/height', resolvedModes)) || 18
76
80
 
77
81
  const containerStyle: ViewStyle = {
78
82
  padding,
@@ -107,20 +111,21 @@ function TransactionBubble({
107
111
  webAccessibilityProps,
108
112
  })
109
113
 
110
- const rightColumn = processedStatusSlot || (
111
- <View style={{ alignItems: 'flex-end', justifyContent: 'flex-end', gap: statusWrapGap }}>
114
+ const statusRow = processedStatusSlot || (
115
+ <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: statusWrapGap, height: statusWrapHeight }}>
112
116
  <TransactionStatus status={status} date={date} modes={resolvedModes} />
113
117
  <NavArrow direction="Forward" modes={resolvedModes} />
114
118
  </View>
115
119
  )
116
120
 
117
121
  const mainContent = (
118
- <View style={{ flexDirection: 'row', gap: wrapGap }}>
119
- <View style={{ flex: 1, gap: bubbleGap }}>
122
+ <View style={{ gap: bubbleGap }}>
123
+ <View style={{ gap: moneyValueWrapGap }}>
120
124
  <Text style={descriptionStyle} numberOfLines={1}>{description}</Text>
121
125
  <MoneyValue value={value} currency={currency} modes={resolvedModes} />
122
126
  </View>
123
- {rightColumn}
127
+ {processedChildren}
128
+ {statusRow}
124
129
  </View>
125
130
  )
126
131
 
@@ -136,7 +141,6 @@ function TransactionBubble({
136
141
  {...rest}
137
142
  >
138
143
  {mainContent}
139
- {processedChildren}
140
144
  </Pressable>
141
145
  )
142
146
  }
@@ -144,7 +148,6 @@ function TransactionBubble({
144
148
  return (
145
149
  <View style={[containerStyle, style]} accessibilityLabel={defaultAccessibilityLabel} accessibilityHint={accessibilityHint}>
146
150
  {mainContent}
147
- {processedChildren}
148
151
  </View>
149
152
  )
150
153
  }
@@ -4,7 +4,7 @@
4
4
  * Auto-generated from SVG files in src/icons/
5
5
  * DO NOT EDIT MANUALLY - Run "npm run icons:generate" to regenerate
6
6
  *
7
- * Generated: 2026-04-14T12:07:25.514Z
7
+ * Generated: 2026-04-14T19:47:53.357Z
8
8
  */
9
9
 
10
10
  // Icon name to SVG data mapping