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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/README.md +113 -0
  2. package/package.json +125 -0
  3. package/src/components/accordion/AGENTS.md +192 -0
  4. package/src/components/accordion/accordion-content.tsx +125 -0
  5. package/src/components/accordion/accordion-description.tsx +17 -0
  6. package/src/components/accordion/accordion-indicator.tsx +104 -0
  7. package/src/components/accordion/accordion-item.tsx +78 -0
  8. package/src/components/accordion/accordion-title.tsx +18 -0
  9. package/src/components/accordion/accordion-trigger.tsx +148 -0
  10. package/src/components/accordion/accordion.context.tsx +153 -0
  11. package/src/components/accordion/accordion.tsx +253 -0
  12. package/src/components/accordion/accordion.types.ts +11 -0
  13. package/src/components/accordion/accordion.variants.test.ts +435 -0
  14. package/src/components/accordion/accordion.variants.ts +358 -0
  15. package/src/components/accordion/index.ts +45 -0
  16. package/src/components/badge/AGENTS.md +83 -0
  17. package/src/components/badge/badge-close-button.tsx +48 -0
  18. package/src/components/badge/badge-end-content.tsx +15 -0
  19. package/src/components/badge/badge-label.tsx +24 -0
  20. package/src/components/badge/badge-start-content.tsx +16 -0
  21. package/src/components/badge/badge.context.tsx +64 -0
  22. package/src/components/badge/badge.tsx +192 -0
  23. package/src/components/badge/badge.types.ts +10 -0
  24. package/src/components/badge/badge.variants.test.ts +269 -0
  25. package/src/components/badge/badge.variants.ts +238 -0
  26. package/src/components/badge/index.ts +17 -0
  27. package/src/components/bottom-sheet/AGENTS.md +232 -0
  28. package/src/components/bottom-sheet/bottom-sheet-background.tsx +35 -0
  29. package/src/components/bottom-sheet/bottom-sheet-close.tsx +60 -0
  30. package/src/components/bottom-sheet/bottom-sheet-container.tsx +245 -0
  31. package/src/components/bottom-sheet/bottom-sheet-content.tsx +66 -0
  32. package/src/components/bottom-sheet/bottom-sheet-description.tsx +29 -0
  33. package/src/components/bottom-sheet/bottom-sheet-footer.tsx +148 -0
  34. package/src/components/bottom-sheet/bottom-sheet-handle.tsx +47 -0
  35. package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +180 -0
  36. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +72 -0
  37. package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +95 -0
  38. package/src/components/bottom-sheet/bottom-sheet-title.tsx +24 -0
  39. package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +73 -0
  40. package/src/components/bottom-sheet/bottom-sheet.context.tsx +135 -0
  41. package/src/components/bottom-sheet/bottom-sheet.tsx +125 -0
  42. package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +276 -0
  43. package/src/components/bottom-sheet/bottom-sheet.variants.ts +199 -0
  44. package/src/components/bottom-sheet/index.ts +40 -0
  45. package/src/components/bottom-sheet/use-bottom-sheet-input.ts +125 -0
  46. package/src/components/button/AGENTS.md +217 -0
  47. package/src/components/button/button-end-content.tsx +15 -0
  48. package/src/components/button/button-group-separator.tsx +37 -0
  49. package/src/components/button/button-group-text.tsx +101 -0
  50. package/src/components/button/button-group.tsx +158 -0
  51. package/src/components/button/button-label.tsx +24 -0
  52. package/src/components/button/button-start-content.tsx +15 -0
  53. package/src/components/button/button.context.tsx +187 -0
  54. package/src/components/button/button.tsx +273 -0
  55. package/src/components/button/button.types.ts +10 -0
  56. package/src/components/button/button.variants.test.ts +811 -0
  57. package/src/components/button/button.variants.ts +480 -0
  58. package/src/components/button/index.ts +50 -0
  59. package/src/components/chart/AGENTS.md +210 -0
  60. package/src/components/chart/chart-area.tsx +93 -0
  61. package/src/components/chart/chart-bar.tsx +103 -0
  62. package/src/components/chart/chart-bars.tsx +75 -0
  63. package/src/components/chart/chart-candlestick.tsx +60 -0
  64. package/src/components/chart/chart-grid.tsx +33 -0
  65. package/src/components/chart/chart-legend.tsx +41 -0
  66. package/src/components/chart/chart-line.tsx +50 -0
  67. package/src/components/chart/chart-scatter.tsx +35 -0
  68. package/src/components/chart/chart-tooltip-dot.tsx +70 -0
  69. package/src/components/chart/chart-tooltip-x.tsx +121 -0
  70. package/src/components/chart/chart-tooltip-y.tsx +55 -0
  71. package/src/components/chart/chart-tooltip.tsx +133 -0
  72. package/src/components/chart/chart-x-axis.tsx +27 -0
  73. package/src/components/chart/chart-y-axis.tsx +21 -0
  74. package/src/components/chart/chart.context.tsx +127 -0
  75. package/src/components/chart/chart.tsx +448 -0
  76. package/src/components/chart/chart.types.ts +108 -0
  77. package/src/components/chart/chart.variants.test.ts +893 -0
  78. package/src/components/chart/chart.variants.ts +726 -0
  79. package/src/components/chart/index.ts +103 -0
  80. package/src/components/chart/pie-chart-center.tsx +40 -0
  81. package/src/components/chart/pie-chart-label.tsx +56 -0
  82. package/src/components/chart/pie-chart-slice.tsx +45 -0
  83. package/src/components/chart/pie-chart-tooltip.tsx +85 -0
  84. package/src/components/chart/pie-chart.context.tsx +77 -0
  85. package/src/components/chart/pie-chart.tsx +247 -0
  86. package/src/components/chart/use-chart-font.ts +25 -0
  87. package/src/components/chart/use-chart-palette.ts +38 -0
  88. package/src/components/checkbox/AGENTS.md +185 -0
  89. package/src/components/checkbox/checkbox-box.tsx +152 -0
  90. package/src/components/checkbox/checkbox-group.tsx +107 -0
  91. package/src/components/checkbox/checkbox-label.tsx +34 -0
  92. package/src/components/checkbox/checkbox.context.tsx +136 -0
  93. package/src/components/checkbox/checkbox.tsx +258 -0
  94. package/src/components/checkbox/checkbox.types.ts +14 -0
  95. package/src/components/checkbox/checkbox.variants.test.ts +634 -0
  96. package/src/components/checkbox/checkbox.variants.ts +484 -0
  97. package/src/components/checkbox/index.ts +42 -0
  98. package/src/components/field/AGENTS.md +100 -0
  99. package/src/components/field/field-content.tsx +24 -0
  100. package/src/components/field/field-description.tsx +29 -0
  101. package/src/components/field/field-error.tsx +61 -0
  102. package/src/components/field/field-group.tsx +22 -0
  103. package/src/components/field/field-label.tsx +33 -0
  104. package/src/components/field/field-legend.tsx +34 -0
  105. package/src/components/field/field-separator.tsx +81 -0
  106. package/src/components/field/field-set.tsx +22 -0
  107. package/src/components/field/field.context.tsx +78 -0
  108. package/src/components/field/field.tsx +144 -0
  109. package/src/components/field/field.types.ts +15 -0
  110. package/src/components/field/field.variants.test.ts +216 -0
  111. package/src/components/field/field.variants.ts +136 -0
  112. package/src/components/field/index.ts +21 -0
  113. package/src/components/icon/AGENTS.md +104 -0
  114. package/src/components/icon/icon.context.tsx +29 -0
  115. package/src/components/icon/icon.tsx +92 -0
  116. package/src/components/icon/icon.variants.test.ts +111 -0
  117. package/src/components/icon/icon.variants.ts +79 -0
  118. package/src/components/icon/index.ts +11 -0
  119. package/src/components/input/AGENTS.md +126 -0
  120. package/src/components/input/index.ts +27 -0
  121. package/src/components/input/input-group-decorator.tsx +84 -0
  122. package/src/components/input/input-group-prefix.tsx +16 -0
  123. package/src/components/input/input-group-suffix.tsx +15 -0
  124. package/src/components/input/input-group.tsx +140 -0
  125. package/src/components/input/input.context.tsx +87 -0
  126. package/src/components/input/input.tsx +184 -0
  127. package/src/components/input/input.types.ts +10 -0
  128. package/src/components/input/input.variants.test.ts +369 -0
  129. package/src/components/input/input.variants.ts +329 -0
  130. package/src/components/list-group/AGENTS.md +67 -0
  131. package/src/components/list-group/index.ts +20 -0
  132. package/src/components/list-group/list-group-item-content.tsx +10 -0
  133. package/src/components/list-group/list-group-item-description.tsx +12 -0
  134. package/src/components/list-group/list-group-item-prefix.tsx +30 -0
  135. package/src/components/list-group/list-group-item-suffix.tsx +55 -0
  136. package/src/components/list-group/list-group-item-title.tsx +17 -0
  137. package/src/components/list-group/list-group-item.tsx +87 -0
  138. package/src/components/list-group/list-group.context.tsx +65 -0
  139. package/src/components/list-group/list-group.tsx +116 -0
  140. package/src/components/list-group/list-group.types.ts +14 -0
  141. package/src/components/list-group/list-group.variants.test.ts +223 -0
  142. package/src/components/list-group/list-group.variants.ts +107 -0
  143. package/src/components/pressable/AGENTS.md +64 -0
  144. package/src/components/pressable/index.ts +9 -0
  145. package/src/components/pressable/pressable.tsx +256 -0
  146. package/src/components/pressable/pressable.variants.test.ts +99 -0
  147. package/src/components/pressable/pressable.variants.ts +63 -0
  148. package/src/components/provider/AGENTS.md +90 -0
  149. package/src/components/provider/index.ts +1 -0
  150. package/src/components/provider/provider.tsx +105 -0
  151. package/src/components/radio/AGENTS.md +247 -0
  152. package/src/components/radio/index.ts +35 -0
  153. package/src/components/radio/radio-group.tsx +126 -0
  154. package/src/components/radio/radio-indicator.tsx +91 -0
  155. package/src/components/radio/radio-label.tsx +39 -0
  156. package/src/components/radio/radio.context.tsx +132 -0
  157. package/src/components/radio/radio.tsx +215 -0
  158. package/src/components/radio/radio.variants.test.ts +581 -0
  159. package/src/components/radio/radio.variants.ts +272 -0
  160. package/src/components/screen/AGENTS.md +287 -0
  161. package/src/components/screen/index.ts +85 -0
  162. package/src/components/screen/screen-chat-list.tsx +485 -0
  163. package/src/components/screen/screen-content.tsx +69 -0
  164. package/src/components/screen/screen-debug.ts +43 -0
  165. package/src/components/screen/screen-error.tsx +61 -0
  166. package/src/components/screen/screen-flat-list.tsx +88 -0
  167. package/src/components/screen/screen-footer-background.tsx +60 -0
  168. package/src/components/screen/screen-footer.tsx +170 -0
  169. package/src/components/screen/screen-header.tsx +36 -0
  170. package/src/components/screen/screen-legend-list.tsx +105 -0
  171. package/src/components/screen/screen-list-component.tsx +23 -0
  172. package/src/components/screen/screen-loading.tsx +50 -0
  173. package/src/components/screen/screen-navbar-back-button.tsx +79 -0
  174. package/src/components/screen/screen-navbar-background.tsx +45 -0
  175. package/src/components/screen/screen-navbar-subtitle.tsx +26 -0
  176. package/src/components/screen/screen-navbar-title.tsx +31 -0
  177. package/src/components/screen/screen-navbar.tsx +154 -0
  178. package/src/components/screen/screen-root.tsx +38 -0
  179. package/src/components/screen/screen-scroll-area.tsx +116 -0
  180. package/src/components/screen/screen-scroll-shadow.tsx +186 -0
  181. package/src/components/screen/screen-section-list.tsx +81 -0
  182. package/src/components/screen/screen-view.tsx +59 -0
  183. package/src/components/screen/screen.context.tsx +167 -0
  184. package/src/components/screen/screen.tsx +114 -0
  185. package/src/components/screen/screen.types.ts +66 -0
  186. package/src/components/screen/screen.variants.test.ts +602 -0
  187. package/src/components/screen/screen.variants.ts +475 -0
  188. package/src/components/screen/use-screen-scroll-insets.ts +218 -0
  189. package/src/components/separator/AGENTS.md +30 -0
  190. package/src/components/separator/index.ts +8 -0
  191. package/src/components/separator/separator.tsx +85 -0
  192. package/src/components/slider/AGENTS.md +274 -0
  193. package/src/components/slider/index.ts +48 -0
  194. package/src/components/slider/slider-fill.tsx +68 -0
  195. package/src/components/slider/slider-output.tsx +55 -0
  196. package/src/components/slider/slider-thumb.tsx +161 -0
  197. package/src/components/slider/slider-track.tsx +233 -0
  198. package/src/components/slider/slider.context.tsx +118 -0
  199. package/src/components/slider/slider.tsx +321 -0
  200. package/src/components/slider/slider.types.ts +26 -0
  201. package/src/components/slider/slider.variants.test.ts +804 -0
  202. package/src/components/slider/slider.variants.ts +607 -0
  203. package/src/components/spinner/AGENTS.md +73 -0
  204. package/src/components/spinner/index.ts +17 -0
  205. package/src/components/spinner/spinner-arc.tsx +90 -0
  206. package/src/components/spinner/spinner-content.tsx +63 -0
  207. package/src/components/spinner/spinner.context.tsx +52 -0
  208. package/src/components/spinner/spinner.tsx +128 -0
  209. package/src/components/spinner/spinner.variants.test.ts +273 -0
  210. package/src/components/spinner/spinner.variants.ts +187 -0
  211. package/src/components/switch/AGENTS.md +213 -0
  212. package/src/components/switch/index.ts +42 -0
  213. package/src/components/switch/switch-content.tsx +111 -0
  214. package/src/components/switch/switch-end-content.tsx +18 -0
  215. package/src/components/switch/switch-start-content.tsx +20 -0
  216. package/src/components/switch/switch-thumb.tsx +102 -0
  217. package/src/components/switch/switch.context.tsx +79 -0
  218. package/src/components/switch/switch.tsx +423 -0
  219. package/src/components/switch/switch.types.ts +14 -0
  220. package/src/components/switch/switch.variants.test.ts +570 -0
  221. package/src/components/switch/switch.variants.ts +511 -0
  222. package/src/components/tabs/AGENTS.md +286 -0
  223. package/src/components/tabs/index.ts +69 -0
  224. package/src/components/tabs/tabs-content.tsx +65 -0
  225. package/src/components/tabs/tabs-indicator.tsx +97 -0
  226. package/src/components/tabs/tabs-label.tsx +62 -0
  227. package/src/components/tabs/tabs-list.tsx +139 -0
  228. package/src/components/tabs/tabs-pager.tsx +59 -0
  229. package/src/components/tabs/tabs-scroll-view.tsx +171 -0
  230. package/src/components/tabs/tabs-separator.tsx +73 -0
  231. package/src/components/tabs/tabs-trigger.tsx +210 -0
  232. package/src/components/tabs/tabs.context.tsx +294 -0
  233. package/src/components/tabs/tabs.tsx +435 -0
  234. package/src/components/tabs/tabs.types.ts +13 -0
  235. package/src/components/tabs/tabs.variants.test.ts +1020 -0
  236. package/src/components/tabs/tabs.variants.ts +672 -0
  237. package/src/components/text/AGENTS.md +95 -0
  238. package/src/components/text/index.ts +25 -0
  239. package/src/components/text/text.context.tsx +60 -0
  240. package/src/components/text/text.tsx +251 -0
  241. package/src/components/text/text.variants.test.ts +422 -0
  242. package/src/components/text/text.variants.ts +282 -0
  243. package/src/display-name.test.ts +145 -0
  244. package/src/docs.test.ts +98 -0
  245. package/src/expo/navigation-theme.tsx +58 -0
  246. package/src/hooks/use-controllable-state.ts +45 -0
  247. package/src/hooks/use-keyboard-state-sync.tsx +147 -0
  248. package/src/hooks/use-navigation-theme.ts +78 -0
  249. package/src/hooks/use-theme-color.ts +44 -0
  250. package/src/icons/central.ts +1 -0
  251. package/src/lib/cn.test.ts +136 -0
  252. package/src/lib/cn.ts +29 -0
  253. package/src/lib/color.test.ts +80 -0
  254. package/src/lib/color.ts +79 -0
  255. package/src/lib/compose-refs.test.ts +64 -0
  256. package/src/lib/compose-refs.ts +37 -0
  257. package/src/lib/keyboard-animation.test.ts +33 -0
  258. package/src/lib/keyboard-animation.ts +28 -0
  259. package/src/lib/merge-props.test.ts +82 -0
  260. package/src/lib/merge-props.ts +47 -0
  261. package/src/lib/navigation-theme.test.ts +49 -0
  262. package/src/lib/navigation-theme.ts +53 -0
  263. package/src/lib/slot.tsx +45 -0
  264. package/src/lib/tv.ts +20 -0
  265. package/src/styles/base.css +2 -0
  266. package/src/styles/geometry.test.ts +117 -0
  267. package/src/styles/index.css +3 -0
  268. package/src/styles/theme-tokens.test.ts +252 -0
  269. package/src/styles/theme.css +447 -0
  270. package/src/styles/tokens.css +132 -0
  271. package/src/styles/tokens.test.ts +181 -0
  272. package/src/styles/tokens.ts +86 -0
  273. package/src/uniwind-env.d.ts +1 -0
@@ -0,0 +1,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";