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>
This commit is contained in:
2026-04-28 09:40:10 +02:00
parent 2fea513c9c
commit ca23858946
17 changed files with 741 additions and 29 deletions

View File

@@ -235,12 +235,86 @@ Use Fluent UI React components as the building blocks. The following mapping sho
| Success messages | `MessageBar` (success) | "IP 1.2.3.4 has been banned in jail sshd." |
| Error messages | `MessageBar` (error) | "Failed to connect to fail2ban server." |
| Warning messages | `MessageBar` (warning) | "Blocklist import encountered 12 invalid entries." |
| Loading states | `Shimmer` | Apply to `DetailsList` rows and stat cards while data loads. |
| Loading states | `SkeletonTable` / `SkeletonChart` (preferred) or `Shimmer` (legacy) | Show skeleton placeholders matching layout. Use `PageLoadingSkeleton` for page-level loading. |
| Empty states | Custom illustration + text | "No bans recorded in the last 24 hours." Centre on the content area. |
| Tooltips | `Tooltip` / `TooltipHost` | Full IP info on hover, full regex on truncated text, icon-only button labels. |
---
## 8a. Loading States & Skeleton Components
Progressive loading states improve perceived responsiveness and reduce cognitive load during data fetches.
### When to Use Skeleton Placeholders
Use skeleton loading states instead of spinners for regions with a fixed, known layout:
- **Tables** — Show skeleton rows that match the actual column layout and row height
- **Charts** — Show skeleton bars matching the chart dimensions
- **Stat cards** — Show skeleton badges matching actual stat dimensions
- **Forms** — Show skeleton fields matching input dimensions
Use full-page spinners only when:
- Loading time is expected to be under 1 second
- Content layout is unknown or highly variable
- Entire page structure is being loaded
### Skeleton Components
BanGUI provides pre-built skeleton components in `src/components/skeletons/`:
| Component | Usage | Notes |
|---|---|---|
| `<SkeletonTable>` | Table/grid loading | Pass `rowCount` and `cellCount` to match real layout. |
| `<SkeletonTableRow>` | Individual table rows | Renders a single animated skeleton row. |
| `<SkeletonChart>` | Chart/graph loading | Pass `barCount` and `height` to match container dimensions. |
| `<SkeletonStat>` | Stat badge loading | Shows label + value stacked. Pass `showLabel={false}` to hide label. |
| `<SkeletonFormField>` | Form input loading | Shows label + input placeholder. Pass `inputHeight` for custom sizing. |
| `<PageLoadingSkeleton>` | Page-level loading | Convenience wrapper. Pass `type="table"` or `type="chart"`. |
### Skeleton Implementation Details
- **Dimensions:** Skeletons must exactly match real content dimensions (height, width, spacing) to prevent layout shift when content arrives.
- **Animation:** All skeletons use a subtle 2-second pulse animation (`skeleton-pulse` keyframe). The animation respects `prefers-reduced-motion`.
- **Accessibility:** Skeleton elements are marked `aria-hidden="true"` and `role="presentation"` — they are not part of the accessible tree.
- **Theming:** Skeleton colour uses `colorNeutralBackground1Hover` token for automatic light/dark mode support.
### Usage Examples
```tsx
// Loading table data
if (loading) {
return <SkeletonTable rowCount={10} cellCount={6} />;
}
// Loading chart
if (chartLoading) {
return <PageLoadingSkeleton type="chart" itemCount={12} chartHeight={220} />;
}
// Loading multiple stats
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)" }}>
{[1, 2, 3].map(i => <SkeletonStat key={i} />)}
</div>
// Loading form
<div style={{ gap: "16px", display: "flex", flexDirection: "column" }}>
<SkeletonFormField />
<SkeletonFormField />
<SkeletonFormField showLabel={false} />
</div>
```
### Avoiding Layout Shift
**Critical:** Skeleton components must reserve the exact space that real content will occupy. Test by:
1. Load skeleton while data is fetching
2. Observe the skeleton render to full height/width
3. Observe real content arriving — no movement or repaint of surrounding elements
If content shifts when it arrives, adjust skeleton dimensions or parent container constraints.
---
## 9. Tables & Data Grids
Tables are the primary UI element in BanGUI. They must be treated with extreme care.
@@ -387,7 +461,7 @@ export const sideNavCollapsedWidth = 48;
| Reference theme tokens for all colours | Hard-code hex values like `#ff0000` in components |
| Follow the 4 px spacing grid | Use arbitrary pixel values (13 px, 7 px, 19 px) |
| Provide a tooltip for every icon-only button | Leave icons unlabelled and inaccessible |
| Use `Shimmer` for loading states | Show a blank screen or a standalone spinner with no context |
| Use skeleton placeholders for loading states | Show a blank screen or a standalone spinner with no context |
| Design for both light and dark themes | Default to white backgrounds assuming light mode only |
| Use `DetailsList` for all tabular data | Use raw HTML `<table>` elements or a third-party data grid |
| Use semantic colour slots (`errorText`, `bodyBackground`) | Use descriptive palette slots (`red`, `neutralLight`) directly |