@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 +1 -1
- package/package.json +2 -2
- package/src/components/bottom-sheet/AGENTS.md +10 -0
- package/src/components/bottom-sheet/bottom-sheet-container.tsx +4 -3
- package/src/components/bottom-sheet/bottom-sheet-footer.tsx +6 -4
- package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +20 -10
- package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +17 -0
- package/src/components/bottom-sheet/bottom-sheet.variants.ts +21 -0
- package/src/components/chart/AGENTS.md +1 -1
- package/src/components/screen/screen-navbar-back-button.tsx +1 -1
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
|
+
"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
|
|
128
|
-
//
|
|
129
|
-
//
|
|
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
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
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
|
|
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 {
|
|
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
|
|
60
|
-
*
|
|
61
|
-
* height
|
|
62
|
-
*
|
|
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
|
-
|
|
81
|
-
|
|
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:
|
|
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.
|