@flamingo-stack/openframe-frontend-core 0.0.243 → 0.0.244

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-MKVKVJQQ.js → chunk-APGMNHPY.js} +19 -32
  2. package/dist/chunk-APGMNHPY.js.map +1 -0
  3. package/dist/{chunk-3TDLFCCS.cjs → chunk-DV3YZWGN.cjs} +20 -33
  4. package/dist/chunk-DV3YZWGN.cjs.map +1 -0
  5. package/dist/{chunk-BKPS6M2F.cjs → chunk-EJ7J4QAD.cjs} +12 -12
  6. package/dist/{chunk-BKPS6M2F.cjs.map → chunk-EJ7J4QAD.cjs.map} +1 -1
  7. package/dist/{chunk-CZITXP77.cjs → chunk-EPXROXYC.cjs} +713 -645
  8. package/dist/chunk-EPXROXYC.cjs.map +1 -0
  9. package/dist/{chunk-SGJBUL2Y.cjs → chunk-FSS2MIEA.cjs} +3 -20
  10. package/dist/chunk-FSS2MIEA.cjs.map +1 -0
  11. package/dist/{chunk-VAUI3CND.js → chunk-LJGBFD52.js} +1 -18
  12. package/dist/chunk-LJGBFD52.js.map +1 -0
  13. package/dist/{chunk-FRRMXLU5.js → chunk-M7DPUWAL.js} +188 -120
  14. package/dist/chunk-M7DPUWAL.js.map +1 -0
  15. package/dist/{chunk-SEVJT2HP.js → chunk-NKGNRFSS.js} +8 -8
  16. package/dist/{chunk-VFKQMAUF.cjs → chunk-OB45JHDY.cjs} +3 -3
  17. package/dist/{chunk-VFKQMAUF.cjs.map → chunk-OB45JHDY.cjs.map} +1 -1
  18. package/dist/{chunk-4XLJWX2N.js → chunk-ORJREQ2W.js} +4 -4
  19. package/dist/{chunk-7ZHFCEJH.js → chunk-P5C732SC.js} +6 -6
  20. package/dist/{chunk-JJV3XGCY.js → chunk-PTSAF7T2.js} +2 -2
  21. package/dist/{chunk-VXP553WT.cjs → chunk-RJC3L5DT.cjs} +7 -7
  22. package/dist/{chunk-VXP553WT.cjs.map → chunk-RJC3L5DT.cjs.map} +1 -1
  23. package/dist/{chunk-MP5HOP7N.cjs → chunk-STKMIMLP.cjs} +12 -12
  24. package/dist/{chunk-MP5HOP7N.cjs.map → chunk-STKMIMLP.cjs.map} +1 -1
  25. package/dist/{chunk-V35PZSUA.js → chunk-WD2MZVJI.js} +5 -5
  26. package/dist/{chunk-DUZBTUXF.cjs → chunk-Z6GGYZDR.cjs} +27 -27
  27. package/dist/{chunk-DUZBTUXF.cjs.map → chunk-Z6GGYZDR.cjs.map} +1 -1
  28. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  29. package/dist/components/chat/hooks/use-chat-card-item.d.ts.map +1 -1
  30. package/dist/components/chat/index.cjs +16 -6
  31. package/dist/components/chat/index.cjs.map +1 -1
  32. package/dist/components/chat/index.js +15 -5
  33. package/dist/components/chat/types/entities/content-ref.d.ts +20 -5
  34. package/dist/components/chat/types/entities/content-ref.d.ts.map +1 -1
  35. package/dist/components/chat/utils/history-merge.d.ts +92 -0
  36. package/dist/components/chat/utils/history-merge.d.ts.map +1 -0
  37. package/dist/components/chat/utils/index.d.ts +1 -0
  38. package/dist/components/chat/utils/index.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 +1 -7
  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/features/index.cjs +6 -6
  46. package/dist/components/features/index.js +5 -5
  47. package/dist/components/features/policy-configuration-panel.d.ts.map +1 -1
  48. package/dist/components/index.cjs +93 -87
  49. package/dist/components/index.cjs.map +1 -1
  50. package/dist/components/index.d.ts +0 -1
  51. package/dist/components/index.d.ts.map +1 -1
  52. package/dist/components/index.js +37 -31
  53. package/dist/components/index.js.map +1 -1
  54. package/dist/components/navigation/index.cjs +6 -6
  55. package/dist/components/navigation/index.js +5 -5
  56. package/dist/components/onboarding-guides/index.cjs +30 -30
  57. package/dist/components/onboarding-guides/index.cjs.map +1 -1
  58. package/dist/components/onboarding-guides/index.js +11 -11
  59. package/dist/components/tickets/index.cjs +73 -73
  60. package/dist/components/tickets/index.cjs.map +1 -1
  61. package/dist/components/tickets/index.js +15 -15
  62. package/dist/components/ui/index.cjs +19 -9
  63. package/dist/components/ui/index.cjs.map +1 -1
  64. package/dist/components/ui/index.js +23 -13
  65. package/dist/embed-shims/index.cjs +3 -3
  66. package/dist/embed-shims/index.cjs.map +1 -1
  67. package/dist/embed-shims/index.js +5 -5
  68. package/dist/index.cjs +11 -17
  69. package/dist/index.cjs.map +1 -1
  70. package/dist/index.js +49 -55
  71. package/dist/types/index.d.ts +0 -1
  72. package/dist/types/index.d.ts.map +1 -1
  73. package/dist/utils/index.cjs +0 -81
  74. package/dist/utils/index.cjs.map +1 -1
  75. package/dist/utils/index.d.ts +1 -4
  76. package/dist/utils/index.d.ts.map +1 -1
  77. package/dist/utils/index.js +1 -74
  78. package/dist/utils/index.js.map +1 -1
  79. package/dist/utils/list-url.d.ts +0 -6
  80. package/dist/utils/list-url.d.ts.map +1 -1
  81. package/package.json +1 -7
  82. package/src/components/chat/__tests__/chat-message-list.test.tsx +92 -0
  83. package/src/components/chat/chat-message-list.tsx +74 -49
  84. package/src/components/chat/hooks/use-chat-card-item.ts +38 -3
  85. package/src/components/chat/types/entities/content-ref.ts +21 -5
  86. package/src/components/chat/utils/__tests__/history-merge.test.ts +339 -0
  87. package/src/components/chat/utils/history-merge.ts +255 -0
  88. package/src/components/chat/utils/index.ts +12 -0
  89. package/src/components/faq/faq-section.tsx +24 -69
  90. package/src/components/features/policy-configuration-panel.tsx +1 -0
  91. package/src/components/index.ts +0 -1
  92. package/src/types/index.ts +0 -1
  93. package/src/utils/index.ts +1 -16
  94. package/src/utils/list-url.ts +0 -9
  95. package/dist/chunk-3TDLFCCS.cjs.map +0 -1
  96. package/dist/chunk-CZITXP77.cjs.map +0 -1
  97. package/dist/chunk-F3GMWNAT.js +0 -285
  98. package/dist/chunk-F3GMWNAT.js.map +0 -1
  99. package/dist/chunk-FRRMXLU5.js.map +0 -1
  100. package/dist/chunk-HU252PYJ.cjs +0 -285
  101. package/dist/chunk-HU252PYJ.cjs.map +0 -1
  102. package/dist/chunk-MKVKVJQQ.js.map +0 -1
  103. package/dist/chunk-SGJBUL2Y.cjs.map +0 -1
  104. package/dist/chunk-VAUI3CND.js.map +0 -1
  105. package/dist/components/related-content/index.cjs +0 -26
  106. package/dist/components/related-content/index.cjs.map +0 -1
  107. package/dist/components/related-content/index.d.ts +0 -2
  108. package/dist/components/related-content/index.d.ts.map +0 -1
  109. package/dist/components/related-content/index.js +0 -26
  110. package/dist/components/related-content/index.js.map +0 -1
  111. package/dist/components/related-content/related-content-section.d.ts +0 -141
  112. package/dist/components/related-content/related-content-section.d.ts.map +0 -1
  113. package/dist/types/content-ref.d.ts +0 -35
  114. package/dist/types/content-ref.d.ts.map +0 -1
  115. package/dist/utils/content-ref-groups.d.ts +0 -58
  116. package/dist/utils/content-ref-groups.d.ts.map +0 -1
  117. package/dist/utils/extract-items.d.ts +0 -18
  118. package/dist/utils/extract-items.d.ts.map +0 -1
  119. package/dist/utils/suggestion-url.d.ts +0 -26
  120. package/dist/utils/suggestion-url.d.ts.map +0 -1
  121. package/src/components/related-content/index.ts +0 -9
  122. package/src/components/related-content/related-content-section.tsx +0 -612
  123. package/src/types/content-ref.ts +0 -34
  124. package/src/utils/content-ref-groups.ts +0 -89
  125. package/src/utils/extract-items.ts +0 -48
  126. package/src/utils/suggestion-url.ts +0 -39
  127. /package/dist/{chunk-SEVJT2HP.js.map → chunk-NKGNRFSS.js.map} +0 -0
  128. /package/dist/{chunk-4XLJWX2N.js.map → chunk-ORJREQ2W.js.map} +0 -0
  129. /package/dist/{chunk-7ZHFCEJH.js.map → chunk-P5C732SC.js.map} +0 -0
  130. /package/dist/{chunk-JJV3XGCY.js.map → chunk-PTSAF7T2.js.map} +0 -0
  131. /package/dist/{chunk-V35PZSUA.js.map → chunk-WD2MZVJI.js.map} +0 -0
@@ -1,10 +1,9 @@
1
1
  "use client"
2
2
 
3
- import React, { useMemo } from 'react'
3
+ import React from 'react'
4
4
  import type { Faq } from '../../types/faq'
5
5
  import { FaqAccordion, type FaqItem } from '../faq-accordion'
6
6
  import { useSelfFetch } from '../../hooks/use-self-fetch'
7
- import { buildSuggestionUrl } from '../../utils/suggestion-url'
8
7
  import { buildFaqJsonLdFromFaqs, type FaqSchemaOptions } from './json-ld'
9
8
 
10
9
  export interface FaqSectionProps {
@@ -29,26 +28,16 @@ export interface FaqSectionProps {
29
28
  /** Overrides for the JSON-LD's name/description/url. */
30
29
  jsonLd?: FaqSchemaOptions
31
30
  className?: string
32
- /** Maps to /api/faqs `?count=` (the 5-tier fill target). Absent → param
33
- * not sent (server default applies). */
34
- minResults?: number
35
- /** Fetch-URL prefix for third-party embeds / reverse proxies
36
- * ('' = same-origin relative). */
37
- apiBaseUrl?: string
38
31
  }
39
32
 
40
33
  const DEFAULT_HEADING_TEXT = 'Frequently Asked Questions'
41
34
 
42
- /** URL composition shared with RelatedContentSection (`buildSuggestionUrl`)
43
- * byte-identical to the historical `buildFaqsUrl` output when
44
- * `minResults`/`apiBaseUrl` are absent. */
45
- function buildFaqsUrl(
46
- entityType?: string,
47
- entityId?: number | string,
48
- minResults?: number,
49
- apiBaseUrl = '',
50
- ): string {
51
- return buildSuggestionUrl('/api/faqs', { apiBaseUrl, entityType, entityId, count: minResults })
35
+ function buildFaqsUrl(entityType?: string, entityId?: number | string): string {
36
+ if (!entityType || entityId === undefined || entityId === null || entityId === '') {
37
+ return '/api/faqs'
38
+ }
39
+ const qs = new URLSearchParams({ entityType, entityId: String(entityId) })
40
+ return `/api/faqs?${qs.toString()}`
52
41
  }
53
42
 
54
43
  function groupBySection(faqs: Faq[]): Array<{ section: string | null; items: FaqItem[] }> {
@@ -108,16 +97,9 @@ export function FaqSection({
108
97
  emitJsonLd = false,
109
98
  jsonLd,
110
99
  className,
111
- minResults,
112
- apiBaseUrl = '',
113
100
  }: FaqSectionProps) {
114
- const url = buildFaqsUrl(entityType, entityId, minResults, apiBaseUrl)
115
- // Memoized useSelfFetch re-syncs on [initialData]; a fresh per-render
116
- // wrapper object would setState-loop under re-rendering parents.
117
- const initialData = useMemo<FaqsResponse | undefined>(
118
- () => (initialFaqs ? { faqs: initialFaqs } : undefined),
119
- [initialFaqs],
120
- )
101
+ const url = buildFaqsUrl(entityType, entityId)
102
+ const initialData = initialFaqs ? { faqs: initialFaqs } : undefined
121
103
  const { data, isLoading, error } = useSelfFetch<FaqsResponse>(url, { initialData })
122
104
 
123
105
  const faqs = data?.faqs ?? []
@@ -134,24 +116,7 @@ export function FaqSection({
134
116
  return null
135
117
  }
136
118
 
137
- // Embedded mode, zero FAQs after load: render nothing — an embedded host
138
- // page (blog/case-study detail) must not show an empty section shell.
139
- // The standalone /faqs page (heading set) keeps its existing behavior.
140
- if (!isLoading && !error && faqs.length === 0 && !showHeading) {
141
- return null
142
- }
143
-
144
119
  if (isLoading && faqs.length === 0) {
145
- // Embedded mode renders skeletons WITHOUT the standalone page shell —
146
- // a host detail page already provides <main> + gutters; nesting them
147
- // here would emit invalid markup and double padding.
148
- if (!showHeading) {
149
- return (
150
- <div className={className}>
151
- <FaqSkeleton />
152
- </div>
153
- )
154
- }
155
120
  return (
156
121
  <main className={className ?? 'bg-ods-bg'}>
157
122
  <div className="max-w-[1920px] px-6 md:px-20 py-6 md:py-10 mx-auto">
@@ -171,33 +136,23 @@ export function FaqSection({
171
136
 
172
137
  const schema = emitJsonLd ? buildFaqJsonLdFromFaqs(faqs, jsonLd) : null
173
138
 
174
- const groupNodes = groups.map(({ section, items }) => (
175
- <div key={section || 'default'} className="space-y-4">
176
- {section && (
177
- <h2 className="text-h2 tracking-[-0.04em] text-ods-text-primary mb-3 md:mb-4">
178
- {section}
179
- </h2>
180
- )}
181
- <FaqAccordion items={items} />
182
- </div>
183
- ))
184
-
185
- // Embedded mode (heading === null): no <main> page shell, no page gutters —
186
- // the host page owns layout. Standalone mode keeps the historical markup.
187
- const body = showHeading ? (
188
- <main className={className ?? 'bg-ods-bg'}>
189
- <div className="max-w-[1920px] px-6 md:px-20 py-6 md:py-10 mx-auto space-y-10">
190
- {headingNode}
191
- {groupNodes}
192
- </div>
193
- </main>
194
- ) : (
195
- <section className={className ?? 'space-y-10'}>{groupNodes}</section>
196
- )
197
-
198
139
  return (
199
140
  <>
200
- {body}
141
+ <main className={className ?? 'bg-ods-bg'}>
142
+ <div className="max-w-[1920px] px-6 md:px-20 py-6 md:py-10 mx-auto space-y-10">
143
+ {showHeading && headingNode}
144
+ {groups.map(({ section, items }) => (
145
+ <div key={section || 'default'} className="space-y-4">
146
+ {section && (
147
+ <h2 className="text-h2 tracking-[-0.04em] text-ods-text-primary mb-3 md:mb-4">
148
+ {section}
149
+ </h2>
150
+ )}
151
+ <FaqAccordion items={items} />
152
+ </div>
153
+ ))}
154
+ </div>
155
+ </main>
201
156
  {schema && (
202
157
  <script
203
158
  type="application/ld+json"
@@ -204,6 +204,7 @@ const CategorySection: React.FC<{
204
204
 
205
205
  {/* Expand/Collapse Button */}
206
206
  <Button
207
+ type="button"
207
208
  variant="transparent"
208
209
  onClick={(e) => {
209
210
  e.stopPropagation();
@@ -26,7 +26,6 @@ export * from './faq-accordion'
26
26
  // dedicated server-safe subpath "./components/faq/json-ld" (built without the
27
27
  // client banner under the server/universal block of tsup.config.ts).
28
28
  export * from './faq'
29
- export * from './related-content'
30
29
  export * from './filter-chip'
31
30
  export * from './footer'
32
31
  export * from './unified-filter-logic'
@@ -38,7 +38,6 @@ export * from './waitlist'
38
38
 
39
39
  // Business logic types
40
40
  export * from './faq'
41
- export * from './content-ref'
42
41
  export * from './report'
43
42
  export * from './stack'
44
43
 
@@ -235,22 +235,7 @@ export {
235
235
  // delegate here (byte-parity test guards the migration); embedders wire
236
236
  // `endpoints.buildListUrl = (t, ids) => buildListUrl(t, ids, '/content')`.
237
237
  // Pure + server-safe (the hub imports it server-side from this barrel).
238
- export { buildListUrl, canonicalContentRefType } from './list-url'
239
-
240
- // Content-ref group registry (labels/order/layout per rail type) + list-API
241
- // response normalizers + the shared suggestion-fetch URL composer — all
242
- // pure + server-safe; the hub re-exports these from its config/util shims.
243
- export {
244
- CONTENT_REF_GROUPS,
245
- getContentRefLabel,
246
- getContentRefLabelOrTitleCase,
247
- orderContentRefTypes,
248
- type ContentRefGroupConfig,
249
- type ContentRefLayout,
250
- type ContentRefGridSize,
251
- } from './content-ref-groups'
252
- export { extractItems, extractItemId } from './extract-items'
253
- export { buildSuggestionUrl, type SuggestionUrlOptions } from './suggestion-url'
238
+ export { buildListUrl } from './list-url'
254
239
 
255
240
  // Embedder-configurable content-URL composer for the existing
256
241
  // `runtime.composeContentUrl` seam — relative href for host-served types,
@@ -37,15 +37,6 @@
37
37
  */
38
38
  const ALIASES: Record<string, string> = { blog_post_existing: 'blog_post' }
39
39
 
40
- /** Resolve a ContentRef rail-vocab type to its canonical `documentType`
41
- * (registry vocabulary) — `blog_post_existing` → `blog_post`, everything
42
- * else passes through. Exported so consumers comparing rail group keys to
43
- * registry entityTypes (e.g. the related-content rail's same-type-first
44
- * ordering) share THIS alias map instead of re-declaring it. */
45
- export function canonicalContentRefType(contentRefType: string): string {
46
- return ALIASES[contentRefType] ?? contentRefType
47
- }
48
-
49
40
  /**
50
41
  * One builder per fetch-mode `contentRefType`, keyed by the canonical
51
42
  * `documentType`. Each body is copied VERBATIM from the matching hub
@@ -1 +0,0 @@
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"]}