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/button/button-provider.svelte.ts

123 lines
3.8 KiB

import { Soma } from '../../core/soma.svelte';
import { FieldProvider } from '../field';
import { buttonMorfo } from '../../../morfo/components/button';
import type { OptsFromProps } from '../../provider';
import type { OnChangeFn, SomaMouseEvent } from '../../types';
import type { ButtonProps, ButtonIntent } from './types';
import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte';
// ── Provider ───────────────────────────────────────────────────────────────
/**
* Provider's reactive contract derived from `ButtonProps`.
*/
type ButtonOpts = OptsFromProps<
ButtonProps,
| 'intent'
| 'color'
| 'type'
| 'disabled'
| 'loading'
| 'loadingText'
| 'spinnerPlacement'
| 'aria-label'
| 'onPress',
never
>;
/**
* Button provider — runtime-direct.
*
* Owns the press → `commit-action` flow. 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.
*/
export class ButtonProvider {
static create(opts: ButtonOpts) {
return new ButtonProvider(opts);
}
readonly opts: ButtonOpts;
readonly field = FieldProvider.get();
readonly runtime: SomaRuntime;
readonly runtimePart: SomaRuntimePart;
readonly soma: Soma;
private constructor(opts: ButtonOpts) {
this.opts = opts;
this.soma = Soma.require();
this.runtime = this.soma.runtime(buttonMorfo, {
states: {
loading: () => opts.loading.current ?? false
},
props: {
disabled: () => this.isDisabled,
loading: () => opts.loading.current ?? false,
intent: () => opts.intent.current ?? 'neutral',
type: () => opts.type.current ?? 'button',
/**
* Resolved visual color. Doctrine: aesthetic must not
* contradict semantic. When intent is evaluative
* (affirm/fulfill/risk/threat/loss), it WINS over any
* hierarchy override. When intent is neutral, `color` may
* promote the visual to primary/secondary; if neither,
* default 'primary' (Button's default is promoted because
* actions are typically the focal point of their surface).
*/
color: () => {
const intent = opts.intent.current ?? 'neutral';
if (intent !== 'neutral') return intent;
return opts.color.current ?? 'primary';
},
'aria-label': () => opts['aria-label'].current
},
events: {
'commit-action': () => {
// The consumer's onclick has already been (or will be)
// invoked by native button click dispatch. We only fire
// onPress for diagnostic / analytics consumers.
opts.onPress.current?.({ intent: opts.intent.current ?? 'neutral' });
}
}
});
this.runtimePart = this.runtime.part('provider', {
id: opts.id,
ref: opts.ref,
owner: this,
syncAttrs: true
});
}
readonly isDisabled = $derived.by(
() => (this.opts.disabled.current ?? false) || (this.field?.isDisabled ?? false)
);
readonly isLoading = $derived.by(() => this.opts.loading.current ?? false);
readonly snippetProps = $derived.by(() => ({
loading: this.isLoading,
disabled: this.isDisabled
}));
readonly onclick = (_e: SomaMouseEvent<HTMLButtonElement>) => {
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');
};
/**
* Render-time props. Composed of:
* - identity from the runtime (`id`, `data-button`, ref attachment)
* - the HTML `disabled` attribute
* - the `onclick` handler
*/
readonly props = $derived.by(() => ({
...this.runtimePart.props,
disabled: this.isDisabled || undefined,
onclick: this.onclick
}));
}
export type { OnChangeFn, ButtonIntent };

Powered by TurnKey Linux.