`. New i18n `common.loading` (en "Loading", sv "Laddar").
- **`ListSkeleton({ rows = 6, rowClassName = "h-9 w-full", className })`** — `space-y-2 p-3` (+ `className`)
with `rows` × `
`.
- **`FormSkeleton({ fields = 5, className })`** — `space-y-4 p-4` (+ `className`); `fields` × a
`space-y-1` group (`
` label + `
`
input) + a trailing `
` (button). Mirrors the object form to avoid shift.
- **`AppShellSkeleton`** — mirrors `AppShell`: `
` → an `aside
w-44 border-r p-3 space-y-2` with ~5 `
` nav rows, and a
`flex-1 flex-col` with a `header border-b px-4 py-2` containing a `
`
and a `
` containing ``. Single top-level `role="status"`
on the outer div (one live region for the whole boot screen).
A `skeletons.stories.tsx` renders the three (visual check + a smoke `play` asserting a `status` region).
### Apply across sites
**Retire "…" (render `ListSkeleton` in place of the `` when loading — valid HTML):**
- `vocabulary-list.tsx`: replace the loading `- …
`. The list is ``; render `{isLoading ? : null}` and keep the
`isError`/empty/data branches in the `` (rendered when not loading). (Keep the column layout —
the skeleton takes the ``'s place during load.)
- `vocabulary-terms.tsx`: replace the loading `- ` — render `` when `isLoading`
instead of the loading `
- ` (keep the `
` for the loaded/empty/error branches).
- `authorities-page.tsx`: same — `` when `isLoading`.
(Concretely: `{isLoading ? : }`, or render the skeleton as a
sibling and gate the `` on `!isLoading`. Implementer picks the cleaner of the two; do not nest a
`` inside `
`.)
**Retire empty `role="status"` divs:**
- `object-edit-form.tsx` (outer `ObjectEditForm`, `isLoading` branch): `return ` (wrap to
match the edit form's container if needed — it renders inside the objects edit pane).
- `require-auth.tsx` (`isLoading`): `return `.
**`app.tsx` lazy fallbacks** — remove `FormFallback`; give each `Suspense` a tailored fallback:
- `ObjectNewPage`, `ObjectEditForm` → `fallback={
}`
(mirrors the new/edit page container `mx-auto max-w-2xl`).
- `FieldsPage` → `fallback={}` (a simple page skeleton; FieldsPage is a 2-col page, but a
list skeleton in the pane is a fine, shift-light placeholder for the brief lazy load).
**Retrofit the good list-like skeletons to `ListSkeleton`** (consistency):
- `field-list.tsx`: `isLoading` → `` (was `space-y-2 p-3` + 6 × `h-9`). Identical output.
- `search-panel.tsx`: the `hasQuery && search.isLoading` block → ``.
- **Keep inline:** `objects-table.tsx` (a `` of ``/`` skeleton rows — table-specific) and
`object-detail.tsx` (single `h-40` block) — both already fitting; not worth forcing into a recipe.
## Data flow / accessibility
Each recipe is a `role="status" aria-busy` live region labelled `common.loading` → screen readers
announce "Loading" (the empty `role="status"` divs announced nothing). Visual skeletons mirror the
loaded layout so there's no jump when content arrives.
## Error handling / edges
- Don't nest `` inside ` ` — render the list skeleton in place of the ``.
- `AppShellSkeleton` is pre-shell (require-auth) — it must not import anything that assumes the shell/
router context beyond `t()` (it only needs `useTranslation`).
- Multiple `role="status"` regions on one screen are acceptable; `AppShellSkeleton` uses ONE outer
region (not one per nested skeleton) to avoid SR noise.
- `FormSkeleton` in the lazy fallback vs the object-edit-form loading branch: both render the same
recipe, so the Suspense fallback → loaded-but-fetching → loaded transition stays visually stable.
## Testing
- `skeletons.stories.tsx`: render `ListSkeleton`/`FormSkeleton`/`AppShellSkeleton`; a `play` asserts a
`role="status"` is present (smoke).
- Existing suite stays green: no test asserts the old "…"/empty-div markup; tests `findBy` content,
which still resolves after loading. (If any test was implicitly relying on the empty `role="status"`
via `getByRole("status")` — none found — update it.)
- Gate: `typecheck`/`lint`/`test`/`build`/`check:size`/`check:colors`; en/sv parity (one new key
`common.loading`); no codename; no new dependency.
## Acceptance criteria
1. `ListSkeleton`, `FormSkeleton`, `AppShellSkeleton` exist (built on `Skeleton`, each a
`role="status" aria-label={t("common.loading")}` live region) with a story.
2. The three "…" placeholders are replaced by `ListSkeleton`; the two empty `role="status"` divs are
replaced (object-edit-form → `FormSkeleton`, require-auth → `AppShellSkeleton`); the lazy
`FormFallback` is replaced by per-route skeleton fallbacks (no full-pane "Loading…").
3. `field-list` + `search-panel` use the shared `ListSkeleton`; objects-table/object-detail keep their
fitting inline skeletons.
4. Loading visuals mirror the loaded layout (no obvious shift); screen readers announce loading.
5. `typecheck`/`lint`/`test`/`build`/`check:colors` green; `check:size` reported; en/sv parity; no
codename; no new dependency.
## Out of scope → follow-ups
- A `Spinner` primitive (Skeleton-only here).
- Reworking objects-table / object-detail inline skeletons.
- Additional route-level Suspense boundaries or data-router `HydrateFallback`s.
|