@passui/react 0.1.27 → 0.1.28
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.
|
@@ -19,12 +19,37 @@ export declare const spring: {
|
|
|
19
19
|
readonly type: "spring";
|
|
20
20
|
};
|
|
21
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* Apple's fluid overshoot: cubic-bezier(0.34, 1.56, 0.1, 1).
|
|
24
|
+
* Replaces traditional ease profiles with an authentic fluid bounce
|
|
25
|
+
* that overshoots and settles, matching Apple's custom interface physics.
|
|
26
|
+
* Used for zoom/matched-geometry transitions where the surface emerges
|
|
27
|
+
* from the trigger and snaps into place with a physical bounce.
|
|
28
|
+
*/
|
|
29
|
+
export declare const fluidOvershoot: {
|
|
30
|
+
readonly duration: 0.4;
|
|
31
|
+
readonly ease: [number, number, number, number];
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Exit motion: content collapses back toward its source.
|
|
35
|
+
* The surface shrinks toward the trigger's position while fading,
|
|
36
|
+
* like a water drop absorbing back into its origin.
|
|
37
|
+
*/
|
|
38
|
+
export declare const fluidCollapse: {
|
|
39
|
+
readonly duration: 0.28;
|
|
40
|
+
readonly ease: [number, number, number, number];
|
|
41
|
+
};
|
|
22
42
|
export type FloatingPanelSide = "above" | "below";
|
|
23
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Anchored panels settle toward their source without shrinking their text.
|
|
45
|
+
* Now uses origin-based scaling: the panel grows from the trigger's
|
|
46
|
+
* position (transform-origin set by the caller) with a scale + opacity
|
|
47
|
+
* crossfade, rather than a plain slide.
|
|
48
|
+
*/
|
|
24
49
|
export declare function floatingPanelMotion(side?: FloatingPanelSide): {
|
|
25
50
|
readonly initial: {
|
|
26
51
|
readonly opacity: 0;
|
|
27
|
-
readonly scale:
|
|
52
|
+
readonly scale: 0.92;
|
|
28
53
|
readonly y: number;
|
|
29
54
|
};
|
|
30
55
|
readonly animate: {
|
|
@@ -34,16 +59,15 @@ export declare function floatingPanelMotion(side?: FloatingPanelSide): {
|
|
|
34
59
|
};
|
|
35
60
|
readonly exit: {
|
|
36
61
|
readonly opacity: 0;
|
|
37
|
-
readonly scale:
|
|
62
|
+
readonly scale: 0.95;
|
|
38
63
|
readonly y: number;
|
|
39
64
|
};
|
|
40
65
|
readonly transition: {
|
|
41
|
-
readonly
|
|
42
|
-
readonly
|
|
43
|
-
readonly mass: 1;
|
|
44
|
-
readonly type: "spring";
|
|
66
|
+
readonly duration: 0.4;
|
|
67
|
+
readonly ease: [number, number, number, number];
|
|
45
68
|
readonly opacity: {
|
|
46
69
|
readonly duration: 0.16;
|
|
70
|
+
readonly ease: [number, number, number, number];
|
|
47
71
|
};
|
|
48
72
|
};
|
|
49
73
|
};
|
|
@@ -51,7 +75,7 @@ export declare function floatingPanelMotion(side?: FloatingPanelSide): {
|
|
|
51
75
|
export declare const popoverMotion: {
|
|
52
76
|
readonly initial: {
|
|
53
77
|
readonly opacity: 0;
|
|
54
|
-
readonly scale:
|
|
78
|
+
readonly scale: 0.92;
|
|
55
79
|
readonly y: number;
|
|
56
80
|
};
|
|
57
81
|
readonly animate: {
|
|
@@ -61,19 +85,68 @@ export declare const popoverMotion: {
|
|
|
61
85
|
};
|
|
62
86
|
readonly exit: {
|
|
63
87
|
readonly opacity: 0;
|
|
64
|
-
readonly scale:
|
|
88
|
+
readonly scale: 0.95;
|
|
65
89
|
readonly y: number;
|
|
66
90
|
};
|
|
67
91
|
readonly transition: {
|
|
68
|
-
readonly
|
|
69
|
-
readonly
|
|
70
|
-
readonly mass: 1;
|
|
71
|
-
readonly type: "spring";
|
|
92
|
+
readonly duration: 0.4;
|
|
93
|
+
readonly ease: [number, number, number, number];
|
|
72
94
|
readonly opacity: {
|
|
73
95
|
readonly duration: 0.16;
|
|
96
|
+
readonly ease: [number, number, number, number];
|
|
74
97
|
};
|
|
75
98
|
};
|
|
76
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* Zoom/matched-geometry transition for dialogs and center-placed sheets.
|
|
102
|
+
* The surface starts small (scale 0.88) and invisible, then grows to
|
|
103
|
+
* full size with the fluid overshoot bounce. Content elements inside
|
|
104
|
+
* crossfade in slightly delayed so they phase into visibility
|
|
105
|
+
* asynchronously — not a harsh switch.
|
|
106
|
+
*/
|
|
107
|
+
export declare const zoomReveal: {
|
|
108
|
+
readonly initial: {
|
|
109
|
+
readonly opacity: 0;
|
|
110
|
+
readonly scale: 0.88;
|
|
111
|
+
};
|
|
112
|
+
readonly animate: {
|
|
113
|
+
readonly opacity: 1;
|
|
114
|
+
readonly scale: 1;
|
|
115
|
+
};
|
|
116
|
+
readonly exit: {
|
|
117
|
+
readonly opacity: 0;
|
|
118
|
+
readonly scale: 0.92;
|
|
119
|
+
};
|
|
120
|
+
readonly transition: {
|
|
121
|
+
readonly duration: 0.4;
|
|
122
|
+
readonly ease: [number, number, number, number];
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Content crossfade for elements inside an overlay.
|
|
127
|
+
* Rather than a harsh switch, sub-elements scale and phase into
|
|
128
|
+
* visibility asynchronously. Use for children of dialogs, sheets,
|
|
129
|
+
* and popovers — the container arrives first, then content fades in.
|
|
130
|
+
*/
|
|
131
|
+
export declare const contentCrossfade: {
|
|
132
|
+
readonly initial: {
|
|
133
|
+
readonly opacity: 0;
|
|
134
|
+
readonly scale: 0.96;
|
|
135
|
+
};
|
|
136
|
+
readonly animate: {
|
|
137
|
+
readonly opacity: 1;
|
|
138
|
+
readonly scale: 1;
|
|
139
|
+
};
|
|
140
|
+
readonly exit: {
|
|
141
|
+
readonly opacity: 0;
|
|
142
|
+
readonly scale: 0.98;
|
|
143
|
+
};
|
|
144
|
+
readonly transition: {
|
|
145
|
+
readonly duration: 0.24;
|
|
146
|
+
readonly ease: [number, number, number, number];
|
|
147
|
+
readonly delay: 0.04;
|
|
148
|
+
};
|
|
149
|
+
};
|
|
77
150
|
/** Floating bars that rise from the bottom edge. */
|
|
78
151
|
export declare const riseMotion: {
|
|
79
152
|
readonly initial: {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@ import { Sheet } from "../sheet/sheet"
|
|
|
2
2
|
import { AnimatePresence, motion, useReducedMotion } from "motion/react"
|
|
3
3
|
import { Button } from "../button/button"
|
|
4
4
|
import { cn } from "../../lib/class-names"
|
|
5
|
-
import { spring } from "../../motion/transitions"
|
|
5
|
+
import { contentCrossfade, spring, zoomReveal } from "../../motion/transitions"
|
|
6
6
|
import { createContext, useContext, useId, type FormEvent, type ReactNode } from "react"
|
|
7
7
|
|
|
8
8
|
const CloseContext = createContext<() => void>(() => {})
|
|
@@ -81,19 +81,31 @@ export function Dialog({
|
|
|
81
81
|
initial={{
|
|
82
82
|
height: reducedMotion ? "auto" : 0,
|
|
83
83
|
opacity: 0,
|
|
84
|
+
scale: reducedMotion ? 1 : 0.97,
|
|
84
85
|
}}
|
|
85
86
|
animate={{
|
|
86
87
|
height: "auto",
|
|
87
88
|
opacity: 1,
|
|
89
|
+
scale: 1,
|
|
88
90
|
}}
|
|
89
91
|
exit={{
|
|
90
92
|
height: reducedMotion ? "auto" : 0,
|
|
91
93
|
opacity: 0,
|
|
94
|
+
scale: reducedMotion ? 1 : 0.98,
|
|
92
95
|
}}
|
|
93
|
-
transition={reducedMotion ? { duration: 0 } : spring.
|
|
96
|
+
transition={reducedMotion ? { duration: 0 } : spring.bouncy}
|
|
94
97
|
className="overflow-hidden"
|
|
98
|
+
style={{ transformOrigin: "top center" }}
|
|
95
99
|
>
|
|
96
|
-
|
|
100
|
+
<motion.div
|
|
101
|
+
initial={reducedMotion ? false : contentCrossfade.initial}
|
|
102
|
+
animate={reducedMotion ? undefined : contentCrossfade.animate}
|
|
103
|
+
transition={
|
|
104
|
+
reducedMotion ? { duration: 0 } : contentCrossfade.transition
|
|
105
|
+
}
|
|
106
|
+
>
|
|
107
|
+
{body}
|
|
108
|
+
</motion.div>
|
|
97
109
|
</motion.div>
|
|
98
110
|
)}
|
|
99
111
|
</AnimatePresence>
|
|
@@ -10,7 +10,12 @@ import { concealMenuSource } from "../context-menu/conceal-menu-source"
|
|
|
10
10
|
import { menuSurfaceExit, menuSurfaceReveal } from "../context-menu/menu-reveal"
|
|
11
11
|
import { cn } from "../../lib/class-names"
|
|
12
12
|
import { screenEdges } from "../../lib/screen-edges"
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
contentCrossfade,
|
|
15
|
+
floatingPanelMotion,
|
|
16
|
+
fluidOvershoot,
|
|
17
|
+
spring,
|
|
18
|
+
} from "../../motion/transitions"
|
|
14
19
|
import { fitPopover, type PopoverFit } from "./popover-fit"
|
|
15
20
|
|
|
16
21
|
export type PopoverOrigin = "top-left" | "top-right"
|
|
@@ -56,6 +61,11 @@ function OpenPopover({
|
|
|
56
61
|
const [reveal, setReveal] = useState<ReturnType<typeof menuSurfaceReveal> | null>(null)
|
|
57
62
|
const popoverMotion = floatingPanelMotion(fit.above ? "above" : "below")
|
|
58
63
|
|
|
64
|
+
// For anchored popovers, the surface morphs from the trigger via
|
|
65
|
+
// menuSurfaceReveal. For non-anchored popovers, we use scale + the
|
|
66
|
+
// fluid overshoot spring so the panel grows from the origin corner
|
|
67
|
+
// with a physical bounce — Apple's matched-geometry feel.
|
|
68
|
+
|
|
59
69
|
useLayoutEffect(() => {
|
|
60
70
|
const refit = () => {
|
|
61
71
|
const element = ref.current
|
|
@@ -283,11 +293,17 @@ function OpenPopover({
|
|
|
283
293
|
transition={
|
|
284
294
|
reducedMotion
|
|
285
295
|
? { duration: 0 }
|
|
286
|
-
: { ...
|
|
296
|
+
: { ...fluidOvershoot, opacity: { duration: 0.16 } }
|
|
287
297
|
}
|
|
288
298
|
/>
|
|
289
299
|
)}
|
|
290
|
-
|
|
300
|
+
<motion.div
|
|
301
|
+
initial={reducedMotion ? false : anchorRef ? { opacity: 0 } : undefined}
|
|
302
|
+
animate={reducedMotion ? undefined : anchorRef ? { opacity: 1 } : undefined}
|
|
303
|
+
transition={reducedMotion ? { duration: 0 } : { duration: 0.16, delay: 0.06 }}
|
|
304
|
+
>
|
|
305
|
+
{children}
|
|
306
|
+
</motion.div>
|
|
291
307
|
</motion.div>
|
|
292
308
|
)
|
|
293
309
|
}
|
|
@@ -2,7 +2,7 @@ import { AnimatePresence, motion, useReducedMotion } from "motion/react"
|
|
|
2
2
|
import { type ReactNode, useRef } from "react"
|
|
3
3
|
import { useVisualViewport } from "../../hooks/use-visual-viewport"
|
|
4
4
|
import { cn } from "../../lib/class-names"
|
|
5
|
-
import { spring } from "../../motion/transitions"
|
|
5
|
+
import { contentCrossfade, spring, zoomReveal } from "../../motion/transitions"
|
|
6
6
|
import { SheetPortal } from "./sheet-portal"
|
|
7
7
|
import { useSheetFocus } from "./use-sheet-focus"
|
|
8
8
|
|
|
@@ -95,12 +95,47 @@ function OpenSheet({
|
|
|
95
95
|
placement === "center" ? "rounded-sheet" : "rounded-t-sheet sm:rounded-sheet",
|
|
96
96
|
className
|
|
97
97
|
)}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
style={{
|
|
99
|
+
transformOrigin: placement === "center" ? "center center" : "bottom center",
|
|
100
|
+
}}
|
|
101
|
+
initial={
|
|
102
|
+
reducedMotion
|
|
103
|
+
? { opacity: 0 }
|
|
104
|
+
: placement === "center"
|
|
105
|
+
? zoomReveal.initial
|
|
106
|
+
: { y: 40, opacity: 0, scale: 0.96 }
|
|
107
|
+
}
|
|
108
|
+
animate={
|
|
109
|
+
reducedMotion
|
|
110
|
+
? { opacity: 1 }
|
|
111
|
+
: placement === "center"
|
|
112
|
+
? zoomReveal.animate
|
|
113
|
+
: { y: 0, opacity: 1, scale: 1 }
|
|
114
|
+
}
|
|
115
|
+
exit={
|
|
116
|
+
reducedMotion
|
|
117
|
+
? { opacity: 0 }
|
|
118
|
+
: placement === "center"
|
|
119
|
+
? zoomReveal.exit
|
|
120
|
+
: { y: 30, opacity: 0, scale: 0.98 }
|
|
121
|
+
}
|
|
122
|
+
transition={
|
|
123
|
+
reducedMotion
|
|
124
|
+
? { duration: 0 }
|
|
125
|
+
: placement === "center"
|
|
126
|
+
? zoomReveal.transition
|
|
127
|
+
: { ...spring.bouncy, opacity: { duration: 0.18 } }
|
|
128
|
+
}
|
|
102
129
|
>
|
|
103
|
-
|
|
130
|
+
<motion.div
|
|
131
|
+
className="flex min-h-0 flex-1 flex-col overflow-y-auto"
|
|
132
|
+
initial={reducedMotion ? false : contentCrossfade.initial}
|
|
133
|
+
animate={reducedMotion ? undefined : contentCrossfade.animate}
|
|
134
|
+
exit={reducedMotion ? undefined : contentCrossfade.exit}
|
|
135
|
+
transition={reducedMotion ? { duration: 0 } : contentCrossfade.transition}
|
|
136
|
+
>
|
|
137
|
+
{children}
|
|
138
|
+
</motion.div>
|
|
104
139
|
</motion.div>
|
|
105
140
|
</div>
|
|
106
141
|
)
|
|
@@ -8,18 +8,45 @@ export const spring = {
|
|
|
8
8
|
bouncy: { type: "spring", ...springs.bouncy },
|
|
9
9
|
} as const satisfies Record<keyof typeof springs, Transition>
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Apple's fluid overshoot: cubic-bezier(0.34, 1.56, 0.1, 1).
|
|
13
|
+
* Replaces traditional ease profiles with an authentic fluid bounce
|
|
14
|
+
* that overshoots and settles, matching Apple's custom interface physics.
|
|
15
|
+
* Used for zoom/matched-geometry transitions where the surface emerges
|
|
16
|
+
* from the trigger and snaps into place with a physical bounce.
|
|
17
|
+
*/
|
|
18
|
+
export const fluidOvershoot = {
|
|
19
|
+
duration: 0.4,
|
|
20
|
+
ease: [0.34, 1.56, 0.1, 1] as [number, number, number, number],
|
|
21
|
+
} as const
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Exit motion: content collapses back toward its source.
|
|
25
|
+
* The surface shrinks toward the trigger's position while fading,
|
|
26
|
+
* like a water drop absorbing back into its origin.
|
|
27
|
+
*/
|
|
28
|
+
export const fluidCollapse = {
|
|
29
|
+
duration: 0.28,
|
|
30
|
+
ease: [0.4, 0, 0.6, 1] as [number, number, number, number],
|
|
31
|
+
} as const
|
|
32
|
+
|
|
11
33
|
export type FloatingPanelSide = "above" | "below"
|
|
12
34
|
|
|
13
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Anchored panels settle toward their source without shrinking their text.
|
|
37
|
+
* Now uses origin-based scaling: the panel grows from the trigger's
|
|
38
|
+
* position (transform-origin set by the caller) with a scale + opacity
|
|
39
|
+
* crossfade, rather than a plain slide.
|
|
40
|
+
*/
|
|
14
41
|
export function floatingPanelMotion(side: FloatingPanelSide = "below") {
|
|
15
42
|
const direction = side === "above" ? 1 : -1
|
|
16
43
|
return {
|
|
17
|
-
initial: { opacity: 0, scale:
|
|
44
|
+
initial: { opacity: 0, scale: 0.92, y: direction * 6 },
|
|
18
45
|
animate: { opacity: 1, scale: 1, y: 0 },
|
|
19
|
-
exit: { opacity: 0, scale:
|
|
46
|
+
exit: { opacity: 0, scale: 0.95, y: direction * 4 },
|
|
20
47
|
transition: {
|
|
21
|
-
...
|
|
22
|
-
opacity: { duration: 0.16 },
|
|
48
|
+
...fluidOvershoot,
|
|
49
|
+
opacity: { duration: 0.16, ease: [0.4, 0, 0.2, 1] as [number, number, number, number] },
|
|
23
50
|
},
|
|
24
51
|
} as const
|
|
25
52
|
}
|
|
@@ -27,6 +54,37 @@ export function floatingPanelMotion(side: FloatingPanelSide = "below") {
|
|
|
27
54
|
/** Default below-anchor presentation, retained for existing consumers. */
|
|
28
55
|
export const popoverMotion = floatingPanelMotion()
|
|
29
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Zoom/matched-geometry transition for dialogs and center-placed sheets.
|
|
59
|
+
* The surface starts small (scale 0.88) and invisible, then grows to
|
|
60
|
+
* full size with the fluid overshoot bounce. Content elements inside
|
|
61
|
+
* crossfade in slightly delayed so they phase into visibility
|
|
62
|
+
* asynchronously — not a harsh switch.
|
|
63
|
+
*/
|
|
64
|
+
export const zoomReveal = {
|
|
65
|
+
initial: { opacity: 0, scale: 0.88 },
|
|
66
|
+
animate: { opacity: 1, scale: 1 },
|
|
67
|
+
exit: { opacity: 0, scale: 0.92 },
|
|
68
|
+
transition: fluidOvershoot,
|
|
69
|
+
} as const
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Content crossfade for elements inside an overlay.
|
|
73
|
+
* Rather than a harsh switch, sub-elements scale and phase into
|
|
74
|
+
* visibility asynchronously. Use for children of dialogs, sheets,
|
|
75
|
+
* and popovers — the container arrives first, then content fades in.
|
|
76
|
+
*/
|
|
77
|
+
export const contentCrossfade = {
|
|
78
|
+
initial: { opacity: 0, scale: 0.96 },
|
|
79
|
+
animate: { opacity: 1, scale: 1 },
|
|
80
|
+
exit: { opacity: 0, scale: 0.98 },
|
|
81
|
+
transition: {
|
|
82
|
+
duration: 0.24,
|
|
83
|
+
ease: [0.4, 0, 0.2, 1] as [number, number, number, number],
|
|
84
|
+
delay: 0.04,
|
|
85
|
+
},
|
|
86
|
+
} as const
|
|
87
|
+
|
|
30
88
|
/** Floating bars that rise from the bottom edge. */
|
|
31
89
|
export const riseMotion = {
|
|
32
90
|
initial: { opacity: 0, y: 32 },
|