diff --git a/scripts/morfo-vocabulary-check.ts b/scripts/morfo-vocabulary-check.ts index 82d831277..0c5f2c225 100644 --- a/scripts/morfo-vocabulary-check.ts +++ b/scripts/morfo-vocabulary-check.ts @@ -52,13 +52,8 @@ const MORFOS_DIR = join(__dirname, '..', 'src', 'uix', 'morfo', 'components'); * canon. Remove an entry the moment the underlying morfo is updated. */ const EVENT_NAME_ALLOWLIST: Record = { - // TODO(Button): `commit-action` uses verb `action`, not in canon. - // Per book cap. 22 §11 "el intent fuerte no debería vivir en el contacto, - // sino en la señal o consecuencia posterior." Deciding between - // (a) `contact-activate` + intent stays as visual chip variant only - // (b) keep `commit` family but pick a canonical verb (e.g. acknowledge) - // is an API decision that needs design sign-off — see Plan B commit 2. - 'button:commit-action': 'Pending Button intent/family design decision (Plan B commit 2)' + // Empty — Plan B commit 2 resolved `button.commit-action` to + // `button.contact-activate` per book cap. 22 §10-11. }; async function loadMorfos(): Promise { diff --git a/src/uix/eidos/components/button/button.svelte b/src/uix/eidos/components/button/button.svelte index 88943dfa4..44855c6dc 100644 --- a/src/uix/eidos/components/button/button.svelte +++ b/src/uix/eidos/components/button/button.svelte @@ -3,10 +3,11 @@ * Eidos ` + +``` + +This way the Button stops "celebrating before time" (book cap. 22 §8) — +the celebration sound plays at the moment the save *actually* resolves, +not when the click is received. ## See also diff --git a/src/uix/soma/components/button/button-provider.svelte.ts b/src/uix/soma/components/button/button-provider.svelte.ts index 720042c9f..2dec172e2 100644 --- a/src/uix/soma/components/button/button-provider.svelte.ts +++ b/src/uix/soma/components/button/button-provider.svelte.ts @@ -28,9 +28,12 @@ type ButtonOpts = OptsFromProps< /** * Button provider — runtime-direct. * - * Owns the press → `commit-action` flow. Sequence is `pre` so the + * Owns the press → `contact-activate` flow per «Diseñando lo que ocurre» + * cap. 22 §10 ("Botón: contact.press"). Sequence is `pre` so the * perceptual signal lands at the moment of the click; the consumer's - * onclick may then run async work without blocking it. + * onclick may then run async work that might fire its own commit/signal + * events when the actual outcome lands. The Button itself does not + * pretend to know what the click resolves. */ export class ButtonProvider { static create(opts: ButtonOpts) { @@ -73,10 +76,13 @@ export class ButtonProvider { 'aria-label': () => opts['aria-label'].current }, events: { - 'commit-action': () => { + 'contact-activate': () => { // The consumer's onclick has already been (or will be) // invoked by native button click dispatch. We only fire - // onPress for diagnostic / analytics consumers. + // onPress for diagnostic / analytics consumers. The + // `intent` echoed back is the visual prop value (visual + // context for analytics), not a semantic event payload — + // the contact carries no intent per book cap. 22 §11. opts.onPress.current?.({ intent: opts.intent.current ?? 'neutral' }); } } @@ -102,8 +108,9 @@ export class ButtonProvider { readonly onclick = (_e: SomaMouseEvent) => { if (this.isDisabled || this.isLoading) return; // Sequence 'pre' — the perceptual signal fires synchronously with - // the click; consumer's onclick runs in parallel. - void this.runtime.trigger('commit-action'); + // the click; consumer's onclick runs in parallel and may dispatch + // its own commit/signal events when the real outcome lands. + void this.runtime.trigger('contact-activate'); }; /** diff --git a/src/uix/soma/components/button/types.ts b/src/uix/soma/components/button/types.ts index 281dad09f..9d0bc0529 100644 --- a/src/uix/soma/components/button/types.ts +++ b/src/uix/soma/components/button/types.ts @@ -2,19 +2,28 @@ import type { WithChild, Without, OnChangeFn } from '../../types'; import type { PrimitiveButtonAttributes, PrimitiveSpanAttributes } from '../../types'; /** - * Doctrinal intent of the Button's commit (anexo Tabla 1). + * Visual evaluative tint of the Button's surface. * - * - `neutral` (default) — action without affective load - * - `affirm` — positive lightweight commit (e.g. "subscribe") + * - `neutral` (default) — no affective tint; `color` hierarchy wins + * - `affirm` — positive lightweight (e.g. "subscribe", "follow") * - `fulfill` — closure / completion (e.g. "save", "submit") * - `risk` — moderate negative consequence (e.g. "leave page") * - `threat` — active negative consequence (e.g. "delete") * - `loss` — irreversible loss (e.g. "delete forever", "sign out") * - * Drives the `commit-action` event's non-visual perceptual signature - * (sound / haptic deltas via `SEMA_MAP.intents`). When non-neutral, also - * wins the visual `data-color` over any `color` hierarchy override — - * doctrine forbids aesthetic from contradicting semantic. + * Per book cap. 22 §11 ("el intent fuerte no debería vivir en el + * contacto"), this prop does NOT load the morfo's `contact-activate` + * event with intent. It drives the visual `data-color` (anticipatory + * weight via form/color) and the eidos chip variant. + * + * For the actual evaluative consequence (the sound at click, the haptic + * at delete, etc.), consumers wire a downstream semantic event: + * + * - `commit.save + affirm` when the save resolves + * - `signal.warn + threat` BEFORE the delete confirmation + * - `commit.delete + loss` AFTER the destructive operation completes + * + * This way the Button stops "celebrating before time" (book cap. 22 §8). */ export type ButtonIntent = 'neutral' | 'affirm' | 'fulfill' | 'risk' | 'threat' | 'loss'; @@ -44,8 +53,11 @@ export type ButtonProps = WithChild< /** DOM id. Auto-generated when omitted. */ id?: string; /** - * Doctrinal intent of the action's consequence. Always present; - * default `neutral`. See {@link ButtonIntent}. + * Visual evaluative tint of the button's surface. Drives the chip + * variant + `data-color`. Does NOT load the contact event with + * intent — per book cap. 22 §11 the semantic intent of the + * consequence lives in whatever commit/signal the consumer fires + * downstream. See {@link ButtonIntent}. * * @default 'neutral' */ @@ -70,7 +82,7 @@ export type ButtonProps = WithChild< disabled?: boolean; /** * Loading state. While true, the click handler is gated (no - * `commit-action` emitted), the button is `aria-busy`, and the + * `contact-activate` emitted), the button is `aria-busy`, and the * Spinner part is rendered. The Eidos recipe may also swap the * label with the localised `loadingText`. * @@ -92,10 +104,11 @@ export type ButtonProps = WithChild< /** Override accessible name. Required when `iconOnly` is true. */ 'aria-label'?: string; /** - * Called on every press change for diagnostic / analytics - * purposes. The actual side effect is the consumer's - * `onclick` — `onPress` is read-only feedback that the - * `commit-action` event fired. + * Called on every press for diagnostic / analytics purposes. The + * actual side effect is the consumer's `onclick` — `onPress` is + * read-only feedback that the `contact-activate` event fired. + * The `intent` payload echoes the visual prop value, not a + * semantic claim about the press. */ onPress?: OnChangeFn<{ intent: ButtonIntent }>; },