208 lines
5.6 KiB
TypeScript
208 lines
5.6 KiB
TypeScript
import { useReducedMotion } from "motion/react";
|
|
import {
|
|
Children,
|
|
cloneElement,
|
|
forwardRef,
|
|
isValidElement,
|
|
useEffect,
|
|
useState,
|
|
type ComponentPropsWithoutRef,
|
|
type CSSProperties
|
|
} from "react";
|
|
|
|
import {
|
|
emptyStateActionsVariants,
|
|
emptyStateDescriptionVariants,
|
|
emptyStateEyebrowVariants,
|
|
emptyStateHeaderVariants,
|
|
emptyStateMediaVariants,
|
|
emptyStateTitleVariants,
|
|
emptyStateVariants
|
|
} from "./empty-state.variants";
|
|
import { cn } from "../lib/cn";
|
|
import type { VariantProps } from "../lib/cva";
|
|
import { createDataAttributes, createSlot } from "../lib/contracts";
|
|
|
|
function getIsStaticMotion() {
|
|
if (typeof document === "undefined") {
|
|
return false;
|
|
}
|
|
|
|
return document.documentElement.dataset.motion === "static";
|
|
}
|
|
|
|
function useMotionDisabled() {
|
|
const prefersReducedMotion = useReducedMotion();
|
|
const [isStaticMotion, setIsStaticMotion] = useState(getIsStaticMotion);
|
|
|
|
useEffect(() => {
|
|
if (typeof document === "undefined") {
|
|
return;
|
|
}
|
|
|
|
const syncMotionMode = () => {
|
|
setIsStaticMotion(getIsStaticMotion());
|
|
};
|
|
|
|
syncMotionMode();
|
|
|
|
const observer = new MutationObserver(syncMotionMode);
|
|
observer.observe(document.documentElement, {
|
|
attributeFilter: ["data-motion"]
|
|
});
|
|
|
|
return () => observer.disconnect();
|
|
}, []);
|
|
|
|
return prefersReducedMotion || isStaticMotion;
|
|
}
|
|
|
|
export type EmptyStateProps = ComponentPropsWithoutRef<"div"> &
|
|
VariantProps<typeof emptyStateVariants>;
|
|
|
|
export const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(function EmptyState(
|
|
{ align, className, layout, tone, ...props },
|
|
ref
|
|
) {
|
|
return (
|
|
<div
|
|
{...props}
|
|
{...createSlot("root")}
|
|
{...createDataAttributes({ align, layout, tone })}
|
|
className={cn(emptyStateVariants({ align, layout, tone }), className)}
|
|
ref={ref}
|
|
/>
|
|
);
|
|
});
|
|
|
|
export type EmptyStateMediaProps = ComponentPropsWithoutRef<"div"> &
|
|
VariantProps<typeof emptyStateMediaVariants>;
|
|
|
|
export const EmptyStateMedia = forwardRef<HTMLDivElement, EmptyStateMediaProps>(
|
|
function EmptyStateMedia({ className, size, ...props }, ref) {
|
|
const disableMotion = useMotionDisabled();
|
|
const ambientMotionClassName =
|
|
disableMotion || size === "compact"
|
|
? undefined
|
|
: size === "hero"
|
|
? "motion-float-delayed will-change-transform"
|
|
: "motion-breathe will-change-transform";
|
|
|
|
return (
|
|
<div
|
|
{...props}
|
|
{...createSlot("media")}
|
|
{...createDataAttributes({ size })}
|
|
className={cn(emptyStateMediaVariants({ size }), ambientMotionClassName, className)}
|
|
ref={ref}
|
|
/>
|
|
);
|
|
}
|
|
);
|
|
|
|
export type EmptyStateHeaderProps = ComponentPropsWithoutRef<"div"> &
|
|
VariantProps<typeof emptyStateHeaderVariants>;
|
|
|
|
export const EmptyStateHeader = forwardRef<HTMLDivElement, EmptyStateHeaderProps>(
|
|
function EmptyStateHeader({ align, className, ...props }, ref) {
|
|
return (
|
|
<div
|
|
{...props}
|
|
{...createSlot("header")}
|
|
{...createDataAttributes({ align })}
|
|
className={cn(emptyStateHeaderVariants({ align }), className)}
|
|
ref={ref}
|
|
/>
|
|
);
|
|
}
|
|
);
|
|
|
|
export type EmptyStateEyebrowProps = ComponentPropsWithoutRef<"p">;
|
|
|
|
export const EmptyStateEyebrow = forwardRef<HTMLParagraphElement, EmptyStateEyebrowProps>(
|
|
function EmptyStateEyebrow({ className, ...props }, ref) {
|
|
return (
|
|
<p
|
|
{...props}
|
|
{...createSlot("eyebrow")}
|
|
className={cn(emptyStateEyebrowVariants(), className)}
|
|
ref={ref}
|
|
/>
|
|
);
|
|
}
|
|
);
|
|
|
|
export type EmptyStateTitleProps = ComponentPropsWithoutRef<"h3">;
|
|
|
|
export const EmptyStateTitle = forwardRef<HTMLHeadingElement, EmptyStateTitleProps>(
|
|
function EmptyStateTitle({ className, ...props }, ref) {
|
|
return (
|
|
<h3
|
|
{...props}
|
|
{...createSlot("label")}
|
|
className={cn(emptyStateTitleVariants(), className)}
|
|
ref={ref}
|
|
/>
|
|
);
|
|
}
|
|
);
|
|
|
|
export type EmptyStateDescriptionProps = ComponentPropsWithoutRef<"p">;
|
|
|
|
export const EmptyStateDescription = forwardRef<
|
|
HTMLParagraphElement,
|
|
EmptyStateDescriptionProps
|
|
>(function EmptyStateDescription({ className, ...props }, ref) {
|
|
return (
|
|
<p
|
|
{...props}
|
|
{...createSlot("description")}
|
|
className={cn(emptyStateDescriptionVariants(), className)}
|
|
ref={ref}
|
|
/>
|
|
);
|
|
});
|
|
|
|
export type EmptyStateActionsProps = ComponentPropsWithoutRef<"div"> &
|
|
VariantProps<typeof emptyStateActionsVariants>;
|
|
|
|
export const EmptyStateActions = forwardRef<HTMLDivElement, EmptyStateActionsProps>(
|
|
function EmptyStateActions({ children, className, layout, ...props }, ref) {
|
|
const disableMotion = useMotionDisabled();
|
|
const animatedChildren = disableMotion
|
|
? children
|
|
: Children.map(children, (child, index) => {
|
|
if (
|
|
!isValidElement<{ className?: string; style?: CSSProperties }>(child) ||
|
|
typeof child.type === "symbol"
|
|
) {
|
|
return child;
|
|
}
|
|
|
|
return cloneElement(child, {
|
|
className: cn(
|
|
"[animation:aiui-slide-up-sm_var(--dur-base)_var(--ease-emphasized)_both]",
|
|
"will-change-transform",
|
|
child.props.className
|
|
),
|
|
style: {
|
|
...(child.props.style ?? {}),
|
|
animationDelay: `${Math.min(index * 70, 140)}ms`
|
|
}
|
|
});
|
|
});
|
|
|
|
return (
|
|
<div
|
|
{...props}
|
|
{...createSlot("actions")}
|
|
{...createDataAttributes({ layout })}
|
|
className={cn(emptyStateActionsVariants({ layout }), className)}
|
|
ref={ref}
|
|
>
|
|
{animatedChildren}
|
|
</div>
|
|
);
|
|
}
|
|
);
|