feat(ui): polish core component surfaces

This commit is contained in:
2026-03-25 19:49:15 +08:00
parent eccaacece7
commit cc1509d2f6
64 changed files with 2707 additions and 353 deletions
+82 -4
View File
@@ -1,4 +1,14 @@
import { forwardRef, type ComponentPropsWithoutRef } from "react";
import { useReducedMotion } from "motion/react";
import {
Children,
cloneElement,
forwardRef,
isValidElement,
useEffect,
useState,
type ComponentPropsWithoutRef,
type CSSProperties
} from "react";
import {
emptyStateActionsVariants,
@@ -13,6 +23,40 @@ 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>;
@@ -36,12 +80,20 @@ export type EmptyStateMediaProps = ComponentPropsWithoutRef<"div"> &
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 }), className)}
className={cn(emptyStateMediaVariants({ size }), ambientMotionClassName, className)}
ref={ref}
/>
);
@@ -115,7 +167,31 @@ export type EmptyStateActionsProps = ComponentPropsWithoutRef<"div"> &
VariantProps<typeof emptyStateActionsVariants>;
export const EmptyStateActions = forwardRef<HTMLDivElement, EmptyStateActionsProps>(
function EmptyStateActions({ className, layout, ...props }, ref) {
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}
@@ -123,7 +199,9 @@ export const EmptyStateActions = forwardRef<HTMLDivElement, EmptyStateActionsPro
{...createDataAttributes({ layout })}
className={cn(emptyStateActionsVariants({ layout }), className)}
ref={ref}
/>
>
{animatedChildren}
</div>
);
}
);