/**
* Counts a number up and down by a configurable step, stopping at optional
* minimum and maximum bounds, and resets it to zero. Exposes the current
* value alongside increment, decrement and reset streams.
*
* Embed it as `` and it
* mutates the host's own cell: the host wires nothing, because passing the
* cell is the wiring. Run it standalone and it counts its own default.
*
* This atom is also the pattern index's measurement control. "Build me a
* counter" is the task the loop's before/after numbers are anchored on, so the
* corpus holds a counter that takes real inputs rather than only whole counter
* applications that take none.
*
* @hashtags counter, increment, decrement, number, stepper, tally
* @keywords count, counting, increment, decrement, plus, minus, step, tally,
* score, quantity, bump, up down, numeric input
*/
import {
action,
computed,
Default,
NAME,
pattern,
Stream,
UI,
type VNode,
Writable,
} from "commonfabric";
export interface CounterInput {
/** The number being counted. A host passes its own cell to share it. */
value?: Writable>;
/** How much one increment or decrement moves `value`. */
step?: Writable>;
/** Heading shown above the number. */
label?: Writable>;
/** Lowest value the buttons will reach. `null` leaves it unbounded. */
min?: Writable>;
/** Highest value the buttons will reach. `null` leaves it unbounded. */
max?: Writable>;
}
export interface CounterOutput {
[NAME]: string;
[UI]: VNode;
value: number;
atMin: boolean;
atMax: boolean;
increment: Stream;
decrement: Stream;
reset: Stream;
}
/** `value` moved by `delta` and held inside whichever bounds are set. */
const clamp = (
value: number,
min: number | null,
max: number | null,
): number => {
if (min !== null && value < min) return min;
if (max !== null && value > max) return max;
return value;
};
export const Counter = pattern(
({ value, step, label, min, max }) => {
const increment = action(() => {
value.set(
clamp((value.get() ?? 0) + (step.get() ?? 1), min.get(), max.get()),
);
});
const decrement = action(() => {
value.set(
clamp((value.get() ?? 0) - (step.get() ?? 1), min.get(), max.get()),
);
});
const reset = action(() => {
value.set(clamp(0, min.get(), max.get()));
});
const atMin = computed(() => {
const floor = min.get();
return floor !== null && (value.get() ?? 0) <= floor;
});
const atMax = computed(() => {
const ceiling = max.get();
return ceiling !== null && (value.get() ?? 0) >= ceiling;
});
return {
[NAME]: computed(() => `${label.get()}: ${value.get() ?? 0}`),
[UI]: (
{label}
{value}
−
Reset
+
),
value,
atMin,
atMax,
increment,
decrement,
reset,
};
},
);
export default Counter;