react-native-terra-ui 0.11.11 → 0.11.12
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 +10 -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 +122 -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/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/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/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/package.json +1 -1
- 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 +125 -0
- package/src/components/header/parts/HeaderGradientOverlay.tsx +9 -3
- package/src/components/header/parts/HeaderSurface.tsx +11 -17
- 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
|
@@ -1,28 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { type LayoutChangeEvent, Text, View } from "react-native";
|
|
1
|
+
import { View } from "react-native";
|
|
3
2
|
|
|
4
|
-
import
|
|
5
|
-
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|
3
|
+
import { StyleSheet } from "react-native-unistyles";
|
|
6
4
|
|
|
7
|
-
import { resolveThemeColor } from "#utils/resolve-theme-color";
|
|
8
|
-
import { FONT_WEIGHT_VALUE } from "#utils/typography";
|
|
9
5
|
import { Portal } from "../../../portal";
|
|
10
6
|
import { useHeaderBackground } from "../../../screen/parts/ScreenHeaderAppearance";
|
|
11
|
-
import { SCREEN_HEADER_PORTAL_HOST
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
7
|
+
import { SCREEN_HEADER_PORTAL_HOST } from "../../../screen/ScreenContext";
|
|
8
|
+
import { useHeaderColor } from "../../hooks/use-header-color";
|
|
9
|
+
import { useLargeTitleHeader } from "../../hooks/use-large-title-header";
|
|
14
10
|
import { HeaderAccessory, HeaderAccessorySpacer } from "../../parts/HeaderAccessory";
|
|
15
|
-
import {
|
|
11
|
+
import { HeaderBar } from "../../parts/HeaderBar";
|
|
16
12
|
import type { HeaderAccessoryBehavior } from "../../types";
|
|
17
|
-
import { isCustomTitle, isTextTitle } from "../../utils";
|
|
18
13
|
import type { TitleHeaderProps } from "../TitleHeader";
|
|
19
|
-
|
|
20
|
-
const AnimatedText = Animated.createAnimatedComponent(Text);
|
|
21
|
-
|
|
22
|
-
/** Compact title offset (px) when the large title is fully expanded. */
|
|
23
|
-
const COMPACT_TITLE_SLIDE_OFFSET = 8;
|
|
24
|
-
/** Large title offset (px) when fully collapsed. */
|
|
25
|
-
const LARGE_TITLE_SLIDE_OFFSET = 6;
|
|
14
|
+
import { LargeTitleContent } from "./parts/LargeTitleContent";
|
|
26
15
|
|
|
27
16
|
export interface LargeTitleHeaderProps extends TitleHeaderProps {
|
|
28
17
|
/** Optional small line shown above the large title. */
|
|
@@ -34,7 +23,7 @@ export interface LargeTitleHeaderProps extends TitleHeaderProps {
|
|
|
34
23
|
/**
|
|
35
24
|
* How `accessory` moves while scrolling. `'sticky'` starts below the large title
|
|
36
25
|
* and pins below the compact bar when scrolled up. A hidden large title always pins.
|
|
37
|
-
* @defaultValue '
|
|
26
|
+
* @defaultValue 'sticky'
|
|
38
27
|
*/
|
|
39
28
|
accessoryBehavior?: HeaderAccessoryBehavior;
|
|
40
29
|
/**
|
|
@@ -44,98 +33,7 @@ export interface LargeTitleHeaderProps extends TitleHeaderProps {
|
|
|
44
33
|
titleAlignment?: "center" | "left";
|
|
45
34
|
}
|
|
46
35
|
|
|
47
|
-
|
|
48
|
-
title,
|
|
49
|
-
overline,
|
|
50
|
-
subtitle,
|
|
51
|
-
hasAccessoryBelow,
|
|
52
|
-
onHeightChange,
|
|
53
|
-
}: Pick<LargeTitleHeaderProps, "title" | "overline" | "subtitle"> & {
|
|
54
|
-
/** Reserve a gap before a sticky accessory below this block. */
|
|
55
|
-
hasAccessoryBelow: boolean;
|
|
56
|
-
/** Reports the large-title block's layout height in dp. */
|
|
57
|
-
onHeightChange?: (height: number) => void;
|
|
58
|
-
}) {
|
|
59
|
-
const { theme } = useUnistyles();
|
|
60
|
-
const { scrollY } = useScreen();
|
|
61
|
-
|
|
62
|
-
const headerCollapseHeight = theme.layout.header.height;
|
|
63
|
-
|
|
64
|
-
const largeTitleAnimatedStyle = useAnimatedStyle(() => {
|
|
65
|
-
const height = headerCollapseHeight;
|
|
66
|
-
if (height <= 0) {
|
|
67
|
-
return { opacity: 1, transform: [{ translateY: 0 }] };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
opacity: interpolate(scrollY.value, [0, height], [1, 0], "clamp"),
|
|
72
|
-
transform: [
|
|
73
|
-
{
|
|
74
|
-
translateY: interpolate(scrollY.value, [0, height], [0, -LARGE_TITLE_SLIDE_OFFSET], "clamp"),
|
|
75
|
-
},
|
|
76
|
-
],
|
|
77
|
-
};
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// Layout height excludes the collapse transform.
|
|
81
|
-
const handleLayout = (event: LayoutChangeEvent) => onHeightChange?.(event.nativeEvent.layout.height);
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<Animated.View
|
|
85
|
-
style={[styles.portalContent(hasAccessoryBelow), largeTitleAnimatedStyle]}
|
|
86
|
-
onLayout={handleLayout}
|
|
87
|
-
>
|
|
88
|
-
{!!overline && (
|
|
89
|
-
<Text
|
|
90
|
-
style={styles.overline}
|
|
91
|
-
maxFontSizeMultiplier={theme.typography.variants["label-sm"].maxFontSizeMultiplier}
|
|
92
|
-
>
|
|
93
|
-
{overline}
|
|
94
|
-
</Text>
|
|
95
|
-
)}
|
|
96
|
-
{isTextTitle(title) ? (
|
|
97
|
-
<Text
|
|
98
|
-
style={styles.largeTitle}
|
|
99
|
-
maxFontSizeMultiplier={theme.typography.variants["headline-lg"].maxFontSizeMultiplier}
|
|
100
|
-
>
|
|
101
|
-
{title}
|
|
102
|
-
</Text>
|
|
103
|
-
) : (
|
|
104
|
-
title
|
|
105
|
-
)}
|
|
106
|
-
{!!subtitle && (
|
|
107
|
-
<Text
|
|
108
|
-
style={styles.subtitle}
|
|
109
|
-
maxFontSizeMultiplier={theme.typography.variants["body-lg"].maxFontSizeMultiplier}
|
|
110
|
-
>
|
|
111
|
-
{subtitle}
|
|
112
|
-
</Text>
|
|
113
|
-
)}
|
|
114
|
-
</Animated.View>
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* iOS-style collapsing large-title header. The large title is injected (via the
|
|
120
|
-
* `Screen`'s portal) into the top of the scroll content, so it scrolls away
|
|
121
|
-
* naturally; the compact title cross-fades over the theme header height.
|
|
122
|
-
* The top safe area and bar overlay fade from transparent when expanded to
|
|
123
|
-
* the header background when collapsed. The large-title content stays transparent.
|
|
124
|
-
* Snapping is opt-in through the scroll container's snapToOffsets.
|
|
125
|
-
*
|
|
126
|
-
* Must be used inside a `Screen` with a `Screen.ScrollView` or `Screen.List`.
|
|
127
|
-
*
|
|
128
|
-
* @example
|
|
129
|
-
* ```tsx
|
|
130
|
-
* <Screen.Header>
|
|
131
|
-
* <Header.LargeTitle
|
|
132
|
-
* overline="Today"
|
|
133
|
-
* title="Meditate"
|
|
134
|
-
* subtitle="Good morning"
|
|
135
|
-
* />
|
|
136
|
-
* </Screen.Header>
|
|
137
|
-
* ```
|
|
138
|
-
*/
|
|
36
|
+
/** Collapsing header. The expanded title lives in scroll content; the bar and accessory overlay it. */
|
|
139
37
|
export function LargeTitleHeader({
|
|
140
38
|
title,
|
|
141
39
|
overline,
|
|
@@ -147,204 +45,61 @@ export function LargeTitleHeader({
|
|
|
147
45
|
accessory,
|
|
148
46
|
accessoryPadding = "x",
|
|
149
47
|
accessoryBehavior = "sticky",
|
|
150
|
-
dismissAction = "none",
|
|
151
|
-
navigation,
|
|
152
|
-
onDismiss,
|
|
153
48
|
titleAlignment = "center",
|
|
154
49
|
isLargeTitleHidden = false,
|
|
155
50
|
background,
|
|
156
51
|
bg,
|
|
52
|
+
...dismissProps
|
|
157
53
|
}: LargeTitleHeaderProps) {
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
(colorToken ? resolveThemeColor(colorToken, theme) : screenBackground) ?? theme.color["surface.canvas"];
|
|
164
|
-
const leadingContent = leadingActions !== undefined ? leadingActions : LeftToolbar;
|
|
165
|
-
const trailingContent = trailingActions !== undefined ? trailingActions : RightToolbar;
|
|
166
|
-
|
|
167
|
-
const isSticky = accessoryBehavior === "sticky" && !isLargeTitleHidden;
|
|
168
|
-
const hasStickyAccessory = isSticky && accessory != null;
|
|
169
|
-
// Sticky accessories reserve space below the large title; pinned ones add top padding.
|
|
170
|
-
const [stickyAccessoryHeight, setStickyAccessoryHeight] = useState(0);
|
|
171
|
-
const [largeTitleHeight, setLargeTitleHeight] = useState(0);
|
|
172
|
-
const isLargeTitleHiddenValue = useSharedValue(isLargeTitleHidden);
|
|
173
|
-
|
|
174
|
-
const handleAccessoryHeightChange = useCallback(
|
|
175
|
-
(height: number) => {
|
|
176
|
-
if (isSticky) setStickyAccessoryHeight(Math.max(0, Math.round(height)));
|
|
177
|
-
else setHeaderAccessoryHeight(height);
|
|
178
|
-
},
|
|
179
|
-
[isSticky, setHeaderAccessoryHeight]
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
const handleLargeTitleHeightChange = useCallback((height: number) => {
|
|
183
|
-
setLargeTitleHeight(Math.max(0, Math.round(height)));
|
|
184
|
-
}, []);
|
|
185
|
-
|
|
186
|
-
// Rest below the title, including the scroll container's top padding.
|
|
187
|
-
const stickyAccessoryOffset = Math.max(
|
|
188
|
-
0,
|
|
189
|
-
contentTopPadding + largeTitleHeight - theme.layout.header.height
|
|
190
|
-
);
|
|
191
|
-
|
|
192
|
-
// Zero translation places the accessory directly below the compact bar.
|
|
193
|
-
const stickyAccessoryStyle = useAnimatedStyle(() => {
|
|
194
|
-
if (isLargeTitleHiddenValue.value) {
|
|
195
|
-
return { transform: [{ translateY: 0 }] };
|
|
196
|
-
}
|
|
197
|
-
return {
|
|
198
|
-
transform: [{ translateY: Math.max(0, stickyAccessoryOffset - scrollY.value) }],
|
|
199
|
-
};
|
|
54
|
+
const color = useHeaderColor(background ?? bg);
|
|
55
|
+
const layout = useLargeTitleHeader({
|
|
56
|
+
hasAccessory: accessory != null,
|
|
57
|
+
accessoryBehavior,
|
|
58
|
+
isLargeTitleHidden,
|
|
200
59
|
});
|
|
201
|
-
|
|
202
|
-
styles.useVariants({ titleAlignment });
|
|
203
|
-
|
|
204
|
-
// Follow the accessory's bottom edge while keeping the background anchored to the bar.
|
|
205
|
-
const stickySurfaceStyle = useAnimatedStyle(() => ({
|
|
206
|
-
height: headerCollapseHeight + stickyAccessoryHeight + Math.max(0, stickyAccessoryOffset - scrollY.value),
|
|
207
|
-
}));
|
|
208
|
-
|
|
209
|
-
// Blend the accessory's resting surface into the shared header background.
|
|
210
|
-
const accessorySurfaceStyle = useAnimatedStyle(() => ({
|
|
211
|
-
opacity:
|
|
212
|
-
stickyAccessoryOffset > 0 ? interpolate(scrollY.value, [0, stickyAccessoryOffset], [1, 0], "clamp") : 0,
|
|
213
|
-
}));
|
|
214
|
-
|
|
215
|
-
// Animate the header surface's opacity: 0 (expanded) → 1 (collapsed).
|
|
216
|
-
const surfaceOpacityStyle = useAnimatedStyle(() => {
|
|
217
|
-
if (isLargeTitleHiddenValue.value) {
|
|
218
|
-
return { opacity: 1 };
|
|
219
|
-
}
|
|
220
|
-
const height = hasStickyAccessory ? stickyAccessoryOffset : headerCollapseHeight;
|
|
221
|
-
if (height <= 0) {
|
|
222
|
-
return { opacity: hasStickyAccessory ? 1 : 0 };
|
|
223
|
-
}
|
|
224
|
-
return {
|
|
225
|
-
opacity: interpolate(scrollY.value, [0, height], [0, 1], "clamp"),
|
|
226
|
-
};
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
useHeaderBackground(bgColor, surfaceOpacityStyle);
|
|
230
|
-
|
|
231
|
-
const compactTitleAnimatedStyle = useAnimatedStyle(() => {
|
|
232
|
-
if (isLargeTitleHiddenValue.value) {
|
|
233
|
-
return { opacity: 1, transform: [{ translateY: 0 }] };
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const height = headerCollapseHeight;
|
|
237
|
-
if (height <= 0) {
|
|
238
|
-
return {
|
|
239
|
-
opacity: 0,
|
|
240
|
-
transform: [{ translateY: COMPACT_TITLE_SLIDE_OFFSET }],
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return {
|
|
245
|
-
opacity: interpolate(scrollY.value, [0, height], [0, 1], "clamp"),
|
|
246
|
-
transform: [
|
|
247
|
-
{
|
|
248
|
-
translateY: interpolate(scrollY.value, [0, height], [COMPACT_TITLE_SLIDE_OFFSET, 0], "clamp"),
|
|
249
|
-
},
|
|
250
|
-
],
|
|
251
|
-
};
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
useEffect(() => {
|
|
255
|
-
isLargeTitleHiddenValue.value = isLargeTitleHidden;
|
|
256
|
-
}, [isLargeTitleHidden, isLargeTitleHiddenValue]);
|
|
257
|
-
|
|
258
|
-
const leading =
|
|
259
|
-
dismissAction === "back" ? (
|
|
260
|
-
<>
|
|
261
|
-
<HeaderDismissButton dismissAction={dismissAction} navigation={navigation} onDismiss={onDismiss} />
|
|
262
|
-
{leadingContent}
|
|
263
|
-
</>
|
|
264
|
-
) : (
|
|
265
|
-
leadingContent
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
const trailing =
|
|
269
|
-
dismissAction === "close" ? (
|
|
270
|
-
<>
|
|
271
|
-
{trailingContent}
|
|
272
|
-
<HeaderDismissButton dismissAction={dismissAction} navigation={navigation} onDismiss={onDismiss} />
|
|
273
|
-
</>
|
|
274
|
-
) : (
|
|
275
|
-
trailingContent
|
|
276
|
-
);
|
|
277
|
-
|
|
60
|
+
useHeaderBackground(color, layout.surfaceStyle);
|
|
278
61
|
return (
|
|
279
|
-
|
|
280
|
-
<View style={styles.container(isSticky ? stickyAccessoryOffset : 0)} pointerEvents="box-none">
|
|
62
|
+
<View style={styles.container(layout.accessoryOffset)} pointerEvents="box-none">
|
|
281
63
|
{!isLargeTitleHidden && (
|
|
282
64
|
<Portal hostName={SCREEN_HEADER_PORTAL_HOST}>
|
|
283
|
-
<
|
|
65
|
+
<LargeTitleContent
|
|
284
66
|
title={title}
|
|
285
67
|
overline={overline}
|
|
286
68
|
subtitle={subtitle}
|
|
287
|
-
hasAccessoryBelow={
|
|
288
|
-
onHeightChange={
|
|
69
|
+
hasAccessoryBelow={layout.isSticky}
|
|
70
|
+
onHeightChange={layout.setLargeTitleHeight}
|
|
71
|
+
animatedStyle={layout.largeTitleStyle}
|
|
289
72
|
/>
|
|
290
|
-
{
|
|
291
|
-
{isSticky && accessory != null && <HeaderAccessorySpacer height={stickyAccessoryHeight} />}
|
|
73
|
+
{layout.isSticky && <HeaderAccessorySpacer height={layout.reservedAccessoryHeight} />}
|
|
292
74
|
</Portal>
|
|
293
75
|
)}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
{
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
>
|
|
315
|
-
{title}
|
|
316
|
-
</AnimatedText>
|
|
317
|
-
) : (
|
|
318
|
-
title
|
|
319
|
-
)}
|
|
320
|
-
</Animated.View>
|
|
321
|
-
{trailing != null && <Toolbar style={styles.trailing}>{trailing}</Toolbar>}
|
|
322
|
-
</View>
|
|
323
|
-
{accessory != null && !isSticky && (
|
|
324
|
-
<HeaderAccessory padding={accessoryPadding} onHeightChange={handleAccessoryHeightChange}>
|
|
325
|
-
{accessory}
|
|
326
|
-
</HeaderAccessory>
|
|
327
|
-
)}
|
|
328
|
-
</View>
|
|
329
|
-
{accessory != null && isSticky && (
|
|
330
|
-
<Animated.View style={[styles.stickyAccessory, stickyAccessoryStyle]}>
|
|
331
|
-
<HeaderSurface
|
|
332
|
-
color={screenBackground ?? theme.color["surface.canvas"]}
|
|
333
|
-
animatedStyle={accessorySurfaceStyle}
|
|
334
|
-
solid
|
|
335
|
-
/>
|
|
336
|
-
<HeaderAccessory padding={accessoryPadding} onHeightChange={handleAccessoryHeightChange}>
|
|
337
|
-
{accessory}
|
|
338
|
-
</HeaderAccessory>
|
|
339
|
-
</Animated.View>
|
|
76
|
+
<HeaderBar
|
|
77
|
+
{...dismissProps}
|
|
78
|
+
title={title}
|
|
79
|
+
titleAlignment={titleAlignment}
|
|
80
|
+
color={color}
|
|
81
|
+
surfaceStyle={layout.surfaceStyle}
|
|
82
|
+
titleStyle={layout.compactTitleStyle}
|
|
83
|
+
leading={leadingActions !== undefined ? leadingActions : LeftToolbar}
|
|
84
|
+
trailing={trailingActions !== undefined ? trailingActions : RightToolbar}
|
|
85
|
+
/>
|
|
86
|
+
{accessory != null && (
|
|
87
|
+
<HeaderAccessory
|
|
88
|
+
padding={accessoryPadding}
|
|
89
|
+
color={color}
|
|
90
|
+
surfaceStyle={layout.surfaceStyle}
|
|
91
|
+
style={layout.accessoryStyle}
|
|
92
|
+
onHeightChange={layout.setAccessoryHeight}
|
|
93
|
+
>
|
|
94
|
+
{accessory}
|
|
95
|
+
</HeaderAccessory>
|
|
340
96
|
)}
|
|
341
97
|
</View>
|
|
342
98
|
);
|
|
343
99
|
}
|
|
344
100
|
|
|
345
|
-
const styles = StyleSheet.create((
|
|
346
|
-
|
|
347
|
-
// Include the translated accessory in the parent's touchable bounds.
|
|
101
|
+
const styles = StyleSheet.create(() => ({
|
|
102
|
+
// Keep the translated accessory inside the overlay's touchable bounds.
|
|
348
103
|
container: (accessoryOffset: number) => ({
|
|
349
104
|
position: "absolute",
|
|
350
105
|
top: 0,
|
|
@@ -353,84 +108,4 @@ const styles = StyleSheet.create((theme) => ({
|
|
|
353
108
|
zIndex: 1000,
|
|
354
109
|
paddingBottom: accessoryOffset,
|
|
355
110
|
}),
|
|
356
|
-
// Normal flow places the accessory directly below the bar.
|
|
357
|
-
stickyAccessory: { position: "relative" },
|
|
358
|
-
// Anchors the absolutely positioned surface over the bar and the accessory both.
|
|
359
|
-
barGroup: { position: "relative" },
|
|
360
|
-
bar: {
|
|
361
|
-
height: theme.layout.header.height,
|
|
362
|
-
flexDirection: "row",
|
|
363
|
-
alignItems: "center",
|
|
364
|
-
paddingHorizontal: theme.layout.screen.padding.x,
|
|
365
|
-
gap: theme.spacing["3"],
|
|
366
|
-
justifyContent: "space-between",
|
|
367
|
-
},
|
|
368
|
-
// A centered title is absolutely positioned, so it leaves the flex row. With
|
|
369
|
-
// no leading slot the trailing toolbar would be the only in-flow child and
|
|
370
|
-
// `space-between` would park it on the left — the auto margin pins it right.
|
|
371
|
-
trailing: {
|
|
372
|
-
justifyContent: "flex-end",
|
|
373
|
-
variants: {
|
|
374
|
-
titleAlignment: {
|
|
375
|
-
center: { marginLeft: "auto" },
|
|
376
|
-
left: {},
|
|
377
|
-
},
|
|
378
|
-
},
|
|
379
|
-
},
|
|
380
|
-
portalContent: (hasAccessoryBelow: boolean) => ({
|
|
381
|
-
gap: 2,
|
|
382
|
-
// Include the gap in the measured height used to position the accessory.
|
|
383
|
-
paddingBottom: hasAccessoryBelow ? theme.spacing["2"] : 0,
|
|
384
|
-
paddingHorizontal: theme.layout.screen.padding.x,
|
|
385
|
-
}),
|
|
386
|
-
overline: {
|
|
387
|
-
fontSize: theme.typography.variants["label-sm"].fontSize,
|
|
388
|
-
lineHeight: theme.typography.variants["label-sm"].lineHeight,
|
|
389
|
-
letterSpacing: theme.typography.variants["label-sm"].letterSpacing,
|
|
390
|
-
fontWeight: FONT_WEIGHT_VALUE[theme.typography.variants["label-sm"].fontWeight],
|
|
391
|
-
color: (theme.color as unknown as Record<string, string | undefined>)["text.tertiary"] ?? "",
|
|
392
|
-
},
|
|
393
|
-
largeTitle: {
|
|
394
|
-
fontSize: theme.typography.variants["headline-lg"].fontSize,
|
|
395
|
-
lineHeight: theme.typography.variants["headline-lg"].lineHeight,
|
|
396
|
-
letterSpacing: theme.typography.variants["headline-lg"].letterSpacing,
|
|
397
|
-
fontWeight: FONT_WEIGHT_VALUE[theme.typography.variants["headline-lg"].fontWeight],
|
|
398
|
-
color: (theme.color as unknown as Record<string, string | undefined>)["text.primary"] ?? "",
|
|
399
|
-
},
|
|
400
|
-
subtitle: {
|
|
401
|
-
fontSize: theme.typography.variants["body-lg"].fontSize,
|
|
402
|
-
lineHeight: theme.typography.variants["body-lg"].lineHeight,
|
|
403
|
-
letterSpacing: theme.typography.variants["body-lg"].letterSpacing,
|
|
404
|
-
color: (theme.color as unknown as Record<string, string | undefined>)["text.tertiary"] ?? "",
|
|
405
|
-
},
|
|
406
|
-
compactTitle: {
|
|
407
|
-
fontSize: theme.typography.variants["title-md"].fontSize,
|
|
408
|
-
lineHeight: theme.typography.variants["title-md"].lineHeight,
|
|
409
|
-
letterSpacing: theme.typography.variants["title-md"].letterSpacing,
|
|
410
|
-
fontWeight: FONT_WEIGHT_VALUE[theme.typography.variants["title-md"].fontWeight],
|
|
411
|
-
color: (theme.color as unknown as Record<string, string | undefined>)["text.primary"] ?? "",
|
|
412
|
-
variants: {
|
|
413
|
-
titleAlignment: {
|
|
414
|
-
center: { textAlign: "center" },
|
|
415
|
-
left: { textAlign: "left" },
|
|
416
|
-
},
|
|
417
|
-
},
|
|
418
|
-
},
|
|
419
|
-
titleContainer: {
|
|
420
|
-
flex: 1,
|
|
421
|
-
variants: {
|
|
422
|
-
titleAlignment: {
|
|
423
|
-
center: {
|
|
424
|
-
position: "absolute",
|
|
425
|
-
left: 0,
|
|
426
|
-
right: 0,
|
|
427
|
-
alignItems: "center",
|
|
428
|
-
},
|
|
429
|
-
left: {
|
|
430
|
-
flex: 1,
|
|
431
|
-
alignItems: "flex-start",
|
|
432
|
-
},
|
|
433
|
-
},
|
|
434
|
-
},
|
|
435
|
-
},
|
|
436
111
|
}));
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import { type LayoutChangeEvent, Text } 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 { isTextTitle } from "../../../utils";
|
|
9
|
+
import type { LargeTitleHeaderProps } from "..";
|
|
10
|
+
|
|
11
|
+
export function LargeTitleContent({
|
|
12
|
+
title,
|
|
13
|
+
overline,
|
|
14
|
+
subtitle,
|
|
15
|
+
hasAccessoryBelow,
|
|
16
|
+
onHeightChange,
|
|
17
|
+
animatedStyle,
|
|
18
|
+
}: Pick<LargeTitleHeaderProps, "title" | "overline" | "subtitle"> & {
|
|
19
|
+
/** Reserve a gap before a sticky accessory below this block. */
|
|
20
|
+
hasAccessoryBelow: boolean;
|
|
21
|
+
/** Reports the large-title block's layout height in dp. */
|
|
22
|
+
onHeightChange?: (height: number) => void;
|
|
23
|
+
animatedStyle: ComponentProps<typeof Animated.View>["style"];
|
|
24
|
+
}) {
|
|
25
|
+
const { theme } = useUnistyles();
|
|
26
|
+
// Layout height excludes the collapse transform.
|
|
27
|
+
const handleLayout = (event: LayoutChangeEvent) => onHeightChange?.(event.nativeEvent.layout.height);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Animated.View style={[styles.portalContent(hasAccessoryBelow), animatedStyle]} onLayout={handleLayout}>
|
|
31
|
+
{!!overline && (
|
|
32
|
+
<Text
|
|
33
|
+
style={styles.overline}
|
|
34
|
+
maxFontSizeMultiplier={theme.typography.variants["label-sm"].maxFontSizeMultiplier}
|
|
35
|
+
>
|
|
36
|
+
{overline}
|
|
37
|
+
</Text>
|
|
38
|
+
)}
|
|
39
|
+
{isTextTitle(title) ? (
|
|
40
|
+
<Text
|
|
41
|
+
style={styles.largeTitle}
|
|
42
|
+
maxFontSizeMultiplier={theme.typography.variants["headline-lg"].maxFontSizeMultiplier}
|
|
43
|
+
>
|
|
44
|
+
{title}
|
|
45
|
+
</Text>
|
|
46
|
+
) : (
|
|
47
|
+
title
|
|
48
|
+
)}
|
|
49
|
+
{!!subtitle && (
|
|
50
|
+
<Text
|
|
51
|
+
style={styles.subtitle}
|
|
52
|
+
maxFontSizeMultiplier={theme.typography.variants["body-lg"].maxFontSizeMultiplier}
|
|
53
|
+
>
|
|
54
|
+
{subtitle}
|
|
55
|
+
</Text>
|
|
56
|
+
)}
|
|
57
|
+
</Animated.View>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const styles = StyleSheet.create((theme) => ({
|
|
62
|
+
portalContent: (hasAccessoryBelow: boolean) => ({
|
|
63
|
+
gap: theme.spacing["1"] / 2,
|
|
64
|
+
// Include the gap in the measured height used to position the accessory.
|
|
65
|
+
paddingBottom: hasAccessoryBelow ? theme.spacing["2"] : 0,
|
|
66
|
+
paddingHorizontal: theme.layout.screen.padding.x,
|
|
67
|
+
}),
|
|
68
|
+
overline: {
|
|
69
|
+
fontSize: theme.typography.variants["label-sm"].fontSize,
|
|
70
|
+
lineHeight: theme.typography.variants["label-sm"].lineHeight,
|
|
71
|
+
letterSpacing: theme.typography.variants["label-sm"].letterSpacing,
|
|
72
|
+
fontWeight: FONT_WEIGHT_VALUE[theme.typography.variants["label-sm"].fontWeight],
|
|
73
|
+
color: (theme.color as unknown as Record<string, string | undefined>)["text.tertiary"] ?? "",
|
|
74
|
+
},
|
|
75
|
+
largeTitle: {
|
|
76
|
+
fontSize: theme.typography.variants["headline-lg"].fontSize,
|
|
77
|
+
lineHeight: theme.typography.variants["headline-lg"].lineHeight,
|
|
78
|
+
letterSpacing: theme.typography.variants["headline-lg"].letterSpacing,
|
|
79
|
+
fontWeight: FONT_WEIGHT_VALUE[theme.typography.variants["headline-lg"].fontWeight],
|
|
80
|
+
color: (theme.color as unknown as Record<string, string | undefined>)["text.primary"] ?? "",
|
|
81
|
+
},
|
|
82
|
+
subtitle: {
|
|
83
|
+
fontSize: theme.typography.variants["body-lg"].fontSize,
|
|
84
|
+
lineHeight: theme.typography.variants["body-lg"].lineHeight,
|
|
85
|
+
letterSpacing: theme.typography.variants["body-lg"].letterSpacing,
|
|
86
|
+
color: (theme.color as unknown as Record<string, string | undefined>)["text.tertiary"] ?? "",
|
|
87
|
+
},
|
|
88
|
+
}));
|