feat(managers): richer channel cards + drop the rows layout, cap the channel column

UAT round 1 feedback:
- Retire the `rows` layout — it barely differed from the table; keep Table + Cards.
- The card now shows the About cell (+ its hover overlay) and the priority stepper
  in every layout, not just the table: ChannelLayoutGrid gains named slots
  (lead / about / actions) and stops hiding the `hideInCard` columns, since the
  roomy desktop card has space the cramped mobile fallback did not.
- Fix the table losing its centering with "Rows per page: All": a long-titled
  channel stretched the uncapped Channel column (~289→429px) and overflowed the
  centered table. Cap it with a max-w element inside the cell (table-layout:auto
  ignores a cell max-width), the same trick AboutCell already uses. Gaps now 163/162.
This commit is contained in:
2026-07-19 22:42:09 +02:00
parent b659ad800c
commit 875c7ad741
6 changed files with 80 additions and 85 deletions
+14 -17
View File
@@ -1,33 +1,30 @@
// The channel manager's layout vocabulary (E4 S3). Named `ChannelLayout` — NOT `ChannelView` — on
// purpose: the manager already has a `ChannelsView` (the subscribed/discovery/blocked TAB), and a
// one-letter-apart type would be a footgun. This is the table-vs-card-vs-row density switch, the
// sibling of the feed's `FeedView`; it rides the same generic `ViewSwitcher`.
export type ChannelLayout = "table" | "cards" | "rows";
// one-letter-apart type would be a footgun. This is the table-vs-card density switch, riding the
// same generic `ViewSwitcher` as the feed. Two layouts: the DataTable, and a virtualized card grid.
// (A `rows` variant existed briefly but was retired — it barely differed from the table.)
export type ChannelLayout = "table" | "cards";
export const CHANNEL_LAYOUTS: readonly ChannelLayout[] = ["table", "cards", "rows"] as const;
export const CHANNEL_LAYOUTS: readonly ChannelLayout[] = ["table", "cards"] as const;
const CHANNEL_LAYOUT_DEFAULT: ChannelLayout = "table";
/** The card/row layouts' grid spec, consumed by VirtualGrid (S3.3). `table` has no entry — it keeps
* the numbered-pager DataTable, not the virtualized grid. Tuned by measuring the real layouts. */
/** The `cards` layout's grid spec, consumed by VirtualGrid. `table` has no entry — it keeps the
* numbered-pager DataTable, not the virtualized grid. */
export interface ChannelLayoutSpec {
/** Narrowest grid column in px, or null for ONE full-width column (the row list). */
minCol: number | null;
/** Single-column reading-width cap in px (null = fill). */
maxCol: number | null;
/** First-paint row-height estimate; real heights are measured per-row after. */
/** Narrowest grid column in px. */
minCol: number;
/** First-paint row-height estimate; real heights are measured per-row after. Measured in the
* browser (channel heading + About line + meta + tags + actions), not guessed. */
rowEst: number;
}
// rowEst measured in the browser (cards ~176 at 3 columns, the one-line row ~62) — not guessed; a
// bad estimate makes the scrollbar jump as you first scroll in.
export const CHANNEL_LAYOUT_SPEC: Record<Exclude<ChannelLayout, "table">, ChannelLayoutSpec> = {
cards: { minCol: 300, maxCol: null, rowEst: 176 },
rows: { minCol: null, maxCol: 1100, rowEst: 62 },
cards: { minCol: 320, rowEst: 232 },
};
/** Coerce a stored value to a layout; anything unknown (corrupt, a rolled-back future layout) falls
* back to the default rather than rendering nothing. */
/** Coerce a stored value to a layout; anything unknown (corrupt, a rolled-back future layout, or the
* retired "rows") falls back to the default rather than rendering nothing. */
export function parseChannelLayout(raw: unknown): ChannelLayout {
return (CHANNEL_LAYOUTS as readonly string[]).includes(raw as string)
? (raw as ChannelLayout)