fix(ui): a sticky table header turns the scroller's top fade off itself; DataTable renders one layout

- "A page with a sticky ColumnHead must declare fadeTop: false" lived in
  prose: a registry flag on three pages plus an App special case for the
  Channels card layout, and the next table page would have re-acquired the
  fade-on-the-header defect with every lane green. ColumnHead now registers
  itself with the enclosing PageScroller (ui/stickyTop) while mounted; the
  registry flag, the App branch and the prop plumbing are gone. Measured on
  localdev: the top mask is 0px under Channels' and Audit's headers, 20px on
  Home and the Feed.
- ui/DataTable mounted its table AND its card list at every width, one
  hidden by CSS: double DOM and render cost, duplicate ids, two keyboard
  copies of every row. It now mounts the one the `md` media query calls for
  (lib/useMediaQuery); the keyboard test asserts one layout at each width.
- The Plex module's content width went px → rem with the rest of the page.
This commit is contained in:
2026-09-19 07:51:33 +02:00
parent 8018cd4685
commit 6fda49369f
10 changed files with 225 additions and 110 deletions
+18
View File
@@ -0,0 +1,18 @@
import { useSyncExternalStore } from "react";
/** Whether a CSS media query matches, live. For a component that must RENDER differently at a
* breakpoint rather than merely hide one of two renders with CSS (`ui/DataTable` mounted its table
* AND its card list at every width: double the DOM, duplicate ids and keyboard targets). Where
* `matchMedia` is missing (jsdom) it answers `fallback`. */
export function useMediaQuery(query: string, fallback = true): boolean {
return useSyncExternalStore(
(onChange) => {
if (typeof window.matchMedia !== "function") return () => {};
const mql = window.matchMedia(query);
mql.addEventListener("change", onChange);
return () => mql.removeEventListener("change", onChange);
},
() => (typeof window.matchMedia === "function" ? window.matchMedia(query).matches : fallback),
() => fallback,
);
}