jfs-components 0.1.25 → 0.1.30

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 (97) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/lib/commonjs/components/AutoplayControl/AutoplayControl.js +66 -0
  3. package/lib/commonjs/components/Carousel/Carousel.js +112 -19
  4. package/lib/commonjs/components/CompareTable/CompareTable.js +191 -25
  5. package/lib/commonjs/components/ContentSheet/ContentSheet.js +104 -10
  6. package/lib/commonjs/components/FormUpload/Additem.js +168 -0
  7. package/lib/commonjs/components/FormUpload/FormUpload.js +198 -0
  8. package/lib/commonjs/components/Grid/Grid.js +206 -0
  9. package/lib/commonjs/components/MoneyValue/MoneyValue.js +179 -54
  10. package/lib/commonjs/components/NumberPagination/NumberPagination.js +144 -0
  11. package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +7 -1
  12. package/lib/commonjs/components/ProfileCard/ProfileCard.js +208 -0
  13. package/lib/commonjs/components/Table/Table.js +27 -7
  14. package/lib/commonjs/components/UpiHandle/UpiHandle.js +23 -11
  15. package/lib/commonjs/components/index.js +42 -0
  16. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  17. package/lib/commonjs/design-tokens/figma-modes.generated.js +21 -10
  18. package/lib/commonjs/icons/components/IconBrokenimage.js +19 -0
  19. package/lib/commonjs/icons/components/IconImage.js +19 -0
  20. package/lib/commonjs/icons/components/IconMic.js +19 -0
  21. package/lib/commonjs/icons/components/IconMicoff.js +19 -0
  22. package/lib/commonjs/icons/components/IconPause.js +19 -0
  23. package/lib/commonjs/icons/components/IconPlay.js +19 -0
  24. package/lib/commonjs/icons/components/index.js +66 -0
  25. package/lib/commonjs/icons/registry.js +2 -2
  26. package/lib/module/components/AutoplayControl/AutoplayControl.js +60 -0
  27. package/lib/module/components/Carousel/Carousel.js +111 -19
  28. package/lib/module/components/CompareTable/CompareTable.js +191 -26
  29. package/lib/module/components/ContentSheet/ContentSheet.js +108 -14
  30. package/lib/module/components/FormUpload/Additem.js +162 -0
  31. package/lib/module/components/FormUpload/FormUpload.js +192 -0
  32. package/lib/module/components/Grid/Grid.js +201 -0
  33. package/lib/module/components/MoneyValue/MoneyValue.js +182 -57
  34. package/lib/module/components/NumberPagination/NumberPagination.js +138 -0
  35. package/lib/module/components/PdpCcCard/PdpCcCard.js +7 -1
  36. package/lib/module/components/ProfileCard/ProfileCard.js +202 -0
  37. package/lib/module/components/Table/Table.js +27 -7
  38. package/lib/module/components/UpiHandle/UpiHandle.js +23 -11
  39. package/lib/module/components/index.js +7 -1
  40. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  41. package/lib/module/design-tokens/figma-modes.generated.js +21 -10
  42. package/lib/module/icons/components/IconBrokenimage.js +12 -0
  43. package/lib/module/icons/components/IconImage.js +12 -0
  44. package/lib/module/icons/components/IconMic.js +12 -0
  45. package/lib/module/icons/components/IconMicoff.js +12 -0
  46. package/lib/module/icons/components/IconPause.js +12 -0
  47. package/lib/module/icons/components/IconPlay.js +12 -0
  48. package/lib/module/icons/components/index.js +6 -0
  49. package/lib/module/icons/registry.js +2 -2
  50. package/lib/typescript/src/components/AutoplayControl/AutoplayControl.d.ts +15 -0
  51. package/lib/typescript/src/components/Carousel/Carousel.d.ts +8 -1
  52. package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +21 -1
  53. package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +20 -1
  54. package/lib/typescript/src/components/FormUpload/Additem.d.ts +96 -0
  55. package/lib/typescript/src/components/FormUpload/FormUpload.d.ts +85 -0
  56. package/lib/typescript/src/components/Grid/Grid.d.ts +87 -0
  57. package/lib/typescript/src/components/MoneyValue/MoneyValue.d.ts +10 -1
  58. package/lib/typescript/src/components/NumberPagination/NumberPagination.d.ts +21 -0
  59. package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +25 -1
  60. package/lib/typescript/src/components/ProfileCard/ProfileCard.d.ts +89 -0
  61. package/lib/typescript/src/components/Table/Table.d.ts +9 -1
  62. package/lib/typescript/src/components/UpiHandle/UpiHandle.d.ts +7 -1
  63. package/lib/typescript/src/components/index.d.ts +34 -28
  64. package/lib/typescript/src/design-tokens/figma-modes.generated.d.ts +10 -4
  65. package/lib/typescript/src/icons/components/IconBrokenimage.d.ts +3 -0
  66. package/lib/typescript/src/icons/components/IconImage.d.ts +3 -0
  67. package/lib/typescript/src/icons/components/IconMic.d.ts +3 -0
  68. package/lib/typescript/src/icons/components/IconMicoff.d.ts +3 -0
  69. package/lib/typescript/src/icons/components/IconPause.d.ts +3 -0
  70. package/lib/typescript/src/icons/components/IconPlay.d.ts +3 -0
  71. package/lib/typescript/src/icons/components/index.d.ts +6 -0
  72. package/lib/typescript/src/icons/registry.d.ts +1 -1
  73. package/package.json +1 -1
  74. package/src/components/AutoplayControl/AutoplayControl.tsx +77 -0
  75. package/src/components/Carousel/Carousel.tsx +133 -23
  76. package/src/components/CompareTable/CompareTable.tsx +224 -30
  77. package/src/components/ContentSheet/ContentSheet.tsx +138 -8
  78. package/src/components/FormUpload/Additem.tsx +262 -0
  79. package/src/components/FormUpload/FormUpload.tsx +313 -0
  80. package/src/components/Grid/Grid.tsx +368 -0
  81. package/src/components/MoneyValue/MoneyValue.tsx +216 -65
  82. package/src/components/NumberPagination/NumberPagination.tsx +170 -0
  83. package/src/components/PdpCcCard/PdpCcCard.tsx +36 -1
  84. package/src/components/ProfileCard/ProfileCard.tsx +268 -0
  85. package/src/components/Table/Table.tsx +29 -4
  86. package/src/components/UpiHandle/UpiHandle.tsx +35 -24
  87. package/src/components/index.ts +329 -182
  88. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  89. package/src/design-tokens/figma-modes.generated.ts +21 -10
  90. package/src/icons/components/IconBrokenimage.tsx +11 -0
  91. package/src/icons/components/IconImage.tsx +11 -0
  92. package/src/icons/components/IconMic.tsx +11 -0
  93. package/src/icons/components/IconMicoff.tsx +11 -0
  94. package/src/icons/components/IconPause.tsx +11 -0
  95. package/src/icons/components/IconPlay.tsx +11 -0
  96. package/src/icons/components/index.ts +6 -0
  97. package/src/icons/registry.ts +25 -1
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+
3
+ import React, { useMemo } from 'react';
4
+ import { Pressable } from 'react-native';
5
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
+ import Icon from '../../icons/Icon';
7
+ import { jsx as _jsx } from "react/jsx-runtime";
8
+ function resolveAutoplayControlTokens(modes, disabled) {
9
+ const width = getVariableByName('autoplayControl/width', modes) ?? 36;
10
+ const height = getVariableByName('autoplayControl/height', modes) ?? 36;
11
+ const radiusRaw = getVariableByName('autoplayControl/radius', modes) ?? 9999;
12
+ const backgroundColor = getVariableByName('autoplayControl/background/color', modes) ?? '#ffffff';
13
+ const iconColor = getVariableByName('autoplayControl/icon/color', modes) ?? '#000000';
14
+ const borderRadius = radiusRaw === 9999 ? width / 2 : radiusRaw;
15
+ return {
16
+ containerStyle: {
17
+ width,
18
+ height,
19
+ borderRadius,
20
+ backgroundColor,
21
+ alignItems: 'center',
22
+ justifyContent: 'center',
23
+ opacity: disabled ? 0.5 : 1
24
+ },
25
+ iconColor
26
+ };
27
+ }
28
+
29
+ /**
30
+ * AutoplayControl is a button component that toggles between play and pause states.
31
+ * All styling values are resolved from Figma design tokens.
32
+ */
33
+ export default function AutoplayControl({
34
+ state = 'pause',
35
+ modes,
36
+ onPress,
37
+ disabled = false,
38
+ style
39
+ }) {
40
+ const tokens = useMemo(() => resolveAutoplayControlTokens(modes ?? {}, disabled), [modes, disabled]);
41
+ const iconName = state === 'play' ? 'ic_play' : 'ic_pause';
42
+ return /*#__PURE__*/_jsx(Pressable, {
43
+ onPress: disabled ? undefined : onPress,
44
+ disabled: disabled,
45
+ style: ({
46
+ pressed
47
+ }) => [tokens.containerStyle, pressed && {
48
+ opacity: 0.7
49
+ }, style],
50
+ accessibilityRole: "button",
51
+ accessibilityState: {
52
+ disabled
53
+ },
54
+ children: /*#__PURE__*/_jsx(Icon, {
55
+ name: iconName,
56
+ size: 24,
57
+ color: tokens.iconColor
58
+ })
59
+ });
60
+ }
@@ -4,6 +4,8 @@ import React, { createContext, useContext, useRef, useState, useEffect, useCallb
4
4
  import { View, ScrollView, Animated, Pressable } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import { EMPTY_MODES } from '../../utils/react-utils';
7
+ import AutoplayControl from '../AutoplayControl/AutoplayControl';
8
+ import NumberPagination from '../NumberPagination/NumberPagination';
7
9
 
8
10
  // ---------------------------------------------------------------------------
9
11
  // Context
@@ -37,9 +39,11 @@ export function Carousel({
37
39
  itemWidth: itemWidthProp,
38
40
  paddingHorizontal: paddingHorizontalProp,
39
41
  paddingVertical: paddingVerticalProp,
42
+ type = 'Default',
40
43
  onIndexChange,
41
44
  style
42
45
  }) {
46
+ const isNumbered = type === 'Numbered';
43
47
  // ---- Token resolution ----
44
48
  const tokenGap = parseFloat(getVariableByName('carousel/gap', modes));
45
49
  const gap = gapProp ?? tokenGap;
@@ -47,27 +51,43 @@ export function Carousel({
47
51
  const containerPaddingV = parseFloat(getVariableByName('carousel/padding/vertical', modes));
48
52
  // Outer container max height per Figma (`carousel/maxHeight`).
49
53
  const maxHeight = parseFloat(getVariableByName('carousel/maxHeight', modes));
54
+ // Gap between AutoplayControl and NumberPagination in the Numbered overlay.
55
+ const controlGap = parseFloat(getVariableByName('carouselControl/gap', modes));
56
+
50
57
  // Spacing between the cards row and the pagination dots uses `carousel/gap`.
51
58
  const paginationOffset = gap;
52
59
 
53
60
  // ---- Refs & state ----
54
61
  const scrollRef = useRef(null);
55
62
  const [activeIndex, setActiveIndex] = useState(0);
63
+ const activeIndexRef = useRef(0);
56
64
  const [containerWidth, setContainerWidth] = useState(0);
57
65
  const autoPlayTimer = useRef(null);
58
66
  const userInteracting = useRef(false);
67
+ // Guard: suppress handleScroll index updates during programmatic scrollTo
68
+ // animations so intermediate offsets don't flicker activeIndex back.
69
+ const programmaticScroll = useRef(false);
70
+ const programmaticTimer = useRef(null);
71
+ useEffect(() => {
72
+ activeIndexRef.current = activeIndex;
73
+ }, [activeIndex]);
74
+
75
+ // Interactive play/pause state for the Numbered variant.
76
+ const [isPlaying, setIsPlaying] = useState(autoPlay);
59
77
 
60
78
  // Flatten children so we can count items
61
79
  const items = useMemo(() => React.Children.toArray(children).filter(React.isValidElement), [children]);
62
80
  const totalItems = items.length;
63
81
 
64
- // Effective item width: provided prop, or full container width minus peek offsets
82
+ // Effective item width: provided prop, or full container width minus peek offsets.
83
+ // For the Numbered variant the items are full-bleed (no peek padding).
65
84
  const effectiveItemWidth = useMemo(() => {
66
85
  if (itemWidthProp != null) return itemWidthProp;
67
86
  if (containerWidth === 0) return 0;
87
+ if (isNumbered) return containerWidth;
68
88
  // Full-width minus peekOffset on each side
69
89
  return containerWidth - containerPaddingH * 4;
70
- }, [itemWidthProp, containerWidth, containerPaddingH]);
90
+ }, [itemWidthProp, containerWidth, containerPaddingH, isNumbered]);
71
91
 
72
92
  // Snap interval = item width + gap
73
93
  const snapInterval = effectiveItemWidth + gap;
@@ -81,6 +101,17 @@ export function Carousel({
81
101
  } else {
82
102
  targetIndex = Math.max(0, Math.min(index, totalItems - 1));
83
103
  }
104
+ // Lock out handleScroll during the animated scroll so intermediate
105
+ // offsets don't flicker activeIndex back to the old value.
106
+ if (animated) {
107
+ programmaticScroll.current = true;
108
+ if (programmaticTimer.current != null) {
109
+ clearTimeout(programmaticTimer.current);
110
+ }
111
+ programmaticTimer.current = setTimeout(() => {
112
+ programmaticScroll.current = false;
113
+ }, 500);
114
+ }
84
115
  scrollRef.current?.scrollTo({
85
116
  x: targetIndex * snapInterval,
86
117
  animated
@@ -95,6 +126,9 @@ export function Carousel({
95
126
  // ---- Scroll event handler ----
96
127
  const handleScroll = useCallback(event => {
97
128
  if (snapInterval === 0) return;
129
+ // Skip index recalculation during programmatic scrollTo animations
130
+ // to prevent the active indicator from flickering back to the old page.
131
+ if (programmaticScroll.current) return;
98
132
  const offsetX = event.nativeEvent.contentOffset.x;
99
133
  const newIndex = Math.round(offsetX / snapInterval);
100
134
  const clampedIndex = Math.max(0, Math.min(newIndex, totalItems - 1));
@@ -112,22 +146,19 @@ export function Carousel({
112
146
  }
113
147
  }, []);
114
148
  const startAutoPlay = useCallback(() => {
115
- if (!autoPlay || totalItems <= 1) return;
149
+ if (!autoPlay && !isPlaying || totalItems <= 1) return;
150
+ if (!isPlaying) return;
116
151
  clearAutoPlay();
117
152
  autoPlayTimer.current = setInterval(() => {
118
153
  if (!userInteracting.current) {
119
- setActiveIndex(prev => {
120
- const next = loop ? (prev + 1) % totalItems : Math.min(prev + 1, totalItems - 1);
121
- scrollRef.current?.scrollTo({
122
- x: next * snapInterval,
123
- animated: true
124
- });
125
- onIndexChange?.(next);
126
- return next;
127
- });
154
+ const prev = activeIndexRef.current;
155
+ const next = loop ? (prev + 1) % totalItems : Math.min(prev + 1, totalItems - 1);
156
+ // Route through scrollToIndex so the programmaticScroll guard
157
+ // suppresses intermediate handleScroll offsets during the animation.
158
+ scrollToIndex(next, true);
128
159
  }
129
160
  }, autoPlayInterval);
130
- }, [autoPlay, totalItems, loop, snapInterval, autoPlayInterval, clearAutoPlay, onIndexChange]);
161
+ }, [autoPlay, isPlaying, totalItems, loop, autoPlayInterval, clearAutoPlay, scrollToIndex]);
131
162
  useEffect(() => {
132
163
  startAutoPlay();
133
164
  return clearAutoPlay;
@@ -138,8 +169,23 @@ export function Carousel({
138
169
  const handleScrollEndDrag = useCallback(() => {
139
170
  userInteracting.current = false;
140
171
  // Reset autoplay timer after interaction
141
- if (autoPlay) startAutoPlay();
142
- }, [autoPlay, startAutoPlay]);
172
+ if (autoPlay || isPlaying) startAutoPlay();
173
+ }, [autoPlay, isPlaying, startAutoPlay]);
174
+ const finishProgrammaticScroll = useCallback(event => {
175
+ programmaticScroll.current = false;
176
+ if (programmaticTimer.current != null) {
177
+ clearTimeout(programmaticTimer.current);
178
+ programmaticTimer.current = null;
179
+ }
180
+ if (snapInterval === 0) return;
181
+ const offsetX = event.nativeEvent.contentOffset.x;
182
+ const newIndex = Math.round(offsetX / snapInterval);
183
+ const clampedIndex = Math.max(0, Math.min(newIndex, totalItems - 1));
184
+ if (clampedIndex !== activeIndexRef.current) {
185
+ setActiveIndex(clampedIndex);
186
+ onIndexChange?.(clampedIndex);
187
+ }
188
+ }, [snapInterval, totalItems, onIndexChange]);
143
189
 
144
190
  // ---- Layout ----
145
191
  const handleLayout = useCallback(e => {
@@ -156,14 +202,37 @@ export function Carousel({
156
202
  goPrev
157
203
  }), [modes, activeIndex, totalItems, goTo, goNext, goPrev]);
158
204
 
205
+ // ---- Toggle handler for Numbered variant ----
206
+ const handleToggleAutoplay = useCallback(() => {
207
+ setIsPlaying(prev => {
208
+ if (prev) {
209
+ // Pausing
210
+ clearAutoPlay();
211
+ return false;
212
+ }
213
+ // Resuming — autoplay will restart via the effect
214
+ return true;
215
+ });
216
+ }, [clearAutoPlay]);
217
+
218
+ // Restart autoplay whenever isPlaying flips to true
219
+ useEffect(() => {
220
+ if (isPlaying) startAutoPlay();else clearAutoPlay();
221
+ }, [isPlaying, startAutoPlay, clearAutoPlay]);
222
+
159
223
  // ---- Render ----
160
224
  // Explicit padding props win verbatim; otherwise fall back to the
161
225
  // token-derived values (horizontal keeps its legacy ×2 "peek" multiplier).
162
- const effectivePaddingH = paddingHorizontalProp ?? containerPaddingH;
163
- const effectivePaddingV = paddingVerticalProp ?? containerPaddingV;
226
+ // Numbered variant is full-bleed: zero padding.
227
+ const effectivePaddingH = isNumbered ? 0 : paddingHorizontalProp ?? containerPaddingH;
228
+ const effectivePaddingV = isNumbered ? 0 : paddingVerticalProp ?? containerPaddingV;
164
229
  const outerStyle = {
165
230
  paddingVertical: effectivePaddingV,
166
- maxHeight
231
+ maxHeight: isNumbered ? undefined : maxHeight,
232
+ // Numbered needs relative positioning for the floating controls overlay.
233
+ ...(isNumbered ? {
234
+ overflow: 'hidden'
235
+ } : {})
167
236
  };
168
237
  const contentContainerStyle = {
169
238
  paddingHorizontal: effectivePaddingH,
@@ -189,6 +258,7 @@ export function Carousel({
189
258
  scrollEventThrottle: 16,
190
259
  onScrollBeginDrag: handleScrollBeginDrag,
191
260
  onScrollEndDrag: handleScrollEndDrag,
261
+ onMomentumScrollEnd: finishProgrammaticScroll,
192
262
  children: items.map((child, index) => {
193
263
  // Strict slot box: width must be honored; never grow or shrink with
194
264
  // content, and clip anything that misbehaves (e.g. a child whose
@@ -225,11 +295,33 @@ export function Carousel({
225
295
  children: childWithModes
226
296
  }, index);
227
297
  })
228
- }), showPagination && totalItems > 1 && /*#__PURE__*/_jsx(Pagination, {
298
+ }), !isNumbered && showPagination && totalItems > 1 && /*#__PURE__*/_jsx(Pagination, {
229
299
  modes: modes,
230
300
  style: {
231
301
  marginTop: paginationOffset
232
302
  }
303
+ }), isNumbered && showPagination && totalItems > 1 && /*#__PURE__*/_jsxs(View, {
304
+ style: {
305
+ position: 'absolute',
306
+ bottom: 12,
307
+ left: 0,
308
+ right: 0,
309
+ flexDirection: 'row',
310
+ justifyContent: 'center',
311
+ alignItems: 'center',
312
+ gap: controlGap
313
+ },
314
+ pointerEvents: "box-none",
315
+ children: [/*#__PURE__*/_jsx(AutoplayControl, {
316
+ state: isPlaying ? 'pause' : 'play',
317
+ onPress: handleToggleAutoplay,
318
+ modes: modes
319
+ }), /*#__PURE__*/_jsx(NumberPagination, {
320
+ activePage: activeIndex + 1,
321
+ totalPages: totalItems,
322
+ onPageChange: page => goTo(page - 1),
323
+ modes: modes
324
+ })]
233
325
  })]
234
326
  })
235
327
  });
@@ -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,8 @@ function CompareTable({
106
106
  maxColumns = 4,
107
107
  modes = EMPTY_MODES,
108
108
  style,
109
- disableTruncation
109
+ disableTruncation,
110
+ columnWidth
110
111
  }) {
111
112
  // --- selection card tokens ------------------------------------------------
112
113
  const cardBg = getVariableByName('selectionCard/background/color', modes) ?? '#ffffff';
@@ -163,6 +164,70 @@ function CompareTable({
163
164
  const showAddCard = onAddColumn != null && columnCount < maxColumns;
164
165
  /** Grid columns shared by the selection-card row and every table row. */
165
166
  const gridColumnCount = columnCount + (showAddCard ? 1 : 0);
167
+ const showCardsRow = columnCount > 0 || showAddCard;
168
+
169
+ // --- fixed-width scroll mode -------------------------------------------
170
+ // `columnWidth` opts the component into scroll mode (see prop doc). In that
171
+ // mode the cards row and each section's table body are independent
172
+ // horizontal ScrollViews; their scroll events are chained so they move in
173
+ // lockstep, and the cards row pins to the top on the vertical axis.
174
+ const useFixedWidth = columnWidth != null;
175
+ /** Total scrollable content width shared by every horizontal surface. */
176
+ const contentWidth = useFixedWidth ? gridColumnCount * columnWidth : 0;
177
+ const cardsScrollRef = useRef(null);
178
+ const bodyScrollRefs = useRef(new Map());
179
+ // Stable per-section ref-callback instances so React doesn't detach/reattach
180
+ // on every render (which would churn the map and re-fire scrollTo).
181
+ const bodyRefCallbacks = useRef(new Map());
182
+ /** Last offset we synced everyone to. Echoed onScroll events reporting this
183
+ * same value are ignored — this is what prevents feedback loops. */
184
+ const lastSyncX = useRef(0);
185
+ const applySync = useCallback((x, source) => {
186
+ if (Math.abs(x - lastSyncX.current) < 0.5) return;
187
+ lastSyncX.current = x;
188
+ if (source !== 'cards' && cardsScrollRef.current) {
189
+ cardsScrollRef.current.scrollTo({
190
+ x,
191
+ animated: false
192
+ });
193
+ }
194
+ bodyScrollRefs.current.forEach((ref, idx) => {
195
+ if (source === 'cards' || idx !== source) {
196
+ ref.scrollTo({
197
+ x,
198
+ animated: false
199
+ });
200
+ }
201
+ });
202
+ }, []);
203
+ const onCardsScroll = useCallback(e => {
204
+ applySync(e.nativeEvent.contentOffset.x, 'cards');
205
+ }, [applySync]);
206
+ const onBodyScroll = useCallback(idx => e => {
207
+ applySync(e.nativeEvent.contentOffset.x, idx);
208
+ }, [applySync]);
209
+ const getBodyRefCallback = useCallback(idx => {
210
+ let cb = bodyRefCallbacks.current.get(idx);
211
+ if (!cb) {
212
+ cb = ref => {
213
+ if (ref) {
214
+ bodyScrollRefs.current.set(idx, ref);
215
+ // Bring a freshly-expanded section's body into sync with
216
+ // the rest of the surfaces.
217
+ if (lastSyncX.current > 0) {
218
+ ref.scrollTo({
219
+ x: lastSyncX.current,
220
+ animated: false
221
+ });
222
+ }
223
+ } else {
224
+ bodyScrollRefs.current.delete(idx);
225
+ }
226
+ };
227
+ bodyRefCallbacks.current.set(idx, cb);
228
+ }
229
+ return cb;
230
+ }, []);
166
231
  const cardLabelStyle = {
167
232
  ...NO_WRAP_TEXT,
168
233
  color: cardLabelColor,
@@ -193,8 +258,12 @@ function CompareTable({
193
258
  fontWeight: toWeight(cellFontWeight)
194
259
  };
195
260
  const cardStyle = {
196
- flex: 1,
197
- minWidth: 0,
261
+ ...(useFixedWidth ? {
262
+ width: columnWidth
263
+ } : {
264
+ flex: 1,
265
+ minWidth: 0
266
+ }),
198
267
  minHeight: 147,
199
268
  backgroundColor: cardBg,
200
269
  borderRadius: cardRadius,
@@ -313,6 +382,7 @@ function CompareTable({
313
382
  return /*#__PURE__*/_jsx(Table.Cell, {
314
383
  modes: modes,
315
384
  flex: 1,
385
+ width: useFixedWidth ? columnWidth : undefined,
316
386
  align: "left",
317
387
  isLastColumn: isLastCol,
318
388
  isLastRow: isLastRow,
@@ -366,31 +436,126 @@ function CompareTable({
366
436
  })
367
437
  })]
368
438
  });
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, {
439
+
440
+ // Render a single section in fixed-width scroll mode. Unlike `renderTable`
441
+ // (used in flex mode), the gray header row is rendered full-width OUTSIDE
442
+ // the body's horizontal ScrollView so it stays put horizontally (it does
443
+ // not slide with the cells) while still scrolling vertically with the table.
444
+ // The body rows live inside a horizontal ScrollView that is scroll-synced
445
+ // with the cards row and every other section's body.
446
+ const renderSection = (section, index) => /*#__PURE__*/_jsxs(Accordion, {
447
+ title: section.title,
448
+ defaultExpanded: section.defaultExpanded ?? index === 0,
449
+ modes: modes,
450
+ children: [section.header != null && /*#__PURE__*/_jsx(View, {
378
451
  style: {
379
- flexDirection: 'row',
380
- width: '100%'
452
+ width: '100%',
453
+ backgroundColor: headerBg,
454
+ paddingHorizontal: headerPaddingH,
455
+ paddingVertical: headerPaddingV,
456
+ borderBottomWidth: cellBorderSize,
457
+ borderBottomColor: cellBorderColor
381
458
  },
382
- children: [columns.map(renderCard), showAddCard && renderAddCard()]
383
- }), /*#__PURE__*/_jsx(View, {
459
+ children: /*#__PURE__*/_jsx(Text, {
460
+ style: headerTextStyle,
461
+ children: section.header
462
+ })
463
+ }), /*#__PURE__*/_jsx(ScrollView, {
464
+ ref: getBodyRefCallback(index),
465
+ horizontal: true,
466
+ showsHorizontalScrollIndicator: false,
467
+ scrollEventThrottle: 16,
468
+ onScroll: onBodyScroll(index),
469
+ children: /*#__PURE__*/_jsx(View, {
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
+ })
485
+ })
486
+ })]
487
+ }, section.key ?? section.title ?? index);
488
+ const outerStyle = {
489
+ width: '100%',
490
+ backgroundColor: cardBg,
491
+ borderWidth: 1,
492
+ borderColor: '#e8e8e8',
493
+ overflow: 'hidden'
494
+ };
495
+
496
+ // --- flex mode (columnWidth omitted): original behaviour, unchanged -------
497
+ if (!useFixedWidth) {
498
+ return /*#__PURE__*/_jsxs(View, {
499
+ style: [outerStyle, style],
500
+ children: [showCardsRow && /*#__PURE__*/_jsxs(View, {
501
+ style: {
502
+ flexDirection: 'row',
503
+ width: '100%'
504
+ },
505
+ children: [columns.map(renderCard), showAddCard && renderAddCard()]
506
+ }), /*#__PURE__*/_jsx(View, {
507
+ style: {
508
+ width: '100%'
509
+ },
510
+ children: sections.map((section, index) => /*#__PURE__*/_jsx(Accordion, {
511
+ title: section.title,
512
+ defaultExpanded: section.defaultExpanded ?? index === 0,
513
+ modes: modes,
514
+ children: renderTable(section)
515
+ }, section.key ?? section.title ?? index))
516
+ })]
517
+ });
518
+ }
519
+
520
+ // --- fixed-width scroll mode: chained horizontal sync + sticky cards -----
521
+ // The cards row (index 0) is a sticky header of the vertical ScrollView so
522
+ // it pins to the top while the sections scroll beneath. The cards row is
523
+ // itself a horizontal ScrollView, synced with every section body. For the
524
+ // vertical scroll (and thus the sticky behaviour) to engage, the host must
525
+ // give CompareTable a bounded height (e.g. a sized container or flex:1).
526
+ return /*#__PURE__*/_jsx(View, {
527
+ style: [outerStyle, style, {
528
+ flexDirection: 'column',
529
+ flex: 1
530
+ }],
531
+ children: /*#__PURE__*/_jsxs(ScrollView, {
384
532
  style: {
385
- width: '100%'
533
+ width: '100%',
534
+ flex: 1
386
535
  },
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
- })]
536
+ stickyHeaderIndices: showCardsRow ? [0] : [],
537
+ showsVerticalScrollIndicator: false,
538
+ directionalLockEnabled: true,
539
+ children: [showCardsRow && /*#__PURE__*/_jsx(ScrollView, {
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
+ })]
558
+ })
394
559
  });
395
560
  }
396
561
  export default CompareTable;