Skip to content

feat: instrument-grade visual pass across the presentation layer - #12

Merged
ribdsp merged 8 commits into
mainfrom
feat/ui-instrument-pass
Aug 29, 2026
Merged

ribdsp merged 8 commits into
mainfrom
feat/ui-instrument-pass

Conversation

@ribdsp

@ribdsp ribdsp commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Traces is judged partly from a compressed screen recording, so this treats appearance as part of the
deliverable. The direction is an instrument, not a dashboard and not a landing page — the reference
points are a DAW timeline and the DevTools performance panel, and CONTRIBUTING.md § UI conventions is
binding: dense, quiet, scannable.

Nothing under src/lib/** or src/types/domain.ts is touched. No dependency added — lucide-react was
already there and is the only icon set. AGENT_LANE_INPUT_ID, SCRUBBER_ID and all five keyboard
shortcuts are unchanged.

The six problems, and what each commit did

Commit Problem
eb69fbc Tokens. Role-named fontSize (micro/label/meta/body/title = 10/11/12/13/15px) so a type floor moves in one file. borderRadius and boxShadow are replaced, not extended, so rounded-lg and shadow-md fail to resolve — "no oversized rounded cards" became a build error. Global :focus-visible outline.
b61f248 Chrome. Header, status pill, recording dropdown (hand-written listbox, full keyboard support), transport controls.
ab91ad8 Agent column. Task queue, activity feed, hypothesis cards, report draft as one instrument surface.
f18ea9c Timeline. Two-weight ruler with labelled ticks, an event-glyph vocabulary distinct by both severity and size, a playhead with a handle. Budget went 96px → 112px, shared as TIMELINE_HEIGHT_PX.
67b06b0 Motion, in four places only: a blocking gate waiting, a claimed task working, bisect probes arriving, a recording loading.
7797ab8 Type floor and focus ring on the surfaces the first five commits missed.
906c766 The forced-open WebMCP panel covered the agent column at 720px — see below.
cdb6a44 The replay readout was 3.09:1; raised to 6.07:1.

Three colour vocabularies are kept disjoint so none of them has to be learned twice: authorship
(human/agent), severity (ok/warn/error), and progress (a pulsing dot, warn when a
person is the blocking dependency, muted when the machine is working).

Verification

  • npx tsc --noEmit clean; 293 tests in 24 files pass. Every test file is under src/lib/, so this
    pass cannot have moved one — no assertion was weakened, skipped or deleted.

  • No top-level scroll at 720 / 900 / 1100 / 1440, measured in both axes, empty and loaded:

    Width Overflow Y / X Agent col Replay col
    720 0 / 0 380 ~298
    900 0 / 0 380 517
    1100 0 / 0 380 717
    1440 0 / 0 380 1057
  • Timeline strip measures exactly 112px at every width, matching TIMELINE_HEIGHT_PX.

  • Reduced motion: globals.css caps animation-iteration-count at 1, which lands animate-pulse on
    opacity: 1 — a solid dot. Zeroing animation-duration alone, the usual reset, does not stop an
    infinite animation; it runs the cycle ~100,000×/second. Every state signalled by movement is also
    legible standing still (two chevron glyphs rather than one rotated, a word beside every dot).

Two things a reviewer should look at deliberately

906c766 is a behaviour change inside a visual pass. The WebMCP panel was pinned open in both
degraded states. At 720px — the width this gets recorded at, and polyfill is what Chrome shows without
the testing flag — 288px of tool grid covered the whole agent column including the textarea page.tsx
focuses on a. A panel with no close button occluding a focusable input is the failure that file's own
note says must never happen. Only error stays pinned now, and the reason is mechanical: the tool grid is
gated on tools.length > 0, an error state has no tools, so what is pinned there is ~180px that clears
the input. ToolStatusBanner is what actually discharges "a degraded surface must not be hideable" —
full width, in flow, no close control. Revertible in one line if you disagree.

Contrast has a systemic finding I did not act on. text-faint (#5b626c) measures 3.09:1 on bg-base,
2.89:1 on bg-panel — below AA for everything except disabled controls and placeholders, which are
exempt. It is used at ~30 sites. Raising the token itself does not work: clearing 4.5:1 on the lightest
surface (raised) needs it at L≈0.239, and muted is L=0.284, so the third text tier collapses into the
second and the quiet hierarchy goes with it. The real fix is retiring faint for prose and keeping it for
the genuinely exempt uses — a colour-system change, not a verification pass. cdb6a44 fixes only the one
site that sits alone over the replay frame.

ribdsp added 8 commits August 29, 2026 20:47
Six problems in the presentation layer trace back to one cause: there were no
tokens to reach for, so every component invented its own. 84 hardcoded
`text-[Npx]` sizes, no radius, no elevation, no focus treatment, no transition.

- Type scale named by role, not size: `text-micro`/`label`/`meta`/`body`/`title`.
  Body copy floors at 13px because this app is demonstrated through a compressed
  screen recording, where 10px prose becomes grey texture. Naming by role is what
  lets the floor move in one file next time.
- `borderRadius` and `boxShadow` are *replaced* rather than extended: nothing above
  6px resolves, and the only shadows are two inset top highlights. "No oversized
  rounded cards, no drop shadows" becomes a build error instead of a review note.
- `border-line-strong` for the outline of a raised surface, since an inset
  highlight lifts one edge and leaves the other three flush.
- A global `:focus-visible` outline rather than a class, so it covers elements
  nobody has written yet. `outline` not `ring-*`: a ring offset must be painted
  the colour of the surface behind it, and there are three surfaces here.
- 120ms colour/border transitions, scoped to elements that answer a pointer.

CONTRIBUTING.md § UI conventions records the floor, the radius scale and the
four places motion is allowed, so the convention and the code agree afterwards.
…port

The header was a 12px label and two rows of bare bordered buttons. Now:

- Header carries a wordmark at the title step, the subtitle beside it, then
  status, recording and keys in that order.
- The healthy WebMCP row is gone from the full-width banner and is a compact
  pill in the header instead. It portals into a slot page.tsx renders, because
  registration is owned by ToolSurface in the root layout — a sibling of the
  page, not a parent. The three degraded states keep their full-width in-flow
  rows, their live regions, their remediation prose and no dismiss control.
- Recording selector is a real listbox: trigger with the open id in mono,
  panel with each sample's blurb and — for the one that is loaded — its
  duration, event count and viewport. Arrows, Home/End, Enter, Escape back to
  the trigger, click-outside, aria-expanded/listbox/option/selected.
- Player: speeds are one segmented control with a raised active segment; the
  scrubber is a 20px hit area over a 4px track with a filled portion and a
  visible handle. Nothing it writes to the store changed.

The scrubber's handle lives in globals.css: two vendor pseudo-elements needing
the same eight declarations is unreadable as arbitrary Tailwind variants.
Every section in the right-hand column now says what it is at a glance, and
what state it is in without being read word by word.

section-heading.tsx now takes the icon *component* rather than a node, so size,
stroke and tint are decided once. Shape says which section you are looking at;
tint says the rank. That overrides the docstring's old argument for a uniform
marker, which was written when every section shared one glyph.

agent-lane.tsx is a task queue rather than a chat box, and the copy names the
mechanism: claim_next_task blocks, so what you type is what the call returns.
The open -> claimed flip is carried by four signals at once — the status word,
its chip, the rail and the row tint — because it happens in under a second.
AGENT_LANE_INPUT_ID is unchanged.

activity-feed.tsx grows a 2px rail per row in its author's colour. The badge
stays: nothing here may depend on telling violet from blue. Scroll anchoring and
per-entry undo are untouched.

hypothesis-cards.tsx and report-draft.tsx take the table treatment — mono
tabular-nums in fixed right-aligned columns, quiet zebra ground rather than a
hairline per row, real chips, evidence that looks clickable. report-draft.tsx
is a literal table with a sticky header; hypothesis-cards.tsx is not, and that
is measured: in a 380px panel its three fixed columns leave a sentence about
fifteen characters a line.

Report steps now carry a chip either way, ok for verified and warn for
unverified. An absent chip reads as "no opinion", which is exactly what a
report must not imply about a step the agent inferred.
The hero surface, and the one a viewer reads continuously. Every band here was
drawn in hairlines at 9-10px, which survives a monitor and does not survive an
encode.

Ruler: two weights now — labelled majors plus five unlabelled minors between
each pair, so the axis is a measure rather than a row of numbers and an eye can
interpolate "just after 14s" from a marker between two labels.

Playhead: a 1px rule with a handle in the ruler, above every band. It used to be
a hairline among hairlines, indistinguishable from a tick.

Events: lozenges rather than 1px marks — wider, rounder, more opaque, standing
on a ground rule. The ratios between kinds are unchanged, so the reading is the
same one, just visible. This band is the evidence a human checks a claimed
timestamp against; if it does not survive the encode it is not evidence.

Markers: fill is now authorship, matching what timeline.tsx has always claimed
this axis shows. The diamond head stays, so a monochrome frame still carries it.
Severity moved to pin height, the hover label's tint and the aria-label. Hit
target is 24px on the axis, and there is a full-height guide on hover so a pin
can say what is underneath it.

Bisect: the funnel is a stack of shaded bars closing on an instant, and the
answer is marked three ways — the surviving window shaded in the agent's colour,
a 2px rule at the instant, and a readout of value, precision and probe count.
The rule matters because the shading does not: converging to +/-41ms of a 44s
recording leaves a window a third of a pixel wide, so the *successful* case is
exactly the one where a shaded window disappears.

The strip's budget went 96px -> 112px, spent on the ruler and the event track.
Both numbers now live in axis.ts, because AnnotationMarker draws a guide the
full height of a strip it is a child of.
Four animated things, no more: a gate held open, a claimed task being
worked, the bisect probe sequence arriving, and a recording loading.
Two of the four already shipped in earlier phases; this adds the gate
and the loading indication, and fixes the mechanism all four rely on.

globals.css zeroed animation-duration under prefers-reduced-motion,
which does not stop an infinite animation — it runs the same keyframes
a hundred thousand times a second. Capping animation-iteration-count at
1 is what actually makes each pulse settle on its last keyframe
(opacity: 1, a solid dot), which is the fallback those components
document. The dot may stop moving; it must not disappear, because in
every case it is the thing saying something is still happening.

mark-point-overlay.tsx and stage-empty-state.tsx had never been through
the phase 1-2 token pass, so they get it here rather than in a second
edit over the same lines: type floor, radius, raised elevation on the
dialog, 14-15px icons, and the global focus ring in place of three
focus-visible:outline-none.
…missed

Four components were never reached by phases 1-2 and still carried hardcoded
sizes, zero radius and recessive 12px icons. Closing that, plus two real
regressions found while grepping for it.

The docked WebMCP badge was pinned at `bottom-24` to clear a timeline that
phase 4 grew from 96px to 112px, so it had been overlapping the top 16px of the
ruler — the end-of-recording markers a judge is meant to click. It now offsets
by `TIMELINE_HEIGHT_PX` itself: a literal that has to be remembered is a
literal that goes stale silently, and this one already had.

`resizable-split.tsx`'s divider is `tabIndex={0}` with `outline-none`, leaving
a 3px colour change as the only focus signal — and the same colour it uses for
dragging. It keeps the global outline now. `recording-picker.tsx`'s listbox had
the same suppression on the element that actually holds focus while open;
options are driven by `aria-activedescendant`, so the row highlight cannot
stand in for a focus ring, not least because `onMouseEnter` sets it too.

Also: the status dot in the badge was a 6px square, alone among every other
dot in the app.

CONTRIBUTING.md's motion section repeated the reduced-motion mechanism that
67b06b0 corrected in the CSS. Fixed there too, since that section is where the
next person will read it.

npx tsc --noEmit clean; 293 tests in 24 files passing.
The panel was pinned open in both degraded states. At 720px — the width this is
recorded at — the amber `polyfill` state put 288px of tool grid over the whole
agent column: the task queue, the activity feed, and the textarea `page.tsx`
focuses on `a`. A panel with no close button occluding a focusable input is
exactly the failure this file's own note says must never happen, and `polyfill`
is what Chrome shows without the testing flag, so it happened on every run.

Only `error` stays pinned now, and the reason is mechanical rather than a
softening of the rule: the tool grid is gated on `tools.length > 0` and an error
state has no tools, so what is forced open there is a definition, a sentence and
four prompts — around 180px, which clears the input. The state that can grow
tall enough to cover something is the one that is no longer pinned.

What discharges "a degraded surface must not be hideable" is `ToolStatusBanner`:
full width, in flow, no close control, naming the mode in words. The chip keeps
the amber triangle and the screen-reader state word either way, so collapsing
changes how much is said, never whether the page admits it.
Measured at 720px and 1440px: `text-faint` (#5b626c) on `bg-base` (#0e1013)
is 3.09:1, and this readout is 10px, so it needed 4.5:1. `text-muted` is
6.07:1. Nothing else about it changes.

Not a new judgement — `timeline.tsx` already makes exactly this swap for the
ruler's tick labels, at the same size, for the same reason, and says so in a
comment. This is the one other 10px readout on the page and it sits alone over
the replay frame, which is the surface a recording is pointed at for its whole
runtime.

The wider finding this came out of is a reporting item rather than a commit:
roughly thirty `text-faint` sites are between 2.89:1 and 3.09:1 depending on
which of the three surfaces they land on, and the palette has no room for a
third text tier that clears 4.5:1 on `raised` without collapsing into `muted`.
Some of those sites are exempt (disabled controls, placeholders); the ones that
are prose are not. Deciding that is a colour-system change, not a verification
pass.
@vercel

vercel Bot commented Aug 29, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
traces Ready Ready Preview Aug 29, 2026 3:29pm

@ribdsp
ribdsp merged commit c67a64c into main Aug 29, 2026
4 of 5 checks passed
@ribdsp
ribdsp deleted the feat/ui-instrument-pass branch August 30, 2026 11:44

This branch was successfully deployed

1 active deployment
Preview — cdb6a444 Deployed Aug 29, 2026 by vercel[bot]
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