Primera de las ocho del plan, en castellano y DERIVADA del codigo vivo: las
tablas leen SEMA_MAP y el resolver real, nunca una transcripcion. Una pagina
que copiara el catalogo envejeceria al primer gain que alguien tocara — que
es exactamente la deriva que este subsistema se reconstruyo para quitar.
SIETE SECCIONES, ordenadas para responder «no se como funciona esto despues
de tantas correcciones»: (1) en una frase — un componente dice una palabra;
(2) el catalogo, los 16 nombres sonando al pulsarlos, agrupados por la FORMA
de cada entrada y no por una lista a mano; (3) el orden con los numeros de
verdad, tabla viva de familia x nombre x intent capa a capa; (4) que puede
llevar un .wav; (5) que no te toca escribir; (6) las correcciones y por que
—la historia ES la explicacion, ocultarla es lo que dejo al autor sin saber
como funciona—; (7) el tema.
EL BLOQUEANTE, PRIMERO. El interruptor Sound del topbar no silenciaba NADA:
el layout no pasaba `preferences`, asi que los canales se quedaban en 'full'
y el switch solo movia el intent de prefs (que gobierna el lado visual).
Publicar paginas que suenan con un mute que miente es peor que no
publicarlas. Ahora pasa un objeto VIVO — ambos canales leen
`preferences.{sound,haptic}` por emision, asi que mutarlo silencia al
instante sin re-registrar nada. Cierra §3.3 de la cola.
MEDIDO EN NAVEGADOR, no supuesto: 7 secciones, 17 chips, la rejilla y los
chips con su CSS aplicado, y la tabla de capas moviendose de verdad — con
`fulfill` el gain pasa de 0.05 a 0.1, el pitch de 700 a 1000 y el contour de
flat a ascending. Es decir: la pagina DEMUESTRA en vivo que el intent
sobrevive al nombre, que es la tesis del rediseño entero.
⚠️ NO he podido hacer captura: el panel del navegador no esta visible en esta
sesion. Verificado por arbol de accesibilidad, consola y estilos computados
— pero no lo he MIRADO. Queda pendiente tu ojo.
⚠️ Corregido de paso un fallo propio: la sonda del resolver usaba `document`
en el render de servidor y la pagina daba 500. Ahora cae a un stub en SSR.
El nav estrena seccion «El sistema perceptual». Quedan las otras siete
paginas, y los seis enlaces muertos que Foundations ya declaraba
(/uix/architecture, /uix/tokens, /uix/morfo, /uix/sema, /uix/eidos,
/uix/getting-started) siguen sin pagina.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-dir-prefs
parent
e0265bed95
commit
d161cedccf
@ -0,0 +1,333 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* `/uix/docs/sound` — how the sound channel actually works.
|
||||
*
|
||||
* DERIVED, NEVER TRANSCRIBED. Every table on this page reads the live
|
||||
* `SEMA_MAP` and the real `resolveSignature`. A page that copied the
|
||||
* catalogue would go stale the first time someone edited a gain — which is
|
||||
* exactly the drift this whole subsystem was rebuilt to remove.
|
||||
*
|
||||
* Audible: the ▶ buttons emit through the real engine, so the topbar's
|
||||
* Sound switch silences them (it now passes `preferences` to the channels).
|
||||
*/
|
||||
import { getActiveUix } from '$active-uix';
|
||||
import { SEMA_MAP, resolveSignature, type SoundName } from '$uix/sema';
|
||||
|
||||
const uix = getActiveUix();
|
||||
|
||||
const NAMES = Object.keys(SEMA_MAP.sounds) as SoundName[];
|
||||
const FAMILIES = Object.keys(SEMA_MAP.families) as (keyof typeof SEMA_MAP.families)[];
|
||||
const INTENTS = Object.keys(SEMA_MAP.intents) as (keyof typeof SEMA_MAP.intents)[];
|
||||
|
||||
/** Groups, read off the catalogue's own shape — no hand-kept list. */
|
||||
function groupOf(name: string): 'level' | 'direction' | 'character' | 'sample' {
|
||||
const entry = SEMA_MAP.sounds[name] as Record<string, unknown>;
|
||||
if (entry.kind === 'sample') return 'sample';
|
||||
if ('contour' in entry && 'pitch' in entry) return 'direction';
|
||||
return Object.keys(entry).length === 1 ? 'level' : 'character';
|
||||
}
|
||||
|
||||
const GROUP_LABEL = {
|
||||
level: 'Nivel — cuánto pesa',
|
||||
direction: 'Dirección — sube o baja',
|
||||
character: 'Carácter — el timbre del gesto',
|
||||
sample: 'Muestras — un fichero, no una receta'
|
||||
} as const;
|
||||
|
||||
// ── The live probe ───────────────────────────────────────────────────────
|
||||
let family = $state<string>('commit');
|
||||
let sound = $state<string>('soft');
|
||||
let intent = $state<string>('neutral');
|
||||
|
||||
let stage = $state<HTMLElement | null>(null);
|
||||
|
||||
/**
|
||||
* Resolve with the REAL resolver, layer by layer, so the table below shows
|
||||
* the same arithmetic the engine does rather than a retelling of it.
|
||||
*/
|
||||
function resolveAt(step: 'base' | 'name' | 'intent') {
|
||||
// SSR-safe: the resolver only needs a target to run `matches()` against,
|
||||
// and there is no DOM on the server. A stub keeps the table rendering
|
||||
// server-side (the numbers are the same — the cascade below matches `*`).
|
||||
const target = (stage ??
|
||||
(typeof document !== 'undefined'
|
||||
? document.createElement('span')
|
||||
: ({ matches: () => true, closest: () => null } as unknown))) as HTMLElement;
|
||||
const cascade = step === 'base' ? [] : [{ selector: '*', sound: sound as SoundName }];
|
||||
return resolveSignature(
|
||||
{
|
||||
name: 'probe',
|
||||
family: family as never,
|
||||
target,
|
||||
...(step === 'intent' && intent !== 'neutral' ? { intent: intent as never } : {})
|
||||
},
|
||||
{ map: SEMA_MAP, cascade: cascade as never }
|
||||
).sound;
|
||||
}
|
||||
|
||||
const layers = $derived([
|
||||
{
|
||||
label: '1 · base de familia',
|
||||
tone: 'La identidad: su timbre, su altura.',
|
||||
sig: resolveAt('base')
|
||||
},
|
||||
{
|
||||
label: `1.5 · el nombre «${sound}»`,
|
||||
tone: 'Lo único que escribe el componente.',
|
||||
sig: resolveAt('name')
|
||||
},
|
||||
{
|
||||
label: `2 · intent «${intent}»`,
|
||||
tone: 'La carga evaluativa. Llega la última.',
|
||||
sig: resolveAt('intent')
|
||||
}
|
||||
]);
|
||||
|
||||
const AXES = ['gain', 'pitch', 'contour', 'centroid', 'roughness', 'duration'] as const;
|
||||
|
||||
function play(name: string) {
|
||||
if (!stage) return;
|
||||
void uix.events?.emit({
|
||||
name: 'probe',
|
||||
family: family as never,
|
||||
target: stage,
|
||||
...(intent !== 'neutral' ? { intent: intent as never } : {})
|
||||
});
|
||||
void name;
|
||||
}
|
||||
|
||||
function playName(name: string) {
|
||||
sound = name;
|
||||
play(name);
|
||||
}
|
||||
|
||||
/** The two sample entries, for the §4 A/B. */
|
||||
const sampleNames = NAMES.filter((n) => groupOf(n) === 'sample');
|
||||
</script>
|
||||
|
||||
<div data-uix-canvas-inner>
|
||||
<div data-uix-eyebrow>Sema · el canal del sonido</div>
|
||||
<h1 data-uix-page-title>Un componente dice una palabra. Nada más.</h1>
|
||||
<p data-uix-page-lede>
|
||||
Toda la superficie de autoría del sonido es <code>sound: 'soft'</code> — un nombre del catálogo,
|
||||
o <code>SILENT</code>. Que ese nombre sea una receta sintetizada o un
|
||||
<code>.wav</code> no es asunto del componente: es del catálogo. Esta página está
|
||||
<strong>derivada del código vivo</strong>, no transcrita: las tablas leen
|
||||
<code>SEMA_MAP</code> y el resolver real.
|
||||
</p>
|
||||
<div data-uix-page-meta>
|
||||
<span data-uix-meta-pill><span data-uix-meta-key>nombres</span>{NAMES.length}</span>
|
||||
<span data-uix-meta-pill><span data-uix-meta-key>familias</span>{FAMILIES.length}</span>
|
||||
<span data-uix-meta-pill><span data-uix-meta-key>intents</span>{INTENTS.length}</span>
|
||||
</div>
|
||||
|
||||
<!-- The emit target. Everything audible on this page fires against it. -->
|
||||
<div bind:this={stage} data-uix-sound-stage aria-hidden="true"></div>
|
||||
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>1 · En una frase</h2>
|
||||
<p data-uix-section-desc>
|
||||
Un pack de componente no autora una firma: <strong>nombra</strong> una entrada del catálogo. El
|
||||
tipo no acepta otra cosa, así que la ley no es una convención que alguien deba recordar — la impone
|
||||
el compilador.
|
||||
</p>
|
||||
<pre data-uix-code><code
|
||||
>{`// src/uix/sema/components/textarea.ts
|
||||
export const textareaSema: Sema = {
|
||||
name: 'textarea',
|
||||
cascade: [
|
||||
{ selector: onInput({ eventName: 'commit-submit' }), sound: 'soft' },
|
||||
{ selector: onInput({ eventName: 'signal-warn-count-overflow' }), sound: 'subtle' }
|
||||
]
|
||||
};`}</code
|
||||
></pre>
|
||||
</section>
|
||||
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>2 · El catálogo</h2>
|
||||
<p data-uix-section-desc>
|
||||
Los {NAMES.length} nombres que existen. Pulsa cualquiera para oírlo con la familia y el intent que
|
||||
tengas seleccionados abajo. <em>Requiere el interruptor «Sound» del topbar encendido.</em>
|
||||
</p>
|
||||
{#each ['level', 'direction', 'character', 'sample'] as const as group}
|
||||
{@const names = NAMES.filter((n) => groupOf(n) === group)}
|
||||
{#if names.length > 0}
|
||||
<h3 data-uix-sound-group>{GROUP_LABEL[group]}</h3>
|
||||
<div data-uix-sound-grid>
|
||||
{#each names as name}
|
||||
<button
|
||||
type="button"
|
||||
data-uix-sound-chip
|
||||
data-active={name === sound ? '' : undefined}
|
||||
onclick={() => playName(name)}
|
||||
>
|
||||
<span data-uix-sound-chip-name>{name}</span>
|
||||
<span data-uix-sound-chip-play>▶</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>3 · El orden, con los números de verdad</h2>
|
||||
<p data-uix-section-desc>
|
||||
Esto es lo que hace legible todo lo demás. La familia pone la
|
||||
<strong>identidad</strong>, el nombre pone
|
||||
<strong>nivel, dirección y carácter</strong>, y el
|
||||
<strong>intent llega el último</strong> — por eso su carga evaluativa siempre se oye. Cambia los
|
||||
tres y mira cómo se mueve cada eje.
|
||||
</p>
|
||||
|
||||
<div data-uix-controls>
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>familia</span>
|
||||
<select bind:value={family}>
|
||||
{#each FAMILIES as f}<option value={f}>{f}</option>{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>nombre</span>
|
||||
<select bind:value={sound}>
|
||||
{#each NAMES as n}<option value={n}>{n}</option>{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>intent</span>
|
||||
<select bind:value={intent}>
|
||||
{#each INTENTS as i}<option value={i}>{i}</option>{/each}
|
||||
</select>
|
||||
</label>
|
||||
<button type="button" data-uix-sound-chip onclick={() => play(sound)}>
|
||||
<span data-uix-sound-chip-name>oír el resultado</span>
|
||||
<span data-uix-sound-chip-play>▶</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div data-uix-sound-table>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>capa</th>
|
||||
{#each AXES as axis}<th>{axis}</th>{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each layers as layer}
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<span data-uix-sound-layer>{layer.label}</span>
|
||||
<span data-uix-sound-layer-note>{layer.tone}</span>
|
||||
</th>
|
||||
{#each AXES as axis}
|
||||
<td>{layer.sig ? (layer.sig[axis] ?? '—') : '—'}</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p data-uix-section-desc>
|
||||
Si la familia no declara sonido (<code>handle</code>, <code>sustain</code>,
|
||||
<code>delegate</code>) las tres filas salen vacías: ahí el gesto suena por otra vía, no por el
|
||||
catálogo.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>4 · Qué puede llevar un <code>.wav</code></h2>
|
||||
<p data-uix-section-desc>
|
||||
Esto es física, no política. <code>playSample</code> lee
|
||||
<strong>exactamente dos campos</strong> de la firma: <code>sampleUrl</code> y
|
||||
<code>gain</code>. No hay <code>playbackRate</code> ni <code>detune</code>. Luego sobre una
|
||||
muestra el intent <strong>sólo puede mover el volumen</strong>: un
|
||||
<code>risk</code> es indistinguible de un <code>neutral</code>, porque lo único que aporta es
|
||||
<code>roughness</code> y la ruta de sample lo ignora.
|
||||
</p>
|
||||
<p data-uix-section-desc>
|
||||
Compruébalo: elige <code>{sampleNames[0]}</code> arriba y ve cambiando el intent. El
|
||||
<code>gain</code> se mueve; el resto de ejes se resuelven pero no se oyen.
|
||||
</p>
|
||||
<p data-uix-section-desc>
|
||||
<strong>Consecuencia de diseño</strong>: una muestra que deba llevar carga evaluativa necesita
|
||||
<strong>un fichero por intent</strong>. Es lo que hace la familia <code>signal</code> con
|
||||
<code>ping</code>
|
||||
frente a <code>error</code>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>5 · Lo que no te toca escribir</h2>
|
||||
<p data-uix-section-desc>
|
||||
Ningún parámetro. Nunca. Ni un <code>gain</code>, ni un <code>centroid</code>, ni un
|
||||
<code>op: 'add'</code>. Si un componente necesita algo que el catálogo no lleva, eso es una
|
||||
decisión <strong>del catálogo</strong>: se toma una vez, en un fichero, y el siguiente
|
||||
componente la reutiliza.
|
||||
</p>
|
||||
<pre data-uix-code><code
|
||||
>{`// ✗ lo que ya no compila
|
||||
sound: { gain: { op: 'add', value: -0.25 }, centroid: { op: 'add', value: 2800 } }
|
||||
|
||||
// ✓ lo único que se escribe
|
||||
sound: 'settle'`}</code
|
||||
></pre>
|
||||
<p data-uix-section-desc>
|
||||
Añadir un sonido es <strong>añadir un nombre al catálogo</strong>, jamás ensanchar una regla.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>6 · Por qué es así — las correcciones</h2>
|
||||
<p data-uix-section-desc>
|
||||
El diseño actual no se entiende sin lo que reemplaza. Hasta el 6 de agosto de 2026 una regla
|
||||
de pack llevaba una bolsa abierta de ocho ejes continuos. Medido sobre los 71 packs:
|
||||
</p>
|
||||
<ul data-uix-prose-list>
|
||||
<li>
|
||||
<strong>214 reglas autoraban 33 firmas distintas</strong>, y <strong>92</strong> de ellas (el
|
||||
43 %) no decían más que un volumen.
|
||||
</li>
|
||||
<li>
|
||||
<strong>69 reglas pisaban el <code>contour</code> del intent</strong> — y no podían hacer otra
|
||||
cosa: es un enum, sólo admite reemplazo.
|
||||
</li>
|
||||
<li>
|
||||
Cinco packs llevaban un <code>centroid +2800</code> copiado que dejaba su
|
||||
<code>commit</code> en 4600 Hz, más brillante que <code>signal</code>, la familia más
|
||||
brillante del mapa. Era el residuo aritmético de una muestra borrada.
|
||||
</li>
|
||||
</ul>
|
||||
<p data-uix-section-desc>
|
||||
Y la pieza que lo explica todo: <strong
|
||||
>los packs corrían los últimos y en modo reemplazo</strong
|
||||
>, así que el sonido elegido por el componente borraba el perfil evaluativo que el intent
|
||||
acababa de poner — un diálogo con <code>threat</code> sonaba exactamente igual que uno neutro.
|
||||
Existían tres mecanismos para vigilar esa inversión. Los tres desaparecieron: con el nombre
|
||||
<em>debajo</em>
|
||||
del intent, la inversión no está prohibida, es <strong>inexpresable</strong>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>7 · Y el tema</h2>
|
||||
<p data-uix-section-desc>
|
||||
El catálogo vive dentro de <code>SEMA_MAP</code>, que es la estructura que un tema direcciona.
|
||||
Así que la voz del sistema se puede cambiar en caliente y revertir:
|
||||
</p>
|
||||
<pre data-uix-code><code
|
||||
>{`eidos.applyTheme({ color: '#3b5bdb', sound: { soft: { gain: 0.08 } } });
|
||||
eidos.clearTheme(); // revierte los siete ejes
|
||||
|
||||
// o sólo el eje perceptual
|
||||
uix.events.applySounds({ soft: { gain: 0.08 } });`}</code
|
||||
></pre>
|
||||
<p data-uix-section-desc>
|
||||
Lo que <strong>no</strong> se abre es el vocabulario: <code>SoundName</code> es
|
||||
<code>keyof</code> del catálogo, así que un tema cambia
|
||||
<em>a qué suena</em> un nombre y no puede inventar uno que ningún componente pueda referenciar.
|
||||
La voz se abre; las palabras no.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
Loading…
Reference in new issue