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.
@@ -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, Platform } from 'react-native';
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
- * Features a background media slot, a large title, and a glass-morphism footer.
14
- *
15
- * The background can be supplied either as `imageSource` (preferred — uses
16
- * the shared `<Image>` primitive under the hood) or as a custom `media` node
17
- * for non-image backgrounds.
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/Title Wrapper
71
- * It seems the title is just floating at the top with padding.
72
- * Figma: "title wrap" p-[16px]
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
- flexGrow: 1,
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 Component
132
- * Tokens: cardMedia/footer/*, glass/minimal, blur/minimal
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
- // Glass Effect
146
- // Figma:
147
- // blur/minimal/background: "#1414174a"
148
- // blur/minimal: 29
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 containerStyle = {
152
- flexDirection: 'row',
153
- alignItems: 'center',
154
- gap,
155
- paddingHorizontal,
156
- paddingVertical,
157
- backgroundColor: glassBgColor,
158
- // Web-specific backdrop filter for glass effect
159
- // @ts-ignore
160
- ...(Platform.OS === 'web' ? {
161
- backdropFilter: `blur(${blurRadius}px)`
162
- } : {})
163
- };
164
- return /*#__PURE__*/_jsx(View, {
165
- style: [containerStyle, style],
166
- children: children
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