@spark-ui/components
Version:
Spark (Leboncoin design system) components.
716 lines (694 loc) • 20.8 kB
JavaScript
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