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)}
-
+ {#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