react-native-terra-ui 0.11.11 → 0.11.13
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 +16 -0
- package/TerraUI.podspec +16 -0
- package/ios/TerraHeaderInsetsComponentView.h +4 -0
- package/ios/TerraHeaderInsetsComponentView.mm +93 -0
- package/lib/module/components/header/hooks/use-header-color.js +16 -0
- package/lib/module/components/header/hooks/use-header-color.js.map +1 -0
- package/lib/module/components/header/hooks/use-large-title-header.js +81 -0
- package/lib/module/components/header/hooks/use-large-title-header.js.map +1 -0
- package/lib/module/components/header/parts/HeaderAccessory.js +21 -6
- package/lib/module/components/header/parts/HeaderAccessory.js.map +1 -1
- package/lib/module/components/header/parts/HeaderBar.js +140 -0
- package/lib/module/components/header/parts/HeaderBar.js.map +1 -0
- package/lib/module/components/header/parts/HeaderGradientOverlay.js +5 -3
- package/lib/module/components/header/parts/HeaderGradientOverlay.js.map +1 -1
- package/lib/module/components/header/parts/HeaderSurface.js +8 -14
- package/lib/module/components/header/parts/HeaderSurface.js.map +1 -1
- package/lib/module/components/header/parts/HeaderWindowInsets.ios.js +29 -0
- package/lib/module/components/header/parts/HeaderWindowInsets.ios.js.map +1 -0
- package/lib/module/components/header/parts/HeaderWindowInsets.js +7 -0
- package/lib/module/components/header/parts/HeaderWindowInsets.js.map +1 -0
- package/lib/module/components/header/variants/LargeTitleHeader/index.js +51 -386
- package/lib/module/components/header/variants/LargeTitleHeader/index.js.map +1 -1
- package/lib/module/components/header/variants/LargeTitleHeader/parts/LargeTitleContent.js +72 -0
- package/lib/module/components/header/variants/LargeTitleHeader/parts/LargeTitleContent.js.map +1 -0
- package/lib/module/components/header/variants/TitleHeader/index.js +32 -160
- package/lib/module/components/header/variants/TitleHeader/index.js.map +1 -1
- package/lib/module/components/surface/Surface.js +2 -0
- package/lib/module/components/surface/Surface.js.map +1 -1
- package/lib/module/specs/TerraHeaderInsetsNativeComponent.ts +12 -0
- package/lib/typescript/src/components/header/hooks/use-header-color.d.ts +4 -0
- package/lib/typescript/src/components/header/hooks/use-header-color.d.ts.map +1 -0
- package/lib/typescript/src/components/header/hooks/use-large-title-header.d.ts +36 -0
- package/lib/typescript/src/components/header/hooks/use-large-title-header.d.ts.map +1 -0
- package/lib/typescript/src/components/header/parts/HeaderAccessory.d.ts +8 -5
- package/lib/typescript/src/components/header/parts/HeaderAccessory.d.ts.map +1 -1
- package/lib/typescript/src/components/header/parts/HeaderBar.d.ts +17 -0
- package/lib/typescript/src/components/header/parts/HeaderBar.d.ts.map +1 -0
- package/lib/typescript/src/components/header/parts/HeaderGradientOverlay.d.ts +3 -1
- package/lib/typescript/src/components/header/parts/HeaderGradientOverlay.d.ts.map +1 -1
- package/lib/typescript/src/components/header/parts/HeaderSurface.d.ts +5 -9
- package/lib/typescript/src/components/header/parts/HeaderSurface.d.ts.map +1 -1
- package/lib/typescript/src/components/header/parts/HeaderWindowInsets.d.ts +9 -0
- package/lib/typescript/src/components/header/parts/HeaderWindowInsets.d.ts.map +1 -0
- package/lib/typescript/src/components/header/parts/HeaderWindowInsets.ios.d.ts +3 -0
- package/lib/typescript/src/components/header/parts/HeaderWindowInsets.ios.d.ts.map +1 -0
- package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts +3 -23
- package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts.map +1 -1
- package/lib/typescript/src/components/header/variants/LargeTitleHeader/parts/LargeTitleContent.d.ts +11 -0
- package/lib/typescript/src/components/header/variants/LargeTitleHeader/parts/LargeTitleContent.d.ts.map +1 -0
- package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts +3 -19
- package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts.map +1 -1
- package/lib/typescript/src/components/surface/Surface.d.ts.map +1 -1
- package/lib/typescript/src/specs/TerraHeaderInsetsNativeComponent.d.ts +10 -0
- package/lib/typescript/src/specs/TerraHeaderInsetsNativeComponent.d.ts.map +1 -0
- package/package.json +14 -1
- package/react-native.config.js +7 -0
- package/src/components/header/hooks/use-header-color.ts +14 -0
- package/src/components/header/hooks/use-large-title-header.ts +78 -0
- package/src/components/header/parts/HeaderAccessory.tsx +26 -7
- package/src/components/header/parts/HeaderBar.tsx +139 -0
- package/src/components/header/parts/HeaderGradientOverlay.tsx +9 -3
- package/src/components/header/parts/HeaderSurface.tsx +11 -17
- package/src/components/header/parts/HeaderWindowInsets.ios.tsx +24 -0
- package/src/components/header/parts/HeaderWindowInsets.tsx +8 -0
- package/src/components/header/variants/LargeTitleHeader/index.tsx +44 -369
- package/src/components/header/variants/LargeTitleHeader/parts/LargeTitleContent.tsx +88 -0
- package/src/components/header/variants/TitleHeader/index.tsx +32 -146
- package/src/components/surface/Surface.tsx +2 -1
- package/src/specs/TerraHeaderInsetsNativeComponent.ts +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-terra-ui",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.13",
|
|
4
4
|
"description": "Themeable React Native UI components built on react-native-unistyles",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
"files": [
|
|
22
22
|
"src",
|
|
23
23
|
"lib",
|
|
24
|
+
"ios",
|
|
25
|
+
"TerraUI.podspec",
|
|
26
|
+
"react-native.config.js",
|
|
24
27
|
"CHANGELOG.md",
|
|
25
28
|
"!**/__tests__",
|
|
26
29
|
"!**/__fixtures__",
|
|
@@ -136,6 +139,16 @@
|
|
|
136
139
|
}
|
|
137
140
|
}
|
|
138
141
|
},
|
|
142
|
+
"codegenConfig": {
|
|
143
|
+
"name": "TerraUISpec",
|
|
144
|
+
"type": "components",
|
|
145
|
+
"jsSrcsDir": "src/specs",
|
|
146
|
+
"ios": {
|
|
147
|
+
"componentProvider": {
|
|
148
|
+
"TerraHeaderInsets": "TerraHeaderInsetsComponentView"
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
139
152
|
"create-react-native-library": {
|
|
140
153
|
"type": "library",
|
|
141
154
|
"languages": "js",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
|
|
3
|
+
import { useUnistyles } from "react-native-unistyles";
|
|
4
|
+
|
|
5
|
+
import type { ColorToken } from "#theme/types";
|
|
6
|
+
import { resolveThemeColor } from "#utils/resolve-theme-color";
|
|
7
|
+
import { ScreenContext } from "../../screen/ScreenContext";
|
|
8
|
+
|
|
9
|
+
/** Explicit header color wins, then Screen, then the theme canvas. */
|
|
10
|
+
export function useHeaderColor(token?: ColorToken) {
|
|
11
|
+
const { theme } = useUnistyles();
|
|
12
|
+
const screen = useContext(ScreenContext);
|
|
13
|
+
return (token ? resolveThemeColor(token, theme) : screen?.background) ?? theme.color["surface.canvas"];
|
|
14
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { interpolate, useAnimatedStyle, useSharedValue } from "react-native-reanimated";
|
|
4
|
+
import { useUnistyles } from "react-native-unistyles";
|
|
5
|
+
|
|
6
|
+
import { useScreen } from "../../screen/ScreenContext";
|
|
7
|
+
import type { HeaderAccessoryBehavior } from "../types";
|
|
8
|
+
|
|
9
|
+
interface LargeTitleLayoutOptions {
|
|
10
|
+
hasAccessory: boolean;
|
|
11
|
+
accessoryBehavior: HeaderAccessoryBehavior;
|
|
12
|
+
isLargeTitleHidden: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Coordinates measured layout and UI-thread animation without animating accessory padding. */
|
|
16
|
+
export function useLargeTitleHeader({
|
|
17
|
+
hasAccessory,
|
|
18
|
+
accessoryBehavior,
|
|
19
|
+
isLargeTitleHidden,
|
|
20
|
+
}: LargeTitleLayoutOptions) {
|
|
21
|
+
const { theme } = useUnistyles();
|
|
22
|
+
const { scrollY, contentTopPadding, setHeaderAccessoryHeight } = useScreen();
|
|
23
|
+
const [accessoryHeight, setAccessoryHeight] = useState(0);
|
|
24
|
+
const [largeTitleHeight, setLargeTitleHeight] = useState(0);
|
|
25
|
+
const hidden = useSharedValue(isLargeTitleHidden);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
hidden.value = isLargeTitleHidden;
|
|
28
|
+
}, [hidden, isLargeTitleHidden]);
|
|
29
|
+
|
|
30
|
+
const barHeight = theme.layout.header.height;
|
|
31
|
+
const isSticky = hasAccessory && accessoryBehavior === "sticky" && !isLargeTitleHidden;
|
|
32
|
+
// The measured overlay always includes bottom padding. Expanded scroll content
|
|
33
|
+
// excludes it; compact-only screens reserve the entire accessory.
|
|
34
|
+
const reservedAccessoryHeight = hasAccessory
|
|
35
|
+
? Math.max(0, Math.round(accessoryHeight - (isLargeTitleHidden ? 0 : theme.spacing["2"])))
|
|
36
|
+
: 0;
|
|
37
|
+
const accessoryOffset = isSticky ? Math.max(0, contentTopPadding + largeTitleHeight - barHeight) : 0;
|
|
38
|
+
|
|
39
|
+
useLayoutEffect(() => {
|
|
40
|
+
setHeaderAccessoryHeight(isSticky ? 0 : reservedAccessoryHeight);
|
|
41
|
+
return () => setHeaderAccessoryHeight(0);
|
|
42
|
+
}, [isSticky, reservedAccessoryHeight, setHeaderAccessoryHeight]);
|
|
43
|
+
|
|
44
|
+
const accessoryStyle = useAnimatedStyle(() => ({
|
|
45
|
+
transform: [{ translateY: isSticky && !hidden.value ? Math.max(0, accessoryOffset - scrollY.value) : 0 }],
|
|
46
|
+
}));
|
|
47
|
+
const surfaceStyle = useAnimatedStyle(() => {
|
|
48
|
+
if (hidden.value) return { opacity: 1 };
|
|
49
|
+
const distance = isSticky ? accessoryOffset : barHeight;
|
|
50
|
+
return {
|
|
51
|
+
opacity: distance > 0 ? interpolate(scrollY.value, [0, distance], [0, 1], "clamp") : isSticky ? 1 : 0,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
const compactTitleStyle = useAnimatedStyle(() => {
|
|
55
|
+
const progress = hidden.value
|
|
56
|
+
? 1
|
|
57
|
+
: barHeight > 0
|
|
58
|
+
? interpolate(scrollY.value, [0, barHeight], [0, 1], "clamp")
|
|
59
|
+
: 0;
|
|
60
|
+
return { opacity: progress, transform: [{ translateY: (1 - progress) * 8 }] };
|
|
61
|
+
});
|
|
62
|
+
const largeTitleStyle = useAnimatedStyle(() => {
|
|
63
|
+
const progress = barHeight > 0 ? interpolate(scrollY.value, [0, barHeight], [0, 1], "clamp") : 0;
|
|
64
|
+
return { opacity: 1 - progress, transform: [{ translateY: -progress * 6 }] };
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
isSticky,
|
|
69
|
+
accessoryOffset,
|
|
70
|
+
reservedAccessoryHeight,
|
|
71
|
+
setAccessoryHeight,
|
|
72
|
+
setLargeTitleHeight,
|
|
73
|
+
accessoryStyle,
|
|
74
|
+
surfaceStyle,
|
|
75
|
+
compactTitleStyle,
|
|
76
|
+
largeTitleStyle,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -1,37 +1,53 @@
|
|
|
1
|
-
import { type ReactNode, useCallback,
|
|
2
|
-
import { type LayoutChangeEvent,
|
|
1
|
+
import { type ComponentProps, type ReactNode, useCallback, useLayoutEffect, useRef } from "react";
|
|
2
|
+
import { type LayoutChangeEvent, View } from "react-native";
|
|
3
3
|
|
|
4
4
|
import Animated from "react-native-reanimated";
|
|
5
5
|
import { StyleSheet } from "react-native-unistyles";
|
|
6
6
|
|
|
7
7
|
import type { HeaderAccessoryPadding } from "../types";
|
|
8
|
+
import { HeaderSurface, type HeaderSurfaceProps } from "./HeaderSurface";
|
|
8
9
|
|
|
9
10
|
export interface HeaderAccessoryProps {
|
|
10
11
|
children: ReactNode;
|
|
11
12
|
/** Horizontal padding around the accessory. */
|
|
12
13
|
padding: HeaderAccessoryPadding;
|
|
14
|
+
color: string;
|
|
15
|
+
surfaceStyle?: HeaderSurfaceProps["animatedStyle"];
|
|
13
16
|
/** Reports layout height in dp; reports zero on cleanup to release reserved space. */
|
|
14
|
-
onHeightChange
|
|
17
|
+
onHeightChange?: (height: number) => void;
|
|
15
18
|
/** Additional styles for the accessory container. */
|
|
16
|
-
style?:
|
|
19
|
+
style?: ComponentProps<typeof Animated.View>["style"];
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
/** Measures accessory content so the header can reserve space in the scroll container. */
|
|
20
|
-
export function HeaderAccessory({
|
|
23
|
+
export function HeaderAccessory({
|
|
24
|
+
children,
|
|
25
|
+
padding,
|
|
26
|
+
color,
|
|
27
|
+
surfaceStyle,
|
|
28
|
+
onHeightChange,
|
|
29
|
+
style,
|
|
30
|
+
}: HeaderAccessoryProps) {
|
|
31
|
+
const measuredHeight = useRef(0);
|
|
21
32
|
const handleLayout = useCallback(
|
|
22
33
|
(event: LayoutChangeEvent) => {
|
|
23
|
-
|
|
34
|
+
measuredHeight.current = event.nativeEvent.layout.height;
|
|
35
|
+
onHeightChange?.(measuredHeight.current);
|
|
24
36
|
},
|
|
25
37
|
[onHeightChange]
|
|
26
38
|
);
|
|
27
39
|
|
|
28
40
|
// Release the previous owner’s space when the callback changes or the accessory unmounts.
|
|
29
|
-
|
|
41
|
+
useLayoutEffect(() => {
|
|
42
|
+
onHeightChange?.(measuredHeight.current);
|
|
43
|
+
return () => onHeightChange?.(0);
|
|
44
|
+
}, [onHeightChange]);
|
|
30
45
|
|
|
31
46
|
styles.useVariants({ padding });
|
|
32
47
|
|
|
33
48
|
return (
|
|
34
49
|
<Animated.View style={[styles.accessory, style]} onLayout={handleLayout}>
|
|
50
|
+
<HeaderSurface color={color} animatedStyle={surfaceStyle} accessory />
|
|
35
51
|
{children}
|
|
36
52
|
</Animated.View>
|
|
37
53
|
);
|
|
@@ -44,6 +60,9 @@ export function HeaderAccessorySpacer({ height }: { height: number }) {
|
|
|
44
60
|
|
|
45
61
|
const styles = StyleSheet.create((theme) => ({
|
|
46
62
|
accessory: {
|
|
63
|
+
// Static layout padding is intentional: the background must measure the full
|
|
64
|
+
// padded height even when a horizontal ScrollView is the accessory content.
|
|
65
|
+
paddingBottom: theme.spacing["2"],
|
|
47
66
|
variants: {
|
|
48
67
|
padding: {
|
|
49
68
|
x: { paddingHorizontal: theme.layout.screen.padding.x },
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactNode, useState } from "react";
|
|
2
|
+
import { Text, View } from "react-native";
|
|
3
|
+
|
|
4
|
+
import Animated from "react-native-reanimated";
|
|
5
|
+
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|
6
|
+
|
|
7
|
+
import { FONT_WEIGHT_VALUE } from "#utils/typography";
|
|
8
|
+
import { resolveScreenPadding } from "../../../theme/screen-padding";
|
|
9
|
+
import { Toolbar } from "../../toolbar";
|
|
10
|
+
import { HeaderDismissButton, type HeaderDismissProps } from "../header-buttons";
|
|
11
|
+
import { isCustomTitle, isTextTitle } from "../utils";
|
|
12
|
+
import { HeaderSurface, type HeaderSurfaceProps } from "./HeaderSurface";
|
|
13
|
+
import { HeaderWindowInsets } from "./HeaderWindowInsets";
|
|
14
|
+
|
|
15
|
+
interface HeaderBarProps extends HeaderDismissProps {
|
|
16
|
+
title?: ReactNode;
|
|
17
|
+
leading?: ReactNode;
|
|
18
|
+
trailing?: ReactNode;
|
|
19
|
+
titleAlignment: "center" | "left";
|
|
20
|
+
color: string;
|
|
21
|
+
surfaceStyle?: HeaderSurfaceProps["animatedStyle"];
|
|
22
|
+
titleStyle?: ComponentProps<typeof Animated.View>["style"];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Shared compact bar. Only the title and background animate; actions stay interactive. */
|
|
26
|
+
export function HeaderBar({
|
|
27
|
+
title,
|
|
28
|
+
leading,
|
|
29
|
+
trailing,
|
|
30
|
+
titleAlignment,
|
|
31
|
+
color,
|
|
32
|
+
surfaceStyle,
|
|
33
|
+
titleStyle,
|
|
34
|
+
dismissAction = "none",
|
|
35
|
+
navigation,
|
|
36
|
+
onDismiss,
|
|
37
|
+
}: HeaderBarProps) {
|
|
38
|
+
const { theme } = useUnistyles();
|
|
39
|
+
const [windowInsets, setWindowInsets] = useState({ left: 0, right: 0 });
|
|
40
|
+
styles.useVariants({ titleAlignment });
|
|
41
|
+
const dismiss = (
|
|
42
|
+
<HeaderDismissButton dismissAction={dismissAction} navigation={navigation} onDismiss={onDismiss} />
|
|
43
|
+
);
|
|
44
|
+
return (
|
|
45
|
+
<View style={[styles.bar, styles.windowInsets(windowInsets.left, windowInsets.right)]}>
|
|
46
|
+
<HeaderSurface color={color} animatedStyle={surfaceStyle} />
|
|
47
|
+
<HeaderWindowInsets onInsetsChange={setWindowInsets} />
|
|
48
|
+
{(leading != null || dismissAction === "back") && (
|
|
49
|
+
<Toolbar>
|
|
50
|
+
{dismissAction === "back" && dismiss}
|
|
51
|
+
{leading}
|
|
52
|
+
</Toolbar>
|
|
53
|
+
)}
|
|
54
|
+
<Animated.View
|
|
55
|
+
style={[
|
|
56
|
+
styles.titleContainer,
|
|
57
|
+
styles.titleInsets(windowInsets.left, windowInsets.right, titleAlignment),
|
|
58
|
+
titleStyle,
|
|
59
|
+
]}
|
|
60
|
+
pointerEvents={isCustomTitle(title) ? "box-none" : "none"}
|
|
61
|
+
>
|
|
62
|
+
{isTextTitle(title)
|
|
63
|
+
? String(title).trim().length > 0 && (
|
|
64
|
+
<Text
|
|
65
|
+
style={styles.title}
|
|
66
|
+
maxFontSizeMultiplier={theme.typography.variants["title-md"].maxFontSizeMultiplier}
|
|
67
|
+
>
|
|
68
|
+
{title}
|
|
69
|
+
</Text>
|
|
70
|
+
)
|
|
71
|
+
: title}
|
|
72
|
+
</Animated.View>
|
|
73
|
+
{(trailing != null || dismissAction === "close") && (
|
|
74
|
+
<Toolbar style={styles.trailing}>
|
|
75
|
+
{dismissAction === "close" && dismiss}
|
|
76
|
+
{trailing}
|
|
77
|
+
</Toolbar>
|
|
78
|
+
)}
|
|
79
|
+
</View>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const styles = StyleSheet.create((theme, runtime) => ({
|
|
84
|
+
windowInsets: (left: number, right: number) => ({
|
|
85
|
+
paddingLeft: Math.max(resolveScreenPadding(theme, runtime.breakpoint).x, left),
|
|
86
|
+
paddingRight: Math.max(resolveScreenPadding(theme, runtime.breakpoint).x, right),
|
|
87
|
+
}),
|
|
88
|
+
titleInsets: (left: number, right: number, alignment: "center" | "left") =>
|
|
89
|
+
alignment === "center" ? { left, right } : {},
|
|
90
|
+
bar: {
|
|
91
|
+
height: theme.layout.header.height,
|
|
92
|
+
flexDirection: "row",
|
|
93
|
+
alignItems: "center",
|
|
94
|
+
paddingHorizontal: theme.layout.screen.padding.x,
|
|
95
|
+
gap: theme.spacing["3"],
|
|
96
|
+
justifyContent: "space-between",
|
|
97
|
+
},
|
|
98
|
+
// A centered title is absolutely positioned, so it leaves the flex row. With
|
|
99
|
+
// no leading slot the trailing toolbar would be the only in-flow child and
|
|
100
|
+
// `space-between` would park it on the left — the auto margin pins it right.
|
|
101
|
+
trailing: {
|
|
102
|
+
justifyContent: "flex-end",
|
|
103
|
+
variants: {
|
|
104
|
+
titleAlignment: {
|
|
105
|
+
center: { marginLeft: "auto" },
|
|
106
|
+
left: {},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
titleContainer: {
|
|
111
|
+
variants: {
|
|
112
|
+
titleAlignment: {
|
|
113
|
+
center: {
|
|
114
|
+
position: "absolute",
|
|
115
|
+
left: 0,
|
|
116
|
+
right: 0,
|
|
117
|
+
alignItems: "center",
|
|
118
|
+
},
|
|
119
|
+
left: {
|
|
120
|
+
flex: 1,
|
|
121
|
+
alignItems: "flex-start",
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
title: {
|
|
127
|
+
fontSize: theme.typography.variants["title-md"].fontSize,
|
|
128
|
+
lineHeight: theme.typography.variants["title-md"].lineHeight,
|
|
129
|
+
letterSpacing: theme.typography.variants["title-md"].letterSpacing,
|
|
130
|
+
fontWeight: FONT_WEIGHT_VALUE[theme.typography.variants["title-md"].fontWeight],
|
|
131
|
+
color: (theme.color as unknown as Record<string, string | undefined>)["text.primary"] ?? "",
|
|
132
|
+
variants: {
|
|
133
|
+
titleAlignment: {
|
|
134
|
+
center: { textAlign: "center" },
|
|
135
|
+
left: { textAlign: "left" },
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
}));
|
|
@@ -12,6 +12,8 @@ export const HEADER_FADE_END_OPACITY = 0.85;
|
|
|
12
12
|
export interface HeaderGradientOverlayProps {
|
|
13
13
|
/** Fill color, ramped from opaque to {@link HEADER_FADE_END_OPACITY}. */
|
|
14
14
|
color: string;
|
|
15
|
+
startOpacity?: number;
|
|
16
|
+
endOpacity?: number;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -21,15 +23,19 @@ export interface HeaderGradientOverlayProps {
|
|
|
21
23
|
* the surface nor reserves a band inside it, so callers size the surface and
|
|
22
24
|
* the ramp simply spans whatever they gave it.
|
|
23
25
|
*/
|
|
24
|
-
export function HeaderGradientOverlay({
|
|
26
|
+
export function HeaderGradientOverlay({
|
|
27
|
+
color,
|
|
28
|
+
startOpacity = 1,
|
|
29
|
+
endOpacity = HEADER_FADE_END_OPACITY,
|
|
30
|
+
}: HeaderGradientOverlayProps) {
|
|
25
31
|
const gradientId = useId();
|
|
26
32
|
|
|
27
33
|
return (
|
|
28
34
|
<Svg height="100%" width="100%">
|
|
29
35
|
<Defs>
|
|
30
36
|
<SvgLinearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
|
|
31
|
-
<Stop offset={0} stopColor={color} stopOpacity={
|
|
32
|
-
<Stop offset={1} stopColor={color} stopOpacity={
|
|
37
|
+
<Stop offset={0} stopColor={color} stopOpacity={startOpacity} />
|
|
38
|
+
<Stop offset={1} stopColor={color} stopOpacity={endOpacity} />
|
|
33
39
|
</SvgLinearGradient>
|
|
34
40
|
</Defs>
|
|
35
41
|
<Rect width="100%" height="100%" fill={`url(#${gradientId})`} />
|
|
@@ -1,35 +1,33 @@
|
|
|
1
|
-
import { View } from "react-native";
|
|
2
|
-
|
|
3
1
|
import Animated, { type useAnimatedStyle } from "react-native-reanimated";
|
|
4
2
|
import { StyleSheet } from "react-native-unistyles";
|
|
5
3
|
|
|
6
|
-
import { HeaderGradientOverlay } from "./HeaderGradientOverlay";
|
|
4
|
+
import { HEADER_FADE_END_OPACITY, HeaderGradientOverlay } from "./HeaderGradientOverlay";
|
|
7
5
|
|
|
8
6
|
type HeaderOpacityStyle = ReturnType<typeof useAnimatedStyle<{ opacity: number }>>;
|
|
9
7
|
|
|
10
8
|
export interface HeaderSurfaceProps {
|
|
11
|
-
/** Fill color, ramped toward the surface's bottom edge
|
|
9
|
+
/** Fill color, ramped toward the surface's bottom edge. */
|
|
12
10
|
color: string;
|
|
13
11
|
/** Opacity style for variants that fade the whole surface in as they collapse. */
|
|
14
12
|
animatedStyle?: HeaderOpacityStyle;
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
* outright, such as a sticky accessory resting over the scroll content.
|
|
18
|
-
*/
|
|
19
|
-
solid?: boolean;
|
|
13
|
+
/** Continue the bar's gradient from 0.85 to 0.80 beneath an accessory. */
|
|
14
|
+
accessory?: boolean;
|
|
20
15
|
}
|
|
21
16
|
|
|
22
17
|
/**
|
|
23
|
-
* Header background
|
|
24
|
-
* content scrolling under the header meets a single boundary rather than two.
|
|
18
|
+
* Header background with matching gradient boundaries for the bar and accessory.
|
|
25
19
|
*
|
|
26
20
|
* The ramp spans the surface itself: it is sized by the caller and stays inside
|
|
27
21
|
* those bounds, so a taller surface stretches the fade rather than adding to it.
|
|
28
22
|
*/
|
|
29
|
-
export function HeaderSurface({ color, animatedStyle,
|
|
23
|
+
export function HeaderSurface({ color, animatedStyle, accessory }: HeaderSurfaceProps) {
|
|
30
24
|
return (
|
|
31
25
|
<Animated.View style={[styles.surface, animatedStyle]} pointerEvents="none">
|
|
32
|
-
|
|
26
|
+
<HeaderGradientOverlay
|
|
27
|
+
color={color}
|
|
28
|
+
startOpacity={accessory ? HEADER_FADE_END_OPACITY : 1}
|
|
29
|
+
endOpacity={accessory ? 0.8 : HEADER_FADE_END_OPACITY}
|
|
30
|
+
/>
|
|
33
31
|
</Animated.View>
|
|
34
32
|
);
|
|
35
33
|
}
|
|
@@ -42,8 +40,4 @@ const styles = StyleSheet.create(() => ({
|
|
|
42
40
|
right: 0,
|
|
43
41
|
bottom: 0,
|
|
44
42
|
},
|
|
45
|
-
fill: (color: string) => ({
|
|
46
|
-
flex: 1,
|
|
47
|
-
backgroundColor: color,
|
|
48
|
-
}),
|
|
49
43
|
}));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Platform } from "react-native";
|
|
2
|
+
|
|
3
|
+
import { StyleSheet } from "react-native-unistyles";
|
|
4
|
+
|
|
5
|
+
import TerraHeaderInsets from "../../../specs/TerraHeaderInsetsNativeComponent";
|
|
6
|
+
import type { HeaderWindowInsetsProps } from "./HeaderWindowInsets";
|
|
7
|
+
|
|
8
|
+
export function HeaderWindowInsets({ onInsetsChange }: HeaderWindowInsetsProps) {
|
|
9
|
+
if (Platform.OS !== "ios" || !Platform.isPad || Number.parseInt(String(Platform.Version), 10) < 26)
|
|
10
|
+
return null;
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<TerraHeaderInsets
|
|
14
|
+
style={styles.probe}
|
|
15
|
+
pointerEvents="none"
|
|
16
|
+
accessible={false}
|
|
17
|
+
onInsetsChange={({ nativeEvent }) => onInsetsChange(nativeEvent)}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const styles = StyleSheet.create(() => ({
|
|
23
|
+
probe: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0 },
|
|
24
|
+
}));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface HeaderWindowInsetsProps {
|
|
2
|
+
onInsetsChange: (insets: { left: number; right: number }) => void;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/** Only iPadOS needs the native window-control measurement. */
|
|
6
|
+
export function HeaderWindowInsets(_: HeaderWindowInsetsProps) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|