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