/** * 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`. * * LOCKSTEP NOTE: this file's per-type card/skeleton dispatch 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 here (cards + skeleton + list URL). */ import React from 'react'; import type { ContentRef, ContentRefWithReason } from '../../types/content-ref'; import type { ChatCardDispatchExtras } from '../chat/entity-cards/dispatch'; /** 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; } /** 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 { 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