Files
openclaw/ui/docs/design-system/settings-design.md
Peter Steinberger 0a8226c3db fix(ui): give embedded settings sections the shared section rhythm (#126541)
* fix(ui): give embedded settings sections the shared section rhythm

The agents tab panel hosted settings sections in a bare div, so sections
stacked with zero separation; the bespoke .agents-main margin rule in
agents.css missed the nested tabpanel entirely. Descriptions also pulled
up to 4px under control-height header actions (squeezed Verify/Save rows).

Add a .settings-stack primitive to settings.css for embedded surfaces,
use it on the agent tab panel, delete the page-local margin fork, and let
section descriptions clear action-bearing headers.

* test(agents): export getRuntimeConfigSourceSnapshot from runtime-snapshot mock

Main's checks-node-compact-large shard is red: #126531 routed
provider-model-routes through projectConfigOntoRuntimeSourceSnapshot,
which reads getRuntimeConfigSourceSnapshot, and this suite's explicit
vi.mock factory did not export it (24 failures). Return null so the
projection no-ops and resolvers keep reading the provided config.
2026-08-19 22:31:35 -07:00

3.6 KiB

Settings Design Language

Every settings surface (the /settings takeover pages plus the Plugins/Skills hubs) uses one structural pattern. Workspace styles live in ui/src/styles/settings.css, while controls shared with startup surfaces live in ui/src/styles/settings-controls.css; templates are built through the helpers in ui/src/components/settings-ui.ts.

Anatomy

.settings-page                     ← single column, 760px (or --wide: 1120px)
  .settings-section                ← repeated per topic
    .settings-section__heading     ← plain uppercase text label, outside any surface
    .settings-group                ← the ONLY surface (card bg, border, radius-lg)
      .settings-row                ← title/description left, one control right
      .settings-row                ← hairline divider between rows
  • Sections are typography, not chrome. Grouping comes from whitespace + a small uppercase heading — never a card header.
  • Embedded surfaces keep the rhythm. A container that hosts sections without .settings-page's centered column (tab panels, split layouts) takes .settings-stack; never re-space sections with page-local margin rules.
  • Exactly one level of elevation. A group never contains another card, callout, or bordered box. Nested detail uses .settings-subrows (indented rows), a stacked row, or a drill-in nav row.
  • Row anatomy: left is title (--control-ui-text-md, weight 500) over an optional one-line description (muted, sm). Right is exactly one control: toggle, select, segmented, button, plain value, or chevron (nav). Wide editors use the stacked variant.
  • Lists are rows too. An entity list (plugin, device, session) is a group whose rows carry an action cluster in the control slot — same anatomy as a toggle row.

Rules

  • No status pills. Status is renderSettingsStatus — a dot + plain text (● Connected). Badges (.settings-count) exist only for genuine counts.
  • Spacing uses --space-* tokens (base.css); no hardcoded paddings/gaps.
  • Motion budget: color/background transitions only. No enter animations, staggered reveals, or hover glows.
  • Buttons: default .btn (quiet). --accent primary at most once per view. Danger actions live in a danger: true section at the page bottom.
  • One control set. Use renderSettingsToggleRow (preferred: label-wrapped, whole row clickable, accessible name for free) or renderSettingsToggle with a required ariaLabel, renderSettingsSegmented, .settings-select, .settings-input. Do not add another toggle or badge variant.
  • Every control needs an accessible name. Row titles are plain text, not <label>s — selects/inputs in a control slot must carry aria-label (usually the row title string).
  • No new page CSS files for settings surfaces. Page-specific styles belong in settings.css only when a primitive is genuinely missing — extend the system, don't fork it.

Helpers

import {
  renderSettingsPage,
  renderSettingsSection,
  renderSettingsRow,
  renderSettingsNavRow,
  renderSettingsToggleRow,
  renderSettingsSegmented,
  renderSettingsStatus,
  renderSettingsValue,
  renderSettingsEmpty,
} from "../../components/settings-ui.ts";

renderSettingsPage([
  renderSettingsSection({ title: t("settings.notifications") }, [
    renderSettingsToggleRow({
      title: t("settings.systemNotifications"),
      description: t("settings.systemNotificationsDesc"),
      checked,
      onChange,
    }),
  ]),
]);

Custom content inside a group (tables, meters) is allowed as an escape hatch — keep it inside one .settings-group and match row paddings (--space-3 --space-4).