fancy-ui-react 0.3.0 → 0.4.0
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 +52 -19
- package/dist/components/animated-beam/AnimatedBeam.d.ts +47 -11
- package/dist/components/animated-beam/AnimatedBeam.js +293 -118
- package/dist/components/animated-tooltip/AnimatedTooltip.d.ts +10 -1
- package/dist/components/animated-tooltip/AnimatedTooltip.js +170 -101
- package/dist/components/bento-grid/BentoFrame.d.ts +16 -0
- package/dist/components/bento-grid/BentoFrame.js +28 -0
- package/dist/components/bento-grid/BentoGrid.d.ts +5 -1
- package/dist/components/bento-grid/BentoGrid.js +71 -2
- package/dist/components/bento-grid/BentoGridCard.js +34 -26
- package/dist/components/bento-grid/BentoGridItem.js +10 -10
- package/dist/components/border-beam/BorderBeam.d.ts +55 -10
- package/dist/components/border-beam/BorderBeam.js +93 -16
- package/dist/components/compare/Compare.d.ts +32 -8
- package/dist/components/compare/Compare.js +213 -95
- package/dist/components/container-scroll/ContainerScroll.d.ts +26 -4
- package/dist/components/container-scroll/ContainerScroll.js +88 -49
- package/dist/components/dock/Dock.d.ts +10 -7
- package/dist/components/dock/Dock.js +76 -31
- package/dist/components/dock/DockIcon.d.ts +14 -3
- package/dist/components/dock/DockIcon.js +23 -19
- package/dist/components/dock/DockSeparator.d.ts +4 -1
- package/dist/components/dock/DockSeparator.js +5 -2
- package/dist/components/dock/types.d.ts +13 -6
- package/dist/components/dropdown-menu/menu-shared.d.ts +11 -8
- package/dist/components/editorial-engine/engine.js +1 -14
- package/dist/components/fireworks-hdr/webgl2-renderer.js +0 -19
- package/dist/components/fireworks-hdr/webgpu-renderer.js +3 -22
- package/dist/components/flip-card/FlipCard.d.ts +42 -1
- package/dist/components/flip-card/FlipCard.js +178 -25
- package/dist/components/flip-card/index.d.ts +1 -1
- package/dist/components/fluid-cursor/FluidCursor.js +5 -4
- package/dist/components/fluid-cursor/dev.d.ts +1 -0
- package/dist/components/fluid-cursor/dev.js +8 -0
- package/dist/components/fluid-cursor/fluid-shared.d.ts +0 -2
- package/dist/components/fluid-cursor/fluid-shared.js +0 -2
- package/dist/components/fluid-cursor/webgpu-engine.js +10 -24
- package/dist/components/glow-border/GlowBorder.d.ts +41 -1
- package/dist/components/glow-border/GlowBorder.js +96 -16
- package/dist/components/glow-border/index.d.ts +2 -2
- package/dist/components/gradient-button/GradientButton.d.ts +10 -5
- package/dist/components/gradient-button/GradientButton.js +25 -18
- package/dist/components/image-trail-cursor/ImageTrailCursor.js +1 -0
- package/dist/components/image-trail-cursor/trail-variants.d.ts +37 -1
- package/dist/components/image-trail-cursor/trail-variants.js +331 -8
- package/dist/components/interactive-hover-button/InteractiveHoverButton.js +30 -35
- package/dist/components/marquee/Marquee.d.ts +7 -3
- package/dist/components/marquee/Marquee.js +134 -2
- package/dist/components/marquee/ReviewCard.d.ts +2 -1
- package/dist/components/marquee/ReviewCard.js +20 -9
- package/dist/components/meteors/Meteors.d.ts +32 -10
- package/dist/components/meteors/Meteors.js +53 -20
- package/dist/components/mosaic-glow/mosaic-glow-core.d.ts +98 -10
- package/dist/components/neon-border/NeonBorder.d.ts +24 -5
- package/dist/components/neon-border/NeonBorder.js +37 -7
- package/dist/components/pagination/Pagination.js +220 -82
- package/dist/components/rainbow-button/RainbowButton.js +13 -5
- package/dist/components/ripple-button/RippleButton.d.ts +20 -1
- package/dist/components/ripple-button/RippleButton.js +44 -21
- package/dist/components/shimmer-button/ShimmerButton.d.ts +5 -5
- package/dist/components/shimmer-button/ShimmerButton.js +17 -19
- package/dist/components/smooth-cursor/SmoothCursor.d.ts +13 -9
- package/dist/components/smooth-cursor/SmoothCursor.js +114 -163
- package/dist/components/smooth-cursor/smooth-cursor-core.d.ts +85 -0
- package/dist/components/smooth-cursor/smooth-cursor-core.js +327 -0
- package/dist/components/stepper/Step.js +32 -19
- package/dist/components/stepper/Stepper.js +18 -2
- package/dist/components/stepper/types.d.ts +13 -0
- package/dist/components/timeline/Timeline.d.ts +8 -1
- package/dist/components/timeline/Timeline.js +165 -76
- package/dist/index.js +2 -1
- package/dist/internals/markdown.d.ts +5 -0
- package/dist/internals/motion/haptics.d.ts +0 -4
- package/dist/internals/motion/presence-core.d.ts +59 -0
- package/dist/internals/motion/presence-core.js +162 -0
- package/dist/internals/motion/presence.d.ts +2 -4
- package/dist/internals/motion/presence.js +1 -156
- package/dist/internals/motion/raf.d.ts +1 -1
- package/dist/internals/motion/stagger.d.ts +3 -4
- package/dist/internals/motion/tokens.js +4 -0
- package/dist/internals/motion/transitions.d.ts +17 -17
- package/dist/internals/motion/types.d.ts +6 -7
- package/dist/internals/relative-time.d.ts +1 -1
- package/dist/styles.css +3470 -203
- package/package.json +18 -2
- package/dist/components/fireworks-hdr/webgpu-types.d.ts +0 -159
|
@@ -1,17 +1,54 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
3
|
import { cn } from "../../utils.js";
|
|
4
4
|
/* empty css */
|
|
5
|
+
const TAIL_SEGMENTS = 18;
|
|
6
|
+
const BLOOM_SEGMENTS = 8;
|
|
7
|
+
function tailSegments(count) {
|
|
8
|
+
return Array.from({ length: count }, (_, i) => {
|
|
9
|
+
const t = (i + 0.5) / count;
|
|
10
|
+
return {
|
|
11
|
+
t: Number(t.toFixed(4)),
|
|
12
|
+
alpha: Number(Math.pow(1 - t, 1.6).toFixed(4)),
|
|
13
|
+
thickness: Number((1 - 0.7 * t).toFixed(4))
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function clamp01(n) {
|
|
18
|
+
return Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : 0;
|
|
19
|
+
}
|
|
20
|
+
function beamPhase(duration, delay, anchor, reverse) {
|
|
21
|
+
const d = duration > 0 ? duration : 1;
|
|
22
|
+
const lag = (delay % d + d) % d;
|
|
23
|
+
const offset = lag === 0 ? 0 : -(d - lag);
|
|
24
|
+
const dir = reverse ? -1 : 1;
|
|
25
|
+
const park = anchor - dir * lag * 100 / d;
|
|
26
|
+
return { offset: Number(offset.toFixed(4)), park: Number(park.toFixed(4)) };
|
|
27
|
+
}
|
|
28
|
+
const tailPieces = tailSegments(TAIL_SEGMENTS);
|
|
29
|
+
const bloomPieces = tailSegments(BLOOM_SEGMENTS);
|
|
30
|
+
function piece(p, count) {
|
|
31
|
+
return {
|
|
32
|
+
"--bb-t": p.t,
|
|
33
|
+
"--bb-alpha": p.alpha,
|
|
34
|
+
"--bb-thick": p.thickness,
|
|
35
|
+
"--bb-count": count
|
|
36
|
+
};
|
|
37
|
+
}
|
|
5
38
|
function BorderBeam({
|
|
6
39
|
className,
|
|
7
40
|
size = 200,
|
|
8
|
-
duration =
|
|
41
|
+
duration = 9,
|
|
9
42
|
borderWidth = 1.5,
|
|
10
43
|
anchor = 90,
|
|
11
|
-
colorFrom = "#
|
|
12
|
-
colorTo = "#
|
|
13
|
-
delay = 0
|
|
44
|
+
colorFrom = "#8ec5ff",
|
|
45
|
+
colorTo = "#c084fc",
|
|
46
|
+
delay = 0,
|
|
47
|
+
tail = 0.25,
|
|
48
|
+
glow = 0.6,
|
|
49
|
+
reverse = false
|
|
14
50
|
}) {
|
|
51
|
+
const phase = beamPhase(duration, delay, anchor, reverse);
|
|
15
52
|
const style = {
|
|
16
53
|
"--border-beam-size": size,
|
|
17
54
|
"--border-beam-duration": `${duration}s`,
|
|
@@ -19,27 +56,67 @@ function BorderBeam({
|
|
|
19
56
|
"--border-beam-border-width": borderWidth,
|
|
20
57
|
"--border-beam-color-from": colorFrom,
|
|
21
58
|
"--border-beam-color-to": colorTo,
|
|
22
|
-
"--border-beam-delay": `${delay}s
|
|
59
|
+
"--border-beam-delay": `${delay}s`,
|
|
60
|
+
"--border-beam-tail": clamp01(tail),
|
|
61
|
+
"--border-beam-glow": clamp01(glow),
|
|
62
|
+
"--border-beam-offset": `${phase.offset}s`,
|
|
63
|
+
"--border-beam-park": `${phase.park}%`,
|
|
64
|
+
"--border-beam-dir": reverse ? -1 : 1
|
|
23
65
|
};
|
|
24
|
-
return /* @__PURE__ */
|
|
66
|
+
return /* @__PURE__ */ jsxs(
|
|
25
67
|
"div",
|
|
26
68
|
{
|
|
27
69
|
className: cn(
|
|
28
70
|
"border-beam",
|
|
29
71
|
"pointer-events-none absolute inset-0 rounded-[inherit]",
|
|
30
|
-
"[border:calc(var(--border-beam-border-width)*1px)_solid_transparent]",
|
|
31
|
-
"![mask-composite:intersect] ![mask-clip:padding-box,border-box] [mask:linear-gradient(transparent,transparent),linear-gradient(white,white)]",
|
|
32
|
-
"after:absolute after:aspect-square after:w-[calc(var(--border-beam-size)*1px)]",
|
|
33
|
-
"after:[animation-delay:var(--border-beam-delay)]",
|
|
34
|
-
"after:[background:linear-gradient(to_left,var(--border-beam-color-from),var(--border-beam-color-to),transparent)]",
|
|
35
|
-
"after:[offset-anchor:calc(var(--border-beam-anchor)*1%)_50%]",
|
|
36
|
-
"after:[offset-path:rect(0_auto_auto_0_round_calc(var(--border-beam-size)*1px))]",
|
|
37
72
|
className
|
|
38
73
|
),
|
|
39
|
-
|
|
74
|
+
"data-direction": reverse ? "reverse" : "forward",
|
|
75
|
+
"aria-hidden": "true",
|
|
76
|
+
style,
|
|
77
|
+
children: [
|
|
78
|
+
/* @__PURE__ */ jsx("div", { className: "bb-spill-clip", children: /* @__PURE__ */ jsx("span", { className: "bb-ride bb-spill" }) }),
|
|
79
|
+
/* @__PURE__ */ jsxs("div", { className: "bb-bloom", children: [
|
|
80
|
+
bloomPieces.map((p) => /* @__PURE__ */ jsx(
|
|
81
|
+
"span",
|
|
82
|
+
{
|
|
83
|
+
className: "bb-ride bb-piece bb-bloom-piece",
|
|
84
|
+
style: piece(p, BLOOM_SEGMENTS)
|
|
85
|
+
},
|
|
86
|
+
p.t
|
|
87
|
+
)),
|
|
88
|
+
/* @__PURE__ */ jsx("span", { className: "bb-ride bb-bloom-head" })
|
|
89
|
+
] }),
|
|
90
|
+
/* @__PURE__ */ jsx("div", { className: "bb-halo", children: bloomPieces.map((p) => /* @__PURE__ */ jsx(
|
|
91
|
+
"span",
|
|
92
|
+
{
|
|
93
|
+
className: "bb-ride bb-piece bb-halo-piece",
|
|
94
|
+
style: piece(p, BLOOM_SEGMENTS)
|
|
95
|
+
},
|
|
96
|
+
p.t
|
|
97
|
+
)) }),
|
|
98
|
+
/* @__PURE__ */ jsxs("div", { className: "bb-ring", children: [
|
|
99
|
+
/* @__PURE__ */ jsx("span", { className: "bb-rail" }),
|
|
100
|
+
/* @__PURE__ */ jsx("span", { className: "bb-ride bb-reach" }),
|
|
101
|
+
tailPieces.map((p) => /* @__PURE__ */ jsx(
|
|
102
|
+
"span",
|
|
103
|
+
{
|
|
104
|
+
className: "bb-ride bb-piece bb-tail-piece",
|
|
105
|
+
style: piece(p, TAIL_SEGMENTS)
|
|
106
|
+
},
|
|
107
|
+
p.t
|
|
108
|
+
)),
|
|
109
|
+
/* @__PURE__ */ jsx("span", { className: "bb-ride bb-head" })
|
|
110
|
+
] }),
|
|
111
|
+
/* @__PURE__ */ jsx("span", { className: "bb-ride bb-spark" })
|
|
112
|
+
]
|
|
40
113
|
}
|
|
41
114
|
);
|
|
42
115
|
}
|
|
43
116
|
export {
|
|
44
|
-
|
|
117
|
+
BLOOM_SEGMENTS,
|
|
118
|
+
BorderBeam,
|
|
119
|
+
TAIL_SEGMENTS,
|
|
120
|
+
beamPhase,
|
|
121
|
+
tailSegments
|
|
45
122
|
};
|
|
@@ -1,4 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare - a before/after slider split by a blade of light
|
|
3
|
+
*
|
|
4
|
+
* The seam is a beam: a white-hot core with a chromatic fringe, a glow
|
|
5
|
+
* that spills onto both sides, and pulses of light running along it. Moving
|
|
6
|
+
* the seam leaves a trail behind it that stretches with the speed of the
|
|
7
|
+
* gesture and settles when it stops.
|
|
8
|
+
*/
|
|
1
9
|
import { type ReactNode } from "react";
|
|
10
|
+
export declare const COMPARE_BEAM_COLORS: string[];
|
|
11
|
+
/** Three colours for the beam (left fringe, centre, right fringe), from one to many given. */
|
|
12
|
+
export declare function beamPalette(colors: string[] | undefined): [string, string, string];
|
|
13
|
+
/**
|
|
14
|
+
* One frame of the beam's motion: the trail follows the seam's velocity
|
|
15
|
+
* (px per frame, signed) and decays; the energy eases toward its target
|
|
16
|
+
* and is kicked up by speed.
|
|
17
|
+
*/
|
|
18
|
+
export declare function beamStep(trail: number, energy: number, deltaPx: number, target: number): {
|
|
19
|
+
trail: number;
|
|
20
|
+
energy: number;
|
|
21
|
+
};
|
|
22
|
+
/** Keyboard step for the slider, in %: arrows move 2, Shift+arrows 10, Home/End jump to the ends. */
|
|
23
|
+
export declare function keyStep(key: string, shift: boolean, current: number): number | null;
|
|
2
24
|
export interface CompareProps {
|
|
3
25
|
firstImage?: string;
|
|
4
26
|
secondImage?: string;
|
|
@@ -12,6 +34,15 @@ export interface CompareProps {
|
|
|
12
34
|
showHandlebar?: boolean;
|
|
13
35
|
autoplay?: boolean;
|
|
14
36
|
autoplayDuration?: number;
|
|
37
|
+
/** Beam colours: left fringe, centre, right fringe (one or more colours) */
|
|
38
|
+
beamColors?: string[];
|
|
39
|
+
/** Accessible name of the slider */
|
|
40
|
+
label?: string;
|
|
41
|
+
/**
|
|
42
|
+
* @deprecated Use `label`. Kept so code written against the earlier port
|
|
43
|
+
* keeps its accessible name; `label` wins when both are given.
|
|
44
|
+
*/
|
|
45
|
+
ariaLabel?: string;
|
|
15
46
|
onpercentagechange?: (percentage: number) => void;
|
|
16
47
|
ondragstart?: () => void;
|
|
17
48
|
ondragend?: () => void;
|
|
@@ -20,12 +51,5 @@ export interface CompareProps {
|
|
|
20
51
|
firstContent?: ReactNode;
|
|
21
52
|
secondContent?: ReactNode;
|
|
22
53
|
handle?: ReactNode;
|
|
23
|
-
/**
|
|
24
|
-
* Accessible name for the slider. The element carries `role="slider"` and
|
|
25
|
-
* sits in the tab order, so it needs one — the two images around it are not
|
|
26
|
-
* its label. Overridable because "before and after" only says something when
|
|
27
|
-
* the caller says what changed.
|
|
28
|
-
*/
|
|
29
|
-
ariaLabel?: string;
|
|
30
54
|
}
|
|
31
|
-
export declare function Compare({ firstImage, secondImage, firstImageAlt, secondImageAlt, className, firstContentClass, secondContentClass, initialSliderPercentage, slideMode, showHandlebar, autoplay, autoplayDuration, onpercentagechange, ondragstart, ondragend, onhoverenter, onhoverleave, firstContent, secondContent, handle,
|
|
55
|
+
export declare function Compare({ firstImage, secondImage, firstImageAlt, secondImageAlt, className, firstContentClass, secondContentClass, initialSliderPercentage, slideMode, showHandlebar, autoplay, autoplayDuration, beamColors, label, ariaLabel, onpercentagechange, ondragstart, ondragend, onhoverenter, onhoverleave, firstContent, secondContent, handle, }: CompareProps): import("react").JSX.Element;
|
|
@@ -1,12 +1,46 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
-
import { useRef, useState } from "react";
|
|
3
|
+
import { useRef, useState, useEffect } from "react";
|
|
4
4
|
import { useIsomorphicLayoutEffect } from "../../internals/dom/ssr.js";
|
|
5
5
|
import { useLiveRef } from "../../internals/dom/use-live-ref.js";
|
|
6
|
+
import { useReducedMotion } from "../../internals/motion/media-query.js";
|
|
6
7
|
import { cn } from "../../utils.js";
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
8
|
+
/* empty css */
|
|
9
|
+
const COMPARE_BEAM_COLORS = ["#22d3ee", "#818cf8", "#f472b6"];
|
|
10
|
+
function beamPalette(colors) {
|
|
11
|
+
const list = (colors ?? []).filter(Boolean);
|
|
12
|
+
if (list.length === 0) return COMPARE_BEAM_COLORS;
|
|
13
|
+
if (list.length === 1) return [list[0], list[0], list[0]];
|
|
14
|
+
if (list.length === 2) return [list[0], `color-mix(in srgb, ${list[0]}, ${list[1]})`, list[1]];
|
|
15
|
+
return [list[0], list[Math.floor((list.length - 1) / 2)], list[list.length - 1]];
|
|
16
|
+
}
|
|
17
|
+
function beamStep(trail, energy, deltaPx, target) {
|
|
18
|
+
const nextTrail = Math.max(-140, Math.min(140, trail * 0.86 + deltaPx * 1.6));
|
|
19
|
+
const kick = Math.min(1, Math.abs(deltaPx) / 12);
|
|
20
|
+
const nextEnergy = Math.min(1, energy + (Math.max(target, kick) - energy) * 0.12);
|
|
21
|
+
return { trail: Math.abs(nextTrail) < 0.25 ? 0 : nextTrail, energy: nextEnergy };
|
|
22
|
+
}
|
|
23
|
+
function keyStep(key, shift, current) {
|
|
24
|
+
const step = shift ? 10 : 2;
|
|
25
|
+
switch (key) {
|
|
26
|
+
case "ArrowLeft":
|
|
27
|
+
case "ArrowDown":
|
|
28
|
+
return Math.max(0, current - step);
|
|
29
|
+
case "ArrowRight":
|
|
30
|
+
case "ArrowUp":
|
|
31
|
+
return Math.min(100, current + step);
|
|
32
|
+
case "PageDown":
|
|
33
|
+
return Math.max(0, current - 10);
|
|
34
|
+
case "PageUp":
|
|
35
|
+
return Math.min(100, current + 10);
|
|
36
|
+
case "Home":
|
|
37
|
+
return 0;
|
|
38
|
+
case "End":
|
|
39
|
+
return 100;
|
|
40
|
+
default:
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
10
44
|
function Compare({
|
|
11
45
|
firstImage = "",
|
|
12
46
|
secondImage = "",
|
|
@@ -20,6 +54,9 @@ function Compare({
|
|
|
20
54
|
showHandlebar = true,
|
|
21
55
|
autoplay = false,
|
|
22
56
|
autoplayDuration = 5e3,
|
|
57
|
+
beamColors,
|
|
58
|
+
label,
|
|
59
|
+
ariaLabel,
|
|
23
60
|
onpercentagechange,
|
|
24
61
|
ondragstart,
|
|
25
62
|
ondragend,
|
|
@@ -27,28 +64,51 @@ function Compare({
|
|
|
27
64
|
onhoverleave,
|
|
28
65
|
firstContent,
|
|
29
66
|
secondContent,
|
|
30
|
-
handle
|
|
31
|
-
ariaLabel = "Image comparison slider"
|
|
67
|
+
handle
|
|
32
68
|
}) {
|
|
69
|
+
const accessibleName = label ?? ariaLabel ?? "Comparison slider";
|
|
70
|
+
const reducedMotion = useReducedMotion();
|
|
71
|
+
const reducedMotionRef = useLiveRef(reducedMotion);
|
|
33
72
|
const sliderRef = useRef(null);
|
|
34
|
-
const [sliderXPercent,
|
|
35
|
-
const
|
|
73
|
+
const [sliderXPercent, setSliderXPercentState] = useState(initialSliderPercentage);
|
|
74
|
+
const percentRef = useRef(initialSliderPercentage);
|
|
75
|
+
const [isDragging, setIsDraggingState] = useState(false);
|
|
36
76
|
const isDraggingRef = useRef(false);
|
|
77
|
+
const [isMouseOver, setIsMouseOverState] = useState(false);
|
|
37
78
|
const isMouseOverRef = useRef(false);
|
|
79
|
+
const [isInteracting, setIsInteractingState] = useState(false);
|
|
80
|
+
const isInteractingRef = useRef(false);
|
|
38
81
|
const autoplayRAF = useRef(null);
|
|
82
|
+
const returnRAF = useRef(null);
|
|
39
83
|
const moveRAF = useRef(null);
|
|
84
|
+
const [trail, setTrail] = useState(0);
|
|
85
|
+
const [energy, setEnergy] = useState(0);
|
|
86
|
+
const trailRef = useRef(0);
|
|
87
|
+
const energyRef = useRef(0);
|
|
88
|
+
const beamRAF = useRef(null);
|
|
89
|
+
const lastPx = useRef(null);
|
|
40
90
|
const onPercentageChangeRef = useLiveRef(onpercentagechange);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
91
|
+
const palette = beamPalette(beamColors);
|
|
92
|
+
function setSliderX(p) {
|
|
93
|
+
percentRef.current = p;
|
|
94
|
+
setSliderXPercentState(p);
|
|
46
95
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
96
|
+
function setIsDragging(v) {
|
|
97
|
+
isDraggingRef.current = v;
|
|
98
|
+
setIsDraggingState(v);
|
|
99
|
+
}
|
|
100
|
+
function setIsMouseOver(v) {
|
|
101
|
+
isMouseOverRef.current = v;
|
|
102
|
+
setIsMouseOverState(v);
|
|
103
|
+
}
|
|
104
|
+
function setIsInteracting(v) {
|
|
105
|
+
isInteractingRef.current = v;
|
|
106
|
+
setIsInteractingState(v);
|
|
107
|
+
}
|
|
108
|
+
function setPercent(p) {
|
|
109
|
+
const next = Math.max(0, Math.min(100, p));
|
|
110
|
+
setSliderX(next);
|
|
111
|
+
onPercentageChangeRef.current?.(next);
|
|
52
112
|
}
|
|
53
113
|
function startAutoplay() {
|
|
54
114
|
if (!autoplay || isMouseOverRef.current || isDraggingRef.current) return;
|
|
@@ -60,38 +120,74 @@ function Compare({
|
|
|
60
120
|
}
|
|
61
121
|
const elapsedTime = Date.now() - startTime;
|
|
62
122
|
const progress = elapsedTime % (autoplayDuration * 2) / autoplayDuration;
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
123
|
+
const linear = progress <= 1 ? progress : 2 - progress;
|
|
124
|
+
const eased = linear < 0.5 ? 4 * linear ** 3 : 1 - (-2 * linear + 2) ** 3 / 2;
|
|
125
|
+
setPercent(eased * 100);
|
|
66
126
|
autoplayRAF.current = requestAnimationFrame(animate);
|
|
67
127
|
}
|
|
68
128
|
animate();
|
|
69
129
|
}
|
|
130
|
+
function stopAutoplay() {
|
|
131
|
+
if (autoplayRAF.current) {
|
|
132
|
+
cancelAnimationFrame(autoplayRAF.current);
|
|
133
|
+
autoplayRAF.current = null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function stopReturn() {
|
|
137
|
+
if (returnRAF.current) {
|
|
138
|
+
cancelAnimationFrame(returnRAF.current);
|
|
139
|
+
returnRAF.current = null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function stopMoveFrame() {
|
|
143
|
+
if (moveRAF.current) {
|
|
144
|
+
cancelAnimationFrame(moveRAF.current);
|
|
145
|
+
moveRAF.current = null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function glideTo(target) {
|
|
149
|
+
stopReturn();
|
|
150
|
+
if (reducedMotionRef.current) {
|
|
151
|
+
setPercent(target);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const from = percentRef.current;
|
|
155
|
+
const start = performance.now();
|
|
156
|
+
const duration = 520;
|
|
157
|
+
const step = (now) => {
|
|
158
|
+
const k = Math.min(1, (now - start) / duration);
|
|
159
|
+
const e = 1 - (1 - k) ** 3;
|
|
160
|
+
setPercent(from + (target - from) * e);
|
|
161
|
+
returnRAF.current = k < 1 ? requestAnimationFrame(step) : null;
|
|
162
|
+
};
|
|
163
|
+
returnRAF.current = requestAnimationFrame(step);
|
|
164
|
+
}
|
|
70
165
|
function mouseEnterHandler() {
|
|
71
|
-
|
|
166
|
+
setIsMouseOver(true);
|
|
167
|
+
stopReturn();
|
|
72
168
|
onhoverenter?.();
|
|
73
169
|
if (autoplay) {
|
|
74
170
|
stopAutoplay();
|
|
75
171
|
}
|
|
76
172
|
}
|
|
77
173
|
function mouseLeaveHandler() {
|
|
78
|
-
|
|
174
|
+
setIsMouseOver(false);
|
|
79
175
|
setIsInteracting(false);
|
|
80
176
|
onhoverleave?.();
|
|
81
177
|
if (slideMode === "hover") {
|
|
82
|
-
|
|
83
|
-
onpercentagechange?.(initialSliderPercentage);
|
|
178
|
+
glideTo(initialSliderPercentage);
|
|
84
179
|
}
|
|
85
180
|
if (slideMode === "drag") {
|
|
86
|
-
|
|
181
|
+
setIsDragging(false);
|
|
87
182
|
}
|
|
88
183
|
if (autoplay) {
|
|
184
|
+
stopReturn();
|
|
89
185
|
startAutoplay();
|
|
90
186
|
}
|
|
91
187
|
}
|
|
92
188
|
function handleStart() {
|
|
93
189
|
if (slideMode === "drag") {
|
|
94
|
-
|
|
190
|
+
setIsDragging(true);
|
|
95
191
|
setIsInteracting(true);
|
|
96
192
|
ondragstart?.();
|
|
97
193
|
stopAutoplay();
|
|
@@ -99,7 +195,7 @@ function Compare({
|
|
|
99
195
|
}
|
|
100
196
|
function handleEnd() {
|
|
101
197
|
if (slideMode === "drag") {
|
|
102
|
-
|
|
198
|
+
setIsDragging(false);
|
|
103
199
|
setIsInteracting(false);
|
|
104
200
|
ondragend?.();
|
|
105
201
|
if (autoplay && !isMouseOverRef.current) {
|
|
@@ -107,56 +203,22 @@ function Compare({
|
|
|
107
203
|
}
|
|
108
204
|
}
|
|
109
205
|
}
|
|
110
|
-
function commitPercent(percent) {
|
|
111
|
-
const newPercent = Math.max(0, Math.min(100, percent));
|
|
112
|
-
setSliderXPercent(newPercent);
|
|
113
|
-
onpercentagechange?.(newPercent);
|
|
114
|
-
}
|
|
115
206
|
function handleMove(clientX) {
|
|
116
207
|
if (!sliderRef.current) return;
|
|
117
208
|
if (slideMode === "hover" || slideMode === "drag" && isDraggingRef.current) {
|
|
118
209
|
setIsInteracting(true);
|
|
119
210
|
stopAutoplay();
|
|
211
|
+
stopReturn();
|
|
120
212
|
const rect = sliderRef.current.getBoundingClientRect();
|
|
121
213
|
const x = clientX - rect.left;
|
|
122
214
|
const percent = x / rect.width * 100;
|
|
123
215
|
stopMoveFrame();
|
|
124
216
|
moveRAF.current = requestAnimationFrame(() => {
|
|
125
217
|
moveRAF.current = null;
|
|
126
|
-
|
|
218
|
+
setPercent(percent);
|
|
127
219
|
});
|
|
128
220
|
}
|
|
129
221
|
}
|
|
130
|
-
function handleKeyDown(event) {
|
|
131
|
-
let next;
|
|
132
|
-
switch (event.key) {
|
|
133
|
-
case "ArrowLeft":
|
|
134
|
-
case "ArrowDown":
|
|
135
|
-
next = sliderXPercent - KEYBOARD_STEP;
|
|
136
|
-
break;
|
|
137
|
-
case "ArrowRight":
|
|
138
|
-
case "ArrowUp":
|
|
139
|
-
next = sliderXPercent + KEYBOARD_STEP;
|
|
140
|
-
break;
|
|
141
|
-
case "PageDown":
|
|
142
|
-
next = sliderXPercent - KEYBOARD_PAGE_STEP;
|
|
143
|
-
break;
|
|
144
|
-
case "PageUp":
|
|
145
|
-
next = sliderXPercent + KEYBOARD_PAGE_STEP;
|
|
146
|
-
break;
|
|
147
|
-
case "Home":
|
|
148
|
-
next = 0;
|
|
149
|
-
break;
|
|
150
|
-
case "End":
|
|
151
|
-
next = 100;
|
|
152
|
-
break;
|
|
153
|
-
default:
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
event.preventDefault();
|
|
157
|
-
stopAutoplay();
|
|
158
|
-
commitPercent(next);
|
|
159
|
-
}
|
|
160
222
|
function handleMouseDown() {
|
|
161
223
|
handleStart();
|
|
162
224
|
}
|
|
@@ -173,29 +235,73 @@ function Compare({
|
|
|
173
235
|
const touch = e.touches[0];
|
|
174
236
|
if (!autoplay && touch) handleMove(touch.clientX);
|
|
175
237
|
}
|
|
238
|
+
function handleKeyDown(e) {
|
|
239
|
+
const next = keyStep(e.key, e.shiftKey, percentRef.current);
|
|
240
|
+
if (next === null) return;
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
stopAutoplay();
|
|
243
|
+
stopReturn();
|
|
244
|
+
setPercent(next);
|
|
245
|
+
}
|
|
246
|
+
function wakeBeam() {
|
|
247
|
+
if (beamRAF.current !== null || reducedMotionRef.current) return;
|
|
248
|
+
const tick = () => {
|
|
249
|
+
const width = sliderRef.current?.clientWidth ?? 0;
|
|
250
|
+
const px = percentRef.current / 100 * width;
|
|
251
|
+
const delta = lastPx.current === null ? 0 : px - lastPx.current;
|
|
252
|
+
lastPx.current = px;
|
|
253
|
+
const target = isInteractingRef.current || isDraggingRef.current ? 1 : isMouseOverRef.current ? 0.7 : 0;
|
|
254
|
+
const next = beamStep(trailRef.current, energyRef.current, delta, target);
|
|
255
|
+
trailRef.current = next.trail;
|
|
256
|
+
energyRef.current = next.energy;
|
|
257
|
+
setTrail(next.trail);
|
|
258
|
+
setEnergy(next.energy);
|
|
259
|
+
const settled = next.trail === 0 && delta === 0 && Math.abs(next.energy - target) < 0.01;
|
|
260
|
+
beamRAF.current = settled ? null : requestAnimationFrame(tick);
|
|
261
|
+
};
|
|
262
|
+
beamRAF.current = requestAnimationFrame(tick);
|
|
263
|
+
}
|
|
176
264
|
useIsomorphicLayoutEffect(() => {
|
|
177
|
-
|
|
265
|
+
setSliderX(initialSliderPercentage);
|
|
178
266
|
}, [initialSliderPercentage]);
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
wakeBeam();
|
|
269
|
+
}, [sliderXPercent, isInteracting, isMouseOver]);
|
|
179
270
|
useIsomorphicLayoutEffect(() => {
|
|
180
271
|
if (autoplay && !isMouseOverRef.current && !isDraggingRef.current) {
|
|
181
272
|
startAutoplay();
|
|
182
273
|
} else {
|
|
183
274
|
stopAutoplay();
|
|
184
275
|
}
|
|
185
|
-
return
|
|
276
|
+
return stopAutoplay;
|
|
277
|
+
}, [autoplay, autoplayDuration]);
|
|
278
|
+
useEffect(
|
|
279
|
+
() => () => {
|
|
186
280
|
stopAutoplay();
|
|
281
|
+
stopReturn();
|
|
187
282
|
stopMoveFrame();
|
|
188
|
-
|
|
189
|
-
|
|
283
|
+
if (beamRAF.current !== null) cancelAnimationFrame(beamRAF.current);
|
|
284
|
+
beamRAF.current = null;
|
|
285
|
+
},
|
|
286
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
287
|
+
[]
|
|
288
|
+
);
|
|
289
|
+
const rootStyle = {
|
|
290
|
+
position: "relative",
|
|
291
|
+
cursor: slideMode === "drag" ? isDragging ? "grabbing" : "grab" : "col-resize",
|
|
292
|
+
"--cmp-c1": palette[0],
|
|
293
|
+
"--cmp-c2": palette[1],
|
|
294
|
+
"--cmp-c3": palette[2],
|
|
295
|
+
"--cmp-energy": energy.toFixed(3),
|
|
296
|
+
"--cmp-trail": trail.toFixed(1)
|
|
297
|
+
};
|
|
190
298
|
return /* @__PURE__ */ jsxs(
|
|
191
299
|
"div",
|
|
192
300
|
{
|
|
193
301
|
ref: sliderRef,
|
|
194
|
-
className: cn("h-[400px] w-[400px] overflow-hidden", className),
|
|
195
|
-
style:
|
|
196
|
-
|
|
197
|
-
cursor: slideMode === "drag" ? "grab" : "col-resize"
|
|
198
|
-
},
|
|
302
|
+
className: cn("compare h-[400px] w-[400px] overflow-hidden", className),
|
|
303
|
+
style: rootStyle,
|
|
304
|
+
"data-active": isInteracting || isDragging ? "" : void 0,
|
|
199
305
|
onMouseMove: handleMouseMove,
|
|
200
306
|
onMouseLeave: mouseLeaveHandler,
|
|
201
307
|
onMouseEnter: mouseEnterHandler,
|
|
@@ -206,41 +312,49 @@ function Compare({
|
|
|
206
312
|
onTouchMove: handleTouchMove,
|
|
207
313
|
onKeyDown: handleKeyDown,
|
|
208
314
|
role: "slider",
|
|
209
|
-
"aria-label":
|
|
210
|
-
"aria-valuenow": sliderXPercent,
|
|
315
|
+
"aria-label": accessibleName,
|
|
316
|
+
"aria-valuenow": Math.round(sliderXPercent),
|
|
317
|
+
"aria-valuetext": `${Math.round(sliderXPercent)}% ${firstImageAlt}`,
|
|
211
318
|
"aria-valuemin": 0,
|
|
212
319
|
"aria-valuemax": 100,
|
|
320
|
+
"aria-orientation": "horizontal",
|
|
213
321
|
tabIndex: 0,
|
|
214
322
|
children: [
|
|
215
323
|
/* @__PURE__ */ jsxs(
|
|
216
324
|
"div",
|
|
217
325
|
{
|
|
218
|
-
className: "pointer-events-none absolute top-0 z-40
|
|
326
|
+
className: "compare-beam pointer-events-none absolute top-0 z-40 h-full w-px",
|
|
219
327
|
style: { left: `${sliderXPercent}%` },
|
|
328
|
+
"aria-hidden": "true",
|
|
220
329
|
children: [
|
|
221
|
-
/* @__PURE__ */ jsx("
|
|
222
|
-
/* @__PURE__ */ jsx("
|
|
223
|
-
/* @__PURE__ */ jsx("
|
|
224
|
-
|
|
330
|
+
/* @__PURE__ */ jsx("span", { className: "compare-beam__glow" }),
|
|
331
|
+
/* @__PURE__ */ jsx("span", { className: "compare-beam__trail" }),
|
|
332
|
+
/* @__PURE__ */ jsx("span", { className: "compare-beam__fringe" }),
|
|
333
|
+
/* @__PURE__ */ jsx("span", { className: "compare-beam__core" }),
|
|
334
|
+
/* @__PURE__ */ jsx("span", { className: "compare-beam__pulse" }),
|
|
335
|
+
/* @__PURE__ */ jsx("span", { className: "compare-beam__pulse compare-beam__pulse--late" })
|
|
336
|
+
]
|
|
337
|
+
}
|
|
338
|
+
),
|
|
339
|
+
/* @__PURE__ */ jsx(
|
|
340
|
+
"div",
|
|
341
|
+
{
|
|
342
|
+
className: "compare-seam pointer-events-none absolute top-0 z-[41] h-full w-px",
|
|
343
|
+
style: { left: `${sliderXPercent}%` },
|
|
344
|
+
children: handle ? handle : showHandlebar ? /* @__PURE__ */ jsxs("div", { className: "compare-handle pointer-events-auto", "aria-hidden": "true", children: [
|
|
345
|
+
/* @__PURE__ */ jsx("span", { className: "compare-handle__ring" }),
|
|
346
|
+
/* @__PURE__ */ jsx(
|
|
225
347
|
"svg",
|
|
226
348
|
{
|
|
227
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
228
|
-
fill: "none",
|
|
229
349
|
viewBox: "0 0 24 24",
|
|
230
|
-
|
|
350
|
+
fill: "none",
|
|
231
351
|
stroke: "currentColor",
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
{
|
|
236
|
-
strokeLinecap: "round",
|
|
237
|
-
strokeLinejoin: "round",
|
|
238
|
-
d: "M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 12.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 18.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z"
|
|
239
|
-
}
|
|
240
|
-
)
|
|
352
|
+
strokeWidth: "2",
|
|
353
|
+
className: "size-4",
|
|
354
|
+
children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M9 7 4 12l5 5M15 7l5 5-5 5" })
|
|
241
355
|
}
|
|
242
|
-
)
|
|
243
|
-
]
|
|
356
|
+
)
|
|
357
|
+
] }) : null
|
|
244
358
|
}
|
|
245
359
|
),
|
|
246
360
|
/* @__PURE__ */ jsx(
|
|
@@ -296,5 +410,9 @@ function Compare({
|
|
|
296
410
|
);
|
|
297
411
|
}
|
|
298
412
|
export {
|
|
299
|
-
|
|
413
|
+
COMPARE_BEAM_COLORS,
|
|
414
|
+
Compare,
|
|
415
|
+
beamPalette,
|
|
416
|
+
beamStep,
|
|
417
|
+
keyStep
|
|
300
418
|
};
|
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* ContainerScroll - a scroll-driven aperture
|
|
4
|
+
*
|
|
5
|
+
* The card starts shut: a slim horizontal slit with a bright seam of light
|
|
6
|
+
* across its middle. As the section scrolls into view the slit opens
|
|
7
|
+
* vertically to the full card, the seam splits into two lips that ride the
|
|
8
|
+
* opening edges and fade, the content settles from a slight zoom into
|
|
9
|
+
* focus, and the title above blurs away to hand the stage to the card.
|
|
10
|
+
* No 3D tilt, no perspective.
|
|
11
|
+
*/
|
|
2
12
|
export interface ContainerScrollProps {
|
|
3
|
-
/** Additional CSS classes */
|
|
13
|
+
/** Additional CSS classes on the section */
|
|
4
14
|
className?: string;
|
|
5
|
-
/**
|
|
15
|
+
/** Title shown above the card (blurs and fades as the card opens) */
|
|
6
16
|
titleContent?: ReactNode;
|
|
7
|
-
/** Content
|
|
17
|
+
/** Content revealed inside the card */
|
|
8
18
|
cardContent?: ReactNode;
|
|
19
|
+
/** Seam colour (any CSS colour). Defaults to a soft blue. */
|
|
20
|
+
accent?: string;
|
|
21
|
+
/** Second seam tint, blended towards the seam ends. Defaults to a soft lilac. */
|
|
22
|
+
accentSecondary?: string;
|
|
9
23
|
}
|
|
10
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Raw scroll progress of the card track, measured at its centre (where the
|
|
26
|
+
* slit is): 0 while the slit is still in the bottom 8% of the viewport,
|
|
27
|
+
* 1 once it has risen to just above the middle of the viewport.
|
|
28
|
+
*/
|
|
29
|
+
export declare function apertureProgress(top: number, height: number, viewport: number): number;
|
|
30
|
+
/** Smoothstep: a gentle start and a slow settle, so the opening reads as a lens, not a shutter. */
|
|
31
|
+
export declare function apertureEase(p: number): number;
|
|
32
|
+
export declare function ContainerScroll({ className, titleContent, cardContent, accent, accentSecondary, }: ContainerScrollProps): import("react").JSX.Element;
|