Skip to content

fix(a11y): compute brand-button text color; fix Create-button flex squeeze - #578

Open
umzcio wants to merge 18 commits into
ui-insight:mainfrom
umzcio:stack/03-a11y-and-button-layout
Open

fix(a11y): compute brand-button text color; fix Create-button flex squeeze#578
umzcio wants to merge 18 commits into
ui-insight:mainfrom
umzcio:stack/03-a11y-and-button-layout

Conversation

@umzcio

@umzcio umzcio commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Stacked PR — depends on #576 and #577.
GitHub cannot base a cross-fork PR on a branch that lives in the fork, so this targets
main directly and its diff currently includes those two PRs' commits. Please merge
#576 and #577 first
; this diff will then shrink to just this PR's own 2 commits:
fix(a11y): compute brand-button text color instead of hardcoding black/white and
fix(admin/teams): prevent flex-shrink squeeze on Create/Assign buttons.
The a11y fix touches TeamsTab.tsx, which only exists after #577's decomposition —
hence the dependency.

Summary

Two small UI bugs, both found by using the admin panel rather than reading it, and both
pre-existing (present in Admin.tsx before the decomposition PR this one builds on).

1. Hardcoded text colour on brand-coloured buttons — a WCAG AA failure. Eight sites set a
literal color: '#000' (or '#fff') on a background of var(--highlight-color, #eab308).
Black is correct for the default gold (10.95:1) but inverts for any dark brand colour. On a
deployment with a dark green brand (#1D3C34), black text measures 1.75:1 — below the
4.5:1 AA floor and below even the 3.0:1 large-text floor. White would be 12.01:1.

The correct mechanism already exists and is already used in 37 places:
BrandingContext sets --highlight-text-color from getContrastTextColor(theme.highlight_color)
(frontend/src/utils/color.ts), which flips on luminance. These eight sites simply weren't
consuming it.

2. The "+ Create" button on the Teams page wrapped its label under the icon. It renders
inside <div style={{ display: 'flex', gap: 8 }}> next to <input style={{ flex: 1 }}>, and
set no flexShrink, so it inherited flex-shrink: 1 and was squeezed narrower than its own
content by the growing input. It also used an older inline-flow alignment hack
(verticalAlign: 'middle', marginRight: 4) rather than flex, which is what made it wrappable.

Changes

  • Replaced the hardcoded text colour at all 8 sites with
    var(--highlight-text-color, <the previous value>) — keeping the old value as the CSS
    fallback, so a default-branded install renders exactly as before while a branded one picks
    up the computed contrast colour. Sites: TeamsTab ×3 (Create button, Assign button, active
    sub-tab pill), DocumentViewer ×1, AddUrlsModal ×1, DocumentPickerModal ×3.
  • Create-team button: added flexShrink: 0, display: 'inline-flex',
    alignItems: 'center', gap: 4, whiteSpace: 'nowrap', and dropped the now-redundant
    verticalAlign/marginRight from the <Plus> icon. This matches the canonical pattern
    already used at QualityTab.tsx:209.
  • The neighbouring "Add to Team" button had the same vulnerability and got
    flexShrink: 0, whiteSpace: 'nowrap' — guards only, no flex/gap, since it has no icon.
  • utils/color.test.ts: added a regression case asserting
    getContrastTextColor('#1D3C34') === '#ffffff', named for the 1.75:1 failure.

Test Plan

  • Shared checks pass (make ci)
  • Release check passes if packaging or deployment changed (make release-check) — n/a
  • Manually tested the affected feature(s) — verified on a deployment with a dark green brand colour: buttons now render light text on the brand background, matching the rest of the admin surface, and "+ Create" sits on one line
  • Updated CHANGELOG.mdnot done; an accessibility fix is arguably user-facing. Advise on wording.
  • Updated deploy/release docs — n/a

Contrast measured, not estimated:

Background Black text White text
#1D3C34 (dark green brand) 1.75:1 12.01:1 ✅
#eab308 (default gold) 10.95:1 ✅ 1.92:1 ❌

No rendering test was added for the layout fix, deliberately. jsdom performs no layout, so
a test asserting "the label doesn't wrap" would pass identically with the bug present. There is
no TeamsTab.test.tsx, and creating one to hold an assertion that proves nothing seemed worse
than no test. If you want real protection for this class of bug, frontend/e2e/ already has 8
Playwright specs and a bounding-box or visual assertion there would actually catch it — happy
to add that if you'd prefer.

One residual, disclosed rather than hidden. DocumentViewer.tsx's CSS fallback remains
#fff, which is itself 1.92:1 on the default gold — so that one site would still fail contrast
on an unbranded install. It is unreachable in practice, since BrandingContext sets the
variable on every app load, which is why it was left rather than changed as a drive-by. Flagging
it so it is a known quantity; happy to correct it in this PR if you'd rather.

Related Issues

Closes #

umzcio and others added 18 commits July 24, 2026 22:07
`tsc --noEmit` is a no-op on this solution-style tsconfig (`files: []`
+ project references) — `npx tsc --noEmit --listFiles` emitted 0 files,
so the typecheck gate in `frontend-ci` was vacuous. Switch to `tsc -b`,
which builds both referenced configs (`tsconfig.app.json` and
`tsconfig.node.json`, both already `noEmit: true`) and now checks 1440
files.

Verified: introduced a deliberate `string` to `number` assignment in
main.tsx and confirmed `npm run typecheck` failed with
`TS2322: Type 'string' is not assignable to type 'number'.`, then
reverted and confirmed it passes again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
v8 only instrumented files a test happened to import (no coverage.include),
so the 40% gate measured an import-driven denominator (45.92% lines,
1404/3057) rather than the whole src/ tree (7.87%, 1404/17839), and was
non-monotonic: importing a large untested file into its first test could
lower the reported number below the gate. Set coverage.include to
'src/**/*.{ts,tsx}' with excludes for test files, main.tsx, and .d.ts files,
so the denominator is fixed and adding a test can only raise the number.

Measured with the plan 009 test tranche in place: statements 8.1%
(1673/20643), branches 6.97% (1347/19312), functions 6.37% (401/6289),
lines 8.37% (1493/17837). Thresholds set just below each, rounded down:
statements 8, branches 6, functions 6, lines 8 — single-sourced in
vitest.config.ts.

Remove the duplicated --coverage.thresholds.lines=40 flag from Makefile's
frontend-test target and frontend/package.json's ci script, and fix the
stale coverage figures in CLAUDE.md and the Makefile comment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eline

The thresholds set alongside the whole-src-tree coverage denominator
(statements/lines 8%, branches/functions 6%) were measured wrong — actual
whole-src coverage is ~6.4% lines / 6.1% statements / 5.1% functions / 5.2%
branches, so CI failed on every push regardless of changes. Lower the
thresholds to sit just below the real numbers (matching the stated intent:
'set just below the honest whole-src measurement'), and correct CLAUDE.md's
stale ~8% note to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move formatNumber/formatDuration and the shared UI primitives
(StatusBadge, RoleBadge, TrendDelta, KpiCard, UserAvatar,
SortableHeader, SearchInput, ExportButton, TimeRangeSelector,
DayOption) out of Admin.tsx into
frontend/src/components/admin/shared/{format.ts,primitives.tsx}
as named exports, and import them back in. Pure move, no logic
changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…k/white

Several buttons hardcoded text color on top of the configurable
var(--highlight-color) background. That's correct only for the default
gold (#eab308, black text = 10.95:1), but any admin-configured brand
color can invert the requirement — e.g. a deployed dark green
(#1D3C34) with hardcoded black text measures 1.75:1, a WCAG AA
contrast failure (needs 4.5:1; white gives 12.01:1 here).

Switch these sites to var(--highlight-text-color, <previous hardcoded
value>), the same computed-contrast variable BrandingContext already
derives via getContrastTextColor() and that 37 other call sites use.
The hardcoded value is kept as the CSS fallback so unbranded
deployments render identically to before.

Sites fixed: TeamsTab.tsx (subTabStyle active tab, Create-team button,
Assign button), DocumentViewer.tsx (download-original link),
AddUrlsModal.tsx (Add URLs button), DocumentPickerModal.tsx (Upload
files, Add entire folder, Add buttons).

Also adds a regression case to color.test.ts asserting
getContrastTextColor('#1D3C34') returns white, using the real-world
value that exposed the bug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Create-team button sat in a flex row next to a flex:1 input with no
flexShrink/whiteSpace of its own, so the growing input squeezed it below
its content width and wrapped "Create" under the Plus icon. Add
flexShrink: 0, whiteSpace: 'nowrap', and the repo's canonical inline-flex
icon+label pattern (display/alignItems/gap, matching QualityTab.tsx:209),
dropping the old verticalAlign/marginRight hack on the Plus icon.

The "Add to Team" button next to the isolated-users <select> has the same
structural vulnerability (a flex:1 sibling with basis:0 absorbs none of
the shrink, so the auto-basis select/button take all of it), so it gets
the same flexShrink/whiteSpace guards. It has no icon, so no display/gap
change is needed there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@umzcio
umzcio force-pushed the stack/03-a11y-and-button-layout branch from 4435a5a to 60380aa Compare July 27, 2026 17:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant