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