feat(design): skeleton first loads by shape on the shared reveal clock (E28 S3d2, B8)
This commit is contained in:
@@ -2,6 +2,7 @@ import { useEffect, useState, type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AlertTriangle, Loader2 } from "lucide-react";
|
||||
import { Button } from "./Button";
|
||||
import { SkeletonShapes, type SkeletonShape } from "./Skeleton";
|
||||
|
||||
// A loading state fades in only after a delay: most loads here answer in tens of milliseconds, and a
|
||||
// spinner shown for two frames before the content replaces it reads as a flicker, not as feedback. A
|
||||
@@ -22,15 +23,19 @@ function revealWait(since: number | undefined, delayMs: number): number {
|
||||
}
|
||||
|
||||
// Centered loading state: a spinning accent ring + an optional label. The single shared loading
|
||||
// visual for a page/section's first load, so all of them read the same (and can be swapped for
|
||||
// skeletons later in one place). `role=status`/`aria-live` announces it to screen readers.
|
||||
// visual for a page/section's first load, so all of them read the same. A grid, a list or a table
|
||||
// passes its `shape` instead (B8): the skeleton of what is coming, on the same reveal clock, with the
|
||||
// label kept for a screen reader only. `role=status`/`aria-live` announces it either way.
|
||||
export function LoadingState({
|
||||
label,
|
||||
className = "",
|
||||
since,
|
||||
delayMs = LOADING_REVEAL_MS,
|
||||
shape,
|
||||
}: {
|
||||
label?: ReactNode;
|
||||
/** The content's shape: its skeleton replaces the spinner (B8). */
|
||||
shape?: SkeletonShape;
|
||||
className?: string;
|
||||
/** When the wait began (a `performance.now()` time), if earlier than this mount — the boot screen
|
||||
* passes 0, navigation start. Only used when this state starts a new clock. */
|
||||
@@ -46,10 +51,18 @@ export function LoadingState({
|
||||
reveal.lastGone = performance.now();
|
||||
};
|
||||
}, []);
|
||||
const style = wait > 0 ? { animation: `fadeIn 0.2s ease ${Math.round(wait)}ms both` } : undefined;
|
||||
if (shape)
|
||||
return (
|
||||
<div className={`w-full ${className}`} style={style} role="status" aria-live="polite">
|
||||
<span className="sr-only">{label ?? t("common.loading")}</span>
|
||||
<SkeletonShapes shape={shape} />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col items-center gap-3 py-10 text-sm text-muted ${className}`}
|
||||
style={wait > 0 ? { animation: `fadeIn 0.2s ease ${Math.round(wait)}ms both` } : undefined}
|
||||
style={style}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user