From 4ee313f91665d75c1eda658294ee6a2f9296ee5d Mon Sep 17 00:00:00 2001 From: aduagba4133 <325302794+aduagba4133@users.noreply.github.com> Date: Mon, 26 Oct 2026 06:57:17 +0100 Subject: [PATCH] fix(ui): separate integrity colors from verdict semantics --- docs/state-vocabulary.md | 10 ++-- server/index.html | 34 +++++++----- server/ui_integrity_palette_test.go | 81 +++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 16 deletions(-) create mode 100644 server/ui_integrity_palette_test.go diff --git a/docs/state-vocabulary.md b/docs/state-vocabulary.md index 4a62b1c7a..232dc9ecf 100644 --- a/docs/state-vocabulary.md +++ b/docs/state-vocabulary.md @@ -80,10 +80,12 @@ reader that announces text and nothing else. `--check-fail`. A metric with no value shows its reason *in place of* the value and is styled as unknown, so an unrun metric cannot look like a failed one. -3. **No brand colour carries financial meaning.** `--brand-*` (the green) is the - identity colour *and* the `GOOD`/`PASS`/`DIRECT` role by historical alias. A - future contributor must not lean on `--accent` to mean "recommended"; the - recommendation is a sentence, not a colour. +3. **No brand colour carries financial meaning.** Brand primitives are only + used through named roles: `--structural-*` for market provenance, + `--severity-*` for pricing outcomes, and `--check-*` for counterparty + results. In particular, `--accent` is an interactive-control affordance, + never a recommendation or investment signal. Recommendations are expressed + in text, not by borrowing a brand colour. 4. **`UNDETERMINED` and `UNAVAILABLE` are distinct.** Undetermined: we reached the source and could not establish the fact. Unavailable: we could not complete the request. The first is a grey with a reason; the second is an diff --git a/server/index.html b/server/index.html index 1752bf17c..7afd2a385 100644 --- a/server/index.html +++ b/server/index.html @@ -58,6 +58,10 @@ --warning-soft: #FEF3C7; --neutral: #475569; --neutral-soft: #E2E8F0; + /* Structural provenance is its own family, not a verdict or warning. */ + --structural-direct: var(--brand-navy); + --structural-derivative: var(--brand-slate); + --structural-nomarket: var(--neutral); --inset: #E2E8F0; --bg: var(--brand-cloud); --panel: #FFFFFF; @@ -82,12 +86,12 @@ paints data with a verdict colour. */ --chart-threshold: var(--critical); /* Structural integrity states, not severities (docs/state-vocabulary.md). - They share the brand primitives the badges use so the same state reads + They share the structural roles used by badges so the same state reads the same colour in both places, and they are deliberately distinct from --ok/--warn/--bad: NO-MARKET must not read as failure. */ - --chart-direct: var(--brand-navy); - --chart-derivative: var(--brand-slate); - --chart-nomarket: var(--brand-coral); + --chart-direct: var(--structural-direct); + --chart-derivative: var(--structural-derivative); + --chart-nomarket: var(--structural-nomarket); /* Movement decomposition (#637): the corridor's own move and the benchmark's move are two data series, painted from chart roles in the same family as the integrity marks — never from a @@ -155,6 +159,9 @@ --bad-soft: rgba(185, 28, 28, 0.18); --unknown: #CBD5E1; --unknown-soft: rgba(148, 163, 184, 0.18); + --structural-direct: #F8FAFC; + --structural-derivative: #CBD5E1; + --structural-nomarket: #94A3B8; --grid: #1E293B; --chart-grid: #334155; --chart-line: #E2E8F0; @@ -196,6 +203,9 @@ --bad-soft: rgba(185, 28, 28, 0.16); --unknown: #CBD5E1; --unknown-soft: rgba(148, 163, 184, 0.14); + --structural-direct: #F8FAFC; + --structural-derivative: #CBD5E1; + --structural-nomarket: #94A3B8; --grid: #1E293B; --chart-grid: #334155; --chart-line: #E2E8F0; @@ -372,16 +382,16 @@ box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.02); } .badge::before { font-size: 1rem; line-height: 1; } - .b-direct { color: var(--unknown); background: var(--neutral-soft); } + .b-direct { color: var(--structural-direct); background: var(--neutral-soft); } .b-direct::before { content: "◆"; } - .b-derivative { color: var(--warn); background: var(--warning-soft); } + .b-derivative { color: var(--structural-derivative); background: var(--neutral-soft); } .b-derivative::before { content: "↪"; } - .b-nomarket { color: var(--brand-coral); background: rgba(249, 115, 22, 0.12); } + .b-nomarket { color: var(--structural-nomarket); background: var(--neutral-soft); } .b-nomarket::before { content: "∅"; } .integrity-copy { min-width: 0; } .integrity-title { font-weight: 700; margin: 0; } .integrity-help { color: var(--muted); font-size: .88rem; margin: var(--space-1) 0 0; } - .integrity-dependency { font-family: var(--mono); font-size: .8rem; margin: var(--space-1) 0 0; color: var(--warn); } + .integrity-dependency { font-family: var(--mono); font-size: .8rem; margin: var(--space-1) 0 0; color: var(--structural-derivative); } .integrity-card .badge[title] { cursor: help; } @media (max-width: 520px) { .integrity-card { grid-template-columns: 1fr; gap: var(--space-2); } } .finding { font-size: 1.02rem; margin: var(--space-3) 0 0; } @@ -488,15 +498,15 @@ .integrity-cell { white-space: nowrap; } .integrity-cell .badge { font-size: .65rem; padding: var(--space-1) var(--space-2); border-width: 1px; } .integrity-cell .badge::before { font-size: .8rem; } - .integrity-cell .integrity-via { font-family: var(--mono); font-size: .72rem; color: var(--warn); margin-left: var(--space-1); } + .integrity-cell .integrity-via { font-family: var(--mono); font-size: .72rem; color: var(--structural-derivative); margin-left: var(--space-1); } .case strong { color: var(--ink); font-weight: 600; } .cs-info { font-size: .8rem; color: var(--muted); margin-left: var(--space-1); } .cs-info .cs-state { font: 600 .72rem/1 var(--mono); letter-spacing: .04em; padding: var(--space-1) var(--space-2); border-radius: var(--radius-xs); border: 1px solid currentColor; vertical-align: middle; } - .cs-info .cs-state.cs-direct { color: var(--unknown); background: var(--neutral-soft); } - .cs-info .cs-state.cs-derivative { color: var(--warn); background: var(--warning-soft); } - .cs-info .cs-state.cs-nomarket { color: var(--brand-coral); background: rgba(249, 115, 22, 0.12); } + .cs-info .cs-state.cs-direct { color: var(--structural-direct); background: var(--neutral-soft); } + .cs-info .cs-state.cs-derivative { color: var(--structural-derivative); background: var(--neutral-soft); } + .cs-info .cs-state.cs-nomarket { color: var(--structural-nomarket); background: var(--neutral-soft); } svg { width: 100%; height: auto; display: block; } .axis { fill: var(--muted); font: 10px var(--mono); } diff --git a/server/ui_integrity_palette_test.go b/server/ui_integrity_palette_test.go new file mode 100644 index 000000000..d417fb54d --- /dev/null +++ b/server/ui_integrity_palette_test.go @@ -0,0 +1,81 @@ +package server + +import ( + "strings" + "testing" +) + +func TestUIIntegrityStatesUseStructuralPalette(t *testing.T) { + css := styleBlock(t) + + for _, role := range []string{"direct", "derivative", "nomarket"} { + token := "--structural-" + role + if !strings.Contains(css, token+":") { + t.Errorf("missing structural palette token %q", token) + } + } + + for _, scheme := range []struct { + name string + start string + }{ + {name: "system dark", start: "@media (prefers-color-scheme: dark)"}, + {name: "explicit dark", start: ":root[data-theme=\"dark\"]"}, + } { + start := strings.Index(css, scheme.start) + if start < 0 { + t.Fatalf("missing %s palette", scheme.name) + } + end := strings.Index(css[start:], "}") + if end < 0 { + t.Fatalf("unterminated %s palette", scheme.name) + } + block := css[start : start+end] + for _, role := range []string{"direct", "derivative", "nomarket"} { + if !strings.Contains(block, "--structural-"+role+":") { + t.Errorf("%s palette is missing structural %s", scheme.name, role) + } + } + } + + for _, role := range []string{"direct", "derivative", "nomarket"} { + token := "--structural-" + role + selector := ".b-" + role + " { color: var(" + token + "); background: var(--neutral-soft); }" + if !strings.Contains(css, selector) { + t.Errorf("integrity badge %q must use its structural token on a neutral background", role) + } + } + + for _, selector := range []string{ + ".b-direct {", ".b-derivative {", ".b-nomarket {", + ".cs-info .cs-state.cs-direct {", ".cs-info .cs-state.cs-derivative {", + ".cs-info .cs-state.cs-nomarket {", ".integrity-dependency {", + ".integrity-cell .integrity-via {", + } { + start := strings.Index(css, selector) + if start < 0 { + t.Fatalf("missing integrity style %q", selector) + } + end := strings.Index(css[start:], "}") + if end < 0 { + t.Fatalf("unterminated integrity style %q", selector) + } + rule := css[start : start+end] + for _, forbidden := range []string{"var(--ok)", "var(--warn)", "var(--bad)", "var(--success)", "var(--warning)", "var(--critical)"} { + if strings.Contains(rule, forbidden) { + t.Errorf("integrity style %q borrows financial severity token %q", selector, forbidden) + } + } + } + for _, selector := range []string{".integrity-dependency {", ".integrity-cell .integrity-via {"} { + start := strings.Index(css, selector) + end := strings.Index(css[start:], "}") + if !strings.Contains(css[start:start+end], "var(--structural-derivative)") { + t.Errorf("derivative detail %q must use the structural palette", selector) + } + } + + if !strings.Contains(css, ".m-state {") || !strings.Contains(css, ".m-unknown { color: var(--unknown); }") { + t.Error("undetermined metrics must keep their neutral unknown styling") + } +}