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.
123 lines
3.8 KiB
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 };
|