diff --git a/apps/website/src/components/shared/NavDesktop.tsx b/apps/website/src/components/shared/NavDesktop.tsx
index 7c1e25513..4f721e2ac 100644
--- a/apps/website/src/components/shared/NavDesktop.tsx
+++ b/apps/website/src/components/shared/NavDesktop.tsx
@@ -10,103 +10,22 @@ import {
import { Button } from '../ui/Button';
import { GitHubIcon } from '../ui/GitHubIcon';
import { GITHUB_REPO_URL } from '../../lib/positioning';
-import { LibraryMark } from '../docs/LibraryMark';
-import { NAV_TRIGGERS, type NavItem, type NavPanel } from './nav-config';
+import { NavPanelBody } from './NavPanelBody';
+import { NAV_TRIGGERS, type NavPanel } from './nav-config';
/** Long enough to cross the gap between trigger and panel diagonally. */
const OPEN_DELAY_MS = 100;
const CLOSE_DELAY_MS = 150;
-export function trackNavItem(item: NavItem, surface: 'nav' | 'mobile_nav') {
- const ctaId: `nav_${string}` | `mobile_nav_${string}` =
- surface === 'nav' ? `nav_${item.ctaId}` : `mobile_nav_${item.ctaId}`;
- if (item.external) {
- trackExternalLinkClick(item.href, {
- surface,
- cta_id: ctaId,
- cta_text: item.label,
- });
- return;
- }
- trackCtaClick({
- surface,
- destination_url: item.href,
- cta_id: ctaId,
- cta_text: item.label,
- });
-}
-
-export function NavPanelItem({
- item,
- surface,
- onNavigate,
-}: {
- item: NavItem;
- surface: 'nav' | 'mobile_nav';
- onNavigate?: () => void;
-}) {
- const Icon = item.icon;
- const body = (
- <>
-
- {item.library ? (
-
- ) : Icon ? (
-
- ) : null}
-
-
- {item.label}
- {item.description}
-
- >
- );
- const onClick = () => {
- trackNavItem(item, surface);
- onNavigate?.();
- };
-
- if (item.external) {
- return (
-
- {body}
-
- );
- }
- return (
-
- {body}
-
- );
-}
-
function Panel({ panel, id }: { panel: NavPanel; id: string }) {
return (
-
- {panel.columns.map((column, index) => (
-
- {column.heading ? (
- {column.heading}
- ) : null}
- {column.items.map((item) => (
-
- ))}
-
- ))}
-
- {panel.footer ? (
-
- {panel.footer.lead}
-
-
- ) : null}
+
);
}
diff --git a/apps/website/src/components/shared/NavMobile.tsx b/apps/website/src/components/shared/NavMobile.tsx
index cecad1578..524abdb4e 100644
--- a/apps/website/src/components/shared/NavMobile.tsx
+++ b/apps/website/src/components/shared/NavMobile.tsx
@@ -19,7 +19,7 @@ import { Button } from '../ui/Button';
import { GitHubIcon } from '../ui/GitHubIcon';
import { GITHUB_REPO_URL } from '../../lib/positioning';
import { DocsContextContent } from '../docs/DocsControlPlane';
-import { NavPanelItem } from './NavDesktop';
+import { NavPanelBody } from './NavPanelBody';
import { NAV_TRIGGERS } from './nav-config';
const toAnalyticsLibrary = (library: LibraryId | null): AnalyticsLibrary => {
@@ -436,38 +436,12 @@ export function NavMobile({
{panel ? (
- {panel.columns.map((column, index) => (
-
- {column.heading ? (
-
- {column.heading}
-
- ) : null}
- {column.items.map((item) => (
- closeMobileMenu()}
- />
- ))}
-
- ))}
- {panel.footer ? (
-
-
- {panel.footer.lead}
-
- closeMobileMenu()}
- />
-
- ) : null}
+
closeMobileMenu()}
+ />
) : null}
diff --git a/apps/website/src/components/shared/NavPanelBody.tsx b/apps/website/src/components/shared/NavPanelBody.tsx
new file mode 100644
index 000000000..9ba2c5656
--- /dev/null
+++ b/apps/website/src/components/shared/NavPanelBody.tsx
@@ -0,0 +1,139 @@
+'use client';
+
+import Link from 'next/link';
+import {
+ trackCtaClick,
+ trackExternalLinkClick,
+} from '../../lib/analytics/client';
+import { LibraryMark } from '../docs/LibraryMark';
+import type { NavItem, NavPanel } from './nav-config';
+
+export function trackNavItem(item: NavItem, surface: 'nav' | 'mobile_nav') {
+ const ctaId: `nav_${string}` | `mobile_nav_${string}` =
+ surface === 'nav' ? `nav_${item.ctaId}` : `mobile_nav_${item.ctaId}`;
+ if (item.external) {
+ trackExternalLinkClick(item.href, {
+ surface,
+ cta_id: ctaId,
+ cta_text: item.label,
+ });
+ return;
+ }
+ trackCtaClick({
+ surface,
+ destination_url: item.href,
+ cta_id: ctaId,
+ cta_text: item.label,
+ });
+}
+
+export function NavPanelItem({
+ item,
+ surface,
+ onNavigate,
+}: {
+ item: NavItem;
+ surface: 'nav' | 'mobile_nav';
+ onNavigate?: () => void;
+}) {
+ const Icon = item.icon;
+ const body = (
+ <>
+
+ {item.library ? (
+
+ ) : Icon ? (
+
+ ) : null}
+
+
+ {item.label}
+ {item.description}
+
+ >
+ );
+ const onClick = () => {
+ trackNavItem(item, surface);
+ onNavigate?.();
+ };
+
+ if (item.external) {
+ return (
+
+ {body}
+
+ );
+ }
+ return (
+
+ {body}
+
+ );
+}
+
+/**
+ * The contents of one nav panel — its columns, then its footer — shared by the
+ * desktop hover panel and the mobile drill-in stack.
+ *
+ * Only the wrapper class names and the mobile drawer's close callback differ
+ * between the two surfaces; the column/footer shape itself is the same, and
+ * writing it twice let the two drift. The caller still owns the outermost
+ * element, because that is where the surfaces genuinely diverge: desktop needs
+ * the panel's id and `data-columns`, mobile does not.
+ */
+export function NavPanelBody({
+ panel,
+ surface,
+ columnsClassName,
+ columnClassName,
+ onNavigate,
+}: {
+ panel: NavPanel;
+ surface: 'nav' | 'mobile_nav';
+ /** Desktop grids its columns inside a wrapper; the mobile stack has none. */
+ columnsClassName?: string;
+ columnClassName: string;
+ onNavigate?: () => void;
+}) {
+ const columns = panel.columns.map((column, index) => (
+
+ {column.heading ? (
+ {column.heading}
+ ) : null}
+ {column.items.map((item) => (
+
+ ))}
+
+ ));
+
+ return (
+ <>
+ {columnsClassName ? (
+ {columns}
+ ) : (
+ columns
+ )}
+ {panel.footer ? (
+
+ {panel.footer.lead}
+
+
+ ) : null}
+ >
+ );
+}