Design Language

Components

Every primitive we ship, live and interactive.

Actions

Five button variants, four sizes. brand and default are the same pink, which is deliberate: shadcn components reach for primary, and pointing --primary at --brand keeps them on-brand without a second colour to maintain. One primary action per block. The filled plate is --brand-solid rather than the fill itself, because --brand-foreground on --brand measured 3.94:1 here (PRO-2137), and it deepens on hover rather than fading: an alpha on a brand plate composites toward the cream page.

variants
sizes
states

A disabled button keeps its shape and loses its reaction. It never disappears.

Toggle

A button that stays pressed. Use it for a view mode, not for a setting that needs saving.

Status and identity

Badges name a state; they are never a button. Avatars fall back to initials rather than to a silhouette, and a group collapses to a count past the third face.

Badge
DefaultSecondaryOutlineDestructiveBrandSuccessWarnInfo

Every status variant pairs the role's tint with its ink member, brand included since PRO-2137. It was the last one still on its own fill, at 3.22:1 against the very tint beside it, and it took its own pass because --brand is re-pointed per team: its ink had to be derived from whatever the team stored rather than measured once. It now reads 5.55.

Avatar and AvatarGroup
CH
CHAMRK
+4
Progress

Determinate only. If you do not know the proportion, use the brand loader instead of a bar that lies.

Skeleton

For a first paint with no data at all. Once a value exists, DataReveal crossfades it in rather than swapping a skeleton row for a content row, because swapping rows makes the page jump.

Spinner
Loading devices

For brief indeterminate work inside an action or status. Keep its loading label available to assistive technology.

Animated icon
Hover the bot

A quiet identity cue that animates once on hover. Reduced motion keeps it still.

Forms

Every control is labelled, and the label is a real <Label htmlFor> rather than a paragraph above the field. Placeholder text is an example, never the label.

Field

The team uses this link to install the approved Shortcut.

Input, Label, Textarea
Select, Checkbox, Switch, Slider
OptionCards

A radio group with room to explain each choice. Use it where the difference between the options needs a sentence; use a Select where it does not.

OrientationSteps
  1. 1Connect the device over USB.
  2. 2Trust the host when the phone asks.
  3. 3Wait for the provider to claim it.

Numbered guidance for a physical task, where a paragraph would be re-read three times.

Overlays

All four float on the same glass and enter on the same motion. The difference is what they are for: a Dialog is a task, an AlertDialog is a decision you cannot undo, a Sheet is a side surface for something long, and a Popover is a detail attached to its trigger.

FormDialogShell is the house form dialog, and its showAccentRule prop defaults off here for the same reason no page in the catalog draws a hairline under a title.

open them

These are the real components. Escape closes, focus is trapped, and the overlay motion is the sanctioned pattern.

Structure

Card for a self-contained block, Tabs for views of one subject, Separator where a gap is not enough, and Table for anything a person will compare down a column.

Card
iPhone 14 Pro
iOS 17.4 · Provider: mac-01
Available. Last used 12 minutes ago.
Tabs and Separator
Summary of the current period.
A Separator carries a real semantic break.
Table
DeviceOSStatusMinutes
iPhone 14 ProiOS 17.4Available1118
Pixel 7Android 14In use911
iPhone SEiOS 16.7Offline41

Numeric columns take .tabular-nums-pro so the digits line up. A status cell pairs a tone-coded dot with its word, because colour is never the only signal.

Page patterns

The recurring shapes that are not single components. Getting these consistent is what makes two pages built months apart feel like one product.

empty state
No devices yet

Connect a phone to a provider host and it shows up here within a few seconds.

Say what would be here, why it is not, and the one action that changes that. Never just 'No results'.

item group
  • 01
    iPhone 14 Pro

    iOS 17.4 · Available

  • 02
    Pixel 7

    Android 14 · In use

Use Item for a compact row with media, a clear label, supporting context, and one trailing action.

filter bar
3 of 41

Chips share one class from lib/ui/filter-chip so every filtered surface reads identically. Active filters stay visible, and the count sits next to the input that drives it.

Alert · inline, persistent

For a condition on the page. A toast is for something that just happened.

toasts · transient, one per outcome

Success is short. An error names what to do next. Never fire two for one action.

Two primitives that need their shell

These are the only entries in components/ui that this page points at rather than renders, and the reason is the same in both cases: faking their context would mean building a copy, and a copy is exactly what this catalog exists to avoid.

sidebar.tsx
Rendered by the authenticated app shell.

Twenty-three exports that compose the app shell's floating glass rail: provider, slab, groups, menu buttons, sub-menus, rail, trigger, inset. It needs the shell's frosted slab, its collapse state, and a route to be active in. See it in the app itself, and read components/layout/app-sidebar.tsx for the composition.

gooey-tab-pill.tsx
Measured from a live rail by its consumer.

useGooeyTabPill measures a live rail with a ResizeObserver and hands back the pill's rect; GooeyTabPill and GooeyTraceBar draw it. The motion is the pill morphing out of the tab you pressed, so it only exists where a real rail owns real tabs. The Tabs demo above and the app's segmented rails are it in situ.

pointer-events-guard.tsx
Renders nothing. Mounted once, at the root.

Renders null, and has no appearance to demonstrate. Mounted once at the root, it releases a `pointer-events: none` that a torn-out modal layer left on <body>. That lock makes every control on every page stop responding until a hard refresh, while everything still paints normally, so it reads as a dropdown that opens but cannot be clicked. It acts only when no layer is mounted, so an open dialog keeps its own lock.