You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/eidos/lib/on-solid.ts

102 lines
4.2 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { apcaLc, oklchToGammaRgb, safeParseColor, wcagContrastRatio } from '$color'
import { PALETTE_SCALES } from './types'
import type { EidosConfig } from './config-types'
/**
* On-solid legibility criterion — THE single source for the "which ink goes
* on a solid fill" decision (rfc-color-engine §6.1 named this `pickOnSolid`;
* materialized 2026-07-06, user decision A.7).
*
* Canonical semantics (ratified 2026-07-06): **light-ink-first, unless it
* fails BOTH floors** — the theme's `onSolid` (white in base) stays on every
* solid unless it is illegible there; only then the slot flips to
* `onSolidContrast` (dark ink). This is deliberately NOT the RFC's earlier
* "pick the larger |Lc|" draft: pick-larger would flip half the palette
* (teal 60.5 · grass 60.2 · jade/green/bronze 61.7 · blue 62.6 · the mid
* grays 63–64) to dark ink — a completely different look, against the field
* (Radix keeps white on its mid 9s). White-first keeps the Radix look with
* the M3-class computed guarantee.
*
* Floors: APCA |Lc| ≥ 60 (perceptual, WCAG-3 draft) AND WCAG 2 ≥ 3:1 (the
* normative AA minimum for UI/large text) — the light ink must clear BOTH.
*/
export const ON_SOLID_APCA_FLOOR = 60
export const ON_SOLID_WCAG_FLOOR = 3
/**
* Does the light ink clear BOTH legibility floors over this solid?
* Returns `null` when either color can't be parsed (a `var()` reference, an
* empty slot…) — the caller decides the fallback.
*/
export function onSolidClearsFloors(onSolidCss: string, solidCss: string): boolean | null {
const solid = safeParseColor(solidCss)
const ink = safeParseColor(onSolidCss)
if (!solid || !ink) return null
const solidRgb = oklchToGammaRgb(solid)
const inkRgb = oklchToGammaRgb(ink)
return (
Math.abs(apcaLc(inkRgb, solidRgb)) >= ON_SOLID_APCA_FLOOR &&
wcagContrastRatio(inkRgb, solidRgb) >= ON_SOLID_WCAG_FLOOR
)
}
/**
* Parse-fallback for scales whose step-9 can't be evaluated (non-hex values,
* exotic configs): Radix's documented bright-9 set + this palette's light
* gold. Everything evaluable is COMPUTED — this list never decides for a
* parseable color (the hand-curated `LIGHT_SOLID_SCALES` it replaces drifted
* exactly here: orange shipped white at 2.97:1 sub-AA while the risk ROLE —
* the same hex — computed and flipped).
*/
const KNOWN_BRIGHT_SOLID_SCALES: ReadonlySet<string> = new Set([
'amber',
'yellow',
'lime',
'mint',
'sky',
'gold'
])
/**
* Compute, from the config's own data, the set of palette scales whose
* step-9 solid rejects the light ink (→ the per-instance `palette-contrast`
* cascade flips them to `onSolidContrast`).
*
* The cascade lives in the STATIC foundation (one answer for every theme),
* so the polarity is computed per configured theme and resolved:
* - unanimous across themes → that answer (today: the 33 scales agree);
* - themes disagree on a scale → the `*-light` theme's answer wins (the
* documented rule; a genuinely diverging theme is the trigger for
* per-theme cascade emission — rfc-color-engine §8 note).
* - unparseable step-9 → `KNOWN_BRIGHT_SOLID_SCALES` membership.
*/
export function computeLightSolidScales(options: EidosConfig): ReadonlySet<string> {
const themes = Object.entries(options.themes ?? {})
const result = new Set<string>()
for (const scaleName of PALETTE_SCALES) {
const votes: { light: boolean | null; fails: boolean }[] = []
for (const [themeId, theme] of themes) {
const scales = { ...options.primitives.color?.scales, ...theme.color?.scales }
const solid = scales[scaleName]?.['9']
const onSolid =
theme.color?.content?.onSolid ?? options.semantics.color.content?.onSolid
if (!solid || !onSolid) continue
const clears = onSolidClearsFloors(onSolid, solid)
if (clears === null) continue
votes.push({ light: themeId.includes('light'), fails: !clears })
}
if (votes.length === 0) {
if (KNOWN_BRIGHT_SOLID_SCALES.has(scaleName)) result.add(scaleName)
continue
}
const unanimous = votes.every((v) => v.fails === votes[0].fails)
if (unanimous) {
if (votes[0].fails) result.add(scaleName)
continue
}
const lightVote = votes.find((v) => v.light) ?? votes[0]
if (lightVote.fails) result.add(scaleName)
}
return result
}

Powered by TurnKey Linux.