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
package/CHANGELOG.md CHANGED
@@ -4,6 +4,59 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [0.1.30] - 2026-07-10
8
+
9
+ - `MoneyValue` — new `editable` prop (inline edit mode): tapping the value shows a numeric `TextInput` that accepts only digits + one decimal point; edit commits on blur/submit via new `onValueChange` callback; keyboard avoidance lifts the component above the keyboard using `Keyboard.addListener` + `measureInWindow` (same pattern as `ActionFooter`).
10
+ - `ContentSheet` — new `visible` prop controls a bottom pop-up / exit animation driven by Reanimated's `withSpring` (same spring config as `Drawer`: damping 32, stiffness 300). Sheet springs up from below the bottom edge on mount / `visible=true` and springs back down on `visible=false`. Animation composes with keyboard avoidance so both `translateY` offsets are summed in a single `useAnimatedStyle`.
11
+ - `CompareTable` — new `columnWidth` prop enables fixed-width scroll mode: each selection card and table cell gets this exact width; the cards row and every section body become independent horizontal `ScrollView`s whose scroll events are chained so they stay aligned in lockstep; the cards row pins to the top as a sticky header on the vertical axis; gray section-header rows render full-width outside the horizontal scroll so they don't slide with the cells.
12
+ - `PdpCcCard` — new `numberOfLines`, `disableTruncation`, and `singleLine` props passed through to the `Title` component for precise control over title truncation and wrapping.
13
+ - `Table` — new `columnWidth` prop applies a fixed width to every column (header + body) for uniform column sizing in scrollable layouts; new `scrollable` prop wraps the table in a horizontal `ScrollView`.
14
+ - Stories and MDX documentation updated for all new features.
15
+
16
+ ---
17
+
18
+ ## [0.1.28] - 2026-07-07
19
+
20
+ - Added `FormUpload` — token-driven attachment field with label, support text, `<Form>` validation integration, and a controlled `attachments` / `onAttachmentsChange` API. Renders a row of `Additem` cells (preview + empty upload trigger) with optional `maxCount` cap; accepts an injected `picker` callback so consumers wire their own image/document picker without adding native deps to the library.
21
+ - Added `Additem` — single upload cell used by `FormUpload` and `Grid`; `'empty'` state opens the injected picker, `'preview'` shows the selected asset with remove affordance (`additem/*` tokens).
22
+ - Added `Grid` — token-driven layout primitive with configurable `columns`, optional fixed `rows`, fractional `columnFrs` / `rowFrs` track weights, gap/padding tokens, and `justifyContent` / `alignItems`; each child becomes a cell with `modes` cascade.
23
+ - `ContentSheet` — new optional `title` prop renders a centered sticky header using the same `drawer/title*` token family as `Drawer`, keeping bottom-sheet title styling consistent across surfaces.
24
+ - Added `ic_broken_image`, `ic_image`, `ic_mic`, and `ic_mic_off` icons to the icon registry.
25
+ - Regenerated component MDX token documentation and `.token-metadata.json` across the library.
26
+
27
+ ---
28
+
29
+ ## [0.1.27] - 2026-07-07
30
+
31
+ - Added `ProfileCard` — dark rounded profile summary card (`profileCard/*` tokens) with an `Attached` monogram `Avatar` (optional `IconCapsule` badge), declarative header props (`name`, `monogram`, `avatarSource`, `upiHandles`) or a full `header` slot override, and a body slot below a `Divider`. Forces `context 10: jioPlus` so nested children (`ListItem`, `UpiHandle`, `Text`, `Button`, …) resolve on-dark profile-card tokens automatically; `Profile Card Appearance` mode (`Default` | `Premium`) recolors the whole card via `modes`.
32
+ - `UpiHandle` — avatar visibility is now **source-driven**: the leading image renders only when `source` / `avatarSource` is provided; omitting it hides the avatar and force-sets `UPI Handle Image: False` so left padding stays symmetric (imageless layout used by `ProfileCard` handle pills). Removed the bundled default `Image.png` placeholder. Consumer `UPI Handle Image` mode values are overridden by source presence so avatar visibility and token layout stay in lockstep. New `WithoutImage` story and MDX usage docs.
33
+ - `Section` — `Section.Bento` stories updated with `UpiHandle` pills that pass explicit avatar `source` values, matching the Figma Bento nav/upi layout (node 216-13845).
34
+ - Figma token sync — `context 10` mode renamed `Profile card` → `jioPlus`; `Profile Card Appearance` collection wired for `ProfileCard` / `UpiHandle`.
35
+ - `Carousel` — new `type="Numbered"` variant: full-bleed slides with floating `AutoplayControl` + `NumberPagination` overlay at the bottom center; interactive play/pause state wired to autoplay.
36
+ - `Carousel` — **Fix:** autoplay pagination no longer flickers back to the previous active page during scroll animations; `activeIndex` is derived from settled offsets instead of intermediate scroll positions.
37
+ - Regenerated component MDX token documentation and `.token-metadata.json` across the library.
38
+
39
+ ---
40
+
41
+ ## [0.1.26] - 2026-07-07
42
+
43
+ - Added `AutoplayControl` — token-driven play/pause toggle button for carousel autoplay (`autoplayControl/*` tokens; `ic_play` / `ic_pause` icons).
44
+ - Added `NumberPagination` — frosted-glass page-number strip with active/inactive control states (`numberPagination/*`, `controlNumber/*`, `control/*` tokens); supports `children` slot override.
45
+ - Added `ic_play` and `ic_pause` icons to the icon registry.
46
+ - Regenerated component MDX token documentation and `.token-metadata.json` across the library.
47
+
48
+ ---
49
+
50
+ ## [0.1.25] - 2026-07-03
51
+
52
+ - Added `MetricData` — compact centered metric block (`title`, `value`, optional `caption` and leading `icon`); typography, colours, gaps, and padding resolve from `metricdata/*` tokens; `value` accepts a string or custom node (e.g. `MoneyValue`).
53
+ - Added `Rating` — token-driven star row (`rating/gap`, `ratingStar/*`); filled vs empty stars share one SVG glyph and differ by `Rating Star State`; optional `onChange` for interactive selection, `readOnly` for static display.
54
+ - `CardCTA` — **Fix:** restore true 2fr right-column geometry on Android. Replaces the `flexBasis: 'auto'` workaround (which over-expanded the icon column and caused early body-text wrapping) with `flexBasis: 0` plus an explicit `minWidth` floor sized to the resolved `IconCapsule` + horizontal padding, so the column keeps the design ratio without collapsing when the left text is long.
55
+ - `ChipSelect` — new `showCloseIcon` prop (default `true`); set `showCloseIcon={false}` to hide the close icon in the active state.
56
+ - `ListItem` — vertical layout support text is hard-clamped to exactly two lines via `numberOfLines={2}` (spaces become line breaks, e.g. `"Split Payments"` → two lines). Fixes Bento grid labels wrapping onto a third line on native, where React Native has no `white-space: nowrap` equivalent. Horizontal layout support text uses the same two-line clamp.
57
+
58
+ ---
59
+
7
60
  ## [0.1.23] - 2026-07-01
8
61
 
9
62
  - Added `Link` — token-driven underlined text link mirroring the `Text` API (`text`/`children`, `autolayout`, `textAlign`, `disableTruncation`, `singleLine`); navigation-agnostic with consumer-provided `onPress`.
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = AutoplayControl;
7
+ var _react = _interopRequireWildcard(require("react"));
8
+ var _reactNative = require("react-native");
9
+ var _figmaVariablesResolver = require("../../design-tokens/figma-variables-resolver");
10
+ var _Icon = _interopRequireDefault(require("../../icons/Icon"));
11
+ var _jsxRuntime = require("react/jsx-runtime");
12
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
13
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
14
+ function resolveAutoplayControlTokens(modes, disabled) {
15
+ const width = (0, _figmaVariablesResolver.getVariableByName)('autoplayControl/width', modes) ?? 36;
16
+ const height = (0, _figmaVariablesResolver.getVariableByName)('autoplayControl/height', modes) ?? 36;
17
+ const radiusRaw = (0, _figmaVariablesResolver.getVariableByName)('autoplayControl/radius', modes) ?? 9999;
18
+ const backgroundColor = (0, _figmaVariablesResolver.getVariableByName)('autoplayControl/background/color', modes) ?? '#ffffff';
19
+ const iconColor = (0, _figmaVariablesResolver.getVariableByName)('autoplayControl/icon/color', modes) ?? '#000000';
20
+ const borderRadius = radiusRaw === 9999 ? width / 2 : radiusRaw;
21
+ return {
22
+ containerStyle: {
23
+ width,
24
+ height,
25
+ borderRadius,
26
+ backgroundColor,
27
+ alignItems: 'center',
28
+ justifyContent: 'center',
29
+ opacity: disabled ? 0.5 : 1
30
+ },
31
+ iconColor
32
+ };
33
+ }
34
+
35
+ /**
36
+ * AutoplayControl is a button component that toggles between play and pause states.
37
+ * All styling values are resolved from Figma design tokens.
38
+ */
39
+ function AutoplayControl({
40
+ state = 'pause',
41
+ modes,
42
+ onPress,
43
+ disabled = false,
44
+ style
45
+ }) {
46
+ const tokens = (0, _react.useMemo)(() => resolveAutoplayControlTokens(modes ?? {}, disabled), [modes, disabled]);
47
+ const iconName = state === 'play' ? 'ic_play' : 'ic_pause';
48
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
49
+ onPress: disabled ? undefined : onPress,
50
+ disabled: disabled,
51
+ style: ({
52
+ pressed
53
+ }) => [tokens.containerStyle, pressed && {
54
+ opacity: 0.7
55
+ }, style],
56
+ accessibilityRole: "button",
57
+ accessibilityState: {
58
+ disabled
59
+ },
60
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Icon.default, {
61
+ name: iconName,
62
+ size: 24,
63
+ color: tokens.iconColor
64
+ })
65
+ });
66
+ }
@@ -11,7 +11,10 @@ var _react = _interopRequireWildcard(require("react"));
11
11
  var _reactNative = require("react-native");
12
12
  var _figmaVariablesResolver = require("../../design-tokens/figma-variables-resolver");
13
13
  var _reactUtils = require("../../utils/react-utils");
14
+ var _AutoplayControl = _interopRequireDefault(require("../AutoplayControl/AutoplayControl"));
15
+ var _NumberPagination = _interopRequireDefault(require("../NumberPagination/NumberPagination"));
14
16
  var _jsxRuntime = require("react/jsx-runtime");
17
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
15
18
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
16
19
  // ---------------------------------------------------------------------------
17
20
  // Context
@@ -45,9 +48,11 @@ function Carousel({
45
48
  itemWidth: itemWidthProp,
46
49
  paddingHorizontal: paddingHorizontalProp,
47
50
  paddingVertical: paddingVerticalProp,
51
+ type = 'Default',
48
52
  onIndexChange,
49
53
  style
50
54
  }) {
55
+ const isNumbered = type === 'Numbered';
51
56
  // ---- Token resolution ----
52
57
  const tokenGap = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carousel/gap', modes));
53
58
  const gap = gapProp ?? tokenGap;
@@ -55,27 +60,43 @@ function Carousel({
55
60
  const containerPaddingV = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carousel/padding/vertical', modes));
56
61
  // Outer container max height per Figma (`carousel/maxHeight`).
57
62
  const maxHeight = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carousel/maxHeight', modes));
63
+ // Gap between AutoplayControl and NumberPagination in the Numbered overlay.
64
+ const controlGap = parseFloat((0, _figmaVariablesResolver.getVariableByName)('carouselControl/gap', modes));
65
+
58
66
  // Spacing between the cards row and the pagination dots uses `carousel/gap`.
59
67
  const paginationOffset = gap;
60
68
 
61
69
  // ---- Refs & state ----
62
70
  const scrollRef = (0, _react.useRef)(null);
63
71
  const [activeIndex, setActiveIndex] = (0, _react.useState)(0);
72
+ const activeIndexRef = (0, _react.useRef)(0);
64
73
  const [containerWidth, setContainerWidth] = (0, _react.useState)(0);
65
74
  const autoPlayTimer = (0, _react.useRef)(null);
66
75
  const userInteracting = (0, _react.useRef)(false);
76
+ // Guard: suppress handleScroll index updates during programmatic scrollTo
77
+ // animations so intermediate offsets don't flicker activeIndex back.
78
+ const programmaticScroll = (0, _react.useRef)(false);
79
+ const programmaticTimer = (0, _react.useRef)(null);
80
+ (0, _react.useEffect)(() => {
81
+ activeIndexRef.current = activeIndex;
82
+ }, [activeIndex]);
83
+
84
+ // Interactive play/pause state for the Numbered variant.
85
+ const [isPlaying, setIsPlaying] = (0, _react.useState)(autoPlay);
67
86
 
68
87
  // Flatten children so we can count items
69
88
  const items = (0, _react.useMemo)(() => _react.default.Children.toArray(children).filter(_react.default.isValidElement), [children]);
70
89
  const totalItems = items.length;
71
90
 
72
- // Effective item width: provided prop, or full container width minus peek offsets
91
+ // Effective item width: provided prop, or full container width minus peek offsets.
92
+ // For the Numbered variant the items are full-bleed (no peek padding).
73
93
  const effectiveItemWidth = (0, _react.useMemo)(() => {
74
94
  if (itemWidthProp != null) return itemWidthProp;
75
95
  if (containerWidth === 0) return 0;
96
+ if (isNumbered) return containerWidth;
76
97
  // Full-width minus peekOffset on each side
77
98
  return containerWidth - containerPaddingH * 4;
78
- }, [itemWidthProp, containerWidth, containerPaddingH]);
99
+ }, [itemWidthProp, containerWidth, containerPaddingH, isNumbered]);
79
100
 
80
101
  // Snap interval = item width + gap
81
102
  const snapInterval = effectiveItemWidth + gap;
@@ -89,6 +110,17 @@ function Carousel({
89
110
  } else {
90
111
  targetIndex = Math.max(0, Math.min(index, totalItems - 1));
91
112
  }
113
+ // Lock out handleScroll during the animated scroll so intermediate
114
+ // offsets don't flicker activeIndex back to the old value.
115
+ if (animated) {
116
+ programmaticScroll.current = true;
117
+ if (programmaticTimer.current != null) {
118
+ clearTimeout(programmaticTimer.current);
119
+ }
120
+ programmaticTimer.current = setTimeout(() => {
121
+ programmaticScroll.current = false;
122
+ }, 500);
123
+ }
92
124
  scrollRef.current?.scrollTo({
93
125
  x: targetIndex * snapInterval,
94
126
  animated
@@ -103,6 +135,9 @@ function Carousel({
103
135
  // ---- Scroll event handler ----
104
136
  const handleScroll = (0, _react.useCallback)(event => {
105
137
  if (snapInterval === 0) return;
138
+ // Skip index recalculation during programmatic scrollTo animations
139
+ // to prevent the active indicator from flickering back to the old page.
140
+ if (programmaticScroll.current) return;
106
141
  const offsetX = event.nativeEvent.contentOffset.x;
107
142
  const newIndex = Math.round(offsetX / snapInterval);
108
143
  const clampedIndex = Math.max(0, Math.min(newIndex, totalItems - 1));
@@ -120,22 +155,19 @@ function Carousel({
120
155
  }
121
156
  }, []);
122
157
  const startAutoPlay = (0, _react.useCallback)(() => {
123
- if (!autoPlay || totalItems <= 1) return;
158
+ if (!autoPlay && !isPlaying || totalItems <= 1) return;
159
+ if (!isPlaying) return;
124
160
  clearAutoPlay();
125
161
  autoPlayTimer.current = setInterval(() => {
126
162
  if (!userInteracting.current) {
127
- setActiveIndex(prev => {
128
- const next = loop ? (prev + 1) % totalItems : Math.min(prev + 1, totalItems - 1);
129
- scrollRef.current?.scrollTo({
130
- x: next * snapInterval,
131
- animated: true
132
- });
133
- onIndexChange?.(next);
134
- return next;
135
- });
163
+ const prev = activeIndexRef.current;
164
+ const next = loop ? (prev + 1) % totalItems : Math.min(prev + 1, totalItems - 1);
165
+ // Route through scrollToIndex so the programmaticScroll guard
166
+ // suppresses intermediate handleScroll offsets during the animation.
167
+ scrollToIndex(next, true);
136
168
  }
137
169
  }, autoPlayInterval);
138
- }, [autoPlay, totalItems, loop, snapInterval, autoPlayInterval, clearAutoPlay, onIndexChange]);
170
+ }, [autoPlay, isPlaying, totalItems, loop, autoPlayInterval, clearAutoPlay, scrollToIndex]);
139
171
  (0, _react.useEffect)(() => {
140
172
  startAutoPlay();
141
173
  return clearAutoPlay;
@@ -146,8 +178,23 @@ function Carousel({
146
178
  const handleScrollEndDrag = (0, _react.useCallback)(() => {
147
179
  userInteracting.current = false;
148
180
  // Reset autoplay timer after interaction
149
- if (autoPlay) startAutoPlay();
150
- }, [autoPlay, startAutoPlay]);
181
+ if (autoPlay || isPlaying) startAutoPlay();
182
+ }, [autoPlay, isPlaying, startAutoPlay]);
183
+ const finishProgrammaticScroll = (0, _react.useCallback)(event => {
184
+ programmaticScroll.current = false;
185
+ if (programmaticTimer.current != null) {
186
+ clearTimeout(programmaticTimer.current);
187
+ programmaticTimer.current = null;
188
+ }
189
+ if (snapInterval === 0) return;
190
+ const offsetX = event.nativeEvent.contentOffset.x;
191
+ const newIndex = Math.round(offsetX / snapInterval);
192
+ const clampedIndex = Math.max(0, Math.min(newIndex, totalItems - 1));
193
+ if (clampedIndex !== activeIndexRef.current) {
194
+ setActiveIndex(clampedIndex);
195
+ onIndexChange?.(clampedIndex);
196
+ }
197
+ }, [snapInterval, totalItems, onIndexChange]);
151
198
 
152
199
  // ---- Layout ----
153
200
  const handleLayout = (0, _react.useCallback)(e => {
@@ -164,14 +211,37 @@ function Carousel({
164
211
  goPrev
165
212
  }), [modes, activeIndex, totalItems, goTo, goNext, goPrev]);
166
213
 
214
+ // ---- Toggle handler for Numbered variant ----
215
+ const handleToggleAutoplay = (0, _react.useCallback)(() => {
216
+ setIsPlaying(prev => {
217
+ if (prev) {
218
+ // Pausing
219
+ clearAutoPlay();
220
+ return false;
221
+ }
222
+ // Resuming — autoplay will restart via the effect
223
+ return true;
224
+ });
225
+ }, [clearAutoPlay]);
226
+
227
+ // Restart autoplay whenever isPlaying flips to true
228
+ (0, _react.useEffect)(() => {
229
+ if (isPlaying) startAutoPlay();else clearAutoPlay();
230
+ }, [isPlaying, startAutoPlay, clearAutoPlay]);
231
+
167
232
  // ---- Render ----
168
233
  // Explicit padding props win verbatim; otherwise fall back to the
169
234
  // token-derived values (horizontal keeps its legacy ×2 "peek" multiplier).
170
- const effectivePaddingH = paddingHorizontalProp ?? containerPaddingH;
171
- const effectivePaddingV = paddingVerticalProp ?? containerPaddingV;
235
+ // Numbered variant is full-bleed: zero padding.
236
+ const effectivePaddingH = isNumbered ? 0 : paddingHorizontalProp ?? containerPaddingH;
237
+ const effectivePaddingV = isNumbered ? 0 : paddingVerticalProp ?? containerPaddingV;
172
238
  const outerStyle = {
173
239
  paddingVertical: effectivePaddingV,
174
- maxHeight
240
+ maxHeight: isNumbered ? undefined : maxHeight,
241
+ // Numbered needs relative positioning for the floating controls overlay.
242
+ ...(isNumbered ? {
243
+ overflow: 'hidden'
244
+ } : {})
175
245
  };
176
246
  const contentContainerStyle = {
177
247
  paddingHorizontal: effectivePaddingH,
@@ -197,6 +267,7 @@ function Carousel({
197
267
  scrollEventThrottle: 16,
198
268
  onScrollBeginDrag: handleScrollBeginDrag,
199
269
  onScrollEndDrag: handleScrollEndDrag,
270
+ onMomentumScrollEnd: finishProgrammaticScroll,
200
271
  children: items.map((child, index) => {
201
272
  // Strict slot box: width must be honored; never grow or shrink with
202
273
  // content, and clip anything that misbehaves (e.g. a child whose
@@ -233,11 +304,33 @@ function Carousel({
233
304
  children: childWithModes
234
305
  }, index);
235
306
  })
236
- }), showPagination && totalItems > 1 && /*#__PURE__*/(0, _jsxRuntime.jsx)(Pagination, {
307
+ }), !isNumbered && showPagination && totalItems > 1 && /*#__PURE__*/(0, _jsxRuntime.jsx)(Pagination, {
237
308
  modes: modes,
238
309
  style: {
239
310
  marginTop: paginationOffset
240
311
  }
312
+ }), isNumbered && showPagination && totalItems > 1 && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
313
+ style: {
314
+ position: 'absolute',
315
+ bottom: 12,
316
+ left: 0,
317
+ right: 0,
318
+ flexDirection: 'row',
319
+ justifyContent: 'center',
320
+ alignItems: 'center',
321
+ gap: controlGap
322
+ },
323
+ pointerEvents: "box-none",
324
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_AutoplayControl.default, {
325
+ state: isPlaying ? 'pause' : 'play',
326
+ onPress: handleToggleAutoplay,
327
+ modes: modes
328
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NumberPagination.default, {
329
+ activePage: activeIndex + 1,
330
+ totalPages: totalItems,
331
+ onPageChange: page => goTo(page - 1),
332
+ modes: modes
333
+ })]
241
334
  })]
242
335
  })
243
336
  });
@@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = void 0;
7
- var _react = _interopRequireDefault(require("react"));
7
+ var _react = _interopRequireWildcard(require("react"));
8
8
  var _reactNative = require("react-native");
9
9
  var _figmaVariablesResolver = require("../../design-tokens/figma-variables-resolver");
10
10
  var _reactUtils = require("../../utils/react-utils");
@@ -15,6 +15,7 @@ var _IconButton = _interopRequireDefault(require("../IconButton/IconButton"));
15
15
  var _Icon = _interopRequireDefault(require("../../icons/Icon"));
16
16
  var _jsxRuntime = require("react/jsx-runtime");
17
17
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
18
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
18
19
  /**
19
20
  * Brand link colour used by the selection-card CTA. The Figma node resolves
20
21
  * `button/foreground` to this value, but no reproducible token mode currently
@@ -111,7 +112,8 @@ function CompareTable({
111
112
  maxColumns = 4,
112
113
  modes = _reactUtils.EMPTY_MODES,
113
114
  style,
114
- disableTruncation
115
+ disableTruncation,
116
+ columnWidth
115
117
  }) {
116
118
  // --- selection card tokens ------------------------------------------------
117
119
  const cardBg = (0, _figmaVariablesResolver.getVariableByName)('selectionCard/background/color', modes) ?? '#ffffff';
@@ -168,6 +170,70 @@ function CompareTable({
168
170
  const showAddCard = onAddColumn != null && columnCount < maxColumns;
169
171
  /** Grid columns shared by the selection-card row and every table row. */
170
172
  const gridColumnCount = columnCount + (showAddCard ? 1 : 0);
173
+ const showCardsRow = columnCount > 0 || showAddCard;
174
+
175
+ // --- fixed-width scroll mode -------------------------------------------
176
+ // `columnWidth` opts the component into scroll mode (see prop doc). In that
177
+ // mode the cards row and each section's table body are independent
178
+ // horizontal ScrollViews; their scroll events are chained so they move in
179
+ // lockstep, and the cards row pins to the top on the vertical axis.
180
+ const useFixedWidth = columnWidth != null;
181
+ /** Total scrollable content width shared by every horizontal surface. */
182
+ const contentWidth = useFixedWidth ? gridColumnCount * columnWidth : 0;
183
+ const cardsScrollRef = (0, _react.useRef)(null);
184
+ const bodyScrollRefs = (0, _react.useRef)(new Map());
185
+ // Stable per-section ref-callback instances so React doesn't detach/reattach
186
+ // on every render (which would churn the map and re-fire scrollTo).
187
+ const bodyRefCallbacks = (0, _react.useRef)(new Map());
188
+ /** Last offset we synced everyone to. Echoed onScroll events reporting this
189
+ * same value are ignored — this is what prevents feedback loops. */
190
+ const lastSyncX = (0, _react.useRef)(0);
191
+ const applySync = (0, _react.useCallback)((x, source) => {
192
+ if (Math.abs(x - lastSyncX.current) < 0.5) return;
193
+ lastSyncX.current = x;
194
+ if (source !== 'cards' && cardsScrollRef.current) {
195
+ cardsScrollRef.current.scrollTo({
196
+ x,
197
+ animated: false
198
+ });
199
+ }
200
+ bodyScrollRefs.current.forEach((ref, idx) => {
201
+ if (source === 'cards' || idx !== source) {
202
+ ref.scrollTo({
203
+ x,
204
+ animated: false
205
+ });
206
+ }
207
+ });
208
+ }, []);
209
+ const onCardsScroll = (0, _react.useCallback)(e => {
210
+ applySync(e.nativeEvent.contentOffset.x, 'cards');
211
+ }, [applySync]);
212
+ const onBodyScroll = (0, _react.useCallback)(idx => e => {
213
+ applySync(e.nativeEvent.contentOffset.x, idx);
214
+ }, [applySync]);
215
+ const getBodyRefCallback = (0, _react.useCallback)(idx => {
216
+ let cb = bodyRefCallbacks.current.get(idx);
217
+ if (!cb) {
218
+ cb = ref => {
219
+ if (ref) {
220
+ bodyScrollRefs.current.set(idx, ref);
221
+ // Bring a freshly-expanded section's body into sync with
222
+ // the rest of the surfaces.
223
+ if (lastSyncX.current > 0) {
224
+ ref.scrollTo({
225
+ x: lastSyncX.current,
226
+ animated: false
227
+ });
228
+ }
229
+ } else {
230
+ bodyScrollRefs.current.delete(idx);
231
+ }
232
+ };
233
+ bodyRefCallbacks.current.set(idx, cb);
234
+ }
235
+ return cb;
236
+ }, []);
171
237
  const cardLabelStyle = {
172
238
  ...NO_WRAP_TEXT,
173
239
  color: cardLabelColor,
@@ -198,8 +264,12 @@ function CompareTable({
198
264
  fontWeight: toWeight(cellFontWeight)
199
265
  };
200
266
  const cardStyle = {
201
- flex: 1,
202
- minWidth: 0,
267
+ ...(useFixedWidth ? {
268
+ width: columnWidth
269
+ } : {
270
+ flex: 1,
271
+ minWidth: 0
272
+ }),
203
273
  minHeight: 147,
204
274
  backgroundColor: cardBg,
205
275
  borderRadius: cardRadius,
@@ -318,6 +388,7 @@ function CompareTable({
318
388
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_Table.default.Cell, {
319
389
  modes: modes,
320
390
  flex: 1,
391
+ width: useFixedWidth ? columnWidth : undefined,
321
392
  align: "left",
322
393
  isLastColumn: isLastCol,
323
394
  isLastRow: isLastRow,
@@ -371,31 +442,126 @@ function CompareTable({
371
442
  })
372
443
  })]
373
444
  });
374
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
375
- style: [{
376
- width: '100%',
377
- backgroundColor: cardBg,
378
- borderWidth: 1,
379
- borderColor: '#e8e8e8',
380
- overflow: 'hidden'
381
- }, style],
382
- children: [(columnCount > 0 || showAddCard) && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
445
+
446
+ // Render a single section in fixed-width scroll mode. Unlike `renderTable`
447
+ // (used in flex mode), the gray header row is rendered full-width OUTSIDE
448
+ // the body's horizontal ScrollView so it stays put horizontally (it does
449
+ // not slide with the cells) while still scrolling vertically with the table.
450
+ // The body rows live inside a horizontal ScrollView that is scroll-synced
451
+ // with the cards row and every other section's body.
452
+ const renderSection = (section, index) => /*#__PURE__*/(0, _jsxRuntime.jsxs)(_Accordion.default, {
453
+ title: section.title,
454
+ defaultExpanded: section.defaultExpanded ?? index === 0,
455
+ modes: modes,
456
+ children: [section.header != null && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
383
457
  style: {
384
- flexDirection: 'row',
385
- width: '100%'
458
+ width: '100%',
459
+ backgroundColor: headerBg,
460
+ paddingHorizontal: headerPaddingH,
461
+ paddingVertical: headerPaddingV,
462
+ borderBottomWidth: cellBorderSize,
463
+ borderBottomColor: cellBorderColor
386
464
  },
387
- children: [columns.map(renderCard), showAddCard && renderAddCard()]
388
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
465
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
466
+ style: headerTextStyle,
467
+ children: section.header
468
+ })
469
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
470
+ ref: getBodyRefCallback(index),
471
+ horizontal: true,
472
+ showsHorizontalScrollIndicator: false,
473
+ scrollEventThrottle: 16,
474
+ onScroll: onBodyScroll(index),
475
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
476
+ style: {
477
+ width: contentWidth
478
+ },
479
+ children: section.rows.map((row, rowIndex) => {
480
+ const isLastRow = rowIndex === section.rows.length - 1;
481
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
482
+ style: {
483
+ flexDirection: 'row',
484
+ width: '100%'
485
+ },
486
+ children: [Array.from({
487
+ length: columnCount
488
+ }).map((_, colIndex) => renderTableCell(renderCellContent(row.values?.[colIndex], `${rowIndex}-${colIndex}`), colIndex, isLastRow, colIndex)), showAddCard && renderTableCell(null, gridColumnCount - 1, isLastRow, '__add_spacer__')]
489
+ }, row.key ?? rowIndex);
490
+ })
491
+ })
492
+ })]
493
+ }, section.key ?? section.title ?? index);
494
+ const outerStyle = {
495
+ width: '100%',
496
+ backgroundColor: cardBg,
497
+ borderWidth: 1,
498
+ borderColor: '#e8e8e8',
499
+ overflow: 'hidden'
500
+ };
501
+
502
+ // --- flex mode (columnWidth omitted): original behaviour, unchanged -------
503
+ if (!useFixedWidth) {
504
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
505
+ style: [outerStyle, style],
506
+ children: [showCardsRow && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
507
+ style: {
508
+ flexDirection: 'row',
509
+ width: '100%'
510
+ },
511
+ children: [columns.map(renderCard), showAddCard && renderAddCard()]
512
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
513
+ style: {
514
+ width: '100%'
515
+ },
516
+ children: sections.map((section, index) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_Accordion.default, {
517
+ title: section.title,
518
+ defaultExpanded: section.defaultExpanded ?? index === 0,
519
+ modes: modes,
520
+ children: renderTable(section)
521
+ }, section.key ?? section.title ?? index))
522
+ })]
523
+ });
524
+ }
525
+
526
+ // --- fixed-width scroll mode: chained horizontal sync + sticky cards -----
527
+ // The cards row (index 0) is a sticky header of the vertical ScrollView so
528
+ // it pins to the top while the sections scroll beneath. The cards row is
529
+ // itself a horizontal ScrollView, synced with every section body. For the
530
+ // vertical scroll (and thus the sticky behaviour) to engage, the host must
531
+ // give CompareTable a bounded height (e.g. a sized container or flex:1).
532
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
533
+ style: [outerStyle, style, {
534
+ flexDirection: 'column',
535
+ flex: 1
536
+ }],
537
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.ScrollView, {
389
538
  style: {
390
- width: '100%'
539
+ width: '100%',
540
+ flex: 1
391
541
  },
392
- children: sections.map((section, index) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_Accordion.default, {
393
- title: section.title,
394
- defaultExpanded: section.defaultExpanded ?? index === 0,
395
- modes: modes,
396
- children: renderTable(section)
397
- }, section.key ?? section.title ?? index))
398
- })]
542
+ stickyHeaderIndices: showCardsRow ? [0] : [],
543
+ showsVerticalScrollIndicator: false,
544
+ directionalLockEnabled: true,
545
+ children: [showCardsRow && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
546
+ ref: cardsScrollRef,
547
+ horizontal: true,
548
+ showsHorizontalScrollIndicator: false,
549
+ scrollEventThrottle: 16,
550
+ onScroll: onCardsScroll,
551
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
552
+ style: {
553
+ flexDirection: 'row',
554
+ width: contentWidth
555
+ },
556
+ children: [columns.map(renderCard), showAddCard && renderAddCard()]
557
+ })
558
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
559
+ style: {
560
+ width: '100%'
561
+ },
562
+ children: sections.map(renderSection)
563
+ })]
564
+ })
399
565
  });
400
566
  }
401
567
  var _default = exports.default = CompareTable;