jfs-components 0.1.28 → 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 +28 -2
- package/lib/commonjs/components/CategoryCard/CategoryCard.js +264 -0
- package/lib/commonjs/components/CompareTable/CompareTable.js +247 -25
- package/lib/commonjs/components/ContentSheet/ContentSheet.js +91 -8
- 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/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/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 +247 -26
- package/lib/module/components/ContentSheet/ContentSheet.js +94 -11
- 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/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/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 +36 -1
- package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +20 -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/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/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 +324 -30
- package/src/components/ContentSheet/ContentSheet.tsx +120 -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/MoneyValue/MoneyValue.tsx +216 -65
- package/src/components/PdpCcCard/PdpCcCard.tsx +36 -1
- package/src/components/Table/Table.tsx +29 -4
- 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,30 @@ 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
|
+
|
|
20
|
+
## [0.1.30] - 2026-07-10
|
|
21
|
+
|
|
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`).
|
|
23
|
+
- `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`.
|
|
24
|
+
- `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.
|
|
25
|
+
- `PdpCcCard` — new `numberOfLines`, `disableTruncation`, and `singleLine` props passed through to the `Title` component for precise control over title truncation and wrapping.
|
|
26
|
+
- `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`.
|
|
27
|
+
- Stories and MDX documentation updated for all new features.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
7
31
|
## [0.1.28] - 2026-07-07
|
|
8
32
|
|
|
9
33
|
- 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 +41,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
17
41
|
|
|
18
42
|
## [0.1.27] - 2026-07-07
|
|
19
43
|
|
|
20
|
-
- Added `ProfileCard` — dark rounded profile summary card with `Attached` monogram `Avatar
|
|
44
|
+
- 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`.
|
|
45
|
+
- `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.
|
|
46
|
+
- `Section` — `Section.Bento` stories updated with `UpiHandle` pills that pass explicit avatar `source` values, matching the Figma Bento nav/upi layout (node 216-13845).
|
|
47
|
+
- Figma token sync — `context 10` mode renamed `Profile card` → `jioPlus`; `Profile Card Appearance` collection wired for `ProfileCard` / `UpiHandle`.
|
|
21
48
|
- `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
49
|
- `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
50
|
- Regenerated component MDX token documentation and `.token-metadata.json` across the library.
|
|
25
51
|
|
|
26
52
|
---
|
|
@@ -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);
|
|
@@ -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,10 @@ function CompareTable({
|
|
|
111
112
|
maxColumns = 4,
|
|
112
113
|
modes = _reactUtils.EMPTY_MODES,
|
|
113
114
|
style,
|
|
114
|
-
disableTruncation
|
|
115
|
+
disableTruncation,
|
|
116
|
+
columnWidth,
|
|
117
|
+
stickyHeaders = true,
|
|
118
|
+
stickyHeaderLabel = true
|
|
115
119
|
}) {
|
|
116
120
|
// --- selection card tokens ------------------------------------------------
|
|
117
121
|
const cardBg = (0, _figmaVariablesResolver.getVariableByName)('selectionCard/background/color', modes) ?? '#ffffff';
|
|
@@ -168,6 +172,95 @@ function CompareTable({
|
|
|
168
172
|
const showAddCard = onAddColumn != null && columnCount < maxColumns;
|
|
169
173
|
/** Grid columns shared by the selection-card row and every table row. */
|
|
170
174
|
const gridColumnCount = columnCount + (showAddCard ? 1 : 0);
|
|
175
|
+
const showCardsRow = columnCount > 0 || showAddCard;
|
|
176
|
+
|
|
177
|
+
// --- fixed-width scroll mode -------------------------------------------
|
|
178
|
+
// `columnWidth` opts the component into scroll mode (see prop doc). In that
|
|
179
|
+
// mode the cards row and each section's table body are independent
|
|
180
|
+
// horizontal ScrollViews; their scroll events are chained so they move in
|
|
181
|
+
// lockstep, and the cards row pins to the top on the vertical axis.
|
|
182
|
+
const useFixedWidth = columnWidth != null;
|
|
183
|
+
/** Total scrollable content width shared by every horizontal surface. */
|
|
184
|
+
const contentWidth = useFixedWidth ? gridColumnCount * columnWidth : 0;
|
|
185
|
+
const cardsScrollRef = (0, _react.useRef)(null);
|
|
186
|
+
const bodyScrollRefs = (0, _react.useRef)(new Map());
|
|
187
|
+
// Stable per-section ref-callback instances so React doesn't detach/reattach
|
|
188
|
+
// on every render (which would churn the map and re-fire scrollTo).
|
|
189
|
+
const bodyRefCallbacks = (0, _react.useRef)(new Map());
|
|
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);
|
|
194
|
+
const lastSyncX = (0, _react.useRef)(0);
|
|
195
|
+
const syncOthers = (0, _react.useCallback)((source, x) => {
|
|
196
|
+
if (source !== 'cards' && cardsScrollRef.current) {
|
|
197
|
+
cardsScrollRef.current.scrollTo({
|
|
198
|
+
x,
|
|
199
|
+
animated: false
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
bodyScrollRefs.current.forEach((ref, idx) => {
|
|
203
|
+
if (source === 'cards' || idx !== source) {
|
|
204
|
+
ref.scrollTo({
|
|
205
|
+
x,
|
|
206
|
+
animated: false
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}, []);
|
|
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]);
|
|
242
|
+
const getBodyRefCallback = (0, _react.useCallback)(idx => {
|
|
243
|
+
let cb = bodyRefCallbacks.current.get(idx);
|
|
244
|
+
if (!cb) {
|
|
245
|
+
cb = ref => {
|
|
246
|
+
if (ref) {
|
|
247
|
+
bodyScrollRefs.current.set(idx, ref);
|
|
248
|
+
// Bring a freshly-expanded section's body into sync with
|
|
249
|
+
// the rest of the surfaces.
|
|
250
|
+
if (lastSyncX.current > 0) {
|
|
251
|
+
ref.scrollTo({
|
|
252
|
+
x: lastSyncX.current,
|
|
253
|
+
animated: false
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
bodyScrollRefs.current.delete(idx);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
bodyRefCallbacks.current.set(idx, cb);
|
|
261
|
+
}
|
|
262
|
+
return cb;
|
|
263
|
+
}, []);
|
|
171
264
|
const cardLabelStyle = {
|
|
172
265
|
...NO_WRAP_TEXT,
|
|
173
266
|
color: cardLabelColor,
|
|
@@ -198,8 +291,12 @@ function CompareTable({
|
|
|
198
291
|
fontWeight: toWeight(cellFontWeight)
|
|
199
292
|
};
|
|
200
293
|
const cardStyle = {
|
|
201
|
-
|
|
202
|
-
|
|
294
|
+
...(useFixedWidth ? {
|
|
295
|
+
width: columnWidth
|
|
296
|
+
} : {
|
|
297
|
+
flex: 1,
|
|
298
|
+
minWidth: 0
|
|
299
|
+
}),
|
|
203
300
|
minHeight: 147,
|
|
204
301
|
backgroundColor: cardBg,
|
|
205
302
|
borderRadius: cardRadius,
|
|
@@ -318,6 +415,7 @@ function CompareTable({
|
|
|
318
415
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_Table.default.Cell, {
|
|
319
416
|
modes: modes,
|
|
320
417
|
flex: 1,
|
|
418
|
+
width: useFixedWidth ? columnWidth : undefined,
|
|
321
419
|
align: "left",
|
|
322
420
|
isLastColumn: isLastCol,
|
|
323
421
|
isLastRow: isLastRow,
|
|
@@ -371,31 +469,155 @@ function CompareTable({
|
|
|
371
469
|
})
|
|
372
470
|
})]
|
|
373
471
|
});
|
|
374
|
-
|
|
375
|
-
style:
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
},
|
|
382
|
-
children:
|
|
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, {
|
|
383
494
|
style: {
|
|
384
|
-
|
|
385
|
-
width: '100%'
|
|
495
|
+
width: contentWidth
|
|
386
496
|
},
|
|
387
|
-
children:
|
|
388
|
-
|
|
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);
|
|
508
|
+
})
|
|
509
|
+
})
|
|
510
|
+
});
|
|
511
|
+
const outerStyle = {
|
|
512
|
+
width: '100%',
|
|
513
|
+
backgroundColor: cardBg,
|
|
514
|
+
borderWidth: 1,
|
|
515
|
+
borderColor: '#e8e8e8',
|
|
516
|
+
overflow: 'hidden'
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// --- flex mode (columnWidth omitted): original behaviour, unchanged -------
|
|
520
|
+
if (!useFixedWidth) {
|
|
521
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
522
|
+
style: [outerStyle, style],
|
|
523
|
+
children: [showCardsRow && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
524
|
+
style: {
|
|
525
|
+
flexDirection: 'row',
|
|
526
|
+
width: '100%'
|
|
527
|
+
},
|
|
528
|
+
children: [columns.map(renderCard), showAddCard && renderAddCard()]
|
|
529
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
530
|
+
style: {
|
|
531
|
+
width: '100%'
|
|
532
|
+
},
|
|
533
|
+
children: sections.map((section, index) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_Accordion.default, {
|
|
534
|
+
title: section.title,
|
|
535
|
+
defaultExpanded: section.defaultExpanded ?? index === 0,
|
|
536
|
+
modes: modes,
|
|
537
|
+
children: renderTable(section)
|
|
538
|
+
}, section.key ?? section.title ?? index))
|
|
539
|
+
})]
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// --- fixed-width scroll mode: chained horizontal sync + sticky cards -----
|
|
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
|
+
});
|
|
606
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
607
|
+
style: [outerStyle, style, {
|
|
608
|
+
flexDirection: 'column',
|
|
609
|
+
flex: 1
|
|
610
|
+
}],
|
|
611
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
|
|
389
612
|
style: {
|
|
390
|
-
width: '100%'
|
|
613
|
+
width: '100%',
|
|
614
|
+
flex: 1
|
|
391
615
|
},
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}, section.key ?? section.title ?? index))
|
|
398
|
-
})]
|
|
616
|
+
stickyHeaderIndices: stickyIndices,
|
|
617
|
+
showsVerticalScrollIndicator: false,
|
|
618
|
+
directionalLockEnabled: true,
|
|
619
|
+
children: scrollChildren
|
|
620
|
+
})
|
|
399
621
|
});
|
|
400
622
|
}
|
|
401
623
|
var _default = exports.default = CompareTable;
|