Skip to content

feat(core): a wheel's or trackpad's scroll fades the scroll edge effect, as a finger's does (spec:AST-094 FR43) - #8700

Draft
vjeux wants to merge 2055 commits into
meta:mainfrom
vjeux:uikit/c16-scroll-edge-wheel
Draft

vjeux wants to merge 2055 commits into
meta:mainfrom
vjeux:uikit/c16-scroll-edge-wheel

Conversation

@vjeux

@vjeux vjeux commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

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-094 FR13), 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-094 FR43, 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, scroll and scrollend listeners on the shared input stream (one native listener per type), only while the effect runs (FR24). Rejected: a pan with allowedScrollTypes (#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

  • Red first: eight new unit cases fail on the review base; ten mutants, one per rule, each fail one or more.
  • Real wheel input, Chromium phone profile: the veil fades in, then the program's scroll switches it; it fails without the hook change. WebKit's mobile emulation has no wheel (skipped).
  • Size (INV6, analyze-pr, gzip, base → head): AppShell's import 88,736 → 88,882 B of JavaScript; CSS 12,305 → 12,305 B.

Scope

  • One capability is added.
  • Unrelated fixes, cleanup, and policy changes were removed or split.
  • Consumer docs, focused tests, and migration evidence ship with the feature.
  • Public text and artifacts contain no internal Meta context.

Testing

  • pnpm exec vitest run packages/core/src/motion/useScrollEdgeEffect.test.tsx: 45 pass. New, each with no finger down:
    • a wheel's scroll of the page fades in and out on the two springs; a wheel step whose scrollend comes before the observer's report still fades (Chromium's order); a trackpad's scroll of a container fades;
    • the scroll stays the wheel's until the scroller's scrollend, and the program's scroll after it switches; a wheel that scrolls nothing, a ctrl-wheel and a wheel outside the container switch;
    • the layer is composited from the wheel's first scroll until its scrollend, never for a wheel that scrolls nothing; without scrollend, a 200 ms quiet spell ends it;
    • the wheel listener is passive and exists only while the effect runs (none with a mouse or while disabled); the hook adds no document listener, and one native listener per type serves every effect.
    • Eight fail on the review base. Mutants, each failing at least one: no wheel fade; the fade read from the running scroll instead of the latest; no wheel compositing; a ctrl-wheel counted; any wheel counted for a container; no 100 ms gap; scrollend not 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 AppShellScrollEdge against a fresh pnpm storybook:build: 13 pass in Chromium, the new wheel case included (real wheel input through webInputDriver, phone profile: the veil fades in, then a scrollTo switches 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. Root pnpm typecheck after pnpm build: 0 errors; CI=true pnpm lint: 0 errors; check:changesets, check:knowledge, the CI duration guard, internalModulesNotExperimental, scripts/lab-core-imports.test.mjs and scripts/core-internal-entries.test.mjs pass; 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.

vjeux and others added 30 commits October 26, 2026 19:07
…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
…r-events claims cancel) as a dependency

# Conflicts:
#	packages/core/src/gestures/GestureHost.ts
#	packages/core/src/gestures/__tests__/GestureHost.test.ts
…8072 9800eb5, the FR18-FR22 browser states) as a dependency
…eta#8061 9adbe2d, meta#8072 9800eb5) into the gesture watcher

The README's touch-action table takes meta#8061's edge-pan rows beside this
PR's watcher row.
…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.
…'s passive-only watcher listeners) into the arena fixes

The host-owner changeset keeps this PR's own listeners for the scroll
lock and finger-scroll watch, and adds meta#8336's scroll-edge hold.
…inter reaches its recognizers) as a dependency
… under a fine pointer) as a dependency
…tion group's finger under a fine pointer) into the gesture watcher
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.
vjeux added 12 commits October 27, 2026 05:52
…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
… 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.
@vercel

vercel Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 27, 2026 2:08pm UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 30, 2026, 5:33 AM UTC

@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

New Components

SearchField (@astryxdesign/lab) · View in Storybook
Metric Value
Entry File (ESM) 253B
Lines of Code 745
Source Files 4
Complexity Very High (31)
Exports SearchField, type, type
Props Count N/A
Has Tests Yes
Has Stories Yes

Modified Components

AppShell (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 785B 785B 0B
Lines of Code 608 675 -
Complexity Very High Very High (130) -
Item (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 284B 284B 0B
Lines of Code 1372 1377 -
Complexity Very High Very High (122) -
TreeList (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 648B 648B 0B
Lines of Code 872 878 -
Complexity Very High Very High (100) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/AlertDialog 56.9KB 57.7KB +803B 12.4KB 12.5KB +24B
core/AppShell 57.9KB 86.8KB +28.9KB ⚠️ 11.6KB 12.0KB +383B
core/AspectRatio 3.7KB 3.8KB +66B 3.5KB 3.5KB +40B
core/Avatar 37.5KB 37.5KB +79B 6.3KB 6.3KB +23B
core/AvatarGroup 39.8KB 39.9KB +77B 6.4KB 6.5KB +22B
core/AvatarGroupOverflow 39.9KB 40.7KB +829B 6.4KB 6.4KB +23B
core/AvatarStatusDot 7.1KB 7.1KB +61B 3.4KB 3.4KB +41B
core/Badge 3.9KB 4.0KB +64B 3.8KB 3.9KB +39B
core/Banner 47.8KB 48.6KB +780B 9.9KB 10.0KB +22B
core/BaseTypeahead 54.3KB 55.0KB +734B 9.9KB 9.9KB +22B
core/Blockquote 3.5KB 3.6KB +66B 3.4KB 3.4KB +39B
core/BottomSheet 34.0KB 34.0KB +69B 7.9KB 7.9KB +24B
core/BottomSheetSwitcher 14.8KB 14.8KB +67B 3.3KB 3.3KB +41B
core/BreadcrumbItem 66.4KB 67.8KB +1.4KB 11.0KB 11.4KB +412B
core/Breadcrumbs 22.4KB 22.5KB +96B 3.3KB 3.3KB +40B
core/Button 43.3KB 44.1KB +808B 9.2KB 9.2KB +21B
core/ButtonGroup 8.4KB 8.5KB +67B 3.3KB 3.3KB +40B
core/Calendar 54.9KB 55.7KB +801B 10.0KB 10.0KB +22B
core/Card 6.4KB 6.5KB +66B 6.3KB 6.4KB +42B
core/Carousel 50.2KB 51.0KB +807B 10.2KB 10.2KB +21B
core/Center 4.2KB 4.2KB +63B 4.6KB 4.6KB +42B
core/ChatComposer 60.6KB 61.4KB +791B 10.8KB 10.8KB +19B
core/ChatComposerDrawer 23.9KB 24.0KB +93B 4.5KB 4.6KB +39B
core/ChatComposerInput 55.7KB 56.5KB +762B 10.1KB 10.1KB +21B
core/ChatComposerTokenElement 55.7KB 56.5KB +762B 10.1KB 10.1KB +21B
core/ChatDictationButton 46.5KB 47.3KB +786B 9.5KB 9.5KB +20B
core/ChatLayout 48.8KB 49.6KB +798B 9.9KB 10.0KB +20B
core/ChatLayoutScrollButton 46.4KB 47.1KB +763B 9.6KB 9.6KB +20B
core/ChatMessage 23.1KB 23.1KB +85B 3.5KB 3.6KB +39B
core/ChatMessageBubble 7.0KB 7.1KB +67B 3.6KB 3.6KB +42B
core/ChatMessageList 28.9KB 29.0KB +99B 7.8KB 7.8KB +21B
core/ChatMessageMetadata 25.8KB 25.9KB +85B 3.9KB 3.9KB +23B
core/ChatSendButton 45.8KB 46.6KB +780B 9.2KB 9.2KB +21B
core/ChatSystemMessage 6.7KB 6.8KB +71B 3.7KB 3.7KB +39B
core/ChatTokenizedText 6.9KB 7.0KB +63B 3.9KB 3.9KB +40B
core/ChatToolCalls 31.9KB 31.9KB +89B 8.9KB 8.9KB +22B
core/CheckIndicator 9.0KB 9.1KB +68B 3.6KB 3.6KB +39B
core/CheckboxIndicator 4.1KB 4.1KB +65B 3.8KB 3.8KB +22B
core/CheckboxInput 49.9KB 50.7KB +757B 9.3KB 9.3KB +22B
core/CheckboxList 46.0KB 46.1KB +90B 8.1KB 8.2KB +22B
core/CheckboxListItem 54.0KB 55.3KB +1.2KB 10.5KB 11.0KB +541B
core/Citation 25.9KB 26.0KB +93B 4.1KB 4.2KB +41B
core/ClickableCard 12.8KB 13.6KB +810B 6.9KB 7.0KB +46B
core/Code 3.5KB 3.6KB +63B 3.4KB 3.4KB +41B
core/CodeBlock 60.5KB 61.3KB +779B 10.7KB 10.7KB +27B
core/Collapsible 11.7KB 12.5KB +771B 4.2KB 4.3KB +39B
core/CollapsibleGroup 6.4KB 6.4KB +71B 3.2KB 3.2KB +40B
core/CommandPalette 50.9KB 51.7KB +778B 11.8KB 11.8KB +23B
core/CommandPaletteEmpty 6.1KB 6.2KB +64B 3.3KB 3.4KB +42B
core/CommandPaletteFooter 23.3KB 23.4KB +95B 3.5KB 3.6KB +39B
core/CommandPaletteGroup 3.5KB 3.5KB +64B 3.3KB 3.4KB +39B
core/CommandPaletteInput 32.3KB 32.4KB +86B 8.1KB 8.1KB +22B
core/CommandPaletteItem 8.4KB 9.2KB +764B 3.5KB 3.5KB +40B
core/CommandPaletteList 22.4KB 22.5KB +89B 3.3KB 3.3KB +41B
core/ComplexSelector 55.8KB 56.6KB +831B 10.6KB 10.7KB +20B
core/ContextMenu 77.7KB 79.1KB +1.4KB 14.6KB 15.2KB +550B
core/ContextMenuItem 16.6KB 17.7KB +1.1KB 5.2KB 5.6KB +420B
core/DateInput 99.9KB 100.5KB +668B 14.8KB 14.8KB +20B
core/DateRangeInput 69.4KB 70.1KB +769B 11.2KB 11.2KB +21B
core/DateTimeInput 110.1KB 110.8KB +696B 15.2KB 15.2KB +20B
core/Dialog 16.2KB 16.3KB +64B 7.1KB 7.1KB +23B
core/DialogHeader 48.8KB 49.6KB +801B 9.8KB 9.8KB +20B
core/Divider 6.5KB 6.5KB +64B 3.6KB 3.6KB +43B
core/Drawer 31.6KB 31.7KB +84B 8.0KB 8.1KB +23B
core/DrawerHeader 48.9KB 49.7KB +760B 11.2KB 11.2KB +22B
core/DropdownMenu 72.7KB 74.0KB +1.3KB 14.7KB 15.3KB +545B
core/DropdownMenuCheckboxItem 16.1KB 17.3KB +1.2KB 5.6KB 6.0KB +404B
core/DropdownMenuDivider 6.6KB 6.6KB +61B 3.6KB 3.6KB +42B
core/DropdownMenuGroup 6.1KB 6.1KB +69B 3.3KB 3.3KB +39B
core/DropdownMenuItem 15.4KB 16.6KB +1.2KB 5.2KB 5.6KB +421B
core/DropdownMenuRadioGroup 6.0KB 6.1KB +62B 3.2KB 3.2KB +43B
core/DropdownMenuRadioItem 16.2KB 17.4KB +1.2KB 5.6KB 6.0KB +404B
core/DropdownMenuSubMenu 58.1KB 59.5KB +1.4KB 9.5KB 9.9KB +408B
core/EmptyState 7.7KB 7.8KB +64B 3.5KB 3.6KB +42B
core/Field 44.2KB 44.3KB +96B 8.0KB 8.0KB +22B
core/FieldLabel 39.8KB 39.9KB +81B 6.5KB 6.5KB +23B
core/FieldStatus 10.4KB 10.5KB +74B 4.2KB 4.3KB +23B
core/FileInput 54.1KB 54.8KB +741B 10.3KB 10.4KB +23B
core/FormLayout 6.3KB 6.3KB +66B 3.4KB 3.4KB +40B
core/Grid 4.3KB 4.4KB +67B 3.7KB 3.7KB +40B
core/GridSpan 3.4KB 3.5KB +63B 3.2KB 3.3KB +40B
core/HStack 7.3KB 7.3KB +62B 4.9KB 4.9KB +44B
core/Heading 9.1KB 9.2KB +64B 6.8KB 6.8KB +23B
core/Icon 8.8KB 8.9KB +60B 3.6KB 3.6KB +39B
core/IconButton 43.3KB 44.1KB +792B 9.2KB 9.2KB +21B
core/InputGroup 44.8KB 44.9KB +88B 8.0KB 8.1KB +21B
core/InputGroupText 6.3KB 6.4KB +66B 3.7KB 3.8KB +40B
core/Item 11.4KB 12.7KB +1.3KB 4.7KB 5.1KB +423B
core/Kbd 6.7KB 6.7KB +67B 3.5KB 3.5KB +40B
core/LayerProvider 63.3KB 64.1KB +808B 10.1KB 10.1KB +23B
core/Layout 8.1KB 8.2KB +63B 5.5KB 5.5KB +43B
core/LayoutContent 7.4KB 7.5KB +64B 4.9KB 5.0KB +43B
core/LayoutFooter 7.3KB 7.4KB +68B 4.8KB 4.9KB +42B
core/LayoutHeader 7.4KB 7.4KB +65B 4.8KB 4.9KB +42B
core/LayoutPanel 7.6KB 7.6KB +65B 4.8KB 4.9KB +43B
core/Lightbox 51.2KB 51.9KB +751B 9.8KB 9.8KB +21B
core/Link 43.5KB 44.3KB +780B 7.6KB 7.6KB +25B
core/List 6.7KB 6.8KB +59B 3.4KB 3.5KB +40B
core/ListItem 53.5KB 54.6KB +1.2KB 10.5KB 11.0KB +541B
core/Markdown 144.5KB 145.8KB +1.4KB 17.4KB 17.9KB +545B
core/MetadataList 23.1KB 23.2KB +91B 3.6KB 3.6KB +42B
core/MetadataListItem 6.5KB 6.5KB +64B 3.4KB 3.4KB +43B
core/MobileNav 51.0KB 51.7KB +739B 10.1KB 10.1KB +22B
core/MobileNavToggle 46.2KB 47.0KB +761B 9.4KB 9.4KB +20B
core/MoreMenu 72.9KB 74.2KB +1.3KB 14.7KB 15.3KB +545B
core/MultiSelector 76.2KB 76.9KB +730B 14.6KB 14.7KB +24B
core/NavHeadingMenu 8.9KB 9.0KB +65B 3.2KB 3.3KB +42B
core/NavIcon 3.4KB 3.5KB +63B 3.3KB 3.3KB +42B
core/NumberInput 55.7KB 56.4KB +776B 10.7KB 10.7KB +31B
core/Outline 28.6KB 29.4KB +794B 4.0KB 4.1KB +40B
core/OverflowList 8.5KB 8.6KB +67B 3.5KB 3.5KB +40B
core/Overlay 19.5KB 19.6KB +43B 3.8KB 3.9KB +17B
core/Pagination 87.7KB 89.0KB +1.3KB 15.3KB 15.7KB +395B
core/Popover 49.8KB 50.5KB +692B 12.1KB 12.1KB +20B
core/PowerSearch 155.1KB 156.4KB +1.3KB 18.6KB 19.0KB +406B
core/ProgressBar 7.7KB 7.8KB +66B 6.6KB 6.6KB +20B
core/RadioIndicator 3.8KB 3.9KB +61B 3.7KB 3.7KB +20B
core/RadioList 45.6KB 45.7KB +87B 8.0KB 8.0KB +22B
core/RadioListItem 52.1KB 53.3KB +1.2KB 9.7KB 10.1KB +401B
core/ResizeHandle 25.1KB 25.2KB +99B 3.9KB 3.9KB +41B
core/Section 7.5KB 7.6KB +67B 6.2KB 6.2KB +42B
core/SegmentedControl 38.8KB 38.9KB +99B 6.3KB 6.4KB +22B
core/SegmentedControlItem 8.6KB 9.3KB +757B 3.9KB 3.9KB +41B
core/SelectableCard 12.8KB 13.6KB +808B 7.1KB 7.2KB +46B
core/Selector 79.1KB 80.5KB +1.4KB 15.0KB 15.4KB +403B
core/SelectorOption 14.5KB 15.9KB +1.3KB 5.1KB 5.5KB +421B
core/SideNav 58.0KB 58.8KB +770B 10.6KB 10.6KB +21B
core/SideNavCollapseButton 46.6KB 47.4KB +774B 9.5KB 9.5KB +20B
core/SideNavHeading 54.7KB 55.5KB +764B 10.1KB 10.2KB +22B
core/SideNavItem 54.5KB 55.2KB +747B 10.1KB 10.1KB +22B
core/SideNavSection 6.5KB 6.6KB +59B 3.6KB 3.6KB +38B
core/Skeleton 3.8KB 3.8KB +60B 3.6KB 3.6KB +22B
core/Slider 48.5KB 48.5KB +84B 8.5KB 8.5KB +21B
core/Spinner 26.3KB 26.4KB +83B 7.5KB 7.5KB +19B
core/Stack 7.1KB 7.1KB +62B 4.9KB 4.9KB +44B
core/StackItem 6.0KB 6.1KB +63B 3.3KB 3.3KB +38B
core/StatusDot 19.0KB 19.0KB +59B 6.2KB 6.2KB +19B
core/Step 32.4KB 33.2KB +788B 5.3KB 5.3KB +22B
core/Stepper 47.4KB 48.2KB +780B 9.6KB 9.6KB +20B
core/Switch 48.4KB 49.2KB +785B 9.3KB 9.4KB +21B
core/Tab 9.7KB 10.5KB +780B 4.1KB 4.1KB +41B
core/TabList 40.2KB 40.3KB +90B 7.2KB 7.2KB +23B
core/TabMenu 51.6KB 52.4KB +786B 10.0KB 10.0KB +21B
core/Table 84.0KB 85.3KB +1.3KB 15.1KB 15.7KB +537B
core/TableBody 3.3KB 3.3KB +63B 3.2KB 3.2KB +42B
core/TableCell 78.8KB 80.1KB +1.3KB 14.8KB 15.4KB +534B
core/TableFooter 3.3KB 3.3KB +63B 3.2KB 3.2KB +42B
core/TableHeader 3.3KB 3.3KB +63B 3.2KB 3.2KB +42B
core/TableHeaderCell 78.6KB 79.9KB +1.3KB 14.8KB 15.3KB +547B
core/TableRow 6.2KB 6.3KB +66B 3.5KB 3.6KB +40B
core/TableSelectionToolbar 52.4KB 53.1KB +801B 11.2KB 11.2KB +21B
core/Text 8.6KB 8.6KB +66B 6.8KB 6.8KB +23B
core/TextArea 49.4KB 49.5KB +96B 9.4KB 9.4KB +21B
core/TextInput 53.5KB 54.3KB +772B 10.5KB 10.5KB +22B
core/Thumbnail 50.7KB 51.4KB +799B 11.0KB 11.0KB +25B
core/TimeInput 86.8KB 87.6KB +763B 13.8KB 13.8KB +21B
core/Timer 9.2KB 9.3KB +70B 6.8KB 6.8KB +23B
core/Timestamp 27.5KB 27.6KB +103B 9.5KB 9.6KB +21B
core/Toast 59.8KB 60.6KB +764B 9.7KB 9.7KB +22B
core/ToggleButton 44.1KB 44.9KB +762B 9.3KB 9.3KB +21B
core/ToggleButtonGroup 6.3KB 6.4KB +68B 3.2KB 3.3KB +42B
core/Token 29.6KB 30.4KB +783B 4.5KB 4.5KB +42B
core/Tokenizer 64.8KB 65.6KB +826B 11.7KB 11.7KB +21B
core/Toolbar 39.8KB 39.9KB +91B 7.7KB 7.8KB +46B
core/TopNav 51.9KB 52.6KB +727B 10.4KB 10.4KB +23B
core/TopNavHeading 54.2KB 54.9KB +748B 9.9KB 10.0KB +20B
core/TopNavItem 9.0KB 9.8KB +777B 3.8KB 3.9KB +39B
core/TopNavMegaMenu 54.2KB 54.9KB +783B 10.8KB 10.9KB +21B
core/TopNavMegaMenuFeaturedCard 6.7KB 6.8KB +66B 3.4KB 3.4KB +38B
core/TopNavMegaMenuItem 9.1KB 9.9KB +776B 4.1KB 4.1KB +38B
core/TopNavMenu 53.6KB 54.3KB +753B 10.1KB 10.1KB +23B
core/TreeList 32.5KB 33.8KB +1.3KB 4.7KB 5.1KB +423B
core/Typeahead 61.5KB 62.2KB +770B 11.3KB 11.3KB +21B
core/TypeaheadItem 6.2KB 6.3KB +60B 3.4KB 3.5KB +39B
core/VStack 7.3KB 7.3KB +61B 4.9KB 4.9KB +44B
lab/NavigationScreen N/A 68.1KB N/A N/A 1.2KB N/A
lab/NavigationStack N/A 68.1KB N/A N/A 1.2KB N/A
lab/SearchField N/A 40.9KB N/A N/A 1.5KB N/A

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.

⚠️ Touch-only size budget

These components grew past the touch-only budget (2 KB gzip JavaScript or 1 KB gzip CSS per component). If the growth runs only on a coarse pointer, load its JavaScript lazily so the import does not carry it, or justify the cost in the pull request description; CSS stays in the package stylesheet, so justify it.

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)
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)

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

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 27, 2026
@cixzhang cixzhang closed this Oct 28, 2026
@cixzhang cixzhang reopened this Oct 28, 2026
@cixzhang

Copy link
Copy Markdown
Contributor

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.

@vjeux
vjeux marked this pull request as draft October 29, 2026 22:39

This branch was successfully deployed

1 active deployment
Preview — aff1de99 Deployed Oct 27, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants