/** * RelatedContentSection * * Renders content references grouped by type using the canonical card * components. MOVED from the hub (`components/shared/related-content-card.tsx`) * so any consuming app can embed it; the hub keeps a thin wrapper that * pre-binds its host-specific injections (nav hook, URL recomposition, * program configs, admin campaign card). * * THREE data modes (precedence top-down): * 1. CONTROLLED — `contentRefs` provided (even `[]`): render exactly those * refs, no suggestion fetch (the original investor-update behavior). * 2. SUGGESTION — `entityType` + `entityId` provided: self-fetch * `GET {apiBaseUrl}/api/related-content?entityType&entityId[&count][&excludeTypes]` * (the generic 5-tier engine's second web service). `minResults` maps to * `count`; absent → param not sent (server default applies). Each ref * carries a `reason` (data-only — never rendered, matching the * FaqSection/FaqWithReason precedent). * 3. SSR-HYDRATED suggestion — also pass `initialItems` (the server page * called the engine directly); the first client fetch is skipped per the * `useSelfFetch` initialData contract. * * Group layout (list vs grid) + card size (lg vs default) come from * `CONTENT_REF_GROUPS` in `../../utils/content-ref-groups` — single source of * truth, no per-type logic in this file. Skeletons come from * `renderSkeletonForType` so the placeholder height matches the loaded card * exactly (zero layout shift on resolve). * * One API call per content type via the shared list-URL builder * (`buildListUrl` — injectable; defaults to the lib's byte-parity-tested * builder prefixed with `apiBaseUrl`). Fetching uses `useSelfFetch` (plain * fetch, NO react-query) so third-party embedders need no QueryClientProvider; * cards are imported via DEEP module paths (not the chat barrel) so this * chunk never reaches `@tanstack/react-query`. * * The per-type card + skeleton dispatch lives in `./card-registry` * (`RELATED_CARD_REGISTRY`): ONE entry per content type, each carrying that * type's own row type and the `unknown` → row decode, so a fetched row reaches * its card concretely typed. This file only picks the entry and builds the * shared render context. * * LOCKSTEP NOTE: that registry is the SIZED sibling of the chat-side * `CHAT_CARD_REGISTRY` (`../chat/entity-cards/dispatch.tsx`), which renders * compact `size='sm'` cards wired to the chat runtime. Two dispatchers by * design — when registering a new fetch-mode content type, add it BOTH there * and in `./card-registry` (card + skeleton + list URL). */ import type React from 'react'; import type { ContentRef, ContentRefWithReason } from '../../types/content-ref'; import type { ChatCardDispatchExtras } from '../chat/entity-cards/dispatch'; import { type CardLinkAnchorProps } from './card-registry'; export type { CardLinkAnchorProps }; /** Render-prop component injection for the navigation decision — keeps hook * calls legal (hooks live INSIDE the injected component; `CardForType` * itself calls zero hooks). The hub injects a `useNavLink`-backed provider; * the default is hook-free (pure `decideNewTab`). MUST be defined at module * scope by hosts — an inline arrow would remount every card each render. */ export interface CardLinkProviderProps { href: string | null; targetPlatform: string | null; children: (linkProps: CardLinkAnchorProps | null) => React.ReactElement | null; } export type CardLinkProvider = React.ComponentType; /** Host-injected renderer pair for the admin-only `marketing_campaign` type. * Absent (every non-hub embed) → the type renders nothing (its list URL * hits `/api/admin`, unreachable outside the hub anyway). */ export interface AdminCampaignCardSlot { /** * The host's own campaign card. Its `campaign` prop is a HOST row type this * lib cannot name (the hub's `MarketingCampaign`), and React props are * CONTRAVARIANT, so this is the one seam in the rail with no honest type. * All three alternatives were compiled against the hub's real * `CampaignCardAdmin`: * * - `unknown` — the host can no longer inject: TS2322, "Type * '{ campaign: unknown }' is not assignable to * '{ campaign: MarketingCampaign }'". * - `never` — every host component assigns, but the rail can no longer * hand it a row: TS2769, "Type 'unknown' is not assignable to 'never'". * - a generic on the slot's OWNER (`AdminCampaignCardSlot` * threaded through `RelatedContentSectionProps`) — inference from the * host's card works, but the render still fails: TS2769, "'TCampaign' * could be instantiated with an arbitrary type". The rail holds an * unvalidated row; only the HOST knows how to narrow it. * * Typing this means changing the injection contract (a host-supplied * `render(row: unknown)` closure instead of a component), which is a * breaking change for every embedder that passes `{ Card, Skeleton }`. * Everything the rail renders ITSELF now goes through the per-type * `RELATED_CARD_REGISTRY`; this is the only `any` left. */ Card: React.ComponentType<{ campaign: any; }>; Skeleton: React.ComponentType<{ size?: 'default' | 'sm'; }>; } /** Items per page within one type group. Groups larger than this paginate * with the standard Pagination control (NO nested scrolling — a bounded * scrollbox inside the page traps wheel events and hides the sections * below it). MUST stay at or above the largest suggestion fill * (RELATED_SAME_TYPE_COUNT in the hub's lib/constants/suggestions.ts) so * current rails never paginate — only genuinely big groups (author pages) * do. Exported through the subpath barrel for the hub's module-load * assertion of that relation (entity-suggestion-sections.tsx). */ export declare const GROUP_PAGE_SIZE = 12; export interface RelatedContentSectionProps { /** CONTROLLED mode (the original behavior). When defined — even `[]` — no * suggestion fetch runs and exactly these refs render. */ contentRefs?: ContentRef[]; /** SUGGESTION mode (with `entityId`): self-fetch suggestions for this host * entity from `{apiBaseUrl}/api/related-content`. Ignored when * `contentRefs` is provided. */ entityType?: string; entityId?: number | string; /** AUTHOR mode: self-fetch ALL published content authored by this profile * from `{apiBaseUrl}/api/related-content?authorId=…` (grouped per type, * endless within each group). Ignored when `contentRefs` is provided; * takes precedence over the entityType/entityId suggestion scope. * SSR-hydrate via `initialItems`, same as suggestion mode. */ authorId?: string; /** Maps to the suggestion API's `count` param — the PER-TYPE fill target * for every candidate type EXCEPT the host's own. Absent → param not sent * (server default applies). */ minResults?: number; /** Maps to the suggestion API's `sameTypeCount` param — the budget for the * candidate type MATCHING the host's own `entityType` (same-type boost: * a blog post's rail leads with more blog posts). Absent → param not * sent (host's type uses the server's `count`). */ sameTypeMinResults?: number; /** SSR hydrate for suggestion mode — the server page ran the engine and * drills the refs here; the first client fetch is skipped (useSelfFetch * initialData contract). */ initialItems?: ContentRefWithReason[]; /** Section title (default: "Related Content") */ title?: string; /** * Grid columns at desktop. 2 = denser cards / wider summary (original * investor-update layout); 3 = more cards per row for dashboards. * Only consulted for grid-layout groups. Default: 2. */ columns?: 2 | 3; /** * ContentRef.type values to exclude. Honored in ALL modes — controlled * mode post-filters (original behavior); suggestion mode ALSO forwards the * list verbatim as the API's `excludeTypes=` param so excluded types never * consume engine fill slots (`minResults` stays honored). The subtraction * happens SERVER-side — this component never mirrors the hub's candidate * list. */ excludeTypes?: string[]; /** * SUGGESTION-mode allow-list (rail vocabulary): which content types * participate in this rail. Sent verbatim as the API's `types=` param — * the SERVER intersects it with its own allowed candidate set, and * platform policy gates (e.g. internal-only types) ALWAYS win: the client * cannot request its way past them. Absent → all server-side candidates. */ includeTypes?: string[]; /** Fetch-URL prefix for third-party embeds / reverse proxies * ('' = same-origin). Applied to BOTH the suggestion fetch and the * default per-group list fetches. */ apiBaseUrl?: string; /** Host injection bundle — REUSES the chat dispatcher's * `ChatCardDispatchExtras` (programConfigs, buildOgPlaceholderUrl, * buildProductReleaseCardProps override). Program groups render nothing * when their config is absent. */ extras?: ChatCardDispatchExtras; /** Hub injects its `buildContentURL` recomposition; default uses the * ref's stored `url`/`targetPlatform` as the API composed them. */ resolveHref?: (ref: ContentRef) => { href: string | null; targetPlatform: string | null; }; /** Hub injects its registry-driven entity-list-api builder; default = the * lib's `buildListUrl(type, ids, apiBaseUrl)`. */ buildListUrl?: (type: string, ids: string[]) => string | null; /** Hub injects a `useNavLink`-backed render-prop provider; default = pure * anchor via `decideNewTab`. MUST be a module-scope component. */ LinkProvider?: CardLinkProvider; /** Renderer pair for the admin-only `marketing_campaign` type. Absent → * the type renders nothing. */ adminCampaignCard?: AdminCampaignCardSlot; /** When true, render the section shell (title + an empty-state line) even * with ZERO refs, instead of returning null. Default false (the original * behavior — empty rail = no shell). Opt-in per host page (e.g. people-hub's * "What I Shipped", where the section should always be present). */ showWhenEmpty?: boolean; /** Empty-state copy shown under the title when `showWhenEmpty` and no refs. * Default: "No related content yet." */ emptyStateText?: string; /** Custom empty-state node (e.g. a hub ``) rendered under the * title when `showWhenEmpty` and there are no refs — overrides * `emptyStateText`. Lets a host match its canonical empty state. */ emptyState?: React.ReactNode; } export declare function RelatedContentSection({ contentRefs, entityType, entityId, authorId, minResults, sameTypeMinResults, includeTypes, initialItems, title, columns, excludeTypes, apiBaseUrl, extras, resolveHref, buildListUrl, LinkProvider, adminCampaignCard, showWhenEmpty, emptyStateText, emptyState, }: RelatedContentSectionProps): React.JSX.Element | null; //# sourceMappingURL=related-content-section.d.ts.map