jfs-components 0.1.23 → 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.
- package/CHANGELOG.md +40 -0
- package/lib/commonjs/components/AutoplayControl/AutoplayControl.js +66 -0
- package/lib/commonjs/components/CardCTA/CardCTA.js +32 -25
- package/lib/commonjs/components/Carousel/Carousel.js +112 -19
- package/lib/commonjs/components/ChipSelect/ChipSelect.js +2 -1
- package/lib/commonjs/components/ContentSheet/ContentSheet.js +39 -5
- package/lib/commonjs/components/FormUpload/Additem.js +168 -0
- package/lib/commonjs/components/FormUpload/FormUpload.js +198 -0
- package/lib/commonjs/components/Grid/Grid.js +206 -0
- package/lib/commonjs/components/ListItem/ListItem.js +11 -4
- package/lib/commonjs/components/MetricData/MetricData.js +132 -0
- package/lib/commonjs/components/NumberPagination/NumberPagination.js +144 -0
- package/lib/commonjs/components/ProfileCard/ProfileCard.js +208 -0
- package/lib/commonjs/components/Rating/Rating.js +137 -0
- package/lib/commonjs/components/UpiHandle/UpiHandle.js +23 -11
- package/lib/commonjs/components/index.js +42 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/design-tokens/figma-modes.generated.js +23 -10
- package/lib/commonjs/icons/components/IconBrokenimage.js +19 -0
- package/lib/commonjs/icons/components/IconImage.js +19 -0
- package/lib/commonjs/icons/components/IconMic.js +19 -0
- package/lib/commonjs/icons/components/IconMicoff.js +19 -0
- package/lib/commonjs/icons/components/IconPause.js +19 -0
- package/lib/commonjs/icons/components/IconPlay.js +19 -0
- package/lib/commonjs/icons/components/index.js +66 -0
- package/lib/commonjs/icons/registry.js +2 -2
- package/lib/module/components/AutoplayControl/AutoplayControl.js +60 -0
- package/lib/module/components/CardCTA/CardCTA.js +32 -25
- package/lib/module/components/Carousel/Carousel.js +111 -19
- package/lib/module/components/ChipSelect/ChipSelect.js +2 -1
- package/lib/module/components/ContentSheet/ContentSheet.js +41 -7
- package/lib/module/components/FormUpload/Additem.js +162 -0
- package/lib/module/components/FormUpload/FormUpload.js +192 -0
- package/lib/module/components/Grid/Grid.js +201 -0
- package/lib/module/components/ListItem/ListItem.js +11 -4
- package/lib/module/components/MetricData/MetricData.js +127 -0
- package/lib/module/components/NumberPagination/NumberPagination.js +138 -0
- package/lib/module/components/ProfileCard/ProfileCard.js +202 -0
- package/lib/module/components/Rating/Rating.js +132 -0
- package/lib/module/components/UpiHandle/UpiHandle.js +23 -11
- package/lib/module/components/index.js +7 -1
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/design-tokens/figma-modes.generated.js +23 -10
- package/lib/module/icons/components/IconBrokenimage.js +12 -0
- package/lib/module/icons/components/IconImage.js +12 -0
- package/lib/module/icons/components/IconMic.js +12 -0
- package/lib/module/icons/components/IconMicoff.js +12 -0
- package/lib/module/icons/components/IconPause.js +12 -0
- package/lib/module/icons/components/IconPlay.js +12 -0
- package/lib/module/icons/components/index.js +6 -0
- package/lib/module/icons/registry.js +2 -2
- package/lib/typescript/src/components/AutoplayControl/AutoplayControl.d.ts +15 -0
- package/lib/typescript/src/components/Carousel/Carousel.d.ts +8 -1
- package/lib/typescript/src/components/ChipSelect/ChipSelect.d.ts +6 -1
- package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +7 -1
- package/lib/typescript/src/components/FormUpload/Additem.d.ts +96 -0
- package/lib/typescript/src/components/FormUpload/FormUpload.d.ts +85 -0
- package/lib/typescript/src/components/Grid/Grid.d.ts +87 -0
- package/lib/typescript/src/components/MetricData/MetricData.d.ts +53 -0
- package/lib/typescript/src/components/NumberPagination/NumberPagination.d.ts +21 -0
- package/lib/typescript/src/components/ProfileCard/ProfileCard.d.ts +89 -0
- package/lib/typescript/src/components/Rating/Rating.d.ts +30 -0
- package/lib/typescript/src/components/UpiHandle/UpiHandle.d.ts +7 -1
- package/lib/typescript/src/components/index.d.ts +34 -28
- package/lib/typescript/src/design-tokens/figma-modes.generated.d.ts +10 -4
- package/lib/typescript/src/icons/components/IconBrokenimage.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconImage.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconMic.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconMicoff.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconPause.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconPlay.d.ts +3 -0
- package/lib/typescript/src/icons/components/index.d.ts +6 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/AutoplayControl/AutoplayControl.tsx +77 -0
- package/src/components/CardCTA/CardCTA.tsx +30 -15
- package/src/components/Carousel/Carousel.tsx +133 -23
- package/src/components/ChipSelect/ChipSelect.tsx +7 -1
- package/src/components/ContentSheet/ContentSheet.tsx +50 -0
- package/src/components/FormUpload/Additem.tsx +262 -0
- package/src/components/FormUpload/FormUpload.tsx +313 -0
- package/src/components/Grid/Grid.tsx +368 -0
- package/src/components/ListItem/ListItem.tsx +11 -4
- package/src/components/MetricData/MetricData.tsx +185 -0
- package/src/components/NumberPagination/NumberPagination.tsx +170 -0
- package/src/components/ProfileCard/ProfileCard.tsx +268 -0
- package/src/components/Rating/Rating.tsx +174 -0
- package/src/components/UpiHandle/UpiHandle.tsx +35 -24
- package/src/components/index.ts +329 -182
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/design-tokens/figma-modes.generated.ts +23 -10
- package/src/icons/components/IconBrokenimage.tsx +11 -0
- package/src/icons/components/IconImage.tsx +11 -0
- package/src/icons/components/IconMic.tsx +11 -0
- package/src/icons/components/IconMicoff.tsx +11 -0
- package/src/icons/components/IconPause.tsx +11 -0
- package/src/icons/components/IconPlay.tsx +11 -0
- package/src/icons/components/index.ts +6 -0
- 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
|
+
}
|
|
@@ -230,22 +230,39 @@ function CardCTA({
|
|
|
230
230
|
zIndex: 1
|
|
231
231
|
};
|
|
232
232
|
|
|
233
|
-
// NOTE: rightWrap must
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
// the
|
|
239
|
-
//
|
|
240
|
-
// this because
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
233
|
+
// NOTE: rightWrap must implement the design's TRUE `2fr` column.
|
|
234
|
+
//
|
|
235
|
+
// History of this style — two Yoga pitfalls pull in opposite directions:
|
|
236
|
+
//
|
|
237
|
+
// 1. `flex: 2` (=> flexBasis: 0, flexShrink: 1): on Android, Yoga could
|
|
238
|
+
// shrink the wrapper below its padding+IconCapsule width when the left
|
|
239
|
+
// text was long, collapsing the padding and gluing the icon to the
|
|
240
|
+
// text. Browsers hide this because they honor `min-width: auto`; Yoga
|
|
241
|
+
// always treats min-width as 0 unless told otherwise.
|
|
242
|
+
// 2. The previous workaround, `flexBasis: 'auto'` + `flexGrow: 2`, fixed
|
|
243
|
+
// #1 but broke the column ratio: Yoga then sizes the wrapper to its
|
|
244
|
+
// content FIRST (icon + padding) and distributes only the leftover
|
|
245
|
+
// space 3:2. The icon width gets baked in on top of the 2fr share, so
|
|
246
|
+
// the right column ends up much wider than the design's 2fr and the
|
|
247
|
+
// body text wraps early with dead space to its right.
|
|
248
|
+
//
|
|
249
|
+
// Fundamental fix: keep the genuine fr-unit geometry (`flexBasis: 0`,
|
|
250
|
+
// grow 3:2, no shrink) and replace Yoga's missing `min-width: auto` with
|
|
251
|
+
// an explicit computed floor — the resolved IconCapsule size plus the
|
|
252
|
+
// wrapper's own horizontal padding. The column is exactly 2fr whenever
|
|
253
|
+
// space allows and can never collapse below its content.
|
|
254
|
+
const iconCapsuleModes = {
|
|
255
|
+
...modes,
|
|
256
|
+
'IconCapsule / Size': 'M',
|
|
257
|
+
Emphasis: 'Medium',
|
|
258
|
+
AppearanceBrand: 'Secondary'
|
|
259
|
+
};
|
|
260
|
+
const rightWrapMinWidth = ((0, _figmaVariablesResolver.getVariableByName)('iconCapsule/size', iconCapsuleModes) || 0) + (rightPaddingH || 0) * 2;
|
|
245
261
|
const rightWrapStyle = {
|
|
246
262
|
flexGrow: 2,
|
|
247
263
|
flexShrink: 0,
|
|
248
|
-
flexBasis:
|
|
264
|
+
flexBasis: 0,
|
|
265
|
+
minWidth: rightWrapMinWidth,
|
|
249
266
|
paddingHorizontal: rightPaddingH,
|
|
250
267
|
paddingVertical: rightPaddingV,
|
|
251
268
|
alignItems: 'flex-end',
|
|
@@ -499,19 +516,9 @@ function CardCTA({
|
|
|
499
516
|
})]
|
|
500
517
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
501
518
|
style: rightWrapStyle,
|
|
502
|
-
children: iconSlot ? (0, _reactUtils.cloneChildrenWithModes)(iconSlot, {
|
|
503
|
-
...modes,
|
|
504
|
-
'IconCapsule / Size': 'M',
|
|
505
|
-
Emphasis: 'Medium',
|
|
506
|
-
AppearanceBrand: 'Secondary'
|
|
507
|
-
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_IconCapsule.default, {
|
|
519
|
+
children: iconSlot ? (0, _reactUtils.cloneChildrenWithModes)(iconSlot, iconCapsuleModes) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_IconCapsule.default, {
|
|
508
520
|
iconName: iconName,
|
|
509
|
-
modes:
|
|
510
|
-
...modes,
|
|
511
|
-
'IconCapsule / Size': 'M',
|
|
512
|
-
Emphasis: 'Medium',
|
|
513
|
-
AppearanceBrand: 'Secondary'
|
|
514
|
-
}
|
|
521
|
+
modes: iconCapsuleModes
|
|
515
522
|
})
|
|
516
523
|
})]
|
|
517
524
|
});
|
|
@@ -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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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,
|
|
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
|
-
|
|
171
|
-
const
|
|
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
|
});
|
|
@@ -19,6 +19,7 @@ function ChipSelect({
|
|
|
19
19
|
label = 'Date',
|
|
20
20
|
active = false,
|
|
21
21
|
icon = 'ic_calendar_week',
|
|
22
|
+
showCloseIcon = true,
|
|
22
23
|
modes = _reactUtils.EMPTY_MODES,
|
|
23
24
|
style,
|
|
24
25
|
labelSlot,
|
|
@@ -92,7 +93,7 @@ function ChipSelect({
|
|
|
92
93
|
name: icon,
|
|
93
94
|
size: iconSize,
|
|
94
95
|
color: textColor
|
|
95
|
-
}), renderLabel(), active && /*#__PURE__*/(0, _jsxRuntime.jsx)(_Icon.default, {
|
|
96
|
+
}), renderLabel(), active && showCloseIcon && /*#__PURE__*/(0, _jsxRuntime.jsx)(_Icon.default, {
|
|
96
97
|
name: "ic_close",
|
|
97
98
|
size: iconSize,
|
|
98
99
|
color: textColor
|
|
@@ -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.
|
|
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.
|
|
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
|
/**
|