diff --git a/web/routes/temas/color/+page.svelte b/web/routes/temas/color/+page.svelte index 91e926eec..9d905dc17 100644 --- a/web/routes/temas/color/+page.svelte +++ b/web/routes/temas/color/+page.svelte @@ -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 => { + const out: Record = {} + 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 , 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 { if (e.currentTarget.value) seedHex = e.currentTarget.value }} @@ -325,7 +367,7 @@ (overrides[r.role] = e.currentTarget.value)} title="sobrescribir {r.role}" /> @@ -459,21 +501,33 @@ El color del texto SOBRE el sólido (--color-{'{role}'}-contrast). El motor prefiere blanco salvo que falle el suelo APCA (|Lc| < 60) o WCAG (< 3) — entonces voltea a oscuro. El chip usa los tokens REALES; el pie muestra - el cálculo APCA en vivo. En el tema base solo risk (naranja) voltea. + el cálculo APCA en vivo. Se agrupan por el pick — texto blanco (sólidos + oscuros) y texto oscuro (sólidos claros); con el tema base solo risk va a + oscuro.

-
- {#each COLOR_ROLES as role} - {@const info = onSolidInfo(role)} -
- {role} - on-solid: {info.picked} · |Lc| {Math.abs(info.lc).toFixed(0)} · WCAG {info.wcag.toFixed(2)} -
- {/each} -
+ {#snippet chipFor(role: ColorRole)} + {@const info = onSolidInfo(role)} +
+ {role} + on-solid: {info.picked} · |Lc| {Math.abs(info.lc).toFixed(0)} · WCAG {info.wcag.toFixed(2)} +
+ {/snippet} + {#if onSolidGroups.white.length} +
Texto blanco ({onSolidGroups.white.length})
+
+ {#each onSolidGroups.white as role}{@render chipFor(role)}{/each} +
+ {/if} + {#if onSolidGroups.dark.length} +
Texto oscuro ({onSolidGroups.dark.length})
+
+ {#each onSolidGroups.dark as role}{@render chipFor(role)}{/each} +
+ {/if} @@ -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)); diff --git a/web/routes/temas/color/+page.ts b/web/routes/temas/color/+page.ts index 2b47e88e5..fdab8edc5 100644 --- a/web/routes/temas/color/+page.ts +++ b/web/routes/temas/color/+page.ts @@ -1,6 +1,2 @@ -// Live demo of the Eidos color system — client-rendered. It is an interactive tool -// (no SEO/SSR need), and ssr:false avoids the Svelte `` -// hydration warning (hydration momentarily sets the value to "" before applying the -// real hex; with no SSR there is no hydration). SSR HTML was already valid. +// Live demo of the Eidos color system — no static prerender. export const prerender = false -export const ssr = false