@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,59 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import Animated, { useAnimatedStyle } from "react-native-reanimated";
4
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
5
+ import { useScreenPart } from "./screen.context";
6
+ import { resolveScreenViewPadding, screenVariants } from "./screen.variants";
7
+
8
+ export type ScreenViewProps = ViewProps & {
9
+ className?: string;
10
+ children?: ReactNode;
11
+ };
12
+
13
+ /**
14
+ * A non-scrolling container that keeps its content clear of the screen's chrome.
15
+ *
16
+ * Two nested boxes, and the split is the whole point: the measured chrome
17
+ * clearance is an inline style on the OUTER one, the caller's `className` and
18
+ * `style` go on the inner one. On a single box they would not stack — uniwind
19
+ * puts a class's style first and the `style` prop second, and Yoga resolves a
20
+ * longhand `paddingTop` ahead of the `padding` a class compiles to — so
21
+ * `<Screen.View className="p-5">` kept its left and right padding and silently
22
+ * lost its top and bottom. Nested, they add, which is what a caller means.
23
+ *
24
+ * The padding is an animated style rather than a class because both numbers are
25
+ * measured at runtime — an overlay navbar and footer publish their heights into
26
+ * the screen context, and reading them on the UI thread means the padding lands
27
+ * in the same frame as the measurement instead of a re-render later.
28
+ *
29
+ * With nothing mounted on an edge it falls back to the raw safe-area inset, so
30
+ * a screen with no navbar still clears the notch. A `static` navbar or footer
31
+ * contributes nothing here: it already took its space in the flow.
32
+ *
33
+ * Use `Screen.ScrollArea` when the content can overflow — this one cannot
34
+ * scroll, so content taller than the viewport is simply clipped.
35
+ */
36
+ export function ScreenView({ className, style, children, ...props }: ScreenViewProps): ReactElement {
37
+ const { navbar, footer } = useScreenPart("Screen.View");
38
+ const { top, bottom } = useSafeAreaInsets();
39
+
40
+ const animatedStyle = useAnimatedStyle(() => {
41
+ return resolveScreenViewPadding({
42
+ footerHeight: footer.height.value,
43
+ footerPlacement: footer.placement.value,
44
+ navbarHeight: navbar.height.value,
45
+ navbarPlacement: navbar.placement.value,
46
+ safeAreaBottom: bottom,
47
+ safeAreaTop: top,
48
+ });
49
+ }, [top, bottom]);
50
+
51
+ return (
52
+ <Animated.View className="flex-1" style={animatedStyle}>
53
+ <View className={screenVariants().view({ className })} style={style} {...props}>
54
+ {children}
55
+ </View>
56
+ </Animated.View>
57
+ );
58
+ }
59
+ ScreenView.displayName = "DelacourUI.Screen.View";
@@ -0,0 +1,167 @@
1
+ import { createContext, type ReactElement, type ReactNode, use, useMemo } from "react";
2
+ import { type SharedValue, useSharedValue } from "react-native-reanimated";
3
+ import type { ScreenPlacement } from "./screen.variants";
4
+
5
+ /** What a navbar publishes about itself, for anything that has to clear it. */
6
+ export type ScreenNavbarMeasurements = {
7
+ /** Whether the navbar overlays the content or sits in the flow above it. */
8
+ placement: SharedValue<ScreenPlacement>;
9
+ /** Measured height including the safe-area band, or 0 with no navbar mounted. */
10
+ height: SharedValue<number>;
11
+ };
12
+
13
+ /** What a footer publishes about itself, for anything that has to clear it. */
14
+ export type ScreenFooterMeasurements = {
15
+ /** Whether the footer overlays the content or sits in the flow below it. */
16
+ placement: SharedValue<ScreenPlacement>;
17
+ /** Content height only — excludes the footer's own padding and safe-area band. */
18
+ height: SharedValue<number>;
19
+ /**
20
+ * First measured content height — the collapsed baseline; 0 until the footer
21
+ * mounts. Lets a consumer split the live height into base plus growth.
22
+ */
23
+ initialHeight: SharedValue<number>;
24
+ /**
25
+ * Extra VISUAL height overlaying above the measured footer layout — a
26
+ * composer pill that expands upward without changing its layout box, say.
27
+ *
28
+ * Driven as a pure UI-thread animation by the footer's own content (see
29
+ * `useScreenFooterOverlayHeight`) so a scrollable can offset for it in the
30
+ * same frame as the keyboard. Never measured via `onLayout`: a layout pass
31
+ * lands a frame late, which reads as the list lagging the keyboard.
32
+ *
33
+ * The footer resets `height` and `initialHeight` when it unmounts but never
34
+ * this — whoever drives it owns clearing it.
35
+ */
36
+ overlayHeight: SharedValue<number>;
37
+ };
38
+
39
+ /** Everything a screen's parts share, all of it readable from the UI thread. */
40
+ export type ScreenContextValue = {
41
+ navbar: ScreenNavbarMeasurements;
42
+ footer: ScreenFooterMeasurements;
43
+ /** Live scroll offset of whichever scrollable the screen holds. */
44
+ scrollY: SharedValue<number>;
45
+ /** Content height of that scrollable. */
46
+ contentHeight: SharedValue<number>;
47
+ /** Viewport height of that scrollable. */
48
+ layoutHeight: SharedValue<number>;
49
+ /** Paint the screen's layout layers in debug colours. */
50
+ debug: boolean;
51
+ };
52
+
53
+ const ScreenContext = createContext<ScreenContextValue | null>(null);
54
+
55
+ export type ScreenProviderProps = {
56
+ /**
57
+ * An enclosing screen's context. When present it is passed straight through
58
+ * rather than shadowed, so a nested `<Screen>` — which `Screen.Loading` and
59
+ * `Screen.Error` both produce — keeps reporting to the outer screen's navbar
60
+ * and footer measurements instead of starting a second, unread set.
61
+ */
62
+ parentContext?: ScreenContextValue | null;
63
+ /** Paint the screen's layout layers in debug colours. */
64
+ debug?: boolean;
65
+ children: ReactNode;
66
+ };
67
+
68
+ /**
69
+ * Creates the shared values a screen's parts measure themselves into.
70
+ *
71
+ * Every value is a Reanimated `SharedValue` rather than React state: a navbar's
72
+ * height reaching a scrollable's inset through a re-render would land a frame
73
+ * or two after the layout that produced it, and the content would visibly jump
74
+ * into place. On the UI thread the two happen together.
75
+ *
76
+ * Lives in its own module, importing nothing but `screen.variants`, so a part
77
+ * can read the context without importing `../screen` and closing a cycle
78
+ * through `screen.tsx`. See AGENTS.md rule 3.
79
+ */
80
+ export function ScreenProvider({ parentContext = null, debug = false, children }: ScreenProviderProps): ReactElement {
81
+ const navbarPlacement = useSharedValue<ScreenPlacement>("overlay");
82
+ const navbarHeight = useSharedValue<number>(0);
83
+
84
+ const footerPlacement = useSharedValue<ScreenPlacement>("overlay");
85
+ const footerHeight = useSharedValue<number>(0);
86
+ const footerInitialHeight = useSharedValue<number>(0);
87
+ const footerOverlayHeight = useSharedValue<number>(0);
88
+
89
+ const scrollY = useSharedValue<number>(0);
90
+ const contentHeight = useSharedValue<number>(0);
91
+ const layoutHeight = useSharedValue<number>(0);
92
+
93
+ const value = useMemo<ScreenContextValue>(() => {
94
+ if (parentContext) return parentContext;
95
+
96
+ return {
97
+ navbar: { placement: navbarPlacement, height: navbarHeight },
98
+ footer: {
99
+ placement: footerPlacement,
100
+ height: footerHeight,
101
+ initialHeight: footerInitialHeight,
102
+ overlayHeight: footerOverlayHeight,
103
+ },
104
+ scrollY,
105
+ contentHeight,
106
+ layoutHeight,
107
+ debug,
108
+ };
109
+ }, [
110
+ parentContext,
111
+ navbarPlacement,
112
+ navbarHeight,
113
+ footerPlacement,
114
+ footerHeight,
115
+ footerInitialHeight,
116
+ footerOverlayHeight,
117
+ scrollY,
118
+ contentHeight,
119
+ layoutHeight,
120
+ debug,
121
+ ]);
122
+
123
+ return <ScreenContext value={value}>{children}</ScreenContext>;
124
+ }
125
+ ScreenProvider.displayName = "DelacourUI.Screen.Provider";
126
+
127
+ /** The enclosing screen's context, or null outside a `<Screen>`. */
128
+ export function useScreenContext(): ScreenContextValue | null {
129
+ return use(ScreenContext);
130
+ }
131
+
132
+ /**
133
+ * Reads the enclosing screen's navbar and footer measurements.
134
+ *
135
+ * Lets a custom scrollable or a footer's own content clear the screen's chrome
136
+ * without the screen having to pass heights down through every slot. Throws
137
+ * outside a `<Screen>` — use {@link useScreenContext} where the enclosing
138
+ * screen is optional.
139
+ */
140
+ export function useScreen(): ScreenContextValue {
141
+ const context = useScreenContext();
142
+ if (!context) {
143
+ throw new Error("useScreen must be called inside a <Screen>.");
144
+ }
145
+ return context;
146
+ }
147
+
148
+ /**
149
+ * The enclosing screen's context, for a compound part that cannot work without
150
+ * one.
151
+ *
152
+ * Internal: deliberately not re-exported from `index.ts`. A caller outside the
153
+ * library wants {@link useScreen}, whose error message names the hook rather
154
+ * than a part.
155
+ */
156
+ export function useScreenPart(component: string): ScreenContextValue {
157
+ const context = useScreenContext();
158
+ if (!context) {
159
+ throw new Error(`${component} must be rendered inside a <Screen>.`);
160
+ }
161
+ return context;
162
+ }
163
+
164
+ /** Whether the enclosing screen has layout debugging turned on. */
165
+ export function useScreenDebug(): boolean {
166
+ return useScreenContext()?.debug ?? false;
167
+ }
@@ -0,0 +1,114 @@
1
+ import { ScreenChatList } from "./screen-chat-list";
2
+ import { ScreenContent } from "./screen-content";
3
+ import { ScreenError } from "./screen-error";
4
+ import { ScreenFlatList } from "./screen-flat-list";
5
+ import { ScreenFooter } from "./screen-footer";
6
+ import { ScreenHeader } from "./screen-header";
7
+ import { ScreenLegendList } from "./screen-legend-list";
8
+ import { ScreenLoading } from "./screen-loading";
9
+ import { ScreenNavbar } from "./screen-navbar";
10
+ import { ScreenRoot } from "./screen-root";
11
+ import { ScreenScrollArea } from "./screen-scroll-area";
12
+ import { ScreenScrollShadow } from "./screen-scroll-shadow";
13
+ import { ScreenSectionList } from "./screen-section-list";
14
+ import { ScreenView } from "./screen-view";
15
+
16
+ /**
17
+ * A screen's frame: pinned chrome, a content region, and whatever scrolls
18
+ * between them.
19
+ *
20
+ * The problem it solves is that a navbar and a footer only know their own
21
+ * heights once they have laid out, and everything else on the screen has to
22
+ * clear them. Each publishes its measured height into a Reanimated context, and
23
+ * every scrollable reserves exactly that — so no screen carries a hand-tuned
24
+ * padding number that is right on one device and wrong on the next.
25
+ *
26
+ * Compose the parts in visual order. The navbar and footer default to
27
+ * `placement="overlay"`, floating above the content with the content insetting
28
+ * itself to match; `placement="static"` puts either in the flow instead.
29
+ *
30
+ * Requires `DelacourProvider` at the app's root — it mounts the safe-area
31
+ * provider, the keyboard provider and the `<KeyboardStateSync />` a screen
32
+ * depends on. Compose those by hand only in an app that already has a root
33
+ * stack of its own, and keep the state sync: without it a keyboard that
34
+ * vanishes without notice leaves every screen in the app believing it is still
35
+ * open.
36
+ *
37
+ * @example
38
+ * // Navbar, scrolling body and a footer action.
39
+ * <Screen>
40
+ * <Screen.Navbar>
41
+ * <Screen.Navbar.BackButton onPress={() => router.back()}>
42
+ * <Screen.Navbar.Title>Settings</Screen.Navbar.Title>
43
+ * </Screen.Navbar.BackButton>
44
+ * </Screen.Navbar>
45
+ * <Screen.ScrollArea contentContainerClassName="gap-4 px-5">
46
+ * <Screen.Header>
47
+ * <Text.Header>Notifications</Text.Header>
48
+ * </Screen.Header>
49
+ * {rows}
50
+ * </Screen.ScrollArea>
51
+ * <Screen.Footer>
52
+ * <Button onPress={save}>Save</Button>
53
+ * </Screen.Footer>
54
+ * </Screen>
55
+ *
56
+ * @example
57
+ * // A form: the focused field stays clear of the keyboard and the footer.
58
+ * <Screen>
59
+ * <Screen.Navbar placement="static" center={<Screen.Navbar.Title>New customer</Screen.Navbar.Title>} />
60
+ * <Screen.ScrollArea keyboardAware contentContainerClassName="gap-4 px-5">
61
+ * <CustomerForm />
62
+ * </Screen.ScrollArea>
63
+ * </Screen>
64
+ *
65
+ * @example
66
+ * // Static content, no scrolling — padded for whatever chrome is mounted.
67
+ * <Screen>
68
+ * <Screen.Navbar />
69
+ * <Screen.View className="items-center justify-center">
70
+ * <Text>Nothing here yet</Text>
71
+ * </Screen.View>
72
+ * </Screen>
73
+ *
74
+ * @example
75
+ * // A conversation: composer rides the keyboard, newest message stays clear of it.
76
+ * <Screen>
77
+ * <Screen.Navbar placement="static" />
78
+ * <Screen.Content textInputNativeID={SCREEN_CHAT_INPUT_NATIVE_ID}>
79
+ * <Screen.ChatList composerBaseHeight={56} data={messages} renderItem={renderMessage} />
80
+ * </Screen.Content>
81
+ * <Screen.Footer sticky>
82
+ * <MessageComposer textInputNativeID={SCREEN_CHAT_INPUT_NATIVE_ID} />
83
+ * </Screen.Footer>
84
+ * </Screen>
85
+ */
86
+ export const Screen = Object.assign(ScreenRoot, {
87
+ /** The bar pinned to the top, with its `Title`, `Subtitle` and `BackButton` slots. */
88
+ Navbar: ScreenNavbar,
89
+ /** The content region: keyboard gesture area, surface, and optional safe-area edges. */
90
+ Content: ScreenContent,
91
+ /** A non-scrolling body, padded for whatever chrome is mounted. */
92
+ View: ScreenView,
93
+ /** A titled block on the screen's own gutter, scrolling with the content. */
94
+ Header: ScreenHeader,
95
+ /** A pinned region at the bottom, optionally riding the keyboard. */
96
+ Footer: ScreenFooter,
97
+ /** A scrolling body that clears the chrome at both ends. */
98
+ ScrollArea: ScreenScrollArea,
99
+ /** A fade at each end of whichever body is mounted, for content that runs past it. */
100
+ ScrollShadow: ScreenScrollShadow,
101
+ /** A virtualised list that clears the chrome at both ends. */
102
+ FlatList: ScreenFlatList,
103
+ /** A sectioned virtualised list, with sticky section headers. */
104
+ SectionList: ScreenSectionList,
105
+ /** A recycling list, for a long or heterogeneous body. */
106
+ LegendList: ScreenLegendList,
107
+ /** A conversation list: composer clearance, keyboard lift and end anchoring. */
108
+ ChatList: ScreenChatList,
109
+ /** A whole screen showing a spinner, for a route whose data is in flight. */
110
+ Loading: ScreenLoading,
111
+ /** A whole screen explaining that something failed. */
112
+ Error: ScreenError,
113
+ displayName: "DelacourUI.Screen",
114
+ });
@@ -0,0 +1,66 @@
1
+ import type { ReactNode } from "react";
2
+ import type { ScrollView, ViewProps } from "react-native";
3
+ import type { ScreenEdge, ScreenPlacement } from "./screen.variants";
4
+
5
+ /**
6
+ * The instance `Screen.ScrollArea` hands back through its `ref`.
7
+ *
8
+ * Named rather than left inline because a caller needs to say it to declare the
9
+ * ref in the first place — `useAnimatedRef<ScreenScrollViewRef>()` for a scroll
10
+ * driven from the UI thread, `useRef<ScreenScrollViewRef>(null)` for one driven
11
+ * from JS. Both reach the same `scrollTo`.
12
+ *
13
+ * React Native's own `ScrollView` instance, which is what both engines actually
14
+ * hand back — `Animated.ScrollView` forwards to it, and
15
+ * `KeyboardAwareScrollViewRef` is defined as that same instance plus one method.
16
+ *
17
+ * Not `ComponentRef<typeof Animated.ScrollView>`: Reanimated's animated
18
+ * component types carry no `RefAttributes`, so that expression collapses to
19
+ * `never` — which still compiles everywhere, silently accepts any ref, and
20
+ * gives a caller a `.current` it can do nothing with.
21
+ */
22
+ export type ScreenScrollViewRef = ScrollView;
23
+
24
+ /**
25
+ * The shape of a container that can inset itself against the safe area.
26
+ *
27
+ * Shared by `Screen.Content` and `Screen.Header`, so it lives in a leaf rather
28
+ * than in one of them arbitrarily.
29
+ */
30
+ export type ScreenInsetProps = ViewProps & {
31
+ /** Safe-area edges to pad against. None by default. */
32
+ insets?: readonly ScreenEdge[];
33
+ className?: string;
34
+ children?: ReactNode;
35
+ };
36
+
37
+ /**
38
+ * The shape of a part that either overlays the content or sits in the flow.
39
+ *
40
+ * Shared by `Screen.Navbar` and `Screen.Footer` — the two parts a scrollable
41
+ * has to clear, and the reason `placement` is one axis rather than a boolean on
42
+ * each of them.
43
+ */
44
+ export type ScreenPlacementProps = ViewProps & {
45
+ /**
46
+ * `overlay` floats the part over the content, which then insets itself by
47
+ * the measured height. `static` puts it in the flow, taking its own space.
48
+ */
49
+ placement?: ScreenPlacement;
50
+ className?: string;
51
+ children?: ReactNode;
52
+ };
53
+
54
+ /**
55
+ * What every Screen scrollable accepts on top of its own list props.
56
+ *
57
+ * `header` is separate from a list's `ListHeaderComponent` on purpose: the
58
+ * screen composes its navbar spacer, this header and the caller's list header
59
+ * in that order, so a caller can use both without having to interleave them.
60
+ */
61
+ export type ScreenScrollableProps = {
62
+ /** Rendered above the content, below the navbar spacer. */
63
+ header?: ReactNode;
64
+ className?: string;
65
+ contentContainerClassName?: string;
66
+ };