Components
Every element Aros is built from, drawn once and specified to the pixel. v1.1 rebuilds the specimens to match what the operator console and the customer portal actually ship — a component here and a component there are the same component. If they disagree, one of them is wrong, and it is usually the one that was drawn by hand.
Reading a spec
Every component below follows the same skeleton so you can scan it the same way each time.
| Anatomy | The named parts and where padding lives |
| Variants | Distinct purposes — not just colours |
| States | Default · hover · focus · active · disabled · loading · error |
| Sizing | The allowed sizes and their tokens, at dense (console) unless stated |
| Spec | Padding, radius, type, border — all as tokens |
| Motion | What animates, for how long, on what curve |
| In code | The primitive that implements it in aros-frontend / aros-portal |
Fields & controls
Inputs collect data. Every field has a visible label, a focus state with a green ring, and clear error messaging — delivered by one field grammar, so a form built next year matches one built today.
2.2.1 The field grammar
- Label — always visible, always associated (
for/id). "optional" is stated; "required" is the default and unmarked. - Control — input, textarea or select on the page surface;
aria-invalid+aria-describedbyare wired by the grammar, not by hand. - Hint or error — one line beneath. The error replaces the hint and is announced (
role="alert").
2.2.2 Text input — states
/ focuses it from anywhere on the page2.2.3 Select, checkbox, radio, toggle
2.2.4 Spec
| control | height 36 · padding 0 12 · radius --r-md · 1px --border-strong on --bg |
| focus ring | border --p500 + 3px --p-tint glow · d-fast |
| error | border --danger + help in --danger-ink, role="alert" |
| label | Atkinson 500 · 14 (dense) / 15 · --text · gap --s2 (6px) to the control |
| hint | 13 · --text-2 · replaced by the error |
| select | native, appearance none, chevron drawn in --text-3 |
| in code | ui/field.tsx + ui/input.tsx (Input · Textarea · Select) — portal: PortalField on the same class |
Status & badges
Aros is a status product. These are its most important components — and the most disciplined. State is always colour + shape + label, never colour alone.
2.3.1 The live dot
Only the healthy dot pulses — the signature heartbeat, meaning "live and observed right now." Degraded, offline, and unknown are static. Under prefers-reduced-motion the ring is removed and the dot stays.
2.3.2 Pills, badges, tags, severity
2.3.3 State → colour → glyph
| State | Fill | Ink | Dot | Words we use |
|---|---|---|---|---|
| Healthy | --positive | --positive-ink | pulsing | Operational · Healthy · Live · Up |
| Degraded | --warn | --warn-ink | static | Degraded · Read-only · At risk · Running · Burning |
| Offline | --danger | --danger-ink | static | Offline · Down · Failed · Exhausted · Detected |
| Unknown | --neutral | --neutral-ink | static | Unknown · Pending · Queued · Paused · Not yet run |
never_run. When the surface knows why, the pill expands to say so — the meaning, the causes, the rule that produced it, when it was evaluated — and says "not reported" where it does not know, rather than staying quiet.| pill | Plex Mono 500 · 13 (dense) / 14 · radius pill · padding 2 10 2 8 · dot 8px · 1px tint border · tint fill · ink text |
| badge | Plex Mono 500 · 13 · UPPER +0.12em · radius --r-sm · padding 2 8 |
| tag | Plex Mono · 13 · UPPER +0.08em · radius --r-xs · --surface-3 · 14px icon |
| severity | Plex Mono 600 · 13 · UPPER · P1/P2 danger (P1 stronger tint) · P3 warn · P4 info |
| in code | ui/status-badge.tsx (exhaustive over every wire status) · ui/badge.tsx · ui/tag-chip.tsx · ui/severity-badge.tsx |
Cards, tiles & metrics
Cards group related information on a raised surface. Tiles are cards that carry one number. The metric strip is the hero ribbon — the instrument's dial.
Storage workhorse
Compute node
RAM upgrade queued
2.4.1 KPI tiles
2.4.2 Metric strip
| card | --surface on --bg · 1px --border · radius --r-lg (14) · inset --s6 · no shadow |
| card title | Atkinson 600 · 16 · +0.01em |
| hover (interactive only) | border --border-strong + translateY(-2px) · d-quick · standard |
| kpi tile | eyebrow + 32px value (tabular) + one line of context or one link |
| metric strip | Plex Mono 500 30 · eyebrow · hairline separators · right-aligned · tone tints the value only, via ink |
| in code | ui/card.tsx · ui/metric-strip.tsx · ui/telemetry-tile.tsx |
Tables
Tables are how Aros shows the estate. Dense, scannable, monospaced where it counts — and since v1.1, selectable, exportable, and pinned.
| Resource | Host | Type | Status | Uptime | RAM | |
|---|---|---|---|---|---|---|
| pve1 · 2 items | ||||||
| aros-pg-dev-1 | pve1 | VM 231 | Live | 42d 6h | 16 GiB | |
| aros-minio-4 | pve1 | VM 274 | SPOF | 18d 2h | 8 GiB | |
| pve2 · 1 item | ||||||
| aros-pg-dev-2 | pve2 | VM 232 | Live | 42d 6h | 16 GiB | |
| header | eyebrow (Plex Mono 13 · +0.14em · UPPER · --text-3) · 1px --border-strong below · sticky when the table scrolls |
| cell padding | 12 × 14 (compact 8 × 12) |
| row divider | 1px --row-divider · hover --wash · d-fast |
| selection | 16px checkbox, accent p500 · selected row --p-tint · header checkbox is tri-state |
| bulk bar | appears only while rows are selected · --p-tint · count in mono · actions are secondary buttons · a blocked action carries its DisabledReason |
| group row | eyebrow on --wash · "key · n items" |
| pagination | Plex Mono 13 · "Page n of m · total" left · Previous/Next right |
| empty | one row, centred, --text-2, names the entity and the reason (§2.13) |
| in code | ui/data-table.tsx (+ selection, bulkActions, csv, views, groupBy, stickyHeader) |
Feedback
Telling the operator what happened. Severity maps to the semantic palette, every time — and every outcome is reported exactly once, on exactly one channel.
2.7.1 Toast
| Channel | Owns | Shape |
|---|---|---|
| alert | A condition you need to know about, in place | Persistent, inline. Tinted bg + 1px border in the semantic colour, icon in the ink. |
| toast | The outcome of a write whose surface is gone (dialog closed, row removed) | --surface-3 · 3px tone edge · eyebrow label in the ink · bottom-right of the content well · z-toast · success auto-dismisses at 6s; deferred and failed persist until dismissed |
| banner | The whole system is affected | Full-width alert at the top of the well |
| inline notice | A write whose surface survives (a form still to correct) | role="alert" beside the field; deferred is a neutral pill with the approval link |
role="alert".Overlays
Layers above the page: modals for decisions, side panels for routine forms, popovers for menus, tooltips for labels. Every one traps focus, closes on Esc, and hands focus back.
Right-click on a row, a card or an identifier opens the same menu the row's ⋯ button opens — same items, same order, same keycaps. Never an action that exists only on right-click.
| modal | --surface-2 · 1px --border-strong · radius --r-lg · shadow-lg · max 512px · scrim --scrim + blur · footer on --wash behind a divider · primary bottom-right |
| side panel | same surface, anchored right, 360–480px · for routine create/edit forms and detail drawers |
| popover / context menu | --surface-3 · 1px --border-strong · radius --r-md · shadow-md · items 14 · icon 16 · keycaps right · destructive last, after a divider, in --danger-ink |
| tooltip | --tip-bg (obsidian) · 13 · radius --r-sm · 300ms hover delay, no delay on focus · mono for identifiers · never a control inside |
| z | menus z-modal · tooltip z-toast (above everything, it is transient) |
| in code | ui/dialog.tsx · ui/dropdown-menu.tsx · ui/context-menu.tsx · ui/tooltip.tsx (Tooltip, DisabledReason) |
Esc and on scrim click, and returns focus to its trigger. The scrim uses z-modal with a darkened backdrop.aria-describedby, and holds text only. If it needs a button, it is a popover.Command palette
One box that finds anything and goes anywhere. It is the keyboard's front door — and since v1.1 it has a visible one in the top bar, so nobody has to be told about ⌘K.
| open | ⌘K / Ctrl+K anywhere · the top-bar front door · / is the page's local search, not the palette |
| groups | Knowledge (live full-text) · Go to (every rail + sub-nav destination, derived from the nav model, never a second list) · Actions (the view's verbs, plus global ones) |
| row | icon 16 · label · hint right in --text-3 · the highlighted row shows ↵ · ↑↓ move, ↵ activates, Tab reaches every row, Esc closes |
| surface | --surface-3 · 1px --border-strong · radius --r-lg · shadow-lg · 12vh from the top · scrim + blur |
| in code | layout/CommandPalette.tsx |
aria-modal, Tab contained, focus returned to the opener on close, and exclusive with every other modal surface — opening it closes the drawer, and vice versa.Keyboard & shortcuts
An instrument is operated, not browsed. The console is fully usable from the keyboard, and the fast paths are learnable because every one of them is written down — in a tooltip, a menu keycap, or the help sheet.
| keycap | Plex Mono 500 13 · 20px tall · radius 4 · 1px --border-strong on --wash-2 · chords as separate caps with a 4px gap |
| grammar | ⌘/Ctrl chords for global surfaces · single keys only outside editable fields · "g then x" sequences for navigation, 1s window · j/k for lists |
| discovery | every shortcut appears at least twice: in the control's tooltip or menu keycap, and on the ? help sheet |
| in code | ui/kbd.tsx · lib/shortcuts.ts |
Page header
The one way a view introduces itself. Eighteen routes once hand-rolled this with three different sizes; now there is one.
Hosts
The physical substrate — Proxmox VE nodes and BMC-backed bare-metal. PVE health is corosync-authoritative.
| eyebrow | optional · the section, a tenant, a status word |
| title | h1 · Atkinson 600 · 24 (dense) / 32 · -0.01em |
| description | 14 (dense) / 16 · --text-2 · max 60ch · one or two sentences that say what the view is for |
| actions · meta | right, on the description's baseline · the view's ONE primary button · a metric strip or refresh cadence |
| in code | ui/page-header.tsx |
Code & terminal
Aros shows commands and output verbatim. Monospace, syntax-tinted, copyable — and on obsidian in both themes.
patroni: dcs: raft # DCS-agnostic toggle nodes: 3 # one per physical host
--surface-code). Never paraphrase a command into prose.| timestamps | relative for scanning ("4m ago"), absolute for the record (2026-09-02 · 14:03:07), always with the UTC instant one hover away and a <time datetime> in the DOM; past a week the relative form becomes the date |
| identifiers | Plex Mono · click copies · "Copy" → "Copied" · a truncated value keeps its full form in the tooltip |
| in code | ui/when.tsx + lib/time.ts · ui/copyable.tsx |
Empty · loading · error
The states most products forget. Aros treats them as first-class, because an operator sees them at the worst moments.
Discovery hasn't found anything on this host.
Shimmer, not a spinner. Resolves into data, empty or error — never hangs.
Connection timed out after 30s.
Retry is offered only when retrying can help — never on a 403.
| empty | 40px glyph disc on --surface-2 · headline 14/600 that names the ENTITY · one sentence that names the REASON · one next action (or a disabled one with its reason) |
| skeleton | --surface-3 blocks · --wash-2 sweep · 1400ms linear · static under reduced-motion · table rows keep the header visible |
| error | danger tint + 1px danger/30 · icon in --danger-ink · what happened · Retry when it can help |
| partial | when some reads answered and some did not, say so above the data: "2 of 5 reads did not answer — this page is incomplete, not quiet" |
| in code | ui/empty.tsx · ui/loading.tsx · ui/error-panel.tsx · ui/query-boundary.tsx · ui/unavailable-reads-notice.tsx |
2.13.1 Promised surfaces — the four tiers of an honest skeleton
A page may be drawn before every part of it works, so the estate can be restructured for the long term without removing what works today. The rule that keeps it honest: a skeleton always says which tier it is in, and the first two tiers link the issue that brings them to life.
Every pending update across the estate, in one train.
PBS job inventory, missed-backup findings, trigger and restore.
Proxmox connector unreachable.
| frontend | designed, data exists, not yet built · info ink · dashed frame · links the frontend issue |
| backend | designed, the backend it needs is not there · warn ink · dashed frame · links the backend issue |
| failing | everything exists and this read is failing now · danger ink + the message · Retry when it can help |
| loading | the ordinary shimmer — no badge, no frame; must resolve |
| silhouette | the designed layout at 50% opacity, aria-hidden and inert — skeleton blocks or real components with empty props, never sample values |
| in code | ui/promised.tsx (Promised, PromisedSilhouette) · lib/forgejo.ts |
Avatars & progress
2.14.1 Progress & meters
Next: Patterns
Components are the vocabulary. Volume III — Patterns → is the grammar: how they compose into the shell, the dashboard, and the flows.