UNPKG

@spark-ui/components

Version:

Spark (Leboncoin design system) components.

733 lines (715 loc) 21.9 kB
import { IconButton } from "../chunk-QLOIAU3C.mjs"; import { Button } from "../chunk-USSL4UZ5.mjs"; import "../chunk-MUNDKRAE.mjs"; import { Icon } from "../chunk-AESXFMCC.mjs"; import "../chunk-NBZKMCHF.mjs"; import "../chunk-4F5DOL57.mjs"; // src/snackbar/Snackbar.tsx import { ToastQueue, useToastQueue } from "@react-stately/toast"; import { useEffect as useEffect2, useRef as useRef4 } from "react"; import { createPortal } from "react-dom"; // src/snackbar/SnackbarRegion.tsx import { useToastRegion } from "@react-aria/toast"; import { cloneElement as cloneElement2, useRef as useRef3 } from "react"; // src/snackbar/SnackbarItem.tsx import { useToast } from "@react-aria/toast"; import { Children, cloneElement, isValidElement, useCallback, useRef as useRef2 } from "react"; // src/snackbar/SnackbarItem.styles.ts import { cva } from "class-variance-authority"; // src/snackbar/snackbarVariants.ts var filledVariants = [ { design: "filled", intent: "success", class: ["bg-success text-on-success"] }, { design: "filled", intent: "alert", class: ["bg-alert text-on-alert"] }, { design: "filled", intent: "error", class: ["bg-error text-on-error"] }, { design: "filled", intent: "info", class: ["bg-info text-on-info"] }, { design: "filled", intent: "neutral", class: ["bg-neutral text-on-neutral"] }, { design: "filled", intent: "main", class: ["bg-main text-on-main"] }, { design: "filled", intent: "basic", class: ["bg-basic text-on-basic"] }, { design: "filled", intent: "support", class: ["bg-support text-on-support"] }, { design: "filled", intent: "accent", class: ["bg-accent text-on-accent"] }, { design: "filled", intent: "inverse", class: ["bg-surface-inverse text-on-surface-inverse"] } ]; var tintedVariants = [ { design: "tinted", intent: "success", class: ["bg-success-container text-on-success-container"] }, { design: "tinted", intent: "alert", class: ["bg-alert-container text-on-alert-container"] }, { design: "tinted", intent: "error", class: ["bg-error-container text-on-error-container"] }, { design: "tinted", intent: "info", class: ["bg-info-container text-on-info-container"] }, { design: "tinted", intent: "neutral", class: ["bg-neutral-container text-on-neutral-container"] }, { design: "tinted", intent: "main", class: ["bg-main-container text-on-main-container"] }, { design: "tinted", intent: "basic", class: ["bg-basic-container text-on-basic-container"] }, { design: "tinted", intent: "support", class: ["bg-support-container text-on-support-container"] }, { design: "tinted", intent: "accent", class: ["bg-accent-container text-on-accent-container"] }, { design: "tinted", intent: "inverse", class: ["bg-surface-inverse text-on-surface-inverse"] } ]; // src/snackbar/SnackbarItem.styles.ts var snackbarItemVariant = cva( [ "rounded-md shadow-sm", "max-w-[600px]", "cursor-default pointer-events-auto touch-none select-none", "absolute", /** * Focus */ "group-focus-visible:outline-hidden group-focus-visible:u-outline group-not-focus-visible:ring-inset", /** * Positionning */ "group-data-[position=bottom]:bottom-0 group-data-[position=bottom-left]:bottom-0 group-data-[position=bottom-right]:bottom-0", "group-data-[position=top]:top-0 group-data-[position=top-left]:top-0 group-data-[position=top-right]:top-0", /** * Animation and opacity */ "[animation-fill-mode: forwards]!", "data-[animation=queued]:animate-fade-in", "data-[animation=entering]:easing-decelerate-back", "data-[animation=exiting]:easing-standard", // Parent position bottom|bottom-left|bottom-right "data-[animation=entering]:group-data-[position=bottom]:[&:not([data-swipe])]:animate-slide-in-bottom", "data-[animation=exiting]:opacity-0 data-[animation=exiting]:transition-opacity", "data-[animation=exiting]:group-data-[position=bottom]:[&:not([data-swipe])]:animate-slide-out-bottom", "data-[animation=entering]:group-data-[position=bottom-left]:[&:not([data-swipe])]:animate-slide-in-bottom", "data-[animation=exiting]:group-data-[position=bottom-left]:[&:not([data-swipe])]:animate-slide-out-bottom", "data-[animation=entering]:group-data-[position=bottom-right]:[&:not([data-swipe])]:animate-slide-in-bottom", "data-[animation=exiting]:group-data-[position=bottom-right]:[&:not([data-swipe])]:animate-slide-out-bottom", // Parent position top|top-left|top-right "data-[animation=entering]:group-data-[position=top]:[&:not([data-swipe])]:animate-slide-in-top", "data-[animation=exiting]:group-data-[position=top]:[&:not([data-swipe])]:animate-slide-out-top", "data-[animation=entering]:group-data-[position=top-left]:[&:not([data-swipe])]:animate-slide-in-top", "data-[animation=exiting]:group-data-[position=top-left]:[&:not([data-swipe])]:animate-slide-out-top", "data-[animation=entering]:group-data-[position=top-right]:[&:not([data-swipe])]:animate-slide-in-top", "data-[animation=exiting]:group-data-[position=top-right]:[&:not([data-swipe])]:animate-slide-out-top", /** * Swipe */ "data-[swipe=move]:data-[swipe-direction=right]:translate-x-(--swipe-position-x)", "data-[swipe=move]:data-[swipe-direction=left]:translate-x-(--swipe-position-x)", "data-[swipe=cancel]:translate-x-0", "data-[swipe=end]:data-[swipe-direction=right]:animate-standalone-swipe-out-right", "data-[swipe=end]:data-[swipe-direction=left]:animate-standalone-swipe-out-left" ], { variants: { /** * Set different look and feel * @default 'filled' */ design: { filled: "", tinted: "" }, /** * Set color intent * @default 'neutral' */ intent: { success: "", alert: "", error: "", info: "", neutral: "", main: "", basic: "", support: "", accent: "", inverse: "" } }, compoundVariants: [...filledVariants, ...tintedVariants], defaultVariants: { design: "filled", intent: "neutral" } } ); var snackbarItemVariantContent = cva( [ "inline-grid items-center", "col-start-1 row-start-1", "px-md" // applying padding on the parent prevents VoiceOver on Safari from reading snackbar content 🤷 ], { variants: { /** * Force action button displaying on a new line * @default false */ actionOnNewline: { true: [ "grid-rows-[52px_1fr_52px]", "grid-cols-[min-content_1fr_min-content]", "[grid-template-areas:'icon_message_close'_'._message_.'_'action_action_action']" ], false: [ "grid-cols-[min-content_1fr_min-content_min-content]", "[grid-template-areas:'icon_message_action_close']" ] } }, defaultVariants: { actionOnNewline: false } } ); // src/snackbar/SnackbarItemAction.tsx import { cx } from "class-variance-authority"; // src/snackbar/SnackbarItemContext.tsx import { createContext, useContext } from "react"; var SnackbarItemContext = createContext({}); var useSnackbarItemContext = () => useContext(SnackbarItemContext); // src/snackbar/SnackbarItemAction.tsx import { jsx } from "react/jsx-runtime"; var SnackbarItemAction = ({ design: designProp = "filled", intent: intentProp = "neutral", onClick, children, className, ref, ...rest }) => { const { toast, state } = useSnackbarItemContext(); const intent = intentProp ?? toast.content.intent; const design = designProp ?? toast.content.design; return /* @__PURE__ */ jsx( Button, { ref, size: "md", shape: "rounded", ...intent === "inverse" ? { design: "ghost", intent: "surface" } : { design, intent: intent === "error" ? "danger" : intent }, onClick: (e) => { onClick?.(e); state.close(toast.key); }, style: { gridArea: "action", ...rest.style }, className: cx("ml-md justify-self-end", className), ...rest, children } ); }; SnackbarItemAction.displayName = "Snackbar.ItemAction"; // src/snackbar/SnackbarItemClose.tsx import { Close } from "@spark-ui/icons/Close"; import { cx as cx2 } from "class-variance-authority"; import { jsx as jsx2 } from "react/jsx-runtime"; var SnackbarItemClose = ({ design: designProp = "filled", intent: intentProp = "neutral", "aria-label": ariaLabel, onClick, className, ref, ...rest }) => { const { toast, state } = useSnackbarItemContext(); const intent = intentProp ?? toast.content.intent; const design = designProp ?? toast.content.design; return /* @__PURE__ */ jsx2( IconButton, { ref, size: "md", shape: "rounded", ...intent === "inverse" ? { design: "ghost", intent: "surface" } : { design, intent: intent === "error" ? "danger" : intent }, "aria-label": ariaLabel, onClick: (e) => { onClick?.(e); state.close(toast.key); }, style: { gridArea: "close", ...rest.style }, className: cx2("ml-md justify-self-end", className), ...rest, children: /* @__PURE__ */ jsx2(Icon, { size: "sm", children: /* @__PURE__ */ jsx2(Close, {}) }) } ); }; SnackbarItemClose.displayName = "Snackbar.ItemClose"; // src/snackbar/SnackbarItemIcon.tsx import { cx as cx3 } from "class-variance-authority"; import { jsx as jsx3 } from "react/jsx-runtime"; var SnackbarItemIcon = ({ children, className, ...rest }) => /* @__PURE__ */ jsx3( Icon, { size: "md", className: cx3("mx-md", className), style: { gridArea: "icon", ...rest.style }, ...rest, children } ); SnackbarItemIcon.displayName = "Snackbar.ItemIcon"; // src/snackbar/useSwipe.ts import { useEffect, useRef, useState } from "react"; var SWIPE_THRESHOLD = 75; var useSwipe = ({ swipeRef, onSwipeStart, onSwipeMove, onSwipeCancel, onSwipeEnd, threshold = 10 }) => { const [state, setState] = useState(); const direction = useRef(null); const origin = useRef(null); const delta = useRef(null); const handleSwipeStart = (evt) => { origin.current = { x: evt.clientX, y: evt.clientY }; document.addEventListener("selectstart", (e) => e.preventDefault()); }; const handleSwipeMove = (evt) => { if (!origin.current) return; const deltaX = Math.abs(evt.clientX - origin.current.x); const deltaY = Math.abs(evt.clientY - origin.current.y); let moveState; if (deltaX > deltaY && deltaX > threshold) { direction.current = evt.clientX > origin.current.x ? "right" : "left"; } else if (deltaY > threshold) { direction.current = evt.clientY > origin.current.y ? "down" : "up"; } if (!direction.current) return; if (!delta.current) { moveState = "start"; delta.current = { x: deltaX, y: deltaY }; onSwipeStart?.({ state: moveState, direction: direction.current }); } else { moveState = "move"; delta.current = { x: deltaX, y: deltaY }; swipeRef.current.style.setProperty( "--swipe-position-x", `${deltaX > deltaY ? evt.clientX - origin.current.x : 0}px` ); swipeRef.current.style.setProperty( "--swipe-position-y", `${!(deltaX > deltaY) ? evt.clientY - origin.current.y : 0}px` ); onSwipeMove?.({ state: moveState, direction: direction.current }); } setState(moveState); }; const handleSwipeEnd = () => { const proxyDelta = delta.current; origin.current = null; delta.current = null; if (proxyDelta) { const { x: deltaX, y: deltaY } = proxyDelta; let endState; if (deltaX > deltaY) { if (deltaX > SWIPE_THRESHOLD) { endState = "end"; onSwipeEnd?.({ state: endState, direction: direction.current }); } else { endState = "cancel"; onSwipeCancel?.({ state: endState, direction: direction.current }); } } else { if (deltaY > SWIPE_THRESHOLD) { endState = "end"; onSwipeEnd?.({ state: endState, direction: direction.current }); } else { endState = "cancel"; onSwipeCancel?.({ state: endState, direction: direction.current }); } } setState(endState); document.removeEventListener("selectstart", (e) => e.preventDefault()); } }; useEffect(() => { if (!swipeRef.current) return; const swipeElement = swipeRef.current; swipeElement.addEventListener("pointerdown", handleSwipeStart); document.addEventListener("pointermove", handleSwipeMove); document.addEventListener("pointerup", handleSwipeEnd); return () => { swipeElement.removeEventListener("pointerdown", handleSwipeStart); document.removeEventListener("pointermove", handleSwipeMove); document.removeEventListener("pointerup", handleSwipeEnd); }; }, []); return { state, direction: direction.current }; }; // src/snackbar/SnackbarItem.tsx import { jsx as jsx4, jsxs } from "react/jsx-runtime"; var SnackbarItem = ({ "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, "aria-details": ariaDetails, design: designProp, intent: intentProp, actionOnNewline: actionOnNewlineProp, className, children, ref: forwardedRef, ...rest }) => { const innerRef = useRef2(null); const ref = typeof forwardedRef !== "function" ? forwardedRef || innerRef : innerRef; const { toast, state } = useSnackbarItemContext(); const { state: swipeState, direction: swipeDirection } = useSwipe({ swipeRef: ref, onSwipeStart: state.pauseAll, onSwipeCancel: state.resumeAll, onSwipeEnd: ({ direction }) => { ; ["left", "right"].includes(`${direction}`) && state.close(toast.key); } }); const { message, icon, isClosable, onAction, actionLabel } = toast.content; const intent = intentProp ?? toast.content.intent; const design = designProp ?? toast.content.design; const actionOnNewline = actionOnNewlineProp ?? toast.content.actionOnNewline; const ariaProps = { ariaLabel, ariaLabelledby, ariaDescribedby, ariaDetails }; const { toastProps, titleProps, closeButtonProps, contentProps } = useToast( { toast, ...ariaProps }, state, ref ); const findElement = useCallback( (elementDisplayName) => { const childrenArray = Children.toArray(children); const match = childrenArray.filter(isValidElement).find( (child) => !!child.type.displayName?.includes( elementDisplayName ) ); return match; }, [children] ); const iconFromChildren = findElement("Snackbar.ItemIcon"); const actionBtnFromChildren = findElement("Snackbar.ItemAction"); const closeBtnFromChildren = findElement("Snackbar.ItemClose"); return /* @__PURE__ */ jsx4( "div", { className: snackbarItemVariant({ design, intent, className }), "data-animation": toast.animation, ...!(swipeState === "cancel" && toast.animation === "exiting") && { "data-swipe": swipeState, "data-swipe-direction": swipeDirection }, ...toast.animation === "exiting" && { // Remove snackbar when the exiting animation completes onAnimationEnd: () => state.remove(toast.key) }, ref, ...toastProps, ...rest, children: /* @__PURE__ */ jsxs("div", { className: snackbarItemVariantContent({ actionOnNewline }), ...contentProps, children: [ renderSubComponent(iconFromChildren, icon ? SnackbarItemIcon : null, { children: icon }), /* @__PURE__ */ jsx4( "p", { className: "px-md py-lg text-body-2 row-span-3", style: { gridArea: "message" }, ...titleProps, children: message } ), renderSubComponent( actionBtnFromChildren, actionLabel && onAction ? SnackbarItemAction : null, { intent, design, onClick: onAction, children: actionLabel } ), renderSubComponent(closeBtnFromChildren, isClosable ? SnackbarItemClose : null, { intent, design, /** * React Spectrum typing of aria-label is inaccurate, and aria-label value should never be undefined. * See https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/i18n/src/useLocalizedStringFormatter.ts#L40 */ "aria-label": closeButtonProps["aria-label"] }) ] }) } ); }; SnackbarItem.displayName = "Snackbar.Item"; var renderSubComponent = (childItem, defaultItem, props) => { if (childItem) { return cloneElement(childItem, { ...props, ...childItem.props }); } else if (defaultItem) { const Item = defaultItem; return /* @__PURE__ */ jsx4(Item, { ...props }); } else { return null; } }; // src/snackbar/SnackbarRegion.styles.ts import { cva as cva2 } from "class-variance-authority"; var snackbarRegionVariant = cva2( [ "fixed inset-x-lg z-toast group", "outline-hidden pointer-events-none", "grid grid-rows-1 grid-cols-1 gap-lg" ], { variants: { /** * Set snackbar item position * @default 'bottom' */ position: { top: "top-lg justify-items-center", "top-right": "top-lg justify-items-end", "top-left": "top-lg justify-items-start", bottom: "bottom-lg justify-items-center", "bottom-right": "bottom-lg justify-items-end", "bottom-left": "bottom-lg justify-items-start" } }, defaultVariants: { position: "bottom" } } ); // src/snackbar/SnackbarRegion.tsx import { jsx as jsx5 } from "react/jsx-runtime"; var SnackbarRegion = ({ children = /* @__PURE__ */ jsx5(SnackbarItem, {}), state, position = "bottom", className, ref: forwardedRef, ...rest }) => { const innerRef = useRef3(null); const ref = forwardedRef && typeof forwardedRef !== "function" ? forwardedRef : innerRef; const { regionProps } = useToastRegion(rest, state, ref); return /* @__PURE__ */ jsx5( "div", { ...regionProps, ref, "data-position": position, className: snackbarRegionVariant({ position, className }), children: state.visibleToasts.map((toast) => /* @__PURE__ */ jsx5(SnackbarItemContext.Provider, { value: { toast, state }, children: cloneElement2(children, { key: toast.key }) }, toast.key)) } ); }; // src/snackbar/useSnackbarGlobalStore.ts import { useCallback as useCallback2, useSyncExternalStore } from "react"; var useSnackbarGlobalStore = ({ providers, subscriptions }) => { const subscribe = useCallback2( (listener) => { subscriptions.add(listener); return () => subscriptions.delete(listener); }, [subscriptions] ); const getLastSnackbarProvider = useCallback2(() => [...providers].reverse()[0], [providers]); const addProvider = useCallback2( (provider2) => { providers.add(provider2); for (const subscribeFn of subscriptions) { subscribeFn(); } }, [providers, subscriptions] ); const deleteProvider = useCallback2( (provider2) => { providers.delete(provider2); for (const subscribeFn of subscriptions) { subscribeFn(); } }, [providers, subscriptions] ); const provider = useSyncExternalStore(subscribe, getLastSnackbarProvider, getLastSnackbarProvider); return { provider, addProvider, deleteProvider }; }; // src/snackbar/Snackbar.tsx import { jsx as jsx6 } from "react/jsx-runtime"; var GLOBAL_SNACKBAR_QUEUE = null; var getGlobalSnackBarQueue = () => { if (!GLOBAL_SNACKBAR_QUEUE) { GLOBAL_SNACKBAR_QUEUE = new ToastQueue({ maxVisibleToasts: 1, hasExitAnimation: true }); } return GLOBAL_SNACKBAR_QUEUE; }; var clearSnackbarQueue = () => { GLOBAL_SNACKBAR_QUEUE = null; }; var GLOBAL_SNACKBAR_STORE = { providers: /* @__PURE__ */ new Set(), subscriptions: /* @__PURE__ */ new Set() }; var Snackbar = ({ ref: forwardedRef, ...props }) => { const ref = useRef4(null); const state = useToastQueue(getGlobalSnackBarQueue()); const { provider, addProvider, deleteProvider } = useSnackbarGlobalStore(GLOBAL_SNACKBAR_STORE); useEffect2(() => { addProvider(ref); return () => { for (const toast of getGlobalSnackBarQueue().visibleToasts) { toast.animation = void 0; } deleteProvider(ref); }; }, []); return ref === provider && state.visibleToasts.length > 0 ? createPortal(/* @__PURE__ */ jsx6(SnackbarRegion, { ref: forwardedRef, state, ...props }), document.body) : null; }; Snackbar.displayName = "Snackbar"; var addSnackbar = ({ onClose, timeout = 5e3, priority, ...content }) => { const queue = getGlobalSnackBarQueue(); queue.add(content, { onClose, timeout: timeout && !content.onAction ? Math.max(timeout, 5e3) : void 0, priority }); }; // src/snackbar/index.ts var Snackbar2 = Object.assign(Snackbar, { Item: SnackbarItem, ItemAction: SnackbarItemAction, ItemClose: SnackbarItemClose, ItemIcon: SnackbarItemIcon }); Snackbar2.displayName = "Snackbar"; SnackbarItem.displayName = "Snackbar.Item"; SnackbarItemAction.displayName = "Snackbar.ItemAction"; SnackbarItemClose.displayName = "Snackbar.ItemClose"; SnackbarItemIcon.displayName = "Snackbar.ItemIcon"; export { Snackbar2 as Snackbar, addSnackbar, clearSnackbarQueue }; //# sourceMappingURL=index.mjs.map