logbubble 0.1.0 → 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/README.md +46 -4
- package/dist/auto.js +0 -2
- package/dist/core/console.js +19 -10
- package/dist/core/dom.js +9 -2
- package/dist/core/fetch.js +12 -2
- package/dist/core/xhr.js +11 -2
- package/dist/init.js +12 -7
- package/dist/ui/dragHandler.d.ts +22 -0
- package/dist/ui/dragHandler.js +127 -0
- package/dist/ui/filterManager.d.ts +15 -0
- package/dist/ui/filterManager.js +61 -0
- package/dist/ui/logStore.d.ts +16 -3
- package/dist/ui/logStore.js +152 -8
- package/dist/ui/logUI.d.ts +28 -1
- package/dist/ui/logUI.js +306 -218
- package/dist/ui/styles.d.ts +1 -0
- package/dist/ui/styles.js +384 -0
- package/dist/ui/virtualScroller.d.ts +24 -0
- package/dist/ui/virtualScroller.js +112 -0
- package/dist/vite-logbubble-plugin.d.ts +3 -0
- package/dist/vite-logbubble-plugin.js +20 -0
- package/package.json +24 -9
- package/auto.ts +0 -4
- package/core/console.ts +0 -79
- package/core/dom.ts +0 -33
- package/core/fetch.ts +0 -41
- package/core/xhr.ts +0 -38
- package/init.ts +0 -41
- package/ui/logStore.ts +0 -49
- package/ui/logUI.ts +0 -395
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/js/logbubble)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://github.com/
|
|
5
|
+
[](https://github.com/dkumar600/LogBubble)
|
|
6
6
|
|
|
7
7
|
A lightweight, floating log viewer for monitoring network requests and console logs in web applications. Perfect for debugging web apps, Capacitor apps, and Progressive Web Apps (PWAs) directly on mobile devices or in production-like environments.
|
|
8
8
|
|
|
@@ -15,6 +15,10 @@ A lightweight, floating log viewer for monitoring network requests and console l
|
|
|
15
15
|
📝 **Console Logging**: Captures `console.log`, `console.warn`, `console.error`, `console.info`, and `console.debug`
|
|
16
16
|
🔔 **Unread Badge**: Shows count of new logs since last view
|
|
17
17
|
📋 **Copy to Clipboard**: Export all logs with timestamps
|
|
18
|
+
🟢 **Draggable Bubble**: Move the log bubble to any corner of the screen
|
|
19
|
+
🟦 **Toggleable Filters**: Easily filter between console and network logs
|
|
20
|
+
🗂️ **Log Details Modal**: Click any log to open a modal with full details, timestamp, and a copy button for better readability
|
|
21
|
+
🛠️ **Built-in Vite Plugin**: Vite plugin included in this project for easy integration during development
|
|
18
22
|
🔄 **Auto-initialization**: Works out of the box with zero configuration
|
|
19
23
|
🎨 **Clean UI**: Modern, minimalist design with color-coded log types
|
|
20
24
|
|
|
@@ -44,6 +48,42 @@ import "logbubble";
|
|
|
44
48
|
|
|
45
49
|
The floating bubble will appear in the top-right corner of your app.
|
|
46
50
|
|
|
51
|
+
## Vite Plugin Integration
|
|
52
|
+
|
|
53
|
+
LogBubble provides a Vite plugin for easy integration during development. This automatically injects LogBubble into your app for local debugging.
|
|
54
|
+
|
|
55
|
+
### Setup
|
|
56
|
+
|
|
57
|
+
1. Install LogBubble (if not already):
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm install logbubble
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
2. Add the plugin to your `vite.config.js` or `vite.config.ts`:
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
// vite.config.js
|
|
67
|
+
import { logbubbleVitePlugin } from "logbubble/vite-logbubble-plugin";
|
|
68
|
+
|
|
69
|
+
export default {
|
|
70
|
+
plugins: [
|
|
71
|
+
logbubbleVitePlugin(),
|
|
72
|
+
// ...other plugins
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
3. Start your Vite dev server:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm run dev
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The LogBubble UI will appear automatically in your app during development.
|
|
84
|
+
|
|
85
|
+
**Note:** The plugin is intended for development only. For production builds, import LogBubble manually as shown above.
|
|
86
|
+
|
|
47
87
|
### Manual initialization
|
|
48
88
|
|
|
49
89
|
If you want more control over when LogBubble initializes:
|
|
@@ -128,6 +168,8 @@ LogBubble automatically intercepts and logs:
|
|
|
128
168
|
|
|
129
169
|
- **Bubble**: Click to toggle the log window
|
|
130
170
|
- **Badge**: Shows unread log count (disappears when window is opened)
|
|
171
|
+
- **Draggable Button**: Button is draggable in all four corners of thr screen
|
|
172
|
+
- **Toggleable Filter**: Toggleable Buttons to filter between consoles and network logs
|
|
131
173
|
- **Clear**: Remove all logs
|
|
132
174
|
- **Copy**: Copy all logs to clipboard with timestamps
|
|
133
175
|
|
|
@@ -218,7 +260,7 @@ Make sure your app is using native `fetch` or `XMLHttpRequest` APIs. Some custom
|
|
|
218
260
|
|
|
219
261
|
## Support
|
|
220
262
|
|
|
221
|
-
For issues, feature requests, or questions, please open an issue on [GitHub](https://github.com/
|
|
263
|
+
For issues, feature requests, or questions, please open an issue on [GitHub](https://github.com/dkumar600/LogBubble/issues).
|
|
222
264
|
|
|
223
265
|
## License
|
|
224
266
|
|
|
@@ -226,7 +268,7 @@ MIT
|
|
|
226
268
|
|
|
227
269
|
## Contributing
|
|
228
270
|
|
|
229
|
-
Contributions are welcome! Please feel free to submit issues or pull requests on [GitHub](https://github.com/
|
|
271
|
+
Contributions are welcome! Please feel free to submit issues or pull requests on [GitHub](https://github.com/dkumar600/LogBubble).
|
|
230
272
|
|
|
231
273
|
### Getting Started with Development
|
|
232
274
|
|
|
@@ -241,4 +283,4 @@ If you want to contribute to LogBubble:
|
|
|
241
283
|
|
|
242
284
|
Built with ❤️ for developers who need better mobile debugging tools.
|
|
243
285
|
|
|
244
|
-
**Made by [Deepak Kumar](https://github.com/
|
|
286
|
+
**Made by [Deepak Kumar](https://github.com/dkumar600)**
|
package/dist/auto.js
CHANGED
package/dist/core/console.js
CHANGED
|
@@ -2,9 +2,18 @@
|
|
|
2
2
|
// Patches console methods to log to the network logger UI
|
|
3
3
|
import { logStore } from "../ui/logStore";
|
|
4
4
|
let isPatching = false;
|
|
5
|
+
const CONSOLE_PATCH_FLAG = "__LOGBUBBLE_CONSOLE_PATCHED__";
|
|
6
|
+
const CONSOLE_PATCH_PROP = "__logbubbleConsolePatched";
|
|
5
7
|
export function patchConsole() {
|
|
6
8
|
if (typeof window === "undefined" || typeof console === "undefined")
|
|
7
9
|
return;
|
|
10
|
+
const g = globalThis;
|
|
11
|
+
if (g[CONSOLE_PATCH_FLAG])
|
|
12
|
+
return;
|
|
13
|
+
if (console[CONSOLE_PATCH_PROP])
|
|
14
|
+
return;
|
|
15
|
+
g[CONSOLE_PATCH_FLAG] = true;
|
|
16
|
+
console[CONSOLE_PATCH_PROP] = true;
|
|
8
17
|
const originalLog = console.log;
|
|
9
18
|
const originalWarn = console.warn;
|
|
10
19
|
const originalError = console.error;
|
|
@@ -14,8 +23,8 @@ export function patchConsole() {
|
|
|
14
23
|
originalLog.apply(console, args);
|
|
15
24
|
if (!isPatching) {
|
|
16
25
|
isPatching = true;
|
|
17
|
-
const
|
|
18
|
-
logStore.addLog(
|
|
26
|
+
const payload = args.length === 1 ? args[0] : args;
|
|
27
|
+
logStore.addLog(payload, "console");
|
|
19
28
|
isPatching = false;
|
|
20
29
|
}
|
|
21
30
|
};
|
|
@@ -23,8 +32,8 @@ export function patchConsole() {
|
|
|
23
32
|
originalWarn.apply(console, args);
|
|
24
33
|
if (!isPatching) {
|
|
25
34
|
isPatching = true;
|
|
26
|
-
const
|
|
27
|
-
logStore.addLog(
|
|
35
|
+
const payload = args.length === 1 ? args[0] : args;
|
|
36
|
+
logStore.addLog(payload, "console");
|
|
28
37
|
isPatching = false;
|
|
29
38
|
}
|
|
30
39
|
};
|
|
@@ -32,8 +41,8 @@ export function patchConsole() {
|
|
|
32
41
|
originalError.apply(console, args);
|
|
33
42
|
if (!isPatching) {
|
|
34
43
|
isPatching = true;
|
|
35
|
-
const
|
|
36
|
-
logStore.addLog(
|
|
44
|
+
const payload = args.length === 1 ? args[0] : args;
|
|
45
|
+
logStore.addLog(payload, "console");
|
|
37
46
|
isPatching = false;
|
|
38
47
|
}
|
|
39
48
|
};
|
|
@@ -41,8 +50,8 @@ export function patchConsole() {
|
|
|
41
50
|
originalInfo.apply(console, args);
|
|
42
51
|
if (!isPatching) {
|
|
43
52
|
isPatching = true;
|
|
44
|
-
const
|
|
45
|
-
logStore.addLog(
|
|
53
|
+
const payload = args.length === 1 ? args[0] : args;
|
|
54
|
+
logStore.addLog(payload, "console");
|
|
46
55
|
isPatching = false;
|
|
47
56
|
}
|
|
48
57
|
};
|
|
@@ -50,8 +59,8 @@ export function patchConsole() {
|
|
|
50
59
|
originalDebug.apply(console, args);
|
|
51
60
|
if (!isPatching) {
|
|
52
61
|
isPatching = true;
|
|
53
|
-
const
|
|
54
|
-
logStore.addLog(
|
|
62
|
+
const payload = args.length === 1 ? args[0] : args;
|
|
63
|
+
logStore.addLog(payload, "console");
|
|
55
64
|
isPatching = false;
|
|
56
65
|
}
|
|
57
66
|
};
|
package/dist/core/dom.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
// dom.ts
|
|
2
|
-
// (Optional) Patch dynamic <script> and <link> loading for network logging
|
|
3
1
|
import { logStore } from "../ui/logStore";
|
|
2
|
+
const DOM_PATCH_FLAG = "__LOGBUBBLE_DOM_PATCHED__";
|
|
3
|
+
const DOM_PATCH_PROP = "__logbubbleDomPatched";
|
|
4
4
|
export function patchDOM(logFn) {
|
|
5
5
|
if (typeof window === "undefined")
|
|
6
6
|
return;
|
|
7
|
+
const g = globalThis;
|
|
8
|
+
if (g[DOM_PATCH_FLAG])
|
|
9
|
+
return;
|
|
10
|
+
if (document.createElement[DOM_PATCH_PROP])
|
|
11
|
+
return;
|
|
12
|
+
g[DOM_PATCH_FLAG] = true;
|
|
7
13
|
const origCreateElement = document.createElement;
|
|
14
|
+
origCreateElement[DOM_PATCH_PROP] = true;
|
|
8
15
|
document.createElement = function (tag, options) {
|
|
9
16
|
const el = origCreateElement.call(this, tag, options);
|
|
10
17
|
if (tag === "script" || tag === "link") {
|
package/dist/core/fetch.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
// fetch.ts
|
|
2
|
-
// Patches window.fetch to log network requests in dev mode (browser/WebView only)
|
|
3
1
|
import { logStore } from "../ui/logStore";
|
|
2
|
+
const FETCH_PATCH_FLAG = "__LOGBUBBLE_FETCH_PATCHED__";
|
|
3
|
+
const FETCH_PATCH_PROP = "__logbubbleFetchPatched";
|
|
4
4
|
export function patchFetch(logFn) {
|
|
5
5
|
if (typeof window === "undefined" || typeof window.fetch !== "function")
|
|
6
6
|
return;
|
|
7
|
+
const g = globalThis;
|
|
8
|
+
if (g[FETCH_PATCH_FLAG])
|
|
9
|
+
return;
|
|
10
|
+
if (window.fetch[FETCH_PATCH_PROP])
|
|
11
|
+
return;
|
|
12
|
+
g[FETCH_PATCH_FLAG] = true;
|
|
7
13
|
const originalFetch = window.fetch;
|
|
14
|
+
originalFetch[FETCH_PATCH_PROP] = true;
|
|
8
15
|
window.fetch = async function (input, init) {
|
|
9
16
|
const method = (init && init.method) ||
|
|
10
17
|
(typeof input === "object" && "method" in input && input.method) ||
|
|
@@ -14,6 +21,9 @@ export function patchFetch(logFn) {
|
|
|
14
21
|
: input instanceof URL
|
|
15
22
|
? input.href
|
|
16
23
|
: input.url;
|
|
24
|
+
if (typeof url === "string" && url.includes("/__logbubble/")) {
|
|
25
|
+
return originalFetch.call(this, input, init);
|
|
26
|
+
}
|
|
17
27
|
const start = Date.now();
|
|
18
28
|
try {
|
|
19
29
|
const response = await originalFetch.call(this, input, init);
|
package/dist/core/xhr.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
// xhr.ts
|
|
2
|
-
// Patches XMLHttpRequest to log network requests in dev mode (browser/WebView only)
|
|
3
1
|
import { logStore } from "../ui/logStore";
|
|
2
|
+
const XHR_PATCH_FLAG = "__LOGBUBBLE_XHR_PATCHED__";
|
|
3
|
+
const XHR_PATCH_PROP = "__logbubbleXhrPatched";
|
|
4
4
|
export function patchXHR(logFn) {
|
|
5
5
|
if (typeof window === "undefined" ||
|
|
6
6
|
typeof window.XMLHttpRequest !== "function")
|
|
7
7
|
return;
|
|
8
|
+
const g = globalThis;
|
|
9
|
+
if (g[XHR_PATCH_FLAG])
|
|
10
|
+
return;
|
|
11
|
+
if (window.XMLHttpRequest[XHR_PATCH_PROP])
|
|
12
|
+
return;
|
|
13
|
+
g[XHR_PATCH_FLAG] = true;
|
|
8
14
|
const OriginalXHR = window.XMLHttpRequest;
|
|
15
|
+
OriginalXHR[XHR_PATCH_PROP] = true;
|
|
9
16
|
function PatchedXHR() {
|
|
10
17
|
const xhr = new OriginalXHR();
|
|
11
18
|
let url = "";
|
|
@@ -22,6 +29,8 @@ export function patchXHR(logFn) {
|
|
|
22
29
|
start = Date.now();
|
|
23
30
|
});
|
|
24
31
|
xhr.addEventListener("loadend", () => {
|
|
32
|
+
if (url && url.includes("/__logbubble/"))
|
|
33
|
+
return;
|
|
25
34
|
const ms = Date.now() - start;
|
|
26
35
|
const message = `[NET] ${method} ${url} ${xhr.status} ${ms}ms`;
|
|
27
36
|
logFn(message);
|
package/dist/init.js
CHANGED
|
@@ -1,34 +1,39 @@
|
|
|
1
|
-
// init.ts
|
|
2
|
-
// Entry point for web-only dev-network-logger
|
|
3
1
|
import { patchFetch } from "./core/fetch";
|
|
4
2
|
import { patchXHR } from "./core/xhr";
|
|
5
3
|
import { patchDOM } from "./core/dom";
|
|
6
4
|
import { patchConsole } from "./core/console";
|
|
7
5
|
import { logUI } from "./ui/logUI";
|
|
8
6
|
let isInitialized = false;
|
|
7
|
+
const GLOBAL_INIT_FLAG = "__LOGBUBBLE_INITIALIZED__";
|
|
9
8
|
function isDev() {
|
|
10
9
|
return (typeof process !== "undefined" &&
|
|
11
10
|
process.env &&
|
|
12
11
|
(process.env.NODE_ENV === "development" || process.env.NODE_ENV === "dev"));
|
|
13
12
|
}
|
|
14
13
|
export function initNetworkLogger() {
|
|
14
|
+
{
|
|
15
|
+
const g = globalThis;
|
|
16
|
+
if (g[GLOBAL_INIT_FLAG]) {
|
|
17
|
+
console.warn("[LogBubble] Already initialized, skipping duplicate init");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
g[GLOBAL_INIT_FLAG] = true;
|
|
21
|
+
if (typeof window !== "undefined") {
|
|
22
|
+
window[GLOBAL_INIT_FLAG] = true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
15
25
|
if (isInitialized) {
|
|
16
26
|
console.warn("[LogBubble] Already initialized, skipping duplicate init");
|
|
17
27
|
return;
|
|
18
28
|
}
|
|
19
29
|
isInitialized = true;
|
|
20
30
|
logUI.init();
|
|
21
|
-
// Patch fetch/XHR/DOM/Console in dev mode
|
|
22
31
|
patchFetch(() => { });
|
|
23
32
|
patchXHR(() => { });
|
|
24
33
|
patchDOM(() => { });
|
|
25
34
|
patchConsole();
|
|
26
35
|
}
|
|
27
|
-
// Auto-init for browser
|
|
28
36
|
if (typeof window !== "undefined") {
|
|
29
37
|
window.initNetworkLogger = initNetworkLogger;
|
|
30
|
-
// Optionally auto-init in dev
|
|
31
|
-
// if (isDev()) {
|
|
32
38
|
initNetworkLogger();
|
|
33
|
-
// }
|
|
34
39
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare class DragHandler {
|
|
2
|
+
private buttonEl;
|
|
3
|
+
private onSnapComplete?;
|
|
4
|
+
private onClick?;
|
|
5
|
+
private isDragging;
|
|
6
|
+
private isPointerDown;
|
|
7
|
+
private dragStartX;
|
|
8
|
+
private dragStartY;
|
|
9
|
+
private mouseDownTime;
|
|
10
|
+
private mouseDownPos;
|
|
11
|
+
private didActuallyDrag;
|
|
12
|
+
buttonX: number;
|
|
13
|
+
buttonY: number;
|
|
14
|
+
constructor(buttonEl: HTMLDivElement, onSnapComplete?: (() => void) | undefined, onClick?: (() => void) | undefined);
|
|
15
|
+
attachListeners(): void;
|
|
16
|
+
isCurrentlyDragging(): boolean;
|
|
17
|
+
private handleDragStart;
|
|
18
|
+
private handleDragMove;
|
|
19
|
+
private handleDragEnd;
|
|
20
|
+
private updateButtonPosition;
|
|
21
|
+
private snapToCorner;
|
|
22
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
export class DragHandler {
|
|
2
|
+
constructor(buttonEl, onSnapComplete, onClick) {
|
|
3
|
+
this.buttonEl = buttonEl;
|
|
4
|
+
this.onSnapComplete = onSnapComplete;
|
|
5
|
+
this.onClick = onClick;
|
|
6
|
+
this.isDragging = false;
|
|
7
|
+
this.isPointerDown = false;
|
|
8
|
+
this.dragStartX = 0;
|
|
9
|
+
this.dragStartY = 0;
|
|
10
|
+
this.mouseDownTime = 0;
|
|
11
|
+
this.mouseDownPos = { x: 0, y: 0 };
|
|
12
|
+
this.didActuallyDrag = false;
|
|
13
|
+
this.buttonX = 0;
|
|
14
|
+
this.buttonY = 0;
|
|
15
|
+
this.buttonX = window.innerWidth - 58;
|
|
16
|
+
this.buttonY = 10;
|
|
17
|
+
this.updateButtonPosition();
|
|
18
|
+
}
|
|
19
|
+
attachListeners() {
|
|
20
|
+
this.buttonEl.addEventListener("mousedown", (e) => this.handleDragStart(e));
|
|
21
|
+
this.buttonEl.addEventListener("touchstart", (e) => this.handleDragStart(e), {
|
|
22
|
+
passive: false,
|
|
23
|
+
});
|
|
24
|
+
document.addEventListener("mousemove", (e) => this.handleDragMove(e));
|
|
25
|
+
document.addEventListener("touchmove", (e) => this.handleDragMove(e), {
|
|
26
|
+
passive: false,
|
|
27
|
+
});
|
|
28
|
+
document.addEventListener("mouseup", () => this.handleDragEnd());
|
|
29
|
+
document.addEventListener("touchend", () => this.handleDragEnd());
|
|
30
|
+
}
|
|
31
|
+
isCurrentlyDragging() {
|
|
32
|
+
return this.isDragging;
|
|
33
|
+
}
|
|
34
|
+
handleDragStart(e) {
|
|
35
|
+
this.isDragging = false;
|
|
36
|
+
this.didActuallyDrag = false;
|
|
37
|
+
this.isPointerDown = true;
|
|
38
|
+
const clientX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX;
|
|
39
|
+
const clientY = e instanceof MouseEvent ? e.clientY : e.touches[0].clientY;
|
|
40
|
+
this.dragStartX = clientX - this.buttonX;
|
|
41
|
+
this.dragStartY = clientY - this.buttonY;
|
|
42
|
+
this.mouseDownTime = Date.now();
|
|
43
|
+
this.mouseDownPos = { x: clientX, y: clientY };
|
|
44
|
+
if (e instanceof TouchEvent) {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
handleDragMove(e) {
|
|
49
|
+
if (!this.isPointerDown)
|
|
50
|
+
return;
|
|
51
|
+
if (this.dragStartX === 0 && this.dragStartY === 0)
|
|
52
|
+
return;
|
|
53
|
+
const clientX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX;
|
|
54
|
+
const clientY = e instanceof MouseEvent ? e.clientY : e.touches[0].clientY;
|
|
55
|
+
const newX = clientX - this.dragStartX;
|
|
56
|
+
const newY = clientY - this.dragStartY;
|
|
57
|
+
const distanceMoved = Math.sqrt(Math.pow(newX - this.buttonX, 2) + Math.pow(newY - this.buttonY, 2));
|
|
58
|
+
if (distanceMoved > 5) {
|
|
59
|
+
this.isDragging = true;
|
|
60
|
+
this.didActuallyDrag = true;
|
|
61
|
+
}
|
|
62
|
+
if (!this.isDragging)
|
|
63
|
+
return;
|
|
64
|
+
const maxX = window.innerWidth - 48;
|
|
65
|
+
const maxY = window.innerHeight - 48;
|
|
66
|
+
this.buttonX = Math.max(0, Math.min(newX, maxX));
|
|
67
|
+
this.buttonY = Math.max(0, Math.min(newY, maxY));
|
|
68
|
+
this.updateButtonPosition();
|
|
69
|
+
if (e instanceof TouchEvent) {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
handleDragEnd() {
|
|
74
|
+
if (!this.isPointerDown)
|
|
75
|
+
return;
|
|
76
|
+
if (this.isDragging) {
|
|
77
|
+
this.snapToCorner();
|
|
78
|
+
}
|
|
79
|
+
else if (!this.didActuallyDrag && this.onClick) {
|
|
80
|
+
this.onClick();
|
|
81
|
+
}
|
|
82
|
+
this.isDragging = false;
|
|
83
|
+
this.isPointerDown = false;
|
|
84
|
+
this.dragStartX = 0;
|
|
85
|
+
this.dragStartY = 0;
|
|
86
|
+
this.mouseDownTime = 0;
|
|
87
|
+
this.mouseDownPos = { x: 0, y: 0 };
|
|
88
|
+
this.didActuallyDrag = false;
|
|
89
|
+
}
|
|
90
|
+
updateButtonPosition() {
|
|
91
|
+
this.buttonEl.style.left = `${this.buttonX}px`;
|
|
92
|
+
this.buttonEl.style.top = `${this.buttonY}px`;
|
|
93
|
+
this.buttonEl.style.right = "auto";
|
|
94
|
+
}
|
|
95
|
+
snapToCorner() {
|
|
96
|
+
const buttonSize = 48;
|
|
97
|
+
const margin = 10;
|
|
98
|
+
const centerX = this.buttonX + buttonSize / 2;
|
|
99
|
+
const centerY = this.buttonY + buttonSize / 2;
|
|
100
|
+
const isLeftHalf = centerX < window.innerWidth / 2;
|
|
101
|
+
const isTopHalf = centerY < window.innerHeight / 2;
|
|
102
|
+
if (isLeftHalf && isTopHalf) {
|
|
103
|
+
this.buttonX = margin;
|
|
104
|
+
this.buttonY = margin;
|
|
105
|
+
}
|
|
106
|
+
else if (!isLeftHalf && isTopHalf) {
|
|
107
|
+
this.buttonX = window.innerWidth - buttonSize - margin;
|
|
108
|
+
this.buttonY = margin;
|
|
109
|
+
}
|
|
110
|
+
else if (isLeftHalf && !isTopHalf) {
|
|
111
|
+
this.buttonX = margin;
|
|
112
|
+
this.buttonY = window.innerHeight - buttonSize - margin;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
this.buttonX = window.innerWidth - buttonSize - margin;
|
|
116
|
+
this.buttonY = window.innerHeight - buttonSize - margin;
|
|
117
|
+
}
|
|
118
|
+
this.buttonEl.style.transition = "left 0.3s ease, top 0.3s ease";
|
|
119
|
+
this.updateButtonPosition();
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
this.buttonEl.style.transition = "none";
|
|
122
|
+
if (this.onSnapComplete) {
|
|
123
|
+
this.onSnapComplete();
|
|
124
|
+
}
|
|
125
|
+
}, 300);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { LogEntry } from "./logStore";
|
|
2
|
+
export type FilterType = "all" | "console" | "network";
|
|
3
|
+
export declare class FilterManager {
|
|
4
|
+
private consoleFilterBtn;
|
|
5
|
+
private networkFilterBtn;
|
|
6
|
+
private onFilterChange;
|
|
7
|
+
private activeFilter;
|
|
8
|
+
private perfMonitor;
|
|
9
|
+
constructor(consoleFilterBtn: HTMLButtonElement, networkFilterBtn: HTMLButtonElement, onFilterChange: (filteredLogs: LogEntry[]) => void);
|
|
10
|
+
toggleFilter(filter: "console" | "network", allLogs: LogEntry[]): void;
|
|
11
|
+
applyFilter(allLogs: LogEntry[]): LogEntry[];
|
|
12
|
+
shouldShowLog(log: LogEntry): boolean;
|
|
13
|
+
getActiveFilter(): FilterType;
|
|
14
|
+
private updateFilterButtons;
|
|
15
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export class FilterManager {
|
|
2
|
+
constructor(consoleFilterBtn, networkFilterBtn, onFilterChange) {
|
|
3
|
+
this.consoleFilterBtn = consoleFilterBtn;
|
|
4
|
+
this.networkFilterBtn = networkFilterBtn;
|
|
5
|
+
this.onFilterChange = onFilterChange;
|
|
6
|
+
this.activeFilter = "all";
|
|
7
|
+
this.perfMonitor = {
|
|
8
|
+
filterToggleStart: 0,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
toggleFilter(filter, allLogs) {
|
|
12
|
+
this.perfMonitor.filterToggleStart = performance.now();
|
|
13
|
+
if (this.activeFilter === filter) {
|
|
14
|
+
this.activeFilter = "all";
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
this.activeFilter = filter;
|
|
18
|
+
}
|
|
19
|
+
this.updateFilterButtons();
|
|
20
|
+
const filteredLogs = this.applyFilter(allLogs);
|
|
21
|
+
this.onFilterChange(filteredLogs);
|
|
22
|
+
const elapsed = performance.now() - this.perfMonitor.filterToggleStart;
|
|
23
|
+
if (elapsed > 16) {
|
|
24
|
+
console.warn(`[LogBubble] Filter toggle took ${elapsed.toFixed(2)}ms (target: ≤16ms)`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
applyFilter(allLogs) {
|
|
28
|
+
if (this.activeFilter === "all") {
|
|
29
|
+
return allLogs;
|
|
30
|
+
}
|
|
31
|
+
return allLogs.filter((log) => {
|
|
32
|
+
if (this.activeFilter === "console") {
|
|
33
|
+
return log.category === "console";
|
|
34
|
+
}
|
|
35
|
+
else if (this.activeFilter === "network") {
|
|
36
|
+
return log.category === "network";
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
shouldShowLog(log) {
|
|
42
|
+
if (this.activeFilter === "all")
|
|
43
|
+
return true;
|
|
44
|
+
if (this.activeFilter === "console")
|
|
45
|
+
return log.category === "console";
|
|
46
|
+
if (this.activeFilter === "network")
|
|
47
|
+
return log.category === "network";
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
getActiveFilter() {
|
|
51
|
+
return this.activeFilter;
|
|
52
|
+
}
|
|
53
|
+
updateFilterButtons() {
|
|
54
|
+
const consoleActive = this.activeFilter === "console";
|
|
55
|
+
const networkActive = this.activeFilter === "network";
|
|
56
|
+
this.consoleFilterBtn.classList.toggle("filter-active", consoleActive);
|
|
57
|
+
this.consoleFilterBtn.setAttribute("aria-pressed", consoleActive.toString());
|
|
58
|
+
this.networkFilterBtn.classList.toggle("filter-active", networkActive);
|
|
59
|
+
this.networkFilterBtn.setAttribute("aria-pressed", networkActive.toString());
|
|
60
|
+
}
|
|
61
|
+
}
|
package/dist/ui/logStore.d.ts
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
export interface LogEntry {
|
|
2
|
+
key: string;
|
|
2
3
|
timestamp: number;
|
|
3
4
|
message: string;
|
|
4
|
-
type: "fetch" | "xhr" | "dom" | "plugin";
|
|
5
|
+
type: "fetch" | "xhr" | "dom" | "plugin" | "console";
|
|
6
|
+
category: "console" | "network";
|
|
7
|
+
isCritical?: boolean;
|
|
8
|
+
count?: number;
|
|
5
9
|
}
|
|
6
10
|
declare class LogStore {
|
|
7
11
|
private logs;
|
|
8
12
|
private listeners;
|
|
9
13
|
private maxLogs;
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
private recentNetworkLogs;
|
|
15
|
+
private nextKeyId;
|
|
16
|
+
private networkDedupeKey;
|
|
17
|
+
private shouldCollapseDuplicate;
|
|
18
|
+
/**
|
|
19
|
+
* Estimates payload size without stringifying.
|
|
20
|
+
* Handles nested objects, arrays, circular references, and deep nesting.
|
|
21
|
+
*/
|
|
22
|
+
private estimatePayloadSize;
|
|
23
|
+
addLog(payload: any, type?: LogEntry["type"]): void;
|
|
24
|
+
getLogs(filterCategory?: "console" | "network"): LogEntry[];
|
|
12
25
|
clearLogs(): void;
|
|
13
26
|
subscribe(listener: (log: LogEntry) => void): () => void;
|
|
14
27
|
}
|