구성
Skeleton, SkeletonText, SkeletonRow를 상황별로 조합합니다.
목적
스피너보다 레이아웃 안정성이 중요한 카드, 텍스트, 표 로딩에 사용합니다.
접근성
대체되는 영역에 role=status, aria-busy, aria-label을 함께 설계합니다.
모션
pulse 애니메이션은 reduced motion에서 제거됩니다.

블록

카드나 그래프처럼 면적이 있는 콘텐츠 자리 표시
코드 보기tsx
import { Skeleton } from "@olundot/ui";

export function SkeletonBlock() {
  return (
    <Skeleton
      aria-label="요약 카드 로드 중"
      style={{ width: "100%", height: 128 }}
    />
  );
}

텍스트 줄

문단 길이를 예측 가능한 줄 단위로 표시
코드 보기tsx
import { SkeletonText } from "@olundot/ui";

export function SkeletonTextLines() {
  return <SkeletonText lines={4} widths={["92%", "78%", "86%", "54%"]} />;
}

행과 표

표와 목록의 열 비율을 유지하며 로딩 상태 표시
코드 보기tsx
import { SkeletonRow } from "@olundot/ui";

export function SkeletonRowTable() {
  return (
    <SkeletonRow surface="canvas"
      aria-label="응시자 목록 로드 중"
      count={5}
      columns={[
        { width: "2fr" },
        { width: "1fr", align: "center" },
        { width: "1fr", align: "right" },
      ]}
    />
  );
}

접근 가능한 로딩 대체

실제 콘텐츠 영역을 aria-busy 상태와 함께 대체
코드 보기tsx
import { useState } from "react";
import { Button, SkeletonText } from "@olundot/ui";

export function SkeletonAccessibleReplacement() {
  const [loading, setLoading] = useState(true);
  return (
    <div style={{ display: "grid", gap: "12px" }}>
      <Button size="sm" variant="outline" onClick={() => setLoading((next) => !next)}>
        {loading ? "콘텐츠 표시" : "다시 로딩"}
      </Button>
      {loading ? (
        <section role="status" aria-busy="true" aria-label="학생 요약 로드 중">
          <SkeletonText lines={3} widths={["88%", "72%", "42%"]} />
        </section>
      ) : (
        <section aria-label="학생 요약">
          <strong>김서연</strong>
          <p style={{ margin: "4px 0 0", color: "var(--text-secondary)" }}>
            최근 7일 평균 정답률 82%, 복습 필요 문항 4개
          </p>
        </section>
      )}
    </div>
  );
}