|
|
import { context, type WithRefOpts } from '../../provider';
|
|
|
import { type ActiveProps } from '$libs/reactive';
|
|
|
import { Soma } from '../../core/soma.svelte';
|
|
|
import type { MeterZone } from './types';
|
|
|
|
|
|
import { meterMorfo } from '../../../morfo/components/meter';
|
|
|
import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte';
|
|
|
|
|
|
function computeZone(value: number, low: number, high: number): MeterZone {
|
|
|
if (value < low) return 'below';
|
|
|
if (value > high) return 'above';
|
|
|
return 'optimum';
|
|
|
}
|
|
|
|
|
|
// ── Root provider ──────────────────────────────────────────────────────────
|
|
|
|
|
|
interface MeterOpts
|
|
|
extends
|
|
|
WithRefOpts,
|
|
|
ActiveProps<{
|
|
|
value: number;
|
|
|
min: number;
|
|
|
max: number;
|
|
|
low: number | undefined;
|
|
|
high: number | undefined;
|
|
|
optimum: number | undefined;
|
|
|
ariaLabelledby: string | undefined;
|
|
|
valueText: string | undefined;
|
|
|
}> {}
|
|
|
|
|
|
export class MeterProvider {
|
|
|
readonly opts: MeterOpts;
|
|
|
readonly runtimePart: SomaRuntimePart<typeof meterMorfo, 'provider'>;
|
|
|
readonly soma: Soma;
|
|
|
readonly runtime: SomaRuntime<typeof meterMorfo>;
|
|
|
static readonly ctx = context<MeterProvider>('Meter');
|
|
|
static get(): MeterProvider | undefined {
|
|
|
return this.ctx.getOr(undefined) as MeterProvider | undefined;
|
|
|
}
|
|
|
static require(): MeterProvider {
|
|
|
return this.ctx.get();
|
|
|
}
|
|
|
|
|
|
static create(opts: MeterOpts) {
|
|
|
return new MeterProvider(opts);
|
|
|
}
|
|
|
|
|
|
private constructor(opts: MeterOpts) {
|
|
|
this.opts = opts;
|
|
|
this.soma = Soma.require();
|
|
|
this.runtime = this.soma.runtime(meterMorfo, {
|
|
|
states: {
|
|
|
optimum: () => this.state
|
|
|
},
|
|
|
props: {
|
|
|
min: () => this.opts.min.current,
|
|
|
max: () => this.opts.max.current,
|
|
|
low: () => this.resolvedLow,
|
|
|
high: () => this.resolvedHigh,
|
|
|
optimum: () => this.resolvedOptimum,
|
|
|
value: () => this.opts.value.current,
|
|
|
valueText: () => this.resolvedValueText,
|
|
|
ariaLabelledby: () => this.opts.ariaLabelledby.current
|
|
|
}
|
|
|
});
|
|
|
this.runtimePart = this.runtime.part('provider', {
|
|
|
id: opts.id,
|
|
|
ref: opts.ref,
|
|
|
owner: this,
|
|
|
context: MeterProvider.ctx
|
|
|
});
|
|
|
}
|
|
|
|
|
|
readonly resolvedLow = $derived.by(() => this.opts.low.current ?? this.opts.min.current);
|
|
|
readonly resolvedHigh = $derived.by(() => this.opts.high.current ?? this.opts.max.current);
|
|
|
readonly resolvedOptimum = $derived.by(
|
|
|
() => this.opts.optimum.current ?? (this.resolvedLow + this.resolvedHigh) / 2
|
|
|
);
|
|
|
|
|
|
readonly state: MeterZone = $derived.by(() =>
|
|
|
computeZone(this.opts.value.current, this.resolvedLow, this.resolvedHigh)
|
|
|
);
|
|
|
|
|
|
private percentageFor(value: number): number {
|
|
|
const min = this.opts.min.current;
|
|
|
const max = this.opts.max.current;
|
|
|
if (max <= min) return 0;
|
|
|
return Math.max(0, Math.min(100, ((value - min) / (max - min)) * 100));
|
|
|
}
|
|
|
|
|
|
/** 0–100 percentage for CSS vars. Clamped to the min/max range. */
|
|
|
readonly percentage = $derived.by(() => this.percentageFor(this.opts.value.current));
|
|
|
readonly lowPercentage = $derived.by(() => this.percentageFor(this.resolvedLow));
|
|
|
readonly highPercentage = $derived.by(() => this.percentageFor(this.resolvedHigh));
|
|
|
readonly optimumPercentage = $derived.by(() => this.percentageFor(this.resolvedOptimum));
|
|
|
|
|
|
readonly resolvedValueText = $derived.by(() => {
|
|
|
const explicit = this.opts.valueText.current;
|
|
|
if (explicit) return explicit;
|
|
|
return `${this.opts.value.current} / ${this.opts.max.current}`;
|
|
|
});
|
|
|
|
|
|
readonly props = $derived.by(() =>
|
|
|
this.runtimePart.assert({
|
|
|
...this.runtimePart.props,
|
|
|
style: {
|
|
|
'--_meter-value-pct': `${this.percentage}`,
|
|
|
'--_meter-low-pct': `${this.lowPercentage}`,
|
|
|
'--_meter-high-pct': `${this.highPercentage}`,
|
|
|
'--_meter-optimum-pct': `${this.optimumPercentage}`
|
|
|
}
|
|
|
} as const)
|
|
|
);
|
|
|
}
|
|
|
|
|
|
// ── Indicator ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
interface MeterIndicatorOpts extends WithRefOpts {}
|
|
|
|
|
|
export class MeterIndicatorProvider {
|
|
|
readonly opts: MeterIndicatorOpts;
|
|
|
readonly runtimePart: SomaRuntimePart<typeof meterMorfo, 'indicator'>;
|
|
|
static create(opts: MeterIndicatorOpts) {
|
|
|
return new MeterIndicatorProvider(opts);
|
|
|
}
|
|
|
|
|
|
readonly provider: MeterProvider;
|
|
|
|
|
|
private constructor(opts: MeterIndicatorOpts) {
|
|
|
this.opts = opts;
|
|
|
this.provider = MeterProvider.require();
|
|
|
this.runtimePart = this.provider.runtime.part('indicator', {
|
|
|
id: opts.id,
|
|
|
ref: opts.ref,
|
|
|
states: {
|
|
|
optimum: () => this.provider.state
|
|
|
},
|
|
|
owner: this
|
|
|
});
|
|
|
}
|
|
|
|
|
|
readonly props = $derived.by(() =>
|
|
|
this.runtimePart.assert({
|
|
|
...this.runtimePart.props
|
|
|
} as const)
|
|
|
);
|
|
|
}
|