/** * Per-type card registry for the related-content rail — the SIZED sibling of * the chat side's `CHAT_CARD_REGISTRY` (`../chat/entity-cards/dispatch.tsx`), * and built to the same shape: ONE entry per content type, carrying that * type's skeleton and its card renderer, dispatched once from * `related-content-section.tsx`. * * WHY THIS FILE EXISTS * -------------------- * The rail fetches one row per ref from that type's public list API. The row * arrives as `unknown` (`useSelfFetch` → `extractItems`), and its * SHAPE is decided at runtime by the ref's `type`. The rail used to bridge * that with a single `item: any` threaded through a ten-branch switch, so * every `prop={item}` forward was an unchecked promise: a row missing a field * reached a card that declared it required, and the card rendered the miss * (an empty heading, a version pill reading "vundefined"). * * Each entry here closes over its OWN row type instead. `cardEntry` pairs * a `decode` (runtime `unknown` → `Row`) with a `render` that receives `Row`, * and erases `Row` from the stored entry — so the registry is homogeneous * (`Record`) while every branch stays * concretely typed. No `any`, no assertions into card row types. * * ON THE `*_ROW_DEFAULTS` CONSTANTS * --------------------------------- * Each card declares the FULL entity type as its row prop (`BlogPostSummary`, * `CaseStudy`, …) while reading only a handful of fields off it, and the list * APIs return projections rather than whole rows. A decoder therefore reads * the card's read set from the row and takes every other required field from * a `*_ROW_DEFAULTS` literal. Those placeholders are inert BY CONSTRUCTION — * the decoded row goes nowhere but its own card, and the card never reads * them. They are annotated with the entity type on purpose: a new required * field on `CaseStudy` surfaces as a compile error here instead of silently * reaching a card as `undefined`, which is the exact failure this file * replaces. * * ADDING A TYPE: one entry below (skeleton + decode + render), plus the chat * side's `CHAT_CARD_REGISTRY` and the list-URL builder — see the LOCKSTEP note * in `related-content-section.tsx`. */ import type React from 'react'; import type { ChatCardDispatchExtras } from '../chat/entity-cards/dispatch'; export type CardSize = 'lg' | 'default' | 'sm'; /** Anchor prop bundle the per-card link surface receives — same shape the * hub's `useNavLink` returns and the chat dispatcher's anchor builders * produce. `null` = non-anchor mode (no URL). */ export interface CardLinkAnchorProps { href: string; target?: '_blank'; rel?: 'noopener noreferrer'; onClick?: (e: React.MouseEvent) => void; } /** A STRING field, or `undefined` when missing or wrong-typed. */ export declare function rowString(row: unknown, key: string): string | undefined; /** Everything a card branch needs beyond its own row — computed once per card * by the rail's dispatcher and shared by every entry. */ export interface RelatedCardContext { /** Card density for this group, from `CONTENT_REF_GROUPS`. */ size: CardSize; /** `size` with `'lg'` collapsed to `'default'` — most card variants accept * only that pair. */ legacySize: 'default' | 'sm'; href: string; targetPlatform: string | null; /** `null` = the host surfaced no URL; the card stays in non-anchor mode. */ linkProps: CardLinkAnchorProps | null; /** `{ target, rel }` for the cards that take them as separate props. */ anchorAttrs: Pick; /** Branded OG fallback for cards whose row has no featured image. */ placeholderUrl?: string; extras?: ChatCardDispatchExtras; } /** One registered content type. `Row` is erased here — `cardEntry` below is * what keeps each branch's row type concrete. */ export interface RelatedCardRegistryEntry { /** Sized to match the resolved card exactly (zero layout shift). */ skeleton: (size: CardSize) => React.ReactNode; render: (row: unknown, ctx: RelatedCardContext) => React.ReactNode; } export declare const RELATED_CARD_REGISTRY: Record; //# sourceMappingURL=card-registry.d.ts.map