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
|
@@ -76,6 +76,26 @@ export type CompareTableProps = {
|
|
|
76
76
|
* lines as it needs even when horizontal space is tight.
|
|
77
77
|
*/
|
|
78
78
|
disableTruncation?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Fixed width (px) for every selection card and every table cell — all
|
|
81
|
+
* columns share this width. When set, the component enters **scroll mode**:
|
|
82
|
+
*
|
|
83
|
+
* - The cards row and each section's table body become independent
|
|
84
|
+
* horizontal `ScrollView`s whose scroll events are **chained** — moving
|
|
85
|
+
* one moves every other in lockstep (the cards always stay aligned with
|
|
86
|
+
* the cells beneath them).
|
|
87
|
+
* - The cards row becomes a **sticky header** at the top: it pins in place
|
|
88
|
+
* on the vertical axis (does not scroll away) while the sections scroll
|
|
89
|
+
* beneath it. Requires the host to give `CompareTable` a bounded height.
|
|
90
|
+
* - The gray table-header rows (each section's `header`) are rendered
|
|
91
|
+
* full-width **outside** the horizontal scroll, so they stay put
|
|
92
|
+
* horizontally (they don't slide with the cells) while still scrolling
|
|
93
|
+
* vertically with the table.
|
|
94
|
+
*
|
|
95
|
+
* Omit to keep the original flex layout (columns share the available width,
|
|
96
|
+
* no scrolling). @default undefined
|
|
97
|
+
*/
|
|
98
|
+
columnWidth?: number;
|
|
79
99
|
};
|
|
80
100
|
/**
|
|
81
101
|
* CompareTable renders a product comparison surface: a row of selection cards
|
|
@@ -89,6 +109,6 @@ export type CompareTableProps = {
|
|
|
89
109
|
*
|
|
90
110
|
* @component
|
|
91
111
|
*/
|
|
92
|
-
declare function CompareTable({ columns, sections, onAddColumn, addColumnLabel, maxColumns, modes, style, disableTruncation, }: CompareTableProps): import("react/jsx-runtime").JSX.Element;
|
|
112
|
+
declare function CompareTable({ columns, sections, onAddColumn, addColumnLabel, maxColumns, modes, style, disableTruncation, columnWidth, }: CompareTableProps): import("react/jsx-runtime").JSX.Element;
|
|
93
113
|
export default CompareTable;
|
|
94
114
|
//# sourceMappingURL=CompareTable.d.ts.map
|
|
@@ -53,6 +53,19 @@ export type ContentSheetProps = {
|
|
|
53
53
|
* works in both modes.
|
|
54
54
|
*/
|
|
55
55
|
pinToBottom?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Controls whether the sheet is on-screen. When this becomes `true` the sheet
|
|
58
|
+
* springs up from the bottom edge (off-screen → on-screen) using the same
|
|
59
|
+
* spring as the {@link Drawer} (`damping: 32`, `stiffness: 300`); when it
|
|
60
|
+
* becomes `false` it springs back down below the bottom edge. The sheet pops
|
|
61
|
+
* up on mount by default.
|
|
62
|
+
*
|
|
63
|
+
* The animation runs entirely on the UI thread via `react-native-reanimated`
|
|
64
|
+
* (no re-renders) and composes with keyboard avoidance — the entrance
|
|
65
|
+
* `translateY` and the keyboard `translateY` are summed in a single
|
|
66
|
+
* `useAnimatedStyle`. Default `true`.
|
|
67
|
+
*/
|
|
68
|
+
visible?: boolean;
|
|
56
69
|
/** Optional style override applied to the sheet container. */
|
|
57
70
|
style?: StyleProp<ViewStyle>;
|
|
58
71
|
} & Omit<ViewProps, 'style' | 'children'>;
|
|
@@ -72,6 +85,6 @@ export type ContentSheetProps = {
|
|
|
72
85
|
* - **Token-driven** styling via `getVariableByName` + `modes`, with `modes`
|
|
73
86
|
* cascaded to all slot children.
|
|
74
87
|
*/
|
|
75
|
-
declare function ContentSheet({ children, modes, title, avoidKeyboard, keyboardSpacing, safeAreaBottom, pinToBottom, style, ...rest }: ContentSheetProps): import("react/jsx-runtime").JSX.Element;
|
|
88
|
+
declare function ContentSheet({ children, modes, title, avoidKeyboard, keyboardSpacing, safeAreaBottom, pinToBottom, visible, style, ...rest }: ContentSheetProps): import("react/jsx-runtime").JSX.Element;
|
|
76
89
|
export default ContentSheet;
|
|
77
90
|
//# sourceMappingURL=ContentSheet.d.ts.map
|
|
@@ -12,6 +12,15 @@ export type MoneyValueProps = {
|
|
|
12
12
|
hidden?: boolean;
|
|
13
13
|
/** When true, a blinking vertical cursor is shown at the end of the value text. */
|
|
14
14
|
focused?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* When true, tapping the component enters an inline edit mode where the value
|
|
17
|
+
* becomes a numeric TextInput. The user can type digits or delete the existing
|
|
18
|
+
* amount. Only numeric characters (including one decimal point) are accepted.
|
|
19
|
+
* On blur or submit the edit is committed and the display restores.
|
|
20
|
+
*/
|
|
21
|
+
editable?: boolean;
|
|
22
|
+
/** Called when the value changes during editing (after commit on blur). */
|
|
23
|
+
onValueChange?: (newValue: string | number) => void;
|
|
15
24
|
/** Modes configuration mapped to Figma tokens. */
|
|
16
25
|
modes?: Modes;
|
|
17
26
|
style?: StyleProp<ViewStyle>;
|
|
@@ -32,6 +41,6 @@ export type MoneyValueProps = {
|
|
|
32
41
|
* the `focused` prop is provided to this component, it will display a natural
|
|
33
42
|
* blinking text cursor.
|
|
34
43
|
*/
|
|
35
|
-
declare function MoneyValue({ value, currency, negative, focused, hidden, modes, style, valueStyle, currencyStyle, negativeSignStyle, accessibilityLabel, accessibilityHint, onPress, ...rest }: MoneyValueProps): import("react/jsx-runtime").JSX.Element;
|
|
44
|
+
declare function MoneyValue({ value, currency, negative, focused, hidden, editable, onValueChange, modes, style, valueStyle, currencyStyle, negativeSignStyle, accessibilityLabel, accessibilityHint, onPress, ...rest }: MoneyValueProps): import("react/jsx-runtime").JSX.Element;
|
|
36
45
|
export default MoneyValue;
|
|
37
46
|
//# sourceMappingURL=MoneyValue.d.ts.map
|
|
@@ -32,6 +32,30 @@ export interface PdpCcCardProps {
|
|
|
32
32
|
title?: string;
|
|
33
33
|
/** Subtitle rendered below the title (14px medium). */
|
|
34
34
|
subtitle?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Number of lines to limit the title to. The `Title` component defaults to
|
|
37
|
+
* `1` when this is unset, so the headline is a single line with an ellipsis
|
|
38
|
+
* by default. Pass a larger value (e.g. `2`) to allow the title to wrap onto
|
|
39
|
+
* that many lines before truncating, or set {@link disableTruncation} to
|
|
40
|
+
* remove the clamp entirely. The subtitle is never line-clamped.
|
|
41
|
+
*/
|
|
42
|
+
numberOfLines?: number;
|
|
43
|
+
/**
|
|
44
|
+
* When `true`, disables truncation for both the title and subtitle: the
|
|
45
|
+
* trailing ellipsis (`…`) is never shown and the `numberOfLines` clamp is
|
|
46
|
+
* ignored. The text is also never shrunk by a tight parent — it keeps its
|
|
47
|
+
* full intrinsic size and overflows (even breaking the parent layout) rather
|
|
48
|
+
* than ever clipping into an ellipsis. The text still wraps onto multiple
|
|
49
|
+
* lines.
|
|
50
|
+
*/
|
|
51
|
+
disableTruncation?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* When `true`, forces both the title and subtitle onto a single line with no
|
|
54
|
+
* wrapping (implies {@link disableTruncation}). On web the line overflows
|
|
55
|
+
* horizontally — brutally breaking the parent layout if it has to — instead
|
|
56
|
+
* of wrapping or truncating.
|
|
57
|
+
*/
|
|
58
|
+
singleLine?: boolean;
|
|
35
59
|
/**
|
|
36
60
|
* The metric columns rendered in the stats row. Vertical dividers are
|
|
37
61
|
* inserted automatically between adjacent metrics. Defaults to two sample
|
|
@@ -79,6 +103,6 @@ export interface PdpCcCardProps {
|
|
|
79
103
|
*
|
|
80
104
|
* All defaults can be overridden via `modes`.
|
|
81
105
|
*/
|
|
82
|
-
declare function PdpCcCard({ imageSource, imageWidth, imageHeight, media, title, subtitle, metrics, buttonLabel, buttonIcon, onButtonPress, button, showButton, onPress, width, modes, style, accessibilityLabel, }: PdpCcCardProps): import("react/jsx-runtime").JSX.Element;
|
|
106
|
+
declare function PdpCcCard({ imageSource, imageWidth, imageHeight, media, title, subtitle, numberOfLines, disableTruncation, singleLine, metrics, buttonLabel, buttonIcon, onButtonPress, button, showButton, onPress, width, modes, style, accessibilityLabel, }: PdpCcCardProps): import("react/jsx-runtime").JSX.Element;
|
|
83
107
|
export default PdpCcCard;
|
|
84
108
|
//# sourceMappingURL=PdpCcCard.d.ts.map
|
|
@@ -102,6 +102,14 @@ export type TableProps<T = Record<string, unknown>> = {
|
|
|
102
102
|
* for wide tables that exceed the viewport on mobile. @default false
|
|
103
103
|
*/
|
|
104
104
|
scrollable?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Fixed width (px) applied to every column — header and body alike — so
|
|
107
|
+
* all columns share the same width. An explicit per-column `width` (on
|
|
108
|
+
* `TableColumn`, or on an individual `Table.Cell`/`Table.HeaderCell`) still
|
|
109
|
+
* takes precedence when set. Omit to let columns flex to share the
|
|
110
|
+
* available space.
|
|
111
|
+
*/
|
|
112
|
+
columnWidth?: number;
|
|
105
113
|
/** Accessibility label for the table container. */
|
|
106
114
|
accessibilityLabel?: string;
|
|
107
115
|
/** Design token modes for theming (e.g. `{ "Color Mode": "Light" }`). */
|
|
@@ -126,7 +134,7 @@ export type TableProps<T = Record<string, unknown>> = {
|
|
|
126
134
|
*
|
|
127
135
|
* @component
|
|
128
136
|
*/
|
|
129
|
-
declare function Table<T = Record<string, unknown>>({ columns, data, children, showHeader, getRowKey, onRowPress, scrollable, accessibilityLabel, modes, style, }: TableProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
137
|
+
declare function Table<T = Record<string, unknown>>({ columns, data, children, showHeader, getRowKey, onRowPress, scrollable, columnWidth, accessibilityLabel, modes, style, }: TableProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
130
138
|
declare namespace Table {
|
|
131
139
|
var Header: typeof TableHeader;
|
|
132
140
|
var HeaderCell: typeof TableHeaderCell;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Auto-generated from SVG files in src/icons/
|
|
5
5
|
* DO NOT EDIT MANUALLY - Run "npm run icons:generate" to regenerate
|
|
6
6
|
*
|
|
7
|
-
* Generated: 2026-07-
|
|
7
|
+
* Generated: 2026-07-10T20:55:36.505Z
|
|
8
8
|
*/
|
|
9
9
|
export declare const iconRegistry: Record<string, {
|
|
10
10
|
path: string;
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import React from 'react'
|
|
1
|
+
import React, { useCallback, useRef } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
5
5
|
Pressable,
|
|
6
6
|
Platform,
|
|
7
|
+
ScrollView,
|
|
7
8
|
type ImageSourcePropType,
|
|
9
|
+
type NativeScrollEvent,
|
|
10
|
+
type NativeSyntheticEvent,
|
|
8
11
|
type StyleProp,
|
|
9
12
|
type ViewStyle,
|
|
10
13
|
type TextStyle,
|
|
@@ -140,6 +143,26 @@ export type CompareTableProps = {
|
|
|
140
143
|
* lines as it needs even when horizontal space is tight.
|
|
141
144
|
*/
|
|
142
145
|
disableTruncation?: boolean;
|
|
146
|
+
/**
|
|
147
|
+
* Fixed width (px) for every selection card and every table cell — all
|
|
148
|
+
* columns share this width. When set, the component enters **scroll mode**:
|
|
149
|
+
*
|
|
150
|
+
* - The cards row and each section's table body become independent
|
|
151
|
+
* horizontal `ScrollView`s whose scroll events are **chained** — moving
|
|
152
|
+
* one moves every other in lockstep (the cards always stay aligned with
|
|
153
|
+
* the cells beneath them).
|
|
154
|
+
* - The cards row becomes a **sticky header** at the top: it pins in place
|
|
155
|
+
* on the vertical axis (does not scroll away) while the sections scroll
|
|
156
|
+
* beneath it. Requires the host to give `CompareTable` a bounded height.
|
|
157
|
+
* - The gray table-header rows (each section's `header`) are rendered
|
|
158
|
+
* full-width **outside** the horizontal scroll, so they stay put
|
|
159
|
+
* horizontally (they don't slide with the cells) while still scrolling
|
|
160
|
+
* vertically with the table.
|
|
161
|
+
*
|
|
162
|
+
* Omit to keep the original flex layout (columns share the available width,
|
|
163
|
+
* no scrolling). @default undefined
|
|
164
|
+
*/
|
|
165
|
+
columnWidth?: number;
|
|
143
166
|
};
|
|
144
167
|
|
|
145
168
|
const DEFAULT_COLUMNS: CompareTableColumn[] = [
|
|
@@ -181,6 +204,7 @@ function CompareTable({
|
|
|
181
204
|
modes = EMPTY_MODES,
|
|
182
205
|
style,
|
|
183
206
|
disableTruncation,
|
|
207
|
+
columnWidth,
|
|
184
208
|
}: CompareTableProps) {
|
|
185
209
|
// --- selection card tokens ------------------------------------------------
|
|
186
210
|
const cardBg = (getVariableByName('selectionCard/background/color', modes) as string) ?? '#ffffff'
|
|
@@ -238,6 +262,72 @@ function CompareTable({
|
|
|
238
262
|
const showAddCard = onAddColumn != null && columnCount < maxColumns
|
|
239
263
|
/** Grid columns shared by the selection-card row and every table row. */
|
|
240
264
|
const gridColumnCount = columnCount + (showAddCard ? 1 : 0)
|
|
265
|
+
const showCardsRow = columnCount > 0 || showAddCard
|
|
266
|
+
|
|
267
|
+
// --- fixed-width scroll mode -------------------------------------------
|
|
268
|
+
// `columnWidth` opts the component into scroll mode (see prop doc). In that
|
|
269
|
+
// mode the cards row and each section's table body are independent
|
|
270
|
+
// horizontal ScrollViews; their scroll events are chained so they move in
|
|
271
|
+
// lockstep, and the cards row pins to the top on the vertical axis.
|
|
272
|
+
const useFixedWidth = columnWidth != null
|
|
273
|
+
/** Total scrollable content width shared by every horizontal surface. */
|
|
274
|
+
const contentWidth = useFixedWidth ? gridColumnCount * (columnWidth as number) : 0
|
|
275
|
+
|
|
276
|
+
const cardsScrollRef = useRef<ScrollView>(null)
|
|
277
|
+
const bodyScrollRefs = useRef<Map<number, ScrollView>>(new Map())
|
|
278
|
+
// Stable per-section ref-callback instances so React doesn't detach/reattach
|
|
279
|
+
// on every render (which would churn the map and re-fire scrollTo).
|
|
280
|
+
const bodyRefCallbacks = useRef<Map<number, (r: ScrollView | null) => void>>(new Map())
|
|
281
|
+
/** Last offset we synced everyone to. Echoed onScroll events reporting this
|
|
282
|
+
* same value are ignored — this is what prevents feedback loops. */
|
|
283
|
+
const lastSyncX = useRef(0)
|
|
284
|
+
|
|
285
|
+
const applySync = useCallback((x: number, source: 'cards' | number) => {
|
|
286
|
+
if (Math.abs(x - lastSyncX.current) < 0.5) return
|
|
287
|
+
lastSyncX.current = x
|
|
288
|
+
if (source !== 'cards' && cardsScrollRef.current) {
|
|
289
|
+
cardsScrollRef.current.scrollTo({ x, animated: false })
|
|
290
|
+
}
|
|
291
|
+
bodyScrollRefs.current.forEach((ref, idx) => {
|
|
292
|
+
if (source === 'cards' || idx !== source) {
|
|
293
|
+
ref.scrollTo({ x, animated: false })
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
}, [])
|
|
297
|
+
|
|
298
|
+
const onCardsScroll = useCallback(
|
|
299
|
+
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
300
|
+
applySync(e.nativeEvent.contentOffset.x, 'cards')
|
|
301
|
+
},
|
|
302
|
+
[applySync],
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
const onBodyScroll = useCallback(
|
|
306
|
+
(idx: number) => (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
307
|
+
applySync(e.nativeEvent.contentOffset.x, idx)
|
|
308
|
+
},
|
|
309
|
+
[applySync],
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
const getBodyRefCallback = useCallback((idx: number) => {
|
|
313
|
+
let cb = bodyRefCallbacks.current.get(idx)
|
|
314
|
+
if (!cb) {
|
|
315
|
+
cb = (ref: ScrollView | null) => {
|
|
316
|
+
if (ref) {
|
|
317
|
+
bodyScrollRefs.current.set(idx, ref)
|
|
318
|
+
// Bring a freshly-expanded section's body into sync with
|
|
319
|
+
// the rest of the surfaces.
|
|
320
|
+
if (lastSyncX.current > 0) {
|
|
321
|
+
ref.scrollTo({ x: lastSyncX.current, animated: false })
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
bodyScrollRefs.current.delete(idx)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
bodyRefCallbacks.current.set(idx, cb)
|
|
328
|
+
}
|
|
329
|
+
return cb
|
|
330
|
+
}, [])
|
|
241
331
|
|
|
242
332
|
const cardLabelStyle: TextStyle = {
|
|
243
333
|
...NO_WRAP_TEXT,
|
|
@@ -273,8 +363,7 @@ function CompareTable({
|
|
|
273
363
|
}
|
|
274
364
|
|
|
275
365
|
const cardStyle: ViewStyle = {
|
|
276
|
-
flex: 1,
|
|
277
|
-
minWidth: 0,
|
|
366
|
+
...(useFixedWidth ? { width: columnWidth as number } : { flex: 1, minWidth: 0 }),
|
|
278
367
|
minHeight: 147,
|
|
279
368
|
backgroundColor: cardBg,
|
|
280
369
|
borderRadius: cardRadius,
|
|
@@ -420,6 +509,7 @@ function CompareTable({
|
|
|
420
509
|
key={key}
|
|
421
510
|
modes={modes}
|
|
422
511
|
flex={1}
|
|
512
|
+
width={useFixedWidth ? columnWidth : undefined}
|
|
423
513
|
align="left"
|
|
424
514
|
isLastColumn={isLastCol}
|
|
425
515
|
isLastRow={isLastRow}
|
|
@@ -486,38 +576,142 @@ function CompareTable({
|
|
|
486
576
|
</Table>
|
|
487
577
|
)
|
|
488
578
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
579
|
+
// Render a single section in fixed-width scroll mode. Unlike `renderTable`
|
|
580
|
+
// (used in flex mode), the gray header row is rendered full-width OUTSIDE
|
|
581
|
+
// the body's horizontal ScrollView so it stays put horizontally (it does
|
|
582
|
+
// not slide with the cells) while still scrolling vertically with the table.
|
|
583
|
+
// The body rows live inside a horizontal ScrollView that is scroll-synced
|
|
584
|
+
// with the cards row and every other section's body.
|
|
585
|
+
const renderSection = (section: CompareTableSection, index: number) => (
|
|
586
|
+
<Accordion
|
|
587
|
+
key={section.key ?? section.title ?? index}
|
|
588
|
+
title={section.title}
|
|
589
|
+
defaultExpanded={section.defaultExpanded ?? index === 0}
|
|
590
|
+
modes={modes}
|
|
501
591
|
>
|
|
502
|
-
{
|
|
503
|
-
<View
|
|
504
|
-
{
|
|
505
|
-
|
|
592
|
+
{section.header != null && (
|
|
593
|
+
<View
|
|
594
|
+
style={{
|
|
595
|
+
width: '100%',
|
|
596
|
+
backgroundColor: headerBg,
|
|
597
|
+
paddingHorizontal: headerPaddingH,
|
|
598
|
+
paddingVertical: headerPaddingV,
|
|
599
|
+
borderBottomWidth: cellBorderSize,
|
|
600
|
+
borderBottomColor: cellBorderColor,
|
|
601
|
+
}}
|
|
602
|
+
>
|
|
603
|
+
<Text style={headerTextStyle}>{section.header}</Text>
|
|
506
604
|
</View>
|
|
507
605
|
)}
|
|
606
|
+
<ScrollView
|
|
607
|
+
ref={getBodyRefCallback(index)}
|
|
608
|
+
horizontal
|
|
609
|
+
showsHorizontalScrollIndicator={false}
|
|
610
|
+
scrollEventThrottle={16}
|
|
611
|
+
onScroll={onBodyScroll(index)}
|
|
612
|
+
>
|
|
613
|
+
<View style={{ width: contentWidth }}>
|
|
614
|
+
{section.rows.map((row, rowIndex) => {
|
|
615
|
+
const isLastRow = rowIndex === section.rows.length - 1
|
|
616
|
+
return (
|
|
617
|
+
<View
|
|
618
|
+
key={row.key ?? rowIndex}
|
|
619
|
+
style={{ flexDirection: 'row', width: '100%' }}
|
|
620
|
+
>
|
|
621
|
+
{Array.from({ length: columnCount }).map((_, colIndex) =>
|
|
622
|
+
renderTableCell(
|
|
623
|
+
renderCellContent(
|
|
624
|
+
row.values?.[colIndex],
|
|
625
|
+
`${rowIndex}-${colIndex}`,
|
|
626
|
+
),
|
|
627
|
+
colIndex,
|
|
628
|
+
isLastRow,
|
|
629
|
+
colIndex,
|
|
630
|
+
),
|
|
631
|
+
)}
|
|
632
|
+
{showAddCard &&
|
|
633
|
+
renderTableCell(
|
|
634
|
+
null,
|
|
635
|
+
gridColumnCount - 1,
|
|
636
|
+
isLastRow,
|
|
637
|
+
'__add_spacer__',
|
|
638
|
+
)}
|
|
639
|
+
</View>
|
|
640
|
+
)
|
|
641
|
+
})}
|
|
642
|
+
</View>
|
|
643
|
+
</ScrollView>
|
|
644
|
+
</Accordion>
|
|
645
|
+
)
|
|
508
646
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
647
|
+
const outerStyle: ViewStyle = {
|
|
648
|
+
width: '100%',
|
|
649
|
+
backgroundColor: cardBg,
|
|
650
|
+
borderWidth: 1,
|
|
651
|
+
borderColor: '#e8e8e8',
|
|
652
|
+
overflow: 'hidden',
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// --- flex mode (columnWidth omitted): original behaviour, unchanged -------
|
|
656
|
+
if (!useFixedWidth) {
|
|
657
|
+
return (
|
|
658
|
+
<View style={[outerStyle, style]}>
|
|
659
|
+
{showCardsRow && (
|
|
660
|
+
<View style={{ flexDirection: 'row', width: '100%' }}>
|
|
661
|
+
{columns.map(renderCard)}
|
|
662
|
+
{showAddCard && renderAddCard()}
|
|
663
|
+
</View>
|
|
664
|
+
)}
|
|
665
|
+
|
|
666
|
+
<View style={{ width: '100%' }}>
|
|
667
|
+
{sections.map((section, index) => (
|
|
668
|
+
<Accordion
|
|
669
|
+
key={section.key ?? section.title ?? index}
|
|
670
|
+
title={section.title}
|
|
671
|
+
defaultExpanded={section.defaultExpanded ?? index === 0}
|
|
672
|
+
modes={modes}
|
|
673
|
+
>
|
|
674
|
+
{renderTable(section)}
|
|
675
|
+
</Accordion>
|
|
676
|
+
))}
|
|
677
|
+
</View>
|
|
520
678
|
</View>
|
|
679
|
+
)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// --- fixed-width scroll mode: chained horizontal sync + sticky cards -----
|
|
683
|
+
// The cards row (index 0) is a sticky header of the vertical ScrollView so
|
|
684
|
+
// it pins to the top while the sections scroll beneath. The cards row is
|
|
685
|
+
// itself a horizontal ScrollView, synced with every section body. For the
|
|
686
|
+
// vertical scroll (and thus the sticky behaviour) to engage, the host must
|
|
687
|
+
// give CompareTable a bounded height (e.g. a sized container or flex:1).
|
|
688
|
+
return (
|
|
689
|
+
<View style={[outerStyle, style, { flexDirection: 'column', flex: 1 }]}>
|
|
690
|
+
<ScrollView
|
|
691
|
+
style={{ width: '100%', flex: 1 }}
|
|
692
|
+
stickyHeaderIndices={showCardsRow ? [0] : []}
|
|
693
|
+
showsVerticalScrollIndicator={false}
|
|
694
|
+
directionalLockEnabled
|
|
695
|
+
>
|
|
696
|
+
{showCardsRow && (
|
|
697
|
+
<ScrollView
|
|
698
|
+
ref={cardsScrollRef}
|
|
699
|
+
horizontal
|
|
700
|
+
showsHorizontalScrollIndicator={false}
|
|
701
|
+
scrollEventThrottle={16}
|
|
702
|
+
onScroll={onCardsScroll}
|
|
703
|
+
>
|
|
704
|
+
<View style={{ flexDirection: 'row', width: contentWidth }}>
|
|
705
|
+
{columns.map(renderCard)}
|
|
706
|
+
{showAddCard && renderAddCard()}
|
|
707
|
+
</View>
|
|
708
|
+
</ScrollView>
|
|
709
|
+
)}
|
|
710
|
+
|
|
711
|
+
<View style={{ width: '100%' }}>
|
|
712
|
+
{sections.map(renderSection)}
|
|
713
|
+
</View>
|
|
714
|
+
</ScrollView>
|
|
521
715
|
</View>
|
|
522
716
|
)
|
|
523
717
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import React, { useContext, useMemo } from 'react'
|
|
1
|
+
import React, { useContext, useEffect, useMemo } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
Platform,
|
|
4
4
|
Text,
|
|
5
5
|
View,
|
|
6
|
+
useWindowDimensions,
|
|
6
7
|
type StyleProp,
|
|
7
8
|
type TextStyle,
|
|
8
9
|
type ViewProps,
|
|
@@ -11,6 +12,9 @@ import {
|
|
|
11
12
|
import Animated, {
|
|
12
13
|
useAnimatedKeyboard,
|
|
13
14
|
useAnimatedStyle,
|
|
15
|
+
useSharedValue,
|
|
16
|
+
withSpring,
|
|
17
|
+
type SharedValue,
|
|
14
18
|
} from 'react-native-reanimated'
|
|
15
19
|
import { SafeAreaInsetsContext } from 'react-native-safe-area-context'
|
|
16
20
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
@@ -19,6 +23,18 @@ import type { Modes } from '../../design-tokens'
|
|
|
19
23
|
|
|
20
24
|
const IS_WEB = Platform.OS === 'web'
|
|
21
25
|
|
|
26
|
+
// Spring config — mirrors the Drawer's bottom pop-up spring so the two
|
|
27
|
+
// bottom-sheet surfaces share the exact same entrance feel (slight bounce,
|
|
28
|
+
// snappy). Keep in sync with `Drawer.tsx` SPRING_CONFIG.
|
|
29
|
+
const SPRING_CONFIG = {
|
|
30
|
+
damping: 32,
|
|
31
|
+
stiffness: 300,
|
|
32
|
+
mass: 1,
|
|
33
|
+
overshootClamping: false,
|
|
34
|
+
restDisplacementThreshold: 0.1,
|
|
35
|
+
restSpeedThreshold: 0.1,
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
export type ContentSheetProps = {
|
|
23
39
|
/**
|
|
24
40
|
* Sheet content. The sheet is one big slot: whatever you place here is
|
|
@@ -71,6 +87,19 @@ export type ContentSheetProps = {
|
|
|
71
87
|
* works in both modes.
|
|
72
88
|
*/
|
|
73
89
|
pinToBottom?: boolean
|
|
90
|
+
/**
|
|
91
|
+
* Controls whether the sheet is on-screen. When this becomes `true` the sheet
|
|
92
|
+
* springs up from the bottom edge (off-screen → on-screen) using the same
|
|
93
|
+
* spring as the {@link Drawer} (`damping: 32`, `stiffness: 300`); when it
|
|
94
|
+
* becomes `false` it springs back down below the bottom edge. The sheet pops
|
|
95
|
+
* up on mount by default.
|
|
96
|
+
*
|
|
97
|
+
* The animation runs entirely on the UI thread via `react-native-reanimated`
|
|
98
|
+
* (no re-renders) and composes with keyboard avoidance — the entrance
|
|
99
|
+
* `translateY` and the keyboard `translateY` are summed in a single
|
|
100
|
+
* `useAnimatedStyle`. Default `true`.
|
|
101
|
+
*/
|
|
102
|
+
visible?: boolean
|
|
74
103
|
/** Optional style override applied to the sheet container. */
|
|
75
104
|
style?: StyleProp<ViewStyle>
|
|
76
105
|
} & Omit<ViewProps, 'style' | 'children'>
|
|
@@ -172,6 +201,7 @@ function ContentSheet({
|
|
|
172
201
|
keyboardSpacing = 0,
|
|
173
202
|
safeAreaBottom = true,
|
|
174
203
|
pinToBottom = true,
|
|
204
|
+
visible = true,
|
|
175
205
|
style,
|
|
176
206
|
...rest
|
|
177
207
|
}: ContentSheetProps) {
|
|
@@ -200,10 +230,23 @@ function ContentSheet({
|
|
|
200
230
|
[pinToBottom, resolved.container, paddingBottom, style]
|
|
201
231
|
)
|
|
202
232
|
|
|
233
|
+
// Entrance / exit "pop up from bottom" animation — mirrors the Drawer. The
|
|
234
|
+
// sheet is bottom-anchored, so a positive `translateY` equal to the window
|
|
235
|
+
// height parks it fully below the bottom edge. Springing it back to `0`
|
|
236
|
+
// produces the same bottom pop-up the Drawer uses. The shared value is
|
|
237
|
+
// initialised off-screen so the very first mount pops in (no flash).
|
|
238
|
+
const { height: windowHeight } = useWindowDimensions()
|
|
239
|
+
const entrance = useSharedValue(windowHeight)
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
entrance.value = withSpring(visible ? 0 : windowHeight, SPRING_CONFIG)
|
|
242
|
+
}, [visible, windowHeight, entrance])
|
|
243
|
+
|
|
203
244
|
// Switching between the keyboard-aware and plain implementation is keyed off
|
|
204
245
|
// `avoidKeyboard` (and platform). Because they are distinct component types,
|
|
205
246
|
// React remounts on toggle, so the conditional `useAnimatedKeyboard` hook
|
|
206
|
-
// inside `KeyboardAwareSheet` always runs in a stable hook order.
|
|
247
|
+
// inside `KeyboardAwareSheet` always runs in a stable hook order. Both
|
|
248
|
+
// branches receive the `entrance` shared value so the bottom pop-up plays in
|
|
249
|
+
// every configuration.
|
|
207
250
|
const header = title ? (
|
|
208
251
|
<View style={resolved.headerStyle}>
|
|
209
252
|
<Text style={resolved.titleStyle}>{title}</Text>
|
|
@@ -215,6 +258,7 @@ function ContentSheet({
|
|
|
215
258
|
<KeyboardAwareSheet
|
|
216
259
|
style={containerStyle}
|
|
217
260
|
spacing={keyboardSpacing}
|
|
261
|
+
entrance={entrance}
|
|
218
262
|
{...rest}
|
|
219
263
|
>
|
|
220
264
|
{header}
|
|
@@ -224,41 +268,77 @@ function ContentSheet({
|
|
|
224
268
|
}
|
|
225
269
|
|
|
226
270
|
return (
|
|
227
|
-
<
|
|
271
|
+
<AnimatedSheet style={containerStyle} entrance={entrance} {...rest}>
|
|
228
272
|
{header}
|
|
229
273
|
{processedChildren}
|
|
230
|
-
</
|
|
274
|
+
</AnimatedSheet>
|
|
231
275
|
)
|
|
232
276
|
}
|
|
233
277
|
|
|
234
278
|
type KeyboardAwareSheetProps = {
|
|
235
279
|
style: StyleProp<ViewStyle>
|
|
236
280
|
spacing: number
|
|
281
|
+
entrance: SharedValue<number>
|
|
282
|
+
children: React.ReactNode
|
|
283
|
+
} & Omit<ViewProps, 'style' | 'children'>
|
|
284
|
+
|
|
285
|
+
type AnimatedSheetProps = {
|
|
286
|
+
style: StyleProp<ViewStyle>
|
|
287
|
+
entrance: SharedValue<number>
|
|
237
288
|
children: React.ReactNode
|
|
238
289
|
} & Omit<ViewProps, 'style' | 'children'>
|
|
239
290
|
|
|
240
291
|
/**
|
|
241
292
|
* Native-only wrapper that lifts the sheet by the live keyboard height. The
|
|
242
293
|
* translation is computed on the UI thread from `useAnimatedKeyboard`, so the
|
|
243
|
-
* sheet tracks the keyboard frame-for-frame without any JS work.
|
|
294
|
+
* sheet tracks the keyboard frame-for-frame without any JS work. The keyboard
|
|
295
|
+
* offset is summed with the entrance `translateY` (the bottom pop-up) inside a
|
|
296
|
+
* single `useAnimatedStyle` so both motions stay on the UI thread and never
|
|
297
|
+
* fight each other.
|
|
244
298
|
*/
|
|
245
299
|
function KeyboardAwareSheet({
|
|
246
300
|
style,
|
|
247
301
|
spacing,
|
|
302
|
+
entrance,
|
|
248
303
|
children,
|
|
249
304
|
...rest
|
|
250
305
|
}: KeyboardAwareSheetProps) {
|
|
251
306
|
const keyboard = useAnimatedKeyboard()
|
|
252
307
|
|
|
253
|
-
const
|
|
308
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
254
309
|
const height = keyboard.height.value
|
|
310
|
+
const keyboardOffset = height > 0 ? -(height + spacing) : 0
|
|
311
|
+
return {
|
|
312
|
+
transform: [{ translateY: entrance.value + keyboardOffset }],
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
<Animated.View style={[style, animatedStyle]} {...rest}>
|
|
318
|
+
{children}
|
|
319
|
+
</Animated.View>
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Entrance-only animated sheet (web, or when keyboard avoidance is disabled).
|
|
325
|
+
* Applies the same bottom pop-up `translateY` as `KeyboardAwareSheet`, minus
|
|
326
|
+
* the keyboard offset.
|
|
327
|
+
*/
|
|
328
|
+
function AnimatedSheet({
|
|
329
|
+
style,
|
|
330
|
+
entrance,
|
|
331
|
+
children,
|
|
332
|
+
...rest
|
|
333
|
+
}: AnimatedSheetProps) {
|
|
334
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
255
335
|
return {
|
|
256
|
-
transform: [{ translateY:
|
|
336
|
+
transform: [{ translateY: entrance.value }],
|
|
257
337
|
}
|
|
258
338
|
})
|
|
259
339
|
|
|
260
340
|
return (
|
|
261
|
-
<Animated.View style={[style,
|
|
341
|
+
<Animated.View style={[style, animatedStyle]} {...rest}>
|
|
262
342
|
{children}
|
|
263
343
|
</Animated.View>
|
|
264
344
|
)
|