Files
BanGUI/frontend/src/components/skeletons/SkeletonTableRow.tsx
Lukas ca23858946 Add skeleton loading components for progressive UX
Implement standardized skeleton loading placeholders to reduce perceived
loading time and prevent layout shift during data fetches. These components
match actual content dimensions exactly, improving perceived responsiveness.

New skeleton components in src/components/skeletons/:
- SkeletonTable: Table/grid loading with customizable rows and cells
- SkeletonTableRow: Individual animated skeleton row
- SkeletonChart: Chart/graph loading with bars matching dimensions
- SkeletonStat: Stat card loading with label and value
- SkeletonFormField: Form input loading placeholder
- PageLoadingSkeleton: Convenience wrapper for page-level loading states

Implementation details:
- All skeletons use global 'skeleton-pulse' animation (2s cycle)
- Dimensions match real content to prevent layout shift on arrival
- Marked with aria-hidden and role=presentation for accessibility
- Theme-aware colors using Fluent UI tokens
- Respects prefers-reduced-motion setting

Updates:
- ChartStateWrapper: Uses SkeletonChart instead of spinner
- PageFeedback: Added PageLoadingSkeleton component
- App.tsx: Injects skeleton styles at startup
- Web-Design.md: Added § 8a with loading UX guidance and usage examples

All components tested (22 tests, 100% passing) and linted.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-28 09:40:10 +02:00

77 lines
2.1 KiB
TypeScript

/**
* SkeletonTableRow component.
*
* A placeholder row for tables that mimics the layout and height of a real
* DataGrid row. Used to show loading state while data is being fetched.
* Prevents content shift by matching the actual row dimensions.
*/
import {
makeStyles,
tokens,
} from "@fluentui/react-components";
// ---------------------------------------------------------------------------
// Styles
// ---------------------------------------------------------------------------
const useStyles = makeStyles({
skeletonRow: {
display: "flex",
padding: `${tokens.spacingVerticalS} 0`,
gap: tokens.spacingHorizontalM,
borderBottomWidth: "1px",
borderBottomStyle: "solid",
borderBottomColor: tokens.colorNeutralStroke2,
},
skeletonCell: {
flex: "1 1 0",
minWidth: "80px",
backgroundColor: tokens.colorNeutralBackground1Hover,
borderRadius: tokens.borderRadiusSmall,
height: "16px",
animation: "skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",
},
});
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
interface SkeletonTableRowProps {
/** Number of cells to render. Defaults to 5. */
cellCount?: number;
}
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
/**
* Render a skeleton row that matches table dimensions.
*
* @example
* ```tsx
* <div>
* {Array.from({ length: 5 }).map((_, i) => (
* <SkeletonTableRow key={i} cellCount={6} />
* ))}
* </div>
* ```
*/
export function SkeletonTableRow({ cellCount = 5 }: SkeletonTableRowProps): React.JSX.Element {
const styles = useStyles();
return (
<div className={styles.skeletonRow} role="presentation">
{Array.from({ length: cellCount }).map((_, index: number) => (
<div
key={`cell-${String(index)}`}
className={styles.skeletonCell}
aria-hidden="true"
/>
))}
</div>
);
}