@astryxdesign/core 0.7.6 → 0.7.7-canary.240e8c5
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.
- package/dist/AppShell/AppShell.d.ts.map +1 -1
- package/dist/AppShell/AppShell.js +23 -3
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +16 -9
- package/dist/Chat/ChatComposerInput.d.ts +9 -0
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +2 -0
- package/dist/Chat/ChatMessage.js +2 -2
- package/dist/Chat/chatMessage.vars.stylex.d.ts +8 -8
- package/dist/Chat/chatMessage.vars.stylex.js +8 -8
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +8 -1
- package/dist/Drawer/Drawer.d.ts.map +1 -1
- package/dist/Drawer/Drawer.js +6 -0
- package/dist/DropdownMenu/useMenuOverflow.d.ts +13 -3
- package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -1
- package/dist/DropdownMenu/useMenuOverflow.js +31 -8
- package/dist/Field/PanelSearchInput.d.ts.map +1 -1
- package/dist/Field/PanelSearchInput.js +8 -1
- package/dist/Field/textEntryTraits.d.ts +6 -6
- package/dist/Field/textEntryTraits.js +6 -6
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +1 -0
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +25 -13
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +404 -131
- package/dist/Slider/sliderTouchController.d.ts +189 -0
- package/dist/Slider/sliderTouchController.d.ts.map +1 -0
- package/dist/Slider/sliderTouchController.js +1017 -0
- package/dist/Slider/useSliderTouch.d.ts +22 -0
- package/dist/Slider/useSliderTouch.d.ts.map +1 -0
- package/dist/Slider/useSliderTouch.js +56 -0
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +19 -9
- package/dist/astryx.css +33 -6
- package/dist/gestures/GestureArena.d.ts.map +1 -1
- package/dist/gestures/GestureArena.js +4 -1
- package/dist/gestures/GestureHost.d.ts +72 -2
- package/dist/gestures/GestureHost.d.ts.map +1 -1
- package/dist/gestures/GestureHost.js +643 -5
- package/dist/gestures/GestureRecognizer.d.ts +15 -1
- package/dist/gestures/GestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/GestureRecognizer.js +12 -0
- package/dist/gestures/PanGestureRecognizer.d.ts +8 -0
- package/dist/gestures/PanGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/PinchGestureRecognizer.d.ts +5 -1
- package/dist/gestures/PinchGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/PinchGestureRecognizer.js +10 -0
- package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +3 -3
- package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/ScreenEdgePanGestureRecognizer.js +7 -5
- package/dist/gestures/nativeScroll.d.ts +16 -2
- package/dist/gestures/nativeScroll.d.ts.map +1 -1
- package/dist/gestures/nativeScroll.js +39 -8
- package/dist/gestures/recognizerInternals.d.ts +4 -2
- package/dist/gestures/recognizerInternals.d.ts.map +1 -1
- package/dist/gestures/types.d.ts +11 -0
- package/dist/gestures/types.d.ts.map +1 -1
- package/dist/gestures/types.js +7 -0
- package/dist/gestures/wheelSegmenter.d.ts +45 -0
- package/dist/gestures/wheelSegmenter.d.ts.map +1 -0
- package/dist/gestures/wheelSegmenter.js +98 -0
- package/dist/motion/animator.d.ts.map +1 -1
- package/dist/motion/animator.js +140 -19
- package/dist/motion/uikit/index.d.ts +2 -1
- package/dist/motion/uikit/index.d.ts.map +1 -1
- package/dist/motion/uikit/index.js +2 -1
- package/dist/motion/uikit/slider.d.ts +97 -0
- package/dist/motion/uikit/slider.d.ts.map +1 -0
- package/dist/motion/uikit/slider.js +111 -0
- package/dist/motion/uikit/springs.d.ts +46 -1
- package/dist/motion/uikit/springs.d.ts.map +1 -1
- package/dist/motion/uikit/springs.js +48 -4
- package/package.json +5 -5
- package/src/AppShell/AppShell.doc.mjs +10 -0
- package/src/AppShell/AppShell.tsx +48 -2
- package/src/BottomSheet/BottomSheetPanel.tsx +16 -9
- package/src/Chat/Chat.doc.mjs +1 -0
- package/src/Chat/ChatComposerInput.doc.mjs +11 -0
- package/src/Chat/ChatComposerInput.tsx +11 -0
- package/src/Chat/ChatMessage.tsx +2 -2
- package/src/Chat/chatMessage.vars.stylex.ts +8 -8
- package/src/CommandPalette/CommandPaletteInput.tsx +6 -0
- package/src/Drawer/Drawer.tsx +6 -0
- package/src/DropdownMenu/useMenuOverflow.ts +44 -8
- package/src/Field/PanelSearchInput.tsx +6 -0
- package/src/Field/textEntryTraits.ts +6 -6
- package/src/Link/Link.tsx +3 -0
- package/src/Selector/Selector.tsx +29 -2
- package/src/Slider/Slider.doc.mjs +11 -7
- package/src/Slider/Slider.tsx +539 -132
- package/src/Slider/sliderTouchController.ts +1209 -0
- package/src/Slider/useSliderTouch.ts +78 -0
- package/src/Typeahead/BaseTypeahead.tsx +16 -8
- package/src/gestures/GestureArena.ts +1 -0
- package/src/gestures/GestureHost.ts +765 -5
- package/src/gestures/GestureRecognizer.ts +28 -0
- package/src/gestures/PanGestureRecognizer.ts +8 -0
- package/src/gestures/PinchGestureRecognizer.ts +16 -1
- package/src/gestures/README.md +63 -0
- package/src/gestures/ScreenEdgePanGestureRecognizer.ts +7 -5
- package/src/gestures/nativeScroll.ts +49 -9
- package/src/gestures/recognizerInternals.ts +9 -2
- package/src/gestures/types.ts +12 -0
- package/src/gestures/wheelSegmenter.ts +114 -0
- package/src/motion/animator.ts +193 -25
- package/src/motion/uikit/index.ts +2 -1
- package/src/motion/uikit/slider.ts +105 -0
- package/src/motion/uikit/springs.ts +37 -2
package/src/motion/animator.ts
CHANGED
|
@@ -517,6 +517,14 @@ interface StartGroup {
|
|
|
517
517
|
madeInFrame?: boolean;
|
|
518
518
|
/** The task it was made in, with every call made in that task. */
|
|
519
519
|
task?: TaskMark;
|
|
520
|
+
/**
|
|
521
|
+
* Per channel, when its animation's first frame ran without a known start.
|
|
522
|
+
* That frame came too late to stand for the start, and the browser had not
|
|
523
|
+
* told it yet. The compositor starts the animation within about a frame of
|
|
524
|
+
* this time. Until the browser tells its start, a read of the channel takes
|
|
525
|
+
* the curve from here.
|
|
526
|
+
*/
|
|
527
|
+
latest?: Map<Channel, number>;
|
|
520
528
|
/** A later call in the same task that took over its curves. */
|
|
521
529
|
adoptedBy?: StartGroup;
|
|
522
530
|
readonly resolve: (start: number) => void;
|
|
@@ -1044,7 +1052,7 @@ export function createAnimator(
|
|
|
1044
1052
|
if (segment == null) {
|
|
1045
1053
|
return null;
|
|
1046
1054
|
}
|
|
1047
|
-
if (time >=
|
|
1055
|
+
if (time >= endOf(property, segment)) {
|
|
1048
1056
|
return null;
|
|
1049
1057
|
}
|
|
1050
1058
|
return segment;
|
|
@@ -1070,16 +1078,89 @@ export function createAnimator(
|
|
|
1070
1078
|
};
|
|
1071
1079
|
|
|
1072
1080
|
/**
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1075
|
-
|
|
1076
|
-
|
|
1081
|
+
* A held curve's start once a frame dated its call, as the browser's start
|
|
1082
|
+
* moves it: by that frame's lead over the call's own time.
|
|
1083
|
+
*/
|
|
1084
|
+
const datedFromLatest = (
|
|
1085
|
+
segment: Segment,
|
|
1086
|
+
latest: number | undefined,
|
|
1087
|
+
): {latest: number; start: number} | null =>
|
|
1088
|
+
latest == null || segment.group == null
|
|
1089
|
+
? null
|
|
1090
|
+
: {latest, start: segment.start + latest - segment.group.provisional};
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* When a waiting curve's call had its first frame, which put the curve on
|
|
1094
|
+
* screen. Undefined for a curve that does not wait, or whose call has had
|
|
1095
|
+
* no first frame.
|
|
1096
|
+
*/
|
|
1097
|
+
const shownSince = (
|
|
1098
|
+
property: MotionProperty,
|
|
1099
|
+
segment: Segment,
|
|
1100
|
+
): number | undefined =>
|
|
1101
|
+
segment.group?.resolved === false
|
|
1102
|
+
? segment.group.latest?.get(CHANNEL_OF[property])
|
|
1103
|
+
: undefined;
|
|
1104
|
+
|
|
1105
|
+
/** A curve's end, dated from its call's first frame while it waits. */
|
|
1106
|
+
const endOf = (property: MotionProperty, segment: Segment): number =>
|
|
1107
|
+
(datedFromLatest(segment, shownSince(property, segment))?.start ??
|
|
1108
|
+
segment.start) + segment.duration;
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* Takes a curve on screen out of the call that still waits for the
|
|
1112
|
+
* browser's start. The curve keeps its course, dated from the call's first
|
|
1113
|
+
* frame. A call that has no channel left starts at that frame.
|
|
1114
|
+
*/
|
|
1115
|
+
const leaveWaitingCall = (
|
|
1116
|
+
segment: Segment,
|
|
1117
|
+
channel: Channel,
|
|
1118
|
+
latest: number,
|
|
1119
|
+
) => {
|
|
1120
|
+
const group = segment.group;
|
|
1121
|
+
if (group == null) {
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const index = group.segments.indexOf(segment);
|
|
1125
|
+
if (index >= 0) {
|
|
1126
|
+
group.segments.splice(index, 1);
|
|
1127
|
+
}
|
|
1128
|
+
segment.start += latest - group.provisional;
|
|
1129
|
+
segment.group = undefined;
|
|
1130
|
+
if (!CHANNELS[channel].some(p => segments.get(p)?.group === group)) {
|
|
1131
|
+
group.latest?.delete(channel);
|
|
1132
|
+
group.channels.delete(channel);
|
|
1133
|
+
}
|
|
1134
|
+
if (group.channels.size === 0 && group.segments.length === 0) {
|
|
1135
|
+
resolveGroup(group, latest);
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* The value on screen at `time`. Until the browser reports its start, a
|
|
1141
|
+
* held curve shows its first value. A frame that ran without that start
|
|
1142
|
+
* dates the curve. Without that frame, a later time reads from the
|
|
1143
|
+
* provisional start, so a companion that samples ahead follows the curve's
|
|
1144
|
+
* shape.
|
|
1077
1145
|
*/
|
|
1078
1146
|
const valueAt = (property: MotionProperty, time: number): number => {
|
|
1079
1147
|
const segment = segments.get(property);
|
|
1080
|
-
if (segment != null && isHeld(segment)
|
|
1148
|
+
if (segment != null && isHeld(segment)) {
|
|
1149
|
+
// The compositor starts the call near its first frame. The curve is
|
|
1150
|
+
// dated as the browser's start would date it. A curve that joined by an
|
|
1151
|
+
// older clock keeps its lead over the call's own time.
|
|
1152
|
+
const latest = segment.group?.latest?.get(CHANNEL_OF[property]);
|
|
1153
|
+
const dated = datedFromLatest(segment, latest);
|
|
1154
|
+
if (dated != null) {
|
|
1155
|
+
const elapsed = Math.max(time, dated.latest) - dated.start;
|
|
1156
|
+
return elapsed >= segment.duration
|
|
1157
|
+
? segment.target
|
|
1158
|
+
: segment.at(elapsed);
|
|
1159
|
+
}
|
|
1081
1160
|
// Nothing has moved on screen until the browser starts the call.
|
|
1082
|
-
|
|
1161
|
+
if (time <= now()) {
|
|
1162
|
+
return segment.at(0);
|
|
1163
|
+
}
|
|
1083
1164
|
}
|
|
1084
1165
|
return curveAt(property, time);
|
|
1085
1166
|
};
|
|
@@ -1087,10 +1168,22 @@ export function createAnimator(
|
|
|
1087
1168
|
const velocityAt = (property: MotionProperty, time: number): number => {
|
|
1088
1169
|
const held = segments.get(property);
|
|
1089
1170
|
if (held != null && isHeld(held)) {
|
|
1090
|
-
// Still at its start on screen, but moving in the model
|
|
1091
|
-
// carries the velocity the curve has reached by now
|
|
1092
|
-
// moves all land before the browser starts any keeps its speed.
|
|
1093
|
-
|
|
1171
|
+
// Still at its start on screen, but moving in the model. A retarget
|
|
1172
|
+
// carries the velocity the curve has reached by now. So a flick whose
|
|
1173
|
+
// moves all land before the browser starts any keeps its speed. Past
|
|
1174
|
+
// its first frame, it reads from that frame, as its value does.
|
|
1175
|
+
const dated = datedFromLatest(
|
|
1176
|
+
held,
|
|
1177
|
+
held.group?.latest?.get(CHANNEL_OF[property]),
|
|
1178
|
+
);
|
|
1179
|
+
return held.speed(
|
|
1180
|
+
Math.max(
|
|
1181
|
+
0,
|
|
1182
|
+
dated != null && time > dated.latest
|
|
1183
|
+
? time - dated.start
|
|
1184
|
+
: time - held.start,
|
|
1185
|
+
),
|
|
1186
|
+
);
|
|
1094
1187
|
}
|
|
1095
1188
|
const segment = segmentAt(property, time);
|
|
1096
1189
|
if (segment == null || time < segment.start) {
|
|
@@ -1270,12 +1363,54 @@ export function createAnimator(
|
|
|
1270
1363
|
// Settled curves become resting values.
|
|
1271
1364
|
for (const property of properties) {
|
|
1272
1365
|
const segment = segments.get(property);
|
|
1273
|
-
if (segment != null && time >=
|
|
1366
|
+
if (segment != null && time >= endOf(property, segment)) {
|
|
1274
1367
|
resting[property] = segment.target;
|
|
1275
1368
|
segments.delete(property);
|
|
1276
1369
|
}
|
|
1277
1370
|
}
|
|
1278
1371
|
const moving = properties.filter(p => segments.has(p));
|
|
1372
|
+
if (callGroup == null) {
|
|
1373
|
+
// A call that starts now rebuilds a channel the compositor has moving
|
|
1374
|
+
// since a first frame without a known start. The channel carries on
|
|
1375
|
+
// from that frame, with a start of its own, instead of starting its
|
|
1376
|
+
// curve again.
|
|
1377
|
+
for (const property of moving) {
|
|
1378
|
+
const segment = segments.get(property) as Segment;
|
|
1379
|
+
const latest = shownSince(property, segment);
|
|
1380
|
+
if (latest != null) {
|
|
1381
|
+
leaveWaitingCall(segment, channel, latest);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
// An explicit call can rebuild a channel while one of its curves still
|
|
1386
|
+
// waits for the browser's start. The animation then joins the current
|
|
1387
|
+
// call, or else that waiting call, with no start of its own.
|
|
1388
|
+
const waiting =
|
|
1389
|
+
callGroup ??
|
|
1390
|
+
moving
|
|
1391
|
+
.map(p => (segments.get(p) as Segment).group)
|
|
1392
|
+
.find(group => group != null && !group.resolved) ??
|
|
1393
|
+
null;
|
|
1394
|
+
const rebuiltAfterFirstFrame = waiting?.framed === true;
|
|
1395
|
+
// The new animation starts at the frame that commits it, from its first
|
|
1396
|
+
// keyframe. That holds for a waiting call it joins after that call's
|
|
1397
|
+
// first frame, and for every waiting call it takes a curve from. So this
|
|
1398
|
+
// channel's reads below, and until that frame, take that first value.
|
|
1399
|
+
const restarted = new Set<StartGroup>();
|
|
1400
|
+
if (rebuiltAfterFirstFrame) {
|
|
1401
|
+
restarted.add(waiting);
|
|
1402
|
+
}
|
|
1403
|
+
if (callGroup != null) {
|
|
1404
|
+
for (const property of moving) {
|
|
1405
|
+
const group = (segments.get(property) as Segment).group;
|
|
1406
|
+
if (group != null && group !== callGroup && !group.resolved) {
|
|
1407
|
+
restarted.add(group);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
for (const group of restarted) {
|
|
1412
|
+
group.latest?.delete(channel);
|
|
1413
|
+
}
|
|
1279
1414
|
if (channel === 'translate') {
|
|
1280
1415
|
// The animation built below starts from the value at `time` (its first
|
|
1281
1416
|
// keyframe); a rebase or a retarget builds a new one, so a new start.
|
|
@@ -1308,15 +1443,6 @@ export function createAnimator(
|
|
|
1308
1443
|
previous?.cancel();
|
|
1309
1444
|
return;
|
|
1310
1445
|
}
|
|
1311
|
-
// A channel rebuilt by an explicit call (set, rebase, a retarget of
|
|
1312
|
-
// another axis) while one of its curves still waits for the browser's
|
|
1313
|
-
// start keeps that start: no explicit start for this animation.
|
|
1314
|
-
const waiting =
|
|
1315
|
-
callGroup ??
|
|
1316
|
-
moving
|
|
1317
|
-
.map(p => (segments.get(p) as Segment).group)
|
|
1318
|
-
.find(group => group != null && !group.resolved) ??
|
|
1319
|
-
null;
|
|
1320
1446
|
// The keyframes start where the animation will: at the waiting call's
|
|
1321
1447
|
// own time, which its start moves to the browser's (resolveGroup), so a
|
|
1322
1448
|
// write later in the task keeps the screen on the model's clock.
|
|
@@ -1431,6 +1557,20 @@ export function createAnimator(
|
|
|
1431
1557
|
waiting.channels.add(channel);
|
|
1432
1558
|
// A cancel before the first frame rejects its ready promise.
|
|
1433
1559
|
(animation as Partial<Animation>).ready?.catch(() => {});
|
|
1560
|
+
if (rebuiltAfterFirstFrame) {
|
|
1561
|
+
// The frame that commits it dates this channel's start again. The
|
|
1562
|
+
// call's other channels keep their own.
|
|
1563
|
+
requestFrame?.(() => {
|
|
1564
|
+
if (
|
|
1565
|
+
!waiting.resolved &&
|
|
1566
|
+
running.get(channel) === animation &&
|
|
1567
|
+
animation.startTime == null &&
|
|
1568
|
+
animation.playState === 'running'
|
|
1569
|
+
) {
|
|
1570
|
+
(waiting.latest ??= new Map()).set(channel, now());
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1434
1574
|
} else {
|
|
1435
1575
|
// One clock: the keyframes were sampled from `time`.
|
|
1436
1576
|
animation.startTime = time;
|
|
@@ -1937,10 +2077,14 @@ export function createAnimator(
|
|
|
1937
2077
|
for (const channel of start === 'commit' ? [] : channels) {
|
|
1938
2078
|
for (const property of CHANNELS[channel]) {
|
|
1939
2079
|
const segment = segments.get(property);
|
|
2080
|
+
// A call's first frame puts its channels on screen, even before the
|
|
2081
|
+
// browser reports the start. So such a channel is not at rest either.
|
|
1940
2082
|
if (
|
|
1941
2083
|
segment != null &&
|
|
1942
|
-
time <
|
|
1943
|
-
(segment.group == null ||
|
|
2084
|
+
time < endOf(property, segment) &&
|
|
2085
|
+
(segment.group == null ||
|
|
2086
|
+
segment.group.resolved ||
|
|
2087
|
+
shownSince(property, segment) != null)
|
|
1944
2088
|
) {
|
|
1945
2089
|
return null;
|
|
1946
2090
|
}
|
|
@@ -2135,6 +2279,22 @@ export function createAnimator(
|
|
|
2135
2279
|
) {
|
|
2136
2280
|
resolveGroup(group, now());
|
|
2137
2281
|
}
|
|
2282
|
+
// The browser's start is still unread. The compositor starts the call
|
|
2283
|
+
// near this frame, so each moving channel's reads take the curve from
|
|
2284
|
+
// now until the browser tells its start.
|
|
2285
|
+
if (!group.resolved) {
|
|
2286
|
+
for (const channel of group.channels) {
|
|
2287
|
+
const animation = running.get(channel);
|
|
2288
|
+
if (
|
|
2289
|
+
animation != null &&
|
|
2290
|
+
group.animations.includes(animation) &&
|
|
2291
|
+
animation.startTime == null &&
|
|
2292
|
+
animation.playState === 'running'
|
|
2293
|
+
) {
|
|
2294
|
+
(group.latest ??= new Map()).set(channel, now());
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2138
2298
|
});
|
|
2139
2299
|
ready.then(
|
|
2140
2300
|
() => syncGroup(group),
|
|
@@ -3643,9 +3803,11 @@ export function createAnimator(
|
|
|
3643
3803
|
}
|
|
3644
3804
|
owners.add(segment.owner);
|
|
3645
3805
|
// A curve still waiting for the browser's start stays in its call:
|
|
3646
|
-
// it starts with that call's commit.
|
|
3806
|
+
// it starts with that call's commit. A curve whose call had its first
|
|
3807
|
+
// frame is on screen already, so it leaves the call (leaveWaitingCall).
|
|
3808
|
+
const latest = shownSince(property, segment);
|
|
3647
3809
|
const group =
|
|
3648
|
-
segment.group != null && !segment.group.resolved
|
|
3810
|
+
latest == null && segment.group != null && !segment.group.resolved
|
|
3649
3811
|
? segment.group
|
|
3650
3812
|
: undefined;
|
|
3651
3813
|
const replace = (next: Segment) => {
|
|
@@ -3662,6 +3824,9 @@ export function createAnimator(
|
|
|
3662
3824
|
if (segment.spring != null) {
|
|
3663
3825
|
const from = valueAt(property, time) + delta;
|
|
3664
3826
|
const velocity = velocityAt(property, time);
|
|
3827
|
+
if (latest != null) {
|
|
3828
|
+
leaveWaitingCall(segment, CHANNEL_OF[property], latest);
|
|
3829
|
+
}
|
|
3665
3830
|
const curve = solveSpring(toSpring(segment.spring), {
|
|
3666
3831
|
from,
|
|
3667
3832
|
to: segment.target,
|
|
@@ -3682,6 +3847,9 @@ export function createAnimator(
|
|
|
3682
3847
|
spring: segment.spring,
|
|
3683
3848
|
});
|
|
3684
3849
|
} else {
|
|
3850
|
+
if (latest != null) {
|
|
3851
|
+
leaveWaitingCall(segment, CHANNEL_OF[property], latest);
|
|
3852
|
+
}
|
|
3685
3853
|
replace({
|
|
3686
3854
|
...segment,
|
|
3687
3855
|
target: segment.target + delta,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @file index.ts
|
|
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
|
-
* …), `scroll`, `timing`, `navigation` and the
|
|
7
|
+
* …), `scroll`, `timing`, `navigation`, the `slider` domain and the
|
|
8
8
|
* gesture thresholds (one `UIKIT_*` group per recognizer, their union
|
|
9
9
|
* `UIKIT_GESTURES`, and the haptic tables)
|
|
10
10
|
* @position Entry of the UIKit constants table (motion/uikit). Internal to
|
|
@@ -40,6 +40,7 @@ export {
|
|
|
40
40
|
} from './gestures';
|
|
41
41
|
export {navigation, registerNavigationTable} from './navigation';
|
|
42
42
|
export {registerScrollTable, scroll} from './scroll';
|
|
43
|
+
export {slider} from './slider';
|
|
43
44
|
export {
|
|
44
45
|
ACTION_SHEET_DISMISS_SPRING,
|
|
45
46
|
ACTION_SHEET_PRESENT_SPRING,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file slider.ts
|
|
5
|
+
* @input Nothing: data only
|
|
6
|
+
* @output Exports `slider`: UISlider's hit target, tracking rule, release
|
|
7
|
+
* coast, lift, drop and end haptic, measured on the iOS 26.2 simulator
|
|
8
|
+
* @position The slider domain of the UIKit constants table (motion/uikit).
|
|
9
|
+
* Lengths are in pt (CSS px). Tests check each leaf's `@source`.
|
|
10
|
+
*
|
|
11
|
+
* SYNC: When modified, update:
|
|
12
|
+
* - /packages/core/src/Slider/sliderTouchController.ts
|
|
13
|
+
* - /packages/core/src/Slider/Slider.tsx
|
|
14
|
+
* - /packages/core/src/motion/uikit/slider.test.ts
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {SLIDER_RELEASE_SPRING} from './springs';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* UISlider's touch values: hit target, tracking, release coast, lift, drop
|
|
21
|
+
* and the end-of-track haptic.
|
|
22
|
+
*/
|
|
23
|
+
export const slider = {
|
|
24
|
+
/**
|
|
25
|
+
* The thumb's touch target, a square in pt centered on the thumb. It is
|
|
26
|
+
* the 37 × 24 pt thumb grown by its hit edge insets: 12 pt above and
|
|
27
|
+
* below, 5.5 pt left and right. UIKit hit-tests inside the control's frame
|
|
28
|
+
* first, so the lab's 40 pt frame clipped it to 48 × 40.
|
|
29
|
+
* @source measured: UIKit lab, iOS 26.2 simulator, iPhone 17 Pro.
|
|
30
|
+
* `_thumbHitEdgeInsets` reads {−12, −5.5, −12, −5.5}. Touch-downs on a
|
|
31
|
+
* 4 pt grid take the thumb from −24 to +20 pt in x. A track tap away
|
|
32
|
+
* from the thumb moves nothing. Oct 23 2026
|
|
33
|
+
*/
|
|
34
|
+
thumbHitTarget: 48,
|
|
35
|
+
/**
|
|
36
|
+
* Value range per track length of finger travel. The value moves by
|
|
37
|
+
* (max − min) × translation ÷ the track's full length. The translation
|
|
38
|
+
* counts from the pan's threshold: the touch-down point plus 10 pt along
|
|
39
|
+
* the track. 1 means one full range per track length.
|
|
40
|
+
* @source measured: UIKit lab, iOS 26.2 simulator, iPhone 17 Pro. In
|
|
41
|
+
* drags at 120 to 2000 pt/s, each event's value is v0 plus the
|
|
42
|
+
* translation past 10 pt ÷ 300. Past the max end, the value leaves 1
|
|
43
|
+
* only once the finger comes back past where it reached 1. Oct 23 2026
|
|
44
|
+
*/
|
|
45
|
+
trackingGain: 1,
|
|
46
|
+
/**
|
|
47
|
+
* The coast after the lift. The thumb and the value spring from where the
|
|
48
|
+
* finger left them to the projected target, with the release velocity.
|
|
49
|
+
* Critically damped, it never overshoots a target ahead of it.
|
|
50
|
+
* @source measured: UIKit lab, iOS 26.2 simulator, iPhone 17 Pro. The
|
|
51
|
+
* `UIViewSpringAnimationBehavior` at the lift reads response 0.4 and
|
|
52
|
+
* damping ratio 1. Four releases fit it per frame, at rms 0.11 pt or
|
|
53
|
+
* less. Oct 23 2026
|
|
54
|
+
*/
|
|
55
|
+
releaseSpring: SLIDER_RELEASE_SPRING,
|
|
56
|
+
/**
|
|
57
|
+
* The deceleration rate of the coast's momentum projection, per ms
|
|
58
|
+
* (WWDC18's `project`). It is not UIScrollView's normal rate (0.998), so
|
|
59
|
+
* pass it to the projection explicitly.
|
|
60
|
+
* @source measured: UIKit lab, iOS 26.2 simulator, iPhone 17 Pro. The
|
|
61
|
+
* release behavior's projection deceleration reads 0.996. Oct 23 2026
|
|
62
|
+
*/
|
|
63
|
+
releaseProjectionRate: 0.996,
|
|
64
|
+
/**
|
|
65
|
+
* How much of the projection the coast travels. The target is the lift
|
|
66
|
+
* value plus this share of the projected distance. A finger lifted outside
|
|
67
|
+
* the control's tracking bounds coasts back to the lift value instead.
|
|
68
|
+
* @source measured: UIKit lab, iOS 26.2 simulator, iPhone 17 Pro. The
|
|
69
|
+
* release behavior's target smoothing reads 0.4. A 126.9 pt/s release
|
|
70
|
+
* travels 12.56 pt, against 12.64 predicted. Oct 23 2026
|
|
71
|
+
*/
|
|
72
|
+
releaseProjectionShare: 0.4,
|
|
73
|
+
/**
|
|
74
|
+
* The thumb's growth under a finger, uniform about its center (37 × 24 to
|
|
75
|
+
* 57.35 × 37.2 pt).
|
|
76
|
+
* @source measured: UIKit lab, iOS 26.2 simulator, iPhone 17 Pro. The lens
|
|
77
|
+
* frame settled during an 800 ms hold. Oct 23 2026
|
|
78
|
+
*/
|
|
79
|
+
liftScale: 1.55,
|
|
80
|
+
/**
|
|
81
|
+
* The lift: the thumb grows to `liftScale` when the finger lands.
|
|
82
|
+
* @source measured: UIKit lab, iOS 26.2 simulator, iPhone 17 Pro. The lens
|
|
83
|
+
* width per frame in an 800 ms hold fits one spring at rms 0.31 pt.
|
|
84
|
+
* Oct 23 2026
|
|
85
|
+
*/
|
|
86
|
+
liftSpring: {response: 0.215, dampingRatio: 0.6},
|
|
87
|
+
/**
|
|
88
|
+
* The drop: the thumb shrinks back to its rest size when the finger lifts,
|
|
89
|
+
* while the coast runs.
|
|
90
|
+
* @source measured: UIKit lab, iOS 26.2 simulator, iPhone 17 Pro. The
|
|
91
|
+
* lens's spring at the lift of an 800 ms hold reads response 0.5 s and
|
|
92
|
+
* damping ratio 0.7. The width per frame fits it at rms 0.10 pt.
|
|
93
|
+
* Oct 23 2026
|
|
94
|
+
*/
|
|
95
|
+
dropSpring: {response: 0.5, dampingRatio: 0.7},
|
|
96
|
+
/**
|
|
97
|
+
* The feedback each time the value reaches an end, once per entry. It
|
|
98
|
+
* re-arms when the value leaves the end, in the drag and the coast. UIKit
|
|
99
|
+
* plays "Micro Tap Medium", and the web stand-in is the closest kind.
|
|
100
|
+
* @source measured: UIKit lab, iOS 26.2 simulator, iPhone 17 Pro. The
|
|
101
|
+
* haptics log of drags into both ends shows the fluid slider generator's
|
|
102
|
+
* "Micro Tap Medium" once per entry. Oct 23 2026
|
|
103
|
+
*/
|
|
104
|
+
endHaptic: 'impactMedium',
|
|
105
|
+
} as const;
|
|
@@ -454,6 +454,33 @@ export const KEYBOARD_SPRING = {
|
|
|
454
454
|
*/
|
|
455
455
|
export const BANNER_SPRING = {mass: 3, stiffness: 1000, damping: 500} as const;
|
|
456
456
|
|
|
457
|
+
/**
|
|
458
|
+
* UISlider's release: after a finger lifts, the thumb settles on this spring
|
|
459
|
+
* toward 0.4 of the release's momentum projection. It is critically damped,
|
|
460
|
+
* so it never overshoots a target ahead of it.
|
|
461
|
+
* @source measured: UIKit lab, iOS 26.2 simulator, iPhone 17 Pro. The
|
|
462
|
+
* `UIViewSpringAnimationBehavior` at the lift reads response 0.4 and
|
|
463
|
+
* damping ratio 1. Four releases fit it per frame (the UIKit slider
|
|
464
|
+
* table). Oct 23 2026
|
|
465
|
+
*/
|
|
466
|
+
export const SLIDER_RELEASE_SPRING = {response: 0.4, dampingRatio: 1} as const;
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* UISlider's lift: a finger's touch-down scales the thumb up on this spring.
|
|
470
|
+
* @source measured: UIKit lab, iOS 26.2 simulator, iPhone 17 Pro. The
|
|
471
|
+
* thumb's scale per frame after touch-down (the UIKit slider table).
|
|
472
|
+
* Oct 23 2026
|
|
473
|
+
*/
|
|
474
|
+
export const SLIDER_LIFT_SPRING = {response: 0.215, dampingRatio: 0.6} as const;
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* UISlider's drop: the lift's end scales the thumb back down on this spring.
|
|
478
|
+
* @source measured: UIKit lab, iOS 26.2 simulator, iPhone 17 Pro. The
|
|
479
|
+
* thumb's scale per frame after the lift (the UIKit slider table).
|
|
480
|
+
* Oct 23 2026
|
|
481
|
+
*/
|
|
482
|
+
export const SLIDER_DROP_SPRING = {response: 0.5, dampingRatio: 0.7} as const;
|
|
483
|
+
|
|
457
484
|
/**
|
|
458
485
|
* Every spring above, by row name: the table the existing readers use. A
|
|
459
486
|
* component imports only the rows it plays (`architecture:component-style-authoring`
|
|
@@ -494,10 +521,15 @@ export const springs = {
|
|
|
494
521
|
segmentedThumb: SEGMENTED_THUMB_SPRING,
|
|
495
522
|
keyboard: KEYBOARD_SPRING,
|
|
496
523
|
banner: BANNER_SPRING,
|
|
524
|
+
sliderRelease: SLIDER_RELEASE_SPRING,
|
|
525
|
+
sliderLift: SLIDER_LIFT_SPRING,
|
|
526
|
+
sliderDrop: SLIDER_DROP_SPRING,
|
|
497
527
|
} as const;
|
|
498
528
|
|
|
499
529
|
/**
|
|
500
|
-
* The
|
|
530
|
+
* The only springs that are motion roles. The touch presentations' record
|
|
531
|
+
* names some, and a component's own record names others, such as Slider's
|
|
532
|
+
* three. The generated motion role table lists each with its record.
|
|
501
533
|
* The registry takes these rows alone, so registering ships no other spring.
|
|
502
534
|
*/
|
|
503
535
|
const SPRING_ROLES = {
|
|
@@ -516,10 +548,13 @@ const SPRING_ROLES = {
|
|
|
516
548
|
contextMenuLift: CONTEXT_MENU_LIFT_SPRING,
|
|
517
549
|
contextMenuDismiss: CONTEXT_MENU_DISMISS_SPRING,
|
|
518
550
|
contextMenuMorph: CONTEXT_MENU_MORPH_SPRING,
|
|
551
|
+
sliderRelease: SLIDER_RELEASE_SPRING,
|
|
552
|
+
sliderLift: SLIDER_LIFT_SPRING,
|
|
553
|
+
sliderDrop: SLIDER_DROP_SPRING,
|
|
519
554
|
};
|
|
520
555
|
|
|
521
556
|
/**
|
|
522
|
-
* Registers the springs
|
|
557
|
+
* Registers the role springs (`SPRING_ROLES`) as internal motion roles
|
|
523
558
|
* (motion/roles.ts), by their row names (`springs.settle`). Every other
|
|
524
559
|
* spring, UIKit's menu spring under Reduce Motion and the keyboard's
|
|
525
560
|
* included, is not a role. Nothing registers on import: the touch code that
|