fix(a11y): make every text colour clear WCAG AA on every ramp
a11y.md flagged --yj-text-tertiary on --yj-bg-surface as 'borderline (~4.1:1) but that needs a real measurement', and plan 007 parked it as 'worth measuring before planning'. Measured, against the rendered app and then across all three background ramps: it failed AA in nine of twelve text/surface combinations, as low as 2.31:1 on dark's overlay and 2.55:1 on light's -- the app's most-used secondary text colour, failing on every view. Not borderline. 110 failing nodes across twelve views, now 0 of 659. Three separate mechanisms, and only the first is the finding: - The ramps. Tertiary is raised per ramp (#a6a6a6 dark, #949494 darker, #5c636a light), sized to the lightest surface it actually sits on and keeping its hue. Sizing it to bgOverlay too would need a grey lighter than secondary, so bgOverlay is documented as not a text surface and the one component that put text there uses primary. - The avatar generator. hsl(hue, 45%, 35%) behind white initials failed for 35 of the 360 hues -- the yellow-green band -- so which artists were unreadable depended on how their names hashed. The two a sweep found were not the finding. 32% clears every hue. - Jobs' local #ff6b6b, at 4.15:1 on elevated. Pinned by a unit test over the palette table rather than a DOM sweep: the ramps are pure data, and checking only what happens to be on screen is exactly how the light ramp went unexamined. Note that make ui-visual cannot see any of this -- the component tier renders the fallbacks, because theme-store sets :root only in the real app.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* The background for a letter avatar, derived from a name.
|
||||
*
|
||||
* Three call sites drew `hsl(nameToHue(name), 45%, 35%)` behind white
|
||||
* initials, from two copies of the same hash function. Measured across
|
||||
* all 360 hues: **35 of them** — the yellow-green band from about 53°
|
||||
* to 88° — put white text below 4.5:1, bottoming out at 4.08:1. Which
|
||||
* artists those were depended entirely on how their names hashed, so
|
||||
* the app had a contrast failure that came and went with the search
|
||||
* results, and the two instances that turned up in a sweep were not the
|
||||
* finding.
|
||||
*
|
||||
* 32% lightness clears every hue with a floor of 4.75:1, so the fix is
|
||||
* a property of the generator rather than of any colour it generates.
|
||||
* `avatar-color.test.ts` walks all 360.
|
||||
*/
|
||||
|
||||
/** Hash a string to a hue value 0–360. */
|
||||
export function nameToHue(name: string): number {
|
||||
let hash = 0;
|
||||
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
|
||||
return Math.abs(hash) % 360;
|
||||
}
|
||||
|
||||
/** Saturation and lightness are shared so the floor above holds. */
|
||||
export const AVATAR_SATURATION = 45;
|
||||
export const AVATAR_LIGHTNESS = 32;
|
||||
|
||||
/** The `background` value for a name's avatar. */
|
||||
export function avatarBackground(name: string): string {
|
||||
return `hsl(${nameToHue(name)}, ${AVATAR_SATURATION}%, ${AVATAR_LIGHTNESS}%)`;
|
||||
}
|
||||
Reference in New Issue
Block a user