@flamingo-stack/openframe-frontend-core 0.0.519 → 0.0.520
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-QQZWLTYU.cjs → chunk-2HL3BXWV.cjs} +4 -4
- package/dist/{chunk-QQZWLTYU.cjs.map → chunk-2HL3BXWV.cjs.map} +1 -1
- package/dist/{chunk-HWA3NZPK.js → chunk-2Y4REKE5.js} +2 -2
- package/dist/{chunk-ZGGI4XMF.cjs → chunk-37OBDW56.cjs} +9 -9
- package/dist/{chunk-ZGGI4XMF.cjs.map → chunk-37OBDW56.cjs.map} +1 -1
- package/dist/{chunk-QEGGWVFV.cjs → chunk-4NEP3KY7.cjs} +26 -26
- package/dist/{chunk-QEGGWVFV.cjs.map → chunk-4NEP3KY7.cjs.map} +1 -1
- package/dist/{chunk-HVXGSQ4V.js → chunk-5PT2RQRU.js} +6 -6
- package/dist/{chunk-6FLRFCK4.cjs → chunk-5ZQKEGL3.cjs} +870 -796
- package/dist/chunk-5ZQKEGL3.cjs.map +1 -0
- package/dist/{chunk-ZA5PFNHR.js → chunk-AQHR5D7X.js} +324 -250
- package/dist/chunk-AQHR5D7X.js.map +1 -0
- package/dist/{chunk-BUZOZOQ2.js → chunk-DMITCSFH.js} +2 -2
- package/dist/{chunk-VH7BOHRG.js → chunk-HI3TQS4D.js} +2 -2
- package/dist/{chunk-S4OAWVKF.js → chunk-JKICCXLC.js} +2 -2
- package/dist/{chunk-QGLEI6T2.cjs → chunk-JZC5BBAF.cjs} +5 -5
- package/dist/{chunk-QGLEI6T2.cjs.map → chunk-JZC5BBAF.cjs.map} +1 -1
- package/dist/{chunk-JMGVYERE.js → chunk-LB66ECD7.js} +2 -2
- package/dist/{chunk-MEWHQVOU.js → chunk-P7AQ5XKO.js} +2 -2
- package/dist/{chunk-5PMRHRMM.cjs → chunk-PA6L55NF.cjs} +14 -14
- package/dist/{chunk-5PMRHRMM.cjs.map → chunk-PA6L55NF.cjs.map} +1 -1
- package/dist/{chunk-YACGPO7I.js → chunk-QICUWGET.js} +2 -2
- package/dist/{chunk-OBJSXQ67.cjs → chunk-R3Y65KPT.cjs} +3 -3
- package/dist/{chunk-OBJSXQ67.cjs.map → chunk-R3Y65KPT.cjs.map} +1 -1
- package/dist/{chunk-FWCENA3G.cjs → chunk-RRCSGTT5.cjs} +31 -31
- package/dist/{chunk-FWCENA3G.cjs.map → chunk-RRCSGTT5.cjs.map} +1 -1
- package/dist/{chunk-SAPKMN4W.js → chunk-RT4IGTJ6.js} +2 -2
- package/dist/{chunk-UBT2KBK7.js → chunk-RTJ6Q7JA.js} +5 -5
- package/dist/{chunk-KCDKQXE5.cjs → chunk-S5HO5E6R.cjs} +7 -7
- package/dist/{chunk-KCDKQXE5.cjs.map → chunk-S5HO5E6R.cjs.map} +1 -1
- package/dist/{chunk-CCK2MFIQ.cjs → chunk-SVYXGHJG.cjs} +26 -26
- package/dist/{chunk-CCK2MFIQ.cjs.map → chunk-SVYXGHJG.cjs.map} +1 -1
- package/dist/{chunk-BFCBR277.cjs → chunk-TRIU3RJD.cjs} +11 -11
- package/dist/{chunk-BFCBR277.cjs.map → chunk-TRIU3RJD.cjs.map} +1 -1
- package/dist/{chunk-GLFQSFYV.cjs → chunk-UDPOAWKH.cjs} +61 -61
- package/dist/{chunk-GLFQSFYV.cjs.map → chunk-UDPOAWKH.cjs.map} +1 -1
- package/dist/{chunk-CRJRVUWB.js → chunk-UOMUR62H.js} +2 -2
- package/dist/{chunk-OQHHJDUB.js → chunk-W3UAL5GU.js} +4 -4
- package/dist/{chunk-6WQN7NLD.js → chunk-Y43UWXF7.js} +4 -4
- package/dist/{chunk-WEPJKF65.cjs → chunk-YD7JM53S.cjs} +37 -37
- package/dist/{chunk-WEPJKF65.cjs.map → chunk-YD7JM53S.cjs.map} +1 -1
- package/dist/{chunk-V3MAG3G5.cjs → chunk-YPHKO6C5.cjs} +87 -87
- package/dist/{chunk-V3MAG3G5.cjs.map → chunk-YPHKO6C5.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/help-center-pages/index.cjs +22 -22
- package/dist/components/help-center-pages/index.js +13 -13
- package/dist/components/index.cjs +132 -132
- package/dist/components/index.js +11 -11
- package/dist/components/meeting-scheduler/index.cjs +34 -34
- package/dist/components/meeting-scheduler/index.js +3 -3
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/index.cjs +2 -2
- package/dist/components/ui/index.js +1 -1
- package/dist/components/ui/modal-v2.d.ts.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/components/ui/modal-v2.tsx +95 -1
- package/dist/chunk-6FLRFCK4.cjs.map +0 -1
- package/dist/chunk-ZA5PFNHR.js.map +0 -1
- /package/dist/{chunk-HWA3NZPK.js.map → chunk-2Y4REKE5.js.map} +0 -0
- /package/dist/{chunk-HVXGSQ4V.js.map → chunk-5PT2RQRU.js.map} +0 -0
- /package/dist/{chunk-BUZOZOQ2.js.map → chunk-DMITCSFH.js.map} +0 -0
- /package/dist/{chunk-VH7BOHRG.js.map → chunk-HI3TQS4D.js.map} +0 -0
- /package/dist/{chunk-S4OAWVKF.js.map → chunk-JKICCXLC.js.map} +0 -0
- /package/dist/{chunk-JMGVYERE.js.map → chunk-LB66ECD7.js.map} +0 -0
- /package/dist/{chunk-MEWHQVOU.js.map → chunk-P7AQ5XKO.js.map} +0 -0
- /package/dist/{chunk-YACGPO7I.js.map → chunk-QICUWGET.js.map} +0 -0
- /package/dist/{chunk-SAPKMN4W.js.map → chunk-RT4IGTJ6.js.map} +0 -0
- /package/dist/{chunk-UBT2KBK7.js.map → chunk-RTJ6Q7JA.js.map} +0 -0
- /package/dist/{chunk-CRJRVUWB.js.map → chunk-UOMUR62H.js.map} +0 -0
- /package/dist/{chunk-OQHHJDUB.js.map → chunk-W3UAL5GU.js.map} +0 -0
- /package/dist/{chunk-6WQN7NLD.js.map → chunk-Y43UWXF7.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-BFCBR277.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-BFCBR277.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"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-TRIU3RJD.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-TRIU3RJD.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"]}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
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
2
|
|
|
3
3
|
|
|
4
|
-
var
|
|
4
|
+
var _chunkR3Y65KPTcjs = require('./chunk-R3Y65KPT.cjs');
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
var
|
|
7
|
+
var _chunkPA6L55NFcjs = require('./chunk-PA6L55NF.cjs');
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
var
|
|
10
|
+
var _chunk2HL3BXWVcjs = require('./chunk-2HL3BXWV.cjs');
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
var
|
|
13
|
+
var _chunkJZC5BBAFcjs = require('./chunk-JZC5BBAF.cjs');
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
var _chunkGYONSC3Ucjs = require('./chunk-GYONSC3U.cjs');
|
|
@@ -45,7 +45,7 @@ var _chunkGYONSC3Ucjs = require('./chunk-GYONSC3U.cjs');
|
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
var
|
|
48
|
+
var _chunk5ZQKEGL3cjs = require('./chunk-5ZQKEGL3.cjs');
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
|
|
@@ -76,8 +76,8 @@ var _chunkKI4EGKNVcjs = require('./chunk-KI4EGKNV.cjs');
|
|
|
76
76
|
_chunkKI4EGKNVcjs.init_cn.call(void 0, );
|
|
77
77
|
var _jsxruntime = require('react/jsx-runtime');
|
|
78
78
|
var DEFAULT_ENDPOINT = "/api/releases";
|
|
79
|
-
var DEFAULT_SEARCH_PARAM_KEY =
|
|
80
|
-
var DEFAULT_STATUS_PARAM_KEY =
|
|
79
|
+
var DEFAULT_SEARCH_PARAM_KEY = _chunk5ZQKEGL3cjs.DEV_SECTION_PARAM_KEYS.search;
|
|
80
|
+
var DEFAULT_STATUS_PARAM_KEY = _chunk5ZQKEGL3cjs.DEV_SECTION_PARAM_KEYS.releaseStatus;
|
|
81
81
|
var DEFAULT_PAGE_PARAM_KEY = "page";
|
|
82
82
|
function ReleaseRow({
|
|
83
83
|
release,
|
|
@@ -86,17 +86,17 @@ function ReleaseRow({
|
|
|
86
86
|
}) {
|
|
87
87
|
const runtime = _chunk7OPWSWEJcjs.useChatRuntime.call(void 0, );
|
|
88
88
|
const router = _chunkSSJNN5BScjs.useRouter.call(void 0, );
|
|
89
|
-
const cta =
|
|
89
|
+
const cta = _chunk5ZQKEGL3cjs.resolveContentHref.call(void 0, _optionalChain([runtime, 'optionalAccess', _2 => _2.composeContentUrl]), {
|
|
90
90
|
type: "product_release",
|
|
91
91
|
slug: release.slug,
|
|
92
92
|
basePath,
|
|
93
93
|
platforms: release.product_release_platforms
|
|
94
94
|
});
|
|
95
|
-
const { target, rel } =
|
|
95
|
+
const { target, rel } = _chunk5ZQKEGL3cjs.useEntityCardLink.call(void 0, { href: cta.href, targetPlatform: cta.targetPlatform });
|
|
96
96
|
const onClick = (e) => {
|
|
97
|
-
if (e.defaultPrevented ||
|
|
97
|
+
if (e.defaultPrevented || _chunk5ZQKEGL3cjs.isModifierClick.call(void 0, e) || target === "_blank") return;
|
|
98
98
|
e.preventDefault();
|
|
99
|
-
|
|
99
|
+
_chunk5ZQKEGL3cjs.executeNavigationImperative.call(void 0, {
|
|
100
100
|
runtime,
|
|
101
101
|
href: cta.href,
|
|
102
102
|
targetPlatform: cta.targetPlatform,
|
|
@@ -104,7 +104,7 @@ function ReleaseRow({
|
|
|
104
104
|
});
|
|
105
105
|
};
|
|
106
106
|
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
107
|
-
|
|
107
|
+
_chunk5ZQKEGL3cjs.ProductReleaseCard,
|
|
108
108
|
{
|
|
109
109
|
size: "lg",
|
|
110
110
|
title: release.title,
|
|
@@ -120,7 +120,7 @@ function ProductReleasesView({
|
|
|
120
120
|
initialData,
|
|
121
121
|
itemsPerPage = 5,
|
|
122
122
|
basePath = "/releases",
|
|
123
|
-
buildCardProps =
|
|
123
|
+
buildCardProps = _chunk5ZQKEGL3cjs.buildProductReleaseCardProps,
|
|
124
124
|
searchParamKey = DEFAULT_SEARCH_PARAM_KEY,
|
|
125
125
|
statusParamKey = DEFAULT_STATUS_PARAM_KEY,
|
|
126
126
|
pageParamKey = DEFAULT_PAGE_PARAM_KEY,
|
|
@@ -158,10 +158,10 @@ function ProductReleasesView({
|
|
|
158
158
|
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
|
|
159
159
|
};
|
|
160
160
|
if (error) {
|
|
161
|
-
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: _chunkKI4EGKNVcjs.cn.call(void 0, "w-full", className), children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
161
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: _chunkKI4EGKNVcjs.cn.call(void 0, "w-full", className), children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk5ZQKEGL3cjs.LoadError, { message: "Failed to load releases.", onRetry: reload }) });
|
|
162
162
|
}
|
|
163
163
|
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: _chunkKI4EGKNVcjs.cn.call(void 0, "w-full flex flex-col gap-[40px]", className), children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "min-h-[600px]", children: showEmpty ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "h-[600px] flex items-center justify-center", children: hasActiveFilters ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
164
|
-
|
|
164
|
+
_chunk2HL3BXWVcjs.EmptyState,
|
|
165
165
|
{
|
|
166
166
|
type: "search",
|
|
167
167
|
title: "No releases found",
|
|
@@ -171,7 +171,7 @@ function ProductReleasesView({
|
|
|
171
171
|
onCtaClick: resetFilters
|
|
172
172
|
}
|
|
173
173
|
) : /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
174
|
-
|
|
174
|
+
_chunk2HL3BXWVcjs.EmptyState,
|
|
175
175
|
{
|
|
176
176
|
type: "generic",
|
|
177
177
|
title: "No releases available",
|
|
@@ -186,13 +186,13 @@ function ProductReleasesView({
|
|
|
186
186
|
"div",
|
|
187
187
|
{
|
|
188
188
|
style: { visibility: isLoading || hasData ? "visible" : "hidden" },
|
|
189
|
-
children: isLoading ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
189
|
+
children: isLoading ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk5ZQKEGL3cjs.ProductReleaseCardSkeleton, { size: "lg" }) : release ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, ReleaseRow, { release, basePath, buildCardProps }) : /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk5ZQKEGL3cjs.ProductReleaseCardSkeleton, { size: "lg" })
|
|
190
190
|
},
|
|
191
191
|
_nullishCoalesce(_optionalChain([release, 'optionalAccess', _5 => _5.id]), () => ( `slot-${i}`))
|
|
192
192
|
);
|
|
193
193
|
}) }),
|
|
194
194
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "mt-6 md:mt-8 flex justify-center", children: isLoading ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "h-12 m-3 w-64" }) : releases.length > 0 && totalPages > 1 ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
195
|
-
|
|
195
|
+
_chunkR3Y65KPTcjs.PersistentPaginationWrapper,
|
|
196
196
|
{
|
|
197
197
|
isLoading: false,
|
|
198
198
|
currentPage,
|
|
@@ -234,10 +234,10 @@ function MediaGalleryStrip({ items, className }) {
|
|
|
234
234
|
mediaItem.id || index
|
|
235
235
|
);
|
|
236
236
|
}
|
|
237
|
-
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: tileClass, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
237
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: tileClass, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk5ZQKEGL3cjs.Video, { url: mediaItem.media_url, layout: "native" }) }, mediaItem.id || index);
|
|
238
238
|
}) }),
|
|
239
239
|
galleryImages.length > 0 && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
240
|
-
|
|
240
|
+
_chunk5ZQKEGL3cjs.ImageGalleryModal,
|
|
241
241
|
{
|
|
242
242
|
images: galleryImages,
|
|
243
243
|
isOpen: galleryOpen,
|
|
@@ -254,7 +254,7 @@ _chunkSSJNN5BScjs.init_next_navigation.call(void 0, );
|
|
|
254
254
|
|
|
255
255
|
var _lucidereact = require('lucide-react');
|
|
256
256
|
|
|
257
|
-
var DefaultMarkdownRenderer =
|
|
257
|
+
var DefaultMarkdownRenderer = _chunk5ZQKEGL3cjs.RichMarkdownRenderer;
|
|
258
258
|
function ReleaseDetailPage({
|
|
259
259
|
authorHref,
|
|
260
260
|
slug,
|
|
@@ -271,7 +271,7 @@ function ReleaseDetailPage({
|
|
|
271
271
|
shell = true
|
|
272
272
|
}) {
|
|
273
273
|
const router = _chunkSSJNN5BScjs.useRouter.call(void 0, );
|
|
274
|
-
const renderShell = (node) => shell ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
274
|
+
const renderShell = (node) => shell ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk5ZQKEGL3cjs.PageShell, { children: node }) : /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "page-shell-content", children: node });
|
|
275
275
|
const { data: fetchedRelease, error, isLoading } = useRelease(initialData ? void 0 : slug);
|
|
276
276
|
const release = initialData || fetchedRelease;
|
|
277
277
|
const showBackButton = backButton !== false;
|
|
@@ -315,7 +315,7 @@ function ReleaseDetailPage({
|
|
|
315
315
|
return renderShell(
|
|
316
316
|
// Match the loaded page's top offset (TitleBlock's
|
|
317
317
|
// `pt-[var(--spacing-system-l)]`) so content doesn't jump on load.
|
|
318
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "pt-[var(--spacing-system-l)]", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
318
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "pt-[var(--spacing-system-l)]", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkJZC5BBAFcjs.DetailPageSkeleton, { bare: true, metadataColumns: 4, showImageGallery: true }) })
|
|
319
319
|
);
|
|
320
320
|
}
|
|
321
321
|
if (error || !release) {
|
|
@@ -351,7 +351,7 @@ function ReleaseDetailPage({
|
|
|
351
351
|
const improvements = release.improvements;
|
|
352
352
|
return renderShell(
|
|
353
353
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
354
|
-
|
|
354
|
+
_chunk5ZQKEGL3cjs.PageLayout,
|
|
355
355
|
{
|
|
356
356
|
title: releaseTitle,
|
|
357
357
|
subtitle: `Version: ${releaseVersion}`,
|
|
@@ -360,7 +360,7 @@ function ReleaseDetailPage({
|
|
|
360
360
|
backButton: showBackButton ? { label: backLabel, onClick: () => router.push(backHref) } : void 0,
|
|
361
361
|
children: [
|
|
362
362
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "space-y-6 md:space-y-8", children: [
|
|
363
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
363
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk5ZQKEGL3cjs.EntityTagBadges, { tags: release.product_release_tags }),
|
|
364
364
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "grid grid-cols-1 md:grid-cols-4 border border-ods-border rounded-md overflow-hidden w-full", children: [
|
|
365
365
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "bg-ods-card border-b md:border-b-0 md:border-r border-ods-border p-4 flex flex-col gap-3", children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex flex-col gap-0", children: [
|
|
366
366
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { className: "text-h4 text-ods-text-primary", children: releaseType.toLocaleUpperCase() }),
|
|
@@ -371,11 +371,11 @@ function ReleaseDetailPage({
|
|
|
371
371
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { className: "text-h6 text-ods-text-secondary", children: "Release Status" })
|
|
372
372
|
] }) }),
|
|
373
373
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "bg-ods-card border-b md:border-b-0 md:border-r border-ods-border p-4 flex flex-col gap-3", children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex flex-col gap-0", children: [
|
|
374
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { className: "text-h4 text-ods-text-primary", children:
|
|
374
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { className: "text-h4 text-ods-text-primary", children: _chunk5ZQKEGL3cjs.formatReleaseDate.call(void 0, releaseDate) }),
|
|
375
375
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { className: "text-h6 text-ods-text-secondary", children: "Release Date" })
|
|
376
376
|
] }) }),
|
|
377
377
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
378
|
-
|
|
378
|
+
_chunk5ZQKEGL3cjs.EntityMetadataAuthorCell,
|
|
379
379
|
{
|
|
380
380
|
author: _nullishCoalesce(author, () => ( { full_name: null, avatar_url: null })),
|
|
381
381
|
authorHref: _optionalChain([author, 'optionalAccess', _6 => _6.full_name]) ? authorHref : void 0
|
|
@@ -393,16 +393,16 @@ function ReleaseDetailPage({
|
|
|
393
393
|
highlightVideoThumbnail,
|
|
394
394
|
title: releaseTitle,
|
|
395
395
|
videoBites,
|
|
396
|
-
bitesTitle:
|
|
396
|
+
bitesTitle: _chunk5ZQKEGL3cjs.DEFAULT_VIDEO_BITES_TITLE,
|
|
397
397
|
filterPublishedBites: true,
|
|
398
|
-
bitesProfile:
|
|
398
|
+
bitesProfile: _chunk5ZQKEGL3cjs.toStripProfile.call(void 0, author),
|
|
399
399
|
bitesHref: typeof release.slug === "string" ? `/releases/${release.slug}` : void 0,
|
|
400
400
|
srtContent: _optionalChain([release, 'optionalAccess', _7 => _7.srt_content]),
|
|
401
401
|
captionsUrl: _optionalChain([release, 'optionalAccess', _8 => _8.captionsUrl])
|
|
402
402
|
}
|
|
403
403
|
) : /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
|
|
404
404
|
youtubeUrl && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
405
|
-
|
|
405
|
+
_chunk5ZQKEGL3cjs.Video,
|
|
406
406
|
{
|
|
407
407
|
kind: "youtube",
|
|
408
408
|
url: youtubeUrl,
|
|
@@ -411,7 +411,7 @@ function ReleaseDetailPage({
|
|
|
411
411
|
}
|
|
412
412
|
),
|
|
413
413
|
!youtubeUrl && mainVideoUrl && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
414
|
-
|
|
414
|
+
_chunk5ZQKEGL3cjs.Video,
|
|
415
415
|
{
|
|
416
416
|
url: mainVideoUrl,
|
|
417
417
|
srtContent: _optionalChain([release, 'optionalAccess', _9 => _9.srt_content]),
|
|
@@ -420,7 +420,7 @@ function ReleaseDetailPage({
|
|
|
420
420
|
}
|
|
421
421
|
),
|
|
422
422
|
highlightVideoUrl && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
423
|
-
|
|
423
|
+
_chunk5ZQKEGL3cjs.Video,
|
|
424
424
|
{
|
|
425
425
|
url: highlightVideoUrl,
|
|
426
426
|
poster: highlightVideoThumbnail,
|
|
@@ -429,7 +429,7 @@ function ReleaseDetailPage({
|
|
|
429
429
|
)
|
|
430
430
|
] }),
|
|
431
431
|
releaseContent && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "text-h4 text-ods-text-primary", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, MarkdownRenderer, { content: releaseContent }) }),
|
|
432
|
-
hasBreakingChanges && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
432
|
+
hasBreakingChanges && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk5ZQKEGL3cjs.Card, { className: "border-ods-error bg-ods-error/10", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk5ZQKEGL3cjs.CardContent, { className: "p-6", children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex items-center gap-3", children: [
|
|
433
433
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.AlertTriangle, { className: "h-6 w-6 text-ods-error" }),
|
|
434
434
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { children: [
|
|
435
435
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "h3", { className: "text-h3 text-ods-error", children: "Breaking Changes" }),
|
|
@@ -437,7 +437,7 @@ function ReleaseDetailPage({
|
|
|
437
437
|
] })
|
|
438
438
|
] }) }) }),
|
|
439
439
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
440
|
-
|
|
440
|
+
_chunk5ZQKEGL3cjs.ReleaseChangelogSection,
|
|
441
441
|
{
|
|
442
442
|
title: "Breaking Changes",
|
|
443
443
|
entries: breakingChanges || [],
|
|
@@ -448,7 +448,7 @@ function ReleaseDetailPage({
|
|
|
448
448
|
}
|
|
449
449
|
),
|
|
450
450
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
451
|
-
|
|
451
|
+
_chunk5ZQKEGL3cjs.ReleaseChangelogSection,
|
|
452
452
|
{
|
|
453
453
|
title: "Features Added",
|
|
454
454
|
entries: featuresAdded || [],
|
|
@@ -458,7 +458,7 @@ function ReleaseDetailPage({
|
|
|
458
458
|
}
|
|
459
459
|
),
|
|
460
460
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
461
|
-
|
|
461
|
+
_chunk5ZQKEGL3cjs.ReleaseChangelogSection,
|
|
462
462
|
{
|
|
463
463
|
title: "Bugs Fixed",
|
|
464
464
|
entries: bugFixed || [],
|
|
@@ -468,7 +468,7 @@ function ReleaseDetailPage({
|
|
|
468
468
|
}
|
|
469
469
|
),
|
|
470
470
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
471
|
-
|
|
471
|
+
_chunk5ZQKEGL3cjs.ReleaseChangelogSection,
|
|
472
472
|
{
|
|
473
473
|
title: "Improvements",
|
|
474
474
|
entries: improvements || [],
|
|
@@ -499,7 +499,7 @@ function ReleaseDetailPage({
|
|
|
499
499
|
(() => {
|
|
500
500
|
const deliveryCount = deliveryData ? deliveryData.completed.length + deliveryData.inProgress.length : 0;
|
|
501
501
|
return deliveryLoading ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, DeliverySection, { data: deliveryData, isLoading: deliveryLoading }) : /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
502
|
-
|
|
502
|
+
_chunk5ZQKEGL3cjs.FadePreview,
|
|
503
503
|
{
|
|
504
504
|
hiddenCount: deliveryCount - 1,
|
|
505
505
|
collapsedHeight: 300,
|
|
@@ -511,7 +511,7 @@ function ReleaseDetailPage({
|
|
|
511
511
|
] }),
|
|
512
512
|
(_optionalChain([githubReleases, 'optionalAccess', _11 => _11.length]) || _optionalChain([knowledgeBaseLinks, 'optionalAccess', _12 => _12.length]) || migrationGuideUrl || documentationUrl) && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "space-y-1 w-full", children: [
|
|
513
513
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { className: "text-h5 tracking-[-0.28px] text-ods-text-secondary", children: "Related Links" }),
|
|
514
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
514
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk5ZQKEGL3cjs.Card, { className: "bg-ods-card border-ods-border p-6", children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "space-y-4", children: [
|
|
515
515
|
githubReleases && githubReleases.length > 0 && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _jsxruntime.Fragment, { children: githubReleases.map((ghRelease) => /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex items-start gap-1", children: [
|
|
516
516
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk3M2MFP2Ycjs.GitHubIcon, { className: "shrink-0", width: 24, height: 24, color: "var(--color-text-secondary)" }),
|
|
517
517
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "text-h4 text-ods-text-primary", children: "Github Release" }),
|
|
@@ -585,7 +585,7 @@ function ReleaseDetailPage({
|
|
|
585
585
|
// src/components/shared/product-release/release-detail-skeleton.tsx
|
|
586
586
|
|
|
587
587
|
function ReleaseDetailSkeleton() {
|
|
588
|
-
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
588
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkJZC5BBAFcjs.DetailPageSkeleton, { metadataColumns: 4, showImageGallery: true });
|
|
589
589
|
}
|
|
590
590
|
|
|
591
591
|
// src/components/shared/roadmap/use-roadmap-voting.ts
|
|
@@ -746,10 +746,10 @@ function RoadmapGridSingle({
|
|
|
746
746
|
// `useScrollToHash` in `roadmap-view.tsx` finds the card by id
|
|
747
747
|
// and scrolls.
|
|
748
748
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
749
|
-
|
|
749
|
+
_chunk5ZQKEGL3cjs.RoadmapCard,
|
|
750
750
|
{
|
|
751
751
|
item,
|
|
752
|
-
id:
|
|
752
|
+
id: _chunk5ZQKEGL3cjs.devSectionAnchorId.call(void 0, "roadmap", item.id),
|
|
753
753
|
userVote: getVote(item.id),
|
|
754
754
|
onVote: (voteType) => onVote(item.id, voteType),
|
|
755
755
|
isVoting: votingTasks.has(item.id)
|
|
@@ -829,7 +829,7 @@ function RoadmapGrid({
|
|
|
829
829
|
}, [hasActiveFilters, sortedQuartersKey, isInitialized, quartersToKeepClosed]);
|
|
830
830
|
if (items.length === 0) {
|
|
831
831
|
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
832
|
-
|
|
832
|
+
_chunk2HL3BXWVcjs.EmptyState,
|
|
833
833
|
{
|
|
834
834
|
type: "generic",
|
|
835
835
|
title: "No roadmap items",
|
|
@@ -850,7 +850,7 @@ function RoadmapGrid({
|
|
|
850
850
|
);
|
|
851
851
|
}
|
|
852
852
|
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
853
|
-
|
|
853
|
+
_chunk5ZQKEGL3cjs.Accordion,
|
|
854
854
|
{
|
|
855
855
|
type: "multiple",
|
|
856
856
|
value: expandedQuarters,
|
|
@@ -860,13 +860,13 @@ function RoadmapGrid({
|
|
|
860
860
|
const itemCount = _optionalChain([itemsByQuarter, 'access', _21 => _21[quarter], 'optionalAccess', _22 => _22.length]) || 0;
|
|
861
861
|
const isExpanded = expandedQuarters.includes(quarter);
|
|
862
862
|
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
863
|
-
|
|
863
|
+
_chunk5ZQKEGL3cjs.AccordionItem,
|
|
864
864
|
{
|
|
865
865
|
value: quarter,
|
|
866
866
|
id: `quarter-${quarter.replace(/\s+/g, "-").toLowerCase()}`,
|
|
867
867
|
className: "border-0",
|
|
868
868
|
children: [
|
|
869
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
869
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk5ZQKEGL3cjs.AccordionTrigger, { className: "w-full p-0 hover:no-underline [&>svg]:h-5 [&>svg]:w-5 [&>svg]:text-ods-text-secondary [&>svg]:ml-auto [&>svg]:shrink-0", children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex items-center gap-3", children: [
|
|
870
870
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
871
871
|
"h3",
|
|
872
872
|
{
|
|
@@ -896,7 +896,7 @@ function RoadmapGrid({
|
|
|
896
896
|
}
|
|
897
897
|
)
|
|
898
898
|
] }) }),
|
|
899
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
899
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk5ZQKEGL3cjs.AccordionContent, { className: "pt-4 pb-0 overflow-hidden data-[state=closed]:animate-none data-[state=open]:animate-none", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
900
900
|
RoadmapGridSingle,
|
|
901
901
|
{
|
|
902
902
|
items: itemsByQuarter[quarter],
|
|
@@ -947,8 +947,8 @@ function RoadmapGridSkeleton({ count = 4, showLeftMargin = true }) {
|
|
|
947
947
|
|
|
948
948
|
|
|
949
949
|
var DEFAULT_ENDPOINT2 = "/api/roadmap";
|
|
950
|
-
var DEFAULT_SEARCH_PARAM_KEY2 =
|
|
951
|
-
var DEFAULT_STATUS_PARAM_KEY2 =
|
|
950
|
+
var DEFAULT_SEARCH_PARAM_KEY2 = _chunk5ZQKEGL3cjs.DEV_SECTION_PARAM_KEYS.search;
|
|
951
|
+
var DEFAULT_STATUS_PARAM_KEY2 = _chunk5ZQKEGL3cjs.DEV_SECTION_PARAM_KEYS.status;
|
|
952
952
|
function RoadmapView({
|
|
953
953
|
endpoint = DEFAULT_ENDPOINT2,
|
|
954
954
|
initialItems,
|
|
@@ -974,7 +974,7 @@ function RoadmapView({
|
|
|
974
974
|
const items = _nullishCoalesce(_optionalChain([data, 'optionalAccess', _23 => _23.items]), () => ( []));
|
|
975
975
|
_chunkSXLEPNY2cjs.useScrollToHash.call(void 0, data, { headerOffset: _chunkSXLEPNY2cjs.STICKY_HEADER_OFFSET_PX });
|
|
976
976
|
if (error) {
|
|
977
|
-
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
977
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk5ZQKEGL3cjs.LoadError, { message: "Failed to load roadmap.", onRetry: reload });
|
|
978
978
|
}
|
|
979
979
|
if (isLoading && !data) {
|
|
980
980
|
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, RoadmapGridSkeleton, { showLeftMargin });
|
|
@@ -1031,7 +1031,7 @@ function DeliveryTable({ items, isLoading = false }) {
|
|
|
1031
1031
|
"div",
|
|
1032
1032
|
{
|
|
1033
1033
|
className: "border-b border-ods-border last:border-b-0",
|
|
1034
|
-
children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
1034
|
+
children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkPA6L55NFcjs.DeliveryRow, { item, id: _chunk5ZQKEGL3cjs.devSectionAnchorId.call(void 0, "delivery", item.id) })
|
|
1035
1035
|
},
|
|
1036
1036
|
item.id
|
|
1037
1037
|
)
|
|
@@ -1043,8 +1043,8 @@ function DeliveryTable({ items, isLoading = false }) {
|
|
|
1043
1043
|
|
|
1044
1044
|
var DEFAULT_COMPLETED_ENDPOINT = "/api/delivery/completed";
|
|
1045
1045
|
var DEFAULT_IN_PROGRESS_ENDPOINT = "/api/delivery/in-progress";
|
|
1046
|
-
var DEFAULT_SEARCH_PARAM_KEY3 =
|
|
1047
|
-
var DEFAULT_TASK_TYPE_PARAM_KEY =
|
|
1046
|
+
var DEFAULT_SEARCH_PARAM_KEY3 = _chunk5ZQKEGL3cjs.DEV_SECTION_PARAM_KEYS.search;
|
|
1047
|
+
var DEFAULT_TASK_TYPE_PARAM_KEY = _chunk5ZQKEGL3cjs.DEV_SECTION_PARAM_KEYS.deliveryTaskType;
|
|
1048
1048
|
function DeliveryLists({
|
|
1049
1049
|
completedApiEndpoint = DEFAULT_COMPLETED_ENDPOINT,
|
|
1050
1050
|
inProgressApiEndpoint = DEFAULT_IN_PROGRESS_ENDPOINT,
|
|
@@ -1105,11 +1105,11 @@ function DeliveryLists({
|
|
|
1105
1105
|
const hasActiveFilters = searchQuery !== "" || taskTypeFilter !== "all";
|
|
1106
1106
|
const hasResults = showCompleted && filteredCompleted.length > 0 || showInProgress && filteredInProgress.length > 0;
|
|
1107
1107
|
if (error) {
|
|
1108
|
-
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "w-full", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
1108
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "w-full", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk5ZQKEGL3cjs.LoadError, { message: error, onRetry: () => window.location.reload() }) });
|
|
1109
1109
|
}
|
|
1110
1110
|
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "w-full flex flex-col gap-[40px]", children: [
|
|
1111
1111
|
!isLoading && !hasResults && (hasActiveFilters ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
1112
|
-
|
|
1112
|
+
_chunk2HL3BXWVcjs.EmptyState,
|
|
1113
1113
|
{
|
|
1114
1114
|
type: "search",
|
|
1115
1115
|
title: "No tasks found",
|
|
@@ -1124,7 +1124,7 @@ function DeliveryLists({
|
|
|
1124
1124
|
}
|
|
1125
1125
|
}
|
|
1126
1126
|
) : /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
1127
|
-
|
|
1127
|
+
_chunk2HL3BXWVcjs.EmptyState,
|
|
1128
1128
|
{
|
|
1129
1129
|
type: "generic",
|
|
1130
1130
|
title: "No tasks available",
|
|
@@ -1226,7 +1226,7 @@ function LegalDocumentPage({
|
|
|
1226
1226
|
initialData = null,
|
|
1227
1227
|
initialLastUpdatedLabel = null,
|
|
1228
1228
|
apiEndpoint,
|
|
1229
|
-
MarkdownRenderer =
|
|
1229
|
+
MarkdownRenderer = _chunk5ZQKEGL3cjs.RichMarkdownRenderer,
|
|
1230
1230
|
backButton,
|
|
1231
1231
|
shell = true
|
|
1232
1232
|
}) {
|
|
@@ -1236,10 +1236,10 @@ function LegalDocumentPage({
|
|
|
1236
1236
|
label: _nullishCoalesce(_optionalChain([backButton, 'optionalAccess', _26 => _26.label]), () => ( "Back to home")),
|
|
1237
1237
|
onClick: () => router.push(_nullishCoalesce(_optionalChain([backButton, 'optionalAccess', _27 => _27.href]), () => ( "/")))
|
|
1238
1238
|
};
|
|
1239
|
-
const fallbackLastUpdatedLabel = _optionalChain([data, 'optionalAccess', _28 => _28.lastSynced]) != null ?
|
|
1239
|
+
const fallbackLastUpdatedLabel = _optionalChain([data, 'optionalAccess', _28 => _28.lastSynced]) != null ? _chunk5ZQKEGL3cjs.formatLegalDate.call(void 0, data.lastSynced) : null;
|
|
1240
1240
|
const effectiveLastUpdatedLabel = _nullishCoalesce(initialLastUpdatedLabel, () => ( fallbackLastUpdatedLabel));
|
|
1241
1241
|
const subtitle = effectiveLastUpdatedLabel ? `Last Updated: ${effectiveLastUpdatedLabel}` : fallbackDescription;
|
|
1242
|
-
const inner = /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
1242
|
+
const inner = /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunk5ZQKEGL3cjs.PageLayout, { title, subtitle, backButton: backCfg, titleSize: "h1", titleWrap: true, children: [
|
|
1243
1243
|
_optionalChain([data, 'optionalAccess', _29 => _29.sourceFile]) && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "p", { className: "text-h6 text-ods-text-secondary opacity-75", children: [
|
|
1244
1244
|
"Source: ",
|
|
1245
1245
|
data.sourceFile
|
|
@@ -1286,7 +1286,7 @@ function LegalDocumentPage({
|
|
|
1286
1286
|
] })
|
|
1287
1287
|
] }) }) }) }) })
|
|
1288
1288
|
] });
|
|
1289
|
-
return shell ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
1289
|
+
return shell ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunk5ZQKEGL3cjs.PageShell, { children: inner }) : /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "page-shell-content", children: inner });
|
|
1290
1290
|
}
|
|
1291
1291
|
|
|
1292
1292
|
|
|
@@ -1303,4 +1303,4 @@ function LegalDocumentPage({
|
|
|
1303
1303
|
|
|
1304
1304
|
|
|
1305
1305
|
exports.ProductReleasesView = ProductReleasesView; exports.MediaGalleryStrip = MediaGalleryStrip; exports.ReleaseDetailPage = ReleaseDetailPage; exports.ReleaseDetailSkeleton = ReleaseDetailSkeleton; exports.useRoadmapVoting = useRoadmapVoting; exports.RoadmapGrid = RoadmapGrid; exports.RoadmapGridSkeleton = RoadmapGridSkeleton; exports.RoadmapView = RoadmapView; exports.DeliveryTable = DeliveryTable; exports.DeliveryLists = DeliveryLists; exports.useLegalDocs = useLegalDocs; exports.LegalDocumentPage = LegalDocumentPage;
|
|
1306
|
-
//# sourceMappingURL=chunk-
|
|
1306
|
+
//# sourceMappingURL=chunk-UDPOAWKH.cjs.map
|