jfs-components 0.0.68 → 0.0.70

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 CHANGED
@@ -4,6 +4,43 @@ 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.70] - 2026-04-23
8
+
9
+ ### Fixed
10
+
11
+ - **`MediaCard.Footer` blur on bare React Native:** `0.0.69` shipped the glass footer using `expo-blur`, which silently required consumers to integrate Expo Modules autolinking (`use_expo_modules!` in `Podfile`, `ExpoModulesPackage` on Android). Bare React Native apps that just installed the library and ran `pod install` hit two failures: a runtime "`Cannot read property 'BlurView' of undefined`" red box on Android and an iOS Xcode build failure after `pod install`. **Replaced `expo-blur` with [`@react-native-community/blur`](https://github.com/Kureev/react-native-blur)** — a regular React Native native module handled by standard autolinking. No Expo runtime required.
12
+
13
+ ### Changed
14
+
15
+ - **`MediaCard.Footer` glass implementation:** Native blur now lives in a small platform-split helper, `MediaCard/GlassFill.tsx` (iOS + Android via the community blur module) and `MediaCard/GlassFill.web.tsx` (`backdrop-filter` via inline style). Metro picks the correct file per platform, so the web bundle never imports the native-only blur module. The Footer's API and design-token contract (`blur/minimal/background`, `blur/minimal`, `Contrast Context` mode) are unchanged.
16
+
17
+ ### Removed
18
+
19
+ - **`expo-blur`** is no longer a runtime dependency.
20
+
21
+ ### Dependencies
22
+
23
+ - **`@react-native-community/blur`** (`>=4.4.0`) added as a **peer dependency**. Consumers must install it once: `npm install @react-native-community/blur && cd ios && pod install`. On Expo, use `npx expo install @react-native-community/blur` and prebuild.
24
+
25
+ ---
26
+
27
+ ## [0.0.69] - 2026-04-22
28
+
29
+ ### Changed
30
+
31
+ - **`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.
32
+ - **`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.
33
+
34
+ ### Added
35
+
36
+ - **`MediaCard.LongTitleDoesNotPushFooter` story:** Stress test demonstrating that a wrapping multi-line title leaves the footer pinned to the bottom.
37
+
38
+ ### Dependencies
39
+
40
+ - **`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.
41
+
42
+ ---
43
+
7
44
  ## [0.0.68] - 2026-04-22
8
45
 
9
46
  ### Added
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _react = _interopRequireDefault(require("react"));
8
+ var _reactNative = require("react-native");
9
+ var _blur = require("@react-native-community/blur");
10
+ var _jsxRuntime = require("react/jsx-runtime");
11
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
12
+ const DEFAULT_FALLBACK_DARK = '#1414174a';
13
+ const DEFAULT_FALLBACK_LIGHT = '#ffffff66';
14
+
15
+ /**
16
+ * Glass / frosted surface for native (iOS + Android).
17
+ *
18
+ * Why this lives in its own platform-split file:
19
+ * - `@react-native-community/blur` is a native-only module. Importing it on
20
+ * web throws because the JS shim references native components that aren't
21
+ * registered there. By using Metro's platform-extension resolution
22
+ * (`GlassFill.tsx` for native, `GlassFill.web.tsx` for web), we keep the
23
+ * web bundle free of any native-only imports.
24
+ * - Centralizes the `intensity` (0–100) -> `blurAmount` (0–32) mapping so
25
+ * callers can keep the Figma token semantics they already know.
26
+ *
27
+ * On iOS this is a real `UIVisualEffectView` (true OS-level live blur).
28
+ * On Android this uses the community blur view (RealtimeBlurView). On devices
29
+ * where realtime blur is unavailable, `reducedTransparencyFallbackColor` (and
30
+ * the explicit `overlayColor`) ensure the surface still renders as a
31
+ * translucent tinted scrim instead of disappearing.
32
+ */
33
+ function GlassFill({
34
+ tint = 'dark',
35
+ intensity = 50,
36
+ overlayColor,
37
+ style
38
+ }) {
39
+ const blurType = tint === 'light' ? 'light' : 'dark';
40
+ const blurAmount = Math.max(0, Math.min(32, Math.round(intensity * 0.32)));
41
+ const fallbackColor = overlayColor ?? (tint === 'light' ? DEFAULT_FALLBACK_LIGHT : DEFAULT_FALLBACK_DARK);
42
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
43
+ style: [_reactNative.StyleSheet.absoluteFill, style],
44
+ pointerEvents: "none",
45
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_blur.BlurView, {
46
+ style: _reactNative.StyleSheet.absoluteFill,
47
+ blurType: blurType,
48
+ blurAmount: blurAmount,
49
+ reducedTransparencyFallbackColor: fallbackColor
50
+ }), overlayColor != null ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
51
+ style: [_reactNative.StyleSheet.absoluteFill, {
52
+ backgroundColor: overlayColor
53
+ }]
54
+ }) : null, _reactNative.Platform.OS === 'android' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
55
+ style: [_reactNative.StyleSheet.absoluteFill, {
56
+ backgroundColor: 'rgba(255,255,255,0.03)',
57
+ opacity: 0.6
58
+ }]
59
+ }) : null]
60
+ });
61
+ }
62
+ var _default = exports.default = GlassFill;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _react = _interopRequireDefault(require("react"));
8
+ var _reactNative = require("react-native");
9
+ var _jsxRuntime = require("react/jsx-runtime");
10
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
11
+ const DEFAULT_FALLBACK_DARK = '#1414174a';
12
+ const DEFAULT_FALLBACK_LIGHT = '#ffffff66';
13
+
14
+ /**
15
+ * Web counterpart of `GlassFill`.
16
+ *
17
+ * `@react-native-community/blur` does not ship a web implementation, so for
18
+ * the web bundle we render a translucent `View` with `backdrop-filter: blur()`
19
+ * — which is exactly how 0.0.67 and earlier shipped the glass effect on web.
20
+ * Native bundles pick up `GlassFill.tsx` instead via Metro's platform
21
+ * resolver; the web bundle picks up this file.
22
+ */
23
+ function GlassFill({
24
+ tint = 'dark',
25
+ intensity = 50,
26
+ overlayColor,
27
+ style
28
+ }) {
29
+ // Approximate mapping: intensity 0-100 -> ~0-30px CSS blur. Keeps parity
30
+ // with the native blur strength so the component looks roughly the same
31
+ // across platforms.
32
+ const blurPx = Math.max(0, Math.min(30, Math.round(intensity * 0.3)));
33
+ const tintColor = overlayColor ?? (tint === 'light' ? DEFAULT_FALLBACK_LIGHT : DEFAULT_FALLBACK_DARK);
34
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
35
+ style: [_reactNative.StyleSheet.absoluteFill, {
36
+ backgroundColor: tintColor
37
+ },
38
+ // backdrop-filter is a web-only CSS property; ignored by RN
39
+ // on native (we never bundle this file there anyway).
40
+ // @ts-ignore web-only style
41
+ {
42
+ backdropFilter: `blur(${blurPx}px)`,
43
+ WebkitBackdropFilter: `blur(${blurPx}px)`
44
+ }, style],
45
+ pointerEvents: "none"
46
+ });
47
+ }
48
+ var _default = exports.default = GlassFill;
@@ -14,6 +14,7 @@ var _react = _interopRequireWildcard(require("react"));
14
14
  var _reactNative = require("react-native");
15
15
  var _figmaVariablesResolver = require("../../design-tokens/figma-variables-resolver");
16
16
  var _Image = _interopRequireDefault(require("../Image/Image"));
17
+ var _GlassFill = _interopRequireDefault(require("./GlassFill"));
17
18
  var _reactUtils = require("../../utils/react-utils");
18
19
  var _jsxRuntime = require("react/jsx-runtime");
19
20
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
@@ -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
- * Features a background media slot, a large title, and a glass-morphism footer.
26
- *
27
- * The background can be supplied either as `imageSource` (preferred — uses
28
- * the shared `<Image>` primitive under the hood) or as a custom `media` node
29
- * for non-image backgrounds.
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/Title Wrapper
83
- * It seems the title is just floating at the top with padding.
84
- * Figma: "title wrap" p-[16px]
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
- flexGrow: 1,
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,27 @@ function Title({
140
133
  }
141
134
 
142
135
  /**
143
- * Glass Footer Component
144
- * Tokens: cardMedia/footer/*, glass/minimal, blur/minimal
136
+ * Glass Footer — pinned to the bottom of the card, **always** on top of the
137
+ * Header (`zIndex: 2`).
138
+ *
139
+ * Glass implementation:
140
+ * - **iOS / Android:** `<GlassFill>` (this folder) wraps
141
+ * `@react-native-community/blur`'s `BlurView`. iOS gets a real
142
+ * `UIVisualEffectView` (live OS blur); Android gets the community
143
+ * `RealtimeBlurView` with a token-driven tinted scrim fallback for
144
+ * devices where realtime blur is unavailable.
145
+ * - **Web:** the platform-extension file `GlassFill.web.tsx` renders a
146
+ * translucent View with `backdrop-filter: blur()` — Metro picks the
147
+ * correct file automatically, so the web bundle never imports the
148
+ * native-only blur module.
149
+ *
150
+ * Why we don't use `expo-blur`: it requires Expo Modules autolinking on the
151
+ * consumer side (`use_expo_modules!` / `ExpoModulesPackage`), which silently
152
+ * breaks bare React Native apps that just install this library and run
153
+ * `pod install`. `@react-native-community/blur` is a regular RN native
154
+ * module — autolinking handles it with no additional setup.
155
+ *
156
+ * Tokens still drive the tint color, blur intensity and inner spacing.
145
157
  */
146
158
  function Footer({
147
159
  children,
@@ -154,28 +166,49 @@ function Footer({
154
166
  const paddingHorizontal = parseFloat((0, _figmaVariablesResolver.getVariableByName)('cardMedia/footer/padding/horizontal', modes) || '16');
155
167
  const paddingVertical = parseFloat((0, _figmaVariablesResolver.getVariableByName)('cardMedia/footer/padding/vertical', modes) || '12');
156
168
 
157
- // Glass Effect
158
- // Figma:
159
- // blur/minimal/background: "#1414174a"
160
- // blur/minimal: 29
169
+ // Figma tokens:
170
+ // blur/minimal/background -> tint laid over the live blur, also used
171
+ // as the iOS reduced-transparency fallback.
172
+ // blur/minimal -> blur radius (px). The community BlurView
173
+ // uses `blurAmount` (~0-32). `GlassFill`
174
+ // accepts a 0-100 "intensity" (kept compat
175
+ // with the previous expo-blur scale) and
176
+ // maps it internally — here we convert the
177
+ // token's radius to that intensity scale.
161
178
  const glassBgColor = (0, _figmaVariablesResolver.getVariableByName)('blur/minimal/background', modes) || '#1414174a';
162
179
  const blurRadius = parseFloat((0, _figmaVariablesResolver.getVariableByName)('blur/minimal', modes) || '29');
163
- const containerStyle = {
164
- flexDirection: 'row',
165
- alignItems: 'center',
166
- gap,
167
- paddingHorizontal,
168
- paddingVertical,
169
- backgroundColor: glassBgColor,
170
- // Web-specific backdrop filter for glass effect
171
- // @ts-ignore
172
- ...(_reactNative.Platform.OS === 'web' ? {
173
- backdropFilter: `blur(${blurRadius}px)`
174
- } : {})
175
- };
176
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
177
- style: [containerStyle, style],
178
- children: children
180
+ const intensity = Math.max(0, Math.min(100, Math.round(blurRadius * 1.7)));
181
+
182
+ // Pick the iOS/Android material tint from "Contrast Context" mode so the
183
+ // glass adapts to dark/light backgrounds the same way the Figma tokens do.
184
+ const contrast = modes['Contrast Context'] || 'on dark';
185
+ const tint = contrast === 'on light' ? 'light' : 'dark';
186
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
187
+ style: [{
188
+ position: 'absolute',
189
+ left: 0,
190
+ right: 0,
191
+ bottom: 0,
192
+ overflow: 'hidden',
193
+ // zIndex 2 ensures Footer always paints above Header,
194
+ // regardless of which is rendered first in the tree.
195
+ zIndex: 2
196
+ }, style],
197
+ pointerEvents: "box-none",
198
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_GlassFill.default, {
199
+ tint: tint,
200
+ intensity: intensity,
201
+ overlayColor: glassBgColor
202
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
203
+ style: {
204
+ flexDirection: 'row',
205
+ alignItems: 'center',
206
+ gap,
207
+ paddingHorizontal,
208
+ paddingVertical
209
+ },
210
+ children: children
211
+ })]
179
212
  });
180
213
  }
181
214