@mmstack/primitives 21.0.23 → 21.0.24

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.
@@ -600,7 +600,30 @@ const mapArray = indexArray;
600
600
  * @param mapFn The mapping function. Receives the item and its index as a Signal.
601
601
  * @param options Optional configuration:
602
602
  * - `onDestroy`: A callback invoked when a mapped item is removed from the array.
603
+ * - `key`: A custom key extractor for identity matching (e.g. `(item) => item.id`)
604
+ * when item references change but conceptual identity is preserved.
603
605
  * @returns A `Signal<U[]>` containing the mapped array.
606
+ *
607
+ * @example
608
+ * ```ts
609
+ * const users = signal([
610
+ * { id: 1, name: 'Alice' },
611
+ * { id: 2, name: 'Bob' },
612
+ * ]);
613
+ *
614
+ * const rows = keyArray(
615
+ * users,
616
+ * (user, index) => ({
617
+ * label: computed(() => `#${index()} ${user.name}`),
618
+ * id: user.id,
619
+ * }),
620
+ * { key: (u) => u.id },
621
+ * );
622
+ *
623
+ * // Reordering users() rebuilds index signals only — `rows` entries
624
+ * // are matched by id and reused, not re-created.
625
+ * users.set([users()[1], users()[0]]);
626
+ * ```
604
627
  */
605
628
  function keyArray(source, mapFn, options = {}) {
606
629
  const sourceSignal = isSignal(source) ? source : computed(source);
@@ -757,15 +780,69 @@ function mapObject(source, mapFn, options = {}) {
757
780
  }).asReadonly();
758
781
  }
759
782
 
760
- /** Project with optional equality. Pure & sync. */
783
+ /**
784
+ * Synchronous projection of a signal value with optional `CreateSignalOptions`
785
+ * (custom `equal`, `debugName`, etc.). Equivalent to `map` plus the ability to
786
+ * pass signal options through to the underlying `computed()`.
787
+ *
788
+ * @example
789
+ * ```ts
790
+ * const user = piped({ id: 1, name: 'Alice' });
791
+ * const name = user.pipe(select((u) => u.name));
792
+ * name(); // 'Alice'
793
+ * ```
794
+ */
761
795
  const select = (projector, opt) => (src) => computed(() => projector(src()), opt);
762
- /** Combine with another signal using a projector. */
796
+ /**
797
+ * Combine the piped signal with another `Signal` using a projector. The result
798
+ * recomputes whenever either source changes.
799
+ *
800
+ * @example
801
+ * ```ts
802
+ * const price = piped(10);
803
+ * const quantity = signal(3);
804
+ * const total = price.pipe(combineWith(quantity, (p, q) => p * q));
805
+ * total(); // 30
806
+ * ```
807
+ */
763
808
  const combineWith = (other, project, opt) => (src) => computed(() => project(src(), other()), opt);
764
- /** Only re-emit when equal(prev, next) is false. */
809
+ /**
810
+ * Suppress emissions while consecutive values are considered equal. The
811
+ * comparator defaults to `Object.is`; pass a custom one for structural or
812
+ * key-based equality (e.g. compare by `id` only).
813
+ *
814
+ * @example
815
+ * ```ts
816
+ * const user = piped({ id: 1, lastSeen: Date.now() });
817
+ * const byId = user.pipe(distinct((a, b) => a.id === b.id));
818
+ * // byId only re-emits when `id` changes, not on every `lastSeen` update
819
+ * ```
820
+ */
765
821
  const distinct = (equal = Object.is) => (src) => computed(() => src(), { equal });
766
- /** map to new value */
822
+ /**
823
+ * Pure synchronous transform from input to output. Equivalent to a `computed()`
824
+ * that reads the source and returns `fn(value)`.
825
+ *
826
+ * @example
827
+ * ```ts
828
+ * const count = piped(2);
829
+ * const doubled = count.pipe(map((n) => n * 2));
830
+ * doubled(); // 4
831
+ * ```
832
+ */
767
833
  const map = (fn) => (src) => computed(() => fn(src()));
768
- /** filter values, keeping the last value if it was ever available, if first value is filtered will return undefined */
834
+ /**
835
+ * Keep only values that pass the predicate. The result holds the last passing
836
+ * value across emissions; before any value passes, the result is `undefined` —
837
+ * see {@link filterWith} when you need a non-`undefined` seed.
838
+ *
839
+ * @example
840
+ * ```ts
841
+ * const event = piped<MouseEvent | null>(null);
842
+ * const clicks = event.pipe(filter((e): e is MouseEvent => e?.type === 'click'));
843
+ * clicks(); // undefined until a click happens, then the last MouseEvent
844
+ * ```
845
+ */
769
846
  const filter = (predicate) => (src) => linkedSignal({
770
847
  source: src,
771
848
  computation: (next, prev) => {
@@ -774,7 +851,19 @@ const filter = (predicate) => (src) => linkedSignal({
774
851
  return prev?.source;
775
852
  },
776
853
  });
777
- /** tap into the value */
854
+ /**
855
+ * Run a side effect on every emission without altering the signal value. Wraps
856
+ * Angular's `effect()`, so it must run in an injection context or receive an
857
+ * explicit `injector`. Use for logging / analytics — not for setting other
858
+ * signals (that's what regular `effect()` is for).
859
+ *
860
+ * @example
861
+ * ```ts
862
+ * const count = piped(0);
863
+ * count.pipe(tap((n) => console.log('count:', n)));
864
+ * count.set(1); // logs 'count: 1'
865
+ * ```
866
+ */
778
867
  const tap = (fn, injector) => (src) => {
779
868
  effect(() => fn(src()), {
780
869
  injector,
@@ -782,24 +871,66 @@ const tap = (fn, injector) => (src) => {
782
871
  return src;
783
872
  };
784
873
  /**
785
- * Like {@link filter}, but emits `initial` until a value passes the predicate
786
- * for the first time. Avoids the `T | undefined` return type.
874
+ * Like {@link filter}, but emits `initial` until a value first passes the
875
+ * predicate. Eliminates the `T | undefined` return type at the cost of an
876
+ * explicit seed value.
877
+ *
878
+ * @example
879
+ * ```ts
880
+ * const event = piped<MouseEvent | null>(null);
881
+ * const lastClick = event.pipe(filterWith((e) => e?.type === 'click', null));
882
+ * lastClick(); // null until the first click, then the most recent click event
883
+ * ```
787
884
  */
788
885
  const filterWith = (predicate, initial) => (src) => linkedSignal({
789
886
  source: src,
790
887
  computation: (next, prev) => predicate(next) ? next : (prev?.value ?? initial),
791
888
  });
792
- /** Emits `initial` first, then mirrors source. */
889
+ /**
890
+ * Emit `initial` on the first read, then mirror the source on every subsequent
891
+ * read. Useful for giving a pipeline a sensible seed value before the source
892
+ * is ready (e.g. loading state).
893
+ *
894
+ * @example
895
+ * ```ts
896
+ * const data = piped<User | null>(null);
897
+ * const view = data.pipe(startWith<User | null, 'loading'>('loading'));
898
+ * view(); // 'loading' on first read, then User | null afterward
899
+ * ```
900
+ */
793
901
  const startWith = (initial) => (src) => linkedSignal({
794
902
  source: src,
795
903
  computation: (next, prev) => (prev === undefined ? initial : next),
796
904
  });
797
- /** Emits `[prev, curr]` pairs. The first emission has prev = undefined. */
905
+ /**
906
+ * Emit `[prev, curr]` tuples so consumers can react to transitions instead of
907
+ * raw values. On the first emission `prev` is `undefined`.
908
+ *
909
+ * @example
910
+ * ```ts
911
+ * const count = piped(0);
912
+ * const delta = count.pipe(pairwise(), map(([prev, curr]) => curr - (prev ?? 0)));
913
+ * count.set(5);
914
+ * delta(); // 5
915
+ * ```
916
+ */
798
917
  const pairwise = () => (src) => linkedSignal({
799
918
  source: src,
800
919
  computation: (next, prev) => [prev?.source, next],
801
920
  });
802
- /** Reduce-like accumulator across emissions. */
921
+ /**
922
+ * Reduce-like accumulator that folds each emission into a running result.
923
+ * Behaves like `Array.prototype.reduce` but applied over time, with the
924
+ * accumulator persisted across emissions.
925
+ *
926
+ * @example
927
+ * ```ts
928
+ * const delta = piped(0);
929
+ * const total = delta.pipe(scan((acc, n) => acc + n, 0));
930
+ * delta.set(5); // total() === 5
931
+ * delta.set(3); // total() === 8
932
+ * ```
933
+ */
803
934
  const scan = (reducer, seed) => (src) => linkedSignal({
804
935
  source: src,
805
936
  computation: (next, prev) => reducer(prev?.value ?? seed, next),
@@ -968,6 +1099,15 @@ const EVENTS = [
968
1099
  * Battery Status API. Returns `null` until the underlying `getBattery()`
969
1100
  * promise resolves, or permanently when the API is unsupported (Firefox /
970
1101
  * Safari at the time of writing). SSR-safe.
1102
+ *
1103
+ * @example
1104
+ * ```ts
1105
+ * const battery = batteryStatus();
1106
+ * effect(() => {
1107
+ * const b = battery();
1108
+ * if (b) console.log(`${Math.round(b.level * 100)}% • charging: ${b.charging}`);
1109
+ * });
1110
+ * ```
971
1111
  */
972
1112
  function batteryStatus(debugName = 'batteryStatus') {
973
1113
  if (isPlatformServer(inject(PLATFORM_ID)) ||
@@ -1250,6 +1390,15 @@ function unwrap$1(target) {
1250
1390
  * Defaults `target` to the current `ElementRef` so it can be used inline in a
1251
1391
  * component's `class` field. SSR-safe — returns a constant `false` signal on
1252
1392
  * the server.
1393
+ *
1394
+ * @example
1395
+ * ```ts
1396
+ * @Component({ ... })
1397
+ * class MenuComponent {
1398
+ * // Defaults to the host element — flips true when focus is inside.
1399
+ * readonly hasFocus = focusWithin();
1400
+ * }
1401
+ * ```
1253
1402
  */
1254
1403
  function focusWithin(target = inject(ElementRef)) {
1255
1404
  if (isPlatformServer(inject(PLATFORM_ID))) {
@@ -1352,6 +1501,14 @@ const serverDate$1 = new Date();
1352
1501
  * activity) resets the timer and flips the signal back to `false`.
1353
1502
  *
1354
1503
  * SSR-safe — always `false` with a frozen `since` date on the server.
1504
+ *
1505
+ * @example
1506
+ * ```ts
1507
+ * const isAway = idle({ ms: 30_000 });
1508
+ * effect(() => {
1509
+ * if (isAway()) console.log('idle since', isAway.since());
1510
+ * });
1511
+ * ```
1355
1512
  */
1356
1513
  function idle(opt) {
1357
1514
  if (isPlatformServer(inject(PLATFORM_ID))) {
@@ -1715,6 +1872,14 @@ const serverDate = new Date();
1715
1872
  *
1716
1873
  * @param debugName Optional debug name for the signal.
1717
1874
  * @returns A `NetworkStatusSignal` instance.
1875
+ *
1876
+ * @example
1877
+ * ```ts
1878
+ * const online = networkStatus();
1879
+ * effect(() => {
1880
+ * if (!online()) console.log('offline since', online.since());
1881
+ * });
1882
+ * ```
1718
1883
  */
1719
1884
  function networkStatus(debugName = 'networkStatus') {
1720
1885
  if (isPlatformServer(inject(PLATFORM_ID))) {
@@ -1754,6 +1919,15 @@ const SSR_FALLBACK = {
1754
1919
  *
1755
1920
  * SSR-safe — returns a constant `portrait-primary / 0°` signal on the server
1756
1921
  * and in environments without `screen.orientation` support.
1922
+ *
1923
+ * @example
1924
+ * ```ts
1925
+ * const screenOrientation = orientation();
1926
+ * effect(() => {
1927
+ * const { type, angle } = screenOrientation();
1928
+ * console.log(`${type} at ${angle}°`);
1929
+ * });
1930
+ * ```
1757
1931
  */
1758
1932
  function orientation(debugName = 'orientation') {
1759
1933
  if (isPlatformServer(inject(PLATFORM_ID)) ||
@@ -2033,6 +2207,27 @@ function sensor(type, options) {
2033
2207
  throw new Error(`Unknown sensor type: ${type}`);
2034
2208
  }
2035
2209
  }
2210
+ /**
2211
+ * Bulk sensor factory — creates several sensor signals at once and returns
2212
+ * them keyed by sensor type. Convenient when a single consumer needs to react
2213
+ * to multiple browser signals; for a single sensor prefer {@link sensor}
2214
+ * directly.
2215
+ *
2216
+ * @typeParam TType The union of sensor keys being requested.
2217
+ * @param track Array of sensor type keys to create.
2218
+ * @param opt Optional per-sensor options keyed by sensor type.
2219
+ * @returns A record `{ [key]: <SensorReturnType> }` for each requested key.
2220
+ *
2221
+ * @example
2222
+ * ```ts
2223
+ * const { windowSize, networkStatus } = sensors(
2224
+ * ['windowSize', 'networkStatus'],
2225
+ * { windowSize: { throttle: 200 } },
2226
+ * );
2227
+ *
2228
+ * effect(() => console.log(windowSize(), networkStatus()));
2229
+ * ```
2230
+ */
2036
2231
  function sensors(track, opt) {
2037
2232
  return track.reduce((result, key) => {
2038
2233
  result[key] = sensor(key, opt?.[key]);