jfs-components 0.1.28 → 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 +28 -2
- package/lib/commonjs/components/CategoryCard/CategoryCard.js +264 -0
- package/lib/commonjs/components/CompareTable/CompareTable.js +247 -25
- package/lib/commonjs/components/ContentSheet/ContentSheet.js +91 -8
- 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/MoneyValue/MoneyValue.js +179 -54
- package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +7 -1
- package/lib/commonjs/components/Table/Table.js +27 -7
- 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 +247 -26
- package/lib/module/components/ContentSheet/ContentSheet.js +94 -11
- 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/MoneyValue/MoneyValue.js +182 -57
- package/lib/module/components/PdpCcCard/PdpCcCard.js +7 -1
- package/lib/module/components/Table/Table.js +27 -7
- 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 +36 -1
- package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +20 -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/MoneyValue/MoneyValue.d.ts +10 -1
- package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +25 -1
- package/lib/typescript/src/components/Table/Table.d.ts +9 -1
- 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 +324 -30
- package/src/components/ContentSheet/ContentSheet.tsx +120 -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/MoneyValue/MoneyValue.tsx +216 -65
- package/src/components/PdpCcCard/PdpCcCard.tsx +36 -1
- package/src/components/Table/Table.tsx +29 -4
- 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);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import { View, Text, Pressable, Platform } from 'react-native';
|
|
3
|
+
import React, { useCallback, useRef } from 'react';
|
|
4
|
+
import { View, Text, Pressable, Platform, ScrollView } from 'react-native';
|
|
5
5
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
6
|
import { EMPTY_MODES, cloneChildrenWithModes, resolveTruncation } from '../../utils/react-utils';
|
|
7
7
|
import Accordion from '../Accordion/Accordion';
|
|
@@ -106,7 +106,10 @@ function CompareTable({
|
|
|
106
106
|
maxColumns = 4,
|
|
107
107
|
modes = EMPTY_MODES,
|
|
108
108
|
style,
|
|
109
|
-
disableTruncation
|
|
109
|
+
disableTruncation,
|
|
110
|
+
columnWidth,
|
|
111
|
+
stickyHeaders = true,
|
|
112
|
+
stickyHeaderLabel = true
|
|
110
113
|
}) {
|
|
111
114
|
// --- selection card tokens ------------------------------------------------
|
|
112
115
|
const cardBg = getVariableByName('selectionCard/background/color', modes) ?? '#ffffff';
|
|
@@ -163,6 +166,95 @@ function CompareTable({
|
|
|
163
166
|
const showAddCard = onAddColumn != null && columnCount < maxColumns;
|
|
164
167
|
/** Grid columns shared by the selection-card row and every table row. */
|
|
165
168
|
const gridColumnCount = columnCount + (showAddCard ? 1 : 0);
|
|
169
|
+
const showCardsRow = columnCount > 0 || showAddCard;
|
|
170
|
+
|
|
171
|
+
// --- fixed-width scroll mode -------------------------------------------
|
|
172
|
+
// `columnWidth` opts the component into scroll mode (see prop doc). In that
|
|
173
|
+
// mode the cards row and each section's table body are independent
|
|
174
|
+
// horizontal ScrollViews; their scroll events are chained so they move in
|
|
175
|
+
// lockstep, and the cards row pins to the top on the vertical axis.
|
|
176
|
+
const useFixedWidth = columnWidth != null;
|
|
177
|
+
/** Total scrollable content width shared by every horizontal surface. */
|
|
178
|
+
const contentWidth = useFixedWidth ? gridColumnCount * columnWidth : 0;
|
|
179
|
+
const cardsScrollRef = useRef(null);
|
|
180
|
+
const bodyScrollRefs = useRef(new Map());
|
|
181
|
+
// Stable per-section ref-callback instances so React doesn't detach/reattach
|
|
182
|
+
// on every render (which would churn the map and re-fire scrollTo).
|
|
183
|
+
const bodyRefCallbacks = useRef(new Map());
|
|
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);
|
|
188
|
+
const lastSyncX = useRef(0);
|
|
189
|
+
const syncOthers = useCallback((source, x) => {
|
|
190
|
+
if (source !== 'cards' && cardsScrollRef.current) {
|
|
191
|
+
cardsScrollRef.current.scrollTo({
|
|
192
|
+
x,
|
|
193
|
+
animated: false
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
bodyScrollRefs.current.forEach((ref, idx) => {
|
|
197
|
+
if (source === 'cards' || idx !== source) {
|
|
198
|
+
ref.scrollTo({
|
|
199
|
+
x,
|
|
200
|
+
animated: false
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}, []);
|
|
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]);
|
|
236
|
+
const getBodyRefCallback = useCallback(idx => {
|
|
237
|
+
let cb = bodyRefCallbacks.current.get(idx);
|
|
238
|
+
if (!cb) {
|
|
239
|
+
cb = ref => {
|
|
240
|
+
if (ref) {
|
|
241
|
+
bodyScrollRefs.current.set(idx, ref);
|
|
242
|
+
// Bring a freshly-expanded section's body into sync with
|
|
243
|
+
// the rest of the surfaces.
|
|
244
|
+
if (lastSyncX.current > 0) {
|
|
245
|
+
ref.scrollTo({
|
|
246
|
+
x: lastSyncX.current,
|
|
247
|
+
animated: false
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
bodyScrollRefs.current.delete(idx);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
bodyRefCallbacks.current.set(idx, cb);
|
|
255
|
+
}
|
|
256
|
+
return cb;
|
|
257
|
+
}, []);
|
|
166
258
|
const cardLabelStyle = {
|
|
167
259
|
...NO_WRAP_TEXT,
|
|
168
260
|
color: cardLabelColor,
|
|
@@ -193,8 +285,12 @@ function CompareTable({
|
|
|
193
285
|
fontWeight: toWeight(cellFontWeight)
|
|
194
286
|
};
|
|
195
287
|
const cardStyle = {
|
|
196
|
-
|
|
197
|
-
|
|
288
|
+
...(useFixedWidth ? {
|
|
289
|
+
width: columnWidth
|
|
290
|
+
} : {
|
|
291
|
+
flex: 1,
|
|
292
|
+
minWidth: 0
|
|
293
|
+
}),
|
|
198
294
|
minHeight: 147,
|
|
199
295
|
backgroundColor: cardBg,
|
|
200
296
|
borderRadius: cardRadius,
|
|
@@ -313,6 +409,7 @@ function CompareTable({
|
|
|
313
409
|
return /*#__PURE__*/_jsx(Table.Cell, {
|
|
314
410
|
modes: modes,
|
|
315
411
|
flex: 1,
|
|
412
|
+
width: useFixedWidth ? columnWidth : undefined,
|
|
316
413
|
align: "left",
|
|
317
414
|
isLastColumn: isLastCol,
|
|
318
415
|
isLastRow: isLastRow,
|
|
@@ -366,31 +463,155 @@ function CompareTable({
|
|
|
366
463
|
})
|
|
367
464
|
})]
|
|
368
465
|
});
|
|
369
|
-
|
|
370
|
-
style:
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
},
|
|
377
|
-
children:
|
|
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, {
|
|
378
488
|
style: {
|
|
379
|
-
|
|
380
|
-
width: '100%'
|
|
489
|
+
width: contentWidth
|
|
381
490
|
},
|
|
382
|
-
children:
|
|
383
|
-
|
|
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);
|
|
502
|
+
})
|
|
503
|
+
})
|
|
504
|
+
});
|
|
505
|
+
const outerStyle = {
|
|
506
|
+
width: '100%',
|
|
507
|
+
backgroundColor: cardBg,
|
|
508
|
+
borderWidth: 1,
|
|
509
|
+
borderColor: '#e8e8e8',
|
|
510
|
+
overflow: 'hidden'
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// --- flex mode (columnWidth omitted): original behaviour, unchanged -------
|
|
514
|
+
if (!useFixedWidth) {
|
|
515
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
516
|
+
style: [outerStyle, style],
|
|
517
|
+
children: [showCardsRow && /*#__PURE__*/_jsxs(View, {
|
|
518
|
+
style: {
|
|
519
|
+
flexDirection: 'row',
|
|
520
|
+
width: '100%'
|
|
521
|
+
},
|
|
522
|
+
children: [columns.map(renderCard), showAddCard && renderAddCard()]
|
|
523
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
524
|
+
style: {
|
|
525
|
+
width: '100%'
|
|
526
|
+
},
|
|
527
|
+
children: sections.map((section, index) => /*#__PURE__*/_jsx(Accordion, {
|
|
528
|
+
title: section.title,
|
|
529
|
+
defaultExpanded: section.defaultExpanded ?? index === 0,
|
|
530
|
+
modes: modes,
|
|
531
|
+
children: renderTable(section)
|
|
532
|
+
}, section.key ?? section.title ?? index))
|
|
533
|
+
})]
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// --- fixed-width scroll mode: chained horizontal sync + sticky cards -----
|
|
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
|
+
});
|
|
600
|
+
return /*#__PURE__*/_jsx(View, {
|
|
601
|
+
style: [outerStyle, style, {
|
|
602
|
+
flexDirection: 'column',
|
|
603
|
+
flex: 1
|
|
604
|
+
}],
|
|
605
|
+
children: /*#__PURE__*/_jsx(ScrollView, {
|
|
384
606
|
style: {
|
|
385
|
-
width: '100%'
|
|
607
|
+
width: '100%',
|
|
608
|
+
flex: 1
|
|
386
609
|
},
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}, section.key ?? section.title ?? index))
|
|
393
|
-
})]
|
|
610
|
+
stickyHeaderIndices: stickyIndices,
|
|
611
|
+
showsVerticalScrollIndicator: false,
|
|
612
|
+
directionalLockEnabled: true,
|
|
613
|
+
children: scrollChildren
|
|
614
|
+
})
|
|
394
615
|
});
|
|
395
616
|
}
|
|
396
617
|
export default CompareTable;
|