fix(sema): los toggles recuperan voz, y el silencio deja de sintetizarse — muere el prefijo form.

Tres cosas que el mismo hilo destapo, de la mas visible a la mas de fondo.

1) LOS TOGGLES SUENAN OTRA VEZ (directiva de autor 2026-08-05)

switch, toggle y toggle-group llevaban SILENCIADOS desde el 2026-05-26: su pack
aplicaba un tuning que restaba exactamente el gain base de la familia commit
(0.3 - 0.3 = 0). Medido en navegador: pico 0.0001, el suelo de la rampa. Un
silencio por defecto no se percibe como sobriedad sino como componente roto.

Pasan a `commit.medium` (gain 0.1): audible, y un tercio de una pulsacion de
boton (contact base 0.25) para que un flip no pese mas que una activacion. Los
deltas de intent siguen montando encima (threat +0.1, fulfill +0.05). El
haptico ligero no cambia. Medido despues: switch pico 0.1.

Conservan el silencio, con su justificacion intacta, solo los dos donde el
silencio ES la firma: tooltip (revela al pasar el raton, sonaria en cada cruce)
y media-player (su propia salida ES audio — D-AP2.7, decision del hilo de audio).

2) EMITIR SILENCIO YA NO CUESTA

`SoundChannel.handle` cortaba DESPUES de resolver la firma, asi que un tuning
silenciador levantaba dos osciladores, un filtro y una envolvente programada
para no sonar, en cada interaccion. Ahora corta antes de sintetizar cuando la
ganancia resuelta es <= 0. El corte mira la ganancia RESUELTA, no el tuning, asi
que un intent que la devuelve por encima de 0 (threat, fulfill) suena igual.
Fijado con test propio, verificado en ROJO quitando el corto.

3) EL AFINADO SE NOMBRA POR LA FAMILIA QUE MODIFICA

El catalogo mezclaba tres ejes en el primer segmento —familia (emerge.*,
contact.silent), componente (tooltip.*, tabs.*) y la categoria huerfana form.*—
sin regla declarada y sin guard que mirara. `form.commit.soft/subtle` entro el
2026-05-19 (09e261878, el commit que CREA sounds.ts) nombrando con verdad a
siete controles de formulario; el 2026-05-26, 2d562f378 lo extendio a menus y
arboles en la misma pasada que escribia la doctrina: 7 -> 29 consumidores en un
commit, 50 hoy, de los que exactamente UNO era el Form. El D.6 de ese commit
llego a poner el nombre correcto —commit.select— en la linea que usaba la clave
incorrecta.

  form.commit.soft   -> commit.soft
  form.commit.subtle -> commit.subtle
  form.toggle.silent -> commit.silent
  tooltip.silent     -> emerge.silent
  tabs.select.soft   -> commit.select.soft

131 ocurrencias en 68 ficheros, dirigido por `git ls-files` — nunca por recorrido
de directorio: hay 3 worktrees vivos y build/ con 449 apariciones mas que un sed
recursivo habria corrompido en silencio.

La cola queda LIBRE a proposito: media-player aplica el silenciador al
commit-set de un slider, no a un toggle — la cola describe la FIRMA, no el
evento, asi que cerrarla contra el vocabulario de verbos fabricaria otra
mentira. Por eso emerge.* no se toca pese a que `exit` no sea verbo canonico.

Quedan tres silenciadores, uno por familia con base audible, con el mismo
nombre: contact.silent (-0.25), commit.silent (-0.3), emerge.silent (-0.2).
Eso convierte la coherencia en INVARIANTE:

- `sounds-grammar.test.ts` (guard nuevo): cabeza ∈ SEMA_FAMILIES; forma
  {family}.{tail}; y todo {family}.silent resta EXACTAMENTE el gain base de su
  familia, leido de sema-map.ts —nunca copiado—. Verificado en ROJO: el guard
  calculo solo el -0.4 esperado de signal.
- Ley escrita en sema.md § Tuning naming shapes, junto a la seccion hermana que
  ya legislaba los nombres de EVENTO.
- vocabularies.md —"the ONE place the closed sets are spelled out"— pasa a
  enumerar los 14 afinados por familia; el generador los extrae del const y
  docs:check vigila su frescura.
- D.5 gana las dos notas: el renombrado y la reversion del silencio.

VERIFICACION: check 75 (linea base exacta) · sema 198/198 · docs:check 0/615 ·
navegador antes/despues (switch 0.0001 -> 0.1; button 0.25 sin tocar) · los 25
avisos de prettier entre lo tocado son deriva YA en HEAD, ninguno mio.

Cronica NO reescrita (4 ficheros): AUDIT-blocks-2026-08-01, PLAN-audio-player,
handoffs-claude-md y audit/components/date-field registran lo que ERA cierto.
DESVIACION DECLARADA: sema/components/chronos.ts entra con 6 sustituciones
mecanicas —chronos es de escritura excluida, pero dejarlo fuera rompia el
type-check del repo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
alpha-0.1-dir-prefs
dev 2 months ago
parent 05add971c7
commit d05ecc1aa8

@ -384,6 +384,41 @@ sample('/sounds/uix/dialog-fail.wav', sound('handle.release.soft'), { preload: t
fetch/decode fails, falls back to the synthetic signature. The rule is that
components reference names; URLs and parameters live in one place.
#### Tuning naming shapes
A `SOUND_TUNINGS` key takes one canonical shape:
```
{family}.{tail}
```
- **`{family}`** — the sema family whose base signature the tuning modifies.
Closed set (`SEMA_FAMILIES`), and the only axis that is both true and
mechanically checkable: a component name is neither. Never a component,
never a UI category.
- **`{tail}`** — free prose describing the resulting SIGNATURE, one or more
lowercase segments (`soft`, `subtle`, `silent`, `exit.deep`, `select.soft`).
Deliberately NOT the triggering event: `commit.silent` silences a slider's
`commit-set` in `media-player.ts` as legitimately as a switch's
`commit-toggle`, because it names the silence, not the gesture. This is the
one place where the tuning vocabulary and the *event* vocabulary
([Naming shapes](#naming-shapes)) deliberately diverge.
**Silencer invariant.** A `{family}.silent` tuning subtracts EXACTLY that
family's base gain, so neutral lands at 0 while the intent deltas (`threat`
+0.1, `fulfill` +0.05) still surface — silence by default, never silence
absolute. Three exist, one per family with an audible base:
`contact.silent` (−0.25), `commit.silent` (−0.3), `emerge.silent` (−0.2).
The families with no `base.sound` (`handle`, `sustain`, `delegate`) can have
no silencer: there is nothing to cancel.
Both rules are enforced by `src/uix/sema/sounds-grammar.test.ts`, which reads
the families from `event.ts` and the base gains from `sema-map.ts` rather
than restating them. The guard exists because `form.*` named seven form
controls truthfully on 2026-05-19, was extended to menus and trees a week
later, and reached 50 packs — one of which was the Form — before anyone
noticed, since no check ever looked at these keys.
### Channels and signatures
| Channel | Consumed slice | Behavior when not applicable |

@ -84,6 +84,22 @@ The canonical verb set an event may use for each family (`SEMA_VERBS`,
| `sustain` | `start` · `progress` · `loading` · `waiting` · `syncing` · `processing` · `streaming` · `pending` · `retrying` · `upload` · `end` |
| `delegate` | `offer` · `plan` · `authorize` · `act` · `review` · `escalate` · `return` |
## Sound tunings (14)
Parametric deltas over a family base signature (`SOUND_TUNINGS`,
`src/uix/sema/sounds.ts`). A key is `{family}.{tail}`: the FIRST segment is
the sema family whose base the tuning modifies — never a component — and the
tail describes the resulting signature, not the triggering event. Every
`{family}.silent` subtracts exactly that family base gain, so neutral lands
at 0 while intent deltas still surface. Enforced by
`src/uix/sema/sounds-grammar.test.ts`.
| Family | Tunings |
| --- | --- |
| `contact` | `contact.silent` |
| `emerge` | `emerge.soft` · `emerge.medium` · `emerge.strong` · `emerge.exit` · `emerge.exit.deep` · `emerge.exit.soft` · `emerge.dismiss.passive` · `emerge.silent` |
| `commit` | `commit.soft` · `commit.subtle` · `commit.select.soft` · `commit.medium` · `commit.silent` |
## Intents (6)
The evaluative axis (`INTENTS`, `src/uix/intent.ts`). `commit` and `signal`

@ -314,10 +314,37 @@ type SemaExpressionMode = 'pack' | 'family-default' | 'delegated' | 'none';
- **Status**: **PROJECT_CANON** (decisión arquitectural sin necesidad de pasar al libro)
- **Caso**: `switch`, `toggle`, `toggle-group` emiten `commit-toggle` en bucle (settings panels, toolbars, segmented controls). Sin tuning, heredan el `family.commit.base.gain = 0.3` que es demasiado para una sesión sostenida.
- **Solución**: nuevo tuning `form.toggle.silent = { gain: { op: 'add', value: -0.3 } }` en `src/uix/sema/sounds.ts`. Cancela exactamente el `gain` del family base, dejando el default en 0 (silencio). Los intent.deltas que añaden gain (`threat: +0.1`, `fulfill: +0.05`) siguen surgiendo, así un toggle destructivo sí emite señal audible.
- **Solución**: nuevo tuning `commit.silent = { gain: { op: 'add', value: -0.3 } }` en `src/uix/sema/sounds.ts`. Cancela exactamente el `gain` del family base, dejando el default en 0 (silencio). Los intent.deltas que añaden gain (`threat: +0.1`, `fulfill: +0.05`) siguen surgiendo, así un toggle destructivo sí emite señal audible.
- **Haptic**: tap leve (`intensity: 0.3, duration: 12, delay: 0`) sustituye el tap medio del family. Replaza el `kind` para que la háptica no oscile con el intent — la carga evaluativa de un toggle se lee en `data-color` + (selectivamente) sonido, no en háptica fluctuante.
- **Doctrina**: el libro habla de "componentes de baja intensidad" (cap. 22) — esta es la materialización runtime. NO ir al libro: es decisión de tuning, no de gramática.
- **Packs concretos**: `src/uix/sema/components/{switch,toggle,toggle-group}.ts` — los tres comparten la misma firma porque comparten rol UX (un press → un flip).
- **REVERTIDO 2026-08-05 — los toggles recuperan voz (directiva de autor)**: el
silencio de arriba se retira para `switch`, `toggle` y `toggle-group`, que pasan a
**`commit.medium` (gain 0.1)** — audible, y un tercio de una pulsación de botón
(`contact` base 0.25) para que un flip nunca pese más que una activación. Los deltas
de intent siguen montando encima (`threat` +0.1, `fulfill` +0.05). Razón: un silencio
por defecto se percibe como componente roto, no como sobriedad; la fatiga se combate
bajando el nivel, no anulándolo. El háptico ligero de abajo NO cambia.
Se conserva el silencio, con su justificación intacta, sólo donde el silencio ES la
firma: `tooltip` (revela al pasar el ratón — sonaría en cada cruce) y `media-player`
(su propia salida ES audio, D-AP2.7).
Además, **emitir silencio dejó de costar**: `SoundChannel.handle` corta antes de
sintetizar cuando la ganancia resuelta es ≤ 0 (`chans/sound.ts`), así que un
`{family}.silent` ya no levanta dos osciladores, un filtro y una envolvente para no
sonar. Fijado por test, verificado en rojo.
- **Renombrado 2026-08-05 — el prefijo `form.` muere**: este tuning nació como
`form.toggle.silent` y hoy es **`commit.silent`**. El prefijo `form.` entró el
2026-05-19 (`09e261878`) nombrando con verdad a siete controles de formulario, pero
ESTE MISMO commit (`2d562f378`) lo extendió a menús y árboles vía `form.commit.subtle`
— el D.6 de abajo llegó a escribir el nombre correcto (`commit.select`) en la misma
línea que usaba la clave incorrecta. Llegó a 50 packs, de los que exactamente uno era
el componente Form. La ley queda escrita en
[`sema.md` § Tuning naming shapes](../architecture/sema.md#tuning-naming-shapes) —
**el primer segmento es la familia cuya base modifica el tuning, nunca un
componente** — y la vigila `src/uix/sema/sounds-grammar.test.ts`. Renombres del
mismo barrido: `form.commit.soft` → `commit.soft`, `form.commit.subtle` →
`commit.subtle`, `tooltip.silent` → `emerge.silent`, `tabs.select.soft` →
`commit.select.soft`. Sin cambio de comportamiento: mismos deltas, mismas ganancias.
### D.6 Packs para superficies de menú y árboles
@ -326,7 +353,7 @@ type SemaExpressionMode = 'pack' | 'family-default' | 'delegated' | 'none';
- **Solución**: packs por componente que combinan tuning emerge soft + commit subtle:
- **emerge.open (menús, expand)**: `emerge.soft` (gain 0.08) — el contenido se revela sin competir con la superficie que lo invoca.
- **emerge.close (menús, collapse)**: `emerge.exit.soft` (gain 0.05, descending) — disciplina de dirección compartida con dialog/drawer/popover.
- **commit.select (item de menú, nodo de árbol)**: `form.commit.subtle` (gain 0.03) + `tap` leve. Más sobrio que radio-group porque la cascada típica es "menú cierra + item commit + nueva superficie aparece" — tres señales en milisegundos, hay que repartir intensidad.
- **commit.select (item de menú, nodo de árbol)**: `commit.subtle` (gain 0.03) + `tap` leve. Más sobrio que radio-group porque la cascada típica es "menú cierra + item commit + nueva superficie aparece" — tres señales en milisegundos, hay que repartir intensidad.
- **Coherencia**: dropdown-menu y context-menu comparten firma idéntica (el usuario no debe aprender dos "sonidos de menú"). Tree-view y tree-grid idem. Menubar y navigation-menu se quedan en solo `commit.subtle` porque no tienen evento de open/close declarado en el morfo (la apertura es de un dropdown adyacente).
- **Caveat (`tree-view` / `tree-grid`) — RESUELTO (verificado 2026-07-12, SEM-4)**: la
emisión aterriza vía `fallbackTarget` en el elemento real (`branchEl` / `rowEl`) y los

@ -21,6 +21,7 @@ import { ARCHETYPE_VOCABULARY, ARCHETYPE_DESCRIPTIONS } from '../src/uix/morfo/t
import { PALETTE_SCALES, SIZES, SIZE_PRIMITIVE_KEYS, EIDOS_VARIANTS } from '../src/uix/eidos/lib/types';
import { SEMA_HOLDS_BY_INTENT } from '../src/uix/sema/holds';
import { SEMA_VERBS } from '../src/uix/sema/verbs';
import { SOUND_TUNINGS } from '../src/uix/sema/sounds';
import { SEMA_DURATIONS } from '../src/uix/sema/durations';
import { INTENTS } from '../src/uix/intent';
import { commonLangs } from '../src/uix/langs/common';
@ -126,6 +127,26 @@ export function generateVocabulariesDoc(): string {
}
L.push('');
// Sound tunings
const tuningKeys = Object.keys(SOUND_TUNINGS);
L.push(`## Sound tunings (${tuningKeys.length})`);
L.push('');
L.push('Parametric deltas over a family base signature (`SOUND_TUNINGS`,');
L.push('`src/uix/sema/sounds.ts`). A key is `{family}.{tail}`: the FIRST segment is');
L.push('the sema family whose base the tuning modifies — never a component — and the');
L.push('tail describes the resulting signature, not the triggering event. Every');
L.push('`{family}.silent` subtracts exactly that family base gain, so neutral lands');
L.push('at 0 while intent deltas still surface. Enforced by');
L.push('`src/uix/sema/sounds-grammar.test.ts`.');
L.push('');
L.push('| Family | Tunings |');
L.push('| --- | --- |');
for (const family of families) {
const owned = tuningKeys.filter((k) => k.split('.')[0] === family);
if (owned.length > 0) L.push(`| \`${family}\` | ${owned.map((k) => `\`${k}\``).join(' · ')} |`);
}
L.push('');
// Intents
L.push(`## Intents (${INTENTS.length})`);
L.push('');

@ -407,9 +407,9 @@ src/uix/soma/components/chronos/
Pack opcional con `cascade` construido vía `semaSelector(chronosMorfo, part, matchers)`.
Sin canales nuevos (sound + haptic son los únicos canónicos). Reusa *tunings* existentes:
- `commit-create` / `commit-update` → `soundTuning('form.commit.soft')` + `haptic: tap`.
- `commit-create` / `commit-update` → `soundTuning('commit.soft')` + `haptic: tap`.
- `commit-delete` → tuning de `loss` (descendente) + `haptic: error` suave.
- `shift-navigate` → `form.commit.subtle` ascendente/descendente según dirección.
- `shift-navigate` → `commit.subtle` ascendente/descendente según dirección.
- `handle-drag` / `handle-resize` → solo `haptic` (la familia `handle` activa haptic; **no
emitir sonido por frame** — misma doctrina que `virtual-*` `handle-scroll`).
- `emerge-open` / `emerge-close` → `emerge.soft` / `emerge.dismiss.passive`.

@ -126,7 +126,7 @@ seeding). None are blocking; each lands behind a real request.
| `commit-set` | commit · set | neutral | post | provider | Blur / Enter, once per changed value |
Typing emits no semantic event (high-frequency editing). Perceptual signature:
`form.commit.subtle` sound + `tap` haptic — identical to NumberField (both are
`commit.subtle` sound + `tap` haptic — identical to NumberField (both are
field commits). Pack: `src/uix/sema/components/mask-field.ts`.
## Anatomy

@ -126,6 +126,23 @@ describe('SoundChannel', () => {
channel.dispose();
});
it('never synthesizes a signature resolved to zero gain', async () => {
const { dom } = createDomHarness();
const audio = createAudioHarness();
(audio.ctx as { state: string }).state = 'running';
const channel = new SoundChannel({ dom, audioContextFactory: audio.factory });
// What a `{family}.silent` tuning produces: the family base cancelled
// exactly. Silence is not a sound — before this guard every such
// emission still built two oscillators, a filter and an envelope to
// render nothing audible, on every single interaction.
await channel.handle(
{ target: document.createElement('button'), name: 'x', family: 'commit' },
{ ...soundEffective, sound: { ...soundEffective.sound, gain: 0 } } as never
);
expect(audio.factory).not.toHaveBeenCalled();
channel.dispose();
});
it('reduces per signal and never moves the document master (BK-REDUCTIONS)', async () => {
const { dom } = createDomHarness();
const audio = createAudioHarness();

@ -167,6 +167,14 @@ export class SoundChannel implements Channel {
if (level === 'off') return;
const gainScale = level === 'reduce' ? SOUND_REDUCE_GAIN_FACTOR : 1;
// Silence is not a sound to synthesize. A `{family}.silent` tuning
// cancels the family base exactly so neutral lands at 0 — without this
// guard every such emission still spun up two oscillators, a filter and
// a scheduled envelope to render nothing audible, on every interaction.
// Intent deltas that push the gain back above 0 (threat, fulfill) play
// normally: the check is on the RESOLVED gain, not on the tuning.
if (typeof sig.gain === 'number' && sig.gain * gainScale <= 0) return;
// The engine never rejects — audio is ornamental. Earcons are UI sound,
// so they ride the `ui` bus (never `content`: `prefs.sound` must not
// touch the work's volume) and speak with sema's registered voice.

@ -19,12 +19,12 @@ export const calendarSema: Sema = {
cascade: [
{
selector: onPart('day', { eventName: 'commit-select' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
},
{
selector: onPart('day', { eventName: 'commit-unselect' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap' }
},
...(
@ -38,7 +38,7 @@ export const calendarSema: Sema = {
] as const
).map((part) => ({
selector: onPart(part, { eventName: 'shift-navigate' }),
sound: soundTuning('form.commit.subtle', {
sound: soundTuning('commit.subtle', {
gain: 0.18,
contour: 'ascending',
pitch: { op: 'add', value: -90 }

@ -20,7 +20,7 @@ export const chatComposerSema: Sema = {
selector: semaSelector(chatComposerMorfo, 'provider', {
eventName: 'commit-submit-send'
}),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap', intensity: 0.3, duration: 12, delay: 0 }
}
]

@ -25,12 +25,12 @@ export const chatMessageSema: Sema = {
cascade: [
{
selector: onReaction({ eventName: 'commit-select-react' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap', intensity: 0.3, duration: 12, delay: 0 }
},
{
selector: onReaction({ eventName: 'commit-unselect-react' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap', intensity: 0.3, duration: 12, delay: 0 }
}
]

@ -36,12 +36,12 @@ export const checkboxSema: Sema = {
cascade: [
{
selector: onProvider({ eventName: 'commit-toggle-check' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
},
{
selector: onProvider({ eventName: 'commit-toggle-uncheck' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap' }
}
]

@ -14,25 +14,25 @@ export const chronosSema: Sema = {
// Event landed (create / edit-save / resize-save).
{
selector: semaSelector(chronosMorfo, 'event-chip', { eventName: 'commit-save' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
},
// Event moved by drag.
{
selector: semaSelector(chronosMorfo, 'event-chip', { eventName: 'commit-move' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
},
// Event deleted (loss).
{
selector: semaSelector(chronosMorfo, 'event-chip', { eventName: 'commit-delete' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap' }
},
// Month navigation.
{
selector: semaSelector(chronosMorfo, 'provider', { eventName: 'shift-navigate' }),
sound: soundTuning('form.commit.subtle', {
sound: soundTuning('commit.subtle', {
gain: 0.18,
contour: 'ascending',
pitch: { op: 'add', value: -90 }
@ -41,11 +41,11 @@ export const chronosSema: Sema = {
// Undo / redo.
{
selector: semaSelector(chronosMorfo, 'provider', { eventName: 'commit-restore' }),
sound: soundTuning('form.commit.subtle')
sound: soundTuning('commit.subtle')
},
{
selector: semaSelector(chronosMorfo, 'provider', { eventName: 'commit-apply' }),
sound: soundTuning('form.commit.subtle')
sound: soundTuning('commit.subtle')
},
// Direct manipulation in flight — haptic only (no sound per frame).
{

@ -15,12 +15,12 @@ export const clipboardSema: Sema = {
cascade: [
{
selector: onTrigger({ eventName: 'commit-save-copy' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
},
{
selector: onTrigger({ eventName: 'commit-fail-copy' }),
sound: soundTuning('form.commit.soft', { contour: 'descending' })
sound: soundTuning('commit.soft', { contour: 'descending' })
}
]
}

@ -13,7 +13,7 @@ export const colorFieldSema: Sema = {
cascade: [
{
selector: onInput({ eventName: 'commit-set' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
}
]

@ -19,12 +19,12 @@ export const comboboxSema: Sema = {
cascade: [
{
selector: onItem({ eventName: 'commit-select' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
},
{
selector: onItem({ eventName: 'commit-unselect' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap' }
}
]

@ -36,7 +36,7 @@ export const contextMenuSema: Sema = {
},
{
selector: onItem({ eventName: 'commit-select' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap', intensity: 0.3, duration: 12, delay: 0 }
}
]

@ -40,7 +40,7 @@ export const cropperSema: Sema = {
},
{
selector: onProvider({ eventName: 'commit-crop' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
}
]

@ -28,7 +28,7 @@ export const cssFieldSema: Sema = {
cascade: [
{
selector: onProvider({ eventName: 'commit-set' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap' }
},
{
@ -44,7 +44,7 @@ export const cssFieldSema: Sema = {
},
{
selector: onInput({ eventName: 'signal-warn-invalid' }),
sound: soundTuning('form.commit.soft', {
sound: soundTuning('commit.soft', {
contour: 'descending',
pitch: { op: 'add', value: -120 }
}),

@ -14,12 +14,12 @@ export const dateFieldSema: Sema = {
cascade: [
{
selector: onInput({ eventName: 'commit-set' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
},
{
selector: onInput({ eventName: 'commit-reset' }),
sound: soundTuning('form.commit.subtle', {
sound: soundTuning('commit.subtle', {
contour: 'descending',
pitch: { op: 'add', value: -80 }
}),

@ -33,7 +33,7 @@ export const dragDropSema: Sema = {
},
{
selector: onDraggable({ eventName: 'commit-cancel' }),
sound: soundTuning('form.commit.subtle', {
sound: soundTuning('commit.subtle', {
contour: 'descending',
pitch: { op: 'add', value: -80 }
})

@ -21,7 +21,7 @@ import type { Sema } from '../sema-map';
* gain 0.05. Even softer than open because closing is usually
* immediately followed by the item-select chime OR the surface the
* user navigated to.
* - SELECT: `form.commit.subtle` (gain 0.03) + light `tap` haptic.
* - SELECT: `commit.subtle` (gain 0.03) + light `tap` haptic.
* Softer than radio-group because the close emerge and the select
* commit usually fire together — two signals in quick succession
* would double-tap perceptually if both were at 0.05.
@ -47,7 +47,7 @@ export const dropdownMenuSema: Sema = {
},
{
selector: onItem({ eventName: 'commit-select' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap', intensity: 0.3, duration: 12, delay: 0 }
}
]

@ -11,7 +11,7 @@ export const editableSema: Sema = {
cascade: [
{
selector: onProvider({ eventName: 'shift-enter-mode' }),
sound: soundTuning('form.commit.subtle', {
sound: soundTuning('commit.subtle', {
contour: 'ascending',
pitch: { op: 'add', value: 80 }
}),
@ -19,12 +19,12 @@ export const editableSema: Sema = {
},
{
selector: onProvider({ eventName: 'commit-save' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'success' }
},
{
selector: onProvider({ eventName: 'commit-cancel' }),
sound: soundTuning('form.commit.subtle', {
sound: soundTuning('commit.subtle', {
contour: 'descending',
pitch: { op: 'add', value: -80 }
}),

@ -17,17 +17,17 @@ export const fileUploadSema: Sema = {
cascade: [
{
selector: onTrigger({ eventName: 'trigger-picker' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tick' }
},
{
selector: onProvider({ eventName: 'commit-set-add' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'success' }
},
{
selector: onProvider({ eventName: 'signal-warn-reject' }),
sound: soundTuning('form.commit.soft', {
sound: soundTuning('commit.soft', {
contour: 'descending',
pitch: { op: 'add', value: -120 }
}),
@ -35,7 +35,7 @@ export const fileUploadSema: Sema = {
},
{
selector: onItem({ eventName: 'commit-remove' }),
sound: soundTuning('form.commit.subtle', {
sound: soundTuning('commit.subtle', {
contour: 'descending',
pitch: { op: 'add', value: -80 }
}),
@ -43,7 +43,7 @@ export const fileUploadSema: Sema = {
},
{
selector: onProvider({ eventName: 'commit-reset' }),
sound: soundTuning('form.commit.subtle', {
sound: soundTuning('commit.subtle', {
contour: 'descending',
pitch: { op: 'add', value: -100 }
}),

@ -11,7 +11,7 @@ export const formSema: Sema = {
cascade: [
{
selector: onProvider({ eventName: 'commit-submit' }),
sound: soundTuning('form.commit.soft', {
sound: soundTuning('commit.soft', {
contour: 'ascending',
pitch: { op: 'add', value: 80 }
}),
@ -24,7 +24,7 @@ export const formSema: Sema = {
},
{
selector: onProvider({ eventName: 'commit-reset' }),
sound: soundTuning('form.commit.subtle', {
sound: soundTuning('commit.subtle', {
contour: 'descending',
pitch: { op: 'add', value: -80 }
}),

@ -24,7 +24,7 @@ export const imageAdjustmentsSema: Sema = {
cascade: [
{
selector: onProvider({ eventName: 'commit-reset' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
}
]

@ -27,12 +27,12 @@ export const imagePickerSema: Sema = {
cascade: [
{
selector: onProvider({ eventName: 'commit-select' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
},
{
selector: onProvider({ eventName: 'commit-remove' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap' }
},
{

@ -13,12 +13,12 @@ export const listboxSema: Sema = {
cascade: [
{
selector: onItem({ eventName: 'commit-select' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
},
{
selector: onItem({ eventName: 'commit-unselect' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap' }
}
]

@ -20,7 +20,7 @@ export const maskFieldSema: Sema = {
cascade: [
{
selector: onProvider({ eventName: 'commit-set' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap' }
}
]

@ -77,19 +77,19 @@ export const mediaPlayerSema: Sema = {
// measured 2026-07-30, H-1/G-4). The subtraction is the commit family
// base (-0.3) — the tuning name comes from D.5.
selector: onPlayButton({ eventName: 'commit-toggle-play' }),
sound: soundTuning('form.toggle.silent'),
sound: soundTuning('commit.silent'),
haptic: { kind: 'tap' }
},
{
selector: semaSelector(mediaPlayerMorfo, 'mute-button', {
eventName: 'commit-toggle-mute'
}),
sound: soundTuning('form.toggle.silent'),
sound: soundTuning('commit.silent'),
haptic: { kind: 'tap' }
},
{
selector: onRateButton({ eventName: 'commit-set-rate' }),
sound: soundTuning('form.toggle.silent'),
sound: soundTuning('commit.silent'),
haptic: { kind: 'tap' }
},
{
@ -101,7 +101,7 @@ export const mediaPlayerSema: Sema = {
{
// The composed Sliders' release (scrubber + volume): same subtraction.
selector: inPlayer(semaSelector(sliderMorfo, 'provider', { eventName: 'commit-set' })),
sound: soundTuning('form.toggle.silent')
sound: soundTuning('commit.silent')
},
{
// The Sliders' grab cue: structural, no intent — silenced by channel
@ -123,7 +123,7 @@ export const mediaPlayerSema: Sema = {
// recovers its full voice. These follow it out.
{
selector: inVolume(semaSelector(sliderMorfo, 'provider', { eventName: 'commit-set' })),
sound: soundTuning('form.toggle.silent')
sound: soundTuning('commit.silent')
},
{
selector: inVolume(semaSelector(sliderMorfo, 'provider', { eventName: 'handle-pick' })),
@ -135,11 +135,11 @@ export const mediaPlayerSema: Sema = {
},
{
selector: onProvider({ eventName: 'commit-complete' }),
sound: soundTuning('form.commit.soft')
sound: soundTuning('commit.soft')
},
{
selector: onProvider({ eventName: 'commit-fail' }),
sound: soundTuning('form.commit.soft', { contour: 'descending' })
sound: soundTuning('commit.soft', { contour: 'descending' })
}
]
};

@ -23,7 +23,7 @@ export const menuDialSema: Sema = {
},
{
selector: onAction({ eventName: 'commit-select' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
}
]

@ -12,7 +12,7 @@ import type { Sema } from '../sema-map';
* trigger is now active". Same frequency profile as a toolbar.
*
* Strategy (book cap. 22 §10 — baja intensidad):
* - SOUND: `form.commit.subtle` (gain 0.03). Triggers fire as the user
* - SOUND: `commit.subtle` (gain 0.03). Triggers fire as the user
* scrubs across the menubar — softer than radio-group / checkbox to
* avoid fatigue during multi-trigger traversal.
* - HAPTIC: light `tap` (intensity 0.3, duration 12). Same family as
@ -29,7 +29,7 @@ export const menubarSema: Sema = {
cascade: [
{
selector: onTrigger({ eventName: 'commit-select' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap', intensity: 0.3, duration: 12, delay: 0 }
}
]

@ -13,7 +13,7 @@ export const monthGridSema: Sema = {
cascade: [
{
selector: onCell({ eventName: 'commit-set' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
}
]

@ -11,7 +11,7 @@ import type { Sema } from '../sema-map';
* fatigue profile as Menubar.
*
* Strategy (book cap. 22 §10):
* - SOUND: `form.commit.subtle` (gain 0.03). Selecting a nav link is
* - SOUND: `commit.subtle` (gain 0.03). Selecting a nav link is
* usually followed by a route change / focus shift, so the chime
* must not compete with the next surface's signals.
* - HAPTIC: light `tap`. Confirms the click landed.
@ -28,7 +28,7 @@ export const navigationMenuSema: Sema = {
cascade: [
{
selector: onItem({ eventName: 'commit-select' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap', intensity: 0.3, duration: 12, delay: 0 }
}
]

@ -24,7 +24,7 @@ export const numberFieldSema: Sema = {
cascade: [
{
selector: onProvider({ eventName: 'commit-set' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap' }
},
{

@ -24,7 +24,7 @@ export const onionMenuSema: Sema = {
},
{
selector: onItem({ eventName: 'commit-select' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
}
]

@ -13,32 +13,32 @@ export const paginationSema: Sema = {
cascade: [
{
selector: onPart('provider', { eventName: 'commit-set' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap' }
},
{
selector: onPart('first-trigger', { eventName: 'commit-set' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap' }
},
{
selector: onPart('prev-trigger', { eventName: 'commit-set' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap' }
},
{
selector: onPart('item', { eventName: 'commit-set' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
},
{
selector: onPart('next-trigger', { eventName: 'commit-set' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap' }
},
{
selector: onPart('last-trigger', { eventName: 'commit-set' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap' }
}
]

@ -27,17 +27,17 @@ export const passwordFieldSema: Sema = {
cascade: [
{
selector: onPart('input', { eventName: 'commit-submit' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
},
{
selector: onPart('visibility-trigger', { eventName: 'commit-toggle-visibility' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap' }
},
{
selector: onPart('caps-lock-indicator', { eventName: 'signal-notify-caps-state' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap' }
}
]

@ -8,7 +8,7 @@ import type { Sema } from '../sema-map'
* (checkpoint S3b ascent, 2026-07-07; pays the morfo's old "later sema pass").
*
* Accept (`commit-set` + fulfill) is the seal of the deferValue transaction:
* the fields' commit voice (`form.commit.soft`) with the fulfill intent delta
* the fields' commit voice (`commit.soft`) with the fulfill intent delta
* on top — slightly resolutive, never celebratory noise. Cancel and Clear are
* quieter, descending: edits abandoned / value emptied, nothing to celebrate
* (undo matrix, CANON §3). Every picker composing this host inherits the
@ -24,19 +24,19 @@ export const pickerSema: Sema = {
cascade: [
{
selector: onProvider({ eventName: 'commit-set' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
},
{
selector: onProvider({ eventName: 'commit-cancel' }),
sound: soundTuning('form.commit.subtle', {
sound: soundTuning('commit.subtle', {
contour: 'descending',
pitch: { op: 'add', value: -80 }
})
},
{
selector: onProvider({ eventName: 'commit-reset' }),
sound: soundTuning('form.commit.subtle', {
sound: soundTuning('commit.subtle', {
contour: 'descending',
pitch: { op: 'add', value: -80 }
})

@ -25,7 +25,7 @@ export const proofOfHumanSema: Sema = {
// Cancel — a quiet descending retreat (mirrors drag-drop).
{
selector: onProvider({ eventName: 'commit-cancel' }),
sound: soundTuning('form.commit.subtle', {
sound: soundTuning('commit.subtle', {
contour: 'descending',
pitch: { op: 'add', value: -80 }
})

@ -28,7 +28,7 @@ export const radioGroupSema: Sema = {
cascade: [
{
selector: onItem({ eventName: 'commit-select' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
}
]

@ -20,17 +20,17 @@ export const rangeCalendarSema: Sema = {
cascade: [
{
selector: onDay({ eventName: 'commit-select-start' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
},
{
selector: onDay({ eventName: 'commit-select-range' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
},
{
selector: onDay({ eventName: 'commit-reset' }),
sound: soundTuning('form.commit.subtle', {
sound: soundTuning('commit.subtle', {
contour: 'descending',
pitch: { op: 'add', value: -80 }
})

@ -14,12 +14,12 @@ export const ratingGroupSema: Sema = {
cascade: [
{
selector: onProvider({ eventName: 'commit-set' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
},
{
selector: onItem({ eventName: 'commit-set' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
}
]

@ -18,12 +18,12 @@ export const searchFieldSema: Sema = {
cascade: [
{
selector: onProvider({ eventName: 'commit-submit' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
},
{
selector: onProvider({ eventName: 'commit-reset' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap' }
}
]

@ -20,12 +20,12 @@ export const selectSema: Sema = {
cascade: [
{
selector: onItem({ eventName: 'commit-select' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
},
{
selector: onItem({ eventName: 'commit-unselect' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap' }
}
]

@ -11,7 +11,7 @@ export const stepperSema: Sema = {
cascade: [
{
selector: onProvider({ eventName: 'shift-step' }),
sound: soundTuning('form.commit.subtle', {
sound: soundTuning('commit.subtle', {
contour: 'ascending',
pitch: { op: 'add', value: 60 }
}),
@ -19,7 +19,7 @@ export const stepperSema: Sema = {
},
{
selector: onProvider({ eventName: 'commit-complete' }),
sound: soundTuning('form.commit.soft', {
sound: soundTuning('commit.soft', {
contour: 'ascending',
pitch: { op: 'add', value: 120 }
}),

@ -4,19 +4,19 @@ import { soundTuning } from '../sounds';
import type { Sema } from '../sema-map';
/**
* Switch perceptual defaults — SILENT-BY-DEFAULT + LIGHT HAPTIC.
* Switch perceptual defaults — RESTRAINED VOICE + LIGHT HAPTIC.
*
* Switches are high-frequency form controls (settings panels, preference
* trays). Fatigue is the dominant risk: a sober signature lets the user
* flip a dozen toggles without auditory noise. Book cap. 22 §10 maps a
* single `commit.toggle` verb to both directions; the intent prop carries
* the evaluative load.
* Switches are high-frequency controls (settings panels, preference trays),
* so the signature is sober — but a flip the user cannot hear is not a
* signature, it is an absence. Book cap. 22 §10 maps a single `commit.toggle`
* verb to both directions; the intent prop carries the evaluative load.
*
* Strategy (per author directive 2026-05-26):
* - SOUND: `form.toggle.silent` subtracts the family.commit gain (0.3),
* so neutral/affirm/risk land silent. Threat (+0.1 from intent.delta)
* still surfaces, so destructive toggles emit a cue. Pitch / contour
* differentiation stays computed but inaudible when gain is 0.
* Strategy (author directive 2026-08-05, superseding the silence of
* 2026-05-26 — see D.5):
* - SOUND: `commit.medium` (gain 0.1) — audible, and a third of a button
* press (contact base 0.25) so a flip never outweighs an activation.
* Intent deltas ride on top: threat (+0.1) and fulfill (+0.05) make a
* destructive or culminating toggle stand out, as before.
* - HAPTIC: light `tap` (intensity 0.3 vs family's 0.5, duration 12 vs
* 20, no delay). Replaces the intent-driven `kind` so toggles always
* feel like a tap — keeping the gesture intelligible without
@ -33,7 +33,7 @@ export const switchSema: Sema = {
cascade: [
{
selector: onProvider({ eventName: 'commit-toggle' }),
sound: soundTuning('form.toggle.silent'),
sound: soundTuning('commit.medium'),
haptic: { kind: 'tap', intensity: 0.3, duration: 12, delay: 0 }
}
]

@ -16,12 +16,12 @@ export const tableSema: Sema = {
cascade: [
{
selector: onRow({ eventName: 'commit-select' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
},
{
selector: onColumnHeader({ eventName: 'commit-set-sort' }),
sound: soundTuning('form.commit.subtle')
sound: soundTuning('commit.subtle')
}
]
}

@ -31,7 +31,7 @@ export const tabsSema: Sema = {
cascade: [
{
selector: onTrigger({ eventName: 'select' }),
sound: soundTuning('tabs.select.soft'),
sound: soundTuning('commit.select.soft'),
haptic: { kind: 'tap' }
}
]

@ -14,17 +14,17 @@ export const tagGroupSema: Sema = {
cascade: [
{
selector: onItem({ eventName: 'commit-select' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
},
{
selector: onItem({ eventName: 'commit-unselect' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap' }
},
{
selector: onItem({ eventName: 'commit-remove' }),
sound: soundTuning('form.commit.subtle', {
sound: soundTuning('commit.subtle', {
contour: 'descending',
pitch: { op: 'add', value: -80 }
}),
@ -32,7 +32,7 @@ export const tagGroupSema: Sema = {
},
{
selector: onProvider({ eventName: 'commit-reset' }),
sound: soundTuning('form.commit.subtle', {
sound: soundTuning('commit.subtle', {
contour: 'descending',
pitch: { op: 'add', value: -100 }
}),

@ -17,12 +17,12 @@ export const tagsInputSema: Sema = {
cascade: [
{
selector: onProvider({ eventName: 'commit-set-add' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'success' }
},
{
selector: onInput({ eventName: 'signal-warn-reject' }),
sound: soundTuning('form.commit.soft', {
sound: soundTuning('commit.soft', {
contour: 'descending',
pitch: { op: 'add', value: -120 }
}),
@ -30,7 +30,7 @@ export const tagsInputSema: Sema = {
},
{
selector: onItem({ eventName: 'commit-remove' }),
sound: soundTuning('form.commit.subtle', {
sound: soundTuning('commit.subtle', {
contour: 'descending',
pitch: { op: 'add', value: -80 }
}),
@ -38,7 +38,7 @@ export const tagsInputSema: Sema = {
},
{
selector: onProvider({ eventName: 'commit-reset' }),
sound: soundTuning('form.commit.subtle', {
sound: soundTuning('commit.subtle', {
contour: 'descending',
pitch: { op: 'add', value: -100 }
}),

@ -19,12 +19,12 @@ export const textareaSema: Sema = {
cascade: [
{
selector: onProvider({ eventName: 'commit-submit' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
},
{
selector: onProvider({ eventName: 'signal-warn-count-overflow' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap' }
}
]

@ -14,7 +14,7 @@ export const timeFieldSema: Sema = {
cascade: [
{
selector: onInput({ eventName: 'commit-set' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
}
]

@ -4,7 +4,7 @@ import { soundTuning } from '../sounds';
import type { Sema } from '../sema-map';
/**
* ToggleGroup perceptual defaults — SILENT-BY-DEFAULT + LIGHT HAPTIC.
* ToggleGroup perceptual defaults — RESTRAINED VOICE + LIGHT HAPTIC.
*
* ToggleGroup composes N toggles under one provider (segmented controls,
* formatter ribbons, filter chips). Fatigue compounds with each
@ -12,7 +12,7 @@ import type { Sema } from '../sema-map';
* than for a lone Switch / Toggle. Each item emits `commit-toggle`
* targeting the `item` part.
*
* Strategy mirrors Switch / Toggle — silent by default, light tap haptic.
* Strategy mirrors Switch / Toggle — `commit.medium`, light tap haptic.
* The pack targets `item` (not provider) so the cascade matches the
* actual element that carries the `data-event-*` stamp.
*/
@ -25,7 +25,7 @@ export const toggleGroupSema: Sema = {
cascade: [
{
selector: onItem({ eventName: 'commit-toggle' }),
sound: soundTuning('form.toggle.silent'),
sound: soundTuning('commit.medium'),
haptic: { kind: 'tap', intensity: 0.3, duration: 12, delay: 0 }
},
{

@ -4,7 +4,7 @@ import { soundTuning } from '../sounds';
import type { Sema } from '../sema-map';
/**
* Toggle perceptual defaults — SILENT-BY-DEFAULT + LIGHT HAPTIC.
* Toggle perceptual defaults — RESTRAINED VOICE + LIGHT HAPTIC.
*
* Toggles are single-button two-state controls (toolbar pressed-states,
* inline filters). Same fatigue profile as Switch and Checkbox: dozens of
@ -25,7 +25,7 @@ export const toggleSema: Sema = {
cascade: [
{
selector: onProvider({ eventName: 'commit-toggle' }),
sound: soundTuning('form.toggle.silent'),
sound: soundTuning('commit.medium'),
haptic: { kind: 'tap', intensity: 0.3, duration: 12, delay: 0 }
}
]

@ -14,12 +14,12 @@ export const toolbarSema: Sema = {
cascade: [
{
selector: onProvider({ eventName: 'commit-toggle' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap' }
},
{
selector: onGroupItem({ eventName: 'commit-toggle' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap' }
}
]

@ -17,7 +17,7 @@ import type { Sema } from '../sema-map';
*
* Strategy:
* - `present` / `dismiss` / `dismiss-escape` (all emerge family on content):
* all routed through `tooltip.silent`. That tuning subtracts the
* all routed through `emerge.silent`. That tuning subtracts the
* family.emerge.base.gain (0.2), so the final gain is 0 for
* neutral/affirm/risk/loss. Intent threat (+0.1) and fulfill (+0.05)
* still surface — a tooltip used to convey an error or success
@ -40,7 +40,7 @@ export const tooltipSema: Sema = {
cascade: [
{
selector: onContent({ eventFamily: 'emerge' }),
sound: soundTuning('tooltip.silent')
sound: soundTuning('emerge.silent')
}
]
};

@ -32,7 +32,7 @@ export const treeGridSema: Sema = {
},
{
selector: onRow({ eventName: 'commit-select' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap', intensity: 0.3, duration: 12, delay: 0 }
}
]

@ -18,7 +18,7 @@ import type { Sema } from '../sema-map';
* family base. Same character as Accordion expand.
* - COLLAPSE: `emerge.exit.soft` — descending pitch + gain 0.05,
* same direction discipline as menus / dialogs.
* - SELECT: `form.commit.subtle` + light `tap`. Soft because tree
* - SELECT: `commit.subtle` + light `tap`. Soft because tree
* exploration is often paired with a focus change (the leaf
* content surfaces elsewhere), and the select chime mustn't
* compete with that downstream signal.
@ -51,12 +51,12 @@ export const treeViewSema: Sema = {
// consistent across leaf and branch selection.
{
selector: onItem({ eventName: 'commit-select' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap', intensity: 0.3, duration: 12, delay: 0 }
},
{
selector: onBranch({ eventName: 'commit-select' }),
sound: soundTuning('form.commit.subtle'),
sound: soundTuning('commit.subtle'),
haptic: { kind: 'tap', intensity: 0.3, duration: 12, delay: 0 }
}
]

@ -13,7 +13,7 @@ export const yearGridSema: Sema = {
cascade: [
{
selector: onCell({ eventName: 'commit-set' }),
sound: soundTuning('form.commit.soft'),
sound: soundTuning('commit.soft'),
haptic: { kind: 'tap' }
}
]

@ -0,0 +1,112 @@
/**
* Census guard — the naming grammar of `SOUND_TUNINGS`.
*
* WHY THIS EXISTS. A tuning is a parametric delta over a FAMILY's base
* signature. Its name must say which family it modifies, because that is the
* only axis that is simultaneously true and mechanically checkable: the eight
* families are a closed set (`SEMA_FAMILIES`), a component name is not.
*
* THE DRIFT THAT PAID FOR IT. `form.commit.soft` / `form.commit.subtle`
* entered on 2026-05-19 (`09e261878`, the commit that created `sounds.ts`)
* naming seven form controls — checkbox, radio-group, stepper, tags-input,
* tag-group, file-upload, editable — truthfully. One week later
* `2d562f378` (2026-05-26) added `form.toggle.silent` for switch/toggle AND
* applied `form.commit.subtle` to menubar, navigation-menu, dropdown-menu,
* context-menu, tree-view and tree-grid. Consumers went 7 → 29 in a single
* commit, and by 2026-08-05 they were 50 — of which exactly ONE was the Form
* component. The doctrine written in that same commit (D.6) even spelled the
* right name, `commit.select`, on the very line that used the wrong key.
*
* Nothing caught it for two and a half months because nothing looked:
* `SoundTuningName = keyof typeof SOUND_TUNINGS` closes the set of keys but
* says nothing about their SHAPE, and no script referenced the catalogue.
* Renamed on 2026-08-05 (`form.*` → `commit.*`, `tooltip.silent` →
* `emerge.silent`, `tabs.select.soft` → `commit.select.soft`).
*
* WHAT IS NOT CHECKED, deliberately: the tail. `commit.silent` is applied to
* a slider's `commit-set` in `media-player.ts` as legitimately as to a
* switch's `commit-toggle` — the tail describes the resulting SIGNATURE, not
* the triggering event, so closing it against the verb vocabulary would
* manufacture a lie. There is no closed set for it in the codebase, and
* inventing one here would be exactly the copied-canon this guard exists to
* prevent.
*/
import { describe, expect, it } from 'vitest';
import { SEMA_FAMILIES } from './event';
import { SEMA_MAP } from './sema-map';
import { SOUND_TUNINGS } from './sounds';
import type { SemaFamily } from './types';
/**
* Keys allowed to break the grammar, each with the reason it earned the pass.
* Growing this list is a decision, not a convenience.
*/
const EXCEPTIONS: Record<string, string> = {};
const TUNING_KEYS = Object.keys(SOUND_TUNINGS);
/** The family base gain, read from the map — never copied into this file. */
function baseGain(family: SemaFamily): number | undefined {
return SEMA_MAP.families[family]?.base?.sound?.gain;
}
describe('SOUND_TUNINGS naming grammar', () => {
it('finds the catalogue (sanity: the census is not scanning an empty const)', () => {
expect(TUNING_KEYS.length).toBeGreaterThan(8);
});
it('heads every key with a sema family', () => {
const families = new Set<string>(SEMA_FAMILIES);
const offenders = TUNING_KEYS.filter((key) => key in EXCEPTIONS === false).filter(
(key) => !families.has(key.split('.')[0])
);
expect(
offenders,
`a tuning is a delta on a FAMILY — head these keys with one of ${[...families].join(' | ')}`
).toEqual([]);
});
it('gives every key a tail describing the signature', () => {
const malformed = TUNING_KEYS.filter((key) => {
const segments = key.split('.');
return segments.length < 2 || segments.some((s) => !/^[a-z][a-z-]*$/.test(s));
});
expect(malformed, 'shape is `{family}.{tail}`, lowercase segments').toEqual([]);
});
it('makes every silencer subtract EXACTLY its family base gain', () => {
const silencers = TUNING_KEYS.filter((key) => key.endsWith('.silent'));
// The silence trick only exists because there is a base to cancel.
expect(silencers.length, 'the catalogue lost its silencers').toBeGreaterThan(0);
const wrong: string[] = [];
for (const key of silencers) {
const family = key.split('.')[0] as SemaFamily;
const base = baseGain(family);
const gain = (SOUND_TUNINGS as Record<string, { gain?: unknown }>)[key].gain;
if (base === undefined) {
wrong.push(`${key}: family '${family}' declares no base.sound.gain to cancel`);
continue;
}
if (
typeof gain !== 'object' ||
gain === null ||
(gain as { op?: string }).op !== 'add' ||
(gain as { value?: number }).value !== -base
) {
wrong.push(`${key}: expected { op: 'add', value: ${-base} }, got ${JSON.stringify(gain)}`);
}
}
expect(
wrong,
'a `.silent` tuning must land the family at gain 0 so intent deltas still surface'
).toEqual([]);
});
it('keeps the exception list honest (every entry still names a live key)', () => {
const stale = Object.keys(EXCEPTIONS).filter((key) => !TUNING_KEYS.includes(key));
expect(stale, 'these exceptions excuse keys that no longer exist — delete them').toEqual([]);
});
});

@ -148,6 +148,25 @@ export const SOUND_LIBRARY = {
})
} as const satisfies Record<string, SoundDefinition>;
/**
* Tuning catalogue — parametric deltas over a family's base signature.
*
* NAMING LAW (2026-08-05): the FIRST segment is the sema family whose base
* signature the tuning modifies — never a component, never a UI category.
* A tuning is a delta on a family, so the family is the only axis that is
* both true and mechanically checkable (`SEMA_FAMILIES` is a closed set;
* a component name is not). The tail is free prose describing the resulting
* SIGNATURE, deliberately NOT the triggering event: `commit.silent` is
* applied to a slider's `commit-set` in `media-player.ts` as legitimately as
* to a switch's `commit-toggle`, because what it names is the silence, not
* the gesture.
*
* Enforced by `sounds-grammar.test.ts`. Born from a real drift: `form.*`
* entered on 2026-05-19 naming seven form controls truthfully, was extended
* to menus and trees on 2026-05-26 (`2d562f378`, the very commit that wrote
* D.5/D.6) and reached 50 packs — one of which was the Form. No guard could
* see it because none looked at these keys.
*/
export const SOUND_TUNINGS = {
'emerge.soft': { gain: 0.08 },
'emerge.medium': { gain: 0.1 },
@ -160,37 +179,45 @@ export const SOUND_TUNINGS = {
gain: 0.05
},
'emerge.dismiss.passive': { gain: 0.06 },
'form.commit.soft': { gain: 0.05 },
'form.commit.subtle': { gain: 0.03 },
'commit.soft': { gain: 0.05 },
'commit.subtle': { gain: 0.03 },
'commit.select.soft': { gain: 0.04 },
/**
* Restrained but plainly audible commit — the toggles' voice (switch,
* toggle, toggle-group; author directive 2026-08-05, superseding the
* silence D.5 gave them in May). Sits between `commit.soft` (0.05) and the
* family base (0.3), mirroring the `emerge.soft/medium/strong` ladder: a
* flip is heard without competing with a button press (contact base 0.25).
*/
'commit.medium': { gain: 0.1 },
/**
* Silent-by-default for high-frequency toggles (switch, toggle,
* toggle-group). Subtracts the `family.commit.base.sound.gain` (0.3),
* so neutral/affirm/risk/loss land at gain 0 (silent). Threat (+0.1)
* Silent-by-default for high-frequency commits (switch, toggle,
* toggle-group; also the player's scrubber and volume `commit-set`).
* Subtracts the `family.commit.base.sound.gain` (0.3), so
* neutral/affirm/risk/loss land at gain 0 (silent). Threat (+0.1)
* and fulfill (+0.05) intent deltas still surface — destructive or
* culminating toggles emit a perceptual cue while everyday on/off
* culminating commits emit a perceptual cue while everyday on/off
* stays mute. Avoids fatigue without erasing evaluative loading.
*/
'form.toggle.silent': { gain: { op: 'add', value: -0.3 } },
'commit.silent': { gain: { op: 'add', value: -0.3 } },
/**
* Silent-by-default for tooltips. Tooltips reveal on hover and would
* "puff" on every mouse cross if the family emerge gain (0.2) reached
* audible output. Subtracts the family base so neutral tooltips land
* at gain 0. Intent threat (+0.1) and fulfill (+0.05) still surface
* — a tooltip used as an error / success hint can still be heard.
* Tooltip is auxiliary presence, not signal; the silence is its
* canonical signature.
* Silent-by-default emergence, the tooltip's canonical signature.
* Tooltips reveal on hover and would "puff" on every mouse cross if the
* family emerge gain (0.2) reached audible output. Subtracts the family
* base so neutral emergences land at gain 0. Intent threat (+0.1) and
* fulfill (+0.05) still surface — a tooltip used as an error / success
* hint can still be heard. Auxiliary presence is not signal.
*/
'tooltip.silent': { gain: { op: 'add', value: -0.2 } },
'emerge.silent': { gain: { op: 'add', value: -0.2 } },
/**
* Silent-by-default for contact inside sound-emitting components (the
* media player's transport buttons — signed 2026-07-31, D-AP2.7 v2): the
* component's own output IS audio, so a UI tick competes with the work by
* definition (D.7). Subtracts the contact family base (0.25) so the press
* lands at gain 0; intent deltas still surface, same shape as
* `form.toggle.silent` / `tooltip.silent`.
* `commit.silent` / `emerge.silent`.
*/
'contact.silent': { gain: { op: 'add', value: -0.25 } },
'tabs.select.soft': { gain: 0.04 }
'contact.silent': { gain: { op: 'add', value: -0.25 } }
} as const satisfies Record<string, SoundOverride>;
export type SoundName = keyof typeof SOUND_LIBRARY;

@ -76,7 +76,7 @@ Both react directions are `affirm` per the undo matrix (CANON §3): removing
an item from a collection is as active a choice as adding it. Delivery
changes are app STATE (`data-delivery` via effects), not events. Pack:
`sema/components/chat-message.ts` — high-frequency sobriety (D.5):
`form.commit.subtle` + light tap on both react directions; retry keeps the
`commit.subtle` + light tap on both react directions; retry keeps the
family base. `expression: 'pack'`.
## Data attributes

@ -61,7 +61,7 @@ A control that allows the user to toggle between checked, unchecked, and optiona
Two events instead of a single `commit-toggle`: the direction matters perceptually. `affirm` (the user confirms inclusion / agreement) maps to a brighter, slightly higher cue; `neutral` (revoking a state) has no evaluative load. Per book cap. 22 §10 (literal: "Toggle: contact.press → commit.toggle + affirm — el press registra; el toggle fija") — both share the canonical verb `commit.toggle`; direction is communicated by intent + `data-state`.
**Pack**: [`src/uix/sema/components/checkbox.ts`](../../../sema/components/checkbox.ts) — `form.commit.soft` (gain 0.05) + tap haptic on check; `form.commit.subtle` (gain 0.03) + tap on uncheck. Discrimination rides on the family/intent base — the pack does NOT override pitch / contour / gain from intent.deltas to preserve the per-intent perceptual signature.
**Pack**: [`src/uix/sema/components/checkbox.ts`](../../../sema/components/checkbox.ts) — `commit.soft` (gain 0.05) + tap haptic on check; `commit.subtle` (gain 0.03) + tap on uncheck. Discrimination rides on the family/intent base — the pack does NOT override pitch / contour / gain from intent.deltas to preserve the per-intent perceptual signature.
## Usage

@ -166,7 +166,7 @@ Contract: [`docs/canon/direction-contract.md`](../../../../../docs/canon/directi
Identical perceptual signature to `dropdown-menu` so the system reads as one consistent "menu sound" regardless of how the menu was triggered. Emission lives in `openAt()`, `handleClose()`, and `ContextMenuItemProvider.onclick/onkeydown`.
**Pack**: [`src/uix/sema/components/context-menu.ts`](../../../sema/components/context-menu.ts) — mirrors dropdown-menu (open: `emerge.soft`, close: `emerge.exit.soft`, item: `form.commit.subtle` + tap).
**Pack**: [`src/uix/sema/components/context-menu.ts`](../../../sema/components/context-menu.ts) — mirrors dropdown-menu (open: `emerge.soft`, close: `emerge.exit.soft`, item: `commit.subtle` + tap).
## Usage

@ -181,7 +181,7 @@ The open/close **sema firma** (`emerge`) plays on the content during the hold (s
**Pack**: [`src/uix/sema/components/dropdown-menu.ts`](../../../sema/components/dropdown-menu.ts):
- `open` → `emerge.soft` (soft chime, gain 0.08).
- `close` → `emerge.exit.soft` (descending pitch + gain 0.05).
- `commit-select` → `form.commit.subtle` + light `tap` haptic. Softer than radio-group because close-emerge + select-commit + downstream-surface fire in quick succession; spreading intensity avoids double-tap fatigue.
- `commit-select` → `commit.subtle` + light `tap` haptic. Softer than radio-group because close-emerge + select-commit + downstream-surface fire in quick succession; spreading intensity avoids double-tap fatigue.
## Usage

@ -63,7 +63,7 @@ intentionally out of scope.
Per-slider drag feedback (`handle-drag`) is owned by the composed `<Slider>` pack —
ImageAdjustments doesn't re-emit it. Pack: `src/uix/sema/components/image-adjustments.ts`
(`form.commit.soft` + a `tap`). `sequence: 'post'` — the values mutate first, then the
(`commit.soft` + a `tap`). `sequence: 'post'` — the values mutate first, then the
pulse.
## Props (Provider)

@ -223,7 +223,7 @@ Triggers use roving tabindex: one trigger has `tabindex="0"`, all others have `t
Emission is centralized in `MenubarProvider.open(value)` so every path (mouse, keyboard, hover-follow) produces one stamp. The fallback target is the trigger element resolved through `triggerRegistry`.
**Pack**: [`src/uix/sema/components/menubar.ts`](../../../sema/components/menubar.ts) — `form.commit.subtle` (gain 0.03) + light `tap` haptic. High-frequency top-level traversal; softer than radio-group to avoid fatigue during multi-trigger scrubbing. See `LIBRO_VARIACIONES_Y_EXTENSIONES.md` D.6.
**Pack**: [`src/uix/sema/components/menubar.ts`](../../../sema/components/menubar.ts) — `commit.subtle` (gain 0.03) + light `tap` haptic. High-frequency top-level traversal; softer than radio-group to avoid fatigue during multi-trigger scrubbing. See `LIBRO_VARIACIONES_Y_EXTENSIONES.md` D.6.
The menu's open/close and item selection live in the nested `DropdownMenu` (see its own README) — those events fire independently from this morfo's emission.

@ -229,7 +229,7 @@ Disabled triggers and Links with no interactivity are included in navigation ord
Emission lives in `NavigationMenuProvider.openNow(value)` — the central state-mutator covers every path (click, keyboard, hover-follow). Fallback target is the trigger element from `triggerRefs`; the cascade selector `[data-navigation-menu-item]` matches via `closest()`.
**Pack**: [`src/uix/sema/components/navigation-menu.ts`](../../../sema/components/navigation-menu.ts) — `form.commit.subtle` + light `tap` haptic. Same soft signature as `menubar` and items in `dropdown-menu` / `context-menu` for system-wide menu coherence.
**Pack**: [`src/uix/sema/components/navigation-menu.ts`](../../../sema/components/navigation-menu.ts) — `commit.subtle` + light `tap` haptic. Same soft signature as `menubar` and items in `dropdown-menu` / `context-menu` for system-wide menu coherence.
## i18n

@ -60,7 +60,7 @@ Contract: [`docs/canon/direction-contract.md`](../../../../../docs/canon/directi
| --------------- | -------- | -------- | ---------- | --------------------------------------------------------------------- |
| `commit-toggle` | `commit` | `toggle` | `provider` | Switch toggled on/off. Intent from prop: `neutral`, `affirm`, `risk`, `threat`. |
**Pack**: [`src/uix/sema/components/switch.ts`](../../../sema/components/switch.ts) — silent-by-default (`form.toggle.silent`) + light `tap` haptic. High-frequency settings panels; intent-loaded toggles (threat) still surface audibly via `intent.deltas`.
**Pack**: [`src/uix/sema/components/switch.ts`](../../../sema/components/switch.ts) — silent-by-default (`commit.silent`) + light `tap` haptic. High-frequency settings panels; intent-loaded toggles (threat) still surface audibly via `intent.deltas`.
## Usage

@ -66,7 +66,7 @@ Contract: [`docs/canon/direction-contract.md`](../../../../../docs/canon/directi
| --------------- | -------- | -------- | ------ | ------------------------------------------------------------------- |
| `commit-toggle` | `commit` | `toggle` | `item` | Item clicked. Intent from prop: `neutral`, `affirm`, `risk`, `threat`. |
**Pack**: [`src/uix/sema/components/toggle-group.ts`](../../../sema/components/toggle-group.ts) — silent-by-default sound (`form.toggle.silent`) + light `tap` haptic. High-frequency segmented controls; intent-driven gain still surfaces (threat +0.1 audible). See `LIBRO_VARIACIONES_Y_EXTENSIONES.md` D.5.
**Pack**: [`src/uix/sema/components/toggle-group.ts`](../../../sema/components/toggle-group.ts) — silent-by-default sound (`commit.silent`) + light `tap` haptic. High-frequency segmented controls; intent-driven gain still surfaces (threat +0.1 audible). See `LIBRO_VARIACIONES_Y_EXTENSIONES.md` D.5.
## Usage

@ -83,7 +83,7 @@ Contract: [`docs/canon/direction-contract.md`](../../../../../docs/canon/directi
| `close` | `emerge` | `close` | `content` | `pre` | Pointer-leave or blur closed it. |
| `close-dismiss` | `emerge` | `dismiss` | `content` | `pre` | Escape key dismissal. |
**Pack**: [`src/uix/sema/components/tooltip.ts`](../../../sema/components/tooltip.ts) — **silent-by-default**. The pack tuning (`tooltip.silent`) subtracts the family.emerge gain entirely, so neutral tooltips emit at gain 0 (no sound) and no haptic. Intent threat / fulfill still surface via `intent.deltas` for the rare error/success tooltip variant.
**Pack**: [`src/uix/sema/components/tooltip.ts`](../../../sema/components/tooltip.ts) — **silent-by-default**. The pack tuning (`emerge.silent`) subtracts the family.emerge gain entirely, so neutral tooltips emit at gain 0 (no sound) and no haptic. Intent threat / fulfill still surface via `intent.deltas` for the rare error/success tooltip variant.
Tooltips are auxiliary presence (not signal). Hovering is a continuous, ambient gesture — emitting audible feedback on every cursor sweep across a dense UI would cause immediate fatigue. The silence is doctrinal, not absence (see `LIBRO_VARIACIONES_Y_EXTENSIONES.md` D.8 — tooltips as canonical example of "auxiliary, not signal").

@ -152,7 +152,7 @@ Per the [APG treegrid spec](https://www.w3.org/WAI/ARIA/apg/patterns/treegrid/):
Unlike tree-view, tree-grid uses a single `Row` part for both leaves and branches — `data-has-children` + `data-expanded` disambiguate. The row element is resolved via `querySelector('[data-tree-grid-row][data-value="..."]')`.
**Pack**: [`src/uix/sema/components/tree-grid.ts`](../../../sema/components/tree-grid.ts) — same signature as tree-view (emerge.soft expand / emerge.exit.soft collapse / form.commit.subtle + tap on select).
**Pack**: [`src/uix/sema/components/tree-grid.ts`](../../../sema/components/tree-grid.ts) — same signature as tree-view (emerge.soft expand / emerge.exit.soft collapse / commit.subtle + tap on select).
## Comparison

@ -133,7 +133,7 @@ Contract: [`docs/canon/direction-contract.md`](../../../../../docs/canon/directi
**Pack**: [`src/uix/sema/components/tree-view.ts`](../../../sema/components/tree-view.ts):
- `emerge-expand` → `emerge.soft` (gain 0.08), same as Accordion expand.
- `emerge-collapse` → `emerge.exit.soft` (descending pitch + gain 0.05).
- `commit-select` → `form.commit.subtle` + light `tap` haptic. Two cascade rules (one on `item`, one on `branch`) so both leaf and branch selection share the perceptual signature.
- `commit-select` → `commit.subtle` + light `tap` haptic. Two cascade rules (one on `item`, one on `branch`) so both leaf and branch selection share the perceptual signature.
## Behavior

Loading…
Cancel
Save

Powered by TurnKey Linux.