@pulse-js/tools 0.1.3 → 0.1.5

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,543 @@ 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
+ if (unit && !("state" in unit)) {
208
+ this.editingUnit = unit;
209
+ this.editValue = JSON.stringify(unit());
210
+ this.render();
211
+ requestAnimationFrame(() => {
212
+ const input = this.shadow.querySelector(".edit-input");
213
+ if (input) {
214
+ input.focus();
215
+ input.select();
216
+ }
217
+ });
218
+ }
219
+ return;
220
+ }
221
+ if (this.editingUnit && !target.closest(".edit-form")) {
222
+ this.editingUnit = null;
223
+ this.render();
224
+ }
225
+ const treeNode = target.closest(".tree-toggle");
226
+ if (treeNode) {
227
+ e.stopPropagation();
228
+ const id = treeNode.getAttribute("data-id");
229
+ if (id) {
230
+ if (this.state.expandedNodes.includes(id)) {
231
+ this.state.expandedNodes = this.state.expandedNodes.filter((n) => n !== id);
232
+ } else {
233
+ this.state.expandedNodes.push(id);
234
+ }
235
+ this.saveState();
236
+ this.render();
237
+ }
238
+ }
239
+ };
240
+ handleEditSubmit = (e) => {
241
+ e.preventDefault();
242
+ if (!this.editingUnit) return;
243
+ try {
244
+ const form = e.target;
245
+ const input = form.querySelector("input");
246
+ let val = input.value;
247
+ try {
248
+ if (val === "true") val = true;
249
+ else if (val === "false") val = false;
250
+ else if (val === "null") val = null;
251
+ else if (val === "undefined") val = void 0;
252
+ else if (!isNaN(Number(val)) && val.trim() !== "") val = Number(val);
253
+ else if (val.startsWith("{") || val.startsWith("[")) val = JSON.parse(val);
254
+ } catch (err) {
255
+ }
256
+ this.editingUnit.set(val);
257
+ this.editingUnit = null;
258
+ this.render();
259
+ } catch (err) {
260
+ console.error("Pulse DevTools: Failed to update value", err);
261
+ }
262
+ };
263
+ handleEditKeydown = (e) => {
264
+ if (this.editingUnit && e.key === "Escape") {
265
+ this.editingUnit = null;
266
+ this.render();
267
+ }
268
+ };
269
+ // --- RENDERERS ---
158
270
  render() {
159
- const w = this.isOpen ? 350 : 120;
160
- const h = this.isOpen ? 450 : 45;
161
- this.shadow.innerHTML = `
271
+ if (!this.state.isOpen) {
272
+ this.shadow.innerHTML = this.getStyles() + `
273
+ <div class="container" style="left: ${this.state.pos.x}px; top: ${this.state.pos.y}px">
274
+ <div class="glass" style="border-radius:30px; width:auto;">
275
+ <div class="toggle-btn" id="toggle">
276
+ <div class="dot"></div>
277
+ Pulse (${this.units.length})
278
+ </div>
279
+ </div>
280
+ </div>
281
+ `;
282
+ return;
283
+ }
284
+ this.shadow.innerHTML = this.getStyles() + `
285
+ <div class="container" style="left: ${this.state.pos.x}px; top: ${this.state.pos.y}px">
286
+ <div class="glass">
287
+
288
+ <div class="header">
289
+ <div style="display:flex;align-items:center;gap:10px">
290
+ <div class="dot" style="width:10px;height:10px"></div>
291
+ <strong style="font-size:14px;letter-spacing:0.5px">PULSE</strong>
292
+ <span style="font-size:10px;opacity:0.5;margin-top:2px">v0.2.0</span>
293
+ </div>
294
+ <div id="close" class="icon-btn">\xD7</div>
295
+ </div>
296
+
297
+ <div class="tabs">
298
+ <div class="tab-btn ${this.state.activeTab === "inspector" ? "active" : ""}" data-tab="inspector">
299
+ ${ICONS.list} Inspector
300
+ </div>
301
+ <div class="tab-btn ${this.state.activeTab === "tree" ? "active" : ""}" data-tab="tree">
302
+ ${ICONS.tree} Pulse Tree
303
+ </div>
304
+ </div>
305
+
306
+ <div class="content">
307
+ ${this.state.activeTab === "inspector" ? this.renderInspector() : this.renderTree()}
308
+ </div>
309
+
310
+ </div>
311
+ </div>
312
+ `;
313
+ }
314
+ renderInspector() {
315
+ if (this.units.length === 0) {
316
+ return `<div class="empty-state">No Pulse units detected.</div>`;
317
+ }
318
+ return `
319
+ <div class="list">
320
+ ${this.units.map((u) => this.renderUnitCard(u)).join("")}
321
+ </div>
322
+ `;
323
+ }
324
+ renderUnitCard(unit) {
325
+ const isGuard = "state" in unit;
326
+ const name = unit._name || "unnamed";
327
+ const explanation = isGuard ? unit.explain() : null;
328
+ const value = isGuard ? explanation.value : unit();
329
+ let status = "ok";
330
+ if (isGuard) status = explanation.status;
331
+ const isEditing = this.editingUnit === unit;
332
+ return `
333
+ <div class="unit-card ${isGuard ? "" : "source-card"}" style="border-left-color: ${this.getStatusColor(status)}">
334
+ <div class="unit-header">
335
+ <div style="display:flex;align-items:center;gap:6px">
336
+ <div class="status-dot status-${status}"></div>
337
+ <strong title="${name}">${name}</strong>
338
+ </div>
339
+ <span class="badg type-${isGuard ? "guard" : "source"}">${isGuard ? "GUARD" : "SOURCE"}</span>
340
+ </div>
341
+
342
+ <div class="unit-body">
343
+ ${isEditing ? `
344
+ <form class="edit-form">
345
+ <input class="edit-input" value='${this.safeStringify(value)}' />
346
+ </form>
347
+ ` : `
348
+ <div class="value-row ${!isGuard ? "editable-value" : ""}" data-name="${name}" title="${!isGuard ? "Click to edit" : ""}">
349
+ <span style="opacity:0.5;margin-right:6px">Value:</span>
350
+ <span class="value-text">${this.formatValue(value)}</span>
351
+ ${!isGuard ? `<span class="edit-icon">${ICONS.edit}</span>` : ""}
352
+ </div>
353
+ `}
354
+
355
+ ${isGuard && explanation.status === "fail" ? this.renderReason(explanation.reason) : ""}
356
+ ${isGuard && explanation.status === "pending" ? `
357
+ <div class="reason pending-reason">
358
+ \u23F3 Evaluating... ${explanation.lastReason ? `<br/><span style="opacity:0.7;font-size:9px">Last error: ${this.formatReasonText(explanation.lastReason)}</span>` : ""}
359
+ </div>
360
+ ` : ""}
361
+ </div>
362
+ </div>
363
+ `;
364
+ }
365
+ renderTree() {
366
+ const guards = this.units.filter((u) => "state" in u);
367
+ if (guards.length === 0) return `<div class="empty-state">No Guards to visualize.</div>`;
368
+ return `
369
+ <div class="tree-view">
370
+ ${guards.map((g) => this.renderTreeNode(g, 0, g._name)).join("")}
371
+ </div>
372
+ `;
373
+ }
374
+ renderTreeNode(guard, depth, key) {
375
+ const isExpanded = this.state.expandedNodes.includes(key);
376
+ const explanation = guard.explain();
377
+ const deps = explanation.dependencies || [];
378
+ const hasDeps = deps.length > 0;
379
+ const statusColor = this.getStatusColor(explanation.status);
380
+ return `
381
+ <div class="tree-node" style="padding-left:${depth * 12}px">
382
+ <div class="tree-row tree-toggle" data-id="${key}">
383
+ <span class="tree-icon" style="visibility:${hasDeps ? "visible" : "hidden"}">
384
+ ${isExpanded ? ICONS.chevronDown : ICONS.chevronRight}
385
+ </span>
386
+ <span class="status-dot status-${explanation.status}" style="width:6px;height:6px;margin:0 6px"></span>
387
+ <span class="tree-name" style="color:${explanation.status === "fail" ? COLORS.error : "inherit"}">
388
+ ${explanation.name}
389
+ </span>
390
+ ${explanation.status === "fail" ? `
391
+ <span class="mini-badge fail">!</span>
392
+ ` : ""}
393
+ </div>
394
+ </div>
395
+ ${isExpanded && hasDeps ? `
396
+ <div class="tree-children">
397
+ ${deps.map((dep) => {
398
+ const unit = this.units.find((u) => u._name === dep.name);
399
+ const childKey = key + "-" + dep.name;
400
+ if (unit && "state" in unit) {
401
+ return this.renderTreeNode(unit, depth + 1, childKey);
402
+ } else {
403
+ return `
404
+ <div class="tree-node" style="padding-left:${(depth + 1) * 12}px">
405
+ <div class="tree-row">
406
+ <span class="tree-icon" style="visibility:hidden"></span>
407
+ <span class="status-dot status-ok" style="background:${COLORS.secondaryText}"></span>
408
+ <span class="tree-name" style="opacity:0.7">${dep.name}</span>
409
+ <span class="mini-badge source">S</span>
410
+ </div>
411
+ </div>
412
+ `;
413
+ }
414
+ }).join("")}
415
+ </div>
416
+ ` : ""}
417
+ `;
418
+ }
419
+ // --- HELPERS ---
420
+ renderReason(reason) {
421
+ if (!reason) return "";
422
+ const text = this.formatReasonText(reason);
423
+ const meta = typeof reason === "object" && reason.meta ? reason.meta : null;
424
+ return `
425
+ <div class="reason">
426
+ <strong>${typeof reason === "object" ? reason.code || "ERROR" : "FAIL"}</strong>
427
+ <div>${text}</div>
428
+ ${meta ? `<pre class="meta-block">${JSON.stringify(meta, null, 2)}</pre>` : ""}
429
+ </div>
430
+ `;
431
+ }
432
+ formatReasonText(reason) {
433
+ if (typeof reason === "string") return reason;
434
+ return reason.message || JSON.stringify(reason);
435
+ }
436
+ formatValue(val) {
437
+ if (val === void 0) return "undefined";
438
+ if (val === null) return "null";
439
+ if (typeof val === "function") return "\u0192()";
440
+ if (typeof val === "object") return "{...}";
441
+ return String(val);
442
+ }
443
+ safeStringify(val) {
444
+ try {
445
+ if (val === void 0) return "undefined";
446
+ return JSON.stringify(val);
447
+ } catch {
448
+ return String(val);
449
+ }
450
+ }
451
+ getStatusColor(status) {
452
+ switch (status) {
453
+ case "ok":
454
+ return COLORS.success;
455
+ case "fail":
456
+ return COLORS.error;
457
+ case "pending":
458
+ return COLORS.pending;
459
+ default:
460
+ return COLORS.secondaryText;
461
+ }
462
+ }
463
+ getStyles() {
464
+ return `
162
465
  <style>
163
- :host {
164
- all: initial;
165
- }
466
+ :host { all: initial; font-family: 'Inter', -apple-system, sans-serif; }
467
+ * { box-sizing: border-box; }
468
+
166
469
  .container {
167
470
  position: fixed;
168
- left: ${this.position.x}px;
169
- top: ${this.position.y}px;
170
- width: ${w}px;
471
+ height: auto;
171
472
  z-index: 999999;
172
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
173
- color: white;
174
- user-select: none;
473
+ filter: drop-shadow(0 8px 32px rgba(0,0,0,0.5));
474
+ color: ${COLORS.text};
175
475
  }
476
+
176
477
  .glass {
177
478
  background: ${COLORS.bg};
178
- backdrop-filter: blur(20px);
179
- -webkit-backdrop-filter: blur(20px);
479
+ backdrop-filter: blur(12px);
180
480
  border: 1px solid ${COLORS.border};
181
- border-radius: ${this.isOpen ? "16px" : "30px"};
182
- box-shadow: 0 10px 40px rgba(0,0,0,0.5);
481
+ border-radius: 12px;
183
482
  overflow: hidden;
184
- transition: border-radius 0.2s ease;
185
483
  display: flex;
186
484
  flex-direction: column;
485
+ width: 400px;
486
+ max-height: 80vh;
487
+ transition: width 0.2s, height 0.2s;
187
488
  }
489
+
490
+ /* TOGGLE BUTTON MODE */
188
491
  .toggle-btn {
189
- width: 120px;
190
- height: 45px;
492
+ width: 140px;
493
+ height: 48px;
191
494
  display: flex;
192
495
  align-items: center;
193
496
  justify-content: center;
194
- cursor: grab;
497
+ gap: 10px;
498
+ cursor: pointer;
195
499
  font-weight: 600;
196
500
  font-size: 13px;
501
+ background: rgba(255,255,255,0.03);
502
+ }
503
+ .toggle-btn:hover { background: rgba(255,255,255,0.08); }
504
+
505
+ /* HEADER */
506
+ .header {
507
+ padding: 12px 16px;
508
+ background: rgba(0,0,0,0.2);
509
+ border-bottom: 1px solid ${COLORS.border};
510
+ display: flex;
511
+ justify-content: space-between;
512
+ align-items: center;
513
+ cursor: move;
514
+ flex-shrink: 0;
197
515
  }
198
516
  .dot {
199
517
  width: 8px;
200
518
  height: 8px;
201
519
  border-radius: 50%;
202
520
  background: ${COLORS.accent};
203
- margin-right: 10px;
204
- box-shadow: 0 0 10px #00d2ff;
521
+ box-shadow: 0 0 12px ${COLORS.accentColor};
205
522
  }
206
- .header {
207
- padding: 12px 16px;
208
- background: rgba(0,0,0,0.3);
523
+ .icon-btn { cursor: pointer; opacity: 0.6; padding: 4px; font-size: 18px; line-height: 1; }
524
+ .icon-btn:hover { opacity: 1; color: white; }
525
+
526
+ /* TABS */
527
+ .tabs {
528
+ display: flex;
529
+ background: rgba(0,0,0,0.2);
209
530
  border-bottom: 1px solid ${COLORS.border};
531
+ font-size: 11px;
532
+ font-weight: 600;
533
+ }
534
+ .tab-btn {
535
+ flex: 1;
536
+ padding: 10px;
537
+ text-align: center;
538
+ cursor: pointer;
539
+ opacity: 0.6;
540
+ border-bottom: 2px solid transparent;
210
541
  display: flex;
211
- justify-content: space-between;
212
542
  align-items: center;
213
- cursor: grab;
543
+ justify-content: center;
544
+ gap: 6px;
214
545
  }
215
- .list {
546
+ .tab-btn:hover { opacity: 0.9; background: rgba(255,255,255,0.02); }
547
+ .tab-btn.active { opacity: 1; border-bottom-color: ${COLORS.accentColor}; color: ${COLORS.accentColor}; background: rgba(0,210,255,0.05); }
548
+
549
+ /* CONTENT */
550
+ .content {
216
551
  flex: 1;
217
552
  overflow-y: auto;
218
- max-height: 380px;
219
- padding: 12px;
553
+ overflow-x: hidden;
554
+ min-height: 300px;
220
555
  }
221
- .list::-webkit-scrollbar { width: 5px; }
222
- .list::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }
223
-
556
+ .content::-webkit-scrollbar { width: 6px; }
557
+ .content::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
558
+
559
+ /* INSPECTOR LIST */
560
+ .list { padding: 12px; display: flex; flex-direction: column; gap: 8px; }
561
+ .empty-state { padding: 40px; text-align: center; opacity: 0.4; font-size: 12px; }
562
+
224
563
  .unit-card {
225
- padding: 10px;
226
- margin-bottom: 10px;
227
- border-radius: 10px;
228
564
  background: ${COLORS.cardBg};
229
565
  border: 1px solid ${COLORS.border};
230
- font-size: 12px;
566
+ border-left: 3px solid transparent;
567
+ border-radius: 6px;
568
+ padding: 10px;
569
+ transition: background 0.1s;
231
570
  }
571
+ .unit-card:hover { background: ${COLORS.cardHover}; }
572
+ .source-card { border-left-color: ${COLORS.secondaryText} !important; }
573
+
232
574
  .unit-header {
233
575
  display: flex;
234
576
  justify-content: space-between;
235
- margin-bottom: 5px;
236
- }
237
- .unit-type {
238
- font-size: 9px;
239
- opacity: 0.5;
240
- text-transform: uppercase;
577
+ align-items: center;
578
+ margin-bottom: 8px;
241
579
  }
242
580
  .status-dot {
243
581
  width: 6px;
244
582
  height: 6px;
245
583
  border-radius: 50%;
246
- display: inline-block;
247
- margin-right: 6px;
248
584
  }
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;
585
+ .status-ok { background: ${COLORS.success}; box-shadow: 0 0 6px ${COLORS.success}; }
586
+ .status-fail { background: ${COLORS.error}; box-shadow: 0 0 6px ${COLORS.error}; }
587
+ .status-pending { background: ${COLORS.pending}; box-shadow: 0 0 6px ${COLORS.pending}; }
588
+
589
+ .badg { font-size: 8px; font-weight: 700; padding: 2px 4px; border-radius: 3px; background: rgba(255,255,255,0.1); }
590
+ .type-guard { color: ${COLORS.accentColor}; background: rgba(0,210,255,0.1); }
591
+ .type-source { color: ${COLORS.secondaryText}; }
592
+
593
+ .value-row {
594
+ font-size: 11px;
595
+ font-family: monospace;
596
+ background: rgba(0,0,0,0.2);
597
+ padding: 6px 8px;
598
+ border-radius: 4px;
599
+ display: flex;
600
+ align-items: center;
601
+ justify-content: space-between;
602
+ }
603
+ .value-text { color: ${COLORS.accentColor}; }
604
+ .editable-value { cursor: pointer; border: 1px dashed transparent; }
605
+ .editable-value:hover { border-color: rgba(255,255,255,0.2); background: rgba(255,255,255,0.05); }
606
+ .edit-icon { opacity: 0; transition: opacity 0.2s; }
607
+ .editable-value:hover .edit-icon { opacity: 0.7; }
608
+
609
+ .edit-form { margin: 0; padding: 0; width: 100%; }
610
+ .edit-input {
611
+ width: 100%;
612
+ background: ${COLORS.inputBg};
613
+ border: 1px solid ${COLORS.accentColor};
614
+ color: white;
255
615
  font-family: monospace;
256
- word-break: break-all;
616
+ font-size: 11px;
617
+ padding: 6px;
618
+ border-radius: 4px;
619
+ outline: none;
257
620
  }
621
+
258
622
  .reason {
623
+ margin-top: 8px;
624
+ padding: 8px;
625
+ background: rgba(255, 75, 43, 0.1);
626
+ border: 1px solid rgba(255, 75, 43, 0.2);
627
+ border-radius: 4px;
259
628
  color: ${COLORS.error};
260
- margin-top: 4px;
261
629
  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
630
  }
273
- .explain-toggle {
274
- margin-top: 8px;
275
- font-size: 10px;
276
- color: ${COLORS.secondaryText};
631
+ .pending-reason { color: ${COLORS.pending} !important; background: rgba(253, 187, 45, 0.1); border-color: rgba(253, 187, 45, 0.2); }
632
+ .meta-block { margin: 4px 0 0 0; opacity: 0.7; font-size: 10px; }
633
+
634
+ /* TREE VIEW */
635
+ .tree-view { padding: 12px; font-size: 12px; }
636
+ .tree-node { margin-bottom: 2px; }
637
+ .tree-row {
638
+ display: flex;
639
+ align-items: center;
640
+ padding: 4px 6px;
641
+ border-radius: 4px;
277
642
  cursor: pointer;
278
- text-decoration: underline;
279
- }
280
- .explanation {
281
- margin-top: 8px;
282
- padding-top: 8px;
283
- border-top: 1px dashed ${COLORS.border};
643
+ user-select: none;
284
644
  }
285
- .dep-item {
286
- display: flex;
287
- justify-content: space-between;
288
- padding: 2px 0;
289
- opacity: 0.8;
645
+ .tree-row:hover { background: rgba(255,255,255,0.05); }
646
+ .tree-icon { margin-right: 4px; opacity: 0.5; width: 14px; height: 14px; display: flex; align-items: center; justify-content: center; }
647
+ .tree-name { opacity: 0.9; }
648
+ .mini-badge {
649
+ font-size: 9px; margin-left: auto; padding: 1px 4px; border-radius: 3px;
650
+ font-weight: bold; opacity: 0.6;
290
651
  }
652
+ .mini-badge.fail { background: ${COLORS.error}; color: white; opacity: 1; }
653
+ .mini-badge.source { background: rgba(255,255,255,0.1); }
654
+
291
655
  </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
656
  `;
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
657
  }
368
658
  };
369
659
  if (!customElements.get("pulse-inspector")) {