@ -18,6 +18,7 @@ import {
alphaOverBackground ,
alphaOverBackground ,
deriveScheme ,
deriveScheme ,
generateScale ,
generateScale ,
oklchToCss ,
oklchToHex ,
oklchToHex ,
parseColor ,
parseColor ,
pickNearestTemplate ,
pickNearestTemplate ,
@ -74,9 +75,11 @@ export interface BuildSchemeOptions {
export interface SchemeRoleResult {
export interface SchemeRoleResult {
readonly role : string ;
readonly role : string ;
/** 12 hex steps (1..12) . */
/** 12 hex steps (1..12) — the gamut-mapped sRGB fallback . */
readonly steps : readonly string [ ] ;
readonly steps : readonly string [ ] ;
/** Step 9 — the solid identity. */
/** 12 `oklch()` steps (1..12) — the wide-gamut value (may exceed sRGB). */
readonly stepsOklch : readonly string [ ] ;
/** Step 9 — the solid identity (hex). */
readonly solid : string ;
readonly solid : string ;
/** The APCA-picked on-solid hex. */
/** The APCA-picked on-solid hex. */
readonly onSolid : string ;
readonly onSolid : string ;
@ -85,8 +88,17 @@ export interface SchemeRoleResult {
}
}
export interface BuildSchemeResult {
export interface BuildSchemeResult {
/** `--primitive-{role}-*` + `--color-{role}-contrast` overrides. */
/ * *
* ` --primitive-{role}-* ` ( hex / rgba ) + ` --color-{role}-contrast ` — the universal
* sRGB - fallback layer + introspection - friendly values .
* /
readonly variables : EidosCssVariableMap ;
readonly variables : EidosCssVariableMap ;
/ * *
* ` --primitive-{role}-{1..12} ` as ` oklch() ` — the wide - gamut siblings ( only the
* opaque steps ; keyed identically to ` variables ` ) . Pair with ` variables ` to emit
* a hex - fallback + oklch - override stack ; see { @link schemeDeclarations } .
* /
readonly wideGamut : EidosCssVariableMap ;
/** Per-role introspection (steps + solid + on-solid + pinned). */
/** Per-role introspection (steps + solid + on-solid + pinned). */
readonly roles : readonly SchemeRoleResult [ ] ;
readonly roles : readonly SchemeRoleResult [ ] ;
}
}
@ -125,14 +137,20 @@ export function buildScheme(seed: string | Oklch, options: BuildSchemeOptions):
}
}
const variables : Record < string , string > = { } ;
const variables : Record < string , string > = { } ;
const wideGamut : Record < string , string > = { } ;
const roles : SchemeRoleResult [ ] = [ ] ;
const roles : SchemeRoleResult [ ] = [ ] ;
const emit = ( role : string , roleSeed : Oklch , pinned : boolean ) : void = > {
const emit = ( role : string , roleSeed : Oklch , pinned : boolean ) : void = > {
const template = templates [ pickNearestTemplate ( roleSeed , templates ) ] ;
const template = templates [ pickNearestTemplate ( roleSeed , templates ) ] ;
// generateScale keeps RAW OKLCH (no gamut clamp); the wide-gamut chroma
// survives into `oklchToCss`, while `oklchToHex` is the gamut-mapped fallback.
const stepsOklch = generateScale ( roleSeed , template ) ;
const stepsOklch = generateScale ( roleSeed , template ) ;
const stepsHex = stepsOklch . map ( oklchToHex ) ;
const stepsHex = stepsOklch . map ( oklchToHex ) ;
const stepsWide = stepsOklch . map ( oklchToCss ) ;
stepsHex . forEach ( ( hex , index ) = > {
stepsHex . forEach ( ( hex , index ) = > {
variables [ ` --primitive- ${ role } - ${ index + 1 } ` ] = hex ;
const key = ` --primitive- ${ role } - ${ index + 1 } ` ;
variables [ key ] = hex ;
wideGamut [ key ] = stepsWide [ index ] ;
} ) ;
} ) ;
const a2 = alphaOverBackground ( stepsOklch [ 1 ] , bg ) ;
const a2 = alphaOverBackground ( stepsOklch [ 1 ] , bg ) ;
const a3 = alphaOverBackground ( stepsOklch [ 2 ] , bg ) ;
const a3 = alphaOverBackground ( stepsOklch [ 2 ] , bg ) ;
@ -140,7 +158,7 @@ export function buildScheme(seed: string | Oklch, options: BuildSchemeOptions):
variables [ ` --primitive- ${ role } -a3 ` ] = rgbaStr ( a3 . rgb , a3 . alpha ) ;
variables [ ` --primitive- ${ role } -a3 ` ] = rgbaStr ( a3 . rgb , a3 . alpha ) ;
const onSolidHex = oklchToHex ( pickOnSolid ( stepsOklch [ 8 ] , onSolid , floor ) . color ) ;
const onSolidHex = oklchToHex ( pickOnSolid ( stepsOklch [ 8 ] , onSolid , floor ) . color ) ;
variables [ ` --color- ${ role } -contrast ` ] = onSolidHex ;
variables [ ` --color- ${ role } -contrast ` ] = onSolidHex ;
roles . push ( { role , steps : stepsHex , s olid: stepsHex [ 8 ] , onSolid : onSolidHex , pinned } ) ;
roles . push ( { role , steps : stepsHex , s tepsOklch: stepsWide , s olid: stepsHex [ 8 ] , onSolid : onSolidHex , pinned } ) ;
} ;
} ;
const overrideSeed = ( role : string ) : Oklch | null = > {
const overrideSeed = ( role : string ) : Oklch | null = > {
@ -168,5 +186,44 @@ export function buildScheme(seed: string | Oklch, options: BuildSchemeOptions):
emit ( intent , temper ( parseColor ( baseHex ) , seedOklch , temperAmount ) , false ) ;
emit ( intent , temper ( parseColor ( baseHex ) , seedOklch , temperAmount ) , false ) ;
}
}
return { variables , roles } ;
return { variables , wideGamut , roles } ;
}
export interface SchemeDeclarationsOptions {
/ * *
* Emit the hex value as a fallback line BEFORE the ` oklch() ` override ( a CSS
* stack : pre - OKLCH browsers take the hex , the rest take the wider ` oklch() ` ) .
* Set ` false ` to emit a single ` oklch() ` line per step — required where the host
* keeps only one value per property ( inline ` style= ` ) . @default true
* /
readonly fallback? : boolean ;
}
/ * *
* Flatten a { @link BuildSchemeResult } into CSS declaration strings ( ` name: value; ` ) .
*
* For each opaque primitive step it stacks the * * hex fallback * * then the wide - gamut
* * * ` oklch() ` * * ( unless ` fallback: false ` , which emits only the ` oklch() ` ) ; alpha
* and contrast tokens emit once . Wrap the result in a selector for a stylesheet , or
* ` join('') ` it for an inline ` style= ` attribute .
* /
export function schemeDeclarations (
result : BuildSchemeResult ,
options : SchemeDeclarationsOptions = { }
) : string [ ] {
const fallback = options . fallback ? ? true ;
const lines : string [ ] = [ ] ;
for ( const [ name , value ] of Object . entries ( result . variables ) ) {
if ( value == null ) continue ;
const wide = result . wideGamut [ name ] ;
if ( wide == null ) {
lines . push ( ` ${ name } : ${ value } ; ` ) ;
} else if ( fallback ) {
lines . push ( ` ${ name } : ${ value } ; ` ) ;
lines . push ( ` ${ name } : ${ wide } ; ` ) ;
} else {
lines . push ( ` ${ name } : ${ wide } ; ` ) ;
}
}
return lines ;
}
}