{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-S2PTGH6H.cjs","../src/components/faq/faq-section.tsx","../src/components/faq-accordion.tsx","../src/components/faq/json-ld.ts","../src/components/faq/faq-document-page.tsx"],"names":["jsx","jsxs"],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-S2PTGH6H.cjs","sourcesContent":[null,"\"use client\"\n\nimport React, { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { Faq } from '../../types/faq'\nimport { FaqAccordion, type FaqItem } from '../faq-accordion'\nimport { useSelfFetch } from '../../hooks/use-self-fetch'\nimport { buildSuggestionUrl } from '../../utils/suggestion-url'\nimport { serializeJsonLd } from '../../utils/common'\nimport { useScrollToHash } from '../../hooks/use-scroll-to-hash'\nimport { navigateSamePageHash, STICKY_HEADER_OFFSET_PX } from '../../utils/same-page-hash-nav'\nimport { faqSectionSlug, parseFaqHash, type FaqHashTarget } from '../../utils/faq-anchor'\nimport { cn } from '../../utils/cn'\nimport { buildFaqJsonLdFromFaqs, type FaqSchemaOptions } from './json-ld'\nimport { SECTION_HEADING_CLASS } from '../layout/page-heading'\n\nexport interface FaqSectionProps {\n  /**\n   * SSR hydrate. When provided, the hook skips the first client fetch (per\n   * useSelfFetch contract). The consuming server page resolves FAQs then drills\n   * them into this prop — the lib never re-fetches what the host already gated on.\n   */\n  initialFaqs?: Faq[]\n  /** Both required together for entity-attached FAQs; partial → bare /api/faqs. */\n  entityType?: string\n  entityId?: number | string\n  /**\n   * Heading node above the grouped list. `undefined` → default\n   * `<h2>`\"Frequently Asked Questions\". `null` → no heading (the host page\n   * owns the `<h1>`, as the standalone /faqs surface does). A React node lets a\n   * platform drill its own <PageHeading> without the lib referencing platform\n   * state. Also drives category nesting so the document outline stays correct:\n   * `null` → categories render `<h2>` (directly under the page `<h1>`);\n   * otherwise categories render `<h3>` beneath this heading.\n   */\n  heading?: React.ReactNode | null\n  /** Inject FAQPage schema.org JSON-LD as a <script>. Off by default so embeds\n   *  don't emit duplicate schema. */\n  emitJsonLd?: boolean\n  /** Overrides for the JSON-LD's name/description/url. */\n  jsonLd?: FaqSchemaOptions\n  className?: string\n  /** Maps to /api/faqs `?count=` (the 5-tier fill target). Absent → param\n   *  not sent (server default applies). */\n  minResults?: number\n  /** Fetch-URL prefix for third-party embeds / reverse proxies\n   *  ('' = same-origin relative). */\n  apiBaseUrl?: string\n}\n\nconst DEFAULT_HEADING_TEXT = 'Frequently Asked Questions'\n\n/** URL composition shared with RelatedContentSection (`buildSuggestionUrl`)\n *  — byte-identical to the historical `buildFaqsUrl` output when\n *  `minResults`/`apiBaseUrl` are absent. */\nfunction buildFaqsUrl(\n  entityType?: string,\n  entityId?: number | string,\n  minResults?: number,\n  apiBaseUrl = '',\n): string {\n  return buildSuggestionUrl('/api/faqs', { apiBaseUrl, entityType, entityId, count: minResults })\n}\n\ninterface FaqGroup {\n  /** null → the uncategorized bucket: no heading, no jump pill, rendered last. */\n  section: string | null\n  slug: string | null\n  items: FaqItem[]\n}\n\n/** Group FAQs by `faq.section`, preserving the server's first-seen\n *  (display_order) order for BOTH the section order and the rows within each\n *  section. The uncategorized bucket (blank/missing section) sinks to the end\n *  since it renders without a heading. Items carry NO badge here — the `<h2>`\n *  IS the category, so a per-row chip would be redundant. */\nfunction groupFaqsBySection(faqs: Faq[]): FaqGroup[] {\n  const order: string[] = []\n  const byName = new Map<string, FaqGroup>()\n  let uncategorized: FaqGroup | null = null\n  for (const faq of faqs) {\n    const item: FaqItem = { id: faq.id, question: faq.question, answer: faq.answer }\n    const name = faq.section?.trim()\n    if (!name) {\n      if (!uncategorized) uncategorized = { section: null, slug: null, items: [] }\n      uncategorized.items.push(item)\n      continue\n    }\n    let group = byName.get(name)\n    if (!group) {\n      group = { section: name, slug: faqSectionSlug(name), items: [] }\n      byName.set(name, group)\n      order.push(name)\n    }\n    group.items.push(item)\n  }\n  const groups = order.map((name) => byName.get(name)!)\n  if (uncategorized) groups.push(uncategorized)\n  return groups\n}\n\n\n/** Map key for the uncategorized bucket — `group.slug` is null for it, so\n *  every per-group map (default-open ids, accordion keys) uses this sentinel\n *  to keep the lookup typed. */\nconst UNCATEGORIZED_KEY = '__uncategorized__'\nconst groupKey = (g: FaqGroup): string => g.slug ?? UNCATEGORIZED_KEY\n\n/**\n * Grouped FAQ layout: a category jump-nav above stacked `<h2>` category\n * sections (each its own accordion). Isolated into its own component so the\n * scroll-spy hooks only mount in grouped mode — `FaqSection`'s own hooks stay\n * unconditional.\n *\n * The pills are real `<a href=\"#slug\">` anchors (crawlable in-page links,\n * deep-linkable, work without JS); the click handler upgrades the jump to the\n * cancellation-proof `scrollElementIntoView` tween and syncs the URL hash.\n */\nfunction GroupedFaqList({\n  groups,\n  categoryHeadingAs,\n}: {\n  groups: FaqGroup[]\n  /** Heading tag for each category, so the document outline nests correctly\n   *  under whatever owns the heading above this block: `h2` on the standalone\n   *  page (the page owns the `<h1>`), `h3` beneath an embed's `<h2>` title.\n   *  The VISUAL is `SECTION_HEADING_CLASS` either way, so categories look\n   *  identical on every surface. */\n  categoryHeadingAs: 'h2' | 'h3'\n}) {\n  const CategoryHeading = categoryHeadingAs\n  const navGroups = useMemo(() => groups.filter((g) => g.slug), [groups])\n  const [activeSlug, setActiveSlug] = useState<string | null>(navGroups[0]?.slug ?? null)\n  // Identity-stable key for the section set so the observer re-binds only when\n  // the categories actually change (not on every parent re-render).\n  const slugKey = navGroups.map((g) => g.slug).join('|')\n\n  // Scroll-spy: mark the pill for the category currently at the top of the\n  // viewport. rootMargin drops the trigger line just below the sticky header\n  // and ignores the bottom ~55% so \"active\" is the section being read, not the\n  // next one peeking in.\n  useEffect(() => {\n    if (navGroups.length < 2) return\n    const tops = new Map<string, number>()\n    const observer = new IntersectionObserver(\n      (entries) => {\n        for (const entry of entries) {\n          const id = (entry.target as HTMLElement).id\n          if (entry.isIntersecting) tops.set(id, entry.boundingClientRect.top)\n          else tops.delete(id)\n        }\n        let bestId: string | null = null\n        let bestTop = Number.POSITIVE_INFINITY\n        for (const [id, top] of tops) {\n          if (top < bestTop) {\n            bestTop = top\n            bestId = id\n          }\n        }\n        if (bestId) setActiveSlug(bestId)\n      },\n      { rootMargin: `-${STICKY_HEADER_OFFSET_PX}px 0px -55% 0px`, threshold: 0 },\n    )\n    for (const group of navGroups) {\n      const el = group.slug ? document.getElementById(group.slug) : null\n      if (el) observer.observe(el)\n    }\n    return () => observer.disconnect()\n    // slugKey encodes the section set; re-observe only when it changes.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [slugKey])\n\n  // ─── Hash dispatch — `/faqs#faq-item-<id>` or `/faqs#faq-<section-slug>` ──\n  // Tracks the current hash so:\n  //   1. an item-kind hash seeds `defaultOpenIds` on the matching accordion\n  //      (auto-expands the cited question);\n  //   2. either kind triggers the cancellation-proof tween scroll with the\n  //      sticky-header offset (native browser hash scroll runs once, ignores\n  //      our offset — re-running the tween puts the target in the right spot).\n  // Listens to `hashchange` so back/forward replays the same behavior. SSR-\n  // safe: initial null state matches the server render; the first effect\n  // tick on the client updates it.\n  const [hashTarget, setHashTarget] = useState<FaqHashTarget | null>(null)\n  useEffect(() => {\n    const refresh = () => setHashTarget(parseFaqHash(window.location.hash))\n    refresh()\n    window.addEventListener('hashchange', refresh)\n    return () => window.removeEventListener('hashchange', refresh)\n  }, [])\n\n  // Per-group default-open set when the hash points at an item. The map key\n  // matches `groupKey(group)` so the render-time lookup is O(1) per group.\n  const defaultOpenByGroupKey = useMemo(() => {\n    if (hashTarget?.kind !== 'item') return null\n    const targetId = hashTarget.rawId\n    const result = new Map<string, (string | number)[]>()\n    for (const group of groups) {\n      const hit = group.items.find((i) => String(i.id) === targetId)\n      if (hit) result.set(groupKey(group), [hit.id])\n    }\n    return result.size > 0 ? result : null\n  }, [groups, hashTarget])\n\n  // Accordion is uncontrolled — `defaultOpenIds` is only consumed at mount,\n  // so a new item hash needs a remount to honor it. Keying off the item-id\n  // suffix triggers exactly the remount we need (and stays stable when the\n  // hash points at a section, so category navigation never disturbs the\n  // accordion's open state).\n  const accordionKeySuffix =\n    hashTarget?.kind === 'item' ? `item:${hashTarget.rawId}` : 'default'\n\n  // Unified scroll-to-hash: rAF-polls for the target row (outlasts the SWR self-fetch\n  // + Radix accordion expand) and re-fires on `hashchange` — including the synthetic\n  // event `navigateSamePageHash` dispatches for a SOFT same-page chat-card nav (a\n  // host-router `pushState` fires no `hashchange`). The ready-dep keys off BOTH the\n  // loaded `groups` (so a deep-link / refresh scrolls once the self-fetched list\n  // mounts) AND `hashTarget` (so a soft nav re-runs the scroll AFTER the re-render\n  // that remounts + opens the cited row, landing on its FINAL expanded position\n  // instead of racing the uncontrolled accordion's key-remount — the bare-`hashchange`\n  // listener alone fires before the remount and lands on the stale closed position).\n  const scrollDep =\n    `${groups.length}|` +\n    (hashTarget?.kind === 'item'\n      ? `item:${hashTarget.rawId}`\n      : hashTarget?.kind === 'section'\n        ? `section:${hashTarget.slug}`\n        : 'none')\n  useScrollToHash(scrollDep, { headerOffset: STICKY_HEADER_OFFSET_PX })\n\n  // A section hash also lights up its category pill; item hashes leave the pills\n  // untouched so deep-linking a question never disturbs scroll-spy state.\n  useEffect(() => {\n    if (hashTarget?.kind === 'section') setActiveSlug(hashTarget.slug)\n  }, [hashTarget])\n\n  // Category pill click. `navigateSamePageHash` owns the entire transition:\n  // replaceState → synthetic `hashchange` → `scrollElementIntoView` tween\n  // with `STICKY_HEADER_OFFSET_PX` so the section heading lands BELOW the\n  // sticky category nav on the FIRST tween (covers the same-target\n  // re-click case, where the `hashTarget` effect at L214 is a no-op\n  // because the state reference is equal). For DIFFERENT-target clicks\n  // the helper's synthetic `hashchange` re-fires that effect, which\n  // re-scrolls with the same offset and cancels this tween (singleton)\n  // — both paths land at the same position. The effect is still\n  // required for back/forward + direct URL edits, where the helper\n  // isn't in the call chain.\n  //\n  // `history: 'replace'` matches the pre-helper behavior: category pills\n  // are a TOC, not a navigation step, so the Back button leaves the\n  // FAQ page in one step regardless of how many categories the user\n  // clicked through.\n  const handleJump = useCallback(\n    (e: React.MouseEvent<HTMLAnchorElement>, slug: string) => {\n      e.preventDefault()\n      navigateSamePageHash('#' + slug, {\n        headerOffset: STICKY_HEADER_OFFSET_PX,\n        history: 'replace',\n      })\n    },\n    [],\n  )\n\n  return (\n    <div className=\"space-y-8\">\n      {navGroups.length > 1 && (\n        <nav aria-label=\"FAQ categories\" className=\"flex flex-wrap gap-2\">\n          {navGroups.map((group) => {\n            const isActive = group.slug === activeSlug\n            return (\n              <a\n                key={group.slug}\n                href={`#${group.slug}`}\n                aria-current={isActive ? 'true' : undefined}\n                onClick={(e) => handleJump(e, group.slug as string)}\n                className={cn(\n                  \"rounded-full border px-4 py-2 text-h6 transition-colors\",\n                  isActive\n                    ? 'border-ods-text-primary bg-ods-card text-ods-text-primary'\n                    : 'border-ods-border bg-ods-card text-ods-text-secondary hover:border-ods-text-secondary hover:text-ods-text-primary',\n                )}\n              >\n                {group.section}\n              </a>\n            )\n          })}\n        </nav>\n      )}\n      <div className=\"space-y-10\">\n        {groups.map((group) => {\n          const key = groupKey(group)\n          return (\n            <section\n              key={key}\n              id={group.slug ?? undefined}\n              className=\"scroll-mt-24 space-y-4\"\n            >\n              {group.section && (\n                <CategoryHeading className={SECTION_HEADING_CLASS}>{group.section}</CategoryHeading>\n              )}\n              <FaqAccordion\n                // Re-key on item-hash changes so the remount picks up the new\n                // `defaultOpenIds` (the accordion is uncontrolled). Stable for\n                // section hashes — category navigation doesn't disturb state.\n                key={`${key}:${accordionKeySuffix}`}\n                items={group.items}\n                defaultOpenIds={defaultOpenByGroupKey?.get(key)}\n              />\n            </section>\n          )\n        })}\n      </div>\n    </div>\n  )\n}\n\nfunction FaqSkeleton() {\n  return (\n    <div className=\"space-y-8 animate-pulse\">\n      <div className=\"h-12 md:h-14 w-2/3 rounded bg-ods-border\" />\n      <div className=\"rounded-md border border-ods-border overflow-hidden bg-transparent divide-y divide-ods-border w-full\">\n        {Array.from({ length: 8 }).map((_, idx) => (\n          <div key={idx} className=\"flex items-center justify-between gap-6 md:gap-10 px-6 py-4\">\n            <div className=\"h-6 w-5/6 rounded bg-ods-border\" />\n            <div className=\"size-6 rounded bg-ods-border\" />\n          </div>\n        ))}\n      </div>\n    </div>\n  )\n}\n\ninterface FaqsResponse {\n  faqs: Faq[]\n}\n\n/**\n * The FAQ display surface — ONE rendering on every host: the list grouped by\n * `faq.section` (each category a heading + its own accordion, with a category\n * jump-nav once there are 2+ categories). There is no flat/ungrouped mode and\n * no page-vs-embedded shell fork; the standalone /faqs page and every embed\n * render through this single path, so they cannot drift.\n *\n * - Standalone /faqs page: pass `initialFaqs` (SSR) + `heading={null}` (the\n *   page owns the <h1>) + `emitJsonLd` with `jsonLd` overrides for SEO.\n * - Per-entity embed: pass `entityType` + `entityId` (no `initialFaqs`); the\n *   hook self-fetches `GET /api/faqs`, and `heading` is this block's own <h2>.\n *\n * CONTRACT: the consuming app MUST implement `GET /api/faqs`. On a fetch error\n * (or zero FAQs) the component renders nothing so the host page isn't\n * disfigured. The host always supplies the page shell — this renders a bare\n * <section>.\n */\nexport function FaqSection({\n  initialFaqs,\n  entityType,\n  entityId,\n  heading,\n  emitJsonLd = false,\n  jsonLd,\n  className,\n  minResults,\n  apiBaseUrl = '',\n}: FaqSectionProps) {\n  const url = buildFaqsUrl(entityType, entityId, minResults, apiBaseUrl)\n  // Memoized — useSelfFetch re-syncs on [initialData]; a fresh per-render\n  // wrapper object would setState-loop under re-rendering parents.\n  const initialData = useMemo<FaqsResponse | undefined>(\n    () => (initialFaqs ? { faqs: initialFaqs } : undefined),\n    [initialFaqs],\n  )\n  const { data, isLoading, error } = useSelfFetch<FaqsResponse>(url, { initialData })\n\n  const faqs = data?.faqs ?? []\n  // Grouped before the early returns so the hook order stays stable.\n  const groups = useMemo(() => (faqs.length > 0 ? groupFaqsBySection(faqs) : []), [faqs])\n\n  // `undefined` → default <h2> title; `null` → the host page owns the <h1>, so\n  // no title renders here. `heading === null` also makes the category headings\n  // <h2> (directly under the page <h1>); otherwise they nest as <h3>.\n  const headingNode =\n    heading === undefined ? <h2 className={SECTION_HEADING_CLASS}>{DEFAULT_HEADING_TEXT}</h2> : heading\n\n  // Degrade silently — never show an error banner or an empty section shell\n  // where FAQs would be (host pages and the standalone surface both rely on it).\n  if (error) return null\n  if (!isLoading && faqs.length === 0) return null\n  if (isLoading && faqs.length === 0) {\n    return (\n      <div className={className}>\n        <FaqSkeleton />\n      </div>\n    )\n  }\n\n  const schema = emitJsonLd ? buildFaqJsonLdFromFaqs(faqs, jsonLd) : null\n\n  return (\n    <>\n      <section className={className ?? 'space-y-10'}>\n        {headingNode}\n        <GroupedFaqList groups={groups} categoryHeadingAs={heading === null ? 'h2' : 'h3'} />\n      </section>\n      {schema && (\n        <script\n          type=\"application/ld+json\"\n          // eslint-disable-next-line react/no-danger\n          // serializeJsonLd, NOT raw JSON.stringify — FAQ answers are\n          // admin-entered; an embedded \"</script>\" must not break the tag.\n          dangerouslySetInnerHTML={{ __html: serializeJsonLd(schema) }}\n        />\n      )}\n    </>\n  )\n}\n","\"use client\"\n\nimport React, { useRef, useState, useEffect, useCallback } from 'react'\nimport { Chevron02DownIcon } from './icons-v2-generated/arrows/chevron-02-down-icon'\nimport { cn } from \"../utils/cn\"\nimport { faqItemAnchor } from \"../utils/faq-anchor\"\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-md border border-ods-border divide-y divide-ods-border bg-transparent 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            // Per-row anchor — chat citation chips (`/faqs#faq-item-<id>`) land\n            // here via native browser hash scroll AND via `FaqSection`'s tween\n            // dispatch. `scroll-mt-24` keeps the row header below the 96px\n            // sticky nav offset (matches `<section>`'s scroll-margin for\n            // category anchors).\n            id={faqItemAnchor(item.id)}\n            className=\"scroll-mt-24 transition-colors hover:bg-ods-bg-hover\"\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 gap-6 md:gap-10 px-6 py-4 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ods-focus transition-colors cursor-pointer\"\n            >\n              <h3 className=\"flex-1 min-w-0 break-words\">\n                {item.question}\n              </h3>\n              <Chevron02DownIcon\n                aria-hidden=\"true\"\n                size={24}\n                className={cn(\n                  'shrink-0 text-ods-text-primary transition-transform duration-300',\n                  isOpen && 'rotate-180',\n                )}\n              />\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\"\n            >\n              {/* break-words: FAQ answers render as plain text, so a long URL or\n                  token has no wrap opportunity — and the parent is overflow-hidden,\n                  which would CLIP it past the viewport on mobile. Mirrors the\n                  markdown-renderer overflow-wrap fix. */}\n              <div ref={ref} className=\"px-6 pb-4 text-ods-text-primary text-h4 break-words\">\n                {item.answer}\n              </div>\n            </div>\n          </div>\n        )\n      })}\n    </div>\n  )\n} ","/**\n * Pure FAQ JSON-LD builder. No React, no client-only deps — safe to import from\n * Server Components via the `./components/faq` subpath export. (Do NOT import\n * through the root `./components` barrel — that barrel is `\"use client\"` and\n * dragging it into a Server Component would force the client graph into the\n * server.)\n *\n * The hub used to harcode `name`/`description` here for OpenMSP; in the lib we\n * accept overrides so every embedder can supply its own platform branding.\n */\nimport type { Faq } from '../../types/faq'\n\nexport interface FaqSchemaOptions {\n  name?: string\n  description?: string\n  url?: string\n}\n\nconst DEFAULT_NAME = 'Frequently Asked Questions'\nconst DEFAULT_DESCRIPTION =\n  'Answers to common questions.'\n\nexport function baseFaqSchema(opts: FaqSchemaOptions = {}) {\n  return {\n    '@context': 'https://schema.org',\n    '@type': 'FAQPage',\n    name: opts.name ?? DEFAULT_NAME,\n    description: opts.description ?? DEFAULT_DESCRIPTION,\n    ...(opts.url ? { url: opts.url } : {}),\n  } as const\n}\n\nexport function buildFaqJsonLdFromFaqs(faqs: Faq[], opts: FaqSchemaOptions = {}) {\n  return {\n    ...baseFaqSchema(opts),\n    mainEntity: faqs.map((faq) => ({\n      '@type': 'Question',\n      name: faq.question,\n      acceptedAnswer: {\n        '@type': 'Answer',\n        text: faq.answer,\n      },\n    })),\n  } as const\n}\n","'use client';\n\n/**\n * FaqDocumentPage — the full `/faqs` page with chrome, so embedders drop in\n * ONE component instead of hand-assembling `PageShell` + `PageLayout` around\n * the bare `<FaqSection>`. Mirrors `LegalDocumentPage` / `DevSectionPage`: the\n * page-level layout lives in the lib, the host page is a thin wrapper that\n * passes only config + the SSR-resolved data.\n *\n * HERO: the title + subtitle route through the canonical (frozen) `PageLayout`\n * `TitleBlock` with `titleSize=\"h1\"` — the unified Help Center header (the\n * `TitleBlock` element is always an `<h1>`; `titleSize` only sets its typography,\n * here `text-h1`). The page owns that `<h1>`, so `<FaqSection heading={null}>`\n * nests its category headings as `<h2>`/`<h3>` (the SEO-recommended FAQ document\n * outline).\n *\n * DATA: pass `initialFaqs` (SSR-resolved, platform-scoped) so `FaqSection`\n * skips its client self-fetch — the standalone page's static platform-only\n * contract. Omit it for embeds that want the self-fetching `/api/faqs`\n * suggestion-fill instead.\n */\n\nimport type { Faq } from '../../types/faq';\nimport { PageShell, PageLayout } from '../ui';\nimport { useRouter } from '../../embed-shims/next-navigation';\nimport { FaqSection } from './faq-section';\nimport type { FaqSchemaOptions } from './json-ld';\n\nexport interface FaqDocumentPageProps {\n  /** Page title (frozen `PageLayout` `TitleBlock`). Default \"Frequently Asked Questions\". */\n  title?: string;\n  /** Subtitle under the title. */\n  subtitle?: string;\n  /** Back-button config — same pattern as `DevSectionPage` / `LegalDocumentPage`.\n   *  Pass `false` to hide. Default `{ label: 'Back to home', href: '/' }`. */\n  backButton?: { label?: string; href?: string } | false;\n  /** Render the standalone `<PageShell>` (own `<main>` + bg + max-width). Default\n   *  true. Pass false when the host layout already provides the page container\n   *  (e.g. openframe-frontend's `AppLayout` `<main>`) — only the padding box\n   *  renders, avoiding a nested `<main>`. */\n  shell?: boolean;\n  /** SSR-hydrate `FaqSection` (skips the client fetch — the platform-only\n   *  contract for the standalone page). Omit for self-fetching embeds. */\n  initialFaqs?: Faq[];\n  /** Emit FAQPage schema.org JSON-LD (forwarded to `FaqSection`). Off by\n   *  default so embeds don't emit duplicate schema. */\n  emitJsonLd?: boolean;\n  /** JSON-LD name/description/url overrides (forwarded to `FaqSection`). */\n  jsonLd?: FaqSchemaOptions;\n  /** Base URL `FaqSection` appends `/api/faqs` to (reverse-proxy embedders). */\n  apiBaseUrl?: string;\n  /** Optional entity scoping forwarded to `FaqSection`. */\n  entityType?: string;\n  entityId?: number | string;\n  /** Minimum FAQ count before the section renders (forwarded to `FaqSection`). */\n  minResults?: number;\n}\n\nexport function FaqDocumentPage({\n  title = 'Frequently Asked Questions',\n  subtitle,\n  backButton,\n  initialFaqs,\n  emitJsonLd,\n  jsonLd,\n  apiBaseUrl,\n  entityType,\n  entityId,\n  minResults,\n  shell = true,\n}: FaqDocumentPageProps) {\n  const router = useRouter();\n\n  // Back-button config — mirrors LegalDocumentPage / DevSectionPage. Hide\n  // entirely when the caller passes `false` (embed-mode where the host owns\n  // nav chrome).\n  const backCfg =\n    backButton === false\n      ? undefined\n      : {\n          label: backButton?.label ?? 'Back to home',\n          onClick: () => router.push(backButton?.href ?? '/'),\n        };\n\n  const inner = (\n    <PageLayout title={title} subtitle={subtitle} backButton={backCfg} titleSize=\"h1\" titleWrap>\n      <FaqSection\n        initialFaqs={initialFaqs}\n        heading={null}\n        emitJsonLd={emitJsonLd}\n        jsonLd={jsonLd}\n        apiBaseUrl={apiBaseUrl}\n        entityType={entityType}\n        entityId={entityId}\n        minResults={minResults}\n      />\n    </PageLayout>\n  );\n\n  return shell ? <PageShell>{inner}</PageShell> : <div className=\"page-shell-content\">{inner}</div>;\n}\n"]}