@flamingo-stack/openframe-frontend-core 0.0.274 → 0.0.275
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-XDGDCO3I.cjs → chunk-5LOLBUZG.cjs} +45 -44
- package/dist/chunk-5LOLBUZG.cjs.map +1 -0
- package/dist/{chunk-4YKGXZ7W.js → chunk-7AH75KFQ.js} +1 -1
- package/dist/chunk-7AH75KFQ.js.map +1 -0
- package/dist/{chunk-CYMMGPPV.js → chunk-CTBL56EC.js} +41 -5
- package/dist/chunk-CTBL56EC.js.map +1 -0
- package/dist/{chunk-2KHFSSFZ.cjs → chunk-DBJZF2OV.cjs} +15 -15
- package/dist/{chunk-2KHFSSFZ.cjs.map → chunk-DBJZF2OV.cjs.map} +1 -1
- package/dist/{chunk-4DYXV3E7.cjs → chunk-DEAWHNYW.cjs} +1177 -1223
- package/dist/chunk-DEAWHNYW.cjs.map +1 -0
- package/dist/{chunk-WU6PMH5L.cjs → chunk-E6RCVOQG.cjs} +41 -43
- package/dist/chunk-E6RCVOQG.cjs.map +1 -0
- package/dist/{chunk-FLIWL6V4.js → chunk-EINXNG5D.js} +4 -4
- package/dist/{chunk-OPSZNW3I.js → chunk-EKWPIEHH.js} +3031 -3077
- package/dist/chunk-EKWPIEHH.js.map +1 -0
- package/dist/{chunk-N5MCKGAB.js → chunk-FDHKJJPK.js} +34 -33
- package/dist/{chunk-N5MCKGAB.js.map → chunk-FDHKJJPK.js.map} +1 -1
- package/dist/{chunk-BSOHVQ7X.js → chunk-GHWOQUB4.js} +366 -391
- package/dist/chunk-GHWOQUB4.js.map +1 -0
- package/dist/{chunk-T7AJBKZP.cjs → chunk-GVSTRN36.cjs} +5 -2
- package/dist/chunk-GVSTRN36.cjs.map +1 -0
- package/dist/{chunk-L4LAAA5Q.js → chunk-IMNGGLKL.js} +5 -2
- package/dist/chunk-IMNGGLKL.js.map +1 -0
- package/dist/{chunk-VASNLWPZ.js → chunk-NY7OG2OJ.js} +5 -7
- package/dist/{chunk-VASNLWPZ.js.map → chunk-NY7OG2OJ.js.map} +1 -1
- package/dist/{chunk-5QJQAFTP.cjs → chunk-OKDCUJOZ.cjs} +48 -12
- package/dist/chunk-OKDCUJOZ.cjs.map +1 -0
- package/dist/{chunk-MEAR2WHS.cjs → chunk-PC7YOZ4I.cjs} +1 -1
- package/dist/chunk-PC7YOZ4I.cjs.map +1 -0
- package/dist/{chunk-QLWBBCI5.cjs → chunk-Q2ITV3NY.cjs} +5 -4
- package/dist/chunk-Q2ITV3NY.cjs.map +1 -0
- package/dist/chunk-RGALV4UX.js +433 -0
- package/dist/chunk-RGALV4UX.js.map +1 -0
- package/dist/{chunk-W5KBDFCR.cjs → chunk-SA4ZTD3G.cjs} +30 -30
- package/dist/{chunk-W5KBDFCR.cjs.map → chunk-SA4ZTD3G.cjs.map} +1 -1
- package/dist/{chunk-GL5X6VVC.js → chunk-T4XXYAYY.js} +4 -4
- package/dist/{chunk-ZMFT5XOB.js → chunk-UXJCZOLA.js} +4 -3
- package/dist/chunk-UXJCZOLA.js.map +1 -0
- package/dist/{chunk-7NJUPRN6.cjs → chunk-XCPBD2PG.cjs} +183 -208
- package/dist/chunk-XCPBD2PG.cjs.map +1 -0
- package/dist/chunk-ZSUNQJRO.cjs +433 -0
- package/dist/chunk-ZSUNQJRO.cjs.map +1 -0
- package/dist/components/chat/chat-message-row.d.ts +1 -1
- package/dist/components/chat/chat-message-row.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +10 -11
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +12 -13
- package/dist/components/contact/index.cjs +7 -9
- package/dist/components/contact/index.cjs.map +1 -1
- package/dist/components/contact/index.js +6 -8
- package/dist/components/faq/faq-document-page.d.ts +21 -0
- package/dist/components/faq/faq-document-page.d.ts.map +1 -0
- package/dist/components/faq/index.cjs +22 -5
- package/dist/components/faq/index.cjs.map +1 -1
- package/dist/components/faq/index.d.ts +1 -0
- package/dist/components/faq/index.d.ts.map +1 -1
- package/dist/components/faq/index.js +21 -4
- package/dist/components/features/index.cjs +6 -8
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.js +5 -7
- package/dist/components/index.cjs +304 -309
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +249 -254
- package/dist/components/index.js.map +1 -1
- package/dist/components/layout/article-detail-layout.d.ts +21 -9
- package/dist/components/layout/article-detail-layout.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +6 -8
- package/dist/components/navigation/index.cjs.map +1 -1
- package/dist/components/navigation/index.js +5 -7
- package/dist/components/onboarding-guides/index.cjs +33 -46
- package/dist/components/onboarding-guides/index.cjs.map +1 -1
- package/dist/components/onboarding-guides/index.js +11 -24
- package/dist/components/onboarding-guides/index.js.map +1 -1
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +8 -10
- package/dist/components/related-content/index.cjs.map +1 -1
- package/dist/components/related-content/index.js +7 -9
- package/dist/components/shared/delivery/delivery-lists.d.ts.map +1 -1
- package/dist/components/shared/detail-page-skeleton.d.ts +6 -1
- package/dist/components/shared/detail-page-skeleton.d.ts.map +1 -1
- package/dist/components/shared/legal-document/use-legal-docs.d.ts.map +1 -1
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/shared/roadmap/roadmap-grid.d.ts.map +1 -1
- package/dist/components/shared/roadmap/use-roadmap-voting.d.ts.map +1 -1
- package/dist/components/tickets/help-center-list.d.ts +9 -1
- package/dist/components/tickets/help-center-list.d.ts.map +1 -1
- package/dist/components/tickets/hooks/use-ticket-actions.d.ts.map +1 -1
- package/dist/components/tickets/hooks/use-ticket-engagements.d.ts.map +1 -1
- package/dist/components/tickets/hooks/use-tickets-list.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +81 -76
- package/dist/components/tickets/index.cjs.map +1 -1
- package/dist/components/tickets/index.js +27 -22
- package/dist/components/tickets/index.js.map +1 -1
- package/dist/components/ui/index.cjs +12 -15
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +24 -27
- package/dist/contexts/chat-runtime-context.d.ts +9 -0
- package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
- package/dist/contexts/index.cjs +2 -2
- package/dist/contexts/index.js +1 -1
- package/dist/hooks/index.cjs +3 -3
- package/dist/hooks/index.js +2 -2
- package/dist/hooks/use-contact-submission.d.ts.map +1 -1
- package/dist/hooks/use-self-fetch.d.ts.map +1 -1
- package/dist/index.cjs +11 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +32 -36
- package/dist/utils/embed-authed-fetch.d.ts +7 -0
- package/dist/utils/embed-authed-fetch.d.ts.map +1 -1
- package/dist/utils/embed-content-fetch.d.ts +2 -0
- package/dist/utils/embed-content-fetch.d.ts.map +1 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/chat-message-row.tsx +12 -2
- package/src/components/faq/faq-document-page.tsx +69 -0
- package/src/components/faq/index.ts +1 -0
- package/src/components/layout/article-detail-layout.tsx +24 -11
- package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +21 -16
- package/src/components/shared/delivery/delivery-lists.tsx +3 -2
- package/src/components/shared/detail-page-skeleton.tsx +18 -10
- package/src/components/shared/legal-document/use-legal-docs.ts +2 -1
- package/src/components/shared/product-release/release-detail-page.tsx +27 -17
- package/src/components/shared/roadmap/roadmap-grid.tsx +2 -1
- package/src/components/shared/roadmap/use-roadmap-voting.ts +3 -2
- package/src/components/tickets/help-center-list.tsx +12 -4
- package/src/components/tickets/hooks/use-ticket-actions.ts +7 -2
- package/src/components/tickets/hooks/use-ticket-engagements.ts +6 -1
- package/src/components/tickets/hooks/use-tickets-list.ts +7 -1
- package/src/components/tickets/ticket-detail-drawer.tsx +1 -0
- package/src/contexts/chat-runtime-context.tsx +9 -0
- package/src/hooks/use-contact-submission.ts +2 -1
- package/src/hooks/use-self-fetch.ts +2 -1
- package/src/styles/index.css +1 -0
- package/src/styles/ods-page-shell.css +29 -0
- package/src/utils/embed-authed-fetch.ts +10 -0
- package/src/utils/embed-content-fetch.ts +30 -0
- package/dist/chunk-4DYXV3E7.cjs.map +0 -1
- package/dist/chunk-4YKGXZ7W.js.map +0 -1
- package/dist/chunk-5QJQAFTP.cjs.map +0 -1
- package/dist/chunk-722TTRIJ.cjs +0 -29
- package/dist/chunk-722TTRIJ.cjs.map +0 -1
- package/dist/chunk-7NJUPRN6.cjs.map +0 -1
- package/dist/chunk-BSOHVQ7X.js.map +0 -1
- package/dist/chunk-CYMMGPPV.js.map +0 -1
- package/dist/chunk-DAAFVOTL.cjs +0 -172
- package/dist/chunk-DAAFVOTL.cjs.map +0 -1
- package/dist/chunk-L4LAAA5Q.js.map +0 -1
- package/dist/chunk-MEAR2WHS.cjs.map +0 -1
- package/dist/chunk-OO4ZGDRO.js +0 -29
- package/dist/chunk-OO4ZGDRO.js.map +0 -1
- package/dist/chunk-OPSZNW3I.js.map +0 -1
- package/dist/chunk-QLWBBCI5.cjs.map +0 -1
- package/dist/chunk-T2URJAX2.js +0 -169
- package/dist/chunk-T2URJAX2.js.map +0 -1
- package/dist/chunk-T7AJBKZP.cjs.map +0 -1
- package/dist/chunk-WU6PMH5L.cjs.map +0 -1
- package/dist/chunk-XDGDCO3I.cjs.map +0 -1
- package/dist/chunk-Y2B6WDU6.cjs +0 -169
- package/dist/chunk-Y2B6WDU6.cjs.map +0 -1
- package/dist/chunk-ZMFT5XOB.js.map +0 -1
- package/dist/chunk-ZXZJSTNV.js +0 -172
- package/dist/chunk-ZXZJSTNV.js.map +0 -1
- /package/dist/{chunk-FLIWL6V4.js.map → chunk-EINXNG5D.js.map} +0 -0
- /package/dist/{chunk-GL5X6VVC.js.map → chunk-T4XXYAYY.js.map} +0 -0
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }"use client";
|
|
2
2
|
|
|
3
|
+
|
|
4
|
+
var _chunkZSUNQJROcjs = require('./chunk-ZSUNQJRO.cjs');
|
|
5
|
+
|
|
3
6
|
// src/hooks/use-self-fetch.ts
|
|
4
7
|
var _react = require('react');
|
|
5
8
|
function useSelfFetch(url, options) {
|
|
@@ -24,7 +27,7 @@ function useSelfFetch(url, options) {
|
|
|
24
27
|
try {
|
|
25
28
|
setIsLoading(true);
|
|
26
29
|
setError(false);
|
|
27
|
-
const res = await
|
|
30
|
+
const res = await _chunkZSUNQJROcjs.contentFetch.call(void 0, url, { signal: ctrl.signal });
|
|
28
31
|
if (!res.ok) throw new Error(`Request failed (${res.status})`);
|
|
29
32
|
const json = await res.json();
|
|
30
33
|
if (!cancelled) {
|
|
@@ -62,4 +65,4 @@ function useSelfFetch(url, options) {
|
|
|
62
65
|
|
|
63
66
|
|
|
64
67
|
exports.useSelfFetch = useSelfFetch;
|
|
65
|
-
//# sourceMappingURL=chunk-
|
|
68
|
+
//# sourceMappingURL=chunk-GVSTRN36.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-GVSTRN36.cjs","../src/hooks/use-self-fetch.ts"],"names":[],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-GVSTRN36.cjs","sourcesContent":[null,"'use client'\n\nimport { useEffect, useRef, useState, type Dispatch, type SetStateAction } from 'react'\nimport { contentFetch } from '../utils/embed-content-fetch'\n\nexport interface UseSelfFetchResult<T> {\n data: T | null\n /** Imperatively patch the fetched data (e.g. optimistic vote updates). */\n setData: Dispatch<SetStateAction<T | null>>\n isLoading: boolean\n error: boolean\n /** Re-run the fetch (error-retry affordance). */\n reload: () => void\n}\n\n/**\n * The single source for the 4 self-fetching content views\n * (`ProductReleasesView`, `RoadmapView`, the onboarding catalog/detail). GETs a\n * configured `url` into component state with **plain `fetch` + `useEffect`** —\n * deliberately NO react-query, so embedders don't need a QueryClient. This is\n * the same technology choice the shipped `DeliveryLists` makes; that component\n * predates this hook and still hand-rolls the loop (a future pass can migrate\n * it onto a two-url variant of this hook).\n *\n * Behaviour:\n * - `url = null` → fetching is DISABLED (controlled / SSR mode, or missing\n * config); returns `initialData ?? null` and never fetches.\n * - `initialData` provided → hydrates immediately and SKIPS the first fetch\n * (so the hub's server-rendered data isn't re-fetched on mount); later\n * `url` changes still re-fetch.\n * - a `cancelled` guard ensures an in-flight response from a STALE `url`\n * (e.g. a fast pagination / section toggle) can't overwrite a newer one.\n * - `reload()` bumps an internal key to retry after an error.\n *\n * Re-fetches whenever `url` changes, so callers fold all query params INTO the\n * url string (the url IS the cache key).\n */\nexport function useSelfFetch<T>(\n url: string | null,\n options?: { initialData?: T },\n): UseSelfFetchResult<T> {\n const initialData = options?.initialData\n const [data, setData] = useState<T | null>(initialData ?? null)\n const [isLoading, setIsLoading] = useState(initialData === undefined && url !== null)\n const [error, setError] = useState(false)\n const [reloadKey, setReloadKey] = useState(0)\n // The url whose data we currently hold — seeded from SSR `initialData`, then set after\n // each completed fetch. The effect skips the fetch when this equals `url` (so server-\n // rendered data isn't re-fetched on mount). A VALUE compare (not a one-shot flag) so the\n // SSR-hydration skip survives React 18 StrictMode's mount→unmount→remount in dev;\n // `reload()` nulls it to force a re-fetch of the same url.\n const dataUrlRef = useRef<string | null>(initialData !== undefined ? url : null)\n\n // Re-sync when a CONTROLLED `initialData` changes (e.g. the host navigates\n // between detail slugs without remounting). No-op in self-fetch mode, where\n // `initialData` is `undefined`.\n useEffect(() => {\n if (initialData !== undefined) setData(initialData)\n }, [initialData])\n\n useEffect(() => {\n if (url === null) {\n setIsLoading(false)\n return\n }\n // Already hold data for this exact url (SSR-hydrated, or a prior completed fetch) →\n // skip. `reload()` clears `dataUrlRef` so a retry of the same url still fetches.\n if (dataUrlRef.current === url) return\n const ctrl = new AbortController()\n let cancelled = false\n async function load() {\n try {\n setIsLoading(true)\n setError(false)\n const res = await contentFetch(url as string, { signal: ctrl.signal })\n if (!res.ok) throw new Error(`Request failed (${res.status})`)\n const json = (await res.json()) as T\n if (!cancelled) {\n setData(json)\n dataUrlRef.current = url // remember the url we now hold data for\n }\n } catch (err) {\n // AbortError on cleanup (unmount / stale-url change / React StrictMode's dev\n // double-invoke) is EXPECTED — the request was intentionally aborted. Aborting\n // also means the orphaned StrictMode fetch shows as \"cancelled\" instead of\n // completing as a wasted duplicate (parity with useChatIdentity). Only real\n // failures fall through to the error state + console.\n if (cancelled || (err as Error)?.name === 'AbortError') return\n setError(true)\n console.error('useSelfFetch:', url, err)\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => {\n cancelled = true\n ctrl.abort()\n }\n // `url` folds in every query param; `reloadKey` drives retry.\n }, [url, reloadKey])\n\n return {\n data,\n setData,\n isLoading,\n error,\n // Force a re-fetch of the current url: clear the held-url ref so the skip above\n // doesn't short-circuit, then bump the effect key.\n reload: () => {\n dataUrlRef.current = null\n setReloadKey((k) => k + 1)\n },\n }\n}\n"]}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import {
|
|
3
|
+
contentFetch
|
|
4
|
+
} from "./chunk-RGALV4UX.js";
|
|
2
5
|
|
|
3
6
|
// src/hooks/use-self-fetch.ts
|
|
4
7
|
import { useEffect, useRef, useState } from "react";
|
|
@@ -24,7 +27,7 @@ function useSelfFetch(url, options) {
|
|
|
24
27
|
try {
|
|
25
28
|
setIsLoading(true);
|
|
26
29
|
setError(false);
|
|
27
|
-
const res = await
|
|
30
|
+
const res = await contentFetch(url, { signal: ctrl.signal });
|
|
28
31
|
if (!res.ok) throw new Error(`Request failed (${res.status})`);
|
|
29
32
|
const json = await res.json();
|
|
30
33
|
if (!cancelled) {
|
|
@@ -62,4 +65,4 @@ function useSelfFetch(url, options) {
|
|
|
62
65
|
export {
|
|
63
66
|
useSelfFetch
|
|
64
67
|
};
|
|
65
|
-
//# sourceMappingURL=chunk-
|
|
68
|
+
//# sourceMappingURL=chunk-IMNGGLKL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/use-self-fetch.ts"],"sourcesContent":["'use client'\n\nimport { useEffect, useRef, useState, type Dispatch, type SetStateAction } from 'react'\nimport { contentFetch } from '../utils/embed-content-fetch'\n\nexport interface UseSelfFetchResult<T> {\n data: T | null\n /** Imperatively patch the fetched data (e.g. optimistic vote updates). */\n setData: Dispatch<SetStateAction<T | null>>\n isLoading: boolean\n error: boolean\n /** Re-run the fetch (error-retry affordance). */\n reload: () => void\n}\n\n/**\n * The single source for the 4 self-fetching content views\n * (`ProductReleasesView`, `RoadmapView`, the onboarding catalog/detail). GETs a\n * configured `url` into component state with **plain `fetch` + `useEffect`** —\n * deliberately NO react-query, so embedders don't need a QueryClient. This is\n * the same technology choice the shipped `DeliveryLists` makes; that component\n * predates this hook and still hand-rolls the loop (a future pass can migrate\n * it onto a two-url variant of this hook).\n *\n * Behaviour:\n * - `url = null` → fetching is DISABLED (controlled / SSR mode, or missing\n * config); returns `initialData ?? null` and never fetches.\n * - `initialData` provided → hydrates immediately and SKIPS the first fetch\n * (so the hub's server-rendered data isn't re-fetched on mount); later\n * `url` changes still re-fetch.\n * - a `cancelled` guard ensures an in-flight response from a STALE `url`\n * (e.g. a fast pagination / section toggle) can't overwrite a newer one.\n * - `reload()` bumps an internal key to retry after an error.\n *\n * Re-fetches whenever `url` changes, so callers fold all query params INTO the\n * url string (the url IS the cache key).\n */\nexport function useSelfFetch<T>(\n url: string | null,\n options?: { initialData?: T },\n): UseSelfFetchResult<T> {\n const initialData = options?.initialData\n const [data, setData] = useState<T | null>(initialData ?? null)\n const [isLoading, setIsLoading] = useState(initialData === undefined && url !== null)\n const [error, setError] = useState(false)\n const [reloadKey, setReloadKey] = useState(0)\n // The url whose data we currently hold — seeded from SSR `initialData`, then set after\n // each completed fetch. The effect skips the fetch when this equals `url` (so server-\n // rendered data isn't re-fetched on mount). A VALUE compare (not a one-shot flag) so the\n // SSR-hydration skip survives React 18 StrictMode's mount→unmount→remount in dev;\n // `reload()` nulls it to force a re-fetch of the same url.\n const dataUrlRef = useRef<string | null>(initialData !== undefined ? url : null)\n\n // Re-sync when a CONTROLLED `initialData` changes (e.g. the host navigates\n // between detail slugs without remounting). No-op in self-fetch mode, where\n // `initialData` is `undefined`.\n useEffect(() => {\n if (initialData !== undefined) setData(initialData)\n }, [initialData])\n\n useEffect(() => {\n if (url === null) {\n setIsLoading(false)\n return\n }\n // Already hold data for this exact url (SSR-hydrated, or a prior completed fetch) →\n // skip. `reload()` clears `dataUrlRef` so a retry of the same url still fetches.\n if (dataUrlRef.current === url) return\n const ctrl = new AbortController()\n let cancelled = false\n async function load() {\n try {\n setIsLoading(true)\n setError(false)\n const res = await contentFetch(url as string, { signal: ctrl.signal })\n if (!res.ok) throw new Error(`Request failed (${res.status})`)\n const json = (await res.json()) as T\n if (!cancelled) {\n setData(json)\n dataUrlRef.current = url // remember the url we now hold data for\n }\n } catch (err) {\n // AbortError on cleanup (unmount / stale-url change / React StrictMode's dev\n // double-invoke) is EXPECTED — the request was intentionally aborted. Aborting\n // also means the orphaned StrictMode fetch shows as \"cancelled\" instead of\n // completing as a wasted duplicate (parity with useChatIdentity). Only real\n // failures fall through to the error state + console.\n if (cancelled || (err as Error)?.name === 'AbortError') return\n setError(true)\n console.error('useSelfFetch:', url, err)\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => {\n cancelled = true\n ctrl.abort()\n }\n // `url` folds in every query param; `reloadKey` drives retry.\n }, [url, reloadKey])\n\n return {\n data,\n setData,\n isLoading,\n error,\n // Force a re-fetch of the current url: clear the held-url ref so the skip above\n // doesn't short-circuit, then bump the effect key.\n reload: () => {\n dataUrlRef.current = null\n setReloadKey((k) => k + 1)\n },\n }\n}\n"],"mappings":";;;;;;AAEA,SAAS,WAAW,QAAQ,gBAAoD;AAmCzE,SAAS,aACd,KACA,SACuB;AACvB,QAAM,cAAc,SAAS;AAC7B,QAAM,CAAC,MAAM,OAAO,IAAI,SAAmB,eAAe,IAAI;AAC9D,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,gBAAgB,UAAa,QAAQ,IAAI;AACpF,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,KAAK;AACxC,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,CAAC;AAM5C,QAAM,aAAa,OAAsB,gBAAgB,SAAY,MAAM,IAAI;AAK/E,YAAU,MAAM;AACd,QAAI,gBAAgB,OAAW,SAAQ,WAAW;AAAA,EACpD,GAAG,CAAC,WAAW,CAAC;AAEhB,YAAU,MAAM;AACd,QAAI,QAAQ,MAAM;AAChB,mBAAa,KAAK;AAClB;AAAA,IACF;AAGA,QAAI,WAAW,YAAY,IAAK;AAChC,UAAM,OAAO,IAAI,gBAAgB;AACjC,QAAI,YAAY;AAChB,mBAAe,OAAO;AACpB,UAAI;AACF,qBAAa,IAAI;AACjB,iBAAS,KAAK;AACd,cAAM,MAAM,MAAM,aAAa,KAAe,EAAE,QAAQ,KAAK,OAAO,CAAC;AACrE,YAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,mBAAmB,IAAI,MAAM,GAAG;AAC7D,cAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,YAAI,CAAC,WAAW;AACd,kBAAQ,IAAI;AACZ,qBAAW,UAAU;AAAA,QACvB;AAAA,MACF,SAAS,KAAK;AAMZ,YAAI,aAAc,KAAe,SAAS,aAAc;AACxD,iBAAS,IAAI;AACb,gBAAQ,MAAM,iBAAiB,KAAK,GAAG;AAAA,MACzC,UAAE;AACA,YAAI,CAAC,UAAW,cAAa,KAAK;AAAA,MACpC;AAAA,IACF;AACA,SAAK;AACL,WAAO,MAAM;AACX,kBAAY;AACZ,WAAK,MAAM;AAAA,IACb;AAAA,EAEF,GAAG,CAAC,KAAK,SAAS,CAAC;AAEnB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA;AAAA,IAGA,QAAQ,MAAM;AACZ,iBAAW,UAAU;AACrB,mBAAa,CAAC,MAAM,IAAI,CAAC;AAAA,IAC3B;AAAA,EACF;AACF;","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import {
|
|
3
3
|
useSelfFetch
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-IMNGGLKL.js";
|
|
5
5
|
import {
|
|
6
6
|
BlogCard,
|
|
7
7
|
BlogCardSkeleton,
|
|
@@ -23,21 +23,19 @@ import {
|
|
|
23
23
|
WhatIShippedCardSkeleton,
|
|
24
24
|
buildListUrl,
|
|
25
25
|
buildProductReleaseCardProps,
|
|
26
|
+
buildSuggestionUrl,
|
|
26
27
|
canonicalContentRefType,
|
|
27
28
|
extractItemId,
|
|
28
29
|
extractItems,
|
|
29
30
|
getContentRefLabelOrTitleCase,
|
|
30
31
|
init_pagination,
|
|
31
32
|
orderContentRefTypes
|
|
32
|
-
} from "./chunk-
|
|
33
|
-
import {
|
|
34
|
-
buildSuggestionUrl
|
|
35
|
-
} from "./chunk-T2URJAX2.js";
|
|
33
|
+
} from "./chunk-EKWPIEHH.js";
|
|
36
34
|
import {
|
|
37
35
|
OnboardingGuideCard,
|
|
38
36
|
OnboardingGuideCardSkeleton,
|
|
39
37
|
decideNewTab
|
|
40
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-GHWOQUB4.js";
|
|
41
39
|
|
|
42
40
|
// src/components/related-content/related-content-section.tsx
|
|
43
41
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
@@ -363,4 +361,4 @@ export {
|
|
|
363
361
|
GROUP_PAGE_SIZE,
|
|
364
362
|
RelatedContentSection
|
|
365
363
|
};
|
|
366
|
-
//# sourceMappingURL=chunk-
|
|
364
|
+
//# sourceMappingURL=chunk-NY7OG2OJ.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/related-content/related-content-section.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * RelatedContentSection\n *\n * Renders content references grouped by type using the canonical card\n * components. MOVED from the hub (`components/shared/related-content-card.tsx`)\n * so any consuming app can embed it; the hub keeps a thin wrapper that\n * pre-binds its host-specific injections (nav hook, URL recomposition,\n * program configs, admin campaign card).\n *\n * THREE data modes (precedence top-down):\n * 1. CONTROLLED — `contentRefs` provided (even `[]`): render exactly those\n * refs, no suggestion fetch (the original investor-update behavior).\n * 2. SUGGESTION — `entityType` + `entityId` provided: self-fetch\n * `GET {apiBaseUrl}/api/related-content?entityType&entityId[&count][&excludeTypes]`\n * (the generic 5-tier engine's second web service). `minResults` maps to\n * `count`; absent → param not sent (server default applies). Each ref\n * carries a `reason` (data-only — never rendered, matching the\n * FaqSection/FaqWithReason precedent).\n * 3. SSR-HYDRATED suggestion — also pass `initialItems` (the server page\n * called the engine directly); the first client fetch is skipped per the\n * `useSelfFetch` initialData contract.\n *\n * Group layout (list vs grid) + card size (lg vs default) come from\n * `CONTENT_REF_GROUPS` in `../../utils/content-ref-groups` — single source of\n * truth, no per-type logic in this file. Skeletons come from\n * `renderSkeletonForType` so the placeholder height matches the loaded card\n * exactly (zero layout shift on resolve).\n *\n * One API call per content type via the shared list-URL builder\n * (`buildListUrl` — injectable; defaults to the lib's byte-parity-tested\n * builder prefixed with `apiBaseUrl`). Fetching uses `useSelfFetch` (plain\n * fetch, NO react-query) so third-party embedders need no QueryClientProvider;\n * cards are imported via DEEP module paths (not the chat barrel) so this\n * chunk never reaches `@tanstack/react-query`.\n *\n * LOCKSTEP NOTE: this file's per-type card/skeleton dispatch is the SIZED\n * sibling of the chat-side `CHAT_CARD_REGISTRY` (`../chat/entity-cards/\n * dispatch.tsx`), which renders compact `size='sm'` cards wired to the chat\n * runtime. Two dispatchers by design — when registering a new fetch-mode\n * content type, add it BOTH there and here (cards + skeleton + list URL).\n */\n\nimport React, { useEffect, useMemo, useRef, useState } from 'react';\nimport {\n CONTENT_REF_GROUPS,\n getContentRefLabelOrTitleCase,\n orderContentRefTypes,\n type ContentRefGroupConfig,\n} from '../../utils/content-ref-groups';\nimport type { ContentRef, ContentRefWithReason } from '../../types/content-ref';\nimport { useSelfFetch } from '../../hooks/use-self-fetch';\nimport { Pagination } from '../pagination';\nimport { extractItems, extractItemId } from '../../utils/extract-items';\nimport { buildListUrl as libBuildListUrl, canonicalContentRefType } from '../../utils/list-url';\nimport { buildSuggestionUrl } from '../../utils/suggestion-url';\nimport { decideNewTab } from '../chat/utils/decide-new-tab';\n// DEEP card imports — NOT the `../chat` barrel (the barrel statically reaches\n// @tanstack/react-query via embeddable-chat + its hooks). Deep paths keep this\n// component's SOURCE graph react-query-free. Note: tsup's shared-chunk\n// splitting may still colocate the cards with chat hooks in one dist chunk\n// (react-query is a required peerDep, so resolution always succeeds) — the\n// guarantee that matters here is the RUNTIME one: nothing on this path ever\n// instantiates a QueryClient, so embedders need NO QueryClientProvider.\nimport { BlogCard, BlogCardSkeleton } from '../chat/entity-cards/blog-card';\nimport { WhatIShippedCard, WhatIShippedCardSkeleton } from '../chat/entity-cards/what-i-shipped-card';\nimport { CaseStudyCard, CaseStudyCardSkeleton } from '../chat/entity-cards/case-study-card';\nimport { CustomerInterviewCard, CustomerInterviewCardSkeleton } from '../chat/entity-cards/customer-interview-card';\nimport { ProductReleaseCard, ProductReleaseCardSkeleton } from '../chat/entity-cards/product-release-card';\nimport { buildProductReleaseCardProps } from '../chat/entity-cards/product-release-card-defaults';\nimport { ProgramCard, ProgramCardSkeleton } from '../chat/entity-cards/program-card';\nimport { InvestorUpdateCard, InvestorUpdateCardSkeleton } from '../chat/entity-cards/investor-update-card';\nimport { OnboardingGuideCard, OnboardingGuideCardSkeleton } from '../chat/entity-cards/onboarding-guide-card';\nimport { RoadmapCard, RoadmapCardSkeleton } from '../chat/entity-cards/roadmap-card';\n// Type-only — erased at build, no runtime dependency on the dispatch module.\nimport type { ChatCardDispatchExtras } from '../chat/entity-cards/dispatch';\n\ntype CardSize = 'lg' | 'default' | 'sm';\n\n/** Anchor prop bundle the per-card link surface receives — same shape the\n * hub's `useNavLink` returns and the chat dispatcher's anchor builders\n * produce. `null` = non-anchor mode (no URL). */\nexport interface CardLinkAnchorProps {\n href: string;\n target?: '_blank';\n rel?: 'noopener noreferrer';\n onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;\n}\n\n/** Render-prop component injection for the navigation decision — keeps hook\n * calls legal (hooks live INSIDE the injected component; `CardForType`\n * itself calls zero hooks). The hub injects a `useNavLink`-backed provider;\n * the default is hook-free (pure `decideNewTab`). MUST be defined at module\n * scope by hosts — an inline arrow would remount every card each render. */\nexport interface CardLinkProviderProps {\n href: string | null;\n targetPlatform: string | null;\n children: (linkProps: CardLinkAnchorProps | null) => React.ReactElement | null;\n}\nexport type CardLinkProvider = React.ComponentType<CardLinkProviderProps>;\n\n/** Default link provider for standalone embeds: relative/-same-origin hrefs\n * stay same-tab, cross-origin pops a new tab (pure `decideNewTab` with no\n * platform context — `currentSource: ''` falls through to the origin\n * check). No router integration, no hooks. */\nfunction DefaultLinkPropsProvider({ href, targetPlatform, children }: CardLinkProviderProps): React.ReactElement | null {\n if (!href) return children(null);\n const newTab = decideNewTab({ href, targetPlatform, currentSource: '' });\n return children(\n newTab\n ? { href, target: '_blank', rel: 'noopener noreferrer' }\n : { href },\n );\n}\n\n/** Default href resolution: trust the ref's stored url/targetPlatform as the\n * API composed them. The hub overrides this with its `buildContentURL`\n * re-composition so dev gets localhost and prod gets platform domains. */\nfunction defaultResolveHref(ref: ContentRef): { href: string | null; targetPlatform: string | null } {\n return { href: ref.url || null, targetPlatform: ref.targetPlatform ?? null };\n}\n\n/** Host-injected renderer pair for the admin-only `marketing_campaign` type.\n * Absent (every non-hub embed) → the type renders nothing (its list URL\n * hits `/api/admin`, unreachable outside the hub anyway). */\nexport interface AdminCampaignCardSlot {\n Card: React.ComponentType<{ campaign: any }>;\n Skeleton: React.ComponentType<{ size?: 'default' | 'sm' }>;\n}\n\n/**\n * Per-type skeleton dispatch — returns the SAME colocated skeleton the\n * resolved card renders, sized to match (zero layout shift on resolve).\n * The chat-side `CHAT_CARD_REGISTRY` already does this via\n * `entry.skeleton()`; this surface exposes the same discipline to the\n * related-content rail.\n */\nfunction renderSkeletonForType(\n type: string,\n size: CardSize,\n adminCampaignCard?: AdminCampaignCardSlot,\n): React.ReactNode {\n // Most card skeletons accept only `{default, sm}`. `'lg'` collapses to\n // `'default'`. ProductReleaseCardSkeleton uses lg/sm pair.\n const legacySize: 'default' | 'sm' = size === 'sm' ? 'sm' : 'default';\n switch (type) {\n case 'blog_post_existing':\n return <BlogCardSkeleton size={legacySize} />;\n case 'case_study':\n return <CaseStudyCardSkeleton size={legacySize} />;\n case 'customer_interview':\n return <CustomerInterviewCardSkeleton size={legacySize} />;\n case 'product_release':\n return <ProductReleaseCardSkeleton size={size === 'sm' ? 'sm' : 'lg'} />;\n case 'podcast':\n case 'webinar':\n case 'event':\n return <ProgramCardSkeleton size={legacySize} />;\n case 'investor_update':\n return <InvestorUpdateCardSkeleton size={legacySize} />;\n case 'onboarding_guide':\n // The rich catalog variant (hero + author grid, clamped description) —\n // the step-numbered 'default' variant is for the guide detail page's\n // \"More in section\" rail, not this full-width row.\n return <OnboardingGuideCardSkeleton size={size === 'sm' ? 'sm' : 'catalog'} />;\n case 'what_i_shipped':\n // Matches the WhatIShippedCard (AdminContentCard 3:2) shape.\n return <WhatIShippedCardSkeleton />;\n case 'marketing_campaign':\n return adminCampaignCard ? <adminCampaignCard.Skeleton size={legacySize} /> : null;\n case 'roadmap_item':\n case 'delivery_item':\n case 'internal_task':\n return <RoadmapCardSkeleton size={legacySize} />;\n default:\n return null;\n }\n}\n\n/**\n * Per-type card dispatch — renders the right card with the right size.\n * Sized cards (`'lg'` / `'default'`) are unique to this rail — the chat\n * dispatcher only renders `'sm'`, so we go directly through the per-type\n * cards here.\n *\n * PURE FUNCTION COMPONENT WITH ZERO HOOK CALLS: the placeholder comes from a\n * plain `extras.buildOgPlaceholderUrl` call (the chat `dispatch.tsx`\n * pattern) and the anchor-prop bundle arrives via the `LinkProvider`\n * render-prop from the parent — so per-card hook legality is owned by the\n * injected provider component, not by this switch.\n *\n * `href` comes from the host's `resolveHref(ref)` (hub: live\n * `buildContentURL` recomposition; default: the ref's stored url).\n */\nfunction CardForType({\n type,\n item,\n size,\n href,\n targetPlatform,\n linkProps,\n extras,\n adminCampaignCard,\n}: {\n type: string;\n item: any;\n size: CardSize;\n href: string;\n targetPlatform: string | null;\n linkProps: CardLinkAnchorProps | null;\n extras?: ChatCardDispatchExtras;\n adminCampaignCard?: AdminCampaignCardSlot;\n}): React.ReactNode {\n // Most card variants accept only `{default, sm}`. `'lg'` collapses to\n // `'default'` for those. ProductReleaseCard uses its own lg/sm pair.\n const legacySize: 'default' | 'sm' = size === 'sm' ? 'sm' : 'default';\n // OG placeholder URL — injected into the pure-presentation cards so they\n // render a branded fallback when the row's featured image is null. Plain\n // function call (NOT a hook). Title is the universal field across all card\n // item shapes used here.\n const placeholderUrl =\n extras?.buildOgPlaceholderUrl?.((item?.title as string | undefined) ?? '') ?? undefined;\n\n // Top-level target/rel for cards that take them as separate props\n // (BlogCard, CaseStudyCard, …). ProductReleaseCard takes the bundle as a\n // single `anchorProps={...}` and uses `linkProps` directly. When the host\n // didn't surface a URL, `linkProps` is null and the card stays in\n // non-anchor mode.\n const anchorAttrs: Pick<CardLinkAnchorProps, 'target' | 'rel'> = linkProps\n ? { target: linkProps.target, rel: linkProps.rel }\n : {};\n\n switch (type) {\n case 'blog_post_existing':\n return <BlogCard post={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'case_study':\n return <CaseStudyCard study={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'customer_interview':\n return <CustomerInterviewCard interview={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'product_release': {\n // Anchor-prop pattern: build product-release lg-variant props from the\n // shared `buildProductReleaseCardProps` so this rail and the /releases\n // catalog page render byte-identically. The card wraps in\n // `<a {...anchorProps}>` ONLY when `anchorProps.href` is set — pass\n // `undefined` (not an empty object) when href is empty so the card\n // stays in non-anchor mode without rendering a dead <a> tag.\n const releaseSize = size === 'sm' ? 'sm' : 'lg';\n const buildReleaseProps = extras?.buildProductReleaseCardProps ?? buildProductReleaseCardProps;\n const releaseProps = buildReleaseProps(item);\n return (\n <ProductReleaseCard\n size={releaseSize}\n title={item.title}\n summary={item.summary}\n version={item.version}\n {...releaseProps}\n anchorProps={linkProps ?? undefined}\n />\n );\n }\n case 'podcast':\n return extras?.programConfigs?.podcast\n ? <ProgramCard config={extras.programConfigs.podcast} item={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />\n : null;\n case 'webinar':\n return extras?.programConfigs?.webinar\n ? <ProgramCard config={extras.programConfigs.webinar} item={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />\n : null;\n case 'event':\n return extras?.programConfigs?.event\n ? <ProgramCard config={extras.programConfigs.event} item={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />\n : null;\n case 'investor_update':\n return <InvestorUpdateCard update={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'onboarding_guide':\n // Catalog variant (see skeleton note) — full-width rich card with a\n // line-clamped description instead of the step-numbered rail card.\n return <OnboardingGuideCard guide={item} size={size === 'sm' ? 'sm' : 'catalog'} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'what_i_shipped':\n // THE single What I Shipped card — same lib component the people-hub\n // dashboard renders, so the card is identical in the rail and the\n // dashboard. `anchorProps` makes the whole card a click-through link\n // (rail is read-only — no owner actions).\n return (\n <WhatIShippedCard\n entry={item}\n placeholderUrl={placeholderUrl}\n // Only pass anchorProps when there's a REAL href — a fallback object\n // with `href: undefined` is still truthy and would make WhatIShippedCard\n // wrap the card in a dead <a> (no URL). Mirrors the ProductReleaseCard\n // `linkProps ?? undefined` pattern above.\n anchorProps={\n linkProps ??\n (href ? ({ href, ...anchorAttrs } as React.AnchorHTMLAttributes<HTMLAnchorElement>) : undefined)\n }\n />\n );\n case 'marketing_campaign':\n return adminCampaignCard ? <adminCampaignCard.Card campaign={item} /> : null;\n case 'roadmap_item':\n case 'delivery_item':\n case 'internal_task':\n return (\n <RoadmapCard\n item={item}\n href={href ?? ''}\n targetPlatform={targetPlatform}\n userVote={null}\n onVote={() => {}}\n size={legacySize}\n cardType={type as 'roadmap_item' | 'delivery_item' | 'internal_task'}\n {...anchorAttrs}\n />\n );\n default:\n return null;\n }\n}\n\n// =============================================================================\n// Fetch all items for a type in ONE server-sorted call, via the injectable\n// list-URL builder. `useSelfFetch` (URL = cache key) replaces the hub's old\n// react-query usage: `enabled` ≙ `url === null`, `!res.ok`/network error ≙\n// `error → items null → group renders nothing`. Accepted deltas vs\n// react-query: no retry/backoff, no focus refetch, no cross-mount cache.\n// =============================================================================\n\nfunction useGroupItems(\n type: string,\n refs: ContentRef[],\n buildUrl: (type: string, ids: string[]) => string | null,\n) {\n const ids = refs.map((r) => r.id);\n const url = ids.length > 0 ? buildUrl(type, ids) : null;\n const { data, isLoading } = useSelfFetch<unknown>(url);\n const items = data != null ? extractItems(data) : null;\n return { items, isLoading };\n}\n\n// =============================================================================\n// Per-group renderer — one API call, server-sorted, then render cards via the\n// dispatcher with per-type skeletons + per-type layout from CONTENT_REF_GROUPS.\n// =============================================================================\n\n/** Map columns prop → tailwind grid class. Only consulted for grid-layout\n * groups; list-layout groups stack vertically. */\nfunction gridClassFor(columns: 2 | 3): string {\n return columns === 3\n ? 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6'\n : 'grid grid-cols-1 sm:grid-cols-2 gap-6';\n}\n\n/** Resolve the group config for a type, falling back to a grid layout with\n * the default card size for unregistered types so the section still renders\n * rather than silently dropping them. The `label` field on the fallback is\n * intentionally a placeholder — the section heading goes through\n * `getContentRefLabelOrTitleCase(type)` instead so cross-surface labels\n * stay consistent between this rail and the investor-email builder. */\nfunction resolveGroupConfig(type: string): ContentRefGroupConfig {\n return CONTENT_REF_GROUPS[type] ?? {\n label: type,\n order: 999,\n layout: 'grid',\n gridSize: 'default',\n };\n}\n\n/** Items per page within one type group. Groups larger than this paginate\n * with the standard Pagination control (NO nested scrolling — a bounded\n * scrollbox inside the page traps wheel events and hides the sections\n * below it). MUST stay at or above the largest suggestion fill\n * (RELATED_SAME_TYPE_COUNT in the hub's lib/constants/suggestions.ts) so\n * current rails never paginate — only genuinely big groups (author pages)\n * do. Exported through the subpath barrel for the hub's module-load\n * assertion of that relation (entity-suggestion-sections.tsx). */\nexport const GROUP_PAGE_SIZE = 12;\n\nfunction ContentGroup({\n type,\n refs,\n columns,\n buildUrl,\n resolveHref,\n LinkProvider,\n extras,\n adminCampaignCard,\n heading,\n}: {\n type: string;\n refs: ContentRef[];\n columns: 2 | 3;\n buildUrl: (type: string, ids: string[]) => string | null;\n resolveHref: (ref: ContentRef) => { href: string | null; targetPlatform: string | null };\n LinkProvider: CardLinkProvider;\n extras?: ChatCardDispatchExtras;\n adminCampaignCard?: AdminCampaignCardSlot;\n /** Group heading, rendered INSIDE the group so a group that resolves to\n * nothing (fetch miss / unsupported type / missing program config) drops\n * its heading too — no orphaned titles. */\n heading: React.ReactNode;\n}) {\n const { items, isLoading } = useGroupItems(type, refs, buildUrl);\n const config = resolveGroupConfig(type);\n const isListLayout = config.layout === 'list';\n const cardSize = config.gridSize;\n\n // Per-group pagination for big groups (author pages): GROUP_PAGE_SIZE items\n // per page with the standard Pagination control below the group. Client-side\n // slicing — useGroupItems already fetched every row in one batched call, so\n // page flips are instant. Hooks live above every early return (file\n // convention). Page is clamped so a shrinking refs array (suggestion\n // refetch) can never strand the view past the last page, and RESET when the\n // ref set actually changes (shrink→grow must not return to a stale page).\n const [page, setPage] = useState(1);\n const refsKey = refs.map((r) => r.id).join('|');\n const prevRefsKeyRef = useRef(refsKey);\n useEffect(() => {\n if (prevRefsKeyRef.current !== refsKey) {\n prevRefsKeyRef.current = refsKey;\n setPage(1);\n }\n }, [refsKey]);\n const totalGroupPages = Math.max(1, Math.ceil(refs.length / GROUP_PAGE_SIZE));\n const safePage = Math.min(page, totalGroupPages);\n const visibleGroupRefs =\n refs.length > GROUP_PAGE_SIZE\n ? refs.slice((safePage - 1) * GROUP_PAGE_SIZE, safePage * GROUP_PAGE_SIZE)\n : refs;\n const groupPagination =\n totalGroupPages > 1 ? (\n <Pagination currentPage={safePage} totalPages={totalGroupPages} onPageChange={setPage} />\n ) : null;\n\n // Skeleton gate: `isLoading && !items` — SSR HTML and the client's first\n // paint render identical skeletons (useSelfFetch starts isLoading=true on\n // both sides), and once items exist they are never replaced by skeletons.\n if (isLoading && !items) {\n const skeletons = visibleGroupRefs.map((r) => (\n <div key={r.id}>{renderSkeletonForType(type, cardSize, adminCampaignCard)}</div>\n ));\n return (\n <div className=\"space-y-4\">\n {heading}\n {isListLayout ? (\n <div className=\"space-y-4\">{skeletons}</div>\n ) : (\n <div className={gridClassFor(columns)}>{skeletons}</div>\n )}\n </div>\n );\n }\n\n if (!items || items.length === 0) return null;\n\n // Index fetched rows by id, then render in REF order — refs carry the\n // intended sequence (suggestion mode: the engine's tier order, so\n // same-platform/tag-matched items lead; controlled mode: the curated\n // display_order). The list APIs return rows date-sorted, which would\n // otherwise scramble that ordering (same-platform items sinking below\n // newer cross-platform ones).\n // Shared extractor (NOT raw `.id`) — some API shapes key items differently\n // (e.g. external_id types); raw access would silently drop valid items.\n const itemById = new Map(\n (items as any[]).map((it) => [extractItemId(type, it) ?? String((it as any)?.id), it]),\n );\n\n const cards = visibleGroupRefs\n .map((contentRef) => {\n const itemId = String(contentRef.id);\n const item = itemById.get(itemId);\n if (!item) return null;\n // Re-compose the URL via the host's resolver (hub: buildContentURL so\n // dev gets localhost and prod the right platform domain; default: the\n // ref's stored url as the API composed it).\n const resolved = resolveHref(contentRef);\n const href = resolved.href ?? '';\n const targetPlatform = resolved.targetPlatform ?? contentRef.targetPlatform ?? null;\n return (\n <div key={itemId}>\n <LinkProvider href={href || null} targetPlatform={targetPlatform}>\n {(linkProps) => (\n <CardForType\n type={type}\n item={item}\n size={cardSize}\n href={href}\n targetPlatform={targetPlatform}\n linkProps={linkProps}\n extras={extras}\n adminCampaignCard={adminCampaignCard}\n />\n )}\n </LinkProvider>\n </div>\n );\n })\n .filter(Boolean);\n\n if (cards.length === 0) {\n // Current PAGE resolved zero cards (rows deleted between the ref fetch\n // and the group fetch, or a stricter list-API gate dropped them). When a\n // pager exists the user must keep the controls to navigate back —\n // dropping the whole group would strand them. A genuinely empty group\n // (no pager) still vanishes with its heading.\n if (groupPagination) {\n return (\n <div className=\"space-y-4\">\n {heading}\n {groupPagination}\n </div>\n );\n }\n return null;\n }\n\n return (\n <div className=\"space-y-4\">\n {heading}\n {isListLayout ? (\n <div className=\"space-y-4\">{cards}</div>\n ) : (\n <div className={gridClassFor(columns)}>{cards}</div>\n )}\n {groupPagination}\n </div>\n );\n}\n\n// =============================================================================\n// Main component\n// =============================================================================\n\ninterface RelatedContentResponse {\n refs: ContentRefWithReason[];\n}\n\nexport interface RelatedContentSectionProps {\n /** CONTROLLED mode (the original behavior). When defined — even `[]` — no\n * suggestion fetch runs and exactly these refs render. */\n contentRefs?: ContentRef[];\n /** SUGGESTION mode (with `entityId`): self-fetch suggestions for this host\n * entity from `{apiBaseUrl}/api/related-content`. Ignored when\n * `contentRefs` is provided. */\n entityType?: string;\n entityId?: number | string;\n /** AUTHOR mode: self-fetch ALL published content authored by this profile\n * from `{apiBaseUrl}/api/related-content?authorId=…` (grouped per type,\n * endless within each group). Ignored when `contentRefs` is provided;\n * takes precedence over the entityType/entityId suggestion scope.\n * SSR-hydrate via `initialItems`, same as suggestion mode. */\n authorId?: string;\n /** Maps to the suggestion API's `count` param — the PER-TYPE fill target\n * for every candidate type EXCEPT the host's own. Absent → param not sent\n * (server default applies). */\n minResults?: number;\n /** Maps to the suggestion API's `sameTypeCount` param — the budget for the\n * candidate type MATCHING the host's own `entityType` (same-type boost:\n * a blog post's rail leads with more blog posts). Absent → param not\n * sent (host's type uses the server's `count`). */\n sameTypeMinResults?: number;\n /** SSR hydrate for suggestion mode — the server page ran the engine and\n * drills the refs here; the first client fetch is skipped (useSelfFetch\n * initialData contract). */\n initialItems?: ContentRefWithReason[];\n /** Section title (default: \"Related Content\") */\n title?: string;\n /**\n * Grid columns at desktop. 2 = denser cards / wider summary (original\n * investor-update layout); 3 = more cards per row for dashboards.\n * Only consulted for grid-layout groups. Default: 2.\n */\n columns?: 2 | 3;\n /**\n * ContentRef.type values to exclude. Honored in ALL modes — controlled\n * mode post-filters (original behavior); suggestion mode ALSO forwards the\n * list verbatim as the API's `excludeTypes=` param so excluded types never\n * consume engine fill slots (`minResults` stays honored). The subtraction\n * happens SERVER-side — this component never mirrors the hub's candidate\n * list.\n */\n excludeTypes?: string[];\n /**\n * SUGGESTION-mode allow-list (rail vocabulary): which content types\n * participate in this rail. Sent verbatim as the API's `types=` param —\n * the SERVER intersects it with its own allowed candidate set, and\n * platform policy gates (e.g. internal-only types) ALWAYS win: the client\n * cannot request its way past them. Absent → all server-side candidates.\n */\n includeTypes?: string[];\n /** Fetch-URL prefix for third-party embeds / reverse proxies\n * ('' = same-origin). Applied to BOTH the suggestion fetch and the\n * default per-group list fetches. */\n apiBaseUrl?: string;\n /** Host injection bundle — REUSES the chat dispatcher's\n * `ChatCardDispatchExtras` (programConfigs, buildOgPlaceholderUrl,\n * buildProductReleaseCardProps override). Program groups render nothing\n * when their config is absent. */\n extras?: ChatCardDispatchExtras;\n /** Hub injects its `buildContentURL` recomposition; default uses the\n * ref's stored `url`/`targetPlatform` as the API composed them. */\n resolveHref?: (ref: ContentRef) => { href: string | null; targetPlatform: string | null };\n /** Hub injects its registry-driven entity-list-api builder; default = the\n * lib's `buildListUrl(type, ids, apiBaseUrl)`. */\n buildListUrl?: (type: string, ids: string[]) => string | null;\n /** Hub injects a `useNavLink`-backed render-prop provider; default = pure\n * anchor via `decideNewTab`. MUST be a module-scope component. */\n LinkProvider?: CardLinkProvider;\n /** Renderer pair for the admin-only `marketing_campaign` type. Absent →\n * the type renders nothing. */\n adminCampaignCard?: AdminCampaignCardSlot;\n /** When true, render the section shell (title + an empty-state line) even\n * with ZERO refs, instead of returning null. Default false (the original\n * behavior — empty rail = no shell). Opt-in per host page (e.g. people-hub's\n * \"What I Shipped\", where the section should always be present). */\n showWhenEmpty?: boolean;\n /** Empty-state copy shown under the title when `showWhenEmpty` and no refs.\n * Default: \"No related content yet.\" */\n emptyStateText?: string;\n /** Custom empty-state node (e.g. a hub `<EmptyState/>`) rendered under the\n * title when `showWhenEmpty` and there are no refs — overrides\n * `emptyStateText`. Lets a host match its canonical empty state. */\n emptyState?: React.ReactNode;\n}\n\nexport function RelatedContentSection({\n contentRefs,\n entityType,\n entityId,\n authorId,\n minResults,\n sameTypeMinResults,\n includeTypes,\n initialItems,\n title = 'Related Content',\n columns = 2,\n excludeTypes,\n apiBaseUrl = '',\n extras,\n resolveHref = defaultResolveHref,\n buildListUrl,\n LinkProvider = DefaultLinkPropsProvider,\n adminCampaignCard,\n showWhenEmpty = false,\n emptyStateText = 'No related content yet.',\n emptyState,\n}: RelatedContentSectionProps) {\n // ── Hooks above EVERY early return (the original `if (!contentRefs.length)\n // return null` guard moved below them). ──\n\n // Suggestion-mode fetch URL — null in controlled mode (contentRefs defined,\n // even []) or when the entity scope is incomplete.\n // `includeTypes: []` is an explicit \"nothing participates\" — skip the fetch\n // entirely (an empty-string `types=` param would be dropped by the URL\n // builder and read server-side as \"all candidates\") AND ignore SSR refs.\n const suggestionsDisabled = includeTypes?.length === 0;\n // Shared type-filter params — one spelling for both fetch modes so a future\n // normalization (trim/dedupe) can't diverge between them.\n const typeFilterParams = {\n types: includeTypes !== undefined ? includeTypes.join(',') : undefined,\n excludeTypes: excludeTypes && excludeTypes.length > 0 ? excludeTypes.join(',') : undefined,\n };\n // AUTHOR mode beats suggestion mode: when `authorId` is set the rail lists\n // everything that profile authored (the server returns ALL, no count).\n const authorUrl =\n contentRefs === undefined && authorId && !suggestionsDisabled\n ? buildSuggestionUrl('/api/related-content', {\n apiBaseUrl,\n extraParams: { authorId, ...typeFilterParams },\n })\n : null;\n const suggestUrl =\n authorUrl ??\n (contentRefs === undefined &&\n entityType &&\n entityId !== undefined &&\n entityId !== null &&\n entityId !== '' &&\n !suggestionsDisabled\n ? buildSuggestionUrl('/api/related-content', {\n apiBaseUrl,\n entityType,\n entityId,\n count: minResults,\n extraParams: {\n sameTypeCount: sameTypeMinResults !== undefined ? String(sameTypeMinResults) : undefined,\n ...typeFilterParams,\n },\n })\n : null);\n // Memoize the initialData wrapper — useSelfFetch re-syncs on [initialData],\n // and a fresh per-render object would loop setState under re-rendering\n // parents (the latent FaqSection bug, fixed there in the same change).\n const initialData = useMemo<RelatedContentResponse | undefined>(\n // An explicitly disabled rail (includeTypes: []) must ignore SSR-hydrated\n // refs too — otherwise useSelfFetch(null, {initialData}) keeps serving\n // initialItems and the \"nothing participates\" contract silently breaks.\n () => (!suggestionsDisabled && initialItems ? { refs: initialItems } : undefined),\n [initialItems, suggestionsDisabled],\n );\n const { data, isLoading } = useSelfFetch<RelatedContentResponse>(suggestUrl, { initialData });\n\n // Default group fetcher: the lib's byte-parity-tested builder, prefixed for\n // embeds. Memoized so group-fetch URLs stay value-stable across renders.\n const effectiveBuildListUrl = useMemo(\n () => buildListUrl ?? ((type: string, ids: string[]) => libBuildListUrl(type, ids, apiBaseUrl)),\n [buildListUrl, apiBaseUrl],\n );\n\n const refs: ContentRef[] = contentRefs ?? data?.refs ?? [];\n\n // Per-consumer type gating — drops refs whose `type` is in the exclude\n // list. In suggestion mode the server already subtracted these (the param\n // is forwarded above); the client filter stays as an idempotent guard and\n // IS the mechanism in controlled mode (original behavior).\n const exclude = new Set(excludeTypes || []);\n const visibleRefs = exclude.size > 0 ? refs.filter((r) => !exclude.has(r.type)) : refs;\n // Zero refs (still loading in suggestion mode, or genuinely empty). Default:\n // no empty shell. Opt-in (`showWhenEmpty`): render the title + an empty-state\n // line so the section is always present (e.g. people-hub \"What I Shipped\").\n if (!visibleRefs.length) {\n if (!showWhenEmpty) return null; // non-showWhenEmpty consumers stay blank (unchanged)\n // Client-fetch loading (author/suggestion mode, no SSR initialItems): render a\n // SKELETON grid — reserves height + matches the rest of the app's loading, so\n // there's no blank-then-pop jump (the prior `return null` collapsed the tab to\n // zero height during the fetch). SSR controlled mode has isLoading=false → it\n // skips straight to the empty state below. Skeleton type = the requested rail\n // type (author mode passes a single `includeTypes`).\n if (isLoading) {\n const skeletonType = includeTypes?.[0] ?? entityType ?? 'blog_post_existing';\n return (\n <div className=\"space-y-8\">\n <h2 className=\"text-2xl font-bold text-ods-text-primary\">{title}</h2>\n <div className={gridClassFor(columns)}>\n {Array.from({ length: 3 }).map((_, i) => (\n <div key={i}>{renderSkeletonForType(skeletonType, 'default', adminCampaignCard)}</div>\n ))}\n </div>\n </div>\n );\n }\n return (\n <div className=\"space-y-8\">\n <h2 className=\"text-2xl font-bold text-ods-text-primary\">{title}</h2>\n {emptyState ?? <p className=\"text-ods-text-secondary\">{emptyStateText}</p>}\n </div>\n );\n }\n\n const grouped: Record<string, ContentRef[]> = {};\n for (const ref of visibleRefs) {\n if (!grouped[ref.type]) grouped[ref.type] = [];\n grouped[ref.type].push(ref);\n }\n\n // Registered types in CONTENT_REF_GROUPS order, then any unregistered\n // types appended (same shape the investor-email builder uses — both\n // consume `orderContentRefTypes` so cross-surface ordering matches).\n // SAME-TYPE FIRST: when a host entityType is known (suggestion / SSR\n // modes), its own content-type group is hoisted to the top — a blog\n // post's rail leads with blog posts. Rail group keys are compared via\n // the shared alias canonicalizer (blog_post_existing ↔ blog_post).\n let orderedTypes = orderContentRefTypes(Object.keys(grouped));\n if (entityType) {\n // Canonicalize BOTH sides — hosts pass registry vocab ('blog_post') but\n // rail-vocab aliases ('blog_post_existing') are also legal inputs; a raw\n // comparison would silently lose the same-type-first hoist for aliases.\n const canonicalEntityType = canonicalContentRefType(entityType);\n const sameType = orderedTypes.filter((t) => canonicalContentRefType(t) === canonicalEntityType);\n if (sameType.length > 0) {\n orderedTypes = [...sameType, ...orderedTypes.filter((t) => canonicalContentRefType(t) !== canonicalEntityType)];\n }\n }\n\n return (\n <div className=\"space-y-8\">\n <h2 className=\"text-2xl font-bold text-ods-text-primary\">{title}</h2>\n {orderedTypes.map((type) => (\n <ContentGroup\n key={type}\n type={type}\n refs={grouped[type]}\n columns={columns}\n buildUrl={effectiveBuildListUrl}\n resolveHref={resolveHref}\n LinkProvider={LinkProvider}\n extras={extras}\n adminCampaignCard={adminCampaignCard}\n heading={\n <h3 className=\"font-['Azeret_Mono'] text-[14px] font-semibold uppercase text-ods-text-secondary tracking-wider\">\n {getContentRefLabelOrTitleCase(type)}\n </h3>\n }\n />\n ))}\n </div>\n );\n}\n"],"mappings":"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","names":["buildListUrl"]}
|
|
1
|
+
{"version":3,"sources":["../src/components/related-content/related-content-section.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * RelatedContentSection\n *\n * Renders content references grouped by type using the canonical card\n * components. MOVED from the hub (`components/shared/related-content-card.tsx`)\n * so any consuming app can embed it; the hub keeps a thin wrapper that\n * pre-binds its host-specific injections (nav hook, URL recomposition,\n * program configs, admin campaign card).\n *\n * THREE data modes (precedence top-down):\n * 1. CONTROLLED — `contentRefs` provided (even `[]`): render exactly those\n * refs, no suggestion fetch (the original investor-update behavior).\n * 2. SUGGESTION — `entityType` + `entityId` provided: self-fetch\n * `GET {apiBaseUrl}/api/related-content?entityType&entityId[&count][&excludeTypes]`\n * (the generic 5-tier engine's second web service). `minResults` maps to\n * `count`; absent → param not sent (server default applies). Each ref\n * carries a `reason` (data-only — never rendered, matching the\n * FaqSection/FaqWithReason precedent).\n * 3. SSR-HYDRATED suggestion — also pass `initialItems` (the server page\n * called the engine directly); the first client fetch is skipped per the\n * `useSelfFetch` initialData contract.\n *\n * Group layout (list vs grid) + card size (lg vs default) come from\n * `CONTENT_REF_GROUPS` in `../../utils/content-ref-groups` — single source of\n * truth, no per-type logic in this file. Skeletons come from\n * `renderSkeletonForType` so the placeholder height matches the loaded card\n * exactly (zero layout shift on resolve).\n *\n * One API call per content type via the shared list-URL builder\n * (`buildListUrl` — injectable; defaults to the lib's byte-parity-tested\n * builder prefixed with `apiBaseUrl`). Fetching uses `useSelfFetch` (plain\n * fetch, NO react-query) so third-party embedders need no QueryClientProvider;\n * cards are imported via DEEP module paths (not the chat barrel) so this\n * chunk never reaches `@tanstack/react-query`.\n *\n * LOCKSTEP NOTE: this file's per-type card/skeleton dispatch is the SIZED\n * sibling of the chat-side `CHAT_CARD_REGISTRY` (`../chat/entity-cards/\n * dispatch.tsx`), which renders compact `size='sm'` cards wired to the chat\n * runtime. Two dispatchers by design — when registering a new fetch-mode\n * content type, add it BOTH there and here (cards + skeleton + list URL).\n */\n\nimport React, { useEffect, useMemo, useRef, useState } from 'react';\nimport {\n CONTENT_REF_GROUPS,\n getContentRefLabelOrTitleCase,\n orderContentRefTypes,\n type ContentRefGroupConfig,\n} from '../../utils/content-ref-groups';\nimport type { ContentRef, ContentRefWithReason } from '../../types/content-ref';\nimport { useSelfFetch } from '../../hooks/use-self-fetch';\nimport { Pagination } from '../pagination';\nimport { extractItems, extractItemId } from '../../utils/extract-items';\nimport { buildListUrl as libBuildListUrl, canonicalContentRefType } from '../../utils/list-url';\nimport { buildSuggestionUrl } from '../../utils/suggestion-url';\nimport { decideNewTab } from '../chat/utils/decide-new-tab';\n// DEEP card imports — NOT the `../chat` barrel (the barrel statically reaches\n// @tanstack/react-query via embeddable-chat + its hooks). Deep paths keep this\n// component's SOURCE graph react-query-free. Note: tsup's shared-chunk\n// splitting may still colocate the cards with chat hooks in one dist chunk\n// (react-query is a required peerDep, so resolution always succeeds) — the\n// guarantee that matters here is the RUNTIME one: nothing on this path ever\n// instantiates a QueryClient, so embedders need NO QueryClientProvider.\nimport { BlogCard, BlogCardSkeleton } from '../chat/entity-cards/blog-card';\nimport { WhatIShippedCard, WhatIShippedCardSkeleton } from '../chat/entity-cards/what-i-shipped-card';\nimport { CaseStudyCard, CaseStudyCardSkeleton } from '../chat/entity-cards/case-study-card';\nimport { CustomerInterviewCard, CustomerInterviewCardSkeleton } from '../chat/entity-cards/customer-interview-card';\nimport { ProductReleaseCard, ProductReleaseCardSkeleton } from '../chat/entity-cards/product-release-card';\nimport { buildProductReleaseCardProps } from '../chat/entity-cards/product-release-card-defaults';\nimport { ProgramCard, ProgramCardSkeleton } from '../chat/entity-cards/program-card';\nimport { InvestorUpdateCard, InvestorUpdateCardSkeleton } from '../chat/entity-cards/investor-update-card';\nimport { OnboardingGuideCard, OnboardingGuideCardSkeleton } from '../chat/entity-cards/onboarding-guide-card';\nimport { RoadmapCard, RoadmapCardSkeleton } from '../chat/entity-cards/roadmap-card';\n// Type-only — erased at build, no runtime dependency on the dispatch module.\nimport type { ChatCardDispatchExtras } from '../chat/entity-cards/dispatch';\n\ntype CardSize = 'lg' | 'default' | 'sm';\n\n/** Anchor prop bundle the per-card link surface receives — same shape the\n * hub's `useNavLink` returns and the chat dispatcher's anchor builders\n * produce. `null` = non-anchor mode (no URL). */\nexport interface CardLinkAnchorProps {\n href: string;\n target?: '_blank';\n rel?: 'noopener noreferrer';\n onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;\n}\n\n/** Render-prop component injection for the navigation decision — keeps hook\n * calls legal (hooks live INSIDE the injected component; `CardForType`\n * itself calls zero hooks). The hub injects a `useNavLink`-backed provider;\n * the default is hook-free (pure `decideNewTab`). MUST be defined at module\n * scope by hosts — an inline arrow would remount every card each render. */\nexport interface CardLinkProviderProps {\n href: string | null;\n targetPlatform: string | null;\n children: (linkProps: CardLinkAnchorProps | null) => React.ReactElement | null;\n}\nexport type CardLinkProvider = React.ComponentType<CardLinkProviderProps>;\n\n/** Default link provider for standalone embeds: relative/-same-origin hrefs\n * stay same-tab, cross-origin pops a new tab (pure `decideNewTab` with no\n * platform context — `currentSource: ''` falls through to the origin\n * check). No router integration, no hooks. */\nfunction DefaultLinkPropsProvider({ href, targetPlatform, children }: CardLinkProviderProps): React.ReactElement | null {\n if (!href) return children(null);\n const newTab = decideNewTab({ href, targetPlatform, currentSource: '' });\n return children(\n newTab\n ? { href, target: '_blank', rel: 'noopener noreferrer' }\n : { href },\n );\n}\n\n/** Default href resolution: trust the ref's stored url/targetPlatform as the\n * API composed them. The hub overrides this with its `buildContentURL`\n * re-composition so dev gets localhost and prod gets platform domains. */\nfunction defaultResolveHref(ref: ContentRef): { href: string | null; targetPlatform: string | null } {\n return { href: ref.url || null, targetPlatform: ref.targetPlatform ?? null };\n}\n\n/** Host-injected renderer pair for the admin-only `marketing_campaign` type.\n * Absent (every non-hub embed) → the type renders nothing (its list URL\n * hits `/api/admin`, unreachable outside the hub anyway). */\nexport interface AdminCampaignCardSlot {\n Card: React.ComponentType<{ campaign: any }>;\n Skeleton: React.ComponentType<{ size?: 'default' | 'sm' }>;\n}\n\n/**\n * Per-type skeleton dispatch — returns the SAME colocated skeleton the\n * resolved card renders, sized to match (zero layout shift on resolve).\n * The chat-side `CHAT_CARD_REGISTRY` already does this via\n * `entry.skeleton()`; this surface exposes the same discipline to the\n * related-content rail.\n */\nfunction renderSkeletonForType(\n type: string,\n size: CardSize,\n adminCampaignCard?: AdminCampaignCardSlot,\n): React.ReactNode {\n // Most card skeletons accept only `{default, sm}`. `'lg'` collapses to\n // `'default'`. ProductReleaseCardSkeleton uses lg/sm pair.\n const legacySize: 'default' | 'sm' = size === 'sm' ? 'sm' : 'default';\n switch (type) {\n case 'blog_post_existing':\n return <BlogCardSkeleton size={legacySize} />;\n case 'case_study':\n return <CaseStudyCardSkeleton size={legacySize} />;\n case 'customer_interview':\n return <CustomerInterviewCardSkeleton size={legacySize} />;\n case 'product_release':\n return <ProductReleaseCardSkeleton size={size === 'sm' ? 'sm' : 'lg'} />;\n case 'podcast':\n case 'webinar':\n case 'event':\n return <ProgramCardSkeleton size={legacySize} />;\n case 'investor_update':\n return <InvestorUpdateCardSkeleton size={legacySize} />;\n case 'onboarding_guide':\n // The rich catalog variant (hero + author grid, clamped description) —\n // the step-numbered 'default' variant is for the guide detail page's\n // \"More in section\" rail, not this full-width row.\n return <OnboardingGuideCardSkeleton size={size === 'sm' ? 'sm' : 'catalog'} />;\n case 'what_i_shipped':\n // Matches the WhatIShippedCard (AdminContentCard 3:2) shape.\n return <WhatIShippedCardSkeleton />;\n case 'marketing_campaign':\n return adminCampaignCard ? <adminCampaignCard.Skeleton size={legacySize} /> : null;\n case 'roadmap_item':\n case 'delivery_item':\n case 'internal_task':\n return <RoadmapCardSkeleton size={legacySize} />;\n default:\n return null;\n }\n}\n\n/**\n * Per-type card dispatch — renders the right card with the right size.\n * Sized cards (`'lg'` / `'default'`) are unique to this rail — the chat\n * dispatcher only renders `'sm'`, so we go directly through the per-type\n * cards here.\n *\n * PURE FUNCTION COMPONENT WITH ZERO HOOK CALLS: the placeholder comes from a\n * plain `extras.buildOgPlaceholderUrl` call (the chat `dispatch.tsx`\n * pattern) and the anchor-prop bundle arrives via the `LinkProvider`\n * render-prop from the parent — so per-card hook legality is owned by the\n * injected provider component, not by this switch.\n *\n * `href` comes from the host's `resolveHref(ref)` (hub: live\n * `buildContentURL` recomposition; default: the ref's stored url).\n */\nfunction CardForType({\n type,\n item,\n size,\n href,\n targetPlatform,\n linkProps,\n extras,\n adminCampaignCard,\n}: {\n type: string;\n item: any;\n size: CardSize;\n href: string;\n targetPlatform: string | null;\n linkProps: CardLinkAnchorProps | null;\n extras?: ChatCardDispatchExtras;\n adminCampaignCard?: AdminCampaignCardSlot;\n}): React.ReactNode {\n // Most card variants accept only `{default, sm}`. `'lg'` collapses to\n // `'default'` for those. ProductReleaseCard uses its own lg/sm pair.\n const legacySize: 'default' | 'sm' = size === 'sm' ? 'sm' : 'default';\n // OG placeholder URL — injected into the pure-presentation cards so they\n // render a branded fallback when the row's featured image is null. Plain\n // function call (NOT a hook). Title is the universal field across all card\n // item shapes used here.\n const placeholderUrl =\n extras?.buildOgPlaceholderUrl?.((item?.title as string | undefined) ?? '') ?? undefined;\n\n // Top-level target/rel for cards that take them as separate props\n // (BlogCard, CaseStudyCard, …). ProductReleaseCard takes the bundle as a\n // single `anchorProps={...}` and uses `linkProps` directly. When the host\n // didn't surface a URL, `linkProps` is null and the card stays in\n // non-anchor mode.\n const anchorAttrs: Pick<CardLinkAnchorProps, 'target' | 'rel'> = linkProps\n ? { target: linkProps.target, rel: linkProps.rel }\n : {};\n\n switch (type) {\n case 'blog_post_existing':\n return <BlogCard post={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'case_study':\n return <CaseStudyCard study={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'customer_interview':\n return <CustomerInterviewCard interview={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'product_release': {\n // Anchor-prop pattern: build product-release lg-variant props from the\n // shared `buildProductReleaseCardProps` so this rail and the /releases\n // catalog page render byte-identically. The card wraps in\n // `<a {...anchorProps}>` ONLY when `anchorProps.href` is set — pass\n // `undefined` (not an empty object) when href is empty so the card\n // stays in non-anchor mode without rendering a dead <a> tag.\n const releaseSize = size === 'sm' ? 'sm' : 'lg';\n const buildReleaseProps = extras?.buildProductReleaseCardProps ?? buildProductReleaseCardProps;\n const releaseProps = buildReleaseProps(item);\n return (\n <ProductReleaseCard\n size={releaseSize}\n title={item.title}\n summary={item.summary}\n version={item.version}\n {...releaseProps}\n anchorProps={linkProps ?? undefined}\n />\n );\n }\n case 'podcast':\n return extras?.programConfigs?.podcast\n ? <ProgramCard config={extras.programConfigs.podcast} item={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />\n : null;\n case 'webinar':\n return extras?.programConfigs?.webinar\n ? <ProgramCard config={extras.programConfigs.webinar} item={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />\n : null;\n case 'event':\n return extras?.programConfigs?.event\n ? <ProgramCard config={extras.programConfigs.event} item={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />\n : null;\n case 'investor_update':\n return <InvestorUpdateCard update={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'onboarding_guide':\n // Catalog variant (see skeleton note) — full-width rich card with a\n // line-clamped description instead of the step-numbered rail card.\n return <OnboardingGuideCard guide={item} size={size === 'sm' ? 'sm' : 'catalog'} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'what_i_shipped':\n // THE single What I Shipped card — same lib component the people-hub\n // dashboard renders, so the card is identical in the rail and the\n // dashboard. `anchorProps` makes the whole card a click-through link\n // (rail is read-only — no owner actions).\n return (\n <WhatIShippedCard\n entry={item}\n placeholderUrl={placeholderUrl}\n // Only pass anchorProps when there's a REAL href — a fallback object\n // with `href: undefined` is still truthy and would make WhatIShippedCard\n // wrap the card in a dead <a> (no URL). Mirrors the ProductReleaseCard\n // `linkProps ?? undefined` pattern above.\n anchorProps={\n linkProps ??\n (href ? ({ href, ...anchorAttrs } as React.AnchorHTMLAttributes<HTMLAnchorElement>) : undefined)\n }\n />\n );\n case 'marketing_campaign':\n return adminCampaignCard ? <adminCampaignCard.Card campaign={item} /> : null;\n case 'roadmap_item':\n case 'delivery_item':\n case 'internal_task':\n return (\n <RoadmapCard\n item={item}\n href={href ?? ''}\n targetPlatform={targetPlatform}\n userVote={null}\n onVote={() => {}}\n size={legacySize}\n cardType={type as 'roadmap_item' | 'delivery_item' | 'internal_task'}\n {...anchorAttrs}\n />\n );\n default:\n return null;\n }\n}\n\n// =============================================================================\n// Fetch all items for a type in ONE server-sorted call, via the injectable\n// list-URL builder. `useSelfFetch` (URL = cache key) replaces the hub's old\n// react-query usage: `enabled` ≙ `url === null`, `!res.ok`/network error ≙\n// `error → items null → group renders nothing`. Accepted deltas vs\n// react-query: no retry/backoff, no focus refetch, no cross-mount cache.\n// =============================================================================\n\nfunction useGroupItems(\n type: string,\n refs: ContentRef[],\n buildUrl: (type: string, ids: string[]) => string | null,\n) {\n const ids = refs.map((r) => r.id);\n const url = ids.length > 0 ? buildUrl(type, ids) : null;\n const { data, isLoading } = useSelfFetch<unknown>(url);\n const items = data != null ? extractItems(data) : null;\n return { items, isLoading };\n}\n\n// =============================================================================\n// Per-group renderer — one API call, server-sorted, then render cards via the\n// dispatcher with per-type skeletons + per-type layout from CONTENT_REF_GROUPS.\n// =============================================================================\n\n/** Map columns prop → tailwind grid class. Only consulted for grid-layout\n * groups; list-layout groups stack vertically. */\nfunction gridClassFor(columns: 2 | 3): string {\n return columns === 3\n ? 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6'\n : 'grid grid-cols-1 sm:grid-cols-2 gap-6';\n}\n\n/** Resolve the group config for a type, falling back to a grid layout with\n * the default card size for unregistered types so the section still renders\n * rather than silently dropping them. The `label` field on the fallback is\n * intentionally a placeholder — the section heading goes through\n * `getContentRefLabelOrTitleCase(type)` instead so cross-surface labels\n * stay consistent between this rail and the investor-email builder. */\nfunction resolveGroupConfig(type: string): ContentRefGroupConfig {\n return CONTENT_REF_GROUPS[type] ?? {\n label: type,\n order: 999,\n layout: 'grid',\n gridSize: 'default',\n };\n}\n\n/** Items per page within one type group. Groups larger than this paginate\n * with the standard Pagination control (NO nested scrolling — a bounded\n * scrollbox inside the page traps wheel events and hides the sections\n * below it). MUST stay at or above the largest suggestion fill\n * (RELATED_SAME_TYPE_COUNT in the hub's lib/constants/suggestions.ts) so\n * current rails never paginate — only genuinely big groups (author pages)\n * do. Exported through the subpath barrel for the hub's module-load\n * assertion of that relation (entity-suggestion-sections.tsx). */\nexport const GROUP_PAGE_SIZE = 12;\n\nfunction ContentGroup({\n type,\n refs,\n columns,\n buildUrl,\n resolveHref,\n LinkProvider,\n extras,\n adminCampaignCard,\n heading,\n}: {\n type: string;\n refs: ContentRef[];\n columns: 2 | 3;\n buildUrl: (type: string, ids: string[]) => string | null;\n resolveHref: (ref: ContentRef) => { href: string | null; targetPlatform: string | null };\n LinkProvider: CardLinkProvider;\n extras?: ChatCardDispatchExtras;\n adminCampaignCard?: AdminCampaignCardSlot;\n /** Group heading, rendered INSIDE the group so a group that resolves to\n * nothing (fetch miss / unsupported type / missing program config) drops\n * its heading too — no orphaned titles. */\n heading: React.ReactNode;\n}) {\n const { items, isLoading } = useGroupItems(type, refs, buildUrl);\n const config = resolveGroupConfig(type);\n const isListLayout = config.layout === 'list';\n const cardSize = config.gridSize;\n\n // Per-group pagination for big groups (author pages): GROUP_PAGE_SIZE items\n // per page with the standard Pagination control below the group. Client-side\n // slicing — useGroupItems already fetched every row in one batched call, so\n // page flips are instant. Hooks live above every early return (file\n // convention). Page is clamped so a shrinking refs array (suggestion\n // refetch) can never strand the view past the last page, and RESET when the\n // ref set actually changes (shrink→grow must not return to a stale page).\n const [page, setPage] = useState(1);\n const refsKey = refs.map((r) => r.id).join('|');\n const prevRefsKeyRef = useRef(refsKey);\n useEffect(() => {\n if (prevRefsKeyRef.current !== refsKey) {\n prevRefsKeyRef.current = refsKey;\n setPage(1);\n }\n }, [refsKey]);\n const totalGroupPages = Math.max(1, Math.ceil(refs.length / GROUP_PAGE_SIZE));\n const safePage = Math.min(page, totalGroupPages);\n const visibleGroupRefs =\n refs.length > GROUP_PAGE_SIZE\n ? refs.slice((safePage - 1) * GROUP_PAGE_SIZE, safePage * GROUP_PAGE_SIZE)\n : refs;\n const groupPagination =\n totalGroupPages > 1 ? (\n <Pagination currentPage={safePage} totalPages={totalGroupPages} onPageChange={setPage} />\n ) : null;\n\n // Skeleton gate: `isLoading && !items` — SSR HTML and the client's first\n // paint render identical skeletons (useSelfFetch starts isLoading=true on\n // both sides), and once items exist they are never replaced by skeletons.\n if (isLoading && !items) {\n const skeletons = visibleGroupRefs.map((r) => (\n <div key={r.id}>{renderSkeletonForType(type, cardSize, adminCampaignCard)}</div>\n ));\n return (\n <div className=\"space-y-4\">\n {heading}\n {isListLayout ? (\n <div className=\"space-y-4\">{skeletons}</div>\n ) : (\n <div className={gridClassFor(columns)}>{skeletons}</div>\n )}\n </div>\n );\n }\n\n if (!items || items.length === 0) return null;\n\n // Index fetched rows by id, then render in REF order — refs carry the\n // intended sequence (suggestion mode: the engine's tier order, so\n // same-platform/tag-matched items lead; controlled mode: the curated\n // display_order). The list APIs return rows date-sorted, which would\n // otherwise scramble that ordering (same-platform items sinking below\n // newer cross-platform ones).\n // Shared extractor (NOT raw `.id`) — some API shapes key items differently\n // (e.g. external_id types); raw access would silently drop valid items.\n const itemById = new Map(\n (items as any[]).map((it) => [extractItemId(type, it) ?? String((it as any)?.id), it]),\n );\n\n const cards = visibleGroupRefs\n .map((contentRef) => {\n const itemId = String(contentRef.id);\n const item = itemById.get(itemId);\n if (!item) return null;\n // Re-compose the URL via the host's resolver (hub: buildContentURL so\n // dev gets localhost and prod the right platform domain; default: the\n // ref's stored url as the API composed it).\n const resolved = resolveHref(contentRef);\n const href = resolved.href ?? '';\n const targetPlatform = resolved.targetPlatform ?? contentRef.targetPlatform ?? null;\n return (\n <div key={itemId}>\n <LinkProvider href={href || null} targetPlatform={targetPlatform}>\n {(linkProps) => (\n <CardForType\n type={type}\n item={item}\n size={cardSize}\n href={href}\n targetPlatform={targetPlatform}\n linkProps={linkProps}\n extras={extras}\n adminCampaignCard={adminCampaignCard}\n />\n )}\n </LinkProvider>\n </div>\n );\n })\n .filter(Boolean);\n\n if (cards.length === 0) {\n // Current PAGE resolved zero cards (rows deleted between the ref fetch\n // and the group fetch, or a stricter list-API gate dropped them). When a\n // pager exists the user must keep the controls to navigate back —\n // dropping the whole group would strand them. A genuinely empty group\n // (no pager) still vanishes with its heading.\n if (groupPagination) {\n return (\n <div className=\"space-y-4\">\n {heading}\n {groupPagination}\n </div>\n );\n }\n return null;\n }\n\n return (\n <div className=\"space-y-4\">\n {heading}\n {isListLayout ? (\n <div className=\"space-y-4\">{cards}</div>\n ) : (\n <div className={gridClassFor(columns)}>{cards}</div>\n )}\n {groupPagination}\n </div>\n );\n}\n\n// =============================================================================\n// Main component\n// =============================================================================\n\ninterface RelatedContentResponse {\n refs: ContentRefWithReason[];\n}\n\nexport interface RelatedContentSectionProps {\n /** CONTROLLED mode (the original behavior). When defined — even `[]` — no\n * suggestion fetch runs and exactly these refs render. */\n contentRefs?: ContentRef[];\n /** SUGGESTION mode (with `entityId`): self-fetch suggestions for this host\n * entity from `{apiBaseUrl}/api/related-content`. Ignored when\n * `contentRefs` is provided. */\n entityType?: string;\n entityId?: number | string;\n /** AUTHOR mode: self-fetch ALL published content authored by this profile\n * from `{apiBaseUrl}/api/related-content?authorId=…` (grouped per type,\n * endless within each group). Ignored when `contentRefs` is provided;\n * takes precedence over the entityType/entityId suggestion scope.\n * SSR-hydrate via `initialItems`, same as suggestion mode. */\n authorId?: string;\n /** Maps to the suggestion API's `count` param — the PER-TYPE fill target\n * for every candidate type EXCEPT the host's own. Absent → param not sent\n * (server default applies). */\n minResults?: number;\n /** Maps to the suggestion API's `sameTypeCount` param — the budget for the\n * candidate type MATCHING the host's own `entityType` (same-type boost:\n * a blog post's rail leads with more blog posts). Absent → param not\n * sent (host's type uses the server's `count`). */\n sameTypeMinResults?: number;\n /** SSR hydrate for suggestion mode — the server page ran the engine and\n * drills the refs here; the first client fetch is skipped (useSelfFetch\n * initialData contract). */\n initialItems?: ContentRefWithReason[];\n /** Section title (default: \"Related Content\") */\n title?: string;\n /**\n * Grid columns at desktop. 2 = denser cards / wider summary (original\n * investor-update layout); 3 = more cards per row for dashboards.\n * Only consulted for grid-layout groups. Default: 2.\n */\n columns?: 2 | 3;\n /**\n * ContentRef.type values to exclude. Honored in ALL modes — controlled\n * mode post-filters (original behavior); suggestion mode ALSO forwards the\n * list verbatim as the API's `excludeTypes=` param so excluded types never\n * consume engine fill slots (`minResults` stays honored). The subtraction\n * happens SERVER-side — this component never mirrors the hub's candidate\n * list.\n */\n excludeTypes?: string[];\n /**\n * SUGGESTION-mode allow-list (rail vocabulary): which content types\n * participate in this rail. Sent verbatim as the API's `types=` param —\n * the SERVER intersects it with its own allowed candidate set, and\n * platform policy gates (e.g. internal-only types) ALWAYS win: the client\n * cannot request its way past them. Absent → all server-side candidates.\n */\n includeTypes?: string[];\n /** Fetch-URL prefix for third-party embeds / reverse proxies\n * ('' = same-origin). Applied to BOTH the suggestion fetch and the\n * default per-group list fetches. */\n apiBaseUrl?: string;\n /** Host injection bundle — REUSES the chat dispatcher's\n * `ChatCardDispatchExtras` (programConfigs, buildOgPlaceholderUrl,\n * buildProductReleaseCardProps override). Program groups render nothing\n * when their config is absent. */\n extras?: ChatCardDispatchExtras;\n /** Hub injects its `buildContentURL` recomposition; default uses the\n * ref's stored `url`/`targetPlatform` as the API composed them. */\n resolveHref?: (ref: ContentRef) => { href: string | null; targetPlatform: string | null };\n /** Hub injects its registry-driven entity-list-api builder; default = the\n * lib's `buildListUrl(type, ids, apiBaseUrl)`. */\n buildListUrl?: (type: string, ids: string[]) => string | null;\n /** Hub injects a `useNavLink`-backed render-prop provider; default = pure\n * anchor via `decideNewTab`. MUST be a module-scope component. */\n LinkProvider?: CardLinkProvider;\n /** Renderer pair for the admin-only `marketing_campaign` type. Absent →\n * the type renders nothing. */\n adminCampaignCard?: AdminCampaignCardSlot;\n /** When true, render the section shell (title + an empty-state line) even\n * with ZERO refs, instead of returning null. Default false (the original\n * behavior — empty rail = no shell). Opt-in per host page (e.g. people-hub's\n * \"What I Shipped\", where the section should always be present). */\n showWhenEmpty?: boolean;\n /** Empty-state copy shown under the title when `showWhenEmpty` and no refs.\n * Default: \"No related content yet.\" */\n emptyStateText?: string;\n /** Custom empty-state node (e.g. a hub `<EmptyState/>`) rendered under the\n * title when `showWhenEmpty` and there are no refs — overrides\n * `emptyStateText`. Lets a host match its canonical empty state. */\n emptyState?: React.ReactNode;\n}\n\nexport function RelatedContentSection({\n contentRefs,\n entityType,\n entityId,\n authorId,\n minResults,\n sameTypeMinResults,\n includeTypes,\n initialItems,\n title = 'Related Content',\n columns = 2,\n excludeTypes,\n apiBaseUrl = '',\n extras,\n resolveHref = defaultResolveHref,\n buildListUrl,\n LinkProvider = DefaultLinkPropsProvider,\n adminCampaignCard,\n showWhenEmpty = false,\n emptyStateText = 'No related content yet.',\n emptyState,\n}: RelatedContentSectionProps) {\n // ── Hooks above EVERY early return (the original `if (!contentRefs.length)\n // return null` guard moved below them). ──\n\n // Suggestion-mode fetch URL — null in controlled mode (contentRefs defined,\n // even []) or when the entity scope is incomplete.\n // `includeTypes: []` is an explicit \"nothing participates\" — skip the fetch\n // entirely (an empty-string `types=` param would be dropped by the URL\n // builder and read server-side as \"all candidates\") AND ignore SSR refs.\n const suggestionsDisabled = includeTypes?.length === 0;\n // Shared type-filter params — one spelling for both fetch modes so a future\n // normalization (trim/dedupe) can't diverge between them.\n const typeFilterParams = {\n types: includeTypes !== undefined ? includeTypes.join(',') : undefined,\n excludeTypes: excludeTypes && excludeTypes.length > 0 ? excludeTypes.join(',') : undefined,\n };\n // AUTHOR mode beats suggestion mode: when `authorId` is set the rail lists\n // everything that profile authored (the server returns ALL, no count).\n const authorUrl =\n contentRefs === undefined && authorId && !suggestionsDisabled\n ? buildSuggestionUrl('/api/related-content', {\n apiBaseUrl,\n extraParams: { authorId, ...typeFilterParams },\n })\n : null;\n const suggestUrl =\n authorUrl ??\n (contentRefs === undefined &&\n entityType &&\n entityId !== undefined &&\n entityId !== null &&\n entityId !== '' &&\n !suggestionsDisabled\n ? buildSuggestionUrl('/api/related-content', {\n apiBaseUrl,\n entityType,\n entityId,\n count: minResults,\n extraParams: {\n sameTypeCount: sameTypeMinResults !== undefined ? String(sameTypeMinResults) : undefined,\n ...typeFilterParams,\n },\n })\n : null);\n // Memoize the initialData wrapper — useSelfFetch re-syncs on [initialData],\n // and a fresh per-render object would loop setState under re-rendering\n // parents (the latent FaqSection bug, fixed there in the same change).\n const initialData = useMemo<RelatedContentResponse | undefined>(\n // An explicitly disabled rail (includeTypes: []) must ignore SSR-hydrated\n // refs too — otherwise useSelfFetch(null, {initialData}) keeps serving\n // initialItems and the \"nothing participates\" contract silently breaks.\n () => (!suggestionsDisabled && initialItems ? { refs: initialItems } : undefined),\n [initialItems, suggestionsDisabled],\n );\n const { data, isLoading } = useSelfFetch<RelatedContentResponse>(suggestUrl, { initialData });\n\n // Default group fetcher: the lib's byte-parity-tested builder, prefixed for\n // embeds. Memoized so group-fetch URLs stay value-stable across renders.\n const effectiveBuildListUrl = useMemo(\n () => buildListUrl ?? ((type: string, ids: string[]) => libBuildListUrl(type, ids, apiBaseUrl)),\n [buildListUrl, apiBaseUrl],\n );\n\n const refs: ContentRef[] = contentRefs ?? data?.refs ?? [];\n\n // Per-consumer type gating — drops refs whose `type` is in the exclude\n // list. In suggestion mode the server already subtracted these (the param\n // is forwarded above); the client filter stays as an idempotent guard and\n // IS the mechanism in controlled mode (original behavior).\n const exclude = new Set(excludeTypes || []);\n const visibleRefs = exclude.size > 0 ? refs.filter((r) => !exclude.has(r.type)) : refs;\n // Zero refs (still loading in suggestion mode, or genuinely empty). Default:\n // no empty shell. Opt-in (`showWhenEmpty`): render the title + an empty-state\n // line so the section is always present (e.g. people-hub \"What I Shipped\").\n if (!visibleRefs.length) {\n if (!showWhenEmpty) return null; // non-showWhenEmpty consumers stay blank (unchanged)\n // Client-fetch loading (author/suggestion mode, no SSR initialItems): render a\n // SKELETON grid — reserves height + matches the rest of the app's loading, so\n // there's no blank-then-pop jump (the prior `return null` collapsed the tab to\n // zero height during the fetch). SSR controlled mode has isLoading=false → it\n // skips straight to the empty state below. Skeleton type = the requested rail\n // type (author mode passes a single `includeTypes`).\n if (isLoading) {\n const skeletonType = includeTypes?.[0] ?? entityType ?? 'blog_post_existing';\n return (\n <div className=\"space-y-8\">\n <h2 className=\"text-2xl font-bold text-ods-text-primary\">{title}</h2>\n <div className={gridClassFor(columns)}>\n {Array.from({ length: 3 }).map((_, i) => (\n <div key={i}>{renderSkeletonForType(skeletonType, 'default', adminCampaignCard)}</div>\n ))}\n </div>\n </div>\n );\n }\n return (\n <div className=\"space-y-8\">\n <h2 className=\"text-2xl font-bold text-ods-text-primary\">{title}</h2>\n {emptyState ?? <p className=\"text-ods-text-secondary\">{emptyStateText}</p>}\n </div>\n );\n }\n\n const grouped: Record<string, ContentRef[]> = {};\n for (const ref of visibleRefs) {\n if (!grouped[ref.type]) grouped[ref.type] = [];\n grouped[ref.type].push(ref);\n }\n\n // Registered types in CONTENT_REF_GROUPS order, then any unregistered\n // types appended (same shape the investor-email builder uses — both\n // consume `orderContentRefTypes` so cross-surface ordering matches).\n // SAME-TYPE FIRST: when a host entityType is known (suggestion / SSR\n // modes), its own content-type group is hoisted to the top — a blog\n // post's rail leads with blog posts. Rail group keys are compared via\n // the shared alias canonicalizer (blog_post_existing ↔ blog_post).\n let orderedTypes = orderContentRefTypes(Object.keys(grouped));\n if (entityType) {\n // Canonicalize BOTH sides — hosts pass registry vocab ('blog_post') but\n // rail-vocab aliases ('blog_post_existing') are also legal inputs; a raw\n // comparison would silently lose the same-type-first hoist for aliases.\n const canonicalEntityType = canonicalContentRefType(entityType);\n const sameType = orderedTypes.filter((t) => canonicalContentRefType(t) === canonicalEntityType);\n if (sameType.length > 0) {\n orderedTypes = [...sameType, ...orderedTypes.filter((t) => canonicalContentRefType(t) !== canonicalEntityType)];\n }\n }\n\n return (\n <div className=\"space-y-8\">\n <h2 className=\"text-2xl font-bold text-ods-text-primary\">{title}</h2>\n {orderedTypes.map((type) => (\n <ContentGroup\n key={type}\n type={type}\n refs={grouped[type]}\n columns={columns}\n buildUrl={effectiveBuildListUrl}\n resolveHref={resolveHref}\n LinkProvider={LinkProvider}\n extras={extras}\n adminCampaignCard={adminCampaignCard}\n heading={\n <h3 className=\"font-['Azeret_Mono'] text-[14px] font-semibold uppercase text-ods-text-secondary tracking-wider\">\n {getContentRefLabelOrTitleCase(type)}\n </h3>\n }\n />\n ))}\n </div>\n );\n}\n"],"mappings":"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","names":["buildListUrl"]}
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }"use client";
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
var
|
|
4
|
+
var _chunkGVSTRN36cjs = require('./chunk-GVSTRN36.cjs');
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
var
|
|
10
|
+
var _chunkDEAWHNYWcjs = require('./chunk-DEAWHNYW.cjs');
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
var _chunkXCPBD2PGcjs = require('./chunk-XCPBD2PG.cjs');
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
var _chunkG7UE6RKVcjs = require('./chunk-G7UE6RKV.cjs');
|
|
14
20
|
|
|
15
21
|
|
|
16
22
|
|
|
@@ -76,7 +82,7 @@ function FaqAccordion({ items, defaultOpenIds = [] }) {
|
|
|
76
82
|
children: [
|
|
77
83
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "min-w-0 pr-4", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "h3", { children: item.question }) }),
|
|
78
84
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "flex-shrink-0", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
79
|
-
|
|
85
|
+
_chunkDEAWHNYWcjs.ChevronButton,
|
|
80
86
|
{
|
|
81
87
|
"aria-label": isOpen ? "Collapse question" : "Expand question",
|
|
82
88
|
size: "md",
|
|
@@ -136,7 +142,7 @@ function buildFaqJsonLdFromFaqs(faqs, opts = {}) {
|
|
|
136
142
|
|
|
137
143
|
var DEFAULT_HEADING_TEXT = "Frequently Asked Questions";
|
|
138
144
|
function buildFaqsUrl(entityType, entityId, minResults, apiBaseUrl = "") {
|
|
139
|
-
return
|
|
145
|
+
return _chunkDEAWHNYWcjs.buildSuggestionUrl.call(void 0, "/api/faqs", { apiBaseUrl, entityType, entityId, count: minResults });
|
|
140
146
|
}
|
|
141
147
|
function sectionSlug(section) {
|
|
142
148
|
return "faq-" + section.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -206,7 +212,7 @@ function GroupedFaqList({
|
|
|
206
212
|
(e, slug) => {
|
|
207
213
|
e.preventDefault();
|
|
208
214
|
setActiveSlug(slug);
|
|
209
|
-
|
|
215
|
+
_chunkDEAWHNYWcjs.scrollElementIntoView.call(void 0, document.getElementById(slug), {
|
|
210
216
|
headerOffset: FAQ_NAV_HEADER_OFFSET
|
|
211
217
|
});
|
|
212
218
|
if (typeof history !== "undefined") history.replaceState(null, "", `#${slug}`);
|
|
@@ -237,7 +243,7 @@ function GroupedFaqList({
|
|
|
237
243
|
id: _nullishCoalesce(group.slug, () => ( void 0)),
|
|
238
244
|
className: "scroll-mt-24 space-y-4",
|
|
239
245
|
children: [
|
|
240
|
-
group.section && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, CategoryHeading, { className:
|
|
246
|
+
group.section && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, CategoryHeading, { className: _chunkXCPBD2PGcjs.SECTION_HEADING_CLASS, children: group.section }),
|
|
241
247
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, FaqAccordion, { items: group.items })
|
|
242
248
|
]
|
|
243
249
|
},
|
|
@@ -270,10 +276,10 @@ function FaqSection({
|
|
|
270
276
|
() => initialFaqs ? { faqs: initialFaqs } : void 0,
|
|
271
277
|
[initialFaqs]
|
|
272
278
|
);
|
|
273
|
-
const { data, isLoading, error } =
|
|
279
|
+
const { data, isLoading, error } = _chunkGVSTRN36cjs.useSelfFetch.call(void 0, url, { initialData });
|
|
274
280
|
const faqs = _nullishCoalesce(_optionalChain([data, 'optionalAccess', _7 => _7.faqs]), () => ( []));
|
|
275
281
|
const groups = _react.useMemo.call(void 0, () => faqs.length > 0 ? groupFaqsBySection(faqs) : [], [faqs]);
|
|
276
|
-
const headingNode = heading === void 0 ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "h2", { className:
|
|
282
|
+
const headingNode = heading === void 0 ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "h2", { className: _chunkXCPBD2PGcjs.SECTION_HEADING_CLASS, children: DEFAULT_HEADING_TEXT }) : heading;
|
|
277
283
|
if (error) return null;
|
|
278
284
|
if (!isLoading && faqs.length === 0) return null;
|
|
279
285
|
if (isLoading && faqs.length === 0) {
|
|
@@ -289,14 +295,44 @@ function FaqSection({
|
|
|
289
295
|
"script",
|
|
290
296
|
{
|
|
291
297
|
type: "application/ld+json",
|
|
292
|
-
dangerouslySetInnerHTML: { __html:
|
|
298
|
+
dangerouslySetInnerHTML: { __html: _chunkDEAWHNYWcjs.serializeJsonLd.call(void 0, schema) }
|
|
293
299
|
}
|
|
294
300
|
)
|
|
295
301
|
] });
|
|
296
302
|
}
|
|
297
303
|
|
|
304
|
+
// src/components/faq/faq-document-page.tsx
|
|
305
|
+
_chunkG7UE6RKVcjs.init_next_navigation.call(void 0, );
|
|
306
|
+
|
|
307
|
+
function FaqDocumentPage({
|
|
308
|
+
title = "FAQs",
|
|
309
|
+
subtitle,
|
|
310
|
+
backButton,
|
|
311
|
+
apiBaseUrl,
|
|
312
|
+
entityType,
|
|
313
|
+
entityId,
|
|
314
|
+
minResults
|
|
315
|
+
}) {
|
|
316
|
+
const router = _chunkG7UE6RKVcjs.useRouter.call(void 0, );
|
|
317
|
+
const backCfg = backButton === false ? void 0 : {
|
|
318
|
+
label: _nullishCoalesce(_optionalChain([backButton, 'optionalAccess', _8 => _8.label]), () => ( "Back to home")),
|
|
319
|
+
onClick: () => router.push(_nullishCoalesce(_optionalChain([backButton, 'optionalAccess', _9 => _9.href]), () => ( "/")))
|
|
320
|
+
};
|
|
321
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkXCPBD2PGcjs.PageShell, { children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkXCPBD2PGcjs.PageLayout, { title, subtitle, backButton: backCfg, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
322
|
+
FaqSection,
|
|
323
|
+
{
|
|
324
|
+
heading: null,
|
|
325
|
+
apiBaseUrl,
|
|
326
|
+
entityType,
|
|
327
|
+
entityId,
|
|
328
|
+
minResults
|
|
329
|
+
}
|
|
330
|
+
) }) });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
298
334
|
|
|
299
335
|
|
|
300
336
|
|
|
301
|
-
exports.FaqAccordion = FaqAccordion; exports.FaqSection = FaqSection;
|
|
302
|
-
//# sourceMappingURL=chunk-
|
|
337
|
+
exports.FaqAccordion = FaqAccordion; exports.FaqSection = FaqSection; exports.FaqDocumentPage = FaqDocumentPage;
|
|
338
|
+
//# sourceMappingURL=chunk-OKDCUJOZ.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-OKDCUJOZ.cjs","../src/components/faq/faq-section.tsx","../src/components/faq-accordion.tsx","../src/components/faq/json-ld.ts","../src/components/faq/faq-document-page.tsx"],"names":["jsx","jsxs"],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-OKDCUJOZ.cjs","sourcesContent":[null,"\"use client\"\n\nimport React, { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { Faq } from '../../types/faq'\nimport { FaqAccordion, type FaqItem } from '../faq-accordion'\nimport { useSelfFetch } from '../../hooks/use-self-fetch'\nimport { buildSuggestionUrl } from '../../utils/suggestion-url'\nimport { serializeJsonLd } from '../../utils/common'\nimport { scrollElementIntoView } from '../../utils/scroll-into-view'\nimport { cn } from '../../utils/cn'\nimport { buildFaqJsonLdFromFaqs, type FaqSchemaOptions } from './json-ld'\nimport { SECTION_HEADING_CLASS } from '../layout/page-heading'\n\nexport interface FaqSectionProps {\n /**\n * SSR hydrate. When provided, the hook skips the first client fetch (per\n * useSelfFetch contract). The consuming server page resolves FAQs then drills\n * them into this prop — the lib never re-fetches what the host already gated on.\n */\n initialFaqs?: Faq[]\n /** Both required together for entity-attached FAQs; partial → bare /api/faqs. */\n entityType?: string\n entityId?: number | string\n /**\n * Heading node above the grouped list. `undefined` → default\n * `<h2>`\"Frequently Asked Questions\". `null` → no heading (the host page\n * owns the `<h1>`, as the standalone /faqs surface does). A React node lets a\n * platform drill its own <PageHeading> without the lib referencing platform\n * state. Also drives category nesting so the document outline stays correct:\n * `null` → categories render `<h2>` (directly under the page `<h1>`);\n * otherwise categories render `<h3>` beneath this heading.\n */\n heading?: React.ReactNode | null\n /** Inject FAQPage schema.org JSON-LD as a <script>. Off by default so embeds\n * don't emit duplicate schema. */\n emitJsonLd?: boolean\n /** Overrides for the JSON-LD's name/description/url. */\n jsonLd?: FaqSchemaOptions\n className?: string\n /** Maps to /api/faqs `?count=` (the 5-tier fill target). Absent → param\n * not sent (server default applies). */\n minResults?: number\n /** Fetch-URL prefix for third-party embeds / reverse proxies\n * ('' = same-origin relative). */\n apiBaseUrl?: string\n}\n\nconst DEFAULT_HEADING_TEXT = 'Frequently Asked Questions'\n\n/** URL composition shared with RelatedContentSection (`buildSuggestionUrl`)\n * — byte-identical to the historical `buildFaqsUrl` output when\n * `minResults`/`apiBaseUrl` are absent. */\nfunction buildFaqsUrl(\n entityType?: string,\n entityId?: number | string,\n minResults?: number,\n apiBaseUrl = '',\n): string {\n return buildSuggestionUrl('/api/faqs', { apiBaseUrl, entityType, entityId, count: minResults })\n}\n\n/** Stable, URL-safe anchor id for a category. Prefixed so it can't collide\n * with other in-page ids, and so a bare numeric/blank section still yields a\n * valid id. */\nfunction sectionSlug(section: string): string {\n return (\n 'faq-' +\n section\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n )\n}\n\ninterface FaqGroup {\n /** null → the uncategorized bucket: no heading, no jump pill, rendered last. */\n section: string | null\n slug: string | null\n items: FaqItem[]\n}\n\n/** Group FAQs by `faq.section`, preserving the server's first-seen\n * (display_order) order for BOTH the section order and the rows within each\n * section. The uncategorized bucket (blank/missing section) sinks to the end\n * since it renders without a heading. Items carry NO badge here — the `<h2>`\n * IS the category, so a per-row chip would be redundant. */\nfunction groupFaqsBySection(faqs: Faq[]): FaqGroup[] {\n const order: string[] = []\n const byName = new Map<string, FaqGroup>()\n let uncategorized: FaqGroup | null = null\n for (const faq of faqs) {\n const item: FaqItem = { id: faq.id, question: faq.question, answer: faq.answer }\n const name = faq.section?.trim()\n if (!name) {\n if (!uncategorized) uncategorized = { section: null, slug: null, items: [] }\n uncategorized.items.push(item)\n continue\n }\n let group = byName.get(name)\n if (!group) {\n group = { section: name, slug: sectionSlug(name), items: [] }\n byName.set(name, group)\n order.push(name)\n }\n group.items.push(item)\n }\n const groups = order.map((name) => byName.get(name)!)\n if (uncategorized) groups.push(uncategorized)\n return groups\n}\n\n/** The standard hub sticky-header height — same offset `useNavLink`'s hash\n * scroll uses, so a category jump lands below the header, not under it. */\nconst FAQ_NAV_HEADER_OFFSET = 96\n\n/**\n * Grouped FAQ layout: a category jump-nav above stacked `<h2>` category\n * sections (each its own accordion). Isolated into its own component so the\n * scroll-spy hooks only mount in grouped mode — `FaqSection`'s own hooks stay\n * unconditional.\n *\n * The pills are real `<a href=\"#slug\">` anchors (crawlable in-page links,\n * deep-linkable, work without JS); the click handler upgrades the jump to the\n * cancellation-proof `scrollElementIntoView` tween and syncs the URL hash.\n */\nfunction GroupedFaqList({\n groups,\n categoryHeadingAs,\n}: {\n groups: FaqGroup[]\n /** Heading tag for each category, so the document outline nests correctly\n * under whatever owns the heading above this block: `h2` on the standalone\n * page (the page owns the `<h1>`), `h3` beneath an embed's `<h2>` title.\n * The VISUAL is `SECTION_HEADING_CLASS` either way, so categories look\n * identical on every surface. */\n categoryHeadingAs: 'h2' | 'h3'\n}) {\n const CategoryHeading = categoryHeadingAs\n const navGroups = useMemo(() => groups.filter((g) => g.slug), [groups])\n const [activeSlug, setActiveSlug] = useState<string | null>(navGroups[0]?.slug ?? null)\n // Identity-stable key for the section set so the observer re-binds only when\n // the categories actually change (not on every parent re-render).\n const slugKey = navGroups.map((g) => g.slug).join('|')\n\n // Scroll-spy: mark the pill for the category currently at the top of the\n // viewport. rootMargin drops the trigger line just below the sticky header\n // and ignores the bottom ~55% so \"active\" is the section being read, not the\n // next one peeking in.\n useEffect(() => {\n if (navGroups.length < 2) return\n const tops = new Map<string, number>()\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n const id = (entry.target as HTMLElement).id\n if (entry.isIntersecting) tops.set(id, entry.boundingClientRect.top)\n else tops.delete(id)\n }\n let bestId: string | null = null\n let bestTop = Number.POSITIVE_INFINITY\n for (const [id, top] of tops) {\n if (top < bestTop) {\n bestTop = top\n bestId = id\n }\n }\n if (bestId) setActiveSlug(bestId)\n },\n { rootMargin: `-${FAQ_NAV_HEADER_OFFSET}px 0px -55% 0px`, threshold: 0 },\n )\n for (const group of navGroups) {\n const el = group.slug ? document.getElementById(group.slug) : null\n if (el) observer.observe(el)\n }\n return () => observer.disconnect()\n // slugKey encodes the section set; re-observe only when it changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [slugKey])\n\n const handleJump = useCallback(\n (e: React.MouseEvent<HTMLAnchorElement>, slug: string) => {\n e.preventDefault()\n setActiveSlug(slug)\n scrollElementIntoView(document.getElementById(slug), {\n headerOffset: FAQ_NAV_HEADER_OFFSET,\n })\n if (typeof history !== 'undefined') history.replaceState(null, '', `#${slug}`)\n },\n [],\n )\n\n return (\n <div className=\"space-y-8\">\n {navGroups.length > 1 && (\n <nav aria-label=\"FAQ categories\" className=\"flex flex-wrap gap-2\">\n {navGroups.map((group) => {\n const isActive = group.slug === activeSlug\n return (\n <a\n key={group.slug}\n href={`#${group.slug}`}\n aria-current={isActive ? 'true' : undefined}\n onClick={(e) => handleJump(e, group.slug as string)}\n className={cn(\n \"rounded-full border px-4 py-2 text-sm font-medium font-['DM_Sans'] transition-colors\",\n isActive\n ? 'border-ods-text-primary bg-ods-card text-ods-text-primary'\n : 'border-ods-border bg-ods-card text-ods-text-secondary hover:border-ods-text-secondary hover:text-ods-text-primary',\n )}\n >\n {group.section}\n </a>\n )\n })}\n </nav>\n )}\n <div className=\"space-y-10\">\n {groups.map((group) => (\n <section\n key={group.slug ?? 'faq-uncategorized'}\n id={group.slug ?? undefined}\n className=\"scroll-mt-24 space-y-4\"\n >\n {group.section && (\n <CategoryHeading className={SECTION_HEADING_CLASS}>{group.section}</CategoryHeading>\n )}\n <FaqAccordion items={group.items} />\n </section>\n ))}\n </div>\n </div>\n )\n}\n\nfunction FaqSkeleton() {\n return (\n <div className=\"space-y-8 animate-pulse\">\n <div className=\"h-12 md:h-14 w-2/3 rounded bg-ods-border\" />\n <div className=\"rounded-3xl border border-ods-border overflow-hidden bg-ods-card divide-y divide-ods-border w-full\">\n {Array.from({ length: 8 }).map((_, idx) => (\n <div key={idx} className=\"flex items-center justify-between px-6 md:px-8 py-6\">\n <div className=\"h-6 w-5/6 rounded bg-ods-border\" />\n <div className=\"h-10 w-10 rounded-md bg-ods-border\" />\n </div>\n ))}\n </div>\n </div>\n )\n}\n\ninterface FaqsResponse {\n faqs: Faq[]\n}\n\n/**\n * The FAQ display surface — ONE rendering on every host: the list grouped by\n * `faq.section` (each category a heading + its own accordion, with a category\n * jump-nav once there are 2+ categories). There is no flat/ungrouped mode and\n * no page-vs-embedded shell fork; the standalone /faqs page and every embed\n * render through this single path, so they cannot drift.\n *\n * - Standalone /faqs page: pass `initialFaqs` (SSR) + `heading={null}` (the\n * page owns the <h1>) + `emitJsonLd` with `jsonLd` overrides for SEO.\n * - Per-entity embed: pass `entityType` + `entityId` (no `initialFaqs`); the\n * hook self-fetches `GET /api/faqs`, and `heading` is this block's own <h2>.\n *\n * CONTRACT: the consuming app MUST implement `GET /api/faqs`. On a fetch error\n * (or zero FAQs) the component renders nothing so the host page isn't\n * disfigured. The host always supplies the page shell — this renders a bare\n * <section>.\n */\nexport function FaqSection({\n initialFaqs,\n entityType,\n entityId,\n heading,\n emitJsonLd = false,\n jsonLd,\n className,\n minResults,\n apiBaseUrl = '',\n}: FaqSectionProps) {\n const url = buildFaqsUrl(entityType, entityId, minResults, apiBaseUrl)\n // Memoized — useSelfFetch re-syncs on [initialData]; a fresh per-render\n // wrapper object would setState-loop under re-rendering parents.\n const initialData = useMemo<FaqsResponse | undefined>(\n () => (initialFaqs ? { faqs: initialFaqs } : undefined),\n [initialFaqs],\n )\n const { data, isLoading, error } = useSelfFetch<FaqsResponse>(url, { initialData })\n\n const faqs = data?.faqs ?? []\n // Grouped before the early returns so the hook order stays stable.\n const groups = useMemo(() => (faqs.length > 0 ? groupFaqsBySection(faqs) : []), [faqs])\n\n // `undefined` → default <h2> title; `null` → the host page owns the <h1>, so\n // no title renders here. `heading === null` also makes the category headings\n // <h2> (directly under the page <h1>); otherwise they nest as <h3>.\n const headingNode =\n heading === undefined ? <h2 className={SECTION_HEADING_CLASS}>{DEFAULT_HEADING_TEXT}</h2> : heading\n\n // Degrade silently — never show an error banner or an empty section shell\n // where FAQs would be (host pages and the standalone surface both rely on it).\n if (error) return null\n if (!isLoading && faqs.length === 0) return null\n if (isLoading && faqs.length === 0) {\n return (\n <div className={className}>\n <FaqSkeleton />\n </div>\n )\n }\n\n const schema = emitJsonLd ? buildFaqJsonLdFromFaqs(faqs, jsonLd) : null\n\n return (\n <>\n <section className={className ?? 'space-y-10'}>\n {headingNode}\n <GroupedFaqList groups={groups} categoryHeadingAs={heading === null ? 'h2' : 'h3'} />\n </section>\n {schema && (\n <script\n type=\"application/ld+json\"\n // eslint-disable-next-line react/no-danger\n // serializeJsonLd, NOT raw JSON.stringify — FAQ answers are\n // admin-entered; an embedded \"</script>\" must not break the tag.\n dangerouslySetInnerHTML={{ __html: serializeJsonLd(schema) }}\n />\n )}\n </>\n )\n}\n","\"use client\"\n\nimport React, { useRef, useState, useEffect, useCallback } from 'react'\nimport { ChevronButton } from './ui/chevron-button'\nimport { cn } from \"../utils/cn\"\n\nexport interface FaqItem {\n id: number | string\n question: string\n answer: string\n}\n\ninterface FaqAccordionProps {\n items: FaqItem[]\n defaultOpenIds?: (number | string)[]\n}\n\n// Utility to measure scrollHeight outside render cycle\nconst useMeasuredHeight = (isOpen: boolean) => {\n const ref = useRef<HTMLDivElement | null>(null)\n const [maxHeight, setMaxHeight] = useState<string>('0px')\n\n const measure = useCallback(() => {\n if (ref.current) {\n const height = ref.current.scrollHeight\n setMaxHeight(`${height}px`)\n }\n }, [])\n\n // Update height only when section is open\n useEffect(() => {\n if (isOpen) {\n measure()\n } else {\n setMaxHeight('0px')\n }\n }, [isOpen, measure])\n\n return { ref, maxHeight }\n}\n\nexport function FaqAccordion({ items, defaultOpenIds = [] }: FaqAccordionProps) {\n const [openSet, setOpenSet] = useState<Set<string | number>>(new Set(defaultOpenIds))\n\n const toggle = (id: string | number) => {\n setOpenSet(prev => {\n const next = new Set(prev)\n if (next.has(id)) next.delete(id)\n else next.add(id)\n return next\n })\n }\n\n return (\n <div className=\"rounded-3xl border border-ods-border divide-y divide-ods-border bg-ods-card overflow-hidden\">\n {items.map(item => {\n const isOpen = openSet.has(item.id)\n const { ref, maxHeight } = useMeasuredHeight(isOpen)\n\n return (\n <div\n key={item.id}\n className={cn('group transition-colors hover:bg-[#1E1E1E]', isOpen ? 'bg-ods-bg' : 'bg-transparent')}\n >\n {/* Header */}\n <div\n role=\"button\"\n tabIndex={0}\n onClick={() => toggle(item.id)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n toggle(item.id);\n }\n }}\n aria-expanded={isOpen}\n className=\"flex w-full items-center justify-between px-6 md:px-8 py-6 text-left focus:outline-none transition-colors cursor-pointer\"\n >\n <div className=\"min-w-0 pr-4\">\n <h3>\n {item.question}\n </h3>\n </div>\n <div className=\"flex-shrink-0\">\n <ChevronButton\n aria-label={isOpen ? 'Collapse question' : 'Expand question'}\n size=\"md\"\n isExpanded={isOpen}\n backgroundColor=\"transparent\"\n borderColor=\"#3A3A3A\"\n />\n </div>\n </div>\n {/* Content wrapper with max-height animation */}\n <div\n style={{ maxHeight, transition: 'max-height 0.35s ease-in-out, opacity 0.35s ease-in-out', opacity: isOpen ? 1 : 0 }}\n className=\"overflow-hidden group-hover:bg-[#1E1E1E]/30\"\n >\n <div ref={ref} className=\"px-6 md:px-8 pb-6 text-ods-text-primary text-h4\">\n {item.answer}\n </div>\n </div>\n </div>\n )\n })}\n </div>\n )\n} ","/**\n * Pure FAQ JSON-LD builder. No React, no client-only deps — safe to import from\n * Server Components via the `./components/faq` subpath export. (Do NOT import\n * through the root `./components` barrel — that barrel is `\"use client\"` and\n * dragging it into a Server Component would force the client graph into the\n * server.)\n *\n * The hub used to harcode `name`/`description` here for OpenMSP; in the lib we\n * accept overrides so every embedder can supply its own platform branding.\n */\nimport type { Faq } from '../../types/faq'\n\nexport interface FaqSchemaOptions {\n name?: string\n description?: string\n url?: string\n}\n\nconst DEFAULT_NAME = 'Frequently Asked Questions'\nconst DEFAULT_DESCRIPTION =\n 'Answers to common questions.'\n\nexport function baseFaqSchema(opts: FaqSchemaOptions = {}) {\n return {\n '@context': 'https://schema.org',\n '@type': 'FAQPage',\n name: opts.name ?? DEFAULT_NAME,\n description: opts.description ?? DEFAULT_DESCRIPTION,\n ...(opts.url ? { url: opts.url } : {}),\n } as const\n}\n\nexport function buildFaqJsonLdFromFaqs(faqs: Faq[], opts: FaqSchemaOptions = {}) {\n return {\n ...baseFaqSchema(opts),\n mainEntity: faqs.map((faq) => ({\n '@type': 'Question',\n name: faq.question,\n acceptedAnswer: {\n '@type': 'Answer',\n text: faq.answer,\n },\n })),\n } as const\n}\n","'use client';\n\n/**\n * FaqDocumentPage — the full `/faqs` page with chrome, so embedders drop in\n * ONE component instead of hand-assembling `PageShell` + `PageLayout` around the\n * bare `<FaqSection>`. Mirrors `LegalDocumentPage` / `DevSectionPage`: the\n * page-level layout lives in the lib, the host passes only config + a back button.\n *\n * `<FaqSection heading={null}>` lets `PageLayout`'s `TitleBlock` own the heading +\n * back button; it self-fetches `${apiBaseUrl}/api/faqs` via the authed\n * `useSelfFetch`, and renders nothing on a fetch error or zero FAQs.\n */\n\nimport { PageShell, PageLayout } from '../ui';\nimport { useRouter } from '../../embed-shims/next-navigation';\nimport { FaqSection } from './faq-section';\n\nexport interface FaqDocumentPageProps {\n /** Page title (PageLayout TitleBlock). Default \"FAQs\". */\n title?: string;\n /** Subtitle under the title. */\n subtitle?: string;\n /** Back-button config — same pattern as `DevSectionPage` / `LegalDocumentPage`.\n * Pass `false` to hide. Default `{ label: 'Back to home', href: '/' }`. */\n backButton?: { label?: string; href?: string } | false;\n /** Base URL `FaqSection` appends `/api/faqs` to (reverse-proxy embedders). */\n apiBaseUrl?: string;\n /** Optional entity scoping forwarded to `FaqSection`. */\n entityType?: string;\n entityId?: number | string;\n /** Minimum FAQ count before the section renders (forwarded to `FaqSection`). */\n minResults?: number;\n}\n\nexport function FaqDocumentPage({\n title = 'FAQs',\n subtitle,\n backButton,\n apiBaseUrl,\n entityType,\n entityId,\n minResults,\n}: FaqDocumentPageProps) {\n const router = useRouter();\n\n // Back-button config — mirrors LegalDocumentPage/DevSectionPage. Hide entirely\n // when the caller passes `false` (embed-mode where the host owns nav chrome).\n const backCfg =\n backButton === false\n ? undefined\n : {\n label: backButton?.label ?? 'Back to home',\n onClick: () => router.push(backButton?.href ?? '/'),\n };\n\n return (\n <PageShell>\n <PageLayout title={title} subtitle={subtitle} backButton={backCfg}>\n <FaqSection\n heading={null}\n apiBaseUrl={apiBaseUrl}\n entityType={entityType}\n entityId={entityId}\n minResults={minResults}\n />\n </PageLayout>\n </PageShell>\n );\n}\n"]}
|
|
@@ -21,4 +21,4 @@ function useRequiredChatRuntime() {
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
exports.ChatRuntimeContext = ChatRuntimeContext; exports.useChatRuntime = useChatRuntime; exports.useRequiredChatRuntime = useRequiredChatRuntime;
|
|
24
|
-
//# sourceMappingURL=chunk-
|
|
24
|
+
//# sourceMappingURL=chunk-PC7YOZ4I.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-PC7YOZ4I.cjs","../src/contexts/chat-runtime-context.tsx"],"names":[],"mappings":"AAAA,qFAAY;AACZ;AACA;ACiCA,8BAA0D;AAqKnD,IAAM,mBAAA,EAAqB,kCAAA,IAAsC,CAAA;AAQjE,SAAS,cAAA,CAAA,EAAqC;AACnD,EAAA,OAAO,+BAAA,kBAA6B,CAAA;AACtC;AAUO,SAAS,sBAAA,CAAA,EAAsC;AACpD,EAAA,MAAM,EAAA,EAAI,+BAAA,kBAA6B,CAAA;AACvC,EAAA,GAAA,CAAI,CAAC,CAAA,EAAG;AACN,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,IAMF,CAAA;AAAA,EACF;AACA,EAAA,OAAO,CAAA;AACT;ADxNA;AACA;AACE;AACA;AACA;AACF,kJAAC","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-PC7YOZ4I.cjs","sourcesContent":[null,"'use client'\n\n/**\n * Chat runtime context — single seam for embedding the chat panel in a\n * different host (e.g. user1.openframe.ai reverse-proxying API calls\n * under /api/mingo-guide/* to hub.openframe.ai/api/*).\n *\n * Three concerns, one context:\n * 1. API endpoints: chatStreamUrl / approvalToolUrl / commandsUrl /\n * buildListUrl + attachment endpoints + chat-identity. The chat\n * reads them from runtime; hub vs embedded app supply different\n * strings via different providers.\n * 2. Navigation mode + callbacks: 'host' or 'embed' mode. Host wires\n * its own router/docNav via the optional `navigate` callback\n * (plain function, NOT a hook); embed forces new-tab via\n * `defaultContentOrigin` + lib's `resolveExternalNavigation`.\n * 3. Identity context: only `source` (required for localStorage\n * namespacing). The display identity (greeting first-name etc.)\n * comes from the server via `useChatIdentity()` — never injected\n * client-side, so it always matches the server-resolved auth.\n *\n * Sibling of EndpointsRuntimeContext (announcement bar, contact form,\n * access codes). Each runtime stays an independent React context so\n * embedders can opt into either feature without forcing the other.\n *\n * IMPORTANT for embedders: memoize the value passed to\n * `<ChatRuntimeContext.Provider value={...}>` (e.g. via React.useMemo).\n * Every change to its reference identity invalidates downstream\n * `useMemo` consumers (the chat input's slash-commands binding,\n * useNavLink's embed-resolution memo, useDocChat's streamFn factory).\n * The hub's `<HubRuntimeProvider>` already memoizes correctly with\n * stable deps. Embedded apps that build the value inline on each render\n * will pay an avoidable re-render cost across the entire chat tree.\n */\n\nimport { createContext, useContext, type ReactNode } from 'react'\n\nimport type { ComposeContentUrl } from '../utils/content-href'\n\n/**\n * Runtime config consumed by the chat panel.\n */\nexport interface ChatRuntime {\n endpoints: {\n /** POST streaming chat. Hub: '/api/docs/chat'. */\n chatStreamUrl: string\n /** POST agent approve/reject. Hub: '/api/chat/agent/confirm-tool'. */\n approvalToolUrl: string\n /** Customer-ticket agent endpoints (Help Center). OPTIONAL — when unset,\n * the ticket hooks fall back to the bare hub paths\n * (`/api/chat/agent/{find-ticket,ticket-action,list-engagements}`).\n * Embedders behind a reverse proxy set these to their proxied paths\n * (e.g. `/content/api/chat/agent/...`) so tickets route through the SAME\n * endpoint config + proxy as every other endpoint. */\n findTicketUrl?: string\n ticketActionUrl?: string\n listEngagementsUrl?: string\n /** GET slash-command catalog. Hub: '/api/docs/commands'. */\n commandsUrl: string\n /** Build entity-card list URL for a content type + ids. Hub delegates\n * to the rag-table-config registry; embedded app provides its own\n * per-type URL builder against the reverse proxy. Returns null when\n * the type has no list endpoint (caller skips rendering). */\n buildListUrl: (type: string, ids: string[]) => string | null\n /** Chat-attachment endpoints — added for the v2 attachment feature.\n *\n * Three concerns:\n * - `attachmentUploadUrl` — POSTed by the chat-attachment hook\n * to mint a Supabase signed-upload-URL + HMAC view token.\n * - `attachmentViewUrlPrefix` — embedded in markdown URLs the\n * chat hosts in user message bubbles (`![]()` / `[Attached]`).\n * Stored in chat history; chosen at SEND time. In host mode the\n * relative `/api/storage/view/chat-attachments/` is sufficient\n * (same-origin); embedders supply an absolute hub URL so the\n * browser can fetch cross-origin.\n * - `identityUrl` — GET endpoint the `useChatIdentity` hook\n * hits to learn the `{authTier, source, attachmentsEnabled}`\n * capability bag for the current session. Used beyond chat\n * (tickets / contact form / any embedded surface that needs\n * to identify the proxied customer), so the name has no\n * \"chat\" prefix even though the consuming hook still does. */\n attachmentUploadUrl: string\n attachmentViewUrlPrefix: string\n identityUrl: string\n /** Optional URL prefix for the image proxy (`<prefix>?url=<external>`).\n * When unset, lib's `getProxiedImageUrl` returns the original URL\n * unchanged. Hub default: '/api/image-proxy'. Embedders that don't\n * host an image-proxy route leave this undefined → images load\n * directly cross-origin (CORS-permitting). */\n imageProxyUrlPrefix?: string\n /** Optional list of hostnames that should bypass the image proxy\n * (rendered direct). Hub uses ['openmsp.ai']; embedders typically\n * leave it unset. Matches the `skipDomains` parameter of\n * `getProxiedImageUrl`. */\n imageProxySkipDomains?: string[]\n /** Supabase storage origin (e.g. `https://xyz.supabase.co`) — used\n * by `useVideoWarmup` to scope the `<link rel=\"preload\" as=\"video\">`\n * hint to MP4s the deployment actually hosts. Hub wires it via\n * `getSupabaseStorageOrigin()`; embedders without a Supabase\n * storage origin leave it unset (preload is then skipped; Mux/\n * YouTube preconnect still fires). */\n supabaseStorageOrigin?: string\n }\n navigation: {\n /** ONE knob, two behaviors:\n * - 'host' = use the host page's existing click-routing untouched.\n * The chat panel calls `navigate?.()` for in-app routing.\n * - 'embed' = guest inside another app: short-circuit at the top\n * of click handlers to force new-tab + absolutize via\n * resolveExternalNavigation. */\n mode: 'host' | 'embed'\n /** Embed-only fallback origin for relative URLs whose target platform\n * can't be inferred. Used by resolveExternalNavigation when\n * `targetPlatform` is null — without this, a relative `/foo` href would\n * window.open against the embedder's origin, which is WRONG.\n * Set to your content host (e.g. 'https://hub.openframe.ai').\n * Required by the embedded app whenever mode='embed'. */\n defaultContentOrigin?: string\n /** Override for opening external URLs. MUST BE SYNCHRONOUS —\n * Safari/Firefox block popups opened outside a direct user gesture.\n * Default: window.open(href, '_blank', 'noopener,noreferrer'). */\n openExternal?: (href: string) => void\n /** Optional in-app navigation callback (host-mode only).\n * Returns `true` if the host handled the click in-app\n * (router.push + docNav.navigate); returns `false`, `undefined`,\n * or `void` → lib falls back to window.location.assign(href).\n * Hub wires this via HubRuntimeProvider's HubNavigationWiring;\n * embedders not in Next.js leave it undefined. */\n navigate?: (input: { href: string; path?: string | null; targetPlatform?: string | null }) => boolean | void\n /** Optional new-tab decision callback. Returns true → lib opens in\n * new tab; false → same tab via `navigate`. Hub wires the existing\n * `decideNewTab` logic from use-nav-link.tsx (re-imports the pure\n * helper from lib). Embedders may omit; lib defaults to:\n * same-origin/same-platform → same tab, else new tab. */\n decideNewTab?: (args: { href: string; targetPlatform?: string | null }) => boolean\n }\n /** Optional OG placeholder URL builder. Returns a branded\n * `/api/og-placeholder?...` URL for the given title. Hub wires this\n * to its `buildOgPlaceholderUrl` (resolves CSS-var ODS colors to\n * hex via the static map). Embedders can wire any equivalent that\n * hits their own placeholder route — or omit, in which case entity\n * cards fall back to no placeholder.\n *\n * Pure synchronous function — NOT a hook. Callers wrap with\n * `useMemo`/`useOgPlaceholder` for memoization. */\n resolvePlaceholderUrl?: (\n title: string,\n options?: { site?: string; aspect?: 'wide' | 'square' },\n ) => string\n /** Optional content-URL composer. Returns the platform-aware href +\n * target-platform tuple for a content entity. Hub wires this to its\n * `buildContentURL(type, slug, extractPrimaryPlatform(platforms))`\n * pipeline so the lib catalog/detail views can derive cross-\n * platform hrefs without knowing the hub's platform topology\n * (openmsp.ai / openframe.app / flamingo.run / tmcg).\n *\n * THE single content-href authority for every embeddable surface — page\n * views (onboarding catalog/detail, releases) AND chat cards / chips /\n * search results all resolve content links through this one seam, so a\n * given type lands in the SAME place regardless of where it's rendered.\n * Embedders wire `makeComposeContentUrl({ hostedTypes, contentOrigin })`;\n * omit it and lib views fall back to a same-origin relative path\n * (`buildDefaultHref`).\n *\n * Takes a single `ComposeContentUrlInput`: `type` + `identifier` (page\n * views pass the slug; chat rows pass the id + `externalUrl`, whose path\n * yields the slug for in-app routing) + optional `platforms` /\n * `externalUrl` / `targetPlatform`. */\n composeContentUrl?: ComposeContentUrl\n /** Per-`documentType` doc-viewer targets — the UNIFIED, DYNAMIC replacement for\n * the single `chipBasePlatform` prop. Maps a doc-table documentType\n * (`'markdown'`, `'data_room_doc'`, …) → `{ platform, basePath }` for the PUBLIC\n * doc viewer that hosts it. Doc chips with no `externalUrl` resolve PER ROW to\n * `getBaseUrl(platform)/<basePath>/<path>`, so a chat mixing several doc sources\n * sends EACH to its own home (markdown→flamingo/knowledge-base,\n * data_room_doc→company-hub/data-room) instead of one static fallback. The hub\n * may keep using `chipBasePlatform` (one doc source per platform); embedders that\n * surface multiple doc sources wire this. Threaded into `resolveSourceRowCTA`. */\n docPlatformTargets?: Record<string, { platform: string; basePath: string }>\n /** Chat source / platform identifier — OPTIONAL. The hub sets it from\n * `currentPlatform()`; EMBEDDERS leave it unset and stay platform-agnostic.\n *\n * It is NOT required for chat to work. The wire resolves source server-side\n * (`/docs/chat|search|commands` reject any client `source`); the\n * same-tab-vs-new-tab link decision falls back to an origin comparison when\n * it's absent (`decideNewTab` → `isCrossOriginUrl`); and the localStorage\n * history namespace falls back to a stable constant. Set it only where the\n * client legitimately needs to know its platform a priori — i.e. the hub,\n * where several platforms share related origins so \"same platform\" can't be\n * inferred from a URL alone. */\n source?: string\n // NOTE: No `user` field. The chat's display identity (greeting\n // first-name, etc.) comes from the SERVER-resolved auth via\n // `useChatIdentity()` — the same identity the server uses to\n // authorize requests. Letting embedders pass a client-side `user`\n // would let it desync from the actual auth tier, causing greetings\n // like \"Hey Bob\" while the server treats the session as\n // alice@example.com. Single source of truth: the server.\n}\n\nexport const ChatRuntimeContext = createContext<ChatRuntime | null>(null)\n\n/**\n * Returns the active runtime, or null when no provider is mounted.\n * NULL is a first-class value — it signals \"no chat runtime configured.\"\n * Optional consumers fall back to no-op behavior; strict consumers\n * use `useRequiredChatRuntime` (below).\n */\nexport function useChatRuntime(): ChatRuntime | null {\n return useContext(ChatRuntimeContext)\n}\n\n/**\n * Strict variant used INSIDE the chat panel. Throws if no provider.\n * The hub guarantees one exists by mounting `<HubRuntimeProvider>` at\n * root; the embedded app mounts its own `<ChatRuntimeContext.Provider>`\n * at the tree root. In Jest / Storybook tests that render chat\n * internals directly, wrap with `<HubRuntimeProvider>` (hub defaults)\n * or supply `<ChatRuntimeContext.Provider value={mockedRuntime}>`.\n */\nexport function useRequiredChatRuntime(): ChatRuntime {\n const v = useContext(ChatRuntimeContext)\n if (!v) {\n throw new Error(\n '[chat-runtime] hook called outside a <ChatRuntimeContext.Provider>. ' +\n 'The hub mounts <HubRuntimeProvider> at root — this only fires when ' +\n 'chat internals are rendered above the provider tree. ' +\n 'Fix: ensure the rendering subtree descends from the runtime provider. ' +\n 'In tests/Storybook: wrap with <HubRuntimeProvider> or supply ' +\n 'a <ChatRuntimeContext.Provider value={mockedRuntime}>.',\n )\n }\n return v\n}\n"]}
|