@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
@@ -0,0 +1,352 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file timeWheelRules.ts
5
+ * @input A time of day, the hour format and the bounds
6
+ * @output Pure rules of UIDatePicker's time wheels: the columns' rows, the
7
+ * time a turned column makes, and which other columns turn with it
8
+ * @position Internal helper of the TimeInput and DateTimeInput sheets: the
9
+ * rows of both their finger and their mouse wheels
10
+ *
11
+ * The columns do not loop. Each rule follows a leaf of motion/uikit/
12
+ * datePicker.ts `wheels`. Pure functions keep the rules testable without a
13
+ * layout engine.
14
+ *
15
+ * SYNC: When modified, update timeWheelRules.test.ts and TimeWheels.tsx.
16
+ */
17
+
18
+ import {
19
+ clampTime,
20
+ formatISOTime,
21
+ parseISOTime,
22
+ type ISOTimeString,
23
+ } from '../utils/timeParser';
24
+
25
+ /** A time of day, 24-hour. */
26
+ export interface TimeParts {
27
+ hour: number;
28
+ minute: number;
29
+ second: number;
30
+ }
31
+
32
+ /** A time wheel column. */
33
+ export type TimeColumn = 'hour' | 'minute' | 'second' | 'meridiem';
34
+
35
+ /** One row of a column: the value it stands for and its label. */
36
+ export interface TimeRow {
37
+ value: number;
38
+ label: string;
39
+ isDisabled: boolean;
40
+ /**
41
+ * Where the row falls when a key steps the column. An hour row holds the hour
42
+ * it stands for, so a key follows the full time: 12 PM comes before 1 PM.
43
+ */
44
+ order?: number;
45
+ }
46
+
47
+ /** Inclusive bounds in seconds since midnight; `null` is unbounded. */
48
+ export interface TimeBounds {
49
+ min: number | null;
50
+ max: number | null;
51
+ }
52
+
53
+ const SECONDS_PER_DAY = 24 * 3600;
54
+
55
+ function secondsOfISO(time: ISOTimeString | undefined): number | null {
56
+ if (time == null) {
57
+ return null;
58
+ }
59
+ const parsed = parseISOTime(time);
60
+ return parsed == null
61
+ ? null
62
+ : parsed.hour * 3600 + parsed.minute * 60 + parsed.second;
63
+ }
64
+
65
+ /** The bounds of a field's `min` and `max` times. */
66
+ export function wheelBounds(
67
+ min: ISOTimeString | undefined,
68
+ max: ISOTimeString | undefined,
69
+ ): TimeBounds {
70
+ return {min: secondsOfISO(min), max: secondsOfISO(max)};
71
+ }
72
+
73
+ /** Whether the hour falls in the PM half (1) or the AM half (0). */
74
+ export function meridiemOf(hour: number): 0 | 1 {
75
+ return hour < 12 ? 0 : 1;
76
+ }
77
+
78
+ /** The 12-hour clock's label for a 24-hour hour: 0 → 12, 13 → 1. */
79
+ export function hour12From24(hour: number): number {
80
+ const h = hour % 12;
81
+ return h === 0 ? 12 : h;
82
+ }
83
+
84
+ /** The 24-hour hour for a 12-hour hour in a half of the day: 12 AM → 0. */
85
+ export function hour24From12(hour: number, meridiem: number): number {
86
+ return (hour % 12) + (meridiem === 1 ? 12 : 0);
87
+ }
88
+
89
+ function partsOf(seconds: number): TimeParts {
90
+ const s = ((seconds % SECONDS_PER_DAY) + SECONDS_PER_DAY) % SECONDS_PER_DAY;
91
+ return {
92
+ hour: Math.floor(s / 3600),
93
+ minute: Math.floor((s % 3600) / 60),
94
+ second: s % 60,
95
+ };
96
+ }
97
+
98
+ /** Whether [start, end] (seconds) overlaps the bounds. */
99
+ function overlaps(start: number, end: number, bounds: TimeBounds): boolean {
100
+ return (
101
+ (bounds.min == null || end >= bounds.min) &&
102
+ (bounds.max == null || start <= bounds.max)
103
+ );
104
+ }
105
+
106
+ /**
107
+ * The time the wheels show for `time`: its seconds kept when the picker has a
108
+ * seconds column and dropped when it has none. A time outside the bounds is
109
+ * shown as it is, on its disabled rows: opening the sheet never moves an
110
+ * untouched value.
111
+ */
112
+ export function displayedTime(time: TimeParts, hasSeconds: boolean): TimeParts {
113
+ return {...time, second: hasSeconds ? time.second : 0};
114
+ }
115
+
116
+ /**
117
+ * The time a column's rest commits when it would make `time`: the nearest
118
+ * time inside the bounds that the columns have a row for. Past a bound, that
119
+ * is the bound itself, or, with no seconds column, the nearest whole minute
120
+ * inside it (a 16:55:30 maximum: 16:55). Inside the bounds, `time` itself.
121
+ */
122
+ export function restingTime(
123
+ time: TimeParts,
124
+ hasSeconds: boolean,
125
+ bounds: TimeBounds,
126
+ ): TimeParts {
127
+ const boundAt = (seconds: number | null) =>
128
+ seconds == null ? undefined : formatISOTime(partsOf(seconds), true);
129
+ const resting = clampTime(
130
+ formatISOTime(time, hasSeconds),
131
+ boundAt(bounds.min),
132
+ boundAt(bounds.max),
133
+ hasSeconds,
134
+ );
135
+ return parseISOTime(resting) ?? time;
136
+ }
137
+
138
+ /**
139
+ * The hour the hour column makes when it comes to rest on `row` from the row
140
+ * that showed `from` (24-hour). In 24-hour time the row is the hour. In 12-hour
141
+ * time the half of the day changes only when the move passes between 11 and 12.
142
+ * The column (1 … 12) does not loop, so 12 counts as next to 1. A move between
143
+ * 12 and 1 to 6 keeps the half of the day. A move between 12 and 7 to 11
144
+ * changes it.
145
+ *
146
+ * @example
147
+ * ```
148
+ * hourAfterTurn(11, 12, '12h'); // 12: 11 AM → 12 PM
149
+ * hourAfterTurn(12, 1, '12h'); // 13: 12 PM → 1 PM
150
+ * hourAfterTurn(13, 9, '12h'); // 21: 1 PM → 9 PM
151
+ * ```
152
+ */
153
+ export function hourAfterTurn(
154
+ from: number,
155
+ row: number,
156
+ hourFormat: '12h' | '24h',
157
+ ): number {
158
+ if (hourFormat === '24h') {
159
+ return row;
160
+ }
161
+ const shown = hour12From24(from);
162
+ if (row === shown) {
163
+ return from;
164
+ }
165
+ const half = meridiemOf(from);
166
+ const passes11And12 =
167
+ (shown === 12 && row >= 7) || (row === 12 && shown >= 7);
168
+ return hour24From12(row, passes11And12 ? 1 - half : half);
169
+ }
170
+
171
+ function hourRowsFor(
172
+ hourFormat: '12h' | '24h',
173
+ hourOf: (row: number) => number,
174
+ bounds: TimeBounds,
175
+ ): TimeRow[] {
176
+ const rows =
177
+ hourFormat === '24h'
178
+ ? Array.from({length: 24}, (_, hour) => hour)
179
+ : Array.from({length: 12}, (_, index) => index + 1);
180
+ return rows.map(row => {
181
+ const hour = hourOf(row);
182
+ return {
183
+ value: row,
184
+ label: hourFormat === '24h' ? String(row).padStart(2, '0') : String(row),
185
+ isDisabled: !overlaps(hour * 3600, hour * 3600 + 3599, bounds),
186
+ order: hour,
187
+ };
188
+ });
189
+ }
190
+
191
+ /**
192
+ * The hour column under a finger. In 24-hour time its rows are 00 … 23. In
193
+ * 12-hour time they are 1 … 12, each row standing for the hour a turn onto it
194
+ * from `from` makes (`hourAfterTurn`). A row is disabled when no minute of that
195
+ * hour is inside the bounds.
196
+ */
197
+ export function hourRows(
198
+ from: number,
199
+ hourFormat: '12h' | '24h',
200
+ bounds: TimeBounds,
201
+ ): TimeRow[] {
202
+ return hourRowsFor(
203
+ hourFormat,
204
+ row => hourAfterTurn(from, row, hourFormat),
205
+ bounds,
206
+ );
207
+ }
208
+
209
+ /**
210
+ * The hour column under a mouse, whose columns turn one at a time. A 12-hour
211
+ * row stands for its hour in the half of the day that `from` is in.
212
+ */
213
+ export function hourRowsInHalf(
214
+ from: number,
215
+ hourFormat: '12h' | '24h',
216
+ bounds: TimeBounds,
217
+ ): TimeRow[] {
218
+ const half = meridiemOf(from);
219
+ return hourRowsFor(
220
+ hourFormat,
221
+ row => (hourFormat === '24h' ? row : hour24From12(row, half)),
222
+ bounds,
223
+ );
224
+ }
225
+
226
+ /**
227
+ * The minute column for `hour`: every minute. A row is disabled when it is
228
+ * outside the bounds (its whole minute, when the picker shows seconds).
229
+ */
230
+ export function minuteRows(
231
+ hour: number,
232
+ hasSeconds: boolean,
233
+ bounds: TimeBounds,
234
+ ): TimeRow[] {
235
+ return Array.from({length: 60}, (_, minute): TimeRow => {
236
+ const start = hour * 3600 + minute * 60;
237
+ return {
238
+ value: minute,
239
+ label: String(minute).padStart(2, '0'),
240
+ isDisabled: !overlaps(start, start + (hasSeconds ? 59 : 0), bounds),
241
+ };
242
+ });
243
+ }
244
+
245
+ /** The second column for `hour`:`minute`. */
246
+ export function secondRows(
247
+ hour: number,
248
+ minute: number,
249
+ bounds: TimeBounds,
250
+ ): TimeRow[] {
251
+ return Array.from({length: 60}, (_, second): TimeRow => {
252
+ const at = hour * 3600 + minute * 60 + second;
253
+ return {
254
+ value: second,
255
+ label: String(second).padStart(2, '0'),
256
+ isDisabled: !overlaps(at, at, bounds),
257
+ };
258
+ });
259
+ }
260
+
261
+ /** The AM/PM column: a half is disabled when none of its hours is in bounds. */
262
+ export function meridiemRows(
263
+ labels: {am: string; pm: string},
264
+ bounds: TimeBounds,
265
+ ): TimeRow[] {
266
+ return [
267
+ {
268
+ value: 0,
269
+ label: labels.am,
270
+ isDisabled: !overlaps(0, 12 * 3600 - 1, bounds),
271
+ },
272
+ {
273
+ value: 1,
274
+ label: labels.pm,
275
+ isDisabled: !overlaps(12 * 3600, SECONDS_PER_DAY - 1, bounds),
276
+ },
277
+ ];
278
+ }
279
+
280
+ /**
281
+ * The time made by turning `column` to `value` (its row's value) from `from`
282
+ * (the time the wheels show). Only that column changes, and the AM/PM column
283
+ * moves the hour by twelve.
284
+ */
285
+ export function turnColumn(
286
+ from: TimeParts,
287
+ column: TimeColumn,
288
+ value: number,
289
+ hourFormat: '12h' | '24h',
290
+ ): TimeParts {
291
+ switch (column) {
292
+ case 'hour':
293
+ return {...from, hour: hourAfterTurn(from.hour, value, hourFormat)};
294
+ case 'minute':
295
+ return {...from, minute: value};
296
+ case 'second':
297
+ return {...from, second: value};
298
+ case 'meridiem':
299
+ return {...from, hour: hour24From12(hour12From24(from.hour), value)};
300
+ }
301
+ }
302
+
303
+ /** What a column shows for a time: its row's value. */
304
+ export function columnValue(
305
+ time: TimeParts,
306
+ column: TimeColumn,
307
+ hourFormat: '12h' | '24h',
308
+ ): number {
309
+ switch (column) {
310
+ case 'hour':
311
+ return hourFormat === '24h' ? time.hour : hour12From24(time.hour);
312
+ case 'minute':
313
+ return time.minute;
314
+ case 'second':
315
+ return time.second;
316
+ case 'meridiem':
317
+ return meridiemOf(time.hour);
318
+ }
319
+ }
320
+
321
+ /**
322
+ * The columns that must turn after `column` came to rest on `value`. That is
323
+ * every column whose row for the committed time differs from the row it shows
324
+ * now. The column that moved shows `value`; the others show `from`. An hour
325
+ * that crossed 11 ↔ 12 turns the AM/PM column; a time brought back onto a bound
326
+ * turns every column whose row changed.
327
+ */
328
+ export function columnsToTurn(
329
+ from: TimeParts,
330
+ column: TimeColumn,
331
+ value: number,
332
+ committed: TimeParts,
333
+ options: {hourFormat: '12h' | '24h'; hasSeconds: boolean},
334
+ ): {column: TimeColumn; value: number}[] {
335
+ const columns: TimeColumn[] = ['hour', 'minute'];
336
+ if (options.hasSeconds) {
337
+ columns.push('second');
338
+ }
339
+ if (options.hourFormat === '12h') {
340
+ columns.push('meridiem');
341
+ }
342
+ const turns: {column: TimeColumn; value: number}[] = [];
343
+ for (const each of columns) {
344
+ const shown =
345
+ each === column ? value : columnValue(from, each, options.hourFormat);
346
+ const target = columnValue(committed, each, options.hourFormat);
347
+ if (shown !== target) {
348
+ turns.push({column: each, value: target});
349
+ }
350
+ }
351
+ return turns;
352
+ }
@@ -354,10 +354,10 @@ function TokenizerSheetContent<T extends SearchableItem>({
354
354
  // has an entrance animation and no exit, so a close is its unmount; the
355
355
  // close handler commits that unmount at once and then places focus itself,
356
356
  // now that the modal is gone and the sheet's content can take focus again
357
- // (`architecture:react-component-runtime` INV2): on the search field
358
- // after a confirmed clear — the clear is spent, and the search field is
359
- // where the next selection starts (FR24, `spec:AST-085` FR4) — and back on
360
- // Clear all after a cancel. A confirmed clear is announced there too — a
357
+ // (`architecture:react-component-runtime` INV2). After a clear, Clear all
358
+ // is aria-disabled but stays focusable, so it keeps focus (FR24,
359
+ // `spec:AST-085` FR4). Focus on the search field opens the on-screen
360
+ // keyboard over the cleared rows. A confirmed clear is announced too — a
361
361
  // change the person hears about, as an add or a remove is
362
362
  // (`component:Tokenizer` AR6) — after the confirmation's dialog has gone,
363
363
  // so the sheet is the modal on top and the words are delivered inside it
@@ -372,11 +372,9 @@ function TokenizerSheetContent<T extends SearchableItem>({
372
372
  const closeClearConfirmation = (confirmed: boolean) => {
373
373
  // eslint-disable-next-line @eslint-react/dom-no-flush-sync -- the confirmation must be gone before focus moves into the sheet and the words are routed into it
374
374
  flushSync(() => setIsClearConfirmationOpen(false));
375
+ clearAllRef.current?.focus();
375
376
  if (confirmed) {
376
- searchRef.current?.focus();
377
377
  announce(t('@astryx.tokenizer.allRemoved'));
378
- } else {
379
- clearAllRef.current?.focus();
380
378
  }
381
379
  };
382
380
  // Keep editing: the confirmation is gone, and focus returns to the search
@@ -1000,11 +998,9 @@ function TokenizerSheetContent<T extends SearchableItem>({
1000
998
  variant="secondary"
1001
999
  size="lg"
1002
1000
  isDisabled={draftValue.length === 0}
1003
- // With a tooltip, Button's disabled mode is `aria-disabled`: the
1004
- // action keeps its focus order and says why it does nothing
1005
- // (`spec:AST-085` FR1), instead of leaving the tab order as a
1006
- // natively disabled button does — a confirmed Clear all returns
1007
- // focus here before the search field takes it (FR24).
1001
+ // With a tooltip, Button uses aria-disabled, so Clear all stays
1002
+ // focusable after a clear and says why it does nothing
1003
+ // (`spec:AST-085` FR1; FR24).
1008
1004
  tooltip={
1009
1005
  draftValue.length === 0
1010
1006
  ? t('@astryx.tokenizer.nothingToClear')
@@ -633,6 +633,12 @@ export function useTooltip(options: TooltipOptions = {}): TooltipReturn {
633
633
  return;
634
634
  }
635
635
  if (handleTouchPointerDown(event)) {
636
+ // A pen in range starts the hover show before it lands. Left pending,
637
+ // that show opens the layer during the press. The tap then closes it.
638
+ if (showTimeoutRef.current) {
639
+ clearTimeout(showTimeoutRef.current);
640
+ showTimeoutRef.current = null;
641
+ }
636
642
  return;
637
643
  }
638
644
  if (isOpen !== undefined) {
@@ -179,6 +179,7 @@ import {
179
179
  import {subscribeInput} from '../utils/inputStream';
180
180
  import {isPrimaryClick} from '../utils/primaryClick';
181
181
  import {
182
+ type DragSelectionHold,
182
183
  type DragSelectionReleaseOptions,
183
184
  lockDragSelection,
184
185
  locksSelectionFor,
@@ -710,10 +711,7 @@ export class GestureHost {
710
711
  (options?: DragSelectionReleaseOptions) => void
711
712
  >();
712
713
  /** Each claimed contact's drag hold, until its lift or a give-back. */
713
- private readonly claimHolds = new Map<
714
- number,
715
- (options?: DragSelectionReleaseOptions) => void
716
- >();
714
+ private readonly claimHolds = new Map<number, DragSelectionHold>();
717
715
  private readonly presentedHitTesting: 'auto' | 'always' | 'never';
718
716
  /**
719
717
  * Contacts sent to what is drawn instead of what the engine hit, by host
@@ -3747,10 +3745,13 @@ export class GestureHost {
3747
3745
  }
3748
3746
  this.pointerIds.delete(event.pointerId);
3749
3747
  this.pointerKinds.delete(event.pointerId);
3748
+ const hold = this.claimHolds.get(id);
3750
3749
  this.unlockSelection(id);
3751
3750
  const dispositions = this.arena.ended([
3752
3751
  {id, x: event.clientX, y: event.clientY, time: eventTime(event)},
3753
3752
  ]);
3753
+ // A selection the lift's own handlers made stays, as a click's does.
3754
+ hold?.keepLiftSelection();
3754
3755
  // A retargeted tap's own click arms the click guard for the engine's.
3755
3756
  const landed = this.retargeted.get(id);
3756
3757
  this.retargeted.delete(id);
@@ -18,15 +18,18 @@ const english: Readonly<Record<string, string>> = {
18
18
  "@astryx.powersearch.sheet.clearAllConfirm": "Clear filters",
19
19
  "@astryx.powersearch.sheet.clearAllDescription": "This removes all editable filters from this search.",
20
20
  "@astryx.powersearch.sheet.clearAllTitle": "Clear all filters?",
21
+ "@astryx.powersearch.sheet.discardConfirm": "Discard changes",
22
+ "@astryx.powersearch.sheet.discardDescription": "Your changes to the filters haven't been applied.",
23
+ "@astryx.powersearch.sheet.discardTitle": "Discard changes?",
21
24
  "@astryx.powersearch.sheet.done": "Done",
22
- "@astryx.powersearch.sheet.editableFiltersClearedWithCount": "Editable filters removed. {resultCount}",
23
25
  "@astryx.powersearch.sheet.fieldOperator": "{field} {operator}",
24
26
  "@astryx.powersearch.sheet.filterUnavailable": "This filter is no longer available",
25
- "@astryx.powersearch.sheet.filtersClearedWithCount": "All filters removed. {resultCount}",
26
27
  "@astryx.powersearch.sheet.filtersTitle": "Filters",
28
+ "@astryx.powersearch.sheet.keepEditing": "Keep editing",
27
29
  "@astryx.powersearch.sheet.labelValue": "{label} {value}",
28
30
  "@astryx.powersearch.sheet.noFilters": "No filters applied",
29
31
  "@astryx.powersearch.sheet.noFiltersDescription": "Add a filter to narrow your results and find what you need faster.",
32
+ "@astryx.powersearch.sheet.noFiltersInDraft": "No filters",
30
33
  "@astryx.powersearch.sheet.noMatchingFields": "No filters match your search",
31
34
  "@astryx.powersearch.sheet.noResults": "No results found",
32
35
  "@astryx.powersearch.sheet.removeFilter": "Remove {field} filter",
@@ -0,0 +1,14 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @generated by scripts/generate-i18n-runtime.mjs from packages/core/locales/en.json,
5
+ * holding the English for the catalog keys named in packages/core/src/Resizable/createResizable.ts.
6
+ * Do not edit by hand. Run `pnpm generate:i18n-runtime` instead.
7
+ */
8
+
9
+ const english: Readonly<Record<string, string>> = {
10
+ "@astryx.resizable.collapsed": "Collapsed",
11
+ "@astryx.resizable.handle.label": "Resize handle"
12
+ };
13
+
14
+ export default english;
@@ -0,0 +1,18 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @generated by scripts/generate-i18n-runtime.mjs from packages/core/locales/en.json,
5
+ * holding the English for the catalog keys named in packages/core/src/TimeInput/TimeWheels.tsx.
6
+ * Do not edit by hand. Run `pnpm generate:i18n-runtime` instead.
7
+ */
8
+
9
+ const english: Readonly<Record<string, string>> = {
10
+ "@astryx.dateTimeInput.hourWheel": "Hour",
11
+ "@astryx.dateTimeInput.meridiemAM": "AM",
12
+ "@astryx.dateTimeInput.meridiemPM": "PM",
13
+ "@astryx.dateTimeInput.meridiemWheel": "AM/PM",
14
+ "@astryx.dateTimeInput.minuteWheel": "Minute",
15
+ "@astryx.dateTimeInput.secondWheel": "Second"
16
+ };
17
+
18
+ export default english;
@@ -81,6 +81,7 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
81
81
  '@astryx.powersearch.sheet.filtersTitle': 'Filters',
82
82
  '@astryx.powersearch.sheet.addFilter': 'Add filter',
83
83
  '@astryx.powersearch.sheet.noFilters': 'No filters applied',
84
+ '@astryx.powersearch.sheet.noFiltersInDraft': 'No filters',
84
85
  '@astryx.powersearch.sheet.noFiltersDescription':
85
86
  'Add a filter to narrow your results and find what you need faster.',
86
87
  '@astryx.powersearch.sheet.clearAll': 'Clear all',
@@ -90,6 +91,11 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
90
91
  '@astryx.powersearch.sheet.clearAllConfirm': 'Clear filters',
91
92
  '@astryx.powersearch.sheet.clearAllCancel': 'Keep filters',
92
93
  '@astryx.powersearch.sheet.done': 'Done',
94
+ '@astryx.powersearch.sheet.discardTitle': 'Discard changes?',
95
+ '@astryx.powersearch.sheet.discardDescription':
96
+ "Your changes to the filters haven't been applied.",
97
+ '@astryx.powersearch.sheet.keepEditing': 'Keep editing',
98
+ '@astryx.powersearch.sheet.discardConfirm': 'Discard changes',
93
99
  '@astryx.powersearch.sheet.search': 'Search',
94
100
  '@astryx.powersearch.sheet.searchResults': 'Search results',
95
101
  '@astryx.powersearch.sheet.searchFields': 'Search filters',
@@ -115,10 +121,6 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
115
121
  '{count, number} {count, plural, one {month} other {months}} from now',
116
122
  '@astryx.powersearch.sheet.fieldOperator': '{field} {operator}',
117
123
  '@astryx.powersearch.sheet.labelValue': '{label} {value}',
118
- '@astryx.powersearch.sheet.filtersClearedWithCount':
119
- 'All filters removed. {resultCount}',
120
- '@astryx.powersearch.sheet.editableFiltersClearedWithCount':
121
- 'Editable filters removed. {resultCount}',
122
124
  '@astryx.alertDialog.cancel': 'Cancel',
123
125
  '@astryx.appShell.mobileNavigation': 'Mobile navigation',
124
126
  '@astryx.appShell.skipToContent': 'Skip to content',
@@ -83,6 +83,7 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
83
83
  '@astryx.powersearch.sheet.filtersTitle': '⟦Ƒíłţéřš⟧',
84
84
  '@astryx.powersearch.sheet.addFilter': '⟦Àðð ƒíłţéř⟧',
85
85
  '@astryx.powersearch.sheet.noFilters': '⟦Ñó ƒíłţéřš àþþłíéð⟧',
86
+ '@astryx.powersearch.sheet.noFiltersInDraft': '⟦Ñó ƒíłţéřš⟧',
86
87
  '@astryx.powersearch.sheet.noFiltersDescription':
87
88
  '⟦Àðð à ƒíłţéř ţó ñàřřóŵ ýóúř řéšúłţš àñð ƒíñð ŵĥàţ ýóú ñééð ƒàšţéř.⟧',
88
89
  '@astryx.powersearch.sheet.clearAll': '⟦Çłéàř àłł⟧',
@@ -92,6 +93,11 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
92
93
  '@astryx.powersearch.sheet.clearAllConfirm': '⟦Çłéàř ƒíłţéřš⟧',
93
94
  '@astryx.powersearch.sheet.clearAllCancel': '⟦Ķééþ ƒíłţéřš⟧',
94
95
  '@astryx.powersearch.sheet.done': '⟦Ðóñé⟧',
96
+ '@astryx.powersearch.sheet.discardTitle': '⟦Ðíšçàřð çĥàñĝéš?⟧',
97
+ '@astryx.powersearch.sheet.discardDescription':
98
+ "⟦Ýóúř çĥàñĝéš ţó ţĥé ƒíłţéřš ĥàṽéñ'ţ ƀééñ àþþłíéð.⟧",
99
+ '@astryx.powersearch.sheet.keepEditing': '⟦Ķééþ éðíţíñĝ⟧',
100
+ '@astryx.powersearch.sheet.discardConfirm': '⟦Ðíšçàřð çĥàñĝéš⟧',
95
101
  '@astryx.powersearch.sheet.search': '⟦Šéàřçĥ⟧',
96
102
  '@astryx.powersearch.sheet.searchResults': '⟦Šéàřçĥ řéšúłţš⟧',
97
103
  '@astryx.powersearch.sheet.searchFields': '⟦Šéàřçĥ ƒíłţéřš⟧',
@@ -118,10 +124,6 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
118
124
  '⟦{count, number} {count, plural, one {month} other {months}} ƒřóɱ ñóŵ⟧',
119
125
  '@astryx.powersearch.sheet.fieldOperator': '⟦{field} {operator}⟧',
120
126
  '@astryx.powersearch.sheet.labelValue': '⟦{label} {value}⟧',
121
- '@astryx.powersearch.sheet.filtersClearedWithCount':
122
- '⟦Àłł ƒíłţéřš řéɱóṽéð. {resultCount}⟧',
123
- '@astryx.powersearch.sheet.editableFiltersClearedWithCount':
124
- '⟦Éðíţàƀłé ƒíłţéřš řéɱóṽéð. {resultCount}⟧',
125
127
  '@astryx.alertDialog.cancel': '⟦Çàñçéł⟧',
126
128
  '@astryx.appShell.mobileNavigation': '⟦Ṁóƀíłé ñàṽíĝàţíóñ⟧',
127
129
  '@astryx.appShell.skipToContent': '⟦Šķíþ ţó çóñţéñţ⟧',