1.AI 协作文档体系重构(objections/worklines/contracts+matrix.md) 2.coord 仲裁文档(final-decisions/cross-review/final-rulings/orchestration) 3.各服务 01/02 文档补全 4.共享包初始化(shared-ts/shared-go/hooks/ui-components/ui-tokens) 5.Proto 契约补全 6.004 架构影响地图更新 7.端口分配表 8.设计规格文档
66 lines
1.6 KiB
TypeScript
66 lines
1.6 KiB
TypeScript
import type { ReactNode } from "react";
|
||
import { cn } from "./utils/cn.js";
|
||
|
||
/**
|
||
* Loading - 骨架屏 / 加载占位
|
||
*
|
||
* 用途:数据加载期间展示骨架屏,避免内容跳动。
|
||
*
|
||
* @example
|
||
* <Loading /> // 默认 3 行骨架
|
||
* <Loading lines={5} /> // 5 行骨架
|
||
* <Loading variant="spinner" /> // 旋转图标
|
||
*/
|
||
|
||
export interface LoadingProps {
|
||
/** 骨架行数(variant="skeleton" 时生效) */
|
||
lines?: number;
|
||
/** 展示形态 */
|
||
variant?: "skeleton" | "spinner";
|
||
/** 自定义类名 */
|
||
className?: string;
|
||
/** aria-label(无障碍) */
|
||
ariaLabel?: string;
|
||
}
|
||
|
||
export function Loading({
|
||
lines = 3,
|
||
variant = "skeleton",
|
||
className,
|
||
ariaLabel = "加载中",
|
||
}: LoadingProps): ReactNode {
|
||
if (variant === "spinner") {
|
||
return (
|
||
<div
|
||
role="status"
|
||
aria-label={ariaLabel}
|
||
className={cn("flex items-center justify-center p-8", className)}
|
||
>
|
||
<span
|
||
className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-muted border-t-primary"
|
||
aria-hidden="true"
|
||
/>
|
||
<span className="sr-only">{ariaLabel}</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div
|
||
role="status"
|
||
aria-label={ariaLabel}
|
||
className={cn("flex flex-col gap-2 p-4", className)}
|
||
>
|
||
{Array.from({ length: lines }, (_, i) => (
|
||
<div
|
||
key={i}
|
||
className="h-4 animate-pulse rounded bg-muted"
|
||
style={{ width: `${100 - i * 10}%` }}
|
||
aria-hidden="true"
|
||
/>
|
||
))}
|
||
<span className="sr-only">{ariaLabel}</span>
|
||
</div>
|
||
);
|
||
}
|