feat(sema): Lectura C steps 5-8 — 11 sema packs + doctrina samples

Step 5 — 3 packs alta-frecuencia (silencio + tap leve):
  switch, toggle, toggle-group
  + tuning `form.toggle.silent: { gain: { op: 'add', value: -0.3 } }`

Step 6 — 6 packs menús + árboles (emerge.soft / form.commit.subtle):
  menubar, navigation-menu, dropdown-menu, context-menu,
  tree-view, tree-grid

Step 7 — card scope: KEEP `sema` + `family-default` (sin código).

Step 8 — 2 packs (versión lean tras descartar 5 prematuros):
  tooltip (silent-by-default)
  collapsible (mirror accordion)
  + tuning `tooltip.silent: { gain: { op: 'add', value: -0.2 } }`

Doctrina codificada en LIBRO_VARIACIONES_Y_EXTENSIONES.md:
  - D.5 — packs alta frecuencia (toggles)
  - D.6 — packs superficies menú / árbol
  - D.7 — sonido canónico vs samples
    SOUND_LIBRARY = recursos | SOUND_TUNINGS = canon | packs = composición
    samples NO sustituyen firma con intent.deltas
    sampleOverlay descartado permanentemente (sobreingeniería)
    family signal admite excepción para marcas culturales

Cierra Lectura C steps 5-8 del audit-codex P1 #3. Total: 11 packs
nuevos sobre 9 perfiles canónicos de tuning. 195/195 tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 5 months ago
parent 504780ad8b
commit 2d562f3788

@ -279,6 +279,74 @@ implemented: true | false
- **Acción del proyecto**: añadir flag `emission` al MorfoEvent type en una iteración futura para hacer explícito qué eventos se emiten en runtime vs cuáles son declarados sin emisor (contract surface para testing, analytics, accesibilidad externa). - **Acción del proyecto**: añadir flag `emission` al MorfoEvent type en una iteración futura para hacer explícito qué eventos se emiten en runtime vs cuáles son declarados sin emisor (contract surface para testing, analytics, accesibilidad externa).
### D.4 Campo `expression` en el morfo: cómo se rellena la firma sema
- **Status**: **IMPLEMENTATION_CONTRACT** (no doctrina del libro)
- **Decisión del autor (Lectura C)**: un morfo con eventos doctrinales declara su modo de expresión perceptual:
```ts
type SemaExpressionMode = 'pack' | 'family-default' | 'delegated' | 'none';
```
- **`pack`**: el morfo tiene un archivo `src/uix/sema/components/{kebab}.ts` con cascade rules específicas (sonido, háptica, prioridad).
- **`family-default`**: el morfo descansa en `SEMA_MAP.families[family].base` sin tuning per-componente. Apropiado cuando el componente no necesita firma diferenciada.
- **`delegated`**: el componente compone otros morfos que sí emiten (p. ej. picker emite via calendar/time-field/color-area que tienen sus propios eventos).
- **`none`**: el morfo declara contrato pero no participa en sema runtime (reservado para utilidades no perceptuales).
- **Lint**: `npm run morfo:vocabulary` valida que todo morfo con `events.length > 0` cumpla:
1. `scope` incluye `'sema'` (FAIL).
2. Existe un pack en `src/uix/sema/components/{kebab}.ts` **o** `expression !== undefined` (WARN si falla).
- **Cuándo crear pack vs `family-default`** (criterios para D.4):
- **Crear pack** si: eventos de alta frecuencia + riesgo de fatiga (toggles, form controls), o el componente necesita una firma sobria distinta del family base, o introduce cascades con prioridad (a11y override).
- **`family-default`** si: el family base ya es la firma correcta y el componente no compite con otros del mismo family por intensidad perceptual.
### D.5 Packs para componentes de alta frecuencia (toggles)
- **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.
- **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).
### D.6 Packs para superficies de menú y árboles
- **Status**: **PROJECT_CANON**
- **Caso**: `menubar`, `navigation-menu`, `context-menu`, `dropdown-menu`, `tree-view`, `tree-grid` — superficies navegacionales de alta frecuencia. El usuario abre/cierra menús y expande/contrae nodos decenas de veces por sesión. Sin tuning, heredan `family.emerge.base.gain = 0.2` y `family.commit.base.gain = 0.3` — demasiado prominente.
- **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.
- **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`)**: el morfo declara `target: v.partRef('item')` para `emerge-expand` / `emerge-collapse`, pero a nivel DOM las branches usan `data-tree-view-branch`, no `data-tree-view-item`. Cuando soma cablee la emisión, hay que verificar dónde aterriza el `data-event-*`; si va a branch, el pack necesita usar `branch` en el selector. Anotación para auditoría soma.
- **Caveat (emisión soma)**: a fecha de este commit, los 6 morfos NO emiten vía `runtime.trigger` (búsqueda en `src/uix/soma/components/*/`). Las cascades del pack son correctas contra el contrato morfo pero quedan dormantes hasta que soma las cablee. Documentado como follow-up sin urgencia.
### D.7 Sonido canónico y samples
- **Status**: **PROJECT_CANON** (regla doctrinal sin necesidad de pasar al libro)
- **Doctrina (verbatim del autor)**:
> "El sonido canónico de la gramática debe ser modulable por familia, intent, frecuencia e intensidad.
>
> Los samples no deben sustituir la firma semántica base cuando esa sustitución impide la modulación por intent.
>
> Los samples pueden existir como recursos de producto, tema o branding, pero no forman parte del canon semántico por defecto.
>
> En eventos frecuentes, la prioridad es evitar fatiga. El silencio es una firma válida.
>
> Los packs solo deben crearse cuando corrigen una diferencia perceptiva real: frecuencia, fatiga, incongruencia, accesibilidad, patrón recurrente o necesidad de diferenciación."
- **Regla práctica**: si un pack solo selecciona un tuning existente y no evita un problema real, no se crea.
- **Decisión arquitectural inmediata**:
- `SOUND_LIBRARY` (samples + synth concretos) = **recursos**. NO se usa en packs canónicos.
- `SOUND_TUNINGS` (deltas paramétricas sobre family base) = **canon semántico**. Esta es la única capa que se usa en packs por defecto.
- Los packs componen tunings, nunca samples directos. Esto preserva `intent.deltas` (capa 2) que es lo que da diferenciación perceptual al sistema.
- **Excepción aceptable**: family `signal` (alarm / notify / announce) admite samples como replacement porque (a) tienen marca cultural prescriptiva (error wav, ping, ding), (b) la intent-variability es efectivamente nula en ese family. Si emerge un caso, se documenta explícitamente.
- **Lo que NO se hace**:
- No hay `sampleOverlay` (sample como capa adicional sobre synth). Sobreingeniería: añade mixing en WebAudio, layer de resolver, knobs extra al diseñador, y los casos donde aportaría son raros. Descartado permanentemente, no como pendiente.
- No se canonizan samples en packs de `commit`, `emerge`, `contact`, `handle`, `shift`, `sustain`, `delegate`. Los packs viven de tunings.
### D.3 Family policy: separar requirement de guidance ### D.3 Family policy: separar requirement de guidance
- **Status**: **BOOK_CANON** (concepto) + cambio inmediato en el proyecto - **Status**: **BOOK_CANON** (concepto) + cambio inmediato en el proyecto
@ -342,6 +410,10 @@ intentGuidance: 'expected' | 'contextual' | 'discouraged'
### IMPLEMENTATION_CONTRACT (no doctrina) ### IMPLEMENTATION_CONTRACT (no doctrina)
- Eventos declarados pero no emitidos (D.2) - Eventos declarados pero no emitidos (D.2)
- Campo `expression` en el morfo (D.4)
- Packs sema soft-tuned para alta frecuencia (D.5, toggles)
- Packs sema para superficies de menú y árboles (D.6)
- Doctrina sonido canónico vs samples (D.7) + packs tooltip / collapsible
--- ---

@ -20,7 +20,11 @@ export const collapsibleMorfo = {
name: 'Collapsible', name: 'Collapsible',
kebab: 'collapsible', kebab: 'collapsible',
scope: ['soma', 'sema'], scope: ['soma', 'sema'],
expression: "family-default", // expression resolved via the sema pack at
// `src/uix/sema/components/collapsible.ts` — mirrors accordion
// (`emerge.soft` expand + `emerge.exit` collapse). Coherence with
// the sibling disclosure pattern.
expression: 'pack',
texts: { texts: {
label: '#?components.collapsible.label|Collapsible' label: '#?components.collapsible.label|Collapsible'
}, },

@ -7,7 +7,11 @@ export const contextMenuMorfo = {
// Eidos wrapper added 2026-05-22 (`src/uix/eidos/components/context-menu/`). // Eidos wrapper added 2026-05-22 (`src/uix/eidos/components/context-menu/`).
// Same family/event vocabulary as dropdown-menu — sound from family bases. // Same family/event vocabulary as dropdown-menu — sound from family bases.
scope: ['soma', 'sema', 'eidos'], scope: ['soma', 'sema', 'eidos'],
expression: "family-default", // expression resolved via the sema pack at
// `src/uix/sema/components/context-menu.ts` — soft emerge open/close
// + subtle commit on item select. Shares the dropdown-menu signature
// so the system reads as one consistent "menu sound".
expression: 'pack',
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/menu/', apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/menu/',
texts: { texts: {
label: '#?components.context-menu.label|Context menu' label: '#?components.context-menu.label|Context menu'

@ -9,7 +9,11 @@ export const dropdownMenuMorfo = {
// commit-select from the `commit` family base — both ship sound // commit-select from the `commit` family base — both ship sound
// out of the box, no per-component cascade needed. // out of the box, no per-component cascade needed.
scope: ['soma', 'sema', 'eidos'], scope: ['soma', 'sema', 'eidos'],
expression: "family-default", // expression resolved via the sema pack at
// `src/uix/sema/components/dropdown-menu.ts` — soft emerge open/close
// + subtle commit on item select. High-frequency surface; sober
// signature avoids fatigue.
expression: 'pack',
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/', apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/',
texts: { texts: {
label: '#?components.dropdown-menu.label|Dropdown menu' label: '#?components.dropdown-menu.label|Dropdown menu'

@ -5,7 +5,10 @@ export const menubarMorfo = {
name: 'Menubar', name: 'Menubar',
kebab: 'menubar', kebab: 'menubar',
scope: ['soma', 'sema'], scope: ['soma', 'sema'],
expression: "family-default", // expression resolved via the sema pack at
// `src/uix/sema/components/menubar.ts` — subtle commit + tap haptic
// for high-frequency top-level menu triggers.
expression: 'pack',
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/menubar/', apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/menubar/',
texts: { texts: {
label: '#?components.menubar.label|Menubar' label: '#?components.menubar.label|Menubar'

@ -5,7 +5,10 @@ export const navigationMenuMorfo = {
name: 'NavigationMenu', name: 'NavigationMenu',
kebab: 'navigation-menu', kebab: 'navigation-menu',
scope: ['soma', 'sema'], scope: ['soma', 'sema'],
expression: "family-default", // expression resolved via the sema pack at
// `src/uix/sema/components/navigation-menu.ts` — subtle commit + tap
// haptic for high-frequency nav link selection.
expression: 'pack',
texts: { texts: {
label: '#?components.navigation-menu.label|Main' label: '#?components.navigation-menu.label|Main'
}, },

@ -22,7 +22,9 @@ export const switchMorfo = {
name: 'Switch', name: 'Switch',
kebab: 'switch', kebab: 'switch',
scope: ['soma', 'sema'], scope: ['soma', 'sema'],
expression: "family-default", // expression resolved via the sema pack at
// `src/uix/sema/components/switch.ts` — silent default + light tap.
expression: 'pack',
texts: { texts: {
label: '#?components.switch.label|Switch' label: '#?components.switch.label|Switch'
}, },

@ -8,7 +8,11 @@ export const toggleGroupMorfo = {
// Sema picks up the `commit-toggle` event via the `commit` family base — // Sema picks up the `commit-toggle` event via the `commit` family base —
// no per-component cascade needed (commit ships sound out of the box). // no per-component cascade needed (commit ships sound out of the box).
scope: ['soma', 'sema', 'eidos'], scope: ['soma', 'sema', 'eidos'],
expression: "family-default", // expression resolved via the sema pack at
// `src/uix/sema/components/toggle-group.ts` — silent default + light
// tap on each item's `commit-toggle`. Pack inherits the soft signature
// shared with Switch / Toggle.
expression: 'pack',
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/button/', apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/button/',
texts: { texts: {
label: '#?components.toggle-group.label|Toggle group' label: '#?components.toggle-group.label|Toggle group'

@ -23,7 +23,9 @@ export const toggleMorfo = {
name: 'Toggle', name: 'Toggle',
kebab: 'toggle', kebab: 'toggle',
scope: ['soma', 'sema'], scope: ['soma', 'sema'],
expression: "family-default", // expression resolved via the sema pack at
// `src/uix/sema/components/toggle.ts` — silent default + light tap.
expression: 'pack',
texts: { texts: {
label: '#?components.toggle.label|Toggle' label: '#?components.toggle.label|Toggle'
}, },

@ -12,7 +12,10 @@ export const tooltipMorfo = {
name: 'Tooltip', name: 'Tooltip',
kebab: 'tooltip', kebab: 'tooltip',
scope: ['soma', 'eidos', 'sema'], scope: ['soma', 'eidos', 'sema'],
expression: "family-default", // expression resolved via the sema pack at
// `src/uix/sema/components/tooltip.ts` — silent by default; the
// family.emerge gain on every hover would be insufferable.
expression: 'pack',
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/', apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/',
texts: { texts: {
label: '#?components.tooltip.label|Tooltip' label: '#?components.tooltip.label|Tooltip'

@ -5,7 +5,10 @@ export const treeGridMorfo = {
name: 'TreeGrid', name: 'TreeGrid',
kebab: 'tree-grid', kebab: 'tree-grid',
scope: ['soma', 'sema'], scope: ['soma', 'sema'],
expression: "family-default", // expression resolved via the sema pack at
// `src/uix/sema/components/tree-grid.ts` — soft emerge expand/collapse
// + subtle commit on row select. Same sober signature as tree-view.
expression: 'pack',
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/treegrid/', apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/treegrid/',
texts: { texts: {
label: '#?components.tree-grid.label|Tree grid', label: '#?components.tree-grid.label|Tree grid',

@ -5,7 +5,11 @@ export const treeViewMorfo = {
name: 'TreeView', name: 'TreeView',
kebab: 'tree-view', kebab: 'tree-view',
scope: ['soma', 'sema'], scope: ['soma', 'sema'],
expression: "family-default", // expression resolved via the sema pack at
// `src/uix/sema/components/tree-view.ts` — soft emerge expand/collapse
// + subtle commit on item select. Trees are explored repeatedly;
// signature stays sober.
expression: 'pack',
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/treeview/', apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/treeview/',
texts: { texts: {
label: '#?components.tree-view.label|Tree' label: '#?components.tree-view.label|Tree'

@ -0,0 +1,41 @@
import { semaSelector } from '$uix/morfo';
import { collapsibleMorfo } from '$uix/morfo/components/collapsible';
import { soundTuning } from '../sounds';
import type { Sema } from '../sema-map';
/**
* Collapsible perceptual defaults — MIRRORS ACCORDION.
*
* Collapsible is the single-panel sibling of Accordion (which already
* ships a pack). Coherence demands they share the perceptual signature:
* a user disclosing a section gets the same cue regardless of whether
* the surface is grouped (accordion) or standalone (collapsible).
*
* Per `src/uix/sema/components/accordion.ts`:
* - `expand` (emerge.open / verb=expand in our case): `emerge.soft`
* — soft chime at gain 0.08. The family base ascending contour
* reads as "panel reveals".
* - `collapse` (emerge.close / verb=collapse): `emerge.exit` at
* gain 0.06 — descending pitch, same direction discipline as the
* rest of the system (dialog, drawer, popover, menus).
*
* No haptic — disclosure is a deliberate, already-acknowledged gesture
* on a trigger button; tactile feedback on top would be noise.
*/
const onContent = (matchers?: Parameters<typeof semaSelector<typeof collapsibleMorfo>>[2]) =>
semaSelector(collapsibleMorfo, 'content', matchers);
export const collapsibleSema: Sema = {
name: 'collapsible',
cascade: [
{
selector: onContent({ eventName: 'expand' }),
sound: soundTuning('emerge.soft')
},
{
selector: onContent({ eventName: 'collapse' }),
sound: soundTuning('emerge.exit', { gain: 0.06 })
}
]
};

@ -0,0 +1,43 @@
import { semaSelector } from '$uix/morfo';
import { contextMenuMorfo } from '$uix/morfo/components/context-menu';
import { soundTuning } from '../sounds';
import type { Sema } from '../sema-map';
/**
* ContextMenu perceptual defaults — SOFT EMERGE + COMMIT.
*
* ContextMenu shares the event surface of DropdownMenu (open / close /
* commit-select), but the gesture is different: triggered by right-click
* or long-press on arbitrary content rather than by a dedicated trigger
* button. Frequency is bursty (one menu per investigative gesture) vs
* DropdownMenu's steady traversal, but the perceptual signature stays
* identical for system coherence — a user shouldn't have to learn two
* different "menu sounds".
*
* Strategy mirrors DropdownMenu — see `dropdown-menu.ts` for the
* doctrinal rationale.
*/
const onContent = (matchers?: Parameters<typeof semaSelector<typeof contextMenuMorfo>>[2]) =>
semaSelector(contextMenuMorfo, 'content', matchers);
const onItem = (matchers?: Parameters<typeof semaSelector<typeof contextMenuMorfo>>[2]) =>
semaSelector(contextMenuMorfo, 'item', matchers);
export const contextMenuSema: Sema = {
name: 'context-menu',
cascade: [
{
selector: onContent({ eventName: 'open' }),
sound: soundTuning('emerge.soft')
},
{
selector: onContent({ eventName: 'close' }),
sound: soundTuning('emerge.exit.soft')
},
{
selector: onItem({ eventName: 'commit-select' }),
sound: soundTuning('form.commit.subtle'),
haptic: { kind: 'tap', intensity: 0.3, duration: 12, delay: 0 }
}
]
};

@ -0,0 +1,54 @@
import { semaSelector } from '$uix/morfo';
import { dropdownMenuMorfo } from '$uix/morfo/components/dropdown-menu';
import { soundTuning } from '../sounds';
import type { Sema } from '../sema-map';
/**
* DropdownMenu perceptual defaults — SOFT EMERGE + COMMIT.
*
* DropdownMenu has three event surfaces:
* - `open` (emerge.open on `content`) — menu reveals
* - `close` (emerge.close on `content`) — menu retracts
* - `commit-select` (commit.select on `item`, affirm) — user picks a
* menu item
*
* Strategy (book cap. 22 §10 + cap. 26 emerge open/close discipline):
* - OPEN: `emerge.soft` (gain 0.08). The family.emerge.base.gain is
* 0.2 — too prominent for a menu the user opens dozens of times per
* session. Soft chime keeps the "menu appeared" cue without fatigue.
* - CLOSE: `emerge.exit.soft` — descending pitch (open ascends, close
* descends — same direction discipline as Dialog / Popover) plus
* 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.
* 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.
* - The morfo event carries `intent: 'affirm'`; pack does NOT override
* pitch / contour, so intent.deltas (capa 2) provide the positive lift.
*/
const onContent = (matchers?: Parameters<typeof semaSelector<typeof dropdownMenuMorfo>>[2]) =>
semaSelector(dropdownMenuMorfo, 'content', matchers);
const onItem = (matchers?: Parameters<typeof semaSelector<typeof dropdownMenuMorfo>>[2]) =>
semaSelector(dropdownMenuMorfo, 'item', matchers);
export const dropdownMenuSema: Sema = {
name: 'dropdown-menu',
cascade: [
{
selector: onContent({ eventName: 'open' }),
sound: soundTuning('emerge.soft')
},
{
selector: onContent({ eventName: 'close' }),
sound: soundTuning('emerge.exit.soft')
},
{
selector: onItem({ eventName: 'commit-select' }),
sound: soundTuning('form.commit.subtle'),
haptic: { kind: 'tap', intensity: 0.3, duration: 12, delay: 0 }
}
]
};

@ -11,14 +11,19 @@
export { accordionSema } from './accordion'; export { accordionSema } from './accordion';
export { checkboxSema } from './checkbox'; export { checkboxSema } from './checkbox';
export { collapsibleSema } from './collapsible';
export { colorPickerSema } from './color-picker'; export { colorPickerSema } from './color-picker';
export { comboboxSema } from './combobox'; export { comboboxSema } from './combobox';
export { contextMenuSema } from './context-menu';
export { dateFieldSema } from './date-field'; export { dateFieldSema } from './date-field';
export { dialogSema } from './dialog'; export { dialogSema } from './dialog';
export { drawerSema } from './drawer'; export { drawerSema } from './drawer';
export { dropdownMenuSema } from './dropdown-menu';
export { editableSema } from './editable'; export { editableSema } from './editable';
export { fileUploadSema } from './file-upload'; export { fileUploadSema } from './file-upload';
export { formSema } from './form'; export { formSema } from './form';
export { menubarSema } from './menubar';
export { navigationMenuSema } from './navigation-menu';
export { numberFieldSema } from './number-field'; export { numberFieldSema } from './number-field';
export { paginationSema } from './pagination'; export { paginationSema } from './pagination';
export { passwordFieldSema } from './password-field'; export { passwordFieldSema } from './password-field';
@ -30,10 +35,16 @@ export { selectSema } from './select';
export { sliderSema } from './slider'; export { sliderSema } from './slider';
export { splitterSema } from './splitter'; export { splitterSema } from './splitter';
export { stepperSema } from './stepper'; export { stepperSema } from './stepper';
export { switchSema } from './switch';
export { tabsSema } from './tabs'; export { tabsSema } from './tabs';
export { tagGroupSema } from './tag-group'; export { tagGroupSema } from './tag-group';
export { tagsInputSema } from './tags-input'; export { tagsInputSema } from './tags-input';
export { textareaSema } from './textarea'; export { textareaSema } from './textarea';
export { toastSema } from './toast'; export { toastSema } from './toast';
export { toggleSema } from './toggle';
export { toggleGroupSema } from './toggle-group';
export { toolbarSema } from './toolbar'; export { toolbarSema } from './toolbar';
export { tooltipSema } from './tooltip';
export { treeViewSema } from './tree-view';
export { treeGridSema } from './tree-grid';
export { wordsSema } from './words'; export { wordsSema } from './words';

@ -0,0 +1,36 @@
import { semaSelector } from '$uix/morfo';
import { menubarMorfo } from '$uix/morfo/components/menubar';
import { soundTuning } from '../sounds';
import type { Sema } from '../sema-map';
/**
* Menubar perceptual defaults — SOFT COMMIT + TAP HAPTIC.
*
* Menubar declares a single `commit-select` on its top-level trigger.
* The actual menu content lives in a sibling DropdownMenu / ContextMenu
* (each with its own pack); Menubar only carries the act of "this top
* 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
* 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
* toggles — sober, frequency-tolerant.
* - The intent on the morfo event is `affirm`; pitch lift comes from
* the intent.deltas (capa 2). Pack does NOT override pitch / contour.
*/
const onTrigger = (matchers?: Parameters<typeof semaSelector<typeof menubarMorfo>>[2]) =>
semaSelector(menubarMorfo, 'trigger', matchers);
export const menubarSema: Sema = {
name: 'menubar',
cascade: [
{
selector: onTrigger({ eventName: 'commit-select' }),
sound: soundTuning('form.commit.subtle'),
haptic: { kind: 'tap', intensity: 0.3, duration: 12, delay: 0 }
}
]
};

@ -0,0 +1,35 @@
import { semaSelector } from '$uix/morfo';
import { navigationMenuMorfo } from '$uix/morfo/components/navigation-menu';
import { soundTuning } from '../sounds';
import type { Sema } from '../sema-map';
/**
* NavigationMenu perceptual defaults — SOFT COMMIT + TAP HAPTIC.
*
* NavigationMenu emits `commit-select` per item (link or trigger).
* Browsing a navigation bar is high-frequency, low-intensity — same
* fatigue profile as Menubar.
*
* Strategy (book cap. 22 §10):
* - SOUND: `form.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.
* - The morfo event carries `intent: 'affirm'`; the pack does NOT
* override pitch / contour — intent.deltas (capa 2) supply the
* positive lift.
*/
const onItem = (matchers?: Parameters<typeof semaSelector<typeof navigationMenuMorfo>>[2]) =>
semaSelector(navigationMenuMorfo, 'item', matchers);
export const navigationMenuSema: Sema = {
name: 'navigation-menu',
cascade: [
{
selector: onItem({ eventName: 'commit-select' }),
sound: soundTuning('form.commit.subtle'),
haptic: { kind: 'tap', intensity: 0.3, duration: 12, delay: 0 }
}
]
};

@ -0,0 +1,40 @@
import { semaSelector } from '$uix/morfo';
import { switchMorfo } from '$uix/morfo/components/switch';
import { soundTuning } from '../sounds';
import type { Sema } from '../sema-map';
/**
* Switch perceptual defaults — SILENT-BY-DEFAULT + 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.
*
* 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.
* - 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
* amplifying frequency. Intent loading lives in `data-color` + (for
* threat only) the audible sound surface.
* - Motion lives in eidos recipes; not part of this pack.
*/
const onProvider = (matchers?: Parameters<typeof semaSelector<typeof switchMorfo>>[2]) =>
semaSelector(switchMorfo, 'provider', matchers);
export const switchSema: Sema = {
name: 'switch',
cascade: [
{
selector: onProvider({ eventName: 'commit-toggle' }),
sound: soundTuning('form.toggle.silent'),
haptic: { kind: 'tap', intensity: 0.3, duration: 12, delay: 0 }
}
]
};

@ -0,0 +1,32 @@
import { semaSelector } from '$uix/morfo';
import { toggleGroupMorfo } from '$uix/morfo/components/toggle-group';
import { soundTuning } from '../sounds';
import type { Sema } from '../sema-map';
/**
* ToggleGroup perceptual defaults — SILENT-BY-DEFAULT + LIGHT HAPTIC.
*
* ToggleGroup composes N toggles under one provider (segmented controls,
* formatter ribbons, filter chips). Fatigue compounds with each
* additional item, so the sober signature is even more important here
* 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.
* The pack targets `item` (not provider) so the cascade matches the
* actual element that carries the `data-event-*` stamp.
*/
const onItem = (matchers?: Parameters<typeof semaSelector<typeof toggleGroupMorfo>>[2]) =>
semaSelector(toggleGroupMorfo, 'item', matchers);
export const toggleGroupSema: Sema = {
name: 'toggle-group',
cascade: [
{
selector: onItem({ eventName: 'commit-toggle' }),
sound: soundTuning('form.toggle.silent'),
haptic: { kind: 'tap', intensity: 0.3, duration: 12, delay: 0 }
}
]
};

@ -0,0 +1,32 @@
import { semaSelector } from '$uix/morfo';
import { toggleMorfo } from '$uix/morfo/components/toggle';
import { soundTuning } from '../sounds';
import type { Sema } from '../sema-map';
/**
* Toggle perceptual defaults — SILENT-BY-DEFAULT + LIGHT HAPTIC.
*
* Toggles are single-button two-state controls (toolbar pressed-states,
* inline filters). Same fatigue profile as Switch and Checkbox: dozens of
* presses in seconds during a typical edit session. Book cap. 22 §10
* declares a single `commit.toggle` verb for both directions; intent
* carries evaluative weight.
*
* Strategy mirrors Switch — see `switch.ts` for the doctrinal rationale.
* Toggle and Switch share the same perceptual signature because they
* share the same UX role at the gesture layer (one press, one flip).
*/
const onProvider = (matchers?: Parameters<typeof semaSelector<typeof toggleMorfo>>[2]) =>
semaSelector(toggleMorfo, 'provider', matchers);
export const toggleSema: Sema = {
name: 'toggle',
cascade: [
{
selector: onProvider({ eventName: 'commit-toggle' }),
sound: soundTuning('form.toggle.silent'),
haptic: { kind: 'tap', intensity: 0.3, duration: 12, delay: 0 }
}
]
};

@ -0,0 +1,46 @@
import { semaSelector } from '$uix/morfo';
import { tooltipMorfo } from '$uix/morfo/components/tooltip';
import { soundTuning } from '../sounds';
import type { Sema } from '../sema-map';
/**
* Tooltip perceptual defaults — SILENT-BY-DEFAULT.
*
* Tooltips reveal on hover or focus. Hovering is a continuous, ambient
* gesture: cursor crosses the trigger and the tooltip pops. If the
* framework shipped audible defaults here, every cursor sweep across a
* dense UI would "puff" emerge.base sounds — fatigue is immediate.
*
* Doctrinal (`LIBRO_VARIACIONES_Y_EXTENSIONES.md` D.7):
* - Tooltip is auxiliary presence, not signal.
* - Silence is a valid canonical signature.
*
* Strategy:
* - `open` / `close` / `close-dismiss` (all emerge family on content):
* all routed through `tooltip.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
* hint can still be heard if the consumer sets `intent`.
* - NO haptic. Hover is not a tactile gesture; tactile feedback on
* hover would be perceptually wrong.
* - NO pack on `Trigger` — the trigger does not emit semantic
* events; only `Content` participates in sema.
*
* Override path for themes: app cascade rule
* `[data-tooltip-content] { sound: { gain: 0.05 } }` resurrects an
* audible signature. The default ships silent.
*/
const onContent = (matchers?: Parameters<typeof semaSelector<typeof tooltipMorfo>>[2]) =>
semaSelector(tooltipMorfo, 'content', matchers);
export const tooltipSema: Sema = {
name: 'tooltip',
cascade: [
{
selector: onContent({ eventFamily: 'emerge' }),
sound: soundTuning('tooltip.silent')
}
]
};

@ -0,0 +1,39 @@
import { semaSelector } from '$uix/morfo';
import { treeGridMorfo } from '$uix/morfo/components/tree-grid';
import { soundTuning } from '../sounds';
import type { Sema } from '../sema-map';
/**
* TreeGrid perceptual defaults — SOFT EMERGE + COMMIT.
*
* TreeGrid composes TreeView's expand/collapse with Table's row
* structure. Three event surfaces, all targeting the `row` part:
* - `commit-select` (commit.select, affirm) — row selected
* - `emerge-expand` (emerge.expand) — row reveals child rows
* - `emerge-collapse` (emerge.collapse) — row hides children
*
* Strategy mirrors TreeView — same frequency / fatigue profile, just on
* a tabular layout. See `tree-view.ts` for the doctrinal rationale.
*/
const onRow = (matchers?: Parameters<typeof semaSelector<typeof treeGridMorfo>>[2]) =>
semaSelector(treeGridMorfo, 'row', matchers);
export const treeGridSema: Sema = {
name: 'tree-grid',
cascade: [
{
selector: onRow({ eventName: 'emerge-expand' }),
sound: soundTuning('emerge.soft')
},
{
selector: onRow({ eventName: 'emerge-collapse' }),
sound: soundTuning('emerge.exit.soft')
},
{
selector: onRow({ eventName: 'commit-select' }),
sound: soundTuning('form.commit.subtle'),
haptic: { kind: 'tap', intensity: 0.3, duration: 12, delay: 0 }
}
]
};

@ -0,0 +1,53 @@
import { semaSelector } from '$uix/morfo';
import { treeViewMorfo } from '$uix/morfo/components/tree-view';
import { soundTuning } from '../sounds';
import type { Sema } from '../sema-map';
/**
* TreeView perceptual defaults — SOFT EMERGE + COMMIT.
*
* TreeView has three event surfaces, all targeting the `item` part:
* - `commit-select` (commit.select, affirm) — leaf or branch selected
* - `emerge-expand` (emerge.expand) — branch reveals children
* - `emerge-collapse` (emerge.collapse) — branch hides children
*
* Strategy (book cap. 22 §10 + cap. 26 emerge open/close discipline):
* - EXPAND: `emerge.soft` (gain 0.08) — content reveals, ascending
* 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
* exploration is often paired with a focus change (the leaf
* content surfaces elsewhere), and the select chime mustn't
* compete with that downstream signal.
*
* Note on target: morfo declares all three events targeting `item`
* (leaves). Branches also have role="treeitem" but a different part
* marker (`data-tree-view-branch`). If branches end up emitting at
* runtime without the `data-tree-view-item` marker, this cascade will
* miss them — a known concern noted in the morfo declaration audit.
* The pack follows the morfo declaration verbatim; any mismatch is a
* morfo-level fix outside this pack's scope.
*/
const onItem = (matchers?: Parameters<typeof semaSelector<typeof treeViewMorfo>>[2]) =>
semaSelector(treeViewMorfo, 'item', matchers);
export const treeViewSema: Sema = {
name: 'tree-view',
cascade: [
{
selector: onItem({ eventName: 'emerge-expand' }),
sound: soundTuning('emerge.soft')
},
{
selector: onItem({ eventName: 'emerge-collapse' }),
sound: soundTuning('emerge.exit.soft')
},
{
selector: onItem({ eventName: 'commit-select' }),
sound: soundTuning('form.commit.subtle'),
haptic: { kind: 'tap', intensity: 0.3, duration: 12, delay: 0 }
}
]
};

@ -138,6 +138,25 @@ export const SOUND_TUNINGS = {
'emerge.dismiss.passive': { gain: 0.06 }, 'emerge.dismiss.passive': { gain: 0.06 },
'form.commit.soft': { gain: 0.05 }, 'form.commit.soft': { gain: 0.05 },
'form.commit.subtle': { gain: 0.03 }, 'form.commit.subtle': { gain: 0.03 },
/**
* 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)
* and fulfill (+0.05) intent deltas still surface — destructive or
* culminating toggles 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 } },
/**
* 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.
*/
'tooltip.silent': { gain: { op: 'add', value: -0.2 } },
'tabs.select.soft': { gain: 0.04 } 'tabs.select.soft': { gain: 0.04 }
} as const satisfies Record<string, SoundOverride>; } as const satisfies Record<string, SoundOverride>;

Loading…
Cancel
Save

Powered by TurnKey Linux.