`. 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.
|