Skip to content

Component kit

Every component the desk is built from, in every variant it supports, with the props beside it. This page is prerendered and uses no live data, so it is also the fastest way to check that a change did not quietly break something three screens away.

Design tokens

Slate neutrals, one indigo accent, semantic tones for state. Nothing else gets to introduce a colour.

Accent

Neutrals

Radius
rounded-xl on surfaces, rounded-full on pills
Depth
1px borders, not shadows
Focus
2px indigo focus-visible ring on every control

UiButton

Renders a NuxtLink when to is set, a button otherwise. Loading implies disabled.

As a link
PropTypeNotes
variantprimary | ghost | danger | subtleDefaults to primary.
sizesm | mdmd is the default.
tostring?Present ⇒ renders NuxtLink instead of button.
typebutton | submitIgnored when to is set.
loadingbooleanShows a spinner and blocks the click.
disabledbooleanAlso removes it from the tab order.

UiInput

Label is always rendered — placeholder-only fields lose their label the moment you type.

Shown on invoices.

must be a valid email

Stored as minor units.

PropTypeNotes
modelValuestring | numberv-model.
labelstringBound to the input with a generated id.
errorstring?Sets aria-invalid and renders below the field.
hintstring?Hidden while an error is showing.
typestringNative input type. Defaults to text.
requiredbooleanMarks the label and sets the attribute.

UiSelect

Native select on purpose — it beats every custom dropdown on mobile and with a screen reader.

pick one

PropTypeNotes
modelValuestringv-model.
labelstringAlways rendered.
options{ value, label }[]Order is respected as given.
errorstring?Same treatment as UiInput.

UiTextarea

For notes and descriptions. Same label and error contract as UiInput.

2000 characters max

PropTypeNotes
modelValuestringv-model.
labelstringAlways rendered.
rowsnumberDefaults to 4.
errorstring?Same treatment as UiInput.

UiCard

The only surface primitive. Header and footer are optional slots that add their own dividers.

With a header

Default slot content sits between the two.

And a footer.

Body only — no dividers rendered.

PropTypeNotes
defaultslotCard body.
headerslot?Adds a bottom-divided header region.
footerslot?Adds a top-divided footer region.

UiBadge

The tone vocabulary every other badge is built from.

graygreenamberredblue
PropTypeNotes
tonegray | green | amber | red | blueDefaults to gray.
defaultslotLabel text. Keep it to one or two words.

StatusBadge

Maps a client or task status to a tone, so status colour is decided once for the whole app.

ActivePausedArchived
To doIn progressBlockedDone
PropTypeNotes
statusClientStatus | TaskStatusLabel comes from statusLabel(), tone from the map.

PriorityBadge

Four levels, ranked urgent → low. Colour is not the only signal; the word is always there.

Urgent High Medium Low
PropTypeNotes
priorityTaskPriorityurgent | high | medium | low.

StatTile

One number, one label, one optional line of context. Used across the dashboard header row.

Active clients

8

12 on the books

Overdue

3

Past their due date

Unbilled work

$4,182.50

Not yet invoiced

Paid this quarter

$19,240.00

PropTypeNotes
labelstringSits above the value.
valuestring | numberPre-format money and durations before passing them.
hintstring?Small line underneath.
tonegray | green | amber | red | blueDefaults to gray.

SparkBars

Inline SVG, no charting library. Zero days stay visible as a baseline tick rather than disappearing.

2 Aug 202615 Aug 2026
PropTypeNotes
data{ date, minutes }[]Oldest first. Scales to the tallest bar in the set.

DonutChart

Also inline SVG. Every segment carries its label and value in text, so it survives being read aloud.

48total
  • To do14
  • In progress9
  • Blocked3
  • Done22
PropTypeNotes
segments{ label, value, tone }[]Zero-value segments are dropped, not drawn as slivers.

UiEmptyState

Every list in the app has one. An empty table with headings and no rows is a bug report waiting to happen.

No clients match those filters

Nothing on the desk matches the current search and status. Clearing the filters brings the full list back.

Clear filters
PropTypeNotes
titlestringOne short line.
descriptionstringSay what to do next, not just that it is empty.
actionLabelstring?Omit both action props for a message-only state.
actionTostring?Route for the action link.

UiPagination

Emits update:page; the page owns the state and writes it to the query string.

Current page: 3

PropTypeNotes
pagenumber1-indexed.
pagesnumberTotal page count, never below 1.
totalnumberRow count, used for the “x–y of z” line.
perPagenumberPage size.
@update:page(n: number) => voidControlled component — it never mutates its own page.

UiSkeleton

Shown only on client-side navigation. SSR renders real data, so the first paint never needs one.

PropTypeNotes
rowsnumberPlaceholder lines to draw. Defaults to 3.

UiModal

Focus is trapped while open and Escape closes it. Used for destructive confirmations.

PropTypeNotes
openbooleanControlled by the parent.
titlestringBecomes the dialog’s accessible name.
@close() => voidFired by Escape, the backdrop and the close button.
footerslot?Action row. Put the destructive button last.

ClientCard

The clients grid tile. Rate is formatted through money() in the client's own currency.

Northline Research

Northline Workforce

Active

Monthly retainer — SEO advisory site and keyword mapping.

Rate
$45.00/h
Updated
4 days ago

Harbour & Vine

Harbour & Vine Ltd

Paused

Nuxt 2 legacy maintenance. Paused pending their rebrand.

Rate
£60.00/h
Updated
4 days ago
PropTypeNotes
clientClientThe whole record — the card links to /clients/:id itself.

TaskRow

One task in a list. Overdue is computed from the due date and the status, never stored.

PropTypeNotes
taskTaskThe whole record.
clientNamestring?Omit inside a single-client view.
@toggle() => voidParent owns the write — the row never calls the API itself.

App chrome

The pieces the default layout mounts once, around whatever page is rendering.

DriverBanner

Silent on Postgres. On the memory driver it says so, because a visitor should not discover that by losing work.

ToastHost

CommandPalette

Press ⌘ K (or CtrlK) anywhere in the app.

PropTypeNotes
DriverBanner—Fetches /api/health. No props.
ToastHost—Reads the queue from useToast(). Mounted once in app.vue.
CommandPalette—Global Cmd/Ctrl+K navigation. Mounted once in the layout.
AppHeader / AppFooter—Nav, theme toggle, locale switch, account menu.

Forge Desk — Nuxt 3 SSR + Nitro API. Portfolio demo, not a real billing system.

Style guidelocal