Docs

ADR-0017: One normalized form layout for the Tranquil settings tab

Status: Active · Date: 2026-07-24

References

Context

ADR-0011 gave Tranquil a real settings tab by reusing core settings-view’s SettingsPanel against a tranquil.* config namespace. That renders controls, but core emits different markup per control type: for a mini-editor (e.g. Toast Duration) the .control-label is nested inside .controls; for an enum <select> the label is a sibling above .controls; the .setting-description sometimes lives inside the label. The result was fields that didn’t line up — one setting’s label sat left of its input, the next sat above it — and the misalignment flipped depending on which control type each field used. Patching individual fields kept moving the problem around.

Two more edges surfaced while adding an in-tab UI-theme picker (so users can switch light/dark without hunting through the Themes panel):

  • A native macOS <select> (appearance: auto) draws its visible control inset within the CSS box, ignoring our border-radius, so its right edge fell short of the sibling text inputs even at an identical 22em width. (Confirmed by reading computed styles off the live renderer — the box rects were pixel-identical; only the native rendering differed.)
  • The picker must not modify settings-view (third-party-package rule) and must survive theme changes made elsewhere.

We want the tab to read as one coherent form, and — since settings will keep being added — we want new settings to inherit the layout without per-field CSS.

Decision

Normalize every setting to one stacked layout in the panel factory, style that single shape in the business themes, and surface a self-contained UI-theme picker at the top of the tab.

  • DOM normalization (tranquil-config createTranquilSettingsPanel). After SettingsPanel renders, walk every .control-group and hoist its .control-label to be the first child and keep the .setting-description tucked inside the label (directly under the title). Whatever per-type markup core produced, each group ends up as label (title + description) → controls → (buttons).
  • One layout in the theme (settings.less, both business themes). Scope a .settings-view .tranquil-settings block that lays each .control-group out as a flex column with explicit order values, so the vertical order is fixed regardless of source markup. Every text/number/select field shares one 22em, box-sizing: border-box box, left-aligned in a full-width .controls row, so all fields line up on both edges; trailing buttons (e.g. “Test”) follow in the same row. Heading is 15px/600 @text-primary; the description reads as a 12px @text-muted hint directly beneath it.
  • Flat selects. Force appearance: none on .settings-view select.form-control so the box renders flat and fills its width (edges match the inputs), and draw our own chevron as a @text-secondary SVG data-URI (URL-encoding #%23, mirroring the existing checkbox mark), with right padding to clear it. border-box keeps the extra padding from shifting the right edge.
  • In-tab UI-theme picker (self-contained). Build a <select> in the factory that reads atom.themes.getLoadedThemes() (filtered to Tranquil’s own tranquil-business-* UI themes) and, on change, writes core.themes = [uiTheme, syntaxTheme] — preserving the active syntax theme exactly as settings-view’s own Themes panel does. It repopulates on atom.themes.onDidChangeActiveThemes (so a switch made elsewhere is reflected) and disposes that subscription on panel destroy(). It touches no settings-view source.

Options considered

  • Normalize DOM + one themed layout (chosen) — the layout lives in exactly two places (a factory loop + one theme block); new settings inherit it automatically. Cost: we depend on core’s class names (.control-group/.control-label/.controls/.setting-description), which we already do for theming.
  • Per-field CSS patches — rejected: what actually happened first, and it whack-a-moled because each control type needs a different patch and future fields would need new ones.
  • Fork/replace SettingsPanel with a bespoke renderer — rejected: violates the reuse win of ADR-0011 (free theming, config-bound controls) and is far more surface to maintain than a normalize pass.
  • Keep appearance: auto and nudge the select width to fake alignment — rejected: the native inset isn’t a fixed number and isn’t controllable; appearance: none + our own chevron is the real fix and makes every settings select render consistently.
  • Link to settings-view’s Themes panel instead of an in-tab picker — rejected: the point is to switch theme where users already are; the picker is a few lines and reads/writes the same core.themes pair the Themes panel does.

Consequences

  • New settings line up for free. Any future tranquil.* control (input, select, checkbox-adjacent, with or without a trailing button) drops into the tab already aligned — no bespoke CSS.
  • All settings selects render flat and consistent, not just the theme picker, because the appearance: none + chevron fix lives on the shared select.form-control rule.
  • We lean on core’s settings-view class names. If a future settings-view rewrite renamed .control-group/.controls, the normalize pass and theme block would need updating — a known, small blast radius, and the same coupling theming already carries.
  • The picker is scoped to Tranquil’s own themes on purpose. It lists only tranquil-business-*; installing a third-party UI theme won’t appear here (that’s still the full Themes panel’s job).
  • Layout changes are theme edits — the two business settings.less files must stay in sync (light differs only by tokens), the standing rule for browser/settings surfaces.
  • Re-evaluate if we ever need a genuinely two-column setting (label beside control) or grouped sub-sections — the current decision commits the whole tab to a single stacked column.