|
|
<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>
|