Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 8 additions & 89 deletions apps/website/src/components/shared/NavDesktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
<>
<span className="nav-panel-item-chip" aria-hidden="true">
{item.library ? (
<LibraryMark library={item.library} size={20} />
) : Icon ? (
<Icon size={16} aria-hidden={true} />
) : null}
</span>
<span className="nav-panel-item-text">
<span className="nav-panel-item-label">{item.label}</span>
<span className="nav-panel-item-desc">{item.description}</span>
</span>
</>
);
const onClick = () => {
trackNavItem(item, surface);
onNavigate?.();
};

if (item.external) {
return (
<a
href={item.href}
target="_blank"
rel="noopener noreferrer"
onClick={onClick}
className="nav-panel-item"
>
{body}
</a>
);
}
return (
<Link href={item.href} onClick={onClick} className="nav-panel-item">
{body}
</Link>
);
}

function Panel({ panel, id }: { panel: NavPanel; id: string }) {
return (
<div id={id} className="nav-panel" data-columns={panel.columns.length}>
<div className="nav-panel-cols">
{panel.columns.map((column, index) => (
<div key={column.heading ?? index} className="nav-panel-col">
{column.heading ? (
<span className="nav-panel-col-head">{column.heading}</span>
) : null}
{column.items.map((item) => (
<NavPanelItem key={item.ctaId} item={item} surface="nav" />
))}
</div>
))}
</div>
{panel.footer ? (
<div className="nav-panel-footer">
<span className="nav-panel-footer-lead">{panel.footer.lead}</span>
<NavPanelItem item={panel.footer} surface="nav" />
</div>
) : null}
<NavPanelBody
panel={panel}
surface="nav"
columnsClassName="nav-panel-cols"
columnClassName="nav-panel-col"
/>
</div>
);
}
Expand Down
40 changes: 7 additions & 33 deletions apps/website/src/components/shared/NavMobile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -436,38 +436,12 @@ export function NavMobile({

{panel ? (
<div className="nav-mobile-panel">
{panel.columns.map((column, index) => (
<div
key={column.heading ?? index}
className="nav-mobile-group"
>
{column.heading ? (
<span className="nav-panel-col-head">
{column.heading}
</span>
) : null}
{column.items.map((item) => (
<NavPanelItem
key={item.ctaId}
item={item}
surface="mobile_nav"
onNavigate={() => closeMobileMenu()}
/>
))}
</div>
))}
{panel.footer ? (
<div className="nav-panel-footer">
<span className="nav-panel-footer-lead">
{panel.footer.lead}
</span>
<NavPanelItem
item={panel.footer}
surface="mobile_nav"
onNavigate={() => closeMobileMenu()}
/>
</div>
) : null}
<NavPanelBody
panel={panel}
surface="mobile_nav"
columnClassName="nav-mobile-group"
onNavigate={() => closeMobileMenu()}
/>
</div>
) : null}
</div>
Expand Down
139 changes: 139 additions & 0 deletions apps/website/src/components/shared/NavPanelBody.tsx
Original file line number Diff line number Diff line change
@@ -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 = (
<>
<span className="nav-panel-item-chip" aria-hidden="true">
{item.library ? (
<LibraryMark library={item.library} size={20} />
) : Icon ? (
<Icon size={16} aria-hidden={true} />
) : null}
</span>
<span className="nav-panel-item-text">
<span className="nav-panel-item-label">{item.label}</span>
<span className="nav-panel-item-desc">{item.description}</span>
</span>
</>
);
const onClick = () => {
trackNavItem(item, surface);
onNavigate?.();
};

if (item.external) {
return (
<a
href={item.href}
target="_blank"
rel="noopener noreferrer"
onClick={onClick}
className="nav-panel-item"
>
{body}
</a>
);
}
return (
<Link href={item.href} onClick={onClick} className="nav-panel-item">
{body}
</Link>
);
}

/**
* 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) => (
<div key={column.heading ?? index} className={columnClassName}>
{column.heading ? (
<span className="nav-panel-col-head">{column.heading}</span>
) : null}
{column.items.map((item) => (
<NavPanelItem
key={item.ctaId}
item={item}
surface={surface}
onNavigate={onNavigate}
/>
))}
</div>
));

return (
<>
{columnsClassName ? (
<div className={columnsClassName}>{columns}</div>
) : (
columns
)}
{panel.footer ? (
<div className="nav-panel-footer">
<span className="nav-panel-footer-lead">{panel.footer.lead}</span>
<NavPanelItem
item={panel.footer}
surface={surface}
onNavigate={onNavigate}
/>
</div>
) : null}
</>
);
}