From 60af383efa47b16b28af8a3a705994e7f1a8e232 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 4 Jun 2026 20:35:08 +0200 Subject: [PATCH] feat(temas/color): per-role overrides in the builder (pin any role) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit deriveScheme gives DEFAULTS, not a cage: each role row gets a color input that PINS that role to the designer's exact color, while the rest keep deriving from the seed. "auto" un-pins; changing the seed re-derives only the unpinned roles. Mirrors M3 (custom colors per role) + Radix (pick accent/gray) + the hand-authored path (grafito maps every role). Verified in-browser: pin secondary=blue + tertiary=gold while primary/neutral stay derived; auto reverts to derived. Documented in COLOR_ENGINE_RFC §6.2. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/uix/eidos/COLOR_ENGINE_RFC.md | 7 ++ web/routes/temas/color/+page.svelte | 100 +++++++++++++++++++++++----- 2 files changed, 90 insertions(+), 17 deletions(-) diff --git a/src/uix/eidos/COLOR_ENGINE_RFC.md b/src/uix/eidos/COLOR_ENGINE_RFC.md index 0bd91b4fc..8a598d04f 100644 --- a/src/uix/eidos/COLOR_ENGINE_RFC.md +++ b/src/uix/eidos/COLOR_ENGINE_RFC.md @@ -317,6 +317,13 @@ contraste lo decide **APCA** (§8). Estructurado para añadir `expressive` / `neutral` / `content` como ~15 líneas de reglas, sin tocar nada más. +**Override por rol** — `deriveScheme` da DEFAULTS, no una jaula. El diseñador puede +**fijar** cualquier rol a su color exacto (reemplaza el seed de ese rol; el resto se +sigue derivando del seed base, y cambiar el seed re-deriva solo los no fijados). Es +el patrón de Radix/M3 (colores custom por rol) y del camino hand-authored (grafito +mapea cada rol explícito: `secondary: 'violet'`). El builder de `/temas/color` lo +expone con un input de color por fila + «auto» para volver a derivado. + **Los 6 intents NO se derivan** — son hues canónicos del libro (un error es rojo siempre). `harmonize(color, toward, amount)` (M3 `blend.harmonize`) los empuja hacia la marca un % si el builder quiere cohesión — opt-in, conserva L y C. diff --git a/web/routes/temas/color/+page.svelte b/web/routes/temas/color/+page.svelte index e3b979c24..51884eb03 100644 --- a/web/routes/temas/color/+page.svelte +++ b/web/routes/temas/color/+page.svelte @@ -44,6 +44,7 @@ parseColor, pickNearestTemplate, pickOnSolid, + safeParseColor, scaleToTemplate, wcagContrastRatio, type Oklch, @@ -118,6 +119,8 @@ let seedHex = $state('#8e4ec6') let builderVariant = $state('tonal') let harmonizeIntents = $state(false) + // Per-role overrides the designer pinned (role → hex). Empty = fully derived. + let overrides = $state>({}) const seedOklch = $derived.by((): Oklch | null => { try { @@ -129,22 +132,34 @@ const ON_SOLID_PAIR = { onSolid: ON_SOLID, onSolidContrast: ON_SOLID_DARK } + // Effective role seeds: deriveScheme's defaults, with any per-role OVERRIDE the + // designer pinned taking precedence. Change the base seed → unpinned roles + // re-derive; pinned ones stay. (Radix/M3: derive defaults, override per role.) + const schemeSeeds = $derived.by((): { role: string; seed: Oklch; pinned: boolean }[] => { + if (!seedOklch) return [] + const derived = deriveScheme(seedOklch, builderVariant) + return Object.entries(derived).map(([role, dseed]) => { + const ov = overrides[role] + const parsed = ov ? safeParseColor(ov) : null + return { role, seed: parsed ?? dseed, pinned: !!ov } + }) + }) + interface BuilderRole { role: string steps: string[] solidHex: string onSolid: string - } - const builderRoles = $derived.by((): BuilderRole[] => { - if (!seedOklch) return [] - const scheme = deriveScheme(seedOklch, builderVariant) - return Object.entries(scheme).map(([role, roleSeed]) => { - const tpl = activeTemplates[pickNearestTemplate(roleSeed, activeTemplates)] - const steps = generateScale(roleSeed, tpl).map(oklchToHex) - const pick = pickOnSolid(roleSeed, ON_SOLID_PAIR, 60) - return { role, steps, solidHex: steps[8], onSolid: oklchToHex(pick.color) } + pinned: boolean + } + const builderRoles = $derived.by((): BuilderRole[] => + schemeSeeds.map(({ role, seed, pinned }) => { + const tpl = activeTemplates[pickNearestTemplate(seed, activeTemplates)] + const steps = generateScale(seed, tpl).map(oklchToHex) + const pick = pickOnSolid(seed, ON_SOLID_PAIR, 60) + return { role, steps, solidHex: steps[8], onSolid: oklchToHex(pick.color), pinned } }) - }) + ) // Live apply: write the derived hierarchy + neutral (and, with harmonize on, the // intents) as CSS-var overrides on `.root` — overriding `--primitive-{role}-*` @@ -165,12 +180,10 @@ const themeOverride = $derived.by((): string => { if (!seedOklch) return '' const bg = parseColor(theme === 'dark' ? '#111111' : '#ffffff') - const scheme = deriveScheme(seedOklch, builderVariant) const parts: string[] = [] - emitRole(parts, 'primary', scheme.primary, bg) - emitRole(parts, 'secondary', scheme.secondary, bg) - emitRole(parts, 'tertiary', scheme.tertiary, bg) - emitRole(parts, 'neutral', scheme.neutral, bg) + for (const { role, seed } of schemeSeeds) { + if (role !== 'neutralVariant') emitRole(parts, role, seed, bg) + } if (harmonizeIntents) { for (const intent of INTENTS) { if (intent === 'neutral') continue @@ -180,6 +193,12 @@ } return parts.join(';') }) + + function clearOverride(role: string): void { + const next = { ...overrides } + delete next[role] + overrides = next + } @@ -234,7 +253,9 @@ (override de --primitive-{'{role}'}-* en .root → reproyecta toda la jerarquía + el chrome neutral; en una app real sería setCssVariables). La librería de 31 escalas y los 6 intents canónicos NO cambian — salvo que actives - harmonize intents (los empuja hacia la temperatura del seed). + harmonize intents (los empuja hacia la temperatura del seed). Y puedes + fijar cualquier rol con su propio color (el input de cada fila); los demás + se siguen derivando del seed.

@@ -262,7 +283,23 @@
{#each builderRoles as r}
- {r.role} + + {r.role} + {#if r.pinned}fijado{/if} + + + (overrides[r.role] = e.currentTarget.value)} + title="sobrescribir {r.role}" + /> + {#if r.pinned} + + {/if} +
{#each r.steps as hex, i}