@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.
@@ -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
+ };