fix(a11y): compute brand-button text color; fix Create-button flex squeeze - #578
Open
umzcio wants to merge 18 commits into
Open
fix(a11y): compute brand-button text color; fix Create-button flex squeeze#578umzcio wants to merge 18 commits into
umzcio wants to merge 18 commits into
Conversation
`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
force-pushed
the
stack/03-a11y-and-button-layout
branch
from
July 27, 2026 17:23
4435a5a to
60380aa
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two small UI bugs, both found by using the admin panel rather than reading it, and both
pre-existing (present in
Admin.tsxbefore 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 ofvar(--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 the4.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:
BrandingContextsets--highlight-text-colorfromgetContrastTextColor(theme.highlight_color)(
frontend/src/utils/color.ts), which flips on luminance. These eight sites simply weren'tconsuming 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 }}>, andset no
flexShrink, so it inheritedflex-shrink: 1and was squeezed narrower than its owncontent 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
var(--highlight-text-color, <the previous value>)— keeping the old value as the CSSfallback, 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, activesub-tab pill),
DocumentViewer×1,AddUrlsModal×1,DocumentPickerModal×3.flexShrink: 0,display: 'inline-flex',alignItems: 'center',gap: 4,whiteSpace: 'nowrap', and dropped the now-redundantverticalAlign/marginRightfrom the<Plus>icon. This matches the canonical patternalready used at
QualityTab.tsx:209.flexShrink: 0, whiteSpace: 'nowrap'— guards only, no flex/gap, since it has no icon.utils/color.test.ts: added a regression case assertinggetContrastTextColor('#1D3C34') === '#ffffff', named for the 1.75:1 failure.Test Plan
make ci)make release-check) — n/aCHANGELOG.md— not done; an accessibility fix is arguably user-facing. Advise on wording.Contrast measured, not estimated:
#1D3C34(dark green brand)#eab308(default gold)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 worsethan no test. If you want real protection for this class of bug,
frontend/e2e/already has 8Playwright 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 contraston an unbranded install. It is unreachable in practice, since
BrandingContextsets thevariable 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 #