feat(ui): give the prompt anchor rail its motion - #2215
Merged
Conversation
Follow-up to apache#2161, which deliberately shipped the rail as a static row of dashes so the positioning fix could be reviewed on its own. - The highlight for the prompt being read is now one bar that travels, anchored to the active tick with CSS anchor positioning — Astryx's own idiom for a sliding indicator, see its Outline. Changing the active prompt moves something the eye can follow instead of swapping two static ticks, which is the part that carries meaning rather than decoration: it shows the reader which way through the conversation they just moved, and how far. The anchor sits on the tick's inner bar, not on the tick: HoverCard sets its own `anchor-name` on the trigger it wraps, inline, and that wins over any rule of ours. A pseudo-element cannot be an anchor, so the bar becomes a real element. - Hovering a tick swells it and its neighbours over three ticks, which reads as the rail leaning toward the pointer and, more practically, makes a 3px target easier to aim at without permanently spending more width on it. Sibling selectors cannot express the falloff — each tick is wrapped in its own `display: contents` element — so the rail tracks the hovered index and hands each tick its distance as a custom property. - The rail rests tucked toward the window edge and settles inward under the pointer, and ticks slide in staggered rather than appearing, so a new prompt's tick arrives and a whole transcript cascades on mount. The stagger is capped at 20 ticks: a 90-prompt conversation should cascade in a third of a second, not nearly two. Every one of these is a CSS transition or animation on the existing duration and easing tokens, which is what puts them under the renderer's global `prefers-reduced-motion` rule rather than needing an opt-out of their own. Testing, and its boundary: e2e-fixture renders carry `data-maka-e2e-fixture`, which `base.css` gives `animation: none` and a 0.01ms transition cap so a fixture's rendered state never depends on the millisecond it settles. The new specs therefore assert the end states the motion resolves to — that the highlight lands on the active tick's centreline and right edge at three scroll positions and actually changes tick, that it keeps up when the rail scrolls itself past its cap, and that the hover falloff steps down on both sides and settles back — and cannot assert that anything animated on the way there. The tick entrance has no end state to check and so has no automated coverage; it was verified by hand in the running app. The sliding highlight is the one that fails silently — an unresolved anchor still renders the bar, just parked — which is why it is asserted in a live window rather than read off the stylesheet.
Astro-Han
approved these changes
Aug 5, 2026
Astro-Han
left a comment
Contributor
There was a problem hiding this comment.
Approved. The motion changes stay within the prompt-rail presentation layer, the #2161 geometry and overflowing-rail behavior remain intact, and the end-state coverage is proportionate to the fixture limitations. CI is green.
Non-blocking P3: the tick entrance keeps up to 360ms of animation-delay under prefers-reduced-motion because the global rule collapses animation-duration but not delay. This only causes a brief late appearance for reduced-motion users; it does not affect interaction, data, or performance. Fine to defer.
Astro-Han
added a commit
that referenced
this pull request
Aug 6, 2026
The rail rested flush against the chat scroller's right edge. On macOS the scroller's vertical scrollbar is overlay: it takes no layout space, so the rail drew on top of it, but the scrollbar's hit region still intercepted every pointer — ticks rendered yet never received a click or hover, and the rail read as gone. #2215's translateX(3px) had pushed the tick centres into that band; before it, the ticks were merely grazed. Linux's in-flow scrollbar shifts the content column left instead, which is why the regression sailed through CI green. The rail now rests at right: space-1 + space-2 (12px) — clear of the measured ~14px dead band — with the hover settle-in moved to (3px further inward); stays, as it only centres vertically and has no part in the hit-region problem. The rail's own scrollbar is hidden: at 22px wide the overlay bar's hit region swallowed tick halves right after the rail scrolled itself (wheel scrolling still works). prompt-rail.spec.ts documents why the reachability assertions are load-bearing on macOS, and the test now asserts the platform-neutral geometry the fix rests on (tick bar at least 14px clear of the scroller's right edge — pre-fix it read ~5px). The suite passes 9/9 on macOS under CI's single worker.
Astro-Han
added a commit
that referenced
this pull request
Aug 6, 2026
The rail rested flush against the chat scroller's right edge. On macOS the scroller's vertical scrollbar is overlay: it takes no layout space, so the rail drew on top of it, but the scrollbar's hit region still intercepted every pointer — ticks rendered yet never received a click or hover, and the rail read as gone. #2215's translateX(3px) had pushed the tick centres into that band; before it, the ticks were merely grazed. Linux's in-flow scrollbar shifts the content column left instead, which is why the regression sailed through CI green. The rail now rests at right: space-1 + space-2 (12px) — clear of the measured ~14px dead band — with the hover settle-in moved to the `right` property (3px further inward); the vertical centring translateY(-50%) stays, as it only centres vertically and has no part in the hit-region problem. The rail's own scrollbar is hidden: at 22px wide the overlay bar's hit region swallowed tick halves right after the rail scrolled itself (wheel scrolling still works). prompt-rail.spec.ts documents why the reachability assertions are load-bearing on macOS, and the "stays inside the scrollport" test now asserts the platform-neutral geometry the fix rests on (tick bar at least 14px clear of the scroller's right edge — pre-fix it read ~5px). The suite passes 9/9 on macOS under CI's single worker.
Astro-Han
added a commit
that referenced
this pull request
Aug 6, 2026
The rail rested flush against the chat scroller's right edge. On macOS the scroller's vertical scrollbar is overlay: it takes no layout space, so the rail drew on top of it, but the scrollbar's hit region still intercepted every pointer — ticks rendered yet never received a click or hover, and the rail read as gone. #2215's translateX(3px) had pushed the tick centres into that band; before it, the ticks were merely grazed. Linux's in-flow scrollbar shifts the content column left instead, which is why the regression sailed through CI green. The rail now rests at right: space-1 + space-2 (12px) — clear of the measured ~14px dead band — with the hover settle-in moved to the `right` property (3px further inward); the vertical centring translateY(-50%) stays, as it only centres vertically and has no part in the hit-region problem. The rail's own scrollbar is hidden: at 22px wide the overlay bar's hit region swallowed tick halves right after the rail scrolled itself (wheel scrolling still works). prompt-rail.spec.ts documents why the reachability assertions are load-bearing on macOS, and the "stays inside the scrollport" test now asserts the platform-neutral geometry the fix rests on (tick bar at least 14px clear of the scroller's right edge — pre-fix it read ~5px). The suite passes 9/9 on macOS under CI's single worker.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Follow-up to #2161, which deliberately shipped the rail as a static row of dashes so the positioning fix could be reviewed on its own. This is the motion, split out as you asked — judged on its own, and easy to decline in parts.
Three things, in descending order of how much they earn their keep:
1. The highlight travels. The bar marking the prompt being read is now one element anchored to the active tick with CSS anchor positioning — Astryx's own idiom for a sliding indicator, see its
Outline. Changing the active prompt moves something the eye can follow instead of swapping two static ticks. This is the one I'd argue carries meaning rather than decoration: in a 90-prompt rail it shows the reader which way through the conversation they just moved, and how far.The anchor sits on the tick's inner bar, not the tick —
HoverCardsets its ownanchor-nameon the trigger it wraps, inline, and that beats any rule of ours. A pseudo-element cannot be an anchor, so the bar becomes a real element.2. Hover falloff over three ticks. Reads as the rail leaning toward the pointer, and practically it makes a 3px target easier to aim at without permanently spending more width on it. Sibling selectors cannot express it (each tick is wrapped in its own
display: contentselement), so the rail tracks the hovered index and hands each tick its distance as a custom property.3. Rest and arrival. The rail rests tucked toward the window edge and settles inward under the pointer; ticks slide in staggered rather than appearing. The stagger is capped at 20 ticks — a 90-prompt conversation should cascade in a third of a second, not nearly two.
All three are CSS transitions/animations on the existing
--duration-*/--ease-*tokens, which is what puts them under the renderer's globalprefers-reduced-motionrule rather than needing an opt-out of their own.Testing, and where it stops
Worth stating plainly, because it surprised me: e2e-fixture renders cannot exercise motion. They carry
data-maka-e2e-fixture, andbase.cssgives thatanimation: none !importantand a 0.01ms transition cap so a fixture's rendered state never depends on the millisecond it settles. I found this writing a test to prove the reduced-motion claim — the durations were already collapsed, with no reduced-motion attribute andprefers-reduced-motionreporting no-preference.So the new specs assert the end states the motion resolves to, which is what the fixture harness can actually see:
overflowing-railfixture from fix(ui): pin the prompt anchor rail to the Astryx chat scrollport #2161) — a case the 24-turn fixture never reaches;What that leaves uncovered: that anything animated on the way there, and the tick entrance, which has no end state to check. The entrance was verified by hand in the running app. The spec's header comment now records this boundary so the next person adding to the file doesn't assume more coverage than exists.
The sliding highlight is the piece that fails silently — an unresolved anchor still renders the bar, just parked at a fixed offset — which is why it is asserted in a live window rather than read off the stylesheet.
Checks
build,typecheck,format:check,check-dead-css,check-a11y,check-consoleclean. 1698 desktop unit tests pass. Desktop e2e: 85 passed, withbot-onboarding.spec.tsflaking under full-suite load only (passes in isolation; it is a QR onboarding flow that never renders the rail).packages/uihas 2 failures inastryx-form-controls-localization— confirmed identical on an untouchedmain, unrelated to this change.If you'd rather take less
The three parts are independent. If the falloff or the entrance read as too much for this surface, say which and I'll drop them and keep the travelling highlight — that is the one I think is worth having.
🤖 Generated with Claude Code