@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.
- package/README.md +113 -0
- package/package.json +125 -0
- package/src/components/accordion/AGENTS.md +192 -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 +435 -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 +232 -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 +245 -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 +148 -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 +95 -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 +276 -0
- package/src/components/bottom-sheet/bottom-sheet.variants.ts +199 -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 +100 -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 +33 -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 +78 -0
- package/src/components/field/field.tsx +144 -0
- package/src/components/field/field.types.ts +15 -0
- package/src/components/field/field.variants.test.ts +216 -0
- package/src/components/field/field.variants.ts +136 -0
- package/src/components/field/index.ts +21 -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 +64 -0
- package/src/components/pressable/index.ts +9 -0
- package/src/components/pressable/pressable.tsx +256 -0
- package/src/components/pressable/pressable.variants.test.ts +99 -0
- package/src/components/pressable/pressable.variants.ts +63 -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 +247 -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 +581 -0
- package/src/components/radio/radio.variants.ts +272 -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 +48 -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 +161 -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 +804 -0
- package/src/components/slider/slider.variants.ts +607 -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 +286 -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 +672 -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,238 @@
|
|
|
1
|
+
import type { VariantProps } from "tailwind-variants";
|
|
2
|
+
import { tv } from "../../lib/tv";
|
|
3
|
+
|
|
4
|
+
/** How the surface is painted. Orthogonal to {@link BADGE_COLORS}. */
|
|
5
|
+
export const BADGE_VARIANTS = ["solid", "soft", "outline", "ghost"] as const;
|
|
6
|
+
|
|
7
|
+
/** What the surface means. Orthogonal to {@link BADGE_VARIANTS}. */
|
|
8
|
+
export const BADGE_COLORS = ["default", "primary", "success", "warning", "destructive", "info"] as const;
|
|
9
|
+
|
|
10
|
+
export const BADGE_SIZES = ["sm", "md", "lg"] as const;
|
|
11
|
+
|
|
12
|
+
export type BadgeVariant = (typeof BADGE_VARIANTS)[number];
|
|
13
|
+
export type BadgeColor = (typeof BADGE_COLORS)[number];
|
|
14
|
+
export type BadgeSize = (typeof BADGE_SIZES)[number];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Theme token whose value gives an `Icon` composed into the badge its colour.
|
|
18
|
+
*
|
|
19
|
+
* The badge's counterpart to `BUTTON_FOREGROUND_TOKEN`, and it exists for the
|
|
20
|
+
* same reason: a colour that has to reach an SVG paint prop cannot be a class.
|
|
21
|
+
* Every entry names the token its own `label` slot resolves to, so a glyph and
|
|
22
|
+
* the text beside it are always the same shade — a test asserts the pair.
|
|
23
|
+
*
|
|
24
|
+
* Nested rather than flattened to twenty-four keys so adding a colour is a
|
|
25
|
+
* compile error in four places instead of a silent gap in one.
|
|
26
|
+
*/
|
|
27
|
+
export const BADGE_FOREGROUND_TOKEN: Record<BadgeVariant, Record<BadgeColor, string>> = {
|
|
28
|
+
solid: {
|
|
29
|
+
default: "secondary-foreground",
|
|
30
|
+
primary: "primary-foreground",
|
|
31
|
+
success: "success-foreground",
|
|
32
|
+
warning: "warning-foreground",
|
|
33
|
+
destructive: "destructive-foreground",
|
|
34
|
+
info: "info-foreground",
|
|
35
|
+
},
|
|
36
|
+
soft: {
|
|
37
|
+
default: "muted-foreground",
|
|
38
|
+
primary: "tertiary-foreground",
|
|
39
|
+
success: "success-soft-foreground",
|
|
40
|
+
warning: "warning-soft-foreground",
|
|
41
|
+
destructive: "destructive-soft-foreground",
|
|
42
|
+
info: "info-soft-foreground",
|
|
43
|
+
},
|
|
44
|
+
outline: {
|
|
45
|
+
default: "muted-foreground",
|
|
46
|
+
primary: "foreground",
|
|
47
|
+
success: "success-soft-foreground",
|
|
48
|
+
warning: "warning-soft-foreground",
|
|
49
|
+
destructive: "destructive-soft-foreground",
|
|
50
|
+
info: "info-soft-foreground",
|
|
51
|
+
},
|
|
52
|
+
ghost: {
|
|
53
|
+
default: "muted-foreground",
|
|
54
|
+
primary: "foreground",
|
|
55
|
+
success: "success-soft-foreground",
|
|
56
|
+
warning: "warning-soft-foreground",
|
|
57
|
+
destructive: "destructive-soft-foreground",
|
|
58
|
+
info: "info-soft-foreground",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Styling for every part of a badge.
|
|
64
|
+
*
|
|
65
|
+
* Two axes rather than one. `variant` says how the surface is painted and
|
|
66
|
+
* `color` says what it means, so a caller reaches `soft` + `warning` without the
|
|
67
|
+
* library having to enumerate a `warning-soft` name for it — the combinatorial
|
|
68
|
+
* growth `Button`'s single axis would pay here, where six semantic colours are
|
|
69
|
+
* the point of the component rather than an afterthought. Neither axis carries a
|
|
70
|
+
* surface on its own, so the twenty-four real pairings live in
|
|
71
|
+
* `compoundVariants`.
|
|
72
|
+
*
|
|
73
|
+
* `self-start` is load-bearing. A badge is sized by its content, and inside a
|
|
74
|
+
* gap column every child is stretch-aligned by default — without it, a one-word
|
|
75
|
+
* badge spans the whole screen.
|
|
76
|
+
*
|
|
77
|
+
* `border border-transparent` sits in the base rather than on `outline` alone.
|
|
78
|
+
* A border declared only where it is visible makes the badge two points wider
|
|
79
|
+
* the moment a caller switches variant; reserving it in the box means `outline`
|
|
80
|
+
* changes nothing but the colour.
|
|
81
|
+
*
|
|
82
|
+
* `overflow-hidden` is likewise not tidiness — a pressed badge fades to the
|
|
83
|
+
* edge of its own capsule.
|
|
84
|
+
*
|
|
85
|
+
* A size is padding and never a height. `Text` respects OS font scaling, so a
|
|
86
|
+
* fixed height clips the label at a large accessibility step instead of growing
|
|
87
|
+
* with it, and unlike `h-button-*` a badge lines up against no chrome that would
|
|
88
|
+
* force the number. The icon step indexes the shared `--spacing-icon-*` scale so
|
|
89
|
+
* a glyph in a badge matches every other glyph in the library.
|
|
90
|
+
*
|
|
91
|
+
* The neutral end of the matrix reuses tokens the theme already has instead of
|
|
92
|
+
* minting `--primary-soft`: this theme's `primary` is a near-black, whose
|
|
93
|
+
* tint *is* the neutral fill, so a new token would duplicate `secondary` exactly.
|
|
94
|
+
* `soft` takes `tertiary` for `primary` and `muted` for `default` — two fills the
|
|
95
|
+
* theme already tunes per mode — which keeps all twenty-four cells distinct.
|
|
96
|
+
*
|
|
97
|
+
* The root holds no `text-*` utility: a React Native `View` does not cascade
|
|
98
|
+
* colour to a `Text` descendant, so label colour lives on the `label` slot and
|
|
99
|
+
* icon colour is resolved from {@link BADGE_FOREGROUND_TOKEN}.
|
|
100
|
+
*
|
|
101
|
+
* Free of React Native imports so it stays unit-testable — `bun test` cannot
|
|
102
|
+
* parse React Native's Flow-typed source. See AGENTS.md.
|
|
103
|
+
*/
|
|
104
|
+
export const badgeVariants = tv({
|
|
105
|
+
slots: {
|
|
106
|
+
root: "flex-row items-center justify-center self-start overflow-hidden rounded-full border border-transparent",
|
|
107
|
+
label: "text-center font-medium",
|
|
108
|
+
startContent: "items-center justify-center",
|
|
109
|
+
endContent: "items-center justify-center",
|
|
110
|
+
closeButton: "items-center justify-center rounded-full",
|
|
111
|
+
/** Edge length an `Icon` composed into the badge inherits. */
|
|
112
|
+
icon: "",
|
|
113
|
+
},
|
|
114
|
+
variants: {
|
|
115
|
+
// Neither axis paints a surface alone — see `compoundVariants`. What
|
|
116
|
+
// each variant does carry is whether the box has a fill at all.
|
|
117
|
+
variant: {
|
|
118
|
+
solid: {},
|
|
119
|
+
soft: {},
|
|
120
|
+
outline: { root: "bg-transparent" },
|
|
121
|
+
ghost: { root: "bg-transparent" },
|
|
122
|
+
},
|
|
123
|
+
color: {
|
|
124
|
+
default: {},
|
|
125
|
+
primary: {},
|
|
126
|
+
success: {},
|
|
127
|
+
warning: {},
|
|
128
|
+
destructive: {},
|
|
129
|
+
info: {},
|
|
130
|
+
},
|
|
131
|
+
size: {
|
|
132
|
+
sm: {
|
|
133
|
+
root: "gap-1 px-2 py-0.5",
|
|
134
|
+
label: "text-xs",
|
|
135
|
+
closeButton: "-mr-0.5",
|
|
136
|
+
icon: "size-icon-xs",
|
|
137
|
+
},
|
|
138
|
+
md: {
|
|
139
|
+
root: "gap-1.5 px-2.5 py-1",
|
|
140
|
+
label: "text-sm",
|
|
141
|
+
closeButton: "-mr-1",
|
|
142
|
+
icon: "size-icon-sm",
|
|
143
|
+
},
|
|
144
|
+
lg: {
|
|
145
|
+
root: "gap-2 px-3 py-1.5",
|
|
146
|
+
label: "text-base",
|
|
147
|
+
closeButton: "-mr-1.5",
|
|
148
|
+
icon: "size-icon-md",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
// The empty `false` branch is load-bearing typing, not a placeholder.
|
|
152
|
+
// See the note in button.variants.ts.
|
|
153
|
+
isDisabled: { true: { root: "opacity-50" }, false: {} },
|
|
154
|
+
},
|
|
155
|
+
compoundVariants: [
|
|
156
|
+
{ variant: "solid", color: "default", class: { root: "bg-secondary", label: "text-secondary-foreground" } },
|
|
157
|
+
{ variant: "solid", color: "primary", class: { root: "bg-primary", label: "text-primary-foreground" } },
|
|
158
|
+
{ variant: "solid", color: "success", class: { root: "bg-success", label: "text-success-foreground" } },
|
|
159
|
+
{ variant: "solid", color: "warning", class: { root: "bg-warning", label: "text-warning-foreground" } },
|
|
160
|
+
{ variant: "solid", color: "destructive", class: { root: "bg-destructive", label: "text-destructive-foreground" } },
|
|
161
|
+
{ variant: "solid", color: "info", class: { root: "bg-info", label: "text-info-foreground" } },
|
|
162
|
+
|
|
163
|
+
{ variant: "soft", color: "default", class: { root: "bg-muted", label: "text-muted-foreground" } },
|
|
164
|
+
{ variant: "soft", color: "primary", class: { root: "bg-tertiary", label: "text-tertiary-foreground" } },
|
|
165
|
+
{
|
|
166
|
+
variant: "soft",
|
|
167
|
+
color: "success",
|
|
168
|
+
class: { root: "bg-success-soft", label: "text-success-soft-foreground" },
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
variant: "soft",
|
|
172
|
+
color: "warning",
|
|
173
|
+
class: { root: "bg-warning-soft", label: "text-warning-soft-foreground" },
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
variant: "soft",
|
|
177
|
+
color: "destructive",
|
|
178
|
+
class: { root: "bg-destructive-soft", label: "text-destructive-soft-foreground" },
|
|
179
|
+
},
|
|
180
|
+
{ variant: "soft", color: "info", class: { root: "bg-info-soft", label: "text-info-soft-foreground" } },
|
|
181
|
+
|
|
182
|
+
{ variant: "outline", color: "default", class: { root: "border-border", label: "text-muted-foreground" } },
|
|
183
|
+
{ variant: "outline", color: "primary", class: { root: "border-primary", label: "text-foreground" } },
|
|
184
|
+
{
|
|
185
|
+
variant: "outline",
|
|
186
|
+
color: "success",
|
|
187
|
+
class: { root: "border-success", label: "text-success-soft-foreground" },
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
variant: "outline",
|
|
191
|
+
color: "warning",
|
|
192
|
+
class: { root: "border-warning", label: "text-warning-soft-foreground" },
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
variant: "outline",
|
|
196
|
+
color: "destructive",
|
|
197
|
+
class: { root: "border-destructive", label: "text-destructive-soft-foreground" },
|
|
198
|
+
},
|
|
199
|
+
{ variant: "outline", color: "info", class: { root: "border-info", label: "text-info-soft-foreground" } },
|
|
200
|
+
|
|
201
|
+
{ variant: "ghost", color: "default", class: { label: "text-muted-foreground" } },
|
|
202
|
+
{ variant: "ghost", color: "primary", class: { label: "text-foreground" } },
|
|
203
|
+
{ variant: "ghost", color: "success", class: { label: "text-success-soft-foreground" } },
|
|
204
|
+
{ variant: "ghost", color: "warning", class: { label: "text-warning-soft-foreground" } },
|
|
205
|
+
{ variant: "ghost", color: "destructive", class: { label: "text-destructive-soft-foreground" } },
|
|
206
|
+
{ variant: "ghost", color: "info", class: { label: "text-info-soft-foreground" } },
|
|
207
|
+
],
|
|
208
|
+
defaultVariants: {
|
|
209
|
+
variant: "solid",
|
|
210
|
+
color: "default",
|
|
211
|
+
size: "md",
|
|
212
|
+
isDisabled: false,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Whether the root should render as a `Pressable` rather than a plain `View`.
|
|
218
|
+
*
|
|
219
|
+
* A badge is content until a caller gives it something to do. Mounting a
|
|
220
|
+
* `GestureDetector` regardless would put one under every tag in a list of fifty
|
|
221
|
+
* and announce each of them to assistive technology as a button with no action.
|
|
222
|
+
*
|
|
223
|
+
* `onClose` is deliberately not read here: the dismiss control is its own
|
|
224
|
+
* pressable, so a badge that can only be dismissed still has an inert root.
|
|
225
|
+
*
|
|
226
|
+
* Pure, so the matrix is reachable from `bun test`. See AGENTS.md.
|
|
227
|
+
*/
|
|
228
|
+
export function resolveBadgeInteractive({
|
|
229
|
+
onPress,
|
|
230
|
+
onLongPress,
|
|
231
|
+
}: {
|
|
232
|
+
onPress?: () => void;
|
|
233
|
+
onLongPress?: () => void;
|
|
234
|
+
}): boolean {
|
|
235
|
+
return onPress !== undefined || onLongPress !== undefined;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export type BadgeVariantProps = VariantProps<typeof badgeVariants>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { Badge, type BadgeProps } from "./badge";
|
|
2
|
+
export { type BadgeContextValue, BadgeProvider, useBadge, useBadgeContext } from "./badge.context";
|
|
3
|
+
export type { BadgeSlotProps } from "./badge.types";
|
|
4
|
+
export {
|
|
5
|
+
BADGE_COLORS,
|
|
6
|
+
BADGE_FOREGROUND_TOKEN,
|
|
7
|
+
BADGE_SIZES,
|
|
8
|
+
BADGE_VARIANTS,
|
|
9
|
+
type BadgeColor,
|
|
10
|
+
type BadgeSize,
|
|
11
|
+
type BadgeVariant,
|
|
12
|
+
type BadgeVariantProps,
|
|
13
|
+
badgeVariants,
|
|
14
|
+
resolveBadgeInteractive,
|
|
15
|
+
} from "./badge.variants";
|
|
16
|
+
export type { BadgeCloseButtonProps } from "./badge-close-button";
|
|
17
|
+
export type { BadgeLabelProps } from "./badge-label";
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# BottomSheet
|
|
2
|
+
|
|
3
|
+
A panel that slides up over everything, on `@gorhom/bottom-sheet`'s modal. The
|
|
4
|
+
library's first overlay. Compound root plus `BottomSheet.Trigger`,
|
|
5
|
+
`BottomSheet.Portal`, `BottomSheet.Overlay`, `BottomSheet.Container`,
|
|
6
|
+
`BottomSheet.Content`, `BottomSheet.ScrollView`, `BottomSheet.Close`,
|
|
7
|
+
`BottomSheet.Title`, `BottomSheet.Description` and `BottomSheet.Footer`.
|
|
8
|
+
|
|
9
|
+
`import { BottomSheet } from "@delacour/react-native-ui/bottom-sheet";`
|
|
10
|
+
|
|
11
|
+
## Files
|
|
12
|
+
|
|
13
|
+
| File | What it holds |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| `index.ts` | → `@delacour/react-native-ui/bottom-sheet` |
|
|
16
|
+
| `bottom-sheet.tsx` | Root + the `Object.assign` compound surface |
|
|
17
|
+
| `bottom-sheet-trigger.tsx` | `BottomSheet.Trigger` |
|
|
18
|
+
| `bottom-sheet-portal.tsx` | `BottomSheet.Portal`, and the overlay hoist |
|
|
19
|
+
| `bottom-sheet-overlay.tsx` | `BottomSheet.Overlay` — one of the two `withUniwind` wrappers |
|
|
20
|
+
| `bottom-sheet-container.tsx` | `BottomSheet.Container`, the modal, and the footer hoist |
|
|
21
|
+
| `bottom-sheet-content.tsx` | `BottomSheet.Content` |
|
|
22
|
+
| `bottom-sheet-scroll-view.tsx` | `BottomSheet.ScrollView` — the other wrapper |
|
|
23
|
+
| `bottom-sheet-close.tsx` | `BottomSheet.Close` |
|
|
24
|
+
| `bottom-sheet-title.tsx` | `BottomSheet.Title` |
|
|
25
|
+
| `bottom-sheet-description.tsx` | `BottomSheet.Description` |
|
|
26
|
+
| `bottom-sheet-footer.tsx` | `BottomSheet.Footer`, its sticky and inline branches |
|
|
27
|
+
| `bottom-sheet-background.tsx` | The sheet's surface — internal |
|
|
28
|
+
| `bottom-sheet-handle.tsx` | The grabber — internal |
|
|
29
|
+
| `bottom-sheet.context.tsx` | Three contexts — the sheet, the portal, the container |
|
|
30
|
+
| `bottom-sheet.variants.ts` | Pure `tv()` slots + two resolvers, no RN imports |
|
|
31
|
+
| `bottom-sheet.variants.test.ts` | |
|
|
32
|
+
| `use-bottom-sheet-input.ts` | What an `Input` needs to be a field the sheet owns |
|
|
33
|
+
|
|
34
|
+
## Design
|
|
35
|
+
|
|
36
|
+
- **Two parts are not drawn where they are written, and that is the component.**
|
|
37
|
+
gorhom takes the scrim and a pinned footer as *render props* on the modal —
|
|
38
|
+
`backdropComponent` and `footerComponent` — not as children. `BottomSheet.Portal`
|
|
39
|
+
lifts the `Overlay` out of its own children and publishes it on context;
|
|
40
|
+
`BottomSheet.Container` lifts a `Footer sticky` out of its own and clones both
|
|
41
|
+
into those slots. The alternative was a `backdrop` prop and a `footer` prop on
|
|
42
|
+
the container, which would put two parts of the anatomy somewhere the anatomy
|
|
43
|
+
cannot see them. `hoistOverlay` and `hoistFooter` live with the part that lifts
|
|
44
|
+
its own children — the rule `withIndicator` follows in [`radio.tsx`](../radio/AGENTS.md) — and the
|
|
45
|
+
decision inside the second, `resolveFooterPlacement`, is pure so `bun test`
|
|
46
|
+
reaches the matrix.
|
|
47
|
+
- **The scrim's press area stops at the top of the sheet, and rebuilding it that
|
|
48
|
+
way is the only reason `Input` works in here.** gorhom's backdrop puts a
|
|
49
|
+
full-screen `Gesture.Tap()` behind the sheet. Gesture Handler resolves a touch
|
|
50
|
+
between competing GESTURES, so a `Button` in the sheet has one of its own and
|
|
51
|
+
wins — but a `TextInput` has none, the backdrop takes the tap, and the sheet
|
|
52
|
+
CLOSES instead of the field focusing. That is what gorhom's own
|
|
53
|
+
`BottomSheetTextInput` exists to fix, and taking that route would have left
|
|
54
|
+
this package's `Input` — its variants, its `Input.Group`, its `Field`
|
|
55
|
+
cascade — unusable inside a sheet. So `BottomSheet.Overlay` passes
|
|
56
|
+
`pressBehavior="none"` (no gesture anywhere) and lays a `Pressable` over the
|
|
57
|
+
band ABOVE the sheet, sized from the sheet's own `animatedPosition`. It cannot
|
|
58
|
+
overlap the sheet, so there is nothing for it to take, and the part of the
|
|
59
|
+
scrim anyone can actually see still closes. Confirmed both ways on a
|
|
60
|
+
simulator — with gorhom's backdrop the field could not be focused at all.
|
|
61
|
+
- **Never call `dismiss()` on a modal that was never presented.** gorhom's modal
|
|
62
|
+
starts at status `INITIAL`; `dismiss()` reads that as a live sheet closing and
|
|
63
|
+
moves it to `DISMISSING`, where it stays. The next `present()` mounts the
|
|
64
|
+
portal and renders the sheet — and it never animates in, never lays out its
|
|
65
|
+
content and never fires `onChange`. Nothing throws, nothing is logged, and the
|
|
66
|
+
screen simply does not change. `BottomSheet.Container` keeps a
|
|
67
|
+
`hasPresentedRef` so the effect that mirrors `isOpen` only ever dismisses
|
|
68
|
+
something it opened. This cost an afternoon; do not "simplify" the guard away.
|
|
69
|
+
- **A portal breaks context, so the container re-publishes its own.** The sheet's
|
|
70
|
+
children are rendered by `@gorhom/portal` into a host inside
|
|
71
|
+
`BottomSheetModalProvider` — a different place in the React tree from where
|
|
72
|
+
they were written — so the `BottomSheetContext` the root provides does not
|
|
73
|
+
reach them, and `BottomSheet.Close` throws "must be rendered inside a
|
|
74
|
+
`<BottomSheet>`". `BottomSheet.Container` wraps the modal's children in the
|
|
75
|
+
same provider again. `BottomSheet.Overlay` cannot be covered that way — gorhom
|
|
76
|
+
renders the backdrop outside those children — so its press area reads gorhom's
|
|
77
|
+
`useBottomSheet()` instead, which lands on `onDismiss` and therefore on
|
|
78
|
+
`onOpenChange` anyway.
|
|
79
|
+
- **There is no size and no colour axis.** A sheet has one shape and one surface;
|
|
80
|
+
a second way to paint it would be a second thing to keep in step for no
|
|
81
|
+
question anyone is asking. `bottomSheetVariants` is slots and nothing else.
|
|
82
|
+
|
|
83
|
+
## Footer and keyboard
|
|
84
|
+
|
|
85
|
+
- **`BottomSheet.Footer` is inline by default, and `sticky` pins it.** Same word,
|
|
86
|
+
same default as [`Screen.Footer`](../screen/AGENTS.md), so the two cannot be read as opposites. The
|
|
87
|
+
two branches deliberately look different: an inline footer is in the flow and
|
|
88
|
+
inherits the sheet's surface, while a pinned one draws OVER the content and so
|
|
89
|
+
brings a background and a top hairline of its own. Without them the content
|
|
90
|
+
scrolls straight through it and its buttons are legible only where they happen
|
|
91
|
+
to overlap blank space — `Screen.Footer`'s rule about its backing, one
|
|
92
|
+
component along. A footer written `sticky` but rendered outside a container
|
|
93
|
+
falls back to the inline branch rather than throwing: there is no footer
|
|
94
|
+
position to ride, and a row of buttons in the flow is the useful failure.
|
|
95
|
+
- **A pinned footer is TALLER by the safe-area band; it is not moved up by it.**
|
|
96
|
+
gorhom's `bottomInset` translates the whole footer up instead, which leaves a
|
|
97
|
+
band the height of the home indicator between the footer and the bottom of the
|
|
98
|
+
sheet — with the content scrolling through it in plain view. So `bottomInset`
|
|
99
|
+
stays 0 and the band is part of the footer's own `paddingBottom`, putting its
|
|
100
|
+
surface against the bottom of the screen.
|
|
101
|
+
- **The keyboard takes the band back, and ONLY the band.**
|
|
102
|
+
`BOTTOM_SHEET_FOOTER_PADDING` is always there and the band collapses into it as
|
|
103
|
+
`useReanimatedKeyboardAnimation()`'s `progress` runs 0 → 1, so the controls
|
|
104
|
+
never end up flush against the keyboard. Sliding the whole footer down by the
|
|
105
|
+
band instead — the first thing tried — parks its padding behind the keyboard
|
|
106
|
+
along with it, and the buttons sit on the keys. It also has to be the footer
|
|
107
|
+
genuinely getting *shorter* rather than moving, because that is what lets the
|
|
108
|
+
body's reserve shrink with it instead of holding back the height of a home
|
|
109
|
+
indicator that is no longer on screen. Both numbers are constants rather than
|
|
110
|
+
classes, for `SCREEN_FOOTER_PADDING`'s reason: an animated padding cannot be a
|
|
111
|
+
class, and the reserve maths has to read the same value.
|
|
112
|
+
- **The footer publishes its measured height and the body reserves it with an
|
|
113
|
+
animated spacer.** Not gorhom's `enableFooterMarginAdjustment`, which turns that
|
|
114
|
+
height into content-container padding through React state — and since the
|
|
115
|
+
footer changes height on every frame of the keyboard animation, that is a
|
|
116
|
+
commit per frame. A shared value read by a spacer's animated style costs
|
|
117
|
+
nothing on the JS thread; [`Screen`](../screen/AGENTS.md) reserves its own chrome exactly this way.
|
|
118
|
+
The spacer sits OUTSIDE the classed content box, or the content's own `gap`
|
|
119
|
+
lands in front of it and silently doubles `BOTTOM_SHEET_FOOTER_GAP`. The
|
|
120
|
+
container owns the shared value because the footer and the body are siblings in
|
|
121
|
+
gorhom's tree and neither can see the other.
|
|
122
|
+
- **Do not put a `KeyboardStickyView` inside a sheet.** This is the one keyboard
|
|
123
|
+
rule that differs from [`Screen`](../screen/AGENTS.md). gorhom's `animatedFooterPosition` already
|
|
124
|
+
carries a pinned footer clear of the keyboard and the translate above gives up
|
|
125
|
+
the safe-area band on top of that; a third mechanism moving the same view would
|
|
126
|
+
fight both for prop ownership every frame.
|
|
127
|
+
- **Exactly one thing in the sheet pays for the home indicator.** With a pinned
|
|
128
|
+
footer that is the footer, whose own padding carries the band and whose height
|
|
129
|
+
the body already reserves; the content asking for it too would count it twice.
|
|
130
|
+
With no pinned footer the content is the bottom-most thing and takes it.
|
|
131
|
+
`resolveSheetBottomInset` is that decision and it is pure, so `bun test` reaches
|
|
132
|
+
it. As everywhere else the band comes from `useSafeAreaInsets()` and never from
|
|
133
|
+
`pb-safe`.
|
|
134
|
+
|
|
135
|
+
## Styling
|
|
136
|
+
|
|
137
|
+
- **`BottomSheet.ScrollView` puts its classes on an inner `View`, not on gorhom's
|
|
138
|
+
content container**, and that is not tidiness. Uniwind compiles a
|
|
139
|
+
`contentContainerClassName` into an **array** alongside any
|
|
140
|
+
`contentContainerStyle`; gorhom's footer-margin adjustment then reads
|
|
141
|
+
`paddingBottom` off a flattened style, an array flattens to an array, the band
|
|
142
|
+
reads as zero and the last row hides behind a pinned footer. One writer for
|
|
143
|
+
that style, and it is the inset. A scrollable sheet also needs
|
|
144
|
+
`enableDynamicSizing={false}` and explicit `snapPoints`, or the sheet grows to
|
|
145
|
+
the content and there is nothing to scroll within.
|
|
146
|
+
- **Two `withUniwind` wrappers, and only two.** `BottomSheetBackdrop` and
|
|
147
|
+
`BottomSheetScrollView` are leaves that take a `style` and no `className`, so
|
|
148
|
+
each is wrapped once, at module scope, in its own file (rule 7).
|
|
149
|
+
`BottomSheetView` and `BottomSheetFooter` are **not** wrapped: both take
|
|
150
|
+
children, so an ordinary `View` nested inside carries the classes instead —
|
|
151
|
+
the move [`Screen.Footer`](../screen/AGENTS.md) already makes with `KeyboardStickyView`. Reach for a
|
|
152
|
+
nested view before a wrapper whenever the third-party component has room for
|
|
153
|
+
one. The scroll view's wrapper also needs its signature restated, the way
|
|
154
|
+
`Screen.LegendList`'s does — `withUniwind`'s mapped return type collapses a
|
|
155
|
+
scrollable's animated props to a single entry.
|
|
156
|
+
- **`BottomSheet.Container` takes no `className`.** gorhom's modal wears an
|
|
157
|
+
animated style its own position writes every frame, so a class would reach
|
|
158
|
+
nothing `backgroundClassName` does not reach better. The background and the
|
|
159
|
+
handle are supplied as `backgroundComponent` and `handleComponent` — this
|
|
160
|
+
package's own views, so they take classes — and a `backgroundStyle`,
|
|
161
|
+
`handleStyle` or `handleIndicatorStyle` still arrives, forwarded as the `style`
|
|
162
|
+
each is handed. **Forwarding the background's `style` is load-bearing:** gorhom
|
|
163
|
+
passes `[StyleSheet.absoluteFill, backgroundStyle]`, and dropping it renders the
|
|
164
|
+
sheet transparent over the app.
|
|
165
|
+
|
|
166
|
+
## Behaviour and defaults
|
|
167
|
+
|
|
168
|
+
- **One callback for every way a sheet closes.** A swipe down, a press on the
|
|
169
|
+
scrim, `BottomSheet.Close` and a controlled `isOpen` all arrive at
|
|
170
|
+
`onOpenChange`, because presenting is driven by state and gorhom's `onDismiss`
|
|
171
|
+
is consumed rather than forwarded. gorhom's own `onClose` fires for the gesture
|
|
172
|
+
alone, which is the shape that makes a caller wire three handlers and still
|
|
173
|
+
miss one. `useBottomSheet().sheetRef` remains for the imperative calls with no
|
|
174
|
+
declarative form — `snapToIndex`, `expand`, `collapse`.
|
|
175
|
+
- **`accessible` defaults to `false`, and it is the least obvious line in the
|
|
176
|
+
component.** gorhom marks the sheet's content container `accessible` with the
|
|
177
|
+
label "Bottom Sheet" and the `adjustable` role. On iOS an `accessible`
|
|
178
|
+
container collapses its whole subtree into ONE element, so every field, button
|
|
179
|
+
and line of copy inside becomes unreachable to VoiceOver — the form gallery
|
|
180
|
+
route was a single "Bottom Sheet" element until this default was flipped. It is
|
|
181
|
+
[`Field`](../field/AGENTS.md)'s rule one component along: the row is `accessible={false}` so the
|
|
182
|
+
control stays the element a screen reader sees. Verified with
|
|
183
|
+
`native-describe-screen` on a simulator, not assumed.
|
|
184
|
+
- **Three defaults differ from gorhom's, and each is a modal-sheet decision.**
|
|
185
|
+
`enablePanDownToClose` is on, because a modal sheet has no collapsed resting
|
|
186
|
+
state to pan down to. The backdrop appears on index `0` and disappears on `-1`
|
|
187
|
+
rather than `1` and `0`, for the same reason — gorhom's defaults suit a
|
|
188
|
+
persistent sheet that dims the app only once expanded. And the keyboard trio
|
|
189
|
+
is `interactive` / `restore` / `adjustResize`: leaving Android on gorhom's
|
|
190
|
+
`adjustPan` slides the whole window up instead of resizing it and puts the
|
|
191
|
+
footer off-screen, where `resize` is already what Expo configures and what
|
|
192
|
+
`KeyboardProvider` requires. All are ordinary props; `BOTTOM_SHEET_KEYBOARD_DEFAULTS`
|
|
193
|
+
is a const so a test pins the trio rather than three inline literals.
|
|
194
|
+
- **`--overlay` is a token, not a `bg-black/50`.** A pure-black scrim over
|
|
195
|
+
a near-black dark theme is nearly invisible, so the two variants carry
|
|
196
|
+
different alphas — and because the alpha lives in the token, the backdrop's
|
|
197
|
+
`opacity` defaults to **1** rather than gorhom's 0.5, which would multiply the
|
|
198
|
+
two and land the scrim at a fifth of what the theme asked for. It takes no
|
|
199
|
+
`-foreground`: nothing is drawn on a scrim, and the sheet in front brings its
|
|
200
|
+
own. A test pins the token against both `@variant` blocks and pins the opacity
|
|
201
|
+
against the alpha being there.
|
|
202
|
+
|
|
203
|
+
## Input
|
|
204
|
+
|
|
205
|
+
- **`useBottomSheetInput()` is why `Input` still works in here.** A sheet only
|
|
206
|
+
grows for the keyboard when it knows the focused field is one of its own, and
|
|
207
|
+
gorhom's answer is to use its `BottomSheetTextInput` — which would leave this
|
|
208
|
+
package's `Input`, its variants, its `Input.Group` and its `Field` cascade
|
|
209
|
+
unusable inside a sheet. The registration is exposed as `{ onFocus, onBlur, ref }`
|
|
210
|
+
instead, so `Input` imports nothing from `@gorhom/bottom-sheet` and the hook is
|
|
211
|
+
inert outside a sheet. The `ref` is not optional polish: without it, moving
|
|
212
|
+
focus between two fields in one sheet reads as the keyboard closing and
|
|
213
|
+
reopening and the sheet resizes twice on the way. It covers the keyboard half only:
|
|
214
|
+
a *drag* across a field can still be claimed by the sheet's content pan, since
|
|
215
|
+
gorhom's input is built on Gesture Handler's `TextInput` and this one is not.
|
|
216
|
+
If that ever bites, the fix is `enableContentPanningGesture={false}` on the
|
|
217
|
+
container. The *tap* half is the scrim's problem and is solved above.
|
|
218
|
+
|
|
219
|
+
## Structure
|
|
220
|
+
|
|
221
|
+
- **There is no `bottom-sheet.types.ts`.** Nothing is shared by two modules that
|
|
222
|
+
is not already a type somewhere better: the text parts take `TextPresetProps`,
|
|
223
|
+
which `Text` already exports, and the container reads its footer's props from
|
|
224
|
+
`bottom-sheet-footer.tsx` directly — no cycle, since a part never imports the
|
|
225
|
+
container. A types leaf holding one alias would be a file with nothing in it.
|
|
226
|
+
- **There is no `FullWindowOverlay`, so nothing imports `react-native-screens`.**
|
|
227
|
+
`BottomSheetModal` hosts itself in the `BottomSheetModalProvider` that
|
|
228
|
+
`DelacourProvider` mounts, which covers everything short of drawing over a
|
|
229
|
+
*native* stack modal. gorhom's `containerComponent` is the escape hatch if a
|
|
230
|
+
call site ever needs that; it is not worth the optional peer, the iOS element
|
|
231
|
+
inspector and the keyboard caveats before then.
|
|
232
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { BottomSheetBackgroundProps as GorhomBottomSheetBackgroundProps } from "@gorhom/bottom-sheet";
|
|
2
|
+
import type { ReactElement } from "react";
|
|
3
|
+
import { View } from "react-native";
|
|
4
|
+
import { bottomSheetVariants } from "./bottom-sheet.variants";
|
|
5
|
+
|
|
6
|
+
export type BottomSheetBackgroundProps = Partial<GorhomBottomSheetBackgroundProps> & {
|
|
7
|
+
className?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The sheet's own surface, drawn behind the handle and the content.
|
|
12
|
+
*
|
|
13
|
+
* Supplied to gorhom as `backgroundComponent` rather than styled through
|
|
14
|
+
* `backgroundStyle`, so the surface is a className like every other surface in
|
|
15
|
+
* this package — and so `backgroundClassName` on `BottomSheet.Container` has
|
|
16
|
+
* something to reach. A `backgroundStyle` a caller passes still arrives, merged
|
|
17
|
+
* into the `style` below.
|
|
18
|
+
*
|
|
19
|
+
* **Forwarding `style` is load-bearing.** gorhom hands this component
|
|
20
|
+
* `[StyleSheet.absoluteFill, backgroundStyle]`; drop it and the surface collapses
|
|
21
|
+
* to nothing and the sheet renders transparent over the app.
|
|
22
|
+
*
|
|
23
|
+
* The two animated variables are destructured off rather than spread: they are
|
|
24
|
+
* shared values, and a `View` has no use for either.
|
|
25
|
+
*/
|
|
26
|
+
export function BottomSheetBackground({
|
|
27
|
+
animatedIndex: _animatedIndex,
|
|
28
|
+
animatedPosition: _animatedPosition,
|
|
29
|
+
className,
|
|
30
|
+
style,
|
|
31
|
+
...props
|
|
32
|
+
}: BottomSheetBackgroundProps): ReactElement {
|
|
33
|
+
return <View className={bottomSheetVariants().background({ className })} style={style} {...props} />;
|
|
34
|
+
}
|
|
35
|
+
BottomSheetBackground.displayName = "DelacourUI.BottomSheet.Background";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type ReactElement, useCallback } from "react";
|
|
2
|
+
import { IconCrossSmall } from "../../icons/central";
|
|
3
|
+
import { Icon } from "../icon";
|
|
4
|
+
import { Pressable, type PressableProps } from "../pressable";
|
|
5
|
+
import { useBottomSheetPart } from "./bottom-sheet.context";
|
|
6
|
+
import { BOTTOM_SHEET_CLOSE_HIT_SLOP, bottomSheetVariants } from "./bottom-sheet.variants";
|
|
7
|
+
|
|
8
|
+
export type BottomSheetCloseProps = Omit<PressableProps, "asChild" | "busy" | "children" | "disabled">;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The sheet's dismiss control.
|
|
12
|
+
*
|
|
13
|
+
* Positioned out of the content's flow, in the top-right of the sheet, so it does
|
|
14
|
+
* not push the title down or take a row of its own. `BottomSheet.Title` reserves
|
|
15
|
+
* the clearance for it on every sheet — see `bottomSheetVariants`.
|
|
16
|
+
*
|
|
17
|
+
* Closing goes through the same `onOpenChange` a swipe-down and a backdrop press
|
|
18
|
+
* do, so a caller has one callback to watch rather than three.
|
|
19
|
+
*
|
|
20
|
+
* `fade` rather than a spring: a scale on a glyph this small reads as a jitter,
|
|
21
|
+
* the reason `Badge.CloseButton` presses the same way. The slop is there because
|
|
22
|
+
* a bare glyph in a corner has no padded capsule to bring it up to the 44pt
|
|
23
|
+
* minimum — the case `Checkbox` mints slop for.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* <BottomSheet.Content>
|
|
27
|
+
* <BottomSheet.Close />
|
|
28
|
+
* <BottomSheet.Title>Filters</BottomSheet.Title>
|
|
29
|
+
* </BottomSheet.Content>
|
|
30
|
+
*/
|
|
31
|
+
export function BottomSheetClose({
|
|
32
|
+
accessibilityLabel = "Close",
|
|
33
|
+
className,
|
|
34
|
+
feedback = "fade",
|
|
35
|
+
hitSlop = BOTTOM_SHEET_CLOSE_HIT_SLOP,
|
|
36
|
+
onPress,
|
|
37
|
+
...props
|
|
38
|
+
}: BottomSheetCloseProps): ReactElement {
|
|
39
|
+
const { close } = useBottomSheetPart("BottomSheet.Close");
|
|
40
|
+
|
|
41
|
+
const handlePress = useCallback(() => {
|
|
42
|
+
close();
|
|
43
|
+
onPress?.();
|
|
44
|
+
}, [close, onPress]);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Pressable
|
|
48
|
+
accessibilityLabel={accessibilityLabel}
|
|
49
|
+
accessibilityRole="button"
|
|
50
|
+
className={bottomSheetVariants().close({ className })}
|
|
51
|
+
feedback={feedback}
|
|
52
|
+
hitSlop={hitSlop}
|
|
53
|
+
onPress={handlePress}
|
|
54
|
+
{...props}
|
|
55
|
+
>
|
|
56
|
+
<Icon color="muted-foreground" icon={IconCrossSmall} />
|
|
57
|
+
</Pressable>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
BottomSheetClose.displayName = "DelacourUI.BottomSheet.Close";
|