Skip to content

feat(ui): give the prompt anchor rail its motion - #2215

Merged
Astro-Han merged 1 commit into
apache:mainfrom
ARE404:are404/feat-prompt-rail-motion
Aug 5, 2026
Merged

feat(ui): give the prompt anchor rail its motion#2215
Astro-Han merged 1 commit into
apache:mainfrom
ARE404:are404/feat-prompt-rail-motion

Conversation

@ARE404

@ARE404 ARE404 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

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 — HoverCard sets its own anchor-name on 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: contents element), 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 global prefers-reduced-motion rule 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, and base.css gives that animation: none !important and 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 and prefers-reduced-motion reporting no-preference.

So the new specs assert the end states the motion resolves to, which is what the fixture harness can actually see:

  • the highlight lands on the active tick's centreline and right edge (±1px) at three scroll positions, and the active tick actually changes across them — so a highlight that never left the first tick fails;
  • it keeps up with the active tick once the rail is past its cap and scrolling itself (the overflowing-rail fixture from fix(ui): pin the prompt anchor rail to the Astryx chat scrollport #2161) — a case the 24-turn fixture never reaches;
  • the hover falloff steps down on both sides of the pointer and settles back to a uniform rail on leave.

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-console clean. 1698 desktop unit tests pass. Desktop e2e: 85 passed, with bot-onboarding.spec.ts flaking under full-suite load only (passes in isolation; it is a QR onboarding flow that never renders the rail). packages/ui has 2 failures in astryx-form-controls-localization — confirmed identical on an untouched main, 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

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 Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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
Astro-Han merged commit c265b4c into apache:main Aug 5, 2026
12 checks passed
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants