jfs-components 0.1.28 → 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.
- package/CHANGELOG.md +15 -2
- package/lib/commonjs/components/CompareTable/CompareTable.js +191 -25
- package/lib/commonjs/components/ContentSheet/ContentSheet.js +66 -6
- package/lib/commonjs/components/MoneyValue/MoneyValue.js +179 -54
- package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +7 -1
- package/lib/commonjs/components/Table/Table.js +27 -7
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/CompareTable/CompareTable.js +191 -26
- package/lib/module/components/ContentSheet/ContentSheet.js +69 -9
- package/lib/module/components/MoneyValue/MoneyValue.js +182 -57
- package/lib/module/components/PdpCcCard/PdpCcCard.js +7 -1
- package/lib/module/components/Table/Table.js +27 -7
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +21 -1
- package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +14 -1
- package/lib/typescript/src/components/MoneyValue/MoneyValue.d.ts +10 -1
- package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +25 -1
- package/lib/typescript/src/components/Table/Table.d.ts +9 -1
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/CompareTable/CompareTable.tsx +224 -30
- package/src/components/ContentSheet/ContentSheet.tsx +88 -8
- package/src/components/MoneyValue/MoneyValue.tsx +216 -65
- package/src/components/PdpCcCard/PdpCcCard.tsx +36 -1
- package/src/components/Table/Table.tsx +29 -4
- package/src/icons/registry.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ 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
|
+
|
|
7
18
|
## [0.1.28] - 2026-07-07
|
|
8
19
|
|
|
9
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.
|
|
@@ -17,10 +28,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
17
28
|
|
|
18
29
|
## [0.1.27] - 2026-07-07
|
|
19
30
|
|
|
20
|
-
- Added `ProfileCard` — dark rounded profile summary card with `Attached` monogram `Avatar
|
|
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`.
|
|
21
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.
|
|
22
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.
|
|
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
37
|
- Regenerated component MDX token documentation and `.token-metadata.json` across the library.
|
|
25
38
|
|
|
26
39
|
---
|
|
@@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = void 0;
|
|
7
|
-
var _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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
385
|
-
|
|
458
|
+
width: '100%',
|
|
459
|
+
backgroundColor: headerBg,
|
|
460
|
+
paddingHorizontal: headerPaddingH,
|
|
461
|
+
paddingVertical: headerPaddingV,
|
|
462
|
+
borderBottomWidth: cellBorderSize,
|
|
463
|
+
borderBottomColor: cellBorderColor
|
|
386
464
|
},
|
|
387
|
-
children:
|
|
388
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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;
|
|
@@ -13,6 +13,18 @@ var _reactUtils = require("../../utils/react-utils");
|
|
|
13
13
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
14
14
|
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); }
|
|
15
15
|
const IS_WEB = _reactNative.Platform.OS === 'web';
|
|
16
|
+
|
|
17
|
+
// Spring config — mirrors the Drawer's bottom pop-up spring so the two
|
|
18
|
+
// bottom-sheet surfaces share the exact same entrance feel (slight bounce,
|
|
19
|
+
// snappy). Keep in sync with `Drawer.tsx` SPRING_CONFIG.
|
|
20
|
+
const SPRING_CONFIG = {
|
|
21
|
+
damping: 32,
|
|
22
|
+
stiffness: 300,
|
|
23
|
+
mass: 1,
|
|
24
|
+
overshootClamping: false,
|
|
25
|
+
restDisplacementThreshold: 0.1,
|
|
26
|
+
restSpeedThreshold: 0.1
|
|
27
|
+
};
|
|
16
28
|
function resolveContentSheetStyle(modes) {
|
|
17
29
|
const backgroundColor = (0, _figmaVariablesResolver.getVariableByName)('contentSheet/bg', modes);
|
|
18
30
|
const gap = (0, _figmaVariablesResolver.getVariableByName)('contentSheet/gap', modes);
|
|
@@ -96,6 +108,7 @@ function ContentSheet({
|
|
|
96
108
|
keyboardSpacing = 0,
|
|
97
109
|
safeAreaBottom = true,
|
|
98
110
|
pinToBottom = true,
|
|
111
|
+
visible = true,
|
|
99
112
|
style,
|
|
100
113
|
...rest
|
|
101
114
|
}) {
|
|
@@ -111,10 +124,25 @@ function ContentSheet({
|
|
|
111
124
|
paddingBottom
|
|
112
125
|
}, style], [pinToBottom, resolved.container, paddingBottom, style]);
|
|
113
126
|
|
|
127
|
+
// Entrance / exit "pop up from bottom" animation — mirrors the Drawer. The
|
|
128
|
+
// sheet is bottom-anchored, so a positive `translateY` equal to the window
|
|
129
|
+
// height parks it fully below the bottom edge. Springing it back to `0`
|
|
130
|
+
// produces the same bottom pop-up the Drawer uses. The shared value is
|
|
131
|
+
// initialised off-screen so the very first mount pops in (no flash).
|
|
132
|
+
const {
|
|
133
|
+
height: windowHeight
|
|
134
|
+
} = (0, _reactNative.useWindowDimensions)();
|
|
135
|
+
const entrance = (0, _reactNativeReanimated.useSharedValue)(windowHeight);
|
|
136
|
+
(0, _react.useEffect)(() => {
|
|
137
|
+
entrance.value = (0, _reactNativeReanimated.withSpring)(visible ? 0 : windowHeight, SPRING_CONFIG);
|
|
138
|
+
}, [visible, windowHeight, entrance]);
|
|
139
|
+
|
|
114
140
|
// Switching between the keyboard-aware and plain implementation is keyed off
|
|
115
141
|
// `avoidKeyboard` (and platform). Because they are distinct component types,
|
|
116
142
|
// React remounts on toggle, so the conditional `useAnimatedKeyboard` hook
|
|
117
|
-
// inside `KeyboardAwareSheet` always runs in a stable hook order.
|
|
143
|
+
// inside `KeyboardAwareSheet` always runs in a stable hook order. Both
|
|
144
|
+
// branches receive the `entrance` shared value so the bottom pop-up plays in
|
|
145
|
+
// every configuration.
|
|
118
146
|
const header = title ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
119
147
|
style: resolved.headerStyle,
|
|
120
148
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
@@ -126,12 +154,14 @@ function ContentSheet({
|
|
|
126
154
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(KeyboardAwareSheet, {
|
|
127
155
|
style: containerStyle,
|
|
128
156
|
spacing: keyboardSpacing,
|
|
157
|
+
entrance: entrance,
|
|
129
158
|
...rest,
|
|
130
159
|
children: [header, processedChildren]
|
|
131
160
|
});
|
|
132
161
|
}
|
|
133
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(
|
|
162
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(AnimatedSheet, {
|
|
134
163
|
style: containerStyle,
|
|
164
|
+
entrance: entrance,
|
|
135
165
|
...rest,
|
|
136
166
|
children: [header, processedChildren]
|
|
137
167
|
});
|
|
@@ -139,25 +169,55 @@ function ContentSheet({
|
|
|
139
169
|
/**
|
|
140
170
|
* Native-only wrapper that lifts the sheet by the live keyboard height. The
|
|
141
171
|
* translation is computed on the UI thread from `useAnimatedKeyboard`, so the
|
|
142
|
-
* sheet tracks the keyboard frame-for-frame without any JS work.
|
|
172
|
+
* sheet tracks the keyboard frame-for-frame without any JS work. The keyboard
|
|
173
|
+
* offset is summed with the entrance `translateY` (the bottom pop-up) inside a
|
|
174
|
+
* single `useAnimatedStyle` so both motions stay on the UI thread and never
|
|
175
|
+
* fight each other.
|
|
143
176
|
*/
|
|
144
177
|
function KeyboardAwareSheet({
|
|
145
178
|
style,
|
|
146
179
|
spacing,
|
|
180
|
+
entrance,
|
|
147
181
|
children,
|
|
148
182
|
...rest
|
|
149
183
|
}) {
|
|
150
184
|
const keyboard = (0, _reactNativeReanimated.useAnimatedKeyboard)();
|
|
151
|
-
const
|
|
185
|
+
const animatedStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => {
|
|
152
186
|
const height = keyboard.height.value;
|
|
187
|
+
const keyboardOffset = height > 0 ? -(height + spacing) : 0;
|
|
188
|
+
return {
|
|
189
|
+
transform: [{
|
|
190
|
+
translateY: entrance.value + keyboardOffset
|
|
191
|
+
}]
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
|
|
195
|
+
style: [style, animatedStyle],
|
|
196
|
+
...rest,
|
|
197
|
+
children: children
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Entrance-only animated sheet (web, or when keyboard avoidance is disabled).
|
|
203
|
+
* Applies the same bottom pop-up `translateY` as `KeyboardAwareSheet`, minus
|
|
204
|
+
* the keyboard offset.
|
|
205
|
+
*/
|
|
206
|
+
function AnimatedSheet({
|
|
207
|
+
style,
|
|
208
|
+
entrance,
|
|
209
|
+
children,
|
|
210
|
+
...rest
|
|
211
|
+
}) {
|
|
212
|
+
const animatedStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => {
|
|
153
213
|
return {
|
|
154
214
|
transform: [{
|
|
155
|
-
translateY:
|
|
215
|
+
translateY: entrance.value
|
|
156
216
|
}]
|
|
157
217
|
};
|
|
158
218
|
});
|
|
159
219
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
|
|
160
|
-
style: [style,
|
|
220
|
+
style: [style, animatedStyle],
|
|
161
221
|
...rest,
|
|
162
222
|
children: children
|
|
163
223
|
});
|