Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
f912e37 to
d8e3297
Compare
PR Analysis Report📚 Storybook Preview🧪 Sandbox PreviewModified ComponentsItem (@astryxdesign/core) · View in Storybook
TreeList (@astryxdesign/core) · View in Storybook
Component Size
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.
|
| 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)
- Rule:
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)
- Rule:
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
There was a problem hiding this comment.
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 inlinetranslatewhen its own spring settles.
[Automated review]
| element.style.removeProperty('overflow'); | ||
| setShown(false); | ||
| }); | ||
| }, [isExpanded, shown, animates, followingContent]); |
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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.
a448c3b to
02811f3
Compare
02811f3 to
4f2fd44
Compare
There was a problem hiding this comment.
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 inlinetranslatewhen 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]
| * 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[] { |
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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.
4f2fd44 to
bea7d0f
Compare
There was a problem hiding this comment.
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 inlinetranslatewhen 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]
bea7d0f to
c1d18da
Compare
There was a problem hiding this comment.
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'scurveoptional, or pass a curve from the new "Back, slowly" story button, so the Storybooktypecheckcheck and thebuildgate pass again. - In
useTreeExpansionMotion, return an effect cleanup so an unmount mid-collapse cannot leave page content translated, and release each element's inlinetranslatewhen 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]
| }; | ||
| const settle = async (to: 0 | 1, timing?: {durationMs: number}) => { | ||
| const current = hold.current; | ||
| const arrived = await current?.animate(to, timing); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
…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.
There was a problem hiding this comment.
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-094FR4 requires, instead of the internallist.update.outlineSpringconstant.
[Automated review]
| springTo, | ||
| } from '../utils/rowMotion'; | ||
|
|
||
| const SPRING = list.update.outlineSpring; |
There was a problem hiding this comment.
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 | |
There was a problem hiding this comment.
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?
| * 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 |
There was a problem hiding this comment.
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.
…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.
There was a problem hiding this comment.
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-094FR4 requires, instead of the internallist.update.outlineSpringconstant.
[Automated review]
…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.
There was a problem hiding this comment.
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-094FR4 requires, instead of the internallist.update.outlineSpringconstant. - 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; |
There was a problem hiding this comment.
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 | |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
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.
…ease change [skip ci]
… change [skip ci]
…get change [skip ci]
… change [skip ci]
There was a problem hiding this comment.
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-094FR4 requires, instead of the internallist.update.outlineSpringconstant.
[Automated review]
| springTo, | ||
| } from '../utils/rowMotion'; | ||
|
|
||
| const SPRING = list.update.outlineSpring; |
There was a problem hiding this comment.
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 | |
There was a problem hiding this comment.
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.
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) andList/__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-105FR2): when it appears, and on a collapse once its rows have slid under the folder (component:TreeListFR8). 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-094FR24).Size (gzip, base → head, minified import): TreeList JS +11,356 B; package CSS +11 B.
Current specification
component:TreeListFR8 (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
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.
Scope
Testing
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
animatedLayouthandle 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.