fix(temas/color): proper color-input fix (action, keep SSR) + split on-solid in 2 groups

- Revert ssr:false (it sidestepped the problem, did not fix it). Set the color
  inputs imperatively via a `colorValue` action instead of reactive value=/bind:value.
  With no Svelte-managed value, hydration no longer assigns "" (the warning) -- and
  SSR stays ON. Verified: SSR HTML emits the 6 color inputs with NO value attr; the
  action populates them client-side with valid hex.
- Split the "Texto on-solid" section into 2 groups by the APCA pick: "Texto blanco"
  (dark solids) and "Texto oscuro" (light solids), computed from the live builder
  solids (effectiveSolids) so the groups match the rendered chips. Default: white(8)
  / dark(1 = risk).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent 1f61ef1b43
commit fc59ceefc0

@ -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"
value={seedHex || '#8e4ec6'}
use:colorValue={seedHex || '#8e4ec6'}
oninput={(e) => {
if (e.currentTarget.value) seedHex = e.currentTarget.value
}}
@ -325,7 +367,7 @@
<span class="role-pin">
<input
type="color"
value={r.solidHex || '#000000'}
use:colorValue={r.solidHex || '#000000'}
oninput={(e) => (overrides[r.role] = e.currentTarget.value)}
title="sobrescribir {r.role}"
/>
@ -459,21 +501,33 @@
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| &lt; 60) o WCAG (&lt; 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}
{@const info = onSolidInfo(role)}
<div
class="chip"
style="background: var(--color-{role}-solid); color: var(--color-{role}-contrast)"
>
<strong>{role}</strong>
<small>on-solid: {info.picked} · |Lc| {Math.abs(info.lc).toFixed(0)} · WCAG {info.wcag.toFixed(2)}</small>
</div>
{/each}
</div>
{#snippet chipFor(role: ColorRole)}
{@const info = onSolidInfo(role)}
<div
class="chip"
style="background: var(--color-{role}-solid); color: var(--color-{role}-contrast)"
>
<strong>{role}</strong>
<small>on-solid: {info.picked} · |Lc| {Math.abs(info.lc).toFixed(0)} · WCAG {info.wcag.toFixed(2)}</small>
</div>
{/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));

@ -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 `<input type="color">`
// 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

Loading…
Cancel
Save

Powered by TurnKey Linux.