Docs

ADR-0013: Business themes export ui-variables

Status: Active · Date: 2026-07-21

Context

Owned package stylesheets (e.g. the vertical tab list) need theme-correct colours on both tranquil-business-dark and -light. The idiomatic Atom/Pulsar way is @import "ui-variables" and use @text-color, @base-background-color, etc. — Pulsar resolves that import to the active UI theme’s styles/ui-variables.less (ThemeManager.getImportPaths() puts each active theme’s styles/ dir on the LESS path).

The business themes are UI themes but shipped no ui-variables.less. So @import "ui-variables" fell through to the bundled light fallback (static/variables/ui-variables.less) — @text-color resolved to #333, @base-background-color to #fff, etc., rendering invisibly on dark. Packages worked around it with color: inherit + hardcoded rgba(127,127,127,…) overlays. Separately, base.less set button { background: @surface-active !important } under the theme scope, so every package button had to !important its own background to escape the forced fill.

Decision

Ship styles/ui-variables.less in both business themes, mapping the full standard variable contract (tranquil-client/static/variables/ui-variables.less) onto each theme’s colors.less tokens — e.g. @text-color → @text-primary, @tool-panel-background-color → @sidebar-bg, @button-background-color → @surface-active, plus sizes (@component-border-radius, …). The file is byte-identical in dark and light; each imports its own colors.less, so the shared mapping yields per-theme values (the “only colors.less differs” rule holds).

Convention: package stylesheets consume ui-variables; theme code (the partials, mockups) uses colors.less tokens directly — the token→variable mapping lives in exactly one place.

Remove the global button !important. Theme sheets already win over packages at equal specificity (priority), so base.less keeps the button background without !important — a package button now overrides with normal class specificity instead of an !important war. Redundant !important were also trimmed from settings.less (kept only where core settings-view is genuinely more specific — the fragile control rendering: mini-editor opacity/border, checkbox/radio appearance + pseudo-suppression, .btn, selects).

Options considered

  • Export ui-variables.less per theme, mapped to tokens (chosen) — the standard Atom mechanism; packages get correct colours for free on both themes, and core settings-view’s own ui-variables-based styling now resolves to business values instead of the light fallback. One byte-identical file per theme.
  • Keep hand-rolling inherit/rgba() in each package — rejected: every package re-derives colours, nothing is theme-aware, and it doesn’t fix core settings-view rendering on dark.
  • Route theme code through ui-variables too — rejected: theme code is the source being mapped from; that’s a pointless indirection back to its own tokens.
  • Remove the button !important by scoping the rule to chrome only — rejected as riskier/larger than dropping !important and letting priority + specificity do their job.

Consequences

  • A real UI platform. Any owned package can @import "ui-variables" and get theme-correct colours; see the widget guide’s “Theme colours in package code” section.
  • Package buttons are free. No more !important to escape the theme’s button fill — a class-scoped selector suffices (the vertical-tabs close button and tree-view row actions were de-!importanted).
  • Better settings on dark. Where the business theme didn’t hand-override core settings-view, its ui-variables now resolve to business colours instead of light-on-dark.
  • Contract to maintain. New mappings go in both theme ui-variables.less files and, if a new variable is needed, the static/variables/ui-variables.less fallback too, so non-business themes still resolve it.
  • Not verified in-app yet. Authored against the LESS/specificity model and compile-checked; a visual pass on both themes is pending (see the delivery notes).