Index

ui-agent

A dev-only panel that edits design tokens in the browser and writes the change back to your stylesheet.

React package, not yet published

This came out of doing design engineering with a coding agent all day. The loop that kept costing me time was not the big changes, it was the last two pixels. I would ask for a radius, look at it, know it was wrong, and then have to say something like a little tighter and wait to see what that meant to somebody else. The knowledge was in my eyes and the file was somewhere else and the agent was in between.

Token editors solve half of that. You get sliders, you find the value you like, and then you go open the file and type it in by hand, which means the panel and the source disagree for as long as it takes you to remember. ui-agent closes that loop: the slider writes to the stylesheet. It also keeps the values you rejected, which turned out to be the more useful half of the record.

It is three tools that share a panel. One edits tokens. One audits the page for the things that are almost right, which is the category the eye reads as sloppy without being able to name. And one turns what you dragged into place by hand into a brief an agent can act on.

Tokens

Each group of tokens becomes a section of sliders, and the units come from the values, so a radius group slides in pixels and a motion group slides in milliseconds. The groups are read from a manifest where every token records the custom property that controls it.

{
  "radius": {
    "soft": {
      "$type": "dimension",
      "$value": "4px",
      "$variable": "--radius-soft",
      "$utilities": ["rounded-soft"]
    }
  }
}

The variable is the required part. It is what makes an edit unambiguous: the panel sets that property at runtime and the writer changes that exact declaration in the file. The utilities list is only displayed, so you can see the blast radius of a change before you make it.

The decision log

Every apply and every reset appends one line. A reverted entry never touches the stylesheet.

{"action":"applied","edits":[{"token":"radius.soft","from":"4px","to":"8px"}],
 "note":"4px reads too soft next to the 2px data surfaces"}
{"action":"reverted","edits":[{"token":"radius.soft","from":"8px","to":"16px"}],
 "note":"way too round, looks like a consumer app not a data tool"}

This is the part I would keep if I had to throw the rest away. The value that stuck is in the stylesheet already and anyone can read it. The value someone tried and threw away, plus the reason, is nowhere, and it is the thing that actually explains a design system. Read a few weeks of this log and the house style is written down whether or not anybody meant to write it.

What it refuses to do

An HTTP endpoint that edits your source deserves guards. A variable name that is not a plain custom property is rejected. A value that is not a bare CSS length is rejected. And a declaration whose value is itself another variable is refused, because in a Tailwind theme block those aliases are what hold a scale together, and overwriting one with a literal would flatten the scale silently.

Align

Click a container and it checks the children. Guide lines are drawn over the page, faint on the edge most items share and hard on the strays.

ul
5 children, 1 clean alignment line

left edges
4× 10  ·  1× 12
2px off. Strays should sit at 10px.

Five checks run: near-miss edges, broken rhythm, off-grid spacing, padding drift between siblings, and padding asymmetry. All five are tuned by the same idea, that a big difference is a decision and a small one is a mistake. A header padded 24 on top and 8 on the bottom is a shape someone chose. 16 and 14 is a typo.

Switched to the whole page, it looks for spines, edges several elements share exactly, and reports what sits near one without landing on it. This is deliberately not a comparison of every element to every other. Across unrelated components almost any pair lands within a few pixels of something, so that version produces noise instead of findings. Anchoring to a line the page already committed to is what makes a cross-component result mean anything.

Two things look like misses and are not, and getting them wrong is the difference between a check people keep open and one they switch off. A card with a one-pixel border holds its rows one pixel inside its own edge, and "move it 1px right" would push them onto the border. And two real lines a pixel apart are two lines, not one line and six strays. Both are counted and set aside, and the counts are always shown, because a check that quietly discards most of what it found is indistinguishable from one that found nothing.

When no value holds a majority it says so rather than picking one. Two items two pixels apart are misaligned, but nothing says which one moved, and naming a target there would be a guess dressed up as a finding.

Nudge

Audit is the tool telling you what is wrong, which is always a guess about intent. Nudge is the other direction. Every line the page commits to is drawn, including the ones nothing misses, since those are the lines you would want to land on. Pick an element up, drag it or use the arrow keys, and an edge within six pixels of a line sticks to it.

Nothing about the page changes while you do this. The move is a transform, so layout, scroll height and every sibling stay where they were, and one drag cannot cascade into a reflow that invalidates the lines you are measuring against.

Copy feedback turns the moves into a brief. Both positions are given rather than a delta, so the agent can check the claim against the layout instead of trusting a number.

I dragged 2 elements into position by hand. The positions below are the
intended result, not the implementation. Change whichever padding, gap,
margin or grid value produces them. Do not add a transform to hit the number.

1. div.pg-seg-item, 3px right and 1px up
   from left 1225, top 33 (145×28) to left 1228, top 32
   lands on the right line at 1228px, which 7 elements share
   rendered by Segmented · components/segmented.tsx:24

The warning at the top of that brief is not decoration. A pixel delta invites the nearest way to produce it, which is a transform, and the result would look right and be wrong, because it moves the element in the paint and not in the layout.

Type

It censuses every rendered size and weight, then flags what is off your scale or spaced too finely to read as steps.

Sizes in use
13px × 11  off-scale
11px × 2   off-scale
12px × 2
10px × 1   off-scale

Ramp too fine to read as steps
6 sizes inside 5px.

Runs are collapsed on purpose. A page using 10 through 15 has one problem, not five: a ramp finer than the eye reads as steps. Reporting each adjacent pair buries the finding under its own restatements.

One house rule flags monospace text that is also uppercased. Mono already says machine. Uppercasing it costs legibility and adds nothing.

Dogfooding it here

This site runs the panel in local development, which is how the type on it got fixed. The first audit found small text at 15.2 pixels sitting 1.8 below the 17 pixel body, close enough that it read as an accident rather than a step. I moved it to 14 from inside the panel, wrote the reason in the note field, and the write-back put it in the stylesheet and the reason in the log.

It also turned up a bug in the panel itself. Apply cleared the runtime override so the page would render from the file, except the browser is still holding the stylesheet it loaded, so the page snapped back to the old value while the panel reported success. Applied edits are now promoted to committed for the session. That is the argument for dogfooding in one paragraph: nothing in the test suite could have found it, because the bug only exists when the file and the loaded document disagree.

Where it stands

It is a v1 and it is not published yet. It runs in two of my own projects and it has changed how I work in both, which is a lower bar than shipping but the right one for deciding whether to keep going.