From 4a1936b5f6504ce0f3b2a344291c15f2d5b3122d Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 4 Jun 2026 20:04:50 +0200 Subject: [PATCH] feat(temas): live theme builder in /temas/color (deriveScheme end-to-end) Seed color + variant (tonal/vibrant/monochrome) -> deriveScheme -> generateScale -> live preview of the 5 hierarchy role scales (12 steps each, step 9 ringed) + their APCA on-solid chips. The full uix.color builder pipeline, reactive and pure. Verified in-browser: blue seed -> tertiary purple (+60deg), monochrome collapses to one gray, primary = seed verbatim. Co-Authored-By: Claude Opus 4.8 (1M context) --- web/routes/temas/color/+page.svelte | 141 +++++++++++++++++++++++++++- 1 file changed, 140 insertions(+), 1 deletion(-) diff --git a/web/routes/temas/color/+page.svelte b/web/routes/temas/color/+page.svelte index fbb806ea1..419895652 100644 --- a/web/routes/temas/color/+page.svelte +++ b/web/routes/temas/color/+page.svelte @@ -32,7 +32,20 @@ type ColorRole } from '$uix/eidos/lib/config-types' import { INTENTS } from '$uix/intent' - import { apcaLc, oklchToGammaRgb, parseColor, wcagContrastRatio } from '$color' + import { + apcaLc, + deriveScheme, + generateScale, + oklchToGammaRgb, + oklchToHex, + parseColor, + pickNearestTemplate, + pickOnSolid, + scaleToTemplate, + wcagContrastRatio, + type ScaleSteps, + type SchemeVariant + } from '$color' let theme = $state<'light' | 'dark'>('light') @@ -77,6 +90,41 @@ const SURFACES = ['default', 'raised', 'overlay', 'muted'] as const const CONTENTS = ['primary', 'secondary', 'muted', 'disabled'] as const const BORDERS = ['subtle', 'default', 'strong'] as const + + // ── Theme builder (deriveScheme + generateScale, live) ──────────────────── + // Curve donors for the generator: the base light scales as OKLCH templates. + const TEMPLATES: Record = Object.fromEntries( + Object.entries(THEME_BASE_LIGHT_COLOR_SCALES).map(([name, scale]) => [ + name, + scaleToTemplate(STEPS.map((s) => scale[s])) + ]) + ) + const BUILDER_VARIANTS = ['tonal', 'vibrant', 'monochrome'] as const satisfies readonly SchemeVariant[] + + let seedHex = $state('#8e4ec6') + let builderVariant = $state('tonal') + + interface BuilderRole { + role: string + steps: string[] + solidHex: string + onSolid: string + } + const builderRoles = $derived.by((): BuilderRole[] => { + let seed: ReturnType + try { + seed = parseColor(seedHex) + } catch { + return [] + } + const scheme = deriveScheme(seed, builderVariant) + return Object.entries(scheme).map(([role, roleSeed]) => { + const tpl = TEMPLATES[pickNearestTemplate(roleSeed, TEMPLATES)] + const steps = generateScale(roleSeed, tpl).map(oklchToHex) + const pick = pickOnSolid(roleSeed, { onSolid: '#ffffff', onSolidContrast: '#1c1917' }, 60) + return { role, steps, solidHex: steps[8], onSolid: oklchToHex(pick.color) } + }) + }) @@ -119,6 +167,63 @@ activas, así que cambia con el tema.

+ +
+
+ uix.color · deriveScheme + generateScale +

Theme builder (1 color → esquema completo · en vivo)

+

+ Elige un color de marca y una variante: el motor deriva los roles de + jerarquía (fórmula de Material 3 en OKLCH) y genera cada escala de 12 pasos + con su pick on-solid (APCA). Los 6 intents NO se derivan (canon del libro). Es el theme + builder de punta a punta — en runtime se aplicaría con setCssVariables. +

+
+
+ +
+ {#each BUILDER_VARIANTS as v} + + {/each} +
+
+
+ {#each builderRoles as r} +
+ {r.role} +
+ {#each r.steps as hex, i} + + {/each} +
+
+ {/each} +
+
+ {#each builderRoles as r} +
+ {r.role} + {r.solidHex} +
+ {/each} +
+
+
@@ -593,4 +698,38 @@ font-size: var(--font-size-xs); color: var(--color-content-secondary); } + + /* Theme builder */ + .builder-controls { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-4); + } + .builder-seed { + display: inline-flex; + align-items: center; + gap: var(--space-2); + font-size: var(--font-size-sm); + color: var(--color-content-secondary); + } + .builder-seed input[type='color'] { + inline-size: 44px; + block-size: 30px; + padding: 0; + border: 1px solid var(--color-border-default); + border-radius: var(--radius-sm); + background: none; + cursor: pointer; + } + .builder-out { + display: flex; + flex-direction: column; + gap: var(--space-2); + } + .builder-chips { + display: flex; + flex-wrap: wrap; + gap: var(--space-2); + }