@androbinco/library-cli 0.1.0 → 0.2.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 +86 -37
- package/package.json +11 -16
- package/src/index.js +70 -4
- package/src/templates/in-view/data.in-view.ts +89 -0
- package/src/templates/in-view/examples/in-view-examples.home.tsx +101 -0
- package/src/templates/in-view/examples/in-view-grid-showcase.tsx +41 -0
- package/src/templates/in-view/in-view-animation.tsx +72 -0
- package/src/templates/in-view/in-view-grid.tsx +79 -0
- package/src/templates/in-view/in-view-hidden-text.tsx +45 -0
- package/src/templates/in-view/in-view-stroke-line.tsx +29 -0
- package/src/templates/scroll-tracker/examples/scroll-tracker-showcase.tsx +90 -0
- package/src/templates/scroll-tracker/scroll-tracker-provider.tsx +78 -0
- package/src/templates/ticker/css-ticker/css-ticker.tsx +61 -0
- package/src/templates/ticker/css-ticker/ticker.keyframes.css +86 -0
- package/src/templates/ticker/examples/ticker-hover-showcase.home.tsx +57 -0
- package/src/templates/ticker/examples/ticker-static-showcase.home.tsx +56 -0
- package/src/templates/ticker/hooks/use-ticker-clones.tsx +70 -0
- package/src/templates/ticker/hooks/use-ticker-incremental.tsx +72 -0
- package/src/templates/ticker/motion-ticker.tsx +93 -0
- package/src/utils/components.js +406 -55
- package/src/utils/files.js +88 -5
- package/src/templates/button/button.tsx +0 -5
- package/src/templates/card/card.tsx +0 -5
- package/src/templates/example/example.tsx +0 -5
- package/src/templates/hero/hero.tsx +0 -5
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Transition } from "motion/react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/common/utils/classname-builder";
|
|
4
|
+
|
|
5
|
+
import InViewAnimation, { InViewAnimationProps } from "./in-view-animation";
|
|
6
|
+
|
|
7
|
+
export const InViewHiddenText = ({
|
|
8
|
+
children,
|
|
9
|
+
className,
|
|
10
|
+
transition = {
|
|
11
|
+
duration: 0.6,
|
|
12
|
+
ease: "easeOut",
|
|
13
|
+
},
|
|
14
|
+
...props
|
|
15
|
+
}: {
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
className?: string;
|
|
18
|
+
transition?: Transition;
|
|
19
|
+
props?: InViewAnimationProps;
|
|
20
|
+
}) => {
|
|
21
|
+
return (
|
|
22
|
+
<div className={cn("overflow-hidden", className)}>
|
|
23
|
+
<InViewAnimation
|
|
24
|
+
className={cn("origin-left overflow-hidden", className)}
|
|
25
|
+
customEffect={{
|
|
26
|
+
hidden: {
|
|
27
|
+
y: "99%",
|
|
28
|
+
opacity: 0,
|
|
29
|
+
rotate: 5,
|
|
30
|
+
},
|
|
31
|
+
visible: {
|
|
32
|
+
y: 0,
|
|
33
|
+
opacity: 1,
|
|
34
|
+
rotate: 0,
|
|
35
|
+
},
|
|
36
|
+
}}
|
|
37
|
+
margin="0%"
|
|
38
|
+
transition={transition}
|
|
39
|
+
{...props}
|
|
40
|
+
>
|
|
41
|
+
{children}
|
|
42
|
+
</InViewAnimation>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Transition } from "motion/react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/common/utils/classname-builder";
|
|
4
|
+
|
|
5
|
+
import InViewAnimation, { InViewAnimationProps } from "./in-view-animation";
|
|
6
|
+
|
|
7
|
+
export const InViewStrokeLine = ({
|
|
8
|
+
className,
|
|
9
|
+
transition,
|
|
10
|
+
...props
|
|
11
|
+
}: {
|
|
12
|
+
className?: string;
|
|
13
|
+
} & InViewAnimationProps) => {
|
|
14
|
+
return (
|
|
15
|
+
<InViewAnimation
|
|
16
|
+
aria-label="separator"
|
|
17
|
+
aria-roledescription="separator"
|
|
18
|
+
className={cn("h-px w-full bg-red-950", className)}
|
|
19
|
+
effect="strokeLine"
|
|
20
|
+
role="separator"
|
|
21
|
+
transition={{
|
|
22
|
+
duration: 0.6,
|
|
23
|
+
ease: "easeInOut",
|
|
24
|
+
...transition,
|
|
25
|
+
}}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { motion, MotionValue, useSpring, useTransform } from "motion/react";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ScrollTrackerProvider,
|
|
6
|
+
useScrollTrackerContext,
|
|
7
|
+
} from "../scroll-tracker-provider";
|
|
8
|
+
import { Center } from "@/common/ui/containers/center";
|
|
9
|
+
import { Text } from "@/common/ui/text/text";
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
This is a scroll tracker component that provides scroll-driven animations.
|
|
13
|
+
It uses the motion library to track scroll progress and provides a context
|
|
14
|
+
for child components to access the scroll progress.
|
|
15
|
+
|
|
16
|
+
To test it you can just use it like this in any page:
|
|
17
|
+
<ScrollTrackerShowcase />
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const AnimatedText = ({
|
|
21
|
+
text,
|
|
22
|
+
motionValue,
|
|
23
|
+
offsetIntro,
|
|
24
|
+
opacityOffset = [0, 0.5, 1],
|
|
25
|
+
yOffset = [100, -20, 0],
|
|
26
|
+
}: {
|
|
27
|
+
text: string;
|
|
28
|
+
motionValue: MotionValue<number>;
|
|
29
|
+
offsetIntro: [number, number] | [number, number, number];
|
|
30
|
+
opacityOffset?: [number, number] | [number, number, number];
|
|
31
|
+
yOffset?: [number, number] | [number, number, number];
|
|
32
|
+
}) => {
|
|
33
|
+
const animateEntry = useTransform(motionValue, offsetIntro, opacityOffset);
|
|
34
|
+
const y = useSpring(useTransform(motionValue, offsetIntro, yOffset));
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<motion.div style={{ opacity: animateEntry, y }}>
|
|
38
|
+
<Text className="text-white">{text}</Text>
|
|
39
|
+
</motion.div>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const ScrollSection = () => {
|
|
44
|
+
const { scrollYProgress } = useScrollTrackerContext();
|
|
45
|
+
const text = "SCROLL DRIVEN ANIMATION".split("");
|
|
46
|
+
const showImage = useTransform(scrollYProgress, [0.3, 1 * 0.9], [0, 1]);
|
|
47
|
+
const rotateImage = useTransform(scrollYProgress, [0.3, 1 * 0.9], [0, 360]);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="relative flex h-full w-full flex-col items-center justify-center rounded-2xl border-2 border-fill-brand-primary bg-bg-primary-inverse p-2">
|
|
51
|
+
<motion.div
|
|
52
|
+
className="absolute top-0 h-2 w-full bg-red-500"
|
|
53
|
+
style={{ scaleX: scrollYProgress, transformOrigin: "left center" }}
|
|
54
|
+
/>
|
|
55
|
+
<div className="flex flex-col gap-2">
|
|
56
|
+
<div className="flex flex-row gap-0.5">
|
|
57
|
+
{text.map((char, index) => (
|
|
58
|
+
<AnimatedText
|
|
59
|
+
key={index}
|
|
60
|
+
motionValue={scrollYProgress}
|
|
61
|
+
offsetIntro={[
|
|
62
|
+
((index + 1) / text.length) * 0.3,
|
|
63
|
+
((index + 1) / text.length) * 0.5,
|
|
64
|
+
((index + 1) / text.length) * 0.9,
|
|
65
|
+
]}
|
|
66
|
+
text={char}
|
|
67
|
+
/>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
<motion.img
|
|
71
|
+
alt="scroll-tracker-showcase"
|
|
72
|
+
className="mx-auto size-40 object-contain"
|
|
73
|
+
src="/img/good-emoji.png"
|
|
74
|
+
style={{ opacity: showImage, rotate: rotateImage }}
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const ScrollTrackerShowcase = () => {
|
|
82
|
+
return (
|
|
83
|
+
<div className="w-full">
|
|
84
|
+
<Text className="mb-4">Scroll Tracker</Text>
|
|
85
|
+
<ScrollTrackerProvider height={200} offset={["0 0", "1 1"]}>
|
|
86
|
+
<ScrollSection />
|
|
87
|
+
</ScrollTrackerProvider>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useRef } from "react";
|
|
4
|
+
|
|
5
|
+
import { MotionValue, useScroll } from "motion/react";
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/common/utils/classname-builder";
|
|
8
|
+
|
|
9
|
+
type Offset =
|
|
10
|
+
| [number, number]
|
|
11
|
+
| [`${number} ${number}`, `${number} ${number}`]
|
|
12
|
+
| [`${number}`, `${number}`];
|
|
13
|
+
|
|
14
|
+
type ScrollProviderProps = {
|
|
15
|
+
height?: number;
|
|
16
|
+
mobileHeight?: string;
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
offset?: Offset;
|
|
19
|
+
className?: string;
|
|
20
|
+
containerClassName?: string;
|
|
21
|
+
};
|
|
22
|
+
type ScrollTrackerContextType = {
|
|
23
|
+
scrollYProgress: MotionValue<number>;
|
|
24
|
+
};
|
|
25
|
+
const ScrollTrackerContext = createContext<ScrollTrackerContextType>({
|
|
26
|
+
scrollYProgress: {} as MotionValue<number>,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const ScrollTrackerProvider = ({
|
|
30
|
+
height = 500,
|
|
31
|
+
mobileHeight,
|
|
32
|
+
children,
|
|
33
|
+
className,
|
|
34
|
+
containerClassName,
|
|
35
|
+
offset = ["0 0", "1 1"],
|
|
36
|
+
}: ScrollProviderProps) => {
|
|
37
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
38
|
+
const { scrollYProgress } = useScroll({ target: ref, offset });
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<ScrollTrackerContext.Provider value={{ scrollYProgress }}>
|
|
42
|
+
<div
|
|
43
|
+
ref={ref}
|
|
44
|
+
className={cn(
|
|
45
|
+
"h-[var(--tracker-mobile-height)] w-full lg:h-[var(--tracker-height)]",
|
|
46
|
+
containerClassName
|
|
47
|
+
)}
|
|
48
|
+
style={
|
|
49
|
+
{
|
|
50
|
+
"--tracker-height": `${height}vh`,
|
|
51
|
+
"--tracker-mobile-height": `${mobileHeight ?? height}vh`,
|
|
52
|
+
} as React.CSSProperties
|
|
53
|
+
}
|
|
54
|
+
>
|
|
55
|
+
<div
|
|
56
|
+
className={cn(
|
|
57
|
+
"sticky top-0 right-0 left-0 h-screen w-full",
|
|
58
|
+
className
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
{children}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</ScrollTrackerContext.Provider>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const useScrollTrackerContext = () => {
|
|
69
|
+
const context = useContext(ScrollTrackerContext);
|
|
70
|
+
|
|
71
|
+
if (!context) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"useScrollTracker must be used within a ScrollTrackerProvider"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return context;
|
|
78
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Fragment, useRef } from 'react';
|
|
3
|
+
|
|
4
|
+
import { cn } from '@/common/utils/classname-builder';
|
|
5
|
+
|
|
6
|
+
import { useTickerClones } from '../hooks/use-ticker-clones';
|
|
7
|
+
import './ticker.keyframes.css';
|
|
8
|
+
|
|
9
|
+
export const TickerStatic = ({
|
|
10
|
+
direction = 'right',
|
|
11
|
+
children,
|
|
12
|
+
speed = 50,
|
|
13
|
+
}: {
|
|
14
|
+
direction?: 'left' | 'right';
|
|
15
|
+
speed?: number;
|
|
16
|
+
delta?: number;
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
}) => {
|
|
19
|
+
const tickerClone = useRef<HTMLDivElement>(null);
|
|
20
|
+
const tickerCard = useRef<HTMLDivElement>(null);
|
|
21
|
+
|
|
22
|
+
const { clonesNeeded } = useTickerClones({
|
|
23
|
+
tickerCard,
|
|
24
|
+
tickerClone,
|
|
25
|
+
direction,
|
|
26
|
+
onCloneWrapper(clone) {
|
|
27
|
+
clone.classList.remove('animate-base-ticker');
|
|
28
|
+
clone.classList.remove('animate-base-ticker-left');
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const baseTickerClass = cn(
|
|
33
|
+
'flex w-max flex-row',
|
|
34
|
+
direction === 'right' ? 'animate-base-ticker' : 'animate-base-ticker-left',
|
|
35
|
+
);
|
|
36
|
+
const clonedTickerClass = cn(
|
|
37
|
+
'absolute top-0 h-full w-max',
|
|
38
|
+
direction === 'right' ? 'animate-cloned-ticker' : 'animate-cloned-ticker-left',
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="ticker group w-full overflow-hidden" role="group">
|
|
43
|
+
<div className="relative w-full">
|
|
44
|
+
<div
|
|
45
|
+
ref={tickerCard}
|
|
46
|
+
className={cn(baseTickerClass)}
|
|
47
|
+
style={{ '--ticker-speed': `${speed}s` } as React.CSSProperties}
|
|
48
|
+
>
|
|
49
|
+
{Array.from({ length: clonesNeeded }).map((_, index) => {
|
|
50
|
+
return <Fragment key={index}>{children}</Fragment>;
|
|
51
|
+
})}
|
|
52
|
+
</div>
|
|
53
|
+
<div
|
|
54
|
+
ref={tickerClone}
|
|
55
|
+
className={cn(clonedTickerClass)}
|
|
56
|
+
style={{ '--ticker-speed': `${speed}s` } as React.CSSProperties}
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
.ticker {
|
|
2
|
+
--ticker-speed: 20s;
|
|
3
|
+
}
|
|
4
|
+
.ticker-slow {
|
|
5
|
+
--ticker-speed: 100s;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
@keyframes base-ticker-right {
|
|
9
|
+
0% {
|
|
10
|
+
transform: translateX(0%);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
50% {
|
|
14
|
+
transform: translateX(100%);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
50.001% {
|
|
18
|
+
transform: translateX(-100%);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
100% {
|
|
22
|
+
transform: translateX(0%);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
@keyframes cloned-ticker-right {
|
|
26
|
+
0% {
|
|
27
|
+
transform: translateX(0%);
|
|
28
|
+
}
|
|
29
|
+
100% {
|
|
30
|
+
transform: translateX(200%);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@keyframes base-ticker-left {
|
|
35
|
+
0% {
|
|
36
|
+
transform: translateX(0%);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
50% {
|
|
40
|
+
transform: translateX(-100%);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
50.001% {
|
|
44
|
+
transform: translateX(100%);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
100% {
|
|
48
|
+
transform: translateX(0%);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
@keyframes cloned-ticker-left {
|
|
52
|
+
0% {
|
|
53
|
+
transform: translateX(0%);
|
|
54
|
+
}
|
|
55
|
+
100% {
|
|
56
|
+
transform: translateX(-200%);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
.animate-base-ticker {
|
|
60
|
+
animation-name: base-ticker-right;
|
|
61
|
+
animation-duration: var(--ticker-speed);
|
|
62
|
+
animation-timing-function: linear;
|
|
63
|
+
animation-iteration-count: infinite;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.animate-cloned-ticker {
|
|
67
|
+
animation-name: cloned-ticker-right;
|
|
68
|
+
animation-duration: var(--ticker-speed);
|
|
69
|
+
animation-timing-function: linear;
|
|
70
|
+
animation-iteration-count: infinite;
|
|
71
|
+
}
|
|
72
|
+
.animate-base-ticker-left {
|
|
73
|
+
animation-name: base-ticker-left;
|
|
74
|
+
animation-duration: var(--ticker-speed);
|
|
75
|
+
animation-timing-function: linear;
|
|
76
|
+
animation-iteration-count: infinite;
|
|
77
|
+
}
|
|
78
|
+
.animate-cloned-ticker-left {
|
|
79
|
+
animation-name: cloned-ticker-left;
|
|
80
|
+
animation-duration: var(--ticker-speed);
|
|
81
|
+
animation-timing-function: linear;
|
|
82
|
+
animation-iteration-count: infinite;
|
|
83
|
+
}
|
|
84
|
+
.animate-pause {
|
|
85
|
+
animation-play-state: paused;
|
|
86
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { MotionTicker } from "../motion-ticker";
|
|
2
|
+
import { Text } from "@/common/ui/text/text";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
This is a ticker component that is used to display a list of items in a ticker.
|
|
6
|
+
It is a wrapper around the motion library and uses the useMotionValue hook to create the ticker.
|
|
7
|
+
It is a client component that is used to display a list of items in a ticker.
|
|
8
|
+
|
|
9
|
+
To test it you can just use it like this in any page:
|
|
10
|
+
<TickerHoverShowcaseHome />
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const TickerExampleCard = ({
|
|
14
|
+
text = "BASIC TICKER EXAMPLE",
|
|
15
|
+
}: {
|
|
16
|
+
text?: string;
|
|
17
|
+
}) => {
|
|
18
|
+
return (
|
|
19
|
+
<div className="border-2 border-x-0 border-fill-brand-primary bg-bg-primary-inverse px-5 py-2">
|
|
20
|
+
<Text className="capitalize" variant="body.2">
|
|
21
|
+
{text}
|
|
22
|
+
</Text>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
const CardWithSpacingExample = ({
|
|
27
|
+
text = "CARD WITH SPACING EXAMPLE",
|
|
28
|
+
}: {
|
|
29
|
+
text?: string;
|
|
30
|
+
}) => {
|
|
31
|
+
return (
|
|
32
|
+
<div className="px-1">
|
|
33
|
+
<div className="border-2 border-fill-brand-primary bg-bg-primary-inverse px-5 py-2">
|
|
34
|
+
<Text className="capitalize" variant="body.2">
|
|
35
|
+
{text}
|
|
36
|
+
</Text>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const TickerHoverShowcaseHome = () => {
|
|
43
|
+
return (
|
|
44
|
+
<div className="flex flex-col gap-12">
|
|
45
|
+
<Text className="px-4 pb-15" variant="title.6">
|
|
46
|
+
Ticker with hover stop
|
|
47
|
+
</Text>
|
|
48
|
+
<MotionTicker>
|
|
49
|
+
<CardWithSpacingExample text="TICKER EXAMPLE" />
|
|
50
|
+
<CardWithSpacingExample text="HOVER TICKER" />
|
|
51
|
+
</MotionTicker>
|
|
52
|
+
<MotionTicker direction="left">
|
|
53
|
+
<TickerExampleCard text="TICKER EXAMPLE" />
|
|
54
|
+
</MotionTicker>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { TickerStatic } from "../css-ticker/css-ticker";
|
|
2
|
+
import { Text } from "@/common/ui/text/text";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
This is a ticker component that is used to display a list of items in a ticker.
|
|
6
|
+
It is a wrapper around the css library and uses the css animations to create the ticker.
|
|
7
|
+
It is a client component that is used to display a list of items in a ticker.
|
|
8
|
+
|
|
9
|
+
To test it you can just use it like this in any page:
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const TickerExampleCard = ({
|
|
13
|
+
text = "BASIC TICKER EXAMPLE",
|
|
14
|
+
}: {
|
|
15
|
+
text?: string;
|
|
16
|
+
}) => {
|
|
17
|
+
return (
|
|
18
|
+
<div className="border-2 border-x-0 border-fill-brand-primary bg-bg-primary-inverse px-5 py-2">
|
|
19
|
+
<Text className="capitalize" variant="body.2">
|
|
20
|
+
{text}
|
|
21
|
+
</Text>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
const CardWithSpacingExample = ({
|
|
26
|
+
text = "CARD WITH SPACING EXAMPLE",
|
|
27
|
+
}: {
|
|
28
|
+
text?: string;
|
|
29
|
+
}) => {
|
|
30
|
+
return (
|
|
31
|
+
<div className="px-1">
|
|
32
|
+
<div className="border-2 border-fill-brand-primary bg-bg-primary-inverse px-5 py-2">
|
|
33
|
+
<Text className="capitalize" variant="body.2">
|
|
34
|
+
{text}
|
|
35
|
+
</Text>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const TickerStaticShowcaseHome = () => {
|
|
42
|
+
return (
|
|
43
|
+
<div className="flex flex-col gap-12">
|
|
44
|
+
<Text className="px-4 pb-15" variant="title.6">
|
|
45
|
+
Ticker static
|
|
46
|
+
</Text>
|
|
47
|
+
<TickerStatic>
|
|
48
|
+
<TickerExampleCard text="STATIC TICKER EXAMPLE" />
|
|
49
|
+
<TickerExampleCard text="TICKER" />
|
|
50
|
+
</TickerStatic>
|
|
51
|
+
<TickerStatic direction="left">
|
|
52
|
+
<CardWithSpacingExample text="TICKER EXAMPLE" />
|
|
53
|
+
</TickerStatic>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export const useTickerClones = ({
|
|
4
|
+
tickerCard,
|
|
5
|
+
tickerClone,
|
|
6
|
+
onCloneWrapper,
|
|
7
|
+
direction,
|
|
8
|
+
}: {
|
|
9
|
+
tickerCard: React.RefObject<HTMLDivElement | null>;
|
|
10
|
+
tickerClone: React.RefObject<HTMLDivElement | null>;
|
|
11
|
+
onCloneWrapper?: (clone: HTMLElement) => void;
|
|
12
|
+
direction: 'left' | 'right';
|
|
13
|
+
}) => {
|
|
14
|
+
const calculatedWidth = useRef(0);
|
|
15
|
+
const [clonesNeeded, setClonesNeeded] = useState(1);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (typeof window === 'undefined') return;
|
|
19
|
+
const sizeCloneNeeded = () => {
|
|
20
|
+
const tickerCardContent = Array.from(tickerCard.current?.childNodes ?? []).reduce(
|
|
21
|
+
(acc, child) => {
|
|
22
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
23
|
+
return acc;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return acc + (child as HTMLElement)?.offsetWidth;
|
|
27
|
+
},
|
|
28
|
+
0,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const tickerCardWidth = tickerCardContent;
|
|
32
|
+
|
|
33
|
+
if (!tickerCardWidth) return;
|
|
34
|
+
calculatedWidth.current = tickerCardWidth;
|
|
35
|
+
const clonesCalc = Math.ceil(window.outerWidth / tickerCardWidth);
|
|
36
|
+
|
|
37
|
+
if (clonesCalc > 1) setClonesNeeded(clonesCalc);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
sizeCloneNeeded();
|
|
41
|
+
window.addEventListener('resize', sizeCloneNeeded);
|
|
42
|
+
|
|
43
|
+
return () => {
|
|
44
|
+
window.removeEventListener('resize', sizeCloneNeeded);
|
|
45
|
+
};
|
|
46
|
+
}, [tickerCard]);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (typeof window === 'undefined') return;
|
|
49
|
+
const clonedTickerContent = tickerCard.current?.cloneNode(true) as HTMLElement;
|
|
50
|
+
|
|
51
|
+
if (!clonedTickerContent) return;
|
|
52
|
+
if (tickerClone?.current?.childNodes.length) {
|
|
53
|
+
tickerClone.current.removeChild(tickerClone.current.childNodes[0]);
|
|
54
|
+
}
|
|
55
|
+
clonedTickerContent.id = 'ticker-content-clone';
|
|
56
|
+
|
|
57
|
+
if (onCloneWrapper) onCloneWrapper(clonedTickerContent);
|
|
58
|
+
|
|
59
|
+
const tickerCardWidth = tickerCard.current?.offsetWidth;
|
|
60
|
+
|
|
61
|
+
tickerClone.current?.style.setProperty(
|
|
62
|
+
'left',
|
|
63
|
+
direction === 'right' ? `-${tickerCardWidth}px` : `${tickerCardWidth}px`,
|
|
64
|
+
'important',
|
|
65
|
+
);
|
|
66
|
+
tickerClone.current?.appendChild(clonedTickerContent);
|
|
67
|
+
}, [clonesNeeded, tickerCard, tickerClone, direction, onCloneWrapper]);
|
|
68
|
+
|
|
69
|
+
return { clonesNeeded, calculatedWidth };
|
|
70
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
|
|
4
|
+
import { MotionValue, useMotionValue, useMotionValueEvent } from 'motion/react';
|
|
5
|
+
|
|
6
|
+
const directionModifier = {
|
|
7
|
+
left: false,
|
|
8
|
+
right: true,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const useTickerIncremental = ({
|
|
12
|
+
direction,
|
|
13
|
+
speed,
|
|
14
|
+
delta,
|
|
15
|
+
stopIncrement,
|
|
16
|
+
onStop,
|
|
17
|
+
}: {
|
|
18
|
+
speed: number;
|
|
19
|
+
delta: number;
|
|
20
|
+
direction: 'left' | 'right';
|
|
21
|
+
stopIncrement?: boolean;
|
|
22
|
+
onStop?: (x: MotionValue<number>) => void;
|
|
23
|
+
}) => {
|
|
24
|
+
const x = useMotionValue(0);
|
|
25
|
+
const lastTimeRef = useRef<number>(0);
|
|
26
|
+
const fpsRef = useRef(60);
|
|
27
|
+
const translateX = useMotionValue('0%');
|
|
28
|
+
const clonedTranslateX = useMotionValue('0%');
|
|
29
|
+
const requestRef = useRef<number>(0);
|
|
30
|
+
|
|
31
|
+
useMotionValueEvent(x, 'change', (value) => {
|
|
32
|
+
const modifier = directionModifier[direction] ? 1 : -1;
|
|
33
|
+
|
|
34
|
+
translateX.set(`${modifier * (value > 100 ? value - 200 : value)}%`);
|
|
35
|
+
clonedTranslateX.set(`${modifier * value}%`);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (stopIncrement) {
|
|
40
|
+
cancelAnimationFrame(requestRef.current);
|
|
41
|
+
onStop?.(x);
|
|
42
|
+
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const animate = (now: number) => {
|
|
46
|
+
const currentX = x.get();
|
|
47
|
+
const deltaMs = now - lastTimeRef.current;
|
|
48
|
+
|
|
49
|
+
lastTimeRef.current = now;
|
|
50
|
+
|
|
51
|
+
fpsRef.current = 1000 / deltaMs;
|
|
52
|
+
|
|
53
|
+
if (currentX >= 200) {
|
|
54
|
+
x.set(0);
|
|
55
|
+
} else {
|
|
56
|
+
const newX = Number((currentX + speed * delta).toFixed(4));
|
|
57
|
+
|
|
58
|
+
x.set(newX);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
requestRef.current = requestAnimationFrame(animate);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
requestRef.current = requestAnimationFrame(animate);
|
|
65
|
+
|
|
66
|
+
return () => {
|
|
67
|
+
cancelAnimationFrame(requestRef.current);
|
|
68
|
+
};
|
|
69
|
+
}, [speed, delta, x, stopIncrement, onStop]);
|
|
70
|
+
|
|
71
|
+
return { x, x1: translateX, x2: clonedTranslateX, fps: fpsRef };
|
|
72
|
+
};
|