electrobun 1.18.4-beta.18 → 1.18.4-beta.21

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 (55) hide show
  1. package/README.md +9 -0
  2. package/bin/electrobun.cjs +165 -0
  3. package/dist/api/browser/ui/__tests__/dom.test.ts +473 -0
  4. package/dist/api/browser/ui/__tests__/domStub.ts +218 -0
  5. package/dist/api/browser/ui/dom.ts +490 -0
  6. package/dist/api/browser/ui/index.ts +44 -0
  7. package/dist/api/browser/ui/jsx-dev-runtime.ts +16 -0
  8. package/dist/api/browser/ui/jsx-runtime.ts +56 -0
  9. package/dist/api/config/ElectrobunConfig.ts +33 -0
  10. package/dist/api/preload/.generated/compiled.ts +1 -1
  11. package/dist/api/preload/index.ts +2 -0
  12. package/dist/api/preload/uiTag.ts +45 -0
  13. package/dist/api/sdks/main/__tests__/utils-quit-exit-code.test.ts +44 -0
  14. package/dist/api/sdks/main/core/GpuWindow.ts +19 -0
  15. package/dist/api/sdks/main/core/Utils.ts +52 -4
  16. package/dist/api/sdks/main/core/WGPUView.ts +9 -0
  17. package/dist/api/sdks/main/entries/ui.ts +1 -0
  18. package/dist/api/sdks/main/proc/native.ts +187 -0
  19. package/dist/api/sdks/main/ui/__tests__/font.test.ts +49 -0
  20. package/dist/api/sdks/main/ui/__tests__/hit.test.ts +64 -0
  21. package/dist/api/sdks/main/ui/__tests__/jsx.test.ts +252 -0
  22. package/dist/api/sdks/main/ui/__tests__/layout.test.ts +159 -0
  23. package/dist/api/sdks/main/ui/__tests__/paint.test.ts +115 -0
  24. package/dist/api/sdks/main/ui/__tests__/reactive.test.ts +456 -0
  25. package/dist/api/sdks/main/ui/__tests__/scroll-focus-input.test.ts +298 -0
  26. package/dist/api/sdks/main/ui/__tests__/tree.test.ts +96 -0
  27. package/dist/api/sdks/main/ui/__tests__/ui.test.ts +170 -0
  28. package/dist/api/sdks/main/ui/elements.ts +135 -0
  29. package/dist/api/sdks/main/ui/font.ts +168 -0
  30. package/dist/api/sdks/main/ui/hit.ts +46 -0
  31. package/dist/api/sdks/main/ui/index.ts +71 -0
  32. package/dist/api/sdks/main/ui/input.ts +268 -0
  33. package/dist/api/sdks/main/ui/jsx-dev-runtime.ts +16 -0
  34. package/dist/api/sdks/main/ui/jsx-runtime.ts +136 -0
  35. package/dist/api/sdks/main/ui/keymap.ts +147 -0
  36. package/dist/api/sdks/main/ui/layout.ts +178 -0
  37. package/dist/api/sdks/main/ui/paint.ts +196 -0
  38. package/dist/api/sdks/main/ui/reactive.ts +4 -0
  39. package/dist/api/sdks/main/ui/renderer.ts +278 -0
  40. package/dist/api/sdks/main/ui/text.ts +175 -0
  41. package/dist/api/sdks/main/ui/textInput.ts +121 -0
  42. package/dist/api/sdks/main/ui/tree.ts +276 -0
  43. package/dist/api/sdks/main/ui/ui.ts +457 -0
  44. package/dist/api/sdks/main/ui/uiTagHost.ts +56 -0
  45. package/dist/api/sdks/main/ui/uiwindow.ts +330 -0
  46. package/dist/api/shared/build-dependencies.test.ts +1 -1
  47. package/dist/api/shared/build-dependencies.ts +4 -4
  48. package/dist/api/shared/linux-webkit-automation.test.ts +1 -1
  49. package/dist/api/shared/warren/jsx.ts +279 -0
  50. package/dist/api/shared/warren/reactive.ts +638 -0
  51. package/dist/api/shared/windows-unicode-ui.test.ts +4 -4
  52. package/dist/preload-full.js +35 -0
  53. package/dist/zig-sdk/electrobun.zig +197 -162
  54. package/{dash.config.ts → hutch.config.ts} +4 -3
  55. package/package.json +14 -2
@@ -0,0 +1,44 @@
1
+ // Warren for the browser — the DOM renderer. Same reactivity core and JSX
2
+ // semantics as electrobun/main/ui, rendered into real DOM nodes inside a
3
+ // webview (or any web page).
4
+
5
+ export {
6
+ batch,
7
+ cleanup,
8
+ createRoot,
9
+ inert,
10
+ isLive,
11
+ live,
12
+ memo,
13
+ setDevMode,
14
+ signal,
15
+ store,
16
+ type Accessor,
17
+ type LiveBinding,
18
+ type Reactive,
19
+ type Setter,
20
+ type StoreSetter,
21
+ } from "../../shared/warren/reactive";
22
+ export { isUIElement } from "../../shared/warren/jsx";
23
+ export type {
24
+ ForProps,
25
+ MatchProps,
26
+ ShowProps,
27
+ SwitchProps,
28
+ UIChild,
29
+ UIElement,
30
+ } from "../../shared/warren/jsx";
31
+ export {
32
+ For,
33
+ Fragment,
34
+ Match,
35
+ Portal,
36
+ Show,
37
+ Switch,
38
+ currentParent,
39
+ jsx,
40
+ jsxs,
41
+ mountChild,
42
+ render,
43
+ } from "./dom";
44
+ export type { DomProps, JSX } from "./jsx-runtime";
@@ -0,0 +1,16 @@
1
+ // Dev-transform entrypoint: same runtime, jsxDEV signature.
2
+ import { Fragment, jsx, type UIElement } from "./jsx-runtime";
3
+
4
+ export { Fragment };
5
+ export type { DomProps, JSX, UIChild, UIElement } from "./jsx-runtime";
6
+
7
+ export function jsxDEV(
8
+ type: Parameters<typeof jsx>[0],
9
+ props: Parameters<typeof jsx>[1],
10
+ key?: unknown,
11
+ _isStaticChildren?: boolean,
12
+ _source?: unknown,
13
+ _self?: unknown,
14
+ ): UIElement {
15
+ return jsx(type, props, key);
16
+ }
@@ -0,0 +1,56 @@
1
+ // JSX runtime for Warren's DOM renderer. Point tsconfig at it:
2
+ //
3
+ // { "jsx": "react-jsx", "jsxImportSource": "electrobun/browser/ui" }
4
+ //
5
+ // (Type-checking configuration only — Cottontail/Bun transpile .tsx natively.)
6
+ //
7
+ // Any HTML/SVG tag works: the runtime never enumerates elements, it calls
8
+ // document.createElement(tag). The IntrinsicElements typing below is
9
+ // deliberately open (Solid-style, types only).
10
+
11
+ import type { Reactive } from "../../shared/warren/reactive";
12
+ import type { UIChild, UIElement } from "../../shared/warren/jsx";
13
+ import { Fragment, jsx, jsxs } from "./dom";
14
+
15
+ export { Fragment, jsx, jsxs };
16
+ export type { UIChild, UIElement };
17
+
18
+ export interface DomProps {
19
+ children?: UIChild;
20
+ class?: Reactive<string>;
21
+ className?: Reactive<string>;
22
+ classList?: Reactive<Record<string, boolean | undefined>>;
23
+ style?: Reactive<string | Record<string, string | number | undefined>>;
24
+ id?: Reactive<string>;
25
+ ref?: (el: any) => void;
26
+ onClick?: (e: any) => void;
27
+ onInput?: (e: any) => void;
28
+ onChange?: (e: any) => void;
29
+ onKeyDown?: (e: any) => void;
30
+ onKeyUp?: (e: any) => void;
31
+ onPointerDown?: (e: any) => void;
32
+ onPointerUp?: (e: any) => void;
33
+ onPointerMove?: (e: any) => void;
34
+ onMouseDown?: (e: any) => void;
35
+ onMouseUp?: (e: any) => void;
36
+ onMouseEnter?: (e: any) => void;
37
+ onMouseLeave?: (e: any) => void;
38
+ onFocus?: (e: any) => void;
39
+ onBlur?: (e: any) => void;
40
+ onScroll?: (e: any) => void;
41
+ onWheel?: (e: any) => void;
42
+ onSubmit?: (e: any) => void;
43
+ onDblClick?: (e: any) => void;
44
+ onContextMenu?: (e: any) => void;
45
+ [key: string]: unknown;
46
+ }
47
+
48
+ export declare namespace JSX {
49
+ type Element = UIElement;
50
+ interface ElementChildrenAttribute {
51
+ children: {};
52
+ }
53
+ interface IntrinsicElements {
54
+ [tag: string]: DomProps;
55
+ }
56
+ }
@@ -173,6 +173,39 @@ export interface ElectrobunConfig {
173
173
  * @default "src/bun/index.ts"
174
174
  */
175
175
  entrypoint?: string;
176
+
177
+ /**
178
+ * Ship the main process as precompiled JavaScriptCore bytecode
179
+ * instead of (or alongside) source. Cottontail-only.
180
+ *
181
+ * > **Coming soon.** This option is declared but not yet wired —
182
+ * > setting it is currently a no-op (the app ships normal source).
183
+ * > Build-time bytecode/obfuscation lands as a fast-follow after
184
+ * > Electrobun 2.0; the underlying Cottontail/JSC support is already
185
+ * > in place.
186
+ *
187
+ * - `false` (default): ship JavaScript source. Normal builds.
188
+ * - `true`: compile the main process to bytecode for faster startup
189
+ * (skips parsing at launch). Source is still shipped as a fallback,
190
+ * so the app stays fully debuggable and `Function.prototype.toString`
191
+ * returns real source. Low risk — a version mismatch falls back to
192
+ * parsing the source.
193
+ * - `"obfuscate"`: bytecode **and** strip the source. Ships only
194
+ * bytecode, so the app's source is not distributed. Tradeoffs:
195
+ * `Function.prototype.toString` returns `[native code]` (can break
196
+ * libraries that introspect function source), error stacks lose
197
+ * source frames, no sourcemaps, and there is no source fallback
198
+ * (a runtime/version mismatch is a hard error rather than a reparse).
199
+ * This is source obfuscation, not encryption.
200
+ *
201
+ * Dev builds (`--env=dev`) ignore this and always ship source so
202
+ * sourcemaps, the debugger, and hot reload keep working. The
203
+ * bytecode is generated against the exact Cottontail bundled into
204
+ * this app and regenerated on every build.
205
+ *
206
+ * @default false
207
+ */
208
+ bytecode?: boolean | "obfuscate";
176
209
  } & BundlerOptions;
177
210
 
178
211
  /**
@@ -2,7 +2,7 @@
2
2
  // Run "hutch build.ts" or "hutch run build:dev" from the package folder to regenerate.
3
3
 
4
4
  // Full preload for trusted webviews (RPC, encryption, drag regions, webview tags)
5
- export const preloadScript = "(function(){// src/preload/encryption.ts\nfunction base64ToUint8Array(base64) {\n return new Uint8Array(atob(base64).split(\"\").map((char) => char.charCodeAt(0)));\n}\nfunction uint8ArrayToBase64(uint8Array) {\n let binary = \"\";\n for (let i = 0;i < uint8Array.length; i++) {\n binary += String.fromCharCode(uint8Array[i]);\n }\n return btoa(binary);\n}\nfunction toArrayBuffer(bytes) {\n const buffer = new ArrayBuffer(bytes.byteLength);\n new Uint8Array(buffer).set(bytes);\n return buffer;\n}\nasync function generateKeyFromBytes(rawKey) {\n return await window.crypto.subtle.importKey(\"raw\", toArrayBuffer(rawKey), { name: \"AES-GCM\" }, true, [\"encrypt\", \"decrypt\"]);\n}\nasync function initEncryption() {\n const secretKey = await generateKeyFromBytes(new Uint8Array(window.__electrobunSecretKeyBytes));\n const encryptString = async (plaintext) => {\n const encoder = new TextEncoder;\n const encodedText = encoder.encode(plaintext);\n const iv = window.crypto.getRandomValues(new Uint8Array(12));\n const encryptedBuffer = await window.crypto.subtle.encrypt({ name: \"AES-GCM\", iv: toArrayBuffer(iv) }, secretKey, toArrayBuffer(encodedText));\n const encryptedData = new Uint8Array(encryptedBuffer.slice(0, -16));\n const tag = new Uint8Array(encryptedBuffer.slice(-16));\n return {\n encryptedData: uint8ArrayToBase64(encryptedData),\n iv: uint8ArrayToBase64(iv),\n tag: uint8ArrayToBase64(tag)\n };\n };\n const decryptString = async (encryptedDataB64, ivB64, tagB64) => {\n const encryptedData = base64ToUint8Array(encryptedDataB64);\n const iv = base64ToUint8Array(ivB64);\n const tag = base64ToUint8Array(tagB64);\n const combinedData = new Uint8Array(encryptedData.length + tag.length);\n combinedData.set(encryptedData);\n combinedData.set(tag, encryptedData.length);\n const decryptedBuffer = await window.crypto.subtle.decrypt({ name: \"AES-GCM\", iv: toArrayBuffer(iv) }, secretKey, toArrayBuffer(combinedData));\n const decoder = new TextDecoder;\n return decoder.decode(decryptedBuffer);\n };\n window.__electrobun_encrypt = encryptString;\n window.__electrobun_decrypt = decryptString;\n}\n\n// src/preload/internalRpc.ts\nvar pendingRequests = {};\nvar requestId = 0;\nvar isProcessingQueue = false;\nvar sendQueue = [];\nfunction processQueue() {\n if (isProcessingQueue) {\n setTimeout(processQueue);\n return;\n }\n if (sendQueue.length === 0)\n return;\n isProcessingQueue = true;\n const batch = JSON.stringify(sendQueue);\n sendQueue.length = 0;\n window.__electrobunInternalBridge?.postMessage(batch);\n setTimeout(() => {\n isProcessingQueue = false;\n }, 2);\n}\nfunction send(type, payload) {\n sendQueue.push(JSON.stringify({ type: \"message\", id: type, payload }));\n processQueue();\n}\nfunction request(type, payload) {\n return new Promise((resolve, reject) => {\n const id = `req_${++requestId}_${Date.now()}`;\n pendingRequests[id] = { resolve, reject };\n sendQueue.push(JSON.stringify({\n type: \"request\",\n method: type,\n id,\n params: payload,\n hostWebviewId: window.__electrobunWebviewId\n }));\n processQueue();\n setTimeout(() => {\n if (pendingRequests[id]) {\n delete pendingRequests[id];\n reject(new Error(`Request timeout: ${type}`));\n }\n }, 1e4);\n });\n}\nfunction handleResponse(msg) {\n if (msg && msg.type === \"response\" && msg.id) {\n const pending = pendingRequests[msg.id];\n if (pending) {\n delete pendingRequests[msg.id];\n if (msg.success)\n pending.resolve(msg.payload);\n else\n pending.reject(msg.payload);\n }\n }\n}\n\n// src/preload/dragRegions.ts\nvar DRAG_CLASS = \"electrobun-webkit-app-region-drag\";\nvar NO_DRAG_CLASS = \"electrobun-webkit-app-region-no-drag\";\nvar MIRRORED_PROPERTY = \"--electrobun-app-region\";\nvar MIRROR_ATTRIBUTE = \"data-electrobun-app-region-mirror\";\nvar APP_REGION_PROPERTIES = [\n MIRRORED_PROPERTY,\n \"-webkit-app-region\",\n \"app-region\",\n \"window-drag\"\n];\nvar DRAG_REGION_COMPATIBILITY_CSS = `\n.electrobun-webkit-app-region-drag {\n\t-webkit-app-region: drag;\n\tapp-region: drag;\n}\n.electrobun-webkit-app-region-no-drag {\n\t-webkit-app-region: no-drag;\n\tapp-region: no-drag;\n}\n`;\nvar processedSourceSignatures = new WeakMap;\nvar stylesheetMirrors = new Map;\nvar pendingLinkSignatures = new WeakMap;\nvar stylesheetMirroringInitialized = false;\nfunction normalizedRegion(value) {\n const normalized = value?.trim().toLowerCase();\n if (normalized === \"drag\" || normalized === \"no-drag\")\n return normalized;\n return null;\n}\nfunction previousSignificantCharacter(source, offset) {\n let index = offset - 1;\n while (index >= 0) {\n while (index >= 0 && /\\s/.test(source[index] ?? \"\"))\n index--;\n if (index >= 1 && source[index] === \"/\" && source[index - 1] === \"*\") {\n const commentStart = source.lastIndexOf(\"/*\", index - 1);\n if (commentStart < 0)\n return \"\";\n index = commentStart - 1;\n continue;\n }\n return source[index] ?? \"\";\n }\n return \"\";\n}\nfunction isIdentifierCharacter(character) {\n return !!character && /[a-zA-Z0-9_-]/.test(character);\n}\nfunction rewriteAppRegionDeclarations(source) {\n const propertyNames = [\"-webkit-app-region\", \"window-drag\", \"app-region\"];\n let output = \"\";\n let cursor = 0;\n let index = 0;\n while (index < source.length) {\n if (source[index] === \"/\" && source[index + 1] === \"*\") {\n const commentEnd = source.indexOf(\"*/\", index + 2);\n index = commentEnd < 0 ? source.length : commentEnd + 2;\n continue;\n }\n if (source[index] === '\"' || source[index] === \"'\") {\n const quote = source[index];\n index++;\n while (index < source.length) {\n if (source[index] === \"\\\\\") {\n index += 2;\n continue;\n }\n if (source[index] === quote) {\n index++;\n break;\n }\n index++;\n }\n continue;\n }\n let matchedProperty;\n for (const propertyName of propertyNames) {\n if (source.slice(index, index + propertyName.length).toLowerCase() === propertyName && !isIdentifierCharacter(source[index - 1]) && !isIdentifierCharacter(source[index + propertyName.length])) {\n matchedProperty = propertyName;\n break;\n }\n }\n if (matchedProperty) {\n let colonOffset = index + matchedProperty.length;\n while (/\\s/.test(source[colonOffset] ?? \"\"))\n colonOffset++;\n const previous = previousSignificantCharacter(source, index);\n if (source[colonOffset] === \":\" && (previous === \"{\" || previous === \";\")) {\n output += source.slice(cursor, index) + MIRRORED_PROPERTY;\n index += matchedProperty.length;\n cursor = index;\n continue;\n }\n }\n index++;\n }\n return output + source.slice(cursor);\n}\nfunction nestedRuleContainer(rule) {\n const candidate = rule;\n if (candidate.cssRules && typeof candidate.deleteRule === \"function\") {\n return candidate;\n }\n return null;\n}\nfunction pruneToAppRegionRules(container) {\n let hasAppRegionRule = false;\n for (let index = container.cssRules.length - 1;index >= 0; index--) {\n const rule = container.cssRules[index];\n if (!rule)\n continue;\n const style = rule.style;\n let hasAppRegionDeclaration = false;\n if (style && typeof style.getPropertyValue === \"function\") {\n for (let propertyIndex = style.length - 1;propertyIndex >= 0; propertyIndex--) {\n const propertyName = style.item(propertyIndex);\n if (propertyName === MIRRORED_PROPERTY) {\n hasAppRegionDeclaration = true;\n } else {\n style.removeProperty(propertyName);\n }\n }\n }\n const nested = nestedRuleContainer(rule);\n const hasNestedAppRegionRule = nested ? pruneToAppRegionRules(nested) : false;\n if (!hasAppRegionDeclaration && !hasNestedAppRegionRule) {\n container.deleteRule(index);\n continue;\n }\n hasAppRegionRule = true;\n }\n return hasAppRegionRule;\n}\nfunction serializeRules(rules) {\n let cssText = \"\";\n for (let index = 0;index < rules.length; index++) {\n const rule = rules[index];\n if (rule)\n cssText += `${rule.cssText}\n`;\n }\n return cssText;\n}\nfunction removeStylesheetMirror(source) {\n stylesheetMirrors.get(source)?.remove();\n stylesheetMirrors.delete(source);\n}\nfunction installStylesheetMirror(source, cssText, signature) {\n if (processedSourceSignatures.get(source) === signature)\n return;\n processedSourceSignatures.set(source, signature);\n removeStylesheetMirror(source);\n const mirroredCss = rewriteAppRegionDeclarations(cssText);\n if (mirroredCss === cssText || !source.parentNode)\n return;\n const mirror = source.ownerDocument.createElement(\"style\");\n mirror.setAttribute(MIRROR_ATTRIBUTE, \"\");\n mirror.media = \"not all\";\n if (source.nonce)\n mirror.nonce = source.nonce;\n mirror.textContent = mirroredCss;\n source.parentNode.insertBefore(mirror, source.nextSibling);\n let sheet = mirror.sheet;\n if (!sheet || !pruneToAppRegionRules(sheet)) {\n mirror.remove();\n return;\n }\n mirror.textContent = serializeRules(sheet.cssRules);\n sheet = mirror.sheet;\n if (!sheet) {\n mirror.remove();\n return;\n }\n mirror.media = source.media;\n if (source.sheet?.disabled)\n sheet.disabled = true;\n stylesheetMirrors.set(source, mirror);\n}\nfunction isMirroredStyle(element) {\n return element.tagName === \"STYLE\" && element.hasAttribute(MIRROR_ATTRIBUTE);\n}\nfunction isStylesheetSource(element) {\n if (element.tagName === \"STYLE\")\n return !isMirroredStyle(element);\n return element.tagName === \"LINK\" && element.relList.contains(\"stylesheet\");\n}\nfunction nodeContainsStylesheetSource(node) {\n const element = node;\n if (typeof element.matches !== \"function\")\n return false;\n return isStylesheetSource(element) || !!element.querySelector(\"style:not([data-electrobun-app-region-mirror]), link[rel~='stylesheet']\");\n}\nfunction mutationsAffectStylesheets(mutations) {\n for (const mutation of mutations) {\n if (mutation.type === \"attributes\") {\n const element = mutation.target;\n if (element.tagName === \"LINK\" || isStylesheetSource(element))\n return true;\n continue;\n }\n if (mutation.type === \"characterData\") {\n const parent = mutation.target.parentElement;\n if (parent && isStylesheetSource(parent))\n return true;\n continue;\n }\n const target = mutation.target;\n if (typeof target.matches === \"function\" && isStylesheetSource(target)) {\n return true;\n }\n for (let index = 0;index < mutation.addedNodes.length; index++) {\n const node = mutation.addedNodes[index];\n if (node && nodeContainsStylesheetSource(node))\n return true;\n }\n for (let index = 0;index < mutation.removedNodes.length; index++) {\n const node = mutation.removedNodes[index];\n if (node && nodeContainsStylesheetSource(node))\n return true;\n }\n }\n return false;\n}\nfunction processStyleElement(style) {\n if (isMirroredStyle(style))\n return;\n const cssText = style.textContent ?? \"\";\n installStylesheetMirror(style, cssText, `${style.media}\n${cssText}`);\n}\nfunction processLinkElement(link) {\n if (!link.relList.contains(\"stylesheet\") || !link.href) {\n removeStylesheetMirror(link);\n return;\n }\n const signature = `${link.media}\n${link.href}`;\n if (processedSourceSignatures.get(link) === signature || pendingLinkSignatures.get(link) === signature) {\n return;\n }\n pendingLinkSignatures.set(link, signature);\n fetch(link.href, { credentials: \"same-origin\" }).then((response) => {\n if (!response.ok)\n throw new Error(`HTTP ${response.status}`);\n return response.text();\n }).then((cssText) => {\n if (link.isConnected && `${link.media}\n${link.href}` === signature) {\n installStylesheetMirror(link, cssText, signature);\n }\n }).catch(() => {}).finally(() => {\n if (pendingLinkSignatures.get(link) === signature) {\n pendingLinkSignatures.delete(link);\n }\n });\n}\nfunction scanStylesheetSources() {\n document.querySelectorAll(\"style, link[rel~='stylesheet']\").forEach((element) => {\n if (element.tagName === \"STYLE\") {\n processStyleElement(element);\n } else {\n processLinkElement(element);\n }\n });\n for (const [source, mirror] of stylesheetMirrors) {\n if (!source.isConnected) {\n mirror.remove();\n stylesheetMirrors.delete(source);\n }\n }\n}\nfunction initStylesheetMirroring() {\n if (stylesheetMirroringInitialized)\n return;\n stylesheetMirroringInitialized = true;\n let scanScheduled = false;\n const scheduleScan = () => {\n if (scanScheduled)\n return;\n scanScheduled = true;\n queueMicrotask(() => {\n scanScheduled = false;\n scanStylesheetSources();\n });\n };\n new MutationObserver((mutations) => {\n if (mutationsAffectStylesheets(mutations))\n scheduleScan();\n }).observe(document, {\n attributes: true,\n attributeFilter: [\"href\", \"media\", \"rel\"],\n characterData: true,\n childList: true,\n subtree: true\n });\n document.addEventListener(\"DOMContentLoaded\", scheduleScan, { once: true });\n scheduleScan();\n}\nfunction installCompatibilityStyles() {\n if (document.querySelector(\"style[data-electrobun-drag-region-compat]\"))\n return;\n const style = document.createElement(\"style\");\n style.setAttribute(\"data-electrobun-drag-region-compat\", \"\");\n style.textContent = DRAG_REGION_COMPATIBILITY_CSS;\n const install = () => {\n if (!document.head || style.isConnected)\n return;\n document.head.insertBefore(style, document.head.firstChild);\n };\n if (document.head)\n install();\n else\n document.addEventListener(\"DOMContentLoaded\", install, { once: true });\n}\nfunction inlineRegion(element) {\n const style = element.getAttribute?.(\"style\");\n if (!style)\n return null;\n const match = style.match(/(?:^|;)\\s*(?:-webkit-app-region|app-region|window-drag)\\s*:\\s*(no-drag|drag)\\b/i);\n return normalizedRegion(match?.[1]);\n}\nfunction computedRegion(element, readComputedStyle) {\n try {\n const style = readComputedStyle(element);\n for (const propertyName of APP_REGION_PROPERTIES) {\n const region = normalizedRegion(style.getPropertyValue(propertyName));\n if (region)\n return region;\n }\n } catch {}\n return null;\n}\nfunction eventTargetElement(target) {\n if (!target || typeof target.getAttribute !== \"function\") {\n return target?.parentElement ?? null;\n }\n return target;\n}\nfunction isAppRegionDragTarget(target, readComputedStyle = (element) => window.getComputedStyle(element)) {\n let element = eventTargetElement(target);\n let foundDragRegion = false;\n while (element) {\n if (element.classList?.contains(NO_DRAG_CLASS))\n return false;\n if (element.classList?.contains(DRAG_CLASS))\n foundDragRegion = true;\n const region = inlineRegion(element) ?? computedRegion(element, readComputedStyle);\n if (region === \"no-drag\")\n return false;\n if (region === \"drag\")\n foundDragRegion = true;\n element = element.parentElement;\n }\n return foundDragRegion;\n}\nfunction registerDragRegionListeners(targetDocument, getWindowId, sendMessage = send, readComputedStyle = (element) => window.getComputedStyle(element)) {\n targetDocument.addEventListener(\"mousedown\", (event) => {\n if (isAppRegionDragTarget(event.target, readComputedStyle)) {\n sendMessage(\"startWindowMove\", { id: getWindowId() });\n }\n });\n targetDocument.addEventListener(\"mouseup\", (event) => {\n if (isAppRegionDragTarget(event.target, readComputedStyle)) {\n sendMessage(\"stopWindowMove\", { id: getWindowId() });\n }\n });\n}\nfunction initDragRegions() {\n installCompatibilityStyles();\n initStylesheetMirroring();\n registerDragRegionListeners(document, () => window.__electrobunWindowId);\n}\n\n// src/preload/externalDropFocus.ts\nfunction initExternalDropFocusRestoration(targetWindow = window, platform = window.__electrobunPlatform, requestFocus = request, schedule = (callback) => setTimeout(callback)) {\n if (platform !== \"windows\")\n return;\n targetWindow.addEventListener(\"drop\", (event) => {\n const types = Array.from(event.dataTransfer?.types ?? []);\n if (!types.includes(\"Files\"))\n return;\n const previouslyFocusedElement = targetWindow.document.activeElement;\n schedule(() => {\n requestFocus(\"restoreWindowFocusAfterExternalDrop\", {\n windowId: targetWindow.__electrobunWindowId\n }).catch(() => {\n return;\n }).then(() => {\n targetWindow.focus();\n if (previouslyFocusedElement && \"focus\" in previouslyFocusedElement && typeof previouslyFocusedElement.focus === \"function\") {\n previouslyFocusedElement.focus();\n }\n });\n });\n }, true);\n}\n\n// src/preload/overlaySync.ts\nclass OverlaySyncController {\n element;\n options;\n lastRect = { x: 0, y: 0, width: 0, height: 0 };\n resizeObserver = null;\n positionLoop = null;\n resizeHandler = null;\n burstUntil = 0;\n constructor(element, options) {\n this.element = element;\n this.options = {\n onSync: options.onSync,\n getMasks: options.getMasks ?? (() => []),\n burstIntervalMs: options.burstIntervalMs ?? 50,\n baseIntervalMs: options.baseIntervalMs ?? 100,\n burstDurationMs: options.burstDurationMs ?? 500\n };\n }\n start() {\n this.resizeObserver = new ResizeObserver(() => this.sync());\n this.resizeObserver.observe(this.element);\n const loop = () => {\n this.sync();\n const now = performance.now();\n const interval = now < this.burstUntil ? this.options.burstIntervalMs : this.options.baseIntervalMs;\n this.positionLoop = setTimeout(loop, interval);\n };\n this.positionLoop = setTimeout(loop, this.options.baseIntervalMs);\n this.resizeHandler = () => this.sync(true);\n window.addEventListener(\"resize\", this.resizeHandler);\n }\n stop() {\n if (this.resizeObserver)\n this.resizeObserver.disconnect();\n if (this.positionLoop)\n clearTimeout(this.positionLoop);\n if (this.resizeHandler) {\n window.removeEventListener(\"resize\", this.resizeHandler);\n }\n this.resizeObserver = null;\n this.positionLoop = null;\n this.resizeHandler = null;\n }\n forceSync() {\n this.sync(true);\n }\n setLastRect(rect) {\n this.lastRect = rect;\n }\n sync(force = false) {\n const rect = this.element.getBoundingClientRect();\n const newRect = {\n x: rect.x,\n y: rect.y,\n width: rect.width,\n height: rect.height\n };\n if (newRect.width === 0 && newRect.height === 0) {\n return;\n }\n if (!force && newRect.x === this.lastRect.x && newRect.y === this.lastRect.y && newRect.width === this.lastRect.width && newRect.height === this.lastRect.height) {\n return;\n }\n this.burstUntil = performance.now() + this.options.burstDurationMs;\n this.lastRect = newRect;\n const masks = this.options.getMasks();\n this.options.onSync(newRect, JSON.stringify(masks));\n }\n}\n\n// src/preload/webviewTagNavigation.ts\nclass WebviewTagNavigationQueue {\n pending = null;\n beginInitialization() {\n this.pending = null;\n }\n defer(navigation) {\n this.pending = navigation;\n }\n take() {\n const navigation = this.pending;\n this.pending = null;\n return navigation;\n }\n}\n\n// src/preload/webviewTag.ts\nvar webviewRegistry = {};\n\nclass ElectrobunWebviewTag extends HTMLElement {\n webviewId = null;\n maskSelectors = new Set;\n _sync = null;\n _navigationQueue = new WebviewTagNavigationQueue;\n _initializationStarted = false;\n transparent = false;\n passthroughEnabled = false;\n spellCheckEnabled = false;\n hidden = false;\n sandboxed = false;\n _eventListeners = {};\n static get observedAttributes() {\n return [\"src\", \"html\", \"spellcheck\"];\n }\n constructor() {\n super();\n }\n connectedCallback() {\n requestAnimationFrame(() => this.initWebview());\n }\n attributeChangedCallback(name, oldValue, newValue) {\n if (oldValue === newValue)\n return;\n if (name === \"src\" && newValue !== null) {\n this.navigate({ kind: \"url\", value: newValue });\n } else if (name === \"html\" && newValue !== null) {\n this.navigate({ kind: \"html\", value: newValue });\n } else if (name === \"spellcheck\" && this.webviewId !== null) {\n this.setSpellCheck(newValue !== null && newValue.toLowerCase() !== \"false\");\n }\n }\n disconnectedCallback() {\n if (this.webviewId !== null) {\n send(\"webviewTagRemove\", { id: this.webviewId });\n delete webviewRegistry[this.webviewId];\n }\n if (this._sync)\n this._sync.stop();\n }\n getInitialNavigationRules() {\n const rawRules = this.getAttribute(\"navigation-rules\");\n if (rawRules === null) {\n return null;\n }\n const trimmed = rawRules.trim();\n if (!trimmed) {\n return [];\n }\n try {\n const parsed = JSON.parse(trimmed);\n if (!Array.isArray(parsed) || !parsed.every((rule) => typeof rule === \"string\")) {\n throw new Error(\"navigation-rules must be a JSON string array\");\n }\n return parsed;\n } catch (error) {\n console.error(\"Invalid navigation-rules attribute:\", error);\n return [];\n }\n }\n async initWebview() {\n this._initializationStarted = true;\n this._navigationQueue.beginInitialization();\n const rect = this.getBoundingClientRect();\n const initialRect = {\n x: rect.x,\n y: rect.y,\n width: rect.width,\n height: rect.height\n };\n const url = this.getAttribute(\"src\");\n const html = this.getAttribute(\"html\");\n const preload = this.getAttribute(\"preload\");\n const partition = this.getAttribute(\"partition\");\n const renderer = this.getAttribute(\"renderer\") || \"native\";\n const masks = this.getAttribute(\"masks\");\n const navigationRules = this.getInitialNavigationRules();\n const sandbox = this.hasAttribute(\"sandbox\");\n this.sandboxed = sandbox;\n const transparent = this.hasAttribute(\"transparent\");\n const passthrough = this.hasAttribute(\"passthrough\");\n const spellCheckAttribute = this.getAttribute(\"spellcheck\");\n const spellCheck = spellCheckAttribute !== null && spellCheckAttribute.toLowerCase() !== \"false\";\n this.transparent = transparent;\n this.passthroughEnabled = passthrough;\n this.spellCheckEnabled = spellCheck;\n if (transparent)\n this.style.opacity = \"0\";\n if (passthrough)\n this.style.pointerEvents = \"none\";\n if (masks) {\n masks.split(\",\").forEach((s) => this.maskSelectors.add(s.trim()));\n }\n try {\n const webviewInitParams = {\n hostWebviewId: window.__electrobunWebviewId,\n windowId: window.__electrobunWindowId,\n renderer,\n url,\n html,\n preload,\n partition,\n frame: initialRect,\n sandbox,\n transparent,\n passthrough,\n spellCheck,\n ...navigationRules === null ? {} : { navigationRules }\n };\n const webviewId = await request(\"webviewTagInit\", webviewInitParams);\n this.webviewId = webviewId;\n this.id = `electrobun-webview-${webviewId}`;\n webviewRegistry[webviewId] = this;\n const pendingNavigation = this._navigationQueue.take();\n if (pendingNavigation)\n this.navigate(pendingNavigation);\n this.setupObservers(initialRect);\n this.syncDimensions(true);\n requestAnimationFrame(() => {\n Object.values(webviewRegistry).forEach((webview) => {\n if (webview !== this && webview.webviewId !== null) {\n webview.syncDimensions(true);\n }\n });\n });\n } catch (err) {\n console.error(\"Failed to init webview:\", err);\n }\n }\n setupObservers(initialRect) {\n const getMasks = () => {\n const rect = this.getBoundingClientRect();\n const masks = [];\n this.maskSelectors.forEach((selector) => {\n try {\n document.querySelectorAll(selector).forEach((el) => {\n const mr = el.getBoundingClientRect();\n masks.push({\n x: mr.x - rect.x,\n y: mr.y - rect.y,\n width: mr.width,\n height: mr.height\n });\n });\n } catch (_e) {}\n });\n return masks;\n };\n this._sync = new OverlaySyncController(this, {\n onSync: (rect, masksJson) => {\n if (this.webviewId === null)\n return;\n send(\"webviewTagResize\", {\n id: this.webviewId,\n frame: rect,\n masks: masksJson\n });\n },\n getMasks,\n burstIntervalMs: 10,\n baseIntervalMs: 100,\n burstDurationMs: 50\n });\n this._sync.setLastRect(initialRect);\n this._sync.start();\n }\n syncDimensions(force = false) {\n if (!this._sync)\n return;\n if (force) {\n this._sync.forceSync();\n }\n }\n navigate(navigation) {\n if (this.webviewId === null) {\n this._navigationQueue.defer(navigation);\n return;\n }\n if (navigation.kind === \"url\") {\n send(\"webviewTagUpdateSrc\", {\n id: this.webviewId,\n url: navigation.value\n });\n } else {\n send(\"webviewTagUpdateHtml\", {\n id: this.webviewId,\n html: navigation.value\n });\n }\n }\n loadURL(url) {\n if (this.getAttribute(\"src\") === url) {\n this.navigate({ kind: \"url\", value: url });\n } else {\n this.setAttribute(\"src\", url);\n }\n }\n loadHTML(html) {\n if (this.webviewId === null && !this._initializationStarted) {\n this.setAttribute(\"html\", html);\n return;\n }\n this.navigate({ kind: \"html\", value: html });\n }\n reload() {\n if (this.webviewId !== null)\n send(\"webviewTagReload\", { id: this.webviewId });\n }\n goBack() {\n if (this.webviewId !== null)\n send(\"webviewTagGoBack\", { id: this.webviewId });\n }\n goForward() {\n if (this.webviewId !== null)\n send(\"webviewTagGoForward\", { id: this.webviewId });\n }\n async canGoBack() {\n if (this.webviewId === null)\n return false;\n return await request(\"webviewTagCanGoBack\", {\n id: this.webviewId\n });\n }\n async canGoForward() {\n if (this.webviewId === null)\n return false;\n return await request(\"webviewTagCanGoForward\", {\n id: this.webviewId\n });\n }\n async setSpellCheck(enabled) {\n this.spellCheckEnabled = enabled;\n if (this.webviewId === null)\n return false;\n return await request(\"webviewTagSetSpellCheck\", {\n id: this.webviewId,\n enabled\n });\n }\n toggleTransparent(value) {\n if (this.webviewId === null)\n return;\n this.transparent = value !== undefined ? value : !this.transparent;\n this.style.opacity = this.transparent ? \"0\" : \"\";\n send(\"webviewTagSetTransparent\", {\n id: this.webviewId,\n transparent: this.transparent\n });\n }\n togglePassthrough(value) {\n if (this.webviewId === null)\n return;\n this.passthroughEnabled = value !== undefined ? value : !this.passthroughEnabled;\n this.style.pointerEvents = this.passthroughEnabled ? \"none\" : \"\";\n send(\"webviewTagSetPassthrough\", {\n id: this.webviewId,\n enablePassthrough: this.passthroughEnabled\n });\n }\n toggleHidden(value) {\n if (this.webviewId === null)\n return;\n this.hidden = value !== undefined ? value : !this.hidden;\n send(\"webviewTagSetHidden\", { id: this.webviewId, hidden: this.hidden });\n }\n addMaskSelector(selector) {\n this.maskSelectors.add(selector);\n this.syncDimensions(true);\n }\n removeMaskSelector(selector) {\n this.maskSelectors.delete(selector);\n this.syncDimensions(true);\n }\n setNavigationRules(rules) {\n if (this.webviewId !== null) {\n send(\"webviewTagSetNavigationRules\", { id: this.webviewId, rules });\n }\n }\n findInPage(searchText, options) {\n if (this.webviewId === null)\n return;\n const forward = options?.forward !== false;\n const matchCase = options?.matchCase || false;\n send(\"webviewTagFindInPage\", {\n id: this.webviewId,\n searchText,\n forward,\n matchCase\n });\n }\n stopFindInPage() {\n if (this.webviewId !== null)\n send(\"webviewTagStopFind\", { id: this.webviewId });\n }\n openDevTools() {\n if (this.webviewId !== null)\n send(\"webviewTagOpenDevTools\", { id: this.webviewId });\n }\n closeDevTools() {\n if (this.webviewId !== null)\n send(\"webviewTagCloseDevTools\", { id: this.webviewId });\n }\n toggleDevTools() {\n if (this.webviewId !== null)\n send(\"webviewTagToggleDevTools\", { id: this.webviewId });\n }\n executeJavascript(js) {\n if (this.webviewId === null)\n return;\n send(\"webviewTagExecuteJavascript\", { id: this.webviewId, js });\n }\n on(event, listener) {\n if (!this._eventListeners[event])\n this._eventListeners[event] = [];\n this._eventListeners[event].push(listener);\n }\n off(event, listener) {\n if (!this._eventListeners[event])\n return;\n const idx = this._eventListeners[event].indexOf(listener);\n if (idx !== -1)\n this._eventListeners[event].splice(idx, 1);\n }\n emit(event, detail) {\n const listeners = this._eventListeners[event];\n if (listeners) {\n const customEvent = new CustomEvent(event, { detail });\n listeners.forEach((fn) => fn(customEvent));\n }\n }\n get src() {\n return this.getAttribute(\"src\");\n }\n set src(value) {\n if (value) {\n this.setAttribute(\"src\", value);\n } else {\n this.removeAttribute(\"src\");\n }\n }\n get html() {\n return this.getAttribute(\"html\");\n }\n set html(value) {\n if (value) {\n this.setAttribute(\"html\", value);\n } else {\n this.removeAttribute(\"html\");\n }\n }\n get preload() {\n return this.getAttribute(\"preload\");\n }\n set preload(value) {\n if (value)\n this.setAttribute(\"preload\", value);\n else\n this.removeAttribute(\"preload\");\n }\n get renderer() {\n return this.getAttribute(\"renderer\") || \"native\";\n }\n set renderer(value) {\n this.setAttribute(\"renderer\", value);\n }\n get sandbox() {\n return this.sandboxed;\n }\n}\nfunction initWebviewTag() {\n if (!customElements.get(\"electrobun-webview\")) {\n customElements.define(\"electrobun-webview\", ElectrobunWebviewTag);\n }\n const injectStyles = () => {\n const style = document.createElement(\"style\");\n style.textContent = `\nelectrobun-webview {\n\tdisplay: block;\n\twidth: 800px;\n\theight: 300px;\n\tbackground: #fff;\n\tbackground-repeat: no-repeat !important;\n\toverflow: hidden;\n}\n`;\n if (document.head?.firstChild) {\n document.head.insertBefore(style, document.head.firstChild);\n } else if (document.head) {\n document.head.appendChild(style);\n }\n };\n if (document.head) {\n injectStyles();\n } else {\n document.addEventListener(\"DOMContentLoaded\", injectStyles);\n }\n}\n\n// src/preload/wgpuTag.ts\nvar wgpuTagRegistry = {};\n\nclass ElectrobunWgpuTag extends HTMLElement {\n wgpuViewId = null;\n maskSelectors = new Set;\n _sync = null;\n transparent = false;\n passthroughEnabled = false;\n hidden = false;\n _ready = false;\n _initializing = false;\n _eventListeners = {};\n constructor() {\n super();\n }\n connectedCallback() {\n requestAnimationFrame(() => {\n if (this.isConnected)\n this.initWgpuView();\n });\n }\n disconnectedCallback() {\n if (this.wgpuViewId !== null) {\n send(\"wgpuTagRemove\", { id: this.wgpuViewId });\n delete wgpuTagRegistry[this.wgpuViewId];\n this.wgpuViewId = null;\n }\n if (this._sync)\n this._sync.stop();\n this._sync = null;\n this._ready = false;\n }\n async initWgpuView() {\n if (this._initializing || this.wgpuViewId !== null)\n return;\n this._initializing = true;\n const rect = this.getBoundingClientRect();\n const initialRect = {\n x: rect.x,\n y: rect.y,\n width: rect.width,\n height: rect.height\n };\n const transparent = this.hasAttribute(\"transparent\");\n const passthrough = this.hasAttribute(\"passthrough\");\n const hidden = this.hasAttribute(\"hidden\");\n const masks = this.getAttribute(\"masks\");\n this.transparent = transparent;\n this.passthroughEnabled = passthrough;\n this.hidden = hidden;\n if (masks) {\n masks.split(\",\").forEach((s) => this.maskSelectors.add(s.trim()));\n }\n if (transparent)\n this.style.opacity = \"0\";\n if (passthrough)\n this.style.pointerEvents = \"none\";\n try {\n const wgpuViewId = await request(\"wgpuTagInit\", {\n windowId: window.__electrobunWindowId,\n frame: initialRect,\n transparent,\n passthrough\n });\n if (!this.isConnected) {\n send(\"wgpuTagRemove\", { id: wgpuViewId });\n return;\n }\n this.wgpuViewId = wgpuViewId;\n if (!this.id) {\n this.id = `electrobun-wgpu-${wgpuViewId}`;\n }\n wgpuTagRegistry[wgpuViewId] = this;\n this.setupObservers(initialRect);\n this.syncDimensions(true);\n if (hidden) {\n this.toggleHidden(true);\n }\n requestAnimationFrame(() => {\n Object.values(wgpuTagRegistry).forEach((view) => {\n if (view !== this && view.wgpuViewId !== null) {\n view.syncDimensions(true);\n }\n });\n });\n this._ready = true;\n this.emit(\"ready\", { id: wgpuViewId });\n } catch (err) {\n console.error(\"Failed to init WGPU view:\", err);\n } finally {\n this._initializing = false;\n }\n }\n setupObservers(initialRect) {\n const getMasks = () => {\n const rect = this.getBoundingClientRect();\n const masks = [];\n this.maskSelectors.forEach((selector) => {\n try {\n document.querySelectorAll(selector).forEach((el) => {\n const mr = el.getBoundingClientRect();\n masks.push({\n x: mr.x - rect.x,\n y: mr.y - rect.y,\n width: mr.width,\n height: mr.height\n });\n });\n } catch (_e) {}\n });\n return masks;\n };\n this._sync = new OverlaySyncController(this, {\n onSync: (rect, masksJson) => {\n if (this.wgpuViewId === null)\n return;\n send(\"wgpuTagResize\", {\n id: this.wgpuViewId,\n frame: rect,\n masks: masksJson\n });\n },\n getMasks,\n burstIntervalMs: 10,\n baseIntervalMs: 100,\n burstDurationMs: 50\n });\n this._sync.setLastRect(initialRect);\n this._sync.start();\n }\n syncDimensions(force = false) {\n if (!this._sync)\n return;\n if (force) {\n this._sync.forceSync();\n }\n }\n toggleTransparent(value) {\n if (this.wgpuViewId === null)\n return;\n this.transparent = value !== undefined ? value : !this.transparent;\n this.style.opacity = this.transparent ? \"0\" : \"\";\n send(\"wgpuTagSetTransparent\", {\n id: this.wgpuViewId,\n transparent: this.transparent\n });\n }\n togglePassthrough(value) {\n if (this.wgpuViewId === null)\n return;\n this.passthroughEnabled = value !== undefined ? value : !this.passthroughEnabled;\n this.style.pointerEvents = this.passthroughEnabled ? \"none\" : \"\";\n send(\"wgpuTagSetPassthrough\", {\n id: this.wgpuViewId,\n passthrough: this.passthroughEnabled\n });\n }\n toggleHidden(value) {\n if (this.wgpuViewId === null)\n return;\n this.hidden = value !== undefined ? value : !this.hidden;\n send(\"wgpuTagSetHidden\", { id: this.wgpuViewId, hidden: this.hidden });\n }\n runTest() {\n if (this.wgpuViewId === null)\n return;\n send(\"wgpuTagRunTest\", { id: this.wgpuViewId });\n }\n addMaskSelector(selector) {\n this.maskSelectors.add(selector);\n this.syncDimensions(true);\n }\n removeMaskSelector(selector) {\n this.maskSelectors.delete(selector);\n this.syncDimensions(true);\n }\n on(event, listener) {\n if (!this._eventListeners[event])\n this._eventListeners[event] = [];\n this._eventListeners[event].push(listener);\n if (event === \"ready\" && this._ready && this.wgpuViewId !== null) {\n const readyEvent = new CustomEvent(event, {\n detail: { id: this.wgpuViewId }\n });\n queueMicrotask(() => {\n if (this._eventListeners[event]?.includes(listener)) {\n listener(readyEvent);\n }\n });\n }\n }\n off(event, listener) {\n if (!this._eventListeners[event])\n return;\n const idx = this._eventListeners[event].indexOf(listener);\n if (idx !== -1)\n this._eventListeners[event].splice(idx, 1);\n }\n emit(event, detail) {\n const listeners = this._eventListeners[event];\n if (listeners) {\n const customEvent = new CustomEvent(event, { detail });\n listeners.forEach((fn) => fn(customEvent));\n }\n }\n}\nfunction initWgpuTag() {\n if (!customElements.get(\"electrobun-wgpu\")) {\n customElements.define(\"electrobun-wgpu\", ElectrobunWgpuTag);\n }\n const injectStyles = () => {\n const style = document.createElement(\"style\");\n style.textContent = `\nelectrobun-wgpu {\n\tdisplay: block;\n\twidth: 800px;\n\theight: 300px;\n\tbackground: #000;\n\toverflow: hidden;\n}\n`;\n if (document.head?.firstChild) {\n document.head.insertBefore(style, document.head.firstChild);\n } else if (document.head) {\n document.head.appendChild(style);\n }\n };\n if (document.head) {\n injectStyles();\n } else {\n document.addEventListener(\"DOMContentLoaded\", injectStyles);\n }\n}\n\n// src/preload/events.ts\nfunction emitWebviewEvent(eventName, detail) {\n setTimeout(() => {\n const bridge = window.__electrobunEventBridge || window.__electrobunInternalBridge;\n bridge?.postMessage(JSON.stringify({\n id: \"webviewEvent\",\n type: \"message\",\n payload: {\n id: window.__electrobunWebviewId,\n eventName,\n detail\n }\n }));\n });\n}\nfunction initHostMessageBridge(targetWindow = window, emit = emitWebviewEvent) {\n targetWindow.__electrobunSendToHost = (message) => {\n emit(\"host-message\", JSON.stringify(message));\n };\n}\nfunction initLifecycleEvents() {\n window.addEventListener(\"load\", () => {\n if (window === window.top) {\n emitWebviewEvent(\"dom-ready\", document.location.href);\n }\n });\n window.addEventListener(\"popstate\", () => {\n emitWebviewEvent(\"did-navigate-in-page\", window.location.href);\n });\n window.addEventListener(\"hashchange\", () => {\n emitWebviewEvent(\"did-navigate-in-page\", window.location.href);\n });\n}\nvar cmdKeyHeld = false;\nvar cmdKeyTimestamp = 0;\nvar CMD_KEY_THRESHOLD_MS = 500;\nfunction isCmdHeld() {\n if (cmdKeyHeld)\n return true;\n return Date.now() - cmdKeyTimestamp < CMD_KEY_THRESHOLD_MS && cmdKeyTimestamp > 0;\n}\nfunction initCmdClickHandling() {\n window.addEventListener(\"keydown\", (event) => {\n if (event.key === \"Meta\" || event.metaKey) {\n cmdKeyHeld = true;\n cmdKeyTimestamp = Date.now();\n }\n }, true);\n window.addEventListener(\"keyup\", (event) => {\n if (event.key === \"Meta\") {\n cmdKeyHeld = false;\n cmdKeyTimestamp = Date.now();\n }\n }, true);\n window.addEventListener(\"blur\", () => {\n cmdKeyHeld = false;\n });\n window.addEventListener(\"click\", (event) => {\n if (event.metaKey || event.ctrlKey) {\n const anchor = event.target?.closest?.(\"a\");\n if (anchor && anchor.href) {\n event.preventDefault();\n event.stopPropagation();\n event.stopImmediatePropagation();\n emitWebviewEvent(\"new-window-open\", JSON.stringify({\n url: anchor.href,\n isCmdClick: true,\n isSPANavigation: false\n }));\n }\n }\n }, true);\n}\nfunction initSPANavigationInterception() {\n const originalPushState = history.pushState;\n const originalReplaceState = history.replaceState;\n history.pushState = function(state, title, url) {\n if (isCmdHeld() && url) {\n const resolvedUrl = new URL(String(url), window.location.href).href;\n emitWebviewEvent(\"new-window-open\", JSON.stringify({\n url: resolvedUrl,\n isCmdClick: true,\n isSPANavigation: true\n }));\n return;\n }\n return originalPushState.apply(this, [state, title, url]);\n };\n history.replaceState = function(state, title, url) {\n if (isCmdHeld() && url) {\n const resolvedUrl = new URL(String(url), window.location.href).href;\n emitWebviewEvent(\"new-window-open\", JSON.stringify({\n url: resolvedUrl,\n isCmdClick: true,\n isSPANavigation: true\n }));\n return;\n }\n return originalReplaceState.apply(this, [state, title, url]);\n };\n}\nfunction shouldApplyOverscrollPrevention(platform) {\n return platform !== \"linux\";\n}\nfunction initOverscrollPrevention(targetDocument = document, platform = window.__electrobunPlatform) {\n if (!shouldApplyOverscrollPrevention(platform))\n return;\n targetDocument.addEventListener(\"DOMContentLoaded\", () => {\n const style = targetDocument.createElement(\"style\");\n style.type = \"text/css\";\n style.appendChild(targetDocument.createTextNode(\"html, body { overscroll-behavior: none; }\"));\n targetDocument.head.appendChild(style);\n });\n}\n\n// src/preload/index.ts\ninitEncryption().catch((err) => console.error(\"Failed to initialize encryption:\", err));\nvar internalMessageHandler = (msg) => {\n handleResponse(msg);\n};\nvar defaultUserMessageHandler = (msg) => {\n if (!window.__electrobunPendingHostMessages) {\n window.__electrobunPendingHostMessages = [];\n }\n window.__electrobunPendingHostMessages.push(msg);\n};\nif (!window.__electrobun) {\n window.__electrobun = {\n receiveInternalMessageFromHost: internalMessageHandler,\n receiveMessageFromHost: defaultUserMessageHandler,\n receiveInternalMessageFromBun: internalMessageHandler,\n receiveMessageFromBun: defaultUserMessageHandler\n };\n} else {\n window.__electrobun.receiveInternalMessageFromHost = internalMessageHandler;\n window.__electrobun.receiveMessageFromHost = defaultUserMessageHandler;\n window.__electrobun.receiveInternalMessageFromBun = internalMessageHandler;\n window.__electrobun.receiveMessageFromBun = defaultUserMessageHandler;\n}\ninitHostMessageBridge();\ninitLifecycleEvents();\ninitCmdClickHandling();\ninitSPANavigationInterception();\ninitOverscrollPrevention();\ninitDragRegions();\ninitExternalDropFocusRestoration();\ninitWebviewTag();\ninitWgpuTag();\n})();";
5
+ export const preloadScript = "(function(){// src/preload/encryption.ts\nfunction base64ToUint8Array(base64) {\n return new Uint8Array(atob(base64).split(\"\").map((char) => char.charCodeAt(0)));\n}\nfunction uint8ArrayToBase64(uint8Array) {\n let binary = \"\";\n for (let i = 0;i < uint8Array.length; i++) {\n binary += String.fromCharCode(uint8Array[i]);\n }\n return btoa(binary);\n}\nfunction toArrayBuffer(bytes) {\n const buffer = new ArrayBuffer(bytes.byteLength);\n new Uint8Array(buffer).set(bytes);\n return buffer;\n}\nasync function generateKeyFromBytes(rawKey) {\n return await window.crypto.subtle.importKey(\"raw\", toArrayBuffer(rawKey), { name: \"AES-GCM\" }, true, [\"encrypt\", \"decrypt\"]);\n}\nasync function initEncryption() {\n const secretKey = await generateKeyFromBytes(new Uint8Array(window.__electrobunSecretKeyBytes));\n const encryptString = async (plaintext) => {\n const encoder = new TextEncoder;\n const encodedText = encoder.encode(plaintext);\n const iv = window.crypto.getRandomValues(new Uint8Array(12));\n const encryptedBuffer = await window.crypto.subtle.encrypt({ name: \"AES-GCM\", iv: toArrayBuffer(iv) }, secretKey, toArrayBuffer(encodedText));\n const encryptedData = new Uint8Array(encryptedBuffer.slice(0, -16));\n const tag = new Uint8Array(encryptedBuffer.slice(-16));\n return {\n encryptedData: uint8ArrayToBase64(encryptedData),\n iv: uint8ArrayToBase64(iv),\n tag: uint8ArrayToBase64(tag)\n };\n };\n const decryptString = async (encryptedDataB64, ivB64, tagB64) => {\n const encryptedData = base64ToUint8Array(encryptedDataB64);\n const iv = base64ToUint8Array(ivB64);\n const tag = base64ToUint8Array(tagB64);\n const combinedData = new Uint8Array(encryptedData.length + tag.length);\n combinedData.set(encryptedData);\n combinedData.set(tag, encryptedData.length);\n const decryptedBuffer = await window.crypto.subtle.decrypt({ name: \"AES-GCM\", iv: toArrayBuffer(iv) }, secretKey, toArrayBuffer(combinedData));\n const decoder = new TextDecoder;\n return decoder.decode(decryptedBuffer);\n };\n window.__electrobun_encrypt = encryptString;\n window.__electrobun_decrypt = decryptString;\n}\n\n// src/preload/internalRpc.ts\nvar pendingRequests = {};\nvar requestId = 0;\nvar isProcessingQueue = false;\nvar sendQueue = [];\nfunction processQueue() {\n if (isProcessingQueue) {\n setTimeout(processQueue);\n return;\n }\n if (sendQueue.length === 0)\n return;\n isProcessingQueue = true;\n const batch = JSON.stringify(sendQueue);\n sendQueue.length = 0;\n window.__electrobunInternalBridge?.postMessage(batch);\n setTimeout(() => {\n isProcessingQueue = false;\n }, 2);\n}\nfunction send(type, payload) {\n sendQueue.push(JSON.stringify({ type: \"message\", id: type, payload }));\n processQueue();\n}\nfunction request(type, payload) {\n return new Promise((resolve, reject) => {\n const id = `req_${++requestId}_${Date.now()}`;\n pendingRequests[id] = { resolve, reject };\n sendQueue.push(JSON.stringify({\n type: \"request\",\n method: type,\n id,\n params: payload,\n hostWebviewId: window.__electrobunWebviewId\n }));\n processQueue();\n setTimeout(() => {\n if (pendingRequests[id]) {\n delete pendingRequests[id];\n reject(new Error(`Request timeout: ${type}`));\n }\n }, 1e4);\n });\n}\nfunction handleResponse(msg) {\n if (msg && msg.type === \"response\" && msg.id) {\n const pending = pendingRequests[msg.id];\n if (pending) {\n delete pendingRequests[msg.id];\n if (msg.success)\n pending.resolve(msg.payload);\n else\n pending.reject(msg.payload);\n }\n }\n}\n\n// src/preload/dragRegions.ts\nvar DRAG_CLASS = \"electrobun-webkit-app-region-drag\";\nvar NO_DRAG_CLASS = \"electrobun-webkit-app-region-no-drag\";\nvar MIRRORED_PROPERTY = \"--electrobun-app-region\";\nvar MIRROR_ATTRIBUTE = \"data-electrobun-app-region-mirror\";\nvar APP_REGION_PROPERTIES = [\n MIRRORED_PROPERTY,\n \"-webkit-app-region\",\n \"app-region\",\n \"window-drag\"\n];\nvar DRAG_REGION_COMPATIBILITY_CSS = `\n.electrobun-webkit-app-region-drag {\n\t-webkit-app-region: drag;\n\tapp-region: drag;\n}\n.electrobun-webkit-app-region-no-drag {\n\t-webkit-app-region: no-drag;\n\tapp-region: no-drag;\n}\n`;\nvar processedSourceSignatures = new WeakMap;\nvar stylesheetMirrors = new Map;\nvar pendingLinkSignatures = new WeakMap;\nvar stylesheetMirroringInitialized = false;\nfunction normalizedRegion(value) {\n const normalized = value?.trim().toLowerCase();\n if (normalized === \"drag\" || normalized === \"no-drag\")\n return normalized;\n return null;\n}\nfunction previousSignificantCharacter(source, offset) {\n let index = offset - 1;\n while (index >= 0) {\n while (index >= 0 && /\\s/.test(source[index] ?? \"\"))\n index--;\n if (index >= 1 && source[index] === \"/\" && source[index - 1] === \"*\") {\n const commentStart = source.lastIndexOf(\"/*\", index - 1);\n if (commentStart < 0)\n return \"\";\n index = commentStart - 1;\n continue;\n }\n return source[index] ?? \"\";\n }\n return \"\";\n}\nfunction isIdentifierCharacter(character) {\n return !!character && /[a-zA-Z0-9_-]/.test(character);\n}\nfunction rewriteAppRegionDeclarations(source) {\n const propertyNames = [\"-webkit-app-region\", \"window-drag\", \"app-region\"];\n let output = \"\";\n let cursor = 0;\n let index = 0;\n while (index < source.length) {\n if (source[index] === \"/\" && source[index + 1] === \"*\") {\n const commentEnd = source.indexOf(\"*/\", index + 2);\n index = commentEnd < 0 ? source.length : commentEnd + 2;\n continue;\n }\n if (source[index] === '\"' || source[index] === \"'\") {\n const quote = source[index];\n index++;\n while (index < source.length) {\n if (source[index] === \"\\\\\") {\n index += 2;\n continue;\n }\n if (source[index] === quote) {\n index++;\n break;\n }\n index++;\n }\n continue;\n }\n let matchedProperty;\n for (const propertyName of propertyNames) {\n if (source.slice(index, index + propertyName.length).toLowerCase() === propertyName && !isIdentifierCharacter(source[index - 1]) && !isIdentifierCharacter(source[index + propertyName.length])) {\n matchedProperty = propertyName;\n break;\n }\n }\n if (matchedProperty) {\n let colonOffset = index + matchedProperty.length;\n while (/\\s/.test(source[colonOffset] ?? \"\"))\n colonOffset++;\n const previous = previousSignificantCharacter(source, index);\n if (source[colonOffset] === \":\" && (previous === \"{\" || previous === \";\")) {\n output += source.slice(cursor, index) + MIRRORED_PROPERTY;\n index += matchedProperty.length;\n cursor = index;\n continue;\n }\n }\n index++;\n }\n return output + source.slice(cursor);\n}\nfunction nestedRuleContainer(rule) {\n const candidate = rule;\n if (candidate.cssRules && typeof candidate.deleteRule === \"function\") {\n return candidate;\n }\n return null;\n}\nfunction pruneToAppRegionRules(container) {\n let hasAppRegionRule = false;\n for (let index = container.cssRules.length - 1;index >= 0; index--) {\n const rule = container.cssRules[index];\n if (!rule)\n continue;\n const style = rule.style;\n let hasAppRegionDeclaration = false;\n if (style && typeof style.getPropertyValue === \"function\") {\n for (let propertyIndex = style.length - 1;propertyIndex >= 0; propertyIndex--) {\n const propertyName = style.item(propertyIndex);\n if (propertyName === MIRRORED_PROPERTY) {\n hasAppRegionDeclaration = true;\n } else {\n style.removeProperty(propertyName);\n }\n }\n }\n const nested = nestedRuleContainer(rule);\n const hasNestedAppRegionRule = nested ? pruneToAppRegionRules(nested) : false;\n if (!hasAppRegionDeclaration && !hasNestedAppRegionRule) {\n container.deleteRule(index);\n continue;\n }\n hasAppRegionRule = true;\n }\n return hasAppRegionRule;\n}\nfunction serializeRules(rules) {\n let cssText = \"\";\n for (let index = 0;index < rules.length; index++) {\n const rule = rules[index];\n if (rule)\n cssText += `${rule.cssText}\n`;\n }\n return cssText;\n}\nfunction removeStylesheetMirror(source) {\n stylesheetMirrors.get(source)?.remove();\n stylesheetMirrors.delete(source);\n}\nfunction installStylesheetMirror(source, cssText, signature) {\n if (processedSourceSignatures.get(source) === signature)\n return;\n processedSourceSignatures.set(source, signature);\n removeStylesheetMirror(source);\n const mirroredCss = rewriteAppRegionDeclarations(cssText);\n if (mirroredCss === cssText || !source.parentNode)\n return;\n const mirror = source.ownerDocument.createElement(\"style\");\n mirror.setAttribute(MIRROR_ATTRIBUTE, \"\");\n mirror.media = \"not all\";\n if (source.nonce)\n mirror.nonce = source.nonce;\n mirror.textContent = mirroredCss;\n source.parentNode.insertBefore(mirror, source.nextSibling);\n let sheet = mirror.sheet;\n if (!sheet || !pruneToAppRegionRules(sheet)) {\n mirror.remove();\n return;\n }\n mirror.textContent = serializeRules(sheet.cssRules);\n sheet = mirror.sheet;\n if (!sheet) {\n mirror.remove();\n return;\n }\n mirror.media = source.media;\n if (source.sheet?.disabled)\n sheet.disabled = true;\n stylesheetMirrors.set(source, mirror);\n}\nfunction isMirroredStyle(element) {\n return element.tagName === \"STYLE\" && element.hasAttribute(MIRROR_ATTRIBUTE);\n}\nfunction isStylesheetSource(element) {\n if (element.tagName === \"STYLE\")\n return !isMirroredStyle(element);\n return element.tagName === \"LINK\" && element.relList.contains(\"stylesheet\");\n}\nfunction nodeContainsStylesheetSource(node) {\n const element = node;\n if (typeof element.matches !== \"function\")\n return false;\n return isStylesheetSource(element) || !!element.querySelector(\"style:not([data-electrobun-app-region-mirror]), link[rel~='stylesheet']\");\n}\nfunction mutationsAffectStylesheets(mutations) {\n for (const mutation of mutations) {\n if (mutation.type === \"attributes\") {\n const element = mutation.target;\n if (element.tagName === \"LINK\" || isStylesheetSource(element))\n return true;\n continue;\n }\n if (mutation.type === \"characterData\") {\n const parent = mutation.target.parentElement;\n if (parent && isStylesheetSource(parent))\n return true;\n continue;\n }\n const target = mutation.target;\n if (typeof target.matches === \"function\" && isStylesheetSource(target)) {\n return true;\n }\n for (let index = 0;index < mutation.addedNodes.length; index++) {\n const node = mutation.addedNodes[index];\n if (node && nodeContainsStylesheetSource(node))\n return true;\n }\n for (let index = 0;index < mutation.removedNodes.length; index++) {\n const node = mutation.removedNodes[index];\n if (node && nodeContainsStylesheetSource(node))\n return true;\n }\n }\n return false;\n}\nfunction processStyleElement(style) {\n if (isMirroredStyle(style))\n return;\n const cssText = style.textContent ?? \"\";\n installStylesheetMirror(style, cssText, `${style.media}\n${cssText}`);\n}\nfunction processLinkElement(link) {\n if (!link.relList.contains(\"stylesheet\") || !link.href) {\n removeStylesheetMirror(link);\n return;\n }\n const signature = `${link.media}\n${link.href}`;\n if (processedSourceSignatures.get(link) === signature || pendingLinkSignatures.get(link) === signature) {\n return;\n }\n pendingLinkSignatures.set(link, signature);\n fetch(link.href, { credentials: \"same-origin\" }).then((response) => {\n if (!response.ok)\n throw new Error(`HTTP ${response.status}`);\n return response.text();\n }).then((cssText) => {\n if (link.isConnected && `${link.media}\n${link.href}` === signature) {\n installStylesheetMirror(link, cssText, signature);\n }\n }).catch(() => {}).finally(() => {\n if (pendingLinkSignatures.get(link) === signature) {\n pendingLinkSignatures.delete(link);\n }\n });\n}\nfunction scanStylesheetSources() {\n document.querySelectorAll(\"style, link[rel~='stylesheet']\").forEach((element) => {\n if (element.tagName === \"STYLE\") {\n processStyleElement(element);\n } else {\n processLinkElement(element);\n }\n });\n for (const [source, mirror] of stylesheetMirrors) {\n if (!source.isConnected) {\n mirror.remove();\n stylesheetMirrors.delete(source);\n }\n }\n}\nfunction initStylesheetMirroring() {\n if (stylesheetMirroringInitialized)\n return;\n stylesheetMirroringInitialized = true;\n let scanScheduled = false;\n const scheduleScan = () => {\n if (scanScheduled)\n return;\n scanScheduled = true;\n queueMicrotask(() => {\n scanScheduled = false;\n scanStylesheetSources();\n });\n };\n new MutationObserver((mutations) => {\n if (mutationsAffectStylesheets(mutations))\n scheduleScan();\n }).observe(document, {\n attributes: true,\n attributeFilter: [\"href\", \"media\", \"rel\"],\n characterData: true,\n childList: true,\n subtree: true\n });\n document.addEventListener(\"DOMContentLoaded\", scheduleScan, { once: true });\n scheduleScan();\n}\nfunction installCompatibilityStyles() {\n if (document.querySelector(\"style[data-electrobun-drag-region-compat]\"))\n return;\n const style = document.createElement(\"style\");\n style.setAttribute(\"data-electrobun-drag-region-compat\", \"\");\n style.textContent = DRAG_REGION_COMPATIBILITY_CSS;\n const install = () => {\n if (!document.head || style.isConnected)\n return;\n document.head.insertBefore(style, document.head.firstChild);\n };\n if (document.head)\n install();\n else\n document.addEventListener(\"DOMContentLoaded\", install, { once: true });\n}\nfunction inlineRegion(element) {\n const style = element.getAttribute?.(\"style\");\n if (!style)\n return null;\n const match = style.match(/(?:^|;)\\s*(?:-webkit-app-region|app-region|window-drag)\\s*:\\s*(no-drag|drag)\\b/i);\n return normalizedRegion(match?.[1]);\n}\nfunction computedRegion(element, readComputedStyle) {\n try {\n const style = readComputedStyle(element);\n for (const propertyName of APP_REGION_PROPERTIES) {\n const region = normalizedRegion(style.getPropertyValue(propertyName));\n if (region)\n return region;\n }\n } catch {}\n return null;\n}\nfunction eventTargetElement(target) {\n if (!target || typeof target.getAttribute !== \"function\") {\n return target?.parentElement ?? null;\n }\n return target;\n}\nfunction isAppRegionDragTarget(target, readComputedStyle = (element) => window.getComputedStyle(element)) {\n let element = eventTargetElement(target);\n let foundDragRegion = false;\n while (element) {\n if (element.classList?.contains(NO_DRAG_CLASS))\n return false;\n if (element.classList?.contains(DRAG_CLASS))\n foundDragRegion = true;\n const region = inlineRegion(element) ?? computedRegion(element, readComputedStyle);\n if (region === \"no-drag\")\n return false;\n if (region === \"drag\")\n foundDragRegion = true;\n element = element.parentElement;\n }\n return foundDragRegion;\n}\nfunction registerDragRegionListeners(targetDocument, getWindowId, sendMessage = send, readComputedStyle = (element) => window.getComputedStyle(element)) {\n targetDocument.addEventListener(\"mousedown\", (event) => {\n if (isAppRegionDragTarget(event.target, readComputedStyle)) {\n sendMessage(\"startWindowMove\", { id: getWindowId() });\n }\n });\n targetDocument.addEventListener(\"mouseup\", (event) => {\n if (isAppRegionDragTarget(event.target, readComputedStyle)) {\n sendMessage(\"stopWindowMove\", { id: getWindowId() });\n }\n });\n}\nfunction initDragRegions() {\n installCompatibilityStyles();\n initStylesheetMirroring();\n registerDragRegionListeners(document, () => window.__electrobunWindowId);\n}\n\n// src/preload/externalDropFocus.ts\nfunction initExternalDropFocusRestoration(targetWindow = window, platform = window.__electrobunPlatform, requestFocus = request, schedule = (callback) => setTimeout(callback)) {\n if (platform !== \"windows\")\n return;\n targetWindow.addEventListener(\"drop\", (event) => {\n const types = Array.from(event.dataTransfer?.types ?? []);\n if (!types.includes(\"Files\"))\n return;\n const previouslyFocusedElement = targetWindow.document.activeElement;\n schedule(() => {\n requestFocus(\"restoreWindowFocusAfterExternalDrop\", {\n windowId: targetWindow.__electrobunWindowId\n }).catch(() => {\n return;\n }).then(() => {\n targetWindow.focus();\n if (previouslyFocusedElement && \"focus\" in previouslyFocusedElement && typeof previouslyFocusedElement.focus === \"function\") {\n previouslyFocusedElement.focus();\n }\n });\n });\n }, true);\n}\n\n// src/preload/overlaySync.ts\nclass OverlaySyncController {\n element;\n options;\n lastRect = { x: 0, y: 0, width: 0, height: 0 };\n resizeObserver = null;\n positionLoop = null;\n resizeHandler = null;\n burstUntil = 0;\n constructor(element, options) {\n this.element = element;\n this.options = {\n onSync: options.onSync,\n getMasks: options.getMasks ?? (() => []),\n burstIntervalMs: options.burstIntervalMs ?? 50,\n baseIntervalMs: options.baseIntervalMs ?? 100,\n burstDurationMs: options.burstDurationMs ?? 500\n };\n }\n start() {\n this.resizeObserver = new ResizeObserver(() => this.sync());\n this.resizeObserver.observe(this.element);\n const loop = () => {\n this.sync();\n const now = performance.now();\n const interval = now < this.burstUntil ? this.options.burstIntervalMs : this.options.baseIntervalMs;\n this.positionLoop = setTimeout(loop, interval);\n };\n this.positionLoop = setTimeout(loop, this.options.baseIntervalMs);\n this.resizeHandler = () => this.sync(true);\n window.addEventListener(\"resize\", this.resizeHandler);\n }\n stop() {\n if (this.resizeObserver)\n this.resizeObserver.disconnect();\n if (this.positionLoop)\n clearTimeout(this.positionLoop);\n if (this.resizeHandler) {\n window.removeEventListener(\"resize\", this.resizeHandler);\n }\n this.resizeObserver = null;\n this.positionLoop = null;\n this.resizeHandler = null;\n }\n forceSync() {\n this.sync(true);\n }\n setLastRect(rect) {\n this.lastRect = rect;\n }\n sync(force = false) {\n const rect = this.element.getBoundingClientRect();\n const newRect = {\n x: rect.x,\n y: rect.y,\n width: rect.width,\n height: rect.height\n };\n if (newRect.width === 0 && newRect.height === 0) {\n return;\n }\n if (!force && newRect.x === this.lastRect.x && newRect.y === this.lastRect.y && newRect.width === this.lastRect.width && newRect.height === this.lastRect.height) {\n return;\n }\n this.burstUntil = performance.now() + this.options.burstDurationMs;\n this.lastRect = newRect;\n const masks = this.options.getMasks();\n this.options.onSync(newRect, JSON.stringify(masks));\n }\n}\n\n// src/preload/webviewTagNavigation.ts\nclass WebviewTagNavigationQueue {\n pending = null;\n beginInitialization() {\n this.pending = null;\n }\n defer(navigation) {\n this.pending = navigation;\n }\n take() {\n const navigation = this.pending;\n this.pending = null;\n return navigation;\n }\n}\n\n// src/preload/webviewTag.ts\nvar webviewRegistry = {};\n\nclass ElectrobunWebviewTag extends HTMLElement {\n webviewId = null;\n maskSelectors = new Set;\n _sync = null;\n _navigationQueue = new WebviewTagNavigationQueue;\n _initializationStarted = false;\n transparent = false;\n passthroughEnabled = false;\n spellCheckEnabled = false;\n hidden = false;\n sandboxed = false;\n _eventListeners = {};\n static get observedAttributes() {\n return [\"src\", \"html\", \"spellcheck\"];\n }\n constructor() {\n super();\n }\n connectedCallback() {\n requestAnimationFrame(() => this.initWebview());\n }\n attributeChangedCallback(name, oldValue, newValue) {\n if (oldValue === newValue)\n return;\n if (name === \"src\" && newValue !== null) {\n this.navigate({ kind: \"url\", value: newValue });\n } else if (name === \"html\" && newValue !== null) {\n this.navigate({ kind: \"html\", value: newValue });\n } else if (name === \"spellcheck\" && this.webviewId !== null) {\n this.setSpellCheck(newValue !== null && newValue.toLowerCase() !== \"false\");\n }\n }\n disconnectedCallback() {\n if (this.webviewId !== null) {\n send(\"webviewTagRemove\", { id: this.webviewId });\n delete webviewRegistry[this.webviewId];\n }\n if (this._sync)\n this._sync.stop();\n }\n getInitialNavigationRules() {\n const rawRules = this.getAttribute(\"navigation-rules\");\n if (rawRules === null) {\n return null;\n }\n const trimmed = rawRules.trim();\n if (!trimmed) {\n return [];\n }\n try {\n const parsed = JSON.parse(trimmed);\n if (!Array.isArray(parsed) || !parsed.every((rule) => typeof rule === \"string\")) {\n throw new Error(\"navigation-rules must be a JSON string array\");\n }\n return parsed;\n } catch (error) {\n console.error(\"Invalid navigation-rules attribute:\", error);\n return [];\n }\n }\n async initWebview() {\n this._initializationStarted = true;\n this._navigationQueue.beginInitialization();\n const rect = this.getBoundingClientRect();\n const initialRect = {\n x: rect.x,\n y: rect.y,\n width: rect.width,\n height: rect.height\n };\n const url = this.getAttribute(\"src\");\n const html = this.getAttribute(\"html\");\n const preload = this.getAttribute(\"preload\");\n const partition = this.getAttribute(\"partition\");\n const renderer = this.getAttribute(\"renderer\") || \"native\";\n const masks = this.getAttribute(\"masks\");\n const navigationRules = this.getInitialNavigationRules();\n const sandbox = this.hasAttribute(\"sandbox\");\n this.sandboxed = sandbox;\n const transparent = this.hasAttribute(\"transparent\");\n const passthrough = this.hasAttribute(\"passthrough\");\n const spellCheckAttribute = this.getAttribute(\"spellcheck\");\n const spellCheck = spellCheckAttribute !== null && spellCheckAttribute.toLowerCase() !== \"false\";\n this.transparent = transparent;\n this.passthroughEnabled = passthrough;\n this.spellCheckEnabled = spellCheck;\n if (transparent)\n this.style.opacity = \"0\";\n if (passthrough)\n this.style.pointerEvents = \"none\";\n if (masks) {\n masks.split(\",\").forEach((s) => this.maskSelectors.add(s.trim()));\n }\n try {\n const webviewInitParams = {\n hostWebviewId: window.__electrobunWebviewId,\n windowId: window.__electrobunWindowId,\n renderer,\n url,\n html,\n preload,\n partition,\n frame: initialRect,\n sandbox,\n transparent,\n passthrough,\n spellCheck,\n ...navigationRules === null ? {} : { navigationRules }\n };\n const webviewId = await request(\"webviewTagInit\", webviewInitParams);\n this.webviewId = webviewId;\n this.id = `electrobun-webview-${webviewId}`;\n webviewRegistry[webviewId] = this;\n const pendingNavigation = this._navigationQueue.take();\n if (pendingNavigation)\n this.navigate(pendingNavigation);\n this.setupObservers(initialRect);\n this.syncDimensions(true);\n requestAnimationFrame(() => {\n Object.values(webviewRegistry).forEach((webview) => {\n if (webview !== this && webview.webviewId !== null) {\n webview.syncDimensions(true);\n }\n });\n });\n } catch (err) {\n console.error(\"Failed to init webview:\", err);\n }\n }\n setupObservers(initialRect) {\n const getMasks = () => {\n const rect = this.getBoundingClientRect();\n const masks = [];\n this.maskSelectors.forEach((selector) => {\n try {\n document.querySelectorAll(selector).forEach((el) => {\n const mr = el.getBoundingClientRect();\n masks.push({\n x: mr.x - rect.x,\n y: mr.y - rect.y,\n width: mr.width,\n height: mr.height\n });\n });\n } catch (_e) {}\n });\n return masks;\n };\n this._sync = new OverlaySyncController(this, {\n onSync: (rect, masksJson) => {\n if (this.webviewId === null)\n return;\n send(\"webviewTagResize\", {\n id: this.webviewId,\n frame: rect,\n masks: masksJson\n });\n },\n getMasks,\n burstIntervalMs: 10,\n baseIntervalMs: 100,\n burstDurationMs: 50\n });\n this._sync.setLastRect(initialRect);\n this._sync.start();\n }\n syncDimensions(force = false) {\n if (!this._sync)\n return;\n if (force) {\n this._sync.forceSync();\n }\n }\n navigate(navigation) {\n if (this.webviewId === null) {\n this._navigationQueue.defer(navigation);\n return;\n }\n if (navigation.kind === \"url\") {\n send(\"webviewTagUpdateSrc\", {\n id: this.webviewId,\n url: navigation.value\n });\n } else {\n send(\"webviewTagUpdateHtml\", {\n id: this.webviewId,\n html: navigation.value\n });\n }\n }\n loadURL(url) {\n if (this.getAttribute(\"src\") === url) {\n this.navigate({ kind: \"url\", value: url });\n } else {\n this.setAttribute(\"src\", url);\n }\n }\n loadHTML(html) {\n if (this.webviewId === null && !this._initializationStarted) {\n this.setAttribute(\"html\", html);\n return;\n }\n this.navigate({ kind: \"html\", value: html });\n }\n reload() {\n if (this.webviewId !== null)\n send(\"webviewTagReload\", { id: this.webviewId });\n }\n goBack() {\n if (this.webviewId !== null)\n send(\"webviewTagGoBack\", { id: this.webviewId });\n }\n goForward() {\n if (this.webviewId !== null)\n send(\"webviewTagGoForward\", { id: this.webviewId });\n }\n async canGoBack() {\n if (this.webviewId === null)\n return false;\n return await request(\"webviewTagCanGoBack\", {\n id: this.webviewId\n });\n }\n async canGoForward() {\n if (this.webviewId === null)\n return false;\n return await request(\"webviewTagCanGoForward\", {\n id: this.webviewId\n });\n }\n async setSpellCheck(enabled) {\n this.spellCheckEnabled = enabled;\n if (this.webviewId === null)\n return false;\n return await request(\"webviewTagSetSpellCheck\", {\n id: this.webviewId,\n enabled\n });\n }\n toggleTransparent(value) {\n if (this.webviewId === null)\n return;\n this.transparent = value !== undefined ? value : !this.transparent;\n this.style.opacity = this.transparent ? \"0\" : \"\";\n send(\"webviewTagSetTransparent\", {\n id: this.webviewId,\n transparent: this.transparent\n });\n }\n togglePassthrough(value) {\n if (this.webviewId === null)\n return;\n this.passthroughEnabled = value !== undefined ? value : !this.passthroughEnabled;\n this.style.pointerEvents = this.passthroughEnabled ? \"none\" : \"\";\n send(\"webviewTagSetPassthrough\", {\n id: this.webviewId,\n enablePassthrough: this.passthroughEnabled\n });\n }\n toggleHidden(value) {\n if (this.webviewId === null)\n return;\n this.hidden = value !== undefined ? value : !this.hidden;\n send(\"webviewTagSetHidden\", { id: this.webviewId, hidden: this.hidden });\n }\n addMaskSelector(selector) {\n this.maskSelectors.add(selector);\n this.syncDimensions(true);\n }\n removeMaskSelector(selector) {\n this.maskSelectors.delete(selector);\n this.syncDimensions(true);\n }\n setNavigationRules(rules) {\n if (this.webviewId !== null) {\n send(\"webviewTagSetNavigationRules\", { id: this.webviewId, rules });\n }\n }\n findInPage(searchText, options) {\n if (this.webviewId === null)\n return;\n const forward = options?.forward !== false;\n const matchCase = options?.matchCase || false;\n send(\"webviewTagFindInPage\", {\n id: this.webviewId,\n searchText,\n forward,\n matchCase\n });\n }\n stopFindInPage() {\n if (this.webviewId !== null)\n send(\"webviewTagStopFind\", { id: this.webviewId });\n }\n openDevTools() {\n if (this.webviewId !== null)\n send(\"webviewTagOpenDevTools\", { id: this.webviewId });\n }\n closeDevTools() {\n if (this.webviewId !== null)\n send(\"webviewTagCloseDevTools\", { id: this.webviewId });\n }\n toggleDevTools() {\n if (this.webviewId !== null)\n send(\"webviewTagToggleDevTools\", { id: this.webviewId });\n }\n executeJavascript(js) {\n if (this.webviewId === null)\n return;\n send(\"webviewTagExecuteJavascript\", { id: this.webviewId, js });\n }\n on(event, listener) {\n if (!this._eventListeners[event])\n this._eventListeners[event] = [];\n this._eventListeners[event].push(listener);\n }\n off(event, listener) {\n if (!this._eventListeners[event])\n return;\n const idx = this._eventListeners[event].indexOf(listener);\n if (idx !== -1)\n this._eventListeners[event].splice(idx, 1);\n }\n emit(event, detail) {\n const listeners = this._eventListeners[event];\n if (listeners) {\n const customEvent = new CustomEvent(event, { detail });\n listeners.forEach((fn) => fn(customEvent));\n }\n }\n get src() {\n return this.getAttribute(\"src\");\n }\n set src(value) {\n if (value) {\n this.setAttribute(\"src\", value);\n } else {\n this.removeAttribute(\"src\");\n }\n }\n get html() {\n return this.getAttribute(\"html\");\n }\n set html(value) {\n if (value) {\n this.setAttribute(\"html\", value);\n } else {\n this.removeAttribute(\"html\");\n }\n }\n get preload() {\n return this.getAttribute(\"preload\");\n }\n set preload(value) {\n if (value)\n this.setAttribute(\"preload\", value);\n else\n this.removeAttribute(\"preload\");\n }\n get renderer() {\n return this.getAttribute(\"renderer\") || \"native\";\n }\n set renderer(value) {\n this.setAttribute(\"renderer\", value);\n }\n get sandbox() {\n return this.sandboxed;\n }\n}\nfunction initWebviewTag() {\n if (!customElements.get(\"electrobun-webview\")) {\n customElements.define(\"electrobun-webview\", ElectrobunWebviewTag);\n }\n const injectStyles = () => {\n const style = document.createElement(\"style\");\n style.textContent = `\nelectrobun-webview {\n\tdisplay: block;\n\twidth: 800px;\n\theight: 300px;\n\tbackground: #fff;\n\tbackground-repeat: no-repeat !important;\n\toverflow: hidden;\n}\n`;\n if (document.head?.firstChild) {\n document.head.insertBefore(style, document.head.firstChild);\n } else if (document.head) {\n document.head.appendChild(style);\n }\n };\n if (document.head) {\n injectStyles();\n } else {\n document.addEventListener(\"DOMContentLoaded\", injectStyles);\n }\n}\n\n// src/preload/wgpuTag.ts\nvar wgpuTagRegistry = {};\n\nclass ElectrobunWgpuTag extends HTMLElement {\n wgpuViewId = null;\n maskSelectors = new Set;\n _sync = null;\n transparent = false;\n passthroughEnabled = false;\n hidden = false;\n _ready = false;\n _initializing = false;\n _eventListeners = {};\n constructor() {\n super();\n }\n connectedCallback() {\n requestAnimationFrame(() => {\n if (this.isConnected)\n this.initWgpuView();\n });\n }\n disconnectedCallback() {\n if (this.wgpuViewId !== null) {\n send(\"wgpuTagRemove\", { id: this.wgpuViewId });\n delete wgpuTagRegistry[this.wgpuViewId];\n this.wgpuViewId = null;\n }\n if (this._sync)\n this._sync.stop();\n this._sync = null;\n this._ready = false;\n }\n async initWgpuView() {\n if (this._initializing || this.wgpuViewId !== null)\n return;\n this._initializing = true;\n const rect = this.getBoundingClientRect();\n const initialRect = {\n x: rect.x,\n y: rect.y,\n width: rect.width,\n height: rect.height\n };\n const transparent = this.hasAttribute(\"transparent\");\n const passthrough = this.hasAttribute(\"passthrough\");\n const hidden = this.hasAttribute(\"hidden\");\n const masks = this.getAttribute(\"masks\");\n this.transparent = transparent;\n this.passthroughEnabled = passthrough;\n this.hidden = hidden;\n if (masks) {\n masks.split(\",\").forEach((s) => this.maskSelectors.add(s.trim()));\n }\n if (transparent)\n this.style.opacity = \"0\";\n if (passthrough)\n this.style.pointerEvents = \"none\";\n try {\n const wgpuViewId = await request(\"wgpuTagInit\", {\n windowId: window.__electrobunWindowId,\n frame: initialRect,\n transparent,\n passthrough\n });\n if (!this.isConnected) {\n send(\"wgpuTagRemove\", { id: wgpuViewId });\n return;\n }\n this.wgpuViewId = wgpuViewId;\n if (!this.id) {\n this.id = `electrobun-wgpu-${wgpuViewId}`;\n }\n wgpuTagRegistry[wgpuViewId] = this;\n this.setupObservers(initialRect);\n this.syncDimensions(true);\n if (hidden) {\n this.toggleHidden(true);\n }\n requestAnimationFrame(() => {\n Object.values(wgpuTagRegistry).forEach((view) => {\n if (view !== this && view.wgpuViewId !== null) {\n view.syncDimensions(true);\n }\n });\n });\n this._ready = true;\n this.emit(\"ready\", { id: wgpuViewId });\n } catch (err) {\n console.error(\"Failed to init WGPU view:\", err);\n } finally {\n this._initializing = false;\n }\n }\n setupObservers(initialRect) {\n const getMasks = () => {\n const rect = this.getBoundingClientRect();\n const masks = [];\n this.maskSelectors.forEach((selector) => {\n try {\n document.querySelectorAll(selector).forEach((el) => {\n const mr = el.getBoundingClientRect();\n masks.push({\n x: mr.x - rect.x,\n y: mr.y - rect.y,\n width: mr.width,\n height: mr.height\n });\n });\n } catch (_e) {}\n });\n return masks;\n };\n this._sync = new OverlaySyncController(this, {\n onSync: (rect, masksJson) => {\n if (this.wgpuViewId === null)\n return;\n send(\"wgpuTagResize\", {\n id: this.wgpuViewId,\n frame: rect,\n masks: masksJson\n });\n },\n getMasks,\n burstIntervalMs: 10,\n baseIntervalMs: 100,\n burstDurationMs: 50\n });\n this._sync.setLastRect(initialRect);\n this._sync.start();\n }\n syncDimensions(force = false) {\n if (!this._sync)\n return;\n if (force) {\n this._sync.forceSync();\n }\n }\n toggleTransparent(value) {\n if (this.wgpuViewId === null)\n return;\n this.transparent = value !== undefined ? value : !this.transparent;\n this.style.opacity = this.transparent ? \"0\" : \"\";\n send(\"wgpuTagSetTransparent\", {\n id: this.wgpuViewId,\n transparent: this.transparent\n });\n }\n togglePassthrough(value) {\n if (this.wgpuViewId === null)\n return;\n this.passthroughEnabled = value !== undefined ? value : !this.passthroughEnabled;\n this.style.pointerEvents = this.passthroughEnabled ? \"none\" : \"\";\n send(\"wgpuTagSetPassthrough\", {\n id: this.wgpuViewId,\n passthrough: this.passthroughEnabled\n });\n }\n toggleHidden(value) {\n if (this.wgpuViewId === null)\n return;\n this.hidden = value !== undefined ? value : !this.hidden;\n send(\"wgpuTagSetHidden\", { id: this.wgpuViewId, hidden: this.hidden });\n }\n runTest() {\n if (this.wgpuViewId === null)\n return;\n send(\"wgpuTagRunTest\", { id: this.wgpuViewId });\n }\n addMaskSelector(selector) {\n this.maskSelectors.add(selector);\n this.syncDimensions(true);\n }\n removeMaskSelector(selector) {\n this.maskSelectors.delete(selector);\n this.syncDimensions(true);\n }\n on(event, listener) {\n if (!this._eventListeners[event])\n this._eventListeners[event] = [];\n this._eventListeners[event].push(listener);\n if (event === \"ready\" && this._ready && this.wgpuViewId !== null) {\n const readyEvent = new CustomEvent(event, {\n detail: { id: this.wgpuViewId }\n });\n queueMicrotask(() => {\n if (this._eventListeners[event]?.includes(listener)) {\n listener(readyEvent);\n }\n });\n }\n }\n off(event, listener) {\n if (!this._eventListeners[event])\n return;\n const idx = this._eventListeners[event].indexOf(listener);\n if (idx !== -1)\n this._eventListeners[event].splice(idx, 1);\n }\n emit(event, detail) {\n const listeners = this._eventListeners[event];\n if (listeners) {\n const customEvent = new CustomEvent(event, { detail });\n listeners.forEach((fn) => fn(customEvent));\n }\n }\n}\nfunction initWgpuTag() {\n if (!customElements.get(\"electrobun-wgpu\")) {\n customElements.define(\"electrobun-wgpu\", ElectrobunWgpuTag);\n }\n const injectStyles = () => {\n const style = document.createElement(\"style\");\n style.textContent = `\nelectrobun-wgpu {\n\tdisplay: block;\n\twidth: 800px;\n\theight: 300px;\n\tbackground: #000;\n\toverflow: hidden;\n}\n`;\n if (document.head?.firstChild) {\n document.head.insertBefore(style, document.head.firstChild);\n } else if (document.head) {\n document.head.appendChild(style);\n }\n };\n if (document.head) {\n injectStyles();\n } else {\n document.addEventListener(\"DOMContentLoaded\", injectStyles);\n }\n}\n\n// src/preload/uiTag.ts\nclass ElectrobunUiTag extends ElectrobunWgpuTag {\n async initWgpuView() {\n await super.initWgpuView();\n if (this.wgpuViewId !== null) {\n send(\"uiTagMount\", {\n id: this.wgpuViewId,\n name: this.getAttribute(\"name\") ?? \"\"\n });\n }\n }\n}\nfunction initUiTag() {\n if (!customElements.get(\"electrobun-ui\")) {\n customElements.define(\"electrobun-ui\", ElectrobunUiTag);\n }\n const injectStyles = () => {\n const style = document.createElement(\"style\");\n style.textContent = `\nelectrobun-ui {\n\tdisplay: block;\n\twidth: 400px;\n\theight: 300px;\n}\n`;\n document.head.appendChild(style);\n };\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", injectStyles);\n } else {\n injectStyles();\n }\n}\n\n// src/preload/events.ts\nfunction emitWebviewEvent(eventName, detail) {\n setTimeout(() => {\n const bridge = window.__electrobunEventBridge || window.__electrobunInternalBridge;\n bridge?.postMessage(JSON.stringify({\n id: \"webviewEvent\",\n type: \"message\",\n payload: {\n id: window.__electrobunWebviewId,\n eventName,\n detail\n }\n }));\n });\n}\nfunction initHostMessageBridge(targetWindow = window, emit = emitWebviewEvent) {\n targetWindow.__electrobunSendToHost = (message) => {\n emit(\"host-message\", JSON.stringify(message));\n };\n}\nfunction initLifecycleEvents() {\n window.addEventListener(\"load\", () => {\n if (window === window.top) {\n emitWebviewEvent(\"dom-ready\", document.location.href);\n }\n });\n window.addEventListener(\"popstate\", () => {\n emitWebviewEvent(\"did-navigate-in-page\", window.location.href);\n });\n window.addEventListener(\"hashchange\", () => {\n emitWebviewEvent(\"did-navigate-in-page\", window.location.href);\n });\n}\nvar cmdKeyHeld = false;\nvar cmdKeyTimestamp = 0;\nvar CMD_KEY_THRESHOLD_MS = 500;\nfunction isCmdHeld() {\n if (cmdKeyHeld)\n return true;\n return Date.now() - cmdKeyTimestamp < CMD_KEY_THRESHOLD_MS && cmdKeyTimestamp > 0;\n}\nfunction initCmdClickHandling() {\n window.addEventListener(\"keydown\", (event) => {\n if (event.key === \"Meta\" || event.metaKey) {\n cmdKeyHeld = true;\n cmdKeyTimestamp = Date.now();\n }\n }, true);\n window.addEventListener(\"keyup\", (event) => {\n if (event.key === \"Meta\") {\n cmdKeyHeld = false;\n cmdKeyTimestamp = Date.now();\n }\n }, true);\n window.addEventListener(\"blur\", () => {\n cmdKeyHeld = false;\n });\n window.addEventListener(\"click\", (event) => {\n if (event.metaKey || event.ctrlKey) {\n const anchor = event.target?.closest?.(\"a\");\n if (anchor && anchor.href) {\n event.preventDefault();\n event.stopPropagation();\n event.stopImmediatePropagation();\n emitWebviewEvent(\"new-window-open\", JSON.stringify({\n url: anchor.href,\n isCmdClick: true,\n isSPANavigation: false\n }));\n }\n }\n }, true);\n}\nfunction initSPANavigationInterception() {\n const originalPushState = history.pushState;\n const originalReplaceState = history.replaceState;\n history.pushState = function(state, title, url) {\n if (isCmdHeld() && url) {\n const resolvedUrl = new URL(String(url), window.location.href).href;\n emitWebviewEvent(\"new-window-open\", JSON.stringify({\n url: resolvedUrl,\n isCmdClick: true,\n isSPANavigation: true\n }));\n return;\n }\n return originalPushState.apply(this, [state, title, url]);\n };\n history.replaceState = function(state, title, url) {\n if (isCmdHeld() && url) {\n const resolvedUrl = new URL(String(url), window.location.href).href;\n emitWebviewEvent(\"new-window-open\", JSON.stringify({\n url: resolvedUrl,\n isCmdClick: true,\n isSPANavigation: true\n }));\n return;\n }\n return originalReplaceState.apply(this, [state, title, url]);\n };\n}\nfunction shouldApplyOverscrollPrevention(platform) {\n return platform !== \"linux\";\n}\nfunction initOverscrollPrevention(targetDocument = document, platform = window.__electrobunPlatform) {\n if (!shouldApplyOverscrollPrevention(platform))\n return;\n targetDocument.addEventListener(\"DOMContentLoaded\", () => {\n const style = targetDocument.createElement(\"style\");\n style.type = \"text/css\";\n style.appendChild(targetDocument.createTextNode(\"html, body { overscroll-behavior: none; }\"));\n targetDocument.head.appendChild(style);\n });\n}\n\n// src/preload/index.ts\ninitEncryption().catch((err) => console.error(\"Failed to initialize encryption:\", err));\nvar internalMessageHandler = (msg) => {\n handleResponse(msg);\n};\nvar defaultUserMessageHandler = (msg) => {\n if (!window.__electrobunPendingHostMessages) {\n window.__electrobunPendingHostMessages = [];\n }\n window.__electrobunPendingHostMessages.push(msg);\n};\nif (!window.__electrobun) {\n window.__electrobun = {\n receiveInternalMessageFromHost: internalMessageHandler,\n receiveMessageFromHost: defaultUserMessageHandler,\n receiveInternalMessageFromBun: internalMessageHandler,\n receiveMessageFromBun: defaultUserMessageHandler\n };\n} else {\n window.__electrobun.receiveInternalMessageFromHost = internalMessageHandler;\n window.__electrobun.receiveMessageFromHost = defaultUserMessageHandler;\n window.__electrobun.receiveInternalMessageFromBun = internalMessageHandler;\n window.__electrobun.receiveMessageFromBun = defaultUserMessageHandler;\n}\ninitHostMessageBridge();\ninitLifecycleEvents();\ninitCmdClickHandling();\ninitSPANavigationInterception();\ninitOverscrollPrevention();\ninitDragRegions();\ninitExternalDropFocusRestoration();\ninitWebviewTag();\ninitWgpuTag();\ninitUiTag();\n})();";
6
6
 
7
7
  // Minimal preload for sandboxed/untrusted webviews (lifecycle events only, no RPC)
8
8
  export const preloadScriptSandboxed = "(function(){// src/preload/events.ts\nfunction emitWebviewEvent(eventName, detail) {\n setTimeout(() => {\n const bridge = window.__electrobunEventBridge || window.__electrobunInternalBridge;\n bridge?.postMessage(JSON.stringify({\n id: \"webviewEvent\",\n type: \"message\",\n payload: {\n id: window.__electrobunWebviewId,\n eventName,\n detail\n }\n }));\n });\n}\nfunction initHostMessageBridge(targetWindow = window, emit = emitWebviewEvent) {\n targetWindow.__electrobunSendToHost = (message) => {\n emit(\"host-message\", JSON.stringify(message));\n };\n}\nfunction initLifecycleEvents() {\n window.addEventListener(\"load\", () => {\n if (window === window.top) {\n emitWebviewEvent(\"dom-ready\", document.location.href);\n }\n });\n window.addEventListener(\"popstate\", () => {\n emitWebviewEvent(\"did-navigate-in-page\", window.location.href);\n });\n window.addEventListener(\"hashchange\", () => {\n emitWebviewEvent(\"did-navigate-in-page\", window.location.href);\n });\n}\nvar cmdKeyHeld = false;\nvar cmdKeyTimestamp = 0;\nvar CMD_KEY_THRESHOLD_MS = 500;\nfunction isCmdHeld() {\n if (cmdKeyHeld)\n return true;\n return Date.now() - cmdKeyTimestamp < CMD_KEY_THRESHOLD_MS && cmdKeyTimestamp > 0;\n}\nfunction initCmdClickHandling() {\n window.addEventListener(\"keydown\", (event) => {\n if (event.key === \"Meta\" || event.metaKey) {\n cmdKeyHeld = true;\n cmdKeyTimestamp = Date.now();\n }\n }, true);\n window.addEventListener(\"keyup\", (event) => {\n if (event.key === \"Meta\") {\n cmdKeyHeld = false;\n cmdKeyTimestamp = Date.now();\n }\n }, true);\n window.addEventListener(\"blur\", () => {\n cmdKeyHeld = false;\n });\n window.addEventListener(\"click\", (event) => {\n if (event.metaKey || event.ctrlKey) {\n const anchor = event.target?.closest?.(\"a\");\n if (anchor && anchor.href) {\n event.preventDefault();\n event.stopPropagation();\n event.stopImmediatePropagation();\n emitWebviewEvent(\"new-window-open\", JSON.stringify({\n url: anchor.href,\n isCmdClick: true,\n isSPANavigation: false\n }));\n }\n }\n }, true);\n}\nfunction initSPANavigationInterception() {\n const originalPushState = history.pushState;\n const originalReplaceState = history.replaceState;\n history.pushState = function(state, title, url) {\n if (isCmdHeld() && url) {\n const resolvedUrl = new URL(String(url), window.location.href).href;\n emitWebviewEvent(\"new-window-open\", JSON.stringify({\n url: resolvedUrl,\n isCmdClick: true,\n isSPANavigation: true\n }));\n return;\n }\n return originalPushState.apply(this, [state, title, url]);\n };\n history.replaceState = function(state, title, url) {\n if (isCmdHeld() && url) {\n const resolvedUrl = new URL(String(url), window.location.href).href;\n emitWebviewEvent(\"new-window-open\", JSON.stringify({\n url: resolvedUrl,\n isCmdClick: true,\n isSPANavigation: true\n }));\n return;\n }\n return originalReplaceState.apply(this, [state, title, url]);\n };\n}\nfunction shouldApplyOverscrollPrevention(platform) {\n return platform !== \"linux\";\n}\nfunction initOverscrollPrevention(targetDocument = document, platform = window.__electrobunPlatform) {\n if (!shouldApplyOverscrollPrevention(platform))\n return;\n targetDocument.addEventListener(\"DOMContentLoaded\", () => {\n const style = targetDocument.createElement(\"style\");\n style.type = \"text/css\";\n style.appendChild(targetDocument.createTextNode(\"html, body { overscroll-behavior: none; }\"));\n targetDocument.head.appendChild(style);\n });\n}\n\n// src/preload/index-sandboxed.ts\ninitHostMessageBridge();\ninitLifecycleEvents();\ninitCmdClickHandling();\ninitSPANavigationInterception();\ninitOverscrollPrevention();\n})();";
@@ -21,6 +21,7 @@ import { initDragRegions } from "./dragRegions";
21
21
  import { initExternalDropFocusRestoration } from "./externalDropFocus";
22
22
  import { initWebviewTag } from "./webviewTag";
23
23
  import { initWgpuTag } from "./wgpuTag";
24
+ import { initUiTag } from "./uiTag";
24
25
  import {
25
26
  initHostMessageBridge,
26
27
  initLifecycleEvents,
@@ -73,3 +74,4 @@ initDragRegions();
73
74
  initExternalDropFocusRestoration();
74
75
  initWebviewTag();
75
76
  initWgpuTag();
77
+ initUiTag();
@@ -0,0 +1,45 @@
1
+ // <electrobun-ui> Custom Element
2
+ // A layout-driven native surface, like <electrobun-wgpu>, whose content is a
3
+ // Cottontail UI tree mounted in the main process via registerUIRoot(name).
4
+ // The DOM element is only the anchor: nativeWrapper composites the Dawn
5
+ // layer, and the main process owns the reactive tree rendered into it.
6
+
7
+ import { send } from "./internalRpc";
8
+ import { ElectrobunWgpuTag } from "./wgpuTag";
9
+
10
+ export class ElectrobunUiTag extends ElectrobunWgpuTag {
11
+ async initWgpuView() {
12
+ await super.initWgpuView();
13
+ if (this.wgpuViewId !== null) {
14
+ // Tell the main process which named UI root should mount here.
15
+ send("uiTagMount", {
16
+ id: this.wgpuViewId,
17
+ name: this.getAttribute("name") ?? "",
18
+ });
19
+ }
20
+ }
21
+ }
22
+
23
+ export function initUiTag() {
24
+ if (!customElements.get("electrobun-ui")) {
25
+ customElements.define("electrobun-ui", ElectrobunUiTag);
26
+ }
27
+
28
+ const injectStyles = () => {
29
+ const style = document.createElement("style");
30
+ style.textContent = `
31
+ electrobun-ui {
32
+ display: block;
33
+ width: 400px;
34
+ height: 300px;
35
+ }
36
+ `;
37
+ document.head.appendChild(style);
38
+ };
39
+
40
+ if (document.readyState === "loading") {
41
+ document.addEventListener("DOMContentLoaded", injectStyles);
42
+ } else {
43
+ injectStyles();
44
+ }
45
+ }
@@ -0,0 +1,44 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import { spawnSync } from "node:child_process";
7
+
8
+ const temporaryDirectories: string[] = [];
9
+
10
+ afterEach(() => {
11
+ for (const directory of temporaryDirectories.splice(0)) {
12
+ rmSync(directory, { recursive: true, force: true });
13
+ }
14
+ });
15
+
16
+ describe("Utils.quit exit codes", () => {
17
+ test("returns the requested status outside the native host", () => {
18
+ const directory = mkdtempSync(join(tmpdir(), "electrobun-quit-test-"));
19
+ temporaryDirectories.push(directory);
20
+ const fixture = join(directory, "quit.mjs");
21
+ const utilsUrl = pathToFileURL(
22
+ join(import.meta.dirname, "../core/Utils.ts"),
23
+ ).href;
24
+ writeFileSync(fixture, `import { quit } from ${JSON.stringify(utilsUrl)};\nquit(7);\n`);
25
+
26
+ const result = spawnSync(process.execPath, [fixture], {
27
+ cwd: directory,
28
+ encoding: "utf8",
29
+ });
30
+ expect(result.error).toBeUndefined();
31
+ expect(result.status).toBe(7);
32
+ });
33
+
34
+ test("forwards the requested status to native graceful shutdown", () => {
35
+ const source = readFileSync(
36
+ join(import.meta.dirname, "../core/Utils.ts"),
37
+ "utf8",
38
+ );
39
+ expect(source).toContain(
40
+ "ffi.request.quitGracefully({ code, timeoutMs: 5000 });",
41
+ );
42
+ expect(source).toContain("quit(code ?? 0);");
43
+ });
44
+ });
@@ -172,6 +172,12 @@ export class GpuWindow {
172
172
  startPassthrough: false,
173
173
  });
174
174
 
175
+ // A transparent window needs its full-window view to alpha-composite
176
+ // (startTransparent would hide the layer entirely — tag semantics).
177
+ if (this.transparent) {
178
+ wgpuView.setAlphaBlending(true);
179
+ }
180
+
175
181
  this.wgpuViewId = wgpuView.id;
176
182
  }
177
183
 
@@ -207,14 +213,27 @@ export class GpuWindow {
207
213
  return this.activate();
208
214
  }
209
215
 
216
+ private visible = true;
217
+
210
218
  show() {
219
+ this.visible = true;
211
220
  return ffi.request.showWindow({ winId: this.id, activate: true });
212
221
  }
213
222
 
214
223
  showInactive() {
224
+ this.visible = true;
215
225
  return ffi.request.showWindow({ winId: this.id, activate: false });
216
226
  }
217
227
 
228
+ hide() {
229
+ this.visible = false;
230
+ return ffi.request.hideWindow({ winId: this.id });
231
+ }
232
+
233
+ isVisible(): boolean {
234
+ return this.visible;
235
+ }
236
+
218
237
  minimize() {
219
238
  return ffi.request.minimizeWindow({ winId: this.id });
220
239
  }
@@ -122,7 +122,7 @@ export const showNotification = (options: NotificationOptions): void => {
122
122
 
123
123
  let isQuitting = false;
124
124
 
125
- export const quit = () => {
125
+ export const quit = (code = 0) => {
126
126
  if (isQuitting) return;
127
127
  isQuitting = true;
128
128
 
@@ -138,9 +138,9 @@ export const quit = () => {
138
138
  }
139
139
 
140
140
  if (native) {
141
- ffi.request.quitGracefully({ code: 0, timeoutMs: 5000 });
141
+ ffi.request.quitGracefully({ code, timeoutMs: 5000 });
142
142
  } else {
143
- process.exit(0);
143
+ process.exit(code);
144
144
  }
145
145
  };
146
146
 
@@ -152,7 +152,7 @@ process.exit = ((code?: number) => {
152
152
  ffi.request.quitGracefully({ code: code ?? 0, timeoutMs: 0 });
153
153
  return;
154
154
  }
155
- quit();
155
+ quit(code ?? 0);
156
156
  } else {
157
157
  _originalProcessExit(code ?? 0);
158
158
  }
@@ -276,6 +276,54 @@ export const clipboardWriteText = (text: string): void => {
276
276
  ffi.request.clipboardWriteText({ text });
277
277
  };
278
278
 
279
+ // Screen Recording permission (macOS). Both calls are struct-free, so they
280
+ // go straight to CoreGraphics; on other platforms they report granted.
281
+ const coreGraphics = (() => {
282
+ if (OS !== "macos") return null;
283
+ try {
284
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
285
+ const { dlopen, FFIType } = require("bun:ffi");
286
+ return dlopen(
287
+ "/System/Library/Frameworks/CoreGraphics.framework/Versions/A/CoreGraphics",
288
+ {
289
+ CGPreflightScreenCaptureAccess: { args: [], returns: FFIType.bool },
290
+ CGRequestScreenCaptureAccess: { args: [], returns: FFIType.bool },
291
+ },
292
+ );
293
+ } catch {
294
+ return null;
295
+ }
296
+ })();
297
+
298
+ export const screenCapture = {
299
+ /** Whether the app currently has Screen Recording permission. */
300
+ hasAccess(): boolean {
301
+ if (OS !== "macos") return true;
302
+ return coreGraphics
303
+ ? Boolean(coreGraphics.symbols.CGPreflightScreenCaptureAccess())
304
+ : false;
305
+ },
306
+ /**
307
+ * Ask macOS for Screen Recording permission. Shows the system prompt the
308
+ * first time; afterwards the user must grant it in System Settings →
309
+ * Privacy & Security → Screen Recording (and relaunch the app).
310
+ */
311
+ requestAccess(): boolean {
312
+ if (OS !== "macos") return true;
313
+ return coreGraphics
314
+ ? Boolean(coreGraphics.symbols.CGRequestScreenCaptureAccess())
315
+ : false;
316
+ },
317
+ /** Open the Screen Recording pane of System Settings (macOS). */
318
+ openSettings(): void {
319
+ if (OS !== "macos") return;
320
+ Bun.spawn([
321
+ "open",
322
+ "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture",
323
+ ]);
324
+ },
325
+ };
326
+
279
327
  /**
280
328
  * Read image from the system clipboard as PNG data.
281
329
  * @returns PNG image data as Uint8Array, or null if no image is available
@@ -97,6 +97,15 @@ export class WGPUView {
97
97
  ffi.request.wgpuViewSetTransparent({ id: this.id, transparent });
98
98
  }
99
99
 
100
+ /**
101
+ * Enable alpha compositing: the surface's alpha channel blends against
102
+ * whatever is behind the view (unlike setTransparent, which hides the
103
+ * layer entirely). Required for transparent GPU-rendered windows.
104
+ */
105
+ setAlphaBlending(enabled: boolean) {
106
+ ffi.request.wgpuViewSetAlphaBlending({ id: this.id, enabled });
107
+ }
108
+
100
109
  setPassthrough(passthrough: boolean) {
101
110
  ffi.request.wgpuViewSetPassthrough({ id: this.id, passthrough });
102
111
  }
@@ -0,0 +1 @@
1
+ export * from "../ui/index";