@androbinco/library-cli 0.2.0 → 0.3.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.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/src/commands/add.js +107 -0
  3. package/src/commands/list.js +51 -0
  4. package/src/index.js +38 -73
  5. package/src/templates/carousel/components/navigation-buttons.tsx +1 -0
  6. package/src/templates/carousel/components/pagination/bullet.pagination.carousel.tsx +69 -0
  7. package/src/templates/carousel/components/pagination/number.pagination.carousel.tsx +30 -0
  8. package/src/templates/carousel/components/pagination/progress/progress.pagination.carousel.tsx +99 -0
  9. package/src/templates/carousel/components/pagination/progress/use-slide-progress.tsx +31 -0
  10. package/src/templates/carousel/components/pagination.tsx +47 -82
  11. package/src/templates/faqs-accordion/examples/faqs-showcase.tsx +42 -0
  12. package/src/templates/faqs-accordion/faqs-accordion.tsx +70 -0
  13. package/src/templates/faqs-accordion/mock-data.ts +38 -0
  14. package/src/templates/faqs-accordion/types.ts +18 -0
  15. package/src/templates/in-view/data.in-view.ts +1 -1
  16. package/src/templates/in-view/in-view-animation.tsx +7 -7
  17. package/src/templates/in-view/in-view-grid.tsx +22 -20
  18. package/src/templates/in-view/in-view-hidden-text.tsx +7 -7
  19. package/src/templates/in-view/in-view-stroke-line.tsx +6 -5
  20. package/src/templates/lenis/examples/providers.tsx +23 -0
  21. package/src/templates/lenis/lenis-provider.tsx +46 -0
  22. package/src/templates/scroll-components/hooks/use-client-dimensions.ts +21 -0
  23. package/src/templates/scroll-components/parallax/examples/parallax-showcase.tsx +87 -0
  24. package/src/templates/scroll-components/parallax/parallax.css +36 -0
  25. package/src/templates/scroll-components/parallax/parallax.tsx +67 -0
  26. package/src/templates/scroll-components/scale-gallery/components/expanding-element.tsx +40 -0
  27. package/src/templates/scroll-components/scale-gallery/examples/scale-gallery-showcase.tsx +68 -0
  28. package/src/templates/scroll-components/scale-gallery/scale-gallery.tsx +57 -0
  29. package/src/templates/scroll-components/scroll-tracker-showcase.tsx +44 -0
  30. package/src/templates/strapi-dynamic-zone/README.md +157 -0
  31. package/src/templates/strapi-dynamic-zone/dynamic-zone.tsx +113 -0
  32. package/src/templates/strapi-dynamic-zone/examples/page.tsx +53 -0
  33. package/src/templates/strapi-dynamic-zone/examples/renderers.tsx +74 -0
  34. package/src/templates/strapi-dynamic-zone/examples/types.ts +41 -0
  35. package/src/templates/strapi-dynamic-zone/index.ts +11 -0
  36. package/src/templates/strapi-dynamic-zone/types.ts +73 -0
  37. package/src/utils/components.js +187 -5
  38. package/src/utils/files.js +13 -12
  39. package/src/templates/scroll-tracker/examples/scroll-tracker-showcase.tsx +0 -90
  40. /package/src/templates/{scroll-tracker → scroll-components}/scroll-tracker-provider.tsx +0 -0
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+
3
+ import { FaqsAccordion, MOCK_FAQS_DATA } from '../faqs-accordion';
4
+
5
+ /**
6
+ * Example showcase for FaqsAccordion component
7
+ *
8
+ * This simulates a Strapi API call with mock data.
9
+ * In production, replace MOCK_FAQS_DATA with:
10
+ *
11
+ * const response = await fetch('your-strapi-url/api/faqs?populate=questions');
12
+ * const faqs = await response.json();
13
+ */
14
+ export default function FaqsShowcase() {
15
+ // Simulating Strapi response with mock data
16
+ const faqsData = MOCK_FAQS_DATA;
17
+
18
+ return (
19
+ <div className="min-h-screen bg-gray-50 py-12">
20
+ <div className="container mx-auto px-4">
21
+ <h1 className="mb-12 text-center text-4xl font-bold">
22
+ FAQs Accordion Example
23
+ </h1>
24
+
25
+ <FaqsAccordion faqs={faqsData} />
26
+
27
+ {/* Example with custom styling */}
28
+ <div className="mt-16">
29
+ <h2 className="mb-8 text-center text-3xl font-bold">
30
+ Custom Styled Version
31
+ </h2>
32
+ <FaqsAccordion
33
+ faqs={faqsData}
34
+ className="rounded-lg bg-white p-8 shadow-lg"
35
+ questionClassName="text-blue-600"
36
+ answerClassName="text-gray-700"
37
+ />
38
+ </div>
39
+ </div>
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,70 @@
1
+ 'use client';
2
+
3
+ import * as Accordion from '@radix-ui/react-accordion';
4
+ import { FaqsAccordionProps } from './types';
5
+
6
+ const ChevronDownIcon = ({ className }: { className?: string }) => (
7
+ <svg
8
+ width="24"
9
+ height="24"
10
+ viewBox="0 0 24 24"
11
+ fill="none"
12
+ xmlns="http://www.w3.org/2000/svg"
13
+ className={className}
14
+ >
15
+ <path
16
+ d="M6 9L12 15L18 9"
17
+ stroke="currentColor"
18
+ strokeWidth="2"
19
+ strokeLinecap="round"
20
+ strokeLinejoin="round"
21
+ />
22
+ </svg>
23
+ );
24
+
25
+ export const FaqsAccordion = ({
26
+ faqs,
27
+ className = '',
28
+ questionClassName = '',
29
+ answerClassName = '',
30
+ }: FaqsAccordionProps) => {
31
+ return (
32
+ <div className={`mx-auto w-full max-w-[910px] ${className}`}>
33
+ <h2 className="mb-8 text-3xl font-bold">{faqs.title}</h2>
34
+
35
+ <Accordion.Root type="single" collapsible className="w-full">
36
+ {faqs.questions.map((faq, index) => (
37
+ <div key={faq.id}>
38
+ <Accordion.Item value={`item-${faq.id}`} className="border-b">
39
+ <Accordion.Header>
40
+ <Accordion.Trigger
41
+ className={`group flex w-full items-center justify-between py-6 text-left transition-all hover:opacity-80 ${questionClassName}`}
42
+ >
43
+ <span className="text-lg font-semibold">{faq.question}</span>
44
+ <ChevronDownIcon className="transition-transform duration-300 group-data-[state=open]:rotate-180" />
45
+ </Accordion.Trigger>
46
+ </Accordion.Header>
47
+
48
+ <Accordion.Content
49
+ className={`overflow-hidden transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down ${answerClassName}`}
50
+ >
51
+ <div className="pb-6 text-base text-gray-600">
52
+ {faq.answer}
53
+ </div>
54
+ </Accordion.Content>
55
+ </Accordion.Item>
56
+ </div>
57
+ ))}
58
+ </Accordion.Root>
59
+ </div>
60
+ );
61
+ };
62
+
63
+ export default FaqsAccordion;
64
+
65
+ export type {
66
+ FaqsAccordionProps,
67
+ FaqsResponse,
68
+ FaqQuestion,
69
+ } from './types';
70
+ export { MOCK_FAQS_DATA } from './mock-data';
@@ -0,0 +1,38 @@
1
+ import { FaqsResponse } from './types';
2
+
3
+ export const MOCK_FAQS_DATA: FaqsResponse = {
4
+ id: 1,
5
+ title: 'Frequently Asked Questions',
6
+ questions: [
7
+ {
8
+ id: 1,
9
+ question: 'What is the return policy?',
10
+ answer:
11
+ 'You can return any item within 30 days of purchase for a full refund. Items must be in original condition with tags attached.',
12
+ },
13
+ {
14
+ id: 2,
15
+ question: 'How long does shipping take?',
16
+ answer:
17
+ 'Standard shipping takes 5-7 business days. Express shipping is available and takes 2-3 business days.',
18
+ },
19
+ {
20
+ id: 3,
21
+ question: 'Do you ship internationally?',
22
+ answer:
23
+ 'Yes, we ship to over 50 countries worldwide. International shipping times vary by location.',
24
+ },
25
+ {
26
+ id: 4,
27
+ question: 'How do I track my order?',
28
+ answer:
29
+ 'Once your order ships, you\'ll receive a tracking number via email. You can use this to track your package on our website.',
30
+ },
31
+ {
32
+ id: 5,
33
+ question: 'What payment methods do you accept?',
34
+ answer:
35
+ 'We accept all major credit cards, PayPal, Apple Pay, and Google Pay for your convenience.',
36
+ },
37
+ ],
38
+ };
@@ -0,0 +1,18 @@
1
+ export type FaqQuestion = {
2
+ id: number;
3
+ question: string;
4
+ answer: string;
5
+ };
6
+
7
+ export type FaqsResponse = {
8
+ id: number;
9
+ title: string;
10
+ questions: FaqQuestion[];
11
+ };
12
+
13
+ export type FaqsAccordionProps = {
14
+ faqs: FaqsResponse;
15
+ className?: string;
16
+ questionClassName?: string;
17
+ answerClassName?: string;
18
+ };
@@ -49,7 +49,7 @@ const IN_VIEW_ANIMATIONS = {
49
49
  },
50
50
  strokeLine: {
51
51
  hidden: {
52
- scaleX: 0,
52
+ scaleX: 0.001,
53
53
  transformOrigin: 'left center',
54
54
  },
55
55
  visible: {
@@ -1,19 +1,19 @@
1
- "use client";
2
- import { FC, ReactNode } from "react";
1
+ 'use client';
2
+ import { FC, ReactNode } from 'react';
3
3
 
4
4
  import {
5
5
  motion,
6
6
  MotionProps,
7
7
  TargetAndTransition,
8
8
  Transition,
9
- } from "motion/react";
9
+ } from 'motion/react';
10
10
 
11
- import IN_VIEW_ANIMATIONS from "./data.in-view";
11
+ import IN_VIEW_ANIMATIONS from './data.in-view';
12
12
 
13
13
  const transitionsDefault: Transition = {
14
14
  duration: 0.6,
15
15
  delay: 0,
16
- ease: "easeInOut",
16
+ ease: 'easeInOut',
17
17
  };
18
18
 
19
19
  export type InViewAnimationProps = {
@@ -33,11 +33,11 @@ export type InViewAnimationProps = {
33
33
  React.HTMLAttributes<HTMLDivElement>;
34
34
 
35
35
  const InViewAnimation: FC<InViewAnimationProps> = ({
36
- margin = "-10% 0%",
36
+ margin = '-10% 0%',
37
37
  execute = true,
38
38
  transition = {},
39
39
  customEffect,
40
- effect = "fadeInUp",
40
+ effect = 'fadeInUp',
41
41
  once = true,
42
42
  style,
43
43
  className,
@@ -1,62 +1,64 @@
1
- "use client";
2
- import { Children, useEffect, useState } from "react";
1
+ 'use client';
2
+ import { Children, useEffect, useState } from 'react';
3
3
 
4
- import { motion, Transition } from "motion/react";
4
+ import { motion, Transition } from 'motion/react';
5
5
 
6
- import { cn } from "@/common/utils/classname-builder";
6
+ import { cn } from '@/common/utils/classname-builder';
7
7
 
8
- import IN_VIEW_ANIMATIONS from "./data.in-view";
8
+ import IN_VIEW_ANIMATIONS from './data.in-view';
9
9
 
10
10
  type GridInViewProps = {
11
+ breakpoint?: number;
11
12
  children: React.ReactNode;
12
13
  className?: string;
13
14
  effect?: keyof typeof IN_VIEW_ANIMATIONS;
14
15
  columns?: number | { desktop: number; mobile: number };
15
16
  delayDelta?: number;
16
17
  delayByColumn?: boolean;
17
- transition?: Omit<Transition, "delay">;
18
+ transition?: Omit<Transition, 'delay'>;
18
19
  columnsMobile?: number;
19
20
  };
20
21
 
21
22
  export const GridInView = ({
22
23
  children,
23
24
  className,
24
- effect = "fadeInUp",
25
- delayDelta = 0.3,
25
+ effect = 'fadeInUp',
26
+ delayDelta = 0.25,
26
27
  delayByColumn = true,
27
28
  transition = {
28
29
  duration: 0.5,
29
- ease: "easeOut",
30
+ ease: 'easeOut',
30
31
  },
32
+ breakpoint = 1024,
31
33
  columns = 5,
32
34
  }: GridInViewProps) => {
33
35
  const childrenArray = Children.toArray(children);
34
- const isColumnsObject = typeof columns === "object";
36
+ const isColumnsObject = typeof columns === 'object';
35
37
  const [columnsValue, setColumnsValue] = useState(
36
38
  isColumnsObject ? columns.desktop : columns
37
39
  );
38
40
 
39
41
  useEffect(() => {
40
42
  if (!delayByColumn) return;
41
- if (typeof window === "undefined" || !isColumnsObject) return;
42
- if (window.innerWidth < 1024) setColumnsValue(columns.mobile);
43
+ if (typeof window === 'undefined' || !isColumnsObject) return;
44
+ if (window.innerWidth < breakpoint) setColumnsValue(columns.mobile);
43
45
  else setColumnsValue(columns.desktop);
44
- }, [columns, isColumnsObject, delayByColumn]);
46
+ }, [columns, isColumnsObject, delayByColumn, breakpoint]);
45
47
  const delayCount = (index: number) =>
46
48
  delayByColumn ? index % columnsValue : index;
47
49
 
48
50
  return (
49
51
  <div
50
52
  className={cn(
51
- "grid gap-4",
52
- "[grid-template-columns:repeat(var(--columns),1fr)]",
53
- "max-lg:[grid-template-columns:repeat(var(--columns-mobile),1fr)]",
54
- className
53
+ 'grid gap-4',
54
+ '[grid-template-columns:repeat(var(--columns),1fr)]',
55
+ 'max-lg:[grid-template-columns:repeat(var(--columns-mobile),1fr)]',
56
+ className,
55
57
  )}
56
58
  style={
57
59
  {
58
- "--columns": isColumnsObject ? columns.desktop : columns,
59
- "--columns-mobile": isColumnsObject ? columns.mobile : columns,
60
+ '--columns': isColumnsObject ? columns.desktop : columns,
61
+ '--columns-mobile': isColumnsObject ? columns.mobile : columns,
60
62
  } as React.CSSProperties
61
63
  }
62
64
  >
@@ -68,7 +70,7 @@ export const GridInView = ({
68
70
  ...transition,
69
71
  delay: delayCount(index) * delayDelta,
70
72
  }}
71
- viewport={{ once: true, margin: "-10% 0%" }}
73
+ viewport={{ once: true, margin: '-10% 0%' }}
72
74
  whileInView={IN_VIEW_ANIMATIONS[effect].visible}
73
75
  >
74
76
  {item}
@@ -1,15 +1,15 @@
1
- import { Transition } from "motion/react";
1
+ import { Transition } from 'motion/react';
2
2
 
3
- import { cn } from "@/common/utils/classname-builder";
3
+ import { cn } from '@/common/utils/classname-builder';
4
4
 
5
- import InViewAnimation, { InViewAnimationProps } from "./in-view-animation";
5
+ import InViewAnimation, { InViewAnimationProps } from './in-view-animation';
6
6
 
7
7
  export const InViewHiddenText = ({
8
8
  children,
9
9
  className,
10
10
  transition = {
11
11
  duration: 0.6,
12
- ease: "easeOut",
12
+ ease: 'easeOut',
13
13
  },
14
14
  ...props
15
15
  }: {
@@ -19,12 +19,12 @@ export const InViewHiddenText = ({
19
19
  props?: InViewAnimationProps;
20
20
  }) => {
21
21
  return (
22
- <div className={cn("overflow-hidden", className)}>
22
+ <div className={cn('overflow-hidden', className)}>
23
23
  <InViewAnimation
24
- className={cn("origin-left overflow-hidden", className)}
24
+ className={cn('origin-left overflow-hidden', className)}
25
25
  customEffect={{
26
26
  hidden: {
27
- y: "99%",
27
+ y: '99%',
28
28
  opacity: 0,
29
29
  rotate: 5,
30
30
  },
@@ -1,8 +1,8 @@
1
- import { Transition } from "motion/react";
1
+ import { Transition } from 'motion/react';
2
2
 
3
- import { cn } from "@/common/utils/classname-builder";
3
+ import { cn } from '@/common/utils/classname-builder';
4
4
 
5
- import InViewAnimation, { InViewAnimationProps } from "./in-view-animation";
5
+ import InViewAnimation, { InViewAnimationProps } from './in-view-animation';
6
6
 
7
7
  export const InViewStrokeLine = ({
8
8
  className,
@@ -15,12 +15,13 @@ export const InViewStrokeLine = ({
15
15
  <InViewAnimation
16
16
  aria-label="separator"
17
17
  aria-roledescription="separator"
18
- className={cn("h-px w-full bg-red-950", className)}
18
+ className={cn('h-px w-full bg-red-950', className)}
19
19
  effect="strokeLine"
20
+ margin="0%"
20
21
  role="separator"
21
22
  transition={{
22
23
  duration: 0.6,
23
- ease: "easeInOut",
24
+ ease: 'easeInOut',
24
25
  ...transition,
25
26
  }}
26
27
  {...props}
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ import { QueryClientProvider } from '@tanstack/react-query';
6
+
7
+ import { LenisProvider } from '@/common/animations/smooth-scroll/lenis.provider';
8
+ import { getQueryClient } from '@/common/hooks/get-query-client';
9
+
10
+ import { ThemeProvider } from './theme-provider';
11
+
12
+ export default function Providers({ children }: { children: React.ReactNode }) {
13
+ const queryClient = getQueryClient();
14
+
15
+ return (
16
+ <QueryClientProvider client={queryClient}>
17
+ <ThemeProvider disableTransitionOnChange enableSystem attribute="class" defaultTheme="system">
18
+ <LenisProvider>{children}</LenisProvider>
19
+ {/* <ReactQueryDevtools /> */}
20
+ </ThemeProvider>
21
+ </QueryClientProvider>
22
+ );
23
+ }
@@ -0,0 +1,46 @@
1
+ 'use client';
2
+
3
+ import { createContext, useEffect, useState } from 'react';
4
+
5
+ import { usePathname } from 'next/navigation';
6
+
7
+ import Lenis from 'lenis';
8
+
9
+ type LenisContextType = {
10
+ lenis: Lenis | null;
11
+ };
12
+
13
+ const LenisContext = createContext<LenisContextType | undefined>(undefined);
14
+
15
+ export const LenisProvider = ({ children }: { children: React.ReactNode }) => {
16
+ const [lenis, setLenis] = useState<Lenis | null>(null);
17
+ const pathname = usePathname();
18
+
19
+ useEffect(() => {
20
+ lenis?.scrollTo(0, { immediate: true });
21
+ }, [pathname, lenis]);
22
+
23
+ useEffect(() => {
24
+ if (typeof window === 'undefined') return;
25
+ const lenisInstance = new Lenis({
26
+ lerp: 0.1,
27
+ smoothWheel: true,
28
+ });
29
+
30
+ setLenis(lenisInstance);
31
+ const raf = (time: number) => {
32
+ lenisInstance.raf(time);
33
+ requestAnimationFrame(raf);
34
+ };
35
+
36
+ const requestId = requestAnimationFrame(raf);
37
+
38
+ return () => {
39
+ lenisInstance.destroy();
40
+ cancelAnimationFrame(requestId);
41
+ setLenis(null);
42
+ };
43
+ }, []);
44
+
45
+ return <LenisContext.Provider value={{ lenis }}>{children}</LenisContext.Provider>;
46
+ };
@@ -0,0 +1,21 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ export const useClientDimensions = () => {
4
+ const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
5
+
6
+ useEffect(() => {
7
+ if (typeof window === 'undefined') return;
8
+ setDimensions({ width: window.innerWidth, height: window.innerHeight });
9
+ window.addEventListener('resize', () => {
10
+ setDimensions({ width: window.innerWidth, height: window.innerHeight });
11
+ });
12
+
13
+ return () => {
14
+ window.removeEventListener('resize', () => {
15
+ setDimensions({ width: window.innerWidth, height: window.innerHeight });
16
+ });
17
+ };
18
+ }, []);
19
+
20
+ return dimensions;
21
+ };
@@ -0,0 +1,87 @@
1
+ "use client";
2
+ import { Parallax } from "../parallax/parallax";
3
+ import { Text } from "@/common/ui/text/text";
4
+
5
+ export const ParallaxShowcase = () => {
6
+ return (
7
+ <div className="grid gap-10">
8
+ <div className="flex flex-col justify-center gap-2 py-3">
9
+ <Text className="px-8 max-lg:px-4" variant="title.6">
10
+ Scroll Parallax
11
+ </Text>
12
+ </div>
13
+ <div className="flex flex-col gap-0">
14
+ <Parallax.Root className="relative h-screen w-full">
15
+ <Parallax.Image
16
+ alt="parallax-image"
17
+ imageClassName="h-screen w-full object-cover"
18
+ src="https://storage.googleapis.com/testing-mets-bucket/Players_Family_Lounge_21537_d8a8b086cb/Players_Family_Lounge_21537_d8a8b086cb.jpg"
19
+ />
20
+ </Parallax.Root>
21
+ <Parallax.Root className="relative h-[90vh] w-full">
22
+ <Parallax.Image
23
+ alt="parallax-image"
24
+ imageClassName="w-full object-cover"
25
+ src="https://storage.googleapis.com/testing-mets-bucket/Players_Family_Lounge_21564_3de5bc0bed/Players_Family_Lounge_21564_3de5bc0bed.jpg"
26
+ />
27
+ </Parallax.Root>
28
+ <Parallax.Root className="relative h-[90vh] w-full">
29
+ <Parallax.Image
30
+ alt="parallax-image"
31
+ imageClassName="w-full object-cover"
32
+ src="https://storage.googleapis.com/testing-mets-bucket/Delta_Sky360_Club_21192_2cee5eec25/Delta_Sky360_Club_21192_2cee5eec25.jpg"
33
+ />
34
+ </Parallax.Root>
35
+ </div>
36
+ <div className="mx-auto w-full max-w-container">
37
+ <div className="flex flex-row flex-wrap items-center justify-between gap-4">
38
+ <div className="flex flex-col items-center gap-2">
39
+ <Parallax.Root
40
+ easing="ease-in"
41
+ offset="200px"
42
+ className="aspect-[9/16] max-h-150 max-w-100 rounded-2xl overflow-hidden"
43
+ >
44
+ <Parallax.Image
45
+ alt="parallax-image"
46
+ imageClassName="h-full w-full object-cover"
47
+ easing="ease-in"
48
+ src="/img/gallery/gallery-example3.jpg"
49
+ />
50
+ </Parallax.Root>
51
+ <Text>Image text</Text>
52
+ </div>
53
+ <div className="flex flex-col items-center gap-2">
54
+ <Parallax.Root
55
+ className="aspect-[9/16] max-h-150 max-w-100 rounded-2xl overflow-hidden"
56
+ easing="ease-in-out"
57
+ offset="20%"
58
+ >
59
+ <Parallax.Image
60
+ alt="parallax-image"
61
+ imageClassName="h-full w-full object-cover"
62
+ easing="ease-in-out"
63
+ src="/img/gallery/gallery-example2.jpg"
64
+ />
65
+ </Parallax.Root>
66
+ <Text>Image text</Text>
67
+ </div>
68
+ <div className="flex flex-col items-center gap-2">
69
+ <Parallax.Root
70
+ className="aspect-[9/16] max-h-150 max-w-100 rounded-2xl overflow-hidden"
71
+ easing="ease-out"
72
+ offset="10%"
73
+ >
74
+ <Parallax.Image
75
+ alt="parallax-image"
76
+ imageClassName="h-full w-full object-cover"
77
+ easing="ease-out"
78
+ src="/img/gallery/gallery-example1.jpg"
79
+ />
80
+ </Parallax.Root>
81
+ <Text>Image text</Text>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ );
87
+ };
@@ -0,0 +1,36 @@
1
+ .parallax-root {
2
+ position: relative;
3
+ --parallax-offset: 20%;
4
+ view-timeline-name: --parallax-tracking;
5
+ }
6
+
7
+ @keyframes parallax-image {
8
+ from {
9
+ transform: translateY(calc((var(--parallax-offset) / 2) * -1));
10
+ }
11
+ to {
12
+ transform: translateY(calc(var(--parallax-offset) / 2));
13
+ }
14
+ }
15
+ .parallax-container {
16
+ height: 100%;
17
+ position: relative;
18
+ overflow: hidden;
19
+ }
20
+
21
+ .parallax-img {
22
+ width: 100%;
23
+ display: block;
24
+ --calc-height: calc(100% + var(--parallax-offset));
25
+ height: var(--calc-height);
26
+ position: relative;
27
+ animation-timeline: --parallax-tracking;
28
+ animation-timing-function: linear;
29
+ animation-range: entry calc(100vh - 100%) exit calc(100vh + (100vh - 100%));
30
+ top: calc((var(--parallax-offset) / 2) * -1);
31
+ /* top:0%; */
32
+ animation-name: parallax-image;
33
+ animation-fill-mode: both;
34
+ animation-duration: 1ms;
35
+ object-fit: cover;
36
+ }
@@ -0,0 +1,67 @@
1
+ import { cn } from "@/common/utils/classname-builder";
2
+ import "./parallax.css";
3
+
4
+ type Easing = "linear" | "ease-in-out" | "ease-in" | "ease-out";
5
+ const ParallaxRoot = ({
6
+ className,
7
+ children,
8
+ offset = "200px",
9
+ easing = "linear",
10
+ }: {
11
+ className?: string;
12
+ children: React.ReactNode;
13
+ easing?: Easing;
14
+ offset?:
15
+ | `${number}px`
16
+ | `${number}%`
17
+ | `${number}vh`
18
+ | `${number}vw`
19
+ | `${number}em`
20
+ | `${number}rem`
21
+ | `${number}ex`
22
+ | `${number}ch`;
23
+ }) => {
24
+ return (
25
+ <div
26
+ className={cn("parallax-root h-max w-max", className)}
27
+ style={
28
+ {
29
+ "--easing": easing,
30
+ "--parallax-offset": offset,
31
+ } as React.CSSProperties
32
+ }
33
+ >
34
+ {children}
35
+ </div>
36
+ );
37
+ };
38
+
39
+ const ParallaxImage = ({
40
+ className,
41
+ src,
42
+ imageClassName,
43
+ alt,
44
+ easing = "linear",
45
+ }: {
46
+ className?: string;
47
+ src: string;
48
+ imageClassName?: string;
49
+ easing?: Easing;
50
+ alt: string;
51
+ }) => {
52
+ return (
53
+ <div className={cn("parallax-container h-full w-full", className)}>
54
+ <img
55
+ alt={alt}
56
+ className={cn("parallax-img", imageClassName)}
57
+ src={src}
58
+ style={{ "--easing": easing } as React.CSSProperties}
59
+ />
60
+ </div>
61
+ );
62
+ };
63
+
64
+ export const Parallax = {
65
+ Root: ParallaxRoot,
66
+ Image: ParallaxImage,
67
+ };