Bare "…" text — vocabulary-list.tsx:55, vocabulary-terms.tsx:57, authorities-page.tsx:66 (looks like a half-rendered bug; no sense of how much is loading).
Empty <div role="status"> with no visible content — require-auth.tsx:8 (blank app on first load), object-edit-form.tsx:30 (edit form looks broken/blank while the object loads).
Plus the lazy-route FormFallback (app.tsx:30) replaces the whole main pane with bare "Loading…" text → full-pane flash + layout shift on first navigation to /objects/new and /objects/:id/edit.
Suggested fix
Standardize on the existing Skeleton component (or a shared Spinner) for all list/detail/form/auth loading; make form fallbacks/skeletons mirror the real layout to avoid layout shift. Retire the "…" placeholders and empty role="status" divs.
Source: frontend UX/design audit, 2026-06-06.
**Severity: Medium.** _From a frontend UX audit._
## Problem
Loading is rendered (at least) three incompatible ways:
- **Skeleton** — `object-list.tsx:32`, `object-detail.tsx:36`, `search-panel.tsx:89`, `field-list.tsx:27` (good).
- **Bare "…" text** — `vocabulary-list.tsx:55`, `vocabulary-terms.tsx:57`, `authorities-page.tsx:66` (looks like a half-rendered bug; no sense of how much is loading).
- **Empty `<div role="status">` with no visible content** — `require-auth.tsx:8` (blank app on first load), `object-edit-form.tsx:30` (edit form looks broken/blank while the object loads).
Plus the lazy-route `FormFallback` (`app.tsx:30`) replaces the whole main pane with bare "Loading…" text → full-pane flash + layout shift on first navigation to `/objects/new` and `/objects/:id/edit`.
## Suggested fix
Standardize on the existing `Skeleton` component (or a shared `Spinner`) for all list/detail/form/auth loading; make form fallbacks/skeletons mirror the real layout to avoid layout shift. Retire the "…" placeholders and empty `role="status"` divs.
_Source: frontend UX/design audit, 2026-06-06._
Added web/src/components/ui/skeletons.tsx with three shared recipes built on the existing Skeleton, each a role="status" aria-busy aria-label={t("common.loading")} live region (so screen readers now announce loading — the old empty role="status" divs announced nothing):
ListSkeleton({ rows, rowClassName, className })
FormSkeleton({ fields }) — label+input pairs + a button, mirroring the object form
Retired the bare "…" → ListSkeleton at vocabulary-list, vocabulary-terms, authorities-page (rendered in place of the <ul> so it stays valid HTML).
Retired the empty role="status" divs → object-edit-form loading uses FormSkeleton; the whole-app first-load gate (require-auth) uses AppShellSkeleton, so the app no longer flashes blank and doesn't shift when the real shell mounts.
Replaced the lazy-route FormFallback ("Loading…" text) with per-route skeleton fallbacks — FormSkeleton for /objects/new + /objects/:id/edit, ListSkeleton for /fields — removing the full-pane flash + layout shift.
Retrofitted field-list and search-panel to the shared ListSkeleton; objects-table (table tbody rows) and object-detail (single block) keep their fitting inline skeletons.
New i18n common.loading (en/sv); en/sv parity; 207 tests green; typecheck/lint/build/check:size (214.7 KB gz)/check:colors clean; no codename; no new dependency.
Follow-up (out of scope): a Spinner primitive if ever wanted (this is Skeleton-only).
Done — merged to `main` (`53c9810`).
Added **`web/src/components/ui/skeletons.tsx`** with three shared recipes built on the existing `Skeleton`, each a `role="status" aria-busy aria-label={t("common.loading")}` live region (so screen readers now announce loading — the old empty `role="status"` divs announced nothing):
- `ListSkeleton({ rows, rowClassName, className })`
- `FormSkeleton({ fields })` — label+input pairs + a button, mirroring the object form
- `AppShellSkeleton` — a faux sidebar + header + content, mirroring `AppShell`
Applied everywhere loading was inconsistent:
- **Retired the bare "…"** → `ListSkeleton` at vocabulary-list, vocabulary-terms, authorities-page (rendered in place of the `<ul>` so it stays valid HTML).
- **Retired the empty `role="status"` divs** → object-edit-form loading uses `FormSkeleton`; the whole-app first-load gate (require-auth) uses `AppShellSkeleton`, so the app no longer flashes blank and doesn't shift when the real shell mounts.
- **Replaced the lazy-route `FormFallback`** ("Loading…" text) with per-route skeleton fallbacks — `FormSkeleton` for `/objects/new` + `/objects/:id/edit`, `ListSkeleton` for `/fields` — removing the full-pane flash + layout shift.
- **Retrofitted** field-list and search-panel to the shared `ListSkeleton`; objects-table (table `tbody` rows) and object-detail (single block) keep their fitting inline skeletons.
New i18n `common.loading` (en/sv); en/sv parity; 207 tests green; typecheck/lint/build/check:size (214.7 KB gz)/check:colors clean; no codename; no new dependency.
Follow-up (out of scope): a `Spinner` primitive if ever wanted (this is Skeleton-only).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Severity: Medium. From a frontend UX audit.
Problem
Loading is rendered (at least) three incompatible ways:
object-list.tsx:32,object-detail.tsx:36,search-panel.tsx:89,field-list.tsx:27(good).vocabulary-list.tsx:55,vocabulary-terms.tsx:57,authorities-page.tsx:66(looks like a half-rendered bug; no sense of how much is loading).<div role="status">with no visible content —require-auth.tsx:8(blank app on first load),object-edit-form.tsx:30(edit form looks broken/blank while the object loads).Plus the lazy-route
FormFallback(app.tsx:30) replaces the whole main pane with bare "Loading…" text → full-pane flash + layout shift on first navigation to/objects/newand/objects/:id/edit.Suggested fix
Standardize on the existing
Skeletoncomponent (or a sharedSpinner) for all list/detail/form/auth loading; make form fallbacks/skeletons mirror the real layout to avoid layout shift. Retire the "…" placeholders and emptyrole="status"divs.Source: frontend UX/design audit, 2026-06-06.
Done — merged to
main(53c9810).Added
web/src/components/ui/skeletons.tsxwith three shared recipes built on the existingSkeleton, each arole="status" aria-busy aria-label={t("common.loading")}live region (so screen readers now announce loading — the old emptyrole="status"divs announced nothing):ListSkeleton({ rows, rowClassName, className })FormSkeleton({ fields })— label+input pairs + a button, mirroring the object formAppShellSkeleton— a faux sidebar + header + content, mirroringAppShellApplied everywhere loading was inconsistent:
ListSkeletonat vocabulary-list, vocabulary-terms, authorities-page (rendered in place of the<ul>so it stays valid HTML).role="status"divs → object-edit-form loading usesFormSkeleton; the whole-app first-load gate (require-auth) usesAppShellSkeleton, so the app no longer flashes blank and doesn't shift when the real shell mounts.FormFallback("Loading…" text) with per-route skeleton fallbacks —FormSkeletonfor/objects/new+/objects/:id/edit,ListSkeletonfor/fields— removing the full-pane flash + layout shift.ListSkeleton; objects-table (tabletbodyrows) and object-detail (single block) keep their fitting inline skeletons.New i18n
common.loading(en/sv); en/sv parity; 207 tests green; typecheck/lint/build/check:size (214.7 KB gz)/check:colors clean; no codename; no new dependency.Follow-up (out of scope): a
Spinnerprimitive if ever wanted (this is Skeleton-only).