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