Repository navigation
Conversation
…t stream Ports cixzhang's meta#8072 action-group press (d2c3c78) onto this pull request's shared input stream, with no change to what it does. Its per-press listeners were registered straight on the document and the window with capture constants this layer no longer has, so the merged host failed to compile, and its window click listener would have doubled the input stream's click subscriber (two onClick calls per click once the host installs, and a listener its guard never removed). The press now subscribes its pointer's moves and lift and the window's own blur through subscribeInput for as long as it is held. It subscribes to click only while the host is not installed, drops that subscription after the lift's task, and an installing host takes it over, so a click is heard once. Her two host tests read the press's listeners on the window, where the stream registers them; a new case counts onClick per click before and after the host installs mid-press, and fails against each of three mutants (no takeover, no drop, no subscription).
…uard At this layer the host has no click listener of its own: the click guard (utils/clickGuard.ts) hears every click through one listener per window. cixzhang's meta#8072 action-group press, ported onto the input stream below, still disarmed the host's swallower and subscribed the host's onClick, so the merged host failed to compile. The press now disarms the click guard when it begins and subscribes no click: what it does is unchanged. The click-count case becomes: a group press adds no click listener of its own, before or after the host installs mid-press. GestureHost.test.ts resets the click guard after each case, so a guard one case armed no longer keeps its pointerdown subscription into the next (it hid the coarse host's pointerdown listener from a later case).
… that finger until it lifts (spec:AST-094 FR14) A recognition that claimed its touches and was then cancelled gave them back to the browser unless another recognition owned them, so a finger resting there could pinch-zoom the page. A gesture that cancels itself on a finger's move, a long press dragged past its tolerance after it began, gave back a finger that was dragging: the host stopped cancelling its moves and the page scrolled the rest of the drag, a hand change FR14 forbids and UIKit never makes (its scroll view never takes a touch another recognizer had). A cancel during a finger's move now keeps that finger claimed until it lifts; a resting finger is given back as before, and a waiting recognition that fails still gives its claim back. The new arena case fails on the previous code: the dragging finger reads unclaimed right after the cancel.
… scroller under it is at the edge spec:AST-094 FR19 and DEC-18 (proposed in meta#8628): a scrollEdge pan may cancel its touch's moves from the first one, only while every scroller under the finger sits at its edge and the finger moves away from it, and only the moves the browser would rubber-band; it still begins at FR17's 10 px. The first case reads the first 2 px move cancelled at the edge and fails on this head; the others keep what must not change: room to scroll leaves every move to the browser, a first move toward the content too, a pan without scrollEdge cancels nothing before it begins, and a touch the browser took for a scroll stays the browser's when the list reaches its edge.
… cancelled at the edge spec:AST-094 FR19 and DEC-18 (proposed in meta#8628). The sheet's scroll-edge pan at the top of its content: every touchmove the page sees is cancelled, the first one included. WebKit sends that first move at 2 px, before the sheet's 10 px begin, and it reaches the page uncancelled on this head; Chromium sends none inside its own 15 px slop, so there it proves only that nothing scrolls.
…e away from an edge spec:AST-094 FR19 and DEC-18 (proposed in meta#8628). GestureRecognizer gains keepsTouch(touchId), false by default and reached by the host through recognizerInternals: whether it holds that finger before it begins. A scrollEdge pan does while it is possible, the touch is a finger the browser has not taken, its travel so far goes the pan's own way, and no scroller under the finger can scroll for it (the probe its begin already reads), so only moves the browser would rubber-band are held, never a real scroll. The host's blocking touchmove check cancels a move a participant holds, as it does a claimed one; the arena already holds that move when the check runs, since the host feeds it from its window-capture subscriber first. The pan still begins at 10 px; a mouse or a pen is never held. The README says so.
…straight, (7, 7) and (8, 8), and the screen-edge band The FR18–FR22 verification row's real-touch states, in the gesture browser spec on a bare page: - a finger moved 9 px straight or (7, 7) over a scroll view keeps its tap and scrolls nothing; one moved 10 px or (8, 8) is a tap or a scroll, never both; a tap never cancels a touchmove; - a screen-edge pan on a strip as wide as its band, landing 40 px from the edge, has not claimed at 14 px of travel and claims at 15; - a touch landing 41 px from the edge never begins the edge pan, and the horizontal scroll it starts is not blocked. The bare fixture exposes the tap and screen-edge pan recognizers.
…edge-zone strip decides at UIKit's 15 px The host's arena environment had no viewSize, so under the real host a screen-edge pan never saw its edge zone while a touch was down and decided at 10 px, while its attach-time touch-action (read through the default environment) had already claimed the axis (spec:AST-094 FR18). Co-authored-by: Christopher Chedeau <vjeux@users.noreply.github.com>
…fter layout rounding Lane 0's real-host browser spec found a strip authored at the 40.2 px edge region lays out at 40.203 px (layout snaps to 1/64 px), so the strict comparison read it as off the zone and the pan decided at 10 px instead of UIKit's 15 (spec:AST-094 FR18). The zone takes half a pixel of tolerance: the rounded strip is the zone, a view a pixel wider still is not. Failing-first test at 40.203 px: it claims pan-x and decides at 15. Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
…w) as a dependency # Conflicts: # packages/core/src/gestures/__tests__/GestureHost.test.ts
…now that a strip within half a pixel is the edge zone (meta#8061)
…, and under inputMode 'pointer-events' a claimed finger's touchmoves are cancelled - spec:AST-094 FR20 and DEC-8: a native scroll above an unclaimed finger counts as the browser taking it only once that finger has travelled the take distance (10 px) from where it landed. A scroll under a still finger (scroll anchoring, a carousel advancing, a transcript pinned to its end) is the page's own, and the touch and its click stay. (The same rule meta#8309 carried, moved down to the host that introduces it.) - spec:AST-094 FR19 and FR21 under inputMode 'pointer-events': a finger comes from its pointer events, so its touches carried no id of ours and a claim never cancelled them. The blocking touchmove listener now matches a touch to the nearest live pointer-fed finger on its target, so a claimed pan's moves are cancelled and the browser cannot scroll under it. Tests (failing first): 9 px and (7, 7) are not taken, 10 px and (8, 8) are, a still finger is not; the FR30 own-scroll case moves its finger first; under 'pointer-events' a pan's touchmoves pass before its claim and are cancelled after it.
…n: pan-y only on a strip within its band, left as is on a wider view
…es its recognizers, and the group keeps answering afterwards Under a fine pointer the host is not installed, so usesTouchEvents keeps its initial true, and a group press's own touch pointermove and pointerup went down the touch-pairing path and returned: the finger's moves and lift never reached the group, its contact stayed in the arena, and the group answered no press after it, a mouse's included. The group press now feeds its own events through the pointer path for every pointer type. Test (failing first): under a fine pointer a finger tap on a group recognizes, and a mouse tap afterwards does too.
… are all passive spec:AST-094 FR20: a watcher registers only passive listeners, but a watcher-only page still got the host's cancelable document touchend and its contextmenu and window click listeners. The document touchend is now passive until an enabled claimer (a recognizer that claims touches, or a consumer) is attached, and the contextmenu and click listeners are on only then, swapped as attachments change. The new test (a lone watcher leaves no cancelable document or window listener; a claiming pan brings the three) fails on the previous head.
…r watchers) into the host line The host keeps this PR's isRtlElement and its fingers held before a recognizer begins (a kept press, a scroll-edge pan), beside meta#8061's view size and meta#8309's pointer-events finger lookup. meta#8589's claimer check counts only participants active on this pointer (isActive, isConsumerActive), and a group press's click listener now stays while a claimer keeps it installed. The browser spec keeps the fine-pointer describe beside the FR18-FR22 real-touch states.
…inter reaches its recognizers) as a dependency
Main's meta#8521 is its head 25b468c squashed, which this pull request already carries: every file it touches is byte-identical to 25b468c. In the two files that conflicted (the motion README and animator.ts), the resolution is this side's version, main's merged meta#8521 with this pull request's later work on top. Nothing of main is dropped.
…er under the pointer (spec:AST-094 FR43, FR19) The wheel gesture now reads the scrollers under the pointer as a finger's probe does, its own scroll owner and the page included, with overscroll-behavior ending the chain, but without touch-action, which narrows only a finger's pan (createNativeScrollProbe takes readsTouchAction: false). beginWheel leaves a wheel to the page when a scroll-edge pan's scrollers could still scroll for it, before anything is prevented, and the wheel contact carries the probe, so the pan's own scroll-edge check reads the same chain at its first move. Other pans keep the rule for the scrollers between the pointer and their element.
…ss (spec:AST-094 FR43, FR24) A pan with no pointer types and scroll input allowed, such as a pull's wheel-only pan beside its finger pan, still armed its element for a press where the primary pointer is fine: a pointerdown listener for a recognizer no pointer can start. Fails until arming counts only recognizers that take a pointer.
…t for a press (spec:AST-094 FR43, FR24) Where the primary pointer is fine, an element is armed for a press while an enabled recognizer or consumer on it could take one. A recognizer whose allowedPointerTypes is empty takes only scroll input, so it no longer arms one: its element keeps the wheel listener alone.
…s its divider with a coarse-pointer rule, and drops its layer where no scrollend fires Review findings on meta#8142: - With a mouse, a variant added by module augmentation painted a surface header. A page-scrolling shell now keeps each variant's own fill (none for an augmented one) and adds only the coarse-pointer transparent rule. - The section divider was dropped through the resolver's guess, so a server render with `defaultIsMobile` lost it. It stays in the markup, and a coarse-pointer rule takes its width away on the touch bar. - Where the browser sends no `scrollend`, the layer stayed promoted after a fling (re-checked only at a finger change). The scroller's scroll events now time a re-check 200 ms after the last one, past the finger-scroll watch's 150 ms spell. - One document listener per effect: `scrollend`, or `scroll` where the browser sends none. This base has no shared input stream (it arrives with meta#8665's line); the effect moves onto it there. - The WebKit workaround names its removal condition. Red first: the five new cases fail on ae06884.
…slates, the Layout/Paint proof waits for demotion, the handover counts at the cast's cadence) into the bar look's bases
…) into the NavigationStack bar look # Conflicts: # packages/core/src/AppShell/AppShell.tsx # packages/core/src/AppShell/AppShellScrollEdge.test.tsx # packages/core/src/hooks/useScrollEdgeEffect.ts # packages/core/src/motion/useScrollEdgeEffect.test.tsx
…ll edge effect's wheel reading
… under the scroll edge effect's wheel reading
…ct, as a finger's does (spec:AST-094 FR43) On the web a pan also takes wheel and trackpad scrolling (FR43). Under a finger, a scroll the wheel or the trackpad drives now fades the touch bar's veil on the finger's springs, instead of switching it at once as the program's scroll does. - A scroll of the scroller that starts within 100 ms of a wheel over it is the wheel's until the scroller's `scrollend` or, where the browser sends none, a 200 ms quiet spell. The observer's report follows the latest scroll event, so the fade reads that scroll's kind: Chromium ends a wheel step's scroll in the same task, before the report. - A ctrl-wheel (a pinch), a wheel that scrolls nothing and a wheel outside a scroll container change nothing; the program's scroll still switches. - The layer is composited only through a wheel's scroll, as through a finger's. - The wheel, scroll and scrollend listeners go through the shared input stream (one native listener per type for every effect), passive, and only while the effect runs (FR24). - AppShell's doc names the wheel and the trackpad. Red first: eight unit cases fail on the review base, and ten mutants each fail at least one. Real wheel input in Chromium (phone profile): the veil fades in, then the program's scroll switches it; the case fails without the change.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report📚 Storybook Preview🧪 Sandbox PreviewNew ComponentsSearchField (@astryxdesign/lab) · View in Storybook
Modified ComponentsAppShell (@astryxdesign/core) · View in Storybook
Item (@astryxdesign/core) · View in Storybook
TreeList (@astryxdesign/core) · View in Storybook
Component Size
Sizes are gzip. Import counts the JavaScript the import loads eagerly; CSS counts the package stylesheet rules referenced by the modules it can load from its own package, lazily loaded ones included.
|
| Component | JavaScript | CSS |
|---|---|---|
| core/AppShell | +28.9KB |
+383B |
Bundle Size Summary
Gzip figures measure each package's entry file, not the code an import pulls in.
| Package | Entry (ESM) | Entry gzip before | Entry gzip after | Delta |
|---|---|---|---|---|
| @astryxdesign/core | 4.9KB | 1.2KB | 1.2KB | 0B |
| @astryxdesign/lab | 5.1KB | 1.9KB | 2.1KB | +185B |
| Package stylesheet | Gzip before | Gzip after | Delta |
|---|---|---|---|
| @astryxdesign/core | 34.6KB | 35.4KB | +855B |
| @astryxdesign/lab | 11.1KB | 12.1KB | +1.0KB |
Accessibility Audit
Status: 2 accessibility violation(s) found — 2 serious.
Radio group pattern - 1 issue(s)
- 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
- Rule:
color-contrast· Affects 3/11 stories · Learn more - WCAG: 1.4.3 (Level AA)
- Rule:
RadioList - 1 issue(s)
- 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
- Rule:
color-contrast· Affects 3/14 stories · Learn more - WCAG: 1.4.3 (Level AA)
- Rule:
Visual Regression
55 of 502 shot(s) changed.
A change here is a question, not a failure: check whether the after is the
picture you intended. Record that review on the PR. Baseline maintenance is an
explicit dispatch of CI; this report never rewrites the baseline or adds a release gate.
| component | story | theme | mode | pixels |
|---|---|---|---|---|
| Selector | Indicator Space Bottom Sheet Narrow End RTL | neutral | light | 753,933 |
| Selector | Indicator Space Bottom Sheet Wide Start | neutral | light | 740,072 |
| RichTextEditor | Markdown parity: side by side | probe | dark | 558,840 |
| RichTextEditor | Markdown parity: side by side | probe | light | 540,756 |
| Chat | Full AI Chat | probe | light | 230,823 |
| Chat | Full AI Chat | probe | dark | 229,805 |
| AppShell | With Mobile Nav | probe | light | 217,010 |
| AppShell | With Mobile Nav | probe | dark | 216,960 |
| Collapsible | Dividers | probe | dark | 66,445 |
| Collapsible | Dividers | probe | light | 66,445 |
| AppShell | Playground | neutral | dark | 65,198 |
| AppShell | Playground | neutral | light | 63,788 |
| Chat | Default | neutral | dark | 36,304 |
| Chat | Default | neutral | light | 35,767 |
| Chat | Empty State | probe | light | 23,531 |
| Chat | Empty State | probe | dark | 22,622 |
| ChatAutoScroll | Scroll Behavior Comparison | neutral | dark | 15,378 |
| ChatAutoScroll | Scroll Behavior Comparison | neutral | light | 14,107 |
| Chat | With Attachments | probe | light | 13,406 |
| Chat | With Attachments | probe | dark | 11,232 |
and 35 more.
Generated by PR Enrichment workflow | Storybook | Sandbox | View full report
There was a problem hiding this comment.
Approval waits on an engineering owner's decision: no current spec says whether a wheel's or trackpad's scroll should fade the touch scroll edge veil like a finger's or switch it at once like the program's, since spec:AST-094 on main has no FR43 yet and no claim about the scroll edge effect. An engineering owner settles it by providing or identifying the governing spec for that behavior and recording the decision with /approve-spec <full head SHA> on this pull request; the author may do so if they are an engineering owner. Separately, the diff carries its unlanded prerequisites (50,172 authored lines across 277 files), so please rebase on main once they land so the pull request shows only its own change.
[Automated review]
| if (next === isShown) { | ||
| return; | ||
| } | ||
| if (isShown != null && (isFingerScroll() || movedByWheel)) { |
There was a problem hiding this comment.
This is where a wheel's or trackpad's scroll starts to fade the veil. No current spec states that yet (spec:AST-094 on main has no FR43 and no claim about the scroll edge effect), so this line waits on an engineering owner's decision between fading and switching.
|
Posted by Cindy's agent on her behalf: closed and reopened only to restart this PR's CI on the current workflow after tonight's queue-wide cancel; nothing in the PR changed. |
Stacked on #8396 (on #8142) and #8676 (F2's wheel pan, with X1's drivers). Review only this PR's change: vjeux/astryx@f56cdf2...aff1de9
Carries #8562 (Core's internal entries) as a dependency; if the entries land differently, this follows her decision.
User need
Under a finger (
spec:AST-094FR13), a wheel's or trackpad's scroll switches the touch header's veil at once, as the program's does; a finger's fades it.Problem and solution fit
A scroll of the scroller that starts within 100 ms of a wheel over it is now the wheel's until the scroller's
scrollend(or a 200 ms quiet spell), momentum included; the fade reads the latest scroll's kind. It fades on the finger's springs, compositing the layer only meanwhile. The program's scroll still switches; a ctrl-wheel, a wheel that scrolls nothing or one outside the container change nothing.Current specification
spec:AST-094FR43, current since #8646: "On the web, a pan also takes wheel and trackpad scrolling", so the wheel's scroll is the user's. DEC-21 maps wheel and trackpad scrolling to a pan and turns off only a mouse drag's swipe actions, a pen's pinch and the screen-edge pan under a mouse, a trackpad or a pen; the scroller's own pan stays on. FR24 for the listener. UIKit fades the effect for every scroll; the program's switch is #8142's WebKit workaround.Public delta and caller burden
Behavior only, under a finger: AppShell's touch header and Lab's NavigationStack bars (same hook); AppShell's doc names the wheel. No API, type, default, theme or accessibility change; a mouse still gets no veil or listener.
Design and alternatives
Passive
wheel,scrollandscrollendlisteners on the shared input stream (one native listener per type), only while the effect runs (FR24). Rejected: a pan withallowedScrollTypes(#8676), which takes the wheel and holds the page still; and counting a wheel with no scroll after it, which would fade the program's next scroll over a composited layer (iOS 26.2's blank frames).Evidence
analyze-pr, gzip, base → head): AppShell's import 88,736 → 88,882 B of JavaScript; CSS 12,305 → 12,305 B.Scope
Testing
pnpm exec vitest run packages/core/src/motion/useScrollEdgeEffect.test.tsx: 45 pass. New, each with no finger down:scrollendcomes before the observer's report still fades (Chromium's order); a trackpad's scroll of a container fades;scrollend, and the program's scroll after it switches; a wheel that scrolls nothing, a ctrl-wheel and a wheel outside the container switch;scrollend, never for a wheel that scrolls nothing; withoutscrollend, a 200 ms quiet spell ends it;scrollendnot ending it; no quiet spell; a quiet spell that re-checks no finger; a non-passive listener.pnpm exec vitest run packages/core/src/{motion,AppShell,gestures,theme,internal,Layout,utils} packages/lab/src/NavigationStack: 131 files, 3579 tests pass.pnpm test:interaction AppShellScrollEdgeagainst a freshpnpm storybook:build: 13 pass in Chromium, the new wheel case included (real wheel input throughwebInputDriver, phone profile: the veil fades in, then ascrollToswitches it); with the review base's hook it fails ("no animation started"). WebKit: 9 pass and 4 skips (the wheel case, since WebKit's mobile emulation has no wheel input, and three Chromium-only cases).NavigationStack gestures.browser: 108 pass and 14 skips in Chromium, 94 pass in WebKit.pnpm exec vitest run --project node packages/cli/test: 309 pass. registry-contract (preview): 1038 items and 1042 routes verified. Rootpnpm typecheckafterpnpm build: 0 errors;CI=true pnpm lint: 0 errors;check:changesets,check:knowledge, the CI duration guard,internalModulesNotExperimental,scripts/lab-core-imports.test.mjsandscripts/core-internal-entries.test.mjspass; the Lab import codemod moves 0.scripts/touch-support-bundle.test.mjs› "is absent from the root entry" is red as on feat(AppShell): under a finger, the pinned touch header is an iOS 26 bar with its scroll edge effect #8142: AppShell's touch bar imports the gesture host statically, as INV13 prescribes; F1's per-component bundle test, test(core): a touch component's import carries its gesture code, and nothing else does #8671, lists AppShell as a gesture user.pnpm a11y:audit -- --components AppShell,NavigationStack: no new violations. It merges cleanly with main d9b43cf.