@mmstack/primitives 20.4.6 → 20.5.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.
package/index.d.ts CHANGED
@@ -1,4 +1,40 @@
1
- import { CreateSignalOptions, DestroyRef, WritableSignal, Signal, ElementRef, EffectCleanupRegisterFn, CreateEffectOptions, Injector, ValueEqualityFn } from '@angular/core';
1
+ import { ValueEqualityFn, Injector, Signal, CreateSignalOptions, DestroyRef, WritableSignal, EffectRef, EffectCleanupRegisterFn, CreateEffectOptions, ElementRef } from '@angular/core';
2
+
3
+ type CreateChunkedOptions<T> = {
4
+ /**
5
+ * The number of items to process in each chunk.
6
+ * @default 50
7
+ */
8
+ chunkSize?: number;
9
+ /**
10
+ * The delay between processing each chunk. Can be a number (milliseconds) or 'frame' to use `requestAnimationFrame`.
11
+ * @default 'frame'
12
+ */
13
+ delay?: number | 'frame' | 'microtask';
14
+ /**
15
+ * A custom equality function to determine if the processed chunk has changed. This can help prevent unnecessary updates if the chunk content is the same as the previous one.
16
+ */
17
+ equal?: ValueEqualityFn<T[]>;
18
+ /**
19
+ * An optional `Injector` to use for the internal effect. This allows the effect to have access to dependency injection if needed.
20
+ */
21
+ injector?: Injector;
22
+ };
23
+ /**
24
+ * Creates a new `Signal` that processes an array of items in time-sliced chunks. This is useful for handling large lists without blocking the main thread.
25
+ *
26
+ * The returned signal will initially contain the first `chunkSize` items from the source array. It will then schedule updates to include additional chunks of items based on the specified `duration`.
27
+ *
28
+ * @template T The type of items in the array.
29
+ * @param source A `Signal` or a function that returns an array of items to be processed in chunks.
30
+ * @param options Configuration options for chunk size, delay duration, equality function, and injector.
31
+ * @returns A `Signal` that emits the current chunk of items being processed.
32
+ *
33
+ * @example
34
+ * const largeList = signal(Array.from({ length: 1000 }, (_, i) => i));
35
+ * const chunkedList = chunked(largeList, { chunkSize: 100, duration: 100 });
36
+ */
37
+ declare function chunked<T>(source: Signal<T[]> | (() => T[]), options?: CreateChunkedOptions<T>): Signal<T[]>;
2
38
 
3
39
  /**
4
40
  * Options for creating a debounced writable signal.
@@ -271,31 +307,32 @@ declare function derived<T, U>(source: WritableSignal<T>, opt: CreateDerivedOpti
271
307
  * console.log(user().name); // Outputs: Jane
272
308
  * ```
273
309
  */
274
- declare function derived<T extends UnknownObject, TKey extends keyof T>(source: WritableSignal<T>, key: TKey, opt?: CreateSignalOptions<T[TKey]>): DerivedSignal<T, T[TKey]>;
310
+ declare function derived<T extends UnknownObject, TKey extends keyof T>(source: MutableSignal<T>, key: TKey, opt?: CreateSignalOptions<T[TKey]>): DerivedSignal<T, T[TKey]> & MutableSignal<T[TKey]>;
275
311
  /**
276
- * Creates a `DerivedSignal` from an array, deriving an element by its index.
277
- * This overload is a convenient shorthand for accessing array elements.
312
+ * Creates a `DerivedSignal` that derives a property from an object held by the source signal.
313
+ * This overload is a convenient shorthand for accessing object properties.
278
314
  *
279
- * @typeParam T The type of the source signal's value (must be an array).
280
- * @param source The source `WritableSignal` (holding an array).
281
- * @param index The index of the element to derive.
315
+ * @typeParam T The type of the source signal's value (must be an object).
316
+ * @typeParam TKey The key of the property to derive.
317
+ * @param source The source `WritableSignal` (holding an object).
318
+ * @param key The key of the property to derive.
282
319
  * @param options Optional signal options for the derived signal.
283
320
  * @returns A `DerivedSignal` instance.
284
321
  *
285
322
  * @example
286
323
  * ```ts
287
- * const numbers = signal([1, 2, 3]);
288
- * const secondNumber = derived(numbers, 1);
324
+ * const user = signal({ name: 'John', age: 30 });
325
+ * const name = derived(user, 'name');
289
326
  *
290
- * console.log(secondNumber()); // Outputs: 2
327
+ * console.log(name()); // Outputs: John
291
328
  *
292
329
  * // Update the derived signal, which also updates the source
293
- * secondNumber.set(5);
330
+ * name.set('Jane');
294
331
  *
295
- * console.log(numbers()); // Outputs: [1, 5, 3]
332
+ * console.log(user().name); // Outputs: Jane
296
333
  * ```
297
334
  */
298
- declare function derived<T extends any[]>(source: WritableSignal<T>, index: number, opt?: CreateSignalOptions<T[number]>): DerivedSignal<T, T[number]>;
335
+ declare function derived<T extends UnknownObject, TKey extends keyof T>(source: WritableSignal<T>, key: TKey, opt?: CreateSignalOptions<T[TKey]>): DerivedSignal<T, T[TKey]>;
299
336
  /**
300
337
  * Creates a `DerivedSignal` that derives its value from another `MutableSignal`.
301
338
  * Use mutuable signals with caution, but very useful for deeply nested structures.
@@ -319,6 +356,30 @@ declare function derived<T extends any[]>(source: WritableSignal<T>, index: numb
319
356
  * ```
320
357
  */
321
358
  declare function derived<T, U>(source: MutableSignal<T>, optOrKey: CreateDerivedOptions<T, U> | keyof T, opt?: CreateSignalOptions<U>): DerivedSignal<T, U> & MutableSignal<U>;
359
+ /**
360
+ * Creates a `DerivedSignal` from an array, deriving an element by its index.
361
+ * This overload is a convenient shorthand for accessing array elements.
362
+ *
363
+ * @typeParam T The type of the source signal's value (must be an array).
364
+ * @param source The source `WritableSignal` (holding an array).
365
+ * @param index The index of the element to derive.
366
+ * @param options Optional signal options for the derived signal.
367
+ * @returns A `DerivedSignal` instance.
368
+ *
369
+ * @example
370
+ * ```ts
371
+ * const numbers = signal([1, 2, 3]);
372
+ * const secondNumber = derived(numbers, 1);
373
+ *
374
+ * console.log(secondNumber()); // Outputs: 2
375
+ *
376
+ * // Update the derived signal, which also updates the source
377
+ * secondNumber.set(5);
378
+ *
379
+ * console.log(numbers()); // Outputs: [1, 5, 3]
380
+ * ```
381
+ */
382
+ declare function derived<T extends any[]>(source: WritableSignal<T>, index: number, opt?: CreateSignalOptions<T[number]>): DerivedSignal<T, T[number]>;
322
383
  /**
323
384
  * Creates a "fake" `DerivedSignal` from a simple value. This is useful for creating
324
385
  * `FormControlSignal` instances that are not directly derived from another signal.
@@ -353,71 +414,87 @@ declare function toFakeSignalDerivation<T, U>(initial: WritableSignal<U>): Deriv
353
414
  */
354
415
  declare function isDerivation<T, U>(sig: WritableSignal<U>): sig is DerivedSignal<T, U>;
355
416
 
356
- /**
357
- * Options for configuring the `elementVisibility` sensor, extending
358
- * standard `IntersectionObserverInit` options.
359
- */
360
- type ElementVisibilityOptions = IntersectionObserverInit & {
361
- /** Optional debug name for the internal signal. */
362
- debugName?: string;
363
- };
364
- type ElementVisibilitySignal = Signal<IntersectionObserverEntry | undefined> & {
365
- readonly visible: Signal<boolean>;
417
+ type Frame = {
418
+ injector: Injector;
419
+ parent: Frame | null;
420
+ children: Set<EffectRef>;
366
421
  };
422
+
367
423
  /**
368
- * Creates a read-only signal that tracks the intersection status of a target DOM element
369
- * with the viewport or a specified root element, using the `IntersectionObserver` API.
424
+ * Creates an effect that can be nested, similar to SolidJS's `createEffect`.
370
425
  *
371
- * It can observe a static `ElementRef`/`Element` or a `Signal` that resolves to one,
372
- * allowing for dynamic targets.
426
+ * This primitive enables true hierarchical reactivity. A `nestedEffect` created
427
+ * within another `nestedEffect` is automatically destroyed and recreated when
428
+ * the parent re-runs.
373
429
  *
374
- * @param target The DOM element (or `ElementRef`, or a `Signal` resolving to one) to observe.
375
- * If the signal resolves to `null`, observation stops.
376
- * @param options Optional `IntersectionObserverInit` options (e.g., `root`, `rootMargin`, `threshold`)
377
- * and an optional `debugName`.
378
- * @returns A `Signal<IntersectionObserverEntry | undefined>`. It emits `undefined` initially,
379
- * on the server, or if the target is `null`. Otherwise, it emits the latest
380
- * `IntersectionObserverEntry`. Consumers can derive a boolean `isVisible` from
381
- * this entry's `isIntersecting` property.
430
+ * It automatically handles injector propagation and lifetime management, allowing
431
+ * you to create fine-grained, conditional side-effects that only track
432
+ * dependencies when they are "live".
433
+ *
434
+ * @param effectFn The side-effect function, which receives a cleanup register function.
435
+ * @param options (Optional) Angular's `CreateEffectOptions`.
436
+ * @returns An `EffectRef` for the created effect.
382
437
  *
383
438
  * @example
384
439
  * ```ts
385
- * import { Component, effect, ElementRef, viewChild } from '@angular/core';
386
- * import { elementVisibility } from '@mmstack/primitives';
387
- * import { computed } from '@angular/core'; // For derived boolean
388
- *
389
- * @Component({
390
- * selector: 'app-lazy-image',
391
- * template: `
392
- * <div #imageContainer style="height: 200px; border: 1px dashed grey;">
393
- * @if (isVisible()) {
394
- * <img src="your-image-url.jpg" alt="Lazy loaded image" />
395
- * <p>Image is VISIBLE!</p>
396
- * } @else {
397
- * <p>Scroll down to see the image...</p>
398
- * }
399
- * </div>
400
- * `
401
- * })
402
- * export class LazyImageComponent {
403
- * readonly imageContainer = viewChild.required<ElementRef<HTMLDivElement>>('imageContainer');
404
- *
405
- * // Observe the element, get the full IntersectionObserverEntry
406
- * readonly intersectionEntry = elementVisibility(this.imageContainer);
440
+ * // Assume `coldGuard` changes rarely, but `hotSignal` changes often.
441
+ * const coldGuard = signal(false);
442
+ * const hotSignal = signal(0);
407
443
  *
408
- * // Derive a simple boolean for visibility
409
- * readonly isVisible = computed(() => this.intersectionEntry()?.isIntersecting ?? false);
444
+ * nestedEffect(() => {
445
+ * // This outer effect only tracks `coldGuard`.
446
+ * if (coldGuard()) {
410
447
  *
411
- * constructor() {
412
- * effect(() => {
413
- * console.log('Intersection Entry:', this.intersectionEntry());
414
- * console.log('Is Visible:', this.isVisible());
448
+ * // This inner effect is CREATED when coldGuard is true
449
+ * // and DESTROYED when it becomes false.
450
+ * nestedEffect(() => {
451
+ * // It only tracks `hotSignal` while it exists.
452
+ * console.log('Hot signal is:', hotSignal());
415
453
  * });
416
454
  * }
417
- * }
455
+ * // If `coldGuard` is false, this outer effect does not track `hotSignal`.
456
+ * });
457
+ * ```
458
+ * @example
459
+ * ```ts
460
+ * const users = signal([
461
+ * { id: 1, name: 'Alice' },
462
+ * { id: 2, name: 'Bob' }
463
+ * ]);
464
+ *
465
+ * // The fine-grained mapped list
466
+ * const mappedUsers = mapArray(
467
+ * users,
468
+ * (userSignal, index) => {
469
+ * // 1. Create a fine-grained SIDE EFFECT for *this item*
470
+ * // This effect's lifetime is now tied to this specific item. created once on init of this index.
471
+ * const effectRef = nestedEffect(() => {
472
+ * // This only runs if *this* userSignal changes,
473
+ * // not if the whole list changes.
474
+ * console.log(`User ${index} updated:`, userSignal().name);
475
+ * });
476
+ *
477
+ * // 2. Return the data AND the cleanup logic
478
+ * return {
479
+ * // The mapped data
480
+ * label: computed(() => `User: ${userSignal().name}`),
481
+ *
482
+ * // The cleanup function
483
+ * destroyEffect: () => effectRef.destroy()
484
+ * };
485
+ * },
486
+ * {
487
+ * // 3. Tell mapArray HOW to clean up when an item is removed, this needs to be manual as it's not a nestedEffect itself
488
+ * onDestroy: (mappedItem) => {
489
+ * mappedItem.destroyEffect();
490
+ * }
491
+ * }
492
+ * );
418
493
  * ```
419
494
  */
420
- declare function elementVisibility(target?: ElementRef<Element> | Element | Signal<ElementRef<Element> | Element | null>, opt?: ElementVisibilityOptions): ElementVisibilitySignal;
495
+ declare function nestedEffect(effectFn: (registerCleanup: EffectCleanupRegisterFn) => void, options?: CreateEffectOptions & {
496
+ bindToFrame?: (parent: Frame | null) => Frame | null;
497
+ }): EffectRef;
421
498
 
422
499
  /**
423
500
  * Reactively maps items from a source array to a new array, creating stable signals for each item.
@@ -449,7 +526,7 @@ declare function elementVisibility(target?: ElementRef<Element> | Element | Sign
449
526
  * ]);
450
527
  *
451
528
  * // The `itemSignal` is writable because `sourceItems` is a WritableSignal.
452
- * const mappedItems = mapArray(sourceItems, (itemSignal, index) => ({
529
+ * const mappedItems = indexArray(sourceItems, (itemSignal, index) => ({
453
530
  * label: computed(() => `${index}: ${itemSignal().name.toUpperCase()}`),
454
531
  * setName: (newName: string) => itemSignal.update(item => ({ ...item, name: newName }))
455
532
  * }));
@@ -458,91 +535,60 @@ declare function elementVisibility(target?: ElementRef<Element> | Element | Sign
458
535
  * mappedItems()[0].setName('Avocado');
459
536
  * // sourceItems() is now: [{ id: 1, name: 'Avocado' }, { id: 2, name: 'Banana' }]
460
537
  */
461
- declare function mapArray<T, U>(source: MutableSignal<T[]>, map: (value: MutableSignal<T>, index: number) => U, options?: CreateSignalOptions<T> & {
538
+ declare function indexArray<T, U>(source: MutableSignal<T[]>, map: (value: MutableSignal<T>, index: number) => U, options?: CreateSignalOptions<T> & {
462
539
  onDestroy?: (value: U) => void;
463
540
  }): Signal<U[]>;
464
- declare function mapArray<T, U>(source: WritableSignal<T[]>, map: (value: WritableSignal<T>, index: number) => U, options?: CreateSignalOptions<T> & {
541
+ declare function indexArray<T, U>(source: WritableSignal<T[]>, map: (value: WritableSignal<T>, index: number) => U, options?: CreateSignalOptions<T> & {
465
542
  onDestroy?: (value: U) => void;
466
543
  }): Signal<U[]>;
467
- declare function mapArray<T, U>(source: Signal<T[]> | (() => T[]), map: (value: Signal<T>, index: number) => U, options?: CreateSignalOptions<T> & {
544
+ declare function indexArray<T, U>(source: Signal<T[]> | (() => T[]), map: (value: Signal<T>, index: number) => U, options?: CreateSignalOptions<T> & {
468
545
  onDestroy?: (value: U) => void;
469
546
  }): Signal<U[]>;
547
+ /**
548
+ * @deprecated use indexArray instead
549
+ */
550
+ declare const mapArray: typeof indexArray;
470
551
 
471
552
  /**
472
- * Creates an effect that can be nested, similar to SolidJS's `createEffect`.
553
+ * Reactively maps items from a source array to a new array by value (identity).
473
554
  *
474
- * This primitive enables true hierarchical reactivity. A `nestedEffect` created
475
- * within another `nestedEffect` is automatically destroyed and recreated when
476
- * the parent re-runs.
477
- *
478
- * It automatically handles injector propagation and lifetime management, allowing
479
- * you to create fine-grained, conditional side-effects that only track
480
- * dependencies when they are "live".
555
+ * similar to `Array.prototype.map`, but:
556
+ * 1. The `mapFn` receives the `index` as a Signal.
557
+ * 2. If an item in the `source` array moves to a new position, the *result* of the map function is reused and moved.
558
+ * The `index` signal is updated to the new index.
559
+ * 3. The `mapFn` is only run for *new* items.
481
560
  *
482
- * @param effectFn The side-effect function, which receives a cleanup register function.
483
- * @param options (Optional) Angular's `CreateEffectOptions`.
484
- * @returns An `EffectRef` for the created effect.
561
+ * This is useful for building efficient lists where DOM nodes or heavy instances should be reused
562
+ * when the list is reordered.
485
563
  *
486
- * @example
487
- * ```ts
488
- * // Assume `coldGuard` changes rarely, but `hotSignal` changes often.
489
- * const coldGuard = signal(false);
490
- * const hotSignal = signal(0);
491
- *
492
- * nestedEffect(() => {
493
- * // This outer effect only tracks `coldGuard`.
494
- * if (coldGuard()) {
495
- *
496
- * // This inner effect is CREATED when coldGuard is true
497
- * // and DESTROYED when it becomes false.
498
- * nestedEffect(() => {
499
- * // It only tracks `hotSignal` while it exists.
500
- * console.log('Hot signal is:', hotSignal());
501
- * });
502
- * }
503
- * // If `coldGuard` is false, this outer effect does not track `hotSignal`.
504
- * });
505
- * ```
506
- * @example
507
- * ```ts
508
- * const users = signal([
509
- { id: 1, name: 'Alice' },
510
- { id: 2, name: 'Bob' }
511
- ]);
512
-
513
- // The fine-grained mapped list
514
- const mappedUsers = mapArray(
515
- users,
516
- (userSignal, index) => {
517
- // 1. Create a fine-grained SIDE EFFECT for *this item*
518
- // This effect's lifetime is now tied to this specific item. created once on init of this index.
519
- const effectRef = nestedEffect(() => {
520
- // This only runs if *this* userSignal changes,
521
- // not if the whole list changes.
522
- console.log(`User ${index} updated:`, userSignal().name);
523
- });
524
-
525
- // 2. Return the data AND the cleanup logic
526
- return {
527
- // The mapped data
528
- label: computed(() => `User: ${userSignal().name}`),
529
-
530
- // The cleanup function
531
- destroyEffect: () => effectRef.destroy()
532
- };
533
- },
534
- {
535
- // 3. Tell mapArray HOW to clean up when an item is removed, this needs to be manual as it's not a nestedEffect itself
536
- onDestroy: (mappedItem) => {
537
- mappedItem.destroyEffect();
538
- }
539
- }
540
- );
541
- * ```
564
+ * @param source A `Signal<T[]>` or a function returning `T[]`.
565
+ * @param mapFn The mapping function. Receives the item and its index as a Signal.
566
+ * @param options Optional configuration:
567
+ * - `onDestroy`: A callback invoked when a mapped item is removed from the array.
568
+ * @returns A `Signal<U[]>` containing the mapped array.
542
569
  */
543
- declare function nestedEffect(effectFn: (registerCleanup: EffectCleanupRegisterFn) => void, options?: CreateEffectOptions): {
544
- destroy: () => void;
570
+ declare function keyArray<T, U, K>(source: Signal<T[]> | (() => T[]), mapFn: (v: T, i: Signal<number>) => U, options?: {
571
+ onDestroy?: (value: U) => void;
572
+ /**
573
+ * Optional function to use a custom key for item comparison.
574
+ * Use this if you want to reuse mapped items based on a property (like an ID)
575
+ * even if the item reference changes.
576
+ */
577
+ key?: (item: T) => K;
578
+ }): Signal<U[]>;
579
+
580
+ type MappedObject<T extends Record<string, any>, U> = {
581
+ [K in keyof T]: U;
545
582
  };
583
+ declare function mapObject<T extends Record<string, any>, U>(source: MutableSignal<T>, mapFn: <K extends keyof T>(key: K, value: MutableSignal<T[K]>) => U, options?: {
584
+ onDestroy?: (value: U) => void;
585
+ }): Signal<MappedObject<T, U>>;
586
+ declare function mapObject<T extends Record<string, any>, U>(source: WritableSignal<T>, mapFn: <K extends keyof T>(key: K, value: WritableSignal<T[K]>) => U, options?: {
587
+ onDestroy?: (value: U) => void;
588
+ }): Signal<MappedObject<T, U>>;
589
+ declare function mapObject<T extends Record<string, any>, U>(source: (() => T) | Signal<T>, mapFn: <K extends keyof T>(key: K, value: Signal<T[K]>) => U, options?: {
590
+ onDestroy?: (value: U) => void;
591
+ }): Signal<MappedObject<T, U>>;
546
592
 
547
593
  /**
548
594
  * A pure, synchronous transform from I -> O.
@@ -640,6 +686,107 @@ declare function pipeable<TSig extends Signal<any>>(signal: TSig): PipeableSigna
640
686
  */
641
687
  declare function piped<T>(initial: T, opt?: CreateSignalOptions<T>): PipeableSignal<T, WritableSignal<T>>;
642
688
 
689
+ /**
690
+ * Represents the size of an element.
691
+ */
692
+ interface ElementSize {
693
+ width: number;
694
+ height: number;
695
+ }
696
+ /**
697
+ * Options for configuring the `elementSize` sensor.
698
+ */
699
+ type ElementSizeOptions = ResizeObserverOptions & {
700
+ /** Optional debug name for the internal signal. */
701
+ debugName?: string;
702
+ };
703
+ type ElementSizeSignal = Signal<ElementSize | undefined>;
704
+ /**
705
+ * Creates a read-only signal that tracks the size of a target DOM element.
706
+ *
707
+ * By default, it observes the `border-box` size to align with `getBoundingClientRect()`,
708
+ * which is used to provide a synchronous initial value if possible.
709
+ *
710
+ * @param target The DOM element (or `ElementRef`, or a `Signal` resolving to one) to observe.
711
+ * @param options Optional configuration including `box` (defaults to 'border-box') and `debugName`.
712
+ * @returns A `Signal<ElementSize | undefined>`.
713
+ *
714
+ * @example
715
+ * ```ts
716
+ * const size = elementSize(elementRef);
717
+ * effect(() => {
718
+ * console.log('Size:', size()?.width, size()?.height);
719
+ * });
720
+ * ```
721
+ */
722
+ declare function elementSize(target?: ElementRef<Element> | Element | Signal<ElementRef<Element> | Element | null>, opt?: ElementSizeOptions): ElementSizeSignal;
723
+
724
+ /**
725
+ * Options for configuring the `elementVisibility` sensor, extending
726
+ * standard `IntersectionObserverInit` options.
727
+ */
728
+ type ElementVisibilityOptions = IntersectionObserverInit & {
729
+ /** Optional debug name for the internal signal. */
730
+ debugName?: string;
731
+ };
732
+ type ElementVisibilitySignal = Signal<IntersectionObserverEntry | undefined> & {
733
+ readonly visible: Signal<boolean>;
734
+ };
735
+ /**
736
+ * Creates a read-only signal that tracks the intersection status of a target DOM element
737
+ * with the viewport or a specified root element, using the `IntersectionObserver` API.
738
+ *
739
+ * It can observe a static `ElementRef`/`Element` or a `Signal` that resolves to one,
740
+ * allowing for dynamic targets.
741
+ *
742
+ * @param target The DOM element (or `ElementRef`, or a `Signal` resolving to one) to observe.
743
+ * If the signal resolves to `null`, observation stops.
744
+ * @param options Optional `IntersectionObserverInit` options (e.g., `root`, `rootMargin`, `threshold`)
745
+ * and an optional `debugName`.
746
+ * @returns A `Signal<IntersectionObserverEntry | undefined>`. It emits `undefined` initially,
747
+ * on the server, or if the target is `null`. Otherwise, it emits the latest
748
+ * `IntersectionObserverEntry`. Consumers can derive a boolean `isVisible` from
749
+ * this entry's `isIntersecting` property.
750
+ *
751
+ * @example
752
+ * ```ts
753
+ * import { Component, effect, ElementRef, viewChild } from '@angular/core';
754
+ * import { elementVisibility } from '@mmstack/primitives';
755
+ * import { computed } from '@angular/core'; // For derived boolean
756
+ *
757
+ * @Component({
758
+ * selector: 'app-lazy-image',
759
+ * template: `
760
+ * <div #imageContainer style="height: 200px; border: 1px dashed grey;">
761
+ * @if (isVisible()) {
762
+ * <img src="your-image-url.jpg" alt="Lazy loaded image" />
763
+ * <p>Image is VISIBLE!</p>
764
+ * } @else {
765
+ * <p>Scroll down to see the image...</p>
766
+ * }
767
+ * </div>
768
+ * `
769
+ * })
770
+ * export class LazyImageComponent {
771
+ * readonly imageContainer = viewChild.required<ElementRef<HTMLDivElement>>('imageContainer');
772
+ *
773
+ * // Observe the element, get the full IntersectionObserverEntry
774
+ * readonly intersectionEntry = elementVisibility(this.imageContainer);
775
+ *
776
+ * // Derive a simple boolean for visibility
777
+ * readonly isVisible = computed(() => this.intersectionEntry()?.isIntersecting ?? false);
778
+ *
779
+ * constructor() {
780
+ * effect(() => {
781
+ * console.log('Intersection Entry:', this.intersectionEntry());
782
+ * console.log('Is Visible:', this.isVisible());
783
+ * });
784
+ * }
785
+ * }
786
+ * ```
787
+ */
788
+ declare function elementVisibility(target?: ElementRef<Element> | Element | Signal<ElementRef<Element> | Element | null>, opt?: ElementVisibilityOptions): ElementVisibilitySignal;
789
+
643
790
  /**
644
791
  * Creates a read-only signal that reactively tracks whether a CSS media query
645
792
  * string currently matches.
@@ -1029,6 +1176,81 @@ type WindowSizeSignal = Signal<WindowSize> & {
1029
1176
  */
1030
1177
  declare function windowSize(opt?: WindowSizeOptions): WindowSizeSignal;
1031
1178
 
1179
+ type SensorTypedOptions = {
1180
+ elementVisibility: {
1181
+ opt: ElementVisibilityOptions & {
1182
+ target?: ElementRef<Element> | Element | Signal<ElementRef<Element> | Element | null>;
1183
+ };
1184
+ returnType: ElementVisibilitySignal;
1185
+ };
1186
+ elementSize: {
1187
+ opt: ElementSizeOptions & {
1188
+ target?: ElementRef<Element> | Element | Signal<ElementRef<Element> | Element | null>;
1189
+ };
1190
+ returnType: ElementSizeSignal;
1191
+ };
1192
+ mousePosition: {
1193
+ opt: MousePositionOptions;
1194
+ returnType: MousePositionSignal;
1195
+ };
1196
+ networkStatus: {
1197
+ opt: {
1198
+ debugName?: string;
1199
+ };
1200
+ returnType: NetworkStatusSignal;
1201
+ };
1202
+ pageVisibility: {
1203
+ opt: {
1204
+ debugName?: string;
1205
+ };
1206
+ returnType: Signal<DocumentVisibilityState>;
1207
+ };
1208
+ darkMode: {
1209
+ opt: {
1210
+ debugName?: string;
1211
+ };
1212
+ returnType: Signal<boolean>;
1213
+ };
1214
+ reducedMotion: {
1215
+ opt: {
1216
+ debugName?: string;
1217
+ };
1218
+ returnType: Signal<boolean>;
1219
+ };
1220
+ scrollPosition: {
1221
+ opt: ScrollPositionOptions;
1222
+ returnType: ScrollPositionSignal;
1223
+ };
1224
+ windowSize: {
1225
+ opt: WindowSizeOptions;
1226
+ returnType: WindowSizeSignal;
1227
+ };
1228
+ mediaQuery: {
1229
+ opt: {
1230
+ query: string;
1231
+ debugName?: string;
1232
+ };
1233
+ returnType: Signal<boolean>;
1234
+ };
1235
+ };
1236
+ /**
1237
+ * Creates a sensor signal that the elements visiblity within the viewport
1238
+ * @param type Must be `'elementVisibility'`.
1239
+ * @param options Optional configuration IntersectionObserver & target.
1240
+ * @returns A `ElementVisibilitySignal` that tracks whether the Element is intersected.
1241
+ * @see {elementVisibility} for detailed documentation and examples.
1242
+ * @example const pos = sensor('elementVisibility');
1243
+ */
1244
+ declare function sensor(type: 'elementVisibility', options?: SensorTypedOptions['elementVisibility']['opt']): ElementVisibilitySignal;
1245
+ /**
1246
+ * Creates a sensor signal that tracks the element's size dimensions.
1247
+ * @param type Must be `'elementSize'`.
1248
+ * @param options Optional configuration ResizeObserver & target.
1249
+ * @returns A `ElementSizeSignal` that tracks the element's size ({ width, height }).
1250
+ * @see {elementSize} for detailed documentation and examples.
1251
+ * @example const size = sensor('elementSize');
1252
+ */
1253
+ declare function sensor(type: 'elementSize', options?: SensorTypedOptions['elementSize']['opt']): ElementSizeSignal;
1032
1254
  /**
1033
1255
  * Creates a sensor signal that tracks the mouse cursor's position.
1034
1256
  * @param type Must be `'mousePosition'`.
@@ -1037,7 +1259,7 @@ declare function windowSize(opt?: WindowSizeOptions): WindowSizeSignal;
1037
1259
  * @see {mousePosition} for detailed documentation and examples.
1038
1260
  * @example const pos = sensor('mousePosition', { coordinateSpace: 'page', throttle: 50 });
1039
1261
  */
1040
- declare function sensor(type: 'mousePosition', options?: MousePositionOptions): MousePositionSignal;
1262
+ declare function sensor(type: 'mousePosition', options?: SensorTypedOptions['mousePosition']['opt']): MousePositionSignal;
1041
1263
  /**
1042
1264
  * Creates a sensor signal that tracks the browser's online/offline status.
1043
1265
  * @param type Must be `'networkStatus'`.
@@ -1046,9 +1268,7 @@ declare function sensor(type: 'mousePosition', options?: MousePositionOptions):
1046
1268
  * @see {networkStatus} for detailed documentation and examples.
1047
1269
  * @example const onlineStatus = sensor('networkStatus');
1048
1270
  */
1049
- declare function sensor(type: 'networkStatus', options?: {
1050
- debugName?: string;
1051
- }): NetworkStatusSignal;
1271
+ declare function sensor(type: 'networkStatus', options?: SensorTypedOptions['networkStatus']['opt']): NetworkStatusSignal;
1052
1272
  /**
1053
1273
  * Creates a sensor signal that tracks the page's visibility state (e.g., 'visible', 'hidden').
1054
1274
  * @param type Must be `'pageVisibility'`.
@@ -1057,31 +1277,34 @@ declare function sensor(type: 'networkStatus', options?: {
1057
1277
  * @see {pageVisibility} for detailed documentation and examples.
1058
1278
  * @example const visibility = sensor('pageVisibility');
1059
1279
  */
1060
- declare function sensor(type: 'pageVisibility', options?: {
1061
- debugName?: string;
1062
- }): Signal<DocumentVisibilityState>;
1280
+ declare function sensor(type: 'pageVisibility', options?: SensorTypedOptions['pageVisibility']['opt']): Signal<DocumentVisibilityState>;
1063
1281
  /**
1064
1282
  * Creates a sensor signal that tracks the user's OS/browser preference for a dark color scheme.
1065
- * @param type Must be `'dark-mode'`.
1283
+ * @param type Must be `'darkMode'`.
1066
1284
  * @param options Optional configuration, currently only `debugName`.
1067
1285
  * @returns A `Signal<boolean>` which is `true` if a dark theme is preferred.
1068
1286
  * @see {prefersDarkMode} for detailed documentation and examples.
1069
1287
  * @example const isDarkMode = sensor('dark-mode');
1070
1288
  */
1071
- declare function sensor(type: 'dark-mode', options?: {
1072
- debugName?: string;
1073
- }): Signal<boolean>;
1289
+ declare function sensor(type: 'darkMode' | 'dark-mode', options?: SensorTypedOptions['darkMode']['opt']): Signal<boolean>;
1074
1290
  /**
1075
1291
  * Creates a sensor signal that tracks the user's OS/browser preference for reduced motion.
1076
- * @param type Must be `'reduced-motion'`.
1292
+ * @param type Must be `'reducedMotion'`.
1077
1293
  * @param options Optional configuration, currently only `debugName`.
1078
1294
  * @returns A `Signal<boolean>` which is `true` if reduced motion is preferred.
1079
1295
  * @see {prefersReducedMotion} for detailed documentation and examples.
1080
1296
  * @example const wantsReducedMotion = sensor('reduced-motion');
1081
1297
  */
1082
- declare function sensor(type: 'reduced-motion', options?: {
1083
- debugName?: string;
1084
- }): Signal<boolean>;
1298
+ declare function sensor(type: 'reducedMotion' | 'reduced-motion', options?: SensorTypedOptions['reducedMotion']['opt']): Signal<boolean>;
1299
+ /**
1300
+ * Creates a sensor signal that tracks the provided media query.
1301
+ * @param type Must be `'mediaQuery'`.
1302
+ * @param options Optional configuration for the media query sensor, including `query` and `debugName`.
1303
+ * @returns A `Signal<boolean>` which is `true` if the media query currently matches.
1304
+ * @see {mediaQuery} for detailed documentation and examples.
1305
+ * @example const isDesktop = sensor('mediaQuery', { query: '(min-width: 1024px)' });
1306
+ */
1307
+ declare function sensor(type: 'mediaQuery', options?: SensorTypedOptions['mediaQuery']['opt']): Signal<boolean>;
1085
1308
  /**
1086
1309
  * Creates a sensor signal that tracks the browser window's inner dimensions (width and height).
1087
1310
  * @param type Must be `'windowSize'`.
@@ -1090,7 +1313,7 @@ declare function sensor(type: 'reduced-motion', options?: {
1090
1313
  * @see {windowSize} for detailed documentation and examples.
1091
1314
  * @example const size = sensor('windowSize', { throttle: 200 });
1092
1315
  */
1093
- declare function sensor(type: 'windowSize', options?: WindowSizeOptions): WindowSizeSignal;
1316
+ declare function sensor(type: 'windowSize', options?: SensorTypedOptions['windowSize']['opt']): WindowSizeSignal;
1094
1317
  /**
1095
1318
  * Creates a sensor signal that tracks the scroll position (x, y) of the window or a specified element.
1096
1319
  * @param type Must be `'scrollPosition'`.
@@ -1099,7 +1322,70 @@ declare function sensor(type: 'windowSize', options?: WindowSizeOptions): Window
1099
1322
  * @see {scrollPosition} for detailed documentation and examples.
1100
1323
  * @example const pageScroll = sensor('scrollPosition', { throttle: 150 });
1101
1324
  */
1102
- declare function sensor(type: 'scrollPosition', options?: ScrollPositionOptions): ScrollPositionSignal;
1325
+ declare function sensor(type: 'scrollPosition', options?: SensorTypedOptions['scrollPosition']['opt']): ScrollPositionSignal;
1326
+ type SensorsOptions<TKey extends keyof SensorTypedOptions> = {
1327
+ [K in TKey]: SensorTypedOptions[K]['opt'];
1328
+ };
1329
+ type Sensors<TKey extends keyof SensorTypedOptions> = {
1330
+ [K in TKey]: SensorTypedOptions[K]['returnType'];
1331
+ };
1332
+ declare function sensors<const TType extends keyof SensorTypedOptions>(track: TType[], opt?: SensorsOptions<TType>): Sensors<TType>;
1333
+
1334
+ type BaseType = string | number | boolean | symbol | undefined | null | Function | Date | RegExp;
1335
+ type Key = string | number;
1336
+ type AnyRecord = Record<Key, any>;
1337
+ /**
1338
+ * @internal
1339
+ * Validates whether a value is a Signal Store.
1340
+ */
1341
+ declare function isStore<T>(value: unknown): value is SignalStore<T>;
1342
+ type SignalArrayStore<T extends any[]> = Signal<T> & {
1343
+ readonly [index: number]: SignalStore<T[number]>;
1344
+ readonly length: Signal<number>;
1345
+ [Symbol.iterator](): Iterator<SignalStore<T[number]>>;
1346
+ };
1347
+ type WritableArrayStore<T extends any[]> = WritableSignal<T> & {
1348
+ readonly asReadonlyStore: () => SignalArrayStore<T>;
1349
+ readonly [index: number]: WritableSignalStore<T[number]>;
1350
+ readonly length: Signal<number>;
1351
+ [Symbol.iterator](): Iterator<WritableSignalStore<T[number]>>;
1352
+ };
1353
+ type MutableArrayStore<T extends any[]> = MutableSignal<T> & {
1354
+ readonly asReadonlyStore: () => SignalArrayStore<T>;
1355
+ readonly [index: number]: MutableSignalStore<T[number]>;
1356
+ readonly length: Signal<number>;
1357
+ [Symbol.iterator](): Iterator<MutableSignalStore<T[number]>>;
1358
+ };
1359
+ type SignalStore<T> = Signal<T> & (NonNullable<T> extends BaseType ? unknown : NonNullable<T> extends Array<any> ? SignalArrayStore<NonNullable<T>> : Readonly<{
1360
+ [K in keyof Required<T>]: SignalStore<NonNullable<T>[K]>;
1361
+ }>);
1362
+ type WritableSignalStore<T> = WritableSignal<T> & {
1363
+ readonly asReadonlyStore: () => SignalStore<T>;
1364
+ } & (NonNullable<T> extends BaseType ? unknown : NonNullable<T> extends Array<any> ? WritableArrayStore<NonNullable<T>> : Readonly<{
1365
+ [K in keyof Required<T>]: WritableSignalStore<NonNullable<T>[K]>;
1366
+ }>);
1367
+ type MutableSignalStore<T> = MutableSignal<T> & {
1368
+ readonly asReadonlyStore: () => SignalStore<T>;
1369
+ } & (NonNullable<T> extends BaseType ? unknown : NonNullable<T> extends Array<any> ? MutableArrayStore<NonNullable<T>> : Readonly<{
1370
+ [K in keyof Required<T>]: MutableSignalStore<NonNullable<T>[K]>;
1371
+ }>);
1372
+ declare function toStore<T extends AnyRecord>(source: MutableSignal<T>, injector?: Injector): MutableSignalStore<T>;
1373
+ declare function toStore<T extends AnyRecord>(source: WritableSignal<T>, injector?: Injector): WritableSignalStore<T>;
1374
+ declare function toStore<T extends AnyRecord>(source: Signal<T>, injector?: Injector): SignalStore<T>;
1375
+ /**
1376
+ * Creates a WritableSignalStore from a value.
1377
+ * @see {@link toStore}
1378
+ */
1379
+ declare function store<T extends AnyRecord>(value: T, opt?: CreateSignalOptions<T> & {
1380
+ injector?: Injector;
1381
+ }): WritableSignalStore<T>;
1382
+ /**
1383
+ * Creates a MutableSignalStore from a value.
1384
+ * @see {@link toStore}
1385
+ */
1386
+ declare function mutableStore<T extends AnyRecord>(value: T, opt?: CreateSignalOptions<T> & {
1387
+ injector?: Injector;
1388
+ }): MutableSignalStore<T>;
1103
1389
 
1104
1390
  /**
1105
1391
  * Interface for storage mechanisms compatible with the `stored` signal.
@@ -1163,6 +1449,10 @@ type CreateStoredOptions<T> = CreateSignalOptions<T> & {
1163
1449
  * If 'true', the signal will remove the old key from storage when the key changes, defaults to `false`.
1164
1450
  */
1165
1451
  cleanupOldKey?: boolean;
1452
+ /**
1453
+ * Optional validator, which is called on load of value. Store will be set to fallback if value is false
1454
+ */
1455
+ validate?: (value: T) => boolean;
1166
1456
  };
1167
1457
  /**
1168
1458
  * A specialized `WritableSignal` returned by the `stored()` function.
@@ -1234,7 +1524,7 @@ type StoredSignal<T> = WritableSignal<T> & {
1234
1524
  * }
1235
1525
  * ```
1236
1526
  */
1237
- declare function stored<T>(fallback: T, { key, store: providedStore, serialize, deserialize, syncTabs, equal, onKeyChange, cleanupOldKey, ...rest }: CreateStoredOptions<T>): StoredSignal<T>;
1527
+ declare function stored<T>(fallback: T, { key, store: providedStore, serialize, deserialize, syncTabs, equal, onKeyChange, cleanupOldKey, validate, ...rest }: CreateStoredOptions<T>): StoredSignal<T>;
1238
1528
 
1239
1529
  type SyncSignalOptions = {
1240
1530
  id?: string;
@@ -1380,7 +1670,14 @@ declare function throttle<T>(source: WritableSignal<T>, opt?: CreateThrottledOpt
1380
1670
  *
1381
1671
  * writableSignal.set(5); // sets value of originalValue.a to 5 & triggers all signals
1382
1672
  */
1383
- declare function toWritable<T>(signal: Signal<T>, set: (value: T) => void, update?: (updater: (value: T) => T) => void): WritableSignal<T>;
1673
+ declare function toWritable<T>(source: Signal<T>, set: (value: T) => void, update?: (updater: (value: T) => T) => void, opt?: CreateSignalOptions<T> & {
1674
+ /**
1675
+ * If `true` (the default), the returned signal will be a computed signal that depends on the source signal.
1676
+ * If `false`, the returned signal will be a direct wrapper around the source signal without creating a new computed signal.
1677
+ * @default true
1678
+ */
1679
+ pure?: boolean;
1680
+ }): WritableSignal<T>;
1384
1681
 
1385
1682
  type UntilOptions = {
1386
1683
  /**
@@ -1532,5 +1829,5 @@ type CreateHistoryOptions<T> = Omit<CreateSignalOptions<T[]>, 'equal'> & {
1532
1829
  */
1533
1830
  declare function withHistory<T>(source: WritableSignal<T>, opt?: CreateHistoryOptions<T>): SignalWithHistory<T>;
1534
1831
 
1535
- export { combineWith, debounce, debounced, derived, distinct, elementVisibility, filter, isDerivation, isMutable, map, mapArray, mediaQuery, mousePosition, mutable, nestedEffect, networkStatus, pageVisibility, pipeable, piped, prefersDarkMode, prefersReducedMotion, scrollPosition, select, sensor, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toWritable, until, windowSize, withHistory };
1536
- export type { CreateDebouncedOptions, CreateHistoryOptions, CreateStoredOptions, CreateThrottledOptions, DebouncedSignal, DerivedSignal, ElementVisibilityOptions, ElementVisibilitySignal, MousePositionOptions, MousePositionSignal, MutableSignal, NetworkStatusSignal, PipeableSignal, ScrollPosition, ScrollPositionOptions, ScrollPositionSignal, SignalWithHistory, StoredSignal, ThrottledSignal, UntilOptions, WindowSize, WindowSizeOptions, WindowSizeSignal };
1832
+ export { chunked, combineWith, debounce, debounced, derived, distinct, elementSize, elementVisibility, filter, indexArray, isDerivation, isMutable, isStore, keyArray, map, mapArray, mapObject, mediaQuery, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, pageVisibility, pipeable, piped, prefersDarkMode, prefersReducedMotion, scrollPosition, select, sensor, sensors, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
1833
+ export type { CreateChunkedOptions, CreateDebouncedOptions, CreateHistoryOptions, CreateStoredOptions, CreateThrottledOptions, DebouncedSignal, DerivedSignal, ElementSize, ElementSizeOptions, ElementSizeSignal, ElementVisibilityOptions, ElementVisibilitySignal, MousePositionOptions, MousePositionSignal, MutableSignal, MutableSignalStore, NetworkStatusSignal, PipeableSignal, ScrollPosition, ScrollPositionOptions, ScrollPositionSignal, SignalStore, SignalWithHistory, StoredSignal, ThrottledSignal, UntilOptions, WindowSize, WindowSizeOptions, WindowSizeSignal, WritableSignalStore };