@mmstack/primitives 20.5.5 → 20.5.6

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { isDevMode, inject, Injector, untracked, effect, DestroyRef, linkedSignal, computed, signal, isSignal, ElementRef, PLATFORM_ID, Injectable, runInInjectionContext } from '@angular/core';
2
+ import { isDevMode, inject, Injector, untracked, effect, DestroyRef, linkedSignal, computed, signal, isSignal, PLATFORM_ID, ElementRef, Injectable, runInInjectionContext } from '@angular/core';
3
3
  import { isPlatformServer } from '@angular/common';
4
4
  import { SIGNAL } from '@angular/core/primitives/signals';
5
5
 
@@ -353,9 +353,9 @@ function debounce(source, opt) {
353
353
  const { is } = Object;
354
354
  function mutable(initial, opt) {
355
355
  const baseEqual = opt?.equal ?? is;
356
- let trigger = false;
356
+ let cnt = 0;
357
357
  const equal = (a, b) => {
358
- if (trigger)
358
+ if (cnt > 0)
359
359
  return false;
360
360
  return baseEqual(a, b);
361
361
  };
@@ -365,9 +365,9 @@ function mutable(initial, opt) {
365
365
  });
366
366
  const internalUpdate = sig.update;
367
367
  sig.mutate = (updater) => {
368
- trigger = true;
368
+ cnt++;
369
369
  internalUpdate(updater);
370
- trigger = false;
370
+ cnt--;
371
371
  };
372
372
  sig.inline = (updater) => {
373
373
  sig.mutate((prev) => {
@@ -434,10 +434,10 @@ function derived(source, optOrKey, opt) {
434
434
  };
435
435
  const rest = typeof optOrKey === 'object' ? { ...optOrKey, ...opt } : opt;
436
436
  const baseEqual = rest?.equal ?? Object.is;
437
- let trigger = false;
437
+ let cnt = 0;
438
438
  const equal = isMutable(source)
439
439
  ? (a, b) => {
440
- if (trigger)
440
+ if (cnt > 0)
441
441
  return false;
442
442
  return baseEqual(a, b);
443
443
  }
@@ -446,9 +446,9 @@ function derived(source, optOrKey, opt) {
446
446
  sig.from = from;
447
447
  if (isMutable(source)) {
448
448
  sig.mutate = (updater) => {
449
- trigger = true;
449
+ cnt++;
450
450
  sig.update(updater);
451
- trigger = false;
451
+ cnt--;
452
452
  };
453
453
  sig.inline = (updater) => {
454
454
  sig.mutate((prev) => {
@@ -553,9 +553,11 @@ function indexArray(source, map, opt = {}) {
553
553
  });
554
554
  if (isWritableSignal(data) && isMutable(data) && !opt.equal) {
555
555
  opt.equal = (a, b) => {
556
- if (a !== b)
557
- return false; // actually check primitives and references
558
- return false; // opt out for same refs
556
+ if (typeof a !== typeof b)
557
+ return false;
558
+ if (typeof a === 'object' || typeof a === 'function')
559
+ return false;
560
+ return a === b;
559
561
  };
560
562
  }
561
563
  return linkedSignal({
@@ -777,10 +779,35 @@ const filter = (predicate) => (src) => linkedSignal({
777
779
  },
778
780
  });
779
781
  /** tap into the value */
780
- const tap = (fn) => (src) => {
781
- effect(() => fn(src()));
782
+ const tap = (fn, injector) => (src) => {
783
+ effect(() => fn(src()), {
784
+ injector,
785
+ });
782
786
  return src;
783
787
  };
788
+ /**
789
+ * Like {@link filter}, but emits `initial` until a value passes the predicate
790
+ * for the first time. Avoids the `T | undefined` return type.
791
+ */
792
+ const filterWith = (predicate, initial) => (src) => linkedSignal({
793
+ source: src,
794
+ computation: (next, prev) => predicate(next) ? next : (prev?.value ?? initial),
795
+ });
796
+ /** Emits `initial` first, then mirrors source. */
797
+ const startWith = (initial) => (src) => linkedSignal({
798
+ source: src,
799
+ computation: (next, prev) => (prev === undefined ? initial : next),
800
+ });
801
+ /** Emits `[prev, curr]` pairs. The first emission has prev = undefined. */
802
+ const pairwise = () => (src) => linkedSignal({
803
+ source: src,
804
+ computation: (next, prev) => [prev?.source, next],
805
+ });
806
+ /** Reduce-like accumulator across emissions. */
807
+ const scan = (reducer, seed) => (src) => linkedSignal({
808
+ source: src,
809
+ computation: (next, prev) => reducer(prev?.value ?? seed, next),
810
+ });
784
811
 
785
812
  /**
786
813
  * Decorate any `Signal<T>` with a chainable `.pipe(...)` method.
@@ -884,12 +911,10 @@ function pooled({ create, reset, computation, ...opt }) {
884
911
  const next = other ?? untracked(() => create());
885
912
  if (current !== undefined) {
886
913
  if (currentFresh) {
887
- // never-mutated buffer leaving the active slot; nothing to clean
888
914
  other = current;
889
915
  }
890
916
  else {
891
- // reset on release: clean the dirty buffer as it goes back into the pool
892
- // (also threads the swap-return correctly into the pool's spare slot)
917
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guarded by the `current !== undefined` check above
893
918
  other = untracked(() => reset(current)) ?? current;
894
919
  }
895
920
  }
@@ -936,6 +961,87 @@ function pooledMap(optOrComputation, signalOpt) {
936
961
  return pooled(toPooledOptions(optOrComputation, createEmptyMap, resetClearable, signalOpt));
937
962
  }
938
963
 
964
+ const EVENTS = [
965
+ 'chargingchange',
966
+ 'levelchange',
967
+ 'chargingtimechange',
968
+ 'dischargingtimechange',
969
+ ];
970
+ /**
971
+ * Creates a read-only signal that tracks the system battery status using the
972
+ * Battery Status API. Returns `null` until the underlying `getBattery()`
973
+ * promise resolves, or permanently when the API is unsupported (Firefox /
974
+ * Safari at the time of writing). SSR-safe.
975
+ */
976
+ function batteryStatus(debugName = 'batteryStatus') {
977
+ if (isPlatformServer(inject(PLATFORM_ID)) ||
978
+ typeof navigator === 'undefined' ||
979
+ typeof navigator.getBattery !== 'function') {
980
+ return computed(() => null, { debugName });
981
+ }
982
+ const state = signal(null, ...(ngDevMode ? [{ debugName: "state", debugName }] : [{ debugName }]));
983
+ const abortController = new AbortController();
984
+ inject(DestroyRef).onDestroy(() => abortController.abort());
985
+ navigator.getBattery().then((battery) => {
986
+ if (abortController.signal.aborted)
987
+ return;
988
+ const read = () => ({
989
+ level: battery.level,
990
+ charging: battery.charging,
991
+ chargingTime: battery.chargingTime,
992
+ dischargingTime: battery.dischargingTime,
993
+ });
994
+ const onChange = () => state.set(read());
995
+ state.set(read());
996
+ for (const ev of EVENTS) {
997
+ battery.addEventListener(ev, onChange, {
998
+ signal: abortController.signal,
999
+ });
1000
+ }
1001
+ });
1002
+ return state.asReadonly();
1003
+ }
1004
+
1005
+ /**
1006
+ * Creates a read-only signal mirroring the system clipboard contents.
1007
+ *
1008
+ * The signal value starts empty and updates whenever a `copy` event fires on
1009
+ * the document (or {@link ClipboardSignal.copy} is invoked from this app).
1010
+ * SSR-safe — returns `''` and `isSupported: false` on the server.
1011
+ *
1012
+ * Note: read access requires the Clipboard API and an active permission grant
1013
+ * in browsers that gate it. Errors from `navigator.clipboard.readText` are
1014
+ * swallowed silently to keep the signal value stable.
1015
+ */
1016
+ function clipboard(debugName = 'clipboard') {
1017
+ if (isPlatformServer(inject(PLATFORM_ID)) ||
1018
+ typeof navigator === 'undefined' ||
1019
+ !navigator.clipboard) {
1020
+ const sig = computed(() => '', { debugName });
1021
+ sig.copy = () => Promise.resolve();
1022
+ sig.isSupported = computed(() => false, ...(ngDevMode ? [{ debugName: "isSupported" }] : []));
1023
+ return sig;
1024
+ }
1025
+ const state = signal('', ...(ngDevMode ? [{ debugName: "state", debugName }] : [{ debugName }]));
1026
+ const refresh = () => {
1027
+ navigator.clipboard.readText().then((value) => state.set(value), () => {
1028
+ // permission denied / focus required — ignore
1029
+ });
1030
+ };
1031
+ const abortController = new AbortController();
1032
+ const onCopy = () => refresh();
1033
+ document.addEventListener('copy', onCopy, { signal: abortController.signal });
1034
+ document.addEventListener('cut', onCopy, { signal: abortController.signal });
1035
+ inject(DestroyRef).onDestroy(() => abortController.abort());
1036
+ const sig = state.asReadonly();
1037
+ sig.copy = async (value) => {
1038
+ await navigator.clipboard.writeText(value);
1039
+ state.set(value);
1040
+ };
1041
+ sig.isSupported = computed(() => true, ...(ngDevMode ? [{ debugName: "isSupported" }] : []));
1042
+ return sig;
1043
+ }
1044
+
939
1045
  function observerSupported$1() {
940
1046
  return typeof ResizeObserver !== 'undefined';
941
1047
  }
@@ -1136,6 +1242,167 @@ function elementVisibility(target = inject(ElementRef), opt) {
1136
1242
  return base;
1137
1243
  }
1138
1244
 
1245
+ function unwrap$1(target) {
1246
+ if (!target)
1247
+ return null;
1248
+ return target instanceof ElementRef ? target.nativeElement : target;
1249
+ }
1250
+ /**
1251
+ * Creates a read-only signal that tracks whether the focused element is the
1252
+ * target or a descendant of it. Mirrors the CSS `:focus-within` pseudo-class.
1253
+ *
1254
+ * Defaults `target` to the current `ElementRef` so it can be used inline in a
1255
+ * component's `class` field. SSR-safe — returns a constant `false` signal on
1256
+ * the server.
1257
+ */
1258
+ function focusWithin(target = inject(ElementRef)) {
1259
+ if (isPlatformServer(inject(PLATFORM_ID))) {
1260
+ return computed(() => false, { debugName: 'focusWithin' });
1261
+ }
1262
+ const state = signal(false, { debugName: 'focusWithin' });
1263
+ const attach = (el) => {
1264
+ state.set(el.contains(document.activeElement));
1265
+ const abortController = new AbortController();
1266
+ el.addEventListener('focusin', () => state.set(true), {
1267
+ signal: abortController.signal,
1268
+ });
1269
+ el.addEventListener('focusout', () => {
1270
+ // Defer so `document.activeElement` reflects the focus move.
1271
+ queueMicrotask(() => state.set(el.contains(document.activeElement)));
1272
+ }, { signal: abortController.signal });
1273
+ return () => abortController.abort();
1274
+ };
1275
+ if (isSignal(target)) {
1276
+ const targetSig = target;
1277
+ effect((cleanup) => {
1278
+ const el = unwrap$1(targetSig());
1279
+ if (!el) {
1280
+ state.set(false);
1281
+ return;
1282
+ }
1283
+ cleanup(attach(el));
1284
+ });
1285
+ }
1286
+ else {
1287
+ const el = unwrap$1(target);
1288
+ if (el) {
1289
+ const detach = attach(el);
1290
+ inject(DestroyRef).onDestroy(detach);
1291
+ }
1292
+ }
1293
+ return state.asReadonly();
1294
+ }
1295
+
1296
+ /**
1297
+ * Creates a read-only signal that exposes the current geolocation position.
1298
+ *
1299
+ * The returned signal carries `error` and `loading` sub-signals for permission
1300
+ * failures and the in-flight initial fetch respectively. SSR-safe — on the
1301
+ * server the position is `null`, loading is `false`, and no API calls are made.
1302
+ *
1303
+ * @example
1304
+ * ```ts
1305
+ * const where = geolocation({ watch: true, enableHighAccuracy: true });
1306
+ * effect(() => console.log(where()?.coords, where.error()));
1307
+ * ```
1308
+ */
1309
+ function geolocation(opt) {
1310
+ if (isPlatformServer(inject(PLATFORM_ID)) || typeof navigator === 'undefined' || !navigator.geolocation) {
1311
+ const sig = computed(() => null, {
1312
+ debugName: opt?.debugName ?? 'geolocation',
1313
+ });
1314
+ sig.error = computed(() => null, ...(ngDevMode ? [{ debugName: "error" }] : []));
1315
+ sig.loading = computed(() => false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
1316
+ return sig;
1317
+ }
1318
+ const position = signal(null, {
1319
+ debugName: opt?.debugName ?? 'geolocation',
1320
+ });
1321
+ const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
1322
+ const loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : []));
1323
+ const onSuccess = (p) => {
1324
+ position.set(p);
1325
+ error.set(null);
1326
+ loading.set(false);
1327
+ };
1328
+ const onError = (e) => {
1329
+ error.set(e);
1330
+ loading.set(false);
1331
+ };
1332
+ if (opt?.watch) {
1333
+ const watchId = navigator.geolocation.watchPosition(onSuccess, onError, opt);
1334
+ inject(DestroyRef).onDestroy(() => navigator.geolocation.clearWatch(watchId));
1335
+ }
1336
+ else {
1337
+ navigator.geolocation.getCurrentPosition(onSuccess, onError, opt);
1338
+ }
1339
+ const sig = position.asReadonly();
1340
+ sig.error = error.asReadonly();
1341
+ sig.loading = loading.asReadonly();
1342
+ return sig;
1343
+ }
1344
+
1345
+ const DEFAULT_EVENTS = [
1346
+ 'mousemove',
1347
+ 'keydown',
1348
+ 'touchstart',
1349
+ 'scroll',
1350
+ 'visibilitychange',
1351
+ ];
1352
+ const serverDate$1 = new Date();
1353
+ /**
1354
+ * Creates a read-only signal that flips to `true` after a window of user
1355
+ * inactivity. Any of the configured `events` (default: pointer/keyboard/scroll
1356
+ * activity) resets the timer and flips the signal back to `false`.
1357
+ *
1358
+ * SSR-safe — always `false` with a frozen `since` date on the server.
1359
+ */
1360
+ function idle(opt) {
1361
+ if (isPlatformServer(inject(PLATFORM_ID))) {
1362
+ const sig = computed(() => false, {
1363
+ debugName: opt?.debugName ?? 'idle',
1364
+ });
1365
+ sig.since = computed(() => serverDate$1, ...(ngDevMode ? [{ debugName: "since" }] : []));
1366
+ return sig;
1367
+ }
1368
+ const ms = opt?.ms ?? 60_000;
1369
+ const events = opt?.events ?? DEFAULT_EVENTS;
1370
+ const state = signal(false, { debugName: opt?.debugName ?? 'idle' });
1371
+ const since = signal(new Date(), ...(ngDevMode ? [{ debugName: "since" }] : []));
1372
+ let timer;
1373
+ const goIdle = () => {
1374
+ if (state())
1375
+ return;
1376
+ state.set(true);
1377
+ since.set(new Date());
1378
+ };
1379
+ const reset = () => {
1380
+ if (timer)
1381
+ clearTimeout(timer);
1382
+ if (state()) {
1383
+ state.set(false);
1384
+ since.set(new Date());
1385
+ }
1386
+ timer = setTimeout(goIdle, ms);
1387
+ };
1388
+ const abortController = new AbortController();
1389
+ for (const ev of events) {
1390
+ window.addEventListener(ev, reset, {
1391
+ passive: true,
1392
+ signal: abortController.signal,
1393
+ });
1394
+ }
1395
+ timer = setTimeout(goIdle, ms);
1396
+ inject(DestroyRef).onDestroy(() => {
1397
+ if (timer)
1398
+ clearTimeout(timer);
1399
+ abortController.abort();
1400
+ });
1401
+ const sig = state.asReadonly();
1402
+ sig.since = since.asReadonly();
1403
+ return sig;
1404
+ }
1405
+
1139
1406
  /**
1140
1407
  * Creates a read-only signal that reactively tracks whether a CSS media query
1141
1408
  * string currently matches.
@@ -1297,26 +1564,41 @@ function throttled(initial, opt) {
1297
1564
  */
1298
1565
  function throttle(source, opt) {
1299
1566
  const ms = opt?.ms ?? 0;
1567
+ const leading = opt?.leading ?? false;
1568
+ const trailing = opt?.trailing ?? true;
1300
1569
  const trigger = signal(false, ...(ngDevMode ? [{ debugName: "trigger" }] : []));
1570
+ const fire = () => trigger.update((c) => !c);
1301
1571
  let timeout;
1572
+ let pendingTrailing = false;
1302
1573
  try {
1303
1574
  const destroyRef = opt?.destroyRef ?? inject(DestroyRef, { optional: true });
1304
1575
  destroyRef?.onDestroy(() => {
1305
1576
  if (timeout)
1306
1577
  clearTimeout(timeout);
1307
1578
  timeout = undefined;
1579
+ pendingTrailing = false;
1308
1580
  });
1309
1581
  }
1310
1582
  catch {
1311
1583
  // not in injection context & no destroyRef provided opting out of cleanup
1312
1584
  }
1313
1585
  const tick = () => {
1314
- if (timeout)
1586
+ if (!timeout) {
1587
+ if (leading)
1588
+ fire();
1589
+ else
1590
+ pendingTrailing = trailing;
1591
+ timeout = setTimeout(() => {
1592
+ timeout = undefined;
1593
+ if (trailing && pendingTrailing) {
1594
+ pendingTrailing = false;
1595
+ fire();
1596
+ }
1597
+ }, ms);
1315
1598
  return;
1316
- timeout = setTimeout(() => {
1317
- trigger.update((c) => !c);
1318
- timeout = undefined;
1319
- }, ms);
1599
+ }
1600
+ if (trailing)
1601
+ pendingTrailing = true;
1320
1602
  };
1321
1603
  const set = (value) => {
1322
1604
  source.set(value);
@@ -1469,6 +1751,38 @@ function networkStatus(debugName = 'networkStatus') {
1469
1751
  return sig;
1470
1752
  }
1471
1753
 
1754
+ const SSR_FALLBACK = {
1755
+ angle: 0,
1756
+ type: 'portrait-primary',
1757
+ };
1758
+ /**
1759
+ * Creates a read-only signal that tracks `screen.orientation`.
1760
+ *
1761
+ * SSR-safe — returns a constant `portrait-primary / 0°` signal on the server
1762
+ * and in environments without `screen.orientation` support.
1763
+ */
1764
+ function orientation(debugName = 'orientation') {
1765
+ if (isPlatformServer(inject(PLATFORM_ID)) ||
1766
+ typeof screen === 'undefined' ||
1767
+ !screen.orientation) {
1768
+ return computed(() => SSR_FALLBACK, { debugName });
1769
+ }
1770
+ const so = screen.orientation;
1771
+ const read = () => ({
1772
+ angle: so.angle,
1773
+ type: so.type,
1774
+ });
1775
+ const state = signal(read(), ...(ngDevMode ? [{ debugName: "state", debugName,
1776
+ equal: (a, b) => a.angle === b.angle && a.type === b.type }] : [{
1777
+ debugName,
1778
+ equal: (a, b) => a.angle === b.angle && a.type === b.type,
1779
+ }]));
1780
+ const onChange = () => state.set(read());
1781
+ so.addEventListener('change', onChange);
1782
+ inject(DestroyRef).onDestroy(() => so.removeEventListener('change', onChange));
1783
+ return state.asReadonly();
1784
+ }
1785
+
1472
1786
  /**
1473
1787
  * Creates a read-only signal that tracks the page's visibility state.
1474
1788
  *
@@ -1685,35 +1999,42 @@ function windowSize(opt) {
1685
1999
  * @internal
1686
2000
  */
1687
2001
  function sensor(type, options) {
2002
+ const opts = options;
1688
2003
  switch (type) {
1689
2004
  case 'mousePosition':
1690
- return mousePosition(options);
2005
+ return mousePosition(opts);
1691
2006
  case 'networkStatus':
1692
- return networkStatus(options?.debugName);
2007
+ return networkStatus(opts?.debugName);
1693
2008
  case 'pageVisibility':
1694
- return pageVisibility(options?.debugName);
2009
+ return pageVisibility(opts?.debugName);
1695
2010
  case 'darkMode':
1696
2011
  case 'dark-mode':
1697
- return prefersDarkMode(options?.debugName);
2012
+ return prefersDarkMode(opts?.debugName);
1698
2013
  case 'reducedMotion':
1699
2014
  case 'reduced-motion':
1700
- return prefersReducedMotion(options?.debugName);
1701
- case 'mediaQuery': {
1702
- const opt = options;
1703
- return mediaQuery(opt.query, opt.debugName);
1704
- }
2015
+ return prefersReducedMotion(opts?.debugName);
2016
+ case 'mediaQuery':
2017
+ return mediaQuery(opts.query, opts.debugName);
1705
2018
  case 'windowSize':
1706
- return windowSize(options);
2019
+ return windowSize(opts);
1707
2020
  case 'scrollPosition':
1708
- return scrollPosition(options);
1709
- case 'elementVisibility': {
1710
- const opt = options;
1711
- return elementVisibility(opt.target, opt);
1712
- }
1713
- case 'elementSize': {
1714
- const opt = options;
1715
- return elementSize(opt.target, opt);
1716
- }
2021
+ return scrollPosition(opts);
2022
+ case 'elementVisibility':
2023
+ return elementVisibility(opts?.target, opts);
2024
+ case 'elementSize':
2025
+ return elementSize(opts?.target, opts);
2026
+ case 'geolocation':
2027
+ return geolocation(opts);
2028
+ case 'clipboard':
2029
+ return clipboard(opts?.debugName);
2030
+ case 'orientation':
2031
+ return orientation(opts?.debugName);
2032
+ case 'batteryStatus':
2033
+ return batteryStatus(opts?.debugName);
2034
+ case 'idle':
2035
+ return idle(opts);
2036
+ case 'focusWithin':
2037
+ return focusWithin(opts?.target);
1717
2038
  default:
1718
2039
  throw new Error(`Unknown sensor type: ${type}`);
1719
2040
  }
@@ -1725,6 +2046,49 @@ function sensors(track, opt) {
1725
2046
  }, {});
1726
2047
  }
1727
2048
 
2049
+ function unwrap(t) {
2050
+ if (!t)
2051
+ return null;
2052
+ return t instanceof ElementRef ? t.nativeElement : t;
2053
+ }
2054
+ function signalFromEvent(target, eventName, initial, projectOrOpt, maybeOpt) {
2055
+ const project = typeof projectOrOpt === 'function' ? projectOrOpt : undefined;
2056
+ const opt = typeof projectOrOpt === 'function' ? maybeOpt : projectOrOpt;
2057
+ const injector = opt?.injector ?? inject(Injector);
2058
+ if (isPlatformServer(injector.get(PLATFORM_ID))) {
2059
+ return computed(() => initial, { debugName: opt?.debugName });
2060
+ }
2061
+ const state = signal(initial, {
2062
+ debugName: opt?.debugName,
2063
+ });
2064
+ const handler = (event) => {
2065
+ if (project)
2066
+ state.set(project(event));
2067
+ else
2068
+ state.set(event);
2069
+ };
2070
+ const { destroyRef: providedDestroyRef, ...listenerOpts } = opt ?? {};
2071
+ if (isSignal(target)) {
2072
+ const targetSig = target;
2073
+ effect((cleanup) => {
2074
+ const resolved = unwrap(targetSig());
2075
+ if (!resolved)
2076
+ return;
2077
+ resolved.addEventListener(eventName, handler, listenerOpts);
2078
+ cleanup(() => resolved.removeEventListener(eventName, handler, listenerOpts));
2079
+ }, { injector });
2080
+ }
2081
+ else {
2082
+ const resolved = unwrap(target);
2083
+ if (resolved) {
2084
+ resolved.addEventListener(eventName, handler, listenerOpts);
2085
+ const destroyRef = providedDestroyRef ?? injector.get(DestroyRef);
2086
+ destroyRef.onDestroy(() => resolved.removeEventListener(eventName, handler, listenerOpts));
2087
+ }
2088
+ }
2089
+ return untracked(() => state.asReadonly());
2090
+ }
2091
+
1728
2092
  const IS_STORE = Symbol('MMSTACK::IS_STORE');
1729
2093
  const PROXY_CACHE = new WeakMap();
1730
2094
  const SIGNAL_FN_PROP = new Set([
@@ -2224,14 +2588,11 @@ function generateDeterministicID() {
2224
2588
  *
2225
2589
  * @template T - The type of the WritableSignal
2226
2590
  * @param sig - The WritableSignal to synchronize across tabs
2227
- * @param opt - Optional configuration object
2228
- * @param opt.id - Explicit channel ID for synchronization. If not provided,
2229
- * a deterministic ID is generated based on the call site.
2230
- * Use explicit IDs in production for reliability.
2591
+ * @param opt - configuration object
2592
+ * @param opt.id - Explicit channel ID for synchronization.
2231
2593
  *
2232
2594
  * @returns The same WritableSignal instance, now synchronized across tabs
2233
2595
  *
2234
- * @throws {Error} When deterministic ID generation fails and no explicit ID is provided
2235
2596
  *
2236
2597
  * @example
2237
2598
  * ```typescript
@@ -2255,7 +2616,7 @@ function generateDeterministicID() {
2255
2616
  function tabSync(sig, opt) {
2256
2617
  if (isPlatformServer(inject(PLATFORM_ID)))
2257
2618
  return sig;
2258
- const id = opt?.id || generateDeterministicID();
2619
+ const id = typeof opt === 'string' ? opt : (opt?.id ?? generateDeterministicID());
2259
2620
  const bus = inject(MessageBus);
2260
2621
  const { unsub, post } = bus.subscribe(id, (next) => sig.set(next));
2261
2622
  let first = false;
@@ -2387,8 +2748,13 @@ function getSignalEquality(sig) {
2387
2748
  * console.log('Can undo:', name.canUndo()); // false
2388
2749
  * ```
2389
2750
  */
2390
- function withHistory(source, opt) {
2391
- const equal = opt?.equal ?? getSignalEquality(source);
2751
+ function withHistory(sourceOrValue, opt) {
2752
+ const equal = (opt?.equal ?? isSignal(sourceOrValue))
2753
+ ? getSignalEquality(sourceOrValue)
2754
+ : Object.is;
2755
+ const source = isSignal(sourceOrValue)
2756
+ ? sourceOrValue
2757
+ : signal(sourceOrValue);
2392
2758
  const maxSize = opt?.maxSize ?? Infinity;
2393
2759
  const history = mutable([], {
2394
2760
  ...opt,
@@ -2396,20 +2762,22 @@ function withHistory(source, opt) {
2396
2762
  });
2397
2763
  const redoArray = mutable([]);
2398
2764
  const originalSet = source.set;
2765
+ const trim = (arr) => {
2766
+ if (arr.length < maxSize)
2767
+ return arr;
2768
+ if (opt?.cleanupStrategy === 'shift') {
2769
+ arr.shift();
2770
+ return arr;
2771
+ }
2772
+ return arr.slice(Math.floor(maxSize / 2));
2773
+ };
2399
2774
  const set = (value) => {
2400
2775
  const current = untracked(source);
2401
2776
  if (equal(value, current))
2402
2777
  return;
2403
2778
  source.set(value);
2404
2779
  history.mutate((c) => {
2405
- if (c.length >= maxSize) {
2406
- if (opt?.cleanupStrategy === 'shift') {
2407
- c.shift();
2408
- }
2409
- else {
2410
- c = c.slice(Math.floor(maxSize / 2));
2411
- }
2412
- }
2780
+ c = trim(c);
2413
2781
  c.push(current);
2414
2782
  return c;
2415
2783
  });
@@ -2433,7 +2801,11 @@ function withHistory(source, opt) {
2433
2801
  return;
2434
2802
  originalSet.call(source, valueToRestore);
2435
2803
  history.inline((h) => h.pop());
2436
- redoArray.inline((r) => r.push(valueForRedo));
2804
+ redoArray.mutate((r) => {
2805
+ r = trim(r);
2806
+ r.push(valueForRedo);
2807
+ return r;
2808
+ });
2437
2809
  };
2438
2810
  internal.redo = () => {
2439
2811
  const redoStack = untracked(redoArray);
@@ -2446,14 +2818,7 @@ function withHistory(source, opt) {
2446
2818
  originalSet.call(source, valueToRestore);
2447
2819
  redoArray.inline((r) => r.pop());
2448
2820
  history.mutate((h) => {
2449
- if (h.length >= maxSize) {
2450
- if (opt?.cleanupStrategy === 'shift') {
2451
- h.shift();
2452
- }
2453
- else {
2454
- h = h.slice(Math.floor(maxSize / 2));
2455
- }
2456
- }
2821
+ h = trim(h);
2457
2822
  h.push(valueForUndo);
2458
2823
  return h;
2459
2824
  });
@@ -2472,5 +2837,5 @@ function withHistory(source, opt) {
2472
2837
  * Generated bundle index. Do not edit.
2473
2838
  */
2474
2839
 
2475
- 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, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, scrollPosition, select, sensor, sensors, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
2840
+ export { batteryStatus, chunked, clipboard, combineWith, debounce, debounced, derived, distinct, elementSize, elementVisibility, filter, filterWith, focusWithin, geolocation, idle, indexArray, isDerivation, isMutable, isStore, keyArray, map, mapArray, mapObject, mediaQuery, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, orientation, pageVisibility, pairwise, pipeable, piped, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
2476
2841
  //# sourceMappingURL=mmstack-primitives.mjs.map