@ -96,6 +96,103 @@ for (const [mode, scales] of MODES) {
}
}
// ── Tier-1: the LOAD-BEARING borders (WCAG 1.4.11 — 3:1 required) ───────────
// The per-scale `border` (step 7) is a DECORATIVE accent (used only when a
// filled/colored element also carries its boundary via bg + content) → exempt.
// The borders that ARE the sole indicator of a control's boundary/state use
// STRONGER tokens; those are the real 3:1 promise. Role bindings in the base:
// neutral→gray, primary→purple, risk→orange.
function compositeSrgb ( fgHex : string , alpha : number , bgHex : string ) : string | null {
const fg = safeParseColor ( fgHex )
const bg = safeParseColor ( bgHex )
if ( ! fg || ! bg ) return null
// `oklchToGammaRgb` returns a normalized [0,1] `[r,g,b]` tuple — scale to [0,255].
const f = oklchToGammaRgb ( fg )
const b = oklchToGammaRgb ( bg )
const hex = ( i : 0 | 1 | 2 ) = >
Math . max ( 0 , Math . min ( 255 , Math . round ( ( f [ i ] * alpha + b [ i ] * ( 1 - alpha ) ) * 255 ) ) )
. toString ( 16 )
. padStart ( 2 , '0' )
return ` # ${ hex ( 0 ) } ${ hex ( 1 ) } ${ hex ( 2 ) } `
}
// Role bindings in the base theme: neutral→gray, primary→purple, risk→orange.
const NEUTRAL = 'gray'
const PRIMARY = 'purple'
const RISK = 'orange'
const FOCUS_ALPHA : Record < string , number > = { light : 0.48 , dark : 0.52 }
const RING_ERROR_ALPHA : Record < string , number > = { light : 0.52 , dark : 0.56 }
// The surfaces a control (and thus its border) sits on.
const SURFACES : readonly { id : string ; step : number } [ ] = [
{ id : 'default' , step : 1 } ,
{ id : 'raised' , step : 2 } ,
{ id : 'muted' , step : 3 }
]
interface BorderToken {
label : string
/** `true` = the sole indicator of a boundary/state → WCAG 1.4.11 wants 3:1. */
loadBearing : boolean
/** Resolve the (already-composited) border color for a mode's scale map. */
color : ( s : Record < string , StepMap > , mode : string , surfaceHex : string ) = > string | undefined
}
const BORDER_TOKENS : readonly BorderToken [ ] = [
{ label : 'border-subtle (neutral·4)' , loadBearing : false , color : ( s ) = > s [ NEUTRAL ] ? . [ 4 ] } ,
{ label : 'border-default (neutral·6) — resting' , loadBearing : false , color : ( s ) = > s [ NEUTRAL ] ? . [ 6 ] } ,
{ label : 'border-strong (neutral·8) — hover/ghost' , loadBearing : true , color : ( s ) = > s [ NEUTRAL ] ? . [ 8 ] } ,
{
label : 'focus ring (primary·8 @ ~50%)' ,
loadBearing : true ,
color : ( s , mode , surf ) = > {
const fg = s [ PRIMARY ] ? . [ 8 ]
return fg ? ( compositeSrgb ( fg , FOCUS_ALPHA [ mode ] , surf ) ? ? undefined ) : undefined
}
} ,
{
label : 'focus ring-error (risk·8 @ ~54%)' ,
loadBearing : true ,
color : ( s , mode , surf ) = > {
const fg = s [ RISK ] ? . [ 8 ]
return fg ? ( compositeSrgb ( fg , RING_ERROR_ALPHA [ mode ] , surf ) ? ? undefined ) : undefined
}
} ,
{ label : 'invalid border (risk·7)' , loadBearing : true , color : ( s ) = > s [ RISK ] ? . [ 7 ] } ,
{ label : 'checked/selected (primary·9 solid)' , loadBearing : true , color : ( s ) = > s [ PRIMARY ] ? . [ 9 ] }
]
interface T1Row {
mode : string
surface : string
label : string
loadBearing : boolean
wcag : number
apca : number
pass : boolean
}
const t1 : T1Row [ ] = [ ]
for ( const [ mode , scales ] of MODES ) {
for ( const surf of SURFACES ) {
const surfaceHex = scales [ NEUTRAL ] ? . [ surf . step ]
if ( ! surfaceHex ) continue
for ( const bt of BORDER_TOKENS ) {
const c = bt . color ( scales , mode , surfaceHex )
if ( ! c ) continue
const m = measure ( c , surfaceHex )
if ( ! m ) continue
t1 . push ( {
mode ,
surface : surf.id ,
label : bt.label ,
loadBearing : bt.loadBearing ,
wcag : m.wcag ,
apca : m.apca ,
pass : m.wcag >= 3
} )
}
}
}
// ── Report ────────────────────────────────────────────────────────────────
const f2 = ( n : number ) = > n . toFixed ( 2 )
console . log ( ` \ nContrast audit — ${ checked } pair-checks ( ${ PALETTE_SCALES . length } scales × 2 modes × ${ PAIRS . length } pairs) \ n ` )
@ -122,9 +219,37 @@ if (failures.length === 0) {
}
// Summary by pair (fail count) — the shape of the drift.
console . log ( 'Summary ( fails per pair):')
console . log ( 'Summary ( per-scale pairs — fails/66 ):')
for ( const p of PAIRS ) {
const n = failures . filter ( ( r ) = > r . pair === p . label ) . length
console . log ( ` ${ n === 0 ? '✅' : '⚠️ ' } ${ p . label . padEnd ( 30 ) } ${ n } /66 ` )
console . log ( ` ${ n === 0 ? '✅' : '⚠️ ' } ${ p . label . padEnd ( 30 ) } ${ n } /66 ${ p . kind === 'non-text' ? '(non-text · see Tier-1 below)' : '' } ` )
}
console . log ( '' )
// ── Semantic border matrix (the tokens that ARE used as borders) ────────────
// `L` = load-bearing (sole indicator → WCAG 1.4.11 wants 3:1); `·` = decorative
// (accompanied by a fill / other cue → exempt). Floor 3:1 for the L rows.
console . log ( 'Semantic border matrix — WCAG:1 vs each surface (L = load-bearing/3:1-required):\n' )
const surfIds = SURFACES . map ( ( s ) = > s . id )
const header = ` ${ 'token' . padEnd ( 38 ) } ${ 'kind' . padEnd ( 4 ) } ` + surfIds . map ( ( s ) = > ` ${ ( '' + s ) . padEnd ( 14 ) } ` ) . join ( '' )
for ( const mode of [ 'light' , 'dark' ] ) {
console . log ( ` ── ${ mode } (surface: default·1 / raised·2 / muted·3) ` )
console . log ( header )
for ( const bt of BORDER_TOKENS ) {
const cells = surfIds . map ( ( sid ) = > {
const r = t1 . find ( ( x ) = > x . mode === mode && x . surface === sid && x . label === bt . label )
if ( ! r ) return '' . padEnd ( 14 )
const mark = ! r . loadBearing ? ' ' : r . pass ? '✅' : '⚠️'
return ` ${ mark } ${ f2 ( r . wcag ) } ` . padEnd ( 14 )
} )
console . log ( ` ${ bt . label . padEnd ( 38 ) } ${ ( bt . loadBearing ? 'L' : '·' ) . padEnd ( 4 ) } ${ cells . join ( '' ) } ` )
}
console . log ( '' )
}
const lbFails = t1 . filter ( ( r ) = > r . loadBearing && ! r . pass )
console . log (
lbFails . length === 0
? 'Load-bearing borders: ✅ all clear 3:1.\n'
: ` Load-bearing 3:1 FAILURES: ${ lbFails . length } → ${ [ . . . new Set ( lbFails . map ( ( r ) = > r . label ) ) ] . join ( ' · ' ) } \ n `
)