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

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 CHANGED
@@ -37,7 +37,7 @@ never makes Metro resolve its peers.
37
37
 
38
38
  ```css
39
39
  @import '@delacour/react-native-ui/styles';
40
- @source '../../../../packages/native-ui/src';
40
+ @source '../../../../packages/react-native-ui/src';
41
41
  ```
42
42
 
43
43
  Use the real workspace path, not a `node_modules` one — Bun symlinks workspace
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@delacour/react-native-ui",
3
- "version": "0.1.0-alpha.3",
3
+ "version": "0.1.0-alpha.4",
4
4
  "description": "React Native UI components \u2014 Uniwind, Reanimated, Gesture Handler",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/delacournz/delacour-ui.git",
10
- "directory": "packages/native-ui"
10
+ "directory": "packages/react-native-ui"
11
11
  },
12
12
  "keywords": [
13
13
  "react-native",
@@ -119,6 +119,16 @@ library's first overlay. Compound root plus `BottomSheet.Trigger`,
119
119
  lands in front of it and silently doubles `BOTTOM_SHEET_FOOTER_GAP`. The
120
120
  container owns the shared value because the footer and the body are siblings in
121
121
  gorhom's tree and neither can see the other.
122
+ - **`BottomSheet.ScrollView` gives the footer's height up as its own frame, not
123
+ as a spacer.** A spacer inside the content leaves the scroll view running the
124
+ sheet's full height, so its indicator and its overscroll bounce carry on
125
+ underneath the footer and the bottom of the track is hidden behind the buttons.
126
+ So the scroll view takes the same shared value as an animated `marginBottom`,
127
+ ends at the footer's hairline, and its content keeps only the gap —
128
+ `resolveSheetScrollEndPadding`. `BottomSheet.Content` keeps the spacer: it does
129
+ not scroll, so there is no indicator to stop. Confirmed on a simulator by
130
+ `measureInWindow` — the scroll view's bottom and the footer's top land on the
131
+ same point.
122
132
  - **Do not put a `KeyboardStickyView` inside a sheet.** This is the one keyboard
123
133
  rule that differs from [`Screen`](../screen/AGENTS.md). gorhom's `animatedFooterPosition` already
124
134
  carries a pinned footer clear of the keyboard and the translate above gives up
@@ -124,9 +124,10 @@ export function BottomSheetContainer({
124
124
  const hasStickyFooter = placement === "sticky";
125
125
  const overlay = portal?.overlay ?? null;
126
126
 
127
- // Written by the pinned footer's own onLayout, read by the body's spacer. It
128
- // lives here because the two are siblings in gorhom's tree and neither can see
129
- // the other — the footer is rendered outside the children this file wraps.
127
+ // Written by the pinned footer's own onLayout, read by the body — the
128
+ // content's spacer, or the scroll view's margin. It lives here because the two
129
+ // are siblings in gorhom's tree and neither can see the other — the footer is
130
+ // rendered outside the children this file wraps.
130
131
  const footerHeight = useSharedValue(0);
131
132
 
132
133
  const containerValue = useMemo(() => ({ footerHeight, hasStickyFooter }), [footerHeight, hasStickyFooter]);
@@ -64,10 +64,12 @@ export type BottomSheetFooterProps = Omit<ViewProps, "children"> & {
64
64
  * lets the body's reserve shrink with it instead of over-reserving by the height
65
65
  * of a home indicator that is no longer there.
66
66
  *
67
- * It publishes its measured height into the sheet, and `BottomSheet.Content` and
68
- * `BottomSheet.ScrollView` reserve exactly that plus a gap. The height is a
69
- * shared value read by an animated spacer, never content-container padding —
70
- * `Screen` reserves its own chrome the same way and for the same reason.
67
+ * It publishes its measured height into the sheet. `BottomSheet.Content` reserves
68
+ * exactly that plus a gap with a spacer, and `BottomSheet.ScrollView` gives it up
69
+ * as its own frame so its scroll indicator stops at this footer's hairline. The
70
+ * height is a shared value read by an animated style either way, never
71
+ * content-container padding — `Screen` reserves its own chrome the same way and
72
+ * for the same reason.
71
73
  *
72
74
  * **Do not put a `KeyboardStickyView` in here.** gorhom's `animatedFooterPosition`
73
75
  * already carries the footer clear of the keyboard; this translate only gives up
@@ -1,12 +1,12 @@
1
1
  import { BottomSheetScrollView as GorhomBottomSheetScrollView } from "@gorhom/bottom-sheet";
2
2
  import type { ComponentProps, ReactElement, ReactNode } from "react";
3
3
  import { View } from "react-native";
4
- import Animated, { useAnimatedStyle } from "react-native-reanimated";
4
+ import { useAnimatedStyle } from "react-native-reanimated";
5
5
  import { useSafeAreaInsets } from "react-native-safe-area-context";
6
6
  import { withUniwind } from "uniwind";
7
7
  import { cn } from "../../lib/cn";
8
8
  import { useBottomSheetContainerContext } from "./bottom-sheet.context";
9
- import { BOTTOM_SHEET_FOOTER_GAP, bottomSheetVariants, resolveSheetBottomInset } from "./bottom-sheet.variants";
9
+ import { bottomSheetVariants, resolveSheetScrollEndPadding } from "./bottom-sheet.variants";
10
10
 
11
11
  type GorhomScrollViewProps = ComponentProps<typeof GorhomBottomSheetScrollView>;
12
12
 
@@ -56,10 +56,16 @@ export type BottomSheetScrollViewProps = Omit<
56
56
  * *array* alongside any `contentContainerStyle`, and the two then fight over the
57
57
  * one style this component has to own — the safe-area band. One writer for it.
58
58
  *
59
- * A pinned footer is reserved by a spacer at the end of the content rather than
60
- * by gorhom's `enableFooterMarginAdjustment`, which routes the footer's measured
61
- * height through React state and would commit a render on every frame of the
62
- * keyboard animation. See `BottomSheet.Content` for the whole reasoning.
59
+ * **A pinned footer shortens the scroll view; it is not reserved inside it.**
60
+ * The footer draws over the sheet, so a scroll view that ran the sheet's full
61
+ * height would carry its indicator and its overscroll bounce on underneath the
62
+ * footer, the bottom of the track hidden behind the buttons. So the frame gives
63
+ * up the footer's measured height as a `marginBottom` and ends at the footer's
64
+ * hairline, and the content keeps only the gap
65
+ * ({@link resolveSheetScrollEndPadding}). The margin is an animated style off
66
+ * the footer's shared value, never gorhom's `enableFooterMarginAdjustment`,
67
+ * which routes that height through React state and would commit a render on
68
+ * every frame of the keyboard animation — see `BottomSheet.Content`.
63
69
  *
64
70
  * @example
65
71
  * <BottomSheet.Container enableDynamicSizing={false} snapPoints={["60%", "90%"]}>
@@ -70,6 +76,7 @@ export function BottomSheetScrollView({
70
76
  className,
71
77
  contentContainerClassName,
72
78
  children,
79
+ style,
73
80
  ...props
74
81
  }: BottomSheetScrollViewProps): ReactElement {
75
82
  const container = useBottomSheetContainerContext();
@@ -77,18 +84,21 @@ export function BottomSheetScrollView({
77
84
  const hasStickyFooter = container?.hasStickyFooter ?? false;
78
85
  const footerHeight = container?.footerHeight;
79
86
 
80
- const reserve = useAnimatedStyle(() => ({
81
- height: footerHeight === undefined ? 0 : footerHeight.value + BOTTOM_SHEET_FOOTER_GAP,
87
+ // The footer's height, band included, as the frame's own margin — so the
88
+ // indicator's track ends where the footer begins and follows it as the
89
+ // keyboard takes the band back.
90
+ const aboveFooter = useAnimatedStyle(() => ({
91
+ marginBottom: hasStickyFooter && footerHeight !== undefined ? footerHeight.value : 0,
82
92
  }));
83
93
 
84
94
  return (
85
95
  <StyledScrollView
86
96
  className={cn(className)}
87
- contentContainerStyle={{ paddingBottom: resolveSheetBottomInset({ bottom, hasStickyFooter }) }}
97
+ contentContainerStyle={{ paddingBottom: resolveSheetScrollEndPadding({ bottom, hasStickyFooter }) }}
88
98
  {...props}
99
+ style={[aboveFooter, style]}
89
100
  >
90
101
  <View className={bottomSheetVariants().scrollContent({ className: contentContainerClassName })}>{children}</View>
91
- {hasStickyFooter ? <Animated.View style={reserve} /> : null}
92
102
  </StyledScrollView>
93
103
  );
94
104
  }
@@ -11,6 +11,7 @@ import {
11
11
  bottomSheetVariants,
12
12
  resolveFooterPlacement,
13
13
  resolveSheetBottomInset,
14
+ resolveSheetScrollEndPadding,
14
15
  } from "./bottom-sheet.variants";
15
16
 
16
17
  const LIGHT = declaredTokens("light");
@@ -183,6 +184,22 @@ describe("resolveSheetBottomInset", () => {
183
184
  });
184
185
  });
185
186
 
187
+ describe("resolveSheetScrollEndPadding", () => {
188
+ test("gives the safe-area band to the content when nothing is pinned below it", () => {
189
+ expect(resolveSheetScrollEndPadding({ bottom: 34, hasStickyFooter: false })).toBe(34);
190
+ });
191
+
192
+ test("holds the content off a pinned footer by the gap alone", () => {
193
+ // The scroll view's frame already stops at the footer's top edge, so the
194
+ // footer's height — band included — is not the content's to reserve.
195
+ expect(resolveSheetScrollEndPadding({ bottom: 34, hasStickyFooter: true })).toBe(BOTTOM_SHEET_FOOTER_GAP);
196
+ });
197
+
198
+ test("keeps the gap on a device with no bottom inset", () => {
199
+ expect(resolveSheetScrollEndPadding({ bottom: 0, hasStickyFooter: true })).toBe(BOTTOM_SHEET_FOOTER_GAP);
200
+ });
201
+ });
202
+
186
203
  describe("bottomSheetVariants slots", () => {
187
204
  test("the title slot carries layout only, never type", () => {
188
205
  // The type comes from the `Text.Header` the part renders. A `text-lg` here
@@ -197,3 +197,24 @@ export function resolveSheetBottomInset({
197
197
  }): number {
198
198
  return hasStickyFooter ? 0 : bottom;
199
199
  }
200
+
201
+ /**
202
+ * The padding at the end of `BottomSheet.ScrollView`'s content.
203
+ *
204
+ * A scroll view does not reserve a pinned footer the way `BottomSheet.Content`
205
+ * does — its FRAME stops at the footer's top edge instead, so the scroll
206
+ * indicator and the overscroll bounce end there too rather than running on
207
+ * underneath it. That leaves the content only the gap to hold it off the
208
+ * footer's hairline. With no pinned footer the content is the bottom-most thing
209
+ * in the sheet and takes the safe-area band, as {@link resolveSheetBottomInset}
210
+ * has it.
211
+ */
212
+ export function resolveSheetScrollEndPadding({
213
+ hasStickyFooter,
214
+ bottom,
215
+ }: {
216
+ hasStickyFooter: boolean;
217
+ bottom: number;
218
+ }): number {
219
+ return hasStickyFooter ? BOTTOM_SHEET_FOOTER_GAP : resolveSheetBottomInset({ bottom, hasStickyFooter });
220
+ }
@@ -1,7 +1,7 @@
1
1
  # Chart
2
2
 
3
3
  Skia charts wearing the theme's five-colour series ramp — the skin over
4
- [`@delacour/react-native-charts`](../../../../charts/AGENTS.md), which does the drawing and
4
+ [`@delacour/react-native-charts`](../../../../react-native-charts/AGENTS.md), which does the drawing and
5
5
  knows nothing about tokens.
6
6
 
7
7
  `import { Chart, PieChart } from "@delacour/react-native-ui/chart";`
@@ -32,7 +32,7 @@ const GLYPH: Record<ScreenBackButtonGlyph, typeof IconChevronLeft> = {
32
32
  /**
33
33
  * The navbar's leading control.
34
34
  *
35
- * Takes an `onPress` rather than calling a router itself. `native-ui` has no
35
+ * Takes an `onPress` rather than calling a router itself. `react-native-ui` has no
36
36
  * navigation dependency and should not gain one for a chevron — the screen
37
37
  * already knows how it was pushed, and an app on a different router still gets
38
38
  * to use this.