react-morpheus 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 CHANGED
@@ -3,11 +3,16 @@
3
3
  A controlled React component for morphing one UI state into another.
4
4
 
5
5
  `Morpheus` does not manage its own open state. Keep the `expanded` boolean in
6
- your app state, render your trigger wherever it belongs, and pass the same
7
- source content to `collapsedContent` so Morpheus can measure and morph from it.
6
+ your app state and pass your trigger to `collapsedContent` so Morpheus can
7
+ render, measure, and morph from it. Opening is the responsibility of the source
8
+ component's own click handler.
9
+ Overlay clicks call `onClose`; update your controlled state there. Use
10
+ `beforeClose` and `afterClose` for work that should run around the closing
11
+ animation.
8
12
 
9
13
  ```tsx
10
14
  import { Morpheus, MorphAnchor } from "react-morpheus";
15
+ import "react-morpheus/style.css";
11
16
 
12
17
  function Example() {
13
18
  const [expanded, setExpanded] = useState(false);
@@ -18,25 +23,35 @@ function Example() {
18
23
  );
19
24
 
20
25
  return (
21
- <>
22
- {source}
23
- <Morpheus
24
- direction="bottom"
25
- anchor={MorphAnchor.TopMiddle}
26
- expanded={expanded}
27
- onClose={() => setExpanded(false)}
28
- collapsedContent={source}
29
- expandedContent={<div>Expanded content</div>}
30
- />
31
- </>
26
+ <Morpheus
27
+ direction="bottom"
28
+ anchor={MorphAnchor.TopMiddle}
29
+ expanded={expanded}
30
+ onClose={() => setExpanded(false)}
31
+ beforeClose={() => console.log("closing")}
32
+ afterClose={() => console.log("closed")}
33
+ collapsedContent={source}
34
+ expandedContent={<div>Expanded content</div>}
35
+ overlayZIndex={1200}
36
+ />
32
37
  );
33
38
  }
34
39
  ```
35
40
 
41
+ `overlayZIndex` controls the expanded morph surface. The overlay renders one
42
+ layer below it so apps can move the whole morphing stack above their own
43
+ chrome. Without this prop, Morpheus uses surface `1000` and overlay `999`.
44
+
45
+ Do not place Morpheus inside a container that clips overflow, such as
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.
49
+
36
50
  ## Scripts
37
51
 
38
52
  - `bun run build` builds declarations and bundled ESM output.
39
- - `bun run tag` creates an annotated git tag for the current package version.
40
- - `bun run tag:push` pushes tags to `origin`.
41
- - `bun run deploy:npm` builds and publishes to npm.
42
- - `bun run release:npm` builds, creates the version tag, and publishes to npm.
53
+ - `bun run check` runs TypeScript without emitting files.
54
+ - `bun run publish:npm` builds and publishes to npm.
55
+ - `bun run release:patch` bumps the patch version, builds, tags, and publishes.
56
+ - `bun run release:minor` bumps the minor version, builds, tags, and publishes.
57
+ - `bun run release:major` bumps the major version, builds, tags, and publishes.
package/dist/index.d.ts CHANGED
@@ -77,14 +77,16 @@ type MorphProps = {
77
77
  direction: MorphDirection;
78
78
  anchor?: MorphAnchor | undefined;
79
79
  expanded: boolean;
80
- onOpen?: (() => void) | undefined;
81
80
  onClose?: (() => void) | undefined;
81
+ beforeClose?: (() => void) | undefined;
82
+ afterClose?: (() => void) | undefined;
82
83
  collapsedContent: ReactNode;
83
84
  expandedContent: ReactNode;
84
85
  className?: string;
85
86
  overlayColor?: string;
86
87
  overlayOpacity?: number;
87
88
  overlayBlur?: number;
89
+ overlayZIndex?: number;
88
90
  spring?: Transition;
89
91
  };
90
92
  type MorphOverlayProps = {
@@ -93,6 +95,7 @@ type MorphOverlayProps = {
93
95
  color: string;
94
96
  opacity: number;
95
97
  blur: number;
98
+ zIndex: number;
96
99
  };
97
100
  type MorphMeasurementNodesProps = {
98
101
  collapsedRef: RefObject<HTMLDivElement | null>;
@@ -106,6 +109,7 @@ type MorphShellProps = {
106
109
  animatedSize: PanelSize;
107
110
  visualStyle: PanelVisualStyle;
108
111
  overflowVisible: boolean;
112
+ zIndex: number;
109
113
  spring: Transition;
110
114
  onMorphComplete: () => void;
111
115
  onMorphStart: () => void;
@@ -126,14 +130,12 @@ type MorphContentLayersProps = {
126
130
  sourceOpacityTransition: Transition;
127
131
  targetTransition: Transition;
128
132
  targetOpacityTransition: Transition;
129
- onOpen?: (() => void) | undefined;
130
133
  };
131
134
  type MorphLayerInteractivityProps = {
132
- "aria-hidden": boolean;
133
135
  inert?: boolean;
134
136
  };
135
137
 
136
- declare function Morpheus({ direction, anchor, expanded, onOpen, onClose, collapsedContent, expandedContent, className, overlayColor, overlayOpacity, overlayBlur, spring, }: MorphProps): react.JSX.Element;
138
+ declare function Morpheus({ direction, anchor, expanded, onClose, beforeClose, afterClose, collapsedContent, expandedContent, className, overlayColor, overlayOpacity, overlayBlur, overlayZIndex: zIndex, spring, }: MorphProps): react.JSX.Element;
137
139
 
138
140
  declare const defaultAnchorByDirection: Record<MorphDirection, MorphAnchor>;
139
141
  declare const morphSpringPresets: {
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/component.tsx
2
2
  import { motion as motion2 } from "framer-motion";
3
- import { useEffect, useState as useState2 } from "react";
3
+ import { useEffect, useRef as useRef2, useState as useState2 } from "react";
4
4
 
5
5
  // src/measurement.ts
6
6
  import { useLayoutEffect, useRef, useState } from "react";
@@ -226,7 +226,7 @@ function invertPosition(position) {
226
226
  };
227
227
  }
228
228
  function getLayerInteractivityProps(interactive) {
229
- return interactive ? { "aria-hidden": false } : { "aria-hidden": true, inert: true };
229
+ return interactive ? {} : { inert: true };
230
230
  }
231
231
  function getMorphMotionState({
232
232
  expanded,
@@ -294,14 +294,15 @@ function MorphOverlay({
294
294
  onClose,
295
295
  color,
296
296
  opacity,
297
- blur
297
+ blur,
298
+ zIndex
298
299
  }) {
299
- return /* @__PURE__ */ jsx(AnimatePresence, { children: expanded && onClose ? /* @__PURE__ */ jsx(
300
- motion.button,
300
+ const MotionOverlay = onClose ? motion.button : motion.div;
301
+ return /* @__PURE__ */ jsx(AnimatePresence, { children: expanded ? /* @__PURE__ */ jsx(
302
+ MotionOverlay,
301
303
  {
302
- type: "button",
303
- "aria-label": "Close morph",
304
- className: "fixed inset-0 z-40 cursor-default",
304
+ ...onClose ? { type: "button", "aria-label": "Close morph" } : { "aria-hidden": true },
305
+ className: "rmorph:fixed rmorph:inset-0 rmorph:cursor-default",
305
306
  initial: {
306
307
  opacity: 0
307
308
  },
@@ -315,7 +316,8 @@ function MorphOverlay({
315
316
  style: {
316
317
  backdropFilter: `blur(${blur}px)`,
317
318
  backgroundColor: getOverlayBackgroundColor(color, opacity),
318
- WebkitBackdropFilter: `blur(${blur}px)`
319
+ WebkitBackdropFilter: `blur(${blur}px)`,
320
+ zIndex: zIndex - 1
319
321
  },
320
322
  onClick: onClose
321
323
  }
@@ -324,7 +326,7 @@ function MorphOverlay({
324
326
 
325
327
  // src/component.tsx
326
328
  import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
327
- var liveSurfaceContentClassName = "absolute [&>*:first-child]:!border-transparent [&>*:first-child]:!bg-transparent [&>*:first-child]:!shadow-none";
329
+ var liveSurfaceContentClassName = "rmorph:absolute";
328
330
  function MorphMeasurementNodes({
329
331
  collapsedRef,
330
332
  expandedRef,
@@ -337,7 +339,7 @@ function MorphMeasurementNodes({
337
339
  {
338
340
  ref: collapsedRef,
339
341
  "aria-hidden": "true",
340
- className: "invisible absolute inset-x-0 top-0 pointer-events-none",
342
+ className: "rmorph:invisible rmorph:absolute rmorph:inset-x-0 rmorph:top-0 rmorph:pointer-events-none",
341
343
  children: collapsedContent
342
344
  }
343
345
  ),
@@ -346,7 +348,7 @@ function MorphMeasurementNodes({
346
348
  {
347
349
  ref: expandedRef,
348
350
  "aria-hidden": "true",
349
- className: "invisible absolute inset-x-0 top-0 pointer-events-none",
351
+ className: "rmorph:invisible rmorph:absolute rmorph:inset-x-0 rmorph:top-0 rmorph:pointer-events-none",
350
352
  children: expandedContent
351
353
  }
352
354
  )
@@ -358,6 +360,7 @@ function MorphShell({
358
360
  animatedSize,
359
361
  visualStyle,
360
362
  overflowVisible,
363
+ zIndex,
361
364
  spring,
362
365
  onMorphComplete,
363
366
  onMorphStart,
@@ -366,7 +369,7 @@ function MorphShell({
366
369
  return /* @__PURE__ */ jsx2(
367
370
  motion2.div,
368
371
  {
369
- className: `absolute shadow-xs ${expanded ? "z-50" : "z-0"}`,
372
+ className: "rmorph:absolute rmorph:shadow-xs",
370
373
  initial: false,
371
374
  animate: {
372
375
  width: animatedSize.width,
@@ -378,7 +381,8 @@ function MorphShell({
378
381
  onAnimationComplete: onMorphComplete,
379
382
  onAnimationStart: onMorphStart,
380
383
  style: {
381
- overflow: overflowVisible ? "visible" : "hidden"
384
+ overflow: overflowVisible ? "visible" : "hidden",
385
+ zIndex: expanded ? zIndex : 0
382
386
  },
383
387
  transition: spring,
384
388
  children
@@ -399,8 +403,7 @@ function MorphContentLayers({
399
403
  sourceTransition,
400
404
  sourceOpacityTransition,
401
405
  targetTransition,
402
- targetOpacityTransition,
403
- onOpen
406
+ targetOpacityTransition
404
407
  }) {
405
408
  return /* @__PURE__ */ jsxs(Fragment, { children: [
406
409
  /* @__PURE__ */ jsx2(
@@ -419,7 +422,6 @@ function MorphContentLayers({
419
422
  ...sourceTransition,
420
423
  opacity: sourceOpacityTransition
421
424
  },
422
- onClick: expanded ? void 0 : onOpen,
423
425
  style: {
424
426
  width: collapsedLayerSize.width,
425
427
  height: collapsedLayerSize.height,
@@ -460,19 +462,23 @@ function Morpheus({
460
462
  direction,
461
463
  anchor,
462
464
  expanded,
463
- onOpen,
464
465
  onClose,
466
+ beforeClose,
467
+ afterClose,
465
468
  collapsedContent,
466
469
  expandedContent,
467
- className = "relative inline-block align-top",
470
+ className = "rmorph:relative rmorph:inline-block rmorph:align-top",
468
471
  overlayColor = "#05070a",
469
472
  overlayOpacity = 0.54,
470
473
  overlayBlur = 0,
474
+ overlayZIndex: zIndex = 1e3,
471
475
  spring = defaultPanelSpring
472
476
  }) {
473
477
  const measurements = useMorphMeasurements();
474
478
  const [animationEnabled, setAnimationEnabled] = useState2(false);
475
479
  const [shellOverflowVisible, setShellOverflowVisible] = useState2(false);
480
+ const hasOpenedRef = useRef2(expanded);
481
+ const beforeCloseCalledRef = useRef2(false);
476
482
  useEffect(() => {
477
483
  if (!measurements.ready) {
478
484
  return;
@@ -499,7 +505,8 @@ function Morpheus({
499
505
  onClose,
500
506
  color: overlayColor,
501
507
  opacity: overlayOpacity,
502
- blur: overlayBlur
508
+ blur: overlayBlur,
509
+ zIndex
503
510
  }
504
511
  ),
505
512
  /* @__PURE__ */ jsxs("div", { className, "aria-live": "polite", children: [
@@ -512,7 +519,7 @@ function Morpheus({
512
519
  expandedContent
513
520
  }
514
521
  ),
515
- /* @__PURE__ */ jsx2("div", { className: "relative mx-auto block w-fit align-top", children: shouldRenderStaticSource ? /* @__PURE__ */ jsx2("div", { className: "inline-block align-top", onClick: onOpen, children: collapsedContent }) : /* @__PURE__ */ jsxs(Fragment, { children: [
522
+ /* @__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: [
516
523
  /* @__PURE__ */ jsx2(
517
524
  "div",
518
525
  {
@@ -532,13 +539,27 @@ function Morpheus({
532
539
  animatedSize: motionState.animatedSize,
533
540
  visualStyle: motionState.visualStyle,
534
541
  overflowVisible,
542
+ zIndex,
535
543
  spring: motionState.activeSpring,
536
544
  onMorphComplete: () => {
537
545
  if (expanded) {
546
+ hasOpenedRef.current = true;
547
+ beforeCloseCalledRef.current = false;
538
548
  setShellOverflowVisible(true);
549
+ return;
539
550
  }
551
+ if (hasOpenedRef.current) {
552
+ hasOpenedRef.current = false;
553
+ afterClose?.();
554
+ }
555
+ },
556
+ onMorphStart: () => {
557
+ if (!expanded && hasOpenedRef.current && !beforeCloseCalledRef.current) {
558
+ beforeCloseCalledRef.current = true;
559
+ beforeClose?.();
560
+ }
561
+ setShellOverflowVisible(false);
540
562
  },
541
- onMorphStart: () => setShellOverflowVisible(false),
542
563
  children: /* @__PURE__ */ jsx2(
543
564
  MorphContentLayers,
544
565
  {
@@ -555,8 +576,7 @@ function Morpheus({
555
576
  sourceTransition: motionState.sourceTransition,
556
577
  sourceOpacityTransition: motionState.sourceOpacityTransition,
557
578
  targetTransition: motionState.targetTransition,
558
- targetOpacityTransition: motionState.targetOpacityTransition,
559
- onOpen
579
+ targetOpacityTransition: motionState.targetOpacityTransition
560
580
  }
561
581
  )
562
582
  }
package/dist/style.css ADDED
@@ -0,0 +1,2 @@
1
+ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}.rmorph\:pointer-events-none{pointer-events:none}.rmorph\:invisible{visibility:hidden}.rmorph\:absolute{position:absolute}.rmorph\:fixed{position:fixed}.rmorph\:relative{position:relative}.rmorph\:inset-0{inset:0}.rmorph\:inset-x-0{inset-inline:0}.rmorph\:top-0{top:0}.rmorph\:mx-auto{margin-inline:auto}.rmorph\:block{display:block}.rmorph\:inline-block{display:inline-block}.rmorph\:w-fit{width:fit-content}.rmorph\:cursor-default{cursor:default}.rmorph\:align-top{vertical-align:top}.rmorph\:shadow-xs{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "react-morpheus",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "description": "A React component for morphing one surface into other.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
- "sideEffects": false,
7
+ "sideEffects": [
8
+ "**/*.css"
9
+ ],
8
10
  "files": [
9
11
  "dist",
10
12
  "README.md"
@@ -12,16 +14,22 @@
12
14
  "main": "./dist/index.js",
13
15
  "module": "./dist/index.js",
14
16
  "types": "./dist/index.d.ts",
17
+ "style": "./dist/style.css",
15
18
  "exports": {
16
19
  ".": {
17
20
  "types": "./dist/index.d.ts",
18
21
  "import": "./dist/index.js"
19
22
  },
23
+ "./style.css": "./dist/style.css",
20
24
  "./package.json": "./package.json"
21
25
  },
22
26
  "scripts": {
23
- "build": "tsup src/index.ts --format esm --dts --external react --external react-dom --external framer-motion --clean",
27
+ "prepare": "bun run build",
28
+ "build": "bun run build:js && bun run build:css",
29
+ "build:js": "tsup src/index.ts --format esm --dts --external react --external react-dom --external framer-motion --clean",
30
+ "build:css": "tailwindcss -i src/style.css -o dist/style.css --minify",
24
31
  "check": "tsc --noEmit",
32
+ "publish:npm": "bun run scripts/publish.ts",
25
33
  "release:minor": "bun run scripts/release.ts minor",
26
34
  "release:major": "bun run scripts/release.ts major",
27
35
  "release:patch": "bun run scripts/release.ts patch"
@@ -32,12 +40,14 @@
32
40
  "react-dom": "^19.0.0"
33
41
  },
34
42
  "devDependencies": {
43
+ "@tailwindcss/cli": "^4.3.3",
35
44
  "@types/bun": "^1.3.14",
36
45
  "@types/react": "^19.2.17",
37
46
  "@types/react-dom": "^19.2.3",
38
47
  "framer-motion": "^12.42.2",
39
48
  "react": "^19.2.7",
40
49
  "react-dom": "^19.2.7",
50
+ "tailwindcss": "^4.3.3",
41
51
  "tsup": "^8.5.1",
42
52
  "typescript": "^6.0.3"
43
53
  },