# The source of truth for the UI's colour and length tokens.
#
# `build.rs` reads this file and generates `theme.slint` into OUT_DIR at
# compile time; nothing here is read at runtime unless the `live-style`
# feature is on. Edit this file, never the generated one.
#
# Leaf values only. A token that binds several values into one concept — a
# panel heading's colour *and* size *and* weight — is a Slint component, not
# a row in a YAML file (see widgets.slint).
#
# Structure:
# preamble — prose emitted at the top of the generated file
# colors: — name -> "#RRGGBB", or `alias:` naming another colour token
# lengths: — name -> pixels
# Any token may carry `note:` (a `//` comment above it) or `doc:` (a `///`
# doc comment, which Slint surfaces to editors).
#
# Two entries are dividers rather than tokens, because YAML discards the blank
# lines between map entries and the generated file's grouping has to survive:
# `section:
` — a headed run, optionally with its own `note:`
# `break: true` — a bare blank line between related tokens
preamble: |
Near-neutral dark palette, achromatic signalling.
Two commitments, both about not lying to the photographer.
**Dark ground.** A light UI surrounding an image biases how that image is
judged — the eye adapts to the brightest thing in view, and a white panel
makes a correctly-exposed photograph look dark.
**Near-neutral greys.** The earlier palette was a warm "darkroom safelight"
brown (ground #14120F, R twelve points above B). That is worse than it
sounds: simultaneous contrast pushes perception of the image *away* from the
surround, so a warm chrome makes a neutral photograph read cool, and the
photographer corrects toward warm to compensate. Every export drifts yellow.
It is the same reason a print viewing booth is neutral grey rather than
whatever colour the room happens to be.
These greys carry a 2–3 point blue lift rather than being flatly achromatic.
Pure R=G=B reads as dead to most eyes; a trace of cool reads as instrument
rather than absence, and biases far less than warmth because the eye is more
tolerant of a cool surround. The lift is small enough not to matter
perceptually and deliberate enough not to be mistaken for drift.
**No hue in the chrome.** Active and modified states are signalled by
brightness alone. An accent sitting beside the image competes with it for
attention and shifts the perception of nearby colours; a photo editor cannot
afford either. The one exception is `warn-ink` — a caution is genuinely a
different kind of thing from an active state, and hue is the fastest way to
say so.
colors:
ground: "#121314"
surface: "#1B1C1E"
surface-raised: "#252629"
rule: "#323438"
_inks: { break: true }
ink: "#EDEEF0"
ink-dim: "#9EA1A6"
ink-faint: "#71747A"
hover:
value: "#2E3034"
note: |
Interactive states for surfaces. Named rather than written inline so a
button, a section header and a list row cannot drift apart: hover lifts
toward the light, press sinks back past the resting surface so the
control reads as depressed rather than merely lit.
pressed: "#17181A"
_signalling:
section: signalling
note: |
Achromatic, so the separation has to come from luminance. These are
spaced further apart than a coloured palette would need: with hue
unavailable, a two-step brightness difference is invisible, and a
modified marker that cannot be spotted at a glance is not a marker.
active:
value: "#FFFFFF"
doc: |
An active or engaged control: a slider fill, a curve line, a checked
box. Brighter than `ink` so it reads as lit rather than merely present.
active-dim:
value: "#C6C9CE"
doc: Active, at rest — a filled control that is not under the pointer.
active-pressed:
value: "#8E9298"
doc: Active, pressed. Sinks rather than lifts, matching `pressed`.
modified:
alias: active
note: |
"This differs from its default." Deliberately the brightest thing in the
chrome: it is the one piece of state the photographer scans for, and
with no hue to carry it, brightness is all there is.
selected:
value: "#383B40"
doc: |
A selected grid cell. A lifted neutral rather than a tint — distinct
from `hover` because a multi-selection must stay legible after the
pointer has moved on, which is the whole point of selecting several
before dragging them.
selected-ring:
value: "#D5D8DD"
doc: |
The ring around a selected cell. Brighter than the fill so selection
survives against a pale thumbnail, where the fill alone would vanish.
warn-ink:
value: "#C9A05A"
note: |
Semantic, and the only hue in the palette. A caution is not an active
state, and it is worth the one exception to say that instantly. Muted
rather than saturated so it does not shift perception of a nearby image.
lengths:
_gaps: { break: true }
gap-sm: 6
gap: 12
gap-lg: 20
_text: { break: true }
text-sm: 11
text: 13
text-lg: 17
text-xl: 24
radius-sm:
value: 3
note: |
Corner radii. Two steps only: `radius-sm` for things that sit inside
other things, `radius` for the controls themselves.
radius: 4
touch-target:
value: 44
note: "FR-UI-3: minimum 44pt hit target under touch."
row-height:
value: 26
note: |
One row of the collections tree, and one level of nesting. Both are
tokens because a tree's indentation has to stay proportional to its row
height; hard-coding either makes the hierarchy read wrong when the other
changes.
indent: 14
control-height:
value: 28
note: |
The drawn height of a button or section header. Deliberately shorter
than `touch-target` — chrome this tall in every row would crowd the
photograph — so controls grow their TouchArea past their own bounds to
meet FR-UI-3 rather than growing their ink.
control-min-width:
value: 88
doc: |
Floor on button width, so a one-word label is still a comfortable
target and a row of buttons has an even rhythm.