demo(temas): /temas/tema — the applyTheme(seed) capstone made tangible

A live theme builder: one seed → the five runtime builders (color · type ·
depth · shape · space) composed in a single uix.eidos.applyTheme() call,
scoped to [data-tema-preview] so the themed mini-app re-skins while the
controls stay put. Five presets set all axes at once (a whole personality);
per-axis sliders + swatches for fine control; live code mirrors the actual
call. Self-contained (boots its own minimal ActiveUix + ActiveEidos with
applyDom). Preview is hand-built from foundation tokens (--color-{role}-* ·
--space-* · --radius-* · --font-size-* + data-shape/data-depth) — the exact
surface applyTheme drives — since component recipes are code-split.

Verified in browser: all 5 axes retune live (color hue follows the seed,
type ratio, shape smoothing/corner-shape, depth elevation, space density),
presets apply atomically, stable (no cycling), prerenders under
adapter-static.

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

@ -0,0 +1,414 @@
<script lang="ts">
/**
* Tema — the runtime theme capstone made tangible.
*
* One seed → the five runtime builders (color · type · depth · shape · space)
* composed in a SINGLE `uix.eidos.applyTheme(seed)` call. The preview is a
* mini-app hand-built from FOUNDATION tokens (--color-{role}-* · --space-* ·
* --radius-* · --font-size-* + data-shape / data-depth) — exactly the surface
* applyTheme retunes. The call is scoped to `[data-tema-preview]`, so the whole
* preview re-skins while the control chrome stays put.
*
* Self-contained: boots its own minimal ActiveUix + ActiveEidos (applyDom) so
* it calls the REAL applyTheme, not a re-implementation.
*/
import { onDestroy } from 'svelte'
import { createActiveUix, setActiveUix } from '$active-uix'
import { siumLangs } from '$sium/langs/langs'
import { secsLangs } from '$libs/secs'
import { ActiveEidos } from '$uix/eidos'
import '@/uix/eidos/index.css'
// ── Runtime (minimal — no Soma/events; the preview is token-driven markup) ──
const uix = createActiveUix({
langs: { schema: { sium: siumLangs, secs: secsLangs }, defaultLocale: 'es' as const },
events: { sound: false, haptic: false }
})
setActiveUix(uix)
const eidos = ActiveEidos.create({ applyDom: true })
onDestroy(() => {
eidos.dispose()
uix.dispose()
})
type Family = 'rounded' | 'continuous' | 'cut'
// ── The seed (five axes) ─────────────────────────────────────────────────
let colorSeed = $state('#6366f1')
let ratio = $state(1.25)
let smoothing = $state(2)
let depth = $state(0.5) // 0 flat → 1 dramatic
let spaceBase = $state(4)
let family = $state<Family>('continuous')
let mode = $state<'light' | 'dark'>('dark')
let activePreset = $state<string | null>('aurora')
// ── The single call: one seed → five builders, scoped to the preview ───────
$effect(() => {
const i = depth
const raisedShadow = `0 ${Math.round(1 + i * 3)}px ${Math.round(5 + i * 16)}px rgb(0 0 0 / ${(
0.05 +
i * 0.22
).toFixed(3)})`
const overlayShadow = `0 ${Math.round(8 + i * 24)}px ${Math.round(24 + i * 46)}px rgb(0 0 0 / ${(
0.14 +
i * 0.4
).toFixed(3)})`
eidos.applyTheme(
{
color: colorSeed,
type: { base: 16, ratio },
shape: { smoothing },
depth: { raised: { shadow: raisedShadow }, overlay: { shadow: overlayShadow } },
space: { base: spaceBase }
},
{ selector: '[data-tema-preview]', color: { mode } }
)
})
// ── Presets — one click sets all five axes (a whole personality) ───────────
type Preset = {
id: string
label: string
blurb: string
color: string
ratio: number
smoothing: number
depth: number
spaceBase: number
family: Family
}
const PRESETS: Preset[] = [
{ id: 'aurora', label: 'Aurora', blurb: 'índigo · squircle · flotante', color: '#6366f1', ratio: 1.25, smoothing: 2.2, depth: 0.7, spaceBase: 4, family: 'continuous' },
{ id: 'editorial', label: 'Editorial', blurb: 'ciruela · escala alta · sutil', color: '#8e4ec6', ratio: 1.333, smoothing: 2, depth: 0.3, spaceBase: 5, family: 'continuous' },
{ id: 'brutalist', label: 'Brutalist', blurb: 'rojo · cortado · plano · denso', color: '#dc2626', ratio: 1.2, smoothing: 1, depth: 0.08, spaceBase: 3, family: 'cut' },
{ id: 'mint', label: 'Mint', blurb: 'jade · suave · aireado', color: '#12a594', ratio: 1.18, smoothing: 2.4, depth: 0.45, spaceBase: 6, family: 'continuous' },
{ id: 'sunset', label: 'Sunset', blurb: 'ámbar · squircle · cálido', color: '#ea580c', ratio: 1.28, smoothing: 2.1, depth: 0.55, spaceBase: 4, family: 'continuous' }
]
function applyPreset(p: Preset): void {
colorSeed = p.color
ratio = p.ratio
smoothing = p.smoothing
depth = p.depth
spaceBase = p.spaceBase
family = p.family
activePreset = p.id
}
const SWATCHES = [
'#6366f1', '#8e4ec6', '#db2777', '#dc2626', '#ea580c',
'#d97706', '#12a594', '#06b6d4', '#2563eb', '#475569'
]
function setSeed(v: string): void {
colorSeed = v
activePreset = null
}
// any manual tweak detaches the active preset chip
function tweak(): void {
activePreset = null
}
const codeSeed = $derived(
`uix.eidos.applyTheme({
color: '${colorSeed}',
type: { base: 16, ratio: ${ratio.toFixed(3)} },
shape: { smoothing: ${smoothing.toFixed(2)} },
depth: { raised: { shadow: … }, overlay: { shadow: … } },
space: { base: ${spaceBase} }
})`
)
let progress = $state(68)
let toggleOn = $state(true)
</script>
<svelte:head><title>Tema · Eidos</title></svelte:head>
<div class="root" data-theme="dark">
<header class="masthead">
<p class="kicker">Eidos · capstone de tema en runtime</p>
<h1 class="mast-title">Un tema, <em>una semilla</em></h1>
<p class="mast-lede">
Color, tipografía, profundidad, forma y espacio son cinco ejes — y
<code>applyTheme(seed)</code> los compone en <strong>una sola llamada</strong>. Mueve un
control o pulsa un preset: el preview entero se reentona a la vez. Lo demás de la página no se
toca — la llamada está <em>scopeada</em> a la región del preview.
</p>
</header>
<div class="lab">
<!-- ── Control panel (the seed) ─────────────────────────────────── -->
<aside class="panel">
<div class="ctl">
<span class="ctl-label">presets · una personalidad</span>
<div class="presets">
{#each PRESETS as p}
<button
class="preset"
data-active={activePreset === p.id}
onclick={() => applyPreset(p)}
>
<span class="preset-dot" style="background: {p.color}"></span>
<span class="preset-text">
<strong>{p.label}</strong>
<small>{p.blurb}</small>
</span>
</button>
{/each}
</div>
</div>
<div class="ctl">
<span class="ctl-label">color · semilla de marca</span>
<div class="swatches">
{#each SWATCHES as s}
<button
class="swatch"
data-active={colorSeed.toLowerCase() === s.toLowerCase()}
style="background: {s}"
aria-label={s}
onclick={() => setSeed(s)}
></button>
{/each}
<label class="swatch-pick" title="color personalizado">
<input type="color" bind:value={colorSeed} oninput={tweak} />
</label>
</div>
</div>
<div class="ctl">
<span class="ctl-label">tipografía · ratio modular</span>
<input type="range" min="1.1" max="1.4" step="0.01" bind:value={ratio} oninput={tweak} />
<span class="ctl-val">{ratio.toFixed(3)} · {ratio < 1.2 ? 'compacta' : ratio > 1.3 ? 'dramática' : 'equilibrada'}</span>
</div>
<div class="ctl">
<span class="ctl-label">forma · continuidad (squircle)</span>
<input type="range" min="1" max="3" step="0.05" bind:value={smoothing} oninput={tweak} />
<span class="ctl-val">{smoothing.toFixed(2)} · {smoothing < 1.3 ? 'arco' : smoothing < 2.2 ? 'squircle' : 'superelipse'}</span>
<div class="chips">
{#each ['rounded', 'continuous', 'cut'] as const as f}
<button class="chip" data-active={family === f} onclick={() => { family = f; tweak() }}>{f}</button>
{/each}
</div>
</div>
<div class="ctl">
<span class="ctl-label">profundidad · elevación</span>
<input type="range" min="0" max="1" step="0.01" bind:value={depth} oninput={tweak} />
<span class="ctl-val">{depth < 0.15 ? 'plano' : depth > 0.7 ? 'flotante' : 'elevado'}</span>
</div>
<div class="ctl">
<span class="ctl-label">espacio · unidad base</span>
<input type="range" min="3" max="6" step="0.5" bind:value={spaceBase} oninput={tweak} />
<span class="ctl-val">{spaceBase}px · {spaceBase < 4 ? 'denso' : spaceBase > 5 ? 'aireado' : 'cómodo'}</span>
</div>
<code class="panel-code">{codeSeed}</code>
</aside>
<!-- ── Preview (the themed mini-app) — scoped target of applyTheme ── -->
<div class="stage">
<div class="stage-bar">
<span class="stage-tag">[data-tema-preview]</span>
<button class="mode-toggle" onclick={() => (mode = mode === 'light' ? 'dark' : 'light')}>
{mode === 'light' ? '◐ claro' : '◑ oscuro'}
</button>
</div>
<div class="preview" data-tema-preview data-theme={mode}>
<div class="tp-card" data-depth="overlay" data-shape={family}>
<div class="tp-head">
<span class="tp-avatar">N</span>
<div class="tp-meta">
<span class="tp-title">Nocturne Studio</span>
<span class="tp-sub">Plan Pro · activo</span>
</div>
<span class="tp-badge" data-shape={family}>FLAC</span>
</div>
<p class="tp-body">
Una semilla compone los cinco ejes perceptuales. Cambia el color y mira la jerarquía
reproyectarse; sube la escala y el ritmo respira.
</p>
<div class="tp-stats">
<div class="tp-stat"><b>2.4k</b><small>oyentes</small></div>
<div class="tp-stat"><b>128</b><small>pistas</small></div>
<div class="tp-stat"><b>97%</b><small>uptime</small></div>
</div>
<div class="tp-track">
<div class="tp-fill" style="inline-size: {progress}%"></div>
</div>
<div class="tp-row">
<input class="tp-input" data-shape={family} placeholder="buscar en la biblioteca…" />
<button class="tp-btn tp-btn-primary" data-shape={family}>Reproducir</button>
</div>
<div class="tp-actions">
<button class="tp-btn tp-btn-soft" data-shape={family}>Compartir</button>
<button class="tp-btn tp-btn-ghost" data-shape={family}>Cola</button>
<label class="tp-switch">
<input type="checkbox" bind:checked={toggleOn} />
<span class="tp-switch-track" data-shape={family}><span class="tp-switch-thumb"></span></span>
<span class="tp-switch-label">Crossfade</span>
</label>
</div>
<div class="tp-chips">
<span class="tp-chip" data-shape={family} data-intent="affirm">Sincronizado</span>
<span class="tp-chip" data-shape={family} data-intent="risk">Beta</span>
<span class="tp-chip" data-shape={family}>Hi-Fi</span>
</div>
</div>
</div>
</div>
</div>
<footer class="footnote">
<p>
<strong>El sexteto de la jaula abierta, en una llamada.</strong> Los cinco builders
(<code>applyColorScheme</code> · <code>applyTypeScale</code> · <code>applyShape</code> ·
<code>applyDepth</code> · <code>applySpacing</code>) componen vía
<code>applyTheme(seed)</code> en una escritura gestionada, atómica: los ejes que das se
aplican, los que omites revierten a la foundation. Ningún referente reúne los cinco ejes
perceptuales bajo un único builder de tema en runtime.
</p>
</footer>
</div>
<style>
.root {
min-block-size: 100vh;
background:
radial-gradient(130% 90% at 50% -10%, #1c1b2e, transparent 55%),
#0c0c12;
color: #e7e7ef;
font-family: var(--font-family-primary, system-ui, sans-serif);
padding: clamp(1.5rem, 5vw, 4.5rem) clamp(1.25rem, 5vw, 5rem) 6rem;
}
.masthead { max-inline-size: 56rem; margin-block-end: clamp(2.5rem, 6vw, 4.5rem); }
.kicker { margin: 0 0 1.5rem; font-family: var(--font-family-mono, monospace); font-size: 0.72rem; letter-spacing: 0.18em; text-transform: uppercase; color: #8b8ba7; }
.mast-title { font-family: var(--font-family-display, serif); font-size: clamp(2.4rem, 7vw, 4.5rem); font-weight: 600; line-height: 1; letter-spacing: -0.03em; margin: 0 0 1.25rem; }
.mast-title em { font-style: italic; color: #a5b4fc; }
.mast-lede { max-inline-size: 54ch; font-size: 1.05rem; line-height: 1.55; color: #b6b6c8; margin: 0; text-wrap: pretty; }
.mast-lede strong { color: #fff; font-weight: 600; }
.mast-lede em { color: #a5b4fc; font-style: normal; font-weight: 600; }
code { font-family: var(--font-family-mono, monospace); font-size: 0.88em; color: #c7d2fe; background: #ffffff14; padding: 0.1em 0.4em; border-radius: 5px; }
.lab { display: grid; grid-template-columns: 1fr; gap: clamp(1.5rem, 4vw, 2.5rem); align-items: start; }
@media (min-width: 60rem) { .lab { grid-template-columns: 22rem 1fr; } }
/* ── Control panel ── */
.panel { display: flex; flex-direction: column; gap: 1.6rem; padding: clamp(1.25rem, 3vw, 1.75rem); border-radius: 18px; background: #15151f; border: 1px solid #2a2a3a; }
.ctl { display: flex; flex-direction: column; gap: 0.6rem; }
.ctl-label { font-family: var(--font-family-mono, monospace); font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.1em; color: #8b8ba7; }
.ctl input[type='range'] { accent-color: #818cf8; inline-size: 100%; }
.ctl-val { font-family: var(--font-family-mono, monospace); font-size: 0.72rem; color: #d8d8e8; }
.presets { display: flex; flex-direction: column; gap: 0.4rem; }
.preset { display: flex; align-items: center; gap: 0.7rem; padding: 0.55rem 0.7rem; border-radius: 11px; cursor: pointer; background: #1d1d2b; border: 1px solid #2a2a3a; text-align: start; color: inherit; }
.preset:hover { border-color: #3d3d52; }
.preset[data-active='true'] { border-color: #818cf8; background: #20203a; }
.preset-dot { inline-size: 1.5rem; block-size: 1.5rem; border-radius: 7px; flex: none; box-shadow: inset 0 0 0 1px #ffffff22; }
.preset-text { display: flex; flex-direction: column; min-inline-size: 0; }
.preset-text strong { font-size: 0.85rem; }
.preset-text small { font-family: var(--font-family-mono, monospace); font-size: 0.64rem; color: #8b8ba7; }
.swatches { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.swatch { inline-size: 1.75rem; block-size: 1.75rem; border-radius: 8px; cursor: pointer; border: 2px solid transparent; box-shadow: inset 0 0 0 1px #ffffff22; padding: 0; }
.swatch[data-active='true'] { border-color: #fff; }
.swatch-pick { inline-size: 1.75rem; block-size: 1.75rem; border-radius: 8px; overflow: hidden; border: 1px dashed #4a4a60; display: grid; place-items: center; cursor: pointer; }
.swatch-pick input { inline-size: 200%; block-size: 200%; cursor: pointer; border: 0; padding: 0; background: none; }
.chips { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-block-start: 0.3rem; }
.chip { font-family: var(--font-family-mono, monospace); font-size: 0.68rem; padding: 0.3rem 0.6rem; border-radius: 999px; cursor: pointer; background: #1d1d2b; color: #b6b6c8; border: 1px solid #2a2a3a; }
.chip[data-active='true'] { background: #818cf8; color: #14142a; border-color: #818cf8; }
.panel-code { display: block; white-space: pre; font-family: var(--font-family-mono, monospace); font-size: 0.66rem; line-height: 1.5; padding: 0.85rem; border-radius: 11px; background: #0d0d15; color: #a5b4fc; border: 1px solid #2a2a3a; overflow-x: auto; }
/* ── Stage + preview ── */
.stage { display: flex; flex-direction: column; gap: 0.75rem; }
.stage-bar { display: flex; align-items: center; justify-content: space-between; padding-inline: 0.25rem; }
.stage-tag { font-family: var(--font-family-mono, monospace); font-size: 0.68rem; color: #8b8ba7; }
.mode-toggle { font-family: var(--font-family-mono, monospace); font-size: 0.7rem; color: #d8d8e8; background: #1d1d2b; border: 1px solid #2a2a3a; border-radius: 999px; padding: 0.4rem 0.85rem; cursor: pointer; }
.mode-toggle:hover { border-color: #3d3d52; }
/* the scoped target: everything inside reads the applyTheme'd tokens */
.preview {
border-radius: 20px;
padding: clamp(1.5rem, 4vw, 3rem);
background:
radial-gradient(120% 80% at 50% 0%, color-mix(in oklab, var(--color-primary-solid) 14%, var(--color-surface-default)), var(--color-surface-default));
border: 1px solid var(--color-border-subtle);
display: grid;
place-items: center;
min-block-size: 30rem;
transition: background 200ms ease;
}
.tp-card {
inline-size: 100%;
max-inline-size: 26rem;
border-radius: var(--radius-xl);
background: var(--color-surface-raised);
border: 1px solid var(--color-border-default);
box-shadow: var(--depth-overlay-shadow), var(--depth-overlay-halo);
padding: var(--space-5);
display: flex;
flex-direction: column;
gap: var(--space-4);
color: var(--color-content-primary);
}
.tp-head { display: flex; align-items: center; gap: var(--space-3); }
.tp-avatar { inline-size: 2.6rem; block-size: 2.6rem; border-radius: 50%; display: grid; place-items: center; background: var(--color-primary-solid); color: var(--color-primary-contrast, #fff); font-weight: 600; font-size: var(--font-size-md); flex: none; }
.tp-meta { display: flex; flex-direction: column; flex: 1; min-inline-size: 0; }
.tp-title { font-family: var(--font-family-display, serif); font-weight: 600; font-size: var(--font-size-lg); color: var(--color-content-primary); }
.tp-sub { font-size: var(--font-size-xs); color: var(--color-content-muted); }
.tp-badge { font-family: var(--font-family-mono, monospace); font-size: 0.62rem; letter-spacing: 0.06em; padding: 0.25rem 0.55rem; border-radius: var(--radius-sm); background: var(--color-primary-surface); color: var(--color-primary-text, var(--color-primary-solid)); border: 1px solid var(--color-primary-border); }
.tp-body { margin: 0; font-size: var(--font-size-sm); line-height: 1.55; color: var(--color-content-secondary); }
.tp-stats { display: flex; gap: var(--space-3); }
.tp-stat { display: flex; flex-direction: column; gap: 0.1rem; flex: 1; padding: var(--space-2-5); border-radius: var(--radius-md); background: var(--color-surface-muted); }
.tp-stat b { font-size: var(--font-size-lg); color: var(--color-content-primary); }
.tp-stat small { font-size: var(--font-size-xs); color: var(--color-content-muted); }
.tp-track { block-size: 6px; border-radius: 999px; background: var(--color-surface-muted); overflow: hidden; }
.tp-fill { block-size: 100%; background: var(--color-primary-solid); transition: inline-size 250ms ease; }
.tp-row { display: flex; gap: var(--space-2); }
.tp-input { flex: 1; min-inline-size: 0; padding: var(--space-2-5) var(--space-3); border-radius: var(--radius-md); border: 1px solid var(--color-border-default); background: var(--color-surface-default); color: var(--color-content-primary); font: inherit; font-size: var(--font-size-sm); outline: none; }
.tp-input:focus { border-color: var(--color-primary-border); }
.tp-btn { font-family: inherit; font-size: var(--font-size-sm); font-weight: 500; padding: var(--space-2-5) var(--space-4); border-radius: var(--radius-md); border: 1px solid transparent; cursor: pointer; white-space: nowrap; }
.tp-btn-primary { background: var(--color-primary-solid); color: var(--color-primary-contrast, #fff); }
.tp-btn-primary:hover { background: var(--color-primary-solid-hover); }
.tp-btn-soft { background: var(--color-primary-surface); color: var(--color-primary-text, var(--color-primary-solid)); border-color: var(--color-primary-border); }
.tp-btn-ghost { background: transparent; color: var(--color-content-secondary); }
.tp-btn-ghost:hover { background: var(--color-surface-muted); }
.tp-actions { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.tp-switch { display: inline-flex; align-items: center; gap: var(--space-2); cursor: pointer; margin-inline-start: auto; font-size: var(--font-size-xs); color: var(--color-content-secondary); }
.tp-switch input { position: absolute; opacity: 0; pointer-events: none; }
.tp-switch-track { inline-size: 2.2rem; block-size: 1.25rem; border-radius: 999px; background: var(--color-surface-muted); border: 1px solid var(--color-border-default); position: relative; transition: background 150ms ease; }
.tp-switch-thumb { position: absolute; inset-block-start: 1px; inset-inline-start: 1px; inline-size: 1rem; block-size: 1rem; border-radius: 50%; background: var(--color-content-muted); transition: transform 150ms ease, background 150ms ease; }
.tp-switch input:checked + .tp-switch-track { background: var(--color-primary-solid); border-color: var(--color-primary-solid); }
.tp-switch input:checked + .tp-switch-track .tp-switch-thumb { transform: translateX(0.95rem); background: #fff; }
.tp-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.tp-chip { font-size: var(--font-size-xs); padding: var(--space-1) var(--space-2-5); border-radius: var(--radius-sm); background: var(--color-surface-muted); color: var(--color-content-secondary); }
.tp-chip[data-intent='affirm'] { background: var(--color-affirm-surface); color: var(--color-affirm-text, var(--color-affirm-solid)); }
.tp-chip[data-intent='risk'] { background: var(--color-risk-surface); color: var(--color-risk-text, var(--color-risk-solid)); }
.footnote { max-inline-size: 60rem; margin-block-start: clamp(2.5rem, 6vw, 4rem); padding-block-start: 2rem; border-block-start: 1px solid #2a2a3a; }
.footnote p { margin: 0; font-size: 0.92rem; line-height: 1.6; color: #b6b6c8; text-wrap: pretty; }
.footnote strong { color: #fff; }
@media (prefers-reduced-motion: reduce) {
.tp-fill, .preview, .tp-switch-track, .tp-switch-thumb { transition: none; }
}
</style>
Loading…
Cancel
Save

Powered by TurnKey Linux.