UNPKG

@spark-ui/components

Version:

Spark (Leboncoin design system) components.

716 lines (694 loc) 20.8 kB
import { IconButton } from "../chunk-QLOIAU3C.mjs"; import "../chunk-USSL4UZ5.mjs"; import "../chunk-MUNDKRAE.mjs"; import { Icon } from "../chunk-AESXFMCC.mjs"; import "../chunk-NBZKMCHF.mjs"; import "../chunk-4F5DOL57.mjs"; // src/carousel/Carousel.tsx import { cx } from "class-variance-authority"; import { createContext, useContext } from "react"; // src/carousel/useCarousel.ts import { useCallback as useCallback2, useEffect as useEffect3, useId, useLayoutEffect as useLayoutEffect3, useRef as useRef4, useState as useState2 } from "react"; // src/carousel/useEvent.ts import { useCallback, useLayoutEffect, useRef } from "react"; function useEvent(callback) { const ref = useRef(() => { throw new Error("Cannot call an event handler while rendering."); }); useLayoutEffect(() => { ref.current = callback; }); return useCallback((...args) => ref.current?.(...args), []); } // src/carousel/useIsMounted.ts import { useEffect, useRef as useRef2 } from "react"; var useIsMounted = () => { const isMounted = useRef2(false); useEffect(() => { isMounted.current = true; return () => { isMounted.current = false; }; }, []); return isMounted; }; // src/carousel/useScrollEnd.ts import { useEffect as useEffect2, useRef as useRef3 } from "react"; function useScrollEnd(scrollRef, callback) { const scrollLeft = useRef3(0); const safariTimeout = useRef3(null); useEffect2(() => { const element = scrollRef.current; if (!element) return; const supportsScrollend = "onscrollend" in window; const handleScrollEnd = () => { callback(); }; const handleSafariScroll = () => { if (safariTimeout.current) { clearTimeout(safariTimeout.current); } if (scrollRef.current) { scrollLeft.current = scrollRef.current.scrollLeft; safariTimeout.current = setTimeout(() => { if (scrollRef.current) { handleScrollEnd(); } }, 150); } }; if (supportsScrollend) { element.addEventListener("scrollend", handleScrollEnd); } else { element.addEventListener("scroll", handleSafariScroll); } return () => { if (safariTimeout.current) { clearTimeout(safariTimeout.current); } if (supportsScrollend) { element.removeEventListener("scrollend", handleScrollEnd); } else { element.removeEventListener("scroll", handleSafariScroll); } }; }, [callback, scrollRef]); } // src/carousel/useSnapPoints.ts import { useMemo, useState } from "react"; // src/carousel/useResizeObserver.ts import { useLayoutEffect as useLayoutEffect2 } from "react"; function useResizeObserver(ref, callback) { useLayoutEffect2(() => { const element = ref.current; if (!element) return; const observer = new ResizeObserver((entries) => { for (const entry of entries) { callback(entry.contentRect.width); } }); observer.observe(element); return () => observer.disconnect(); }, [ref, callback]); } // src/carousel/utils.ts function getSnapIndices({ totalSlides, slidesPerMove, slidesPerPage }) { const slideBy = slidesPerMove === "auto" ? slidesPerPage : slidesPerMove; const snapPoints = []; const lastSnapIndex = Math.floor((totalSlides - slidesPerPage) / slideBy) * slideBy; for (let i = 0; i <= lastSnapIndex; i += slideBy) { snapPoints.push(i); } if (snapPoints[snapPoints.length - 1] !== totalSlides - slidesPerPage) { snapPoints.push(totalSlides - slidesPerPage); } return snapPoints; } function getSlideElements(container) { return container ? Array.from(container.querySelectorAll('[data-part="item"]')) : []; } function isSnapPoint(slideIndex, { container, slidesPerMove, slidesPerPage }) { return getSnapIndices({ totalSlides: getSlideElements(container).length, slidesPerPage, slidesPerMove }).includes(slideIndex); } function getSnapPositions({ container, slidesPerMove, slidesPerPage }) { if (!container) return []; return getSlideElements(container).filter((_, index) => { return isSnapPoint(index, { slidesPerMove, slidesPerPage, container }); }).map((slide) => slide.offsetLeft); } // src/carousel/useSnapPoints.ts function useSnapPoints(initialSnapPoints = [], { carouselRef, slidesPerMove, slidesPerPage }) { const [pageSnapPoints, setPageSnapPoints] = useState(initialSnapPoints); const stableSnapPoints = useMemo(() => pageSnapPoints, [pageSnapPoints]); useResizeObserver(carouselRef, () => { const newSnapPoints = getSnapPositions({ slidesPerMove, slidesPerPage, container: carouselRef.current }); if (JSON.stringify(pageSnapPoints) !== JSON.stringify(newSnapPoints)) { setPageSnapPoints(newSnapPoints); } }); return [stableSnapPoints, setPageSnapPoints]; } // src/carousel/useCarousel.ts var DATA_SCOPE = "carousel"; var DIRECTION = "ltr"; var useCarousel = ({ defaultPage, gap = 16, snapType = "mandatory", snapStop = "always", scrollPadding = 0, slidesPerPage = 1, slidesPerMove = "auto", scrollBehavior = "smooth", loop = false, // state control page: controlledPage, onPageChange: onPageChangeProp }) => { const carouselId = useId(); const [pageState, setPageState] = useState2(defaultPage || controlledPage || 0); const carouselRef = useRef4(null); const pageIndicatorsRefs = useRef4([]); const isMountedRef = useIsMounted(); const isMounted = isMountedRef.current; const onPageChange = useEvent(onPageChangeProp); const [pageSnapPoints] = useSnapPoints([], { carouselRef, slidesPerMove, slidesPerPage }); const canScrollPrev = useRef4(loop || pageState > 0); const canScrollNext = useRef4(loop || pageState < pageSnapPoints.length - 1); canScrollPrev.current = loop || pageState > 0; canScrollNext.current = loop || pageState < pageSnapPoints.length - 1; const handlePageChange = useCallback2( (page) => { if (page !== pageState) { setPageState(page); onPageChange?.(page); } }, [onPageChange, pageState] ); const scrollTo = useCallback2( (page, behavior) => { if (carouselRef.current) { carouselRef.current.scrollTo({ left: pageSnapPoints[page], behavior: behavior === "instant" ? "auto" : "smooth" }); handlePageChange(page); } }, [handlePageChange, pageSnapPoints] ); const scrollPrev = useCallback2( (cb) => { if (canScrollPrev) { const targetPage = loop && pageState === 0 ? pageSnapPoints.length - 1 : Math.max(pageState - 1, 0); scrollTo(targetPage, scrollBehavior); cb?.(targetPage); } }, [loop, pageSnapPoints, pageState, scrollBehavior, scrollTo] ); const scrollNext = useCallback2( (cb) => { if (canScrollNext) { const targetPage = loop && pageState === pageSnapPoints.length - 1 ? 0 : Math.min(pageState + 1, pageSnapPoints.length - 1); scrollTo(targetPage, scrollBehavior); cb?.(targetPage); } }, [loop, pageSnapPoints, pageState, scrollBehavior, scrollTo] ); useEffect3(() => { if (controlledPage != null) { scrollTo(controlledPage, scrollBehavior); } }, [controlledPage, scrollBehavior, scrollTo]); useLayoutEffect3(() => { if (defaultPage != null && !isMounted && carouselRef.current) { const snapPositions = getSnapPositions({ container: carouselRef.current, slidesPerMove, slidesPerPage }); carouselRef.current.scrollTo({ left: snapPositions[defaultPage], behavior: "instant" }); } }, [defaultPage, isMounted, slidesPerMove, slidesPerPage]); const syncPageStateWithScrollPosition = useCallback2(() => { if (!carouselRef.current || pageSnapPoints.length === 0) return; const { scrollLeft } = carouselRef.current; const distances = pageSnapPoints.map((pagePosition) => Math.abs(scrollLeft - pagePosition)); const pageInViewport = distances.indexOf(Math.min(...distances)); if (pageInViewport !== -1) { handlePageChange(pageInViewport); } }, [pageSnapPoints, handlePageChange]); useScrollEnd(carouselRef, syncPageStateWithScrollPosition); const contextValue = { ref: carouselRef, pageIndicatorsRefs, // props gap, snapType, snapStop, scrollPadding, slidesPerPage, slidesPerMove, scrollBehavior, loop, // computed state page: pageState, pageSnapPoints, canScrollNext: canScrollNext.current, canScrollPrev: canScrollPrev.current, scrollTo, scrollPrev, scrollNext, // anatomy getRootProps: () => ({ id: `carousel::${carouselId}:`, role: "region", "aria-roledescription": "carousel", "data-scope": DATA_SCOPE, "data-part": "root", "data-orientation": "horizontal", dir: DIRECTION, style: { "--slides-per-page": slidesPerPage, "--slide-spacing": `${gap}px`, "--slide-item-size": "calc(100% / var(--slides-per-page) - var(--slide-spacing) * (var(--slides-per-page) - 1) / var(--slides-per-page))" } }), getControlProps: () => ({ "data-scope": DATA_SCOPE, "data-part": "control", "data-orientation": "horizontal" }), getPrevTriggerProps: () => ({ id: `carousel::${carouselId}::prev-trigger`, "aria-controls": `carousel::${carouselId}::item-group`, "data-scope": DATA_SCOPE, "data-part": "prev-trigger", "data-orientation": "horizontal", type: "button", dir: DIRECTION, disabled: !canScrollPrev.current, onClick: () => scrollPrev() }), getNextTriggerProps: () => ({ id: `carousel::${carouselId}::next-trigger`, "aria-controls": `carousel::${carouselId}::item-group`, "data-scope": DATA_SCOPE, "data-part": "next-trigger", "data-orientation": "horizontal", type: "button", dir: DIRECTION, disabled: !canScrollNext.current, onClick: () => scrollNext() }), getSlidesContainerProps: () => ({ id: `carousel::${carouselId}::item-group`, "aria-live": slidesPerPage > 1 ? "off" : "polite", "data-scope": DATA_SCOPE, "data-part": "item-group", "data-orientation": "horizontal", dir: DIRECTION, tabIndex: 0, style: { display: "grid", gap: "var(--slide-spacing)", scrollSnapType: `x ${snapType}`, gridAutoFlow: "column", scrollbarWidth: "none", overscrollBehavior: "contain", gridAutoColumns: "var(--slide-item-size)", overflowX: "auto" }, ref: carouselRef }), getSlideProps: ({ index }) => { const isStopPoint = isSnapPoint(index, { container: carouselRef.current, slidesPerMove, slidesPerPage }); return { id: `carousel::${carouselId}::item:${index}`, role: "group", "aria-roledescription": "slide", "data-scope": DATA_SCOPE, "data-part": "item", "data-index": index, "data-orientation": "horizontal", dir: DIRECTION, style: { ...isStopPoint && { scrollSnapAlign: "start", scrollSnapStop: snapStop } } }; }, getIndicatorGroupProps: () => ({ role: "radiogroup", id: `carousel::${carouselId}::indicator-group`, "data-scope": DATA_SCOPE, "data-part": "indicator-group", "data-orientation": "horizontal", dir: DIRECTION }), getIndicatorProps: ({ index }) => { const isActivePage = index === pageState; return { role: "radio", id: `carousel::${carouselId}::indicator:${index}`, "aria-checked": isActivePage, "data-scope": DATA_SCOPE, "data-part": "indicator", "data-orientation": "horizontal", "data-index": index, "data-state": isActivePage ? "active" : "inactive", tabIndex: isActivePage ? 0 : -1, onClick: () => { scrollTo(index, scrollBehavior); }, onKeyDown: (event) => { const focusActiveIndicator = (page) => { pageIndicatorsRefs.current[page]?.focus(); }; if (event.key === "ArrowRight" && canScrollNext) { scrollNext(focusActiveIndicator); } else if (event.key === "ArrowLeft" && canScrollPrev) { scrollPrev(focusActiveIndicator); } } }; } }; return contextValue; }; // src/carousel/Carousel.tsx import { jsx } from "react/jsx-runtime"; var CarouselContext = createContext(null); var Carousel = ({ className, snapType = "mandatory", snapStop = "always", scrollBehavior = "smooth", slidesPerMove = "auto", slidesPerPage = 1, loop = false, children, gap = 16, defaultPage, page, onPageChange }) => { const carouselApi = useCarousel({ defaultPage, slidesPerPage, slidesPerMove, loop, gap, scrollBehavior, snapStop, snapType, page, onPageChange }); return /* @__PURE__ */ jsx( CarouselContext.Provider, { value: { ...carouselApi, scrollBehavior }, children: /* @__PURE__ */ jsx( "div", { className: cx("gap-lg relative box-border flex flex-col", className), ...carouselApi.getRootProps(), children } ) } ); }; Carousel.displayName = "Carousel"; var useCarouselContext = () => { const context = useContext(CarouselContext); if (!context) { throw Error("useCarouselContext must be used within a Carousel provider"); } return context; }; // src/carousel/CarouselControls.tsx import { jsx as jsx2 } from "react/jsx-runtime"; var CarouselControls = ({ children }) => { const ctx = useCarouselContext(); return /* @__PURE__ */ jsx2( "div", { className: "default:px-lg pointer-events-none absolute inset-0 flex flex-row items-center justify-between", ...ctx.getControlProps(), children } ); }; CarouselControls.displayName = "Carousel.Controls"; // src/carousel/CarouselNextButton.tsx import { ArrowVerticalRight } from "@spark-ui/icons/ArrowVerticalRight"; import { jsx as jsx3 } from "react/jsx-runtime"; var CarouselNextButton = ({ "aria-label": ariaLabel, ...buttonProps }) => { const ctx = useCarouselContext(); return /* @__PURE__ */ jsx3( IconButton, { ...ctx.getNextTriggerProps(), intent: "surface", design: "filled", className: "pointer-events-auto cursor-pointer shadow-sm disabled:invisible", "aria-label": ariaLabel, ...buttonProps, children: /* @__PURE__ */ jsx3(Icon, { children: /* @__PURE__ */ jsx3(ArrowVerticalRight, {}) }) } ); }; CarouselNextButton.displayName = "Carousel.NextButton"; // src/carousel/CarouselPageIndicator.tsx import { cx as cx2 } from "class-variance-authority"; import { useEffect as useEffect4, useRef as useRef5 } from "react"; import { jsx as jsx4 } from "react/jsx-runtime"; var CarouselPageIndicator = ({ children, unstyled = false, index, "aria-label": ariaLabel, className }) => { const ctx = useCarouselContext(); const ref = useRef5(null); useEffect4(() => { if (ctx.pageIndicatorsRefs.current) { ctx.pageIndicatorsRefs.current[index] = ref.current; } }); const styles = cx2( "group h-sz-16 relative flex", "hover:cursor-pointer", "w-sz-16 data-[state=active]:w-sz-44" ); const dotsStyles = cx2( "before:rounded-sm before:block before:size-md", "before:absolute before:left-1/2 before:top-1/2 before:-translate-x-1/2 before:-translate-y-1/2", "data-[state=active]:before:w-sz-32 data-[state=active]:before:bg-basic", "data-[state=inactive]:before:bg-on-surface/dim-3" ); return /* @__PURE__ */ jsx4( "button", { ref, ...ctx.getIndicatorProps({ index }), "aria-label": ariaLabel, className: cx2( { [styles]: !unstyled, [dotsStyles]: !unstyled }, className ), children }, index ); }; CarouselPageIndicator.displayName = "Carousel.PageIndicator"; // src/carousel/CarouselPagePicker.tsx import { cx as cx3 } from "class-variance-authority"; import { Fragment, jsx as jsx5 } from "react/jsx-runtime"; var CarouselPagePicker = ({ children, className }) => { const ctx = useCarouselContext(); return /* @__PURE__ */ jsx5(Fragment, { children: /* @__PURE__ */ jsx5( "div", { ...ctx.getIndicatorGroupProps(), className: cx3( "default:min-h-sz-16 flex w-full flex-wrap items-center justify-center", className ), children: ctx.pageSnapPoints.length <= 1 ? null : children({ ...ctx, pages: Array.from({ length: ctx.pageSnapPoints.length }, (_, i) => i) }) } ) }); }; CarouselPagePicker.displayName = "Carousel.PagePicker"; // src/carousel/CarouselPrevButton.tsx import { ArrowVerticalLeft } from "@spark-ui/icons/ArrowVerticalLeft"; import { jsx as jsx6 } from "react/jsx-runtime"; var CarouselPrevButton = ({ "aria-label": ariaLabel, ...buttonProps }) => { const ctx = useCarouselContext(); return /* @__PURE__ */ jsx6( IconButton, { ...ctx.getPrevTriggerProps(), intent: "surface", design: "filled", className: "pointer-events-auto cursor-pointer shadow-sm disabled:invisible", "aria-label": ariaLabel, ...buttonProps, children: /* @__PURE__ */ jsx6(Icon, { children: /* @__PURE__ */ jsx6(ArrowVerticalLeft, {}) }) } ); }; CarouselPrevButton.displayName = "Carousel.PrevButton"; // src/carousel/CarouselSlide.tsx import { cx as cx4 } from "class-variance-authority"; import { useRef as useRef6 } from "react"; // src/carousel/useIsVisible.ts import { useLayoutEffect as useLayoutEffect4, useState as useState3 } from "react"; function useIsVisible(elementRef, parentRef) { const [isVisible, setIsVisible] = useState3(true); useLayoutEffect4(() => { const el = elementRef.current; const parent = parentRef.current; if (!parent || !el) return; const observer = new IntersectionObserver( ([entry]) => { if (entry) { setIsVisible(entry.isIntersecting); } }, { root: parent, threshold: 0.2 } ); observer.observe(el); return () => observer.disconnect(); }); return isVisible; } // src/carousel/CarouselSlide.tsx import { jsx as jsx7 } from "react/jsx-runtime"; var CarouselSlide = ({ children, index = 0, totalSlides, className = "", ...props }) => { const itemRef = useRef6(null); const ctx = useCarouselContext(); const isVisible = useIsVisible(itemRef, ctx.ref); return /* @__PURE__ */ jsx7( "div", { ref: itemRef, ...ctx.getSlideProps({ index, totalSlides }), className: cx4("default:bg-surface relative overflow-hidden", className), "aria-hidden": !isVisible, inert: !isVisible, ...props, children } ); }; CarouselSlide.displayName = "Carousel.Slide"; // src/carousel/CarouselSlides.tsx import { cx as cx5 } from "class-variance-authority"; import { Children, cloneElement, isValidElement } from "react"; import { jsx as jsx8 } from "react/jsx-runtime"; var CarouselSlides = ({ children, className = "" }) => { const ctx = useCarouselContext(); const childrenElements = Children.toArray(children); return /* @__PURE__ */ jsx8( "div", { ...ctx.getSlidesContainerProps(), className: cx5( "focus-visible:u-outline relative w-full", "[-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", className ), children: childrenElements.map( (child, index) => isValidElement(child) ? cloneElement(child, { index, totalSlides: childrenElements.length }) : child ) } ); }; CarouselSlides.displayName = "Carousel.Slides"; // src/carousel/CarouselViewport.tsx import { jsx as jsx9 } from "react/jsx-runtime"; var CarouselViewport = ({ children }) => { return /* @__PURE__ */ jsx9("div", { className: "relative flex items-center justify-around p-0", children }); }; CarouselViewport.displayName = "Carousel.Viewport"; // src/carousel/index.ts var Carousel2 = Object.assign(Carousel, { Controls: CarouselControls, NextButton: CarouselNextButton, PrevButton: CarouselPrevButton, Slide: CarouselSlide, Slides: CarouselSlides, Viewport: CarouselViewport, PagePicker: CarouselPagePicker, PageIndicator: CarouselPageIndicator }); Carousel2.displayName = "Carousel"; export { Carousel2 as Carousel }; //# sourceMappingURL=index.mjs.map