jfs-components 0.0.78 → 0.0.79

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/lib/commonjs/components/Attached/Attached.js +144 -0
  3. package/lib/commonjs/components/Card/Card.js +25 -2
  4. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +4 -6
  5. package/lib/commonjs/components/ListItem/ListItem.js +22 -15
  6. package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +328 -0
  7. package/lib/commonjs/components/Slot/Slot.js +73 -0
  8. package/lib/commonjs/components/index.js +21 -0
  9. package/lib/commonjs/icons/registry.js +1 -1
  10. package/lib/module/components/Attached/Attached.js +139 -0
  11. package/lib/module/components/Card/Card.js +25 -2
  12. package/lib/module/components/FullscreenModal/FullscreenModal.js +4 -6
  13. package/lib/module/components/ListItem/ListItem.js +22 -15
  14. package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +322 -0
  15. package/lib/module/components/Slot/Slot.js +68 -0
  16. package/lib/module/components/index.js +3 -0
  17. package/lib/module/icons/registry.js +1 -1
  18. package/lib/typescript/src/components/Attached/Attached.d.ts +61 -0
  19. package/lib/typescript/src/components/Card/Card.d.ts +9 -2
  20. package/lib/typescript/src/components/ListItem/ListItem.d.ts +15 -5
  21. package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +64 -0
  22. package/lib/typescript/src/components/Slot/Slot.d.ts +52 -0
  23. package/lib/typescript/src/components/index.d.ts +3 -0
  24. package/lib/typescript/src/icons/registry.d.ts +1 -1
  25. package/package.json +1 -1
  26. package/src/components/Attached/Attached.tsx +181 -0
  27. package/src/components/Card/Card.tsx +28 -1
  28. package/src/components/FullscreenModal/FullscreenModal.tsx +3 -3
  29. package/src/components/ListItem/ListItem.tsx +35 -16
  30. package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +426 -0
  31. package/src/components/Slot/Slot.tsx +91 -0
  32. package/src/components/index.ts +3 -0
  33. package/src/icons/registry.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [0.0.79] - 2026-05-30
8
+
9
+ - Added `Attached` — positions a badge over main content at nine anchor points (corners, edges, center).
10
+ - Added `PlanComparisonCard` — plan comparison table with column headers, feature rows, brand highlight, and cell values (text / cross / custom node).
11
+ - Added `Slot` — token-driven vertical/horizontal layout container with `modes` cascade to children.
12
+ - `Card` — new `header` slot above media (e.g. brand logo).
13
+ - `ListItem` — new `trailing` slot; `endSlot` deprecated (still honored).
14
+ - `FullscreenModal` — footer action stack now uses `Slot` instead of a raw `View`.
15
+
16
+ ---
17
+
7
18
  ## [0.0.78] - 2026-05-29
8
19
 
9
20
  - Added `ExpandableCheckbox` — checkbox row with collapsible long label and Read more / Read less toggle.
@@ -0,0 +1,144 @@
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 _JFSThemeProvider = require("../../design-tokens/JFSThemeProvider");
10
+ var _reactUtils = require("../../utils/react-utils");
11
+ var _jsxRuntime = require("react/jsx-runtime");
12
+ 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); }
13
+ /**
14
+ * Anchor point on the main content where the attached `badge` is centered.
15
+ * Mirrors the nine Figma `position` variants (corners, edge midpoints, center).
16
+ */
17
+
18
+ const ZERO_SIZE = {
19
+ width: 0,
20
+ height: 0
21
+ };
22
+
23
+ /**
24
+ * Fraction (0 | 0.5 | 1) of the main content's width/height at which the badge
25
+ * center should sit, derived from the `position` anchor.
26
+ */
27
+ function resolveAnchorFractions(position) {
28
+ const fx = position.includes('left') ? 0 : position.includes('right') ? 1 : 0.5;
29
+ const fy = position.startsWith('top') ? 0 : position.startsWith('bottom') ? 1 : 0.5;
30
+ return {
31
+ fx,
32
+ fy
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Attached — overlays a small `badge` on top of arbitrary main content,
38
+ * centered on one of nine anchor points (corners, edge midpoints, or center).
39
+ *
40
+ * The badge straddles the chosen anchor regardless of either element's size:
41
+ * both the main content and the badge are measured via `onLayout`, then the
42
+ * badge is absolutely positioned so its center lands exactly on the anchor.
43
+ *
44
+ * @example
45
+ * ```tsx
46
+ * <Attached position="bottom-right" badge={<InstitutionBadge modes={modes} />} modes={modes}>
47
+ * <IconCapsule iconName="ic_card" modes={modes} />
48
+ * </Attached>
49
+ * ```
50
+ */
51
+ function Attached({
52
+ children,
53
+ badge,
54
+ position = 'bottom-right',
55
+ circular = true,
56
+ modes: propModes = _reactUtils.EMPTY_MODES,
57
+ style,
58
+ ...rest
59
+ }) {
60
+ const {
61
+ modes: globalModes
62
+ } = (0, _JFSThemeProvider.useTokens)();
63
+ const modes = (0, _react.useMemo)(() => globalModes === _reactUtils.EMPTY_MODES && propModes === _reactUtils.EMPTY_MODES ? _reactUtils.EMPTY_MODES : {
64
+ ...globalModes,
65
+ ...propModes
66
+ }, [globalModes, propModes]);
67
+ const [mainSize, setMainSize] = (0, _react.useState)(ZERO_SIZE);
68
+ const [badgeSize, setBadgeSize] = (0, _react.useState)(ZERO_SIZE);
69
+ const onMainLayout = (0, _react.useCallback)(e => {
70
+ const {
71
+ width,
72
+ height
73
+ } = e.nativeEvent.layout;
74
+ setMainSize(prev => prev.width === width && prev.height === height ? prev : {
75
+ width,
76
+ height
77
+ });
78
+ }, []);
79
+ const onBadgeLayout = (0, _react.useCallback)(e => {
80
+ const {
81
+ width,
82
+ height
83
+ } = e.nativeEvent.layout;
84
+ setBadgeSize(prev => prev.width === width && prev.height === height ? prev : {
85
+ width,
86
+ height
87
+ });
88
+ }, []);
89
+ const mainChildren = (0, _react.useMemo)(() => children != null ? (0, _reactUtils.cloneChildrenWithModes)(children, modes) : null, [children, modes]);
90
+ const badgeChildren = (0, _react.useMemo)(() => badge != null ? (0, _reactUtils.cloneChildrenWithModes)(badge, modes) : null, [badge, modes]);
91
+ const badgePlacement = (0, _react.useMemo)(() => {
92
+ const {
93
+ fx,
94
+ fy
95
+ } = resolveAnchorFractions(position);
96
+ const measured = mainSize.width > 0 && badgeSize.width > 0;
97
+ let anchorX;
98
+ let anchorY;
99
+ if (circular) {
100
+ // Project the anchor onto the circle inscribed in the bounding box, so
101
+ // corner badges land on the circumference (45°) instead of the box corner.
102
+ const cx = mainSize.width / 2;
103
+ const cy = mainSize.height / 2;
104
+ const radius = Math.min(mainSize.width, mainSize.height) / 2;
105
+ const dx = (fx - 0.5) * 2; // -1 | 0 | 1
106
+ const dy = (fy - 0.5) * 2; // -1 | 0 | 1
107
+ const len = Math.hypot(dx, dy) || 1; // 'center' → 0, guard against /0
108
+ anchorX = cx + dx / len * radius;
109
+ anchorY = cy + dy / len * radius;
110
+ } else {
111
+ anchorX = mainSize.width * fx;
112
+ anchorY = mainSize.height * fy;
113
+ }
114
+ return {
115
+ position: 'absolute',
116
+ left: anchorX - badgeSize.width / 2,
117
+ top: anchorY - badgeSize.height / 2,
118
+ // Hide until both elements are measured to avoid a one-frame flash at (0,0).
119
+ opacity: measured ? 1 : 0
120
+ };
121
+ }, [position, circular, mainSize, badgeSize]);
122
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
123
+ style: [styles.container, style],
124
+ ...rest,
125
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
126
+ onLayout: onMainLayout,
127
+ children: mainChildren
128
+ }), badgeChildren != null && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
129
+ style: badgePlacement,
130
+ onLayout: onBadgeLayout,
131
+ pointerEvents: "box-none",
132
+ children: badgeChildren
133
+ })]
134
+ });
135
+ }
136
+ const styles = {
137
+ // alignSelf flex-start so the wrapper hugs the main content; anchors are then
138
+ // computed relative to the content size rather than a stretched parent.
139
+ container: {
140
+ position: 'relative',
141
+ alignSelf: 'flex-start'
142
+ }
143
+ };
144
+ var _default = exports.default = /*#__PURE__*/_react.default.memo(Attached);
@@ -20,9 +20,11 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
20
20
  * Card component implementation from Figma node 765:6186.
21
21
  *
22
22
  * Supports a `media` slot (with aspect ratio) and a content area.
23
+ * Supports an optional `header` slot (e.g. a brand logo), a `media` slot
24
+ * (with aspect ratio) and a content area.
23
25
  * Usage:
24
26
  * ```tsx
25
- * <Card media={<Image source={...} />} modes={modes}>
27
+ * <Card header={<GoldLogo />} media={<Image source={...} />} modes={modes}>
26
28
  * <Card.SupportText>Support text</Card.SupportText>
27
29
  * <Card.Title>Title</Card.Title>
28
30
  * <Card.SupportText>Support text</Card.SupportText>
@@ -30,6 +32,7 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
30
32
  * ```
31
33
  */
32
34
  function Card({
35
+ header,
33
36
  media,
34
37
  children,
35
38
  modes = _reactUtils.EMPTY_MODES,
@@ -57,6 +60,14 @@ function Card({
57
60
  ...modes
58
61
  }
59
62
  }) : media;
63
+
64
+ // Clone header to pass modes if it's a valid element
65
+ const headerWithModes = /*#__PURE__*/(0, _react.isValidElement)(header) ? /*#__PURE__*/(0, _react.cloneElement)(header, {
66
+ modes: {
67
+ ...header.props.modes,
68
+ ...modes
69
+ }
70
+ }) : header;
60
71
  const containerStyle = {
61
72
  backgroundColor,
62
73
  borderColor,
@@ -67,6 +78,15 @@ function Card({
67
78
  paddingVertical,
68
79
  overflow: 'hidden' // Ensure border radius clips content
69
80
  };
81
+
82
+ // Header wrap uses fixed padding from Figma (no dedicated tokens defined).
83
+ const headerWrapperStyle = {
84
+ width: '100%',
85
+ flexDirection: 'row',
86
+ alignItems: 'flex-start',
87
+ paddingHorizontal: 12,
88
+ paddingVertical: 16
89
+ };
70
90
  const mediaWrapperStyle = {
71
91
  width: '100%',
72
92
  aspectRatio: mediaAspectRatio,
@@ -87,7 +107,10 @@ function Card({
87
107
  },
88
108
  children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
89
109
  style: [containerStyle, style],
90
- children: [media && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
110
+ children: [header && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
111
+ style: headerWrapperStyle,
112
+ children: headerWithModes
113
+ }), media && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
91
114
  style: mediaWrapperStyle,
92
115
  children: mediaWithModes
93
116
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
@@ -14,6 +14,7 @@ var _Button = _interopRequireDefault(require("../Button/Button"));
14
14
  var _Disclaimer = _interopRequireDefault(require("../Disclaimer/Disclaimer"));
15
15
  var _IconButton = _interopRequireDefault(require("../IconButton/IconButton"));
16
16
  var _ActionFooter = _interopRequireDefault(require("../ActionFooter/ActionFooter"));
17
+ var _Slot = _interopRequireDefault(require("../Slot/Slot"));
17
18
  var _jsxRuntime = require("react/jsx-runtime");
18
19
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
19
20
  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); }
@@ -274,8 +275,9 @@ function FullscreenModal({
274
275
  if (footer) {
275
276
  footerContent = footer;
276
277
  } else if (primaryActionLabel) {
277
- footerContent = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
278
- style: footerColumnStyle,
278
+ footerContent = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_Slot.default, {
279
+ layoutDirection: "vertical",
280
+ modes: modes,
279
281
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_Button.default, {
280
282
  label: primaryActionLabel,
281
283
  modes: modes,
@@ -340,10 +342,6 @@ const scrollViewStyle = {
340
342
  const scrollContentStyle = {
341
343
  flexGrow: 1
342
344
  };
343
- const footerColumnStyle = {
344
- width: '100%',
345
- gap: 8
346
- };
347
345
  const fullWidthStyle = {
348
346
  width: '100%'
349
347
  };
@@ -21,9 +21,10 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
21
21
  const IS_IOS = _reactNative.Platform.OS === 'ios';
22
22
  const PRESS_DELAY = IS_IOS ? 130 : 0;
23
23
 
24
- // Forced modes for the endSlot — `Context: 'ListItem'` can never be
25
- // overridden by external modes. Frozen so identity is stable across renders.
26
- const END_SLOT_FORCED_MODES = Object.freeze({
24
+ // Forced modes for the leading/trailing slots — `Context: 'ListItem'` can
25
+ // never be overridden by external modes. Frozen so identity is stable across
26
+ // renders. Applied to both slots so they cascade modes identically.
27
+ const SLOT_FORCED_MODES = Object.freeze({
27
28
  Context: 'ListItem'
28
29
  });
29
30
 
@@ -38,7 +39,7 @@ const pressedOverlayStyle = {
38
39
  // ---------------------------------------------------------------------------
39
40
 
40
41
  function resolveListItemTokens(modes) {
41
- // Modes used to cascade into slot children (leading / supportSlot / endSlot).
42
+ // Modes used to cascade into slot children (leading / supportSlot / trailing).
42
43
  // We do NOT inject an `AppearanceBrand` default here: slot content such as
43
44
  // Buttons or Badges carry their own intended appearance, so forcing one onto
44
45
  // them would be surprising.
@@ -137,9 +138,11 @@ const verticalSupportTextOverride = {
137
138
  * - **design-token driven styling** via `getVariableByName` and `modes`
138
139
  *
139
140
  * Wherever the Figma layer name contains "Slot", this component exposes a
140
- * dedicated React "slot" prop:
141
+ * dedicated React "slot" prop. The leading and trailing edges share a
142
+ * symmetric `leading` / `trailing` slot API:
143
+ * - Slot "leading" → `leading`
141
144
  * - Slot "support text" → `supportSlot`
142
- * - Slot "end" → `endSlot`
145
+ * - Slot "trailing" → `trailing`
143
146
  *
144
147
  * @component
145
148
  * @param {Object} props
@@ -147,9 +150,9 @@ const verticalSupportTextOverride = {
147
150
  * @param {string} [props.title='Title'] - Primary title used in the horizontal layout.
148
151
  * @param {string} [props.supportText='Support Text'] - Support text used in both layouts when `supportSlot` is not provided.
149
152
  * @param {boolean} [props.showSupportText=true] - Toggles rendering of the support text in Horizontal layout.
150
- * @param {React.ReactNode} [props.leading] - Optional leading element. Defaults to `IconCapsule`.
153
+ * @param {React.ReactNode} [props.leading] - Optional leading slot. Defaults to `IconCapsule`.
151
154
  * @param {React.ReactNode} [props.supportSlot] - Optional custom slot used instead of the default support text block.
152
- * @param {React.ReactNode} [props.endSlot] - Optional custom trailing slot (Figma Slot "end").
155
+ * @param {React.ReactNode} [props.trailing] - Optional trailing slot (Figma Slot "trailing"). Horizontal layout only.
153
156
  * @param {boolean} [props.navArrow=true] - Whether to show NavArrow on the far right (Horizontal layout only).
154
157
  * @param {Object} [props.modes={}] - Modes object passed to `getVariableByName` for all design tokens.
155
158
  * @param {Function} [props.onPress] - When provided, the entire item becomes pressable (navigation variant).
@@ -178,6 +181,7 @@ function ListItemImpl({
178
181
  showSupportText = true,
179
182
  leading,
180
183
  supportSlot,
184
+ trailing,
181
185
  endSlot,
182
186
  navArrow = true,
183
187
  modes = _reactUtils.EMPTY_MODES,
@@ -215,7 +219,7 @@ function ListItemImpl({
215
219
  // Process leading slot to pass modes to children. Memoized on
216
220
  // (leading, resolvedModes) so a parent re-render doesn't re-walk the tree.
217
221
  const leadingElement = (0, _react.useMemo)(() => {
218
- const processed = leading ? (0, _reactUtils.cloneChildrenWithModes)(_react.default.Children.toArray(leading), tokens.resolvedModes) : [];
222
+ const processed = leading ? (0, _reactUtils.cloneChildrenWithModes)(_react.default.Children.toArray(leading), tokens.resolvedModes, SLOT_FORCED_MODES) : [];
219
223
  if (processed.length === 0) {
220
224
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_IconCapsule.default, {
221
225
  modes: tokens.resolvedModes,
@@ -229,11 +233,14 @@ function ListItemImpl({
229
233
  const processed = (0, _reactUtils.cloneChildrenWithModes)(_react.default.Children.toArray(supportSlot), tokens.resolvedModes);
230
234
  return processed.length === 1 ? processed[0] : processed;
231
235
  }, [supportSlot, tokens.resolvedModes]);
232
- const processedEndSlot = (0, _react.useMemo)(() => {
233
- if (!endSlot) return null;
234
- const processed = (0, _reactUtils.cloneChildrenWithModes)(_react.default.Children.toArray(endSlot), tokens.resolvedModes, END_SLOT_FORCED_MODES);
236
+
237
+ // `trailing` wins; `endSlot` is the deprecated alias kept for back-compat.
238
+ const trailingContent = trailing ?? endSlot;
239
+ const processedTrailing = (0, _react.useMemo)(() => {
240
+ if (!trailingContent) return null;
241
+ const processed = (0, _reactUtils.cloneChildrenWithModes)(_react.default.Children.toArray(trailingContent), tokens.resolvedModes, SLOT_FORCED_MODES);
235
242
  return processed.length === 1 ? processed[0] : processed;
236
- }, [endSlot, tokens.resolvedModes]);
243
+ }, [trailingContent, tokens.resolvedModes]);
237
244
  const renderSupportContent = () => {
238
245
  if (processedSupportSlot) return processedSupportSlot;
239
246
 
@@ -279,9 +286,9 @@ function ListItemImpl({
279
286
  numberOfLines: 1,
280
287
  children: title
281
288
  }), showSupportText && renderSupportContent()]
282
- }), processedEndSlot ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
289
+ }), processedTrailing ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
283
290
  style: tokens.trailingWrapperStyle,
284
- children: processedEndSlot
291
+ children: processedTrailing
285
292
  }) : null, navArrow && /*#__PURE__*/(0, _jsxRuntime.jsx)(_NavArrow.default, {
286
293
  direction: "Forward",
287
294
  modes: tokens.resolvedModes