feat(managers): table / cards / rows layouts for the channel manager (E4 S3)

A generic ViewSwitcher (reused from the feed) drives a per-account layout
shared by the Subscriptions + Discovery tabs. `table` keeps the DataTable
(numbered pager); `cards` and `rows` render a virtualized grid over the same
client-side list via the new ChannelLayoutGrid, which composes cells straight
from each table's Column<T> render closures (cardPrimary/hideInCard/cardLabel),
so both tabs — with their different columns — get the views for free. Status
chips surface in the fixed band when the table's own controls row is gone.
This commit is contained in:
2026-07-19 21:52:16 +02:00
parent 5ea82095a1
commit 1703d4ab5e
8 changed files with 226 additions and 9 deletions
+33
View File
@@ -0,0 +1,33 @@
// 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";
export const CHANNEL_LAYOUTS: readonly ChannelLayout[] = ["table", "cards", "rows"] 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. */
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. */
rowEst: number;
}
export const CHANNEL_LAYOUT_SPEC: Record<Exclude<ChannelLayout, "table">, ChannelLayoutSpec> = {
cards: { minCol: 300, maxCol: null, rowEst: 210 },
rows: { minCol: null, maxCol: 1100, rowEst: 88 },
};
/** Coerce a stored value to a layout; anything unknown (corrupt, a rolled-back future layout) 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)
: CHANNEL_LAYOUT_DEFAULT;
}
+2
View File
@@ -24,6 +24,8 @@ export const LS = {
feedView: "siftlode.feedView",
channelFilter: "siftlode.channelFilter",
channelsView: "siftlode.channelsView",
// The channel manager's layout (table / cards / rows — see lib/channelLayout.ts), per-account.
channelLayout: "siftlode.channelLayout",
channelsTable: "siftlode.channelsTable",
channelDiscoveryTable: "siftlode.channelDiscoveryTable",
settingsTab: "siftlode.settingsTab",