Pages and sections

shipyard.trian.space/docs/design-system

Design system

Drydock, the visual language of the Shipyard dashboard — tokens, type, components and the rules that keep them consistent.

View as .md

Shipyard's dashboard is built on Drydock: graphite and concrete neutrals carry the interface, one signal colour marks what you can act on, and status colours are reserved for state. It is deliberately quiet. The interesting thing on screen should be your services, not the chrome around them.

The source of truth is frontend/src/routes/layout.css (tokens) and frontend/src/lib/components/ui/ (components). This page documents both. If you are building a screen, an extension or a generated UI, follow it.

#Principles

  1. One signal. --accent (orange) means this is the action or this is where you are. Never use it for decoration, charts or illustration.
  2. Status is state. Green, red, yellow and blue only describe the state of something — running, failed, pending, informational. They never decorate.
  3. Never colour alone. Every status also has a shape or a word: a glyph, a label, a badge. Colour-blind users and grayscale screenshots must read the same.
  4. Flat by default. Surfaces are separated by 1px rules, not shadows. Shadows exist only on things that float: menus, drawers, dialogs.
  5. Machines speak mono. IBM Plex Mono is for what a machine produced or will read — image refs, ports, hashes, slugs, log lines. Prose is always Plex Sans.

#Colour

All colours are CSS custom properties on :root, redefined under [data-theme="dark"]. Components reference tokens, never hex values.

#Surfaces

TokenLightDarkUse
--bg-base#ECECE8#0E0F11App ground, canvas — "concrete"
--bg-surface#FFFFFF#17191CCards, panels, inputs
--bg-elevated#F7F7F5#131518Side panels, table headers, field fills
--bg-hover#E6E6E1#22252AHover state on rows and ghost controls
--border#DCDCD6#2A2D32Every 1px rule
--border-control#8A9097#6B7178Form field edges (3:1 against surfaces)

The rail is graphite in both themes so navigation reads as one fixed object.

TokenLightDarkUse
--rail-bg#141619#0A0B0CRail background
--rail-fg#8B9097#8B9097Idle icon and label
--rail-fg-active#FFFFFF#FFFFFFCurrent destination
--rail-hover-bg#22252A#1B1D21Icon tile on hover, rail hairlines
--rail-active-bg#262A30#22252AIcon tile of the current destination

#Signal

TokenLightDarkUse
--accent#C2410C#F26B1DPrimary action, current location, focus ring
--accent-hover#9A3412#FB8B47Hover on primary
--accent-fg#FFFFFF#141619Text on an accent fill (graphite in dark: white on orange fails contrast)
--accent-muted9% accent14% accentSelected row, selected segment

#Status

Each status has a solid, a -muted fill for backgrounds and a -text for words on that fill.

TokenLightDarkMeans
--accent-green#15803D#4ADE80Running, healthy, succeeded
--accent-red#B91C1C#F87171Failed, error, destructive
--accent-yellow#A16207#FBBF24Pending, degraded, warning
--accent-blue#1D4ED8#60A5FAInformational, deploying

#Text

TokenLightDarkUse
--text-primary#16181B#ECEDEEHeadings, values
--text-secondary#41464D#C3C6CABody
--text-muted#5F656C#9097A0Labels, meta
--text-dim#8A9097#767C83Placeholders, disabled

#Terminal

Logs, build output and exec sessions sit on a dark surface in both themes, so output looks the same wherever it appears.

TokenValueUse
--terminal-bg#141619Console background
--terminal-border#2A2D32Rules inside the console
--terminal-fg#C3C6CAOutput text
--terminal-dim#8B9097Timestamps, debug lines, column heads
--terminal-red#F87171Error level
--terminal-yellow#FBBF24Warn level
--terminal-blue#60A5FAInfo level, links
--terminal-green#4ADE80Live indicator, success
--terminal-orange#F0B07EOrigin column (service or source)

#Typography

RoleFontSize / weight
Page titleIBM Plex Sans20px / 700, -0.02em
EyebrowIBM Plex Sans11px / 700, --accent
Section titleIBM Plex Sans13–14px / 600
BodyIBM Plex Sans13px / 400
LabelIBM Plex Sans11–12px / 500–600, --text-muted
Machine outputIBM Plex Mono12px / 400
MetricIBM Plex Mono18px / 600, tabular figures

Fonts are bundled with @fontsource rather than fetched from Google Fonts: Shipyard is self-hosted and often runs on networks without outside access.

#Shape, elevation, motion

TokenValueUse
--radius-sm4pxChips, segments inside a group
--radius-md6pxButtons, inputs, icon tiles
--radius-lg8pxCards, consoles, panels
--radius-xl12pxFloating layers only
--shadow-md—Dropdowns, popovers
--shadow-lg—Drawers, dialogs
--transition-fast120msHover, colour changes
--transition-normal200msIndicators, drawers
--rail-width64pxNavigation rail
--slide-panel-width480pxRight-hand slide panels (100vw on phones)

All motion respects prefers-reduced-motion. Focus is always a 2px --accent outline with a 2px offset.

#Components

Components live in $lib/components/ui and are imported from its index:

Usage
<script lang="ts">
  import { Button, Badge, SearchInput, Select, Toggle, PageHeader } from '$lib/components/ui';
</script>

#Buttons

<Button variant size> with variants primary, secondary, ghost, danger, danger-outline and sizes sm (30px), md (36px), icon (32px square). Hover and press are a currentColor overlay at 8% / 12%, so every variant gets the same feedback. One primary button per view.

Deploy Redeploy Cancel Delete service

#Badges and status

<Badge tone> for a state word (green, red, yellow, blue, neutral); <StatusDot> for a compact live state next to a name. Pair a dot with text whenever there is room.

Running Failed Pending Deploying Stopped

#Segmented control

Filters with a handful of mutually exclusive options (time range, log level, source) use a segmented group: one surface with a 1px border, the selected segment filled with --accent-muted and labelled in --accent. Levels add a glyph so the state is not carried by colour alone: ◆ error, ■ warn, ● info, ○ debug.

1h6h24h7d30d AllErrorWarnInfoDebug

The rail is the app's primary navigation: a 64px graphite column with the brand mark, destinations and account controls. Each destination is one target containing its icon tile and label.

The current destination gets three cues together:

  • a 3px --accent bar flush with the rail's leading edge, 22px tall and rounded on its outer side — it sits outside the icon tile so the tile's radius never clips it;
  • the icon tile filled with --rail-active-bg and the icon stroke thickened;
  • the label in --rail-fg-active at weight 600.

Hovering an idle destination previews the bar as an 8px neutral tick. On phones the rail becomes a 56px bottom bar and the indicator moves to the top edge.

Home Projects Logs Settings

#Panels and drawers

  • Nav drawer — opens beside the rail for a group's sub-pages (admin) or the project list (tenant). Active item: --accent-muted fill, --accent label.
  • Slide panel — detail views for one resource open from the right at --slide-panel-width, stacked through uiStore.pushPanel(). Actions inside a detail panel push a sub-panel; they never inline a form.
  • Dialog — confirmation for destructive actions, always requiring the resource name to be typed.

#Log console

Runtime logs use the terminal surface. Columns are time, level, origin and message; rows are 12px mono at 1.55 line height. Error and warn rows carry a 2px leading bar in their level colour; the selected row uses the signal bar instead. Masked secrets render as a dim ••• [kind] chip. Selecting a row opens a detail pane beside the console with the full message, metadata and structured fields.

14:02:11.204INFapi.1GET /healthz 200 1ms
14:02:12.870WRNworker.2retrying job 812 (attempt 2/5)
14:02:13.019ERRapi.1db connect failed: password ••• [secret] rejected
14:02:14.551DBGapi.1pool size 8, idle 6

#Page layout

  • Tenant pages: rail → project drawer (collapsible) → page content on --bg-base, padded 20px 28px.
  • Admin pages: rail → modal nav drawer → page content, padded 24px 28px.
  • Every page opens with <PageHeader eyebrow title subtitle actions>; the eyebrow names the area ("Observability"), the title names the page.
  • Content width follows the content: tables and consoles go full width, forms cap at about 640px.

#Do and don't

DoDon't
Reference tokens: var(--accent-red)Hard-code hex values (except inside color-mix())
Use <Button> and the shared ui componentsRe-style buttons per component
Put icon and label in one clickable targetMake only the icon clickable
Mark the current place with the signal bar + filled tileUse a coloured background alone, or an inset shadow clipped by a radius
Use mono for hashes, ports, image refs, logsUse mono for sentences
Separate surfaces with 1px --border rulesAdd shadows to cards that do not float
Use class={cond ? 'a b' : 'a'} on Svelte componentsUse class:name directives on components