Skip to content

Loading states (skeleton UI)

Keprix uses shape-accurate skeleton placeholders while API data loads. Slow responses are acceptable; layout collapse and blank screens are not.

Module location

frontend/src/components/ui/loading/
  SkeletonBlock.tsx
  SkeletonText.tsx
  SkeletonList.tsx
  SkeletonTable.tsx
  SkeletonStatGrid.tsx
  SkeletonChart.tsx
  SkeletonDetailPanel.tsx
  AsyncView.tsx
  index.ts

Import from the barrel:

import { AsyncView, SkeletonList, SkeletonTable } from "@/components/ui/loading";

Decision table

Situation Use Avoid
List of records SkeletonList Centered CircularProgress
Table of records SkeletonTable Loading items... text
Stat cards / dashboard grid SkeletonStatGrid One large block
Chart or wide panel SkeletonChart Text-only loading
Master-detail pane SkeletonDetailPanel Loading record...
Full page data region Skeleton under PageHeader Full-page spinner
Form submit Button disabled + Saving... Page skeleton
Chat token stream Stream indicators Per-token skeleton

Keep PageHeader, tabs, and filters visible during load. Skeleton only the data region.

Primitives

SkeletonBlock

Single rounded block. Base primitive; respects prefers-reduced-motion.

<SkeletonBlock height={120} />
<SkeletonBlock height={16} width="60%" />

SkeletonText

Stacked lines; last line shorter (default 3 lines).

<SkeletonText lines={4} />

SkeletonList

Vertical row placeholders (default 5 rows, 72px height).

<SkeletonList rows={6} rowHeight={72} />

SkeletonTable

Table inside TableContainer (default 6 rows, 4 columns).

<SkeletonTable rows={8} columns={5} />

SkeletonStatGrid

Responsive grid of stat-card blocks (~120px tall).

<SkeletonStatGrid count={4} />

SkeletonChart

Chart panel placeholder (default height 280).

<SkeletonChart height={280} />

SkeletonDetailPanel

Title, status pill, field rows, action buttons.

<SkeletonDetailPanel fields={6} />

AsyncView

Standard loading / error / content switch.

const { data, isLoading, error } = useSWR("tasks", fetchTasks);

return (
  <AsyncView
    loading={isLoading}
    error={error?.message}
    errorTitle="Could not load tasks"
    skeleton={<SkeletonList rows={6} />}
  >
    <TaskList tasks={data ?? []} />
  </AsyncView>
);

Reduced motion

usePrefersReducedMotion (frontend/src/hooks/usePrefersReducedMotion.ts) disables skeleton wave animation when the user prefers reduced motion.

Shared components (upgraded in Prompt 168)

Component Loading UI
DataTable SkeletonTable
RecordDetail SkeletonDetailPanel
Timeline SkeletonList (4 rows)
CitationList SkeletonList (4 rows)
ToolRunTrace SkeletonList (3 rows)

Reference page

/tasks uses SkeletonList rows={6} under tabs while fetchTasks runs.

Migration status

Batch Prompt Status
Primitives + shared UI 168 Shipped
Workspace / settings pages 169 Shipped
Admin normalization + CI contract 170 Shipped

Series prompts: 167 (architecture), 168 (primitives), 169 (workspace), 170 (admin + contract).

Batch A: Workspace core (169)

File Primitive
notes/page.tsx SkeletonList
documents/page.tsx SkeletonList + SkeletonDetailPanel
contacts/page.tsx SkeletonList
contacts/[id]/page.tsx SkeletonDetailPanel
contacts/preferences/page.tsx SkeletonDetailPanel
vault/page.tsx SkeletonTable
calendar/page.tsx SkeletonList
email/page.tsx SkeletonList + SkeletonDetailPanel
memory/page.tsx SkeletonTable
skills/page.tsx SkeletonTable
gallery/page.tsx SkeletonBlock grid
tasks/page.tsx SkeletonList (168)

Batch B: Settings and billing (169)

File Primitive
settings/billing/page.tsx SkeletonDetailPanel
BillingSettingsContent.tsx SkeletonStatGrid + SkeletonTable
BillingSeatsPanel.tsx SkeletonList
BillingInvoiceTable.tsx SkeletonTable
BillingSubscriptionSummary.tsx SkeletonDetailPanel
settings/governance/page.tsx SkeletonDetailPanel
settings/users/page.tsx SkeletonTable
WorkspaceUsersManager.tsx SkeletonTable
settings/web-search/page.tsx SkeletonList
settings/localization/metrics/page.tsx SkeletonStatGrid + SkeletonChart
settings/localization/corrections/page.tsx SkeletonDetailPanel (dialog)
settings/voice-templates/[id]/page.tsx SkeletonDetailPanel

Batch C: Feature workspaces (169)

File Primitive
research/page.tsx LinearProgress only (job progress)
analytics/page.tsx SkeletonChart + SkeletonTable while analyzing
compare/page.tsx SkeletonStatGrid + column SkeletonBlock
opportunities/[id]/page.tsx SkeletonDetailPanel
playbook/page.tsx SkeletonStatGrid
PersonaSelector.tsx SkeletonBlock grid
RepoMapPanel.tsx SkeletonChart
OpportunityArtifactViewer.tsx SkeletonDetailPanel

Batch D: Shell and auth (169)

File Primitive
AppShell.tsx SkeletonText
auth/login/page.tsx SkeletonDetailPanel (Suspense)
auth/accept-invite/page.tsx SkeletonDetailPanel

Deferred to Prompt 170: admin dashboard widgets, admin/mcp, admin/backup, admin/cron, chat bootstrap spinner.

Batch E: Admin and usage (170)

File Primitive
components/admin/* widgets SkeletonBlock, SkeletonChart, SkeletonTable, SkeletonList
components/admin/usage/*, components/usage/* Matching chart/table primitives
components/mutation/CompoundingMetricsCard.tsx SkeletonBlock (loading)
components/mutation/MutationHistoryTable.tsx SkeletonTable
(admin)/dashboard/mutation/[id]/page.tsx SkeletonDetailPanel
(admin)/dashboard/settings/page.tsx SkeletonDetailPanel
admin/teams/page.tsx SkeletonList
admin/tools/page.tsx SkeletonBlock + SkeletonTable
admin/mcp/page.tsx SkeletonList
admin/backup/page.tsx SkeletonDetailPanel
admin/cron/page.tsx SkeletonTable
developer/page.tsx, developer/sdk/page.tsx Primitives throughout
components/developer/OpenApiExplorer.tsx SkeletonDetailPanel
notifications/page.tsx SkeletonList

Allowed spinners: chat/page.tsx (bootstrap), analytics/page.tsx (upload), button actions with @loading-contract-ignore.

Tests

cd frontend && pnpm test -- loading
cd .. && .venv/bin/python -m pytest tests/ui/test_loading_contract.py -q

CI runs tests/ui/test_loading_contract.py via the backend pytest job.