@delacour/react-native-ui 0.1.0-alpha.20260925053522
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 +113 -0
- package/package.json +125 -0
- package/src/components/accordion/AGENTS.md +193 -0
- package/src/components/accordion/accordion-content.tsx +125 -0
- package/src/components/accordion/accordion-description.tsx +17 -0
- package/src/components/accordion/accordion-indicator.tsx +104 -0
- package/src/components/accordion/accordion-item.tsx +78 -0
- package/src/components/accordion/accordion-title.tsx +18 -0
- package/src/components/accordion/accordion-trigger.tsx +148 -0
- package/src/components/accordion/accordion.context.tsx +153 -0
- package/src/components/accordion/accordion.tsx +253 -0
- package/src/components/accordion/accordion.types.ts +11 -0
- package/src/components/accordion/accordion.variants.test.ts +434 -0
- package/src/components/accordion/accordion.variants.ts +358 -0
- package/src/components/accordion/index.ts +45 -0
- package/src/components/badge/AGENTS.md +83 -0
- package/src/components/badge/badge-close-button.tsx +48 -0
- package/src/components/badge/badge-end-content.tsx +15 -0
- package/src/components/badge/badge-label.tsx +24 -0
- package/src/components/badge/badge-start-content.tsx +16 -0
- package/src/components/badge/badge.context.tsx +64 -0
- package/src/components/badge/badge.tsx +192 -0
- package/src/components/badge/badge.types.ts +10 -0
- package/src/components/badge/badge.variants.test.ts +269 -0
- package/src/components/badge/badge.variants.ts +238 -0
- package/src/components/badge/index.ts +17 -0
- package/src/components/bottom-sheet/AGENTS.md +242 -0
- package/src/components/bottom-sheet/bottom-sheet-background.tsx +35 -0
- package/src/components/bottom-sheet/bottom-sheet-close.tsx +60 -0
- package/src/components/bottom-sheet/bottom-sheet-container.tsx +246 -0
- package/src/components/bottom-sheet/bottom-sheet-content.tsx +66 -0
- package/src/components/bottom-sheet/bottom-sheet-description.tsx +29 -0
- package/src/components/bottom-sheet/bottom-sheet-footer.tsx +150 -0
- package/src/components/bottom-sheet/bottom-sheet-handle.tsx +47 -0
- package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +180 -0
- package/src/components/bottom-sheet/bottom-sheet-portal.tsx +72 -0
- package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +105 -0
- package/src/components/bottom-sheet/bottom-sheet-title.tsx +24 -0
- package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +73 -0
- package/src/components/bottom-sheet/bottom-sheet.context.tsx +135 -0
- package/src/components/bottom-sheet/bottom-sheet.tsx +125 -0
- package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +293 -0
- package/src/components/bottom-sheet/bottom-sheet.variants.ts +220 -0
- package/src/components/bottom-sheet/index.ts +40 -0
- package/src/components/bottom-sheet/use-bottom-sheet-input.ts +125 -0
- package/src/components/button/AGENTS.md +217 -0
- package/src/components/button/button-end-content.tsx +15 -0
- package/src/components/button/button-group-separator.tsx +37 -0
- package/src/components/button/button-group-text.tsx +101 -0
- package/src/components/button/button-group.tsx +158 -0
- package/src/components/button/button-label.tsx +24 -0
- package/src/components/button/button-start-content.tsx +15 -0
- package/src/components/button/button.context.tsx +187 -0
- package/src/components/button/button.tsx +273 -0
- package/src/components/button/button.types.ts +10 -0
- package/src/components/button/button.variants.test.ts +811 -0
- package/src/components/button/button.variants.ts +480 -0
- package/src/components/button/index.ts +50 -0
- package/src/components/chart/AGENTS.md +210 -0
- package/src/components/chart/chart-area.tsx +93 -0
- package/src/components/chart/chart-bar.tsx +103 -0
- package/src/components/chart/chart-bars.tsx +75 -0
- package/src/components/chart/chart-candlestick.tsx +60 -0
- package/src/components/chart/chart-grid.tsx +33 -0
- package/src/components/chart/chart-legend.tsx +41 -0
- package/src/components/chart/chart-line.tsx +50 -0
- package/src/components/chart/chart-scatter.tsx +35 -0
- package/src/components/chart/chart-tooltip-dot.tsx +70 -0
- package/src/components/chart/chart-tooltip-x.tsx +121 -0
- package/src/components/chart/chart-tooltip-y.tsx +55 -0
- package/src/components/chart/chart-tooltip.tsx +133 -0
- package/src/components/chart/chart-x-axis.tsx +27 -0
- package/src/components/chart/chart-y-axis.tsx +21 -0
- package/src/components/chart/chart.context.tsx +127 -0
- package/src/components/chart/chart.tsx +448 -0
- package/src/components/chart/chart.types.ts +108 -0
- package/src/components/chart/chart.variants.test.ts +893 -0
- package/src/components/chart/chart.variants.ts +726 -0
- package/src/components/chart/index.ts +103 -0
- package/src/components/chart/pie-chart-center.tsx +40 -0
- package/src/components/chart/pie-chart-label.tsx +56 -0
- package/src/components/chart/pie-chart-slice.tsx +45 -0
- package/src/components/chart/pie-chart-tooltip.tsx +85 -0
- package/src/components/chart/pie-chart.context.tsx +77 -0
- package/src/components/chart/pie-chart.tsx +247 -0
- package/src/components/chart/use-chart-font.ts +25 -0
- package/src/components/chart/use-chart-palette.ts +38 -0
- package/src/components/checkbox/AGENTS.md +185 -0
- package/src/components/checkbox/checkbox-box.tsx +152 -0
- package/src/components/checkbox/checkbox-group.tsx +107 -0
- package/src/components/checkbox/checkbox-label.tsx +34 -0
- package/src/components/checkbox/checkbox.context.tsx +136 -0
- package/src/components/checkbox/checkbox.tsx +258 -0
- package/src/components/checkbox/checkbox.types.ts +14 -0
- package/src/components/checkbox/checkbox.variants.test.ts +634 -0
- package/src/components/checkbox/checkbox.variants.ts +484 -0
- package/src/components/checkbox/index.ts +42 -0
- package/src/components/field/AGENTS.md +112 -0
- package/src/components/field/field-content.tsx +24 -0
- package/src/components/field/field-description.tsx +29 -0
- package/src/components/field/field-error.tsx +61 -0
- package/src/components/field/field-group.tsx +22 -0
- package/src/components/field/field-label.tsx +47 -0
- package/src/components/field/field-legend.tsx +34 -0
- package/src/components/field/field-separator.tsx +81 -0
- package/src/components/field/field-set.tsx +22 -0
- package/src/components/field/field.context.tsx +95 -0
- package/src/components/field/field.tsx +157 -0
- package/src/components/field/field.types.ts +15 -0
- package/src/components/field/field.variants.test.ts +240 -0
- package/src/components/field/field.variants.ts +156 -0
- package/src/components/field/index.ts +22 -0
- package/src/components/icon/AGENTS.md +104 -0
- package/src/components/icon/icon.context.tsx +29 -0
- package/src/components/icon/icon.tsx +92 -0
- package/src/components/icon/icon.variants.test.ts +111 -0
- package/src/components/icon/icon.variants.ts +79 -0
- package/src/components/icon/index.ts +11 -0
- package/src/components/input/AGENTS.md +126 -0
- package/src/components/input/index.ts +27 -0
- package/src/components/input/input-group-decorator.tsx +84 -0
- package/src/components/input/input-group-prefix.tsx +16 -0
- package/src/components/input/input-group-suffix.tsx +15 -0
- package/src/components/input/input-group.tsx +140 -0
- package/src/components/input/input.context.tsx +87 -0
- package/src/components/input/input.tsx +184 -0
- package/src/components/input/input.types.ts +10 -0
- package/src/components/input/input.variants.test.ts +369 -0
- package/src/components/input/input.variants.ts +329 -0
- package/src/components/list-group/AGENTS.md +67 -0
- package/src/components/list-group/index.ts +20 -0
- package/src/components/list-group/list-group-item-content.tsx +10 -0
- package/src/components/list-group/list-group-item-description.tsx +12 -0
- package/src/components/list-group/list-group-item-prefix.tsx +30 -0
- package/src/components/list-group/list-group-item-suffix.tsx +55 -0
- package/src/components/list-group/list-group-item-title.tsx +17 -0
- package/src/components/list-group/list-group-item.tsx +87 -0
- package/src/components/list-group/list-group.context.tsx +65 -0
- package/src/components/list-group/list-group.tsx +116 -0
- package/src/components/list-group/list-group.types.ts +14 -0
- package/src/components/list-group/list-group.variants.test.ts +223 -0
- package/src/components/list-group/list-group.variants.ts +107 -0
- package/src/components/pressable/AGENTS.md +74 -0
- package/src/components/pressable/index.ts +9 -0
- package/src/components/pressable/pressable.tsx +261 -0
- package/src/components/pressable/pressable.variants.test.ts +128 -0
- package/src/components/pressable/pressable.variants.ts +80 -0
- package/src/components/provider/AGENTS.md +90 -0
- package/src/components/provider/index.ts +1 -0
- package/src/components/provider/provider.tsx +105 -0
- package/src/components/radio/AGENTS.md +245 -0
- package/src/components/radio/index.ts +35 -0
- package/src/components/radio/radio-group.tsx +126 -0
- package/src/components/radio/radio-indicator.tsx +91 -0
- package/src/components/radio/radio-label.tsx +39 -0
- package/src/components/radio/radio.context.tsx +132 -0
- package/src/components/radio/radio.tsx +215 -0
- package/src/components/radio/radio.variants.test.ts +580 -0
- package/src/components/radio/radio.variants.ts +271 -0
- package/src/components/screen/AGENTS.md +287 -0
- package/src/components/screen/index.ts +85 -0
- package/src/components/screen/screen-chat-list.tsx +485 -0
- package/src/components/screen/screen-content.tsx +69 -0
- package/src/components/screen/screen-debug.ts +43 -0
- package/src/components/screen/screen-error.tsx +61 -0
- package/src/components/screen/screen-flat-list.tsx +88 -0
- package/src/components/screen/screen-footer-background.tsx +60 -0
- package/src/components/screen/screen-footer.tsx +170 -0
- package/src/components/screen/screen-header.tsx +36 -0
- package/src/components/screen/screen-legend-list.tsx +105 -0
- package/src/components/screen/screen-list-component.tsx +23 -0
- package/src/components/screen/screen-loading.tsx +50 -0
- package/src/components/screen/screen-navbar-back-button.tsx +79 -0
- package/src/components/screen/screen-navbar-background.tsx +45 -0
- package/src/components/screen/screen-navbar-subtitle.tsx +26 -0
- package/src/components/screen/screen-navbar-title.tsx +31 -0
- package/src/components/screen/screen-navbar.tsx +154 -0
- package/src/components/screen/screen-root.tsx +38 -0
- package/src/components/screen/screen-scroll-area.tsx +116 -0
- package/src/components/screen/screen-scroll-shadow.tsx +186 -0
- package/src/components/screen/screen-section-list.tsx +81 -0
- package/src/components/screen/screen-view.tsx +59 -0
- package/src/components/screen/screen.context.tsx +167 -0
- package/src/components/screen/screen.tsx +114 -0
- package/src/components/screen/screen.types.ts +66 -0
- package/src/components/screen/screen.variants.test.ts +602 -0
- package/src/components/screen/screen.variants.ts +475 -0
- package/src/components/screen/use-screen-scroll-insets.ts +218 -0
- package/src/components/separator/AGENTS.md +30 -0
- package/src/components/separator/index.ts +8 -0
- package/src/components/separator/separator.tsx +85 -0
- package/src/components/slider/AGENTS.md +274 -0
- package/src/components/slider/index.ts +50 -0
- package/src/components/slider/slider-fill.tsx +68 -0
- package/src/components/slider/slider-output.tsx +55 -0
- package/src/components/slider/slider-thumb.tsx +193 -0
- package/src/components/slider/slider-track.tsx +233 -0
- package/src/components/slider/slider.context.tsx +118 -0
- package/src/components/slider/slider.tsx +321 -0
- package/src/components/slider/slider.types.ts +26 -0
- package/src/components/slider/slider.variants.test.ts +856 -0
- package/src/components/slider/slider.variants.ts +661 -0
- package/src/components/spinner/AGENTS.md +73 -0
- package/src/components/spinner/index.ts +17 -0
- package/src/components/spinner/spinner-arc.tsx +90 -0
- package/src/components/spinner/spinner-content.tsx +63 -0
- package/src/components/spinner/spinner.context.tsx +52 -0
- package/src/components/spinner/spinner.tsx +128 -0
- package/src/components/spinner/spinner.variants.test.ts +273 -0
- package/src/components/spinner/spinner.variants.ts +187 -0
- package/src/components/switch/AGENTS.md +213 -0
- package/src/components/switch/index.ts +42 -0
- package/src/components/switch/switch-content.tsx +111 -0
- package/src/components/switch/switch-end-content.tsx +18 -0
- package/src/components/switch/switch-start-content.tsx +20 -0
- package/src/components/switch/switch-thumb.tsx +102 -0
- package/src/components/switch/switch.context.tsx +79 -0
- package/src/components/switch/switch.tsx +423 -0
- package/src/components/switch/switch.types.ts +14 -0
- package/src/components/switch/switch.variants.test.ts +570 -0
- package/src/components/switch/switch.variants.ts +511 -0
- package/src/components/tabs/AGENTS.md +287 -0
- package/src/components/tabs/index.ts +69 -0
- package/src/components/tabs/tabs-content.tsx +65 -0
- package/src/components/tabs/tabs-indicator.tsx +97 -0
- package/src/components/tabs/tabs-label.tsx +62 -0
- package/src/components/tabs/tabs-list.tsx +139 -0
- package/src/components/tabs/tabs-pager.tsx +59 -0
- package/src/components/tabs/tabs-scroll-view.tsx +171 -0
- package/src/components/tabs/tabs-separator.tsx +73 -0
- package/src/components/tabs/tabs-trigger.tsx +210 -0
- package/src/components/tabs/tabs.context.tsx +294 -0
- package/src/components/tabs/tabs.tsx +435 -0
- package/src/components/tabs/tabs.types.ts +13 -0
- package/src/components/tabs/tabs.variants.test.ts +1020 -0
- package/src/components/tabs/tabs.variants.ts +670 -0
- package/src/components/text/AGENTS.md +95 -0
- package/src/components/text/index.ts +25 -0
- package/src/components/text/text.context.tsx +60 -0
- package/src/components/text/text.tsx +251 -0
- package/src/components/text/text.variants.test.ts +422 -0
- package/src/components/text/text.variants.ts +282 -0
- package/src/display-name.test.ts +145 -0
- package/src/docs.test.ts +98 -0
- package/src/expo/navigation-theme.tsx +58 -0
- package/src/hooks/use-controllable-state.ts +45 -0
- package/src/hooks/use-keyboard-state-sync.tsx +147 -0
- package/src/hooks/use-navigation-theme.ts +78 -0
- package/src/hooks/use-theme-color.ts +44 -0
- package/src/icons/central.ts +1 -0
- package/src/lib/cn.test.ts +136 -0
- package/src/lib/cn.ts +29 -0
- package/src/lib/color.test.ts +80 -0
- package/src/lib/color.ts +79 -0
- package/src/lib/compose-refs.test.ts +64 -0
- package/src/lib/compose-refs.ts +37 -0
- package/src/lib/keyboard-animation.test.ts +33 -0
- package/src/lib/keyboard-animation.ts +28 -0
- package/src/lib/merge-props.test.ts +82 -0
- package/src/lib/merge-props.ts +47 -0
- package/src/lib/navigation-theme.test.ts +49 -0
- package/src/lib/navigation-theme.ts +53 -0
- package/src/lib/slot.tsx +45 -0
- package/src/lib/tv.ts +20 -0
- package/src/styles/base.css +2 -0
- package/src/styles/geometry.test.ts +117 -0
- package/src/styles/index.css +3 -0
- package/src/styles/theme-tokens.test.ts +252 -0
- package/src/styles/theme.css +447 -0
- package/src/styles/tokens.css +132 -0
- package/src/styles/tokens.test.ts +181 -0
- package/src/styles/tokens.ts +86 -0
- package/src/uniwind-env.d.ts +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import type { TextProps } from "react-native";
|
|
3
|
+
import { Text } from "../text";
|
|
4
|
+
import { screenVariants } from "./screen.variants";
|
|
5
|
+
|
|
6
|
+
export type ScreenNavbarSubtitleProps = Omit<TextProps, "children"> & {
|
|
7
|
+
className?: string;
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The navbar's secondary line — a step down in scale, on the muted token.
|
|
13
|
+
*
|
|
14
|
+
* Follows `Screen.Navbar.Title`'s rule: a string child is wrapped in a `Text`,
|
|
15
|
+
* any other node passes straight through.
|
|
16
|
+
*/
|
|
17
|
+
export function ScreenNavbarSubtitle({ className, children, ...props }: ScreenNavbarSubtitleProps): ReactElement {
|
|
18
|
+
if (typeof children !== "string") return <>{children}</>;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Text className={screenVariants().navbarSubtitle({ className })} numberOfLines={1} {...props}>
|
|
22
|
+
{children}
|
|
23
|
+
</Text>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
ScreenNavbarSubtitle.displayName = "DelacourUI.Screen.Navbar.Subtitle";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import type { TextProps } from "react-native";
|
|
3
|
+
import { Text } from "../text";
|
|
4
|
+
import { screenVariants } from "./screen.variants";
|
|
5
|
+
|
|
6
|
+
export type ScreenNavbarTitleProps = Omit<TextProps, "children"> & {
|
|
7
|
+
className?: string;
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The navbar's primary line.
|
|
13
|
+
*
|
|
14
|
+
* A string child is wrapped in a `Text`; any other node is passed straight
|
|
15
|
+
* through, so a title can be an avatar row or a dropdown trigger without this
|
|
16
|
+
* component second-guessing it. React Native crashes on bare text outside a
|
|
17
|
+
* `<Text>`, which is why the string case cannot simply be forwarded too — the
|
|
18
|
+
* same rule `Button` and `ListGroup.Item` follow.
|
|
19
|
+
*
|
|
20
|
+
* Carries its own colour: a `View` does not cascade one to a `Text` descendant.
|
|
21
|
+
*/
|
|
22
|
+
export function ScreenNavbarTitle({ className, children, ...props }: ScreenNavbarTitleProps): ReactElement {
|
|
23
|
+
if (typeof children !== "string") return <>{children}</>;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Text className={screenVariants().navbarTitle({ className })} numberOfLines={1} {...props}>
|
|
27
|
+
{children}
|
|
28
|
+
</Text>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
ScreenNavbarTitle.displayName = "DelacourUI.Screen.Navbar.Title";
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode, useCallback, useEffect } from "react";
|
|
2
|
+
import { type LayoutChangeEvent, View } from "react-native";
|
|
3
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
4
|
+
import { useScreenPart } from "./screen.context";
|
|
5
|
+
import type { ScreenPlacementProps } from "./screen.types";
|
|
6
|
+
import { screenVariants } from "./screen.variants";
|
|
7
|
+
import { ScreenNavbarBackButton } from "./screen-navbar-back-button";
|
|
8
|
+
import { ScreenNavbarBackground } from "./screen-navbar-background";
|
|
9
|
+
import { ScreenNavbarSubtitle } from "./screen-navbar-subtitle";
|
|
10
|
+
import { ScreenNavbarTitle } from "./screen-navbar-title";
|
|
11
|
+
|
|
12
|
+
export type ScreenNavbarProps = ScreenPlacementProps & {
|
|
13
|
+
/** Leading content — a back button, a title, an avatar row. */
|
|
14
|
+
children?: ReactNode;
|
|
15
|
+
/** Trailing content, laid out in a row against the right edge. */
|
|
16
|
+
actions?: ReactNode;
|
|
17
|
+
/**
|
|
18
|
+
* Content centred across the full width, independent of what the leading and
|
|
19
|
+
* trailing slots take. Absolutely positioned, so a long title on one side
|
|
20
|
+
* cannot push it off centre.
|
|
21
|
+
*/
|
|
22
|
+
center?: ReactNode;
|
|
23
|
+
/** Classes for the navbar's opaque backing, to tint or blur it. */
|
|
24
|
+
backgroundClassName?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Ramp the bottom hairline in as the screen scrolls, instead of drawing it at
|
|
27
|
+
* rest.
|
|
28
|
+
*
|
|
29
|
+
* Off by default: a screen whose content starts flush against the bar wants
|
|
30
|
+
* the line from the first frame. Turn it on for a header that should read as
|
|
31
|
+
* undivided until the content moves — which only says anything on a screen
|
|
32
|
+
* that scrolls, since the value it reads is published by the scrollable.
|
|
33
|
+
*
|
|
34
|
+
* @default false
|
|
35
|
+
*/
|
|
36
|
+
fadeBorderOnScroll?: boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The bar pinned to the top of a screen.
|
|
41
|
+
*
|
|
42
|
+
* Measures its own height — safe-area band included — into the screen context,
|
|
43
|
+
* so every scrollable reserves exactly what it covers rather than each screen
|
|
44
|
+
* guessing a number. That measurement is the reason this cannot be a standalone
|
|
45
|
+
* component: it only means anything inside a `<Screen>`.
|
|
46
|
+
*
|
|
47
|
+
* `overlay` by default, so content scrolls under it and the hairline appearing
|
|
48
|
+
* on scroll says there is more above. `placement="static"` puts it in the flow
|
|
49
|
+
* instead, for a screen whose content should never pass beneath it.
|
|
50
|
+
*
|
|
51
|
+
* The bottom hairline is drawn at rest. `fadeBorderOnScroll` ramps it in with
|
|
52
|
+
* the content instead, for a header that should read as undivided until the
|
|
53
|
+
* screen moves.
|
|
54
|
+
*
|
|
55
|
+
* Every layout box is `pointerEvents="box-none"`, so the bar itself never
|
|
56
|
+
* swallows a tap meant for the content beneath — only its actual controls do.
|
|
57
|
+
* The centre slot in particular spans the full width and would otherwise block
|
|
58
|
+
* both the leading and trailing controls.
|
|
59
|
+
*
|
|
60
|
+
* Actions are composed rather than configured: pass whatever the screen needs.
|
|
61
|
+
* A `Button` already covers the icon-button case, so there is no `Navbar.Action`
|
|
62
|
+
* restating its loading, sizing and haptics vocabulary.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* <Screen.Navbar>
|
|
66
|
+
* <Screen.Navbar.BackButton onPress={() => router.back()}>
|
|
67
|
+
* <Screen.Navbar.Title>Settings</Screen.Navbar.Title>
|
|
68
|
+
* </Screen.Navbar.BackButton>
|
|
69
|
+
* </Screen.Navbar>
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* <Screen.Navbar
|
|
73
|
+
* actions={
|
|
74
|
+
* <Button accessibilityLabel="Search" size="icon-sm" variant="secondary" onPress={search}>
|
|
75
|
+
* <Icon icon={IconSearch} />
|
|
76
|
+
* </Button>
|
|
77
|
+
* }
|
|
78
|
+
* center={<Screen.Navbar.Title>Inbox</Screen.Navbar.Title>}
|
|
79
|
+
* placement="static"
|
|
80
|
+
* />
|
|
81
|
+
*/
|
|
82
|
+
function ScreenNavbarRoot({
|
|
83
|
+
placement = "overlay",
|
|
84
|
+
className,
|
|
85
|
+
backgroundClassName,
|
|
86
|
+
fadeBorderOnScroll = false,
|
|
87
|
+
children,
|
|
88
|
+
actions,
|
|
89
|
+
center,
|
|
90
|
+
onLayout,
|
|
91
|
+
...props
|
|
92
|
+
}: ScreenNavbarProps): ReactElement {
|
|
93
|
+
const { navbar } = useScreenPart("Screen.Navbar");
|
|
94
|
+
// Not uniwind's `pt-safe`: that compiles to env(safe-area-inset-top), which
|
|
95
|
+
// resolves to zero on React Native and would silently draw the bar over the
|
|
96
|
+
// status bar. The hook is also what the footer reads, so the two agree.
|
|
97
|
+
const { top } = useSafeAreaInsets();
|
|
98
|
+
|
|
99
|
+
const handleLayout = useCallback(
|
|
100
|
+
(event: LayoutChangeEvent) => {
|
|
101
|
+
onLayout?.(event);
|
|
102
|
+
navbar.height.value = event.nativeEvent.layout.height;
|
|
103
|
+
},
|
|
104
|
+
[navbar, onLayout]
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
navbar.placement.value = placement;
|
|
109
|
+
}, [placement, navbar.placement]);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<View
|
|
113
|
+
className={screenVariants({ placement }).navbar({ className })}
|
|
114
|
+
onLayout={handleLayout}
|
|
115
|
+
pointerEvents="box-none"
|
|
116
|
+
{...props}
|
|
117
|
+
>
|
|
118
|
+
<ScreenNavbarBackground className={backgroundClassName} fadeOnScroll={fadeBorderOnScroll} />
|
|
119
|
+
<View pointerEvents="box-none" style={{ paddingTop: top }}>
|
|
120
|
+
<View className={screenVariants().navbarRow()} pointerEvents="box-none">
|
|
121
|
+
<View className={screenVariants().navbarStart()} pointerEvents="box-none">
|
|
122
|
+
{children}
|
|
123
|
+
</View>
|
|
124
|
+
{actions ? (
|
|
125
|
+
<View className={screenVariants().navbarActions()} pointerEvents="box-none">
|
|
126
|
+
{actions}
|
|
127
|
+
</View>
|
|
128
|
+
) : null}
|
|
129
|
+
{center ? (
|
|
130
|
+
<View className={screenVariants().navbarCenter()} pointerEvents="box-none">
|
|
131
|
+
{center}
|
|
132
|
+
</View>
|
|
133
|
+
) : null}
|
|
134
|
+
</View>
|
|
135
|
+
</View>
|
|
136
|
+
</View>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The bar pinned to the top of a screen, with its title and back button slots.
|
|
142
|
+
*
|
|
143
|
+
* Reached as `Screen.Navbar`; see {@link ScreenNavbarRoot} for the layout and
|
|
144
|
+
* the measurement it publishes.
|
|
145
|
+
*/
|
|
146
|
+
export const ScreenNavbar = Object.assign(ScreenNavbarRoot, {
|
|
147
|
+
/** The primary line. A string child is wrapped in a `Text`; any other node passes through. */
|
|
148
|
+
Title: ScreenNavbarTitle,
|
|
149
|
+
/** The secondary line, a step down in scale and on the muted token. */
|
|
150
|
+
Subtitle: ScreenNavbarSubtitle,
|
|
151
|
+
/** The leading control. Takes an `onPress` — this library wires no router of its own. */
|
|
152
|
+
BackButton: ScreenNavbarBackButton,
|
|
153
|
+
displayName: "DelacourUI.Screen.Navbar",
|
|
154
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import { ScreenProvider, useScreenContext } from "./screen.context";
|
|
4
|
+
import { screenVariants } from "./screen.variants";
|
|
5
|
+
|
|
6
|
+
export type ScreenRootProps = ViewProps & {
|
|
7
|
+
/**
|
|
8
|
+
* Paint the screen's layout layers in debug colours — the footer's bands, a
|
|
9
|
+
* scrollable's spacers, a chat list's composer clearance.
|
|
10
|
+
*
|
|
11
|
+
* Opt-in per screen and not gated behind `__DEV__`, so a layout can still be
|
|
12
|
+
* inspected on a release build where a reserve only goes wrong.
|
|
13
|
+
*/
|
|
14
|
+
debug?: boolean;
|
|
15
|
+
className?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The root box of a screen, and the provider its parts measure themselves into.
|
|
20
|
+
*
|
|
21
|
+
* Lives in its own module rather than in `screen.tsx` because `Screen.Loading`
|
|
22
|
+
* and `Screen.Error` are whole screens — they render this — and importing
|
|
23
|
+
* `./screen` from either would close a cycle through the `Object.assign` that
|
|
24
|
+
* names them. See AGENTS.md rule 3.
|
|
25
|
+
*/
|
|
26
|
+
export function ScreenRoot({ debug = false, className, children, ...props }: ScreenRootProps): ReactElement {
|
|
27
|
+
// A nested screen keeps reporting to the outer one rather than starting a
|
|
28
|
+
// second, unread set of measurements.
|
|
29
|
+
const parentContext = useScreenContext();
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<ScreenProvider debug={debug} parentContext={parentContext}>
|
|
33
|
+
<View className={screenVariants().root({ className })} {...props}>
|
|
34
|
+
{children}
|
|
35
|
+
</View>
|
|
36
|
+
</ScreenProvider>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { forwardRef, type ReactElement, type Ref } from "react";
|
|
2
|
+
import type { ScrollViewProps } from "react-native";
|
|
3
|
+
import { KeyboardAwareScrollView, type KeyboardAwareScrollViewRef } from "react-native-keyboard-controller";
|
|
4
|
+
import Animated from "react-native-reanimated";
|
|
5
|
+
import { withUniwind } from "uniwind";
|
|
6
|
+
import { cn } from "../../lib/cn";
|
|
7
|
+
import type { ScreenScrollableProps, ScreenScrollViewRef } from "./screen.types";
|
|
8
|
+
import { screenVariants } from "./screen.variants";
|
|
9
|
+
import { useScreenFooterKeyboardClearance, useScreenScrollInsets } from "./use-screen-scroll-insets";
|
|
10
|
+
|
|
11
|
+
// `contentContainerClassName` only resolves on a uniwind-wrapped component, and
|
|
12
|
+
// the wrapper has to be built at module scope — in render it would mint a new
|
|
13
|
+
// component type every frame and remount the whole scroll view.
|
|
14
|
+
const StyledKeyboardAwareScrollView = withUniwind(KeyboardAwareScrollView);
|
|
15
|
+
|
|
16
|
+
export type ScreenScrollAreaProps = ScrollViewProps &
|
|
17
|
+
ScreenScrollableProps & {
|
|
18
|
+
/**
|
|
19
|
+
* Scroll the focused `TextInput` clear of the keyboard, rather than only
|
|
20
|
+
* reserving space for it. Turn this on for form screens. Off by default —
|
|
21
|
+
* it costs a second scroll driver that a read-only screen never needs.
|
|
22
|
+
*/
|
|
23
|
+
keyboardAware?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A scrolling region that keeps its content clear of the screen's chrome.
|
|
28
|
+
*
|
|
29
|
+
* The navbar and footer reserves are spacer views at either end of the content
|
|
30
|
+
* rather than padding on the content container, so both can animate on the UI
|
|
31
|
+
* thread as the chrome measures itself and the keyboard moves.
|
|
32
|
+
*
|
|
33
|
+
* The ref is a `ScreenScrollViewRef`, for a caller that has to move the scroll
|
|
34
|
+
* view — jumping to an offset, or driving it from a gesture on the UI thread.
|
|
35
|
+
* One type covers both engines: `keyboardAware` swaps in
|
|
36
|
+
* `KeyboardAwareScrollView`, whose ref is a SUPERSET of this one — it adds
|
|
37
|
+
* `assureFocusedInputVisible` to the same scroll view instance — so what a
|
|
38
|
+
* caller receives always satisfies what it declared.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* <Screen.ScrollArea contentContainerClassName="gap-4 px-5">
|
|
42
|
+
* {rows}
|
|
43
|
+
* </Screen.ScrollArea>
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* <Screen.ScrollArea keyboardAware contentContainerClassName="gap-4 px-5">
|
|
47
|
+
* <Form />
|
|
48
|
+
* </Screen.ScrollArea>
|
|
49
|
+
*/
|
|
50
|
+
export const ScreenScrollArea = forwardRef<ScreenScrollViewRef, ScreenScrollAreaProps>(function ScreenScrollAreaRender(
|
|
51
|
+
{ header, className, contentContainerClassName, keyboardAware = false, children, ...props },
|
|
52
|
+
ref
|
|
53
|
+
): ReactElement {
|
|
54
|
+
const { scrollHandler, insetTopAnimatedStyle, insetBottomAnimatedStyle } = useScreenScrollInsets(
|
|
55
|
+
keyboardAware ? "keyboard-aware" : "standard"
|
|
56
|
+
);
|
|
57
|
+
// A sticky footer rides the keyboard, so the focused field has to clear the
|
|
58
|
+
// footer too — not just the keyboard. Deliberately not the chat list's
|
|
59
|
+
// occupancy: the footer's safe-area band is parked behind the keyboard by the
|
|
60
|
+
// sticky shift, so counting it here would overshoot.
|
|
61
|
+
const footerClearance = useScreenFooterKeyboardClearance();
|
|
62
|
+
|
|
63
|
+
const content = (
|
|
64
|
+
<>
|
|
65
|
+
<Animated.View style={insetTopAnimatedStyle} />
|
|
66
|
+
{header}
|
|
67
|
+
{children}
|
|
68
|
+
<Animated.View style={insetBottomAnimatedStyle} />
|
|
69
|
+
</>
|
|
70
|
+
);
|
|
71
|
+
const contentContainer = cn(screenVariants().scrollContent({ className: "py-0" }), contentContainerClassName);
|
|
72
|
+
|
|
73
|
+
// Two branches rather than one `keyboardAware ? A : B` component variable.
|
|
74
|
+
// A variable holding either component types its `ref` as the two refs'
|
|
75
|
+
// intersection, which is neither of them, so `forwardRef` could not name a
|
|
76
|
+
// single instance type at all — and the branch is what lets each engine
|
|
77
|
+
// receive the ref that is actually its own.
|
|
78
|
+
//
|
|
79
|
+
// The cast in that branch is variance, not a guess: `RefObject.current` is
|
|
80
|
+
// mutable and therefore invariant, so TypeScript rejects the narrower ref
|
|
81
|
+
// even though the value flowing back into it is a `KeyboardAwareScrollViewRef`
|
|
82
|
+
// — which IS a `ScreenScrollViewRef` plus one method.
|
|
83
|
+
if (keyboardAware) {
|
|
84
|
+
return (
|
|
85
|
+
<StyledKeyboardAwareScrollView
|
|
86
|
+
className={className}
|
|
87
|
+
keyboardShouldPersistTaps="handled"
|
|
88
|
+
onScroll={scrollHandler}
|
|
89
|
+
showsHorizontalScrollIndicator={false}
|
|
90
|
+
showsVerticalScrollIndicator={false}
|
|
91
|
+
bottomOffset={footerClearance}
|
|
92
|
+
{...props}
|
|
93
|
+
ref={ref as Ref<KeyboardAwareScrollViewRef>}
|
|
94
|
+
contentContainerClassName={contentContainer}
|
|
95
|
+
>
|
|
96
|
+
{content}
|
|
97
|
+
</StyledKeyboardAwareScrollView>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Animated.ScrollView
|
|
103
|
+
className={className}
|
|
104
|
+
keyboardShouldPersistTaps="handled"
|
|
105
|
+
onScroll={scrollHandler}
|
|
106
|
+
showsHorizontalScrollIndicator={false}
|
|
107
|
+
showsVerticalScrollIndicator={false}
|
|
108
|
+
{...props}
|
|
109
|
+
ref={ref}
|
|
110
|
+
contentContainerClassName={contentContainer}
|
|
111
|
+
>
|
|
112
|
+
{content}
|
|
113
|
+
</Animated.ScrollView>
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
ScreenScrollArea.displayName = "DelacourUI.Screen.ScrollArea";
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { LinearGradient } from "expo-linear-gradient";
|
|
2
|
+
import type { ReactElement } from "react";
|
|
3
|
+
import { StyleSheet, View, type ViewProps } from "react-native";
|
|
4
|
+
import Animated, { interpolate, useAnimatedStyle } from "react-native-reanimated";
|
|
5
|
+
import { useThemeColor } from "../../hooks/use-theme-color";
|
|
6
|
+
import { transparentOf } from "../../lib/color";
|
|
7
|
+
import { useScreenPart } from "./screen.context";
|
|
8
|
+
|
|
9
|
+
/** How tall the fade is, in points, when a caller names no size. */
|
|
10
|
+
const SCROLL_SHADOW_SIZE = 28;
|
|
11
|
+
/**
|
|
12
|
+
* How far the scrollable travels before a fade is fully in, as a share of `size`.
|
|
13
|
+
*
|
|
14
|
+
* A quarter, so the fade arrives while the first row is still leaving rather
|
|
15
|
+
* than trailing it: matching the ramp to the fade's own height reads as the
|
|
16
|
+
* shadow lagging the finger.
|
|
17
|
+
*/
|
|
18
|
+
const SCROLL_SHADOW_RAMP = 0.25;
|
|
19
|
+
|
|
20
|
+
/** Which ends of the scrollable fade. */
|
|
21
|
+
export const SCROLL_SHADOW_EDGES = ["top", "bottom", "both"] as const;
|
|
22
|
+
export type ScreenScrollShadowEdge = (typeof SCROLL_SHADOW_EDGES)[number];
|
|
23
|
+
|
|
24
|
+
/** What the fade measures its position from. */
|
|
25
|
+
export const SCROLL_SHADOW_ANCHORS = ["screen", "parent"] as const;
|
|
26
|
+
export type ScreenScrollShadowAnchor = (typeof SCROLL_SHADOW_ANCHORS)[number];
|
|
27
|
+
|
|
28
|
+
export type ScreenScrollShadowProps = Omit<ViewProps, "children" | "pointerEvents"> & {
|
|
29
|
+
/** How tall the fade is, in points. @default SCROLL_SHADOW_SIZE */
|
|
30
|
+
size?: number;
|
|
31
|
+
/** Which ends fade. @default "both" */
|
|
32
|
+
edges?: ScreenScrollShadowEdge;
|
|
33
|
+
/**
|
|
34
|
+
* What the fade positions itself against.
|
|
35
|
+
*
|
|
36
|
+
* `screen` measures from the navbar and footer, which is right whenever this
|
|
37
|
+
* is a sibling of the body. `parent` pins it to its container instead, for a
|
|
38
|
+
* caller that has already placed it — chrome drawing this behind itself,
|
|
39
|
+
* because paint order is source order and nothing outside a navigator can
|
|
40
|
+
* slot between the pages it renders and the bar it renders over them.
|
|
41
|
+
*
|
|
42
|
+
* @default "screen"
|
|
43
|
+
*/
|
|
44
|
+
anchor?: ScreenScrollShadowAnchor;
|
|
45
|
+
/**
|
|
46
|
+
* The colour the content fades into.
|
|
47
|
+
*
|
|
48
|
+
* Defaults to the `background` token, which is what a screen paints. Name
|
|
49
|
+
* another when the shadow sits over a different surface — a `card`, say —
|
|
50
|
+
* because a fade into the wrong colour reads as a grey band rather than as
|
|
51
|
+
* the content running out.
|
|
52
|
+
*/
|
|
53
|
+
color?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Points of SOLID colour above the gradient, for chrome floating over the body.
|
|
56
|
+
*
|
|
57
|
+
* Not an offset — the fade still begins at the navbar. This is the band the
|
|
58
|
+
* floating chrome occupies, painted opaque so content is already gone by the
|
|
59
|
+
* time it reaches the chrome, with the gradient below it doing the dissolving.
|
|
60
|
+
* Offsetting the fade past the chrome instead leaves content crisp behind
|
|
61
|
+
* whatever parts of it are transparent, and then cuts abruptly where the fade
|
|
62
|
+
* finally starts — which is the whole artefact this is here to avoid.
|
|
63
|
+
*/
|
|
64
|
+
coverTop?: number;
|
|
65
|
+
/** Points of solid colour below the gradient, for chrome floating at the end. */
|
|
66
|
+
coverBottom?: number;
|
|
67
|
+
className?: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A fade at each end of the screen's scrollable, for content that runs past it.
|
|
72
|
+
*
|
|
73
|
+
* The problem it solves is that a scrollable with a hard edge looks finished. A
|
|
74
|
+
* row cut exactly in half by the bottom of the viewport reads as a layout bug;
|
|
75
|
+
* the same row fading out reads as more to come. That signal matters most under
|
|
76
|
+
* chrome that floats over the content, where the cut lands mid-component and
|
|
77
|
+
* there is no bar edge to explain it.
|
|
78
|
+
*
|
|
79
|
+
* **It is a sibling of the scrollable, not a wrapper around it.** The screen
|
|
80
|
+
* context already publishes `scrollY`, `contentHeight` and `layoutHeight` from
|
|
81
|
+
* whichever body is mounted, so this reads the numbers rather than intercepting
|
|
82
|
+
* them. Wrapping would mean cloning the child to attach an `onScroll`, and every
|
|
83
|
+
* scrollable this package ships already has one of its own — the reserve
|
|
84
|
+
* animations in `use-screen-scroll-insets` are driven by it — so a second writer
|
|
85
|
+
* would take a handler that is already spoken for.
|
|
86
|
+
*
|
|
87
|
+
* It follows that this works over any of them: `ScrollArea`, `FlatList`,
|
|
88
|
+
* `SectionList`, `LegendList` and `ChatList` all publish to the same context.
|
|
89
|
+
*
|
|
90
|
+
* **It places itself against the screen's own chrome.** The top fade starts at
|
|
91
|
+
* the navbar's measured height and the bottom above the footer's, both read from
|
|
92
|
+
* the same context, so it is correct under either placement without a caller
|
|
93
|
+
* measuring anything.
|
|
94
|
+
*
|
|
95
|
+
* **`coverTop` extends the band, it does not move it.** Chrome floating between
|
|
96
|
+
* the navbar and the body — a tab bar, say — wants the content already gone
|
|
97
|
+
* where it passes behind, so the band is solid for those points and only then
|
|
98
|
+
* begins to fade. Pushing the whole fade below the chrome instead leaves content
|
|
99
|
+
* crisp behind whatever parts of it are transparent, and cuts abruptly where the
|
|
100
|
+
* fade finally starts.
|
|
101
|
+
*
|
|
102
|
+
* **Each end fades only when there is something past it.** The top is out at
|
|
103
|
+
* rest and arrives over the first `size` points of travel; the bottom is there
|
|
104
|
+
* from the start and leaves over the last `size`. Content shorter than its
|
|
105
|
+
* viewport never scrolls, so `maxScroll` is zero and both stay out — a fade over
|
|
106
|
+
* a short page would be an edge that promises content that does not exist.
|
|
107
|
+
*
|
|
108
|
+
* **The far stop is the near colour at zero alpha, never `transparent`.** The
|
|
109
|
+
* keyword is transparent BLACK, so interpolating toward it drags every stop
|
|
110
|
+
* between through grey — a dark bloom over a light ground and a milky one over
|
|
111
|
+
* a dark. `transparentOf` takes the alpha off the colour instead, and this
|
|
112
|
+
* declines to draw at all rather than fall back to the keyword.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* <Screen>
|
|
116
|
+
* <Screen.ScrollArea>{rows}</Screen.ScrollArea>
|
|
117
|
+
* <Screen.ScrollShadow />
|
|
118
|
+
* </Screen>
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* // Under a bar floating over the content.
|
|
122
|
+
* <Screen.ScrollShadow insetTop={barHeight} />
|
|
123
|
+
*/
|
|
124
|
+
export function ScreenScrollShadow({
|
|
125
|
+
size = SCROLL_SHADOW_SIZE,
|
|
126
|
+
edges = "both",
|
|
127
|
+
anchor = "screen",
|
|
128
|
+
color,
|
|
129
|
+
coverTop = 0,
|
|
130
|
+
coverBottom = 0,
|
|
131
|
+
className,
|
|
132
|
+
style,
|
|
133
|
+
...props
|
|
134
|
+
}: ScreenScrollShadowProps): ReactElement {
|
|
135
|
+
const { scrollY, contentHeight, layoutHeight, navbar, footer } = useScreenPart("Screen.ScrollShadow");
|
|
136
|
+
const background = useThemeColor("background");
|
|
137
|
+
const paint = color ?? background;
|
|
138
|
+
const faded = transparentOf(paint);
|
|
139
|
+
const ramp = size * SCROLL_SHADOW_RAMP;
|
|
140
|
+
|
|
141
|
+
// The chrome's height is part of the position, not something a caller adds.
|
|
142
|
+
// This fills the whole screen, so a fade placed at zero would sit behind the
|
|
143
|
+
// navbar under a static one and under an overlay one alike — visible in
|
|
144
|
+
// neither, which is a component that silently does nothing.
|
|
145
|
+
const topStyle = useAnimatedStyle(() => ({
|
|
146
|
+
opacity: interpolate(scrollY.value, [0, ramp], [0, 1], "clamp"),
|
|
147
|
+
top: anchor === "parent" ? 0 : navbar.height.value,
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
const bottomStyle = useAnimatedStyle(() => {
|
|
151
|
+
const maxScroll = contentHeight.value - layoutHeight.value;
|
|
152
|
+
const bottom = anchor === "parent" ? 0 : footer.height.value + footer.overlayHeight.value;
|
|
153
|
+
if (maxScroll <= 0) return { bottom, opacity: 0 };
|
|
154
|
+
|
|
155
|
+
return { bottom, opacity: interpolate(scrollY.value, [maxScroll - ramp, maxScroll], [1, 0], "clamp") };
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Without a colour to fade from there is no honest gradient to draw, and the
|
|
159
|
+
// keyword `transparent` is not a substitute — see `transparentOf`.
|
|
160
|
+
if (!(paint && faded)) return <View />;
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<View className={className} pointerEvents="none" style={[StyleSheet.absoluteFill, style]} {...props}>
|
|
164
|
+
{edges === "bottom" ? null : (
|
|
165
|
+
<Animated.View style={[{ height: coverTop + size, left: 0, position: "absolute", right: 0 }, topStyle]}>
|
|
166
|
+
<LinearGradient
|
|
167
|
+
colors={[paint, paint, faded]}
|
|
168
|
+
locations={[0, coverTop / (coverTop + size), 1]}
|
|
169
|
+
style={StyleSheet.absoluteFill}
|
|
170
|
+
/>
|
|
171
|
+
</Animated.View>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{edges === "top" ? null : (
|
|
175
|
+
<Animated.View style={[{ height: coverBottom + size, left: 0, position: "absolute", right: 0 }, bottomStyle]}>
|
|
176
|
+
<LinearGradient
|
|
177
|
+
colors={[faded, paint, paint]}
|
|
178
|
+
locations={[0, size / (coverBottom + size), 1]}
|
|
179
|
+
style={StyleSheet.absoluteFill}
|
|
180
|
+
/>
|
|
181
|
+
</Animated.View>
|
|
182
|
+
)}
|
|
183
|
+
</View>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
ScreenScrollShadow.displayName = "DelacourUI.Screen.ScrollShadow";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type ForwardedRef, forwardRef, type ReactElement, type RefAttributes, useMemo } from "react";
|
|
2
|
+
import { type DefaultSectionT, SectionList, type SectionListProps, View } from "react-native";
|
|
3
|
+
import Animated from "react-native-reanimated";
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
import type { ScreenScrollableProps } from "./screen.types";
|
|
6
|
+
import { screenVariants } from "./screen.variants";
|
|
7
|
+
import { resolveListComponent } from "./screen-list-component";
|
|
8
|
+
import { useScreenScrollInsets } from "./use-screen-scroll-insets";
|
|
9
|
+
|
|
10
|
+
// `Animated.createAnimatedComponent` erases the generic parameters; the recast
|
|
11
|
+
// puts them back so `sections` and `renderItem` still check against each other.
|
|
12
|
+
const AnimatedSectionList = Animated.createAnimatedComponent(SectionList) as unknown as typeof SectionList;
|
|
13
|
+
|
|
14
|
+
export type ScreenSectionListProps<ItemT, SectionT = DefaultSectionT> = Omit<
|
|
15
|
+
SectionListProps<ItemT, SectionT>,
|
|
16
|
+
"children"
|
|
17
|
+
> &
|
|
18
|
+
ScreenScrollableProps;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The component's own type, restated so the generics survive `forwardRef`.
|
|
22
|
+
*
|
|
23
|
+
* Same reasoning as `ScreenFlatListComponent`: `forwardRef` erases type
|
|
24
|
+
* parameters, so `sections` and `renderItem` would check against `unknown`.
|
|
25
|
+
*/
|
|
26
|
+
type ScreenSectionListComponent = (<ItemT, SectionT = DefaultSectionT>(
|
|
27
|
+
props: ScreenSectionListProps<ItemT, SectionT> & RefAttributes<SectionList<ItemT, SectionT>>
|
|
28
|
+
) => ReactElement) & { displayName?: string };
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A sectioned virtualised list that keeps its content clear of the screen's
|
|
32
|
+
* chrome. `Screen.FlatList`'s behaviour, with sticky section headers.
|
|
33
|
+
*
|
|
34
|
+
* The ref is the list itself, for `scrollToLocation` and the rest.
|
|
35
|
+
*/
|
|
36
|
+
export const ScreenSectionList = forwardRef(function ScreenSectionListRender<ItemT, SectionT = DefaultSectionT>(
|
|
37
|
+
{
|
|
38
|
+
header,
|
|
39
|
+
contentContainerClassName,
|
|
40
|
+
ListHeaderComponent: ListHeaderComponentProp,
|
|
41
|
+
ListFooterComponent: ListFooterComponentProp,
|
|
42
|
+
...props
|
|
43
|
+
}: ScreenSectionListProps<ItemT, SectionT>,
|
|
44
|
+
ref: ForwardedRef<SectionList<ItemT, SectionT>>
|
|
45
|
+
): ReactElement {
|
|
46
|
+
const { scrollHandler, insetTopAnimatedStyle, insetBottomAnimatedStyle } = useScreenScrollInsets("standard");
|
|
47
|
+
|
|
48
|
+
const ListHeaderComponent = useMemo<ReactElement>(
|
|
49
|
+
() => (
|
|
50
|
+
<>
|
|
51
|
+
<Animated.View style={insetTopAnimatedStyle} />
|
|
52
|
+
{header}
|
|
53
|
+
{resolveListComponent(ListHeaderComponentProp)}
|
|
54
|
+
</>
|
|
55
|
+
),
|
|
56
|
+
[header, ListHeaderComponentProp, insetTopAnimatedStyle]
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const ListFooterComponent = useMemo<ReactElement>(
|
|
60
|
+
() => (
|
|
61
|
+
<View>
|
|
62
|
+
{resolveListComponent(ListFooterComponentProp)}
|
|
63
|
+
<Animated.View style={insetBottomAnimatedStyle} />
|
|
64
|
+
</View>
|
|
65
|
+
),
|
|
66
|
+
[ListFooterComponentProp, insetBottomAnimatedStyle]
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<AnimatedSectionList
|
|
71
|
+
showsVerticalScrollIndicator={false}
|
|
72
|
+
{...props}
|
|
73
|
+
contentContainerClassName={cn(screenVariants().scrollContent(), contentContainerClassName)}
|
|
74
|
+
ListFooterComponent={ListFooterComponent}
|
|
75
|
+
ListHeaderComponent={ListHeaderComponent}
|
|
76
|
+
onScroll={scrollHandler}
|
|
77
|
+
ref={ref}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
}) as unknown as ScreenSectionListComponent;
|
|
81
|
+
ScreenSectionList.displayName = "DelacourUI.Screen.SectionList";
|