resources/ui_framework.md
A semantic styling framework: every color, size, and state is defined by what a thing is, what it does, and its current state — never by a raw literal at the call site. This indirection lets us re-theme the app by editing the token values here without touching the semantics that reference them.
The framework has two axes:
Every color is given for both the Dark and Light themes.
A surface is a background role. Pick the surface by the region's purpose, then read its color from the theme in use.
| # | Surface | Dark | Light |
|---|---|---|---|
| 1 | Banner | #1C1C22 | #D6D6D6 |
| 2 | Backdrop | #373743 | #EBEBEB |
| 3 | Data Backdrop | #5C5C70 | #FFFFFF |
| 4 | Input | #535365 | #F5F5F5 |
| 5 | Separation | #D2D2DA | #ADADAD |
| 6 | Banner Input | #25252D | #E0E0E0 |
| 7 | Scroll Handle | #007AF5 | #47A3FF |
A horizontal or vertical bar that visually separates the content that follows. It usually holds a title on the leading side and a workspace on the trailing side.
A banner has a uniform height (if horizontal) or width (if vertical). A horizontal banner is usually followed by a separator below it when at the top of the window, and by separators above and below when in the middle.
#1C1C22#D6D6D6Examples
"Datasets [Filter] Kebab", "Custom Series [Filter]""Sequences [Filter]", "Topics [Filter]"The default window background color. It is the starting point for all windows, frames, popups, and dialogs.
#373743#EBEBEBThe background of any widget, label, or frame that contains interactable, editable data — charts, tree views, spin boxes, scrubbers. It is intentionally lighter than the Backdrop so text and values read more easily.
#5C5C70#FFFFFFExamples
Any editable widget with text entry — mostly line edits and text edits, including filter searches.
#535365#F5F5F5A line or gap that acts as a buffer or border between objects. Used mostly by the draggable Separator widget, which turns a purple color when grabbed.
#D2D2DA#ADADADThe special case of a line edit or text edit that lives inside a Banner.
#25252D#E0E0E0Anything that can be grabbed to move a page forward or backward — excluding special sliders such as the timeline and playback controls.
#007AF5#47A3FFInteractable frames and buttons (elevated surfaces) split into these variants. Each variant's per-state colors are defined in § Interactions.
Named by visual emphasis, not by semantic state (we rarely mean a literal error/warning):
An interactive element that is not disabled walks through this state ramp:
Crossing the 5 surface variants with these 7 states yields a color grid per theme.
| Variant | Nominal | Hovered | Checked | Pressed | Checked Hovered | Checked Pressed | Disabled |
|---|---|---|---|---|---|---|---|
| Neutral | #8F8FA3 | #787891 | #5C5C70 | #5C5C70 | #40404F | #2E2E38 | #25252D |
| Accent | #1F8FFF | #007AF5 | #0066CC | #0066CC | #00478F | #003366 | #25252D |
| Highlight | #FF33A7 | #FF0A95 | #CC0074 | #CC0074 | #8F0051 | #66003A | #25252D |
| Emphasis | #F5D33D | #F3CA16 | #C2A00A | #C2A00A | #887007 | #615005 | #25252D |
| Variant | Nominal | Hovered | Checked | Pressed | Checked Hovered | Checked Pressed | Disabled |
|---|---|---|---|---|---|---|---|
| Neutral | #E0E0E0 | #CCCCCC | #B8B8B8 | #B8B8B8 | #A3A3A3 | #999999 | #333333 |
| Accent | #D6EBFF | #C2E0FF | #99CCFF | #99CCFF | #70B8FF | #5CADFF | #333333 |
| Highlight | #FFD6FF | #FFC2FF | #FF99FF | #FF99FF | #FF70FF | #FF5CFF | #333333 |
| Emphasis | #F5D33D | #F3CA16 | #C2A00A | #C2A00A | #887007 | #615005 | #333333 |
The Special variant is not a fixed color: for each state it is a gradient that runs from the Accent value to the Highlight value of that same state (in the active theme).
| State | Gradient |
|---|---|
| Nominal | Accent Nominal → Highlight Nominal |
| Hovered | Accent Hovered → Highlight Hovered |
| Checked | Accent Checked → Highlight Checked |
| Pressed | Accent Pressed → Highlight Pressed |
| Checked Hovered | Accent Checked Hovered → Highlight Checked Hovered |
| Checked Pressed | Accent Checked Pressed → Highlight Checked Pressed |
| Disabled | Accent Disabled → Highlight Disabled |
Content ink for text and icons. A caller asks for ink relative to the fill it sits on — never a raw literal:
onSurface(surface, emphasis) — ink on a background surface; emphasis ∈
{Default, Muted, Disabled}.onFill(variant, state) — ink on a solid interaction fill. The legible ink
flips by state (WCAG): e.g. dark Highlight Nominal #FF33A7 needs dark ink
but dark Highlight Checked #CC0074 needs light ink, so ink is chosen per cell,
not per variant.onSpecial(state) — ink over the Special (Accent→Highlight) gradient.text is a compatibility alias for onSurface(Backdrop, Default) — the primary
body-text ink:
#FFFFFF#333333On-fill ink is near-black #111111 / near-white #F0F0F0 chosen per cell for
≥4.5:1 contrast; a few pre-migration mid-grey fills fall back to absolute
#000/#FFF and are flagged for retuning in
ui_framework_roadmap.md.
Icons carry their own icon ink, with a nominal and a disabled state. Nominal icons match the body-text ink; disabled icons drop to a muted mid-grey.
#FFFFFF#333333#8F8FA3#A3A3A3Stroke ink for borders, dividers, gridlines, focus rings, and checked
outlines — distinct from Surface::Separation, which is a background fill.
Accessor: outline(role, state) with roles {Default, Interactive, Divider,
Gridline} and states {Rest, Hovered, Focused, Checked, Disabled}. Rest reuses the
old border_default, Hovered reuses border_hover, and Focus is a distinct
ring (#1177FF) rather than being collapsed into hover. Focus is drawn as a
ring over the nominal fill, so State::Focused on the interaction grid resolves
to Nominal.
Overlays are alpha-bearing (overlay(kind)): Hover / Pressed / Selected
state layers (theme-inverted: white-alpha on dark, black-alpha on light), plus
Scrim (modal dim) and Hud (scene panel veil). Selection is a fill+ink pair
(selectionFill/onSelection), distinct from interaction Checked: Item for
opaque view/list/tab selection and TextEdit for the translucent editor
selection.
A parallel ElevatedSurface tier for raised/floating shades that aren't among the
base seven: Dialog (dialog/menu/popup body), Card + CardHover, Toast, and
Disabled. Gradients are named two-stop endpoint pairs (gradient(role) →
{start, end}): Accent (light-purple→light-blue) and Brand
(blue→purple); QSS composes qlineargradient(... stop:0 ${gradient_accent_start}, stop:1 ${gradient_accent_end}).
Geometry, typography, and motion are semantic roles too (theme-agnostic; stored in
the same palette). Accessors: space() (None/Tight/Snug/Comfortable/Section =
0/2/4/8/16), metric() (the chrome/input heights — promoting the previously-dead
chrome_* tokens to role names), radius() (Square/Input/Card/Pill), stroke()
(Hairline/Emphasis), duration() (Fast/Base/Slow/RevealHold = 140/180/300/1200
ms), and type() → {size, weight, family} for Caption/Body/Title/Heading.
Family is a sans-serif placeholder until a specific app face is chosen.
Planned framework extensions and the full list of missing token types live in
ui_framework_roadmap.md.