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.
| Role | Light | Dark | Use |
|---|---|---|---|
| canvas | #F6F6F4 | #16171A | Page background |
| surface | #FFFFFF | #1F2023 | Panels, drawers, dialogs |
| ink | #17181A | #F2F2EF | Text and primary buttons |
| ink2 / ink3 | #5F6368 / #9A9EA3 | #A7AAAE / #6F7378 | Secondary and tertiary text |
| highlight | #FFF3A3 | #4A4310 | The current thing only |
| highlight-edge | #E8C800 | #F3D94A | Marker edge and selection mark |
| ring | #D9B800 | #F3D94A | Focus ring |
| rule / rule-strong | #E5E6E3 / #D3D5D1 | #2C2D31 / #3A3C41 | Hairlines 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.
| Element | Style |
|---|---|
| Page title | text-[26px] font-semibold tracking-[-0.01em] |
| Body | text-sm |
| Label | text-[13px] |
| Caption | text-[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
| Component | Rule |
|---|---|
| Buttons | Primary 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. |
| Sidebar | White surface. The current item is the yellow wash. No left accent bar. The avatar is an ink disc with a white initial. |
| Top bar | Canvas background, hairline, quiet. |
| Lists | Hairline rows; hover bg-muted; selected bg-highlight. A group header is a quiet label, not a black bar. |
| Tabs | The current tab is a solid ink pill with white text. |
| Drawers | Right sheet, white, soft shadow, header, scrolling body, footer. A plain-words banner at the top ("Overdue by 4 days..."). |
| Dialogs | Centred; interrupt only when needed. Same paper, hairline and footer. |
| Fields | 36px (44px on field surfaces), hairline border, yellow focus ring (ring-ring). |
| Links | Ink, underlined on hover. Never a brand or green colour. |
| Status | A 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:
| Primitive | Where | Used for |
|---|---|---|
RecordsExplorer | src/components/redesign/RecordsExplorer.tsx | The 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, FactsRail | redesign/DetailLayout.tsx | Detail pages: breadcrumbs, a title block, content, and a facts rail of fixed width |
DetailDrawer | redesign/DetailDrawer.tsx | Side panels over a list |
| Kit | redesign/kit.tsx | StatusDot, Pill, Stat, StatStrip, PageHeader, PillTabs, SearchBox, FilterChips, EmptyState, SectionNav, ObligationBanner |
| Filters | redesign/filters.tsx, src/lib/urlFilters.ts | Filter menus and URL-backed filter state. The ""-deletes convention in applyParamPatch is load-bearing: a cleared filter must leave no trace in the URL. |
| Shell | src/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 lintandnpm run buildmust pass, andnpm run testif you touched anything with a test. Check both themes and keyboard focus.