sia-reactor 0.0.20 → 0.0.21

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/README.md CHANGED
@@ -143,8 +143,8 @@ getReactor(state); state.__Reactor__; // Reference to the underlying reactor
143
143
 
144
144
  Alternatively, you can instantiate the `Reactor` class directly to keep the API from interfering with your data or [try this](#reactive-preferences-method-naming):
145
145
  ```javascript
146
- const reactor = new Reactor({ player: { volume: 50 } }, { debug: true, referenceTracking: true });
147
- reactor.core.player.volume = 100;
146
+ const reactor = new Reactor({ player: { volume: 50 } }, { debug: true });
147
+ reactor.core.player.volume = 100; // re-assign core if desired
148
148
  ```
149
149
 
150
150
  ### Core Methods
@@ -197,13 +197,13 @@ The engine provides native React bindings utilizing `useSyncExternalStore` and a
197
197
 
198
198
  ```javascript
199
199
  import { reactive } from 'sia-reactor';
200
- import { useReactor, useSelector, usePath, effect } from 'sia-reactor/react';
200
+ import { useReactor, useAnyReactor, useSelector, useAnySelector, usePath, effect } from 'sia-reactor/react';
201
201
 
202
202
  const state = reactive({ user: { name: "Ada", age: 25 }, theme: "dark" });
203
203
 
204
204
  // 1. The Tracked State (Valtio-style)
205
205
  function Profile() {
206
- const sameState = useReactor(state); // pass in a normal object for an auto-scoped instance
206
+ const sameState = useReactor(state); // `useReactorSnapshot()` if mutable issues arise
207
207
  useAnyReactor(); // use this when you just want to use any state from any reactor
208
208
  // Only re-renders if state.user.name mutates. Completely ignores age and theme!
209
209
  return <div>{sameState.user.name + otherState.user.name}</div>;
@@ -211,7 +211,7 @@ function Profile() {
211
211
 
212
212
  // 2. The Slice Selector (Zustand-style)
213
213
  function Theme() {
214
- const theme = useSelector(state, (s) => s.theme); // pass in a normal object for an auto-scoped instance
214
+ const theme = useSelector(state, (s) => s.theme); // `useSelectorSnapshot()` if mutable issues arise
215
215
  const newName = useAnySelector(() => state.user.name + spouseState.user.name); // use this when you just want to derive any state from any reactor
216
216
  return <div>Theme: {theme}</div>;
217
217
  }
@@ -223,9 +223,7 @@ function AgeObserver() {
223
223
  }
224
224
 
225
225
  // 4. Vanilla Side Effects (Runs anywhere, framework agnostic)
226
- const stopTracking = effect(() => {
227
- console.log("User name changed to:", state.user.name);
228
- });
226
+ const stopTracking = effect(() => console.log("User name changed to:", state.user.name)); // read or write as you wish
229
227
  ```
230
228
 
231
229
  ### Plugins: The Extension Port
@@ -1,5 +1,5 @@
1
- import { R as Reactive } from './index-DCG3sacH.cjs';
2
- import { T as TimeTravelPlugin } from './timeTravel-L8CEhHIo.cjs';
1
+ import { R as Reactive } from './index-Oie9hhE8.cjs';
2
+ import { T as TimeTravelPlugin } from './timeTravel-WpgWmKu-.cjs';
3
3
 
4
4
  /** Reactive options for the TimeTravel overlay instance. */
5
5
  interface TimeTravelConfig {
@@ -1,5 +1,5 @@
1
- import { R as Reactive } from './index-DCG3sacH.js';
2
- import { T as TimeTravelPlugin } from './timeTravel-Bv_u5M1D.js';
1
+ import { R as Reactive } from './index-Oie9hhE8.js';
2
+ import { T as TimeTravelPlugin } from './timeTravel-B1vedDQc.js';
3
3
 
4
4
  /** Reactive options for the TimeTravel overlay instance. */
5
5
  interface TimeTravelConfig {
@@ -26,7 +26,9 @@ __export(react_exports, {
26
26
  useISOLayoutEffect: () => useISOLayoutEffect,
27
27
  usePath: () => usePath,
28
28
  useReactor: () => useReactor,
29
- useSelector: () => useSelector
29
+ useReactorSnapshot: () => useReactorSnapshot,
30
+ useSelector: () => useSelector,
31
+ useSelectorSnapshot: () => useSelectorSnapshot
30
32
  });
31
33
  module.exports = __toCommonJS(react_exports);
32
34
 
@@ -326,27 +328,27 @@ var Reactor = class {
326
328
  listeners;
327
329
  /**
328
330
  * Creates a new Reactor instance.
329
- * @param object Initial state object.
331
+ * @param target Initial state target.
330
332
  * @param build Reactor bootstrap/build configuration.
331
333
  * @example
332
334
  * const rtr = new Reactor({ count: 0 });
333
335
  */
334
- constructor(object = {}, build) {
336
+ constructor(target = {}, build) {
335
337
  this[INERTIA] = true;
336
338
  this.config = { crossRealms: false, smartCloning: false, eventBubbling: true, lineageTracing: false, preserveContext: false, equalityFunction: Object.is, batchingFunction: RTR_BATCH, ...build };
337
- this.core = this.proxied(object);
339
+ this.core = this.proxied(target);
338
340
  if (build) this.canLog = !!build.debug;
339
341
  }
340
- proxied(obj, rejectable = false, indiffable = false, parent, key, path) {
341
- if (!obj || "object" !== typeof obj) return obj;
342
- obj = obj[RAW] || obj;
343
- if (this.config.referenceTracking && parent && key && !this.link(obj, parent, key, false)) return obj;
344
- const cached = this.proxyCache.get(obj);
342
+ proxied(target, rejectable = false, indiffable = false, parent, key, path) {
343
+ if (!target || "object" !== typeof target) return target;
344
+ target = target[RAW] || target;
345
+ if (this.config.referenceTracking && parent && key && !this.link(target, parent, key, false)) return target;
346
+ const cached = this.proxyCache.get(target);
345
347
  if (cached) return cached;
346
- if (obj[INERTIA] || !canHandle(obj, this.config, false)) return obj;
347
- rejectable ||= obj[REJECTABLE];
348
- indiffable ||= obj[INDIFFABLE];
349
- const proxy = new Proxy(obj, {
348
+ if (target[INERTIA] || !canHandle(target, this.config, false)) return target;
349
+ rejectable ||= target[REJECTABLE];
350
+ indiffable ||= target[INDIFFABLE];
351
+ const proxy = new Proxy(target, {
350
352
  // Robust Proxy handler
351
353
  get: (object, key2, receiver) => {
352
354
  if (key2 === RAW) return this.log(`\u{1F440} [Reactor \`get\` Trap] Peeked at ${object}`), object;
@@ -359,10 +361,10 @@ var Reactor = class {
359
361
  for (let i = 0, len = this.config.lineageTracing ? paths.length : 1; i < len; i++) {
360
362
  const currPath = this.config.lineageTracing ? paths[i] : fullPath, cords = this.getters.get(currPath);
361
363
  if (!cords && !wildcords) continue;
362
- const target = { path: currPath, value, key: keyStr, hadKey: true, object: receiver }, payload = { type: "get", target, currentTarget: target, root: this.core, rejectable };
364
+ const target2 = { path: currPath, value, key: keyStr, hadKey: true, object: receiver }, payload = { type: "get", target: target2, currentTarget: target2, root: this.core, rejectable };
363
365
  if (cords) value = this.mediate(currPath, payload, "get", cords);
364
366
  if (!wildcords) continue;
365
- target.value = value;
367
+ target2.value = value;
366
368
  value = this.mediate("*", payload, "get", wildcords);
367
369
  }
368
370
  }
@@ -384,13 +386,13 @@ var Reactor = class {
384
386
  for (let i = 0; i < loopLen; i++) {
385
387
  const currPath = this.config.lineageTracing ? paths[i] : fullPath, cords = this.setters.get(currPath);
386
388
  if (!cords && !wildcords) continue;
387
- const target = { path: currPath, value, oldValue, key: keyStr, hadKey, object: receiver }, payload = { type: "set", target, currentTarget: target, root: this.core, terminated, rejectable };
389
+ const target2 = { path: currPath, value, oldValue, key: keyStr, hadKey, object: receiver }, payload = { type: "set", target: target2, currentTarget: target2, root: this.core, terminated, rejectable };
388
390
  if (cords) {
389
391
  const result2 = this.mediate(currPath, payload, "set", cords);
390
392
  if (!(terminated ||= payload.terminated)) value = result2;
391
393
  }
392
394
  if (!wildcords) continue;
393
- target.value = value;
395
+ target2.value = value;
394
396
  const result = this.mediate("*", payload, "set", wildcords);
395
397
  if (!(terminated ||= payload.terminated)) value = result;
396
398
  }
@@ -401,8 +403,8 @@ var Reactor = class {
401
403
  if (this.config.referenceTracking && !unchanged) this.config.smartCloning && this.stamp(object), this.unlink(safeOldValue, object, keyStr), this.link(safeValue, object, keyStr);
402
404
  if (this.watchers || this.listeners)
403
405
  for (let i = 0; i < loopLen; i++) {
404
- const currPath = this.config.lineageTracing ? paths[i] : fullPath, target = { path: currPath, value, oldValue, key: keyStr, hadKey, object: receiver };
405
- this.notify(currPath, { type: "set", target, currentTarget: target, root: this.core, terminated, rejectable });
406
+ const currPath = this.config.lineageTracing ? paths[i] : fullPath, target2 = { path: currPath, value, oldValue, key: keyStr, hadKey, object: receiver };
407
+ this.notify(currPath, { type: "set", target: target2, currentTarget: target2, root: this.core, terminated, rejectable });
406
408
  }
407
409
  return true;
408
410
  },
@@ -416,7 +418,7 @@ var Reactor = class {
416
418
  for (let i = 0; i < loopLen; i++) {
417
419
  const currPath = this.config.lineageTracing ? paths[i] : fullPath, cords = this.deleters.get(currPath);
418
420
  if (!cords && !wildcords) continue;
419
- const target = { path: currPath, value, oldValue, key: keyStr, hadKey, object: receiver }, payload = { type: "delete", target, currentTarget: target, root: this.core, rejectable };
421
+ const target2 = { path: currPath, value, oldValue, key: keyStr, hadKey, object: receiver }, payload = { type: "delete", target: target2, currentTarget: target2, root: this.core, rejectable };
420
422
  if (cords) {
421
423
  const result2 = this.mediate(currPath, payload, "delete", cords);
422
424
  if (!(terminated ||= payload.terminated)) value = result2;
@@ -432,8 +434,8 @@ var Reactor = class {
432
434
  if (this.config.referenceTracking) this.config.smartCloning && this.stamp(object), this.unlink(oldValue?.[RAW] || oldValue, object, keyStr);
433
435
  if (this.watchers || this.listeners)
434
436
  for (let i = 0; i < loopLen; i++) {
435
- const currPath = this.config.lineageTracing ? paths[i] : fullPath, target = { path: currPath, value, oldValue, key: keyStr, hadKey, object: receiver };
436
- this.notify(currPath, { type: "delete", target, currentTarget: target, root: this.core, rejectable });
437
+ const currPath = this.config.lineageTracing ? paths[i] : fullPath, target2 = { path: currPath, value, oldValue, key: keyStr, hadKey, object: receiver };
438
+ this.notify(currPath, { type: "delete", target: target2, currentTarget: target2, root: this.core, rejectable });
437
439
  }
438
440
  return true;
439
441
  },
@@ -459,7 +461,7 @@ var Reactor = class {
459
461
  return ownKeys;
460
462
  }
461
463
  });
462
- return this.proxyCache.set(obj, proxy), proxy;
464
+ return this.proxyCache.set(target, proxy), proxy;
463
465
  }
464
466
  trace(target, path, paths = [], seen = /* @__PURE__ */ new WeakSet()) {
465
467
  if (Object.is(target, this.core[RAW] || this.core)) return paths.push(path), paths;
@@ -612,15 +614,14 @@ var Reactor = class {
612
614
  if (sig) sig.aborted ? cord.clup() : sig.addEventListener("abort", cord.clup, { once: true });
613
615
  return cord.sclup = !sig || sig.aborted ? NOOP : () => sig.removeEventListener("abort", cord.clup), cord.clup;
614
616
  }
615
- cloned(obj, raw, seen = /* @__PURE__ */ new WeakMap()) {
616
- if (!obj || "object" !== typeof obj) return obj;
617
- obj = obj[RAW] || obj;
618
- const cloned = seen.get(obj);
617
+ cloned(target, raw, seen = /* @__PURE__ */ new WeakMap()) {
618
+ if (!target || "object" !== typeof target) return target;
619
+ const obj = target[RAW] || target, cloned = seen.get(obj);
619
620
  if (cloned) return cloned;
620
621
  if (!canHandle(obj, this.config, false)) return obj;
621
622
  const version = obj[VERSION] || 0, cached = !raw && this.config.smartCloning && (this.snapCache ??= /* @__PURE__ */ new WeakMap()).get(obj);
622
623
  if (cached && obj[SSVERSION] === version) return cached;
623
- const clone = !raw ? this.config.preserveContext ? Object.create(Object.getPrototypeOf(obj)) : Array.isArray(obj) ? [] : {} : obj[RAW] || obj;
624
+ const clone = !raw ? this.config.preserveContext ? Object.create(Object.getPrototypeOf(obj)) : Array.isArray(obj) ? [] : {} : obj;
624
625
  seen.set(obj, clone);
625
626
  const keys2 = this.config.preserveContext ? Reflect.ownKeys(obj) : Object.keys(obj);
626
627
  for (let i = 0, len = keys2.length; i < len; i++) clone[keys2[i]] = this.cloned(obj[keys2[i]], raw, seen);
@@ -804,19 +805,8 @@ var Reactor = class {
804
805
  for (let i = 0, len = cords.length; i < len; i++) if (Object.is(cords[i].cb, callback) && cords[i].capture === capture) return cords[i].sclup(), cords.splice((len--, i--), 1), !cords.length && this.listeners.delete(path), true;
805
806
  return false;
806
807
  }
807
- /**
808
- * Creates a snapshot; possibly clone of state (or a state branch).
809
- * You could alternatively use or serialize your proxied state "as is" except the environment demands no proxies or new references.
810
- * @param raw Use raw (deep unproxied & uncloned) version of branch.
811
- * @param branch Branch to clone.
812
- * @returns Snapshot deep or smart (structurally shared) clone.
813
- * @example
814
- * const snap = rtr.snapshot();
815
- * @example
816
- * const snap = rtr.snapshot(false, rtr.core.history.past);
817
- */
818
- snapshot(raw = !this.config.smartCloning, branch = this.core) {
819
- return this.cloned(branch, raw);
808
+ snapshot(raw = !this.config.smartCloning, branch) {
809
+ return this.cloned(arguments.length < 2 ? this.core : branch, raw);
820
810
  }
821
811
  /**
822
812
  * Cascades object updates into direct child paths.
@@ -1032,7 +1022,7 @@ function useReactor(target, options = NIL, build) {
1032
1022
  const versionRef = (0, import_react2.useRef)(0), tgtRef = (0, import_react2.useRef)(), rtrRef = (0, import_react2.useRef)(), rtr = tgtRef.current !== target || !rtrRef.current ? (tgtRef.current = target, rtrRef.current = getReactor(target, true, build)) : rtrRef.current, atrkrRef = (0, import_react2.useRef)(), prevTrkr = CTX.autotracker, atrkr = CTX.autotracker = atrkrRef.current ||= new Autotracker(), optsRef = (0, import_react2.useRef)(options), notifyRef = (0, import_react2.useRef)(NOOP);
1033
1023
  optsRef.current = options;
1034
1024
  atrkr.unblock(rtr), queueMicrotask(() => CTX.autotracker === atrkr && (CTX.autotracker = prevTrkr));
1035
- const subscribe = (0, import_react2.useCallback)((notify) => atrkr.callback(notifyRef.current = () => (versionRef.current++, notify())), []);
1025
+ const subscribe = (0, import_react2.useCallback)((notify) => atrkr.callback(notifyRef.current = () => (versionRef.current++, notify())), [atrkr]);
1036
1026
  const getSnapshot = (0, import_react2.useCallback)(() => versionRef.current, []);
1037
1027
  (0, import_react2.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
1038
1028
  return useISOLayoutEffect(() => (CTX.autotracker = prevTrkr, atrkr.callback(notifyRef.current, optsRef.current)), [atrkr]), rtr.core;
@@ -1041,11 +1031,21 @@ function useAnyReactor(options = NIL) {
1041
1031
  const versionRef = (0, import_react2.useRef)(0), atrkrRef = (0, import_react2.useRef)(), prevTrkr = CTX.autotracker, atrkr = CTX.autotracker = atrkrRef.current ||= new Autotracker(), optsRef = (0, import_react2.useRef)(options), notifyRef = (0, import_react2.useRef)(NOOP);
1042
1032
  optsRef.current = options;
1043
1033
  atrkr.unblock(), queueMicrotask(() => CTX.autotracker === atrkr && (CTX.autotracker = prevTrkr));
1044
- const subscribe = (0, import_react2.useCallback)((notify) => atrkr.callback(notifyRef.current = () => (versionRef.current++, notify())), []);
1034
+ const subscribe = (0, import_react2.useCallback)((notify) => atrkr.callback(notifyRef.current = () => (versionRef.current++, notify())), [atrkr]);
1045
1035
  const getSnapshot = (0, import_react2.useCallback)(() => versionRef.current, []);
1046
1036
  (0, import_react2.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
1047
1037
  useISOLayoutEffect(() => (CTX.autotracker = prevTrkr, atrkr.callback(notifyRef.current, optsRef.current)), [atrkr]);
1048
1038
  }
1039
+ function useReactorSnapshot(target, options, build = { referenceTracking: true, smartCloning: true }) {
1040
+ const tgtRef = (0, import_react2.useRef)(), rtrRef = (0, import_react2.useRef)(), rtr = tgtRef.current !== target || !rtrRef.current ? (tgtRef.current = target, rtrRef.current = getReactor(target, true, build)) : rtrRef.current, atrkrRef = (0, import_react2.useRef)(), atrkrRtrRef = (0, import_react2.useRef)(), atrkr = atrkrRtrRef.current !== rtr || !atrkrRef.current ? (atrkrRtrRef.current = rtr, atrkrRef.current = new Autotracker(rtr)) : atrkrRef.current, notifyRef = (0, import_react2.useRef)(NOOP), optsRef = (0, import_react2.useRef)(options);
1041
+ rtr.config.referenceTracking = rtr.config.smartCloning = true;
1042
+ optsRef.current = options;
1043
+ const subscribe = (0, import_react2.useCallback)((notify) => (atrkr.callback(notifyRef.current = notify, optsRef.current), () => atrkr.cleanup()), [atrkr]);
1044
+ const getSnapshot = () => rtr.snapshot();
1045
+ const snapshot = (0, import_react2.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
1046
+ const proxy = (0, import_react2.useMemo)(() => atrkr.tracked(snapshot), [atrkr, snapshot]);
1047
+ return useISOLayoutEffect(() => atrkr.callback(notifyRef.current, optsRef.current), [atrkr, proxy]), proxy;
1048
+ }
1049
1049
 
1050
1050
  // src/ts/adapters/react/hooks/useSelector.ts
1051
1051
  var import_react3 = require("react");
@@ -1071,6 +1071,18 @@ function useAnySelector(sel, eq = Object.is, options = NIL) {
1071
1071
  const slice = (0, import_react3.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
1072
1072
  return useISOLayoutEffect(() => atrkr.callback(notifyRef.current, optsRef.current), [atrkr, slice]), slice;
1073
1073
  }
1074
+ function useSelectorSnapshot(target, sel, eq = Object.is, options, build = { referenceTracking: true, smartCloning: true }) {
1075
+ const tgtRef = (0, import_react3.useRef)(), rtrRef = (0, import_react3.useRef)(), rtr = tgtRef.current !== target || !rtrRef.current ? (tgtRef.current = target, rtrRef.current = getReactor(target, true, build)) : rtrRef.current, atrkrRef = (0, import_react3.useRef)(), atrkrRtrRef = (0, import_react3.useRef)(), atrkr = atrkrRtrRef.current !== rtr || !atrkrRef.current ? (atrkrRtrRef.current = rtr, atrkrRef.current = new Autotracker(rtr)) : atrkrRef.current, notifyRef = (0, import_react3.useRef)(NOOP), sliceRef = (0, import_react3.useRef)(), selRef = (0, import_react3.useRef)(sel), eqRef = (0, import_react3.useRef)(eq), optsRef = (0, import_react3.useRef)(options);
1076
+ rtr.config.referenceTracking = rtr.config.smartCloning = true;
1077
+ optsRef.current = options, selRef.current = sel, eqRef.current = eq;
1078
+ const subscribe = (0, import_react3.useCallback)((notify) => (atrkr.callback(notifyRef.current = notify, optsRef.current), () => atrkr.cleanup()), [atrkr]);
1079
+ const getSnapshot = (0, import_react3.useCallback)(() => {
1080
+ const next = selRef.current(atrkr.tracked(rtr.snapshot()));
1081
+ return eqRef.current(sliceRef.current, next) ? sliceRef.current : sliceRef.current = next;
1082
+ }, [atrkr]);
1083
+ const slice = (0, import_react3.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
1084
+ return useISOLayoutEffect(() => atrkr.callback(notifyRef.current, optsRef.current), [atrkr, slice]), slice;
1085
+ }
1074
1086
 
1075
1087
  // src/ts/adapters/react/hooks/usePath.ts
1076
1088
  var import_react4 = require("react");
@@ -1187,41 +1199,16 @@ var TimeTravelOverlay = class _TimeTravelOverlay {
1187
1199
  */
1188
1200
  constructor(time, build = {}) {
1189
1201
  this.time = time;
1190
- this.config = reactive({ container: document.body, color: "", startOpen: false, devOnly: true, title: `Time Travel Overlay ${this.index = ++_TimeTravelOverlay.count}`, ...build });
1202
+ this.config = reactive({ title: `Time Travel Overlay ${this.index = ++_TimeTravelOverlay.count}`, ...build });
1191
1203
  this.state.open = !!this.config.startOpen;
1192
- const host = createEl("div", { className: "tt-overlay-host" });
1193
- const toggle = createEl("button", { className: "tt-overlay-toggle", type: "button", onclick: () => this.state.open = !this.state.open });
1194
- const panel = createEl("aside", { className: "tt-overlay", ariaLabel: "time travel overlay" });
1195
- const title = createEl("div", { className: "title" });
1196
- const frame = createEl("span", { className: "muted" });
1197
- const history = createEl("span", { className: "muted" });
1198
- const paused = createEl("span", { className: "muted" });
1199
- const clrHistory = createEl("button", { textContent: `Clear History${formatKeyForDisplay(keys.shortcuts.clrHistory)}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.clrHistory, false), onclick: () => (this.time.clear(), this.state.import = "") });
1200
- const undo = createEl("button", { textContent: `Undo${formatKeyForDisplay(keys.shortcuts.undo[0])}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.undo, false), onclick: this.time.undo });
1201
- const redo = createEl("button", { textContent: `Redo${formatKeyForDisplay(keys.shortcuts.redo[0])}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.redo, false), onclick: this.time.redo });
1202
- const genesis = createEl("button", { textContent: `Genesis${formatKeyForDisplay(keys.shortcuts.genesis[0])}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.genesis, false), onclick: () => this.time.jumpTo(0) });
1203
- const playPause = createEl("button", { onclick: () => this.time[this.time.state.paused ? "play" : "pause"](), ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.playPause, false) });
1204
- const rewind = createEl("button", { textContent: `Rewind${formatKeyForDisplay(keys.shortcuts.rewind)}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.rewind, false), onclick: this.time.rewind });
1205
- const range = createEl("input", { type: "range", min: "0", max: "0", value: "0", title: "time travel frame", ariaLabel: "time travel frame", oninput: () => this.time.jumpTo(Number(range.value)) });
1206
- const exp = createEl("button", { textContent: `Export${formatKeyForDisplay(keys.shortcuts.export)}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.export, false), onclick: () => this.state.import = this.time.export() });
1207
- const imp = createEl("button", { textContent: `Import${formatKeyForDisplay(keys.shortcuts.import)}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.import, false), onclick: () => this.state.import.trim().length && this.time.import(this.state.import) });
1208
- const clr = createEl("button", { textContent: `Clear${formatKeyForDisplay(keys.shortcuts.clear)}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.clear, false), onclick: () => this.state.import = "" });
1209
- const io = createEl("textarea", { className: "tt-io", placeholder: "timeline payload json", oninput: () => this.state.import = io.value });
1210
- const foot = createEl("p", { className: "tt-footnote", textContent: "Want this in your app? " });
1211
- const link = createEl("a", { target: "_blank", rel: "noreferrer noopener", textContent: "sia-reactor", href: "https://www.npmjs.com/package/sia-reactor" });
1212
- const box = createEl("div", { className: "tt-status-box" });
1213
- const status = createEl("div", { className: "tt-status-row" });
1214
- const row1 = createEl("div", { className: "tt-row" });
1215
- const row2 = createEl("div", { className: "tt-row" });
1216
- const row3 = createEl("div", { className: "tt-row" });
1217
- status.append((box.append(frame, history, paused), box), clrHistory);
1218
- panel.append(title, status, (row1.append(undo, redo, genesis), row1), (row2.append(playPause, rewind), row2), range, (row3.append(exp, imp, clr), row3), io, (foot.appendChild(link), foot));
1204
+ const s = this.time.state, host = createEl("div", { className: "tt-overlay-host" }), toggle = createEl("button", { className: "tt-overlay-toggle", type: "button", onclick: () => this.state.open = !this.state.open }), panel = createEl("aside", { className: "tt-overlay", ariaLabel: "time travel overlay" }), title = createEl("div", { className: "title" }), frame = createEl("span", { className: "muted" }), clrHistory = createEl("button", { textContent: `Clear History${formatKeyForDisplay(keys.shortcuts.clrHistory)}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.clrHistory, false), onclick: () => (this.time.clear(), this.state.import = "") }), undo = createEl("button", { textContent: `Undo${formatKeyForDisplay(keys.shortcuts.undo[0])}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.undo, false), onclick: this.time.undo }), redo = createEl("button", { textContent: `Redo${formatKeyForDisplay(keys.shortcuts.redo[0])}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.redo, false), onclick: this.time.redo }), genesis = createEl("button", { textContent: `Genesis${formatKeyForDisplay(keys.shortcuts.genesis[0])}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.genesis, false), onclick: () => this.time.jumpTo(0) }), playPause = createEl("button", { onclick: () => this.time[s.paused ? "play" : "pause"](), ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.playPause, false) }), rewind = createEl("button", { textContent: `Rewind${formatKeyForDisplay(keys.shortcuts.rewind)}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.rewind, false), onclick: this.time.rewind }), range = createEl("input", { type: "range", min: "0", max: "0", value: "0", title: "time travel frame", ariaLabel: "time travel frame", oninput: () => this.time.jumpTo(Number(range.value)) }), exp = createEl("button", { textContent: `Export${formatKeyForDisplay(keys.shortcuts.export)}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.export, false), onclick: () => this.state.import = this.time.export(null, 2) }), imp = createEl("button", { textContent: `Import${formatKeyForDisplay(keys.shortcuts.import)}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.import, false), onclick: () => this.state.import.trim().length && this.time.import(this.state.import) }), clr = createEl("button", { textContent: `Clear${formatKeyForDisplay(keys.shortcuts.clear)}`, ariaKeyShortcuts: parseForARIAKS(keys.shortcuts.clear, false), onclick: () => this.state.import = "" }), payload = createEl("textarea", { className: "tt-io", readOnly: true, placeholder: "current payload json", title: "current payload" }), io = createEl("textarea", { className: "tt-io", placeholder: "timeline payload json", oninput: () => this.state.import = io.value }), foot = createEl("p", { className: "tt-footnote", textContent: "Want this in your app? " }), link = createEl("a", { target: "_blank", rel: "noreferrer noopener", textContent: "sia-reactor", href: "https://www.npmjs.com/package/sia-reactor" }), box = createEl("div", { className: "tt-status-box" }), status = createEl("div", { className: "tt-status-row" }), row1 = createEl("div", { className: "tt-row" }), row2 = createEl("div", { className: "tt-row" }), row3 = createEl("div", { className: "tt-row" });
1205
+ status.append((box.append(frame), box), clrHistory);
1206
+ panel.append(title, status, (row1.append(undo, redo, genesis), row1), (row2.append(playPause, rewind), row2), payload, range, (row3.append(exp, imp, clr), row3), io, (foot.appendChild(link), foot));
1219
1207
  host.append(toggle, panel);
1220
- this.els = { host, toggle, panel, title, frame, history, paused, clrHistory, undo, redo, genesis, playPause, rewind, range, exp, imp, clr, io };
1208
+ this.els = { host, toggle, panel, title, frame, clrHistory, undo, redo, genesis, playPause, rewind, range, exp, imp, clr, payload, io };
1221
1209
  this.keyup = (e) => {
1222
- if (!this.state.open) return;
1223
- const a = keyEventAllowed(e, keys);
1224
- a === "undo" ? this.time.undo() : a === "redo" ? this.time.redo() : a === "genesis" ? this.time.jumpTo(0) : a === "prevFrame" ? this.time.step(1, false) : a === "nextFrame" ? this.time.step(1, true) : a === "skipBwd" ? this.time.step(5, false) : a === "skipFwd" ? this.time.step(5, true) : a === "rewind" ? this.time.rewind() : a === "playPause" ? this.time[this.time.state.paused ? "play" : "pause"]() : a === "clrHistory" ? this.time.clear() : a === "closeOverlay" ? this.state.open = false : a === "export" ? this.state.import = this.time.export() : a === "import" ? this.state.import.trim().length && this.time.import(this.state.import) : a === "clear" && (this.state.import = "");
1210
+ const a = this.state.open && keyEventAllowed(e, keys);
1211
+ a === "undo" ? this.time.undo() : a === "redo" ? this.time.redo() : a === "genesis" ? this.time.jumpTo(0) : a === "prevFrame" ? this.time.step(1, false) : a === "nextFrame" ? this.time.step(1, true) : a === "skipBwd" ? this.time.step(5, false) : a === "skipFwd" ? this.time.step(5, true) : a === "rewind" ? this.time.rewind() : a === "playPause" ? this.time[s.paused ? "play" : "pause"]() : a === "clrHistory" ? this.time.clear() : a === "closeOverlay" ? this.state.open = false : a === "export" ? this.state.import = this.time.export() : a === "import" ? this.state.import.trim().length && this.time.import(this.state.import) : a === "clear" && (this.state.import = "");
1225
1212
  };
1226
1213
  window.addEventListener("keydown", this.keyup);
1227
1214
  const sync = [
@@ -1231,26 +1218,19 @@ var TimeTravelOverlay = class _TimeTravelOverlay {
1231
1218
  const dock = getDock(this.config.container);
1232
1219
  if (host.parentNode !== dock) dock.appendChild(host);
1233
1220
  }),
1234
- effect(() => (toggle.textContent = `${this.state.open ? "Hide" : "Show"} ${this.config.title ?? ""}`, panel.hidden = !this.state.open)),
1221
+ effect(() => toggle.textContent = `${(panel.hidden = !this.state.open) ? "Show" : "Hide"} ${title.textContent = this.config.title ?? ""}`),
1222
+ effect(() => playPause.textContent = `${s.paused ? "Play" : "Pause"}${formatKeyForDisplay(keys.shortcuts.playPause)}`),
1235
1223
  effect(() => {
1236
- title.textContent = this.config.title ?? "";
1237
- frame.textContent = `Frame: ${this.time.state.currentFrame} / ${this.time.state.history.length}`;
1238
- history.textContent = `History: ${this.time.state.history.length}`;
1239
- paused.textContent = `Paused: ${this.time.state.paused ? "Yes" : "No"}`;
1240
- playPause.textContent = `${this.time.state.paused ? "Play" : "Pause"}${formatKeyForDisplay(keys.shortcuts.playPause)}`;
1224
+ frame.textContent = `Frame: ${s.currentFrame} / ${s.history.length}`;
1225
+ range.disabled = clrHistory.disabled = !s.history.length;
1226
+ genesis.disabled = rewind.disabled = undo.disabled = !s.currentFrame;
1227
+ playPause.disabled = redo.disabled = s.currentFrame >= s.history.length;
1228
+ range.max = String(s.history.length);
1229
+ range.value = String(Math.min(s.currentFrame, s.history.length));
1230
+ payload.value = JSON.stringify(s.currentFrame ? s.history[s.currentFrame - 1] : { type: "genesis", value: s.initialState }, null, 2);
1241
1231
  }),
1242
1232
  effect(() => {
1243
- clrHistory.disabled = !this.time.state.history.length;
1244
- undo.disabled = this.time.state.currentFrame <= 0;
1245
- redo.disabled = this.time.state.currentFrame >= this.time.state.history.length;
1246
- genesis.disabled = this.time.state.currentFrame <= 0;
1247
- playPause.disabled = this.time.state.currentFrame === this.time.state.history.length;
1248
- rewind.disabled = !this.time.state.currentFrame;
1249
- range.max = String(this.time.state.history.length);
1250
- range.value = String(Math.min(this.time.state.currentFrame, this.time.state.history.length));
1251
- range.disabled = !this.time.state.history.length;
1252
- imp.disabled = !this.state.import.trim().length;
1253
- clr.disabled = !this.state.import.trim().length;
1233
+ clr.disabled = imp.disabled = !this.state.import.trim().length;
1254
1234
  io.value !== this.state.import && (io.value = this.state.import);
1255
1235
  })
1256
1236
  ];
@@ -1277,12 +1257,12 @@ function getDock(container) {
1277
1257
 
1278
1258
  // src/ts/adapters/react/TimeTravelOverlay.tsx
1279
1259
  function TimeTravelOverlay2(props) {
1280
- const vRef = (0, import_react5.useRef)(null), { time, title, color = "", devOnly = true, startOpen = false, container = document.body } = props;
1281
- (0, import_react5.useEffect)(() => {
1260
+ const vRef = (0, import_react5.useRef)(null), { time, title, color, devOnly, startOpen, container } = props;
1261
+ useISOLayoutEffect(() => {
1282
1262
  vRef.current = new TimeTravelOverlay(time, props);
1283
1263
  return () => void (vRef.current?.destroy(), vRef.current = null);
1284
1264
  }, [time]);
1285
- useISOLayoutEffect(() => void (vRef.current && (title !== void 0 && (vRef.current.config.title = title), vRef.current.config.color = color, vRef.current.config.devOnly = devOnly, vRef.current.config.container = container ?? document.body, vRef.current.state.open = startOpen)), [title, color, devOnly, container, startOpen]);
1265
+ useISOLayoutEffect(() => void (vRef.current && (title !== void 0 && (vRef.current.config.title = title), vRef.current.config.color = color, vRef.current.config.devOnly = devOnly, vRef.current.config.container = container, vRef.current.state.open = startOpen)), [title, color, devOnly, container, startOpen]);
1286
1266
  return null;
1287
1267
  }
1288
1268
  // Annotate the CommonJS export names for ESM import in node:
@@ -1293,5 +1273,7 @@ function TimeTravelOverlay2(props) {
1293
1273
  useISOLayoutEffect,
1294
1274
  usePath,
1295
1275
  useReactor,
1296
- useSelector
1276
+ useReactorSnapshot,
1277
+ useSelector,
1278
+ useSelectorSnapshot
1297
1279
  });
@@ -1,7 +1,7 @@
1
- import { E as EffectOptions, b as Reactor, R as Reactive, o as ReactorBuild, W as WildPaths, q as PathValue } from '../index-DCG3sacH.cjs';
1
+ import { E as EffectOptions, b as Reactor, R as Reactive, o as ReactorBuild, W as WildPaths, q as PathValue } from '../index-Oie9hhE8.cjs';
2
2
  import { useLayoutEffect } from 'react';
3
- import { T as TimeTravelPlugin } from '../timeTravel-L8CEhHIo.cjs';
4
- import { T as TimeTravelConfig } from '../TimeTravelOverlay-BYSnHBXx.cjs';
3
+ import { T as TimeTravelPlugin } from '../timeTravel-WpgWmKu-.cjs';
4
+ import { T as TimeTravelConfig } from '../TimeTravelOverlay-CJv-S_Km.cjs';
5
5
 
6
6
  /**
7
7
  * Subscribes a component to desired Reactor state and returns it.
@@ -12,7 +12,7 @@ import { T as TimeTravelConfig } from '../TimeTravelOverlay-BYSnHBXx.cjs';
12
12
  * @param build Optional Reactor build options used when creating a scoped Reactor for plain objects.
13
13
  * @returns State for render usage if state is scoped locally or just desired.
14
14
  * @example
15
- * const a = useReactor({ user: { name: "Ada" } });
15
+ * const a = useReactor({ user: { name: "Ada" } }); // per-component scoped
16
16
  * @example
17
17
  * const state = reactive({ user: { name: "Ada" } });
18
18
  * const b = useReactor(state);
@@ -29,6 +29,25 @@ declare function useReactor<T extends object>(target: T | Reactor<T> | Reactive<
29
29
  * useAnyReactor();
30
30
  */
31
31
  declare function useAnyReactor(options?: EffectOptions): void;
32
+ /**
33
+ * Subscribes a component to Reactor state and returns a tracked snapshot.
34
+ * Rule of thumb: read from snapshots, mutate the source.
35
+ * The hook uses access tracking so re-renders occur only when accessed fields change.
36
+ * @typeParam T Root state object type.
37
+ * @param target Reactive object, Reactor instance, or plain object.
38
+ * @param options Watcher options if `options.sync: false` else Listener options.
39
+ * @param build Optional Reactor build options used when creating a scoped Reactor for plain objects.
40
+ * @returns Tracked snapshot snap for render usage.
41
+ * @example
42
+ * const a = useReactorSnapshot({ user: { name: "Ada" } }); // per-component scoped
43
+ * @example
44
+ * const state = reactive({ user: { name: "Ada" } });
45
+ * const b = useReactorSnapshot(state);
46
+ * @example
47
+ * const rtr = new Reactor({ user: { name: "Ada" } });
48
+ * const c = useReactorSnapshot(rtr);
49
+ */
50
+ declare function useReactorSnapshot<T extends object>(target: T | Reactor<T> | Reactive<T>, options?: EffectOptions, build?: ReactorBuild<T>): T;
32
51
 
33
52
  /**
34
53
  * Subscribes to a derived slice of Reactor state.
@@ -43,7 +62,7 @@ declare function useAnyReactor(options?: EffectOptions): void;
43
62
  * @param build Optional Reactor build options used when creating a scoped Reactor for plain objects.
44
63
  * @returns The selected slice.
45
64
  * @example
46
- * const a = useSelector({ user: { name: "Ada" } }, (s) => s.user.name);
65
+ * const a = useSelector({ user: { name: "Ada" } }, (s) => s.user.name); // per-component scoped
47
66
  * @example
48
67
  * const state = reactive({ user: { name: "Ada" } });
49
68
  * const b = useSelector(state, (s) => s.user.name);
@@ -63,6 +82,28 @@ declare function useSelector<T extends object, R>(target: T | Reactor<T> | React
63
82
  * @returns The selected slice.
64
83
  */
65
84
  declare function useAnySelector<R>(sel: () => R, eq?: (value1: any, value2: any) => boolean, options?: EffectOptions): R;
85
+ /**
86
+ * Subscribes to a derived slice of Reactor state.
87
+ * The selector runs against a tracked snapshot and uses the provided equality function
88
+ * to suppress unchanged results.
89
+ * @typeParam T Root state object type.
90
+ * @typeParam R Selector return type.
91
+ * @param target Reactive object, Reactor instance, or plain object.
92
+ * @param sel Slice selector.
93
+ * @param eq Equality function used to compare consecutive selector results.
94
+ * @param options Watcher options if `options.sync: false` else Listener options.
95
+ * @param build Optional Reactor build options used when creating a scoped Reactor for plain objects.
96
+ * @returns The selected slice.
97
+ * @example
98
+ * const a = useSelectorSnapshot({ user: { name: "Ada" } }, (s) => s.user.name); // per-component scoped
99
+ * @example
100
+ * const state = reactive({ user: { name: "Ada" } });
101
+ * const b = useSelectorSnapshot(state, (s) => s.user.name);
102
+ * @example
103
+ * const rtr = new Reactor({ user: { name: "Ada" } });
104
+ * const c = useSelectorSnapshot(rtr, (s) => s.user.name);
105
+ */
106
+ declare function useSelectorSnapshot<T extends object, R>(target: T | Reactor<T> | Reactive<T>, sel: (state: T) => R, eq?: (value1: any, value2: any) => boolean, options?: EffectOptions, build?: ReactorBuild<T>): R;
66
107
 
67
108
  /**
68
109
  * Subscribes to a single path in Reactor state.
@@ -102,4 +143,4 @@ interface TimeTravelOverlayProps extends Partial<TimeTravelConfig> {
102
143
  */
103
144
  declare function TimeTravelOverlay(props: TimeTravelOverlayProps): null;
104
145
 
105
- export { TimeTravelOverlay, type TimeTravelOverlayProps, useAnyReactor, useAnySelector, useISOLayoutEffect, usePath, useReactor, useSelector };
146
+ export { TimeTravelOverlay, type TimeTravelOverlayProps, useAnyReactor, useAnySelector, useISOLayoutEffect, usePath, useReactor, useReactorSnapshot, useSelector, useSelectorSnapshot };
@@ -1,7 +1,7 @@
1
- import { E as EffectOptions, b as Reactor, R as Reactive, o as ReactorBuild, W as WildPaths, q as PathValue } from '../index-DCG3sacH.js';
1
+ import { E as EffectOptions, b as Reactor, R as Reactive, o as ReactorBuild, W as WildPaths, q as PathValue } from '../index-Oie9hhE8.js';
2
2
  import { useLayoutEffect } from 'react';
3
- import { T as TimeTravelPlugin } from '../timeTravel-Bv_u5M1D.js';
4
- import { T as TimeTravelConfig } from '../TimeTravelOverlay-DoNrZwvX.js';
3
+ import { T as TimeTravelPlugin } from '../timeTravel-B1vedDQc.js';
4
+ import { T as TimeTravelConfig } from '../TimeTravelOverlay-DxqJL0Zk.js';
5
5
 
6
6
  /**
7
7
  * Subscribes a component to desired Reactor state and returns it.
@@ -12,7 +12,7 @@ import { T as TimeTravelConfig } from '../TimeTravelOverlay-DoNrZwvX.js';
12
12
  * @param build Optional Reactor build options used when creating a scoped Reactor for plain objects.
13
13
  * @returns State for render usage if state is scoped locally or just desired.
14
14
  * @example
15
- * const a = useReactor({ user: { name: "Ada" } });
15
+ * const a = useReactor({ user: { name: "Ada" } }); // per-component scoped
16
16
  * @example
17
17
  * const state = reactive({ user: { name: "Ada" } });
18
18
  * const b = useReactor(state);
@@ -29,6 +29,25 @@ declare function useReactor<T extends object>(target: T | Reactor<T> | Reactive<
29
29
  * useAnyReactor();
30
30
  */
31
31
  declare function useAnyReactor(options?: EffectOptions): void;
32
+ /**
33
+ * Subscribes a component to Reactor state and returns a tracked snapshot.
34
+ * Rule of thumb: read from snapshots, mutate the source.
35
+ * The hook uses access tracking so re-renders occur only when accessed fields change.
36
+ * @typeParam T Root state object type.
37
+ * @param target Reactive object, Reactor instance, or plain object.
38
+ * @param options Watcher options if `options.sync: false` else Listener options.
39
+ * @param build Optional Reactor build options used when creating a scoped Reactor for plain objects.
40
+ * @returns Tracked snapshot snap for render usage.
41
+ * @example
42
+ * const a = useReactorSnapshot({ user: { name: "Ada" } }); // per-component scoped
43
+ * @example
44
+ * const state = reactive({ user: { name: "Ada" } });
45
+ * const b = useReactorSnapshot(state);
46
+ * @example
47
+ * const rtr = new Reactor({ user: { name: "Ada" } });
48
+ * const c = useReactorSnapshot(rtr);
49
+ */
50
+ declare function useReactorSnapshot<T extends object>(target: T | Reactor<T> | Reactive<T>, options?: EffectOptions, build?: ReactorBuild<T>): T;
32
51
 
33
52
  /**
34
53
  * Subscribes to a derived slice of Reactor state.
@@ -43,7 +62,7 @@ declare function useAnyReactor(options?: EffectOptions): void;
43
62
  * @param build Optional Reactor build options used when creating a scoped Reactor for plain objects.
44
63
  * @returns The selected slice.
45
64
  * @example
46
- * const a = useSelector({ user: { name: "Ada" } }, (s) => s.user.name);
65
+ * const a = useSelector({ user: { name: "Ada" } }, (s) => s.user.name); // per-component scoped
47
66
  * @example
48
67
  * const state = reactive({ user: { name: "Ada" } });
49
68
  * const b = useSelector(state, (s) => s.user.name);
@@ -63,6 +82,28 @@ declare function useSelector<T extends object, R>(target: T | Reactor<T> | React
63
82
  * @returns The selected slice.
64
83
  */
65
84
  declare function useAnySelector<R>(sel: () => R, eq?: (value1: any, value2: any) => boolean, options?: EffectOptions): R;
85
+ /**
86
+ * Subscribes to a derived slice of Reactor state.
87
+ * The selector runs against a tracked snapshot and uses the provided equality function
88
+ * to suppress unchanged results.
89
+ * @typeParam T Root state object type.
90
+ * @typeParam R Selector return type.
91
+ * @param target Reactive object, Reactor instance, or plain object.
92
+ * @param sel Slice selector.
93
+ * @param eq Equality function used to compare consecutive selector results.
94
+ * @param options Watcher options if `options.sync: false` else Listener options.
95
+ * @param build Optional Reactor build options used when creating a scoped Reactor for plain objects.
96
+ * @returns The selected slice.
97
+ * @example
98
+ * const a = useSelectorSnapshot({ user: { name: "Ada" } }, (s) => s.user.name); // per-component scoped
99
+ * @example
100
+ * const state = reactive({ user: { name: "Ada" } });
101
+ * const b = useSelectorSnapshot(state, (s) => s.user.name);
102
+ * @example
103
+ * const rtr = new Reactor({ user: { name: "Ada" } });
104
+ * const c = useSelectorSnapshot(rtr, (s) => s.user.name);
105
+ */
106
+ declare function useSelectorSnapshot<T extends object, R>(target: T | Reactor<T> | Reactive<T>, sel: (state: T) => R, eq?: (value1: any, value2: any) => boolean, options?: EffectOptions, build?: ReactorBuild<T>): R;
66
107
 
67
108
  /**
68
109
  * Subscribes to a single path in Reactor state.
@@ -102,4 +143,4 @@ interface TimeTravelOverlayProps extends Partial<TimeTravelConfig> {
102
143
  */
103
144
  declare function TimeTravelOverlay(props: TimeTravelOverlayProps): null;
104
145
 
105
- export { TimeTravelOverlay, type TimeTravelOverlayProps, useAnyReactor, useAnySelector, useISOLayoutEffect, usePath, useReactor, useSelector };
146
+ export { TimeTravelOverlay, type TimeTravelOverlayProps, useAnyReactor, useAnySelector, useISOLayoutEffect, usePath, useReactor, useReactorSnapshot, useSelector, useSelectorSnapshot };