@growth-angels/ds-core 1.3.5 → 1.5.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.
@@ -2,4 +2,5 @@ export declare function useBreakpointObserver(breakpoints?: {
2
2
  sm: number;
3
3
  md: number;
4
4
  lg: number;
5
- }): "sm" | "md" | "lg";
5
+ xl: number;
6
+ }): "sm" | "md" | "lg" | "xl";
@@ -1,5 +1,5 @@
1
1
  import { useReactAdapter } from './useReactAdaptater';
2
- export function useBreakpointObserver(breakpoints = { sm: 640, md: 1024, lg: 1280 }) {
2
+ export function useBreakpointObserver(breakpoints = { sm: 640, md: 1024, lg: 1280, xl: 1400 }) {
3
3
  const { useEffect, useState } = useReactAdapter();
4
4
  const [current, setCurrent] = useState('sm');
5
5
  useEffect(() => {
@@ -7,10 +7,14 @@ export function useBreakpointObserver(breakpoints = { sm: 640, md: 1024, lg: 128
7
7
  const check = () => {
8
8
  const width = window.innerWidth;
9
9
  let bp = 'sm';
10
- if (width >= breakpoints.md)
10
+ if (width >= breakpoints.xl)
11
+ bp = 'xl';
12
+ else if (width >= breakpoints.lg)
11
13
  bp = 'lg';
12
- else if (width >= breakpoints.sm)
14
+ else if (width >= breakpoints.md)
13
15
  bp = 'md';
16
+ else if (width >= breakpoints.sm)
17
+ bp = 'sm';
14
18
  setCurrent((prev) => (prev !== bp ? bp : prev));
15
19
  frameId = requestAnimationFrame(check);
16
20
  };
@@ -3,56 +3,131 @@ import { Button } from "../../atoms/atoms";
3
3
  import { useBreakpointObserver } from "../../hooks/useBreakPointObserver";
4
4
  import { useReactAdapter } from "../../hooks/useReactAdaptater";
5
5
  export const Carousel = (props) => {
6
- const { children, slidesPerView = { sm: 1, md: 2, lg: 3 }, spaceBetween = 20, navigation, pagination, context, hasPagination, hasNavigation, } = props;
6
+ const { children, slidesPerView = { sm: 1, md: 2, lg: 3, xl: 4 }, spaceBetween = 20, navigation, pagination, context, hasPagination, hasNavigation, loop = false, } = props;
7
7
  const { useEffect, useState, useRef, Children } = useReactAdapter();
8
8
  const trackRef = useRef(null);
9
+ const isNavigatingRef = useRef(false);
9
10
  const slides = Children.toArray(children);
10
11
  const calculatePages = (totalSlides = 0, slidesPerView = 0, step = 1) => {
11
12
  return Math.max(1, Math.ceil((totalSlides - slidesPerView) / step) + 1);
12
13
  };
13
14
  const [activeSlideIndex, setActiveSlideIndex] = useState(0);
15
+ const [activeDOMIndex, setActiveDOMIndex] = useState(0);
14
16
  const [isAtEnd, setIsAtEnd] = useState(false);
15
17
  const [isAtStart, setIsAtStart] = useState(true);
16
18
  const [totalPages, setTotalPages] = useState(0);
17
19
  useEffect(() => {
18
- setIsAtStart(activeSlideIndex === 0);
19
- setIsAtEnd(activeSlideIndex === totalPages - 1);
20
- }, [activeSlideIndex, totalPages]);
21
- const breakpoint = useBreakpointObserver({ sm: 768, md: 992, lg: 1200 });
20
+ if (loop) {
21
+ setIsAtStart(false);
22
+ setIsAtEnd(false);
23
+ }
24
+ else {
25
+ setIsAtStart(activeSlideIndex === 0);
26
+ setIsAtEnd(activeSlideIndex === totalPages - 1);
27
+ }
28
+ }, [activeSlideIndex, totalPages, loop]);
29
+ const breakpoint = useBreakpointObserver({ sm: 768, md: 992, lg: 1200, xl: 1400 });
22
30
  useEffect(() => {
23
31
  setTotalPages(calculatePages(slides.length, slidesPerView[breakpoint], 1));
24
32
  }, [breakpoint, slidesPerView]);
33
+ useEffect(() => {
34
+ const track = trackRef.current;
35
+ if (!track)
36
+ return;
37
+ const handleScroll = () => {
38
+ if (loop) {
39
+ const slideWidth = track.scrollWidth / (slides.length * 3);
40
+ const scrollLeft = track.scrollLeft;
41
+ // Repositionnement infini (uniquement si pas en navigation)
42
+ if (!isNavigatingRef.current) {
43
+ if (scrollLeft <= slideWidth * slides.length) {
44
+ track.scrollLeft = scrollLeft + slideWidth * slides.length;
45
+ }
46
+ else if (scrollLeft >= slideWidth * slides.length * 2) {
47
+ track.scrollLeft = scrollLeft - slideWidth * slides.length;
48
+ }
49
+ }
50
+ const scrollPosition = track.scrollLeft + slideWidth / 2;
51
+ const domIndex = Math.floor(scrollPosition / slideWidth);
52
+ const index = domIndex % slides.length;
53
+ setActiveDOMIndex(domIndex);
54
+ setActiveSlideIndex(index);
55
+ }
56
+ else {
57
+ const slideWidth = track.scrollWidth / slides.length;
58
+ const scrollPosition = track.scrollLeft + slideWidth / 2;
59
+ const index = Math.floor(scrollPosition / slideWidth);
60
+ setActiveSlideIndex(Math.min(index, slides.length - 1));
61
+ setActiveDOMIndex(index);
62
+ }
63
+ };
64
+ track.addEventListener("scroll", handleScroll);
65
+ // Position initiale au milieu en mode loop
66
+ if (loop) {
67
+ setTimeout(() => {
68
+ const slideWidth = track.scrollWidth / (slides.length * 3);
69
+ track.scrollLeft = slideWidth * slides.length;
70
+ }, 0);
71
+ }
72
+ return () => track.removeEventListener("scroll", handleScroll);
73
+ }, [slides.length, loop]);
25
74
  const style = Object.fromEntries(Object.entries(slidesPerView).map(([key, value]) => [`--ga-ds-slides-per-view-${key}`, `${value}`]));
26
75
  const goPrev = () => {
27
76
  if (!trackRef.current)
28
77
  return;
29
- const slides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide");
30
- if (activeSlideIndex === 0)
78
+ const allSlides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide");
79
+ if (activeSlideIndex === 0 && !loop)
31
80
  return;
32
- const nextIndex = activeSlideIndex - 1;
33
- slides[nextIndex]?.scrollIntoView({
34
- behavior: "smooth",
35
- block: "nearest",
36
- inline: "start",
37
- });
38
- setActiveSlideIndex(nextIndex);
81
+ isNavigatingRef.current = true;
82
+ if (loop) {
83
+ // En mode loop, on utilise l'index DOM actuel
84
+ allSlides[activeDOMIndex - 1]?.scrollIntoView({
85
+ behavior: "smooth",
86
+ block: "nearest",
87
+ inline: "start",
88
+ });
89
+ }
90
+ else {
91
+ const nextIndex = activeSlideIndex - 1;
92
+ allSlides[nextIndex]?.scrollIntoView({
93
+ behavior: "smooth",
94
+ block: "nearest",
95
+ inline: "start",
96
+ });
97
+ }
98
+ setTimeout(() => {
99
+ isNavigatingRef.current = false;
100
+ }, 600);
39
101
  };
40
102
  const goNext = () => {
41
103
  if (!trackRef.current)
42
104
  return;
43
- const slides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide");
44
- const totalSlides = slides.length;
45
- if (activeSlideIndex >= totalSlides - 1)
46
- return;
47
- if (isAtEnd)
48
- return;
49
- const nextIndex = activeSlideIndex + 1;
50
- slides[nextIndex]?.scrollIntoView({
51
- behavior: "smooth",
52
- block: "nearest",
53
- inline: "start",
54
- });
55
- setActiveSlideIndex(nextIndex);
105
+ const allSlides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide");
106
+ isNavigatingRef.current = true;
107
+ if (loop) {
108
+ // En mode loop, on utilise l'index DOM actuel
109
+ allSlides[activeDOMIndex + 1]?.scrollIntoView({
110
+ behavior: "smooth",
111
+ block: "nearest",
112
+ inline: "start",
113
+ });
114
+ }
115
+ else {
116
+ const totalSlides = allSlides.length;
117
+ if (activeSlideIndex >= totalSlides - 1)
118
+ return;
119
+ if (isAtEnd)
120
+ return;
121
+ const nextIndex = activeSlideIndex + 1;
122
+ allSlides[nextIndex]?.scrollIntoView({
123
+ behavior: "smooth",
124
+ block: "nearest",
125
+ inline: "start",
126
+ });
127
+ }
128
+ setTimeout(() => {
129
+ isNavigatingRef.current = false;
130
+ }, 600);
56
131
  };
57
132
  const goTo = (nextIndex) => {
58
133
  if (!trackRef.current)
@@ -63,17 +138,23 @@ export const Carousel = (props) => {
63
138
  block: "nearest",
64
139
  inline: "start",
65
140
  });
66
- setActiveSlideIndex(nextIndex);
67
141
  };
68
142
  const classes = ["ga-ds-carousel"];
69
143
  if (props.extraClassNames) {
70
- classes.push(...props.extraClassNames);
144
+ if (typeof props.extraClassNames === "string") {
145
+ classes.push(props.extraClassNames);
146
+ }
147
+ else {
148
+ classes.push(...props.extraClassNames);
149
+ }
71
150
  }
72
151
  return (_jsxs("div", { className: classes.join(" "), style: style, children: [navigation?.positionY === "top" && hasNavigation && (_jsx(CarouselNavigation, { goPrev: goPrev, goNext: goNext, isAtStart: isAtStart, isAtEnd: isAtEnd })), _jsx("div", { className: "ga-ds-carousel__track", ref: trackRef, style: {
73
152
  "--ga-ds-space-between": `${spaceBetween / 10}rem`,
74
153
  }, children: context === "wp-editor"
75
154
  ? children
76
- : slides.map((child, index) => (_jsx("div", { className: `ga-ds-carousel__slide ${index === activeSlideIndex ? "ga-ds-carousel__slide--active" : ""}`, children: child }, index))) }), _jsxs("div", { className: "ga-ds-carousel__navigation", children: [pagination && hasPagination && (_jsx(CarouselPagination, { totalPages: totalPages, activeSlideIndex: activeSlideIndex, goTo: goTo, clickable: pagination.clickable })), hasNavigation && navigation?.positionY === "bottom" && (_jsx(CarouselNavigation, { goPrev: goPrev, goNext: goNext, isAtStart: isAtStart, isAtEnd: isAtEnd }))] })] }));
155
+ : slides.map((child, index) => {
156
+ return (_jsx("div", { className: `ga-ds-carousel__slide ${index === activeDOMIndex ? "ga-ds-carousel__slide--active" : ""}`, children: child }, index));
157
+ }) }), _jsxs("div", { className: "ga-ds-carousel__navigation", children: [pagination && hasPagination && (_jsx(CarouselPagination, { totalPages: totalPages, activeSlideIndex: activeSlideIndex, goTo: goTo, clickable: pagination.clickable })), hasNavigation && navigation?.positionY === "bottom" && (_jsx(CarouselNavigation, { goPrev: goPrev, goNext: goNext, isAtStart: isAtStart, isAtEnd: isAtEnd }))] })] }));
77
158
  };
78
159
  const CarouselNavigation = ({ goPrev, goNext, isAtStart, isAtEnd, }) => {
79
160
  return (_jsxs("div", { className: `ga-ds-carousel__arrows`, children: [_jsx(Button, { extraClassNames: ["ga-ds-carousel__button", "ga-ds-carousel__button--prev"], icon: "chevron-left", onClick: goPrev, disabled: isAtStart }), _jsx(Button, { extraClassNames: ["ga-ds-carousel__button", "ga-ds-carousel__button--next"], icon: "chevron-right", onClick: goNext, disabled: isAtEnd })] }));
@@ -4,3 +4,4 @@ declare const meta: Meta<typeof Carousel>;
4
4
  export default meta;
5
5
  type Story = StoryObj<typeof Carousel>;
6
6
  export declare const Primary: Story;
7
+ export declare const LoopingCarousel: Story;
@@ -17,7 +17,7 @@ export const Primary = {
17
17
  _jsx("div", { children: "Slide 6" }),
18
18
  _jsx("div", { children: "Slide 7" }),
19
19
  ],
20
- slidesPerView: { sm: 1, md: 2, lg: 3 },
20
+ slidesPerView: { sm: 1, md: 2, lg: 3, xl: 4 },
21
21
  spaceBetween: 16,
22
22
  pagination: {
23
23
  clickable: true,
@@ -29,3 +29,19 @@ export const Primary = {
29
29
  hasPagination: true,
30
30
  },
31
31
  };
32
+ export const LoopingCarousel = {
33
+ args: {
34
+ children: [_jsx("div", { children: "Slide A" }), _jsx("div", { children: "Slide B" }), _jsx("div", { children: "Slide C" }), _jsx("div", { children: "Slide D" })],
35
+ slidesPerView: { sm: 1, md: 1, lg: 1, xl: 1 },
36
+ spaceBetween: 24,
37
+ loop: true,
38
+ pagination: {
39
+ clickable: true,
40
+ },
41
+ navigation: {
42
+ positionY: "top",
43
+ },
44
+ hasNavigation: true,
45
+ hasPagination: true,
46
+ },
47
+ };
@@ -1,14 +1,16 @@
1
1
  import { WordpressDefault } from "../../global.types";
2
- export type Attributes = {
2
+ export type CarouselAttributes = {
3
3
  context?: 'wp-editor';
4
4
  slidesPerView?: {
5
5
  sm: number;
6
6
  md: number;
7
7
  lg: number;
8
+ xl: number;
8
9
  };
9
10
  spaceBetween?: number;
10
11
  hasPagination?: boolean;
11
12
  hasNavigation?: boolean;
13
+ loop?: boolean;
12
14
  pagination?: {
13
15
  clickable: boolean;
14
16
  };
@@ -16,6 +18,6 @@ export type Attributes = {
16
18
  positionY: 'bottom' | 'top';
17
19
  };
18
20
  };
19
- export interface CarouselProps extends Attributes, WordpressDefault {
21
+ export interface CarouselProps extends CarouselAttributes, WordpressDefault {
20
22
  children: React.ReactNode | React.ReactNode[];
21
23
  }
@@ -1,14 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useReactAdapter } from "../../hooks/useReactAdaptater";
3
3
  export const Tabs = ({ tabs, children, extraClassNames, edit }) => {
4
- const { useEffect, useState, useRef, Children, Fragment } = useReactAdapter();
5
- console.log(edit);
4
+ const { useState, useRef, Children } = useReactAdapter();
6
5
  const [activeTab, setActiveTab] = useState(tabs[0]?.id || "");
7
6
  const tabsRef = useRef([]);
8
- useEffect(() => {
9
- const activeIndex = tabs.findIndex((tab) => tab.id === activeTab);
10
- tabsRef.current[activeIndex]?.focus();
11
- }, [activeTab, tabs]);
12
7
  const classes = ["ga-ds-tabs"];
13
8
  if (extraClassNames) {
14
9
  classes.push(...extraClassNames);
@@ -1,6 +1,7 @@
1
1
  export * from './Card/Card';
2
2
  export * from './Card/Card.types';
3
3
  export * from './Carousel/Carousel';
4
+ export * from './Carousel/Carousel.types';
4
5
  export * from './SpoilerList/SpoilerList';
5
6
  export * from './Tabs/Tabs';
6
7
  export * from './Tabs/Tabs.types';
@@ -1,6 +1,7 @@
1
1
  export * from './Card/Card';
2
2
  export * from './Card/Card.types';
3
3
  export * from './Carousel/Carousel';
4
+ export * from './Carousel/Carousel.types';
4
5
  export * from './SpoilerList/SpoilerList';
5
6
  export * from './Tabs/Tabs';
6
7
  export * from './Tabs/Tabs.types';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growth-angels/ds-core",
3
- "version": "1.3.5",
3
+ "version": "1.5.0",
4
4
  "description": "Design system by Growth Angels",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -17,7 +17,8 @@
17
17
  "types": "./dist/index.d.ts"
18
18
  },
19
19
  "./styles": "./src/styles.ts",
20
- "./styles/scss": "./src/index.scss"
20
+ "./styles/scss": "./src/index.scss",
21
+ "./mixins": "./src/mixins.scss"
21
22
  },
22
23
  "files": [
23
24
  "dist",
@@ -0,0 +1,29 @@
1
+ @mixin button {
2
+ padding: var(--ga-button-padding);
3
+ border: none;
4
+ font-size: var(--ga-font-sizes-base);
5
+ font-weight: var(--ga-font-weight-base);
6
+ font-family: var(--ga-font-family-button);
7
+ border-radius: var(--ga-button-radius, 0);
8
+ cursor: pointer;
9
+ }
10
+
11
+ @mixin button--primary {
12
+ background-color: var(--ga-button-background-primary, #eee);
13
+ color: var(--ga-button-color-primary, #000);
14
+ }
15
+
16
+ @mixin button--secondary {
17
+ background-color: var(--ga-button-background-secondary, #424242);
18
+ color: var(--ga-button-color-secondary, #fff);
19
+ }
20
+
21
+ @mixin button--link {
22
+ color: var(--ga-button-color-link, purple);
23
+ }
24
+
25
+ @mixin button--icon {
26
+ --ga-button-padding: 0.8rem 1.2rem;
27
+ justify-content: center;
28
+ align-items: center;
29
+ }
@@ -1,35 +1,21 @@
1
- .ga-ds-button > .wp-block-button__link.wp-element-button,
2
- button.ga-ds-button {
3
- padding: var(--ga-button-padding);
4
- border: none;
5
- font-size: var(--ga-font-sizes-base);
6
- font-weight: var(--ga-font-weight-base);
7
- font-family: var(--ga-font-family-button);
8
- border-radius: var(--ga-button-radius, 0);
9
- cursor: pointer;
1
+ @use "./Button-mixins.scss" as *;
2
+
3
+ .ga-ds-button {
4
+ @include button;
10
5
  }
11
6
 
12
- .ga-ds-button--primary > .wp-block-button__link.wp-element-button,
13
- button.ga-ds-button--primary {
14
- background-color: var(--ga-button-background-primary, #eee);
15
- color: var(--ga-button-color-primary, #000);
7
+ .ga-ds-button--primary {
8
+ @include button--primary;
16
9
  }
17
10
 
18
- .ga-ds-button--secondary > .wp-block-button__link.wp-element-button,
19
- button.ga-ds-button--secondary {
20
- background-color: var(--ga-button-background-secondary, #424242);
21
- color: var(--ga-button-color-secondary, #fff);
11
+ .ga-ds-button--secondary {
12
+ @include button--secondary;
22
13
  }
23
14
 
24
- .ga-ds-button--link > .wp-block-button__link.wp-element-button,
25
- button.ga-ds-button--link {
26
- color: var(--ga-button-color-link, purple);
27
- padding: 0;
15
+ .ga-ds-button--link {
16
+ @include button--link;
28
17
  }
29
18
 
30
- .ga-ds-button--icon > .wp-block-button__link.wp-element-button,
31
- button.ga-ds-button--icon {
32
- --ga-button-padding: 0.8rem 1.2rem;
33
- justify-content: center;
34
- align-items: center;
19
+ .ga-ds-button--icon {
20
+ @include button--icon;
35
21
  }
@@ -0,0 +1 @@
1
+ @forward "./Button/Button-mixins.scss";
@@ -1,17 +1,19 @@
1
1
  import { useReactAdapter } from './useReactAdaptater'
2
2
 
3
- export function useBreakpointObserver(breakpoints = { sm: 640, md: 1024, lg: 1280 }) {
3
+ export function useBreakpointObserver(breakpoints = { sm: 640, md: 1024, lg: 1280, xl: 1400 }) {
4
4
  const { useEffect, useState } = useReactAdapter()
5
- const [current, setCurrent] = useState<'sm' | 'md' | 'lg'>('sm')
5
+ const [current, setCurrent] = useState<'sm' | 'md' | 'lg' | 'xl'>('sm')
6
6
  useEffect(() => {
7
7
  let frameId: number
8
8
 
9
9
  const check = () => {
10
10
  const width = window.innerWidth
11
- let bp: 'sm' | 'md' | 'lg' = 'sm'
11
+ let bp: 'sm' | 'md' | 'lg' | 'xl' = 'sm'
12
12
 
13
- if (width >= breakpoints.md) bp = 'lg'
14
- else if (width >= breakpoints.sm) bp = 'md'
13
+ if (width >= breakpoints.xl) bp = 'xl'
14
+ else if (width >= breakpoints.lg) bp = 'lg'
15
+ else if (width >= breakpoints.md) bp = 'md'
16
+ else if (width >= breakpoints.sm) bp = 'sm'
15
17
 
16
18
  setCurrent((prev) => (prev !== bp ? bp : prev))
17
19
  frameId = requestAnimationFrame(check)
@@ -0,0 +1 @@
1
+ @forward "./atoms/atoms-mixins.scss";
@@ -21,7 +21,7 @@ export const Primary: Story = {
21
21
  <div>Slide 6</div>,
22
22
  <div>Slide 7</div>,
23
23
  ],
24
- slidesPerView: { sm: 1, md: 2, lg: 3 },
24
+ slidesPerView: { sm: 1, md: 2, lg: 3, xl: 4 },
25
25
  spaceBetween: 16,
26
26
  pagination: {
27
27
  clickable: true,
@@ -33,3 +33,20 @@ export const Primary: Story = {
33
33
  hasPagination: true,
34
34
  },
35
35
  }
36
+
37
+ export const LoopingCarousel: Story = {
38
+ args: {
39
+ children: [<div>Slide A</div>, <div>Slide B</div>, <div>Slide C</div>, <div>Slide D</div>],
40
+ slidesPerView: { sm: 1, md: 1, lg: 1, xl: 1 },
41
+ spaceBetween: 24,
42
+ loop: true,
43
+ pagination: {
44
+ clickable: true,
45
+ },
46
+ navigation: {
47
+ positionY: "top",
48
+ },
49
+ hasNavigation: true,
50
+ hasPagination: true,
51
+ },
52
+ }
@@ -6,16 +6,18 @@ import { CarouselProps } from "./Carousel.types"
6
6
  export const Carousel = (props: CarouselProps) => {
7
7
  const {
8
8
  children,
9
- slidesPerView = { sm: 1, md: 2, lg: 3 },
9
+ slidesPerView = { sm: 1, md: 2, lg: 3, xl: 4 },
10
10
  spaceBetween = 20,
11
11
  navigation,
12
12
  pagination,
13
13
  context,
14
14
  hasPagination,
15
15
  hasNavigation,
16
+ loop = false,
16
17
  } = props
17
18
  const { useEffect, useState, useRef, Children } = useReactAdapter()
18
19
  const trackRef = useRef<HTMLDivElement>(null)
20
+ const isNavigatingRef = useRef(false)
19
21
  const slides = Children.toArray(children)
20
22
 
21
23
  const calculatePages = (totalSlides = 0, slidesPerView = 0, step = 1) => {
@@ -23,54 +25,133 @@ export const Carousel = (props: CarouselProps) => {
23
25
  }
24
26
 
25
27
  const [activeSlideIndex, setActiveSlideIndex] = useState(0)
28
+ const [activeDOMIndex, setActiveDOMIndex] = useState(0)
26
29
  const [isAtEnd, setIsAtEnd] = useState(false)
27
30
  const [isAtStart, setIsAtStart] = useState(true)
28
31
  const [totalPages, setTotalPages] = useState(0)
29
32
 
30
33
  useEffect(() => {
31
- setIsAtStart(activeSlideIndex === 0)
32
- setIsAtEnd(activeSlideIndex === totalPages - 1)
33
- }, [activeSlideIndex, totalPages])
34
+ if (loop) {
35
+ setIsAtStart(false)
36
+ setIsAtEnd(false)
37
+ } else {
38
+ setIsAtStart(activeSlideIndex === 0)
39
+ setIsAtEnd(activeSlideIndex === totalPages - 1)
40
+ }
41
+ }, [activeSlideIndex, totalPages, loop])
34
42
 
35
- const breakpoint = useBreakpointObserver({ sm: 768, md: 992, lg: 1200 })
43
+ const breakpoint = useBreakpointObserver({ sm: 768, md: 992, lg: 1200, xl: 1400 })
36
44
 
37
45
  useEffect(() => {
38
46
  setTotalPages(calculatePages(slides.length, slidesPerView[breakpoint], 1))
39
47
  }, [breakpoint, slidesPerView])
40
48
 
49
+ useEffect(() => {
50
+ const track = trackRef.current
51
+ if (!track) return
52
+
53
+ const handleScroll = () => {
54
+ if (loop) {
55
+ const slideWidth = track.scrollWidth / (slides.length * 3)
56
+ const scrollLeft = track.scrollLeft
57
+
58
+ // Repositionnement infini (uniquement si pas en navigation)
59
+ if (!isNavigatingRef.current) {
60
+ if (scrollLeft <= slideWidth * slides.length) {
61
+ track.scrollLeft = scrollLeft + slideWidth * slides.length
62
+ } else if (scrollLeft >= slideWidth * slides.length * 2) {
63
+ track.scrollLeft = scrollLeft - slideWidth * slides.length
64
+ }
65
+ }
66
+
67
+ const scrollPosition = track.scrollLeft + slideWidth / 2
68
+ const domIndex = Math.floor(scrollPosition / slideWidth)
69
+ const index = domIndex % slides.length
70
+ setActiveDOMIndex(domIndex)
71
+ setActiveSlideIndex(index)
72
+ } else {
73
+ const slideWidth = track.scrollWidth / slides.length
74
+ const scrollPosition = track.scrollLeft + slideWidth / 2
75
+ const index = Math.floor(scrollPosition / slideWidth)
76
+ setActiveSlideIndex(Math.min(index, slides.length - 1))
77
+ setActiveDOMIndex(index)
78
+ }
79
+ }
80
+
81
+ track.addEventListener("scroll", handleScroll)
82
+
83
+ // Position initiale au milieu en mode loop
84
+ if (loop) {
85
+ setTimeout(() => {
86
+ const slideWidth = track.scrollWidth / (slides.length * 3)
87
+ track.scrollLeft = slideWidth * slides.length
88
+ }, 0)
89
+ }
90
+
91
+ return () => track.removeEventListener("scroll", handleScroll)
92
+ }, [slides.length, loop])
93
+
41
94
  const style = Object.fromEntries(
42
95
  Object.entries(slidesPerView).map(([key, value]) => [`--ga-ds-slides-per-view-${key}`, `${value}`])
43
96
  )
44
97
 
45
98
  const goPrev = () => {
46
99
  if (!trackRef.current) return
47
- const slides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide")
48
- if (activeSlideIndex === 0) return
100
+ const allSlides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide")
101
+ if (activeSlideIndex === 0 && !loop) return
49
102
 
50
- const nextIndex = activeSlideIndex - 1
51
- slides[nextIndex]?.scrollIntoView({
52
- behavior: "smooth",
53
- block: "nearest",
54
- inline: "start",
55
- })
56
- setActiveSlideIndex(nextIndex)
103
+ isNavigatingRef.current = true
104
+
105
+ if (loop) {
106
+ // En mode loop, on utilise l'index DOM actuel
107
+ allSlides[activeDOMIndex - 1]?.scrollIntoView({
108
+ behavior: "smooth",
109
+ block: "nearest",
110
+ inline: "start",
111
+ })
112
+ } else {
113
+ const nextIndex = activeSlideIndex - 1
114
+ allSlides[nextIndex]?.scrollIntoView({
115
+ behavior: "smooth",
116
+ block: "nearest",
117
+ inline: "start",
118
+ })
119
+ }
120
+
121
+ setTimeout(() => {
122
+ isNavigatingRef.current = false
123
+ }, 600)
57
124
  }
58
125
 
59
126
  const goNext = () => {
60
127
  if (!trackRef.current) return
61
- const slides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide")
62
- const totalSlides = slides.length
128
+ const allSlides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide")
63
129
 
64
- if (activeSlideIndex >= totalSlides - 1) return
65
- if (isAtEnd) return
130
+ isNavigatingRef.current = true
66
131
 
67
- const nextIndex = activeSlideIndex + 1
68
- slides[nextIndex]?.scrollIntoView({
69
- behavior: "smooth",
70
- block: "nearest",
71
- inline: "start",
72
- })
73
- setActiveSlideIndex(nextIndex)
132
+ if (loop) {
133
+ // En mode loop, on utilise l'index DOM actuel
134
+ allSlides[activeDOMIndex + 1]?.scrollIntoView({
135
+ behavior: "smooth",
136
+ block: "nearest",
137
+ inline: "start",
138
+ })
139
+ } else {
140
+ const totalSlides = allSlides.length
141
+ if (activeSlideIndex >= totalSlides - 1) return
142
+ if (isAtEnd) return
143
+
144
+ const nextIndex = activeSlideIndex + 1
145
+ allSlides[nextIndex]?.scrollIntoView({
146
+ behavior: "smooth",
147
+ block: "nearest",
148
+ inline: "start",
149
+ })
150
+ }
151
+
152
+ setTimeout(() => {
153
+ isNavigatingRef.current = false
154
+ }, 600)
74
155
  }
75
156
 
76
157
  const goTo = (nextIndex: number) => {
@@ -81,13 +162,16 @@ export const Carousel = (props: CarouselProps) => {
81
162
  block: "nearest",
82
163
  inline: "start",
83
164
  })
84
- setActiveSlideIndex(nextIndex)
85
165
  }
86
166
 
87
167
  const classes = ["ga-ds-carousel"]
88
168
 
89
169
  if (props.extraClassNames) {
90
- classes.push(...props.extraClassNames)
170
+ if (typeof props.extraClassNames === "string") {
171
+ classes.push(props.extraClassNames)
172
+ } else {
173
+ classes.push(...props.extraClassNames)
174
+ }
91
175
  }
92
176
 
93
177
  return (
@@ -106,14 +190,16 @@ export const Carousel = (props: CarouselProps) => {
106
190
  >
107
191
  {context === "wp-editor"
108
192
  ? children
109
- : slides.map((child, index) => (
110
- <div
111
- key={index}
112
- className={`ga-ds-carousel__slide ${index === activeSlideIndex ? "ga-ds-carousel__slide--active" : ""}`}
113
- >
114
- {child}
115
- </div>
116
- ))}
193
+ : slides.map((child, index) => {
194
+ return (
195
+ <div
196
+ key={index}
197
+ className={`ga-ds-carousel__slide ${index === activeDOMIndex ? "ga-ds-carousel__slide--active" : ""}`}
198
+ >
199
+ {child}
200
+ </div>
201
+ )
202
+ })}
117
203
  </div>
118
204
 
119
205
  <div className="ga-ds-carousel__navigation">
@@ -1,15 +1,17 @@
1
1
  import { WordpressDefault } from "../../global.types"
2
2
 
3
- export type Attributes = {
3
+ export type CarouselAttributes = {
4
4
  context?: 'wp-editor'
5
5
  slidesPerView?: {
6
6
  sm: number
7
7
  md: number
8
8
  lg: number
9
+ xl: number
9
10
  }
10
11
  spaceBetween?: number
11
12
  hasPagination?: boolean
12
13
  hasNavigation?: boolean
14
+ loop?: boolean
13
15
  pagination?: {
14
16
  clickable: boolean
15
17
  },
@@ -17,6 +19,6 @@ export type Attributes = {
17
19
  positionY: 'bottom' | 'top';
18
20
  }
19
21
  }
20
- export interface CarouselProps extends Attributes, WordpressDefault {
22
+ export interface CarouselProps extends CarouselAttributes, WordpressDefault {
21
23
  children: React.ReactNode | React.ReactNode[]
22
24
  }
@@ -1,16 +1,11 @@
1
1
  import { useReactAdapter } from "../../hooks/useReactAdaptater"
2
2
  import { TabsProps } from "./Tabs.types"
3
3
  export const Tabs = ({ tabs, children, extraClassNames, edit }: TabsProps) => {
4
- const { useEffect, useState, useRef, Children, Fragment } = useReactAdapter()
5
- console.log(edit)
4
+ const { useState, useRef, Children } = useReactAdapter()
5
+
6
6
  const [activeTab, setActiveTab] = useState(tabs[0]?.id || "")
7
7
  const tabsRef = useRef<Array<HTMLButtonElement | null>>([])
8
8
 
9
- useEffect(() => {
10
- const activeIndex = tabs.findIndex((tab) => tab.id === activeTab)
11
- tabsRef.current[activeIndex]?.focus()
12
- }, [activeTab, tabs])
13
-
14
9
  const classes = ["ga-ds-tabs"]
15
10
  if (extraClassNames) {
16
11
  classes.push(...extraClassNames)
@@ -1,6 +1,7 @@
1
1
  export * from './Card/Card'
2
2
  export * from './Card/Card.types'
3
3
  export * from './Carousel/Carousel'
4
+ export * from './Carousel/Carousel.types'
4
5
  export * from './SpoilerList/SpoilerList'
5
6
  export * from './Tabs/Tabs'
6
7
  export * from './Tabs/Tabs.types'