diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31bdd44..821666a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,7 +113,14 @@ jobs: cache: "yarn" - run: yarn install --immutable - name: Install Electron ${{ matrix.electron }} - run: yarn add -D electron@${{ matrix.electron }} + # yarn 1 sometimes skips electron's postinstall when `yarn add` + # swaps versions over an existing install — `dist/` ends up empty + # and every later step (chrome-sandbox fix, build, e2e) fails. The + # install script downloads the prebuilt binary if it's missing and + # is a no-op if it's already there. + run: | + yarn add -D electron@${{ matrix.electron }} + node node_modules/electron/install.js # Re-run after the electron install — `yarn add` reinstalls the binary # and resets permissions. Same Ubuntu 24.04 userns issue as the e2e job. - name: Fix chrome-sandbox diff --git a/README.md b/README.md index 9b02e6b..3a8730f 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ The `default*` / controlled split follows React's `defaultValue` / `value` patte | `persistBounds` | `string` | — | Unique key. Saves bounds to localStorage, restores on reopen. | | `recreateOnShapeChange` | `boolean` | `false` | Recreate window when creation-only props change. | | `name` | `string` | — | Debug label for DevTools and warning messages. | -| `injectStyles` | `"auto" \| false \| (doc) => void` | `"auto"` | How to copy styles into the child window. `false` for CSS-in-JS. | +| `injectStyles` | `"auto" \| false \| (doc) => void` | `"auto"` | How to copy styles into the child window. `false` for CSS-in-JS. The function form replaces auto mirroring entirely. | ## Hooks @@ -267,6 +267,44 @@ On `open={true}`: acquires a pre-warmed window from the pool (instant). On `open Shape props (`transparent`, `frame`, `titleBarStyle`, `vibrancy`) and `injectStyles` are fixed by the pool definition. Most other props work per-use: `defaultWidth`/`defaultHeight` size the window on each acquire, and behavior props (`alwaysOnTop`, `opacity`, etc.) update live while open. `targetDisplay`, `persistBounds`, and `recreateOnShapeChange` are not accepted on `` (TypeScript error) — pool windows are pre-created and reused, so these don't fit the model. +### Per-window setup (`onWindowSetup`) + +Each pooled window is its own browsing-context realm — it has its own `customElements` registry, its own prototypes, its own `document`. Code that registers globals on the *parent* window (e.g. a library calling `customElements.define(...)` at import time) won't be visible inside a pooled window. + +`onWindowSetup` is a per-window hook that runs once for each window the pool creates — including pre-warmed idle windows — right after the document is initialized (``, styles, and the `#root` container are in place) and **before React portals any content in**. That ordering matters: custom elements registered here upgrade synchronously as React inserts them, so there's no flash of unupgraded content the way a `useLayoutEffect` (which runs after mount) would risk. + +```tsx +const popoutPool = createWindowPool( + { frame: true }, + { minIdle: 1 }, + { + // injectStyles stays "auto" — onWindowSetup is additive, not a replacement + onWindowSetup: (childWindow, doc) => { + // Register custom elements on the child realm's registry + if (!childWindow.customElements.get("my-widget")) { + childWindow.customElements.define("my-widget", class extends childWindow.HTMLElement {}); + } + // Sync a documentElement attribute from the parent + doc.documentElement.dataset.theme = document.documentElement.dataset.theme; + + // Optional cleanup, runs when the window is destroyed (pool teardown, + // idle eviction, or external close — not on hide/release back to pool). + return () => { + /* tear down listeners, observers, etc. */ + }; + }, + }, +); +``` + +This is the sanctioned home for "auto styles **plus** a bit more per-window setup." The function form of `injectStyles` *replaces* the auto `