jfs-components 0.0.54 → 0.0.56

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 (55) hide show
  1. package/lib/commonjs/components/Accordion/Accordion.js +2 -34
  2. package/lib/commonjs/components/AppBar/AppBar.js +4 -36
  3. package/lib/commonjs/components/Drawer/Drawer.js +11 -4
  4. package/lib/commonjs/components/FilterBar/FilterBar.js +2 -34
  5. package/lib/commonjs/components/LazyList/LazyList.js +2 -34
  6. package/lib/commonjs/components/ListGroup/ListGroup.js +15 -9
  7. package/lib/commonjs/components/MoneyValue/MoneyValue.js +1 -1
  8. package/lib/commonjs/components/NavArrow/NavArrow.js +45 -44
  9. package/lib/commonjs/components/Numpad/Numpad.js +6 -5
  10. package/lib/commonjs/components/Section/Section.js +7 -8
  11. package/lib/commonjs/components/TextInput/TextInput.js +4 -38
  12. package/lib/commonjs/components/TransactionBubble/TransactionBubble.js +145 -0
  13. package/lib/commonjs/components/index.js +7 -0
  14. package/lib/commonjs/design-tokens/JFSThemeProvider.js +38 -3
  15. package/lib/commonjs/icons/registry.js +1 -1
  16. package/lib/commonjs/utils/react-utils.js +18 -13
  17. package/lib/module/components/Accordion/Accordion.js +1 -33
  18. package/lib/module/components/AppBar/AppBar.js +1 -34
  19. package/lib/module/components/Drawer/Drawer.js +11 -4
  20. package/lib/module/components/FilterBar/FilterBar.js +1 -35
  21. package/lib/module/components/LazyList/LazyList.js +1 -35
  22. package/lib/module/components/ListGroup/ListGroup.js +15 -9
  23. package/lib/module/components/MoneyValue/MoneyValue.js +1 -1
  24. package/lib/module/components/NavArrow/NavArrow.js +44 -44
  25. package/lib/module/components/Numpad/Numpad.js +5 -5
  26. package/lib/module/components/Section/Section.js +8 -9
  27. package/lib/module/components/TextInput/TextInput.js +2 -36
  28. package/lib/module/components/TransactionBubble/TransactionBubble.js +140 -0
  29. package/lib/module/components/index.js +1 -0
  30. package/lib/module/design-tokens/JFSThemeProvider.js +35 -3
  31. package/lib/module/icons/registry.js +1 -1
  32. package/lib/module/utils/react-utils.js +18 -13
  33. package/lib/typescript/src/components/ListGroup/ListGroup.d.ts +12 -7
  34. package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +6 -11
  35. package/lib/typescript/src/components/TransactionBubble/TransactionBubble.d.ts +36 -0
  36. package/lib/typescript/src/components/index.d.ts +1 -0
  37. package/lib/typescript/src/design-tokens/JFSThemeProvider.d.ts +15 -0
  38. package/lib/typescript/src/icons/registry.d.ts +1 -1
  39. package/package.json +1 -1
  40. package/src/components/Accordion/Accordion.tsx +1 -44
  41. package/src/components/AppBar/AppBar.tsx +1 -44
  42. package/src/components/Drawer/Drawer.tsx +12 -4
  43. package/src/components/FilterBar/FilterBar.tsx +1 -44
  44. package/src/components/LazyList/LazyList.tsx +1 -41
  45. package/src/components/ListGroup/ListGroup.tsx +21 -11
  46. package/src/components/MoneyValue/MoneyValue.tsx +1 -1
  47. package/src/components/NavArrow/NavArrow.tsx +46 -43
  48. package/src/components/Numpad/Numpad.tsx +5 -5
  49. package/src/components/Section/Section.tsx +8 -8
  50. package/src/components/TextInput/TextInput.tsx +1 -44
  51. package/src/components/TransactionBubble/TransactionBubble.tsx +152 -0
  52. package/src/components/index.ts +1 -0
  53. package/src/design-tokens/JFSThemeProvider.tsx +37 -2
  54. package/src/icons/registry.ts +1 -1
  55. package/src/utils/react-utils.ts +29 -21
@@ -1,7 +1,7 @@
1
1
  import React from 'react'
2
2
  import { View, type ViewStyle } from 'react-native'
3
+ import Svg, { Polyline } from 'react-native-svg'
3
4
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
4
- import Icon from '../../icons/Icon'
5
5
 
6
6
  type NavArrowDirection = 'Back' | 'Forward' | 'Down'
7
7
 
@@ -17,23 +17,18 @@ type NavArrowProps = {
17
17
  } & Omit<React.ComponentProps<typeof View>, 'style' | 'accessibilityLabel'>
18
18
 
19
19
  /**
20
- * NavArrow component that displays a small chevron arrow for navigation.
20
+ * NavArrow component that displays a chevron arrow for navigation.
21
21
  *
22
- * This component uses design tokens for all visual properties:
22
+ * Renders a stroked SVG chevron whose dimensions and thickness are
23
+ * fully driven by design tokens:
23
24
  * - navArrow/icon/color - chevron stroke color
24
- * - navArrow/icon/width - icon width
25
- * - navArrow/icon/height - icon height
26
- * - navArrow/icon/strokeWeight - stroke width
25
+ * - navArrow/icon/width - chevron arm width (horizontal spread)
26
+ * - navArrow/icon/height - chevron arm height (vertical spread)
27
+ * - navArrow/icon/strokeWeight - stroke thickness
27
28
  * - navArrow/width - container width
28
29
  * - navArrow/height - container height
29
30
  * - navArrow/radius - border radius
30
31
  * - navArrow/background - background color
31
- *
32
- * @component
33
- * @param {Object} props
34
- * @param {'Back'|'Forward'|'Down'} [props.direction='Back'] - Arrow direction
35
- * @param {Object} [props.modes={}] - Modes for design token resolution
36
- * @param {Object} [props.style] - Additional container styles
37
32
  */
38
33
  export default function NavArrow({
39
34
  direction = 'Back',
@@ -42,11 +37,9 @@ export default function NavArrow({
42
37
  accessibilityLabel,
43
38
  ...rest
44
39
  }: NavArrowProps) {
45
- // Resolve design tokens
46
40
  const iconColor =
47
41
  (getVariableByName('navArrow/icon/color', modes) as string) || '#24262b'
48
42
 
49
- // Dimensions from tokens
50
43
  const widthToken = Number(getVariableByName('navArrow/width', modes)) || 6
51
44
  const heightToken = Number(getVariableByName('navArrow/height', modes)) || 10
52
45
  const borderRadius =
@@ -54,14 +47,18 @@ export default function NavArrow({
54
47
  const backgroundColor =
55
48
  (getVariableByName('navArrow/background', modes) as string) || 'transparent'
56
49
 
57
- // Swap dimensions if direction is Down
50
+ const iconWidth = Number(getVariableByName('navArrow/icon/width', modes)) || 4
51
+ const iconHeight = Number(getVariableByName('navArrow/icon/height', modes)) || 8
52
+ const strokeWeight =
53
+ Number(getVariableByName('navArrow/icon/strokeWeight', modes)) || 2
54
+
58
55
  const isDown = direction === 'Down'
59
- const width = isDown ? heightToken : widthToken
60
- const height = isDown ? widthToken : heightToken
56
+ const containerWidth = isDown ? heightToken : widthToken
57
+ const containerHeight = isDown ? widthToken : heightToken
61
58
 
62
59
  const containerStyle: ViewStyle = {
63
- width,
64
- height,
60
+ width: containerWidth,
61
+ height: containerHeight,
65
62
  borderRadius,
66
63
  backgroundColor,
67
64
  alignItems: 'center',
@@ -77,38 +74,44 @@ export default function NavArrow({
77
74
  ? 'Go forward'
78
75
  : 'Go down')
79
76
 
80
- // Map direction to icon name
81
- let iconName = 'ic_chevron_left' // Default for Back
82
- if (direction === 'Forward') {
83
- iconName = 'ic_chevron_right'
84
- } else if (direction === 'Down') {
85
- iconName = 'ic_chevron_down'
77
+ const chevronW = isDown ? iconHeight : iconWidth
78
+ const chevronH = isDown ? iconWidth : iconHeight
79
+
80
+ const pad = strokeWeight / 2
81
+ const svgWidth = chevronW + pad * 2
82
+ const svgHeight = chevronH + pad * 2
83
+
84
+ let points: string
85
+ switch (direction) {
86
+ case 'Forward':
87
+ points = `${pad},${pad} ${chevronW + pad},${chevronH / 2 + pad} ${pad},${chevronH + pad}`
88
+ break
89
+ case 'Down':
90
+ points = `${pad},${pad} ${chevronW / 2 + pad},${chevronH + pad} ${chevronW + pad},${pad}`
91
+ break
92
+ case 'Back':
93
+ default:
94
+ points = `${chevronW + pad},${pad} ${pad},${chevronH / 2 + pad} ${chevronW + pad},${chevronH + pad}`
95
+ break
86
96
  }
87
97
 
88
98
  return (
89
99
  <View
90
100
  style={containerStyle}
91
101
  accessibilityRole="image"
92
- accessibilityLabel={undefined}
102
+ accessibilityLabel={defaultAccessibilityLabel}
93
103
  {...rest}
94
104
  >
95
- <Icon
96
- name={iconName}
97
- size={24} // Internal icon size is fixed/clipped by container in design but Icon requires a size
98
- color={iconColor}
99
- style={{
100
- // Center the larger icon within the small container if needed,
101
- // though flex center on container handles this.
102
- // If the container is 6x10 and icon is 24, we might want to ensure it doesn't affect layout
103
- // but Flexbox 'center' centers the content.
104
- // However, if the icon component has its own frame, it might overflow.
105
- // React Native View has overflow: 'hidden' by default if borderRadius is set? No.
106
- // We might want overflow: 'hidden' if strictly following design clip.
107
- // Figma design had "overflow-clip" class.
108
- }}
109
- />
105
+ <Svg width={svgWidth} height={svgHeight} viewBox={`0 0 ${svgWidth} ${svgHeight}`}>
106
+ <Polyline
107
+ points={points}
108
+ stroke={iconColor}
109
+ strokeWidth={strokeWeight}
110
+ strokeLinecap="round"
111
+ strokeLinejoin="round"
112
+ fill="none"
113
+ />
114
+ </Svg>
110
115
  </View>
111
116
  )
112
117
  }
113
-
114
-
@@ -8,7 +8,7 @@ import {
8
8
  type TextStyle,
9
9
  } from 'react-native'
10
10
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
11
- import { IconDeletebackspace } from '../../icons/components/IconDeletebackspace'
11
+ import Icon from '../../icons/Icon'
12
12
 
13
13
  export type NumpadKeyValue =
14
14
  | '0'
@@ -142,10 +142,10 @@ function Numpad({
142
142
  accessibilityLabel={isBackspace ? 'Backspace' : key}
143
143
  >
144
144
  {isBackspace ? (
145
- <IconDeletebackspace
146
- width={fontSize as number}
147
- height={fontSize as number}
148
- fill={foreground as string}
145
+ <Icon
146
+ name="ic_delete_backspace"
147
+ size={fontSize as number}
148
+ color={foreground as string}
149
149
  />
150
150
  ) : (
151
151
  <Text style={[textStyle, keyTextStyle]}>{key}</Text>
@@ -3,7 +3,7 @@ import { View, Text, Pressable, Platform, type StyleProp, type ViewStyle, type P
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
- import { cloneChildrenWithModes } from '../../utils/react-utils'
6
+ import { cloneChildrenWithModes, flattenChildren } from '../../utils/react-utils'
7
7
 
8
8
  type SectionProps = {
9
9
  title?: string;
@@ -69,7 +69,8 @@ function Section({
69
69
  const headerPressedStyle: ViewStyle = isHeaderPressed ? { opacity: 0.85 } : {}
70
70
  // Resolve section container tokens
71
71
  const backgroundColor = getVariableByName('section/background/color', modes) || '#ffffff'
72
- const gap = getVariableByName('slot/gap', modes) || 12
72
+ const sectionGap = getVariableByName('section/gap', modes) || 12
73
+ const slotGap = getVariableByName('slot/gap', modes) || 12
73
74
  const paddingHorizontal = getVariableByName('section/padding/horizontal', modes) || 12
74
75
  const paddingVertical = getVariableByName('section/padding/vertical', modes) || 16
75
76
  const radius = getVariableByName('section/radius', modes) || 12
@@ -106,7 +107,7 @@ function Section({
106
107
  paddingHorizontal,
107
108
  paddingVertical,
108
109
  borderRadius: radius,
109
- gap,
110
+ gap: sectionGap,
110
111
  }
111
112
 
112
113
  const headerStyle = {
@@ -233,8 +234,8 @@ function Section({
233
234
  </View>
234
235
  )}
235
236
  {slot && (
236
- <View style={{ flexDirection: slotDirection, gap }}>
237
- {cloneChildrenWithModes(React.Children.toArray(slot), modes)}
237
+ <View style={{ flexDirection: slotDirection, gap: slotGap }}>
238
+ {cloneChildrenWithModes(flattenChildren(slot), modes)}
238
239
  </View>
239
240
  )}
240
241
  </View>
@@ -302,13 +303,12 @@ function SectionBento({
302
303
 
303
304
 
304
305
 
305
- // Process slots to pass modes to children
306
306
  const processedNavSlot = navSlot
307
- ? cloneChildrenWithModes(React.Children.toArray(navSlot), modes)
307
+ ? cloneChildrenWithModes(flattenChildren(navSlot), modes)
308
308
  : null
309
309
 
310
310
  const processedUpiSlot = upiSlot
311
- ? cloneChildrenWithModes(React.Children.toArray(upiSlot), modes)
311
+ ? cloneChildrenWithModes(flattenChildren(upiSlot), modes)
312
312
  : null
313
313
 
314
314
  return (
@@ -2,50 +2,7 @@ import React, { useState } from 'react'
2
2
  import { Pressable, View, TextInput as RNTextInput, type StyleProp, type ViewStyle, type TextInputProps as RNTextInputProps, type TextStyle } from 'react-native'
3
3
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
4
4
  import Icon from '../../icons/Icon'
5
-
6
- /**
7
- * Helper function to recursively clone children and pass modes prop to components that accept it.
8
- * This ensures that all child components in slots receive the modes prop from the parent.
9
- */
10
- function cloneChildrenWithModes(
11
- children: React.ReactNode,
12
- modes: Record<string, any>
13
- ): React.ReactNode[] {
14
- const result = React.Children.map(children, (child) => {
15
- if (!React.isValidElement(child)) {
16
- return child
17
- }
18
-
19
- // Get existing children
20
- const childChildren = (child.props as any)?.children
21
- const hasChildren = childChildren !== undefined && childChildren !== null
22
-
23
- // Clone the child with modes prop if it doesn't already have one
24
- // or merge with existing modes if it does
25
- // Merge order: parent modes first, then child's explicit modes override them
26
- const existingModes = (child.props as any)?.modes
27
- const mergedModes = existingModes ? { ...modes, ...existingModes } : modes
28
-
29
- // Recursively process children if they exist
30
- const processedChildren: React.ReactNode | undefined = hasChildren
31
- ? cloneChildrenWithModes(
32
- React.Children.toArray(childChildren),
33
- modes
34
- )
35
- : undefined
36
-
37
- // Clone element with modes and processed children
38
- return React.cloneElement(
39
- child,
40
- {
41
- ...(child.props as any),
42
- modes: mergedModes,
43
- },
44
- processedChildren
45
- )
46
- })
47
- return result || []
48
- }
5
+ import { cloneChildrenWithModes } from '../../utils/react-utils'
49
6
 
50
7
  /**
51
8
  * TextInput component that mirrors the Figma "textInput" component.
@@ -0,0 +1,152 @@
1
+ import React from 'react'
2
+ import { View, Text, Pressable, type ViewStyle, type TextStyle, type PressableProps } from 'react-native'
3
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
4
+ import MoneyValue from '../MoneyValue/MoneyValue'
5
+ import TransactionStatus from '../TransactionStatus/TransactionStatus'
6
+ import NavArrow from '../NavArrow/NavArrow'
7
+ import { cloneChildrenWithModes } from '../../utils/react-utils'
8
+ import { usePressableWebSupport, type WebAccessibilityProps } from '../../utils/web-platform-utils'
9
+
10
+ export type TransactionBubbleProps = {
11
+ description?: string
12
+ value?: string | number
13
+ currency?: string
14
+ status?: string
15
+ date?: string
16
+ /** Slot for the status area. When provided, replaces the default TransactionStatus + NavArrow. */
17
+ statusSlot?: React.ReactNode
18
+ children?: React.ReactNode
19
+ modes?: Record<string, any>
20
+ onPress?: () => void
21
+ style?: ViewStyle
22
+ accessibilityLabel?: string
23
+ accessibilityHint?: string
24
+ webAccessibilityProps?: WebAccessibilityProps
25
+ } & Omit<PressableProps, 'style' | 'children' | 'onPress'>
26
+
27
+ /**
28
+ * TransactionBubble — Figma node 1517:1155.
29
+ *
30
+ * Layout (single horizontal row inside a rounded bordered pill):
31
+ *
32
+ * ┌──────────────────────────────────────────────┐
33
+ * │ Description Status · Date │
34
+ * │ ₹56 › │
35
+ * └──────────────────────────────────────────────┘
36
+ *
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.
40
+ */
41
+ function TransactionBubble({
42
+ description = 'Payment to Uber India',
43
+ value = '56',
44
+ currency = '₹',
45
+ status = 'Expired',
46
+ date = '20 Mar 2025',
47
+ statusSlot,
48
+ children,
49
+ modes = {},
50
+ onPress,
51
+ style,
52
+ accessibilityLabel,
53
+ accessibilityHint,
54
+ webAccessibilityProps,
55
+ ...rest
56
+ }: TransactionBubbleProps) {
57
+ const resolvedModes = {
58
+ ...modes,
59
+ 'Context3': 'Transaction Bubble',
60
+ }
61
+
62
+ const padding = Number(getVariableByName('transactionBubble/padding', resolvedModes)) || 16
63
+ const radius = Number(getVariableByName('transactionBubble/radius', resolvedModes)) || 23
64
+ const borderSize = Number(getVariableByName('transactionBubble/border/size', resolvedModes)) || 1
65
+ const backgroundColor = getVariableByName('transactionBubble/background', resolvedModes) || '#ffffff'
66
+ const borderColor = getVariableByName('transactionBubble/border/color', resolvedModes) || '#e5e5e5'
67
+ const bubbleGap = Number(getVariableByName('transactionBubble/gap', resolvedModes)) || 8
68
+ const wrapGap = Number(getVariableByName('transactionBubble/wrap/gap', resolvedModes)) || 8
69
+
70
+ const descriptionColor = getVariableByName('transactionBubble/description/color', resolvedModes) || '#24262b'
71
+ const descriptionFontSize = Number(getVariableByName('transactionBubble/description/fontSize', resolvedModes)) || 14
72
+ const descriptionLineHeight = Number(getVariableByName('transactionBubble/description/lineHeight', resolvedModes)) || 17
73
+ const descriptionFontFamily = getVariableByName('transactionBubble/description/fontFamily', resolvedModes) || 'JioType Var'
74
+
75
+ const statusWrapGap = Number(getVariableByName('transactionBubble/statusWrap/gap', resolvedModes)) || 4
76
+
77
+ const containerStyle: ViewStyle = {
78
+ padding,
79
+ borderRadius: radius,
80
+ borderWidth: borderSize,
81
+ borderColor,
82
+ backgroundColor,
83
+ }
84
+
85
+ const descriptionStyle: TextStyle = {
86
+ color: descriptionColor,
87
+ fontSize: descriptionFontSize,
88
+ lineHeight: descriptionLineHeight,
89
+ fontFamily: descriptionFontFamily,
90
+ }
91
+
92
+ const processedChildren = children
93
+ ? cloneChildrenWithModes(React.Children.toArray(children), resolvedModes)
94
+ : null
95
+
96
+ const processedStatusSlot = statusSlot
97
+ ? cloneChildrenWithModes(React.Children.toArray(statusSlot), resolvedModes)
98
+ : null
99
+
100
+ const defaultAccessibilityLabel = accessibilityLabel || `${description} • ${status}`
101
+
102
+ const webProps = usePressableWebSupport({
103
+ restProps: rest,
104
+ onPress,
105
+ disabled: false,
106
+ accessibilityLabel: defaultAccessibilityLabel,
107
+ webAccessibilityProps,
108
+ })
109
+
110
+ const rightColumn = processedStatusSlot || (
111
+ <View style={{ alignItems: 'flex-end', justifyContent: 'flex-end', gap: statusWrapGap }}>
112
+ <TransactionStatus status={status} date={date} modes={resolvedModes} />
113
+ <NavArrow direction="Forward" modes={resolvedModes} />
114
+ </View>
115
+ )
116
+
117
+ const mainContent = (
118
+ <View style={{ flexDirection: 'row', gap: wrapGap }}>
119
+ <View style={{ flex: 1, gap: bubbleGap }}>
120
+ <Text style={descriptionStyle} numberOfLines={1}>{description}</Text>
121
+ <MoneyValue value={value} currency={currency} modes={resolvedModes} />
122
+ </View>
123
+ {rightColumn}
124
+ </View>
125
+ )
126
+
127
+ if (onPress) {
128
+ return (
129
+ <Pressable
130
+ onPress={onPress}
131
+ style={({ pressed }) => [containerStyle, style, pressed && { opacity: 0.85 }]}
132
+ accessibilityRole="button"
133
+ accessibilityLabel={defaultAccessibilityLabel}
134
+ accessibilityHint={accessibilityHint}
135
+ {...webProps}
136
+ {...rest}
137
+ >
138
+ {mainContent}
139
+ {processedChildren}
140
+ </Pressable>
141
+ )
142
+ }
143
+
144
+ return (
145
+ <View style={[containerStyle, style]} accessibilityLabel={defaultAccessibilityLabel} accessibilityHint={accessibilityHint}>
146
+ {mainContent}
147
+ {processedChildren}
148
+ </View>
149
+ )
150
+ }
151
+
152
+ export default TransactionBubble
@@ -48,6 +48,7 @@ export { Tooltip } from './Tooltip/Tooltip';
48
48
 
49
49
  export { default as TransactionDetails } from './TransactionDetails/TransactionDetails';
50
50
  export { default as TransactionStatus } from './TransactionStatus/TransactionStatus';
51
+ export { default as TransactionBubble, type TransactionBubbleProps } from './TransactionBubble/TransactionBubble';
51
52
  export { default as UpiHandle } from './UpiHandle/UpiHandle';
52
53
  export { default as VStack, type VStackProps } from './VStack/VStack';
53
54
  export { default as ChipGroup, type ChipGroupProps } from './ChipGroup/ChipGroup';
@@ -1,6 +1,7 @@
1
1
 
2
- import React, { createContext, useContext, ReactNode, useMemo } from 'react';
2
+ import React, { createContext, useContext, ReactNode, useMemo, useState, useEffect } from 'react';
3
3
  import { getVariableByName } from './figma-variables-resolver';
4
+ import * as Font from 'expo-font';
4
5
 
5
6
  /**
6
7
  * Shape of the TokenContext
@@ -42,7 +43,6 @@ export const JFSThemeProvider: React.FC<JFSThemeProviderProps> = ({
42
43
  children
43
44
  }) => {
44
45
  const value = useMemo(() => {
45
- // We bind the current modes to getVariableByName so consumers don't have to pass it
46
46
  const getVariable = (name: string) => getVariableByName(name, modes);
47
47
 
48
48
  return {
@@ -77,3 +77,38 @@ export const useTokens = (): TokenContextType => {
77
77
  }
78
78
  return context;
79
79
  };
80
+
81
+ /**
82
+ * Returns the JFS font map. The TTF is encapsulated within the package at
83
+ * src/assets/fonts/JioType Var.ttf (included via package.json "files").
84
+ * Call this inside load functions to avoid top-level require errors if font missing.
85
+ */
86
+ export const getJFSFonts = () => ({
87
+ 'JioType Var': require('../assets/fonts/JioType Var.ttf'),
88
+ } as const);
89
+
90
+ /**
91
+ * Hook for loading JFS fonts using expo-font. This improves Android font support by explicitly registering
92
+ * the custom 'JioType Var' font (encapsulated in the package) via Font.loadAsync before components render.
93
+ * Without it, Android defaults to Roboto. Call at app root (e.g. before JFSThemeProvider). Returns loaded state.
94
+ * See getJFSFonts() for direct use with Font.loadAsync. Handles missing font gracefully for web/Storybook.
95
+ */
96
+ export function useJFSFonts(): boolean {
97
+ const [fontsLoaded, setFontsLoaded] = useState(false);
98
+
99
+ useEffect(() => {
100
+ async function loadJFSFonts() {
101
+ try {
102
+ await Font.loadAsync(getJFSFonts());
103
+ setFontsLoaded(true);
104
+ } catch (error) {
105
+ console.warn('Failed to load JFS fonts (this is common in web/Storybook or if font file missing):', error);
106
+ setFontsLoaded(true);
107
+ }
108
+ }
109
+
110
+ loadJFSFonts();
111
+ }, []);
112
+
113
+ return fontsLoaded;
114
+ }
@@ -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-13T20:33:05.821Z
7
+ * Generated: 2026-04-14T12:07:25.514Z
8
8
  */
9
9
 
10
10
  // Icon name to SVG data mapping
@@ -9,17 +9,27 @@ export function cloneChildrenWithModes(
9
9
  modes: Record<string, any>,
10
10
  forcedModes?: Record<string, any>
11
11
  ): React.ReactNode[] {
12
- return React.Children.map(children, (child) => {
12
+ const result: React.ReactNode[] = [];
13
+
14
+ React.Children.forEach(children, (child) => {
13
15
  if (!React.isValidElement(child)) {
14
- return child
16
+ if (child !== null && child !== undefined) {
17
+ result.push(child);
18
+ }
19
+ return;
20
+ }
21
+
22
+ // Unwrap Fragments: Fragments can't accept arbitrary props like `modes`,
23
+ // so recurse into their children and process each one individually.
24
+ if (child.type === React.Fragment) {
25
+ const fragment = child as React.ReactElement<{ children?: React.ReactNode }>;
26
+ result.push(...cloneChildrenWithModes(fragment.props.children, modes, forcedModes));
27
+ return;
15
28
  }
16
29
 
17
- // Get existing children
18
30
  const childChildren = (child.props as any)?.children
19
31
  const hasChildren = childChildren !== undefined && childChildren !== null
20
32
 
21
- // Clone the child with modes prop if it doesn't already have one
22
- // or merge with existing modes if it does
23
33
  // Merge order: parent modes first, then child's explicit modes override them,
24
34
  // then forcedModes (if provided) are applied last and can never be overridden
25
35
  const existingModes = (child.props as any)?.modes
@@ -29,25 +39,23 @@ export function cloneChildrenWithModes(
29
39
  ? { ...modes, ...existingModes }
30
40
  : modes
31
41
 
32
- // Recursively process children if they exist
33
42
  const processedChildren: React.ReactNode | undefined = hasChildren
34
- ? cloneChildrenWithModes(
35
- React.Children.toArray(childChildren),
36
- modes,
37
- forcedModes
38
- )
43
+ ? cloneChildrenWithModes(childChildren, modes, forcedModes)
39
44
  : undefined
40
45
 
41
- // Clone element with modes and processed children
42
- return React.cloneElement(
43
- child,
44
- {
45
- ...(child.props as any),
46
- modes: mergedModes,
47
- },
48
- processedChildren
49
- )
50
- })?.filter((child) => child !== null && child !== undefined) as React.ReactNode[] ?? []
46
+ result.push(
47
+ React.cloneElement(
48
+ child,
49
+ {
50
+ ...(child.props as any),
51
+ modes: mergedModes,
52
+ },
53
+ processedChildren
54
+ )
55
+ );
56
+ });
57
+
58
+ return result;
51
59
  }
52
60
 
53
61
  /**