@ -109,8 +109,9 @@
// Mirror render-css's policy: white stays unless it fails APCA |Lc| < 60 OR
// WCAG < 3 on this role ' s solid ; then the slot flips to the dark on-solid .
function onSolidInfo(role: ColorRole): { picked : 'white' | 'dark' ; lc : number ; wcag : number } {
const hex = activeScales[roleScale(role)]?.['9'] ?? '#888888'
const solid = oklchToGammaRgb(parseColor(hex))
const solidOklch = effectiveSolids[role]
if (!solidOklch) return { picked : 'white' , lc : 0 , wcag : 0 }
const solid = oklchToGammaRgb(solidOklch)
const white = oklchToGammaRgb(parseColor(ON_SOLID))
const lcWhite = apcaLc(white, solid)
const wcagWhite = wcagContrastRatio(white, solid)
@ -176,6 +177,34 @@
})
})
// The CURRENT solid (step 9) per role — what the page actually renders. Hierarchy +
// neutral come from the derived/overridden scheme; the intents from temper (or the
// canonical scale at amount 0). onSolidInfo + the on-solid grouping read this so
// caption + groups match the rendered chips.
const effectiveSolids = $derived.by((): Record< string , Oklch > => {
const out: Record< string , Oklch > = {}
for (const { role , seed } of schemeSeeds) if (role !== 'neutralVariant') out[role] = seed
for (const intent of INTENTS) {
if (intent === 'neutral') continue
const baseHex = activeScales[CANONICAL_INTENT_SCALES[intent]]?.['9']
if (!baseHex) continue
const base = parseColor(baseHex)
out[intent] = seedOklch & & temperAmount > 0 ? temper(base, seedOklch, temperAmount) : base
}
return out
})
// Split the roles by their on-solid TEXT pick (white vs dark) — the 2 groups.
const onSolidGroups = $derived.by((): { white : ColorRole []; dark : ColorRole [] } => {
const white: ColorRole[] = []
const dark: ColorRole[] = []
for (const role of COLOR_ROLES) {
if (onSolidInfo(role).picked === 'dark') dark.push(role)
else white.push(role)
}
return { white , dark }
})
interface BuilderRole {
role: string
steps: string[]
@ -226,6 +255,19 @@
return parts.join(';')
})
// Set a color input's value imperatively (client-only action). Avoids a reactive
// `value=` / `bind:value` on < input type = "color" > , which Svelte momentarily sets
// to "" while hydrating (→ "specified value '' does not conform" warning). With
// the action there is no Svelte-managed value to apply during hydration. Keeps SSR.
function colorValue(node: HTMLInputElement, hex: string) {
node.value = hex || '#000000'
return {
update(next: string) {
node.value = next || '#000000'
}
}
}
function clearOverride(role: string): void {
const next = { ... overrides }
delete next[role]
@ -296,7 +338,7 @@
semilla
< input
type="color"
v alue={ seedHex || '#8e4ec6' }
use:colorV alue={ seedHex || '#8e4ec6' }
oninput={( e ) => {
if (e.currentTarget.value) seedHex = e.currentTarget.value
}}
@ -325,7 +367,7 @@
< span class = "role-pin" >
< input
type="color"
v alue={ r . solidHex || '#000000' }
use:colorV alue={ r . solidHex || '#000000' }
oninput={( e ) => ( overrides [ r . role ] = e . currentTarget . value )}
title="sobrescribir { r . role } "
/>
@ -459,11 +501,12 @@
El color del texto SOBRE el sólido (< code > --color-{ '{role}' } -contrast</ code > ). El motor
prefiere < strong > blanco< / strong > salvo que falle el suelo APCA (|Lc| < 60) o WCAG (< 3)
— entonces voltea a < strong > oscuro< / strong > . El chip usa los tokens REALES; el pie muestra
el cálculo APCA en vivo. En el tema base solo < strong > risk< / strong > (naranja) voltea.
el cálculo APCA en vivo. Se agrupan por el pick — < strong > texto blanco< / strong > (sólidos
oscuros) y < strong > texto oscuro< / strong > (sólidos claros); con el tema base solo risk va a
oscuro.
< / p >
< / div >
< div class = "onsolid-grid" >
{ #each COLOR_ROLES as role }
{ # snippet chipFor ( role : ColorRole )}
{ @const info = onSolidInfo ( role )}
< div
class="chip"
@ -472,8 +515,19 @@
< strong > { role } </ strong >
< small > on-solid: { info . picked } · |Lc| { Math . abs ( info . lc ). toFixed ( 0 )} · WCAG { info . wcag . toFixed ( 2 )} </ small >
< / div >
{ /each }
{ /snippet }
{ #if onSolidGroups . white . length }
< div class = "onsolid-group-label" > Texto blanco < small > ({ onSolidGroups . white . length } )</ small ></ div >
< div class = "onsolid-grid" >
{ #each onSolidGroups . white as role }{ @render chipFor ( role )}{ /each }
< / div >
{ /if }
{ #if onSolidGroups . dark . length }
< div class = "onsolid-group-label" > Texto oscuro < small > ({ onSolidGroups . dark . length } )</ small ></ div >
< div class = "onsolid-grid" >
{ #each onSolidGroups . dark as role }{ @render chipFor ( role )}{ /each }
< / div >
{ /if }
< / section >
<!-- Superficies translúcidas — compositing - inverse alpha -->
@ -776,6 +830,17 @@
}
/* On-solid chips */
.onsolid-group-label {
margin-block: var(--space-2) var(--space-1);
font-family: var(--font-family-mono, 'Roboto Mono', ui-monospace, monospace);
font-size: var(--font-size-xs);
font-weight: 600;
color: var(--color-content-secondary);
}
.onsolid-group-label small {
font-weight: 400;
color: var(--color-content-muted);
}
.onsolid-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));