@astryxdesign/core 0.4.3 → 0.4.4-canary.0e8af82

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 (254) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +79 -45
  3. package/dist/Banner/Banner.d.ts +3 -0
  4. package/dist/Banner/Banner.d.ts.map +1 -1
  5. package/dist/Banner/Banner.js +13 -3
  6. package/dist/BottomSheet/BottomSheet.d.ts +75 -0
  7. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -0
  8. package/dist/BottomSheet/BottomSheet.js +396 -0
  9. package/dist/BottomSheet/BottomSheetPanel.d.ts +63 -0
  10. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -0
  11. package/dist/BottomSheet/BottomSheetPanel.js +441 -0
  12. package/dist/BottomSheet/BottomSheetSwitcher.d.ts +61 -0
  13. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -0
  14. package/dist/BottomSheet/BottomSheetSwitcher.js +410 -0
  15. package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts +21 -0
  16. package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts.map +1 -0
  17. package/dist/BottomSheet/BottomSheetSwitcherContext.js +13 -0
  18. package/dist/BottomSheet/index.d.ts +11 -0
  19. package/dist/BottomSheet/index.d.ts.map +1 -0
  20. package/dist/BottomSheet/index.js +12 -0
  21. package/dist/BottomSheet/snapOffsets.d.ts +109 -0
  22. package/dist/BottomSheet/snapOffsets.d.ts.map +1 -0
  23. package/dist/BottomSheet/snapOffsets.js +206 -0
  24. package/dist/BottomSheet/useMobileKeyboard.d.ts +35 -0
  25. package/dist/BottomSheet/useMobileKeyboard.d.ts.map +1 -0
  26. package/dist/BottomSheet/useMobileKeyboard.js +373 -0
  27. package/dist/BottomSheet/useSheetGestures.d.ts +185 -0
  28. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -0
  29. package/dist/BottomSheet/useSheetGestures.js +1038 -0
  30. package/dist/Calendar/Calendar.d.ts +17 -0
  31. package/dist/Calendar/Calendar.d.ts.map +1 -1
  32. package/dist/Calendar/Calendar.js +25 -1
  33. package/dist/Calendar/hooks/useCalendarConstraints.d.ts +22 -1
  34. package/dist/Calendar/hooks/useCalendarConstraints.d.ts.map +1 -1
  35. package/dist/Calendar/hooks/useCalendarConstraints.js +26 -4
  36. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  37. package/dist/Chat/ChatComposerInput.js +3 -4
  38. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  39. package/dist/CheckboxInput/CheckboxInput.js +9 -0
  40. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  41. package/dist/ComplexSelector/ComplexSelector.js +18 -7
  42. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  43. package/dist/ContextMenu/ContextMenu.js +4 -2
  44. package/dist/DateInput/DateInput.d.ts.map +1 -1
  45. package/dist/DateInput/DateInput.js +15 -2
  46. package/dist/DateRangeInput/DateRangeInput.d.ts +23 -1
  47. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  48. package/dist/DateRangeInput/DateRangeInput.js +38 -3
  49. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  50. package/dist/DateTimeInput/DateTimeInput.js +38 -3
  51. package/dist/Dialog/Dialog.d.ts.map +1 -1
  52. package/dist/Dialog/Dialog.js +2 -1
  53. package/dist/Field/FieldLabel.d.ts +2 -1
  54. package/dist/Field/FieldLabel.d.ts.map +1 -1
  55. package/dist/Field/FieldLabel.js +21 -3
  56. package/dist/Field/InputClearButton.d.ts +2 -1
  57. package/dist/Field/InputClearButton.d.ts.map +1 -1
  58. package/dist/Field/InputClearButton.js +6 -1
  59. package/dist/FormLayout/FormLayout.d.ts +24 -2
  60. package/dist/FormLayout/FormLayout.d.ts.map +1 -1
  61. package/dist/FormLayout/FormLayout.js +7 -2
  62. package/dist/FormLayout/FormLayoutContext.d.ts +15 -2
  63. package/dist/FormLayout/FormLayoutContext.d.ts.map +1 -1
  64. package/dist/FormLayout/FormLayoutContext.js +19 -4
  65. package/dist/FormLayout/index.d.ts +1 -1
  66. package/dist/FormLayout/index.d.ts.map +1 -1
  67. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  68. package/dist/Lightbox/Lightbox.js +9 -3
  69. package/dist/MobileNav/MobileNav.d.ts +12 -0
  70. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  71. package/dist/MobileNav/MobileNav.js +102 -30
  72. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  73. package/dist/MultiSelector/MultiSelector.js +27 -8
  74. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  75. package/dist/NumberInput/NumberInput.js +15 -2
  76. package/dist/PowerSearch/PowerSearchEditPopover.js +1 -1
  77. package/dist/RadioList/RadioList.d.ts.map +1 -1
  78. package/dist/RadioList/RadioList.js +12 -1
  79. package/dist/Selector/Selector.d.ts.map +1 -1
  80. package/dist/Selector/Selector.js +20 -4
  81. package/dist/StatusDot/StatusDot.d.ts +43 -3
  82. package/dist/StatusDot/StatusDot.d.ts.map +1 -1
  83. package/dist/StatusDot/StatusDot.js +42 -5
  84. package/dist/Switch/Switch.d.ts.map +1 -1
  85. package/dist/Switch/Switch.js +14 -1
  86. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  87. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +7 -2
  88. package/dist/TextArea/TextArea.d.ts.map +1 -1
  89. package/dist/TextArea/TextArea.js +6 -1
  90. package/dist/TextInput/TextInput.d.ts.map +1 -1
  91. package/dist/TextInput/TextInput.js +6 -1
  92. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  93. package/dist/TimeInput/TimeInput.js +14 -2
  94. package/dist/Tokenizer/Tokenizer.js +11 -5
  95. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  96. package/dist/Tooltip/useTooltip.js +4 -1
  97. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  98. package/dist/Typeahead/BaseTypeahead.js +5 -3
  99. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  100. package/dist/Typeahead/Typeahead.js +10 -1
  101. package/dist/astryx.css +21 -0
  102. package/dist/hooks/index.d.ts +8 -1
  103. package/dist/hooks/index.d.ts.map +1 -1
  104. package/dist/hooks/index.js +8 -1
  105. package/dist/hooks/useFocusTrap.d.ts +0 -11
  106. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  107. package/dist/hooks/useFocusTrap.js +1 -11
  108. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
  109. package/dist/hooks/useInputStatusIcon.js +1 -0
  110. package/dist/hooks/useResolvedRequired.d.ts +19 -0
  111. package/dist/hooks/useResolvedRequired.d.ts.map +1 -0
  112. package/dist/hooks/useResolvedRequired.js +40 -0
  113. package/dist/index.d.ts +1 -0
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +1 -0
  116. package/dist/theme/defineTheme.d.ts +22 -4
  117. package/dist/theme/defineTheme.d.ts.map +1 -1
  118. package/dist/theme/expandColorScale.d.ts +17 -2
  119. package/dist/theme/expandColorScale.d.ts.map +1 -1
  120. package/dist/theme/expandColorScale.js +63 -33
  121. package/dist/utils/ime.d.ts +5 -0
  122. package/dist/utils/ime.d.ts.map +1 -0
  123. package/dist/utils/ime.js +63 -0
  124. package/dist/utils/index.d.ts +1 -0
  125. package/dist/utils/index.d.ts.map +1 -1
  126. package/dist/utils/index.js +2 -1
  127. package/dist/utils/plainDate.d.ts +6 -0
  128. package/dist/utils/plainDate.d.ts.map +1 -1
  129. package/dist/utils/plainDate.js +12 -0
  130. package/locales/af-ZA.json +958 -0
  131. package/locales/ar-SA.json +998 -0
  132. package/locales/ca-ES.json +974 -0
  133. package/locales/cs-CZ.json +990 -0
  134. package/locales/da-DK.json +966 -0
  135. package/locales/de-DE.json +966 -0
  136. package/locales/el-GR.json +990 -0
  137. package/locales/en.json +52 -0
  138. package/locales/es-ES.json +978 -0
  139. package/locales/fi-FI.json +990 -0
  140. package/locales/fr-FR.json +928 -0
  141. package/locales/he-IL.json +990 -0
  142. package/locales/hu-HU.json +990 -0
  143. package/locales/it-IT.json +978 -0
  144. package/locales/ja-JP.json +998 -0
  145. package/locales/ko-KR.json +990 -0
  146. package/locales/nl-NL.json +954 -0
  147. package/locales/no-NO.json +978 -0
  148. package/locales/pl-PL.json +982 -0
  149. package/locales/pseudo.json +39 -0
  150. package/locales/pt-BR.json +982 -0
  151. package/locales/pt-PT.json +982 -0
  152. package/locales/ro-RO.json +982 -0
  153. package/locales/ru-RU.json +990 -0
  154. package/locales/sr-SP.json +990 -0
  155. package/locales/sv-SE.json +986 -0
  156. package/locales/tr-TR.json +990 -0
  157. package/locales/uk-UA.json +990 -0
  158. package/locales/vi-VN.json +986 -0
  159. package/locales/zh-CN.json +998 -0
  160. package/locales/zh-TW.json +998 -0
  161. package/package.json +9 -8
  162. package/src/Banner/Banner.doc.mjs +3 -3
  163. package/src/Banner/Banner.test.tsx +40 -0
  164. package/src/Banner/Banner.tsx +27 -2
  165. package/src/BottomSheet/BottomSheet.doc.mjs +299 -0
  166. package/src/BottomSheet/BottomSheet.test.tsx +2265 -0
  167. package/src/BottomSheet/BottomSheet.tsx +559 -0
  168. package/src/BottomSheet/BottomSheetPanel.test.tsx +240 -0
  169. package/src/BottomSheet/BottomSheetPanel.tsx +601 -0
  170. package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +219 -0
  171. package/src/BottomSheet/BottomSheetSwitcher.test.tsx +850 -0
  172. package/src/BottomSheet/BottomSheetSwitcher.tsx +628 -0
  173. package/src/BottomSheet/BottomSheetSwitcherContext.ts +51 -0
  174. package/src/BottomSheet/index.ts +19 -0
  175. package/src/BottomSheet/snapOffsets.test.ts +254 -0
  176. package/src/BottomSheet/snapOffsets.ts +245 -0
  177. package/src/BottomSheet/useMobileKeyboard.ts +499 -0
  178. package/src/BottomSheet/useSheetGestures.test.ts +1112 -0
  179. package/src/BottomSheet/useSheetGestures.ts +1503 -0
  180. package/src/Calendar/Calendar.doc.mjs +16 -0
  181. package/src/Calendar/Calendar.test.tsx +96 -0
  182. package/src/Calendar/Calendar.tsx +55 -0
  183. package/src/Calendar/hooks/useCalendarConstraints.ts +54 -3
  184. package/src/Chat/ChatComposerInput.tsx +3 -4
  185. package/src/CheckboxInput/CheckboxInput.tsx +6 -0
  186. package/src/ComplexSelector/ComplexSelector.tsx +15 -6
  187. package/src/ContextMenu/ContextMenu.tsx +4 -2
  188. package/src/DateInput/DateInput.test.tsx +21 -0
  189. package/src/DateInput/DateInput.tsx +12 -2
  190. package/src/DateRangeInput/DateRangeInput.doc.mjs +16 -0
  191. package/src/DateRangeInput/DateRangeInput.test.tsx +81 -1
  192. package/src/DateRangeInput/DateRangeInput.tsx +72 -1
  193. package/src/DateTimeInput/DateTimeInput.doc.mjs +4 -0
  194. package/src/DateTimeInput/DateTimeInput.test.tsx +137 -0
  195. package/src/DateTimeInput/DateTimeInput.tsx +43 -4
  196. package/src/Dialog/Dialog.tsx +2 -1
  197. package/src/Field/Field.doc.mjs +1 -0
  198. package/src/Field/FieldLabel.tsx +20 -4
  199. package/src/Field/InputClearButton.test.tsx +25 -0
  200. package/src/Field/InputClearButton.tsx +4 -1
  201. package/src/FileInput/FileInput.test.tsx +53 -9
  202. package/src/FormLayout/FormLayout.doc.mjs +13 -0
  203. package/src/FormLayout/FormLayout.test.tsx +181 -4
  204. package/src/FormLayout/FormLayout.tsx +33 -2
  205. package/src/FormLayout/FormLayoutContext.ts +22 -7
  206. package/src/FormLayout/index.ts +1 -1
  207. package/src/Lightbox/Lightbox.test.tsx +49 -28
  208. package/src/Lightbox/Lightbox.tsx +7 -3
  209. package/src/MobileNav/MobileNav.tsx +128 -37
  210. package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +426 -0
  211. package/src/MobileNav/MobileNavCloseTiming.test.ts +58 -0
  212. package/src/MobileNav/MobileNavCloseVisibility.test.tsx +172 -0
  213. package/src/MultiSelector/MultiSelector.test.tsx +195 -51
  214. package/src/MultiSelector/MultiSelector.tsx +25 -9
  215. package/src/NumberInput/NumberInput.test.tsx +18 -0
  216. package/src/NumberInput/NumberInput.tsx +12 -2
  217. package/src/PowerSearch/PowerSearchEditPopover.tsx +1 -1
  218. package/src/RadioList/RadioList.tsx +9 -1
  219. package/src/Selector/Selector.test.tsx +85 -26
  220. package/src/Selector/Selector.tsx +16 -5
  221. package/src/StatusDot/StatusDot.doc.mjs +13 -0
  222. package/src/StatusDot/StatusDot.test.tsx +115 -8
  223. package/src/StatusDot/StatusDot.tsx +74 -9
  224. package/src/Switch/Switch.test.tsx +16 -0
  225. package/src/Switch/Switch.tsx +13 -0
  226. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +20 -2
  227. package/src/TextArea/TextArea.tsx +3 -1
  228. package/src/TextInput/TextInput.tsx +3 -1
  229. package/src/TimeInput/TimeInput.test.tsx +23 -0
  230. package/src/TimeInput/TimeInput.tsx +11 -1
  231. package/src/Tokenizer/Tokenizer.test.tsx +50 -32
  232. package/src/Tokenizer/Tokenizer.tsx +5 -5
  233. package/src/Tooltip/useTooltip.tsx +4 -1
  234. package/src/Typeahead/BaseTypeahead.tsx +10 -3
  235. package/src/Typeahead/Typeahead.test.tsx +116 -7
  236. package/src/Typeahead/Typeahead.tsx +10 -1
  237. package/src/__tests__/stubMatchMedia.test.ts +79 -0
  238. package/src/__tests__/stubMatchMedia.ts +95 -0
  239. package/src/hooks/index.ts +9 -5
  240. package/src/hooks/useFocusTrap.ts +1 -14
  241. package/src/hooks/useInputStatusIcon.test.tsx +19 -0
  242. package/src/hooks/useInputStatusIcon.tsx +7 -0
  243. package/src/hooks/useResolvedRequired.ts +42 -0
  244. package/src/index.ts +1 -0
  245. package/src/theme/defineTheme.ts +22 -4
  246. package/src/theme/expandColorScale.test.ts +138 -0
  247. package/src/theme/expandColorScale.ts +89 -44
  248. package/src/utils/ime.test.ts +32 -0
  249. package/src/utils/ime.ts +69 -0
  250. package/src/utils/index.ts +2 -0
  251. package/src/utils/plainDate.test.ts +50 -0
  252. package/src/utils/plainDate.ts +12 -0
  253. package/dist/astryx.umd.js +0 -147
  254. package/dist/astryx.umd.js.map +0 -7
@@ -0,0 +1,441 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file BottomSheetPanel.tsx
7
+ * @input Uses React, StyleX, theme tokens, useSheetGestures
8
+ * @output Internal BottomSheetPanel surface and motion-state types
9
+ * @position Shared presentation layer for standalone and switcher BottomSheets
10
+ *
11
+ * This component owns everything intrinsic to a sheet surface: height budgets,
12
+ * drag and snap gestures, the handle and scrolling body, motion styles, and
13
+ * transition completion. It deliberately does not own a dialog, focus, inert
14
+ * state, or switcher registration; those belong to the hosting controller.
15
+ *
16
+ * SYNC: When modified, update these files to stay in sync:
17
+ * - /packages/core/src/BottomSheet/BottomSheet.tsx
18
+ * - /packages/core/src/BottomSheet/BottomSheetPanel.test.tsx
19
+ * - /packages/core/src/BottomSheet/snapOffsets.ts
20
+ * - /packages/core/src/BottomSheet/useMobileKeyboard.ts
21
+ * - /packages/core/src/BottomSheet/useSheetGestures.ts
22
+ */
23
+ import { useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef } from 'react';
24
+ import * as stylex from '@stylexjs/stylex';
25
+ import { useDevWarning } from "../hooks/index.js";
26
+ import "../theme/tokens.stylex.js";
27
+ import { colorVars, durationVars, easeVars, radiusVars, shadowVars, sizeVars, spacingVars } from "../theme/tokens.stylex.js";
28
+ import { mergeProps, themeProps } from "../utils/index.js";
29
+ import { isValidSnapPoint, resolveSnapPoints } from "./snapOffsets.js";
30
+ import { useMobileKeyboard } from "./useMobileKeyboard.js";
31
+ import { useSheetGestures } from "./useSheetGestures.js";
32
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
33
+ const HEIGHT_BUDGETS = {
34
+ hug: '92dvh',
35
+ capped: '62dvh',
36
+ tall: '92dvh'
37
+ };
38
+ // SYNC: must match OVERSCROLL_MAX in useSheetGestures.ts.
39
+ const OVERSCROLL_PADDING = 48;
40
+ const MOBILE_KEYBOARD_BOTTOM_CLEARANCE = 48;
41
+ const TRANSITION_BACKSTOP_BUFFER_MS = 50;
42
+
43
+ // Measure the same viewport the height budgets are written against. Those are
44
+ // `dvh`, which the virtual keyboard does not shrink, so reading
45
+ // `visualViewport` here would make the two disagree by exactly the keyboard's
46
+ // height: every detent would move while the sheet it measures did not. A
47
+ // keyboard is `useMobileKeyboard`'s business — it holds the sheet still and
48
+ // scrolls the body — and it does not redefine the sheet's detents.
49
+ function layoutViewportHeight() {
50
+ return typeof window === 'undefined' ? 0 : window.innerHeight;
51
+ }
52
+
53
+ /**
54
+ * A stable identity for a set of snap points. Type-tagged, so the fraction
55
+ * `0.5` and the (invalid) string `'0.5'` cannot collide on one key.
56
+ *
57
+ * Recomputed every render — the panel re-renders on every frame of a drag — so
58
+ * it stays a single pass over a handful of values, and everything derived from
59
+ * the points hangs off it instead of being rebuilt per frame.
60
+ */
61
+ function snapPointsKeyFor(points) {
62
+ let key = '';
63
+ for (const point of points ?? []) {
64
+ key += `${typeof point}:${point}|`;
65
+ }
66
+ return key;
67
+ }
68
+ const styles = {
69
+ sheet: {
70
+ kfzvcC: "x67bb7w",
71
+ kB7OPa: "x9f619",
72
+ k1xSpc: "x78zum5",
73
+ kXwgrk: "xdt5ytf",
74
+ kAzted: "x2lwn1j",
75
+ kzqmXN: "xh8yej3",
76
+ ks0D6T: "x11gisft",
77
+ kWkggS: "x10xzikg",
78
+ krdFHd: "x81l70g",
79
+ kfmiAY: "x7hs6f1",
80
+ kGVxlE: "x1kcpxr7",
81
+ kI3sdo: "x1a2a7pz",
82
+ kVQacm: "xb3r6kr",
83
+ kGO01o: "x1wkw6tp",
84
+ k1K539: "x1if0o47",
85
+ k3aq6I: "xnn1q72 xhbqy3z",
86
+ kSiTet: "x1hc1fzr",
87
+ k1ekBW: "xwcsmn1",
88
+ kIyJzY: "x80gvsz",
89
+ kAMwcw: "xlr8y92",
90
+ k6sLGO: "x1q1rmc8",
91
+ k6CgDc: "xzg1mie",
92
+ $$css: true
93
+ },
94
+ sheetClosing: {
95
+ k3aq6I: "x1weeur4",
96
+ $$css: true
97
+ },
98
+ sheetFading: {
99
+ kSiTet: "xg01cxk",
100
+ $$css: true
101
+ },
102
+ sheetInactive: {
103
+ kfzvcC: "x47corl",
104
+ $$css: true
105
+ },
106
+ budget: {
107
+ kZKoxP: "x1tpcejd",
108
+ $$css: true
109
+ },
110
+ hugHeight: {
111
+ kZKoxP: "xg7h5cd",
112
+ kskxy: "x14bu9tk",
113
+ $$css: true
114
+ }
115
+ };
116
+ function motionForState(state) {
117
+ if (state.kind === 'open') {
118
+ return state.entering ? 'entering' : null;
119
+ }
120
+ if (state.kind === 'retained') {
121
+ return state.motion === 'covered' ? null : state.motion;
122
+ }
123
+ return state.kind === 'exiting' ? 'exiting' : null;
124
+ }
125
+ function waitForTransition(element, propertyName, complete) {
126
+ if (element == null) {
127
+ complete();
128
+ return () => {};
129
+ }
130
+ let done = false;
131
+ let timer = null;
132
+ const finish = () => {
133
+ if (done) {
134
+ return;
135
+ }
136
+ done = true;
137
+ if (timer != null) {
138
+ clearTimeout(timer);
139
+ }
140
+ element.removeEventListener('transitionend', handleTransitionEnd);
141
+ element.removeEventListener('transitioncancel', handleTransitionEnd);
142
+ complete();
143
+ };
144
+ const handleTransitionEnd = event => {
145
+ if (event.target === element && event.propertyName === propertyName) {
146
+ finish();
147
+ }
148
+ };
149
+ element.addEventListener('transitionend', handleTransitionEnd);
150
+ element.addEventListener('transitioncancel', handleTransitionEnd);
151
+ const computedStyle = getComputedStyle(element);
152
+ if (element.style.transition.trim() === 'none' || computedStyle.transition.trim() === 'none') {
153
+ finish();
154
+ return () => {};
155
+ }
156
+ const properties = computedStyle.transitionProperty.split(',').map(value => value.trim());
157
+ const durations = computedStyle.transitionDuration.split(',').map(parseTransitionTime);
158
+ const delays = computedStyle.transitionDelay.split(',').map(parseTransitionTime);
159
+ let hasUnresolvedTiming = false;
160
+ const transitionMs = properties.reduce((longest, property, index) => {
161
+ if (property !== propertyName && property !== 'all') {
162
+ return longest;
163
+ }
164
+ const duration = durations[index % durations.length];
165
+ const delay = delays[index % delays.length];
166
+ if (duration == null || delay == null) {
167
+ hasUnresolvedTiming = true;
168
+ return longest;
169
+ }
170
+ return Math.max(longest, duration + delay);
171
+ }, 0);
172
+ if (hasUnresolvedTiming) {
173
+ // JSDOM can leave CSS variables unresolved. In that case the native event
174
+ // remains authoritative; choosing a fixed timeout here would make an
175
+ // assumption about the consumer's theme.
176
+ return () => {
177
+ element.removeEventListener('transitionend', handleTransitionEnd);
178
+ element.removeEventListener('transitioncancel', handleTransitionEnd);
179
+ };
180
+ }
181
+ if (transitionMs <= 0) {
182
+ finish();
183
+ return () => {};
184
+ }
185
+ timer = setTimeout(finish, transitionMs + TRANSITION_BACKSTOP_BUFFER_MS);
186
+ return () => {
187
+ if (timer != null) {
188
+ clearTimeout(timer);
189
+ }
190
+ element.removeEventListener('transitionend', handleTransitionEnd);
191
+ element.removeEventListener('transitioncancel', handleTransitionEnd);
192
+ };
193
+ }
194
+ function parseTransitionTime(value) {
195
+ const normalizedValue = value.trim();
196
+ if (!/^-?(?:\d+|\d*\.\d+)(?:ms|s)$/.test(normalizedValue)) {
197
+ return null;
198
+ }
199
+ const time = Number.parseFloat(value);
200
+ return normalizedValue.endsWith('ms') ? time : time * 1000;
201
+ }
202
+
203
+ /** Internal visual and gesture surface shared by every BottomSheet host. */
204
+ export function BottomSheetPanel({
205
+ ref,
206
+ state,
207
+ height,
208
+ children,
209
+ snapPoints,
210
+ className,
211
+ style,
212
+ tabIndex,
213
+ xstyle,
214
+ isSwipeDismissAllowed = true,
215
+ isPageScrollLocked = false,
216
+ onDismiss,
217
+ onScrimOpacity,
218
+ onElementChange,
219
+ onMotionStart,
220
+ onMotionComplete,
221
+ ...props
222
+ }) {
223
+ const elementRef = useRef(null);
224
+ const previousStateRef = useRef(state);
225
+ const reactivatedEntranceRef = useRef(false);
226
+ const onMotionStartRef = useRef(onMotionStart);
227
+ const onMotionCompleteRef = useRef(onMotionComplete);
228
+ const startedMotionRef = useRef(null);
229
+ const pendingMotionCompleteRef = useRef(null);
230
+ const isEntering = state.kind === 'open' && state.entering;
231
+ const previousState = previousStateRef.current;
232
+ const wasEntering = previousState.kind === 'open' && previousState.entering;
233
+ if (isEntering && !wasEntering) {
234
+ reactivatedEntranceRef.current = previousState.kind === 'retained';
235
+ } else if (!isEntering) {
236
+ reactivatedEntranceRef.current = false;
237
+ }
238
+ useLayoutEffect(() => {
239
+ previousStateRef.current = state;
240
+ onMotionStartRef.current = onMotionStart;
241
+ onMotionCompleteRef.current = onMotionComplete;
242
+ }, [onMotionComplete, onMotionStart, state]);
243
+ const isInteractive = state.kind === 'open';
244
+ const isPresented = state.kind !== 'hidden';
245
+ const isRetained = state.kind === 'retained';
246
+ const isInactive = isRetained || state.kind === 'exiting';
247
+ const isClosing = state.kind === 'exiting';
248
+ const isFading = isRetained && state.motion === 'fading';
249
+ const alignmentOffset = isRetained ? state.alignmentOffset : 0;
250
+
251
+ // Everything derived from the snap points keys off their identity, not the
252
+ // array's: the resolver's identity is the hook's signal that the stops
253
+ // changed, so it must not churn on a re-render the drag caused. The memo
254
+ // reads the points through a ref, so the resolver resolves them at call time
255
+ // — against whatever the viewport is then, not what it was at memo time.
256
+ const snapPointsKey = snapPointsKeyFor(snapPoints);
257
+ const snapPointsRef = useRef(snapPoints);
258
+ snapPointsRef.current = snapPoints;
259
+ const {
260
+ snapHeights,
261
+ ignoredSnapPointsMessage
262
+ } = useMemo(() => {
263
+ if (snapPointsKey === '') {
264
+ return {
265
+ snapHeights: undefined,
266
+ ignoredSnapPointsMessage: ''
267
+ };
268
+ }
269
+ const ignored = (snapPointsRef.current ?? []).filter(point => !isValidSnapPoint(point));
270
+ return {
271
+ snapHeights: () => resolveSnapPoints(snapPointsRef.current ?? [], layoutViewportHeight()),
272
+ ignoredSnapPointsMessage: ignored.length === 0 ? '' : `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%').`
273
+ };
274
+ }, [snapPointsKey]);
275
+ useDevWarning('BottomSheet', ignoredSnapPointsMessage, ignoredSnapPointsMessage !== '');
276
+ const {
277
+ contentProps,
278
+ handleProps,
279
+ bodyProps,
280
+ bodyElementRef,
281
+ sheetRef,
282
+ dragOffset,
283
+ settledOffset,
284
+ isDragging,
285
+ sheetHeight,
286
+ scrollPreservationInset,
287
+ settlingLayoutOffset,
288
+ settledLayoutOffset,
289
+ completeScrollAreaSettle
290
+ } = useSheetGestures({
291
+ isOpen: isInteractive,
292
+ canDismiss: isSwipeDismissAllowed,
293
+ offscreenBlockEndInset: OVERSCROLL_PADDING,
294
+ onDismiss,
295
+ snapHeights,
296
+ onScrimOpacity
297
+ });
298
+ const setElement = useCallback(element => {
299
+ sheetRef(element);
300
+ elementRef.current = element;
301
+ onElementChange?.(element);
302
+ }, [onElementChange, sheetRef]);
303
+ useMobileKeyboard({
304
+ bodyRef: bodyElementRef,
305
+ bottomClearance: MOBILE_KEYBOARD_BOTTOM_CLEARANCE,
306
+ isEnabled: height === 'tall',
307
+ isFullyExpanded: settledOffset === 0,
308
+ isPageScrollLocked,
309
+ isSheetTraveling: isDragging && dragOffset !== settledOffset,
310
+ isOpen: isInteractive,
311
+ isPresented,
312
+ sheetRef: elementRef
313
+ });
314
+ // Keep controller registration attached to one stable host ref. React
315
+ // detaches an old callback ref when a consumer supplies a new identity; if
316
+ // that public ref were merged with setElement, an ordinary parent rerender
317
+ // could be mistaken for the panel unmounting and cancel a sheet handoff.
318
+ useImperativeHandle(ref, () => elementRef.current, []);
319
+ const motion = motionForState(state);
320
+ useLayoutEffect(() => {
321
+ if (motion == null) {
322
+ return;
323
+ }
324
+ startedMotionRef.current = null;
325
+ pendingMotionCompleteRef.current = null;
326
+ if (motion === 'entering' && reactivatedEntranceRef.current) {
327
+ pendingMotionCompleteRef.current = motion;
328
+ return;
329
+ }
330
+ return waitForTransition(elementRef.current, motion === 'fading' ? 'opacity' : 'transform', () => {
331
+ if (startedMotionRef.current === motion) {
332
+ onMotionCompleteRef.current?.(motion);
333
+ } else {
334
+ pendingMotionCompleteRef.current = motion;
335
+ }
336
+ });
337
+ }, [motion]);
338
+ // The snap is transform-only; the layout height reconciles when it lands.
339
+ // waitForTransition — the same helper the motion states use — resolves that
340
+ // even when no `transitionend` is coming: inline or computed
341
+ // `transition: none`, a zero duration, and a timer backstop otherwise.
342
+ useLayoutEffect(() => {
343
+ if (settlingLayoutOffset == null) {
344
+ return;
345
+ }
346
+ return waitForTransition(elementRef.current, 'transform', completeScrollAreaSettle);
347
+ }, [completeScrollAreaSettle, settlingLayoutOffset]);
348
+ useEffect(() => {
349
+ if (motion == null) {
350
+ return;
351
+ }
352
+ onMotionStartRef.current?.(motion);
353
+ startedMotionRef.current = motion;
354
+ if (pendingMotionCompleteRef.current === motion) {
355
+ pendingMotionCompleteRef.current = null;
356
+ onMotionCompleteRef.current?.(motion);
357
+ }
358
+ return () => {
359
+ if (startedMotionRef.current === motion) {
360
+ startedMotionRef.current = null;
361
+ }
362
+ if (pendingMotionCompleteRef.current === motion) {
363
+ pendingMotionCompleteRef.current = null;
364
+ }
365
+ };
366
+ }, [motion]);
367
+ const isNamedHeight = typeof height === 'string' && height in HEIGHT_BUDGETS;
368
+ const budget = isNamedHeight ? HEIGHT_BUDGETS[height] : typeof height === 'number' ? `${height}px` : height;
369
+ const hasMeasuredSheet = sheetHeight > 0;
370
+ let gestureTransform = contentProps.style.transform;
371
+ let resizedHeight;
372
+ if (hasMeasuredSheet) {
373
+ // The sheet's travel is split across two properties: `layoutOffset` is the
374
+ // part the scrolling area gives up as layout height, and the remainder is
375
+ // a compositor transform. Live gestures and snaps only ever move the
376
+ // transform — the layout height changes at rest, in one reconciling render
377
+ // whose visible geometry is identical (see useSheetGestures).
378
+ // - dragging above the base restores the full height below the viewport
379
+ // - a peek settles with layout 0, so it slides rather than reflowing
380
+ const layoutOffset = isDragging ? dragOffset < settledOffset ? 0 : settledLayoutOffset : settlingLayoutOffset ?? settledLayoutOffset;
381
+ const activeOffset = isDragging ? dragOffset : settledOffset;
382
+ const translation = activeOffset - layoutOffset;
383
+
384
+ // At layout 0 the sheet is its natural height, so leave that to CSS —
385
+ // `hug` sheets must stay fit-content. While the sheet is in motion it is
386
+ // pinned in px instead: content reflowing mid-gesture would move the
387
+ // surface out from under the finger, or under the snap.
388
+ const isTraveling = isDragging || settlingLayoutOffset != null;
389
+ resizedHeight = layoutOffset > 0 || isTraveling ? `${Math.max(0, sheetHeight - Math.max(0, layoutOffset))}px` : undefined;
390
+ gestureTransform = translation !== 0 ? `translateY(${translation}px)` : undefined;
391
+ }
392
+ const retainedTransform = alignmentOffset > 0 ? [gestureTransform, `translateY(${alignmentOffset}px)`].filter(Boolean).join(' ') : gestureTransform;
393
+ const gestureStyle = {
394
+ ...contentProps.style,
395
+ transform: gestureTransform,
396
+ height: resizedHeight
397
+ };
398
+ return /*#__PURE__*/_jsxs("div", {
399
+ ...props,
400
+ ref: setElement,
401
+ tabIndex: tabIndex ?? -1,
402
+ ...mergeProps(themeProps('bottom-sheet'), stylex.props(styles.sheet, height === 'hug' ? styles.hugHeight : styles.budget, isClosing && styles.sheetClosing, isFading && styles.sheetFading, isInactive && styles.sheetInactive, xstyle), className, {
403
+ ['--_sheet-budget']: budget,
404
+ ...(isInteractive ? gestureStyle : isRetained ? {
405
+ transform: retainedTransform,
406
+ height: resizedHeight
407
+ } : isClosing ? {
408
+ height: resizedHeight
409
+ } : {}),
410
+ ...style
411
+ }),
412
+ children: [/*#__PURE__*/_jsx("div", {
413
+ ...{
414
+ className: "x2lah0s x78zum5 x6s0dn4 xl56j7k x1k15mir x5ve5x3 x1jm3nie"
415
+ },
416
+ ...handleProps,
417
+ "aria-hidden": "true",
418
+ children: /*#__PURE__*/_jsx("div", {
419
+ ...{
420
+ className: "x1m747yf x11c6zpc xjspbzw x1m4xfpy"
421
+ }
422
+ })
423
+ }), /*#__PURE__*/_jsx("div", {
424
+ ...mergeProps({
425
+ 0: {
426
+ className: "x1iyjqo2 x2lwn1j x9f619 x1odjw0f x8du1vd xx69xxh x18d9i69"
427
+ },
428
+ 1: {
429
+ className: "x1iyjqo2 x2lwn1j x9f619 x1odjw0f x8du1vd xx69xxh x18d9i69 xrw9175 x1s928wv xhkezso xu6fald x2q1x1w"
430
+ }
431
+ }[!!(height === 'tall') << 0], scrollPreservationInset > 0 ? {
432
+ style: {
433
+ paddingBlockEnd: `${scrollPreservationInset}px`
434
+ }
435
+ } : {}),
436
+ ...bodyProps,
437
+ children: children
438
+ })]
439
+ });
440
+ }
441
+ BottomSheetPanel.displayName = 'BottomSheetPanel';
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @file BottomSheetSwitcher.tsx
3
+ * @input Uses React context, StyleX, theme tokens, focus/scroll-lock hooks, BottomSheetSwitcherContext
4
+ * @output Exports BottomSheetSwitcher and BottomSheetSwitcherProps
5
+ * @position Core switcher for mutually exclusive BottomSheet flows
6
+ *
7
+ * The switcher turns a set of declaratively nested BottomSheets into a
8
+ * controlled single-selection group: `activeSheet` names the one interactive
9
+ * child, or is null when the flow is closed. During a handoff the new sheet
10
+ * enters above the previous sheet. If it is shorter, the previous sheet moves
11
+ * down at the same time until their top edges align; otherwise it stays
12
+ * stationary. The previous sheet fades only after both motions complete.
13
+ *
14
+ * All child sheets render as panels inside one switcher-owned `<dialog>`. A
15
+ * scrim flow calls showModal() once and keeps that native top-layer dialog open
16
+ * across every handoff. A no-scrim flow calls show() on the same non-modal shell.
17
+ * This keeps one modal boundary and one native ::backdrop without a portal.
18
+ *
19
+ * SYNC: When modified, update these files to stay in sync:
20
+ * - /packages/core/src/BottomSheet/BottomSheet.tsx
21
+ * - /packages/core/src/BottomSheet/BottomSheetSwitcher.doc.mjs
22
+ * - /packages/core/src/BottomSheet/BottomSheetSwitcher.test.tsx
23
+ * - /packages/core/src/BottomSheet/index.ts
24
+ * - /apps/storybook/stories/BottomSheetSwitcher.stories.tsx
25
+ * - /packages/cli/assets/templates/blocks/components/BottomSheet/BottomSheetSwitcherShowcase.tsx
26
+ */
27
+ import { type ReactNode, type SyntheticEvent } from 'react';
28
+ import type { BaseProps } from '../BaseProps';
29
+ export interface BottomSheetSwitcherProps extends BaseProps<HTMLDialogElement> {
30
+ /** Ref forwarded to the shared native dialog. */
31
+ ref?: React.Ref<HTMLDialogElement>;
32
+ /** Called when the shared native dialog receives a cancel event. */
33
+ onCancel?: (event: SyntheticEvent<HTMLDialogElement>) => void;
34
+ /**
35
+ * ID of the interactive BottomSheet, or null when the flow should close.
36
+ * Must match a nested BottomSheet's `sheetId`. The previous sheet may remain
37
+ * visually present and inert while the new sheet enters, moving downward at
38
+ * the same time if the new sheet is shorter, then fade away.
39
+ */
40
+ activeSheet: string | null;
41
+ /** Called with null when the active sheet requests dismissal. */
42
+ onActiveSheetChange: (sheetId: string | null) => void;
43
+ /**
44
+ * Whether to open the shared dialog modally with its native ::backdrop.
45
+ * Disable for a viewport-anchored, non-modal flow over an interactive page.
46
+ * @default true
47
+ */
48
+ hasScrim?: boolean;
49
+ /** BottomSheets identified by unique `sheetId` values. */
50
+ children: ReactNode;
51
+ }
52
+ /**
53
+ * Coordinates a set of BottomSheets so zero or one is active at a time inside
54
+ * one shared native dialog. During a handoff the previous panel stays visible
55
+ * and inert beneath the entering panel, then fades after motion completes.
56
+ */
57
+ export declare function BottomSheetSwitcher({ activeSheet, onActiveSheetChange, hasScrim, children, ref, xstyle, className, style, onCancel, onClick, onKeyDown, ...props }: BottomSheetSwitcherProps): import("react").JSX.Element;
58
+ export declare namespace BottomSheetSwitcher {
59
+ var displayName: string;
60
+ }
61
+ //# sourceMappingURL=BottomSheetSwitcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BottomSheetSwitcher.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/BottomSheetSwitcher.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAQL,KAAK,SAAS,EACd,KAAK,cAAc,EACpB,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AA2H5C,MAAM,WAAW,wBAAyB,SAAQ,SAAS,CAAC,iBAAiB,CAAC;IAC5E,iDAAiD;IACjD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAEnC,oEAAoE;IACpE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;IAE9D;;;;;OAKG;IACH,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAE3B,iEAAiE;IACjE,mBAAmB,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAEtD;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,0DAA0D;IAC1D,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,WAAW,EACX,mBAAmB,EACnB,QAAe,EACf,QAAQ,EACR,GAAG,EACH,MAAM,EACN,SAAS,EACT,KAAK,EACL,QAAQ,EACR,OAAO,EACP,SAAS,EACT,GAAG,KAAK,EACT,EAAE,wBAAwB,+BA4Z1B;yBAzae,mBAAmB"}