@astryxdesign/core 0.4.4 → 0.4.5-canary.1fdecb1

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 (275) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/BottomSheet/BottomSheet.d.ts +11 -2
  3. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheet.js +4 -0
  5. package/dist/BottomSheet/BottomSheetPanel.d.ts +5 -2
  6. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  7. package/dist/BottomSheet/BottomSheetPanel.js +65 -15
  8. package/dist/BottomSheet/index.d.ts +1 -1
  9. package/dist/BottomSheet/index.d.ts.map +1 -1
  10. package/dist/BottomSheet/snapOffsets.d.ts +57 -20
  11. package/dist/BottomSheet/snapOffsets.d.ts.map +1 -1
  12. package/dist/BottomSheet/snapOffsets.js +106 -30
  13. package/dist/BottomSheet/useSheetGestures.d.ts +13 -8
  14. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  15. package/dist/BottomSheet/useSheetGestures.js +160 -64
  16. package/dist/Calendar/Calendar.d.ts +17 -0
  17. package/dist/Calendar/Calendar.d.ts.map +1 -1
  18. package/dist/Calendar/Calendar.js +34 -3
  19. package/dist/Calendar/getStandaloneShortWeekdayNames.d.ts +14 -0
  20. package/dist/Calendar/getStandaloneShortWeekdayNames.d.ts.map +1 -0
  21. package/dist/Calendar/getStandaloneShortWeekdayNames.js +26 -0
  22. package/dist/Calendar/hooks/useCalendarConstraints.d.ts +22 -1
  23. package/dist/Calendar/hooks/useCalendarConstraints.d.ts.map +1 -1
  24. package/dist/Calendar/hooks/useCalendarConstraints.js +26 -4
  25. package/dist/Calendar/hooks/useCalendarDays.d.ts.map +1 -1
  26. package/dist/Calendar/hooks/useCalendarDays.js +14 -10
  27. package/dist/Calendar/standaloneShortWeekdayNames.generated.d.ts +47 -0
  28. package/dist/Calendar/standaloneShortWeekdayNames.generated.d.ts.map +1 -0
  29. package/dist/Calendar/standaloneShortWeekdayNames.generated.js +40 -0
  30. package/dist/Center/Center.d.ts +23 -1
  31. package/dist/Center/Center.d.ts.map +1 -1
  32. package/dist/Center/Center.js +12 -6
  33. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  34. package/dist/CheckboxInput/CheckboxInput.js +9 -0
  35. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  36. package/dist/ComplexSelector/ComplexSelector.js +6 -1
  37. package/dist/DateInput/DateInput.d.ts.map +1 -1
  38. package/dist/DateInput/DateInput.js +6 -1
  39. package/dist/DateRangeInput/DateRangeInput.d.ts +23 -1
  40. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  41. package/dist/DateRangeInput/DateRangeInput.js +38 -3
  42. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  43. package/dist/DateTimeInput/DateTimeInput.js +7 -2
  44. package/dist/Dialog/Dialog.d.ts.map +1 -1
  45. package/dist/Dialog/Dialog.js +3 -3
  46. package/dist/Field/FieldLabel.d.ts +2 -1
  47. package/dist/Field/FieldLabel.d.ts.map +1 -1
  48. package/dist/Field/FieldLabel.js +21 -3
  49. package/dist/Field/InputClearButton.d.ts +2 -1
  50. package/dist/Field/InputClearButton.d.ts.map +1 -1
  51. package/dist/Field/InputClearButton.js +6 -1
  52. package/dist/FormLayout/FormLayout.d.ts +24 -2
  53. package/dist/FormLayout/FormLayout.d.ts.map +1 -1
  54. package/dist/FormLayout/FormLayout.js +7 -2
  55. package/dist/FormLayout/FormLayoutContext.d.ts +15 -2
  56. package/dist/FormLayout/FormLayoutContext.d.ts.map +1 -1
  57. package/dist/FormLayout/FormLayoutContext.js +19 -4
  58. package/dist/FormLayout/index.d.ts +1 -1
  59. package/dist/FormLayout/index.d.ts.map +1 -1
  60. package/dist/Item/Item.d.ts +9 -1
  61. package/dist/Item/Item.d.ts.map +1 -1
  62. package/dist/Item/Item.js +45 -7
  63. package/dist/Layer/useLayer.d.ts.map +1 -1
  64. package/dist/Layer/useLayer.js +3 -2
  65. package/dist/Layout/container.stylex.d.ts +4 -2
  66. package/dist/Layout/container.stylex.d.ts.map +1 -1
  67. package/dist/Layout/container.stylex.js +19 -11
  68. package/dist/Layout/index.d.ts +1 -0
  69. package/dist/Layout/index.d.ts.map +1 -1
  70. package/dist/Layout/index.js +4 -0
  71. package/dist/Layout/padding.stylex.d.ts +315 -12
  72. package/dist/Layout/padding.stylex.d.ts.map +1 -1
  73. package/dist/Layout/padding.stylex.js +389 -12
  74. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  75. package/dist/Lightbox/Lightbox.js +2 -1
  76. package/dist/Markdown/Markdown.d.ts +8 -0
  77. package/dist/Markdown/Markdown.d.ts.map +1 -1
  78. package/dist/Markdown/Markdown.js +31 -3
  79. package/dist/Markdown/parser.d.ts +14 -2
  80. package/dist/Markdown/parser.d.ts.map +1 -1
  81. package/dist/Markdown/parser.js +50 -2
  82. package/dist/MobileNav/MobileNav.d.ts +1 -0
  83. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  84. package/dist/MobileNav/MobileNav.js +24 -6
  85. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  86. package/dist/MultiSelector/MultiSelector.js +6 -1
  87. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  88. package/dist/NumberInput/NumberInput.js +36 -3
  89. package/dist/Outline/parseOutlineFromMarkdown.d.ts +2 -0
  90. package/dist/Outline/parseOutlineFromMarkdown.d.ts.map +1 -1
  91. package/dist/Outline/parseOutlineFromMarkdown.js +5 -31
  92. package/dist/RadioList/RadioList.d.ts.map +1 -1
  93. package/dist/RadioList/RadioList.js +12 -1
  94. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  95. package/dist/Resizable/ResizeHandle.js +3 -3
  96. package/dist/Section/Section.d.ts +36 -1
  97. package/dist/Section/Section.d.ts.map +1 -1
  98. package/dist/Section/Section.js +12 -2
  99. package/dist/Selector/Selector.d.ts +26 -1
  100. package/dist/Selector/Selector.d.ts.map +1 -1
  101. package/dist/Selector/Selector.js +93 -15
  102. package/dist/Selector/SelectorOption.d.ts +14 -1
  103. package/dist/Selector/SelectorOption.d.ts.map +1 -1
  104. package/dist/Selector/SelectorOption.js +6 -0
  105. package/dist/Selector/SelectorRowLayoutContext.d.ts +8 -0
  106. package/dist/Selector/SelectorRowLayoutContext.d.ts.map +1 -0
  107. package/dist/Selector/SelectorRowLayoutContext.js +27 -0
  108. package/dist/Selector/types.d.ts +1 -0
  109. package/dist/Selector/types.d.ts.map +1 -1
  110. package/dist/Slider/Slider.d.ts.map +1 -1
  111. package/dist/Slider/Slider.js +2 -2
  112. package/dist/Stack/Stack.d.ts +23 -1
  113. package/dist/Stack/Stack.d.ts.map +1 -1
  114. package/dist/Stack/Stack.js +12 -6
  115. package/dist/StatusDot/StatusDot.d.ts +43 -3
  116. package/dist/StatusDot/StatusDot.d.ts.map +1 -1
  117. package/dist/StatusDot/StatusDot.js +42 -5
  118. package/dist/Switch/Switch.d.ts.map +1 -1
  119. package/dist/Switch/Switch.js +9 -0
  120. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  121. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +7 -2
  122. package/dist/TextArea/TextArea.d.ts.map +1 -1
  123. package/dist/TextArea/TextArea.js +6 -1
  124. package/dist/TextInput/TextInput.d.ts.map +1 -1
  125. package/dist/TextInput/TextInput.js +6 -1
  126. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  127. package/dist/TimeInput/TimeInput.js +6 -1
  128. package/dist/astryx.css +40 -21
  129. package/dist/hooks/scrollbarGutter.d.ts +28 -0
  130. package/dist/hooks/scrollbarGutter.d.ts.map +1 -0
  131. package/dist/hooks/scrollbarGutter.js +103 -0
  132. package/dist/hooks/useResolvedRequired.d.ts +19 -0
  133. package/dist/hooks/useResolvedRequired.d.ts.map +1 -0
  134. package/dist/hooks/useResolvedRequired.js +40 -0
  135. package/dist/hooks/useScrollLock.d.ts +4 -0
  136. package/dist/hooks/useScrollLock.d.ts.map +1 -1
  137. package/dist/hooks/useScrollLock.js +13 -1
  138. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  139. package/dist/theme/derivedVarRegistry.js +7 -0
  140. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  141. package/dist/theme/generateThemeRules.js +37 -4
  142. package/dist/utils/plainDate.d.ts +6 -0
  143. package/dist/utils/plainDate.d.ts.map +1 -1
  144. package/dist/utils/plainDate.js +12 -0
  145. package/locales/af-ZA.json +958 -0
  146. package/locales/ar-SA.json +998 -0
  147. package/locales/ca-ES.json +974 -0
  148. package/locales/cs-CZ.json +990 -0
  149. package/locales/da-DK.json +966 -0
  150. package/locales/de-DE.json +966 -0
  151. package/locales/el-GR.json +990 -0
  152. package/locales/en.json +8 -0
  153. package/locales/es-ES.json +978 -0
  154. package/locales/fi-FI.json +990 -0
  155. package/locales/fr-FR.json +928 -0
  156. package/locales/he-IL.json +990 -0
  157. package/locales/hu-HU.json +990 -0
  158. package/locales/it-IT.json +978 -0
  159. package/locales/ja-JP.json +998 -0
  160. package/locales/ko-KR.json +990 -0
  161. package/locales/nl-NL.json +954 -0
  162. package/locales/no-NO.json +978 -0
  163. package/locales/pl-PL.json +982 -0
  164. package/locales/pseudo.json +6 -0
  165. package/locales/pt-BR.json +982 -0
  166. package/locales/pt-PT.json +982 -0
  167. package/locales/ro-RO.json +982 -0
  168. package/locales/ru-RU.json +990 -0
  169. package/locales/sr-SP.json +990 -0
  170. package/locales/sv-SE.json +986 -0
  171. package/locales/tr-TR.json +990 -0
  172. package/locales/uk-UA.json +990 -0
  173. package/locales/vi-VN.json +986 -0
  174. package/locales/zh-CN.json +998 -0
  175. package/locales/zh-TW.json +998 -0
  176. package/package.json +3 -3
  177. package/src/BottomSheet/BottomSheet.doc.mjs +36 -3
  178. package/src/BottomSheet/BottomSheet.test.tsx +177 -2
  179. package/src/BottomSheet/BottomSheet.tsx +16 -2
  180. package/src/BottomSheet/BottomSheetPanel.test.tsx +41 -0
  181. package/src/BottomSheet/BottomSheetPanel.tsx +95 -15
  182. package/src/BottomSheet/index.ts +5 -1
  183. package/src/BottomSheet/snapOffsets.test.ts +114 -47
  184. package/src/BottomSheet/snapOffsets.ts +119 -33
  185. package/src/BottomSheet/useSheetGestures.test.ts +113 -15
  186. package/src/BottomSheet/useSheetGestures.ts +196 -100
  187. package/src/Calendar/Calendar.doc.mjs +16 -0
  188. package/src/Calendar/Calendar.test.tsx +137 -0
  189. package/src/Calendar/Calendar.tsx +65 -2
  190. package/src/Calendar/getStandaloneShortWeekdayNames.test.ts +54 -0
  191. package/src/Calendar/getStandaloneShortWeekdayNames.ts +38 -0
  192. package/src/Calendar/hooks/useCalendarConstraints.ts +54 -3
  193. package/src/Calendar/hooks/useCalendarDays.ts +13 -10
  194. package/src/Calendar/standaloneShortWeekdayNames.generated.ts +50 -0
  195. package/src/Center/Center.doc.mjs +48 -0
  196. package/src/Center/Center.test.tsx +158 -0
  197. package/src/Center/Center.tsx +50 -9
  198. package/src/CheckboxInput/CheckboxInput.tsx +6 -0
  199. package/src/ComplexSelector/ComplexSelector.tsx +3 -1
  200. package/src/DateInput/DateInput.tsx +3 -1
  201. package/src/DateRangeInput/DateRangeInput.doc.mjs +16 -0
  202. package/src/DateRangeInput/DateRangeInput.test.tsx +81 -1
  203. package/src/DateRangeInput/DateRangeInput.tsx +72 -1
  204. package/src/DateTimeInput/DateTimeInput.tsx +4 -2
  205. package/src/Dialog/Dialog.test.tsx +24 -0
  206. package/src/Dialog/Dialog.tsx +3 -0
  207. package/src/Field/Field.doc.mjs +1 -0
  208. package/src/Field/FieldLabel.tsx +20 -4
  209. package/src/Field/InputClearButton.test.tsx +25 -0
  210. package/src/Field/InputClearButton.tsx +4 -1
  211. package/src/FormLayout/FormLayout.doc.mjs +13 -0
  212. package/src/FormLayout/FormLayout.test.tsx +181 -4
  213. package/src/FormLayout/FormLayout.tsx +33 -2
  214. package/src/FormLayout/FormLayoutContext.ts +22 -7
  215. package/src/FormLayout/index.ts +1 -1
  216. package/src/Item/Item.doc.mjs +1 -0
  217. package/src/Item/Item.test.tsx +50 -0
  218. package/src/Item/Item.tsx +40 -1
  219. package/src/Layer/useLayer.tsx +13 -2
  220. package/src/Layout/Layout.doc.mjs +18 -0
  221. package/src/Layout/container.stylex.ts +11 -3
  222. package/src/Layout/index.ts +4 -0
  223. package/src/Layout/overlayPaddingReset.test.tsx +222 -0
  224. package/src/Layout/padding.stylex.ts +192 -12
  225. package/src/Lightbox/Lightbox.tsx +2 -1
  226. package/src/Markdown/Markdown.doc.mjs +3 -0
  227. package/src/Markdown/Markdown.test.tsx +78 -0
  228. package/src/Markdown/Markdown.tsx +41 -1
  229. package/src/Markdown/parser.ts +60 -2
  230. package/src/MobileNav/MobileNav.tsx +61 -5
  231. package/src/MobileNav/MobileNavEntryAnimation.test.tsx +212 -0
  232. package/src/MobileNav/MobileNavScrollbarGutter.test.tsx +109 -0
  233. package/src/MultiSelector/MultiSelector.tsx +3 -1
  234. package/src/NumberInput/NumberInput.doc.mjs +24 -0
  235. package/src/NumberInput/NumberInput.test.tsx +138 -0
  236. package/src/NumberInput/NumberInput.tsx +43 -5
  237. package/src/Outline/Outline.doc.mjs +2 -0
  238. package/src/Outline/parseOutlineFromMarkdown.ts +10 -40
  239. package/src/RadioList/RadioList.tsx +9 -1
  240. package/src/Resizable/ResizeHandle.test.tsx +22 -0
  241. package/src/Resizable/ResizeHandle.tsx +9 -3
  242. package/src/Section/Section.doc.mjs +55 -0
  243. package/src/Section/Section.test.tsx +201 -4
  244. package/src/Section/Section.tsx +69 -0
  245. package/src/Selector/Selector.doc.mjs +16 -2
  246. package/src/Selector/Selector.test.tsx +524 -0
  247. package/src/Selector/Selector.tsx +161 -17
  248. package/src/Selector/SelectorOption.doc.mjs +7 -0
  249. package/src/Selector/SelectorOption.tsx +21 -0
  250. package/src/Selector/SelectorRowLayoutContext.ts +32 -0
  251. package/src/Selector/types.ts +4 -4
  252. package/src/Slider/Slider.tsx +10 -0
  253. package/src/Stack/Stack.doc.mjs +72 -0
  254. package/src/Stack/Stack.test.tsx +156 -0
  255. package/src/Stack/Stack.tsx +50 -8
  256. package/src/StatusDot/StatusDot.doc.mjs +13 -0
  257. package/src/StatusDot/StatusDot.test.tsx +115 -8
  258. package/src/StatusDot/StatusDot.tsx +74 -9
  259. package/src/Switch/Switch.tsx +6 -0
  260. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +20 -2
  261. package/src/TextArea/TextArea.tsx +3 -1
  262. package/src/TextInput/TextInput.tsx +3 -1
  263. package/src/TimeInput/TimeInput.tsx +3 -1
  264. package/src/hooks/scrollbarGutter.test.ts +167 -0
  265. package/src/hooks/scrollbarGutter.ts +121 -0
  266. package/src/hooks/useResolvedRequired.ts +42 -0
  267. package/src/hooks/useScrollLock.doc.mjs +4 -4
  268. package/src/hooks/useScrollLock.test.ts +73 -0
  269. package/src/hooks/useScrollLock.ts +14 -0
  270. package/src/theme/derivedVarRegistry.test.ts +14 -3
  271. package/src/theme/derivedVarRegistry.ts +4 -0
  272. package/src/theme/generateThemeRules.test.ts +79 -0
  273. package/src/theme/generateThemeRules.ts +49 -4
  274. package/src/utils/plainDate.test.ts +50 -0
  275. package/src/utils/plainDate.ts +12 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.4.4",
3
+ "version": "0.4.5-canary.1fdecb1",
4
4
  "displayName": "Astryx Core",
5
5
  "description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
6
6
  "author": "Meta Open Source",
@@ -656,6 +656,7 @@
656
656
  "react-dom": ">=19.0.0"
657
657
  },
658
658
  "devDependencies": {
659
+ "@astryxdesign/cli": "0.4.5-canary.1fdecb1",
659
660
  "@babel/cli": "^8.0.4",
660
661
  "@babel/core": "^7.29.7",
661
662
  "@babel/preset-react": "^8.0.1",
@@ -664,8 +665,7 @@
664
665
  "@testing-library/dom": "^10.0.0",
665
666
  "@testing-library/jest-dom": "^6.6.0",
666
667
  "@testing-library/react": "^16.3.2",
667
- "rimraf": "^6.0.1",
668
- "@astryxdesign/cli": "0.4.4"
668
+ "rimraf": "^6.0.1"
669
669
  },
670
670
  "dependencies": {
671
671
  "intl-messageformat": "^11.2.9"
@@ -13,6 +13,9 @@ export const docs = {
13
13
  'touch',
14
14
  'drag',
15
15
  'swipe',
16
+ 'snap point',
17
+ 'detent',
18
+ 'resize',
16
19
  'dismiss',
17
20
  'grab handle',
18
21
  'dialog',
@@ -54,7 +57,7 @@ export const docs = {
54
57
  targets: [{className: 'astryx-bottom-sheet', visualProps: []}],
55
58
  },
56
59
  description:
57
- "A mobile touch sheet that rises from the bottom edge, with animated entrance and exit, a grab handle, drag-to-resize snap points, and purpose-controlled dismissal. A standalone sheet owns a native <dialog>; inside BottomSheetSwitcher it renders a panel in the switcher's shared dialog. In both modes, ref and shared DOM props target the visual panel <div>.",
60
+ "A mobile touch sheet that rises from the bottom edge, with animated entrance and exit, a grab handle, optional drag-to-resize snap points, and purpose-controlled dismissal. A standalone sheet owns a native <dialog>; inside BottomSheetSwitcher it renders a panel in the switcher's shared dialog. In both modes, ref and shared DOM props target the visual panel <div>.",
58
61
  props: [
59
62
  {
60
63
  name: 'isOpen',
@@ -99,9 +102,15 @@ export const docs = {
99
102
  name: 'height',
100
103
  type: "'hug' | 'capped' | 'tall' | number | string",
101
104
  description:
102
- "How tall the sheet is. Named budgets: 'hug' fits its content up to 92% of the viewport, 'capped' is a scrolling mid-height panel (~62%), and 'tall' is a pinned near-full panel (~92%) for content that streams in. Or pass a number (px) / CSS length for a custom budget. The user can drag between snap points regardless. On shorter viewports the sheet fills the available height. Only a fully expanded 'tall' sheet provides mobile-keyboard accommodation: it stays put and scrolls each focused control above the keyboard. Hug, Capped, numeric and CSS-length heights never do, and a Tall sheet stops doing it the moment the user drags it to a shorter detent — resuming when they drag it back. Outside that state the sheet neither moves nor adds keyboard scroll space, and the browser's own focus reveal is left in place; on iOS that reveal can shift the whole page.",
105
+ "How tall the sheet is. Named budgets: 'hug' fits its content up to 92% of the viewport, 'capped' is a scrolling mid-height panel (~62%), and 'tall' is a pinned near-full panel (~92%) for content that streams in. Or pass a number (px) / CSS length for a custom budget. Give snapPoints to let the user drag between heights. On shorter viewports the sheet fills the available height. Only a fully expanded 'tall' sheet provides mobile-keyboard accommodation: it stays put and scrolls each focused control above the keyboard. Hug, Capped, numeric and CSS-length heights never do, and a Tall sheet stops doing it the moment the user drags it to a shorter detent — resuming when they drag it back. Outside that state the sheet neither moves nor adds keyboard scroll space, and the browser's own focus reveal is left in place; on iOS that reveal can shift the whole page.",
103
106
  default: "'capped'",
104
107
  },
108
+ {
109
+ name: 'snapPoints',
110
+ type: 'ReadonlyArray<number | string>',
111
+ description:
112
+ "Extra heights the sheet can rest at when dragged; its own height is always the tallest stop, and omitting this gives a sheet that only opens and closes. Each stop is the sheet's visible height: a number is a viewport fraction (0.5 is half the screen), '50%' the same in CSS, '320px' an absolute length. A stop of a quarter of the sheet or less is a peek — it slides away instead of reflowing, and thins the scrim.",
113
+ },
105
114
  {
106
115
  name: 'hasScrim',
107
116
  type: 'boolean',
@@ -156,6 +165,30 @@ export const docs = {
156
165
  label="Nearby places"
157
166
  height="tall">
158
167
  <PlaceList />
168
+ </BottomSheet>`,
169
+ },
170
+ {
171
+ label: 'Collapsible to half the screen',
172
+ code: `const [isOpen, setIsOpen] = useState(false);
173
+ <BottomSheet
174
+ isOpen={isOpen}
175
+ onOpenChange={setIsOpen}
176
+ label="Nearby places"
177
+ height="tall"
178
+ snapPoints={[0.5]}>
179
+ <PlaceList />
180
+ </BottomSheet>`,
181
+ },
182
+ {
183
+ label: 'A peek, a working height, and full',
184
+ code: `const [isOpen, setIsOpen] = useState(false);
185
+ <BottomSheet
186
+ isOpen={isOpen}
187
+ onOpenChange={setIsOpen}
188
+ label="Route"
189
+ height="tall"
190
+ snapPoints={['96px', '50%']}>
191
+ <RouteDetails />
159
192
  </BottomSheet>`,
160
193
  },
161
194
  {
@@ -236,7 +269,7 @@ export const docs = {
236
269
  /** @type {import('@astryxdesign/cli/authoring').ComponentTranslationDoc} */
237
270
  export const docsDense = {
238
271
  description:
239
- 'mobile touch sheet rising from the bottom edge (native <dialog>): grab handle, transform-based drag-to-resize snap points, scrolling area resizes to the snapped visible height on release (the peek detent keeps the full height and slides instead), Dialog-aligned dismissal purpose (info/form/required), purpose-gated swipe-to-dismiss, fully-expanded Tall visual-viewport mobile-keyboard handling, named height scale, modal (default) or non-modal (hasScrim={false}) presentation',
272
+ 'mobile touch sheet rising from the bottom edge (native <dialog>): grab handle, opt-in transform-based drag-to-resize snap points (snapPoints: viewport fraction, percent or px length), scrolling area resizes to the snapped visible height on release (a peek stop — a quarter of the sheet or less — keeps the full height and slides instead), Dialog-aligned dismissal purpose (info/form/required), purpose-gated swipe-to-dismiss, fully-expanded Tall visual-viewport mobile-keyboard handling, named height scale, modal (default) or non-modal (hasScrim={false}) presentation',
240
273
  usage: {
241
274
  description:
242
275
  'Mobile touch surface for filters, actions, forms, and detail views that should rise from the bottom of the viewport; use BottomSheetSwitcher for multi-step flows.',
@@ -15,6 +15,12 @@ import {createRef, useState} from 'react';
15
15
  import {BottomSheet} from './BottomSheet';
16
16
  import {BottomSheetSwitcher} from './BottomSheetSwitcher';
17
17
 
18
+ // A sheet has no stops unless its host asks for them, so the tests that
19
+ // exercise detents pass their own. These three — a 14% peek, a half-height
20
+ // stop, and a 92% stop the Tall budget already covers — are the geometry the
21
+ // height assertions below are written against.
22
+ const SNAP_POINTS: ReadonlyArray<number> = [0.14, 0.5, 0.92];
23
+
18
24
  // jsdom doesn't implement <dialog> open/close or pointer capture; stub them.
19
25
  beforeEach(() => {
20
26
  HTMLDialogElement.prototype.showModal = vi.fn(function (
@@ -178,13 +184,20 @@ function finishSheetExit() {
178
184
  fireEvent.transitionEnd(getSheet(), {propertyName: 'transform'});
179
185
  }
180
186
 
181
- function ExitHarness({hasScrim}: {hasScrim?: boolean}) {
187
+ function ExitHarness({
188
+ hasScrim,
189
+ snapPoints,
190
+ }: {
191
+ hasScrim?: boolean;
192
+ snapPoints?: ReadonlyArray<number>;
193
+ }) {
182
194
  const [isOpen, setIsOpen] = useState(true);
183
195
  return (
184
196
  <BottomSheet
185
197
  isOpen={isOpen}
186
198
  onOpenChange={setIsOpen}
187
199
  label="Filters"
200
+ snapPoints={snapPoints}
188
201
  hasScrim={hasScrim}>
189
202
  <button type="button" onClick={() => setIsOpen(false)}>
190
203
  Close sheet
@@ -616,6 +629,7 @@ describe('BottomSheet', () => {
616
629
  isOpen
617
630
  onOpenChange={() => {}}
618
631
  label="Release notes"
632
+ snapPoints={SNAP_POINTS}
619
633
  height="tall">
620
634
  Content
621
635
  </BottomSheet>,
@@ -667,6 +681,7 @@ describe('BottomSheet', () => {
667
681
  isOpen
668
682
  onOpenChange={() => {}}
669
683
  label="Release notes"
684
+ snapPoints={SNAP_POINTS}
670
685
  height="tall">
671
686
  Content
672
687
  </BottomSheet>,
@@ -720,6 +735,7 @@ describe('BottomSheet', () => {
720
735
  isOpen
721
736
  onOpenChange={() => {}}
722
737
  label="Release notes"
738
+ snapPoints={SNAP_POINTS}
723
739
  height="tall">
724
740
  Content
725
741
  </BottomSheet>,
@@ -797,6 +813,7 @@ describe('BottomSheet', () => {
797
813
  isOpen
798
814
  onOpenChange={() => {}}
799
815
  label="Release notes"
816
+ snapPoints={SNAP_POINTS}
800
817
  height="tall"
801
818
  style={{transition: 'none'}}>
802
819
  Content
@@ -830,6 +847,7 @@ describe('BottomSheet', () => {
830
847
  isOpen
831
848
  onOpenChange={() => {}}
832
849
  label="Release notes"
850
+ snapPoints={SNAP_POINTS}
833
851
  height="tall">
834
852
  Content
835
853
  </BottomSheet>,
@@ -890,7 +908,7 @@ describe('BottomSheet', () => {
890
908
  const observers = mockResizeObserverInstances();
891
909
  mockVisualViewport(800);
892
910
  mockWindowHeight(800);
893
- render(<ExitHarness />);
911
+ render(<ExitHarness snapPoints={SNAP_POINTS} />);
894
912
  const sheet = getSheet();
895
913
  const sheetObserver = observers.find(instance =>
896
914
  instance.observed.has(sheet),
@@ -936,6 +954,7 @@ describe('BottomSheet', () => {
936
954
  isOpen
937
955
  onOpenChange={() => {}}
938
956
  label="Release notes"
957
+ snapPoints={SNAP_POINTS}
939
958
  height="tall">
940
959
  Content
941
960
  </BottomSheet>,
@@ -1000,6 +1019,7 @@ describe('BottomSheet', () => {
1000
1019
  isOpen
1001
1020
  onOpenChange={() => {}}
1002
1021
  label="Release notes"
1022
+ snapPoints={SNAP_POINTS}
1003
1023
  height="tall">
1004
1024
  Content
1005
1025
  </BottomSheet>,
@@ -1039,6 +1059,136 @@ describe('BottomSheet', () => {
1039
1059
  });
1040
1060
  });
1041
1061
 
1062
+ describe('snapPoints', () => {
1063
+ // A Tall sheet in an 800px window: a 784px border box, 48px of which is
1064
+ // the reserve below the fold, so 736px of it is visible.
1065
+ function renderTallSheet(
1066
+ snapPoints?: ReadonlyArray<number | string>,
1067
+ onOpenChange: (isOpen: boolean) => void = () => {},
1068
+ ) {
1069
+ const observers = mockResizeObserverInstances();
1070
+ mockVisualViewport(800);
1071
+ mockWindowHeight(800);
1072
+ const view = render(
1073
+ <BottomSheet
1074
+ isOpen
1075
+ onOpenChange={onOpenChange}
1076
+ label="Release notes"
1077
+ snapPoints={snapPoints}
1078
+ height="tall">
1079
+ Content
1080
+ </BottomSheet>,
1081
+ );
1082
+ const sheet = getSheet();
1083
+ const sheetObserver = observers.find(instance =>
1084
+ instance.observed.has(sheet),
1085
+ );
1086
+ act(() => {
1087
+ sheetObserver?.callback(
1088
+ [resizeEntry(784, 736)],
1089
+ sheetObserver as unknown as ResizeObserver,
1090
+ );
1091
+ });
1092
+ return {sheet, view};
1093
+ }
1094
+
1095
+ function dragHandleTo(y: number) {
1096
+ fireTimedPointer(getHandle(), 'pointerdown', {time: 0, y: 0});
1097
+ fireTimedPointer(getHandle(), 'pointermove', {time: 1000, y});
1098
+ fireTimedPointer(getHandle(), 'pointerup', {time: 2000, y});
1099
+ fireEvent.transitionEnd(getSheet(), {propertyName: 'transform'});
1100
+ }
1101
+
1102
+ it('has no stops of its own, so a released drag springs back', () => {
1103
+ const onOpenChange = vi.fn();
1104
+ const {sheet} = renderTallSheet(undefined, onOpenChange);
1105
+
1106
+ // 200px down is well short of the dismiss threshold, and there is no
1107
+ // stop to catch it, so the sheet returns to fully open.
1108
+ dragHandleTo(200);
1109
+
1110
+ expect(sheet.style.transform).toBe('');
1111
+ expect(sheet.style.height).toBe('');
1112
+ expect(onOpenChange).not.toHaveBeenCalled();
1113
+ });
1114
+
1115
+ it('rests at a stop given as a fraction of the viewport', () => {
1116
+ const {sheet} = renderTallSheet([0.5]);
1117
+
1118
+ // Half of the 800px window is 400px of visible sheet: 736 - 400 = 336px
1119
+ // of travel, taken as layout height once the snap lands.
1120
+ dragHandleTo(336);
1121
+
1122
+ expect(sheet.style.height).toBe('448px');
1123
+ expect(sheet.style.transform).toBe('');
1124
+ });
1125
+
1126
+ it('reads a percentage as the same stop as the fraction', () => {
1127
+ const {sheet} = renderTallSheet(['50%']);
1128
+ dragHandleTo(336);
1129
+ expect(sheet.style.height).toBe('448px');
1130
+ });
1131
+
1132
+ it('rests at a stop given as an absolute px length', () => {
1133
+ const {sheet} = renderTallSheet(['320px']);
1134
+
1135
+ // A 320px stop sits 736 - 320 = 416px down, whatever the window does.
1136
+ dragHandleTo(416);
1137
+
1138
+ expect(sheet.style.height).toBe('368px');
1139
+ expect(sheet.style.transform).toBe('');
1140
+ });
1141
+
1142
+ it('re-anchors to the same stop when the points change under a resting sheet', () => {
1143
+ const {sheet, view} = renderTallSheet([0.5]);
1144
+ dragHandleTo(336);
1145
+ expect(sheet.style.height).toBe('448px');
1146
+
1147
+ // Without an inline height the sheet renders its natural 92dvh budget.
1148
+ vi.spyOn(sheet, 'getBoundingClientRect').mockImplementation(() =>
1149
+ rect({
1150
+ top: 0,
1151
+ bottom: sheet.style.height
1152
+ ? Number.parseFloat(sheet.style.height)
1153
+ : 784,
1154
+ }),
1155
+ );
1156
+
1157
+ // The host moves its one stop from half the window to a quarter of it.
1158
+ // The sheet is resting on that stop, so it follows — no gesture, and
1159
+ // nothing to animate.
1160
+ view.rerender(
1161
+ <BottomSheet
1162
+ isOpen
1163
+ onOpenChange={() => {}}
1164
+ label="Release notes"
1165
+ snapPoints={[0.25]}
1166
+ height="tall">
1167
+ Content
1168
+ </BottomSheet>,
1169
+ );
1170
+
1171
+ // 200px of visible sheet is 736 - 200 = 536px of travel.
1172
+ expect(sheet.style.height).toBe('248px');
1173
+ expect(sheet.style.transform).toBe('');
1174
+ });
1175
+
1176
+ it('ignores a stop it cannot resolve, and warns which one', () => {
1177
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
1178
+ // 200 is the px mistake: a bare number is a fraction, never a length.
1179
+ const {sheet} = renderTallSheet([0.5, 200]);
1180
+
1181
+ expect(
1182
+ warn.mock.calls.some(args => String(args[0]).includes('200')),
1183
+ ).toBe(true);
1184
+
1185
+ // The stop it could read still works.
1186
+ dragHandleTo(336);
1187
+ expect(sheet.style.height).toBe('448px');
1188
+ warn.mockRestore();
1189
+ });
1190
+ });
1191
+
1042
1192
  describe('mobile keyboard', () => {
1043
1193
  it('claims a transition between fields and delivers it with preventScroll', () => {
1044
1194
  mockIOSWebKit();
@@ -1293,6 +1443,7 @@ describe('BottomSheet', () => {
1293
1443
  isOpen
1294
1444
  onOpenChange={() => {}}
1295
1445
  label="Add a comment"
1446
+ snapPoints={SNAP_POINTS}
1296
1447
  height="tall">
1297
1448
  <input aria-label="Comment" />
1298
1449
  </BottomSheet>,
@@ -2111,4 +2262,28 @@ describe('BottomSheet', () => {
2111
2262
  expect(screen.getByRole('dialog')).toBeInTheDocument();
2112
2263
  });
2113
2264
  });
2265
+
2266
+ describe('container padding isolation', () => {
2267
+ it('resets container padding custom properties to 0px on the sheet panel element', () => {
2268
+ render(
2269
+ <BottomSheet isOpen onOpenChange={() => {}} label="Filters">
2270
+ Content
2271
+ </BottomSheet>,
2272
+ );
2273
+ const sheet = getSheet();
2274
+ const computed = window.getComputedStyle(sheet);
2275
+ expect(
2276
+ computed.getPropertyValue('--container-padding-inline-start'),
2277
+ ).toBe('0px');
2278
+ expect(computed.getPropertyValue('--container-padding-inline-end')).toBe(
2279
+ '0px',
2280
+ );
2281
+ expect(computed.getPropertyValue('--container-padding-block-start')).toBe(
2282
+ '0px',
2283
+ );
2284
+ expect(computed.getPropertyValue('--container-padding-block-end')).toBe(
2285
+ '0px',
2286
+ );
2287
+ });
2288
+ });
2114
2289
  });
@@ -49,8 +49,8 @@ import {
49
49
  type BottomSheetSwitcherPhase,
50
50
  } from './BottomSheetSwitcherContext';
51
51
 
52
- export type {BottomSheetHeight} from './BottomSheetPanel';
53
- import type {BottomSheetHeight} from './BottomSheetPanel';
52
+ export type {BottomSheetHeight, BottomSheetSnapPoint} from './BottomSheetPanel';
53
+ import type {BottomSheetHeight, BottomSheetSnapPoint} from './BottomSheetPanel';
54
54
 
55
55
  const styles = stylex.create({
56
56
  dialog: {
@@ -122,6 +122,16 @@ interface BottomSheetSharedProps extends BaseProps<HTMLDivElement> {
122
122
  /** Height budget or custom CSS length. Only fully expanded Tall is keyboard-aware. @default 'capped' */
123
123
  height?: BottomSheetHeight | number | string;
124
124
 
125
+ /**
126
+ * Extra heights the sheet can rest at when dragged; its own height is always
127
+ * the tallest stop, and omitting this gives a sheet that only opens and
128
+ * closes. Each stop is the sheet's visible height: a number is a viewport
129
+ * fraction (`0.5` is half the screen), `'50%'` the same in CSS, `'320px'` an
130
+ * absolute length. A stop of a quarter of the sheet or less is a peek — it
131
+ * slides away instead of reflowing, and thins the scrim.
132
+ */
133
+ snapPoints?: ReadonlyArray<BottomSheetSnapPoint>;
134
+
125
135
  /**
126
136
  * Configures implicit dismissal behavior, matching Dialog.
127
137
  * - required: Blocks swipe, scrim click, and Escape
@@ -193,6 +203,7 @@ function StandaloneBottomSheet({
193
203
  label,
194
204
  children,
195
205
  height = 'capped',
206
+ snapPoints,
196
207
  hasScrim = true,
197
208
  purpose = 'info',
198
209
  xstyle,
@@ -333,6 +344,7 @@ function StandaloneBottomSheet({
333
344
  ref={ref}
334
345
  state={panelState}
335
346
  height={height}
347
+ snapPoints={snapPoints}
336
348
  isSwipeDismissAllowed={purpose === 'info'}
337
349
  isPageScrollLocked={shouldPresent && hasScrim}
338
350
  xstyle={xstyle}
@@ -358,6 +370,7 @@ function SwitcherBottomSheetItem({
358
370
  label,
359
371
  children,
360
372
  height = 'capped',
373
+ snapPoints,
361
374
  purpose = 'info',
362
375
  xstyle,
363
376
  ...props
@@ -488,6 +501,7 @@ function SwitcherBottomSheetItem({
488
501
  ref={ref}
489
502
  state={panelState}
490
503
  height={height}
504
+ snapPoints={snapPoints}
491
505
  isSwipeDismissAllowed={purpose === 'info'}
492
506
  isPageScrollLocked={hasScrim}
493
507
  xstyle={xstyle}
@@ -237,4 +237,45 @@ describe('BottomSheetPanel', () => {
237
237
  expect(panelRef).toHaveBeenLastCalledWith(null);
238
238
  expect(panelRef).toHaveBeenCalledTimes(2);
239
239
  });
240
+
241
+ it('floats the handle bar over content that starts at the sheet top edge', () => {
242
+ renderPanel({kind: 'open', entering: false});
243
+
244
+ const bar = getPanel().querySelector<HTMLElement>(
245
+ 'div[aria-hidden="true"]',
246
+ );
247
+ if (bar == null) {
248
+ throw new Error('handle bar not found');
249
+ }
250
+ const body = bar.nextElementSibling as HTMLElement;
251
+
252
+ // Out of flow: the bar costs the content no layout space. That is the
253
+ // point of it -- the content sits closer to the sheet's top edge, and
254
+ // scrolled content passes beneath the pill rather than stopping at a
255
+ // hard edge.
256
+ expect(getComputedStyle(bar).position).toBe('absolute');
257
+
258
+ // So the body must not reserve room for it. A top padding here would
259
+ // push the content back down and undo the change.
260
+ expect(getComputedStyle(body).paddingTop).toBe('');
261
+ });
262
+
263
+ // The pill is only legible over the content riding up beneath it because
264
+ // the bar carries a backdrop; without it, text runs straight through the
265
+ // pill. jsdom's StyleX runtime does not inject the rule, so assert on the
266
+ // style definition itself (same approach as AspectRatio's reset.css tests).
267
+ it('backs the floating handle bar with a surface gradient', async () => {
268
+ const fs = await import('fs');
269
+ const path = await import('path');
270
+ const source = fs.readFileSync(
271
+ path.resolve(__dirname, './BottomSheetPanel.tsx'),
272
+ 'utf-8',
273
+ );
274
+
275
+ const handleBar = source.match(/handleBar: \{([\s\S]*?)\n {2}\},/);
276
+ expect(handleBar).not.toBeNull();
277
+ expect(handleBar![1]).toContain("position: 'absolute'");
278
+ expect(handleBar![1]).toContain('linear-gradient(to bottom');
279
+ expect(handleBar![1]).toContain("colorVars['--color-background-surface']");
280
+ });
240
281
  });
@@ -16,6 +16,7 @@
16
16
  * SYNC: When modified, update these files to stay in sync:
17
17
  * - /packages/core/src/BottomSheet/BottomSheet.tsx
18
18
  * - /packages/core/src/BottomSheet/BottomSheetPanel.test.tsx
19
+ * - /packages/core/src/BottomSheet/snapOffsets.ts
19
20
  * - /packages/core/src/BottomSheet/useMobileKeyboard.ts
20
21
  * - /packages/core/src/BottomSheet/useSheetGestures.ts
21
22
  */
@@ -25,11 +26,13 @@ import {
25
26
  useEffect,
26
27
  useImperativeHandle,
27
28
  useLayoutEffect,
29
+ useMemo,
28
30
  useRef,
29
31
  type ReactNode,
30
32
  } from 'react';
31
33
  import * as stylex from '@stylexjs/stylex';
32
34
  import type {BaseProps} from '../BaseProps';
35
+ import {useDevWarning} from '../hooks';
33
36
  import {
34
37
  colorVars,
35
38
  durationVars,
@@ -40,11 +43,15 @@ import {
40
43
  spacingVars,
41
44
  } from '../theme/tokens.stylex';
42
45
  import {mergeProps, themeProps} from '../utils';
46
+ import {overlayPaddingReset} from '../Layout/padding.stylex';
47
+ import {
48
+ isValidSnapPoint,
49
+ resolveSnapPoints,
50
+ type BottomSheetSnapPoint,
51
+ } from './snapOffsets';
43
52
  import {useMobileKeyboard} from './useMobileKeyboard';
44
53
  import {useSheetGestures} from './useSheetGestures';
45
54
 
46
- const SNAP_FRACTIONS = [0.14, 0.5, 0.92];
47
-
48
55
  const HEIGHT_BUDGETS = {
49
56
  hug: '92dvh',
50
57
  capped: '62dvh',
@@ -52,29 +59,53 @@ const HEIGHT_BUDGETS = {
52
59
  } as const;
53
60
 
54
61
  export type BottomSheetHeight = keyof typeof HEIGHT_BUDGETS;
62
+ export type {BottomSheetSnapPoint};
55
63
 
56
64
  // SYNC: must match OVERSCROLL_MAX in useSheetGestures.ts.
57
65
  const OVERSCROLL_PADDING = 48;
58
66
  const MOBILE_KEYBOARD_BOTTOM_CLEARANCE = 48;
59
67
  const TRANSITION_BACKSTOP_BUFFER_MS = 50;
68
+ // The floating handle bar's height. The bar is out of flow, so this is not
69
+ // layout space the content pays for -- the pill (4px, centered) lands 10-14px
70
+ // from the sheet's top edge, inside the space a content wrapper's own top
71
+ // padding already provides.
72
+ const HANDLE_BAR_HEIGHT = spacingVars['--spacing-6'];
73
+
74
+ // Measure the same viewport the height budgets are written against. Those are
75
+ // `dvh`, which the virtual keyboard does not shrink, so reading
76
+ // `visualViewport` here would make the two disagree by exactly the keyboard's
77
+ // height: every detent would move while the sheet it measures did not. A
78
+ // keyboard is `useMobileKeyboard`'s business — it holds the sheet still and
79
+ // scrolls the body — and it does not redefine the sheet's detents.
80
+ function layoutViewportHeight(): number {
81
+ return typeof window === 'undefined' ? 0 : window.innerHeight;
82
+ }
60
83
 
61
- function defaultSnapHeights(): number[] {
62
- if (typeof window === 'undefined') {
63
- return [];
84
+ /**
85
+ * A stable identity for a set of snap points. Type-tagged, so the fraction
86
+ * `0.5` and the (invalid) string `'0.5'` cannot collide on one key.
87
+ *
88
+ * Recomputed every render — the panel re-renders on every frame of a drag — so
89
+ * it stays a single pass over a handful of values, and everything derived from
90
+ * the points hangs off it instead of being rebuilt per frame.
91
+ */
92
+ function snapPointsKeyFor(
93
+ points: ReadonlyArray<BottomSheetSnapPoint> | undefined,
94
+ ): string {
95
+ let key = '';
96
+ for (const point of points ?? []) {
97
+ key += `${typeof point}:${point}|`;
64
98
  }
65
- // Measure the same viewport the height budgets are written against. Those
66
- // are `dvh`, which the virtual keyboard does not shrink, so reading
67
- // `visualViewport` here made the two disagree by exactly the keyboard's
68
- // height: every detent moved while the sheet it was measuring did not. A
69
- // keyboard is `useMobileKeyboard`'s business — it holds the sheet still and
70
- // scrolls the body — and it does not redefine the sheet's detents.
71
- return SNAP_FRACTIONS.map(fraction => fraction * window.innerHeight);
99
+ return key;
72
100
  }
73
101
 
74
102
  const styles = stylex.create({
75
103
  sheet: {
76
104
  pointerEvents: 'auto',
77
105
  boxSizing: 'border-box',
106
+ // Containing block for the floating handle bar, which is lifted out of
107
+ // flow so the scrolling body reaches the sheet's top edge.
108
+ position: 'relative',
78
109
  display: 'flex',
79
110
  flexDirection: 'column',
80
111
  minHeight: 0,
@@ -111,11 +142,23 @@ const styles = stylex.create({
111
142
  pointerEvents: 'none',
112
143
  },
113
144
  handleBar: {
114
- flexShrink: 0,
145
+ // Floats over the body rather than taking a row in the flex column. The
146
+ // content starts at the sheet's top edge and rides up under the pill --
147
+ // both moving it closer to the top and letting scrolled content pass
148
+ // beneath instead of stopping at a hard edge.
149
+ position: 'absolute',
150
+ insetBlockStart: 0,
151
+ insetInlineStart: 0,
152
+ insetInlineEnd: 0,
153
+ zIndex: 1,
115
154
  display: 'flex',
116
155
  alignItems: 'center',
117
156
  justifyContent: 'center',
118
- height: spacingVars['--spacing-12'],
157
+ height: HANDLE_BAR_HEIGHT,
158
+ // Keeps the pill legible over whatever sits or scrolls under it: opaque
159
+ // surface behind the pill itself, fading out across the lower half so the
160
+ // content emerging below has no visible cut line.
161
+ backgroundImage: `linear-gradient(to bottom, ${colorVars['--color-background-surface']} 60%, transparent)`,
119
162
  touchAction: 'none',
120
163
  cursor: 'grab',
121
164
  },
@@ -132,6 +175,10 @@ const styles = stylex.create({
132
175
  overflowY: 'auto',
133
176
  overscrollBehavior: 'none',
134
177
  touchAction: 'pan-y',
178
+ // No reserve for the handle bar: it floats, so the content starts at the
179
+ // sheet's top edge and rides up under the pill. The pill is 4px centered
180
+ // in a 24px band, so it occupies only 10-14px from the edge -- inside the
181
+ // space a content wrapper's own top padding already provides.
135
182
  paddingBlockEnd: 0,
136
183
  },
137
184
  tallKeyboardBody: {
@@ -171,6 +218,7 @@ interface BottomSheetPanelProps extends BaseProps<HTMLDivElement> {
171
218
  state: BottomSheetPanelState;
172
219
  height: BottomSheetHeight | number | string;
173
220
  children: ReactNode;
221
+ snapPoints?: ReadonlyArray<BottomSheetSnapPoint>;
174
222
  isSwipeDismissAllowed?: boolean;
175
223
  /** Whether the host has locked page scrolling (a modal, scrim-backed sheet). */
176
224
  isPageScrollLocked?: boolean;
@@ -296,6 +344,7 @@ export function BottomSheetPanel({
296
344
  state,
297
345
  height,
298
346
  children,
347
+ snapPoints,
299
348
  className,
300
349
  style,
301
350
  tabIndex,
@@ -340,6 +389,36 @@ export function BottomSheetPanel({
340
389
  const isFading = isRetained && state.motion === 'fading';
341
390
  const alignmentOffset = isRetained ? state.alignmentOffset : 0;
342
391
 
392
+ // Everything derived from the snap points keys off their identity, not the
393
+ // array's: the resolver's identity is the hook's signal that the stops
394
+ // changed, so it must not churn on a re-render the drag caused. The memo
395
+ // reads the points through a ref, so the resolver resolves them at call time
396
+ // — against whatever the viewport is then, not what it was at memo time.
397
+ const snapPointsKey = snapPointsKeyFor(snapPoints);
398
+ const snapPointsRef = useRef(snapPoints);
399
+ snapPointsRef.current = snapPoints;
400
+ const {snapHeights, ignoredSnapPointsMessage} = useMemo(() => {
401
+ if (snapPointsKey === '') {
402
+ return {snapHeights: undefined, ignoredSnapPointsMessage: ''};
403
+ }
404
+ const ignored = (snapPointsRef.current ?? []).filter(
405
+ point => !isValidSnapPoint(point),
406
+ );
407
+ return {
408
+ snapHeights: () =>
409
+ resolveSnapPoints(snapPointsRef.current ?? [], layoutViewportHeight()),
410
+ ignoredSnapPointsMessage:
411
+ ignored.length === 0
412
+ ? ''
413
+ : `snapPoints ignored ${JSON.stringify(ignored)}. A snap point is a viewport fraction above 0 and up to 1 (0.5 is half the screen), a px length ('320px'), or a percentage ('50%').`,
414
+ };
415
+ }, [snapPointsKey]);
416
+ useDevWarning(
417
+ 'BottomSheet',
418
+ ignoredSnapPointsMessage,
419
+ ignoredSnapPointsMessage !== '',
420
+ );
421
+
343
422
  const {
344
423
  contentProps,
345
424
  handleProps,
@@ -359,7 +438,7 @@ export function BottomSheetPanel({
359
438
  canDismiss: isSwipeDismissAllowed,
360
439
  offscreenBlockEndInset: OVERSCROLL_PADDING,
361
440
  onDismiss,
362
- snapHeights: defaultSnapHeights,
441
+ snapHeights,
363
442
  onScrimOpacity,
364
443
  });
365
444
 
@@ -502,6 +581,7 @@ export function BottomSheetPanel({
502
581
  themeProps('bottom-sheet'),
503
582
  stylex.props(
504
583
  styles.sheet,
584
+ overlayPaddingReset.reset,
505
585
  height === 'hug' ? styles.hugHeight : styles.budget,
506
586
  isClosing && styles.sheetClosing,
507
587
  isFading && styles.sheetFading,