@12-apps/ui 6.41.1 → 6.41.2

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.
@@ -1,16 +1,16 @@
1
1
  "use client";
2
2
  import {
3
3
  SettingsLayout
4
- } from "../../../../chunk-RZ2VZ7SL.js";
4
+ } from "../../../../chunk-JMEPGXJG.js";
5
5
  import "../../../../chunk-TNYKM4UU.js";
6
6
  import "../../../../chunk-V5EWCJBT.js";
7
7
  import "../../../../chunk-BXC6GRLE.js";
8
- import "../../../../chunk-TCNTIGMY.js";
8
+ import "../../../../chunk-TDXEO2UI.js";
9
9
  import "../../../../chunk-6KEXHOIF.js";
10
- import "../../../../chunk-CKHFYTIX.js";
10
+ import "../../../../chunk-TBMGYQJF.js";
11
11
  import "../../../../chunk-DBFRCTI5.js";
12
12
  import "../../../../chunk-NYFNF7I5.js";
13
- import "../../../../chunk-SLJAQA2X.js";
13
+ import "../../../../chunk-GT2RCOHM.js";
14
14
  import "../../../../chunk-HPK33ACY.js";
15
15
  import "../../../../chunk-YKCAJ2TN.js";
16
16
  import "../../../../chunk-PZ5AY32C.js";
@@ -1,12 +1,12 @@
1
1
  "use client";
2
2
  import {
3
3
  SettingsPanel
4
- } from "../../../../chunk-TCNTIGMY.js";
4
+ } from "../../../../chunk-TDXEO2UI.js";
5
5
  import "../../../../chunk-6KEXHOIF.js";
6
- import "../../../../chunk-CKHFYTIX.js";
6
+ import "../../../../chunk-TBMGYQJF.js";
7
7
  import "../../../../chunk-DBFRCTI5.js";
8
8
  import "../../../../chunk-NYFNF7I5.js";
9
- import "../../../../chunk-SLJAQA2X.js";
9
+ import "../../../../chunk-GT2RCOHM.js";
10
10
  import "../../../../chunk-YKCAJ2TN.js";
11
11
  import "../../../../chunk-PZ5AY32C.js";
12
12
  export {
@@ -1,10 +1,10 @@
1
1
  "use client";
2
2
  import {
3
3
  SettingsSectionChips
4
- } from "../../../../chunk-CKHFYTIX.js";
4
+ } from "../../../../chunk-TBMGYQJF.js";
5
5
  import "../../../../chunk-DBFRCTI5.js";
6
6
  import "../../../../chunk-NYFNF7I5.js";
7
- import "../../../../chunk-SLJAQA2X.js";
7
+ import "../../../../chunk-GT2RCOHM.js";
8
8
  import "../../../../chunk-YKCAJ2TN.js";
9
9
  import "../../../../chunk-PZ5AY32C.js";
10
10
  export {
@@ -1,8 +1,14 @@
1
1
  import {
2
+ isRtlStrip,
3
+ physicalScrollLeft,
4
+ toRawScrollLeft,
2
5
  watchVisitorScroll
3
- } from "../../../../chunk-SLJAQA2X.js";
6
+ } from "../../../../chunk-GT2RCOHM.js";
4
7
  import "../../../../chunk-PZ5AY32C.js";
5
8
  export {
9
+ isRtlStrip,
10
+ physicalScrollLeft,
11
+ toRawScrollLeft,
6
12
  watchVisitorScroll
7
13
  };
8
14
  //# sourceMappingURL=SettingsSectionChips.scroll.js.map
@@ -0,0 +1,184 @@
1
+ // src/components/layout/SettingsLayout/SettingsSectionChips.scroll.ts
2
+ var LANDED_SLACK = 1;
3
+ var QUIET_BEFORE_SETTLED_MS = 250;
4
+ var SCROLL_DELTA_EPSILON = 2;
5
+ var POINTER_MOVE_EPSILON = 2;
6
+ function horizontalDelta(event) {
7
+ if (!(event instanceof WheelEvent)) return 0;
8
+ return event.deltaX !== 0 ? event.deltaX : event.shiftKey ? event.deltaY : 0;
9
+ }
10
+ var cachedRtlScrollType;
11
+ function detectRtlScrollType(doc) {
12
+ if (cachedRtlScrollType) return cachedRtlScrollType;
13
+ const probe = doc.createElement("div");
14
+ const inner = doc.createElement("div");
15
+ inner.style.width = "10px";
16
+ probe.appendChild(inner);
17
+ probe.dir = "rtl";
18
+ probe.style.width = "4px";
19
+ probe.style.position = "absolute";
20
+ probe.style.top = "-1000px";
21
+ probe.style.overflow = "scroll";
22
+ doc.body.appendChild(probe);
23
+ let type = "reverse";
24
+ if (probe.scrollLeft > 0) {
25
+ type = "default";
26
+ } else {
27
+ probe.scrollLeft = 1;
28
+ if (probe.scrollLeft === 0) type = "negative";
29
+ }
30
+ doc.body.removeChild(probe);
31
+ cachedRtlScrollType = type;
32
+ return type;
33
+ }
34
+ function isRtlStrip(strip) {
35
+ const view = strip.ownerDocument.defaultView;
36
+ const getComputedStyle = view?.getComputedStyle ?? globalThis.getComputedStyle;
37
+ return getComputedStyle(strip).direction === "rtl";
38
+ }
39
+ function physicalScrollLeft(strip) {
40
+ const raw = strip.scrollLeft;
41
+ if (!isRtlStrip(strip)) return raw;
42
+ const max = strip.scrollWidth - strip.clientWidth;
43
+ const type = detectRtlScrollType(strip.ownerDocument);
44
+ if (type === "negative") return raw + max;
45
+ if (type === "reverse") return max - raw;
46
+ return raw;
47
+ }
48
+ function toRawScrollLeft(strip, physical) {
49
+ if (!isRtlStrip(strip)) return physical;
50
+ const max = strip.scrollWidth - strip.clientWidth;
51
+ const type = detectRtlScrollType(strip.ownerDocument);
52
+ if (type === "negative") return physical - max;
53
+ if (type === "reverse") return max - physical;
54
+ return physical;
55
+ }
56
+ function restsOnAim(strip, aimed) {
57
+ const reachable = Math.min(aimed, strip.scrollWidth - strip.clientWidth);
58
+ return Math.abs(physicalScrollLeft(strip) - reachable) <= LANDED_SLACK;
59
+ }
60
+ function createGestureListeners(state) {
61
+ const onPointerDown = (event) => {
62
+ state.pointer = true;
63
+ state.pointerMoved = false;
64
+ state.pointerDownAt = event instanceof MouseEvent ? event.clientX : Number.NaN;
65
+ };
66
+ const onPointerMove = (event) => {
67
+ if (!(event instanceof MouseEvent) || Number.isNaN(state.pointerDownAt)) return;
68
+ if (Math.abs(event.clientX - state.pointerDownAt) > POINTER_MOVE_EPSILON) state.pointerMoved = true;
69
+ };
70
+ const onPointerUp = () => {
71
+ state.pointer = false;
72
+ state.pointerDownAt = Number.NaN;
73
+ };
74
+ const onTouchStart = (event) => {
75
+ state.touch = true;
76
+ state.touchMoved = false;
77
+ state.touchDownAt = event instanceof TouchEvent ? event.touches[0]?.clientX ?? Number.NaN : Number.NaN;
78
+ };
79
+ const onTouchMove = (event) => {
80
+ if (!(event instanceof TouchEvent) || Number.isNaN(state.touchDownAt)) return;
81
+ const clientX = event.touches[0]?.clientX;
82
+ if (clientX !== void 0 && Math.abs(clientX - state.touchDownAt) > POINTER_MOVE_EPSILON) {
83
+ state.touchMoved = true;
84
+ }
85
+ };
86
+ const onTouchEnd = () => {
87
+ state.touch = false;
88
+ state.touchDownAt = Number.NaN;
89
+ };
90
+ const onPointerCancel = () => {
91
+ state.nudged = true;
92
+ onPointerUp();
93
+ };
94
+ const onKeydown = () => {
95
+ state.nudged = true;
96
+ };
97
+ const onWheel = (event) => {
98
+ if (horizontalDelta(event) !== 0) state.nudged = true;
99
+ };
100
+ return {
101
+ onStrip: [
102
+ ["pointerdown", onPointerDown],
103
+ ["pointermove", onPointerMove],
104
+ ["touchstart", onTouchStart],
105
+ ["touchmove", onTouchMove],
106
+ ["pointercancel", onPointerCancel],
107
+ ["keydown", onKeydown],
108
+ ["wheel", onWheel]
109
+ ],
110
+ // A gesture can END off the strip, so its end is heard on the window.
111
+ onView: [
112
+ ["pointerup", onPointerUp],
113
+ ["pointercancel", onPointerUp],
114
+ ["touchend", onTouchEnd],
115
+ ["touchcancel", onTouchEnd]
116
+ ]
117
+ };
118
+ }
119
+ function watchVisitorScroll(strip) {
120
+ const view = strip.ownerDocument.defaultView;
121
+ const hasScrollEnd = "onscrollend" in strip;
122
+ const state = {
123
+ pointer: false,
124
+ touch: false,
125
+ nudged: false,
126
+ undecided: false,
127
+ scrolled: false,
128
+ aimed: Number.NaN,
129
+ quiet: void 0,
130
+ settled: () => void 0,
131
+ lastScrollLeft: physicalScrollLeft(strip),
132
+ pointerDownAt: Number.NaN,
133
+ pointerMoved: false,
134
+ touchDownAt: Number.NaN,
135
+ touchMoved: false
136
+ };
137
+ const settle = () => {
138
+ if (!state.undecided) return;
139
+ state.undecided = false;
140
+ state.nudged = false;
141
+ if (!restsOnAim(strip, state.aimed)) state.scrolled = true;
142
+ state.settled();
143
+ };
144
+ const onScroll = () => {
145
+ const now = physicalScrollLeft(strip);
146
+ const movedFarEnough = Math.abs(now - state.lastScrollLeft) > SCROLL_DELTA_EPSILON;
147
+ state.lastScrollLeft = now;
148
+ const pointerDrag = state.pointer && state.pointerMoved && movedFarEnough;
149
+ const touchDrag = state.touch && state.touchMoved && movedFarEnough;
150
+ if (pointerDrag || touchDrag || state.nudged) state.undecided = true;
151
+ if (!state.undecided || hasScrollEnd) return;
152
+ clearTimeout(state.quiet);
153
+ state.quiet = setTimeout(settle, QUIET_BEFORE_SETTLED_MS);
154
+ };
155
+ const gestures = createGestureListeners(state);
156
+ const onStrip = [...gestures.onStrip, ["scroll", onScroll], ["scrollend", settle]];
157
+ const onView = gestures.onView;
158
+ for (const [type, listener] of onStrip) strip.addEventListener(type, listener, { passive: true });
159
+ for (const [type, listener] of onView) view?.addEventListener(type, listener, { passive: true });
160
+ return {
161
+ hasScrolled: () => state.scrolled,
162
+ isUndecided: () => state.undecided,
163
+ aimedAt: () => state.aimed,
164
+ beforeOwnScroll: (left) => {
165
+ state.aimed = left;
166
+ },
167
+ onSettled: (listener) => {
168
+ state.settled = listener;
169
+ },
170
+ detach: () => {
171
+ clearTimeout(state.quiet);
172
+ for (const [type, listener] of onStrip) strip.removeEventListener(type, listener);
173
+ for (const [type, listener] of onView) view?.removeEventListener(type, listener);
174
+ }
175
+ };
176
+ }
177
+
178
+ export {
179
+ isRtlStrip,
180
+ physicalScrollLeft,
181
+ toRawScrollLeft,
182
+ watchVisitorScroll
183
+ };
184
+ //# sourceMappingURL=chunk-GT2RCOHM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/layout/SettingsLayout/SettingsSectionChips.scroll.ts"],"sourcesContent":["/**\n * Telling the strip's own scrolls from the visitor's (FUT-2606).\n *\n * Split from `SettingsSectionChips.tsx` so the chips stay a component file and\n * the rule for \"who moved the strip\" can be read in one place.\n */\n\n/**\n * How near to where the strip aimed itself it has to come to rest for the scroll\n * to count as the strip's own. A smooth scroll lands on a device pixel, and the\n * target it was given is fractional.\n */\nconst LANDED_SLACK = 1;\n\n/**\n * Where `scrollend` is missing: how long the strip has to go without a `scroll`\n * before it counts as settled. Generous on purpose — a busy main thread can go\n * several frames between ticks of a smooth scroll, and judging one mid-flight\n * would read the strip's own animation as the visitor's. Too long only delays a\n * re-centre; too short can latch the strip for good.\n */\nconst QUIET_BEFORE_SETTLED_MS = 250;\n\n/**\n * How much `scrollLeft` has to move, tick to tick, before a `scroll` event is\n * even a CANDIDATE for arming (FUT-2775). Below this, a browser's own rounding\n * or a repeated event carrying no real motion cannot arm anything by itself.\n *\n * This alone does not stop the false latch the ticket describes: the strip's\n * own `scrollTo` produces real per-tick deltas well past this line, same as a\n * drag would. It only rules out a scroll event that moved nothing.\n */\nconst SCROLL_DELTA_EPSILON = 2;\n\n/**\n * How far a pointer or touch has to travel from where it went down before it\n * counts as having MOVED, rather than merely resting (FUT-2775). Below this,\n * an held-still finger's sub-pixel tremor does not count as a drag.\n */\nconst POINTER_MOVE_EPSILON = 2;\n\ninterface VisitorScrollWatch {\n /** True once the visitor has moved the strip themselves. */\n hasScrolled: () => boolean;\n /**\n * True while a scroll the visitor MAY be making has not come to rest yet. The\n * strip must not scroll itself under it: that would yank a drag in progress.\n */\n isUndecided: () => boolean;\n /**\n * Where the strip last aimed itself, PHYSICAL pixels (see {@link physicalScrollLeft}) —\n * direction-blind, like every other geometry read in this file. `NaN` before it has.\n */\n aimedAt: () => number;\n /**\n * The strip is about to scroll itself to `left` (physical pixels). Records the\n * aim and NOTHING\n * else: it used to disarm too, and a re-centre landing between a visitor's key\n * press (or wheel, or pan) and the first tick of the scroll it causes wiped\n * the arming — their scroll went unclaimed, read as the strip's own. Left\n * armed, the worst a stale arming does is put the strip's OWN next scroll\n * under judgement, which rests on its aim, is not latched, and disarms.\n */\n beforeOwnScroll: (left: number) => void;\n /** Called once an undecided scroll has come to rest and been judged. */\n onSettled: (listener: () => void) => void;\n detach: () => void;\n}\n\ntype Listeners = Array<[string, (event: Event) => void]>;\n\n/** A wheel's sideways travel: `deltaX`, or `deltaY` with Shift held, as browsers read it. */\nfunction horizontalDelta(event: Event): number {\n if (!(event instanceof WheelEvent)) return 0;\n return event.deltaX !== 0 ? event.deltaX : event.shiftKey ? event.deltaY : 0;\n}\n\ninterface WatchState {\n pointer: boolean;\n touch: boolean;\n nudged: boolean;\n undecided: boolean;\n scrolled: boolean;\n aimed: number;\n quiet: ReturnType<typeof setTimeout> | undefined;\n settled: () => void;\n /** `scrollLeft` as of the last `scroll` tick, updated every tick regardless of arming. */\n lastScrollLeft: number;\n /** Where a `pointerdown` on the strip landed, in viewport `clientX`. `NaN` while up. */\n pointerDownAt: number;\n /** True once that pointer has travelled past {@link POINTER_MOVE_EPSILON} from `pointerDownAt`. */\n pointerMoved: boolean;\n /** Same pair, for `touchstart`/`touchmove` — a distinct event stream on some engines. */\n touchDownAt: number;\n touchMoved: boolean;\n}\n\ntype RtlScrollType = 'default' | 'negative' | 'reverse';\n\nlet cachedRtlScrollType: RtlScrollType | undefined;\n\n/**\n * Which of the three incompatible conventions this engine reports an RTL\n * scroller's `scrollLeft` in, probed once and cached (FUT-2775).\n *\n * Per the CSSOM View spec, an RTL scroller's `scrollLeft` is `0` at its\n * (right-hand) start and goes NEGATIVE toward the end — `'negative'` below,\n * what every current evergreen engine implements. Older WebKit mirrored that\n * (`'reverse'`: `0` at the start, growing toward `+max`), and older\n * Chromium/Safari ignored `dir` altogether and kept the plain left-to-right\n * numbering (`'default'`). A hidden probe, not a version sniff, tells them\n * apart — the same technique `@mui/utils`'s `detectScrollType` uses for\n * `Tabs`; reimplemented here rather than taking a new dependency for one\n * probe.\n */\nfunction detectRtlScrollType(doc: Document): RtlScrollType {\n if (cachedRtlScrollType) return cachedRtlScrollType;\n // The exact pixel values below are the probe's mechanism, not a design size:\n // what matters is only that the INNER box is wider than the OUTER one, so the\n // outer one has something to scroll — same technique `@mui/utils` uses,\n // detached and off-screen either way.\n const probe = doc.createElement('div');\n const inner = doc.createElement('div');\n inner.style.width = '10px';\n probe.appendChild(inner);\n probe.dir = 'rtl';\n probe.style.width = '4px';\n probe.style.position = 'absolute';\n probe.style.top = '-1000px';\n probe.style.overflow = 'scroll';\n doc.body.appendChild(probe);\n let type: RtlScrollType = 'reverse';\n if (probe.scrollLeft > 0) {\n type = 'default';\n } else {\n probe.scrollLeft = 1;\n if (probe.scrollLeft === 0) type = 'negative';\n }\n doc.body.removeChild(probe);\n cachedRtlScrollType = type;\n return type;\n}\n\n/** True when the strip's own computed direction is right-to-left. */\nexport function isRtlStrip(strip: HTMLElement): boolean {\n const view = strip.ownerDocument.defaultView;\n const getComputedStyle = view?.getComputedStyle ?? globalThis.getComputedStyle;\n return getComputedStyle(strip).direction === 'rtl';\n}\n\n/**\n * `strip.scrollLeft`, renumbered so `0` is always the strip's physical LEFT\n * edge and larger is always further physically right — whatever `direction`\n * and whatever convention this engine uses to report it raw (FUT-2775).\n *\n * Every geometry read in this file (`getBoundingClientRect`) is already in\n * that same physical, direction-blind coordinate space, so this is what makes\n * `scrollLeft` combinable with it at all.\n */\nexport function physicalScrollLeft(strip: HTMLElement): number {\n const raw = strip.scrollLeft;\n if (!isRtlStrip(strip)) return raw;\n const max = strip.scrollWidth - strip.clientWidth;\n const type = detectRtlScrollType(strip.ownerDocument);\n if (type === 'negative') return raw + max;\n if (type === 'reverse') return max - raw;\n return raw;\n}\n\n/** The inverse of {@link physicalScrollLeft}: a physical offset back to this engine's raw `scrollLeft`. */\nexport function toRawScrollLeft(strip: HTMLElement, physical: number): number {\n if (!isRtlStrip(strip)) return physical;\n const max = strip.scrollWidth - strip.clientWidth;\n const type = detectRtlScrollType(strip.ownerDocument);\n if (type === 'negative') return physical - max;\n if (type === 'reverse') return max - physical;\n return physical;\n}\n\n/** Whether the strip came to rest where it last aimed itself — clamped as the browser would. */\nfunction restsOnAim(strip: HTMLElement, aimed: number): boolean {\n const reachable = Math.min(aimed, strip.scrollWidth - strip.clientWidth);\n // Written so that `NaN` — the strip never aimed — reads as \"not on aim\".\n return Math.abs(physicalScrollLeft(strip) - reachable) <= LANDED_SLACK;\n}\n\n/** The pointer/touch/keydown/wheel listeners that arm `state`, split out of `watchVisitorScroll` (line budget). */\nfunction createGestureListeners(state: WatchState): { onStrip: Listeners; onView: Listeners } {\n // `MouseEvent`, not `PointerEvent`: a real `PointerEvent` IS one (it extends\n // `MouseEvent`), so this reads `clientX` off it just the same in a browser —\n // but jsdom (this file's own test environment) has never implemented\n // `PointerEvent` at all, and the bare identifier throws a `ReferenceError`\n // the moment anything evaluates `instanceof PointerEvent`, pointer event or\n // not. Same reasoning as feature-checking `scrollTo` elsewhere in this file:\n // nothing in here is worth a crash over which constructor a probe prefers.\n const onPointerDown = (event: Event): void => {\n state.pointer = true;\n state.pointerMoved = false;\n state.pointerDownAt = event instanceof MouseEvent ? event.clientX : Number.NaN;\n };\n const onPointerMove = (event: Event): void => {\n if (!(event instanceof MouseEvent) || Number.isNaN(state.pointerDownAt)) return;\n if (Math.abs(event.clientX - state.pointerDownAt) > POINTER_MOVE_EPSILON) state.pointerMoved = true;\n };\n const onPointerUp = (): void => {\n state.pointer = false;\n state.pointerDownAt = Number.NaN;\n };\n const onTouchStart = (event: Event): void => {\n state.touch = true;\n state.touchMoved = false;\n state.touchDownAt = event instanceof TouchEvent ? (event.touches[0]?.clientX ?? Number.NaN) : Number.NaN;\n };\n const onTouchMove = (event: Event): void => {\n if (!(event instanceof TouchEvent) || Number.isNaN(state.touchDownAt)) return;\n const clientX = event.touches[0]?.clientX;\n if (clientX !== undefined && Math.abs(clientX - state.touchDownAt) > POINTER_MOVE_EPSILON) {\n state.touchMoved = true;\n }\n };\n const onTouchEnd = (): void => {\n state.touch = false;\n state.touchDownAt = Number.NaN;\n };\n const onPointerCancel = (): void => {\n state.nudged = true;\n onPointerUp();\n };\n const onKeydown = (): void => {\n state.nudged = true;\n };\n const onWheel = (event: Event): void => {\n if (horizontalDelta(event) !== 0) state.nudged = true;\n };\n return {\n onStrip: [\n ['pointerdown', onPointerDown],\n ['pointermove', onPointerMove],\n ['touchstart', onTouchStart],\n ['touchmove', onTouchMove],\n ['pointercancel', onPointerCancel],\n ['keydown', onKeydown],\n ['wheel', onWheel],\n ],\n // A gesture can END off the strip, so its end is heard on the window.\n onView: [\n ['pointerup', onPointerUp],\n ['pointercancel', onPointerUp],\n ['touchend', onTouchEnd],\n ['touchcancel', onTouchEnd],\n ],\n };\n}\n\n/**\n * Tell a scroll the VISITOR made from every other one.\n *\n * The `scroll` event cannot say: the strip's own `scrollTo` fires it, and so does\n * the browser clamping a strip whose content shrank. So a scroll is only a\n * CANDIDATE when it arrives during something only a visitor does:\n *\n * - a finger held on the strip (`touchstart` until `touchend`) — a drag;\n * - a pointer held on it (`pointerdown` until `pointerup`);\n * - a key press, a SIDEWAYS wheel, or the browser taking a pointer over to pan\n * it (`pointercancel`), each armed until the next scroll under judgement has\n * been judged — whoever's it turns out to be.\n *\n * and the verdict waits until the strip comes to REST (`scrollend`, or a quiet\n * spell where the browser has no `scrollend`): it is the visitor's only if the\n * strip rests somewhere other than where it last aimed itself.\n *\n * Judged at rest rather than per `scroll` because the strip's own smooth scroll\n * fires `scroll` for a third of a second, and a visitor who presses Tab or puts\n * a finger on a chip to scroll the PAGE in that window is not scrolling the\n * strip. Every tick of that animation used to count as theirs, the strip stopped\n * re-centring for good, and the open chip could be left clipped (FUT-2606\n * again, by another door). A visitor who really drags or wheels the strip\n * mid-animation cancels it, so the strip rests where THEY left it — and that\n * still counts. The one loss: a visitor who happens to leave it within a pixel\n * of the strip's own aim is not told apart from it, and the strip there is\n * already where it would have put itself.\n *\n * A tap is a pointer held with no scroll under it, so it never counts. A\n * vertical wheel scrolls the PAGE — the strip is `overflowY: hidden` — and is\n * not armed at all.\n */\nexport function watchVisitorScroll(strip: HTMLElement): VisitorScrollWatch {\n const view = strip.ownerDocument.defaultView;\n const hasScrollEnd = 'onscrollend' in strip;\n const state: WatchState = {\n pointer: false,\n touch: false,\n nudged: false,\n undecided: false,\n scrolled: false,\n aimed: Number.NaN,\n quiet: undefined,\n settled: () => undefined,\n lastScrollLeft: physicalScrollLeft(strip),\n pointerDownAt: Number.NaN,\n pointerMoved: false,\n touchDownAt: Number.NaN,\n touchMoved: false,\n };\n const settle = (): void => {\n // Only a scroll that was UNDER JUDGEMENT disarms. Chromium can end the\n // strip's own smooth scroll with a `scrollend` when another scroll cuts it\n // off, fired BEFORE that scroll's first `scroll` (seen with an instant one);\n // disarming there would hand the visitor's scroll to nobody.\n if (!state.undecided) return;\n state.undecided = false;\n state.nudged = false;\n if (!restsOnAim(strip, state.aimed)) state.scrolled = true;\n state.settled();\n };\n const onScroll = (): void => {\n const now = physicalScrollLeft(strip);\n const movedFarEnough = Math.abs(now - state.lastScrollLeft) > SCROLL_DELTA_EPSILON;\n state.lastScrollLeft = now;\n // Pointer/touch need BOTH signals (FUT-2775): the pointer/finger must have\n // itself MOVED since going down, AND this tick's delta must be real. Either\n // alone still latches on the other's shape — a pointer merely resting on\n // the strip while its OWN `scrollTo` produces real per-tick deltas would\n // arm under a delta-only check (`pointerMoved`/`touchMoved` rules that out);\n // a pointer that drifts a sub-epsilon amount while resting would arm under\n // a moved-only check with no delta floor (`movedFarEnough` rules that out).\n const pointerDrag = state.pointer && state.pointerMoved && movedFarEnough;\n const touchDrag = state.touch && state.touchMoved && movedFarEnough;\n // `nudged` (a keypress, a sideways wheel, the browser taking the pointer\n // over to pan) is already a discrete, one-shot gesture with no \"resting\n // pointer coincides with an unrelated animation\" shape to rule out — unlike\n // pointer/touch, requiring a delta on the SAME tick that arms it would only\n // race the strip's own scroll ending before a qualifying tick arrives (seen:\n // it broke the existing \"keeps a wheel/drag scroll\" stories against a real\n // browser's `scrollend` timing). So it arms as it always has.\n if (pointerDrag || touchDrag || state.nudged) state.undecided = true;\n if (!state.undecided || hasScrollEnd) return;\n // Every tick while undecided, armed or not: a quiet spell that began while\n // the strip was still moving would judge it mid-flight, away from its aim.\n clearTimeout(state.quiet);\n state.quiet = setTimeout(settle, QUIET_BEFORE_SETTLED_MS);\n };\n const gestures = createGestureListeners(state);\n const onStrip: Listeners = [...gestures.onStrip, ['scroll', onScroll], ['scrollend', settle]];\n const onView = gestures.onView;\n for (const [type, listener] of onStrip) strip.addEventListener(type, listener, { passive: true });\n for (const [type, listener] of onView) view?.addEventListener(type, listener, { passive: true });\n return {\n hasScrolled: () => state.scrolled,\n isUndecided: () => state.undecided,\n aimedAt: () => state.aimed,\n beforeOwnScroll: (left) => {\n state.aimed = left;\n },\n onSettled: (listener) => {\n state.settled = listener;\n },\n detach: () => {\n clearTimeout(state.quiet);\n for (const [type, listener] of onStrip) strip.removeEventListener(type, listener);\n for (const [type, listener] of onView) view?.removeEventListener(type, listener);\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-BXC6GRLE.js";
8
8
  import {
9
9
  SettingsPanel
10
- } from "./chunk-TCNTIGMY.js";
10
+ } from "./chunk-TDXEO2UI.js";
11
11
  import {
12
12
  atLeastRail
13
13
  } from "./chunk-NYFNF7I5.js";
@@ -135,4 +135,4 @@ SettingsLayout.displayName = "SettingsLayout";
135
135
  export {
136
136
  SettingsLayout
137
137
  };
138
- //# sourceMappingURL=chunk-RZ2VZ7SL.js.map
138
+ //# sourceMappingURL=chunk-JMEPGXJG.js.map
@@ -5,8 +5,10 @@ import {
5
5
  TOUCH_TARGET
6
6
  } from "./chunk-NYFNF7I5.js";
7
7
  import {
8
+ physicalScrollLeft,
9
+ toRawScrollLeft,
8
10
  watchVisitorScroll
9
- } from "./chunk-SLJAQA2X.js";
11
+ } from "./chunk-GT2RCOHM.js";
10
12
  import {
11
13
  rem,
12
14
  sxRem
@@ -25,7 +27,7 @@ function prefersReducedMotion() {
25
27
  function centredScrollLeft(strip, chip) {
26
28
  const chipBox = chip.getBoundingClientRect();
27
29
  const visibleStart = strip.getBoundingClientRect().left + strip.clientLeft;
28
- const chipCentre = chipBox.left - visibleStart + strip.scrollLeft + chipBox.width / 2;
30
+ const chipCentre = chipBox.left - visibleStart + physicalScrollLeft(strip) + chipBox.width / 2;
29
31
  const target = chipCentre - strip.clientWidth / 2;
30
32
  return Math.max(0, Math.min(target, strip.scrollWidth - strip.clientWidth));
31
33
  }
@@ -46,7 +48,7 @@ function useCentreActiveChip(stripRef, activeItemId, itemsKey) {
46
48
  const left = centredScrollLeft(strip, chip);
47
49
  if (Math.abs(left - visitor.aimedAt()) < 0.5) return;
48
50
  visitor.beforeOwnScroll(left);
49
- strip.scrollTo({ left, behavior: prefersReducedMotion() ? "auto" : "smooth" });
51
+ strip.scrollTo({ left: toRawScrollLeft(strip, left), behavior: prefersReducedMotion() ? "auto" : "smooth" });
50
52
  };
51
53
  visitor.onSettled(centre);
52
54
  centre();
@@ -149,7 +151,15 @@ function SettingsSectionChips({
149
151
  display: "flex",
150
152
  alignItems: "center",
151
153
  gap: 0.75,
152
- px: 0.25,
154
+ // The start side keeps its old, symmetric 0.25 — nothing sits flush
155
+ // against it once scrolled. The END side is where a scrolled-to-end
156
+ // chip's own border used to sit almost against the clip boundary
157
+ // (FUT-2775): sized like the gap BETWEEN chips (`gap` above) rather
158
+ // than invented fresh, so the last chip clears the edge by the same
159
+ // margin any two chips clear each other. Logical, not `pr`: the side
160
+ // that gets clipped is the strip's END, whichever edge that is.
161
+ paddingInlineStart: 0.25,
162
+ paddingInlineEnd: 0.75,
153
163
  py: 0.25,
154
164
  overflowX: "auto",
155
165
  overflowY: "hidden",
@@ -176,4 +186,4 @@ SettingsSectionChips.displayName = "SettingsSectionChips";
176
186
  export {
177
187
  SettingsSectionChips
178
188
  };
179
- //# sourceMappingURL=chunk-CKHFYTIX.js.map
189
+ //# sourceMappingURL=chunk-TBMGYQJF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/layout/SettingsLayout/SettingsSectionChips.tsx"],"sourcesContent":["'use client';\n\nimport Box from '@mui/material/Box/index.js';\nimport { alpha, useTheme, type Theme } from '@mui/material/styles/index.js';\nimport React, { useEffect, useRef, type RefObject } from 'react';\n\nimport { physicalScrollLeft, toRawScrollLeft, watchVisitorScroll } from './SettingsSectionChips.scroll';\nimport { SettingsStatusMarker } from './SettingsStatusMarker';\nimport { TOUCH_TARGET } from './SettingsLayout.styles';\nimport { rem, sxRem } from '../../../tokens/relative';\n\n/**\n * The chip's drawn height, against `TOUCH_TARGET`'s 44 for the tappable one.\n *\n * The strip sits between a section's header and its first field on the width\n * with the least room for either, so the pill is sized to its text rather than\n * to the thumb — and the thumb is served by the hit area instead.\n */\nconst CHIP_HEIGHT = sxRem(34);\nimport type { SettingsLayoutProps, SettingsNavItem } from './SettingsLayout.types';\n\nexport interface SettingsSectionChipsProps {\n /** The sibling sections to offer — usually the open item's own group. */\n items: SettingsNavItem[];\n /** Which one is open. */\n activeItemId?: string;\n /** Accessible name of the strip, in the host's language. */\n ariaLabel: string;\n linkComponent?: SettingsLayoutProps['linkComponent'];\n onSelectItem?: (id: string) => void;\n testIdPrefix: string;\n}\n\n/** True when the visitor asked the OS for less movement. Re-read per scroll. */\nfunction prefersReducedMotion(): boolean {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;\n return window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n}\n\n/**\n * Where the strip has to scroll for `chip`'s centre to sit on its own, clamped\n * to what the strip can scroll.\n *\n * Measured from the two boxes rather than from `chip.offsetLeft`. `offsetLeft`\n * counts from the nearest POSITIONED ancestor, and the strip is not one — nor\n * are the panel and layout around it — so it counted from whatever box the host\n * happened to position, and every target overshot by the strip's offset inside\n * it. The clamp hid that for the last chip; a middle one was simply off-centre.\n * The boxes are both in viewport coordinates, so their difference is the strip's\n * own, whatever the host wraps it in.\n *\n * Read and returned in PHYSICAL pixels (`physicalScrollLeft`, FUT-2775): the\n * bounding-rect arithmetic above is already direction-blind, and an RTL\n * scroller's raw `scrollLeft` is not — the CSSOM View spec has it `0` at the\n * strip's right-hand start and NEGATIVE past it, not `0`-to-`scrollWidth` like\n * an LTR one. `Math.max(0, …)` alone, against that raw value, would clamp every\n * legitimate RTL target straight back to the start. Physical throughout keeps\n * this comparable to {@link VisitorScrollWatch.aimedAt}; the caller converts to\n * this engine's raw `scrollLeft` (`toRawScrollLeft`) only at the `scrollTo` call.\n */\nfunction centredScrollLeft(strip: HTMLElement, chip: HTMLElement): number {\n const chipBox = chip.getBoundingClientRect();\n const visibleStart = strip.getBoundingClientRect().left + strip.clientLeft;\n const chipCentre = chipBox.left - visibleStart + physicalScrollLeft(strip) + chipBox.width / 2;\n const target = chipCentre - strip.clientWidth / 2;\n return Math.max(0, Math.min(target, strip.scrollWidth - strip.clientWidth));\n}\n\n/** The chip for `id`, walked rather than selected. */\nfunction findChip(strip: HTMLElement, id: string): HTMLElement | undefined {\n // An id is host data, and a section id carrying a quote would turn a selector\n // string into a thrown SyntaxError — which here would take the whole strip\n // down to centre one chip.\n return Array.from(strip.children).find(\n (node): node is HTMLElement => node instanceof HTMLElement && node.dataset.chipId === id,\n );\n}\n\n/**\n * Keep the open section's chip in view, however the visitor got here — and\n * however the strip's content settles after it mounted.\n *\n * Keyed on `activeItemId` in an effect rather than done in a click handler:\n * arriving by the desktop rail, by a card, by a pasted link and by the browser's\n * back button all have to land the same way, and only one of those is a click on\n * a chip. Every re-render otherwise starts the strip at scroll 0, which parks\n * the active chip off the right edge on any list longer than a screen — the\n * visitor is told where they are by an element they cannot see.\n *\n * Centring ONCE is not enough (FUT-2606). A host can widen a chip after mount\n * with the ids unchanged — a status marker whose read resolves late, a web font\n * arriving — and a strip that fitted at mount then overflows with its last chip\n * clipped, because the one centring saw nothing to scroll. So the strip and\n * every chip are watched with a `ResizeObserver`, and a resize re-aims at the\n * active chip when its target has moved.\n *\n * Never against the visitor, though: once they have scrolled the strip\n * themselves, it is theirs until the open section changes\n * ({@link watchVisitorScroll}, which also says why the strip's OWN smooth scroll\n * never counts as theirs). A strip that yanks back mid-drag because a\n * marker loaded is worse than one that does not centre.\n *\n * `itemsKey` rather than `items`: a host almost always rebuilds that array per\n * render (`groups.flatMap(...)`), and depending on it would re-run this on every\n * render and reset the visitor's latch with it.\n *\n * The key joins on NUL — as the ESCAPE `'\\u0000'`, which it must stay. A\n * literal NUL byte works identically at runtime and makes git classify this file\n * as BINARY: `git diff` prints \"Binary file not shown\" and the GitHub review UI\n * shows nothing at all. This file shipped that way once, and the cost was not\n * the byte — it was a whole change nobody could read.\n */\nfunction useCentreActiveChip(\n stripRef: RefObject<HTMLDivElement | null>,\n activeItemId: string | undefined,\n itemsKey: string,\n): void {\n useEffect(() => {\n const strip = stripRef.current;\n if (!strip || !activeItemId) return undefined;\n // Feature-checked, not assumed. `Element.scrollTo` is absent in jsdom and in\n // a few embedded webviews, and an unguarded call there THROWS out of an\n // effect — which React escalates to the nearest error boundary. The whole\n // settings area would go down to centre one chip, and the host's own tests\n // could not render the shell at all. Same reasoning as walking the children\n // instead of building a selector: nothing in here is worth a crash.\n if (typeof strip.scrollTo !== 'function') return undefined;\n const visitor = watchVisitorScroll(strip);\n const centre = (): void => {\n const chip = findChip(strip, activeItemId);\n // Not while a scroll that may be the visitor's is still moving: a resize\n // mid-drag must not yank the strip. It is re-tried once that scroll rests.\n if (!chip || visitor.hasScrolled() || visitor.isUndecided()) return;\n // Physical pixels throughout (FUT-2775) — comparable to `aimedAt()` and to\n // what `beforeOwnScroll` records; converted to this engine's raw\n // `scrollLeft` only for the actual `scrollTo` call below.\n const left = centredScrollLeft(strip, chip);\n // Only when the target MOVED: a resize that leaves it where it was must not\n // restart a smooth scroll that is already on its way there. The aim is a\n // runtime-computed scroll offset, like the exempt `left` below — not a size.\n if (Math.abs(left - visitor.aimedAt()) < 0.5) return;\n visitor.beforeOwnScroll(left);\n strip.scrollTo({ left: toRawScrollLeft(strip, left), behavior: prefersReducedMotion() ? 'auto' : 'smooth' });\n };\n visitor.onSettled(centre);\n centre();\n // Also feature-checked: without it the strip still centres once, as before.\n const resizes = typeof ResizeObserver === 'function' ? new ResizeObserver(centre) : undefined;\n resizes?.observe(strip);\n for (const child of Array.from(strip.children)) resizes?.observe(child);\n return () => {\n resizes?.disconnect();\n visitor.detach();\n };\n }, [stripRef, activeItemId, itemsKey]);\n}\n\n/** One chip: a link when the host gave it a target, else a button. */\nfunction SectionChip({\n item,\n active,\n theme,\n linkComponent,\n onSelectItem,\n testIdPrefix,\n}: {\n item: SettingsNavItem;\n active: boolean;\n theme: Theme;\n linkComponent?: SettingsLayoutProps['linkComponent'];\n onSelectItem?: (id: string) => void;\n testIdPrefix: string;\n}): React.JSX.Element {\n const asLink = item.href !== undefined && linkComponent !== undefined;\n const behaviour = asLink\n ? { component: linkComponent, href: item.href }\n : { component: 'button' as const, type: 'button' as const, onClick: () => onSelectItem?.(item.id) };\n\n return (\n <Box\n {...behaviour}\n data-chip-id={item.id}\n data-testid={`${testIdPrefix}-chip-${item.id}`}\n aria-current={active ? 'page' : undefined}\n sx={{\n display: 'inline-flex',\n alignItems: 'center',\n gap: 0.75,\n flex: '0 0 auto',\n position: 'relative',\n // The chip LOOKS this tall; it is still tapped at `TOUCH_TARGET`.\n //\n // Shrinking `minHeight` alone would have bought a slimmer strip with a\n // smaller hit area — on the one width where the strip IS the navigation\n // and the input is a thumb. The `::after` below keeps the tappable\n // region at the full target while the pill itself takes less room, so\n // this is a visual change and not an accessibility trade.\n minHeight: CHIP_HEIGHT,\n px: 1.5,\n borderRadius: 999,\n cursor: 'pointer',\n font: 'inherit',\n fontSize: rem(theme, 13),\n fontWeight: active ? 700 : 500,\n textDecoration: 'none',\n whiteSpace: 'nowrap',\n color: active ? 'primary.main' : 'text.secondary',\n bgcolor: active ? alpha(theme.palette.primary.main, 0.12) : 'transparent',\n border: `1px solid ${active ? alpha(theme.palette.primary.main, 0.4) : theme.palette.divider}`,\n // Spans the chip's width only, so neighbours never overlap: the strip\n // is a row, and the extra reach is vertical.\n '&::after': {\n content: '\"\"',\n position: 'absolute',\n insetInlineStart: 0,\n insetInlineEnd: 0,\n top: '50%',\n transform: 'translateY(-50%)',\n height: rem(theme, TOUCH_TARGET),\n },\n }}\n >\n {item.status ? (\n <SettingsStatusMarker\n status={item.status}\n label={item.statusLabel}\n testId={`${testIdPrefix}-chip-status-${item.id}`}\n />\n ) : null}\n {item.label}\n </Box>\n );\n}\n\n/**\n * The narrow-width section strip: horizontally scrollable chips for the sections\n * beside the open one.\n *\n * It clips its own overflow — `overflow-x: auto` on the strip, never on an\n * ancestor. A strip that pushes the page wide makes every OTHER screen at that\n * width scroll sideways too, and the cause is nowhere near the symptom.\n */\nexport function SettingsSectionChips({\n items,\n activeItemId,\n ariaLabel,\n linkComponent,\n onSelectItem,\n testIdPrefix,\n}: SettingsSectionChipsProps): React.JSX.Element {\n const theme = useTheme();\n const stripRef = useRef<HTMLDivElement | null>(null);\n useCentreActiveChip(stripRef, activeItemId, items.map((item) => item.id).join('\\u0000'));\n\n return (\n <Box\n ref={stripRef}\n component=\"nav\"\n aria-label={ariaLabel}\n data-testid={`${testIdPrefix}-chips`}\n sx={{\n display: 'flex',\n alignItems: 'center',\n gap: 0.75,\n // The start side keeps its old, symmetric 0.25 — nothing sits flush\n // against it once scrolled. The END side is where a scrolled-to-end\n // chip's own border used to sit almost against the clip boundary\n // (FUT-2775): sized like the gap BETWEEN chips (`gap` above) rather\n // than invented fresh, so the last chip clears the edge by the same\n // margin any two chips clear each other. Logical, not `pr`: the side\n // that gets clipped is the strip's END, whichever edge that is.\n paddingInlineStart: 0.25,\n paddingInlineEnd: 0.75,\n py: 0.25,\n overflowX: 'auto',\n overflowY: 'hidden',\n scrollbarWidth: 'none',\n '&::-webkit-scrollbar': { display: 'none' },\n }}\n >\n {items.map((item) => (\n <SectionChip\n key={item.id}\n item={item}\n active={item.id === activeItemId}\n theme={theme}\n linkComponent={linkComponent}\n onSelectItem={onSelectItem}\n testIdPrefix={testIdPrefix}\n />\n ))}\n </Box>\n );\n}\n\nSettingsSectionChips.displayName = 'SettingsSectionChips';\n"],"mappings":";;;;;;;;;;;;;;;;;AAEA,OAAO,SAAS;AAChB,SAAS,OAAO,gBAA4B;AAC5C,SAAgB,WAAW,cAA8B;AA+KrD,SA4CI,KA5CJ;AAjKJ,IAAM,cAAc,MAAM,EAAE;AAgB5B,SAAS,uBAAgC;AACvC,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,WAAY,QAAO;AACrF,SAAO,OAAO,WAAW,kCAAkC,EAAE;AAC/D;AAuBA,SAAS,kBAAkB,OAAoB,MAA2B;AACxE,QAAM,UAAU,KAAK,sBAAsB;AAC3C,QAAM,eAAe,MAAM,sBAAsB,EAAE,OAAO,MAAM;AAChE,QAAM,aAAa,QAAQ,OAAO,eAAe,mBAAmB,KAAK,IAAI,QAAQ,QAAQ;AAC7F,QAAM,SAAS,aAAa,MAAM,cAAc;AAChD,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,QAAQ,MAAM,cAAc,MAAM,WAAW,CAAC;AAC5E;AAGA,SAAS,SAAS,OAAoB,IAAqC;AAIzE,SAAO,MAAM,KAAK,MAAM,QAAQ,EAAE;AAAA,IAChC,CAAC,SAA8B,gBAAgB,eAAe,KAAK,QAAQ,WAAW;AAAA,EACxF;AACF;AAoCA,SAAS,oBACP,UACA,cACA,UACM;AACN,YAAU,MAAM;AACd,UAAM,QAAQ,SAAS;AACvB,QAAI,CAAC,SAAS,CAAC,aAAc,QAAO;AAOpC,QAAI,OAAO,MAAM,aAAa,WAAY,QAAO;AACjD,UAAM,UAAU,mBAAmB,KAAK;AACxC,UAAM,SAAS,MAAY;AACzB,YAAM,OAAO,SAAS,OAAO,YAAY;AAGzC,UAAI,CAAC,QAAQ,QAAQ,YAAY,KAAK,QAAQ,YAAY,EAAG;AAI7D,YAAM,OAAO,kBAAkB,OAAO,IAAI;AAI1C,UAAI,KAAK,IAAI,OAAO,QAAQ,QAAQ,CAAC,IAAI,IAAK;AAC9C,cAAQ,gBAAgB,IAAI;AAC5B,YAAM,SAAS,EAAE,MAAM,gBAAgB,OAAO,IAAI,GAAG,UAAU,qBAAqB,IAAI,SAAS,SAAS,CAAC;AAAA,IAC7G;AACA,YAAQ,UAAU,MAAM;AACxB,WAAO;AAEP,UAAM,UAAU,OAAO,mBAAmB,aAAa,IAAI,eAAe,MAAM,IAAI;AACpF,aAAS,QAAQ,KAAK;AACtB,eAAW,SAAS,MAAM,KAAK,MAAM,QAAQ,EAAG,UAAS,QAAQ,KAAK;AACtE,WAAO,MAAM;AACX,eAAS,WAAW;AACpB,cAAQ,OAAO;AAAA,IACjB;AAAA,EACF,GAAG,CAAC,UAAU,cAAc,QAAQ,CAAC;AACvC;AAGA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOsB;AACpB,QAAM,SAAS,KAAK,SAAS,UAAa,kBAAkB;AAC5D,QAAM,YAAY,SACd,EAAE,WAAW,eAAe,MAAM,KAAK,KAAK,IAC5C,EAAE,WAAW,UAAmB,MAAM,UAAmB,SAAS,MAAM,eAAe,KAAK,EAAE,EAAE;AAEpG,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,gBAAc,KAAK;AAAA,MACnB,eAAa,GAAG,YAAY,SAAS,KAAK,EAAE;AAAA,MAC5C,gBAAc,SAAS,SAAS;AAAA,MAChC,IAAI;AAAA,QACF,SAAS;AAAA,QACT,YAAY;AAAA,QACZ,KAAK;AAAA,QACL,MAAM;AAAA,QACN,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAQV,WAAW;AAAA,QACX,IAAI;AAAA,QACJ,cAAc;AAAA,QACd,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,UAAU,IAAI,OAAO,EAAE;AAAA,QACvB,YAAY,SAAS,MAAM;AAAA,QAC3B,gBAAgB;AAAA,QAChB,YAAY;AAAA,QACZ,OAAO,SAAS,iBAAiB;AAAA,QACjC,SAAS,SAAS,MAAM,MAAM,QAAQ,QAAQ,MAAM,IAAI,IAAI;AAAA,QAC5D,QAAQ,aAAa,SAAS,MAAM,MAAM,QAAQ,QAAQ,MAAM,GAAG,IAAI,MAAM,QAAQ,OAAO;AAAA;AAAA;AAAA,QAG5F,YAAY;AAAA,UACV,SAAS;AAAA,UACT,UAAU;AAAA,UACV,kBAAkB;AAAA,UAClB,gBAAgB;AAAA,UAChB,KAAK;AAAA,UACL,WAAW;AAAA,UACX,QAAQ,IAAI,OAAO,YAAY;AAAA,QACjC;AAAA,MACF;AAAA,MAEC;AAAA,aAAK,SACJ;AAAA,UAAC;AAAA;AAAA,YACC,QAAQ,KAAK;AAAA,YACb,OAAO,KAAK;AAAA,YACZ,QAAQ,GAAG,YAAY,gBAAgB,KAAK,EAAE;AAAA;AAAA,QAChD,IACE;AAAA,QACH,KAAK;AAAA;AAAA;AAAA,EACR;AAEJ;AAUO,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAiD;AAC/C,QAAM,QAAQ,SAAS;AACvB,QAAM,WAAW,OAA8B,IAAI;AACnD,sBAAoB,UAAU,cAAc,MAAM,IAAI,CAAC,SAAS,KAAK,EAAE,EAAE,KAAK,IAAQ,CAAC;AAEvF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL,WAAU;AAAA,MACV,cAAY;AAAA,MACZ,eAAa,GAAG,YAAY;AAAA,MAC5B,IAAI;AAAA,QACF,SAAS;AAAA,QACT,YAAY;AAAA,QACZ,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAQL,oBAAoB;AAAA,QACpB,kBAAkB;AAAA,QAClB,IAAI;AAAA,QACJ,WAAW;AAAA,QACX,WAAW;AAAA,QACX,gBAAgB;AAAA,QAChB,wBAAwB,EAAE,SAAS,OAAO;AAAA,MAC5C;AAAA,MAEC,gBAAM,IAAI,CAAC,SACV;AAAA,QAAC;AAAA;AAAA,UAEC;AAAA,UACA,QAAQ,KAAK,OAAO;AAAA,UACpB;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA;AAAA,QANK,KAAK;AAAA,MAOZ,CACD;AAAA;AAAA,EACH;AAEJ;AAEA,qBAAqB,cAAc;","names":[]}
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-6KEXHOIF.js";
4
4
  import {
5
5
  SettingsSectionChips
6
- } from "./chunk-CKHFYTIX.js";
6
+ } from "./chunk-TBMGYQJF.js";
7
7
  import {
8
8
  TOUCH_TARGET,
9
9
  atLeastRail,
@@ -167,4 +167,4 @@ function SettingsPanel({
167
167
  export {
168
168
  SettingsPanel
169
169
  };
170
- //# sourceMappingURL=chunk-TCNTIGMY.js.map
170
+ //# sourceMappingURL=chunk-TDXEO2UI.js.map
@@ -1,19 +1,19 @@
1
1
  import {
2
2
  SettingsLayout
3
- } from "../chunk-RZ2VZ7SL.js";
3
+ } from "../chunk-JMEPGXJG.js";
4
4
  import "../chunk-TNYKM4UU.js";
5
5
  import "../chunk-V5EWCJBT.js";
6
6
  import "../chunk-BXC6GRLE.js";
7
- import "../chunk-TCNTIGMY.js";
7
+ import "../chunk-TDXEO2UI.js";
8
8
  import "../chunk-6KEXHOIF.js";
9
9
  import {
10
10
  SettingsSectionChips
11
- } from "../chunk-CKHFYTIX.js";
11
+ } from "../chunk-TBMGYQJF.js";
12
12
  import {
13
13
  SettingsStatusMarker
14
14
  } from "../chunk-DBFRCTI5.js";
15
15
  import "../chunk-NYFNF7I5.js";
16
- import "../chunk-SLJAQA2X.js";
16
+ import "../chunk-GT2RCOHM.js";
17
17
  import "../chunk-HPK33ACY.js";
18
18
  import "../chunk-YKCAJ2TN.js";
19
19
  import "../chunk-PZ5AY32C.js";
@@ -1 +1 @@
1
- {"version":3,"file":"SettingsSectionChips.d.ts","sourceRoot":"","sources":["../../../../../src/components/layout/SettingsLayout/SettingsSectionChips.tsx"],"names":[],"mappings":"AAIA,OAAO,KAA4C,MAAM,OAAO,CAAC;AAejE,OAAO,KAAK,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEnF,MAAM,WAAW,yBAAyB;IACxC,yEAAyE;IACzE,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,yBAAyB;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,mBAAmB,CAAC,eAAe,CAAC,CAAC;IACrD,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,YAAY,EAAE,MAAM,CAAC;CACtB;AA+LD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,YAAY,EACZ,SAAS,EACT,aAAa,EACb,YAAY,EACZ,YAAY,GACb,EAAE,yBAAyB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAoC/C;yBA3Ce,oBAAoB"}
1
+ {"version":3,"file":"SettingsSectionChips.d.ts","sourceRoot":"","sources":["../../../../../src/components/layout/SettingsLayout/SettingsSectionChips.tsx"],"names":[],"mappings":"AAIA,OAAO,KAA4C,MAAM,OAAO,CAAC;AAejE,OAAO,KAAK,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEnF,MAAM,WAAW,yBAAyB;IACxC,yEAAyE;IACzE,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,yBAAyB;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,mBAAmB,CAAC,eAAe,CAAC,CAAC;IACrD,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,YAAY,EAAE,MAAM,CAAC;CACtB;AA2MD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,YAAY,EACZ,SAAS,EACT,aAAa,EACb,YAAY,EACZ,YAAY,GACb,EAAE,yBAAyB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CA4C/C;yBAnDe,oBAAoB"}
@@ -12,10 +12,14 @@ interface VisitorScrollWatch {
12
12
  * strip must not scroll itself under it: that would yank a drag in progress.
13
13
  */
14
14
  isUndecided: () => boolean;
15
- /** Where the strip last aimed itself, in its own pixels; `NaN` before it has. */
15
+ /**
16
+ * Where the strip last aimed itself, PHYSICAL pixels (see {@link physicalScrollLeft}) —
17
+ * direction-blind, like every other geometry read in this file. `NaN` before it has.
18
+ */
16
19
  aimedAt: () => number;
17
20
  /**
18
- * The strip is about to scroll itself to `left`. Records the aim and NOTHING
21
+ * The strip is about to scroll itself to `left` (physical pixels). Records the
22
+ * aim and NOTHING
19
23
  * else: it used to disarm too, and a re-centre landing between a visitor's key
20
24
  * press (or wheel, or pan) and the first tick of the scroll it causes wiped
21
25
  * the arming — their scroll went unclaimed, read as the strip's own. Left
@@ -27,6 +31,20 @@ interface VisitorScrollWatch {
27
31
  onSettled: (listener: () => void) => void;
28
32
  detach: () => void;
29
33
  }
34
+ /** True when the strip's own computed direction is right-to-left. */
35
+ export declare function isRtlStrip(strip: HTMLElement): boolean;
36
+ /**
37
+ * `strip.scrollLeft`, renumbered so `0` is always the strip's physical LEFT
38
+ * edge and larger is always further physically right — whatever `direction`
39
+ * and whatever convention this engine uses to report it raw (FUT-2775).
40
+ *
41
+ * Every geometry read in this file (`getBoundingClientRect`) is already in
42
+ * that same physical, direction-blind coordinate space, so this is what makes
43
+ * `scrollLeft` combinable with it at all.
44
+ */
45
+ export declare function physicalScrollLeft(strip: HTMLElement): number;
46
+ /** The inverse of {@link physicalScrollLeft}: a physical offset back to this engine's raw `scrollLeft`. */
47
+ export declare function toRawScrollLeft(strip: HTMLElement, physical: number): number;
30
48
  /**
31
49
  * Tell a scroll the VISITOR made from every other one.
32
50
  *
@@ -1 +1 @@
1
- {"version":3,"file":"SettingsSectionChips.scroll.d.ts","sourceRoot":"","sources":["../../../../../src/components/layout/SettingsLayout/SettingsSectionChips.scroll.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAkBH,UAAU,kBAAkB;IAC1B,4DAA4D;IAC5D,WAAW,EAAE,MAAM,OAAO,CAAC;IAC3B;;;OAGG;IACH,WAAW,EAAE,MAAM,OAAO,CAAC;IAC3B,iFAAiF;IACjF,OAAO,EAAE,MAAM,MAAM,CAAC;IACtB;;;;;;;OAOG;IACH,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,wEAAwE;IACxE,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IAC1C,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AA4BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,kBAAkB,CA0EzE"}
1
+ {"version":3,"file":"SettingsSectionChips.scroll.d.ts","sourceRoot":"","sources":["../../../../../src/components/layout/SettingsLayout/SettingsSectionChips.scroll.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoCH,UAAU,kBAAkB;IAC1B,4DAA4D;IAC5D,WAAW,EAAE,MAAM,OAAO,CAAC;IAC3B;;;OAGG;IACH,WAAW,EAAE,MAAM,OAAO,CAAC;IAC3B;;;OAGG;IACH,OAAO,EAAE,MAAM,MAAM,CAAC;IACtB;;;;;;;;OAQG;IACH,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,wEAAwE;IACxE,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IAC1C,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AA4ED,qEAAqE;AACrE,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAItD;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAQ7D;AAED,2GAA2G;AAC3G,wBAAgB,eAAe,CAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAO5E;AA6ED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,kBAAkB,CA6EzE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/ui",
3
- "version": "6.41.1",
3
+ "version": "6.41.2",
4
4
  "license": "MIT",
5
5
  "packageManager": "pnpm@10.34.5",
6
6
  "type": "module",
@@ -21,6 +21,24 @@ const LANDED_SLACK = 1;
21
21
  */
22
22
  const QUIET_BEFORE_SETTLED_MS = 250;
23
23
 
24
+ /**
25
+ * How much `scrollLeft` has to move, tick to tick, before a `scroll` event is
26
+ * even a CANDIDATE for arming (FUT-2775). Below this, a browser's own rounding
27
+ * or a repeated event carrying no real motion cannot arm anything by itself.
28
+ *
29
+ * This alone does not stop the false latch the ticket describes: the strip's
30
+ * own `scrollTo` produces real per-tick deltas well past this line, same as a
31
+ * drag would. It only rules out a scroll event that moved nothing.
32
+ */
33
+ const SCROLL_DELTA_EPSILON = 2;
34
+
35
+ /**
36
+ * How far a pointer or touch has to travel from where it went down before it
37
+ * counts as having MOVED, rather than merely resting (FUT-2775). Below this,
38
+ * an held-still finger's sub-pixel tremor does not count as a drag.
39
+ */
40
+ const POINTER_MOVE_EPSILON = 2;
41
+
24
42
  interface VisitorScrollWatch {
25
43
  /** True once the visitor has moved the strip themselves. */
26
44
  hasScrolled: () => boolean;
@@ -29,10 +47,14 @@ interface VisitorScrollWatch {
29
47
  * strip must not scroll itself under it: that would yank a drag in progress.
30
48
  */
31
49
  isUndecided: () => boolean;
32
- /** Where the strip last aimed itself, in its own pixels; `NaN` before it has. */
50
+ /**
51
+ * Where the strip last aimed itself, PHYSICAL pixels (see {@link physicalScrollLeft}) —
52
+ * direction-blind, like every other geometry read in this file. `NaN` before it has.
53
+ */
33
54
  aimedAt: () => number;
34
55
  /**
35
- * The strip is about to scroll itself to `left`. Records the aim and NOTHING
56
+ * The strip is about to scroll itself to `left` (physical pixels). Records the
57
+ * aim and NOTHING
36
58
  * else: it used to disarm too, and a re-centre landing between a visitor's key
37
59
  * press (or wheel, or pan) and the first tick of the scroll it causes wiped
38
60
  * the arming — their scroll went unclaimed, read as the strip's own. Left
@@ -62,13 +84,172 @@ interface WatchState {
62
84
  aimed: number;
63
85
  quiet: ReturnType<typeof setTimeout> | undefined;
64
86
  settled: () => void;
87
+ /** `scrollLeft` as of the last `scroll` tick, updated every tick regardless of arming. */
88
+ lastScrollLeft: number;
89
+ /** Where a `pointerdown` on the strip landed, in viewport `clientX`. `NaN` while up. */
90
+ pointerDownAt: number;
91
+ /** True once that pointer has travelled past {@link POINTER_MOVE_EPSILON} from `pointerDownAt`. */
92
+ pointerMoved: boolean;
93
+ /** Same pair, for `touchstart`/`touchmove` — a distinct event stream on some engines. */
94
+ touchDownAt: number;
95
+ touchMoved: boolean;
96
+ }
97
+
98
+ type RtlScrollType = 'default' | 'negative' | 'reverse';
99
+
100
+ let cachedRtlScrollType: RtlScrollType | undefined;
101
+
102
+ /**
103
+ * Which of the three incompatible conventions this engine reports an RTL
104
+ * scroller's `scrollLeft` in, probed once and cached (FUT-2775).
105
+ *
106
+ * Per the CSSOM View spec, an RTL scroller's `scrollLeft` is `0` at its
107
+ * (right-hand) start and goes NEGATIVE toward the end — `'negative'` below,
108
+ * what every current evergreen engine implements. Older WebKit mirrored that
109
+ * (`'reverse'`: `0` at the start, growing toward `+max`), and older
110
+ * Chromium/Safari ignored `dir` altogether and kept the plain left-to-right
111
+ * numbering (`'default'`). A hidden probe, not a version sniff, tells them
112
+ * apart — the same technique `@mui/utils`'s `detectScrollType` uses for
113
+ * `Tabs`; reimplemented here rather than taking a new dependency for one
114
+ * probe.
115
+ */
116
+ function detectRtlScrollType(doc: Document): RtlScrollType {
117
+ if (cachedRtlScrollType) return cachedRtlScrollType;
118
+ // The exact pixel values below are the probe's mechanism, not a design size:
119
+ // what matters is only that the INNER box is wider than the OUTER one, so the
120
+ // outer one has something to scroll — same technique `@mui/utils` uses,
121
+ // detached and off-screen either way.
122
+ const probe = doc.createElement('div');
123
+ const inner = doc.createElement('div');
124
+ inner.style.width = '10px';
125
+ probe.appendChild(inner);
126
+ probe.dir = 'rtl';
127
+ probe.style.width = '4px';
128
+ probe.style.position = 'absolute';
129
+ probe.style.top = '-1000px';
130
+ probe.style.overflow = 'scroll';
131
+ doc.body.appendChild(probe);
132
+ let type: RtlScrollType = 'reverse';
133
+ if (probe.scrollLeft > 0) {
134
+ type = 'default';
135
+ } else {
136
+ probe.scrollLeft = 1;
137
+ if (probe.scrollLeft === 0) type = 'negative';
138
+ }
139
+ doc.body.removeChild(probe);
140
+ cachedRtlScrollType = type;
141
+ return type;
142
+ }
143
+
144
+ /** True when the strip's own computed direction is right-to-left. */
145
+ export function isRtlStrip(strip: HTMLElement): boolean {
146
+ const view = strip.ownerDocument.defaultView;
147
+ const getComputedStyle = view?.getComputedStyle ?? globalThis.getComputedStyle;
148
+ return getComputedStyle(strip).direction === 'rtl';
149
+ }
150
+
151
+ /**
152
+ * `strip.scrollLeft`, renumbered so `0` is always the strip's physical LEFT
153
+ * edge and larger is always further physically right — whatever `direction`
154
+ * and whatever convention this engine uses to report it raw (FUT-2775).
155
+ *
156
+ * Every geometry read in this file (`getBoundingClientRect`) is already in
157
+ * that same physical, direction-blind coordinate space, so this is what makes
158
+ * `scrollLeft` combinable with it at all.
159
+ */
160
+ export function physicalScrollLeft(strip: HTMLElement): number {
161
+ const raw = strip.scrollLeft;
162
+ if (!isRtlStrip(strip)) return raw;
163
+ const max = strip.scrollWidth - strip.clientWidth;
164
+ const type = detectRtlScrollType(strip.ownerDocument);
165
+ if (type === 'negative') return raw + max;
166
+ if (type === 'reverse') return max - raw;
167
+ return raw;
168
+ }
169
+
170
+ /** The inverse of {@link physicalScrollLeft}: a physical offset back to this engine's raw `scrollLeft`. */
171
+ export function toRawScrollLeft(strip: HTMLElement, physical: number): number {
172
+ if (!isRtlStrip(strip)) return physical;
173
+ const max = strip.scrollWidth - strip.clientWidth;
174
+ const type = detectRtlScrollType(strip.ownerDocument);
175
+ if (type === 'negative') return physical - max;
176
+ if (type === 'reverse') return max - physical;
177
+ return physical;
65
178
  }
66
179
 
67
180
  /** Whether the strip came to rest where it last aimed itself — clamped as the browser would. */
68
181
  function restsOnAim(strip: HTMLElement, aimed: number): boolean {
69
182
  const reachable = Math.min(aimed, strip.scrollWidth - strip.clientWidth);
70
183
  // Written so that `NaN` — the strip never aimed — reads as "not on aim".
71
- return Math.abs(strip.scrollLeft - reachable) <= LANDED_SLACK;
184
+ return Math.abs(physicalScrollLeft(strip) - reachable) <= LANDED_SLACK;
185
+ }
186
+
187
+ /** The pointer/touch/keydown/wheel listeners that arm `state`, split out of `watchVisitorScroll` (line budget). */
188
+ function createGestureListeners(state: WatchState): { onStrip: Listeners; onView: Listeners } {
189
+ // `MouseEvent`, not `PointerEvent`: a real `PointerEvent` IS one (it extends
190
+ // `MouseEvent`), so this reads `clientX` off it just the same in a browser —
191
+ // but jsdom (this file's own test environment) has never implemented
192
+ // `PointerEvent` at all, and the bare identifier throws a `ReferenceError`
193
+ // the moment anything evaluates `instanceof PointerEvent`, pointer event or
194
+ // not. Same reasoning as feature-checking `scrollTo` elsewhere in this file:
195
+ // nothing in here is worth a crash over which constructor a probe prefers.
196
+ const onPointerDown = (event: Event): void => {
197
+ state.pointer = true;
198
+ state.pointerMoved = false;
199
+ state.pointerDownAt = event instanceof MouseEvent ? event.clientX : Number.NaN;
200
+ };
201
+ const onPointerMove = (event: Event): void => {
202
+ if (!(event instanceof MouseEvent) || Number.isNaN(state.pointerDownAt)) return;
203
+ if (Math.abs(event.clientX - state.pointerDownAt) > POINTER_MOVE_EPSILON) state.pointerMoved = true;
204
+ };
205
+ const onPointerUp = (): void => {
206
+ state.pointer = false;
207
+ state.pointerDownAt = Number.NaN;
208
+ };
209
+ const onTouchStart = (event: Event): void => {
210
+ state.touch = true;
211
+ state.touchMoved = false;
212
+ state.touchDownAt = event instanceof TouchEvent ? (event.touches[0]?.clientX ?? Number.NaN) : Number.NaN;
213
+ };
214
+ const onTouchMove = (event: Event): void => {
215
+ if (!(event instanceof TouchEvent) || Number.isNaN(state.touchDownAt)) return;
216
+ const clientX = event.touches[0]?.clientX;
217
+ if (clientX !== undefined && Math.abs(clientX - state.touchDownAt) > POINTER_MOVE_EPSILON) {
218
+ state.touchMoved = true;
219
+ }
220
+ };
221
+ const onTouchEnd = (): void => {
222
+ state.touch = false;
223
+ state.touchDownAt = Number.NaN;
224
+ };
225
+ const onPointerCancel = (): void => {
226
+ state.nudged = true;
227
+ onPointerUp();
228
+ };
229
+ const onKeydown = (): void => {
230
+ state.nudged = true;
231
+ };
232
+ const onWheel = (event: Event): void => {
233
+ if (horizontalDelta(event) !== 0) state.nudged = true;
234
+ };
235
+ return {
236
+ onStrip: [
237
+ ['pointerdown', onPointerDown],
238
+ ['pointermove', onPointerMove],
239
+ ['touchstart', onTouchStart],
240
+ ['touchmove', onTouchMove],
241
+ ['pointercancel', onPointerCancel],
242
+ ['keydown', onKeydown],
243
+ ['wheel', onWheel],
244
+ ],
245
+ // A gesture can END off the strip, so its end is heard on the window.
246
+ onView: [
247
+ ['pointerup', onPointerUp],
248
+ ['pointercancel', onPointerUp],
249
+ ['touchend', onTouchEnd],
250
+ ['touchcancel', onTouchEnd],
251
+ ],
252
+ };
72
253
  }
73
254
 
74
255
  /**
@@ -115,12 +296,12 @@ export function watchVisitorScroll(strip: HTMLElement): VisitorScrollWatch {
115
296
  aimed: Number.NaN,
116
297
  quiet: undefined,
117
298
  settled: () => undefined,
299
+ lastScrollLeft: physicalScrollLeft(strip),
300
+ pointerDownAt: Number.NaN,
301
+ pointerMoved: false,
302
+ touchDownAt: Number.NaN,
303
+ touchMoved: false,
118
304
  };
119
- const set =
120
- (key: 'pointer' | 'touch' | 'nudged', value: boolean) =>
121
- (): void => {
122
- state[key] = value;
123
- };
124
305
  const settle = (): void => {
125
306
  // Only a scroll that was UNDER JUDGEMENT disarms. Chromium can end the
126
307
  // strip's own smooth scroll with a `scrollend` when another scroll cuts it
@@ -132,33 +313,36 @@ export function watchVisitorScroll(strip: HTMLElement): VisitorScrollWatch {
132
313
  if (!restsOnAim(strip, state.aimed)) state.scrolled = true;
133
314
  state.settled();
134
315
  };
135
- const onWheel = (event: Event): void => {
136
- if (horizontalDelta(event) !== 0) state.nudged = true;
137
- };
138
316
  const onScroll = (): void => {
139
- if (state.pointer || state.touch || state.nudged) state.undecided = true;
317
+ const now = physicalScrollLeft(strip);
318
+ const movedFarEnough = Math.abs(now - state.lastScrollLeft) > SCROLL_DELTA_EPSILON;
319
+ state.lastScrollLeft = now;
320
+ // Pointer/touch need BOTH signals (FUT-2775): the pointer/finger must have
321
+ // itself MOVED since going down, AND this tick's delta must be real. Either
322
+ // alone still latches on the other's shape — a pointer merely resting on
323
+ // the strip while its OWN `scrollTo` produces real per-tick deltas would
324
+ // arm under a delta-only check (`pointerMoved`/`touchMoved` rules that out);
325
+ // a pointer that drifts a sub-epsilon amount while resting would arm under
326
+ // a moved-only check with no delta floor (`movedFarEnough` rules that out).
327
+ const pointerDrag = state.pointer && state.pointerMoved && movedFarEnough;
328
+ const touchDrag = state.touch && state.touchMoved && movedFarEnough;
329
+ // `nudged` (a keypress, a sideways wheel, the browser taking the pointer
330
+ // over to pan) is already a discrete, one-shot gesture with no "resting
331
+ // pointer coincides with an unrelated animation" shape to rule out — unlike
332
+ // pointer/touch, requiring a delta on the SAME tick that arms it would only
333
+ // race the strip's own scroll ending before a qualifying tick arrives (seen:
334
+ // it broke the existing "keeps a wheel/drag scroll" stories against a real
335
+ // browser's `scrollend` timing). So it arms as it always has.
336
+ if (pointerDrag || touchDrag || state.nudged) state.undecided = true;
140
337
  if (!state.undecided || hasScrollEnd) return;
141
338
  // Every tick while undecided, armed or not: a quiet spell that began while
142
339
  // the strip was still moving would judge it mid-flight, away from its aim.
143
340
  clearTimeout(state.quiet);
144
341
  state.quiet = setTimeout(settle, QUIET_BEFORE_SETTLED_MS);
145
342
  };
146
- const onStrip: Listeners = [
147
- ['pointerdown', set('pointer', true)],
148
- ['touchstart', set('touch', true)],
149
- ['pointercancel', set('nudged', true)],
150
- ['keydown', set('nudged', true)],
151
- ['wheel', onWheel],
152
- ['scroll', onScroll],
153
- ['scrollend', settle],
154
- ];
155
- // A gesture can END off the strip, so its end is heard on the window.
156
- const onView: Listeners = [
157
- ['pointerup', set('pointer', false)],
158
- ['pointercancel', set('pointer', false)],
159
- ['touchend', set('touch', false)],
160
- ['touchcancel', set('touch', false)],
161
- ];
343
+ const gestures = createGestureListeners(state);
344
+ const onStrip: Listeners = [...gestures.onStrip, ['scroll', onScroll], ['scrollend', settle]];
345
+ const onView = gestures.onView;
162
346
  for (const [type, listener] of onStrip) strip.addEventListener(type, listener, { passive: true });
163
347
  for (const [type, listener] of onView) view?.addEventListener(type, listener, { passive: true });
164
348
  return {
@@ -4,7 +4,7 @@ import Box from '@mui/material/Box/index.js';
4
4
  import { alpha, useTheme, type Theme } from '@mui/material/styles/index.js';
5
5
  import React, { useEffect, useRef, type RefObject } from 'react';
6
6
 
7
- import { watchVisitorScroll } from './SettingsSectionChips.scroll';
7
+ import { physicalScrollLeft, toRawScrollLeft, watchVisitorScroll } from './SettingsSectionChips.scroll';
8
8
  import { SettingsStatusMarker } from './SettingsStatusMarker';
9
9
  import { TOUCH_TARGET } from './SettingsLayout.styles';
10
10
  import { rem, sxRem } from '../../../tokens/relative';
@@ -48,11 +48,20 @@ function prefersReducedMotion(): boolean {
48
48
  * it. The clamp hid that for the last chip; a middle one was simply off-centre.
49
49
  * The boxes are both in viewport coordinates, so their difference is the strip's
50
50
  * own, whatever the host wraps it in.
51
+ *
52
+ * Read and returned in PHYSICAL pixels (`physicalScrollLeft`, FUT-2775): the
53
+ * bounding-rect arithmetic above is already direction-blind, and an RTL
54
+ * scroller's raw `scrollLeft` is not — the CSSOM View spec has it `0` at the
55
+ * strip's right-hand start and NEGATIVE past it, not `0`-to-`scrollWidth` like
56
+ * an LTR one. `Math.max(0, …)` alone, against that raw value, would clamp every
57
+ * legitimate RTL target straight back to the start. Physical throughout keeps
58
+ * this comparable to {@link VisitorScrollWatch.aimedAt}; the caller converts to
59
+ * this engine's raw `scrollLeft` (`toRawScrollLeft`) only at the `scrollTo` call.
51
60
  */
52
61
  function centredScrollLeft(strip: HTMLElement, chip: HTMLElement): number {
53
62
  const chipBox = chip.getBoundingClientRect();
54
63
  const visibleStart = strip.getBoundingClientRect().left + strip.clientLeft;
55
- const chipCentre = chipBox.left - visibleStart + strip.scrollLeft + chipBox.width / 2;
64
+ const chipCentre = chipBox.left - visibleStart + physicalScrollLeft(strip) + chipBox.width / 2;
56
65
  const target = chipCentre - strip.clientWidth / 2;
57
66
  return Math.max(0, Math.min(target, strip.scrollWidth - strip.clientWidth));
58
67
  }
@@ -122,13 +131,16 @@ function useCentreActiveChip(
122
131
  // Not while a scroll that may be the visitor's is still moving: a resize
123
132
  // mid-drag must not yank the strip. It is re-tried once that scroll rests.
124
133
  if (!chip || visitor.hasScrolled() || visitor.isUndecided()) return;
134
+ // Physical pixels throughout (FUT-2775) — comparable to `aimedAt()` and to
135
+ // what `beforeOwnScroll` records; converted to this engine's raw
136
+ // `scrollLeft` only for the actual `scrollTo` call below.
125
137
  const left = centredScrollLeft(strip, chip);
126
138
  // Only when the target MOVED: a resize that leaves it where it was must not
127
139
  // restart a smooth scroll that is already on its way there. The aim is a
128
140
  // runtime-computed scroll offset, like the exempt `left` below — not a size.
129
141
  if (Math.abs(left - visitor.aimedAt()) < 0.5) return;
130
142
  visitor.beforeOwnScroll(left);
131
- strip.scrollTo({ left, behavior: prefersReducedMotion() ? 'auto' : 'smooth' });
143
+ strip.scrollTo({ left: toRawScrollLeft(strip, left), behavior: prefersReducedMotion() ? 'auto' : 'smooth' });
132
144
  };
133
145
  visitor.onSettled(centre);
134
146
  centre();
@@ -250,7 +262,15 @@ export function SettingsSectionChips({
250
262
  display: 'flex',
251
263
  alignItems: 'center',
252
264
  gap: 0.75,
253
- px: 0.25,
265
+ // The start side keeps its old, symmetric 0.25 — nothing sits flush
266
+ // against it once scrolled. The END side is where a scrolled-to-end
267
+ // chip's own border used to sit almost against the clip boundary
268
+ // (FUT-2775): sized like the gap BETWEEN chips (`gap` above) rather
269
+ // than invented fresh, so the last chip clears the edge by the same
270
+ // margin any two chips clear each other. Logical, not `pr`: the side
271
+ // that gets clipped is the strip's END, whichever edge that is.
272
+ paddingInlineStart: 0.25,
273
+ paddingInlineEnd: 0.75,
254
274
  py: 0.25,
255
275
  overflowX: 'auto',
256
276
  overflowY: 'hidden',
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/layout/SettingsLayout/SettingsSectionChips.tsx"],"sourcesContent":["'use client';\n\nimport Box from '@mui/material/Box/index.js';\nimport { alpha, useTheme, type Theme } from '@mui/material/styles/index.js';\nimport React, { useEffect, useRef, type RefObject } from 'react';\n\nimport { watchVisitorScroll } from './SettingsSectionChips.scroll';\nimport { SettingsStatusMarker } from './SettingsStatusMarker';\nimport { TOUCH_TARGET } from './SettingsLayout.styles';\nimport { rem, sxRem } from '../../../tokens/relative';\n\n/**\n * The chip's drawn height, against `TOUCH_TARGET`'s 44 for the tappable one.\n *\n * The strip sits between a section's header and its first field on the width\n * with the least room for either, so the pill is sized to its text rather than\n * to the thumb — and the thumb is served by the hit area instead.\n */\nconst CHIP_HEIGHT = sxRem(34);\nimport type { SettingsLayoutProps, SettingsNavItem } from './SettingsLayout.types';\n\nexport interface SettingsSectionChipsProps {\n /** The sibling sections to offer — usually the open item's own group. */\n items: SettingsNavItem[];\n /** Which one is open. */\n activeItemId?: string;\n /** Accessible name of the strip, in the host's language. */\n ariaLabel: string;\n linkComponent?: SettingsLayoutProps['linkComponent'];\n onSelectItem?: (id: string) => void;\n testIdPrefix: string;\n}\n\n/** True when the visitor asked the OS for less movement. Re-read per scroll. */\nfunction prefersReducedMotion(): boolean {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;\n return window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n}\n\n/**\n * Where the strip has to scroll for `chip`'s centre to sit on its own, clamped\n * to what the strip can scroll.\n *\n * Measured from the two boxes rather than from `chip.offsetLeft`. `offsetLeft`\n * counts from the nearest POSITIONED ancestor, and the strip is not one — nor\n * are the panel and layout around it — so it counted from whatever box the host\n * happened to position, and every target overshot by the strip's offset inside\n * it. The clamp hid that for the last chip; a middle one was simply off-centre.\n * The boxes are both in viewport coordinates, so their difference is the strip's\n * own, whatever the host wraps it in.\n */\nfunction centredScrollLeft(strip: HTMLElement, chip: HTMLElement): number {\n const chipBox = chip.getBoundingClientRect();\n const visibleStart = strip.getBoundingClientRect().left + strip.clientLeft;\n const chipCentre = chipBox.left - visibleStart + strip.scrollLeft + chipBox.width / 2;\n const target = chipCentre - strip.clientWidth / 2;\n return Math.max(0, Math.min(target, strip.scrollWidth - strip.clientWidth));\n}\n\n/** The chip for `id`, walked rather than selected. */\nfunction findChip(strip: HTMLElement, id: string): HTMLElement | undefined {\n // An id is host data, and a section id carrying a quote would turn a selector\n // string into a thrown SyntaxError — which here would take the whole strip\n // down to centre one chip.\n return Array.from(strip.children).find(\n (node): node is HTMLElement => node instanceof HTMLElement && node.dataset.chipId === id,\n );\n}\n\n/**\n * Keep the open section's chip in view, however the visitor got here — and\n * however the strip's content settles after it mounted.\n *\n * Keyed on `activeItemId` in an effect rather than done in a click handler:\n * arriving by the desktop rail, by a card, by a pasted link and by the browser's\n * back button all have to land the same way, and only one of those is a click on\n * a chip. Every re-render otherwise starts the strip at scroll 0, which parks\n * the active chip off the right edge on any list longer than a screen — the\n * visitor is told where they are by an element they cannot see.\n *\n * Centring ONCE is not enough (FUT-2606). A host can widen a chip after mount\n * with the ids unchanged — a status marker whose read resolves late, a web font\n * arriving — and a strip that fitted at mount then overflows with its last chip\n * clipped, because the one centring saw nothing to scroll. So the strip and\n * every chip are watched with a `ResizeObserver`, and a resize re-aims at the\n * active chip when its target has moved.\n *\n * Never against the visitor, though: once they have scrolled the strip\n * themselves, it is theirs until the open section changes\n * ({@link watchVisitorScroll}, which also says why the strip's OWN smooth scroll\n * never counts as theirs). A strip that yanks back mid-drag because a\n * marker loaded is worse than one that does not centre.\n *\n * `itemsKey` rather than `items`: a host almost always rebuilds that array per\n * render (`groups.flatMap(...)`), and depending on it would re-run this on every\n * render and reset the visitor's latch with it.\n *\n * The key joins on NUL — as the ESCAPE `'\\u0000'`, which it must stay. A\n * literal NUL byte works identically at runtime and makes git classify this file\n * as BINARY: `git diff` prints \"Binary file not shown\" and the GitHub review UI\n * shows nothing at all. This file shipped that way once, and the cost was not\n * the byte — it was a whole change nobody could read.\n */\nfunction useCentreActiveChip(\n stripRef: RefObject<HTMLDivElement | null>,\n activeItemId: string | undefined,\n itemsKey: string,\n): void {\n useEffect(() => {\n const strip = stripRef.current;\n if (!strip || !activeItemId) return undefined;\n // Feature-checked, not assumed. `Element.scrollTo` is absent in jsdom and in\n // a few embedded webviews, and an unguarded call there THROWS out of an\n // effect — which React escalates to the nearest error boundary. The whole\n // settings area would go down to centre one chip, and the host's own tests\n // could not render the shell at all. Same reasoning as walking the children\n // instead of building a selector: nothing in here is worth a crash.\n if (typeof strip.scrollTo !== 'function') return undefined;\n const visitor = watchVisitorScroll(strip);\n const centre = (): void => {\n const chip = findChip(strip, activeItemId);\n // Not while a scroll that may be the visitor's is still moving: a resize\n // mid-drag must not yank the strip. It is re-tried once that scroll rests.\n if (!chip || visitor.hasScrolled() || visitor.isUndecided()) return;\n const left = centredScrollLeft(strip, chip);\n // Only when the target MOVED: a resize that leaves it where it was must not\n // restart a smooth scroll that is already on its way there. The aim is a\n // runtime-computed scroll offset, like the exempt `left` below — not a size.\n if (Math.abs(left - visitor.aimedAt()) < 0.5) return;\n visitor.beforeOwnScroll(left);\n strip.scrollTo({ left, behavior: prefersReducedMotion() ? 'auto' : 'smooth' });\n };\n visitor.onSettled(centre);\n centre();\n // Also feature-checked: without it the strip still centres once, as before.\n const resizes = typeof ResizeObserver === 'function' ? new ResizeObserver(centre) : undefined;\n resizes?.observe(strip);\n for (const child of Array.from(strip.children)) resizes?.observe(child);\n return () => {\n resizes?.disconnect();\n visitor.detach();\n };\n }, [stripRef, activeItemId, itemsKey]);\n}\n\n/** One chip: a link when the host gave it a target, else a button. */\nfunction SectionChip({\n item,\n active,\n theme,\n linkComponent,\n onSelectItem,\n testIdPrefix,\n}: {\n item: SettingsNavItem;\n active: boolean;\n theme: Theme;\n linkComponent?: SettingsLayoutProps['linkComponent'];\n onSelectItem?: (id: string) => void;\n testIdPrefix: string;\n}): React.JSX.Element {\n const asLink = item.href !== undefined && linkComponent !== undefined;\n const behaviour = asLink\n ? { component: linkComponent, href: item.href }\n : { component: 'button' as const, type: 'button' as const, onClick: () => onSelectItem?.(item.id) };\n\n return (\n <Box\n {...behaviour}\n data-chip-id={item.id}\n data-testid={`${testIdPrefix}-chip-${item.id}`}\n aria-current={active ? 'page' : undefined}\n sx={{\n display: 'inline-flex',\n alignItems: 'center',\n gap: 0.75,\n flex: '0 0 auto',\n position: 'relative',\n // The chip LOOKS this tall; it is still tapped at `TOUCH_TARGET`.\n //\n // Shrinking `minHeight` alone would have bought a slimmer strip with a\n // smaller hit area — on the one width where the strip IS the navigation\n // and the input is a thumb. The `::after` below keeps the tappable\n // region at the full target while the pill itself takes less room, so\n // this is a visual change and not an accessibility trade.\n minHeight: CHIP_HEIGHT,\n px: 1.5,\n borderRadius: 999,\n cursor: 'pointer',\n font: 'inherit',\n fontSize: rem(theme, 13),\n fontWeight: active ? 700 : 500,\n textDecoration: 'none',\n whiteSpace: 'nowrap',\n color: active ? 'primary.main' : 'text.secondary',\n bgcolor: active ? alpha(theme.palette.primary.main, 0.12) : 'transparent',\n border: `1px solid ${active ? alpha(theme.palette.primary.main, 0.4) : theme.palette.divider}`,\n // Spans the chip's width only, so neighbours never overlap: the strip\n // is a row, and the extra reach is vertical.\n '&::after': {\n content: '\"\"',\n position: 'absolute',\n insetInlineStart: 0,\n insetInlineEnd: 0,\n top: '50%',\n transform: 'translateY(-50%)',\n height: rem(theme, TOUCH_TARGET),\n },\n }}\n >\n {item.status ? (\n <SettingsStatusMarker\n status={item.status}\n label={item.statusLabel}\n testId={`${testIdPrefix}-chip-status-${item.id}`}\n />\n ) : null}\n {item.label}\n </Box>\n );\n}\n\n/**\n * The narrow-width section strip: horizontally scrollable chips for the sections\n * beside the open one.\n *\n * It clips its own overflow — `overflow-x: auto` on the strip, never on an\n * ancestor. A strip that pushes the page wide makes every OTHER screen at that\n * width scroll sideways too, and the cause is nowhere near the symptom.\n */\nexport function SettingsSectionChips({\n items,\n activeItemId,\n ariaLabel,\n linkComponent,\n onSelectItem,\n testIdPrefix,\n}: SettingsSectionChipsProps): React.JSX.Element {\n const theme = useTheme();\n const stripRef = useRef<HTMLDivElement | null>(null);\n useCentreActiveChip(stripRef, activeItemId, items.map((item) => item.id).join('\\u0000'));\n\n return (\n <Box\n ref={stripRef}\n component=\"nav\"\n aria-label={ariaLabel}\n data-testid={`${testIdPrefix}-chips`}\n sx={{\n display: 'flex',\n alignItems: 'center',\n gap: 0.75,\n px: 0.25,\n py: 0.25,\n overflowX: 'auto',\n overflowY: 'hidden',\n scrollbarWidth: 'none',\n '&::-webkit-scrollbar': { display: 'none' },\n }}\n >\n {items.map((item) => (\n <SectionChip\n key={item.id}\n item={item}\n active={item.id === activeItemId}\n theme={theme}\n linkComponent={linkComponent}\n onSelectItem={onSelectItem}\n testIdPrefix={testIdPrefix}\n />\n ))}\n </Box>\n );\n}\n\nSettingsSectionChips.displayName = 'SettingsSectionChips';\n"],"mappings":"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","names":[]}
@@ -1,82 +0,0 @@
1
- // src/components/layout/SettingsLayout/SettingsSectionChips.scroll.ts
2
- var LANDED_SLACK = 1;
3
- var QUIET_BEFORE_SETTLED_MS = 250;
4
- function horizontalDelta(event) {
5
- if (!(event instanceof WheelEvent)) return 0;
6
- return event.deltaX !== 0 ? event.deltaX : event.shiftKey ? event.deltaY : 0;
7
- }
8
- function restsOnAim(strip, aimed) {
9
- const reachable = Math.min(aimed, strip.scrollWidth - strip.clientWidth);
10
- return Math.abs(strip.scrollLeft - reachable) <= LANDED_SLACK;
11
- }
12
- function watchVisitorScroll(strip) {
13
- const view = strip.ownerDocument.defaultView;
14
- const hasScrollEnd = "onscrollend" in strip;
15
- const state = {
16
- pointer: false,
17
- touch: false,
18
- nudged: false,
19
- undecided: false,
20
- scrolled: false,
21
- aimed: Number.NaN,
22
- quiet: void 0,
23
- settled: () => void 0
24
- };
25
- const set = (key, value) => () => {
26
- state[key] = value;
27
- };
28
- const settle = () => {
29
- if (!state.undecided) return;
30
- state.undecided = false;
31
- state.nudged = false;
32
- if (!restsOnAim(strip, state.aimed)) state.scrolled = true;
33
- state.settled();
34
- };
35
- const onWheel = (event) => {
36
- if (horizontalDelta(event) !== 0) state.nudged = true;
37
- };
38
- const onScroll = () => {
39
- if (state.pointer || state.touch || state.nudged) state.undecided = true;
40
- if (!state.undecided || hasScrollEnd) return;
41
- clearTimeout(state.quiet);
42
- state.quiet = setTimeout(settle, QUIET_BEFORE_SETTLED_MS);
43
- };
44
- const onStrip = [
45
- ["pointerdown", set("pointer", true)],
46
- ["touchstart", set("touch", true)],
47
- ["pointercancel", set("nudged", true)],
48
- ["keydown", set("nudged", true)],
49
- ["wheel", onWheel],
50
- ["scroll", onScroll],
51
- ["scrollend", settle]
52
- ];
53
- const onView = [
54
- ["pointerup", set("pointer", false)],
55
- ["pointercancel", set("pointer", false)],
56
- ["touchend", set("touch", false)],
57
- ["touchcancel", set("touch", false)]
58
- ];
59
- for (const [type, listener] of onStrip) strip.addEventListener(type, listener, { passive: true });
60
- for (const [type, listener] of onView) view?.addEventListener(type, listener, { passive: true });
61
- return {
62
- hasScrolled: () => state.scrolled,
63
- isUndecided: () => state.undecided,
64
- aimedAt: () => state.aimed,
65
- beforeOwnScroll: (left) => {
66
- state.aimed = left;
67
- },
68
- onSettled: (listener) => {
69
- state.settled = listener;
70
- },
71
- detach: () => {
72
- clearTimeout(state.quiet);
73
- for (const [type, listener] of onStrip) strip.removeEventListener(type, listener);
74
- for (const [type, listener] of onView) view?.removeEventListener(type, listener);
75
- }
76
- };
77
- }
78
-
79
- export {
80
- watchVisitorScroll
81
- };
82
- //# sourceMappingURL=chunk-SLJAQA2X.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/layout/SettingsLayout/SettingsSectionChips.scroll.ts"],"sourcesContent":["/**\n * Telling the strip's own scrolls from the visitor's (FUT-2606).\n *\n * Split from `SettingsSectionChips.tsx` so the chips stay a component file and\n * the rule for \"who moved the strip\" can be read in one place.\n */\n\n/**\n * How near to where the strip aimed itself it has to come to rest for the scroll\n * to count as the strip's own. A smooth scroll lands on a device pixel, and the\n * target it was given is fractional.\n */\nconst LANDED_SLACK = 1;\n\n/**\n * Where `scrollend` is missing: how long the strip has to go without a `scroll`\n * before it counts as settled. Generous on purpose — a busy main thread can go\n * several frames between ticks of a smooth scroll, and judging one mid-flight\n * would read the strip's own animation as the visitor's. Too long only delays a\n * re-centre; too short can latch the strip for good.\n */\nconst QUIET_BEFORE_SETTLED_MS = 250;\n\ninterface VisitorScrollWatch {\n /** True once the visitor has moved the strip themselves. */\n hasScrolled: () => boolean;\n /**\n * True while a scroll the visitor MAY be making has not come to rest yet. The\n * strip must not scroll itself under it: that would yank a drag in progress.\n */\n isUndecided: () => boolean;\n /** Where the strip last aimed itself, in its own pixels; `NaN` before it has. */\n aimedAt: () => number;\n /**\n * The strip is about to scroll itself to `left`. Records the aim and NOTHING\n * else: it used to disarm too, and a re-centre landing between a visitor's key\n * press (or wheel, or pan) and the first tick of the scroll it causes wiped\n * the arming — their scroll went unclaimed, read as the strip's own. Left\n * armed, the worst a stale arming does is put the strip's OWN next scroll\n * under judgement, which rests on its aim, is not latched, and disarms.\n */\n beforeOwnScroll: (left: number) => void;\n /** Called once an undecided scroll has come to rest and been judged. */\n onSettled: (listener: () => void) => void;\n detach: () => void;\n}\n\ntype Listeners = Array<[string, (event: Event) => void]>;\n\n/** A wheel's sideways travel: `deltaX`, or `deltaY` with Shift held, as browsers read it. */\nfunction horizontalDelta(event: Event): number {\n if (!(event instanceof WheelEvent)) return 0;\n return event.deltaX !== 0 ? event.deltaX : event.shiftKey ? event.deltaY : 0;\n}\n\ninterface WatchState {\n pointer: boolean;\n touch: boolean;\n nudged: boolean;\n undecided: boolean;\n scrolled: boolean;\n aimed: number;\n quiet: ReturnType<typeof setTimeout> | undefined;\n settled: () => void;\n}\n\n/** Whether the strip came to rest where it last aimed itself — clamped as the browser would. */\nfunction restsOnAim(strip: HTMLElement, aimed: number): boolean {\n const reachable = Math.min(aimed, strip.scrollWidth - strip.clientWidth);\n // Written so that `NaN` — the strip never aimed — reads as \"not on aim\".\n return Math.abs(strip.scrollLeft - reachable) <= LANDED_SLACK;\n}\n\n/**\n * Tell a scroll the VISITOR made from every other one.\n *\n * The `scroll` event cannot say: the strip's own `scrollTo` fires it, and so does\n * the browser clamping a strip whose content shrank. So a scroll is only a\n * CANDIDATE when it arrives during something only a visitor does:\n *\n * - a finger held on the strip (`touchstart` until `touchend`) — a drag;\n * - a pointer held on it (`pointerdown` until `pointerup`);\n * - a key press, a SIDEWAYS wheel, or the browser taking a pointer over to pan\n * it (`pointercancel`), each armed until the next scroll under judgement has\n * been judged — whoever's it turns out to be.\n *\n * and the verdict waits until the strip comes to REST (`scrollend`, or a quiet\n * spell where the browser has no `scrollend`): it is the visitor's only if the\n * strip rests somewhere other than where it last aimed itself.\n *\n * Judged at rest rather than per `scroll` because the strip's own smooth scroll\n * fires `scroll` for a third of a second, and a visitor who presses Tab or puts\n * a finger on a chip to scroll the PAGE in that window is not scrolling the\n * strip. Every tick of that animation used to count as theirs, the strip stopped\n * re-centring for good, and the open chip could be left clipped (FUT-2606\n * again, by another door). A visitor who really drags or wheels the strip\n * mid-animation cancels it, so the strip rests where THEY left it — and that\n * still counts. The one loss: a visitor who happens to leave it within a pixel\n * of the strip's own aim is not told apart from it, and the strip there is\n * already where it would have put itself.\n *\n * A tap is a pointer held with no scroll under it, so it never counts. A\n * vertical wheel scrolls the PAGE — the strip is `overflowY: hidden` — and is\n * not armed at all.\n */\nexport function watchVisitorScroll(strip: HTMLElement): VisitorScrollWatch {\n const view = strip.ownerDocument.defaultView;\n const hasScrollEnd = 'onscrollend' in strip;\n const state: WatchState = {\n pointer: false,\n touch: false,\n nudged: false,\n undecided: false,\n scrolled: false,\n aimed: Number.NaN,\n quiet: undefined,\n settled: () => undefined,\n };\n const set =\n (key: 'pointer' | 'touch' | 'nudged', value: boolean) =>\n (): void => {\n state[key] = value;\n };\n const settle = (): void => {\n // Only a scroll that was UNDER JUDGEMENT disarms. Chromium can end the\n // strip's own smooth scroll with a `scrollend` when another scroll cuts it\n // off, fired BEFORE that scroll's first `scroll` (seen with an instant one);\n // disarming there would hand the visitor's scroll to nobody.\n if (!state.undecided) return;\n state.undecided = false;\n state.nudged = false;\n if (!restsOnAim(strip, state.aimed)) state.scrolled = true;\n state.settled();\n };\n const onWheel = (event: Event): void => {\n if (horizontalDelta(event) !== 0) state.nudged = true;\n };\n const onScroll = (): void => {\n if (state.pointer || state.touch || state.nudged) state.undecided = true;\n if (!state.undecided || hasScrollEnd) return;\n // Every tick while undecided, armed or not: a quiet spell that began while\n // the strip was still moving would judge it mid-flight, away from its aim.\n clearTimeout(state.quiet);\n state.quiet = setTimeout(settle, QUIET_BEFORE_SETTLED_MS);\n };\n const onStrip: Listeners = [\n ['pointerdown', set('pointer', true)],\n ['touchstart', set('touch', true)],\n ['pointercancel', set('nudged', true)],\n ['keydown', set('nudged', true)],\n ['wheel', onWheel],\n ['scroll', onScroll],\n ['scrollend', settle],\n ];\n // A gesture can END off the strip, so its end is heard on the window.\n const onView: Listeners = [\n ['pointerup', set('pointer', false)],\n ['pointercancel', set('pointer', false)],\n ['touchend', set('touch', false)],\n ['touchcancel', set('touch', false)],\n ];\n for (const [type, listener] of onStrip) strip.addEventListener(type, listener, { passive: true });\n for (const [type, listener] of onView) view?.addEventListener(type, listener, { passive: true });\n return {\n hasScrolled: () => state.scrolled,\n isUndecided: () => state.undecided,\n aimedAt: () => state.aimed,\n beforeOwnScroll: (left) => {\n state.aimed = left;\n },\n onSettled: (listener) => {\n state.settled = listener;\n },\n detach: () => {\n clearTimeout(state.quiet);\n for (const [type, listener] of onStrip) strip.removeEventListener(type, listener);\n for (const [type, listener] of onView) view?.removeEventListener(type, listener);\n },\n };\n}\n"],"mappings":";AAYA,IAAM,eAAe;AASrB,IAAM,0BAA0B;AA6BhC,SAAS,gBAAgB,OAAsB;AAC7C,MAAI,EAAE,iBAAiB,YAAa,QAAO;AAC3C,SAAO,MAAM,WAAW,IAAI,MAAM,SAAS,MAAM,WAAW,MAAM,SAAS;AAC7E;AAcA,SAAS,WAAW,OAAoB,OAAwB;AAC9D,QAAM,YAAY,KAAK,IAAI,OAAO,MAAM,cAAc,MAAM,WAAW;AAEvE,SAAO,KAAK,IAAI,MAAM,aAAa,SAAS,KAAK;AACnD;AAkCO,SAAS,mBAAmB,OAAwC;AACzE,QAAM,OAAO,MAAM,cAAc;AACjC,QAAM,eAAe,iBAAiB;AACtC,QAAM,QAAoB;AAAA,IACxB,SAAS;AAAA,IACT,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW;AAAA,IACX,UAAU;AAAA,IACV,OAAO,OAAO;AAAA,IACd,OAAO;AAAA,IACP,SAAS,MAAM;AAAA,EACjB;AACA,QAAM,MACJ,CAAC,KAAqC,UACtC,MAAY;AACV,UAAM,GAAG,IAAI;AAAA,EACf;AACF,QAAM,SAAS,MAAY;AAKzB,QAAI,CAAC,MAAM,UAAW;AACtB,UAAM,YAAY;AAClB,UAAM,SAAS;AACf,QAAI,CAAC,WAAW,OAAO,MAAM,KAAK,EAAG,OAAM,WAAW;AACtD,UAAM,QAAQ;AAAA,EAChB;AACA,QAAM,UAAU,CAAC,UAAuB;AACtC,QAAI,gBAAgB,KAAK,MAAM,EAAG,OAAM,SAAS;AAAA,EACnD;AACA,QAAM,WAAW,MAAY;AAC3B,QAAI,MAAM,WAAW,MAAM,SAAS,MAAM,OAAQ,OAAM,YAAY;AACpE,QAAI,CAAC,MAAM,aAAa,aAAc;AAGtC,iBAAa,MAAM,KAAK;AACxB,UAAM,QAAQ,WAAW,QAAQ,uBAAuB;AAAA,EAC1D;AACA,QAAM,UAAqB;AAAA,IACzB,CAAC,eAAe,IAAI,WAAW,IAAI,CAAC;AAAA,IACpC,CAAC,cAAc,IAAI,SAAS,IAAI,CAAC;AAAA,IACjC,CAAC,iBAAiB,IAAI,UAAU,IAAI,CAAC;AAAA,IACrC,CAAC,WAAW,IAAI,UAAU,IAAI,CAAC;AAAA,IAC/B,CAAC,SAAS,OAAO;AAAA,IACjB,CAAC,UAAU,QAAQ;AAAA,IACnB,CAAC,aAAa,MAAM;AAAA,EACtB;AAEA,QAAM,SAAoB;AAAA,IACxB,CAAC,aAAa,IAAI,WAAW,KAAK,CAAC;AAAA,IACnC,CAAC,iBAAiB,IAAI,WAAW,KAAK,CAAC;AAAA,IACvC,CAAC,YAAY,IAAI,SAAS,KAAK,CAAC;AAAA,IAChC,CAAC,eAAe,IAAI,SAAS,KAAK,CAAC;AAAA,EACrC;AACA,aAAW,CAAC,MAAM,QAAQ,KAAK,QAAS,OAAM,iBAAiB,MAAM,UAAU,EAAE,SAAS,KAAK,CAAC;AAChG,aAAW,CAAC,MAAM,QAAQ,KAAK,OAAQ,OAAM,iBAAiB,MAAM,UAAU,EAAE,SAAS,KAAK,CAAC;AAC/F,SAAO;AAAA,IACL,aAAa,MAAM,MAAM;AAAA,IACzB,aAAa,MAAM,MAAM;AAAA,IACzB,SAAS,MAAM,MAAM;AAAA,IACrB,iBAAiB,CAAC,SAAS;AACzB,YAAM,QAAQ;AAAA,IAChB;AAAA,IACA,WAAW,CAAC,aAAa;AACvB,YAAM,UAAU;AAAA,IAClB;AAAA,IACA,QAAQ,MAAM;AACZ,mBAAa,MAAM,KAAK;AACxB,iBAAW,CAAC,MAAM,QAAQ,KAAK,QAAS,OAAM,oBAAoB,MAAM,QAAQ;AAChF,iBAAW,CAAC,MAAM,QAAQ,KAAK,OAAQ,OAAM,oBAAoB,MAAM,QAAQ;AAAA,IACjF;AAAA,EACF;AACF;","names":[]}