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.lessper 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
!importantby scoping the rule to chrome only — rejected as riskier/larger than dropping!importantand 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
!importantto 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.lessfiles and, if a new variable is needed, thestatic/variables/ui-variables.lessfallback 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).