{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-TIIHOVUA.cjs","../src/components/docs/doc-viewer.tsx","../src/components/persistent-filter-controls.tsx","../src/components/loading/page-layout-skeleton.tsx","../src/components/loading/unified-skeleton.tsx","../src/components/loading/card-skeleton.tsx","../src/components/docs/use-document-tree.ts","../src/components/docs/doc-navigation-context.tsx","../src/components/docs/use-scroll-spy.ts","../src/components/docs/use-docs-resolve-link.ts","../src/components/docs/skeletons.tsx","../src/components/docs/docs-hub-page.tsx"],"names":["jsx","jsxs","Fragment","useRef","useCallback","pathParts","currentPath","useState","useMemo"],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-TIIHOVUA.cjs","sourcesContent":[null,"\"use client\"\n\nimport React, { useMemo } from \"react\"\nimport { MultiLevelNavigation, MobileNavigationDropdown } from \"../navigation/multi-level-navigation\"\nimport { PageLayout } from \"../layout/page-layout\"\nimport { PageShell } from \"../layout/article-detail-layout\"\nimport { useRouter } from \"../../embed-shims/next-navigation\"\nimport { PersistentSidebar, PersistentMobileDropdown } from \"../persistent-filter-controls\"\nimport { CategorySidebarSkeleton } from \"../loading/page-layout-skeleton\"\nimport { DocSearchBar, useDocSearch } from \"../shared/doc-search\"\nimport { StickySectionNav } from \"../navigation/sticky-section-nav\"\nimport { useDocumentTree } from \"./use-document-tree\"\nimport { useScrollSpy } from \"./use-scroll-spy\"\nimport { useDocNavigation } from \"./doc-navigation-context\"\nimport { findDocNodeByPath } from \"../../utils/doc-tree-nav\"\nimport type { DocContent, DocNode, DocRenderHandlers, DocSourceId } from \"../../types/doc-source\"\nimport { useDocsResolveLink } from \"./use-docs-resolve-link\"\n\n/** Color tokens for the doc-viewer chrome. Hub-side `DocViewer` callers share\n *  this constant; no need to override per source — the palette is intentionally\n *  uniform across knowledge-base + data-room (both use ODS dark tokens). */\nexport const DEFAULT_DOC_VIEWER_PALETTE = {\n  background: \"bg-ods-bg\",\n  containerBackground: \"transparent\",\n  headerText: \"text-ods-text-primary\",\n  primaryText: \"text-ods-text-primary\",\n  secondaryText: \"text-ods-text-secondary\",\n  accent: \"var(--ods-accent)\",\n  border: \"border-ods-border\",\n  cardBackground: \"bg-ods-card\",\n} as const\n\nexport interface DocViewerProps {\n  /**\n   * Registry source id (`'openframe-docs'`, `'data-room-docs'`, …). Flowed through\n   * `renderContent`'s handlers for `/api/docs/resolve-link` POSTs.\n   */\n  sourceId: DocSourceId\n\n  /**\n   * Render the content body. The page shell owns this — it picks the markdown\n   * renderer, dispatches PDF/Figma/Sheets/file branches, etc. No renderer\n   * interface in between.\n   */\n  renderContent: (content: DocContent, handlers: DocRenderHandlers) => React.ReactNode\n\n  /**\n   * Render the loading skeleton. Receives the selected node's `documentType`\n   * (undefined while structure is still loading) so the caller can return a\n   * markdown-shaped skeleton vs an embed-shaped skeleton.\n   */\n  renderSkeleton: (documentType: DocNode['documentType']) => React.ReactNode\n\n  /**\n   * Chat-source identifier — passed in by the page shell from server-side\n   * `currentPlatform()`. Lib has no platform context; the page shell is the\n   * trusted boundary that wires this. NEVER pass user input here.\n   */\n  chatSource: string\n\n  /** Page title — rendered as the inline hero `<h1>` (same DOM\n   *  `<DevSectionView>`'s hero uses) so the doc-viewer chrome matches the\n   *  dev-section pages. ReactNode is intentionally not supported here —\n   *  every consumer renders the same typography. */\n  title?: string\n  /** Optional icon rendered inline before the title text — same slot\n   *  `<DevSectionView>`'s hero uses (Map for Roadmap, Rocket for Releases,\n   *  etc.). Pass a pre-rendered React element styled with\n   *  `SECTION_HERO_ICON_CLASS` (`h-10 w-10 text-ods-accent`) for visual\n   *  parity with other lib pages. */\n  titleIcon?: React.ReactNode\n  /** Subtitle (h6, secondary text) rendered beneath the title. */\n  subtitle?: string\n  /** Render a yellow accent dot (`.`) after the title — same flag as\n   *  the hub's legacy `<AdminPageHeader accentDot>` so the docs-hub\n   *  surface keeps its existing accent styling after the migration. */\n  accentDot?: boolean\n  /** Override the default ODS palette. Optional — most callers should omit. */\n  colorPalette?: typeof DEFAULT_DOC_VIEWER_PALETTE\n  className?: string\n  /** Render the standalone `<PageShell>` (own `<main>` + bg + max-width). Default\n   *  true. Pass false when the host layout already provides the page container —\n   *  only the padding box renders, avoiding a nested `<main>`. */\n  shell?: boolean\n\n  /** Initial doc path (URL `[...path]`). */\n  docPath?: string\n\n  /** Sidebar header copy (`'DOCUMENTATION'`, `'DATA ROOM'`). */\n  sidebarLabel?: string\n\n  /**\n   * API endpoint for fetching the document tree structure. Defaults to the\n   * dispatcher path `/api/docs/sources/${sourceId}/structure`. Override only\n   * if hosting the viewer behind a different route.\n   */\n  structureEndpoint?: string\n  /** Same shape as `structureEndpoint`. Defaults to `/api/docs/sources/${sourceId}/content`. */\n  contentEndpoint?: string\n  /** RAG-search endpoint that backs the in-source search bar (when `showAIChat`\n   *  is on). Defaults to `/api/docs/search`. Override for proxy-prefix embeds —\n   *  same injectability pattern as `structureEndpoint` / `contentEndpoint`. */\n  searchEndpoint?: string\n  /** POST internal-link resolver. The viewer threads an async `onResolveLink`\n   *  into `renderContent`'s `handlers` that posts `{ link, currentPath, source }`\n   *  here. Defaults to `/api/docs/resolve-link`. Override for proxy-prefix embeds —\n   *  same injectability pattern as `structureEndpoint` / `contentEndpoint` /\n   *  `searchEndpoint`, with `ChatRuntime.endpoints.docsResolveLinkUrl` as a\n   *  runtime fallback (prop → runtime → default). */\n  resolveLinkEndpoint?: string\n  /** Base route path for URL navigation. */\n  baseRoute: string\n\n  /** Empty state copy when no doc is selected. */\n  emptyStateText?: string\n\n  /** Whether to render the doc-search bar (bound to chat). */\n  showAIChat?: boolean\n\n  /** Folder-index filename (default `'README.md'`). */\n  folderIndexFile?: string\n\n  /** Back-button shown above the title. Mirrors `<DevSectionPage>` /\n   *  `<HelpCenterList>` / `<LegalDocumentPage>` so every embeddable surface\n   *  shares the same chrome. Defaults to `{ label: 'Back to home', href: '/' }`.\n   *  Pass `false` to hide; pass `{ href: '/docs' }` etc. when the embed's\n   *  home isn't `/`. */\n  backButton?: { label?: string; href?: string } | false\n}\n\nexport function DocViewer(props: DocViewerProps) {\n  return <DocViewerContent {...props} />\n}\n\nfunction DocViewerContent({\n  sourceId,\n  renderContent,\n  renderSkeleton,\n  chatSource,\n  title,\n  subtitle,\n  colorPalette = DEFAULT_DOC_VIEWER_PALETTE,\n  className = \"\",\n  shell = true,\n  docPath,\n  sidebarLabel = \"DOCUMENTATION\",\n  structureEndpoint,\n  contentEndpoint,\n  searchEndpoint,\n  resolveLinkEndpoint,\n  baseRoute,\n  emptyStateText,\n  showAIChat = false,\n  folderIndexFile,\n  backButton,\n}: DocViewerProps) {\n  // Default endpoints derived from sourceId. Hub callers omit the props in 99%\n  // of cases; the override is for embed contexts where the doc-viewer sits\n  // behind a non-standard route.\n  const resolvedStructureEndpoint =\n    structureEndpoint ?? `/api/docs/sources/${sourceId}/structure`\n  const resolvedContentEndpoint =\n    contentEndpoint ?? `/api/docs/sources/${sourceId}/content`\n  // Resolve-link endpoint chain (prop → ChatRuntime.endpoints → hub default)\n  // + the full fetch + JSON-parse pipeline live in `useDocsResolveLink`.\n  // Keeping it factored out as a proper hook makes the contract reusable\n  // by any embedder rendering doc content outside `<DocViewer>` (custom\n  // markdown renderers, link-resolver previews, etc.) and keeps this\n  // component focused on layout + state.\n  const resolveLink = useDocsResolveLink(sourceId, resolveLinkEndpoint)\n  const {\n    structure,\n    selectedPath,\n    content,\n    isLoadingStructure,\n    isLoadingContent,\n    error,\n    expandedNodes,\n    selectNode,\n    toggleNode,\n    navigateToDoc,\n  } = useDocumentTree(\n    {\n      structureEndpoint: resolvedStructureEndpoint,\n      contentEndpoint: resolvedContentEndpoint,\n      baseRoute,\n      folderIndexFile,\n    },\n    docPath,\n  )\n\n  const { activeSection, handleSectionClick } = useScrollSpy(content?.sections)\n\n  const docNav = useDocNavigation()\n\n  // Back-button config — mirrors `<DevSectionPage>` so the docs surface\n  // matches every other embeddable page's chrome. Default target is `/`\n  // (the embed's home); pass `backButton: false` to hide entirely, or\n  // override the href when the embed's home isn't `/`.\n  const router = useRouter()\n  const backCfg =\n    backButton === false\n      ? null\n      : {\n          label: backButton?.label ?? 'Back to home',\n          onClick: () => router.push(backButton?.href ?? '/'),\n        }\n  const docSearch = useDocSearch({\n    source: chatSource,\n    baseRoute,\n    searchEndpoint,\n    onNavigate: (path) => navigateToDoc(path, { fromInternalLink: true }),\n    onInPageSwap: (path) => docNav.navigate(path),\n  })\n\n  const renderedContent = useMemo(() => {\n    if (!content) return null\n    return renderContent(content, {\n      onInternalLinkClick: navigateToDoc,\n      // Relative-link base = the RENDERED document's path, NOT `selectedPath`.\n      // They diverge for a no-README folder: selection stays on the folder\n      // (e.g. `repo/diagrams`) while the body is its first descendant doc\n      // (e.g. `repo/diagrams/architecture/README.md`, via `findFirstDocPath`).\n      // Resolving `./sibling.mmd` against the folder would 404; resolving it\n      // against `content.path` lands in the descendant's directory. The DAL\n      // sets `content.path` to the served doc in all cases (file / README\n      // folder / first-child fallback), so this is correct everywhere.\n      currentPath: content.path,\n      sourceId,\n      onResolveLink: resolveLink,\n    })\n  }, [content, renderContent, navigateToDoc, sourceId, resolveLink])\n\n  // Selected node's documentType drives:\n  //   - which skeleton the caller renders during fetch (markdown vs embed)\n  //   - the article max-width + sticky-nav visibility (markdown only)\n  // `undefined` documentType is treated as `'markdown'` (per the DocNode\n  // discriminator's documented default).\n  const selectedNodeDocType =\n    selectedPath && structure.length > 0\n      ? findDocNodeByPath(selectedPath, structure)?.documentType\n      : undefined\n  // During loading, the in-flight content's type isn't known yet — fall back\n  // to the selected node's type (or markdown if neither is set).\n  const activeDocType = content?.documentType ?? selectedNodeDocType\n  const isMarkdownContent = !activeDocType || activeDocType === 'markdown'\n  const showStickyNav = isMarkdownContent\n\n  const stickyNavSections =\n    content?.sections?.map((s) => ({ id: s.id, label: s.title })) ?? []\n\n  const isColorValue =\n    colorPalette.background.startsWith('#') ||\n    colorPalette.background.startsWith('rgb') ||\n    colorPalette.background.startsWith('var(')\n\n  const bgStyle = isColorValue ? { backgroundColor: colorPalette.background } : {}\n  const bgClass = !isColorValue ? colorPalette.background : ''\n  const containerBgStyle =\n    colorPalette.containerBackground !== 'transparent'\n      ? { backgroundColor: colorPalette.containerBackground }\n      : {}\n\n  const defaultEmptyText =\n    structure.length > 0\n      ? 'Select a document from the sidebar to view'\n      : 'No documents yet. Add content from the admin panel.'\n  const resolvedEmptyText = emptyStateText || defaultEmptyText\n\n  // Unified header: title/subtitle route through the canonical (frozen)\n  // `PageLayout` `TitleBlock` (text-h2) — same as every other help-center page —\n  // so the docs hub shares one header. The `gap-10` column then holds the search\n  // bar + content grid. `colorPalette` / `className` / `bgStyle` flow through the\n  // shell's contentClassName + an inner style-passthrough wrapper.\n  const inner = (\n      <div style={{ ...bgStyle, ...containerBgStyle }}>\n        <PageLayout title={title} subtitle={subtitle} titleSize=\"h1\" titleWrap backButton={backCfg ?? undefined}>\n          <div className=\"w-full flex flex-col gap-10\">\n          {showAIChat && (\n            <DocSearchBar\n              placeholder={`Search ${sidebarLabel?.toLowerCase() || 'documents'}...`}\n              query={docSearch.query}\n              onQueryChange={docSearch.setQuery}\n              results={docSearch.results}\n              isLoading={docSearch.isLoading}\n              onResultSelect={docSearch.handleResultSelect}\n              showDropdown={docSearch.keepDropdownOpen}\n            />\n          )}\n\n          {error && (\n            <div className=\"flex justify-center\">\n              <div className=\"rounded-lg border bg-ods-card p-8 text-center max-w-md border-ods-border\">\n                <h2 className=\"text-h3 text-ods-text-primary\">\n                  Error Loading Documents\n                </h2>\n                <p className=\"mt-2 text-ods-text-secondary\">{error}. Please try again later.</p>\n              </div>\n            </div>\n          )}\n\n          {!error && (\n            <div className=\"flex flex-col lg:flex-row gap-6 lg:gap-10 items-start flex-1\">\n              <div className=\"w-full lg:w-[320px] lg:shrink-0\">\n                <div className=\"lg:sticky lg:top-20\">\n                  {isLoadingStructure ? (\n                    <CategorySidebarSkeleton />\n                  ) : (\n                    <>\n                      <PersistentMobileDropdown isLoading={false}>\n                        <MobileNavigationDropdown\n                          nodes={structure}\n                          selectedPath={selectedPath}\n                          expandedNodes={expandedNodes}\n                          onNodeClick={selectNode}\n                          onToggleExpand={toggleNode}\n                          isLoading={false}\n                          folderIndexFile={folderIndexFile}\n                        />\n                      </PersistentMobileDropdown>\n\n                      <PersistentSidebar isLoading={false}>\n                        <div className=\"hidden lg:block\">\n                          <div className=\"space-y-4\">\n                            <h3 className=\"text-h5 font-semibold text-ods-text-secondary\">\n                              {sidebarLabel}\n                            </h3>\n                            <MultiLevelNavigation\n                              nodes={structure}\n                              selectedPath={selectedPath}\n                              expandedNodes={expandedNodes}\n                              onNodeClick={selectNode}\n                              onToggleExpand={toggleNode}\n                              isLoading={false}\n                              folderIndexFile={folderIndexFile}\n                            />\n                          </div>\n                        </div>\n                      </PersistentSidebar>\n                    </>\n                  )}\n                </div>\n              </div>\n\n              <div className=\"flex-1 min-w-0 w-full\">\n                <div\n                  className={`grid grid-cols-1 ${\n                    // \"On this page\" right column only makes sense for\n                    // MARKDOWN content (PDFs / Sheets / Figma / file have no\n                    // sections to navigate to). Gating the grid template on\n                    // `isMarkdownContent` also suppresses the section-skeleton\n                    // bars during embed loads — the user-reported \"skeleton\n                    // shouldn't be on file pages\" bug.\n                    isMarkdownContent &&\n                    ((showStickyNav && stickyNavSections.length > 0) ||\n                      isLoadingContent ||\n                      isLoadingStructure)\n                      ? 'lg:grid-cols-[1fr_280px]'\n                      : ''\n                  } gap-8`}\n                >\n                  {/* min-w-0: grid items default to min-width:auto, which would\n                      let a long unbreakable token push this column past the\n                      track width. Pair with the inherited overflow-wrap:anywhere\n                      (app-globals.css) so content wraps instead of overflowing. */}\n                  <div className={`w-full min-w-0 ${isMarkdownContent ? 'max-w-4xl mx-auto' : ''}`}>\n                    <article className=\"space-y-2\">\n                      {(isLoadingContent || isLoadingStructure) ? (\n                        renderSkeleton(selectedNodeDocType)\n                      ) : !content ? (\n                        <div className=\"text-center py-16\">\n                          <p className=\"text-h4 text-ods-text-secondary\">{resolvedEmptyText}</p>\n                        </div>\n                      ) : (\n                        renderedContent\n                      )}\n                    </article>\n                  </div>\n\n                  {isMarkdownContent && (isLoadingContent || isLoadingStructure) && (\n                    <div className=\"hidden lg:block\">\n                      <div className=\"sticky top-24\">\n                        <div className=\"h-[14px] w-28 bg-ods-border rounded animate-pulse mb-5\" />\n                        <div className=\"space-y-0\">\n                          {[130, 170, 190, 220, 110, 200, 80, 100, 120, 140, 90].map((w, i) => (\n                            <div\n                              key={i}\n                              className={`py-[13px] pl-3 border-l-2 ${\n                                i === 0 ? 'border-ods-accent' : 'border-transparent'\n                              }`}\n                            >\n                              <div\n                                className=\"h-[13px] bg-ods-border rounded animate-pulse\"\n                                style={{ width: w }}\n                              />\n                            </div>\n                          ))}\n                        </div>\n                      </div>\n                    </div>\n                  )}\n\n                  {showStickyNav &&\n                    content &&\n                    stickyNavSections.length > 0 &&\n                    !isLoadingContent && (\n                      <div className=\"hidden lg:block\">\n                        <div className=\"sticky top-24\">\n                          <h3 className=\"text-h5 font-semibold text-ods-text-secondary mb-4\">\n                            ON THIS PAGE\n                          </h3>\n                          <StickySectionNav\n                            sections={stickyNavSections}\n                            activeSection={activeSection}\n                            onSectionClick={handleSectionClick}\n                            ribbonPosition=\"left\"\n                            ribbonColor=\"var(--ods-accent)\"\n                          />\n                        </div>\n                      </div>\n                    )}\n                </div>\n              </div>\n            </div>\n          )}\n          </div>\n        </PageLayout>\n      </div>\n  )\n\n  // `shell` true → standalone `<PageShell>`; false → padding-only box (no nested\n  // <main>) for hosts whose layout already provides the container. Both carry the\n  // palette/className via the same `page-shell-content` styling hook.\n  return shell ? (\n    <PageShell contentClassName={`${bgClass} ${className}`}>{inner}</PageShell>\n  ) : (\n    <div className={`page-shell-content ${bgClass} ${className}`.trim()}>{inner}</div>\n  )\n}\n","\"use client\"\n\nimport { ReactNode } from \"react\"\nimport { cn } from \"../utils/cn\"\n\ninterface PersistentFilterControlsProps {\n  /**\n   * Whether the filters are currently in a loading state\n   */\n  isLoading: boolean\n  /**\n   * The filter control components (search, sidebar, mobile dropdown)\n   */\n  children: ReactNode\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 prevent pointer events during loading\n   */\n  preventInteraction?: boolean\n}\n\n/**\n * PersistentFilterControls\n * \n * A wrapper component that keeps filter controls visible during loading states\n * but provides visual feedback that they are temporarily disabled.\n * \n * Features:\n * - Reduces opacity and disables pointer events during loading\n * - Maintains layout and accessibility during loading states\n * - Provides smooth transitions between enabled/disabled states\n * - Preserves keyboard navigation and screen reader functionality\n * - Applies consistent disabled styling across all filter types\n * \n * Usage:\n * ```tsx\n * <PersistentFilterControls isLoading={isLoadingVendors}>\n *   <SearchContainer />\n *   <CategoryFilterSidebar />\n *   <MobileDropdown />\n * </PersistentFilterControls>\n * ```\n */\nexport function PersistentFilterControls({\n  isLoading,\n  children,\n  className,\n  disabledOpacity = 0.6,\n  transitionDuration = 300,\n  preventInteraction = true,\n}: PersistentFilterControlsProps) {\n  return (\n    <div \n      className={cn(\n        \"relative transition-all ease-in-out\",\n        isLoading && preventInteraction && \"pointer-events-none\",\n        className\n      )}\n      style={{\n        opacity: isLoading ? disabledOpacity : 1,\n        transitionDuration: `${transitionDuration}ms`\n      }}\n      role=\"region\"\n      aria-label=\"Filter controls\"\n      aria-busy={isLoading}\n      data-loading={isLoading}\n    >\n      {/* Loading indicator overlay for screen readers */}\n      {isLoading && (\n        <div \n          className=\"sr-only\" \n          role=\"status\" \n          aria-live=\"polite\"\n        >\n          Filters temporarily disabled while loading content\n        </div>\n      )}\n\n      {/* Filter controls with disabled state styling */}\n      <div \n        className={cn(\n          \"transition-all ease-in-out\",\n          isLoading && \"cursor-not-allowed\"\n        )}\n        style={{\n          transitionDuration: `${transitionDuration}ms`\n        }}\n      >\n        {children}\n      </div>\n\n      {/* Visual loading indicator */}\n      {isLoading && (\n        <div \n          className=\"absolute top-2 right-2 z-10\"\n          role=\"status\"\n          aria-label=\"Loading filters\"\n        >\n          <div className=\"w-4 h-4 border-2 border-ods-border border-t-ods-accent rounded-full animate-spin\" />\n        </div>\n      )}\n    </div>\n  )\n}\n\n/**\n * Enhanced SearchContainer wrapper that integrates with persistent loading\n */\ninterface PersistentSearchContainerProps {\n  isLoading: boolean\n  children: ReactNode\n  className?: string\n}\n\nexport function PersistentSearchContainer({\n  isLoading,\n  children,\n  className\n}: PersistentSearchContainerProps) {\n  return (\n    <PersistentFilterControls \n      isLoading={isLoading}\n      preventInteraction={false} // Keep search interactive during loading\n      className={className}\n    >\n      {children}\n    </PersistentFilterControls>\n  )\n}\n\n/**\n * Enhanced sidebar wrapper for desktop filter controls\n */\ninterface PersistentSidebarProps {\n  isLoading: boolean\n  children: ReactNode\n  className?: string\n}\n\nexport function PersistentSidebar({\n  isLoading,\n  children,\n  className\n}: PersistentSidebarProps) {\n  return (\n    <PersistentFilterControls \n      isLoading={isLoading}\n      disabledOpacity={0.5}\n      className={cn(\"lg:sticky lg:top-20\", className)}\n    >\n      {children}\n    </PersistentFilterControls>\n  )\n}\n\n/**\n * Enhanced mobile dropdown wrapper\n */\ninterface PersistentMobileDropdownProps {\n  isLoading: boolean\n  children: ReactNode\n  className?: string\n}\n\nexport function PersistentMobileDropdown({\n  isLoading,\n  children,\n  className\n}: PersistentMobileDropdownProps) {\n  return (\n    <PersistentFilterControls \n      isLoading={isLoading}\n      disabledOpacity={0.7}\n      className={cn(\"lg:hidden\", className)}\n    >\n      {children}\n    </PersistentFilterControls>\n  )\n} ","import type React from \"react\"\nimport { cn } from \"../../utils/cn\"\nimport { UnifiedSkeleton, TextSkeleton, MediaSkeleton, InteractiveSkeleton } from \"./unified-skeleton\"\nimport { CardSkeletonGrid } from \"./card-skeleton\"\n\ninterface PageLayoutSkeletonProps {\n  className?: string\n}\n\n/**\n * Announcement bar skeleton that matches the AnnouncementBar component\n */\nexport function AnnouncementBarSkeleton() {\n  return (\n    <div className=\"bg-ods-skeleton relative w-full animate-pulse\">\n      <div className=\"flex flex-row items-center relative w-full\">\n        <div className=\"box-border flex flex-row gap-4 md:gap-6 items-center justify-start pl-4 md:pl-6 pr-12 md:pr-16 py-3 md:py-4 relative w-full\">\n          {/* Logo skeleton */}\n          <div className=\"relative shrink-0 w-6 h-6 md:w-8 md:h-8 bg-ods-border rounded\"></div>\n          \n          {/* Text content skeleton */}\n          <div className=\"flex-1 min-w-0 space-y-1 md:space-y-2\">\n            <div className=\"h-[14px] md:h-[18px] bg-ods-border rounded w-3/4 max-w-md\"></div>\n            <div className=\"h-[12px] md:h-[18px] bg-ods-border rounded w-full max-w-lg hidden md:block\"></div>\n          </div>\n          \n          {/* Close button skeleton */}\n          <div className=\"absolute right-2 top-2 w-6 h-6 bg-ods-border rounded\"></div>\n        </div>\n      </div>\n    </div>\n  )\n}\n\n/**\n * Header skeleton that matches the ClientOnlyHeader placeholder but with proper animations\n */\nexport function HeaderSkeleton() {\n  return (\n    <header className=\"sticky top-0 z-40 w-full flex items-center justify-between border-b border-ods-border bg-ods-card backdrop-blur-sm bg-ods-card/95 px-4 md:px-[80px] py-3 md:py-[12px] animate-pulse\">\n      {/* Left: Logo skeleton */}\n      <div className=\"flex items-center justify-start flex-1 min-w-0\">\n        <div className=\"w-[110px] h-[26px] md:w-[137px] md:h-8 bg-ods-skeleton rounded\"></div>\n      </div>\n\n      {/* Center: Navigation skeleton - hidden on mobile, visible on desktop */}\n      <nav className=\"hidden md:flex flex-1 basis-1/3 justify-center items-center gap-2 min-w-0\">\n        <div className=\"flex items-center gap-2\">\n          <InteractiveSkeleton.Button className=\"w-24 h-10\" />\n          <InteractiveSkeleton.Button className=\"w-24 h-10\" />\n          <InteractiveSkeleton.Button className=\"w-24 h-10\" />\n        </div>\n      </nav>\n\n      {/* Right: Actions skeleton */}\n      <div className=\"flex items-center justify-end gap-4 flex-1 min-w-0\">\n        {/* Mobile: Show hamburger skeleton */}\n        <div className=\"md:hidden\">\n          <InteractiveSkeleton.Button className=\"h-10 w-10\" />\n        </div>\n        \n        {/* Desktop: Show action buttons skeletons */}\n        <div className=\"hidden md:flex items-center gap-4\">\n          <InteractiveSkeleton.Button className=\"w-10 h-10\" />\n          <InteractiveSkeleton.Button className=\"w-32 h-10\" />\n          <InteractiveSkeleton.Button className=\"w-20 h-10\" />\n        </div>\n      </div>\n    </header>\n  )\n}\n\n/**\n * Hero section skeleton for static content areas\n */\nexport function HeroSkeleton() {\n  return (\n    <section\n      className=\"w-full flex flex-col items-center justify-center py-12 px-4 md:py-20 md:px-8 text-center animate-pulse\"\n      style={{\n        background: 'radial-gradient(circle at 50% 0%, #242323 0%, #1A1A1A 100%)'\n      }}\n    >\n      {/* Title skeleton */}\n      <TextSkeleton.Heading className=\"h-12 md:h-20 lg:h-24 mb-4 md:mb-6 max-w-4xl w-full\" />\n      \n      {/* Subtitle skeleton */}\n      <div className=\"space-y-3 mb-8 md:mb-10 max-w-4xl w-full px-2\">\n        <TextSkeleton.Body className=\"h-5 md:h-7\" />\n        <TextSkeleton.Body className=\"h-5 md:h-7 w-3/4 mx-auto\" />\n      </div>\n      \n      {/* CTA Button skeleton */}\n      <InteractiveSkeleton.Button className=\"w-full md:w-64 h-12\" />\n    </section>\n  )\n}\n\n/**\n * Search container skeleton with filters\n */\nexport function SearchContainerSkeleton({ \n  className,\n  showFilters = true \n}: PageLayoutSkeletonProps & { showFilters?: boolean }) {\n  return (\n    <div className={cn(\"space-y-4\", className || \"\")}>\n      {/* Search input and button */}\n      <div className=\"flex gap-2 md:gap-4\">\n        <InteractiveSkeleton.Input className=\"flex-1\" />\n        <InteractiveSkeleton.Button className=\"w-32 md:w-40\" />\n      </div>\n\n      {/* Filter chips */}\n      {showFilters && (\n        <div className=\"flex gap-2 flex-wrap\">\n          <InteractiveSkeleton.Chip />\n          <InteractiveSkeleton.Chip className=\"w-24\" />\n          <InteractiveSkeleton.Chip className=\"w-16\" />\n          <InteractiveSkeleton.Chip className=\"w-20\" />\n        </div>\n      )}\n    </div>\n  )\n}\n\n/**\n * Category sidebar skeleton for filtering pages\n */\nexport function CategorySidebarSkeleton({ className }: PageLayoutSkeletonProps) {\n  // Matches the actual MultiLevelNavigation sidebar with folder/file icons, README badges, and chevrons\n  const items = [\n    { type: 'file', width: 'w-20', hasBadge: false },\n    { type: 'folder', width: 'w-24', hasBadge: true },\n    { type: 'folder', width: 'w-20', hasBadge: true },\n    { type: 'folder', width: 'w-24', hasBadge: true },\n    { type: 'folder', width: 'w-16', hasBadge: true },\n    { type: 'folder', width: 'w-20', hasBadge: true },\n    { type: 'folder', width: 'w-28', hasBadge: true },\n    { type: 'folder', width: 'w-16', hasBadge: true },\n    { type: 'folder', width: 'w-24', hasBadge: true },\n    { type: 'file', width: 'w-28' },\n    { type: 'file', width: 'w-36' },\n  ]\n\n  return (\n    <div className={cn(\"w-full lg:w-[320px]\", className)}>\n      {/* DATA ROOM label */}\n      <UnifiedSkeleton className=\"h-[14px] w-24 rounded mb-4\" />\n\n      {/* Navigation items — each has card background matching actual MultiLevelNavigation */}\n      <div className=\"space-y-1.5\">\n        {items.map((item, index) => (\n          <div\n            key={index}\n            className={cn(\n              \"flex items-center justify-between py-4 px-4 rounded-lg border border-ods-border min-h-[50px]\",\n              index === 0 ? \"bg-ods-accent/20\" : \"bg-ods-card\"\n            )}\n          >\n            <div className=\"flex items-center gap-2.5\">\n              <UnifiedSkeleton className=\"w-4 h-4 shrink-0 rounded\" />\n              <UnifiedSkeleton className={`h-[14px] ${item.width} rounded`} />\n            </div>\n            <div className=\"flex items-center gap-2 h-[18px]\">\n              {item.hasBadge && <UnifiedSkeleton className=\"h-[18px] w-14 rounded\" />}\n              {item.type === 'folder' && <UnifiedSkeleton className=\"w-4 h-4 rounded\" />}\n            </div>\n          </div>\n        ))}\n      </div>\n    </div>\n  )\n}\n\n/**\n * Breadcrumb navigation skeleton\n */\nexport function BreadcrumbSkeleton({ className }: PageLayoutSkeletonProps) {\n  return (\n    <div className={cn(\"flex items-center space-x-1 mb-6\", className)}>\n      <TextSkeleton.Caption className=\"w-16\" />\n      <UnifiedSkeleton variant=\"default\" className=\"w-4 h-4 rounded-full\" />\n      <TextSkeleton.Caption className=\"w-24\" />\n      <UnifiedSkeleton variant=\"default\" className=\"w-4 h-4 rounded-full\" />\n      <TextSkeleton.Caption className=\"w-32\" />\n      <UnifiedSkeleton variant=\"default\" className=\"w-4 h-4 rounded-full\" />\n      <TextSkeleton.Caption className=\"w-24\" />\n    </div>\n  )\n}\n\n/**\n * Results header skeleton with count and sorting\n */\nexport function ResultsHeaderSkeleton({ className }: PageLayoutSkeletonProps) {\n  return (\n    <div className={cn(\"flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-6\", className)}>\n      <div className=\"space-y-1\">\n        <TextSkeleton.Body className=\"w-48\" />\n        {/* <TextSkeleton.Caption className=\"w-32\" /> */}\n      </div>\n    </div>\n  )\n}\n\n/**\n * Two-column layout skeleton (sidebar + main content)\n */\nexport function TwoColumnLayoutSkeleton({\n  className,\n  sidebarContent,\n  mainContent,\n  sidebarPosition = 'left'\n}: PageLayoutSkeletonProps & {\n  sidebarContent?: React.ReactNode\n  mainContent?: React.ReactNode\n  sidebarPosition?: 'left' | 'right'\n}) {\n  const sidebar = sidebarContent || <CategorySidebarSkeleton />\n  const main = mainContent || <CardSkeletonGrid count={6} />\n\n  return (\n    <div className={cn(\n      \"grid grid-cols-1 lg:grid-cols-[320px_1fr] gap-6 lg:gap-8\",\n      sidebarPosition === 'right' && \"lg:grid-cols-[1fr_320px]\",\n      className\n    )}>\n      {sidebarPosition === 'left' ? (\n        <>\n          <aside className=\"order-2 lg:order-1\">{sidebar}</aside>\n          <main className=\"order-1 lg:order-2\">{main}</main>\n        </>\n      ) : (\n        <>\n          <main className=\"order-1\">{main}</main>\n          <aside className=\"order-2\">{sidebar}</aside>\n        </>\n      )}\n    </div>\n  )\n}\n\n/**\n * Article/blog post layout skeleton\n */\nexport function ArticleLayoutSkeleton({ className }: PageLayoutSkeletonProps) {\n  return (\n    <article className={cn(\"max-w-4xl mx-auto\", className)}>\n      {/* Article header */}\n      <header className=\"mb-8 md:mb-12 space-y-4 md:space-y-6\">\n        {/* Category/tags */}\n        <div className=\"flex gap-2\">\n          <InteractiveSkeleton.Chip />\n          <InteractiveSkeleton.Chip className=\"w-16\" />\n        </div>\n        \n        {/* Title */}\n        <div className=\"space-y-3\">\n          <TextSkeleton.Heading className=\"w-full\" />\n          <TextSkeleton.Heading className=\"w-3/4\" />\n        </div>\n        \n        {/* Metadata */}\n        <div className=\"flex items-center gap-4 pt-4 border-t border-ods-divider\">\n          <div className=\"flex items-center gap-2\">\n            <MediaSkeleton.Avatar size=\"sm\" />\n            <TextSkeleton.Caption className=\"w-24\" />\n          </div>\n          <TextSkeleton.Caption className=\"w-20\" />\n          <TextSkeleton.Caption className=\"w-16\" />\n        </div>\n      </header>\n\n      {/* Featured image */}\n      <div className=\"mb-8 md:mb-12\">\n        <MediaSkeleton.CardImage className=\"w-full h-64 md:h-96\" />\n      </div>\n\n      {/* Article content */}\n      <div className=\"prose prose-invert max-w-none space-y-6\">\n        {/* Paragraphs */}\n        {Array.from({ length: 12 }).map((_, index) => (\n          <div key={index} className=\"space-y-2\">\n            <TextSkeleton.Body className=\"w-full\" />\n            <TextSkeleton.Body className=\"w-full\" />\n            <TextSkeleton.Body className=\"w-5/6\" />\n            <TextSkeleton.Body className=\"w-3/4\" />\n          </div>\n        ))}\n        \n        {/* Subheading in content */}\n        <div className=\"py-4\">\n          <TextSkeleton.Subheading className=\"w-2/3 mb-4\" />\n          <div className=\"space-y-2\">\n            <TextSkeleton.Body className=\"w-full\" />\n            <TextSkeleton.Body className=\"w-4/5\" />\n          </div>\n        </div>\n      </div>\n    </article>\n  )\n}\n\n/**\n * Vendor Detail Layout Skeleton - Complete vendor detail page structure\n * Matches the refactored vendor detail page with proper platform colors and responsive layout\n */\nexport function VendorDetailLayoutSkeleton({ className }: PageLayoutSkeletonProps) {\n  return (\n    <main className={cn(\"bg-ods-bg min-h-screen\", className)}>\n      <div className=\"max-w-[1920px] mx-auto px-6 md:px-20 py-6 md:py-10\">\n        {/* Breadcrumb */}\n        <BreadcrumbSkeleton className=\"mb-6\" />\n\n        {/* Main Layout Container */}\n        <div className=\"flex flex-col lg:flex-row lg:gap-10\">\n          {/* Left Content Area */}\n          <div className=\"flex-1 min-w-0\">\n            \n            {/* Vendor Hero Section */}\n            <div className=\"mb-10\">\n              {/* Header - Logo and Title Side by Side */}\n              <div className=\"flex gap-6 mb-6\">\n                <div className=\"w-20 h-20 bg-ods-card border border-ods-border rounded-lg animate-pulse flex-shrink-0\"></div>\n                \n                <div className=\"flex flex-col gap-3\">\n                  <div className=\"flex flex-col gap-2\">\n                    {/* Large title skeleton */}\n                    <div className=\"h-12 md:h-16 lg:h-20 bg-ods-skeleton rounded animate-pulse w-80 max-w-full\"></div>\n                    {/* Category text */}\n                    <div className=\"h-5 md:h-6 bg-ods-skeleton rounded animate-pulse w-32\"></div>\n                  </div>\n                  \n                  {/* Pricing tags */}\n                  <div className=\"flex items-center gap-2\">\n                    <div className=\"h-8 bg-ods-skeleton rounded animate-pulse w-20\"></div>\n                    <div className=\"h-8 bg-ods-skeleton rounded animate-pulse w-16\"></div>\n                  </div>\n                </div>\n              </div>\n\n              {/* Vendor Image Display Skeleton */}\n              <div className=\"w-full h-[300px] md:h-[400px] lg:h-[500px] bg-ods-card border border-ods-border rounded-lg animate-pulse mb-2\"></div>\n              <div className=\"text-center\">\n                <div className=\"h-4 bg-ods-skeleton rounded animate-pulse w-24 mx-auto\"></div>\n              </div>\n            </div>\n\n            {/* Mobile Sidebar - Show on mobile only, positioned after title */}\n            <div className=\"lg:hidden mb-10\">\n              <div className=\"space-y-4\">\n                {/* Deploy Button */}\n                <div className=\"h-12 bg-ods-card border border-ods-border rounded-lg animate-pulse\"></div>\n                \n                {/* Voting Buttons */}\n                <div className=\"space-y-2\">\n                  <div className=\"h-12 bg-ods-card border border-ods-border rounded-lg animate-pulse\"></div>\n                  <div className=\"h-12 bg-ods-card border border-ods-border rounded-lg animate-pulse\"></div>\n                </div>\n                \n                {/* GitHub Score Section */}\n                <div className=\"border border-ods-border rounded-lg overflow-hidden\">\n                  {/* Header */}\n                  <div className=\"bg-ods-card p-4 flex items-center gap-3\">\n                    <div className=\"w-8 h-8 bg-ods-skeleton rounded animate-pulse\"></div>\n                    <div className=\"flex flex-col gap-1\">\n                      <div className=\"h-4 bg-ods-skeleton rounded animate-pulse w-20\"></div>\n                      <div className=\"h-6 bg-ods-skeleton rounded animate-pulse w-12\"></div>\n                    </div>\n                  </div>\n                  \n                  {/* Stats */}\n                  <div className=\"p-4 space-y-3\">\n                    {Array.from({ length: 4 }).map((_, i) => (\n                      <div key={i} className=\"flex items-center gap-3\">\n                        <div className=\"w-6 h-6 bg-ods-skeleton rounded animate-pulse\"></div>\n                        <div className=\"flex items-end gap-1\">\n                          <div className=\"h-4 bg-ods-skeleton rounded animate-pulse w-12\"></div>\n                          <div className=\"h-3 bg-ods-skeleton rounded animate-pulse w-8\"></div>\n                        </div>\n                      </div>\n                    ))}\n                  </div>\n                </div>\n                \n                {/* Action Buttons */}\n                <div className=\"space-y-3\">\n                  <div className=\"h-12 bg-ods-card border border-ods-border rounded-lg animate-pulse\"></div>\n                  <div className=\"h-12 bg-ods-card border border-ods-border rounded-lg animate-pulse\"></div>\n                  <div className=\"h-12 bg-ods-card border border-ods-border rounded-lg animate-pulse\"></div>\n                </div>\n              </div>\n            </div>\n\n            {/* Alternatives Container */}\n            <div className=\"flex flex-col gap-6 mb-20\">\n              <div className=\"h-8 md:h-10 bg-ods-skeleton rounded animate-pulse w-48\"></div>\n              \n              {/* Open Source Alternatives */}\n              <div className=\"flex flex-col gap-4\">\n                <div className=\"h-5 bg-ods-skeleton rounded animate-pulse w-48\"></div>\n                <div className=\"flex flex-wrap gap-2\">\n                  {Array.from({ length: 6 }).map((_, i) => (\n                    <div key={i} className=\"h-10 bg-ods-card border border-ods-border rounded-lg animate-pulse w-24\"></div>\n                  ))}\n                </div>\n              </div>\n\n              {/* Commercial Alternatives */}\n              <div className=\"flex flex-col gap-4\">\n                <div className=\"h-5 bg-ods-skeleton rounded animate-pulse w-52\"></div>\n                <div className=\"flex flex-wrap gap-2\">\n                  {Array.from({ length: 4 }).map((_, i) => (\n                    <div key={i} className=\"h-10 bg-ods-card border border-ods-border rounded-lg animate-pulse w-28\"></div>\n                  ))}\n                </div>\n              </div>\n            </div>\n\n            {/* About Section */}\n            <div className=\"flex flex-col gap-6 mb-20\">\n              <div className=\"h-8 md:h-10 bg-ods-skeleton rounded animate-pulse w-64\"></div>\n              \n              <div className=\"bg-ods-card border border-ods-border rounded-lg p-6 md:p-8\">\n                <div className=\"space-y-4\">\n                  <div className=\"h-6 bg-ods-skeleton rounded animate-pulse\"></div>\n                  <div className=\"h-6 bg-ods-skeleton rounded animate-pulse\"></div>\n                  <div className=\"h-6 bg-ods-skeleton rounded animate-pulse w-5/6\"></div>\n                  <div className=\"h-6 bg-ods-skeleton rounded animate-pulse w-4/5\"></div>\n                </div>\n              </div>\n            </div>\n\n            {/* Key Features Section */}\n            <div className=\"flex flex-col gap-6 mb-20\">\n              <div className=\"h-8 md:h-10 bg-ods-skeleton rounded animate-pulse w-72\"></div>\n              <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n                {Array.from({ length: 6 }).map((_, i) => (\n                  <div key={i} className=\"bg-ods-card border border-ods-border rounded-lg p-4 flex gap-4\">\n                    <div className=\"w-12 h-12 bg-ods-skeleton rounded flex-shrink-0 animate-pulse\"></div>\n                    <div className=\"flex-1 space-y-2\">\n                      <div className=\"h-5 bg-ods-skeleton rounded animate-pulse w-3/4\"></div>\n                      <div className=\"h-4 bg-ods-skeleton rounded animate-pulse\"></div>\n                    </div>\n                  </div>\n                ))}\n              </div>\n            </div>\n\n            {/* Pros and Cons Section */}\n            <div className=\"flex flex-col gap-6 mb-20\">\n              <div className=\"h-8 md:h-10 bg-ods-skeleton rounded animate-pulse w-80\"></div>\n              <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n                {/* Pros Column */}\n                <div className=\"flex flex-col gap-6\">\n                  <div className=\"h-5 bg-ods-skeleton rounded animate-pulse w-16\"></div>\n                  {Array.from({ length: 3 }).map((_, i) => (\n                    <div key={i} className=\"bg-ods-card border border-ods-border rounded-lg p-4 flex gap-3\">\n                      <div className=\"w-12 h-12 bg-ods-bg border border-ods-border rounded flex-shrink-0 animate-pulse\"></div>\n                      <div className=\"flex-1 space-y-2\">\n                        <div className=\"h-5 bg-ods-skeleton rounded animate-pulse w-2/3\"></div>\n                        <div className=\"h-4 bg-ods-skeleton rounded animate-pulse\"></div>\n                      </div>\n                    </div>\n                  ))}\n                </div>\n                {/* Cons Column */}\n                <div className=\"flex flex-col gap-6\">\n                  <div className=\"h-5 bg-ods-skeleton rounded animate-pulse w-16\"></div>\n                  {Array.from({ length: 3 }).map((_, i) => (\n                    <div key={i} className=\"bg-ods-card border border-ods-border rounded-lg p-4 flex gap-3\">\n                      <div className=\"w-12 h-12 bg-ods-bg border border-ods-border rounded flex-shrink-0 animate-pulse\"></div>\n                      <div className=\"flex-1 space-y-2\">\n                        <div className=\"h-5 bg-ods-skeleton rounded animate-pulse w-2/3\"></div>\n                        <div className=\"h-4 bg-ods-skeleton rounded animate-pulse\"></div>\n                      </div>\n                    </div>\n                  ))}\n                </div>\n              </div>\n            </div>\n\n            {/* Alternatives Section */}\n            <div className=\"flex flex-col gap-6 mb-20\">\n              <div className=\"h-8 md:h-10 bg-ods-skeleton rounded animate-pulse w-72\"></div>\n              <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n                {Array.from({ length: 6 }).map((_, i) => (\n                  <div key={i} className=\"bg-ods-card border border-ods-border rounded-lg p-6 space-y-4\">\n                    {/* Header */}\n                    <div className=\"flex items-start gap-4\">\n                      <div className=\"w-16 h-16 bg-ods-bg border border-ods-border rounded-lg animate-pulse flex-shrink-0\"></div>\n                      <div className=\"flex-1 space-y-2\">\n                        <div className=\"h-6 bg-ods-skeleton rounded animate-pulse w-3/4\"></div>\n                        <div className=\"h-4 bg-ods-skeleton rounded animate-pulse w-1/2\"></div>\n                      </div>\n                      <div className=\"h-6 bg-ods-skeleton rounded animate-pulse w-20\"></div>\n                    </div>\n                    \n                    {/* Description */}\n                    <div className=\"space-y-2\">\n                      <div className=\"h-4 bg-ods-skeleton rounded animate-pulse\"></div>\n                      <div className=\"h-4 bg-ods-skeleton rounded animate-pulse w-5/6\"></div>\n                      <div className=\"h-4 bg-ods-skeleton rounded animate-pulse w-4/5\"></div>\n                    </div>\n                    \n                    {/* Footer */}\n                    <div className=\"flex items-center justify-between pt-2 border-t border-ods-border\">\n                      <div className=\"h-4 bg-ods-skeleton rounded animate-pulse w-16\"></div>\n                      <div className=\"h-4 bg-ods-skeleton rounded animate-pulse w-20\"></div>\n                    </div>\n                  </div>\n                ))}\n              </div>\n              {/* Show All Button */}\n              <div className=\"h-12 bg-ods-skeleton rounded-md animate-pulse\"></div>\n            </div>\n\n            {/* Comments Section */}\n            <div className=\"flex flex-col gap-6 mb-20\">\n              <div className=\"h-8 md:h-10 bg-ods-skeleton rounded animate-pulse w-64\"></div>\n              \n              {/* Comment Form Skeleton */}\n              <div className=\"space-y-3\">\n                <div className=\"flex gap-3\">\n                  <div className=\"flex-1 bg-ods-bg border border-ods-border rounded-lg overflow-hidden\">\n                    {/* Title Section */}\n                    <div className=\"border-b border-ods-border p-3\">\n                      <div className=\"h-3 bg-ods-skeleton rounded animate-pulse w-8 mb-2\"></div>\n                      <div className=\"h-6 bg-ods-skeleton rounded animate-pulse w-full\"></div>\n                    </div>\n                    \n                    {/* Description Section */}\n                    <div className=\"p-3\">\n                      <div className=\"h-3 bg-ods-skeleton rounded animate-pulse w-16 mb-2\"></div>\n                      <div className=\"space-y-2\">\n                        <div className=\"h-4 bg-ods-skeleton rounded animate-pulse w-full\"></div>\n                        <div className=\"h-4 bg-ods-skeleton rounded animate-pulse w-3/4\"></div>\n                        <div className=\"h-4 bg-ods-skeleton rounded animate-pulse w-1/2\"></div>\n                      </div>\n                    </div>\n                  </div>\n                  \n                  {/* Send Button */}\n                  <div className=\"flex items-stretch\">\n                    <div className=\"h-[120px] w-20 bg-ods-skeleton rounded-lg animate-pulse\"></div>\n                  </div>\n                </div>\n              </div>\n              \n              {/* Sample Comment Cards */}\n              <div className=\"space-y-4\">\n                {Array.from({ length: 3 }).map((_, i) => (\n                  <div key={i} className=\"bg-ods-card border border-ods-border rounded-lg p-4\">\n                    {/* Comment Header */}\n                    <div className=\"flex flex-col gap-3 mb-3 min-[420px]:flex-row min-[420px]:items-center min-[420px]:justify-between min-[420px]:gap-2\">\n                      <div className=\"flex items-center gap-2\">\n                        {/* User Info */}\n                        <div className=\"flex items-center gap-2 bg-ods-card border border-ods-border rounded-lg py-2 px-3\">\n                          <div className=\"w-8 h-8 bg-ods-skeleton rounded-lg animate-pulse\"></div>\n                          <div className=\"h-4 bg-ods-skeleton rounded animate-pulse w-20\"></div>\n                        </div>\n                        \n                        {/* Timestamp */}\n                        <div className=\"h-4 bg-ods-skeleton rounded animate-pulse w-12\"></div>\n                      </div>\n                    </div>\n                    \n                    {/* Comment Content */}\n                    <div className=\"space-y-2\">\n                      <div className=\"h-6 bg-ods-skeleton rounded animate-pulse w-2/3\"></div>\n                      <div className=\"space-y-1\">\n                        <div className=\"h-4 bg-ods-skeleton rounded animate-pulse w-full\"></div>\n                        <div className=\"h-4 bg-ods-skeleton rounded animate-pulse w-4/5\"></div>\n                        <div className=\"h-4 bg-ods-skeleton rounded animate-pulse w-3/5\"></div>\n                      </div>\n                    </div>\n                  </div>\n                ))}\n              </div>\n            </div>\n          </div>\n\n          {/* Right Sidebar - Desktop Only */}\n          <div className=\"hidden lg:block w-[290px] flex-shrink-0\">\n            <div className=\"space-y-4\">\n              {/* Deploy Button */}\n              <div className=\"h-12 bg-ods-card border border-ods-border rounded-lg animate-pulse\"></div>\n              \n              {/* Voting Buttons */}\n              <div className=\"space-y-2\">\n                <div className=\"h-12 bg-ods-card border border-ods-border rounded-lg animate-pulse\"></div>\n                <div className=\"h-12 bg-ods-card border border-ods-border rounded-lg animate-pulse\"></div>\n              </div>\n              \n              {/* GitHub Score Section */}\n              <div className=\"border border-ods-border rounded-lg overflow-hidden\">\n                {/* Header */}\n                <div className=\"bg-ods-card p-4 flex items-center gap-3\">\n                  <div className=\"w-8 h-8 bg-ods-skeleton rounded animate-pulse\"></div>\n                  <div className=\"flex flex-col gap-1\">\n                    <div className=\"h-4 bg-ods-skeleton rounded animate-pulse w-20\"></div>\n                    <div className=\"h-6 bg-ods-skeleton rounded animate-pulse w-12\"></div>\n                  </div>\n                </div>\n                \n                {/* Stats */}\n                <div className=\"p-4 space-y-3\">\n                  {Array.from({ length: 4 }).map((_, i) => (\n                    <div key={i} className=\"flex items-center gap-3\">\n                      <div className=\"w-6 h-6 bg-ods-skeleton rounded animate-pulse\"></div>\n                      <div className=\"flex items-end gap-1\">\n                        <div className=\"h-4 bg-ods-skeleton rounded animate-pulse w-12\"></div>\n                        <div className=\"h-3 bg-ods-skeleton rounded animate-pulse w-8\"></div>\n                      </div>\n                    </div>\n                  ))}\n                </div>\n              </div>\n              \n              {/* Action Buttons */}\n              <div className=\"space-y-3\">\n                <div className=\"h-12 bg-ods-card border border-ods-border rounded-lg animate-pulse\"></div>\n                <div className=\"h-12 bg-ods-card border border-ods-border rounded-lg animate-pulse\"></div>\n                <div className=\"h-12 bg-ods-card border border-ods-border rounded-lg animate-pulse\"></div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </main>\n  )\n}\n\n/**\n * Stats/features section skeleton for homepage\n */\nexport function StatsSectionSkeleton({ \n  className,\n  columns = 3 \n}: PageLayoutSkeletonProps & { columns?: number }) {\n  return (\n    <div className={cn(\n      \"grid gap-6 mb-12 md:mb-16\",\n      columns === 3 && \"grid-cols-1 md:grid-cols-3\",\n      columns === 4 && \"grid-cols-1 md:grid-cols-2 lg:grid-cols-4\",\n      className\n    )}>\n      {Array.from({ length: columns }).map((_, i) => (\n        <div key={i} className=\"bg-ods-card border border-ods-border rounded-lg p-6\">\n          <div className=\"space-y-4\">\n            <MediaSkeleton.Icon size=\"lg\" />\n            <TextSkeleton.Subheading className=\"w-3/4\" />\n            <div className=\"space-y-2\">\n              <TextSkeleton.Body className=\"w-full\" />\n              <TextSkeleton.Body className=\"w-3/4\" />\n            </div>\n          </div>\n        </div>\n      ))}\n    </div>\n  )\n}\n\n\n/**\n * Blog Card Grid Skeleton - Always displays exactly 4 blog card skeletons\n * Used for consistent blog page layout with predictable height\n */\nexport function BlogCardGridSkeleton({ className }: PageLayoutSkeletonProps) {\n  return (\n    <div className={cn(\"space-y-6\", className)}>\n      <CardSkeletonGrid \n        count={4}\n        variant=\"blog\"\n        className=\"grid-cols-1 md:grid-cols-2\"\n      />\n    </div>\n  )\n}\n\n/**\n * Vendor Grid Skeleton - Always displays exactly 12 vendor card skeletons\n * Used for consistent vendor page layout with predictable height\n */\nexport function VendorGridSkeleton({ className }: PageLayoutSkeletonProps) {\n  return (\n    <div className={cn(\"space-y-6\", className)}>\n      <CardSkeletonGrid \n        count={12}\n        variant=\"vendor\"\n        className=\"grid-cols-1 md:grid-cols-2 lg:grid-cols-3\"\n      />\n    </div>\n  )\n}\n\n/**\n * Slack Community Section Skeleton\n * Matches SlackCommunity component structure with title, channel list, and chat interface\n */\nexport function SlackCommunitySkeleton() {\n  return (\n    <section \n      className=\"w-full bg-ods-bg px-4 md:px-20 lg:px-20 py-12 md:py-20\"\n      aria-label=\"Slack Community Loading\"\n    >\n      {/* Frame 651 Container */}\n      <div className=\"w-full flex flex-col gap-4 md:gap-6\">\n        \n        {/* Title Skeleton */}\n        <div className=\"w-full\">\n          <div className=\"h-8 md:h-12 lg:h-14 bg-ods-skeleton rounded-lg animate-pulse max-w-md\"></div>\n        </div>\n\n        {/* Content Area - Channel List + Chat Interface */}\n        <div className=\"w-full flex flex-col lg:flex-row lg:justify-end lg:items-start gap-4 md:gap-6 min-w-0\">\n          \n          {/* Channel List Skeleton */}\n          <div className=\"w-full lg:w-[290px] lg:max-w-[290px] bg-ods-bg border border-ods-border rounded flex flex-col flex-shrink-0 overflow-hidden animate-pulse\">\n            <div className=\"p-4 space-y-4\">\n              {/* Header */}\n              <div className=\"flex flex-col gap-2\">\n                <div className=\"flex items-center justify-between\">\n                  <div className=\"h-4 bg-ods-skeleton rounded w-24\"></div>\n                  <div className=\"h-3 bg-ods-skeleton rounded w-16\"></div>\n                </div>\n                <div className=\"h-3 bg-ods-skeleton rounded w-32\"></div>\n              </div>\n\n              {/* Channels */}\n              <div className=\"flex flex-col gap-1\">\n                <div className=\"h-3 bg-ods-skeleton rounded w-16 mb-2\"></div>\n                {Array.from({ length: 5 }).map((_, i) => (\n                  <div key={i} className=\"flex items-center gap-2 px-2 py-3 min-h-[48px]\">\n                    <div className=\"w-4 h-4 bg-ods-skeleton rounded\"></div>\n                    <div className=\"flex-1\">\n                      <div className=\"h-3 bg-ods-skeleton rounded w-24 mb-1\"></div>\n                      <div className=\"h-2 bg-ods-skeleton rounded w-16\"></div>\n                    </div>\n                  </div>\n                ))}\n              </div>\n\n              {/* Stats */}\n              <div className=\"border-t border-ods-border pt-3 mt-2\">\n                <div className=\"space-y-2\">\n                  <div className=\"flex justify-between\">\n                    <div className=\"h-3 bg-ods-skeleton rounded w-16\"></div>\n                    <div className=\"h-3 bg-ods-skeleton rounded w-8\"></div>\n                  </div>\n                  <div className=\"flex justify-between\">\n                    <div className=\"h-3 bg-ods-skeleton rounded w-20\"></div>\n                    <div className=\"h-3 bg-ods-skeleton rounded w-8\"></div>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n\n          {/* Chat Interface Skeleton */}\n          <div className=\"flex-1 min-h-[450px] h-[450px] md:min-h-[500px] md:h-[500px] lg:min-h-[600px] lg:h-[600px] bg-ods-card border border-ods-border rounded-lg flex flex-col min-w-0 animate-pulse\">\n            \n            {/* Header */}\n            <div className=\"flex items-center justify-between p-4 md:p-6 border-b border-ods-border bg-ods-bg h-[56px] md:h-[60px] flex-shrink-0\">\n              <div className=\"flex items-center gap-2\">\n                <div className=\"h-3 md:h-4 bg-ods-skeleton rounded w-3 md:w-4\"></div>\n                <div className=\"h-3 md:h-4 bg-ods-skeleton rounded w-24 md:w-32\"></div>\n              </div>\n              <div className=\"h-3 bg-ods-skeleton rounded w-12 md:w-16\"></div>\n            </div>\n\n            {/* Messages */}\n            <div className=\"flex-1 bg-ods-bg p-4 md:p-6 space-y-3 md:space-y-4 overflow-hidden min-h-[280px] md:min-h-[320px] lg:min-h-[420px]\">\n              {Array.from({ length: 6 }).map((_, i) => (\n                <div key={i} className=\"flex gap-2 md:gap-3\">\n                  <div className=\"w-10 h-10 md:w-12 md:h-12 bg-ods-skeleton rounded-lg flex-shrink-0\"></div>\n                  <div className=\"flex-1\">\n                    <div className=\"flex items-center gap-2 mb-1\">\n                      <div className=\"h-3 bg-ods-skeleton rounded w-16 md:w-20\"></div>\n                      <div className=\"h-3 bg-ods-skeleton rounded w-12 md:w-16\"></div>\n                    </div>\n                    <div className=\"h-3 md:h-4 bg-ods-skeleton rounded w-full\"></div>\n                  </div>\n                </div>\n              ))}\n            </div>\n\n            {/* Input Area */}\n            <div className=\"p-4 md:p-6 border-t border-ods-border bg-ods-card h-[72px] md:h-[80px] flex-shrink-0\">\n              <div className=\"flex items-end gap-3 justify-center md:justify-end\">\n                <div className=\"h-10 md:h-12 bg-ods-skeleton rounded-lg w-28 md:w-32\"></div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n} ","import type React from \"react\"\nimport { cn } from \"../../utils/cn\"\n\ninterface UnifiedSkeletonProps extends React.HTMLAttributes<HTMLDivElement> {\n  className?: string\n  variant?: 'default' | 'text' | 'circular' | 'rectangular'\n  width?: string | number\n  height?: string | number\n  /**\n   * Disable animation for users who prefer reduced motion\n   */\n  animate?: boolean\n  /**\n   * Accessibility label for screen readers\n   */\n  'aria-label'?: string\n}\n\n/**\n * UnifiedSkeleton Component\n * \n * Base skeleton component with consistent styling across the application.\n * \n * Design Specifications:\n * - Color: #2A2A2A base with #3A3A3A pulse highlight\n * - Animation: 1.5s pulse duration, ease-in-out timing\n * - Responsive: Mobile-first approach with proper scaling\n * - Accessibility: ARIA labels and reduced motion support\n * \n * @param variant - Shape variant: default (rounded), text (rounded-sm), circular, rectangular\n * @param width - Custom width (defaults to full width)\n * @param height - Custom height (defaults to auto based on variant)\n * @param animate - Enable/disable animation (respects prefers-reduced-motion)\n * @param aria-label - Accessibility label for screen readers\n */\nexport function UnifiedSkeleton({\n  className,\n  variant = 'default',\n  width,\n  height,\n  animate = true,\n  'aria-label': ariaLabel,\n  ...props\n}: UnifiedSkeletonProps) {\n  const baseClasses = \"bg-ods-skeleton\"\n  \n  const variantClasses = {\n    default: \"rounded-md\",\n    text: \"rounded-sm\",\n    circular: \"rounded-full\",\n    rectangular: \"rounded-none\"\n  }\n  \n  const defaultHeights = {\n    default: \"h-4\",\n    text: \"h-4\", \n    circular: \"h-12 w-12\",\n    rectangular: \"h-4\"\n  }\n  \n  const animationClasses = animate \n    ? \"animate-pulse motion-reduce:animate-none\" \n    : \"\"\n  \n  const style: React.CSSProperties = {\n    width: width,\n    height: height,\n  }\n  \n  return (\n    <div\n      className={cn(\n        baseClasses,\n        variantClasses[variant],\n        !height && !style.height && defaultHeights[variant],\n        !width && !style.width && variant !== 'circular' && \"w-full\",\n        animationClasses,\n        className\n      )}\n      style={style}\n      role=\"status\"\n      aria-label={ariaLabel || \"Loading content\"}\n      {...props}\n    />\n  )\n}\n\n/**\n * Text skeleton variants for consistent typography loading\n */\nexport const TextSkeleton = {\n  /**\n   * Large heading skeleton (h1, h2)\n   */\n  Heading: ({ className, ...props }: Omit<UnifiedSkeletonProps, 'variant'>) => (\n    <UnifiedSkeleton \n      variant=\"text\" \n      className={cn(\"h-8 md:h-10 lg:h-12\", className)} \n      aria-label=\"Loading heading\"\n      {...props} \n    />\n  ),\n  \n  /**\n   * Medium heading skeleton (h3, h4)\n   */\n  Subheading: ({ className, ...props }: Omit<UnifiedSkeletonProps, 'variant'>) => (\n    <UnifiedSkeleton \n      variant=\"text\" \n      className={cn(\"h-6 md:h-7 lg:h-8\", className)} \n      aria-label=\"Loading subheading\"\n      {...props} \n    />\n  ),\n  \n  /**\n   * Body text skeleton\n   */\n  Body: ({ className, ...props }: Omit<UnifiedSkeletonProps, 'variant'>) => (\n    <UnifiedSkeleton \n      variant=\"text\" \n      className={cn(\"h-4 md:h-5\", className)} \n      aria-label=\"Loading text\"\n      {...props} \n    />\n  ),\n  \n  /**\n   * Small text skeleton (captions, metadata)\n   */\n  Caption: ({ className, ...props }: Omit<UnifiedSkeletonProps, 'variant'>) => (\n    <UnifiedSkeleton \n      variant=\"text\" \n      className={cn(\"h-3 md:h-4\", className)} \n      aria-label=\"Loading caption\"\n      {...props} \n    />\n  ),\n}\n\n/**\n * Interactive element skeletons\n */\nexport const InteractiveSkeleton = {\n  /**\n   * Button skeleton with proper touch targets\n   */\n  Button: ({ className, ...props }: Omit<UnifiedSkeletonProps, 'variant'>) => (\n    <UnifiedSkeleton \n      variant=\"default\" \n      className={cn(\"h-10 md:h-12 w-32 md:w-40\", className)} \n      aria-label=\"Loading button\"\n      {...props} \n    />\n  ),\n  \n  /**\n   * Input field skeleton\n   */\n  Input: ({ className, ...props }: Omit<UnifiedSkeletonProps, 'variant'>) => (\n    <UnifiedSkeleton \n      variant=\"default\" \n      className={cn(\"h-10 md:h-12 w-full\", className)} \n      aria-label=\"Loading input field\"\n      {...props} \n    />\n  ),\n  \n  /**\n   * Filter chip skeleton\n   */\n  Chip: ({ className, ...props }: Omit<UnifiedSkeletonProps, 'variant'>) => (\n    <UnifiedSkeleton \n      variant=\"default\" \n      className={cn(\"h-8 w-20 rounded-full\", className)} \n      aria-label=\"Loading filter\"\n      {...props} \n    />\n  ),\n}\n\n/**\n * Media skeletons for images and icons\n */\nexport const MediaSkeleton = {\n  /**\n   * Avatar/profile image skeleton\n   */\n  Avatar: ({ size = 'md', className, ...props }: Omit<UnifiedSkeletonProps, 'variant'> & { size?: 'sm' | 'md' | 'lg' }) => {\n    const sizeClasses = {\n      sm: \"h-8 w-8\",\n      md: \"h-12 w-12\", \n      lg: \"h-16 w-16\"\n    }\n    \n    return (\n      <UnifiedSkeleton \n        variant=\"circular\" \n        className={cn(sizeClasses[size], className)} \n        aria-label=\"Loading profile image\"\n        {...props} \n      />\n    )\n  },\n  \n  /**\n   * Card image skeleton (1200×630 OG aspect — matches entity-cards default\n   * so loading state never causes a height shift when card data arrives)\n   */\n  CardImage: ({ className, ...props }: Omit<UnifiedSkeletonProps, 'variant'>) => (\n    <UnifiedSkeleton\n      variant=\"default\"\n      className={cn(\"aspect-[1200/630] w-full\", className)}\n      aria-label=\"Loading image\"\n      {...props}\n    />\n  ),\n  \n  /**\n   * Icon skeleton\n   */\n  Icon: ({ size = 'md', className, ...props }: Omit<UnifiedSkeletonProps, 'variant'> & { size?: 'sm' | 'md' | 'lg' }) => {\n    const sizeClasses = {\n      sm: \"h-4 w-4\",\n      md: \"h-6 w-6\",\n      lg: \"h-8 w-8\"\n    }\n    \n    return (\n      <UnifiedSkeleton \n        variant=\"default\" \n        className={cn(sizeClasses[size], className)} \n        aria-label=\"Loading icon\"\n        {...props} \n    />\n    )\n  },\n} ","import type React from \"react\"\nimport { cn } from \"../../utils/cn\"\nimport { UnifiedSkeleton, TextSkeleton, MediaSkeleton, InteractiveSkeleton } from \"./unified-skeleton\"\n\ninterface CardSkeletonProps {\n  className?: string\n  /**\n   * Card layout variant\n   */\n  variant?: 'vendor' | 'blog' | 'category' | 'alternative'\n  /**\n   * Show action buttons area\n   */\n  showActions?: boolean\n  /**\n   * Show metadata footer\n   */\n  showMetadata?: boolean\n  /** Optional tailwind classes to override the card container background & border */\n  containerClassName?: string\n}\n \n/**\n * Unified card skeleton component for consistent card loading states\n * \n * Supports different card types used across the application:\n * - vendor: Vendor cards with logo, title, description, and actions\n * - blog: Blog post cards with image, title, summary, and metadata  \n * - category: Category cards with icon, title, and description\n * - alternative: Alternative vendor cards in comparison lists\n */\nexport function CardSkeleton({\n  className,\n  containerClassName,\n  variant = 'vendor',\n  showActions = true,\n  showMetadata = true,\n  ...props\n}: CardSkeletonProps) {\n  const cardContent = {\n    vendor: <VendorCardContent showActions={showActions} showMetadata={showMetadata} />,\n    blog: <BlogCardContent showActions={showActions} showMetadata={showMetadata} />,\n    category: <CategoryCardContent />,\n    alternative: <AlternativeCardContent showActions={showActions} />\n  }\n\n  return (\n    <div \n      className={cn(\n        containerClassName || \"bg-ods-card border border-ods-border\",\n        \"rounded-lg overflow-hidden\",\n        // Flex layouts for certain variants\n        variant === 'blog' && \"h-full flex flex-col\",\n        variant === 'vendor' && \"h-full flex flex-col\",\n        className\n      )}\n      role=\"status\"\n      aria-label={`Loading ${variant} card`}\n      {...props}\n    >\n      {cardContent[variant]}\n    </div>\n  )\n}\n\n/**\n * Vendor card skeleton content - matches exact VendorCard structure\n */\nfunction VendorCardContent({ showActions, showMetadata }: { showActions: boolean; showMetadata: boolean }) {\n  return (\n    <div className=\"p-4 gap-3 flex flex-col\">\n      {/* Header Section - Row layout matching actual VendorCard */}\n      <div className=\"flex items-start gap-3 w-full\">\n        {/* Logo Frame - 60px width fixed, matching actual structure */}\n        <div className=\"w-[60px] h-[60px] bg-ods-bg border border-ods-border rounded-lg p-2 flex items-center justify-center flex-shrink-0\">\n          <MediaSkeleton.Avatar size=\"sm\" className=\"w-11 h-11\" />\n        </div>\n        \n        {/* Text Container - Column layout, matching actual structure */}\n        <div className=\"flex-1 flex flex-col justify-center py-2 min-w-0 space-y-1\">\n          {/* Title - Single line with proper width */}\n          <TextSkeleton.Subheading className=\"w-3/4\" />\n          {/* Category - Single line, shorter */}\n          <TextSkeleton.Caption className=\"w-1/2\" />\n        </div>\n      </div>\n\n      {/* Description Section - Fixed 48px height matching actual VendorCard */}\n      <div className=\"w-full h-12 overflow-hidden flex items-center\">\n        <div className=\"space-y-1 w-full\">\n          <TextSkeleton.Body className=\"w-full\" />\n          <TextSkeleton.Body className=\"w-2/3\" />\n        </div>\n      </div>\n\n      {/* Footer Section - Responsive layout matching actual structure */}\n      <div className=\"flex items-center justify-between gap-2 w-full min-w-0\">\n        {/* Stats Container - Flexible width, no overflow */}\n        <div className=\"flex items-center gap-3 md:gap-4 min-w-0 flex-shrink\">\n          {/* OpenMSP Score skeleton */}\n          <div className=\"flex items-center gap-1 flex-shrink-0\">\n            <MediaSkeleton.Icon size=\"sm\" className=\"w-5 h-5\" />\n            <TextSkeleton.Caption className=\"w-8\" />\n          </div>\n          \n          {/* GitHub Stats skeleton */}\n          <div className=\"flex items-center gap-1 flex-shrink-0\">\n            <MediaSkeleton.Icon size=\"sm\" className=\"w-5 h-5\" />\n            <TextSkeleton.Caption className=\"w-10\" />\n          </div>\n        </div>\n\n        {/* Tag Section - Contained within card boundaries */}\n        <div className=\"flex-shrink-0\">\n          <div className=\"flex items-center gap-1.5 bg-ods-bg border border-ods-border rounded px-2.5 py-1.5\">\n            <div className=\"w-4 h-4 bg-ods-border rounded-sm flex items-center justify-center flex-shrink-0\">\n              <MediaSkeleton.Icon size=\"sm\" className=\"w-2.5 h-2.5\" />\n            </div>\n            <TextSkeleton.Caption className=\"w-16\" />\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n\n/**\n * Blog card skeleton content - matches fixed height structure\n */\nfunction BlogCardContent({ showActions, showMetadata }: { showActions: boolean; showMetadata: boolean }) {\n  return (\n    <>\n      {/* Image Section — OG 1200×630 aspect (matches blog-card.tsx loaded state) */}\n      <div className=\"blog-card-image-container relative w-full aspect-[1200/630] overflow-hidden bg-ods-bg\">\n        <MediaSkeleton.CardImage />\n      </div>\n      \n      {/* Content - Fixed height structure to match BlogCard */}\n      <div className=\"p-4 flex flex-col flex-grow\">\n        {/* Title Section - Fixed 2 lines with vertical centering */}\n        <div className=\"mb-3 flex items-center min-h-[50.4px] md:min-h-[56px] lg:min-h-[61.6px]\">\n          <div className=\"space-y-1 w-full\">\n            <TextSkeleton.Subheading className=\"w-full\" />\n            <TextSkeleton.Subheading className=\"w-3/4\" />\n          </div>\n        </div>\n        \n        {/* Chips Section - Fixed single line height */}\n        <div className=\"mb-3 flex gap-2 h-[28px] items-center\">\n          <InteractiveSkeleton.Chip className=\"w-16\" />\n          <InteractiveSkeleton.Chip className=\"w-12\" />\n        </div>\n        \n        {/* Description Section - Fixed 2 lines with vertical centering */}\n        <div className=\"mb-3 flex items-center min-h-[42px] md:min-h-[45px] lg:min-h-[48px]\">\n          <div className=\"space-y-1 w-full\">\n            <TextSkeleton.Body className=\"w-full\" />\n            <TextSkeleton.Body className=\"w-1/2\" />\n          </div>\n        </div>\n\n        {/* Actions - only if requested */}\n        {showActions && (\n          <div className=\"pt-2\">\n            <InteractiveSkeleton.Button className=\"w-24 h-8\" />\n          </div>\n        )}\n        \n        {/* Metadata footer - Matches BlogMeta horizontal layout */}\n        {showMetadata && (\n          <div className=\"mt-auto\">\n            <div className=\"flex items-center justify-between gap-4 pt-4 border-t border-ods-border\">\n              {/* Author section - matches AuthorMeta */}\n              <div className=\"flex items-center gap-2 min-w-0 flex-1\">\n                <MediaSkeleton.Avatar size=\"sm\" />\n                <TextSkeleton.Caption className=\"w-16\" />\n              </div>\n              \n              {/* Date and reading time section - matches BlogMeta right side */}\n              <div className=\"flex items-center gap-3 text-ods-text-muted shrink-0\">\n                <TextSkeleton.Caption className=\"w-12\" />\n                {/* Separator dot */}\n                <div className=\"w-1 h-1 bg-ods-skeleton rounded-full\"></div>\n                <TextSkeleton.Caption className=\"w-16\" />\n              </div>\n            </div>\n          </div>\n        )}\n      </div>\n    </>\n  )\n}\n\n/**\n * Category card skeleton content\n */\nfunction CategoryCardContent() {\n  return (\n    <div className=\"p-4 md:p-6 space-y-4\">\n      {/* Icon grid */}\n      <div className=\"flex gap-2 md:gap-3\">\n        {Array.from({ length: 6 }).map((_, index) => (\n          <MediaSkeleton.Icon \n            key={index}\n            size=\"lg\"\n            className=\"flex-shrink-0\"\n          />\n        ))}\n      </div>\n\n      {/* Content */}\n      <div className=\"space-y-2\">\n        <div className=\"space-y-1\">\n          <TextSkeleton.Subheading className=\"w-3/4\" />\n          <TextSkeleton.Caption className=\"w-1/2\" />\n        </div>\n        \n        <div className=\"flex items-start md:items-end justify-between gap-4 md:gap-6\">\n          <div className=\"flex-1 space-y-2\">\n            <TextSkeleton.Body className=\"w-full\" />\n            <TextSkeleton.Body className=\"w-2/3\" />\n          </div>\n          \n          <InteractiveSkeleton.Button className=\"w-10 h-10 md:w-12 md:h-12 flex-shrink-0\" />\n        </div>\n      </div>\n    </div>\n  )\n}\n\n/**\n * Alternative card skeleton content (for vendor alternatives/comparisons)\n */\nfunction AlternativeCardContent({ showActions }: { showActions: boolean }) {\n  return (\n    <div className=\"flex items-start gap-4 p-4\">\n      <MediaSkeleton.Avatar size=\"md\" className=\"flex-shrink-0\" />\n      \n      <div className=\"flex-1 space-y-2\">\n        <div className=\"flex items-center justify-between\">\n          <TextSkeleton.Subheading className=\"w-1/3\" />\n          <TextSkeleton.Caption className=\"w-16\" />\n        </div>\n        \n        <div className=\"space-y-1\">\n          <TextSkeleton.Body className=\"w-full\" />\n          <TextSkeleton.Body className=\"w-5/6\" />\n        </div>\n\n        {showActions && (\n          <div className=\"pt-2\">\n            <InteractiveSkeleton.Button className=\"w-20 h-8\" />\n          </div>\n        )}\n      </div>\n    </div>\n  )\n}\n\n/**\n * Grid of card skeletons for loading lists\n */\nexport function CardSkeletonGrid({\n  count = 6,\n  variant = 'vendor',\n  className,\n  containerClassName,\n  ...props\n}: {\n  count?: number\n  variant?: CardSkeletonProps['variant']\n  className?: string\n  containerClassName?: string\n} & Omit<CardSkeletonProps, 'variant'>) {\n  return (\n    <div \n      className={cn(\n        \"grid gap-4 md:gap-6\",\n        // Responsive grid based on card type\n        variant === 'vendor' && \"grid-cols-1 md:grid-cols-2 lg:grid-cols-3\",\n        variant === 'blog' && \"grid-cols-1 md:grid-cols-2\",\n        variant === 'category' && \"grid-cols-1 md:grid-cols-2 lg:grid-cols-4\",\n        variant === 'alternative' && \"grid-cols-1\",\n        className\n      )}\n      role=\"status\"\n      aria-label={`Loading ${count} ${variant} cards`}\n    >\n      {Array.from({ length: count }, (_, index) => (\n        <CardSkeleton \n          key={index} \n          variant={variant}\n          containerClassName={containerClassName}\n          {...props}\n        />\n      ))}\n    </div>\n  )\n} ","\"use client\"\n\nimport { useState, useEffect, useCallback, useRef } from 'react'\nimport type { DocNode, DocContent } from '../../types/doc-source'\nimport {\n  stripFolderIndexFromPath,\n  findDocNodeByPath,\n  getDocAncestorNodeIds,\n  DEFAULT_FOLDER_INDEX_FILE,\n} from '../../utils/doc-tree-nav'\nimport { useDocNavigation } from './doc-navigation-context'\nimport { contentFetch } from '../../utils/embed-content-fetch'\nimport { scrollElementIntoView } from '../../utils/scroll-into-view'\nimport { navigateSamePageHash, HUB_HEADER_OFFSET_PX } from '../../utils/same-page-hash-nav'\n\nfunction scrollToContent() {\n  const article = document.querySelector('article') as HTMLElement | null\n  if (article) {\n    scrollElementIntoView(article, { headerOffset: HUB_HEADER_OFFSET_PX })\n  } else {\n    // Same anchoring-proof tween for the no-article fallback — native smooth\n    // scrollTo is cancelled by scroll anchoring while the new doc renders in.\n    scrollElementIntoView(document.documentElement)\n  }\n}\n\n/**\n * First displayable document inside a folder (depth-first): a direct non-Mermaid\n * file, else the first doc found in a subfolder. Used so a folder WITHOUT a\n * README shows its first child's content (mirroring how a README folder shows\n * its README) instead of a blank panel or a redundant in-page listing — the\n * sidebar tree is the directory browser. Returns null for a folder with no docs.\n */\nfunction findFirstDocPath(folder: DocNode): string | null {\n  const children = folder.children ?? []\n  for (const child of children) {\n    if (child.type === 'file' && !child.path.toLowerCase().endsWith('.mmd')) {\n      return child.path\n    }\n  }\n  for (const child of children) {\n    if (child.type === 'folder') {\n      const nested = findFirstDocPath(child)\n      if (nested) return nested\n    }\n  }\n  return null\n}\n\nexport interface UseDocumentTreeConfig {\n  /** API endpoint for fetching the document tree structure */\n  structureEndpoint: string\n  /** API endpoint for fetching document content */\n  contentEndpoint: string\n  /** Base route path for URL navigation (e.g., '/knowledge-base', '/data-room') */\n  baseRoute: string\n  /** Folder-index filename (defaults to 'README.md'). */\n  folderIndexFile?: string\n}\n\n/**\n * Generic hook for document tree navigation and content fetching.\n * Drives DocViewer across all doc-source consumers.\n *\n * Client-only: structure + content fetches run in parallel on first mount.\n * No SSR pre-population — the previous SSR path required a Supabase admin\n * client (service role key) and silently fell back to client fetches on\n * envs where the key wasn't set; the parallel client fetches keep behavior\n * uniform across local + prod (latency ~= max(structure, content), not sum).\n */\nexport function useDocumentTree(\n  config: UseDocumentTreeConfig,\n  initialPath?: string,\n) {\n  const { structureEndpoint, contentEndpoint, baseRoute } = config\n  const folderIndexFile = config.folderIndexFile ?? DEFAULT_FOLDER_INDEX_FILE\n\n  const cleanInitialPath = stripFolderIndexFromPath(\n    initialPath?.replace(/\\/$/, '') || '',\n    folderIndexFile,\n  )\n\n  const [structure, setStructure] = useState<DocNode[]>([])\n  const [selectedPath, setSelectedPath] = useState<string>(cleanInitialPath)\n  const [content, setContent] = useState<DocContent | null>(null)\n  const [isLoadingStructure, setIsLoadingStructure] = useState(true)\n  const [isLoadingContent, setIsLoadingContent] = useState(false)\n  const [error, setError] = useState<string | null>(null)\n  const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set())\n  const [isInitialized, setIsInitialized] = useState(false)\n  const lastFetchedPath = useRef<string | null>(null)\n\n  const normalizedBaseRoute = baseRoute.replace(/\\/$/, '')\n\n  const selectedPathRef = useRef(selectedPath)\n  selectedPathRef.current = selectedPath\n\n  const docNavigation = useDocNavigation()\n\n  useEffect(() => {\n    const handlePopState = () => {\n      const pathname = window.location.pathname\n      let pathFromUrl = ''\n\n      if (pathname === normalizedBaseRoute || pathname === `${normalizedBaseRoute}/`) {\n        pathFromUrl = ''\n      } else if (pathname.startsWith(`${normalizedBaseRoute}/`)) {\n        pathFromUrl = pathname.substring(`${normalizedBaseRoute}/`.length)\n      }\n\n      pathFromUrl = stripFolderIndexFromPath(pathFromUrl, folderIndexFile)\n\n      if (pathFromUrl !== selectedPathRef.current) {\n        setSelectedPath(pathFromUrl)\n        if (pathFromUrl) {\n          const parentPath = pathFromUrl.includes('/')\n            ? pathFromUrl.substring(0, pathFromUrl.lastIndexOf('/'))\n            : pathFromUrl\n          setExpandedNodes(new Set(getDocAncestorNodeIds(parentPath)))\n        }\n        setTimeout(() => {\n          scrollToContent()\n        }, 150)\n      }\n    }\n\n    window.addEventListener('popstate', handlePopState)\n    return () => window.removeEventListener('popstate', handlePopState)\n  }, [normalizedBaseRoute, folderIndexFile])\n\n  // External-URL → state sync. The popstate listener above catches browser\n  // back/forward, but client-side routers (react-router, Next App Router…)\n  // change the URL via `history.pushState` which does NOT fire popstate.\n  // The host re-renders the viewer with a new `initialPath` prop instead, so\n  // we mirror the popstate logic here against the (memoized) `cleanInitialPath`.\n  // Without this, a chat-card click that soft-navigates via react-router\n  // updates the URL but the viewer stays on the previously-selected doc.\n  useEffect(() => {\n    if (cleanInitialPath === selectedPathRef.current) return\n    setSelectedPath(cleanInitialPath)\n    if (cleanInitialPath) {\n      const parentPath = cleanInitialPath.includes('/')\n        ? cleanInitialPath.substring(0, cleanInitialPath.lastIndexOf('/'))\n        : cleanInitialPath\n      setExpandedNodes(new Set(getDocAncestorNodeIds(parentPath)))\n    }\n    // Match popstate's scroll-to-content delay; the targeted content fetch\n    // dispatched by the selectedPath effect lands before this fires.\n    setTimeout(scrollToContent, 150)\n  }, [cleanInitialPath])\n\n  useEffect(() => {\n    if (!isInitialized) {\n      // Kick off the speculative content fetch IN PARALLEL with the structure\n      // fetch — the two endpoints are independent and most landing pages have\n      // a root README (the default folder-index). If the structure ends up\n      // pointing at a different path (e.g. knowledge-base falls back to the\n      // first-folder README because there's no root README), the content\n      // useEffect issues the correct fetch after structure arrives — the\n      // speculative result silently no-ops (the content state update gets\n      // overwritten by the targeted fetch).\n      const speculativeContentPath = cleanInitialPath || folderIndexFile\n      lastFetchedPath.current = speculativeContentPath\n      fetchContent(speculativeContentPath)\n      fetchStructure()\n      setIsInitialized(true)\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [isInitialized])\n\n  useEffect(() => {\n    if (selectedPath === null || selectedPath === undefined) return\n    if (!structure || structure.length === 0) return\n\n    let pathToFetch: string | null = null\n\n    if (selectedPath === '') {\n      pathToFetch = folderIndexFile\n    } else {\n      const node = findDocNodeByPath(selectedPath, structure)\n\n      if (node && node.type === 'folder' && !node.hasReadme) {\n        // No-README folder has no body of its own — show its FIRST child doc\n        // (mirrors a README folder showing its README). selectedPath stays the\n        // folder, so the sidebar keeps it highlighted/expanded; the sidebar is\n        // the directory browser, so we render NO separate in-page listing.\n        const firstDocPath = findFirstDocPath(node)\n        if (!firstDocPath) {\n          // Genuinely empty folder — clear to the empty state.\n          lastFetchedPath.current = null\n          setError(null)\n          setContent(null)\n          setIsLoadingContent(false)\n          return\n        }\n        pathToFetch = firstDocPath\n      } else if (node && node.type === 'folder' && node.hasReadme) {\n        // `getContent(folder)` already resolves a README folder to its README,\n        // so the initial speculative fetch (which uses the bare folder path)\n        // ALREADY loaded this content. Re-fetching the `${folder}/README.md`\n        // variant is a redundant 2nd request whose in-flight `isLoadingContent`\n        // flashes the skeleton — content → skeleton → content — on first load.\n        // Skip it when the folder path was already the (speculatively) fetched\n        // path; the result (or its in-flight request) covers the README.\n        if (lastFetchedPath.current === selectedPath) {\n          return\n        }\n        pathToFetch = `${selectedPath}/${folderIndexFile}`\n      } else {\n        pathToFetch = selectedPath\n      }\n    }\n\n    if (pathToFetch === lastFetchedPath.current) {\n      return\n    }\n\n    if (pathToFetch) {\n      lastFetchedPath.current = pathToFetch\n      fetchContent(pathToFetch)\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [selectedPath, structure, folderIndexFile])\n\n  const fetchStructure = async () => {\n    try {\n      setIsLoadingStructure(true)\n      setError(null)\n\n      const response = await contentFetch(structureEndpoint)\n\n      if (!response.ok) {\n        throw new Error('Failed to load documentation structure')\n      }\n\n      const result = await response.json()\n\n      if (result.success && result.data) {\n        setStructure(result.data)\n\n        if (cleanInitialPath) {\n          const pathForExpansion = cleanInitialPath.includes('.')\n            ? cleanInitialPath.substring(0, cleanInitialPath.lastIndexOf('/'))\n            : cleanInitialPath\n          if (pathForExpansion) {\n            setExpandedNodes(new Set(getDocAncestorNodeIds(pathForExpansion)))\n          }\n        } else if (result.data.length > 0) {\n          const hasRootReadme = result.data.some(\n            (node: DocNode) => node.type === 'file' && node.path === folderIndexFile\n          )\n\n          if (!hasRootReadme) {\n            const firstNode = result.data[0]\n            if (firstNode.type === 'folder') {\n              setExpandedNodes(new Set([firstNode.id]))\n              if (firstNode.hasReadme) {\n                setSelectedPath(firstNode.path)\n                window.history.replaceState({}, '', `${normalizedBaseRoute}/${firstNode.path}`)\n              }\n            }\n          }\n        }\n      } else {\n        setError('Failed to load documentation structure')\n      }\n    } catch (err) {\n      console.error('Error fetching documentation structure:', err)\n      setError(err instanceof Error ? err.message : 'Failed to load documentation')\n    } finally {\n      setIsLoadingStructure(false)\n    }\n  }\n\n  const fetchContent = async (path: string) => {\n    try {\n      setIsLoadingContent(true)\n      // Don't clear `error` here — if a previous fetch set an error and this\n      // is a stale/speculative call that gets superseded, the guard below\n      // returns early without writing to state. Clearing error here would\n      // briefly flicker the user-visible error message.\n\n      const response = await contentFetch(`${contentEndpoint}?path=${encodeURIComponent(path)}`)\n\n      // Request-id guard: between awaits, `lastFetchedPath.current` may have\n      // been bumped by a newer fetch (the structure-arrives auto-select issues\n      // a more-targeted fetch while the speculative one is in flight). Bail\n      // BEFORE writing to state — otherwise the late 404 of the speculative\n      // fetch overwrites the targeted fetch's good content with null.\n      if (path !== lastFetchedPath.current) return\n\n      if (!response.ok) {\n        if (response.status === 404) {\n          const result = await response.json().catch(() => ({}))\n          if (path !== lastFetchedPath.current) return\n          // Landing-page silent fallback: when the user lands on the source's\n          // root URL and there's no root `README.md` (knowledge-base case),\n          // the speculative fetch 404s — surface an empty state instead of\n          // an error banner. The structure-arrives auto-select will fire\n          // a targeted fetch for the first-folder README on the next render.\n          if (path === folderIndexFile && selectedPath === '') {\n            // Superseded by the auto-select fetch the structure effect fires.\n            // Null the request id so the `finally` does NOT drop the spinner —\n            // otherwise there's a 1-frame gap (isLoadingContent false, content\n            // null) where the empty state flashes before the real fetch starts.\n            lastFetchedPath.current = null\n            setError(null)\n            setContent(null)\n            return\n          }\n          // No-README FOLDER → stay silent: the content effect resolves it to\n          // the folder's first child, so the folder-path 404 from the direct-\n          // load speculative fetch is expected, not an error to flash.\n          const probe = findDocNodeByPath(\n            stripFolderIndexFromPath(path, folderIndexFile),\n            structure,\n          )\n          const probeIsNoReadmeFolder =\n            !!probe && probe.type === 'folder' && !probe.hasReadme\n          // Before the structure has loaded to classify the path (`structure`\n          // is [] in the speculative call's closure), only silence FOLDER-LIKE\n          // paths (no `.md`) — a genuinely missing `*.md` leaf must still error.\n          const preStructureFolderLike =\n            structure.length === 0 && !path.endsWith('.md')\n          if (probeIsNoReadmeFolder || preStructureFolderLike) {\n            // Superseded by the targeted fetch (first-child / reclassified path)\n            // the structure effect fires. Null the request id so the `finally`\n            // keeps the spinner up instead of flashing an empty state for a\n            // frame before that fetch starts.\n            lastFetchedPath.current = null\n            setError(null)\n            setContent(null)\n            return\n          }\n          setError(result.error || 'Documentation file not found')\n          setContent(null)\n          return\n        }\n        throw new Error('Failed to load documentation content')\n      }\n\n      const result = await response.json()\n      if (path !== lastFetchedPath.current) return\n      setError(null)\n\n      if (result.success && result.data) {\n        if (result.redirect && result.correctPath !== undefined) {\n          const correctedPath = result.correctPath\n          setSelectedPath(correctedPath)\n          window.history.replaceState({}, '', `${normalizedBaseRoute}/${correctedPath}`)\n          setContent(result.data)\n        } else {\n          setContent(result.data)\n        }\n      } else {\n        setError(result.error || 'Failed to load content')\n        setContent(null)\n      }\n    } catch (err) {\n      if (path !== lastFetchedPath.current) return\n      console.error('Error fetching documentation content:', err)\n      setError(err instanceof Error ? err.message : 'Failed to load content')\n      setContent(null)\n    } finally {\n      // Only clear loading state if THIS fetch is still the active one — a\n      // superseded speculative shouldn't flip the spinner off while the\n      // targeted fetch is still in flight.\n      if (path === lastFetchedPath.current) setIsLoadingContent(false)\n    }\n  }\n\n  const toggleNode = useCallback((nodeId: string) => {\n    setExpandedNodes(prev => {\n      const next = new Set(prev)\n      if (next.has(nodeId)) {\n        next.delete(nodeId)\n      } else {\n        next.add(nodeId)\n      }\n      return next\n    })\n  }, [])\n\n  // Structural minimum the body uses — `id`, `path`, `type`, `hasReadme`.\n  // Widening from `DocNode` lets the navigation components (which carry the\n  // narrower `NavigationNode` row shape) pass their own node back without the\n  // cross-type `as` cast. Both DocNode and NavigationNode satisfy this.\n  const selectNode = useCallback((node: Pick<DocNode, 'id' | 'path' | 'type' | 'hasReadme'>) => {\n    // Expansion only: clicking a folder toggles its own subtree; clicking a file\n    // reveals its ancestor chain.\n    if (node.type === 'folder') {\n      setExpandedNodes(prev => {\n        if (prev.has(node.id)) {\n          const ancestorIds = getDocAncestorNodeIds(node.path)\n          ancestorIds.pop() // collapse self, keep ancestors open\n          return new Set(ancestorIds)\n        }\n        return new Set(getDocAncestorNodeIds(node.path))\n      })\n    } else {\n      const lastSlash = node.path.lastIndexOf('/')\n      if (lastSlash > 0) {\n        setExpandedNodes(new Set(getDocAncestorNodeIds(node.path.substring(0, lastSlash))))\n      }\n    }\n\n    // Every node is a navigable destination — a file shows its body, a README\n    // folder its README, a no-README folder its first child doc (the content\n    // effect resolves which). So selection + URL + scroll are identical for all\n    // node types; no per-type special-casing.\n    lastFetchedPath.current = null\n    setSelectedPath(node.path)\n    window.history.pushState({}, '', `${normalizedBaseRoute}/${node.path}`)\n    setTimeout(scrollToContent, 150)\n  }, [normalizedBaseRoute])\n\n  const navigateToDoc = useCallback((path: string, options?: { expandFolder?: boolean; fromInternalLink?: boolean }) => {\n    const hashIndex = path.indexOf('#')\n    const anchor = hashIndex !== -1 ? path.substring(hashIndex) : ''\n    const cleanPath = path.replace(/\\/$/, '').split('#')[0]\n\n    // Same-doc-different-anchor shortcut. Content is already mounted, so we\n    // don't need the 300ms \"wait-for-fetch\" bandaid — the canonical helper\n    // owns pushState + synthetic `hashchange` (so any in-doc TOC / accordion\n    // bound to the URL hash re-renders) + the anchoring-proof tween in one\n    // sync call. `headerOffset: HUB_HEADER_OFFSET_PX` matches the cross-doc path below so\n    // anchors land BELOW the docs sticky header on every same-doc internal\n    // link click. Cross-doc nav (different cleanPath) falls through to the\n    // existing fetch-then-scroll path below.\n    //\n    // We pass the BARE-hash form to the helper rather than reconstructing\n    // a full `${normalizedBaseRoute}/${cleanPath}${anchor}` path: the\n    // helper's pathname check compares against `window.location.pathname`,\n    // which carries the FOLDER-INDEX-STRIPPED form (`/docs/foo` for\n    // `foo/README.md`, `/docs` for the root index). Handing it `cleanPath`\n    // — the raw resolved path — produces e.g. `/docs/foo/README.md` and\n    // the compare fails → helper returns false → silent dead-click. The\n    // bare-hash form sidesteps that entirely: the helper reconstructs\n    // `pathname + search + hash` from `window.location`, so the compare\n    // is trivially equal. Covers bare `#anchor` links (resolve to\n    // `cleanPath=''`) AND folder-index links (`foo/README.md` resolving\n    // to the current `/docs/foo`).\n    // Bare-hash internal links (`[Click](#section)`) come in as\n    // `path === '#section'`, so `cleanPath` becomes `''` and the naive\n    // strip-then-compare misses the same-doc shortcut on every NON-root\n    // doc (selectedPath is e.g. `'foo/bar'`, not `''`). For that case the\n    // current doc IS the same-doc target by definition — short-circuit\n    // pathForSelection to the current selection so the shortcut fires.\n    const pathForSelection =\n      anchor && options?.fromInternalLink && cleanPath === ''\n        ? selectedPathRef.current\n        : stripFolderIndexFromPath(cleanPath, folderIndexFile)\n    if (\n      anchor &&\n      options?.fromInternalLink &&\n      pathForSelection === selectedPathRef.current\n    ) {\n      navigateSamePageHash(anchor, { headerOffset: HUB_HEADER_OFFSET_PX })\n      return\n    }\n\n    const scrollAfterNav = () => {\n      if (anchor) {\n        setTimeout(() => {\n          const el = document.getElementById(anchor.substring(1))\n          if (el) {\n            scrollElementIntoView(el, { headerOffset: HUB_HEADER_OFFSET_PX })\n          } else {\n            scrollToContent()\n          }\n        }, 300)\n      } else {\n        setTimeout(() => {\n          scrollToContent()\n        }, 150)\n      }\n    }\n\n    if (options?.expandFolder) {\n      lastFetchedPath.current = null\n      setSelectedPath(cleanPath)\n      window.history.pushState({}, '', `${normalizedBaseRoute}/${cleanPath}${anchor}`)\n\n      const pathParts = cleanPath.split('/')\n      const nodeIdsToExpand: string[] = []\n      let currentPath = ''\n\n      for (const part of pathParts) {\n        currentPath = currentPath ? `${currentPath}-${part}` : part\n        nodeIdsToExpand.push(currentPath.toLowerCase())\n      }\n\n      setExpandedNodes(new Set(nodeIdsToExpand))\n\n      if (options?.fromInternalLink) {\n        scrollAfterNav()\n      }\n\n      setError(null)\n      return\n    }\n\n    // `pathForSelection` was already computed above (inside the\n    // same-doc-anchor shortcut check); reuse it here for cross-doc nav.\n    const urlPath = pathForSelection\n\n    lastFetchedPath.current = null\n    setSelectedPath(pathForSelection)\n    window.history.pushState({}, '', `${normalizedBaseRoute}/${urlPath}${anchor}`)\n\n    if (options?.fromInternalLink) {\n      scrollAfterNav()\n    }\n\n    const pathParts = cleanPath.split('/')\n    const parentIds: string[] = []\n    let currentPath = ''\n    const partsToProcess = cleanPath.includes('.') ? pathParts.slice(0, -1) : pathParts\n\n    for (const part of partsToProcess) {\n      currentPath = currentPath ? `${currentPath}-${part}` : part\n      parentIds.push(currentPath.toLowerCase())\n    }\n\n    if (parentIds.length > 0) {\n      setExpandedNodes(new Set(parentIds))\n    }\n  }, [structure, normalizedBaseRoute, folderIndexFile])\n\n  useEffect(() => {\n    return docNavigation.register({\n      baseRoute: normalizedBaseRoute,\n      findNodeByPath: (path) => {\n        const clean = stripFolderIndexFromPath(path.replace(/\\/$/, '').split('#')[0], folderIndexFile)\n        return findDocNodeByPath(clean, structure) ?? null\n      },\n      selectNode,\n    })\n  }, [docNavigation, normalizedBaseRoute, structure, selectNode, folderIndexFile])\n\n  return {\n    structure,\n    selectedPath,\n    setSelectedPath,\n    content,\n    isLoadingStructure,\n    isLoadingContent,\n    error,\n    expandedNodes,\n    toggleNode,\n    selectNode,\n    navigateToDoc,\n    refetch: fetchStructure,\n    setExpandedNodes,\n  }\n}\n\nexport type { DocNode, DocContent }\n","\"use client\"\n\nimport React, { createContext, useCallback, useContext, useRef, type ReactNode } from 'react'\nimport type { DocNode } from '../../types/doc-source'\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface DocNavigator {\n  /** Base route this navigator owns (e.g., '/knowledge-base', '/data-room'). */\n  baseRoute: string\n  /** Look up a node in the tree by its storage path (e.g., 'openframe-cli/README.md'). */\n  findNodeByPath: (path: string) => DocNode | null\n  /** Navigate to a node using the same flow as a sidebar menu click. */\n  selectNode: (node: DocNode) => void\n}\n\ninterface DocNavigationContextValue {\n  /**\n   * Register a navigator for a `baseRoute`. Returns a cleanup function that\n   * removes the navigator from the map IF the slot still owns it (StrictMode-safe).\n   * Duplicate registration for the same baseRoute logs a console.warn and replaces.\n   */\n  register: (nav: DocNavigator) => () => void\n  /**\n   * Navigate to a path inside whichever registered navigator owns its baseRoute prefix.\n   * Returns true if handled, false otherwise (caller falls back to opening in a new tab).\n   */\n  navigate: (path: string) => boolean\n  /** Whether ANY navigator is currently mounted. */\n  isAvailable: () => boolean\n}\n\n// =============================================================================\n// Context\n// =============================================================================\n\nconst DocNavigationContext = createContext<DocNavigationContextValue | null>(null)\n\n/**\n * Bridges `useDocumentTree` instances (deep in the page tree) to `GlobalAskAI`\n * (high up near the root) without global events or URL parsing.\n *\n * Multi-navigator design: a `Map<baseRoute, DocNavigator>` lets multiple\n * viewers coexist (nested viewers, dual-pane scenarios). Today only one\n * viewer mounts at a time, so behavior is unchanged.\n */\nexport function DocNavigationProvider({ children }: { children: ReactNode }) {\n  const navMap = useRef<Map<string, DocNavigator>>(new Map())\n\n  const register = useCallback((nav: DocNavigator) => {\n    const existing = navMap.current.get(nav.baseRoute)\n    if (existing && existing !== nav) {\n      // eslint-disable-next-line no-console\n      console.warn(\n        `[DocNavigationContext] duplicate registration for ${nav.baseRoute}; replacing`,\n      )\n    }\n    navMap.current.set(nav.baseRoute, nav)\n    return () => {\n      // StrictMode-safe identity check: only delete if THIS nav still owns the slot.\n      if (navMap.current.get(nav.baseRoute) === nav) {\n        navMap.current.delete(nav.baseRoute)\n      }\n    }\n  }, [])\n\n  const findOwningNavigator = useCallback((path: string): DocNavigator | null => {\n    // First try: a navigator whose baseRoute is a prefix of `path`. Walks the\n    // longest match first so nested routes win over a parent.\n    const baseRoutes = Array.from(navMap.current.keys()).sort((a, b) => b.length - a.length)\n    for (const baseRoute of baseRoutes) {\n      if (path === baseRoute || path.startsWith(`${baseRoute}/`)) {\n        return navMap.current.get(baseRoute) ?? null\n      }\n    }\n    // Fallback: a single navigator handles any path (existing behavior — chat\n    // chips emit storage paths, not absolute URLs, so prefix matching misses).\n    if (navMap.current.size === 1) {\n      return navMap.current.values().next().value ?? null\n    }\n    return null\n  }, [])\n\n  const navigate = useCallback(\n    (path: string): boolean => {\n      const nav = findOwningNavigator(path)\n      if (!nav) return false\n      const node = nav.findNodeByPath(path)\n      if (!node) return false\n      nav.selectNode(node)\n      return true\n    },\n    [findOwningNavigator],\n  )\n\n  const isAvailable = useCallback(() => navMap.current.size > 0, [])\n\n  const value = React.useMemo(\n    () => ({ register, navigate, isAvailable }),\n    [register, navigate, isAvailable],\n  )\n\n  return <DocNavigationContext.Provider value={value}>{children}</DocNavigationContext.Provider>\n}\n\n// =============================================================================\n// Hooks\n// =============================================================================\n\nexport function useDocNavigation(): DocNavigationContextValue {\n  const ctx = useContext(DocNavigationContext)\n  if (!ctx) {\n    // Safe no-op fallback when used outside the provider.\n    return {\n      register: () => () => {},\n      navigate: () => false,\n      isAvailable: () => false,\n    }\n  }\n  return ctx\n}\n","\"use client\"\n\nimport { useState, useRef, useCallback, useEffect, useMemo } from \"react\"\nimport { scrollElementIntoView } from \"../../utils/scroll-into-view\"\n\n// Single source for the sticky-chrome height. Used for BOTH the scroll target\n// offset (where a clicked section lands) AND the active-section detection\n// threshold (where the scroll listener flips highlight). They must match —\n// previously 100 vs 150 caused a 50px window where the indicator jumped to\n// the next section even though that section's top was still below the\n// clicked one's resting offset.\nconst SCROLL_OFFSET = 100\n\ninterface ScrollSpySection {\n  id: string\n  title?: string\n  level?: number\n}\n\ninterface UseScrollSpyReturn {\n  activeSection: string\n  handleSectionClick: (sectionId: string) => void\n}\n\n/**\n * Shared scroll spy hook for tracking active section based on scroll position.\n * Used by DocViewer for sticky section navigation.\n */\nexport function useScrollSpy(sections: ScrollSpySection[] | undefined): UseScrollSpyReturn {\n  const [activeSection, setActiveSection] = useState(\"\")\n  const isScrollingFromClick = useRef(false)\n\n  const sectionIds = useMemo(\n    () => sections?.map(s => s.id).join(',') ?? '',\n    [sections]\n  )\n  const stableSections = useRef(sections)\n  if (sectionIds !== (stableSections.current?.map(s => s.id).join(',') ?? '')) {\n    stableSections.current = sections\n  }\n\n  const handleSectionClick = useCallback((sectionId: string) => {\n    const targetElement = document.getElementById(sectionId)\n    if (!targetElement) return\n\n    isScrollingFromClick.current = true\n    setActiveSection(sectionId)\n\n    scrollElementIntoView(targetElement, { headerOffset: SCROLL_OFFSET })\n\n    setTimeout(() => {\n      isScrollingFromClick.current = false\n    }, 800)\n  }, [])\n\n  useEffect(() => {\n    const currentSections = stableSections.current\n    if (!currentSections || currentSections.length === 0) return\n\n    const handleScroll = () => {\n      if (isScrollingFromClick.current) return\n\n      const scrollPosition = window.scrollY + SCROLL_OFFSET\n      let currentSection = currentSections[0]?.id || \"\"\n\n      for (let i = currentSections.length - 1; i >= 0; i--) {\n        const element = document.getElementById(currentSections[i].id)\n        if (element && scrollPosition >= element.offsetTop) {\n          currentSection = currentSections[i].id\n          break\n        }\n      }\n\n      setActiveSection((prev) => (prev !== currentSection ? currentSection : prev))\n    }\n\n    let scrollTimer: ReturnType<typeof setTimeout>\n    const throttledScroll = () => {\n      clearTimeout(scrollTimer)\n      scrollTimer = setTimeout(handleScroll, 100)\n    }\n\n    window.addEventListener(\"scroll\", throttledScroll)\n    handleScroll()\n\n    return () => {\n      window.removeEventListener(\"scroll\", throttledScroll)\n      clearTimeout(scrollTimer)\n    }\n  }, [sectionIds])\n\n  return { activeSection, handleSectionClick }\n}\n","import { useCallback } from 'react'\nimport { useChatRuntime } from '../../contexts/chat-runtime-context'\nimport type { ResolveLinkResult } from '../../types/doc-source'\nimport { contentFetch } from '../../utils/embed-content-fetch'\n\n/**\n * `useDocsResolveLink(sourceId, override?)` — POST `/api/docs/resolve-link`\n * (or the override / `ChatRuntime.endpoints.docsResolveLinkUrl`) for a\n * relative href inside a doc body, returning a `ResolveLinkResult`\n * envelope.\n *\n * The endpoint chain (`override ?? runtime.endpoints.docsResolveLinkUrl\n * ?? '/api/docs/resolve-link'`) mirrors `searchEndpoint` resolution in\n * `<DocViewer>` so embedders configure both the same way: per-instance\n * prop OR ambient `ChatRuntimeProvider`.\n *\n * The full fetch + JSON-parse pipeline is wrapped in try/catch so a\n * network throw (DNS / CORS / offline) or a non-JSON response surfaces\n * as `{ success: false, error }` — the markdown renderer's broken-link\n * badge handles that branch instead of swallowing an unhandled rejection\n * past the click handler.\n */\nexport function useDocsResolveLink(\n  sourceId: string,\n  resolveLinkEndpoint?: string | null,\n) {\n  const chatRuntime = useChatRuntime()\n  const resolvedResolveLinkEndpoint =\n    resolveLinkEndpoint ?? chatRuntime?.endpoints.docsResolveLinkUrl ?? '/api/docs/resolve-link'\n\n  return useCallback(\n    async (href: string, currentPath: string): Promise<ResolveLinkResult> => {\n      try {\n        const response = await contentFetch(resolvedResolveLinkEndpoint, {\n          method: 'POST',\n          headers: { 'Content-Type': 'application/json' },\n          body: JSON.stringify({ link: href, currentPath, source: sourceId }),\n        })\n        if (!response.ok) {\n          return { success: false, error: `Resolve failed: ${response.status}` }\n        }\n        const json = await response.json()\n        return (json.data ?? json) as ResolveLinkResult\n      } catch (error) {\n        return {\n          success: false,\n          error: error instanceof Error ? error.message : 'Resolve failed',\n        }\n      }\n    },\n    [resolvedResolveLinkEndpoint, sourceId],\n  )\n}\n","import React from 'react'\n\n/**\n * Text-style skeleton — matches a rendered markdown article layout.\n * Used by `<DocsHubPage>` as the default for `markdown` (and unknown\n * document types). Embedders can override via `renderSkeleton`.\n */\nexport function MarkdownSkeleton() {\n  return (\n    <div className=\"space-y-7 mt-6\">\n      <div className=\"space-y-[14px]\">\n        <div className=\"h-[16px] bg-ods-border rounded w-full animate-pulse\" />\n        <div className=\"h-[16px] bg-ods-border rounded w-full animate-pulse\" />\n        <div className=\"h-[16px] bg-ods-border rounded w-full animate-pulse\" />\n        <div className=\"h-[16px] bg-ods-border rounded w-3/4 animate-pulse\" />\n      </div>\n      <div className=\"space-y-[14px]\">\n        <div className=\"h-[16px] bg-ods-border rounded w-full animate-pulse\" />\n        <div className=\"h-[16px] bg-ods-border rounded w-full animate-pulse\" />\n        <div className=\"h-[16px] bg-ods-border rounded w-full animate-pulse\" />\n        <div className=\"h-[16px] bg-ods-border rounded w-5/6 animate-pulse\" />\n      </div>\n      <div className=\"space-y-[14px]\">\n        <div className=\"h-[16px] bg-ods-border rounded w-full animate-pulse\" />\n        <div className=\"h-[16px] bg-ods-border rounded w-full animate-pulse\" />\n        <div className=\"h-[16px] bg-ods-border rounded w-full animate-pulse\" />\n        <div className=\"h-[16px] bg-ods-border rounded w-full animate-pulse\" />\n        <div className=\"h-[16px] bg-ods-border rounded w-2/3 animate-pulse\" />\n      </div>\n      <div className=\"h-[88px] bg-ods-card border border-ods-border rounded-lg animate-pulse\" />\n      <div className=\"h-7 bg-ods-border rounded w-1/3 animate-pulse\" />\n      <div className=\"space-y-[14px]\">\n        <div className=\"h-[16px] bg-ods-border rounded w-full animate-pulse\" />\n        <div className=\"h-[16px] bg-ods-border rounded w-full animate-pulse\" />\n        <div className=\"h-[16px] bg-ods-border rounded w-full animate-pulse\" />\n        <div className=\"h-[16px] bg-ods-border rounded w-[72%] animate-pulse\" />\n      </div>\n      <div className=\"space-y-[14px]\">\n        <div className=\"h-[16px] bg-ods-border rounded w-full animate-pulse\" />\n        <div className=\"h-[16px] bg-ods-border rounded w-full animate-pulse\" />\n        <div className=\"h-[16px] bg-ods-border rounded w-full animate-pulse\" />\n        <div className=\"h-[16px] bg-ods-border rounded w-full animate-pulse\" />\n        <div className=\"h-[16px] bg-ods-border rounded w-[58%] animate-pulse\" />\n      </div>\n      <div className=\"h-7 bg-ods-border rounded w-2/5 animate-pulse\" />\n      <div className=\"space-y-[14px]\">\n        <div className=\"h-[16px] bg-ods-border rounded w-full animate-pulse\" />\n        <div className=\"h-[16px] bg-ods-border rounded w-full animate-pulse\" />\n        <div className=\"h-[16px] bg-ods-border rounded w-[90%] animate-pulse\" />\n      </div>\n      <div className=\"space-y-[14px]\">\n        <div className=\"h-[16px] bg-ods-border rounded w-full animate-pulse\" />\n        <div className=\"h-[16px] bg-ods-border rounded w-full animate-pulse\" />\n        <div className=\"h-[16px] bg-ods-border rounded w-full animate-pulse\" />\n        <div className=\"h-[16px] bg-ods-border rounded w-[70%] animate-pulse\" />\n      </div>\n      <div className=\"h-[88px] bg-ods-card border border-ods-border rounded-lg animate-pulse\" />\n    </div>\n  )\n}\n\n/**\n * Embed-style skeleton — matches the iframe loading state for `pdf`,\n * `google_sheet`, `figma`, and `file` document types. Used by\n * `<DocsHubPage>` as the default for non-markdown documentTypes.\n *\n * The skeleton is documentType-aware so its layout matches the actual\n * viewer that will replace it:\n *   - `pdf`                  → header with title + TWO buttons (Preview, Download)\n *   - `google_sheet`/`figma` → header with title + ONE button/toggle\n *   - `file`                 → centered FileDownloadCard-style box\n *   - undefined / others     → generic (1-button header)\n *\n * IMPORTANT: bars use `bg-ods-border` (NOT `bg-ods-skeleton`). The\n * `--ods-skeleton` token resolves to TRANSPARENT in this build, leaving\n * the skeleton box visually empty — the embed skeleton was the loudest\n * surface affected (a full-height iframe area showing nothing). Same fix\n * the chat-message-row skeleton already documents in its inline comment.\n */\nexport interface EmbedSkeletonProps {\n  /** When provided, the header layout matches the eventual viewer's\n   *  button count + arrangement, so the layout doesn't shift on load. */\n  documentType?: 'pdf' | 'google_sheet' | 'figma' | 'file' | string\n}\n\nexport function EmbedSkeleton({ documentType }: EmbedSkeletonProps = {}) {\n  // Centered card shape for the `file` documentType — matches\n  // `<FileDownloadCard>`'s `flex flex-col items-center justify-center py-16`\n  // + bordered card with icon, name, type/size row, Download button.\n  if (documentType === 'file') {\n    return (\n      <div className=\"flex flex-col items-center justify-center py-16\">\n        <div className=\"bg-ods-card border border-ods-border rounded-xl p-8 max-w-md w-full text-center space-y-4\">\n          <div className=\"w-16 h-16 rounded mx-auto bg-ods-border animate-pulse\" />\n          <div className=\"space-y-2\">\n            <div className=\"h-5 w-2/3 mx-auto rounded bg-ods-border animate-pulse\" />\n            <div className=\"h-4 w-1/2 mx-auto rounded bg-ods-border animate-pulse\" />\n          </div>\n          <div className=\"h-10 w-full rounded bg-ods-border animate-pulse\" />\n        </div>\n      </div>\n    )\n  }\n\n  // PDF viewer has TWO buttons (Preview + Download); Sheets / Figma\n  // render ONE (Open / view-toggle). Default to one for unknown types.\n  const buttonCount = documentType === 'pdf' ? 2 : 1\n\n  return (\n    <div className=\"space-y-4\">\n      {/* Header — matches the actual viewer's\n       *  `flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`\n       *  (mobile-stacked, desktop-row). */}\n      <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n        {/* Left: icon + title */}\n        <div className=\"flex items-center gap-2 min-w-0 flex-1\">\n          <div className=\"w-5 h-5 shrink-0 rounded bg-ods-border animate-pulse\" />\n          <div className=\"h-6 w-2/3 rounded bg-ods-border animate-pulse\" />\n        </div>\n        {/* Right: 1 or 2 buttons. Mobile = full-width; desktop = auto. */}\n        <div className=\"flex items-center gap-2 w-full sm:w-auto\">\n          {Array.from({ length: buttonCount }).map((_, i) => (\n            <div\n              key={i}\n              className=\"h-10 w-full sm:w-32 rounded bg-ods-border animate-pulse flex-1 sm:flex-initial\"\n            />\n          ))}\n        </div>\n      </div>\n      {/* Body — clean iframe-sized rectangle, no fake inner placeholder\n       *  cruft. Matches the viewer's default `calc(100vh - 250px)` height. */}\n      <div\n        className=\"w-full rounded-lg border border-ods-border bg-ods-card animate-pulse\"\n        style={{ height: 'calc(100vh - 250px)' }}\n      />\n    </div>\n  )\n}\n","import React from 'react'\nimport { DocViewer, type DocViewerProps } from './doc-viewer'\nimport { MarkdownSkeleton, EmbedSkeleton } from './skeletons'\nimport { PdfViewer } from '../embeds/pdf-viewer'\nimport { GoogleSheetsViewer } from '../embeds/google-sheets-viewer'\nimport { FigmaEmbed } from '../embeds/figma-embed'\nimport { FileDownloadCard } from '../embeds/file-download-card'\nimport type {\n  DocContent,\n  DocRenderHandlers,\n  DocumentType,\n} from '../../types/doc-source'\n\ntype DocRenderer = (content: DocContent, handlers: DocRenderHandlers) => React.ReactNode\n\n/**\n * Per-document-type renderer map. `markdown` is required (the lib does NOT\n * ship a default markdown renderer — embedders pick their own library +\n * sanitization to avoid an XSS surface in the lib).\n *\n * `pdf` / `google_sheet` / `figma` / `file` are optional — the lib provides\n * defaults from `components/embeds`. Override only when you want different\n * props than the default (e.g. a custom PDF toolbar, embedded credentials).\n */\nexport type DocumentTypeRenderers = { markdown: DocRenderer } & Partial<\n  Record<DocumentType, DocRenderer>\n>\n\nexport interface DocsHubPageProps\n  extends Omit<DocViewerProps, 'renderContent' | 'renderSkeleton' | 'showAIChat'> {\n  /** Per-document-type renderer map. `markdown` is REQUIRED. */\n  documentTypeRenderers: DocumentTypeRenderers\n\n  /** Renderer for unknown / future document types. Defaults to a lib-styled\n   *  \"Unsupported document type\" message. */\n  fallbackRenderer?: DocRenderer\n\n  /** Loading skeleton picker. Defaults: `markdown` / `undefined` →\n   *  `<MarkdownSkeleton>`, everything else → `<EmbedSkeleton>`. */\n  renderSkeleton?: (documentType: DocumentType | undefined) => React.ReactNode\n\n  /** Defaults to `true` (the embeddable wrapper favors the chat-enabled\n   *  experience). Only mounts the in-source RAG search bar\n   *  (`<DocSearchBar>`) — does NOT require `ChatRuntimeContext`. */\n  showAIChat?: boolean\n}\n\nconst DEFAULT_TITLE = 'Documents'\n\nconst defaultFallbackRenderer: DocRenderer = () => (\n  <div className=\"text-center py-16\">\n    <p className=\"text-ods-text-secondary\">Unsupported document type</p>\n  </div>\n)\n\n// When the DAL hasn't populated the URL field for a rich-content type, the\n// embed-viewer components render a broken iframe (empty src). Fall back to\n// the same lib-styled \"Unsupported document type\" panel the explicit\n// fallback uses — the surface is honest about the missing data instead of\n// pretending to load. `FileDownloadCard` handles its own missing-URL state\n// (hides the Download button), so it doesn't need this guard.\nconst defaultPdfRenderer: DocRenderer = (content, handlers) =>\n  content.fileUrl ? (\n    <PdfViewer src={content.fileUrl} fileName={content.fileName} />\n  ) : (\n    defaultFallbackRenderer(content, handlers)\n  )\n\nconst defaultGoogleSheetRenderer: DocRenderer = (content, handlers) =>\n  content.externalUrl ? (\n    <GoogleSheetsViewer externalUrl={content.externalUrl} fileName={content.fileName} />\n  ) : (\n    defaultFallbackRenderer(content, handlers)\n  )\n\nconst defaultFigmaRenderer: DocRenderer = (content, handlers) =>\n  content.externalUrl ? (\n    <FigmaEmbed url={content.externalUrl} title={content.fileName} loading=\"eager\" />\n  ) : (\n    defaultFallbackRenderer(content, handlers)\n  )\n\nconst defaultFileRenderer: DocRenderer = (content) => (\n  <FileDownloadCard\n    fileName={content.fileName}\n    mimeType={content.mimeType}\n    fileSize={content.fileSize}\n    fileUrl={content.fileUrl}\n  />\n)\n\nconst defaultRenderSkeleton = (documentType: DocumentType | undefined) =>\n  !documentType || documentType === 'markdown' ? (\n    <MarkdownSkeleton />\n  ) : (\n    // Forward the documentType so the embed skeleton renders the right\n    // shape (PDF=2 buttons, sheets/figma=1 button, file=centered card).\n    <EmbedSkeleton documentType={documentType} />\n  )\n\n/**\n * Embeddable docs-hub page. Bundles `<DocViewer>` with safe defaults so the\n * minimum embed is a one-line mount (consumer only has to supply\n * `documentTypeRenderers.markdown`).\n *\n * Used by the hub at `/knowledge-base` and `/data-room`, and by third-party\n * React apps that embed the docs experience behind their own proxy. See\n * `docs/EMBEDDING_DOCS_HUB.md` for the embedder setup.\n *\n * SEO note: this component is `'use client'` (via the docs barrel) — server-\n * side SEO is the host's responsibility. The hub's `<DocSeoContent>` is the\n * canonical implementation embedders can reference.\n */\nexport function DocsHubPage({\n  title = DEFAULT_TITLE,\n  documentTypeRenderers,\n  fallbackRenderer = defaultFallbackRenderer,\n  renderSkeleton = defaultRenderSkeleton,\n  showAIChat = true,\n  className = 'min-h-screen',\n  sidebarLabel = 'DOCUMENTATION',\n  ...docViewerProps\n}: DocsHubPageProps) {\n  const resolvedRenderers: DocumentTypeRenderers = {\n    markdown: documentTypeRenderers.markdown,\n    pdf: documentTypeRenderers.pdf ?? defaultPdfRenderer,\n    google_sheet: documentTypeRenderers.google_sheet ?? defaultGoogleSheetRenderer,\n    figma: documentTypeRenderers.figma ?? defaultFigmaRenderer,\n    file: documentTypeRenderers.file ?? defaultFileRenderer,\n  }\n\n  const renderContent: DocViewerProps['renderContent'] = (content, handlers) => {\n    const type = (content.documentType ?? 'markdown') as DocumentType\n    const renderer = resolvedRenderers[type] ?? fallbackRenderer\n    return renderer(content, handlers)\n  }\n\n  return (\n    <DocViewer\n      {...docViewerProps}\n      title={title}\n      showAIChat={showAIChat}\n      className={className}\n      sidebarLabel={sidebarLabel}\n      renderContent={renderContent}\n      renderSkeleton={renderSkeleton}\n    />\n  )\n}\n"]}