ouroboros/docs/DESIGN.md
Ouroboros b5187ffb8d Merge branch 'sprint/agents-settings' into sprint/synthesis
# Conflicts:
#	docs/DEVELOPMENT.md
#	web/modules/claudexor_status_store.js
#	web/modules/harness_accounts.js
#	web/modules/reviewer_slots.js
#	web/tests/claudexor_status_store.test.js
#	web/tests/harness_accounts.test.js
#	web/tests/reviewer_slots.test.js
#	web/tests/subagents_settings.test.js

Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
2026-08-09 06:14:40 +03:00

10 KiB

Ouroboros Design System

Normative authority for the visual and interaction semantics of the Ouroboros UI: what a size means, what a colour claims, what a chip is allowed to say.

Authority split:

  • This file decides semantics — type scale, hierarchy, foreground and state colour, status conventions, card/row anatomy, density.
  • docs/DEVELOPMENT.md## Design System decides the engineering rules that keep those semantics intact — where values may live, which component is the SSOT, what counts as review debt, how a visual change is verified.

Values themselves live in web/style.css :root (and are mirrored by value in web/onboarding.css, which is inlined standalone and cannot import it). This file names roles; it does not copy an inventory.

The theme is dark only. There is no light-theme plumbing, and adding a second theme is an architecture change, not a styling change.


1. Type scale

Four sizes. There is no fifth.

Token Value Role
--type-meta 12px Labels, notes, chips, timing/quota lines, captions
--type-body 14px Default reading text, values, controls, row titles
--type-section 16px Section and card titles
--type-page 24px Page / wizard step title, and display text (a login code)

Line heights: --line-meta (1.35) for short meta lines, --line-body (1.5) for prose, --line-title (1.3) for headings.

Rules:

  • A value with no exact token rounds to the nearest token. It never mints a fifth size. 13px becomes --type-body, 15px becomes --type-section, 11px becomes --type-meta.
  • No new raw 10px / 11px text on a migrated surface. Below 12px the dark theme forces a choice between illegible and glaring, and the glaring option is what produced the owner's "too much small high-contrast white text" report. tests/test_web_typography_static.py enforces this.
  • Control chrome keeps its own dimension tokens (--button-font-size, --pill-font-size). They are control geometry, not the reading scale; do not replace them with type tokens or vice versa.

2. Hierarchy rule

In any row, card or field, exactly one thing is primary. Everything else steps down. Concretely:

  • Label--type-meta in --text-meta, sentence case.
  • Value (the thing the owner came to read or change) → --type-body in --text-primary.
  • Meta (last run, quota, effort, timing, provenance) → --type-meta in --text-meta.
  • Section title--type-section semibold; subsection heading--type-body semibold. A bare <h3>/<h4>/<strong> that inherits the browser default is a defect: it lands at bold 16px full white and ties with, or beats, the content it introduces.

The 12px UPPERCASE label pattern is retired on migrated surfaces. All-caps at a small size costs legibility, widens every label, and when a panel repeats it dozens of times the labels collectively out-shout the values they describe. Authored label strings should read as sentence case; CSS must not manufacture caps with text-transform.

3. Foreground and state colour

Token Meaning
--text-primary The one thing this row/card is about; interactive control labels
--text-meta Real secondary content: labels, notes, hints, meta lines
--text-disabled Genuinely inert or incidental content only

--text-muted is a legacy alias of --text-disabled; new work names --text-meta or --text-disabled so the intent is readable in the diff.

Two failure modes this table exists to prevent, both observed in this codebase:

  • Secondary content parked at the disabled foreground. A load-bearing caveat rendered at --text-disabled reads as greyed-out chrome and gets skipped. If the owner is meant to read it, it is --text-meta.
  • Secondary content with no foreground at all, inheriting --text-primary. This is the loudest of all failures because it is invisible in the CSS — the rule simply declares a size and says nothing about colour.

.muted

.muted is a colour-only utility: color: var(--text-meta), nothing else. It must never set font-size. It is written at ~50 call sites that already sized themselves, and a size here would silently resize all of them. A scoped rule (.some-context .muted) still wins on specificity where a surface needs a local variant.

Dark-theme contrast

WCAG 4.5:1 is a floor, not a target. On near-black, pushing small text toward pure white causes halation — the glyphs bloom, and because everything is maximally bright, nothing is emphasised. The result is a screen that is simultaneously harder to read and flatter in hierarchy.

De-emphasise the secondary rather than amplifying the primary. When something needs to stand out, drop the ink around it, do not raise its own. All Primary (15.9:1) and meta (9.2:1) clear the 4.5:1 floor against --bg-primary with room to spare. --text-disabled is deliberately BELOW it (3.5:1) and is therefore reserved for genuinely disabled or incidental content, which WCAG exempts; it must never carry meaning a reader has to obtain.

4. Status and chips

A status has an explicit foreground/background pair, never a foreground derived from whatever generic opacity happens to sit on the element.

Role Foreground Background
Success / connected --status-ok-fg --status-ok-bg
Warning / degraded --status-warn-fg --status-warn-bg
Error / failed --status-error-fg --status-error-bg
Neutral / classification --status-neutral-fg --status-neutral-bg
  • Status renders as dot + text. The dot carries the state at a glance, so the sentence does not have to shout it in saturated colour and can sit at ordinary reading contrast.
  • Neutral is a real state, not an absence of one. A classification chip (which agent, which family) is neutral: it is a tag, not an alarm. A tone value the code actually emits (muted) must have a rule; falling through to a default is how chips end up white.
  • Chips are --type-meta, not smaller, and are not uppercased.
  • --tone-ok / --tone-warn / --tone-danger remain the saturated role hues for large fills, borders and indicators. The --status-*-fg tints are for 12px text on near-black; do not swap them.

5. Card and section composition

  • A panel is one .ui-card-family surface: --ui-card-border, --ui-card-bg, --radius. Nested emphasis uses --ui-card-bg-soft, not a second border weight.
  • A section is: title (--type-section) → optional one-paragraph description (--type-body, --text-meta) → content → optional note (--type-meta, --text-meta). The description explains what the section decides; the note carries consequences and caveats.
  • Subsections inside a section use a --type-body semibold heading and stay visually grouped with their own rows and their own action toolbar. A heading that floats equidistant between two groups belongs to neither.
  • Spacing comes from the 8pt tokens (--space-*); a new visual dimension becomes a CSS variable before it becomes a page-local literal.

6. Account group / row anatomy

For a repeated identity row (a connected agent account, a reviewer slot, a server entry):

  1. Classification chip — neutral pair, --type-meta. Only where the row's family is not already expressed by the group it sits in; inside a per-family card the chip repeats the header and is dropped.
  2. Name--type-body semibold, --text-primary. The one primary thing.
  3. Identity detail (email, plan) — --type-meta, --text-meta.
  4. Status — dot + text from the status pairs.
  5. Meta line--type-meta, --text-meta, on its own line under the name. Quantities are stated in human words ("38% used · resets in 2h"), and an instant is humanized. A row never leads with a raw ISO timestamp.
  6. Actions — docked right, legible at rest. A control rendered at secondary ink reads as disabled; if the owner can click it, it is --text-primary.

Rows of the same kind are equivalent: no row gets extra visual weight for being first, default, or native. Grouping and section-level actions express which family a row belongs to, and a section-level action (add, connect) belongs in its group's header rather than attached to one privileged row.

A degraded row is emphasised, not dimmed. Lowering a whole row's opacity greys out the sentence that reports the problem and makes its still-clickable controls read as disabled. Tint the row with the matching --status-*-bg instead, and let the status text carry the claim.

7. Onboarding density

The first-run wizard is a compact flow that must not scroll at the default desktop window size merely because a step has several fields.

  • Step title --type-page; card titles --type-section; field labels and notes --type-meta. No display size above --type-page.
  • Short-viewport adaptation hides explanatory copy rather than shrinking type. Once copy is hidden, shaving pixels off a title buys nothing and costs the scale.
  • Field labels are sentence case at meta ink — a wizard step shows a dozen at once, and its job is to get one value typed, not to present a grid of headings.

8. Migration state

The scale is applied surface by surface. Migrated today:

  • web/settings.css (settings shell, model/effort cards, MCP cards)
  • web/onboarding.css (the whole first-run wizard)
  • web/style.css between the design-system:migrated-begin and design-system:migrated-end markers — harness accounts and reviewer slots
  • the global .muted, .form-section h3 and shared .ui-status tone rules

Not yet migrated: chat, skills, marketplace, widgets, logs, evolution. They are historical and keep their literals until their own pass. Do not part-migrate a surface: a half-tokenised stylesheet is harder to reason about than an untouched one.

tests/test_web_typography_static.py guards the migrated set only. Extending the guard to a new surface and migrating that surface are the same commit.