@proulr/sdui-components 0.1.75 → 0.1.77
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
1
|
{
|
|
2
2
|
"name": "@proulr/sdui-components",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.77",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/renderer/index.ts",
|
|
6
6
|
"types": "./src/renderer/index.ts",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"react-native-qrcode-svg": "^6.3.21"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"expo-blur": ">=
|
|
46
|
+
"expo-blur": ">=56",
|
|
47
47
|
"expo-linear-gradient": ">=56",
|
|
48
48
|
"react": ">=19",
|
|
49
49
|
"react-native": ">=0.85",
|
|
@@ -13,3 +13,11 @@ export const CHROME_GLASS_BLUR_INTENSITY = 50;
|
|
|
13
13
|
export const CHROME_GLASS_BLUR_TINT = 'dark' as const;
|
|
14
14
|
|
|
15
15
|
export type ChromeGlassBlurTint = typeof CHROME_GLASS_BLUR_TINT;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Android: blur real exige `blurMethod` + `blurTarget` (BlurTargetView) —
|
|
19
|
+
* o default 'none' vira só um overlay translúcido, sem frost.
|
|
20
|
+
*/
|
|
21
|
+
export const CHROME_GLASS_ANDROID_BLUR_METHOD = 'dimezisBlurView' as const;
|
|
22
|
+
|
|
23
|
+
export type ChromeGlassAndroidBlurMethod = typeof CHROME_GLASS_ANDROID_BLUR_METHOD;
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useRef,
|
|
5
|
+
type ReactNode,
|
|
6
|
+
type RefObject,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import { BlurTargetView, BlurView } from 'expo-blur';
|
|
3
9
|
import {
|
|
10
|
+
Platform,
|
|
4
11
|
StyleSheet,
|
|
5
12
|
View,
|
|
6
13
|
type StyleProp,
|
|
@@ -9,6 +16,7 @@ import {
|
|
|
9
16
|
} from 'react-native';
|
|
10
17
|
import { colors } from '../theme/proulr-theme';
|
|
11
18
|
import {
|
|
19
|
+
CHROME_GLASS_ANDROID_BLUR_METHOD,
|
|
12
20
|
CHROME_GLASS_BLUR_INTENSITY,
|
|
13
21
|
CHROME_GLASS_BLUR_TINT,
|
|
14
22
|
} from '../theme/chrome-glass';
|
|
@@ -22,6 +30,52 @@ const absoluteFill: ViewStyle = {
|
|
|
22
30
|
left: 0,
|
|
23
31
|
};
|
|
24
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Android: `BlurView` só faz blur real apontando para um `BlurTargetView`
|
|
35
|
+
* (ref via `blurTarget`) — iOS/web usam o backdrop nativo e ignoram o target.
|
|
36
|
+
*/
|
|
37
|
+
const ChromeGlassTargetContext = createContext<RefObject<View | null> | null>(null);
|
|
38
|
+
|
|
39
|
+
/** Provider do alvo de blur — envolve chrome + conteúdo (uma vez por shell). */
|
|
40
|
+
export function ChromeGlassTargetProvider({ children }: { children: ReactNode }) {
|
|
41
|
+
const targetRef = useRef<View | null>(null);
|
|
42
|
+
return (
|
|
43
|
+
<ChromeGlassTargetContext.Provider value={targetRef}>
|
|
44
|
+
{children}
|
|
45
|
+
</ChromeGlassTargetContext.Provider>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type ChromeGlassBlurTargetProps = {
|
|
50
|
+
children: ReactNode;
|
|
51
|
+
style?: StyleProp<ViewStyle>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Envolve o conteúdo que fica atrás do vidro. No Android vira `BlurTargetView`;
|
|
56
|
+
* nas demais plataformas (ou sem provider) é um `View` comum.
|
|
57
|
+
*/
|
|
58
|
+
export function ChromeGlassBlurTarget({ children, style }: ChromeGlassBlurTargetProps) {
|
|
59
|
+
const targetRef = useContext(ChromeGlassTargetContext);
|
|
60
|
+
if (Platform.OS !== 'android' || !targetRef) {
|
|
61
|
+
return <View style={style}>{children}</View>;
|
|
62
|
+
}
|
|
63
|
+
return (
|
|
64
|
+
<BlurTargetView ref={targetRef} style={style}>
|
|
65
|
+
{children}
|
|
66
|
+
</BlurTargetView>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function useAndroidBlurProps(): {
|
|
71
|
+
blurTarget: RefObject<View | null>;
|
|
72
|
+
blurMethod: typeof CHROME_GLASS_ANDROID_BLUR_METHOD;
|
|
73
|
+
} | null {
|
|
74
|
+
const targetRef = useContext(ChromeGlassTargetContext);
|
|
75
|
+
if (Platform.OS !== 'android' || !targetRef) return null;
|
|
76
|
+
return { blurTarget: targetRef, blurMethod: CHROME_GLASS_ANDROID_BLUR_METHOD };
|
|
77
|
+
}
|
|
78
|
+
|
|
25
79
|
type ChromeGlassLayerProps = {
|
|
26
80
|
style?: StyleProp<ViewStyle>;
|
|
27
81
|
pointerEvents?: ViewProps['pointerEvents'];
|
|
@@ -35,12 +89,14 @@ export function ChromeGlassLayer({
|
|
|
35
89
|
style,
|
|
36
90
|
pointerEvents = 'none',
|
|
37
91
|
}: ChromeGlassLayerProps) {
|
|
92
|
+
const androidBlurProps = useAndroidBlurProps();
|
|
38
93
|
return (
|
|
39
94
|
<View style={[absoluteFill, style]} pointerEvents={pointerEvents}>
|
|
40
95
|
<BlurView
|
|
41
96
|
intensity={CHROME_GLASS_BLUR_INTENSITY}
|
|
42
97
|
tint={CHROME_GLASS_BLUR_TINT}
|
|
43
98
|
style={StyleSheet.absoluteFill}
|
|
99
|
+
{...(androidBlurProps ?? {})}
|
|
44
100
|
/>
|
|
45
101
|
<View style={[absoluteFill, styles.tint]} />
|
|
46
102
|
</View>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { View, type DimensionValue, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
Easing,
|
|
5
|
+
ReduceMotion,
|
|
6
|
+
useAnimatedStyle,
|
|
7
|
+
useSharedValue,
|
|
8
|
+
withRepeat,
|
|
9
|
+
withTiming,
|
|
10
|
+
} from 'react-native-reanimated';
|
|
11
|
+
import { m3Colors, m3Shape, spacing } from '../theme/proulr-theme';
|
|
12
|
+
|
|
13
|
+
const PULSE_DURATION_MS = 900;
|
|
14
|
+
const PULSE_MIN_OPACITY = 0.45;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Pulso de opacidade compartilhado por cada bloco de skeleton. Respeita o
|
|
18
|
+
* "reduzir movimento" do sistema (fica estatico). Como `react-native-reanimated`
|
|
19
|
+
* ja e usado nos primitivos do pacote (ex.: RangeSlider), importamos direto.
|
|
20
|
+
*/
|
|
21
|
+
function usePulseStyle() {
|
|
22
|
+
const progress = useSharedValue(PULSE_MIN_OPACITY);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
progress.value = withRepeat(
|
|
26
|
+
withTiming(1, {
|
|
27
|
+
duration: PULSE_DURATION_MS,
|
|
28
|
+
easing: Easing.inOut(Easing.ease),
|
|
29
|
+
reduceMotion: ReduceMotion.System,
|
|
30
|
+
}),
|
|
31
|
+
-1,
|
|
32
|
+
true,
|
|
33
|
+
);
|
|
34
|
+
}, [progress]);
|
|
35
|
+
|
|
36
|
+
return useAnimatedStyle(() => ({ opacity: progress.value }));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type SkeletonProps = {
|
|
40
|
+
width?: DimensionValue;
|
|
41
|
+
height?: DimensionValue;
|
|
42
|
+
radius?: number;
|
|
43
|
+
style?: StyleProp<ViewStyle>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Bloco tonal com shimmer — base de qualquer skeleton. */
|
|
47
|
+
export function Skeleton({
|
|
48
|
+
width = '100%',
|
|
49
|
+
height = 16,
|
|
50
|
+
radius = m3Shape.cornerSmall,
|
|
51
|
+
style,
|
|
52
|
+
}: SkeletonProps) {
|
|
53
|
+
const pulse = usePulseStyle();
|
|
54
|
+
return (
|
|
55
|
+
<Animated.View
|
|
56
|
+
accessibilityRole="none"
|
|
57
|
+
style={[
|
|
58
|
+
{ width, height, borderRadius: radius, backgroundColor: m3Colors.surfaceContainerHigh },
|
|
59
|
+
pulse,
|
|
60
|
+
style,
|
|
61
|
+
]}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type SkeletonCircleProps = {
|
|
67
|
+
size?: number;
|
|
68
|
+
style?: StyleProp<ViewStyle>;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Placeholder circular (avatar). */
|
|
72
|
+
export function SkeletonCircle({ size = 48, style }: SkeletonCircleProps) {
|
|
73
|
+
return <Skeleton width={size} height={size} radius={size / 2} style={style} />;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type SkeletonTextProps = {
|
|
77
|
+
lines?: number;
|
|
78
|
+
lineHeight?: number;
|
|
79
|
+
gap?: number;
|
|
80
|
+
/** Largura da ultima linha (efeito paragrafo). */
|
|
81
|
+
lastLineWidth?: DimensionValue;
|
|
82
|
+
style?: StyleProp<ViewStyle>;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Bloco de linhas de texto (ultima linha mais curta). */
|
|
86
|
+
export function SkeletonText({
|
|
87
|
+
lines = 3,
|
|
88
|
+
lineHeight = 12,
|
|
89
|
+
gap = spacing.xs,
|
|
90
|
+
lastLineWidth = '60%',
|
|
91
|
+
style,
|
|
92
|
+
}: SkeletonTextProps) {
|
|
93
|
+
return (
|
|
94
|
+
<View style={[{ gap }, style]}>
|
|
95
|
+
{Array.from({ length: Math.max(1, lines) }).map((_, index) => {
|
|
96
|
+
const isLast = index === lines - 1;
|
|
97
|
+
return (
|
|
98
|
+
<Skeleton
|
|
99
|
+
key={index}
|
|
100
|
+
height={lineHeight}
|
|
101
|
+
width={isLast && lines > 1 ? lastLineWidth : '100%'}
|
|
102
|
+
radius={m3Shape.cornerFull}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
})}
|
|
106
|
+
</View>
|
|
107
|
+
);
|
|
108
|
+
}
|
package/src/renderer/ui/index.ts
CHANGED
|
@@ -11,7 +11,12 @@ export { AuthStepShell } from './AuthStepShell';
|
|
|
11
11
|
export { Card } from './Card';
|
|
12
12
|
export { Chip } from './Chip';
|
|
13
13
|
export { ChipRow } from './ChipRow';
|
|
14
|
-
export {
|
|
14
|
+
export {
|
|
15
|
+
ChromeGlassBlurTarget,
|
|
16
|
+
ChromeGlassLayer,
|
|
17
|
+
ChromeGlassShell,
|
|
18
|
+
ChromeGlassTargetProvider,
|
|
19
|
+
} from './ChromeGlass';
|
|
15
20
|
export { ComposeAudiencePill } from './compose-audience-pill';
|
|
16
21
|
export {
|
|
17
22
|
AppIcon,
|
|
@@ -59,6 +64,14 @@ export { ProgressBar } from './ProgressBar';
|
|
|
59
64
|
export { RangeSlider, type RangeSliderProps } from './RangeSlider';
|
|
60
65
|
export { SectionHeader } from './SectionHeader';
|
|
61
66
|
export { SelectRow, MultiSelectRow } from './SelectRow';
|
|
67
|
+
export {
|
|
68
|
+
Skeleton,
|
|
69
|
+
SkeletonCircle,
|
|
70
|
+
SkeletonText,
|
|
71
|
+
type SkeletonProps,
|
|
72
|
+
type SkeletonCircleProps,
|
|
73
|
+
type SkeletonTextProps,
|
|
74
|
+
} from './Skeleton';
|
|
62
75
|
export { StatRow } from './StatRow';
|
|
63
76
|
export { ToggleRow } from './ToggleRow';
|
|
64
77
|
export { PasswordField, TextField } from './TextField';
|