Skip to content

fix(Item): a list row's touch press release runs on the compositor - #8077

Draft
vjeux wants to merge 1704 commits into
meta:mainfrom
vjeux:uikit/c11-row-press-release
Draft

vjeux wants to merge 1704 commits into
meta:mainfrom
vjeux:uikit/c11-row-press-release

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Review only this PR's change: https://github.com/vjeux/astryx/compare/af9e82db13..83cc9619d7 (depends on #8890, which depends on #8360; the base adds main). Start with fix(Item): a list row's touch press release runs on the compositor.

User impact

Under a finger (ListItem, an actionable Item, a TreeList row), a pressed row's highlight should fade over 200 ms after the lift; it stayed pressed while the app's script ran, then snapped off.

Problem and solution fit

What led to it: this is part of a program to make Astryx's list components behave like iOS tables on phones. A tap usually starts work (navigating, loading), so the main thread is busiest exactly when the release plays, and the release animated a gradient that only the main thread can run.

The press now paints on a layer under the row's content and fades its opacity, which the browser animates off the main thread, on the same clock and curve (spec:AST-094 FR13, component:List FR11). #8890, split out of this pull request, makes the press machine end the press when that fade has played, not on a clock from the lift, which cut the fade short when script delayed its first frame. A mouse or trackpad row is unchanged (spec:AST-017 FR59).

Size (gzip, base → head, the repository's size report run against #8890): Item +481 B JS and +338 B CSS, ListItem +357/+326 B, TreeList +320/+335 B, and the 18 menus, tables, selectors and other components built on Item +364–554 B JS and +326–347 B CSS; every other core component 0 B, ContextMenuItem +11 B JS; the package stylesheet +348 B.

Expected behavior and authority

usePressFeedback ("fades the release over 200 ms"), design:user-states DR1/DR5.

Smallest restoration

Alternatives: keep the gradient (it freezes) or fade in JavaScript (it freezes the same way). An opacity layer is the one paint the compositor runs alone. Rows compose it from utils/rowPressLayer.ts, reading the press state utils/pressFeedback.ts writes: no second press machine.

Evidence

iOS 26.2 simulator, Mobile Safari, touch input (recorded at an earlier head; at this head the block and Reduce Motion paths were recorded again and match):

Path Clip
700 ms block at the lift, before: frozen, snapped Before
After: fades through the block After
Reduce Motion (DR2) Reduce Motion

Testing

  1. On an iPhone (or the iOS simulator), open Storybook's List "Interactive" story. Press and hold a row until it highlights, then lift: the highlight fades over about 0.2 s.
  2. In Safari's Web Inspector console, freeze the page for 0.7 s on the next lift: addEventListener('pointerup', () => { const end = performance.now() + 700; while (performance.now() < end); }, {once: true}). Press and lift a row: the highlight keeps fading through the freeze (before, it stayed pressed, then snapped off).
  3. Turn on Reduce Motion and repeat step 1: the same short fade.
  4. With a mouse: rows look and behave as before.

What was run: steps 1–3 on the iOS 26.2 simulator (the clips above), and unit and browser tests of each step in Chromium and WebKit, including a row held and released between two divided rows, whose dividers stay drawn by pixels and computed border (component:List FR11). All pass; the new ones failed before the change, and a page style that hides a pressed row's divider fails the divider case. These are local runs; no CI run is expected for this pull request.

@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 Error Error Oct 30, 2026 3:13am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 24, 2026
@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 24, 2026
@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 30, 2026, 3:35 AM UTC

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

This needs an engineering owner's decision before it can be approved: the draft spec:AST-094 adds new touch-presentation requirements, and the two row changes you marked "Proposed; pending owner review" (an interactive row's ::before becoming the press layer, and the untinted hasDividers divider) have no governing spec yet. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, which you can do yourself as an engineering owner, or by amending the Item, List and TreeList records. Whatever that decision, these need changes before merge:

  • RadioList options now tint the whole row under a finger and their indicator loses the pressed overlay that design:user-states DR1 requires, because Item adds the press layer to RadioListItem's delegate row.
  • The changeset says a caller's ::before replaces the press paint, but with xstyle the caller's ::before inherits the layer's opacity: 0 and positioning, so the caller's decoration disappears instead.
  • At 12,456 authored lines this carries ten separable predecessor commits; landing them first would leave only the row fix here.

[Automated review]

Comment thread packages/core/src/Item/Item.tsx Outdated
// The touch press on a compositor layer (after the overlay, whose
// own touch paint it turns off): the release keeps fading while
// script runs.
isInteractive && pressLayerStyles.cell,

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 also reaches RadioListItem, whose row is interactive through interactiveRef. There --_press-paint: transparent empties the radio indicator's press layer (it reads --_press-paint-image) and the new ::before, which rowStyles.root does not neutralize, tints the whole row, so under a finger the indicator no longer paints --color-overlay-pressed as design:user-states DR1 requires for RadioList. Keeping the layer off rows whose press paints elsewhere, plus a RadioList case in pressLayer.test.ts, would fix it.

Comment thread .changeset/list-row-press-compositor.md Outdated
'@astryxdesign/core': patch
---

[fix] A list row's touch press release keeps fading while the page runs script. `ListItem`, `Item` and `TreeList` rows used to stay fully pressed after the finger lifted until the main thread was free, and every frame of the fade repainted the row; the release now runs on the compositor, with the same color, clock and curve as before. On an interactive row the press paints on the row's `::before`, so a row's own `::before` set through `xstyle` or `className` replaces the press paint.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With xstyle, StyleX merges a caller's ::before with the layer per property: the caller's pseudo-element keeps the layer's opacity: 0, position: absolute, inset: 0, z-index: -1 and pressed background. So it is the caller's decoration that disappears (and turns into a press tint), not the press paint that is replaced; the note should say that.

follows.
- **FR7 — Exits animate as UIKit's do.** A touch presentation MUST play the
exit its UIKit counterpart plays. **Proposed; pending owner review:** this
departs from the draft `spec:AST-090` FR1 (exits off by default) and FR10

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

spec:AST-090 does not exist on main, so this amendment (and the matching line in Current-state impact) points at no record; cite the record that exists or drop the clause until it lands.

@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

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

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/AlertDialog 56.9KB 57.4KB +499B 12.4KB 12.5KB +24B
core/AppShell 57.9KB 58.3KB +418B 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 40.4KB +514B 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.3KB +497B 9.9KB 10.0KB +22B
core/BaseTypeahead 54.3KB 54.8KB +440B 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.3KB +933B 11.0KB 11.4KB +360B
core/Breadcrumbs 22.4KB 22.5KB +71B 3.3KB 3.3KB +40B
core/Button 43.3KB 43.8KB +489B 9.2KB 9.2KB +21B
core/ButtonGroup 8.4KB 8.5KB +67B 3.3KB 3.3KB +40B
core/Calendar 54.9KB 55.4KB +493B 10.0KB 10.0KB +22B
core/Card 6.4KB 6.5KB +66B 6.3KB 6.4KB +42B
core/Carousel 50.2KB 50.7KB +479B 10.2KB 10.2KB +21B
core/Center 4.2KB 4.2KB +63B 4.6KB 4.6KB +42B
core/ChatComposer 60.6KB 61.1KB +475B 10.8KB 10.8KB +19B
core/ChatComposerDrawer 23.9KB 23.9KB +66B 4.5KB 4.6KB +39B
core/ChatComposerInput 55.7KB 56.2KB +480B 10.1KB 10.1KB +21B
core/ChatComposerTokenElement 55.7KB 56.2KB +479B 10.1KB 10.1KB +21B
core/ChatDictationButton 46.5KB 47.0KB +475B 9.5KB 9.5KB +20B
core/ChatLayout 48.8KB 49.3KB +470B 9.9KB 10.0KB +20B
core/ChatLayoutScrollButton 46.4KB 46.8KB +461B 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 46.3KB +483B 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 50.4KB +461B 9.3KB 9.3KB +22B
core/CheckboxList 46.0KB 46.1KB +63B 8.1KB 8.2KB +22B
core/CheckboxListItem 54.0KB 54.8KB +804B 10.5KB 10.8KB +340B
core/Citation 25.9KB 26.0KB +69B 4.1KB 4.2KB +41B
core/ClickableCard 12.8KB 13.3KB +531B 6.9KB 7.0KB +46B
core/Code 3.5KB 3.6KB +63B 3.4KB 3.4KB +41B
core/CodeBlock 60.5KB 61.0KB +473B 10.7KB 10.7KB +27B
core/Collapsible 11.7KB 12.2KB +508B 4.2KB 4.3KB +39B
core/CollapsibleGroup 6.4KB 6.4KB +71B 3.2KB 3.2KB +40B
core/CommandPalette 50.9KB 51.4KB +476B 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 8.9KB +487B 3.5KB 3.5KB +40B
core/CommandPaletteList 22.4KB 22.5KB +65B 3.3KB 3.3KB +41B
core/ComplexSelector 55.8KB 56.3KB +518B 10.6KB 10.7KB +20B
core/ContextMenu 77.7KB 78.6KB +852B 14.6KB 15.0KB +360B
core/ContextMenuItem 16.6KB 17.4KB +831B 5.2KB 5.6KB +369B
core/DateInput 99.9KB 100.3KB +394B 14.8KB 14.8KB +20B
core/DateRangeInput 69.4KB 69.8KB +475B 11.2KB 11.2KB +21B
core/DateTimeInput 110.1KB 110.5KB +412B 15.2KB 15.2KB +20B
core/Dialog 16.2KB 16.3KB +64B 7.1KB 7.1KB +23B
core/DialogHeader 48.8KB 49.3KB +484B 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 49.4KB +466B 11.2KB 11.2KB +22B
core/DropdownMenu 72.7KB 73.5KB +854B 14.7KB 15.1KB +358B
core/DropdownMenuCheckboxItem 16.1KB 17.1KB +942B 5.6KB 5.9KB +350B
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.3KB +949B 5.2KB 5.5KB +370B
core/DropdownMenuRadioGroup 6.0KB 6.1KB +62B 3.2KB 3.2KB +43B
core/DropdownMenuRadioItem 16.2KB 17.1KB +936B 5.6KB 5.9KB +350B
core/DropdownMenuSubMenu 58.1KB 59.0KB +891B 9.5KB 9.9KB +357B
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 54.5KB +444B 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 43.8KB +477B 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 12.2KB +859B 4.7KB 5.0KB +372B
core/Kbd 6.7KB 6.7KB +67B 3.5KB 3.5KB +40B
core/LayerProvider 63.3KB 63.8KB +517B 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.6KB +446B 9.8KB 9.8KB +21B
core/Link 43.5KB 44.0KB +481B 7.6KB 7.6KB +25B
core/List 6.7KB 6.8KB +59B 3.4KB 3.5KB +40B
core/ListItem 53.5KB 54.2KB +792B 10.5KB 10.8KB +340B
core/Markdown 144.5KB 145.2KB +799B 17.4KB 17.7KB +350B
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 51.4KB +424B 10.1KB 10.1KB +22B
core/MobileNavToggle 46.2KB 46.7KB +477B 9.4KB 9.4KB +20B
core/MoreMenu 72.9KB 73.7KB +848B 14.7KB 15.1KB +358B
core/MultiSelector 76.2KB 76.6KB +439B 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.1KB +464B 10.7KB 10.7KB +31B
core/Outline 28.6KB 29.1KB +473B 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 88.6KB +870B 15.3KB 15.7KB +348B
core/Popover 49.8KB 50.2KB +402B 12.1KB 12.1KB +20B
core/PowerSearch 155.1KB 155.9KB +847B 18.6KB 19.0KB +354B
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 53.0KB +839B 9.7KB 10.0KB +351B
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 9.1KB +495B 3.9KB 3.9KB +41B
core/SelectableCard 12.8KB 13.3KB +523B 7.1KB 7.2KB +46B
core/Selector 79.1KB 80.0KB +900B 15.0KB 15.3KB +355B
core/SelectorOption 14.5KB 15.3KB +857B 5.1KB 5.4KB +370B
core/SideNav 58.0KB 58.4KB +445B 10.6KB 10.6KB +21B
core/SideNavCollapseButton 46.6KB 47.1KB +460B 9.5KB 9.5KB +20B
core/SideNavHeading 54.7KB 55.2KB +453B 10.1KB 10.2KB +22B
core/SideNavItem 54.5KB 54.9KB +455B 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 32.9KB +489B 5.3KB 5.3KB +22B
core/Stepper 47.4KB 47.8KB +450B 9.6KB 9.6KB +20B
core/Switch 48.4KB 48.9KB +471B 9.3KB 9.4KB +21B
core/Tab 9.7KB 10.2KB +506B 4.1KB 4.1KB +41B
core/TabList 40.2KB 40.3KB +63B 7.2KB 7.2KB +23B
core/TabMenu 51.6KB 52.1KB +455B 10.0KB 10.0KB +21B
core/Table 84.0KB 84.8KB +838B 15.1KB 15.5KB +347B
core/TableBody 3.3KB 3.3KB +63B 3.2KB 3.2KB +42B
core/TableCell 78.8KB 79.6KB +864B 14.8KB 15.2KB +347B
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.4KB +852B 14.8KB 15.2KB +360B
core/TableRow 6.2KB 6.3KB +66B 3.5KB 3.6KB +40B
core/TableSelectionToolbar 52.4KB 52.8KB +467B 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 54.0KB +470B 10.5KB 10.5KB +22B
core/Thumbnail 50.7KB 51.2KB +505B 11.0KB 11.0KB +25B
core/TimeInput 86.8KB 87.3KB +446B 13.8KB 13.8KB +21B
core/Timer 9.2KB 9.3KB +70B 6.8KB 6.8KB +23B
core/Timestamp 27.5KB 27.6KB +75B 9.5KB 9.6KB +21B
core/Toast 59.8KB 60.3KB +474B 9.7KB 9.7KB +22B
core/ToggleButton 44.1KB 44.6KB +457B 9.3KB 9.3KB +21B
core/ToggleButtonGroup 6.3KB 6.4KB +68B 3.2KB 3.3KB +42B
core/Token 29.6KB 30.1KB +504B 4.5KB 4.5KB +42B
core/Tokenizer 64.8KB 65.3KB +505B 11.7KB 11.7KB +21B
core/Toolbar 39.8KB 39.8KB +63B 7.7KB 7.8KB +46B
core/TopNav 51.9KB 52.3KB +428B 10.4KB 10.4KB +23B
core/TopNavHeading 54.2KB 54.6KB +431B 9.9KB 10.0KB +20B
core/TopNavItem 9.0KB 9.5KB +509B 3.8KB 3.9KB +39B
core/TopNavMegaMenu 54.2KB 54.6KB +474B 10.8KB 10.9KB +21B
core/TopNavMegaMenuFeaturedCard 6.7KB 6.8KB +66B 3.4KB 3.4KB +38B
core/TopNavMegaMenuItem 9.1KB 9.6KB +505B 4.1KB 4.1KB +38B
core/TopNavMenu 53.6KB 54.1KB +476B 10.1KB 10.1KB +23B
core/TreeList 32.5KB 33.3KB +771B 4.7KB 5.0KB +372B
core/Typeahead 61.5KB 61.9KB +453B 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.

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 34.9KB +355B

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

This needs an engineering owner's decision before it can be approved: the draft spec:AST-094 adds new touch-presentation requirements, and the two row changes you marked "Proposed; pending owner review" (an interactive row's ::before becoming the press layer, and the untinted hasDividers divider) have no governing spec yet. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, which you can do yourself as an engineering owner, or by amending the Item, List and TreeList records. Whatever that decision, these need changes before merge:

  • RadioList options now tint the whole row under a finger and their indicator loses the pressed overlay that design:user-states DR1 requires, because Item adds the press layer to RadioListItem's delegated row.
  • The changeset says a caller's ::before replaces the press paint, but with xstyle the caller's ::before inherits the layer's opacity: 0 and positioning, so the caller's decoration disappears instead.
  • lint runs out of heap at this head, and spec:AST-094 and the new motion docs cite records and files that do not exist (spec:AST-090, packages/core/src/gestures/README.md, sources.test.ts).
  • At 13,376 authored lines this carries ten separable predecessor commits; landing them first would leave only the row fix here.

[Automated review]

* tap-to-click, long press, double tap and the start of a page scroll, so
* many guesses can be settled by measuring Safari itself.
*
* SYNC: When modified, update packages/core/src/gestures/README.md (the

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

packages/core/src/gestures/README.md and the ../../gestures module named in this header (line 7) do not exist at this head, and scroll.ts, springs.ts and timing.ts credit a sources.test.ts that is really uikit/uikit.test.ts. Point these at files that exist, or drop the clauses until the gesture recognizers land.

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

This needs an engineering owner's decision before it can be approved: the draft spec:AST-094 adds new touch-presentation requirements, and the two row changes you marked "Proposed; pending owner review" (an interactive row's ::before becoming the press layer, and the untinted hasDividers divider) have no governing spec yet. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, which you can do yourself as an engineering owner, or by amending the Item, List and TreeList records. Whatever that decision, these need changes before merge:

  • RadioList options now tint the whole row under a finger and their indicator loses the pressed overlay that design:user-states DR1 requires, because Item adds the press layer to RadioListItem's delegated row; on its own this pull request ships that regression.
  • The changeset says a caller's ::before replaces the press paint, but with xstyle the caller's ::before inherits the layer's opacity: 0 and positioning, so the caller's decoration disappears instead.
  • spec:AST-094 and the new motion docs cite records and files that do not exist (spec:AST-090, packages/core/src/gestures/README.md, sources.test.ts, uikit/switch.ts, uikit/contextMenu.ts), and the description has no ## Writing rubric results for the new record.
  • At 13,532 authored lines this carries ten separable predecessor commits; landing them first would leave only the row fix here.

[Automated review]

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 24, 2026
vjeux added 5 commits October 29, 2026 10:18
…er and for the two stops the input stream fixes

- A still finger with sub-pixel jitter over a scroll view keeps its tap
  and scrolls nothing (spec:AST-094 FR18–FR22).
- A tap is still recognized under an app's window-capture
  stopPropagation, and under a document-capture
  stopImmediatePropagation registered before core's: the recognizers hear
  touches on the window's capture phase.
Chromium and WebKit pass.
…s once

Where nothing that claims is attached, the host's touchend listener is
passive and cannot cancel the engine's own click after a retargeted tap
clicks what is drawn. The shared click guard swallows that late click
whatever is attached; this case pins it, as asked on meta#8293.
…esture host's subscriptions

The gesture host subscribes no click: the one cancelling click subscriber
is the click guard's, added at a window's first arm and kept. The release
note, the stream's header and the gestures README listed click with the
host's touchend and contextmenu; they now name the guard's own.

@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 requested: the list-row release fix itself holds up (under a finger the highlight now keeps fading through script, held, resting and mouse rows match main, and the ::before and divider trade-offs follow the engineering owner's decision on this pull request), but the branch still carries all of #8890 with #8360 and #8076, about 27,600 of its 29,400 changed lines. Please rebase onto main once #8360 and #8890 land (or retarget the pull request) so the diff is the 18-file, 1,799-line row-press change.

[Automated review]

vjeux added 6 commits October 29, 2026 14:54
…tion constant

From review on meta#8890: spec:AST-094 FR4 lists the 200 ms Reduce Motion cap
among the touch motion roles and says a component never plays a literal.
The release capped its clock at a literal 200 in pressFeedback.ts
(REDUCED_MOTION_MAX_MS) and at a literal 200ms in the overlay's reduced
release duration. Both now read the shared constant: the controller
timing.reducedMotionCrossFadeMs, the stylesheet
reducedMotionFadeConsts.duration. Both are 200 ms, so nothing on screen
changes; the built stylesheet's cap is still 200 ms.

Red first: a unit case that stubs timing.reducedMotionCrossFadeMs at
120 ms found the release still fading at 120 ms on the change before
this one; it now ends there.
…Motion constant too

The row press layer capped its release under reduced motion at a literal
200ms (CELL_RELEASE_REDUCED), the same cap the review on meta#8890 asked the
overlay to read from the shared constant (spec:AST-094 FR4: the 200 ms
Reduce Motion cap is a role, never a literal). It now reads
reducedMotionFadeConsts.duration, as the overlay does; both are 200 ms,
and the built stylesheet's cap is still 200 ms.

A unit case holds every reduced-motion cap in the layer and the overlay to
the constant. Red first: it found the layer's literal on the change before
this one.
…ame window

Both 'a dip held before its first frame lands once the hold is …' proofs
waited for no running animation, then one frame and a timeout, assuming
the next step starts within a frame. The dip lands on a timer due on the
call's own clock, though, and the proofs passed only because the evaluate
round trips covered the gap: in WebKit on a loaded box the window closed
first and the proof read the element mid-dip (opacity 0, translate none;
reproduced 1 in 4 with the round trips removed). They now wait on the
call's own finished, then on the browser's own animations still running
or waiting for their first frame, and read nothing from the animator.
…ment at once and settles then

A Reduce Motion dip whose landing runs more than its whole fade back in
late (a hold released long after the dip was due) played a 1 ms opacity
animation, which WebKit keeps pending until its next frame, and settled
the call on a 1 ms timer: the call's finished could resolve while the
element still showed opacity 0. The animator's notes say the element
appears at once there. Fails now: the landing plays one opacity animation
and the call has not settled.
…element at once and settles

A Reduce Motion landing that runs at least the whole fade back in late
had nothing left of the cross-fade, but still played a 1 ms opacity
animation and settled the call 1 ms later. WebKit keeps that animation
pending until its next frame, so the call's finished could resolve with
the element still at opacity 0. Such a landing now writes the end opacity
at once (handing a borrowed one back to the stylesheet), plays nothing,
and settles the call then, as the animator's notes say.

@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 requested: the list-row release fix itself holds up (under a finger the highlight keeps fading through script, mouse and trackpad rows are unchanged, and an engineering owner has accepted the ::before and divider behaviour), but this pull request still carries all of #8890 with #8360 and #8076, about 27,600 of its 29,500 authored lines. Please rebase onto main once #8360 and #8890 land so the diff is the 18-file row-press change, and add a touch browser case that holds and releases a row between two divided rows of a hasDividers list and checks the divider stays drawn, which component:List FR11's verification asks for.

[Automated review]

import {TOUCH_QUERY} from './touchPresentation';
import {timing} from '../motion/uikit/timing';

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 import brings the whole UIKit timing table, and through timing.ts's own import the motion role registry module, into the press controller every pressable component loads, for one value. architecture:component-style-authoring INV6 says a shared value table "ships to a component only the rows that component uses". The line comes from #8890 and leaves this pull request with the rebase; it is worth fixing there, for example by reading the one constant without the table, as PRESS_FADE_MS reads pressConsts.

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.

Thanks, fixed in #8890 (286adf2): the controller now reads reducedMotionCrossFade.durationMs, the fade's own export, as the animator does, instead of the timing table. A bundle test of pressFeedback.ts finds durationMs and none of the table's other rows; before the change it found them.

expect(look.layer).toBe('none');
});

test('under a finger, a row at rest is as it was: its own ::before keeps its look and it is no stacking context; the press layer takes the ::before only while pressed', async ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

component:List FR11's verification also asks for "a pressed row between two divided rows" ("a divider hidden during a press" fails), and none of these cases uses a hasDividers list. A touch case that holds and releases a row between two divided rows and checks the divider stays drawn, by pixels and computed border-block-end, through the press and the release would close it.

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.

Thanks, added at 0d989ae: a touch case holds and releases the middle row of the Interactive story with hasDividers, and checks the dividers above and below it by computed border-bottom and by pixels, while held, halfway through the release, and after it. A page style that hides a pressed row's divider fails it.

vjeux added 5 commits October 29, 2026 16:34
…ot the timing table

From review (an inline comment on meta#8077 about this line): the press
controller, which every pressable component loads, imported the whole
UIKit timing table for one value, its reduced-motion cap, while
architecture:component-style-authoring INV6 ships a component only the
rows of a shared table it uses. It now reads the fade's own export,
reducedMotionCrossFade.durationMs, as the animator does; the value is
the same 200 ms.

Red first: a bundle of pressFeedback.ts carried the table's other rows
(stateChangeMs, buttonReleaseMs) on the change before this one; it now
carries the fade alone.
…th dividers drawn

From review on meta#8077: component:List FR11's verification asks for a
pressed row between two divided rows, and no case used a list with
dividers. A touch case opens the Interactive story with hasDividers,
holds the middle row and releases it, and checks the divider above it and
its own divider below, by computed border and by the pixels of each
divider's strip, while held, halfway through the release fade and after
it; each strip must differ from the row's surface just inside it at rest,
so a match is a drawn divider.

It passes in Chromium and WebKit. A page style that hides a pressed row's
divider fails it while the row is held.
… judges its first frame on that frame's time

'a spring started right before a block plays in full after it' waited a
fixed 300 ms of wall clock after the block, then read the screencast. In
the full interaction run on a loaded box no moving frame had reached the
screencast by then, and the proof failed on an empty read. It now waits
until the page shows the box a quarter of the way along and three more
frames reach the screencast. Its bound changes form, not strength: the
curve's own start (the handle's started) must come after the block, and
the first moving frame may be no further along than a curve started a
frame before the block ended, at that frame's own time, which is the old
30 px for a frame right after the block and still holds when a loaded
machine sends frames sparsely. A curve dated at the call is 107 px in.
…tor's notes drop an undefined law

The animator spec's header cited 'law M1' for compositor-only motion and
useAnimator.ts cited 'law M4' for an animator that never re-renders, and
no document defines either. The spec now cites spec:AST-094 FR5, which
keeps touch motion to transform, translate, scale, rotate and opacity;
useAnimator's sentence says what it means without the citation.

@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 requested: the list-row release fix itself holds up (under a finger the highlight keeps fading through script, mouse and trackpad rows are unchanged, an engineering owner has accepted the ::before and divider behaviour, and the new divided-row case covers the last verification case of component:List FR11), but this pull request still carries all of #8890 with #8360 and #8076, about 27,700 of its 29,600 authored lines. Please rebase onto main once #8360 and #8890 land, so the diff is the 18-file row-press change.

[Automated review]

vjeux added 5 commits October 29, 2026 18:21
…e surface

releaseAnimationsOf counted only the press owner's own animations (on it,
or its ::before or ::after). A component that draws its release on a
descendant, such as CheckboxInput's release on its box wrapper's ::after,
then lost the end of it: fading went at the 200 ms clock while the 250 ms
release still ran, and the frame it went relaid and repainted the box.

It now also counts a CSS release animation on a descendant whose nearest
press owner (closest [data-astryx-pressable]) is this element, the same
nearest-owner rule the layers read by, so a press owner nested inside
plays its own press's release and never extends this one.

Red first: a unit case whose release runs on a box inside the row found
fading gone at the clock on the change before this one; it now holds to
the release's end. A second case keeps a nested press owner's release,
on it or inside it, out of the row's.
…ide lands when its retire runs first

Reported as a WebKit hang of the held-dip proof under an 8-worker run:
the proof's wait for the call's finished never settled. Instrumented,
the page kept drawing frames (about 60 a second) while the element sat at
opacity 0 where it started, with no animation left: the held fade out had
been finished from outside, then retired (cancelled) by its timer before
the frame that reads the call's start, so the call never started and its
landing was never armed. This case orders the retire timer before that
frame and fails on this head: the move never lands.
… that cannot come [skip ci]

The retire timer cancels a finished animation once the main thread runs
past its curve. When a sampler finished a call's animations before the
browser started them, a loaded page could run that timer before the
frame that reads the call's start: the cancel left only idle animations,
the start was never read, and the call's Reduce Motion landing was never
armed, so the element stayed hidden where it started and the call never
settled. The retire now reads such a call's start from the finished
animation before cancelling it, and with nothing left to wait on the
call keeps its own clock, so its landing still runs.

@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 requested: the list-row release fix itself holds up (under a finger the highlight keeps fading through script, mouse and trackpad rows are unchanged, and an engineering owner has accepted the ::before and divider behaviour), but this pull request still carries all of #8890 with #8360 and #8076, about 27,800 of its 29,800 authored lines. Please rebase onto main once #8360 and #8890 land, so the diff is the 18-file row-press change. The description's compare link for this pull request's own change also stops before its last two commits.

[Automated review]

vjeux added 4 commits October 29, 2026 19:46
… curve, so only rows ship the row layer's constants

The shared press overlay (utils/interactionOverlay.stylex.ts) read its
release curve from the row layer's constants (cellPressConsts in
rowPressLayer.stylex.ts). The overlay reaches nearly every component,
and .stylex modules are side effects in core's package.json, so every
component then shipped the row layer's constants (the repository's size
report: +17-47 B gzip on each component without a press row), against
architecture:component-style-authoring INV6. The overlay goes back to
its own copy of the curve, main's literal; it and the row layer's copy
are both the constants table's timing.press.highlightOutCurve, and a
unit case fails if the overlay's drifts.

Red first: a source bundle of Card carried the row layer's constants
(cellRelease) on the change before this one; it now carries the overlay
and none of them.
The changeset, the release lookup's comment and its unit case named a
checkbox's box as a part that draws its release inside the surface. No
component in this change does: the checkbox's release runs on the row
around it. The lookup still counts a release on a descendant whose
nearest press owner is the surface; the text now says that without the
example.

@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 requested: the list-row release fix itself holds up (under a finger the highlight keeps fading through script, mouse and trackpad rows are unchanged, and an engineering owner has accepted the ::before and divider behaviour), and the newest commit keeps the row constants out of components that render no press row. This pull request still carries all of #8890 with #8360 and #8076, about 27,900 of its 29,900 authored lines, so please rebase onto main once #8360 and #8890 land, leaving the 18-file row-press change. While there, refresh the description's compare link and Size line, which still describe the previous revision.

[Automated review]

This branch had an error being deployed

1 failed deployment
Preview — 83cc9619 Deployed Oct 30, 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