@studio-piot/native-ui 0.2.5 → 0.16.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/README.md +22 -18
- package/dist/components/Button/Button.d.ts +20 -4
- package/dist/components/Button/Button.d.ts.map +1 -1
- package/dist/components/Button/Button.js +34 -21
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Button/buttonAppearance.d.ts +20 -0
- package/dist/components/Button/buttonAppearance.d.ts.map +1 -0
- package/dist/components/Button/buttonAppearance.js +28 -0
- package/dist/components/Button/buttonAppearance.js.map +1 -0
- package/dist/components/Button/buttonThemeRoles.d.ts +6 -0
- package/dist/components/Button/buttonThemeRoles.d.ts.map +1 -0
- package/dist/components/Button/buttonThemeRoles.js +8 -0
- package/dist/components/Button/buttonThemeRoles.js.map +1 -0
- package/dist/components/Button/index.d.ts +3 -1
- package/dist/components/Button/index.d.ts.map +1 -1
- package/dist/components/Button/index.js +4 -1
- package/dist/components/Button/index.js.map +1 -1
- package/dist/components/Choice/Choice.d.ts +8 -0
- package/dist/components/Choice/Choice.d.ts.map +1 -0
- package/dist/components/Choice/Choice.js +60 -0
- package/dist/components/Choice/Choice.js.map +1 -0
- package/dist/components/Choice/choiceLayout.d.ts +7 -0
- package/dist/components/Choice/choiceLayout.d.ts.map +1 -0
- package/dist/components/Choice/choiceLayout.js +13 -0
- package/dist/components/Choice/choiceLayout.js.map +1 -0
- package/dist/components/Choice/choiceThemeRoles.d.ts +8 -0
- package/dist/components/Choice/choiceThemeRoles.d.ts.map +1 -0
- package/dist/components/Choice/choiceThemeRoles.js +10 -0
- package/dist/components/Choice/choiceThemeRoles.js.map +1 -0
- package/dist/components/Choice/index.d.ts +5 -0
- package/dist/components/Choice/index.d.ts.map +1 -0
- package/dist/components/Choice/index.js +10 -0
- package/dist/components/Choice/index.js.map +1 -0
- package/dist/components/Choice/types.d.ts +26 -0
- package/dist/components/Choice/types.d.ts.map +1 -0
- package/dist/components/Choice/types.js +3 -0
- package/dist/components/Choice/types.js.map +1 -0
- package/dist/components/ErrorBanner/ErrorBanner.d.ts +1 -1
- package/dist/components/ErrorBanner/ErrorBanner.d.ts.map +1 -1
- package/dist/components/ErrorBanner/ErrorBanner.js +19 -19
- package/dist/components/ErrorBanner/ErrorBanner.js.map +1 -1
- package/dist/components/ErrorBanner/errorBannerContext.d.ts +23 -3
- package/dist/components/ErrorBanner/errorBannerContext.d.ts.map +1 -1
- package/dist/components/ErrorBanner/errorBannerContext.js +9 -5
- package/dist/components/ErrorBanner/errorBannerContext.js.map +1 -1
- package/dist/components/ErrorBanner/errorBannerLayout.d.ts +18 -0
- package/dist/components/ErrorBanner/errorBannerLayout.d.ts.map +1 -0
- package/dist/components/ErrorBanner/errorBannerLayout.js +54 -0
- package/dist/components/ErrorBanner/errorBannerLayout.js.map +1 -0
- package/dist/components/ErrorBanner/icons.d.ts +7 -5
- package/dist/components/ErrorBanner/icons.d.ts.map +1 -1
- package/dist/components/ErrorBanner/icons.js +8 -4
- package/dist/components/ErrorBanner/icons.js.map +1 -1
- package/dist/components/ErrorBanner/index.d.ts +1 -1
- package/dist/components/ErrorBanner/index.d.ts.map +1 -1
- package/dist/components/ErrorBanner/index.js.map +1 -1
- package/dist/components/Form/Form.d.ts +9 -1
- package/dist/components/Form/Form.d.ts.map +1 -1
- package/dist/components/Form/Form.js +56 -78
- package/dist/components/Form/Form.js.map +1 -1
- package/dist/components/Form/FormCloseButton.d.ts +3 -1
- package/dist/components/Form/FormCloseButton.d.ts.map +1 -1
- package/dist/components/Form/FormCloseButton.js +5 -4
- package/dist/components/Form/FormCloseButton.js.map +1 -1
- package/dist/components/Form/FormErrorBanner.d.ts +15 -0
- package/dist/components/Form/FormErrorBanner.d.ts.map +1 -0
- package/dist/components/Form/FormErrorBanner.js +30 -0
- package/dist/components/Form/FormErrorBanner.js.map +1 -0
- package/dist/components/Form/FormSubmitFooter.d.ts +17 -0
- package/dist/components/Form/FormSubmitFooter.d.ts.map +1 -0
- package/dist/components/Form/FormSubmitFooter.js +32 -0
- package/dist/components/Form/FormSubmitFooter.js.map +1 -0
- package/dist/components/Form/formThemeRoles.d.ts +7 -0
- package/dist/components/Form/formThemeRoles.d.ts.map +1 -0
- package/dist/components/Form/formThemeRoles.js +9 -0
- package/dist/components/Form/formThemeRoles.js.map +1 -0
- package/dist/components/Form/index.d.ts +1 -0
- package/dist/components/Form/index.d.ts.map +1 -1
- package/dist/components/Form/index.js +3 -1
- package/dist/components/Form/index.js.map +1 -1
- package/dist/components/Form/types.d.ts +14 -10
- package/dist/components/Form/types.d.ts.map +1 -1
- package/dist/components/FormSheet/FormSheet.d.ts +10 -0
- package/dist/components/FormSheet/FormSheet.d.ts.map +1 -0
- package/dist/components/FormSheet/FormSheet.js +153 -0
- package/dist/components/FormSheet/FormSheet.js.map +1 -0
- package/dist/components/FormSheet/formSheetLayout.d.ts +32 -0
- package/dist/components/FormSheet/formSheetLayout.d.ts.map +1 -0
- package/dist/components/FormSheet/formSheetLayout.js +43 -0
- package/dist/components/FormSheet/formSheetLayout.js.map +1 -0
- package/dist/components/FormSheet/formSheetThemeRoles.d.ts +8 -0
- package/dist/components/FormSheet/formSheetThemeRoles.d.ts.map +1 -0
- package/dist/components/FormSheet/formSheetThemeRoles.js +10 -0
- package/dist/components/FormSheet/formSheetThemeRoles.js.map +1 -0
- package/dist/components/FormSheet/index.d.ts +5 -0
- package/dist/components/FormSheet/index.d.ts.map +1 -0
- package/dist/components/FormSheet/index.js +10 -0
- package/dist/components/FormSheet/index.js.map +1 -0
- package/dist/components/FormSheet/types.d.ts +64 -0
- package/dist/components/FormSheet/types.d.ts.map +1 -0
- package/dist/components/FormSheet/types.js +3 -0
- package/dist/components/FormSheet/types.js.map +1 -0
- package/dist/components/HeaderButton/HeaderCapsule.d.ts +4 -0
- package/dist/components/HeaderButton/HeaderCapsule.d.ts.map +1 -0
- package/dist/components/HeaderButton/HeaderCapsule.js +32 -0
- package/dist/components/HeaderButton/HeaderCapsule.js.map +1 -0
- package/dist/components/HeaderButton/HeaderGroup.d.ts +4 -0
- package/dist/components/HeaderButton/HeaderGroup.d.ts.map +1 -0
- package/dist/components/HeaderButton/HeaderGroup.js +23 -0
- package/dist/components/HeaderButton/HeaderGroup.js.map +1 -0
- package/dist/components/HeaderButton/HeaderIcon.d.ts +4 -0
- package/dist/components/HeaderButton/HeaderIcon.d.ts.map +1 -0
- package/dist/components/HeaderButton/HeaderIcon.js +28 -0
- package/dist/components/HeaderButton/HeaderIcon.js.map +1 -0
- package/dist/components/HeaderButton/headerButtonLayout.d.ts +21 -0
- package/dist/components/HeaderButton/headerButtonLayout.d.ts.map +1 -0
- package/dist/components/HeaderButton/headerButtonLayout.js +34 -0
- package/dist/components/HeaderButton/headerButtonLayout.js.map +1 -0
- package/dist/components/HeaderButton/headerButtonThemeRoles.d.ts +5 -0
- package/dist/components/HeaderButton/headerButtonThemeRoles.d.ts.map +1 -0
- package/dist/components/HeaderButton/headerButtonThemeRoles.js +7 -0
- package/dist/components/HeaderButton/headerButtonThemeRoles.js.map +1 -0
- package/dist/components/HeaderButton/index.d.ts +7 -0
- package/dist/components/HeaderButton/index.d.ts.map +1 -0
- package/dist/components/HeaderButton/index.js +14 -0
- package/dist/components/HeaderButton/index.js.map +1 -0
- package/dist/components/HeaderButton/types.d.ts +40 -0
- package/dist/components/HeaderButton/types.d.ts.map +1 -0
- package/dist/components/HeaderButton/types.js +3 -0
- package/dist/components/HeaderButton/types.js.map +1 -0
- package/dist/components/HeaderWash/HeaderWash.d.ts +10 -0
- package/dist/components/HeaderWash/HeaderWash.d.ts.map +1 -0
- package/dist/components/HeaderWash/HeaderWash.js +19 -0
- package/dist/components/HeaderWash/HeaderWash.js.map +1 -0
- package/dist/components/HeaderWash/headerWashGradient.d.ts +8 -0
- package/dist/components/HeaderWash/headerWashGradient.d.ts.map +1 -0
- package/dist/components/HeaderWash/headerWashGradient.js +21 -0
- package/dist/components/HeaderWash/headerWashGradient.js.map +1 -0
- package/dist/components/HeaderWash/index.d.ts +2 -0
- package/dist/components/HeaderWash/index.d.ts.map +1 -0
- package/dist/components/HeaderWash/index.js +6 -0
- package/dist/components/HeaderWash/index.js.map +1 -0
- package/dist/components/Page/Page.d.ts +13 -0
- package/dist/components/Page/Page.d.ts.map +1 -0
- package/dist/components/Page/Page.js +49 -0
- package/dist/components/Page/Page.js.map +1 -0
- package/dist/components/Page/PageBleed.d.ts +9 -0
- package/dist/components/Page/PageBleed.d.ts.map +1 -0
- package/dist/components/Page/PageBleed.js +12 -0
- package/dist/components/Page/PageBleed.js.map +1 -0
- package/dist/components/Page/index.d.ts +5 -0
- package/dist/components/Page/index.d.ts.map +1 -0
- package/dist/components/Page/index.js +10 -0
- package/dist/components/Page/index.js.map +1 -0
- package/dist/components/Page/pageContext.d.ts +4 -0
- package/dist/components/Page/pageContext.d.ts.map +1 -0
- package/dist/components/Page/pageContext.js +11 -0
- package/dist/components/Page/pageContext.js.map +1 -0
- package/dist/components/Page/pageInsets.d.ts +26 -0
- package/dist/components/Page/pageInsets.d.ts.map +1 -0
- package/dist/components/Page/pageInsets.js +37 -0
- package/dist/components/Page/pageInsets.js.map +1 -0
- package/dist/components/Page/pageLayout.d.ts +5 -0
- package/dist/components/Page/pageLayout.d.ts.map +1 -0
- package/dist/components/Page/pageLayout.js +8 -0
- package/dist/components/Page/pageLayout.js.map +1 -0
- package/dist/components/Page/pageThemeRoles.d.ts +5 -0
- package/dist/components/Page/pageThemeRoles.d.ts.map +1 -0
- package/dist/components/Page/pageThemeRoles.js +7 -0
- package/dist/components/Page/pageThemeRoles.js.map +1 -0
- package/dist/components/Page/types.d.ts +22 -0
- package/dist/components/Page/types.d.ts.map +1 -0
- package/dist/components/Page/types.js +3 -0
- package/dist/components/Page/types.js.map +1 -0
- package/dist/components/Select/Select.d.ts +1 -1
- package/dist/components/Select/Select.d.ts.map +1 -1
- package/dist/components/Select/Select.js +8 -9
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/Select/selectThemeRoles.d.ts +2 -0
- package/dist/components/Select/selectThemeRoles.d.ts.map +1 -1
- package/dist/components/Select/selectThemeRoles.js +2 -0
- package/dist/components/Select/selectThemeRoles.js.map +1 -1
- package/dist/components/Select/types.d.ts +4 -0
- package/dist/components/Select/types.d.ts.map +1 -1
- package/dist/components/TabAccessoryButton/TabAccessoryButton.d.ts +8 -0
- package/dist/components/TabAccessoryButton/TabAccessoryButton.d.ts.map +1 -0
- package/dist/components/TabAccessoryButton/TabAccessoryButton.js +77 -0
- package/dist/components/TabAccessoryButton/TabAccessoryButton.js.map +1 -0
- package/dist/components/TabAccessoryButton/index.d.ts +5 -0
- package/dist/components/TabAccessoryButton/index.d.ts.map +1 -0
- package/dist/components/TabAccessoryButton/index.js +11 -0
- package/dist/components/TabAccessoryButton/index.js.map +1 -0
- package/dist/components/TabAccessoryButton/tabAccessoryLayout.d.ts +27 -0
- package/dist/components/TabAccessoryButton/tabAccessoryLayout.d.ts.map +1 -0
- package/dist/components/TabAccessoryButton/tabAccessoryLayout.js +38 -0
- package/dist/components/TabAccessoryButton/tabAccessoryLayout.js.map +1 -0
- package/dist/components/TabAccessoryButton/tabAccessoryThemeRoles.d.ts +14 -0
- package/dist/components/TabAccessoryButton/tabAccessoryThemeRoles.d.ts.map +1 -0
- package/dist/components/TabAccessoryButton/tabAccessoryThemeRoles.js +19 -0
- package/dist/components/TabAccessoryButton/tabAccessoryThemeRoles.js.map +1 -0
- package/dist/components/TabAccessoryButton/types.d.ts +27 -0
- package/dist/components/TabAccessoryButton/types.d.ts.map +1 -0
- package/dist/components/TabAccessoryButton/types.js +3 -0
- package/dist/components/TabAccessoryButton/types.js.map +1 -0
- package/dist/components/index.d.ts +9 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +30 -1
- package/dist/components/index.js.map +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -2
- package/dist/index.js.map +1 -1
- package/dist/layout/HeaderInsetProbe.d.ts +15 -0
- package/dist/layout/HeaderInsetProbe.d.ts.map +1 -0
- package/dist/layout/HeaderInsetProbe.js +28 -0
- package/dist/layout/HeaderInsetProbe.js.map +1 -0
- package/dist/layout/headerInset.d.ts +26 -0
- package/dist/layout/headerInset.d.ts.map +1 -0
- package/dist/layout/headerInset.js +29 -0
- package/dist/layout/headerInset.js.map +1 -0
- package/dist/layout/index.d.ts +1 -0
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/layout/index.js +3 -1
- package/dist/layout/index.js.map +1 -1
- package/dist/layout/sheet.d.ts +3 -0
- package/dist/layout/sheet.d.ts.map +1 -0
- package/dist/layout/sheet.js +6 -0
- package/dist/layout/sheet.js.map +1 -0
- package/dist/theme/ThemeProvider.d.ts +4 -1
- package/dist/theme/ThemeProvider.d.ts.map +1 -1
- package/dist/theme/ThemeProvider.js +5 -2
- package/dist/theme/ThemeProvider.js.map +1 -1
- package/dist/theme/index.d.ts +2 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +6 -1
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/resolveThemeSpacing.d.ts +12 -0
- package/dist/theme/resolveThemeSpacing.d.ts.map +1 -0
- package/dist/theme/resolveThemeSpacing.js +14 -0
- package/dist/theme/resolveThemeSpacing.js.map +1 -0
- package/dist/theme/types.d.ts +2 -0
- package/dist/theme/types.d.ts.map +1 -1
- package/dist/theme/useThemeSpacing.d.ts +4 -0
- package/dist/theme/useThemeSpacing.d.ts.map +1 -0
- package/dist/theme/useThemeSpacing.js +11 -0
- package/dist/theme/useThemeSpacing.js.map +1 -0
- package/docs/components/button.md +92 -38
- package/docs/components/choice.md +88 -0
- package/docs/components/error-banner.md +46 -3
- package/docs/components/form-sheet.md +140 -0
- package/docs/components/form.md +80 -45
- package/docs/components/header-button.md +310 -0
- package/docs/components/header-wash.md +40 -0
- package/docs/components/page.md +183 -0
- package/docs/components/select.md +2 -2
- package/docs/components/tab-accessory-button.md +146 -0
- package/docs/specifications/Liquid Glass header buttons in Expo Router.md +327 -0
- package/package.json +1 -1
- package/src/components/Button/Button.tsx +82 -38
- package/src/components/Button/buttonAppearance.test.ts +44 -0
- package/src/components/Button/buttonAppearance.ts +38 -0
- package/src/components/Button/buttonThemeRoles.test.ts +10 -0
- package/src/components/Button/buttonThemeRoles.ts +6 -0
- package/src/components/Button/index.ts +12 -1
- package/src/components/Choice/Choice.tsx +109 -0
- package/src/components/Choice/choiceLayout.test.ts +14 -0
- package/src/components/Choice/choiceLayout.ts +9 -0
- package/src/components/Choice/choiceThemeRoles.test.ts +14 -0
- package/src/components/Choice/choiceThemeRoles.ts +8 -0
- package/src/components/Choice/index.ts +4 -0
- package/src/components/Choice/types.ts +26 -0
- package/src/components/ErrorBanner/ErrorBanner.tsx +32 -22
- package/src/components/ErrorBanner/errorBannerContext.tsx +31 -6
- package/src/components/ErrorBanner/errorBannerLayout.test.ts +49 -0
- package/src/components/ErrorBanner/errorBannerLayout.ts +59 -0
- package/src/components/ErrorBanner/icons.tsx +25 -6
- package/src/components/ErrorBanner/index.ts +1 -0
- package/src/components/Form/Form.tsx +77 -173
- package/src/components/Form/FormCloseButton.tsx +12 -4
- package/src/components/Form/FormErrorBanner.tsx +57 -0
- package/src/components/Form/FormSubmitFooter.tsx +70 -0
- package/src/components/Form/formThemeRoles.test.ts +13 -0
- package/src/components/Form/formThemeRoles.ts +7 -0
- package/src/components/Form/index.ts +1 -0
- package/src/components/Form/types.ts +14 -10
- package/src/components/FormSheet/FormSheet.tsx +279 -0
- package/src/components/FormSheet/formSheetLayout.test.ts +66 -0
- package/src/components/FormSheet/formSheetLayout.ts +55 -0
- package/src/components/FormSheet/formSheetThemeRoles.test.ts +14 -0
- package/src/components/FormSheet/formSheetThemeRoles.ts +8 -0
- package/src/components/FormSheet/index.ts +7 -0
- package/src/components/FormSheet/types.ts +66 -0
- package/src/components/HeaderButton/HeaderCapsule.tsx +63 -0
- package/src/components/HeaderButton/HeaderGroup.tsx +36 -0
- package/src/components/HeaderButton/HeaderIcon.tsx +56 -0
- package/src/components/HeaderButton/headerButtonLayout.test.ts +47 -0
- package/src/components/HeaderButton/headerButtonLayout.ts +36 -0
- package/src/components/HeaderButton/headerButtonThemeRoles.test.ts +9 -0
- package/src/components/HeaderButton/headerButtonThemeRoles.ts +5 -0
- package/src/components/HeaderButton/index.ts +14 -0
- package/src/components/HeaderButton/types.ts +40 -0
- package/src/components/HeaderWash/HeaderWash.tsx +20 -0
- package/src/components/HeaderWash/headerWashGradient.test.ts +22 -0
- package/src/components/{Form/withAlpha.ts → HeaderWash/headerWashGradient.ts} +9 -0
- package/src/components/HeaderWash/index.ts +1 -0
- package/src/components/Page/Page.tsx +80 -0
- package/src/components/Page/PageBleed.tsx +14 -0
- package/src/components/Page/index.ts +4 -0
- package/src/components/Page/pageContext.ts +8 -0
- package/src/components/Page/pageInsets.test.ts +73 -0
- package/src/components/Page/pageInsets.ts +52 -0
- package/src/components/Page/pageThemeRoles.test.ts +9 -0
- package/src/components/Page/pageThemeRoles.ts +5 -0
- package/src/components/Page/types.ts +26 -0
- package/src/components/Select/Select.tsx +11 -4
- package/src/components/Select/selectThemeRoles.test.ts +5 -0
- package/src/components/Select/selectThemeRoles.ts +2 -0
- package/src/components/Select/types.ts +4 -0
- package/src/components/TabAccessoryButton/TabAccessoryButton.tsx +150 -0
- package/src/components/TabAccessoryButton/index.ts +12 -0
- package/src/components/TabAccessoryButton/tabAccessoryLayout.test.ts +42 -0
- package/src/components/TabAccessoryButton/tabAccessoryLayout.ts +43 -0
- package/src/components/TabAccessoryButton/tabAccessoryThemeRoles.test.ts +28 -0
- package/src/components/TabAccessoryButton/tabAccessoryThemeRoles.ts +22 -0
- package/src/components/TabAccessoryButton/types.ts +27 -0
- package/src/components/index.ts +64 -2
- package/src/index.ts +61 -2
- package/src/layout/headerInset.test.ts +60 -0
- package/src/layout/headerInset.ts +41 -0
- package/src/layout/index.ts +1 -0
- package/src/layout/sheet.ts +2 -0
- package/src/theme/ThemeProvider.tsx +9 -1
- package/src/theme/index.ts +6 -0
- package/src/theme/resolveThemeSpacing.test.ts +29 -0
- package/src/theme/resolveThemeSpacing.ts +21 -0
- package/src/theme/types.ts +3 -0
- package/src/theme/useThemeSpacing.ts +8 -0
- package/src/components/Form/withAlpha.test.ts +0 -13
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Platform, Pressable, StyleSheet, Text } from "react-native";
|
|
2
|
+
import { useResolvedFieldColor } from "../Field/useResolvedFieldColor";
|
|
3
|
+
import {
|
|
4
|
+
HEADER_BUTTON_SIZE,
|
|
5
|
+
HEADER_CAPSULE_FONT_SIZE,
|
|
6
|
+
HEADER_CAPSULE_ICON_SIZE,
|
|
7
|
+
HEADER_HIT_SLOP,
|
|
8
|
+
headerFallbackStyle,
|
|
9
|
+
supportsLiquidGlass,
|
|
10
|
+
} from "./headerButtonLayout";
|
|
11
|
+
import { HEADER_BUTTON_THEME_ROLES } from "./headerButtonThemeRoles";
|
|
12
|
+
import type { HeaderCapsuleProps } from "./types";
|
|
13
|
+
|
|
14
|
+
const GLASS = supportsLiquidGlass(Platform.OS, Platform.Version);
|
|
15
|
+
|
|
16
|
+
/** Header button with a label (and optional icon). Wider than tall, so iOS 26+ draws a glass pill. */
|
|
17
|
+
export function HeaderCapsule({
|
|
18
|
+
icon,
|
|
19
|
+
label,
|
|
20
|
+
onPress,
|
|
21
|
+
iconSize = HEADER_CAPSULE_ICON_SIZE,
|
|
22
|
+
fontSize = HEADER_CAPSULE_FONT_SIZE,
|
|
23
|
+
disabled = false,
|
|
24
|
+
color: colorProp,
|
|
25
|
+
fallbackBackgroundColor,
|
|
26
|
+
hitSlop = HEADER_HIT_SLOP,
|
|
27
|
+
style,
|
|
28
|
+
}: HeaderCapsuleProps) {
|
|
29
|
+
const color = useResolvedFieldColor(colorProp, HEADER_BUTTON_THEME_ROLES.color);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Pressable
|
|
33
|
+
onPress={onPress}
|
|
34
|
+
disabled={disabled}
|
|
35
|
+
hitSlop={hitSlop}
|
|
36
|
+
accessibilityRole="button"
|
|
37
|
+
accessibilityLabel={label}
|
|
38
|
+
accessibilityState={{ disabled }}
|
|
39
|
+
style={({ pressed }) => [
|
|
40
|
+
styles.capsule,
|
|
41
|
+
headerFallbackStyle(GLASS, fallbackBackgroundColor),
|
|
42
|
+
{ opacity: disabled ? 0.4 : pressed ? 0.5 : 1 },
|
|
43
|
+
style,
|
|
44
|
+
]}
|
|
45
|
+
>
|
|
46
|
+
{icon?.({ color, size: iconSize })}
|
|
47
|
+
<Text style={[styles.label, { color, fontSize }]}>{label}</Text>
|
|
48
|
+
</Pressable>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const styles = StyleSheet.create({
|
|
53
|
+
capsule: {
|
|
54
|
+
flexDirection: "row",
|
|
55
|
+
alignItems: "center",
|
|
56
|
+
gap: 6,
|
|
57
|
+
height: HEADER_BUTTON_SIZE,
|
|
58
|
+
paddingHorizontal: 12,
|
|
59
|
+
},
|
|
60
|
+
label: {
|
|
61
|
+
fontWeight: "600",
|
|
62
|
+
},
|
|
63
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Platform, StyleSheet, View } from "react-native";
|
|
2
|
+
import {
|
|
3
|
+
HEADER_GROUP_GAP,
|
|
4
|
+
headerFallbackStyle,
|
|
5
|
+
supportsLiquidGlass,
|
|
6
|
+
} from "./headerButtonLayout";
|
|
7
|
+
import type { HeaderGroupProps } from "./types";
|
|
8
|
+
|
|
9
|
+
const GLASS = supportsLiquidGlass(Platform.OS, Platform.Version);
|
|
10
|
+
|
|
11
|
+
/** Row of header buttons that share one glass capsule on iOS 26+. */
|
|
12
|
+
export function HeaderGroup({
|
|
13
|
+
children,
|
|
14
|
+
fallbackBackgroundColor,
|
|
15
|
+
style,
|
|
16
|
+
}: HeaderGroupProps) {
|
|
17
|
+
return (
|
|
18
|
+
<View
|
|
19
|
+
style={[
|
|
20
|
+
styles.row,
|
|
21
|
+
headerFallbackStyle(GLASS, fallbackBackgroundColor),
|
|
22
|
+
style,
|
|
23
|
+
]}
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
</View>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const styles = StyleSheet.create({
|
|
31
|
+
row: {
|
|
32
|
+
flexDirection: "row",
|
|
33
|
+
alignItems: "center",
|
|
34
|
+
gap: HEADER_GROUP_GAP,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Platform, Pressable, StyleSheet } from "react-native";
|
|
2
|
+
import { useResolvedFieldColor } from "../Field/useResolvedFieldColor";
|
|
3
|
+
import {
|
|
4
|
+
HEADER_BUTTON_SIZE,
|
|
5
|
+
HEADER_HIT_SLOP,
|
|
6
|
+
HEADER_ICON_SIZE,
|
|
7
|
+
headerFallbackStyle,
|
|
8
|
+
supportsLiquidGlass,
|
|
9
|
+
} from "./headerButtonLayout";
|
|
10
|
+
import { HEADER_BUTTON_THEME_ROLES } from "./headerButtonThemeRoles";
|
|
11
|
+
import type { HeaderIconProps } from "./types";
|
|
12
|
+
|
|
13
|
+
const GLASS = supportsLiquidGlass(Platform.OS, Platform.Version);
|
|
14
|
+
|
|
15
|
+
/** Icon-only header button. A 36×36 box becomes a glass circle on iOS 26+. */
|
|
16
|
+
export function HeaderIcon({
|
|
17
|
+
icon,
|
|
18
|
+
label,
|
|
19
|
+
onPress,
|
|
20
|
+
size = HEADER_ICON_SIZE,
|
|
21
|
+
disabled = false,
|
|
22
|
+
color: colorProp,
|
|
23
|
+
fallbackBackgroundColor,
|
|
24
|
+
hitSlop = HEADER_HIT_SLOP,
|
|
25
|
+
style,
|
|
26
|
+
}: HeaderIconProps) {
|
|
27
|
+
const color = useResolvedFieldColor(colorProp, HEADER_BUTTON_THEME_ROLES.color);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Pressable
|
|
31
|
+
onPress={onPress}
|
|
32
|
+
disabled={disabled}
|
|
33
|
+
hitSlop={hitSlop}
|
|
34
|
+
accessibilityRole="button"
|
|
35
|
+
accessibilityLabel={label}
|
|
36
|
+
accessibilityState={{ disabled }}
|
|
37
|
+
style={({ pressed }) => [
|
|
38
|
+
styles.box,
|
|
39
|
+
headerFallbackStyle(GLASS, fallbackBackgroundColor),
|
|
40
|
+
{ opacity: disabled ? 0.4 : pressed ? 0.5 : 1 },
|
|
41
|
+
style,
|
|
42
|
+
]}
|
|
43
|
+
>
|
|
44
|
+
{icon({ color, size })}
|
|
45
|
+
</Pressable>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const styles = StyleSheet.create({
|
|
50
|
+
box: {
|
|
51
|
+
width: HEADER_BUTTON_SIZE,
|
|
52
|
+
height: HEADER_BUTTON_SIZE,
|
|
53
|
+
alignItems: "center",
|
|
54
|
+
justifyContent: "center",
|
|
55
|
+
},
|
|
56
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
HEADER_BUTTON_SIZE,
|
|
5
|
+
headerFallbackStyle,
|
|
6
|
+
supportsLiquidGlass,
|
|
7
|
+
} from "./headerButtonLayout.ts";
|
|
8
|
+
|
|
9
|
+
describe("supportsLiquidGlass", () => {
|
|
10
|
+
it("is true on iOS 26 and later", () => {
|
|
11
|
+
assert.equal(supportsLiquidGlass("ios", "26.0"), true);
|
|
12
|
+
assert.equal(supportsLiquidGlass("ios", "26.1.2"), true);
|
|
13
|
+
assert.equal(supportsLiquidGlass("ios", "27"), true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("is false on iOS 18 and earlier", () => {
|
|
17
|
+
assert.equal(supportsLiquidGlass("ios", "18.6"), false);
|
|
18
|
+
assert.equal(supportsLiquidGlass("ios", "17.0"), false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("is false on Android and web whatever the version", () => {
|
|
22
|
+
assert.equal(supportsLiquidGlass("android", 35), false);
|
|
23
|
+
assert.equal(supportsLiquidGlass("web", "26"), false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("is false when the version cannot be parsed", () => {
|
|
27
|
+
assert.equal(supportsLiquidGlass("ios", ""), false);
|
|
28
|
+
assert.equal(supportsLiquidGlass("ios", "unknown"), false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("headerFallbackStyle", () => {
|
|
33
|
+
it("adds nothing on glass platforms, even with a colour", () => {
|
|
34
|
+
assert.equal(headerFallbackStyle(true, "#eee"), undefined);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("adds nothing on other platforms without a colour", () => {
|
|
38
|
+
assert.equal(headerFallbackStyle(false, undefined), undefined);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("draws a pill background on other platforms when a colour is passed", () => {
|
|
42
|
+
assert.deepEqual(headerFallbackStyle(false, "#eee"), {
|
|
43
|
+
backgroundColor: "#eee",
|
|
44
|
+
borderRadius: HEADER_BUTTON_SIZE / 2,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Height of every header button; a square of this size becomes a glass circle. */
|
|
2
|
+
export const HEADER_BUTTON_SIZE = 36;
|
|
3
|
+
export const HEADER_ICON_SIZE = 17;
|
|
4
|
+
export const HEADER_CAPSULE_ICON_SIZE = 15;
|
|
5
|
+
export const HEADER_CAPSULE_FONT_SIZE = 16;
|
|
6
|
+
export const HEADER_GROUP_GAP = 4;
|
|
7
|
+
export const HEADER_HIT_SLOP = 6;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Liquid Glass bar button backgrounds exist on iOS 26 and later only.
|
|
11
|
+
* Pass `Platform.OS` and `Platform.Version`. Pure so it can be unit-tested.
|
|
12
|
+
*/
|
|
13
|
+
export function supportsLiquidGlass(
|
|
14
|
+
os: string,
|
|
15
|
+
version: string | number,
|
|
16
|
+
): boolean {
|
|
17
|
+
if (os !== "ios") {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const major = Number.parseInt(String(version), 10);
|
|
21
|
+
return Number.isFinite(major) && major >= 26;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Background drawn on platforms without glass. On iOS 26+ the system draws
|
|
26
|
+
* the glass, so a background there would give two shapes, one inside the other.
|
|
27
|
+
*/
|
|
28
|
+
export function headerFallbackStyle(
|
|
29
|
+
glass: boolean,
|
|
30
|
+
backgroundColor: string | undefined,
|
|
31
|
+
): { backgroundColor: string; borderRadius: number } | undefined {
|
|
32
|
+
if (glass || backgroundColor == null) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return { backgroundColor, borderRadius: HEADER_BUTTON_SIZE / 2 };
|
|
36
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { HEADER_BUTTON_THEME_ROLES } from "./headerButtonThemeRoles.ts";
|
|
4
|
+
|
|
5
|
+
describe("HEADER_BUTTON_THEME_ROLES", () => {
|
|
6
|
+
it("maps the colour prop to a dedicated header button role", () => {
|
|
7
|
+
assert.equal(HEADER_BUTTON_THEME_ROLES.color, "headerButton");
|
|
8
|
+
});
|
|
9
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { HeaderCapsule } from "./HeaderCapsule";
|
|
2
|
+
export { HeaderGroup } from "./HeaderGroup";
|
|
3
|
+
export { HeaderIcon } from "./HeaderIcon";
|
|
4
|
+
export { supportsLiquidGlass } from "./headerButtonLayout";
|
|
5
|
+
export {
|
|
6
|
+
HEADER_BUTTON_THEME_ROLES,
|
|
7
|
+
type HeaderButtonThemeColorProp,
|
|
8
|
+
} from "./headerButtonThemeRoles";
|
|
9
|
+
export type {
|
|
10
|
+
HeaderButtonIcon,
|
|
11
|
+
HeaderCapsuleProps,
|
|
12
|
+
HeaderGroupProps,
|
|
13
|
+
HeaderIconProps,
|
|
14
|
+
} from "./types";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { StyleProp, ViewStyle } from "react-native";
|
|
3
|
+
|
|
4
|
+
/** Renders the icon with the resolved tint colour and size, e.g. an expo-symbols `SymbolView`. */
|
|
5
|
+
export type HeaderButtonIcon = (props: { color: string; size: number }) => ReactNode;
|
|
6
|
+
|
|
7
|
+
type HeaderButtonSharedProps = {
|
|
8
|
+
onPress: () => void;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
/** Icon and label colour. Defaults to the `headerButton` theme role. */
|
|
11
|
+
color?: string;
|
|
12
|
+
/** Background on Android and iOS 18 and earlier only. iOS 26+ draws glass instead. */
|
|
13
|
+
fallbackBackgroundColor?: string;
|
|
14
|
+
hitSlop?: number;
|
|
15
|
+
style?: StyleProp<ViewStyle>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type HeaderIconProps = HeaderButtonSharedProps & {
|
|
19
|
+
icon: HeaderButtonIcon;
|
|
20
|
+
/** Accessibility label. Required: icon-only buttons have no text for VoiceOver. */
|
|
21
|
+
label: string;
|
|
22
|
+
/** Icon box size. Default 17 (range 15–20). */
|
|
23
|
+
size?: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type HeaderCapsuleProps = HeaderButtonSharedProps & {
|
|
27
|
+
icon?: HeaderButtonIcon;
|
|
28
|
+
label: string;
|
|
29
|
+
/** Default 15, close to the height of the 16pt label. */
|
|
30
|
+
iconSize?: number;
|
|
31
|
+
/** Default 16. */
|
|
32
|
+
fontSize?: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type HeaderGroupProps = {
|
|
36
|
+
children: ReactNode;
|
|
37
|
+
/** Shared background on Android and iOS 18 and earlier only. */
|
|
38
|
+
fallbackBackgroundColor?: string;
|
|
39
|
+
style?: StyleProp<ViewStyle>;
|
|
40
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { StyleSheet, View } from "react-native";
|
|
2
|
+
import { useResolvedFieldColor } from "../Field/useResolvedFieldColor";
|
|
3
|
+
import { FORM_THEME_ROLES } from "../Form/formThemeRoles";
|
|
4
|
+
import { headerWashGradient } from "./headerWashGradient";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Background for a transparent native header: a wash from `color` to clear.
|
|
8
|
+
* Use it as the stack's `headerBackground` with `headerTransparent: true`.
|
|
9
|
+
* The stack sizes it to the header and draws it above the screen content,
|
|
10
|
+
* so content fades as it scrolls under the bar.
|
|
11
|
+
*/
|
|
12
|
+
export function HeaderWash({ color: colorProp }: { color?: string }) {
|
|
13
|
+
const color = useResolvedFieldColor(colorProp, FORM_THEME_ROLES.backgroundColor);
|
|
14
|
+
return (
|
|
15
|
+
<View
|
|
16
|
+
pointerEvents="none"
|
|
17
|
+
style={[StyleSheet.absoluteFill, { experimental_backgroundImage: headerWashGradient(color) }]}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { headerWashGradient, withAlpha } from "./headerWashGradient.ts";
|
|
4
|
+
|
|
5
|
+
describe("headerWashGradient", () => {
|
|
6
|
+
it("runs from the colour, nearly opaque, to clear at the header's bottom", () => {
|
|
7
|
+
assert.equal(
|
|
8
|
+
headerWashGradient("#F9FAFB"),
|
|
9
|
+
"linear-gradient(to bottom, rgba(249, 250, 251, 0.98) 0%, rgba(249, 250, 251, 0.92) 55%, rgba(249, 250, 251, 0) 100%)",
|
|
10
|
+
);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("withAlpha", () => {
|
|
15
|
+
it("converts a hex colour to rgba", () => {
|
|
16
|
+
assert.equal(withAlpha("#F9FAFB", 0.98), "rgba(249, 250, 251, 0.98)");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("expands three-digit hex", () => {
|
|
20
|
+
assert.equal(withAlpha("#abc", 0.5), "rgba(170, 187, 204, 0.5)");
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -7,3 +7,12 @@ export function withAlpha(hex: string, alpha: number): string {
|
|
|
7
7
|
const b = Number.parseInt(full.slice(4, 6), 16);
|
|
8
8
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
9
9
|
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The header wash: `color` nearly opaque at the top of the header, fading to
|
|
13
|
+
* clear at its bottom, so content fades out as it scrolls under the bar.
|
|
14
|
+
* Pure so it can be unit-tested.
|
|
15
|
+
*/
|
|
16
|
+
export function headerWashGradient(color: string): string {
|
|
17
|
+
return `linear-gradient(to bottom, ${withAlpha(color, 0.98)} 0%, ${withAlpha(color, 0.92)} 55%, ${withAlpha(color, 0)} 100%)`;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { HeaderWash } from "./HeaderWash";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Platform, ScrollView, StyleSheet } from "react-native";
|
|
2
|
+
import { useResolvedFieldColor } from "../Field/useResolvedFieldColor";
|
|
3
|
+
import { supportsLiquidGlass } from "../HeaderButton/headerButtonLayout";
|
|
4
|
+
import { TabAccessoryButton } from "../TabAccessoryButton";
|
|
5
|
+
import { tabAccessoryScreenClearance } from "../TabAccessoryButton/tabAccessoryLayout";
|
|
6
|
+
import { useThemeSpacing } from "../../theme/useThemeSpacing";
|
|
7
|
+
import { PageInsetContext } from "./pageContext";
|
|
8
|
+
import { pageContentInset, pagePaddingTop, pageSideInset } from "./pageInsets";
|
|
9
|
+
import { PAGE_THEME_ROLES } from "./pageThemeRoles";
|
|
10
|
+
import type { PageProps } from "./types";
|
|
11
|
+
|
|
12
|
+
const GLASS = supportsLiquidGlass(Platform.OS, Platform.Version);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Scrolling screen body for a native stack inside native tabs. iOS insets
|
|
16
|
+
* the content for the header (including the large title) and the tab bar,
|
|
17
|
+
* so it starts just below the header, scrolls behind it, and clears the
|
|
18
|
+
* tabs. The title belongs to the header: set it in the stack options.
|
|
19
|
+
*
|
|
20
|
+
* The ScrollView must be a direct child of the screen: react-native-screens
|
|
21
|
+
* only links the header (large title, insets) to a direct child, so there is
|
|
22
|
+
* no wrapper view. Render `Page` as the screen's root element.
|
|
23
|
+
*/
|
|
24
|
+
export function Page({
|
|
25
|
+
children,
|
|
26
|
+
tabAccessory,
|
|
27
|
+
backgroundColor: backgroundColorProp,
|
|
28
|
+
padded = true,
|
|
29
|
+
contentUnderHeader,
|
|
30
|
+
contentInset,
|
|
31
|
+
contentContainerStyle,
|
|
32
|
+
style,
|
|
33
|
+
...scrollProps
|
|
34
|
+
}: PageProps) {
|
|
35
|
+
const backgroundColor = useResolvedFieldColor(
|
|
36
|
+
backgroundColorProp,
|
|
37
|
+
PAGE_THEME_ROLES.backgroundColor,
|
|
38
|
+
);
|
|
39
|
+
const spacing = useThemeSpacing();
|
|
40
|
+
const sideInset = pageSideInset(padded, spacing.screen);
|
|
41
|
+
const paddingBottom =
|
|
42
|
+
(padded ? spacing.pageBottom : 0) +
|
|
43
|
+
(tabAccessory ? tabAccessoryScreenClearance(GLASS, spacing.screen) : 0);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
<ScrollView
|
|
48
|
+
{...scrollProps}
|
|
49
|
+
contentInsetAdjustmentBehavior="automatic"
|
|
50
|
+
contentInset={pageContentInset(Platform.OS, contentUnderHeader, contentInset)}
|
|
51
|
+
style={[styles.screen, { backgroundColor }, style]}
|
|
52
|
+
contentContainerStyle={[
|
|
53
|
+
{ paddingHorizontal: sideInset },
|
|
54
|
+
{
|
|
55
|
+
paddingTop: pagePaddingTop({
|
|
56
|
+
padded,
|
|
57
|
+
underHeader: contentUnderHeader != null,
|
|
58
|
+
pageTop: spacing.pageTop,
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
{ paddingBottom },
|
|
62
|
+
contentContainerStyle,
|
|
63
|
+
]}
|
|
64
|
+
>
|
|
65
|
+
<PageInsetContext.Provider value={sideInset}>
|
|
66
|
+
{children}
|
|
67
|
+
</PageInsetContext.Provider>
|
|
68
|
+
</ScrollView>
|
|
69
|
+
{tabAccessory ? (
|
|
70
|
+
<TabAccessoryButton placement="screen" {...tabAccessory} />
|
|
71
|
+
) : null}
|
|
72
|
+
</>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const styles = StyleSheet.create({
|
|
77
|
+
screen: {
|
|
78
|
+
flex: 1,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { View, type StyleProp, type ViewStyle } from "react-native";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { usePageSideInset } from "./pageContext";
|
|
4
|
+
import { pageBleedStyle } from "./pageInsets";
|
|
5
|
+
|
|
6
|
+
export type PageBleedProps = {
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
style?: StyleProp<ViewStyle>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Takes one section of a `Page` out to the screen edges, e.g. a full-width image or map. */
|
|
12
|
+
export function PageBleed({ children, style }: PageBleedProps) {
|
|
13
|
+
return <View style={[pageBleedStyle(usePageSideInset()), style]}>{children}</View>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
pageBleedStyle,
|
|
5
|
+
pageContentInset,
|
|
6
|
+
pagePaddingTop,
|
|
7
|
+
pageSideInset,
|
|
8
|
+
} from "./pageInsets.ts";
|
|
9
|
+
|
|
10
|
+
describe("pageSideInset", () => {
|
|
11
|
+
it("uses the theme screen spacing when padded", () => {
|
|
12
|
+
assert.equal(pageSideInset(true, 16), 16);
|
|
13
|
+
assert.equal(pageSideInset(true, 20), 20);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("is zero when the page is not padded", () => {
|
|
17
|
+
assert.equal(pageSideInset(false, 20), 0);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("pageBleedStyle", () => {
|
|
22
|
+
it("cancels the page side inset exactly", () => {
|
|
23
|
+
assert.deepEqual(pageBleedStyle(20), { marginHorizontal: -20 });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("adds no margin when there is no inset", () => {
|
|
27
|
+
assert.deepEqual(pageBleedStyle(0), { marginHorizontal: 0 });
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("pageContentInset", () => {
|
|
32
|
+
it("leaves the inset alone without contentUnderHeader", () => {
|
|
33
|
+
assert.equal(pageContentInset("ios", undefined, undefined), undefined);
|
|
34
|
+
const own = { top: 4, bottom: 8 };
|
|
35
|
+
assert.equal(pageContentInset("ios", undefined, own), own);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("cancels only the header clearance on iOS", () => {
|
|
39
|
+
assert.deepEqual(pageContentInset("ios", 96, undefined), {
|
|
40
|
+
top: -96,
|
|
41
|
+
left: 0,
|
|
42
|
+
bottom: 0,
|
|
43
|
+
right: 0,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("adds to an inset the app already set", () => {
|
|
48
|
+
assert.deepEqual(pageContentInset("ios", 96, { top: 10, bottom: 20 }), {
|
|
49
|
+
top: -86,
|
|
50
|
+
left: 0,
|
|
51
|
+
bottom: 20,
|
|
52
|
+
right: 0,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("does nothing on Android, where the header never adds an inset", () => {
|
|
57
|
+
assert.equal(pageContentInset("android", 96, undefined), undefined);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("pagePaddingTop", () => {
|
|
62
|
+
it("uses the theme gap under the header", () => {
|
|
63
|
+
assert.equal(pagePaddingTop({ padded: true, underHeader: false, pageTop: 16 }), 16);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("starts at the very top when content goes under the header", () => {
|
|
67
|
+
assert.equal(pagePaddingTop({ padded: true, underHeader: true, pageTop: 16 }), 0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("is zero when not padded", () => {
|
|
71
|
+
assert.equal(pagePaddingTop({ padded: false, underHeader: false, pageTop: 16 }), 0);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Side padding the page content actually has. Pure so it can be unit-tested. */
|
|
2
|
+
export function pageSideInset(padded: boolean, screen: number): number {
|
|
3
|
+
return padded ? screen : 0;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** Pulls a section out to the screen edges by cancelling the page side inset. */
|
|
7
|
+
export function pageBleedStyle(sideInset: number): { marginHorizontal: number } {
|
|
8
|
+
// `0 - 0` keeps +0 rather than -0, so equality checks stay simple.
|
|
9
|
+
return { marginHorizontal: sideInset === 0 ? 0 : -sideInset };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type PageInsets = {
|
|
13
|
+
top?: number;
|
|
14
|
+
left?: number;
|
|
15
|
+
bottom?: number;
|
|
16
|
+
right?: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* iOS adds `contentInset` on top of the automatic inset, so a negative top
|
|
21
|
+
* equal to the header height cancels only the header clearance; the tab
|
|
22
|
+
* bar clearance stays automatic. Android has no automatic inset, so there
|
|
23
|
+
* is nothing to cancel. Pure so it can be unit-tested.
|
|
24
|
+
*/
|
|
25
|
+
export function pageContentInset(
|
|
26
|
+
os: string,
|
|
27
|
+
contentUnderHeader: number | undefined,
|
|
28
|
+
contentInset: PageInsets | undefined,
|
|
29
|
+
): PageInsets | undefined {
|
|
30
|
+
if (contentUnderHeader == null || os !== "ios") {
|
|
31
|
+
return contentInset;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
top: (contentInset?.top ?? 0) - contentUnderHeader,
|
|
35
|
+
left: contentInset?.left ?? 0,
|
|
36
|
+
bottom: contentInset?.bottom ?? 0,
|
|
37
|
+
right: contentInset?.right ?? 0,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Gap above the first item. None when the content starts under the header. */
|
|
42
|
+
export function pagePaddingTop({
|
|
43
|
+
padded,
|
|
44
|
+
underHeader,
|
|
45
|
+
pageTop,
|
|
46
|
+
}: {
|
|
47
|
+
padded: boolean;
|
|
48
|
+
underHeader: boolean;
|
|
49
|
+
pageTop: number;
|
|
50
|
+
}): number {
|
|
51
|
+
return padded && !underHeader ? pageTop : 0;
|
|
52
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { PAGE_THEME_ROLES } from "./pageThemeRoles.ts";
|
|
4
|
+
|
|
5
|
+
describe("PAGE_THEME_ROLES", () => {
|
|
6
|
+
it("maps the background to a page-prefixed role, not the app's own name", () => {
|
|
7
|
+
assert.equal(PAGE_THEME_ROLES.backgroundColor, "pageBackground");
|
|
8
|
+
});
|
|
9
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ReactNode, Ref } from "react";
|
|
2
|
+
import type { ScrollView, ScrollViewProps } from "react-native";
|
|
3
|
+
import type { TabAccessoryButtonProps } from "../TabAccessoryButton/types";
|
|
4
|
+
|
|
5
|
+
/** The in-screen `TabAccessoryButton` (Android and iOS 18 and earlier). */
|
|
6
|
+
export type PageTabAccessory = Omit<
|
|
7
|
+
TabAccessoryButtonProps,
|
|
8
|
+
"placement" | "variant" | "color"
|
|
9
|
+
>;
|
|
10
|
+
|
|
11
|
+
export type PageProps = Omit<ScrollViewProps, "contentInsetAdjustmentBehavior"> & {
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
ref?: Ref<ScrollView>;
|
|
14
|
+
/** Renders the floating fallback pill and adds bottom room for it. */
|
|
15
|
+
tabAccessory?: PageTabAccessory;
|
|
16
|
+
/** Screen fill. Defaults to theme role `background`. */
|
|
17
|
+
backgroundColor?: string;
|
|
18
|
+
/** Screen-edge, top and bottom padding on the content. Default `true`. */
|
|
19
|
+
padded?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Start the content at the very top, behind the header, e.g. for a hero
|
|
22
|
+
* image. Pass the header height: `useHeaderHeight()` from
|
|
23
|
+
* `expo-router/react-navigation`. The tab bar clearance is kept.
|
|
24
|
+
*/
|
|
25
|
+
contentUnderHeader?: number;
|
|
26
|
+
};
|