@delacour/react-native-ui 0.1.0-alpha.3

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.
Files changed (273) hide show
  1. package/README.md +113 -0
  2. package/package.json +125 -0
  3. package/src/components/accordion/AGENTS.md +192 -0
  4. package/src/components/accordion/accordion-content.tsx +125 -0
  5. package/src/components/accordion/accordion-description.tsx +17 -0
  6. package/src/components/accordion/accordion-indicator.tsx +104 -0
  7. package/src/components/accordion/accordion-item.tsx +78 -0
  8. package/src/components/accordion/accordion-title.tsx +18 -0
  9. package/src/components/accordion/accordion-trigger.tsx +148 -0
  10. package/src/components/accordion/accordion.context.tsx +153 -0
  11. package/src/components/accordion/accordion.tsx +253 -0
  12. package/src/components/accordion/accordion.types.ts +11 -0
  13. package/src/components/accordion/accordion.variants.test.ts +435 -0
  14. package/src/components/accordion/accordion.variants.ts +358 -0
  15. package/src/components/accordion/index.ts +45 -0
  16. package/src/components/badge/AGENTS.md +83 -0
  17. package/src/components/badge/badge-close-button.tsx +48 -0
  18. package/src/components/badge/badge-end-content.tsx +15 -0
  19. package/src/components/badge/badge-label.tsx +24 -0
  20. package/src/components/badge/badge-start-content.tsx +16 -0
  21. package/src/components/badge/badge.context.tsx +64 -0
  22. package/src/components/badge/badge.tsx +192 -0
  23. package/src/components/badge/badge.types.ts +10 -0
  24. package/src/components/badge/badge.variants.test.ts +269 -0
  25. package/src/components/badge/badge.variants.ts +238 -0
  26. package/src/components/badge/index.ts +17 -0
  27. package/src/components/bottom-sheet/AGENTS.md +232 -0
  28. package/src/components/bottom-sheet/bottom-sheet-background.tsx +35 -0
  29. package/src/components/bottom-sheet/bottom-sheet-close.tsx +60 -0
  30. package/src/components/bottom-sheet/bottom-sheet-container.tsx +245 -0
  31. package/src/components/bottom-sheet/bottom-sheet-content.tsx +66 -0
  32. package/src/components/bottom-sheet/bottom-sheet-description.tsx +29 -0
  33. package/src/components/bottom-sheet/bottom-sheet-footer.tsx +148 -0
  34. package/src/components/bottom-sheet/bottom-sheet-handle.tsx +47 -0
  35. package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +180 -0
  36. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +72 -0
  37. package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +95 -0
  38. package/src/components/bottom-sheet/bottom-sheet-title.tsx +24 -0
  39. package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +73 -0
  40. package/src/components/bottom-sheet/bottom-sheet.context.tsx +135 -0
  41. package/src/components/bottom-sheet/bottom-sheet.tsx +125 -0
  42. package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +276 -0
  43. package/src/components/bottom-sheet/bottom-sheet.variants.ts +199 -0
  44. package/src/components/bottom-sheet/index.ts +40 -0
  45. package/src/components/bottom-sheet/use-bottom-sheet-input.ts +125 -0
  46. package/src/components/button/AGENTS.md +217 -0
  47. package/src/components/button/button-end-content.tsx +15 -0
  48. package/src/components/button/button-group-separator.tsx +37 -0
  49. package/src/components/button/button-group-text.tsx +101 -0
  50. package/src/components/button/button-group.tsx +158 -0
  51. package/src/components/button/button-label.tsx +24 -0
  52. package/src/components/button/button-start-content.tsx +15 -0
  53. package/src/components/button/button.context.tsx +187 -0
  54. package/src/components/button/button.tsx +273 -0
  55. package/src/components/button/button.types.ts +10 -0
  56. package/src/components/button/button.variants.test.ts +811 -0
  57. package/src/components/button/button.variants.ts +480 -0
  58. package/src/components/button/index.ts +50 -0
  59. package/src/components/chart/AGENTS.md +210 -0
  60. package/src/components/chart/chart-area.tsx +93 -0
  61. package/src/components/chart/chart-bar.tsx +103 -0
  62. package/src/components/chart/chart-bars.tsx +75 -0
  63. package/src/components/chart/chart-candlestick.tsx +60 -0
  64. package/src/components/chart/chart-grid.tsx +33 -0
  65. package/src/components/chart/chart-legend.tsx +41 -0
  66. package/src/components/chart/chart-line.tsx +50 -0
  67. package/src/components/chart/chart-scatter.tsx +35 -0
  68. package/src/components/chart/chart-tooltip-dot.tsx +70 -0
  69. package/src/components/chart/chart-tooltip-x.tsx +121 -0
  70. package/src/components/chart/chart-tooltip-y.tsx +55 -0
  71. package/src/components/chart/chart-tooltip.tsx +133 -0
  72. package/src/components/chart/chart-x-axis.tsx +27 -0
  73. package/src/components/chart/chart-y-axis.tsx +21 -0
  74. package/src/components/chart/chart.context.tsx +127 -0
  75. package/src/components/chart/chart.tsx +448 -0
  76. package/src/components/chart/chart.types.ts +108 -0
  77. package/src/components/chart/chart.variants.test.ts +893 -0
  78. package/src/components/chart/chart.variants.ts +726 -0
  79. package/src/components/chart/index.ts +103 -0
  80. package/src/components/chart/pie-chart-center.tsx +40 -0
  81. package/src/components/chart/pie-chart-label.tsx +56 -0
  82. package/src/components/chart/pie-chart-slice.tsx +45 -0
  83. package/src/components/chart/pie-chart-tooltip.tsx +85 -0
  84. package/src/components/chart/pie-chart.context.tsx +77 -0
  85. package/src/components/chart/pie-chart.tsx +247 -0
  86. package/src/components/chart/use-chart-font.ts +25 -0
  87. package/src/components/chart/use-chart-palette.ts +38 -0
  88. package/src/components/checkbox/AGENTS.md +185 -0
  89. package/src/components/checkbox/checkbox-box.tsx +152 -0
  90. package/src/components/checkbox/checkbox-group.tsx +107 -0
  91. package/src/components/checkbox/checkbox-label.tsx +34 -0
  92. package/src/components/checkbox/checkbox.context.tsx +136 -0
  93. package/src/components/checkbox/checkbox.tsx +258 -0
  94. package/src/components/checkbox/checkbox.types.ts +14 -0
  95. package/src/components/checkbox/checkbox.variants.test.ts +634 -0
  96. package/src/components/checkbox/checkbox.variants.ts +484 -0
  97. package/src/components/checkbox/index.ts +42 -0
  98. package/src/components/field/AGENTS.md +100 -0
  99. package/src/components/field/field-content.tsx +24 -0
  100. package/src/components/field/field-description.tsx +29 -0
  101. package/src/components/field/field-error.tsx +61 -0
  102. package/src/components/field/field-group.tsx +22 -0
  103. package/src/components/field/field-label.tsx +33 -0
  104. package/src/components/field/field-legend.tsx +34 -0
  105. package/src/components/field/field-separator.tsx +81 -0
  106. package/src/components/field/field-set.tsx +22 -0
  107. package/src/components/field/field.context.tsx +78 -0
  108. package/src/components/field/field.tsx +144 -0
  109. package/src/components/field/field.types.ts +15 -0
  110. package/src/components/field/field.variants.test.ts +216 -0
  111. package/src/components/field/field.variants.ts +136 -0
  112. package/src/components/field/index.ts +21 -0
  113. package/src/components/icon/AGENTS.md +104 -0
  114. package/src/components/icon/icon.context.tsx +29 -0
  115. package/src/components/icon/icon.tsx +92 -0
  116. package/src/components/icon/icon.variants.test.ts +111 -0
  117. package/src/components/icon/icon.variants.ts +79 -0
  118. package/src/components/icon/index.ts +11 -0
  119. package/src/components/input/AGENTS.md +126 -0
  120. package/src/components/input/index.ts +27 -0
  121. package/src/components/input/input-group-decorator.tsx +84 -0
  122. package/src/components/input/input-group-prefix.tsx +16 -0
  123. package/src/components/input/input-group-suffix.tsx +15 -0
  124. package/src/components/input/input-group.tsx +140 -0
  125. package/src/components/input/input.context.tsx +87 -0
  126. package/src/components/input/input.tsx +184 -0
  127. package/src/components/input/input.types.ts +10 -0
  128. package/src/components/input/input.variants.test.ts +369 -0
  129. package/src/components/input/input.variants.ts +329 -0
  130. package/src/components/list-group/AGENTS.md +67 -0
  131. package/src/components/list-group/index.ts +20 -0
  132. package/src/components/list-group/list-group-item-content.tsx +10 -0
  133. package/src/components/list-group/list-group-item-description.tsx +12 -0
  134. package/src/components/list-group/list-group-item-prefix.tsx +30 -0
  135. package/src/components/list-group/list-group-item-suffix.tsx +55 -0
  136. package/src/components/list-group/list-group-item-title.tsx +17 -0
  137. package/src/components/list-group/list-group-item.tsx +87 -0
  138. package/src/components/list-group/list-group.context.tsx +65 -0
  139. package/src/components/list-group/list-group.tsx +116 -0
  140. package/src/components/list-group/list-group.types.ts +14 -0
  141. package/src/components/list-group/list-group.variants.test.ts +223 -0
  142. package/src/components/list-group/list-group.variants.ts +107 -0
  143. package/src/components/pressable/AGENTS.md +64 -0
  144. package/src/components/pressable/index.ts +9 -0
  145. package/src/components/pressable/pressable.tsx +256 -0
  146. package/src/components/pressable/pressable.variants.test.ts +99 -0
  147. package/src/components/pressable/pressable.variants.ts +63 -0
  148. package/src/components/provider/AGENTS.md +90 -0
  149. package/src/components/provider/index.ts +1 -0
  150. package/src/components/provider/provider.tsx +105 -0
  151. package/src/components/radio/AGENTS.md +247 -0
  152. package/src/components/radio/index.ts +35 -0
  153. package/src/components/radio/radio-group.tsx +126 -0
  154. package/src/components/radio/radio-indicator.tsx +91 -0
  155. package/src/components/radio/radio-label.tsx +39 -0
  156. package/src/components/radio/radio.context.tsx +132 -0
  157. package/src/components/radio/radio.tsx +215 -0
  158. package/src/components/radio/radio.variants.test.ts +581 -0
  159. package/src/components/radio/radio.variants.ts +272 -0
  160. package/src/components/screen/AGENTS.md +287 -0
  161. package/src/components/screen/index.ts +85 -0
  162. package/src/components/screen/screen-chat-list.tsx +485 -0
  163. package/src/components/screen/screen-content.tsx +69 -0
  164. package/src/components/screen/screen-debug.ts +43 -0
  165. package/src/components/screen/screen-error.tsx +61 -0
  166. package/src/components/screen/screen-flat-list.tsx +88 -0
  167. package/src/components/screen/screen-footer-background.tsx +60 -0
  168. package/src/components/screen/screen-footer.tsx +170 -0
  169. package/src/components/screen/screen-header.tsx +36 -0
  170. package/src/components/screen/screen-legend-list.tsx +105 -0
  171. package/src/components/screen/screen-list-component.tsx +23 -0
  172. package/src/components/screen/screen-loading.tsx +50 -0
  173. package/src/components/screen/screen-navbar-back-button.tsx +79 -0
  174. package/src/components/screen/screen-navbar-background.tsx +45 -0
  175. package/src/components/screen/screen-navbar-subtitle.tsx +26 -0
  176. package/src/components/screen/screen-navbar-title.tsx +31 -0
  177. package/src/components/screen/screen-navbar.tsx +154 -0
  178. package/src/components/screen/screen-root.tsx +38 -0
  179. package/src/components/screen/screen-scroll-area.tsx +116 -0
  180. package/src/components/screen/screen-scroll-shadow.tsx +186 -0
  181. package/src/components/screen/screen-section-list.tsx +81 -0
  182. package/src/components/screen/screen-view.tsx +59 -0
  183. package/src/components/screen/screen.context.tsx +167 -0
  184. package/src/components/screen/screen.tsx +114 -0
  185. package/src/components/screen/screen.types.ts +66 -0
  186. package/src/components/screen/screen.variants.test.ts +602 -0
  187. package/src/components/screen/screen.variants.ts +475 -0
  188. package/src/components/screen/use-screen-scroll-insets.ts +218 -0
  189. package/src/components/separator/AGENTS.md +30 -0
  190. package/src/components/separator/index.ts +8 -0
  191. package/src/components/separator/separator.tsx +85 -0
  192. package/src/components/slider/AGENTS.md +274 -0
  193. package/src/components/slider/index.ts +48 -0
  194. package/src/components/slider/slider-fill.tsx +68 -0
  195. package/src/components/slider/slider-output.tsx +55 -0
  196. package/src/components/slider/slider-thumb.tsx +161 -0
  197. package/src/components/slider/slider-track.tsx +233 -0
  198. package/src/components/slider/slider.context.tsx +118 -0
  199. package/src/components/slider/slider.tsx +321 -0
  200. package/src/components/slider/slider.types.ts +26 -0
  201. package/src/components/slider/slider.variants.test.ts +804 -0
  202. package/src/components/slider/slider.variants.ts +607 -0
  203. package/src/components/spinner/AGENTS.md +73 -0
  204. package/src/components/spinner/index.ts +17 -0
  205. package/src/components/spinner/spinner-arc.tsx +90 -0
  206. package/src/components/spinner/spinner-content.tsx +63 -0
  207. package/src/components/spinner/spinner.context.tsx +52 -0
  208. package/src/components/spinner/spinner.tsx +128 -0
  209. package/src/components/spinner/spinner.variants.test.ts +273 -0
  210. package/src/components/spinner/spinner.variants.ts +187 -0
  211. package/src/components/switch/AGENTS.md +213 -0
  212. package/src/components/switch/index.ts +42 -0
  213. package/src/components/switch/switch-content.tsx +111 -0
  214. package/src/components/switch/switch-end-content.tsx +18 -0
  215. package/src/components/switch/switch-start-content.tsx +20 -0
  216. package/src/components/switch/switch-thumb.tsx +102 -0
  217. package/src/components/switch/switch.context.tsx +79 -0
  218. package/src/components/switch/switch.tsx +423 -0
  219. package/src/components/switch/switch.types.ts +14 -0
  220. package/src/components/switch/switch.variants.test.ts +570 -0
  221. package/src/components/switch/switch.variants.ts +511 -0
  222. package/src/components/tabs/AGENTS.md +286 -0
  223. package/src/components/tabs/index.ts +69 -0
  224. package/src/components/tabs/tabs-content.tsx +65 -0
  225. package/src/components/tabs/tabs-indicator.tsx +97 -0
  226. package/src/components/tabs/tabs-label.tsx +62 -0
  227. package/src/components/tabs/tabs-list.tsx +139 -0
  228. package/src/components/tabs/tabs-pager.tsx +59 -0
  229. package/src/components/tabs/tabs-scroll-view.tsx +171 -0
  230. package/src/components/tabs/tabs-separator.tsx +73 -0
  231. package/src/components/tabs/tabs-trigger.tsx +210 -0
  232. package/src/components/tabs/tabs.context.tsx +294 -0
  233. package/src/components/tabs/tabs.tsx +435 -0
  234. package/src/components/tabs/tabs.types.ts +13 -0
  235. package/src/components/tabs/tabs.variants.test.ts +1020 -0
  236. package/src/components/tabs/tabs.variants.ts +672 -0
  237. package/src/components/text/AGENTS.md +95 -0
  238. package/src/components/text/index.ts +25 -0
  239. package/src/components/text/text.context.tsx +60 -0
  240. package/src/components/text/text.tsx +251 -0
  241. package/src/components/text/text.variants.test.ts +422 -0
  242. package/src/components/text/text.variants.ts +282 -0
  243. package/src/display-name.test.ts +145 -0
  244. package/src/docs.test.ts +98 -0
  245. package/src/expo/navigation-theme.tsx +58 -0
  246. package/src/hooks/use-controllable-state.ts +45 -0
  247. package/src/hooks/use-keyboard-state-sync.tsx +147 -0
  248. package/src/hooks/use-navigation-theme.ts +78 -0
  249. package/src/hooks/use-theme-color.ts +44 -0
  250. package/src/icons/central.ts +1 -0
  251. package/src/lib/cn.test.ts +136 -0
  252. package/src/lib/cn.ts +29 -0
  253. package/src/lib/color.test.ts +80 -0
  254. package/src/lib/color.ts +79 -0
  255. package/src/lib/compose-refs.test.ts +64 -0
  256. package/src/lib/compose-refs.ts +37 -0
  257. package/src/lib/keyboard-animation.test.ts +33 -0
  258. package/src/lib/keyboard-animation.ts +28 -0
  259. package/src/lib/merge-props.test.ts +82 -0
  260. package/src/lib/merge-props.ts +47 -0
  261. package/src/lib/navigation-theme.test.ts +49 -0
  262. package/src/lib/navigation-theme.ts +53 -0
  263. package/src/lib/slot.tsx +45 -0
  264. package/src/lib/tv.ts +20 -0
  265. package/src/styles/base.css +2 -0
  266. package/src/styles/geometry.test.ts +117 -0
  267. package/src/styles/index.css +3 -0
  268. package/src/styles/theme-tokens.test.ts +252 -0
  269. package/src/styles/theme.css +447 -0
  270. package/src/styles/tokens.css +132 -0
  271. package/src/styles/tokens.test.ts +181 -0
  272. package/src/styles/tokens.ts +86 -0
  273. 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";