/** * 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;