@mmstack/primitives 19.3.6 → 19.3.7

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