jfs-components 0.0.65 → 0.0.67

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.
@@ -1,81 +1,99 @@
1
- import React, { createContext, useContext, isValidElement, cloneElement } from 'react';
2
- import { View, Text, StyleSheet, type ViewStyle, type TextStyle, type StyleProp, Platform, Image } from 'react-native';
3
- import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
4
- import Button, { type ButtonProps } from '../Button/Button';
5
- import Avatar, { type AvatarProps } from '../Avatar/Avatar';
6
- import { EMPTY_MODES } from '../../utils/react-utils';
1
+ import React, { createContext, useContext } from 'react'
2
+ import { View, Text, StyleSheet, type ViewStyle, type TextStyle, type StyleProp, type ImageSourcePropType, Platform } from 'react-native'
3
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
4
+ import Image from '../Image/Image'
5
+ import { EMPTY_MODES } from '../../utils/react-utils'
7
6
 
8
- /**
9
- * Context to share 'modes' with child components.
10
- */
11
- const MediaCardContext = createContext<{ modes?: Record<string, any> }>({});
7
+ const MediaCardContext = createContext<{ modes?: Record<string, any> }>({})
12
8
 
13
9
  export interface MediaCardProps {
14
10
  /**
15
- * The background media content (e.g. an Image).
16
- * It will be positioned absolutely to cover the card.
11
+ * Image source for the background media. Same shape as the rest of the
12
+ * library (`Avatar`, `ProductLabel`, etc.) accepts a URL string or any
13
+ * RN `ImageSourcePropType`. The card renders this through the shared
14
+ * `<Image>` component, so all image-rendering details (normalization,
15
+ * resize behaviour, `aspectRatio`) live there, not here.
16
+ */
17
+ imageSource?: ImageSourcePropType | string | undefined
18
+ /**
19
+ * Width-to-height aspect ratio for the background image when using
20
+ * `imageSource`, e.g. `16 / 9`. Forwarded to `<Image ratio>`.
17
21
  */
18
- media?: React.ReactNode;
22
+ ratio?: number | undefined
23
+ /**
24
+ * Escape hatch: a fully custom background node (e.g. a gradient view, a
25
+ * video). Takes precedence over `imageSource`. Prefer `imageSource` for
26
+ * the common case of a single background image.
27
+ */
28
+ media?: React.ReactNode
19
29
  /**
20
30
  * Content to render inside the card (e.g. MediaCard.Title, MediaCard.Footer).
21
31
  */
22
- children?: React.ReactNode;
32
+ children?: React.ReactNode
23
33
  /**
24
34
  * Modes object for token resolution.
25
35
  */
26
- modes?: Record<string, any>;
36
+ modes?: Record<string, any>
27
37
  /**
28
38
  * Style overrides for the card container.
29
39
  */
30
- style?: StyleProp<ViewStyle>;
40
+ style?: StyleProp<ViewStyle>
31
41
  }
32
42
 
33
43
  /**
34
44
  * MediaCard component implementation from Figma node 1241:4140.
35
- *
45
+ *
36
46
  * Features a background media slot, a large title, and a glass-morphism footer.
47
+ *
48
+ * The background can be supplied either as `imageSource` (preferred — uses
49
+ * the shared `<Image>` primitive under the hood) or as a custom `media` node
50
+ * for non-image backgrounds.
37
51
  */
38
52
  export function MediaCard({
53
+ imageSource,
54
+ ratio,
39
55
  media,
40
56
  children,
41
57
  modes = EMPTY_MODES,
42
58
  style,
43
59
  }: MediaCardProps) {
44
- // Container Tokens
45
- const radius = parseFloat(getVariableByName('cardMedia/radius', modes) || '24');
46
- const gap = parseFloat(getVariableByName('cardMedia/gap', modes) || '0');
47
- // Dimensions from Figma: w=369, h=308. We can make it flexible or default to these?
48
- // Usually components should be flexible, but stories will constrain them.
49
- // Figma context shows fixed/hug behavior. Let's start with flex container.
60
+ const radius = parseFloat(getVariableByName('cardMedia/radius', modes) || '24')
61
+ const gap = parseFloat(getVariableByName('cardMedia/gap', modes) || '0')
50
62
 
51
63
  const containerStyle: ViewStyle = {
52
64
  borderRadius: radius,
53
65
  gap,
54
66
  overflow: 'hidden',
55
67
  position: 'relative',
56
- // Default dimensions from Figma if needed, but better to let parent control or use defaults in stories.
57
- // However, to match "Maximize existing component usage", we follow patterns.
58
- // We'll trust the parent layout or style prop for width/height.
59
- minHeight: 308, // inferred from Figma height as a good default or minimum
60
- };
61
-
62
- const mediaWithModes = isValidElement(media)
63
- ? cloneElement(media as any, { modes: { ...(media.props as any).modes, ...modes } })
64
- : media;
68
+ minHeight: 308,
69
+ }
70
+
71
+ // `media` wins for back-compat / custom nodes; otherwise we delegate to
72
+ // the shared <Image> for image-source backgrounds. All raster-rendering
73
+ // concerns (URL-vs-{uri}, resizeMode, aspect-ratio) live in <Image>.
74
+ const background = media ?? (
75
+ imageSource != null ? (
76
+ <Image
77
+ imageSource={imageSource}
78
+ ratio={ratio}
79
+ resizeMode="cover"
80
+ accessibilityElementsHidden
81
+ importantForAccessibility="no"
82
+ />
83
+ ) : null
84
+ )
65
85
 
66
86
  return (
67
87
  <MediaCardContext.Provider value={{ modes }}>
68
88
  <View style={[containerStyle, style]}>
69
- {/* Background Media Layer */}
70
- <View style={StyleSheet.absoluteFill}>
71
- {mediaWithModes}
72
- </View>
89
+ {background ? (
90
+ <View style={StyleSheet.absoluteFill}>{background}</View>
91
+ ) : null}
73
92
 
74
- {/* Content Layer */}
75
93
  {children}
76
94
  </View>
77
95
  </MediaCardContext.Provider>
78
- );
96
+ )
79
97
  }
80
98
 
81
99
  // ----------------------------------------------------------------------------
@@ -88,11 +106,23 @@ export function MediaCard({
88
106
  * Figma: "title wrap" p-[16px]
89
107
  */
90
108
  export function Header({ children, style }: { children?: React.ReactNode; style?: StyleProp<ViewStyle> }) {
109
+ // NOTE: the previous `flex: 1` shorthand expanded on Yoga (Android) to
110
+ // `{ flexGrow: 1, flexShrink: 1, flexBasis: 0 }`. With `flexBasis: 0` the
111
+ // Header has *no intrinsic floor*, so when MediaCard is placed inside a
112
+ // height-unbounded parent — e.g. a Carousel slot whose contentContainer
113
+ // is `alignItems: 'flex-start'` — Yoga's first measurement pass sizes
114
+ // the Header at 0 and the card's overall height becomes non-deterministic.
115
+ // On native this manifests as the card "over-stretching" vertically (the
116
+ // same Yoga foot-gun we fixed in `CardCTA` rightWrap). Web hides it
117
+ // because browsers honor `min-height: auto` on flex items. Use explicit
118
+ // `flexGrow / flexShrink: 0 / flexBasis: 'auto'` so the Header is sized
119
+ // to its content as a floor and only grows to consume the extra space
120
+ // contributed by `MediaCard`'s `minHeight: 308`.
91
121
  return (
92
- <View style={[{ padding: 16, flex: 1 }, style]}>
122
+ <View style={[{ padding: 16, flexGrow: 1, flexShrink: 0, flexBasis: 'auto' }, style]}>
93
123
  {children}
94
124
  </View>
95
- );
125
+ )
96
126
  }
97
127
 
98
128
  /**
@@ -100,14 +130,14 @@ export function Header({ children, style }: { children?: React.ReactNode; style?
100
130
  * Tokens: cardMedia/title/*
101
131
  */
102
132
  export function Title({ children, style, modes: propModes }: { children?: React.ReactNode; style?: StyleProp<TextStyle>; modes?: Record<string, any> }) {
103
- const context = useContext(MediaCardContext);
104
- const modes = propModes || context.modes || {};
133
+ const context = useContext(MediaCardContext)
134
+ const modes = propModes || context.modes || {}
105
135
 
106
- const color = getVariableByName('cardMedia/title/color', modes) || '#ffffff';
107
- const fontSize = parseFloat(getVariableByName('cardMedia/title/fontSize', modes) || '52');
108
- const fontFamily = getVariableByName('cardMedia/title/fontFamily', modes) || 'JioType Var';
109
- const lineHeight = parseFloat(getVariableByName('cardMedia/title/lineHeight', modes) || '68');
110
- const fontWeight = getVariableByName('cardMedia/title/fontWeight', modes) || '900';
136
+ const color = getVariableByName('cardMedia/title/color', modes) || '#ffffff'
137
+ const fontSize = parseFloat(getVariableByName('cardMedia/title/fontSize', modes) || '52')
138
+ const fontFamily = getVariableByName('cardMedia/title/fontFamily', modes) || 'JioType Var'
139
+ const lineHeight = parseFloat(getVariableByName('cardMedia/title/lineHeight', modes) || '68')
140
+ const fontWeight = getVariableByName('cardMedia/title/fontWeight', modes) || '900'
111
141
 
112
142
  const textStyle: TextStyle = {
113
143
  color,
@@ -115,9 +145,9 @@ export function Title({ children, style, modes: propModes }: { children?: React.
115
145
  fontFamily,
116
146
  lineHeight,
117
147
  fontWeight: fontWeight as TextStyle['fontWeight'],
118
- };
148
+ }
119
149
 
120
- return <Text style={[textStyle, style]}>{children}</Text>;
150
+ return <Text style={[textStyle, style]}>{children}</Text>
121
151
  }
122
152
 
123
153
  /**
@@ -125,20 +155,19 @@ export function Title({ children, style, modes: propModes }: { children?: React.
125
155
  * Tokens: cardMedia/footer/*, glass/minimal, blur/minimal
126
156
  */
127
157
  export function Footer({ children, style, modes: propModes }: { children?: React.ReactNode; style?: StyleProp<ViewStyle>; modes?: Record<string, any> }) {
128
- const context = useContext(MediaCardContext);
129
- const modes = propModes || context.modes || {};
158
+ const context = useContext(MediaCardContext)
159
+ const modes = propModes || context.modes || {}
130
160
 
131
- // Tokens
132
- const gap = parseFloat(getVariableByName('cardMedia/footer/gap', modes) || '24');
133
- const paddingHorizontal = parseFloat(getVariableByName('cardMedia/footer/padding/horizontal', modes) || '16');
134
- const paddingVertical = parseFloat(getVariableByName('cardMedia/footer/padding/vertical', modes) || '12');
161
+ const gap = parseFloat(getVariableByName('cardMedia/footer/gap', modes) || '24')
162
+ const paddingHorizontal = parseFloat(getVariableByName('cardMedia/footer/padding/horizontal', modes) || '16')
163
+ const paddingVertical = parseFloat(getVariableByName('cardMedia/footer/padding/vertical', modes) || '12')
135
164
 
136
165
  // Glass Effect
137
166
  // Figma:
138
167
  // blur/minimal/background: "#1414174a"
139
168
  // blur/minimal: 29
140
- const glassBgColor = getVariableByName('blur/minimal/background', modes) || '#1414174a';
141
- const blurRadius = parseFloat(getVariableByName('blur/minimal', modes) || '29');
169
+ const glassBgColor = getVariableByName('blur/minimal/background', modes) || '#1414174a'
170
+ const blurRadius = parseFloat(getVariableByName('blur/minimal', modes) || '29')
142
171
 
143
172
  const containerStyle: ViewStyle = {
144
173
  flexDirection: 'row',
@@ -150,13 +179,13 @@ export function Footer({ children, style, modes: propModes }: { children?: React
150
179
  // Web-specific backdrop filter for glass effect
151
180
  // @ts-ignore
152
181
  ...(Platform.OS === 'web' ? { backdropFilter: `blur(${blurRadius}px)` } : {}),
153
- };
182
+ }
154
183
 
155
184
  return (
156
185
  <View style={[containerStyle, style]}>
157
186
  {children}
158
187
  </View>
159
- );
188
+ )
160
189
  }
161
190
 
162
191
  /**
@@ -164,20 +193,20 @@ export function Footer({ children, style, modes: propModes }: { children?: React
164
193
  * Tokens: cardMedia/footer/title/*
165
194
  */
166
195
  export function FooterTitle({ children, style, modes: propModes }: { children?: React.ReactNode; style?: StyleProp<TextStyle>; modes?: Record<string, any> }) {
167
- const context = useContext(MediaCardContext);
168
- const modes = propModes || context.modes || {};
196
+ const context = useContext(MediaCardContext)
197
+ const modes = propModes || context.modes || {}
169
198
 
170
- const color = getVariableByName('cardMedia/footer/title/color', modes) || '#ffffff';
171
- const fontSize = parseFloat(getVariableByName('cardMedia/footer/title/fontSize', modes) || '14');
172
- const fontFamily = getVariableByName('cardMedia/footer/title/fontFamily', modes) || 'JioType Var';
173
- const lineHeight = parseFloat(getVariableByName('cardMedia/footer/title/lineHeight', modes) || '16');
174
- const fontWeight = getVariableByName('cardMedia/footer/title/fontWeight', modes) || '800';
199
+ const color = getVariableByName('cardMedia/footer/title/color', modes) || '#ffffff'
200
+ const fontSize = parseFloat(getVariableByName('cardMedia/footer/title/fontSize', modes) || '14')
201
+ const fontFamily = getVariableByName('cardMedia/footer/title/fontFamily', modes) || 'JioType Var'
202
+ const lineHeight = parseFloat(getVariableByName('cardMedia/footer/title/lineHeight', modes) || '16')
203
+ const fontWeight = getVariableByName('cardMedia/footer/title/fontWeight', modes) || '800'
175
204
 
176
205
  return (
177
206
  <Text style={[{ color, fontSize, fontFamily, lineHeight, fontWeight: fontWeight as any }, style]}>
178
207
  {children}
179
208
  </Text>
180
- );
209
+ )
181
210
  }
182
211
 
183
212
  /**
@@ -185,27 +214,26 @@ export function FooterTitle({ children, style, modes: propModes }: { children?:
185
214
  * Tokens: cardMedia/footer/subtitle/*
186
215
  */
187
216
  export function FooterSubtitle({ children, style, modes: propModes }: { children?: React.ReactNode; style?: StyleProp<TextStyle>; modes?: Record<string, any> }) {
188
- const context = useContext(MediaCardContext);
189
- const modes = propModes || context.modes || {};
217
+ const context = useContext(MediaCardContext)
218
+ const modes = propModes || context.modes || {}
190
219
 
191
- const color = getVariableByName('cardMedia/footer/subtitle/color', modes) || '#f5f7f7a1';
192
- const fontSize = parseFloat(getVariableByName('cardMedia/footer/subtitle/fontSize', modes) || '12');
193
- const fontFamily = getVariableByName('cardMedia/footer/subtitle/fontFamily', modes) || 'JioType Var';
194
- const lineHeight = parseFloat(getVariableByName('cardMedia/footer/subtitle/lineHeight', modes) || '14');
195
- const fontWeight = getVariableByName('cardMedia/footer/subtitle/fontWeight', modes) || '400';
220
+ const color = getVariableByName('cardMedia/footer/subtitle/color', modes) || '#f5f7f7a1'
221
+ const fontSize = parseFloat(getVariableByName('cardMedia/footer/subtitle/fontSize', modes) || '12')
222
+ const fontFamily = getVariableByName('cardMedia/footer/subtitle/fontFamily', modes) || 'JioType Var'
223
+ const lineHeight = parseFloat(getVariableByName('cardMedia/footer/subtitle/lineHeight', modes) || '14')
224
+ const fontWeight = getVariableByName('cardMedia/footer/subtitle/fontWeight', modes) || '400'
196
225
 
197
226
  return (
198
227
  <Text style={[{ color, fontSize, fontFamily, lineHeight, fontWeight: fontWeight as any }, style]}>
199
228
  {children}
200
229
  </Text>
201
- );
230
+ )
202
231
  }
203
232
 
204
- // Attach sub-components
205
- MediaCard.Header = Header;
206
- MediaCard.Title = Title;
207
- MediaCard.Footer = Footer;
208
- MediaCard.FooterTitle = FooterTitle;
209
- MediaCard.FooterSubtitle = FooterSubtitle;
233
+ MediaCard.Header = Header
234
+ MediaCard.Title = Title
235
+ MediaCard.Footer = Footer
236
+ MediaCard.FooterTitle = FooterTitle
237
+ MediaCard.FooterSubtitle = FooterSubtitle
210
238
 
211
- export default MediaCard;
239
+ export default MediaCard
@@ -1,12 +1,13 @@
1
1
  import React, { useState, useMemo, useRef, useCallback } from 'react'
2
2
  import { View, Text, Pressable, Platform, type StyleProp, type ViewStyle, type PressableStateCallbackType } from 'react-native'
3
3
  import Animated, {
4
+ Easing,
4
5
  FadeInUp,
5
6
  FadeOutUp,
6
7
  ReduceMotion,
7
8
  useAnimatedStyle,
8
9
  useSharedValue,
9
- withSpring,
10
+ withTiming,
10
11
  } from 'react-native-reanimated'
11
12
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
12
13
  import NavArrow from '../NavArrow/NavArrow'
@@ -82,7 +83,14 @@ const SLOT_GRID_MAX_COLUMNS = 4
82
83
  const SLOT_GRID_STAGGER_CAP = 8
83
84
  const SLOT_GRID_ENTER_STAGGER_MS = 35
84
85
  const SLOT_GRID_EXIT_STAGGER_MS = 20
86
+ const SLOT_GRID_ENTER_DURATION_MS = 220
85
87
  const SLOT_GRID_EXIT_DURATION_MS = 160
88
+ const SLOT_GRID_HEIGHT_DURATION_MS = 280
89
+
90
+ // Standard ease-out cubic curve. Calm, professional, no overshoot — matches
91
+ // system-style transitions. Defined once at module scope so it isn't
92
+ // re-allocated per render.
93
+ const SLOT_GRID_EASING = Easing.out(Easing.cubic)
86
94
 
87
95
  type SlotGridProps = {
88
96
  items: React.ReactNode[];
@@ -97,10 +105,10 @@ type SlotGridProps = {
97
105
  animateExtrasFromIndex?: number;
98
106
  /**
99
107
  * If true, the rows container animates its height via an explicit
100
- * `useSharedValue` + `withSpring` driven by `onLayout` measurements of the
101
- * inner content (with `overflow: 'hidden'` to clip mid-animation). Cells
102
- * inside always render at their natural size — they are *never* resized
103
- * during the transition. Default false.
108
+ * `useSharedValue` + `withTiming` (ease-out cubic, no overshoot) driven by
109
+ * `onLayout` measurements of the inner content, with `overflow: 'hidden'`
110
+ * to clip mid-animation. Cells inside always render at their natural size
111
+ * — they are *never* resized during the transition. Default false.
104
112
  */
105
113
  animateContainerLayout?: boolean;
106
114
  }
@@ -157,6 +165,13 @@ const SlotGrid = React.memo(function SlotGrid({
157
165
  }
158
166
 
159
167
  const containerStyle = useMemo<ViewStyle>(() => ({ gap }), [gap])
168
+ // Strict `width` (not `minWidth`) so every cell in every row is exactly the
169
+ // same size — `space-between` then distributes identical leftover into
170
+ // identical inter-cell gaps on every row, which keeps column N of row 1
171
+ // aligned with column N of rows 2/3/etc. Cells whose label is wider than
172
+ // `cellWidth` simply wrap their text onto more lines (taking more vertical
173
+ // space; the row's height grows naturally to fit the tallest cell, and the
174
+ // animated-height clip springs to the new total).
160
175
  const cellStyle = useMemo<ViewStyle | undefined>(
161
176
  () => (cellWidth !== null ? { width: cellWidth } : undefined),
162
177
  [cellWidth]
@@ -197,8 +212,9 @@ const SlotGrid = React.memo(function SlotGrid({
197
212
  // and an explicit `height` driven by a shared value.
198
213
  // 3. The inner view reports its natural height via `onLayout`. The first
199
214
  // measurement snaps the shared value (no first-mount animation). Every
200
- // subsequent change (e.g. expand/collapse adds or removes rows) springs
201
- // the shared value to the new natural height.
215
+ // subsequent change (e.g. expand/collapse adds or removes rows) eases
216
+ // the shared value to the new natural height with a calm ease-out
217
+ // timing curve — no spring, no bounce, no overshoot.
202
218
  //
203
219
  // Visually: the container reveals/conceals content like a curtain, and the
204
220
  // cells never deform.
@@ -213,9 +229,9 @@ const SlotGrid = React.memo(function SlotGrid({
213
229
  animatedHeight.value = h
214
230
  return
215
231
  }
216
- animatedHeight.value = withSpring(h, {
217
- damping: 22,
218
- stiffness: 180,
232
+ animatedHeight.value = withTiming(h, {
233
+ duration: SLOT_GRID_HEIGHT_DURATION_MS,
234
+ easing: SLOT_GRID_EASING,
219
235
  reduceMotion: ReduceMotion.System,
220
236
  })
221
237
  },
@@ -261,11 +277,12 @@ const SlotGrid = React.memo(function SlotGrid({
261
277
  reverseOrdinal,
262
278
  SLOT_GRID_STAGGER_CAP
263
279
  )
264
- const entering = FadeInUp.springify()
265
- .damping(18)
280
+ const entering = FadeInUp.duration(SLOT_GRID_ENTER_DURATION_MS)
281
+ .easing(SLOT_GRID_EASING)
266
282
  .delay(enterStaggerSteps * SLOT_GRID_ENTER_STAGGER_MS)
267
283
  .reduceMotion(ReduceMotion.System)
268
284
  const exiting = FadeOutUp.duration(SLOT_GRID_EXIT_DURATION_MS)
285
+ .easing(SLOT_GRID_EASING)
269
286
  .delay(exitStaggerSteps * SLOT_GRID_EXIT_STAGGER_MS)
270
287
  .reduceMotion(ReduceMotion.System)
271
288
  return (
@@ -24,11 +24,12 @@ export { default as HoldingsCard, type HoldingsCardProps } from './HoldingsCard/
24
24
  export { default as HStack, type HStackProps } from './HStack/HStack';
25
25
  export { default as IconButton } from './IconButton/IconButton';
26
26
  export { default as IconCapsule } from './IconCapsule/IconCapsule';
27
+ export { default as Image, type ImageProps } from './Image/Image';
27
28
  export { default as LazyList } from './LazyList/LazyList';
28
29
  export { default as LinearMeter, type LinearMeterProps } from './LinearMeter/LinearMeter';
29
30
  export { default as ListGroup } from './ListGroup/ListGroup';
30
31
  export { default as ListItem } from './ListItem/ListItem';
31
- export { default as MediaCard } from './MediaCard/MediaCard';
32
+ export { default as MediaCard, type MediaCardProps } from './MediaCard/MediaCard';
32
33
  export { default as MerchantProfile, type MerchantProfileProps } from './MerchantProfile/MerchantProfile';
33
34
  export { default as MoneyValue } from './MoneyValue/MoneyValue';
34
35
  export { default as NoteInput, type NoteInputProps } from './NoteInput/NoteInput';
@@ -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-21T13:27:57.213Z
7
+ * Generated: 2026-04-22T11:47:15.563Z
8
8
  */
9
9
 
10
10
  // Icon name to SVG data mapping