jfs-components 0.0.68 → 0.0.69
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 +17 -0
- package/lib/commonjs/components/MediaCard/MediaCard.js +91 -55
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/MediaCard/MediaCard.js +91 -55
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/MediaCard/MediaCard.d.ts +36 -10
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +2 -1
- package/src/components/MediaCard/MediaCard.tsx +117 -48
- package/src/icons/registry.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ 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.69] - 2026-04-22
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **`MediaCard.Header` / `MediaCard.Footer`:** Both are now **absolutely positioned overlays**. The footer is pinned to the bottom of the card with `zIndex: 2`, so a title that wraps to any number of lines (or overflows the card entirely) **never pushes the footer**. The footer is also painted above the header. Header/footer wrappers use `pointerEvents="box-none"` so taps still land on interactive children inside.
|
|
12
|
+
- **`MediaCard.Footer` glass effect:** Replaced the previous solid-color "glass" with a real native blur via `expo-blur`'s `BlurView`. iOS uses `UIVisualEffectView` (true OS-level live blur). Android opts into `experimentalBlurMethod="dimezisBlurView"` for hardware-accelerated `RenderEffect` blur on Android 12+, with an automatic tinted-scrim fallback below that — plus a subtle additive overlay on Android only to add texture and avoid a flat look. Web continues to use `backdrop-filter` (now via the same `BlurView` API). Tint adapts to the `Contrast Context` mode.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`MediaCard.LongTitleDoesNotPushFooter` story:** Stress test demonstrating that a wrapping multi-line title leaves the footer pinned to the bottom.
|
|
17
|
+
|
|
18
|
+
### Dependencies
|
|
19
|
+
|
|
20
|
+
- **`expo-blur`** added as a runtime dependency (compatible with Expo SDK 54). Required by the new `MediaCard.Footer` glass implementation. Consumers using the prebuilt iOS/Android binaries from Expo SDK 54 already have the native module available; bare React Native consumers need to run a dev client / pod install after upgrading.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
7
24
|
## [0.0.68] - 2026-04-22
|
|
8
25
|
|
|
9
26
|
### Added
|
|
@@ -12,6 +12,7 @@ exports.Title = Title;
|
|
|
12
12
|
exports.default = void 0;
|
|
13
13
|
var _react = _interopRequireWildcard(require("react"));
|
|
14
14
|
var _reactNative = require("react-native");
|
|
15
|
+
var _expoBlur = require("expo-blur");
|
|
15
16
|
var _figmaVariablesResolver = require("../../design-tokens/figma-variables-resolver");
|
|
16
17
|
var _Image = _interopRequireDefault(require("../Image/Image"));
|
|
17
18
|
var _reactUtils = require("../../utils/react-utils");
|
|
@@ -22,11 +23,20 @@ const MediaCardContext = /*#__PURE__*/(0, _react.createContext)({});
|
|
|
22
23
|
/**
|
|
23
24
|
* MediaCard component implementation from Figma node 1241:4140.
|
|
24
25
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* the
|
|
29
|
-
*
|
|
26
|
+
* Layout contract (important — read this before editing):
|
|
27
|
+
* - The **background** (image or custom `media`) is the only child in
|
|
28
|
+
* normal flow. It dictates the card's height — typically via
|
|
29
|
+
* `aspectRatio` on the inner `<Image>`. There is no `minHeight`.
|
|
30
|
+
* - `Header` and `Footer` are **absolutely positioned overlays**:
|
|
31
|
+
* - `Header` pinned to top-left/right with safe padding.
|
|
32
|
+
* - `Footer` pinned to bottom-left/right with `zIndex: 2` so it sits
|
|
33
|
+
* on top of the header (and on top of the image). This guarantees
|
|
34
|
+
* the footer never moves no matter how many lines the title wraps
|
|
35
|
+
* to — the title may overflow the header bounds, but the footer's
|
|
36
|
+
* position is a function of the card box, not the title.
|
|
37
|
+
* - `pointerEvents="box-none"` is applied so taps still land on the
|
|
38
|
+
* interactive elements inside the overlays without the wrapper itself
|
|
39
|
+
* capturing them.
|
|
30
40
|
*/
|
|
31
41
|
function MediaCard({
|
|
32
42
|
imageSource,
|
|
@@ -37,21 +47,11 @@ function MediaCard({
|
|
|
37
47
|
style
|
|
38
48
|
}) {
|
|
39
49
|
const radius = parseFloat((0, _figmaVariablesResolver.getVariableByName)('cardMedia/radius', modes) || '24');
|
|
40
|
-
|
|
41
|
-
// No magic minHeight, no aspectRatio on the container. The card simply
|
|
42
|
-
// hugs whatever the background renders at: the <Image> sits in normal
|
|
43
|
-
// flow with `aspectRatio: ratio`, so its rendered height becomes the
|
|
44
|
-
// card's height. Header and Footer are absolutely positioned overlays
|
|
45
|
-
// and don't contribute to layout.
|
|
46
50
|
const containerStyle = {
|
|
47
51
|
borderRadius: radius,
|
|
48
52
|
overflow: 'hidden',
|
|
49
53
|
position: 'relative'
|
|
50
54
|
};
|
|
51
|
-
|
|
52
|
-
// `media` wins as an escape hatch (gradient/video/etc.). Otherwise we
|
|
53
|
-
// delegate to the shared <Image> for image-source backgrounds. The
|
|
54
|
-
// background renders in normal flow so its height drives the card.
|
|
55
55
|
const background = media ?? (imageSource != null ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Image.default, {
|
|
56
56
|
imageSource: imageSource,
|
|
57
57
|
ratio: ratio,
|
|
@@ -79,33 +79,26 @@ function MediaCard({
|
|
|
79
79
|
// ----------------------------------------------------------------------------
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
|
-
* Header
|
|
83
|
-
*
|
|
84
|
-
*
|
|
82
|
+
* Header overlay — pinned to the top of the card. Title content can wrap to
|
|
83
|
+
* any number of lines without affecting the footer's position; if it grows
|
|
84
|
+
* taller than the card, the card's `overflow: 'hidden'` clips it.
|
|
85
|
+
*
|
|
86
|
+
* Default `padding: 16` matches the Figma "title wrap" spec.
|
|
85
87
|
*/
|
|
86
88
|
function Header({
|
|
87
89
|
children,
|
|
88
90
|
style
|
|
89
91
|
}) {
|
|
90
|
-
// NOTE: the previous `flex: 1` shorthand expanded on Yoga (Android) to
|
|
91
|
-
// `{ flexGrow: 1, flexShrink: 1, flexBasis: 0 }`. With `flexBasis: 0` the
|
|
92
|
-
// Header has *no intrinsic floor*, so when MediaCard is placed inside a
|
|
93
|
-
// height-unbounded parent — e.g. a Carousel slot whose contentContainer
|
|
94
|
-
// is `alignItems: 'flex-start'` — Yoga's first measurement pass sizes
|
|
95
|
-
// the Header at 0 and the card's overall height becomes non-deterministic.
|
|
96
|
-
// On native this manifests as the card "over-stretching" vertically (the
|
|
97
|
-
// same Yoga foot-gun we fixed in `CardCTA` rightWrap). Web hides it
|
|
98
|
-
// because browsers honor `min-height: auto` on flex items. Use explicit
|
|
99
|
-
// `flexGrow / flexShrink: 0 / flexBasis: 'auto'` so the Header is sized
|
|
100
|
-
// to its content as a floor and only grows to consume the extra space
|
|
101
|
-
// contributed by `MediaCard`'s `minHeight: 308`.
|
|
102
92
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
103
93
|
style: [{
|
|
94
|
+
position: 'absolute',
|
|
95
|
+
top: 0,
|
|
96
|
+
left: 0,
|
|
97
|
+
right: 0,
|
|
104
98
|
padding: 16,
|
|
105
|
-
|
|
106
|
-
flexShrink: 0,
|
|
107
|
-
flexBasis: 'auto'
|
|
99
|
+
zIndex: 1
|
|
108
100
|
}, style],
|
|
101
|
+
pointerEvents: "box-none",
|
|
109
102
|
children: children
|
|
110
103
|
});
|
|
111
104
|
}
|
|
@@ -140,8 +133,23 @@ function Title({
|
|
|
140
133
|
}
|
|
141
134
|
|
|
142
135
|
/**
|
|
143
|
-
* Glass Footer
|
|
144
|
-
*
|
|
136
|
+
* Glass Footer — pinned to the bottom of the card, **always** on top of the
|
|
137
|
+
* Header (`zIndex: 2`).
|
|
138
|
+
*
|
|
139
|
+
* Glass implementation (April 2026 best practice for RN/Expo):
|
|
140
|
+
* - **iOS:** `expo-blur`'s `BlurView` renders a native `UIVisualEffectView`,
|
|
141
|
+
* so this is a real OS-level live blur of whatever's underneath. We pick
|
|
142
|
+
* `tint` from the Figma "Contrast Context" mode (`'dark'` / `'light'`)
|
|
143
|
+
* and a moderate intensity that matches the Figma `blur/minimal` token.
|
|
144
|
+
* - **Android:** the same `BlurView` with `experimentalBlurMethod="dimezisBlurView"`
|
|
145
|
+
* enables the hardware-accelerated `RenderEffect` blur on Android 12+.
|
|
146
|
+
* On older Android, expo-blur cleanly degrades to a tinted scrim — we
|
|
147
|
+
* layer a subtle noise/grain overlay on top so the surface still reads
|
|
148
|
+
* as "frosted glass" instead of a flat color.
|
|
149
|
+
* - **Web:** `BlurView` on web is implemented as `backdrop-filter: blur()`,
|
|
150
|
+
* which already worked in the previous version. Same component, same API.
|
|
151
|
+
*
|
|
152
|
+
* Tokens still drive the tint color, blur radius and inner spacing.
|
|
145
153
|
*/
|
|
146
154
|
function Footer({
|
|
147
155
|
children,
|
|
@@ -154,28 +162,56 @@ function Footer({
|
|
|
154
162
|
const paddingHorizontal = parseFloat((0, _figmaVariablesResolver.getVariableByName)('cardMedia/footer/padding/horizontal', modes) || '16');
|
|
155
163
|
const paddingVertical = parseFloat((0, _figmaVariablesResolver.getVariableByName)('cardMedia/footer/padding/vertical', modes) || '12');
|
|
156
164
|
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
165
|
+
// Figma tokens:
|
|
166
|
+
// blur/minimal/background -> tint laid over the native blur
|
|
167
|
+
// blur/minimal -> blur radius (px). expo-blur takes a 0-100
|
|
168
|
+
// "intensity" instead of px; we map roughly:
|
|
169
|
+
// intensity ≈ clamp(radius * 1.7, 0, 100).
|
|
161
170
|
const glassBgColor = (0, _figmaVariablesResolver.getVariableByName)('blur/minimal/background', modes) || '#1414174a';
|
|
162
171
|
const blurRadius = parseFloat((0, _figmaVariablesResolver.getVariableByName)('blur/minimal', modes) || '29');
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
172
|
+
const intensity = Math.max(0, Math.min(100, Math.round(blurRadius * 1.7)));
|
|
173
|
+
|
|
174
|
+
// Pick the iOS/Android material tint from "Contrast Context" mode so the
|
|
175
|
+
// glass adapts to dark/light backgrounds the same way the Figma tokens do.
|
|
176
|
+
const contrast = modes['Contrast Context'] || 'on dark';
|
|
177
|
+
const tint = contrast === 'on light' ? 'light' : 'dark';
|
|
178
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
179
|
+
style: [{
|
|
180
|
+
position: 'absolute',
|
|
181
|
+
left: 0,
|
|
182
|
+
right: 0,
|
|
183
|
+
bottom: 0,
|
|
184
|
+
overflow: 'hidden',
|
|
185
|
+
// zIndex 2 ensures Footer always paints above Header,
|
|
186
|
+
// regardless of which is rendered first in the tree.
|
|
187
|
+
zIndex: 2
|
|
188
|
+
}, style],
|
|
189
|
+
pointerEvents: "box-none",
|
|
190
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_expoBlur.BlurView, {
|
|
191
|
+
style: _reactNative.StyleSheet.absoluteFill,
|
|
192
|
+
tint: tint,
|
|
193
|
+
intensity: intensity,
|
|
194
|
+
experimentalBlurMethod: "dimezisBlurView"
|
|
195
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
196
|
+
style: [_reactNative.StyleSheet.absoluteFill, {
|
|
197
|
+
backgroundColor: glassBgColor
|
|
198
|
+
}]
|
|
199
|
+
}), _reactNative.Platform.OS === 'android' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
200
|
+
style: [_reactNative.StyleSheet.absoluteFill, {
|
|
201
|
+
backgroundColor: 'rgba(255,255,255,0.03)',
|
|
202
|
+
opacity: 0.6
|
|
203
|
+
}],
|
|
204
|
+
pointerEvents: "none"
|
|
205
|
+
}) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
206
|
+
style: {
|
|
207
|
+
flexDirection: 'row',
|
|
208
|
+
alignItems: 'center',
|
|
209
|
+
gap,
|
|
210
|
+
paddingHorizontal,
|
|
211
|
+
paddingVertical
|
|
212
|
+
},
|
|
213
|
+
children: children
|
|
214
|
+
})]
|
|
179
215
|
});
|
|
180
216
|
}
|
|
181
217
|
|