{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-GNVIMSJG.cjs","../src/components/onboarding-guides/onboarding-guides-catalog-view.tsx","../src/components/onboarding-guides/onboarding-guides-catalog-skeleton.tsx","../src/components/onboarding-guides/onboarding-guide-detail-view.tsx","../src/components/onboarding-guides/hooks/use-onboarding-guides.ts"],"names":["jsx","jsxs"],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-GNVIMSJG.cjs","sourcesContent":[null,"'use client'\n\n/**\n * Public-facing catalog view for `/onboarding-guides`.\n *\n * Two data modes:\n *   - **controlled** (hub SSR): pass `initialGuides` + `initialSections`\n *     (server-fetched; section changes re-render the host with new props).\n *   - **self-fetching** (config-only embed): omit the data props and pass\n *     `guidesEndpoint` + `sectionsEndpoint` (the api routes). The view fetches\n *     the guides (refetching on `?section=` change) + the section list itself —\n *     no host data layer. (Plain fetch + useEffect, the DeliveryLists pattern.)\n *\n * The view renders ONLY its body + its OWN controls (the RAG `<DocSearchBar>` +\n * dynamic section pills — onboarding's controls are content-driven, unlike the\n * static search/filter the `DevSectionView` chrome ships). It does NOT render the\n * page chrome: the CALLER wraps it in `<DevSectionPage sectionKey=\"onboarding\">`\n * (hero + back button), exactly like the roadmap / releases / delivery pages — so\n * every dev-center surface composes the same way and the view stays embeddable in\n * tabbed contexts. Card hrefs flow through `runtime.composeContentUrl`.\n */\n\nimport { useMemo, useTransition, type ReactNode } from 'react'\nimport { GraduationCap } from 'lucide-react'\n\nimport { useRouter, useSearchParams } from '../../embed-shims'\nimport { useSelfFetch } from '../../hooks/use-self-fetch'\nimport { DocSearchBar, useDocSearch } from '../shared/doc-search'\nimport { FilterPillRow } from '../ui/filter-pill-row'\nimport { LoadError } from '../ui/error-state'\nimport { OnboardingGuideCard } from '../chat/entity-cards/onboarding-guide-card'\nimport { OnboardingGuidesCatalogSkeleton } from './onboarding-guides-catalog-skeleton'\nimport { useChatRuntime } from '../../contexts/chat-runtime-context'\nimport type { OnboardingGuide } from '../chat/types/entities/onboarding-guide'\nimport { resolveContentHref } from '../../utils/content-href'\n\ntype SectionSummary = { section: string; section_order: number; count: number }\n\nexport interface OnboardingGuidesCatalogViewProps {\n  /** Controlled / SSR: server-fetched guides. Omit + pass `guidesEndpoint`\n   *  for the self-fetching config-only mode. */\n  initialGuides?: OnboardingGuide[]\n  initialSections?: SectionSummary[]\n  initialSection?: string\n  /** Self-fetch: GET list endpoint (the api route). Appends `?section=`. */\n  guidesEndpoint?: string\n  /** Self-fetch: GET section-summary endpoint (the api route). */\n  sectionsEndpoint?: string\n  /** Optional per-row card renderer override. */\n  renderCard?: (guide: OnboardingGuide) => ReactNode\n  /** Base path the catalog is mounted under (fallback href prefix + `?section=`\n   *  push target). Default `/onboarding-guides`. */\n  basePath?: string\n}\n\nexport function OnboardingGuidesCatalogView({\n  initialGuides,\n  initialSections,\n  initialSection = '',\n  guidesEndpoint,\n  sectionsEndpoint,\n  renderCard,\n  basePath = '/onboarding-guides',\n}: OnboardingGuidesCatalogViewProps) {\n  const router = useRouter()\n  const searchParams = useSearchParams()\n  const [isPending, startTransition] = useTransition()\n  const runtime = useChatRuntime()\n\n  // Self-fetch only when the host supplied NO data but DID give an endpoint.\n  const selfFetch = initialGuides === undefined && guidesEndpoint !== undefined\n  // Active section: from the URL when self-fetching, from the prop (host RSC\n  // already read the URL server-side) when controlled.\n  const sectionValue = selfFetch ? searchParams.get('section') ?? '' : initialSection\n  const activeSection = sectionValue || 'all'\n\n  // Self-fetch the guides (refetched whenever `?section=` changes — it's folded\n  // into the url) + the section summaries. Controlled mode passes `null` urls so\n  // the hook never fetches. The guides fetch carries the error/retry affordance.\n  const guidesQs = sectionValue ? `?section=${encodeURIComponent(sectionValue)}` : ''\n  const guidesRes = useSelfFetch<{ data?: OnboardingGuide[] }>(\n    selfFetch && guidesEndpoint ? `${guidesEndpoint}${guidesQs}` : null,\n  )\n  const sectionsRes = useSelfFetch<SectionSummary[]>(\n    selfFetch && sectionsEndpoint ? sectionsEndpoint : null,\n  )\n\n  // Invariant: `guidesRes.data` is `null` until the first fetch resolves, then\n  // `{ data: [...] }`. The `?.data ?? []` chain collapses never-fetched (null) and\n  // legitimately-empty ([]) to the same `[]` — so the first-load skeleton gate below\n  // keys on the OUTER `guidesRes.data == null` to distinguish them (an empty section\n  // toggle keeps the chrome; only a true cold start shows the full-page skeleton).\n  const guides = initialGuides ?? guidesRes.data?.data ?? []\n  const sections = initialSections ?? (Array.isArray(sectionsRes.data) ? sectionsRes.data : [])\n  const isLoading = selfFetch ? guidesRes.isLoading : false\n\n  // Section grouping. Data arrives already filtered (server-side `?section=`\n  // for the host, or our `?section=` fetch); this just buckets visible rows.\n  const grouped = useMemo(() => {\n    const map = new Map<string, { section_order: number; guides: OnboardingGuide[] }>()\n    for (const g of guides) {\n      const existing = map.get(g.section)\n      if (existing) {\n        if (g.section_order < existing.section_order) existing.section_order = g.section_order\n        existing.guides.push(g)\n      } else {\n        map.set(g.section, { section_order: g.section_order, guides: [g] })\n      }\n    }\n    for (const entry of map.values()) {\n      entry.guides.sort((a, b) => a.step_order - b.step_order || a.title.localeCompare(b.title))\n    }\n    return Array.from(map.entries())\n      .map(([section, info]) => ({ section, ...info }))\n      .sort((a, b) => a.section_order - b.section_order || a.section.localeCompare(b.section))\n  }, [guides])\n\n  const sectionFilterOptions = useMemo(\n    () => [\n      { value: 'all', label: `All (${guides.length})` },\n      ...sections.map((s) => ({ value: s.section, label: `${s.section} (${s.count})` })),\n    ],\n    [guides.length, sections],\n  )\n\n  // Section pill change → push `?section=X`. Controlled mode: host RSC\n  // re-fetches. Self-fetch mode: our guides effect re-fetches on the URL change.\n  const setSection = (value: string) => {\n    const params = new URLSearchParams(searchParams.toString())\n    if (value === 'all') {\n      params.delete('section')\n    } else {\n      params.set('section', value)\n    }\n    const qs = params.toString()\n    startTransition(() => {\n      router.push(qs ? `${basePath}?${qs}` : basePath)\n    })\n  }\n\n  // Search bar — scoped to onboarding-guides only via RAG-search `tableIds`.\n  const source = runtime?.source ?? 'openframe'\n  const docSearch = useDocSearch({\n    source,\n    baseRoute: basePath,\n    onNavigate: (path) => router.push(path),\n    tableIds: ['onboarding-guides'],\n  })\n\n  // Per-row card renderer — runtime-composed href, fallback to relative.\n  const defaultRenderCard = (guide: OnboardingGuide) => {\n    const cta = resolveContentHref(runtime?.composeContentUrl, {\n      type: 'onboarding_guide',\n      slug: guide.slug,\n      basePath,\n      platforms: guide.onboarding_guide_platforms,\n    })\n    return (\n      <OnboardingGuideCard\n        guide={guide}\n        href={cta.href}\n        targetPlatform={cta.targetPlatform}\n        size=\"catalog\"\n      />\n    )\n  }\n  const renderCardFn = renderCard ?? defaultRenderCard\n\n  const preControls = (\n    <div className=\"space-y-4\">\n      <DocSearchBar\n        placeholder=\"Search onboarding guides, releases, case studies…\"\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      {sections.length > 0 && (\n        <FilterPillRow\n          label=\"Section\"\n          selectedValue={activeSection}\n          onValueChange={setSection}\n          options={sectionFilterOptions}\n        />\n      )}\n    </div>\n  )\n\n  // A failed guides fetch → a RETRYABLE error (not the indistinguishable-from-\n  // empty \"no guides found\" state) — parity with the sibling self-fetch views.\n  if (selfFetch && guidesRes.error) {\n    return <LoadError message=\"Failed to load onboarding guides.\" onRetry={guidesRes.reload} />\n  }\n  // Full-page skeleton ONLY on the very first load (no data has loaded yet) — a\n  // later section toggle that legitimately returns ZERO guides keeps the page\n  // chrome (search + section pills) and dims the grid via the className below.\n  if (selfFetch && guidesRes.data == null && isLoading) {\n    return <OnboardingGuidesCatalogSkeleton />\n  }\n\n  return (\n    <div className=\"w-full flex flex-col gap-10\">\n      {preControls}\n      {guides.length === 0 ? (\n        <div className=\"text-center py-16\">\n          <GraduationCap className=\"h-12 w-12 text-ods-text-secondary mx-auto mb-4\" />\n          <h2 className=\"text-ods-text-primary text-h3 mb-2\">\n            No onboarding guides found\n          </h2>\n          <p className=\"text-ods-text-secondary text-h6\">\n            {activeSection !== 'all'\n              ? 'No guides in this section yet.'\n              : \"We're working on the onboarding library. Check back soon.\"}\n          </p>\n        </div>\n      ) : (\n        <div className={isPending || isLoading ? 'opacity-60 transition-opacity space-y-10' : 'space-y-10'}>\n          {grouped.map((sec) => (\n            <section key={sec.section} className=\"space-y-4\">\n              <h2 className=\"text-h3 text-ods-text-primary flex items-center gap-2\">\n                {sec.section}\n                <span className=\"inline-flex items-center justify-center rounded-full bg-ods-text-secondary/20 text-ods-text-secondary text-h6 px-2 py-0.5\">\n                  {sec.guides.length}\n                </span>\n              </h2>\n              {/* HORIZONTAL catalog list — single column so consecutive steps\n                  read top-to-bottom; a grid would visually reorder them. */}\n              <ul className=\"flex flex-col gap-4\">\n                {sec.guides.map((guide) => (\n                  <li key={guide.id}>{renderCardFn(guide)}</li>\n                ))}\n              </ul>\n            </section>\n          ))}\n        </div>\n      )}\n    </div>\n  )\n}\n","'use client'\n\nimport { OnboardingGuideCardSkeleton } from '../chat/entity-cards/onboarding-guide-card'\n\n/**\n * Loading skeleton for `/onboarding-guides` — CHROME-LESS, exactly like the\n * loaded `<OnboardingGuidesCatalogView>`. The HOST page owns the\n * `<DevSectionPage sectionKey=\"onboarding\">` shell (hero + back button);\n * this component renders only what replaces the view: the search/pill\n * placeholder block and the section card lists.\n *\n * HISTORY: this used to mount its own `<DevSectionPage>` — nested inside the\n * page's shell that double-rendered the hero + back button during loading\n * (the view was made chrome-less in the page restructure; the skeleton was\n * missed). Keep BOTH chrome-less.\n *\n * Wrapper mirrors the loaded view's `w-full flex flex-col gap-10`; per-card\n * height matches the loaded catalog card so resolve shifts are zero.\n */\nexport function OnboardingGuidesCatalogSkeleton() {\n  return (\n    <div className=\"w-full flex flex-col gap-10 animate-pulse\">\n      {/* Search input placeholder — matches `<SearchInput>` h-12 — plus the\n          section pill row (~74 px incl. padding), same as the loaded\n          preControls block. */}\n      <div className=\"space-y-4\">\n        <div className=\"h-12 w-full bg-ods-card border border-ods-border rounded-md\" />\n        <div className=\"flex flex-wrap items-center gap-3 p-4 bg-ods-card border border-ods-border rounded-lg\">\n          <div className=\"h-4 w-14 bg-ods-border/60 rounded\" />\n          {[0, 1, 2, 3].map((i) => (\n            <div\n              key={i}\n              className=\"h-10 w-24 bg-ods-card border border-ods-border rounded-md\"\n            />\n          ))}\n        </div>\n      </div>\n      <div className=\"space-y-10\">\n        {[4, 3, 3].map((cardCount, sectionIdx) => (\n          <section key={sectionIdx} className=\"space-y-4\">\n            <h2 className=\"text-h3 tracking-[-0.36px] text-ods-text-primary flex items-center gap-2\">\n              <span className=\"h-6 w-40 bg-ods-border/70 rounded\" />\n              <span className=\"h-5 w-8 bg-ods-text-secondary/20 rounded-full\" />\n            </h2>\n            <ul className=\"flex flex-col gap-4\">\n              {Array.from({ length: cardCount }).map((_, cardIdx) => (\n                <li key={cardIdx}>\n                  <OnboardingGuideCardSkeleton size=\"catalog\" />\n                </li>\n              ))}\n            </ul>\n          </section>\n        ))}\n      </div>\n    </div>\n  )\n}\n","'use client'\n\n/**\n * Public-facing detail view for `/onboarding-guides/<slug>`.\n *\n * Two modes:\n *   - **controlled** (hub SSR): pass `initialData` (a server-fetched guide).\n *   - **self-fetching** (config-only embed): pass `slug` + `guideEndpoint`\n *     (the api route) and the view fetches the guide itself — no host data\n *     layer. (Plain fetch + useEffect, the DeliveryLists pattern.)\n *\n * Everything else flows through lib primitives + the ChatRuntime context\n * (markdown, video warmup, captions, related-card hrefs via\n * `runtime.composeContentUrl`). Optional `MarkdownRenderer` lets hosts swap a\n * renderer with extra plugins.\n */\n\nimport { Fragment, type ComponentType, type ReactNode } from 'react'\nimport { PageLayout } from '../layout/page-layout'\n\nimport { useRouter } from '../../embed-shims'\n// PageShell (max-w-[1920px]) — the guide detail content must share the SAME\n// container sizing as the related-content/FAQ rail the host page renders\n// below it (which uses the wide shell). ArticleDetailLayout (1280px) made\n// the detail block visibly narrower than the rail.\nimport { PageShell } from '../layout/article-detail-layout'\nimport { DetailPageSkeleton } from '../shared/detail-page-skeleton'\nimport { EntityVideoSection } from '../features/entity-video-section'\nimport { VideoBitesStrip } from '../features/video-bites-strip'\nimport { toStripProfile } from '../features/video-bites-shared'\nimport { useVideoWarmup } from '../features/use-video-warmup'\nimport { useCaptions } from '../features/use-captions'\nimport { RichMarkdownRenderer } from '../ui/markdown'\nimport { EntityTagBadges } from '../features/entity-tag-badges'\nimport { LoadError } from '../ui/error-state'\nimport { ArticleAuthorByline } from '../shared/article-author-byline'\nimport { EntityAuthorCard } from '../chat/entity-cards/entity-author-card'\nimport { OnboardingGuideCard } from '../chat/entity-cards/onboarding-guide-card'\nimport { useChatRuntime } from '../../contexts/chat-runtime-context'\nimport type { OnboardingGuide } from '../chat/types/entities/onboarding-guide'\nimport type { VideoTeaser } from '../../types/video-processing'\nimport { resolveContentHref } from '../../utils/content-href'\nimport { buildOgPlaceholderUrl } from '../../utils/og-placeholder'\nimport { useSelfFetch } from '../../hooks/use-self-fetch'\n\nexport interface OnboardingGuideDetailViewProps {\n  /** Server-fetched guide (controlled / SSR mode). Omit it and pass\n   *  `slug` + `guideEndpoint` for the self-fetching config-only mode. */\n  initialData?: OnboardingGuide\n  /** Self-fetch: the guide's slug (the host reads it from its route). */\n  slug?: string\n  /** Self-fetch: builds the GET url for a single guide (the api route).\n   *  e.g. `(s) => \\`/content/api/onboarding-guides/${s}\\``. */\n  guideEndpoint?: (slug: string) => string\n  related?: OnboardingGuide[]\n  /** Link target for the author name in the metadata grid — the host\n   *  computes it (public author page; absent ⇒ plain text). */\n  authorHref?: string\n  /** Bio paragraph for the end-of-article author byline. Defaults to the\n   *  guide payload's `author.about` (hubs that hydrate it can omit this). */\n  authorBio?: string | null\n  /** Byline fallback paragraph when the bio is empty — the host passes its\n   *  platform-aware copy (the lib has no config awareness). Absent ⇒ the\n   *  byline renders nothing below the name when the bio is empty. */\n  fallbackBio?: string | null\n  /** Optional markdown renderer override. Defaults to lib\n   *  `<RichMarkdownRenderer>`. */\n  MarkdownRenderer?: ComponentType<{ content: string }>\n  /** Optional per-row related-card renderer override. */\n  renderRelatedCard?: (guide: OnboardingGuide) => ReactNode\n  /** Back-link target. Defaults to `basePath`. */\n  backHref?: string\n  /** Back-link label. Defaults to \"Back to Getting Started\". */\n  backLabel?: string\n  /** Base path the related-card hrefs default to when\n   *  `runtime.composeContentUrl` is not wired. Default `/onboarding-guides`. */\n  basePath?: string\n  /** Optional slot rendered inside the page chrome, BELOW the article + related\n   *  guides — e.g. the hub's cross-type related-content / FAQ rail. Lets the hub\n   *  mount this view directly (no local wrapper); embedders omit it. */\n  relatedContent?: ReactNode\n  /** Render the standalone `<PageShell>`. Default true. Pass false when the host\n   *  layout already provides the page container — only the padding box renders,\n   *  avoiding a nested `<main>`. */\n  shell?: boolean\n}\n\nexport function OnboardingGuideDetailView({\n  authorHref,\n  authorBio,\n  fallbackBio,\n  initialData,\n  slug,\n  guideEndpoint,\n  related = [],\n  MarkdownRenderer = RichMarkdownRenderer,\n  renderRelatedCard,\n  backHref,\n  backLabel = 'Back to Getting Started',\n  basePath = '/onboarding-guides',\n  relatedContent,\n  shell = true,\n}: OnboardingGuideDetailViewProps) {\n  const resolvedBackHref = backHref ?? basePath\n  const runtime = useChatRuntime()\n  const captions = useCaptions()\n  const router = useRouter()\n  // `shell` true → standalone `<PageShell>`; false → padding-only box (no nested\n  // <main>) for hosts whose layout already provides the container.\n  const renderShell = (node: ReactNode) =>\n    shell ? <PageShell>{node}</PageShell> : <div className=\"page-shell-content\">{node}</div>\n\n  // Controlled (hub SSR `initialData`) OR self-fetch by slug (config-only embed).\n  const url = initialData ? null : slug && guideEndpoint ? guideEndpoint(slug) : null\n  const { data: guide, isLoading, error, reload } = useSelfFetch<OnboardingGuide>(url, { initialData })\n\n  // Hooks must run unconditionally — feed them optional-chained values so they\n  // no-op while the guide is still loading.\n  const { ref: videoWarmupRef } = useVideoWarmup<HTMLDivElement>({\n    videoUrl: guide?.main_video_url,\n    supabaseStorageOrigin: runtime?.endpoints.supabaseStorageOrigin,\n  })\n\n  if (error || (!guide && !isLoading)) {\n    return renderShell(<LoadError message=\"Guide not found.\" onRetry={reload} />)\n  }\n  if (!guide) {\n    // Skeleton (not a bare \"Loading…\") for parity with every other shared view —\n    // catalog, roadmap, releases all render a skeleton on first load, so the detail\n    // page shouldn't flash text then content. `bare` + `PageShell` so the loading\n    // state matches the loaded page's full width / padding / min-height.\n    return renderShell(\n      // Match the loaded page's top offset (TitleBlock's\n      // `pt-[var(--spacing-system-l)]`) so content doesn't jump on load.\n      <div className=\"pt-[var(--spacing-system-l)]\">\n        <DetailPageSkeleton bare showImageGallery={false} />\n      </div>\n    )\n  }\n\n  // Track URLs via the ONE captions configurer (`useCaptions`); same-origin\n  // hosts get the relative `/api/captions/...` default. Zero per-page wiring.\n  const { captionsUrl, highlightCaptionsUrl } = captions.forEntity('onboarding_guide', guide)\n\n  const videoPoster =\n    guide.main_video_thumbnail ||\n    guide.featured_image ||\n    guide.og_image_url ||\n    // `buildOgPlaceholderUrl` always returns a usable string (relative route at\n    // worst), so it's the terminal fallback — no trailing `|| undefined` needed.\n    buildOgPlaceholderUrl(runtime?.endpoints, guide.title, { aspect: 'wide' })\n\n  const defaultRenderRelatedCard = (g: OnboardingGuide) => {\n    const cta = resolveContentHref(runtime?.composeContentUrl, {\n      type: 'onboarding_guide',\n      slug: g.slug,\n      basePath,\n      platforms: g.onboarding_guide_platforms,\n    })\n    return <OnboardingGuideCard guide={g} href={cta.href} targetPlatform={cta.targetPlatform} />\n  }\n  const renderRelatedCardFn = renderRelatedCard ?? defaultRenderRelatedCard\n\n  return renderShell(\n    <PageLayout\n        title={guide.title}\n        titleSize=\"h1\"\n        titleWrap\n        backButton={{ label: backLabel, onClick: () => router.push(resolvedBackHref) }}\n      >\n        {/* Tags — flat onboarding_guide_tags[] from entity_tags. */}\n        <EntityTagBadges tags={(guide as any).onboarding_guide_tags} />\n\n        {/* Metadata grid — Section · Step | Published | Author. */}\n        <EntityAuthorCard\n          author={guide.author}\n          authorHref={authorHref}\n          publishedAt={guide.published_at}\n          extraCells={[\n            {\n              value: `${guide.section} · Step ${guide.step_order}`,\n              label: 'Section',\n              uppercase: false,\n            },\n          ]}\n        />\n\n        {/* Video. `main_video_url` (Mux/MP4) and `youtube_url` are independent\n            columns — either populated renders the player. */}\n        {(guide.main_video_url || guide.youtube_url) && (\n          <div ref={videoWarmupRef}>\n            <EntityVideoSection\n              mainVideoUrl={guide.main_video_url}\n              youtubeUrl={guide.youtube_url || undefined}\n              highlightVideoUrl={guide.highlight_video_url}\n              mainVideoPoster={videoPoster}\n              highlightVideoThumbnail={guide.highlight_video_thumbnail || undefined}\n              videoSummary={undefined}\n              videoBites={undefined}\n              title={guide.title}\n              srtContent={guide.srt_content}\n              captionsUrl={captionsUrl}\n              highlightCaptionsUrl={highlightCaptionsUrl}\n              MarkdownRenderer={MarkdownRenderer}\n            />\n          </div>\n        )}\n\n        {/* Markdown body */}\n        {guide.content && (\n          <div className=\"space-y-4\">\n            <MarkdownRenderer content={guide.content} />\n          </div>\n        )}\n\n        {/* Video Bites — unified strip; renders nothing when no published bites. */}\n        <VideoBitesStrip\n          bites={(guide.video_bites as VideoTeaser[] | null) ?? []}\n          filterPublished={true}\n          showTitle={false}\n          profile={toStripProfile(guide.author)}\n          href={`/onboarding-guides/${guide.slug}`}\n        />\n\n        {/* End-of-article author byline (avatar + linked name + bio) — the\n            author DESCRIPTION block every article-shaped detail page renders.\n            Hidden when the guide has no author (the byline returns null). */}\n        <ArticleAuthorByline\n          author={guide.author?.full_name ?? null}\n          avatar={guide.author?.avatar_url}\n          jobTitle={guide.author?.job_title}\n          bio={authorBio ?? guide.author?.about}\n          href={authorHref}\n          fallbackBio={fallbackBio}\n        />\n\n        {/* Related — same-section, ordered by step. */}\n        {related.length > 0 && (\n          <div className=\"space-y-4 pt-8 border-t border-ods-border\">\n            <h2 className=\"text-h3 tracking-[-0.36px] text-ods-text-primary\">\n              More in {guide.section}\n            </h2>\n            <ul className=\"flex flex-col gap-3\">\n              {related.map((r) => (\n                <li key={r.id}>{renderRelatedCardFn(r)}</li>\n              ))}\n            </ul>\n          </div>\n        )}\n\n        {/*\n         * Host slot — cross-type related-content / FAQ rail.\n         *\n         * KEYED DELIBERATELY. This is one member of PageLayout's static children\n         * array, and the only member whose element may be created in a SERVER\n         * component and crossed over the RSC boundary. Static JSX children are\n         * normally exempt from the key warning because `jsxs` runs\n         * `validateChildKeys` and stamps `_store.validated = 1` on each element —\n         * but a slot arriving over the wire is referenced as a lazy chunk wrapper,\n         * so validation marks the WRAPPER and the fulfilled path never copies the\n         * flag onto the revived element. It reaches the reconciler with\n         * `validated: 0, key: null` and warns, blaming PageLayout (the owner of the\n         * enclosing element) rather than anything actually at fault.\n         *\n         * A keyed Fragment emits no DOM, so rendered output is byte-identical.\n         * Do NOT \"fix\" this in PageLayout via `React.Children.toArray` — that would\n         * auto-key every consumer's children and change reconciliation identity for\n         * all of them, and that file carries an explicit FROZEN contract.\n         */}\n        <Fragment key=\"related-content\">{relatedContent}</Fragment>\n      </PageLayout>\n  )\n}\n","'use client'\n\n/**\n * React Query hooks for Onboarding Guides — PUBLIC reads only.\n *\n * Admin mutations (create/update/delete/publish/stats/save-highlight)\n * stay HUB-side in `hooks/use-onboarding-guides.ts` and import the\n * `onboardingGuideKeys.admin` sub-namespace from this module so the\n * cache namespace `['onboarding-guides']` has a single source of truth.\n *\n * Endpoints (`/api/onboarding-guides*`) are host-supplied — non-Next.js\n * embedders must reverse-proxy the same routes. Same precedent as the\n * tickets list hook.\n */\n\nimport { useQuery } from '@tanstack/react-query'\n\nimport type {\n  OnboardingGuide,\n  OnboardingGuideFilters,\n  OnboardingGuideListResponse,\n  OnboardingGuideSectionSummary,\n} from '../../chat/types/entities/onboarding-guide'\n\n/**\n * Cache key builder for the `['onboarding-guides']` namespace.\n *\n * Includes BOTH public-read sub-keys (`lists`, `list`, `details`,\n * `detail`, `sections`) AND the `admin` sub-namespace. Admin mutations\n * live hub-side but invalidate against this same builder so a single\n * `qc.invalidateQueries({ queryKey: onboardingGuideKeys.all })` from\n * an admin hook also clears the public read cache.\n */\nexport const onboardingGuideKeys = {\n  all: ['onboarding-guides'] as const,\n  lists: () => [...onboardingGuideKeys.all, 'list'] as const,\n  list: (filters: OnboardingGuideFilters) =>\n    [...onboardingGuideKeys.lists(), filters] as const,\n  details: () => [...onboardingGuideKeys.all, 'detail'] as const,\n  detail: (slug: string) => [...onboardingGuideKeys.details(), slug] as const,\n  sections: () => [...onboardingGuideKeys.all, 'sections'] as const,\n  admin: {\n    all: ['admin', 'onboarding-guides'] as const,\n    lists: () => ['admin', 'onboarding-guides', 'list'] as const,\n    detail: (slug: string) =>\n      ['admin', 'onboarding-guide', slug] as const,\n    stats: () => ['admin', 'onboarding-guides', 'stats'] as const,\n  },\n}\n\nexport function useOnboardingGuides(filters?: OnboardingGuideFilters) {\n  return useQuery({\n    queryKey: onboardingGuideKeys.list(filters || {}),\n    queryFn: async (): Promise<OnboardingGuideListResponse> => {\n      const params = new URLSearchParams()\n      if (filters?.search) params.set('search', filters.search)\n      if (filters?.section) params.set('section', filters.section)\n      if (filters?.limit) params.set('limit', filters.limit.toString())\n      if (filters?.offset) params.set('offset', filters.offset.toString())\n      const res = await fetch(`/api/onboarding-guides?${params}`)\n      if (!res.ok) throw new Error('Failed to fetch onboarding guides')\n      return res.json()\n    },\n  })\n}\n\nexport function useOnboardingGuide(slug: string | undefined) {\n  return useQuery({\n    queryKey: onboardingGuideKeys.detail(slug || ''),\n    queryFn: async (): Promise<OnboardingGuide> => {\n      const res = await fetch(`/api/onboarding-guides/${slug}`)\n      if (!res.ok) throw new Error('Failed to fetch onboarding guide')\n      return res.json()\n    },\n    enabled: !!slug,\n  })\n}\n\nexport function useOnboardingGuideSections() {\n  return useQuery({\n    queryKey: onboardingGuideKeys.sections(),\n    queryFn: async (): Promise<OnboardingGuideSectionSummary[]> => {\n      const res = await fetch('/api/onboarding-guides/sections')\n      if (!res.ok)\n        throw new Error('Failed to fetch onboarding-guide sections')\n      return res.json()\n    },\n    staleTime: 0,\n  })\n}\n"]}