Skip to content

feat(TreeList): rows expand and collapse the way an iOS outline list does - #8088

Draft
vjeux wants to merge 1859 commits into
meta:mainfrom
vjeux:uikit/c11-tree-expand
Draft

vjeux wants to merge 1859 commits into
meta:mainfrom
vjeux:uikit/c11-tree-expand

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Review only this PR's change: vjeux/astryx@4fa4a28...47e1a44 (depends on #8085).

Two test helpers from the closed #8082 stay here because these specs use them: List/__tests__/rowPixels.ts (screencast samples and WebKit video proofs) and List/__tests__/reducedMotion.ts (the Reduce Motion cross-fade check).

User need

On iOS, opening a folder in an outline list slides its children out from under it, so the eye follows where they came from. In Astryx, under a finger, they appeared in one frame.

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. The tree rendered an expanded group's rows with no motion, which on a phone reads as a glitch next to native apps.

On a touch screen, the group's rows now slide out from under the folder as one block, clipped to the group's box, while the chevron turns; collapsing runs it back. The rows below the group take their new places at once, without animating (spec:AST-105 FR2): when it appears, and on a collapse once its rows have slid under the folder (component:TreeList FR8). A mouse keeps today's tree, and Reduce Motion fades instead of sliding. With an animated-layout handle (#8568), the handle's glide plays instead, under every pointer. The tree reads the touch presentation once and passes it to its rows, so a repeated row adds no touch observer of its own (spec:AST-094 FR24).

Size (gzip, base → head, minified import): TreeList JS +11,356 B; package CSS +11 B.

Current specification

component:TreeList FR8 (current, #8691), amended here: the rows after a collapsing group hold until its rows have slid under the parent, as in UIKit, then take their places at once. It stays proposed until an engineering owner approves it.

Public delta and caller burden

None.

Design and alternatives

  • Animate the group's height: lays out every frame, so it stutters while the app is busy.
  • A clip-path reveal: main-thread work in WebKit, the same problem.
  • Slide the rows with transforms under a fixed clip (chosen): runs on the compositor, so it stays smooth while script runs.

On collapse, the rows below wait for the children to slide away, as in UIKit (FR8).

Evidence

iOS 26.2 simulator, Mobile Safari, touch input; a real device is the release gate.

Path Clip
Expand, then collapse Expand and collapse
Reduce Motion Reduce

Scope

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

Testing

  1. On an iPhone (or the iOS simulator), open Storybook's TreeList "With content below" story and tap "components". Its children slide down from under it as one block while the chevron turns, and the rows below move to their new places at once.
  2. Tap "components" again: the children slide back up under it, and the rows below move up as the last of them disappears.
  3. Turn on Reduce Motion and repeat: the children fade in and out where they are; nothing slides.
  4. With a mouse: the children appear and disappear at once, as today.

What was run: these steps on the iOS 26.2 simulator (the clips above), and browser tests of each step in Chromium and WebKit, busy main thread included, plus a toggle mid-motion, a tree with an animatedLayout handle under both pointers, the collapse's Layout and Paint trace, and a collapse from rest after a 150 ms busy first frame. All pass. The fixes' tests failed before them: a unit case where a 30-row tree registered 60 media-query listeners against 4 for a 2-row tree, and one where a collapse from rest got its own start time instead of the browser's commit. Vercel's failed deployment on 0249148 was on Vercel's side: its build command passes locally on that commit, and later heads that contain it deployed.

Writing rubric results

R1 narration: none; FR8's amended clause states the behavior in the present tense.
R2 research: one measured fact (UIKit's 0.24–0.37 s hold) stays in FR8's basis cell; no survey or comparison.
R3 evidence: FR8's basis names UIKit's measured behavior, not a change.
R4 rejected alternatives: none in the change.
R5 change references: none added to the record.
R6 open questions: none affected.

@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 30, 2026 3:14am 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
❓ Needs maintainer input (for maintainers only) Oct 30, 2026, 3:40 AM UTC

@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.2KB N/A N/A 20B N/A
charts/ChartAxis N/A 6.7KB N/A N/A 112B N/A
charts/ChartLegend N/A 53.5KB N/A N/A 20B N/A
charts/ChartTooltip N/A 53.4KB N/A N/A 20B N/A
core/AlertDialog 57.0KB 57.5KB +492B 12.4KB 12.5KB +24B
core/AppShell 58.0KB 58.4KB +415B 11.6KB 11.7KB +22B
core/AspectRatio 3.7KB 3.8KB +66B 3.5KB 3.5KB +40B
core/Avatar 37.6KB 37.6KB +53B 6.3KB 6.3KB +23B
core/AvatarGroup 39.9KB 40.0KB +50B 6.4KB 6.5KB +22B
core/AvatarGroupOverflow 40.1KB 40.6KB +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 48.0KB 48.4KB +496B 9.9KB 10.0KB +22B
core/BaseTypeahead 54.5KB 54.9KB +439B 9.9KB 9.9KB +22B
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 66.5KB 67.7KB +1.1KB 11.0KB 11.4KB +360B
core/Breadcrumbs 22.6KB 22.6KB +65B 3.3KB 3.3KB +40B
core/Button 43.4KB 43.9KB +484B 9.2KB 9.2KB +21B
core/ButtonGroup 8.4KB 8.5KB +67B 3.3KB 3.3KB +40B
core/Calendar 55.1KB 55.5KB +489B 10.0KB 10.0KB +22B
core/Card 6.4KB 6.5KB +66B 6.3KB 6.4KB +42B
core/Carousel 50.4KB 50.8KB +430B 10.2KB 10.2KB +21B
core/Center 4.2KB 4.2KB +63B 4.6KB 4.6KB +42B
core/ChatComposer 60.8KB 61.2KB +476B 10.8KB 10.8KB +19B
core/ChatComposerDrawer 24.0KB 24.1KB +66B 4.5KB 4.6KB +39B
core/ChatComposerInput 55.9KB 56.4KB +474B 10.1KB 10.1KB +21B
core/ChatComposerTokenElement 55.9KB 56.4KB +475B 10.1KB 10.1KB +21B
core/ChatDictationButton 46.6KB 47.1KB +472B 9.5KB 9.5KB +20B
core/ChatLayout 49.0KB 49.4KB +467B 9.9KB 10.0KB +20B
core/ChatLayoutScrollButton 46.5KB 47.0KB +461B 9.6KB 9.6KB +20B
core/ChatMessage 23.2KB 23.3KB +65B 3.5KB 3.6KB +39B
core/ChatMessageBubble 7.0KB 7.1KB +67B 3.6KB 3.6KB +42B
core/ChatMessageList 29.1KB 29.1KB +71B 7.8KB 7.8KB +21B
core/ChatMessageMetadata 26.0KB 26.0KB +60B 3.9KB 3.9KB +23B
core/ChatSendButton 46.0KB 46.4KB +485B 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 32.0KB 32.1KB +62B 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 50.1KB 50.5KB +445B 9.3KB 9.3KB +22B
core/CheckboxList 46.2KB 46.2KB +67B 8.1KB 8.2KB +22B
core/CheckboxListItem 54.2KB 57.8KB +3.6KB ⚠️ 10.5KB 11.0KB +538B
core/Citation 26.1KB 26.1KB +72B 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.7KB 61.1KB +464B 10.7KB 10.7KB +27B
core/Collapsible 17.0KB 17.4KB +439B 4.2KB 4.3KB +39B
core/CollapsibleGroup 6.4KB 6.4KB +71B 3.2KB 3.2KB +40B
core/CommandPalette 51.0KB 51.5KB +469B 11.8KB 11.8KB +23B
core/CommandPaletteEmpty 6.1KB 6.2KB +64B 3.3KB 3.4KB +42B
core/CommandPaletteFooter 23.4KB 23.5KB +71B 3.5KB 3.6KB +39B
core/CommandPaletteGroup 3.5KB 3.5KB +64B 3.3KB 3.4KB +39B
core/CommandPaletteInput 32.5KB 32.5KB +60B 8.1KB 8.1KB +22B
core/CommandPaletteItem 8.4KB 8.9KB +487B 3.5KB 3.5KB +40B
core/CommandPaletteList 22.6KB 22.6KB +60B 3.3KB 3.3KB +41B
core/ComplexSelector 56.0KB 56.4KB +463B 10.6KB 10.7KB +20B
core/ContextMenu 74.7KB 78.4KB +3.7KB ⚠️ 14.6KB 15.2KB +552B
core/ContextMenuItem 53.0KB 53.4KB +453B 9.6KB 9.6KB +21B
core/DateInput 99.7KB 100.1KB +485B 14.8KB 14.8KB +20B
core/DateRangeInput 69.5KB 70.0KB +487B 11.2KB 11.2KB +21B
core/DateTimeInput 109.8KB 110.3KB +492B 15.2KB 15.2KB +20B
core/Dialog 16.2KB 16.3KB +64B 7.1KB 7.1KB +23B
core/DialogHeader 49.0KB 49.4KB +476B 9.8KB 9.8KB +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.1KB 49.5KB +469B 11.2KB 11.2KB +22B
core/DropdownMenu 72.8KB 74.0KB +1.2KB 14.7KB 15.3KB +549B
core/DropdownMenuCheckboxItem 16.1KB 17.3KB +1.2KB 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.6KB +1.2KB 5.2KB 5.5KB +370B
core/DropdownMenuRadioGroup 6.0KB 6.1KB +62B 3.2KB 3.2KB +43B
core/DropdownMenuRadioItem 16.2KB 17.4KB +1.2KB 5.6KB 5.9KB +350B
core/DropdownMenuSubMenu 58.2KB 59.4KB +1.2KB 9.5KB 9.9KB +357B
core/EmptyState 7.7KB 7.8KB +64B 3.5KB 3.6KB +42B
core/Field 44.4KB 44.5KB +62B 8.0KB 8.0KB +22B
core/FieldLabel 39.9KB 40.0KB +63B 6.5KB 6.5KB +23B
core/FieldStatus 10.4KB 10.5KB +74B 4.2KB 4.3KB +23B
core/FileInput 54.3KB 54.7KB +425B 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.5KB 43.9KB +482B 9.2KB 9.2KB +21B
core/InputGroup 45.0KB 45.0KB +69B 8.0KB 8.1KB +21B
core/InputGroupText 6.3KB 6.4KB +66B 3.7KB 3.8KB +40B
core/Item 11.4KB 12.5KB +1.1KB 4.7KB 5.0KB +372B
core/Kbd 6.7KB 6.7KB +67B 3.5KB 3.5KB +40B
core/LayerProvider 63.4KB 63.9KB +511B 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.4KB 51.8KB +441B 9.8KB 9.8KB +21B
core/Link 43.7KB 44.1KB +473B 7.6KB 7.6KB +25B
core/List 6.7KB 6.8KB +59B 3.4KB 3.5KB +40B
core/ListItem 53.7KB 57.3KB +3.6KB ⚠️ 10.5KB 11.0KB +538B
core/Markdown 144.5KB 148.2KB +3.7KB ⚠️ 17.4KB 17.9KB +543B
core/MetadataList 23.3KB 23.4KB +70B 3.6KB 3.6KB +42B
core/MetadataListItem 6.5KB 6.5KB +64B 3.4KB 3.4KB +43B
core/MobileNav 51.2KB 51.6KB +429B 10.1KB 10.1KB +22B
core/MobileNavToggle 46.4KB 46.8KB +472B 9.4KB 9.4KB +20B
core/MoreMenu 73.2KB 74.3KB +1.2KB 14.7KB 15.3KB +549B
core/MultiSelector 73.1KB 73.5KB +485B 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.9KB 56.3KB +473B 10.7KB 10.7KB +31B
core/Outline 28.8KB 29.2KB +472B 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 84.9KB 86.0KB +1.1KB 15.3KB 15.7KB +348B
core/Popover 50.0KB 50.5KB +480B 12.1KB 12.1KB +20B
core/PowerSearch 164.5KB 176.3KB +11.9KB ⚠️ 19.9KB 20.2KB +366B
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.8KB 45.8KB +64B 8.0KB 8.0KB +22B
core/RadioListItem 52.4KB 53.4KB +1.1KB 9.7KB 10.0KB +351B
core/ResizeHandle 25.2KB 25.3KB +74B 3.9KB 3.9KB +41B
core/Section 7.5KB 7.6KB +67B 6.2KB 6.2KB +42B
core/SegmentedControl 38.9KB 39.0KB +76B 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 76.5KB 77.6KB +1.2KB 15.0KB 15.3KB +355B
core/SelectorOption 14.5KB 15.7KB +1.2KB 5.1KB 5.4KB +370B
core/SideNav 58.1KB 58.6KB +451B 10.6KB 10.6KB +21B
core/SideNavCollapseButton 46.8KB 47.2KB +463B 9.5KB 9.5KB +20B
core/SideNavHeading 54.9KB 55.3KB +446B 10.1KB 10.2KB +22B
core/SideNavItem 54.6KB 55.1KB +459B 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.6KB 48.7KB +68B 8.5KB 8.5KB +21B
core/Spinner 26.4KB 26.5KB +57B 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.6KB 33.0KB +489B 5.3KB 5.3KB +22B
core/Stepper 47.5KB 48.0KB +456B 9.6KB 9.6KB +20B
core/Switch 48.5KB 49.0KB +462B 9.3KB 9.4KB +21B
core/Tab 9.7KB 10.2KB +501B 4.1KB 4.1KB +41B
core/TabList 40.3KB 40.4KB +63B 7.2KB 7.2KB +23B
core/TabMenu 51.8KB 52.2KB +465B 10.0KB 10.0KB +21B
core/Table 84.0KB 87.7KB +3.7KB ⚠️ 15.2KB 15.7KB +537B
core/TableBody 3.3KB 3.3KB +63B 3.2KB 3.2KB +42B
core/TableCell 75.8KB 79.5KB +3.7KB ⚠️ 14.9KB 15.4KB +539B
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 75.5KB 79.3KB +3.7KB ⚠️ 14.9KB 15.4KB +553B
core/TableRow 6.2KB 6.3KB +66B 3.6KB 3.6KB +40B
core/TableSelectionToolbar 52.5KB 53.0KB +472B 11.2KB 11.2KB +21B
core/Text 8.6KB 8.6KB +66B 6.8KB 6.8KB +23B
core/TextArea 49.6KB 49.6KB +69B 9.4KB 9.4KB +21B
core/TextInput 53.7KB 54.2KB +466B 10.5KB 10.5KB +22B
core/Thumbnail 50.8KB 51.3KB +500B 11.0KB 11.0KB +25B
core/TimeInput 86.9KB 87.3KB +387B 13.8KB 13.8KB +21B
core/Timer 9.2KB 9.3KB +70B 6.8KB 6.8KB +23B
core/Timestamp 27.7KB 27.7KB +72B 9.5KB 9.6KB +21B
core/Toast 60.0KB 60.5KB +476B 9.7KB 9.7KB +22B
core/ToggleButton 44.3KB 44.7KB +454B 9.3KB 9.3KB +21B
core/ToggleButtonGroup 6.3KB 6.4KB +68B 3.2KB 3.3KB +42B
core/Token 29.7KB 30.2KB +504B 4.5KB 4.5KB +42B
core/Tokenizer 68.8KB 69.2KB +488B 16.5KB 16.5KB +19B
core/Toolbar 39.9KB 40.0KB +64B 7.7KB 7.8KB +46B
core/TopNav 52.0KB 52.4KB +423B 10.4KB 10.4KB +23B
core/TopNavHeading 54.3KB 54.7KB +426B 9.9KB 10.0KB +20B
core/TopNavItem 9.0KB 9.5KB +509B 3.8KB 3.9KB +39B
core/TopNavMegaMenu 54.3KB 54.8KB +478B 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.7KB 54.2KB +476B 10.1KB 10.1KB +23B
core/TreeList 38.4KB 49.4KB +11.0KB ⚠️ 4.7KB 5.0KB +386B
core/Typeahead 61.6KB 62.1KB +462B 11.3KB 11.3KB +21B
core/TypeaheadItem 6.2KB 6.3KB +60B 3.4KB 3.5KB +39B
core/VStack 7.3KB 7.3KB +61B 4.9KB 4.9KB +44B
lab/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 65.7KB N/A N/A 773B N/A
lab/ChatReactionBar N/A 66.2KB 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 23.7KB 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 94.6KB N/A N/A 1.9KB N/A
lab/CommentPin N/A 48.0KB N/A N/A 1.2KB N/A
lab/CommentThread N/A 219.7KB N/A N/A 2.4KB N/A
lab/Comments N/A 224.3KB N/A N/A 2.5KB N/A
lab/DiffViewer N/A 46.2KB N/A N/A 2.1KB N/A
lab/EmojiPicker N/A 63.5KB N/A N/A 722B N/A
lab/InfoTip N/A 21.8KB N/A N/A 458B N/A
lab/ListInput N/A 75.7KB 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 150.7KB N/A N/A 1.4KB N/A
lab/MobileTokenizer N/A 92.7KB 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.4KB 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 55.9KB N/A N/A 590B N/A
lab/TransferList N/A 68.0KB N/A N/A 1.7KB N/A
lab/TransferListSelector N/A 73.5KB N/A N/A 1.9KB N/A
richtext/RichTextEditor N/A 271.3KB N/A N/A 2.6KB N/A
richtext/RichTextEditorToolbar N/A 213.7KB N/A N/A 454B N/A
richtext/RichTextView N/A 251.8KB 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/CheckboxListItem +3.6KB ⚠️ +538B
core/ContextMenu +3.7KB ⚠️ +552B
core/ListItem +3.6KB ⚠️ +538B
core/Markdown +3.7KB ⚠️ +543B
core/PowerSearch +11.9KB ⚠️ +366B
core/Table +3.7KB ⚠️ +537B
core/TableCell +3.7KB ⚠️ +539B
core/TableHeaderCell +3.7KB ⚠️ +553B
core/TreeList +11.0KB ⚠️ +386B

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.9KB 35.5KB +572B

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

57 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 37 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 for an owner decision: the TreeList touch expansion, its new followingContent prop, the transient translate TreeList writes onto page content below it, and the rest of the touch stack (List row heights, holdSelection, the adaptive threshold) have no current spec, and spec:AST-094 is still a draft. An engineering owner records that decision by commenting /approve-spec <full head SHA> once the records describe these behaviors (the author may do so as an engineering owner), and independently of it the change needs these fixes:

  • Merge the predecessor PRs first so this one is only its two TreeList commits, since the diff now carries 15,771 authored lines.
  • In useTreeExpansionMotion, add an effect cleanup so an unmount mid-collapse cannot leave page content translated, and release each row's inline translate when its own spring settles.

[Automated review]

element.style.removeProperty('overflow');
setShown(false);
});
}, [isExpanded, shown, animates, followingContent]);

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 layout effect starts animations and writes inline overflow and translate (on rows and on page content outside the tree) but returns no cleanup, so a tree that unmounts mid-collapse leaves the content below at translate: 0px -<group height>px (a jsdom probe still reads -80px 1.5 s after the unmount). architecture:react-component-runtime INV3 and INV5 require release on unmount: return a cleanup that stops each moved element's animator and clears its translate and the group's overflow.

releaseMotion(child, 'translate');
}
for (const row of rows) {
releaseMotion(row, 'translate');

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 runs off the children's promises, while each row's own spring is usually still tracked, so releaseMotion returns early (utils/rowMotion.ts:150) and the row keeps an inline translate: 0px at rest (Chromium: public in the Basic story, app in WithContentBelow). That contradicts "a transient translate, cleared at rest" and leaves a containing block for fixed descendants; release each row from its own spring's promise instead.

@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 for an owner decision: the TreeList touch expansion, its new followingContent prop, the transient translate it writes onto page content outside the tree, and the rest of the touch stack (List row heights, holdSelection, the adaptive threshold) have no current spec, and spec:AST-094 is still a draft. An engineering owner records that decision by commenting /approve-spec <full head SHA> once the records describe these behaviors, including whether the slide stays under Reduce Motion (the author may do so as an engineering owner). Independently of that decision, the change needs these fixes:

  • Land the predecessor PRs first so this one is only its two TreeList commits; the diff now carries 16,735 authored lines.
  • In useTreeExpansionMotion, return an effect cleanup so an unmount mid-collapse cannot leave page content translated, and release each element's inline translate when its own spring settles.
  • Move content below the tree by its measured layout change instead of the group height, so content layout does not move (a footer below a taller column) stops sliding.

[Automated review]

Comment thread packages/core/src/utils/rowMotion.ts Outdated
* layout: positioned and sticky elements, and any element that already
* carries a `translate` or a running animation another owner put there.
*/
export function contentBelow(component: HTMLElement): HTMLElement[] {

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 collects every in-flow element after the tree at each ancestor level up to the nearest scroll container or body, and the hook then moves all of them by the group's height without measuring whether layout moved them. With the tree in a short column beside a taller one, a footer below both keeps offsetTop 572 before and after the expand, yet is painted 35.9 px higher 100 ms in and slides back down; Storybook's own body-level #storybook-highlights-root gets the same translate. Could each candidate be translated by its measured layout change (skipping elements that did not move), and the walk stop at a boundary the tree owns or the caller names?

for (const row of rows) {
const animator = animatorFor(row);
animator.set({y: animator.get('y', at) + height});
releaseMotion(row, 'translate');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

On the collapse path this release runs right after animator.set(...), while each row's handle is usually still tracked (its own settle timer has not fired, or set just interrupted it and the track cleanup is still a pending microtask), so releaseMotion returns early and nothing releases the row later. In Chromium, after a collapse settles in the Basic story, the public row and a Storybook container under body keep inline translate: 0px. Releasing each element from its own handle's settlement would cover both this path and the expand path at line 217.

@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 for an owner decision: the TreeList touch expansion, its new followingContent prop, the transient translate it writes onto page content outside the tree, and the rest of the touch stack (List row heights and dividers, holdSelection, the adaptive threshold) have no current spec, and spec:AST-094 is still a draft. An engineering owner records that decision by commenting /approve-spec <full head SHA> once the records describe these behaviors, including whether the slide stays under Reduce Motion (the author may do so as an engineering owner). Independently of that decision, the change needs these fixes:

  • Land the predecessor PRs first so this one is only its two TreeList commits; the diff now carries 17,240 authored lines.
  • In useTreeExpansionMotion, return an effect cleanup so an unmount mid-collapse cannot leave page content translated, and release each element's inline translate when its own spring settles.
  • Move content below the tree by its measured layout change instead of the group height, so content that layout does not move (a footer below a taller column) stops sliding.
  • In spec:AST-094, fix FR7's reference to an AST-090 record that does not exist in the repository.

[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 for an owner decision: the TreeList touch expansion, its new followingContent prop, the transient translate it writes onto page content outside the tree, and the rest of the touch stack (List row heights and dividers, holdSelection, the adaptive threshold) have no current spec, and spec:AST-094 is still a draft. An engineering owner records that decision by commenting /approve-spec <full head SHA> once the records describe these behaviors, including whether the slide stays under Reduce Motion (the author may do so as an engineering owner). Independently of that decision, the change needs these fixes:

  • Make HoldCurveTiming's curve optional, or pass a curve from the new "Back, slowly" story button, so the Storybook typecheck check and the build gate pass again.
  • In useTreeExpansionMotion, return an effect cleanup so an unmount mid-collapse cannot leave page content translated, and release each element's inline translate when its own spring settles.
  • Move content below the tree by its measured layout change instead of the group height, so content that layout does not move (a footer below a taller column) stops sliding.
  • Land the predecessor PRs first so this one is only its two TreeList commits; the diff now carries 17,251 authored lines.
  • In spec:AST-094, fix FR7's reference to an AST-090 record that does not exist in the repository.

[Automated review]

Comment thread apps/storybook/stories/List.stories.tsx Outdated
};
const settle = async (to: 0 | 1, timing?: {durationMs: number}) => {
const current = hold.current;
const arrived = await current?.animate(to, 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.

timing is typed {durationMs: number}, but animate takes HoldTiming, whose duration form (HoldCurveTiming) also requires curve, so tsc fails here (TS2345) and the typecheck and build checks are red on this head. Making curve optional in HoldCurveTiming matches what animate already does (it falls back to list.deselect.navigationPopCurve); passing a curve from the story also works.

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.

No spec:AST-090 record exists on main or at this head (docs/specs/ goes from AST-087 to AST-091), so an owner approving FR7 cannot read the FR1 and FR10 it amends. Link the record once it lands, or drop the reference here and in Current-state impact.

vjeux added 5 commits October 29, 2026 14:54
…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.
…and turn it around early in the curve

The mid-motion case held every sampled frame's step to a fixed share of
the travel, so a frame the browser delayed under load carried two frames'
travel into one sample and failed (WebKit, a 55 px step on a 126 px
group). Each sample now carries its frame's time, and a step is held to
twice the outline spring's peak speed times the time since the last
frame, plus a pixel (a degree for the chevron).

The page now turns the expand around itself, on the first frame the block
moves, so the turn around lands far from either end of the curve under any
load; a tap after a poll often landed it near the end, where a restart
moved too little to tell.

A mutation that restarts the collapse from rest fails it (a 112 px step in
one 17 ms frame, against a 40 px bound); the product passes, three runs in
each engine.

@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 about component:TreeList FR8 as this head words it: under a finger and without an animatedLayout handle, the rows after a closing folder and the content after the tree hold until its rows have slid under it (about 0.3 s) instead of taking their places at once, and FR8 itself is still marked proposed. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner). Whatever the ruling, two changes are needed:

  • Land #8360, #8890, #8077 and #8085 first, so this pull request carries only its 15 TreeList files instead of 33,866 authored lines.
  • Play the outline spring through a motion role, as spec:AST-094 FR4 requires, instead of the internal list.update.outlineSpring constant.

[Automated review]

springTo,
} from '../utils/rowMotion';

const SPRING = list.update.outlineSpring;

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 plays a literal spring for the slide, its clip timing and the chevron. spec:AST-094 FR4 says every spring a touch presentation plays MUST be a motion role and that a component MUST NOT play a literal; a theme's motion knobs never reach this one (the browser case at TreeListExpand.interaction.browser.spec.ts:521 pins that). Could it read a role instead, either springs.settle (the role a tree with an animatedLayout handle already plays) or a TreeList role that component:TreeList names and registers through DEC-5's generated table?

| FR5 | `TreeListItemData` MUST admit `className`, `style`, and `xstyle` as one per-item styling seam. Every supplied value MUST compose on the same Item row box; an implementation MUST NOT treat current support for only one mechanism as authority to exclude the other two. | Owner decision; `architecture:public-component-api` INV5–INV6; `spec:AST-002/DEC-1` | Settled intent; implementation may be staged |
| FR6 | The Item row box is the logical element that paints the row, owns its hover state, and contains Start content, Item label and description, and End content. The styling seam MUST NOT move to the semantic `<li>` or another element without a compatibility decision. | Owner decision and the caller-owned per-row interaction boundary | Settled intent |
| FR7 | A click on an Item row outside its Item label performs the row's action: an Item with `onClick` runs it; otherwise an Item with children toggles; otherwise an Item whose only action is its link follows that link as Enter does, with modified and middle clicks following the shared enlarged-surface rule in `family:navigation-destinations`. The Chevron and controls in Start content and End content keep their own activation, and a disabled Item does nothing. | Owner decision; `family:navigation-destinations` | Settled intent; implementation may be staged |
| FR8 | Expanding or collapsing an Item with children MUST play its group's own motion: under a coarse primary pointer its rows slide out from under the parent as one block, clipped to the group's box, and back on collapse, a collapsing group's rows leaving the accessibility tree as the slide starts, while the Chevron turns, by transform only (`spec:AST-094` FR4–FR5); under a fine pointer the group appears and leaves at once. With an `animatedLayout` handle, the group and the rows after it follow `spec:AST-105`'s shared motion under every pointer (FR4, FR5); without one, the group's own slide stays touch-only and the rows after the group take their places at once (`spec:AST-105` FR2): as an expanding group appears and, under a coarse primary pointer, as a collapsing group's rows finish sliding under the parent, until which they hold their places. | UIKit outline list (`UICollectionView`), iOS 26.2, whose rows after a closing group hold until its rows have slid under the parent (0.24–0.37 s measured); `spec:AST-105` FR2 | Proposed; awaiting an engineering owner's approval |

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 new clause holds the rows after a collapsing group until its rows have slid under the parent, where CURRENT FR8 and spec:AST-105 FR2 say they take their places at once, and the row's contract state still reads proposed; it needs an engineering owner's exact-head approval. Its basis cites UIKit holding those rows for 0.24–0.37 s, but the measurement note at motion/uikit/list.ts:56-61 says UIKit springs "the children, the rows below, the clip and the chevron" together. Which measurement should the basis cite?

Comment on lines +56 to +61
* block inside a clip that grows with them, the rows below move with
* them, and the disclosure chevron turns 90°, all on this spring
* (critically damped, response 0.25 s, 0.367 s to settle). No fade.
* @source measured: UIKit lab (injected host app), iOS 26.2 sim iPhone
* 17 Pro, CAAnimation dump (CASpringAnimation m1 k631.655 c50.265 on
* the children, the rows below, the clip and the chevron), Oct 23 2026

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 note says that in UIKit the rows below move with the children on this spring, while TreeList FR8's new basis says UIKit holds them until the children have slid away, and the code does the latter. One of the two descriptions of UIKit is wrong; please reconcile them so the owner ruling on FR8 rests on the right measurement.

vjeux added 9 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.
…finger

From review on meta#8085: component:List FR10's verification asks for every
density, and the browser cases opened only balanced and compact lists.
A touch case opens the Interactive story at density="spacious" (the
density of the bottom-sheet menu rows): its one-line rows are 48 px,
already past the 44 px target, so their layers reach 0 px on both sides
and no two targets overlap. The story's density comes through a new args
option of the spec's openStory helper.

It passes in Chromium and WebKit. With the target raised to 56 px, it
fails: the rows then grow.
… 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.
…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.

@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 about component:TreeList FR8 as this pull request words it: under a finger and without an animatedLayout handle, the rows after a closing folder and the content after the tree hold until its rows have slid under it (about 0.3 s) instead of taking their places at once, and FR8 itself is still marked proposed. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner). Whatever the ruling, two changes are needed:

  • Land #8360, #8890, #8077 and #8085 first, #8360 at its own latest commit (this branch carries an older one), so this pull request carries only its 15 TreeList files instead of 34,040 authored lines.
  • Play the outline spring through a motion role, as spec:AST-094 FR4 requires, instead of the internal list.update.outlineSpring constant.

[Automated review]

vjeux added 7 commits October 29, 2026 18:23
…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.
… 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.

@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 about component:TreeList FR8 as this pull request amends it: under a finger and without an animatedLayout handle, the rows after a closing folder and the content after the tree hold until its rows have slid under it (about 0.3 s) instead of taking their places at once, and FR8 itself is still marked proposed. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner). Whatever the ruling, these changes are needed:

  • Land #8360, #8890, #8077 and #8085 first, so this pull request carries only its 15 TreeList files instead of 34,186 authored lines.
  • Play the outline spring through a motion role, as spec:AST-094 FR4 requires, instead of the internal list.update.outlineSpring constant.
  • Drop the checkbox example from the press-release changeset (#8890): CheckboxInput's release runs on its row, not inside its box.

[Automated review]

springTo,
} from '../utils/rowMotion';

const SPRING = list.update.outlineSpring;

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-094 FR4 requires every touch spring to be a motion role and forbids playing a literal; the slide, its clip timing and the chevron play list.update.outlineSpring directly, so a theme's motion knobs never reach them. Play it through a role (for example springs.settle, which a tree with an animatedLayout handle already uses) or a TreeList role registered through the generated role table.

| FR5 | `TreeListItemData` MUST admit `className`, `style`, and `xstyle` as one per-item styling seam. Every supplied value MUST compose on the same Item row box; an implementation MUST NOT treat current support for only one mechanism as authority to exclude the other two. | Owner decision; `architecture:public-component-api` INV5–INV6; `spec:AST-002/DEC-1` | Settled intent; implementation may be staged |
| FR6 | The Item row box is the logical element that paints the row, owns its hover state, and contains Start content, Item label and description, and End content. The styling seam MUST NOT move to the semantic `<li>` or another element without a compatibility decision. | Owner decision and the caller-owned per-row interaction boundary | Settled intent |
| FR7 | A click on an Item row outside its Item label performs the row's action: an Item with `onClick` runs it; otherwise an Item with children toggles; otherwise an Item whose only action is its link follows that link as Enter does, with modified and middle clicks following the shared enlarged-surface rule in `family:navigation-destinations`. The Chevron and controls in Start content and End content keep their own activation, and a disabled Item does nothing. | Owner decision; `family:navigation-destinations` | Settled intent; implementation may be staged |
| FR8 | Expanding or collapsing an Item with children MUST play its group's own motion: under a coarse primary pointer its rows slide out from under the parent as one block, clipped to the group's box, and back on collapse, a collapsing group's rows leaving the accessibility tree as the slide starts, while the Chevron turns, by transform only (`spec:AST-094` FR4–FR5); under a fine pointer the group appears and leaves at once. With an `animatedLayout` handle, the group and the rows after it follow `spec:AST-105`'s shared motion under every pointer (FR4, FR5); without one, the group's own slide stays touch-only and the rows after the group take their places at once (`spec:AST-105` FR2): as an expanding group appears and, under a coarse primary pointer, as a collapsing group's rows finish sliding under the parent, until which they hold their places. | UIKit outline list (`UICollectionView`), iOS 26.2, whose rows after a closing group hold until its rows have slid under the parent (0.24–0.37 s measured); `spec:AST-105` FR2 | Proposed; awaiting an engineering owner's approval |

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 amends FR8 so the rows after a collapsing group hold until its rows have slid under the parent, instead of taking their places at once, and FR8 is still marked proposed; it needs an engineering owner's approval on this head (/approve-spec <full head SHA>). The new basis cites a 0.24–0.37 s UIKit hold with no recorded source, while motion/uikit/list.ts records UIKit moving the rows below on the same spring.

'@astryxdesign/core': patch
---

[fix] Under a finger, a press's release now lasts until its fade has played. The touch press controller ended the release on a clock started at the lift, so when the page's script delayed the fade's first frame, the release was cut short mid-fade. It now ends when the press's release animation ends, on the surface or on a part a component draws it on inside the surface (a checkbox's box), or on its clock where no release animation runs (an instant release, a surface without one, or an engine without Web Animations). This applies to every surface the touch press controller drives: buttons, switches, checkboxes, cards, menu rows and list rows.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CheckboxInput's release animation runs on its row, the element that carries the press, and its box's ::after only paints the inherited strength with no animation of its own, so no checkbox draws a release inside its surface. Drop the checkbox example, or name a component that does draw one there.

vjeux added 5 commits October 29, 2026 20:02
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.

Approval waits on an engineering owner's decision about component:TreeList FR8 as this pull request amends it: under a finger and without an animatedLayout handle, the rows after a closing folder and the content after the tree hold until its rows have slid under it (about 0.3 s) instead of taking their places at once, and FR8 itself is still marked proposed. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner). Whatever the ruling, these changes are needed:

  • Land #8360, #8890, #8077 and #8085 first, so this pull request carries only its 15 TreeList files instead of 34,285 authored lines.
  • Play the outline spring through a motion role, as spec:AST-094 FR4 requires, instead of the internal list.update.outlineSpring constant.

[Automated review]

springTo,
} from '../utils/rowMotion';

const SPRING = list.update.outlineSpring;

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-094 FR4: "A component MUST read touch motion through these roles and MUST NOT play a literal." This plays list.update.outlineSpring as an internal constant (and TreeListExpand.interaction.browser.spec.ts:521-546 pins that a theme's motion knobs cannot reach it). Please play the slide through a motion role, either springs.settle or a TreeList role registered through DEC-5's generated table.

| FR5 | `TreeListItemData` MUST admit `className`, `style`, and `xstyle` as one per-item styling seam. Every supplied value MUST compose on the same Item row box; an implementation MUST NOT treat current support for only one mechanism as authority to exclude the other two. | Owner decision; `architecture:public-component-api` INV5–INV6; `spec:AST-002/DEC-1` | Settled intent; implementation may be staged |
| FR6 | The Item row box is the logical element that paints the row, owns its hover state, and contains Start content, Item label and description, and End content. The styling seam MUST NOT move to the semantic `<li>` or another element without a compatibility decision. | Owner decision and the caller-owned per-row interaction boundary | Settled intent |
| FR7 | A click on an Item row outside its Item label performs the row's action: an Item with `onClick` runs it; otherwise an Item with children toggles; otherwise an Item whose only action is its link follows that link as Enter does, with modified and middle clicks following the shared enlarged-surface rule in `family:navigation-destinations`. The Chevron and controls in Start content and End content keep their own activation, and a disabled Item does nothing. | Owner decision; `family:navigation-destinations` | Settled intent; implementation may be staged |
| FR8 | Expanding or collapsing an Item with children MUST play its group's own motion: under a coarse primary pointer its rows slide out from under the parent as one block, clipped to the group's box, and back on collapse, a collapsing group's rows leaving the accessibility tree as the slide starts, while the Chevron turns, by transform only (`spec:AST-094` FR4–FR5); under a fine pointer the group appears and leaves at once. With an `animatedLayout` handle, the group and the rows after it follow `spec:AST-105`'s shared motion under every pointer (FR4, FR5); without one, the group's own slide stays touch-only and the rows after the group take their places at once (`spec:AST-105` FR2): as an expanding group appears and, under a coarse primary pointer, as a collapsing group's rows finish sliding under the parent, until which they hold their places. | UIKit outline list (`UICollectionView`), iOS 26.2, whose rows after a closing group hold until its rows have slid under the parent (0.24–0.37 s measured); `spec:AST-105` FR2 | Proposed; awaiting an engineering owner's approval |

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 amends FR8 so that, under a coarse pointer and without a handle, the rows after a collapsing group hold until its rows have slid under the parent, against FR8's and spec:AST-105 FR2's "take their places at once". FR8 is still marked proposed; it needs an engineering owner's exact-head /approve-spec. Separately, the basis cites a measured 0.24–0.37 s UIKit hold with no recorded source, while motion/uikit/list.ts:56-61 records UIKit moving the rows below on the same spring.

This branch was successfully deployed

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