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.
- package/CHANGELOG.md +8 -0
- package/lib/commonjs/components/CardCTA/CardCTA.js +28 -2
- package/lib/commonjs/components/Carousel/Carousel.js +22 -4
- package/lib/commonjs/components/Image/Image.js +78 -0
- package/lib/commonjs/components/MediaCard/MediaCard.js +40 -27
- package/lib/commonjs/components/Section/Section.js +22 -7
- package/lib/commonjs/components/index.js +7 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/CardCTA/CardCTA.js +28 -2
- package/lib/module/components/Carousel/Carousel.js +22 -4
- package/lib/module/components/Image/Image.js +73 -0
- package/lib/module/components/MediaCard/MediaCard.js +39 -29
- package/lib/module/components/Section/Section.js +23 -8
- package/lib/module/components/index.js +1 -0
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/Image/Image.d.ts +60 -0
- package/lib/typescript/src/components/MediaCard/MediaCard.d.ts +22 -4
- package/lib/typescript/src/components/index.d.ts +2 -1
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/CardCTA/CardCTA.tsx +25 -1
- package/src/components/Carousel/Carousel.tsx +21 -3
- package/src/components/Image/Image.tsx +125 -0
- package/src/components/MediaCard/MediaCard.tsx +110 -82
- package/src/components/Section/Section.tsx +29 -12
- package/src/components/index.ts +2 -1
- package/src/icons/registry.ts +1 -1
|
@@ -1,81 +1,99 @@
|
|
|
1
|
-
import React, { createContext, useContext
|
|
2
|
-
import { View, Text, StyleSheet, type ViewStyle, type TextStyle, type StyleProp,
|
|
3
|
-
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
4
|
-
import
|
|
5
|
-
import
|
|
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
|
-
*
|
|
16
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
const
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
{
|
|
70
|
-
|
|
71
|
-
|
|
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,
|
|
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
|
-
|
|
132
|
-
const
|
|
133
|
-
const
|
|
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
|
-
|
|
205
|
-
MediaCard.
|
|
206
|
-
MediaCard.
|
|
207
|
-
MediaCard.
|
|
208
|
-
MediaCard.
|
|
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
|
-
|
|
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` + `
|
|
101
|
-
* inner content
|
|
102
|
-
* inside always render at their natural size
|
|
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)
|
|
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 =
|
|
217
|
-
|
|
218
|
-
|
|
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.
|
|
265
|
-
.
|
|
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 (
|
package/src/components/index.ts
CHANGED
|
@@ -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';
|
package/src/icons/registry.ts
CHANGED