@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.
- package/dist/{chunk-PN64LWNT.js → chunk-2GMHLKEO.js} +8 -8
- package/dist/{chunk-36G2QXYZ.cjs → chunk-3TDLFCCS.cjs} +37 -23
- package/dist/chunk-3TDLFCCS.cjs.map +1 -0
- package/dist/{chunk-ORJREQ2W.js → chunk-4XLJWX2N.js} +4 -4
- package/dist/chunk-5KAQW6Q3.js +285 -0
- package/dist/chunk-5KAQW6Q3.js.map +1 -0
- package/dist/{chunk-7LLS3TWN.js → chunk-6IFN7F3C.js} +5 -5
- package/dist/{chunk-5VU66BD2.cjs → chunk-EFT34DLB.cjs} +498 -461
- package/dist/chunk-EFT34DLB.cjs.map +1 -0
- package/dist/{chunk-Y6463TD6.js → chunk-FEZSQQTH.js} +2 -2
- package/dist/{chunk-IJOJWBME.cjs → chunk-GRD7M6IZ.cjs} +27 -27
- package/dist/{chunk-IJOJWBME.cjs.map → chunk-GRD7M6IZ.cjs.map} +1 -1
- package/dist/{chunk-ELMYRGWG.cjs → chunk-JEPYFYQP.cjs} +12 -12
- package/dist/{chunk-ELMYRGWG.cjs.map → chunk-JEPYFYQP.cjs.map} +1 -1
- package/dist/{chunk-Y7DCQUNL.js → chunk-KOHE4F6Q.js} +91 -54
- package/dist/chunk-KOHE4F6Q.js.map +1 -0
- package/dist/{chunk-JDQY7JCV.js → chunk-MKVKVJQQ.js} +36 -22
- package/dist/chunk-MKVKVJQQ.js.map +1 -0
- package/dist/{chunk-MTROCWO4.cjs → chunk-QJULZ4PB.cjs} +7 -7
- package/dist/{chunk-MTROCWO4.cjs.map → chunk-QJULZ4PB.cjs.map} +1 -1
- package/dist/{chunk-FSS2MIEA.cjs → chunk-SGJBUL2Y.cjs} +20 -3
- package/dist/chunk-SGJBUL2Y.cjs.map +1 -0
- package/dist/{chunk-SZ3BBI6H.cjs → chunk-SWXWKVCC.cjs} +12 -12
- package/dist/{chunk-SZ3BBI6H.cjs.map → chunk-SWXWKVCC.cjs.map} +1 -1
- package/dist/{chunk-LJGBFD52.js → chunk-VAUI3CND.js} +18 -1
- package/dist/chunk-VAUI3CND.js.map +1 -0
- package/dist/{chunk-OB45JHDY.cjs → chunk-VFKQMAUF.cjs} +3 -3
- package/dist/{chunk-OB45JHDY.cjs.map → chunk-VFKQMAUF.cjs.map} +1 -1
- package/dist/chunk-WKZS5HWH.cjs +285 -0
- package/dist/chunk-WKZS5HWH.cjs.map +1 -0
- package/dist/{chunk-G6YZ7O27.js → chunk-Z3657YGL.js} +6 -6
- package/dist/components/chat/hooks/use-chat-card-item.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +6 -6
- package/dist/components/chat/index.js +5 -5
- package/dist/components/chat/types/entities/content-ref.d.ts +5 -20
- package/dist/components/chat/types/entities/content-ref.d.ts.map +1 -1
- package/dist/components/chevron-button.d.ts +3 -1
- package/dist/components/chevron-button.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +7 -7
- package/dist/components/contact/index.js +6 -6
- package/dist/components/faq/faq-section.d.ts +8 -2
- package/dist/components/faq/faq-section.d.ts.map +1 -1
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/faq/json-ld.cjs +1 -1
- package/dist/components/faq/json-ld.cjs.map +1 -1
- package/dist/components/faq/{faq-json-ld.d.ts → json-ld.d.ts} +1 -1
- package/dist/components/faq/json-ld.d.ts.map +1 -0
- package/dist/components/faq/json-ld.js +1 -1
- package/dist/components/faq/json-ld.js.map +1 -1
- package/dist/components/faq-accordion.d.ts.map +1 -1
- package/dist/components/features/index.cjs +6 -6
- package/dist/components/features/index.js +5 -5
- package/dist/components/index.cjs +89 -85
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +31 -27
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +6 -6
- package/dist/components/navigation/index.js +5 -5
- package/dist/components/onboarding-guides/index.cjs +30 -30
- package/dist/components/onboarding-guides/index.cjs.map +1 -1
- package/dist/components/onboarding-guides/index.js +11 -11
- package/dist/components/related-content/index.cjs +26 -0
- package/dist/components/related-content/index.cjs.map +1 -0
- package/dist/components/related-content/index.d.ts +2 -0
- package/dist/components/related-content/index.d.ts.map +1 -0
- package/dist/components/related-content/index.js +26 -0
- package/dist/components/related-content/index.js.map +1 -0
- package/dist/components/related-content/related-content-section.d.ts +141 -0
- package/dist/components/related-content/related-content-section.d.ts.map +1 -0
- package/dist/components/tickets/index.cjs +73 -73
- package/dist/components/tickets/index.cjs.map +1 -1
- package/dist/components/tickets/index.js +15 -15
- package/dist/components/ui/index.cjs +11 -11
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +13 -13
- package/dist/embed-shims/index.cjs +3 -3
- package/dist/embed-shims/index.cjs.map +1 -1
- package/dist/embed-shims/index.js +5 -5
- package/dist/index.cjs +27 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +55 -39
- package/dist/types/content-ref.d.ts +35 -0
- package/dist/types/content-ref.d.ts.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/content-ref-groups.d.ts +58 -0
- package/dist/utils/content-ref-groups.d.ts.map +1 -0
- package/dist/utils/extract-items.d.ts +18 -0
- package/dist/utils/extract-items.d.ts.map +1 -0
- package/dist/utils/index.cjs +81 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +4 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +74 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/list-url.d.ts +6 -0
- package/dist/utils/list-url.d.ts.map +1 -1
- package/dist/utils/suggestion-url.d.ts +26 -0
- package/dist/utils/suggestion-url.d.ts.map +1 -0
- package/package.json +7 -1
- package/src/components/chat/hooks/use-chat-card-item.ts +3 -38
- package/src/components/chat/types/entities/content-ref.ts +5 -21
- package/src/components/chevron-button.tsx +5 -1
- package/src/components/faq/faq-section.tsx +71 -26
- package/src/components/faq-accordion.tsx +2 -1
- package/src/components/index.ts +1 -0
- package/src/components/related-content/index.ts +9 -0
- package/src/components/related-content/related-content-section.tsx +603 -0
- package/src/types/content-ref.ts +34 -0
- package/src/types/index.ts +1 -0
- package/src/utils/content-ref-groups.ts +89 -0
- package/src/utils/extract-items.ts +48 -0
- package/src/utils/index.ts +16 -1
- package/src/utils/list-url.ts +9 -0
- package/src/utils/suggestion-url.ts +39 -0
- package/dist/chunk-36G2QXYZ.cjs.map +0 -1
- package/dist/chunk-5VU66BD2.cjs.map +0 -1
- package/dist/chunk-FSS2MIEA.cjs.map +0 -1
- package/dist/chunk-JDQY7JCV.js.map +0 -1
- package/dist/chunk-LJGBFD52.js.map +0 -1
- package/dist/chunk-Y7DCQUNL.js.map +0 -1
- package/dist/components/faq/faq-json-ld.d.ts.map +0 -1
- /package/dist/{chunk-PN64LWNT.js.map → chunk-2GMHLKEO.js.map} +0 -0
- /package/dist/{chunk-ORJREQ2W.js.map → chunk-4XLJWX2N.js.map} +0 -0
- /package/dist/{chunk-7LLS3TWN.js.map → chunk-6IFN7F3C.js.map} +0 -0
- /package/dist/{chunk-Y6463TD6.js.map → chunk-FEZSQQTH.js.map} +0 -0
- /package/dist/{chunk-G6YZ7O27.js.map → chunk-Z3657YGL.js.map} +0 -0
- /package/src/components/faq/{faq-json-ld.ts → json-ld.ts} +0 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Ref Group Configuration
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for grouping content_refs by type. Used by both:
|
|
5
|
+
* - components/related-content (detail page rail — lives in this lib)
|
|
6
|
+
* - the hub's lib/utils/investor-email-utils.ts (email builder, via the
|
|
7
|
+
* hub's re-export shim at lib/config/content-ref-groups.ts)
|
|
8
|
+
*
|
|
9
|
+
* Lives in utils/ (server-safe tsup block) so hub server-side code can
|
|
10
|
+
* import it.
|
|
11
|
+
*
|
|
12
|
+
* Each entry carries the metadata RelatedContentSection needs to render the
|
|
13
|
+
* group WITHOUT re-implementing per-type logic locally:
|
|
14
|
+
* - `label`: section heading.
|
|
15
|
+
* - `order`: source-stable ordering (lowest first).
|
|
16
|
+
* - `layout`: 'list' (one-per-row, full-width cards) or 'grid' (responsive
|
|
17
|
+
* column grid).
|
|
18
|
+
* - `gridSize`: card-size variant passed to the per-type card dispatch.
|
|
19
|
+
* `'lg'` matches the canonical large-card variant openframe + flamingo
|
|
20
|
+
* apps already use; `'default'` is the legacy denser variant. New types
|
|
21
|
+
* should pick `'lg'` whenever the card has a large variant.
|
|
22
|
+
*
|
|
23
|
+
* Adding a new content type = one entry here. RelatedContentSection picks up
|
|
24
|
+
* the new group automatically (no hardcoded set to update). NOTE: the hub's
|
|
25
|
+
* related-content SUGGESTION candidate list is DERIVED from these keys (minus
|
|
26
|
+
* an explicit exclusion knob) — a new entry here becomes a suggestion
|
|
27
|
+
* candidate iff it also resolves through the hub's TYPE_TO_ENTITY → RAG
|
|
28
|
+
* config chain (unresolvable entries are dropped loudly at module load).
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
export type ContentRefLayout = 'list' | 'grid'
|
|
32
|
+
|
|
33
|
+
/** Subset of `EntityCardSize` (entity-card dispatch) appropriate for
|
|
34
|
+
* grid/list rendering in RelatedContentSection. Duplicated as a literal
|
|
35
|
+
* union here to avoid a config→component import cycle. Kept in lockstep
|
|
36
|
+
* with `EntityCardSize`; widen here when a new size is added there. */
|
|
37
|
+
export type ContentRefGridSize = 'lg' | 'default' | 'sm'
|
|
38
|
+
|
|
39
|
+
export interface ContentRefGroupConfig {
|
|
40
|
+
label: string
|
|
41
|
+
order: number
|
|
42
|
+
layout: ContentRefLayout
|
|
43
|
+
gridSize: ContentRefGridSize
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const CONTENT_REF_GROUPS: Record<string, ContentRefGroupConfig> = {
|
|
47
|
+
investor_update: { label: 'Investor Updates', order: 1, layout: 'grid', gridSize: 'default' },
|
|
48
|
+
product_release: { label: 'Product Releases', order: 2, layout: 'list', gridSize: 'lg' },
|
|
49
|
+
podcast: { label: 'Podcasts', order: 3, layout: 'list', gridSize: 'default' },
|
|
50
|
+
webinar: { label: 'Webinars', order: 4, layout: 'list', gridSize: 'default' },
|
|
51
|
+
case_study: { label: 'Case Studies', order: 5, layout: 'grid', gridSize: 'default' },
|
|
52
|
+
event: { label: 'Events', order: 6, layout: 'list', gridSize: 'default' },
|
|
53
|
+
blog_post_existing: { label: 'Blog Posts', order: 7, layout: 'grid', gridSize: 'default' },
|
|
54
|
+
customer_interview: { label: 'Customer Interviews', order: 8, layout: 'grid', gridSize: 'default' },
|
|
55
|
+
onboarding_guide: { label: 'Onboarding Guides', order: 9, layout: 'grid', gridSize: 'default' },
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Human-readable label for a content_ref `type`. Returns null when the type
|
|
59
|
+
* isn't registered — caller decides the fallback. Use `getContentRefLabelOrTitleCase`
|
|
60
|
+
* to get a consistent title-cased fallback across all surfaces. */
|
|
61
|
+
export function getContentRefLabel(type: string): string | null {
|
|
62
|
+
return CONTENT_REF_GROUPS[type]?.label ?? null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Resolved label with a single shared fallback shape — title-cased version
|
|
66
|
+
* of the raw type string for any unregistered type. Both RelatedContentSection
|
|
67
|
+
* AND the investor-email builder consume this so cross-surface label rendering
|
|
68
|
+
* for unregistered types is identical (no drift between "podcast guest" and
|
|
69
|
+
* "Podcast Guest"). */
|
|
70
|
+
export function getContentRefLabelOrTitleCase(type: string): string {
|
|
71
|
+
return (
|
|
72
|
+
CONTENT_REF_GROUPS[type]?.label ??
|
|
73
|
+
type.replace(/_/g, ' ').replace(/\b\w/g, (ch) => ch.toUpperCase())
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Sort a set of present content_ref types into the canonical group order
|
|
78
|
+
* (registered types first in CONTENT_REF_GROUPS `order`, then unregistered
|
|
79
|
+
* types appended in insertion order). Used by RelatedContentSection AND the
|
|
80
|
+
* investor-email builder so cross-surface ordering stays identical. */
|
|
81
|
+
export function orderContentRefTypes(present: Iterable<string>): string[] {
|
|
82
|
+
const presentSet = new Set(present)
|
|
83
|
+
const registeredInOrder = Object.entries(CONTENT_REF_GROUPS)
|
|
84
|
+
.sort(([, a], [, b]) => a.order - b.order)
|
|
85
|
+
.map(([type]) => type)
|
|
86
|
+
.filter((type) => presentSet.has(type))
|
|
87
|
+
const unregistered = [...presentSet].filter((type) => !CONTENT_REF_GROUPS[type])
|
|
88
|
+
return [...registeredInOrder, ...unregistered]
|
|
89
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List-API response normalizers — HOISTED from
|
|
3
|
+
* `components/chat/hooks/use-chat-card-item.ts` (private copies) so the
|
|
4
|
+
* related-content rail can import them WITHOUT touching the chat hooks
|
|
5
|
+
* chunk (which reaches `@tanstack/react-query`). Pure + server-safe.
|
|
6
|
+
*
|
|
7
|
+
* The hub's `lib/utils/entity-list-api.ts` re-exports these — one
|
|
8
|
+
* implementation of response-shape normalization across both repos.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Extract the items array from each endpoint's response shape (some
|
|
12
|
+
* return `{ items }`, others `{ posts }`, etc.). Single normalization
|
|
13
|
+
* point — callers always read `Item[]`. */
|
|
14
|
+
export function extractItems(data: unknown): unknown[] {
|
|
15
|
+
if (!data || typeof data !== 'object') return []
|
|
16
|
+
const obj = data as Record<string, unknown>
|
|
17
|
+
if (Array.isArray(obj.items)) return obj.items
|
|
18
|
+
if (Array.isArray(obj.posts)) return obj.posts
|
|
19
|
+
if (Array.isArray(obj.campaigns)) return obj.campaigns
|
|
20
|
+
if (Array.isArray(obj.completed) || Array.isArray(obj.inProgress)) {
|
|
21
|
+
// Delivery endpoint splits into completed/inProgress arrays — flatten.
|
|
22
|
+
const completed = Array.isArray(obj.completed) ? obj.completed : []
|
|
23
|
+
const inProgress = Array.isArray(obj.inProgress) ? obj.inProgress : []
|
|
24
|
+
return [...completed, ...inProgress]
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(obj.data)) return obj.data
|
|
27
|
+
if (Array.isArray(data)) return data
|
|
28
|
+
return []
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Extract a stable id from a fetched item — different shapes use
|
|
32
|
+
* `id` vs `external_id`. Used by the chat loader to match the fetched
|
|
33
|
+
* array element back to the marker's id. */
|
|
34
|
+
export function extractItemId(type: string, item: unknown): string | null {
|
|
35
|
+
if (!item || typeof item !== 'object') return null
|
|
36
|
+
const obj = item as Record<string, unknown>
|
|
37
|
+
if (type === 'roadmap_item' || type === 'delivery_item' || type === 'internal_task') {
|
|
38
|
+
const ext = obj.external_id
|
|
39
|
+
if (typeof ext === 'string') return ext
|
|
40
|
+
}
|
|
41
|
+
// RoadmapItem-shaped responses use `id` to carry the external_id; the
|
|
42
|
+
// mapper renames external_id→id at the API boundary. Treat both id and
|
|
43
|
+
// external_id as primary-key candidates for these clickup-backed types.
|
|
44
|
+
const id = obj.id
|
|
45
|
+
if (typeof id === 'string') return id
|
|
46
|
+
if (typeof id === 'number') return String(id)
|
|
47
|
+
return null
|
|
48
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -235,7 +235,22 @@ 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 } from './list-url'
|
|
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'
|
|
239
254
|
|
|
240
255
|
// Embedder-configurable content-URL composer for the existing
|
|
241
256
|
// `runtime.composeContentUrl` seam — relative href for host-served types,
|
package/src/utils/list-url.ts
CHANGED
|
@@ -37,6 +37,15 @@
|
|
|
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
|
+
|
|
40
49
|
/**
|
|
41
50
|
* One builder per fetch-mode `contentRefType`, keyed by the canonical
|
|
42
51
|
* `documentType`. Each body is copied VERBATIM from the matching hub
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared fetch-URL composer for the two suggestion sections (FaqSection +
|
|
3
|
+
* RelatedContentSection) — ONE owner of the entityType/entityId/count query
|
|
4
|
+
* shape so the sections can never drift. Pure + server-safe.
|
|
5
|
+
*
|
|
6
|
+
* Byte-parity contract with the original `buildFaqsUrl`: when only
|
|
7
|
+
* entityType/entityId are set, the param order is `entityType`, `entityId`;
|
|
8
|
+
* with nothing set, the bare `path` is returned (no `?`).
|
|
9
|
+
*/
|
|
10
|
+
export interface SuggestionUrlOptions {
|
|
11
|
+
/** Fetch-URL prefix for third-party embeds / reverse proxies
|
|
12
|
+
* ('' = same-origin relative). */
|
|
13
|
+
apiBaseUrl?: string
|
|
14
|
+
/** Both required together for entity scope; partial → bare path. */
|
|
15
|
+
entityType?: string
|
|
16
|
+
entityId?: number | string
|
|
17
|
+
/** Maps to the server's `count` param. Undefined → param not sent
|
|
18
|
+
* (server default applies). */
|
|
19
|
+
count?: number
|
|
20
|
+
/** Extra query params appended after the shared trio (e.g. the
|
|
21
|
+
* related-content `excludeTypes` deny-list). Empty/undefined values
|
|
22
|
+
* are skipped. */
|
|
23
|
+
extraParams?: Record<string, string | undefined>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildSuggestionUrl(path: string, opts: SuggestionUrlOptions = {}): string {
|
|
27
|
+
const qs = new URLSearchParams()
|
|
28
|
+
const { entityType, entityId } = opts
|
|
29
|
+
if (entityType && entityId !== undefined && entityId !== null && entityId !== '') {
|
|
30
|
+
qs.set('entityType', entityType)
|
|
31
|
+
qs.set('entityId', String(entityId))
|
|
32
|
+
}
|
|
33
|
+
if (opts.count !== undefined) qs.set('count', String(opts.count))
|
|
34
|
+
for (const [key, value] of Object.entries(opts.extraParams ?? {})) {
|
|
35
|
+
if (value !== undefined && value !== '') qs.set(key, value)
|
|
36
|
+
}
|
|
37
|
+
const query = qs.toString()
|
|
38
|
+
return `${opts.apiBaseUrl ?? ''}${path}${query ? `?${query}` : ''}`
|
|
39
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-36G2QXYZ.cjs","../src/components/faq-accordion.tsx","../src/components/faq/faq-json-ld.ts","../src/components/faq/faq-section.tsx"],"names":["jsx"],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-36G2QXYZ.cjs","sourcesContent":[null,"\"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\" aria-label={isOpen ? 'Collapse question' : 'Expand question'}>\n <ChevronButton\n size=\"md\"\n isExpanded={isOpen}\n backgroundColor=\"transparent\"\n borderColor=\"#3A3A3A\"\n />\n </div>\n </div>\n {/* Content wrapper with max-height animation */}\n <div\n style={{ maxHeight, transition: 'max-height 0.35s ease-in-out, opacity 0.35s ease-in-out', opacity: isOpen ? 1 : 0 }}\n className=\"overflow-hidden group-hover:bg-[#1E1E1E]/30\"\n >\n <div ref={ref} className=\"px-6 md:px-8 pb-6 text-ods-text-primary text-h4\">\n {item.answer}\n </div>\n </div>\n </div>\n )\n })}\n </div>\n )\n} ","/**\n * Pure FAQ JSON-LD builder. No React, no client-only deps — safe to import from\n * Server Components via the `./components/faq` subpath export. (Do NOT import\n * through the root `./components` barrel — that barrel is `\"use client\"` and\n * dragging it into a Server Component would force the client graph into the\n * server.)\n *\n * The hub used to harcode `name`/`description` here for OpenMSP; in the lib we\n * accept overrides so every embedder can supply its own platform branding.\n */\nimport type { Faq } from '../../types/faq'\n\nexport interface FaqSchemaOptions {\n name?: string\n description?: string\n url?: string\n}\n\nconst DEFAULT_NAME = 'Frequently Asked Questions'\nconst DEFAULT_DESCRIPTION =\n 'Answers to common questions.'\n\nexport function baseFaqSchema(opts: FaqSchemaOptions = {}) {\n return {\n '@context': 'https://schema.org',\n '@type': 'FAQPage',\n name: opts.name ?? DEFAULT_NAME,\n description: opts.description ?? DEFAULT_DESCRIPTION,\n ...(opts.url ? { url: opts.url } : {}),\n } as const\n}\n\nexport function buildFaqJsonLdFromFaqs(faqs: Faq[], opts: FaqSchemaOptions = {}) {\n return {\n ...baseFaqSchema(opts),\n mainEntity: faqs.map((faq) => ({\n '@type': 'Question',\n name: faq.question,\n acceptedAnswer: {\n '@type': 'Answer',\n text: faq.answer,\n },\n })),\n } as const\n}\n","\"use client\"\n\nimport React from 'react'\nimport type { Faq } from '../../types/faq'\nimport { FaqAccordion, type FaqItem } from '../faq-accordion'\nimport { useSelfFetch } from '../../hooks/use-self-fetch'\nimport { buildFaqJsonLdFromFaqs, type FaqSchemaOptions } from './faq-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}\n\nconst DEFAULT_HEADING_TEXT = 'Frequently Asked Questions'\n\nfunction buildFaqsUrl(entityType?: string, entityId?: number | string): string {\n if (!entityType || entityId === undefined || entityId === null || entityId === '') {\n return '/api/faqs'\n }\n const qs = new URLSearchParams({ entityType, entityId: String(entityId) })\n return `/api/faqs?${qs.toString()}`\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-[48px] md:h-[56px] 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}: FaqSectionProps) {\n const url = buildFaqsUrl(entityType, entityId)\n const initialData = initialFaqs ? { faqs: initialFaqs } : undefined\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 if (isLoading && faqs.length === 0) {\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 return (\n <>\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 {showHeading && headingNode}\n {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 </div>\n </main>\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"]}
|