diff --git a/docs/DESIGN.md b/docs/DESIGN.md
index 7d02ad02..90fa1955 100644
--- a/docs/DESIGN.md
+++ b/docs/DESIGN.md
@@ -191,7 +191,8 @@ Also in force:
3px edge bar. Labels under the icons are optional, a per-user switch in Settings (off by default); they add
height, never width, so each module has a short rail label that fits, and the full name stays in the
tooltip. No wide padding around the icons.
-- **Loading (B8):** a first load of a grid, list or table shows its skeleton (a `Skeleton` primitive), so
+- **Loading (B8):** a first load of a grid, list or table shows its skeleton (`LoadingState shape="grid" |
+"poster" | "list" | "table"`, built from the `Skeleton` primitive), so
nothing jumps when the data lands; a control's own work shows its spinner (`loading`). Neither appears
before ~300ms. [Nielsen #1]
- **The public watch page (B6)** follows these same rules (glass, `Button`, the pressed-pill toggle, the
diff --git a/frontend/src/components/AdminLogs.tsx b/frontend/src/components/AdminLogs.tsx
index a8ea913e..623d98cb 100644
--- a/frontend/src/components/AdminLogs.tsx
+++ b/frontend/src/components/AdminLogs.tsx
@@ -369,7 +369,7 @@ export default function AdminLogs() {
{t("common.loadError")}
) : logs.isLoading ? (
-
{t("adminLogs.empty")}
diff --git a/frontend/src/components/AdminUsers.tsx b/frontend/src/components/AdminUsers.tsx
index 40853cd2..a475bff4 100644
--- a/frontend/src/components/AdminUsers.tsx
+++ b/frontend/src/components/AdminUsers.tsx
@@ -224,7 +224,7 @@ function UsersRoles({ me }: { me: Me }) {
{t("common.loadError")}
) : q.isLoading && !q.data ? (
-
{t("users.roles.empty")}
) : ( @@ -395,7 +395,7 @@ function PendingAccess() { {t("common.loadError")} ) : rows.isLoading && !rows.data ? ( -{t("settings.invites.noPending")}
) : ( diff --git a/frontend/src/components/AuditLog.tsx b/frontend/src/components/AuditLog.tsx index 9aafe88d..36a5316b 100644 --- a/frontend/src/components/AuditLog.tsx +++ b/frontend/src/components/AuditLog.tsx @@ -62,7 +62,7 @@ export default function AuditLog() { {t("common.loadError")} ) : q.isLoading ? ( -{t("audit.empty")}
) : ( diff --git a/frontend/src/components/ChannelDiscovery.tsx b/frontend/src/components/ChannelDiscovery.tsx index 2db1ff5d..f69b8ac7 100644 --- a/frontend/src/components/ChannelDiscovery.tsx +++ b/frontend/src/components/ChannelDiscovery.tsx @@ -228,7 +228,7 @@ export default function ChannelDiscovery({ {t("common.loadError")} ) : query.isLoading ? ( -{t("common.loadError")}
; if (results.length === 0) return{t("plex.collection.addNoResults")}
; diff --git a/frontend/src/components/PlexPlaylistView.tsx b/frontend/src/components/PlexPlaylistView.tsx index baf9687b..383da8f8 100644 --- a/frontend/src/components/PlexPlaylistView.tsx +++ b/frontend/src/components/PlexPlaylistView.tsx @@ -592,7 +592,7 @@ export default function PlexPlaylistView({ {t("common.loadError")} ) : q.isLoading ? ( -{t("plex.playlist.empty")}
) : ( diff --git a/frontend/src/components/plexBrowseLists.tsx b/frontend/src/components/plexBrowseLists.tsx index d9ced901..f2642ae7 100644 --- a/frontend/src/components/plexBrowseLists.tsx +++ b/frontend/src/components/plexBrowseLists.tsx @@ -49,7 +49,7 @@ export function PlexPlaylistsBrowse({ onOpen }: { onOpen: (id: number) => void } ); } - if (q.isLoading) return{t("plex.playlist.none")}
; @@ -212,7 +212,7 @@ export function PlexCollectionsBrowse({ onPick }: { onPick: (id: string, title: {t("common.loadError")} ) : q.isLoading ? ( -{t("plex.filter.collectionEmpty")}
) : ( diff --git a/frontend/src/components/ui/QueryState.dom.test.tsx b/frontend/src/components/ui/QueryState.dom.test.tsx index 0b18581c..5e4183ca 100644 --- a/frontend/src/components/ui/QueryState.dom.test.tsx +++ b/frontend/src/components/ui/QueryState.dom.test.tsx @@ -75,3 +75,15 @@ describe("LoadingState reveal", () => { expect(revealDelay(container)).toBe(150); }); }); + +describe("LoadingState shape (B8)", () => { + it("shows the content's skeleton on the same clock, the label for a screen reader only", () => { + now = 90_000; + const { container } = render(