Raxol. UI. Harness. Prominence
(Raxol v2.6.1)
View Source
Maps a continuous prominence (0.0..1.0) attribute onto a resolved hex
color through the Raxol.UI.Theming.Salience H-K solver.
Ground-aware fade
Fading a foreground color toward a hardcoded reference ground (rather
than the terminal's actual background) gets the direction wrong on a
light theme: fading toward a dark reference moves a dark foreground
away from a light background, so contrast rises as prominence drops
instead of falling. resolve/3 (and fade/3) take the real ground as
an argument, threaded to Salience.solve_lightness/3 the same way
Salience.solve/4 already accepts :ground -- the formula
faded_apparent = ground + (apparent - ground) * prominence interpolates
APPARENT lightness toward the ground point, which is direction-correct on
either side (dark ground: fades down toward it; light ground: fades up
toward it) as long as ground is the real one, not a hardcoded reference.
Two modes -- pure fade (default) vs floored (opt-in)
This is the load-bearing design decision of this module. The salience gradient is the point: context text is meant to recede as its prominence drops, and "faded but not lost" means recoverable when a caller later raises the block back to prominence 1.0 (e.g. on focus), NOT readable-at-a-glance. A universal legibility floor would fight that gradient -- flattening the very tier separation the gradient exists to create. So:
Pure fade (default).
resolve(hex, prominence)andresolve(hex, prominence, ground: g)apply only the ground-aware fade. No floor. The output recedes monotonically toward the ground asprominencedrops -- the gradient callers depend on, with no tier collapse. Only guaranteed legible on promotion (raising the block back to prominence 1.0 = identity = the seed), not at rest. Atprominence: 0.0with the floor off, the color fades all the way to ground -- effectively invisible against the background. This is an intentional boundary of the pure-fade contract, not a bug.Floored (opt-in,
legibility_floor: true). Additionally clamps the output to a WCAG-style contrast floor. Callers engage this only for acting / interactive content -- the tiers where blind-reading risk actually lives -- while context/history content fades free. A floored output is guaranteed legible at rest.
The legibility clamp (opt-in), and why it is opt-in
A prominence scalar floors the INPUT multiplier, not the OUTPUT
contrast: |faded_apparent - ground| = |apparent - ground| * prominence,
which depends on how much contrast the source color started with. A
low-apparent-contrast source (a receding neutral, or any near-ground
seed) can drop below any legibility threshold. When legibility_floor: true, resolve/3 clamps the WCAG-style relative-luminance contrast
ratio (Salience.relative_luminance/1, Rec. 709 -- an external check the
solver's own apparent-lightness model cannot self-certify) against
FLOOR_RATIO: if the raw fade falls short, the clamp walks back up the
fade line toward higher prominence until the floor is met, via bisection
(the fade-to-clamped mapping isn't analytically invertible once gamut
shrink is in play). "The fade line" is the one-parameter family
fade_color(t) = build_hex(ground + (apparent - ground) * t, c * t, h)
for t running from the requested prominence up to 1.0 -- it moves
BOTH apparent lightness and chroma together, so its endpoint at t = 1.0
is the true full-chroma prominence:1.0 color (≈ the input hex
itself, up to gamut round-trip), NOT a reduced-chroma proxy. That makes
the ceiling exact: the clamp never manufactures more contrast than the
color's own full-strength self, and the floor-reachability check is run
against that true ceiling. When even the full-strength color misses the
floor (a genuinely low-contrast seed against this ground -- a
palette-design gap the clamp cannot paper over without exceeding its own
ceiling), resolve/3 returns that true full-chroma ceiling as documented
best effort.
FLOOR_RATIO (3.0, 3.0:1) is a placeholder, pending
ratification by a human-eye review pass -- the first playground matrix
run picks the highest ratio at which every cell is legible and pins it
here permanently.
The needs-input starvation guard (policy floor)
Content that is waiting on the user (an approval prompt, a composer
asking for input) must never be starved of visibility below the ordinary
context it interrupts, no matter what prominence value a demotion sweep
hands it -- a projection bug must not be able to fade the one thing the
user has to see. needs_input: true floors the effective prominence
at needs_input_floor/0 (0.6, the ordinary-context tier of the
shipped salience ladder) before the fade runs. The floor raises only:
prominences at or above it pass through byte-identical, and
prominence >= 1.0 remains the identity. It composes with (and applies
before) the opt-in legibility clamp.
Truecolor-only floor; 256-color deferred
The clamp guarantees the floor on the 24-bit hex it emits -- it is
truecolor-only. 256-color survival is out of scope for this module,
and callers must not assume the clamp survives quantization:
downsampling a floored hex to the xterm-256 cube can drop it back under
the floor (and can collapse adjacent fade tiers). The eventual answer is
a redistributed per-ground tier ladder for 256-color (fewer,
wider-spaced tiers chosen in-palette), not a post-hoc clamp on a
quantized value -- deferred here because it needs a quantization-pipeline
decision this module does not make. One minimum IS pinned by regression
test: the 1.0/0.6 prominence pair must survive
Raxol.UI.Theming.Colors.find_closest_256_color/1 as distinct palette
indices (the coarsest tier separation the ladder needs on a 256-color
terminal).
Scope
This module resolves a single foreground color; it does not decide
which prominence a block gets nor whether the floor is engaged for it
(that's the caller's policy -- it owns the acting-vs-context call and
passes legibility_floor:/needs_input: accordingly), nor whether a
sealed block may be restyled at all (that's separate fold/authorization
territory).
Summary
Functions
The placeholder WCAG-style legibility floor ratio (see moduledoc).
The prominence floor applied under needs_input: true -- the
ordinary-context tier of the shipped salience ladder, so content awaiting
user input never resolves below ordinary context content.
Resolves hex at prominence (0.0..1.0) against a ground.
WCAG-style contrast ratio between two hex colors: sRGB relative luminance
(Salience.relative_luminance/1, Rec. 709), (max(Y) + 0.05) / (min(Y) + 0.05).
This is the external, standards-based legibility check, distinct from the
solver's own internal apparent-lightness model. Raises ArgumentError if
either hex string is malformed (see Salience.relative_luminance/1).
Types
Functions
The raw ground-aware fade -- identical to resolve/3's default (pure
fade) mode, and exposed as its own name so callers/tests can name the
intent explicitly, with no chance of the opt-in floor engaging. A
non-numeric ground falls back to the same lazy default as resolve/3.
@spec floor_ratio() :: float()
The placeholder WCAG-style legibility floor ratio (see moduledoc).
@spec needs_input_floor() :: float()
The prominence floor applied under needs_input: true -- the
ordinary-context tier of the shipped salience ladder, so content awaiting
user input never resolves below ordinary context content.
Resolves hex at prominence (0.0..1.0) against a ground.
Default is the pure ground-aware fade (the salience gradient) -- no
legibility floor. Pass legibility_floor: true to additionally clamp the
output to a WCAG contrast floor; callers engage this only for acting /
interactive tiers. See the moduledoc's "Two modes" section.
Options
:ground- ground (background) OKLCH lightness. Default: the OSC-11-detected terminal background (Raxol.UI.Theming.SalienceTheme.detect_ground/0), falling back toSalience.reference_ground/0when detection is unavailable. Pass explicitly to test against a specific ground without touching:persistent_term. A non-numeric:ground(a color string, an atom, ...) can't feed the fade math -- it is treated as absent and falls back to the same lazy default instead of reaching the fade math and raising.:legibility_floor-trueengages the opt-in legibility clamp (defaultfalse= pure fade).:floor_ratio- override the legibility floor ratio (default3.0); only consulted whenlegibility_floor: true.:needs_input-truefloors the effective prominence atneeds_input_floor/0(0.6) before the fade -- the starvation guard for content awaiting user input (see moduledoc). Defaultfalse. Raises the effective prominence only; inert at or above the floor.
prominence >= 1.0 is the identity (byte-identical hex back, no ground
lookup, no clamp, regardless of :legibility_floor) -- a neutrality
guarantee: a caller that never sets prominence below 1.0 sees zero
change. prominence < 0.0 is clamped to 0.0 -- a negative prominence
would extrapolate past the ground rather than fade toward it, which is
gamut-undefined.
WCAG-style contrast ratio between two hex colors: sRGB relative luminance
(Salience.relative_luminance/1, Rec. 709), (max(Y) + 0.05) / (min(Y) + 0.05).
This is the external, standards-based legibility check, distinct from the
solver's own internal apparent-lightness model. Raises ArgumentError if
either hex string is malformed (see Salience.relative_luminance/1).