@flamingo-stack/openframe-frontend-core 0.0.315 → 0.0.316
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-7U4YFQX2.js → chunk-2Y4DLBFO.js} +96 -92
- package/dist/{chunk-7U4YFQX2.js.map → chunk-2Y4DLBFO.js.map} +1 -1
- package/dist/{chunk-HATCNFQL.cjs → chunk-4MCMPYEM.cjs} +12 -12
- package/dist/{chunk-HATCNFQL.cjs.map → chunk-4MCMPYEM.cjs.map} +1 -1
- package/dist/{chunk-VY5YF4B7.js → chunk-4NVA6W3J.js} +27 -22
- package/dist/chunk-4NVA6W3J.js.map +1 -0
- package/dist/chunk-4V3TCOFC.cjs +394 -0
- package/dist/chunk-4V3TCOFC.cjs.map +1 -0
- package/dist/{chunk-A2H6TFS4.cjs → chunk-63A53WQN.cjs} +33 -33
- package/dist/{chunk-A2H6TFS4.cjs.map → chunk-63A53WQN.cjs.map} +1 -1
- package/dist/{chunk-6W54MBU2.js → chunk-64DZ2J7Q.js} +5 -5
- package/dist/{chunk-47JZOP7Y.js → chunk-6KERXOFE.js} +3 -3
- package/dist/{chunk-JALO4TAZ.js → chunk-AI5X5JTD.js} +4 -4
- package/dist/chunk-CSLMCBZV.js +1464 -0
- package/dist/chunk-CSLMCBZV.js.map +1 -0
- package/dist/{chunk-BSAFGQVW.cjs → chunk-CUNMBP3A.cjs} +13 -13
- package/dist/{chunk-BSAFGQVW.cjs.map → chunk-CUNMBP3A.cjs.map} +1 -1
- package/dist/{chunk-TVNILN2F.cjs → chunk-DHVL36CA.cjs} +40 -40
- package/dist/{chunk-TVNILN2F.cjs.map → chunk-DHVL36CA.cjs.map} +1 -1
- package/dist/chunk-FCEVVNWY.cjs +1916 -0
- package/dist/chunk-FCEVVNWY.cjs.map +1 -0
- package/dist/chunk-FOVX3W3C.cjs +1464 -0
- package/dist/chunk-FOVX3W3C.cjs.map +1 -0
- package/dist/{chunk-4D37W55K.js → chunk-GHVVOST5.js} +95 -116
- package/dist/chunk-GHVVOST5.js.map +1 -0
- package/dist/{chunk-TRSDXD23.js → chunk-JAZM3A7E.js} +2 -2
- package/dist/{chunk-TK6OABYF.js → chunk-JEBL5PQK.js} +21 -35
- package/dist/{chunk-TK6OABYF.js.map → chunk-JEBL5PQK.js.map} +1 -1
- package/dist/{chunk-5ATH263N.cjs → chunk-L5JSGNT3.cjs} +35 -35
- package/dist/{chunk-5ATH263N.cjs.map → chunk-L5JSGNT3.cjs.map} +1 -1
- package/dist/{chunk-TQ7CMFSY.cjs → chunk-LAMDFGE3.cjs} +41 -36
- package/dist/chunk-LAMDFGE3.cjs.map +1 -0
- package/dist/{chunk-V4IIBNTA.js → chunk-LQHMXPOJ.js} +5 -5
- package/dist/{chunk-LGLPNWS6.cjs → chunk-LWNPMLIH.cjs} +3 -3
- package/dist/{chunk-LGLPNWS6.cjs.map → chunk-LWNPMLIH.cjs.map} +1 -1
- package/dist/chunk-M3NULYCR.js +1916 -0
- package/dist/chunk-M3NULYCR.js.map +1 -0
- package/dist/{chunk-MOOV4ORG.js → chunk-OKGZK6TT.js} +3 -3
- package/dist/{chunk-WFHNXCI3.cjs → chunk-OLEW7FYZ.cjs} +123 -144
- package/dist/chunk-OLEW7FYZ.cjs.map +1 -0
- package/dist/chunk-PIJ4JLJU.js +394 -0
- package/dist/chunk-PIJ4JLJU.js.map +1 -0
- package/dist/{chunk-E4CQ4RUG.js → chunk-Q4AMYLKX.js} +11 -11
- package/dist/{chunk-FQOTC3UU.cjs → chunk-QJGRP2YE.cjs} +4 -4
- package/dist/{chunk-FQOTC3UU.cjs.map → chunk-QJGRP2YE.cjs.map} +1 -1
- package/dist/{chunk-ZPK5HW7B.cjs → chunk-UGDGUO26.cjs} +3 -3
- package/dist/{chunk-ZPK5HW7B.cjs.map → chunk-UGDGUO26.cjs.map} +1 -1
- package/dist/{chunk-QW6OL4NY.cjs → chunk-VCE3ZEN3.cjs} +5 -5
- package/dist/{chunk-QW6OL4NY.cjs.map → chunk-VCE3ZEN3.cjs.map} +1 -1
- package/dist/{chunk-2JPSWDSM.cjs → chunk-XAQJ4ZLY.cjs} +447 -443
- package/dist/{chunk-2JPSWDSM.cjs.map → chunk-XAQJ4ZLY.cjs.map} +1 -1
- package/dist/{chunk-2MLMZAK4.js → chunk-YFGDZFUG.js} +4 -4
- package/dist/{chunk-VFIWQGJZ.js → chunk-Z3YORGG4.js} +2 -2
- package/dist/{chunk-OSEKWT6X.cjs → chunk-ZYGVJXJ5.cjs} +33 -47
- package/dist/chunk-ZYGVJXJ5.cjs.map +1 -0
- package/dist/components/case-studies/index.cjs +18 -18
- package/dist/components/case-studies/index.cjs.map +1 -1
- package/dist/components/case-studies/index.js +8 -8
- package/dist/components/chat/index.cjs +8 -8
- package/dist/components/chat/index.js +7 -7
- package/dist/components/contact/index.cjs +9 -9
- package/dist/components/contact/index.js +8 -8
- package/dist/components/docs/doc-viewer.d.ts +4 -0
- package/dist/components/docs/doc-viewer.d.ts.map +1 -1
- package/dist/components/docs/index.cjs +11 -11
- package/dist/components/docs/index.js +10 -10
- package/dist/components/embeds/index.cjs +9 -9
- package/dist/components/embeds/index.js +8 -8
- package/dist/components/faq/faq-document-page.d.ts +18 -20
- package/dist/components/faq/faq-document-page.d.ts.map +1 -1
- package/dist/components/faq/index.cjs +10 -10
- package/dist/components/faq/index.js +9 -9
- package/dist/components/features/index.cjs +8 -8
- package/dist/components/features/index.js +7 -7
- package/dist/components/help-center-pages/delivery-page.d.ts +27 -0
- package/dist/components/help-center-pages/delivery-page.d.ts.map +1 -0
- package/dist/components/help-center-pages/index.cjs +164 -0
- package/dist/components/help-center-pages/index.cjs.map +1 -0
- package/dist/components/help-center-pages/index.d.ts +25 -0
- package/dist/components/help-center-pages/index.d.ts.map +1 -0
- package/dist/components/help-center-pages/index.js +164 -0
- package/dist/components/help-center-pages/index.js.map +1 -0
- package/dist/components/help-center-pages/onboarding-guides-catalog-page.d.ts +41 -0
- package/dist/components/help-center-pages/onboarding-guides-catalog-page.d.ts.map +1 -0
- package/dist/components/help-center-pages/product-releases-list-page.d.ts +34 -0
- package/dist/components/help-center-pages/product-releases-list-page.d.ts.map +1 -0
- package/dist/components/help-center-pages/roadmap-page.d.ts +40 -0
- package/dist/components/help-center-pages/roadmap-page.d.ts.map +1 -0
- package/dist/components/icons/index.cjs +3 -3
- package/dist/components/icons/index.js +2 -2
- package/dist/components/index.cjs +177 -1555
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +348 -1726
- package/dist/components/index.js.map +1 -1
- package/dist/components/layout/page-layout.d.ts +4 -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 +8 -8
- package/dist/components/navigation/index.js +7 -7
- package/dist/components/onboarding-guides/index.cjs +15 -364
- package/dist/components/onboarding-guides/index.cjs.map +1 -1
- package/dist/components/onboarding-guides/index.js +20 -369
- package/dist/components/onboarding-guides/index.js.map +1 -1
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts +9 -1
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +10 -10
- package/dist/components/related-content/index.js +9 -9
- package/dist/components/shared/dev-section/dev-section-page.d.ts +7 -1
- package/dist/components/shared/dev-section/dev-section-page.d.ts.map +1 -1
- package/dist/components/shared/dev-section/dev-section-view.d.ts +7 -1
- package/dist/components/shared/dev-section/dev-section-view.d.ts.map +1 -1
- package/dist/components/shared/legal-document/legal-document-page.d.ts +5 -1
- package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -1
- package/dist/components/shared/product-release/release-detail-page.d.ts +11 -2
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/tickets/help-center-list.d.ts +5 -1
- package/dist/components/tickets/help-center-list.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +15 -1882
- package/dist/components/tickets/index.cjs.map +1 -1
- package/dist/components/tickets/index.js +28 -1895
- package/dist/components/tickets/index.js.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +53 -53
- package/dist/components/ui/file-manager/index.cjs.map +1 -1
- package/dist/components/ui/file-manager/index.js +4 -4
- package/dist/components/ui/index.cjs +8 -8
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +7 -7
- package/dist/hooks/index.cjs +5 -5
- package/dist/hooks/index.js +4 -4
- package/dist/index.cjs +10 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +9 -9
- package/package.json +7 -1
- package/src/components/docs/doc-viewer.tsx +21 -34
- package/src/components/faq/faq-document-page.tsx +33 -60
- package/src/components/help-center-pages/delivery-page.tsx +45 -0
- package/src/components/help-center-pages/index.ts +41 -0
- package/src/components/help-center-pages/onboarding-guides-catalog-page.tsx +66 -0
- package/src/components/help-center-pages/product-releases-list-page.tsx +58 -0
- package/src/components/help-center-pages/roadmap-page.tsx +68 -0
- package/src/components/layout/page-layout.tsx +11 -0
- package/src/components/layout/title-block.tsx +15 -2
- package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +30 -19
- package/src/components/shared/dev-section/dev-section-page.tsx +29 -19
- package/src/components/shared/dev-section/dev-section-view.tsx +26 -19
- package/src/components/shared/legal-document/legal-document-page.tsx +19 -23
- package/src/components/shared/product-release/release-detail-page.tsx +36 -36
- package/src/components/tickets/help-center-list.tsx +11 -3
- package/dist/chunk-4D37W55K.js.map +0 -1
- package/dist/chunk-OSEKWT6X.cjs.map +0 -1
- package/dist/chunk-TQ7CMFSY.cjs.map +0 -1
- package/dist/chunk-VY5YF4B7.js.map +0 -1
- package/dist/chunk-WFHNXCI3.cjs.map +0 -1
- /package/dist/{chunk-6W54MBU2.js.map → chunk-64DZ2J7Q.js.map} +0 -0
- /package/dist/{chunk-47JZOP7Y.js.map → chunk-6KERXOFE.js.map} +0 -0
- /package/dist/{chunk-JALO4TAZ.js.map → chunk-AI5X5JTD.js.map} +0 -0
- /package/dist/{chunk-TRSDXD23.js.map → chunk-JAZM3A7E.js.map} +0 -0
- /package/dist/{chunk-V4IIBNTA.js.map → chunk-LQHMXPOJ.js.map} +0 -0
- /package/dist/{chunk-MOOV4ORG.js.map → chunk-OKGZK6TT.js.map} +0 -0
- /package/dist/{chunk-E4CQ4RUG.js.map → chunk-Q4AMYLKX.js.map} +0 -0
- /package/dist/{chunk-2MLMZAK4.js.map → chunk-YFGDZFUG.js.map} +0 -0
- /package/dist/{chunk-VFIWQGJZ.js.map → chunk-Z3YORGG4.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-FOVX3W3C.cjs","../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"],"names":["jsx","jsxs","Fragment","useState","useEffect","DEFAULT_SEARCH_PARAM_KEY","DEFAULT_STATUS_PARAM_KEY","DEFAULT_ENDPOINT","useCallback"],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-FOVX3W3C.cjs","sourcesContent":[null,"\"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 SimpleMarkdownRenderer={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 SimpleMarkdownRenderer={MarkdownRenderer}\n />\n <ReleaseChangelogSection\n title=\"Bugs Fixed\"\n entries={bugFixed || []}\n icon={<Wrench className=\"h-6 w-6\" />}\n previewFirst\n SimpleMarkdownRenderer={MarkdownRenderer}\n />\n <ReleaseChangelogSection\n title=\"Improvements\"\n entries={improvements || []}\n icon={<TrendingUp className=\"h-6 w-6\" />}\n previewFirst\n SimpleMarkdownRenderer={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"]}
|
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
FileDownloadCard,
|
|
4
4
|
GoogleSheetsViewer,
|
|
5
5
|
PdfViewer
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-64DZ2J7Q.js";
|
|
7
7
|
import {
|
|
8
8
|
DocSearchBar,
|
|
9
9
|
useDocSearch
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-6KERXOFE.js";
|
|
11
11
|
import {
|
|
12
12
|
DEFAULT_FOLDER_INDEX_FILE,
|
|
13
13
|
FigmaEmbed,
|
|
@@ -19,13 +19,13 @@ import {
|
|
|
19
19
|
findDocNodeByPath,
|
|
20
20
|
getDocAncestorNodeIds,
|
|
21
21
|
stripFolderIndexFromPath
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-2Y4DLBFO.js";
|
|
23
23
|
import {
|
|
24
24
|
HUB_HEADER_OFFSET_PX,
|
|
25
25
|
contentFetch,
|
|
26
26
|
navigateSamePageHash,
|
|
27
27
|
scrollElementIntoView
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-Q4AMYLKX.js";
|
|
29
29
|
import {
|
|
30
30
|
useChatRuntime
|
|
31
31
|
} from "./chunk-2FI3USTC.js";
|
|
@@ -1485,11 +1485,10 @@ function DocViewerContent({
|
|
|
1485
1485
|
renderSkeleton,
|
|
1486
1486
|
chatSource,
|
|
1487
1487
|
title,
|
|
1488
|
-
titleIcon,
|
|
1489
1488
|
subtitle,
|
|
1490
|
-
accentDot,
|
|
1491
1489
|
colorPalette = DEFAULT_DOC_VIEWER_PALETTE,
|
|
1492
1490
|
className = "",
|
|
1491
|
+
shell = true,
|
|
1493
1492
|
docPath,
|
|
1494
1493
|
sidebarLabel = "DOCUMENTATION",
|
|
1495
1494
|
structureEndpoint,
|
|
@@ -1567,50 +1566,44 @@ function DocViewerContent({
|
|
|
1567
1566
|
const containerBgStyle = colorPalette.containerBackground !== "transparent" ? { backgroundColor: colorPalette.containerBackground } : {};
|
|
1568
1567
|
const defaultEmptyText = structure.length > 0 ? "Select a document from the sidebar to view" : "No documents yet. Add content from the admin panel.";
|
|
1569
1568
|
const resolvedEmptyText = emptyStateText || defaultEmptyText;
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
/* @__PURE__ */ jsx6("
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
] })
|
|
1609
|
-
] }) }),
|
|
1610
|
-
!error && /* @__PURE__ */ jsxs4("div", { className: "flex flex-col lg:flex-row gap-6 lg:gap-10 items-start flex-1", children: [
|
|
1611
|
-
/* @__PURE__ */ jsx6("div", { className: "w-full lg:w-[320px] lg:shrink-0", children: /* @__PURE__ */ jsx6("div", { className: "lg:sticky lg:top-20", children: isLoadingStructure ? /* @__PURE__ */ jsx6(CategorySidebarSkeleton, {}) : /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
1612
|
-
/* @__PURE__ */ jsx6(PersistentMobileDropdown, { isLoading: false, children: /* @__PURE__ */ jsx6(
|
|
1613
|
-
MobileNavigationDropdown,
|
|
1569
|
+
const inner = /* @__PURE__ */ jsx6("div", { style: { ...bgStyle, ...containerBgStyle }, children: /* @__PURE__ */ jsx6(PageLayout, { title, subtitle, titleSize: "h1", backButton: backCfg ?? void 0, children: /* @__PURE__ */ jsxs4("div", { className: "w-full flex flex-col gap-10", children: [
|
|
1570
|
+
showAIChat && /* @__PURE__ */ jsx6(
|
|
1571
|
+
DocSearchBar,
|
|
1572
|
+
{
|
|
1573
|
+
placeholder: `Search ${sidebarLabel?.toLowerCase() || "documents"}...`,
|
|
1574
|
+
query: docSearch.query,
|
|
1575
|
+
onQueryChange: docSearch.setQuery,
|
|
1576
|
+
results: docSearch.results,
|
|
1577
|
+
isLoading: docSearch.isLoading,
|
|
1578
|
+
onResultSelect: docSearch.handleResultSelect,
|
|
1579
|
+
showDropdown: docSearch.keepDropdownOpen
|
|
1580
|
+
}
|
|
1581
|
+
),
|
|
1582
|
+
error && /* @__PURE__ */ jsx6("div", { className: "flex justify-center", children: /* @__PURE__ */ jsxs4("div", { className: "rounded-lg border bg-ods-card p-8 text-center max-w-md border-ods-border", children: [
|
|
1583
|
+
/* @__PURE__ */ jsx6("h2", { className: "text-xl font-semibold text-ods-text-primary", children: "Error Loading Documents" }),
|
|
1584
|
+
/* @__PURE__ */ jsxs4("p", { className: "mt-2 text-ods-text-secondary", children: [
|
|
1585
|
+
error,
|
|
1586
|
+
". Please try again later."
|
|
1587
|
+
] })
|
|
1588
|
+
] }) }),
|
|
1589
|
+
!error && /* @__PURE__ */ jsxs4("div", { className: "flex flex-col lg:flex-row gap-6 lg:gap-10 items-start flex-1", children: [
|
|
1590
|
+
/* @__PURE__ */ jsx6("div", { className: "w-full lg:w-[320px] lg:shrink-0", children: /* @__PURE__ */ jsx6("div", { className: "lg:sticky lg:top-20", children: isLoadingStructure ? /* @__PURE__ */ jsx6(CategorySidebarSkeleton, {}) : /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
1591
|
+
/* @__PURE__ */ jsx6(PersistentMobileDropdown, { isLoading: false, children: /* @__PURE__ */ jsx6(
|
|
1592
|
+
MobileNavigationDropdown,
|
|
1593
|
+
{
|
|
1594
|
+
nodes: structure,
|
|
1595
|
+
selectedPath,
|
|
1596
|
+
expandedNodes,
|
|
1597
|
+
onNodeClick: selectNode,
|
|
1598
|
+
onToggleExpand: toggleNode,
|
|
1599
|
+
isLoading: false,
|
|
1600
|
+
folderIndexFile
|
|
1601
|
+
}
|
|
1602
|
+
) }),
|
|
1603
|
+
/* @__PURE__ */ jsx6(PersistentSidebar, { isLoading: false, children: /* @__PURE__ */ jsx6("div", { className: "hidden lg:block", children: /* @__PURE__ */ jsxs4("div", { className: "space-y-4", children: [
|
|
1604
|
+
/* @__PURE__ */ jsx6("h3", { className: "text-[14px] font-['Azeret_Mono'] font-semibold uppercase text-ods-text-secondary tracking-[-0.02em] leading-[1.43em]", children: sidebarLabel }),
|
|
1605
|
+
/* @__PURE__ */ jsx6(
|
|
1606
|
+
MultiLevelNavigation,
|
|
1614
1607
|
{
|
|
1615
1608
|
nodes: structure,
|
|
1616
1609
|
selectedPath,
|
|
@@ -1620,71 +1613,57 @@ function DocViewerContent({
|
|
|
1620
1613
|
isLoading: false,
|
|
1621
1614
|
folderIndexFile
|
|
1622
1615
|
}
|
|
1623
|
-
)
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
)
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
sections: stickyNavSections,
|
|
1675
|
-
activeSection,
|
|
1676
|
-
onSectionClick: handleSectionClick,
|
|
1677
|
-
ribbonPosition: "left",
|
|
1678
|
-
ribbonColor: "var(--ods-accent)"
|
|
1679
|
-
}
|
|
1680
|
-
)
|
|
1681
|
-
] }) })
|
|
1682
|
-
]
|
|
1683
|
-
}
|
|
1684
|
-
) })
|
|
1685
|
-
] })
|
|
1686
|
-
] }) }) }) })
|
|
1687
|
-
);
|
|
1616
|
+
)
|
|
1617
|
+
] }) }) })
|
|
1618
|
+
] }) }) }),
|
|
1619
|
+
/* @__PURE__ */ jsx6("div", { className: "flex-1 min-w-0 w-full", children: /* @__PURE__ */ jsxs4(
|
|
1620
|
+
"div",
|
|
1621
|
+
{
|
|
1622
|
+
className: `grid grid-cols-1 ${// "On this page" right column only makes sense for
|
|
1623
|
+
// MARKDOWN content (PDFs / Sheets / Figma / file have no
|
|
1624
|
+
// sections to navigate to). Gating the grid template on
|
|
1625
|
+
// `isMarkdownContent` also suppresses the section-skeleton
|
|
1626
|
+
// bars during embed loads — the user-reported "skeleton
|
|
1627
|
+
// shouldn't be on file pages" bug.
|
|
1628
|
+
isMarkdownContent && (showStickyNav && stickyNavSections.length > 0 || isLoadingContent || isLoadingStructure) ? "lg:grid-cols-[1fr_280px]" : ""} gap-8`,
|
|
1629
|
+
children: [
|
|
1630
|
+
/* @__PURE__ */ jsx6("div", { className: `w-full min-w-0 ${isMarkdownContent ? "max-w-4xl mx-auto" : ""}`, children: /* @__PURE__ */ jsx6("article", { className: "space-y-2", children: isLoadingContent || isLoadingStructure ? renderSkeleton(selectedNodeDocType) : !content ? /* @__PURE__ */ jsx6("div", { className: "text-center py-16", children: /* @__PURE__ */ jsx6("p", { className: "text-xl text-ods-text-secondary", children: resolvedEmptyText }) }) : renderedContent }) }),
|
|
1631
|
+
isMarkdownContent && (isLoadingContent || isLoadingStructure) && /* @__PURE__ */ jsx6("div", { className: "hidden lg:block", children: /* @__PURE__ */ jsxs4("div", { className: "sticky top-24", children: [
|
|
1632
|
+
/* @__PURE__ */ jsx6("div", { className: "h-[14px] w-28 bg-ods-border rounded animate-pulse mb-5" }),
|
|
1633
|
+
/* @__PURE__ */ jsx6("div", { className: "space-y-0", children: [130, 170, 190, 220, 110, 200, 80, 100, 120, 140, 90].map((w, i) => /* @__PURE__ */ jsx6(
|
|
1634
|
+
"div",
|
|
1635
|
+
{
|
|
1636
|
+
className: `py-[13px] pl-3 border-l-2 ${i === 0 ? "border-ods-accent" : "border-transparent"}`,
|
|
1637
|
+
children: /* @__PURE__ */ jsx6(
|
|
1638
|
+
"div",
|
|
1639
|
+
{
|
|
1640
|
+
className: "h-[13px] bg-ods-border rounded animate-pulse",
|
|
1641
|
+
style: { width: w }
|
|
1642
|
+
}
|
|
1643
|
+
)
|
|
1644
|
+
},
|
|
1645
|
+
i
|
|
1646
|
+
)) })
|
|
1647
|
+
] }) }),
|
|
1648
|
+
showStickyNav && content && stickyNavSections.length > 0 && !isLoadingContent && /* @__PURE__ */ jsx6("div", { className: "hidden lg:block", children: /* @__PURE__ */ jsxs4("div", { className: "sticky top-24", children: [
|
|
1649
|
+
/* @__PURE__ */ jsx6("h3", { className: "text-[14px] font-['Azeret_Mono'] font-semibold uppercase text-ods-text-secondary tracking-[-0.02em] leading-[1.43em] mb-4", children: "ON THIS PAGE" }),
|
|
1650
|
+
/* @__PURE__ */ jsx6(
|
|
1651
|
+
StickySectionNav,
|
|
1652
|
+
{
|
|
1653
|
+
sections: stickyNavSections,
|
|
1654
|
+
activeSection,
|
|
1655
|
+
onSectionClick: handleSectionClick,
|
|
1656
|
+
ribbonPosition: "left",
|
|
1657
|
+
ribbonColor: "var(--ods-accent)"
|
|
1658
|
+
}
|
|
1659
|
+
)
|
|
1660
|
+
] }) })
|
|
1661
|
+
]
|
|
1662
|
+
}
|
|
1663
|
+
) })
|
|
1664
|
+
] })
|
|
1665
|
+
] }) }) });
|
|
1666
|
+
return shell ? /* @__PURE__ */ jsx6(PageShell, { contentClassName: `${bgClass} ${className}`, children: inner }) : /* @__PURE__ */ jsx6("div", { className: `page-shell-content ${bgClass} ${className}`.trim(), children: inner });
|
|
1688
1667
|
}
|
|
1689
1668
|
|
|
1690
1669
|
// src/components/docs/skeletons.tsx
|
|
@@ -1866,4 +1845,4 @@ export {
|
|
|
1866
1845
|
EmbedSkeleton,
|
|
1867
1846
|
DocsHubPage
|
|
1868
1847
|
};
|
|
1869
|
-
//# sourceMappingURL=chunk-
|
|
1848
|
+
//# sourceMappingURL=chunk-GHVVOST5.js.map
|