You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/soma/components/toggle/toggle-provider.svelte.ts

163 lines
5.5 KiB

import { Soma } from '../../core/soma.svelte';
import { FieldProvider } from '../field/field-provider.svelte';
import { toggleMorfo } from '../../../morfo/components/toggle';
import type { OptsFromProps } from '../../provider';
import type { OnChangeFn, SomaMouseEvent } from '../../types';
import type { ToggleProps } from './types';
import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte';
// ── Provider ───────────────────────────────────────────────────────────────
/**
* Provider's reactive contract derived from `ToggleProps`.
*
* - `Managed` — the SOMA-managed subset of public Props (excludes
* `children`, `child`, HTML passthroughs).
* - `'pressed'` — the bindable subset.
* - `id` and `ref` are baked in by `OptsFromProps`.
*
* Add a new prop in `types.ts` → list it in the `Managed` union here →
* pass it through `bindProps` in the wrapper. No third place to keep in sync.
*/
type ToggleOpts = OptsFromProps<
ToggleProps,
| 'pressed'
| 'disabled'
| 'readonly'
| 'required'
| 'invalid'
| 'intent'
| 'color'
| 'name'
| 'value'
| 'aria-label'
| 'onPressedChange',
'pressed'
>;
/**
* Toggle provider — runtime-direct.
*
* Earlier soma components extended a `Provider` base class that built
* render props from the morfo. Toggle is the pilot for skipping that base:
* the morfo runtime
* (`soma.runtime(toggleMorfo, sources)`) already owns the data-/aria-attr
* derivation and ref attachment; the provider only needs handler wiring
* and a few derived values (`isDisabled`, `snippetProps`).
*
* No inheritance, no local prop-contract helper, no module-level contract
* registration. Eidos's `lint.ts` and the morfo type system already catch
* contract violations at design time.
*/
export class ToggleProvider {
static create(opts: ToggleOpts) {
return new ToggleProvider(opts);
}
readonly opts: ToggleOpts;
readonly field = FieldProvider.get();
readonly runtime: SomaRuntime;
readonly runtimePart: SomaRuntimePart;
readonly soma: Soma;
private constructor(opts: ToggleOpts) {
this.opts = opts;
// Register as the Field's primary input so `Field.Label`'s `for`
// attribute lands on this button. Direct assign — NOT $effect (A30).
if (this.field) this.field.inputId.current = opts.id.current;
// SomaRuntime — owns the morfo-derived attrs (data-state, aria-pressed,
// aria-disabled, etc.) plus ref attachment via `part(...)`.
//
// Single doctrinal event (anexo del libro, Tabla 1):
// - `commit-toggle` (commit / neutral) — el cierre perceptivo del
// toggle. El handler flipea el estado bound. El gesto de contact
// está implícito en el botón nativo; no se descompone en dos
// eventos porque la operación es instantánea.
this.soma = Soma.require();
this.runtime = this.soma.runtime(toggleMorfo, {
states: {
on: () => opts.pressed.current
},
props: {
disabled: () => this.isDisabled,
readonly: () => this.isReadonly,
required: () => this.isRequired,
invalid: () => this.isInvalid,
intent: () => opts.intent.current ?? 'neutral',
/**
* Resolved visual color. Doctrine: aesthetic must not
* contradict semantic. When intent is evaluative
* (affirm/risk/threat), it WINS over any hierarchy
* override. When intent is neutral, `color` may promote
* the visual to primary/secondary hierarchy; if neither,
* default 'neutral'.
*/
color: () => {
const intent = opts.intent.current ?? 'neutral';
if (intent !== 'neutral') return intent;
return opts.color.current ?? 'neutral';
},
'aria-label': () => opts['aria-label'].current
},
events: {
'commit-toggle': () => {
const next = !opts.pressed.current;
opts.pressed.current = next;
opts.onPressedChange.current?.(next);
}
}
});
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
owner: this,
syncAttrs: true
});
}
readonly isDisabled = $derived.by(
() => this.opts.disabled.current || (this.field?.isDisabled ?? false)
);
readonly isReadonly = $derived.by(
() => this.opts.readonly.current || (this.field?.isReadonly ?? false)
);
readonly isRequired = $derived.by(
() => this.opts.required.current || (this.field?.isRequired ?? false)
);
readonly isInvalid = $derived.by(
() => this.opts.invalid.current || (this.field?.isInvalid ?? false)
);
readonly snippetProps = $derived.by(() => ({
pressed: this.opts.pressed.current
}));
readonly onclick = (_e: SomaMouseEvent<HTMLButtonElement>) => {
if (this.isDisabled || this.isReadonly) return;
// Single perceptual moment: the consolidation of the new state.
// `commit-toggle` is post-sequence: handler flips state first,
// effects flush, then semantic.emit runs visual + sound + haptic
// channels against the resolved DOM.
void this.runtime.trigger('commit-toggle');
};
/**
* Render-time props. Composed of:
* - identity from the runtime (`id`, `data-toggle`, ref attachment)
* - the HTML `disabled` attribute (not a morfo concern — primitive
* button semantics)
* - the `onclick` handler
*/
readonly props = $derived.by(() => ({
...this.runtimePart.props,
disabled: this.isDisabled || undefined,
onclick: this.onclick
}));
}
// `OnChangeFn` re-export — kept so consumers' editor inspection on the wrapper
// still resolves the type by the provider symbol path (legacy import shape).
export type { OnChangeFn };

Powered by TurnKey Linux.