@pulse-js/tools 0.1.1

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 ADDED
@@ -0,0 +1,55 @@
1
+ # @pulse-js/tools
2
+
3
+ The official visual debugging suite for the Pulse ecosystem. Inspect, monitor, and debug your reactive graph in real-time with a premium, developer-focused UI.
4
+
5
+ ## Features
6
+
7
+ - **Draggable UI**: A floating widget that lives anywhere on your screen.
8
+ - **Quadrant-Aware Anchoring**: Intelligent positioning system. The panel automatically expands from the corner closest to its current position (top-left, bottom-right, etc.), ensuring the UI never jumps or floats awkwardly.
9
+ - **Persistent State**: innovative positioning engine remembers exactly where you left the widget, persisting across page reloads and HMR updates.
10
+ - **Real-Time Inspection**: Visualize the status (OK, FAIL, PENDING) and values of all registered Sources and Guards instantly.
11
+ - **Glassmorphism Design**: A modern, dark-themed aesthetic that fits seamlessly into developer workflows without obstructing functionality.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @pulse-js/tools
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### React Integration
22
+
23
+ Simply inject the `<PulseDevTools />` component anywhere in your root application tree. It renders as a portal and will not affect your layout.
24
+
25
+ ```tsx
26
+ import React from "react";
27
+ import ReactDOM from "react-dom/client";
28
+ import { PulseDevTools } from "@pulse-js/tools";
29
+ import App from "./App";
30
+
31
+ ReactDOM.createRoot(document.getElementById("root")!).render(
32
+ <React.StrictMode>
33
+ <App />
34
+ <PulseDevTools shortcut="Ctrl+D" />
35
+ </React.StrictMode>
36
+ );
37
+ ```
38
+
39
+ ### Configuration Props
40
+
41
+ | Prop | Type | Default | Description |
42
+ | ---------- | -------- | ---------- | -------------------------------------------------------- |
43
+ | `shortcut` | `string` | `'Ctrl+D'` | Keyboard shortcut to toggle the visibility of the panel. |
44
+
45
+ ## Tips
46
+
47
+ - **Naming Matters**: Ensure you provide string names to your Sources and Guards (e.g., `source(val, { name: 'my-source' })`). The DevTools rely on these names to provide meaningful debugging information. Unnamed units will appear but are harder to trace.
48
+ - **Status Indicators**:
49
+ - 🟢 **Green**: OK / Active
50
+ - 🔴 **Red**: Fails (Hover to see semantic failure reasons)
51
+ - 🟡 **Yellow**: Pending (Async operations in flight)
52
+
53
+ ## Architecture
54
+
55
+ The DevTools communicate with the core library via the global `PulseRegistry`. This means it works seamlessly even if your application code is split across multiple modules or bundles, as long as they share the same Pulse instance.
package/dist/index.cjs ADDED
@@ -0,0 +1,349 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.tsx
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ PulseDevTools: () => PulseDevTools
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+ var import_react = require("react");
27
+ var import_core = require("@pulse-js/core");
28
+ var import_react2 = require("@pulse-js/react");
29
+ var import_jsx_runtime = require("react/jsx-runtime");
30
+ var COLORS = {
31
+ bg: "rgba(13, 13, 18, 0.85)",
32
+ border: "rgba(255, 255, 255, 0.1)",
33
+ accent: "linear-gradient(135deg, #00d2ff 0%, #3a7bd5 100%)",
34
+ error: "#ff4b2b",
35
+ success: "#00f260",
36
+ pending: "#fdbb2d",
37
+ text: "#ffffff",
38
+ secondaryText: "#a0a0a0",
39
+ cardBg: "rgba(255, 255, 255, 0.05)"
40
+ };
41
+ var SCROLLBAR_CSS = `
42
+ .pulse-devtools-list::-webkit-scrollbar {
43
+ width: 6px;
44
+ }
45
+ .pulse-devtools-list::-webkit-scrollbar-track {
46
+ background: transparent;
47
+ }
48
+ .pulse-devtools-list::-webkit-scrollbar-thumb {
49
+ background: rgba(255, 255, 255, 0.1);
50
+ border-radius: 10px;
51
+ }
52
+ .pulse-devtools-list::-webkit-scrollbar-thumb:hover {
53
+ background: rgba(255, 255, 255, 0.2);
54
+ }
55
+ `;
56
+ var HeaderStyle = {
57
+ padding: "12px 16px",
58
+ background: "rgba(0,0,0,0.3)",
59
+ borderBottom: `1px solid ${COLORS.border}`,
60
+ display: "flex",
61
+ justifyContent: "space-between",
62
+ alignItems: "center",
63
+ cursor: "grab",
64
+ userSelect: "none"
65
+ };
66
+ var ListStyle = {
67
+ padding: "12px",
68
+ overflowY: "auto",
69
+ maxHeight: "320px",
70
+ // Limits height after ~4-5 items
71
+ flex: "0 1 auto"
72
+ };
73
+ var UnitStyle = (status, isError) => ({
74
+ padding: "10px",
75
+ marginBottom: "10px",
76
+ borderRadius: "10px",
77
+ backgroundColor: COLORS.cardBg,
78
+ border: `1px solid ${isError ? COLORS.error : COLORS.border}`,
79
+ fontSize: "13px",
80
+ transition: "all 0.2s ease",
81
+ boxShadow: isError ? `0 0 10px ${COLORS.error}44` : "none"
82
+ });
83
+ var StatusDot = (status) => ({
84
+ width: "8px",
85
+ height: "8px",
86
+ borderRadius: "50%",
87
+ display: "inline-block",
88
+ marginRight: "8px",
89
+ backgroundColor: status === "ok" ? COLORS.success : status === "fail" ? COLORS.error : COLORS.pending,
90
+ boxShadow: `0 0 8px ${status === "ok" ? COLORS.success : status === "fail" ? COLORS.error : COLORS.pending}`
91
+ });
92
+ var STORAGE_KEY = "pulse-devtools-pos";
93
+ var clamp = (val, min, max) => Math.max(min, Math.min(max, val));
94
+ function useDraggable() {
95
+ const [position, setPositionState] = (0, import_react.useState)(() => {
96
+ try {
97
+ const saved = localStorage.getItem(STORAGE_KEY);
98
+ if (saved) return JSON.parse(saved);
99
+ } catch (e) {
100
+ }
101
+ return { x: window.innerWidth - 140, y: window.innerHeight - 65 };
102
+ });
103
+ const isDragging = (0, import_react.useRef)(false);
104
+ const startMousePos = (0, import_react.useRef)({ x: 0, y: 0 });
105
+ const offset = (0, import_react.useRef)({ x: 0, y: 0 });
106
+ const totalMovement = (0, import_react.useRef)(0);
107
+ const updatePosition = (0, import_react.useCallback)((newPos) => {
108
+ setPositionState(newPos);
109
+ try {
110
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(newPos));
111
+ } catch (e) {
112
+ }
113
+ }, []);
114
+ const onMouseMove = (0, import_react.useCallback)((e, currentW, currentH) => {
115
+ if (!isDragging.current) return;
116
+ totalMovement.current += Math.abs(e.clientX - startMousePos.current.x) + Math.abs(e.clientY - startMousePos.current.y);
117
+ startMousePos.current = { x: e.clientX, y: e.clientY };
118
+ updatePosition({
119
+ x: clamp(e.clientX - offset.current.x, 0, Math.max(0, window.innerWidth - currentW)),
120
+ y: clamp(e.clientY - offset.current.y, 0, Math.max(0, window.innerHeight - currentH))
121
+ });
122
+ }, [updatePosition]);
123
+ const startDragging = (0, import_react.useCallback)((e, w, h) => {
124
+ isDragging.current = true;
125
+ totalMovement.current = 0;
126
+ startMousePos.current = { x: e.clientX, y: e.clientY };
127
+ offset.current = {
128
+ x: e.clientX - position.x,
129
+ y: e.clientY - position.y
130
+ };
131
+ const handleMove = (moveEv) => onMouseMove(moveEv, w, h);
132
+ const handleUp = () => {
133
+ isDragging.current = false;
134
+ document.removeEventListener("mousemove", handleMove);
135
+ document.removeEventListener("mouseup", handleUp);
136
+ };
137
+ document.addEventListener("mousemove", handleMove);
138
+ document.addEventListener("mouseup", handleUp);
139
+ }, [position, onMouseMove]);
140
+ return { position, updatePosition, startDragging, totalMovement };
141
+ }
142
+ var UnitItem = ({ unit }) => {
143
+ const isGuard = "state" in unit;
144
+ const name = unit._name || (isGuard ? "Unnamed Guard" : "Unnamed Source");
145
+ const hasWarning = !unit._name;
146
+ if (isGuard) {
147
+ const state = (0, import_react2.usePulse)(unit);
148
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: UnitStyle(state.status, state.status === "fail"), children: [
149
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "6px" }, children: [
150
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
151
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: StatusDot(state.status) }),
152
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { color: COLORS.text }, children: name })
153
+ ] }),
154
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "10px", color: COLORS.secondaryText }, children: "GUARD" })
155
+ ] }),
156
+ state.status === "ok" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { wordBreak: "break-all", color: COLORS.secondaryText }, children: [
157
+ "Value: ",
158
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: { color: COLORS.success }, children: JSON.stringify(state.value) })
159
+ ] }),
160
+ state.status === "fail" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { color: COLORS.error, fontSize: "11px", marginTop: "4px", background: `${COLORS.error}11`, padding: "4px", borderRadius: "4px" }, children: [
161
+ "Error: ",
162
+ state.reason
163
+ ] }),
164
+ hasWarning && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: "10px", color: COLORS.pending, marginTop: "5px", opacity: 0.8 }, children: "\u26A0\uFE0F Best practice: Give guards a name for better debugging." })
165
+ ] });
166
+ } else {
167
+ const value = (0, import_react2.usePulse)(unit);
168
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: UnitStyle("ok", false), children: [
169
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "6px" }, children: [
170
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
171
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: StatusDot("ok") }),
172
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { color: COLORS.text }, children: name })
173
+ ] }),
174
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "10px", color: COLORS.secondaryText }, children: "SOURCE" })
175
+ ] }),
176
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { wordBreak: "break-all", color: COLORS.secondaryText }, children: [
177
+ "Value: ",
178
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: { color: "#00d2ff" }, children: JSON.stringify(value) })
179
+ ] }),
180
+ hasWarning && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { fontSize: "10px", color: COLORS.pending, marginTop: "5px", opacity: 0.8 }, children: [
181
+ "\u26A0\uFE0F Tip: Name this source in `source(val, ",
182
+ "{ name: '...' }",
183
+ ")`"
184
+ ] })
185
+ ] });
186
+ }
187
+ };
188
+ var PulseDevTools = ({ shortcut = "Ctrl+D" }) => {
189
+ const [units, setUnits] = (0, import_react.useState)(() => import_core.PulseRegistry.getAll());
190
+ const [isOpen, setIsOpen] = (0, import_react.useState)(false);
191
+ const W = 350;
192
+ const H = 450;
193
+ const BTN_W = 120;
194
+ const BTN_H = 45;
195
+ const { position, updatePosition, startDragging, totalMovement } = useDraggable();
196
+ const handleToggle = (0, import_react.useCallback)(() => {
197
+ if (totalMovement.current < 5) {
198
+ setIsOpen((prev) => {
199
+ const next = !prev;
200
+ const isLeft = position.x < window.innerWidth / 2;
201
+ const isTop = position.y < window.innerHeight / 2;
202
+ if (next) {
203
+ updatePosition({
204
+ x: clamp(isLeft ? position.x : position.x + BTN_W - W, 0, window.innerWidth - W),
205
+ y: clamp(isTop ? position.y : position.y + BTN_H - H, 0, window.innerHeight - H)
206
+ });
207
+ } else {
208
+ updatePosition({
209
+ x: clamp(isLeft ? position.x : position.x + W - BTN_W, 0, window.innerWidth - BTN_W),
210
+ y: clamp(isTop ? position.y : position.y + H - BTN_H, 0, window.innerHeight - BTN_H)
211
+ });
212
+ }
213
+ return next;
214
+ });
215
+ }
216
+ }, [totalMovement, updatePosition, position.x, position.y]);
217
+ (0, import_react.useEffect)(() => {
218
+ const style = document.createElement("style");
219
+ style.innerHTML = SCROLLBAR_CSS;
220
+ document.head.appendChild(style);
221
+ return () => {
222
+ document.head.removeChild(style);
223
+ };
224
+ }, []);
225
+ (0, import_react.useEffect)(() => {
226
+ const unsubscribe = import_core.PulseRegistry.onRegister((newUnit) => {
227
+ setUnits((prev) => {
228
+ const name = newUnit._name;
229
+ if (name) {
230
+ const index = prev.findIndex((u) => u._name === name);
231
+ if (index !== -1) {
232
+ const next = [...prev];
233
+ next[index] = newUnit;
234
+ return next;
235
+ }
236
+ }
237
+ return [...prev, newUnit];
238
+ });
239
+ });
240
+ return () => {
241
+ unsubscribe();
242
+ };
243
+ }, []);
244
+ (0, import_react.useEffect)(() => {
245
+ const handleKey = (e) => {
246
+ const parts = shortcut.toLowerCase().split("+");
247
+ const needsCtrl = parts.includes("ctrl");
248
+ const key = parts[parts.length - 1];
249
+ if (needsCtrl && e.ctrlKey && e.key.toLowerCase() === key) {
250
+ e.preventDefault();
251
+ handleToggle();
252
+ }
253
+ };
254
+ window.addEventListener("keydown", handleKey);
255
+ return () => window.removeEventListener("keydown", handleKey);
256
+ }, [shortcut, handleToggle]);
257
+ if (!isOpen) {
258
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
259
+ "button",
260
+ {
261
+ onMouseDown: (e) => startDragging(e, BTN_W, BTN_H),
262
+ onClick: handleToggle,
263
+ title: `Toggle Pulse DevTools (${shortcut})`,
264
+ style: {
265
+ position: "fixed",
266
+ left: `${position.x}px`,
267
+ top: `${position.y}px`,
268
+ width: `${BTN_W}px`,
269
+ height: `${BTN_H}px`,
270
+ padding: 0,
271
+ borderRadius: "30px",
272
+ border: `1px solid ${COLORS.border}`,
273
+ background: COLORS.bg,
274
+ backdropFilter: "blur(15px)",
275
+ color: "white",
276
+ fontWeight: 600,
277
+ cursor: "grab",
278
+ boxShadow: "0 8px 32px rgba(0,0,0,0.4)",
279
+ zIndex: 9999,
280
+ fontSize: "13px",
281
+ display: "flex",
282
+ alignItems: "center",
283
+ justifyContent: "center",
284
+ userSelect: "none"
285
+ },
286
+ children: [
287
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { width: "10px", height: "10px", borderRadius: "50%", background: COLORS.accent, marginRight: "10px", boxShadow: `0 0 10px #00d2ff` } }),
288
+ "Pulse (",
289
+ units.length,
290
+ ")"
291
+ ]
292
+ }
293
+ );
294
+ }
295
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
296
+ "div",
297
+ {
298
+ style: {
299
+ position: "fixed",
300
+ left: `${position.x}px`,
301
+ top: `${position.y}px`,
302
+ width: `${W}px`,
303
+ maxHeight: `${H}px`,
304
+ backgroundColor: COLORS.bg,
305
+ backdropFilter: "blur(15px)",
306
+ border: `1px solid ${COLORS.border}`,
307
+ borderRadius: "16px",
308
+ boxShadow: "0 20px 40px rgba(0,0,0,0.4)",
309
+ display: "flex",
310
+ flexDirection: "column",
311
+ zIndex: 9999,
312
+ overflow: "hidden"
313
+ },
314
+ children: [
315
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: HeaderStyle, onMouseDown: (e) => startDragging(e, W, H), children: [
316
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center" }, children: [
317
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { width: "12px", height: "12px", borderRadius: "4px", background: COLORS.accent, marginRight: "10px", boxShadow: `0 0 8px #00d2ff` } }),
318
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: "white", fontWeight: 600 }, children: "Pulse Inspector" })
319
+ ] }),
320
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
321
+ "button",
322
+ {
323
+ onClick: handleToggle,
324
+ style: { background: "transparent", border: "none", color: COLORS.secondaryText, cursor: "pointer", fontSize: "20px", padding: "0 5px" },
325
+ children: "\xD7"
326
+ }
327
+ )
328
+ ] }),
329
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: ListStyle, className: "pulse-devtools-list", children: units.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { textAlign: "center", color: COLORS.secondaryText, marginTop: "40px", fontSize: "14px" }, children: [
330
+ "No reactive units detected.",
331
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("br", {}),
332
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "11px", opacity: 0.6 }, children: "Use source() or guard() to begin." })
333
+ ] }) : units.map((u, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(UnitItem, { unit: u }, u._name || i)) }),
334
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: "10px", background: "rgba(0,0,0,0.2)", display: "flex", justifyContent: "space-between", fontSize: "10px", color: COLORS.secondaryText }, children: [
335
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "v0.1.0" }),
336
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
337
+ "Drag header to move \u2022 ",
338
+ shortcut,
339
+ " to toggle"
340
+ ] })
341
+ ] })
342
+ ]
343
+ }
344
+ );
345
+ };
346
+ // Annotate the CommonJS export names for ESM import in node:
347
+ 0 && (module.exports = {
348
+ PulseDevTools
349
+ });
@@ -0,0 +1,5 @@
1
+ declare const PulseDevTools: ({ shortcut }: {
2
+ shortcut?: string;
3
+ }) => any;
4
+
5
+ export { PulseDevTools };
@@ -0,0 +1,5 @@
1
+ declare const PulseDevTools: ({ shortcut }: {
2
+ shortcut?: string;
3
+ }) => any;
4
+
5
+ export { PulseDevTools };
package/dist/index.js ADDED
@@ -0,0 +1,324 @@
1
+ // src/index.tsx
2
+ import { useState, useEffect, useRef, useCallback } from "react";
3
+ import { PulseRegistry } from "@pulse-js/core";
4
+ import { usePulse } from "@pulse-js/react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ var COLORS = {
7
+ bg: "rgba(13, 13, 18, 0.85)",
8
+ border: "rgba(255, 255, 255, 0.1)",
9
+ accent: "linear-gradient(135deg, #00d2ff 0%, #3a7bd5 100%)",
10
+ error: "#ff4b2b",
11
+ success: "#00f260",
12
+ pending: "#fdbb2d",
13
+ text: "#ffffff",
14
+ secondaryText: "#a0a0a0",
15
+ cardBg: "rgba(255, 255, 255, 0.05)"
16
+ };
17
+ var SCROLLBAR_CSS = `
18
+ .pulse-devtools-list::-webkit-scrollbar {
19
+ width: 6px;
20
+ }
21
+ .pulse-devtools-list::-webkit-scrollbar-track {
22
+ background: transparent;
23
+ }
24
+ .pulse-devtools-list::-webkit-scrollbar-thumb {
25
+ background: rgba(255, 255, 255, 0.1);
26
+ border-radius: 10px;
27
+ }
28
+ .pulse-devtools-list::-webkit-scrollbar-thumb:hover {
29
+ background: rgba(255, 255, 255, 0.2);
30
+ }
31
+ `;
32
+ var HeaderStyle = {
33
+ padding: "12px 16px",
34
+ background: "rgba(0,0,0,0.3)",
35
+ borderBottom: `1px solid ${COLORS.border}`,
36
+ display: "flex",
37
+ justifyContent: "space-between",
38
+ alignItems: "center",
39
+ cursor: "grab",
40
+ userSelect: "none"
41
+ };
42
+ var ListStyle = {
43
+ padding: "12px",
44
+ overflowY: "auto",
45
+ maxHeight: "320px",
46
+ // Limits height after ~4-5 items
47
+ flex: "0 1 auto"
48
+ };
49
+ var UnitStyle = (status, isError) => ({
50
+ padding: "10px",
51
+ marginBottom: "10px",
52
+ borderRadius: "10px",
53
+ backgroundColor: COLORS.cardBg,
54
+ border: `1px solid ${isError ? COLORS.error : COLORS.border}`,
55
+ fontSize: "13px",
56
+ transition: "all 0.2s ease",
57
+ boxShadow: isError ? `0 0 10px ${COLORS.error}44` : "none"
58
+ });
59
+ var StatusDot = (status) => ({
60
+ width: "8px",
61
+ height: "8px",
62
+ borderRadius: "50%",
63
+ display: "inline-block",
64
+ marginRight: "8px",
65
+ backgroundColor: status === "ok" ? COLORS.success : status === "fail" ? COLORS.error : COLORS.pending,
66
+ boxShadow: `0 0 8px ${status === "ok" ? COLORS.success : status === "fail" ? COLORS.error : COLORS.pending}`
67
+ });
68
+ var STORAGE_KEY = "pulse-devtools-pos";
69
+ var clamp = (val, min, max) => Math.max(min, Math.min(max, val));
70
+ function useDraggable() {
71
+ const [position, setPositionState] = useState(() => {
72
+ try {
73
+ const saved = localStorage.getItem(STORAGE_KEY);
74
+ if (saved) return JSON.parse(saved);
75
+ } catch (e) {
76
+ }
77
+ return { x: window.innerWidth - 140, y: window.innerHeight - 65 };
78
+ });
79
+ const isDragging = useRef(false);
80
+ const startMousePos = useRef({ x: 0, y: 0 });
81
+ const offset = useRef({ x: 0, y: 0 });
82
+ const totalMovement = useRef(0);
83
+ const updatePosition = useCallback((newPos) => {
84
+ setPositionState(newPos);
85
+ try {
86
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(newPos));
87
+ } catch (e) {
88
+ }
89
+ }, []);
90
+ const onMouseMove = useCallback((e, currentW, currentH) => {
91
+ if (!isDragging.current) return;
92
+ totalMovement.current += Math.abs(e.clientX - startMousePos.current.x) + Math.abs(e.clientY - startMousePos.current.y);
93
+ startMousePos.current = { x: e.clientX, y: e.clientY };
94
+ updatePosition({
95
+ x: clamp(e.clientX - offset.current.x, 0, Math.max(0, window.innerWidth - currentW)),
96
+ y: clamp(e.clientY - offset.current.y, 0, Math.max(0, window.innerHeight - currentH))
97
+ });
98
+ }, [updatePosition]);
99
+ const startDragging = useCallback((e, w, h) => {
100
+ isDragging.current = true;
101
+ totalMovement.current = 0;
102
+ startMousePos.current = { x: e.clientX, y: e.clientY };
103
+ offset.current = {
104
+ x: e.clientX - position.x,
105
+ y: e.clientY - position.y
106
+ };
107
+ const handleMove = (moveEv) => onMouseMove(moveEv, w, h);
108
+ const handleUp = () => {
109
+ isDragging.current = false;
110
+ document.removeEventListener("mousemove", handleMove);
111
+ document.removeEventListener("mouseup", handleUp);
112
+ };
113
+ document.addEventListener("mousemove", handleMove);
114
+ document.addEventListener("mouseup", handleUp);
115
+ }, [position, onMouseMove]);
116
+ return { position, updatePosition, startDragging, totalMovement };
117
+ }
118
+ var UnitItem = ({ unit }) => {
119
+ const isGuard = "state" in unit;
120
+ const name = unit._name || (isGuard ? "Unnamed Guard" : "Unnamed Source");
121
+ const hasWarning = !unit._name;
122
+ if (isGuard) {
123
+ const state = usePulse(unit);
124
+ return /* @__PURE__ */ jsxs("div", { style: UnitStyle(state.status, state.status === "fail"), children: [
125
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "6px" }, children: [
126
+ /* @__PURE__ */ jsxs("span", { children: [
127
+ /* @__PURE__ */ jsx("span", { style: StatusDot(state.status) }),
128
+ /* @__PURE__ */ jsx("strong", { style: { color: COLORS.text }, children: name })
129
+ ] }),
130
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "10px", color: COLORS.secondaryText }, children: "GUARD" })
131
+ ] }),
132
+ state.status === "ok" && /* @__PURE__ */ jsxs("div", { style: { wordBreak: "break-all", color: COLORS.secondaryText }, children: [
133
+ "Value: ",
134
+ /* @__PURE__ */ jsx("code", { style: { color: COLORS.success }, children: JSON.stringify(state.value) })
135
+ ] }),
136
+ state.status === "fail" && /* @__PURE__ */ jsxs("div", { style: { color: COLORS.error, fontSize: "11px", marginTop: "4px", background: `${COLORS.error}11`, padding: "4px", borderRadius: "4px" }, children: [
137
+ "Error: ",
138
+ state.reason
139
+ ] }),
140
+ hasWarning && /* @__PURE__ */ jsx("div", { style: { fontSize: "10px", color: COLORS.pending, marginTop: "5px", opacity: 0.8 }, children: "\u26A0\uFE0F Best practice: Give guards a name for better debugging." })
141
+ ] });
142
+ } else {
143
+ const value = usePulse(unit);
144
+ return /* @__PURE__ */ jsxs("div", { style: UnitStyle("ok", false), children: [
145
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "6px" }, children: [
146
+ /* @__PURE__ */ jsxs("span", { children: [
147
+ /* @__PURE__ */ jsx("span", { style: StatusDot("ok") }),
148
+ /* @__PURE__ */ jsx("strong", { style: { color: COLORS.text }, children: name })
149
+ ] }),
150
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "10px", color: COLORS.secondaryText }, children: "SOURCE" })
151
+ ] }),
152
+ /* @__PURE__ */ jsxs("div", { style: { wordBreak: "break-all", color: COLORS.secondaryText }, children: [
153
+ "Value: ",
154
+ /* @__PURE__ */ jsx("code", { style: { color: "#00d2ff" }, children: JSON.stringify(value) })
155
+ ] }),
156
+ hasWarning && /* @__PURE__ */ jsxs("div", { style: { fontSize: "10px", color: COLORS.pending, marginTop: "5px", opacity: 0.8 }, children: [
157
+ "\u26A0\uFE0F Tip: Name this source in `source(val, ",
158
+ "{ name: '...' }",
159
+ ")`"
160
+ ] })
161
+ ] });
162
+ }
163
+ };
164
+ var PulseDevTools = ({ shortcut = "Ctrl+D" }) => {
165
+ const [units, setUnits] = useState(() => PulseRegistry.getAll());
166
+ const [isOpen, setIsOpen] = useState(false);
167
+ const W = 350;
168
+ const H = 450;
169
+ const BTN_W = 120;
170
+ const BTN_H = 45;
171
+ const { position, updatePosition, startDragging, totalMovement } = useDraggable();
172
+ const handleToggle = useCallback(() => {
173
+ if (totalMovement.current < 5) {
174
+ setIsOpen((prev) => {
175
+ const next = !prev;
176
+ const isLeft = position.x < window.innerWidth / 2;
177
+ const isTop = position.y < window.innerHeight / 2;
178
+ if (next) {
179
+ updatePosition({
180
+ x: clamp(isLeft ? position.x : position.x + BTN_W - W, 0, window.innerWidth - W),
181
+ y: clamp(isTop ? position.y : position.y + BTN_H - H, 0, window.innerHeight - H)
182
+ });
183
+ } else {
184
+ updatePosition({
185
+ x: clamp(isLeft ? position.x : position.x + W - BTN_W, 0, window.innerWidth - BTN_W),
186
+ y: clamp(isTop ? position.y : position.y + H - BTN_H, 0, window.innerHeight - BTN_H)
187
+ });
188
+ }
189
+ return next;
190
+ });
191
+ }
192
+ }, [totalMovement, updatePosition, position.x, position.y]);
193
+ useEffect(() => {
194
+ const style = document.createElement("style");
195
+ style.innerHTML = SCROLLBAR_CSS;
196
+ document.head.appendChild(style);
197
+ return () => {
198
+ document.head.removeChild(style);
199
+ };
200
+ }, []);
201
+ useEffect(() => {
202
+ const unsubscribe = PulseRegistry.onRegister((newUnit) => {
203
+ setUnits((prev) => {
204
+ const name = newUnit._name;
205
+ if (name) {
206
+ const index = prev.findIndex((u) => u._name === name);
207
+ if (index !== -1) {
208
+ const next = [...prev];
209
+ next[index] = newUnit;
210
+ return next;
211
+ }
212
+ }
213
+ return [...prev, newUnit];
214
+ });
215
+ });
216
+ return () => {
217
+ unsubscribe();
218
+ };
219
+ }, []);
220
+ useEffect(() => {
221
+ const handleKey = (e) => {
222
+ const parts = shortcut.toLowerCase().split("+");
223
+ const needsCtrl = parts.includes("ctrl");
224
+ const key = parts[parts.length - 1];
225
+ if (needsCtrl && e.ctrlKey && e.key.toLowerCase() === key) {
226
+ e.preventDefault();
227
+ handleToggle();
228
+ }
229
+ };
230
+ window.addEventListener("keydown", handleKey);
231
+ return () => window.removeEventListener("keydown", handleKey);
232
+ }, [shortcut, handleToggle]);
233
+ if (!isOpen) {
234
+ return /* @__PURE__ */ jsxs(
235
+ "button",
236
+ {
237
+ onMouseDown: (e) => startDragging(e, BTN_W, BTN_H),
238
+ onClick: handleToggle,
239
+ title: `Toggle Pulse DevTools (${shortcut})`,
240
+ style: {
241
+ position: "fixed",
242
+ left: `${position.x}px`,
243
+ top: `${position.y}px`,
244
+ width: `${BTN_W}px`,
245
+ height: `${BTN_H}px`,
246
+ padding: 0,
247
+ borderRadius: "30px",
248
+ border: `1px solid ${COLORS.border}`,
249
+ background: COLORS.bg,
250
+ backdropFilter: "blur(15px)",
251
+ color: "white",
252
+ fontWeight: 600,
253
+ cursor: "grab",
254
+ boxShadow: "0 8px 32px rgba(0,0,0,0.4)",
255
+ zIndex: 9999,
256
+ fontSize: "13px",
257
+ display: "flex",
258
+ alignItems: "center",
259
+ justifyContent: "center",
260
+ userSelect: "none"
261
+ },
262
+ children: [
263
+ /* @__PURE__ */ jsx("div", { style: { width: "10px", height: "10px", borderRadius: "50%", background: COLORS.accent, marginRight: "10px", boxShadow: `0 0 10px #00d2ff` } }),
264
+ "Pulse (",
265
+ units.length,
266
+ ")"
267
+ ]
268
+ }
269
+ );
270
+ }
271
+ return /* @__PURE__ */ jsxs(
272
+ "div",
273
+ {
274
+ style: {
275
+ position: "fixed",
276
+ left: `${position.x}px`,
277
+ top: `${position.y}px`,
278
+ width: `${W}px`,
279
+ maxHeight: `${H}px`,
280
+ backgroundColor: COLORS.bg,
281
+ backdropFilter: "blur(15px)",
282
+ border: `1px solid ${COLORS.border}`,
283
+ borderRadius: "16px",
284
+ boxShadow: "0 20px 40px rgba(0,0,0,0.4)",
285
+ display: "flex",
286
+ flexDirection: "column",
287
+ zIndex: 9999,
288
+ overflow: "hidden"
289
+ },
290
+ children: [
291
+ /* @__PURE__ */ jsxs("div", { style: HeaderStyle, onMouseDown: (e) => startDragging(e, W, H), children: [
292
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center" }, children: [
293
+ /* @__PURE__ */ jsx("div", { style: { width: "12px", height: "12px", borderRadius: "4px", background: COLORS.accent, marginRight: "10px", boxShadow: `0 0 8px #00d2ff` } }),
294
+ /* @__PURE__ */ jsx("span", { style: { color: "white", fontWeight: 600 }, children: "Pulse Inspector" })
295
+ ] }),
296
+ /* @__PURE__ */ jsx(
297
+ "button",
298
+ {
299
+ onClick: handleToggle,
300
+ style: { background: "transparent", border: "none", color: COLORS.secondaryText, cursor: "pointer", fontSize: "20px", padding: "0 5px" },
301
+ children: "\xD7"
302
+ }
303
+ )
304
+ ] }),
305
+ /* @__PURE__ */ jsx("div", { style: ListStyle, className: "pulse-devtools-list", children: units.length === 0 ? /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", color: COLORS.secondaryText, marginTop: "40px", fontSize: "14px" }, children: [
306
+ "No reactive units detected.",
307
+ /* @__PURE__ */ jsx("br", {}),
308
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "11px", opacity: 0.6 }, children: "Use source() or guard() to begin." })
309
+ ] }) : units.map((u, i) => /* @__PURE__ */ jsx(UnitItem, { unit: u }, u._name || i)) }),
310
+ /* @__PURE__ */ jsxs("div", { style: { padding: "10px", background: "rgba(0,0,0,0.2)", display: "flex", justifyContent: "space-between", fontSize: "10px", color: COLORS.secondaryText }, children: [
311
+ /* @__PURE__ */ jsx("span", { children: "v0.1.0" }),
312
+ /* @__PURE__ */ jsxs("span", { children: [
313
+ "Drag header to move \u2022 ",
314
+ shortcut,
315
+ " to toggle"
316
+ ] })
317
+ ] })
318
+ ]
319
+ }
320
+ );
321
+ };
322
+ export {
323
+ PulseDevTools
324
+ };
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@pulse-js/tools",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "main": "dist/index.cjs",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "bun x tsup src/index.tsx --format esm,cjs --dts --clean --external react,react-dom,@pulse-js/core,@pulse-js/react",
13
+ "lint": "bun x tsc --noEmit"
14
+ },
15
+ "peerDependencies": {
16
+ "react": ">=18.0.0",
17
+ "react-dom": ">=18.0.0"
18
+ },
19
+ "dependencies": {
20
+ "@pulse-js/core": "^0.1.1",
21
+ "@pulse-js/react": "^0.1.1"
22
+ }
23
+ }