@flamingo-stack/openframe-frontend-core 0.0.365 → 0.0.366
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-N6WPRZTS.js → chunk-3VGGQJIG.js} +19 -12
- package/dist/chunk-3VGGQJIG.js.map +1 -0
- package/dist/{chunk-5VDFSOY5.js → chunk-57YGF3UX.js} +5 -5
- package/dist/chunk-57YGF3UX.js.map +1 -0
- package/dist/{chunk-SUX2M6N4.js → chunk-5UYS6HX6.js} +5 -4
- package/dist/{chunk-SUX2M6N4.js.map → chunk-5UYS6HX6.js.map} +1 -1
- package/dist/{chunk-VQEIDKRF.cjs → chunk-65R4KAF3.cjs} +16 -15
- package/dist/chunk-65R4KAF3.cjs.map +1 -0
- package/dist/{chunk-5HC75IGV.js → chunk-AWSBFLM4.js} +2 -2
- package/dist/{chunk-6SPGZUEK.cjs → chunk-BFIXLKD7.cjs} +26 -25
- package/dist/chunk-BFIXLKD7.cjs.map +1 -0
- package/dist/{chunk-VKGU6VUH.js → chunk-BTPPX4OH.js} +3 -3
- package/dist/{chunk-VKGU6VUH.js.map → chunk-BTPPX4OH.js.map} +1 -1
- package/dist/{chunk-UD4KDJI7.cjs → chunk-BW5TNAAB.cjs} +12 -12
- package/dist/{chunk-UD4KDJI7.cjs.map → chunk-BW5TNAAB.cjs.map} +1 -1
- package/dist/{chunk-ZP3RBV5V.js → chunk-FGSAACQ6.js} +2 -2
- package/dist/{chunk-SBLUFKYO.cjs → chunk-HY6SX7SY.cjs} +26 -26
- package/dist/{chunk-SBLUFKYO.cjs.map → chunk-HY6SX7SY.cjs.map} +1 -1
- package/dist/{chunk-WRNHB7VU.cjs → chunk-J4TUSKUD.cjs} +5 -5
- package/dist/{chunk-WRNHB7VU.cjs.map → chunk-J4TUSKUD.cjs.map} +1 -1
- package/dist/{chunk-A3CPTGGM.cjs → chunk-JA6Y4RQA.cjs} +7 -7
- package/dist/{chunk-A3CPTGGM.cjs.map → chunk-JA6Y4RQA.cjs.map} +1 -1
- package/dist/{chunk-FCOF37RK.js → chunk-JCVQ25SH.js} +4 -4
- package/dist/{chunk-YM47ANOU.js → chunk-KY5R4WEH.js} +3 -2
- package/dist/chunk-KY5R4WEH.js.map +1 -0
- package/dist/{chunk-BRTTK62V.js → chunk-LB7GFBSI.js} +2 -2
- package/dist/{chunk-WHBKC37Z.cjs → chunk-MHQ7FGTU.cjs} +290 -260
- package/dist/chunk-MHQ7FGTU.cjs.map +1 -0
- package/dist/{chunk-RZMKRZRV.cjs → chunk-OYBPLAEL.cjs} +62 -62
- package/dist/{chunk-RZMKRZRV.cjs.map → chunk-OYBPLAEL.cjs.map} +1 -1
- package/dist/{chunk-NPTT2NWR.cjs → chunk-R7REILGJ.cjs} +9 -9
- package/dist/{chunk-NPTT2NWR.cjs.map → chunk-R7REILGJ.cjs.map} +1 -1
- package/dist/{chunk-Y32I474I.js → chunk-SELQ4BMB.js} +2 -2
- package/dist/{chunk-KLTOLXUE.js → chunk-VJNA6NSE.js} +3625 -3595
- package/dist/chunk-VJNA6NSE.js.map +1 -0
- package/dist/{chunk-CW3724TS.js → chunk-XDAAWSBX.js} +2 -2
- package/dist/{chunk-4H7U7XUG.cjs → chunk-ZDXDRYAC.cjs} +70 -63
- package/dist/chunk-ZDXDRYAC.cjs.map +1 -0
- package/dist/{chunk-VM7UPWHI.cjs → chunk-ZV4736W2.cjs} +37 -37
- package/dist/{chunk-VM7UPWHI.cjs.map → chunk-ZV4736W2.cjs.map} +1 -1
- package/dist/{chunk-54LUSGMX.cjs → chunk-ZYK4YPUJ.cjs} +31 -31
- package/dist/chunk-ZYK4YPUJ.cjs.map +1 -0
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/help-center-pages/index.cjs +20 -20
- package/dist/components/help-center-pages/index.js +11 -11
- package/dist/components/index.cjs +122 -120
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +11 -9
- package/dist/components/index.js.map +1 -1
- package/dist/components/layout/page-layout.d.ts +5 -1
- package/dist/components/layout/page-layout.d.ts.map +1 -1
- package/dist/components/layout/title-block.d.ts +5 -1
- package/dist/components/layout/title-block.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/shared/dev-section/dev-section-page.d.ts.map +1 -1
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +5 -5
- package/dist/components/tickets/index.js +4 -4
- package/dist/components/ui/fade-preview.d.ts +33 -0
- package/dist/components/ui/fade-preview.d.ts.map +1 -0
- package/dist/components/ui/index.cjs +4 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +3 -1
- package/dist/components/ui/release-changelog-section.d.ts.map +1 -1
- package/dist/index.cjs +4 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -1
- package/package.json +1 -1
- package/src/components/docs/doc-viewer.tsx +1 -1
- package/src/components/faq/faq-document-page.tsx +1 -1
- package/src/components/layout/page-layout.tsx +11 -0
- package/src/components/layout/title-block.tsx +24 -4
- package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +1 -0
- package/src/components/shared/dev-section/dev-section-page.tsx +1 -0
- package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
- package/src/components/shared/product-release/release-detail-page.tsx +26 -5
- package/src/components/ui/fade-preview.tsx +87 -0
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/release-changelog-section.tsx +12 -50
- package/dist/chunk-4H7U7XUG.cjs.map +0 -1
- package/dist/chunk-54LUSGMX.cjs.map +0 -1
- package/dist/chunk-5VDFSOY5.js.map +0 -1
- package/dist/chunk-6SPGZUEK.cjs.map +0 -1
- package/dist/chunk-KLTOLXUE.js.map +0 -1
- package/dist/chunk-N6WPRZTS.js.map +0 -1
- package/dist/chunk-VQEIDKRF.cjs.map +0 -1
- package/dist/chunk-WHBKC37Z.cjs.map +0 -1
- package/dist/chunk-YM47ANOU.js.map +0 -1
- /package/dist/{chunk-5HC75IGV.js.map → chunk-AWSBFLM4.js.map} +0 -0
- /package/dist/{chunk-ZP3RBV5V.js.map → chunk-FGSAACQ6.js.map} +0 -0
- /package/dist/{chunk-FCOF37RK.js.map → chunk-JCVQ25SH.js.map} +0 -0
- /package/dist/{chunk-BRTTK62V.js.map → chunk-LB7GFBSI.js.map} +0 -0
- /package/dist/{chunk-Y32I474I.js.map → chunk-SELQ4BMB.js.map} +0 -0
- /package/dist/{chunk-CW3724TS.js.map → chunk-XDAAWSBX.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/persistent-pagination.tsx","../src/components/shared/product-release/product-releases-view.tsx","../src/components/shared/media-gallery-strip.tsx","../src/components/shared/product-release/release-detail-page.tsx","../src/components/shared/product-release/release-detail-skeleton.tsx","../src/components/shared/roadmap/use-roadmap-voting.ts","../src/components/shared/roadmap/roadmap-grid.tsx","../src/components/shared/roadmap/roadmap-grid-skeleton.tsx","../src/components/shared/roadmap/roadmap-view.tsx","../src/components/shared/delivery/delivery-table.tsx","../src/components/shared/delivery/delivery-lists.tsx","../src/components/shared/legal-document/use-legal-docs.ts","../src/components/shared/legal-document/legal-document-page.tsx"],"sourcesContent":["\"use client\"\n\nimport { ReactNode } from \"react\"\nimport { cn } from \"../utils/cn\"\n\ninterface PersistentPaginationProps {\n /**\n * Whether pagination is currently in a loading state\n */\n isLoading: boolean\n /**\n * The pagination component\n */\n children: ReactNode\n /**\n * Current page number\n */\n currentPage: number\n /**\n * Total number of pages\n */\n totalPages: number\n /**\n * Additional CSS classes\n */\n className?: string\n /**\n * Disabled opacity (0-1)\n */\n disabledOpacity?: number\n /**\n * Transition duration in milliseconds\n */\n transitionDuration?: number\n /**\n * Whether to show loading state in pagination\n */\n showLoadingState?: boolean\n}\n\n/**\n * PersistentPagination\n * \n * A wrapper component that keeps pagination visible during loading states\n * but provides visual feedback that it's temporarily disabled.\n * \n * Features:\n * - Maintains pagination visibility during content loading\n * - Disables interaction but preserves layout\n * - Shows current page context even when loading\n * - Provides accessibility support for loading states\n * - Smooth transitions between enabled/disabled states\n * \n * Usage:\n * ```tsx\n * <PersistentPagination\n * isLoading={isLoadingVendors}\n * currentPage={currentPage}\n * totalPages={totalPages}\n * >\n * <Pagination currentPage={currentPage} totalPages={totalPages} />\n * </PersistentPagination>\n * ```\n */\nexport function PersistentPagination({\n isLoading,\n children,\n currentPage,\n totalPages,\n className,\n disabledOpacity = 0.5,\n transitionDuration = 300,\n showLoadingState = true,\n}: PersistentPaginationProps) {\n // ALWAYS show pagination for predictable layout - just gray it out when not needed\n // Removed condition that hides pagination completely\n\n return (\n <div \n className={cn(\n \"relative transition-all ease-in-out\",\n \"flex justify-center items-center\",\n isLoading && \"pointer-events-none\",\n className\n )}\n style={{\n opacity: isLoading ? disabledOpacity : 1,\n transitionDuration: `${transitionDuration}ms`\n }}\n role=\"navigation\"\n aria-label=\"Pagination\"\n aria-busy={isLoading}\n data-loading={isLoading}\n >\n {/* Loading state announcement for screen readers */}\n {isLoading && (\n <div \n className=\"sr-only\" \n role=\"status\" \n aria-live=\"polite\"\n >\n Pagination temporarily disabled while loading page {currentPage} of {totalPages}\n </div>\n )}\n\n {/* Pagination controls with disabled state */}\n <div \n className={cn(\n \"relative transition-all ease-in-out\",\n isLoading && \"cursor-not-allowed\"\n )}\n style={{\n transitionDuration: `${transitionDuration}ms`\n }}\n aria-hidden={isLoading}\n >\n {children}\n </div>\n\n {/* REMOVED: Loading overlay - only card skeletons should show during loading */}\n </div>\n )\n}\n\n/**\n * Hook for managing pagination loading states\n */\nexport function usePaginationLoading(\n isLoading: boolean,\n currentPage: number,\n totalPages: number\n) {\n const shouldShowPagination = totalPages > 1 || isLoading\n \n const paginationProps = {\n 'aria-busy': isLoading,\n 'data-loading': isLoading,\n 'data-current-page': currentPage,\n 'data-total-pages': totalPages,\n }\n\n const getLoadingMessage = () => {\n if (isLoading) {\n return `Loading page ${currentPage} of ${totalPages}`\n }\n return `Page ${currentPage} of ${totalPages}`\n }\n\n return {\n shouldShowPagination,\n paginationProps,\n getLoadingMessage,\n }\n}\n\n/**\n * Enhanced pagination component that includes loading states\n */\ninterface PersistentPaginationWrapperProps {\n isLoading: boolean\n currentPage: number\n totalPages: number\n onPageChange?: (page: number) => void\n className?: string\n variant?: 'vendor' | 'blog'\n}\n\nexport function PersistentPaginationWrapper({\n isLoading,\n currentPage,\n totalPages,\n onPageChange,\n className,\n variant = 'vendor'\n}: PersistentPaginationWrapperProps) {\n const { getLoadingMessage } = usePaginationLoading(\n isLoading,\n currentPage,\n totalPages\n )\n\n // ALWAYS show pagination for predictable layout\n // For no results (totalPages = 0), show grayed out pagination\n const hasResults = totalPages > 0\n const displayTotalPages = hasResults ? totalPages : 1\n const displayCurrentPage = hasResults ? currentPage : 1\n // Only disable during loading, but still show when no results (just grayed out)\n const isPaginationDisabled = isLoading\n const hasNoResults = !hasResults && !isLoading\n\n // Use UnifiedPagination directly from ui-kit instead of importing from consuming apps\n // Both Pagination and BlogPagination in multi-platform-hub are just wrappers around UnifiedPagination\n const PaginationComponent = require('./unified-pagination').UnifiedPagination\n\n return (\n <div \n className={cn(\n \"relative transition-all ease-in-out flex justify-center items-center\",\n (isPaginationDisabled || hasNoResults) && \"pointer-events-none\",\n className\n )}\n style={{\n opacity: isPaginationDisabled ? 0.3 : hasNoResults ? 0.5 : 1,\n transitionDuration: \"300ms\"\n }}\n role=\"navigation\"\n aria-label=\"Pagination\"\n aria-busy={isLoading}\n data-loading={isLoading}\n data-has-results={hasResults}\n >\n {/* Loading/no results state announcement for screen readers */}\n {(isLoading || !hasResults) && (\n <div \n className=\"sr-only\" \n role=\"status\" \n aria-live=\"polite\"\n >\n {isLoading \n ? `Pagination temporarily disabled while loading page ${displayCurrentPage} of ${displayTotalPages}`\n : `No results available - pagination disabled`\n }\n </div>\n )}\n\n {/* Pagination controls with disabled state */}\n <div \n className={cn(\n \"relative transition-all ease-in-out\",\n isPaginationDisabled && \"cursor-not-allowed\"\n )}\n style={{\n transitionDuration: \"300ms\"\n }}\n aria-hidden={isPaginationDisabled}\n >\n <PaginationComponent\n currentPage={displayCurrentPage}\n totalPages={displayTotalPages}\n onPageChange={hasResults ? onPageChange : () => {}} // Provide empty function instead of undefined\n />\n </div>\n\n {/* REMOVED: Loading overlays - only card skeletons should show during loading */}\n </div>\n )\n} ","'use client'\n\n/**\n * `<ProductReleasesView />` — the shared, SELF-CONTAINED releases LIST surface.\n *\n * The host configures only two things: the **api route** (`endpoint`, default\n * `/api/releases`) and the **internal page route** for a release's detail page\n * (via `runtime.composeContentUrl` / `basePath`). Everything else — reading the\n * search / status / page URL params the section chrome writes, fetching the\n * page, pagination, empty / error / loading states, card composition + nav —\n * happens INSIDE the lib. Mirrors the `DeliveryLists` internal-fetch pattern\n * (plain `fetch` + `useEffect`/`useState`, no react-query dependency).\n *\n * SSR: pass `initialData` to hydrate the first page without a client round-trip\n * (the hub can server-fetch); embedders omit it and the view fetches on mount.\n *\n * Card props: `buildCardProps` defaults to the lib's RICH `buildProductReleaseCardProps`\n * (full lg metadata — Type / Status / author / changelog counts); pass your own to customize.\n */\n\nimport * as React from 'react'\n\nimport { useRouter, useSearchParams, usePathname } from '../../../embed-shims'\nimport { useSelfFetch } from '../../../hooks/use-self-fetch'\nimport { useChatRuntime } from '../../../contexts/chat-runtime-context'\nimport type { ProductRelease, ProductReleaseListResponse } from '../../../types/product-release'\nimport { cn } from '../../../utils/cn'\nimport { resolveContentHref } from '../../../utils/content-href'\nimport { isModifierClick } from '../../chat/utils/chat-nav-resolution'\nimport { executeNavigationImperative } from '../../chat/utils/execute-navigation'\nimport { useEntityCardLink } from '../../chat/entity-cards/use-entity-card-link'\nimport { buildProductReleaseCardProps } from '../../chat/entity-cards/product-release-card-defaults'\nimport { EmptyState } from '../../empty-state'\nimport { LoadError } from '../../ui/error-state'\nimport { PersistentPaginationWrapper } from '../../persistent-pagination'\nimport { ProductReleaseCard, type ProductReleaseCardProps } from './product-release-card'\nimport { ProductReleaseCardSkeleton } from './product-release-card-skeleton'\nimport { DEV_SECTION_PARAM_KEYS } from '../../../utils/dev-sections/dev-section-param-keys'\n\nconst DEFAULT_ENDPOINT = '/api/releases'\n// Param keys sourced from the shared registry (see RoadmapView) — single source for the\n// chrome's written `?key=` and this view's read.\nconst DEFAULT_SEARCH_PARAM_KEY = DEV_SECTION_PARAM_KEYS.search\nconst DEFAULT_STATUS_PARAM_KEY = DEV_SECTION_PARAM_KEYS.releaseStatus\nconst DEFAULT_PAGE_PARAM_KEY = 'page'\n\n/** The `<ProductReleaseCard>` props the caller derives per release — the card's\n * own data fields minus the ones this view supplies (title/summary/version/\n * size/nav). `formattedDate` stays required so the spread satisfies the card. */\nexport type ProductReleaseCardExtras = Omit<\n ProductReleaseCardProps,\n 'title' | 'summary' | 'version' | 'size' | 'anchorProps' | 'onClick' | 'className'\n>\n\nexport interface ProductReleasesViewProps {\n /** GET endpoint for the releases list (the api route). The view appends\n * `?limit&offset&<search>&<release_status>`. Default `/api/releases`. */\n endpoint?: string\n /** Optional SSR hydrate for the first page — skips the initial client fetch. */\n initialData?: ProductReleaseListResponse\n /** Page size → fixed slot count + offset math. Default 5. */\n itemsPerPage?: number\n /** Fallback detail-href prefix when `runtime.composeContentUrl` is not wired\n * (single-platform embedders). Default `/releases`. */\n basePath?: string\n /** Derive the per-card prop bundle. Defaults to the lib's RICH lg builder\n * (`buildProductReleaseCardProps` — full Type/Status/author/changelog metadata)\n * so embedders get the complete card with zero config; pass your own to customize. */\n buildCardProps?: (release: ProductRelease) => ProductReleaseCardExtras\n /** URL param key for the search input. MUST match the chrome that writes it.\n * Default `'search'` (also the outbound query-param name). */\n searchParamKey?: string\n /** URL param key for the status filter. Default `'release_status'`. */\n statusParamKey?: string\n /** URL param key for the page number. Default `'page'`. */\n pageParamKey?: string\n className?: string\n}\n\n/**\n * Per-row child — owns the per-release hooks (`useChatRuntime` +\n * `useEntityCardLink` + `useRouter`) so they run at a component top level, NOT\n * inside the parent `.map()` (Rules-of-Hooks).\n */\nfunction ReleaseRow({\n release,\n basePath,\n buildCardProps,\n}: {\n release: ProductRelease\n basePath: string\n buildCardProps: (release: ProductRelease) => ProductReleaseCardExtras\n}) {\n const runtime = useChatRuntime()\n const router = useRouter()\n // Pass the HYDRATED junction (`product_release_platforms` — the release DAL\n // flattens each platform's `name` onto it), like the blog / case-study /\n // interview grids. The composer reads `name` → resolves to the CURRENT\n // platform when the release belongs to it → relative same-tab href.\n const cta = resolveContentHref(runtime?.composeContentUrl, {\n type: 'product_release',\n slug: release.slug,\n basePath,\n platforms: release.product_release_platforms,\n })\n const { target, rel } = useEntityCardLink({ href: cta.href, targetPlatform: cta.targetPlatform })\n\n const onClick = (e: React.MouseEvent<HTMLAnchorElement>) => {\n // Let the browser handle modifier / middle / new-tab clicks (shared\n // `isModifierClick` rule). Otherwise soft same-origin nav: the host's\n // `navigate` (hub docNav) if wired, else the registered embed-shims router.\n if (e.defaultPrevented || isModifierClick(e) || target === '_blank') return\n e.preventDefault()\n executeNavigationImperative({\n runtime,\n href: cta.href,\n targetPlatform: cta.targetPlatform,\n fallbackNavigate: router.push,\n })\n }\n\n return (\n <ProductReleaseCard\n size=\"lg\"\n title={release.title}\n summary={release.summary}\n version={release.version}\n {...buildCardProps(release)}\n anchorProps={{ href: cta.href, target, rel, onClick }}\n />\n )\n}\n\nexport function ProductReleasesView({\n endpoint = DEFAULT_ENDPOINT,\n initialData,\n itemsPerPage = 5,\n basePath = '/releases',\n buildCardProps = buildProductReleaseCardProps,\n searchParamKey = DEFAULT_SEARCH_PARAM_KEY,\n statusParamKey = DEFAULT_STATUS_PARAM_KEY,\n pageParamKey = DEFAULT_PAGE_PARAM_KEY,\n className,\n}: ProductReleasesViewProps = {}) {\n const searchParams = useSearchParams()\n const router = useRouter()\n const pathname = usePathname()\n\n // Filter / page state from the URL (written by the section chrome above).\n const search = searchParams.get(searchParamKey) || ''\n const status = searchParams.get(statusParamKey) || 'all'\n const currentPage = Math.max(1, parseInt(searchParams.get(pageParamKey) || '1', 10) || 1)\n const offset = (currentPage - 1) * itemsPerPage\n\n // Fold every query param into the url so it IS the fetch key.\n const listParams = new URLSearchParams({ limit: String(itemsPerPage), offset: String(offset) })\n if (search) listParams.set(searchParamKey, search)\n if (status && status !== 'all') listParams.set(statusParamKey, status)\n const { data, isLoading, error, reload } = useSelfFetch<ProductReleaseListResponse>(\n `${endpoint}?${listParams.toString()}`,\n { initialData },\n )\n\n const releases = data?.data ?? []\n const totalCount = data?.count ?? 0\n const totalPages = Math.ceil(totalCount / itemsPerPage)\n const hasActiveFilters = search !== '' || status !== 'all'\n const showEmpty = !isLoading && !error && releases.length === 0\n\n const goToPage = (page: number) => {\n const params = new URLSearchParams(searchParams.toString())\n params.set(pageParamKey, String(page))\n router.replace(`${pathname}?${params.toString()}`, { scroll: false })\n }\n const resetFilters = () => {\n const params = new URLSearchParams(searchParams.toString())\n params.delete(searchParamKey)\n params.delete(statusParamKey)\n params.delete(pageParamKey)\n router.replace(`${pathname}?${params.toString()}`, { scroll: false })\n }\n\n if (error) {\n return (\n <div className={cn('w-full', className)}>\n <LoadError message=\"Failed to load releases.\" onRetry={reload} />\n </div>\n )\n }\n\n return (\n <div className={cn('w-full flex flex-col gap-[40px]', className)}>\n <div className=\"min-h-[600px]\">\n {showEmpty ? (\n <div className=\"h-[600px] flex items-center justify-center\">\n {hasActiveFilters ? (\n <EmptyState\n type=\"search\"\n title=\"No releases found\"\n description=\"No releases match your current filters. Try adjusting your search or status filter.\"\n showCTA\n ctaText=\"Reset Filters\"\n onCtaClick={resetFilters}\n />\n ) : (\n <EmptyState\n type=\"generic\"\n title=\"No releases available\"\n description=\"Check back soon for product updates!\"\n showCTA={false}\n />\n )}\n </div>\n ) : (\n <>\n {/* ALWAYS render `itemsPerPage` slots — visibility toggles so the\n list height is stable across loading ↔ loaded. */}\n <div className=\"flex flex-col gap-6\">\n {Array.from({ length: itemsPerPage }).map((_, i) => {\n const release = releases[i]\n const hasData = !!release\n return (\n <div\n key={release?.id ?? `slot-${i}`}\n style={{ visibility: isLoading || hasData ? 'visible' : 'hidden' }}\n >\n {isLoading ? (\n <ProductReleaseCardSkeleton size=\"lg\" />\n ) : release ? (\n <ReleaseRow release={release} basePath={basePath} buildCardProps={buildCardProps} />\n ) : (\n <ProductReleaseCardSkeleton size=\"lg\" />\n )}\n </div>\n )\n })}\n </div>\n\n {/* Pagination — always present at the bottom for consistent spacing. */}\n <div className=\"mt-6 md:mt-8 flex justify-center\">\n {isLoading ? (\n <div className=\"h-12 m-3 w-64\" />\n ) : releases.length > 0 && totalPages > 1 ? (\n <PersistentPaginationWrapper\n isLoading={false}\n currentPage={currentPage}\n totalPages={totalPages}\n onPageChange={goToPage}\n variant=\"blog\"\n />\n ) : (\n <div className=\"h-12 m-3 w-64\" style={{ visibility: 'hidden' }} />\n )}\n </div>\n </>\n )}\n </div>\n </div>\n )\n}\n","\"use client\";\n\nimport React, { useState } from 'react';\nimport { Video } from '../features/video';\nimport { ImageGalleryModal } from '../ui/image-gallery-modal';\n\nexport interface MediaGalleryStripItem {\n media_type: string;\n media_url: string;\n title?: string | null;\n /** Stable key when present (e.g. release_media rows); falls back to index. */\n id?: string;\n}\n\n/** Still-images open the lightbox; clips ('video'/'demo') play inline. */\nfunction isGalleryImage(mediaType: string): boolean {\n return mediaType !== 'video' && mediaType !== 'demo';\n}\n\nexport interface MediaGalleryStripProps {\n items: MediaGalleryStripItem[];\n /** Optional cap on tiles shown (e.g. product-release shows 5). Default: all. */\n maxDisplay?: number;\n className?: string;\n}\n\n/**\n * Read-only media gallery — a horizontal-scroll strip of 240×200 tiles where\n * still-images open an {@link ImageGalleryModal} lightbox and video clips play\n * inline via {@link Video}. The lightbox index is the tile's rank among the\n * IMAGE-only items (clips are skipped), so the modal opens on the correct image.\n *\n * Single source of truth for the detail-page media strip — replaces the markup\n * that was hand-rolled inline in the product-release and \"What I Shipped\" detail\n * pages (the release copy also had a raw-index lightbox bug this component fixes).\n */\nexport function MediaGalleryStrip({ items, maxDisplay, className }: MediaGalleryStripProps) {\n const [galleryOpen, setGalleryOpen] = useState(false);\n const [galleryIndex, setGalleryIndex] = useState(0);\n\n if (!items || items.length === 0) return null;\n\n // Explicit numeric check (clamped): `maxDisplay={0}` means \"show none\", which a\n // truthy check would wrongly treat as \"no cap → show all\".\n const display = typeof maxDisplay === 'number' ? items.slice(0, Math.max(0, maxDisplay)) : items;\n const galleryImages = display.filter((m) => isGalleryImage(m.media_type)).map((m) => m.media_url);\n\n const tileClass =\n 'shrink-0 w-[240px] h-[200px] rounded-md overflow-hidden border border-ods-border bg-black transition-opacity';\n\n return (\n <>\n <div className={`flex gap-6 overflow-x-auto w-full ${className ?? ''}`}>\n {display.map((mediaItem, index) => {\n // Image tiles open the lightbox, so they're real <button>s — keyboard\n // focusable + Enter/Space activatable. Clips render in <Video>, which\n // owns its own controls, so their tile stays a plain container.\n if (isGalleryImage(mediaItem.media_type)) {\n return (\n <button\n key={mediaItem.id || index}\n type=\"button\"\n className={`${tileClass} cursor-pointer hover:opacity-80`}\n aria-label={`Open ${mediaItem.title || `media ${index + 1}`} in gallery`}\n onClick={() => {\n // Lightbox position = rank among image-only items (clips skipped).\n setGalleryIndex(display.slice(0, index).filter((m) => isGalleryImage(m.media_type)).length);\n setGalleryOpen(true);\n }}\n >\n <img src={mediaItem.media_url} alt={mediaItem.title || `Media ${index + 1}`} className=\"w-full h-full object-cover\" />\n </button>\n );\n }\n return (\n <div key={mediaItem.id || index} className={tileClass}>\n <Video url={mediaItem.media_url} layout=\"native\" />\n </div>\n );\n })}\n </div>\n\n {galleryImages.length > 0 && (\n <ImageGalleryModal\n images={galleryImages}\n isOpen={galleryOpen}\n onClose={() => setGalleryOpen(false)}\n initialIndex={galleryIndex}\n />\n )}\n </>\n );\n}\n","\"use client\";\n\nimport { useState, useEffect, ComponentType, type ReactNode } from 'react';\nimport Link from '../../../embed-shims/next-link';\nimport { useRouter } from '../../../embed-shims/next-navigation';\nimport { Card, CardContent } from '../../ui/card';\n// PageShell (wide) — match the related-content/FAQ rail container the hub\n// renders below this view (was ArticleDetailLayout, 1280px — narrower than\n// the rail, see hub detail-container alignment decision 2026-06-10).\nimport { PageShell } from '../../layout/article-detail-layout';\nimport { PageLayout } from '../../layout/page-layout';\nimport { ReleaseChangelogSection } from '../../ui/release-changelog-section';\nimport { RichMarkdownRenderer } from '../../ui/rich-markdown-renderer';\nimport { EntityTagBadges } from '../../features/entity-tag-badges';\nimport { EntityMetadataAuthorCell } from '../../chat/entity-cards/entity-author-card';\nimport type { EntityAuthor } from '../../../types/entity-author';\nimport { MediaGalleryStrip } from '../media-gallery-strip';\nimport { GitHubIcon } from '../../icons/github-icon';\nimport { AlertTriangle, ExternalLink, BookMarked, Sparkles, TrendingUp, Wrench } from 'lucide-react';\nimport { formatReleaseDate } from '../../../utils/date-formatters';\nimport { contentFetch } from '../../../utils/embed-content-fetch';\nimport { Video } from '../../features/video';\nimport { DetailPageSkeleton } from '../detail-page-skeleton';\nimport type { ChangelogEntry } from '../../../types/product-release';\nimport type { TagAssoc } from '../../../types/blog';\nimport type { VideoTeaser } from '../../../types/video-processing';\n\n// Types for injectable components\nexport interface MarkdownRendererProps {\n content: string;\n}\n\n// Canonical RoadmapItem shape lives in chat entity types — see\n// `src/components/chat/types/entities/roadmap-item.ts`. The product-release\n// detail page previously declared a structural placeholder\n// (`{ id; [k: string]: unknown }`) that conflicted with the canonical\n// shape once the entities barrel was added; re-exporting the canonical\n// type fixes the collision while keeping the same import path for\n// downstream consumers of `./release-detail-page`.\nimport type { RoadmapItem } from '../../chat/types/entities/roadmap-item';\nimport type { DeliveryResponse } from '../../../types/delivery';\n// Re-export both types for source-compat with consumers importing\n// through this module. Canonical sources:\n// - RoadmapItem → `../../chat/types/entities/roadmap-item`\n// - DeliveryResponse → `../../../types/delivery` (single source of\n// truth, shared with the lib `<DeliveryLists>` / `<DeliveryTable>`\n// components and the new types barrel).\nexport type { RoadmapItem, DeliveryResponse };\n\nexport interface RoadmapSectionProps {\n items: RoadmapItem[];\n isLoading: boolean;\n onItemUpdate?: (item: RoadmapItem) => void;\n}\n\nexport interface DeliverySectionProps {\n data: DeliveryResponse | null;\n isLoading: boolean;\n}\n\nexport interface VideoSectionProps {\n bites: VideoTeaser[];\n title?: string;\n filterPublished?: boolean;\n}\n\n// Type for the useRelease hook result\nexport interface UseReleaseResult {\n data: unknown;\n error: Error | null;\n isLoading: boolean;\n}\n\nexport interface VideoDisplaySectionProps {\n mainVideoUrl?: string | null;\n youtubeUrl?: string | null;\n highlightVideoUrl?: string | null;\n highlightVideoThumbnail?: string | null;\n mainVideoPoster?: string | null;\n title?: string;\n videoSummary?: string | null;\n videoBites?: VideoTeaser[];\n bitesTitle?: string;\n filterPublishedBites?: boolean;\n srtContent?: string | null;\n captionsUrl?: string | null;\n}\n\nexport interface ReleaseDetailPageProps {\n slug: string;\n initialData?: unknown; // Optional pre-fetched data for admin preview\n // Required: Hook for fetching release data (must be from app-level to use correct QueryClient)\n useRelease: (slug: string | undefined) => UseReleaseResult;\n // Injectable components for app-specific rendering\n MarkdownRenderer?: ComponentType<MarkdownRendererProps>;\n RoadmapSection?: ComponentType<RoadmapSectionProps>;\n DeliverySection?: ComponentType<DeliverySectionProps>;\n VideoSection?: ComponentType<VideoSectionProps>;\n /** Injectable video display section with tabs for full/highlight video + summary + bites */\n VideoDisplaySection?: ComponentType<VideoDisplaySectionProps>;\n // API endpoints for fetching linked tasks\n roadmapApiEndpoint?: string;\n deliveryApiEndpoint?: string;\n /** Back-button config — same pattern as `DevSectionPage` /\n * `LegalDocumentPage`. Pass `false` to hide. Default\n * `{ label: 'Back to home', href: '/' }`. */\n backButton?: { label?: string; href?: string } | false;\n /** Link target for the author name in the metadata grid — the host\n * computes it (public author page; absent ⇒ plain text). */\n authorHref?: string;\n /** Optional slot rendered inside the page chrome, BELOW the article body —\n * e.g. the hub's end-of-article author byline + related-content / FAQ rail.\n * Lets the hub mount this page directly (no local wrapper component) while\n * embedders that don't have those extras simply omit it. */\n relatedContent?: ReactNode;\n /** Render the standalone `<PageShell>`. Default true. Pass false when the host\n * layout already provides the page container — only the padding box renders,\n * avoiding a nested `<main>`. */\n shell?: boolean;\n}\n\n// Default renderer = the lib's `RichMarkdownRenderer` so out-of-the-box\n// release pages get full rich-link previews, embedded media, social cards,\n// etc. Hosts that want a different rendering (or a Supabase-aware preset)\n// override via the `MarkdownRenderer` prop.\nconst DefaultMarkdownRenderer = RichMarkdownRenderer;\n\nexport function ReleaseDetailPage({\n authorHref,\n slug,\n initialData,\n useRelease,\n MarkdownRenderer = DefaultMarkdownRenderer,\n RoadmapSection,\n DeliverySection,\n VideoSection,\n VideoDisplaySection,\n roadmapApiEndpoint = '/api/roadmap',\n deliveryApiEndpoint = '/api/delivery',\n backButton,\n relatedContent,\n shell = true\n}: ReleaseDetailPageProps) {\n const router = useRouter();\n // `shell` true → standalone `<PageShell>`; false → padding-only box (no nested\n // <main>) for hosts whose layout already provides the container.\n const renderShell = (node: ReactNode) =>\n shell ? <PageShell>{node}</PageShell> : <div className=\"page-shell-content\">{node}</div>;\n // Use pre-fetched data if provided (admin preview), otherwise fetch via hook (public)\n const { data: fetchedRelease, error, isLoading } = useRelease(initialData ? undefined : slug);\n const release = (initialData || fetchedRelease) as Record<string, unknown> | undefined;\n\n // Back-button config — mirrors DevSectionPage / LegalDocumentPage.\n // Default: { label: 'Back to home', href: '/' }. Pass `false` to hide\n // (e.g. embed-mode where the host owns navigation chrome).\n // Narrowing note: `backButton &&` already eliminates the `false` branch,\n // so the inner expressions are typed as `{ label?, href? } | undefined`.\n // Don't re-compare to `false` here — tsc TS2367s on the dead branch.\n const showBackButton = backButton !== false;\n const backLabel = (backButton ? backButton.label : undefined) ?? 'Back to home';\n const backHref = (backButton ? backButton.href : undefined) ?? '/';\n\n // Fetch roadmap and delivery tasks if linked to this release\n const [roadmapTasks, setRoadmapTasks] = useState<RoadmapItem[]>([]);\n const [deliveryData, setDeliveryData] = useState<DeliveryResponse | null>(null);\n const [roadmapLoading, setRoadmapLoading] = useState(false);\n const [deliveryLoading, setDeliveryLoading] = useState(false);\n\n useEffect(() => {\n async function fetchLinkedTasks() {\n if (!release) return;\n\n try {\n // Fetch roadmap tasks if linked\n const roadmapTasksData = release.clickup_roadmap_tasks as Array<{ clickup_task_id: string }> | undefined;\n if (roadmapTasksData && roadmapTasksData.length > 0 && RoadmapSection) {\n setRoadmapLoading(true);\n const roadmapIds = roadmapTasksData.map(t => t.clickup_task_id).join(',');\n const roadmapResponse = await contentFetch(`${roadmapApiEndpoint}?task_ids=${roadmapIds}`);\n const roadmapData = await roadmapResponse.json();\n setRoadmapTasks(roadmapData.items || []);\n setRoadmapLoading(false);\n }\n\n // Fetch delivery tasks if linked\n const deliveryTasksData = release.clickup_delivery_tasks as Array<{ clickup_task_id: string }> | undefined;\n if (deliveryTasksData && deliveryTasksData.length > 0 && DeliverySection) {\n setDeliveryLoading(true);\n const deliveryIds = deliveryTasksData.map(t => t.clickup_task_id).join(',');\n const deliveryResponse = await contentFetch(`${deliveryApiEndpoint}?task_ids=${deliveryIds}`);\n const deliveryResponseData = await deliveryResponse.json();\n setDeliveryData(deliveryResponseData);\n setDeliveryLoading(false);\n }\n } catch (err) {\n console.error('Error fetching linked tasks:', err);\n setRoadmapLoading(false);\n setDeliveryLoading(false);\n }\n }\n\n fetchLinkedTasks();\n }, [release, RoadmapSection, DeliverySection, roadmapApiEndpoint, deliveryApiEndpoint]);\n\n // Don't show loading skeleton if we have initialData\n if (!initialData && isLoading) {\n // `bare` + `PageShell` so the loading state matches the loaded page's full\n // width / padding / min-height (the wrapper supplies the page chrome).\n return renderShell(\n // Match the loaded page's top offset (TitleBlock's\n // `pt-[var(--spacing-system-l)]`) so content doesn't jump on load.\n <div className=\"pt-[var(--spacing-system-l)]\">\n <DetailPageSkeleton bare metadataColumns={4} showImageGallery={true} />\n </div>\n );\n }\n\n if (error || !release) {\n return renderShell(\n <div className=\"text-center py-16\">\n <h1 className=\"text-4xl font-bold text-ods-text-primary mb-4\">Release Not Found</h1>\n <p className=\"text-xl text-ods-text-secondary\">The release you're looking for doesn't exist.</p>\n </div>\n );\n }\n\n const hasBreakingChanges = Array.isArray(release.breaking_changes) && release.breaking_changes.length > 0;\n\n // Type assertions for release data\n const releaseTitle = release.title as string;\n const releaseVersion = release.version as string;\n const releaseSummary = release.summary as string | null;\n const releaseContent = release.content as string | null;\n const releaseDate = release.release_date as string;\n const releaseType = release.release_type as string;\n const releaseStatus = release.release_status as string;\n const releaseMedia = release.release_media as Array<{ id?: string; media_type: string; media_url: string; title?: string }> | undefined;\n // Field-cast per this file's loose-release idiom (release_type etc. above)\n // — but to the SHARED EntityAuthor, never an inline shadow author shape.\n const author = release.author as EntityAuthor | undefined;\n const githubReleases = release.github_releases as Array<{ id: string; github_release_url: string }> | undefined;\n const knowledgeBaseLinks = release.knowledge_base_links as Array<{ id?: string; kb_article_path: string }> | string[] | undefined;\n const migrationGuideUrl = release.migration_guide_url as string | undefined;\n const documentationUrl = release.documentation_url as string | undefined;\n const youtubeUrl = release.youtube_url as string | undefined;\n const mainVideoUrl = release.main_video_url as string | undefined;\n const videoBites = release.video_bites as VideoTeaser[] | undefined;\n const highlightVideoUrl = release.highlight_video_url as string | undefined;\n const highlightVideoThumbnail = release.highlight_video_thumbnail as string | undefined;\n const breakingChanges = release.breaking_changes as ChangelogEntry[] | undefined;\n const featuresAdded = release.features_added as ChangelogEntry[] | undefined;\n const bugFixed = release.bugs_fixed as ChangelogEntry[] | undefined;\n const improvements = release.improvements as ChangelogEntry[] | undefined;\n\n return renderShell(\n <PageLayout\n title={releaseTitle}\n subtitle={`Version: ${releaseVersion}`}\n titleSize=\"h1\"\n backButton={\n showBackButton ? { label: backLabel, onClick: () => router.push(backHref) } : undefined\n }\n >\n <div className=\"space-y-6 md:space-y-8\">\n {/* Tags — flat product_release_tags[] from entity_tags */}\n <EntityTagBadges tags={release.product_release_tags as TagAssoc[] | undefined} />\n\n {/* Metadata Grid */}\n <div className=\"grid grid-cols-1 md:grid-cols-4 border border-ods-border rounded-md overflow-hidden w-full\">\n {/* Release Type */}\n <div className=\"bg-ods-card border-b md:border-b-0 md:border-r border-ods-border p-4 flex flex-col gap-3\">\n <div className=\"flex flex-col gap-0\">\n <p className=\"text-h4 text-ods-text-primary\">\n {releaseType.toLocaleUpperCase()}\n </p>\n <p className=\"font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary\">\n Release Type\n </p>\n </div>\n </div>\n\n {/* Release Status */}\n <div className=\"bg-ods-card border-b md:border-b-0 md:border-r border-ods-border p-4 flex flex-col gap-3\">\n <div className=\"flex flex-col gap-0\">\n <p className=\"text-h4 text-ods-text-primary\">\n {releaseStatus.toLocaleUpperCase()}\n </p>\n <p className=\"font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary\">\n Release Status\n </p>\n </div>\n </div>\n\n {/* Release Date */}\n <div className=\"bg-ods-card border-b md:border-b-0 md:border-r border-ods-border p-4 flex flex-col gap-3\">\n <div className=\"flex flex-col gap-0\">\n <p className=\"text-h4 text-ods-text-primary\">\n {formatReleaseDate(releaseDate)}\n </p>\n <p className=\"font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary\">\n Release Date\n </p>\n </div>\n </div>\n\n {/* Author — the shared metadata-grid author cell (it was extracted\n FROM this page; rendering the export instead of an inline copy\n keeps the two in lockstep). The stub author preserves the legacy\n \"Unknown Author\" rendering for author-less releases; the\n job_title-over-roleLabel rule matches the guide detail page. */}\n <EntityMetadataAuthorCell\n author={author ?? { full_name: null, avatar_url: null }}\n authorHref={author?.full_name ? authorHref : undefined}\n />\n </div>\n\n {/* Image gallery — shared strip + lightbox (images) / inline clips. */}\n <MediaGalleryStrip items={releaseMedia ?? []} maxDisplay={5} />\n\n {/* Summary */}\n {releaseSummary && (\n <div className=\"text-h4 text-ods-text-primary\">\n <p>{releaseSummary}</p>\n </div>\n )}\n\n {/* Video Display Section - Injectable or fallback */}\n {VideoDisplaySection ? (\n <VideoDisplaySection\n mainVideoUrl={mainVideoUrl}\n youtubeUrl={youtubeUrl}\n highlightVideoUrl={highlightVideoUrl}\n highlightVideoThumbnail={highlightVideoThumbnail}\n title={releaseTitle}\n videoBites={videoBites}\n bitesTitle=\"Video Clips\"\n filterPublishedBites={true}\n srtContent={release?.srt_content as string | null | undefined}\n captionsUrl={release?.captionsUrl as string | undefined}\n />\n ) : (\n <>\n {/*\n Fallback when no `VideoDisplaySection` is injected. `<Video>` is the\n SSoT for every video surface — single source of truth across YouTube,\n HLS, and MP4 paths.\n */}\n {youtubeUrl && (\n <Video\n kind=\"youtube\"\n url={youtubeUrl}\n title={`${releaseTitle} - Video`}\n layout=\"native\"\n />\n )}\n {!youtubeUrl && mainVideoUrl && (\n <Video\n url={mainVideoUrl}\n srtContent={release?.srt_content as string | undefined}\n captionsUrl={release?.captionsUrl as string | undefined}\n layout=\"centered\"\n />\n )}\n {highlightVideoUrl && (\n <Video\n url={highlightVideoUrl}\n poster={highlightVideoThumbnail}\n layout=\"centered\"\n />\n )}\n </>\n )}\n\n {/* Content */}\n {releaseContent && (\n <div className=\"text-h4 text-ods-text-primary\">\n <MarkdownRenderer content={releaseContent} />\n </div>\n )}\n\n {/* Breaking Changes Warning */}\n {hasBreakingChanges && (\n <Card className=\"border-red-500 bg-red-500/10\">\n <CardContent className=\"p-6\">\n <div className=\"flex items-center gap-3\">\n <AlertTriangle className=\"h-6 w-6 text-red-500\" />\n <div>\n <h3 className=\"font-bold text-red-500 text-lg\">Breaking Changes</h3>\n <p className=\"text-ods-text-secondary\">This release contains breaking changes. Review carefully before upgrading.</p>\n </div>\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* Changelog Sections — icons match the catalog card's changelog\n strip taxonomy (Sparkles/Wrench/TrendingUp/AlertTriangle) so the\n user sees a consistent visual signature across catalog → detail. */}\n <ReleaseChangelogSection\n title=\"Breaking Changes\"\n entries={breakingChanges || []}\n isBreaking\n hideTitle\n icon={<AlertTriangle className=\"h-6 w-6\" />}\n MarkdownRenderer={MarkdownRenderer}\n />\n {/* Features / Bugs / Improvements use `previewFirst` — same\n progressive-disclosure pattern as the investor-update detail\n page's Key Highlights / Financial Notes sections. Shows the\n first entry in full + fade-masks the rest, with a \"Show N\n more / Show less\" toggle. Breaking Changes (above) stays\n fully expanded — it's critical info, not skim-friendly. */}\n <ReleaseChangelogSection\n title=\"Features Added\"\n entries={featuresAdded || []}\n icon={<Sparkles className=\"h-6 w-6\" />}\n previewFirst\n MarkdownRenderer={MarkdownRenderer}\n />\n <ReleaseChangelogSection\n title=\"Bugs Fixed\"\n entries={bugFixed || []}\n icon={<Wrench className=\"h-6 w-6\" />}\n previewFirst\n MarkdownRenderer={MarkdownRenderer}\n />\n <ReleaseChangelogSection\n title=\"Improvements\"\n entries={improvements || []}\n icon={<TrendingUp className=\"h-6 w-6\" />}\n previewFirst\n MarkdownRenderer={MarkdownRenderer}\n />\n\n {/* Video Bites Section - Only when VideoDisplaySection is not handling it */}\n {!VideoDisplaySection && VideoSection && videoBites && videoBites.length > 0 && (\n <VideoSection\n bites={videoBites}\n title=\"Video Clips\"\n filterPublished={true}\n />\n )}\n\n {/* Related Roadmap Items */}\n {RoadmapSection && (roadmapLoading || roadmapTasks.length > 0) && (\n <div className=\"space-y-4 w-full\">\n <p className=\"text-h5 tracking-[-0.28px] text-ods-text-secondary\">\n Related Roadmap Items\n </p>\n <RoadmapSection\n items={roadmapTasks}\n isLoading={roadmapLoading}\n onItemUpdate={(updatedItem) => {\n setRoadmapTasks(prevTasks =>\n prevTasks.map(task =>\n task.id === updatedItem.id ? updatedItem : task\n )\n );\n }}\n />\n </div>\n )}\n\n {/* Bug-fixes & Enhancements Section */}\n {DeliverySection && (deliveryLoading || (deliveryData && (deliveryData.completed.length > 0 || deliveryData.inProgress.length > 0))) && (\n <div className=\"w-full space-y-4\">\n <p className=\"text-h5 tracking-[-0.28px] text-ods-text-secondary\">\n Related Enhancements and Bug-fixes\n </p>\n <DeliverySection\n data={deliveryData}\n isLoading={deliveryLoading}\n />\n </div>\n )}\n\n {/* Related Links */}\n {(githubReleases?.length || knowledgeBaseLinks?.length || migrationGuideUrl || documentationUrl) && (\n <div className=\"space-y-1 w-full\">\n <p className=\"text-h5 tracking-[-0.28px] text-ods-text-secondary\">\n Related Links\n </p>\n <Card className=\"bg-ods-card border-ods-border p-6\">\n <div className=\"space-y-4\">\n {/* GitHub Releases */}\n {githubReleases && githubReleases.length > 0 && (\n <>\n {githubReleases.map((ghRelease) => (\n <div key={ghRelease.id} className=\"flex items-start gap-1\">\n <GitHubIcon className=\"shrink-0\" width={24} height={24} color=\"var(--color-text-secondary)\" />\n <span className=\"text-h4 text-ods-text-primary\">\n Github Release\n </span>\n <a\n href={ghRelease.github_release_url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-h4 text-[#ffc008] hover:underline\"\n >\n {ghRelease.github_release_url.split('/').pop()}\n </a>\n <ExternalLink className=\"h-6 w-6 text-[#ffc008] shrink-0\" />\n </div>\n ))}\n </>\n )}\n\n {/* Knowledge Base Links */}\n {knowledgeBaseLinks && knowledgeBaseLinks.length > 0 && (\n <>\n {knowledgeBaseLinks.map((linkObj) => {\n const path = typeof linkObj === 'string' ? linkObj : linkObj.kb_article_path;\n const linkId = typeof linkObj === 'string' ? path : linkObj.id || path;\n return (\n <div key={linkId} className=\"flex items-start gap-1\">\n <BookMarked className=\"h-6 w-6 text-ods-text-secondary shrink-0\" />\n <span className=\"text-h4 text-ods-text-primary\">\n Knowledge Base\n </span>\n <Link\n href={path.startsWith('http') ? path : `/knowledge-base${path.startsWith('/') ? '' : '/'}${path}`}\n className=\"text-h4 text-[#ffc008] hover:underline\"\n >\n {path.replace(/^\\//, '').split('/').pop()?.replace(/-/g, ' ') || 'View Article'}\n </Link>\n <ExternalLink className=\"h-6 w-6 text-[#ffc008] shrink-0\" />\n </div>\n );\n })}\n </>\n )}\n\n {/* Migration Guide */}\n {migrationGuideUrl && (\n <div className=\"flex items-start gap-1\">\n <BookMarked className=\"h-6 w-6 text-ods-text-secondary shrink-0\" />\n <a\n href={migrationGuideUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-h4 text-[#ffc008] hover:underline\"\n >\n 📖 Migration Guide\n </a>\n <ExternalLink className=\"h-6 w-6 text-[#ffc008] shrink-0\" />\n </div>\n )}\n\n {/* Documentation */}\n {documentationUrl && (\n <div className=\"flex items-start gap-1\">\n <BookMarked className=\"h-6 w-6 text-ods-text-secondary shrink-0\" />\n <a\n href={documentationUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-h4 text-[#ffc008] hover:underline\"\n >\n 📚 Documentation\n </a>\n <ExternalLink className=\"h-6 w-6 text-[#ffc008] shrink-0\" />\n </div>\n )}\n </div>\n </Card>\n </div>\n )}\n </div>\n\n {/* Host slot — end-of-article byline + related-content / FAQ rail. */}\n {relatedContent}\n </PageLayout>\n );\n}\n","\"use client\";\n\nimport { DetailPageSkeleton } from '../detail-page-skeleton';\n\nexport function ReleaseDetailSkeleton() {\n return <DetailPageSkeleton metadataColumns={4} showImageGallery={true} />;\n}\n","'use client';\n\n/**\n * useRoadmapVoting — localStorage-backed optimistic voting for roadmap cards.\n *\n * One vote per task per user (storage key scoped per `storageKey` option,\n * default `'roadmap_votes_v1'`). Toggling the same vote removes it;\n * switching directions sends a remove + add pair so the server's running\n * totals stay correct.\n *\n * Endpoint configuration — `voteApiEndpoint`:\n * The hook posts to ONE endpoint (default `/api/roadmap/vote`) for\n * BOTH the optimistic add AND the opposite-vote remove. Reverse-proxy\n * embedders override this with their proxied path; lib otherwise\n * matches the hub's pre-migration call shape.\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { contentFetch } from '../../../utils/embed-content-fetch';\n\nexport type VoteType = 'up' | 'down' | null;\n\nexport interface VoteState {\n [taskId: string]: VoteType;\n}\n\nexport interface UseRoadmapVotingOptions {\n /** Vote endpoint URL. Default `/api/roadmap/vote`. */\n voteApiEndpoint?: string;\n /** localStorage key. Default `'roadmap_votes_v1'`. Embedders mounting\n * multiple roadmap surfaces in the same origin can scope per-surface\n * (e.g. `'roadmap_votes_v1_main'` vs `'roadmap_votes_v1_admin'`) so\n * votes don't cross-contaminate. */\n storageKey?: string;\n}\n\nconst DEFAULT_VOTE_ENDPOINT = '/api/roadmap/vote';\nconst DEFAULT_STORAGE_KEY = 'roadmap_votes_v1';\n\nexport function useRoadmapVoting(options: UseRoadmapVotingOptions = {}) {\n const voteApiEndpoint = options.voteApiEndpoint ?? DEFAULT_VOTE_ENDPOINT;\n const storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY;\n\n const [votes, setVotes] = useState<VoteState>({});\n const [isLoading, setIsLoading] = useState(true);\n\n // Load votes from localStorage. Runs on mount AND whenever `storageKey`\n // changes — when the key changes mid-lifecycle (e.g. an embedder\n // remounts with a new namespace), we MUST reset state first so the\n // save-effect below doesn't write the old key's data into the new\n // key. We also re-enter the loading phase so the load completes\n // before any save runs.\n useEffect(() => {\n setIsLoading(true);\n setVotes({});\n try {\n const stored = localStorage.getItem(storageKey);\n if (stored) {\n setVotes(JSON.parse(stored));\n }\n } catch (error) {\n console.error('[Voting] Error loading votes from localStorage:', error);\n } finally {\n setIsLoading(false);\n }\n }, [storageKey]);\n\n // Save votes to localStorage whenever they change\n useEffect(() => {\n if (!isLoading) {\n try {\n localStorage.setItem(storageKey, JSON.stringify(votes));\n } catch (error) {\n console.error('[Voting] Error saving votes to localStorage:', error);\n }\n }\n }, [votes, isLoading, storageKey]);\n\n const getVote = useCallback(\n (taskId: string): VoteType => {\n return votes[taskId] || null;\n },\n [votes]\n );\n\n const toggleVote = useCallback(\n async (\n taskId: string,\n voteType: 'up' | 'down'\n ): Promise<{ success: boolean; newVote: VoteType; action: 'add' | 'remove' }> => {\n const currentVote = votes[taskId];\n\n let newVote: VoteType = null;\n let action: 'add' | 'remove' = 'add';\n\n if (currentVote === voteType) {\n // User clicked same vote - remove it\n newVote = null;\n action = 'remove';\n } else {\n // User clicked different vote - set it. If they had an opposite\n // vote, remove that first so the server totals stay consistent.\n if (currentVote) {\n await contentFetch(voteApiEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n taskId,\n voteType: currentVote,\n action: 'remove',\n }),\n }).catch(err => console.error('[Voting] Error removing opposite vote:', err));\n }\n\n newVote = voteType;\n action = 'add';\n }\n\n // Optimistic update\n setVotes(prev => ({\n ...prev,\n [taskId]: newVote,\n }));\n\n try {\n const response = await contentFetch(voteApiEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ taskId, voteType, action }),\n });\n\n if (!response.ok) {\n throw new Error('Vote API request failed');\n }\n\n return { success: true, newVote, action };\n } catch (error) {\n console.error('[Voting] API error:', error);\n\n // Revert optimistic update on error\n setVotes(prev => ({\n ...prev,\n [taskId]: currentVote,\n }));\n\n return { success: false, newVote: currentVote, action };\n }\n },\n [votes, voteApiEndpoint]\n );\n\n const clearVotes = useCallback(() => {\n setVotes({});\n localStorage.removeItem(storageKey);\n }, [storageKey]);\n\n return {\n votes,\n isLoading,\n getVote,\n toggleVote,\n clearVotes,\n };\n}\n","'use client';\n\n/**\n * RoadmapGrid — the shared roadmap LIST surface.\n *\n * Two modes (one component, one voting state):\n * - `groupByQuarter` (DEFAULT): buckets items by `item.quarter`, sorts the\n * quarters chronologically, and renders each in a collapsible `<Accordion>`\n * — the same quarter grouping the hub's roadmap page has, now shared so\n * every embedder gets it for free. Quarters at/after the current quarter\n * (and within `quartersToKeepClosed` of it) open by default; older ones\n * collapse. When `hasActiveFilters`, all quarters expand.\n * - `groupByQuarter={false}`: a flat 2-col grid (related-content rails that\n * pass a small pre-filtered `items` slice).\n *\n * Voting state (`useRoadmapVoting` + the in-flight set) lives ONCE at this\n * level and is shared across every quarter's grid, so a vote in Q3 and a vote\n * in Q4 can't race separate states. A successful vote triggers a single-task\n * refresh (`buildRefreshUrl`) and patches the parent list via `onItemUpdate`.\n *\n * Hydration: `expandedQuarters` starts `[]` and is populated in a client-only\n * effect (mirrors the hub) so SSR markup matches first client paint.\n */\n\nimport { useEffect, useRef, useState } from 'react';\nimport { RoadmapCard } from '../../chat/entity-cards/roadmap-card';\nimport { useRoadmapVoting, type UseRoadmapVotingOptions } from './use-roadmap-voting';\nimport { EmptyState } from '../../empty-state';\nimport {\n Accordion,\n AccordionItem,\n AccordionTrigger,\n AccordionContent,\n} from '../../ui';\nimport { cn } from '../../../utils/cn';\nimport { devSectionAnchorId } from '../../../utils/dev-sections/dev-section-param-keys';\nimport { contentFetch } from '../../../utils/embed-content-fetch';\nimport type { RoadmapItem } from '../../chat/types/entities/roadmap-item';\n\nconst DEFAULT_BUILD_REFRESH_URL = (taskId: string) => `/api/roadmap/${taskId}`;\nconst BACKLOG = 'Backlog';\n\n// ── Quarter helpers (pure; lifted from the hub roadmap section) ──────────────\n\n/** Status → sort priority (Complete → Working → Review → To-Do → other). */\nfunction getStatusPriority(status: string): number {\n const s = (status || '').toLowerCase();\n if (s.includes('complete') || s.includes('done')) return 1;\n if (s.includes('working') || s.includes('progress')) return 2;\n if (s.includes('review')) return 3;\n if (s.includes('to do') || s.includes('plan')) return 4;\n return 5;\n}\n\n/** Parse a `\"Q<n> <year>\"` label → {quarter, year}; `null` when unparseable. */\nfunction parseQuarterString(q: string): { quarter: number; year: number } | null {\n const match = q.match(/Q(\\d+)\\s+(\\d+)/);\n if (!match) return null;\n return { quarter: parseInt(match[1], 10), year: parseInt(match[2], 10) };\n}\n\nfunction compareQuarters(\n a: { quarter: number; year: number },\n b: { quarter: number; year: number },\n): number {\n if (a.year !== b.year) return a.year - b.year;\n return a.quarter - b.quarter;\n}\n\n/** Today's quarter — client-only (called from an effect, never during SSR). */\nfunction getCurrentQuarter(): { quarter: number; year: number } {\n const now = new Date();\n return { quarter: Math.floor(now.getMonth() / 3) + 1, year: now.getFullYear() };\n}\n\n/** Quarters open by default: current + future, plus the recent past within\n * `quartersToKeepClosed` of the current quarter; Backlog always open. */\nfunction computeDefaultExpandedQuarters(quarters: string[], quartersToKeepClosed: number): string[] {\n const currentQ = getCurrentQuarter();\n const out: string[] = [];\n for (const q of quarters) {\n if (q === BACKLOG) continue;\n const parsed = parseQuarterString(q);\n if (!parsed) continue;\n const diff = compareQuarters(parsed, currentQ);\n if (diff >= 0) {\n out.push(q);\n } else {\n const quartersAgo = currentQ.year * 4 + currentQ.quarter - (parsed.year * 4 + parsed.quarter);\n if (quartersAgo < quartersToKeepClosed) out.push(q);\n }\n }\n if (quarters.includes(BACKLOG)) out.push(BACKLOG);\n return out;\n}\n\nexport interface RoadmapGridProps {\n items: RoadmapItem[];\n onItemUpdate?: (updatedItem: RoadmapItem) => void;\n /** Show the desktop left margin (~120px) that aligns the grid with the page\n * hero. Default `true`. Related-content rails pass `false`. */\n showLeftMargin?: boolean;\n /** URL builder for the per-task refresh call after a successful vote. */\n buildRefreshUrl?: (taskId: string) => string;\n /** Voting hook options (vote endpoint + storage key). */\n votingOptions?: UseRoadmapVotingOptions;\n /** Group items into collapsible per-quarter accordions. Default `false`\n * (flat grid) so EXISTING callers — the hub's per-quarter RoadmapGrid calls\n * and related-content rails — stay unchanged. `RoadmapView` / full-page\n * callers pass `true` to get the shared quarter grouping. */\n groupByQuarter?: boolean;\n /** When true (search/filter active), every quarter expands so results aren't\n * hidden in collapsed sections. Threaded from `RoadmapView`. */\n hasActiveFilters?: boolean;\n /** Past quarters within this window of the current quarter stay open by\n * default; older ones collapse. Default `2`. */\n quartersToKeepClosed?: number;\n}\n\n/** Internal flat 2-col grid for ONE set of items. Voting comes from the parent\n * so the state is shared across quarters. */\nfunction RoadmapGridSingle({\n items,\n showLeftMargin,\n getVote,\n onVote,\n votingTasks,\n}: {\n items: RoadmapItem[];\n showLeftMargin: boolean;\n getVote: (taskId: string) => 'up' | 'down' | null;\n onVote: (taskId: string, voteType: 'up' | 'down') => void;\n votingTasks: Set<string>;\n}) {\n return (\n <div className={`grid grid-cols-1 md:grid-cols-2 gap-6 ${showLeftMargin ? 'md:ml-[120px]' : ''}`}>\n {items.map((item) => (\n // DOM id + sticky-header scroll offset live ON RoadmapCard's own\n // outer element (no wrapper div). Anchor mirrors\n // `buildDevSectionUrl('roadmap', <id>)` → `#roadmap-<external_id>`;\n // `useScrollToHash` in `roadmap-view.tsx` finds the card by id\n // and scrolls.\n <RoadmapCard\n key={item.id}\n item={item}\n id={devSectionAnchorId('roadmap', item.id)}\n userVote={getVote(item.id)}\n onVote={(voteType) => onVote(item.id, voteType)}\n isVoting={votingTasks.has(item.id)}\n />\n ))}\n </div>\n );\n}\n\nexport function RoadmapGrid({\n items,\n onItemUpdate,\n showLeftMargin = true,\n buildRefreshUrl = DEFAULT_BUILD_REFRESH_URL,\n votingOptions,\n groupByQuarter = false,\n hasActiveFilters = false,\n quartersToKeepClosed = 2,\n}: RoadmapGridProps) {\n // ── Voting (shared across all quarters) ──\n const { getVote, toggleVote } = useRoadmapVoting(votingOptions);\n const [votingTasks, setVotingTasks] = useState<Set<string>>(new Set());\n\n const handleVote = async (taskId: string, voteType: 'up' | 'down') => {\n if (votingTasks.has(taskId)) return;\n setVotingTasks((prev) => new Set(prev).add(taskId));\n try {\n const result = await toggleVote(taskId, voteType);\n if (result.success) {\n const response = await contentFetch(buildRefreshUrl(taskId));\n if (response.ok) {\n const data = await response.json();\n if (data.item && onItemUpdate) onItemUpdate(data.item);\n }\n }\n } finally {\n setVotingTasks((prev) => {\n const next = new Set(prev);\n next.delete(taskId);\n return next;\n });\n }\n };\n\n // ── Quarter bucketing + chronological sort (recomputed each render; cheap) ──\n const itemsByQuarter = items.reduce<Record<string, RoadmapItem[]>>((acc, item) => {\n const q = item.quarter || BACKLOG;\n (acc[q] ||= []).push(item);\n return acc;\n }, {});\n for (const q of Object.keys(itemsByQuarter)) {\n itemsByQuarter[q].sort((a, b) => getStatusPriority(a.status) - getStatusPriority(b.status));\n }\n const sortedQuarters = Object.keys(itemsByQuarter).sort((a, b) => {\n if (a === BACKLOG) return 1;\n if (b === BACKLOG) return -1;\n const aD = parseQuarterString(a) ?? { quarter: 0, year: 0 };\n const bD = parseQuarterString(b) ?? { quarter: 0, year: 0 };\n return compareQuarters(aD, bD);\n });\n const sortedQuartersKey = sortedQuarters.join(',');\n\n // ── Accordion expand state (hydration-safe: start [], populate in effects) ──\n const [expandedQuarters, setExpandedQuarters] = useState<string[]>([]);\n const [isInitialized, setIsInitialized] = useState(false);\n const hasSetInitialState = useRef(false);\n const prevItemsLength = useRef(0);\n\n // Initial expand state once data loads (runs once, or when data first arrives).\n useEffect(() => {\n const itemsJustLoaded = prevItemsLength.current === 0 && items.length > 0;\n prevItemsLength.current = items.length;\n if (sortedQuarters.length > 0 && (!hasSetInitialState.current || itemsJustLoaded)) {\n hasSetInitialState.current = true;\n setExpandedQuarters(\n hasActiveFilters\n ? [...sortedQuarters]\n : computeDefaultExpandedQuarters(sortedQuarters, quartersToKeepClosed),\n );\n setIsInitialized(true);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [sortedQuarters.length, items.length]);\n\n // React to filter toggles AFTER init: filters on → expand all; off → defaults.\n useEffect(() => {\n if (!isInitialized || sortedQuarters.length === 0) return;\n setExpandedQuarters(\n hasActiveFilters\n ? [...sortedQuarters]\n : computeDefaultExpandedQuarters(sortedQuarters, quartersToKeepClosed),\n );\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hasActiveFilters, sortedQuartersKey, isInitialized, quartersToKeepClosed]);\n\n if (items.length === 0) {\n return (\n <EmptyState\n type=\"generic\"\n title=\"No roadmap items\"\n description=\"Check back soon for upcoming features and improvements!\"\n />\n );\n }\n\n // Flat mode (related-content rails) — no accordion.\n if (!groupByQuarter) {\n return (\n <RoadmapGridSingle\n items={items}\n showLeftMargin={showLeftMargin}\n getVote={getVote}\n onVote={handleVote}\n votingTasks={votingTasks}\n />\n );\n }\n\n return (\n <Accordion\n type=\"multiple\"\n value={expandedQuarters}\n onValueChange={setExpandedQuarters}\n className=\"flex flex-col gap-10\"\n >\n {sortedQuarters.map((quarter) => {\n const itemCount = itemsByQuarter[quarter]?.length || 0;\n const isExpanded = expandedQuarters.includes(quarter);\n return (\n <AccordionItem\n key={quarter}\n value={quarter}\n id={`quarter-${quarter.replace(/\\s+/g, '-').toLowerCase()}`}\n className=\"border-0\"\n >\n <AccordionTrigger className=\"w-full p-0 hover:no-underline [&>svg]:h-5 [&>svg]:w-5 [&>svg]:text-ods-text-secondary [&>svg]:ml-auto [&>svg]:shrink-0\">\n <div className=\"flex items-center gap-3\">\n <h3\n className={cn(\n \"font-['Azeret_Mono'] font-semibold text-[24px] md:text-[28px] lg:text-[32px] leading-[32px] md:leading-[36px] lg:leading-[40px] text-ods-text-primary tracking-[-0.48px] md:tracking-[-0.56px] lg:tracking-[-0.64px] transition-opacity\",\n isExpanded ? 'opacity-100' : 'opacity-60',\n )}\n >\n {quarter}\n <span className=\"text-ods-accent\">:</span>\n </h3>\n <span\n className={cn(\n 'text-sm font-medium transition-opacity',\n isExpanded ? 'text-ods-text-secondary opacity-100' : 'text-ods-text-tertiary opacity-60',\n )}\n >\n {itemCount} {itemCount === 1 ? 'item' : 'items'}\n {isInitialized && !isExpanded && <span className=\"ml-2 text-ods-accent\">Click to expand</span>}\n </span>\n </div>\n </AccordionTrigger>\n <AccordionContent className=\"pt-4 pb-0 overflow-hidden data-[state=closed]:animate-none data-[state=open]:animate-none\">\n <RoadmapGridSingle\n items={itemsByQuarter[quarter]}\n showLeftMargin={showLeftMargin}\n getVote={getVote}\n onVote={handleVote}\n votingTasks={votingTasks}\n />\n </AccordionContent>\n </AccordionItem>\n );\n })}\n </Accordion>\n );\n}\n","/**\n * RoadmapGridSkeleton — loading state for the `/roadmap` grid view.\n *\n * Pure JSX (no hooks, no events) — `'use client'` not strictly required\n * here; tsup's client-entry banner injects it automatically when this\n * file is bundled into the client output. We match the playbook's\n * skeleton-file convention (no directive when no hooks).\n *\n * NOTE: lib's `chat/entity-cards/roadmap-card.tsx` also exports a\n * `RoadmapCardSkeleton` — that one is the COMPACT 56px chat-card\n * variant. This file's internal card-skeleton (340px grid card)\n * intentionally stays file-internal to avoid the naming collision;\n * only `RoadmapGridSkeleton` is exported.\n */\n\nfunction RoadmapCardSkeleton() {\n return (\n <div className=\"bg-ods-card border border-ods-border rounded-[6px] p-[24px] flex flex-col gap-[16px] min-h-[340px] relative\">\n {/* Status Badge Skeleton - Top Right */}\n <div className=\"absolute top-[24px] right-[24px]\">\n <div className=\"h-[20px] w-[80px] bg-ods-border rounded animate-pulse\"></div>\n </div>\n\n {/* Icon and title skeleton */}\n <div className=\"flex items-center gap-[16px] pr-[120px]\">\n <div className=\"w-[80px] h-[80px] bg-ods-border rounded-lg flex-shrink-0 animate-pulse\"></div>\n <div className=\"flex-1 min-w-0 flex flex-col gap-1\">\n <div className=\"min-h-[48px] flex items-center\">\n <div className=\"h-[24px] w-full bg-ods-border rounded animate-pulse\"></div>\n </div>\n <div className=\"min-h-[20px] flex items-center\">\n <div className=\"h-[14px] w-1/2 bg-ods-border rounded animate-pulse\"></div>\n </div>\n </div>\n </div>\n\n {/* Description skeleton - exactly 3 lines */}\n <div className=\"min-h-[72px] flex items-center\">\n <div className=\"w-full space-y-2\">\n <div className=\"h-[24px] bg-ods-border rounded animate-pulse\"></div>\n <div className=\"h-[24px] bg-ods-border rounded animate-pulse\"></div>\n <div className=\"h-[24px] w-4/5 bg-ods-border rounded animate-pulse\"></div>\n </div>\n </div>\n\n <div className=\"flex-1\"></div>\n\n {/* Bottom skeleton */}\n <div className=\"flex items-center justify-between\">\n <div className=\"h-[48px] w-[120px] bg-ods-border rounded animate-pulse\"></div>\n <div className=\"h-[32px] w-[100px] bg-ods-border rounded animate-pulse\"></div>\n </div>\n </div>\n );\n}\n\nexport interface RoadmapGridSkeletonProps {\n /** Number of skeleton cards to show. Default 4. */\n count?: number;\n /** Show the desktop left margin (~120px) that aligns the grid with\n * the page hero's title block. Default `true`. Related-content rails\n * inside narrower surfaces (e.g. the release detail page) pass `false`. */\n showLeftMargin?: boolean;\n}\n\nexport function RoadmapGridSkeleton({ count = 4, showLeftMargin = true }: RoadmapGridSkeletonProps) {\n return (\n <div className={`grid grid-cols-1 md:grid-cols-2 gap-6 ${showLeftMargin ? 'md:ml-[120px]' : ''}`}>\n {Array.from({ length: count }).map((_, i) => (\n <RoadmapCardSkeleton key={i} />\n ))}\n </div>\n );\n}\n","'use client'\n\n/**\n * `<RoadmapView />` — the SELF-CONTAINED roadmap LIST surface.\n *\n * Fetches the roadmap list via the shared `useSelfFetch` hook and renders the\n * pure controlled `<RoadmapGrid>` (kept controlled so related-content rails can\n * still pass `items`). The host configures only **api routes**: the list\n * `endpoint` (default `/api/roadmap`), the per-task `buildRefreshUrl`, and the\n * vote endpoint via `votingOptions`. Optional `initialItems` hydrates SSR.\n */\n\nimport { useMemo } from 'react'\n\nimport { useSearchParams } from '../../../embed-shims'\nimport { LoadError } from '../../ui/error-state'\nimport { useSelfFetch } from '../../../hooks/use-self-fetch'\nimport { useScrollToHash } from '../../../hooks/use-scroll-to-hash'\nimport type { RoadmapItem } from '../../chat/types/entities/roadmap-item'\nimport { RoadmapGrid } from './roadmap-grid'\nimport { RoadmapGridSkeleton } from './roadmap-grid-skeleton'\nimport type { UseRoadmapVotingOptions } from './use-roadmap-voting'\nimport { DEV_SECTION_PARAM_KEYS } from '../../../utils/dev-sections/dev-section-param-keys'\nimport { STICKY_HEADER_OFFSET_PX } from '../../../utils/same-page-hash-nav'\n\nconst DEFAULT_ENDPOINT = '/api/roadmap'\n// Defaults sourced from the ONE param-key registry the chrome (OPENFRAME_DEV_SECTIONS) also\n// reads, so the chrome's written `?key=` and this view's read can't silently diverge.\nconst DEFAULT_SEARCH_PARAM_KEY = DEV_SECTION_PARAM_KEYS.search\nconst DEFAULT_STATUS_PARAM_KEY = DEV_SECTION_PARAM_KEYS.status\n\nexport interface RoadmapViewProps {\n /** GET list endpoint (the api route). Returns `{ items }`. Default\n * `/api/roadmap`. */\n endpoint?: string\n /** Optional SSR hydrate — skips the initial client fetch. */\n initialItems?: RoadmapItem[]\n showLeftMargin?: boolean\n /** Per-task refresh URL builder (after a vote). Default `/api/roadmap/<id>`. */\n buildRefreshUrl?: (taskId: string) => string\n /** Voting hook options (vote endpoint + storage key). */\n votingOptions?: UseRoadmapVotingOptions\n /** URL param key for the search input — MUST match the section chrome\n * (`DevSectionView`) that writes it. Default `'search'`. */\n searchParamKey?: string\n /** URL param key for the status filter. Default `'status'` (the roadmap\n * section's `filter.paramKey`). `'all'` means no filter. */\n statusParamKey?: string\n}\n\nexport function RoadmapView({\n endpoint = DEFAULT_ENDPOINT,\n initialItems,\n showLeftMargin,\n buildRefreshUrl,\n votingOptions,\n searchParamKey = DEFAULT_SEARCH_PARAM_KEY,\n statusParamKey = DEFAULT_STATUS_PARAM_KEY,\n}: RoadmapViewProps = {}) {\n // Read the search + status params the section chrome (`DevSectionView`) writes\n // and fold them INTO the fetch url so the url IS the cache key — the list\n // refetches filtered whenever the controls change. Mirrors `ProductReleasesView`.\n const searchParams = useSearchParams()\n const search = searchParams.get(searchParamKey) || ''\n const status = searchParams.get(statusParamKey) || 'all'\n const listParams = new URLSearchParams()\n if (search) listParams.set(searchParamKey, search)\n if (status && status !== 'all') listParams.set(statusParamKey, status)\n const qs = listParams.toString()\n const url = qs ? `${endpoint}?${qs}` : endpoint\n\n // Memoize so the SSR `initialItems` wrapper keeps a STABLE identity — else the\n // hook's initialData re-sync effect fires every render and clobbers the\n // optimistic vote patch below.\n const initialData = useMemo(() => (initialItems ? { items: initialItems } : undefined), [initialItems])\n const { data, setData, isLoading, error, reload } = useSelfFetch<{ items?: RoadmapItem[] }>(\n url,\n { initialData },\n )\n const items = data?.items ?? []\n\n // Deep-link hash dispatch — `?search=<id>#roadmap-<id>` from a chat card.\n // Shared hook owns the poll-until-mount + hashchange-listener wiring\n // (same instance used by DeliveryLists). The rAF poll inside the hook\n // handles the Radix `<AccordionItem>` lazy-unmount: on first paint\n // every quarter is collapsed; an effect in `roadmap-grid.tsx` expands\n // them when `hasActiveFilters` is true (chat URL carries `?search=<id>`).\n // The card mounts one tick after `data` lands; the hook waits.\n useScrollToHash(data, { headerOffset: STICKY_HEADER_OFFSET_PX })\n\n if (error) {\n return <LoadError message=\"Failed to load roadmap.\" onRetry={reload} />\n }\n // Skeleton only while the FIRST fetch is in flight (no data yet) — a malformed\n // body lacking `items` renders the grid (empty), never a stuck skeleton.\n if (isLoading && !data) {\n return <RoadmapGridSkeleton showLeftMargin={showLeftMargin} />\n }\n\n return (\n <RoadmapGrid\n items={items}\n showLeftMargin={showLeftMargin}\n buildRefreshUrl={buildRefreshUrl}\n votingOptions={votingOptions}\n // Full-page roadmap → collapsible quarter grouping (the shared RoadmapGrid\n // capability; flat grids stay the RoadmapGrid default). When the chrome's\n // search / status filter is active, expand every quarter so matches aren't hidden.\n groupByQuarter\n hasActiveFilters={search !== '' || status !== 'all'}\n // After a vote refreshes a single task, patch it into the fetched list so\n // the displayed counts stay live.\n onItemUpdate={(updated) =>\n setData((prev) =>\n prev\n ? { ...prev, items: (prev.items ?? []).map((it) => (it.id === updated.id ? updated : it)) }\n : prev,\n )\n }\n />\n )\n}\n","'use client';\n\n/**\n * DeliveryTable — bordered card containing one `<DeliveryRow />` per\n * item. Visual rendering of each row lives in `delivery-row.tsx` so the\n * exact same primitive can be composed elsewhere (notably the linked-\n * delivery surface inside `<TicketDetailDrawer>`).\n *\n * Props:\n * - `items` — flat list of `DeliveryItem`. Two buckets (completed +\n * in-progress) are rendered as two separate `DeliveryTable`s by\n * the parent `DeliveryLists`.\n * - `isLoading` — skeleton rows.\n * - `focusId` — `?focus=<id>` URL param. Marks the matching row\n * `id=\"delivery-<id>\"` and applies the highlight ring so the\n * deep-link from a ticket's linked-card scrolls + flashes the\n * right row.\n */\n\nimport { DeliveryRow } from './delivery-row';\nimport type { DeliveryItem } from '../../../types/delivery';\nimport { devSectionAnchorId } from '../../../utils/dev-sections/dev-section-param-keys';\n\ninterface DeliveryTableProps {\n items: DeliveryItem[];\n isLoading?: boolean;\n}\n\n/**\n * Skeleton loader for rows - matching responsive structure\n */\nfunction SkeletonRow() {\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 {/* Left: Title, subtitle, and description skeleton */}\n <div className=\"flex-1 min-w-0 w-full md:w-auto flex flex-col gap-[12px] md:gap-[16px]\">\n {/* Title skeleton - responsive */}\n <div className=\"min-h-[24px] flex items-center\">\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-full\"></div>\n </div>\n {/* Subtitle skeleton - 1 line */}\n <div className=\"min-h-[20px] flex items-center\">\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-1/2\"></div>\n </div>\n {/* Description skeleton - 3 lines */}\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\"></div>\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-full\"></div>\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-2/3\"></div>\n </div>\n </div>\n </div>\n\n {/* Right: Badge skeleton - two stacked badges */}\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\"></div>\n <div className=\"h-[32px] w-[120px] bg-ods-border rounded animate-pulse\"></div>\n </div>\n </div>\n </div>\n );\n}\n\n/**\n * DeliveryTable Component\n * Displays bug fixes and enhancements with fixed-height rows\n */\nexport function DeliveryTable({ items, isLoading = false }: DeliveryTableProps) {\n // Show skeletons while loading\n if (isLoading) {\n return (\n <div className=\"bg-ods-card border border-ods-border rounded-[6px] overflow-hidden w-full\">\n <div className=\"w-full\">\n {[1, 2, 3, 4, 5].map((i) => (\n <SkeletonRow key={i} />\n ))}\n </div>\n </div>\n );\n }\n\n // Empty state\n if (items.length === 0) {\n return (\n <div className=\"bg-ods-card border border-ods-border rounded-[6px] p-[40px] text-center w-full\">\n <p className=\"text-ods-text-secondary text-[14px] font-['DM_Sans'] font-medium\">\n No tasks available\n </p>\n </div>\n );\n }\n\n return (\n <div className=\"bg-ods-card border border-ods-border rounded-[6px] overflow-hidden w-full\">\n <div className=\"w-full\">\n {items.map((item) => (\n // DOM id lives on DeliveryRow's own outer element (no wrapper\n // div). Anchor mirrors `buildDevSectionUrl('delivery', <id>)`\n // → `#delivery-<external_id>`; `useScrollToHash` in\n // `delivery-lists.tsx` finds the row by id and scrolls. The\n // outer wrapper here ONLY exists for the row separators.\n <div\n key={item.id}\n className=\"border-b border-ods-border last:border-b-0\"\n >\n <DeliveryRow item={item} id={devSectionAnchorId('delivery', item.id)} />\n </div>\n ))}\n </div>\n </div>\n );\n}\n","'use client';\n\n/**\n * DeliveryLists — the delivery section body (two tables: recently\n * completed + active). Reads `search` and `task_type` URL params\n * written by the shared `<DevSectionView>` chrome and refetches on\n * change.\n *\n * Endpoint configuration:\n * - `completedApiEndpoint` / `inProgressApiEndpoint` are the two\n * per-bucket GET endpoints. Defaults match the hub's pre-migration\n * routes (`/api/delivery/completed`, `/api/delivery/in-progress`).\n *\n * Coupling constraint — `searchParamKey` / `taskTypeParamKey`:\n * These props serve TWO purposes:\n * 1. URL READS — keys this component reads via `useSearchParams()`.\n * MUST match the consuming chrome's `section.search.paramKey` /\n * `section.filter.paramKey` (the chrome WRITES the URL params).\n * 2. API WRITES — keys this component sends as query params on the\n * outbound fetch to `{completedApiEndpoint,inProgressApiEndpoint}`.\n * The hub API contract uses `'search'` / `'task_type'`; embedders\n * reverse-proxying those routes must preserve the same names OR\n * rewrite the inbound query string on the proxy side.\n *\n * Defaults align with `OPENFRAME_DEV_SECTIONS.delivery.{search.paramKey,filter.paramKey}`\n * AND the hub API contract, so the OpenFrame zero-config case \"just\n * works\". Custom chrome overriding the param keys must override BOTH\n * ends consistently AND ensure the backend reads the same names.\n */\n\nimport { useEffect, useState } from 'react';\nimport { useSearchParams, useRouter, usePathname } from '../../../embed-shims';\nimport type { DeliveryResponse } from '../../../types/delivery';\nimport { DeliveryTable } from './delivery-table';\nimport { EmptyState } from '../../empty-state';\nimport { LoadError } from '../../ui/error-state';\nimport { DEV_SECTION_PARAM_KEYS } from '../../../utils/dev-sections/dev-section-param-keys';\nimport { STICKY_HEADER_OFFSET_PX } from '../../../utils/same-page-hash-nav';\nimport { contentFetch } from '../../../utils/embed-content-fetch';\nimport { useScrollToHash } from '../../../hooks/use-scroll-to-hash';\n\nconst DEFAULT_COMPLETED_ENDPOINT = '/api/delivery/completed';\nconst DEFAULT_IN_PROGRESS_ENDPOINT = '/api/delivery/in-progress';\n// Param keys sourced from the shared registry (see RoadmapView) — single source for the\n// chrome's written `?key=` and this view's read.\nconst DEFAULT_SEARCH_PARAM_KEY = DEV_SECTION_PARAM_KEYS.search;\nconst DEFAULT_TASK_TYPE_PARAM_KEY = DEV_SECTION_PARAM_KEYS.deliveryTaskType;\n\nexport interface DeliveryListsProps {\n /** GET endpoint for the \"Recently Completed\" bucket. Default\n * `/api/delivery/completed`. */\n completedApiEndpoint?: string;\n /** GET endpoint for the \"Active Tasks\" bucket. Default\n * `/api/delivery/in-progress`. */\n inProgressApiEndpoint?: string;\n /** URL param key for the search input. MUST match the consuming\n * chrome's `section.search.paramKey`. Default `'search'`. */\n searchParamKey?: string;\n /** URL param key for the task-type filter. MUST match the consuming\n * chrome's `section.filter.paramKey`. Default `'task_type'`. */\n taskTypeParamKey?: string;\n}\n\nexport function DeliveryLists({\n completedApiEndpoint = DEFAULT_COMPLETED_ENDPOINT,\n inProgressApiEndpoint = DEFAULT_IN_PROGRESS_ENDPOINT,\n searchParamKey = DEFAULT_SEARCH_PARAM_KEY,\n taskTypeParamKey = DEFAULT_TASK_TYPE_PARAM_KEY,\n}: DeliveryListsProps = {}) {\n const searchParams = useSearchParams();\n const router = useRouter();\n const pathname = usePathname();\n\n const [data, setData] = useState<DeliveryResponse | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n\n // Get filter state from URL\n const searchQuery = searchParams.get(searchParamKey) || '';\n const taskTypeFilter = searchParams.get(taskTypeParamKey) || 'all';\n\n useEffect(() => {\n async function fetchDeliveryData() {\n try {\n setIsLoading(true);\n setError(null);\n\n // Build query parameters for filtering. The outbound key names\n // mirror the inbound URL-param keys — see \"Coupling constraint\"\n // in the file docblock for why.\n const params = new URLSearchParams();\n if (searchQuery) {\n params.set(searchParamKey, searchQuery);\n }\n if (taskTypeFilter && taskTypeFilter !== 'all') {\n params.set(taskTypeParamKey, taskTypeFilter);\n }\n const queryString = params.toString();\n const queryParam = queryString ? `?${queryString}` : '';\n\n // Fetch completed and in-progress tasks separately with filters\n const [completedResponse, inProgressResponse] = await Promise.all([\n contentFetch(`${completedApiEndpoint}${queryParam}`),\n contentFetch(`${inProgressApiEndpoint}${queryParam}`),\n ]);\n\n if (!completedResponse.ok || !inProgressResponse.ok) {\n throw new Error('Failed to fetch delivery items');\n }\n\n const [completedResult, inProgressResult] = await Promise.all([\n completedResponse.json(),\n inProgressResponse.json(),\n ]);\n\n setData({\n completed: completedResult.items || [],\n inProgress: inProgressResult.items || [],\n });\n } catch (err) {\n console.error('Error fetching delivery items:', err);\n setError('Failed to load delivery items. Please try again later.');\n } finally {\n setIsLoading(false);\n }\n }\n\n fetchDeliveryData();\n }, [searchQuery, taskTypeFilter, completedApiEndpoint, inProgressApiEndpoint, searchParamKey, taskTypeParamKey]);\n\n const filteredCompleted = data?.completed || [];\n const filteredInProgress = data?.inProgress || [];\n\n // Deep-link hash dispatch — `?search=<id>#delivery-<id>` from a chat\n // card or a linked-delivery card on a ticket. Shared hook owns the\n // poll-until-mount + hashchange-listener wiring (same instance used\n // by RoadmapView). 96 matches the sticky-header offset every\n // hash-scroll surface in the app uses.\n useScrollToHash(data, { headerOffset: STICKY_HEADER_OFFSET_PX });\n\n const showCompleted = true;\n const showInProgress = true;\n\n const hasActiveFilters = searchQuery !== '' || taskTypeFilter !== 'all';\n const hasResults = (showCompleted && filteredCompleted.length > 0) || (showInProgress && filteredInProgress.length > 0);\n\n // Error state — consume lib's canonical LoadError so ODS tokens +\n // retry affordance stay in lockstep with every other surface.\n if (error) {\n return (\n <div className=\"w-full\">\n <LoadError message={error} onRetry={() => window.location.reload()} />\n </div>\n );\n }\n\n return (\n <div className=\"w-full flex flex-col gap-[40px]\">\n {/* Empty state if no results after filtering */}\n {!isLoading && !hasResults && (\n hasActiveFilters ? (\n <EmptyState\n type=\"search\"\n title=\"No tasks found\"\n description=\"No tasks match your current filters. Try adjusting your search or status filter.\"\n showCTA={true}\n ctaText=\"Reset Filters\"\n onCtaClick={() => {\n const params = new URLSearchParams(searchParams.toString());\n params.delete(searchParamKey);\n params.delete(taskTypeParamKey);\n router.replace(`${pathname}?${params.toString()}`, { scroll: false });\n }}\n />\n ) : (\n <EmptyState\n type=\"generic\"\n title=\"No tasks available\"\n description=\"Check back soon for upcoming tasks!\"\n showCTA={false}\n />\n )\n )}\n\n {/* Completed Tasks Table */}\n {showCompleted && (hasResults || isLoading) && (\n <div className=\"w-full\">\n <h3 className=\"text-h2 text-ods-text-primary tracking-[-0.48px] md:tracking-[-0.56px] lg:tracking-[-0.64px] mb-4\">\n Recently Completed<span className=\"text-ods-accent\">:</span>\n </h3>\n <DeliveryTable\n items={filteredCompleted}\n isLoading={isLoading}\n />\n </div>\n )}\n\n {/* In Progress Tasks Table */}\n {showInProgress && (hasResults || isLoading) && (\n <div className=\"w-full\">\n <h3 className=\"text-h2 text-ods-text-primary tracking-[-0.48px] md:tracking-[-0.56px] lg:tracking-[-0.64px] mb-4\">\n Active Tasks<span className=\"text-ods-accent\">:</span>\n </h3>\n <DeliveryTable\n items={filteredInProgress}\n isLoading={isLoading}\n />\n </div>\n )}\n </div>\n );\n}\n","'use client';\n\n/**\n * useLegalDocs — fetches a legal document (privacy policy, terms of\n * service, or any other markdown-backed legal page) from a hub API.\n *\n * Endpoint configuration — `apiEndpoint`:\n * Default `/api/legal/<docType>`. Reverse-proxy embedders override\n * with their proxied path (e.g. `/proxy/legal/privacy`).\n *\n * Data shape mirrors the hub's `lib/data/legal-utils.ts:LegalDocument`\n * server type. The hook intentionally re-declares the type here so\n * lib consumers don't need to import a server-side type.\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { contentFetch } from '../../../utils/embed-content-fetch';\n\nexport interface LegalDocument {\n title: string;\n content: string;\n sourceFile: string;\n lastSynced: string | null;\n githubSha: string | null;\n sections: Array<{ id: string; title: string; level: number }>;\n docType: string;\n meta: {\n sectionsCount: number;\n contentLength: number;\n lastSyncedAgo: string;\n };\n}\n\nexport interface UseLegalDocsReturn {\n data: LegalDocument | null;\n isLoading: boolean;\n error: string | null;\n refetch: () => void;\n}\n\nexport interface UseLegalDocsOptions {\n /** Optional pre-fetched payload from server (SSR / RSC). When set,\n * the hook skips the initial client fetch. */\n initialData?: LegalDocument | null;\n /** Full GET endpoint URL. Default `/api/legal/<docType>`. */\n apiEndpoint?: string;\n}\n\n/**\n * Hook to fetch a legal document.\n * @param docType — short identifier for the document (drives the\n * default endpoint path AND the error-log prefix). Common values:\n * `'privacy'` (SECURITY.md), `'terms'` (LICENSE). Embedders may use\n * any string — the hook treats it as opaque.\n */\nexport function useLegalDocs(\n docType: string,\n options: UseLegalDocsOptions = {}\n): UseLegalDocsReturn {\n const { initialData = null, apiEndpoint } = options;\n const effectiveEndpoint = apiEndpoint ?? `/api/legal/${docType}`;\n\n const [data, setData] = useState<LegalDocument | null>(initialData ?? null);\n const [isLoading, setIsLoading] = useState(!initialData);\n const [error, setError] = useState<string | null>(null);\n\n const fetchDocument = useCallback(async () => {\n try {\n setIsLoading(true);\n setError(null);\n\n const response = await contentFetch(effectiveEndpoint);\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch ${docType} document: ${response.status} ${response.statusText}`\n );\n }\n\n const result = await response.json();\n\n // Validate the response has required fields\n if (!result.content) {\n throw new Error(`${docType} document content is empty`);\n }\n\n setData(result);\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';\n // `docType` is externally controlled (URL path segment in embedders), so it must NOT sit in\n // console.error's FIRST (format-string) argument — Node interprets %s/%j/%o there\n // (CodeQL js/tainted-format-string). Keep the format string constant; pass docType + err as\n // plain trailing args.\n console.error('Error fetching legal document:', docType, err);\n setError(errorMessage);\n } finally {\n setIsLoading(false);\n }\n }, [docType, effectiveEndpoint]);\n\n // Reset cached data when docType changes — otherwise an embedder using\n // the same hook instance for sequential docTypes (privacy → terms)\n // would briefly render the OLD doc's content while the new fetch is\n // in-flight. Not currently triggered by hub's per-route SSR (each\n // docType mounts in a fresh component), but enforces the contract.\n useEffect(() => {\n setData(initialData ?? null);\n setError(null);\n setIsLoading(!initialData);\n }, [docType, initialData]);\n\n // Fetch on mount (only if we don't already have server-provided initialData)\n useEffect(() => {\n if (initialData) return;\n fetchDocument();\n }, [fetchDocument, initialData]);\n\n const refetch = () => {\n fetchDocument();\n };\n\n return {\n data,\n isLoading,\n error,\n refetch,\n };\n}\n","'use client';\n\n/**\n * LegalDocumentPage — unified UI for privacy-policy, terms-of-service,\n * and any other markdown-backed legal document.\n *\n * Replaces two near-identical hub components (`PrivacyPolicyPage` +\n * `TermsOfServicePage`) that differed only in title, contact email,\n * and copy strings. Caller passes those as props.\n *\n * Markdown rendering: defaults to lib's `SimpleMarkdownRenderer`\n * (sufficient for plain-markdown legal docs). Embedders that need\n * richer markdown (embeds, video, OG previews) pass their own via\n * the `MarkdownRenderer` prop — same injection pattern as\n * `ReleaseDetailPage`.\n *\n * Endpoint configuration: forwarded to `useLegalDocs(docType, { apiEndpoint })`.\n */\n\nimport type { ComponentType } from 'react';\nimport { PageShell, PageLayout } from '../../ui';\nimport { RichMarkdownRenderer } from '../../ui/rich-markdown-renderer';\nimport { useRouter } from '../../../embed-shims/next-navigation';\nimport { useLegalDocs, type LegalDocument } from './use-legal-docs';\nimport { formatLegalDate } from '../../../utils/format';\n\nexport interface LegalDocumentMarkdownRendererProps {\n content: string;\n sectionIds?: Array<{ id: string; title: string; level: number }>;\n demoteMarkdownH1ToH2?: boolean;\n}\n\nexport interface LegalDocumentPageProps {\n /** Document type identifier — drives the default API endpoint\n * `/api/legal/<docType>` AND the error-log prefix. Common values:\n * `'privacy'`, `'terms'`. Embedders may use any string. */\n docType: string;\n /** Heading text (e.g. \"Privacy Policy\", \"Terms of Service\"). */\n title: string;\n /** Fallback subtitle shown when no `lastUpdated` date is available\n * (e.g. \"Our privacy policy and data protection practices\"). */\n fallbackDescription: string;\n /** Email shown in the error + empty-state copy\n * (e.g. `'privacy@openframe.io'`, `'legal@openframe.io'`). */\n contactEmail: string;\n /** Prompt shown above the contact link in the error state\n * (e.g. \"For privacy-related questions, please contact:\"). */\n errorContactPrompt: string;\n /** Title for the error block (e.g. \"Unable to load privacy policy\"). */\n errorTitle: string;\n /** Sentence shown when the API returns no document\n * (e.g. \"Privacy policy content is not available at this time.\"). */\n emptyStateMessage: string;\n /** SSR-prepared document, if available. */\n initialData?: LegalDocument | null;\n /** SSR-prepared formatted \"Last Updated\" label. Stable across hydration. */\n initialLastUpdatedLabel?: string | null;\n /** Override the default `/api/legal/<docType>` endpoint\n * (reverse-proxy embedders, alternate API paths). */\n apiEndpoint?: string;\n /** Override the default markdown renderer. */\n MarkdownRenderer?: ComponentType<LegalDocumentMarkdownRendererProps>;\n /** Back-button config — same pattern as `DevSectionPage`. Pass `false`\n * to hide. Default `{ label: 'Back to home', href: '/' }`. */\n backButton?: { label?: string; href?: string } | false;\n /** Render the standalone `<PageShell>`. Default true. Pass false when the host\n * layout already provides the page container — only the padding box renders,\n * avoiding a nested `<main>`. */\n shell?: boolean;\n}\n\nexport function LegalDocumentPage({\n docType,\n title,\n fallbackDescription,\n contactEmail,\n errorContactPrompt,\n errorTitle,\n emptyStateMessage,\n initialData = null,\n initialLastUpdatedLabel = null,\n apiEndpoint,\n MarkdownRenderer = RichMarkdownRenderer,\n backButton,\n shell = true,\n}: LegalDocumentPageProps) {\n const router = useRouter();\n const { data, isLoading, error } = useLegalDocs(docType, { initialData, apiEndpoint });\n\n // Back-button config — mirrors DevSectionPage's `{ label: 'Back to home',\n // onClick: () => router.push('/') }`. Hide entirely when caller passes\n // `false` (e.g. embed-mode where the host owns navigation chrome).\n const backCfg =\n backButton === false\n ? undefined\n : {\n label: backButton?.label ?? 'Back to home',\n onClick: () => router.push(backButton?.href ?? '/'),\n };\n\n const fallbackLastUpdatedLabel =\n data?.lastSynced != null ? formatLegalDate(data.lastSynced) : null;\n const effectiveLastUpdatedLabel = initialLastUpdatedLabel ?? fallbackLastUpdatedLabel;\n\n // Subtitle routes through the frozen `PageLayout` `TitleBlock` (text-h2 title\n // + subtitle) — unified header across all help-center pages. Shows the\n // last-updated date when known, else the fallback description.\n const subtitle = effectiveLastUpdatedLabel ? `Last Updated: ${effectiveLastUpdatedLabel}` : fallbackDescription;\n\n const inner = (\n <PageLayout title={title} subtitle={subtitle} backButton={backCfg} titleSize=\"h1\">\n {data?.sourceFile && (\n <p className=\"font-['DM_Sans'] text-sm text-ods-text-secondary opacity-75\">Source: {data.sourceFile}</p>\n )}\n\n <div className=\"flex flex-col lg:flex-row gap-6 lg:gap-10 items-start flex-1\">\n <div className=\"flex-1\">\n <div className=\"w-full\">\n <article className=\"space-y-2\">\n {isLoading ? (\n // Loading skeleton matching Knowledge Hub pattern\n <div className=\"space-y-6\">\n <div className=\"h-10 bg-ods-skeleton rounded-lg w-3/4 animate-pulse\"></div>\n <div className=\"space-y-4\">\n <div className=\"h-4 bg-ods-skeleton rounded w-full animate-pulse\"></div>\n <div className=\"h-4 bg-ods-skeleton rounded w-full animate-pulse\"></div>\n <div className=\"h-4 bg-ods-skeleton rounded w-5/6 animate-pulse\"></div>\n </div>\n <div className=\"h-32 bg-ods-card border border-ods-border rounded-lg animate-pulse\"></div>\n <div className=\"space-y-4\">\n <div className=\"h-4 bg-ods-skeleton rounded w-full animate-pulse\"></div>\n <div className=\"h-4 bg-ods-skeleton rounded w-4/5 animate-pulse\"></div>\n </div>\n </div>\n ) : error ? (\n <div className=\"text-center space-y-4\">\n <div className=\"bg-red-900/20 border border-red-700 rounded-lg p-6\">\n <p className=\"text-red-400 mb-2\">{errorTitle}</p>\n <p className=\"text-red-300 text-sm\">{error}</p>\n </div>\n <div className=\"text-ods-text-secondary\">\n <p>{errorContactPrompt}</p>\n <a href={`mailto:${contactEmail}`} className=\"text-ods-accent hover:underline\">\n {contactEmail}\n </a>\n </div>\n </div>\n ) : data ? (\n <MarkdownRenderer\n content={data.content}\n sectionIds={data.sections || []}\n demoteMarkdownH1ToH2\n />\n ) : (\n <div className=\"text-center text-ods-text-secondary py-16\">\n <p className=\"text-xl\">{emptyStateMessage}</p>\n <p className=\"mt-2\">\n Please contact{' '}\n <a href={`mailto:${contactEmail}`} className=\"text-ods-accent hover:underline\">\n {contactEmail}\n </a>{' '}\n for more information.\n </p>\n </div>\n )}\n </article>\n </div>\n </div>\n </div>\n </PageLayout>\n );\n\n return shell ? <PageShell>{inner}</PageShell> : <div className=\"page-shell-content\">{inner}</div>;\n}\n"],"mappings":"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","names":["jsx","jsxs","Fragment","jsx","jsxs","useState","Fragment","jsx","jsxs","useState","jsx","useState","useEffect","useState","useEffect","useEffect","useState","jsx","jsxs","useState","useEffect","jsx","jsxs","jsx","DEFAULT_ENDPOINT","DEFAULT_SEARCH_PARAM_KEY","DEFAULT_STATUS_PARAM_KEY","jsx","jsxs","useEffect","useState","jsx","jsxs","DEFAULT_SEARCH_PARAM_KEY","useState","useEffect","useState","useEffect","useCallback","useState","useCallback","useEffect","jsx","jsxs"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-VQEIDKRF.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-VQEIDKRF.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 /** Render this component's own title heading (hero or compact `h2`). Default\n * `true` — the tab context renders the compact heading itself. Pass `false`\n * when the host already renders the title elsewhere (e.g. `DevSectionPage`\n * routes it through the unified `PageLayout` `TitleBlock`): then only the\n * search + filter controls + list render, with no duplicate heading. */\n showHeading?: boolean;\n}\n\nexport function DevSectionView({ sectionKey, hero, preControls, children, showHeading = true }: 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 {showHeading &&\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\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 /** Render the standalone `<PageShell>` (own `<main>` + bg + max-width). Default\n * `true` — the contract for marketing/hub surfaces with no app shell. Pass\n * `false` when the host layout already provides the page container (e.g.\n * openframe-frontend's `AppLayout` `<main>`): then only the\n * `page-shell-content` padding box is rendered, avoiding a nested `<main>`. */\n shell?: boolean;\n}\n\nexport function DevSectionPage({\n sectionKey,\n children,\n preControls,\n backButton,\n title,\n subtitle,\n shell = true,\n}: DevSectionPageProps) {\n const router = useRouter();\n const section = OPENFRAME_DEV_SECTIONS[sectionKey];\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 const inner = (\n // Unified header: title/description route through the canonical (frozen)\n // `PageLayout` `TitleBlock` (text-h2) — same as FAQ / Legal / detail pages —\n // so every help-center surface shares one header. `DevSectionView` then\n // renders ONLY its search + filter controls (`showHeading={false}`), no\n // duplicate title. (The hero icon is intentionally dropped: TitleBlock is\n // frozen and renders title-only.)\n <PageLayout\n title={title ?? section.hero.title}\n subtitle={subtitle ?? section.hero.description}\n titleSize=\"h1\"\n backButton={backCfg}\n >\n <DevSectionView sectionKey={sectionKey} showHeading={false} preControls={preControls}>\n {children}\n </DevSectionView>\n </PageLayout>\n );\n\n // `shell` true → standalone `<PageShell>` (own <main> + bg + max-width).\n // false → padding-only box (no nested <main>) for hosts whose layout already\n // provides the container; both consume the host's `--page-shell-*` vars.\n return shell ? <PageShell>{inner}</PageShell> : <div className=\"page-shell-content\">{inner}</div>;\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 /** DOM `id` applied to the row's outer element. `DeliveryTable`\n * always sets `delivery-<external_id>` so chat-card deep-links\n * (`?search=<id>#delivery-<id>`) and the ticket linked-card path\n * both have a target for `useScrollToHash` to scroll to. Always\n * paired with `scroll-mt-24` on the outer element so the row lands\n * BELOW the sticky chrome after the scroll. */\n id?: string\n className?: string\n}\n\nexport function DeliveryRow({\n item,\n href,\n caption,\n id,\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 // `scroll-mt-24` is paid for whether `id` is set or not (it's a\n // single Tailwind utility, no runtime cost). Keeping it\n // unconditional means a future caller adding `id` doesn't also\n // have to remember to ask for the offset.\n 'scroll-mt-24',\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} id={id} className={baseClass} prefetch={false}>\n {inner}\n </Link>\n )\n }\n\n return <div id={id} className={baseClass}>{inner}</div>\n}\n"]}
|