Skip to content

feat(core): under a finger a pressable highlights at touch-down, and waits only inside a scroller - #8117

Draft
vjeux wants to merge 1728 commits into
meta:mainfrom
vjeux:uikit/c01-press-profiles
Draft

vjeux wants to merge 1728 commits into
meta:mainfrom
vjeux:uikit/c01-press-profiles

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Review this PR alone: vjeux/astryx@uikit/c11-row-press-release...uikit/c01-press-profiles

User need

Part of the UIKit-parity work, which makes Astryx's touch presses feel like an iPhone's. Under a finger, every pressable waits 150 ms before showing the press. In a scrolling list that's right, because the touch may become a scroll. On a button outside any list it reads as lag: iOS highlights a control the moment it's touched.

Problem and solution fit

What led to it: comparing real touches on the iOS 26.2 simulator with Astryx. UIKit delays a touch only inside a scroll view (delaysContentTouches).

  • Under a finger, a pressable now highlights at touch-down, unless a scroller sits above it, where it still waits 150 ms (spec:AST-094 FR11). Rows follow the same rule.
  • Moving past the slop, or a scroll taking the touch, still cancels the press with no fade.

Current specification

spec:AST-094 FR11; design:user-states DR4.

Public delta and caller burden

No API change; usePressFeedback keeps its signature.

Design and alternatives

  • One wait for everything (before): rows right, buttons slow.
  • A profile per kind of surface (an earlier version of this PR): identical once rows follow FR11, so dropped.
  • Chosen: one rule by position, decided at touch-down. The controller walks up from the touched surface for a box that can scroll on an axis touch-action leaves to the browser, stopping at a fixed bar. It lives in the press controller, not the gesture core's scroll probe, because Core's main entry must stay free of the gesture core.

Testing

By hand on a phone or the iOS simulator, in the Press feedback stories: a held button in "Buttons" highlights at once; in "Buttons in a scroller" it waits briefly, and a flick scrolls without painting.

What was run: on #8077's controller without this change, 8 unit cases, 2 of the 7 real-touch button cases in Chromium (those outside a scroller) and the finger press accessibility test fail; with it they pass, along with the core unit suite, the button and list-row press specs in Chromium and WebKit, and the press accessibility tests. GitHub Actions isn't running on this repository right now, so these are local runs.

Evidence

Real touches Before After
iOS 26.2, a held button
Android 15, a held button

Size (gzip): Button JS +427 B, Item +423 B, TreeList +466 B; CSS +0.

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.

Stack: on #8077, which carries #8076 and #8360. Nothing of the closed #8082 and #8079 remains here.

@vercel

vercel Bot commented Oct 24, 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 29, 2026 12:45am UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟡 Reviewed (for maintainers only) Oct 29, 2026, 1:03 AM UTC

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 24, 2026
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 24, 2026
@vjeux
vjeux force-pushed the uikit/c01-press-profiles branch from 456f661 to 99d3b7d Compare October 24, 2026 06:34

@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 is on hold until an engineering owner rules on the draft touch-presentation spec (AST-094) that this pull request adds and builds on, including its proposed amendment of the current user-states pressed-overlay rules (DR1 and DR3, with DR4 also in play). An engineering owner records approval by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Whatever that ruling, these changes are needed:

  • A scroll the finger did not drive (a transcript pinned to the bottom, scrollIntoView, scroll anchoring) cancels the press and suppresses the tap's click in GestureHost's scroll handler, where only the browser's own pan with that finger should.
  • The cell profile waits 150 ms everywhere, while FR11 limits the delay to scroll views; make the code and the record agree.
  • The changeset and docs say the release fades over 250 ms, but at this head the stylesheet still fades over 200 ms.
  • The diff carries the whole 28,508-line stack; landing it in its stated order would leave this pull request reviewable as its own commit.

[Automated review]

}
}
if (taken.length > 0) {
this.arena.browserTookOver(taken, time);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Every scroll above a live touch lands here, including one the app drives (a transcript pinned to its end, scrollIntoView, scroll anchoring). browserTookOver sets suppressClick, so the lift cancels touchend and swallows the tap's click. The base only dropped the paint. Consider treating only the browser's own pan with this finger as a takeover, and dropping just the paint for any other scroll.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, and confirmed in the code: any ancestor scroll during a live touch went through browserTookOver, which sets suppressClick. The gesture host fix is #8309. A scroll takes the touch only after the finger has travelled the slop; a page-driven scroll under a still finger leaves the press and its click alone. Its test: held button, scrollTop changed by script, lift, and the click fires. This PR picks it up when #8309 lands.

},
/** A table or collection cell: delayed highlight, cancelled by the scroll. */
cell: {
highlightDelay: 'always',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

'always' makes a row wait 150 ms even where nothing can scroll, but draft FR11 says "The 150 ms delay applies only inside scroll views." Either use 'inScrollers' for cells or change FR11 to say a cell always waits.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The code is the intended behavior. A UIKit cell always sits in a scroll view, so it always waits the content-touch delay; the UIKit reference measured list cells highlighting about 145 to 150 ms after touch-down. FR11 is being amended to "a cell always waits the delay; a control waits only inside a scroller". That amendment is recorded in the body as proposed, pending owner review.

Comment thread .changeset/press-control-profile.md Outdated
'@astryxdesign/core': patch
---

[fix] Under a finger, a button, card, link or other control now highlights in the frame the finger lands, as UIKit's controls do, instead of after a 150 ms delay; inside a scroller it still waits that delay (a flick paints nothing), and a scroll that takes a held press cancels it with no fade and no click. The release fades over UIButton's 250 ms. List rows (Item, TreeList) keep a list cell's press: the 150 ms delay wherever they are, cancelled by 10 px of travel, released on the deselect clock. `usePressFeedback` takes `{profile: 'cell'}` for a local list row; the default is a control. A mouse on a desktop keeps `:active`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

At this head the stylesheet's fading animation still runs pressConsts.releaseMs: 200 (utils/interactionOverlay.stylex.ts:116); only the attribute lasts 250 ms. If this merges before the stylesheet follow-up, this note describes a fade users do not get.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 96cd3d7: this PR's changeset, hook docs and story no longer claim a 250 ms fade. The move of the stylesheet's release to UIButton's 250 ms, and those numbers, are in #8118.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

Modified Components

Button (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 712B 712B 0B
Lines of Code 584 584 -
Complexity Very High Very High (83) -
Item (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 284B 284B 0B
Lines of Code 1372 1378 -
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 879 -
Complexity Very High Very High (100) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/AlertDialog 56.9KB 58.7KB +1.8KB 12.4KB 12.5KB +24B
core/AppShell 57.9KB 59.5KB +1.6KB 11.6KB 11.7KB +22B
core/AspectRatio 3.7KB 3.8KB +66B 3.5KB 3.5KB +40B
core/Avatar 37.5KB 37.5KB +54B 6.3KB 6.3KB +23B
core/AvatarGroup 39.8KB 39.8KB +50B 6.4KB 6.5KB +22B
core/AvatarGroupOverflow 39.9KB 41.8KB +1.9KB 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 49.7KB +1.9KB 9.9KB 10.0KB +22B
core/BaseTypeahead 54.3KB 56.2KB +1.8KB 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 68.7KB +2.4KB ⚠️ 11.0KB 11.4KB +412B
core/Breadcrumbs 22.4KB 22.5KB +71B 3.3KB 3.3KB +40B
core/Button 43.3KB 45.1KB +1.8KB 9.2KB 9.2KB +21B
core/ButtonGroup 8.4KB 8.5KB +67B 3.3KB 3.3KB +40B
core/Calendar 54.9KB 56.8KB +1.9KB 10.0KB 10.0KB +22B
core/Card 6.4KB 6.5KB +66B 6.3KB 6.4KB +42B
core/Carousel 50.2KB 52.1KB +1.9KB 10.2KB 10.2KB +21B
core/Center 4.2KB 4.2KB +63B 4.6KB 4.6KB +42B
core/ChatComposer 60.6KB 62.5KB +1.9KB 10.8KB 10.8KB +19B
core/ChatComposerDrawer 23.9KB 23.9KB +66B 4.5KB 4.6KB +39B
core/ChatComposerInput 55.7KB 57.6KB +1.8KB 10.1KB 10.1KB +21B
core/ChatComposerTokenElement 55.7KB 57.6KB +1.8KB 10.1KB 10.1KB +21B
core/ChatDictationButton 46.5KB 48.3KB +1.8KB 9.5KB 9.5KB +20B
core/ChatLayout 48.8KB 50.7KB +1.9KB 9.9KB 10.0KB +20B
core/ChatLayoutScrollButton 46.4KB 48.3KB +1.9KB 9.6KB 9.6KB +20B
core/ChatMessage 23.1KB 23.1KB +59B 3.5KB 3.6KB +39B
core/ChatMessageBubble 7.0KB 7.1KB +67B 3.6KB 3.6KB +42B
core/ChatMessageList 28.9KB 29.0KB +71B 7.8KB 7.8KB +21B
core/ChatMessageMetadata 25.8KB 25.9KB +60B 3.9KB 3.9KB +23B
core/ChatSendButton 45.8KB 47.7KB +1.8KB 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 +65B 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 51.8KB +1.9KB 9.3KB 9.3KB +22B
core/CheckboxList 46.0KB 46.1KB +63B 8.1KB 8.2KB +22B
core/CheckboxListItem 54.0KB 56.3KB +2.3KB ⚠️ 10.5KB 11.0KB +541B
core/Citation 25.9KB 26.0KB +69B 4.1KB 4.2KB +41B
core/ClickableCard 12.8KB 14.7KB +1.9KB 6.9KB 7.0KB +46B
core/Code 3.5KB 3.6KB +63B 3.4KB 3.4KB +41B
core/CodeBlock 60.5KB 62.4KB +1.9KB 10.7KB 10.7KB +27B
core/Collapsible 11.7KB 13.6KB +1.9KB 4.2KB 4.3KB +39B
core/CollapsibleGroup 6.4KB 6.4KB +71B 3.2KB 3.2KB +40B
core/CommandPalette 50.9KB 52.7KB +1.8KB 11.8KB 11.8KB +23B
core/CommandPaletteEmpty 6.1KB 6.2KB +64B 3.3KB 3.4KB +42B
core/CommandPaletteFooter 23.3KB 23.3KB +68B 3.5KB 3.6KB +39B
core/CommandPaletteGroup 3.5KB 3.5KB +64B 3.3KB 3.4KB +39B
core/CommandPaletteInput 32.3KB 32.4KB +60B 8.1KB 8.1KB +22B
core/CommandPaletteItem 8.4KB 10.3KB +1.9KB 3.5KB 3.5KB +40B
core/CommandPaletteList 22.4KB 22.5KB +65B 3.3KB 3.3KB +41B
core/ComplexSelector 55.8KB 57.7KB +1.9KB 10.6KB 10.7KB +20B
core/ContextMenu 77.7KB 79.7KB +2.0KB ⚠️ 14.6KB 15.2KB +550B
core/ContextMenuItem 16.6KB 18.8KB +2.2KB ⚠️ 5.2KB 5.6KB +420B
core/DateInput 99.9KB 101.5KB +1.7KB 14.8KB 14.8KB +20B
core/DateRangeInput 69.4KB 71.2KB +1.8KB 11.2KB 11.2KB +21B
core/DateTimeInput 110.1KB 111.8KB +1.7KB 15.2KB 15.2KB +20B
core/Dialog 16.2KB 16.3KB +64B 7.1KB 7.1KB +23B
core/DialogHeader 48.8KB 50.7KB +1.9KB 9.8KB 9.8KB +20B
core/Divider 6.5KB 6.5KB +64B 3.6KB 3.6KB +43B
core/Drawer 31.6KB 31.7KB +85B 8.0KB 8.1KB +23B
core/DrawerHeader 48.9KB 50.8KB +1.9KB 11.2KB 11.2KB +22B
core/DropdownMenu 72.7KB 74.7KB +2.0KB ⚠️ 14.7KB 15.3KB +545B
core/DropdownMenuCheckboxItem 16.1KB 18.4KB +2.3KB ⚠️ 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 17.7KB +2.3KB ⚠️ 5.2KB 5.6KB +421B
core/DropdownMenuRadioGroup 6.0KB 6.1KB +62B 3.2KB 3.2KB +43B
core/DropdownMenuRadioItem 16.2KB 18.5KB +2.3KB ⚠️ 5.6KB 6.0KB +404B
core/DropdownMenuSubMenu 58.1KB 60.4KB +2.3KB ⚠️ 9.5KB 9.9KB +408B
core/EmptyState 7.7KB 7.8KB +64B 3.5KB 3.6KB +42B
core/Field 44.2KB 44.3KB +71B 8.0KB 8.0KB +22B
core/FieldLabel 39.8KB 39.8KB +58B 6.5KB 6.5KB +23B
core/FieldStatus 10.4KB 10.5KB +74B 4.2KB 4.3KB +23B
core/FileInput 54.1KB 55.9KB +1.9KB 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 45.1KB +1.8KB 9.2KB 9.2KB +21B
core/InputGroup 44.8KB 44.9KB +61B 8.0KB 8.1KB +21B
core/InputGroupText 6.3KB 6.4KB +66B 3.7KB 3.8KB +40B
core/Item 11.4KB 13.8KB +2.5KB ⚠️ 4.7KB 5.1KB +423B
core/Kbd 6.7KB 6.7KB +67B 3.5KB 3.5KB +40B
core/LayerProvider 63.3KB 65.1KB +1.8KB 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 53.0KB +1.8KB 9.8KB 9.8KB +21B
core/Link 43.5KB 45.3KB +1.8KB 7.6KB 7.6KB +25B
core/List 6.7KB 6.8KB +59B 3.4KB 3.5KB +40B
core/ListItem 53.5KB 55.7KB +2.3KB ⚠️ 10.5KB 11.0KB +541B
core/Markdown 144.5KB 146.4KB +1.9KB 17.4KB 17.9KB +545B
core/MetadataList 23.1KB 23.2KB +67B 3.6KB 3.6KB +42B
core/MetadataListItem 6.5KB 6.5KB +64B 3.4KB 3.4KB +43B
core/MobileNav 51.0KB 52.8KB +1.8KB 10.1KB 10.1KB +22B
core/MobileNavToggle 46.2KB 48.1KB +1.9KB 9.4KB 9.4KB +20B
core/MoreMenu 72.9KB 74.9KB +2.0KB ⚠️ 14.7KB 15.3KB +545B
core/MultiSelector 76.2KB 77.6KB +1.4KB 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 57.6KB +1.9KB 10.7KB 10.7KB +31B
core/Outline 28.6KB 30.4KB +1.8KB 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.6KB +1.9KB 15.3KB 15.7KB +395B
core/Popover 49.8KB 51.5KB +1.6KB 12.1KB 12.1KB +20B
core/PowerSearch 155.1KB 156.9KB +1.8KB 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.6KB +61B 8.0KB 8.0KB +22B
core/RadioListItem 52.1KB 54.4KB +2.3KB ⚠️ 9.7KB 10.1KB +401B
core/ResizeHandle 25.1KB 25.2KB +73B 3.9KB 3.9KB +41B
core/Section 7.5KB 7.6KB +67B 6.2KB 6.2KB +42B
core/SegmentedControl 38.8KB 38.9KB +73B 6.3KB 6.4KB +22B
core/SegmentedControlItem 8.6KB 10.5KB +1.9KB 3.9KB 3.9KB +41B
core/SelectableCard 12.8KB 14.6KB +1.8KB 7.1KB 7.2KB +46B
core/Selector 79.1KB 81.1KB +1.9KB 15.0KB 15.4KB +403B
core/SelectorOption 14.5KB 16.9KB +2.4KB ⚠️ 5.1KB 5.5KB +421B
core/SideNav 58.0KB 59.8KB +1.8KB 10.6KB 10.6KB +21B
core/SideNavCollapseButton 46.6KB 48.5KB +1.9KB 9.5KB 9.5KB +20B
core/SideNavHeading 54.7KB 56.5KB +1.8KB 10.1KB 10.2KB +22B
core/SideNavItem 54.5KB 56.4KB +1.9KB 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 +58B 8.5KB 8.5KB +21B
core/Spinner 26.3KB 26.4KB +58B 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 34.3KB +1.9KB 5.3KB 5.3KB +22B
core/Stepper 47.4KB 49.3KB +1.9KB 9.6KB 9.6KB +20B
core/Switch 48.4KB 50.3KB +1.9KB 9.3KB 9.4KB +21B
core/Tab 9.7KB 11.6KB +1.9KB 4.1KB 4.1KB +41B
core/TabList 40.2KB 40.3KB +63B 7.2KB 7.2KB +23B
core/TabMenu 51.6KB 53.5KB +1.9KB 10.0KB 10.0KB +21B
core/Table 84.0KB 85.9KB +1.9KB 15.1KB 15.7KB +537B
core/TableBody 3.3KB 3.3KB +63B 3.2KB 3.2KB +42B
core/TableCell 78.8KB 80.8KB +2.0KB 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 80.5KB +2.0KB 14.8KB 15.3KB +547B
core/TableRow 6.2KB 6.3KB +66B 3.5KB 3.6KB +40B
core/TableSelectionToolbar 52.4KB 54.2KB +1.9KB 11.2KB 11.2KB +21B
core/Text 8.6KB 8.6KB +66B 6.8KB 6.8KB +23B
core/TextArea 49.4KB 49.5KB +68B 9.4KB 9.4KB +21B
core/TextInput 53.5KB 55.4KB +1.9KB 10.5KB 10.5KB +22B
core/Thumbnail 50.7KB 52.6KB +1.9KB 11.0KB 11.0KB +25B
core/TimeInput 86.8KB 88.7KB +1.9KB 13.8KB 13.8KB +21B
core/Timer 9.2KB 9.3KB +70B 6.8KB 6.8KB +23B
core/Timestamp 27.5KB 27.6KB +74B 9.5KB 9.6KB +21B
core/Toast 59.8KB 61.7KB +1.9KB 9.7KB 9.7KB +22B
core/ToggleButton 44.1KB 45.9KB +1.8KB 9.3KB 9.3KB +21B
core/ToggleButtonGroup 6.3KB 6.4KB +68B 3.2KB 3.3KB +42B
core/Token 29.6KB 31.5KB +1.9KB 4.5KB 4.5KB +42B
core/Tokenizer 64.8KB 66.7KB +1.9KB 11.7KB 11.7KB +21B
core/Toolbar 39.8KB 39.8KB +63B 7.7KB 7.8KB +46B
core/TopNav 51.9KB 53.7KB +1.8KB 10.4KB 10.4KB +23B
core/TopNavHeading 54.2KB 55.9KB +1.8KB 9.9KB 10.0KB +20B
core/TopNavItem 9.0KB 10.9KB +1.9KB 3.8KB 3.9KB +39B
core/TopNavMegaMenu 54.2KB 56.0KB +1.9KB 10.8KB 10.9KB +21B
core/TopNavMegaMenuFeaturedCard 6.7KB 6.8KB +66B 3.4KB 3.4KB +38B
core/TopNavMegaMenuItem 9.1KB 11.0KB +1.9KB 4.1KB 4.1KB +38B
core/TopNavMenu 53.6KB 55.4KB +1.8KB 10.1KB 10.1KB +23B
core/TreeList 32.5KB 34.9KB +2.4KB ⚠️ 4.7KB 5.1KB +423B
core/Typeahead 61.5KB 63.3KB +1.8KB 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

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/BreadcrumbItem +2.4KB ⚠️ +412B
core/CheckboxListItem +2.3KB ⚠️ +541B
core/ContextMenu +2.0KB ⚠️ +550B
core/ContextMenuItem +2.2KB ⚠️ +420B
core/DropdownMenu +2.0KB ⚠️ +545B
core/DropdownMenuCheckboxItem +2.3KB ⚠️ +404B
core/DropdownMenuItem +2.3KB ⚠️ +421B
core/DropdownMenuRadioItem +2.3KB ⚠️ +404B
core/DropdownMenuSubMenu +2.3KB ⚠️ +408B
core/Item +2.5KB ⚠️ +423B
core/ListItem +2.3KB ⚠️ +541B
core/MoreMenu +2.0KB ⚠️ +545B
core/RadioListItem +2.3KB ⚠️ +401B
core/SelectorOption +2.4KB ⚠️ +421B
core/TreeList +2.4KB ⚠️ +423B

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
Package stylesheet Gzip before Gzip after Delta
@astryxdesign/core 34.6KB 35.1KB +555B

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 is on hold until an engineering owner rules on the draft touch-presentation spec (AST-094) that this pull request adds and builds on, including its proposed amendment of the current user-states pressed-overlay rules (DR1 and DR3, with DR4 also in play); an engineering owner records approval by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Whatever that ruling, these changes are needed:

  • A scroll the finger did not drive (a transcript pinned to its end, scrollIntoView, scroll anchoring) cancels a held press and swallows the tap's click in GestureHost's scroll handler, where only the browser's own pan with that finger should.
  • The cell profile waits 150 ms everywhere, while FR11 limits the delay to scroll views; make the code and the record agree.
  • The changeset and docs say the release fades over 250 ms, but the stylesheet still fades over 200 ms.
  • The diff carries the whole 28,511-line stack; landing it in its stated order would leave this pull request reviewable as its own commit.

[Automated review]

@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 is on hold until an engineering owner rules on the draft touch-presentation spec (AST-094) that this pull request adds and builds on, and on its proposed amendment of the current user-states pressed-overlay rules (DR1 and DR3), which an owner must approve or update; an engineering owner records approval by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Whatever that ruling, these changes are needed:

  • A scroll the finger did not drive (a transcript pinned to its end, scrollIntoView, scroll anchoring) cancels a held press and swallows the tap's click in GestureHost's scroll handler, where only a pan the finger drives should take it.
  • The cell profile waits 150 ms everywhere, while FR11 limits the delay to scroll views; make the code and the record agree.
  • The changeset, hook docs and story say the release fades over 250 ms, but the stylesheet still fades over 200 ms.
  • UsePressFeedbackOptions is not exported from @astryxdesign/core/hooks, unlike the option types of the other hooks.
  • The diff carries the whole 30,194-line stack; landing it in its stated order would leave this pull request reviewable as its own commit.

[Automated review]

@vjeux

vjeux commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

On the diff size: this PR is stacked on the foundation PRs listed at the top of the body and in its stack line. Only its last commit, 96cd3d7, is this change; the rest of the diff is its predecessors. It narrows as they land in the stated order.

@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: every runtime change rests on draft spec:AST-094, which no owner has approved and whose FR11 would amend the current pressed-state rules in design:user-states (DR1 and DR3). An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner, ideally on a head whose FR11 says what the code does: a list cell always waits 150 ms. Whatever the ruling, these still need changes:

  • A scroll the app performs during a held tap cancels the tap's click (gestures/GestureHost.ts:1214-1230); only a scroll the finger drove should take the touch.
  • gestures/README.md and the usePressFeedback docs still describe a 0.25 s control fade, while the stylesheet fades over 200 ms at this head.
  • The pull request carries its whole predecessor stack (30,332 authored lines for a 2,509-line commit); land or rebase onto the predecessors so only this commit remains.

[Automated review]

Comment on lines +1214 to +1230
const taken: number[] = [];
for (const touch of this.arena.touches) {
if (
touch.takenByBrowser ||
touch.phase === 'ended' ||
touch.phase === 'cancelled' ||
touch.pointerType !== 'touch' ||
this.arena.isClaimed(touch.id)
) {
continue;
}
if (touch.target instanceof Node && scroller.contains(touch.target)) {
taken.push(touch.id);
}
}
if (taken.length > 0) {
this.arena.browserTookOver(taken, time);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Every live, unclaimed touch under any scroll is handed to browserTookOver here, including a scroll the app performs (a pinned transcript, scrollIntoView). GestureArena.ts:342 then sets suppressClick, so the held tap loses its click. The finger-driven test above only arms the brake; apply a finger test here too (the touch travelled past the slop, or the browser sent pointercancel or an uncancelable touchmove).

},
/** A table or collection cell: delayed highlight, cancelled by the scroll. */
cell: {
highlightDelay: 'always',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The cell profile waits 150 ms wherever it is, while draft FR11 at this head says "The 150 ms delay applies only inside scroll views." Either the record or this value has to change before the record is approved; the author's reply says the record will.

Comment thread packages/core/src/gestures/README.md Outdated

- `control` (a button, a switch): the highlight comes on in the same frame
outside a scroller, the finger is tracked 70 pt beyond the bounds, and
the release takes UIButton's 0.25 s;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

At this head the stylesheet fades every profile over 200 ms (utils/interactionOverlay.stylex.ts:116); only the attribute's timer is 250 ms. Describe the 200 ms fade until the fade moves.

vjeux added 4 commits October 27, 2026 14:39
…busy first frame outran

pressFeedback.ts removes data-astryx-press="fading" on a timer of the
release's duration started at the write, for the controller's release and
for paintPress's. These fail on that code: with a release animation still
running, or still waiting for its first frame, when the clock runs out,
the release must stay fading until the animation finishes. Guards: a
cancelled animation ends it; no animation ends it on the clock (reduced
motion's instant release, a surface without a press layer); a
descendant's, an endless or a finished animation is not waited for; a new
press keeps its paint when the old release ends later.
…y first frame started it

endAfterRelease replaces the clock that removed data-astryx-press="fading"
at the release's duration after the write, for the controller's release
(apply's fade) and for every other painter's (paintPress). The clock now
only looks: at the release's duration, a release animation still running
on the surface (one of its own CSS animations, on it or its ::before or
::after, finite and unfinished) is waited for until it finishes or is
cancelled, so a busy main thread that starts the animation late ends it
late too, at strength 0. With none (reduced motion's instant release, a
surface without a press layer, an engine without Web Animations) the
release ends on the clock, as before. Nothing at the write reads the
document timeline: WebKit starts an animation created after such a read
at the time read, which would skip a busy first frame into the release.
Any new write, or a new press, cancels the wait; the controller's pending
clock is now a cancel function, so the release's wait and every timer
clear the same way.
Two quick taps on a button, the second while the first one's release
still runs, leave data-astryx-press="on" on the button until the next
touch. These fail on that code: pressFeedback.test.ts (a control tapped
twice 70 ms apart has no press once its releases end) and
ButtonPress.interaction.browser.spec.ts (two touchscreen taps 70 ms
apart, both engines: the press goes).
When a touch lands while the last tap's release still runs, the press
controller finishes that release first. It was the only press live and
the new finger was not recorded yet, so finishing it took the tracking
listeners down just before the new touch's press was painted: its lift
went unheard and data-astryx-press="on" stayed until the next touch.
The listeners are armed again after that finish.

@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.

Incremental review of f67ec83..2a8afd4; prior findings carried: spec_authority, authored_loc, context, user_impact, mechanism, evidence_by_spec_claim, concrete_defects, disposition

Changes are needed before this can be approved: the press profiles follow spec:AST-094 FR11 and the new fix clears a quick second tap as design:user-states DR1 requires, but the diff still carries the whole predecessor stack, 22,632 authored lines for a 2,533-line change, so please land #8082 and #8046 in the stated order and rebase, leaving only the press-profile change to review.

  • Before the gesture pull requests land, GestureHost's scroll handler should take a touch only after 10 px of travel (FR20, as in #8309).
  • main now attaches gestures for every pointer (FR19, FR43 and architecture:react-component-runtime INV13), so the host's (pointer: coarse) gate and its instanceof checks on recognizers need to change in those pull requests.

[Automated review]

Comment on lines +314 to +321
const coarseQuery =
options.isPrimaryPointerCoarse == null &&
typeof view?.matchMedia === 'function'
? view.matchMedia('(pointer: coarse)')
: null;
this.isPrimaryPointerCoarse =
options.isPrimaryPointerCoarse ?? (() => coarseQuery?.matches ?? false);
this.coarseQuery = coarseQuery;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

main now attaches gestures for every pointer: spec:AST-094 FR19 reads "Recognizers attach for every pointer, with FR43's exceptions", and architecture:react-component-runtime INV13 says "The gesture host attaches for every pointer, with no touch-only query". This (pointer: coarse) read still makes the host register nothing under a fine pointer except an action group's pointerdown (:476-494), so on a desktop a mouse or pen would get no pan or long press once a component adopts the host. The host and its tracker also tell recognizers apart with instanceof (:160, :344, :361), which INV13 now rules out. This belongs in the gesture pull requests before they land.

@vjeux

vjeux commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. This PR's own change is the press profiles and the double-tap fix. The 10 px scroll take is in #8309, the recognizer instanceof checks go in #8320, and the coarse-pointer gate goes in #8665. #8082, #8046, then this one is the intended merge order.

vjeux and others added 18 commits October 28, 2026 10:39
Main merged meta#8045 and meta#8046. The one conflict is utils/pressFeedback.ts:
it takes main's merged meta#8046 text (the finger and button slots, the mouse
and key press) and keeps this pull request's own additions, the one
writer of the press attribute (writePress, paintPress, readPressPaint)
and the header line naming them.
# Conflicts:
#	internal/interaction-harness/README.md
#	internal/interaction-harness/src/busy.test.ts
#	internal/interaction-harness/src/busy.ts
#	internal/interaction-harness/src/frames.ts
#	internal/interaction-harness/src/harness.interaction.browser.spec.ts
Main's button slot, merged with meta#8046, ends a press's release at once
when the next press starts, by clearing the press attribute directly. On
a row another painter holds, that clear erased the hold, which this pull
request's one writer refuses. A mouse press on a held row, released, then
a press elsewhere must leave the row held. Fails until the early end goes
through the one writer.
finish() writes none through writePress, as this pull request's
controller paints, so a held row stays its owner's when the next press
ends a release early, under the finger and the button slots alike.
Both upstream heads are folded onto main 70ea859 (her meta#8045 frame
evidence and meta#8046 coarse press): meta#8446 takes main's harness files, and
meta#8073 carries the press controller onto meta#8046's slots. meta#8076 has no change
of its own in either set of files, so the merge takes theirs with no hand
resolution, and meta#8076 is clean against main again.
# Conflicts:
#	internal/interaction-harness/README.md
#	internal/interaction-harness/src/busy.test.ts
#	internal/interaction-harness/src/busy.ts
#	internal/interaction-harness/src/frames.ts
#	internal/interaction-harness/src/harness.interaction.browser.spec.ts
#	packages/core/src/utils/pressFeedback.ts
…bute

From the review of meta#8076: the README said paintPress in
utils/pressFeedback.ts is the one writer of data-astryx-press, but the
action-group tracker on main (meta#8346) writes its groups' presses itself.
The sentence now says what holds at this head.
meta#8076's new head carries main 70ea859 with meta#8073's writer of the press
attribute on main's press controller, and its README names both writers.
utils/pressFeedback.ts keeps that controller with this line's own changes
on it: the pointer, drag, scroll and visibility listeners through the
shared input stream (meta#8356) and touchcancel only while the primary pointer
is coarse (meta#8270); the key path stays at the window's bubble phase, so a
Space press the page cancels paints nothing. usePressTracker stays in its
own module, one tracker for a component's life (meta#8315), so meta#8073's
rebuilt-tracker hook and its test are not taken. The harness files are
meta#8076's.
…s, and the action-group tracker writes its own

The header and writePress called it the one writer of the press attribute,
but main's action-group tracker writes its groups' presses itself. They now
say what holds, as meta#8076's gestures README does.
…ss release change

# Conflicts:
#	packages/core/src/utils/pressFeedback.ts
…he press attribute

The release note said every press painter writes data-astryx-press
through one internal writer, but the action-group tracker
(hooks/actionGroupTracker.ts) writes its groups' presses itself, as the
gestures README and utils/pressFeedback.ts already say. The note now says
paintPress writes every press it tracks and the action-group tracker
writes its own.
…) into the touch-down highlight

Takes meta#8077's press controller as it now stands (main's finger and
button slots, meta#8046's mouse-and-Space press, and the release that ends
with its animation) and keeps only this PR's behavior on it: under a
finger a surface highlights in the frame the finger lands unless a
scroller sits above it, where it waits the content-touch delay
(spec:AST-094 FR11). That is pressGesture's down.delayed and the
controller's isInsideScroller, with their tests.

The two press profiles go: they had become identical, meta#8077 moved the
row's release to its stylesheet, so Item and TreeList keep meta#8077's
pressable props. This PR's listener arming and brake rewrite are not
carried either; meta#8077's own listeners and brake stand.
…his branch

meta#8082 (a pushed row stays selected until the pop) and meta#8079 (a touch
press hides the dividers beside the row) are closed, so their code
leaves this branch: the selection hold and its tests, the divider
hiding and its tests, their changesets, and their edits to List, Item,
TreeListItem, the motion table, the row press layer and the coverage
and CSS tests, each back to meta#8077's text.
…4 FR11)

One reason per change: what this branch still carried of its earlier
scope goes. The listener-on-demand arming and the fling brake are gone
from the controller, so their browser case in ListRowPressPaths, the
motion table's brake comments and the hook's profile-export test go
too; the release-clock guard now reads PRESS_FADE_MS; the story,
the browser proofs and the changeset say what remains: under a finger
a pressable highlights in the frame the finger lands, and waits the
content-touch delay inside a scroller. The double-tap case goes with
the arming it guarded; meta#8077's 'two touches in a row' covers a second
tap.
… release change

# Conflicts:
#	packages/core/src/utils/pressFeedback.test.ts
…wn highlight

The only conflict is two test helpers added side by side
(makeScroller here, coarsePrimaryPointer there); both stay.
@vercel

vercel Bot commented Oct 28, 2026

Copy link
Copy Markdown

Deployment failed for project astryx with the following error:

The provided GitHub repository does not contain the requested branch or commit reference. Please ensure the repository is not empty.

@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.

Changes are needed before this can be approved: the touch-down highlight follows spec:AST-094 FR11 as written, but the diff still carries the whole predecessor stack, 29,580 authored lines for a 682-line change, so please land #8077 (with #8076 and #8360) and rebase, or retarget this pull request onto #8077's branch.

  • isInsideScroller steps by parentNode, so a pressable slotted into a shadow-tree scroller lights at touch-down; stepping with parentAcrossShadow from utils/flatTree.ts, with a test, fixes it.
  • Before the gesture pull requests land, the host needs to attach for every pointer, as FR19, FR43 and architecture:react-component-runtime INV13 now require.

[Automated review]

Comment on lines +190 to +193
node =
node.parentNode instanceof ShadowRoot
? node.parentNode.host
: node.parentNode;

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 steps from a slotted element to its light-DOM parent, so a scroller around the slot inside a shadow tree is never visited and the press lights at touch-down where spec:AST-094 FR11 asks for the 150 ms delay. parentAcrossShadow in utils/flatTree.ts already takes the flat-tree step (a slotted node goes to its assignedSlot), as the gesture core's scroll probe and the selection lock do; using it here, with a slotted test case, keeps the two scroller tests in agreement.

vjeux added 2 commits October 28, 2026 17:25
…he content-touch delay

The browser scrolls the flat tree, so a pressable slotted into a
scroller inside a shadow tree is inside that scroller (spec:AST-094
FR11). The controller's scroller check steps by parentNode, from the
slotted element to its host, and never sees the shadow scroller: the
press lights at touch-down. It must wait the delay.
…able in a shadow scroller waits

isInsideScroller now steps with parentAcrossShadow (utils/flatTree.ts),
the flat-tree parent the gesture host and the selection lock already
use: a slotted node goes to its slot and a shadow root to its host, so a
pressable slotted into a scroller inside a shadow tree waits the
content-touch delay (spec:AST-094 FR11).
@vjeux

vjeux commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. The shadow-tree case is fixed: the scroller check now walks the flat tree with parentAcrossShadow, with a test that failed first. The stack merges in order (#8360 and #8076, then #8077, then this PR); the host attaching for every pointer is #8665's.

@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.

Changes are needed before this can be approved: the touch-down highlight follows spec:AST-094 FR11 as written, and a pressable slotted into a shadow-tree scroller now waits too, but the diff still carries the whole predecessor stack, 29,603 authored lines for a 705-line change. Please land #8077 (with #8076 and #8360) and rebase, or retarget this pull request onto #8077's branch, so only this change remains.

  • Before the gesture pull requests land, the host needs to attach for every pointer, as FR19, FR43 and architecture:react-component-runtime INV13 require (gestures/GestureHost.ts:12-23).

[Automated review]

This branch was successfully deployed

1 active deployment
Preview — 0781001a Deployed Oct 29, 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