@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,485 @@
|
|
|
1
|
+
import { KeyboardAwareLegendList } from "@legendapp/list/keyboard";
|
|
2
|
+
import type { LegendListProps, LegendListRef, LegendListRenderItemProps } from "@legendapp/list/react-native";
|
|
3
|
+
import type { AnimatedLegendListProps } from "@legendapp/list/reanimated";
|
|
4
|
+
import {
|
|
5
|
+
type ForwardedRef,
|
|
6
|
+
forwardRef,
|
|
7
|
+
type ReactElement,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
type Ref,
|
|
10
|
+
type RefAttributes,
|
|
11
|
+
useCallback,
|
|
12
|
+
useEffect,
|
|
13
|
+
useMemo,
|
|
14
|
+
useRef,
|
|
15
|
+
} from "react";
|
|
16
|
+
import { FlatList, type FlatListProps, type ScrollViewProps, View } from "react-native";
|
|
17
|
+
import { KeyboardChatScrollView, type KeyboardChatScrollViewProps } from "react-native-keyboard-controller";
|
|
18
|
+
import Animated, { type SharedValue } from "react-native-reanimated";
|
|
19
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
20
|
+
import { withUniwind } from "uniwind";
|
|
21
|
+
import { cn } from "../../lib/cn";
|
|
22
|
+
import { composeRefs } from "../../lib/compose-refs";
|
|
23
|
+
import { useScreenDebug } from "./screen.context";
|
|
24
|
+
import type { ScreenScrollableProps } from "./screen.types";
|
|
25
|
+
import { SCREEN_DEBUG_COLORS } from "./screen-debug";
|
|
26
|
+
import { resolveListComponent } from "./screen-list-component";
|
|
27
|
+
import {
|
|
28
|
+
type ChatComposerSpacer,
|
|
29
|
+
useChatComposerBaseSpacerHeight,
|
|
30
|
+
useChatComposerGrowthPadding,
|
|
31
|
+
useScreenScrollInsets,
|
|
32
|
+
} from "./use-screen-scroll-insets";
|
|
33
|
+
|
|
34
|
+
// `Animated.createAnimatedComponent` erases the generic parameters; the recast
|
|
35
|
+
// puts them back.
|
|
36
|
+
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList) as unknown as typeof FlatList;
|
|
37
|
+
|
|
38
|
+
/** See the note on the same shape in `screen-legend-list` — the wrapper collapses `<ItemT>`. */
|
|
39
|
+
type StyledKeyboardAwareLegendListComponent = <ItemT>(
|
|
40
|
+
props: Omit<
|
|
41
|
+
AnimatedLegendListProps<ItemT>,
|
|
42
|
+
"anchoredEndSpace" | "contentInsetEndAdjustment" | "renderScrollComponent"
|
|
43
|
+
> &
|
|
44
|
+
ChatKeyboardProps & {
|
|
45
|
+
ref?: Ref<LegendListRef>;
|
|
46
|
+
className?: string;
|
|
47
|
+
contentContainerClassName?: string;
|
|
48
|
+
contentInsetEndAdjustment?: SharedValue<number>;
|
|
49
|
+
keyboardOffset?: number;
|
|
50
|
+
keyboardDismissMode?: ScrollViewProps["keyboardDismissMode"];
|
|
51
|
+
keyboardShouldPersistTaps?: ScrollViewProps["keyboardShouldPersistTaps"];
|
|
52
|
+
showsVerticalScrollIndicator?: boolean;
|
|
53
|
+
automaticallyAdjustContentInsets?: boolean;
|
|
54
|
+
contentInsetAdjustmentBehavior?: ScrollViewProps["contentInsetAdjustmentBehavior"];
|
|
55
|
+
contentContainerStyle?: ScrollViewProps["contentContainerStyle"];
|
|
56
|
+
style?: ScrollViewProps["style"];
|
|
57
|
+
onScroll?: ScrollViewProps["onScroll"];
|
|
58
|
+
}
|
|
59
|
+
) => ReactElement | null;
|
|
60
|
+
|
|
61
|
+
// Third-party, so `className` needs the wrapper, built once at module scope.
|
|
62
|
+
const StyledKeyboardAwareLegendList = withUniwind(
|
|
63
|
+
KeyboardAwareLegendList
|
|
64
|
+
) as unknown as StyledKeyboardAwareLegendListComponent;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A chat gets the horizontal gutter but no vertical padding of its own.
|
|
68
|
+
*
|
|
69
|
+
* Its bubbles carry their own vertical rhythm, and both ends are already spoken
|
|
70
|
+
* for: the navbar spacer at the top and a composer clearance at the bottom that
|
|
71
|
+
* is computed to the point. Padding the container would push the newest message
|
|
72
|
+
* off that reserve.
|
|
73
|
+
*/
|
|
74
|
+
const CHAT_CONTENT_CLASS = "px-screen-gutter";
|
|
75
|
+
|
|
76
|
+
/** How the keyboard moves a chat list, shared by both variants. */
|
|
77
|
+
type ChatKeyboardProps = {
|
|
78
|
+
/**
|
|
79
|
+
* Distance from the bottom of the screen to the list. The keyboard pushes
|
|
80
|
+
* content by `keyboardHeight - offset` rather than the full height, so this
|
|
81
|
+
* is what stops the lift overshooting past a footer. Defaults to the
|
|
82
|
+
* safe-area inset.
|
|
83
|
+
*/
|
|
84
|
+
offset?: number;
|
|
85
|
+
keyboardLiftBehavior?: KeyboardChatScrollViewProps["keyboardLiftBehavior"];
|
|
86
|
+
freeze?: KeyboardChatScrollViewProps["freeze"];
|
|
87
|
+
applyWorkaroundForContentInsetHitTestBug?: KeyboardChatScrollViewProps["applyWorkaroundForContentInsetHitTestBug"];
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** What both chat variants accept, regardless of which engine draws the rows. */
|
|
91
|
+
type ChatListSharedProps = ScreenScrollableProps &
|
|
92
|
+
ChatKeyboardProps & {
|
|
93
|
+
/**
|
|
94
|
+
* Collapsed height of the `Screen.Footer` CONTENT box — excluding the
|
|
95
|
+
* footer's own padding and the safe area.
|
|
96
|
+
*
|
|
97
|
+
* Seeds the composer clearance so the list's FIRST layout already clears
|
|
98
|
+
* the composer. The measured height corrects it a commit later if the two
|
|
99
|
+
* disagree, but by then the list has already scrolled to the end against
|
|
100
|
+
* the wrong reserve — which is why the newest message hides under the
|
|
101
|
+
* composer only sometimes. Pass this whenever the collapsed height is a
|
|
102
|
+
* known constant.
|
|
103
|
+
*/
|
|
104
|
+
composerBaseHeight?: number;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type ScreenChatListFlatProps<ItemT> = ChatListSharedProps &
|
|
108
|
+
Omit<FlatListProps<ItemT>, "children" | "inverted" | "onScroll"> & {
|
|
109
|
+
/** An inverted `FlatList`. Data must be newest-first. */
|
|
110
|
+
variant: "flat";
|
|
111
|
+
inverted?: boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Override the scroll range a growing input adds.
|
|
114
|
+
* @default the composer's growth above its own baseline, from the screen context
|
|
115
|
+
*/
|
|
116
|
+
extraContentPadding?: KeyboardChatScrollViewProps["extraContentPadding"];
|
|
117
|
+
blankSpace?: KeyboardChatScrollViewProps["blankSpace"];
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export type ScreenChatListLegendProps<ItemT> = ChatListSharedProps &
|
|
121
|
+
Omit<
|
|
122
|
+
AnimatedLegendListProps<ItemT>,
|
|
123
|
+
"anchoredEndSpace" | "contentInsetEndAdjustment" | "renderScrollComponent" | "onScroll"
|
|
124
|
+
> & {
|
|
125
|
+
/** LegendList in chat layout, with chronological oldest-first data. The default. */
|
|
126
|
+
variant?: "legend";
|
|
127
|
+
/**
|
|
128
|
+
* Override the padding that overlay growth adds above the collapsed
|
|
129
|
+
* baseline.
|
|
130
|
+
* @default the footer's overlay height from the screen context
|
|
131
|
+
*/
|
|
132
|
+
contentInsetEndAdjustment?: SharedValue<number>;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export type ScreenChatListProps<ItemT> = ScreenChatListFlatProps<ItemT> | ScreenChatListLegendProps<ItemT>;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* One name, two ref types, correlated with `variant`.
|
|
139
|
+
*
|
|
140
|
+
* `forwardRef<T, P>` has a single `T`, so the correlation cannot be expressed by
|
|
141
|
+
* the render function — it is stated here and re-established by a cast per
|
|
142
|
+
* branch inside it. Those casts are the price of `forwardRef` on a discriminated
|
|
143
|
+
* union: nothing checks them, so these two arms and those two branches are
|
|
144
|
+
* edited together or not at all.
|
|
145
|
+
*/
|
|
146
|
+
type ScreenChatListComponent = (<ItemT>(
|
|
147
|
+
props:
|
|
148
|
+
| (ScreenChatListFlatProps<ItemT> & RefAttributes<FlatList<ItemT>>)
|
|
149
|
+
| (ScreenChatListLegendProps<ItemT> & RefAttributes<LegendListRef>)
|
|
150
|
+
) => ReactElement) & { displayName?: string };
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A conversation list: composer clearance, keyboard lift and end anchoring.
|
|
154
|
+
*
|
|
155
|
+
* Two engines behind one name, chosen by `variant`, because the two ways to
|
|
156
|
+
* build a chat list differ in what their data means rather than only in how
|
|
157
|
+
* they draw:
|
|
158
|
+
*
|
|
159
|
+
* - `legend` (default) — LegendList in chat layout, chronological oldest-first
|
|
160
|
+
* data, anchored at the end. `renderItem` takes LegendList's own info object.
|
|
161
|
+
* Its ref is a `LegendListRef`.
|
|
162
|
+
* - `flat` — an inverted `FlatList`, newest-first data, header and footer
|
|
163
|
+
* spacers swapped so the composer clearance stays adjacent to the composer.
|
|
164
|
+
* `renderItem` takes React Native's `ListRenderItemInfo`. Its ref is the
|
|
165
|
+
* inverted `FlatList` itself.
|
|
166
|
+
*
|
|
167
|
+
* The two `renderItem` contracts are genuinely different, so the union exposes
|
|
168
|
+
* each engine's own rather than adapting one into the other — an adapter would
|
|
169
|
+
* have to fabricate the `separators` object React Native's signature promises,
|
|
170
|
+
* and nothing would honour it.
|
|
171
|
+
*
|
|
172
|
+
* Composer clearance is a static layout spacer, not a content inset: the list
|
|
173
|
+
* scrolls to the end on its first layout, and an inset applied afterwards
|
|
174
|
+
* arrives too late to be part of that. Live growth and keyboard motion stay on
|
|
175
|
+
* the UI thread through `extraContentPadding` / `contentInsetEndAdjustment`.
|
|
176
|
+
*/
|
|
177
|
+
export const ScreenChatList = forwardRef(function ScreenChatListRender<ItemT>(
|
|
178
|
+
props: ScreenChatListProps<ItemT>,
|
|
179
|
+
ref: ForwardedRef<FlatList<ItemT> | LegendListRef>
|
|
180
|
+
): ReactElement {
|
|
181
|
+
if (props.variant === "flat") {
|
|
182
|
+
return <ScreenChatListFlat {...props} ref={ref as unknown as ForwardedRef<FlatList<ItemT>>} />;
|
|
183
|
+
}
|
|
184
|
+
return <ScreenChatListLegend {...props} ref={ref as unknown as ForwardedRef<LegendListRef>} />;
|
|
185
|
+
}) as unknown as ScreenChatListComponent;
|
|
186
|
+
ScreenChatList.displayName = "DelacourUI.Screen.ChatList";
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The composer clearance, as two separately visible bands.
|
|
190
|
+
*
|
|
191
|
+
* Two views rather than one total: under `<Screen debug>` the `occupancy`
|
|
192
|
+
* band is EXACTLY what the footer covers, so its edge must land on the
|
|
193
|
+
* footer's own. A single lumped spacer would hide a shortfall inside itself.
|
|
194
|
+
*
|
|
195
|
+
* An inverted list renders its header and footer contents bottom-to-top, so the
|
|
196
|
+
* order flips to keep `occupancy` the band adjacent to the composer.
|
|
197
|
+
*/
|
|
198
|
+
function useComposerSpacerNode(spacer: ChatComposerSpacer, isInverted: boolean, debug: boolean): ReactNode {
|
|
199
|
+
return useMemo(() => {
|
|
200
|
+
const bands = [
|
|
201
|
+
<View
|
|
202
|
+
key="gap"
|
|
203
|
+
style={[{ height: spacer.gap }, debug ? { backgroundColor: SCREEN_DEBUG_COLORS.listComposerGap } : null]}
|
|
204
|
+
/>,
|
|
205
|
+
<View
|
|
206
|
+
key="occupancy"
|
|
207
|
+
style={[
|
|
208
|
+
{ height: spacer.occupancy },
|
|
209
|
+
debug ? { backgroundColor: SCREEN_DEBUG_COLORS.listComposerSpacer } : null,
|
|
210
|
+
]}
|
|
211
|
+
/>,
|
|
212
|
+
];
|
|
213
|
+
return <>{isInverted ? bands.reverse() : bands}</>;
|
|
214
|
+
}, [spacer.gap, spacer.occupancy, isInverted, debug]);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** The flat variant's own type, restated so `<ItemT>` survives `forwardRef`. */
|
|
218
|
+
type ScreenChatListFlatComponent = (<ItemT>(
|
|
219
|
+
props: ScreenChatListFlatProps<ItemT> & RefAttributes<FlatList<ItemT>>
|
|
220
|
+
) => ReactElement) & { displayName?: string };
|
|
221
|
+
|
|
222
|
+
const ScreenChatListFlat = forwardRef(function ScreenChatListFlatRender<ItemT>(
|
|
223
|
+
{
|
|
224
|
+
header,
|
|
225
|
+
className,
|
|
226
|
+
contentContainerClassName,
|
|
227
|
+
composerBaseHeight,
|
|
228
|
+
inverted = true,
|
|
229
|
+
offset: offsetProp,
|
|
230
|
+
keyboardLiftBehavior,
|
|
231
|
+
freeze,
|
|
232
|
+
applyWorkaroundForContentInsetHitTestBug,
|
|
233
|
+
extraContentPadding: extraContentPaddingProp,
|
|
234
|
+
blankSpace,
|
|
235
|
+
ListHeaderComponent: ListHeaderComponentProp,
|
|
236
|
+
ListFooterComponent: ListFooterComponentProp,
|
|
237
|
+
contentContainerStyle,
|
|
238
|
+
...props
|
|
239
|
+
}: ScreenChatListFlatProps<ItemT>,
|
|
240
|
+
ref: ForwardedRef<FlatList<ItemT>>
|
|
241
|
+
): ReactElement {
|
|
242
|
+
const { scrollHandler, insetTopAnimatedStyle } = useScreenScrollInsets("chat");
|
|
243
|
+
const composerSpacer = useChatComposerBaseSpacerHeight(composerBaseHeight);
|
|
244
|
+
const composerGrowthPadding = useChatComposerGrowthPadding();
|
|
245
|
+
const { bottom } = useSafeAreaInsets();
|
|
246
|
+
const debug = useScreenDebug();
|
|
247
|
+
|
|
248
|
+
const isInverted = Boolean(inverted);
|
|
249
|
+
// The occupancy maths assumes the lift is short by exactly the safe-area
|
|
250
|
+
// band: `KeyboardChatScrollView` scrolls by `keyboardHeight - offset`, and
|
|
251
|
+
// that band is the one the sticky footer parks behind the keyboard. Lifting
|
|
252
|
+
// by the full height instead leaves the newest message an inset too high.
|
|
253
|
+
const offset = offsetProp ?? bottom;
|
|
254
|
+
const extraContentPadding = extraContentPaddingProp ?? composerGrowthPadding;
|
|
255
|
+
const composerSpacerNode = useComposerSpacerNode(composerSpacer, isInverted, debug);
|
|
256
|
+
const topSpacerDebugStyle = debug ? { backgroundColor: SCREEN_DEBUG_COLORS.listTopSpacer } : null;
|
|
257
|
+
|
|
258
|
+
const renderScrollComponent = useCallback(
|
|
259
|
+
(scrollProps: ScrollViewProps) => (
|
|
260
|
+
<KeyboardChatScrollView
|
|
261
|
+
{...scrollProps}
|
|
262
|
+
applyWorkaroundForContentInsetHitTestBug={applyWorkaroundForContentInsetHitTestBug}
|
|
263
|
+
// iOS's own inset adjustment fights the composer clearance this list
|
|
264
|
+
// manages itself, and the two compound into a drifting bottom gap.
|
|
265
|
+
automaticallyAdjustContentInsets={false}
|
|
266
|
+
blankSpace={blankSpace}
|
|
267
|
+
contentInsetAdjustmentBehavior="never"
|
|
268
|
+
extraContentPadding={extraContentPadding}
|
|
269
|
+
freeze={freeze}
|
|
270
|
+
inverted={isInverted}
|
|
271
|
+
keyboardDismissMode="interactive"
|
|
272
|
+
keyboardLiftBehavior={keyboardLiftBehavior}
|
|
273
|
+
offset={offset}
|
|
274
|
+
/>
|
|
275
|
+
),
|
|
276
|
+
[
|
|
277
|
+
applyWorkaroundForContentInsetHitTestBug,
|
|
278
|
+
blankSpace,
|
|
279
|
+
extraContentPadding,
|
|
280
|
+
freeze,
|
|
281
|
+
isInverted,
|
|
282
|
+
keyboardLiftBehavior,
|
|
283
|
+
offset,
|
|
284
|
+
]
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
const ListHeaderComponent = useMemo<ReactElement>(() => {
|
|
288
|
+
const userHeader = resolveListComponent(ListHeaderComponentProp);
|
|
289
|
+
// Inverted, the list header renders at the visual BOTTOM — beside the
|
|
290
|
+
// composer — so the clearance belongs here rather than in the footer.
|
|
291
|
+
if (isInverted) {
|
|
292
|
+
return (
|
|
293
|
+
<>
|
|
294
|
+
{userHeader}
|
|
295
|
+
{composerSpacerNode}
|
|
296
|
+
</>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
return (
|
|
300
|
+
<>
|
|
301
|
+
<Animated.View style={[insetTopAnimatedStyle, topSpacerDebugStyle]} />
|
|
302
|
+
{header}
|
|
303
|
+
{userHeader}
|
|
304
|
+
</>
|
|
305
|
+
);
|
|
306
|
+
}, [isInverted, ListHeaderComponentProp, header, insetTopAnimatedStyle, topSpacerDebugStyle, composerSpacerNode]);
|
|
307
|
+
|
|
308
|
+
const ListFooterComponent = useMemo<ReactElement>(() => {
|
|
309
|
+
const userFooter = resolveListComponent(ListFooterComponentProp);
|
|
310
|
+
// Inverted, the list footer renders at the visual TOP, under the navbar.
|
|
311
|
+
if (isInverted) {
|
|
312
|
+
return (
|
|
313
|
+
<View>
|
|
314
|
+
<Animated.View style={[insetTopAnimatedStyle, topSpacerDebugStyle]} />
|
|
315
|
+
{header}
|
|
316
|
+
{userFooter}
|
|
317
|
+
</View>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
return (
|
|
321
|
+
<View>
|
|
322
|
+
{userFooter}
|
|
323
|
+
{composerSpacerNode}
|
|
324
|
+
</View>
|
|
325
|
+
);
|
|
326
|
+
}, [isInverted, ListFooterComponentProp, header, insetTopAnimatedStyle, topSpacerDebugStyle, composerSpacerNode]);
|
|
327
|
+
|
|
328
|
+
return (
|
|
329
|
+
<AnimatedFlatList
|
|
330
|
+
className={className}
|
|
331
|
+
keyboardShouldPersistTaps="handled"
|
|
332
|
+
showsVerticalScrollIndicator={false}
|
|
333
|
+
{...props}
|
|
334
|
+
contentContainerClassName={cn(CHAT_CONTENT_CLASS, contentContainerClassName)}
|
|
335
|
+
contentContainerStyle={
|
|
336
|
+
debug ? [contentContainerStyle, { backgroundColor: SCREEN_DEBUG_COLORS.listContent }] : contentContainerStyle
|
|
337
|
+
}
|
|
338
|
+
inverted={isInverted}
|
|
339
|
+
ListFooterComponent={ListFooterComponent}
|
|
340
|
+
ListHeaderComponent={ListHeaderComponent}
|
|
341
|
+
onScroll={scrollHandler}
|
|
342
|
+
ref={ref}
|
|
343
|
+
renderScrollComponent={renderScrollComponent}
|
|
344
|
+
/>
|
|
345
|
+
);
|
|
346
|
+
}) as unknown as ScreenChatListFlatComponent;
|
|
347
|
+
ScreenChatListFlat.displayName = "DelacourUI.Screen.ChatList.Flat";
|
|
348
|
+
|
|
349
|
+
/** The legend variant's own type, restated so `<ItemT>` survives `forwardRef`. */
|
|
350
|
+
type ScreenChatListLegendComponent = (<ItemT>(
|
|
351
|
+
props: ScreenChatListLegendProps<ItemT> & RefAttributes<LegendListRef>
|
|
352
|
+
) => ReactElement) & { displayName?: string };
|
|
353
|
+
|
|
354
|
+
const ScreenChatListLegend = forwardRef(function ScreenChatListLegendRender<ItemT>(
|
|
355
|
+
{
|
|
356
|
+
header,
|
|
357
|
+
className,
|
|
358
|
+
contentContainerClassName,
|
|
359
|
+
composerBaseHeight,
|
|
360
|
+
offset: offsetProp,
|
|
361
|
+
keyboardLiftBehavior,
|
|
362
|
+
freeze,
|
|
363
|
+
applyWorkaroundForContentInsetHitTestBug,
|
|
364
|
+
contentInsetEndAdjustment: contentInsetEndAdjustmentProp,
|
|
365
|
+
ListHeaderComponent: ListHeaderComponentProp,
|
|
366
|
+
ListFooterComponent: ListFooterComponentProp,
|
|
367
|
+
contentContainerStyle,
|
|
368
|
+
// A homogeneous list's rows all average out to one number. A list whose rows
|
|
369
|
+
// run 40pt to 350pt must override this AND pass `getItemType`, or every
|
|
370
|
+
// unmeasured row resolves to a single average spanning the whole range —
|
|
371
|
+
// which is what makes a mixed list jump when a page of it prepends.
|
|
372
|
+
estimatedItemSize = 72,
|
|
373
|
+
alignItemsAtEnd = true,
|
|
374
|
+
initialScrollAtEnd = true,
|
|
375
|
+
maintainScrollAtEnd = true,
|
|
376
|
+
maintainVisibleContentPosition,
|
|
377
|
+
...props
|
|
378
|
+
}: ScreenChatListLegendProps<ItemT>,
|
|
379
|
+
ref: ForwardedRef<LegendListRef>
|
|
380
|
+
): ReactElement {
|
|
381
|
+
const { insetTopAnimatedStyle, scrollHandler } = useScreenScrollInsets("chat");
|
|
382
|
+
const composerSpacer = useChatComposerBaseSpacerHeight(composerBaseHeight);
|
|
383
|
+
const composerGrowthPadding = useChatComposerGrowthPadding();
|
|
384
|
+
const { bottom } = useSafeAreaInsets();
|
|
385
|
+
const debug = useScreenDebug();
|
|
386
|
+
|
|
387
|
+
// Same reasoning as the flat variant: the lift is deliberately short by the
|
|
388
|
+
// safe-area band the sticky footer hides behind the keyboard.
|
|
389
|
+
const keyboardOffset = offsetProp ?? bottom;
|
|
390
|
+
const composerSpacerNode = useComposerSpacerNode(composerSpacer, false, debug);
|
|
391
|
+
const contentInsetEndAdjustment = contentInsetEndAdjustmentProp ?? composerGrowthPadding;
|
|
392
|
+
|
|
393
|
+
const ListHeaderComponent = useMemo<ReactElement>(
|
|
394
|
+
() => (
|
|
395
|
+
<>
|
|
396
|
+
<Animated.View
|
|
397
|
+
style={[insetTopAnimatedStyle, debug ? { backgroundColor: SCREEN_DEBUG_COLORS.listTopSpacer } : null]}
|
|
398
|
+
/>
|
|
399
|
+
{header}
|
|
400
|
+
{resolveListComponent(ListHeaderComponentProp)}
|
|
401
|
+
</>
|
|
402
|
+
),
|
|
403
|
+
[ListHeaderComponentProp, header, insetTopAnimatedStyle, debug]
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
const ListFooterComponent = useMemo<ReactElement>(
|
|
407
|
+
() => (
|
|
408
|
+
<>
|
|
409
|
+
{resolveListComponent(ListFooterComponentProp)}
|
|
410
|
+
{composerSpacerNode}
|
|
411
|
+
</>
|
|
412
|
+
),
|
|
413
|
+
[ListFooterComponentProp, composerSpacerNode]
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
// Fallback for a seed that turned out wrong, or absent. When the baseline
|
|
417
|
+
// spacer GROWS, the content the list was anchored to has shifted up by that
|
|
418
|
+
// much — re-pin to the end, but only if the list was already parked there, so
|
|
419
|
+
// a reader scrolled up into history is never yanked back down. With a correct
|
|
420
|
+
// `composerBaseHeight` this never fires; if it does, the seed is stale.
|
|
421
|
+
const innerRef = useRef<LegendListRef | null>(null);
|
|
422
|
+
const previousSpacerRef = useRef(composerSpacer.total);
|
|
423
|
+
|
|
424
|
+
useEffect(() => {
|
|
425
|
+
const previous = previousSpacerRef.current;
|
|
426
|
+
previousSpacerRef.current = composerSpacer.total;
|
|
427
|
+
if (composerSpacer.total <= previous) return;
|
|
428
|
+
|
|
429
|
+
const state = innerRef.current?.getState();
|
|
430
|
+
if (!state?.isAtEnd && !state?.isWithinMaintainScrollAtEndThreshold) return;
|
|
431
|
+
|
|
432
|
+
// One frame of slack: LegendList picks the new footer size up in ITS own
|
|
433
|
+
// footer `onLayout`, which runs after this commit's native layout, so
|
|
434
|
+
// scrolling now would target the stale size.
|
|
435
|
+
const frame = requestAnimationFrame(() => {
|
|
436
|
+
void innerRef.current?.scrollToEnd({ animated: false });
|
|
437
|
+
});
|
|
438
|
+
return () => cancelAnimationFrame(frame);
|
|
439
|
+
}, [composerSpacer.total]);
|
|
440
|
+
|
|
441
|
+
// Memoised, not called inline: `composeRefs` mints a new callback per call,
|
|
442
|
+
// and React detaches and reattaches a ref whose identity changed. On a chat
|
|
443
|
+
// list that would null `innerRef` and re-resolve the native handle on every
|
|
444
|
+
// render, so the `scrollToEnd` above could fire against a ref mid-swap.
|
|
445
|
+
const setRef = useMemo(() => composeRefs(innerRef, ref), [ref]);
|
|
446
|
+
|
|
447
|
+
return (
|
|
448
|
+
<StyledKeyboardAwareLegendList
|
|
449
|
+
automaticallyAdjustContentInsets={false}
|
|
450
|
+
className={className}
|
|
451
|
+
contentInsetAdjustmentBehavior="never"
|
|
452
|
+
keyboardDismissMode="interactive"
|
|
453
|
+
keyboardShouldPersistTaps="handled"
|
|
454
|
+
showsVerticalScrollIndicator={false}
|
|
455
|
+
{...(props as AnimatedLegendListProps<ItemT>)}
|
|
456
|
+
alignItemsAtEnd={alignItemsAtEnd}
|
|
457
|
+
applyWorkaroundForContentInsetHitTestBug={applyWorkaroundForContentInsetHitTestBug}
|
|
458
|
+
contentContainerClassName={cn(CHAT_CONTENT_CLASS, contentContainerClassName)}
|
|
459
|
+
contentContainerStyle={
|
|
460
|
+
debug ? [contentContainerStyle, { backgroundColor: SCREEN_DEBUG_COLORS.listContent }] : contentContainerStyle
|
|
461
|
+
}
|
|
462
|
+
contentInsetEndAdjustment={contentInsetEndAdjustment}
|
|
463
|
+
estimatedItemSize={estimatedItemSize}
|
|
464
|
+
freeze={freeze}
|
|
465
|
+
initialScrollAtEnd={initialScrollAtEnd}
|
|
466
|
+
keyboardLiftBehavior={keyboardLiftBehavior}
|
|
467
|
+
keyboardOffset={keyboardOffset}
|
|
468
|
+
ListFooterComponent={ListFooterComponent}
|
|
469
|
+
ListHeaderComponent={ListHeaderComponent}
|
|
470
|
+
maintainScrollAtEnd={maintainScrollAtEnd}
|
|
471
|
+
// `data: true` keeps the scroll stable when older pages prepend.
|
|
472
|
+
// `size` MUST stay off: size stabilisation reacts to the animated
|
|
473
|
+
// keyboard and composer inset changes and compounds against
|
|
474
|
+
// `maintainScrollAtEnd`'s end anchoring, intermittently shoving the
|
|
475
|
+
// whole list out of the viewport.
|
|
476
|
+
maintainVisibleContentPosition={maintainVisibleContentPosition ?? { data: true, size: false }}
|
|
477
|
+
onScroll={scrollHandler}
|
|
478
|
+
ref={setRef}
|
|
479
|
+
/>
|
|
480
|
+
);
|
|
481
|
+
}) as unknown as ScreenChatListLegendComponent;
|
|
482
|
+
ScreenChatListLegend.displayName = "DelacourUI.Screen.ChatList.Legend";
|
|
483
|
+
|
|
484
|
+
/** Re-exported so a caller can type its own `renderItem` for the legend variant. */
|
|
485
|
+
export type { LegendListProps, LegendListRef, LegendListRenderItemProps };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { KeyboardGestureArea } from "react-native-keyboard-controller";
|
|
4
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
5
|
+
import type { ScreenInsetProps } from "./screen.types";
|
|
6
|
+
import { resolveScreenEdgePadding, screenVariants } from "./screen.variants";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The `nativeID` linking a chat composer's `TextInput` to `Screen.Content`'s
|
|
10
|
+
* gesture area.
|
|
11
|
+
*
|
|
12
|
+
* iOS applies the interactive-dismiss offset only to a focused input whose
|
|
13
|
+
* `nativeID` matches the gesture area's, so both ends have to agree on one
|
|
14
|
+
* string. Exported rather than left to each screen to invent.
|
|
15
|
+
*/
|
|
16
|
+
export const SCREEN_CHAT_INPUT_NATIVE_ID = "screen-chat-input";
|
|
17
|
+
|
|
18
|
+
export type ScreenContentProps = ScreenInsetProps & {
|
|
19
|
+
/**
|
|
20
|
+
* Links the keyboard gesture area to a `TextInput` with this `nativeID`, so
|
|
21
|
+
* dragging the keyboard down carries the input with it. Pass
|
|
22
|
+
* {@link SCREEN_CHAT_INPUT_NATIVE_ID} on both ends for a chat composer.
|
|
23
|
+
*/
|
|
24
|
+
textInputNativeID?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A screen's content region: the keyboard gesture area plus a surface.
|
|
29
|
+
*
|
|
30
|
+
* The gesture area is what makes a downward drag on the keyboard dismiss it
|
|
31
|
+
* interactively rather than only on a tap. It takes an inline `flex: 1` rather
|
|
32
|
+
* than a class — it is a third-party component, so a `className` would need a
|
|
33
|
+
* `withUniwind` wrapper, and AGENTS.md rule 6 keeps that budget for components
|
|
34
|
+
* that genuinely need styling rather than a single flex value.
|
|
35
|
+
*
|
|
36
|
+
* No edges are inset by default: a screen with an overlay navbar wants its
|
|
37
|
+
* content to run under the notch, and only the caller knows which edges matter.
|
|
38
|
+
*
|
|
39
|
+
* The insets sit on an outer box and the caller's `className` on an inner one,
|
|
40
|
+
* so the two add rather than fight. Collapsed onto one box the inset style beat
|
|
41
|
+
* the class outright and `<Screen.Content className="p-5">` got no padding at
|
|
42
|
+
* all — see `resolveScreenEdgePadding` for why.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* <Screen.Content insets={["bottom"]}>
|
|
46
|
+
* <Screen.ScrollArea>{rows}</Screen.ScrollArea>
|
|
47
|
+
* </Screen.Content>
|
|
48
|
+
*/
|
|
49
|
+
export function ScreenContent({
|
|
50
|
+
insets,
|
|
51
|
+
className,
|
|
52
|
+
textInputNativeID,
|
|
53
|
+
children,
|
|
54
|
+
style,
|
|
55
|
+
...props
|
|
56
|
+
}: ScreenContentProps): ReactElement {
|
|
57
|
+
const safeArea = useSafeAreaInsets();
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<KeyboardGestureArea interpolator="ios" style={{ flex: 1 }} textInputNativeID={textInputNativeID}>
|
|
61
|
+
<View className="flex-1" style={resolveScreenEdgePadding(insets, safeArea)}>
|
|
62
|
+
<View className={screenVariants().content({ className })} style={style} {...props}>
|
|
63
|
+
{children}
|
|
64
|
+
</View>
|
|
65
|
+
</View>
|
|
66
|
+
</KeyboardGestureArea>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
ScreenContent.displayName = "DelacourUI.Screen.Content";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translucent colours for the layers `<Screen debug>` paints.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately raw and garish — debug tooling, never product UI, so these are
|
|
5
|
+
* literals rather than theme tokens on purpose. A layer painted in a semantic
|
|
6
|
+
* colour would be invisible against the surface it sits on, which is the one
|
|
7
|
+
* thing this must never be.
|
|
8
|
+
*
|
|
9
|
+
* Exported so a screen's own footer content — a chat composer, say — can paint
|
|
10
|
+
* its bands in the same palette and line up with what the screen draws.
|
|
11
|
+
*/
|
|
12
|
+
export const SCREEN_DEBUG_COLORS = {
|
|
13
|
+
/** `Screen.Footer`'s sticky wrapper — the layer that shifts with the keyboard. */
|
|
14
|
+
footerSticky: "rgba(255, 0, 0, 0.2)", // red
|
|
15
|
+
/** The footer's safe-area band, which the sticky shift parks behind the keyboard. */
|
|
16
|
+
footerSafeArea: "rgba(255, 140, 0, 0.3)", // orange
|
|
17
|
+
/** The footer's measured content — what drives `footer.height`. */
|
|
18
|
+
footerContent: "rgba(255, 220, 0, 0.3)", // yellow
|
|
19
|
+
/** A scrollable's top spacer, reserving the overlay navbar's height. */
|
|
20
|
+
listTopSpacer: "rgba(0, 90, 255, 0.25)", // blue
|
|
21
|
+
/**
|
|
22
|
+
* A scrollable's composer spacer — exactly the footer's occupancy, so its
|
|
23
|
+
* edge nearest the footer must land ON the footer's own edge. A sliver of
|
|
24
|
+
* `footerSticky` red showing past it means the reserve is short.
|
|
25
|
+
*/
|
|
26
|
+
listComposerSpacer: "rgba(0, 200, 90, 0.3)", // green
|
|
27
|
+
/** Breathing room between the newest message and the composer, above the spacer. */
|
|
28
|
+
listComposerGap: "rgba(0, 200, 200, 0.35)", // cyan
|
|
29
|
+
/** A list's content container bounds. */
|
|
30
|
+
listContent: "rgba(0, 200, 255, 0.1)", // light blue
|
|
31
|
+
/** A composer's fixed layout box — what the footer actually measures. */
|
|
32
|
+
composerLayoutBox: "rgba(150, 0, 255, 0.2)", // purple
|
|
33
|
+
/** A composer's overlay pill: visual height including expansion, driving `overlayHeight`. */
|
|
34
|
+
composerPill: "rgba(255, 0, 180, 0.15)", // pink
|
|
35
|
+
/** A composer's input row wrapper, growing with its content while focused. */
|
|
36
|
+
composerInputWrapper: "rgba(0, 255, 60, 0.2)", // lime
|
|
37
|
+
/** A composer's absolutely positioned input area, bottom-anchored and growing upward. */
|
|
38
|
+
composerInput: "rgba(255, 120, 0, 0.2)", // orange
|
|
39
|
+
/** A composer's actions row. */
|
|
40
|
+
composerActions: "rgba(90, 60, 255, 0.2)", // indigo
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
export type ScreenDebugLayer = keyof typeof SCREEN_DEBUG_COLORS;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { Text } from "../text";
|
|
4
|
+
import { screenVariants } from "./screen.variants";
|
|
5
|
+
import { ScreenContent } from "./screen-content";
|
|
6
|
+
import { ScreenNavbar } from "./screen-navbar";
|
|
7
|
+
import { ScreenRoot } from "./screen-root";
|
|
8
|
+
|
|
9
|
+
export type ScreenErrorProps = {
|
|
10
|
+
/** Headline. Defaults to a generic apology. */
|
|
11
|
+
title?: string;
|
|
12
|
+
/** What went wrong, in a sentence the user can act on. */
|
|
13
|
+
message: string;
|
|
14
|
+
/** Back handler. Omit it and the navbar renders without a back control. */
|
|
15
|
+
onBack?: () => void;
|
|
16
|
+
/** Show the navbar at all. On by default — a dead end the user cannot leave is worse than the error. */
|
|
17
|
+
showNavbar?: boolean;
|
|
18
|
+
/** A retry button, or anything else worth offering below the message. */
|
|
19
|
+
children?: ReactNode;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A whole screen explaining that something failed.
|
|
24
|
+
*
|
|
25
|
+
* The title and message carry their own colour: a `View` does not cascade one
|
|
26
|
+
* to a `Text` descendant.
|
|
27
|
+
*
|
|
28
|
+
* Deliberately plain. A richer empty state — illustration, media slot, its own
|
|
29
|
+
* compound surface — is its own component, and building one into this would
|
|
30
|
+
* make the error screen the place it lives.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* <Screen.Error message={error.message} onBack={() => router.back()}>
|
|
34
|
+
* <Button onPress={refetch} size="sm">Try again</Button>
|
|
35
|
+
* </Screen.Error>
|
|
36
|
+
*/
|
|
37
|
+
export function ScreenError({
|
|
38
|
+
title = "Something went wrong",
|
|
39
|
+
message,
|
|
40
|
+
onBack,
|
|
41
|
+
showNavbar = true,
|
|
42
|
+
children,
|
|
43
|
+
}: ScreenErrorProps): ReactElement {
|
|
44
|
+
const slots = screenVariants();
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<ScreenRoot>
|
|
48
|
+
{showNavbar ? <ScreenNavbar>{onBack ? <ScreenNavbar.BackButton onPress={onBack} /> : null}</ScreenNavbar> : null}
|
|
49
|
+
<ScreenContent insets={["bottom"]}>
|
|
50
|
+
<View className={slots.errorContent()}>
|
|
51
|
+
<View className="gap-1">
|
|
52
|
+
<Text className={slots.errorTitle()}>{title}</Text>
|
|
53
|
+
<Text className={slots.errorMessage()}>{message}</Text>
|
|
54
|
+
</View>
|
|
55
|
+
{children}
|
|
56
|
+
</View>
|
|
57
|
+
</ScreenContent>
|
|
58
|
+
</ScreenRoot>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
ScreenError.displayName = "DelacourUI.Screen.Error";
|