@flamingo-stack/openframe-frontend-core 0.0.278 → 0.0.279

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 (72) hide show
  1. package/dist/{chunk-MVUN35FH.js → chunk-6ZMD3KBP.js} +2 -2
  2. package/dist/{chunk-DO55GWNT.cjs → chunk-776ZURDZ.cjs} +10 -10
  3. package/dist/{chunk-DO55GWNT.cjs.map → chunk-776ZURDZ.cjs.map} +1 -1
  4. package/dist/{chunk-5NL62LIF.js → chunk-7ECI5ZVO.js} +2 -2
  5. package/dist/{chunk-BBDJCXHW.cjs → chunk-7XVVRE53.cjs} +6 -6
  6. package/dist/{chunk-BBDJCXHW.cjs.map → chunk-7XVVRE53.cjs.map} +1 -1
  7. package/dist/{chunk-GO45CXT5.js → chunk-AVLSICZO.js} +49 -33
  8. package/dist/chunk-AVLSICZO.js.map +1 -0
  9. package/dist/{chunk-L4BJU5CE.cjs → chunk-QP4H2JJN.cjs} +34 -34
  10. package/dist/{chunk-L4BJU5CE.cjs.map → chunk-QP4H2JJN.cjs.map} +1 -1
  11. package/dist/{chunk-MDHNLZME.js → chunk-SZG56DA3.js} +2 -2
  12. package/dist/{chunk-WYQ5VFXA.cjs → chunk-XBTQK32O.cjs} +25 -25
  13. package/dist/{chunk-WYQ5VFXA.cjs.map → chunk-XBTQK32O.cjs.map} +1 -1
  14. package/dist/{chunk-737ZLQ2T.js → chunk-YK6FFIVG.js} +2 -2
  15. package/dist/{chunk-7LTWNUHB.cjs → chunk-Z53YSXDD.cjs} +149 -133
  16. package/dist/chunk-Z53YSXDD.cjs.map +1 -0
  17. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  18. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -1
  19. package/dist/components/chat/index.cjs +2 -2
  20. package/dist/components/chat/index.js +1 -1
  21. package/dist/components/chat/types/api.types.d.ts +7 -2
  22. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  23. package/dist/components/chat/types/message.types.d.ts +10 -0
  24. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  25. package/dist/components/chat/types/network.types.d.ts +2 -0
  26. package/dist/components/chat/types/network.types.d.ts.map +1 -1
  27. package/dist/components/chat/types/processing.types.d.ts +1 -0
  28. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  29. package/dist/components/chat/utils/chunk-parser.d.ts.map +1 -1
  30. package/dist/components/chat/utils/history-merge.d.ts +8 -0
  31. package/dist/components/chat/utils/history-merge.d.ts.map +1 -1
  32. package/dist/components/chat/utils/message-segment-accumulator.d.ts +5 -3
  33. package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
  34. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  35. package/dist/components/contact/index.cjs +3 -3
  36. package/dist/components/contact/index.js +2 -2
  37. package/dist/components/faq/index.cjs +3 -3
  38. package/dist/components/faq/index.js +2 -2
  39. package/dist/components/features/index.cjs +2 -2
  40. package/dist/components/features/index.js +1 -1
  41. package/dist/components/index.cjs +53 -53
  42. package/dist/components/index.js +4 -4
  43. package/dist/components/navigation/index.cjs +2 -2
  44. package/dist/components/navigation/index.js +1 -1
  45. package/dist/components/related-content/index.cjs +3 -3
  46. package/dist/components/related-content/index.js +2 -2
  47. package/dist/components/tickets/index.cjs +52 -52
  48. package/dist/components/tickets/index.js +3 -3
  49. package/dist/components/ui/index.cjs +2 -2
  50. package/dist/components/ui/index.js +1 -1
  51. package/dist/index.cjs +2 -2
  52. package/dist/index.js +1 -1
  53. package/dist/utils/index.cjs.map +1 -1
  54. package/dist/utils/index.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/components/chat/chat-message-enhanced.tsx +1 -0
  57. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +34 -21
  58. package/src/components/chat/types/api.types.ts +7 -2
  59. package/src/components/chat/types/message.types.ts +10 -0
  60. package/src/components/chat/types/network.types.ts +2 -0
  61. package/src/components/chat/types/processing.types.ts +1 -1
  62. package/src/components/chat/utils/__tests__/history-merge.test.ts +70 -0
  63. package/src/components/chat/utils/chunk-parser.ts +12 -1
  64. package/src/components/chat/utils/history-merge.ts +22 -2
  65. package/src/components/chat/utils/message-segment-accumulator.ts +10 -4
  66. package/src/components/chat/utils/process-historical-messages.ts +4 -1
  67. package/dist/chunk-7LTWNUHB.cjs.map +0 -1
  68. package/dist/chunk-GO45CXT5.js.map +0 -1
  69. /package/dist/{chunk-MVUN35FH.js.map → chunk-6ZMD3KBP.js.map} +0 -0
  70. /package/dist/{chunk-5NL62LIF.js.map → chunk-7ECI5ZVO.js.map} +0 -0
  71. /package/dist/{chunk-MDHNLZME.js.map → chunk-SZG56DA3.js.map} +0 -0
  72. /package/dist/{chunk-737ZLQ2T.js.map → chunk-YK6FFIVG.js.map} +0 -0
@@ -30,7 +30,7 @@ import {
30
30
  getContentRefLabelOrTitleCase,
31
31
  init_pagination,
32
32
  orderContentRefTypes
33
- } from "./chunk-GO45CXT5.js";
33
+ } from "./chunk-AVLSICZO.js";
34
34
  import {
35
35
  OnboardingGuideCard,
36
36
  OnboardingGuideCardSkeleton,
@@ -361,4 +361,4 @@ export {
361
361
  GROUP_PAGE_SIZE,
362
362
  RelatedContentSection
363
363
  };
364
- //# sourceMappingURL=chunk-MVUN35FH.js.map
364
+ //# sourceMappingURL=chunk-6ZMD3KBP.js.map
@@ -7,7 +7,7 @@
7
7
 
8
8
 
9
9
 
10
- var _chunk7LTWNUHBcjs = require('./chunk-7LTWNUHB.cjs');
10
+ var _chunkZ53YSXDDcjs = require('./chunk-Z53YSXDD.cjs');
11
11
 
12
12
 
13
13
 
@@ -71,7 +71,7 @@ function UnifiedPagination({
71
71
  };
72
72
  if (totalPages <= 1) return null;
73
73
  return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
74
- _chunk7LTWNUHBcjs.Pagination,
74
+ _chunkZ53YSXDDcjs.Pagination,
75
75
  {
76
76
  currentPage,
77
77
  totalPages,
@@ -84,7 +84,7 @@ var init_unified_pagination = _chunkVRHGVLSLcjs.__esm.call(void 0, {
84
84
  "use strict";
85
85
  "use client";
86
86
  _chunkG7UE6RKVcjs.init_next_navigation.call(void 0, );
87
- _chunk7LTWNUHBcjs.init_pagination.call(void 0, );
87
+ _chunkZ53YSXDDcjs.init_pagination.call(void 0, );
88
88
  }
89
89
  });
90
90
 
@@ -238,7 +238,7 @@ function EmptyState({
238
238
  var _react = require('react');
239
239
 
240
240
  function DevSectionView({ sectionKey, hero, preControls, children }) {
241
- const section = _chunk7LTWNUHBcjs.OPENFRAME_DEV_SECTIONS[sectionKey];
241
+ const section = _chunkZ53YSXDDcjs.OPENFRAME_DEV_SECTIONS[sectionKey];
242
242
  const router = _chunkG7UE6RKVcjs.useRouter.call(void 0, );
243
243
  const pathname = _chunkG7UE6RKVcjs.usePathname.call(void 0, );
244
244
  const searchParams = _chunkG7UE6RKVcjs.useSearchParams.call(void 0, );
@@ -288,7 +288,7 @@ function DevSectionView({ sectionKey, hero, preControls, children }) {
288
288
  }
289
289
  ),
290
290
  filter && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
291
- _chunk7LTWNUHBcjs.StatusFilterComponent,
291
+ _chunkZ53YSXDDcjs.StatusFilterComponent,
292
292
  {
293
293
  selectedStatus: currentFilterValue,
294
294
  onStatusChange: handleFilterChange,
@@ -313,7 +313,7 @@ function DevSectionPage({
313
313
  subtitle
314
314
  }) {
315
315
  const router = _chunkG7UE6RKVcjs.useRouter.call(void 0, );
316
- const section = _chunk7LTWNUHBcjs.OPENFRAME_DEV_SECTIONS[sectionKey];
316
+ const section = _chunkZ53YSXDDcjs.OPENFRAME_DEV_SECTIONS[sectionKey];
317
317
  const Icon = section.icon;
318
318
  const backCfg = backButton === false ? void 0 : {
319
319
  label: _nullishCoalesce((backButton ? backButton.label : void 0), () => ( "Back to home")),
@@ -395,9 +395,9 @@ function DeliveryRow({
395
395
  className
396
396
  }) {
397
397
  const taskType = item.taskType;
398
- const typeBadgeLabel = _chunk7LTWNUHBcjs.TASK_TYPE_LABELS[taskType] || "TASK";
399
- const typeBadgeTextColor = _chunk7LTWNUHBcjs.TASK_TYPE_TEXT_COLORS[taskType] || "";
400
- const statusBadgeScheme = _chunk7LTWNUHBcjs.getStatusColorScheme.call(void 0, item.status);
398
+ const typeBadgeLabel = _chunkZ53YSXDDcjs.TASK_TYPE_LABELS[taskType] || "TASK";
399
+ const typeBadgeTextColor = _chunkZ53YSXDDcjs.TASK_TYPE_TEXT_COLORS[taskType] || "";
400
+ const statusBadgeScheme = _chunkZ53YSXDDcjs.getStatusColorScheme.call(void 0, item.status);
401
401
  const relativeTime = getRelativeTime(item.dateUpdated);
402
402
  const subtitle = `ACTIVE ${relativeTime}${item.listNames.length > 0 ? `, ${item.listNames.join(", ")}` : ""}, ${item.id}`;
403
403
  const inner = /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex flex-col md:flex-row items-start justify-between gap-[12px] md:gap-[16px] w-full", children: [
@@ -450,4 +450,4 @@ function DeliveryRow({
450
450
 
451
451
 
452
452
  exports.EmptyState = EmptyState; exports.UnifiedPagination = UnifiedPagination; exports.unified_pagination_exports = unified_pagination_exports; exports.init_unified_pagination = init_unified_pagination; exports.DevSectionView = DevSectionView; exports.DevSectionPage = DevSectionPage; exports.DevCardRowContent = DevCardRowContent; exports.DevCardRowSkeleton = DevCardRowSkeleton; exports.DevCardRowSkeletonList = DevCardRowSkeletonList; exports.DeliveryRow = DeliveryRow;
453
- //# sourceMappingURL=chunk-DO55GWNT.cjs.map
453
+ //# sourceMappingURL=chunk-776ZURDZ.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-DO55GWNT.cjs","../src/components/unified-pagination.tsx","../src/components/empty-state.tsx","../src/components/shared/dev-section/dev-section-view.tsx","../src/components/shared/dev-section/dev-section-page.tsx","../src/components/shared/dev-section/dev-card-row.tsx","../src/components/shared/delivery/delivery-row.tsx"],"names":["jsx","jsxs"],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-DO55GWNT.cjs","sourcesContent":[null,"\"use client\"\n\nimport { useRouter, useSearchParams, usePathname } from \"../embed-shims/next-navigation\"\nimport { Pagination } from \"./pagination\"\n\ninterface UnifiedPaginationProps {\n currentPage: number\n totalPages: number\n onPageChange?: (page: number) => void\n className?: string\n}\n\nexport function UnifiedPagination({ \n currentPage, \n totalPages, \n onPageChange,\n className = \"mt-8 flex justify-center w-full\"\n}: UnifiedPaginationProps) {\n const router = useRouter()\n const searchParams = useSearchParams()\n const pathname = usePathname()\n\n const handlePageChange = (page: number) => {\n // Preserve current scroll position\n const currentScrollY = window.scrollY\n \n // Call the callback to update local state (prevents reload)\n if (onPageChange) {\n onPageChange(page)\n }\n \n // Update URL for bookmarking without navigation\n const params = new URLSearchParams(searchParams.toString())\n params.set(\"page\", page.toString())\n \n // Update URL without navigation (for bookmarking support)\n const newUrl = `${pathname}?${params.toString()}`\n window.history.replaceState(null, '', newUrl)\n \n // Restore scroll position after a brief delay to allow content to render\n setTimeout(() => {\n window.scrollTo({\n top: currentScrollY,\n behavior: 'instant' // Instant to prevent any scroll animation\n })\n }, 0)\n }\n\n // Don't render pagination if there's only one page\n if (totalPages <= 1) return null\n\n return (\n <div className={className}>\n <Pagination\n currentPage={currentPage}\n totalPages={totalPages}\n onPageChange={handlePageChange}\n />\n </div>\n )\n} ","\"use client\";\n\nimport { Search, FileText, Package } from \"lucide-react\"\nimport { Button } from \"./ui/button\"\nimport { useRouter } from \"../embed-shims/next-navigation\"\n\nexport interface EmptyStateProps {\n type: 'vendors' | 'posts' | 'search' | 'generic'\n title?: string\n description?: string\n showBackButton?: boolean\n onGoBack?: () => void\n backButtonText?: string\n // New CTA properties\n showCTA?: boolean\n ctaText?: string\n onCtaClick?: () => void\n ctaVariant?: 'primary' | 'secondary'\n}\n\nexport function EmptyState({\n type,\n title,\n description,\n showBackButton = false,\n onGoBack,\n backButtonText = \"Go Back\",\n showCTA = true,\n ctaText,\n onCtaClick,\n ctaVariant = 'primary'\n}: EmptyStateProps) {\n const router = useRouter()\n\n // Default content based on type\n const getDefaultContent = () => {\n switch (type) {\n case 'vendors':\n return {\n icon: <Package className=\"w-full h-full\" />,\n title: \"No vendors found\",\n description: \"We couldn't find any vendors matching your criteria. Try adjusting your filters or search terms.\"\n }\n case 'posts':\n return {\n icon: <FileText className=\"w-full h-full\" />,\n title: \"No articles found\",\n description: \"We couldn't find any articles matching your criteria. Try different categories, tags, or search terms.\"\n }\n case 'search':\n return {\n icon: <Search className=\"w-full h-full\" />,\n title: \"No results found\",\n description: \"Your search didn't return any results. Try different keywords or browse our categories.\"\n }\n default:\n return {\n icon: <Search className=\"w-full h-full\" />,\n title: \"Nothing found\",\n description: \"We couldn't find what you're looking for. Try adjusting your search or filters.\"\n }\n }\n }\n\n // Smart CTA logic based on context\n const getSmartCTA = () => {\n // If custom CTA is provided, use it\n if (ctaText && onCtaClick) {\n return {\n text: ctaText,\n action: onCtaClick\n }\n }\n\n // Check if we're on the client side\n const isClient = typeof window !== 'undefined'\n const currentPath = isClient ? window.location.pathname : ''\n\n // Smart defaults based on type and context\n switch (type) {\n case 'search':\n return {\n text: \"Reset Filters\",\n action: () => {\n if (isClient) {\n // Try to reset search by clearing URL params and refreshing\n const url = new URL(window.location.href)\n url.search = ''\n router.push(url.pathname)\n }\n }\n }\n case 'posts':\n // If we're on blog/community pages, reset blog filters\n if (currentPath.includes('/blog')) {\n return {\n text: \"Reset Filters\",\n action: () => {\n if (isClient) {\n // Reset blog search and filters by clearing URL params\n const url = new URL(window.location.href)\n url.search = ''\n router.push(url.pathname)\n }\n }\n }\n } else if (currentPath.includes('/profile')) {\n return {\n text: \"Browse Vendors\",\n action: () => router.push('/vendors')\n }\n }\n return {\n text: \"View All Posts\",\n action: () => router.push('/blog')\n }\n case 'vendors':\n // If we're in profile or other pages, direct to main content\n if (currentPath.includes('/profile')) {\n return {\n text: \"Browse Vendors\",\n action: () => router.push('/vendors')\n }\n } else if (currentPath.includes('/vendors') || currentPath.includes('/margin-increase/compare')) {\n return {\n text: \"Reset Filters\",\n action: () => {\n if (isClient) {\n // Reset vendor search and filters by clearing URL params\n const url = new URL(window.location.href)\n url.search = ''\n router.push(url.pathname)\n }\n }\n }\n }\n return {\n text: \"Browse Vendors\",\n action: () => router.push('/vendors')\n }\n default:\n return {\n text: \"Browse Vendors\",\n action: () => router.push('/vendors')\n }\n }\n }\n\n const defaultContent = getDefaultContent()\n const displayTitle = title || defaultContent.title\n const displayDescription = description || defaultContent.description\n const smartCTA = getSmartCTA()\n\n return (\n <div className=\"flex flex-col items-center justify-center py-6 md:py-16 px-6 text-center\">\n {/* Icon */}\n <div className=\"mb-3 md:mb-6 flex items-center justify-center\">\n <div className=\"rounded-full bg-ods-card p-3 md:p-6 border border-ods-border\">\n <div className=\"w-8 h-8 md:w-16 md:h-16 text-ods-text-secondary flex items-center justify-center\">\n {defaultContent.icon}\n </div>\n </div>\n </div>\n\n {/* Title */}\n <h2 className=\"mb-2 md:mb-3 text-lg md:text-xl font-semibold font-['DM_Sans'] text-ods-text-primary tracking-[-0.02em]\">\n {displayTitle}\n </h2>\n\n {/* Description */}\n <p className=\"mb-4 md:mb-8 max-w-md text-sm font-medium font-['DM_Sans'] text-ods-text-secondary leading-[1.43em]\">\n {displayDescription}\n </p>\n\n {/* Smart CTA Button */}\n {showCTA && smartCTA && (\n <div className=\"w-full max-w-xs mb-3\">\n <Button\n onClick={smartCTA.action}\n className={ctaVariant === 'primary'\n ? \"w-full bg-ods-accent text-ods-text-on-accent hover:bg-ods-accent-hover transition-all duration-150 font-['DM_Sans'] font-medium\"\n : \"w-full bg-transparent border border-ods-border text-ods-text-primary hover:border-ods-accent hover:text-ods-accent transition-all duration-150 font-['DM_Sans'] font-medium\"\n }\n >\n {smartCTA.text}\n </Button>\n </div>\n )}\n\n {/* Optional Back Button */}\n {showBackButton && onGoBack && (\n <div className=\"w-full max-w-xs\">\n <Button\n onClick={onGoBack}\n variant=\"outline\"\n className=\"w-full transition-all duration-150 font-['DM_Sans'] font-medium\"\n >\n {backButtonText}\n </Button>\n </div>\n )}\n </div>\n )\n} ","'use client';\n\n/**\n * DevSectionView — the canonical chrome for ANY dev-center section\n * (Roadmap / Delivery / Releases). One component, used in BOTH:\n *\n * - tabbed `/roadmap-and-releases` (compact title mode, no `hero`)\n * - full-page `/roadmap`, `/bug-fixes-and-enhancements`, `/releases`\n * (hero mode with icon + description + back link)\n *\n * Owns: title rendering, the inline search input, the filter pill row,\n * and the URL-param wiring that connects both. The list `children`\n * receive a clean URL contract — they read `?<paramKey>=...` via\n * `useSearchParams()` and refetch on change. No duplicated controls.\n */\n\nimport type { ReactNode } from 'react';\nimport { useState, useEffect } from 'react';\nimport { useRouter, useSearchParams, usePathname } from '../../../embed-shims';\nimport { SearchInput } from '../../ui';\nimport { StatusFilterComponent } from '../../features';\nimport {\n OPENFRAME_DEV_SECTIONS,\n type OpenframeDevSectionKey,\n} from '../../../utils/dev-sections/openframe-dev-sections';\n\nexport interface DevSectionViewProps {\n /** Which section to render — drives title, search, and filter\n * config via the `OPENFRAME_DEV_SECTIONS` registry. */\n sectionKey: OpenframeDevSectionKey;\n /** When set, renders the rich page-level hero (icon + h1 + description).\n * Omit for the compact tab-context heading. */\n hero?: {\n /** Pre-rendered icon JSX. Server components render the icon themselves\n * and pass the element here — function references can't cross the\n * server→client boundary, but React elements can. */\n icon: ReactNode;\n /** Hero title. Falls back to `OPENFRAME_DEV_SECTIONS[sectionKey].hero.title`\n * when omitted, so embedders can override the (OpenFrame-specific) default\n * copy without forking the registry. */\n title?: string;\n description: string;\n };\n /** Optional slot rendered BETWEEN the hero and the search/filter\n * controls. Use this for an entry-action surface that should sit\n * above the list (e.g. the Help Center's \"Open a new ticket\" form).\n * The slot is wrapped in the same `gap-10` flex column so spacing\n * matches the surrounding chrome — callers should NOT add their\n * own top/bottom margin. Renders `null` (no DOM) when omitted. */\n preControls?: ReactNode;\n /** The page-specific list body. Reads URL params written by this\n * component (search input + filter pills). */\n children: ReactNode;\n}\n\nexport function DevSectionView({ sectionKey, hero, preControls, children }: DevSectionViewProps) {\n const section = OPENFRAME_DEV_SECTIONS[sectionKey];\n const router = useRouter();\n const pathname = usePathname();\n const searchParams = useSearchParams();\n\n const search = section.search;\n const filter = section.filter;\n\n const currentSearch = search ? searchParams.get(search.paramKey) || '' : '';\n const currentFilterValue = filter\n ? searchParams.get(filter.paramKey) || filter.defaultValue\n : '';\n\n // Controlled search-input state — input commits to the URL only on\n // Enter (not on every keystroke), preserving the legacy behavior.\n // Lazy init from URL avoids a brief flash of stale value on first\n // paint after URL-driven re-render (e.g. tab switch).\n const [searchValue, setSearchValue] = useState(() => currentSearch);\n useEffect(() => {\n setSearchValue(currentSearch);\n }, [currentSearch]);\n\n const handleSearchSubmit = (value: string) => {\n if (!search) return;\n const params = new URLSearchParams(searchParams.toString());\n if (value.trim()) params.set(search.paramKey, value.trim());\n else params.delete(search.paramKey);\n router.replace(`${pathname}?${params.toString()}`, { scroll: false });\n };\n\n const handleFilterChange = (value: string) => {\n if (!filter) return;\n const params = new URLSearchParams(searchParams.toString());\n if (value === filter.defaultValue) params.delete(filter.paramKey);\n else params.set(filter.paramKey, value);\n router.replace(`${pathname}?${params.toString()}`, { scroll: false });\n };\n\n return (\n <div className=\"w-full flex flex-col gap-10\">\n {hero ? (\n <div className=\"space-y-4\">\n <h1 className=\"text-h1 tracking-[-1.12px] text-ods-text-primary flex items-center gap-3\">\n {hero.icon}\n {hero.title ?? section.hero.title}\n </h1>\n <p className=\"font-['DM_Sans'] font-medium text-[18px] leading-[28px] text-ods-text-secondary max-w-3xl\">\n {hero.description}\n </p>\n </div>\n ) : (\n <div className=\"flex items-center justify-between w-full\">\n <h2 className=\"font-['Azeret_Mono'] font-semibold text-[32px] md:text-[40px] lg:text-[48px] leading-[40px] md:leading-[48px] lg:leading-[56px] text-ods-text-primary tracking-[-0.64px] md:tracking-[-0.8px] lg:tracking-[-0.96px]\">\n {section.hero.title}\n <span className=\"text-ods-accent\">:</span>\n </h2>\n </div>\n )}\n\n {preControls}\n\n {(search || filter) && (\n <div className=\"space-y-4\">\n {search && (\n <SearchInput\n showDropdown={false}\n placeholder={search.placeholder}\n value={searchValue}\n onChange={setSearchValue}\n onSubmit={handleSearchSubmit}\n />\n )}\n {filter && (\n <StatusFilterComponent\n selectedStatus={currentFilterValue}\n onStatusChange={handleFilterChange}\n statusOptions={[...filter.options]}\n />\n )}\n </div>\n )}\n\n {children}\n </div>\n );\n}\n","'use client';\n\n/**\n * DevSectionPage — full-page wrapper for a dev-center section\n * (`/roadmap`, `/bug-fixes-and-enhancements`, `/releases`).\n *\n * Mounts the lib's canonical `PageLayout` directly (no in-app wrapper)\n * so the back-button affordance stays in lockstep with whatever the\n * design system ships — any future lib change to BackButton / TitleBlock\n * propagates automatically.\n *\n * Composition: `PageShell` → `PageLayout` (back-to-home wired) →\n * `DevSectionView` (icon hero + search + filter pills) → list body.\n *\n * Adding a new section is one entry in `OPENFRAME_DEV_SECTIONS` plus a\n * single-line page file mounting this factory with the new key.\n */\n\nimport type { ReactNode } from 'react';\nimport { useRouter } from '../../../embed-shims/next-navigation';\nimport { PageShell, PageLayout } from '../../ui';\nimport { DevSectionView } from './dev-section-view';\nimport {\n OPENFRAME_DEV_SECTIONS,\n type OpenframeDevSectionKey,\n} from '../../../utils/dev-sections/openframe-dev-sections';\n\nconst SECTION_HERO_ICON_CLASS = 'h-10 w-10 text-ods-accent';\n\nexport interface DevSectionPageProps {\n sectionKey: OpenframeDevSectionKey;\n /** The page-specific list body (e.g. `<RoadmapList />`). */\n children: ReactNode;\n /** Optional slot rendered BETWEEN the hero and search/filter — see\n * `DevSectionView.preControls`. Used by surfaces that want an entry\n * action (e.g. Help Center's \"Open a new ticket\" form) above the\n * controls instead of below them. */\n preControls?: ReactNode;\n /** Back-button config — same shape as `LegalDocumentPage` /\n * `ReleaseDetailPage`. Pass `false` to hide entirely. Default\n * `{ label: 'Back to home', href: '/' }` — embedders whose \"home\" isn't `/`\n * should override `href`, or pass `false` if the embed has no home page. */\n backButton?: { label?: string; href?: string } | false;\n /** Override the hero title. Defaults to the (OpenFrame-specific) copy in\n * `OPENFRAME_DEV_SECTIONS[sectionKey].hero.title`. Set this to brand the\n * section for a non-OpenFrame embed. */\n title?: string;\n /** Override the hero subtitle/description. Defaults to\n * `OPENFRAME_DEV_SECTIONS[sectionKey].hero.description`. */\n subtitle?: string;\n}\n\nexport function DevSectionPage({\n sectionKey,\n children,\n preControls,\n backButton,\n title,\n subtitle,\n}: DevSectionPageProps) {\n const router = useRouter();\n const section = OPENFRAME_DEV_SECTIONS[sectionKey];\n const Icon = section.icon;\n\n // Back-button config — mirrors LegalDocumentPage / ReleaseDetailPage.\n // Default: { label: 'Back to home', href: '/' }. Pass `false` to hide.\n // After `backButton &&` narrowing, inner type is `{ label?, href? } |\n // undefined`; don't re-compare to `false` (TS2367).\n const backCfg =\n backButton === false\n ? undefined\n : {\n label: (backButton ? backButton.label : undefined) ?? 'Back to home',\n onClick: () => router.push((backButton ? backButton.href : undefined) ?? '/'),\n };\n\n return (\n <PageShell>\n <PageLayout backButton={backCfg}>\n <DevSectionView\n sectionKey={sectionKey}\n hero={{\n icon: <Icon className={SECTION_HERO_ICON_CLASS} />,\n title,\n description: subtitle ?? section.hero.description,\n }}\n preControls={preControls}\n >\n {children}\n </DevSectionView>\n </PageLayout>\n </PageShell>\n );\n}\n","'use client';\n\n/**\n * Shared row chrome for any `DevSectionPage` list (delivery, tickets,\n * future sections). One source of truth for the layout that every\n * dev-section card row uses:\n * left column → title (h3) / subtitle (h5 uppercase) / description\n * (h4 line-clamp-3), each in a fixed min-height block\n * so rows align across the grid\n * right column → caller-supplied stacked badges\n *\n * Surface stays small on purpose — `rightBadges` is a `ReactNode` so\n * the caller decides how many badges (delivery: 2, tickets: 1-2,\n * future: anything). No behavior baked in: the caller wraps the row\n * in a `<div>` (static, like delivery) or `<button>` (clickable, like\n * tickets) and renders the row content via this component.\n *\n * Pair with `DevCardRowSkeletonList` for the loading state — the\n * skeleton mirrors the same min-heights so the in-flight UI doesn't\n * shift the layout when real data lands.\n *\n * NOTE: the ticket conversation row is NOT here — it renders the shared\n * `<ChatMessageRow>` (`components/chat/chat-message-row.tsx`), the SAME\n * component the OpenMSP Slack-community feed uses, so the two surfaces stay\n * pixel-identical by construction.\n */\n\nimport type { ReactNode } from 'react';\n\nexport interface DevCardRowContentProps {\n title: string;\n /** Single-line uppercase metadata (e.g. \"UPDATED today, #4271, Code review\"). */\n subtitle: string;\n /** 3-line description block. Empty string renders the fallback. */\n description: string;\n /** Fallback copy when `description` is empty. Defaults to a generic\n * string; ticket / delivery surfaces override. */\n emptyDescription?: string;\n /** Right column — caller renders its own stacked badges. */\n rightBadges: ReactNode;\n}\n\nexport function DevCardRowContent({\n title,\n subtitle,\n description,\n emptyDescription = 'No description provided',\n rightBadges,\n}: DevCardRowContentProps) {\n return (\n <div className=\"flex flex-col md:flex-row items-start justify-between gap-[12px] md:gap-[16px] w-full\">\n <div className=\"flex-1 min-w-0 w-full md:w-auto flex flex-col gap-[12px] md:gap-[16px]\">\n <div className=\"min-h-[24px] flex items-center\">\n <h3 className=\"text-h3 text-ods-text-primary tracking-[-0.36px] flex-1 line-clamp-2 md:truncate break-words\">\n {title}\n </h3>\n </div>\n <div className=\"min-h-[20px] flex items-center\">\n <p className=\"text-h5 text-ods-text-secondary uppercase tracking-[-0.28px] truncate\">\n {subtitle}\n </p>\n </div>\n <div className=\"min-h-[72px] flex items-center\">\n <p className=\"text-h4 text-ods-text-secondary line-clamp-3 break-words\">\n {description || emptyDescription}\n </p>\n </div>\n </div>\n <div className=\"flex-shrink-0 self-start flex flex-col gap-2\">\n {rightBadges}\n </div>\n </div>\n );\n}\n\n/**\n * Skeleton rendering for a single row — the bars mirror the same\n * min-heights as `DevCardRowContent` so the loading→loaded swap\n * doesn't reflow.\n */\nexport function DevCardRowSkeleton() {\n return (\n <div className=\"border-b border-ods-border last:border-b-0 p-[12px] md:p-[16px]\">\n <div className=\"flex flex-col md:flex-row items-start justify-between gap-[12px] md:gap-[16px] w-full\">\n <div className=\"flex-1 min-w-0 w-full md:w-auto flex flex-col gap-[12px] md:gap-[16px]\">\n <div className=\"min-h-[24px] flex items-center\">\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-full\" />\n </div>\n <div className=\"min-h-[20px] flex items-center\">\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-1/2\" />\n </div>\n <div className=\"min-h-[72px] flex items-center\">\n <div className=\"flex-1 space-y-1\">\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-full\" />\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-full\" />\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-2/3\" />\n </div>\n </div>\n </div>\n <div className=\"flex-shrink-0 self-start flex flex-col gap-2\">\n <div className=\"h-[32px] w-[100px] bg-ods-border rounded animate-pulse\" />\n <div className=\"h-[32px] w-[120px] bg-ods-border rounded animate-pulse\" />\n </div>\n </div>\n </div>\n );\n}\n\n/**\n * The standard \"5 skeleton rows inside a bordered card\" loading state\n * used by every list shell. Both delivery (`delivery-table.tsx`) and\n * tickets (`tickets-list.tsx`) mount this directly.\n */\nexport function DevCardRowSkeletonList({ rows = 5 }: { rows?: number }) {\n return (\n <div className=\"bg-ods-card border border-ods-border rounded-[6px] overflow-hidden w-full\">\n {Array.from({ length: rows }, (_, i) => (\n <DevCardRowSkeleton key={i} />\n ))}\n </div>\n );\n}\n","'use client'\n\n/**\n * `<DeliveryRow />` — canonical single-row presentation for a ClickUp\n * delivery item.\n *\n * Single source of truth: both the `/bug-fixes-and-enhancements` page\n * (via `DeliveryTable`) AND the linked-delivery card on a HubSpot ticket\n * (via `TicketLinkedDeliveryCard`) compose this primitive. Visual parity\n * across those two surfaces is the design goal — the user reads the\n * card on their ticket and recognises it as a row from the public\n * delivery list.\n *\n * Behaviors:\n * - `href` set → outer element is an `<a>`, the whole row becomes\n * clickable (used by the linked-card surface to deep-link into\n * `/bug-fixes-and-enhancements?focus=<id>`).\n * - `id` set → outer element gets that DOM id so the consuming page\n * can `scrollIntoView` to it when the URL carries `?focus=<id>`.\n * - `highlighted` true → brief accent border + background pulse\n * (`animate-flash-focus` keyframe defined in `tailwind.config.ts`).\n * - `caption` set → small uppercase label rendered above the title\n * (\"LINKED DELIVERY\" on the ticket-side variant). Omitted on the\n * standard list rendering.\n */\n\nimport * as React from 'react'\nimport Link from '../../../embed-shims/next-link'\nimport { StatusBadge } from '../../ui/status-badge'\nimport { getStatusColorScheme } from '../../../utils'\nimport {\n type DeliveryItem,\n TASK_TYPE_LABELS,\n TASK_TYPE_TEXT_COLORS,\n} from '../../../types/delivery'\nimport { cn } from '../../../utils/cn'\n\n/** Same heuristic as DeliveryTable's local helper. Inlined so the row\n * primitive owns its complete rendering contract. */\nfunction getRelativeTime(timestamp: number): string {\n const now = Date.now()\n const diff = now - timestamp\n const days = Math.floor(diff / (1000 * 60 * 60 * 24))\n const weeks = Math.floor(days / 7)\n const months = Math.floor(days / 30)\n if (months > 0) return months === 1 ? 'last month' : `${months} months ago`\n if (weeks > 0) return weeks === 1 ? 'last week' : `${weeks} weeks ago`\n if (days > 0) return days === 1 ? 'yesterday' : `${days} days ago`\n return 'today'\n}\n\nexport interface DeliveryRowProps {\n item: DeliveryItem\n /** When set, the row becomes a clickable anchor. The ticket-side\n * linked-card composes this from `buildDevSectionUrl('delivery', id)`\n * which carries `?search=<id>` — the delivery list filters to that\n * exact task on landing (canonical deep-link mechanism, same one\n * the chat-inline delivery card uses). */\n href?: string\n /** Small uppercase caption rendered above the title. Used by the\n * linked-delivery card variant (\"LINKED DELIVERY\"). */\n caption?: string\n className?: string\n}\n\nexport function DeliveryRow({\n item,\n href,\n caption,\n className,\n}: DeliveryRowProps) {\n const taskType = item.taskType as keyof typeof TASK_TYPE_LABELS\n const typeBadgeLabel = TASK_TYPE_LABELS[taskType] || 'TASK'\n const typeBadgeTextColor = TASK_TYPE_TEXT_COLORS[taskType] || ''\n const statusBadgeScheme = getStatusColorScheme(item.status)\n const relativeTime = getRelativeTime(item.dateUpdated)\n const subtitle = `ACTIVE ${relativeTime}${item.listNames.length > 0 ? `, ${item.listNames.join(', ')}` : ''}, ${item.id}`\n\n const inner = (\n <div className=\"flex flex-col md:flex-row items-start justify-between gap-[12px] md:gap-[16px] w-full\">\n {/* Left: caption (optional) + title + subtitle + description */}\n <div className=\"flex-1 min-w-0 w-full md:w-auto flex flex-col gap-[12px] md:gap-[16px]\">\n {caption && (\n <p className=\"text-xs font-medium uppercase tracking-wider text-ods-text-secondary\">\n {caption}\n </p>\n )}\n <div className=\"min-h-[24px] md:min-h-[24px] flex items-center\">\n <h3 className=\"text-h3 text-ods-text-primary tracking-[-0.36px] flex-1 line-clamp-2 md:truncate break-words\">\n {item.title}\n </h3>\n </div>\n <div className=\"min-h-[20px] flex items-center\">\n <p className=\"text-h5 text-ods-text-secondary uppercase tracking-[-0.28px] truncate\">\n {subtitle}\n </p>\n </div>\n <div className=\"min-h-[72px] flex items-center\">\n <p className=\"text-h4 text-ods-text-secondary line-clamp-3 break-words\">\n {item.description || 'No description provided'}\n </p>\n </div>\n </div>\n\n {/* Right: status + task-type badges */}\n <div className=\"flex-shrink-0 self-start flex flex-col gap-2\">\n <StatusBadge\n text={item.status.toUpperCase()}\n colorScheme={statusBadgeScheme}\n variant=\"card\"\n className=\"border border-ods-border\"\n />\n <StatusBadge\n text={typeBadgeLabel}\n variant=\"card\"\n className={`border border-ods-border ${typeBadgeTextColor}`}\n />\n </div>\n </div>\n )\n\n const baseClass = cn(\n 'block p-[12px] md:p-[16px] no-underline text-inherit transition-colors duration-150',\n href && 'hover:bg-ods-bg-hover cursor-pointer',\n className,\n )\n\n if (href) {\n // `Link` is the env-aware embed-shim — delegates to `next/link` on\n // a Next.js host (soft RSC nav, back-button restores the previous\n // page's React state intact), falls back to a plain `<a>` on\n // non-Next embedders. A raw `<a href>` was hard-navigating +\n // losing TanStack-Query state on back, leaving /tickets stuck on\n // its skeleton.\n return (\n <Link href={href} className={baseClass} prefetch={false}>\n {inner}\n </Link>\n )\n }\n\n return <div className={baseClass}>{inner}</div>\n}\n"]}
1
+ {"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-776ZURDZ.cjs","../src/components/unified-pagination.tsx","../src/components/empty-state.tsx","../src/components/shared/dev-section/dev-section-view.tsx","../src/components/shared/dev-section/dev-section-page.tsx","../src/components/shared/dev-section/dev-card-row.tsx","../src/components/shared/delivery/delivery-row.tsx"],"names":["jsx","jsxs"],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-776ZURDZ.cjs","sourcesContent":[null,"\"use client\"\n\nimport { useRouter, useSearchParams, usePathname } from \"../embed-shims/next-navigation\"\nimport { Pagination } from \"./pagination\"\n\ninterface UnifiedPaginationProps {\n currentPage: number\n totalPages: number\n onPageChange?: (page: number) => void\n className?: string\n}\n\nexport function UnifiedPagination({ \n currentPage, \n totalPages, \n onPageChange,\n className = \"mt-8 flex justify-center w-full\"\n}: UnifiedPaginationProps) {\n const router = useRouter()\n const searchParams = useSearchParams()\n const pathname = usePathname()\n\n const handlePageChange = (page: number) => {\n // Preserve current scroll position\n const currentScrollY = window.scrollY\n \n // Call the callback to update local state (prevents reload)\n if (onPageChange) {\n onPageChange(page)\n }\n \n // Update URL for bookmarking without navigation\n const params = new URLSearchParams(searchParams.toString())\n params.set(\"page\", page.toString())\n \n // Update URL without navigation (for bookmarking support)\n const newUrl = `${pathname}?${params.toString()}`\n window.history.replaceState(null, '', newUrl)\n \n // Restore scroll position after a brief delay to allow content to render\n setTimeout(() => {\n window.scrollTo({\n top: currentScrollY,\n behavior: 'instant' // Instant to prevent any scroll animation\n })\n }, 0)\n }\n\n // Don't render pagination if there's only one page\n if (totalPages <= 1) return null\n\n return (\n <div className={className}>\n <Pagination\n currentPage={currentPage}\n totalPages={totalPages}\n onPageChange={handlePageChange}\n />\n </div>\n )\n} ","\"use client\";\n\nimport { Search, FileText, Package } from \"lucide-react\"\nimport { Button } from \"./ui/button\"\nimport { useRouter } from \"../embed-shims/next-navigation\"\n\nexport interface EmptyStateProps {\n type: 'vendors' | 'posts' | 'search' | 'generic'\n title?: string\n description?: string\n showBackButton?: boolean\n onGoBack?: () => void\n backButtonText?: string\n // New CTA properties\n showCTA?: boolean\n ctaText?: string\n onCtaClick?: () => void\n ctaVariant?: 'primary' | 'secondary'\n}\n\nexport function EmptyState({\n type,\n title,\n description,\n showBackButton = false,\n onGoBack,\n backButtonText = \"Go Back\",\n showCTA = true,\n ctaText,\n onCtaClick,\n ctaVariant = 'primary'\n}: EmptyStateProps) {\n const router = useRouter()\n\n // Default content based on type\n const getDefaultContent = () => {\n switch (type) {\n case 'vendors':\n return {\n icon: <Package className=\"w-full h-full\" />,\n title: \"No vendors found\",\n description: \"We couldn't find any vendors matching your criteria. Try adjusting your filters or search terms.\"\n }\n case 'posts':\n return {\n icon: <FileText className=\"w-full h-full\" />,\n title: \"No articles found\",\n description: \"We couldn't find any articles matching your criteria. Try different categories, tags, or search terms.\"\n }\n case 'search':\n return {\n icon: <Search className=\"w-full h-full\" />,\n title: \"No results found\",\n description: \"Your search didn't return any results. Try different keywords or browse our categories.\"\n }\n default:\n return {\n icon: <Search className=\"w-full h-full\" />,\n title: \"Nothing found\",\n description: \"We couldn't find what you're looking for. Try adjusting your search or filters.\"\n }\n }\n }\n\n // Smart CTA logic based on context\n const getSmartCTA = () => {\n // If custom CTA is provided, use it\n if (ctaText && onCtaClick) {\n return {\n text: ctaText,\n action: onCtaClick\n }\n }\n\n // Check if we're on the client side\n const isClient = typeof window !== 'undefined'\n const currentPath = isClient ? window.location.pathname : ''\n\n // Smart defaults based on type and context\n switch (type) {\n case 'search':\n return {\n text: \"Reset Filters\",\n action: () => {\n if (isClient) {\n // Try to reset search by clearing URL params and refreshing\n const url = new URL(window.location.href)\n url.search = ''\n router.push(url.pathname)\n }\n }\n }\n case 'posts':\n // If we're on blog/community pages, reset blog filters\n if (currentPath.includes('/blog')) {\n return {\n text: \"Reset Filters\",\n action: () => {\n if (isClient) {\n // Reset blog search and filters by clearing URL params\n const url = new URL(window.location.href)\n url.search = ''\n router.push(url.pathname)\n }\n }\n }\n } else if (currentPath.includes('/profile')) {\n return {\n text: \"Browse Vendors\",\n action: () => router.push('/vendors')\n }\n }\n return {\n text: \"View All Posts\",\n action: () => router.push('/blog')\n }\n case 'vendors':\n // If we're in profile or other pages, direct to main content\n if (currentPath.includes('/profile')) {\n return {\n text: \"Browse Vendors\",\n action: () => router.push('/vendors')\n }\n } else if (currentPath.includes('/vendors') || currentPath.includes('/margin-increase/compare')) {\n return {\n text: \"Reset Filters\",\n action: () => {\n if (isClient) {\n // Reset vendor search and filters by clearing URL params\n const url = new URL(window.location.href)\n url.search = ''\n router.push(url.pathname)\n }\n }\n }\n }\n return {\n text: \"Browse Vendors\",\n action: () => router.push('/vendors')\n }\n default:\n return {\n text: \"Browse Vendors\",\n action: () => router.push('/vendors')\n }\n }\n }\n\n const defaultContent = getDefaultContent()\n const displayTitle = title || defaultContent.title\n const displayDescription = description || defaultContent.description\n const smartCTA = getSmartCTA()\n\n return (\n <div className=\"flex flex-col items-center justify-center py-6 md:py-16 px-6 text-center\">\n {/* Icon */}\n <div className=\"mb-3 md:mb-6 flex items-center justify-center\">\n <div className=\"rounded-full bg-ods-card p-3 md:p-6 border border-ods-border\">\n <div className=\"w-8 h-8 md:w-16 md:h-16 text-ods-text-secondary flex items-center justify-center\">\n {defaultContent.icon}\n </div>\n </div>\n </div>\n\n {/* Title */}\n <h2 className=\"mb-2 md:mb-3 text-lg md:text-xl font-semibold font-['DM_Sans'] text-ods-text-primary tracking-[-0.02em]\">\n {displayTitle}\n </h2>\n\n {/* Description */}\n <p className=\"mb-4 md:mb-8 max-w-md text-sm font-medium font-['DM_Sans'] text-ods-text-secondary leading-[1.43em]\">\n {displayDescription}\n </p>\n\n {/* Smart CTA Button */}\n {showCTA && smartCTA && (\n <div className=\"w-full max-w-xs mb-3\">\n <Button\n onClick={smartCTA.action}\n className={ctaVariant === 'primary'\n ? \"w-full bg-ods-accent text-ods-text-on-accent hover:bg-ods-accent-hover transition-all duration-150 font-['DM_Sans'] font-medium\"\n : \"w-full bg-transparent border border-ods-border text-ods-text-primary hover:border-ods-accent hover:text-ods-accent transition-all duration-150 font-['DM_Sans'] font-medium\"\n }\n >\n {smartCTA.text}\n </Button>\n </div>\n )}\n\n {/* Optional Back Button */}\n {showBackButton && onGoBack && (\n <div className=\"w-full max-w-xs\">\n <Button\n onClick={onGoBack}\n variant=\"outline\"\n className=\"w-full transition-all duration-150 font-['DM_Sans'] font-medium\"\n >\n {backButtonText}\n </Button>\n </div>\n )}\n </div>\n )\n} ","'use client';\n\n/**\n * DevSectionView — the canonical chrome for ANY dev-center section\n * (Roadmap / Delivery / Releases). One component, used in BOTH:\n *\n * - tabbed `/roadmap-and-releases` (compact title mode, no `hero`)\n * - full-page `/roadmap`, `/bug-fixes-and-enhancements`, `/releases`\n * (hero mode with icon + description + back link)\n *\n * Owns: title rendering, the inline search input, the filter pill row,\n * and the URL-param wiring that connects both. The list `children`\n * receive a clean URL contract — they read `?<paramKey>=...` via\n * `useSearchParams()` and refetch on change. No duplicated controls.\n */\n\nimport type { ReactNode } from 'react';\nimport { useState, useEffect } from 'react';\nimport { useRouter, useSearchParams, usePathname } from '../../../embed-shims';\nimport { SearchInput } from '../../ui';\nimport { StatusFilterComponent } from '../../features';\nimport {\n OPENFRAME_DEV_SECTIONS,\n type OpenframeDevSectionKey,\n} from '../../../utils/dev-sections/openframe-dev-sections';\n\nexport interface DevSectionViewProps {\n /** Which section to render — drives title, search, and filter\n * config via the `OPENFRAME_DEV_SECTIONS` registry. */\n sectionKey: OpenframeDevSectionKey;\n /** When set, renders the rich page-level hero (icon + h1 + description).\n * Omit for the compact tab-context heading. */\n hero?: {\n /** Pre-rendered icon JSX. Server components render the icon themselves\n * and pass the element here — function references can't cross the\n * server→client boundary, but React elements can. */\n icon: ReactNode;\n /** Hero title. Falls back to `OPENFRAME_DEV_SECTIONS[sectionKey].hero.title`\n * when omitted, so embedders can override the (OpenFrame-specific) default\n * copy without forking the registry. */\n title?: string;\n description: string;\n };\n /** Optional slot rendered BETWEEN the hero and the search/filter\n * controls. Use this for an entry-action surface that should sit\n * above the list (e.g. the Help Center's \"Open a new ticket\" form).\n * The slot is wrapped in the same `gap-10` flex column so spacing\n * matches the surrounding chrome — callers should NOT add their\n * own top/bottom margin. Renders `null` (no DOM) when omitted. */\n preControls?: ReactNode;\n /** The page-specific list body. Reads URL params written by this\n * component (search input + filter pills). */\n children: ReactNode;\n}\n\nexport function DevSectionView({ sectionKey, hero, preControls, children }: DevSectionViewProps) {\n const section = OPENFRAME_DEV_SECTIONS[sectionKey];\n const router = useRouter();\n const pathname = usePathname();\n const searchParams = useSearchParams();\n\n const search = section.search;\n const filter = section.filter;\n\n const currentSearch = search ? searchParams.get(search.paramKey) || '' : '';\n const currentFilterValue = filter\n ? searchParams.get(filter.paramKey) || filter.defaultValue\n : '';\n\n // Controlled search-input state — input commits to the URL only on\n // Enter (not on every keystroke), preserving the legacy behavior.\n // Lazy init from URL avoids a brief flash of stale value on first\n // paint after URL-driven re-render (e.g. tab switch).\n const [searchValue, setSearchValue] = useState(() => currentSearch);\n useEffect(() => {\n setSearchValue(currentSearch);\n }, [currentSearch]);\n\n const handleSearchSubmit = (value: string) => {\n if (!search) return;\n const params = new URLSearchParams(searchParams.toString());\n if (value.trim()) params.set(search.paramKey, value.trim());\n else params.delete(search.paramKey);\n router.replace(`${pathname}?${params.toString()}`, { scroll: false });\n };\n\n const handleFilterChange = (value: string) => {\n if (!filter) return;\n const params = new URLSearchParams(searchParams.toString());\n if (value === filter.defaultValue) params.delete(filter.paramKey);\n else params.set(filter.paramKey, value);\n router.replace(`${pathname}?${params.toString()}`, { scroll: false });\n };\n\n return (\n <div className=\"w-full flex flex-col gap-10\">\n {hero ? (\n <div className=\"space-y-4\">\n <h1 className=\"text-h1 tracking-[-1.12px] text-ods-text-primary flex items-center gap-3\">\n {hero.icon}\n {hero.title ?? section.hero.title}\n </h1>\n <p className=\"font-['DM_Sans'] font-medium text-[18px] leading-[28px] text-ods-text-secondary max-w-3xl\">\n {hero.description}\n </p>\n </div>\n ) : (\n <div className=\"flex items-center justify-between w-full\">\n <h2 className=\"font-['Azeret_Mono'] font-semibold text-[32px] md:text-[40px] lg:text-[48px] leading-[40px] md:leading-[48px] lg:leading-[56px] text-ods-text-primary tracking-[-0.64px] md:tracking-[-0.8px] lg:tracking-[-0.96px]\">\n {section.hero.title}\n <span className=\"text-ods-accent\">:</span>\n </h2>\n </div>\n )}\n\n {preControls}\n\n {(search || filter) && (\n <div className=\"space-y-4\">\n {search && (\n <SearchInput\n showDropdown={false}\n placeholder={search.placeholder}\n value={searchValue}\n onChange={setSearchValue}\n onSubmit={handleSearchSubmit}\n />\n )}\n {filter && (\n <StatusFilterComponent\n selectedStatus={currentFilterValue}\n onStatusChange={handleFilterChange}\n statusOptions={[...filter.options]}\n />\n )}\n </div>\n )}\n\n {children}\n </div>\n );\n}\n","'use client';\n\n/**\n * DevSectionPage — full-page wrapper for a dev-center section\n * (`/roadmap`, `/bug-fixes-and-enhancements`, `/releases`).\n *\n * Mounts the lib's canonical `PageLayout` directly (no in-app wrapper)\n * so the back-button affordance stays in lockstep with whatever the\n * design system ships — any future lib change to BackButton / TitleBlock\n * propagates automatically.\n *\n * Composition: `PageShell` → `PageLayout` (back-to-home wired) →\n * `DevSectionView` (icon hero + search + filter pills) → list body.\n *\n * Adding a new section is one entry in `OPENFRAME_DEV_SECTIONS` plus a\n * single-line page file mounting this factory with the new key.\n */\n\nimport type { ReactNode } from 'react';\nimport { useRouter } from '../../../embed-shims/next-navigation';\nimport { PageShell, PageLayout } from '../../ui';\nimport { DevSectionView } from './dev-section-view';\nimport {\n OPENFRAME_DEV_SECTIONS,\n type OpenframeDevSectionKey,\n} from '../../../utils/dev-sections/openframe-dev-sections';\n\nconst SECTION_HERO_ICON_CLASS = 'h-10 w-10 text-ods-accent';\n\nexport interface DevSectionPageProps {\n sectionKey: OpenframeDevSectionKey;\n /** The page-specific list body (e.g. `<RoadmapList />`). */\n children: ReactNode;\n /** Optional slot rendered BETWEEN the hero and search/filter — see\n * `DevSectionView.preControls`. Used by surfaces that want an entry\n * action (e.g. Help Center's \"Open a new ticket\" form) above the\n * controls instead of below them. */\n preControls?: ReactNode;\n /** Back-button config — same shape as `LegalDocumentPage` /\n * `ReleaseDetailPage`. Pass `false` to hide entirely. Default\n * `{ label: 'Back to home', href: '/' }` — embedders whose \"home\" isn't `/`\n * should override `href`, or pass `false` if the embed has no home page. */\n backButton?: { label?: string; href?: string } | false;\n /** Override the hero title. Defaults to the (OpenFrame-specific) copy in\n * `OPENFRAME_DEV_SECTIONS[sectionKey].hero.title`. Set this to brand the\n * section for a non-OpenFrame embed. */\n title?: string;\n /** Override the hero subtitle/description. Defaults to\n * `OPENFRAME_DEV_SECTIONS[sectionKey].hero.description`. */\n subtitle?: string;\n}\n\nexport function DevSectionPage({\n sectionKey,\n children,\n preControls,\n backButton,\n title,\n subtitle,\n}: DevSectionPageProps) {\n const router = useRouter();\n const section = OPENFRAME_DEV_SECTIONS[sectionKey];\n const Icon = section.icon;\n\n // Back-button config — mirrors LegalDocumentPage / ReleaseDetailPage.\n // Default: { label: 'Back to home', href: '/' }. Pass `false` to hide.\n // After `backButton &&` narrowing, inner type is `{ label?, href? } |\n // undefined`; don't re-compare to `false` (TS2367).\n const backCfg =\n backButton === false\n ? undefined\n : {\n label: (backButton ? backButton.label : undefined) ?? 'Back to home',\n onClick: () => router.push((backButton ? backButton.href : undefined) ?? '/'),\n };\n\n return (\n <PageShell>\n <PageLayout backButton={backCfg}>\n <DevSectionView\n sectionKey={sectionKey}\n hero={{\n icon: <Icon className={SECTION_HERO_ICON_CLASS} />,\n title,\n description: subtitle ?? section.hero.description,\n }}\n preControls={preControls}\n >\n {children}\n </DevSectionView>\n </PageLayout>\n </PageShell>\n );\n}\n","'use client';\n\n/**\n * Shared row chrome for any `DevSectionPage` list (delivery, tickets,\n * future sections). One source of truth for the layout that every\n * dev-section card row uses:\n * left column → title (h3) / subtitle (h5 uppercase) / description\n * (h4 line-clamp-3), each in a fixed min-height block\n * so rows align across the grid\n * right column → caller-supplied stacked badges\n *\n * Surface stays small on purpose — `rightBadges` is a `ReactNode` so\n * the caller decides how many badges (delivery: 2, tickets: 1-2,\n * future: anything). No behavior baked in: the caller wraps the row\n * in a `<div>` (static, like delivery) or `<button>` (clickable, like\n * tickets) and renders the row content via this component.\n *\n * Pair with `DevCardRowSkeletonList` for the loading state — the\n * skeleton mirrors the same min-heights so the in-flight UI doesn't\n * shift the layout when real data lands.\n *\n * NOTE: the ticket conversation row is NOT here — it renders the shared\n * `<ChatMessageRow>` (`components/chat/chat-message-row.tsx`), the SAME\n * component the OpenMSP Slack-community feed uses, so the two surfaces stay\n * pixel-identical by construction.\n */\n\nimport type { ReactNode } from 'react';\n\nexport interface DevCardRowContentProps {\n title: string;\n /** Single-line uppercase metadata (e.g. \"UPDATED today, #4271, Code review\"). */\n subtitle: string;\n /** 3-line description block. Empty string renders the fallback. */\n description: string;\n /** Fallback copy when `description` is empty. Defaults to a generic\n * string; ticket / delivery surfaces override. */\n emptyDescription?: string;\n /** Right column — caller renders its own stacked badges. */\n rightBadges: ReactNode;\n}\n\nexport function DevCardRowContent({\n title,\n subtitle,\n description,\n emptyDescription = 'No description provided',\n rightBadges,\n}: DevCardRowContentProps) {\n return (\n <div className=\"flex flex-col md:flex-row items-start justify-between gap-[12px] md:gap-[16px] w-full\">\n <div className=\"flex-1 min-w-0 w-full md:w-auto flex flex-col gap-[12px] md:gap-[16px]\">\n <div className=\"min-h-[24px] flex items-center\">\n <h3 className=\"text-h3 text-ods-text-primary tracking-[-0.36px] flex-1 line-clamp-2 md:truncate break-words\">\n {title}\n </h3>\n </div>\n <div className=\"min-h-[20px] flex items-center\">\n <p className=\"text-h5 text-ods-text-secondary uppercase tracking-[-0.28px] truncate\">\n {subtitle}\n </p>\n </div>\n <div className=\"min-h-[72px] flex items-center\">\n <p className=\"text-h4 text-ods-text-secondary line-clamp-3 break-words\">\n {description || emptyDescription}\n </p>\n </div>\n </div>\n <div className=\"flex-shrink-0 self-start flex flex-col gap-2\">\n {rightBadges}\n </div>\n </div>\n );\n}\n\n/**\n * Skeleton rendering for a single row — the bars mirror the same\n * min-heights as `DevCardRowContent` so the loading→loaded swap\n * doesn't reflow.\n */\nexport function DevCardRowSkeleton() {\n return (\n <div className=\"border-b border-ods-border last:border-b-0 p-[12px] md:p-[16px]\">\n <div className=\"flex flex-col md:flex-row items-start justify-between gap-[12px] md:gap-[16px] w-full\">\n <div className=\"flex-1 min-w-0 w-full md:w-auto flex flex-col gap-[12px] md:gap-[16px]\">\n <div className=\"min-h-[24px] flex items-center\">\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-full\" />\n </div>\n <div className=\"min-h-[20px] flex items-center\">\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-1/2\" />\n </div>\n <div className=\"min-h-[72px] flex items-center\">\n <div className=\"flex-1 space-y-1\">\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-full\" />\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-full\" />\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-2/3\" />\n </div>\n </div>\n </div>\n <div className=\"flex-shrink-0 self-start flex flex-col gap-2\">\n <div className=\"h-[32px] w-[100px] bg-ods-border rounded animate-pulse\" />\n <div className=\"h-[32px] w-[120px] bg-ods-border rounded animate-pulse\" />\n </div>\n </div>\n </div>\n );\n}\n\n/**\n * The standard \"5 skeleton rows inside a bordered card\" loading state\n * used by every list shell. Both delivery (`delivery-table.tsx`) and\n * tickets (`tickets-list.tsx`) mount this directly.\n */\nexport function DevCardRowSkeletonList({ rows = 5 }: { rows?: number }) {\n return (\n <div className=\"bg-ods-card border border-ods-border rounded-[6px] overflow-hidden w-full\">\n {Array.from({ length: rows }, (_, i) => (\n <DevCardRowSkeleton key={i} />\n ))}\n </div>\n );\n}\n","'use client'\n\n/**\n * `<DeliveryRow />` — canonical single-row presentation for a ClickUp\n * delivery item.\n *\n * Single source of truth: both the `/bug-fixes-and-enhancements` page\n * (via `DeliveryTable`) AND the linked-delivery card on a HubSpot ticket\n * (via `TicketLinkedDeliveryCard`) compose this primitive. Visual parity\n * across those two surfaces is the design goal — the user reads the\n * card on their ticket and recognises it as a row from the public\n * delivery list.\n *\n * Behaviors:\n * - `href` set → outer element is an `<a>`, the whole row becomes\n * clickable (used by the linked-card surface to deep-link into\n * `/bug-fixes-and-enhancements?focus=<id>`).\n * - `id` set → outer element gets that DOM id so the consuming page\n * can `scrollIntoView` to it when the URL carries `?focus=<id>`.\n * - `highlighted` true → brief accent border + background pulse\n * (`animate-flash-focus` keyframe defined in `tailwind.config.ts`).\n * - `caption` set → small uppercase label rendered above the title\n * (\"LINKED DELIVERY\" on the ticket-side variant). Omitted on the\n * standard list rendering.\n */\n\nimport * as React from 'react'\nimport Link from '../../../embed-shims/next-link'\nimport { StatusBadge } from '../../ui/status-badge'\nimport { getStatusColorScheme } from '../../../utils'\nimport {\n type DeliveryItem,\n TASK_TYPE_LABELS,\n TASK_TYPE_TEXT_COLORS,\n} from '../../../types/delivery'\nimport { cn } from '../../../utils/cn'\n\n/** Same heuristic as DeliveryTable's local helper. Inlined so the row\n * primitive owns its complete rendering contract. */\nfunction getRelativeTime(timestamp: number): string {\n const now = Date.now()\n const diff = now - timestamp\n const days = Math.floor(diff / (1000 * 60 * 60 * 24))\n const weeks = Math.floor(days / 7)\n const months = Math.floor(days / 30)\n if (months > 0) return months === 1 ? 'last month' : `${months} months ago`\n if (weeks > 0) return weeks === 1 ? 'last week' : `${weeks} weeks ago`\n if (days > 0) return days === 1 ? 'yesterday' : `${days} days ago`\n return 'today'\n}\n\nexport interface DeliveryRowProps {\n item: DeliveryItem\n /** When set, the row becomes a clickable anchor. The ticket-side\n * linked-card composes this from `buildDevSectionUrl('delivery', id)`\n * which carries `?search=<id>` — the delivery list filters to that\n * exact task on landing (canonical deep-link mechanism, same one\n * the chat-inline delivery card uses). */\n href?: string\n /** Small uppercase caption rendered above the title. Used by the\n * linked-delivery card variant (\"LINKED DELIVERY\"). */\n caption?: string\n className?: string\n}\n\nexport function DeliveryRow({\n item,\n href,\n caption,\n className,\n}: DeliveryRowProps) {\n const taskType = item.taskType as keyof typeof TASK_TYPE_LABELS\n const typeBadgeLabel = TASK_TYPE_LABELS[taskType] || 'TASK'\n const typeBadgeTextColor = TASK_TYPE_TEXT_COLORS[taskType] || ''\n const statusBadgeScheme = getStatusColorScheme(item.status)\n const relativeTime = getRelativeTime(item.dateUpdated)\n const subtitle = `ACTIVE ${relativeTime}${item.listNames.length > 0 ? `, ${item.listNames.join(', ')}` : ''}, ${item.id}`\n\n const inner = (\n <div className=\"flex flex-col md:flex-row items-start justify-between gap-[12px] md:gap-[16px] w-full\">\n {/* Left: caption (optional) + title + subtitle + description */}\n <div className=\"flex-1 min-w-0 w-full md:w-auto flex flex-col gap-[12px] md:gap-[16px]\">\n {caption && (\n <p className=\"text-xs font-medium uppercase tracking-wider text-ods-text-secondary\">\n {caption}\n </p>\n )}\n <div className=\"min-h-[24px] md:min-h-[24px] flex items-center\">\n <h3 className=\"text-h3 text-ods-text-primary tracking-[-0.36px] flex-1 line-clamp-2 md:truncate break-words\">\n {item.title}\n </h3>\n </div>\n <div className=\"min-h-[20px] flex items-center\">\n <p className=\"text-h5 text-ods-text-secondary uppercase tracking-[-0.28px] truncate\">\n {subtitle}\n </p>\n </div>\n <div className=\"min-h-[72px] flex items-center\">\n <p className=\"text-h4 text-ods-text-secondary line-clamp-3 break-words\">\n {item.description || 'No description provided'}\n </p>\n </div>\n </div>\n\n {/* Right: status + task-type badges */}\n <div className=\"flex-shrink-0 self-start flex flex-col gap-2\">\n <StatusBadge\n text={item.status.toUpperCase()}\n colorScheme={statusBadgeScheme}\n variant=\"card\"\n className=\"border border-ods-border\"\n />\n <StatusBadge\n text={typeBadgeLabel}\n variant=\"card\"\n className={`border border-ods-border ${typeBadgeTextColor}`}\n />\n </div>\n </div>\n )\n\n const baseClass = cn(\n 'block p-[12px] md:p-[16px] no-underline text-inherit transition-colors duration-150',\n href && 'hover:bg-ods-bg-hover cursor-pointer',\n className,\n )\n\n if (href) {\n // `Link` is the env-aware embed-shim — delegates to `next/link` on\n // a Next.js host (soft RSC nav, back-button restores the previous\n // page's React state intact), falls back to a plain `<a>` on\n // non-Next embedders. A raw `<a href>` was hard-navigating +\n // losing TanStack-Query state on back, leaving /tickets stuck on\n // its skeleton.\n return (\n <Link href={href} className={baseClass} prefetch={false}>\n {inner}\n </Link>\n )\n }\n\n return <div className={baseClass}>{inner}</div>\n}\n"]}
@@ -11,7 +11,7 @@ import {
11
11
  SelectValue,
12
12
  Textarea,
13
13
  useChatAttachments
14
- } from "./chunk-GO45CXT5.js";
14
+ } from "./chunk-AVLSICZO.js";
15
15
  import {
16
16
  useContactSubmission,
17
17
  useHumanitySignals
@@ -376,4 +376,4 @@ function ContactForm({
376
376
  export {
377
377
  ContactForm
378
378
  };
379
- //# sourceMappingURL=chunk-5NL62LIF.js.map
379
+ //# sourceMappingURL=chunk-7ECI5ZVO.js.map
@@ -7,7 +7,7 @@ var _chunkGVSTRN36cjs = require('./chunk-GVSTRN36.cjs');
7
7
 
8
8
 
9
9
 
10
- var _chunk7LTWNUHBcjs = require('./chunk-7LTWNUHB.cjs');
10
+ var _chunkZ53YSXDDcjs = require('./chunk-Z53YSXDD.cjs');
11
11
 
12
12
 
13
13
 
@@ -82,7 +82,7 @@ function FaqAccordion({ items, defaultOpenIds = [] }) {
82
82
  children: [
83
83
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "min-w-0 pr-4", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "h3", { children: item.question }) }),
84
84
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "flex-shrink-0", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
85
- _chunk7LTWNUHBcjs.ChevronButton,
85
+ _chunkZ53YSXDDcjs.ChevronButton,
86
86
  {
87
87
  "aria-label": isOpen ? "Collapse question" : "Expand question",
88
88
  size: "md",
@@ -142,7 +142,7 @@ function buildFaqJsonLdFromFaqs(faqs, opts = {}) {
142
142
 
143
143
  var DEFAULT_HEADING_TEXT = "Frequently Asked Questions";
144
144
  function buildFaqsUrl(entityType, entityId, minResults, apiBaseUrl = "") {
145
- return _chunk7LTWNUHBcjs.buildSuggestionUrl.call(void 0, "/api/faqs", { apiBaseUrl, entityType, entityId, count: minResults });
145
+ return _chunkZ53YSXDDcjs.buildSuggestionUrl.call(void 0, "/api/faqs", { apiBaseUrl, entityType, entityId, count: minResults });
146
146
  }
147
147
  function sectionSlug(section) {
148
148
  return "faq-" + section.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -212,7 +212,7 @@ function GroupedFaqList({
212
212
  (e, slug) => {
213
213
  e.preventDefault();
214
214
  setActiveSlug(slug);
215
- _chunk7LTWNUHBcjs.scrollElementIntoView.call(void 0, document.getElementById(slug), {
215
+ _chunkZ53YSXDDcjs.scrollElementIntoView.call(void 0, document.getElementById(slug), {
216
216
  headerOffset: FAQ_NAV_HEADER_OFFSET
217
217
  });
218
218
  if (typeof history !== "undefined") history.replaceState(null, "", `#${slug}`);
@@ -295,7 +295,7 @@ function FaqSection({
295
295
  "script",
296
296
  {
297
297
  type: "application/ld+json",
298
- dangerouslySetInnerHTML: { __html: _chunk7LTWNUHBcjs.serializeJsonLd.call(void 0, schema) }
298
+ dangerouslySetInnerHTML: { __html: _chunkZ53YSXDDcjs.serializeJsonLd.call(void 0, schema) }
299
299
  }
300
300
  )
301
301
  ] });
@@ -335,4 +335,4 @@ function FaqDocumentPage({
335
335
 
336
336
 
337
337
  exports.FaqAccordion = FaqAccordion; exports.FaqSection = FaqSection; exports.FaqDocumentPage = FaqDocumentPage;
338
- //# sourceMappingURL=chunk-BBDJCXHW.cjs.map
338
+ //# sourceMappingURL=chunk-7XVVRE53.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-BBDJCXHW.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-BBDJCXHW.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 { 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","'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 the\n * bare `<FaqSection>`. Mirrors `LegalDocumentPage` / `DevSectionPage`: the\n * page-level layout lives in the lib, the host passes only config + a back button.\n *\n * `<FaqSection heading={null}>` lets `PageLayout`'s `TitleBlock` own the heading +\n * back button; it self-fetches `${apiBaseUrl}/api/faqs` via the authed\n * `useSelfFetch`, and renders nothing on a fetch error or zero FAQs.\n */\n\nimport { PageShell, PageLayout } from '../ui';\nimport { useRouter } from '../../embed-shims/next-navigation';\nimport { FaqSection } from './faq-section';\n\nexport interface FaqDocumentPageProps {\n /** Page title (PageLayout TitleBlock). Default \"FAQs\". */\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 /** 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 = 'FAQs',\n subtitle,\n backButton,\n apiBaseUrl,\n entityType,\n entityId,\n minResults,\n}: FaqDocumentPageProps) {\n const router = useRouter();\n\n // Back-button config — mirrors LegalDocumentPage/DevSectionPage. Hide entirely\n // when the caller passes `false` (embed-mode where the host owns 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 return (\n <PageShell>\n <PageLayout title={title} subtitle={subtitle} backButton={backCfg}>\n <FaqSection\n heading={null}\n apiBaseUrl={apiBaseUrl}\n entityType={entityType}\n entityId={entityId}\n minResults={minResults}\n />\n </PageLayout>\n </PageShell>\n );\n}\n"]}
1
+ {"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-7XVVRE53.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-7XVVRE53.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 { 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","'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 the\n * bare `<FaqSection>`. Mirrors `LegalDocumentPage` / `DevSectionPage`: the\n * page-level layout lives in the lib, the host passes only config + a back button.\n *\n * `<FaqSection heading={null}>` lets `PageLayout`'s `TitleBlock` own the heading +\n * back button; it self-fetches `${apiBaseUrl}/api/faqs` via the authed\n * `useSelfFetch`, and renders nothing on a fetch error or zero FAQs.\n */\n\nimport { PageShell, PageLayout } from '../ui';\nimport { useRouter } from '../../embed-shims/next-navigation';\nimport { FaqSection } from './faq-section';\n\nexport interface FaqDocumentPageProps {\n /** Page title (PageLayout TitleBlock). Default \"FAQs\". */\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 /** 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 = 'FAQs',\n subtitle,\n backButton,\n apiBaseUrl,\n entityType,\n entityId,\n minResults,\n}: FaqDocumentPageProps) {\n const router = useRouter();\n\n // Back-button config — mirrors LegalDocumentPage/DevSectionPage. Hide entirely\n // when the caller passes `false` (embed-mode where the host owns 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 return (\n <PageShell>\n <PageLayout title={title} subtitle={subtitle} backButton={backCfg}>\n <FaqSection\n heading={null}\n apiBaseUrl={apiBaseUrl}\n entityType={entityType}\n entityId={entityId}\n minResults={minResults}\n />\n </PageLayout>\n </PageShell>\n );\n}\n"]}