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
+3 -1
View File
@@ -7,6 +7,7 @@ import { useScrollFade } from "../../lib/useScrollFade";
import { type SortState } from "../../lib/columnSort";
import { isActive, type Column, type FilterMap } from "../../lib/tableColumns";
import { alignClass } from "./tableAlign";
import { useStickyTop } from "./stickyTop";
// The column-header machinery — the sortable header row with its filter funnel + popover — lifted
// OUT of DataTable so a non-DataTable body can render under the same headers. The column contract
@@ -185,7 +186,7 @@ function FilterFunnel<T>({
* through; the bottom border rides a `box-shadow` because a sticky `<tr>`'s own border scrolls away
* with the collapsed table box in some browsers. The `::after` paints a short fade just BELOW the
* header so rows dissolve as they slide under it while the header itself stays crisp — which is why
* pages using this pass `PageShell fadeTop={false}`. */
* it switches the page scroller's own top fade off while it is mounted (`useStickyTop`). */
export function ColumnHead<T>({
columns,
sort,
@@ -199,6 +200,7 @@ export function ColumnHead<T>({
filters: FilterMap;
onFilter: (key: string, value: string | string[]) => void;
}) {
useStickyTop();
// Only one column's filter panel is open at a time; each funnel owns its own anchoring.
const [openFilter, setOpenFilter] = useState<string | null>(null);