@astryxdesign/core 0.4.4 → 0.4.5-canary.33ba2b6

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 (177) hide show
  1. package/CHANGELOG.md +39 -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 +56 -13
  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/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  31. package/dist/CheckboxInput/CheckboxInput.js +9 -0
  32. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  33. package/dist/ComplexSelector/ComplexSelector.js +6 -1
  34. package/dist/DateInput/DateInput.d.ts.map +1 -1
  35. package/dist/DateInput/DateInput.js +6 -1
  36. package/dist/DateRangeInput/DateRangeInput.d.ts +23 -1
  37. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  38. package/dist/DateRangeInput/DateRangeInput.js +38 -3
  39. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  40. package/dist/DateTimeInput/DateTimeInput.js +7 -2
  41. package/dist/Field/FieldLabel.d.ts +2 -1
  42. package/dist/Field/FieldLabel.d.ts.map +1 -1
  43. package/dist/Field/FieldLabel.js +21 -3
  44. package/dist/Field/InputClearButton.d.ts +2 -1
  45. package/dist/Field/InputClearButton.d.ts.map +1 -1
  46. package/dist/Field/InputClearButton.js +6 -1
  47. package/dist/FormLayout/FormLayout.d.ts +24 -2
  48. package/dist/FormLayout/FormLayout.d.ts.map +1 -1
  49. package/dist/FormLayout/FormLayout.js +7 -2
  50. package/dist/FormLayout/FormLayoutContext.d.ts +15 -2
  51. package/dist/FormLayout/FormLayoutContext.d.ts.map +1 -1
  52. package/dist/FormLayout/FormLayoutContext.js +19 -4
  53. package/dist/FormLayout/index.d.ts +1 -1
  54. package/dist/FormLayout/index.d.ts.map +1 -1
  55. package/dist/Markdown/Markdown.d.ts +8 -0
  56. package/dist/Markdown/Markdown.d.ts.map +1 -1
  57. package/dist/Markdown/Markdown.js +31 -3
  58. package/dist/Markdown/parser.d.ts +14 -2
  59. package/dist/Markdown/parser.d.ts.map +1 -1
  60. package/dist/Markdown/parser.js +50 -2
  61. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  62. package/dist/MultiSelector/MultiSelector.js +6 -1
  63. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  64. package/dist/NumberInput/NumberInput.js +6 -1
  65. package/dist/Outline/parseOutlineFromMarkdown.d.ts +2 -0
  66. package/dist/Outline/parseOutlineFromMarkdown.d.ts.map +1 -1
  67. package/dist/Outline/parseOutlineFromMarkdown.js +5 -31
  68. package/dist/RadioList/RadioList.d.ts.map +1 -1
  69. package/dist/RadioList/RadioList.js +12 -1
  70. package/dist/Selector/Selector.d.ts.map +1 -1
  71. package/dist/Selector/Selector.js +6 -1
  72. package/dist/StatusDot/StatusDot.d.ts +43 -3
  73. package/dist/StatusDot/StatusDot.d.ts.map +1 -1
  74. package/dist/StatusDot/StatusDot.js +42 -5
  75. package/dist/Switch/Switch.d.ts.map +1 -1
  76. package/dist/Switch/Switch.js +9 -0
  77. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  78. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +7 -2
  79. package/dist/TextArea/TextArea.d.ts.map +1 -1
  80. package/dist/TextArea/TextArea.js +6 -1
  81. package/dist/TextInput/TextInput.d.ts.map +1 -1
  82. package/dist/TextInput/TextInput.js +6 -1
  83. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  84. package/dist/TimeInput/TimeInput.js +6 -1
  85. package/dist/hooks/useResolvedRequired.d.ts +19 -0
  86. package/dist/hooks/useResolvedRequired.d.ts.map +1 -0
  87. package/dist/hooks/useResolvedRequired.js +40 -0
  88. package/dist/utils/plainDate.d.ts +6 -0
  89. package/dist/utils/plainDate.d.ts.map +1 -1
  90. package/dist/utils/plainDate.js +12 -0
  91. package/locales/af-ZA.json +958 -0
  92. package/locales/ar-SA.json +998 -0
  93. package/locales/ca-ES.json +974 -0
  94. package/locales/cs-CZ.json +990 -0
  95. package/locales/da-DK.json +966 -0
  96. package/locales/de-DE.json +966 -0
  97. package/locales/el-GR.json +990 -0
  98. package/locales/en.json +4 -0
  99. package/locales/es-ES.json +978 -0
  100. package/locales/fi-FI.json +990 -0
  101. package/locales/fr-FR.json +928 -0
  102. package/locales/he-IL.json +990 -0
  103. package/locales/hu-HU.json +990 -0
  104. package/locales/it-IT.json +978 -0
  105. package/locales/ja-JP.json +998 -0
  106. package/locales/ko-KR.json +990 -0
  107. package/locales/nl-NL.json +954 -0
  108. package/locales/no-NO.json +978 -0
  109. package/locales/pl-PL.json +982 -0
  110. package/locales/pseudo.json +3 -0
  111. package/locales/pt-BR.json +982 -0
  112. package/locales/pt-PT.json +982 -0
  113. package/locales/ro-RO.json +982 -0
  114. package/locales/ru-RU.json +990 -0
  115. package/locales/sr-SP.json +990 -0
  116. package/locales/sv-SE.json +986 -0
  117. package/locales/tr-TR.json +990 -0
  118. package/locales/uk-UA.json +990 -0
  119. package/locales/vi-VN.json +986 -0
  120. package/locales/zh-CN.json +998 -0
  121. package/locales/zh-TW.json +998 -0
  122. package/package.json +3 -3
  123. package/src/BottomSheet/BottomSheet.doc.mjs +36 -3
  124. package/src/BottomSheet/BottomSheet.test.tsx +153 -2
  125. package/src/BottomSheet/BottomSheet.tsx +16 -2
  126. package/src/BottomSheet/BottomSheetPanel.tsx +67 -13
  127. package/src/BottomSheet/index.ts +5 -1
  128. package/src/BottomSheet/snapOffsets.test.ts +114 -47
  129. package/src/BottomSheet/snapOffsets.ts +119 -33
  130. package/src/BottomSheet/useSheetGestures.test.ts +113 -15
  131. package/src/BottomSheet/useSheetGestures.ts +196 -100
  132. package/src/Calendar/Calendar.doc.mjs +16 -0
  133. package/src/Calendar/Calendar.test.tsx +137 -0
  134. package/src/Calendar/Calendar.tsx +65 -2
  135. package/src/Calendar/getStandaloneShortWeekdayNames.test.ts +54 -0
  136. package/src/Calendar/getStandaloneShortWeekdayNames.ts +38 -0
  137. package/src/Calendar/hooks/useCalendarConstraints.ts +54 -3
  138. package/src/Calendar/hooks/useCalendarDays.ts +13 -10
  139. package/src/Calendar/standaloneShortWeekdayNames.generated.ts +50 -0
  140. package/src/CheckboxInput/CheckboxInput.tsx +6 -0
  141. package/src/ComplexSelector/ComplexSelector.tsx +3 -1
  142. package/src/DateInput/DateInput.tsx +3 -1
  143. package/src/DateRangeInput/DateRangeInput.doc.mjs +16 -0
  144. package/src/DateRangeInput/DateRangeInput.test.tsx +81 -1
  145. package/src/DateRangeInput/DateRangeInput.tsx +72 -1
  146. package/src/DateTimeInput/DateTimeInput.tsx +4 -2
  147. package/src/Field/Field.doc.mjs +1 -0
  148. package/src/Field/FieldLabel.tsx +20 -4
  149. package/src/Field/InputClearButton.test.tsx +25 -0
  150. package/src/Field/InputClearButton.tsx +4 -1
  151. package/src/FormLayout/FormLayout.doc.mjs +13 -0
  152. package/src/FormLayout/FormLayout.test.tsx +181 -4
  153. package/src/FormLayout/FormLayout.tsx +33 -2
  154. package/src/FormLayout/FormLayoutContext.ts +22 -7
  155. package/src/FormLayout/index.ts +1 -1
  156. package/src/Layout/Layout.doc.mjs +18 -0
  157. package/src/Markdown/Markdown.doc.mjs +3 -0
  158. package/src/Markdown/Markdown.test.tsx +78 -0
  159. package/src/Markdown/Markdown.tsx +41 -1
  160. package/src/Markdown/parser.ts +60 -2
  161. package/src/MultiSelector/MultiSelector.tsx +3 -1
  162. package/src/NumberInput/NumberInput.tsx +3 -1
  163. package/src/Outline/Outline.doc.mjs +2 -0
  164. package/src/Outline/parseOutlineFromMarkdown.ts +10 -40
  165. package/src/RadioList/RadioList.tsx +9 -1
  166. package/src/Selector/Selector.tsx +3 -1
  167. package/src/StatusDot/StatusDot.doc.mjs +13 -0
  168. package/src/StatusDot/StatusDot.test.tsx +115 -8
  169. package/src/StatusDot/StatusDot.tsx +74 -9
  170. package/src/Switch/Switch.tsx +6 -0
  171. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +20 -2
  172. package/src/TextArea/TextArea.tsx +3 -1
  173. package/src/TextInput/TextInput.tsx +3 -1
  174. package/src/TimeInput/TimeInput.tsx +3 -1
  175. package/src/hooks/useResolvedRequired.ts +42 -0
  176. package/src/utils/plainDate.test.ts +50 -0
  177. package/src/utils/plainDate.ts +12 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # @xds/core
2
2
 
3
+ # 0.4.5
4
+
5
+ #### New Features
6
+
7
+ - BottomSheet: `snapPoints` makes the drag-to-resize stops the host's choice. A stop is the sheet's visible height, written as a viewport fraction (`0.5`), a percentage (`'50%'`), or a px length (`'320px'`) — matching `height`, where a bare number is also px and a string carries its unit. Fractions and percentages re-resolve when the viewport changes, so a sheet keeps the stop the user chose across a rotation, and swapping the points under a resting sheet re-anchors it the same way. A stop of a quarter of the sheet or less is a peek: it slides away rather than reflowing into a sliver, and thins the scrim. Taller stops are working surfaces, so they lay their content out and keep the scrim full — previously the shortest stop was always a peek, which would have thinned the backdrop of a half-height sheet (#5203).
8
+ Behavior change, deliberate and not breaking: every sheet used to carry three built-in stops (14%, 50% and 92% of the viewport), so a drag could leave it resting somewhere the host never asked for. A sheet now opens and closes unless `snapPoints` says otherwise; pass `snapPoints={[0.14, 0.5, 0.92]}` to keep the old stops. No prop, type, or DOM output was removed or renamed, and swipe-to-dismiss, the height budgets, and mobile-keyboard accommodation are untouched.
9
+ - Astryx ships translations for 28 more locales. `packages/core/locales/` went from `en` and `fr-FR` to 30 files — Arabic, Catalan, Chinese (Simplified and Traditional), Czech, Danish, Dutch, Finnish, German, Greek, Hebrew, Hungarian, Italian, Japanese, Korean, Norwegian, Polish, Portuguese (Brazil and Portugal), Romanian, Russian, Serbian, Spanish, Swedish, Turkish, Ukrainian, Vietnamese and Afrikaans — covering every `@astryx.*` message the components announce or display (#5185).
10
+ Nothing changes unless you ask for it: `InternationalizationProvider` still defaults to English, and the catalogs are loaded through the existing `./locales/*.json` export. An app that already passes a `locale` now gets translated component strings where it previously fell back to English.
11
+
12
+ The catalogs come from Crowdin and are refreshed nightly (#5186), so a translation landing upstream reaches a release without anyone opening a PR by hand.
13
+ - DateRangeInput / Calendar: add `maxRangeSpan` and `minRangeSpan` to constrain the size of a selected range. Once a start date is picked, days outside the allowed window are disabled — e.g. `maxRangeSpan={7}` keeps the range within a 7-day window of the start (#5145).
14
+ - FormLayout: add `defaultOptionality` — set a form-wide default (`'optional'` or `'required'`) so only the exception carries a visible indicator. Under `'optional'` only `isRequired` fields show one; under `'required'` only `isOptional` fields do; a field that restates the default shows nothing. Under `'required'` the unmarked fields also expose `aria-required` so screen readers match what sighted users see — resolved on `aria-required` only, never the native `required` attribute. Unset keeps today's per-field behavior (#4791).
15
+ - Add `astryx-input-clear-button` theme target on the shared clear button wrapper. Themes can now control the clear button's height and hover independently of other ghost buttons — for example suppressing the hover fill or matching a different element size scale (#5093).
16
+
17
+ #### Fixes
18
+
19
+ - BottomSheet: a pull up from the scroll area now expands the sheet on iOS. Below the tallest detent, dragging up inside the content did nothing on a real device while the grab handle worked — the sheet took the gesture and then froze for the rest of the pull. iOS Safari raises PointerEvents for a finger under the same numeric id it puts in `Touch.identifier`, so the drag the touch path started was keyed to a live pointer: `beginDrag` captured that pointer, WebKit handed the capture straight back, and the `lostpointercapture` a millisecond later cancelled the drag. Touch-driven drags are now marked as such — they take no pointer capture, and `lostpointercapture`, `pointercancel` and `pointermove` for that same finger no longer cancel, end or double-drive them. Browsers that keep the two id spaces apart were never affected, which is why this only showed up on device (#5178).
20
+ - Calendar weekday headers now use compact CLDR stand-alone-short names for the selected locale, while preserving the existing `Su` / `Mo` / `Tu` English labels.
21
+ - Render generated id attributes on Markdown headings so Outline hash links scroll to their target. Heading slugs now come from parser helpers shared with parseOutlineFromMarkdown, and the components.heading override receives the generated id (#4765).
22
+ - StatusDot: pair each variant with a distinct built-in shape drawn from the system's semantic icon vocabulary — success a check, warning an exclamation, error a cross, neutral a ring, accent the plain filled dot — so status no longer relies on colour alone (WCAG 2.1 SC 1.4.1). The shapes mirror the marks `Banner`/`FieldStatus` render via `defaultIcons`, a different axis of consistency from `AvatarStatusDot`'s presence shapes (the two share only the neutral ring, intentionally). The diagonal check and cross take a slightly heavier stroke so they stay crisp and distinct at 8px. Also adds an `icon` prop for API parity with `AvatarStatusDot`: a rendered icon replaces the built-in glyph, while booleans and empty renders are ignored so `cond && <Icon />` stays safe. The built-in glyphs resolve through the icon registry under scoped `statusdot:<variant>` keys (the `richtext:*` precedent), so themes can reshape a variant's mark everywhere via `defineTheme({icons})` / `registerIcons` — including marks for augmented custom variants — while overrides of the standard 24px semantic icons deliberately do not leak into the 8px field. Themes can also target the new stable `astryx-statusdot-glyph` class and its `data-shape` attribute — a stroked inline `<svg>` painted from the dot's `currentColor` (#4373).
23
+ - `Table`'s row-expansion chevron now mirrors correctly under RTL. It previously rotated on expand with no RTL handling at all, so the directional glyph pointed the same way regardless of text direction, matching the pattern already used by `TreeListItem`'s chevron (#5153).
24
+
25
+ #### Contributors
26
+
27
+ Thanks to everyone who contributed to this release:
28
+
29
+ - @athz
30
+ - @bhamodi
31
+ - @cixzhang
32
+ - @freddymeta
33
+ - @HelloOjasMutreja
34
+ - @imdreamrunner
35
+ - @jiunshinn
36
+ - @nynexman4464
37
+
38
+ ---
39
+
3
40
  # 0.4.4
4
41
 
5
42
  #### New Components
@@ -16,6 +53,7 @@
16
53
  Three more lessons came out of building a real app on it. The page now `<link>`s the theme's webfont from Google Fonts, because the theme _names_ Figtree and never loads it, so every viewer silently got the fallback stack (#5015 again). It imports the theme OBJECT and wraps in `<Theme theme={neutralTheme} mode="system">`, so light and dark follow the OS — the `data-astryx-theme` attribute alone scopes the stylesheet but cannot switch modes. And `#root:empty` carries a "Loading…" state, because ESM-from-CDN has real latency and a blank page reads as broken. Markup is `htm`, with a comment saying it is optional and `createElement` is the dependency-free alternative.
17
54
 
18
55
  A recipe that is only read is a recipe that is only assumed to work, so CI renders it: `.github/scripts/cdn-template-smoke-test.mjs` scaffolds the page with the real CLI and opens it in headless Chromium, failing on any console error, page error or failed request, and on a page that loads without rendering.
56
+
19
57
  - DateTimeInput: expose `date-time-input-toggle-icon` (calendar glyph, with open/closed `state`) and `date-time-input-clock-icon` (leading time glyph) theme targets, so a theme can size and color the leading icons — matching the `date-input-toggle-icon` seam DateInput already offers (#5148).
20
58
  - `defineTheme`: `color.accent` accepts a `[light, dark]` tuple (#2279)
21
59
  `ColorScaleConfig.accent` now takes either a single hex or a `[light, dark]` tuple, matching `TokenValue`. With a tuple, `expandColorScale` derives the light half of every generated `light-dark()` pair from the light seed's palettes and the dark half from the dark seed's, so each scheme gets a consistent derived palette (muted, on-accent, neutrals) instead of the `tokens['--color-accent']` workaround that skips scale generation. Single-string configs are unchanged, token for token. Also documents the precedence between `color` and `tokens` for accent-derived values: `tokens` entries win token by token, the `var(--color-accent)` reference tokens follow a `--color-accent` override at runtime, and the baked `--color-on-accent` stays derived from the `color.accent` seed.
@@ -43,6 +81,7 @@
43
81
  Nobody has a migration to make, because there was no working configuration to migrate from. The bundle binds Astryx to `window.React` and `window.ReactDOM`, and React 19 does not ship a build that defines them: "UMD builds removed: To load React 19 with a script tag, we recommend using an ESM-based CDN such as esm.sh." `https://unpkg.com/react@19.2.0/umd/react.production.min.js` is a 404 where 18.3.1 is a 200. Our `peerDependencies` are `react >= 19.0.0`, so every supported React is one without a global for the bundle to bind to — it documented a path that never had an entrance.
44
82
 
45
83
  If you were loading it with an older React anyway, load the same components as modules instead: an import map for `react`, `react/jsx-runtime`, `react-dom`, `react-dom/client` and `@astryxdesign/core` (pinned, with `?external=react,react-dom`), then one `<script type="module">`. `astryx template --cdn` writes that page for you, pinned to your installed version and annotated; the recipe is also in the core README under "No build step (CDN)".
84
+
46
85
  - `isImeKeyEvent` — the guard that stops an IME composition keystroke being read as a command — now lives at `@astryxdesign/core/utils` alongside the other pure helpers, with the reasoning for its two signals written down in one place. It stays exported from `@astryxdesign/core/hooks` for this release but is deprecated there: it is a plain predicate, not a hook, and that barrel is a `'use client'` boundary, so importing it from `hooks` pulls a server-safe function onto a client path. Move imports to `@astryxdesign/core/utils`; the `hooks` re-export will be removed in an upcoming major (#4907).
47
86
 
48
87
  #### Contributors
@@ -22,8 +22,8 @@
22
22
  import { type ReactNode } from 'react';
23
23
  import type { BaseProps } from '../BaseProps';
24
24
  import type { DialogPurpose } from '../Dialog';
25
- export type { BottomSheetHeight } from './BottomSheetPanel';
26
- import type { BottomSheetHeight } from './BottomSheetPanel';
25
+ export type { BottomSheetHeight, BottomSheetSnapPoint } from './BottomSheetPanel';
26
+ import type { BottomSheetHeight, BottomSheetSnapPoint } from './BottomSheetPanel';
27
27
  interface BottomSheetSharedProps extends BaseProps<HTMLDivElement> {
28
28
  /** Ref forwarded to the visual sheet panel <div>. */
29
29
  ref?: React.Ref<HTMLDivElement>;
@@ -33,6 +33,15 @@ interface BottomSheetSharedProps extends BaseProps<HTMLDivElement> {
33
33
  children: ReactNode;
34
34
  /** Height budget or custom CSS length. Only fully expanded Tall is keyboard-aware. @default 'capped' */
35
35
  height?: BottomSheetHeight | number | string;
36
+ /**
37
+ * Extra heights the sheet can rest at when dragged; its own height is always
38
+ * the tallest stop, and omitting this gives a sheet that only opens and
39
+ * closes. Each stop is the sheet's visible height: a number is a viewport
40
+ * fraction (`0.5` is half the screen), `'50%'` the same in CSS, `'320px'` an
41
+ * absolute length. A stop of a quarter of the sheet or less is a peek — it
42
+ * slides away instead of reflowing, and thins the scrim.
43
+ */
44
+ snapPoints?: ReadonlyArray<BottomSheetSnapPoint>;
36
45
  /**
37
46
  * Configures implicit dismissal behavior, matching Dialog.
38
47
  * - required: Blocks swipe, scrim click, and Escape
@@ -1 +1 @@
1
- {"version":3,"file":"BottomSheet.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/BottomSheet.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,WAAW,CAAC;AAc7C,YAAY,EAAC,iBAAiB,EAAC,MAAM,oBAAoB,CAAC;AAC1D,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,oBAAoB,CAAC;AA2D1D,UAAU,sBAAuB,SAAQ,SAAS,CAAC,cAAc,CAAC;IAChE,qDAAqD;IACrD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAEhC,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IAEd,0EAA0E;IAC1E,QAAQ,EAAE,SAAS,CAAC;IAEpB,wGAAwG;IACxG,MAAM,CAAC,EAAE,iBAAiB,GAAG,MAAM,GAAG,MAAM,CAAC;IAE7C;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,UAAU,0BAA2B,SAAQ,sBAAsB;IACjE,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,KAAK,CAAC;CACjB;AAED,UAAU,wBAAyB,SAAQ,sBAAsB;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,YAAY,CAAC,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,EAAE,KAAK,CAAC;CAClB;AAED,MAAM,MAAM,gBAAgB,GAC1B,0BAA0B,GAAG,wBAAwB,CAAC;AAwWxD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,+BA6BlD;yBA7Be,WAAW"}
1
+ {"version":3,"file":"BottomSheet.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/BottomSheet.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,WAAW,CAAC;AAc7C,YAAY,EAAC,iBAAiB,EAAE,oBAAoB,EAAC,MAAM,oBAAoB,CAAC;AAChF,OAAO,KAAK,EAAC,iBAAiB,EAAE,oBAAoB,EAAC,MAAM,oBAAoB,CAAC;AA2DhF,UAAU,sBAAuB,SAAQ,SAAS,CAAC,cAAc,CAAC;IAChE,qDAAqD;IACrD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAEhC,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IAEd,0EAA0E;IAC1E,QAAQ,EAAE,SAAS,CAAC;IAEpB,wGAAwG;IACxG,MAAM,CAAC,EAAE,iBAAiB,GAAG,MAAM,GAAG,MAAM,CAAC;IAE7C;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,aAAa,CAAC,oBAAoB,CAAC,CAAC;IAEjD;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,UAAU,0BAA2B,SAAQ,sBAAsB;IACjE,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,KAAK,CAAC;CACjB;AAED,UAAU,wBAAyB,SAAQ,sBAAsB;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,YAAY,CAAC,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,EAAE,KAAK,CAAC;CAClB;AAED,MAAM,MAAM,gBAAgB,GAC1B,0BAA0B,GAAG,wBAAwB,CAAC;AA4WxD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,+BA6BlD;yBA7Be,WAAW"}
@@ -89,6 +89,7 @@ function StandaloneBottomSheet({
89
89
  label,
90
90
  children,
91
91
  height = 'capped',
92
+ snapPoints,
92
93
  hasScrim = true,
93
94
  purpose = 'info',
94
95
  xstyle,
@@ -224,6 +225,7 @@ function StandaloneBottomSheet({
224
225
  ref: ref,
225
226
  state: panelState,
226
227
  height: height,
228
+ snapPoints: snapPoints,
227
229
  isSwipeDismissAllowed: purpose === 'info',
228
230
  isPageScrollLocked: shouldPresent && hasScrim,
229
231
  xstyle: xstyle,
@@ -243,6 +245,7 @@ function SwitcherBottomSheetItem({
243
245
  label,
244
246
  children,
245
247
  height = 'capped',
248
+ snapPoints,
246
249
  purpose = 'info',
247
250
  xstyle,
248
251
  ...props
@@ -353,6 +356,7 @@ function SwitcherBottomSheetItem({
353
356
  ref: ref,
354
357
  state: panelState,
355
358
  height: height,
359
+ snapPoints: snapPoints,
356
360
  isSwipeDismissAllowed: purpose === 'info',
357
361
  isPageScrollLocked: hasScrim,
358
362
  xstyle: xstyle,
@@ -12,17 +12,20 @@
12
12
  * SYNC: When modified, update these files to stay in sync:
13
13
  * - /packages/core/src/BottomSheet/BottomSheet.tsx
14
14
  * - /packages/core/src/BottomSheet/BottomSheetPanel.test.tsx
15
+ * - /packages/core/src/BottomSheet/snapOffsets.ts
15
16
  * - /packages/core/src/BottomSheet/useMobileKeyboard.ts
16
17
  * - /packages/core/src/BottomSheet/useSheetGestures.ts
17
18
  */
18
19
  import { type ReactNode } from 'react';
19
20
  import type { BaseProps } from '../BaseProps';
21
+ import { type BottomSheetSnapPoint } from './snapOffsets';
20
22
  declare const HEIGHT_BUDGETS: {
21
23
  readonly hug: "92dvh";
22
24
  readonly capped: "62dvh";
23
25
  readonly tall: "92dvh";
24
26
  };
25
27
  export type BottomSheetHeight = keyof typeof HEIGHT_BUDGETS;
28
+ export type { BottomSheetSnapPoint };
26
29
  export type BottomSheetPanelMotion = 'entering' | 'aligning' | 'fading' | 'exiting';
27
30
  export type BottomSheetPanelState = {
28
31
  kind: 'hidden';
@@ -42,6 +45,7 @@ interface BottomSheetPanelProps extends BaseProps<HTMLDivElement> {
42
45
  state: BottomSheetPanelState;
43
46
  height: BottomSheetHeight | number | string;
44
47
  children: ReactNode;
48
+ snapPoints?: ReadonlyArray<BottomSheetSnapPoint>;
45
49
  isSwipeDismissAllowed?: boolean;
46
50
  /** Whether the host has locked page scrolling (a modal, scrim-backed sheet). */
47
51
  isPageScrollLocked?: boolean;
@@ -52,9 +56,8 @@ interface BottomSheetPanelProps extends BaseProps<HTMLDivElement> {
52
56
  onMotionComplete?: (motion: BottomSheetPanelMotion) => void;
53
57
  }
54
58
  /** Internal visual and gesture surface shared by every BottomSheet host. */
55
- export declare function BottomSheetPanel({ ref, state, height, children, className, style, tabIndex, xstyle, isSwipeDismissAllowed, isPageScrollLocked, onDismiss, onScrimOpacity, onElementChange, onMotionStart, onMotionComplete, ...props }: BottomSheetPanelProps): import("react").JSX.Element;
59
+ export declare function BottomSheetPanel({ ref, state, height, children, snapPoints, className, style, tabIndex, xstyle, isSwipeDismissAllowed, isPageScrollLocked, onDismiss, onScrimOpacity, onElementChange, onMotionStart, onMotionComplete, ...props }: BottomSheetPanelProps): import("react").JSX.Element;
56
60
  export declare namespace BottomSheetPanel {
57
61
  var displayName: string;
58
62
  }
59
- export {};
60
63
  //# sourceMappingURL=BottomSheetPanel.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"BottomSheetPanel.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/BottomSheetPanel.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAgB5C,QAAA,MAAM,cAAc;;;;CAIV,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,cAAc,CAAC;AAqG5D,MAAM,MAAM,sBAAsB,GAChC,UAAU,GAAG,UAAU,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEjD,MAAM,MAAM,qBAAqB,GAC7B;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAC,GAChB;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAC,GACjC;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,QAAQ,CAAC;IAC1C,eAAe,EAAE,MAAM,CAAC;CACzB,GACD;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,CAAC;AAEtB,UAAU,qBAAsB,SAAQ,SAAS,CAAC,cAAc,CAAC;IAC/D,+CAA+C;IAC/C,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC,KAAK,EAAE,qBAAqB,CAAC;IAC7B,MAAM,EAAE,iBAAiB,GAAG,MAAM,GAAG,MAAM,CAAC;IAC5C,QAAQ,EAAE,SAAS,CAAC;IACpB,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,gFAAgF;IAChF,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,KAAK,IAAI,CAAC;IAC3D,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,IAAI,CAAC;IACzD,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,IAAI,CAAC;CAC7D;AA+GD,4EAA4E;AAC5E,wBAAgB,gBAAgB,CAAC,EAC/B,GAAG,EACH,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,EACT,KAAK,EACL,QAAQ,EACR,MAAM,EACN,qBAA4B,EAC5B,kBAA0B,EAC1B,SAAS,EACT,cAAc,EACd,eAAe,EACf,aAAa,EACb,gBAAgB,EAChB,GAAG,KAAK,EACT,EAAE,qBAAqB,+BA0OvB;yBA3Pe,gBAAgB"}
1
+ {"version":3,"file":"BottomSheetPanel.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/BottomSheetPanel.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAY5C,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,eAAe,CAAC;AAIvB,QAAA,MAAM,cAAc;;;;CAIV,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,cAAc,CAAC;AAC5D,YAAY,EAAC,oBAAoB,EAAC,CAAC;AAoHnC,MAAM,MAAM,sBAAsB,GAChC,UAAU,GAAG,UAAU,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEjD,MAAM,MAAM,qBAAqB,GAC7B;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAC,GAChB;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAC,GACjC;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,QAAQ,CAAC;IAC1C,eAAe,EAAE,MAAM,CAAC;CACzB,GACD;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,CAAC;AAEtB,UAAU,qBAAsB,SAAQ,SAAS,CAAC,cAAc,CAAC;IAC/D,+CAA+C;IAC/C,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC,KAAK,EAAE,qBAAqB,CAAC;IAC7B,MAAM,EAAE,iBAAiB,GAAG,MAAM,GAAG,MAAM,CAAC;IAC5C,QAAQ,EAAE,SAAS,CAAC;IACpB,UAAU,CAAC,EAAE,aAAa,CAAC,oBAAoB,CAAC,CAAC;IACjD,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,gFAAgF;IAChF,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,KAAK,IAAI,CAAC;IAC3D,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,IAAI,CAAC;IACzD,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,IAAI,CAAC;CAC7D;AA+GD,4EAA4E;AAC5E,wBAAgB,gBAAgB,CAAC,EAC/B,GAAG,EACH,KAAK,EACL,MAAM,EACN,QAAQ,EACR,UAAU,EACV,SAAS,EACT,KAAK,EACL,QAAQ,EACR,MAAM,EACN,qBAA4B,EAC5B,kBAA0B,EAC1B,SAAS,EACT,cAAc,EACd,eAAe,EACf,aAAa,EACb,gBAAgB,EAChB,GAAG,KAAK,EACT,EAAE,qBAAqB,+BAwQvB;yBA1Re,gBAAgB"}
@@ -16,18 +16,20 @@
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
  */
22
- import { useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef } from 'react';
23
+ import { useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef } from 'react';
23
24
  import * as stylex from '@stylexjs/stylex';
25
+ import { useDevWarning } from "../hooks/index.js";
24
26
  import "../theme/tokens.stylex.js";
25
27
  import { colorVars, durationVars, easeVars, radiusVars, shadowVars, sizeVars, spacingVars } from "../theme/tokens.stylex.js";
26
28
  import { mergeProps, themeProps } from "../utils/index.js";
29
+ import { isValidSnapPoint, resolveSnapPoints } from "./snapOffsets.js";
27
30
  import { useMobileKeyboard } from "./useMobileKeyboard.js";
28
31
  import { useSheetGestures } from "./useSheetGestures.js";
29
32
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
30
- const SNAP_FRACTIONS = [0.14, 0.5, 0.92];
31
33
  const HEIGHT_BUDGETS = {
32
34
  hug: '92dvh',
33
35
  capped: '62dvh',
@@ -37,17 +39,31 @@ const HEIGHT_BUDGETS = {
37
39
  const OVERSCROLL_PADDING = 48;
38
40
  const MOBILE_KEYBOARD_BOTTOM_CLEARANCE = 48;
39
41
  const TRANSITION_BACKSTOP_BUFFER_MS = 50;
40
- function defaultSnapHeights() {
41
- if (typeof window === 'undefined') {
42
- return [];
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}|`;
43
65
  }
44
- // Measure the same viewport the height budgets are written against. Those
45
- // are `dvh`, which the virtual keyboard does not shrink, so reading
46
- // `visualViewport` here made the two disagree by exactly the keyboard's
47
- // height: every detent moved while the sheet it was measuring did not. A
48
- // keyboard is `useMobileKeyboard`'s business — it holds the sheet still and
49
- // scrolls the body — and it does not redefine the sheet's detents.
50
- return SNAP_FRACTIONS.map(fraction => fraction * window.innerHeight);
66
+ return key;
51
67
  }
52
68
  const styles = {
53
69
  sheet: {
@@ -190,6 +206,7 @@ export function BottomSheetPanel({
190
206
  state,
191
207
  height,
192
208
  children,
209
+ snapPoints,
193
210
  className,
194
211
  style,
195
212
  tabIndex,
@@ -230,6 +247,32 @@ export function BottomSheetPanel({
230
247
  const isClosing = state.kind === 'exiting';
231
248
  const isFading = isRetained && state.motion === 'fading';
232
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 !== '');
233
276
  const {
234
277
  contentProps,
235
278
  handleProps,
@@ -249,7 +292,7 @@ export function BottomSheetPanel({
249
292
  canDismiss: isSwipeDismissAllowed,
250
293
  offscreenBlockEndInset: OVERSCROLL_PADDING,
251
294
  onDismiss,
252
- snapHeights: defaultSnapHeights,
295
+ snapHeights,
253
296
  onScrimOpacity
254
297
  });
255
298
  const setElement = useCallback(element => {
@@ -5,7 +5,7 @@
5
5
  * @position Component entry point; re-exported by /packages/core/src/index.ts
6
6
  */
7
7
  export { BottomSheet } from './BottomSheet';
8
- export type { BottomSheetHeight, BottomSheetProps } from './BottomSheet';
8
+ export type { BottomSheetHeight, BottomSheetProps, BottomSheetSnapPoint, } from './BottomSheet';
9
9
  export { BottomSheetSwitcher } from './BottomSheetSwitcher';
10
10
  export type { BottomSheetSwitcherProps } from './BottomSheetSwitcher';
11
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/index.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AAEH,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAC1C,YAAY,EAAC,iBAAiB,EAAE,gBAAgB,EAAC,MAAM,eAAe,CAAC;AACvE,OAAO,EAAC,mBAAmB,EAAC,MAAM,uBAAuB,CAAC;AAC1D,YAAY,EAAC,wBAAwB,EAAC,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/index.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AAEH,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAC1C,YAAY,EACV,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAC,mBAAmB,EAAC,MAAM,uBAAuB,CAAC;AAC1D,YAAY,EAAC,wBAAwB,EAAC,MAAM,uBAAuB,CAAC"}
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * @file snapOffsets.ts
3
3
  * @input Pure geometry helpers — no React, no DOM.
4
- * @output Converts snap points to translateY offsets for the sheet surface.
5
- * @position Internal to BottomSheet; consumed by useSheetGestures, tested by
6
- * snapOffsets.test.ts.
4
+ * @output Resolves snap points and converts them to translateY offsets.
5
+ * @position Internal to BottomSheet; consumed by useSheetGestures and
6
+ * BottomSheetPanel, tested by snapOffsets.test.ts.
7
7
  *
8
8
  * A detent is represented as an offset in px from the fully-open position
9
9
  * (0 = fully open, larger = more collapsed). The panel may render that offset
@@ -11,6 +11,20 @@
11
11
  * helpers turn snap points into the shared offset list; they are pure so the
12
12
  * geometry can be unit-tested without a DOM.
13
13
  */
14
+ /**
15
+ * A height the sheet can rest at, expressed as the sheet's VISIBLE height:
16
+ *
17
+ * - a number in `(0, 1]` — a fraction of the viewport (`0.5` is half the
18
+ * screen), so the stop tracks rotation and window resizes;
19
+ * - a `'<n>%'` string — the same thing spelled in CSS;
20
+ * - a `'<n>px'` string — an absolute height that does not scale.
21
+ *
22
+ * Anything else — a unitless string, `calc()`, `vh`, `rem`, a number outside
23
+ * `(0, 1]` — is ignored with a dev warning. Percentages resolve against the
24
+ * layout viewport (`window.innerHeight`), the same box the height budgets are
25
+ * written against, so the mobile keyboard never moves a stop.
26
+ */
27
+ export type BottomSheetSnapPoint = number | string;
14
28
  /**
15
29
  * Detents whose resting offsets land within this many px of each other are
16
30
  * treated as the same stop (the taller one wins). Stops the sheet from having
@@ -18,8 +32,24 @@
18
32
  * a hair away from a fractional snap point.
19
33
  */
20
34
  export declare const DETENT_DEDUP_PX = 48;
21
- /** Resolve snap points given as viewport fractions (0, 1] to px heights. */
22
- export declare function snapFractionsToHeights(fractions: ReadonlyArray<number>, viewportPx: number): number[];
35
+ /**
36
+ * Largest share of the fully open sheet the shortest stop may fill and still
37
+ * be a peek. Above it the stop is a working surface — it lays its content out
38
+ * and keeps a full scrim — so a two-stop sheet like `[0.5]` behaves like the
39
+ * half-height panel it asks for rather than like a glance.
40
+ */
41
+ export declare const PEEK_MAX_HEIGHT_RATIO = 0.25;
42
+ /**
43
+ * Whether a snap point can be resolved at all. The viewport only scales the
44
+ * result, so validity is independent of it.
45
+ */
46
+ export declare function isValidSnapPoint(point: BottomSheetSnapPoint): boolean;
47
+ /**
48
+ * Resolve snap points to candidate visible heights in px, dropping the ones
49
+ * this sheet cannot honor. Callers warn about those separately — this stays
50
+ * silent because it runs on every drag frame.
51
+ */
52
+ export declare function resolveSnapPoints(points: ReadonlyArray<BottomSheetSnapPoint>, viewportPx: number): number[];
23
53
  /**
24
54
  * Given the sheet's full height (px, as rendered fully open) and a set of
25
55
  * candidate detent *visible heights* (px), return the resting offsets from
@@ -42,26 +72,33 @@ export declare function nearestOffset(value: number, offsets: ReadonlyArray<numb
42
72
  */
43
73
  export declare const MIN_PEEK_SCRIM_OPACITY = 0.3;
44
74
  /**
45
- * Whether `offset` is the peek detent: the shortest stop, where the sheet is a
46
- * glance rather than a working surface. A sheet with no collapsed stop at all
47
- * has no peek — the same rule `scrimOpacityForOffset` thins the scrim by.
75
+ * The peek detent's offset, or `null` when this sheet has no peek.
48
76
  *
49
- * The peek is the one detent the sheet does NOT express as layout height: at a
50
- * sliver of viewport there is nothing useful to lay out, and reflowing the
51
- * content into it (then back out on the way up) is churn the user sees. It
52
- * keeps the full layout height and slides below the viewport instead.
77
+ * A peek is the shortest stop AND a sliver at most `PEEK_MAX_HEIGHT_RATIO`
78
+ * of the fully open sheet. It is the one detent the sheet does NOT express as
79
+ * layout height: at a sliver there is nothing useful to lay out, and reflowing
80
+ * the content into it (then back out on the way up) is churn the user sees, so
81
+ * it keeps the full layout height and slides below the viewport instead. It is
82
+ * also the only stop that thins the scrim.
83
+ *
84
+ * A sheet whose shortest stop is a working height — `[0.5]`, say — has no
85
+ * peek: half a screen of content deserves to be laid out at half a screen, and
86
+ * a scrim that thinned there would read as a dismissed sheet.
53
87
  */
54
- export declare function isPeekOffset(offset: number, offsets: ReadonlyArray<number>, tolerancePx?: number): boolean;
88
+ export declare function peekOffsetFor(offsets: ReadonlyArray<number>, visibleSheetHeight: number): number | null;
55
89
  /**
56
90
  * Scrim opacity (1 = fully visible) for a drag/settle `offset`.
57
- * The scrim stays full while the sheet is at or above its second-shortest
58
- * detent, then fades as it collapses onto the shortest ("peek") detent a
59
- * glance state that thins the backdrop to `MIN_PEEK_SCRIM_OPACITY` (not fully
60
- * gone, since the sheet is still modal) and holds there below it.
61
- * A single-detent sheet has no peek, so it instead fades all the way to 0
62
- * across the dismiss overshoot toward `dismissOffset` (the sheet is leaving).
91
+ *
92
+ * The scrim is full down to the shortest stop that is still a working surface,
93
+ * and only thins past it onto a peek, where it holds at
94
+ * `MIN_PEEK_SCRIM_OPACITY` (a glance state, and the sheet is still modal), or
95
+ * out through the dismiss overshoot toward `dismissOffset`, where it clears
96
+ * completely because the sheet is leaving.
97
+ *
98
+ * So a sheet whose stops are all working heights keeps a full scrim at every
99
+ * one of them; only the sliver of a peek, or a sheet on its way out, dims less.
63
100
  */
64
- export declare function scrimOpacityForOffset(offset: number, offsets: ReadonlyArray<number>, dismissOffset: number): number;
101
+ export declare function scrimOpacityForOffset(offset: number, offsets: ReadonlyArray<number>, dismissOffset: number, peekOffset: number | null): number;
65
102
  /**
66
103
  * Settle target for a released drag. Restricts candidates to the drag
67
104
  * direction so a committed drag never snaps *back past* where it started
@@ -1 +1 @@
1
- {"version":3,"file":"snapOffsets.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/snapOffsets.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC,4EAA4E;AAC5E,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,aAAa,CAAC,MAAM,CAAC,EAChC,UAAU,EAAE,MAAM,GACjB,MAAM,EAAE,CAEV;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,EACpC,OAAO,GAAE,MAAwB,GAChC,MAAM,EAAE,CAcV;AAED,yEAAyE;AACzE,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,GAC7B,MAAM,CAKR;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,EAC9B,WAAW,GAAE,MAAY,GACxB,OAAO,CAKT;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,EAC9B,aAAa,EAAE,MAAM,GACpB,MAAM,CAeR;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,EAC9B,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,GACjB,MAAM,CAcR"}
1
+ {"version":3,"file":"snapOffsets.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/snapOffsets.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AAEH;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,MAAM,CAAC;AAEnD;;;;;GAKG;AACH,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAkC1C;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAErE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,aAAa,CAAC,oBAAoB,CAAC,EAC3C,UAAU,EAAE,MAAM,GACjB,MAAM,EAAE,CASV;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,EACpC,OAAO,GAAE,MAAwB,GAChC,MAAM,EAAE,CAcV;AAED,yEAAyE;AACzE,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,GAC7B,MAAM,CAKR;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,EAC9B,kBAAkB,EAAE,MAAM,GACzB,MAAM,GAAG,IAAI,CASf;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,EAC9B,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,GAAG,IAAI,GACxB,MAAM,CAgBR;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,EAC9B,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,GACjB,MAAM,CAcR"}
@@ -3,9 +3,9 @@
3
3
  /**
4
4
  * @file snapOffsets.ts
5
5
  * @input Pure geometry helpers — no React, no DOM.
6
- * @output Converts snap points to translateY offsets for the sheet surface.
7
- * @position Internal to BottomSheet; consumed by useSheetGestures, tested by
8
- * snapOffsets.test.ts.
6
+ * @output Resolves snap points and converts them to translateY offsets.
7
+ * @position Internal to BottomSheet; consumed by useSheetGestures and
8
+ * BottomSheetPanel, tested by snapOffsets.test.ts.
9
9
  *
10
10
  * A detent is represented as an offset in px from the fully-open position
11
11
  * (0 = fully open, larger = more collapsed). The panel may render that offset
@@ -14,6 +14,20 @@
14
14
  * geometry can be unit-tested without a DOM.
15
15
  */
16
16
 
17
+ /**
18
+ * A height the sheet can rest at, expressed as the sheet's VISIBLE height:
19
+ *
20
+ * - a number in `(0, 1]` — a fraction of the viewport (`0.5` is half the
21
+ * screen), so the stop tracks rotation and window resizes;
22
+ * - a `'<n>%'` string — the same thing spelled in CSS;
23
+ * - a `'<n>px'` string — an absolute height that does not scale.
24
+ *
25
+ * Anything else — a unitless string, `calc()`, `vh`, `rem`, a number outside
26
+ * `(0, 1]` — is ignored with a dev warning. Percentages resolve against the
27
+ * layout viewport (`window.innerHeight`), the same box the height budgets are
28
+ * written against, so the mobile keyboard never moves a stop.
29
+ */
30
+
17
31
  /**
18
32
  * Detents whose resting offsets land within this many px of each other are
19
33
  * treated as the same stop (the taller one wins). Stops the sheet from having
@@ -22,9 +36,63 @@
22
36
  */
23
37
  export const DETENT_DEDUP_PX = 48;
24
38
 
25
- /** Resolve snap points given as viewport fractions (0, 1] to px heights. */
26
- export function snapFractionsToHeights(fractions, viewportPx) {
27
- return fractions.filter(f => f > 0 && f <= 1).map(f => f * viewportPx);
39
+ /**
40
+ * Largest share of the fully open sheet the shortest stop may fill and still
41
+ * be a peek. Above it the stop is a working surface it lays its content out
42
+ * and keeps a full scrim — so a two-stop sheet like `[0.5]` behaves like the
43
+ * half-height panel it asks for rather than like a glance.
44
+ */
45
+ export const PEEK_MAX_HEIGHT_RATIO = 0.25;
46
+
47
+ // A snap point string: a positive number with a `px` or `%` unit. Deliberately
48
+ // narrow — every accepted unit has to be resolvable by arithmetic on the
49
+ // viewport height, because this runs inside the drag loop. Units that need the
50
+ // DOM to resolve (`rem`, `em`, `calc()`) would cost a layout per frame.
51
+ const SNAP_POINT_PATTERN = /^(\d+(?:\.\d+)?|\.\d+)(px|%)$/i;
52
+
53
+ /**
54
+ * Resolve one snap point to a visible height in px, or `null` when it is not a
55
+ * snap point this sheet can honor.
56
+ */
57
+ function parseSnapPoint(point, viewportPx) {
58
+ if (typeof point === 'number') {
59
+ // A bare number is a viewport fraction. Anything above 1 is a px value in
60
+ // disguise; the caller warns rather than guessing which was meant.
61
+ return Number.isFinite(point) && point > 0 && point <= 1 ? point * viewportPx : null;
62
+ }
63
+ const match = SNAP_POINT_PATTERN.exec(point.trim());
64
+ if (match === null) {
65
+ return null;
66
+ }
67
+ const value = Number.parseFloat(match[1]);
68
+ if (!(value > 0)) {
69
+ return null;
70
+ }
71
+ return match[2].toLowerCase() === '%' ? value / 100 * viewportPx : value;
72
+ }
73
+
74
+ /**
75
+ * Whether a snap point can be resolved at all. The viewport only scales the
76
+ * result, so validity is independent of it.
77
+ */
78
+ export function isValidSnapPoint(point) {
79
+ return parseSnapPoint(point, 1) !== null;
80
+ }
81
+
82
+ /**
83
+ * Resolve snap points to candidate visible heights in px, dropping the ones
84
+ * this sheet cannot honor. Callers warn about those separately — this stays
85
+ * silent because it runs on every drag frame.
86
+ */
87
+ export function resolveSnapPoints(points, viewportPx) {
88
+ const heights = [];
89
+ for (const point of points) {
90
+ const height = parseSnapPoint(point, viewportPx);
91
+ if (height !== null) {
92
+ heights.push(height);
93
+ }
94
+ }
95
+ return heights;
28
96
  }
29
97
 
30
98
  /**
@@ -65,38 +133,46 @@ export function nearestOffset(value, offsets) {
65
133
  export const MIN_PEEK_SCRIM_OPACITY = 0.3;
66
134
 
67
135
  /**
68
- * Whether `offset` is the peek detent: the shortest stop, where the sheet is a
69
- * glance rather than a working surface. A sheet with no collapsed stop at all
70
- * has no peek — the same rule `scrimOpacityForOffset` thins the scrim by.
136
+ * The peek detent's offset, or `null` when this sheet has no peek.
71
137
  *
72
- * The peek is the one detent the sheet does NOT express as layout height: at a
73
- * sliver of viewport there is nothing useful to lay out, and reflowing the
74
- * content into it (then back out on the way up) is churn the user sees. It
75
- * keeps the full layout height and slides below the viewport instead.
138
+ * A peek is the shortest stop AND a sliver at most `PEEK_MAX_HEIGHT_RATIO`
139
+ * of the fully open sheet. It is the one detent the sheet does NOT express as
140
+ * layout height: at a sliver there is nothing useful to lay out, and reflowing
141
+ * the content into it (then back out on the way up) is churn the user sees, so
142
+ * it keeps the full layout height and slides below the viewport instead. It is
143
+ * also the only stop that thins the scrim.
144
+ *
145
+ * A sheet whose shortest stop is a working height — `[0.5]`, say — has no
146
+ * peek: half a screen of content deserves to be laid out at half a screen, and
147
+ * a scrim that thinned there would read as a dismissed sheet.
76
148
  */
77
- export function isPeekOffset(offset, offsets, tolerancePx = 0.5) {
78
- if (offsets.length < 2) {
79
- return false;
149
+ export function peekOffsetFor(offsets, visibleSheetHeight) {
150
+ if (offsets.length < 2 || visibleSheetHeight <= 0) {
151
+ return null;
80
152
  }
81
- return Math.abs(offset - offsets[offsets.length - 1]) <= tolerancePx;
153
+ const shortest = offsets[offsets.length - 1];
154
+ const heightAtShortest = visibleSheetHeight - shortest;
155
+ return heightAtShortest <= PEEK_MAX_HEIGHT_RATIO * visibleSheetHeight ? shortest : null;
82
156
  }
83
157
 
84
158
  /**
85
159
  * Scrim opacity (1 = fully visible) for a drag/settle `offset`.
86
- * The scrim stays full while the sheet is at or above its second-shortest
87
- * detent, then fades as it collapses onto the shortest ("peek") detent a
88
- * glance state that thins the backdrop to `MIN_PEEK_SCRIM_OPACITY` (not fully
89
- * gone, since the sheet is still modal) and holds there below it.
90
- * A single-detent sheet has no peek, so it instead fades all the way to 0
91
- * across the dismiss overshoot toward `dismissOffset` (the sheet is leaving).
160
+ *
161
+ * The scrim is full down to the shortest stop that is still a working surface,
162
+ * and only thins past it onto a peek, where it holds at
163
+ * `MIN_PEEK_SCRIM_OPACITY` (a glance state, and the sheet is still modal), or
164
+ * out through the dismiss overshoot toward `dismissOffset`, where it clears
165
+ * completely because the sheet is leaving.
166
+ *
167
+ * So a sheet whose stops are all working heights keeps a full scrim at every
168
+ * one of them; only the sliver of a peek, or a sheet on its way out, dims less.
92
169
  */
93
- export function scrimOpacityForOffset(offset, offsets, dismissOffset) {
94
- const shortest = offsets[offsets.length - 1];
95
- const hasPeek = offsets.length >= 2;
96
- const fadeStart = hasPeek ? offsets[offsets.length - 2] : 0;
97
- const fadeEnd = hasPeek ? shortest : dismissOffset;
98
- // Peek is a resting state on a still-modal sheet, so keep a floor; the
99
- // dismiss overshoot is a sheet on its way out, so let it clear completely.
170
+ export function scrimOpacityForOffset(offset, offsets, dismissOffset, peekOffset) {
171
+ const hasPeek = peekOffset !== null;
172
+ // The last stop that is still a working surface — the peek's neighbor when
173
+ // there is a peek, otherwise the shortest stop itself.
174
+ const fadeStart = hasPeek ? offsets[offsets.length - 2] : offsets[offsets.length - 1];
175
+ const fadeEnd = hasPeek ? peekOffset : dismissOffset;
100
176
  const floor = hasPeek ? MIN_PEEK_SCRIM_OPACITY : 0;
101
177
  if (offset <= fadeStart) {
102
178
  return 1;