sparkbun 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 (131) hide show
  1. package/bin/sparkbun.cjs +18 -0
  2. package/dist-linux-arm64/bsdiff +0 -0
  3. package/dist-linux-arm64/bspatch +0 -0
  4. package/dist-linux-arm64/libElectrobunCore.so +0 -0
  5. package/dist-linux-arm64/libNativeWrapper.so +0 -0
  6. package/dist-linux-arm64/libasar.so +0 -0
  7. package/dist-linux-x64/bsdiff +0 -0
  8. package/dist-linux-x64/bspatch +0 -0
  9. package/dist-linux-x64/libElectrobunCore.so +0 -0
  10. package/dist-linux-x64/libNativeWrapper.so +0 -0
  11. package/dist-linux-x64/libasar.so +0 -0
  12. package/dist-macos-arm64/bsdiff +0 -0
  13. package/dist-macos-arm64/bspatch +0 -0
  14. package/dist-macos-arm64/libElectrobunCore.dylib +0 -0
  15. package/dist-macos-arm64/libNativeWrapper.dylib +0 -0
  16. package/dist-macos-arm64/libasar.dylib +0 -0
  17. package/dist-macos-arm64/libwebgpu_dawn.dylib +0 -0
  18. package/dist-macos-arm64/preload-full.js +885 -0
  19. package/dist-macos-arm64/preload-sandboxed.js +111 -0
  20. package/dist-macos-arm64/process_helper +0 -0
  21. package/dist-win-x64/ElectrobunCore.dll +0 -0
  22. package/dist-win-x64/WebView2Loader.dll +0 -0
  23. package/dist-win-x64/bsdiff.exe +0 -0
  24. package/dist-win-x64/bspatch.exe +0 -0
  25. package/dist-win-x64/libNativeWrapper.dll +0 -0
  26. package/dist-win-x64/zig-asar/arm64/libasar.dll +0 -0
  27. package/dist-win-x64/zig-asar/x64/libasar.dll +0 -0
  28. package/package.json +47 -0
  29. package/scripts/build-and-upload-artifacts.js +207 -0
  30. package/scripts/gen-webgpu-ffi.mjs +162 -0
  31. package/scripts/install-windows-deps.ps1 +80 -0
  32. package/scripts/package-release.js +237 -0
  33. package/scripts/push-version.js +84 -0
  34. package/scripts/update-bun-version.ts +122 -0
  35. package/scripts/update-cef-version.ts +145 -0
  36. package/src/browser/builtinrpcSchema.ts +19 -0
  37. package/src/browser/global.d.ts +36 -0
  38. package/src/browser/index.ts +234 -0
  39. package/src/browser/webviewtag.ts +88 -0
  40. package/src/browser/wgputag.ts +48 -0
  41. package/src/bun/SparkBunConfig.ts +497 -0
  42. package/src/bun/__tests__/ffi-contract.test.ts +105 -0
  43. package/src/bun/core/ApplicationMenu.ts +70 -0
  44. package/src/bun/core/BrowserView.ts +416 -0
  45. package/src/bun/core/BrowserWindow.ts +396 -0
  46. package/src/bun/core/BuildConfig.ts +71 -0
  47. package/src/bun/core/ContextMenu.ts +75 -0
  48. package/src/bun/core/GpuWindow.ts +289 -0
  49. package/src/bun/core/Paths.ts +5 -0
  50. package/src/bun/core/Socket.ts +22 -0
  51. package/src/bun/core/Tray.ts +197 -0
  52. package/src/bun/core/Updater.ts +1131 -0
  53. package/src/bun/core/Utils.ts +487 -0
  54. package/src/bun/core/WGPUView.ts +167 -0
  55. package/src/bun/core/menuRoles.ts +181 -0
  56. package/src/bun/events/ApplicationEvents.ts +22 -0
  57. package/src/bun/events/event.ts +27 -0
  58. package/src/bun/events/eventEmitter.ts +45 -0
  59. package/src/bun/events/trayEvents.ts +11 -0
  60. package/src/bun/events/webviewEvents.ts +39 -0
  61. package/src/bun/events/windowEvents.ts +23 -0
  62. package/src/bun/index.ts +120 -0
  63. package/src/bun/preload/.generated/compiled.ts +2 -0
  64. package/src/bun/preload/build.ts +65 -0
  65. package/src/bun/preload/dragRegions.ts +41 -0
  66. package/src/bun/preload/encryption.ts +86 -0
  67. package/src/bun/preload/events.ts +171 -0
  68. package/src/bun/preload/globals.d.ts +45 -0
  69. package/src/bun/preload/index-sandboxed.ts +28 -0
  70. package/src/bun/preload/index.ts +77 -0
  71. package/src/bun/preload/internalRpc.ts +80 -0
  72. package/src/bun/preload/overlaySync.ts +107 -0
  73. package/src/bun/preload/webviewTag.ts +451 -0
  74. package/src/bun/preload/wgpuTag.ts +246 -0
  75. package/src/bun/proc/linux.md +43 -0
  76. package/src/bun/proc/native.ts +3253 -0
  77. package/src/bun/webGPU.ts +346 -0
  78. package/src/bun/webgpuAdapter.ts +3011 -0
  79. package/src/cli/bun.lockb +0 -0
  80. package/src/cli/index.ts +4653 -0
  81. package/src/cli/package-lock.json +81 -0
  82. package/src/cli/package.json +11 -0
  83. package/src/cli/templates/embedded.ts +2 -0
  84. package/src/core/build.zig +16 -0
  85. package/src/core/main.zig +3378 -0
  86. package/src/extractor/build.zig +22 -0
  87. package/src/installer/installer-template.ts +216 -0
  88. package/src/launcher/main.ts +221 -0
  89. package/src/native/build/libNativeWrapper.so +0 -0
  90. package/src/native/linux/build/nativeWrapper.o +0 -0
  91. package/src/native/linux/cef_loader.cpp +110 -0
  92. package/src/native/linux/cef_loader.h +28 -0
  93. package/src/native/linux/cef_process_helper_linux.cpp +160 -0
  94. package/src/native/linux/nativeWrapper.cpp +11768 -0
  95. package/src/native/macos/cef_process_helper_mac.cc +160 -0
  96. package/src/native/macos/nativeWrapper.mm +9172 -0
  97. package/src/native/shared/accelerator_parser.h +72 -0
  98. package/src/native/shared/app_paths.h +110 -0
  99. package/src/native/shared/asar.h +35 -0
  100. package/src/native/shared/cache_migration.h +244 -0
  101. package/src/native/shared/callbacks.h +57 -0
  102. package/src/native/shared/cef_response_filter.h +189 -0
  103. package/src/native/shared/chromium_flags.h +181 -0
  104. package/src/native/shared/config.h +66 -0
  105. package/src/native/shared/download_event.h +197 -0
  106. package/src/native/shared/ffi_helpers.h +139 -0
  107. package/src/native/shared/glob_match.h +59 -0
  108. package/src/native/shared/json_menu_parser.h +223 -0
  109. package/src/native/shared/mime_types.h +101 -0
  110. package/src/native/shared/navigation_rules.h +98 -0
  111. package/src/native/shared/partition_context.h +137 -0
  112. package/src/native/shared/pending_resize_queue.h +45 -0
  113. package/src/native/shared/permissions.h +118 -0
  114. package/src/native/shared/permissions_cef.h +74 -0
  115. package/src/native/shared/preload_script.h +71 -0
  116. package/src/native/shared/shutdown_guard.h +134 -0
  117. package/src/native/shared/thread_safe_map.h +138 -0
  118. package/src/native/shared/webview_storage.h +91 -0
  119. package/src/native/win/cef_process_helper_win.cpp +143 -0
  120. package/src/native/win/dcomp_compositor.h +352 -0
  121. package/src/native/win/nativeWrapper.cpp +12434 -0
  122. package/src/npmbin/index.js +34 -0
  123. package/src/shared/bun-version.ts +3 -0
  124. package/src/shared/cef-version.ts +5 -0
  125. package/src/shared/naming.test.ts +327 -0
  126. package/src/shared/naming.ts +188 -0
  127. package/src/shared/platform.ts +48 -0
  128. package/src/shared/rpc.ts +541 -0
  129. package/src/shared/sparkbun-version.ts +2 -0
  130. package/src/types/three.d.ts +1 -0
  131. package/tsconfig.json +31 -0
@@ -0,0 +1,451 @@
1
+ // <sparkbun-webview> Custom Element
2
+ // Provides OOPIF (out-of-process iframe) functionality
3
+
4
+ import "./globals.d.ts";
5
+ import { send, request } from "./internalRpc";
6
+ import { OverlaySyncController, type Rect } from "./overlaySync";
7
+
8
+ type WebviewEventType =
9
+ | "will-navigate"
10
+ | "did-navigate"
11
+ | "did-navigate-in-page"
12
+ | "did-commit-navigation"
13
+ | "dom-ready"
14
+ | "new-window-open"
15
+ | "host-message"
16
+ | "download-started"
17
+ | "download-progress"
18
+ | "download-completed"
19
+ | "download-failed"
20
+ | "load-started"
21
+ | "load-committed"
22
+ | "load-finished";
23
+
24
+ // Registry for webview instances (for event routing from bun)
25
+ export const webviewRegistry: Record<number, SparkBunWebviewTag> = {};
26
+
27
+ export class SparkBunWebviewTag extends HTMLElement {
28
+ webviewId: number | null = null;
29
+ maskSelectors: Set<string> = new Set();
30
+ private _sync: OverlaySyncController | null = null;
31
+ transparent = false;
32
+ passthroughEnabled = false;
33
+ hidden = false;
34
+ // Sandbox mode: when true, disables RPC and only allows event emission in the child webview
35
+ sandboxed = false;
36
+ private _eventListeners: Record<string, Array<(event: CustomEvent) => void>> =
37
+ {};
38
+
39
+ static get observedAttributes() {
40
+ return ["src", "html"];
41
+ }
42
+
43
+ constructor() {
44
+ super();
45
+ }
46
+
47
+ connectedCallback() {
48
+ requestAnimationFrame(() => this.initWebview());
49
+ }
50
+
51
+ attributeChangedCallback(
52
+ name: string,
53
+ oldValue: string | null,
54
+ newValue: string | null,
55
+ ) {
56
+ if (oldValue === newValue) return;
57
+ if (newValue === null) return;
58
+ if (this.webviewId === null) return;
59
+ if (name === "src") this.loadURL(newValue);
60
+ else if (name === "html") this.loadHTML(newValue);
61
+ }
62
+
63
+ disconnectedCallback() {
64
+ if (this.webviewId !== null) {
65
+ send("webviewTagRemove", { id: this.webviewId });
66
+ delete webviewRegistry[this.webviewId];
67
+ }
68
+ if (this._sync) this._sync.stop();
69
+ }
70
+
71
+ private getInitialNavigationRules(): string[] | null {
72
+ const rawRules = this.getAttribute("navigation-rules");
73
+ if (rawRules === null) {
74
+ return null;
75
+ }
76
+
77
+ const trimmed = rawRules.trim();
78
+ if (!trimmed) {
79
+ return [];
80
+ }
81
+
82
+ try {
83
+ const parsed = JSON.parse(trimmed);
84
+ if (!Array.isArray(parsed) || !parsed.every((rule) => typeof rule === "string")) {
85
+ throw new Error("navigation-rules must be a JSON string array");
86
+ }
87
+ return parsed;
88
+ } catch (error) {
89
+ console.error("Invalid navigation-rules attribute:", error);
90
+ return [];
91
+ }
92
+ }
93
+
94
+ async initWebview() {
95
+ const rect = this.getBoundingClientRect();
96
+ const initialRect = {
97
+ x: rect.x,
98
+ y: rect.y,
99
+ width: rect.width,
100
+ height: rect.height,
101
+ };
102
+
103
+ const url = this.getAttribute("src");
104
+ const html = this.getAttribute("html");
105
+ const preload = this.getAttribute("preload");
106
+ const partition = this.getAttribute("partition");
107
+ const renderer = (this.getAttribute("renderer") || "native") as
108
+ | "native"
109
+ | "cef";
110
+ const masks = this.getAttribute("masks");
111
+ const navigationRules = this.getInitialNavigationRules();
112
+ // Sandbox attribute: when present, the child webview is sandboxed (no RPC, events only)
113
+ const sandbox = this.hasAttribute("sandbox");
114
+ this.sandboxed = sandbox;
115
+ // Read transparent/passthrough attributes for initial state (avoids flash)
116
+ const transparent = this.hasAttribute("transparent");
117
+ const passthrough = this.hasAttribute("passthrough");
118
+ this.transparent = transparent;
119
+ this.passthroughEnabled = passthrough;
120
+ if (transparent) this.style.opacity = "0";
121
+ if (passthrough) this.style.pointerEvents = "none";
122
+
123
+ if (masks) {
124
+ masks.split(",").forEach((s) => this.maskSelectors.add(s.trim()));
125
+ }
126
+
127
+ try {
128
+ const webviewInitParams = {
129
+ hostWebviewId: window.__sparkbunWebviewId,
130
+ windowId: window.__sparkbunWindowId,
131
+ renderer,
132
+ url,
133
+ html,
134
+ preload,
135
+ partition,
136
+ frame: {
137
+ width: rect.width,
138
+ height: rect.height,
139
+ x: rect.x,
140
+ y: rect.y,
141
+ },
142
+ sandbox,
143
+ transparent,
144
+ passthrough,
145
+ ...(navigationRules === null ? {} : { navigationRules }),
146
+ };
147
+
148
+ const webviewId = (await request(
149
+ "webviewTagInit",
150
+ webviewInitParams,
151
+ )) as number;
152
+
153
+ this.webviewId = webviewId;
154
+ this.id = `sparkbun-webview-${webviewId}`;
155
+ webviewRegistry[webviewId] = this;
156
+
157
+ this.setupObservers(initialRect);
158
+ // Force immediate sync after initialization
159
+ this.syncDimensions(true);
160
+
161
+ // When adding a new webview, force all existing webviews to re-sync their positions
162
+ // This handles layout changes caused by the new webview
163
+ // Use requestAnimationFrame to ensure DOM layout is complete
164
+ requestAnimationFrame(() => {
165
+ Object.values(webviewRegistry).forEach((webview) => {
166
+ if (webview !== this && webview.webviewId !== null) {
167
+ webview.syncDimensions(true);
168
+ }
169
+ });
170
+ });
171
+ } catch (err) {
172
+ console.error("Failed to init webview:", err);
173
+ }
174
+ }
175
+
176
+ setupObservers(initialRect: Rect) {
177
+ const getMasks = () => {
178
+ const rect = this.getBoundingClientRect();
179
+ const masks: Rect[] = [];
180
+ this.maskSelectors.forEach((selector) => {
181
+ try {
182
+ document.querySelectorAll(selector).forEach((el) => {
183
+ const mr = el.getBoundingClientRect();
184
+ masks.push({
185
+ x: mr.x - rect.x,
186
+ y: mr.y - rect.y,
187
+ width: mr.width,
188
+ height: mr.height,
189
+ });
190
+ });
191
+ } catch (_e) {
192
+ // Invalid selector, ignore
193
+ }
194
+ });
195
+ return masks;
196
+ };
197
+
198
+ this._sync = new OverlaySyncController(this, {
199
+ onSync: (rect, masksJson) => {
200
+ if (this.webviewId === null) return;
201
+ send("webviewTagResize", {
202
+ id: this.webviewId,
203
+ frame: rect,
204
+ masks: masksJson,
205
+ });
206
+ },
207
+ getMasks,
208
+ burstIntervalMs: 10,
209
+ baseIntervalMs: 100,
210
+ burstDurationMs: 50,
211
+ });
212
+ this._sync.setLastRect(initialRect);
213
+ this._sync.start();
214
+ }
215
+
216
+ syncDimensions(force = false) {
217
+ if (!this._sync) return;
218
+ if (force) {
219
+ this._sync.forceSync();
220
+ }
221
+ }
222
+
223
+ // Navigation methods
224
+ loadURL(url: string) {
225
+ if (this.webviewId === null) return;
226
+ this.setAttribute("src", url);
227
+ send("webviewTagUpdateSrc", { id: this.webviewId, url });
228
+ }
229
+
230
+ loadHTML(html: string) {
231
+ if (this.webviewId === null) return;
232
+ send("webviewTagUpdateHtml", { id: this.webviewId, html });
233
+ }
234
+
235
+ reload() {
236
+ if (this.webviewId !== null)
237
+ send("webviewTagReload", { id: this.webviewId });
238
+ }
239
+
240
+ goBack() {
241
+ if (this.webviewId !== null)
242
+ send("webviewTagGoBack", { id: this.webviewId });
243
+ }
244
+
245
+ goForward() {
246
+ if (this.webviewId !== null)
247
+ send("webviewTagGoForward", { id: this.webviewId });
248
+ }
249
+
250
+ async canGoBack(): Promise<boolean> {
251
+ if (this.webviewId === null) return false;
252
+ return (await request("webviewTagCanGoBack", {
253
+ id: this.webviewId,
254
+ })) as boolean;
255
+ }
256
+
257
+ async canGoForward(): Promise<boolean> {
258
+ if (this.webviewId === null) return false;
259
+ return (await request("webviewTagCanGoForward", {
260
+ id: this.webviewId,
261
+ })) as boolean;
262
+ }
263
+
264
+ // Visibility methods
265
+ toggleTransparent(value?: boolean) {
266
+ if (this.webviewId === null) return;
267
+ this.transparent = value !== undefined ? value : !this.transparent;
268
+ this.style.opacity = this.transparent ? "0" : "";
269
+ send("webviewTagSetTransparent", {
270
+ id: this.webviewId,
271
+ transparent: this.transparent,
272
+ });
273
+ }
274
+
275
+ togglePassthrough(value?: boolean) {
276
+ if (this.webviewId === null) return;
277
+ this.passthroughEnabled =
278
+ value !== undefined ? value : !this.passthroughEnabled;
279
+ this.style.pointerEvents = this.passthroughEnabled ? "none" : "";
280
+ send("webviewTagSetPassthrough", {
281
+ id: this.webviewId,
282
+ enablePassthrough: this.passthroughEnabled,
283
+ });
284
+ }
285
+
286
+ toggleHidden(value?: boolean) {
287
+ if (this.webviewId === null) return;
288
+ this.hidden = value !== undefined ? value : !this.hidden;
289
+ send("webviewTagSetHidden", { id: this.webviewId, hidden: this.hidden });
290
+ }
291
+
292
+ // Mask management
293
+ addMaskSelector(selector: string) {
294
+ this.maskSelectors.add(selector);
295
+ this.syncDimensions(true);
296
+ }
297
+
298
+ removeMaskSelector(selector: string) {
299
+ this.maskSelectors.delete(selector);
300
+ this.syncDimensions(true);
301
+ }
302
+
303
+ // Navigation rules
304
+ setNavigationRules(rules: string[]) {
305
+ if (this.webviewId !== null) {
306
+ send("webviewTagSetNavigationRules", { id: this.webviewId, rules });
307
+ }
308
+ }
309
+
310
+ // Find in page
311
+ findInPage(
312
+ searchText: string,
313
+ options?: { forward?: boolean; matchCase?: boolean },
314
+ ) {
315
+ if (this.webviewId === null) return;
316
+ const forward = options?.forward !== false;
317
+ const matchCase = options?.matchCase || false;
318
+ send("webviewTagFindInPage", {
319
+ id: this.webviewId,
320
+ searchText,
321
+ forward,
322
+ matchCase,
323
+ });
324
+ }
325
+
326
+ stopFindInPage() {
327
+ if (this.webviewId !== null)
328
+ send("webviewTagStopFind", { id: this.webviewId });
329
+ }
330
+
331
+ // DevTools
332
+ openDevTools() {
333
+ if (this.webviewId !== null)
334
+ send("webviewTagOpenDevTools", { id: this.webviewId });
335
+ }
336
+
337
+ closeDevTools() {
338
+ if (this.webviewId !== null)
339
+ send("webviewTagCloseDevTools", { id: this.webviewId });
340
+ }
341
+
342
+ toggleDevTools() {
343
+ if (this.webviewId !== null)
344
+ send("webviewTagToggleDevTools", { id: this.webviewId });
345
+ }
346
+
347
+ // JavaScript execution
348
+ executeJavascript(js: string) {
349
+ if (this.webviewId === null) return;
350
+ send("webviewTagExecuteJavascript", { id: this.webviewId, js });
351
+ }
352
+
353
+ // Event handling
354
+ on(event: WebviewEventType, listener: (event: CustomEvent) => void) {
355
+ if (!this._eventListeners[event]) this._eventListeners[event] = [];
356
+ this._eventListeners[event].push(listener);
357
+ }
358
+
359
+ off(event: WebviewEventType, listener: (event: CustomEvent) => void) {
360
+ if (!this._eventListeners[event]) return;
361
+ const idx = this._eventListeners[event].indexOf(listener);
362
+ if (idx !== -1) this._eventListeners[event].splice(idx, 1);
363
+ }
364
+
365
+ emit(event: WebviewEventType, detail: unknown) {
366
+ const listeners = this._eventListeners[event];
367
+ if (listeners) {
368
+ const customEvent = new CustomEvent(event, { detail });
369
+ listeners.forEach((fn) => fn(customEvent));
370
+ }
371
+ }
372
+
373
+ // Property getters/setters
374
+ get src(): string | null {
375
+ return this.getAttribute("src");
376
+ }
377
+ set src(value: string | null) {
378
+ if (value) {
379
+ this.setAttribute("src", value);
380
+ } else {
381
+ this.removeAttribute("src");
382
+ }
383
+ }
384
+
385
+ get html(): string | null {
386
+ return this.getAttribute("html");
387
+ }
388
+ set html(value: string | null) {
389
+ if (value) {
390
+ this.setAttribute("html", value);
391
+ } else {
392
+ this.removeAttribute("html");
393
+ }
394
+ }
395
+
396
+ get preload(): string | null {
397
+ return this.getAttribute("preload");
398
+ }
399
+ set preload(value: string | null) {
400
+ if (value) this.setAttribute("preload", value);
401
+ else this.removeAttribute("preload");
402
+ }
403
+
404
+ get renderer(): "native" | "cef" {
405
+ return (this.getAttribute("renderer") as "native" | "cef") || "native";
406
+ }
407
+ set renderer(value: "native" | "cef") {
408
+ this.setAttribute("renderer", value);
409
+ }
410
+
411
+ // Sandbox is read-only after creation (set via attribute before adding to DOM)
412
+ get sandbox(): boolean {
413
+ return this.sandboxed;
414
+ }
415
+ }
416
+
417
+ export function initWebviewTag() {
418
+ // Register the custom element if not already registered
419
+ if (!customElements.get("sparkbun-webview")) {
420
+ customElements.define("sparkbun-webview", SparkBunWebviewTag);
421
+ }
422
+
423
+ // Add default styles for <sparkbun-webview> elements
424
+ // These can be easily overridden in the host document
425
+ const injectStyles = () => {
426
+ const style = document.createElement("style");
427
+ style.textContent = `
428
+ sparkbun-webview {
429
+ display: block;
430
+ width: 800px;
431
+ height: 300px;
432
+ background: #fff;
433
+ background-repeat: no-repeat !important;
434
+ overflow: hidden;
435
+ }
436
+ `;
437
+ // Insert at the beginning of <head> so app styles take precedence
438
+ if (document.head?.firstChild) {
439
+ document.head.insertBefore(style, document.head.firstChild);
440
+ } else if (document.head) {
441
+ document.head.appendChild(style);
442
+ }
443
+ };
444
+
445
+ // document.head may not exist at document start, defer if needed
446
+ if (document.head) {
447
+ injectStyles();
448
+ } else {
449
+ document.addEventListener("DOMContentLoaded", injectStyles);
450
+ }
451
+ }
@@ -0,0 +1,246 @@
1
+ // <sparkbun-wgpu> Custom Element
2
+ // Provides a layout-driven native WGPU view that is positioned via a DOM element.
3
+
4
+ import "./globals.d.ts";
5
+ import { send, request } from "./internalRpc";
6
+ import { OverlaySyncController, type Rect } from "./overlaySync";
7
+
8
+ type WgpuTagEventType = "ready";
9
+
10
+ // Registry for WGPU view instances (for event routing if needed)
11
+ export const wgpuTagRegistry: Record<number, SparkBunWgpuTag> = {};
12
+
13
+ export class SparkBunWgpuTag extends HTMLElement {
14
+ wgpuViewId: number | null = null;
15
+ maskSelectors: Set<string> = new Set();
16
+ private _sync: OverlaySyncController | null = null;
17
+ transparent = false;
18
+ passthroughEnabled = false;
19
+ hidden = false;
20
+ private _eventListeners: Record<string, Array<(event: CustomEvent) => void>> =
21
+ {};
22
+
23
+ constructor() {
24
+ super();
25
+ }
26
+
27
+ connectedCallback() {
28
+ requestAnimationFrame(() => this.initWgpuView());
29
+ }
30
+
31
+ disconnectedCallback() {
32
+ if (this.wgpuViewId !== null) {
33
+ send("wgpuTagRemove", { id: this.wgpuViewId });
34
+ delete wgpuTagRegistry[this.wgpuViewId];
35
+ }
36
+ if (this._sync) this._sync.stop();
37
+ }
38
+
39
+ async initWgpuView() {
40
+ const rect = this.getBoundingClientRect();
41
+ const initialRect = {
42
+ x: rect.x,
43
+ y: rect.y,
44
+ width: rect.width,
45
+ height: rect.height,
46
+ };
47
+
48
+ const transparent = this.hasAttribute("transparent");
49
+ const passthrough = this.hasAttribute("passthrough");
50
+ const hidden = this.hasAttribute("hidden");
51
+ const masks = this.getAttribute("masks");
52
+
53
+ this.transparent = transparent;
54
+ this.passthroughEnabled = passthrough;
55
+ this.hidden = hidden;
56
+
57
+ if (masks) {
58
+ masks.split(",").forEach((s) => this.maskSelectors.add(s.trim()));
59
+ }
60
+
61
+ if (transparent) this.style.opacity = "0";
62
+ if (passthrough) this.style.pointerEvents = "none";
63
+
64
+ try {
65
+ const wgpuViewId = (await request("wgpuTagInit", {
66
+ windowId: window.__sparkbunWindowId,
67
+ frame: {
68
+ width: rect.width,
69
+ height: rect.height,
70
+ x: rect.x,
71
+ y: rect.y,
72
+ },
73
+ transparent,
74
+ passthrough,
75
+ })) as number;
76
+
77
+ this.wgpuViewId = wgpuViewId;
78
+ this.id = `sparkbun-wgpu-${wgpuViewId}`;
79
+ wgpuTagRegistry[wgpuViewId] = this;
80
+
81
+ this.setupObservers(initialRect);
82
+ // Force immediate sync after initialization
83
+ this.syncDimensions(true);
84
+
85
+ // Apply hidden state after creation (no init flag for hidden)
86
+ if (hidden) {
87
+ this.toggleHidden(true);
88
+ }
89
+
90
+ // When adding a new WGPU view, force all existing WGPU views to re-sync
91
+ requestAnimationFrame(() => {
92
+ Object.values(wgpuTagRegistry).forEach((view) => {
93
+ if (view !== this && view.wgpuViewId !== null) {
94
+ view.syncDimensions(true);
95
+ }
96
+ });
97
+ });
98
+
99
+ this.emit("ready", { id: wgpuViewId });
100
+ } catch (err) {
101
+ console.error("Failed to init WGPU view:", err);
102
+ }
103
+ }
104
+
105
+ setupObservers(initialRect: Rect) {
106
+ const getMasks = () => {
107
+ const rect = this.getBoundingClientRect();
108
+ const masks: Rect[] = [];
109
+ this.maskSelectors.forEach((selector) => {
110
+ try {
111
+ document.querySelectorAll(selector).forEach((el) => {
112
+ const mr = el.getBoundingClientRect();
113
+ masks.push({
114
+ x: mr.x - rect.x,
115
+ y: mr.y - rect.y,
116
+ width: mr.width,
117
+ height: mr.height,
118
+ });
119
+ });
120
+ } catch (_e) {
121
+ // Invalid selector, ignore
122
+ }
123
+ });
124
+ return masks;
125
+ };
126
+
127
+ this._sync = new OverlaySyncController(this, {
128
+ onSync: (rect, masksJson) => {
129
+ if (this.wgpuViewId === null) return;
130
+ send("wgpuTagResize", {
131
+ id: this.wgpuViewId,
132
+ frame: rect,
133
+ masks: masksJson,
134
+ });
135
+ },
136
+ getMasks,
137
+ burstIntervalMs: 10,
138
+ baseIntervalMs: 100,
139
+ burstDurationMs: 50,
140
+ });
141
+ this._sync.setLastRect(initialRect);
142
+ this._sync.start();
143
+ }
144
+
145
+ syncDimensions(force = false) {
146
+ if (!this._sync) return;
147
+ if (force) {
148
+ this._sync.forceSync();
149
+ }
150
+ }
151
+
152
+ // Visibility methods
153
+ toggleTransparent(value?: boolean) {
154
+ if (this.wgpuViewId === null) return;
155
+ this.transparent = value !== undefined ? value : !this.transparent;
156
+ this.style.opacity = this.transparent ? "0" : "";
157
+ send("wgpuTagSetTransparent", {
158
+ id: this.wgpuViewId,
159
+ transparent: this.transparent,
160
+ });
161
+ }
162
+
163
+ togglePassthrough(value?: boolean) {
164
+ if (this.wgpuViewId === null) return;
165
+ this.passthroughEnabled =
166
+ value !== undefined ? value : !this.passthroughEnabled;
167
+ this.style.pointerEvents = this.passthroughEnabled ? "none" : "";
168
+ send("wgpuTagSetPassthrough", {
169
+ id: this.wgpuViewId,
170
+ passthrough: this.passthroughEnabled,
171
+ });
172
+ }
173
+
174
+ toggleHidden(value?: boolean) {
175
+ if (this.wgpuViewId === null) return;
176
+ this.hidden = value !== undefined ? value : !this.hidden;
177
+ send("wgpuTagSetHidden", { id: this.wgpuViewId, hidden: this.hidden });
178
+ }
179
+
180
+ // Debug helper (native test renderer)
181
+ runTest() {
182
+ if (this.wgpuViewId === null) return;
183
+ send("wgpuTagRunTest", { id: this.wgpuViewId });
184
+ }
185
+
186
+ // Mask management
187
+ addMaskSelector(selector: string) {
188
+ this.maskSelectors.add(selector);
189
+ this.syncDimensions(true);
190
+ }
191
+
192
+ removeMaskSelector(selector: string) {
193
+ this.maskSelectors.delete(selector);
194
+ this.syncDimensions(true);
195
+ }
196
+
197
+ // Event handling
198
+ on(event: WgpuTagEventType, listener: (event: CustomEvent) => void) {
199
+ if (!this._eventListeners[event]) this._eventListeners[event] = [];
200
+ this._eventListeners[event].push(listener);
201
+ }
202
+
203
+ off(event: WgpuTagEventType, listener: (event: CustomEvent) => void) {
204
+ if (!this._eventListeners[event]) return;
205
+ const idx = this._eventListeners[event].indexOf(listener);
206
+ if (idx !== -1) this._eventListeners[event].splice(idx, 1);
207
+ }
208
+
209
+ emit(event: WgpuTagEventType, detail: unknown) {
210
+ const listeners = this._eventListeners[event];
211
+ if (listeners) {
212
+ const customEvent = new CustomEvent(event, { detail });
213
+ listeners.forEach((fn) => fn(customEvent));
214
+ }
215
+ }
216
+ }
217
+
218
+ export function initWgpuTag() {
219
+ if (!customElements.get("sparkbun-wgpu")) {
220
+ customElements.define("sparkbun-wgpu", SparkBunWgpuTag);
221
+ }
222
+
223
+ const injectStyles = () => {
224
+ const style = document.createElement("style");
225
+ style.textContent = `
226
+ sparkbun-wgpu {
227
+ display: block;
228
+ width: 800px;
229
+ height: 300px;
230
+ background: #000;
231
+ overflow: hidden;
232
+ }
233
+ `;
234
+ if (document.head?.firstChild) {
235
+ document.head.insertBefore(style, document.head.firstChild);
236
+ } else if (document.head) {
237
+ document.head.appendChild(style);
238
+ }
239
+ };
240
+
241
+ if (document.head) {
242
+ injectStyles();
243
+ } else {
244
+ document.addEventListener("DOMContentLoaded", injectStyles);
245
+ }
246
+ }