electrobun 0.0.19-beta.97 → 0.1.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.
Files changed (77) hide show
  1. package/README.md +1 -1
  2. package/dist/api/browser/webviewtag.ts +54 -2
  3. package/dist/api/bun/ElectrobunConfig.ts +171 -0
  4. package/dist/api/bun/core/BrowserWindow.ts +4 -0
  5. package/dist/api/bun/core/Tray.ts +14 -0
  6. package/dist/api/bun/core/Updater.ts +4 -3
  7. package/dist/api/bun/index.ts +2 -0
  8. package/dist/api/bun/proc/native.ts +107 -5
  9. package/dist/main.js +5 -4
  10. package/package.json +4 -2
  11. package/src/cli/index.ts +565 -148
  12. package/templates/hello-world/bun.lock +164 -2
  13. package/templates/hello-world/electrobun.config.ts +28 -0
  14. package/templates/hello-world/src/bun/index.ts +2 -2
  15. package/templates/hello-world/src/mainview/index.html +5 -6
  16. package/templates/hello-world/src/mainview/index.ts +1 -5
  17. package/templates/interactive-playground/README.md +26 -0
  18. package/templates/interactive-playground/assets/tray-icon.png +0 -0
  19. package/templates/interactive-playground/electrobun.config.ts +36 -0
  20. package/templates/interactive-playground/package-lock.json +36 -0
  21. package/templates/interactive-playground/package.json +15 -0
  22. package/templates/interactive-playground/src/bun/demos/files.ts +70 -0
  23. package/templates/interactive-playground/src/bun/demos/menus.ts +139 -0
  24. package/templates/interactive-playground/src/bun/demos/rpc.ts +83 -0
  25. package/templates/interactive-playground/src/bun/demos/system.ts +72 -0
  26. package/templates/interactive-playground/src/bun/demos/updates.ts +105 -0
  27. package/templates/interactive-playground/src/bun/demos/windows.ts +90 -0
  28. package/templates/interactive-playground/src/bun/index.ts +124 -0
  29. package/templates/interactive-playground/src/bun/types/rpc.ts +109 -0
  30. package/templates/interactive-playground/src/mainview/components/EventLog.ts +107 -0
  31. package/templates/interactive-playground/src/mainview/components/Sidebar.ts +65 -0
  32. package/templates/interactive-playground/src/mainview/components/Toast.ts +57 -0
  33. package/templates/interactive-playground/src/mainview/demos/FileDemo.ts +211 -0
  34. package/templates/interactive-playground/src/mainview/demos/MenuDemo.ts +102 -0
  35. package/templates/interactive-playground/src/mainview/demos/RPCDemo.ts +229 -0
  36. package/templates/interactive-playground/src/mainview/demos/TrayDemo.ts +132 -0
  37. package/templates/interactive-playground/src/mainview/demos/WebViewDemo.ts +411 -0
  38. package/templates/interactive-playground/src/mainview/demos/WindowDemo.ts +207 -0
  39. package/templates/interactive-playground/src/mainview/index.css +538 -0
  40. package/templates/interactive-playground/src/mainview/index.html +103 -0
  41. package/templates/interactive-playground/src/mainview/index.ts +238 -0
  42. package/templates/multitab-browser/README.md +34 -0
  43. package/templates/multitab-browser/bun.lock +224 -0
  44. package/templates/multitab-browser/electrobun.config.ts +32 -0
  45. package/templates/multitab-browser/package-lock.json +20 -0
  46. package/templates/multitab-browser/package.json +12 -0
  47. package/templates/multitab-browser/src/bun/index.ts +144 -0
  48. package/templates/multitab-browser/src/bun/tabManager.ts +200 -0
  49. package/templates/multitab-browser/src/bun/types/rpc.ts +78 -0
  50. package/templates/multitab-browser/src/mainview/index.css +487 -0
  51. package/templates/multitab-browser/src/mainview/index.html +94 -0
  52. package/templates/multitab-browser/src/mainview/index.ts +634 -0
  53. package/templates/photo-booth/README.md +108 -0
  54. package/templates/photo-booth/bun.lock +239 -0
  55. package/templates/photo-booth/electrobun.config.ts +28 -0
  56. package/templates/photo-booth/package.json +16 -0
  57. package/templates/photo-booth/src/bun/index.ts +92 -0
  58. package/templates/photo-booth/src/mainview/index.css +465 -0
  59. package/templates/photo-booth/src/mainview/index.html +124 -0
  60. package/templates/photo-booth/src/mainview/index.ts +499 -0
  61. package/tests/bun.lock +14 -0
  62. package/tests/electrobun.config.ts +45 -0
  63. package/tests/package-lock.json +36 -0
  64. package/tests/package.json +13 -0
  65. package/tests/src/bun/index.ts +100 -0
  66. package/tests/src/bun/test-runner.ts +508 -0
  67. package/tests/src/mainview/index.html +110 -0
  68. package/tests/src/mainview/index.ts +458 -0
  69. package/tests/src/mainview/styles/main.css +451 -0
  70. package/tests/src/testviews/tray-test.html +57 -0
  71. package/tests/src/testviews/webview-mask.html +114 -0
  72. package/tests/src/testviews/webview-navigation.html +36 -0
  73. package/tests/src/testviews/window-create.html +17 -0
  74. package/tests/src/testviews/window-events.html +29 -0
  75. package/tests/src/testviews/window-focus.html +37 -0
  76. package/tests/src/webviewtag/index.ts +11 -0
  77. package/templates/hello-world/electrobun.config +0 -18
@@ -0,0 +1,83 @@
1
+ class RPCTester {
2
+ async doMath(data: { a: number; b: number; operation: string }): Promise<number> {
3
+ const startTime = Date.now();
4
+ let result: number;
5
+
6
+ switch (data.operation) {
7
+ case 'add':
8
+ result = data.a + data.b;
9
+ break;
10
+ case 'subtract':
11
+ result = data.a - data.b;
12
+ break;
13
+ case 'multiply':
14
+ result = data.a * data.b;
15
+ break;
16
+ case 'divide':
17
+ if (data.b === 0) throw new Error("Division by zero");
18
+ result = data.a / data.b;
19
+ break;
20
+ case 'power':
21
+ result = Math.pow(data.a, data.b);
22
+ break;
23
+ default:
24
+ throw new Error(`Unknown operation: ${data.operation}`);
25
+ }
26
+
27
+ const duration = Date.now() - startTime;
28
+
29
+ // Don't send notification here - let the frontend handle timing and display
30
+ // to avoid duplicate entries
31
+
32
+ return result;
33
+ }
34
+
35
+ async echoBigData(data: string): Promise<string> {
36
+ const startTime = Date.now();
37
+
38
+ // Simulate some processing time
39
+ await new Promise(resolve => setTimeout(resolve, 100));
40
+
41
+ const response = `Echo: ${data.slice(0, 100)}... (${data.length} chars)`;
42
+ const duration = Date.now() - startTime;
43
+
44
+ // Don't send notification here - let the frontend handle timing and display
45
+ // to avoid duplicate entries
46
+
47
+ return response;
48
+ }
49
+
50
+ async performanceTest(messageSize: number, messageCount: number): Promise<{
51
+ totalTime: number;
52
+ averageTime: number;
53
+ messagesPerSecond: number;
54
+ }> {
55
+ const testData = "x".repeat(messageSize);
56
+ const results: number[] = [];
57
+
58
+ const startTime = Date.now();
59
+
60
+ for (let i = 0; i < messageCount; i++) {
61
+ const messageStart = Date.now();
62
+ await this.echoBigData(testData);
63
+ results.push(Date.now() - messageStart);
64
+ }
65
+
66
+ const totalTime = Date.now() - startTime;
67
+ const averageTime = results.reduce((a, b) => a + b, 0) / results.length;
68
+ const messagesPerSecond = (messageCount / totalTime) * 1000;
69
+
70
+ // Don't send notification here - let the frontend handle timing and display
71
+
72
+ return {
73
+ totalTime,
74
+ averageTime,
75
+ messagesPerSecond
76
+ };
77
+ }
78
+
79
+ // Event callbacks
80
+ onRpcTestResult?: (data: { operation: string; result: any; duration: number }) => void;
81
+ }
82
+
83
+ export const rpcTester = new RPCTester();
@@ -0,0 +1,72 @@
1
+ import Electrobun from "electrobun/bun";
2
+ import { platform, arch, release } from "os";
3
+
4
+ class SystemManager {
5
+ async getPlatformInfo() {
6
+ return {
7
+ platform: platform(),
8
+ arch: arch(),
9
+ version: release(),
10
+ electrobunVersion: "0.0.19-beta.118", // This should come from the actual API
11
+ };
12
+ }
13
+
14
+ async showNotification(options: {
15
+ title: string;
16
+ body: string;
17
+ icon?: string;
18
+ }) {
19
+ // Note: Notification API needs to be implemented in Electrobun
20
+ // For now, we'll simulate it
21
+ console.log("Notification:", options);
22
+
23
+ this.onSystemEvent?.({
24
+ type: 'notification-shown',
25
+ details: {
26
+ title: options.title,
27
+ body: options.body,
28
+ timestamp: new Date().toISOString()
29
+ }
30
+ });
31
+
32
+ // In a real implementation, this would use the native notification system
33
+ return Promise.resolve();
34
+ }
35
+
36
+ async getClipboardText(): Promise<string> {
37
+ // Note: Clipboard API needs to be implemented in Electrobun
38
+ // This is a placeholder
39
+ return "Clipboard API not yet implemented";
40
+ }
41
+
42
+ async setClipboardText(text: string): Promise<void> {
43
+ // Note: Clipboard API needs to be implemented in Electrobun
44
+ console.log("Setting clipboard text:", text);
45
+
46
+ this.onSystemEvent?.({
47
+ type: 'clipboard-set',
48
+ details: { text, timestamp: new Date().toISOString() }
49
+ });
50
+ }
51
+
52
+ async getScreenInfo() {
53
+ // Note: Screen API needs to be implemented in Electrobun
54
+ // This is a placeholder
55
+ return {
56
+ displays: [
57
+ {
58
+ id: 1,
59
+ width: 1920,
60
+ height: 1080,
61
+ scaleFactor: 1,
62
+ primary: true
63
+ }
64
+ ]
65
+ };
66
+ }
67
+
68
+ // Event callbacks
69
+ onSystemEvent?: (event: { type: string; details: any }) => void;
70
+ }
71
+
72
+ export const systemManager = new SystemManager();
@@ -0,0 +1,105 @@
1
+ import Electrobun from "electrobun/bun";
2
+
3
+ class UpdateManager {
4
+ async checkForUpdates() {
5
+ try {
6
+ const updateInfo = await Electrobun.Updater.checkForUpdate();
7
+
8
+ this.onUpdateStatus?.({
9
+ status: 'checked',
10
+ progress: 100
11
+ });
12
+
13
+ return {
14
+ updateAvailable: updateInfo.updateAvailable,
15
+ currentVersion: Electrobun.Updater.getLocalVersion?.() || "0.0.19-beta.118",
16
+ latestVersion: updateInfo.latestVersion,
17
+ };
18
+ } catch (error) {
19
+ console.error("Update check error:", error);
20
+
21
+ this.onUpdateStatus?.({
22
+ status: 'error',
23
+ progress: 0
24
+ });
25
+
26
+ return {
27
+ updateAvailable: false,
28
+ currentVersion: "0.0.19-beta.118",
29
+ error: error.message
30
+ };
31
+ }
32
+ }
33
+
34
+ async downloadUpdate() {
35
+ try {
36
+ this.onUpdateStatus?.({
37
+ status: 'downloading',
38
+ progress: 0
39
+ });
40
+
41
+ // Simulate download progress
42
+ for (let i = 0; i <= 100; i += 10) {
43
+ await new Promise(resolve => setTimeout(resolve, 200));
44
+ this.onUpdateStatus?.({
45
+ status: 'downloading',
46
+ progress: i
47
+ });
48
+ }
49
+
50
+ await Electrobun.Updater.downloadUpdate();
51
+
52
+ this.onUpdateStatus?.({
53
+ status: 'downloaded',
54
+ progress: 100
55
+ });
56
+
57
+ return { success: true };
58
+ } catch (error) {
59
+ console.error("Update download error:", error);
60
+
61
+ this.onUpdateStatus?.({
62
+ status: 'error',
63
+ progress: 0
64
+ });
65
+
66
+ return { success: false, error: error.message };
67
+ }
68
+ }
69
+
70
+ async applyUpdate() {
71
+ try {
72
+ this.onUpdateStatus?.({
73
+ status: 'applying',
74
+ progress: 50
75
+ });
76
+
77
+ await Electrobun.Updater.applyUpdate();
78
+
79
+ this.onUpdateStatus?.({
80
+ status: 'applied',
81
+ progress: 100
82
+ });
83
+
84
+ return { success: true };
85
+ } catch (error) {
86
+ console.error("Update apply error:", error);
87
+
88
+ this.onUpdateStatus?.({
89
+ status: 'error',
90
+ progress: 0
91
+ });
92
+
93
+ return { success: false, error: error.message };
94
+ }
95
+ }
96
+
97
+ getUpdateInfo() {
98
+ return Electrobun.Updater.updateInfo?.() || null;
99
+ }
100
+
101
+ // Event callbacks
102
+ onUpdateStatus?: (data: { status: string; progress?: number }) => void;
103
+ }
104
+
105
+ export const updateManager = new UpdateManager();
@@ -0,0 +1,90 @@
1
+ import { BrowserWindow } from "electrobun/bun";
2
+
3
+ class WindowManager {
4
+ private windows = new Map<number, BrowserWindow>();
5
+ private nextId = 1;
6
+
7
+ async createWindow(options: {
8
+ width: number;
9
+ height: number;
10
+ x: number;
11
+ y: number;
12
+ frameless?: boolean;
13
+ transparent?: boolean;
14
+ alwaysOnTop?: boolean;
15
+ }) {
16
+ const id = this.nextId++;
17
+
18
+ const window = new BrowserWindow({
19
+ title: `Demo Window ${id}`,
20
+ url: "views://mainview/index.html",
21
+ renderer: "cef",
22
+ frame: {
23
+ width: options.width,
24
+ height: options.height,
25
+ x: options.x,
26
+ y: options.y,
27
+ },
28
+ titleBarStyle: options.frameless ? "hiddenInset" : "default",
29
+ // For completely frameless, we need to set styleMask
30
+ styleMask: options.frameless ? {
31
+ Borderless: true,
32
+ Titled: false,
33
+ Closable: true,
34
+ Miniaturizable: true,
35
+ Resizable: true,
36
+ } : undefined,
37
+ });
38
+
39
+ this.windows.set(id, window);
40
+
41
+ // Listen for window events
42
+ window.on("close", () => {
43
+ this.windows.delete(id);
44
+ this.onWindowClosed?.(id);
45
+ });
46
+
47
+ window.on("resize", (event) => {
48
+ this.onWindowEvent?.({ type: 'resize', id, data: event.data });
49
+ });
50
+
51
+ window.on("move", (event) => {
52
+ this.onWindowEvent?.({ type: 'move', id, data: event.data });
53
+ });
54
+
55
+ this.onWindowCreated?.(id, `Demo Window ${id}`);
56
+
57
+ return { id };
58
+ }
59
+
60
+ async closeWindow(id: number) {
61
+ const window = this.windows.get(id);
62
+ if (window) {
63
+ window.close();
64
+ this.windows.delete(id);
65
+ }
66
+ }
67
+
68
+ async focusWindow(id: number) {
69
+ const window = this.windows.get(id);
70
+ if (window) {
71
+ window.focus();
72
+ this.onWindowFocused?.(id);
73
+ }
74
+ }
75
+
76
+ async getWindowList() {
77
+ return Array.from(this.windows.entries()).map(([id, window]) => ({
78
+ id,
79
+ title: window.getTitle() || `Window ${id}`,
80
+ }));
81
+ }
82
+
83
+ // Event callbacks
84
+ onWindowCreated?: (id: number, title: string) => void;
85
+ onWindowClosed?: (id: number) => void;
86
+ onWindowFocused?: (id: number) => void;
87
+ onWindowEvent?: (event: { type: string; id: number; data: any }) => void;
88
+ }
89
+
90
+ export const windowManager = new WindowManager();
@@ -0,0 +1,124 @@
1
+ import Electrobun, { BrowserWindow, BrowserView } from "electrobun/bun";
2
+ import { type PlaygroundRPC } from "./types/rpc";
3
+
4
+ // Import demo modules
5
+ import { windowManager } from "./demos/windows";
6
+ import { menuManager } from "./demos/menus";
7
+ import { fileManager } from "./demos/files";
8
+ import { rpcTester } from "./demos/rpc";
9
+
10
+ console.log("🚀 Electrobun Interactive Playground starting...");
11
+
12
+ // Set up RPC communication
13
+ const rpc = BrowserView.defineRPC<PlaygroundRPC>({
14
+ maxRequestTime: 10000,
15
+ handlers: {
16
+ requests: {
17
+ // Window Management
18
+ createWindow: windowManager.createWindow.bind(windowManager),
19
+ closeWindow: windowManager.closeWindow.bind(windowManager),
20
+ focusWindow: windowManager.focusWindow.bind(windowManager),
21
+ getWindowList: windowManager.getWindowList.bind(windowManager),
22
+
23
+ // RPC Testing
24
+ doMath: rpcTester.doMath.bind(rpcTester),
25
+ echoBigData: rpcTester.echoBigData.bind(rpcTester),
26
+
27
+ // Menu Operations
28
+ createTray: menuManager.createTray.bind(menuManager),
29
+ removeTray: menuManager.removeTray.bind(menuManager),
30
+ showContextMenu: menuManager.showContextMenu.bind(menuManager),
31
+
32
+ // File Operations
33
+ openFileDialog: fileManager.openFileDialog.bind(fileManager),
34
+ moveToTrash: fileManager.moveToTrash.bind(fileManager),
35
+ showInFinder: fileManager.showInFinder.bind(fileManager),
36
+
37
+ // WebView Operations (placeholder)
38
+ createWebView: async (url: string) => ({ id: 1 }),
39
+ executeJSInWebView: async (params: { id: number; script: string }) => null,
40
+
41
+
42
+ },
43
+ messages: {
44
+ "*": (messageName, payload) => {
45
+ console.log(`📨 Message received: ${messageName}`, payload);
46
+ },
47
+ },
48
+ },
49
+ });
50
+
51
+ // Create main playground window
52
+ const mainWindow = new BrowserWindow({
53
+ title: "Electrobun Interactive Playground",
54
+ url: "views://mainview/index.html",
55
+ renderer: "cef",
56
+ frame: {
57
+ width: 1400,
58
+ height: 900,
59
+ x: 100,
60
+ y: 100,
61
+ },
62
+ titleBarStyle: "default",
63
+ rpc,
64
+ });
65
+
66
+ // Set up event forwarding from demo modules to the UI
67
+ windowManager.onWindowCreated = (id, title) => {
68
+ rpc.send.windowCreated({ id, title });
69
+ };
70
+
71
+ windowManager.onWindowClosed = (id) => {
72
+ rpc.send.windowClosed({ id });
73
+ };
74
+
75
+ windowManager.onWindowFocused = (id) => {
76
+ rpc.send.windowFocused({ id });
77
+ };
78
+
79
+ menuManager.onTrayClicked = (id, action) => {
80
+ rpc.send.trayClicked({ id, action });
81
+ console.log(`🔔 Tray ${id} clicked: ${action}`);
82
+ };
83
+
84
+ menuManager.onMenuClicked = (action) => {
85
+ rpc.send.menuClicked({ action });
86
+ console.log(`🎛️ Menu clicked: ${action}`);
87
+ };
88
+
89
+ fileManager.onFileSelected = (paths) => {
90
+ rpc.send.fileSelected({ paths });
91
+ console.log(`📁 Files selected:`, paths);
92
+ };
93
+
94
+ fileManager.onSystemEvent = (event) => {
95
+ rpc.send.systemEvent(event);
96
+ console.log(`⚙️ System event:`, event);
97
+ };
98
+
99
+ rpcTester.onRpcTestResult = (data) => {
100
+ rpc.send.rpcTestResult(data);
101
+ console.log(`📡 RPC test result:`, data);
102
+ };
103
+
104
+
105
+
106
+ // Listen for global events
107
+ Electrobun.events.on("application-menu-clicked", (e) => {
108
+ menuManager.onMenuClicked?.(e.data.action);
109
+ });
110
+
111
+ Electrobun.events.on("context-menu-clicked", (e) => {
112
+ menuManager.onMenuClicked?.(e.data.action);
113
+ });
114
+
115
+ // Send initial status
116
+ mainWindow.webview.on("dom-ready", () => {
117
+ console.log("✅ Main window DOM ready");
118
+ rpc.send.logMessage({
119
+ level: 'info',
120
+ message: 'Electrobun Interactive Playground loaded successfully!'
121
+ });
122
+ });
123
+
124
+ console.log("🎮 Playground initialized successfully");
@@ -0,0 +1,109 @@
1
+ import { type RPCSchema } from "electrobun";
2
+
3
+ export type PlaygroundRPC = {
4
+ bun: RPCSchema<{
5
+ requests: {
6
+ // Window Management
7
+ createWindow: {
8
+ params: {
9
+ width: number;
10
+ height: number;
11
+ x: number;
12
+ y: number;
13
+ frameless?: boolean;
14
+ transparent?: boolean;
15
+ alwaysOnTop?: boolean;
16
+ };
17
+ response: { id: number };
18
+ };
19
+ closeWindow: {
20
+ params: number; // window id
21
+ response: void;
22
+ };
23
+ focusWindow: {
24
+ params: number; // window id
25
+ response: void;
26
+ };
27
+ getWindowList: {
28
+ params: void;
29
+ response: Array<{ id: number; title: string }>;
30
+ };
31
+
32
+ // RPC Testing
33
+ doMath: {
34
+ params: { a: number; b: number; operation: string };
35
+ response: number;
36
+ };
37
+ echoBigData: {
38
+ params: string;
39
+ response: string;
40
+ };
41
+
42
+ // Menu Operations
43
+ createTray: {
44
+ params: { title: string; image?: string };
45
+ response: { id: number };
46
+ };
47
+ removeTray: {
48
+ params: number; // tray id
49
+ response: void;
50
+ };
51
+ showContextMenu: {
52
+ params: { x: number; y: number };
53
+ response: void;
54
+ };
55
+
56
+ // File Operations
57
+ openFileDialog: {
58
+ params: {
59
+ multiple?: boolean;
60
+ fileTypes?: string[];
61
+ startingFolder?: string;
62
+ };
63
+ response: string[];
64
+ };
65
+ moveToTrash: {
66
+ params: string; // file path
67
+ response: void;
68
+ };
69
+ showInFinder: {
70
+ params: string; // file path
71
+ response: void;
72
+ };
73
+
74
+ // WebView Operations
75
+ createWebView: {
76
+ params: string; // url
77
+ response: { id: number };
78
+ };
79
+ executeJSInWebView: {
80
+ params: { id: number; script: string };
81
+ response: any;
82
+ };
83
+
84
+
85
+ };
86
+ messages: {};
87
+ }>;
88
+
89
+ webview: RPCSchema<{
90
+ requests: {};
91
+ messages: {
92
+ // Messages from bun to webview
93
+ windowCreated: { id: number; title: string };
94
+ windowClosed: { id: number };
95
+ windowFocused: { id: number };
96
+
97
+ trayClicked: { id: number; action: string };
98
+ menuClicked: { action: string };
99
+
100
+ fileSelected: { paths: string[] };
101
+
102
+ rpcTestResult: { operation: string; result: any; duration: number };
103
+
104
+ systemEvent: { type: string; details: any };
105
+
106
+ logMessage: { level: 'info' | 'warn' | 'error'; message: string };
107
+ };
108
+ }>;
109
+ };
@@ -0,0 +1,107 @@
1
+ interface LogEntry {
2
+ timestamp: Date;
3
+ level: 'info' | 'warn' | 'error';
4
+ message: string;
5
+ data?: any;
6
+ }
7
+
8
+ export class EventLog {
9
+ private entries: LogEntry[] = [];
10
+ private isOpen: boolean = false;
11
+ private filters: Set<string> = new Set(['info', 'warn', 'error']);
12
+
13
+ constructor() {
14
+ this.initializeEventListeners();
15
+ }
16
+
17
+ private initializeEventListeners() {
18
+ const toggle = document.getElementById('event-log-toggle');
19
+ const clearBtn = document.getElementById('clear-log');
20
+ const filterCheckboxes = document.querySelectorAll('.event-log-filters input[type="checkbox"]');
21
+
22
+ toggle?.addEventListener('click', () => {
23
+ this.toggle();
24
+ });
25
+
26
+ clearBtn?.addEventListener('click', () => {
27
+ this.clear();
28
+ });
29
+
30
+ filterCheckboxes.forEach(checkbox => {
31
+ checkbox.addEventListener('change', (e) => {
32
+ const target = e.target as HTMLInputElement;
33
+ const level = target.getAttribute('data-level');
34
+ if (level) {
35
+ if (target.checked) {
36
+ this.filters.add(level);
37
+ } else {
38
+ this.filters.delete(level);
39
+ }
40
+ this.render();
41
+ }
42
+ });
43
+ });
44
+ }
45
+
46
+ toggle() {
47
+ this.isOpen = !this.isOpen;
48
+ const eventLog = document.getElementById('event-log');
49
+ eventLog?.classList.toggle('open', this.isOpen);
50
+ }
51
+
52
+ addEntry(level: 'info' | 'warn' | 'error', message: string, data?: any) {
53
+ const entry: LogEntry = {
54
+ timestamp: new Date(),
55
+ level,
56
+ message,
57
+ data
58
+ };
59
+
60
+ this.entries.unshift(entry); // Add to beginning
61
+
62
+ // Keep only last 100 entries
63
+ if (this.entries.length > 100) {
64
+ this.entries = this.entries.slice(0, 100);
65
+ }
66
+
67
+ this.render();
68
+ }
69
+
70
+ clear() {
71
+ this.entries = [];
72
+ this.render();
73
+ }
74
+
75
+ private render() {
76
+ const container = document.getElementById('event-log-list');
77
+ if (!container) return;
78
+
79
+ const filteredEntries = this.entries.filter(entry => this.filters.has(entry.level));
80
+
81
+ container.innerHTML = filteredEntries.map(entry => `
82
+ <div class="event-entry ${entry.level}">
83
+ <div class="event-time">${this.formatTime(entry.timestamp)}</div>
84
+ <div class="event-message">${this.escapeHtml(entry.message)}</div>
85
+ ${entry.data ? `<pre class="event-data">${this.escapeHtml(JSON.stringify(entry.data, null, 2))}</pre>` : ''}
86
+ </div>
87
+ `).join('');
88
+
89
+ // Auto-scroll to top for new entries
90
+ container.scrollTop = 0;
91
+ }
92
+
93
+ private formatTime(date: Date): string {
94
+ return date.toLocaleTimeString('en-US', {
95
+ hour12: false,
96
+ hour: '2-digit',
97
+ minute: '2-digit',
98
+ second: '2-digit'
99
+ });
100
+ }
101
+
102
+ private escapeHtml(text: string): string {
103
+ const div = document.createElement('div');
104
+ div.textContent = text;
105
+ return div.innerHTML;
106
+ }
107
+ }