jfs-components 0.0.57 → 0.0.59

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 (28) hide show
  1. package/lib/commonjs/components/ActionFooter/ActionFooter.js +20 -14
  2. package/lib/commonjs/components/Button/Button.js +1 -1
  3. package/lib/commonjs/components/ButtonGroup/ButtonGroup.js +11 -8
  4. package/lib/commonjs/components/Drawer/Drawer.js +1 -1
  5. package/lib/commonjs/components/ListGroup/ListGroup.js +1 -1
  6. package/lib/commonjs/components/Section/Section.js +71 -8
  7. package/lib/commonjs/components/Tabs/Tabs.js +6 -3
  8. package/lib/commonjs/icons/registry.js +1 -1
  9. package/lib/module/components/ActionFooter/ActionFooter.js +21 -15
  10. package/lib/module/components/Button/Button.js +1 -1
  11. package/lib/module/components/ButtonGroup/ButtonGroup.js +11 -8
  12. package/lib/module/components/Drawer/Drawer.js +1 -1
  13. package/lib/module/components/ListGroup/ListGroup.js +1 -1
  14. package/lib/module/components/Section/Section.js +73 -9
  15. package/lib/module/components/Tabs/Tabs.js +6 -3
  16. package/lib/module/icons/registry.js +1 -1
  17. package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +4 -10
  18. package/lib/typescript/src/components/ButtonGroup/ButtonGroup.d.ts +3 -1
  19. package/lib/typescript/src/icons/registry.d.ts +1 -1
  20. package/package.json +1 -1
  21. package/src/components/ActionFooter/ActionFooter.tsx +20 -14
  22. package/src/components/Button/Button.tsx +1 -1
  23. package/src/components/ButtonGroup/ButtonGroup.tsx +17 -11
  24. package/src/components/Drawer/Drawer.tsx +1 -1
  25. package/src/components/ListGroup/ListGroup.tsx +1 -1
  26. package/src/components/Section/Section.tsx +90 -8
  27. package/src/components/Tabs/Tabs.tsx +2 -2
  28. 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
  */
@@ -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-14T14:14:19.990Z
7
+ * Generated: 2026-04-14T20:02:24.340Z
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.57",
3
+ "version": "0.0.59",
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
  )
@@ -97,7 +97,7 @@ function Button({
97
97
  const lineHeight = getVariableByName('button/lineHeight', modes) || 19
98
98
  const fontSize = getVariableByName('button/fontSize', modes) || 16
99
99
  const textColor = getVariableByName('button/foreground', modes) || '#0f0d0a'
100
- const iconColor = getVariableByName('button/icon/color', modes) ?? textColor
100
+ const iconColor = textColor
101
101
  const iconSize = getVariableByName('button/icon/size', modes) ?? 18
102
102
 
103
103
  const [isHovered, setIsHovered] = useState(false)
@@ -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
  */
@@ -45,7 +48,7 @@ function ButtonGroup({
45
48
  style,
46
49
  }: ButtonGroupProps) {
47
50
  // Resolve design tokens
48
- const gap = getVariableByName('buttonGroup/padding/gap', modes) ?? 12
51
+ const gap = getVariableByName('buttonGroup/gap', modes) ?? 12
49
52
  const paddingHorizontal = getVariableByName('buttonGroup/padding/horizontal', modes) ?? 0
50
53
  const paddingVertical = getVariableByName('buttonGroup/padding/vertical', modes) ?? 0
51
54
 
@@ -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
  })
@@ -276,7 +276,7 @@ function Drawer({
276
276
 
277
277
  // Design Tokens
278
278
  const backgroundColor = getVariableByName('drawer/background', modes) || '#f5f5f5'
279
- const radius = getVariableByName('drawer/radius', modes) || 12
279
+ const radius = getVariableByName('drawer/radius/top', modes) || 12
280
280
 
281
281
  // Handle
282
282
  const handleColor = getVariableByName('drawer/handlebar/background', modes) || '#e0e0e3'
@@ -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
@@ -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-14T14:14:19.990Z
7
+ * Generated: 2026-04-14T20:02:24.340Z
8
8
  */
9
9
 
10
10
  // Icon name to SVG data mapping