react-morpheus 0.1.3 → 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,12 @@
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.
8
- Overlay clicks and Escape key presses call `onCloseIntent`; update your
9
- controlled state there. Use `afterClose` for work that should run after the
10
- closing animation completes.
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.
11
12
 
12
13
  ```tsx
13
14
  import { Morpheus, MorphAnchor } from "react-morpheus";
@@ -22,26 +23,35 @@ function Example() {
22
23
  );
23
24
 
24
25
  return (
25
- <>
26
- {source}
27
- <Morpheus
28
- direction="bottom"
29
- anchor={MorphAnchor.TopMiddle}
30
- expanded={expanded}
31
- onCloseIntent={() => setExpanded(false)}
32
- afterClose={() => console.log("closed")}
33
- collapsedContent={source}
34
- expandedContent={<div>Expanded content</div>}
35
- />
36
- </>
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
+ />
37
37
  );
38
38
  }
39
39
  ```
40
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
+
41
50
  ## Scripts
42
51
 
43
52
  - `bun run build` builds declarations and bundled ESM output.
44
- - `bun run tag` creates an annotated git tag for the current package version.
45
- - `bun run tag:push` pushes tags to `origin`.
46
- - `bun run deploy:npm` builds and publishes to npm.
47
- - `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,7 +77,8 @@ type MorphProps = {
77
77
  direction: MorphDirection;
78
78
  anchor?: MorphAnchor | undefined;
79
79
  expanded: boolean;
80
- onCloseIntent?: (() => void) | undefined;
80
+ onClose?: (() => void) | undefined;
81
+ beforeClose?: (() => void) | undefined;
81
82
  afterClose?: (() => void) | undefined;
82
83
  collapsedContent: ReactNode;
83
84
  expandedContent: ReactNode;
@@ -85,14 +86,16 @@ type MorphProps = {
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 = {
91
93
  expanded: boolean;
92
- onCloseIntent?: (() => void) | undefined;
94
+ onClose?: (() => void) | undefined;
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;
@@ -128,11 +132,10 @@ type MorphContentLayersProps = {
128
132
  targetOpacityTransition: Transition;
129
133
  };
130
134
  type MorphLayerInteractivityProps = {
131
- "aria-hidden": boolean;
132
135
  inert?: boolean;
133
136
  };
134
137
 
135
- declare function Morpheus({ direction, anchor, expanded, onCloseIntent, afterClose, 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;
136
139
 
137
140
  declare const defaultAnchorByDirection: Record<MorphDirection, MorphAnchor>;
138
141
  declare const morphSpringPresets: {
package/dist/index.js CHANGED
@@ -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,
@@ -291,17 +291,18 @@ function getOverlayBackgroundColor(color, opacity) {
291
291
  }
292
292
  function MorphOverlay({
293
293
  expanded,
294
- onCloseIntent,
294
+ onClose,
295
295
  color,
296
296
  opacity,
297
- blur
297
+ blur,
298
+ zIndex
298
299
  }) {
299
- return /* @__PURE__ */ jsx(AnimatePresence, { children: expanded && onCloseIntent ? /* @__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,16 +316,17 @@ 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
- onClick: onCloseIntent
322
+ onClick: onClose
321
323
  }
322
324
  ) : null });
323
325
  }
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
@@ -458,20 +462,23 @@ function Morpheus({
458
462
  direction,
459
463
  anchor,
460
464
  expanded,
461
- onCloseIntent,
465
+ onClose,
466
+ beforeClose,
462
467
  afterClose,
463
468
  collapsedContent,
464
469
  expandedContent,
465
- className = "relative inline-block align-top",
470
+ className = "rmorph:relative rmorph:inline-block rmorph:align-top",
466
471
  overlayColor = "#05070a",
467
472
  overlayOpacity = 0.54,
468
473
  overlayBlur = 0,
474
+ overlayZIndex: zIndex = 1e3,
469
475
  spring = defaultPanelSpring
470
476
  }) {
471
477
  const measurements = useMorphMeasurements();
472
478
  const [animationEnabled, setAnimationEnabled] = useState2(false);
473
479
  const [shellOverflowVisible, setShellOverflowVisible] = useState2(false);
474
480
  const hasOpenedRef = useRef2(expanded);
481
+ const beforeCloseCalledRef = useRef2(false);
475
482
  useEffect(() => {
476
483
  if (!measurements.ready) {
477
484
  return;
@@ -479,18 +486,6 @@ function Morpheus({
479
486
  const frame = requestAnimationFrame(() => setAnimationEnabled(true));
480
487
  return () => cancelAnimationFrame(frame);
481
488
  }, [measurements.ready]);
482
- useEffect(() => {
483
- if (!expanded || !onCloseIntent) {
484
- return;
485
- }
486
- const handleKeyDown = (event) => {
487
- if (event.key === "Escape" && !event.repeat) {
488
- onCloseIntent();
489
- }
490
- };
491
- document.addEventListener("keydown", handleKeyDown);
492
- return () => document.removeEventListener("keydown", handleKeyDown);
493
- }, [expanded, onCloseIntent]);
494
489
  const motionState = getMorphMotionState({
495
490
  expanded,
496
491
  direction,
@@ -507,10 +502,11 @@ function Morpheus({
507
502
  MorphOverlay,
508
503
  {
509
504
  expanded,
510
- onCloseIntent,
505
+ onClose,
511
506
  color: overlayColor,
512
507
  opacity: overlayOpacity,
513
- blur: overlayBlur
508
+ blur: overlayBlur,
509
+ zIndex
514
510
  }
515
511
  ),
516
512
  /* @__PURE__ */ jsxs("div", { className, "aria-live": "polite", children: [
@@ -523,7 +519,7 @@ function Morpheus({
523
519
  expandedContent
524
520
  }
525
521
  ),
526
- /* @__PURE__ */ jsx2("div", { className: "relative mx-auto block w-fit align-top", children: shouldRenderStaticSource ? /* @__PURE__ */ jsx2("div", { className: "inline-block align-top", 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: [
527
523
  /* @__PURE__ */ jsx2(
528
524
  "div",
529
525
  {
@@ -543,10 +539,12 @@ function Morpheus({
543
539
  animatedSize: motionState.animatedSize,
544
540
  visualStyle: motionState.visualStyle,
545
541
  overflowVisible,
542
+ zIndex,
546
543
  spring: motionState.activeSpring,
547
544
  onMorphComplete: () => {
548
545
  if (expanded) {
549
546
  hasOpenedRef.current = true;
547
+ beforeCloseCalledRef.current = false;
550
548
  setShellOverflowVisible(true);
551
549
  return;
552
550
  }
@@ -555,7 +553,13 @@ function Morpheus({
555
553
  afterClose?.();
556
554
  }
557
555
  },
558
- onMorphStart: () => setShellOverflowVisible(false),
556
+ onMorphStart: () => {
557
+ if (!expanded && hasOpenedRef.current && !beforeCloseCalledRef.current) {
558
+ beforeCloseCalledRef.current = true;
559
+ beforeClose?.();
560
+ }
561
+ setShellOverflowVisible(false);
562
+ },
559
563
  children: /* @__PURE__ */ jsx2(
560
564
  MorphContentLayers,
561
565
  {
package/dist/style.css CHANGED
@@ -1,2 +1,2 @@
1
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;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1)}}@layer utilities{.pointer-events-none{pointer-events:none}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:0}.inset-x-0{inset-inline:0}.top-0{top:0}.z-0{z-index:0}.z-40{z-index:40}.z-50{z-index:50}.mx-auto{margin-inline:auto}.block{display:block}.hidden{display:none}.inline-block{display:inline-block}.w-fit{width:fit-content}.cursor-default{cursor:default}.align-top{vertical-align:top}.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)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.\[\&\>\*\:first-child\]\:\!border-transparent>:first-child{border-color:#0000!important}.\[\&\>\*\:first-child\]\:\!bg-transparent>:first-child{background-color:#0000!important}.\[\&\>\*\:first-child\]\:\!shadow-none>:first-child{--tw-shadow:0 0 #0000!important;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)!important}}@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}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
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,6 +1,6 @@
1
1
  {
2
2
  "name": "react-morpheus",
3
- "version": "0.1.3",
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",
@@ -24,6 +24,7 @@
24
24
  "./package.json": "./package.json"
25
25
  },
26
26
  "scripts": {
27
+ "prepare": "bun run build",
27
28
  "build": "bun run build:js && bun run build:css",
28
29
  "build:js": "tsup src/index.ts --format esm --dts --external react --external react-dom --external framer-motion --clean",
29
30
  "build:css": "tailwindcss -i src/style.css -o dist/style.css --minify",