diff --git a/src/docs/LIBRO_VARIACIONES_Y_EXTENSIONES.md b/src/docs/LIBRO_VARIACIONES_Y_EXTENSIONES.md index f07062ce1..0cce4714f 100644 --- a/src/docs/LIBRO_VARIACIONES_Y_EXTENSIONES.md +++ b/src/docs/LIBRO_VARIACIONES_Y_EXTENSIONES.md @@ -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). +### 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 - **Status**: **BOOK_CANON** (concepto) + cambio inmediato en el proyecto @@ -342,6 +410,10 @@ intentGuidance: 'expected' | 'contextual' | 'discouraged' ### IMPLEMENTATION_CONTRACT (no doctrina) - 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 --- diff --git a/src/uix/morfo/components/collapsible.ts b/src/uix/morfo/components/collapsible.ts index 7a2d26888..93a347a7e 100644 --- a/src/uix/morfo/components/collapsible.ts +++ b/src/uix/morfo/components/collapsible.ts @@ -20,7 +20,11 @@ export const collapsibleMorfo = { name: 'Collapsible', kebab: 'collapsible', 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: { label: '#?components.collapsible.label|Collapsible' }, diff --git a/src/uix/morfo/components/context-menu.ts b/src/uix/morfo/components/context-menu.ts index 7748fa23d..b6b7ffcec 100644 --- a/src/uix/morfo/components/context-menu.ts +++ b/src/uix/morfo/components/context-menu.ts @@ -7,7 +7,11 @@ export const contextMenuMorfo = { // Eidos wrapper added 2026-05-22 (`src/uix/eidos/components/context-menu/`). // Same family/event vocabulary as dropdown-menu — sound from family bases. 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/', texts: { label: '#?components.context-menu.label|Context menu' diff --git a/src/uix/morfo/components/dropdown-menu.ts b/src/uix/morfo/components/dropdown-menu.ts index 7dff8fc34..93556a1cf 100644 --- a/src/uix/morfo/components/dropdown-menu.ts +++ b/src/uix/morfo/components/dropdown-menu.ts @@ -9,7 +9,11 @@ export const dropdownMenuMorfo = { // commit-select from the `commit` family base — both ship sound // out of the box, no per-component cascade needed. 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/', texts: { label: '#?components.dropdown-menu.label|Dropdown menu' diff --git a/src/uix/morfo/components/menubar.ts b/src/uix/morfo/components/menubar.ts index 9f6a4a846..9c2b9f9e9 100644 --- a/src/uix/morfo/components/menubar.ts +++ b/src/uix/morfo/components/menubar.ts @@ -5,7 +5,10 @@ export const menubarMorfo = { name: 'Menubar', kebab: 'menubar', 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/', texts: { label: '#?components.menubar.label|Menubar' diff --git a/src/uix/morfo/components/navigation-menu.ts b/src/uix/morfo/components/navigation-menu.ts index c10b89a88..624df6e00 100644 --- a/src/uix/morfo/components/navigation-menu.ts +++ b/src/uix/morfo/components/navigation-menu.ts @@ -5,7 +5,10 @@ export const navigationMenuMorfo = { name: 'NavigationMenu', kebab: 'navigation-menu', 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: { label: '#?components.navigation-menu.label|Main' }, diff --git a/src/uix/morfo/components/switch.ts b/src/uix/morfo/components/switch.ts index d9f7ad5ad..b877b2ec8 100644 --- a/src/uix/morfo/components/switch.ts +++ b/src/uix/morfo/components/switch.ts @@ -22,7 +22,9 @@ export const switchMorfo = { name: 'Switch', kebab: 'switch', 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: { label: '#?components.switch.label|Switch' }, diff --git a/src/uix/morfo/components/toggle-group.ts b/src/uix/morfo/components/toggle-group.ts index a80913b77..612f91c1a 100644 --- a/src/uix/morfo/components/toggle-group.ts +++ b/src/uix/morfo/components/toggle-group.ts @@ -8,7 +8,11 @@ export const toggleGroupMorfo = { // Sema picks up the `commit-toggle` event via the `commit` family base — // no per-component cascade needed (commit ships sound out of the box). 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/', texts: { label: '#?components.toggle-group.label|Toggle group' diff --git a/src/uix/morfo/components/toggle.ts b/src/uix/morfo/components/toggle.ts index c0a26ca0e..281ef92cc 100644 --- a/src/uix/morfo/components/toggle.ts +++ b/src/uix/morfo/components/toggle.ts @@ -23,7 +23,9 @@ export const toggleMorfo = { name: 'Toggle', kebab: 'toggle', 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: { label: '#?components.toggle.label|Toggle' }, diff --git a/src/uix/morfo/components/tooltip.ts b/src/uix/morfo/components/tooltip.ts index 68dac7595..cd060e1f1 100644 --- a/src/uix/morfo/components/tooltip.ts +++ b/src/uix/morfo/components/tooltip.ts @@ -12,7 +12,10 @@ export const tooltipMorfo = { name: 'Tooltip', kebab: 'tooltip', 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/', texts: { label: '#?components.tooltip.label|Tooltip' diff --git a/src/uix/morfo/components/tree-grid.ts b/src/uix/morfo/components/tree-grid.ts index 235e45395..edf5fd987 100644 --- a/src/uix/morfo/components/tree-grid.ts +++ b/src/uix/morfo/components/tree-grid.ts @@ -5,7 +5,10 @@ export const treeGridMorfo = { name: 'TreeGrid', kebab: 'tree-grid', 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/', texts: { label: '#?components.tree-grid.label|Tree grid', diff --git a/src/uix/morfo/components/tree-view.ts b/src/uix/morfo/components/tree-view.ts index 2ed8b49c8..ce3a4ec63 100644 --- a/src/uix/morfo/components/tree-view.ts +++ b/src/uix/morfo/components/tree-view.ts @@ -5,7 +5,11 @@ export const treeViewMorfo = { name: 'TreeView', kebab: 'tree-view', 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/', texts: { label: '#?components.tree-view.label|Tree' diff --git a/src/uix/sema/components/collapsible.ts b/src/uix/sema/components/collapsible.ts new file mode 100644 index 000000000..0772d8af2 --- /dev/null +++ b/src/uix/sema/components/collapsible.ts @@ -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>[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 }) + } + ] +}; diff --git a/src/uix/sema/components/context-menu.ts b/src/uix/sema/components/context-menu.ts new file mode 100644 index 000000000..f999eb9e9 --- /dev/null +++ b/src/uix/sema/components/context-menu.ts @@ -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>[2]) => + semaSelector(contextMenuMorfo, 'content', matchers); +const onItem = (matchers?: Parameters>[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 } + } + ] +}; diff --git a/src/uix/sema/components/dropdown-menu.ts b/src/uix/sema/components/dropdown-menu.ts new file mode 100644 index 000000000..894d44fe3 --- /dev/null +++ b/src/uix/sema/components/dropdown-menu.ts @@ -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>[2]) => + semaSelector(dropdownMenuMorfo, 'content', matchers); +const onItem = (matchers?: Parameters>[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 } + } + ] +}; diff --git a/src/uix/sema/components/index.ts b/src/uix/sema/components/index.ts index 73d9d9626..511087377 100644 --- a/src/uix/sema/components/index.ts +++ b/src/uix/sema/components/index.ts @@ -11,14 +11,19 @@ export { accordionSema } from './accordion'; export { checkboxSema } from './checkbox'; +export { collapsibleSema } from './collapsible'; export { colorPickerSema } from './color-picker'; export { comboboxSema } from './combobox'; +export { contextMenuSema } from './context-menu'; export { dateFieldSema } from './date-field'; export { dialogSema } from './dialog'; export { drawerSema } from './drawer'; +export { dropdownMenuSema } from './dropdown-menu'; export { editableSema } from './editable'; export { fileUploadSema } from './file-upload'; export { formSema } from './form'; +export { menubarSema } from './menubar'; +export { navigationMenuSema } from './navigation-menu'; export { numberFieldSema } from './number-field'; export { paginationSema } from './pagination'; export { passwordFieldSema } from './password-field'; @@ -30,10 +35,16 @@ export { selectSema } from './select'; export { sliderSema } from './slider'; export { splitterSema } from './splitter'; export { stepperSema } from './stepper'; +export { switchSema } from './switch'; export { tabsSema } from './tabs'; export { tagGroupSema } from './tag-group'; export { tagsInputSema } from './tags-input'; export { textareaSema } from './textarea'; export { toastSema } from './toast'; +export { toggleSema } from './toggle'; +export { toggleGroupSema } from './toggle-group'; export { toolbarSema } from './toolbar'; +export { tooltipSema } from './tooltip'; +export { treeViewSema } from './tree-view'; +export { treeGridSema } from './tree-grid'; export { wordsSema } from './words'; diff --git a/src/uix/sema/components/menubar.ts b/src/uix/sema/components/menubar.ts new file mode 100644 index 000000000..32dc3a99f --- /dev/null +++ b/src/uix/sema/components/menubar.ts @@ -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>[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 } + } + ] +}; diff --git a/src/uix/sema/components/navigation-menu.ts b/src/uix/sema/components/navigation-menu.ts new file mode 100644 index 000000000..639c133f3 --- /dev/null +++ b/src/uix/sema/components/navigation-menu.ts @@ -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>[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 } + } + ] +}; diff --git a/src/uix/sema/components/switch.ts b/src/uix/sema/components/switch.ts new file mode 100644 index 000000000..41dce9e15 --- /dev/null +++ b/src/uix/sema/components/switch.ts @@ -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>[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 } + } + ] +}; diff --git a/src/uix/sema/components/toggle-group.ts b/src/uix/sema/components/toggle-group.ts new file mode 100644 index 000000000..19b1decde --- /dev/null +++ b/src/uix/sema/components/toggle-group.ts @@ -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>[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 } + } + ] +}; diff --git a/src/uix/sema/components/toggle.ts b/src/uix/sema/components/toggle.ts new file mode 100644 index 000000000..462dff5db --- /dev/null +++ b/src/uix/sema/components/toggle.ts @@ -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>[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 } + } + ] +}; diff --git a/src/uix/sema/components/tooltip.ts b/src/uix/sema/components/tooltip.ts new file mode 100644 index 000000000..c6f5ae486 --- /dev/null +++ b/src/uix/sema/components/tooltip.ts @@ -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>[2]) => + semaSelector(tooltipMorfo, 'content', matchers); + +export const tooltipSema: Sema = { + name: 'tooltip', + cascade: [ + { + selector: onContent({ eventFamily: 'emerge' }), + sound: soundTuning('tooltip.silent') + } + ] +}; diff --git a/src/uix/sema/components/tree-grid.ts b/src/uix/sema/components/tree-grid.ts new file mode 100644 index 000000000..692456c29 --- /dev/null +++ b/src/uix/sema/components/tree-grid.ts @@ -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>[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 } + } + ] +}; diff --git a/src/uix/sema/components/tree-view.ts b/src/uix/sema/components/tree-view.ts new file mode 100644 index 000000000..b56e5b82b --- /dev/null +++ b/src/uix/sema/components/tree-view.ts @@ -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>[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 } + } + ] +}; diff --git a/src/uix/sema/sounds.ts b/src/uix/sema/sounds.ts index d3ad5b54d..566d59f92 100644 --- a/src/uix/sema/sounds.ts +++ b/src/uix/sema/sounds.ts @@ -138,6 +138,25 @@ export const SOUND_TUNINGS = { 'emerge.dismiss.passive': { gain: 0.06 }, 'form.commit.soft': { gain: 0.05 }, '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 } } as const satisfies Record;