@pip-it-up/react 0.1.1 → 0.1.4
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 +60 -12
- package/dist/index.d.mts +36 -4
- package/dist/index.d.ts +36 -4
- package/dist/index.js +238 -82
- package/dist/index.mjs +210 -54
- package/package.json +17 -4
package/README.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# @pip-it-up/react
|
|
2
2
|
|
|
3
|
-
React bindings for
|
|
3
|
+
React bindings for `pip-it-up` — the **Document Picture-in-Picture** engine.
|
|
4
|
+
|
|
5
|
+
## What is Document Picture-in-Picture?
|
|
6
|
+
|
|
7
|
+
The [Document Picture-in-Picture API](https://developer.mozilla.org/en-US/docs/Web/API/Document_Picture-in-Picture_API) is a new browser capability that allows you to open a floating window that can be populated with any arbitrary HTML content, rather than just a video element.
|
|
8
|
+
|
|
9
|
+
`@pip-it-up/react` makes it trivial to use this API in React applications with familiar patterns like Portals, Hooks, and Controlled Components.
|
|
4
10
|
|
|
5
11
|
## Installation
|
|
6
12
|
|
|
@@ -12,7 +18,7 @@ npm install @pip-it-up/react @pip-it-up/core
|
|
|
12
18
|
|
|
13
19
|
### `<PipWrapper>`
|
|
14
20
|
|
|
15
|
-
Wraps the content you want to move into the
|
|
21
|
+
Wraps the content you want to move into the **Picture-in-Picture** window.
|
|
16
22
|
|
|
17
23
|
#### Uncontrolled (Default)
|
|
18
24
|
```tsx
|
|
@@ -32,34 +38,76 @@ const [isOpen, setIsOpen] = useState(false);
|
|
|
32
38
|
```
|
|
33
39
|
|
|
34
40
|
#### Props
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
- `
|
|
41
|
+
|
|
42
|
+
Supports all `PipOptions` from `@pip-it-up/core`, including:
|
|
43
|
+
|
|
44
|
+
- **`width` / `height`** (number, optional): If provided, forces the PiP window to these dimensions. **If omitted, the library uses a `ResizeObserver` to automatically match the component's exact size on the page.**
|
|
45
|
+
- **`mode`** (`"move" | "portal"`, default: `"move"`):
|
|
46
|
+
- In this React package, both `"move"` and `"portal"` utilize **React Portals** to move content safely between windows. They behave identically: a placeholder is left in the original spot, and the component stays in the same React tree.
|
|
47
|
+
- **`copyStyles`** (`"sync" | "once" | false`, default: `"sync"`):
|
|
48
|
+
- `"sync"`: Real-time synchronization of CSS changes (MutationObserver).
|
|
49
|
+
- `"once"`: One-time copy at window open.
|
|
50
|
+
- **`reserveSpace`** (boolean, default: `true`): Whether to show a placeholder in the original position to prevent layout jumps.
|
|
51
|
+
- **`placeholder`** (ReactNode): Custom component to show in the placeholder area.
|
|
52
|
+
- **`centerInPip`** (boolean, default: `false`): Automatically centers your content in the PiP window.
|
|
53
|
+
- **`open`** (boolean): Controlled state for the window.
|
|
54
|
+
- **`onOpenChange`** (callback): Fired when the window opens or closes.
|
|
39
55
|
|
|
40
56
|
### `<PipTrigger>`
|
|
41
57
|
|
|
42
|
-
A button that toggles the
|
|
58
|
+
A button that toggles the **Picture-in-Picture** window.
|
|
43
59
|
|
|
44
60
|
```tsx
|
|
45
61
|
<PipTrigger asChild>
|
|
46
|
-
<button className="my-custom-btn">Open
|
|
62
|
+
<button className="my-custom-btn">Open Picture-in-Picture</button>
|
|
47
63
|
</PipTrigger>
|
|
48
64
|
```
|
|
49
65
|
|
|
50
66
|
## Hooks
|
|
51
67
|
|
|
52
68
|
### `usePip()`
|
|
53
|
-
Returns the context state.
|
|
69
|
+
Returns the context state for managing the **Picture-in-Picture** lifecycle.
|
|
54
70
|
```tsx
|
|
55
|
-
const { isOpen, pipWindow, instance } = usePip();
|
|
71
|
+
const { isOpen, pipWindow, instance, isInsidePip } = usePip();
|
|
56
72
|
```
|
|
73
|
+
- **`isOpen`**: Boolean indicating if the PiP window is open.
|
|
74
|
+
- **`pipWindow`**: The native `Window` object of the PiP instance (null if closed).
|
|
75
|
+
- **`isInsidePip`**: Boolean that is `true` only when the component is being rendered inside the PiP window.
|
|
76
|
+
- **`instance`**: The underlying `@pip-it-up/core` instance.
|
|
77
|
+
|
|
78
|
+
> [!NOTE]
|
|
79
|
+
> `usePipContext()` is also available if you only need the raw context without the extra convenience properties of `usePip()`.
|
|
57
80
|
|
|
58
81
|
### `useIsPipSupported()`
|
|
59
|
-
Returns
|
|
82
|
+
Returns `true` if the browser natively supports the **Document Picture-in-Picture API**.
|
|
60
83
|
```tsx
|
|
61
84
|
const isSupported = useIsPipSupported();
|
|
62
85
|
```
|
|
63
86
|
|
|
87
|
+
## Tips & Gotchas
|
|
88
|
+
|
|
89
|
+
### CSS Inheritance
|
|
90
|
+
The Picture-in-Picture window is a separate document. While `@pip-it-up` automatically copies stylesheets and `body`/`html` classes, your content will **not** inherit styles from parent elements outside the `<PipWrapper>` (like a `#root` div or a theme provider).
|
|
91
|
+
* **Fix**: Add necessary alignment or theme classes (e.g., `text-center`, `dark`) directly to the content inside the `<PipWrapper>`.
|
|
92
|
+
|
|
93
|
+
### Complex Editors (Monaco, TipTap, Canvas)
|
|
94
|
+
Editors that bind to the `document` object at initialization may break when moved to a new window.
|
|
95
|
+
* **Fix 1**: Force a remount of the editor when moving to PiP by using the `isInsidePip` state as a React `key`.
|
|
96
|
+
* **Fix 2**: Lift your editor state (content, cursors, etc.) **above** the `<PipWrapper>`. Since the wrapper unmounts and remounts its children when moving them into the PiP window, any state stored *inside* the wrapper will be lost during the transition.
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
const [content, setContent] = useState("");
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<PipWrapper>
|
|
103
|
+
<RichTextEditor
|
|
104
|
+
key={isInsidePip ? 'pip' : 'main'}
|
|
105
|
+
value={content}
|
|
106
|
+
onChange={setContent}
|
|
107
|
+
/>
|
|
108
|
+
</PipWrapper>
|
|
109
|
+
);
|
|
110
|
+
```
|
|
111
|
+
|
|
64
112
|
## Next.js / SSR
|
|
65
|
-
Because the Document
|
|
113
|
+
Because the **Document Picture-in-Picture API** is browser-only, ensure components interacting with it are rendered on the client (`"use client"`).
|
package/dist/index.d.mts
CHANGED
|
@@ -2,14 +2,44 @@ import * as React from 'react';
|
|
|
2
2
|
import React__default, { ElementType, ReactNode } from 'react';
|
|
3
3
|
import { PipOptions, PipInstance, PipState } from '@pip-it-up/core';
|
|
4
4
|
|
|
5
|
-
interface PipWrapperProps extends PipOptions {
|
|
5
|
+
interface PipWrapperProps extends Omit<PipOptions, 'mode'> {
|
|
6
6
|
open?: boolean;
|
|
7
7
|
defaultOpen?: boolean;
|
|
8
8
|
onOpenChange?: (open: boolean) => void;
|
|
9
9
|
as?: ElementType;
|
|
10
10
|
originAs?: ElementType;
|
|
11
11
|
children?: ReactNode;
|
|
12
|
+
placeholder?: ReactNode;
|
|
13
|
+
placeholderClassName?: string;
|
|
14
|
+
/**
|
|
15
|
+
* @deprecated `mode` is not supported in PipWrapper. The React package always
|
|
16
|
+
* uses portal mode internally because React manages its own DOM — vanilla
|
|
17
|
+
* `move`/`clone` modes would conflict with React's reconciler. For clone mode,
|
|
18
|
+
* use the vanilla `createPip()` API directly.
|
|
19
|
+
*/
|
|
20
|
+
mode?: PipOptions['mode'];
|
|
12
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* A React wrapper for the Document Picture-in-Picture API.
|
|
24
|
+
*
|
|
25
|
+
* **Important**: The underlying core instance always uses `mode: 'portal'`
|
|
26
|
+
* regardless of any `mode` prop. This is because React handles DOM movement
|
|
27
|
+
* via its own Portal system — using the core's vanilla DOM `move`/`clone`
|
|
28
|
+
* modes would cause `removeChild` errors and reconciliation crashes.
|
|
29
|
+
*
|
|
30
|
+
* **Implication for vanilla interop**: If you register a `<PipWrapper id="foo">`
|
|
31
|
+
* and then call `getPip('foo').open({ contentEl, originEl })` from vanilla JS,
|
|
32
|
+
* the core runs in portal mode — meaning `applyMoveMode` and `applyCloneMode`
|
|
33
|
+
* are skipped. The vanilla caller would get an empty PiP window. For vanilla
|
|
34
|
+
* DOM manipulation, create a separate instance with `createPip()` directly.
|
|
35
|
+
*
|
|
36
|
+
* **Layout note**: The outer wrapper element (`originAs`, defaults to `div`)
|
|
37
|
+
* uses `display: contents` to avoid adding an extra layout box. This means:
|
|
38
|
+
* - `getBoundingClientRect()` on the forwarded ref will return all zeros.
|
|
39
|
+
* - Screen readers may skip the element.
|
|
40
|
+
* If you need to measure the wrapper, measure the inner content element
|
|
41
|
+
* (the `as` element) instead.
|
|
42
|
+
*/
|
|
13
43
|
declare const PipWrapper: React__default.ForwardRefExoticComponent<PipWrapperProps & React__default.RefAttributes<HTMLElement>>;
|
|
14
44
|
|
|
15
45
|
interface PipTriggerProps extends React__default.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
@@ -20,6 +50,7 @@ interface PipTriggerProps extends React__default.ButtonHTMLAttributes<HTMLButton
|
|
|
20
50
|
renderOpen?: React__default.ReactNode;
|
|
21
51
|
renderClose?: React__default.ReactNode;
|
|
22
52
|
renderUnsupported?: React__default.ReactNode | null;
|
|
53
|
+
hideInPip?: boolean;
|
|
23
54
|
}
|
|
24
55
|
declare const PipTrigger: React__default.ForwardRefExoticComponent<PipTriggerProps & React__default.RefAttributes<HTMLElement>>;
|
|
25
56
|
|
|
@@ -34,13 +65,14 @@ declare function usePip<T extends HTMLElement = HTMLDivElement>(options?: PipOpt
|
|
|
34
65
|
pipWindow: Window | null;
|
|
35
66
|
};
|
|
36
67
|
|
|
37
|
-
interface PipContextValue
|
|
68
|
+
interface PipContextValue {
|
|
38
69
|
instance: PipInstance;
|
|
39
70
|
state: PipState;
|
|
71
|
+
isInsidePip: boolean;
|
|
40
72
|
}
|
|
41
|
-
declare const PipContext: React.Context<PipContextValue
|
|
73
|
+
declare const PipContext: React.Context<PipContextValue | null>;
|
|
42
74
|
|
|
43
|
-
declare function usePipContext
|
|
75
|
+
declare function usePipContext(): PipContextValue;
|
|
44
76
|
|
|
45
77
|
declare const useIsPipSupported: () => boolean;
|
|
46
78
|
|
package/dist/index.d.ts
CHANGED
|
@@ -2,14 +2,44 @@ import * as React from 'react';
|
|
|
2
2
|
import React__default, { ElementType, ReactNode } from 'react';
|
|
3
3
|
import { PipOptions, PipInstance, PipState } from '@pip-it-up/core';
|
|
4
4
|
|
|
5
|
-
interface PipWrapperProps extends PipOptions {
|
|
5
|
+
interface PipWrapperProps extends Omit<PipOptions, 'mode'> {
|
|
6
6
|
open?: boolean;
|
|
7
7
|
defaultOpen?: boolean;
|
|
8
8
|
onOpenChange?: (open: boolean) => void;
|
|
9
9
|
as?: ElementType;
|
|
10
10
|
originAs?: ElementType;
|
|
11
11
|
children?: ReactNode;
|
|
12
|
+
placeholder?: ReactNode;
|
|
13
|
+
placeholderClassName?: string;
|
|
14
|
+
/**
|
|
15
|
+
* @deprecated `mode` is not supported in PipWrapper. The React package always
|
|
16
|
+
* uses portal mode internally because React manages its own DOM — vanilla
|
|
17
|
+
* `move`/`clone` modes would conflict with React's reconciler. For clone mode,
|
|
18
|
+
* use the vanilla `createPip()` API directly.
|
|
19
|
+
*/
|
|
20
|
+
mode?: PipOptions['mode'];
|
|
12
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* A React wrapper for the Document Picture-in-Picture API.
|
|
24
|
+
*
|
|
25
|
+
* **Important**: The underlying core instance always uses `mode: 'portal'`
|
|
26
|
+
* regardless of any `mode` prop. This is because React handles DOM movement
|
|
27
|
+
* via its own Portal system — using the core's vanilla DOM `move`/`clone`
|
|
28
|
+
* modes would cause `removeChild` errors and reconciliation crashes.
|
|
29
|
+
*
|
|
30
|
+
* **Implication for vanilla interop**: If you register a `<PipWrapper id="foo">`
|
|
31
|
+
* and then call `getPip('foo').open({ contentEl, originEl })` from vanilla JS,
|
|
32
|
+
* the core runs in portal mode — meaning `applyMoveMode` and `applyCloneMode`
|
|
33
|
+
* are skipped. The vanilla caller would get an empty PiP window. For vanilla
|
|
34
|
+
* DOM manipulation, create a separate instance with `createPip()` directly.
|
|
35
|
+
*
|
|
36
|
+
* **Layout note**: The outer wrapper element (`originAs`, defaults to `div`)
|
|
37
|
+
* uses `display: contents` to avoid adding an extra layout box. This means:
|
|
38
|
+
* - `getBoundingClientRect()` on the forwarded ref will return all zeros.
|
|
39
|
+
* - Screen readers may skip the element.
|
|
40
|
+
* If you need to measure the wrapper, measure the inner content element
|
|
41
|
+
* (the `as` element) instead.
|
|
42
|
+
*/
|
|
13
43
|
declare const PipWrapper: React__default.ForwardRefExoticComponent<PipWrapperProps & React__default.RefAttributes<HTMLElement>>;
|
|
14
44
|
|
|
15
45
|
interface PipTriggerProps extends React__default.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
@@ -20,6 +50,7 @@ interface PipTriggerProps extends React__default.ButtonHTMLAttributes<HTMLButton
|
|
|
20
50
|
renderOpen?: React__default.ReactNode;
|
|
21
51
|
renderClose?: React__default.ReactNode;
|
|
22
52
|
renderUnsupported?: React__default.ReactNode | null;
|
|
53
|
+
hideInPip?: boolean;
|
|
23
54
|
}
|
|
24
55
|
declare const PipTrigger: React__default.ForwardRefExoticComponent<PipTriggerProps & React__default.RefAttributes<HTMLElement>>;
|
|
25
56
|
|
|
@@ -34,13 +65,14 @@ declare function usePip<T extends HTMLElement = HTMLDivElement>(options?: PipOpt
|
|
|
34
65
|
pipWindow: Window | null;
|
|
35
66
|
};
|
|
36
67
|
|
|
37
|
-
interface PipContextValue
|
|
68
|
+
interface PipContextValue {
|
|
38
69
|
instance: PipInstance;
|
|
39
70
|
state: PipState;
|
|
71
|
+
isInsidePip: boolean;
|
|
40
72
|
}
|
|
41
|
-
declare const PipContext: React.Context<PipContextValue
|
|
73
|
+
declare const PipContext: React.Context<PipContextValue | null>;
|
|
42
74
|
|
|
43
|
-
declare function usePipContext
|
|
75
|
+
declare function usePipContext(): PipContextValue;
|
|
44
76
|
|
|
45
77
|
declare const useIsPipSupported: () => boolean;
|
|
46
78
|
|
package/dist/index.js
CHANGED
|
@@ -40,7 +40,7 @@ __export(index_exports, {
|
|
|
40
40
|
module.exports = __toCommonJS(index_exports);
|
|
41
41
|
|
|
42
42
|
// src/PipWrapper.tsx
|
|
43
|
-
var
|
|
43
|
+
var import_react3 = require("react");
|
|
44
44
|
var import_core = require("@pip-it-up/core");
|
|
45
45
|
|
|
46
46
|
// src/PipContext.tsx
|
|
@@ -49,15 +49,32 @@ var PipContext = (0, import_react.createContext)(null);
|
|
|
49
49
|
|
|
50
50
|
// src/PipPortal.tsx
|
|
51
51
|
var import_react_dom = require("react-dom");
|
|
52
|
+
|
|
53
|
+
// src/usePipContext.ts
|
|
54
|
+
var import_react2 = require("react");
|
|
55
|
+
function usePipContext() {
|
|
56
|
+
const context = (0, import_react2.useContext)(PipContext);
|
|
57
|
+
if (!context) {
|
|
58
|
+
throw new Error("usePipContext must be used within a <PipWrapper>");
|
|
59
|
+
}
|
|
60
|
+
return context;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/PipPortal.tsx
|
|
64
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
52
65
|
var PipPortal = ({ children, pipWindow }) => {
|
|
53
|
-
|
|
66
|
+
const context = usePipContext();
|
|
67
|
+
return (0, import_react_dom.createPortal)(
|
|
68
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PipContext.Provider, { value: { ...context, isInsidePip: true }, children }),
|
|
69
|
+
pipWindow.document.body
|
|
70
|
+
);
|
|
54
71
|
};
|
|
55
72
|
|
|
56
73
|
// src/PipWrapper.tsx
|
|
57
|
-
var
|
|
74
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
58
75
|
var emptyServerState = { isOpen: false, isSupported: false, pipWindow: null };
|
|
59
76
|
var getServerState = () => emptyServerState;
|
|
60
|
-
var PipWrapper = (0,
|
|
77
|
+
var PipWrapper = (0, import_react3.forwardRef)((props, ref) => {
|
|
61
78
|
const {
|
|
62
79
|
open: controlledOpen,
|
|
63
80
|
defaultOpen = false,
|
|
@@ -65,35 +82,59 @@ var PipWrapper = (0, import_react2.forwardRef)((props, ref) => {
|
|
|
65
82
|
as: Component = "div",
|
|
66
83
|
originAs: OriginComponent = "div",
|
|
67
84
|
children,
|
|
85
|
+
placeholder,
|
|
86
|
+
placeholderClassName,
|
|
68
87
|
...coreOptions
|
|
69
88
|
} = props;
|
|
70
|
-
const contentRef = (0,
|
|
71
|
-
const originRef = (0,
|
|
72
|
-
const instanceRef = (0,
|
|
73
|
-
(0,
|
|
89
|
+
const contentRef = (0, import_react3.useRef)(null);
|
|
90
|
+
const originRef = (0, import_react3.useRef)(null);
|
|
91
|
+
const instanceRef = (0, import_react3.useRef)(null);
|
|
92
|
+
(0, import_react3.useImperativeHandle)(ref, () => originRef.current);
|
|
74
93
|
if (!instanceRef.current) {
|
|
75
|
-
|
|
94
|
+
const { id: _id, mode: _mode, ...factoryOptions } = coreOptions;
|
|
95
|
+
if (_mode === "clone") {
|
|
96
|
+
console.warn(
|
|
97
|
+
'[PipWrapper] mode="clone" is not supported in the React package. PipWrapper always uses portal mode internally. For clone mode, use the vanilla createPip() API directly. Falling back to portal mode.'
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
instanceRef.current = (0, import_core.createPip)({ ...factoryOptions, mode: "portal" });
|
|
76
101
|
}
|
|
77
102
|
const instance = instanceRef.current;
|
|
78
|
-
(0,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
103
|
+
(0, import_react3.useEffect)(() => {
|
|
104
|
+
if (coreOptions.id) {
|
|
105
|
+
(0, import_core.registerPip)(coreOptions.id, instance);
|
|
106
|
+
return () => {
|
|
107
|
+
(0, import_core.unregisterPip)(coreOptions.id);
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}, [coreOptions.id, instance]);
|
|
111
|
+
(0, import_react3.useEffect)(() => {
|
|
85
112
|
return () => {
|
|
86
|
-
|
|
113
|
+
if (instanceRef.current) {
|
|
114
|
+
instanceRef.current.destroy();
|
|
115
|
+
}
|
|
87
116
|
};
|
|
88
|
-
}, [
|
|
89
|
-
const state = (0,
|
|
117
|
+
}, []);
|
|
118
|
+
const state = (0, import_react3.useSyncExternalStore)(
|
|
90
119
|
instance.subscribe,
|
|
91
120
|
instance.getState,
|
|
92
121
|
getServerState
|
|
93
122
|
);
|
|
123
|
+
(0, import_react3.useLayoutEffect)(() => {
|
|
124
|
+
console.log("[PipWrapper] instance:", instance);
|
|
125
|
+
console.log("[PipWrapper] instance.setDefaultElements:", typeof instance.setDefaultElements);
|
|
126
|
+
if (typeof instance.setDefaultElements === "function") {
|
|
127
|
+
instance.setDefaultElements({
|
|
128
|
+
contentEl: contentRef.current || void 0,
|
|
129
|
+
originEl: originRef.current || void 0
|
|
130
|
+
});
|
|
131
|
+
} else {
|
|
132
|
+
console.error("[PipWrapper] setDefaultElements is MISSING on instance!");
|
|
133
|
+
}
|
|
134
|
+
}, [instance, state.isOpen]);
|
|
94
135
|
const isControlled = controlledOpen !== void 0;
|
|
95
|
-
const prevOpenRef = (0,
|
|
96
|
-
(0,
|
|
136
|
+
const prevOpenRef = (0, import_react3.useRef)(state.isOpen);
|
|
137
|
+
(0, import_react3.useEffect)(() => {
|
|
97
138
|
if (state.isOpen !== prevOpenRef.current) {
|
|
98
139
|
if (onOpenChange) {
|
|
99
140
|
onOpenChange(state.isOpen);
|
|
@@ -101,48 +142,140 @@ var PipWrapper = (0, import_react2.forwardRef)((props, ref) => {
|
|
|
101
142
|
prevOpenRef.current = state.isOpen;
|
|
102
143
|
}
|
|
103
144
|
}, [state.isOpen, onOpenChange]);
|
|
104
|
-
(0,
|
|
145
|
+
const prevControlledOpenRef = (0, import_react3.useRef)(false);
|
|
146
|
+
(0, import_react3.useEffect)(() => {
|
|
105
147
|
if (isControlled) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
148
|
+
const changedToOpen = controlledOpen && !prevControlledOpenRef.current;
|
|
149
|
+
const changedToClosed = !controlledOpen && prevControlledOpenRef.current;
|
|
150
|
+
if (changedToOpen && !state.isOpen) {
|
|
151
|
+
if (contentRef.current) {
|
|
152
|
+
const rect = contentRef.current.getBoundingClientRect();
|
|
153
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
154
|
+
lastSizeRef.current = { width: rect.width, height: rect.height };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
instance.open().catch((err) => {
|
|
158
|
+
if (err.name === "NotAllowedError") {
|
|
159
|
+
console.warn('[PipWrapper] PiP window opening blocked: requires user activation. Ensure the "open" prop is changed within a user gesture handler.');
|
|
160
|
+
} else {
|
|
161
|
+
console.error("[PipWrapper] Failed to open PiP window", err);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
} else if (changedToClosed && state.isOpen) {
|
|
109
165
|
instance.close();
|
|
110
166
|
}
|
|
167
|
+
prevControlledOpenRef.current = controlledOpen;
|
|
111
168
|
}
|
|
112
169
|
}, [controlledOpen, isControlled, state.isOpen, instance]);
|
|
113
|
-
const defaultOpenHandled = (0,
|
|
114
|
-
(0,
|
|
170
|
+
const defaultOpenHandled = (0, import_react3.useRef)(false);
|
|
171
|
+
(0, import_react3.useEffect)(() => {
|
|
115
172
|
if (!isControlled && defaultOpen && !defaultOpenHandled.current) {
|
|
116
173
|
defaultOpenHandled.current = true;
|
|
117
|
-
instance.open(
|
|
174
|
+
instance.open().catch((err) => {
|
|
175
|
+
if (err.name === "NotAllowedError") {
|
|
176
|
+
console.warn("[PipWrapper] PiP window defaultOpen blocked: requires user activation.");
|
|
177
|
+
} else {
|
|
178
|
+
console.error("[PipWrapper] Failed to open PiP window via defaultOpen", err);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
118
181
|
}
|
|
119
182
|
}, [defaultOpen, isControlled, instance]);
|
|
120
|
-
const mode = coreOptions.mode || "move";
|
|
121
|
-
|
|
183
|
+
const mode = coreOptions.mode === "clone" ? "move" : coreOptions.mode || "move";
|
|
184
|
+
const defaultPlaceholder = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { padding: "12px", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", backgroundColor: "transparent", border: "1px dashed color-mix(in srgb, currentColor 30%, #ccc)", borderRadius: "inherit", width: "100%", height: "100%", boxSizing: "border-box", overflow: "hidden" }, children: [
|
|
185
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { marginBottom: "4px", fontSize: "0.875rem", fontWeight: 500, opacity: 0.6, textAlign: "center" }, children: "\u{1F4FA} In PiP" }),
|
|
186
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { onClick: () => instance.close(), style: { fontSize: "0.75rem", padding: "4px 8px", cursor: "pointer", borderRadius: "4px", border: "1px solid currentColor", background: "transparent", opacity: 0.6 }, children: "Restore" })
|
|
187
|
+
] });
|
|
188
|
+
const placeholderContent = placeholder !== void 0 ? placeholder : defaultPlaceholder;
|
|
189
|
+
const lastSizeRef = (0, import_react3.useRef)({ width: 0, height: 0 });
|
|
190
|
+
(0, import_react3.useEffect)(() => {
|
|
191
|
+
if (!contentRef.current || state.isOpen) return;
|
|
192
|
+
const observer = new ResizeObserver((entries) => {
|
|
193
|
+
for (const entry of entries) {
|
|
194
|
+
if (entry.contentRect.width > 0 && entry.contentRect.height > 0) {
|
|
195
|
+
lastSizeRef.current = {
|
|
196
|
+
width: entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width,
|
|
197
|
+
height: entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
observer.observe(contentRef.current);
|
|
203
|
+
return () => observer.disconnect();
|
|
204
|
+
}, [state.isOpen]);
|
|
205
|
+
const prevIsOpenRef = (0, import_react3.useRef)(state.isOpen);
|
|
206
|
+
(0, import_react3.useLayoutEffect)(() => {
|
|
207
|
+
if (prevIsOpenRef.current && !state.isOpen && contentRef.current) {
|
|
208
|
+
const rect = contentRef.current.getBoundingClientRect();
|
|
209
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
210
|
+
lastSizeRef.current = { width: rect.width, height: rect.height };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
prevIsOpenRef.current = state.isOpen;
|
|
214
|
+
}, [state.isOpen]);
|
|
215
|
+
(0, import_react3.useLayoutEffect)(() => {
|
|
216
|
+
if ((mode === "move" || mode === "portal") && state.isOpen && coreOptions.reserveSpace !== false && originRef.current) {
|
|
217
|
+
const { width, height } = lastSizeRef.current;
|
|
218
|
+
const origin = originRef.current;
|
|
219
|
+
if (width > 0 && height > 0) {
|
|
220
|
+
origin.style.minWidth = `${width}px`;
|
|
221
|
+
origin.style.minHeight = `${height}px`;
|
|
222
|
+
origin.style.width = `${width}px`;
|
|
223
|
+
origin.style.height = `${height}px`;
|
|
224
|
+
origin.style.display = "inline-block";
|
|
225
|
+
origin.style.verticalAlign = "top";
|
|
226
|
+
return () => {
|
|
227
|
+
origin.style.minWidth = "";
|
|
228
|
+
origin.style.minHeight = "";
|
|
229
|
+
origin.style.width = "";
|
|
230
|
+
origin.style.height = "";
|
|
231
|
+
origin.style.display = "";
|
|
232
|
+
origin.style.verticalAlign = "";
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}, [mode, state.isOpen, coreOptions.reserveSpace, instance]);
|
|
237
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PipContext.Provider, { value: { instance, state, isInsidePip: false }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(OriginComponent, { ref: originRef, style: { display: "contents" }, children: (mode === "portal" || mode === "move") && state.isOpen && state.pipWindow ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
238
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
239
|
+
"div",
|
|
240
|
+
{
|
|
241
|
+
className: placeholderClassName,
|
|
242
|
+
style: {
|
|
243
|
+
width: coreOptions.width ? `${coreOptions.width}px` : "100%",
|
|
244
|
+
height: lastSizeRef.current.height ? `${lastSizeRef.current.height}px` : "auto",
|
|
245
|
+
display: "inline-block",
|
|
246
|
+
verticalAlign: "top",
|
|
247
|
+
boxSizing: "border-box"
|
|
248
|
+
},
|
|
249
|
+
children: placeholderContent
|
|
250
|
+
},
|
|
251
|
+
"placeholder"
|
|
252
|
+
),
|
|
253
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PipPortal, { pipWindow: state.pipWindow, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Component, { ref: contentRef, children }) })
|
|
254
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Component, { ref: contentRef, children }, "content") }) });
|
|
122
255
|
});
|
|
123
256
|
PipWrapper.displayName = "PipWrapper";
|
|
124
257
|
|
|
125
258
|
// src/PipTrigger.tsx
|
|
126
|
-
var
|
|
259
|
+
var import_react6 = __toESM(require("react"));
|
|
127
260
|
var import_core3 = require("@pip-it-up/core");
|
|
128
261
|
|
|
129
262
|
// src/useIsPipSupported.ts
|
|
130
|
-
var
|
|
263
|
+
var import_react4 = require("react");
|
|
131
264
|
var import_core2 = require("@pip-it-up/core");
|
|
132
265
|
var useIsPipSupported = () => {
|
|
133
|
-
const [supported, setSupported] = (0,
|
|
134
|
-
(0,
|
|
266
|
+
const [supported, setSupported] = (0, import_react4.useState)(false);
|
|
267
|
+
(0, import_react4.useEffect)(() => {
|
|
135
268
|
setSupported((0, import_core2.isSupported)());
|
|
136
269
|
}, []);
|
|
137
270
|
return supported;
|
|
138
271
|
};
|
|
139
272
|
|
|
140
273
|
// src/Slot.tsx
|
|
141
|
-
var
|
|
142
|
-
var Slot = (0,
|
|
274
|
+
var import_react5 = __toESM(require("react"));
|
|
275
|
+
var Slot = (0, import_react5.forwardRef)((props, ref) => {
|
|
143
276
|
const { children, ...slotProps } = props;
|
|
144
|
-
if (
|
|
145
|
-
return
|
|
277
|
+
if (import_react5.default.isValidElement(children)) {
|
|
278
|
+
return import_react5.default.cloneElement(children, {
|
|
146
279
|
...slotProps,
|
|
147
280
|
...children.props,
|
|
148
281
|
style: {
|
|
@@ -153,26 +286,26 @@ var Slot = (0, import_react4.forwardRef)((props, ref) => {
|
|
|
153
286
|
ref: (node) => {
|
|
154
287
|
if (typeof ref === "function") ref(node);
|
|
155
288
|
else if (ref) ref.current = node;
|
|
156
|
-
const childRef = children.ref;
|
|
289
|
+
const childRef = children.props.ref || children.ref;
|
|
157
290
|
if (typeof childRef === "function") childRef(node);
|
|
158
291
|
else if (childRef) childRef.current = node;
|
|
159
292
|
}
|
|
160
293
|
});
|
|
161
294
|
}
|
|
162
|
-
if (
|
|
163
|
-
|
|
295
|
+
if (import_react5.default.Children.count(children) > 1) {
|
|
296
|
+
throw new Error("PipTrigger asChild expects a single React element child");
|
|
164
297
|
}
|
|
165
298
|
return null;
|
|
166
299
|
});
|
|
167
300
|
Slot.displayName = "Slot";
|
|
168
301
|
|
|
169
302
|
// src/PipTrigger.tsx
|
|
170
|
-
var
|
|
303
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
171
304
|
var emptySubscribe = () => () => {
|
|
172
305
|
};
|
|
173
306
|
var emptyServerState2 = { isOpen: false, isSupported: false, pipWindow: null };
|
|
174
307
|
var emptyGetState = () => emptyServerState2;
|
|
175
|
-
var PipTrigger = (0,
|
|
308
|
+
var PipTrigger = (0, import_react6.forwardRef)((props, ref) => {
|
|
176
309
|
const {
|
|
177
310
|
pipId,
|
|
178
311
|
asChild,
|
|
@@ -181,83 +314,116 @@ var PipTrigger = (0, import_react5.forwardRef)((props, ref) => {
|
|
|
181
314
|
renderOpen,
|
|
182
315
|
renderClose,
|
|
183
316
|
renderUnsupported = null,
|
|
317
|
+
hideInPip = true,
|
|
184
318
|
onClick,
|
|
185
319
|
children,
|
|
186
320
|
...rest
|
|
187
321
|
} = props;
|
|
188
322
|
const isSupported2 = useIsPipSupported();
|
|
189
|
-
const context = (0,
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const unsub = (0, import_core3.subscribeRegistry)(pipId, () => {
|
|
195
|
-
setRegistryInstance((0, import_core3.getPip)(pipId));
|
|
196
|
-
});
|
|
197
|
-
return unsub;
|
|
198
|
-
}
|
|
323
|
+
const context = (0, import_react6.useContext)(PipContext);
|
|
324
|
+
const registrySubscribe = import_react6.default.useCallback((callback) => {
|
|
325
|
+
if (!pipId) return () => {
|
|
326
|
+
};
|
|
327
|
+
return (0, import_core3.subscribeRegistry)(pipId, callback);
|
|
199
328
|
}, [pipId]);
|
|
329
|
+
const getRegistrySnapshot = import_react6.default.useCallback(() => {
|
|
330
|
+
return pipId ? (0, import_core3.getPip)(pipId) : null;
|
|
331
|
+
}, [pipId]);
|
|
332
|
+
const registryInstance = (0, import_react6.useSyncExternalStore)(
|
|
333
|
+
registrySubscribe,
|
|
334
|
+
getRegistrySnapshot,
|
|
335
|
+
() => null
|
|
336
|
+
);
|
|
200
337
|
const instance = pipId ? registryInstance : context?.instance;
|
|
201
338
|
const subscribe = instance?.subscribe || emptySubscribe;
|
|
202
339
|
const getState = instance?.getState || emptyGetState;
|
|
203
|
-
const state = (0,
|
|
340
|
+
const state = (0, import_react6.useSyncExternalStore)(
|
|
204
341
|
subscribe,
|
|
205
342
|
getState,
|
|
206
343
|
emptyGetState
|
|
207
344
|
);
|
|
208
|
-
const [mounted, setMounted] = (0,
|
|
209
|
-
(0,
|
|
210
|
-
if (!instance && pipId) {
|
|
211
|
-
|
|
345
|
+
const [mounted, setMounted] = (0, import_react6.useState)(false);
|
|
346
|
+
(0, import_react6.useEffect)(() => setMounted(true), []);
|
|
347
|
+
if (!instance && pipId && mounted) {
|
|
348
|
+
const Comp2 = asChild ? Slot : "button";
|
|
349
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
350
|
+
Comp2,
|
|
351
|
+
{
|
|
352
|
+
ref,
|
|
353
|
+
disabled: true,
|
|
354
|
+
style: { opacity: 0.5, cursor: "not-allowed" },
|
|
355
|
+
...asChild ? {} : { type: "button" },
|
|
356
|
+
...rest,
|
|
357
|
+
children: asChild ? children : children ?? renderOpen ?? openLabel
|
|
358
|
+
}
|
|
359
|
+
);
|
|
212
360
|
}
|
|
213
361
|
if (!mounted) {
|
|
214
362
|
return null;
|
|
215
363
|
}
|
|
216
364
|
if (!isSupported2) {
|
|
217
365
|
if (renderUnsupported === null) return null;
|
|
218
|
-
return /* @__PURE__ */ (0,
|
|
366
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: renderUnsupported });
|
|
219
367
|
}
|
|
220
|
-
const handleClick =
|
|
368
|
+
const handleClick = (e) => {
|
|
221
369
|
if (onClick) onClick(e);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
370
|
+
const target = instance ?? context?.instance;
|
|
371
|
+
target?.toggle();
|
|
225
372
|
};
|
|
226
373
|
const isOpen = state.isOpen;
|
|
374
|
+
if (hideInPip && isOpen && !!context) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
227
377
|
const content = isOpen ? renderClose ?? closeLabel : renderOpen ?? openLabel;
|
|
228
378
|
const Comp = asChild ? Slot : "button";
|
|
229
|
-
return /* @__PURE__ */ (0,
|
|
379
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
230
380
|
Comp,
|
|
231
381
|
{
|
|
232
382
|
ref,
|
|
233
383
|
onClick: handleClick,
|
|
234
384
|
...asChild ? {} : { type: "button" },
|
|
235
385
|
...rest,
|
|
236
|
-
children: asChild ? children : content
|
|
386
|
+
children: asChild ? children : children ?? content
|
|
237
387
|
}
|
|
238
388
|
);
|
|
239
389
|
});
|
|
240
390
|
PipTrigger.displayName = "PipTrigger";
|
|
241
391
|
|
|
242
392
|
// src/usePip.ts
|
|
243
|
-
var
|
|
393
|
+
var import_react7 = require("react");
|
|
244
394
|
var import_core4 = require("@pip-it-up/core");
|
|
245
395
|
var emptyServerState3 = { isOpen: false, isSupported: false, pipWindow: null };
|
|
246
396
|
var emptyGetState2 = () => emptyServerState3;
|
|
247
397
|
function usePip(options = {}) {
|
|
248
|
-
const contentRef = (0,
|
|
249
|
-
const originRef = (0,
|
|
250
|
-
const instanceRef = (0,
|
|
398
|
+
const contentRef = (0, import_react7.useRef)(null);
|
|
399
|
+
const originRef = (0, import_react7.useRef)(null);
|
|
400
|
+
const instanceRef = (0, import_react7.useRef)(null);
|
|
251
401
|
if (!instanceRef.current) {
|
|
252
|
-
|
|
402
|
+
const { id: _id, ...factoryOptions } = options;
|
|
403
|
+
instanceRef.current = (0, import_core4.createPip)(factoryOptions);
|
|
253
404
|
}
|
|
254
405
|
const instance = instanceRef.current;
|
|
255
|
-
(0,
|
|
406
|
+
(0, import_react7.useEffect)(() => {
|
|
407
|
+
if (options.id) {
|
|
408
|
+
(0, import_core4.registerPip)(options.id, instance);
|
|
409
|
+
return () => (0, import_core4.unregisterPip)(options.id);
|
|
410
|
+
}
|
|
411
|
+
}, [options.id, instance]);
|
|
412
|
+
(0, import_react7.useEffect)(() => {
|
|
256
413
|
return () => {
|
|
257
414
|
instance.destroy();
|
|
415
|
+
instanceRef.current = null;
|
|
258
416
|
};
|
|
259
417
|
}, [instance]);
|
|
260
|
-
|
|
418
|
+
(0, import_react7.useLayoutEffect)(() => {
|
|
419
|
+
if (typeof instance.setDefaultElements === "function") {
|
|
420
|
+
instance.setDefaultElements({
|
|
421
|
+
contentEl: contentRef.current || void 0,
|
|
422
|
+
originEl: originRef.current || void 0
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}, [instance]);
|
|
426
|
+
const state = (0, import_react7.useSyncExternalStore)(
|
|
261
427
|
instance.subscribe,
|
|
262
428
|
instance.getState,
|
|
263
429
|
emptyGetState2
|
|
@@ -273,16 +439,6 @@ function usePip(options = {}) {
|
|
|
273
439
|
pipWindow: state.pipWindow
|
|
274
440
|
};
|
|
275
441
|
}
|
|
276
|
-
|
|
277
|
-
// src/usePipContext.ts
|
|
278
|
-
var import_react7 = require("react");
|
|
279
|
-
function usePipContext() {
|
|
280
|
-
const context = (0, import_react7.useContext)(PipContext);
|
|
281
|
-
if (!context) {
|
|
282
|
-
throw new Error("usePipContext must be used within a <PipWrapper>");
|
|
283
|
-
}
|
|
284
|
-
return context;
|
|
285
|
-
}
|
|
286
442
|
// Annotate the CommonJS export names for ESM import in node:
|
|
287
443
|
0 && (module.exports = {
|
|
288
444
|
PipContext,
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/PipWrapper.tsx
|
|
2
|
-
import { forwardRef, useEffect, useRef, useSyncExternalStore, useImperativeHandle } from "react";
|
|
3
|
-
import { createPip } from "@pip-it-up/core";
|
|
2
|
+
import { forwardRef, useEffect, useRef, useSyncExternalStore, useImperativeHandle, useLayoutEffect } from "react";
|
|
3
|
+
import { createPip, registerPip, unregisterPip } from "@pip-it-up/core";
|
|
4
4
|
|
|
5
5
|
// src/PipContext.tsx
|
|
6
6
|
import { createContext } from "react";
|
|
@@ -8,12 +8,29 @@ var PipContext = createContext(null);
|
|
|
8
8
|
|
|
9
9
|
// src/PipPortal.tsx
|
|
10
10
|
import { createPortal } from "react-dom";
|
|
11
|
+
|
|
12
|
+
// src/usePipContext.ts
|
|
13
|
+
import { useContext } from "react";
|
|
14
|
+
function usePipContext() {
|
|
15
|
+
const context = useContext(PipContext);
|
|
16
|
+
if (!context) {
|
|
17
|
+
throw new Error("usePipContext must be used within a <PipWrapper>");
|
|
18
|
+
}
|
|
19
|
+
return context;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/PipPortal.tsx
|
|
23
|
+
import { jsx } from "react/jsx-runtime";
|
|
11
24
|
var PipPortal = ({ children, pipWindow }) => {
|
|
12
|
-
|
|
25
|
+
const context = usePipContext();
|
|
26
|
+
return createPortal(
|
|
27
|
+
/* @__PURE__ */ jsx(PipContext.Provider, { value: { ...context, isInsidePip: true }, children }),
|
|
28
|
+
pipWindow.document.body
|
|
29
|
+
);
|
|
13
30
|
};
|
|
14
31
|
|
|
15
32
|
// src/PipWrapper.tsx
|
|
16
|
-
import { jsx } from "react/jsx-runtime";
|
|
33
|
+
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
17
34
|
var emptyServerState = { isOpen: false, isSupported: false, pipWindow: null };
|
|
18
35
|
var getServerState = () => emptyServerState;
|
|
19
36
|
var PipWrapper = forwardRef((props, ref) => {
|
|
@@ -24,6 +41,8 @@ var PipWrapper = forwardRef((props, ref) => {
|
|
|
24
41
|
as: Component = "div",
|
|
25
42
|
originAs: OriginComponent = "div",
|
|
26
43
|
children,
|
|
44
|
+
placeholder,
|
|
45
|
+
placeholderClassName,
|
|
27
46
|
...coreOptions
|
|
28
47
|
} = props;
|
|
29
48
|
const contentRef = useRef(null);
|
|
@@ -31,25 +50,47 @@ var PipWrapper = forwardRef((props, ref) => {
|
|
|
31
50
|
const instanceRef = useRef(null);
|
|
32
51
|
useImperativeHandle(ref, () => originRef.current);
|
|
33
52
|
if (!instanceRef.current) {
|
|
34
|
-
|
|
53
|
+
const { id: _id, mode: _mode, ...factoryOptions } = coreOptions;
|
|
54
|
+
if (_mode === "clone") {
|
|
55
|
+
console.warn(
|
|
56
|
+
'[PipWrapper] mode="clone" is not supported in the React package. PipWrapper always uses portal mode internally. For clone mode, use the vanilla createPip() API directly. Falling back to portal mode.'
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
instanceRef.current = createPip({ ...factoryOptions, mode: "portal" });
|
|
35
60
|
}
|
|
36
61
|
const instance = instanceRef.current;
|
|
37
62
|
useEffect(() => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
63
|
+
if (coreOptions.id) {
|
|
64
|
+
registerPip(coreOptions.id, instance);
|
|
65
|
+
return () => {
|
|
66
|
+
unregisterPip(coreOptions.id);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}, [coreOptions.id, instance]);
|
|
43
70
|
useEffect(() => {
|
|
44
71
|
return () => {
|
|
45
|
-
|
|
72
|
+
if (instanceRef.current) {
|
|
73
|
+
instanceRef.current.destroy();
|
|
74
|
+
}
|
|
46
75
|
};
|
|
47
|
-
}, [
|
|
76
|
+
}, []);
|
|
48
77
|
const state = useSyncExternalStore(
|
|
49
78
|
instance.subscribe,
|
|
50
79
|
instance.getState,
|
|
51
80
|
getServerState
|
|
52
81
|
);
|
|
82
|
+
useLayoutEffect(() => {
|
|
83
|
+
console.log("[PipWrapper] instance:", instance);
|
|
84
|
+
console.log("[PipWrapper] instance.setDefaultElements:", typeof instance.setDefaultElements);
|
|
85
|
+
if (typeof instance.setDefaultElements === "function") {
|
|
86
|
+
instance.setDefaultElements({
|
|
87
|
+
contentEl: contentRef.current || void 0,
|
|
88
|
+
originEl: originRef.current || void 0
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
console.error("[PipWrapper] setDefaultElements is MISSING on instance!");
|
|
92
|
+
}
|
|
93
|
+
}, [instance, state.isOpen]);
|
|
53
94
|
const isControlled = controlledOpen !== void 0;
|
|
54
95
|
const prevOpenRef = useRef(state.isOpen);
|
|
55
96
|
useEffect(() => {
|
|
@@ -60,29 +101,121 @@ var PipWrapper = forwardRef((props, ref) => {
|
|
|
60
101
|
prevOpenRef.current = state.isOpen;
|
|
61
102
|
}
|
|
62
103
|
}, [state.isOpen, onOpenChange]);
|
|
104
|
+
const prevControlledOpenRef = useRef(false);
|
|
63
105
|
useEffect(() => {
|
|
64
106
|
if (isControlled) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
107
|
+
const changedToOpen = controlledOpen && !prevControlledOpenRef.current;
|
|
108
|
+
const changedToClosed = !controlledOpen && prevControlledOpenRef.current;
|
|
109
|
+
if (changedToOpen && !state.isOpen) {
|
|
110
|
+
if (contentRef.current) {
|
|
111
|
+
const rect = contentRef.current.getBoundingClientRect();
|
|
112
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
113
|
+
lastSizeRef.current = { width: rect.width, height: rect.height };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
instance.open().catch((err) => {
|
|
117
|
+
if (err.name === "NotAllowedError") {
|
|
118
|
+
console.warn('[PipWrapper] PiP window opening blocked: requires user activation. Ensure the "open" prop is changed within a user gesture handler.');
|
|
119
|
+
} else {
|
|
120
|
+
console.error("[PipWrapper] Failed to open PiP window", err);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
} else if (changedToClosed && state.isOpen) {
|
|
68
124
|
instance.close();
|
|
69
125
|
}
|
|
126
|
+
prevControlledOpenRef.current = controlledOpen;
|
|
70
127
|
}
|
|
71
128
|
}, [controlledOpen, isControlled, state.isOpen, instance]);
|
|
72
129
|
const defaultOpenHandled = useRef(false);
|
|
73
130
|
useEffect(() => {
|
|
74
131
|
if (!isControlled && defaultOpen && !defaultOpenHandled.current) {
|
|
75
132
|
defaultOpenHandled.current = true;
|
|
76
|
-
instance.open(
|
|
133
|
+
instance.open().catch((err) => {
|
|
134
|
+
if (err.name === "NotAllowedError") {
|
|
135
|
+
console.warn("[PipWrapper] PiP window defaultOpen blocked: requires user activation.");
|
|
136
|
+
} else {
|
|
137
|
+
console.error("[PipWrapper] Failed to open PiP window via defaultOpen", err);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
77
140
|
}
|
|
78
141
|
}, [defaultOpen, isControlled, instance]);
|
|
79
|
-
const mode = coreOptions.mode || "move";
|
|
80
|
-
|
|
142
|
+
const mode = coreOptions.mode === "clone" ? "move" : coreOptions.mode || "move";
|
|
143
|
+
const defaultPlaceholder = /* @__PURE__ */ jsxs("div", { style: { padding: "12px", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", backgroundColor: "transparent", border: "1px dashed color-mix(in srgb, currentColor 30%, #ccc)", borderRadius: "inherit", width: "100%", height: "100%", boxSizing: "border-box", overflow: "hidden" }, children: [
|
|
144
|
+
/* @__PURE__ */ jsx2("div", { style: { marginBottom: "4px", fontSize: "0.875rem", fontWeight: 500, opacity: 0.6, textAlign: "center" }, children: "\u{1F4FA} In PiP" }),
|
|
145
|
+
/* @__PURE__ */ jsx2("button", { onClick: () => instance.close(), style: { fontSize: "0.75rem", padding: "4px 8px", cursor: "pointer", borderRadius: "4px", border: "1px solid currentColor", background: "transparent", opacity: 0.6 }, children: "Restore" })
|
|
146
|
+
] });
|
|
147
|
+
const placeholderContent = placeholder !== void 0 ? placeholder : defaultPlaceholder;
|
|
148
|
+
const lastSizeRef = useRef({ width: 0, height: 0 });
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (!contentRef.current || state.isOpen) return;
|
|
151
|
+
const observer = new ResizeObserver((entries) => {
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
if (entry.contentRect.width > 0 && entry.contentRect.height > 0) {
|
|
154
|
+
lastSizeRef.current = {
|
|
155
|
+
width: entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width,
|
|
156
|
+
height: entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
observer.observe(contentRef.current);
|
|
162
|
+
return () => observer.disconnect();
|
|
163
|
+
}, [state.isOpen]);
|
|
164
|
+
const prevIsOpenRef = useRef(state.isOpen);
|
|
165
|
+
useLayoutEffect(() => {
|
|
166
|
+
if (prevIsOpenRef.current && !state.isOpen && contentRef.current) {
|
|
167
|
+
const rect = contentRef.current.getBoundingClientRect();
|
|
168
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
169
|
+
lastSizeRef.current = { width: rect.width, height: rect.height };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
prevIsOpenRef.current = state.isOpen;
|
|
173
|
+
}, [state.isOpen]);
|
|
174
|
+
useLayoutEffect(() => {
|
|
175
|
+
if ((mode === "move" || mode === "portal") && state.isOpen && coreOptions.reserveSpace !== false && originRef.current) {
|
|
176
|
+
const { width, height } = lastSizeRef.current;
|
|
177
|
+
const origin = originRef.current;
|
|
178
|
+
if (width > 0 && height > 0) {
|
|
179
|
+
origin.style.minWidth = `${width}px`;
|
|
180
|
+
origin.style.minHeight = `${height}px`;
|
|
181
|
+
origin.style.width = `${width}px`;
|
|
182
|
+
origin.style.height = `${height}px`;
|
|
183
|
+
origin.style.display = "inline-block";
|
|
184
|
+
origin.style.verticalAlign = "top";
|
|
185
|
+
return () => {
|
|
186
|
+
origin.style.minWidth = "";
|
|
187
|
+
origin.style.minHeight = "";
|
|
188
|
+
origin.style.width = "";
|
|
189
|
+
origin.style.height = "";
|
|
190
|
+
origin.style.display = "";
|
|
191
|
+
origin.style.verticalAlign = "";
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}, [mode, state.isOpen, coreOptions.reserveSpace, instance]);
|
|
196
|
+
return /* @__PURE__ */ jsx2(PipContext.Provider, { value: { instance, state, isInsidePip: false }, children: /* @__PURE__ */ jsx2(OriginComponent, { ref: originRef, style: { display: "contents" }, children: (mode === "portal" || mode === "move") && state.isOpen && state.pipWindow ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
197
|
+
/* @__PURE__ */ jsx2(
|
|
198
|
+
"div",
|
|
199
|
+
{
|
|
200
|
+
className: placeholderClassName,
|
|
201
|
+
style: {
|
|
202
|
+
width: coreOptions.width ? `${coreOptions.width}px` : "100%",
|
|
203
|
+
height: lastSizeRef.current.height ? `${lastSizeRef.current.height}px` : "auto",
|
|
204
|
+
display: "inline-block",
|
|
205
|
+
verticalAlign: "top",
|
|
206
|
+
boxSizing: "border-box"
|
|
207
|
+
},
|
|
208
|
+
children: placeholderContent
|
|
209
|
+
},
|
|
210
|
+
"placeholder"
|
|
211
|
+
),
|
|
212
|
+
/* @__PURE__ */ jsx2(PipPortal, { pipWindow: state.pipWindow, children: /* @__PURE__ */ jsx2(Component, { ref: contentRef, children }) })
|
|
213
|
+
] }) : /* @__PURE__ */ jsx2(Component, { ref: contentRef, children }, "content") }) });
|
|
81
214
|
});
|
|
82
215
|
PipWrapper.displayName = "PipWrapper";
|
|
83
216
|
|
|
84
217
|
// src/PipTrigger.tsx
|
|
85
|
-
import { forwardRef as forwardRef3, useEffect as useEffect3, useState as useState2, useSyncExternalStore as useSyncExternalStore2, useContext } from "react";
|
|
218
|
+
import React3, { forwardRef as forwardRef3, useEffect as useEffect3, useState as useState2, useSyncExternalStore as useSyncExternalStore2, useContext as useContext2 } from "react";
|
|
86
219
|
import { getPip, subscribeRegistry } from "@pip-it-up/core";
|
|
87
220
|
|
|
88
221
|
// src/useIsPipSupported.ts
|
|
@@ -112,21 +245,21 @@ var Slot = forwardRef2((props, ref) => {
|
|
|
112
245
|
ref: (node) => {
|
|
113
246
|
if (typeof ref === "function") ref(node);
|
|
114
247
|
else if (ref) ref.current = node;
|
|
115
|
-
const childRef = children.ref;
|
|
248
|
+
const childRef = children.props.ref || children.ref;
|
|
116
249
|
if (typeof childRef === "function") childRef(node);
|
|
117
250
|
else if (childRef) childRef.current = node;
|
|
118
251
|
}
|
|
119
252
|
});
|
|
120
253
|
}
|
|
121
254
|
if (React2.Children.count(children) > 1) {
|
|
122
|
-
|
|
255
|
+
throw new Error("PipTrigger asChild expects a single React element child");
|
|
123
256
|
}
|
|
124
257
|
return null;
|
|
125
258
|
});
|
|
126
259
|
Slot.displayName = "Slot";
|
|
127
260
|
|
|
128
261
|
// src/PipTrigger.tsx
|
|
129
|
-
import { Fragment, jsx as
|
|
262
|
+
import { Fragment as Fragment2, jsx as jsx3 } from "react/jsx-runtime";
|
|
130
263
|
var emptySubscribe = () => () => {
|
|
131
264
|
};
|
|
132
265
|
var emptyServerState2 = { isOpen: false, isSupported: false, pipWindow: null };
|
|
@@ -140,22 +273,26 @@ var PipTrigger = forwardRef3((props, ref) => {
|
|
|
140
273
|
renderOpen,
|
|
141
274
|
renderClose,
|
|
142
275
|
renderUnsupported = null,
|
|
276
|
+
hideInPip = true,
|
|
143
277
|
onClick,
|
|
144
278
|
children,
|
|
145
279
|
...rest
|
|
146
280
|
} = props;
|
|
147
281
|
const isSupported2 = useIsPipSupported();
|
|
148
|
-
const context =
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const unsub = subscribeRegistry(pipId, () => {
|
|
154
|
-
setRegistryInstance(getPip(pipId));
|
|
155
|
-
});
|
|
156
|
-
return unsub;
|
|
157
|
-
}
|
|
282
|
+
const context = useContext2(PipContext);
|
|
283
|
+
const registrySubscribe = React3.useCallback((callback) => {
|
|
284
|
+
if (!pipId) return () => {
|
|
285
|
+
};
|
|
286
|
+
return subscribeRegistry(pipId, callback);
|
|
158
287
|
}, [pipId]);
|
|
288
|
+
const getRegistrySnapshot = React3.useCallback(() => {
|
|
289
|
+
return pipId ? getPip(pipId) : null;
|
|
290
|
+
}, [pipId]);
|
|
291
|
+
const registryInstance = useSyncExternalStore2(
|
|
292
|
+
registrySubscribe,
|
|
293
|
+
getRegistrySnapshot,
|
|
294
|
+
() => null
|
|
295
|
+
);
|
|
159
296
|
const instance = pipId ? registryInstance : context?.instance;
|
|
160
297
|
const subscribe = instance?.subscribe || emptySubscribe;
|
|
161
298
|
const getState = instance?.getState || emptyGetState;
|
|
@@ -166,41 +303,54 @@ var PipTrigger = forwardRef3((props, ref) => {
|
|
|
166
303
|
);
|
|
167
304
|
const [mounted, setMounted] = useState2(false);
|
|
168
305
|
useEffect3(() => setMounted(true), []);
|
|
169
|
-
if (!instance && pipId) {
|
|
170
|
-
|
|
306
|
+
if (!instance && pipId && mounted) {
|
|
307
|
+
const Comp2 = asChild ? Slot : "button";
|
|
308
|
+
return /* @__PURE__ */ jsx3(
|
|
309
|
+
Comp2,
|
|
310
|
+
{
|
|
311
|
+
ref,
|
|
312
|
+
disabled: true,
|
|
313
|
+
style: { opacity: 0.5, cursor: "not-allowed" },
|
|
314
|
+
...asChild ? {} : { type: "button" },
|
|
315
|
+
...rest,
|
|
316
|
+
children: asChild ? children : children ?? renderOpen ?? openLabel
|
|
317
|
+
}
|
|
318
|
+
);
|
|
171
319
|
}
|
|
172
320
|
if (!mounted) {
|
|
173
321
|
return null;
|
|
174
322
|
}
|
|
175
323
|
if (!isSupported2) {
|
|
176
324
|
if (renderUnsupported === null) return null;
|
|
177
|
-
return /* @__PURE__ */
|
|
325
|
+
return /* @__PURE__ */ jsx3(Fragment2, { children: renderUnsupported });
|
|
178
326
|
}
|
|
179
|
-
const handleClick =
|
|
327
|
+
const handleClick = (e) => {
|
|
180
328
|
if (onClick) onClick(e);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
329
|
+
const target = instance ?? context?.instance;
|
|
330
|
+
target?.toggle();
|
|
184
331
|
};
|
|
185
332
|
const isOpen = state.isOpen;
|
|
333
|
+
if (hideInPip && isOpen && !!context) {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
186
336
|
const content = isOpen ? renderClose ?? closeLabel : renderOpen ?? openLabel;
|
|
187
337
|
const Comp = asChild ? Slot : "button";
|
|
188
|
-
return /* @__PURE__ */
|
|
338
|
+
return /* @__PURE__ */ jsx3(
|
|
189
339
|
Comp,
|
|
190
340
|
{
|
|
191
341
|
ref,
|
|
192
342
|
onClick: handleClick,
|
|
193
343
|
...asChild ? {} : { type: "button" },
|
|
194
344
|
...rest,
|
|
195
|
-
children: asChild ? children : content
|
|
345
|
+
children: asChild ? children : children ?? content
|
|
196
346
|
}
|
|
197
347
|
);
|
|
198
348
|
});
|
|
199
349
|
PipTrigger.displayName = "PipTrigger";
|
|
200
350
|
|
|
201
351
|
// src/usePip.ts
|
|
202
|
-
import { useRef as useRef2, useEffect as useEffect4, useSyncExternalStore as useSyncExternalStore3 } from "react";
|
|
203
|
-
import { createPip as createPip2 } from "@pip-it-up/core";
|
|
352
|
+
import { useRef as useRef2, useEffect as useEffect4, useSyncExternalStore as useSyncExternalStore3, useLayoutEffect as useLayoutEffect2 } from "react";
|
|
353
|
+
import { createPip as createPip2, registerPip as registerPip2, unregisterPip as unregisterPip2 } from "@pip-it-up/core";
|
|
204
354
|
var emptyServerState3 = { isOpen: false, isSupported: false, pipWindow: null };
|
|
205
355
|
var emptyGetState2 = () => emptyServerState3;
|
|
206
356
|
function usePip(options = {}) {
|
|
@@ -208,14 +358,30 @@ function usePip(options = {}) {
|
|
|
208
358
|
const originRef = useRef2(null);
|
|
209
359
|
const instanceRef = useRef2(null);
|
|
210
360
|
if (!instanceRef.current) {
|
|
211
|
-
|
|
361
|
+
const { id: _id, ...factoryOptions } = options;
|
|
362
|
+
instanceRef.current = createPip2(factoryOptions);
|
|
212
363
|
}
|
|
213
364
|
const instance = instanceRef.current;
|
|
365
|
+
useEffect4(() => {
|
|
366
|
+
if (options.id) {
|
|
367
|
+
registerPip2(options.id, instance);
|
|
368
|
+
return () => unregisterPip2(options.id);
|
|
369
|
+
}
|
|
370
|
+
}, [options.id, instance]);
|
|
214
371
|
useEffect4(() => {
|
|
215
372
|
return () => {
|
|
216
373
|
instance.destroy();
|
|
374
|
+
instanceRef.current = null;
|
|
217
375
|
};
|
|
218
376
|
}, [instance]);
|
|
377
|
+
useLayoutEffect2(() => {
|
|
378
|
+
if (typeof instance.setDefaultElements === "function") {
|
|
379
|
+
instance.setDefaultElements({
|
|
380
|
+
contentEl: contentRef.current || void 0,
|
|
381
|
+
originEl: originRef.current || void 0
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}, [instance]);
|
|
219
385
|
const state = useSyncExternalStore3(
|
|
220
386
|
instance.subscribe,
|
|
221
387
|
instance.getState,
|
|
@@ -232,16 +398,6 @@ function usePip(options = {}) {
|
|
|
232
398
|
pipWindow: state.pipWindow
|
|
233
399
|
};
|
|
234
400
|
}
|
|
235
|
-
|
|
236
|
-
// src/usePipContext.ts
|
|
237
|
-
import { useContext as useContext2 } from "react";
|
|
238
|
-
function usePipContext() {
|
|
239
|
-
const context = useContext2(PipContext);
|
|
240
|
-
if (!context) {
|
|
241
|
-
throw new Error("usePipContext must be used within a <PipWrapper>");
|
|
242
|
-
}
|
|
243
|
-
return context;
|
|
244
|
-
}
|
|
245
401
|
export {
|
|
246
402
|
PipContext,
|
|
247
403
|
PipTrigger,
|
package/package.json
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pip-it-up/react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "React components and hooks for the Document Picture-in-Picture API — PipWrapper, PipTrigger, usePip, useIsPipSupported",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"picture-in-picture",
|
|
7
|
+
"pip",
|
|
8
|
+
"react-pip",
|
|
9
|
+
"document-picture-in-picture",
|
|
10
|
+
"react-picture-in-picture",
|
|
11
|
+
"floating-window",
|
|
12
|
+
"pip-window",
|
|
13
|
+
"react-hooks",
|
|
14
|
+
"react-components",
|
|
15
|
+
"document-pip"
|
|
16
|
+
],
|
|
4
17
|
"repository": {
|
|
5
18
|
"type": "git",
|
|
6
19
|
"url": "git+https://github.com/Shakya47/pip-it-up.git",
|
|
@@ -29,11 +42,11 @@
|
|
|
29
42
|
"dist"
|
|
30
43
|
],
|
|
31
44
|
"dependencies": {
|
|
32
|
-
"@pip-it-up/core": "0.1.
|
|
45
|
+
"@pip-it-up/core": "0.1.4"
|
|
33
46
|
},
|
|
34
47
|
"peerDependencies": {
|
|
35
|
-
"react": ">=
|
|
36
|
-
"react-dom": ">=
|
|
48
|
+
"react": ">=18",
|
|
49
|
+
"react-dom": ">=18"
|
|
37
50
|
},
|
|
38
51
|
"devDependencies": {
|
|
39
52
|
"@testing-library/jest-dom": "^6.9.1",
|