@astryxdesign/core 0.7.8-canary.ffd29c5 → 0.7.8

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 (189) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/dist/BottomSheet/BottomSheetPanel.d.ts +1 -1
  3. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheetPanel.js +30 -14
  5. package/dist/BottomSheet/bottomSheetStack.js +1 -1
  6. package/dist/BottomSheet/useSheetGestures.d.ts +3 -1
  7. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  8. package/dist/BottomSheet/useSheetGestures.js +4 -0
  9. package/dist/CheckboxList/CheckboxList.d.ts +13 -2
  10. package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
  11. package/dist/CheckboxList/CheckboxList.js +10 -4
  12. package/dist/CheckboxList/CheckboxListContext.d.ts +7 -0
  13. package/dist/CheckboxList/CheckboxListContext.d.ts.map +1 -1
  14. package/dist/CheckboxList/CheckboxListContext.js +2 -1
  15. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  16. package/dist/CheckboxList/CheckboxListItem.js +14 -2
  17. package/dist/CheckboxList/requiredGroup.d.ts +22 -0
  18. package/dist/CheckboxList/requiredGroup.d.ts.map +1 -0
  19. package/dist/CheckboxList/requiredGroup.js +52 -0
  20. package/dist/DateInput/PickerWheel.d.ts +1 -1
  21. package/dist/DateInput/PickerWheel.d.ts.map +1 -1
  22. package/dist/DateInput/PickerWheel.js +27 -6
  23. package/dist/DateInput/Wheel.d.ts +2 -2
  24. package/dist/DateInput/Wheel.d.ts.map +1 -1
  25. package/dist/DateInput/Wheel.js +20 -6
  26. package/dist/DateInput/datePickerTouch.d.ts +13 -0
  27. package/dist/DateInput/datePickerTouch.d.ts.map +1 -0
  28. package/dist/DateInput/datePickerTouch.js +14 -0
  29. package/dist/DateInput/wheelOptions.d.ts +22 -2
  30. package/dist/DateInput/wheelOptions.d.ts.map +1 -1
  31. package/dist/DateInput/wheelOptions.js +14 -2
  32. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  33. package/dist/DateTimeInput/TouchDateTimeField.js +62 -119
  34. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  35. package/dist/HoverCard/useHoverCard.js +6 -1
  36. package/dist/Overlay/Overlay.d.ts.map +1 -1
  37. package/dist/Overlay/Overlay.js +7 -2
  38. package/dist/PowerSearch/PowerSearch.d.ts +4 -2
  39. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  40. package/dist/PowerSearch/PowerSearchBottomSheet.d.ts +24 -1
  41. package/dist/PowerSearch/PowerSearchBottomSheet.d.ts.map +1 -1
  42. package/dist/PowerSearch/PowerSearchBottomSheet.js +172 -50
  43. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  44. package/dist/Resizable/ResizeHandle.js +15 -15
  45. package/dist/Resizable/createResizable.d.ts +70 -16
  46. package/dist/Resizable/createResizable.d.ts.map +1 -1
  47. package/dist/Resizable/createResizable.js +180 -22
  48. package/dist/Resizable/separatorAttributes.d.ts +45 -0
  49. package/dist/Resizable/separatorAttributes.d.ts.map +1 -0
  50. package/dist/Resizable/separatorAttributes.js +35 -0
  51. package/dist/Resizable/useResizable.js +2 -2
  52. package/dist/TimeInput/TimeWheels.d.ts +32 -0
  53. package/dist/TimeInput/TimeWheels.d.ts.map +1 -0
  54. package/dist/TimeInput/TimeWheels.js +144 -0
  55. package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
  56. package/dist/TimeInput/TouchTimeField.js +63 -118
  57. package/dist/TimeInput/timeWheelRules.d.ts +124 -0
  58. package/dist/TimeInput/timeWheelRules.d.ts.map +1 -0
  59. package/dist/TimeInput/timeWheelRules.js +277 -0
  60. package/dist/Tokenizer/TokenizerBottomSheet.js +8 -12
  61. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  62. package/dist/Tooltip/useTooltip.js +6 -0
  63. package/dist/astryx.css +3 -1
  64. package/dist/gestures/GestureHost.d.ts.map +1 -1
  65. package/dist/gestures/GestureHost.js +3 -0
  66. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts.map +1 -1
  67. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.js +5 -2
  68. package/dist/i18n/generated-english/Resizable/createResizable.generated.d.ts +8 -0
  69. package/dist/i18n/generated-english/Resizable/createResizable.generated.d.ts.map +1 -0
  70. package/dist/i18n/generated-english/Resizable/createResizable.generated.js +13 -0
  71. package/dist/i18n/generated-english/TimeInput/TimeWheels.generated.d.ts +8 -0
  72. package/dist/i18n/generated-english/TimeInput/TimeWheels.generated.d.ts.map +1 -0
  73. package/dist/i18n/generated-english/TimeInput/TimeWheels.generated.js +17 -0
  74. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  75. package/dist/i18n/generated-locales/en.generated.js +5 -2
  76. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  77. package/dist/i18n/generated-locales/pseudo.generated.js +5 -2
  78. package/dist/keyboard/index.d.ts +17 -0
  79. package/dist/keyboard/index.d.ts.map +1 -0
  80. package/dist/keyboard/index.js +19 -0
  81. package/dist/keyboard/keyboardHeight.d.ts +149 -0
  82. package/dist/keyboard/keyboardHeight.d.ts.map +1 -0
  83. package/dist/keyboard/keyboardHeight.js +432 -0
  84. package/dist/keyboard/keyboardLayoutGuide.d.ts +59 -0
  85. package/dist/keyboard/keyboardLayoutGuide.d.ts.map +1 -0
  86. package/dist/keyboard/keyboardLayoutGuide.js +207 -0
  87. package/dist/keyboard/keyboardModel.d.ts +93 -0
  88. package/dist/keyboard/keyboardModel.d.ts.map +1 -0
  89. package/dist/keyboard/keyboardModel.js +220 -0
  90. package/dist/keyboard/rideKeyboard.d.ts +42 -0
  91. package/dist/keyboard/rideKeyboard.d.ts.map +1 -0
  92. package/dist/keyboard/rideKeyboard.js +77 -0
  93. package/dist/keyboard/useMobileKeyboard.d.ts +61 -0
  94. package/dist/keyboard/useMobileKeyboard.d.ts.map +1 -0
  95. package/dist/keyboard/useMobileKeyboard.js +699 -0
  96. package/dist/motion/android/index.d.ts +16 -0
  97. package/dist/motion/android/index.d.ts.map +1 -0
  98. package/dist/motion/android/index.js +18 -0
  99. package/dist/motion/android/keyboard.d.ts +83 -0
  100. package/dist/motion/android/keyboard.d.ts.map +1 -0
  101. package/dist/motion/android/keyboard.js +96 -0
  102. package/dist/motion/uikit/datePicker.d.ts +58 -0
  103. package/dist/motion/uikit/datePicker.d.ts.map +1 -0
  104. package/dist/motion/uikit/datePicker.js +60 -0
  105. package/dist/motion/uikit/index.d.ts +4 -3
  106. package/dist/motion/uikit/index.d.ts.map +1 -1
  107. package/dist/motion/uikit/index.js +4 -3
  108. package/dist/utils/interactionOverlay.stylex.d.ts +3 -2
  109. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  110. package/dist/utils/interactionOverlay.stylex.js +3 -2
  111. package/dist/utils/layoutViewport.d.ts +25 -0
  112. package/dist/utils/layoutViewport.d.ts.map +1 -0
  113. package/dist/utils/layoutViewport.js +102 -0
  114. package/dist/utils/pressFeedback.d.ts.map +1 -1
  115. package/dist/utils/pressFeedback.js +74 -9
  116. package/dist/utils/pressGesture.d.ts +4 -2
  117. package/dist/utils/pressGesture.d.ts.map +1 -1
  118. package/dist/utils/pressGesture.js +3 -1
  119. package/dist/utils/selectionLock.d.ts +10 -1
  120. package/dist/utils/selectionLock.d.ts.map +1 -1
  121. package/dist/utils/selectionLock.js +31 -5
  122. package/dist/utils/sharedMutationObserver.d.ts +2 -1
  123. package/dist/utils/sharedMutationObserver.d.ts.map +1 -1
  124. package/dist/utils/sharedMutationObserver.js +2 -1
  125. package/dist/utils/timeParser.d.ts +2 -1
  126. package/dist/utils/timeParser.d.ts.map +1 -1
  127. package/dist/utils/timeParser.js +29 -3
  128. package/locales/en.json +21 -9
  129. package/locales/pseudo.json +15 -6
  130. package/package.json +5 -5
  131. package/src/BottomSheet/BottomSheet.doc.mjs +1 -1
  132. package/src/BottomSheet/BottomSheetPanel.tsx +41 -13
  133. package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +5 -0
  134. package/src/BottomSheet/bottomSheetStack.ts +1 -1
  135. package/src/BottomSheet/useSheetGestures.ts +6 -0
  136. package/src/CheckboxList/CheckboxList.doc.mjs +7 -0
  137. package/src/CheckboxList/CheckboxList.tsx +34 -2
  138. package/src/CheckboxList/CheckboxListContext.tsx +9 -1
  139. package/src/CheckboxList/CheckboxListItem.tsx +23 -2
  140. package/src/CheckboxList/requiredGroup.ts +77 -0
  141. package/src/DateInput/PickerWheel.tsx +37 -4
  142. package/src/DateInput/Wheel.tsx +30 -3
  143. package/src/DateInput/datePickerTouch.ts +15 -0
  144. package/src/DateInput/wheelOptions.ts +31 -2
  145. package/src/DateTimeInput/TouchDateTimeField.tsx +94 -178
  146. package/src/HoverCard/useHoverCard.tsx +6 -1
  147. package/src/Overlay/Overlay.tsx +7 -2
  148. package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
  149. package/src/PowerSearch/PowerSearch.tsx +4 -2
  150. package/src/PowerSearch/PowerSearchBottomSheet.tsx +237 -62
  151. package/src/Resizable/ResizeHandle.tsx +18 -26
  152. package/src/Resizable/createResizable.ts +242 -27
  153. package/src/Resizable/separatorAttributes.ts +73 -0
  154. package/src/Resizable/useResizable.ts +2 -2
  155. package/src/TimeInput/TimeWheels.tsx +218 -0
  156. package/src/TimeInput/TouchTimeField.tsx +93 -157
  157. package/src/TimeInput/timeWheelRules.ts +352 -0
  158. package/src/Tokenizer/TokenizerBottomSheet.tsx +8 -12
  159. package/src/Tooltip/useTooltip.tsx +6 -0
  160. package/src/gestures/GestureHost.ts +5 -4
  161. package/src/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.ts +5 -2
  162. package/src/i18n/generated-english/Resizable/createResizable.generated.ts +14 -0
  163. package/src/i18n/generated-english/TimeInput/TimeWheels.generated.ts +18 -0
  164. package/src/i18n/generated-locales/en.generated.ts +6 -4
  165. package/src/i18n/generated-locales/pseudo.generated.ts +6 -4
  166. package/src/keyboard/README.md +197 -0
  167. package/src/keyboard/index.ts +48 -0
  168. package/src/keyboard/keyboardHeight.ts +543 -0
  169. package/src/keyboard/keyboardLayoutGuide.ts +266 -0
  170. package/src/keyboard/keyboardModel.ts +301 -0
  171. package/src/keyboard/rideKeyboard.ts +100 -0
  172. package/src/keyboard/useMobileKeyboard.ts +866 -0
  173. package/src/motion/MEASUREMENTS.md +10 -0
  174. package/src/motion/README.md +2 -0
  175. package/src/motion/android/index.ts +18 -0
  176. package/src/motion/android/keyboard.ts +96 -0
  177. package/src/motion/uikit/datePicker.ts +60 -0
  178. package/src/motion/uikit/index.ts +4 -3
  179. package/src/utils/interactionOverlay.stylex.ts +3 -2
  180. package/src/utils/layoutViewport.ts +104 -0
  181. package/src/utils/pressFeedback.ts +99 -21
  182. package/src/utils/pressGesture.ts +4 -2
  183. package/src/utils/selectionLock.ts +40 -6
  184. package/src/utils/sharedMutationObserver.ts +2 -1
  185. package/src/utils/timeParser.ts +40 -3
  186. package/dist/BottomSheet/useMobileKeyboard.d.ts +0 -21
  187. package/dist/BottomSheet/useMobileKeyboard.d.ts.map +0 -1
  188. package/dist/BottomSheet/useMobileKeyboard.js +0 -413
  189. package/src/BottomSheet/useMobileKeyboard.ts +0 -542
@@ -268,6 +268,16 @@ match the probe.
268
268
  | M-SH21 | SheetProbe on iPad, iOS 27 `preferredPlacement` each case, RTL | present | placement geometry and motion | B, C |
269
269
  | M-SH22 | SheetProbe iOS 26 `preferredTransition = .zoom` from a bar button | present, dismiss | morph geometry per frame | A, C |
270
270
 
271
+ ## Date picker (UIDatePicker)
272
+
273
+ Rules the DateInput, DateTimeInput and TimeInput touch sheets follow, in their
274
+ own table (`uikit/datePicker.ts`). The wheels' physics are UIPickerView's: the
275
+ table only names them, and their values belong to the wheel's own table.
276
+
277
+ Done (UIKit lab, iOS 26.2 simulator, Oct 23 2026; in the date picker's table):
278
+ the month/year wheels changing the date, the AM/PM column following the hour,
279
+ the turn back onto a bound, and the day clamped to the month.
280
+
271
281
  ## Context menu (UIContextMenuInteraction)
272
282
 
273
283
  Values the context menu reads from its own table, which lands with the
@@ -26,6 +26,8 @@ roles through `resolveMotion`.
26
26
  | `uikit/picker.ts` | UIPickerView: row pitch, height and rubber band, velocity floor, the coast's stop speed, snap at rest, tap spin, tick. |
27
27
  | `uikit/segmented.ts` | UISegmentedControl: the selection indicator's spring; under Reduce Motion the animator's cross-fade replaces it. |
28
28
  | `uikit/switch.ts` | UISwitch on iOS 26.2: geometry, the lift, settle, travel and tracking springs, the flip distance, the track colour. |
29
+ | `uikit/datePicker.ts` | UIDatePicker: the rules it adds to its wheels. |
30
+ | `android/keyboard.ts` | Android's software keyboard (Chrome with Gboard): its slides' durations and curves (no theme's) and when they start against Chrome's report and a blur. |
29
31
  | `roles.ts` | The internal motion roles: the springs, durations and curves `spec:AST-094` FR4 names, registered by the shared tables, and a role's CSS form read back. |
30
32
  | `roleValue.ts` | A role's CSS form, written: a spring's duration and bounce, a duration in ms, a curve's `cubic-bezier()`. |
31
33
  | `resolveMotion.ts` | A role's value: its table value, as the per-role tables stay internal (`spec:AST-094` DEC-15). |
@@ -0,0 +1,18 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file index.ts
5
+ * @input The domain files of the Android constants table
6
+ * @output Re-exports `keyboard`
7
+ * @position Entry of the Android constants table (motion/android). It is
8
+ * internal to core, not exported from the package entry point.
9
+ *
10
+ * Where a touch presentation follows something Android draws itself, such as
11
+ * the software keyboard, its numbers come from here with their sources. The
12
+ * UIKit table does the same for iOS.
13
+ *
14
+ * SYNC: When you add a domain file, re-export it here and list it in
15
+ * /packages/core/src/motion/README.md.
16
+ */
17
+
18
+ export {keyboard} from './keyboard';
@@ -0,0 +1,96 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file keyboard.ts
5
+ * @input None
6
+ * @output `keyboard`: how the software keyboard moves on Android, in Chrome
7
+ * with Gboard, as the page sees it. It holds the slides' durations and
8
+ * curves, and when they start against Chrome's report and a blur.
9
+ * @position The keyboard domain of the Android constants table, read by
10
+ * keyboard/keyboardModel.ts. The UIKit table holds iOS's keyboard.
11
+ *
12
+ * SYNC: When modified, update /packages/core/src/keyboard/README.md
13
+ */
14
+
15
+ // Chrome reports the keyboard once per slide, through the visual viewport,
16
+ // just after the slide starts, never per frame. Content that rides the
17
+ // keyboard plays these curves from the slide's start, so it moves in step
18
+ // with Gboard. They are Gboard's own motion, with no motion role. The system
19
+ // moves the keyboard on them whatever the page does, so any other curve
20
+ // would leave the content out of step.
21
+ //
22
+ // The values come from the Android 15 emulator with Chromium 158 and Gboard.
23
+ // A screen recording at 540 × 1200 gives Gboard's top in every frame, beside
24
+ // a marker the page paints on each report. Every leaf carries one or more
25
+ // `@source`, and android.test.ts enforces it.
26
+ export const keyboard = {
27
+ /**
28
+ * How long Gboard takes to slide up, in ms: from its first moved frame to
29
+ * its resting top.
30
+ *
31
+ * @source measured: Chromium 158 with Gboard on Android 15 (API 35
32
+ * emulator); 20 shows recorded frame by frame: 250 to 282 ms, 18 of
33
+ * them 264 to 268 ms, Oct 25 2026
34
+ */
35
+ showMs: 267,
36
+ /**
37
+ * The curve of Gboard's slide up, cubic-bézier control points: fast out,
38
+ * a long settle.
39
+ *
40
+ * @source measured: Chromium 158 with Gboard on Android 15 (API 35
41
+ * emulator); the same 20 shows fit this curve over `showMs` within
42
+ * 2.4 px rms, Oct 25 2026
43
+ */
44
+ showCurve: [0.1, 0, 0, 1],
45
+ /**
46
+ * How long after Gboard starts to slide up Chrome's report of it fires, in
47
+ * ms. A show rides from the report dated back by this, joining the slide
48
+ * where it is.
49
+ *
50
+ * @source measured: Chromium 158 with Gboard on Android 15 (API 35
51
+ * emulator); twelve shows: the first frame the page painted after the
52
+ * visual viewport's resize came 14 to 35 ms (median 33) after Gboard's
53
+ * first moved frame, less the 8 ms an event takes on average to reach
54
+ * the screen, Oct 25 2026
55
+ */
56
+ showReportLagMs: 25,
57
+ /**
58
+ * How long Gboard takes to slide down out of the page, in ms.
59
+ *
60
+ * @source measured: Chromium 158 with Gboard on Android 15 (API 35
61
+ * emulator); 21 hides recorded frame by frame, from Gboard's first
62
+ * moved frame until it leaves the screen: 148 to 153 ms in 20 of them;
63
+ * this length with `hideCurve` fits them within 7.7 px rms,
64
+ * Oct 25 2026
65
+ */
66
+ hideMs: 167,
67
+ /**
68
+ * The curve of Gboard's slide down, cubic-bézier control points: a slow
69
+ * first frame, then fast.
70
+ *
71
+ * @source measured: Chromium 158 with Gboard on Android 15 (API 35
72
+ * emulator); the same 21 hides, a free fit over `hideMs`, Oct 25 2026
73
+ */
74
+ hideCurve: [0.3, 0.1, 0.2, 0.9],
75
+ /**
76
+ * How long after the focused field blurs (to nothing that brings up a
77
+ * keyboard) Gboard starts to slide down, in ms.
78
+ *
79
+ * @source measured: Chromium 158 with Gboard on Android 15 (API 35
80
+ * emulator); five scripted blurs with a marker the page paints on the
81
+ * blur: Gboard's first moved frame came from a frame before the marker
82
+ * to 33 ms after it (median 16), Oct 25 2026
83
+ */
84
+ hideAfterBlurMs: 16,
85
+ /**
86
+ * How long after Gboard starts to slide down Chrome's report of the hide
87
+ * fires, in ms, measured on blurs. A hide nothing predicts, such as Back,
88
+ * reuses it, though Chrome can report Back later in the slide.
89
+ *
90
+ * @source measured: Chromium 158 with Gboard on Android 15 (API 35
91
+ * emulator); twelve scripted blurs: the visual viewport's resize came
92
+ * 42 to 52 ms after the blur (median 49), less `hideAfterBlurMs`,
93
+ * Oct 25 2026
94
+ */
95
+ hideReportLagMs: 33,
96
+ } as const;
@@ -0,0 +1,60 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file datePicker.ts
5
+ * @input Nothing; data only
6
+ * @output Exports `datePicker`: the rules UIDatePicker adds to its wheels. The
7
+ * month/year wheels change the date, the AM/PM column follows the hour, a
8
+ * column turns back onto a bound, and a day clamps to its month.
9
+ * @position The date picker domain of the UIKit constants table
10
+ * (motion/uikit), for the date and time touch sheets. Every leaf names its
11
+ * source, which uikit.test.ts checks.
12
+ *
13
+ * SYNC: When modified, update /packages/core/src/motion/MEASUREMENTS.md and
14
+ * /packages/core/src/motion/README.md.
15
+ */
16
+
17
+ export const datePicker = {
18
+ calendar: {
19
+ monthYearWheels: {
20
+ /**
21
+ * Turning the month or year wheel changes the picker's date, keeping
22
+ * the day, and fires `valueChanged`.
23
+ * @source measured: UIKit lab, iOS 26.2 simulator, `.date` `.inline`,
24
+ * October 15 → May 15 after the month wheel's fling, with a
25
+ * `valueChanged` carrying May 15, Oct 23 2026
26
+ */
27
+ changesDate: true,
28
+ },
29
+ },
30
+ wheels: {
31
+ /**
32
+ * In 12-hour time the AM/PM column follows the hour. Turning the hour from
33
+ * 11 to 12 turns the time from AM to PM, and back. From 12 to 1 it keeps
34
+ * the half of the day.
35
+ * @source measured: UIKit lab, iOS 26.2 simulator, `.time` `.wheels`,
36
+ * en_US: 11:58 AM, hour wheel +1 row → `valueChanged` 12:58 PM; then
37
+ * the minute wheel's +4 rows carried the hour from 12 to 1 → 1:02 PM,
38
+ * Oct 23 2026
39
+ */
40
+ meridiemFollowsHour: true,
41
+ /**
42
+ * A column that comes to rest past `minimumDate` or `maximumDate` turns
43
+ * back onto the bound. Its `valueChanged` already carries the bound. For
44
+ * example, with bounds 9:00–17:00, the hour of 10:00 dragged to 7 reads
45
+ * 9:00 at rest. The hour column then turns 64 pt, two rows, back to 9.
46
+ * @source measured: UIKit lab, iOS 26.2 simulator, `.time` `.wheels`
47
+ * with bounds 9:00–17:00 and `minuteInterval` 15, per-frame hour
48
+ * offsets after the rest (0.4, 2, 7.4, 16, 26.4, 37.7, 48.3, 56.7,
49
+ * 62, 64 pt at 33 ms steps) and the `valueChanged` time, Oct 23 2026
50
+ */
51
+ restPastBound: 'turnToBound',
52
+ /**
53
+ * In `.date` wheels a day the month does not have clamps to the month's
54
+ * last day (January 30, month +1 → February 28).
55
+ * @source measured: UIKit lab, iOS 26.2 simulator, `.date` `.wheels`
56
+ * with bounds Jun 1 2026 – Mar 15 2027, Oct 23 2026
57
+ */
58
+ dayClampsToMonth: true,
59
+ },
60
+ } as const;
@@ -5,9 +5,9 @@
5
5
  * @input The domain files of the UIKit constants table
6
6
  * @output Re-exports `springs` and one constant per spring (`SHEET_PRESENT_SPRING`,
7
7
  * …), `scroll`, `timing`, `navigation`, `picker`, `progressBar`,
8
- * `segmentedControl`, the `slider` and switch (`switchControl`) domains, and
9
- * the gesture thresholds (one `UIKIT_*` group per recognizer, their union
10
- * `UIKIT_GESTURES`, and the haptic tables)
8
+ * `segmentedControl`, `datePicker`, `slider` and `switchControl`. It also
9
+ * re-exports the gesture thresholds: one `UIKIT_*` group per recognizer,
10
+ * their union `UIKIT_GESTURES`, and the haptic tables
11
11
  * @position Entry of the UIKit constants table (motion/uikit). Internal to
12
12
  * core: not exported from the package entry point
13
13
  *
@@ -39,6 +39,7 @@ export {
39
39
  type GestureConstantUnit,
40
40
  type HapticFeedbackKind,
41
41
  } from './gestures';
42
+ export {datePicker} from './datePicker';
42
43
  export {navigation, registerNavigationTable} from './navigation';
43
44
  export {picker} from './picker';
44
45
  export {progressBar, registerProgressBarTable} from './progressBar';
@@ -76,8 +76,9 @@ export const pressVars = stylex.defineVars({
76
76
  *
77
77
  * `defineConsts`, so the one number is inlined here at build time and is the
78
78
  * controller's constant at run time (`PRESS_FADE_MS` in pressGesture.ts):
79
- * the release animation and the timer that removes the attribute after it
80
- * cannot drift apart.
79
+ * the release animation and the controller's clock cannot drift apart. When
80
+ * that clock runs out, the controller waits for a release animation that a
81
+ * busy main thread started late, then removes the attribute.
81
82
  */
82
83
  export const pressConsts = stylex.defineConsts({
83
84
  /** How long the release takes, in ms. UIKit's deselect clock. */
@@ -0,0 +1,104 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file layoutViewport.ts
5
+ * @input A window: its innerHeight and innerWidth, its visual viewport's
6
+ * width and scale, and its document's root box
7
+ * @output layoutViewportHeight, the layout viewport's height in layout px
8
+ * less a classic horizontal scrollbar, and __resetLayoutViewportForTest
9
+ * @position Internal shared measurement, not re-exported from the utils
10
+ * entry. The keyboard module, BottomSheet's keyboard obstruction and any
11
+ * layout that sizes against the layout viewport read it.
12
+ */
13
+
14
+ /**
15
+ * The layout viewport's height, CSS px, in the units of element rects.
16
+ *
17
+ * `innerHeight` is the layout viewport's height in most engines. WebKit
18
+ * reports `innerHeight` and `innerWidth` in the pinch-zoomed visual
19
+ * viewport's px instead. At a 1.33 zoom of a 402 × 714 layout viewport,
20
+ * Safari on iOS 26.2 reports 536 and 302. Element rects and the visual
21
+ * viewport's height times its scale stay in layout px. So, read as is, a zoom
22
+ * shrinks the layout viewport by the zoom, and the function scales the height
23
+ * back.
24
+ */
25
+ export function layoutViewportHeight(win: Window): number {
26
+ const viewport = win.visualViewport;
27
+ const width = viewport?.width ?? 0;
28
+ if (viewport == null || !(width > 0)) {
29
+ return win.innerHeight - horizontalScrollbarHeight(win);
30
+ }
31
+ const scale = viewport.scale;
32
+ // Which units `innerWidth` is in tells the two apart without naming an
33
+ // engine. It is the visual viewport's width where the zoom applies, and
34
+ // that width times the scale where it does not. `innerWidth` is a whole
35
+ // number of px against a fractional width, so it never equals either, and
36
+ // the nearest wins with no tolerance constant. At a scale of 1 the two
37
+ // widths are equal, so the function takes `innerHeight` as it is. A classic
38
+ // scrollbar, which `innerWidth` counts, then changes nothing.
39
+ const fromVisual = Math.abs(win.innerWidth - width);
40
+ const fromLayout = Math.abs(win.innerWidth - width * scale);
41
+ const height =
42
+ fromVisual < fromLayout ? win.innerHeight * scale : win.innerHeight;
43
+ return height - horizontalScrollbarHeight(win);
44
+ }
45
+
46
+ /**
47
+ * The document's classic horizontal scrollbar, CSS px. It is in
48
+ * `innerHeight` but not in the visual viewport's height. On a desktop page
49
+ * wider than its window, Chromium's `innerHeight` is 800 for a 785 px visual
50
+ * viewport. It is the part of `innerHeight` the root's client height leaves
51
+ * out, up to the thickness of a scrollbar that takes room. Where scrollbars
52
+ * overlay the content, as on every phone, that thickness is 0. So the room a
53
+ * hidden toolbar adds to `innerHeight` is never taken for a scrollbar. Chrome
54
+ * on Android sizes the root to the viewport with its toolbar shown.
55
+ */
56
+ function horizontalScrollbarHeight(win: Window): number {
57
+ const root = win.document.documentElement;
58
+ const gap = win.innerHeight - root.clientHeight;
59
+ if (!(gap > 0) || root.scrollWidth <= root.clientWidth) {
60
+ return 0;
61
+ }
62
+ return Math.min(gap, scrollbarThickness(win.document));
63
+ }
64
+
65
+ // The scrollbar thickness each document measured, and the device pixel ratio
66
+ // it measured it at. A browser zoom changes the ratio, and with it how many
67
+ // CSS px a classic scrollbar takes. Measuring writes a probe to the document
68
+ // and forces a layout, so it runs once per document and zoom. Every later
69
+ // measurement stays one layout read.
70
+ let thicknesses = new WeakMap<
71
+ Document,
72
+ {devicePixelRatio: number; thickness: number}
73
+ >();
74
+
75
+ /** How much room a scrollbar takes from a scrolling box: 0 where overlaid. */
76
+ function scrollbarThickness(doc: Document): number {
77
+ const devicePixelRatio = doc.defaultView?.devicePixelRatio ?? 1;
78
+ const known = thicknesses.get(doc);
79
+ if (known != null && known.devicePixelRatio === devicePixelRatio) {
80
+ return known.thickness;
81
+ }
82
+ const thickness = measureScrollbarThickness(doc);
83
+ thicknesses.set(doc, {devicePixelRatio, thickness});
84
+ return thickness;
85
+ }
86
+
87
+ function measureScrollbarThickness(doc: Document): number {
88
+ const probe = doc.createElement('div');
89
+ probe.style.position = 'absolute';
90
+ probe.style.top = '-200px';
91
+ probe.style.width = '100px';
92
+ probe.style.height = '100px';
93
+ probe.style.overflow = 'scroll';
94
+ probe.style.visibility = 'hidden';
95
+ (doc.body ?? doc.documentElement).append(probe);
96
+ const thickness = probe.offsetHeight - probe.clientHeight;
97
+ probe.remove();
98
+ return thickness;
99
+ }
100
+
101
+ /** Forgets every measured scrollbar thickness. Tests only. */
102
+ export function __resetLayoutViewportForTest(): void {
103
+ thicknesses = new WeakMap();
104
+ }
@@ -109,7 +109,8 @@ interface LiveGesture {
109
109
  * for a press of the Space key.
110
110
  */
111
111
  pointerId: number | null;
112
- timer: ReturnType<typeof setTimeout> | null;
112
+ /** Cancels the clock the current phase owes, or the release it waits for. */
113
+ timer: (() => void) | null;
113
114
  }
114
115
 
115
116
  /**
@@ -165,6 +166,76 @@ export function pressFadeMs(element: Element): number {
165
166
  return reduced ? Math.min(scaled, PRESS_FADE_MS) : scaled;
166
167
  }
167
168
 
169
+ /**
170
+ * The release animations that still run for `element`'s press. Each is a
171
+ * finite CSS animation that has not finished. It runs on the element, on its
172
+ * `::before` or `::after`, or on a descendant whose nearest press owner is
173
+ * the element, because a component can draw its release on a part inside the
174
+ * surface. A press owner nested inside plays the release of its own press,
175
+ * so it never extends this one. The list is empty where the engine has no Web
176
+ * Animations.
177
+ */
178
+ function releaseAnimationsOf(element: Element): Animation[] {
179
+ if (typeof element.getAnimations !== 'function') {
180
+ return [];
181
+ }
182
+ return element.getAnimations({subtree: true}).filter(animation => {
183
+ // The effect of a CSS animation is a KeyframeEffect. Its target is the
184
+ // element, also when the animation runs on a pseudo-element.
185
+ const effect = animation.effect as KeyframeEffect | null;
186
+ const target = effect?.target ?? null;
187
+ if (
188
+ effect == null ||
189
+ target == null ||
190
+ !('animationName' in animation) ||
191
+ (target !== element &&
192
+ target.closest(`[${PRESSABLE_ATTRIBUTE}]`) !== element)
193
+ ) {
194
+ return false;
195
+ }
196
+ if (!Number.isFinite(Number(effect.getComputedTiming().endTime))) {
197
+ return false;
198
+ }
199
+ return animation.playState !== 'finished';
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Calls `end` when the release on `element` is over. A busy main thread
205
+ * delays the first frame of the release animation, and with it the end. So a
206
+ * clock that starts at the `fading` write can cut the release short. When
207
+ * `clockMs` runs out, the release waits for each release animation that still
208
+ * runs or still waits for its first frame. It ends when those finish or are
209
+ * cancelled, as when the surface is unmounted or restyled. With no release
210
+ * animation (an instant release, a surface without one, an engine without Web
211
+ * Animations), the release ends at the clock. Returns a cancel.
212
+ */
213
+ function endAfterRelease(
214
+ element: Element,
215
+ clockMs: number,
216
+ end: () => void,
217
+ ): () => void {
218
+ let waiting = true;
219
+ const timer = setTimeout(() => {
220
+ const running = releaseAnimationsOf(element);
221
+ if (running.length === 0) {
222
+ end();
223
+ return;
224
+ }
225
+ void Promise.allSettled(
226
+ running.map(async animation => animation.finished),
227
+ ).then(() => {
228
+ if (waiting) {
229
+ end();
230
+ }
231
+ });
232
+ }, clockMs);
233
+ return () => {
234
+ waiting = false;
235
+ clearTimeout(timer);
236
+ };
237
+ }
238
+
168
239
  const TIMER_EVENT = {
169
240
  fade: 'fadeEnd',
170
241
  flash: 'flashEnd',
@@ -181,7 +252,7 @@ function paintElement(element: HTMLElement, paint: PressPaint): void {
181
252
 
182
253
  function clearTimer(slot: Slot): void {
183
254
  if (slot.live?.timer != null) {
184
- clearTimeout(slot.live.timer);
255
+ slot.live.timer();
185
256
  slot.live.timer = null;
186
257
  }
187
258
  }
@@ -217,25 +288,32 @@ function apply(slot: Slot, step: PressStep): void {
217
288
  }
218
289
  if (step.timer != null) {
219
290
  const kind = step.timer;
220
- // A TIMER, never `requestAnimationFrame`: iOS throttles frame callbacks
221
- // while a scroll runs, so a frame-based delay would fire late, after the
222
- // scroll, and paint a control nobody is pressing any more.
223
- live.timer = setTimeout(
224
- () => {
225
- if (slot.live != null) {
226
- slot.live.timer = null;
227
- }
228
- apply(
229
- slot,
230
- pressGestureStep(slot.live?.gesture ?? idlePressGesture(), {
231
- kind: TIMER_EVENT[kind],
232
- }),
233
- );
234
- },
235
- // The release fade follows the theme's tempo (AST-108). The onset delay
236
- // and the tap flash do not.
237
- kind === 'fade' ? pressFadeMs(live.element) : TIMER_MS[kind],
238
- );
291
+ const fire = () => {
292
+ if (slot.live != null) {
293
+ slot.live.timer = null;
294
+ }
295
+ apply(
296
+ slot,
297
+ pressGestureStep(slot.live?.gesture ?? idlePressGesture(), {
298
+ kind: TIMER_EVENT[kind],
299
+ }),
300
+ );
301
+ };
302
+ if (kind === 'fade') {
303
+ // The release fade follows the theme's tempo (AST-108). It ends when
304
+ // its animation ends, however late a busy main thread let it start.
305
+ live.timer = endAfterRelease(
306
+ live.element,
307
+ pressFadeMs(live.element),
308
+ fire,
309
+ );
310
+ } else {
311
+ // A TIMER, never `requestAnimationFrame`: iOS throttles frame callbacks
312
+ // while a scroll runs, so a frame-based delay would fire late, after the
313
+ // scroll, and paint a control nobody is pressing any more.
314
+ const timer = setTimeout(fire, TIMER_MS[kind]);
315
+ live.timer = () => clearTimeout(timer);
316
+ }
239
317
  }
240
318
  }
241
319
 
@@ -62,7 +62,9 @@ export const PRESS_FLASH_MS = 100;
62
62
  * How long the paint takes to leave after a release: UIKit's deselect
63
63
  * crossfade. The one number, shared with the stylesheet: the release
64
64
  * animation in interactionOverlay.stylex.ts runs on the same constant, so the
65
- * timer that removes the attribute and the fade cannot drift apart.
65
+ * controller's release clock and the fade cannot drift apart. When the clock
66
+ * runs out, the controller waits for a release animation that still runs,
67
+ * then removes the attribute.
66
68
  */
67
69
  export const PRESS_FADE_MS: number = pressConsts.releaseMs;
68
70
 
@@ -119,7 +121,7 @@ export type PressEvent =
119
121
  | {kind: 'onset'}
120
122
  /** {@link PRESS_FLASH_MS} elapsed on a too-quick tap. */
121
123
  | {kind: 'flashEnd'}
122
- /** {@link PRESS_FADE_MS} elapsed; the paint is gone. */
124
+ /** The release is over: its animation ended, or its clock ran out with none running. */
123
125
  | {kind: 'fadeEnd'};
124
126
 
125
127
  export interface PressStep {
@@ -116,6 +116,16 @@ export interface DragSelectionReleaseOptions {
116
116
  isCancel?: boolean;
117
117
  }
118
118
 
119
+ /** A drag's selection hold: call it to release the hold. */
120
+ export interface DragSelectionHold {
121
+ (options?: DragSelectionReleaseOptions): void;
122
+ /**
123
+ * Called after the lift's own handlers ran. A selection they made since
124
+ * the release stays: the hold then puts it back, not the press's.
125
+ */
126
+ keepLiftSelection(): void;
127
+ }
128
+
119
129
  /**
120
130
  * Refuse a new selection while a mouse or pen drag that a recognizer claimed
121
131
  * runs, until the returned function runs. `kept` is the page's selection
@@ -140,9 +150,13 @@ export function lockDragSelection(
140
150
  view: Window | null | undefined,
141
151
  kept: ReadonlyArray<AbstractRange>,
142
152
  onReleased?: () => void,
143
- ): (options?: DragSelectionReleaseOptions) => void {
153
+ ): DragSelectionHold {
144
154
  const doc = view?.document ?? null;
145
- const roots = shadowRootsOfRanges(kept);
155
+ let keptRanges = kept;
156
+ let roots = shadowRootsOfRanges(keptRanges);
157
+ // The selection when the lift released the hold, until the lift's
158
+ // handlers ran.
159
+ let atLift: ReadonlyArray<AbstractRange> | null = null;
146
160
  const restore = () => {
147
161
  const selection = view?.getSelection();
148
162
  // The focused field keeps its own selection (a drag never starts in one).
@@ -150,7 +164,7 @@ export function lockDragSelection(
150
164
  return;
151
165
  }
152
166
  const held = heldRanges(selection, roots);
153
- if (kept.length === 0) {
167
+ if (keptRanges.length === 0) {
154
168
  if (held.some(range => !range.collapsed)) {
155
169
  selection.removeAllRanges();
156
170
  }
@@ -158,10 +172,10 @@ export function lockDragSelection(
158
172
  }
159
173
  // A render can remove what the kept selection held: then there is
160
174
  // nothing to put back.
161
- if (sameRanges(held, kept) || !kept.every(isValidRange)) {
175
+ if (sameRanges(held, keptRanges) || !keptRanges.every(isValidRange)) {
162
176
  return;
163
177
  }
164
- selectRanges(selection, kept);
178
+ selectRanges(selection, keptRanges);
165
179
  };
166
180
  restore();
167
181
  const set = new Set(elements);
@@ -201,9 +215,10 @@ export function lockDragSelection(
201
215
  onReleased?.();
202
216
  }
203
217
  };
204
- return ({isCancel = false}: DragSelectionReleaseOptions = {}) => {
218
+ const release = (({isCancel = false}: DragSelectionReleaseOptions = {}) => {
205
219
  if (stage === 'held') {
206
220
  stage = 'lifted';
221
+ atLift = selectionRangesOf(view);
207
222
  for (const element of set) {
208
223
  element.removeEventListener('selectstart', refuse);
209
224
  }
@@ -218,7 +233,26 @@ export function lockDragSelection(
218
233
  } else if (stage === 'lifted' && isCancel) {
219
234
  releaseDocument();
220
235
  }
236
+ }) as DragSelectionHold;
237
+ release.keepLiftSelection = () => {
238
+ if (stage !== 'lifted' || atLift == null) {
239
+ return;
240
+ }
241
+ const before = atLift;
242
+ atLift = null;
243
+ const keepIfChanged = () => {
244
+ const now = selectionRangesOf(view);
245
+ if (stage !== 'done' && !sameRanges(now, before)) {
246
+ keptRanges = now;
247
+ roots = shadowRootsOfRanges(now);
248
+ }
249
+ };
250
+ keepIfChanged();
251
+ // A click's state update renders in a microtask, and its layout effect
252
+ // may select then. That still runs before the press's own mouseup.
253
+ queueMicrotask(keepIfChanged);
221
254
  };
255
+ return release;
222
256
  }
223
257
 
224
258
  /**
@@ -8,7 +8,8 @@
8
8
  * @output Exports observeMutations (one shared observer per set of options)
9
9
  * and observeContent, its text-and-children form
10
10
  * @position Internal utility; consumed by useTruncation, hitArea, AppShell's
11
- * main-overflow observer and ModalAnnounceProvider's presentation watch. The mutation counterpart of sharedResizeObserver:
11
+ * main-overflow observer, ModalAnnounceProvider's presentation watch and
12
+ * CheckboxList's required group. The mutation counterpart of sharedResizeObserver:
12
13
  * one observer for every element observed with the same options, so a
13
14
  * repeated cell, row or region does not create one observer per instance.
14
15
  */