@flamingo-stack/openframe-frontend-core 0.0.274 → 0.0.275

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/dist/{chunk-XDGDCO3I.cjs → chunk-5LOLBUZG.cjs} +45 -44
  2. package/dist/chunk-5LOLBUZG.cjs.map +1 -0
  3. package/dist/{chunk-4YKGXZ7W.js → chunk-7AH75KFQ.js} +1 -1
  4. package/dist/chunk-7AH75KFQ.js.map +1 -0
  5. package/dist/{chunk-CYMMGPPV.js → chunk-CTBL56EC.js} +41 -5
  6. package/dist/chunk-CTBL56EC.js.map +1 -0
  7. package/dist/{chunk-2KHFSSFZ.cjs → chunk-DBJZF2OV.cjs} +15 -15
  8. package/dist/{chunk-2KHFSSFZ.cjs.map → chunk-DBJZF2OV.cjs.map} +1 -1
  9. package/dist/{chunk-4DYXV3E7.cjs → chunk-DEAWHNYW.cjs} +1177 -1223
  10. package/dist/chunk-DEAWHNYW.cjs.map +1 -0
  11. package/dist/{chunk-WU6PMH5L.cjs → chunk-E6RCVOQG.cjs} +41 -43
  12. package/dist/chunk-E6RCVOQG.cjs.map +1 -0
  13. package/dist/{chunk-FLIWL6V4.js → chunk-EINXNG5D.js} +4 -4
  14. package/dist/{chunk-OPSZNW3I.js → chunk-EKWPIEHH.js} +3031 -3077
  15. package/dist/chunk-EKWPIEHH.js.map +1 -0
  16. package/dist/{chunk-N5MCKGAB.js → chunk-FDHKJJPK.js} +34 -33
  17. package/dist/{chunk-N5MCKGAB.js.map → chunk-FDHKJJPK.js.map} +1 -1
  18. package/dist/{chunk-BSOHVQ7X.js → chunk-GHWOQUB4.js} +366 -391
  19. package/dist/chunk-GHWOQUB4.js.map +1 -0
  20. package/dist/{chunk-T7AJBKZP.cjs → chunk-GVSTRN36.cjs} +5 -2
  21. package/dist/chunk-GVSTRN36.cjs.map +1 -0
  22. package/dist/{chunk-L4LAAA5Q.js → chunk-IMNGGLKL.js} +5 -2
  23. package/dist/chunk-IMNGGLKL.js.map +1 -0
  24. package/dist/{chunk-VASNLWPZ.js → chunk-NY7OG2OJ.js} +5 -7
  25. package/dist/{chunk-VASNLWPZ.js.map → chunk-NY7OG2OJ.js.map} +1 -1
  26. package/dist/{chunk-5QJQAFTP.cjs → chunk-OKDCUJOZ.cjs} +48 -12
  27. package/dist/chunk-OKDCUJOZ.cjs.map +1 -0
  28. package/dist/{chunk-MEAR2WHS.cjs → chunk-PC7YOZ4I.cjs} +1 -1
  29. package/dist/chunk-PC7YOZ4I.cjs.map +1 -0
  30. package/dist/{chunk-QLWBBCI5.cjs → chunk-Q2ITV3NY.cjs} +5 -4
  31. package/dist/chunk-Q2ITV3NY.cjs.map +1 -0
  32. package/dist/chunk-RGALV4UX.js +433 -0
  33. package/dist/chunk-RGALV4UX.js.map +1 -0
  34. package/dist/{chunk-W5KBDFCR.cjs → chunk-SA4ZTD3G.cjs} +30 -30
  35. package/dist/{chunk-W5KBDFCR.cjs.map → chunk-SA4ZTD3G.cjs.map} +1 -1
  36. package/dist/{chunk-GL5X6VVC.js → chunk-T4XXYAYY.js} +4 -4
  37. package/dist/{chunk-ZMFT5XOB.js → chunk-UXJCZOLA.js} +4 -3
  38. package/dist/chunk-UXJCZOLA.js.map +1 -0
  39. package/dist/{chunk-7NJUPRN6.cjs → chunk-XCPBD2PG.cjs} +183 -208
  40. package/dist/chunk-XCPBD2PG.cjs.map +1 -0
  41. package/dist/chunk-ZSUNQJRO.cjs +433 -0
  42. package/dist/chunk-ZSUNQJRO.cjs.map +1 -0
  43. package/dist/components/chat/chat-message-row.d.ts +1 -1
  44. package/dist/components/chat/chat-message-row.d.ts.map +1 -1
  45. package/dist/components/chat/index.cjs +10 -11
  46. package/dist/components/chat/index.cjs.map +1 -1
  47. package/dist/components/chat/index.js +12 -13
  48. package/dist/components/contact/index.cjs +7 -9
  49. package/dist/components/contact/index.cjs.map +1 -1
  50. package/dist/components/contact/index.js +6 -8
  51. package/dist/components/faq/faq-document-page.d.ts +21 -0
  52. package/dist/components/faq/faq-document-page.d.ts.map +1 -0
  53. package/dist/components/faq/index.cjs +22 -5
  54. package/dist/components/faq/index.cjs.map +1 -1
  55. package/dist/components/faq/index.d.ts +1 -0
  56. package/dist/components/faq/index.d.ts.map +1 -1
  57. package/dist/components/faq/index.js +21 -4
  58. package/dist/components/features/index.cjs +6 -8
  59. package/dist/components/features/index.cjs.map +1 -1
  60. package/dist/components/features/index.js +5 -7
  61. package/dist/components/index.cjs +304 -309
  62. package/dist/components/index.cjs.map +1 -1
  63. package/dist/components/index.js +249 -254
  64. package/dist/components/index.js.map +1 -1
  65. package/dist/components/layout/article-detail-layout.d.ts +21 -9
  66. package/dist/components/layout/article-detail-layout.d.ts.map +1 -1
  67. package/dist/components/navigation/index.cjs +6 -8
  68. package/dist/components/navigation/index.cjs.map +1 -1
  69. package/dist/components/navigation/index.js +5 -7
  70. package/dist/components/onboarding-guides/index.cjs +33 -46
  71. package/dist/components/onboarding-guides/index.cjs.map +1 -1
  72. package/dist/components/onboarding-guides/index.js +11 -24
  73. package/dist/components/onboarding-guides/index.js.map +1 -1
  74. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  75. package/dist/components/related-content/index.cjs +8 -10
  76. package/dist/components/related-content/index.cjs.map +1 -1
  77. package/dist/components/related-content/index.js +7 -9
  78. package/dist/components/shared/delivery/delivery-lists.d.ts.map +1 -1
  79. package/dist/components/shared/detail-page-skeleton.d.ts +6 -1
  80. package/dist/components/shared/detail-page-skeleton.d.ts.map +1 -1
  81. package/dist/components/shared/legal-document/use-legal-docs.d.ts.map +1 -1
  82. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  83. package/dist/components/shared/roadmap/roadmap-grid.d.ts.map +1 -1
  84. package/dist/components/shared/roadmap/use-roadmap-voting.d.ts.map +1 -1
  85. package/dist/components/tickets/help-center-list.d.ts +9 -1
  86. package/dist/components/tickets/help-center-list.d.ts.map +1 -1
  87. package/dist/components/tickets/hooks/use-ticket-actions.d.ts.map +1 -1
  88. package/dist/components/tickets/hooks/use-ticket-engagements.d.ts.map +1 -1
  89. package/dist/components/tickets/hooks/use-tickets-list.d.ts.map +1 -1
  90. package/dist/components/tickets/index.cjs +81 -76
  91. package/dist/components/tickets/index.cjs.map +1 -1
  92. package/dist/components/tickets/index.js +27 -22
  93. package/dist/components/tickets/index.js.map +1 -1
  94. package/dist/components/ui/index.cjs +12 -15
  95. package/dist/components/ui/index.cjs.map +1 -1
  96. package/dist/components/ui/index.js +24 -27
  97. package/dist/contexts/chat-runtime-context.d.ts +9 -0
  98. package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
  99. package/dist/contexts/index.cjs +2 -2
  100. package/dist/contexts/index.js +1 -1
  101. package/dist/hooks/index.cjs +3 -3
  102. package/dist/hooks/index.js +2 -2
  103. package/dist/hooks/use-contact-submission.d.ts.map +1 -1
  104. package/dist/hooks/use-self-fetch.d.ts.map +1 -1
  105. package/dist/index.cjs +11 -15
  106. package/dist/index.cjs.map +1 -1
  107. package/dist/index.js +32 -36
  108. package/dist/utils/embed-authed-fetch.d.ts +7 -0
  109. package/dist/utils/embed-authed-fetch.d.ts.map +1 -1
  110. package/dist/utils/embed-content-fetch.d.ts +2 -0
  111. package/dist/utils/embed-content-fetch.d.ts.map +1 -0
  112. package/dist/utils/index.cjs.map +1 -1
  113. package/dist/utils/index.js.map +1 -1
  114. package/package.json +1 -1
  115. package/src/components/chat/chat-message-row.tsx +12 -2
  116. package/src/components/faq/faq-document-page.tsx +69 -0
  117. package/src/components/faq/index.ts +1 -0
  118. package/src/components/layout/article-detail-layout.tsx +24 -11
  119. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +21 -16
  120. package/src/components/shared/delivery/delivery-lists.tsx +3 -2
  121. package/src/components/shared/detail-page-skeleton.tsx +18 -10
  122. package/src/components/shared/legal-document/use-legal-docs.ts +2 -1
  123. package/src/components/shared/product-release/release-detail-page.tsx +27 -17
  124. package/src/components/shared/roadmap/roadmap-grid.tsx +2 -1
  125. package/src/components/shared/roadmap/use-roadmap-voting.ts +3 -2
  126. package/src/components/tickets/help-center-list.tsx +12 -4
  127. package/src/components/tickets/hooks/use-ticket-actions.ts +7 -2
  128. package/src/components/tickets/hooks/use-ticket-engagements.ts +6 -1
  129. package/src/components/tickets/hooks/use-tickets-list.ts +7 -1
  130. package/src/components/tickets/ticket-detail-drawer.tsx +1 -0
  131. package/src/contexts/chat-runtime-context.tsx +9 -0
  132. package/src/hooks/use-contact-submission.ts +2 -1
  133. package/src/hooks/use-self-fetch.ts +2 -1
  134. package/src/styles/index.css +1 -0
  135. package/src/styles/ods-page-shell.css +29 -0
  136. package/src/utils/embed-authed-fetch.ts +10 -0
  137. package/src/utils/embed-content-fetch.ts +30 -0
  138. package/dist/chunk-4DYXV3E7.cjs.map +0 -1
  139. package/dist/chunk-4YKGXZ7W.js.map +0 -1
  140. package/dist/chunk-5QJQAFTP.cjs.map +0 -1
  141. package/dist/chunk-722TTRIJ.cjs +0 -29
  142. package/dist/chunk-722TTRIJ.cjs.map +0 -1
  143. package/dist/chunk-7NJUPRN6.cjs.map +0 -1
  144. package/dist/chunk-BSOHVQ7X.js.map +0 -1
  145. package/dist/chunk-CYMMGPPV.js.map +0 -1
  146. package/dist/chunk-DAAFVOTL.cjs +0 -172
  147. package/dist/chunk-DAAFVOTL.cjs.map +0 -1
  148. package/dist/chunk-L4LAAA5Q.js.map +0 -1
  149. package/dist/chunk-MEAR2WHS.cjs.map +0 -1
  150. package/dist/chunk-OO4ZGDRO.js +0 -29
  151. package/dist/chunk-OO4ZGDRO.js.map +0 -1
  152. package/dist/chunk-OPSZNW3I.js.map +0 -1
  153. package/dist/chunk-QLWBBCI5.cjs.map +0 -1
  154. package/dist/chunk-T2URJAX2.js +0 -169
  155. package/dist/chunk-T2URJAX2.js.map +0 -1
  156. package/dist/chunk-T7AJBKZP.cjs.map +0 -1
  157. package/dist/chunk-WU6PMH5L.cjs.map +0 -1
  158. package/dist/chunk-XDGDCO3I.cjs.map +0 -1
  159. package/dist/chunk-Y2B6WDU6.cjs +0 -169
  160. package/dist/chunk-Y2B6WDU6.cjs.map +0 -1
  161. package/dist/chunk-ZMFT5XOB.js.map +0 -1
  162. package/dist/chunk-ZXZJSTNV.js +0 -172
  163. package/dist/chunk-ZXZJSTNV.js.map +0 -1
  164. /package/dist/{chunk-FLIWL6V4.js.map → chunk-EINXNG5D.js.map} +0 -0
  165. /package/dist/{chunk-GL5X6VVC.js.map → chunk-T4XXYAYY.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/faq/faq-section.tsx","../src/components/faq-accordion.tsx","../src/components/faq/json-ld.ts"],"sourcesContent":["\"use client\"\n\nimport React, { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { Faq } from '../../types/faq'\nimport { FaqAccordion, type FaqItem } from '../faq-accordion'\nimport { useSelfFetch } from '../../hooks/use-self-fetch'\nimport { buildSuggestionUrl } from '../../utils/suggestion-url'\nimport { serializeJsonLd } from '../../utils/common'\nimport { scrollElementIntoView } from '../../utils/scroll-into-view'\nimport { cn } from '../../utils/cn'\nimport { buildFaqJsonLdFromFaqs, type FaqSchemaOptions } from './json-ld'\nimport { SECTION_HEADING_CLASS } from '../layout/page-heading'\n\nexport interface FaqSectionProps {\n /**\n * SSR hydrate. When provided, the hook skips the first client fetch (per\n * useSelfFetch contract). The consuming server page resolves FAQs then drills\n * them into this prop — the lib never re-fetches what the host already gated on.\n */\n initialFaqs?: Faq[]\n /** Both required together for entity-attached FAQs; partial → bare /api/faqs. */\n entityType?: string\n entityId?: number | string\n /**\n * Heading node above the grouped list. `undefined` → default\n * `<h2>`\"Frequently Asked Questions\". `null` → no heading (the host page\n * owns the `<h1>`, as the standalone /faqs surface does). A React node lets a\n * platform drill its own <PageHeading> without the lib referencing platform\n * state. Also drives category nesting so the document outline stays correct:\n * `null` → categories render `<h2>` (directly under the page `<h1>`);\n * otherwise categories render `<h3>` beneath this heading.\n */\n heading?: React.ReactNode | null\n /** Inject FAQPage schema.org JSON-LD as a <script>. Off by default so embeds\n * don't emit duplicate schema. */\n emitJsonLd?: boolean\n /** Overrides for the JSON-LD's name/description/url. */\n jsonLd?: FaqSchemaOptions\n className?: string\n /** Maps to /api/faqs `?count=` (the 5-tier fill target). Absent → param\n * not sent (server default applies). */\n minResults?: number\n /** Fetch-URL prefix for third-party embeds / reverse proxies\n * ('' = same-origin relative). */\n apiBaseUrl?: string\n}\n\nconst DEFAULT_HEADING_TEXT = 'Frequently Asked Questions'\n\n/** URL composition shared with RelatedContentSection (`buildSuggestionUrl`)\n * — byte-identical to the historical `buildFaqsUrl` output when\n * `minResults`/`apiBaseUrl` are absent. */\nfunction buildFaqsUrl(\n entityType?: string,\n entityId?: number | string,\n minResults?: number,\n apiBaseUrl = '',\n): string {\n return buildSuggestionUrl('/api/faqs', { apiBaseUrl, entityType, entityId, count: minResults })\n}\n\n/** Stable, URL-safe anchor id for a category. Prefixed so it can't collide\n * with other in-page ids, and so a bare numeric/blank section still yields a\n * valid id. */\nfunction sectionSlug(section: string): string {\n return (\n 'faq-' +\n section\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n )\n}\n\ninterface FaqGroup {\n /** null → the uncategorized bucket: no heading, no jump pill, rendered last. */\n section: string | null\n slug: string | null\n items: FaqItem[]\n}\n\n/** Group FAQs by `faq.section`, preserving the server's first-seen\n * (display_order) order for BOTH the section order and the rows within each\n * section. The uncategorized bucket (blank/missing section) sinks to the end\n * since it renders without a heading. Items carry NO badge here — the `<h2>`\n * IS the category, so a per-row chip would be redundant. */\nfunction groupFaqsBySection(faqs: Faq[]): FaqGroup[] {\n const order: string[] = []\n const byName = new Map<string, FaqGroup>()\n let uncategorized: FaqGroup | null = null\n for (const faq of faqs) {\n const item: FaqItem = { id: faq.id, question: faq.question, answer: faq.answer }\n const name = faq.section?.trim()\n if (!name) {\n if (!uncategorized) uncategorized = { section: null, slug: null, items: [] }\n uncategorized.items.push(item)\n continue\n }\n let group = byName.get(name)\n if (!group) {\n group = { section: name, slug: sectionSlug(name), items: [] }\n byName.set(name, group)\n order.push(name)\n }\n group.items.push(item)\n }\n const groups = order.map((name) => byName.get(name)!)\n if (uncategorized) groups.push(uncategorized)\n return groups\n}\n\n/** The standard hub sticky-header height — same offset `useNavLink`'s hash\n * scroll uses, so a category jump lands below the header, not under it. */\nconst FAQ_NAV_HEADER_OFFSET = 96\n\n/**\n * Grouped FAQ layout: a category jump-nav above stacked `<h2>` category\n * sections (each its own accordion). Isolated into its own component so the\n * scroll-spy hooks only mount in grouped mode — `FaqSection`'s own hooks stay\n * unconditional.\n *\n * The pills are real `<a href=\"#slug\">` anchors (crawlable in-page links,\n * deep-linkable, work without JS); the click handler upgrades the jump to the\n * cancellation-proof `scrollElementIntoView` tween and syncs the URL hash.\n */\nfunction GroupedFaqList({\n groups,\n categoryHeadingAs,\n}: {\n groups: FaqGroup[]\n /** Heading tag for each category, so the document outline nests correctly\n * under whatever owns the heading above this block: `h2` on the standalone\n * page (the page owns the `<h1>`), `h3` beneath an embed's `<h2>` title.\n * The VISUAL is `SECTION_HEADING_CLASS` either way, so categories look\n * identical on every surface. */\n categoryHeadingAs: 'h2' | 'h3'\n}) {\n const CategoryHeading = categoryHeadingAs\n const navGroups = useMemo(() => groups.filter((g) => g.slug), [groups])\n const [activeSlug, setActiveSlug] = useState<string | null>(navGroups[0]?.slug ?? null)\n // Identity-stable key for the section set so the observer re-binds only when\n // the categories actually change (not on every parent re-render).\n const slugKey = navGroups.map((g) => g.slug).join('|')\n\n // Scroll-spy: mark the pill for the category currently at the top of the\n // viewport. rootMargin drops the trigger line just below the sticky header\n // and ignores the bottom ~55% so \"active\" is the section being read, not the\n // next one peeking in.\n useEffect(() => {\n if (navGroups.length < 2) return\n const tops = new Map<string, number>()\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n const id = (entry.target as HTMLElement).id\n if (entry.isIntersecting) tops.set(id, entry.boundingClientRect.top)\n else tops.delete(id)\n }\n let bestId: string | null = null\n let bestTop = Number.POSITIVE_INFINITY\n for (const [id, top] of tops) {\n if (top < bestTop) {\n bestTop = top\n bestId = id\n }\n }\n if (bestId) setActiveSlug(bestId)\n },\n { rootMargin: `-${FAQ_NAV_HEADER_OFFSET}px 0px -55% 0px`, threshold: 0 },\n )\n for (const group of navGroups) {\n const el = group.slug ? document.getElementById(group.slug) : null\n if (el) observer.observe(el)\n }\n return () => observer.disconnect()\n // slugKey encodes the section set; re-observe only when it changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [slugKey])\n\n const handleJump = useCallback(\n (e: React.MouseEvent<HTMLAnchorElement>, slug: string) => {\n e.preventDefault()\n setActiveSlug(slug)\n scrollElementIntoView(document.getElementById(slug), {\n headerOffset: FAQ_NAV_HEADER_OFFSET,\n })\n if (typeof history !== 'undefined') history.replaceState(null, '', `#${slug}`)\n },\n [],\n )\n\n return (\n <div className=\"space-y-8\">\n {navGroups.length > 1 && (\n <nav aria-label=\"FAQ categories\" className=\"flex flex-wrap gap-2\">\n {navGroups.map((group) => {\n const isActive = group.slug === activeSlug\n return (\n <a\n key={group.slug}\n href={`#${group.slug}`}\n aria-current={isActive ? 'true' : undefined}\n onClick={(e) => handleJump(e, group.slug as string)}\n className={cn(\n \"rounded-full border px-4 py-2 text-sm font-medium font-['DM_Sans'] transition-colors\",\n isActive\n ? 'border-ods-text-primary bg-ods-card text-ods-text-primary'\n : 'border-ods-border bg-ods-card text-ods-text-secondary hover:border-ods-text-secondary hover:text-ods-text-primary',\n )}\n >\n {group.section}\n </a>\n )\n })}\n </nav>\n )}\n <div className=\"space-y-10\">\n {groups.map((group) => (\n <section\n key={group.slug ?? 'faq-uncategorized'}\n id={group.slug ?? undefined}\n className=\"scroll-mt-24 space-y-4\"\n >\n {group.section && (\n <CategoryHeading className={SECTION_HEADING_CLASS}>{group.section}</CategoryHeading>\n )}\n <FaqAccordion items={group.items} />\n </section>\n ))}\n </div>\n </div>\n )\n}\n\nfunction FaqSkeleton() {\n return (\n <div className=\"space-y-8 animate-pulse\">\n <div className=\"h-12 md:h-14 w-2/3 rounded bg-ods-border\" />\n <div className=\"rounded-3xl border border-ods-border overflow-hidden bg-ods-card divide-y divide-ods-border w-full\">\n {Array.from({ length: 8 }).map((_, idx) => (\n <div key={idx} className=\"flex items-center justify-between px-6 md:px-8 py-6\">\n <div className=\"h-6 w-5/6 rounded bg-ods-border\" />\n <div className=\"h-10 w-10 rounded-md bg-ods-border\" />\n </div>\n ))}\n </div>\n </div>\n )\n}\n\ninterface FaqsResponse {\n faqs: Faq[]\n}\n\n/**\n * The FAQ display surface — ONE rendering on every host: the list grouped by\n * `faq.section` (each category a heading + its own accordion, with a category\n * jump-nav once there are 2+ categories). There is no flat/ungrouped mode and\n * no page-vs-embedded shell fork; the standalone /faqs page and every embed\n * render through this single path, so they cannot drift.\n *\n * - Standalone /faqs page: pass `initialFaqs` (SSR) + `heading={null}` (the\n * page owns the <h1>) + `emitJsonLd` with `jsonLd` overrides for SEO.\n * - Per-entity embed: pass `entityType` + `entityId` (no `initialFaqs`); the\n * hook self-fetches `GET /api/faqs`, and `heading` is this block's own <h2>.\n *\n * CONTRACT: the consuming app MUST implement `GET /api/faqs`. On a fetch error\n * (or zero FAQs) the component renders nothing so the host page isn't\n * disfigured. The host always supplies the page shell — this renders a bare\n * <section>.\n */\nexport function FaqSection({\n initialFaqs,\n entityType,\n entityId,\n heading,\n emitJsonLd = false,\n jsonLd,\n className,\n minResults,\n apiBaseUrl = '',\n}: FaqSectionProps) {\n const url = buildFaqsUrl(entityType, entityId, minResults, apiBaseUrl)\n // Memoized — useSelfFetch re-syncs on [initialData]; a fresh per-render\n // wrapper object would setState-loop under re-rendering parents.\n const initialData = useMemo<FaqsResponse | undefined>(\n () => (initialFaqs ? { faqs: initialFaqs } : undefined),\n [initialFaqs],\n )\n const { data, isLoading, error } = useSelfFetch<FaqsResponse>(url, { initialData })\n\n const faqs = data?.faqs ?? []\n // Grouped before the early returns so the hook order stays stable.\n const groups = useMemo(() => (faqs.length > 0 ? groupFaqsBySection(faqs) : []), [faqs])\n\n // `undefined` → default <h2> title; `null` → the host page owns the <h1>, so\n // no title renders here. `heading === null` also makes the category headings\n // <h2> (directly under the page <h1>); otherwise they nest as <h3>.\n const headingNode =\n heading === undefined ? <h2 className={SECTION_HEADING_CLASS}>{DEFAULT_HEADING_TEXT}</h2> : heading\n\n // Degrade silently — never show an error banner or an empty section shell\n // where FAQs would be (host pages and the standalone surface both rely on it).\n if (error) return null\n if (!isLoading && faqs.length === 0) return null\n if (isLoading && faqs.length === 0) {\n return (\n <div className={className}>\n <FaqSkeleton />\n </div>\n )\n }\n\n const schema = emitJsonLd ? buildFaqJsonLdFromFaqs(faqs, jsonLd) : null\n\n return (\n <>\n <section className={className ?? 'space-y-10'}>\n {headingNode}\n <GroupedFaqList groups={groups} categoryHeadingAs={heading === null ? 'h2' : 'h3'} />\n </section>\n {schema && (\n <script\n type=\"application/ld+json\"\n // eslint-disable-next-line react/no-danger\n // serializeJsonLd, NOT raw JSON.stringify — FAQ answers are\n // admin-entered; an embedded \"</script>\" must not break the tag.\n dangerouslySetInnerHTML={{ __html: serializeJsonLd(schema) }}\n />\n )}\n </>\n )\n}\n","\"use client\"\n\nimport React, { useRef, useState, useEffect, useCallback } from 'react'\nimport { ChevronButton } from './ui/chevron-button'\nimport { cn } from \"../utils/cn\"\n\nexport interface FaqItem {\n id: number | string\n question: string\n answer: string\n}\n\ninterface FaqAccordionProps {\n items: FaqItem[]\n defaultOpenIds?: (number | string)[]\n}\n\n// Utility to measure scrollHeight outside render cycle\nconst useMeasuredHeight = (isOpen: boolean) => {\n const ref = useRef<HTMLDivElement | null>(null)\n const [maxHeight, setMaxHeight] = useState<string>('0px')\n\n const measure = useCallback(() => {\n if (ref.current) {\n const height = ref.current.scrollHeight\n setMaxHeight(`${height}px`)\n }\n }, [])\n\n // Update height only when section is open\n useEffect(() => {\n if (isOpen) {\n measure()\n } else {\n setMaxHeight('0px')\n }\n }, [isOpen, measure])\n\n return { ref, maxHeight }\n}\n\nexport function FaqAccordion({ items, defaultOpenIds = [] }: FaqAccordionProps) {\n const [openSet, setOpenSet] = useState<Set<string | number>>(new Set(defaultOpenIds))\n\n const toggle = (id: string | number) => {\n setOpenSet(prev => {\n const next = new Set(prev)\n if (next.has(id)) next.delete(id)\n else next.add(id)\n return next\n })\n }\n\n return (\n <div className=\"rounded-3xl border border-ods-border divide-y divide-ods-border bg-ods-card overflow-hidden\">\n {items.map(item => {\n const isOpen = openSet.has(item.id)\n const { ref, maxHeight } = useMeasuredHeight(isOpen)\n\n return (\n <div\n key={item.id}\n className={cn('group transition-colors hover:bg-[#1E1E1E]', isOpen ? 'bg-ods-bg' : 'bg-transparent')}\n >\n {/* Header */}\n <div\n role=\"button\"\n tabIndex={0}\n onClick={() => toggle(item.id)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n toggle(item.id);\n }\n }}\n aria-expanded={isOpen}\n className=\"flex w-full items-center justify-between px-6 md:px-8 py-6 text-left focus:outline-none transition-colors cursor-pointer\"\n >\n <div className=\"min-w-0 pr-4\">\n <h3>\n {item.question}\n </h3>\n </div>\n <div className=\"flex-shrink-0\">\n <ChevronButton\n aria-label={isOpen ? 'Collapse question' : 'Expand question'}\n size=\"md\"\n isExpanded={isOpen}\n backgroundColor=\"transparent\"\n borderColor=\"#3A3A3A\"\n />\n </div>\n </div>\n {/* Content wrapper with max-height animation */}\n <div\n style={{ maxHeight, transition: 'max-height 0.35s ease-in-out, opacity 0.35s ease-in-out', opacity: isOpen ? 1 : 0 }}\n className=\"overflow-hidden group-hover:bg-[#1E1E1E]/30\"\n >\n <div ref={ref} className=\"px-6 md:px-8 pb-6 text-ods-text-primary text-h4\">\n {item.answer}\n </div>\n </div>\n </div>\n )\n })}\n </div>\n )\n} ","/**\n * Pure FAQ JSON-LD builder. No React, no client-only deps — safe to import from\n * Server Components via the `./components/faq` subpath export. (Do NOT import\n * through the root `./components` barrel — that barrel is `\"use client\"` and\n * dragging it into a Server Component would force the client graph into the\n * server.)\n *\n * The hub used to harcode `name`/`description` here for OpenMSP; in the lib we\n * accept overrides so every embedder can supply its own platform branding.\n */\nimport type { Faq } from '../../types/faq'\n\nexport interface FaqSchemaOptions {\n name?: string\n description?: string\n url?: string\n}\n\nconst DEFAULT_NAME = 'Frequently Asked Questions'\nconst DEFAULT_DESCRIPTION =\n 'Answers to common questions.'\n\nexport function baseFaqSchema(opts: FaqSchemaOptions = {}) {\n return {\n '@context': 'https://schema.org',\n '@type': 'FAQPage',\n name: opts.name ?? DEFAULT_NAME,\n description: opts.description ?? DEFAULT_DESCRIPTION,\n ...(opts.url ? { url: opts.url } : {}),\n } as const\n}\n\nexport function buildFaqJsonLdFromFaqs(faqs: Faq[], opts: FaqSchemaOptions = {}) {\n return {\n ...baseFaqSchema(opts),\n mainEntity: faqs.map((faq) => ({\n '@type': 'Question',\n name: faq.question,\n acceptedAnswer: {\n '@type': 'Answer',\n text: faq.answer,\n },\n })),\n } as const\n}\n"],"mappings":"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","names":["useCallback","useEffect","useState","jsx","jsxs","useState","useEffect","useCallback"]}
@@ -1,172 +0,0 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }"use client";
2
-
3
- // src/hooks/ui/use-auto-limit-tags.ts
4
- var _react = require('react');
5
- function useAutoLimitTags({
6
- count,
7
- limitTags = "auto",
8
- placeholder = "",
9
- reserveInputWidth = true
10
- }) {
11
- const middleRef = _react.useRef.call(void 0, null);
12
- const measureRef = _react.useRef.call(void 0, null);
13
- const textMeasureRef = _react.useRef.call(void 0, null);
14
- const badgeRef = _react.useRef.call(void 0, null);
15
- const inputRef = _react.useRef.call(void 0, null);
16
- const [visibleCount, setVisibleCount] = _react.useState.call(void 0, count);
17
- const recalculate = _react.useCallback.call(void 0, () => {
18
- if (limitTags !== "auto") {
19
- setVisibleCount(Math.min(limitTags, count));
20
- return;
21
- }
22
- const middle = middleRef.current;
23
- const measure = measureRef.current;
24
- if (!middle || !measure) {
25
- setVisibleCount(count);
26
- return;
27
- }
28
- if (count === 0) {
29
- setVisibleCount(0);
30
- return;
31
- }
32
- const cs = getComputedStyle(middle);
33
- const padL = parseFloat(cs.paddingLeft) || 0;
34
- const padR = parseFloat(cs.paddingRight) || 0;
35
- const gap = parseFloat(cs.gap) || 0;
36
- const middleW = middle.clientWidth;
37
- let inputReservedW = 0;
38
- let trailingGap = 0;
39
- if (reserveInputWidth) {
40
- const textW = _nullishCoalesce(_optionalChain([textMeasureRef, 'access', _ => _.current, 'optionalAccess', _2 => _2.offsetWidth]), () => ( 60));
41
- const inputMinW = inputRef.current ? parseFloat(getComputedStyle(inputRef.current).minWidth) || 60 : 60;
42
- inputReservedW = Math.max(textW + 8, inputMinW);
43
- trailingGap = gap;
44
- }
45
- const available = middleW - padL - padR - inputReservedW - trailingGap;
46
- const tagEls = Array.from(measure.children);
47
- const widths = tagEls.map((el) => el.offsetWidth);
48
- let total = 0;
49
- for (let i = 0; i < widths.length; i++) {
50
- total += widths[i] + (i > 0 ? gap : 0);
51
- }
52
- if (total <= available) {
53
- setVisibleCount(count);
54
- return;
55
- }
56
- const badgeW = _nullishCoalesce(_optionalChain([badgeRef, 'access', _3 => _3.current, 'optionalAccess', _4 => _4.offsetWidth]), () => ( 40));
57
- const spaceWithBadge = available - badgeW - gap;
58
- let used = 0;
59
- let fitCount = 0;
60
- for (let i = 0; i < widths.length; i++) {
61
- const need = widths[i] + (i > 0 ? gap : 0);
62
- if (used + need > spaceWithBadge) break;
63
- used += need;
64
- fitCount++;
65
- }
66
- setVisibleCount(Math.max(0, fitCount));
67
- }, [count, limitTags, placeholder, reserveInputWidth]);
68
- _react.useEffect.call(void 0, () => {
69
- recalculate();
70
- }, [recalculate]);
71
- _react.useEffect.call(void 0, () => {
72
- const el = middleRef.current;
73
- if (!el) return;
74
- const ro = new ResizeObserver(recalculate);
75
- ro.observe(el);
76
- return () => ro.disconnect();
77
- }, [recalculate]);
78
- return { visibleCount, middleRef, measureRef, textMeasureRef, badgeRef, inputRef };
79
- }
80
-
81
- // src/hooks/ui/use-debounce.ts
82
-
83
- function useDebounce(value, delay = 500) {
84
- const [debouncedValue, setDebouncedValue] = _react.useState.call(void 0, value);
85
- _react.useEffect.call(void 0, () => {
86
- const timer = setTimeout(() => {
87
- setDebouncedValue(value);
88
- }, delay);
89
- return () => {
90
- clearTimeout(timer);
91
- };
92
- }, [value, delay]);
93
- return debouncedValue;
94
- }
95
-
96
- // src/hooks/use-near-viewport.ts
97
-
98
- var observers = /* @__PURE__ */ new Map();
99
- var subscribers = /* @__PURE__ */ new WeakMap();
100
- function getObserverFor(rootMargin) {
101
- const existing = observers.get(rootMargin);
102
- if (existing) return existing;
103
- const io = new IntersectionObserver(
104
- (entries) => {
105
- entries.forEach((entry) => {
106
- if (!entry.isIntersecting) return;
107
- const cb = subscribers.get(entry.target);
108
- if (cb) {
109
- cb();
110
- io.unobserve(entry.target);
111
- subscribers.delete(entry.target);
112
- }
113
- });
114
- },
115
- { rootMargin }
116
- );
117
- observers.set(rootMargin, io);
118
- return io;
119
- }
120
- function useNearViewport(rootMargin = "500px") {
121
- const [isNear, setIsNear] = _react.useState.call(void 0, false);
122
- const elRef = _react.useRef.call(void 0, null);
123
- const ref = _react.useCallback.call(void 0,
124
- (node) => {
125
- const prev = elRef.current;
126
- if (prev) {
127
- const stillOurs = subscribers.get(prev);
128
- if (stillOurs) {
129
- subscribers.delete(prev);
130
- _optionalChain([observers, 'access', _5 => _5.get, 'call', _6 => _6(rootMargin), 'optionalAccess', _7 => _7.unobserve, 'call', _8 => _8(prev)]);
131
- }
132
- }
133
- elRef.current = node;
134
- if (!node) return;
135
- const cb = () => setIsNear(true);
136
- subscribers.set(node, cb);
137
- getObserverFor(rootMargin).observe(node);
138
- },
139
- [rootMargin]
140
- );
141
- _react.useEffect.call(void 0, () => {
142
- return () => {
143
- const el = elRef.current;
144
- if (!el) return;
145
- if (subscribers.get(el)) {
146
- subscribers.delete(el);
147
- _optionalChain([observers, 'access', _9 => _9.get, 'call', _10 => _10(rootMargin), 'optionalAccess', _11 => _11.unobserve, 'call', _12 => _12(el)]);
148
- }
149
- };
150
- }, [rootMargin]);
151
- return { ref, isNear };
152
- }
153
-
154
- // src/hooks/use-og-placeholder.ts
155
-
156
- function useOgPlaceholder(buildUrl, title, siteName = "", enabled = true, aspect = "wide") {
157
- return _react.useMemo.call(void 0, () => {
158
- if (!enabled || !title) return null;
159
- const options = {};
160
- if (siteName) options.site = siteName;
161
- if (aspect === "square") options.aspect = "square";
162
- return buildUrl(title, options);
163
- }, [buildUrl, title, siteName, enabled, aspect]);
164
- }
165
-
166
-
167
-
168
-
169
-
170
-
171
- exports.useAutoLimitTags = useAutoLimitTags; exports.useDebounce = useDebounce; exports.useNearViewport = useNearViewport; exports.useOgPlaceholder = useOgPlaceholder;
172
- //# sourceMappingURL=chunk-DAAFVOTL.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-DAAFVOTL.cjs","../src/hooks/ui/use-auto-limit-tags.ts","../src/hooks/ui/use-debounce.ts","../src/hooks/use-near-viewport.ts","../src/hooks/use-og-placeholder.ts"],"names":["useState","useEffect","useRef","useCallback"],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-DAAFVOTL.cjs","sourcesContent":[null,"\"use client\"\n\nimport { useCallback, useEffect, useRef, useState } from \"react\"\n\nexport interface UseAutoLimitTagsOptions {\n /** Total number of tags */\n count: number\n /** Fixed limit or \"auto\" for DOM-based measurement. Default \"auto\" */\n limitTags?: number | \"auto\"\n /** Placeholder text used to reserve input width (only used in \"auto\" mode) */\n placeholder?: string\n /**\n * Reserve trailing space for a text input (the autocomplete combobox). Set\n * `false` for a pure chip row that has no input — then only the chips and the\n * \"+N\" / \"⋯\" overflow badge compete for the available width. Default `true`.\n */\n reserveInputWidth?: boolean\n}\n\nexport interface UseAutoLimitTagsReturn {\n /** How many tags to show */\n visibleCount: number\n /** Ref for the zone that contains tags + input (must have overflow-hidden, gap, padding) */\n middleRef: React.RefObject<HTMLDivElement | null>\n /** Ref for the off-screen container that holds measurement copies of ALL tags */\n measureRef: React.RefObject<HTMLDivElement | null>\n /** Ref for the hidden span that measures placeholder text width */\n textMeasureRef: React.RefObject<HTMLSpanElement | null>\n /** Ref for the \"+N\" badge element (used to measure its width) */\n badgeRef: React.RefObject<HTMLButtonElement | null>\n /** Ref for the input element (used to read its min-width) */\n inputRef: React.RefObject<HTMLInputElement | null>\n}\n\n/**\n * Calculates how many tags fit in a single-line container.\n *\n * Requires three off-screen measurement elements rendered by the consumer:\n * 1. A `<div ref={measureRef}>` containing Tag copies for every item (to measure widths)\n * 2. A `<span ref={textMeasureRef}>` containing the placeholder text (to reserve input width)\n * 3. The `<button ref={badgeRef}>` for the \"+N\" badge (to measure badge width)\n *\n * The hook reads real CSS values (padding, gap) from the middleRef container,\n * so it works regardless of responsive breakpoints or custom styling.\n */\nexport function useAutoLimitTags({\n count,\n limitTags = \"auto\",\n placeholder = \"\",\n reserveInputWidth = true,\n}: UseAutoLimitTagsOptions): UseAutoLimitTagsReturn {\n const middleRef = useRef<HTMLDivElement>(null)\n const measureRef = useRef<HTMLDivElement>(null)\n const textMeasureRef = useRef<HTMLSpanElement>(null)\n const badgeRef = useRef<HTMLButtonElement>(null)\n const inputRef = useRef<HTMLInputElement>(null)\n const [visibleCount, setVisibleCount] = useState(count)\n\n const recalculate = useCallback(() => {\n // Fixed limit — skip DOM measurement\n if (limitTags !== \"auto\") {\n setVisibleCount(Math.min(limitTags, count))\n return\n }\n\n const middle = middleRef.current\n const measure = measureRef.current\n if (!middle || !measure) {\n setVisibleCount(count)\n return\n }\n if (count === 0) {\n setVisibleCount(0)\n return\n }\n\n // Read real CSS metrics from the middle zone\n const cs = getComputedStyle(middle)\n const padL = parseFloat(cs.paddingLeft) || 0\n const padR = parseFloat(cs.paddingRight) || 0\n const gap = parseFloat(cs.gap) || 0\n const middleW = middle.clientWidth\n\n // Reserve trailing space for the input (autocomplete only). A pure chip row\n // (`reserveInputWidth: false`) has no input, so nothing — and no gap before\n // it — is reserved.\n let inputReservedW = 0\n let trailingGap = 0\n if (reserveInputWidth) {\n const textW = textMeasureRef.current?.offsetWidth ?? 60\n const inputMinW = inputRef.current\n ? parseFloat(getComputedStyle(inputRef.current).minWidth) || 60\n : 60\n inputReservedW = Math.max(textW + 8, inputMinW)\n trailingGap = gap\n }\n\n // Available = middle zone − padding − input reserved − gap before input\n const available = middleW - padL - padR - inputReservedW - trailingGap\n\n // Measure every tag from the off-screen container\n const tagEls = Array.from(measure.children) as HTMLElement[]\n const widths = tagEls.map((el) => el.offsetWidth)\n\n // Fast check: do ALL tags fit?\n let total = 0\n for (let i = 0; i < widths.length; i++) {\n total += widths[i] + (i > 0 ? gap : 0)\n }\n if (total <= available) {\n setVisibleCount(count)\n return\n }\n\n // Not all fit → reserve space for the \"+N\" badge\n const badgeW = badgeRef.current?.offsetWidth ?? 40\n const spaceWithBadge = available - badgeW - gap\n\n let used = 0\n let fitCount = 0\n for (let i = 0; i < widths.length; i++) {\n const need = widths[i] + (i > 0 ? gap : 0)\n if (used + need > spaceWithBadge) break\n used += need\n fitCount++\n }\n\n setVisibleCount(Math.max(0, fitCount))\n }, [count, limitTags, placeholder, reserveInputWidth])\n\n // Recalculate when inputs change\n useEffect(() => {\n recalculate()\n }, [recalculate])\n\n // Recalculate on container resize\n useEffect(() => {\n const el = middleRef.current\n if (!el) return\n const ro = new ResizeObserver(recalculate)\n ro.observe(el)\n return () => ro.disconnect()\n }, [recalculate])\n\n return { visibleCount, middleRef, measureRef, textMeasureRef, badgeRef, inputRef }\n}\n","\"use client\"\n\nimport { useState, useEffect } from \"react\"\n\n/**\n * Hook to debounce a value\n * @param value - Value to debounce\n * @param delay - Delay in milliseconds\n * @returns Debounced value\n */\nexport function useDebounce<T>(value: T, delay = 500): T {\n const [debouncedValue, setDebouncedValue] = useState<T>(value)\n\n useEffect(() => {\n // Set up timeout to update debounced value\n const timer = setTimeout(() => {\n setDebouncedValue(value)\n }, delay)\n\n // Clean up timeout on value change or unmount\n return () => {\n clearTimeout(timer)\n }\n }, [value, delay])\n\n return debouncedValue\n}\n","/**\n * useNearViewport — module-level shared IntersectionObserver in hook form.\n *\n * Single IO instance per `rootMargin` value, shared across every component\n * that mounts the hook. Reduces overhead vs. one IO per component on\n * grid/list pages where many subscribers observe the viewport with the same\n * margin. Promoted from the inline singleton at\n * `multi-platform-hub/components/shared/video-bites-display.tsx:21-43`,\n * which is the only IO pattern in either repo today.\n *\n * Usage:\n * ```tsx\n * function MyCard() {\n * const { ref, isNear } = useNearViewport('500px');\n * return <div ref={ref}>{isNear ? <HeavyChild /> : <Placeholder />}</div>;\n * }\n * ```\n *\n * StrictMode safety: cleanup uses an identity check on the registered\n * callback so React's dev double-mount (mount → cleanup → re-mount) does\n * not drop the second mount's freshly-set subscription. The IO callback\n * also checks `subscribers.get(target)` before invoking so a fire that\n * races with unmount cannot crash on a torn-down component.\n *\n * The hook fires once — on first intersection it sets `isNear=true` and\n * unobserves the element. Callers that need re-observation should\n * unmount and remount (or fork the hook for two-way behavior).\n */\n\nimport { useEffect, useRef, useState, useCallback } from 'react';\n\n// Per-rootMargin IO map. Multiple call sites with different margins each\n// get their own singleton observer.\nconst observers = new Map<string, IntersectionObserver>();\nconst subscribers = new WeakMap<Element, () => void>();\n\nfunction getObserverFor(rootMargin: string): IntersectionObserver {\n const existing = observers.get(rootMargin);\n if (existing) return existing;\n\n const io = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (!entry.isIntersecting) return;\n // Race-safe: re-read the callback at fire time. A late IO firing\n // after cleanup must not invoke a stale callback.\n const cb = subscribers.get(entry.target);\n if (cb) {\n cb();\n io.unobserve(entry.target);\n subscribers.delete(entry.target);\n }\n });\n },\n { rootMargin }\n );\n observers.set(rootMargin, io);\n return io;\n}\n\nexport interface UseNearViewportResult<T extends Element = HTMLElement> {\n /** Ref to attach to the element you want to gate on visibility. */\n ref: (node: T | null) => void;\n /** Flips to `true` once the element enters within `rootMargin` of the viewport. Never flips back. */\n isNear: boolean;\n}\n\n/**\n * @param rootMargin Margin around the viewport (CSS-style string).\n * '500px' = element starts mounting 500px before scroll-in.\n * '1000px' = a full viewport's worth of lookahead.\n * '0px' = strict on-screen detection.\n */\nexport function useNearViewport<T extends Element = HTMLElement>(\n rootMargin: string = '500px'\n): UseNearViewportResult<T> {\n const [isNear, setIsNear] = useState(false);\n const elRef = useRef<T | null>(null);\n\n // Subscribe/unsubscribe on element change.\n const ref = useCallback(\n (node: T | null) => {\n const prev = elRef.current;\n\n // Unsubscribe previous, if any. Identity-check the callback so a\n // StrictMode re-mount that has already re-registered keeps its sub.\n if (prev) {\n const stillOurs = subscribers.get(prev);\n if (stillOurs) {\n subscribers.delete(prev);\n observers.get(rootMargin)?.unobserve(prev);\n }\n }\n\n elRef.current = node;\n if (!node) return;\n\n const cb = () => setIsNear(true);\n subscribers.set(node, cb);\n getObserverFor(rootMargin).observe(node);\n },\n [rootMargin]\n );\n\n // Unsubscribe on unmount. Identity check guards the StrictMode race.\n useEffect(() => {\n return () => {\n const el = elRef.current;\n if (!el) return;\n if (subscribers.get(el)) {\n subscribers.delete(el);\n observers.get(rootMargin)?.unobserve(el);\n }\n };\n }, [rootMargin]);\n\n return { ref, isNear };\n}\n","'use client'\n\nimport { useMemo } from 'react'\n\n/**\n * Hook that generates a branded placeholder image URL using the\n * host's `/api/og-placeholder` server-side image generator (or any\n * builder the caller supplies).\n *\n * THIS HOOK IS A THIN CLIENT-SIDE WRAPPER. The hub-side wrapper\n * (`hooks/use-og-placeholder.ts` in multi-platform-hub) injects its\n * canonical `buildOgPlaceholderUrl` from `lib/utils/entity-image.ts`.\n * The lib version takes the builder as an argument so the lib has no\n * hub dependency.\n *\n * @param buildUrl - Function that builds the placeholder URL from a\n * title + options object (`{ site?, aspect? }`).\n * Hub passes `buildOgPlaceholderUrl` from\n * `lib/utils/entity-image.ts`. Embedded apps can\n * wire any equivalent that hits their own placeholder\n * route.\n * @param title - Text to display on the placeholder\n * @param siteName - Site name shown below the title (optional —\n * defaults to empty)\n * @param enabled - Whether to generate the URL (default: true)\n * @param aspect - `'wide'` (1200×630 social-card; default) or `'square'`\n * (1024×1024 — used by compact chat-inline card slots\n * so `object-cover` doesn't crop the title off).\n * @returns Placeholder image URL or null if disabled / no title\n */\nexport function useOgPlaceholder(\n buildUrl: (title: string, options: { site?: string; aspect?: 'wide' | 'square' }) => string,\n title: string | undefined | null,\n siteName: string = '',\n enabled: boolean = true,\n aspect: 'wide' | 'square' = 'wide',\n): string | null {\n return useMemo(() => {\n if (!enabled || !title) return null\n const options: { site?: string; aspect?: 'wide' | 'square' } = {}\n if (siteName) options.site = siteName\n if (aspect === 'square') options.aspect = 'square'\n return buildUrl(title, options)\n }, [buildUrl, title, siteName, enabled, aspect])\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/hooks/use-self-fetch.ts"],"sourcesContent":["'use client'\n\nimport { useEffect, useRef, useState, type Dispatch, type SetStateAction } from 'react'\n\nexport interface UseSelfFetchResult<T> {\n data: T | null\n /** Imperatively patch the fetched data (e.g. optimistic vote updates). */\n setData: Dispatch<SetStateAction<T | null>>\n isLoading: boolean\n error: boolean\n /** Re-run the fetch (error-retry affordance). */\n reload: () => void\n}\n\n/**\n * The single source for the 4 self-fetching content views\n * (`ProductReleasesView`, `RoadmapView`, the onboarding catalog/detail). GETs a\n * configured `url` into component state with **plain `fetch` + `useEffect`** —\n * deliberately NO react-query, so embedders don't need a QueryClient. This is\n * the same technology choice the shipped `DeliveryLists` makes; that component\n * predates this hook and still hand-rolls the loop (a future pass can migrate\n * it onto a two-url variant of this hook).\n *\n * Behaviour:\n * - `url = null` → fetching is DISABLED (controlled / SSR mode, or missing\n * config); returns `initialData ?? null` and never fetches.\n * - `initialData` provided → hydrates immediately and SKIPS the first fetch\n * (so the hub's server-rendered data isn't re-fetched on mount); later\n * `url` changes still re-fetch.\n * - a `cancelled` guard ensures an in-flight response from a STALE `url`\n * (e.g. a fast pagination / section toggle) can't overwrite a newer one.\n * - `reload()` bumps an internal key to retry after an error.\n *\n * Re-fetches whenever `url` changes, so callers fold all query params INTO the\n * url string (the url IS the cache key).\n */\nexport function useSelfFetch<T>(\n url: string | null,\n options?: { initialData?: T },\n): UseSelfFetchResult<T> {\n const initialData = options?.initialData\n const [data, setData] = useState<T | null>(initialData ?? null)\n const [isLoading, setIsLoading] = useState(initialData === undefined && url !== null)\n const [error, setError] = useState(false)\n const [reloadKey, setReloadKey] = useState(0)\n // The url whose data we currently hold — seeded from SSR `initialData`, then set after\n // each completed fetch. The effect skips the fetch when this equals `url` (so server-\n // rendered data isn't re-fetched on mount). A VALUE compare (not a one-shot flag) so the\n // SSR-hydration skip survives React 18 StrictMode's mount→unmount→remount in dev;\n // `reload()` nulls it to force a re-fetch of the same url.\n const dataUrlRef = useRef<string | null>(initialData !== undefined ? url : null)\n\n // Re-sync when a CONTROLLED `initialData` changes (e.g. the host navigates\n // between detail slugs without remounting). No-op in self-fetch mode, where\n // `initialData` is `undefined`.\n useEffect(() => {\n if (initialData !== undefined) setData(initialData)\n }, [initialData])\n\n useEffect(() => {\n if (url === null) {\n setIsLoading(false)\n return\n }\n // Already hold data for this exact url (SSR-hydrated, or a prior completed fetch) →\n // skip. `reload()` clears `dataUrlRef` so a retry of the same url still fetches.\n if (dataUrlRef.current === url) return\n const ctrl = new AbortController()\n let cancelled = false\n async function load() {\n try {\n setIsLoading(true)\n setError(false)\n const res = await fetch(url as string, { signal: ctrl.signal })\n if (!res.ok) throw new Error(`Request failed (${res.status})`)\n const json = (await res.json()) as T\n if (!cancelled) {\n setData(json)\n dataUrlRef.current = url // remember the url we now hold data for\n }\n } catch (err) {\n // AbortError on cleanup (unmount / stale-url change / React StrictMode's dev\n // double-invoke) is EXPECTED — the request was intentionally aborted. Aborting\n // also means the orphaned StrictMode fetch shows as \"cancelled\" instead of\n // completing as a wasted duplicate (parity with useChatIdentity). Only real\n // failures fall through to the error state + console.\n if (cancelled || (err as Error)?.name === 'AbortError') return\n setError(true)\n console.error('useSelfFetch:', url, err)\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => {\n cancelled = true\n ctrl.abort()\n }\n // `url` folds in every query param; `reloadKey` drives retry.\n }, [url, reloadKey])\n\n return {\n data,\n setData,\n isLoading,\n error,\n // Force a re-fetch of the current url: clear the held-url ref so the skip above\n // doesn't short-circuit, then bump the effect key.\n reload: () => {\n dataUrlRef.current = null\n setReloadKey((k) => k + 1)\n },\n }\n}\n"],"mappings":";;;AAEA,SAAS,WAAW,QAAQ,gBAAoD;AAkCzE,SAAS,aACd,KACA,SACuB;AACvB,QAAM,cAAc,SAAS;AAC7B,QAAM,CAAC,MAAM,OAAO,IAAI,SAAmB,eAAe,IAAI;AAC9D,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,gBAAgB,UAAa,QAAQ,IAAI;AACpF,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,KAAK;AACxC,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,CAAC;AAM5C,QAAM,aAAa,OAAsB,gBAAgB,SAAY,MAAM,IAAI;AAK/E,YAAU,MAAM;AACd,QAAI,gBAAgB,OAAW,SAAQ,WAAW;AAAA,EACpD,GAAG,CAAC,WAAW,CAAC;AAEhB,YAAU,MAAM;AACd,QAAI,QAAQ,MAAM;AAChB,mBAAa,KAAK;AAClB;AAAA,IACF;AAGA,QAAI,WAAW,YAAY,IAAK;AAChC,UAAM,OAAO,IAAI,gBAAgB;AACjC,QAAI,YAAY;AAChB,mBAAe,OAAO;AACpB,UAAI;AACF,qBAAa,IAAI;AACjB,iBAAS,KAAK;AACd,cAAM,MAAM,MAAM,MAAM,KAAe,EAAE,QAAQ,KAAK,OAAO,CAAC;AAC9D,YAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,mBAAmB,IAAI,MAAM,GAAG;AAC7D,cAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,YAAI,CAAC,WAAW;AACd,kBAAQ,IAAI;AACZ,qBAAW,UAAU;AAAA,QACvB;AAAA,MACF,SAAS,KAAK;AAMZ,YAAI,aAAc,KAAe,SAAS,aAAc;AACxD,iBAAS,IAAI;AACb,gBAAQ,MAAM,iBAAiB,KAAK,GAAG;AAAA,MACzC,UAAE;AACA,YAAI,CAAC,UAAW,cAAa,KAAK;AAAA,MACpC;AAAA,IACF;AACA,SAAK;AACL,WAAO,MAAM;AACX,kBAAY;AACZ,WAAK,MAAM;AAAA,IACb;AAAA,EAEF,GAAG,CAAC,KAAK,SAAS,CAAC;AAEnB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA;AAAA,IAGA,QAAQ,MAAM;AACZ,iBAAW,UAAU;AACrB,mBAAa,CAAC,MAAM,IAAI,CAAC;AAAA,IAC3B;AAAA,EACF;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-MEAR2WHS.cjs","../src/contexts/chat-runtime-context.tsx"],"names":[],"mappings":"AAAA,qFAAY;AACZ;AACA;ACiCA,8BAA0D;AA4JnD,IAAM,mBAAA,EAAqB,kCAAA,IAAsC,CAAA;AAQjE,SAAS,cAAA,CAAA,EAAqC;AACnD,EAAA,OAAO,+BAAA,kBAA6B,CAAA;AACtC;AAUO,SAAS,sBAAA,CAAA,EAAsC;AACpD,EAAA,MAAM,EAAA,EAAI,+BAAA,kBAA6B,CAAA;AACvC,EAAA,GAAA,CAAI,CAAC,CAAA,EAAG;AACN,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,IAMF,CAAA;AAAA,EACF;AACA,EAAA,OAAO,CAAA;AACT;AD/MA;AACA;AACE;AACA;AACA;AACF,kJAAC","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-MEAR2WHS.cjs","sourcesContent":[null,"'use client'\n\n/**\n * Chat runtime context — single seam for embedding the chat panel in a\n * different host (e.g. user1.openframe.ai reverse-proxying API calls\n * under /api/mingo-guide/* to hub.openframe.ai/api/*).\n *\n * Three concerns, one context:\n * 1. API endpoints: chatStreamUrl / approvalToolUrl / commandsUrl /\n * buildListUrl + attachment endpoints + chat-identity. The chat\n * reads them from runtime; hub vs embedded app supply different\n * strings via different providers.\n * 2. Navigation mode + callbacks: 'host' or 'embed' mode. Host wires\n * its own router/docNav via the optional `navigate` callback\n * (plain function, NOT a hook); embed forces new-tab via\n * `defaultContentOrigin` + lib's `resolveExternalNavigation`.\n * 3. Identity context: only `source` (required for localStorage\n * namespacing). The display identity (greeting first-name etc.)\n * comes from the server via `useChatIdentity()` — never injected\n * client-side, so it always matches the server-resolved auth.\n *\n * Sibling of EndpointsRuntimeContext (announcement bar, contact form,\n * access codes). Each runtime stays an independent React context so\n * embedders can opt into either feature without forcing the other.\n *\n * IMPORTANT for embedders: memoize the value passed to\n * `<ChatRuntimeContext.Provider value={...}>` (e.g. via React.useMemo).\n * Every change to its reference identity invalidates downstream\n * `useMemo` consumers (the chat input's slash-commands binding,\n * useNavLink's embed-resolution memo, useDocChat's streamFn factory).\n * The hub's `<HubRuntimeProvider>` already memoizes correctly with\n * stable deps. Embedded apps that build the value inline on each render\n * will pay an avoidable re-render cost across the entire chat tree.\n */\n\nimport { createContext, useContext, type ReactNode } from 'react'\n\nimport type { ComposeContentUrl } from '../utils/content-href'\n\n/**\n * Runtime config consumed by the chat panel.\n */\nexport interface ChatRuntime {\n endpoints: {\n /** POST streaming chat. Hub: '/api/docs/chat'. */\n chatStreamUrl: string\n /** POST agent approve/reject. Hub: '/api/chat/agent/confirm-tool'. */\n approvalToolUrl: string\n /** GET slash-command catalog. Hub: '/api/docs/commands'. */\n commandsUrl: string\n /** Build entity-card list URL for a content type + ids. Hub delegates\n * to the rag-table-config registry; embedded app provides its own\n * per-type URL builder against the reverse proxy. Returns null when\n * the type has no list endpoint (caller skips rendering). */\n buildListUrl: (type: string, ids: string[]) => string | null\n /** Chat-attachment endpoints — added for the v2 attachment feature.\n *\n * Three concerns:\n * - `attachmentUploadUrl` — POSTed by the chat-attachment hook\n * to mint a Supabase signed-upload-URL + HMAC view token.\n * - `attachmentViewUrlPrefix` — embedded in markdown URLs the\n * chat hosts in user message bubbles (`![]()` / `[Attached]`).\n * Stored in chat history; chosen at SEND time. In host mode the\n * relative `/api/storage/view/chat-attachments/` is sufficient\n * (same-origin); embedders supply an absolute hub URL so the\n * browser can fetch cross-origin.\n * - `identityUrl` — GET endpoint the `useChatIdentity` hook\n * hits to learn the `{authTier, source, attachmentsEnabled}`\n * capability bag for the current session. Used beyond chat\n * (tickets / contact form / any embedded surface that needs\n * to identify the proxied customer), so the name has no\n * \"chat\" prefix even though the consuming hook still does. */\n attachmentUploadUrl: string\n attachmentViewUrlPrefix: string\n identityUrl: string\n /** Optional URL prefix for the image proxy (`<prefix>?url=<external>`).\n * When unset, lib's `getProxiedImageUrl` returns the original URL\n * unchanged. Hub default: '/api/image-proxy'. Embedders that don't\n * host an image-proxy route leave this undefined → images load\n * directly cross-origin (CORS-permitting). */\n imageProxyUrlPrefix?: string\n /** Optional list of hostnames that should bypass the image proxy\n * (rendered direct). Hub uses ['openmsp.ai']; embedders typically\n * leave it unset. Matches the `skipDomains` parameter of\n * `getProxiedImageUrl`. */\n imageProxySkipDomains?: string[]\n /** Supabase storage origin (e.g. `https://xyz.supabase.co`) — used\n * by `useVideoWarmup` to scope the `<link rel=\"preload\" as=\"video\">`\n * hint to MP4s the deployment actually hosts. Hub wires it via\n * `getSupabaseStorageOrigin()`; embedders without a Supabase\n * storage origin leave it unset (preload is then skipped; Mux/\n * YouTube preconnect still fires). */\n supabaseStorageOrigin?: string\n }\n navigation: {\n /** ONE knob, two behaviors:\n * - 'host' = use the host page's existing click-routing untouched.\n * The chat panel calls `navigate?.()` for in-app routing.\n * - 'embed' = guest inside another app: short-circuit at the top\n * of click handlers to force new-tab + absolutize via\n * resolveExternalNavigation. */\n mode: 'host' | 'embed'\n /** Embed-only fallback origin for relative URLs whose target platform\n * can't be inferred. Used by resolveExternalNavigation when\n * `targetPlatform` is null — without this, a relative `/foo` href would\n * window.open against the embedder's origin, which is WRONG.\n * Set to your content host (e.g. 'https://hub.openframe.ai').\n * Required by the embedded app whenever mode='embed'. */\n defaultContentOrigin?: string\n /** Override for opening external URLs. MUST BE SYNCHRONOUS —\n * Safari/Firefox block popups opened outside a direct user gesture.\n * Default: window.open(href, '_blank', 'noopener,noreferrer'). */\n openExternal?: (href: string) => void\n /** Optional in-app navigation callback (host-mode only).\n * Returns `true` if the host handled the click in-app\n * (router.push + docNav.navigate); returns `false`, `undefined`,\n * or `void` → lib falls back to window.location.assign(href).\n * Hub wires this via HubRuntimeProvider's HubNavigationWiring;\n * embedders not in Next.js leave it undefined. */\n navigate?: (input: { href: string; path?: string | null; targetPlatform?: string | null }) => boolean | void\n /** Optional new-tab decision callback. Returns true → lib opens in\n * new tab; false → same tab via `navigate`. Hub wires the existing\n * `decideNewTab` logic from use-nav-link.tsx (re-imports the pure\n * helper from lib). Embedders may omit; lib defaults to:\n * same-origin/same-platform → same tab, else new tab. */\n decideNewTab?: (args: { href: string; targetPlatform?: string | null }) => boolean\n }\n /** Optional OG placeholder URL builder. Returns a branded\n * `/api/og-placeholder?...` URL for the given title. Hub wires this\n * to its `buildOgPlaceholderUrl` (resolves CSS-var ODS colors to\n * hex via the static map). Embedders can wire any equivalent that\n * hits their own placeholder route — or omit, in which case entity\n * cards fall back to no placeholder.\n *\n * Pure synchronous function — NOT a hook. Callers wrap with\n * `useMemo`/`useOgPlaceholder` for memoization. */\n resolvePlaceholderUrl?: (\n title: string,\n options?: { site?: string; aspect?: 'wide' | 'square' },\n ) => string\n /** Optional content-URL composer. Returns the platform-aware href +\n * target-platform tuple for a content entity. Hub wires this to its\n * `buildContentURL(type, slug, extractPrimaryPlatform(platforms))`\n * pipeline so the lib catalog/detail views can derive cross-\n * platform hrefs without knowing the hub's platform topology\n * (openmsp.ai / openframe.app / flamingo.run / tmcg).\n *\n * THE single content-href authority for every embeddable surface — page\n * views (onboarding catalog/detail, releases) AND chat cards / chips /\n * search results all resolve content links through this one seam, so a\n * given type lands in the SAME place regardless of where it's rendered.\n * Embedders wire `makeComposeContentUrl({ hostedTypes, contentOrigin })`;\n * omit it and lib views fall back to a same-origin relative path\n * (`buildDefaultHref`).\n *\n * Takes a single `ComposeContentUrlInput`: `type` + `identifier` (page\n * views pass the slug; chat rows pass the id + `externalUrl`, whose path\n * yields the slug for in-app routing) + optional `platforms` /\n * `externalUrl` / `targetPlatform`. */\n composeContentUrl?: ComposeContentUrl\n /** Per-`documentType` doc-viewer targets — the UNIFIED, DYNAMIC replacement for\n * the single `chipBasePlatform` prop. Maps a doc-table documentType\n * (`'markdown'`, `'data_room_doc'`, …) → `{ platform, basePath }` for the PUBLIC\n * doc viewer that hosts it. Doc chips with no `externalUrl` resolve PER ROW to\n * `getBaseUrl(platform)/<basePath>/<path>`, so a chat mixing several doc sources\n * sends EACH to its own home (markdown→flamingo/knowledge-base,\n * data_room_doc→company-hub/data-room) instead of one static fallback. The hub\n * may keep using `chipBasePlatform` (one doc source per platform); embedders that\n * surface multiple doc sources wire this. Threaded into `resolveSourceRowCTA`. */\n docPlatformTargets?: Record<string, { platform: string; basePath: string }>\n /** Chat source / platform identifier — OPTIONAL. The hub sets it from\n * `currentPlatform()`; EMBEDDERS leave it unset and stay platform-agnostic.\n *\n * It is NOT required for chat to work. The wire resolves source server-side\n * (`/docs/chat|search|commands` reject any client `source`); the\n * same-tab-vs-new-tab link decision falls back to an origin comparison when\n * it's absent (`decideNewTab` → `isCrossOriginUrl`); and the localStorage\n * history namespace falls back to a stable constant. Set it only where the\n * client legitimately needs to know its platform a priori — i.e. the hub,\n * where several platforms share related origins so \"same platform\" can't be\n * inferred from a URL alone. */\n source?: string\n // NOTE: No `user` field. The chat's display identity (greeting\n // first-name, etc.) comes from the SERVER-resolved auth via\n // `useChatIdentity()` — the same identity the server uses to\n // authorize requests. Letting embedders pass a client-side `user`\n // would let it desync from the actual auth tier, causing greetings\n // like \"Hey Bob\" while the server treats the session as\n // alice@example.com. Single source of truth: the server.\n}\n\nexport const ChatRuntimeContext = createContext<ChatRuntime | null>(null)\n\n/**\n * Returns the active runtime, or null when no provider is mounted.\n * NULL is a first-class value — it signals \"no chat runtime configured.\"\n * Optional consumers fall back to no-op behavior; strict consumers\n * use `useRequiredChatRuntime` (below).\n */\nexport function useChatRuntime(): ChatRuntime | null {\n return useContext(ChatRuntimeContext)\n}\n\n/**\n * Strict variant used INSIDE the chat panel. Throws if no provider.\n * The hub guarantees one exists by mounting `<HubRuntimeProvider>` at\n * root; the embedded app mounts its own `<ChatRuntimeContext.Provider>`\n * at the tree root. In Jest / Storybook tests that render chat\n * internals directly, wrap with `<HubRuntimeProvider>` (hub defaults)\n * or supply `<ChatRuntimeContext.Provider value={mockedRuntime}>`.\n */\nexport function useRequiredChatRuntime(): ChatRuntime {\n const v = useContext(ChatRuntimeContext)\n if (!v) {\n throw new Error(\n '[chat-runtime] hook called outside a <ChatRuntimeContext.Provider>. ' +\n 'The hub mounts <HubRuntimeProvider> at root — this only fires when ' +\n 'chat internals are rendered above the provider tree. ' +\n 'Fix: ensure the rendering subtree descends from the runtime provider. ' +\n 'In tests/Storybook: wrap with <HubRuntimeProvider> or supply ' +\n 'a <ChatRuntimeContext.Provider value={mockedRuntime}>.',\n )\n }\n return v\n}\n"]}
@@ -1,29 +0,0 @@
1
- "use client";
2
-
3
- // src/components/layout/page-heading.tsx
4
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
- var PAGE_HEADING_CLASS = "text-h1 text-ods-text-primary";
6
- var SECTION_HEADING_CLASS = "text-h2 text-ods-text-primary";
7
- var DESCRIPTION_CLASS = "mt-6 max-w-[640px] font-['DM_Sans'] text-[16px] md:text-[18px] leading-[24px] md:leading-[28px] text-ods-text-secondary";
8
- function PageHeading({
9
- children,
10
- as: Tag = "h1",
11
- description,
12
- className,
13
- descriptionClassName
14
- }) {
15
- const headingClass = className ? `${PAGE_HEADING_CLASS} ${className}` : PAGE_HEADING_CLASS;
16
- const descClass = descriptionClassName ? `${DESCRIPTION_CLASS} ${descriptionClassName}` : DESCRIPTION_CLASS;
17
- const hasDescription = description != null && description !== "" && typeof description !== "boolean";
18
- return /* @__PURE__ */ jsxs(Fragment, { children: [
19
- /* @__PURE__ */ jsx(Tag, { className: headingClass, children }),
20
- hasDescription && /* @__PURE__ */ jsx("p", { className: descClass, children: description })
21
- ] });
22
- }
23
-
24
- export {
25
- PAGE_HEADING_CLASS,
26
- SECTION_HEADING_CLASS,
27
- PageHeading
28
- };
29
- //# sourceMappingURL=chunk-OO4ZGDRO.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/layout/page-heading.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\n\n/**\n * THE page-title style — the ODS `text-h1` token (`--font-size-h1-title` =\n * 40 / 48 / 56px, Azeret Mono semibold), which already owns the responsive\n * letter-spacing (-0.02em). Mirrors the global `h1 { @apply text-h1 }` base, so\n * every page heading is the SAME size across the lib and every consuming app.\n *\n * Single source of truth — do NOT hardcode a px size or re-assert the token's\n * tracking; render `<PageHeading>` (or, for the rare non-h1 caller, apply\n * `PAGE_HEADING_CLASS`).\n */\nexport const PAGE_HEADING_CLASS = 'text-h1 text-ods-text-primary'\n\n/**\n * THE section-heading style — the ODS `text-h2` sub-title token\n * (`--font-size-h2-sub-title` = 24 / 32 / 32px), one step below the page\n * title so a section `<h2>`/`<h3>` is always visually distinguishable from\n * the page's `<h1>`. Same single-source rule as PAGE_HEADING_CLASS: never\n * hardcode a px ramp or re-assert the token's tracking on a section heading.\n */\nexport const SECTION_HEADING_CLASS = 'text-h2 text-ods-text-primary'\n\nconst DESCRIPTION_CLASS =\n \"mt-6 max-w-[640px] font-['DM_Sans'] text-[16px] md:text-[18px] leading-[24px] md:leading-[28px] text-ods-text-secondary\"\n\nexport interface PageHeadingProps {\n /** Heading content — plain text or nodes (e.g. an accent <span>). */\n children: ReactNode\n /**\n * Heading level. Defaults to 'h1' (exactly one per page). Pass 'h2' where the\n * page already renders an <h1> above (e.g. a hero/featured item).\n */\n as?: 'h1' | 'h2'\n /** Optional supporting copy rendered as a <p> beneath the heading. */\n description?: ReactNode\n /** Extra classes merged onto the heading (margins, width, truncate, etc.). */\n className?: string\n /** Extra classes merged onto the description <p>. */\n descriptionClassName?: string\n}\n\n/**\n * Unified page heading. Renders the canonical page-title <h1> (or <h2>) plus an\n * optional description, so every page shares one consistent style instead of\n * duplicating the class string. Layout (PageContainer, margins, surrounding\n * sections) stays with the caller — this owns only the heading + description.\n */\nexport function PageHeading({\n children,\n as: Tag = 'h1',\n description,\n className,\n descriptionClassName,\n}: PageHeadingProps) {\n const headingClass = className ? `${PAGE_HEADING_CLASS} ${className}` : PAGE_HEADING_CLASS\n const descClass = descriptionClassName ? `${DESCRIPTION_CLASS} ${descriptionClassName}` : DESCRIPTION_CLASS\n // `description` is a ReactNode, so `description={cond && '...'}` can pass a\n // boolean `false` — exclude it (and empty string) so we never render an empty\n // <p> that adds phantom vertical gap beneath the heading.\n const hasDescription =\n description != null && description !== '' && typeof description !== 'boolean'\n return (\n <>\n <Tag className={headingClass}>{children}</Tag>\n {hasDescription && <p className={descClass}>{description}</p>}\n </>\n )\n}\n"],"mappings":";;;AA+DI,mBACE,KADF;AAnDG,IAAM,qBAAqB;AAS3B,IAAM,wBAAwB;AAErC,IAAM,oBACJ;AAwBK,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,IAAI,MAAM;AAAA,EACV;AAAA,EACA;AAAA,EACA;AACF,GAAqB;AACnB,QAAM,eAAe,YAAY,GAAG,kBAAkB,IAAI,SAAS,KAAK;AACxE,QAAM,YAAY,uBAAuB,GAAG,iBAAiB,IAAI,oBAAoB,KAAK;AAI1F,QAAM,iBACJ,eAAe,QAAQ,gBAAgB,MAAM,OAAO,gBAAgB;AACtE,SACE,iCACE;AAAA,wBAAC,OAAI,WAAW,cAAe,UAAS;AAAA,IACvC,kBAAkB,oBAAC,OAAE,WAAW,WAAY,uBAAY;AAAA,KAC3D;AAEJ;","names":[]}