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.
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
- One signal.
--accent(orange) means this is the action or this is where you are. Never use it for decoration, charts or illustration. - Status is state. Green, red, yellow and blue only describe the state of something — running, failed, pending, informational. They never decorate.
- 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.
- Flat by default. Surfaces are separated by 1px rules, not shadows. Shadows exist only on things that float: menus, drawers, dialogs.
- 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
| Token | Light | Dark | Use |
|---|---|---|---|
--bg-base | #ECECE8 | #0E0F11 | App ground, canvas — "concrete" |
--bg-surface | #FFFFFF | #17191C | Cards, panels, inputs |
--bg-elevated | #F7F7F5 | #131518 | Side panels, table headers, field fills |
--bg-hover | #E6E6E1 | #22252A | Hover state on rows and ghost controls |
--border | #DCDCD6 | #2A2D32 | Every 1px rule |
--border-control | #8A9097 | #6B7178 | Form field edges (3:1 against surfaces) |
#Navigation rail
The rail is graphite in both themes so navigation reads as one fixed object.
| Token | Light | Dark | Use |
|---|---|---|---|
--rail-bg | #141619 | #0A0B0C | Rail background |
--rail-fg | #8B9097 | #8B9097 | Idle icon and label |
--rail-fg-active | #FFFFFF | #FFFFFF | Current destination |
--rail-hover-bg | #22252A | #1B1D21 | Icon tile on hover, rail hairlines |
--rail-active-bg | #262A30 | #22252A | Icon tile of the current destination |
#Signal
| Token | Light | Dark | Use |
|---|---|---|---|
--accent | #C2410C | #F26B1D | Primary action, current location, focus ring |
--accent-hover | #9A3412 | #FB8B47 | Hover on primary |
--accent-fg | #FFFFFF | #141619 | Text on an accent fill (graphite in dark: white on orange fails contrast) |
--accent-muted | 9% accent | 14% accent | Selected row, selected segment |
#Status
Each status has a solid, a -muted fill for backgrounds and a -text for words on that fill.
| Token | Light | Dark | Means |
|---|---|---|---|
--accent-green | #15803D | #4ADE80 | Running, healthy, succeeded |
--accent-red | #B91C1C | #F87171 | Failed, error, destructive |
--accent-yellow | #A16207 | #FBBF24 | Pending, degraded, warning |
--accent-blue | #1D4ED8 | #60A5FA | Informational, deploying |
#Text
| Token | Light | Dark | Use |
|---|---|---|---|
--text-primary | #16181B | #ECEDEE | Headings, values |
--text-secondary | #41464D | #C3C6CA | Body |
--text-muted | #5F656C | #9097A0 | Labels, meta |
--text-dim | #8A9097 | #767C83 | Placeholders, disabled |
#Terminal
Logs, build output and exec sessions sit on a dark surface in both themes, so output looks the same wherever it appears.
| Token | Value | Use |
|---|---|---|
--terminal-bg | #141619 | Console background |
--terminal-border | #2A2D32 | Rules inside the console |
--terminal-fg | #C3C6CA | Output text |
--terminal-dim | #8B9097 | Timestamps, debug lines, column heads |
--terminal-red | #F87171 | Error level |
--terminal-yellow | #FBBF24 | Warn level |
--terminal-blue | #60A5FA | Info level, links |
--terminal-green | #4ADE80 | Live indicator, success |
--terminal-orange | #F0B07E | Origin column (service or source) |
#Typography
| Role | Font | Size / weight |
|---|---|---|
| Page title | IBM Plex Sans | 20px / 700, -0.02em |
| Eyebrow | IBM Plex Sans | 11px / 700, --accent |
| Section title | IBM Plex Sans | 13–14px / 600 |
| Body | IBM Plex Sans | 13px / 400 |
| Label | IBM Plex Sans | 11–12px / 500–600, --text-muted |
| Machine output | IBM Plex Mono | 12px / 400 |
| Metric | IBM Plex Mono | 18px / 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
| Token | Value | Use |
|---|---|---|
--radius-sm | 4px | Chips, segments inside a group |
--radius-md | 6px | Buttons, inputs, icon tiles |
--radius-lg | 8px | Cards, consoles, panels |
--radius-xl | 12px | Floating layers only |
--shadow-md | — | Dropdowns, popovers |
--shadow-lg | — | Drawers, dialogs |
--transition-fast | 120ms | Hover, colour changes |
--transition-normal | 200ms | Indicators, drawers |
--rail-width | 64px | Navigation rail |
--slide-panel-width | 480px | Right-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:
<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.
#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.
#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.
#Navigation rail
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
--accentbar 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-bgand the icon stroke thickened; - the label in
--rail-fg-activeat 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.
#Panels and drawers
- Nav drawer — opens beside the rail for a group's sub-pages (admin) or the project list (tenant). Active item:
--accent-mutedfill,--accentlabel. - Slide panel — detail views for one resource open from the right at
--slide-panel-width, stacked throughuiStore.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.
#Page layout
- Tenant pages: rail → project drawer (collapsible) → page content on
--bg-base, padded20px 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
| Do | Don't |
|---|---|
Reference tokens: var(--accent-red) | Hard-code hex values (except inside color-mix()) |
Use <Button> and the shared ui components | Re-style buttons per component |
| Put icon and label in one clickable target | Make only the icon clickable |
| Mark the current place with the signal bar + filled tile | Use a coloured background alone, or an inset shadow clipped by a radius |
| Use mono for hashes, ports, image refs, logs | Use mono for sentences |
Separate surfaces with 1px --border rules | Add shadows to cards that do not float |
Use class={cond ? 'a b' : 'a'} on Svelte components | Use class:name directives on components |