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 +37 -0
- package/lib/commonjs/components/MediaCard/GlassFill.js +62 -0
- package/lib/commonjs/components/MediaCard/GlassFill.web.js +48 -0
- package/lib/commonjs/components/MediaCard/MediaCard.js +88 -55
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/MediaCard/GlassFill.js +57 -0
- package/lib/module/components/MediaCard/GlassFill.web.js +43 -0
- package/lib/module/components/MediaCard/MediaCard.js +89 -56
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/MediaCard/GlassFill.d.ts +47 -0
- package/lib/typescript/src/components/MediaCard/GlassFill.web.d.ts +20 -0
- package/lib/typescript/src/components/MediaCard/MediaCard.d.ts +40 -10
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +3 -1
- package/src/components/MediaCard/GlassFill.tsx +89 -0
- package/src/components/MediaCard/GlassFill.web.tsx +53 -0
- package/src/components/MediaCard/MediaCard.tsx +99 -49
- package/src/icons/registry.ts +1 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { View, StyleSheet, Platform } from 'react-native';
|
|
5
|
+
import { BlurView } from '@react-native-community/blur';
|
|
6
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
7
|
+
const DEFAULT_FALLBACK_DARK = '#1414174a';
|
|
8
|
+
const DEFAULT_FALLBACK_LIGHT = '#ffffff66';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Glass / frosted surface for native (iOS + Android).
|
|
12
|
+
*
|
|
13
|
+
* Why this lives in its own platform-split file:
|
|
14
|
+
* - `@react-native-community/blur` is a native-only module. Importing it on
|
|
15
|
+
* web throws because the JS shim references native components that aren't
|
|
16
|
+
* registered there. By using Metro's platform-extension resolution
|
|
17
|
+
* (`GlassFill.tsx` for native, `GlassFill.web.tsx` for web), we keep the
|
|
18
|
+
* web bundle free of any native-only imports.
|
|
19
|
+
* - Centralizes the `intensity` (0–100) -> `blurAmount` (0–32) mapping so
|
|
20
|
+
* callers can keep the Figma token semantics they already know.
|
|
21
|
+
*
|
|
22
|
+
* On iOS this is a real `UIVisualEffectView` (true OS-level live blur).
|
|
23
|
+
* On Android this uses the community blur view (RealtimeBlurView). On devices
|
|
24
|
+
* where realtime blur is unavailable, `reducedTransparencyFallbackColor` (and
|
|
25
|
+
* the explicit `overlayColor`) ensure the surface still renders as a
|
|
26
|
+
* translucent tinted scrim instead of disappearing.
|
|
27
|
+
*/
|
|
28
|
+
function GlassFill({
|
|
29
|
+
tint = 'dark',
|
|
30
|
+
intensity = 50,
|
|
31
|
+
overlayColor,
|
|
32
|
+
style
|
|
33
|
+
}) {
|
|
34
|
+
const blurType = tint === 'light' ? 'light' : 'dark';
|
|
35
|
+
const blurAmount = Math.max(0, Math.min(32, Math.round(intensity * 0.32)));
|
|
36
|
+
const fallbackColor = overlayColor ?? (tint === 'light' ? DEFAULT_FALLBACK_LIGHT : DEFAULT_FALLBACK_DARK);
|
|
37
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
38
|
+
style: [StyleSheet.absoluteFill, style],
|
|
39
|
+
pointerEvents: "none",
|
|
40
|
+
children: [/*#__PURE__*/_jsx(BlurView, {
|
|
41
|
+
style: StyleSheet.absoluteFill,
|
|
42
|
+
blurType: blurType,
|
|
43
|
+
blurAmount: blurAmount,
|
|
44
|
+
reducedTransparencyFallbackColor: fallbackColor
|
|
45
|
+
}), overlayColor != null ? /*#__PURE__*/_jsx(View, {
|
|
46
|
+
style: [StyleSheet.absoluteFill, {
|
|
47
|
+
backgroundColor: overlayColor
|
|
48
|
+
}]
|
|
49
|
+
}) : null, Platform.OS === 'android' ? /*#__PURE__*/_jsx(View, {
|
|
50
|
+
style: [StyleSheet.absoluteFill, {
|
|
51
|
+
backgroundColor: 'rgba(255,255,255,0.03)',
|
|
52
|
+
opacity: 0.6
|
|
53
|
+
}]
|
|
54
|
+
}) : null]
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export default GlassFill;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { View, StyleSheet } from 'react-native';
|
|
5
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
6
|
+
const DEFAULT_FALLBACK_DARK = '#1414174a';
|
|
7
|
+
const DEFAULT_FALLBACK_LIGHT = '#ffffff66';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Web counterpart of `GlassFill`.
|
|
11
|
+
*
|
|
12
|
+
* `@react-native-community/blur` does not ship a web implementation, so for
|
|
13
|
+
* the web bundle we render a translucent `View` with `backdrop-filter: blur()`
|
|
14
|
+
* — which is exactly how 0.0.67 and earlier shipped the glass effect on web.
|
|
15
|
+
* Native bundles pick up `GlassFill.tsx` instead via Metro's platform
|
|
16
|
+
* resolver; the web bundle picks up this file.
|
|
17
|
+
*/
|
|
18
|
+
function GlassFill({
|
|
19
|
+
tint = 'dark',
|
|
20
|
+
intensity = 50,
|
|
21
|
+
overlayColor,
|
|
22
|
+
style
|
|
23
|
+
}) {
|
|
24
|
+
// Approximate mapping: intensity 0-100 -> ~0-30px CSS blur. Keeps parity
|
|
25
|
+
// with the native blur strength so the component looks roughly the same
|
|
26
|
+
// across platforms.
|
|
27
|
+
const blurPx = Math.max(0, Math.min(30, Math.round(intensity * 0.3)));
|
|
28
|
+
const tintColor = overlayColor ?? (tint === 'light' ? DEFAULT_FALLBACK_LIGHT : DEFAULT_FALLBACK_DARK);
|
|
29
|
+
return /*#__PURE__*/_jsx(View, {
|
|
30
|
+
style: [StyleSheet.absoluteFill, {
|
|
31
|
+
backgroundColor: tintColor
|
|
32
|
+
},
|
|
33
|
+
// backdrop-filter is a web-only CSS property; ignored by RN
|
|
34
|
+
// on native (we never bundle this file there anyway).
|
|
35
|
+
// @ts-ignore web-only style
|
|
36
|
+
{
|
|
37
|
+
backdropFilter: `blur(${blurPx}px)`,
|
|
38
|
+
WebkitBackdropFilter: `blur(${blurPx}px)`
|
|
39
|
+
}, style],
|
|
40
|
+
pointerEvents: "none"
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export default GlassFill;
|
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import React, { createContext, useContext } from 'react';
|
|
4
|
-
import { View, Text, StyleSheet
|
|
4
|
+
import { View, Text, StyleSheet } from 'react-native';
|
|
5
5
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
6
|
import Image from '../Image/Image';
|
|
7
|
+
import GlassFill from './GlassFill';
|
|
7
8
|
import { EMPTY_MODES } from '../../utils/react-utils';
|
|
8
9
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
9
10
|
const MediaCardContext = /*#__PURE__*/createContext({});
|
|
10
11
|
/**
|
|
11
12
|
* MediaCard component implementation from Figma node 1241:4140.
|
|
12
13
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* the
|
|
17
|
-
*
|
|
14
|
+
* Layout contract (important — read this before editing):
|
|
15
|
+
* - The **background** (image or custom `media`) is the only child in
|
|
16
|
+
* normal flow. It dictates the card's height — typically via
|
|
17
|
+
* `aspectRatio` on the inner `<Image>`. There is no `minHeight`.
|
|
18
|
+
* - `Header` and `Footer` are **absolutely positioned overlays**:
|
|
19
|
+
* - `Header` pinned to top-left/right with safe padding.
|
|
20
|
+
* - `Footer` pinned to bottom-left/right with `zIndex: 2` so it sits
|
|
21
|
+
* on top of the header (and on top of the image). This guarantees
|
|
22
|
+
* the footer never moves no matter how many lines the title wraps
|
|
23
|
+
* to — the title may overflow the header bounds, but the footer's
|
|
24
|
+
* position is a function of the card box, not the title.
|
|
25
|
+
* - `pointerEvents="box-none"` is applied so taps still land on the
|
|
26
|
+
* interactive elements inside the overlays without the wrapper itself
|
|
27
|
+
* capturing them.
|
|
18
28
|
*/
|
|
19
29
|
export function MediaCard({
|
|
20
30
|
imageSource,
|
|
@@ -25,21 +35,11 @@ export function MediaCard({
|
|
|
25
35
|
style
|
|
26
36
|
}) {
|
|
27
37
|
const radius = parseFloat(getVariableByName('cardMedia/radius', modes) || '24');
|
|
28
|
-
|
|
29
|
-
// No magic minHeight, no aspectRatio on the container. The card simply
|
|
30
|
-
// hugs whatever the background renders at: the <Image> sits in normal
|
|
31
|
-
// flow with `aspectRatio: ratio`, so its rendered height becomes the
|
|
32
|
-
// card's height. Header and Footer are absolutely positioned overlays
|
|
33
|
-
// and don't contribute to layout.
|
|
34
38
|
const containerStyle = {
|
|
35
39
|
borderRadius: radius,
|
|
36
40
|
overflow: 'hidden',
|
|
37
41
|
position: 'relative'
|
|
38
42
|
};
|
|
39
|
-
|
|
40
|
-
// `media` wins as an escape hatch (gradient/video/etc.). Otherwise we
|
|
41
|
-
// delegate to the shared <Image> for image-source backgrounds. The
|
|
42
|
-
// background renders in normal flow so its height drives the card.
|
|
43
43
|
const background = media ?? (imageSource != null ? /*#__PURE__*/_jsx(Image, {
|
|
44
44
|
imageSource: imageSource,
|
|
45
45
|
ratio: ratio,
|
|
@@ -67,33 +67,26 @@ export function MediaCard({
|
|
|
67
67
|
// ----------------------------------------------------------------------------
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
* Header
|
|
71
|
-
*
|
|
72
|
-
*
|
|
70
|
+
* Header overlay — pinned to the top of the card. Title content can wrap to
|
|
71
|
+
* any number of lines without affecting the footer's position; if it grows
|
|
72
|
+
* taller than the card, the card's `overflow: 'hidden'` clips it.
|
|
73
|
+
*
|
|
74
|
+
* Default `padding: 16` matches the Figma "title wrap" spec.
|
|
73
75
|
*/
|
|
74
76
|
export function Header({
|
|
75
77
|
children,
|
|
76
78
|
style
|
|
77
79
|
}) {
|
|
78
|
-
// NOTE: the previous `flex: 1` shorthand expanded on Yoga (Android) to
|
|
79
|
-
// `{ flexGrow: 1, flexShrink: 1, flexBasis: 0 }`. With `flexBasis: 0` the
|
|
80
|
-
// Header has *no intrinsic floor*, so when MediaCard is placed inside a
|
|
81
|
-
// height-unbounded parent — e.g. a Carousel slot whose contentContainer
|
|
82
|
-
// is `alignItems: 'flex-start'` — Yoga's first measurement pass sizes
|
|
83
|
-
// the Header at 0 and the card's overall height becomes non-deterministic.
|
|
84
|
-
// On native this manifests as the card "over-stretching" vertically (the
|
|
85
|
-
// same Yoga foot-gun we fixed in `CardCTA` rightWrap). Web hides it
|
|
86
|
-
// because browsers honor `min-height: auto` on flex items. Use explicit
|
|
87
|
-
// `flexGrow / flexShrink: 0 / flexBasis: 'auto'` so the Header is sized
|
|
88
|
-
// to its content as a floor and only grows to consume the extra space
|
|
89
|
-
// contributed by `MediaCard`'s `minHeight: 308`.
|
|
90
80
|
return /*#__PURE__*/_jsx(View, {
|
|
91
81
|
style: [{
|
|
82
|
+
position: 'absolute',
|
|
83
|
+
top: 0,
|
|
84
|
+
left: 0,
|
|
85
|
+
right: 0,
|
|
92
86
|
padding: 16,
|
|
93
|
-
|
|
94
|
-
flexShrink: 0,
|
|
95
|
-
flexBasis: 'auto'
|
|
87
|
+
zIndex: 1
|
|
96
88
|
}, style],
|
|
89
|
+
pointerEvents: "box-none",
|
|
97
90
|
children: children
|
|
98
91
|
});
|
|
99
92
|
}
|
|
@@ -128,8 +121,27 @@ export function Title({
|
|
|
128
121
|
}
|
|
129
122
|
|
|
130
123
|
/**
|
|
131
|
-
* Glass Footer
|
|
132
|
-
*
|
|
124
|
+
* Glass Footer — pinned to the bottom of the card, **always** on top of the
|
|
125
|
+
* Header (`zIndex: 2`).
|
|
126
|
+
*
|
|
127
|
+
* Glass implementation:
|
|
128
|
+
* - **iOS / Android:** `<GlassFill>` (this folder) wraps
|
|
129
|
+
* `@react-native-community/blur`'s `BlurView`. iOS gets a real
|
|
130
|
+
* `UIVisualEffectView` (live OS blur); Android gets the community
|
|
131
|
+
* `RealtimeBlurView` with a token-driven tinted scrim fallback for
|
|
132
|
+
* devices where realtime blur is unavailable.
|
|
133
|
+
* - **Web:** the platform-extension file `GlassFill.web.tsx` renders a
|
|
134
|
+
* translucent View with `backdrop-filter: blur()` — Metro picks the
|
|
135
|
+
* correct file automatically, so the web bundle never imports the
|
|
136
|
+
* native-only blur module.
|
|
137
|
+
*
|
|
138
|
+
* Why we don't use `expo-blur`: it requires Expo Modules autolinking on the
|
|
139
|
+
* consumer side (`use_expo_modules!` / `ExpoModulesPackage`), which silently
|
|
140
|
+
* breaks bare React Native apps that just install this library and run
|
|
141
|
+
* `pod install`. `@react-native-community/blur` is a regular RN native
|
|
142
|
+
* module — autolinking handles it with no additional setup.
|
|
143
|
+
*
|
|
144
|
+
* Tokens still drive the tint color, blur intensity and inner spacing.
|
|
133
145
|
*/
|
|
134
146
|
export function Footer({
|
|
135
147
|
children,
|
|
@@ -142,28 +154,49 @@ export function Footer({
|
|
|
142
154
|
const paddingHorizontal = parseFloat(getVariableByName('cardMedia/footer/padding/horizontal', modes) || '16');
|
|
143
155
|
const paddingVertical = parseFloat(getVariableByName('cardMedia/footer/padding/vertical', modes) || '12');
|
|
144
156
|
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
157
|
+
// Figma tokens:
|
|
158
|
+
// blur/minimal/background -> tint laid over the live blur, also used
|
|
159
|
+
// as the iOS reduced-transparency fallback.
|
|
160
|
+
// blur/minimal -> blur radius (px). The community BlurView
|
|
161
|
+
// uses `blurAmount` (~0-32). `GlassFill`
|
|
162
|
+
// accepts a 0-100 "intensity" (kept compat
|
|
163
|
+
// with the previous expo-blur scale) and
|
|
164
|
+
// maps it internally — here we convert the
|
|
165
|
+
// token's radius to that intensity scale.
|
|
149
166
|
const glassBgColor = getVariableByName('blur/minimal/background', modes) || '#1414174a';
|
|
150
167
|
const blurRadius = parseFloat(getVariableByName('blur/minimal', modes) || '29');
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
168
|
+
const intensity = Math.max(0, Math.min(100, Math.round(blurRadius * 1.7)));
|
|
169
|
+
|
|
170
|
+
// Pick the iOS/Android material tint from "Contrast Context" mode so the
|
|
171
|
+
// glass adapts to dark/light backgrounds the same way the Figma tokens do.
|
|
172
|
+
const contrast = modes['Contrast Context'] || 'on dark';
|
|
173
|
+
const tint = contrast === 'on light' ? 'light' : 'dark';
|
|
174
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
175
|
+
style: [{
|
|
176
|
+
position: 'absolute',
|
|
177
|
+
left: 0,
|
|
178
|
+
right: 0,
|
|
179
|
+
bottom: 0,
|
|
180
|
+
overflow: 'hidden',
|
|
181
|
+
// zIndex 2 ensures Footer always paints above Header,
|
|
182
|
+
// regardless of which is rendered first in the tree.
|
|
183
|
+
zIndex: 2
|
|
184
|
+
}, style],
|
|
185
|
+
pointerEvents: "box-none",
|
|
186
|
+
children: [/*#__PURE__*/_jsx(GlassFill, {
|
|
187
|
+
tint: tint,
|
|
188
|
+
intensity: intensity,
|
|
189
|
+
overlayColor: glassBgColor
|
|
190
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
191
|
+
style: {
|
|
192
|
+
flexDirection: 'row',
|
|
193
|
+
alignItems: 'center',
|
|
194
|
+
gap,
|
|
195
|
+
paddingHorizontal,
|
|
196
|
+
paddingVertical
|
|
197
|
+
},
|
|
198
|
+
children: children
|
|
199
|
+
})]
|
|
167
200
|
});
|
|
168
201
|
}
|
|
169
202
|
|