jfs-components 0.1.30 → 0.1.32
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 +13 -0
- package/lib/commonjs/components/CategoryCard/CategoryCard.js +264 -0
- package/lib/commonjs/components/CompareTable/CompareTable.js +140 -84
- package/lib/commonjs/components/ContentSheet/ContentSheet.js +28 -5
- package/lib/commonjs/components/CounterBadge/CounterBadge.js +78 -0
- package/lib/commonjs/components/FavoriteToggle/FavoriteToggle.js +180 -0
- package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +287 -0
- package/lib/commonjs/components/ValueBackMetric/ValueBackMetric.js +219 -0
- package/lib/commonjs/components/index.js +35 -0
- package/lib/commonjs/design-tokens/figma-modes.generated.js +3 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/CategoryCard/CategoryCard.js +258 -0
- package/lib/module/components/CompareTable/CompareTable.js +140 -84
- package/lib/module/components/ContentSheet/ContentSheet.js +29 -6
- package/lib/module/components/CounterBadge/CounterBadge.js +73 -0
- package/lib/module/components/FavoriteToggle/FavoriteToggle.js +174 -0
- package/lib/module/components/HelloJioInput/HelloJioInput.js +281 -0
- package/lib/module/components/ValueBackMetric/ValueBackMetric.js +214 -0
- package/lib/module/components/index.js +6 -1
- package/lib/module/design-tokens/figma-modes.generated.js +3 -0
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/CategoryCard/CategoryCard.d.ts +72 -0
- package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +16 -1
- package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +7 -1
- package/lib/typescript/src/components/CounterBadge/CounterBadge.d.ts +19 -0
- package/lib/typescript/src/components/FavoriteToggle/FavoriteToggle.d.ts +66 -0
- package/lib/typescript/src/components/HelloJioInput/HelloJioInput.d.ts +111 -0
- package/lib/typescript/src/components/ValueBackMetric/ValueBackMetric.d.ts +86 -0
- package/lib/typescript/src/components/index.d.ts +5 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/CategoryCard/CategoryCard.tsx +404 -0
- package/src/components/CompareTable/CompareTable.tsx +201 -101
- package/src/components/ContentSheet/ContentSheet.tsx +40 -11
- package/src/components/CounterBadge/CounterBadge.tsx +95 -0
- package/src/components/FavoriteToggle/FavoriteToggle.tsx +243 -0
- package/src/components/HelloJioInput/HelloJioInput.tsx +513 -0
- package/src/components/ValueBackMetric/ValueBackMetric.tsx +298 -0
- package/src/components/index.ts +5 -0
- package/src/design-tokens/figma-modes.generated.ts +3 -0
- package/src/icons/registry.ts +1 -1
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React, { useCallback, useMemo } from 'react';
|
|
4
|
+
import { Pressable, Text, View } from 'react-native';
|
|
5
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider';
|
|
7
|
+
import { EMPTY_MODES, cloneChildrenWithModes, resolveTextLayout } from '../../utils/react-utils';
|
|
8
|
+
import { usePressableWebSupport } from '../../utils/web-platform-utils';
|
|
9
|
+
import Badge from '../Badge/Badge';
|
|
10
|
+
import Image from '../Image/Image';
|
|
11
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
12
|
+
const CATEGORY_CARD_STATE = 'Category Card State';
|
|
13
|
+
const BADGE_OVERLAP = 11;
|
|
14
|
+
const DEFAULT_CARD_WIDTH = 60;
|
|
15
|
+
|
|
16
|
+
// Default modes from the Figma CategoryCard component. Overridable via `modes`.
|
|
17
|
+
const CATEGORY_CARD_DEFAULT_MODES = Object.freeze({
|
|
18
|
+
AppearanceBrand: 'Tertiary',
|
|
19
|
+
'Badge Size': 'Small',
|
|
20
|
+
Context4: 'Badge',
|
|
21
|
+
"Radius": "None"
|
|
22
|
+
});
|
|
23
|
+
const toNumber = (value, fallback) => {
|
|
24
|
+
if (typeof value === 'number') {
|
|
25
|
+
return Number.isFinite(value) ? value : fallback;
|
|
26
|
+
}
|
|
27
|
+
if (typeof value === 'string') {
|
|
28
|
+
const parsed = Number(value);
|
|
29
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
30
|
+
}
|
|
31
|
+
return fallback;
|
|
32
|
+
};
|
|
33
|
+
const toFontWeight = (value, fallback) => {
|
|
34
|
+
if (typeof value === 'number') return String(value);
|
|
35
|
+
if (typeof value === 'string') return value;
|
|
36
|
+
return fallback;
|
|
37
|
+
};
|
|
38
|
+
function resolveCategoryCardTokens(modes, active) {
|
|
39
|
+
const idleModes = {
|
|
40
|
+
...modes,
|
|
41
|
+
[CATEGORY_CARD_STATE]: 'Idle'
|
|
42
|
+
};
|
|
43
|
+
const activeModes = {
|
|
44
|
+
...modes,
|
|
45
|
+
[CATEGORY_CARD_STATE]: 'Active'
|
|
46
|
+
};
|
|
47
|
+
const resolvedModes = active ? activeModes : idleModes;
|
|
48
|
+
const gap = toNumber(getVariableByName('cardTab/gap', resolvedModes), 6);
|
|
49
|
+
const paddingHorizontal = toNumber(getVariableByName('cardTab/padding/horizontal', resolvedModes), 8);
|
|
50
|
+
const paddingVertical = toNumber(getVariableByName('cardTab/padding/vertical', resolvedModes), 8);
|
|
51
|
+
const radius = toNumber(getVariableByName('cardTab/radius', resolvedModes), 8);
|
|
52
|
+
const strokeWidth = toNumber(getVariableByName('cardTab/strokeWidth', resolvedModes), 1);
|
|
53
|
+
const strokeColor = getVariableByName('cardTab/stroke/color', resolvedModes) ?? '#f5f5f5';
|
|
54
|
+
const idleBackground = getVariableByName('cardTab/background/color', idleModes) ?? '#ffffff';
|
|
55
|
+
const activeBackground = getVariableByName('cardTab/background/color', activeModes) ?? '#f5f5f5';
|
|
56
|
+
const backgroundColor = active ? activeBackground : idleBackground;
|
|
57
|
+
const imageSize = toNumber(getVariableByName('cardTab/image/width', resolvedModes), 36);
|
|
58
|
+
const imageRadius = toNumber(getVariableByName('image/radius', resolvedModes), 0);
|
|
59
|
+
const fontFamily = getVariableByName('cardTab/label/fontFamily', resolvedModes) ?? 'JioType Var';
|
|
60
|
+
const fontSize = toNumber(getVariableByName('cardTab/label/fontSize', resolvedModes), 12);
|
|
61
|
+
const lineHeight = toNumber(getVariableByName('cardTab/label/lineHeight', resolvedModes), Math.round(fontSize * 1.3));
|
|
62
|
+
const fontWeight = toFontWeight(getVariableByName('cardTab/label/fontWeight', resolvedModes), '500');
|
|
63
|
+
const labelColor = getVariableByName('cardTab/label/color', resolvedModes) ?? '#000000';
|
|
64
|
+
return {
|
|
65
|
+
containerStyle: {
|
|
66
|
+
position: 'relative',
|
|
67
|
+
alignItems: 'center',
|
|
68
|
+
justifyContent: 'center',
|
|
69
|
+
backgroundColor,
|
|
70
|
+
borderColor: strokeColor,
|
|
71
|
+
borderWidth: strokeWidth,
|
|
72
|
+
borderStyle: 'solid',
|
|
73
|
+
borderRadius: radius,
|
|
74
|
+
paddingHorizontal,
|
|
75
|
+
paddingTop: paddingVertical,
|
|
76
|
+
paddingBottom: paddingVertical,
|
|
77
|
+
gap
|
|
78
|
+
},
|
|
79
|
+
labelStyle: {
|
|
80
|
+
color: labelColor,
|
|
81
|
+
fontFamily,
|
|
82
|
+
fontSize,
|
|
83
|
+
fontWeight,
|
|
84
|
+
lineHeight,
|
|
85
|
+
textAlign: 'center'
|
|
86
|
+
},
|
|
87
|
+
imageSize,
|
|
88
|
+
imageRadius
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const pressedOverlayStyle = {
|
|
92
|
+
opacity: 0.85
|
|
93
|
+
};
|
|
94
|
+
const disabledOverlayStyle = {
|
|
95
|
+
opacity: 0.5
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* `CategoryCard` is a compact, tappable tile for category pickers and
|
|
100
|
+
* merchandising rails. It stacks a square image above a short label and can
|
|
101
|
+
* optionally show an overlapping badge at the top edge. Cards are typically
|
|
102
|
+
* laid out in a horizontal `ScrollView` or `HStack`.
|
|
103
|
+
*
|
|
104
|
+
* All visual values resolve through the `cardTab/*` design tokens with
|
|
105
|
+
* sensible Figma defaults so the card renders correctly out of the box.
|
|
106
|
+
*
|
|
107
|
+
* @component
|
|
108
|
+
* @param {CategoryCardProps} props
|
|
109
|
+
*/
|
|
110
|
+
function CategoryCard({
|
|
111
|
+
label = 'Popular',
|
|
112
|
+
imageSource,
|
|
113
|
+
imageSlot,
|
|
114
|
+
badge,
|
|
115
|
+
active = false,
|
|
116
|
+
width = DEFAULT_CARD_WIDTH,
|
|
117
|
+
disableTruncation,
|
|
118
|
+
singleLine,
|
|
119
|
+
onPress,
|
|
120
|
+
disabled = false,
|
|
121
|
+
modes: propModes = EMPTY_MODES,
|
|
122
|
+
style,
|
|
123
|
+
labelStyle,
|
|
124
|
+
accessibilityLabel,
|
|
125
|
+
accessibilityHint,
|
|
126
|
+
accessibilityState,
|
|
127
|
+
webAccessibilityProps,
|
|
128
|
+
testID,
|
|
129
|
+
...rest
|
|
130
|
+
}) {
|
|
131
|
+
const {
|
|
132
|
+
modes: globalModes
|
|
133
|
+
} = useTokens();
|
|
134
|
+
const modes = useMemo(() => ({
|
|
135
|
+
...globalModes,
|
|
136
|
+
...CATEGORY_CARD_DEFAULT_MODES,
|
|
137
|
+
...propModes
|
|
138
|
+
}), [globalModes, propModes]);
|
|
139
|
+
const resolvedModes = useMemo(() => ({
|
|
140
|
+
...modes,
|
|
141
|
+
[CATEGORY_CARD_STATE]: active ? 'Active' : 'Idle'
|
|
142
|
+
}), [modes, active]);
|
|
143
|
+
const tokens = useMemo(() => resolveCategoryCardTokens(resolvedModes, active), [resolvedModes, active]);
|
|
144
|
+
const {
|
|
145
|
+
style: labelLayoutStyle,
|
|
146
|
+
...labelTruncation
|
|
147
|
+
} = useMemo(() => resolveTextLayout({
|
|
148
|
+
disableTruncation,
|
|
149
|
+
singleLine,
|
|
150
|
+
numberOfLines: 1,
|
|
151
|
+
ellipsizeMode: 'tail'
|
|
152
|
+
}), [disableTruncation, singleLine]);
|
|
153
|
+
const processedImageSlot = useMemo(() => {
|
|
154
|
+
if (!imageSlot) return null;
|
|
155
|
+
const processed = cloneChildrenWithModes(imageSlot, resolvedModes);
|
|
156
|
+
return processed.length === 1 ? processed[0] : processed;
|
|
157
|
+
}, [imageSlot, resolvedModes]);
|
|
158
|
+
const imageNode = processedImageSlot ?? (imageSource !== undefined ? /*#__PURE__*/_jsx(Image, {
|
|
159
|
+
imageSource: imageSource,
|
|
160
|
+
width: tokens.imageSize,
|
|
161
|
+
height: tokens.imageSize,
|
|
162
|
+
ratio: 1,
|
|
163
|
+
resizeMode: "cover",
|
|
164
|
+
borderRadius: tokens.imageRadius,
|
|
165
|
+
accessibilityElementsHidden: true,
|
|
166
|
+
importantForAccessibility: "no"
|
|
167
|
+
}) : /*#__PURE__*/_jsx(Image, {
|
|
168
|
+
imageSource: "https://www.figma.com/api/mcp/asset/0ee34871-646f-4736-be06-22fb6c0a4eed",
|
|
169
|
+
width: tokens.imageSize,
|
|
170
|
+
height: tokens.imageSize,
|
|
171
|
+
ratio: 1,
|
|
172
|
+
resizeMode: "cover",
|
|
173
|
+
borderRadius: tokens.imageRadius,
|
|
174
|
+
accessibilityElementsHidden: true,
|
|
175
|
+
importantForAccessibility: "no"
|
|
176
|
+
}));
|
|
177
|
+
const a11yLabel = accessibilityLabel ?? (badge ? `${label}, ${badge}` : label);
|
|
178
|
+
const webProps = usePressableWebSupport({
|
|
179
|
+
restProps: rest,
|
|
180
|
+
onPress: disabled ? undefined : onPress,
|
|
181
|
+
disabled,
|
|
182
|
+
accessibilityLabel: a11yLabel,
|
|
183
|
+
webAccessibilityProps
|
|
184
|
+
});
|
|
185
|
+
const badgeNode = badge ? /*#__PURE__*/_jsx(View, {
|
|
186
|
+
style: BADGE_ANCHOR,
|
|
187
|
+
pointerEvents: "none",
|
|
188
|
+
children: /*#__PURE__*/_jsx(Badge, {
|
|
189
|
+
label: badge,
|
|
190
|
+
modes: resolvedModes,
|
|
191
|
+
style: BADGE_CENTER_STYLE
|
|
192
|
+
})
|
|
193
|
+
}) : null;
|
|
194
|
+
|
|
195
|
+
// Truncation needs a width bound so the ellipsis engages. `singleLine`
|
|
196
|
+
// must NOT fill the content box — otherwise the text is pinned to the left
|
|
197
|
+
// padding edge and only grows rightward. Intrinsic width + centered
|
|
198
|
+
// alignment lets overflow expand equally on both sides.
|
|
199
|
+
const labelWidthStyle = singleLine ? LABEL_OVERFLOW_CENTER_STYLE : LABEL_FILL_STYLE;
|
|
200
|
+
const pressableStyle = useCallback(({
|
|
201
|
+
pressed
|
|
202
|
+
}) => [tokens.containerStyle, {
|
|
203
|
+
width
|
|
204
|
+
}, badge ? {
|
|
205
|
+
marginTop: BADGE_OVERLAP
|
|
206
|
+
} : null, disabled ? disabledOverlayStyle : null, pressed && !disabled ? pressedOverlayStyle : null, style], [tokens.containerStyle, width, badge, disabled, style]);
|
|
207
|
+
return /*#__PURE__*/_jsxs(Pressable, {
|
|
208
|
+
accessibilityRole: "button",
|
|
209
|
+
accessibilityLabel: a11yLabel,
|
|
210
|
+
accessibilityHint: accessibilityHint,
|
|
211
|
+
accessibilityState: {
|
|
212
|
+
disabled,
|
|
213
|
+
selected: active,
|
|
214
|
+
...accessibilityState
|
|
215
|
+
},
|
|
216
|
+
onPress: onPress,
|
|
217
|
+
disabled: disabled,
|
|
218
|
+
style: pressableStyle,
|
|
219
|
+
testID: testID,
|
|
220
|
+
...webProps,
|
|
221
|
+
children: [badgeNode, imageNode, /*#__PURE__*/_jsx(Text, {
|
|
222
|
+
style: [tokens.labelStyle, labelWidthStyle, labelLayoutStyle, labelStyle],
|
|
223
|
+
...labelTruncation,
|
|
224
|
+
accessibilityElementsHidden: true,
|
|
225
|
+
importantForAccessibility: "no",
|
|
226
|
+
children: label
|
|
227
|
+
})]
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
const BADGE_ANCHOR = {
|
|
231
|
+
position: 'absolute',
|
|
232
|
+
top: -BADGE_OVERLAP,
|
|
233
|
+
left: 0,
|
|
234
|
+
right: 0,
|
|
235
|
+
alignItems: 'center',
|
|
236
|
+
justifyContent: 'center',
|
|
237
|
+
zIndex: 1
|
|
238
|
+
};
|
|
239
|
+
const BADGE_CENTER_STYLE = {
|
|
240
|
+
alignSelf: 'center'
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/** Lets the label fill the card width so the one-line ellipsis has a bound. */
|
|
244
|
+
const LABEL_FILL_STYLE = {
|
|
245
|
+
width: '100%',
|
|
246
|
+
alignSelf: 'stretch'
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Intrinsic width, horizontally centred. When the label is wider than the
|
|
251
|
+
* card it overflows left and right equally instead of growing only rightward
|
|
252
|
+
* from the padded content edge.
|
|
253
|
+
*/
|
|
254
|
+
const LABEL_OVERFLOW_CENTER_STYLE = {
|
|
255
|
+
alignSelf: 'center',
|
|
256
|
+
textAlign: 'center'
|
|
257
|
+
};
|
|
258
|
+
export default /*#__PURE__*/React.memo(CategoryCard);
|
|
@@ -107,7 +107,9 @@ function CompareTable({
|
|
|
107
107
|
modes = EMPTY_MODES,
|
|
108
108
|
style,
|
|
109
109
|
disableTruncation,
|
|
110
|
-
columnWidth
|
|
110
|
+
columnWidth,
|
|
111
|
+
stickyHeaders = true,
|
|
112
|
+
stickyHeaderLabel = true
|
|
111
113
|
}) {
|
|
112
114
|
// --- selection card tokens ------------------------------------------------
|
|
113
115
|
const cardBg = getVariableByName('selectionCard/background/color', modes) ?? '#ffffff';
|
|
@@ -179,12 +181,12 @@ function CompareTable({
|
|
|
179
181
|
// Stable per-section ref-callback instances so React doesn't detach/reattach
|
|
180
182
|
// on every render (which would churn the map and re-fire scrollTo).
|
|
181
183
|
const bodyRefCallbacks = useRef(new Map());
|
|
182
|
-
/**
|
|
183
|
-
*
|
|
184
|
+
/** Tracks which ScrollView the user is currently dragging. Only the
|
|
185
|
+
* actively-dragged ScrollView drives horizontal sync — programmatic
|
|
186
|
+
* scrollTo echoes from synced peers are ignored, eliminating jitter. */
|
|
187
|
+
const draggingSource = useRef(null);
|
|
184
188
|
const lastSyncX = useRef(0);
|
|
185
|
-
const
|
|
186
|
-
if (Math.abs(x - lastSyncX.current) < 0.5) return;
|
|
187
|
-
lastSyncX.current = x;
|
|
189
|
+
const syncOthers = useCallback((source, x) => {
|
|
188
190
|
if (source !== 'cards' && cardsScrollRef.current) {
|
|
189
191
|
cardsScrollRef.current.scrollTo({
|
|
190
192
|
x,
|
|
@@ -200,12 +202,37 @@ function CompareTable({
|
|
|
200
202
|
}
|
|
201
203
|
});
|
|
202
204
|
}, []);
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
}, [
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
205
|
+
const onScrollBeginDrag = useCallback(source => () => {
|
|
206
|
+
draggingSource.current = source;
|
|
207
|
+
}, []);
|
|
208
|
+
const onScrollEndDrag = useCallback(source => e => {
|
|
209
|
+
const x = Math.round(e.nativeEvent.contentOffset.x);
|
|
210
|
+
if (x === lastSyncX.current) return;
|
|
211
|
+
lastSyncX.current = x;
|
|
212
|
+
syncOthers(source, x);
|
|
213
|
+
}, [syncOthers]);
|
|
214
|
+
const onMomentumScrollEnd = useCallback(source => e => {
|
|
215
|
+
if (draggingSource.current === source) {
|
|
216
|
+
draggingSource.current = null;
|
|
217
|
+
}
|
|
218
|
+
const x = Math.round(e.nativeEvent.contentOffset.x);
|
|
219
|
+
if (x === lastSyncX.current) return;
|
|
220
|
+
lastSyncX.current = x;
|
|
221
|
+
syncOthers(source, x);
|
|
222
|
+
}, [syncOthers]);
|
|
223
|
+
|
|
224
|
+
/** Real-time scroll sync: only the actively-dragged ScrollView drives
|
|
225
|
+
* sync on native — echoed onScroll events from programmatic scrollTo
|
|
226
|
+
* on other ScrollViews are ignored because draggingSource won't match.
|
|
227
|
+
* On web, onScrollBeginDrag never fires for mouse/trackpad, so skip
|
|
228
|
+
* the guard and rely on the lastSyncX rounded-value check instead. */
|
|
229
|
+
const onScroll = useCallback(source => e => {
|
|
230
|
+
if (Platform.OS !== 'web' && draggingSource.current !== source) return;
|
|
231
|
+
const x = Math.round(e.nativeEvent.contentOffset.x);
|
|
232
|
+
if (x === lastSyncX.current) return;
|
|
233
|
+
lastSyncX.current = x;
|
|
234
|
+
syncOthers(source, x);
|
|
235
|
+
}, [syncOthers]);
|
|
209
236
|
const getBodyRefCallback = useCallback(idx => {
|
|
210
237
|
let cb = bodyRefCallbacks.current.get(idx);
|
|
211
238
|
if (!cb) {
|
|
@@ -436,55 +463,45 @@ function CompareTable({
|
|
|
436
463
|
})
|
|
437
464
|
})]
|
|
438
465
|
});
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
466
|
+
const renderHeaderContent = section => section.header != null ? /*#__PURE__*/_jsx(View, {
|
|
467
|
+
style: {
|
|
468
|
+
backgroundColor: headerBg,
|
|
469
|
+
paddingHorizontal: headerPaddingH,
|
|
470
|
+
paddingVertical: headerPaddingV,
|
|
471
|
+
borderBottomWidth: cellBorderSize,
|
|
472
|
+
borderBottomColor: cellBorderColor
|
|
473
|
+
},
|
|
474
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
475
|
+
style: headerTextStyle,
|
|
476
|
+
children: section.header
|
|
477
|
+
})
|
|
478
|
+
}) : null;
|
|
479
|
+
const renderBodyContent = (section, index) => /*#__PURE__*/_jsx(ScrollView, {
|
|
480
|
+
ref: getBodyRefCallback(index),
|
|
481
|
+
horizontal: true,
|
|
482
|
+
showsHorizontalScrollIndicator: false,
|
|
483
|
+
onScroll: onScroll(index),
|
|
484
|
+
onScrollBeginDrag: onScrollBeginDrag(index),
|
|
485
|
+
onScrollEndDrag: onScrollEndDrag(index),
|
|
486
|
+
onMomentumScrollEnd: onMomentumScrollEnd(index),
|
|
487
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
451
488
|
style: {
|
|
452
|
-
width:
|
|
453
|
-
backgroundColor: headerBg,
|
|
454
|
-
paddingHorizontal: headerPaddingH,
|
|
455
|
-
paddingVertical: headerPaddingV,
|
|
456
|
-
borderBottomWidth: cellBorderSize,
|
|
457
|
-
borderBottomColor: cellBorderColor
|
|
489
|
+
width: contentWidth
|
|
458
490
|
},
|
|
459
|
-
children:
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
style: {
|
|
471
|
-
width: contentWidth
|
|
472
|
-
},
|
|
473
|
-
children: section.rows.map((row, rowIndex) => {
|
|
474
|
-
const isLastRow = rowIndex === section.rows.length - 1;
|
|
475
|
-
return /*#__PURE__*/_jsxs(View, {
|
|
476
|
-
style: {
|
|
477
|
-
flexDirection: 'row',
|
|
478
|
-
width: '100%'
|
|
479
|
-
},
|
|
480
|
-
children: [Array.from({
|
|
481
|
-
length: columnCount
|
|
482
|
-
}).map((_, colIndex) => renderTableCell(renderCellContent(row.values?.[colIndex], `${rowIndex}-${colIndex}`), colIndex, isLastRow, colIndex)), showAddCard && renderTableCell(null, gridColumnCount - 1, isLastRow, '__add_spacer__')]
|
|
483
|
-
}, row.key ?? rowIndex);
|
|
484
|
-
})
|
|
491
|
+
children: section.rows.map((row, rowIndex) => {
|
|
492
|
+
const isLastRow = rowIndex === section.rows.length - 1;
|
|
493
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
494
|
+
style: {
|
|
495
|
+
flexDirection: 'row',
|
|
496
|
+
width: '100%'
|
|
497
|
+
},
|
|
498
|
+
children: [Array.from({
|
|
499
|
+
length: columnCount
|
|
500
|
+
}).map((_, colIndex) => renderTableCell(renderCellContent(row.values?.[colIndex], `${rowIndex}-${colIndex}`), colIndex, isLastRow, colIndex)), showAddCard && renderTableCell(null, gridColumnCount - 1, isLastRow, '__add_spacer__')]
|
|
501
|
+
}, row.key ?? rowIndex);
|
|
485
502
|
})
|
|
486
|
-
})
|
|
487
|
-
}
|
|
503
|
+
})
|
|
504
|
+
});
|
|
488
505
|
const outerStyle = {
|
|
489
506
|
width: '100%',
|
|
490
507
|
backgroundColor: cardBg,
|
|
@@ -518,43 +535,82 @@ function CompareTable({
|
|
|
518
535
|
}
|
|
519
536
|
|
|
520
537
|
// --- fixed-width scroll mode: chained horizontal sync + sticky cards -----
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
|
|
525
|
-
|
|
538
|
+
// Build children and stickyHeaderIndices for the vertical ScrollView.
|
|
539
|
+
// When stickyHeaders is on, section headers are injected as direct children
|
|
540
|
+
// so React Native's stickyHeaderIndices can pin them with stacking.
|
|
541
|
+
const scrollChildren = [];
|
|
542
|
+
const stickyIndices = [];
|
|
543
|
+
if (showCardsRow) {
|
|
544
|
+
stickyIndices.push(scrollChildren.length);
|
|
545
|
+
scrollChildren.push(/*#__PURE__*/_jsx(ScrollView, {
|
|
546
|
+
ref: cardsScrollRef,
|
|
547
|
+
horizontal: true,
|
|
548
|
+
showsHorizontalScrollIndicator: false,
|
|
549
|
+
scrollEventThrottle: 16,
|
|
550
|
+
onScroll: onScroll('cards'),
|
|
551
|
+
onScrollBeginDrag: onScrollBeginDrag('cards'),
|
|
552
|
+
onScrollEndDrag: onScrollEndDrag('cards'),
|
|
553
|
+
onMomentumScrollEnd: onMomentumScrollEnd('cards'),
|
|
554
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
555
|
+
style: {
|
|
556
|
+
flexDirection: 'row',
|
|
557
|
+
width: contentWidth
|
|
558
|
+
},
|
|
559
|
+
children: [columns.map(renderCard), showAddCard && renderAddCard()]
|
|
560
|
+
})
|
|
561
|
+
}, "cards-row"));
|
|
562
|
+
}
|
|
563
|
+
sections.forEach((section, index) => {
|
|
564
|
+
const sectionKey = section.key ?? section.title ?? index;
|
|
565
|
+
const headerContent = renderHeaderContent(section);
|
|
566
|
+
if (stickyHeaders && headerContent != null) {
|
|
567
|
+
stickyIndices.push(scrollChildren.length);
|
|
568
|
+
scrollChildren.push(/*#__PURE__*/_jsx(View, {
|
|
569
|
+
style: {
|
|
570
|
+
width: '100%'
|
|
571
|
+
},
|
|
572
|
+
children: stickyHeaderLabel ? headerContent : /*#__PURE__*/_jsx(ScrollView, {
|
|
573
|
+
horizontal: true,
|
|
574
|
+
showsHorizontalScrollIndicator: false,
|
|
575
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
576
|
+
style: {
|
|
577
|
+
width: contentWidth
|
|
578
|
+
},
|
|
579
|
+
children: headerContent
|
|
580
|
+
})
|
|
581
|
+
})
|
|
582
|
+
}, `sticky-header-${sectionKey}`));
|
|
583
|
+
}
|
|
584
|
+
scrollChildren.push(/*#__PURE__*/_jsxs(Accordion, {
|
|
585
|
+
title: section.title,
|
|
586
|
+
defaultExpanded: section.defaultExpanded ?? index === 0,
|
|
587
|
+
modes: modes,
|
|
588
|
+
children: [!stickyHeaders && headerContent != null && (stickyHeaderLabel ? headerContent : /*#__PURE__*/_jsx(ScrollView, {
|
|
589
|
+
horizontal: true,
|
|
590
|
+
showsHorizontalScrollIndicator: false,
|
|
591
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
592
|
+
style: {
|
|
593
|
+
width: contentWidth
|
|
594
|
+
},
|
|
595
|
+
children: headerContent
|
|
596
|
+
})
|
|
597
|
+
})), renderBodyContent(section, index)]
|
|
598
|
+
}, sectionKey));
|
|
599
|
+
});
|
|
526
600
|
return /*#__PURE__*/_jsx(View, {
|
|
527
601
|
style: [outerStyle, style, {
|
|
528
602
|
flexDirection: 'column',
|
|
529
603
|
flex: 1
|
|
530
604
|
}],
|
|
531
|
-
children: /*#__PURE__*/
|
|
605
|
+
children: /*#__PURE__*/_jsx(ScrollView, {
|
|
532
606
|
style: {
|
|
533
607
|
width: '100%',
|
|
534
608
|
flex: 1
|
|
535
609
|
},
|
|
536
|
-
stickyHeaderIndices:
|
|
610
|
+
stickyHeaderIndices: stickyIndices,
|
|
537
611
|
showsVerticalScrollIndicator: false,
|
|
538
612
|
directionalLockEnabled: true,
|
|
539
|
-
children:
|
|
540
|
-
ref: cardsScrollRef,
|
|
541
|
-
horizontal: true,
|
|
542
|
-
showsHorizontalScrollIndicator: false,
|
|
543
|
-
scrollEventThrottle: 16,
|
|
544
|
-
onScroll: onCardsScroll,
|
|
545
|
-
children: /*#__PURE__*/_jsxs(View, {
|
|
546
|
-
style: {
|
|
547
|
-
flexDirection: 'row',
|
|
548
|
-
width: contentWidth
|
|
549
|
-
},
|
|
550
|
-
children: [columns.map(renderCard), showAddCard && renderAddCard()]
|
|
551
|
-
})
|
|
552
|
-
}), /*#__PURE__*/_jsx(View, {
|
|
553
|
-
style: {
|
|
554
|
-
width: '100%'
|
|
555
|
-
},
|
|
556
|
-
children: sections.map(renderSection)
|
|
557
|
-
})]
|
|
613
|
+
children: scrollChildren
|
|
558
614
|
})
|
|
559
615
|
});
|
|
560
616
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import React, { useContext, useEffect, useMemo } from 'react';
|
|
4
|
-
import { Platform, Text, View, useWindowDimensions } from 'react-native';
|
|
4
|
+
import { Platform, ScrollView, Text, View, useWindowDimensions } from 'react-native';
|
|
5
5
|
import Animated, { useAnimatedKeyboard, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
|
|
6
6
|
import { SafeAreaInsetsContext } from 'react-native-safe-area-context';
|
|
7
7
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
@@ -104,6 +104,7 @@ function ContentSheet({
|
|
|
104
104
|
safeAreaBottom = true,
|
|
105
105
|
pinToBottom = true,
|
|
106
106
|
visible = true,
|
|
107
|
+
maxHeightPercent = 0.7,
|
|
107
108
|
style,
|
|
108
109
|
...rest
|
|
109
110
|
}) {
|
|
@@ -115,9 +116,6 @@ function ContentSheet({
|
|
|
115
116
|
const resolved = useMemo(() => resolveContentSheetStyle(modes), [modes]);
|
|
116
117
|
const processedChildren = useMemo(() => cloneChildrenWithModes(flattenChildren(children), modes), [children, modes]);
|
|
117
118
|
const paddingBottom = resolved.paddingBottom + (safeAreaBottom ? bottomInset : 0);
|
|
118
|
-
const containerStyle = useMemo(() => [pinToBottom ? pinnedStyle : null, resolved.container, {
|
|
119
|
-
paddingBottom
|
|
120
|
-
}, style], [pinToBottom, resolved.container, paddingBottom, style]);
|
|
121
119
|
|
|
122
120
|
// Entrance / exit "pop up from bottom" animation — mirrors the Drawer. The
|
|
123
121
|
// sheet is bottom-anchored, so a positive `translateY` equal to the window
|
|
@@ -127,6 +125,11 @@ function ContentSheet({
|
|
|
127
125
|
const {
|
|
128
126
|
height: windowHeight
|
|
129
127
|
} = useWindowDimensions();
|
|
128
|
+
const maxHeight = windowHeight * maxHeightPercent;
|
|
129
|
+
const containerStyle = useMemo(() => [pinToBottom ? pinnedStyle : null, resolved.container, {
|
|
130
|
+
maxHeight,
|
|
131
|
+
paddingBottom
|
|
132
|
+
}, style], [pinToBottom, resolved.container, maxHeight, paddingBottom, style]);
|
|
130
133
|
const entrance = useSharedValue(windowHeight);
|
|
131
134
|
useEffect(() => {
|
|
132
135
|
entrance.value = withSpring(visible ? 0 : windowHeight, SPRING_CONFIG);
|
|
@@ -150,6 +153,7 @@ function ContentSheet({
|
|
|
150
153
|
style: containerStyle,
|
|
151
154
|
spacing: keyboardSpacing,
|
|
152
155
|
entrance: entrance,
|
|
156
|
+
maxHeight: maxHeight,
|
|
153
157
|
...rest,
|
|
154
158
|
children: [header, processedChildren]
|
|
155
159
|
});
|
|
@@ -157,6 +161,7 @@ function ContentSheet({
|
|
|
157
161
|
return /*#__PURE__*/_jsxs(AnimatedSheet, {
|
|
158
162
|
style: containerStyle,
|
|
159
163
|
entrance: entrance,
|
|
164
|
+
maxHeight: maxHeight,
|
|
160
165
|
...rest,
|
|
161
166
|
children: [header, processedChildren]
|
|
162
167
|
});
|
|
@@ -173,6 +178,7 @@ function KeyboardAwareSheet({
|
|
|
173
178
|
style,
|
|
174
179
|
spacing,
|
|
175
180
|
entrance,
|
|
181
|
+
maxHeight,
|
|
176
182
|
children,
|
|
177
183
|
...rest
|
|
178
184
|
}) {
|
|
@@ -189,7 +195,15 @@ function KeyboardAwareSheet({
|
|
|
189
195
|
return /*#__PURE__*/_jsx(Animated.View, {
|
|
190
196
|
style: [style, animatedStyle],
|
|
191
197
|
...rest,
|
|
192
|
-
children:
|
|
198
|
+
children: /*#__PURE__*/_jsx(ScrollView, {
|
|
199
|
+
style: {
|
|
200
|
+
maxHeight
|
|
201
|
+
},
|
|
202
|
+
bounces: false,
|
|
203
|
+
showsVerticalScrollIndicator: false,
|
|
204
|
+
keyboardShouldPersistTaps: "handled",
|
|
205
|
+
children: children
|
|
206
|
+
})
|
|
193
207
|
});
|
|
194
208
|
}
|
|
195
209
|
|
|
@@ -201,6 +215,7 @@ function KeyboardAwareSheet({
|
|
|
201
215
|
function AnimatedSheet({
|
|
202
216
|
style,
|
|
203
217
|
entrance,
|
|
218
|
+
maxHeight,
|
|
204
219
|
children,
|
|
205
220
|
...rest
|
|
206
221
|
}) {
|
|
@@ -214,7 +229,15 @@ function AnimatedSheet({
|
|
|
214
229
|
return /*#__PURE__*/_jsx(Animated.View, {
|
|
215
230
|
style: [style, animatedStyle],
|
|
216
231
|
...rest,
|
|
217
|
-
children:
|
|
232
|
+
children: /*#__PURE__*/_jsx(ScrollView, {
|
|
233
|
+
style: {
|
|
234
|
+
maxHeight
|
|
235
|
+
},
|
|
236
|
+
bounces: false,
|
|
237
|
+
showsVerticalScrollIndicator: false,
|
|
238
|
+
keyboardShouldPersistTaps: "handled",
|
|
239
|
+
children: children
|
|
240
|
+
})
|
|
218
241
|
});
|
|
219
242
|
}
|
|
220
243
|
export default ContentSheet;
|