@delacour/react-native-ui 0.1.0-alpha.20260925053522
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -0
- package/package.json +125 -0
- package/src/components/accordion/AGENTS.md +193 -0
- package/src/components/accordion/accordion-content.tsx +125 -0
- package/src/components/accordion/accordion-description.tsx +17 -0
- package/src/components/accordion/accordion-indicator.tsx +104 -0
- package/src/components/accordion/accordion-item.tsx +78 -0
- package/src/components/accordion/accordion-title.tsx +18 -0
- package/src/components/accordion/accordion-trigger.tsx +148 -0
- package/src/components/accordion/accordion.context.tsx +153 -0
- package/src/components/accordion/accordion.tsx +253 -0
- package/src/components/accordion/accordion.types.ts +11 -0
- package/src/components/accordion/accordion.variants.test.ts +434 -0
- package/src/components/accordion/accordion.variants.ts +358 -0
- package/src/components/accordion/index.ts +45 -0
- package/src/components/badge/AGENTS.md +83 -0
- package/src/components/badge/badge-close-button.tsx +48 -0
- package/src/components/badge/badge-end-content.tsx +15 -0
- package/src/components/badge/badge-label.tsx +24 -0
- package/src/components/badge/badge-start-content.tsx +16 -0
- package/src/components/badge/badge.context.tsx +64 -0
- package/src/components/badge/badge.tsx +192 -0
- package/src/components/badge/badge.types.ts +10 -0
- package/src/components/badge/badge.variants.test.ts +269 -0
- package/src/components/badge/badge.variants.ts +238 -0
- package/src/components/badge/index.ts +17 -0
- package/src/components/bottom-sheet/AGENTS.md +242 -0
- package/src/components/bottom-sheet/bottom-sheet-background.tsx +35 -0
- package/src/components/bottom-sheet/bottom-sheet-close.tsx +60 -0
- package/src/components/bottom-sheet/bottom-sheet-container.tsx +246 -0
- package/src/components/bottom-sheet/bottom-sheet-content.tsx +66 -0
- package/src/components/bottom-sheet/bottom-sheet-description.tsx +29 -0
- package/src/components/bottom-sheet/bottom-sheet-footer.tsx +150 -0
- package/src/components/bottom-sheet/bottom-sheet-handle.tsx +47 -0
- package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +180 -0
- package/src/components/bottom-sheet/bottom-sheet-portal.tsx +72 -0
- package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +105 -0
- package/src/components/bottom-sheet/bottom-sheet-title.tsx +24 -0
- package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +73 -0
- package/src/components/bottom-sheet/bottom-sheet.context.tsx +135 -0
- package/src/components/bottom-sheet/bottom-sheet.tsx +125 -0
- package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +293 -0
- package/src/components/bottom-sheet/bottom-sheet.variants.ts +220 -0
- package/src/components/bottom-sheet/index.ts +40 -0
- package/src/components/bottom-sheet/use-bottom-sheet-input.ts +125 -0
- package/src/components/button/AGENTS.md +217 -0
- package/src/components/button/button-end-content.tsx +15 -0
- package/src/components/button/button-group-separator.tsx +37 -0
- package/src/components/button/button-group-text.tsx +101 -0
- package/src/components/button/button-group.tsx +158 -0
- package/src/components/button/button-label.tsx +24 -0
- package/src/components/button/button-start-content.tsx +15 -0
- package/src/components/button/button.context.tsx +187 -0
- package/src/components/button/button.tsx +273 -0
- package/src/components/button/button.types.ts +10 -0
- package/src/components/button/button.variants.test.ts +811 -0
- package/src/components/button/button.variants.ts +480 -0
- package/src/components/button/index.ts +50 -0
- package/src/components/chart/AGENTS.md +210 -0
- package/src/components/chart/chart-area.tsx +93 -0
- package/src/components/chart/chart-bar.tsx +103 -0
- package/src/components/chart/chart-bars.tsx +75 -0
- package/src/components/chart/chart-candlestick.tsx +60 -0
- package/src/components/chart/chart-grid.tsx +33 -0
- package/src/components/chart/chart-legend.tsx +41 -0
- package/src/components/chart/chart-line.tsx +50 -0
- package/src/components/chart/chart-scatter.tsx +35 -0
- package/src/components/chart/chart-tooltip-dot.tsx +70 -0
- package/src/components/chart/chart-tooltip-x.tsx +121 -0
- package/src/components/chart/chart-tooltip-y.tsx +55 -0
- package/src/components/chart/chart-tooltip.tsx +133 -0
- package/src/components/chart/chart-x-axis.tsx +27 -0
- package/src/components/chart/chart-y-axis.tsx +21 -0
- package/src/components/chart/chart.context.tsx +127 -0
- package/src/components/chart/chart.tsx +448 -0
- package/src/components/chart/chart.types.ts +108 -0
- package/src/components/chart/chart.variants.test.ts +893 -0
- package/src/components/chart/chart.variants.ts +726 -0
- package/src/components/chart/index.ts +103 -0
- package/src/components/chart/pie-chart-center.tsx +40 -0
- package/src/components/chart/pie-chart-label.tsx +56 -0
- package/src/components/chart/pie-chart-slice.tsx +45 -0
- package/src/components/chart/pie-chart-tooltip.tsx +85 -0
- package/src/components/chart/pie-chart.context.tsx +77 -0
- package/src/components/chart/pie-chart.tsx +247 -0
- package/src/components/chart/use-chart-font.ts +25 -0
- package/src/components/chart/use-chart-palette.ts +38 -0
- package/src/components/checkbox/AGENTS.md +185 -0
- package/src/components/checkbox/checkbox-box.tsx +152 -0
- package/src/components/checkbox/checkbox-group.tsx +107 -0
- package/src/components/checkbox/checkbox-label.tsx +34 -0
- package/src/components/checkbox/checkbox.context.tsx +136 -0
- package/src/components/checkbox/checkbox.tsx +258 -0
- package/src/components/checkbox/checkbox.types.ts +14 -0
- package/src/components/checkbox/checkbox.variants.test.ts +634 -0
- package/src/components/checkbox/checkbox.variants.ts +484 -0
- package/src/components/checkbox/index.ts +42 -0
- package/src/components/field/AGENTS.md +112 -0
- package/src/components/field/field-content.tsx +24 -0
- package/src/components/field/field-description.tsx +29 -0
- package/src/components/field/field-error.tsx +61 -0
- package/src/components/field/field-group.tsx +22 -0
- package/src/components/field/field-label.tsx +47 -0
- package/src/components/field/field-legend.tsx +34 -0
- package/src/components/field/field-separator.tsx +81 -0
- package/src/components/field/field-set.tsx +22 -0
- package/src/components/field/field.context.tsx +95 -0
- package/src/components/field/field.tsx +157 -0
- package/src/components/field/field.types.ts +15 -0
- package/src/components/field/field.variants.test.ts +240 -0
- package/src/components/field/field.variants.ts +156 -0
- package/src/components/field/index.ts +22 -0
- package/src/components/icon/AGENTS.md +104 -0
- package/src/components/icon/icon.context.tsx +29 -0
- package/src/components/icon/icon.tsx +92 -0
- package/src/components/icon/icon.variants.test.ts +111 -0
- package/src/components/icon/icon.variants.ts +79 -0
- package/src/components/icon/index.ts +11 -0
- package/src/components/input/AGENTS.md +126 -0
- package/src/components/input/index.ts +27 -0
- package/src/components/input/input-group-decorator.tsx +84 -0
- package/src/components/input/input-group-prefix.tsx +16 -0
- package/src/components/input/input-group-suffix.tsx +15 -0
- package/src/components/input/input-group.tsx +140 -0
- package/src/components/input/input.context.tsx +87 -0
- package/src/components/input/input.tsx +184 -0
- package/src/components/input/input.types.ts +10 -0
- package/src/components/input/input.variants.test.ts +369 -0
- package/src/components/input/input.variants.ts +329 -0
- package/src/components/list-group/AGENTS.md +67 -0
- package/src/components/list-group/index.ts +20 -0
- package/src/components/list-group/list-group-item-content.tsx +10 -0
- package/src/components/list-group/list-group-item-description.tsx +12 -0
- package/src/components/list-group/list-group-item-prefix.tsx +30 -0
- package/src/components/list-group/list-group-item-suffix.tsx +55 -0
- package/src/components/list-group/list-group-item-title.tsx +17 -0
- package/src/components/list-group/list-group-item.tsx +87 -0
- package/src/components/list-group/list-group.context.tsx +65 -0
- package/src/components/list-group/list-group.tsx +116 -0
- package/src/components/list-group/list-group.types.ts +14 -0
- package/src/components/list-group/list-group.variants.test.ts +223 -0
- package/src/components/list-group/list-group.variants.ts +107 -0
- package/src/components/pressable/AGENTS.md +74 -0
- package/src/components/pressable/index.ts +9 -0
- package/src/components/pressable/pressable.tsx +261 -0
- package/src/components/pressable/pressable.variants.test.ts +128 -0
- package/src/components/pressable/pressable.variants.ts +80 -0
- package/src/components/provider/AGENTS.md +90 -0
- package/src/components/provider/index.ts +1 -0
- package/src/components/provider/provider.tsx +105 -0
- package/src/components/radio/AGENTS.md +245 -0
- package/src/components/radio/index.ts +35 -0
- package/src/components/radio/radio-group.tsx +126 -0
- package/src/components/radio/radio-indicator.tsx +91 -0
- package/src/components/radio/radio-label.tsx +39 -0
- package/src/components/radio/radio.context.tsx +132 -0
- package/src/components/radio/radio.tsx +215 -0
- package/src/components/radio/radio.variants.test.ts +580 -0
- package/src/components/radio/radio.variants.ts +271 -0
- package/src/components/screen/AGENTS.md +287 -0
- package/src/components/screen/index.ts +85 -0
- package/src/components/screen/screen-chat-list.tsx +485 -0
- package/src/components/screen/screen-content.tsx +69 -0
- package/src/components/screen/screen-debug.ts +43 -0
- package/src/components/screen/screen-error.tsx +61 -0
- package/src/components/screen/screen-flat-list.tsx +88 -0
- package/src/components/screen/screen-footer-background.tsx +60 -0
- package/src/components/screen/screen-footer.tsx +170 -0
- package/src/components/screen/screen-header.tsx +36 -0
- package/src/components/screen/screen-legend-list.tsx +105 -0
- package/src/components/screen/screen-list-component.tsx +23 -0
- package/src/components/screen/screen-loading.tsx +50 -0
- package/src/components/screen/screen-navbar-back-button.tsx +79 -0
- package/src/components/screen/screen-navbar-background.tsx +45 -0
- package/src/components/screen/screen-navbar-subtitle.tsx +26 -0
- package/src/components/screen/screen-navbar-title.tsx +31 -0
- package/src/components/screen/screen-navbar.tsx +154 -0
- package/src/components/screen/screen-root.tsx +38 -0
- package/src/components/screen/screen-scroll-area.tsx +116 -0
- package/src/components/screen/screen-scroll-shadow.tsx +186 -0
- package/src/components/screen/screen-section-list.tsx +81 -0
- package/src/components/screen/screen-view.tsx +59 -0
- package/src/components/screen/screen.context.tsx +167 -0
- package/src/components/screen/screen.tsx +114 -0
- package/src/components/screen/screen.types.ts +66 -0
- package/src/components/screen/screen.variants.test.ts +602 -0
- package/src/components/screen/screen.variants.ts +475 -0
- package/src/components/screen/use-screen-scroll-insets.ts +218 -0
- package/src/components/separator/AGENTS.md +30 -0
- package/src/components/separator/index.ts +8 -0
- package/src/components/separator/separator.tsx +85 -0
- package/src/components/slider/AGENTS.md +274 -0
- package/src/components/slider/index.ts +50 -0
- package/src/components/slider/slider-fill.tsx +68 -0
- package/src/components/slider/slider-output.tsx +55 -0
- package/src/components/slider/slider-thumb.tsx +193 -0
- package/src/components/slider/slider-track.tsx +233 -0
- package/src/components/slider/slider.context.tsx +118 -0
- package/src/components/slider/slider.tsx +321 -0
- package/src/components/slider/slider.types.ts +26 -0
- package/src/components/slider/slider.variants.test.ts +856 -0
- package/src/components/slider/slider.variants.ts +661 -0
- package/src/components/spinner/AGENTS.md +73 -0
- package/src/components/spinner/index.ts +17 -0
- package/src/components/spinner/spinner-arc.tsx +90 -0
- package/src/components/spinner/spinner-content.tsx +63 -0
- package/src/components/spinner/spinner.context.tsx +52 -0
- package/src/components/spinner/spinner.tsx +128 -0
- package/src/components/spinner/spinner.variants.test.ts +273 -0
- package/src/components/spinner/spinner.variants.ts +187 -0
- package/src/components/switch/AGENTS.md +213 -0
- package/src/components/switch/index.ts +42 -0
- package/src/components/switch/switch-content.tsx +111 -0
- package/src/components/switch/switch-end-content.tsx +18 -0
- package/src/components/switch/switch-start-content.tsx +20 -0
- package/src/components/switch/switch-thumb.tsx +102 -0
- package/src/components/switch/switch.context.tsx +79 -0
- package/src/components/switch/switch.tsx +423 -0
- package/src/components/switch/switch.types.ts +14 -0
- package/src/components/switch/switch.variants.test.ts +570 -0
- package/src/components/switch/switch.variants.ts +511 -0
- package/src/components/tabs/AGENTS.md +287 -0
- package/src/components/tabs/index.ts +69 -0
- package/src/components/tabs/tabs-content.tsx +65 -0
- package/src/components/tabs/tabs-indicator.tsx +97 -0
- package/src/components/tabs/tabs-label.tsx +62 -0
- package/src/components/tabs/tabs-list.tsx +139 -0
- package/src/components/tabs/tabs-pager.tsx +59 -0
- package/src/components/tabs/tabs-scroll-view.tsx +171 -0
- package/src/components/tabs/tabs-separator.tsx +73 -0
- package/src/components/tabs/tabs-trigger.tsx +210 -0
- package/src/components/tabs/tabs.context.tsx +294 -0
- package/src/components/tabs/tabs.tsx +435 -0
- package/src/components/tabs/tabs.types.ts +13 -0
- package/src/components/tabs/tabs.variants.test.ts +1020 -0
- package/src/components/tabs/tabs.variants.ts +670 -0
- package/src/components/text/AGENTS.md +95 -0
- package/src/components/text/index.ts +25 -0
- package/src/components/text/text.context.tsx +60 -0
- package/src/components/text/text.tsx +251 -0
- package/src/components/text/text.variants.test.ts +422 -0
- package/src/components/text/text.variants.ts +282 -0
- package/src/display-name.test.ts +145 -0
- package/src/docs.test.ts +98 -0
- package/src/expo/navigation-theme.tsx +58 -0
- package/src/hooks/use-controllable-state.ts +45 -0
- package/src/hooks/use-keyboard-state-sync.tsx +147 -0
- package/src/hooks/use-navigation-theme.ts +78 -0
- package/src/hooks/use-theme-color.ts +44 -0
- package/src/icons/central.ts +1 -0
- package/src/lib/cn.test.ts +136 -0
- package/src/lib/cn.ts +29 -0
- package/src/lib/color.test.ts +80 -0
- package/src/lib/color.ts +79 -0
- package/src/lib/compose-refs.test.ts +64 -0
- package/src/lib/compose-refs.ts +37 -0
- package/src/lib/keyboard-animation.test.ts +33 -0
- package/src/lib/keyboard-animation.ts +28 -0
- package/src/lib/merge-props.test.ts +82 -0
- package/src/lib/merge-props.ts +47 -0
- package/src/lib/navigation-theme.test.ts +49 -0
- package/src/lib/navigation-theme.ts +53 -0
- package/src/lib/slot.tsx +45 -0
- package/src/lib/tv.ts +20 -0
- package/src/styles/base.css +2 -0
- package/src/styles/geometry.test.ts +117 -0
- package/src/styles/index.css +3 -0
- package/src/styles/theme-tokens.test.ts +252 -0
- package/src/styles/theme.css +447 -0
- package/src/styles/tokens.css +132 -0
- package/src/styles/tokens.test.ts +181 -0
- package/src/styles/tokens.ts +86 -0
- package/src/uniwind-env.d.ts +1 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode, useCallback, useMemo, useRef } from "react";
|
|
2
|
+
import { useControllableState } from "../../hooks/use-controllable-state";
|
|
3
|
+
import { type BottomSheetContextValue, type BottomSheetHandle, BottomSheetProvider } from "./bottom-sheet.context";
|
|
4
|
+
import { BottomSheetClose } from "./bottom-sheet-close";
|
|
5
|
+
import { BottomSheetContainer } from "./bottom-sheet-container";
|
|
6
|
+
import { BottomSheetContent } from "./bottom-sheet-content";
|
|
7
|
+
import { BottomSheetDescription } from "./bottom-sheet-description";
|
|
8
|
+
import { BottomSheetFooter } from "./bottom-sheet-footer";
|
|
9
|
+
import { BottomSheetOverlay } from "./bottom-sheet-overlay";
|
|
10
|
+
import { BottomSheetPortal } from "./bottom-sheet-portal";
|
|
11
|
+
import { BottomSheetScrollView } from "./bottom-sheet-scroll-view";
|
|
12
|
+
import { BottomSheetTitle } from "./bottom-sheet-title";
|
|
13
|
+
import { BottomSheetTrigger } from "./bottom-sheet-trigger";
|
|
14
|
+
|
|
15
|
+
export type BottomSheetProps = {
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
/** Controlled open state. Leave it off and the sheet holds its own. */
|
|
18
|
+
isOpen?: boolean;
|
|
19
|
+
/** Initial open state while uncontrolled. */
|
|
20
|
+
defaultOpen?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Called whenever the sheet opens or closes — by a trigger, a swipe down, a
|
|
23
|
+
* press on the backdrop, `BottomSheet.Close`, or a controlled `isOpen`.
|
|
24
|
+
*
|
|
25
|
+
* One callback for every path, deliberately. gorhom's own `onClose` fires for
|
|
26
|
+
* the gesture alone, which is the shape that makes a caller wire three
|
|
27
|
+
* handlers and still miss one.
|
|
28
|
+
*/
|
|
29
|
+
onOpenChange?: (isOpen: boolean) => void;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function BottomSheetRoot({ children, defaultOpen = false, isOpen, onOpenChange }: BottomSheetProps): ReactElement {
|
|
33
|
+
const sheetRef = useRef<BottomSheetHandle | null>(null);
|
|
34
|
+
const [isSheetOpen, setOpen] = useControllableState<boolean>({
|
|
35
|
+
defaultValue: defaultOpen,
|
|
36
|
+
onChange: onOpenChange,
|
|
37
|
+
value: isOpen,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const open = useCallback(() => setOpen(true), [setOpen]);
|
|
41
|
+
const close = useCallback(() => setOpen(false), [setOpen]);
|
|
42
|
+
|
|
43
|
+
const context = useMemo<BottomSheetContextValue>(
|
|
44
|
+
() => ({ close, isOpen: isSheetOpen, open, setOpen, sheetRef }),
|
|
45
|
+
[close, isSheetOpen, open, setOpen]
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return <BottomSheetProvider value={context}>{children}</BottomSheetProvider>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A panel that slides up from the bottom of the screen, over everything.
|
|
53
|
+
*
|
|
54
|
+
* The library's first overlay, built on `@gorhom/bottom-sheet`'s modal. It
|
|
55
|
+
* renders no view of its own — it owns the open state and hands it to the parts
|
|
56
|
+
* through context, so a trigger anywhere inside can open a sheet declared
|
|
57
|
+
* anywhere else in the same tree.
|
|
58
|
+
*
|
|
59
|
+
* **The app must mount `DelacourProvider` at its root.** The sheet hosts itself in
|
|
60
|
+
* the `BottomSheetModalProvider` that provider's innermost layer supplies, and it
|
|
61
|
+
* needs the gesture root above it for the pan.
|
|
62
|
+
*
|
|
63
|
+
* **Two parts are not drawn where they are written.** `BottomSheet.Overlay` and a
|
|
64
|
+
* `BottomSheet.Footer sticky` are lifted out of the tree and handed to gorhom as
|
|
65
|
+
* render props — see `BottomSheet.Portal` and `BottomSheet.Container` for why the
|
|
66
|
+
* anatomy is worth that. Everything else renders in place.
|
|
67
|
+
*
|
|
68
|
+
* Controlled or not, from one hook: pass `isOpen` and `onOpenChange` to own the
|
|
69
|
+
* state, or nothing at all and let the sheet hold it. Which mode is in play is
|
|
70
|
+
* locked in on first render.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* <BottomSheet>
|
|
74
|
+
* <BottomSheet.Trigger asChild>
|
|
75
|
+
* <Button variant="secondary">Open</Button>
|
|
76
|
+
* </BottomSheet.Trigger>
|
|
77
|
+
* <BottomSheet.Portal>
|
|
78
|
+
* <BottomSheet.Overlay />
|
|
79
|
+
* <BottomSheet.Container>
|
|
80
|
+
* <BottomSheet.Content>
|
|
81
|
+
* <BottomSheet.Close />
|
|
82
|
+
* <BottomSheet.Title>Keep yourself safe</BottomSheet.Title>
|
|
83
|
+
* <BottomSheet.Description>Update to the latest version.</BottomSheet.Description>
|
|
84
|
+
* </BottomSheet.Content>
|
|
85
|
+
* <BottomSheet.Footer sticky>
|
|
86
|
+
* <Button onPress={update}>Update now</Button>
|
|
87
|
+
* </BottomSheet.Footer>
|
|
88
|
+
* </BottomSheet.Container>
|
|
89
|
+
* </BottomSheet.Portal>
|
|
90
|
+
* </BottomSheet>
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* // Controlled, and opened from somewhere other than a trigger.
|
|
94
|
+
* <BottomSheet isOpen={isOpen} onOpenChange={setOpen}>
|
|
95
|
+
* <BottomSheet.Portal>
|
|
96
|
+
* <BottomSheet.Overlay />
|
|
97
|
+
* <BottomSheet.Container>
|
|
98
|
+
* <BottomSheet.Content>{…}</BottomSheet.Content>
|
|
99
|
+
* </BottomSheet.Container>
|
|
100
|
+
* </BottomSheet.Portal>
|
|
101
|
+
* </BottomSheet>
|
|
102
|
+
*/
|
|
103
|
+
export const BottomSheet = Object.assign(BottomSheetRoot, {
|
|
104
|
+
/** The control that opens the sheet. `asChild` to make a `Button` the trigger. */
|
|
105
|
+
Trigger: BottomSheetTrigger,
|
|
106
|
+
/** Everything drawn above the app. Lifts the overlay out to the container. */
|
|
107
|
+
Portal: BottomSheetPortal,
|
|
108
|
+
/** The scrim. Written beside the container, drawn as its backdrop. */
|
|
109
|
+
Overlay: BottomSheetOverlay,
|
|
110
|
+
/** The sheet itself. Every gorhom prop passes through it. */
|
|
111
|
+
Container: BottomSheetContainer,
|
|
112
|
+
/** The sheet's body, sized to itself under `enableDynamicSizing`. */
|
|
113
|
+
Content: BottomSheetContent,
|
|
114
|
+
/** A scrolling body. Needs `enableDynamicSizing={false}` and snap points. */
|
|
115
|
+
ScrollView: BottomSheetScrollView,
|
|
116
|
+
/** The dismiss control, positioned out of the content's flow. */
|
|
117
|
+
Close: BottomSheetClose,
|
|
118
|
+
/** The sheet's heading — a `Text.Header` with the close control's clearance. */
|
|
119
|
+
Title: BottomSheetTitle,
|
|
120
|
+
/** Supporting copy under the title — a muted `Text.Paragraph`. */
|
|
121
|
+
Description: BottomSheetDescription,
|
|
122
|
+
/** Controls at the bottom. `sticky` pins them above the keyboard. */
|
|
123
|
+
Footer: BottomSheetFooter,
|
|
124
|
+
displayName: "DelacourUI.BottomSheet",
|
|
125
|
+
});
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { declaredTokens, tokenValue } from "../../styles/theme-tokens.test";
|
|
3
|
+
import {
|
|
4
|
+
BOTTOM_SHEET_BACKDROP_INDICES,
|
|
5
|
+
BOTTOM_SHEET_CLOSE_HIT_SLOP,
|
|
6
|
+
BOTTOM_SHEET_FOOTER_GAP,
|
|
7
|
+
BOTTOM_SHEET_FOOTER_PADDING,
|
|
8
|
+
BOTTOM_SHEET_KEYBOARD_DEFAULTS,
|
|
9
|
+
BOTTOM_SHEET_OVERLAY_OPACITY,
|
|
10
|
+
BOTTOM_SHEET_OVERLAY_TOKEN,
|
|
11
|
+
bottomSheetVariants,
|
|
12
|
+
resolveFooterPlacement,
|
|
13
|
+
resolveSheetBottomInset,
|
|
14
|
+
resolveSheetScrollEndPadding,
|
|
15
|
+
} from "./bottom-sheet.variants";
|
|
16
|
+
|
|
17
|
+
const LIGHT = declaredTokens("light");
|
|
18
|
+
const DARK = declaredTokens("dark");
|
|
19
|
+
|
|
20
|
+
/** `border-t` and friends set a width, not a colour, and name no token. */
|
|
21
|
+
const STRUCTURAL_BORDER_SUFFIXES = new Set(["t", "b", "l", "r", "x", "y", "s", "e"]);
|
|
22
|
+
|
|
23
|
+
/** Every theme token a class string paints with, with any `/alpha` suffix dropped. */
|
|
24
|
+
function colorTokens(cls: string): string[] {
|
|
25
|
+
const tokens: string[] = [];
|
|
26
|
+
|
|
27
|
+
for (const [, utility, token] of cls.matchAll(/\b(bg|border|text)-([a-z][\w-]*)(?:\/\d+)?\b/g)) {
|
|
28
|
+
if (utility === "border" && STRUCTURAL_BORDER_SUFFIXES.has(token)) continue;
|
|
29
|
+
tokens.push(token);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return tokens;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const SLOTS = bottomSheetVariants();
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The slots this component declares, pinned rather than derived.
|
|
39
|
+
*
|
|
40
|
+
* `tv` adds a `base` slot of its own that emits nothing, so iterating the
|
|
41
|
+
* returned object sweeps one entry that is not ours. Listing them also means a
|
|
42
|
+
* new slot has to be added here before the checks below can miss it.
|
|
43
|
+
*/
|
|
44
|
+
const SLOT_NAMES = [
|
|
45
|
+
"overlay",
|
|
46
|
+
"background",
|
|
47
|
+
"handle",
|
|
48
|
+
"handleIndicator",
|
|
49
|
+
"content",
|
|
50
|
+
"scrollContent",
|
|
51
|
+
"footer",
|
|
52
|
+
"stickyFooter",
|
|
53
|
+
"close",
|
|
54
|
+
"title",
|
|
55
|
+
] as const;
|
|
56
|
+
|
|
57
|
+
describe("the theme.css reader", () => {
|
|
58
|
+
// The token assertions below are only worth anything if the parse found something.
|
|
59
|
+
test("finds both variants", () => {
|
|
60
|
+
expect(LIGHT.size).toBeGreaterThan(0);
|
|
61
|
+
expect(DARK.size).toBeGreaterThan(0);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("the overlay token", () => {
|
|
66
|
+
test("is declared in both variants", () => {
|
|
67
|
+
expect(LIGHT.has(BOTTOM_SHEET_OVERLAY_TOKEN)).toBe(true);
|
|
68
|
+
expect(DARK.has(BOTTOM_SHEET_OVERLAY_TOKEN)).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("is the token the overlay slot actually paints", () => {
|
|
72
|
+
expect(SLOTS.overlay()).toContain(`bg-${BOTTOM_SHEET_OVERLAY_TOKEN}`);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("carries its own alpha in both variants, which is what makes the opacity 1", () => {
|
|
76
|
+
for (const variant of ["light", "dark"] as const) {
|
|
77
|
+
const value = tokenValue(variant, BOTTOM_SHEET_OVERLAY_TOKEN);
|
|
78
|
+
if (value === undefined) throw new Error(`theme.css declares no --overlay under ${variant}`);
|
|
79
|
+
|
|
80
|
+
const alpha = Number(value.match(/\/\s*([\d.]+)%\s*\)/)?.[1]) / 100;
|
|
81
|
+
expect(alpha).toBeGreaterThan(0);
|
|
82
|
+
expect(alpha).toBeLessThan(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Leaving gorhom's 0.5 in place would multiply against that alpha and land
|
|
86
|
+
// the scrim at roughly a fifth of what the theme asked for.
|
|
87
|
+
expect(BOTTOM_SHEET_OVERLAY_OPACITY).toBe(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("the two variants differ — a black scrim over a near-black theme is invisible", () => {
|
|
91
|
+
const light = tokenValue("light", BOTTOM_SHEET_OVERLAY_TOKEN);
|
|
92
|
+
const dark = tokenValue("dark", BOTTOM_SHEET_OVERLAY_TOKEN);
|
|
93
|
+
expect(light).not.toBe(dark);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("every token the slots name", () => {
|
|
98
|
+
test("is declared in both variants of theme.css", () => {
|
|
99
|
+
for (const name of SLOT_NAMES) {
|
|
100
|
+
for (const token of colorTokens(SLOTS[name]())) {
|
|
101
|
+
expect({ inLight: LIGHT.has(token), slot: name, token }).toEqual({ inLight: true, slot: name, token });
|
|
102
|
+
expect({ inDark: DARK.has(token), slot: name, token }).toEqual({ inDark: true, slot: name, token });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("the reader found tokens at all", () => {
|
|
108
|
+
// Every assertion above passes vacuously if the extractor matches nothing.
|
|
109
|
+
expect(colorTokens(SLOTS.stickyFooter())).toEqual(expect.arrayContaining(["popover", "border"]));
|
|
110
|
+
expect(colorTokens(SLOTS.handleIndicator())).toContain("muted-foreground");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("the backdrop indices", () => {
|
|
115
|
+
test("show the scrim from the first snap point and hide it only when closed", () => {
|
|
116
|
+
// gorhom's own defaults (1 / 0) suit a persistent sheet resting collapsed
|
|
117
|
+
// on screen. A modal sheet has no resting state.
|
|
118
|
+
expect(BOTTOM_SHEET_BACKDROP_INDICES.appearsOnIndex).toBe(0);
|
|
119
|
+
expect(BOTTOM_SHEET_BACKDROP_INDICES.disappearsOnIndex).toBe(-1);
|
|
120
|
+
expect(BOTTOM_SHEET_BACKDROP_INDICES.disappearsOnIndex).toBeLessThan(BOTTOM_SHEET_BACKDROP_INDICES.appearsOnIndex);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("the keyboard defaults", () => {
|
|
125
|
+
test("follow the keyboard, restore on blur, and resize the Android window", () => {
|
|
126
|
+
expect(BOTTOM_SHEET_KEYBOARD_DEFAULTS).toEqual({
|
|
127
|
+
keyboardBehavior: "interactive",
|
|
128
|
+
keyboardBlurBehavior: "restore",
|
|
129
|
+
android_keyboardInputMode: "adjustResize",
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("does not leave Android on gorhom's adjustPan", () => {
|
|
134
|
+
// `adjustPan` slides the whole window up instead of resizing it, which puts
|
|
135
|
+
// a sheet's footer off-screen. `resize` is what Expo configures and what
|
|
136
|
+
// `KeyboardProvider` requires, so this is the value that matches reality.
|
|
137
|
+
expect(BOTTOM_SHEET_KEYBOARD_DEFAULTS.android_keyboardInputMode).not.toBe("adjustPan");
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("resolveFooterPlacement", () => {
|
|
142
|
+
const footer = (isSticky: boolean) => ({ isFooter: true, isSticky });
|
|
143
|
+
const other = { isFooter: false, isSticky: false };
|
|
144
|
+
|
|
145
|
+
test("reports none for children holding no footer", () => {
|
|
146
|
+
expect(resolveFooterPlacement([])).toBe("none");
|
|
147
|
+
expect(resolveFooterPlacement([other, other])).toBe("none");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("reports inline for a footer that did not ask to be pinned", () => {
|
|
151
|
+
expect(resolveFooterPlacement([other, footer(false)])).toBe("inline");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("reports sticky for one that did, wherever it sits", () => {
|
|
155
|
+
expect(resolveFooterPlacement([footer(true), other])).toBe("sticky");
|
|
156
|
+
expect(resolveFooterPlacement([other, footer(true)])).toBe("sticky");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("lets a single sticky footer win over an inline one", () => {
|
|
160
|
+
// Two footers is not a state worth expressing, and the pinned one is the
|
|
161
|
+
// one a caller meant.
|
|
162
|
+
expect(resolveFooterPlacement([footer(false), footer(true)])).toBe("sticky");
|
|
163
|
+
expect(resolveFooterPlacement([footer(true), footer(false)])).toBe("sticky");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("never reads a sticky flag off a child that is not a footer", () => {
|
|
167
|
+
expect(resolveFooterPlacement([{ isFooter: false, isSticky: true }])).toBe("none");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("resolveSheetBottomInset", () => {
|
|
172
|
+
test("gives the safe-area band to the content when nothing is pinned below it", () => {
|
|
173
|
+
expect(resolveSheetBottomInset({ bottom: 34, hasStickyFooter: false })).toBe(34);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("withholds it once a pinned footer's own box is carrying it", () => {
|
|
177
|
+
// gorhom adds the footer's whole measured height — band included — to the
|
|
178
|
+
// content's reserve, so asking for it here too counts it twice.
|
|
179
|
+
expect(resolveSheetBottomInset({ bottom: 34, hasStickyFooter: true })).toBe(0);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("is a no-op on a device with no bottom inset", () => {
|
|
183
|
+
expect(resolveSheetBottomInset({ bottom: 0, hasStickyFooter: false })).toBe(0);
|
|
184
|
+
});
|
|
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
|
+
|
|
203
|
+
describe("bottomSheetVariants slots", () => {
|
|
204
|
+
test("the title slot carries layout only, never type", () => {
|
|
205
|
+
// The type comes from the `Text.Header` the part renders. A `text-lg` here
|
|
206
|
+
// would be a second definition of that preset which can drift from it.
|
|
207
|
+
expect(SLOTS.title()).not.toMatch(/\btext-\w/);
|
|
208
|
+
expect(SLOTS.title()).not.toMatch(/\bfont-\w/);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("the title reserves room for the close control on every sheet", () => {
|
|
212
|
+
// Reserved unconditionally, the way `Badge` reserves its border on every
|
|
213
|
+
// variant: conditional clearance reflows the title the moment one is added.
|
|
214
|
+
expect(SLOTS.title()).toMatch(/\bpr-[\d.]+\b/);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("every slot emits something, so none of them is untestable", () => {
|
|
218
|
+
// `tv` returns undefined for an empty class string, and a slot that says
|
|
219
|
+
// nothing is a slot no assertion here can reach.
|
|
220
|
+
for (const name of SLOT_NAMES) {
|
|
221
|
+
expect({ emits: typeof SLOTS[name](), name }).toEqual({ emits: "string", name });
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("declares no slot the pinned list has not seen", () => {
|
|
226
|
+
// `base` is tv's own, and is the one entry that is not ours.
|
|
227
|
+
const declared = Object.keys(SLOTS).filter((name) => name !== "base");
|
|
228
|
+
expect(declared.sort()).toEqual([...SLOT_NAMES].sort());
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("the sheet's surface rounds its top corners only", () => {
|
|
232
|
+
const background = SLOTS.background();
|
|
233
|
+
expect(background).toMatch(/\brounded-t-/);
|
|
234
|
+
expect(background).not.toMatch(/\brounded-b-/);
|
|
235
|
+
// A bare `rounded-*` would round the bottom edge too, and that edge runs off
|
|
236
|
+
// the screen — the radius shows as two notches of the app behind it.
|
|
237
|
+
expect(background).not.toMatch(/\brounded-(?:none|xs|sm|md|lg|xl|2xl|3xl|full)\b/);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("a pinned footer brings a surface and a line where an inline one does not", () => {
|
|
241
|
+
// It draws OVER the content, so without them the content scrolls straight
|
|
242
|
+
// through it. An inline footer is in the flow and needs neither.
|
|
243
|
+
expect(SLOTS.stickyFooter()).toMatch(/\bbg-\w/);
|
|
244
|
+
expect(SLOTS.stickyFooter()).toMatch(/\bborder-t\b/);
|
|
245
|
+
expect(SLOTS.footer()).not.toMatch(/\bbg-\w/);
|
|
246
|
+
expect(SLOTS.footer()).not.toMatch(/\bborder-t\b/);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("a pinned footer writes its vertical padding as a style, never a class", () => {
|
|
250
|
+
// The bottom padding is animated — the safe-area band collapses into it as
|
|
251
|
+
// the keyboard arrives — and a class cannot carry an animated value. The
|
|
252
|
+
// top is written the same way so the two cannot drift.
|
|
253
|
+
expect(SLOTS.stickyFooter()).not.toMatch(/\bp[tby]?-[\d.]+\b/);
|
|
254
|
+
expect(BOTTOM_SHEET_FOOTER_PADDING).toBeGreaterThan(0);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("the content is held off a pinned footer by a real gap", () => {
|
|
258
|
+
// Without it the last row sits flush against the footer's hairline, which
|
|
259
|
+
// reads as content clipped rather than content ended.
|
|
260
|
+
expect(BOTTOM_SHEET_FOOTER_GAP).toBeGreaterThan(0);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("both footers share their gutter and gap, so pinning one moves nothing sideways", () => {
|
|
264
|
+
const gutter = /\bpx-screen-gutter\b/;
|
|
265
|
+
expect(SLOTS.footer()).toMatch(gutter);
|
|
266
|
+
expect(SLOTS.stickyFooter()).toMatch(gutter);
|
|
267
|
+
expect(SLOTS.footer().match(/\bgap-[\d.]+\b/)?.[0]).toBe(SLOTS.stickyFooter().match(/\bgap-[\d.]+\b/)?.[0]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("scrolling content sits on the same gutter as static content", () => {
|
|
271
|
+
// A sheet should not shift sideways because its body became scrollable.
|
|
272
|
+
expect(SLOTS.content()).toMatch(/\bpx-screen-gutter\b/);
|
|
273
|
+
expect(SLOTS.scrollContent()).toMatch(/\bpx-screen-gutter\b/);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("the overlay sets no opacity of its own", () => {
|
|
277
|
+
// gorhom animates the scrim's opacity off the sheet's position; a class here
|
|
278
|
+
// would be a second writer of the same style property.
|
|
279
|
+
expect(SLOTS.overlay()).not.toMatch(/\bopacity-\d/);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("the close control is positioned out of the content flow", () => {
|
|
283
|
+
expect(SLOTS.close()).toContain("absolute");
|
|
284
|
+
expect(BOTTOM_SHEET_CLOSE_HIT_SLOP).toBeGreaterThan(0);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("the grabber is a pill with both dimensions set", () => {
|
|
288
|
+
const indicator = SLOTS.handleIndicator();
|
|
289
|
+
expect(indicator).toMatch(/\bh-[\d.]+\b/);
|
|
290
|
+
expect(indicator).toMatch(/\bw-[\d.]+\b/);
|
|
291
|
+
expect(indicator).toContain("rounded-full");
|
|
292
|
+
});
|
|
293
|
+
});
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { VariantProps } from "tailwind-variants";
|
|
2
|
+
import { tv } from "../../lib/tv";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The scrim token, named here so a test can pin it against `theme.css`.
|
|
6
|
+
*
|
|
7
|
+
* A token rather than a `bg-black/50` written at a call site: the two theme
|
|
8
|
+
* variants carry different alphas, because a pure-black scrim over a near-black
|
|
9
|
+
* dark theme is nearly invisible.
|
|
10
|
+
*/
|
|
11
|
+
export const BOTTOM_SHEET_OVERLAY_TOKEN = "overlay";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The backdrop's opacity at full appearance.
|
|
15
|
+
*
|
|
16
|
+
* **1, not gorhom's 0.5.** The alpha lives in `--overlay`, so leaving the
|
|
17
|
+
* library default in place would multiply the two and land the scrim at roughly
|
|
18
|
+
* a fifth of what the theme asked for. One source for the alpha, and it is the
|
|
19
|
+
* token — which is also what lets the two theme variants differ.
|
|
20
|
+
*/
|
|
21
|
+
export const BOTTOM_SHEET_OVERLAY_OPACITY = 1;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The snap indices the scrim appears and disappears on.
|
|
25
|
+
*
|
|
26
|
+
* gorhom defaults to `{ appearsOnIndex: 1, disappearsOnIndex: 0 }`, which suits
|
|
27
|
+
* a persistent sheet that rests collapsed on the screen and only dims the app
|
|
28
|
+
* once it is expanded. A modal sheet has no resting state: it is either
|
|
29
|
+
* presented or gone, so the scrim belongs from the FIRST snap point (`0`) and
|
|
30
|
+
* is only absent when the sheet is closed (`-1`).
|
|
31
|
+
*/
|
|
32
|
+
export const BOTTOM_SHEET_BACKDROP_INDICES = { appearsOnIndex: 0, disappearsOnIndex: -1 } as const;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Keyboard behaviour every `BottomSheet.Container` starts from.
|
|
36
|
+
*
|
|
37
|
+
* A const rather than three defaults spelled inline, so `bun test` pins them and
|
|
38
|
+
* a retune is one edit rather than three.
|
|
39
|
+
*
|
|
40
|
+
* - `interactive` — the sheet grows to keep its content above the keyboard,
|
|
41
|
+
* following it frame by frame rather than snapping once it has settled.
|
|
42
|
+
* - `restore` — blurring returns the sheet to the snap point it was on. Without
|
|
43
|
+
* it a sheet that grew for the keyboard stays grown over empty screen.
|
|
44
|
+
* - `adjustResize` — gorhom's own default is `adjustPan`, which leaves the
|
|
45
|
+
* Android window height alone and slides the whole view up, so a sheet's
|
|
46
|
+
* footer lands off-screen. `resize` is already what Expo configures and what
|
|
47
|
+
* `KeyboardProvider` requires, so this is the value that matches the window.
|
|
48
|
+
*/
|
|
49
|
+
export const BOTTOM_SHEET_KEYBOARD_DEFAULTS = {
|
|
50
|
+
keyboardBehavior: "interactive",
|
|
51
|
+
keyboardBlurBehavior: "restore",
|
|
52
|
+
android_keyboardInputMode: "adjustResize",
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Slop around `BottomSheet.Close`.
|
|
57
|
+
*
|
|
58
|
+
* The glyph is a bare icon in a corner with no padded capsule to absorb the
|
|
59
|
+
* difference against the 44pt minimum — the case `Checkbox` mints slop for and
|
|
60
|
+
* `Badge.CloseButton` does not need.
|
|
61
|
+
*/
|
|
62
|
+
export const BOTTOM_SHEET_CLOSE_HIT_SLOP = 8;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The padding a pinned footer keeps whatever the keyboard is doing.
|
|
66
|
+
*
|
|
67
|
+
* A number rather than a `p-4`, and for `SCREEN_FOOTER_PADDING`'s reason: the
|
|
68
|
+
* footer's bottom padding is an animated value — the safe-area band collapses
|
|
69
|
+
* into it as the keyboard arrives — and a class cannot carry one. Its top
|
|
70
|
+
* padding is written the same way so the two cannot drift.
|
|
71
|
+
*/
|
|
72
|
+
export const BOTTOM_SHEET_FOOTER_PADDING = 16;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The gap between the last of the content and a pinned footer.
|
|
76
|
+
*
|
|
77
|
+
* Also a number, because the reserve it belongs to is computed at runtime from
|
|
78
|
+
* the footer's measured height. Without it the last row of a list sits flush
|
|
79
|
+
* against the footer's hairline, which reads as content clipped rather than
|
|
80
|
+
* content ended.
|
|
81
|
+
*/
|
|
82
|
+
export const BOTTOM_SHEET_FOOTER_GAP = 16;
|
|
83
|
+
|
|
84
|
+
export const bottomSheetVariants = tv({
|
|
85
|
+
slots: {
|
|
86
|
+
/**
|
|
87
|
+
* The scrim. Carries the colour and nothing else — the fade is an animated
|
|
88
|
+
* opacity gorhom drives off the sheet's own position.
|
|
89
|
+
*/
|
|
90
|
+
overlay: "bg-overlay",
|
|
91
|
+
/**
|
|
92
|
+
* The sheet's surface. `popover` rather than `card`: a sheet is a layer
|
|
93
|
+
* over the app, which is what that token already means, and it is the one
|
|
94
|
+
* surface token nothing else in the package had claimed.
|
|
95
|
+
*
|
|
96
|
+
* Only the top corners round. The bottom edge runs off the screen, and a
|
|
97
|
+
* radius there shows as two notches of the app behind it.
|
|
98
|
+
*/
|
|
99
|
+
background: "rounded-t-2xl bg-popover",
|
|
100
|
+
handle: "items-center justify-center pt-3 pb-1",
|
|
101
|
+
handleIndicator: "h-1 w-9 rounded-full bg-muted-foreground/40",
|
|
102
|
+
content: "gap-4 px-screen-gutter pt-2",
|
|
103
|
+
/** A scrollable body's content container. Same treatment as `content`. */
|
|
104
|
+
scrollContent: "gap-4 px-screen-gutter pt-2",
|
|
105
|
+
footer: "gap-3 px-screen-gutter pt-4",
|
|
106
|
+
/**
|
|
107
|
+
* A pinned footer draws OVER the content, so unlike the inline one it has
|
|
108
|
+
* to bring a surface and a line of its own — otherwise the content scrolls
|
|
109
|
+
* straight through it and its buttons are legible only where they happen
|
|
110
|
+
* to overlap blank space. The same reason `Screen.Footer`'s backing lives
|
|
111
|
+
* inside its sticky view.
|
|
112
|
+
*
|
|
113
|
+
* It carries no vertical padding: that is an animated style, because the
|
|
114
|
+
* safe-area band inside it collapses as the keyboard arrives. See
|
|
115
|
+
* {@link BOTTOM_SHEET_FOOTER_PADDING}.
|
|
116
|
+
*/
|
|
117
|
+
stickyFooter: "gap-3 border-border border-t bg-popover px-screen-gutter",
|
|
118
|
+
close: "absolute top-4 right-4 z-10",
|
|
119
|
+
/**
|
|
120
|
+
* Clearance for `BottomSheet.Close`, and nothing else.
|
|
121
|
+
*
|
|
122
|
+
* The close control is absolutely positioned, so it is out of the flow and
|
|
123
|
+
* a long title runs straight under it. The gutter is reserved on every
|
|
124
|
+
* sheet rather than only where a close is written, for the reason `Badge`
|
|
125
|
+
* reserves its border on every variant: declaring it conditionally makes
|
|
126
|
+
* the title reflow the moment someone adds one.
|
|
127
|
+
*
|
|
128
|
+
* No type. That comes from the `Text` preset the part renders, the way
|
|
129
|
+
* `Field.Label` *is* a `Text.Label` — a `text-lg font-semibold` here would
|
|
130
|
+
* be a second definition of `Text.Header` that can drift from it.
|
|
131
|
+
*/
|
|
132
|
+
title: "pr-8",
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* There is no `description` or `scrollView` slot.
|
|
138
|
+
*
|
|
139
|
+
* Neither has any layout of its own — a description is a `Text.Paragraph` in a
|
|
140
|
+
* gap column and a scroll view fills whatever it is given — and `tv` emits
|
|
141
|
+
* `undefined` for a slot whose class string is empty, so a slot that says
|
|
142
|
+
* nothing is a slot no test can assert against. Those two parts merge the
|
|
143
|
+
* caller's `className` with `cn()` instead.
|
|
144
|
+
*/
|
|
145
|
+
export type BottomSheetVariantProps = VariantProps<typeof bottomSheetVariants>;
|
|
146
|
+
|
|
147
|
+
/** Where a `BottomSheet.Footer` among a container's children has to be rendered. */
|
|
148
|
+
export type BottomSheetFooterPlacement = "sticky" | "inline" | "none";
|
|
149
|
+
|
|
150
|
+
/** One child of a `BottomSheet.Container`, reduced to what the placement turns on. */
|
|
151
|
+
export type BottomSheetFooterFlag = {
|
|
152
|
+
isFooter: boolean;
|
|
153
|
+
isSticky: boolean;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Whether a container's children hold a footer, and where it has to go.
|
|
158
|
+
*
|
|
159
|
+
* gorhom takes a footer as a `footerComponent` render prop rather than as a
|
|
160
|
+
* child, so a sticky one has to be lifted out of the tree it was written in and
|
|
161
|
+
* handed over; an inline one simply stays put. This is the decision, kept pure
|
|
162
|
+
* so `bun test` reaches the whole matrix — the walk over `Children.toArray` that
|
|
163
|
+
* produces the flags lives with its caller, the way `withIndicator` does in
|
|
164
|
+
* `radio.tsx`.
|
|
165
|
+
*
|
|
166
|
+
* A single sticky footer anywhere in the children wins, because two footers is
|
|
167
|
+
* not a state worth expressing and the pinned one is the one a caller meant.
|
|
168
|
+
*/
|
|
169
|
+
export function resolveFooterPlacement(flags: readonly BottomSheetFooterFlag[]): BottomSheetFooterPlacement {
|
|
170
|
+
let hasFooter = false;
|
|
171
|
+
|
|
172
|
+
for (const flag of flags) {
|
|
173
|
+
if (!flag.isFooter) continue;
|
|
174
|
+
if (flag.isSticky) return "sticky";
|
|
175
|
+
hasFooter = true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return hasFooter ? "inline" : "none";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The safe-area band `BottomSheet.Content` and `BottomSheet.ScrollView` reserve.
|
|
183
|
+
*
|
|
184
|
+
* Exactly one thing in the sheet pays for the home indicator. A pinned footer is
|
|
185
|
+
* the bottom-most thing there is, and its own box carries the band so its surface
|
|
186
|
+
* reaches the bottom of the screen — gorhom then adds that whole measured height
|
|
187
|
+
* to the content's reserve, so the content asking for the band as well would
|
|
188
|
+
* count it twice and leave a gap the height of the indicator above the footer.
|
|
189
|
+
* With no pinned footer the content IS the bottom-most thing and takes it.
|
|
190
|
+
*/
|
|
191
|
+
export function resolveSheetBottomInset({
|
|
192
|
+
hasStickyFooter,
|
|
193
|
+
bottom,
|
|
194
|
+
}: {
|
|
195
|
+
hasStickyFooter: boolean;
|
|
196
|
+
bottom: number;
|
|
197
|
+
}): number {
|
|
198
|
+
return hasStickyFooter ? 0 : bottom;
|
|
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
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export { BottomSheet, type BottomSheetProps } from "./bottom-sheet";
|
|
2
|
+
export {
|
|
3
|
+
type BottomSheetContainerContextValue,
|
|
4
|
+
BottomSheetContainerProvider,
|
|
5
|
+
type BottomSheetContextValue,
|
|
6
|
+
type BottomSheetHandle,
|
|
7
|
+
type BottomSheetPortalContextValue,
|
|
8
|
+
BottomSheetPortalProvider,
|
|
9
|
+
BottomSheetProvider,
|
|
10
|
+
useBottomSheet,
|
|
11
|
+
useBottomSheetContainerContext,
|
|
12
|
+
useBottomSheetContext,
|
|
13
|
+
useBottomSheetPortalContext,
|
|
14
|
+
} from "./bottom-sheet.context";
|
|
15
|
+
export {
|
|
16
|
+
BOTTOM_SHEET_BACKDROP_INDICES,
|
|
17
|
+
BOTTOM_SHEET_CLOSE_HIT_SLOP,
|
|
18
|
+
BOTTOM_SHEET_FOOTER_GAP,
|
|
19
|
+
BOTTOM_SHEET_FOOTER_PADDING,
|
|
20
|
+
BOTTOM_SHEET_KEYBOARD_DEFAULTS,
|
|
21
|
+
BOTTOM_SHEET_OVERLAY_OPACITY,
|
|
22
|
+
BOTTOM_SHEET_OVERLAY_TOKEN,
|
|
23
|
+
type BottomSheetFooterFlag,
|
|
24
|
+
type BottomSheetFooterPlacement,
|
|
25
|
+
type BottomSheetVariantProps,
|
|
26
|
+
bottomSheetVariants,
|
|
27
|
+
resolveFooterPlacement,
|
|
28
|
+
resolveSheetBottomInset,
|
|
29
|
+
} from "./bottom-sheet.variants";
|
|
30
|
+
export type { BottomSheetCloseProps } from "./bottom-sheet-close";
|
|
31
|
+
export type { BottomSheetContainerProps } from "./bottom-sheet-container";
|
|
32
|
+
export type { BottomSheetContentProps } from "./bottom-sheet-content";
|
|
33
|
+
export type { BottomSheetDescriptionProps } from "./bottom-sheet-description";
|
|
34
|
+
export type { BottomSheetFooterProps } from "./bottom-sheet-footer";
|
|
35
|
+
export type { BottomSheetOverlayProps } from "./bottom-sheet-overlay";
|
|
36
|
+
export type { BottomSheetPortalProps } from "./bottom-sheet-portal";
|
|
37
|
+
export type { BottomSheetScrollViewProps } from "./bottom-sheet-scroll-view";
|
|
38
|
+
export type { BottomSheetTitleProps } from "./bottom-sheet-title";
|
|
39
|
+
export type { BottomSheetTriggerProps } from "./bottom-sheet-trigger";
|
|
40
|
+
export { type BottomSheetInputHandlers, useBottomSheetInput } from "./use-bottom-sheet-input";
|