@flamingo-stack/openframe-frontend-core 0.0.239 → 0.0.241

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.
Files changed (131) hide show
  1. package/dist/{chunk-PN64LWNT.js → chunk-2GMHLKEO.js} +8 -8
  2. package/dist/{chunk-36G2QXYZ.cjs → chunk-3TDLFCCS.cjs} +37 -23
  3. package/dist/chunk-3TDLFCCS.cjs.map +1 -0
  4. package/dist/{chunk-ORJREQ2W.js → chunk-4XLJWX2N.js} +4 -4
  5. package/dist/chunk-5KAQW6Q3.js +285 -0
  6. package/dist/chunk-5KAQW6Q3.js.map +1 -0
  7. package/dist/{chunk-7LLS3TWN.js → chunk-6IFN7F3C.js} +5 -5
  8. package/dist/{chunk-5VU66BD2.cjs → chunk-EFT34DLB.cjs} +498 -461
  9. package/dist/chunk-EFT34DLB.cjs.map +1 -0
  10. package/dist/{chunk-Y6463TD6.js → chunk-FEZSQQTH.js} +2 -2
  11. package/dist/{chunk-IJOJWBME.cjs → chunk-GRD7M6IZ.cjs} +27 -27
  12. package/dist/{chunk-IJOJWBME.cjs.map → chunk-GRD7M6IZ.cjs.map} +1 -1
  13. package/dist/{chunk-ELMYRGWG.cjs → chunk-JEPYFYQP.cjs} +12 -12
  14. package/dist/{chunk-ELMYRGWG.cjs.map → chunk-JEPYFYQP.cjs.map} +1 -1
  15. package/dist/{chunk-Y7DCQUNL.js → chunk-KOHE4F6Q.js} +91 -54
  16. package/dist/chunk-KOHE4F6Q.js.map +1 -0
  17. package/dist/{chunk-JDQY7JCV.js → chunk-MKVKVJQQ.js} +36 -22
  18. package/dist/chunk-MKVKVJQQ.js.map +1 -0
  19. package/dist/{chunk-MTROCWO4.cjs → chunk-QJULZ4PB.cjs} +7 -7
  20. package/dist/{chunk-MTROCWO4.cjs.map → chunk-QJULZ4PB.cjs.map} +1 -1
  21. package/dist/{chunk-FSS2MIEA.cjs → chunk-SGJBUL2Y.cjs} +20 -3
  22. package/dist/chunk-SGJBUL2Y.cjs.map +1 -0
  23. package/dist/{chunk-SZ3BBI6H.cjs → chunk-SWXWKVCC.cjs} +12 -12
  24. package/dist/{chunk-SZ3BBI6H.cjs.map → chunk-SWXWKVCC.cjs.map} +1 -1
  25. package/dist/{chunk-LJGBFD52.js → chunk-VAUI3CND.js} +18 -1
  26. package/dist/chunk-VAUI3CND.js.map +1 -0
  27. package/dist/{chunk-OB45JHDY.cjs → chunk-VFKQMAUF.cjs} +3 -3
  28. package/dist/{chunk-OB45JHDY.cjs.map → chunk-VFKQMAUF.cjs.map} +1 -1
  29. package/dist/chunk-WKZS5HWH.cjs +285 -0
  30. package/dist/chunk-WKZS5HWH.cjs.map +1 -0
  31. package/dist/{chunk-G6YZ7O27.js → chunk-Z3657YGL.js} +6 -6
  32. package/dist/components/chat/hooks/use-chat-card-item.d.ts.map +1 -1
  33. package/dist/components/chat/index.cjs +6 -6
  34. package/dist/components/chat/index.js +5 -5
  35. package/dist/components/chat/types/entities/content-ref.d.ts +5 -20
  36. package/dist/components/chat/types/entities/content-ref.d.ts.map +1 -1
  37. package/dist/components/chevron-button.d.ts +3 -1
  38. package/dist/components/chevron-button.d.ts.map +1 -1
  39. package/dist/components/contact/index.cjs +7 -7
  40. package/dist/components/contact/index.js +6 -6
  41. package/dist/components/faq/faq-section.d.ts +8 -2
  42. package/dist/components/faq/faq-section.d.ts.map +1 -1
  43. package/dist/components/faq/index.cjs +3 -3
  44. package/dist/components/faq/index.js +2 -2
  45. package/dist/components/faq/json-ld.cjs +1 -1
  46. package/dist/components/faq/json-ld.cjs.map +1 -1
  47. package/dist/components/faq/{faq-json-ld.d.ts → json-ld.d.ts} +1 -1
  48. package/dist/components/faq/json-ld.d.ts.map +1 -0
  49. package/dist/components/faq/json-ld.js +1 -1
  50. package/dist/components/faq/json-ld.js.map +1 -1
  51. package/dist/components/faq-accordion.d.ts.map +1 -1
  52. package/dist/components/features/index.cjs +6 -6
  53. package/dist/components/features/index.js +5 -5
  54. package/dist/components/index.cjs +89 -85
  55. package/dist/components/index.cjs.map +1 -1
  56. package/dist/components/index.d.ts +1 -0
  57. package/dist/components/index.d.ts.map +1 -1
  58. package/dist/components/index.js +31 -27
  59. package/dist/components/index.js.map +1 -1
  60. package/dist/components/navigation/index.cjs +6 -6
  61. package/dist/components/navigation/index.js +5 -5
  62. package/dist/components/onboarding-guides/index.cjs +30 -30
  63. package/dist/components/onboarding-guides/index.cjs.map +1 -1
  64. package/dist/components/onboarding-guides/index.js +11 -11
  65. package/dist/components/related-content/index.cjs +26 -0
  66. package/dist/components/related-content/index.cjs.map +1 -0
  67. package/dist/components/related-content/index.d.ts +2 -0
  68. package/dist/components/related-content/index.d.ts.map +1 -0
  69. package/dist/components/related-content/index.js +26 -0
  70. package/dist/components/related-content/index.js.map +1 -0
  71. package/dist/components/related-content/related-content-section.d.ts +141 -0
  72. package/dist/components/related-content/related-content-section.d.ts.map +1 -0
  73. package/dist/components/tickets/index.cjs +73 -73
  74. package/dist/components/tickets/index.cjs.map +1 -1
  75. package/dist/components/tickets/index.js +15 -15
  76. package/dist/components/ui/index.cjs +11 -11
  77. package/dist/components/ui/index.cjs.map +1 -1
  78. package/dist/components/ui/index.js +13 -13
  79. package/dist/embed-shims/index.cjs +3 -3
  80. package/dist/embed-shims/index.cjs.map +1 -1
  81. package/dist/embed-shims/index.js +5 -5
  82. package/dist/index.cjs +27 -11
  83. package/dist/index.cjs.map +1 -1
  84. package/dist/index.js +55 -39
  85. package/dist/types/content-ref.d.ts +35 -0
  86. package/dist/types/content-ref.d.ts.map +1 -0
  87. package/dist/types/index.d.ts +1 -0
  88. package/dist/types/index.d.ts.map +1 -1
  89. package/dist/utils/content-ref-groups.d.ts +58 -0
  90. package/dist/utils/content-ref-groups.d.ts.map +1 -0
  91. package/dist/utils/extract-items.d.ts +18 -0
  92. package/dist/utils/extract-items.d.ts.map +1 -0
  93. package/dist/utils/index.cjs +81 -0
  94. package/dist/utils/index.cjs.map +1 -1
  95. package/dist/utils/index.d.ts +4 -1
  96. package/dist/utils/index.d.ts.map +1 -1
  97. package/dist/utils/index.js +74 -1
  98. package/dist/utils/index.js.map +1 -1
  99. package/dist/utils/list-url.d.ts +6 -0
  100. package/dist/utils/list-url.d.ts.map +1 -1
  101. package/dist/utils/suggestion-url.d.ts +26 -0
  102. package/dist/utils/suggestion-url.d.ts.map +1 -0
  103. package/package.json +7 -1
  104. package/src/components/chat/hooks/use-chat-card-item.ts +3 -38
  105. package/src/components/chat/types/entities/content-ref.ts +5 -21
  106. package/src/components/chevron-button.tsx +5 -1
  107. package/src/components/faq/faq-section.tsx +71 -26
  108. package/src/components/faq-accordion.tsx +2 -1
  109. package/src/components/index.ts +1 -0
  110. package/src/components/related-content/index.ts +9 -0
  111. package/src/components/related-content/related-content-section.tsx +603 -0
  112. package/src/types/content-ref.ts +34 -0
  113. package/src/types/index.ts +1 -0
  114. package/src/utils/content-ref-groups.ts +89 -0
  115. package/src/utils/extract-items.ts +48 -0
  116. package/src/utils/index.ts +16 -1
  117. package/src/utils/list-url.ts +9 -0
  118. package/src/utils/suggestion-url.ts +39 -0
  119. package/dist/chunk-36G2QXYZ.cjs.map +0 -1
  120. package/dist/chunk-5VU66BD2.cjs.map +0 -1
  121. package/dist/chunk-FSS2MIEA.cjs.map +0 -1
  122. package/dist/chunk-JDQY7JCV.js.map +0 -1
  123. package/dist/chunk-LJGBFD52.js.map +0 -1
  124. package/dist/chunk-Y7DCQUNL.js.map +0 -1
  125. package/dist/components/faq/faq-json-ld.d.ts.map +0 -1
  126. /package/dist/{chunk-PN64LWNT.js.map → chunk-2GMHLKEO.js.map} +0 -0
  127. /package/dist/{chunk-ORJREQ2W.js.map → chunk-4XLJWX2N.js.map} +0 -0
  128. /package/dist/{chunk-7LLS3TWN.js.map → chunk-6IFN7F3C.js.map} +0 -0
  129. /package/dist/{chunk-Y6463TD6.js.map → chunk-FEZSQQTH.js.map} +0 -0
  130. /package/dist/{chunk-G6YZ7O27.js.map → chunk-Z3657YGL.js.map} +0 -0
  131. /package/src/components/faq/{faq-json-ld.ts → json-ld.ts} +0 -0
@@ -6,7 +6,13 @@ import {
6
6
  TASK_TYPE_TEXT_COLORS,
7
7
  getStatusColorScheme,
8
8
  init_pagination
9
- } from "./chunk-Y7DCQUNL.js";
9
+ } from "./chunk-KOHE4F6Q.js";
10
+ import {
11
+ init_next_navigation,
12
+ usePathname,
13
+ useRouter,
14
+ useSearchParams
15
+ } from "./chunk-PLJLE4A4.js";
10
16
  import {
11
17
  Button,
12
18
  init_button2 as init_button
@@ -15,12 +21,6 @@ import {
15
21
  init_next_link,
16
22
  next_link_default
17
23
  } from "./chunk-OHPI2HRK.js";
18
- import {
19
- init_next_navigation,
20
- usePathname,
21
- useRouter,
22
- useSearchParams
23
- } from "./chunk-PLJLE4A4.js";
24
24
  import {
25
25
  cn,
26
26
  init_cn
@@ -299,4 +299,4 @@ export {
299
299
  init_unified_pagination,
300
300
  DeliveryRow
301
301
  };
302
- //# sourceMappingURL=chunk-PN64LWNT.js.map
302
+ //# sourceMappingURL=chunk-2GMHLKEO.js.map
@@ -4,14 +4,18 @@
4
4
  var _chunkT7AJBKZPcjs = require('./chunk-T7AJBKZP.cjs');
5
5
 
6
6
 
7
- var _chunkFSS2MIEAcjs = require('./chunk-FSS2MIEA.cjs');
7
+
8
+ var _chunkSGJBUL2Ycjs = require('./chunk-SGJBUL2Y.cjs');
8
9
 
9
10
 
10
11
 
11
12
  var _chunkFIG2RKZFcjs = require('./chunk-FIG2RKZF.cjs');
12
13
 
13
- // src/components/faq-accordion.tsx
14
+ // src/components/faq/faq-section.tsx
14
15
  var _react = require('react');
16
+
17
+ // src/components/faq-accordion.tsx
18
+
15
19
  _chunkFIG2RKZFcjs.init_cn.call(void 0, );
16
20
  var _jsxruntime = require('react/jsx-runtime');
17
21
  var useMeasuredHeight = (isOpen) => {
@@ -66,9 +70,10 @@ function FaqAccordion({ items, defaultOpenIds = [] }) {
66
70
  className: "flex w-full items-center justify-between px-6 md:px-8 py-6 text-left focus:outline-none transition-colors cursor-pointer",
67
71
  children: [
68
72
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "h3", { children: item.question }),
69
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "flex-shrink-0", "aria-label": isOpen ? "Collapse question" : "Expand question", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
70
- _chunkFSS2MIEAcjs.ChevronButton,
73
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "flex-shrink-0", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
74
+ _chunkSGJBUL2Ycjs.ChevronButton,
71
75
  {
76
+ "aria-label": isOpen ? "Collapse question" : "Expand question",
72
77
  size: "md",
73
78
  isExpanded: isOpen,
74
79
  backgroundColor: "transparent",
@@ -93,7 +98,7 @@ function FaqAccordion({ items, defaultOpenIds = [] }) {
93
98
  }) });
94
99
  }
95
100
 
96
- // src/components/faq/faq-json-ld.ts
101
+ // src/components/faq/json-ld.ts
97
102
  var DEFAULT_NAME = "Frequently Asked Questions";
98
103
  var DEFAULT_DESCRIPTION = "Answers to common questions.";
99
104
  function baseFaqSchema(opts = {}) {
@@ -122,12 +127,8 @@ function buildFaqJsonLdFromFaqs(faqs, opts = {}) {
122
127
  // src/components/faq/faq-section.tsx
123
128
 
124
129
  var DEFAULT_HEADING_TEXT = "Frequently Asked Questions";
125
- function buildFaqsUrl(entityType, entityId) {
126
- if (!entityType || entityId === void 0 || entityId === null || entityId === "") {
127
- return "/api/faqs";
128
- }
129
- const qs = new URLSearchParams({ entityType, entityId: String(entityId) });
130
- return `/api/faqs?${qs.toString()}`;
130
+ function buildFaqsUrl(entityType, entityId, minResults, apiBaseUrl = "") {
131
+ return _chunkSGJBUL2Ycjs.buildSuggestionUrl.call(void 0, "/api/faqs", { apiBaseUrl, entityType, entityId, count: minResults });
131
132
  }
132
133
  function groupBySection(faqs) {
133
134
  const map = /* @__PURE__ */ new Map();
@@ -147,7 +148,7 @@ function groupBySection(faqs) {
147
148
  }
148
149
  function FaqSkeleton() {
149
150
  return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "space-y-8 animate-pulse", children: [
150
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "h-[48px] md:h-[56px] w-2/3 rounded bg-ods-border" }),
151
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "h-12 md:h-14 w-2/3 rounded bg-ods-border" }),
151
152
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "rounded-3xl border border-ods-border overflow-hidden bg-ods-card divide-y divide-ods-border w-full", children: Array.from({ length: 8 }).map((_, idx) => /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex items-center justify-between px-6 md:px-8 py-6", children: [
152
153
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "h-6 w-5/6 rounded bg-ods-border" }),
153
154
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "h-10 w-10 rounded-md bg-ods-border" })
@@ -161,10 +162,15 @@ function FaqSection({
161
162
  heading,
162
163
  emitJsonLd = false,
163
164
  jsonLd,
164
- className
165
+ className,
166
+ minResults,
167
+ apiBaseUrl = ""
165
168
  }) {
166
- const url = buildFaqsUrl(entityType, entityId);
167
- const initialData = initialFaqs ? { faqs: initialFaqs } : void 0;
169
+ const url = buildFaqsUrl(entityType, entityId, minResults, apiBaseUrl);
170
+ const initialData = _react.useMemo.call(void 0,
171
+ () => initialFaqs ? { faqs: initialFaqs } : void 0,
172
+ [initialFaqs]
173
+ );
168
174
  const { data, isLoading, error } = _chunkT7AJBKZPcjs.useSelfFetch.call(void 0, url, { initialData });
169
175
  const faqs = _nullishCoalesce(_optionalChain([data, 'optionalAccess', _2 => _2.faqs]), () => ( []));
170
176
  const showHeading = heading !== null;
@@ -173,21 +179,29 @@ function FaqSection({
173
179
  if (error && !showHeading) {
174
180
  return null;
175
181
  }
182
+ if (!isLoading && !error && faqs.length === 0 && !showHeading) {
183
+ return null;
184
+ }
176
185
  if (isLoading && faqs.length === 0) {
186
+ if (!showHeading) {
187
+ return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, FaqSkeleton, {}) });
188
+ }
177
189
  return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "main", { className: _nullishCoalesce(className, () => ( "bg-ods-bg")), children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "max-w-[1920px] px-6 md:px-20 py-6 md:py-10 mx-auto", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, FaqSkeleton, {}) }) });
178
190
  }
179
191
  if (error && faqs.length === 0) {
180
192
  return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "main", { className: _nullishCoalesce(className, () => ( "bg-ods-bg flex items-center justify-center py-20")), children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { className: "text-ods-text-primary font-medium", children: "Failed to load FAQs. Please try again later." }) });
181
193
  }
182
194
  const schema = emitJsonLd ? buildFaqJsonLdFromFaqs(faqs, jsonLd) : null;
195
+ const groupNodes = groups.map(({ section, items }) => /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "space-y-4", children: [
196
+ section && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "h2", { className: "text-h2 tracking-[-0.04em] text-ods-text-primary mb-3 md:mb-4", children: section }),
197
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, FaqAccordion, { items })
198
+ ] }, section || "default"));
199
+ const body = showHeading ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "main", { className: _nullishCoalesce(className, () => ( "bg-ods-bg")), children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "max-w-[1920px] px-6 md:px-20 py-6 md:py-10 mx-auto space-y-10", children: [
200
+ headingNode,
201
+ groupNodes
202
+ ] }) }) : /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "section", { className: _nullishCoalesce(className, () => ( "space-y-10")), children: groupNodes });
183
203
  return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
184
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "main", { className: _nullishCoalesce(className, () => ( "bg-ods-bg")), children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "max-w-[1920px] px-6 md:px-20 py-6 md:py-10 mx-auto space-y-10", children: [
185
- showHeading && headingNode,
186
- groups.map(({ section, items }) => /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "space-y-4", children: [
187
- section && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "h2", { className: "text-h2 tracking-[-0.04em] text-ods-text-primary mb-3 md:mb-4", children: section }),
188
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, FaqAccordion, { items })
189
- ] }, section || "default"))
190
- ] }) }),
204
+ body,
191
205
  schema && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
192
206
  "script",
193
207
  {
@@ -202,4 +216,4 @@ function FaqSection({
202
216
 
203
217
 
204
218
  exports.FaqAccordion = FaqAccordion; exports.FaqSection = FaqSection;
205
- //# sourceMappingURL=chunk-36G2QXYZ.cjs.map
219
+ //# sourceMappingURL=chunk-3TDLFCCS.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-3TDLFCCS.cjs","../src/components/faq/faq-section.tsx","../src/components/faq-accordion.tsx","../src/components/faq/json-ld.ts"],"names":["jsx"],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-3TDLFCCS.cjs","sourcesContent":[null,"\"use client\"\n\nimport React, { useMemo } 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 { buildFaqJsonLdFromFaqs, type FaqSchemaOptions } from './json-ld'\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 * Page heading. Pass null to render in embedded mode (no <h1>, error branch\n * suppresses any banner). React node so platforms can drill their local\n * <PageHeading> component without the lib referencing platform state.\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\nfunction groupBySection(faqs: Faq[]): Array<{ section: string | null; items: FaqItem[] }> {\n const map = new Map<string, FaqItem[]>()\n for (const faq of faqs) {\n const key = faq.section || ''\n let arr = map.get(key)\n if (!arr) {\n arr = []\n map.set(key, arr)\n }\n arr.push({ id: faq.id, question: faq.question, answer: faq.answer })\n }\n return Array.from(map.entries()).map(([section, items]) => ({\n section: section || null,\n items,\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 * Generic, embeddable FAQ display surface.\n *\n * - Standalone /faqs page: pass `initialFaqs` from the server + `heading` +\n * `emitJsonLd` with `jsonLd` overrides for SEO.\n * - Per-entity embed: pass `entityType` + `entityId` (no `initialFaqs`); the\n * hook self-fetches and the public /api/faqs endpoint picks override-vs-fallback.\n *\n * CONTRACT: the consuming app MUST implement `GET /api/faqs`. Without it,\n * useSelfFetch reports `error:true` (logged once); in embedded mode (heading\n * === null) this component renders nothing so the host page isn't disfigured.\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 const showHeading = heading !== null\n const headingNode =\n heading === undefined\n ? <h1 className=\"text-h1 tracking-[-0.04em] text-ods-text-primary\">{DEFAULT_HEADING_TEXT}</h1>\n : heading\n\n const groups = groupBySection(faqs)\n\n // Embedded mode (no heading): degrade silently on error.\n if (error && !showHeading) {\n return null\n }\n\n // Embedded mode, zero FAQs after load: render nothing — an embedded host\n // page (blog/case-study detail) must not show an empty section shell.\n // The standalone /faqs page (heading set) keeps its existing behavior.\n if (!isLoading && !error && faqs.length === 0 && !showHeading) {\n return null\n }\n\n if (isLoading && faqs.length === 0) {\n // Embedded mode renders skeletons WITHOUT the standalone page shell —\n // a host detail page already provides <main> + gutters; nesting them\n // here would emit invalid markup and double padding.\n if (!showHeading) {\n return (\n <div className={className}>\n <FaqSkeleton />\n </div>\n )\n }\n return (\n <main className={className ?? 'bg-ods-bg'}>\n <div className=\"max-w-[1920px] px-6 md:px-20 py-6 md:py-10 mx-auto\">\n <FaqSkeleton />\n </div>\n </main>\n )\n }\n\n if (error && faqs.length === 0) {\n return (\n <main className={className ?? 'bg-ods-bg flex items-center justify-center py-20'}>\n <p className=\"text-ods-text-primary font-medium\">Failed to load FAQs. Please try again later.</p>\n </main>\n )\n }\n\n const schema = emitJsonLd ? buildFaqJsonLdFromFaqs(faqs, jsonLd) : null\n\n const groupNodes = groups.map(({ section, items }) => (\n <div key={section || 'default'} className=\"space-y-4\">\n {section && (\n <h2 className=\"text-h2 tracking-[-0.04em] text-ods-text-primary mb-3 md:mb-4\">\n {section}\n </h2>\n )}\n <FaqAccordion items={items} />\n </div>\n ))\n\n // Embedded mode (heading === null): no <main> page shell, no page gutters —\n // the host page owns layout. Standalone mode keeps the historical markup.\n const body = showHeading ? (\n <main className={className ?? 'bg-ods-bg'}>\n <div className=\"max-w-[1920px] px-6 md:px-20 py-6 md:py-10 mx-auto space-y-10\">\n {headingNode}\n {groupNodes}\n </div>\n </main>\n ) : (\n <section className={className ?? 'space-y-10'}>{groupNodes}</section>\n )\n\n return (\n <>\n {body}\n {schema && (\n <script\n type=\"application/ld+json\"\n // eslint-disable-next-line react/no-danger\n dangerouslySetInnerHTML={{ __html: JSON.stringify(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 <h3>\n {item.question}\n </h3>\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"]}
@@ -1,12 +1,12 @@
1
1
  "use client";
2
- import {
3
- init_next_link
4
- } from "./chunk-OHPI2HRK.js";
5
2
  import {
6
3
  init_next_navigation
7
4
  } from "./chunk-PLJLE4A4.js";
5
+ import {
6
+ init_next_link
7
+ } from "./chunk-OHPI2HRK.js";
8
8
 
9
9
  // src/embed-shims/index.ts
10
10
  init_next_link();
11
11
  init_next_navigation();
12
- //# sourceMappingURL=chunk-ORJREQ2W.js.map
12
+ //# sourceMappingURL=chunk-4XLJWX2N.js.map
@@ -0,0 +1,285 @@
1
+ "use client";
2
+ import {
3
+ useSelfFetch
4
+ } from "./chunk-L4LAAA5Q.js";
5
+ import {
6
+ BlogCard,
7
+ BlogCardSkeleton,
8
+ CONTENT_REF_GROUPS,
9
+ CaseStudyCard,
10
+ CaseStudyCardSkeleton,
11
+ CustomerInterviewCard,
12
+ CustomerInterviewCardSkeleton,
13
+ InvestorUpdateCard,
14
+ InvestorUpdateCardSkeleton,
15
+ OnboardingGuideCard,
16
+ OnboardingGuideCardSkeleton,
17
+ ProductReleaseCard,
18
+ ProductReleaseCardSkeleton,
19
+ ProgramCard,
20
+ ProgramCardSkeleton,
21
+ RoadmapCard,
22
+ RoadmapCardSkeleton,
23
+ buildListUrl,
24
+ buildProductReleaseCardProps,
25
+ canonicalContentRefType,
26
+ decideNewTab,
27
+ extractItems,
28
+ getContentRefLabelOrTitleCase,
29
+ orderContentRefTypes
30
+ } from "./chunk-KOHE4F6Q.js";
31
+ import {
32
+ buildSuggestionUrl
33
+ } from "./chunk-VAUI3CND.js";
34
+
35
+ // src/components/related-content/related-content-section.tsx
36
+ import { useMemo } from "react";
37
+ import { jsx, jsxs } from "react/jsx-runtime";
38
+ function DefaultLinkPropsProvider({ href, targetPlatform, children }) {
39
+ if (!href) return children(null);
40
+ const newTab = decideNewTab({ href, targetPlatform, currentSource: "" });
41
+ return children(
42
+ newTab ? { href, target: "_blank", rel: "noopener noreferrer" } : { href }
43
+ );
44
+ }
45
+ function defaultResolveHref(ref) {
46
+ return { href: ref.url || null, targetPlatform: ref.targetPlatform ?? null };
47
+ }
48
+ function renderSkeletonForType(type, size, adminCampaignCard) {
49
+ const legacySize = size === "sm" ? "sm" : "default";
50
+ switch (type) {
51
+ case "blog_post_existing":
52
+ return /* @__PURE__ */ jsx(BlogCardSkeleton, { size: legacySize });
53
+ case "case_study":
54
+ return /* @__PURE__ */ jsx(CaseStudyCardSkeleton, { size: legacySize });
55
+ case "customer_interview":
56
+ return /* @__PURE__ */ jsx(CustomerInterviewCardSkeleton, { size: legacySize });
57
+ case "product_release":
58
+ return /* @__PURE__ */ jsx(ProductReleaseCardSkeleton, { size: size === "sm" ? "sm" : "lg" });
59
+ case "podcast":
60
+ case "webinar":
61
+ case "event":
62
+ return /* @__PURE__ */ jsx(ProgramCardSkeleton, { size: legacySize });
63
+ case "investor_update":
64
+ return /* @__PURE__ */ jsx(InvestorUpdateCardSkeleton, { size: legacySize });
65
+ case "onboarding_guide":
66
+ return /* @__PURE__ */ jsx(OnboardingGuideCardSkeleton, { size: legacySize });
67
+ case "marketing_campaign":
68
+ return adminCampaignCard ? /* @__PURE__ */ jsx(adminCampaignCard.Skeleton, { size: legacySize }) : null;
69
+ case "roadmap_item":
70
+ case "delivery_item":
71
+ case "internal_task":
72
+ return /* @__PURE__ */ jsx(RoadmapCardSkeleton, { size: legacySize });
73
+ default:
74
+ return null;
75
+ }
76
+ }
77
+ function CardForType({
78
+ type,
79
+ item,
80
+ size,
81
+ href,
82
+ targetPlatform,
83
+ linkProps,
84
+ extras,
85
+ adminCampaignCard
86
+ }) {
87
+ const legacySize = size === "sm" ? "sm" : "default";
88
+ const placeholderUrl = extras?.buildOgPlaceholderUrl?.(item?.title ?? "") ?? void 0;
89
+ const anchorAttrs = linkProps ? { target: linkProps.target, rel: linkProps.rel } : {};
90
+ switch (type) {
91
+ case "blog_post_existing":
92
+ return /* @__PURE__ */ jsx(BlogCard, { post: item, size: legacySize, href, targetPlatform, placeholderUrl, ...anchorAttrs });
93
+ case "case_study":
94
+ return /* @__PURE__ */ jsx(CaseStudyCard, { study: item, size: legacySize, href, targetPlatform, placeholderUrl, ...anchorAttrs });
95
+ case "customer_interview":
96
+ return /* @__PURE__ */ jsx(CustomerInterviewCard, { interview: item, size: legacySize, href, targetPlatform, placeholderUrl, ...anchorAttrs });
97
+ case "product_release": {
98
+ const releaseSize = size === "sm" ? "sm" : "lg";
99
+ const buildReleaseProps = extras?.buildProductReleaseCardProps ?? buildProductReleaseCardProps;
100
+ const releaseProps = buildReleaseProps(item);
101
+ return /* @__PURE__ */ jsx(
102
+ ProductReleaseCard,
103
+ {
104
+ size: releaseSize,
105
+ title: item.title,
106
+ summary: item.summary,
107
+ version: item.version,
108
+ ...releaseProps,
109
+ anchorProps: linkProps ?? void 0
110
+ }
111
+ );
112
+ }
113
+ case "podcast":
114
+ return extras?.programConfigs?.podcast ? /* @__PURE__ */ jsx(ProgramCard, { config: extras.programConfigs.podcast, item, size: legacySize, href, targetPlatform, placeholderUrl, ...anchorAttrs }) : null;
115
+ case "webinar":
116
+ return extras?.programConfigs?.webinar ? /* @__PURE__ */ jsx(ProgramCard, { config: extras.programConfigs.webinar, item, size: legacySize, href, targetPlatform, placeholderUrl, ...anchorAttrs }) : null;
117
+ case "event":
118
+ return extras?.programConfigs?.event ? /* @__PURE__ */ jsx(ProgramCard, { config: extras.programConfigs.event, item, size: legacySize, href, targetPlatform, placeholderUrl, ...anchorAttrs }) : null;
119
+ case "investor_update":
120
+ return /* @__PURE__ */ jsx(InvestorUpdateCard, { update: item, size: legacySize, href, targetPlatform, placeholderUrl, ...anchorAttrs });
121
+ case "onboarding_guide":
122
+ return /* @__PURE__ */ jsx(OnboardingGuideCard, { guide: item, size: legacySize, href, targetPlatform, placeholderUrl, ...anchorAttrs });
123
+ case "marketing_campaign":
124
+ return adminCampaignCard ? /* @__PURE__ */ jsx(adminCampaignCard.Card, { campaign: item }) : null;
125
+ case "roadmap_item":
126
+ case "delivery_item":
127
+ case "internal_task":
128
+ return /* @__PURE__ */ jsx(
129
+ RoadmapCard,
130
+ {
131
+ item,
132
+ href: href ?? "",
133
+ targetPlatform,
134
+ userVote: null,
135
+ onVote: () => {
136
+ },
137
+ size: legacySize,
138
+ cardType: type,
139
+ ...anchorAttrs
140
+ }
141
+ );
142
+ default:
143
+ return null;
144
+ }
145
+ }
146
+ function useGroupItems(type, refs, buildUrl) {
147
+ const ids = refs.map((r) => r.id);
148
+ const url = ids.length > 0 ? buildUrl(type, ids) : null;
149
+ const { data, isLoading } = useSelfFetch(url);
150
+ const items = data != null ? extractItems(data) : null;
151
+ return { items, isLoading };
152
+ }
153
+ function gridClassFor(columns) {
154
+ return columns === 3 ? "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6" : "grid grid-cols-1 sm:grid-cols-2 gap-6";
155
+ }
156
+ function resolveGroupConfig(type) {
157
+ return CONTENT_REF_GROUPS[type] ?? {
158
+ label: type,
159
+ order: 999,
160
+ layout: "grid",
161
+ gridSize: "default"
162
+ };
163
+ }
164
+ function ContentGroup({
165
+ type,
166
+ refs,
167
+ columns,
168
+ buildUrl,
169
+ resolveHref,
170
+ LinkProvider,
171
+ extras,
172
+ adminCampaignCard
173
+ }) {
174
+ const { items, isLoading } = useGroupItems(type, refs, buildUrl);
175
+ const config = resolveGroupConfig(type);
176
+ const isListLayout = config.layout === "list";
177
+ const cardSize = config.gridSize;
178
+ if (isLoading && !items) {
179
+ const skeletons = refs.map((r) => /* @__PURE__ */ jsx("div", { children: renderSkeletonForType(type, cardSize, adminCampaignCard) }, r.id));
180
+ return isListLayout ? /* @__PURE__ */ jsx("div", { className: "space-y-4", children: skeletons }) : /* @__PURE__ */ jsx("div", { className: gridClassFor(columns), children: skeletons });
181
+ }
182
+ if (!items || items.length === 0) return null;
183
+ const refMap = new Map(refs.map((r) => [String(r.id), r]));
184
+ const cards = items.map((item) => {
185
+ const itemId = String(item.id);
186
+ const contentRef = refMap.get(itemId);
187
+ if (!contentRef) return null;
188
+ const resolved = resolveHref(contentRef);
189
+ const href = resolved.href ?? "";
190
+ const targetPlatform = resolved.targetPlatform ?? contentRef.targetPlatform ?? null;
191
+ return /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(LinkProvider, { href: href || null, targetPlatform, children: (linkProps) => /* @__PURE__ */ jsx(
192
+ CardForType,
193
+ {
194
+ type,
195
+ item,
196
+ contentRef,
197
+ size: cardSize,
198
+ href,
199
+ targetPlatform,
200
+ linkProps,
201
+ extras,
202
+ adminCampaignCard
203
+ }
204
+ ) }) }, itemId);
205
+ }).filter(Boolean);
206
+ if (cards.length === 0) return null;
207
+ return isListLayout ? /* @__PURE__ */ jsx("div", { className: "space-y-4", children: cards }) : /* @__PURE__ */ jsx("div", { className: gridClassFor(columns), children: cards });
208
+ }
209
+ function RelatedContentSection({
210
+ contentRefs,
211
+ entityType,
212
+ entityId,
213
+ minResults,
214
+ sameTypeMinResults,
215
+ initialItems,
216
+ title = "Related Content",
217
+ columns = 2,
218
+ excludeTypes,
219
+ apiBaseUrl = "",
220
+ extras,
221
+ resolveHref = defaultResolveHref,
222
+ buildListUrl: buildListUrl2,
223
+ LinkProvider = DefaultLinkPropsProvider,
224
+ adminCampaignCard
225
+ }) {
226
+ const suggestUrl = contentRefs === void 0 && entityType && entityId !== void 0 && entityId !== null && entityId !== "" ? buildSuggestionUrl("/api/related-content", {
227
+ apiBaseUrl,
228
+ entityType,
229
+ entityId,
230
+ count: minResults,
231
+ extraParams: {
232
+ sameTypeCount: sameTypeMinResults !== void 0 ? String(sameTypeMinResults) : void 0,
233
+ excludeTypes: excludeTypes && excludeTypes.length > 0 ? excludeTypes.join(",") : void 0
234
+ }
235
+ }) : null;
236
+ const initialData = useMemo(
237
+ () => initialItems ? { refs: initialItems } : void 0,
238
+ [initialItems]
239
+ );
240
+ const { data } = useSelfFetch(suggestUrl, { initialData });
241
+ const effectiveBuildListUrl = useMemo(
242
+ () => buildListUrl2 ?? ((type, ids) => buildListUrl(type, ids, apiBaseUrl)),
243
+ [buildListUrl2, apiBaseUrl]
244
+ );
245
+ const refs = contentRefs ?? data?.refs ?? [];
246
+ const exclude = new Set(excludeTypes || []);
247
+ const visibleRefs = exclude.size > 0 ? refs.filter((r) => !exclude.has(r.type)) : refs;
248
+ if (!visibleRefs.length) return null;
249
+ const grouped = {};
250
+ for (const ref of visibleRefs) {
251
+ if (!grouped[ref.type]) grouped[ref.type] = [];
252
+ grouped[ref.type].push(ref);
253
+ }
254
+ let orderedTypes = orderContentRefTypes(Object.keys(grouped));
255
+ if (entityType) {
256
+ const sameType = orderedTypes.filter((t) => canonicalContentRefType(t) === entityType);
257
+ if (sameType.length > 0) {
258
+ orderedTypes = [...sameType, ...orderedTypes.filter((t) => canonicalContentRefType(t) !== entityType)];
259
+ }
260
+ }
261
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-8", children: [
262
+ /* @__PURE__ */ jsx("h2", { className: "text-2xl font-bold text-ods-text-primary", children: title }),
263
+ orderedTypes.map((type) => /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
264
+ /* @__PURE__ */ jsx("h3", { className: "font-['Azeret_Mono'] text-[14px] font-semibold uppercase text-ods-text-secondary tracking-wider", children: getContentRefLabelOrTitleCase(type) }),
265
+ /* @__PURE__ */ jsx(
266
+ ContentGroup,
267
+ {
268
+ type,
269
+ refs: grouped[type],
270
+ columns,
271
+ buildUrl: effectiveBuildListUrl,
272
+ resolveHref,
273
+ LinkProvider,
274
+ extras,
275
+ adminCampaignCard
276
+ }
277
+ )
278
+ ] }, type))
279
+ ] });
280
+ }
281
+
282
+ export {
283
+ RelatedContentSection
284
+ };
285
+ //# sourceMappingURL=chunk-5KAQW6Q3.js.map
@@ -0,0 +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, { useMemo } 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 { extractItems } 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 { 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 return <OnboardingGuideCardSkeleton size={legacySize} />;\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 contentRef: ContentRef;\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 return <OnboardingGuideCard guide={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\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\nfunction ContentGroup({\n type,\n refs,\n columns,\n buildUrl,\n resolveHref,\n LinkProvider,\n extras,\n adminCampaignCard,\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}) {\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 // 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 = refs.map((r) => (\n <div key={r.id}>{renderSkeletonForType(type, cardSize, adminCampaignCard)}</div>\n ));\n return isListLayout ? (\n <div className=\"space-y-4\">{skeletons}</div>\n ) : (\n <div className={gridClassFor(columns)}>{skeletons}</div>\n );\n }\n\n if (!items || items.length === 0) return null;\n\n // Match fetched items to refs (for url/visibility data).\n const refMap = new Map(refs.map((r) => [String(r.id), r]));\n\n // Items come back server-sorted (date descending). Render in that order.\n const cards = items\n .map((item: any) => {\n const itemId = String(item.id);\n const contentRef = refMap.get(itemId);\n if (!contentRef) 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 contentRef={contentRef}\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) return null;\n\n return isListLayout ? (\n <div className=\"space-y-4\">{cards}</div>\n ) : (\n <div className={gridClassFor(columns)}>{cards}</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 /** 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 /** 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}\n\nexport function RelatedContentSection({\n contentRefs,\n entityType,\n entityId,\n minResults,\n sameTypeMinResults,\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}: 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 const suggestUrl =\n contentRefs === undefined && entityType && entityId !== undefined && entityId !== null && entityId !== ''\n ? buildSuggestionUrl('/api/related-content', {\n apiBaseUrl,\n entityType,\n entityId,\n count: minResults,\n extraParams: {\n sameTypeCount: sameTypeMinResults !== undefined ? String(sameTypeMinResults) : undefined,\n excludeTypes: excludeTypes && excludeTypes.length > 0 ? excludeTypes.join(',') : undefined,\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 () => (initialItems ? { refs: initialItems } : undefined),\n [initialItems],\n );\n const { data } = 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) → no\n // empty shell.\n if (!visibleRefs.length) return null;\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 const sameType = orderedTypes.filter((t) => canonicalContentRefType(t) === entityType);\n if (sameType.length > 0) {\n orderedTypes = [...sameType, ...orderedTypes.filter((t) => canonicalContentRefType(t) !== entityType)];\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 <div key={type} className=\"space-y-4\">\n <h3 className=\"font-['Azeret_Mono'] text-[14px] font-semibold uppercase text-ods-text-secondary tracking-wider\">\n {getContentRefLabelOrTitleCase(type)}\n </h3>\n <ContentGroup\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 />\n </div>\n ))}\n </div>\n );\n}\n"],"mappings":"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","names":["buildListUrl"]}
@@ -7,13 +7,13 @@ import {
7
7
  resolveExternalNavigation,
8
8
  resolveSourceIcon,
9
9
  stripSameOriginToPath
10
- } from "./chunk-Y7DCQUNL.js";
11
- import {
12
- useChatRuntime
13
- } from "./chunk-4YKGXZ7W.js";
10
+ } from "./chunk-KOHE4F6Q.js";
14
11
  import {
15
12
  useDebounce
16
13
  } from "./chunk-UECRKCKH.js";
14
+ import {
15
+ useChatRuntime
16
+ } from "./chunk-4YKGXZ7W.js";
17
17
  import {
18
18
  useRouter
19
19
  } from "./chunk-PLJLE4A4.js";
@@ -386,4 +386,4 @@ export {
386
386
  useDocSearch,
387
387
  DetailPageSkeleton
388
388
  };
389
- //# sourceMappingURL=chunk-7LLS3TWN.js.map
389
+ //# sourceMappingURL=chunk-6IFN7F3C.js.map