@studio-piot/native-ui 0.2.5 → 0.17.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 +10 -0
- package/dist/components/Choice/Choice.d.ts.map +1 -0
- package/dist/components/Choice/Choice.js +66 -0
- package/dist/components/Choice/Choice.js.map +1 -0
- package/dist/components/Choice/choiceLayout.d.ts +23 -0
- package/dist/components/Choice/choiceLayout.d.ts.map +1 -0
- package/dist/components/Choice/choiceLayout.js +32 -0
- package/dist/components/Choice/choiceLayout.js.map +1 -0
- package/dist/components/Choice/choiceThemeRoles.d.ts +9 -0
- package/dist/components/Choice/choiceThemeRoles.d.ts.map +1 -0
- package/dist/components/Choice/choiceThemeRoles.js +11 -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 +33 -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 +111 -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 +120 -0
- package/src/components/Choice/choiceLayout.test.ts +49 -0
- package/src/components/Choice/choiceLayout.ts +34 -0
- package/src/components/Choice/choiceThemeRoles.test.ts +15 -0
- package/src/components/Choice/choiceThemeRoles.ts +9 -0
- package/src/components/Choice/index.ts +4 -0
- package/src/components/Choice/types.ts +33 -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,183 @@
|
|
|
1
|
+
# Page — usage
|
|
2
|
+
|
|
3
|
+
The standard scrolling screen for a tab: a native stack screen inside `NativeTabs`, such as Today.
|
|
4
|
+
|
|
5
|
+
- **Header clearance.** At rest, the content sits just below the header, including the large title. When you scroll, it moves behind the header. When the large title collapses, the content follows it. On iOS this comes from UIKit through `contentInsetAdjustmentBehavior="automatic"`, which `Page` always sets. On Android the native stack header is laid out above the screen, so nothing overlaps.
|
|
6
|
+
- **Tab bar clearance.** The same automatic inset keeps the last item clear of the tab bar and home indicator on iOS. On Android the tab bar sits outside the screen.
|
|
7
|
+
- **Tab accessory clearance.** Pass `tabAccessory` and `Page` renders the floating fallback pill (`TabAccessoryButton placement="screen"`) and adds bottom room for it. On iOS 26+ the accessory belongs to the native tab bar, so the in-screen pill and the extra room are left out.
|
|
8
|
+
- **Padding.** Content gets the theme spacing: `spacing.screen` at the sides, `spacing.pageTop` at the top and `spacing.pageBottom` at the bottom (defaults 16, 16 and 24; see [Spacing](./button.md#spacing)). Turn it off with `padded={false}`.
|
|
9
|
+
|
|
10
|
+
## Title
|
|
11
|
+
|
|
12
|
+
The title belongs to the header, not the page. Set it in the stack options and turn on the large title to get the iOS collapsing title:
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
// app/(tabs)/today/_layout.tsx
|
|
16
|
+
<Stack screenOptions={{ headerLargeTitle: true }}>
|
|
17
|
+
<Stack.Screen name="index" options={{ title: "Today" }} />
|
|
18
|
+
</Stack>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
For a page with no title, leave `title` empty or set `headerShown: false`. `Page` needs no change either way.
|
|
22
|
+
|
|
23
|
+
## Basic
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
// app/(tabs)/today/index.tsx
|
|
27
|
+
import { Page } from "@studio-piot/native-ui";
|
|
28
|
+
|
|
29
|
+
export default function TodayScreen() {
|
|
30
|
+
return (
|
|
31
|
+
<Page>
|
|
32
|
+
<SummaryCard />
|
|
33
|
+
<RecentActivities />
|
|
34
|
+
</Page>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## With the tab accessory
|
|
40
|
+
|
|
41
|
+
Mount the iOS 26 copy in the tabs layout as usual (see [TabAccessoryButton](./tab-accessory-button.md)). `Page` handles the in-screen copy:
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { Page } from "@studio-piot/native-ui";
|
|
45
|
+
import { router } from "expo-router";
|
|
46
|
+
import { SymbolView } from "expo-symbols";
|
|
47
|
+
|
|
48
|
+
export default function TodayScreen() {
|
|
49
|
+
return (
|
|
50
|
+
<Page
|
|
51
|
+
tabAccessory={{
|
|
52
|
+
label: "New activity",
|
|
53
|
+
icon: ({ color, size }) => (
|
|
54
|
+
<SymbolView name={{ ios: "plus", android: "add" }} size={size} tintColor={color} />
|
|
55
|
+
),
|
|
56
|
+
onPress: () => router.push("/new-activity"),
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
{/* content */}
|
|
60
|
+
</Page>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`tabAccessory` takes the same props as `TabAccessoryButton`, except `placement`, `variant` and `color`, which only apply to the iOS 26 copy.
|
|
66
|
+
|
|
67
|
+
## Full-width sections
|
|
68
|
+
|
|
69
|
+
To take one section out to the screen edges, such as an image, a map or a horizontal carousel, wrap it in `PageBleed`. It cancels exactly the side padding the page has: `spacing.screen` normally, or nothing with `padded={false}`.
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import { Page, PageBleed } from "@studio-piot/native-ui";
|
|
73
|
+
|
|
74
|
+
<Page>
|
|
75
|
+
<Text>Padded text</Text>
|
|
76
|
+
<PageBleed>
|
|
77
|
+
<Image source={hero} style={{ width: "100%", aspectRatio: 16 / 9 }} />
|
|
78
|
+
</PageBleed>
|
|
79
|
+
<Text>Padded again</Text>
|
|
80
|
+
</Page>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
For a page where everything is full width, use `padded={false}` instead.
|
|
84
|
+
|
|
85
|
+
`PageBleed` only knows the padding `Page` itself applies. If you change the side padding with `contentContainerStyle`, it won't follow; set `spacing.screen` on the theme instead.
|
|
86
|
+
|
|
87
|
+
## Content under the header
|
|
88
|
+
|
|
89
|
+
For a hero image or map that should start at the very top, behind the header, pass the header height as `contentUnderHeader`. Expo Router provides it through `useHeaderHeight()`:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
// app/(tabs)/today/_layout.tsx
|
|
93
|
+
<Stack.Screen
|
|
94
|
+
name="index"
|
|
95
|
+
options={{ title: "", headerTransparent: true, headerLargeTitle: false }}
|
|
96
|
+
/>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
// app/(tabs)/today/index.tsx
|
|
101
|
+
import { Page, PageBleed } from "@studio-piot/native-ui";
|
|
102
|
+
import { useHeaderHeight } from "expo-router/react-navigation";
|
|
103
|
+
|
|
104
|
+
export default function TodayScreen() {
|
|
105
|
+
const headerHeight = useHeaderHeight();
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Page contentUnderHeader={headerHeight}>
|
|
109
|
+
<PageBleed>
|
|
110
|
+
<Image source={hero} style={{ width: "100%", height: 320 }} />
|
|
111
|
+
</PageBleed>
|
|
112
|
+
<Text>Padded content below the hero</Text>
|
|
113
|
+
</Page>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
- **What changes.** iOS adds `contentInset` on top of its automatic inset. `Page` sets a negative top inset equal to the header height, which cancels only the header clearance. The tab bar and accessory clearance stay automatic, and `spacing.pageTop` is dropped so the first item touches the top.
|
|
119
|
+
- **Use a small, transparent header.** A large title collapses as you scroll, so its height changes and no single value cancels it. Set `headerLargeTitle: false` and `headerTransparent: true` on hero screens.
|
|
120
|
+
- **Wrap the hero in `PageBleed`** so it also reaches the side edges.
|
|
121
|
+
- **Android.** The native header adds no inset, so `contentUnderHeader` does nothing. With `headerTransparent: true` the content already starts under the header. With an opaque header, content can't go under it.
|
|
122
|
+
|
|
123
|
+
## ScrollView props
|
|
124
|
+
|
|
125
|
+
`Page` passes everything else to its `ScrollView`: `ref`, `refreshControl`, `onScroll`, `keyboardShouldPersistTaps` and so on. `contentContainerStyle` is merged after the built-in padding, so it can override it.
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
const scrollRef = useRef<ScrollView>(null);
|
|
129
|
+
|
|
130
|
+
<Page ref={scrollRef} refreshControl={<RefreshControl refreshing={loading} onRefresh={reload} />}>
|
|
131
|
+
…
|
|
132
|
+
</Page>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`contentInsetAdjustmentBehavior` can't be changed. It is what gives the header and tab bar clearance. Your own `contentInset` is kept, and `contentUnderHeader` is added to it.
|
|
136
|
+
|
|
137
|
+
## Theme roles
|
|
138
|
+
|
|
139
|
+
| Usage | Role |
|
|
140
|
+
|---|---|
|
|
141
|
+
| Screen fill | `pageBackground` |
|
|
142
|
+
|
|
143
|
+
When `tabAccessory` is set, the fallback pill also reads `buttonBackground` and `buttonLabel`.
|
|
144
|
+
|
|
145
|
+
## Props
|
|
146
|
+
|
|
147
|
+
| Prop | Type | Default | Notes |
|
|
148
|
+
|---|---|---|---|
|
|
149
|
+
| `children` | `ReactNode` | — | Page content |
|
|
150
|
+
| `tabAccessory` | `PageTabAccessory` | — | Floating fallback pill and its bottom room |
|
|
151
|
+
| `backgroundColor` | `string` | theme `pageBackground` | Optional override |
|
|
152
|
+
| `padded` | `boolean` | `true` | Side, top and bottom padding from the theme spacing |
|
|
153
|
+
| `contentUnderHeader` | `number` | — | Header height. Content starts at the very top, behind the header (iOS) |
|
|
154
|
+
| `ref` | `Ref<ScrollView>` | — | Forwarded to the scroll view |
|
|
155
|
+
| `style` | view style | — | Scroll view style, merged after `flex: 1` and the background |
|
|
156
|
+
| …`ScrollViewProps` | | | Everything except `contentInsetAdjustmentBehavior` |
|
|
157
|
+
|
|
158
|
+
### PageBleed
|
|
159
|
+
|
|
160
|
+
| Prop | Type | Notes |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| `children` | `ReactNode` | The full-width section |
|
|
163
|
+
| `style` | view style | Merged after the bleed margin |
|
|
164
|
+
|
|
165
|
+
## Gotchas
|
|
166
|
+
|
|
167
|
+
- **Make `Page` the screen's root element.** react-native-screens only links the header (large title collapse and insets) to a `ScrollView` that is a direct child of the screen. `Page` renders its `ScrollView` as that direct child, with no wrapper view. If you wrap `Page` in a `View`, content starts at the very top, under the header. `<Stack.Screen options>` next to it in a fragment is fine, because it renders nothing.
|
|
168
|
+
- **Don't wrap `Page` in a `SafeAreaView` or add `insets.top` padding.** That clears the header twice, leaving a gap under it.
|
|
169
|
+
- **The header must be translucent for content to scroll behind it.** That is the default on iOS. An opaque `headerStyle: { backgroundColor }` still works, but content then scrolls under a solid bar.
|
|
170
|
+
- **On iOS 26, check that the last item clears the native accessory on a device.** If it doesn't, add room with `contentContainerStyle={{ paddingBottom: … }}`.
|
|
171
|
+
|
|
172
|
+
## Imports
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import {
|
|
176
|
+
Page,
|
|
177
|
+
PageBleed,
|
|
178
|
+
PAGE_THEME_ROLES,
|
|
179
|
+
type PageBleedProps,
|
|
180
|
+
type PageProps,
|
|
181
|
+
type PageTabAccessory,
|
|
182
|
+
} from "@studio-piot/native-ui";
|
|
183
|
+
```
|
|
@@ -36,7 +36,7 @@ Import `react-native-gesture-handler` at the top of the entry layout (before oth
|
|
|
36
36
|
|
|
37
37
|
## Theme roles
|
|
38
38
|
|
|
39
|
-
Uses Field roles: `fieldBackground`, `fieldText`, `fieldPlaceholder`, `fieldLabel`, `fieldDescription`, `fieldBorder`, `fieldError`, plus **`fieldSelectSelected`** for the selected option check / label tint.
|
|
39
|
+
Uses Field roles: `fieldBackground`, `fieldText`, `fieldPlaceholder`, `fieldLabel`, `fieldDescription`, `fieldBorder`, `fieldError`, plus **`fieldSelectSelected`** for the selected option check / label tint, and **`selectCloseBackground`** / **`selectCloseIcon`** for the default sheet close disc and its cross. Each has a matching optional prop (`closeBackgroundColor`, `closeIconColor`).
|
|
40
40
|
|
|
41
41
|
## Basic usage
|
|
42
42
|
|
|
@@ -58,7 +58,7 @@ import { Select } from "@studio-piot/native-ui";
|
|
|
58
58
|
|
|
59
59
|
## Close control
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
The default is a filled disc with a cross (`SelectCloseButton`), coloured by `selectCloseBackground` and `selectCloseIcon`. Override:
|
|
62
62
|
|
|
63
63
|
```tsx
|
|
64
64
|
closeButton={(dismiss) => <YourClose onPress={dismiss} />}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# TabAccessoryButton — usage
|
|
2
|
+
|
|
3
|
+
A primary action that floats above the bottom tabs, such as "New activity" on a Today tab.
|
|
4
|
+
|
|
5
|
+
- **iOS 26 and later.** Expo Router's `NativeTabs.BottomAccessory` is UIKit's tab bar accessory: the glass strip above the tabs that Apple Music uses for its mini player. iOS draws the glass, sets the position and handles safe areas. `TabAccessoryButton` supplies the content only: an icon and a label, no fill. A filled button inside the accessory would give two shapes, one inside the other.
|
|
6
|
+
- **Android and iOS 18 and earlier.** There is no accessory. The button floats a filled pill (a `Button` with `radius="full"`) at the bottom of the tab screen, just above the tabs.
|
|
7
|
+
|
|
8
|
+
You mount it twice, and `placement` makes each copy render only where it belongs. You don't need a platform check in the app.
|
|
9
|
+
|
|
10
|
+
| `placement` | Mount it in | Renders on |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `"accessory"` | `NativeTabs.BottomAccessory` | iOS 26+ |
|
|
13
|
+
| `"screen"` | The tab screen, after the scroll view | Android, iOS 18 and earlier |
|
|
14
|
+
|
|
15
|
+
## Tabs layout
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
// app/(tabs)/_layout.tsx
|
|
19
|
+
import { TabAccessoryButton } from "@studio-piot/native-ui";
|
|
20
|
+
import { router, useSegments } from "expo-router";
|
|
21
|
+
import { NativeTabs } from "expo-router/unstable-native-tabs";
|
|
22
|
+
import { SymbolView } from "expo-symbols";
|
|
23
|
+
|
|
24
|
+
export default function TabsLayout() {
|
|
25
|
+
const onToday = useSegments().some((segment) => segment === "today");
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<NativeTabs minimizeBehavior="never">
|
|
29
|
+
{onToday ? (
|
|
30
|
+
<NativeTabs.BottomAccessory>
|
|
31
|
+
<TabAccessoryButton
|
|
32
|
+
placement="accessory"
|
|
33
|
+
label="New activity"
|
|
34
|
+
icon={({ color, size }) => (
|
|
35
|
+
<SymbolView name="plus" size={size} weight="semibold" tintColor={color} />
|
|
36
|
+
)}
|
|
37
|
+
onPress={() => router.push("/new-activity")}
|
|
38
|
+
/>
|
|
39
|
+
</NativeTabs.BottomAccessory>
|
|
40
|
+
) : null}
|
|
41
|
+
<NativeTabs.Trigger name="today">{/* … */}</NativeTabs.Trigger>
|
|
42
|
+
</NativeTabs>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The accessory belongs to the tab bar, not to a tab, so render it conditionally to show it on one tab only. Without that check it shows on every tab.
|
|
48
|
+
|
|
49
|
+
## Tab screen
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
// app/(tabs)/today/index.tsx
|
|
53
|
+
import {
|
|
54
|
+
TabAccessoryButton,
|
|
55
|
+
tabAccessoryScreenInset,
|
|
56
|
+
useThemeSpacing,
|
|
57
|
+
} from "@studio-piot/native-ui";
|
|
58
|
+
|
|
59
|
+
export default function TodayScreen() {
|
|
60
|
+
const { screen } = useThemeSpacing();
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<View style={{ flex: 1 }}>
|
|
64
|
+
<ScrollView contentContainerStyle={{ paddingBottom: tabAccessoryScreenInset(screen) }}>
|
|
65
|
+
{/* content */}
|
|
66
|
+
</ScrollView>
|
|
67
|
+
<TabAccessoryButton
|
|
68
|
+
placement="screen"
|
|
69
|
+
label="New activity"
|
|
70
|
+
icon={plusIcon}
|
|
71
|
+
onPress={() => router.push("/new-activity")}
|
|
72
|
+
/>
|
|
73
|
+
</View>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The screen's root `View` needs `flex: 1`, because the pill is positioned absolutely against it. It sits `spacing.screen` from the theme (default 16) from each side and from the bottom, so the gap around it is even. `tabAccessoryScreenInset(spacing.screen)` keeps the last item of the scroll view clear of the pill: the pill height plus that gap below and above it. `TAB_ACCESSORY_SCREEN_INSET` (84) is the same value with the default spacing. On iOS 26 the in-screen copy renders nothing, so the padding is only extra space at the end of the scroll.
|
|
79
|
+
|
|
80
|
+
`Page` does all of this for you with its `tabAccessory` prop. See [Page](./page.md).
|
|
81
|
+
|
|
82
|
+
## Filled (brand colour)
|
|
83
|
+
|
|
84
|
+
The accessory takes no tint, so `variant="filled"` covers the glass with a pill in the button colours instead (`buttonBackground` and `buttonLabel`). The pill fills the accessory edge to edge and is fully rounded, so you see one coloured capsule above the tabs, not a shape inside the glass. It matches the fallback pill on other platforms.
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
<NativeTabs.BottomAccessory>
|
|
88
|
+
<TabAccessoryButton
|
|
89
|
+
placement="accessory"
|
|
90
|
+
variant="filled"
|
|
91
|
+
label="New activity"
|
|
92
|
+
icon={plusIcon}
|
|
93
|
+
onPress={() => router.push("/new-activity")}
|
|
94
|
+
/>
|
|
95
|
+
</NativeTabs.BottomAccessory>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Use it for one primary brand action. For anything secondary, keep the default glass so the tab bar area stays calm.
|
|
99
|
+
|
|
100
|
+
## Icons
|
|
101
|
+
|
|
102
|
+
`icon` is a `ButtonIcon` render function, the same one `Button` takes: `({ color, size }) => ReactNode`. The colour is `tabAccessoryLabel` for the glass variant and `buttonLabel` for the filled variant and the fallback pill. Icon size is 20. SF Symbols are iOS only; on Android use a platform name (`name={{ ios: "plus", android: "add" }}` with recent expo-symbols) or another icon set.
|
|
103
|
+
|
|
104
|
+
## Theme roles
|
|
105
|
+
|
|
106
|
+
| Usage | Role |
|
|
107
|
+
|---|---|
|
|
108
|
+
| Glass label and icon (iOS 26+) | `tabAccessoryLabel` |
|
|
109
|
+
| Filled variant and fallback pill: fill | `buttonBackground` |
|
|
110
|
+
| Filled variant and fallback pill: label and icon | `buttonLabel` |
|
|
111
|
+
|
|
112
|
+
Only the roles for the platform you are on are read, but add all three to both light and dark maps. `tabAccessoryLabel` is usually your `text` colour, since it sits on glass like the tab labels.
|
|
113
|
+
|
|
114
|
+
## Props
|
|
115
|
+
|
|
116
|
+
| Prop | Type | Default | Notes |
|
|
117
|
+
|---|---|---|---|
|
|
118
|
+
| `placement` | `"accessory" \| "screen"` | — | Required. See above |
|
|
119
|
+
| `label` | `string` | — | Required. Shown text and accessibility label |
|
|
120
|
+
| `onPress` | `() => void` | — | Required |
|
|
121
|
+
| `icon` | `ButtonIcon` | — | Before the label |
|
|
122
|
+
| `variant` | `"glass" \| "filled"` | `"glass"` | iOS 26+ look. The fallback is always filled |
|
|
123
|
+
| `disabled` | `boolean` | `false` | 40% opacity; not pressable |
|
|
124
|
+
| `color` | `string` | theme `tabAccessoryLabel` | Glass content override |
|
|
125
|
+
| `backgroundColor` | `string` | theme `buttonBackground` | Filled and fallback pill fill override |
|
|
126
|
+
| `labelColor` | `string` | theme `buttonLabel` | Filled and fallback pill label override |
|
|
127
|
+
|
|
128
|
+
## Gotchas
|
|
129
|
+
|
|
130
|
+
- **Don't put a `Button` in the accessory.** Its padding leaves a smaller shape inside the system glass. For a coloured accessory, use `variant="filled"`, which fills it edge to edge.
|
|
131
|
+
- **The accessory is full width.** iOS sets its size. If you need a small centred pill instead, that is a custom `GlassView` from expo-glass-effect, not this component.
|
|
132
|
+
- **`minimizeBehavior`.** With `"onScrollDown"`, iOS moves the accessory inline with the collapsed tab bar, and the content gets less room. Keep the label short.
|
|
133
|
+
|
|
134
|
+
## Imports
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import {
|
|
138
|
+
TabAccessoryButton,
|
|
139
|
+
TAB_ACCESSORY_SCREEN_INSET,
|
|
140
|
+
TAB_ACCESSORY_THEME_ROLES,
|
|
141
|
+
tabAccessoryScreenInset,
|
|
142
|
+
type TabAccessoryButtonProps,
|
|
143
|
+
type TabAccessoryPlacement,
|
|
144
|
+
type TabAccessoryVariant,
|
|
145
|
+
} from "@studio-piot/native-ui";
|
|
146
|
+
```
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# Liquid Glass header buttons in Expo Router
|
|
2
|
+
|
|
3
|
+
Oct 2, 2026 · @Guillaume
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
On iOS 26, the native stack puts whatever `headerLeft` or `headerRight` returns inside a native `UIBarButtonItem`. iOS draws the Liquid Glass background around it automatically. Your job is to supply the content only: an icon, or an icon and a label.
|
|
8
|
+
|
|
9
|
+
- **Never add your own background or border.** A styled button inside the glass gives you two shapes, one inside the other.
|
|
10
|
+
- **The shape follows the content.** A square (36×36) gives a circle. Wider content gives a capsule.
|
|
11
|
+
- **One function, one glass shape.** Everything returned by `headerRight` shares one capsule, even when it contains several buttons.
|
|
12
|
+
- **Android and iOS 18 and earlier have no glass.** The same code shows plain icons there.
|
|
13
|
+
|
|
14
|
+
## Icon only (round)
|
|
15
|
+
|
|
16
|
+
A 36×36 `Pressable` holding one SF Symbol becomes a glass circle. `size` sets the icon size and defaults to 17. The other examples reuse this component.
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
// components/header/HeaderIcon.tsx
|
|
20
|
+
import { useThemeColor } from "@/hooks/useThemeColor";
|
|
21
|
+
import { SFSymbol, SymbolView } from "expo-symbols";
|
|
22
|
+
import { Pressable } from "react-native";
|
|
23
|
+
|
|
24
|
+
type HeaderIconProps = {
|
|
25
|
+
name: SFSymbol;
|
|
26
|
+
label: string; // accessibility label, required for icon-only buttons
|
|
27
|
+
onPress: () => void;
|
|
28
|
+
size?: number; // icon size, default 17
|
|
29
|
+
weight?: "regular" | "medium" | "semibold" | "bold";
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function HeaderIcon({
|
|
33
|
+
name,
|
|
34
|
+
label,
|
|
35
|
+
onPress,
|
|
36
|
+
size = 17,
|
|
37
|
+
weight = "semibold",
|
|
38
|
+
}: HeaderIconProps) {
|
|
39
|
+
const text = useThemeColor("text");
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Pressable
|
|
43
|
+
onPress={onPress}
|
|
44
|
+
hitSlop={6}
|
|
45
|
+
accessibilityRole="button"
|
|
46
|
+
accessibilityLabel={label}
|
|
47
|
+
style={({ pressed }) => ({
|
|
48
|
+
width: 36,
|
|
49
|
+
height: 36,
|
|
50
|
+
alignItems: "center",
|
|
51
|
+
justifyContent: "center",
|
|
52
|
+
opacity: pressed ? 0.5 : 1,
|
|
53
|
+
})}
|
|
54
|
+
>
|
|
55
|
+
<SymbolView name={name} size={size} weight={weight} tintColor={text} />
|
|
56
|
+
</Pressable>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Usage:
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
<Stack.Screen
|
|
65
|
+
name="index"
|
|
66
|
+
options={{
|
|
67
|
+
headerLeft: () => (
|
|
68
|
+
<HeaderIcon name="xmark" label="Close" size={18} onPress={() => router.back()} />
|
|
69
|
+
),
|
|
70
|
+
}}
|
|
71
|
+
/>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Capsule (icon and label)
|
|
75
|
+
|
|
76
|
+
An icon, a gap, a label and horizontal padding make content wider than it is tall, so iOS draws a pill. The icon defaults to 15 so it sits close to the height of the 16pt label.
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
// components/header/HeaderCapsule.tsx
|
|
80
|
+
import { useThemeColor } from "@/hooks/useThemeColor";
|
|
81
|
+
import { SFSymbol, SymbolView } from "expo-symbols";
|
|
82
|
+
import { Pressable, Text } from "react-native";
|
|
83
|
+
|
|
84
|
+
type HeaderCapsuleProps = {
|
|
85
|
+
icon: SFSymbol;
|
|
86
|
+
label: string;
|
|
87
|
+
onPress: () => void;
|
|
88
|
+
iconSize?: number; // default 15
|
|
89
|
+
fontSize?: number; // default 16
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export function HeaderCapsule({
|
|
93
|
+
icon,
|
|
94
|
+
label,
|
|
95
|
+
onPress,
|
|
96
|
+
iconSize = 15,
|
|
97
|
+
fontSize = 16,
|
|
98
|
+
}: HeaderCapsuleProps) {
|
|
99
|
+
const text = useThemeColor("text");
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Pressable
|
|
103
|
+
onPress={onPress}
|
|
104
|
+
accessibilityRole="button"
|
|
105
|
+
accessibilityLabel={label}
|
|
106
|
+
style={({ pressed }) => ({
|
|
107
|
+
flexDirection: "row",
|
|
108
|
+
alignItems: "center",
|
|
109
|
+
gap: 6,
|
|
110
|
+
height: 36,
|
|
111
|
+
paddingHorizontal: 12,
|
|
112
|
+
opacity: pressed ? 0.5 : 1,
|
|
113
|
+
})}
|
|
114
|
+
>
|
|
115
|
+
<SymbolView name={icon} size={iconSize} weight="semibold" tintColor={text} />
|
|
116
|
+
<Text style={{ color: text, fontSize, fontWeight: "600" }}>{label}</Text>
|
|
117
|
+
</Pressable>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Usage:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
headerLeft: () => (
|
|
126
|
+
<HeaderCapsule icon="xmark" label="Close" iconSize={16} onPress={() => router.back()} />
|
|
127
|
+
),
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Multiple icons (bundled)
|
|
131
|
+
|
|
132
|
+
Put several `HeaderIcon`s in one row `View` and they share a single glass capsule. Each icon keeps its own size and action.
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
// components/header/HeaderGroup.tsx
|
|
136
|
+
import { ReactNode } from "react";
|
|
137
|
+
import { View } from "react-native";
|
|
138
|
+
|
|
139
|
+
export function HeaderGroup({ children }: { children: ReactNode }) {
|
|
140
|
+
return (
|
|
141
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
|
|
142
|
+
{children}
|
|
143
|
+
</View>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Usage:
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
headerRight: () => (
|
|
152
|
+
<HeaderGroup>
|
|
153
|
+
<HeaderIcon
|
|
154
|
+
name="bookmark"
|
|
155
|
+
label="Saved routes"
|
|
156
|
+
size={19}
|
|
157
|
+
onPress={() => router.push("/new-activity/saved-routes")}
|
|
158
|
+
/>
|
|
159
|
+
<HeaderIcon
|
|
160
|
+
name="gearshape"
|
|
161
|
+
label="Settings"
|
|
162
|
+
onPress={() => router.push("/settings")}
|
|
163
|
+
/>
|
|
164
|
+
</HeaderGroup>
|
|
165
|
+
),
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
A custom-view `headerRight` gives you one shared capsule. To get two separate circles, each button has to be its own native bar button item, which needs the native bar-button-items API in your Expo Router or react-native-screens version.
|
|
169
|
+
|
|
170
|
+
## Header left and right
|
|
171
|
+
|
|
172
|
+
The full `_layout.tsx` for the New activity flow has a Close capsule on the left and a bundled pair of icons on the right. Child screens keep the default back button.
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
// app/new-activity/_layout.tsx
|
|
176
|
+
import { HeaderCapsule } from "@/components/header/HeaderCapsule";
|
|
177
|
+
import { HeaderGroup } from "@/components/header/HeaderGroup";
|
|
178
|
+
import { HeaderIcon } from "@/components/header/HeaderIcon";
|
|
179
|
+
import { useThemeColor } from "@/hooks/useThemeColor";
|
|
180
|
+
import { router, Stack } from "expo-router";
|
|
181
|
+
|
|
182
|
+
export default function NewActivityLayout() {
|
|
183
|
+
const background = useThemeColor("background");
|
|
184
|
+
const text = useThemeColor("text");
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<Stack
|
|
188
|
+
screenOptions={{
|
|
189
|
+
headerLargeTitle: false,
|
|
190
|
+
headerTintColor: text,
|
|
191
|
+
headerTitleStyle: { color: text },
|
|
192
|
+
headerStyle: { backgroundColor: background },
|
|
193
|
+
headerShadowVisible: false,
|
|
194
|
+
contentStyle: { backgroundColor: background },
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
<Stack.Screen
|
|
198
|
+
name="index"
|
|
199
|
+
options={{
|
|
200
|
+
title: "New activity",
|
|
201
|
+
headerBackVisible: false,
|
|
202
|
+
headerLeft: () => (
|
|
203
|
+
<HeaderCapsule icon="xmark" label="Close" onPress={() => router.back()} />
|
|
204
|
+
),
|
|
205
|
+
headerRight: () => (
|
|
206
|
+
<HeaderGroup>
|
|
207
|
+
<HeaderIcon
|
|
208
|
+
name="bookmark"
|
|
209
|
+
label="Saved routes"
|
|
210
|
+
size={19}
|
|
211
|
+
onPress={() => router.push("/new-activity/saved-routes")}
|
|
212
|
+
/>
|
|
213
|
+
<HeaderIcon
|
|
214
|
+
name="gearshape"
|
|
215
|
+
label="Settings"
|
|
216
|
+
onPress={() => router.push("/settings")}
|
|
217
|
+
/>
|
|
218
|
+
</HeaderGroup>
|
|
219
|
+
),
|
|
220
|
+
}}
|
|
221
|
+
/>
|
|
222
|
+
<Stack.Screen name="running" options={{ title: "Running" }} />
|
|
223
|
+
<Stack.Screen name="saved-routes" options={{ title: "Saved routes" }} />
|
|
224
|
+
<Stack.Screen name="hiit" options={{ title: "HIIT" }} />
|
|
225
|
+
</Stack>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Route paths such as `/new-activity/saved-routes` and `/settings` are placeholders. Change them to match your app.
|
|
231
|
+
|
|
232
|
+
**Setting buttons from inside a screen.** When a button needs the screen's state, set the options in the screen file with `<Stack.Screen options={{ headerRight: … }} />` instead of in the layout:
|
|
233
|
+
|
|
234
|
+
```tsx
|
|
235
|
+
// app/new-activity/running.tsx
|
|
236
|
+
export default function RunningScreen() {
|
|
237
|
+
const [favourite, setFavourite] = useState(false);
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<>
|
|
241
|
+
<Stack.Screen
|
|
242
|
+
options={{
|
|
243
|
+
headerRight: () => (
|
|
244
|
+
<HeaderIcon
|
|
245
|
+
name={favourite ? "heart.fill" : "heart"}
|
|
246
|
+
label={favourite ? "Remove favourite" : "Add favourite"}
|
|
247
|
+
onPress={() => setFavourite((f) => !f)}
|
|
248
|
+
/>
|
|
249
|
+
),
|
|
250
|
+
}}
|
|
251
|
+
/>
|
|
252
|
+
{/* screen content */}
|
|
253
|
+
</>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Actions
|
|
259
|
+
|
|
260
|
+
Every button takes an `onPress`, so any function works. These are the common ones.
|
|
261
|
+
|
|
262
|
+
| Goal | Call | Notes |
|
|
263
|
+
| --- | --- | --- |
|
|
264
|
+
| Go back one screen | `router.back()` | From the first screen of a nested stack, this closes the modal |
|
|
265
|
+
| Close the modal | `router.dismiss()` | Use when the stack is presented with `presentation: "modal"` |
|
|
266
|
+
| Close the whole flow from a deep screen | `router.dismissAll()` | Returns to the first screen of the stack. Follow with `router.back()` to leave the modal |
|
|
267
|
+
| Open another screen | `router.push("/path")` | Adds to the history |
|
|
268
|
+
| Swap the current screen | `router.replace("/path")` | No back step to the old screen |
|
|
269
|
+
| Open a sheet | `router.push("/sheet-route")` | Set `presentation: "formSheet"` on that route |
|
|
270
|
+
| Share | `Share.share({ message })` | From `react-native` |
|
|
271
|
+
| Toggle state | `setState(...)` | Define the button in the screen file (see above) |
|
|
272
|
+
|
|
273
|
+
**Safe close.** This closes the modal whether or not there is something to go back to:
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
import { router } from "expo-router";
|
|
277
|
+
|
|
278
|
+
export function closeFlow() {
|
|
279
|
+
if (router.canDismiss()) {
|
|
280
|
+
router.dismissAll();
|
|
281
|
+
}
|
|
282
|
+
router.back();
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Confirm before discarding.** Ask before closing when the user has unsaved changes:
|
|
287
|
+
|
|
288
|
+
```tsx
|
|
289
|
+
import { Alert } from "react-native";
|
|
290
|
+
|
|
291
|
+
function confirmClose(hasChanges: boolean) {
|
|
292
|
+
if (!hasChanges) return router.back();
|
|
293
|
+
|
|
294
|
+
Alert.alert("Discard activity?", "Your changes will be lost.", [
|
|
295
|
+
{ text: "Keep editing", style: "cancel" },
|
|
296
|
+
{ text: "Discard", style: "destructive", onPress: () => router.back() },
|
|
297
|
+
]);
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**Haptic tap.** A light haptic makes the button feel native. Add it inside `HeaderIcon`'s `onPress`:
|
|
302
|
+
|
|
303
|
+
```tsx
|
|
304
|
+
import * as Haptics from "expo-haptics";
|
|
305
|
+
|
|
306
|
+
onPress={() => {
|
|
307
|
+
Haptics.selectionAsync();
|
|
308
|
+
onPress();
|
|
309
|
+
}}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Sizing and gotchas
|
|
313
|
+
|
|
314
|
+
| Element | Value | Why |
|
|
315
|
+
| --- | --- | --- |
|
|
316
|
+
| Round button box | 36×36 | Gives a circle that matches system buttons |
|
|
317
|
+
| Icon in a round button | 17 (range 15–20) | Above about 24 the icon fills the circle and looks cramped |
|
|
318
|
+
| Icon in a capsule | 15–16 | Close to the height of a 16pt label |
|
|
319
|
+
| Capsule label | 16pt, weight 600 | Matches system bar button text |
|
|
320
|
+
| Gap between bundled icons | 4 | Keeps the capsule tight |
|
|
321
|
+
| `hitSlop` | 6–8 | A bigger tap area without changing the glass size |
|
|
322
|
+
|
|
323
|
+
- **Thin symbols look smaller.** `xmark` fills less of its box than `gearshape` at the same size. Going about 1pt bigger on thin symbols makes up for it.
|
|
324
|
+
- **`SymbolView` `size` is a box, not a font size.** Wide or tall symbols are shrunk to fit, so check alignment on a device.
|
|
325
|
+
- **An empty `View` still shows glass.** `headerLeft: () => <View />` draws an empty circle. Hide the button with `headerLeft: undefined` instead.
|
|
326
|
+
- **Always set `accessibilityLabel`.** VoiceOver has no text to read on icon-only buttons otherwise.
|
|
327
|
+
- **No glass outside iOS 26.** If you want a visible circle on Android or older iOS, add a background only on those platforms.
|
package/package.json
CHANGED