Hauler design system

Every token, chip and component

One page that renders the whole system in both themes, so a change can be reviewed in place rather than hunted for across screens. Nothing on this page invents styling: every colour comes from globals.css, every status from src/lib/status.ts, every value from src/lib/format.ts.

Light is the defaultStatus is never colour aloneHairlines, not shadows
Colour

Cool, slightly blue-cast neutrals — galvanised steel in shade, not pure grey. The low but non-zero chroma is what stops this reading as a default install. Swatch values below are read live from the DOM, so the dark column reports the dark value.

Light — the default
Surfaces
--background page canvas
--surface-raised cards, panels, rows
--surface-sunken table head, wells
--secondary row hover, skeletons
--muted quiet fills
--rail console icon rail
--foreground body text
--muted-foreground secondary text
Lines
--border the hairline
--border-strong focused frame, sticky edge
--input form control edge
--ring focus ring
Brand
--hivis the one action colour
--hivis-ink text ON hi-vis
--hivis-quiet selected row, active tab
--diesel structural accent
--destructive void, delete, failure
Chart series
--chart-1 revenue
--chart-2 margin
--chart-3 driver payout
--chart-4 disposal fee
--chart-5 tonnage / volume
Dark — opt-in
Surfaces
--background page canvas
--surface-raised cards, panels, rows
--surface-sunken table head, wells
--secondary row hover, skeletons
--muted quiet fills
--rail console icon rail
--foreground body text
--muted-foreground secondary text
Lines
--border the hairline
--border-strong focused frame, sticky edge
--input form control edge
--ring focus ring
Brand
--hivis the one action colour
--hivis-ink text ON hi-vis
--hivis-quiet selected row, active tab
--diesel structural accent
--destructive void, delete, failure
Chart series
--chart-1 revenue
--chart-2 margin
--chart-3 driver payout
--chart-4 disposal fee
--chart-5 tonnage / volume
The one risk taken
Hi-vis · action only

Hi-vis yellow-green is the exact colour of a Class-3 safety vest and of the reflective panels on a roll-off truck’s rear bumper. It is loud, so it occupies about 2% of pixels — primary buttons, the focus ring, the active nav indicator, the logo bar — and it is never used for status, which is what lets it acquire one stable meaning: this is the thing you press. On white it measures ~1.4:1, so it is a fill or a ≥2px ring only, never text and never a hairline.

Status palette — nine display tones

Each tone has a distinct lightness step as well as a hue, so the chips still form a readable ladder when hue collapses. Minimum ΔL between any two warm tones is 0.11 — plus a different icon, a different word, and a hazard stripe on overdue.

Light — the default
availableAvailable
AVAIL
reservedReserved
RSVD
scheduledScheduled
SCHED
transitIn transit
TRANSIT
onsiteOn site
ON SITE
fullPickup requested
FULL
overdueOverdue
OVERDUE
disposalReturning
DISPOSAL
oosMaintenance
OOS
Dark — opt-in
availableAvailable
AVAIL
reservedReserved
RSVD
scheduledScheduled
SCHED
transitIn transit
TRANSIT
onsiteOn site
ON SITE
fullPickup requested
FULL
overdueOverdue
OVERDUE
disposalReturning
DISPOSAL
oosMaintenance
OOS
Legend form — a dot is always paired with its word
AVAILRSVDSCHEDTRANSITON SITEFULLOVERDUEDISPOSALOOS
StatusChip

The atom of the system. Leading status bar, icon, label, optional trailing meta. Colour is never the only signal, the label is never omitted, and the aria-label is always the full sentence — “Bin RB-014, on site, day 12”.

Sizes
On siteD-12On siteD-12On siteD-1222px list · 28px default · 40px driver
Escalation — derived, never stored
On siteD-12OverdueD-47 ▏+3W

Nothing physically changes about a bin on day 31, so nothing in the database changes either — and no background job is needed to flip a status.

Compact label — ≤360px only
PICKUPOUT-DELMAINT

Drop the icon before you drop the label. The short form is still a word, never a bare colour.

bin_status
AvailableReservedIn transitOn siteReturningMaintenanceRetiredLostOverdue

on_site escalates to the overdue tone when the snapshotted included period has passed.

order_status
RequestedScheduledOut for deliveryOn sitePickup requestedPickup scheduledOut for pickupPicked upReady to invoiceInvoicedPaidCancelledOverdue

dumped reads as READY TO INVOICE — the label says the action, because that is the cue.

service_event_status
UnassignedAssignedEn routeOn siteCompletedFailedCancelled

unassigned is deliberately loud — a promised bin nobody is driving is the worst failure in this business.

invoice_status
DraftIssuedPart paidPaidVoidWritten offOverdue

past_due is a derived escalation of issued/partially_paid, never a stored value.

AssetPlate

The signature component: the stencilled number painted on a container flank, rendered as a stamped steel tag with a clipped corner. Identical in every surface — manager table, driver card, customer portal, printed invoice — so a bin number is instantly recognisable as a thing you can click.

Light — the default
30 ydRB-01440 ydRB-00230 ydRB-02030 ydRB-04430 ydRB-014
Dark — opt-in
30 ydRB-01440 ydRB-00230 ydRB-02030 ydRB-04430 ydRB-014
Typography

Barlow for the interface — a low-contrast grotesk rooted in Californian transport signage. Barlow Condensed for display, labels and asset plates, because condensed uppercase is the vernacular of stencilled container numbers. Azeret Mono for every numeral that sits in a column, because it reads like a scale-ticket printout and guarantees alignment.

  • Dispatch.type-displayCondensed 700 · 34/34 · −0.015em · UPPER
  • Order #1042.type-h1Barlow 600 · 24/30
  • Proof of delivery.type-h2Barlow 600 · 19/26
  • Scale ticket.type-h3Barlow 600 · 16/22
  • The bin sat on site for fifty-two days..type-bodyBarlow 400 · 15/22
  • Dense secondary text and table cells..type-smBarlow 400 · 13.5/20
  • Days on site.type-labelCondensed 600 · 12/14 · 0.09em · UPPER
  • Captured 08:12.type-label-smCondensed 600 · 11/13 · 0.10em · UPPER
  • Priced with 40 yd config v3..type-microBarlow 500 · 11/15
  • RB-014.type-plateCondensed 700 · 15/15 · 0.11em · UPPER
  • RB-014.type-plate-lgCondensed 700 · 22/22 · 0.10em · UPPER
  • $940.00.type-data-xlAzeret 500 · 30/32
  • 8,220 lb.type-data-lgAzeret 500 · 20/24
  • 0123456789 · $500.00.type-dataAzeret 500 · 13/18
  • 30.2672, −97.7431.type-data-smAzeret 400 · 12/16

Never set Azeret Mono below 12px, and never set condensed uppercase below 11px. Inside a driver surface every text node has a 16px floor — anything smaller triggers iOS zoom-on-focus in an input and is unreadable in a cab.

Radius, elevation, motion

4px base radius, deliberately flatter than the shadcn default — soft corners read as consumer SaaS. Hairlines carry structure; shadow is reserved for things that genuinely float. Nothing is a pill except an avatar.

Radius
2px · cell, checkbox
3px · chip, plate
4px · button, input
6px · card, panel
10px · dialog, sheet
full · avatar only
Elevation — the only four shadows
pop · dropdown
modal · dialog, sheet
toast
driver dock (points up)
Hazard treatments
.hazard-hatch-warn · overdue edge
.hazard-hatch · out of service
Section rule
StatTile

Flat, bordered, no gradient, no icon in a coloured circle, and no animated count-up — a ticking figure is unreadable and unverifiable. The warning footer renders only when it is actionable and non-zero: an absent warning is the good state, and “0 overdue” is never rendered.

Bins on site31524 overdueIn yard17
Runs today72 unassigned
Awaiting pickup5
Unbilled$4,180.003
MoneyCell · WeightCell · DaysOnSite

Every number that sits in a column is monospaced and tabular. Money is integer cents end to end and the formatter never receives a float; weight is integer pounds because that is what the scale prints, with short tons derived for display.

Money
Base
$500.00
Invoice total
$940.00
Driver payout
−$150.00
Disposal fee
−$182.50
Margin
$607.50
Margin, negative
-$42.00
Weight
Net, tons
4.11 t
Net, pounds
8,220 lb
Scale ticket net
8,220 lb · 4.11 t
Over allowance
6.10 t
Within allowance
2.40 t
Days on site — three bands
Neutral
12
At risk
264D LEFT
Overdue
47+3W
Inline, overdue
47 days17 days past included
Unknown

The band changes the wording and the icon, not only the colour.

src/lib/format.ts — the formatters, verbatim
formatMoney(94000)$940.00
formatMoneySigned(-15000)−$150.00
formatPercent(0.646)64.6%
formatTons(8220)4.11 t
formatWeight(8220)8,220 lb · 4.11 t
formatDateTime(…)23 Jul 2026 · 08:12
formatRelative(…, NOW)4 days ago
formatDaysOnSite(21)21 days on site
formatPastIncluded(39, 30)9 days past included
formatBinLabel('RB-014', 30)RB-014 · 30 yd
formatPhone('5125550143')(512) 555-0143
formatAddress({…})4412 Riverside Dr, Austin, TX 78741
formatCoords(30.2672, -97.7431)30.2672, −97.7431
ceilingNotation(22, 7, 4)⌈22 ÷ 7⌉ = 4
Timeline · EventFeed

The order lifecycle drawn as a route, not a vertical stepper — a bin travels a physical line: yard → site → disposal → yard. The delivered-to-picked-up segment is drawn heavy because it is the billable clock, and on this page that is the money. Below md the rail turns vertical with the same node vocabulary.

Light — the default
  1. Requested02 JUN 202609:41
  2. Scheduled02 JUN 202614:20
  3. Delivered03 JUN 202608:12
  4. On siteDAY 52Billable — 52 d
  5. Picked up
  6. At disposal
  7. Invoiced
Dark — opt-in
  1. Requested02 JUN 202609:41
  2. Scheduled02 JUN 202614:20
  3. Delivered03 JUN 202608:12
  4. On siteDAY 52Billable — 52 d
  5. Picked up
  6. At disposal
  7. Invoiced
Event feed — Robert's replacement for a text thread
  1. Scale ticket recordedD. Ramirez
    23 JUL 2026 · 11:22
    Ridgeline Transfer · TS-448120 · 8,220 lb net · fee $284.00
  2. Pickup confirmedD. Ramirez
    23 JUL 2026 · 09:04
    Photo + GPS captured 14 m from the recorded site.
  3. Pickup scheduledRobert H.
    22 JUL 2026 · 16:40
  4. Pickup requestedBergstrom Ridge Concrete
    22 JUL 2026 · 07:15
    “Bin is full, we're done — can you grab it Thursday?”
PhotoCard · PhotoGrid

Driver-captured proof. The metadata is as important as the picture, because this is evidence in a billing dispute — so the timestamp, coordinates, accuracy, distance from the recorded site and the capturing driver are on the face of the card. A GPS mismatch is shown, never suppressed.

DeliveryGPS
23 JUL 2026 · 08:1230.2672, −97.7431 ±6 mD. Ramirez

Set on gravel pad, gate code 4417.

PickupOff site 1.4 km
23 JUL 2026 · 09:0430.2801, −97.7554 ±22 mD. Ramirez
Scale ticketGPS
23 JUL 2026 · 11:2230.1899, −97.7912 ±9 mGPS override — No signal at the tipping floorD. Ramirez
No photo captured
20 JUL 2026 · 16:02
Empty · Error · Loading

Empty is an instruction, not a mood: icon in a hairline square, condensed headline, one sentence, one action. Errors state what failed and what to do — never “Oops!”, never “Something went wrong.” Skeletons are static blocks; a shimmer sweep implies progress that is not happening.

EmptyState

No runs scheduled

Nothing is going out today. Assign an order to a driver to build the run sheet.

ErrorState
ErrorState, inline — what a driver sees
LoadingState
Loading the board
CardSkeleton
Skeleton primitives
DataTable

Sorting, filtering, pagination and an empty state, with view state synced to the URL (?fleet_q=, ?fleet_sort=, ?fleet_page=) — never useState. A manager on the phone must be able to paste the URL of exactly what he is looking at, and a refresh must not lose the view. Structure comes from horizontal hairlines only: no zebra, no shadow, no row-enter animation.

Zero rows
shadcn primitives, restyled

The primitive set keeps its shadcn API and gets the industrial treatment in globals.css: Card loses its shadow for a hairline, Badge becomes a rectangle, table heads become condensed uppercase labels, and Button's default variant inherits the hi-vis fill through --primary.

Light — the default
AdminManagerDriverDeactivated
Dark — opt-in
AdminManagerDriverDeactivated
Shells

Three archetypes, three densities. The console is compact and desktop-first; the driver view is touch-density, mobile-first and forced-daylight capable; the portal is comfortable and calm. Each is a full-viewport surface, so they are reviewed on their own pages.

Review checklist

Run these before signing off a change to any surface.

  • Screenshot the dispatch board, desaturate it, and confirm every status is still identifiable. Greyscale review is a required step, not a nice-to-have.
  • Tab through the page: the dual focus ring must be visible on every interactive element, on every background, in both themes.
  • Zoom to 200%: the customer and driver views must not scroll the body horizontally. The manager board may — it is a data grid.
  • Confirm no component hardcodes a colour, a status label or an icon. Colours live in globals.css; statuses live in src/lib/status.ts.
  • Confirm every money value entered the formatter as integer cents and every weight as integer pounds.