Skip to content

web: FieldConsole row links are invisible in dark theme (1.02:1), and the contrast guard cannot see it #930

Description

@mforce

Summary

Inline text actions inside FieldConsole are effectively invisible in dark theme. Measured idle contrast is 1.02:1 against --surface-2; WCAG AA requires 4.5:1 for text. Only the underline survives, so the control reads as a bare underscore.

The guard that exists to prevent exactly this (web/src/styles.test.ts, the #834 link-retirement test) passes today, because it asserts against static token defaults and never renders a FieldConsole-scoped DOM.

Root cause

web/src/components/FieldConsole.tsx:96 scopes the --link custom property for its whole subtree:

// Concept B uses brand-coloured row links; other pages retain the shared theme default.
"--link": "var(--brand)",

--brand is a fixed palette hex (#4a154b on the default palette, styles.css:14) that does not vary between light and dark. It therefore shadows the global :root[data-theme="dark"] { --link: var(--ink) } (styles.css:205) that every --link consumer expects.

Confirmed by walking getComputedStyle(el).getPropertyValue('--link') up a live dark-theme render: correct #f2ecf4 at <html>, still correct at MAIN.content, then flipping to #4a154b at the FieldConsole <section data-field-console>.

This is not an emotion cache-order or MUI specificity problem. It is a custom property being shadowed by a non-theme-aware value.

Measured contrast (Chromium, live render, dark theme, default palette)

State Computed color vs --surface-2 vs --surface
Idle #4a154b (--brand) 1.02:1 1.18:1
Busy / disabled rgba(255,255,255,0.3) 2.64:1 2.71:1
If --link were not shadowed #f2ecf4 (--ink) 12.35:1 —

Both states fail. The disabled state is a separate, secondary MUI .Mui-disabled override landing on top of an already-broken idle colour.

In light theme the same brand colour scores 13.78:1, which is why this shipped unnoticed.

Blast radius

FieldConsole is consumed by seven routes on main today: ExpensesPage, FeedPage, HistoryPage, InventoryPage, ReportsPage, StockPage, WaterPage. Affected controls include History's edit/adjust/void, Expenses' manage-categories, Water's correct, Inventory's choose-another-item, and Stock's Audit history.

PR #927 adds SalesPage as an eighth, via its own LINK_ACTION_SX copy — including the save/cancel pair fixed in that PR.

Introduced by #899, not by #927.

Not verified: only the default palette was measured. --brand is redefined per palette (styles.css:142, 160, 178). The other three palettes' dark-mode contrast is unknown and may differ in either direction.

The guard is blind, and that is half the bug

web/src/styles.test.ts (~161-175) asserts --link === --ink and checks contrast >= 4.5:1 against --surface/--surface-2. It reads static token defaults via resolveTokens and never mounts a FieldConsole-scoped DOM, so a runtime sx-level override shadowing the token is invisible to it.

Per AGENTS.md's "Writing a guard" section: "A wrong guard is worse than none because it looks safe." This one tests configuration rather than the live cascade, and it reported green through eight affected screens.

Scope

  • Fix the contrast. Either make the FieldConsole link colour theme-aware, or drop the --link override and let the theme default apply. Decide whether brand-coloured row links are still wanted in light mode, and record the decision.
  • Fix the guard so it measures the rendered cascade, not token defaults — it must fail on a FieldConsole-scoped override. Mutate it red before claiming it catches this, per the repo's mutation-first rule.
  • Check all four palettes in dark, not only the default.
  • Verify save on the Sales Draft line item specifically, which routes through the same path as of feat(web): redesign Sales with a MUI order desk and settlement rail #927.

Verification

  • Measured contrast >= 4.5:1 for idle and >= 3:1 for disabled, in dark, on every palette, on all eight screens.
  • The repaired guard fails against the current FieldConsole.tsx:96 and passes after the fix.
  • Before/after dark-theme screenshots at 1:1.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions