jfs-components 0.1.30 → 0.1.32
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 +13 -0
- package/lib/commonjs/components/CategoryCard/CategoryCard.js +264 -0
- package/lib/commonjs/components/CompareTable/CompareTable.js +140 -84
- package/lib/commonjs/components/ContentSheet/ContentSheet.js +28 -5
- package/lib/commonjs/components/CounterBadge/CounterBadge.js +78 -0
- package/lib/commonjs/components/FavoriteToggle/FavoriteToggle.js +180 -0
- package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +287 -0
- package/lib/commonjs/components/ValueBackMetric/ValueBackMetric.js +219 -0
- package/lib/commonjs/components/index.js +35 -0
- package/lib/commonjs/design-tokens/figma-modes.generated.js +3 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/CategoryCard/CategoryCard.js +258 -0
- package/lib/module/components/CompareTable/CompareTable.js +140 -84
- package/lib/module/components/ContentSheet/ContentSheet.js +29 -6
- package/lib/module/components/CounterBadge/CounterBadge.js +73 -0
- package/lib/module/components/FavoriteToggle/FavoriteToggle.js +174 -0
- package/lib/module/components/HelloJioInput/HelloJioInput.js +281 -0
- package/lib/module/components/ValueBackMetric/ValueBackMetric.js +214 -0
- package/lib/module/components/index.js +6 -1
- package/lib/module/design-tokens/figma-modes.generated.js +3 -0
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/CategoryCard/CategoryCard.d.ts +72 -0
- package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +16 -1
- package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +7 -1
- package/lib/typescript/src/components/CounterBadge/CounterBadge.d.ts +19 -0
- package/lib/typescript/src/components/FavoriteToggle/FavoriteToggle.d.ts +66 -0
- package/lib/typescript/src/components/HelloJioInput/HelloJioInput.d.ts +111 -0
- package/lib/typescript/src/components/ValueBackMetric/ValueBackMetric.d.ts +86 -0
- package/lib/typescript/src/components/index.d.ts +5 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/CategoryCard/CategoryCard.tsx +404 -0
- package/src/components/CompareTable/CompareTable.tsx +201 -101
- package/src/components/ContentSheet/ContentSheet.tsx +40 -11
- package/src/components/CounterBadge/CounterBadge.tsx +95 -0
- package/src/components/FavoriteToggle/FavoriteToggle.tsx +243 -0
- package/src/components/HelloJioInput/HelloJioInput.tsx +513 -0
- package/src/components/ValueBackMetric/ValueBackMetric.tsx +298 -0
- package/src/components/index.ts +5 -0
- package/src/design-tokens/figma-modes.generated.ts +3 -0
- package/src/icons/registry.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ 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.31] - 2026-07-15
|
|
8
|
+
|
|
9
|
+
- Added `CategoryCard` — compact category rail tile (`categoryCard/*` tokens) with image (or `imageSlot`), optional overlapping `Badge`, fixed 60dp width (overridable via `width`), and `active` / `disabled` press states. Label truncation mirrors `Text` via `disableTruncation` / `singleLine`.
|
|
10
|
+
- Added `HelloJioInput` — pill-shaped HelloJio prompt field (`helloJioInput/*` tokens) with leading brand icon, single-line input, and send `IconButton`. Focus drives Idle → Active; `jioPlus` enables the frosted IdleJioPlus glass surface. Controlled/uncontrolled text with `onSubmit` from the send button and keyboard return key; `leading` / `trailing` slots for customization.
|
|
11
|
+
- Added `CounterBadge` — small token-driven value pill (`counterBadge/*` tokens).
|
|
12
|
+
- Added `FavoriteToggle` — glass circular favorite control (`favoriteToggle/*` tokens) with controlled `isActive` / uncontrolled `defaultActive`, `onChange`, and skeleton loading support.
|
|
13
|
+
- Added `ValueBackMetric` — compact metric card (JioPoints-style) with header icon + title, value, caption, and optional bottom `Link`; colours/gaps/padding from `metricdata/*` tokens.
|
|
14
|
+
- `CompareTable` — new `stickyHeaders` and `stickyHeaderLabel` props (default `true`) for sticky section headers in fixed-width scroll mode; horizontal scroll sync rewritten to track the actively dragged `ScrollView` and eliminate jitter from programmatic `scrollTo` echoes.
|
|
15
|
+
- `ContentSheet` — new `maxHeightPercent` prop (default `0.7`) caps sheet height as a fraction of the window and scrolls overflow content.
|
|
16
|
+
- Stories and MDX documentation updated for all new components and props.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
7
20
|
## [0.1.30] - 2026-07-10
|
|
8
21
|
|
|
9
22
|
- `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`).
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _react = _interopRequireWildcard(require("react"));
|
|
8
|
+
var _reactNative = require("react-native");
|
|
9
|
+
var _figmaVariablesResolver = require("../../design-tokens/figma-variables-resolver");
|
|
10
|
+
var _JFSThemeProvider = require("../../design-tokens/JFSThemeProvider");
|
|
11
|
+
var _reactUtils = require("../../utils/react-utils");
|
|
12
|
+
var _webPlatformUtils = require("../../utils/web-platform-utils");
|
|
13
|
+
var _Badge = _interopRequireDefault(require("../Badge/Badge"));
|
|
14
|
+
var _Image = _interopRequireDefault(require("../Image/Image"));
|
|
15
|
+
var _jsxRuntime = require("react/jsx-runtime");
|
|
16
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
17
|
+
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
|
+
const CATEGORY_CARD_STATE = 'Category Card State';
|
|
19
|
+
const BADGE_OVERLAP = 11;
|
|
20
|
+
const DEFAULT_CARD_WIDTH = 60;
|
|
21
|
+
|
|
22
|
+
// Default modes from the Figma CategoryCard component. Overridable via `modes`.
|
|
23
|
+
const CATEGORY_CARD_DEFAULT_MODES = Object.freeze({
|
|
24
|
+
AppearanceBrand: 'Tertiary',
|
|
25
|
+
'Badge Size': 'Small',
|
|
26
|
+
Context4: 'Badge',
|
|
27
|
+
"Radius": "None"
|
|
28
|
+
});
|
|
29
|
+
const toNumber = (value, fallback) => {
|
|
30
|
+
if (typeof value === 'number') {
|
|
31
|
+
return Number.isFinite(value) ? value : fallback;
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === 'string') {
|
|
34
|
+
const parsed = Number(value);
|
|
35
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
36
|
+
}
|
|
37
|
+
return fallback;
|
|
38
|
+
};
|
|
39
|
+
const toFontWeight = (value, fallback) => {
|
|
40
|
+
if (typeof value === 'number') return String(value);
|
|
41
|
+
if (typeof value === 'string') return value;
|
|
42
|
+
return fallback;
|
|
43
|
+
};
|
|
44
|
+
function resolveCategoryCardTokens(modes, active) {
|
|
45
|
+
const idleModes = {
|
|
46
|
+
...modes,
|
|
47
|
+
[CATEGORY_CARD_STATE]: 'Idle'
|
|
48
|
+
};
|
|
49
|
+
const activeModes = {
|
|
50
|
+
...modes,
|
|
51
|
+
[CATEGORY_CARD_STATE]: 'Active'
|
|
52
|
+
};
|
|
53
|
+
const resolvedModes = active ? activeModes : idleModes;
|
|
54
|
+
const gap = toNumber((0, _figmaVariablesResolver.getVariableByName)('cardTab/gap', resolvedModes), 6);
|
|
55
|
+
const paddingHorizontal = toNumber((0, _figmaVariablesResolver.getVariableByName)('cardTab/padding/horizontal', resolvedModes), 8);
|
|
56
|
+
const paddingVertical = toNumber((0, _figmaVariablesResolver.getVariableByName)('cardTab/padding/vertical', resolvedModes), 8);
|
|
57
|
+
const radius = toNumber((0, _figmaVariablesResolver.getVariableByName)('cardTab/radius', resolvedModes), 8);
|
|
58
|
+
const strokeWidth = toNumber((0, _figmaVariablesResolver.getVariableByName)('cardTab/strokeWidth', resolvedModes), 1);
|
|
59
|
+
const strokeColor = (0, _figmaVariablesResolver.getVariableByName)('cardTab/stroke/color', resolvedModes) ?? '#f5f5f5';
|
|
60
|
+
const idleBackground = (0, _figmaVariablesResolver.getVariableByName)('cardTab/background/color', idleModes) ?? '#ffffff';
|
|
61
|
+
const activeBackground = (0, _figmaVariablesResolver.getVariableByName)('cardTab/background/color', activeModes) ?? '#f5f5f5';
|
|
62
|
+
const backgroundColor = active ? activeBackground : idleBackground;
|
|
63
|
+
const imageSize = toNumber((0, _figmaVariablesResolver.getVariableByName)('cardTab/image/width', resolvedModes), 36);
|
|
64
|
+
const imageRadius = toNumber((0, _figmaVariablesResolver.getVariableByName)('image/radius', resolvedModes), 0);
|
|
65
|
+
const fontFamily = (0, _figmaVariablesResolver.getVariableByName)('cardTab/label/fontFamily', resolvedModes) ?? 'JioType Var';
|
|
66
|
+
const fontSize = toNumber((0, _figmaVariablesResolver.getVariableByName)('cardTab/label/fontSize', resolvedModes), 12);
|
|
67
|
+
const lineHeight = toNumber((0, _figmaVariablesResolver.getVariableByName)('cardTab/label/lineHeight', resolvedModes), Math.round(fontSize * 1.3));
|
|
68
|
+
const fontWeight = toFontWeight((0, _figmaVariablesResolver.getVariableByName)('cardTab/label/fontWeight', resolvedModes), '500');
|
|
69
|
+
const labelColor = (0, _figmaVariablesResolver.getVariableByName)('cardTab/label/color', resolvedModes) ?? '#000000';
|
|
70
|
+
return {
|
|
71
|
+
containerStyle: {
|
|
72
|
+
position: 'relative',
|
|
73
|
+
alignItems: 'center',
|
|
74
|
+
justifyContent: 'center',
|
|
75
|
+
backgroundColor,
|
|
76
|
+
borderColor: strokeColor,
|
|
77
|
+
borderWidth: strokeWidth,
|
|
78
|
+
borderStyle: 'solid',
|
|
79
|
+
borderRadius: radius,
|
|
80
|
+
paddingHorizontal,
|
|
81
|
+
paddingTop: paddingVertical,
|
|
82
|
+
paddingBottom: paddingVertical,
|
|
83
|
+
gap
|
|
84
|
+
},
|
|
85
|
+
labelStyle: {
|
|
86
|
+
color: labelColor,
|
|
87
|
+
fontFamily,
|
|
88
|
+
fontSize,
|
|
89
|
+
fontWeight,
|
|
90
|
+
lineHeight,
|
|
91
|
+
textAlign: 'center'
|
|
92
|
+
},
|
|
93
|
+
imageSize,
|
|
94
|
+
imageRadius
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const pressedOverlayStyle = {
|
|
98
|
+
opacity: 0.85
|
|
99
|
+
};
|
|
100
|
+
const disabledOverlayStyle = {
|
|
101
|
+
opacity: 0.5
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* `CategoryCard` is a compact, tappable tile for category pickers and
|
|
106
|
+
* merchandising rails. It stacks a square image above a short label and can
|
|
107
|
+
* optionally show an overlapping badge at the top edge. Cards are typically
|
|
108
|
+
* laid out in a horizontal `ScrollView` or `HStack`.
|
|
109
|
+
*
|
|
110
|
+
* All visual values resolve through the `cardTab/*` design tokens with
|
|
111
|
+
* sensible Figma defaults so the card renders correctly out of the box.
|
|
112
|
+
*
|
|
113
|
+
* @component
|
|
114
|
+
* @param {CategoryCardProps} props
|
|
115
|
+
*/
|
|
116
|
+
function CategoryCard({
|
|
117
|
+
label = 'Popular',
|
|
118
|
+
imageSource,
|
|
119
|
+
imageSlot,
|
|
120
|
+
badge,
|
|
121
|
+
active = false,
|
|
122
|
+
width = DEFAULT_CARD_WIDTH,
|
|
123
|
+
disableTruncation,
|
|
124
|
+
singleLine,
|
|
125
|
+
onPress,
|
|
126
|
+
disabled = false,
|
|
127
|
+
modes: propModes = _reactUtils.EMPTY_MODES,
|
|
128
|
+
style,
|
|
129
|
+
labelStyle,
|
|
130
|
+
accessibilityLabel,
|
|
131
|
+
accessibilityHint,
|
|
132
|
+
accessibilityState,
|
|
133
|
+
webAccessibilityProps,
|
|
134
|
+
testID,
|
|
135
|
+
...rest
|
|
136
|
+
}) {
|
|
137
|
+
const {
|
|
138
|
+
modes: globalModes
|
|
139
|
+
} = (0, _JFSThemeProvider.useTokens)();
|
|
140
|
+
const modes = (0, _react.useMemo)(() => ({
|
|
141
|
+
...globalModes,
|
|
142
|
+
...CATEGORY_CARD_DEFAULT_MODES,
|
|
143
|
+
...propModes
|
|
144
|
+
}), [globalModes, propModes]);
|
|
145
|
+
const resolvedModes = (0, _react.useMemo)(() => ({
|
|
146
|
+
...modes,
|
|
147
|
+
[CATEGORY_CARD_STATE]: active ? 'Active' : 'Idle'
|
|
148
|
+
}), [modes, active]);
|
|
149
|
+
const tokens = (0, _react.useMemo)(() => resolveCategoryCardTokens(resolvedModes, active), [resolvedModes, active]);
|
|
150
|
+
const {
|
|
151
|
+
style: labelLayoutStyle,
|
|
152
|
+
...labelTruncation
|
|
153
|
+
} = (0, _react.useMemo)(() => (0, _reactUtils.resolveTextLayout)({
|
|
154
|
+
disableTruncation,
|
|
155
|
+
singleLine,
|
|
156
|
+
numberOfLines: 1,
|
|
157
|
+
ellipsizeMode: 'tail'
|
|
158
|
+
}), [disableTruncation, singleLine]);
|
|
159
|
+
const processedImageSlot = (0, _react.useMemo)(() => {
|
|
160
|
+
if (!imageSlot) return null;
|
|
161
|
+
const processed = (0, _reactUtils.cloneChildrenWithModes)(imageSlot, resolvedModes);
|
|
162
|
+
return processed.length === 1 ? processed[0] : processed;
|
|
163
|
+
}, [imageSlot, resolvedModes]);
|
|
164
|
+
const imageNode = processedImageSlot ?? (imageSource !== undefined ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Image.default, {
|
|
165
|
+
imageSource: imageSource,
|
|
166
|
+
width: tokens.imageSize,
|
|
167
|
+
height: tokens.imageSize,
|
|
168
|
+
ratio: 1,
|
|
169
|
+
resizeMode: "cover",
|
|
170
|
+
borderRadius: tokens.imageRadius,
|
|
171
|
+
accessibilityElementsHidden: true,
|
|
172
|
+
importantForAccessibility: "no"
|
|
173
|
+
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_Image.default, {
|
|
174
|
+
imageSource: "https://www.figma.com/api/mcp/asset/0ee34871-646f-4736-be06-22fb6c0a4eed",
|
|
175
|
+
width: tokens.imageSize,
|
|
176
|
+
height: tokens.imageSize,
|
|
177
|
+
ratio: 1,
|
|
178
|
+
resizeMode: "cover",
|
|
179
|
+
borderRadius: tokens.imageRadius,
|
|
180
|
+
accessibilityElementsHidden: true,
|
|
181
|
+
importantForAccessibility: "no"
|
|
182
|
+
}));
|
|
183
|
+
const a11yLabel = accessibilityLabel ?? (badge ? `${label}, ${badge}` : label);
|
|
184
|
+
const webProps = (0, _webPlatformUtils.usePressableWebSupport)({
|
|
185
|
+
restProps: rest,
|
|
186
|
+
onPress: disabled ? undefined : onPress,
|
|
187
|
+
disabled,
|
|
188
|
+
accessibilityLabel: a11yLabel,
|
|
189
|
+
webAccessibilityProps
|
|
190
|
+
});
|
|
191
|
+
const badgeNode = badge ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
192
|
+
style: BADGE_ANCHOR,
|
|
193
|
+
pointerEvents: "none",
|
|
194
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Badge.default, {
|
|
195
|
+
label: badge,
|
|
196
|
+
modes: resolvedModes,
|
|
197
|
+
style: BADGE_CENTER_STYLE
|
|
198
|
+
})
|
|
199
|
+
}) : null;
|
|
200
|
+
|
|
201
|
+
// Truncation needs a width bound so the ellipsis engages. `singleLine`
|
|
202
|
+
// must NOT fill the content box — otherwise the text is pinned to the left
|
|
203
|
+
// padding edge and only grows rightward. Intrinsic width + centered
|
|
204
|
+
// alignment lets overflow expand equally on both sides.
|
|
205
|
+
const labelWidthStyle = singleLine ? LABEL_OVERFLOW_CENTER_STYLE : LABEL_FILL_STYLE;
|
|
206
|
+
const pressableStyle = (0, _react.useCallback)(({
|
|
207
|
+
pressed
|
|
208
|
+
}) => [tokens.containerStyle, {
|
|
209
|
+
width
|
|
210
|
+
}, badge ? {
|
|
211
|
+
marginTop: BADGE_OVERLAP
|
|
212
|
+
} : null, disabled ? disabledOverlayStyle : null, pressed && !disabled ? pressedOverlayStyle : null, style], [tokens.containerStyle, width, badge, disabled, style]);
|
|
213
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
|
|
214
|
+
accessibilityRole: "button",
|
|
215
|
+
accessibilityLabel: a11yLabel,
|
|
216
|
+
accessibilityHint: accessibilityHint,
|
|
217
|
+
accessibilityState: {
|
|
218
|
+
disabled,
|
|
219
|
+
selected: active,
|
|
220
|
+
...accessibilityState
|
|
221
|
+
},
|
|
222
|
+
onPress: onPress,
|
|
223
|
+
disabled: disabled,
|
|
224
|
+
style: pressableStyle,
|
|
225
|
+
testID: testID,
|
|
226
|
+
...webProps,
|
|
227
|
+
children: [badgeNode, imageNode, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
228
|
+
style: [tokens.labelStyle, labelWidthStyle, labelLayoutStyle, labelStyle],
|
|
229
|
+
...labelTruncation,
|
|
230
|
+
accessibilityElementsHidden: true,
|
|
231
|
+
importantForAccessibility: "no",
|
|
232
|
+
children: label
|
|
233
|
+
})]
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
const BADGE_ANCHOR = {
|
|
237
|
+
position: 'absolute',
|
|
238
|
+
top: -BADGE_OVERLAP,
|
|
239
|
+
left: 0,
|
|
240
|
+
right: 0,
|
|
241
|
+
alignItems: 'center',
|
|
242
|
+
justifyContent: 'center',
|
|
243
|
+
zIndex: 1
|
|
244
|
+
};
|
|
245
|
+
const BADGE_CENTER_STYLE = {
|
|
246
|
+
alignSelf: 'center'
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/** Lets the label fill the card width so the one-line ellipsis has a bound. */
|
|
250
|
+
const LABEL_FILL_STYLE = {
|
|
251
|
+
width: '100%',
|
|
252
|
+
alignSelf: 'stretch'
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Intrinsic width, horizontally centred. When the label is wider than the
|
|
257
|
+
* card it overflows left and right equally instead of growing only rightward
|
|
258
|
+
* from the padded content edge.
|
|
259
|
+
*/
|
|
260
|
+
const LABEL_OVERFLOW_CENTER_STYLE = {
|
|
261
|
+
alignSelf: 'center',
|
|
262
|
+
textAlign: 'center'
|
|
263
|
+
};
|
|
264
|
+
var _default = exports.default = /*#__PURE__*/_react.default.memo(CategoryCard);
|
|
@@ -113,7 +113,9 @@ function CompareTable({
|
|
|
113
113
|
modes = _reactUtils.EMPTY_MODES,
|
|
114
114
|
style,
|
|
115
115
|
disableTruncation,
|
|
116
|
-
columnWidth
|
|
116
|
+
columnWidth,
|
|
117
|
+
stickyHeaders = true,
|
|
118
|
+
stickyHeaderLabel = true
|
|
117
119
|
}) {
|
|
118
120
|
// --- selection card tokens ------------------------------------------------
|
|
119
121
|
const cardBg = (0, _figmaVariablesResolver.getVariableByName)('selectionCard/background/color', modes) ?? '#ffffff';
|
|
@@ -185,12 +187,12 @@ function CompareTable({
|
|
|
185
187
|
// Stable per-section ref-callback instances so React doesn't detach/reattach
|
|
186
188
|
// on every render (which would churn the map and re-fire scrollTo).
|
|
187
189
|
const bodyRefCallbacks = (0, _react.useRef)(new Map());
|
|
188
|
-
/**
|
|
189
|
-
*
|
|
190
|
+
/** Tracks which ScrollView the user is currently dragging. Only the
|
|
191
|
+
* actively-dragged ScrollView drives horizontal sync — programmatic
|
|
192
|
+
* scrollTo echoes from synced peers are ignored, eliminating jitter. */
|
|
193
|
+
const draggingSource = (0, _react.useRef)(null);
|
|
190
194
|
const lastSyncX = (0, _react.useRef)(0);
|
|
191
|
-
const
|
|
192
|
-
if (Math.abs(x - lastSyncX.current) < 0.5) return;
|
|
193
|
-
lastSyncX.current = x;
|
|
195
|
+
const syncOthers = (0, _react.useCallback)((source, x) => {
|
|
194
196
|
if (source !== 'cards' && cardsScrollRef.current) {
|
|
195
197
|
cardsScrollRef.current.scrollTo({
|
|
196
198
|
x,
|
|
@@ -206,12 +208,37 @@ function CompareTable({
|
|
|
206
208
|
}
|
|
207
209
|
});
|
|
208
210
|
}, []);
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
}, [
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
211
|
+
const onScrollBeginDrag = (0, _react.useCallback)(source => () => {
|
|
212
|
+
draggingSource.current = source;
|
|
213
|
+
}, []);
|
|
214
|
+
const onScrollEndDrag = (0, _react.useCallback)(source => e => {
|
|
215
|
+
const x = Math.round(e.nativeEvent.contentOffset.x);
|
|
216
|
+
if (x === lastSyncX.current) return;
|
|
217
|
+
lastSyncX.current = x;
|
|
218
|
+
syncOthers(source, x);
|
|
219
|
+
}, [syncOthers]);
|
|
220
|
+
const onMomentumScrollEnd = (0, _react.useCallback)(source => e => {
|
|
221
|
+
if (draggingSource.current === source) {
|
|
222
|
+
draggingSource.current = null;
|
|
223
|
+
}
|
|
224
|
+
const x = Math.round(e.nativeEvent.contentOffset.x);
|
|
225
|
+
if (x === lastSyncX.current) return;
|
|
226
|
+
lastSyncX.current = x;
|
|
227
|
+
syncOthers(source, x);
|
|
228
|
+
}, [syncOthers]);
|
|
229
|
+
|
|
230
|
+
/** Real-time scroll sync: only the actively-dragged ScrollView drives
|
|
231
|
+
* sync on native — echoed onScroll events from programmatic scrollTo
|
|
232
|
+
* on other ScrollViews are ignored because draggingSource won't match.
|
|
233
|
+
* On web, onScrollBeginDrag never fires for mouse/trackpad, so skip
|
|
234
|
+
* the guard and rely on the lastSyncX rounded-value check instead. */
|
|
235
|
+
const onScroll = (0, _react.useCallback)(source => e => {
|
|
236
|
+
if (_reactNative.Platform.OS !== 'web' && draggingSource.current !== source) return;
|
|
237
|
+
const x = Math.round(e.nativeEvent.contentOffset.x);
|
|
238
|
+
if (x === lastSyncX.current) return;
|
|
239
|
+
lastSyncX.current = x;
|
|
240
|
+
syncOthers(source, x);
|
|
241
|
+
}, [syncOthers]);
|
|
215
242
|
const getBodyRefCallback = (0, _react.useCallback)(idx => {
|
|
216
243
|
let cb = bodyRefCallbacks.current.get(idx);
|
|
217
244
|
if (!cb) {
|
|
@@ -442,55 +469,45 @@ function CompareTable({
|
|
|
442
469
|
})
|
|
443
470
|
})]
|
|
444
471
|
});
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
472
|
+
const renderHeaderContent = section => section.header != null ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
473
|
+
style: {
|
|
474
|
+
backgroundColor: headerBg,
|
|
475
|
+
paddingHorizontal: headerPaddingH,
|
|
476
|
+
paddingVertical: headerPaddingV,
|
|
477
|
+
borderBottomWidth: cellBorderSize,
|
|
478
|
+
borderBottomColor: cellBorderColor
|
|
479
|
+
},
|
|
480
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
481
|
+
style: headerTextStyle,
|
|
482
|
+
children: section.header
|
|
483
|
+
})
|
|
484
|
+
}) : null;
|
|
485
|
+
const renderBodyContent = (section, index) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
|
|
486
|
+
ref: getBodyRefCallback(index),
|
|
487
|
+
horizontal: true,
|
|
488
|
+
showsHorizontalScrollIndicator: false,
|
|
489
|
+
onScroll: onScroll(index),
|
|
490
|
+
onScrollBeginDrag: onScrollBeginDrag(index),
|
|
491
|
+
onScrollEndDrag: onScrollEndDrag(index),
|
|
492
|
+
onMomentumScrollEnd: onMomentumScrollEnd(index),
|
|
493
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
457
494
|
style: {
|
|
458
|
-
width:
|
|
459
|
-
backgroundColor: headerBg,
|
|
460
|
-
paddingHorizontal: headerPaddingH,
|
|
461
|
-
paddingVertical: headerPaddingV,
|
|
462
|
-
borderBottomWidth: cellBorderSize,
|
|
463
|
-
borderBottomColor: cellBorderColor
|
|
495
|
+
width: contentWidth
|
|
464
496
|
},
|
|
465
|
-
children:
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
})
|
|
497
|
+
children: section.rows.map((row, rowIndex) => {
|
|
498
|
+
const isLastRow = rowIndex === section.rows.length - 1;
|
|
499
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
500
|
+
style: {
|
|
501
|
+
flexDirection: 'row',
|
|
502
|
+
width: '100%'
|
|
503
|
+
},
|
|
504
|
+
children: [Array.from({
|
|
505
|
+
length: columnCount
|
|
506
|
+
}).map((_, colIndex) => renderTableCell(renderCellContent(row.values?.[colIndex], `${rowIndex}-${colIndex}`), colIndex, isLastRow, colIndex)), showAddCard && renderTableCell(null, gridColumnCount - 1, isLastRow, '__add_spacer__')]
|
|
507
|
+
}, row.key ?? rowIndex);
|
|
491
508
|
})
|
|
492
|
-
})
|
|
493
|
-
}
|
|
509
|
+
})
|
|
510
|
+
});
|
|
494
511
|
const outerStyle = {
|
|
495
512
|
width: '100%',
|
|
496
513
|
backgroundColor: cardBg,
|
|
@@ -524,43 +541,82 @@ function CompareTable({
|
|
|
524
541
|
}
|
|
525
542
|
|
|
526
543
|
// --- fixed-width scroll mode: chained horizontal sync + sticky cards -----
|
|
527
|
-
//
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
|
|
531
|
-
|
|
544
|
+
// Build children and stickyHeaderIndices for the vertical ScrollView.
|
|
545
|
+
// When stickyHeaders is on, section headers are injected as direct children
|
|
546
|
+
// so React Native's stickyHeaderIndices can pin them with stacking.
|
|
547
|
+
const scrollChildren = [];
|
|
548
|
+
const stickyIndices = [];
|
|
549
|
+
if (showCardsRow) {
|
|
550
|
+
stickyIndices.push(scrollChildren.length);
|
|
551
|
+
scrollChildren.push(/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
|
|
552
|
+
ref: cardsScrollRef,
|
|
553
|
+
horizontal: true,
|
|
554
|
+
showsHorizontalScrollIndicator: false,
|
|
555
|
+
scrollEventThrottle: 16,
|
|
556
|
+
onScroll: onScroll('cards'),
|
|
557
|
+
onScrollBeginDrag: onScrollBeginDrag('cards'),
|
|
558
|
+
onScrollEndDrag: onScrollEndDrag('cards'),
|
|
559
|
+
onMomentumScrollEnd: onMomentumScrollEnd('cards'),
|
|
560
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
561
|
+
style: {
|
|
562
|
+
flexDirection: 'row',
|
|
563
|
+
width: contentWidth
|
|
564
|
+
},
|
|
565
|
+
children: [columns.map(renderCard), showAddCard && renderAddCard()]
|
|
566
|
+
})
|
|
567
|
+
}, "cards-row"));
|
|
568
|
+
}
|
|
569
|
+
sections.forEach((section, index) => {
|
|
570
|
+
const sectionKey = section.key ?? section.title ?? index;
|
|
571
|
+
const headerContent = renderHeaderContent(section);
|
|
572
|
+
if (stickyHeaders && headerContent != null) {
|
|
573
|
+
stickyIndices.push(scrollChildren.length);
|
|
574
|
+
scrollChildren.push(/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
575
|
+
style: {
|
|
576
|
+
width: '100%'
|
|
577
|
+
},
|
|
578
|
+
children: stickyHeaderLabel ? headerContent : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
|
|
579
|
+
horizontal: true,
|
|
580
|
+
showsHorizontalScrollIndicator: false,
|
|
581
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
582
|
+
style: {
|
|
583
|
+
width: contentWidth
|
|
584
|
+
},
|
|
585
|
+
children: headerContent
|
|
586
|
+
})
|
|
587
|
+
})
|
|
588
|
+
}, `sticky-header-${sectionKey}`));
|
|
589
|
+
}
|
|
590
|
+
scrollChildren.push(/*#__PURE__*/(0, _jsxRuntime.jsxs)(_Accordion.default, {
|
|
591
|
+
title: section.title,
|
|
592
|
+
defaultExpanded: section.defaultExpanded ?? index === 0,
|
|
593
|
+
modes: modes,
|
|
594
|
+
children: [!stickyHeaders && headerContent != null && (stickyHeaderLabel ? headerContent : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
|
|
595
|
+
horizontal: true,
|
|
596
|
+
showsHorizontalScrollIndicator: false,
|
|
597
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
598
|
+
style: {
|
|
599
|
+
width: contentWidth
|
|
600
|
+
},
|
|
601
|
+
children: headerContent
|
|
602
|
+
})
|
|
603
|
+
})), renderBodyContent(section, index)]
|
|
604
|
+
}, sectionKey));
|
|
605
|
+
});
|
|
532
606
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
533
607
|
style: [outerStyle, style, {
|
|
534
608
|
flexDirection: 'column',
|
|
535
609
|
flex: 1
|
|
536
610
|
}],
|
|
537
|
-
children: /*#__PURE__*/(0, _jsxRuntime.
|
|
611
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
|
|
538
612
|
style: {
|
|
539
613
|
width: '100%',
|
|
540
614
|
flex: 1
|
|
541
615
|
},
|
|
542
|
-
stickyHeaderIndices:
|
|
616
|
+
stickyHeaderIndices: stickyIndices,
|
|
543
617
|
showsVerticalScrollIndicator: false,
|
|
544
618
|
directionalLockEnabled: true,
|
|
545
|
-
children:
|
|
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
|
-
})]
|
|
619
|
+
children: scrollChildren
|
|
564
620
|
})
|
|
565
621
|
});
|
|
566
622
|
}
|
|
@@ -109,6 +109,7 @@ function ContentSheet({
|
|
|
109
109
|
safeAreaBottom = true,
|
|
110
110
|
pinToBottom = true,
|
|
111
111
|
visible = true,
|
|
112
|
+
maxHeightPercent = 0.7,
|
|
112
113
|
style,
|
|
113
114
|
...rest
|
|
114
115
|
}) {
|
|
@@ -120,9 +121,6 @@ function ContentSheet({
|
|
|
120
121
|
const resolved = (0, _react.useMemo)(() => resolveContentSheetStyle(modes), [modes]);
|
|
121
122
|
const processedChildren = (0, _react.useMemo)(() => (0, _reactUtils.cloneChildrenWithModes)((0, _reactUtils.flattenChildren)(children), modes), [children, modes]);
|
|
122
123
|
const paddingBottom = resolved.paddingBottom + (safeAreaBottom ? bottomInset : 0);
|
|
123
|
-
const containerStyle = (0, _react.useMemo)(() => [pinToBottom ? pinnedStyle : null, resolved.container, {
|
|
124
|
-
paddingBottom
|
|
125
|
-
}, style], [pinToBottom, resolved.container, paddingBottom, style]);
|
|
126
124
|
|
|
127
125
|
// Entrance / exit "pop up from bottom" animation — mirrors the Drawer. The
|
|
128
126
|
// sheet is bottom-anchored, so a positive `translateY` equal to the window
|
|
@@ -132,6 +130,11 @@ function ContentSheet({
|
|
|
132
130
|
const {
|
|
133
131
|
height: windowHeight
|
|
134
132
|
} = (0, _reactNative.useWindowDimensions)();
|
|
133
|
+
const maxHeight = windowHeight * maxHeightPercent;
|
|
134
|
+
const containerStyle = (0, _react.useMemo)(() => [pinToBottom ? pinnedStyle : null, resolved.container, {
|
|
135
|
+
maxHeight,
|
|
136
|
+
paddingBottom
|
|
137
|
+
}, style], [pinToBottom, resolved.container, maxHeight, paddingBottom, style]);
|
|
135
138
|
const entrance = (0, _reactNativeReanimated.useSharedValue)(windowHeight);
|
|
136
139
|
(0, _react.useEffect)(() => {
|
|
137
140
|
entrance.value = (0, _reactNativeReanimated.withSpring)(visible ? 0 : windowHeight, SPRING_CONFIG);
|
|
@@ -155,6 +158,7 @@ function ContentSheet({
|
|
|
155
158
|
style: containerStyle,
|
|
156
159
|
spacing: keyboardSpacing,
|
|
157
160
|
entrance: entrance,
|
|
161
|
+
maxHeight: maxHeight,
|
|
158
162
|
...rest,
|
|
159
163
|
children: [header, processedChildren]
|
|
160
164
|
});
|
|
@@ -162,6 +166,7 @@ function ContentSheet({
|
|
|
162
166
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(AnimatedSheet, {
|
|
163
167
|
style: containerStyle,
|
|
164
168
|
entrance: entrance,
|
|
169
|
+
maxHeight: maxHeight,
|
|
165
170
|
...rest,
|
|
166
171
|
children: [header, processedChildren]
|
|
167
172
|
});
|
|
@@ -178,6 +183,7 @@ function KeyboardAwareSheet({
|
|
|
178
183
|
style,
|
|
179
184
|
spacing,
|
|
180
185
|
entrance,
|
|
186
|
+
maxHeight,
|
|
181
187
|
children,
|
|
182
188
|
...rest
|
|
183
189
|
}) {
|
|
@@ -194,7 +200,15 @@ function KeyboardAwareSheet({
|
|
|
194
200
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
|
|
195
201
|
style: [style, animatedStyle],
|
|
196
202
|
...rest,
|
|
197
|
-
children:
|
|
203
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
|
|
204
|
+
style: {
|
|
205
|
+
maxHeight
|
|
206
|
+
},
|
|
207
|
+
bounces: false,
|
|
208
|
+
showsVerticalScrollIndicator: false,
|
|
209
|
+
keyboardShouldPersistTaps: "handled",
|
|
210
|
+
children: children
|
|
211
|
+
})
|
|
198
212
|
});
|
|
199
213
|
}
|
|
200
214
|
|
|
@@ -206,6 +220,7 @@ function KeyboardAwareSheet({
|
|
|
206
220
|
function AnimatedSheet({
|
|
207
221
|
style,
|
|
208
222
|
entrance,
|
|
223
|
+
maxHeight,
|
|
209
224
|
children,
|
|
210
225
|
...rest
|
|
211
226
|
}) {
|
|
@@ -219,7 +234,15 @@ function AnimatedSheet({
|
|
|
219
234
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
|
|
220
235
|
style: [style, animatedStyle],
|
|
221
236
|
...rest,
|
|
222
|
-
children:
|
|
237
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
|
|
238
|
+
style: {
|
|
239
|
+
maxHeight
|
|
240
|
+
},
|
|
241
|
+
bounces: false,
|
|
242
|
+
showsVerticalScrollIndicator: false,
|
|
243
|
+
keyboardShouldPersistTaps: "handled",
|
|
244
|
+
children: children
|
|
245
|
+
})
|
|
223
246
|
});
|
|
224
247
|
}
|
|
225
248
|
var _default = exports.default = ContentSheet;
|