@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/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.95)",
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 STORAGE_KEY = "pulse-devtools-pos";
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
- isOpen = false;
19
- position = { x: window.innerWidth - 140, y: window.innerHeight - 65 };
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
- totalMovement = 0;
23
- lastMousePos = { x: 0, y: 0 };
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.loadPosition();
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
- loadPosition() {
62
+ loadState() {
46
63
  try {
47
64
  const saved = localStorage.getItem(STORAGE_KEY);
48
- if (saved) this.position = JSON.parse(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
- savePosition() {
72
+ saveState() {
53
73
  try {
54
- localStorage.setItem(STORAGE_KEY, JSON.stringify(this.position));
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", (e) => this.startDragging(e));
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() === "d") {
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
- if (this.totalMovement < 5) {
84
- this.isOpen = !this.isOpen;
85
- if (this.isOpen) {
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
- startDragging(e) {
97
- const target = e.target;
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
- if (!isHeader) return;
123
+ const isClose = target.closest("#close");
124
+ if (!isHeader || isClose) return;
100
125
  this.isDragging = true;
101
- this.totalMovement = 0;
102
- this.lastMousePos = { x: e.clientX, y: e.clientY };
126
+ this.totalDragDistance = 0;
103
127
  this.offset = {
104
- x: e.clientX - this.position.x,
105
- y: e.clientY - this.position.y
128
+ x: me.clientX - this.state.pos.x,
129
+ y: me.clientY - this.state.pos.y
106
130
  };
107
- const onMouseMove = (moveEv) => {
131
+ const move = (ev) => {
108
132
  if (!this.isDragging) return;
109
- this.totalMovement += Math.abs(moveEv.clientX - this.lastMousePos.x) + Math.abs(moveEv.clientY - this.lastMousePos.y);
110
- this.lastMousePos = { x: moveEv.clientX, y: moveEv.clientY };
111
- const w = this.isOpen ? 350 : 120;
112
- const h = this.isOpen ? 450 : 45;
113
- this.position = {
114
- x: Math.max(0, Math.min(window.innerWidth - w, moveEv.clientX - this.offset.x)),
115
- y: Math.max(0, Math.min(window.innerHeight - h, moveEv.clientY - this.offset.y))
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.position.x}px`;
120
- container.style.top = `${this.position.y}px`;
143
+ container.style.left = `${this.state.pos.x}px`;
144
+ container.style.top = `${this.state.pos.y}px`;
121
145
  }
122
146
  };
123
- const onMouseUp = () => {
147
+ const up = () => {
124
148
  this.isDragging = false;
125
- this.savePosition();
126
- document.removeEventListener("mousemove", onMouseMove);
127
- document.removeEventListener("mouseup", onMouseUp);
149
+ this.saveState();
150
+ document.removeEventListener("mousemove", move);
151
+ document.removeEventListener("mouseup", up);
128
152
  };
129
- document.addEventListener("mousemove", onMouseMove);
130
- document.addEventListener("mouseup", onMouseUp);
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
- const w = this.isOpen ? 350 : 120;
134
- const h = this.isOpen ? 450 : 45;
135
- this.shadow.innerHTML = `
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
- all: initial;
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
- left: ${this.position.x}px;
143
- top: ${this.position.y}px;
144
- width: ${w}px;
449
+ height: auto;
145
450
  z-index: 999999;
146
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
147
- color: white;
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(20px);
153
- -webkit-backdrop-filter: blur(20px);
457
+ backdrop-filter: blur(12px);
154
458
  border: 1px solid ${COLORS.border};
155
- border-radius: ${this.isOpen ? "16px" : "30px"};
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: 120px;
164
- height: 45px;
470
+ width: 140px;
471
+ height: 48px;
165
472
  display: flex;
166
473
  align-items: center;
167
474
  justify-content: center;
168
- cursor: grab;
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
- margin-right: 10px;
178
- box-shadow: 0 0 10px #00d2ff;
499
+ box-shadow: 0 0 12px ${COLORS.accentColor};
179
500
  }
180
- .header {
181
- padding: 12px 16px;
182
- background: rgba(0,0,0,0.3);
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
- cursor: grab;
521
+ justify-content: center;
522
+ gap: 6px;
188
523
  }
189
- .list {
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
- max-height: 380px;
193
- padding: 12px;
531
+ overflow-x: hidden;
532
+ min-height: 300px;
194
533
  }
195
- .list::-webkit-scrollbar { width: 5px; }
196
- .list::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }
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
- font-size: 12px;
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
- margin-bottom: 5px;
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 5px ${COLORS.success}; }
224
- .status-fail { background: ${COLORS.error}; box-shadow: 0 0 5px ${COLORS.error}; }
225
- .status-pending { background: ${COLORS.pending}; box-shadow: 0 0 5px ${COLORS.pending}; }
226
-
227
- .value {
228
- color: #00d2ff;
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
- word-break: break-all;
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
- .reason {
233
- color: ${COLORS.error};
234
- margin-top: 4px;
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
- background: rgba(255,75,43,0.1);
237
- padding: 4px;
595
+ padding: 6px;
238
596
  border-radius: 4px;
597
+ outline: none;
239
598
  }
240
- .footer {
241
- padding: 8px 12px;
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-top: 8px;
257
- border-top: 1px dashed ${COLORS.border};
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
- .dep-item {
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
- justify-content: space-between;
262
- padding: 2px 0;
263
- opacity: 0.8;
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")) {