@ -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< string , ScaleSteps > = 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< SchemeVariant > ('tonal')
interface BuilderRole {
role: string
steps: string[]
solidHex: string
onSolid: string
}
const builderRoles = $derived.by((): BuilderRole[] => {
let seed: ReturnType< typeof parseColor >
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 ) }
})
})
< / script >
< svelte:head >
@ -119,6 +167,63 @@
activas, así que cambia con el tema.
< / p >
<!-- Theme builder — deriveScheme + generateScale, live -->
< section class = "preset" >
< div class = "head" >
< span class = "eyebrow" > uix.color · deriveScheme + generateScale< / span >
< h2 > Theme builder < small > (1 color → esquema completo · en vivo)< / small > < / h2 >
< p >
Elige un color de marca y una variante: el motor < strong > deriva< / strong > los roles de
jerarquía (fórmula de Material 3 en OKLCH) y < strong > genera< / strong > 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 < code > setCssVariables< / code > .
< / p >
< / div >
< div class = "builder-controls" >
< label class = "builder-seed" >
semilla
< input type = "color" bind:value = { seedHex } / >
< code > { seedHex } </ code >
< / label >
< div class = "seg" role = "group" aria-label = "Variante" >
{ #each BUILDER_VARIANTS as v }
< button
type="button"
data-active={ builderVariant === v }
onclick={() => ( builderVariant = v )}
>
{ v }
< / button >
{ /each }
< / div >
< / div >
< div class = "builder-out" >
{ #each builderRoles as r }
< div class = "role-row" >
< span class = "role-name" >< strong > { r . role } </ strong ></ span >
< div class = "swatches" >
{ #each r . steps as hex , i }
< span
class="sw"
class:nine={ i === 8 }
style="background: { hex } "
title="{ r . role } { i + 1 } "
>< / span >
{ /each }
< / div >
< / div >
{ /each }
< / div >
< div class = "builder-chips" >
{ #each builderRoles as r }
< div class = "chip" style = "background: { r . solidHex } ; color: { r . onSolid } " >
< strong > { r . role } </ strong >
< small > { r . solidHex } </ small >
< / div >
{ /each }
< / div >
< / section >
<!-- La paleta — 31 escalas × 12 pasos -->
< section class = "preset" >
< div class = "head" >
@ -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);
}
< / style >