feat(eidos): shape Fase 5 — /temas/forma showcase + docs (channel complete)

The shape channel end to end, live:
- Continuidad — round vs squircle + a live --shape-smoothing dial (1 -> 3.2).
- Familias — rounded/continuous/cut/scoop (one corner-shape mechanism).
- Armonia anidada — concentric data-shape-nest (inner = outer - gap), toggle to
  see it break.
- Ocurre — press -> the corner firms (the press-squeeze cross-modal firma).

Docs: SHAPE_ENGINE_RFC Fases 1-5 done; THEMING 30 (shape).

Verified: check 0 errors. Browser (Chrome 146): continuous->squircle, cut->bevel,
scoop->scoop; nest inner radius = outer(16) - gap(8) = 8px; press-squeeze firma
fires on the continuous button. corner-shape degrades to arc where unsupported.

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

@ -103,17 +103,21 @@ nada → cero rotura):
## 6. Fases (la flexibilidad va horneada en cada una)
1. **Continuidad + escala** — `--shape-smoothing` + emisión progresiva (`border-radius` +
`corner-shape` donde se soporte) + validación + test + regen. Puertas: smoothing
config-driven/override, `--radius-*` intactos, escape al `border-radius` crudo.
2. **Armonía anidada** — convención/helper de radio anidado computado (`--shape-nest-gap`).
3. **Forma _eventful_ (momento-evento)** — firma `shape-morph` (squeeze/round) **sobre el
sistema de `signatures` existente**, coordinada con motion/depth desde un evento, degradando
con reduced-motion. Anulable (sobreescribe keyframes/signatures). No un sistema paralelo.
4. **Familias** — `rounded/continuous/cut/pill/sharp` como atributo + recipe + builder
`applyShape(seed)`.
5. **Showcase + docs** — `/temas/forma` (continuidad/anidado/familias/eventful en vivo) +
THEMING §shape.
1. ✅ **Continuidad + escala + familias** — `--shape-smoothing` + `data-shape` →
`corner-shape` con las 4 familias (`rounded`/`continuous`/`cut`/`scoop`). Emisión
progresiva: `border-radius` universal (la magnitud, `--radius-*` intactos) + `corner-shape`
donde se soporte (degrada al arco). Config-driven + validación + test + regen. (Las familias
se adelantaron aquí — son la misma mecánica `corner-shape`.)
2. ✅ **Armonía anidada** — `--shape-nest-gap` + regla `[data-shape-nest]`:
`border-radius: max(0, var(--shape-outer-radius) − gap)` (concéntrico).
3. ✅ **Forma _eventful_ (momento-evento)** — el morph de esquina **plegado en la firma
`press-squeeze`** (cross-modal: un press = escala + sombra + esquina) + `@property
--shape-smoothing` para que interpole. Sobre el sistema de `signatures` existente; degrada con
reduced-motion. No un sistema paralelo.
4. ✅ **Builder runtime `applyShape(seed)`** — dial de `smoothing`/`nestGap` + override de
familias, bloque gestionado (hermano de `applyColorScheme`/`applyTypeScale`/`applyDepth`).
5. ✅ **Showcase + docs** — `/temas/forma` (continuidad con dial + familias + anidado +
eventful en vivo) + THEMING §30.
## 7. Composición con lo existente

@ -2308,6 +2308,34 @@ en vivo — hermano de `applyColorScheme` / `applyTypeScale`. Demo: `/temas/prof
**Pendiente** (menor): el cue `scrim` está disponible como token (`--depth-{plane}-scrim`) pero
sin regla cableada — el backdrop dim de los modales lo gestiona hoy cada componente.
## 30. Forma (shape) — continuidad + familias + anidado + eventful (2026-06-05)
La forma es un **canal**, no un número de `border-radius`. Guía canónica:
`SHAPE_ENGINE_RFC.md`. La **magnitud** sigue en `--radius-*` (intacta); shape añade los ejes
que todos dejan planos. **El primer squircle-como-token de la web** (el campo entero es
arco + estático; la continuidad solo existía en Apple, atada a plataforma).
- **Continuidad** — `--shape-smoothing` (exponente superelipse: 1 = arco, 2 = squircle) +
familias vía `data-shape='{family}'` → `corner-shape`: `rounded` (round) · `continuous`
(`superellipse(var(--shape-smoothing))`) · `cut` (bevel) · `scoop`. **Opt-in** (no pisa
círculos/píldoras) y **progresivo**: degrada al arco de `border-radius` donde no hay
`corner-shape` (Chromium 2025+).
- **Armonía anidada** — `[data-shape-nest]` deriva `border-radius: max(0px,
var(--shape-outer-radius) − var(--shape-nest-gap))`: el hijo queda concéntrico al padre (que
expone su radio en `--shape-outer-radius`).
- **Eventful (dos momentos)** — la forma en reposo (`data-shape`) + el **morph** al pulsar: la
firma `press-squeeze` cuadra la esquina un instante (`--shape-smoothing` 2→3→2, registrado con
`@property` para que interpole). Cross-modal: un evento mueve escala + sombra + esquina.
Degrada con `prefers-reduced-motion`. No-op en familias no-`continuous`.
- **Jaula abierta** — escala + familias config-driven (`EidosConfig.primitives.shape`); el
`border-radius` crudo siempre a un paso; builder runtime `ActiveEidos.applyShape(seed)` /
`clearShape()` (+ `buildShape` puro, exportado de `$uix/eidos`) para dialar continuidad /
nestGap / familias en vivo. Demo: `/temas/forma`.
**Pendiente** (futuro): adopción por componentes (hoy las recipes usan `--radius-*` con arco;
optar a `data-shape='continuous'` en las superficies rectangulares es una pasada separada, con
cuidado de no squircle-izar avatares/píldoras).
---
**Última revisión**: 2026-06-05. Si algo en este doc no coincide con

@ -0,0 +1,271 @@
<script lang="ts">
/**
* Shape demo — the Eidos shape channel, the 8th book axis no web DS has elevated.
*
* 1. Continuidad — round vs squircle, with a live `--shape-smoothing` dial.
* 2. Familias — rounded · continuous · cut · scoop (one `corner-shape` mechanism).
* 3. Anidado — concentric radii (data-shape-nest): inner = outer − gap, corners parallel.
* 4. Ocurre — press → the corner FIRMS (the press-squeeze firma, cross-modal).
*
* Self-contained: eidos foundation (shape tokens + families + firma) + `data-theme`.
* Continuity needs `corner-shape` (Chromium 2025+); it degrades to the plain arc elsewhere.
*/
import '@/uix/eidos/index.css'
let theme = $state<'light' | 'dark'>('dark')
// Mirror the theme onto <html> so the foundation tokens resolve at :root (faithful real-app).
$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)
}
})
// 1 · live continuity dial (1 = circular arc · 2 = squircle · 3+ = squarer)
let smoothing = $state(2)
// 2 · families
const FAMILIES = [
{ id: 'rounded', label: 'rounded', note: 'arco circular — el default de todos' },
{ id: 'continuous', label: 'continuous', note: 'squircle — iOS, ahora token web' },
{ id: 'cut', label: 'cut', note: 'bisel — ángulo' },
{ id: 'scoop', label: 'scoop', note: 'cóncavo — entrante' }
] as const
// 3 · nesting toggle
let nest = $state(true)
// 4 · eventful press — stamp the contact event so the press-squeeze firma fires
let pressed = $state(false)
function firePress(): void {
pressed = false
requestAnimationFrame(() => (pressed = true))
}
</script>
<svelte:head><title>Forma · Eidos</title></svelte:head>
<div class="root" data-theme={theme}>
<header class="masthead">
<div class="mast-row">
<p class="kicker">Eidos · canal de forma</p>
<button class="theme-toggle" onclick={() => (theme = theme === 'light' ? 'dark' : 'light')}>
{theme === 'light' ? '◐ claro' : '◑ oscuro'}
</button>
</div>
<h1 class="mast-title">La esquina, <em>repensada</em></h1>
<p class="mast-lede">
Toda la web reduce la forma a un número: <code>border-radius</code>, un arco circular. La
esquina tiene más ejes — <strong>continuidad</strong>, <strong>familia</strong>,
<strong>armonía</strong> y <strong>tensión</strong>. Aquí la forma es un canal, y también
<i>ocurre</i>.
</p>
</header>
<!-- ── 1 · Continuidad ──────────────────────────────────────────────── -->
<section class="section">
<div class="sec-head">
<h2 class="sec-title">Continuidad</h2>
<p class="sec-note">
El arco circular tiene un quiebre donde el lado encuentra la curva. El <em>squircle</em>
(superelipse) fluye sin costura — lo que hace premium a iOS. En Eidos es un token:
<code>--shape-smoothing</code>. Muévelo.
</p>
</div>
<div class="cont">
<div class="cont-pair">
<figure class="cont-item">
<div class="cont-box" data-shape="rounded"></div>
<figcaption><b>rounded</b> · arco · superellipse(1)</figcaption>
</figure>
<figure class="cont-item">
<div class="cont-box cont-live" data-shape="continuous" style="--shape-smoothing: {smoothing}"></div>
<figcaption><b>continuous</b> · superellipse({smoothing.toFixed(2)})</figcaption>
</figure>
</div>
<div class="dial">
<label class="dial-row">
<span class="dial-label">suavizado</span>
<input type="range" min="1" max="3.2" step="0.05" bind:value={smoothing} />
<span class="dial-val">{smoothing.toFixed(2)}</span>
</label>
<code class="dial-code">corner-shape: superellipse(var(--shape-smoothing));</code>
</div>
</div>
</section>
<!-- ── 2 · Familias ─────────────────────────────────────────────────── -->
<section class="section">
<div class="sec-head">
<h2 class="sec-title">Familias</h2>
<p class="sec-note">
Una sola mecánica (<code>corner-shape</code> vía <code>data-shape</code>) cubre cuatro
familias perceptuales. Material solo tiene <code>cut</code>; Apple solo
<code>continuous</code>. Aquí, las cuatro.
</p>
</div>
<div class="fams">
{#each FAMILIES as f}
<figure class="fam">
<div class="fam-box" data-shape={f.id}></div>
<figcaption><b>{f.label}</b><span>{f.note}</span></figcaption>
</figure>
{/each}
</div>
</section>
<!-- ── 3 · Anidado ──────────────────────────────────────────────────── -->
<section class="section">
<div class="sec-head">
<h2 class="sec-title">Armonía anidada</h2>
<p class="sec-note">
Radios concéntricos: el interior = exterior − gap, así las esquinas quedan
<em>paralelas</em>. Sin esto, una tarjeta dentro de otra desencaja. Solo SwiftUI lo tiene;
aquí es <code>data-shape-nest</code>. Apágalo y míralo romperse.
</p>
</div>
<div class="nest-stage">
<div class="nest-outer" data-shape="continuous" style="--shape-outer-radius: var(--radius-xl)">
<div class="nest-row">
{#if nest}
<div class="nest-inner" data-shape="continuous" data-shape-nest></div>
<div class="nest-inner" data-shape="continuous" data-shape-nest></div>
{:else}
<div class="nest-inner nest-naive" data-shape="continuous"></div>
<div class="nest-inner nest-naive" data-shape="continuous"></div>
{/if}
</div>
</div>
<label class="nest-toggle">
<input type="checkbox" bind:checked={nest} />
<span>radios concéntricos {nest ? '· on' : '· off (radio fijo, desencaja)'}</span>
</label>
</div>
</section>
<!-- ── 4 · Ocurre (eventful · la firma) ─────────────────────────────── -->
<section class="section">
<div class="sec-head">
<h2 class="sec-title">La forma <em>ocurre</em></h2>
<p class="sec-note">
Púlsalo: la esquina <em>se afirma</em> bajo el dedo (la continuidad se cuadra un instante)
— a la vez que baja de escala y se aplana la sombra. Un solo evento, tres canales: la
<em>firma cross-modal</em>. (Solo el botón <code>continuous</code> morfea; el otro es el
control.)
</p>
</div>
<div class="press-row">
<button
class="press-btn"
data-shape="continuous"
data-event-family={pressed ? 'contact' : undefined}
data-event-phase={pressed ? 'active' : undefined}
onanimationend={() => (pressed = false)}
onpointerdown={firePress}
>continuous — pulsa</button>
<button class="press-btn press-ctrl" data-shape="rounded">rounded — sin morph</button>
</div>
</section>
<footer class="footnote">
<p>
<strong>El primer squircle-como-token de la web.</strong> Todo el campo (Tailwind · shadcn ·
Chakra · Mantine · Ant · Radix · Carbon · Fluent · Spectrum · Polaris) es escala de magnitud
+ arco + estático. La continuidad solo existía en Apple (plataforma); el morph solo en
Material (ad-hoc). Eidos reúne continuidad + familias + anidado + eventful como tokens
abiertos — magnitud intacta en <code>--radius-*</code>, degradando al arco donde no hay
<code>corner-shape</code>. Runtime: <code>applyShape()</code>.
</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: 50ch; font-size: var(--font-size-lg); line-height: 1.5; color: var(--color-content-secondary); margin: 0; text-wrap: pretty; }
.mast-lede i { font-style: italic; }
.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-title em { font-style: italic; color: var(--color-primary-text, var(--color-content-primary)); }
.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 · Continuidad */
.cont { display: grid; grid-template-columns: 1fr; gap: clamp(1.5rem, 4vw, 3rem); align-items: center; }
@media (min-width: 46rem) { .cont { grid-template-columns: 1.2fr 1fr; } }
.cont-pair { display: flex; gap: clamp(1.5rem, 4vw, 3rem); justify-content: center; }
.cont-item { margin: 0; display: flex; flex-direction: column; gap: 1rem; align-items: center; }
.cont-box { inline-size: clamp(8rem, 20vw, 11rem); aspect-ratio: 1; border-radius: 38px; background: linear-gradient(145deg, var(--color-primary-solid), color-mix(in oklab, var(--color-primary-solid) 40%, #12a594)); }
.cont-item figcaption { font-family: var(--font-family-mono); font-size: var(--font-size-xs); color: var(--color-content-muted); }
.cont-item figcaption b { color: var(--color-content-primary); font-weight: 600; }
.dial { display: flex; flex-direction: column; gap: 1rem; }
.dial-row { display: flex; align-items: center; gap: 1rem; }
.dial-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); inline-size: 5rem; }
.dial-row input { flex: 1; accent-color: var(--color-primary-solid); }
.dial-val { font-family: var(--font-family-mono); font-size: var(--font-size-sm); color: var(--color-content-primary); inline-size: 3rem; text-align: end; }
.dial-code { display: block; font-family: var(--font-family-mono); font-size: var(--font-size-xs); padding: 0.85rem 1rem; border-radius: var(--radius-md); background: var(--color-surface-muted); color: var(--color-content-secondary); word-break: break-all; }
/* 2 · Familias */
.fams { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr)); gap: clamp(1.25rem, 3vw, 2rem); }
.fam { margin: 0; display: flex; flex-direction: column; gap: 1rem; align-items: center; }
.fam-box { inline-size: 100%; aspect-ratio: 1.4; border-radius: 32px; background: linear-gradient(145deg, var(--color-surface-raised), var(--color-surface-overlay)); border: 1px solid var(--rule); box-shadow: var(--depth-raised-shadow); }
.fam figcaption { display: flex; flex-direction: column; gap: 0.2rem; text-align: center; }
.fam figcaption b { font-family: var(--font-family-mono); font-size: var(--font-size-sm); color: var(--color-content-primary); }
.fam figcaption span { font-size: var(--font-size-xs); color: var(--color-content-muted); }
/* 3 · Anidado */
.nest-stage { display: flex; flex-direction: column; gap: 1.5rem; align-items: center; }
.nest-outer { inline-size: min(30rem, 100%); padding: var(--space-2); border-radius: var(--radius-xl); background: var(--color-surface-overlay); border: 1px solid var(--rule); box-shadow: var(--depth-overlay-shadow); }
.nest-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-2); }
.nest-inner { aspect-ratio: 1.6; background: linear-gradient(145deg, var(--color-primary-solid), color-mix(in oklab, var(--color-primary-solid) 55%, transparent)); }
/* naive = fixed inner radius that ignores the outer/gap → corners visibly desync */
.nest-naive { border-radius: var(--radius-xl); }
.nest-toggle { display: flex; align-items: center; gap: 0.6rem; font-family: var(--font-family-mono); font-size: var(--font-size-xs); color: var(--color-content-muted); cursor: pointer; }
.nest-toggle input { accent-color: var(--color-primary-solid); }
/* 4 · Ocurre */
.press-row { display: flex; flex-wrap: wrap; gap: 1.5rem; }
.press-btn {
font-family: var(--font-family-primary);
font-size: var(--font-size-md);
font-weight: 600;
padding: 1rem 2rem;
border-radius: 28px;
border: 1px solid color-mix(in srgb, var(--color-primary-solid) 50%, var(--rule));
background: linear-gradient(145deg, var(--color-surface-raised), var(--color-surface-overlay));
color: var(--color-content-primary);
box-shadow: var(--depth-raised-shadow);
cursor: pointer;
will-change: transform, border-radius;
}
.press-ctrl { opacity: 0.85; }
@media (prefers-reduced-motion: reduce) { .press-btn { animation: none !important; } }
.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.