Skip to main content

Design system

The visual contract for the Complied app, called Highlighter: tokens, type, shape, components, copy, and the shared UI primitives that carry them. For engineers and designers building or changing screens. Tokens are defined in src/index.css; this page says how to use them.

The idea​

A clipboard with one highlighter. Plain paper, black type, black buttons. A single yellow wash marks the thing you are on right now: the current nav item, the selected row, the focused field, selected text. Everywhere else, colour means status, and status is always a dot plus a word.

Consequently the app chrome uses no blue, purple or green, no display and body font pairing, and no eyebrow or kicker line above page titles.

Scope​

The rules apply inside the app shell (data-app-shell on the layout root, which switches font, corners and selection colour for everything below it). The public landing page (src/pages/LandingPage.tsx, src/components/landing/*) is a separate design system with its own fonts and colours and is out of scope; do not change it, and do not change the base Button in a way that restyles the landing page's calls to action beyond the existing dark primary.

Tokens​

Use the canonical names: canvas, surface, ink, ink2, ink3, rule, rule-strong, highlight, highlight-edge, and the status-* set. Never hard-code a hex value in a component.

RoleLightDarkUse
canvas#F6F6F4#16171APage background
surface#FFFFFF#1F2023Panels, drawers, dialogs
ink#17181A#F2F2EFText and primary buttons
ink2 / ink3#5F6368 / #9A9EA3#A7AAAE / #6F7378Secondary and tertiary text
highlight#FFF3A3#4A4310The current thing only
highlight-edge#E8C800#F3D94AMarker edge and selection mark
ring#D9B800#F3D94AFocus ring
rule / rule-strong#E5E6E3 / #D3D5D1#2C2D31 / #3A3C41Hairlines and input borders

Status. Overdue #C62828; due soon #C75B12, which is never yellow; scheduled is a hollow ink dot; in progress is ink; done #8A8F94; service is grey; lab is tan. Each status has dot, background and text tokens (--status-overdue-dot, -bg, -text, and so on) tuned per theme so text on its wash meets AA contrast.

Brand is ink. --os-brand resolves to ink, not a colour. The yellow wash is --highlight. Do not use bg-os-brand/5 for "selected"; use bg-highlight or the highlight-stroke utility. The os-*, tint-* and rail-* names and the older five-state status set remain declared only as deprecated aliases; do not use them in new code.

Yellow means "the thing you are on right now", never "warning". Warnings and urgency use the status colours.

Both themes are first-class (next-themes). Check light and dark before calling a screen done.

Type​

One family inside the app: Atkinson Hyperlegible Next (font-ui), applied by data-app-shell. Do not use font-heading or font-display in the app; those stay Space Grotesk for the landing page.

ElementStyle
Page titletext-[26px] font-semibold tracking-[-0.01em]
Bodytext-sm
Labeltext-[13px]
Captiontext-[11px], sentence case; tracked all-caps only for a table column header

Shape and density​

Corners are 12px inside the app (--radius; the landing page keeps 10px). Pills are fully round. Controls are 36px, list rows are 56px, and layout follows an 8px grid. src/index.css defines the density variables --row-h, --control-h and --section-gap for [data-density] values comfortable (56 / 36), compact (44 / 32) and field (56 / 44).

Field and tablet surfaces are the exception. Inspectors work in apartments on tablets, so on /field, /floor-plans and the inspection wizard (/field/:inspectionId and its field-job tabs) every control is 44px minimum. Page-local touch tabs, empty-state actions and descendant rules implement this; do not shrink them back to 36px.

Components​

ComponentRule
ButtonsPrimary is solid ink with white text. Secondary and outline are white with a hairline. Ghost is text. Destructive is red text on a transparent fill with a light red hover wash, never a red fill. There are no coloured fills.
SidebarWhite surface. The current item is the yellow wash. No left accent bar. The avatar is an ink disc with a white initial.
Top barCanvas background, hairline, quiet.
ListsHairline rows; hover bg-muted; selected bg-highlight. A group header is a quiet label, not a black bar.
TabsThe current tab is a solid ink pill with white text.
DrawersRight sheet, white, soft shadow, header, scrolling body, footer. A plain-words banner at the top ("Overdue by 4 days...").
DialogsCentred; interrupt only when needed. Same paper, hairline and footer.
Fields36px (44px on field surfaces), hairline border, yellow focus ring (ring-ring).
LinksInk, underlined on hover. Never a brand or green colour.
StatusA dot plus a word, always; never colour alone. The StatusDot and Pill components in redesign/kit.tsx take a tone.

Interactive hover is elevation only, with no scaling. Every interactive element keeps a visible focus ring and an accessible name. Anything made sticky must not trap keyboard focus.

Shared primitives​

Build screens from the shared primitives rather than re-implementing layout:

PrimitiveWhereUsed for
RecordsExplorersrc/components/redesign/RecordsExplorer.tsxThe list surfaces (/buildings, /projects, /clients, /deadlines, /field, /floor-plans, the documents table and several portal lists): columns, view tabs, groups, saved views, column picker, toolbar
DetailLayout, FactsRailredesign/DetailLayout.tsxDetail pages: breadcrumbs, a title block, content, and a facts rail of fixed width
DetailDrawerredesign/DetailDrawer.tsxSide panels over a list
Kitredesign/kit.tsxStatusDot, Pill, Stat, StatStrip, PageHeader, PillTabs, SearchBox, FilterChips, EmptyState, SectionNav, ObligationBanner
Filtersredesign/filters.tsx, src/lib/urlFilters.tsFilter menus and URL-backed filter state. The ""-deletes convention in applyParamPatch is load-bearing: a cleared filter must leave no trace in the URL.
Shellsrc/components/app/AppLayout, AppSidebar, TopBar, CommandPalette (global jump-to-record search), MobileNav, PageContainer (default content shell; usePageFullBleed lets a page such as the map run edge to edge), PortalLayout

Filter state lives in the URL so a view is a link and a saved view is a stored query string. Lists, detail pages and the portal share these primitives, so a change to a primitive changes every surface that uses it; check them all.

Copy​

Plain words. A screen answers "what needs doing, by when, and what happens if not" before anything else. Controls name the action ("Send proposal", not "Submit"). Use the glossary's terms (REQUIREMENTS.md); do not invent testimonials or marketing language inside the app.

Change rules​

  • Layout, structure, hierarchy and flow can change freely within these rules. Changing a token in src/index.css, a font or a radius is a design-system decision, not a screen-level tweak.
  • Do not change behaviour, routes or data while restyling a screen.
  • Do not reintroduce eco green, Command green or Space Grotesk in the app.
  • npm run lint and npm run build must pass, and npm run test if you touched anything with a test. Check both themes and keyboard focus.