@pulse-js/tools 0.2.0 → 0.3.0

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