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.
Files changed (53) hide show
  1. package/CHANGELOG.md +28 -2
  2. package/lib/commonjs/components/CategoryCard/CategoryCard.js +264 -0
  3. package/lib/commonjs/components/CompareTable/CompareTable.js +247 -25
  4. package/lib/commonjs/components/ContentSheet/ContentSheet.js +91 -8
  5. package/lib/commonjs/components/CounterBadge/CounterBadge.js +78 -0
  6. package/lib/commonjs/components/FavoriteToggle/FavoriteToggle.js +180 -0
  7. package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +287 -0
  8. package/lib/commonjs/components/MoneyValue/MoneyValue.js +179 -54
  9. package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +7 -1
  10. package/lib/commonjs/components/Table/Table.js +27 -7
  11. package/lib/commonjs/components/ValueBackMetric/ValueBackMetric.js +219 -0
  12. package/lib/commonjs/components/index.js +35 -0
  13. package/lib/commonjs/design-tokens/figma-modes.generated.js +3 -0
  14. package/lib/commonjs/icons/registry.js +1 -1
  15. package/lib/module/components/CategoryCard/CategoryCard.js +258 -0
  16. package/lib/module/components/CompareTable/CompareTable.js +247 -26
  17. package/lib/module/components/ContentSheet/ContentSheet.js +94 -11
  18. package/lib/module/components/CounterBadge/CounterBadge.js +73 -0
  19. package/lib/module/components/FavoriteToggle/FavoriteToggle.js +174 -0
  20. package/lib/module/components/HelloJioInput/HelloJioInput.js +281 -0
  21. package/lib/module/components/MoneyValue/MoneyValue.js +182 -57
  22. package/lib/module/components/PdpCcCard/PdpCcCard.js +7 -1
  23. package/lib/module/components/Table/Table.js +27 -7
  24. package/lib/module/components/ValueBackMetric/ValueBackMetric.js +214 -0
  25. package/lib/module/components/index.js +6 -1
  26. package/lib/module/design-tokens/figma-modes.generated.js +3 -0
  27. package/lib/module/icons/registry.js +1 -1
  28. package/lib/typescript/src/components/CategoryCard/CategoryCard.d.ts +72 -0
  29. package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +36 -1
  30. package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +20 -1
  31. package/lib/typescript/src/components/CounterBadge/CounterBadge.d.ts +19 -0
  32. package/lib/typescript/src/components/FavoriteToggle/FavoriteToggle.d.ts +66 -0
  33. package/lib/typescript/src/components/HelloJioInput/HelloJioInput.d.ts +111 -0
  34. package/lib/typescript/src/components/MoneyValue/MoneyValue.d.ts +10 -1
  35. package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +25 -1
  36. package/lib/typescript/src/components/Table/Table.d.ts +9 -1
  37. package/lib/typescript/src/components/ValueBackMetric/ValueBackMetric.d.ts +86 -0
  38. package/lib/typescript/src/components/index.d.ts +5 -0
  39. package/lib/typescript/src/icons/registry.d.ts +1 -1
  40. package/package.json +1 -1
  41. package/src/components/CategoryCard/CategoryCard.tsx +404 -0
  42. package/src/components/CompareTable/CompareTable.tsx +324 -30
  43. package/src/components/ContentSheet/ContentSheet.tsx +120 -11
  44. package/src/components/CounterBadge/CounterBadge.tsx +95 -0
  45. package/src/components/FavoriteToggle/FavoriteToggle.tsx +243 -0
  46. package/src/components/HelloJioInput/HelloJioInput.tsx +513 -0
  47. package/src/components/MoneyValue/MoneyValue.tsx +216 -65
  48. package/src/components/PdpCcCard/PdpCcCard.tsx +36 -1
  49. package/src/components/Table/Table.tsx +29 -4
  50. package/src/components/ValueBackMetric/ValueBackMetric.tsx +298 -0
  51. package/src/components/index.ts +5 -0
  52. package/src/design-tokens/figma-modes.generated.ts +3 -0
  53. 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
- flex: 1,
197
- minWidth: 0,
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
- return /*#__PURE__*/_jsxs(View, {
370
- style: [{
371
- width: '100%',
372
- backgroundColor: cardBg,
373
- borderWidth: 1,
374
- borderColor: '#e8e8e8',
375
- overflow: 'hidden'
376
- }, style],
377
- children: [(columnCount > 0 || showAddCard) && /*#__PURE__*/_jsxs(View, {
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
- flexDirection: 'row',
380
- width: '100%'
489
+ width: contentWidth
381
490
  },
382
- children: [columns.map(renderCard), showAddCard && renderAddCard()]
383
- }), /*#__PURE__*/_jsx(View, {
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
- children: sections.map((section, index) => /*#__PURE__*/_jsx(Accordion, {
388
- title: section.title,
389
- defaultExpanded: section.defaultExpanded ?? index === 0,
390
- modes: modes,
391
- children: renderTable(section)
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;