@pulse-js/tools 0.2.0 → 0.3.1

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/dist/index.cjs CHANGED
@@ -20,12 +20,154 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- PulseInspector: () => PulseInspector
23
+ PulseAgent: () => PulseAgent,
24
+ PulseInspector: () => PulseInspector,
25
+ TimeTravel: () => TimeTravel
24
26
  });
25
27
  module.exports = __toCommonJS(index_exports);
26
28
 
27
- // src/pulse-inspector.ts
29
+ // src/agent.ts
28
30
  var import_core = require("@pulse-js/core");
31
+ var PulseAgent = class _PulseAgent {
32
+ static instance;
33
+ listeners = /* @__PURE__ */ new Set();
34
+ registryUnsubscribe = null;
35
+ unsubs = /* @__PURE__ */ new Map();
36
+ broadcastTimer = null;
37
+ constructor() {
38
+ this.setupRegistryObserver();
39
+ }
40
+ static getInstance() {
41
+ if (!this.instance) {
42
+ this.instance = new _PulseAgent();
43
+ }
44
+ return this.instance;
45
+ }
46
+ setupRegistryObserver() {
47
+ this.syncSubscriptions();
48
+ this.registryUnsubscribe = import_core.PulseRegistry.onRegister(() => {
49
+ this.syncSubscriptions();
50
+ this.broadcastStateDebounced();
51
+ });
52
+ }
53
+ /**
54
+ * Synchronizes subscriptions with the registry.
55
+ */
56
+ syncSubscriptions() {
57
+ const units = import_core.PulseRegistry.getAllWithMeta();
58
+ units.forEach(({ unit, uid }) => {
59
+ if (!this.unsubs.has(uid)) {
60
+ const subscribeFn = unit.subscribe || unit.$subscribe;
61
+ if (typeof subscribeFn === "function") {
62
+ const unsub = subscribeFn.call(unit, () => {
63
+ this.broadcastStateDebounced();
64
+ });
65
+ this.unsubs.set(uid, unsub);
66
+ }
67
+ }
68
+ });
69
+ }
70
+ /**
71
+ * Debounced broadcast to avoid rapid-fire messages.
72
+ */
73
+ broadcastStateDebounced() {
74
+ if (this.broadcastTimer) clearTimeout(this.broadcastTimer);
75
+ this.broadcastTimer = setTimeout(() => {
76
+ this.broadcastState();
77
+ this.broadcastTimer = null;
78
+ }, 50);
79
+ }
80
+ /**
81
+ * Broadcasts the current state of all registered units.
82
+ */
83
+ broadcastState() {
84
+ const units = import_core.PulseRegistry.getAllWithMeta();
85
+ const payload = units.map(({ unit, uid }) => {
86
+ const isGuard = unit.state;
87
+ const state = isGuard ? unit.explain() : { value: unit() };
88
+ return this.serialize({
89
+ uid,
90
+ name: unit._name || unit.name || "unnamed",
91
+ type: isGuard ? "guard" : "source",
92
+ ...state
93
+ });
94
+ });
95
+ this.emit({
96
+ type: "STATE_UPDATE",
97
+ payload
98
+ });
99
+ }
100
+ /**
101
+ * Recursively strips non-cloneable values (functions) from an object.
102
+ */
103
+ serialize(obj) {
104
+ if (obj === null || typeof obj !== "object") {
105
+ return typeof obj === "function" ? void 0 : obj;
106
+ }
107
+ if (Array.isArray(obj)) {
108
+ return obj.map((item) => this.serialize(item));
109
+ }
110
+ const cleaned = {};
111
+ for (const key in obj) {
112
+ const val = this.serialize(obj[key]);
113
+ if (val !== void 0) {
114
+ cleaned[key] = val;
115
+ }
116
+ }
117
+ return cleaned;
118
+ }
119
+ /**
120
+ * Emits an event to any connected clients.
121
+ */
122
+ emit(event) {
123
+ if (typeof window !== "undefined") {
124
+ try {
125
+ window.postMessage({ source: "pulse-agent", ...event }, "*");
126
+ } catch (e) {
127
+ console.error("[Pulse Agent] Failed to emit state update:", e);
128
+ }
129
+ }
130
+ this.listeners.forEach((l) => l(event));
131
+ }
132
+ /**
133
+ * Connect a local listener (useful for the same-page UI).
134
+ */
135
+ onEvent(listener) {
136
+ this.listeners.add(listener);
137
+ return () => this.listeners.delete(listener);
138
+ }
139
+ /**
140
+ * Handles an action from the DevTools UI.
141
+ */
142
+ handleAction(action) {
143
+ const unit = action.uid ? import_core.PulseRegistry.get(action.uid) : null;
144
+ if (!unit) return;
145
+ switch (action.type) {
146
+ case "RESET_GUARD":
147
+ if (unit._evaluate) unit._evaluate();
148
+ break;
149
+ case "SET_SOURCE_VALUE":
150
+ if (unit.set) unit.set(action.payload);
151
+ break;
152
+ }
153
+ }
154
+ dispose() {
155
+ if (this.registryUnsubscribe) this.registryUnsubscribe();
156
+ this.unsubs.forEach((unsub) => unsub());
157
+ this.unsubs.clear();
158
+ this.listeners.clear();
159
+ }
160
+ };
161
+ if (typeof window !== "undefined" && window.process?.env?.NODE_ENV !== "production") {
162
+ PulseAgent.getInstance();
163
+ window.addEventListener("message", (event) => {
164
+ if (event.data?.source === "pulse-ui") {
165
+ PulseAgent.getInstance().handleAction(event.data.action);
166
+ }
167
+ });
168
+ }
169
+
170
+ // src/inspector.ts
29
171
  var COLORS = {
30
172
  bg: "rgba(13, 13, 18, 0.96)",
31
173
  border: "rgba(255, 255, 255, 0.1)",
@@ -40,14 +182,6 @@ var COLORS = {
40
182
  cardHover: "rgba(255, 255, 255, 0.08)",
41
183
  inputBg: "rgba(0,0,0,0.3)"
42
184
  };
43
- var ICONS = {
44
- tree: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12h-4l-3 8-4-16-3 8H2"/></svg>`,
45
- list: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>`,
46
- chevronRight: `<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>`,
47
- chevronDown: `<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>`,
48
- edit: `<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`,
49
- refresh: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"/></svg>`
50
- };
51
185
  var STORAGE_KEY = "pulse-devtools-state";
52
186
  var PulseInspector = class extends HTMLElement {
53
187
  shadow;
@@ -55,49 +189,29 @@ var PulseInspector = class extends HTMLElement {
55
189
  // State
56
190
  state = {
57
191
  pos: { x: window.innerWidth - 360, y: 20 },
58
- isOpen: true,
59
- activeTab: "inspector",
60
- expandedNodes: []
192
+ isOpen: true
61
193
  };
62
194
  isDragging = false;
63
195
  offset = { x: 0, y: 0 };
64
- editingUnit = null;
65
- editValue = "";
66
- unsubscribeRegistry;
67
- unitSubscriptions = /* @__PURE__ */ new Map();
68
- shortcut = "Ctrl+M";
69
- // Default shortcut
196
+ totalDragDistance = 0;
70
197
  constructor() {
71
198
  super();
72
199
  this.shadow = this.attachShadow({ mode: "open" });
73
200
  this.loadState();
74
- const shortcutAttr = this.getAttribute("shortcut");
75
- if (shortcutAttr) {
76
- this.shortcut = shortcutAttr;
77
- }
78
201
  }
79
202
  connectedCallback() {
203
+ this.setupAgentConnection();
204
+ this.setupGlobalListeners();
80
205
  this.render();
81
- this.setupListeners();
82
- this.refreshUnits();
83
- this.unsubscribeRegistry = import_core.PulseRegistry.onRegister(() => {
84
- this.refreshUnits();
85
- });
86
- window.addEventListener("keydown", this.handleKeyDown);
87
- window.addEventListener("resize", this.handleResize);
88
206
  }
89
207
  disconnectedCallback() {
90
- if (this.unsubscribeRegistry) this.unsubscribeRegistry();
91
- this.unitSubscriptions.forEach((unsub) => unsub());
92
- window.removeEventListener("keydown", this.handleKeyDown);
93
208
  window.removeEventListener("resize", this.handleResize);
94
209
  }
95
210
  loadState() {
96
211
  try {
97
212
  const saved = localStorage.getItem(STORAGE_KEY);
98
213
  if (saved) {
99
- const parsed = JSON.parse(saved);
100
- this.state = { ...this.state, ...parsed };
214
+ this.state = { ...this.state, ...JSON.parse(saved) };
101
215
  }
102
216
  } catch (e) {
103
217
  }
@@ -108,72 +222,47 @@ var PulseInspector = class extends HTMLElement {
108
222
  } catch (e) {
109
223
  }
110
224
  }
111
- refreshUnits() {
112
- this.units = import_core.PulseRegistry.getAll();
113
- this.updateUnitSubscriptions();
114
- this.render();
115
- }
116
- updateUnitSubscriptions() {
117
- this.unitSubscriptions.forEach((unsub) => unsub());
118
- this.unitSubscriptions.clear();
119
- this.units.forEach((unit) => {
120
- const unsub = unit.subscribe(() => {
225
+ setupAgentConnection() {
226
+ const agent = PulseAgent.getInstance();
227
+ agent.onEvent((event) => {
228
+ if (event.type === "STATE_UPDATE") {
229
+ this.units = event.payload;
121
230
  this.render();
122
- });
123
- this.unitSubscriptions.set(unit, unsub);
231
+ }
124
232
  });
233
+ agent.broadcastState();
125
234
  }
126
- setupListeners() {
235
+ setupGlobalListeners() {
127
236
  this.shadow.addEventListener("mousedown", this.handleMouseDown);
128
237
  this.shadow.addEventListener("click", this.handleClick);
129
- this.shadow.addEventListener("submit", this.handleEditSubmit);
130
- this.shadow.addEventListener("keydown", this.handleEditKeydown);
238
+ window.addEventListener("resize", this.handleResize);
131
239
  }
132
- handleKeyDown = (e) => {
133
- const parts = this.shortcut.split("+").map((p) => p.trim().toLowerCase());
134
- const needsCtrl = parts.includes("ctrl");
135
- const needsShift = parts.includes("shift");
136
- const needsAlt = parts.includes("alt");
137
- const key = parts.find((p) => !["ctrl", "shift", "alt"].includes(p));
138
- if (!key) return;
139
- const matches = e.ctrlKey === needsCtrl && e.shiftKey === needsShift && e.altKey === needsAlt && e.key.toLowerCase() === key;
140
- if (matches) {
141
- e.preventDefault();
142
- this.toggle();
143
- }
144
- };
145
240
  handleResize = () => {
146
- const maxW = window.innerWidth - (this.state.isOpen ? 400 : 120);
147
- const maxH = window.innerHeight - (this.state.isOpen ? 500 : 45);
148
- this.state.pos.x = Math.max(0, Math.min(maxW, this.state.pos.x));
149
- this.state.pos.y = Math.max(0, Math.min(maxH, this.state.pos.y));
241
+ const width = this.state.isOpen ? 400 : 140;
242
+ const height = this.state.isOpen ? 600 : 48;
243
+ this.state.pos.x = Math.max(0, Math.min(window.innerWidth - width, this.state.pos.x));
244
+ this.state.pos.y = Math.max(0, Math.min(window.innerHeight - height, this.state.pos.y));
150
245
  this.render();
151
246
  };
152
- toggle() {
153
- this.state.isOpen = !this.state.isOpen;
154
- this.saveState();
155
- this.render();
156
- }
157
- // --- DRAG LOGIC ---
158
- totalDragDistance = 0;
159
247
  handleMouseDown = (e) => {
160
- const me = e;
161
- const target = me.target;
162
- const isHeader = target.closest(".header") || target.classList.contains("toggle-btn");
163
- const isClose = target.closest("#close");
164
- if (!isHeader || isClose) return;
165
- this.isDragging = true;
166
248
  this.totalDragDistance = 0;
249
+ const target = e.target;
250
+ const isHeader = target.closest(".header") || target.closest(".toggle-btn");
251
+ const isAction = target.closest("#close") || target.closest("[data-action]");
252
+ if (!isHeader || isAction) return;
253
+ this.isDragging = true;
167
254
  this.offset = {
168
- x: me.clientX - this.state.pos.x,
169
- y: me.clientY - this.state.pos.y
255
+ x: e.clientX - this.state.pos.x,
256
+ y: e.clientY - this.state.pos.y
170
257
  };
171
258
  const move = (ev) => {
172
259
  if (!this.isDragging) return;
173
260
  ev.preventDefault();
174
- this.totalDragDistance += Math.abs(ev.movementX) + Math.abs(ev.movementY);
175
- const width = this.state.isOpen ? 400 : 140;
176
- const height = this.state.isOpen ? 600 : 48;
261
+ const dx = ev.clientX - (this.state.pos.x + this.offset.x);
262
+ const dy = ev.clientY - (this.state.pos.y + this.offset.y);
263
+ this.totalDragDistance += Math.abs(dx) + Math.abs(dy);
264
+ const width = this.state.isOpen ? 380 : 140;
265
+ const height = this.state.isOpen ? 400 : 48;
177
266
  this.state.pos = {
178
267
  x: Math.max(0, Math.min(window.innerWidth - width, ev.clientX - this.offset.x)),
179
268
  y: Math.max(0, Math.min(window.innerHeight - height, ev.clientY - this.offset.y))
@@ -193,108 +282,35 @@ var PulseInspector = class extends HTMLElement {
193
282
  document.addEventListener("mousemove", move);
194
283
  document.addEventListener("mouseup", up);
195
284
  };
196
- // --- INTERACTION LOGIC ---
197
285
  handleClick = (e) => {
198
- if (this.totalDragDistance > 5) {
199
- this.totalDragDistance = 0;
200
- return;
201
- }
286
+ if (this.totalDragDistance > 10) return;
202
287
  const target = e.target;
203
- if (target.id === "toggle" || target.closest("#close")) {
204
- this.toggle();
205
- return;
206
- }
207
- if (target.id === "refresh" || target.closest("#refresh")) {
208
- this.refreshUnits();
209
- return;
210
- }
211
- const tabEl = target.closest(".tab-btn");
212
- if (tabEl) {
213
- const tab = tabEl.getAttribute("data-tab");
214
- if (tab) {
215
- this.state.activeTab = tab;
216
- this.saveState();
217
- this.render();
218
- }
219
- return;
220
- }
221
- const valueEl = target.closest(".editable-value");
222
- if (valueEl) {
223
- const id = valueEl.getAttribute("data-id");
224
- const name = valueEl.getAttribute("data-name");
225
- const unit = this.units.find((u) => u._id === id || u._name === name);
226
- if (unit && !("state" in unit)) {
227
- this.editingUnit = unit;
228
- this.editValue = JSON.stringify(unit());
229
- this.render();
230
- requestAnimationFrame(() => {
231
- const input = this.shadow.querySelector(".edit-input");
232
- if (input) {
233
- input.focus();
234
- input.select();
235
- }
236
- });
237
- }
238
- return;
239
- }
240
- if (this.editingUnit && !target.closest(".edit-form")) {
241
- this.editingUnit = null;
242
- this.render();
243
- }
244
- const treeNode = target.closest(".tree-toggle");
245
- if (treeNode) {
246
- e.stopPropagation();
247
- const id = treeNode.getAttribute("data-id");
248
- if (id) {
249
- if (this.state.expandedNodes.includes(id)) {
250
- this.state.expandedNodes = this.state.expandedNodes.filter((n) => n !== id);
251
- } else {
252
- this.state.expandedNodes.push(id);
253
- }
254
- this.saveState();
255
- this.render();
256
- }
257
- }
258
- };
259
- handleEditSubmit = (e) => {
260
- e.preventDefault();
261
- if (!this.editingUnit) return;
262
- try {
263
- const form = e.target;
264
- const input = form.querySelector("input");
265
- let val = input.value;
266
- try {
267
- if (val === "true") val = true;
268
- else if (val === "false") val = false;
269
- else if (val === "null") val = null;
270
- else if (val === "undefined") val = void 0;
271
- else if (!isNaN(Number(val)) && val.trim() !== "") val = Number(val);
272
- else if (val.startsWith("{") || val.startsWith("[")) val = JSON.parse(val);
273
- } catch (err) {
274
- }
275
- this.editingUnit.set(val);
276
- this.editingUnit = null;
288
+ if (target.closest("#toggle") || target.closest("#close")) {
289
+ this.state.isOpen = !this.state.isOpen;
290
+ this.saveState();
277
291
  this.render();
278
- } catch (err) {
279
- console.error("Pulse DevTools: Failed to update value", err);
292
+ return;
280
293
  }
281
- };
282
- handleEditKeydown = (e) => {
283
- if (this.editingUnit && e.key === "Escape") {
284
- this.editingUnit = null;
285
- this.render();
294
+ const actionBtn = target.closest("[data-action]");
295
+ if (actionBtn) {
296
+ const type = actionBtn.getAttribute("data-action");
297
+ const uid = actionBtn.getAttribute("data-uid");
298
+ this.sendAction(type, uid);
286
299
  }
287
300
  };
288
- // --- RENDERERS ---
301
+ sendAction(type, uid, payload) {
302
+ window.postMessage({
303
+ source: "pulse-ui",
304
+ action: { type, uid, payload }
305
+ }, "*");
306
+ }
289
307
  render() {
290
308
  if (!this.state.isOpen) {
291
309
  this.shadow.innerHTML = this.getStyles() + `
292
310
  <div class="container" style="left: ${this.state.pos.x}px; top: ${this.state.pos.y}px">
293
- <div class="glass" style="border-radius:30px; width:auto;">
294
- <div class="toggle-btn" id="toggle">
295
- <div class="dot"></div>
296
- Pulse (${this.units.length})
297
- </div>
311
+ <div class="glass toggle-btn" id="toggle">
312
+ <div class="dot"></div>
313
+ Pulse (${this.units.length})
298
314
  </div>
299
315
  </div>
300
316
  `;
@@ -302,177 +318,76 @@ var PulseInspector = class extends HTMLElement {
302
318
  }
303
319
  this.shadow.innerHTML = this.getStyles() + `
304
320
  <div class="container" style="left: ${this.state.pos.x}px; top: ${this.state.pos.y}px">
305
- <div class="glass">
321
+ <div class="glass window">
306
322
 
307
323
  <div class="header">
308
324
  <div style="display:flex;align-items:center;gap:10px">
309
325
  <div class="dot" style="width:10px;height:10px"></div>
310
326
  <strong style="font-size:14px;letter-spacing:0.5px">PULSE</strong>
311
- <span style="font-size:10px;opacity:0.5;margin-top:2px">v0.2.0</span>
327
+ <span style="font-size:10px;opacity:0.5;margin-top:2px">v2.0</span>
312
328
  </div>
313
329
  <div style="display:flex;gap:8px;align-items:center">
314
- <div id="refresh" class="icon-btn" title="Refresh" style="font-size:16px">${ICONS.refresh}</div>
315
- <div id="close" class="icon-btn" title="Close (${this.shortcut})">\xD7</div>
316
- </div>
317
- </div>
318
-
319
- <div class="tabs">
320
- <div class="tab-btn ${this.state.activeTab === "inspector" ? "active" : ""}" data-tab="inspector">
321
- ${ICONS.list} Inspector
322
- </div>
323
- <div class="tab-btn ${this.state.activeTab === "tree" ? "active" : ""}" data-tab="tree">
324
- ${ICONS.tree} Pulse Tree
330
+ <div id="close" class="icon-btn" title="Minimize">\xD7</div>
325
331
  </div>
326
332
  </div>
327
333
 
328
334
  <div class="content">
329
- ${this.state.activeTab === "inspector" ? this.renderInspector() : this.renderTree()}
335
+ ${this.units.length === 0 ? `<div class="empty-state">No reactive units detected.</div>` : `<div class="list">${this.units.map((u) => this.renderUnitCard(u)).join("")}</div>`}
330
336
  </div>
331
337
 
332
338
  </div>
333
339
  </div>
334
340
  `;
335
341
  }
336
- renderInspector() {
337
- if (this.units.length === 0) {
338
- return `<div class="empty-state">No Pulse units detected.</div>`;
339
- }
342
+ renderUnitCard(u) {
343
+ const status = u.status || "ok";
344
+ const hasDeps = u.dependencies && u.dependencies.length > 0;
340
345
  return `
341
- <div class="list">
342
- ${this.units.map((u) => this.renderUnitCard(u)).join("")}
343
- </div>
344
- `;
345
- }
346
- renderUnitCard(unit) {
347
- const isGuard = "state" in unit;
348
- const name = unit._name || "unnamed";
349
- const id = unit._id;
350
- const explanation = isGuard ? unit.explain() : null;
351
- const value = isGuard ? explanation.value : unit();
352
- let status = "ok";
353
- if (isGuard) status = explanation.status;
354
- const isEditing = this.editingUnit === unit;
355
- return `
356
- <div class="unit-card ${isGuard ? "" : "source-card"}" style="border-left-color: ${this.getStatusColor(status)}">
346
+ <div class="unit-card ${u.type}-card" style="border-left-color: ${this.getStatusColor(status)}">
357
347
  <div class="unit-header">
358
- <div style="display:flex;align-items:center;gap:6px">
359
- <div class="status-dot status-${status}"></div>
360
- <strong title="${name}">${name}</strong>
361
- </div>
362
- <span class="badg type-${isGuard ? "guard" : "source"}">${isGuard ? "GUARD" : "SOURCE"}</span>
348
+ <div style="display:flex;align-items:center;gap:6px">
349
+ <div class="status-dot status-${status}"></div>
350
+ <strong title="${u.name}">${u.name}</strong>
351
+ </div>
352
+ <span class="badg type-${u.type}">${u.type.toUpperCase()}</span>
363
353
  </div>
364
-
365
- <div class="unit-body">
366
- ${isEditing ? `
367
- <form class="edit-form">
368
- <input class="edit-input" value='${this.safeStringify(value)}' />
369
- </form>
370
- ` : `
371
- <div class="value-row ${!isGuard ? "editable-value" : ""}"
372
- data-id="${id}"
373
- data-name="${name}"
374
- title="${!isGuard ? "Click to edit" : ""}">
375
- <span style="opacity:0.5;margin-right:6px">Value:</span>
376
- <span class="value-text">${this.formatValue(value)}</span>
377
- ${!isGuard ? `<span class="edit-icon">${ICONS.edit}</span>` : ""}
378
- </div>
379
- `}
380
-
381
- ${isGuard && explanation.status === "fail" ? this.renderReason(explanation.reason) : ""}
382
- ${isGuard && explanation.status === "pending" ? `
383
- <div class="reason pending-reason">
384
- \u23F3 Evaluating... ${explanation.lastReason ? `<br/><span style="opacity:0.7;font-size:9px">Last error: ${this.formatReasonText(explanation.lastReason)}</span>` : ""}
385
- </div>
386
- ` : ""}
354
+ <div class="value-row">
355
+ <span style="opacity:0.5;margin-right:6px">Value:</span>
356
+ <span class="value-text">${this.formatValue(u.value)}</span>
387
357
  </div>
358
+
359
+ ${u.reason ? `
360
+ <div class="reason-box">
361
+ <strong>${u.reason.code || "FAILURE"}</strong>
362
+ <div>${u.reason.message}</div>
363
+ </div>
364
+ ` : ""}
365
+
366
+ ${hasDeps ? `
367
+ <div class="deps-list">
368
+ <div class="deps-label">Dependencies:</div>
369
+ ${u.dependencies.map((d) => `
370
+ <div class="dep-item">
371
+ <span class="dep-dot"></span>
372
+ ${d.name}
373
+ </div>
374
+ `).join("")}
375
+ </div>
376
+ ` : ""}
388
377
  </div>
389
378
  `;
390
379
  }
391
- renderTree() {
392
- const guards = this.units.filter((u) => "state" in u);
393
- if (guards.length === 0) return `<div class="empty-state">No Guards to visualize.</div>`;
394
- return `
395
- <div class="tree-view">
396
- ${guards.map((g) => this.renderTreeNode(g, 0, g._name)).join("")}
397
- </div>
398
- `;
399
- }
400
- renderTreeNode(guard, depth, key) {
401
- const isExpanded = this.state.expandedNodes.includes(key);
402
- const explanation = guard.explain();
403
- const deps = explanation.dependencies || [];
404
- const hasDeps = deps.length > 0;
405
- const statusColor = this.getStatusColor(explanation.status);
406
- return `
407
- <div class="tree-node" style="padding-left:${depth * 12}px">
408
- <div class="tree-row tree-toggle" data-id="${key}">
409
- <span class="tree-icon" style="visibility:${hasDeps ? "visible" : "hidden"}">
410
- ${isExpanded ? ICONS.chevronDown : ICONS.chevronRight}
411
- </span>
412
- <span class="status-dot status-${explanation.status}" style="width:6px;height:6px;margin:0 6px"></span>
413
- <span class="tree-name" style="color:${explanation.status === "fail" ? COLORS.error : "inherit"}">
414
- ${explanation.name}
415
- </span>
416
- ${explanation.status === "fail" ? `
417
- <span class="mini-badge fail">!</span>
418
- ` : ""}
419
- </div>
420
- </div>
421
- ${isExpanded && hasDeps ? `
422
- <div class="tree-children">
423
- ${deps.map((dep) => {
424
- const unit = this.units.find((u) => u._name === dep.name);
425
- const childKey = key + "-" + dep.name;
426
- if (unit && "state" in unit) {
427
- return this.renderTreeNode(unit, depth + 1, childKey);
428
- } else {
429
- return `
430
- <div class="tree-node" style="padding-left:${(depth + 1) * 12}px">
431
- <div class="tree-row">
432
- <span class="tree-icon" style="visibility:hidden"></span>
433
- <span class="status-dot status-ok" style="background:${COLORS.secondaryText}"></span>
434
- <span class="tree-name" style="opacity:0.7">${dep.name}</span>
435
- <span class="mini-badge source">S</span>
436
- </div>
437
- </div>
438
- `;
439
- }
440
- }).join("")}
441
- </div>
442
- ` : ""}
443
- `;
444
- }
445
- // --- HELPERS ---
446
- renderReason(reason) {
447
- if (!reason) return "";
448
- const text = this.formatReasonText(reason);
449
- const meta = typeof reason === "object" && reason.meta ? reason.meta : null;
450
- return `
451
- <div class="reason">
452
- <strong>${typeof reason === "object" ? reason.code || "ERROR" : "FAIL"}</strong>
453
- <div>${text}</div>
454
- ${meta ? `<pre class="meta-block">${JSON.stringify(meta, null, 2)}</pre>` : ""}
455
- </div>
456
- `;
457
- }
458
- formatReasonText(reason) {
459
- if (typeof reason === "string") return reason;
460
- return reason.message || JSON.stringify(reason);
461
- }
462
380
  formatValue(val) {
463
381
  if (val === void 0) return "undefined";
464
382
  if (val === null) return "null";
465
- if (typeof val === "function") return "\u0192()";
466
- if (typeof val === "object") return "{...}";
467
- return String(val);
468
- }
469
- safeStringify(val) {
470
- try {
471
- if (val === void 0) return "undefined";
472
- return JSON.stringify(val);
473
- } catch {
474
- return String(val);
383
+ if (typeof val === "object") {
384
+ try {
385
+ return JSON.stringify(val);
386
+ } catch (e) {
387
+ return "{...}";
388
+ }
475
389
  }
390
+ return String(val);
476
391
  }
477
392
  getStatusColor(status) {
478
393
  switch (status) {
@@ -489,163 +404,31 @@ var PulseInspector = class extends HTMLElement {
489
404
  getStyles() {
490
405
  return `
491
406
  <style>
492
- :host { all: initial; font-family: 'Inter', -apple-system, sans-serif; }
493
- * { box-sizing: border-box; }
494
-
495
- .container {
496
- position: fixed;
497
- height: auto;
498
- z-index: 999999;
499
- filter: drop-shadow(0 8px 32px rgba(0,0,0,0.5));
500
- color: ${COLORS.text};
501
- }
502
-
503
- .glass {
504
- background: ${COLORS.bg};
505
- backdrop-filter: blur(12px);
506
- border: 1px solid ${COLORS.border};
507
- border-radius: 12px;
508
- overflow: hidden;
509
- display: flex;
510
- flex-direction: column;
511
- width: 400px;
512
- max-height: 80vh;
513
- transition: width 0.2s, height 0.2s;
514
- }
515
-
516
- /* TOGGLE BUTTON MODE */
517
- .toggle-btn {
518
- width: 140px;
519
- height: 48px;
520
- display: flex;
521
- align-items: center;
522
- justify-content: center;
523
- gap: 10px;
524
- cursor: pointer;
525
- font-weight: 600;
526
- font-size: 13px;
527
- background: rgba(255,255,255,0.03);
528
- }
529
- .toggle-btn:hover { background: rgba(255,255,255,0.08); }
530
-
531
- /* HEADER */
532
- .header {
533
- padding: 12px 16px;
534
- background: rgba(0,0,0,0.2);
535
- border-bottom: 1px solid ${COLORS.border};
536
- display: flex;
537
- justify-content: space-between;
538
- align-items: center;
539
- cursor: move;
540
- flex-shrink: 0;
541
- }
542
- .dot {
543
- width: 8px;
544
- height: 8px;
545
- border-radius: 50%;
546
- background: ${COLORS.accent};
547
- box-shadow: 0 0 12px ${COLORS.accentColor};
548
- }
549
- .icon-btn { cursor: pointer; opacity: 0.6; padding: 4px; font-size: 18px; line-height: 1; }
550
- .icon-btn:hover { opacity: 1; color: white; }
551
-
552
- /* TABS */
553
- .tabs {
554
- display: flex;
555
- background: rgba(0,0,0,0.2);
556
- border-bottom: 1px solid ${COLORS.border};
557
- font-size: 11px;
558
- font-weight: 600;
559
- }
560
- .tab-btn {
561
- flex: 1;
562
- padding: 10px;
563
- text-align: center;
564
- cursor: pointer;
565
- opacity: 0.6;
566
- border-bottom: 2px solid transparent;
567
- display: flex;
568
- align-items: center;
569
- justify-content: center;
570
- gap: 6px;
571
- }
572
- .tab-btn:hover { opacity: 0.9; background: rgba(255,255,255,0.02); }
573
- .tab-btn.active { opacity: 1; border-bottom-color: ${COLORS.accentColor}; color: ${COLORS.accentColor}; background: rgba(0,210,255,0.05); }
574
-
575
- /* CONTENT */
576
- .content {
577
- flex: 1;
578
- overflow-y: auto;
579
- overflow-x: hidden;
580
- min-height: 300px;
581
- }
407
+ :host { all: initial; font-family: 'Inter', system-ui, sans-serif; }
408
+ .container { position: fixed; z-index: 1000000; filter: drop-shadow(0 8px 32px rgba(0,0,0,0.5)); color: white; }
409
+ .glass { background: ${COLORS.bg}; backdrop-filter: blur(12px); border: 1px solid ${COLORS.border}; border-radius: 12px; overflow: hidden; }
410
+ .window { width: 380px; max-height: 80vh; display: flex; flex-direction: column; }
411
+ .toggle-btn { width: 140px; height: 48px; display: flex; align-items: center; justify-content: center; gap: 10px; cursor: pointer; font-weight: 600; font-size: 13px; }
412
+ .header { padding: 12px 16px; background: rgba(0,0,0,0.2); border-bottom: 1px solid ${COLORS.border}; display: flex; justify-content: space-between; align-items: center; cursor: move; }
413
+ .dot { width: 8px; height: 8px; border-radius: 50%; background: ${COLORS.accent}; box-shadow: 0 0 12px ${COLORS.accentColor}; }
414
+ .icon-btn { cursor: pointer; opacity: 0.6; padding: 4px; font-size: 18px; line-height: 1; display: flex; align-items: center; }
415
+ .icon-btn:hover { opacity: 1; }
416
+ .content { flex: 1; overflow-y: auto; padding: 12px; min-height: 200px; }
582
417
  .content::-webkit-scrollbar { width: 6px; }
583
418
  .content::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
584
-
585
- /* INSPECTOR LIST */
586
- .list { padding: 12px; display: flex; flex-direction: column; gap: 8px; }
587
- .empty-state { padding: 40px; text-align: center; opacity: 0.4; font-size: 12px; }
588
-
589
- .unit-card {
590
- background: ${COLORS.cardBg};
591
- border: 1px solid ${COLORS.border};
592
- border-left: 3px solid transparent;
593
- border-radius: 6px;
594
- padding: 10px;
595
- transition: background 0.1s;
596
- }
597
- .unit-card:hover { background: ${COLORS.cardHover}; }
598
- .source-card { border-left-color: ${COLORS.secondaryText} !important; }
599
-
600
- .unit-header {
601
- display: flex;
602
- justify-content: space-between;
603
- align-items: center;
604
- margin-bottom: 8px;
605
- }
606
- .status-dot {
607
- width: 6px;
608
- height: 6px;
609
- border-radius: 50%;
610
- }
419
+ .list { display: flex; flex-direction: column; gap: 8px; }
420
+ .unit-card { background: ${COLORS.cardBg}; border: 1px solid ${COLORS.border}; border-left: 3px solid transparent; border-radius: 6px; padding: 10px; }
421
+ .unit-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
422
+ .status-dot { width: 6px; height: 6px; border-radius: 50%; }
611
423
  .status-ok { background: ${COLORS.success}; box-shadow: 0 0 6px ${COLORS.success}; }
612
424
  .status-fail { background: ${COLORS.error}; box-shadow: 0 0 6px ${COLORS.error}; }
613
425
  .status-pending { background: ${COLORS.pending}; box-shadow: 0 0 6px ${COLORS.pending}; }
614
-
615
- .badg { font-size: 8px; font-weight: 700; padding: 2px 4px; border-radius: 3px; background: rgba(255,255,255,0.1); }
426
+ .badg { font-size: 8px; font-weight: 700; padding: 2px 4px; border-radius: 3px; }
616
427
  .type-guard { color: ${COLORS.accentColor}; background: rgba(0,210,255,0.1); }
617
- .type-source { color: ${COLORS.secondaryText}; }
618
-
619
- .value-row {
620
- font-size: 11px;
621
- font-family: monospace;
622
- background: rgba(0,0,0,0.2);
623
- padding: 6px 8px;
624
- border-radius: 4px;
625
- display: flex;
626
- align-items: center;
627
- justify-content: space-between;
628
- }
428
+ .type-source { color: ${COLORS.secondaryText}; background: rgba(255,255,255,0.05); }
429
+ .value-row { font-size: 11px; font-family: monospace; background: rgba(0,0,0,0.2); padding: 6px 8px; border-radius: 4px; display: flex; align-items: center; justify-content: space-between; }
629
430
  .value-text { color: ${COLORS.accentColor}; }
630
- .editable-value { cursor: pointer; border: 1px dashed transparent; }
631
- .editable-value:hover { border-color: rgba(255,255,255,0.2); background: rgba(255,255,255,0.05); }
632
- .edit-icon { opacity: 0; transition: opacity 0.2s; }
633
- .editable-value:hover .edit-icon { opacity: 0.7; }
634
-
635
- .edit-form { margin: 0; padding: 0; width: 100%; }
636
- .edit-input {
637
- width: 100%;
638
- background: ${COLORS.inputBg};
639
- border: 1px solid ${COLORS.accentColor};
640
- color: white;
641
- font-family: monospace;
642
- font-size: 11px;
643
- padding: 6px;
644
- border-radius: 4px;
645
- outline: none;
646
- }
647
-
648
- .reason {
431
+ .reason-box {
649
432
  margin-top: 8px;
650
433
  padding: 8px;
651
434
  background: rgba(255, 75, 43, 0.1);
@@ -654,30 +437,14 @@ var PulseInspector = class extends HTMLElement {
654
437
  color: ${COLORS.error};
655
438
  font-size: 11px;
656
439
  }
657
- .pending-reason { color: ${COLORS.pending} !important; background: rgba(253, 187, 45, 0.1); border-color: rgba(253, 187, 45, 0.2); }
658
- .meta-block { margin: 4px 0 0 0; opacity: 0.7; font-size: 10px; }
659
-
660
- /* TREE VIEW */
661
- .tree-view { padding: 12px; font-size: 12px; }
662
- .tree-node { margin-bottom: 2px; }
663
- .tree-row {
664
- display: flex;
665
- align-items: center;
666
- padding: 4px 6px;
667
- border-radius: 4px;
668
- cursor: pointer;
669
- user-select: none;
670
- }
671
- .tree-row:hover { background: rgba(255,255,255,0.05); }
672
- .tree-icon { margin-right: 4px; opacity: 0.5; width: 14px; height: 14px; display: flex; align-items: center; justify-content: center; }
673
- .tree-name { opacity: 0.9; }
674
- .mini-badge {
675
- font-size: 9px; margin-left: auto; padding: 1px 4px; border-radius: 3px;
676
- font-weight: bold; opacity: 0.6;
677
- }
678
- .mini-badge.fail { background: ${COLORS.error}; color: white; opacity: 1; }
679
- .mini-badge.source { background: rgba(255,255,255,0.1); }
680
-
440
+ .reason-box strong { font-size: 10px; display: block; opacity: 0.8; margin-bottom: 2px; }
441
+ .deps-list { margin-top: 10px; border-top: 1px solid ${COLORS.border}; padding-top: 8px; }
442
+ .deps-label { font-size: 9px; opacity: 0.5; font-weight: bold; margin-bottom: 4px; text-transform: uppercase; }
443
+ .dep-item { font-size: 10px; opacity: 0.8; display: flex; align-items: center; gap: 6px; padding: 2px 0; }
444
+ .dep-dot { width: 4px; height: 4px; background: ${COLORS.accentColor}; border-radius: 50%; opacity: 0.5; }
445
+ .error-text { color: ${COLORS.error}; font-size: 10px; margin-top: 6px; }
446
+ .empty-state { padding: 40px; text-align: center; opacity: 0.4; font-size: 12px; }
447
+ .mini-btn { background: ${COLORS.accent}; border: none; color: white; border-radius: 3px; font-size: 9px; padding: 2px 6px; cursor: pointer; }
681
448
  </style>
682
449
  `;
683
450
  }
@@ -685,7 +452,54 @@ var PulseInspector = class extends HTMLElement {
685
452
  if (!customElements.get("pulse-inspector")) {
686
453
  customElements.define("pulse-inspector", PulseInspector);
687
454
  }
455
+
456
+ // src/timetravel.ts
457
+ var import_core2 = require("@pulse-js/core");
458
+ var TimeTravel = class {
459
+ snapshots = [];
460
+ maxSnapshots = 100;
461
+ /**
462
+ * Captures a point-in-time snapshot of all registered sources.
463
+ */
464
+ capture() {
465
+ const data = {};
466
+ const units = import_core2.PulseRegistry.getAllWithMeta();
467
+ units.forEach(({ unit, uid }) => {
468
+ if (!unit.state) {
469
+ data[uid] = unit();
470
+ }
471
+ });
472
+ this.snapshots.push({
473
+ timestamp: Date.now(),
474
+ data
475
+ });
476
+ if (this.snapshots.length > this.maxSnapshots) {
477
+ this.snapshots.shift();
478
+ }
479
+ }
480
+ /**
481
+ * Reverts the registry state to a specific snapshot index.
482
+ */
483
+ travel(index) {
484
+ const snapshot = this.snapshots[index];
485
+ if (!snapshot) return;
486
+ import_core2.PulseRegistry.getAllWithMeta().forEach(({ unit, uid }) => {
487
+ const value = snapshot.data[uid];
488
+ if (value !== void 0 && unit.set) {
489
+ unit.set(value);
490
+ }
491
+ });
492
+ }
493
+ getHistory() {
494
+ return this.snapshots;
495
+ }
496
+ clear() {
497
+ this.snapshots = [];
498
+ }
499
+ };
688
500
  // Annotate the CommonJS export names for ESM import in node:
689
501
  0 && (module.exports = {
690
- PulseInspector
502
+ PulseAgent,
503
+ PulseInspector,
504
+ TimeTravel
691
505
  });