jfs-components 0.1.25 → 0.1.28

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 (81) hide show
  1. package/CHANGELOG.md +40 -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/ContentSheet/ContentSheet.js +39 -5
  5. package/lib/commonjs/components/FormUpload/Additem.js +168 -0
  6. package/lib/commonjs/components/FormUpload/FormUpload.js +198 -0
  7. package/lib/commonjs/components/Grid/Grid.js +206 -0
  8. package/lib/commonjs/components/NumberPagination/NumberPagination.js +144 -0
  9. package/lib/commonjs/components/ProfileCard/ProfileCard.js +208 -0
  10. package/lib/commonjs/components/UpiHandle/UpiHandle.js +23 -11
  11. package/lib/commonjs/components/index.js +42 -0
  12. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  13. package/lib/commonjs/design-tokens/figma-modes.generated.js +21 -10
  14. package/lib/commonjs/icons/components/IconBrokenimage.js +19 -0
  15. package/lib/commonjs/icons/components/IconImage.js +19 -0
  16. package/lib/commonjs/icons/components/IconMic.js +19 -0
  17. package/lib/commonjs/icons/components/IconMicoff.js +19 -0
  18. package/lib/commonjs/icons/components/IconPause.js +19 -0
  19. package/lib/commonjs/icons/components/IconPlay.js +19 -0
  20. package/lib/commonjs/icons/components/index.js +66 -0
  21. package/lib/commonjs/icons/registry.js +2 -2
  22. package/lib/module/components/AutoplayControl/AutoplayControl.js +60 -0
  23. package/lib/module/components/Carousel/Carousel.js +111 -19
  24. package/lib/module/components/ContentSheet/ContentSheet.js +41 -7
  25. package/lib/module/components/FormUpload/Additem.js +162 -0
  26. package/lib/module/components/FormUpload/FormUpload.js +192 -0
  27. package/lib/module/components/Grid/Grid.js +201 -0
  28. package/lib/module/components/NumberPagination/NumberPagination.js +138 -0
  29. package/lib/module/components/ProfileCard/ProfileCard.js +202 -0
  30. package/lib/module/components/UpiHandle/UpiHandle.js +23 -11
  31. package/lib/module/components/index.js +7 -1
  32. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  33. package/lib/module/design-tokens/figma-modes.generated.js +21 -10
  34. package/lib/module/icons/components/IconBrokenimage.js +12 -0
  35. package/lib/module/icons/components/IconImage.js +12 -0
  36. package/lib/module/icons/components/IconMic.js +12 -0
  37. package/lib/module/icons/components/IconMicoff.js +12 -0
  38. package/lib/module/icons/components/IconPause.js +12 -0
  39. package/lib/module/icons/components/IconPlay.js +12 -0
  40. package/lib/module/icons/components/index.js +6 -0
  41. package/lib/module/icons/registry.js +2 -2
  42. package/lib/typescript/src/components/AutoplayControl/AutoplayControl.d.ts +15 -0
  43. package/lib/typescript/src/components/Carousel/Carousel.d.ts +8 -1
  44. package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +7 -1
  45. package/lib/typescript/src/components/FormUpload/Additem.d.ts +96 -0
  46. package/lib/typescript/src/components/FormUpload/FormUpload.d.ts +85 -0
  47. package/lib/typescript/src/components/Grid/Grid.d.ts +87 -0
  48. package/lib/typescript/src/components/NumberPagination/NumberPagination.d.ts +21 -0
  49. package/lib/typescript/src/components/ProfileCard/ProfileCard.d.ts +89 -0
  50. package/lib/typescript/src/components/UpiHandle/UpiHandle.d.ts +7 -1
  51. package/lib/typescript/src/components/index.d.ts +34 -28
  52. package/lib/typescript/src/design-tokens/figma-modes.generated.d.ts +10 -4
  53. package/lib/typescript/src/icons/components/IconBrokenimage.d.ts +3 -0
  54. package/lib/typescript/src/icons/components/IconImage.d.ts +3 -0
  55. package/lib/typescript/src/icons/components/IconMic.d.ts +3 -0
  56. package/lib/typescript/src/icons/components/IconMicoff.d.ts +3 -0
  57. package/lib/typescript/src/icons/components/IconPause.d.ts +3 -0
  58. package/lib/typescript/src/icons/components/IconPlay.d.ts +3 -0
  59. package/lib/typescript/src/icons/components/index.d.ts +6 -0
  60. package/lib/typescript/src/icons/registry.d.ts +1 -1
  61. package/package.json +1 -1
  62. package/src/components/AutoplayControl/AutoplayControl.tsx +77 -0
  63. package/src/components/Carousel/Carousel.tsx +133 -23
  64. package/src/components/ContentSheet/ContentSheet.tsx +50 -0
  65. package/src/components/FormUpload/Additem.tsx +262 -0
  66. package/src/components/FormUpload/FormUpload.tsx +313 -0
  67. package/src/components/Grid/Grid.tsx +368 -0
  68. package/src/components/NumberPagination/NumberPagination.tsx +170 -0
  69. package/src/components/ProfileCard/ProfileCard.tsx +268 -0
  70. package/src/components/UpiHandle/UpiHandle.tsx +35 -24
  71. package/src/components/index.ts +329 -182
  72. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  73. package/src/design-tokens/figma-modes.generated.ts +21 -10
  74. package/src/icons/components/IconBrokenimage.tsx +11 -0
  75. package/src/icons/components/IconImage.tsx +11 -0
  76. package/src/icons/components/IconMic.tsx +11 -0
  77. package/src/icons/components/IconMicoff.tsx +11 -0
  78. package/src/icons/components/IconPause.tsx +11 -0
  79. package/src/icons/components/IconPlay.tsx +11 -0
  80. package/src/icons/components/index.ts +6 -0
  81. package/src/icons/registry.ts +25 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,46 @@ 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.28] - 2026-07-07
8
+
9
+ - 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.
10
+ - 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).
11
+ - 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.
12
+ - `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.
13
+ - Added `ic_broken_image`, `ic_image`, `ic_mic`, and `ic_mic_off` icons to the icon registry.
14
+ - Regenerated component MDX token documentation and `.token-metadata.json` across the library.
15
+
16
+ ---
17
+
18
+ ## [0.1.27] - 2026-07-07
19
+
20
+ - Added `ProfileCard` — dark rounded profile summary card with `Attached` monogram `Avatar`, optional badge, name + `UpiHandle` pill row, and a body slot; `Profile Card Appearance` mode (`Default` | `Premium`) recolors the whole card via `modes`.
21
+ - `Carousel` — new `type="Numbered"` variant: full-bleed slides with floating `AutoplayControl` + `NumberPagination` overlay at the bottom center; interactive play/pause state wired to autoplay.
22
+ - `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.
23
+ - `UpiHandle` — **Fix:** avatar image visibility now follows the presence of `source` / `avatarSource` (`showImage` no longer renders an empty avatar slot when no source is provided); `UPI Handle Image` mode is force-set from the resolved source so token layout stays in sync.
24
+ - Regenerated component MDX token documentation and `.token-metadata.json` across the library.
25
+
26
+ ---
27
+
28
+ ## [0.1.26] - 2026-07-07
29
+
30
+ - Added `AutoplayControl` — token-driven play/pause toggle button for carousel autoplay (`autoplayControl/*` tokens; `ic_play` / `ic_pause` icons).
31
+ - Added `NumberPagination` — frosted-glass page-number strip with active/inactive control states (`numberPagination/*`, `controlNumber/*`, `control/*` tokens); supports `children` slot override.
32
+ - Added `ic_play` and `ic_pause` icons to the icon registry.
33
+ - Regenerated component MDX token documentation and `.token-metadata.json` across the library.
34
+
35
+ ---
36
+
37
+ ## [0.1.25] - 2026-07-03
38
+
39
+ - 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`).
40
+ - 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.
41
+ - `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.
42
+ - `ChipSelect` — new `showCloseIcon` prop (default `true`); set `showCloseIcon={false}` to hide the close icon in the active state.
43
+ - `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.
44
+
45
+ ---
46
+
7
47
  ## [0.1.23] - 2026-07-01
8
48
 
9
49
  - 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
  });
@@ -21,6 +21,18 @@ function resolveContentSheetStyle(modes) {
21
21
  const paddingBottom = (0, _figmaVariablesResolver.getVariableByName)('contentSheet/padding/bottom', modes);
22
22
  const radiusTop = (0, _figmaVariablesResolver.getVariableByName)('contentSheet/padding/radius/top', modes);
23
23
  const radiusBottom = (0, _figmaVariablesResolver.getVariableByName)('contentSheet/padding/radius/bottom', modes);
24
+
25
+ // Title tokens mirror the Drawer title so the two bottom-sheet surfaces stay
26
+ // visually aligned.
27
+ const titleColor = (0, _figmaVariablesResolver.getVariableByName)('drawer/title/color', modes);
28
+ const titleSize = (0, _figmaVariablesResolver.getVariableByName)('drawer/title/fontSize', modes);
29
+ const titleLineHeight = (0, _figmaVariablesResolver.getVariableByName)('drawer/title/lineHeight', modes);
30
+ const titleFontFamily = (0, _figmaVariablesResolver.getVariableByName)('drawer/title/fontFamily', modes);
31
+ const titleFontWeightRaw = (0, _figmaVariablesResolver.getVariableByName)('drawer/title/fontWeight', modes);
32
+ const titleFontWeight = typeof titleFontWeightRaw === 'number' ? titleFontWeightRaw.toString() : titleFontWeightRaw;
33
+ const titleWrapPaddingTop = (0, _figmaVariablesResolver.getVariableByName)('drawer/titleWrap/padding/top', modes);
34
+ const titleWrapPaddingBottom = (0, _figmaVariablesResolver.getVariableByName)('drawer/titleWrap/padding/bottom', modes);
35
+ const titleWrapPaddingHorizontal = (0, _figmaVariablesResolver.getVariableByName)('drawer/titleWrap/padding/horizontal', modes);
24
36
  return {
25
37
  container: {
26
38
  backgroundColor,
@@ -36,7 +48,21 @@ function resolveContentSheetStyle(modes) {
36
48
  alignItems: 'stretch',
37
49
  overflow: 'hidden'
38
50
  },
39
- paddingBottom
51
+ paddingBottom,
52
+ headerStyle: {
53
+ paddingTop: titleWrapPaddingTop,
54
+ paddingBottom: titleWrapPaddingBottom,
55
+ paddingHorizontal: titleWrapPaddingHorizontal,
56
+ alignItems: 'center'
57
+ },
58
+ titleStyle: {
59
+ color: titleColor,
60
+ fontSize: titleSize,
61
+ lineHeight: titleLineHeight,
62
+ fontFamily: titleFontFamily,
63
+ fontWeight: titleFontWeight,
64
+ textAlign: 'center'
65
+ }
40
66
  };
41
67
  }
42
68
  const pinnedStyle = {
@@ -65,6 +91,7 @@ const pinnedStyle = {
65
91
  function ContentSheet({
66
92
  children,
67
93
  modes = _reactUtils.EMPTY_MODES,
94
+ title,
68
95
  avoidKeyboard = true,
69
96
  keyboardSpacing = 0,
70
97
  safeAreaBottom = true,
@@ -88,18 +115,25 @@ function ContentSheet({
88
115
  // `avoidKeyboard` (and platform). Because they are distinct component types,
89
116
  // React remounts on toggle, so the conditional `useAnimatedKeyboard` hook
90
117
  // inside `KeyboardAwareSheet` always runs in a stable hook order.
118
+ const header = title ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
119
+ style: resolved.headerStyle,
120
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
121
+ style: resolved.titleStyle,
122
+ children: title
123
+ })
124
+ }) : null;
91
125
  if (avoidKeyboard && !IS_WEB) {
92
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(KeyboardAwareSheet, {
126
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(KeyboardAwareSheet, {
93
127
  style: containerStyle,
94
128
  spacing: keyboardSpacing,
95
129
  ...rest,
96
- children: processedChildren
130
+ children: [header, processedChildren]
97
131
  });
98
132
  }
99
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
133
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
100
134
  style: containerStyle,
101
135
  ...rest,
102
- children: processedChildren
136
+ children: [header, processedChildren]
103
137
  });
104
138
  }
105
139
  /**
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _react = _interopRequireWildcard(require("react"));
8
+ var _reactNative = require("react-native");
9
+ var _figmaVariablesResolver = require("../../design-tokens/figma-variables-resolver");
10
+ var _JFSThemeProvider = require("../../design-tokens/JFSThemeProvider");
11
+ var _reactUtils = require("../../utils/react-utils");
12
+ var _Image = _interopRequireDefault(require("../Image/Image"));
13
+ var _IconCapsule = _interopRequireDefault(require("../IconCapsule/IconCapsule"));
14
+ var _jsxRuntime = require("react/jsx-runtime");
15
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
16
+ 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); }
17
+ /**
18
+ * Callback that opens a platform picker and resolves with the selected assets.
19
+ * This is the integration point for libraries like `react-native-image-picker`,
20
+ * `expo-image-picker`, or `react-native-document-picker`.
21
+ */
22
+
23
+ function toNumber(value, fallback) {
24
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
25
+ if (typeof value === 'string') {
26
+ const parsed = parseFloat(value);
27
+ if (Number.isFinite(parsed)) return parsed;
28
+ }
29
+ return fallback;
30
+ }
31
+ function resolveAdditemTokens(modes) {
32
+ const backgroundColor = (0, _figmaVariablesResolver.getVariableByName)('compareCardItem/background', modes);
33
+ const borderRadius = toNumber((0, _figmaVariablesResolver.getVariableByName)('compareCardItem/radius', modes), 8);
34
+ const paddingHorizontal = toNumber((0, _figmaVariablesResolver.getVariableByName)('compareCardItem/padding/horizontal', modes), 6);
35
+ const paddingVertical = toNumber((0, _figmaVariablesResolver.getVariableByName)('compareCardItem/padding/vertical', modes), 8);
36
+ // The Figma design uses a fixed 44 × 44 dp cell. There is no
37
+ // `compareCardItem/size` token in the variables JSON, so we keep the size
38
+ // constant to match the design exactly.
39
+ const size = 44;
40
+ return {
41
+ backgroundColor,
42
+ borderRadius,
43
+ paddingHorizontal,
44
+ paddingVertical,
45
+ size
46
+ };
47
+ }
48
+
49
+ /** Figma defaults for the inner `IconCapsule` icons. Overridable via `modes`. */
50
+ const ADDITEM_DEFAULT_MODES = Object.freeze({
51
+ AppearanceBrand: 'Neutral',
52
+ Emphasis: 'Low',
53
+ 'Icon Capsule Size': 'S'
54
+ });
55
+
56
+ /**
57
+ * `Additem` — a small, reusable upload/preview cell used inside form uploaders.
58
+ *
59
+ * In its default `'empty'` state it shows a centered add/upload icon. In
60
+ * `'preview'` state it renders a thumbnail with a removable overlay. The
61
+ * component is intentionally unopinionated about *how* files are picked: pass a
62
+ * `picker` callback that wraps `react-native-image-picker`,
63
+ * `expo-image-picker`, `react-native-document-picker`, or any custom selector,
64
+ * and the cell will invoke it, surface the result via `onAssetsPicked`, and
65
+ * handle cancellation/errors gracefully.
66
+ */
67
+ function Additem({
68
+ state = 'empty',
69
+ imageSource,
70
+ onPress,
71
+ onRemove,
72
+ picker,
73
+ onAssetsPicked,
74
+ modes: propModes = _reactUtils.EMPTY_MODES,
75
+ accessibilityLabel,
76
+ isDisabled = false,
77
+ testID
78
+ }) {
79
+ const {
80
+ modes: globalModes
81
+ } = (0, _JFSThemeProvider.useTokens)();
82
+ const modes = (0, _react.useMemo)(() => globalModes === _reactUtils.EMPTY_MODES && propModes === _reactUtils.EMPTY_MODES ? ADDITEM_DEFAULT_MODES : {
83
+ ...ADDITEM_DEFAULT_MODES,
84
+ ...globalModes,
85
+ ...propModes
86
+ }, [globalModes, propModes]);
87
+ const tokens = (0, _react.useMemo)(() => resolveAdditemTokens(modes), [modes]);
88
+ const handlePress = _react.default.useCallback(async () => {
89
+ if (isDisabled) return;
90
+ if (state === 'empty' && picker) {
91
+ try {
92
+ const result = await picker();
93
+ if (result.didCancel) return;
94
+ if (result.error) {
95
+ if (__DEV__) {
96
+ console.warn('[Additem] picker error:', result.error);
97
+ }
98
+ return;
99
+ }
100
+ if (result.assets.length > 0) {
101
+ onAssetsPicked?.(result.assets);
102
+ }
103
+ } catch (err) {
104
+ if (__DEV__) {
105
+ console.warn('[Additem] unexpected picker error:', err);
106
+ }
107
+ }
108
+ return;
109
+ }
110
+ onPress?.();
111
+ }, [isDisabled, onPress, onAssetsPicked, picker, state]);
112
+ const handleRemove = _react.default.useCallback(() => {
113
+ if (!isDisabled) {
114
+ onRemove?.();
115
+ }
116
+ }, [isDisabled, onRemove]);
117
+ const resolvedAccessibilityLabel = accessibilityLabel ?? (state === 'empty' ? 'Add attachment' : 'Attachment preview');
118
+ const containerStyle = (0, _react.useMemo)(() => ({
119
+ width: tokens.size,
120
+ height: tokens.size,
121
+ borderRadius: tokens.borderRadius,
122
+ backgroundColor: tokens.backgroundColor,
123
+ paddingHorizontal: tokens.paddingHorizontal,
124
+ paddingVertical: tokens.paddingVertical,
125
+ alignItems: 'center',
126
+ justifyContent: 'center',
127
+ overflow: 'hidden',
128
+ opacity: isDisabled ? 0.5 : 1
129
+ }), [tokens, isDisabled]);
130
+ const isPreview = state === 'preview';
131
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
132
+ onPress: handlePress,
133
+ disabled: isDisabled,
134
+ accessibilityRole: "button",
135
+ accessibilityLabel: resolvedAccessibilityLabel,
136
+ accessibilityState: {
137
+ disabled: isDisabled
138
+ },
139
+ testID: testID,
140
+ style: containerStyle,
141
+ children: isPreview ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
142
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_Image.default, {
143
+ imageSource: imageSource,
144
+ width: "100%",
145
+ height: tokens.size - tokens.paddingVertical * 2,
146
+ resizeMode: "cover",
147
+ borderRadius: tokens.borderRadius - 2
148
+ }), onRemove != null && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
149
+ style: {
150
+ position: 'absolute',
151
+ top: 2,
152
+ right: 2
153
+ },
154
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_IconCapsule.default, {
155
+ iconName: "ic_close",
156
+ modes: modes,
157
+ onTouchEnd: handleRemove,
158
+ accessibilityRole: "button",
159
+ accessibilityLabel: "Remove attachment"
160
+ })
161
+ })]
162
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_IconCapsule.default, {
163
+ iconName: "ic_add",
164
+ modes: modes
165
+ })
166
+ });
167
+ }
168
+ var _default = exports.default = /*#__PURE__*/_react.default.memo(Additem);