@mmstack/primitives 19.1.1 → 19.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,75 @@
1
+ import { ElementRef, Signal } from '@angular/core';
2
+ type MousePosition = {
3
+ x: number;
4
+ y: number;
5
+ };
6
+ /**
7
+ * Options for configuring the `mousePosition` sensor.
8
+ */
9
+ export type MousePositionOptions = {
10
+ /**
11
+ * The target element to listen for mouse movements on.
12
+ * Can be `window`, `document`, an `HTMLElement`, or an `ElementRef<HTMLElement>`.
13
+ * @default window
14
+ */
15
+ target?: Window | Document | HTMLElement | ElementRef<HTMLElement>;
16
+ /**
17
+ * Defines the coordinate system for the reported position.
18
+ * - `'client'`: Coordinates relative to the viewport (`clientX`, `clientY`).
19
+ * - `'page'`: Coordinates relative to the entire document (`pageX`, `pageY`).
20
+ * @default 'client'
21
+ */
22
+ coordinateSpace?: 'client' | 'page';
23
+ /**
24
+ * If `true`, the sensor will also listen to `touchmove` events and report
25
+ * the coordinates of the first touch point.
26
+ * @default false
27
+ */
28
+ touch?: boolean;
29
+ /**
30
+ * Optional debug name for the internal signal.
31
+ */
32
+ debugName?: string;
33
+ /**
34
+ * Optional delay in milliseconds to throttle the updates.
35
+ * @default 100
36
+ */
37
+ throttle?: number;
38
+ };
39
+ export type MousePositionSignal = Signal<MousePosition> & {
40
+ readonly unthrottled: Signal<MousePosition>;
41
+ };
42
+ /**
43
+ * Creates a read-only signal that tracks the mouse cursor's position.
44
+ *
45
+ * It can track mouse movements on a specific target (window, document, or element)
46
+ * and optionally include touch movements. The coordinate space ('client' or 'page')
47
+ * can also be configured.
48
+ * The primitive is SSR-safe and automatically cleans up its event listeners.
49
+ *
50
+ * @param options Optional configuration for the sensor.
51
+ * @returns A read-only `Signal<MousePosition>`. On the server, it returns a static
52
+ * signal with `{ x: 0, y: 0 }`.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * import { Component, effect } from '@angular/core';
57
+ * import { mousePosition } from '@mmstack/primitives';
58
+ *
59
+ * @Component({
60
+ * selector: 'app-mouse-tracker',
61
+ * template: `<p>Mouse Position: X: {{ pos().x }}, Y: {{ pos().y }}</p>`
62
+ * })
63
+ * export class MouseTrackerComponent {
64
+ * readonly pos = mousePosition({ coordinateSpace: 'page' });
65
+ *
66
+ * constructor() {
67
+ * effect(() => {
68
+ * console.log('Mouse moved to:', this.pos());
69
+ * });
70
+ * }
71
+ * }
72
+ * ```
73
+ */
74
+ export declare function mousePosition(opt?: MousePositionOptions): MousePositionSignal;
75
+ export {};
@@ -0,0 +1,20 @@
1
+ import { Signal } from '@angular/core';
2
+ /**
3
+ * A specialized Signal that tracks network status.
4
+ * It's a boolean signal with an attached `since` signal.
5
+ */
6
+ export type NetworkStatusSignal = Signal<boolean> & {
7
+ /** A signal tracking the timestamp of the last status change. */
8
+ readonly since: Signal<Date>;
9
+ };
10
+ /**
11
+ * Creates a read-only signal that tracks the browser's online status.
12
+ *
13
+ * The main signal returns a boolean (`true` for online, `false` for offline).
14
+ * An additional `since` signal is attached, tracking when the status last changed.
15
+ * It's SSR-safe and automatically cleans up its event listeners.
16
+ *
17
+ * @param debugName Optional debug name for the signal.
18
+ * @returns A `NetworkStatusSignal` instance.
19
+ */
20
+ export declare function networkStatus(debugName?: string): NetworkStatusSignal;
@@ -0,0 +1,38 @@
1
+ import { Signal } from '@angular/core';
2
+ /**
3
+ * Creates a read-only signal that tracks the page's visibility state.
4
+ *
5
+ * It uses the browser's Page Visibility API to reactively report if the
6
+ * current document is `'visible'`, `'hidden'`, or in another state.
7
+ * The primitive is SSR-safe and automatically cleans up its event listeners
8
+ * when the creating context is destroyed.
9
+ *
10
+ * @param debugName Optional debug name for the signal.
11
+ * @returns A read-only `Signal<DocumentVisibilityState>`. On the server,
12
+ * it returns a static signal with a value of `'visible'`.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { Component, effect } from '@angular/core';
17
+ * import { pageVisibility } from '@mmstack/primitives';
18
+ *
19
+ * @Component({
20
+ * selector: 'app-visibility-tracker',
21
+ * template: `<p>Page is currently: {{ visibilityState() }}</p>`
22
+ * })
23
+ * export class VisibilityTrackerComponent {
24
+ * readonly visibilityState = pageVisibility();
25
+ *
26
+ * constructor() {
27
+ * effect(() => {
28
+ * if (this.visibilityState() === 'hidden') {
29
+ * console.log('Page is hidden, pausing expensive animations...');
30
+ * } else {
31
+ * console.log('Page is visible, resuming activity.');
32
+ * }
33
+ * });
34
+ * }
35
+ * }
36
+ * ```
37
+ */
38
+ export declare function pageVisibility(debugName?: string): Signal<DocumentVisibilityState>;
@@ -0,0 +1,56 @@
1
+ import { Signal } from '@angular/core';
2
+ import { MousePositionOptions, MousePositionSignal } from './mouse-position';
3
+ import { NetworkStatusSignal } from './network-status';
4
+ /**
5
+ * Creates a sensor signal that tracks the mouse cursor's position.
6
+ * @param type Must be `'mousePosition'`.
7
+ * @param options Optional configuration for the mouse position sensor.
8
+ * @returns A `MousePositionSignal` that tracks mouse coordinates and provides an unthrottled version.
9
+ * @see {mousePosition} for detailed documentation and examples.
10
+ * @example const pos = sensor('mousePosition', { coordinateSpace: 'page', throttle: 50 });
11
+ */
12
+ export declare function sensor(type: 'mousePosition', options?: MousePositionOptions): MousePositionSignal;
13
+ /**
14
+ * Creates a sensor signal that tracks the browser's online/offline status.
15
+ * @param type Must be `'networkStatus'`.
16
+ * @param options Optional configuration, currently only `debugName`.
17
+ * @returns A `NetworkStatusSignal` which is a boolean indicating online status, with an attached `since` signal.
18
+ * @see {networkStatus} for detailed documentation and examples.
19
+ * @example const onlineStatus = sensor('networkStatus');
20
+ */
21
+ export declare function sensor(type: 'networkStatus', options?: {
22
+ debugName?: string;
23
+ }): NetworkStatusSignal;
24
+ /**
25
+ * Creates a sensor signal that tracks the page's visibility state (e.g., 'visible', 'hidden').
26
+ * @param type Must be `'pageVisibility'`.
27
+ * @param options Optional configuration, currently only `debugName`.
28
+ * @returns A `Signal<DocumentVisibilityState>` indicating the page's current visibility.
29
+ * @see {pageVisibility} for detailed documentation and examples.
30
+ * @example const visibility = sensor('pageVisibility');
31
+ */
32
+ export declare function sensor(type: 'pageVisibility', options?: {
33
+ debugName?: string;
34
+ }): Signal<DocumentVisibilityState>;
35
+ /**
36
+ * Creates a sensor signal that tracks the user's OS/browser preference for a dark color scheme.
37
+ * @param type Must be `'dark-mode'`.
38
+ * @param options Optional configuration, currently only `debugName`.
39
+ * @returns A `Signal<boolean>` which is `true` if a dark theme is preferred.
40
+ * @see {prefersDarkMode} for detailed documentation and examples.
41
+ * @example const isDarkMode = sensor('dark-mode');
42
+ */
43
+ export declare function sensor(type: 'dark-mode', options?: {
44
+ debugName?: string;
45
+ }): Signal<boolean>;
46
+ /**
47
+ * Creates a sensor signal that tracks the user's OS/browser preference for reduced motion.
48
+ * @param type Must be `'reduced-motion'`.
49
+ * @param options Optional configuration, currently only `debugName`.
50
+ * @returns A `Signal<boolean>` which is `true` if reduced motion is preferred.
51
+ * @see {prefersReducedMotion} for detailed documentation and examples.
52
+ * @example const wantsReducedMotion = sensor('reduced-motion');
53
+ */
54
+ export declare function sensor(type: 'reduced-motion', options?: {
55
+ debugName?: string;
56
+ }): Signal<boolean>;
package/lib/stored.d.ts CHANGED
@@ -51,6 +51,16 @@ export type CreateStoredOptions<T> = CreateSignalOptions<T> & {
51
51
  * Requires a browser environment. Defaults to `false`.
52
52
  */
53
53
  syncTabs?: boolean;
54
+ /**
55
+ * Optional parameter to specify how key changes should be handled, load is the default.
56
+ * - `load`: The signal will load the value from storage when the key changes & replace the signal's value.
57
+ * - `store`: The signal will store the current value to the new key when the key changes.
58
+ */
59
+ onKeyChange?: 'load' | 'store';
60
+ /**
61
+ * If 'true', the signal will remove the old key from storage when the key changes, defaults to `false`.
62
+ */
63
+ cleanupOldKey?: boolean;
54
64
  };
55
65
  /**
56
66
  * A specialized `WritableSignal` returned by the `stored()` function.
@@ -122,5 +132,5 @@ export type StoredSignal<T> = WritableSignal<T> & {
122
132
  * }
123
133
  * ```
124
134
  */
125
- export declare function stored<T>(fallback: T, { key, store: providedStore, serialize, deserialize, syncTabs, equal, ...rest }: CreateStoredOptions<T>): StoredSignal<T>;
135
+ export declare function stored<T>(fallback: T, { key, store: providedStore, serialize, deserialize, syncTabs, equal, onKeyChange, cleanupOldKey, ...rest }: CreateStoredOptions<T>): StoredSignal<T>;
126
136
  export {};
@@ -0,0 +1,75 @@
1
+ import { type CreateSignalOptions, DestroyRef, type WritableSignal } from '@angular/core';
2
+ import { DebouncedSignal } from './debounced';
3
+ /**
4
+ * Options for creating a throttled writable signal.
5
+ * Extends Angular's `CreateSignalOptions` with a throttle time setting.
6
+ *
7
+ * @template T The type of value held by the signal.
8
+ */
9
+ export type CreateThrottledOptions<T> = CreateSignalOptions<T> & {
10
+ /**
11
+ * The throttle delay in milliseconds. The minimum time
12
+ * in milliseconds that must pass between updates to the throttled signal's value.
13
+ */
14
+ ms?: number;
15
+ /**
16
+ * Optional `DestroyRef` to clean up the throttle timer when the signal is destroyed.
17
+ * If provided, the timer will be cleared when the signal is destroyed.
18
+ * If the signal is called within a reactive context a DestroyRef is injected automatically.
19
+ * If it is not provided or injected, the timer will not be cleared automatically...which is usually fine :)
20
+ */
21
+ destroyRef?: DestroyRef;
22
+ };
23
+ /**
24
+ * A specialized `WritableSignal` whose publicly readable value updates are throttled.
25
+ *
26
+ * It provides access to the underlying, non-throttled signal via the `original` property.
27
+ *
28
+ * @template T The type of value held by the signal.
29
+ * @see {DebouncedSignal} as the output type has the same structure.
30
+ */
31
+ export type ThrottledSignal<T> = DebouncedSignal<T>;
32
+ /**
33
+ * A convenience function that creates and throttles a new `WritableSignal` in one step.
34
+ *
35
+ * @see {throttle} for the core implementation details.
36
+ *
37
+ * @template T The type of value the signal holds.
38
+ * @param initial The initial value of the signal.
39
+ * @param opt Options for signal creation, including throttle time `ms`.
40
+ * @returns A `ThrottledSignal<T>` instance.
41
+ *
42
+ * @example
43
+ * const query = throttled('', { ms: 500 });
44
+ * effect(() => console.log('Throttled Query:', query()));
45
+ *
46
+ * query.set('a');
47
+ * query.set('b');
48
+ * query.set('c');
49
+ * // With a trailing-edge throttle, the final value 'c' would be set
50
+ * // after the 500ms cooldown.
51
+ */
52
+ export declare function throttled<T>(initial: T, opt?: CreateThrottledOptions<T>): DebouncedSignal<T>;
53
+ /**
54
+ * Wraps an existing `WritableSignal` to create a new one whose readable value is throttled.
55
+ *
56
+ * This implementation avoids using `effect` by pairing a trigger signal with an `untracked`
57
+ * read of the source signal to control when the throttled value is re-evaluated.
58
+ *
59
+ * @template T The type of value the signal holds.
60
+ * @param source The source `WritableSignal` to wrap. Writes are applied to this signal immediately.
61
+ * @param opt Options for throttling, including throttle time `ms` and an optional `DestroyRef`.
62
+ * @returns A new `ThrottledSignal<T>` whose read value is throttled. The `.original` property
63
+ * of the returned signal is a reference back to the provided `source` signal.
64
+ *
65
+ * @example
66
+ * const query = throttled('', { ms: 500 });
67
+ * effect(() => console.log('Throttled Query:', query()));
68
+ *
69
+ * query.set('a');
70
+ * query.set('b');
71
+ * query.set('c');
72
+ * // With a trailing-edge throttle, the final value 'c' would be set
73
+ * // after the 500ms cooldown.
74
+ */
75
+ export declare function throttle<T>(source: WritableSignal<T>, opt?: CreateThrottledOptions<T>): ThrottledSignal<T>;
package/lib/until.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ import { DestroyRef, Signal } from '@angular/core';
2
+ export type UntilOptions = {
3
+ /**
4
+ * Optional timeout in milliseconds. If the condition is not met
5
+ * within this period, the promise will reject.
6
+ */
7
+ timeout?: number;
8
+ /**
9
+ * Optional DestroyRef. If provided and the component/context is destroyed
10
+ * before the condition is met or timeout occurs, the promise will reject.
11
+ * If not provided, it will attempt to inject one if called in an injection context.
12
+ */
13
+ destroyRef?: DestroyRef;
14
+ };
15
+ /**
16
+ * Creates a Promise that resolves when a signal's value satisfies a given predicate.
17
+ *
18
+ * This is useful for imperatively waiting for a reactive state to change,
19
+ * for example, in tests or to orchestrate complex asynchronous operations.
20
+ *
21
+ * @template T The type of the signal's value.
22
+ * @param sourceSignal The signal to observe.
23
+ * @param predicate A function that takes the signal's value and returns `true` if the condition is met.
24
+ * @param options Optional configuration for timeout and explicit destruction.
25
+ * @returns A Promise that resolves with the signal's value when the predicate is true,
26
+ * or rejects on timeout or context destruction.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * const count = signal(0);
31
+ *
32
+ * async function waitForCount() {
33
+ * console.log('Waiting for count to be >= 3...');
34
+ * try {
35
+ * const finalCount = await until(count, c => c >= 3, { timeout: 5000 });
36
+ * console.log(`Count reached: ${finalCount}`);
37
+ * } catch (e: any) { // Ensure 'e' is typed if you access properties like e.message
38
+ * console.error(e.message); // e.g., "until: Timeout after 5000ms."
39
+ * }
40
+ * }
41
+ *
42
+ * // Simulate updates
43
+ * setTimeout(() => count.set(1), 500);
44
+ * setTimeout(() => count.set(2), 1000);
45
+ * setTimeout(() => count.set(3), 1500);
46
+ *
47
+ * waitForCount();
48
+ * ```
49
+ */
50
+ export declare function until<T>(sourceSignal: Signal<T>, predicate: (value: T) => boolean, options?: UntilOptions): Promise<T>;
@@ -1,14 +1,94 @@
1
- import { type CreateSignalOptions, type Signal, type WritableSignal } from '@angular/core';
1
+ import { type CreateSignalOptions, type Signal, type ValueEqualityFn, type WritableSignal } from '@angular/core';
2
+ /**
3
+ * A WritableSignal enhanced with undo/redo capabilities and history tracking.
4
+ *
5
+ * @template T The type of value held by the signal.
6
+ */
2
7
  export type SignalWithHistory<T> = WritableSignal<T> & {
8
+ /** A read-only signal of the undo history stack. The oldest changes are at the start of the array. */
3
9
  history: Signal<T[]>;
10
+ /** Reverts the signal to its most recent previous state in the history. */
4
11
  undo: () => void;
12
+ /** Re-applies the last state that was undone. */
5
13
  redo: () => void;
14
+ /** A signal that is `true` if there are states in the redo stack. */
6
15
  canRedo: Signal<boolean>;
16
+ /** A signal that is `true` if there are states in the undo history. */
7
17
  canUndo: Signal<boolean>;
18
+ /** Clears both the undo and redo history stacks. */
8
19
  clear: () => void;
20
+ /** A signal that is `true` if there is any history that can be cleared. */
9
21
  canClear: Signal<boolean>;
10
22
  };
23
+ /**
24
+ * Options for creating a signal with history tracking.
25
+ *
26
+ * @template T The type of value held by the signal.
27
+ */
11
28
  export type CreateHistoryOptions<T> = Omit<CreateSignalOptions<T[]>, 'equal'> & {
29
+ /**
30
+ * Optional custom equality function to determine if a value has changed before
31
+ * adding it to history. Defaults to the source signal's equality function or `Object.is`.
32
+ */
33
+ equal?: ValueEqualityFn<T>;
34
+ /**
35
+ * The maximum number of undo states to keep in the history.
36
+ * @default Infinity
37
+ */
12
38
  maxSize?: number;
39
+ /**
40
+ * The strategy for trimming the history when `maxSize` is reached.
41
+ * - `shift`: Removes the single oldest entry from the history.
42
+ * - `halve`: Removes the oldest half of the history stack.
43
+ * @default 'halve'
44
+ */
45
+ cleanupStrategy?: 'shift' | 'halve';
13
46
  };
47
+ /**
48
+ * Enhances an existing `WritableSignal` by adding a complete undo/redo history
49
+ * stack and an API to control it.
50
+ *
51
+ * @template T The type of value held by the signal.
52
+ * @param source The source `WritableSignal` to add history tracking to.
53
+ * @param options Optional configuration for the history behavior.
54
+ * @returns A `SignalWithHistory<T>` instance, augmenting the source signal with history APIs.
55
+ *
56
+ * @remarks
57
+ * - Any new `.set()` or `.update()` call on the signal will clear the entire redo stack.
58
+ * - The primitive attempts to automatically use the source signal's own `equal` function,
59
+ * but this relies on an internal Angular API. For maximum stability across Angular
60
+ * versions, it is recommended to provide an explicit `equal` function in the options.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * import { signal } from '@angular/core';
65
+ * import { withHistory } from '@mmstack/primitives';
66
+ *
67
+ * const name = withHistory(signal('John'), { maxSize: 5 });
68
+ *
69
+ * console.log('Initial value:', name()); // "John"
70
+ *
71
+ * name.set('John Doe');
72
+ * name.set('Jane Doe');
73
+ *
74
+ * console.log('Current value:', name()); // "Jane Doe"
75
+ * console.log('History:', name.history()); // ["John", "John Doe"]
76
+ * console.log('Can undo:', name.canUndo()); // true
77
+ * console.log('Can redo:', name.canRedo()); // false
78
+ *
79
+ * name.undo();
80
+ * console.log('After undo:', name()); // "John Doe"
81
+ * console.log('Can redo:', name.canRedo()); // true
82
+ *
83
+ * name.redo();
84
+ * console.log('After redo:', name()); // "Jane Doe"
85
+ *
86
+ * // A new change will clear the redo history
87
+ * name.set('Janine Doe');
88
+ * console.log('Can redo:', name.canRedo()); // false
89
+ *
90
+ * name.clear();
91
+ * console.log('Can undo:', name.canUndo()); // false
92
+ * ```
93
+ */
14
94
  export declare function withHistory<T>(source: WritableSignal<T>, opt?: CreateHistoryOptions<T>): SignalWithHistory<T>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmstack/primitives",
3
- "version": "19.1.1",
3
+ "version": "19.2.0",
4
4
  "keywords": [
5
5
  "angular",
6
6
  "signals",