react-morpheus 0.1.4 → 0.1.7
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 +125 -23
- package/dist/index.d.ts +13 -8
- package/dist/index.js +555 -101
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -2,56 +2,158 @@
|
|
|
2
2
|
|
|
3
3
|
A controlled React component for morphing one UI state into another.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
React Morpheus exists for interfaces where a control should become the next
|
|
6
|
+
piece of UI instead of opening a detached popover. The component keeps the
|
|
7
|
+
source surface, destination surface, and overlay explicit. Your app still owns
|
|
8
|
+
state and content; Morpheus owns the measured transition between those two
|
|
9
|
+
surfaces.
|
|
10
|
+
|
|
11
|
+
## Demo
|
|
12
|
+
|
|
13
|
+
<!-- react-morpheus-demo:start -->
|
|
14
|
+
https://github.com/user-attachments/assets/f90e7096-f587-4793-8e80-476cf844569d
|
|
15
|
+
<!-- react-morpheus-demo:end -->
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
bun add react-morpheus
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm i react-morpheus
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
yarn add react-morpheus
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
Import the component and stylesheet, keep the open state in your app, and pass
|
|
34
|
+
both surfaces to Morpheus.
|
|
12
35
|
|
|
13
36
|
```tsx
|
|
14
|
-
import {
|
|
37
|
+
import { useState } from "react";
|
|
38
|
+
import {
|
|
39
|
+
Morpheus,
|
|
40
|
+
MorphAnchor,
|
|
41
|
+
morphSpringPresets,
|
|
42
|
+
} from "react-morpheus";
|
|
15
43
|
import "react-morpheus/style.css";
|
|
16
44
|
|
|
17
45
|
function Example() {
|
|
18
46
|
const [expanded, setExpanded] = useState(false);
|
|
19
47
|
const source = (
|
|
20
48
|
<button type="button" onClick={() => setExpanded(true)}>
|
|
21
|
-
Open
|
|
49
|
+
Open menu
|
|
22
50
|
</button>
|
|
23
51
|
);
|
|
24
52
|
|
|
25
53
|
return (
|
|
26
54
|
<Morpheus
|
|
27
|
-
direction="bottom"
|
|
28
55
|
anchor={MorphAnchor.TopMiddle}
|
|
29
56
|
expanded={expanded}
|
|
30
57
|
onClose={() => setExpanded(false)}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
expandedContent={<div>Expanded content</div>}
|
|
58
|
+
overlayColor="#0f172a"
|
|
59
|
+
overlayOpacity={0.18}
|
|
60
|
+
overlayBlur={3}
|
|
35
61
|
overlayZIndex={1200}
|
|
62
|
+
viewportPadding={16}
|
|
63
|
+
spring={morphSpringPresets.smooth}
|
|
64
|
+
collapsedContent={source}
|
|
65
|
+
expandedContent={
|
|
66
|
+
<section>
|
|
67
|
+
<h2>San Francisco</h2>
|
|
68
|
+
<p>Context, actions, and details live here.</p>
|
|
69
|
+
<button type="button" onClick={() => setExpanded(false)}>
|
|
70
|
+
Close
|
|
71
|
+
</button>
|
|
72
|
+
</section>
|
|
73
|
+
}
|
|
36
74
|
/>
|
|
37
75
|
);
|
|
38
76
|
}
|
|
39
77
|
```
|
|
40
78
|
|
|
41
|
-
`
|
|
42
|
-
|
|
43
|
-
|
|
79
|
+
`Morpheus` does not manage its own open state. Opening is the responsibility of
|
|
80
|
+
the source component's own click handler. Overlay clicks call `onClose`; update
|
|
81
|
+
your controlled state there. Use `beforeClose` and `afterClose` for work that
|
|
82
|
+
should run around the closing animation.
|
|
83
|
+
|
|
84
|
+
## Props
|
|
85
|
+
|
|
86
|
+
| Prop | Type | Required | Notes |
|
|
87
|
+
| --- | --- | --- | --- |
|
|
88
|
+
| `anchor` | `MorphAnchor` | Required | The edge or point that stays visually anchored. |
|
|
89
|
+
| `expanded` | `boolean` | Required | Controlled open state owned by your app. |
|
|
90
|
+
| `onClose` | `() => void` | Optional | Called when the overlay requests closing. Use it to update your controlled open state. |
|
|
91
|
+
| `beforeClose` | `() => void` | Optional | Called when the closing animation starts, after `expanded` has changed to `false`. Useful for close-intent side effects. |
|
|
92
|
+
| `afterClose` | `() => void` | Optional | Called after the closing animation completes. |
|
|
93
|
+
| `collapsedContent` | `ReactNode` | Required | The source surface Morpheus measures, renders, and uses as the opener. |
|
|
94
|
+
| `expandedContent` | `ReactNode` | Required | The destination surface Morpheus measures and animates into. |
|
|
95
|
+
| `className` | `string` | Optional | Classes applied to the root wrapper. Useful for block layout and responsive width constraints. |
|
|
96
|
+
| `shellClassName` | `string` | Optional | Classes applied directly to the animated shell, including when it is portalled. |
|
|
97
|
+
| `portalContainer` | `HTMLElement \| null` | Optional | Portal root for the expanded surface. Defaults to `document.body` after mount; pass `null` for legacy inline rendering. |
|
|
98
|
+
| `viewportPadding` | `number` | Optional | Minimum viewport gutter for the expanded shell. Defaults to `16`. |
|
|
99
|
+
| `overlayColor` | `string` | Optional | Backdrop color shown while expanded. |
|
|
100
|
+
| `overlayOpacity` | `number` | Optional | Backdrop opacity from `0` to `1`. |
|
|
101
|
+
| `overlayBlur` | `number` | Optional | Backdrop blur in pixels. |
|
|
102
|
+
| `overlayZIndex` | `number` | Optional | Stacking level for the expanded surface. Defaults to `1000`; the overlay renders one layer below it. |
|
|
103
|
+
| `spring` | `Transition` | Optional | Framer Motion transition. Pass your own transition or use `morphSpringPresets`. |
|
|
44
104
|
|
|
45
|
-
|
|
46
|
-
`overflow: hidden`, `overflow: clip`, or scroll containers. The expanded surface
|
|
47
|
-
is positioned from the local source element and can extend outside its original
|
|
48
|
-
container while animating.
|
|
105
|
+
## Pitfalls
|
|
49
106
|
|
|
50
|
-
|
|
107
|
+
### Responsiveness
|
|
51
108
|
|
|
52
|
-
|
|
109
|
+
Let the collapsed trigger keep its natural size. Morpheus reads that box before
|
|
110
|
+
animating to the expanded surface.
|
|
111
|
+
|
|
112
|
+
Put responsive width rules on `className`, such as viewport-bound max widths, so
|
|
113
|
+
the animated shell has stable constraints on narrow screens.
|
|
114
|
+
|
|
115
|
+
Pick an anchor that matches the source location. Top anchors feel right for
|
|
116
|
+
menus opening below a trigger; bottom anchors feel right for surfaces opening
|
|
117
|
+
upward; middle anchors feel better for centered inspectors or command surfaces.
|
|
118
|
+
|
|
119
|
+
### Parent Container Overflow
|
|
120
|
+
|
|
121
|
+
Expanded surfaces and their overlays portal to `document.body` by default, so
|
|
122
|
+
they can escape clipped, transformed, and independently stacked ancestors. The
|
|
123
|
+
collapsed source remains in its original layout position. Pass another
|
|
124
|
+
document-level `portalContainer` when needed, or `null` to opt into the legacy
|
|
125
|
+
inline behavior.
|
|
126
|
+
|
|
127
|
+
Targets are clamped to the viewport using `viewportPadding`. When their natural
|
|
128
|
+
size is larger than the available viewport, the shell stays bounded and becomes
|
|
129
|
+
scrollable after the opening animation settles.
|
|
130
|
+
|
|
131
|
+
### Stacking
|
|
132
|
+
|
|
133
|
+
`overlayZIndex` controls the portalled expanded surface. The overlay renders one
|
|
134
|
+
layer below it so apps can move the whole morphing stack above their own chrome.
|
|
135
|
+
Without this prop, Morpheus uses surface `1000` and overlay `999`.
|
|
136
|
+
|
|
137
|
+
## Development
|
|
138
|
+
|
|
139
|
+
`example.html` is a small local playground for trying Morpheus in a browser
|
|
140
|
+
without setting up an app shell. Use it to check the default interaction,
|
|
141
|
+
anchors and overlay behavior while changing the component.
|
|
142
|
+
|
|
143
|
+
### Scripts
|
|
144
|
+
|
|
145
|
+
- `bun run build` builds declarations, bundled ESM output, and CSS.
|
|
53
146
|
- `bun run check` runs TypeScript without emitting files.
|
|
54
147
|
- `bun run publish:npm` builds and publishes to npm.
|
|
55
148
|
- `bun run release:patch` bumps the patch version, builds, tags, and publishes.
|
|
56
149
|
- `bun run release:minor` bumps the minor version, builds, tags, and publishes.
|
|
57
150
|
- `bun run release:major` bumps the major version, builds, tags, and publishes.
|
|
151
|
+
|
|
152
|
+
## Credits
|
|
153
|
+
|
|
154
|
+
Inspired by Family Wallet and Benji Taylor's
|
|
155
|
+
[Family Values](https://benji.org/family-values) post.
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
Released under the [MIT License](https://github.com/shivekkhurana/react-morpheus/blob/master/LICENSE).
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ import * as react from 'react';
|
|
|
2
2
|
import { ReactNode, RefObject } from 'react';
|
|
3
3
|
import { Transition } from 'framer-motion';
|
|
4
4
|
|
|
5
|
-
type MorphDirection = "top" | "right" | "bottom" | "left";
|
|
6
5
|
declare enum MorphAnchor {
|
|
7
6
|
LeftTop = "left-top",
|
|
8
7
|
LeftMiddle = "left-middle",
|
|
@@ -18,6 +17,10 @@ type PanelSize = {
|
|
|
18
17
|
width: number;
|
|
19
18
|
height: number;
|
|
20
19
|
};
|
|
20
|
+
type PanelRect = PanelSize & {
|
|
21
|
+
left: number;
|
|
22
|
+
top: number;
|
|
23
|
+
};
|
|
21
24
|
type ContentOffset = {
|
|
22
25
|
x: number;
|
|
23
26
|
y: number;
|
|
@@ -49,8 +52,7 @@ type MorphMeasurements = {
|
|
|
49
52
|
};
|
|
50
53
|
type MorphMotionInput = {
|
|
51
54
|
expanded: boolean;
|
|
52
|
-
|
|
53
|
-
anchor?: MorphAnchor | undefined;
|
|
55
|
+
anchor: MorphAnchor;
|
|
54
56
|
spring: Transition;
|
|
55
57
|
animationEnabled: boolean;
|
|
56
58
|
collapsed: MorphMeasuredSurface;
|
|
@@ -74,8 +76,7 @@ type MorphMotionState = {
|
|
|
74
76
|
visualStyle: PanelVisualStyle;
|
|
75
77
|
};
|
|
76
78
|
type MorphProps = {
|
|
77
|
-
|
|
78
|
-
anchor?: MorphAnchor | undefined;
|
|
79
|
+
anchor: MorphAnchor;
|
|
79
80
|
expanded: boolean;
|
|
80
81
|
onClose?: (() => void) | undefined;
|
|
81
82
|
beforeClose?: (() => void) | undefined;
|
|
@@ -83,6 +84,9 @@ type MorphProps = {
|
|
|
83
84
|
collapsedContent: ReactNode;
|
|
84
85
|
expandedContent: ReactNode;
|
|
85
86
|
className?: string;
|
|
87
|
+
shellClassName?: string;
|
|
88
|
+
portalContainer?: HTMLElement | null;
|
|
89
|
+
viewportPadding?: number;
|
|
86
90
|
overlayColor?: string;
|
|
87
91
|
overlayOpacity?: number;
|
|
88
92
|
overlayBlur?: number;
|
|
@@ -114,9 +118,11 @@ type MorphShellProps = {
|
|
|
114
118
|
onMorphComplete: () => void;
|
|
115
119
|
onMorphStart: () => void;
|
|
116
120
|
children: ReactNode;
|
|
121
|
+
className?: string | undefined;
|
|
117
122
|
};
|
|
118
123
|
type MorphContentLayersProps = {
|
|
119
124
|
expanded: boolean;
|
|
125
|
+
settledState: "collapsed" | "expanded" | null;
|
|
120
126
|
collapsedContent: ReactNode;
|
|
121
127
|
expandedContent: ReactNode;
|
|
122
128
|
collapsedLayerSize: PanelSize;
|
|
@@ -135,9 +141,8 @@ type MorphLayerInteractivityProps = {
|
|
|
135
141
|
inert?: boolean;
|
|
136
142
|
};
|
|
137
143
|
|
|
138
|
-
declare function Morpheus(
|
|
144
|
+
declare function Morpheus(props: MorphProps): react.JSX.Element;
|
|
139
145
|
|
|
140
|
-
declare const defaultAnchorByDirection: Record<MorphDirection, MorphAnchor>;
|
|
141
146
|
declare const morphSpringPresets: {
|
|
142
147
|
balanced: {
|
|
143
148
|
type: "spring";
|
|
@@ -171,4 +176,4 @@ declare const morphSpringPresets: {
|
|
|
171
176
|
};
|
|
172
177
|
};
|
|
173
178
|
|
|
174
|
-
export { type ContentOffset, MorphAnchor, type MorphContentLayersProps, type
|
|
179
|
+
export { type ContentOffset, MorphAnchor, type MorphContentLayersProps, type MorphLayerInteractivityProps, type MorphMeasuredSurface, type MorphMeasurementNodesProps, type MorphMeasurements, type MorphMotionInput, type MorphMotionState, type MorphOverlayProps, type MorphProps, type MorphShellProps, type MorphSpringPreset, type MorphSurfaceSnapshot, Morpheus, type PanelPosition, type PanelRect, type PanelSize, type PanelVisualStyle, Morpheus as default, morphSpringPresets };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
// src/component.tsx
|
|
2
2
|
import { motion as motion2 } from "framer-motion";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useLayoutEffect as useLayoutEffect2,
|
|
9
|
+
useRef as useRef2,
|
|
10
|
+
useState as useState2
|
|
11
|
+
} from "react";
|
|
12
|
+
import { createPortal } from "react-dom";
|
|
4
13
|
|
|
5
14
|
// src/measurement.ts
|
|
6
15
|
import { useLayoutEffect, useRef, useState } from "react";
|
|
@@ -40,8 +49,14 @@ function getSurfaceVisualStyle(element) {
|
|
|
40
49
|
}
|
|
41
50
|
function measureSurface(element) {
|
|
42
51
|
const measuredElement = getMeasuredElement(element);
|
|
43
|
-
const width =
|
|
44
|
-
|
|
52
|
+
const width = Math.max(
|
|
53
|
+
measuredElement.offsetWidth,
|
|
54
|
+
measuredElement.scrollWidth
|
|
55
|
+
);
|
|
56
|
+
const height = Math.max(
|
|
57
|
+
measuredElement.offsetHeight,
|
|
58
|
+
measuredElement.scrollHeight
|
|
59
|
+
);
|
|
45
60
|
if (width === 0 || height === 0) {
|
|
46
61
|
return null;
|
|
47
62
|
}
|
|
@@ -115,12 +130,6 @@ var MorphAnchor = /* @__PURE__ */ ((MorphAnchor2) => {
|
|
|
115
130
|
})(MorphAnchor || {});
|
|
116
131
|
|
|
117
132
|
// src/motion.ts
|
|
118
|
-
var defaultAnchorByDirection = {
|
|
119
|
-
top: "bottom-middle" /* BottomMiddle */,
|
|
120
|
-
right: "left-middle" /* LeftMiddle */,
|
|
121
|
-
bottom: "top-middle" /* TopMiddle */,
|
|
122
|
-
left: "right-middle" /* RightMiddle */
|
|
123
|
-
};
|
|
124
133
|
var anchorTransformOrigins = {
|
|
125
134
|
["left-top" /* LeftTop */]: "left top",
|
|
126
135
|
["left-middle" /* LeftMiddle */]: "left center",
|
|
@@ -173,8 +182,6 @@ var sourceContentFade = {
|
|
|
173
182
|
duration: 0.12,
|
|
174
183
|
ease: "easeOut"
|
|
175
184
|
};
|
|
176
|
-
var sourceReturnDelay = 0.06;
|
|
177
|
-
var sourceGrowthRatio = 2;
|
|
178
185
|
var instantTransition = { duration: 0 };
|
|
179
186
|
var createSourceContentMotion = (spring) => ({
|
|
180
187
|
opacity: sourceContentFade,
|
|
@@ -191,6 +198,12 @@ var createTargetContentMotion = (spring) => ({
|
|
|
191
198
|
top: spring
|
|
192
199
|
});
|
|
193
200
|
var safeScale = (from, to) => to === 0 ? 1 : from / to;
|
|
201
|
+
function getAxisScale(fromSize, toSize) {
|
|
202
|
+
return {
|
|
203
|
+
x: safeScale(toSize.width, fromSize.width),
|
|
204
|
+
y: safeScale(toSize.height, fromSize.height)
|
|
205
|
+
};
|
|
206
|
+
}
|
|
194
207
|
function getAnchoredPanelPosition(anchor, collapsedSize, targetSize) {
|
|
195
208
|
const alignLeft = 0;
|
|
196
209
|
const alignCenterX = (collapsedSize.width - targetSize.width) / 2;
|
|
@@ -230,7 +243,6 @@ function getLayerInteractivityProps(interactive) {
|
|
|
230
243
|
}
|
|
231
244
|
function getMorphMotionState({
|
|
232
245
|
expanded,
|
|
233
|
-
direction,
|
|
234
246
|
anchor,
|
|
235
247
|
spring,
|
|
236
248
|
animationEnabled,
|
|
@@ -241,10 +253,9 @@ function getMorphMotionState({
|
|
|
241
253
|
const expandedLayerSize = expandedSurface.size;
|
|
242
254
|
const animatedSize = expanded ? expandedLayerSize : collapsedLayerSize;
|
|
243
255
|
const visualStyle = expanded ? expandedSurface.visualStyle : collapsed.visualStyle;
|
|
244
|
-
const panelAnchor = anchor ?? defaultAnchorByDirection[direction];
|
|
245
256
|
const sourcePanelPosition = { left: 0, top: 0 };
|
|
246
257
|
const targetPanelPosition = getAnchoredPanelPosition(
|
|
247
|
-
|
|
258
|
+
anchor,
|
|
248
259
|
collapsedLayerSize,
|
|
249
260
|
expandedLayerSize
|
|
250
261
|
);
|
|
@@ -254,25 +265,19 @@ function getMorphMotionState({
|
|
|
254
265
|
anchorPosition: expanded ? targetPanelPosition : sourcePanelPosition,
|
|
255
266
|
animatedSize,
|
|
256
267
|
collapsedLayerSize,
|
|
257
|
-
collapsedToExpandedScale:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
268
|
+
collapsedToExpandedScale: getAxisScale(
|
|
269
|
+
expandedLayerSize,
|
|
270
|
+
collapsedLayerSize
|
|
271
|
+
),
|
|
261
272
|
expandedLayerSize,
|
|
262
|
-
sourceGrowthScale:
|
|
263
|
-
|
|
264
|
-
y: sourceGrowthRatio
|
|
265
|
-
},
|
|
266
|
-
sourceOpacityTransition: {
|
|
267
|
-
...sourceContentFade,
|
|
268
|
-
delay: expanded ? 0 : sourceReturnDelay
|
|
269
|
-
},
|
|
273
|
+
sourceGrowthScale: getAxisScale(collapsedLayerSize, expandedLayerSize),
|
|
274
|
+
sourceOpacityTransition: { ...sourceContentFade },
|
|
270
275
|
sourcePosition: expanded ? invertPosition(targetPanelPosition) : sourcePanelPosition,
|
|
271
276
|
sourceTransition: createSourceContentMotion(activeSpring),
|
|
272
277
|
targetOpacityTransition: { ...contentFade, delay: expanded ? 0.03 : 0 },
|
|
273
278
|
targetPosition: expanded ? sourcePanelPosition : targetPanelPosition,
|
|
274
279
|
targetTransition: createTargetContentMotion(activeSpring),
|
|
275
|
-
transformOrigin: anchorTransformOrigins[
|
|
280
|
+
transformOrigin: anchorTransformOrigins[anchor],
|
|
276
281
|
visualStyle
|
|
277
282
|
};
|
|
278
283
|
}
|
|
@@ -324,22 +329,129 @@ function MorphOverlay({
|
|
|
324
329
|
) : null });
|
|
325
330
|
}
|
|
326
331
|
|
|
332
|
+
// src/placement.ts
|
|
333
|
+
var clamp = (value, minimum, maximum) => Math.min(Math.max(value, minimum), Math.max(minimum, maximum));
|
|
334
|
+
function getBoundedPanelSize(naturalSize, viewport, padding) {
|
|
335
|
+
const safePadding = Math.max(0, padding);
|
|
336
|
+
return {
|
|
337
|
+
width: Math.min(
|
|
338
|
+
naturalSize.width,
|
|
339
|
+
Math.max(1, viewport.width - safePadding * 2)
|
|
340
|
+
),
|
|
341
|
+
height: Math.min(
|
|
342
|
+
naturalSize.height,
|
|
343
|
+
Math.max(1, viewport.height - safePadding * 2)
|
|
344
|
+
)
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function getAnchoredTargetRect(anchor, source, targetSize, viewport, padding) {
|
|
348
|
+
const sourceCenterX = source.left + source.width / 2;
|
|
349
|
+
const sourceCenterY = source.top + source.height / 2;
|
|
350
|
+
let left = source.left;
|
|
351
|
+
let top = source.top;
|
|
352
|
+
switch (anchor) {
|
|
353
|
+
case "left-top" /* LeftTop */:
|
|
354
|
+
break;
|
|
355
|
+
case "left-middle" /* LeftMiddle */:
|
|
356
|
+
top = sourceCenterY - targetSize.height / 2;
|
|
357
|
+
break;
|
|
358
|
+
case "left-bottom" /* LeftBottom */:
|
|
359
|
+
top = source.top + source.height - targetSize.height;
|
|
360
|
+
break;
|
|
361
|
+
case "top-middle" /* TopMiddle */:
|
|
362
|
+
left = sourceCenterX - targetSize.width / 2;
|
|
363
|
+
break;
|
|
364
|
+
case "middle-middle" /* MiddleMiddle */:
|
|
365
|
+
left = sourceCenterX - targetSize.width / 2;
|
|
366
|
+
top = sourceCenterY - targetSize.height / 2;
|
|
367
|
+
break;
|
|
368
|
+
case "right-top" /* RightTop */:
|
|
369
|
+
left = source.left + source.width - targetSize.width;
|
|
370
|
+
break;
|
|
371
|
+
case "right-middle" /* RightMiddle */:
|
|
372
|
+
left = source.left + source.width - targetSize.width;
|
|
373
|
+
top = sourceCenterY - targetSize.height / 2;
|
|
374
|
+
break;
|
|
375
|
+
case "right-bottom" /* RightBottom */:
|
|
376
|
+
left = source.left + source.width - targetSize.width;
|
|
377
|
+
top = source.top + source.height - targetSize.height;
|
|
378
|
+
break;
|
|
379
|
+
case "bottom-middle" /* BottomMiddle */:
|
|
380
|
+
left = sourceCenterX - targetSize.width / 2;
|
|
381
|
+
top = source.top + source.height - targetSize.height;
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
const safePadding = Math.max(0, padding);
|
|
385
|
+
const minimumLeft = viewport.left + safePadding;
|
|
386
|
+
const minimumTop = viewport.top + safePadding;
|
|
387
|
+
const maximumLeft = viewport.left + viewport.width - safePadding - targetSize.width;
|
|
388
|
+
const maximumTop = viewport.top + viewport.height - safePadding - targetSize.height;
|
|
389
|
+
return {
|
|
390
|
+
left: clamp(left, minimumLeft, maximumLeft),
|
|
391
|
+
top: clamp(top, minimumTop, maximumTop),
|
|
392
|
+
...targetSize
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function getPortalLayerPositions(expanded, source, target) {
|
|
396
|
+
const sourceFromTarget = {
|
|
397
|
+
left: source.left - target.left,
|
|
398
|
+
top: source.top - target.top
|
|
399
|
+
};
|
|
400
|
+
const targetFromSource = {
|
|
401
|
+
left: target.left - source.left,
|
|
402
|
+
top: target.top - source.top
|
|
403
|
+
};
|
|
404
|
+
const origin = { left: 0, top: 0 };
|
|
405
|
+
return expanded ? {
|
|
406
|
+
sourcePosition: sourceFromTarget,
|
|
407
|
+
targetPosition: origin
|
|
408
|
+
} : {
|
|
409
|
+
sourcePosition: origin,
|
|
410
|
+
targetPosition: targetFromSource
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
327
414
|
// src/component.tsx
|
|
328
415
|
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
329
416
|
var liveSurfaceContentClassName = "rmorph:absolute";
|
|
417
|
+
var MorphMeasurementContext = createContext(false);
|
|
418
|
+
function mergeClassNames(...classNames) {
|
|
419
|
+
return classNames.filter(Boolean).join(" ");
|
|
420
|
+
}
|
|
421
|
+
function getViewportBounds() {
|
|
422
|
+
const viewport = window.visualViewport;
|
|
423
|
+
if (viewport) {
|
|
424
|
+
return {
|
|
425
|
+
left: viewport.offsetLeft,
|
|
426
|
+
top: viewport.offsetTop,
|
|
427
|
+
width: viewport.width,
|
|
428
|
+
height: viewport.height
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
left: 0,
|
|
433
|
+
top: 0,
|
|
434
|
+
width: document.documentElement.clientWidth,
|
|
435
|
+
height: document.documentElement.clientHeight
|
|
436
|
+
};
|
|
437
|
+
}
|
|
330
438
|
function MorphMeasurementNodes({
|
|
331
439
|
collapsedRef,
|
|
332
440
|
expandedRef,
|
|
333
441
|
collapsedContent,
|
|
334
442
|
expandedContent
|
|
335
443
|
}) {
|
|
336
|
-
|
|
444
|
+
const measurementStyle = {
|
|
445
|
+
overflow: "clip"
|
|
446
|
+
};
|
|
447
|
+
return /* @__PURE__ */ jsxs(MorphMeasurementContext.Provider, { value: true, children: [
|
|
337
448
|
/* @__PURE__ */ jsx2(
|
|
338
449
|
"div",
|
|
339
450
|
{
|
|
340
451
|
ref: collapsedRef,
|
|
341
|
-
|
|
452
|
+
inert: true,
|
|
342
453
|
className: "rmorph:invisible rmorph:absolute rmorph:inset-x-0 rmorph:top-0 rmorph:pointer-events-none",
|
|
454
|
+
style: measurementStyle,
|
|
343
455
|
children: collapsedContent
|
|
344
456
|
}
|
|
345
457
|
),
|
|
@@ -347,8 +459,9 @@ function MorphMeasurementNodes({
|
|
|
347
459
|
"div",
|
|
348
460
|
{
|
|
349
461
|
ref: expandedRef,
|
|
350
|
-
|
|
462
|
+
inert: true,
|
|
351
463
|
className: "rmorph:invisible rmorph:absolute rmorph:inset-x-0 rmorph:top-0 rmorph:pointer-events-none",
|
|
464
|
+
style: measurementStyle,
|
|
352
465
|
children: expandedContent
|
|
353
466
|
}
|
|
354
467
|
)
|
|
@@ -364,12 +477,13 @@ function MorphShell({
|
|
|
364
477
|
spring,
|
|
365
478
|
onMorphComplete,
|
|
366
479
|
onMorphStart,
|
|
367
|
-
children
|
|
480
|
+
children,
|
|
481
|
+
className
|
|
368
482
|
}) {
|
|
369
483
|
return /* @__PURE__ */ jsx2(
|
|
370
484
|
motion2.div,
|
|
371
485
|
{
|
|
372
|
-
className: "rmorph:absolute rmorph:shadow-xs",
|
|
486
|
+
className: mergeClassNames("rmorph:absolute rmorph:shadow-xs", className),
|
|
373
487
|
initial: false,
|
|
374
488
|
animate: {
|
|
375
489
|
width: animatedSize.width,
|
|
@@ -389,8 +503,54 @@ function MorphShell({
|
|
|
389
503
|
}
|
|
390
504
|
);
|
|
391
505
|
}
|
|
506
|
+
function PortalMorphShell({
|
|
507
|
+
expanded,
|
|
508
|
+
lifecyclePhase,
|
|
509
|
+
geometry,
|
|
510
|
+
collapsedVisualStyle,
|
|
511
|
+
expandedVisualStyle,
|
|
512
|
+
overflow,
|
|
513
|
+
zIndex,
|
|
514
|
+
spring,
|
|
515
|
+
onMorphComplete,
|
|
516
|
+
onMorphStart,
|
|
517
|
+
children,
|
|
518
|
+
className
|
|
519
|
+
}) {
|
|
520
|
+
const rect = expanded ? geometry.target : geometry.source;
|
|
521
|
+
const visualStyle = expanded ? expandedVisualStyle : collapsedVisualStyle;
|
|
522
|
+
return /* @__PURE__ */ jsx2(
|
|
523
|
+
motion2.div,
|
|
524
|
+
{
|
|
525
|
+
"data-morpheus-portal-shell": "",
|
|
526
|
+
"data-morpheus-phase": lifecyclePhase,
|
|
527
|
+
inert: lifecyclePhase === "handoff" || void 0,
|
|
528
|
+
className: mergeClassNames("rmorph:fixed rmorph:shadow-xs", className),
|
|
529
|
+
initial: false,
|
|
530
|
+
animate: {
|
|
531
|
+
left: rect.left,
|
|
532
|
+
top: rect.top,
|
|
533
|
+
width: rect.width,
|
|
534
|
+
height: rect.height,
|
|
535
|
+
backgroundColor: visualStyle.backgroundColor,
|
|
536
|
+
borderRadius: visualStyle.borderRadius
|
|
537
|
+
},
|
|
538
|
+
onAnimationComplete: onMorphComplete,
|
|
539
|
+
...onMorphStart ? { onAnimationStart: onMorphStart } : {},
|
|
540
|
+
style: {
|
|
541
|
+
overflow,
|
|
542
|
+
overscrollBehavior: "contain",
|
|
543
|
+
pointerEvents: lifecyclePhase === "handoff" ? "none" : void 0,
|
|
544
|
+
zIndex
|
|
545
|
+
},
|
|
546
|
+
transition: spring,
|
|
547
|
+
children
|
|
548
|
+
}
|
|
549
|
+
);
|
|
550
|
+
}
|
|
392
551
|
function MorphContentLayers({
|
|
393
552
|
expanded,
|
|
553
|
+
settledState,
|
|
394
554
|
collapsedContent,
|
|
395
555
|
expandedContent,
|
|
396
556
|
collapsedLayerSize,
|
|
@@ -423,6 +583,7 @@ function MorphContentLayers({
|
|
|
423
583
|
opacity: sourceOpacityTransition
|
|
424
584
|
},
|
|
425
585
|
style: {
|
|
586
|
+
display: expanded && settledState === "expanded" ? "none" : void 0,
|
|
426
587
|
width: collapsedLayerSize.width,
|
|
427
588
|
height: collapsedLayerSize.height,
|
|
428
589
|
pointerEvents: expanded ? "none" : "auto",
|
|
@@ -448,6 +609,7 @@ function MorphContentLayers({
|
|
|
448
609
|
opacity: targetOpacityTransition
|
|
449
610
|
},
|
|
450
611
|
style: {
|
|
612
|
+
display: !expanded && settledState === "collapsed" ? "none" : void 0,
|
|
451
613
|
width: expandedLayerSize.width,
|
|
452
614
|
height: expandedLayerSize.height,
|
|
453
615
|
pointerEvents: expanded ? "auto" : "none",
|
|
@@ -458,8 +620,26 @@ function MorphContentLayers({
|
|
|
458
620
|
)
|
|
459
621
|
] });
|
|
460
622
|
}
|
|
461
|
-
function
|
|
462
|
-
|
|
623
|
+
function MeasurementTreePlaceholder({
|
|
624
|
+
className,
|
|
625
|
+
collapsedContent
|
|
626
|
+
}) {
|
|
627
|
+
return /* @__PURE__ */ jsx2("div", { className, children: /* @__PURE__ */ jsx2("div", { className: "rmorph:relative rmorph:mx-auto rmorph:block rmorph:w-fit rmorph:align-top", children: /* @__PURE__ */ jsx2("div", { className: "rmorph:inline-block rmorph:align-top", children: collapsedContent }) }) });
|
|
628
|
+
}
|
|
629
|
+
function Morpheus(props) {
|
|
630
|
+
const insideMeasurementTree = useContext(MorphMeasurementContext);
|
|
631
|
+
if (insideMeasurementTree) {
|
|
632
|
+
return /* @__PURE__ */ jsx2(
|
|
633
|
+
MeasurementTreePlaceholder,
|
|
634
|
+
{
|
|
635
|
+
className: props.className ?? "rmorph:relative rmorph:inline-block rmorph:align-top",
|
|
636
|
+
collapsedContent: props.collapsedContent
|
|
637
|
+
}
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
return /* @__PURE__ */ jsx2(MorpheusRoot, { ...props });
|
|
641
|
+
}
|
|
642
|
+
function MorpheusRoot({
|
|
463
643
|
anchor,
|
|
464
644
|
expanded,
|
|
465
645
|
onClose,
|
|
@@ -468,6 +648,9 @@ function Morpheus({
|
|
|
468
648
|
collapsedContent,
|
|
469
649
|
expandedContent,
|
|
470
650
|
className = "rmorph:relative rmorph:inline-block rmorph:align-top",
|
|
651
|
+
shellClassName,
|
|
652
|
+
portalContainer,
|
|
653
|
+
viewportPadding = 16,
|
|
471
654
|
overlayColor = "#05070a",
|
|
472
655
|
overlayOpacity = 0.54,
|
|
473
656
|
overlayBlur = 0,
|
|
@@ -475,10 +658,51 @@ function Morpheus({
|
|
|
475
658
|
spring = defaultPanelSpring
|
|
476
659
|
}) {
|
|
477
660
|
const measurements = useMorphMeasurements();
|
|
661
|
+
const anchorRef = useRef2(null);
|
|
662
|
+
const [defaultPortalContainer, setDefaultPortalContainer] = useState2(null);
|
|
478
663
|
const [animationEnabled, setAnimationEnabled] = useState2(false);
|
|
479
664
|
const [shellOverflowVisible, setShellOverflowVisible] = useState2(false);
|
|
665
|
+
const [inlineSettledState, setInlineSettledState] = useState2(expanded ? "expanded" : "collapsed");
|
|
666
|
+
const [portalPhase, setPortalPhase] = useState2(
|
|
667
|
+
expanded ? "expanded" : "collapsed"
|
|
668
|
+
);
|
|
669
|
+
const [portalVisualExpanded, setPortalVisualExpanded] = useState2(expanded);
|
|
670
|
+
const [portalGeometry, setPortalGeometry] = useState2(
|
|
671
|
+
null
|
|
672
|
+
);
|
|
480
673
|
const hasOpenedRef = useRef2(expanded);
|
|
481
674
|
const beforeCloseCalledRef = useRef2(false);
|
|
675
|
+
const beforeCloseRef = useRef2(beforeClose);
|
|
676
|
+
const afterCloseRef = useRef2(afterClose);
|
|
677
|
+
const previousExpandedRef = useRef2(expanded);
|
|
678
|
+
const portalStartFrameRef = useRef2(null);
|
|
679
|
+
const handoffFirstFrameRef = useRef2(null);
|
|
680
|
+
const handoffSecondFrameRef = useRef2(null);
|
|
681
|
+
const portalPhaseRef = useRef2(portalPhase);
|
|
682
|
+
const expandedRef = useRef2(expanded);
|
|
683
|
+
portalPhaseRef.current = portalPhase;
|
|
684
|
+
expandedRef.current = expanded;
|
|
685
|
+
beforeCloseRef.current = beforeClose;
|
|
686
|
+
afterCloseRef.current = afterClose;
|
|
687
|
+
const cancelPortalFrames = useCallback(() => {
|
|
688
|
+
if (portalStartFrameRef.current !== null) {
|
|
689
|
+
cancelAnimationFrame(portalStartFrameRef.current);
|
|
690
|
+
portalStartFrameRef.current = null;
|
|
691
|
+
}
|
|
692
|
+
if (handoffFirstFrameRef.current !== null) {
|
|
693
|
+
cancelAnimationFrame(handoffFirstFrameRef.current);
|
|
694
|
+
handoffFirstFrameRef.current = null;
|
|
695
|
+
}
|
|
696
|
+
if (handoffSecondFrameRef.current !== null) {
|
|
697
|
+
cancelAnimationFrame(handoffSecondFrameRef.current);
|
|
698
|
+
handoffSecondFrameRef.current = null;
|
|
699
|
+
}
|
|
700
|
+
}, []);
|
|
701
|
+
useEffect(() => {
|
|
702
|
+
if (portalContainer === void 0) {
|
|
703
|
+
setDefaultPortalContainer(document.body);
|
|
704
|
+
}
|
|
705
|
+
}, [portalContainer]);
|
|
482
706
|
useEffect(() => {
|
|
483
707
|
if (!measurements.ready) {
|
|
484
708
|
return;
|
|
@@ -486,109 +710,339 @@ function Morpheus({
|
|
|
486
710
|
const frame = requestAnimationFrame(() => setAnimationEnabled(true));
|
|
487
711
|
return () => cancelAnimationFrame(frame);
|
|
488
712
|
}, [measurements.ready]);
|
|
489
|
-
const
|
|
713
|
+
const resolvedPortalContainer = portalContainer === void 0 ? defaultPortalContainer : portalContainer;
|
|
714
|
+
const portalEnabled = resolvedPortalContainer !== null;
|
|
715
|
+
const panelAnchor = anchor;
|
|
716
|
+
const updatePortalGeometry = useCallback(() => {
|
|
717
|
+
const sourceElement = anchorRef.current;
|
|
718
|
+
if (!sourceElement || !measurements.ready) {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const sourceBounds = sourceElement.getBoundingClientRect();
|
|
722
|
+
const source = {
|
|
723
|
+
left: sourceBounds.left,
|
|
724
|
+
top: sourceBounds.top,
|
|
725
|
+
width: sourceBounds.width,
|
|
726
|
+
height: sourceBounds.height
|
|
727
|
+
};
|
|
728
|
+
const viewport = getViewportBounds();
|
|
729
|
+
const targetSize = getBoundedPanelSize(
|
|
730
|
+
measurements.expanded.size,
|
|
731
|
+
viewport,
|
|
732
|
+
viewportPadding
|
|
733
|
+
);
|
|
734
|
+
const target = getAnchoredTargetRect(
|
|
735
|
+
panelAnchor,
|
|
736
|
+
source,
|
|
737
|
+
targetSize,
|
|
738
|
+
viewport,
|
|
739
|
+
viewportPadding
|
|
740
|
+
);
|
|
741
|
+
setPortalGeometry((current) => {
|
|
742
|
+
if (current?.source.left === source.left && current.source.top === source.top && current.source.width === source.width && current.source.height === source.height && current.target.left === target.left && current.target.top === target.top && current.target.width === target.width && current.target.height === target.height) {
|
|
743
|
+
return current;
|
|
744
|
+
}
|
|
745
|
+
return { source, target };
|
|
746
|
+
});
|
|
747
|
+
}, [
|
|
748
|
+
measurements.expanded.size,
|
|
749
|
+
measurements.ready,
|
|
750
|
+
panelAnchor,
|
|
751
|
+
viewportPadding
|
|
752
|
+
]);
|
|
753
|
+
useLayoutEffect2(() => {
|
|
754
|
+
if (!portalEnabled || !measurements.ready) {
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const expandedChanged = previousExpandedRef.current !== expanded;
|
|
758
|
+
previousExpandedRef.current = expanded;
|
|
759
|
+
updatePortalGeometry();
|
|
760
|
+
if (!expandedChanged) {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
cancelPortalFrames();
|
|
764
|
+
if (expanded) {
|
|
765
|
+
setPortalPhase("opening");
|
|
766
|
+
setPortalVisualExpanded(false);
|
|
767
|
+
portalStartFrameRef.current = requestAnimationFrame(() => {
|
|
768
|
+
portalStartFrameRef.current = null;
|
|
769
|
+
if (!expandedRef.current || portalPhaseRef.current !== "opening") {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
setPortalVisualExpanded(true);
|
|
773
|
+
});
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (portalPhaseRef.current !== "collapsed" && portalPhaseRef.current !== "closing") {
|
|
777
|
+
setPortalPhase("closing");
|
|
778
|
+
setPortalVisualExpanded(false);
|
|
779
|
+
if (hasOpenedRef.current && !beforeCloseCalledRef.current) {
|
|
780
|
+
beforeCloseCalledRef.current = true;
|
|
781
|
+
beforeCloseRef.current?.();
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}, [
|
|
785
|
+
cancelPortalFrames,
|
|
490
786
|
expanded,
|
|
491
|
-
|
|
787
|
+
measurements.ready,
|
|
788
|
+
portalEnabled,
|
|
789
|
+
updatePortalGeometry
|
|
790
|
+
]);
|
|
791
|
+
useLayoutEffect2(() => {
|
|
792
|
+
if (portalPhase !== "handoff" || expanded) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
handoffFirstFrameRef.current = requestAnimationFrame(() => {
|
|
796
|
+
handoffFirstFrameRef.current = null;
|
|
797
|
+
if (expandedRef.current || portalPhaseRef.current !== "handoff") {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
handoffSecondFrameRef.current = requestAnimationFrame(() => {
|
|
801
|
+
handoffSecondFrameRef.current = null;
|
|
802
|
+
if (expandedRef.current || portalPhaseRef.current !== "handoff") {
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
setPortalPhase("collapsed");
|
|
806
|
+
setPortalGeometry(null);
|
|
807
|
+
if (hasOpenedRef.current) {
|
|
808
|
+
hasOpenedRef.current = false;
|
|
809
|
+
afterCloseRef.current?.();
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
return cancelPortalFrames;
|
|
814
|
+
}, [cancelPortalFrames, expanded, portalPhase]);
|
|
815
|
+
useEffect(() => {
|
|
816
|
+
if (!portalEnabled || portalPhase === "collapsed" || !measurements.ready) {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
let frame = null;
|
|
820
|
+
const scheduleUpdate = () => {
|
|
821
|
+
if (frame !== null) {
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
frame = requestAnimationFrame(() => {
|
|
825
|
+
frame = null;
|
|
826
|
+
updatePortalGeometry();
|
|
827
|
+
});
|
|
828
|
+
};
|
|
829
|
+
const observer = new ResizeObserver(scheduleUpdate);
|
|
830
|
+
if (anchorRef.current) {
|
|
831
|
+
observer.observe(anchorRef.current);
|
|
832
|
+
}
|
|
833
|
+
window.addEventListener("scroll", scheduleUpdate, true);
|
|
834
|
+
window.addEventListener("resize", scheduleUpdate);
|
|
835
|
+
window.visualViewport?.addEventListener("resize", scheduleUpdate);
|
|
836
|
+
window.visualViewport?.addEventListener("scroll", scheduleUpdate);
|
|
837
|
+
return () => {
|
|
838
|
+
if (frame !== null) {
|
|
839
|
+
cancelAnimationFrame(frame);
|
|
840
|
+
}
|
|
841
|
+
observer.disconnect();
|
|
842
|
+
window.removeEventListener("scroll", scheduleUpdate, true);
|
|
843
|
+
window.removeEventListener("resize", scheduleUpdate);
|
|
844
|
+
window.visualViewport?.removeEventListener("resize", scheduleUpdate);
|
|
845
|
+
window.visualViewport?.removeEventListener("scroll", scheduleUpdate);
|
|
846
|
+
};
|
|
847
|
+
}, [measurements.ready, portalEnabled, portalPhase, updatePortalGeometry]);
|
|
848
|
+
useEffect(() => cancelPortalFrames, [cancelPortalFrames]);
|
|
849
|
+
const motionState = getMorphMotionState({
|
|
850
|
+
expanded: portalEnabled ? portalVisualExpanded : expanded,
|
|
492
851
|
anchor,
|
|
493
852
|
spring,
|
|
494
853
|
animationEnabled,
|
|
495
854
|
collapsed: measurements.collapsed,
|
|
496
855
|
expandedSurface: measurements.expanded
|
|
497
856
|
});
|
|
498
|
-
const overflowVisible = expanded && shellOverflowVisible;
|
|
499
857
|
const shouldRenderStaticSource = !measurements.ready && !expanded;
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
858
|
+
const portalPresent = portalPhase !== "collapsed";
|
|
859
|
+
const portalIsRendered = portalEnabled && portalPresent && portalGeometry !== null && resolvedPortalContainer !== null;
|
|
860
|
+
const inlineSourceHidden = portalIsRendered && portalPhase !== "handoff";
|
|
861
|
+
const portalSettledState = portalPhase === "expanded" || portalPhase === "closing" ? "expanded" : "collapsed";
|
|
862
|
+
const portalTargetIsBounded = portalGeometry !== null && (portalGeometry.target.width < measurements.expanded.size.width || portalGeometry.target.height < measurements.expanded.size.height);
|
|
863
|
+
const contentLayers = (surfaceExpanded, settledState, portalMode = false) => {
|
|
864
|
+
const layerPositions = portalMode && portalGeometry ? getPortalLayerPositions(
|
|
865
|
+
surfaceExpanded,
|
|
866
|
+
portalGeometry.source,
|
|
867
|
+
portalGeometry.target
|
|
868
|
+
) : {
|
|
869
|
+
sourcePosition: motionState.sourcePosition,
|
|
870
|
+
targetPosition: motionState.targetPosition
|
|
871
|
+
};
|
|
872
|
+
const sourceGrowthScale = portalMode && portalGeometry ? getAxisScale(
|
|
873
|
+
motionState.collapsedLayerSize,
|
|
874
|
+
portalGeometry.target
|
|
875
|
+
) : motionState.sourceGrowthScale;
|
|
876
|
+
const collapsedToExpandedScale = portalMode && portalGeometry ? getAxisScale(
|
|
877
|
+
motionState.expandedLayerSize,
|
|
878
|
+
portalGeometry.source
|
|
879
|
+
) : motionState.collapsedToExpandedScale;
|
|
880
|
+
return /* @__PURE__ */ jsx2(
|
|
881
|
+
MorphContentLayers,
|
|
503
882
|
{
|
|
504
|
-
expanded,
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
883
|
+
expanded: surfaceExpanded,
|
|
884
|
+
settledState,
|
|
885
|
+
collapsedContent,
|
|
886
|
+
expandedContent,
|
|
887
|
+
collapsedLayerSize: motionState.collapsedLayerSize,
|
|
888
|
+
expandedLayerSize: motionState.expandedLayerSize,
|
|
889
|
+
sourcePosition: layerPositions.sourcePosition,
|
|
890
|
+
targetPosition: layerPositions.targetPosition,
|
|
891
|
+
sourceGrowthScale,
|
|
892
|
+
collapsedToExpandedScale,
|
|
893
|
+
transformOrigin: motionState.transformOrigin,
|
|
894
|
+
sourceTransition: motionState.sourceTransition,
|
|
895
|
+
sourceOpacityTransition: motionState.sourceOpacityTransition,
|
|
896
|
+
targetTransition: motionState.targetTransition,
|
|
897
|
+
targetOpacityTransition: motionState.targetOpacityTransition
|
|
510
898
|
}
|
|
511
|
-
)
|
|
512
|
-
|
|
899
|
+
);
|
|
900
|
+
};
|
|
901
|
+
const measurementNodes = /* @__PURE__ */ jsx2(
|
|
902
|
+
MorphMeasurementNodes,
|
|
903
|
+
{
|
|
904
|
+
collapsedRef: measurements.collapsedRef,
|
|
905
|
+
expandedRef: measurements.expandedRef,
|
|
906
|
+
collapsedContent,
|
|
907
|
+
expandedContent
|
|
908
|
+
}
|
|
909
|
+
);
|
|
910
|
+
if (!portalEnabled && portalContainer === null) {
|
|
911
|
+
const overflowVisible = expanded && shellOverflowVisible;
|
|
912
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
513
913
|
/* @__PURE__ */ jsx2(
|
|
514
|
-
|
|
914
|
+
MorphOverlay,
|
|
515
915
|
{
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
916
|
+
expanded,
|
|
917
|
+
onClose,
|
|
918
|
+
color: overlayColor,
|
|
919
|
+
opacity: overlayOpacity,
|
|
920
|
+
blur: overlayBlur,
|
|
921
|
+
zIndex
|
|
520
922
|
}
|
|
521
923
|
),
|
|
522
|
-
/* @__PURE__ */
|
|
924
|
+
/* @__PURE__ */ jsxs("div", { className, "aria-live": "polite", children: [
|
|
925
|
+
measurementNodes,
|
|
926
|
+
/* @__PURE__ */ jsx2("div", { className: "rmorph:relative rmorph:mx-auto rmorph:block rmorph:w-fit rmorph:align-top", children: shouldRenderStaticSource ? /* @__PURE__ */ jsx2("div", { className: "rmorph:inline-block rmorph:align-top", children: collapsedContent }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
927
|
+
/* @__PURE__ */ jsx2(
|
|
928
|
+
"div",
|
|
929
|
+
{
|
|
930
|
+
"aria-hidden": "true",
|
|
931
|
+
style: {
|
|
932
|
+
visibility: "hidden",
|
|
933
|
+
width: motionState.collapsedLayerSize.width,
|
|
934
|
+
height: motionState.collapsedLayerSize.height
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
),
|
|
938
|
+
/* @__PURE__ */ jsx2(
|
|
939
|
+
MorphShell,
|
|
940
|
+
{
|
|
941
|
+
expanded,
|
|
942
|
+
position: motionState.anchorPosition,
|
|
943
|
+
animatedSize: motionState.animatedSize,
|
|
944
|
+
visualStyle: motionState.visualStyle,
|
|
945
|
+
overflowVisible,
|
|
946
|
+
zIndex,
|
|
947
|
+
spring: motionState.activeSpring,
|
|
948
|
+
className: shellClassName,
|
|
949
|
+
onMorphComplete: () => {
|
|
950
|
+
if (expanded) {
|
|
951
|
+
hasOpenedRef.current = true;
|
|
952
|
+
beforeCloseCalledRef.current = false;
|
|
953
|
+
setInlineSettledState("expanded");
|
|
954
|
+
setShellOverflowVisible(true);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
setInlineSettledState("collapsed");
|
|
958
|
+
if (hasOpenedRef.current) {
|
|
959
|
+
hasOpenedRef.current = false;
|
|
960
|
+
afterClose?.();
|
|
961
|
+
}
|
|
962
|
+
},
|
|
963
|
+
onMorphStart: () => {
|
|
964
|
+
if (!expanded && hasOpenedRef.current && !beforeCloseCalledRef.current) {
|
|
965
|
+
beforeCloseCalledRef.current = true;
|
|
966
|
+
beforeClose?.();
|
|
967
|
+
}
|
|
968
|
+
setShellOverflowVisible(false);
|
|
969
|
+
setInlineSettledState(expanded ? "collapsed" : "expanded");
|
|
970
|
+
},
|
|
971
|
+
children: contentLayers(expanded, inlineSettledState)
|
|
972
|
+
}
|
|
973
|
+
)
|
|
974
|
+
] }) })
|
|
975
|
+
] })
|
|
976
|
+
] });
|
|
977
|
+
}
|
|
978
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
979
|
+
/* @__PURE__ */ jsxs("div", { className, "aria-live": "polite", children: [
|
|
980
|
+
measurementNodes,
|
|
981
|
+
/* @__PURE__ */ jsx2("div", { className: "rmorph:relative rmorph:mx-auto rmorph:block rmorph:w-fit rmorph:align-top", children: /* @__PURE__ */ jsx2(
|
|
982
|
+
"div",
|
|
983
|
+
{
|
|
984
|
+
ref: anchorRef,
|
|
985
|
+
"data-morpheus-inline-source": "",
|
|
986
|
+
...getLayerInteractivityProps(!inlineSourceHidden),
|
|
987
|
+
className: mergeClassNames(
|
|
988
|
+
"rmorph:inline-block rmorph:align-top rmorph:shadow-xs",
|
|
989
|
+
shellClassName
|
|
990
|
+
),
|
|
991
|
+
style: {
|
|
992
|
+
visibility: inlineSourceHidden ? "hidden" : void 0,
|
|
993
|
+
pointerEvents: inlineSourceHidden ? "none" : void 0
|
|
994
|
+
},
|
|
995
|
+
children: collapsedContent
|
|
996
|
+
}
|
|
997
|
+
) })
|
|
998
|
+
] }),
|
|
999
|
+
portalIsRendered && portalGeometry && resolvedPortalContainer ? createPortal(
|
|
1000
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
523
1001
|
/* @__PURE__ */ jsx2(
|
|
524
|
-
|
|
1002
|
+
MorphOverlay,
|
|
525
1003
|
{
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
1004
|
+
expanded,
|
|
1005
|
+
onClose,
|
|
1006
|
+
color: overlayColor,
|
|
1007
|
+
opacity: overlayOpacity,
|
|
1008
|
+
blur: overlayBlur,
|
|
1009
|
+
zIndex
|
|
532
1010
|
}
|
|
533
1011
|
),
|
|
534
1012
|
/* @__PURE__ */ jsx2(
|
|
535
|
-
|
|
1013
|
+
PortalMorphShell,
|
|
536
1014
|
{
|
|
537
|
-
expanded,
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
1015
|
+
expanded: portalVisualExpanded,
|
|
1016
|
+
lifecyclePhase: portalPhase,
|
|
1017
|
+
geometry: portalGeometry,
|
|
1018
|
+
collapsedVisualStyle: measurements.collapsed.visualStyle,
|
|
1019
|
+
expandedVisualStyle: measurements.expanded.visualStyle,
|
|
1020
|
+
overflow: portalPhase === "expanded" ? portalTargetIsBounded ? "auto" : "visible" : "hidden",
|
|
542
1021
|
zIndex,
|
|
543
1022
|
spring: motionState.activeSpring,
|
|
1023
|
+
className: shellClassName,
|
|
544
1024
|
onMorphComplete: () => {
|
|
545
|
-
if (
|
|
1025
|
+
if (expandedRef.current && portalPhaseRef.current === "opening" && portalVisualExpanded) {
|
|
546
1026
|
hasOpenedRef.current = true;
|
|
547
1027
|
beforeCloseCalledRef.current = false;
|
|
548
|
-
|
|
1028
|
+
setPortalPhase("expanded");
|
|
549
1029
|
return;
|
|
550
1030
|
}
|
|
551
|
-
if (
|
|
552
|
-
|
|
553
|
-
afterClose?.();
|
|
554
|
-
}
|
|
555
|
-
},
|
|
556
|
-
onMorphStart: () => {
|
|
557
|
-
if (!expanded && hasOpenedRef.current && !beforeCloseCalledRef.current) {
|
|
558
|
-
beforeCloseCalledRef.current = true;
|
|
559
|
-
beforeClose?.();
|
|
1031
|
+
if (!expandedRef.current && portalPhaseRef.current === "closing" && !portalVisualExpanded) {
|
|
1032
|
+
setPortalPhase("handoff");
|
|
560
1033
|
}
|
|
561
|
-
setShellOverflowVisible(false);
|
|
562
1034
|
},
|
|
563
|
-
children:
|
|
564
|
-
MorphContentLayers,
|
|
565
|
-
{
|
|
566
|
-
expanded,
|
|
567
|
-
collapsedContent,
|
|
568
|
-
expandedContent,
|
|
569
|
-
collapsedLayerSize: motionState.collapsedLayerSize,
|
|
570
|
-
expandedLayerSize: motionState.expandedLayerSize,
|
|
571
|
-
sourcePosition: motionState.sourcePosition,
|
|
572
|
-
targetPosition: motionState.targetPosition,
|
|
573
|
-
sourceGrowthScale: motionState.sourceGrowthScale,
|
|
574
|
-
collapsedToExpandedScale: motionState.collapsedToExpandedScale,
|
|
575
|
-
transformOrigin: motionState.transformOrigin,
|
|
576
|
-
sourceTransition: motionState.sourceTransition,
|
|
577
|
-
sourceOpacityTransition: motionState.sourceOpacityTransition,
|
|
578
|
-
targetTransition: motionState.targetTransition,
|
|
579
|
-
targetOpacityTransition: motionState.targetOpacityTransition
|
|
580
|
-
}
|
|
581
|
-
)
|
|
1035
|
+
children: contentLayers(portalVisualExpanded, portalSettledState, true)
|
|
582
1036
|
}
|
|
583
1037
|
)
|
|
584
|
-
] })
|
|
585
|
-
|
|
1038
|
+
] }),
|
|
1039
|
+
resolvedPortalContainer
|
|
1040
|
+
) : null
|
|
586
1041
|
] });
|
|
587
1042
|
}
|
|
588
1043
|
export {
|
|
589
1044
|
MorphAnchor,
|
|
590
1045
|
Morpheus,
|
|
591
1046
|
Morpheus as default,
|
|
592
|
-
defaultAnchorByDirection,
|
|
593
1047
|
morphSpringPresets
|
|
594
1048
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-morpheus",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "A React component for morphing one surface into other.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"build:js": "tsup src/index.ts --format esm --dts --external react --external react-dom --external framer-motion --clean",
|
|
30
30
|
"build:css": "tailwindcss -i src/style.css -o dist/style.css --minify",
|
|
31
31
|
"check": "tsc --noEmit",
|
|
32
|
+
"test": "bun test",
|
|
32
33
|
"publish:npm": "bun run scripts/publish.ts",
|
|
33
34
|
"release:minor": "bun run scripts/release.ts minor",
|
|
34
35
|
"release:major": "bun run scripts/release.ts major",
|
|
@@ -40,7 +41,9 @@
|
|
|
40
41
|
"react-dom": "^19.0.0"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
44
|
+
"@happy-dom/global-registrator": "^20.11.1",
|
|
43
45
|
"@tailwindcss/cli": "^4.3.3",
|
|
46
|
+
"@testing-library/react": "^16.3.2",
|
|
44
47
|
"@types/bun": "^1.3.14",
|
|
45
48
|
"@types/react": "^19.2.17",
|
|
46
49
|
"@types/react-dom": "^19.2.3",
|