@oxyhq/bloom 0.87.0 → 0.88.0
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/NOTICE +1 -1
- package/README.md +1 -1
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js +7 -12
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/commonjs/button/Button.web.js +2 -6
- package/lib/commonjs/button/Button.web.js.map +1 -1
- package/lib/commonjs/connection-dots/ConnectionDots.web.js +2 -6
- package/lib/commonjs/connection-dots/ConnectionDots.web.js.map +1 -1
- package/lib/commonjs/design-tokens/index.js +19 -0
- package/lib/commonjs/design-tokens/index.js.map +1 -1
- package/lib/commonjs/design-tokens/tokens-json.js +149 -0
- package/lib/commonjs/design-tokens/tokens-json.js.map +1 -0
- package/lib/commonjs/design-tokens/tokens.json +6681 -0
- package/lib/commonjs/dialog/Dialog.web.js +2 -6
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/fab/Fab.web.js +2 -6
- package/lib/commonjs/fab/Fab.web.js.map +1 -1
- package/lib/commonjs/fonts/apply-font-faces.web.js +8 -10
- package/lib/commonjs/fonts/apply-font-faces.web.js.map +1 -1
- package/lib/commonjs/frosted-icon-button/FrostedIconButton.web.js +2 -6
- package/lib/commonjs/frosted-icon-button/FrostedIconButton.web.js.map +1 -1
- package/lib/commonjs/loading/SpinnerIcon.web.js +2 -6
- package/lib/commonjs/loading/SpinnerIcon.web.js.map +1 -1
- package/lib/commonjs/overlay/index.js +102 -24
- package/lib/commonjs/overlay/index.js.map +1 -1
- package/lib/commonjs/styles/adopt-style-sheet.js +126 -0
- package/lib/commonjs/styles/adopt-style-sheet.js.map +1 -0
- package/lib/module/bottom-sheet/BottomSheetBase.js +7 -12
- package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/module/button/Button.web.js +2 -6
- package/lib/module/button/Button.web.js.map +1 -1
- package/lib/module/connection-dots/ConnectionDots.web.js +2 -6
- package/lib/module/connection-dots/ConnectionDots.web.js.map +1 -1
- package/lib/module/design-tokens/index.js +5 -0
- package/lib/module/design-tokens/index.js.map +1 -1
- package/lib/module/design-tokens/tokens-json.js +144 -0
- package/lib/module/design-tokens/tokens-json.js.map +1 -0
- package/lib/module/design-tokens/tokens.json +6681 -0
- package/lib/module/dialog/Dialog.web.js +2 -6
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/fab/Fab.web.js +2 -6
- package/lib/module/fab/Fab.web.js.map +1 -1
- package/lib/module/fonts/apply-font-faces.web.js +8 -10
- package/lib/module/fonts/apply-font-faces.web.js.map +1 -1
- package/lib/module/frosted-icon-button/FrostedIconButton.web.js +2 -6
- package/lib/module/frosted-icon-button/FrostedIconButton.web.js.map +1 -1
- package/lib/module/loading/SpinnerIcon.web.js +2 -6
- package/lib/module/loading/SpinnerIcon.web.js.map +1 -1
- package/lib/module/overlay/index.js +103 -25
- package/lib/module/overlay/index.js.map +1 -1
- package/lib/module/styles/adopt-style-sheet.js +121 -0
- package/lib/module/styles/adopt-style-sheet.js.map +1 -0
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/button/Button.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/connection-dots/ConnectionDots.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/design-tokens/index.d.ts +2 -0
- package/lib/typescript/commonjs/design-tokens/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/design-tokens/tokens-json.d.ts +85 -0
- package/lib/typescript/commonjs/design-tokens/tokens-json.d.ts.map +1 -0
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/fab/Fab.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/fonts/apply-font-faces.web.d.ts +5 -3
- package/lib/typescript/commonjs/fonts/apply-font-faces.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/frosted-icon-button/FrostedIconButton.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/loading/SpinnerIcon.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/overlay/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/styles/adopt-style-sheet.d.ts +22 -0
- package/lib/typescript/commonjs/styles/adopt-style-sheet.d.ts.map +1 -0
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/module/button/Button.web.d.ts.map +1 -1
- package/lib/typescript/module/connection-dots/ConnectionDots.web.d.ts.map +1 -1
- package/lib/typescript/module/design-tokens/index.d.ts +2 -0
- package/lib/typescript/module/design-tokens/index.d.ts.map +1 -1
- package/lib/typescript/module/design-tokens/tokens-json.d.ts +85 -0
- package/lib/typescript/module/design-tokens/tokens-json.d.ts.map +1 -0
- package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/module/fab/Fab.web.d.ts.map +1 -1
- package/lib/typescript/module/fonts/apply-font-faces.web.d.ts +5 -3
- package/lib/typescript/module/fonts/apply-font-faces.web.d.ts.map +1 -1
- package/lib/typescript/module/frosted-icon-button/FrostedIconButton.web.d.ts.map +1 -1
- package/lib/typescript/module/loading/SpinnerIcon.web.d.ts.map +1 -1
- package/lib/typescript/module/overlay/index.d.ts.map +1 -1
- package/lib/typescript/module/styles/adopt-style-sheet.d.ts +22 -0
- package/lib/typescript/module/styles/adopt-style-sheet.d.ts.map +1 -0
- package/package.json +4 -2
- package/src/__tests__/adopt-style-sheet.node.test.ts +21 -0
- package/src/__tests__/adopt-style-sheet.test.ts +165 -0
- package/src/__tests__/apply-font-faces.test.ts +32 -0
- package/src/__tests__/design-tokens-json.test.ts +112 -0
- package/src/__tests__/exports-map-contract.test.ts +5 -1
- package/src/__tests__/support/constructed-style-sheets.ts +68 -0
- package/src/bottom-sheet/BottomSheetBase.tsx +10 -14
- package/src/button/Button.web.tsx +2 -6
- package/src/connection-dots/ConnectionDots.web.tsx +2 -6
- package/src/design-tokens/index.ts +16 -0
- package/src/design-tokens/tokens-json.ts +214 -0
- package/src/design-tokens/tokens.json +6681 -0
- package/src/dialog/Dialog.stories.tsx +37 -0
- package/src/dialog/Dialog.web.tsx +2 -6
- package/src/fab/Fab.web.tsx +2 -6
- package/src/fonts/apply-font-faces.web.ts +11 -11
- package/src/frosted-icon-button/FrostedIconButton.web.tsx +2 -6
- package/src/loading/SpinnerIcon.web.tsx +2 -6
- package/src/overlay/index.tsx +123 -24
- package/src/styles/adopt-style-sheet.ts +131 -0
|
@@ -85,10 +85,47 @@ function ThreeActionDemo() {
|
|
|
85
85
|
);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* The side-sheet placement, which is a DIFFERENT web surface from the centered
|
|
90
|
+
* card: it resolves `SheetSurface` in `Dialog.web.tsx`, whose entry animation is
|
|
91
|
+
* a CSS transition handed to the backdrop layers rather than a shared value.
|
|
92
|
+
*
|
|
93
|
+
* It needs its own story because that difference is exactly what broke. The
|
|
94
|
+
* layers used to be reanimated components, and on web reanimated routes a style
|
|
95
|
+
* carrying `transitionProperty` into its CSS transitions manager, which writes
|
|
96
|
+
* to `ref.style` — undefined on expo-blur's web `BlurView`, whose ref is a
|
|
97
|
+
* `setNativeProps`-only handle. Opening the sheet threw before it painted. Only
|
|
98
|
+
* a real browser sees it: jest mocks both packages, so the whole suite passed.
|
|
99
|
+
*/
|
|
100
|
+
function SideSheetDemo({ placement }: { placement: 'left' | 'right' }) {
|
|
101
|
+
const control = useDialogControl();
|
|
102
|
+
return (
|
|
103
|
+
<>
|
|
104
|
+
<Button onPress={() => control.open()}>Open {placement} sheet</Button>
|
|
105
|
+
<Dialog control={control} placement={placement} title="Store menu">
|
|
106
|
+
<View style={{ gap: 12 }}>
|
|
107
|
+
<Text>An anchored drawer, blurred and dimmed behind.</Text>
|
|
108
|
+
<Button variant="secondary" onPress={() => control.close()}>
|
|
109
|
+
Done
|
|
110
|
+
</Button>
|
|
111
|
+
</View>
|
|
112
|
+
</Dialog>
|
|
113
|
+
</>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
88
117
|
export const Basic: Story = {
|
|
89
118
|
render: () => <DeclarativeDemo />,
|
|
90
119
|
};
|
|
91
120
|
|
|
121
|
+
export const SideSheetLeft: Story = {
|
|
122
|
+
render: () => <SideSheetDemo placement="left" />,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const SideSheetRight: Story = {
|
|
126
|
+
render: () => <SideSheetDemo placement="right" />,
|
|
127
|
+
};
|
|
128
|
+
|
|
92
129
|
export const CustomChildren: Story = {
|
|
93
130
|
render: () => <CustomChildrenDemo />,
|
|
94
131
|
};
|
|
@@ -25,6 +25,7 @@ import { RemoveScrollBar } from 'react-remove-scroll-bar';
|
|
|
25
25
|
|
|
26
26
|
import { Backdrop, OverlayRoot } from '../overlay';
|
|
27
27
|
import { Portal } from '../portal/index.web';
|
|
28
|
+
import { adoptStyleSheet } from '../styles/adopt-style-sheet';
|
|
28
29
|
import { Z_INDEX } from '../styles/z-index';
|
|
29
30
|
import { WEB_POSITION_FIXED, type WebCssStyle } from '../styles/web-view-style';
|
|
30
31
|
import { bloomShadowStyle } from '../design-tokens/shadows';
|
|
@@ -106,12 +107,7 @@ const DIALOG_STYLE_ID = 'bloom-dialog-web-css';
|
|
|
106
107
|
|
|
107
108
|
function useDialogCss(): void {
|
|
108
109
|
useEffect(() => {
|
|
109
|
-
|
|
110
|
-
if (document.getElementById(DIALOG_STYLE_ID)) return;
|
|
111
|
-
const style = document.createElement('style');
|
|
112
|
-
style.id = DIALOG_STYLE_ID;
|
|
113
|
-
style.textContent = BLOOM_DIALOG_CSS;
|
|
114
|
-
document.head.appendChild(style);
|
|
110
|
+
adoptStyleSheet(DIALOG_STYLE_ID, BLOOM_DIALOG_CSS);
|
|
115
111
|
}, []);
|
|
116
112
|
}
|
|
117
113
|
|
package/src/fab/Fab.web.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import React, {
|
|
|
10
10
|
|
|
11
11
|
import { useTheme } from '../theme/use-theme';
|
|
12
12
|
import type { Theme } from '../theme/types';
|
|
13
|
+
import { adoptStyleSheet } from '../styles/adopt-style-sheet';
|
|
13
14
|
import { flattenWebStyle } from '../styles/flatten-web-style';
|
|
14
15
|
import type { FabPlacement, FabProps, FabSize, FabVariant } from './types';
|
|
15
16
|
|
|
@@ -103,12 +104,7 @@ const BLOOM_FAB_CSS = `
|
|
|
103
104
|
|
|
104
105
|
function useFabCss(): void {
|
|
105
106
|
useEffect(() => {
|
|
106
|
-
|
|
107
|
-
if (document.getElementById(STYLE_ID)) return;
|
|
108
|
-
const style = document.createElement('style');
|
|
109
|
-
style.id = STYLE_ID;
|
|
110
|
-
style.textContent = BLOOM_FAB_CSS;
|
|
111
|
-
document.head.appendChild(style);
|
|
107
|
+
adoptStyleSheet(STYLE_ID, BLOOM_FAB_CSS);
|
|
112
108
|
}, []);
|
|
113
109
|
}
|
|
114
110
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { adoptStyleSheet } from '../styles/adopt-style-sheet';
|
|
1
2
|
import {
|
|
2
3
|
blomusModernusRegular,
|
|
3
4
|
blomusModernusBold,
|
|
@@ -14,9 +15,11 @@ const STYLE_ID = 'bloom-fonts';
|
|
|
14
15
|
* Web-only. The native counterpart in `apply-font-faces.ts` is a no-op
|
|
15
16
|
* stub — native loads its fonts through `useFonts(FONT_ASSETS)`, so the file
|
|
16
17
|
* split keeps the web font payload out of the native bundle entirely.
|
|
17
|
-
* Idempotent: safe to call multiple times;
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* Idempotent: safe to call multiple times; `adoptStyleSheet` keys the sheet by
|
|
19
|
+
* id, so repeat calls re-parse nothing. SSR-safe via its `typeof document`
|
|
20
|
+
* guard. `BloomThemeProvider` calls this on every mount by default, which is
|
|
21
|
+
* why it also has to survive a page whose CSP forbids inline styles — see
|
|
22
|
+
* `styles/adopt-style-sheet.ts`.
|
|
20
23
|
*
|
|
21
24
|
* The URLs come from `./font-urls.web`, which imports the `.woff2` files so
|
|
22
25
|
* the consuming bundler emits them as separate, cacheable assets. See that
|
|
@@ -24,12 +27,9 @@ const STYLE_ID = 'bloom-fonts';
|
|
|
24
27
|
* provide (Metro needs `woff2` in `assetExts`).
|
|
25
28
|
*/
|
|
26
29
|
export function applyFontFaces(): void {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const style = document.createElement('style');
|
|
31
|
-
style.id = STYLE_ID;
|
|
32
|
-
style.textContent = `
|
|
30
|
+
adoptStyleSheet(
|
|
31
|
+
STYLE_ID,
|
|
32
|
+
`
|
|
33
33
|
@font-face { font-family: 'BlomusModernus'; src: url("${blomusModernusRegular}") format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
|
|
34
34
|
@font-face { font-family: 'BlomusModernus'; src: url("${blomusModernusBold}") format('woff2'); font-weight: 700; font-style: normal; font-display: swap; }
|
|
35
35
|
@font-face { font-family: 'Inter'; src: url("${interVariable}") format('woff2-variations'); font-weight: 100 900; font-style: normal; font-display: swap; }
|
|
@@ -39,6 +39,6 @@ export function applyFontFaces(): void {
|
|
|
39
39
|
--bloom-font-sans: ${fontFamilies.sans};
|
|
40
40
|
--bloom-font-mono: ${fontFamilies.mono};
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
`,
|
|
43
|
+
);
|
|
44
44
|
}
|
|
@@ -9,6 +9,7 @@ import React, {
|
|
|
9
9
|
} from 'react';
|
|
10
10
|
|
|
11
11
|
import { useTheme } from '../theme/use-theme';
|
|
12
|
+
import { adoptStyleSheet } from '../styles/adopt-style-sheet';
|
|
12
13
|
import { flattenWebStyle } from '../styles/flatten-web-style';
|
|
13
14
|
import {
|
|
14
15
|
applyIconColor,
|
|
@@ -74,12 +75,7 @@ const BLOOM_FROSTED_ICON_BUTTON_CSS = `
|
|
|
74
75
|
|
|
75
76
|
function useFrostedIconButtonCss(): void {
|
|
76
77
|
useEffect(() => {
|
|
77
|
-
|
|
78
|
-
if (document.getElementById(STYLE_ID)) return;
|
|
79
|
-
const style = document.createElement('style');
|
|
80
|
-
style.id = STYLE_ID;
|
|
81
|
-
style.textContent = BLOOM_FROSTED_ICON_BUTTON_CSS;
|
|
82
|
-
document.head.appendChild(style);
|
|
78
|
+
adoptStyleSheet(STYLE_ID, BLOOM_FROSTED_ICON_BUTTON_CSS);
|
|
83
79
|
}, []);
|
|
84
80
|
}
|
|
85
81
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useEffect } from 'react';
|
|
2
2
|
import { StyleSheet, View, type ViewStyle } from 'react-native';
|
|
3
3
|
|
|
4
|
+
import { adoptStyleSheet } from '../styles/adopt-style-sheet';
|
|
4
5
|
import type { WebCssStyle } from '../styles/web-view-style';
|
|
5
6
|
|
|
6
7
|
interface SpinnerIconProps {
|
|
@@ -72,12 +73,7 @@ const SPIN_STYLE: WebCssStyle = {
|
|
|
72
73
|
|
|
73
74
|
function useKeyframes(): void {
|
|
74
75
|
useEffect(() => {
|
|
75
|
-
|
|
76
|
-
if (document.getElementById(KEYFRAMES_ID)) return;
|
|
77
|
-
const style = document.createElement('style');
|
|
78
|
-
style.id = KEYFRAMES_ID;
|
|
79
|
-
style.textContent = BLOOM_SPINNER_CSS;
|
|
80
|
-
document.head.appendChild(style);
|
|
76
|
+
adoptStyleSheet(KEYFRAMES_ID, BLOOM_SPINNER_CSS);
|
|
81
77
|
}, []);
|
|
82
78
|
}
|
|
83
79
|
|
package/src/overlay/index.tsx
CHANGED
|
@@ -225,6 +225,111 @@ export interface BackdropProps {
|
|
|
225
225
|
testID?: string;
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
interface BackdropLayerProps {
|
|
229
|
+
blurIntensity: number;
|
|
230
|
+
blurTint: 'light' | 'dark' | 'default';
|
|
231
|
+
dimColor: string;
|
|
232
|
+
dimOpacity: number;
|
|
233
|
+
/** Opacity the caller asked for, already lifted off the press target. */
|
|
234
|
+
staticOpacity: number;
|
|
235
|
+
layerStyle?: StyleProp<ViewStyle>;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Layers fading under a shared value. Reanimated owns the opacity on both, so
|
|
240
|
+
* the fade runs off the JS thread and the caller never has to animate an
|
|
241
|
+
* ancestor (which would erase the blur — see `BackdropProps['style']`).
|
|
242
|
+
*/
|
|
243
|
+
function AnimatedBackdropLayers({
|
|
244
|
+
progress,
|
|
245
|
+
blurIntensity,
|
|
246
|
+
blurTint,
|
|
247
|
+
dimColor,
|
|
248
|
+
dimOpacity,
|
|
249
|
+
staticOpacity,
|
|
250
|
+
layerStyle,
|
|
251
|
+
}: BackdropLayerProps & { progress: SharedValue<number> }) {
|
|
252
|
+
// The fade lives on each LAYER, never on their shared ancestor.
|
|
253
|
+
const blurFade = useAnimatedStyle(
|
|
254
|
+
() => ({ opacity: progress.value * staticOpacity }),
|
|
255
|
+
[progress, staticOpacity],
|
|
256
|
+
);
|
|
257
|
+
const dimFade = useAnimatedStyle(
|
|
258
|
+
() => ({ opacity: progress.value * staticOpacity * dimOpacity }),
|
|
259
|
+
[progress, staticOpacity, dimOpacity],
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<>
|
|
264
|
+
{blurIntensity > 0 ? (
|
|
265
|
+
<AnimatedBlurView
|
|
266
|
+
intensity={blurIntensity}
|
|
267
|
+
tint={blurTint}
|
|
268
|
+
// Android's default blur is a no-op on many devices; this is the
|
|
269
|
+
// implementation that actually renders there.
|
|
270
|
+
experimentalBlurMethod="dimezisBlurView"
|
|
271
|
+
pointerEvents="none"
|
|
272
|
+
style={[StyleSheet.absoluteFill, layerStyle, blurFade]}
|
|
273
|
+
/>
|
|
274
|
+
) : null}
|
|
275
|
+
<Animated.View
|
|
276
|
+
pointerEvents="none"
|
|
277
|
+
style={[StyleSheet.absoluteFill, { backgroundColor: dimColor }, layerStyle, dimFade]}
|
|
278
|
+
/>
|
|
279
|
+
</>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Layers with no shared value driving them: the opacity is whatever the caller
|
|
285
|
+
* asked for, and a caller that still wants movement hands a CSS transition down
|
|
286
|
+
* through `layerStyle` — which is exactly what the web side sheet does, flipping
|
|
287
|
+
* its own `opacity` between 0 and 1 and letting the browser interpolate.
|
|
288
|
+
*
|
|
289
|
+
* These MUST stay plain, non-reanimated components. On web, an animated
|
|
290
|
+
* component whose style carries `transitionProperty` is routed through
|
|
291
|
+
* reanimated's CSS transitions manager, which assigns straight into
|
|
292
|
+
* `element.style` — where `element` is whatever ref the wrapped component
|
|
293
|
+
* exposes. expo-blur's web `BlurView` exposes a `useImperativeHandle` object
|
|
294
|
+
* carrying only `setNativeProps`, so there is no `.style` on it and opening the
|
|
295
|
+
* surface throws `Cannot set properties of undefined (setting
|
|
296
|
+
* 'transitionProperty')`. A plain layer puts the transition on the DOM node
|
|
297
|
+
* itself, which is both where it belongs and the only place it works.
|
|
298
|
+
*/
|
|
299
|
+
function StaticBackdropLayers({
|
|
300
|
+
blurIntensity,
|
|
301
|
+
blurTint,
|
|
302
|
+
dimColor,
|
|
303
|
+
dimOpacity,
|
|
304
|
+
staticOpacity,
|
|
305
|
+
layerStyle,
|
|
306
|
+
}: BackdropLayerProps) {
|
|
307
|
+
return (
|
|
308
|
+
<>
|
|
309
|
+
{blurIntensity > 0 ? (
|
|
310
|
+
<BlurView
|
|
311
|
+
intensity={blurIntensity}
|
|
312
|
+
tint={blurTint}
|
|
313
|
+
experimentalBlurMethod="dimezisBlurView"
|
|
314
|
+
pointerEvents="none"
|
|
315
|
+
// Opacity last, as in the animated pair: the level this component
|
|
316
|
+
// resolved wins over anything `layerStyle` happens to carry.
|
|
317
|
+
style={[StyleSheet.absoluteFill, layerStyle, { opacity: staticOpacity }]}
|
|
318
|
+
/>
|
|
319
|
+
) : null}
|
|
320
|
+
<View
|
|
321
|
+
pointerEvents="none"
|
|
322
|
+
style={[
|
|
323
|
+
StyleSheet.absoluteFill,
|
|
324
|
+
{ backgroundColor: dimColor },
|
|
325
|
+
layerStyle,
|
|
326
|
+
{ opacity: staticOpacity * dimOpacity },
|
|
327
|
+
]}
|
|
328
|
+
/>
|
|
329
|
+
</>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
228
333
|
/**
|
|
229
334
|
* Full-bleed blur + dim that dismisses the surface when pressed.
|
|
230
335
|
*
|
|
@@ -271,15 +376,14 @@ export const Backdrop = memo(function Backdrop({
|
|
|
271
376
|
const staticOpacity = typeof styleOpacity === 'number' ? styleOpacity : 1;
|
|
272
377
|
const resolvedDimColor = typeof styleBackground === 'string' ? styleBackground : dimColor;
|
|
273
378
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
);
|
|
379
|
+
const layerProps: BackdropLayerProps = {
|
|
380
|
+
blurIntensity,
|
|
381
|
+
blurTint,
|
|
382
|
+
dimColor: resolvedDimColor,
|
|
383
|
+
dimOpacity,
|
|
384
|
+
staticOpacity,
|
|
385
|
+
layerStyle,
|
|
386
|
+
};
|
|
283
387
|
|
|
284
388
|
return (
|
|
285
389
|
// `box-none` so this box never takes a press itself: the hit box below and
|
|
@@ -298,21 +402,16 @@ export const Backdrop = memo(function Backdrop({
|
|
|
298
402
|
testID={testID}
|
|
299
403
|
style={StyleSheet.absoluteFill}
|
|
300
404
|
>
|
|
301
|
-
{
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
) : null}
|
|
312
|
-
<Animated.View
|
|
313
|
-
pointerEvents="none"
|
|
314
|
-
style={[StyleSheet.absoluteFill, { backgroundColor: resolvedDimColor }, layerStyle, dimFade]}
|
|
315
|
-
/>
|
|
405
|
+
{/* Split into two components rather than branching inside one, so the
|
|
406
|
+
reanimated hooks stay unconditional AND a caller without a shared
|
|
407
|
+
value gets layers reanimated never touches. Which branch a call site
|
|
408
|
+
takes is fixed by whether it passes `progress` at all, so this never
|
|
409
|
+
swaps mid-life. */}
|
|
410
|
+
{progress ? (
|
|
411
|
+
<AnimatedBackdropLayers progress={progress} {...layerProps} />
|
|
412
|
+
) : (
|
|
413
|
+
<StaticBackdropLayers {...layerProps} />
|
|
414
|
+
)}
|
|
316
415
|
</Pressable>
|
|
317
416
|
{children}
|
|
318
417
|
</View>
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// adoptStyleSheet — the ONE way a Bloom web fork puts CSS on the page.
|
|
3
|
+
//
|
|
4
|
+
// Bloom's `.web.tsx` forks self-inject the CSS they need (`@keyframes`,
|
|
5
|
+
// pseudo-classes, `::-webkit-scrollbar`) rather than asking consumers to paste
|
|
6
|
+
// it into a global stylesheet — see AGENTS.md, "Web Fork CSS & Style
|
|
7
|
+
// Conventions". The mechanism used to be a `<style>` element carrying the CSS
|
|
8
|
+
// as text content. That is exactly the shape a Content-Security-Policy of
|
|
9
|
+
// `style-src 'self'` blocks: the check runs when a style element's text
|
|
10
|
+
// content is parsed into a sheet, so the rules are dropped, the component
|
|
11
|
+
// renders with none of its CSS, and NOTHING is thrown — the fork's own
|
|
12
|
+
// "already injected?" guard even finds its element in the DOM and reports
|
|
13
|
+
// success. Astro's `astro://` WebUI pages ship that CSP, and every other Oxy
|
|
14
|
+
// surface behind one would fail the same silent way.
|
|
15
|
+
//
|
|
16
|
+
// A constructed stylesheet is not subject to `style-src` at all — CSSOM is
|
|
17
|
+
// outside CSP's inline hooks — so `new CSSStyleSheet()` + `replaceSync()` +
|
|
18
|
+
// `document.adoptedStyleSheets` applies the same CSS under a strict policy
|
|
19
|
+
// with no nonce, no hash, and no consumer-side wiring. (This is also why
|
|
20
|
+
// react-native-web is unaffected: it appends an EMPTY style element and fills
|
|
21
|
+
// it through `sheet.insertRule`, never through text content.)
|
|
22
|
+
//
|
|
23
|
+
// The `<style>` path survives only as the fallback for a document without
|
|
24
|
+
// constructed-stylesheet support — jsdom 26 has the `CSSStyleSheet`
|
|
25
|
+
// constructor but neither `replaceSync` nor `adoptedStyleSheets`, so the whole
|
|
26
|
+
// jest suite takes it, and so do browsers older than Chrome 73 / Safari 16.4 /
|
|
27
|
+
// Firefox 101. There it behaves exactly as Bloom did before, CSP included:
|
|
28
|
+
// the fallback cannot make anything worse, and cannot make a strict policy
|
|
29
|
+
// work either.
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
interface AdoptedSheet {
|
|
33
|
+
sheet: CSSStyleSheet;
|
|
34
|
+
/** The CSS currently in `sheet`, so a re-render re-parses nothing. */
|
|
35
|
+
css: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The sheets Bloom has constructed, per document. Keyed by document rather than
|
|
40
|
+
* held in one flat map because a constructed sheet belongs to the realm that
|
|
41
|
+
* created it — adopting one into a different document throws — and Bloom runs
|
|
42
|
+
* in more than one when a consumer opens a popout window.
|
|
43
|
+
*/
|
|
44
|
+
const adoptedByDocument = new WeakMap<Document, Map<string, AdoptedSheet>>();
|
|
45
|
+
|
|
46
|
+
function sheetsFor(doc: Document): Map<string, AdoptedSheet> {
|
|
47
|
+
const existing = adoptedByDocument.get(doc);
|
|
48
|
+
if (existing) return existing;
|
|
49
|
+
const created = new Map<string, AdoptedSheet>();
|
|
50
|
+
adoptedByDocument.set(doc, created);
|
|
51
|
+
return created;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Whether `doc` can take a constructed stylesheet. Evaluated per call, not once
|
|
56
|
+
* at module scope: a test installs the API on its window after this module has
|
|
57
|
+
* already been imported, and a module-scope constant would freeze the answer
|
|
58
|
+
* from before that.
|
|
59
|
+
*/
|
|
60
|
+
function supportsAdoption(doc: Document): boolean {
|
|
61
|
+
return (
|
|
62
|
+
'adoptedStyleSheets' in doc &&
|
|
63
|
+
typeof CSSStyleSheet === 'function' &&
|
|
64
|
+
typeof CSSStyleSheet.prototype.replaceSync === 'function'
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Make `css` apply to the document, under the identity `id`.
|
|
70
|
+
*
|
|
71
|
+
* Idempotent in both directions: calling it again with the same CSS re-parses
|
|
72
|
+
* nothing and adopts nothing twice (the usual case — every mount of every
|
|
73
|
+
* instance of a component calls it), and calling it with DIFFERENT CSS under
|
|
74
|
+
* the same id replaces the previous content in place (the theme-dependent case,
|
|
75
|
+
* `BottomSheetBase`'s scrollbar colours).
|
|
76
|
+
*
|
|
77
|
+
* No-op off the web, so a platform-neutral component can call it unguarded.
|
|
78
|
+
*/
|
|
79
|
+
export function adoptStyleSheet(id: string, css: string): void {
|
|
80
|
+
if (typeof document === 'undefined') return;
|
|
81
|
+
|
|
82
|
+
if (supportsAdoption(document)) {
|
|
83
|
+
const sheets = sheetsFor(document);
|
|
84
|
+
let entry = sheets.get(id);
|
|
85
|
+
if (!entry) {
|
|
86
|
+
entry = { sheet: new CSSStyleSheet(), css: '' };
|
|
87
|
+
sheets.set(id, entry);
|
|
88
|
+
}
|
|
89
|
+
if (entry.css !== css) {
|
|
90
|
+
entry.sheet.replaceSync(css);
|
|
91
|
+
entry.css = css;
|
|
92
|
+
}
|
|
93
|
+
// Re-adoption after a consumer (or a test) has reset the list. `includes`
|
|
94
|
+
// is what makes the common repeat call free.
|
|
95
|
+
if (!document.adoptedStyleSheets.includes(entry.sheet)) {
|
|
96
|
+
document.adoptedStyleSheets = [...document.adoptedStyleSheets, entry.sheet];
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const existing = document.getElementById(id);
|
|
102
|
+
if (existing instanceof HTMLStyleElement) {
|
|
103
|
+
if (existing.textContent !== css) existing.textContent = css;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const element = document.createElement('style');
|
|
107
|
+
element.id = id;
|
|
108
|
+
element.textContent = css;
|
|
109
|
+
document.head.appendChild(element);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Detach the stylesheet `id`, whichever path installed it.
|
|
114
|
+
*
|
|
115
|
+
* Only for CSS whose lifetime is a component's — `BottomSheetBase` removes its
|
|
116
|
+
* scrollbar rules when the last sheet unmounts. The static per-component CSS is
|
|
117
|
+
* never dropped: it is the same bytes for the life of the page, and removing it
|
|
118
|
+
* on unmount would make a re-mount re-parse it.
|
|
119
|
+
*/
|
|
120
|
+
export function dropStyleSheet(id: string): void {
|
|
121
|
+
if (typeof document === 'undefined') return;
|
|
122
|
+
|
|
123
|
+
const entry = adoptedByDocument.get(document)?.get(id);
|
|
124
|
+
if (entry) {
|
|
125
|
+
document.adoptedStyleSheets = document.adoptedStyleSheets.filter(
|
|
126
|
+
(sheet) => sheet !== entry.sheet,
|
|
127
|
+
);
|
|
128
|
+
adoptedByDocument.get(document)?.delete(id);
|
|
129
|
+
}
|
|
130
|
+
document.getElementById(id)?.remove();
|
|
131
|
+
}
|