A live freight-brokerage console on UAT: framework-free JavaScript inside a Next.js shell, thirty-five modules, guard tests that pin its markup. The brief was cosmetic only, nothing functional, nothing that could break production. This is what the design system and its skill found, fixed and proved, page by page.
The AIXFreight broker console: a Next.js 15 shell that boots a framework-free static console (sidebar, seven views, modals, live-call surfaces) written as plain JavaScript modules that coordinate through window. One 205 KB stylesheet holds every token, including a dark theme switched on data-theme. Backend guard tests assert literal strings in the frontend files, so markup, class names and even one grid track list are pinned.
Review every page and popup on the UAT workspace and log one to-do per flaw with page, steps, expected vs actual, severity and a screenshot. Then improve the UI using the design system. Screens only: the console dials real phone numbers, so no campaign or call could be started during the review.
Minimal, cosmetic changes. No functionality touched, no production risk. The product owner reviews and merges; nothing goes to staging from the review branch. Every edit had to go through the repository's own engineering-agent rules and keep its guard tests green.
The skill's definition of done asks for light and dark at three widths, no clipped text, every state handled, contrast and focus checked. Doing that by eye across ninety-seven screens is unreliable, so each step became a script.
UAT ran the staging branch, 95 commits ahead of the review branch. Staging was merged in first so every fix sits on the code users actually see.
Playwright signs in, walks each hash route and popup, answers discard confirms, and takes tall screenshots at 1280 in both themes plus 768 and 390. Nothing that launches, saves, deletes or dials is ever clicked.
Each capture was read against the skill's non-negotiables: text never overflows, one orange call to action, semantic tokens only, every state handled, 4.5:1 contrast, visible focus. Eight layout flaws came out of this pass.
A second script measured every visible text node's contrast in both themes, hovered every interactive element and compared computed styles, tabbed through every control, and logged failing network calls. That found the token-level problems.
Three passes, each an exact spec handed to the repository's console engineer agent: layout, then shell pages and empty states, then contrast, hover and focus. Guard tests decided the shape of two fixes.
Type-check, build, guard functions called directly (no database on the review machine), and the live DOM of each screen re-rendered offline with the new stylesheet to measure before and after. Findings went to the tracker as one to-do per flaw.
The value of the skill was not new visuals. It was a short list of rules that turned "looks fine" into a checklist with numbers, and a token vocabulary that made most fixes one-line changes.
| Skill rule | What it caught | What changed on the branch |
|---|---|---|
| Text never overflows §4.6 · min-width:0, wrap, nowrap only with ellipsis | Detail values cut to "27 Sept 20…" and "ALEEM ROA…"; transactions headers colliding into "DurationRate"; a mobile button wrapped into three lines. | .field-value wraps; the transactions grid gets a column gap, a floor on the Time column and sideways scroll inside its card; .sp-actions .linkbtn stays on one line. |
| Accessibility is part of done §4.8 · 4.5:1 body text, visible focus, 40px targets | The muted-text token measured 4.28:1 on white and 3.95:1 on the canvas across 1,600+ labels; status pills at 4.0–4.5:1; inputs, selects and the custom select trigger with outline:none and no replacement. | --z400 #6F7B90 → #5F6E88 (light) and #7C8899 → #8591A2 (dark); --success #15803D → #12703A; one :focus-visible rule for form controls; sidebar label lifted from 40% to 55% white. |
| Semantic tokens, never raw hex §4.5 · a hard-coded colour is a dark-mode bug waiting to happen | #E11D48, #D97706, #15803D and #B91C1C written into nine stylesheet rules and into inline styles in seven scripts, so dark mode kept light-theme reds and greens at 3.2–3.5:1. | Every literal replaced by --danger, --amber, --success; the login screen pins the light orange so links stay readable when the OS is dark. |
| Handle every state §3.6 · never a blank region | A blank white page for the seconds the console takes to boot; a blank Loads pane before selection; two dashboard cards rendering nothing when empty; Next's default 404; no styled error boundary. | Server-rendered boot splash inside the mount; "Select a load" placeholder; empty states on both cards; not-found, error and global-error pages on one shared component. |
| One orange call to action §4.1 · navy chrome, flat solids | The console's own toolbars were already navy/secondary; the new system pages needed a single decision each. | System pages: flat brand navy, mono eyebrow, one orange button ("Back to the console", "Try again"), a ghost secondary. No gradient, unlike the older boot-error screen. |
| Hover, press, select Guidance · interaction states | Pipeline stages had a hover rule that an inline style cancelled; the date chip, compare toggle and funnel rows had none. | Inline style moved into CSS so the existing hover applies; hover rules added for the other three; native selects left to the browser. |
| Layout recipes grids use minmax(0,1fr); tables that hold IDs and money get fixed widths | Five KPI cards in a six-column grid; a ragged flex-wrap of agent cards; the transactions table squeezed into a 560px pane. | repeat(auto-fit, minmax(150px,1fr)); repeat(auto-fill, minmax(240px,1fr)); a 980px floor with the header bar sticky while scrolling. |
| Writing §6 · what happened + how to fix, no blame, no exclamation marks | Error and empty copy needed for the new states. | "This page does not exist. The link may be out of date or the page has moved." · "Nothing was lost. Try again, or reload the console." · "No live outreach right now. Start a campaign from Loads to see calls here." |
"Before" is the live UAT build. "After" is the same saved DOM re-rendered with the branch stylesheet; the offline "before" run reproduces the live numbers exactly, which is what makes the comparison honest. Changes made in scripts (inline colours, empty states, the splash) do not show in the fixture measurement, which is why the after column says "expected 0 on the deployed build" rather than 0.
| Check | Before | After |
|---|---|---|
| Text nodes below AA contrast, light | 1,646 live · 1,773 offline (14 colour pairs) | 7 offline, all inline styles frozen in the saved DOM → expected 0 |
| Text nodes below AA contrast, dark | 21 | 2 offline → expected 0 |
| Form controls / custom select with no focus ring | 7 kinds | 0 |
| Clickable controls with no hover feedback | 9 kinds | 4 fixed · 1 false positive · native selects by design |
| Blank regions | boot, Loads pane, 2 dashboard cards | 0 |
| Unknown URL | Next.js default 404 | styled page, HTTP 404, light/dark/mobile |
| JavaScript page errors | 0 | 0 |
| Files changed for the visual fixes | 1 stylesheet + 1 auth stylesheet + 8 scripts (colour strings only) + 1 badge component; 4 new shell files | |
| Colour pair (light) | Was | Now |
|---|---|---|
| Muted text --z400 on white / canvas | 4.28 / 3.95 | 5.16 / 4.77 |
| Muted text --z400 on dark sunken surface | 4.28 | 4.82 |
| Green pill label on its tint | 4.39–4.49 | 5.40 |
| Red pill label on its tint (light / dark) | 4.02 / 3.43 | 5.52 / 6.11 |
| Amber "pending" pill label | 2.90 | 4.58 |
| Sidebar section label on navy | 3.77 | 6.03 |
| Login links, OS in dark mode | 2.17 | 5.18 |
| Version stamp | 2.64 | 4.82 / 6.19 |
Scale order was preserved on every token change (z300 < z400 < z500 in both themes), so nothing that relied on the ramp reordered.
Both columns are the same saved DOM rendered with the staging stylesheet (before) and the branch stylesheet (after), so differences are only the change. Pick a variant to switch theme or width. Screenshots scroll inside their frame; open one for full size.
Redacted for publication: the client's logo and name, staff names, e-mail addresses and phone numbers were replaced at the DOM level on both sides. Agent company names are the workspace's test data.
Almost everything is a token or a rule. The largest single diff is the transactions grid, and a guard test decided its shape.
/* light root */ - --z400:#6F7B90; + --z400:#5F6E88; /* 5.16:1 white · 4.77:1 canvas */ - --success:#15803D; + --success:#12703A; /* 5.4:1 on its tint */ /* dark root */ - --z400:#7C8899; + --z400:#8591A2; /* 4.82:1 on --surface-3 */ /* nine rules and seven scripts */ - color:#E11D48 + color:var(--danger) /* #BE123C light · #FB7185 dark */
/* a guard pins the ratio string, so the floor rides on the cells */ - .txn-table{…overflow:hidden} + .txn-table{…overflow:hidden;overflow-x:auto} + .txn-th.txn-grid,.txn-th.txn-grid+.txn-body{min-width:980px} - .txn-grid{display:grid;grid-template-columns:1fr 1.3fr .8fr 1.3fr 1.7fr .6fr .7fr 1.3fr;…} + .txn-grid{display:grid;column-gap:12px;grid-template-columns:1fr 1.3fr .8fr 1.3fr 1.7fr .6fr .7fr 1.3fr;…} + .txn-grid>:nth-child(3){min-width:150px} /* Time stays one line */ + .txn-sec{…position:sticky;left:0} /* second pass: inside the detail pane, two aligned lines instead of a scroll */ + .detail{container-type:inline-size} + @container (max-width:1099px){ + #cmp-detail-pane .txn-th.txn-grid,#cmp-detail-pane .txn-th.txn-grid+.txn-body{min-width:0} + #cmp-detail-pane .txn-grid{grid-template-columns:repeat(4,minmax(0,1fr));row-gap:6px} + }
+ body input:focus-visible,body select:focus-visible, + body textarea:focus-visible,body .xsel-trigger:focus-visible{ + outline:2px solid var(--teal);outline-offset:1px} - .field-value{…overflow:hidden;text-overflow:ellipsis;white-space:nowrap} + .field-value{…overflow-wrap:anywhere} + .detail:empty::before{content:"Select a load to see its details";…} + @media (max-width:1024px){ .ll-head{padding-top:64px} } + .kpi-grid{grid-template-columns:repeat(auto-fit,minmax(150px,1fr))}
// components/Console.tsx — the mount point, server-rendered + <div className="app" id="app" suppressHydrationWarning> + <div className="aix-boot" role="status" aria-live="polite"> + <span className="aix-boot-ring" aria-hidden="true" /> + <span className="aix-boot-t">Loading your workspace…</span> + </div> + </div> // app/not-found.tsx · app/error.tsx · app/global-error.tsx + <SystemPage eyebrow="Error 404" title="This page does not exist" message="…"> + <Link href="/" style={systemStyles.primary}>Back to the console</Link> + </SystemPage>
Each code pass was one message to a sub-agent running the repository's console-engineer rules, with the instruction "fix the change, not the test" whenever a guard went red. Condensed only where file paths repeated.
Log in with the UAT admin. Never click anything that launches, stops, approves, saves, deletes, exports, invites or dials. For each hash view (dashboard, loads, agents, campaign, history, analytics, latency, settings + every settings-tab) take a tall 1280px screenshot in light and dark; open each popup (add-load-open → al-insert-sample → al-parse, select-load → load-edit, agent-add, agent-select → agent-edit-fromdrawer, camp-setup, open-campaign → open-txn → each tab:*, rate-evidence) and screenshot it; close modals through modal-close, answer any 'Discard changes?' confirm, fall back to window.closeModal(). Repeat the main views at 768px and 390px and record horizontal overflow. Log console errors and every response ≥ 400.
For every visible text node: computed colour composited over the first opaque ancestor background, WCAG ratio vs 4.5:1 (3:1 for ≥24px or ≥18.66px bold); skip gradients/images. For every interactive element (button, a[href], [data-action], [role=button], rows, chips): snapshot background/colour/border/shadow/transform/outline/::before, hover, snapshot again, flag no change. Tab through the page and flag focused elements whose outline/shadow/border did not change. Record responses ≥ 400 and page errors. Group failures by colour pair.
Apply exactly these edits to frontend/app/aixfreight.css (+ one value in VersionBadge.tsx): 1 .field-value wraps (drop overflow/ellipsis/nowrap); 2 .txn-grid: content-floored 8 tracks + column-gap, .txn-table overflow-x:auto, .txn-th/.txn-body min-width 980px, .txn-sec sticky left; 3 .kpi-grid repeat(auto-fit,minmax(150px,1fr)); 4 .camp-chips grid auto-fill minmax(240px,1fr); 5 .detail:empty::before 'Select a load to see its details'; 6 @media ≤1024 .ll-head padding-top 64px; 7 .sp-actions .linkbtn nowrap + wrap at ≤720; 8 VersionBadge right 8 → 84. Keep .txn-grid on ONE line with exactly 8 track tokens (test_txn_agent_column_contract parses it). Verify with tsc, next build and the guard modules called directly (no Postgres). Fix the change, not the test.
Add components/SystemPage.tsx (inline styles: navy #0A1931, orange eyebrow, one orange action), app/not-found.tsx, app/error.tsx, app/global-error.tsx with the given content. In Console.tsx, first verify Auth.render(), AIXFreightMount() and renderBootError() all REPLACE #app innerHTML; only then render a splash (.aix-boot, role=status, 'Loading your workspace…') inside #app. Append the .aix-boot / .sys-page CSS to aixfreight.css. In views-home.js give activityCard() and monitorCard() the existing .ll-empty pattern when their list is empty. Do not touch MODULES (seven guard tests parse it). No new data-action, no id change.
Tokens: light --z400 → #5F6E88, dark --z400 → #8591A2, light --success → #12703A. Replace color:#E11D48 with var(--danger) in the nine listed rules; --amber-icon/#d97706 text → var(--amber); .sb-sec and .cobrand .cb-for 40% → 55% white. Hover: .date-chip, .ctl-toggle (before .on), .funnel-row[data-action]; move .pl-stage's inline style into CSS. Focus: body input/select/ textarea/.xsel-trigger:focus-visible outline 2px var(--teal). auth.css: pin --teal/--teal-ink/ --teal-soft and the new --z400. VersionBadge on var(--z500)/var(--surface-3). Replace inline '#15803D'/'#B91C1C'/'#E11D48'/'#D97706' in details.js, app.js, views-analytics.js, views-agents.js, agent-picker.js, agent-detail.js, whatsapp.js with the tokens — first grep backend/tests to be sure no guard pins those literals. Verify as in pass 1 plus node --check on each script.
Product repository and tracker are private. The design system, its skill and this page are public: github.com/aleemcolaberry/AiXFreight-Design-System.