Skip to content

chore(core): a table's reorder values and model, measured on iOS 26.2 - #8138

Draft
vjeux wants to merge 1931 commits into
meta:mainfrom
vjeux:uikit/c13-reorder-values
Draft

vjeux wants to merge 1931 commits into
meta:mainfrom
vjeux:uikit/c13-reorder-values

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #8106, and carries its current head. Review only this PR's change: vjeux/astryx@781c381...26a8bca (only merges since: main, then #8106's current head).

Why

A reorderable list on a phone should feel like the iOS one people know: the same pickup delay, autoscroll speed and drop rules. Those numbers must come from measuring iOS, not guessing.

What led to it

This is part of the UIKit-parity program, which makes Astryx's components behave like their iOS counterparts on touch screens. The List reorder (#8139) needs its values and rules first. This PR adds them, unused until then:

  • motion/uikit/reorder.ts holds values measured on iOS 26.2. A still finger picks a row up after 150 ms, so a flick scrolls instead. Autoscroll moves 2 to 10 points every 20 ms, depending on how deep the finger is into the edge.
  • List/reorderModel.ts holds the rules: where a dragged row lands, which rows it may pass (only pinned rows at the ends stop it), and keeping it inside the list.
  • MEASUREMENTS.md lists the three values that are still guesses.

Options and tradeoffs

  • A lift effect (scale, shadow) on a dragged row, or none.
  • Rows during the drag: slide aside as UIKit does, or stay still.
  • A guessed autoscroll start inset now, or wait to measure it.
  • Where a dragged row lands: UIKit's measured rule (a row changes places once the dragged centre passes its far edge less the dragged height), or spec:AST-103 FR1's slot whose centre is nearest the dragged centre. They agree for rows of one height and differ past a row of another (past the 88 pt row: 308 pt against 275 to 286 pt).

Why this choice

iOS 26.2 shows no lift, so neither does this. design:ordered-collection-reordering chose still rows, so UIKit's measured slide stays in the file header only. The 8 pt inset ships as a guess, marked in MEASUREMENTS.md, rather than holding the reorder back.

Owner decision: the model follows UIKit's measured landing rule, which the replay test pins. I propose that FR1's nearest-centre rule cover positions() and attached containers, with a list without positions() following UIKit's rule. The alternative is conforming the model to FR1 for lists too, which gives up iOS's measured landing points past rows of different heights.

How to check it

Nothing is visible yet; #8139 uses these values. I ran reorderModel.test.ts, and it passes: it replays a drag measured on iOS, a 44 pt row past an 88 pt row and back, and all 8 landing positions match. It also checks that a row at rest never moves and that a pinned row in the middle doesn't stop a drag; that one failed before its fix.

Evidence

The model's tests.

Stack (merge in this order): the motion foundation and the list stack through #8106 → this → the List reorder.

@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 6:31pm 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
@astracat-bot

astracat-bot Bot commented Oct 24, 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:32 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 owner decision before it can be approved. The reorder values and model rest only on draft spec:AST-094 FR1–FR3, which this branch adds without owner approval, and draft design:ordered-collection-reordering DR3 asks for surrounding rows to stay put until commit, while offsetsFor slides them during the drag. An engineering owner can settle it by commenting /approve-spec <full head SHA> on this pull request to approve FR1–FR3 for these values (the author can do this as an engineering owner), or by naming the record that should govern instead. Whatever that decision, these still need changes:

  • Land or retarget the stacked predecessors (#8045, #8062, #8106) so this pull request carries only the reorder commit; today it carries 18,141 authored lines.
  • proposedSlot proposes a new slot at pickup for any row more than twice the height of the row below it (a 100 pt row over 44 pt rows proposes slot 2 at rest), so count only the boundaries crossed since pickup and add a tall-row test.
  • The lint job runs out of memory on this head but passes on the base.

[Automated review]

}
const top = index < dragged ? rows[index].top : rows[index].top - shift;
const boundary = top + rows[index].height + gap / 2;
if (boundary < center) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

proposedSlot counts every boundary above the centre, so a row more than twice the height of the row below it starts past that row's boundary. For rows [44, 100, 44, 44], row 1 at its resting centre (94) returns slot 2, and offsetsFor slides row 2 up 100 pt before the finger moves. Counting only the boundaries crossed since pickup (start from dragged and step each time the centre passes the next boundary in the direction it moved) keeps every measured 44 pt case. A tall-row case in reorderModel.test.ts would pin it.

@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 1424 -
Complexity Very High Very High (140) -
TreeList (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 648B 648B 0B
Lines of Code 1068 1259 -
Complexity Very High Very High (138) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
charts/Chart N/A 64.7KB N/A N/A 20B N/A
charts/ChartAxis N/A 6.7KB N/A N/A 112B N/A
charts/ChartLegend N/A 54.0KB N/A N/A 20B N/A
charts/ChartTooltip N/A 54.0KB N/A N/A 20B N/A
core/AlertDialog 57.3KB 57.8KB +489B 12.5KB 12.6KB +21B
core/AppShell 66.8KB 67.2KB +430B 11.8KB 11.8KB +22B
core/AspectRatio 3.7KB 3.8KB +66B 3.5KB 3.5KB +40B
core/Avatar 37.8KB 37.9KB +52B 6.3KB 6.3KB +23B
core/AvatarGroup 40.2KB 40.2KB +51B 6.4KB 6.5KB +22B
core/AvatarGroupOverflow 40.3KB 40.8KB +512B 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 48.5KB 49.0KB +492B 10.1KB 10.1KB +21B
core/BaseTypeahead 55.0KB 55.4KB +468B 10.1KB 10.1KB +21B
core/Blockquote 3.5KB 3.6KB +66B 3.4KB 3.4KB +39B
core/BottomSheet 33.9KB 34.0KB +71B 7.9KB 7.9KB +24B
core/BottomSheetSwitcher 14.8KB 14.8KB +62B 3.3KB 3.3KB +41B
core/BreadcrumbItem 67.3KB 68.4KB +1.1KB 11.2KB 11.5KB +362B
core/Breadcrumbs 22.8KB 22.9KB +64B 3.3KB 3.3KB +40B
core/Button 43.7KB 44.2KB +494B 9.3KB 9.3KB +21B
core/ButtonGroup 8.4KB 8.5KB +67B 3.3KB 3.3KB +40B
core/Calendar 55.6KB 56.1KB +469B 10.2KB 10.2KB +23B
core/Card 6.4KB 6.5KB +66B 6.3KB 6.4KB +42B
core/Carousel 50.9KB 51.3KB +423B 10.3KB 10.4KB +22B
core/Center 4.2KB 4.2KB +63B 4.6KB 4.6KB +42B
core/ChatComposer 61.3KB 61.8KB +472B 11.0KB 11.0KB +20B
core/ChatComposerDrawer 24.3KB 24.3KB +65B 4.5KB 4.6KB +39B
core/ChatComposerInput 56.2KB 56.6KB +470B 10.2KB 10.2KB +20B
core/ChatComposerTokenElement 56.2KB 56.6KB +469B 10.2KB 10.2KB +20B
core/ChatDictationButton 47.2KB 47.6KB +472B 9.6KB 9.7KB +21B
core/ChatLayout 49.5KB 50.0KB +502B 10.1KB 10.1KB +22B
core/ChatLayoutScrollButton 47.0KB 47.5KB +474B 9.7KB 9.8KB +21B
core/ChatMessage 23.4KB 23.5KB +65B 3.5KB 3.6KB +39B
core/ChatMessageBubble 7.0KB 7.1KB +67B 3.6KB 3.6KB +42B
core/ChatMessageList 29.3KB 29.4KB +69B 7.8KB 7.8KB +21B
core/ChatMessageMetadata 26.4KB 26.5KB +65B 4.0KB 4.0KB +22B
core/ChatSendButton 46.4KB 46.9KB +487B 9.3KB 9.3KB +20B
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 32.4KB 32.5KB +69B 9.1KB 9.1KB +22B
core/CheckIndicator 9.2KB 9.3KB +64B 3.7KB 3.8KB +41B
core/CheckboxIndicator 4.1KB 4.1KB +65B 3.8KB 3.8KB +22B
core/CheckboxInput 50.5KB 50.9KB +452B 9.5KB 9.5KB +22B
core/CheckboxList 46.6KB 54.3KB +7.7KB ⚠️ 8.3KB 8.3KB +22B
core/CheckboxListItem 54.7KB 58.5KB +3.8KB ⚠️ 10.6KB 11.2KB +611B
core/Citation 26.5KB 26.6KB +68B 4.3KB 4.3KB +40B
core/ClickableCard 12.8KB 13.3KB +531B 6.9KB 7.0KB +45B
core/Code 3.5KB 3.6KB +63B 3.4KB 3.4KB +41B
core/CodeBlock 61.2KB 61.6KB +476B 10.8KB 10.9KB +21B
core/Collapsible 18.7KB 19.1KB +426B 4.4KB 4.4KB +41B
core/CollapsibleGroup 6.4KB 6.4KB +71B 3.2KB 3.2KB +40B
core/CommandPalette 51.5KB 52.0KB +474B 12.0KB 12.0KB +24B
core/CommandPaletteEmpty 6.1KB 6.2KB +64B 3.3KB 3.4KB +42B
core/CommandPaletteFooter 23.6KB 23.7KB +69B 3.5KB 3.6KB +39B
core/CommandPaletteGroup 3.5KB 3.5KB +64B 3.3KB 3.4KB +39B
core/CommandPaletteInput 32.9KB 33.0KB +60B 8.3KB 8.3KB +23B
core/CommandPaletteItem 8.4KB 8.9KB +487B 3.5KB 3.5KB +40B
core/CommandPaletteList 22.8KB 22.9KB +61B 3.3KB 3.3KB +41B
core/ComplexSelector 56.5KB 57.0KB +479B 10.8KB 10.8KB +20B
core/ContextMenu 75.3KB 87.4KB +12.1KB ⚠️ 14.8KB 15.4KB +626B
core/ContextMenuItem 53.5KB 54.0KB +448B 9.8KB 9.8KB +21B
core/DateInput 100.2KB 100.7KB +506B 14.9KB 15.0KB +20B
core/DateRangeInput 70.1KB 70.6KB +477B 11.4KB 11.4KB +21B
core/DateTimeInput 110.4KB 110.9KB +475B 15.4KB 15.4KB +21B
core/Dialog 16.2KB 16.3KB +64B 7.1KB 7.1KB +23B
core/DialogHeader 49.5KB 50.0KB +499B 10.0KB 10.0KB +20B
core/Divider 6.5KB 6.5KB +64B 3.6KB 3.6KB +43B
core/Drawer 31.6KB 31.6KB +57B 8.0KB 8.1KB +23B
core/DrawerHeader 49.6KB 50.1KB +482B 11.4KB 11.4KB +21B
core/DropdownMenu 73.4KB 74.6KB +1.2KB 15.0KB 15.6KB +613B
core/DropdownMenuCheckboxItem 16.4KB 17.5KB +1.2KB 5.7KB 6.1KB +352B
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.6KB 16.8KB +1.2KB 5.3KB 5.7KB +373B
core/DropdownMenuRadioGroup 6.0KB 6.1KB +62B 3.2KB 3.2KB +43B
core/DropdownMenuRadioItem 16.4KB 17.6KB +1.2KB 5.7KB 6.1KB +352B
core/DropdownMenuSubMenu 58.7KB 59.8KB +1.1KB 9.7KB 10.0KB +361B
core/EmptyState 7.7KB 7.8KB +64B 3.5KB 3.6KB +42B
core/Field 44.8KB 44.9KB +65B 8.1KB 8.2KB +22B
core/FieldLabel 40.3KB 40.4KB +61B 6.7KB 6.7KB +21B
core/FieldStatus 10.6KB 10.6KB +70B 4.4KB 4.4KB +22B
core/FileInput 54.8KB 55.2KB +442B 10.5KB 10.5KB +22B
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 9.0KB 9.1KB +67B 3.7KB 3.8KB +41B
core/IconButton 43.7KB 44.2KB +488B 9.3KB 9.3KB +21B
core/InputGroup 45.4KB 45.5KB +60B 8.2KB 8.2KB +22B
core/InputGroupText 6.3KB 6.4KB +66B 3.7KB 3.8KB +40B
core/Item 11.4KB 12.5KB +1.1KB 4.7KB 5.0KB +373B
core/Kbd 23.4KB 23.4KB +65B 3.5KB 3.5KB +40B
core/LayerProvider 64.0KB 64.5KB +475B 10.3KB 10.3KB +24B
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.9KB 52.3KB +448B 10.0KB 10.0KB +22B
core/Link 44.1KB 44.5KB +470B 7.7KB 7.8KB +25B
core/List 6.7KB 14.6KB +7.9KB ⚠️ 3.4KB 3.5KB +40B
core/ListItem 54.1KB 57.9KB +3.8KB ⚠️ 10.6KB 11.2KB +611B
core/Markdown 145.2KB 157.2KB +12.0KB ⚠️ 17.8KB 18.4KB +620B
core/MetadataList 23.5KB 23.6KB +71B 3.6KB 3.6KB +42B
core/MetadataListItem 6.5KB 6.5KB +64B 3.4KB 3.4KB +43B
core/MobileNav 51.7KB 52.1KB +410B 10.2KB 10.3KB +23B
core/MobileNavToggle 46.9KB 47.3KB +472B 9.6KB 9.6KB +21B
core/MoreMenu 73.7KB 74.9KB +1.2KB 15.0KB 15.6KB +613B
core/MultiSelector 73.6KB 74.1KB +483B 14.8KB 14.8KB +24B
core/NavHeadingMenu 13.0KB 13.0KB +77B 3.3KB 3.3KB +39B
core/NavIcon 3.4KB 3.5KB +63B 3.3KB 3.3KB +42B
core/NumberInput 56.4KB 56.9KB +493B 10.9KB 10.9KB +20B
core/Outline 29.0KB 29.5KB +471B 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 85.5KB 86.6KB +1.2KB 15.5KB 15.8KB +348B
core/Popover 50.3KB 50.7KB +480B 12.2KB 12.2KB +20B
core/PowerSearch 166.6KB 178.4KB +11.8KB ⚠️ 20.1KB 20.4KB +355B
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 46.2KB 46.3KB +64B 8.2KB 8.2KB +21B
core/RadioListItem 52.8KB 53.9KB +1.1KB 9.8KB 10.2KB +352B
core/ResizeHandle 25.5KB 25.5KB +71B 3.9KB 3.9KB +41B
core/Section 7.5KB 7.6KB +67B 6.2KB 6.2KB +42B
core/SegmentedControl 39.1KB 39.2KB +72B 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 +43B
core/Selector 77.0KB 78.2KB +1.1KB 15.2KB 15.5KB +349B
core/SelectorOption 14.7KB 15.9KB +1.2KB 5.2KB 5.6KB +373B
core/SideNav 64.6KB 65.0KB +418B 10.7KB 10.7KB +21B
core/SideNavCollapseButton 47.3KB 47.8KB +482B 9.6KB 9.6KB +20B
core/SideNavHeading 59.2KB 59.6KB +467B 10.4KB 10.4KB +20B
core/SideNavItem 55.3KB 55.7KB +450B 10.3KB 10.3KB +21B
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 49.1KB 49.2KB +65B 8.7KB 8.7KB +20B
core/Spinner 26.7KB 26.8KB +55B 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 33.0KB 33.5KB +484B 5.5KB 5.5KB +23B
core/Stepper 48.1KB 48.5KB +463B 9.7KB 9.8KB +21B
core/Switch 49.0KB 49.4KB +453B 9.5KB 9.5KB +21B
core/Tab 9.7KB 10.2KB +501B 4.1KB 4.1KB +41B
core/TabList 40.6KB 40.7KB +72B 7.3KB 7.3KB +22B
core/TabMenu 55.9KB 56.4KB +444B 10.2KB 10.3KB +20B
core/Table 84.7KB 96.7KB +12.0KB ⚠️ 15.6KB 16.2KB +618B
core/TableBody 3.3KB 3.3KB +63B 3.2KB 3.2KB +42B
core/TableCell 76.5KB 88.5KB +12.0KB ⚠️ 15.3KB 15.9KB +620B
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 76.1KB 88.2KB +12.1KB ⚠️ 15.0KB 15.6KB +611B
core/TableRow 6.4KB 6.5KB +57B 3.8KB 3.8KB +42B
core/TableSelectionToolbar 52.8KB 53.3KB +473B 11.3KB 11.3KB +21B
core/Text 8.6KB 8.6KB +66B 6.8KB 6.8KB +23B
core/TextArea 50.0KB 50.1KB +80B 9.5KB 9.6KB +22B
core/TextInput 54.2KB 54.6KB +427B 10.6KB 10.6KB +20B
core/Thumbnail 51.4KB 51.8KB +476B 11.1KB 11.2KB +23B
core/TimeInput 87.5KB 87.9KB +401B 13.9KB 13.9KB +20B
core/Timer 9.2KB 9.3KB +70B 6.8KB 6.8KB +23B
core/Timestamp 27.9KB 28.0KB +71B 9.7KB 9.7KB +21B
core/Toast 60.6KB 61.0KB +488B 9.9KB 9.9KB +22B
core/ToggleButton 44.5KB 45.0KB +463B 9.4KB 9.4KB +21B
core/ToggleButtonGroup 6.3KB 6.4KB +68B 3.2KB 3.3KB +42B
core/Token 30.2KB 30.6KB +482B 4.6KB 4.7KB +41B
core/Tokenizer 69.3KB 69.8KB +470B 16.7KB 16.7KB +20B
core/Toolbar 40.1KB 40.2KB +67B 7.7KB 7.8KB +46B
core/TopNav 52.5KB 52.9KB +431B 10.5KB 10.5KB +21B
core/TopNavHeading 58.6KB 59.1KB +490B 10.2KB 10.2KB +20B
core/TopNavItem 9.0KB 9.5KB +509B 3.8KB 3.9KB +39B
core/TopNavMegaMenu 55.0KB 55.5KB +464B 11.0KB 11.0KB +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 58.3KB 58.7KB +458B 10.3KB 10.3KB +23B
core/TreeList 40.4KB 51.4KB +11.0KB ⚠️ 4.8KB 5.2KB +384B
core/Typeahead 62.2KB 62.6KB +476B 11.4KB 11.5KB +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/ChartDotGL N/A 7.4KB N/A N/A 20B N/A
lab/ChartDotGLInteractive N/A 8.3KB N/A N/A 20B N/A
lab/ChartHeatmapGL N/A 8.5KB N/A N/A 20B N/A
lab/ChartStreamGL N/A 7.4KB N/A N/A 20B N/A
lab/ChatEmojiPicker N/A 66.2KB N/A N/A 773B N/A
lab/ChatReactionBar N/A 66.8KB N/A N/A 1.1KB N/A
lab/ChatReasoning N/A 7.0KB N/A N/A 967B N/A
lab/ChatTypingIndicator N/A 24.0KB N/A N/A 612B N/A
lab/ChatUnreadDivider N/A 3.6KB N/A N/A 348B N/A
lab/CircularProgress N/A 7.2KB N/A N/A 1.0KB N/A
lab/CodeEditor N/A 14.6KB N/A N/A 665B N/A
lab/CommentComposer N/A 95.2KB N/A N/A 1.9KB N/A
lab/CommentPin N/A 48.4KB N/A N/A 1.2KB N/A
lab/CommentThread N/A 228.3KB N/A N/A 2.4KB N/A
lab/Comments N/A 233.0KB N/A N/A 2.5KB N/A
lab/DiffViewer N/A 46.6KB N/A N/A 2.1KB N/A
lab/EmojiPicker N/A 64.1KB N/A N/A 722B N/A
lab/InfoTip N/A 22.0KB N/A N/A 458B N/A
lab/ListInput N/A 76.3KB N/A N/A 1.6KB N/A
lab/LogStream N/A 8.0KB N/A N/A 1.6KB N/A
lab/MobilePowerSearch N/A 159.3KB N/A N/A 1.4KB N/A
lab/MobileTokenizer N/A 93.2KB N/A N/A 809B N/A
lab/SVGIcon N/A 7.6KB N/A N/A 764B N/A
lab/Schedule N/A 60.9KB N/A N/A 3.9KB N/A
lab/Stat N/A 4.1KB N/A N/A 646B N/A
lab/ThreeDScatterGL N/A 7.6KB N/A N/A 20B N/A
lab/TourStep N/A 56.1KB N/A N/A 590B N/A
lab/TransferList N/A 76.7KB N/A N/A 1.7KB N/A
lab/TransferListSelector N/A 82.4KB N/A N/A 1.9KB N/A
richtext/RichTextEditor N/A 271.9KB N/A N/A 2.6KB N/A
richtext/RichTextEditorToolbar N/A 214.1KB N/A N/A 454B N/A
richtext/RichTextView N/A 252.3KB N/A N/A 2.3KB 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/CheckboxList +7.7KB ⚠️ +22B
core/CheckboxListItem +3.8KB ⚠️ +611B
core/ContextMenu +12.1KB ⚠️ +626B
core/List +7.9KB ⚠️ +40B
core/ListItem +3.8KB ⚠️ +611B
core/Markdown +12.0KB ⚠️ +620B
core/PowerSearch +11.8KB ⚠️ +355B
core/Table +12.0KB ⚠️ +618B
core/TableCell +12.0KB ⚠️ +620B
core/TableHeaderCell +12.1KB ⚠️ +611B
core/TreeList +11.0KB ⚠️ +384B

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 35.4KB 36.0KB +648B

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

@vjeux

vjeux commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

On the predecessor stack: this pull request is stacked, as its first line says, and only its last commit (a0df534, 7 files, +606 −2) is its own. It rebases onto main as each predecessor lands, and the diff then shrinks to that commit.

On lint: the job runs out of JavaScript heap on the CI runner with the stacked diff. ESLint passes on every file this commit touches.

@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf.

Parts of this PR re-implement behavior that already exists, or is being specified, as a shared part of core. Converging keeps one implementation per behavior:

This is about where the behavior lives, not about the PR's intent.

@vjeux

vjeux commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Agreed: one implementation.

This pull request's own commit (the reorder values and model) reads no pointer, writing direction or motion preference. The List reorder that uses it keys its motion on the resolver's touch presentation (useTouchPresentation), the same on every touch screen.

@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Pushed: bf440cf, on #8106 at 4d54dbc, which carries all three.

@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 reorder values and model rest only on draft spec:AST-094 FR1–FR3, which this branch adds without owner approval, while draft design:ordered-collection-reordering DR3 asks for surrounding rows to stay put until commit and AST-094 FR4 says touch motion is not themable, which the new reorder.slide.spring role contradicts. An engineering owner can settle it by commenting /approve-spec <full head SHA> on this pull request to approve AST-094 for these values (the author can do this as an engineering owner), or by naming the record that should govern them instead; that approval reads the record against the ## Writing rubric results the description does not yet carry. Whatever that decision, these still need changes:

  • Land or retarget the stacked predecessors so this pull request carries only the reorder commit; it now carries 39,053 authored lines.
  • The carried platform adaptation condition in themeAdaptations.ts contradicts the closed when vocabulary of spec:AST-012 FR2.
  • uikit.reorder now reaches @astryxdesign/core/motion/experimental without @experimental or an [experimental] changeset; drop the barrel re-export or declare it.

[Automated review]

* is everything else. Pair `ios` with `pointer: 'coarse'` for a touch
* profile, such as touch motion tuned per platform.
*/
platform?: 'ios' | 'other';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

platform widens the adaptation when vocabulary that spec:AST-012 FR2 closes: "A when object may contain width, height, pointer, contrast, and motion". Drop it here, or have an engineering owner amend FR2 before it ships.

} from './gestures';
export {list} from './list';
export {navigation} from './navigation';
export {reorder} from './reorder';

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 re-export puts reorder on the experimental @astryxdesign/core/motion/experimental subpath as uikit.reorder (motion/experimental.ts:71), but reorder carries no @experimental and .changeset/reorder-values.md is a [chore] that says it is not exported (spec:AST-017 FR21–FR22). registerTables.ts and reorderModel.ts import ./reorder by path, so dropping this line keeps the table internal; otherwise tag it @experimental and make the changeset [experimental].

Comment thread packages/core/src/List/reorderModel.ts Outdated
Comment on lines +14 to +20
* UIKit's rules (iOS 26.2, measured): the proposed slot changes when the
* dragged row's centre crosses a row boundary of the layout WITHOUT the
* dragged row, with no hysteresis; the rows between the dragged row's place
* and its slot each move by the dragged row's height; the drop goes to the
* current slot, from rest, on a spring whose duration scales with the
* remaining distance; autoscroll moves a whole number of points per tick,
* more the deeper the dragged row's edge is past the scroll view's edge.

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 still states the measured rule as a crossing of "the layout WITHOUT the dragged row". proposedSlot (lines 102–108) follows that only while the row below is at least as tall, or the row above at most as tall, as the dragged row; otherwise it clamps to the neighbour's near edge, and no measurement covers those cases (a 44 pt row under an 88 pt row now changes places after 66 pt, where this rule gives 22 pt). Stating the implemented rule here and marking the clamp cases unmeasured would keep the header honest.

Comment thread packages/core/src/List/ListItem.tsx Outdated
Comment on lines +171 to +182
'::after': {
content: {
default: null,
'[data-astryx-travelling]:not([aria-selected="true"],[aria-current="true"])':
'""',
},
position: 'absolute',
inset: 0,
zIndex: -2,
borderRadius: 'inherit',
pointerEvents: 'none',
backgroundColor: colorVars['--color-background-surface'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These ::after properties apply to every row, and the caller's xstyle is merged after them, so an ::after decoration passed through xstyle inherits position: absolute, inset: 0, z-index: -2 and the surface background unless it sets each one. .changeset/list-row-press-compositor.md tells callers to move row decoration to ::after. Scoping these properties to [data-astryx-travelling], or pointing the changeset at a child element for ListItem, avoids the trap.

vjeux added 27 commits October 28, 2026 17:42
…lowance under every primary pointer

The useMenuPress record's FR9 governs the trigger's held finger by
spec:AST-094 FR29 under every primary pointer (FR43). The trigger now
hands the long press FR29's 13 px drift allowance whatever the primary
pointer, so it no longer reads (pointer: coarse) as its press starts, and
the menus drop their coarsePointer import. ContextMenu keeps the long
press's own allowable movement. The changeset says the same.
From review on meta#8106: component:List FR12's verification lists layout or
paint per frame as a failure for an insert, a removal and a reorder, and
the trace covered the insert only. It now runs for all three; each
passes in Chromium.
…wever late a busy first frame started it

The touch press controller removed `fading` on a clock started at the
lift. A busy main thread delays the release animation's first frame, and
with it its end, so the clock cut the release short mid-fade. The
controller now checks at that clock for the surface's own finite release
animation (on it, or its `::before` or `::after`) and, while one runs or
still waits for its first frame, ends the release when it finishes or is
cancelled; with none, the release ends at the clock. Timers become cancel
functions, so a release that waits on an animation is cancelled the same
way.

The clock is the table's, capped at 200 ms under reduced motion, and the
overlay's release animation is capped the same way
(design:reduced-motion DR2: a touch transition finishes in 200 ms or
less); the stylesheet tests read the capped durations.

This reaches every surface the touch press controller drives (buttons,
switches, checkboxes, cards, menu rows, list rows). Split out of meta#8077 on
review; meta#8077 builds its compositor row release on it.

Red first: with the previous controller, three of the seven new cases
fail (a release whose first frame came late, one still waiting when the
clock ran out, another painter's release); the other four guard what
stays (a cancelled release ends, no animation ends at the clock, other
animations are ignored, a new press owns its paint).
…e row press release change

# Conflicts:
#	packages/core/src/__tests__/pressState.ts
#	packages/core/src/utils/interactionOverlay.stylex.ts
#	packages/core/src/utils/pressFeedback.test.ts
#	packages/core/src/utils/pressFeedback.ts
#	scripts/build-css.test.mjs
…d the changeset says what a caller's ::before does

From review on meta#8077:
- The headers of motion/uikit/list.ts, both ListRowPress browser specs
  and interactionOverlay.cell.test.ts, and the notes in build-css.test.mjs
  and __tests__/pressState.ts, pointed the row press layer at
  utils/interactionOverlay.stylex.ts; it lives in utils/rowPressLayer.ts
  (rowPressLayerStyles.cell), its clocks in rowPressLayer.stylex.ts
  (cellPressConsts).
- Notes that described the release as ending on a timer now say the
  controller checks at the release clock and the release ends with its
  animation; the stylesheet tests no longer parse a theme motion role
  the release stopped playing.
- The changeset said a caller's ::before decoration takes the layer's
  paint during a press. Under a finger any ::before property a caller
  sets wins over the layer's, so a decoration with its own size and
  background jumps to the row's corner and the row shows no pressed fill
  until the release ends; it now says so and keeps the advice to
  decorate with ::after or a child.
…ontroller change split out) into the touch row target change

# Conflicts:
#	.changeset/list-row-press-compositor.md
…le uses

From review on meta#8085: the exemption let a file that paints a press pass
the guard whenever it rendered a marked component through `xstyle`. It
came with ListItem's divider arms, which the revert of meta#8079 took out,
so no file at this head reaches it, and in place it would wave through a
file whose press styles never reach that component's root. The guard is
again exactly as on the line below; it passes without the exemption.
…never needed

From review on meta#8077: the build-css press-paint check filtered out a
`--_press-paint: transparent` declaration, with a note, that the row
layer never writes (only the overlay declares the press paint), and
interactionOverlay.cell.test.ts defined a `hostRules` helper no case
uses. Both go; the stylesheet check is as on the line below again.
…ers a row with an end control

From review on meta#8085:
- `isTouchClick` and its comment sat between `Item`'s docblock and
  `export function Item`, so the docblock attached to the helper and
  `Item` lost its description in editors and Storybook. The helper now
  sits above the docblock.
- component:List FR10's verification names a row with an end control.
  A new List story puts a compact row whose end content is a switch
  between two interactive rows, and the browser spec checks that the
  interactive rows grow only to half the gap to the switch, that the
  browser hit-tests the switch at its center, and that a real tap toggles
  it with no tap reaching the rows beside it. With the hit area's
  neighbour clamp removed, the case fails (a band of 5.5 px where half
  the gap is 2 px).
…meta#8890 and meta#8360's current head) into the tree expansion change

# Conflicts:
#	packages/core/src/motion/uikit/list.ts
…ast its divider

From review on meta#8085, two ways a tap in an interactive row's band missed
the row:
- The band is the row's ::after, and the rows after it are positioned
  with no z-index, so a later row that takes no taps (a static row, such
  as the switch row in the "With end controls" story) painted and
  hit-tested over the band. The layer now has z-index: 1 on itself; its
  even-odd clip still leaves the row's own box to the row's content, and
  the row still makes no stacking context.
- The layer is offset from the row's padding box, while the insets are
  measured from its border box, so below a divided row the band was the
  divider's width short. A host that draws a border now declares it
  (--_hit-border-{top,right,bottom,left}), and a divided ListItem declares
  its divider's, so the band starts at the divider's outer edge.

Above everything at z-index auto, the band still takes no tap from a
control near the list: the hit area's neighbour clamp measures every
interactive element on the page, not only rows. The "With end controls"
story gains a button in its header and one 4 px after the list, and a
browser case hit-tests both; with the clamp removed it fails (a 6 px band
where half the gap is 2 px).

Red first: the browser spec now reads the band past the border box and
hit-tests points inside Network's band. On the previous CSS, the
end-control case read a 1 px band where half the gap is 2 px and a
compact divided row's band read -1 px; with z-index removed, a point
0.5 px below Network hit the Wi-Fi row.
…nd the list bounds what rises

From review on meta#8085 and meta#8106:
- A ListItem with swipeActions is a stacking context at rest (Item
  isolates and translates its swipe root), so its touch band's z-index
  ordered only inside it and a following static row still took taps in
  the band. Under a finger, a swipeable interactive row now raises itself
  (z-index: 1 on the row; with a mouse nothing changes). The "With end
  controls" story's Network row is now swipeable, and the browser case
  checks that the row isolates and that taps in its band are its own.
- The list's ul now isolates (isolation: isolate), so what rises inside
  it (a row's band, a swipeable row, and later a crossing row) orders
  above the rows after it but stays below a page-level sticky header at
  the same z-index (spec:AST-027 DEC-1, DEC-2).

Red first: with Network swipeable, a point 0.5 px below it hit the Wi-Fi
row; the unit cases for the list's isolation and the swipeable row's
raise failed on the previous CSS.
From review on meta#8085: ListItem raised every interactive row with a
swipeActions prop, while Item makes a row a stacking context (serves its
swipe actions) only when a side has an entry, and not for a disabled
row, a link root, a role that forbids interactive descendants, or a row
whose press goes to a control inside it (interactiveRef). Rows with empty
swipe sides or a delegate control were raised for nothing. Item's
condition is now one function, servesSwipeActions, which Item and
ListItem both use.

Red first: in List.test.tsx, a row with empty swipe sides and a row with
a delegate control both carried the raise on the change before this one.
From review on meta#8106: a swipeable interactive row's raise (meta#8085) and
the travelling raise both set the row's zIndex, and StyleX keeps one per
element. The swipe raise came later, so under a finger every swipeable
row sat at 1, the moved row included, and the moved row (first in the
document) slid beneath the rows it crossed; with a mouse the swipe
raise's default dropped the travelling raise altogether. The swipe raise
now carries the travelling raise above its own (2), with a mouse as
under a finger.

The swipe story gains a "Move the last row first" button, and a browser
case freezes that reorder halfway and hit-tests where the moved row
overlaps a row it crosses, under a finger and with a mouse. On the change
before this one it hit Inbox; it now hits Archive.
…e rows it crosses

From review on meta#8106: the travelling raise was z-index 1, the same as a
swipeable row's raise under a finger, so a moved row without swipe
actions (first in the document) slid beneath the swipeable rows it
crossed. The travelling raise is now 2, above the swipe raise.

The swipe story gains a row without swipe actions (Notes) and two move
buttons, "Move Archive first" (a swipeable row) and "Move Notes first".
A new browser case freezes Notes' move halfway and hit-tests where it
overlaps a row it crosses, under a finger and with a mouse: on the
change before this one it hit Inbox under a finger; it now hits Notes.
The swipeable-row case moves Archive with its own button.
ListItem declares --_hit-border-bottom (the divider's width, 0 on the
last row) for the touch hit layer, and List.doc.mjs did not list it, so
derivedVarRegistry.test.ts failed: "List has undocumented CSS vars in
source: --_hit-border-bottom". It is now in List.doc.mjs theming.vars[]
as a private var, and in VARS_WITHOUT_DERIVED_MAPPING with its reason:
it is the ::after layer's geometry, which follows --border-width, not a
property a theme author sets (as with --_thumbnail-hit-inset and
--_input-clear-hit-inset).

Red first: "List: all source vars are in doc file" failed on the change
before this one; it passes now (122 of 122).
…head

Main's merges of meta#8045 and meta#8046 (the frame-evidence harness and the coarse
press) conflict with the older foundation heads this branch carries, in the
interaction harness files. meta#8106's current head carries that main and the
foundation it now rests on, so taking it leaves this branch clean against
main with nothing resolved by hand. A clean merge.

@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: proposedSlot keeps UIKit's crossing rule, which departs from spec:AST-103 FR1 (a drag reports the slot whose centre is nearest the dragged item's centre) wherever neighbouring rows differ in height, so past the 88 pt row of the measured table it moves to slot 6 at a centre of 308 pt where FR1 does at 286 pt, and the measured 306.8 pt sample lands on different slots. An engineering owner settles it by updating FR1 so a list without positions() follows UIKit's rule and recording that by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner), or by keeping FR1, in which case proposedSlot and its replay test conform to it. Whatever is decided, the diff still carries #8106's current head, 35,537 authored lines against this pull request's own 510, so land #8106 first and this pull request shrinks to the seven reorder files.

[Automated review]

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

This branch was successfully deployed

1 active deployment
Preview — 4439688e 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