@jielga/react-popup-window 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,249 +1,371 @@
1
- # @jielga/react-popup-window
2
-
3
- [![CI](https://github.com/jielga/react-popup-window/actions/workflows/ci.yml/badge.svg)](https://github.com/jielga/react-popup-window/actions/workflows/ci.yml)
4
- [![npm](https://img.shields.io/npm/v/%40jielga%2Freact-popup-window)](https://www.npmjs.com/package/@jielga/react-popup-window)
5
- [![license](https://img.shields.io/npm/l/%40jielga%2Freact-popup-window)](./LICENSE)
6
-
7
- React hook for rendering part of a component tree in a separate browser
8
- window. Content is rendered through a portal into the popup document, so it
9
- remains part of the calling tree: state, context, and event handlers work
10
- across windows without bridging.
11
-
12
- [Documentation and live examples](https://jielga.github.io/react-popup-window/)
13
-
14
- ## Features
15
-
16
- - Portal-based rendering — popup content keeps access to all ancestor
17
- context (state managers, data fetching, theming, routing)
18
- - Stylesheet synchronization — `<style>` and `<link>` elements, root
19
- `class`/`data-*` attributes, and `adoptedStyleSheets` are mirrored into
20
- the popup and kept current while it is open
21
- - Lifecycle management — detects the user closing the window, closes the
22
- popup on owner unmount and opener unload, reports blocked popups
23
- - No dependencies beyond `react` and `react-dom`
24
- - TypeScript, ESM and CJS builds, SSR-safe
25
-
26
- ## Installation
27
-
28
- ```sh
29
- npm install @jielga/react-popup-window
30
- ```
31
-
32
- Requires React 19.2 or later.
33
-
34
- ## Usage
35
-
36
- ```tsx
37
- import { usePopupWindow } from '@jielga/react-popup-window'
38
-
39
- function Dashboard() {
40
- const { open, close, isOpen, Popup } = usePopupWindow({
41
- title: 'Detached panel',
42
- features: { width: 640, height: 480 },
43
- })
44
-
45
- return (
46
- <>
47
- <button onClick={open}>Open in new window</button>
48
- <Popup>
49
- <MyPanel />
50
- </Popup>
51
- </>
52
- )
53
- }
54
- ```
55
-
56
- `Popup` renders its children into the popup window while open and nothing
57
- otherwise. It has a stable identity and can be destructured from the hook
58
- result.
59
-
60
- ### Detaching a section
61
-
62
- To hide a section in the main window while it is popped out, render it in
63
- one of two places depending on `isOpen`:
64
-
65
- ```tsx
66
- const { open, close, focus, isOpen, Popup } = usePopupWindow({ title: 'People' })
67
-
68
- const table = <DataTable />
69
-
70
- return (
71
- <>
72
- {isOpen ? (
73
- <div>
74
- <button onClick={focus}>Focus window</button>
75
- <button onClick={close}>Bring back</button>
76
- </div>
77
- ) : (
78
- <>
79
- <button onClick={open}>Open in new window</button>
80
- {table}
81
- </>
82
- )}
83
- <Popup>{table}</Popup>
84
- </>
85
- )
86
- ```
87
-
88
- `isOpen` also updates when the user closes the window directly, so the
89
- inline branch is restored in every case.
90
-
91
- ## How it works
92
-
93
- The hook opens a same-origin `about:blank` window and creates a container
94
- element in its body. The `Popup` component renders its children with
95
- `createPortal` into that container. A portal changes where DOM output is
96
- placed, not where the components sit in the tree, so popup content
97
- participates in the calling tree's state, context, and event system. React
98
- supports cross-document portals: it attaches its event delegation to the
99
- portal container when the container belongs to another document.
100
-
101
- Physically moving DOM nodes into another document is not a viable
102
- alternative: React delegates events on the root container in the main
103
- document, and a node moved elsewhere stops receiving synthetic events.
104
-
105
- The popup document runs no JavaScript of its own. All rendering and event
106
- handling execute in the opener window.
107
-
108
- ## API
109
-
110
- ### `usePopupWindow(options?)`
111
-
112
- ```ts
113
- interface UsePopupWindowOptions {
114
- /** Popup document title. Defaults to the opener document's title. */
115
- title?: string
116
- /** window.open target name. The same name reuses the window. Default: '_blank'. */
117
- name?: string
118
- /** window.open features, merged over { popup: true, width: 640, height: 480 }. */
119
- features?: PopupWindowFeatures
120
- /** Center the popup over the opener when no left/top feature is given. Default: true. */
121
- center?: boolean
122
- /** Mirror and synchronize stylesheets into the popup. Default: true. */
123
- copyStyles?: boolean
124
- /** Called after the popup window is opened and prepared. */
125
- onOpen?: (popupWindow: Window) => void
126
- /** Called when the popup closes: close(), user close, or opener unload. */
127
- onClose?: () => void
128
- /** Called when window.open returns null (popup blocked). */
129
- onBlocked?: () => void
130
- }
131
- ```
132
-
133
- Returns:
134
-
135
- | Member | Type | Description |
136
- | ------------- | -------------------------- | ------------------------------------------------------------------------------------------------- |
137
- | `Popup` | `FC<{ children? }>` | Portal component. Renders children into the popup while open. |
138
- | `open` | `() => Window \| null` | Opens the popup, or focuses it if already open. Returns `null` when blocked. Requires a user gesture. |
139
- | `close` | `() => void` | Closes the popup. |
140
- | `toggle` | `() => void` | Opens if closed, closes if open. |
141
- | `focus` | `() => void` | Focuses the popup window. |
142
- | `isOpen` | `boolean` | Whether the popup is open. |
143
- | `isBlocked` | `boolean` | Whether the last `open()` call was blocked. |
144
- | `popupWindow` | `Window \| null` | The popup `Window` while open. |
145
-
146
- ### `copyStyles(source, target, watch?)`
147
-
148
- The style synchronization used by the hook, exported for windows managed
149
- outside of it. Copies stylesheets from `source` to `target` and, when
150
- `watch` is true (default), observes the source document for changes.
151
- Returns a function that stops observing.
152
-
153
- ## Style synchronization
154
-
155
- While the popup is open, the following are mirrored from the opener
156
- document and kept current:
157
-
158
- - `<style>` and `<link rel="stylesheet">` elements. `<style>` contents are
159
- serialized from the CSSOM, so rules injected with `insertRule` are
160
- included.
161
- - Additions, removals, and text edits of style nodes in `<head>`. This
162
- covers Vite HMR, lazily loaded chunk CSS, and CSS-in-JS libraries.
163
- - `class`, `style`, and `data-*` attributes on `<html>` and `<body>`.
164
- Theme systems keyed on root attributes propagate to the popup.
165
- - `document.adoptedStyleSheets`.
166
-
167
- Set `copyStyles: false` to disable.
168
-
169
- ## Communication
170
-
171
- Popup content rendered through `Popup` is part of the calling component
172
- tree and executes in the opener's JavaScript realm. Props, state, and
173
- context are the communication mechanism; no message channel is required or
174
- provided.
175
-
176
- `postMessage` remains relevant only for scripts hosted in the popup
177
- document itself (for example, an injected non-React widget). Such scripts
178
- run in the popup's realm and can post to `window.opener`; the exposed
179
- `popupWindow` handle can be used from the opener side. Note that calling
180
- `opener.postMessage` from a portal event handler posts from the opener's
181
- own realm — the browser reports the main window, not the popup, as
182
- `event.source`.
183
-
184
- ## Portal-based UI libraries
185
-
186
- Component libraries typically mount overlays — menus, popovers, modals,
187
- tooltips — into `document.body`, which is the main window's body even for
188
- components rendered inside the popup. Provide a portal target inside the
189
- popup document instead:
190
-
191
- - Mantine: portal defaults can be set through the theme. See
192
- [`SameWindowPortals`](docs/src/examples/SameWindowPortals.tsx) for a
193
- wrapper that resolves its own `ownerDocument` and supplies that
194
- document's body as the default `Portal` target.
195
- - Radix, MUI, and similar: use the per-component portal `container` prop
196
- with `popupWindow.document.body`.
197
-
198
- ## Limitations
199
-
200
- - Browsers do not allow hiding the address bar entirely. `popup: true`
201
- (the default) requests the minimal window chrome the platform provides.
202
- - `open()` must be called from a user gesture; otherwise the browser's
203
- popup blocker intervenes and `open()` returns `null`.
204
- - Popup content unmounts and remounts when it moves between windows. State
205
- that should survive detaching belongs in the component that owns the
206
- hook, or in an external store.
207
- - The popup closes when the owning component unmounts and when the opener
208
- window unloads. The popup document cannot outlive the opener.
209
-
210
- ## Agent skills
211
-
212
- The package ships [Agent Skills](https://agentskills.io) for AI coding
213
- agents, managed with [`@tanstack/intent`](https://www.npmjs.com/package/@tanstack/intent):
214
-
215
- ```sh
216
- npx @tanstack/intent@latest list
217
- npx @tanstack/intent@latest load @jielga/react-popup-window#getting-started
218
- ```
219
-
220
- | Skill | Contents |
221
- | ----------------- | -------------------------------------------------------------------- |
222
- | `getting-started` | Hook API, options, lifecycle, common mistakes |
223
- | `popup-content` | Style synchronization, bounded-height layouts, portal-based overlays |
224
-
225
- ## Development
226
-
227
- | Path | Contents |
228
- | ----------- | --------------------------------------------------------------- |
229
- | `src/` | Library source. Vite library mode; ESM, CJS, and declarations. |
230
- | `docs/` | Documentation site with live examples, deployed to GitHub Pages. |
231
- | `e2e/` | Playwright tests that exercise real popup windows in Chromium. |
232
- | `skills/` | Agent skills shipped with the package. |
233
-
234
- ```sh
235
- npm install
236
- npm run dev # documentation site with the library aliased to source
237
- npm test # unit tests (Vitest, jsdom)
238
- npm run e2e # end-to-end tests (Playwright)
239
- npm run build # build the library into dist/
240
- npm run skills:validate # validate agent skills
241
- npm run changeset # describe a change for the changelog and the next release
242
- ```
243
-
244
- Releases are published to npm by Changesets when the **chore: version packages**
245
- pull request is merged — see [RELEASING.md](./RELEASING.md).
246
-
247
- ## License
248
-
249
- [MIT](./LICENSE)
1
+ # @jielga/react-popup-window
2
+
3
+ [![CI](https://github.com/jielga/react-popup-window/actions/workflows/ci.yml/badge.svg)](https://github.com/jielga/react-popup-window/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/%40jielga%2Freact-popup-window)](https://www.npmjs.com/package/@jielga/react-popup-window)
5
+ [![license](https://img.shields.io/npm/l/%40jielga%2Freact-popup-window)](./LICENSE)
6
+
7
+ React hook for rendering part of a component tree in a separate browser
8
+ window. Content is rendered through a portal into the popup document, so it
9
+ remains part of the calling tree: state, context, and event handlers work
10
+ across windows without bridging.
11
+
12
+ [Documentation and live examples](https://jielga.github.io/react-popup-window/)
13
+
14
+ ## Features
15
+
16
+ - Portal-based rendering — popup content keeps access to all ancestor
17
+ context (state managers, data fetching, theming, routing)
18
+ - Stylesheet synchronization — `<style>` and `<link>` elements, root
19
+ `class`/`data-*` attributes, and `adoptedStyleSheets` are mirrored into
20
+ the popup and kept current while it is open
21
+ - Lifecycle management — detects the user closing the window, closes the
22
+ popup on owner unmount and opener unload, reports blocked popups
23
+ - No dependencies beyond `react` and `react-dom`
24
+ - TypeScript, ESM and CJS builds, SSR-safe
25
+
26
+ ## Installation
27
+
28
+ ```sh
29
+ npm install @jielga/react-popup-window
30
+ ```
31
+
32
+ Requires React 19.2 or later.
33
+
34
+ ## Usage
35
+
36
+ ```tsx
37
+ import { usePopupWindow } from '@jielga/react-popup-window'
38
+
39
+ function Dashboard() {
40
+ const { open, close, isOpen, Popup } = usePopupWindow({
41
+ title: 'Detached panel',
42
+ features: { width: 640, height: 480 },
43
+ })
44
+
45
+ return (
46
+ <>
47
+ <button onClick={open}>Open in new window</button>
48
+ <Popup>
49
+ <MyPanel />
50
+ </Popup>
51
+ </>
52
+ )
53
+ }
54
+ ```
55
+
56
+ `Popup` renders its children into the popup window while open and nothing
57
+ otherwise. It has a stable identity and can be destructured from the hook
58
+ result.
59
+
60
+ ### Detaching a section
61
+
62
+ To hide a section in the main window while it is popped out, render it in
63
+ one of two places depending on `isOpen`:
64
+
65
+ ```tsx
66
+ const { open, close, focus, isOpen, Popup } = usePopupWindow({ title: 'People' })
67
+
68
+ const table = <DataTable />
69
+
70
+ return (
71
+ <>
72
+ {isOpen ? (
73
+ <div>
74
+ <button onClick={focus}>Focus window</button>
75
+ <button onClick={close}>Bring back</button>
76
+ </div>
77
+ ) : (
78
+ <>
79
+ <button onClick={open}>Open in new window</button>
80
+ {table}
81
+ </>
82
+ )}
83
+ <Popup>{table}</Popup>
84
+ </>
85
+ )
86
+ ```
87
+
88
+ `isOpen` also updates when the user closes the window directly, so the
89
+ inline branch is restored in every case.
90
+
91
+ ### Show your own URL in the address bar
92
+
93
+ By default, the popup loads `about:blank`, and its address bar shows that.
94
+ To show your own domain instead, set `url` to an empty page served on the same origin.
95
+ For a Vite app, add `public/popup.html`:
96
+
97
+ ```html
98
+ <!doctype html>
99
+ <html lang="en">
100
+ <head>
101
+ <meta charset="utf-8" />
102
+ <title>Loading</title>
103
+ </head>
104
+ <body></body>
105
+ </html>
106
+ ```
107
+
108
+ ```tsx
109
+ const { open, Popup } = usePopupWindow({ title: 'Panel', url: '/popup.html' })
110
+ ```
111
+
112
+ `isOpen` turns `true` as soon as the window opens; `Popup` renders once the page has loaded.
113
+ Important: do not point `url` at a route of the app, as that starts a second copy of the app inside the popup.
114
+ If the page redirects to another origin, the popup closes and `onBlocked` is called.
115
+
116
+ ## How it works
117
+
118
+ The hook opens a same-origin window, `about:blank` by default or the page
119
+ set with `url`, and creates a container element in its body. The `Popup` component renders its children with
120
+ `createPortal` into that container. A portal changes where DOM output is
121
+ placed, not where the components sit in the tree, so popup content
122
+ participates in the calling tree's state, context, and event system. React
123
+ supports cross-document portals: it attaches its event delegation to the
124
+ portal container when the container belongs to another document.
125
+
126
+ Physically moving DOM nodes into another document is not a viable
127
+ alternative: React delegates events on the root container in the main
128
+ document, and a node moved elsewhere stops receiving synthetic events.
129
+
130
+ The popup document runs no JavaScript of its own. All rendering and event
131
+ handling execute in the opener window.
132
+
133
+ ## API
134
+
135
+ ### `usePopupWindow(options?)`
136
+
137
+ ```ts
138
+ interface UsePopupWindowOptions {
139
+ /** Popup document title. Defaults to the opener document's title. */
140
+ title?: string
141
+ /** window.open target name. The same name reuses the window. Default: '_blank'. */
142
+ name?: string
143
+ /** Same-origin URL to load instead of about:blank, such as an empty /popup.html. Default: 'about:blank'. */
144
+ url?: string
145
+ /** window.open features, merged over { popup: true, width: 640, height: 480 }. */
146
+ features?: PopupWindowFeatures
147
+ /** Center the popup over the opener when no left/top feature is given. Default: true. */
148
+ center?: boolean
149
+ /** Mirror and synchronize stylesheets into the popup. Default: true. */
150
+ copyStyles?: boolean
151
+ /** Called when Popup starts rendering into the popup window. */
152
+ onOpen?: (popupWindow: Window) => void
153
+ /** Called when the popup closes: close(), user close, or opener unload. */
154
+ onClose?: () => void
155
+ /** Called when the popup is blocked or its document is not scriptable. */
156
+ onBlocked?: () => void
157
+ }
158
+ ```
159
+
160
+ Returns:
161
+
162
+ | Member | Type | Description |
163
+ | ------------- | -------------------------- | ------------------------------------------------------------------------------------------------- |
164
+ | `Popup` | `FC<{ children? }>` | Portal component. Renders children into the popup while open. |
165
+ | `open` | `() => Window \| null` | Opens the popup, or focuses it if already open. Returns `null` when blocked. Requires a user gesture. |
166
+ | `close` | `() => void` | Closes the popup. |
167
+ | `toggle` | `() => void` | Opens if closed, closes if open. |
168
+ | `focus` | `() => void` | Focuses the popup window. |
169
+ | `isOpen` | `boolean` | Whether the popup is open. |
170
+ | `isBlocked` | `boolean` | Whether the last `open()` call was blocked. |
171
+ | `popupWindow` | `Window \| null` | The popup `Window` while open. |
172
+
173
+ ### `copyStyles(source, target, watch?)`
174
+
175
+ The style synchronization used by the hook, exported for windows managed
176
+ outside of it. Copies stylesheets from `source` to `target` and, when
177
+ `watch` is true (default), observes the source document for changes.
178
+ Returns a function that stops observing.
179
+
180
+ ## Style synchronization
181
+
182
+ While the popup is open, the following are mirrored from the opener
183
+ document and kept current:
184
+
185
+ - `<style>` and `<link rel="stylesheet">` elements. `<style>` contents are
186
+ serialized from the CSSOM, so rules injected with `insertRule` are
187
+ included.
188
+ - Additions, removals, and text edits of style nodes in `<head>`. This
189
+ covers Vite HMR, lazily loaded chunk CSS, and CSS-in-JS libraries.
190
+ - `class`, `style`, and `data-*` attributes on `<html>` and `<body>`.
191
+ Theme systems keyed on root attributes propagate to the popup.
192
+ - `document.adoptedStyleSheets`.
193
+
194
+ The popup loads each copied `<link>` stylesheet again.
195
+ `Popup` renders once they have loaded or failed, so the first frame shown is styled.
196
+ Alternate stylesheets and stylesheets whose `media` does not match are not waited for.
197
+ If a stylesheet takes longer than 3 seconds, `Popup` renders without it.
198
+ Note that a `<link>` added to the opener while the popup is open loads in the popup after the content is shown.
199
+
200
+ Set `copyStyles: false` to disable.
201
+
202
+ ## Communication
203
+
204
+ Popup content rendered through `Popup` is part of the calling component
205
+ tree and executes in the opener's JavaScript realm. Props, state, and
206
+ context are the communication mechanism; no message channel is required or
207
+ provided.
208
+
209
+ `postMessage` remains relevant only for scripts hosted in the popup
210
+ document itself (for example, an injected non-React widget). Such scripts
211
+ run in the popup's realm and can post to `window.opener`; the exposed
212
+ `popupWindow` handle can be used from the opener side. Note that calling
213
+ `opener.postMessage` from a portal event handler posts from the opener's
214
+ own realm — the browser reports the main window, not the popup, as
215
+ `event.source`.
216
+
217
+ ## Keep overlays in the popup window
218
+
219
+ Component libraries mount overlays - menus, popovers, modals, tooltips -
220
+ through portals into `document.body`. Popup content executes in the
221
+ opener's JavaScript realm, so the bare `document` global is the main
222
+ window's document even for components rendered inside the popup, and the
223
+ overlay opens in the main window. The `ownerDocument` of a node rendered
224
+ inside the popup is the popup's document; the portal container must come
225
+ from there. The rule covers any portal, including a raw `createPortal` in
226
+ application code.
227
+
228
+ ### Mantine
229
+
230
+ Mantine reads portal defaults from the theme. The following wrapper
231
+ resolves its own `ownerDocument` after mount and supplies that document's
232
+ body as the default `Portal` target, so `Menu`, `Popover`, `Tooltip`, and
233
+ `Modal` open in the window they are rendered in:
234
+
235
+ ```tsx
236
+ import { MantineThemeProvider, Portal } from '@mantine/core'
237
+ import { useEffect, useMemo, useRef, useState } from 'react'
238
+ import type { ReactNode } from 'react'
239
+
240
+ export function SameWindowPortals({ children }: { children: ReactNode }) {
241
+ const probeRef = useRef<HTMLDivElement>(null)
242
+ const [target, setTarget] = useState<HTMLElement | null>(null)
243
+
244
+ useEffect(() => {
245
+ setTarget(probeRef.current?.ownerDocument.body ?? null)
246
+ }, [])
247
+
248
+ const theme = useMemo(
249
+ () => ({
250
+ components: {
251
+ Portal: Portal.extend({ defaultProps: target ? { target } : {} }),
252
+ },
253
+ }),
254
+ [target],
255
+ )
256
+
257
+ return (
258
+ <div ref={probeRef} style={{ display: 'contents' }}>
259
+ <MantineThemeProvider inherit theme={theme}>
260
+ {children}
261
+ </MantineThemeProvider>
262
+ </div>
263
+ )
264
+ }
265
+ ```
266
+
267
+ Render the wrapper inside `<Popup>`, around the content that opens
268
+ overlays; it resolves the container from the tree it mounts in, so the
269
+ same component also works inline. The
270
+ [live examples](https://jielga.github.io/react-popup-window/) use it
271
+ around each grid, including a `Modal` opened from a popped-out grid; the
272
+ shipped `popup-content` skill contains the same code and the common
273
+ mistakes around it.
274
+
275
+ Mantine's own portal props interact with the wrapper as follows:
276
+
277
+ - An explicit `target` or `portalProps` on a component overrides the
278
+ theme default, so the overlay opens in the main window again.
279
+ - `withinPortal={false}` needs no wrapper: the overlay renders inline and
280
+ therefore inside the popup document. It then clips against
281
+ `overflow: hidden` ancestors and local stacking contexts, which is why
282
+ the theme default is preferred.
283
+
284
+ ### Radix, MUI, and similar
285
+
286
+ Pass the per-component portal `container` prop an element of the popup
287
+ document: `popupWindow.document.body` where the hook's result is in
288
+ scope, or `node.ownerDocument.body` from a ref on any rendered node
289
+ deeper in the tree - the same probe the wrapper above uses.
290
+
291
+ ### Window-bound overlay behavior
292
+
293
+ A correctly placed overlay can still bind window-level behavior to the
294
+ main window, because those bindings also run in the opener's realm.
295
+ Mantine's `Modal` listens for Escape with `window.addEventListener` and
296
+ locks scroll on the main window's body, so Escape pressed in the popup
297
+ does not close the modal. This cannot be redirected from outside the
298
+ component; for state you own, add a listener on the popup window:
299
+
300
+ ```tsx
301
+ // Close our own modal on Escape pressed in the popup.
302
+ useEffect(() => {
303
+ if (!opened) return
304
+ const win = hostRef.current?.ownerDocument.defaultView
305
+ const onKeyDown = (e: KeyboardEvent) => e.key === 'Escape' && setOpened(false)
306
+ win?.addEventListener('keydown', onKeyDown)
307
+ return () => win?.removeEventListener('keydown', onKeyDown)
308
+ }, [opened])
309
+ ```
310
+
311
+ ## Limitations
312
+
313
+ - Browsers do not allow hiding the address bar entirely. `popup: true`
314
+ (the default) requests the minimal window chrome the platform provides.
315
+ By default, the bar shows `about:blank`; `url` replaces that with a page
316
+ of your own, see
317
+ [Show your own URL in the address bar](#show-your-own-url-in-the-address-bar).
318
+ - `open()` must be called from a user gesture; otherwise the browser's
319
+ popup blocker intervenes and `open()` returns `null`.
320
+ - Popup content unmounts and remounts when it moves between windows. State
321
+ that should survive detaching belongs in the component that owns the
322
+ hook, or in an external store.
323
+ - The popup closes when the owning component unmounts and when the opener
324
+ window unloads. The popup document cannot outlive the opener.
325
+ - Sandboxed embedders — VS Code's built-in browser, CodeSandbox and
326
+ StackBlitz previews, iframes sandboxed without
327
+ `allow-popups-to-escape-sandbox` — open popups with an opaque origin the
328
+ opener cannot script, so the portal cannot render into them. `open()`
329
+ detects this, closes the window, reports it through `isBlocked` and
330
+ `onBlocked`, and returns `null`. Use a real browser tab instead.
331
+
332
+ ## Agent skills
333
+
334
+ The package ships [Agent Skills](https://agentskills.io) for AI coding
335
+ agents, managed with [`@tanstack/intent`](https://www.npmjs.com/package/@tanstack/intent):
336
+
337
+ ```sh
338
+ npx @tanstack/intent@latest list
339
+ npx @tanstack/intent@latest load @jielga/react-popup-window#getting-started
340
+ ```
341
+
342
+ | Skill | Contents |
343
+ | ----------------- | -------------------------------------------------------------------- |
344
+ | `getting-started` | Hook API, options, lifecycle, common mistakes |
345
+ | `popup-content` | Style synchronization, bounded-height layouts, portal-based overlays |
346
+
347
+ ## Development
348
+
349
+ | Path | Contents |
350
+ | ----------- | --------------------------------------------------------------- |
351
+ | `src/` | Library source. Vite library mode; ESM, CJS, and declarations. |
352
+ | `docs/` | Documentation site with live examples, deployed to GitHub Pages. |
353
+ | `e2e/` | Playwright tests that exercise real popup windows in Chromium. |
354
+ | `skills/` | Agent skills shipped with the package. |
355
+
356
+ ```sh
357
+ npm install
358
+ npm run dev # documentation site with the library aliased to source
359
+ npm test # unit tests (Vitest, jsdom)
360
+ npm run e2e # end-to-end tests (Playwright)
361
+ npm run build # build the library into dist/
362
+ npm run skills:validate # validate agent skills
363
+ npm run changeset # describe a change for the changelog and the next release
364
+ ```
365
+
366
+ Releases are published to npm by Changesets when the **chore: version packages**
367
+ pull request is merged — see [RELEASING.md](./RELEASING.md).
368
+
369
+ ## License
370
+
371
+ [MIT](./LICENSE)
@@ -15,4 +15,15 @@
15
15
  * Returns a function that stops observing.
16
16
  */
17
17
  export declare function copyStyles(source: Document, target: Document, watch?: boolean): () => void;
18
+ export interface StyleMirror {
19
+ /** Stops observing the source document. */
20
+ stop: () => void;
21
+ /**
22
+ * The `<link>` elements created in `target` by the initial copy. The target
23
+ * fetches each of them asynchronously.
24
+ */
25
+ links: HTMLLinkElement[];
26
+ }
27
+ /** {@link copyStyles}, plus the `<link>` elements the initial copy created. */
28
+ export declare function mirrorStyles(source: Document, target: Document, watch?: boolean): StyleMirror;
18
29
  //# sourceMappingURL=copyStyles.d.ts.map