@proulr/sdui-components 0.1.80 → 0.1.82
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { memo } from 'react';
|
|
2
2
|
import { StyleSheet } from 'react-native';
|
|
3
|
-
import type { SduiScreen } from '@proulr/sdui-contract';
|
|
3
|
+
import type { SduiScreen, SduiBlock } from '@proulr/sdui-contract';
|
|
4
4
|
import { colors } from './theme/proulr-theme';
|
|
5
5
|
import { renderBlockTree } from './sdui-component-registry';
|
|
6
6
|
import { SduiLayoutDebugFrame } from './sdui-layout-debug';
|
|
@@ -13,23 +13,40 @@ type Props = {
|
|
|
13
13
|
/** Blocos overlay (FAB etc.) — não competem por flex com o Scroll. */
|
|
14
14
|
const OVERLAY_BLOCK_TYPES = new Set(['floating_create_menu', 'inbox_hub_chrome']);
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
type ScreenBlockProps = {
|
|
17
|
+
block: SduiBlock;
|
|
18
|
+
data: SduiScreen['data'];
|
|
19
|
+
onAction: Props['onAction'];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Bloco memoizado: re-renderiza só quando `block`/`data`/`onAction` mudam de
|
|
24
|
+
* identidade (novo wire). Re-renders do shell (header, insets, sheets) não
|
|
25
|
+
* reconstroem a árvore de todos os blocos — custo direto de INP no web.
|
|
26
|
+
*/
|
|
27
|
+
const SduiScreenBlock = memo(function SduiScreenBlock({ block, data, onAction }: ScreenBlockProps) {
|
|
28
|
+
const overlay = OVERLAY_BLOCK_TYPES.has(block.type);
|
|
29
|
+
return (
|
|
30
|
+
<SduiLayoutDebugFrame
|
|
31
|
+
layer="block"
|
|
32
|
+
extra={block.type}
|
|
33
|
+
style={overlay ? styles.overlayBlock : styles.block}
|
|
34
|
+
pointerEvents={overlay ? 'box-none' : undefined}
|
|
35
|
+
>
|
|
36
|
+
{renderBlockTree(block, data, onAction)}
|
|
37
|
+
</SduiLayoutDebugFrame>
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const SduiScreenRenderer = memo(function SduiScreenRenderer({ screen, onAction }: Props) {
|
|
17
42
|
return (
|
|
18
43
|
<SduiLayoutDebugFrame layer="renderer" style={styles.screen}>
|
|
19
44
|
{screen.blocks.map((block) => (
|
|
20
|
-
<
|
|
21
|
-
key={block.id}
|
|
22
|
-
layer="block"
|
|
23
|
-
extra={block.type}
|
|
24
|
-
style={OVERLAY_BLOCK_TYPES.has(block.type) ? styles.overlayBlock : styles.block}
|
|
25
|
-
pointerEvents={OVERLAY_BLOCK_TYPES.has(block.type) ? 'box-none' : undefined}
|
|
26
|
-
>
|
|
27
|
-
{renderBlockTree(block, screen.data, onAction)}
|
|
28
|
-
</SduiLayoutDebugFrame>
|
|
45
|
+
<SduiScreenBlock key={block.id} block={block} data={screen.data} onAction={onAction} />
|
|
29
46
|
))}
|
|
30
47
|
</SduiLayoutDebugFrame>
|
|
31
48
|
);
|
|
32
|
-
}
|
|
49
|
+
});
|
|
33
50
|
|
|
34
51
|
const styles = StyleSheet.create({
|
|
35
52
|
screen: { flex: 1, minHeight: 0, backgroundColor: colors.bg },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useEffect, useState } from 'react';
|
|
2
|
-
import { StyleSheet, Text, View } from 'react-native';
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { AppState, type AppStateStatus, StyleSheet, Text, View } from 'react-native';
|
|
3
3
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
4
4
|
import {
|
|
5
5
|
DEFAULT_NATIVE_AD_ASPECT_RATIO,
|
|
@@ -12,6 +12,13 @@ import type { SduiBlockRenderProps } from './SduiPrimitiveBlocks';
|
|
|
12
12
|
type NativeAdModule = typeof import('react-native-google-mobile-ads');
|
|
13
13
|
type LoadedNativeAd = Awaited<ReturnType<NativeAdModule['NativeAd']['createForAdRequest']>>;
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Ao voltar do segundo plano o `NativeMediaView` do Android costuma renderizar
|
|
17
|
+
* em branco e o ad expira (~1h). Recarrega quando falhou antes ou está mais
|
|
18
|
+
* velho que isto; abaixo do limiar são só app-switches rápidos.
|
|
19
|
+
*/
|
|
20
|
+
const NATIVE_AD_RESUME_RELOAD_MS = 30_000;
|
|
21
|
+
|
|
15
22
|
function resolveBlockWidth(props: Record<string, unknown>): number | undefined {
|
|
16
23
|
if (typeof props.width === 'number' && props.width > 0) return props.width;
|
|
17
24
|
return undefined;
|
|
@@ -42,6 +49,10 @@ export function SduiNativeAdBlock({ block }: SduiBlockRenderProps) {
|
|
|
42
49
|
const [adsModule, setAdsModule] = useState<NativeAdModule | null>(null);
|
|
43
50
|
const [nativeAd, setNativeAd] = useState<LoadedNativeAd | null>(null);
|
|
44
51
|
const [failed, setFailed] = useState(false);
|
|
52
|
+
const [reloadToken, setReloadToken] = useState(0);
|
|
53
|
+
|
|
54
|
+
const loadedAtRef = useRef(0);
|
|
55
|
+
const failedRef = useRef(false);
|
|
45
56
|
|
|
46
57
|
useEffect(() => {
|
|
47
58
|
if (!catalog?.isEnabled()) return;
|
|
@@ -66,16 +77,22 @@ export function SduiNativeAdBlock({ block }: SduiBlockRenderProps) {
|
|
|
66
77
|
return;
|
|
67
78
|
}
|
|
68
79
|
|
|
80
|
+
loadedAtRef.current = Date.now();
|
|
81
|
+
failedRef.current = false;
|
|
82
|
+
setFailed(false);
|
|
69
83
|
setNativeAd(ad);
|
|
70
84
|
} catch {
|
|
71
|
-
if (!cancelled)
|
|
85
|
+
if (!cancelled) {
|
|
86
|
+
failedRef.current = true;
|
|
87
|
+
setFailed(true);
|
|
88
|
+
}
|
|
72
89
|
}
|
|
73
90
|
})();
|
|
74
91
|
|
|
75
92
|
return () => {
|
|
76
93
|
cancelled = true;
|
|
77
94
|
};
|
|
78
|
-
}, [catalog, placement]);
|
|
95
|
+
}, [catalog, placement, reloadToken]);
|
|
79
96
|
|
|
80
97
|
useEffect(() => {
|
|
81
98
|
if (!nativeAd) return;
|
|
@@ -84,6 +101,35 @@ export function SduiNativeAdBlock({ block }: SduiBlockRenderProps) {
|
|
|
84
101
|
};
|
|
85
102
|
}, [nativeAd]);
|
|
86
103
|
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!catalog?.isEnabled()) return;
|
|
106
|
+
|
|
107
|
+
let backgroundSince: number | null = null;
|
|
108
|
+
|
|
109
|
+
const subscription = AppState.addEventListener('change', (state: AppStateStatus) => {
|
|
110
|
+
if (state === 'active') {
|
|
111
|
+
if (backgroundSince === null) return;
|
|
112
|
+
const backgroundMs = Date.now() - backgroundSince;
|
|
113
|
+
backgroundSince = null;
|
|
114
|
+
if (backgroundMs < 3_000) return;
|
|
115
|
+
const stale =
|
|
116
|
+
loadedAtRef.current === 0 ||
|
|
117
|
+
Date.now() - loadedAtRef.current > NATIVE_AD_RESUME_RELOAD_MS;
|
|
118
|
+
if (failedRef.current || stale) {
|
|
119
|
+
setReloadToken((token) => token + 1);
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (backgroundSince === null) {
|
|
124
|
+
backgroundSince = Date.now();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return () => {
|
|
129
|
+
subscription.remove();
|
|
130
|
+
};
|
|
131
|
+
}, [catalog]);
|
|
132
|
+
|
|
87
133
|
if (!catalog?.isEnabled()) {
|
|
88
134
|
return null;
|
|
89
135
|
}
|
|
@@ -91,12 +91,24 @@ type ChromeGlassLayerProps = {
|
|
|
91
91
|
/**
|
|
92
92
|
* Camada de frost única do sistema (igual AppSideDrawer topo/base).
|
|
93
93
|
* BlurView + `colors.chromeGlassOverlay` — sem cast cinza.
|
|
94
|
+
*
|
|
95
|
+
* Web: BlurView vira `backdrop-filter` em camada fixa sobre conteúdo que rola —
|
|
96
|
+
* repaint contínuo no scroll (custo direto de INP). Fallback: pílula OLED opaca
|
|
97
|
+
* (`colors.chromePillBackground`), mesmo visual do chrome sem glass.
|
|
94
98
|
*/
|
|
95
99
|
export function ChromeGlassLayer({
|
|
96
100
|
style,
|
|
97
101
|
pointerEvents = 'none',
|
|
98
102
|
}: ChromeGlassLayerProps) {
|
|
99
103
|
const androidBlurProps = useAndroidBlurProps();
|
|
104
|
+
if (Platform.OS === 'web') {
|
|
105
|
+
return (
|
|
106
|
+
<View
|
|
107
|
+
style={[absoluteFill, styles.webGlassFallback, style]}
|
|
108
|
+
pointerEvents={pointerEvents}
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
100
112
|
return (
|
|
101
113
|
<View style={[absoluteFill, style]} pointerEvents={pointerEvents}>
|
|
102
114
|
<BlurView
|
|
@@ -132,6 +144,9 @@ const styles = StyleSheet.create({
|
|
|
132
144
|
tint: {
|
|
133
145
|
backgroundColor: colors.chromeGlassOverlay,
|
|
134
146
|
},
|
|
147
|
+
webGlassFallback: {
|
|
148
|
+
backgroundColor: colors.chromePillBackground,
|
|
149
|
+
},
|
|
135
150
|
shellTransparent: {
|
|
136
151
|
backgroundColor: 'transparent',
|
|
137
152
|
},
|