feat(demos): /temas/estructura showcase + THEMING 31 (structural systems)

Live structural workshop: a real settings panel whose every gap/padding is
var(--space-*), retuned by four controls — densidad (data-density) · zoom
(data-scaling) · unidad base · fluido — composing on the panel via the preserved
calc(value * density * scaling). The space ladder (buildSpaceScale steps) visualizes
the modular rhythm + the fluid grow range. Plus THEMING 31 (three structural axes;
space as rhythm).

Verified: check 0 errors. Browser: panel --space-4 = calc(16px * density * scaling);
compact -> 0.84, base/growth re-derive the ladder live.

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

@ -2336,6 +2336,33 @@ arco + estático; la continuidad solo existía en Apple, atada a plataforma).
optar a `data-shape='continuous'` en las superficies rectangulares es una pasada separada, con
cuidado de no squircle-izar avatares/píldoras).
## 31. Estructura (espacio · densidad · escala) — el espacio como ritmo (2026-06-05)
Los sistemas **estructurales** (a diferencia de los expresivos) son **solo-estado** — el
escenario, no el suceso. Guía canónica: `STRUCTURE_ENGINE_RFC.md`. Tres ejes ortogonales:
- **Densidad** — `[data-density='compact'|'comfortable'|'spacious']` reescala el layout (`space`
+ `control-height`) sin tocar la legibilidad del texto. 3 niveles × 2 ejes (config-driven).
- **Scaling** — `[data-scaling='90'..'110']` es el zoom global (incluye tipografía; paridad
Radix), compone con densidad.
- **Espacio (el ritmo)** — `--space-{key}` se emite como
`calc(value · var(--density-space-scale) · var(--scaling))`. El **value** ya no es solo px
plano: `buildSpaceScale(seed)` (puro) + `ActiveEidos.applySpacing(seed)` / `clearSpacing()`
lo regeneran desde **una unidad base** (`base × N`, modular) y opcionalmente **fluido**
(`growth > 1` → cada paso `clamp()` que respira entre 480 y 1280px, reusando el `fluidClamp`
del type scale). **Preserva** la composición density × scaling. Hermano de
`applyTypeScale` — opt-in sobre la escala authored (`STATIC_SPACE` intacta). Exportado de
`$uix/eidos`. Demo: `/temas/estructura`.
**Doctrina**: el espacio es **ritmo, no una tabla de píxeles**. Modular + fluido + compuesto con
densidad × zoom desde una semilla. El campo entero shippea una escala plana estática; el espacio
fluido (que casi nadie hace para el espacio, solo para el tipo) + los tres ejes integrados son el
diferencial. Estructural = solo-estado (sin dos momentos — el modelo eventful es de los canales
expresivos).
**Pendiente** (futuro): `applyTheme(seed)` — una semilla que componga tipo + espacio (ritmo
compartido), capstone del cuarteto→quinteto de builders.
---
**Última revisión**: 2026-06-05. Si algo en este doc no coincide con

@ -0,0 +1,264 @@
<script lang="ts">
/**
* Structural demo — the three structural axes, live on one real panel.
*
* 1. El taller — a settings panel whose every gap/padding is `var(--space-*)`, retuned live by
* four controls: densidad (compacidad) · zoom (scaling) · unidad base (el ritmo) · fluido
* (clamp con el viewport). Un solo panel, tres ejes ortogonales + el builder de espacio.
* 2. El ritmo — la escalera de espacio (base × N) a la base/fluido actuales.
*
* Self-contained: eidos foundation + the pure `buildSpaceScale` (the applySpacing engine).
* Density / scaling apply via `data-*` on the panel; the space ladder via inline `--space-*`.
*/
import '@/uix/eidos/index.css'
import { buildSpaceScale } from '$uix/eidos'
let theme = $state<'light' | 'dark'>('dark')
$effect(() => {
const el = document.documentElement
const prev = el.getAttribute('data-theme')
el.setAttribute('data-theme', theme)
return () => {
if (prev === null) el.removeAttribute('data-theme')
else el.setAttribute('data-theme', prev)
}
})
const DENSITIES = ['compact', 'comfortable', 'spacious'] as const
type Density = (typeof DENSITIES)[number]
const SCALINGS = ['90', '95', '100', '105', '110'] as const
type Scaling = (typeof SCALINGS)[number]
let density = $state<Density>('comfortable')
let scaling = $state<Scaling>('100')
let base = $state(4)
let growth = $state(1)
// The pure builder — exactly what ActiveEidos.applySpacing runs. Drag base/fluido → the whole
// `--space-*` ladder re-derives; density/scaling compose via the preserved calc().
const scale = $derived(buildSpaceScale({ base, growth, minVw: 480, maxVw: 1280 }))
const spaceVars = $derived(scale.variables.join(' '))
const fluid = $derived(growth > 1)
</script>
<svelte:head><title>Estructura · Eidos</title></svelte:head>
<div class="root" data-theme={theme}>
<header class="masthead">
<div class="mast-row">
<p class="kicker">Eidos · sistemas estructurales</p>
<button class="theme-toggle" onclick={() => (theme = theme === 'light' ? 'dark' : 'light')}>
{theme === 'light' ? '◐ claro' : '◑ oscuro'}
</button>
</div>
<h1 class="mast-title">El espacio es <em>ritmo</em></h1>
<p class="mast-lede">
Todos shippean una escala de espacio plana y estática. Aquí el espacio es un
<strong>ritmo</strong> derivado de una unidad base, <strong>fluido</strong> (respira con el
viewport) y compuesto con <strong>densidad</strong> y <strong>zoom</strong> — tres ejes desde
una semilla. Toca los cuatro mandos y mira el mismo panel recomponerse.
</p>
</header>
<!-- ── 1 · El taller estructural ─────────────────────────────────────── -->
<section class="section">
<div class="sec-head">
<h2 class="sec-title">El taller</h2>
<p class="sec-note">
Cada hueco y relleno del panel es <code>var(--space-*)</code>. Densidad aprieta el
layout, zoom escala todo (incluido el texto), la unidad base reentona el ritmo y el fluido
lo hace respirar. <em>Estira la ventana</em> con fluido activo para verlo crecer.
</p>
</div>
<div class="lab">
<!-- the live sample: a settings panel, all spacing from --space-* -->
<div class="panel" style={spaceVars} data-density={density} data-scaling={scaling}>
<div class="panel-head">
<span class="panel-avatar">N</span>
<div class="panel-id">
<span class="panel-name">Nora Vega</span>
<span class="panel-mail">nora@activething.com</span>
</div>
<span class="panel-plan">Pro</span>
</div>
<div class="panel-list">
{#each [['Notificaciones', 'Activadas'], ['Idioma', 'Español'], ['Tema', 'Sistema'], ['Privacidad', '2 activas']] as [k, v]}
<div class="panel-row">
<span class="panel-k">{k}</span>
<span class="panel-v">{v}</span>
</div>
{/each}
</div>
<div class="panel-foot">
<button class="p-btn p-ghost">Cancelar</button>
<button class="p-btn p-solid">Guardar</button>
</div>
</div>
<!-- control panel -->
<div class="ctl-panel">
<div class="ctl">
<span class="ctl-label">densidad</span>
<div class="chips">
{#each DENSITIES as d}
<button class="chip" data-active={density === d} onclick={() => (density = d)}>{d}</button>
{/each}
</div>
</div>
<div class="ctl">
<span class="ctl-label">zoom (scaling)</span>
<div class="chips">
{#each SCALINGS as s}
<button class="chip" data-active={scaling === s} onclick={() => (scaling = s)}>{s}</button>
{/each}
</div>
</div>
<div class="ctl">
<label class="ctl-row">
<span class="ctl-label">unidad base</span>
<input type="range" min="3" max="6" step="0.5" bind:value={base} />
<span class="ctl-val">{base}px</span>
</label>
</div>
<div class="ctl">
<label class="ctl-row">
<span class="ctl-label">fluido ×</span>
<input type="range" min="1" max="1.6" step="0.05" bind:value={growth} />
<span class="ctl-val">{growth.toFixed(2)}</span>
</label>
</div>
<code class="ctl-code"
>--space-4: {scale.steps.find((s) => s.key === '4')?.value} · density · scaling</code
>
</div>
</div>
</section>
<!-- ── 2 · El ritmo ──────────────────────────────────────────────────── -->
<section class="section">
<div class="sec-head">
<h2 class="sec-title">El ritmo</h2>
<p class="sec-note">
La escalera completa a <code>base {base}px</code>{fluid ? ` · fluido ×${growth.toFixed(2)}` : ''}.
Cada peldaño = base × N (modular). {fluid
? 'Con fluido, cada barra crece de su mínimo a su máximo entre 480 y 1280px.'
: 'Sube el fluido para que respiren.'}
</p>
</div>
<div class="ladder">
{#each scale.steps as step}
{#if step.minPx > 0}
<div class="ladder-row">
<span class="ladder-key">space-{step.key}</span>
<span class="ladder-bar-wrap">
<span class="ladder-bar" style="inline-size: {step.minPx}px;"></span>
{#if fluid && step.maxPx > step.minPx}
<span class="ladder-bar-grow" style="inline-size: {step.maxPx - step.minPx}px;"></span>
{/if}
</span>
<span class="ladder-px">{step.minPx}{fluid ? `–${step.maxPx}` : ''}px</span>
</div>
{/if}
{/each}
</div>
</section>
<footer class="footnote">
<p>
<strong>El espacio como ritmo, no como tabla de píxeles.</strong> El campo entero (Tailwind ·
Material · Radix · Chakra · Mantine · Ant · Carbon · Fluent) shippea una escala de space
plana y estática. Eidos la deriva de una unidad base, la hace <strong>fluida</strong> (como su
tipo fluido — casi nadie lo hace para el espacio) y la compone con densidad × zoom. Builder
runtime: <code>applySpacing(seed)</code>. La escala authored sigue intacta — el builder es la
alternativa matemática opt-in. (Lo estructural es solo-estado: el escenario, no el suceso.)
</p>
</footer>
</div>
<style>
.root {
min-block-size: 100vh;
background:
radial-gradient(130% 90% at 50% -15%, var(--color-surface-raised), transparent 55%),
var(--color-surface-default);
color: var(--color-content-primary);
font-family: var(--font-family-primary);
padding: clamp(1.5rem, 5vw, 5rem) clamp(1.25rem, 5vw, 6rem) 6rem;
--rule: var(--color-border-default);
}
.masthead { max-inline-size: 56rem; margin-block-end: clamp(3rem, 8vw, 6rem); }
.mast-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-block-end: 2.5rem; }
.kicker { margin: 0; font-family: var(--font-family-mono); font-size: var(--font-size-xs); letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-content-muted); }
.theme-toggle { font-family: var(--font-family-mono); font-size: var(--font-size-xs); color: var(--color-content-secondary); background: var(--color-surface-raised); border: 1px solid var(--rule); border-radius: 999px; padding: 0.5rem 1rem; cursor: pointer; }
.theme-toggle:hover { color: var(--color-content-primary); }
.mast-title { font-family: var(--font-family-display); font-size: var(--font-size-xxxl); font-weight: 600; line-height: 1; letter-spacing: -0.03em; margin: 0 0 1.25rem; text-wrap: balance; }
.mast-title em { font-style: italic; color: var(--color-primary-text, var(--color-content-primary)); }
.mast-lede { max-inline-size: 52ch; font-size: var(--font-size-lg); line-height: 1.5; color: var(--color-content-secondary); margin: 0; text-wrap: pretty; }
.mast-lede strong { color: var(--color-content-primary); font-weight: 600; }
.section { margin-block-end: clamp(3.5rem, 9vw, 7rem); }
.sec-head { margin-block-end: 2.5rem; }
.sec-title { font-family: var(--font-family-display); font-size: var(--font-size-xl); font-weight: 600; letter-spacing: -0.01em; margin: 0 0 0.5rem; }
.sec-note { margin: 0; max-inline-size: 64ch; font-size: var(--font-size-sm); line-height: 1.55; color: var(--color-content-muted); }
.sec-note em { color: var(--color-content-primary); font-style: normal; font-weight: 600; }
code { font-family: var(--font-family-mono); font-size: 0.9em; color: var(--color-content-secondary); background: var(--color-surface-muted); padding: 0.1em 0.4em; border-radius: 4px; }
/* 1 · El taller */
.lab { display: grid; grid-template-columns: 1fr; gap: clamp(1.5rem, 4vw, 3rem); align-items: start; }
@media (min-width: 52rem) { .lab { grid-template-columns: 1fr 20rem; } }
/* the sample panel — ALL spacing below is var(--space-*), which the controls retune live */
.panel {
inline-size: 100%;
max-inline-size: 30rem;
background: var(--color-surface-raised);
border: 1px solid var(--rule);
border-radius: var(--radius-lg);
box-shadow: var(--depth-raised-shadow, var(--shadow-raised));
padding: var(--space-5);
display: flex;
flex-direction: column;
gap: var(--space-5);
}
.panel-head { display: flex; align-items: center; gap: var(--space-3); }
.panel-avatar { inline-size: var(--space-10); block-size: var(--space-10); border-radius: 50%; display: grid; place-items: center; background: var(--color-primary-solid); color: var(--color-primary-on-solid, #fff); font-weight: 600; flex: none; }
.panel-id { display: flex; flex-direction: column; gap: var(--space-0-5); flex: 1; min-inline-size: 0; }
.panel-name { font-family: var(--font-family-display); font-weight: 600; color: var(--color-content-primary); }
.panel-mail { font-size: var(--font-size-xs); color: var(--color-content-muted); }
.panel-plan { font-family: var(--font-family-mono); font-size: var(--font-size-xs); padding: var(--space-0-5) var(--space-2); border-radius: 999px; background: var(--color-surface-overlay); color: var(--color-content-secondary); border: 1px solid var(--rule); }
.panel-list { display: flex; flex-direction: column; gap: var(--space-1); padding-block: var(--space-3); border-block: 1px solid color-mix(in srgb, var(--rule) 60%, transparent); }
.panel-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding: var(--space-2) var(--space-1); border-radius: var(--radius-sm); font-size: var(--font-size-sm); }
.panel-k { color: var(--color-content-secondary); }
.panel-v { color: var(--color-content-muted); font-size: var(--font-size-xs); }
.panel-foot { display: flex; justify-content: flex-end; gap: var(--space-2); }
.p-btn { font-family: var(--font-family-primary); font-size: var(--font-size-sm); padding: var(--space-2) var(--space-4); border-radius: var(--radius-md); border: 1px solid transparent; cursor: pointer; }
.p-solid { background: var(--color-primary-solid); color: var(--color-primary-on-solid, #fff); }
.p-ghost { background: transparent; color: var(--color-content-secondary); border-color: var(--rule); }
.ctl-panel { display: flex; flex-direction: column; gap: 1.5rem; padding: clamp(1.25rem, 3vw, 2rem); border-radius: var(--radius-lg); background: var(--color-surface-default); border: 1px solid var(--rule); }
.ctl { display: flex; flex-direction: column; gap: 0.65rem; }
.ctl-row { display: flex; align-items: center; gap: 0.75rem; }
.ctl-row input { flex: 1; accent-color: var(--color-primary-solid); }
.ctl-label { font-family: var(--font-family-mono); font-size: var(--font-size-xs); text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-content-muted); }
.ctl-val { font-family: var(--font-family-mono); font-size: var(--font-size-xs); color: var(--color-content-primary); inline-size: 3.5rem; text-align: end; }
.chips { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.chip { font-family: var(--font-family-mono); font-size: var(--font-size-xs); padding: 0.35rem 0.75rem; border-radius: 999px; cursor: pointer; background: var(--color-surface-raised); color: var(--color-content-secondary); border: 1px solid var(--rule); }
.chip:hover { color: var(--color-content-primary); }
.chip[data-active='true'] { background: var(--color-primary-solid); color: var(--color-primary-on-solid, #fff); border-color: var(--color-primary-solid); }
.ctl-code { display: block; font-family: var(--font-family-mono); font-size: var(--font-size-xs); padding: 0.7rem 0.9rem; border-radius: var(--radius-md); background: var(--color-surface-muted); color: var(--color-content-secondary); word-break: break-all; }
/* 2 · El ritmo */
.ladder { display: flex; flex-direction: column; gap: 0.4rem; }
.ladder-row { display: grid; grid-template-columns: 5.5rem 1fr 6rem; gap: 1rem; align-items: center; }
.ladder-key { font-family: var(--font-family-mono); font-size: var(--font-size-xs); color: var(--color-content-secondary); }
.ladder-bar-wrap { display: flex; align-items: center; min-inline-size: 0; }
.ladder-bar { block-size: 0.85rem; border-radius: 3px; background: var(--color-primary-solid); flex: none; }
.ladder-bar-grow { block-size: 0.85rem; border-radius: 0 3px 3px 0; background: color-mix(in oklab, var(--color-primary-solid) 35%, transparent); flex: none; }
.ladder-px { font-family: var(--font-family-mono); font-size: var(--font-size-xs); color: var(--color-content-muted); text-align: end; }
.footnote { max-inline-size: 60rem; padding-block-start: 2rem; border-block-start: 1px solid var(--rule); }
.footnote p { margin: 0; font-size: var(--font-size-sm); line-height: 1.6; color: var(--color-content-muted); text-wrap: pretty; }
.footnote strong { color: var(--color-content-primary); font-weight: 600; }
</style>
Loading…
Cancel
Save

Powered by TurnKey Linux.