Skip to content

feat(ui): align high-traffic chrome with Astryx primitives - #2580

Merged
jackwener merged 12 commits into
mainfrom
feat/astryx-surface-alignment
Aug 9, 2026
Merged

feat(ui): align high-traffic chrome with Astryx primitives#2580
jackwener merged 12 commits into
mainfrom
feat/astryx-surface-alignment

Conversation

@jackwener

Copy link
Copy Markdown
Member

Summary

  • Full surface inventory: docs/astryx-alignment-inventory.md (wiki smell map + every product family).
  • High-severity raw controls → Astryx:
    • Composer no-model / revision cancel → Button
    • Inspector failed filter → ToggleButton
    • External session import source → SegmentedControl; rows → Item
    • Plan execution expand → Collapsible
    • Workbar launcher menu → Item (role=menuitem)
  • Control heights on task ledger / module skeleton / launcher → 28/32/36 rhythm.
  • Gate: scripts/check-astryx-alignment.mjs + scripts/check-astryx-alignment.test.mjs wired into test:scripts.

Intentional non-changes

  • Workbar tab strip stays custom (role=tab + dnd-kit).
  • Quote/prompt-rail chips stay product-shaped hit targets.
  • SideNav nested-button prohibition remains as documented.

Test plan

  • node scripts/check-astryx-alignment.mjs
  • node --test scripts/check-astryx-alignment.test.mjs
  • npm run build/typecheck --workspace=@maka/ui
  • npx tsc -p apps/desktop/tsconfig.renderer.json --noEmit
  • tool-trow stability tests green

Scan product surfaces against the Astryx wiki (Design / API-Styling /
Theming / Container Padding) and close high-severity gaps: composer
actions, inspector filter, import dialog source/rows, plan execution
expand, and workbar launcher menu now use published Astryx components.
Bring control min-heights onto the 28/32/36 rhythm and add a structural
alignment gate with inventory docs.
- Workbar launcher: keep AT text on aria-description, not Item description
- Disabled launcher rows target [aria-disabled] from Astryx Item menuitems
- Import session pick uses listbox/option so rows stay keyboard-activatable
- Selected import rows style aria-selected/aria-current, not aria-pressed
- Inventory and height gate name skeleton rows, not the module page bar
Drop the listbox/option parent-role path that forced selected-only tabIndex
and left every option at -1 when nothing was selected. Item without a parent
role renders a real button for onClick, so rows are focusable after catalog
load. Tighten the alignment gate to reject the trap pattern.
Replace family-batch claims with one inventory row per on-disk product
.tsx/.css under renderer and packages/ui. Generator analyzes raw controls
and Astryx usage; coverage gate and tests enforce path-set equality with disk.
Stop matching component names in comments. The surface inventory analyzer
now binds names from @astryxdesign/core (and narrow @maka/ui re-exports)
and counts only JSX tags using those bindings. Regenerated inventory and
added a regression test for comment-only false positives.
Quote chips, prompt-rail ticks, titlebar rename, and workbar tabs now use
Button/IconButton/TextInput instead of raw HTML. Product CSS only strips
control chrome where the surface needs chip/rail/tab geometry.
…ockers

Replace remaining hand-rolled empty and loading chrome with EmptyState,
Spinner, and Banner; drop noise comments; regenerate surface inventory
to 0 blockers.
Deep-read all product settings, shell, module, and packages/ui surfaces.
Convert remaining hand-rolled error/empty/loading chrome to Banner,
EmptyState, and Spinner; document intentional exceptions and backlog.
Restore workbar launcher Enter/Space activation, quote-chip overflow
measure on the clipped text node, tab accessible names that include
counts, unsupported-artifact meta, and custom-pet loading empty state.
@jackwener
jackwener force-pushed the feat/astryx-surface-alignment branch from 2f4abd6 to 310de56 Compare August 9, 2026 08:49
Main switches sessions via conversationKey (Astryx patch). Explicitly
declare and forward the prop so desktop typecheck no longer depends on
ComponentProps seeing only the stock ChatLayout destructure list.
The CI typecheck workflow runs biome format checks; keep inventory
generators and gates formatted so the job can proceed past format.
Drop unused preview/import/launcher/connection helpers and the unused
warning-wash-strong tokens so check-dead-css stays at baseline 0.
@jackwener
jackwener merged commit 0ad579d into main Aug 9, 2026
14 checks passed
@jackwener
jackwener deleted the feat/astryx-surface-alignment branch August 9, 2026 09:16
jackwener added a commit that referenced this pull request Aug 9, 2026
Full-stack frontend analysis after #2580: architecture smells with
severities, remaining Astryx gaps beyond raw controls, and a ranked
P0–P3 backlog. Regenerated surface inventory baseline (0 blockers).
ARE404 added a commit to ARE404/maka-agent that referenced this pull request Aug 13, 2026
apache#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.

`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.

This is the third time the rail has failed by rendering and not painting
— apache#2161 pinned it against a containing block as tall as the conversation,
apache#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in apache#2462, and the multi-prompt fixtures it ran on in apache#2656, so
this adds back the smallest thing that closes the gap:

- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
  below three prompts, so the shipped single-prompt fixture cannot show it
  at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
  box, the rail stays inside the scrollport at both scroll extremes, and a
  tick is what the pointer lands on. Three tests where the deleted suite
  had nine.

Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
Astro-Han pushed a commit that referenced this pull request Aug 13, 2026
…2923)

* fix(ui): give the prompt rail's tick bars a box again

#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.

`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.

This is the third time the rail has failed by rendering and not painting
— #2161 pinned it against a containing block as tall as the conversation,
#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in #2462, and the multi-prompt fixtures it ran on in #2656, so
this adds back the smallest thing that closes the gap:

- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
  below three prompts, so the shipped single-prompt fixture cannot show it
  at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
  box, the rail stays inside the scrollport at both scroll extremes, and a
  tick is what the pointer lands on. Three tests where the deleted suite
  had nine.

Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.

* fix(ui): make the prompt rail's hover and jump behave

Four things the rail got wrong once it was visible again, found by using it:

- A 4px gap between ticks was a band where the pointer was over the rail
  and over no tick, so the dock-style hover falloff dropped out and picked
  up again every few pixels of travel. The rail's `gap` moves into the
  ticks' own `padding-block`: same pitch, hit boxes now tile.
- The hover preview waited 300ms before opening — Astryx's HoverCard
  default, meant for a pointer crossing a wide row on its way somewhere
  else. A tick is 22px of rail that nothing is on the way to, and the wait
  is the one part of this hover with no motion in it. Now 120ms.
- The highlight glided 280ms to wherever a click landed, so crossing
  twenty prompts read as the bar flying off across the rail. A click now
  owns the highlight until its scroll settles: no glide, and the scroll
  no longer walks the highlight through every prompt it passes.
- The first click into a session did nothing until the reader scrolled by
  hand. See below.

That last one is a collision between Astryx's auto-follow lock and the
progressive transcript mount, and neither side is wrong on its own.
`useChatStreamScroll` unlocks on a scroll up, detected by comparing
scrollTop between events — but it ignores any scroll event that arrives
with a changed scrollHeight or offsetHeight, because Chrome fires those
when content resizes and they are not the reader moving. A jump into an
unmounted turn mounts it and the fill that follows changes scrollHeight
for several frames, so the jump's own scroll is invisible to the lock: it
stays on, and `scrollIfLocked` pulls the transcript back to the bottom.
Only a wheel gesture broke it, which takes a separate path in Astryx.

`holdJumpDestination` re-aims at the target on each height change until
the fill stops. The last of those scrolls lands with a stable height,
which is the one the lock finally reads as a scroll up. Measured on the
30-prompt fixture: clicking the first tick went to scrollTop 7042 (the
bottom) and now goes to 24 and holds.

The fixture grows from 8 prompts to 30 because the progressive mount's
initial window is 10 — at 8 the head of the transcript is already mounted
and the jump-into-unmounted-turns path never runs at all.

Coverage note: the e2e case for the first click is an end-to-end check,
not a guard. Whether the lock wins depends on which frame the fill lands
on relative to a smooth scroll still in flight, and it goes green against
the unfixed renderer often enough to be worthless as one. The guard is
the `holdJumpDestination` unit test, which drives the frames itself.

* fix(ui): own a rail jump through the mount instead of racing it

Review of #2923 found the jump's ownership bound to a clock rather than to
the navigation, and the e2e case that was supposed to guard it asserting
almost nothing. Both hold.

Jump ownership:

- A second click during a jump only replaced the target; the first click's
  700ms timer still governed, and could clear the second jump mid-flight.
  Each click now carries its own sequence and starts its own hold.
- The fixed window is gone. A hold runs until the progressive mount reports
  the transcript filled AND nothing has moved for a few frames, so a long
  transcript is never released mid-fill, and it ends the moment the reader
  touches the transcript (wheel, touch, pointer, key) rather than outliving
  their interest in it.

Chasing the "just release auto-follow" direction the review preferred found
that ChatLayout publishes no such seam, so this adds one — `unlockAutoFollow`
on `ChatLayoutContextValue`, exposing the scroll hook's existing `unlock`
(patch hunk + patches/README entry). It is necessary and it is not
sufficient, which the earlier framing got wrong:

- Astryx re-locks on any `scrollend` that settles near the bottom, and a
  session that opens at the bottom produces exactly that while the mount is
  still catching up. Releasing once at the click is undone before the jump
  goes anywhere — traced: released at the click, landed at 154ms, dragged
  back to the bottom by 166ms. The release is now re-asserted for the life
  of the hold.
- Auto-follow is not the only thing moving the transcript. The progressive
  mount's own scroll compensation holds the reader's position across each
  fill step, and mounting the turn a jump asked for IS a fill step, so it
  lands after the jump and restores the position the jump just left. That
  one no seam can fix; it is what the hold is for.

Jumps also scroll instantly now, whatever the app's scroll-motion policy
says. A jump is a teleport the reader asked for, and an animated one does
not survive this surface: traced on the 30-prompt fixture, the smooth scroll
was cancelled by the mount's compensation and by the follow spring and
stalled two pixels from where it started.

Coverage:

- The first-click e2e case named the wrong turn (`[data-turn-id]` is the
  first MOUNTED turn, whose top is already negative at the opening scroll
  position, so an upper-bound-only check passed without the jump doing
  anything). It now names `turn-prompt-rail-1`, bounds it on both sides, and
  asserts that tick's `aria-current`.
- `emulateMedia` could not put that case on the production scroll path:
  `resolveScrollMotionBehavior` collapses motion for ANY fixture, keyed on
  `data-maka-e2e-fixture` rather than on the media query. Fixtures can now
  ask for a behavior back (`scrollMotion`, per launch — it costs seconds of
  settling per window, so only the case that needs it pays), with unit
  coverage for the precedence: a fixture request never outranks a stated
  preference for less motion.
- `holdJumpDestination`'s unit tests grew the two cases its rewrite is
  about: it must not settle while the transcript is still filling, and it
  must hand the transcript back the moment the reader touches it.

Verified 5/5 on the smooth-scroll fixture, where the previous revision lost
1 in 4. `quote-selection.spec.ts` flakes on this machine (1 in 4) at
upstream/main as well, unchanged by this branch.
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.

1 participant