Merged in feat/SW-1542-carousel-functionality (pull request #1311)
feat(SW-1542): Carousel component * feat(SW-1542): add Embla Carousel component and use in CarouselCards * fix(SW-1542): remove max-width constraint for card on ipad * fix(SW-1542): Add padding to start page content container * refactor(SW-1542): Improve Embla Carousel type imports * refactor(SW-1542): Remove unnecessary carousel wrapper div * refactor(SW-1542): Modularize Carousel component structure * refactor(SW-1542): Remove carousel dots display * feat(SW-1542): Add carousel dots navigation * refactor(SW-1542): Update Carousel component styling and types * refactor(SW-1542): Remove uneeded useCallback from Carousel navigation methods * refactor(SW-1542): Modify CarouselContextProps type to exclude className * refactor(SW-1542): Optimize React imports in Carousel components * refactor(SW-1542): Consolidate Carousel component and remove CarouselRoot * refactor(SW-1542): Update Carousel navigation methods to use function-based scroll checks * refactor(SW-1542): Add explicit children prop support to CarouselContent component * refactor(SW-1542): Add children prop support to CarouselItem component Approved-by: Christian Andolf
This commit is contained in:
@@ -29,3 +29,7 @@
|
|||||||
grid-auto-columns: calc((100% - var(--Spacing-x3) * 2) / 3);
|
grid-auto-columns: calc((100% - var(--Spacing-x3) * 2) / 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navigationButton {
|
||||||
|
top: 30%;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
|
||||||
|
import { Carousel } from "@/components/Carousel"
|
||||||
import ContentCard from "@/components/ContentCard"
|
import ContentCard from "@/components/ContentCard"
|
||||||
import SectionContainer from "@/components/Section/Container"
|
import SectionContainer from "@/components/Section/Container"
|
||||||
import SectionHeader from "@/components/Section/Header"
|
import SectionHeader from "@/components/Section/Header"
|
||||||
@@ -43,19 +44,24 @@ export default function CarouselCards({ carousel_cards }: CarouselCardsProps) {
|
|||||||
onFilterSelect={setActiveFilter}
|
onFilterSelect={setActiveFilter}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* Carousel functionality will go here */}
|
<Carousel>
|
||||||
<div className={styles.cardsContainer}>
|
<Carousel.Content>
|
||||||
{filteredCards.map((card) => (
|
{filteredCards.map((card, index) => (
|
||||||
<ContentCard
|
<Carousel.Item key={`${card.heading}-${index}`}>
|
||||||
key={card.heading}
|
<ContentCard
|
||||||
heading={card.heading}
|
heading={card.heading}
|
||||||
image={card.image}
|
image={card.image}
|
||||||
bodyText={card.bodyText}
|
bodyText={card.bodyText}
|
||||||
promoText={card.promoText}
|
promoText={card.promoText}
|
||||||
link={card.link}
|
link={card.link}
|
||||||
/>
|
/>
|
||||||
))}
|
</Carousel.Item>
|
||||||
</div>
|
))}
|
||||||
|
</Carousel.Content>
|
||||||
|
<Carousel.Previous className={styles.navigationButton} />
|
||||||
|
<Carousel.Next className={styles.navigationButton} />
|
||||||
|
<Carousel.Dots />
|
||||||
|
</Carousel>
|
||||||
<SectionLink link={link} variant="mobile" />
|
<SectionLink link={link} variant="mobile" />
|
||||||
</SectionContainer>
|
</SectionContainer>
|
||||||
)
|
)
|
||||||
|
|||||||
23
components/Carousel/CarouselContent.tsx
Normal file
23
components/Carousel/CarouselContent.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { cx } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { useCarousel } from "./CarouselContext"
|
||||||
|
|
||||||
|
import styles from "./carousel.module.css"
|
||||||
|
|
||||||
|
export function CarouselContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
const { carouselRef } = useCarousel()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={carouselRef} className={styles.viewport}>
|
||||||
|
<div className={cx(styles.container, className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
17
components/Carousel/CarouselContext.tsx
Normal file
17
components/Carousel/CarouselContext.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { createContext, useContext } from "react"
|
||||||
|
|
||||||
|
import type { CarouselContextProps } from "./types"
|
||||||
|
|
||||||
|
const CarouselContext = createContext<CarouselContextProps | null>(null)
|
||||||
|
|
||||||
|
export function useCarousel() {
|
||||||
|
const context = useContext(CarouselContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useCarousel must be used within a <Carousel />")
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
export { CarouselContext }
|
||||||
45
components/Carousel/CarouselDots.tsx
Normal file
45
components/Carousel/CarouselDots.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { cx } from "class-variance-authority"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
|
import { useCarousel } from "./CarouselContext"
|
||||||
|
|
||||||
|
import styles from "./carousel.module.css"
|
||||||
|
|
||||||
|
export function CarouselDots({ className }: { className?: string }) {
|
||||||
|
const { selectedIndex, api } = useCarousel()
|
||||||
|
const [scrollSnaps, setScrollSnaps] = useState<number[]>([])
|
||||||
|
|
||||||
|
// Update scroll snaps when the carousel is initialized or viewport changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!api) return
|
||||||
|
|
||||||
|
const onInit = () => {
|
||||||
|
setScrollSnaps(api.scrollSnapList())
|
||||||
|
}
|
||||||
|
|
||||||
|
onInit()
|
||||||
|
api.on("reInit", onInit)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
api.off("reInit", onInit)
|
||||||
|
}
|
||||||
|
}, [api])
|
||||||
|
|
||||||
|
// Don't render dots if we have 1 or fewer scroll positions
|
||||||
|
if (scrollSnaps.length <= 1) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx(styles.dots, className)}>
|
||||||
|
{scrollSnaps.map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={styles.dot}
|
||||||
|
data-active={index === selectedIndex}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
22
components/Carousel/CarouselItem.tsx
Normal file
22
components/Carousel/CarouselItem.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { cx } from "class-variance-authority"
|
||||||
|
|
||||||
|
import styles from "./carousel.module.css"
|
||||||
|
|
||||||
|
export function CarouselItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="group"
|
||||||
|
aria-roledescription="slide"
|
||||||
|
className={cx(styles.item, className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
45
components/Carousel/CarouselNavigation.tsx
Normal file
45
components/Carousel/CarouselNavigation.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { cx } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { ArrowRightIcon } from "@/components/Icons"
|
||||||
|
|
||||||
|
import { useCarousel } from "./CarouselContext"
|
||||||
|
|
||||||
|
import styles from "./carousel.module.css"
|
||||||
|
|
||||||
|
import type { CarouselButtonProps } from "./types"
|
||||||
|
|
||||||
|
export function CarouselPrevious({ className, ...props }: CarouselButtonProps) {
|
||||||
|
const { scrollPrev, canScrollPrev } = useCarousel()
|
||||||
|
|
||||||
|
if (!canScrollPrev()) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={cx(styles.button, styles.buttonPrev, className)}
|
||||||
|
onClick={scrollPrev}
|
||||||
|
aria-label="Previous slide"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ArrowRightIcon color="burgundy" className={styles.prevIcon} />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CarouselNext({ className, ...props }: CarouselButtonProps) {
|
||||||
|
const { scrollNext, canScrollNext } = useCarousel()
|
||||||
|
|
||||||
|
if (!canScrollNext()) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={cx(styles.button, styles.buttonNext, className)}
|
||||||
|
onClick={scrollNext}
|
||||||
|
aria-label="Next slide"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ArrowRightIcon color="burgundy" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
82
components/Carousel/carousel.module.css
Normal file
82
components/Carousel/carousel.module.css
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
.root {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewport {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-auto-columns: 100%;
|
||||||
|
gap: var(--Spacing-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: var(--Base-Surface-Primary-light-Normal);
|
||||||
|
box-shadow: 0px 0px 8px 1px rgba(0, 0, 0, 0.1);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: var(--Spacing-x1);
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonPrev {
|
||||||
|
left: calc(-1 * var(--Spacing-x3));
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonNext {
|
||||||
|
right: calc(-1 * var(--Spacing-x3));
|
||||||
|
}
|
||||||
|
|
||||||
|
.prevIcon {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dots {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
margin-top: var(--Spacing-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
height: 8px;
|
||||||
|
width: 8px;
|
||||||
|
border-radius: var(--Corner-radius-Large);
|
||||||
|
background: var(--UI-Grey-40);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot[data-active="true"] {
|
||||||
|
width: 22px;
|
||||||
|
background: var(--UI-Text-Medium-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.container {
|
||||||
|
grid-auto-columns: calc((100% - var(--Spacing-x3)) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.container {
|
||||||
|
grid-auto-columns: calc((100% - var(--Spacing-x3) * 2) / 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
108
components/Carousel/index.tsx
Normal file
108
components/Carousel/index.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { cx } from "class-variance-authority"
|
||||||
|
import useEmblaCarousel from "embla-carousel-react"
|
||||||
|
import { useCallback, useEffect, useState } from "react"
|
||||||
|
|
||||||
|
import { CarouselContent } from "./CarouselContent"
|
||||||
|
import { CarouselContext } from "./CarouselContext"
|
||||||
|
import { CarouselDots } from "./CarouselDots"
|
||||||
|
import { CarouselItem } from "./CarouselItem"
|
||||||
|
import { CarouselNext, CarouselPrevious } from "./CarouselNavigation"
|
||||||
|
|
||||||
|
import styles from "./carousel.module.css"
|
||||||
|
|
||||||
|
import type { CarouselApi, CarouselProps } from "./types"
|
||||||
|
|
||||||
|
function Carousel({
|
||||||
|
opts,
|
||||||
|
setApi,
|
||||||
|
plugins,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}: CarouselProps) {
|
||||||
|
const [carouselRef, api] = useEmblaCarousel(
|
||||||
|
{
|
||||||
|
...opts,
|
||||||
|
axis: "x",
|
||||||
|
},
|
||||||
|
plugins
|
||||||
|
)
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||||
|
|
||||||
|
const onSelect = useCallback((api: CarouselApi) => {
|
||||||
|
if (!api) return
|
||||||
|
setSelectedIndex(api.selectedScrollSnap())
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
function scrollPrev() {
|
||||||
|
api?.scrollPrev()
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollNext() {
|
||||||
|
api?.scrollNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
|
if (event.key === "ArrowLeft") {
|
||||||
|
event.preventDefault()
|
||||||
|
scrollPrev()
|
||||||
|
} else if (event.key === "ArrowRight") {
|
||||||
|
event.preventDefault()
|
||||||
|
scrollNext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!api || !setApi) return
|
||||||
|
setApi(api)
|
||||||
|
}, [api, setApi])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!api) return
|
||||||
|
|
||||||
|
onSelect(api)
|
||||||
|
api.on("reInit", onSelect)
|
||||||
|
api.on("select", onSelect)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
api.off("select", onSelect)
|
||||||
|
}
|
||||||
|
}, [api, onSelect])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CarouselContext.Provider
|
||||||
|
value={{
|
||||||
|
carouselRef,
|
||||||
|
api,
|
||||||
|
opts,
|
||||||
|
plugins,
|
||||||
|
setApi,
|
||||||
|
children,
|
||||||
|
scrollPrev,
|
||||||
|
scrollNext,
|
||||||
|
canScrollPrev: api?.canScrollPrev.bind(api) ?? (() => false),
|
||||||
|
canScrollNext: api?.canScrollNext.bind(api) ?? (() => false),
|
||||||
|
selectedIndex,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onKeyDownCapture={handleKeyDown}
|
||||||
|
className={cx(styles.root, className)}
|
||||||
|
role="region"
|
||||||
|
aria-roledescription="carousel"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</CarouselContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Carousel.Content = CarouselContent
|
||||||
|
Carousel.Item = CarouselItem
|
||||||
|
Carousel.Next = CarouselNext
|
||||||
|
Carousel.Previous = CarouselPrevious
|
||||||
|
Carousel.Dots = CarouselDots
|
||||||
|
|
||||||
|
export { Carousel }
|
||||||
|
export type { CarouselApi }
|
||||||
34
components/Carousel/types.ts
Normal file
34
components/Carousel/types.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Must be imported from the core package.
|
||||||
|
* @see https://www.embla-carousel.com/api/methods/#typescript
|
||||||
|
*/
|
||||||
|
import type { EmblaCarouselType } from "embla-carousel"
|
||||||
|
import type useEmblaCarousel from "embla-carousel-react"
|
||||||
|
import type { PropsWithChildren } from "react"
|
||||||
|
|
||||||
|
export type CarouselApi = EmblaCarouselType
|
||||||
|
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
||||||
|
type CarouselOptions = UseCarouselParameters[0]
|
||||||
|
type CarouselPlugin = UseCarouselParameters[1]
|
||||||
|
|
||||||
|
export interface CarouselProps extends PropsWithChildren {
|
||||||
|
opts?: CarouselOptions
|
||||||
|
plugins?: CarouselPlugin
|
||||||
|
setApi?: (api: CarouselApi) => void
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CarouselContextProps extends Omit<CarouselProps, "className"> {
|
||||||
|
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
||||||
|
api: ReturnType<typeof useEmblaCarousel>[1]
|
||||||
|
scrollPrev: () => void
|
||||||
|
scrollNext: () => void
|
||||||
|
canScrollPrev: () => boolean
|
||||||
|
canScrollNext: () => boolean
|
||||||
|
selectedIndex: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CarouselButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
@@ -44,10 +44,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.card {
|
|
||||||
max-width: 413px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding: var(--Spacing-x2) var(--Spacing-x2) var(--Spacing-x2) 0;
|
padding: var(--Spacing-x2) var(--Spacing-x2) var(--Spacing-x2) 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,12 +49,6 @@
|
|||||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
|
||||||
.main {
|
|
||||||
gap: calc(var(--Spacing-x5) * 3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.section:empty {
|
.section:empty {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -65,3 +59,13 @@
|
|||||||
max-width: var(--max-width-content);
|
max-width: var(--max-width-content);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.main {
|
||||||
|
gap: calc(var(--Spacing-x5) * 3);
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
padding-left: var(--Spacing-x5);
|
||||||
|
padding-right: var(--Spacing-x5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
30
package-lock.json
generated
30
package-lock.json
generated
@@ -39,6 +39,8 @@
|
|||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
"downshift": "^9.0.8",
|
"downshift": "^9.0.8",
|
||||||
|
"embla-carousel": "^8.5.2",
|
||||||
|
"embla-carousel-react": "^8.5.2",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fetch-retry": "^6.0.0",
|
"fetch-retry": "^6.0.0",
|
||||||
"framer-motion": "^11.3.28",
|
"framer-motion": "^11.3.28",
|
||||||
@@ -11849,6 +11851,34 @@
|
|||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.52.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.52.tgz",
|
||||||
"integrity": "sha512-xtoijJTZ+qeucLBDNztDOuQBE1ksqjvNjvqFoST3nGC7fSpqJ+X6BdTBaY5BHG+IhWWmpc6b/KfpeuEDupEPOQ=="
|
"integrity": "sha512-xtoijJTZ+qeucLBDNztDOuQBE1ksqjvNjvqFoST3nGC7fSpqJ+X6BdTBaY5BHG+IhWWmpc6b/KfpeuEDupEPOQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/embla-carousel": {
|
||||||
|
"version": "8.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.5.2.tgz",
|
||||||
|
"integrity": "sha512-xQ9oVLrun/eCG/7ru3R+I5bJ7shsD8fFwLEY7yPe27/+fDHCNj0OT5EoG5ZbFyOxOcG6yTwW8oTz/dWyFnyGpg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/embla-carousel-react": {
|
||||||
|
"version": "8.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.5.2.tgz",
|
||||||
|
"integrity": "sha512-Tmx+uY3MqseIGdwp0ScyUuxpBgx5jX1f7od4Cm5mDwg/dptEiTKf9xp6tw0lZN2VA9JbnVMl/aikmbc53c6QFA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"embla-carousel": "8.5.2",
|
||||||
|
"embla-carousel-reactive-utils": "8.5.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/embla-carousel-reactive-utils": {
|
||||||
|
"version": "8.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.5.2.tgz",
|
||||||
|
"integrity": "sha512-QC8/hYSK/pEmqEdU1IO5O+XNc/Ptmmq7uCB44vKplgLKhB/l0+yvYx0+Cv0sF6Ena8Srld5vUErZkT+yTahtDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"embla-carousel": "8.5.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/emittery": {
|
"node_modules/emittery": {
|
||||||
"version": "0.13.1",
|
"version": "0.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
|
||||||
|
|||||||
@@ -54,6 +54,8 @@
|
|||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
"downshift": "^9.0.8",
|
"downshift": "^9.0.8",
|
||||||
|
"embla-carousel": "^8.5.2",
|
||||||
|
"embla-carousel-react": "^8.5.2",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fetch-retry": "^6.0.0",
|
"fetch-retry": "^6.0.0",
|
||||||
"framer-motion": "^11.3.28",
|
"framer-motion": "^11.3.28",
|
||||||
|
|||||||
Reference in New Issue
Block a user