@couch-kit/devtools 0.2.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # @couch-kit/devtools
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#21](https://github.com/faluciano/react-native-couch-kit/pull/21) [`f1e836d`](https://github.com/faluciano/react-native-couch-kit/commit/f1e836d8f17821f0da20db7c3c552552c9b22133) Thanks [@faluciano](https://github.com/faluciano)! - Initial release of `@couch-kit/devtools` package with `DebugOverlay` React component. Provides a collapsible debug overlay showing action log, state tree, connection status, and RTT.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [[`f1e836d`](https://github.com/faluciano/react-native-couch-kit/commit/f1e836d8f17821f0da20db7c3c552552c9b22133)]:
12
+ - @couch-kit/client@0.8.0
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # @couch-kit/devtools
2
+
3
+ Developer tooling and debug overlay for Couch Kit games.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @couch-kit/devtools
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```tsx
14
+ import { DebugOverlay } from "@couch-kit/devtools";
15
+
16
+ function App() {
17
+ return (
18
+ <div>
19
+ <Game />
20
+ <DebugOverlay />
21
+ </div>
22
+ );
23
+ }
24
+ ```
package/dist/index.js ADDED
@@ -0,0 +1,372 @@
1
+ // src/DebugOverlay.tsx
2
+ import { useState, useCallback, useRef } from "react";
3
+ import { jsxDEV } from "react/jsx-dev-runtime";
4
+ var STATUS_COLORS = {
5
+ connected: "#4ade80",
6
+ connecting: "#facc15",
7
+ reconnecting: "#facc15",
8
+ disconnected: "#f87171",
9
+ error: "#f87171"
10
+ };
11
+ function formatTimestamp(ts) {
12
+ const date = new Date(ts);
13
+ return date.toLocaleTimeString("en-US", {
14
+ hour12: false,
15
+ hour: "2-digit",
16
+ minute: "2-digit",
17
+ second: "2-digit"
18
+ }) + "." + String(date.getMilliseconds()).padStart(3, "0");
19
+ }
20
+ function JsonTree({ data, depth = 0 }) {
21
+ if (data === null || data === undefined) {
22
+ return /* @__PURE__ */ jsxDEV("span", {
23
+ style: { color: "#9ca3af" },
24
+ children: String(data)
25
+ }, undefined, false, undefined, this);
26
+ }
27
+ if (typeof data === "boolean") {
28
+ return /* @__PURE__ */ jsxDEV("span", {
29
+ style: { color: "#c084fc" },
30
+ children: String(data)
31
+ }, undefined, false, undefined, this);
32
+ }
33
+ if (typeof data === "number") {
34
+ return /* @__PURE__ */ jsxDEV("span", {
35
+ style: { color: "#60a5fa" },
36
+ children: data
37
+ }, undefined, false, undefined, this);
38
+ }
39
+ if (typeof data === "string") {
40
+ return /* @__PURE__ */ jsxDEV("span", {
41
+ style: { color: "#4ade80" },
42
+ children: [
43
+ '"',
44
+ data,
45
+ '"'
46
+ ]
47
+ }, undefined, true, undefined, this);
48
+ }
49
+ if (Array.isArray(data)) {
50
+ if (data.length === 0)
51
+ return /* @__PURE__ */ jsxDEV("span", {
52
+ children: "[]"
53
+ }, undefined, false, undefined, this);
54
+ return /* @__PURE__ */ jsxDEV("span", {
55
+ children: [
56
+ `[
57
+ `,
58
+ data.map((item, i) => /* @__PURE__ */ jsxDEV("span", {
59
+ children: [
60
+ " ".repeat(depth + 1),
61
+ /* @__PURE__ */ jsxDEV(JsonTree, {
62
+ data: item,
63
+ depth: depth + 1
64
+ }, undefined, false, undefined, this),
65
+ i < data.length - 1 ? "," : "",
66
+ `
67
+ `
68
+ ]
69
+ }, i, true, undefined, this)),
70
+ " ".repeat(depth),
71
+ "]"
72
+ ]
73
+ }, undefined, true, undefined, this);
74
+ }
75
+ if (typeof data === "object") {
76
+ const entries = Object.entries(data);
77
+ if (entries.length === 0)
78
+ return /* @__PURE__ */ jsxDEV("span", {
79
+ children: "{}"
80
+ }, undefined, false, undefined, this);
81
+ return /* @__PURE__ */ jsxDEV("span", {
82
+ children: [
83
+ `{
84
+ `,
85
+ entries.map(([key, value], i) => /* @__PURE__ */ jsxDEV("span", {
86
+ children: [
87
+ " ".repeat(depth + 1),
88
+ /* @__PURE__ */ jsxDEV("span", {
89
+ style: { color: "#93c5fd" },
90
+ children: key
91
+ }, undefined, false, undefined, this),
92
+ ": ",
93
+ /* @__PURE__ */ jsxDEV(JsonTree, {
94
+ data: value,
95
+ depth: depth + 1
96
+ }, undefined, false, undefined, this),
97
+ i < entries.length - 1 ? "," : "",
98
+ `
99
+ `
100
+ ]
101
+ }, key, true, undefined, this)),
102
+ " ".repeat(depth),
103
+ "}"
104
+ ]
105
+ }, undefined, true, undefined, this);
106
+ }
107
+ return /* @__PURE__ */ jsxDEV("span", {
108
+ children: String(data)
109
+ }, undefined, false, undefined, this);
110
+ }
111
+ function DebugOverlay({
112
+ data,
113
+ defaultCollapsed = true,
114
+ position = "bottom-right",
115
+ maxHeight = 400
116
+ }) {
117
+ const [collapsed, setCollapsed] = useState(defaultCollapsed);
118
+ const [activeTab, setActiveTab] = useState("actions");
119
+ const tapCountRef = useRef(0);
120
+ const tapTimerRef = useRef(null);
121
+ const handleToggle = useCallback(() => {
122
+ tapCountRef.current++;
123
+ if (tapTimerRef.current) {
124
+ clearTimeout(tapTimerRef.current);
125
+ }
126
+ tapTimerRef.current = setTimeout(() => {
127
+ tapCountRef.current = 0;
128
+ }, 300);
129
+ setCollapsed((prev) => !prev);
130
+ }, []);
131
+ if (!data.enabled)
132
+ return null;
133
+ const positionStyles = {
134
+ position: "fixed",
135
+ zIndex: 99999,
136
+ ...position.includes("top") ? { top: 8 } : { bottom: 8 },
137
+ ...position.includes("left") ? { left: 8 } : { right: 8 }
138
+ };
139
+ const statusColor = STATUS_COLORS[data.connectionStatus] ?? "#9ca3af";
140
+ const panelStyle = {
141
+ ...positionStyles,
142
+ fontFamily: "'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace",
143
+ fontSize: 11,
144
+ lineHeight: 1.4,
145
+ color: "#e5e7eb",
146
+ backgroundColor: "rgba(17, 24, 39, 0.95)",
147
+ border: "1px solid rgba(75, 85, 99, 0.6)",
148
+ borderRadius: 8,
149
+ overflow: "hidden",
150
+ width: collapsed ? "auto" : 360,
151
+ backdropFilter: "blur(8px)",
152
+ boxShadow: "0 4px 24px rgba(0, 0, 0, 0.4)"
153
+ };
154
+ const buttonStyle = {
155
+ background: "none",
156
+ border: "none",
157
+ color: "#e5e7eb",
158
+ cursor: "pointer",
159
+ padding: "6px 10px",
160
+ fontSize: 11,
161
+ fontFamily: "inherit"
162
+ };
163
+ const tabStyle = (isActive) => ({
164
+ ...buttonStyle,
165
+ borderBottom: isActive ? "2px solid #60a5fa" : "2px solid transparent",
166
+ color: isActive ? "#60a5fa" : "#9ca3af",
167
+ paddingBottom: 4
168
+ });
169
+ if (collapsed) {
170
+ return /* @__PURE__ */ jsxDEV("div", {
171
+ style: positionStyles,
172
+ children: /* @__PURE__ */ jsxDEV("button", {
173
+ onClick: handleToggle,
174
+ style: {
175
+ ...buttonStyle,
176
+ backgroundColor: "rgba(17, 24, 39, 0.95)",
177
+ border: "1px solid rgba(75, 85, 99, 0.6)",
178
+ borderRadius: 8,
179
+ display: "flex",
180
+ alignItems: "center",
181
+ gap: 6,
182
+ padding: "6px 12px",
183
+ backdropFilter: "blur(8px)",
184
+ boxShadow: "0 2px 12px rgba(0, 0, 0, 0.3)"
185
+ },
186
+ children: [
187
+ /* @__PURE__ */ jsxDEV("span", {
188
+ style: {
189
+ width: 8,
190
+ height: 8,
191
+ borderRadius: "50%",
192
+ backgroundColor: statusColor,
193
+ display: "inline-block"
194
+ }
195
+ }, undefined, false, undefined, this),
196
+ /* @__PURE__ */ jsxDEV("span", {
197
+ children: "Debug"
198
+ }, undefined, false, undefined, this),
199
+ data.rtt !== null && /* @__PURE__ */ jsxDEV("span", {
200
+ style: { color: "#9ca3af" },
201
+ children: [
202
+ data.rtt,
203
+ "ms"
204
+ ]
205
+ }, undefined, true, undefined, this)
206
+ ]
207
+ }, undefined, true, undefined, this)
208
+ }, undefined, false, undefined, this);
209
+ }
210
+ const latestState = data.stateHistory.length > 0 ? data.stateHistory[data.stateHistory.length - 1]?.state : null;
211
+ return /* @__PURE__ */ jsxDEV("div", {
212
+ style: panelStyle,
213
+ children: [
214
+ /* @__PURE__ */ jsxDEV("div", {
215
+ style: {
216
+ display: "flex",
217
+ alignItems: "center",
218
+ justifyContent: "space-between",
219
+ padding: "6px 10px",
220
+ borderBottom: "1px solid rgba(75, 85, 99, 0.4)"
221
+ },
222
+ children: [
223
+ /* @__PURE__ */ jsxDEV("div", {
224
+ style: { display: "flex", alignItems: "center", gap: 6 },
225
+ children: [
226
+ /* @__PURE__ */ jsxDEV("span", {
227
+ style: {
228
+ width: 8,
229
+ height: 8,
230
+ borderRadius: "50%",
231
+ backgroundColor: statusColor,
232
+ display: "inline-block"
233
+ }
234
+ }, undefined, false, undefined, this),
235
+ /* @__PURE__ */ jsxDEV("span", {
236
+ style: { fontWeight: 600 },
237
+ children: "Debug"
238
+ }, undefined, false, undefined, this),
239
+ /* @__PURE__ */ jsxDEV("span", {
240
+ style: { color: "#9ca3af" },
241
+ children: [
242
+ data.connectionStatus,
243
+ data.rtt !== null ? ` · ${data.rtt}ms` : ""
244
+ ]
245
+ }, undefined, true, undefined, this)
246
+ ]
247
+ }, undefined, true, undefined, this),
248
+ /* @__PURE__ */ jsxDEV("div", {
249
+ style: { display: "flex", gap: 4 },
250
+ children: [
251
+ /* @__PURE__ */ jsxDEV("button", {
252
+ onClick: data.clearHistory,
253
+ style: buttonStyle,
254
+ title: "Clear",
255
+ children: "✕"
256
+ }, undefined, false, undefined, this),
257
+ /* @__PURE__ */ jsxDEV("button", {
258
+ onClick: handleToggle,
259
+ style: buttonStyle,
260
+ title: "Collapse",
261
+ children: "▾"
262
+ }, undefined, false, undefined, this)
263
+ ]
264
+ }, undefined, true, undefined, this)
265
+ ]
266
+ }, undefined, true, undefined, this),
267
+ /* @__PURE__ */ jsxDEV("div", {
268
+ style: {
269
+ display: "flex",
270
+ gap: 0,
271
+ borderBottom: "1px solid rgba(75, 85, 99, 0.4)",
272
+ padding: "0 6px"
273
+ },
274
+ children: [
275
+ /* @__PURE__ */ jsxDEV("button", {
276
+ onClick: () => setActiveTab("actions"),
277
+ style: tabStyle(activeTab === "actions"),
278
+ children: [
279
+ "Actions (",
280
+ data.actionLog.length,
281
+ ")"
282
+ ]
283
+ }, undefined, true, undefined, this),
284
+ /* @__PURE__ */ jsxDEV("button", {
285
+ onClick: () => setActiveTab("state"),
286
+ style: tabStyle(activeTab === "state"),
287
+ children: "State"
288
+ }, undefined, false, undefined, this)
289
+ ]
290
+ }, undefined, true, undefined, this),
291
+ /* @__PURE__ */ jsxDEV("div", {
292
+ style: {
293
+ maxHeight,
294
+ overflowY: "auto",
295
+ padding: 8
296
+ },
297
+ children: [
298
+ activeTab === "actions" && /* @__PURE__ */ jsxDEV("div", {
299
+ children: data.actionLog.length === 0 ? /* @__PURE__ */ jsxDEV("div", {
300
+ style: {
301
+ color: "#6b7280",
302
+ textAlign: "center",
303
+ padding: 16
304
+ },
305
+ children: "No actions recorded"
306
+ }, undefined, false, undefined, this) : data.actionLog.slice().reverse().map((entry) => /* @__PURE__ */ jsxDEV("div", {
307
+ style: {
308
+ padding: "4px 6px",
309
+ borderBottom: "1px solid rgba(75, 85, 99, 0.2)",
310
+ display: "flex",
311
+ flexDirection: "column",
312
+ gap: 2
313
+ },
314
+ children: [
315
+ /* @__PURE__ */ jsxDEV("div", {
316
+ style: {
317
+ display: "flex",
318
+ justifyContent: "space-between",
319
+ alignItems: "center"
320
+ },
321
+ children: [
322
+ /* @__PURE__ */ jsxDEV("span", {
323
+ style: {
324
+ color: entry.source === "local" ? "#60a5fa" : "#4ade80",
325
+ fontSize: 10,
326
+ textTransform: "uppercase"
327
+ },
328
+ children: entry.source
329
+ }, undefined, false, undefined, this),
330
+ /* @__PURE__ */ jsxDEV("span", {
331
+ style: { color: "#6b7280", fontSize: 10 },
332
+ children: formatTimestamp(entry.timestamp)
333
+ }, undefined, false, undefined, this)
334
+ ]
335
+ }, undefined, true, undefined, this),
336
+ /* @__PURE__ */ jsxDEV("pre", {
337
+ style: {
338
+ margin: 0,
339
+ fontSize: 10,
340
+ color: "#d1d5db",
341
+ whiteSpace: "pre-wrap",
342
+ wordBreak: "break-all"
343
+ },
344
+ children: /* @__PURE__ */ jsxDEV(JsonTree, {
345
+ data: entry.action
346
+ }, undefined, false, undefined, this)
347
+ }, undefined, false, undefined, this)
348
+ ]
349
+ }, entry.id, true, undefined, this))
350
+ }, undefined, false, undefined, this),
351
+ activeTab === "state" && /* @__PURE__ */ jsxDEV("pre", {
352
+ style: {
353
+ margin: 0,
354
+ fontSize: 10,
355
+ whiteSpace: "pre-wrap",
356
+ wordBreak: "break-all"
357
+ },
358
+ children: latestState !== null ? /* @__PURE__ */ jsxDEV(JsonTree, {
359
+ data: latestState
360
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV("span", {
361
+ style: { color: "#6b7280" },
362
+ children: "No state captured"
363
+ }, undefined, false, undefined, this)
364
+ }, undefined, false, undefined, this)
365
+ ]
366
+ }, undefined, true, undefined, this)
367
+ ]
368
+ }, undefined, true, undefined, this);
369
+ }
370
+ export {
371
+ DebugOverlay
372
+ };
@@ -0,0 +1,13 @@
1
+ import type { DebugPanelData } from "@couch-kit/client";
2
+ export interface DebugOverlayProps {
3
+ /** Debug panel data from useDebugPanel hook */
4
+ data: DebugPanelData;
5
+ /** Initial collapsed state */
6
+ defaultCollapsed?: boolean;
7
+ /** Position on screen */
8
+ position?: "top-left" | "top-right" | "bottom-left" | "bottom-right";
9
+ /** Maximum height of the panel */
10
+ maxHeight?: number;
11
+ }
12
+ export declare function DebugOverlay({ data, defaultCollapsed, position, maxHeight, }: DebugOverlayProps): import("react/jsx-runtime").JSX.Element | null;
13
+ //# sourceMappingURL=DebugOverlay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DebugOverlay.d.ts","sourceRoot":"","sources":["../src/DebugOverlay.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD,MAAM,WAAW,iBAAiB;IAChC,+CAA+C;IAC/C,IAAI,EAAE,cAAc,CAAC;IACrB,8BAA8B;IAC9B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,yBAAyB;IACzB,QAAQ,CAAC,EAAE,UAAU,GAAG,WAAW,GAAG,aAAa,GAAG,cAAc,CAAC;IACrE,kCAAkC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAqFD,wBAAgB,YAAY,CAAC,EAC3B,IAAI,EACJ,gBAAuB,EACvB,QAAyB,EACzB,SAAe,GAChB,EAAE,iBAAiB,kDAmQnB"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { DebugOverlay } from "./DebugOverlay";
2
+ export type { DebugOverlayProps } from "./DebugOverlay";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@couch-kit/devtools",
3
+ "version": "0.2.0",
4
+ "description": "Developer tooling and debug overlay for Couch Kit games",
5
+ "main": "./dist/index.js",
6
+ "types": "./lib/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./lib/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "sideEffects": false,
15
+ "scripts": {
16
+ "build": "bun build ./src/index.ts --outdir ./dist --target browser --external react --external @couch-kit/client --external @couch-kit/core && tsc -p tsconfig.build.json",
17
+ "test": "bun test",
18
+ "typecheck": "tsc --noEmit",
19
+ "lint": "eslint src/",
20
+ "clean": "rm -rf dist lib"
21
+ },
22
+ "files": [
23
+ "src",
24
+ "dist",
25
+ "lib",
26
+ "README.md",
27
+ "CHANGELOG.md"
28
+ ],
29
+ "dependencies": {
30
+ "@couch-kit/client": "0.8.0"
31
+ },
32
+ "devDependencies": {
33
+ "@couch-kit/core": "0.8.0",
34
+ "@types/react": "^19.1.1",
35
+ "react": "^18.2.0",
36
+ "typescript": "^5.4.0",
37
+ "eslint": "^8.57.0"
38
+ },
39
+ "peerDependencies": {
40
+ "react": ">=18.0.0"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public",
44
+ "provenance": true
45
+ },
46
+ "license": "MIT",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/faluciano/react-native-couch-kit.git",
50
+ "directory": "packages/devtools"
51
+ }
52
+ }
@@ -0,0 +1,362 @@
1
+ import { useState, useCallback, useRef, type CSSProperties } from "react";
2
+ import type { DebugPanelData } from "@couch-kit/client";
3
+
4
+ export interface DebugOverlayProps {
5
+ /** Debug panel data from useDebugPanel hook */
6
+ data: DebugPanelData;
7
+ /** Initial collapsed state */
8
+ defaultCollapsed?: boolean;
9
+ /** Position on screen */
10
+ position?: "top-left" | "top-right" | "bottom-left" | "bottom-right";
11
+ /** Maximum height of the panel */
12
+ maxHeight?: number;
13
+ }
14
+
15
+ const STATUS_COLORS: Record<string, string> = {
16
+ connected: "#4ade80",
17
+ connecting: "#facc15",
18
+ reconnecting: "#facc15",
19
+ disconnected: "#f87171",
20
+ error: "#f87171",
21
+ };
22
+
23
+ function formatTimestamp(ts: number): string {
24
+ const date = new Date(ts);
25
+ return (
26
+ date.toLocaleTimeString("en-US", {
27
+ hour12: false,
28
+ hour: "2-digit",
29
+ minute: "2-digit",
30
+ second: "2-digit",
31
+ }) +
32
+ "." +
33
+ String(date.getMilliseconds()).padStart(3, "0")
34
+ );
35
+ }
36
+
37
+ function JsonTree({ data, depth = 0 }: { data: unknown; depth?: number }) {
38
+ if (data === null || data === undefined) {
39
+ return <span style={{ color: "#9ca3af" }}>{String(data)}</span>;
40
+ }
41
+
42
+ if (typeof data === "boolean") {
43
+ return <span style={{ color: "#c084fc" }}>{String(data)}</span>;
44
+ }
45
+
46
+ if (typeof data === "number") {
47
+ return <span style={{ color: "#60a5fa" }}>{data}</span>;
48
+ }
49
+
50
+ if (typeof data === "string") {
51
+ return <span style={{ color: "#4ade80" }}>&quot;{data}&quot;</span>;
52
+ }
53
+
54
+ if (Array.isArray(data)) {
55
+ if (data.length === 0) return <span>{"[]"}</span>;
56
+ return (
57
+ <span>
58
+ {"[\n"}
59
+ {data.map((item, i) => (
60
+ <span key={i}>
61
+ {" ".repeat(depth + 1)}
62
+ <JsonTree data={item} depth={depth + 1} />
63
+ {i < data.length - 1 ? "," : ""}
64
+ {"\n"}
65
+ </span>
66
+ ))}
67
+ {" ".repeat(depth)}
68
+ {"]"}
69
+ </span>
70
+ );
71
+ }
72
+
73
+ if (typeof data === "object") {
74
+ const entries = Object.entries(data as Record<string, unknown>);
75
+ if (entries.length === 0) return <span>{"{}"}</span>;
76
+ return (
77
+ <span>
78
+ {"{\n"}
79
+ {entries.map(([key, value], i) => (
80
+ <span key={key}>
81
+ {" ".repeat(depth + 1)}
82
+ <span style={{ color: "#93c5fd" }}>{key}</span>
83
+ {": "}
84
+ <JsonTree data={value} depth={depth + 1} />
85
+ {i < entries.length - 1 ? "," : ""}
86
+ {"\n"}
87
+ </span>
88
+ ))}
89
+ {" ".repeat(depth)}
90
+ {"}"}
91
+ </span>
92
+ );
93
+ }
94
+
95
+ return <span>{String(data)}</span>;
96
+ }
97
+
98
+ export function DebugOverlay({
99
+ data,
100
+ defaultCollapsed = true,
101
+ position = "bottom-right",
102
+ maxHeight = 400,
103
+ }: DebugOverlayProps) {
104
+ const [collapsed, setCollapsed] = useState(defaultCollapsed);
105
+ const [activeTab, setActiveTab] = useState<"actions" | "state">("actions");
106
+ const tapCountRef = useRef(0);
107
+ const tapTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
108
+
109
+ const handleToggle = useCallback(() => {
110
+ tapCountRef.current++;
111
+
112
+ if (tapTimerRef.current) {
113
+ clearTimeout(tapTimerRef.current);
114
+ }
115
+
116
+ tapTimerRef.current = setTimeout(() => {
117
+ tapCountRef.current = 0;
118
+ }, 300);
119
+
120
+ setCollapsed((prev) => !prev);
121
+ }, []);
122
+
123
+ // Early exit: nothing to render when debug is disabled
124
+ if (!data.enabled) return null;
125
+
126
+ const positionStyles: CSSProperties = {
127
+ position: "fixed",
128
+ zIndex: 99999,
129
+ ...(position.includes("top") ? { top: 8 } : { bottom: 8 }),
130
+ ...(position.includes("left") ? { left: 8 } : { right: 8 }),
131
+ };
132
+
133
+ const statusColor = STATUS_COLORS[data.connectionStatus] ?? "#9ca3af";
134
+
135
+ const panelStyle: CSSProperties = {
136
+ ...positionStyles,
137
+ fontFamily:
138
+ "'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace",
139
+ fontSize: 11,
140
+ lineHeight: 1.4,
141
+ color: "#e5e7eb",
142
+ backgroundColor: "rgba(17, 24, 39, 0.95)",
143
+ border: "1px solid rgba(75, 85, 99, 0.6)",
144
+ borderRadius: 8,
145
+ overflow: "hidden",
146
+ width: collapsed ? "auto" : 360,
147
+ backdropFilter: "blur(8px)",
148
+ boxShadow: "0 4px 24px rgba(0, 0, 0, 0.4)",
149
+ };
150
+
151
+ const buttonStyle: CSSProperties = {
152
+ background: "none",
153
+ border: "none",
154
+ color: "#e5e7eb",
155
+ cursor: "pointer",
156
+ padding: "6px 10px",
157
+ fontSize: 11,
158
+ fontFamily: "inherit",
159
+ };
160
+
161
+ const tabStyle = (isActive: boolean): CSSProperties => ({
162
+ ...buttonStyle,
163
+ borderBottom: isActive ? "2px solid #60a5fa" : "2px solid transparent",
164
+ color: isActive ? "#60a5fa" : "#9ca3af",
165
+ paddingBottom: 4,
166
+ });
167
+
168
+ if (collapsed) {
169
+ return (
170
+ <div style={positionStyles}>
171
+ <button
172
+ onClick={handleToggle}
173
+ style={{
174
+ ...buttonStyle,
175
+ backgroundColor: "rgba(17, 24, 39, 0.95)",
176
+ border: "1px solid rgba(75, 85, 99, 0.6)",
177
+ borderRadius: 8,
178
+ display: "flex",
179
+ alignItems: "center",
180
+ gap: 6,
181
+ padding: "6px 12px",
182
+ backdropFilter: "blur(8px)",
183
+ boxShadow: "0 2px 12px rgba(0, 0, 0, 0.3)",
184
+ }}
185
+ >
186
+ <span
187
+ style={{
188
+ width: 8,
189
+ height: 8,
190
+ borderRadius: "50%",
191
+ backgroundColor: statusColor,
192
+ display: "inline-block",
193
+ }}
194
+ />
195
+ <span>Debug</span>
196
+ {data.rtt !== null && (
197
+ <span style={{ color: "#9ca3af" }}>{data.rtt}ms</span>
198
+ )}
199
+ </button>
200
+ </div>
201
+ );
202
+ }
203
+
204
+ const latestState =
205
+ data.stateHistory.length > 0
206
+ ? data.stateHistory[data.stateHistory.length - 1]?.state
207
+ : null;
208
+
209
+ return (
210
+ <div style={panelStyle}>
211
+ {/* Header */}
212
+ <div
213
+ style={{
214
+ display: "flex",
215
+ alignItems: "center",
216
+ justifyContent: "space-between",
217
+ padding: "6px 10px",
218
+ borderBottom: "1px solid rgba(75, 85, 99, 0.4)",
219
+ }}
220
+ >
221
+ <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
222
+ <span
223
+ style={{
224
+ width: 8,
225
+ height: 8,
226
+ borderRadius: "50%",
227
+ backgroundColor: statusColor,
228
+ display: "inline-block",
229
+ }}
230
+ />
231
+ <span style={{ fontWeight: 600 }}>Debug</span>
232
+ <span style={{ color: "#9ca3af" }}>
233
+ {data.connectionStatus}
234
+ {data.rtt !== null ? ` · ${data.rtt}ms` : ""}
235
+ </span>
236
+ </div>
237
+ <div style={{ display: "flex", gap: 4 }}>
238
+ <button onClick={data.clearHistory} style={buttonStyle} title="Clear">
239
+ ✕
240
+ </button>
241
+ <button onClick={handleToggle} style={buttonStyle} title="Collapse">
242
+ ▾
243
+ </button>
244
+ </div>
245
+ </div>
246
+
247
+ {/* Tabs */}
248
+ <div
249
+ style={{
250
+ display: "flex",
251
+ gap: 0,
252
+ borderBottom: "1px solid rgba(75, 85, 99, 0.4)",
253
+ padding: "0 6px",
254
+ }}
255
+ >
256
+ <button
257
+ onClick={() => setActiveTab("actions")}
258
+ style={tabStyle(activeTab === "actions")}
259
+ >
260
+ Actions ({data.actionLog.length})
261
+ </button>
262
+ <button
263
+ onClick={() => setActiveTab("state")}
264
+ style={tabStyle(activeTab === "state")}
265
+ >
266
+ State
267
+ </button>
268
+ </div>
269
+
270
+ {/* Content */}
271
+ <div
272
+ style={{
273
+ maxHeight,
274
+ overflowY: "auto",
275
+ padding: 8,
276
+ }}
277
+ >
278
+ {activeTab === "actions" && (
279
+ <div>
280
+ {data.actionLog.length === 0 ? (
281
+ <div
282
+ style={{
283
+ color: "#6b7280",
284
+ textAlign: "center",
285
+ padding: 16,
286
+ }}
287
+ >
288
+ No actions recorded
289
+ </div>
290
+ ) : (
291
+ data.actionLog
292
+ .slice()
293
+ .reverse()
294
+ .map((entry) => (
295
+ <div
296
+ key={entry.id}
297
+ style={{
298
+ padding: "4px 6px",
299
+ borderBottom: "1px solid rgba(75, 85, 99, 0.2)",
300
+ display: "flex",
301
+ flexDirection: "column",
302
+ gap: 2,
303
+ }}
304
+ >
305
+ <div
306
+ style={{
307
+ display: "flex",
308
+ justifyContent: "space-between",
309
+ alignItems: "center",
310
+ }}
311
+ >
312
+ <span
313
+ style={{
314
+ color:
315
+ entry.source === "local" ? "#60a5fa" : "#4ade80",
316
+ fontSize: 10,
317
+ textTransform: "uppercase",
318
+ }}
319
+ >
320
+ {entry.source}
321
+ </span>
322
+ <span style={{ color: "#6b7280", fontSize: 10 }}>
323
+ {formatTimestamp(entry.timestamp)}
324
+ </span>
325
+ </div>
326
+ <pre
327
+ style={{
328
+ margin: 0,
329
+ fontSize: 10,
330
+ color: "#d1d5db",
331
+ whiteSpace: "pre-wrap",
332
+ wordBreak: "break-all",
333
+ }}
334
+ >
335
+ <JsonTree data={entry.action} />
336
+ </pre>
337
+ </div>
338
+ ))
339
+ )}
340
+ </div>
341
+ )}
342
+
343
+ {activeTab === "state" && (
344
+ <pre
345
+ style={{
346
+ margin: 0,
347
+ fontSize: 10,
348
+ whiteSpace: "pre-wrap",
349
+ wordBreak: "break-all",
350
+ }}
351
+ >
352
+ {latestState !== null ? (
353
+ <JsonTree data={latestState} />
354
+ ) : (
355
+ <span style={{ color: "#6b7280" }}>No state captured</span>
356
+ )}
357
+ </pre>
358
+ )}
359
+ </div>
360
+ </div>
361
+ );
362
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { DebugOverlay } from "./DebugOverlay";
2
+ export type { DebugOverlayProps } from "./DebugOverlay";