AiXFreight Case study
Case study · September 2026 · design system + Claude Code skill

Re-skinning a shipped console without touching its behaviour

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.

97screens captured · light, dark, tablet, mobile
1,646 → 0text nodes below AA contrast (light)
22flaws logged · 14 fixed on the branch
0behaviour, API or data-action changes
Context

The build, the brief, the constraints

The build

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.

The brief

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.

The constraints

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.

Method

Six steps, all scripted, all repeatable

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.

Level the branch with UAT

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.

Capture every screen

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.

Review against the skill

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.

Measure what eyes miss

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.

Fix through the repo's own rules

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.

Prove it and publish

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.

What the skill contributed

Rule → what it caught → what changed

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 ruleWhat it caughtWhat 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."
Numbers

Before and after, measured the same way

"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.

CheckBeforeAfter
Text nodes below AA contrast, light1,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, dark212 offline → expected 0
Form controls / custom select with no focus ring7 kinds0
Clickable controls with no hover feedback9 kinds4 fixed · 1 false positive · native selects by design
Blank regionsboot, Loads pane, 2 dashboard cards0
Unknown URLNext.js default 404styled page, HTTP 404, light/dark/mobile
JavaScript page errors00
Files changed for the visual fixes1 stylesheet + 1 auth stylesheet + 8 scripts (colour strings only) + 1 badge component; 4 new shell files
Colour pair (light)WasNow
Muted text --z400 on white / canvas4.28 / 3.955.16 / 4.77
Muted text --z400 on dark sunken surface4.284.82
Green pill label on its tint4.39–4.495.40
Red pill label on its tint (light / dark)4.02 / 3.435.52 / 6.11
Amber "pending" pill label2.904.58
Sidebar section label on navy3.776.03
Login links, OS in dark mode2.175.18
Version stamp2.644.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.

Code

What the changes look like

Almost everything is a token or a rule. The largest single diff is the transactions grid, and a guard test decided its shape.

Tokens

/* 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 */

Transactions grid

/* 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}
+ }

Focus, hover, overflow

+ 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))}

Shell

// 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>
Prompts

The working instructions, verbatim where it matters

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.

Capture — Playwright, screens only
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.
Audit — contrast, hover, focus, network
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.
Pass 1 — layout fixes, CSS only
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.
Pass 2 — shell pages, boot splash, empty states
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.
Pass 3 — contrast, hover, focus from the measured audit
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.
Verification & what stayed open

Proof, and the honest gaps

How it was proved

  • npx tsc --noEmit and npm run build pass; node --check on every edited console script.
  • No database on the review machine, so every guard module that reads the touched files was loaded directly and its test functions called: 542 pass; the failures are tests that need Postgres or a fixture, plus one guard that was already red on staging.
  • Two guards shaped the work: one requires exactly eight track tokens on the transactions grid line, another pins its literal ratio string. The fix respected both instead of editing them.
  • The production build was started locally without a backend to photograph the 404 page (real HTTP 404) and the splash handing over to the login screen.
  • The product owner was asked to run the full suite and the Playwright end-to-end specs before merging.

Left for product and backend decisions

  • The New Campaign dialog stays open when you navigate with the sidebar.
  • The transcript tab shows a server-error card for calls that were never answered; it should be an empty state.
  • A raw status enum ("COMPLETED_CALL") is shown next to the status.
  • Campaign filter labels sit off their controls; one page has two names; an unlabelled "0" badge in Add Agent.
  • Admin page loads fire 403 and 404 API calls; the theme-contrast guard is red on staging for a co-brand chip.
  • The "Heard at" final-rate link could not be reviewed: no call in the workspace had a captured final rate.

Product repository and tracker are private. The design system, its skill and this page are public: github.com/aleemcolaberry/AiXFreight-Design-System.