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
6 changes: 4 additions & 2 deletions docs/src/app/(home)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { getGitHubStars } from '@/lib/github-stars';
import { SiteNav } from './site-nav';

export default function Layout({ children }: LayoutProps<'/'>) {
export default async function Layout({ children }: LayoutProps<'/'>) {
const stars = await getGitHubStars();
return (
<>
<SiteNav />
<SiteNav stars={stars} />
<main>{children}</main>
</>
);
Expand Down
234 changes: 161 additions & 73 deletions docs/src/app/(home)/site-nav.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { Menu, X } from 'lucide-react';
import { Menu, Star, X } from 'lucide-react';
import Link from 'next/link';
import type { FC, SVGProps } from 'react';
import { useEffect, useRef, useState } from 'react';
Expand All @@ -17,16 +17,12 @@ type NavLink = {
external: boolean;
icon?: FC<SVGProps<SVGSVGElement>>;
iconOnly?: boolean;
showStars?: boolean;
};

const navLinks: NavLink[] = [
{ href: '/docs', label: 'Docs', external: false },
{
href: 'https://github.com/inkeep/open-knowledge',
label: 'GitHub',
external: true,
icon: GitHubIcon,
},
const docsLink: NavLink = { href: '/docs', label: 'Docs', external: false };

const socialLinks: NavLink[] = [
{
href: 'https://x.com/OpenKnowledgeAI',
label: 'X',
Expand All @@ -43,9 +39,124 @@ const navLinks: NavLink[] = [
},
];

const githubLink: NavLink = {
href: 'https://github.com/inkeep/open-knowledge',
label: 'GitHub',
external: true,
icon: GitHubIcon,
showStars: true,
};

const mobileLinks: NavLink[] = [docsLink, ...socialLinks, githubLink];

const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])';

export function SiteNav() {
const starFormatter = new Intl.NumberFormat('en-US', {
notation: 'compact',
maximumFractionDigits: 1,
});

const fullStarFormatter = new Intl.NumberFormat('en-US');

function NavLinkContent({ link }: { link: NavLink }) {
const Icon = link.icon;
return (
<>
{Icon ? <Icon className="size-4 shrink-0" aria-hidden="true" /> : null}
{link.iconOnly ? null : link.label}
</>
);
}

function NavItem({ link, className }: { link: NavLink; className: string }) {
const ariaLabel = link.iconOnly ? link.label : undefined;
return link.external ? (
<a
href={link.href}
target="_blank"
rel="noreferrer"
aria-label={ariaLabel}
className={className}
>
<NavLinkContent link={link} />
</a>
) : (
<Link href={link.href} aria-label={ariaLabel} className={className}>
<NavLinkContent link={link} />
</Link>
);
}

function StarCount({ stars }: { stars: number }) {
return (
<>
<Star
className="size-3.5 shrink-0 text-golden-sun-300 fill-golden-sun-300"
aria-hidden="true"
/>
{starFormatter.format(stars)}
</>
);
}

function GitHubStarButton({
link,
stars,
variant,
}: {
link: NavLink;
stars: number | null;
variant: 'pill' | 'row';
}) {
const Icon = link.icon;
const title = stars != null ? `${fullStarFormatter.format(stars)} GitHub stars` : undefined;

if (variant === 'row') {
return (
<a
href={link.href}
target="_blank"
rel="noreferrer"
title={title}
className="flex items-center justify-between gap-2 rounded-md px-3 py-2 text-slide-text transition-colors hover:bg-slide-bg-elevated"
>
<span className="flex items-center gap-2">
{Icon ? <Icon className="size-4 shrink-0" aria-hidden="true" /> : null}
{link.label}
</span>
{stars != null ? (
<span className="flex items-center gap-1.5 tabular-nums text-slide-muted">
<StarCount stars={stars} />
</span>
) : null}
</a>
);
}

return (
<a
href={link.href}
target="_blank"
rel="noreferrer"
className="inline-flex items-stretch overflow-hidden rounded-full border text-slide-muted hover:text-slide-text transition-colors hover:bg-slide-bg-elevated h-9"
>
<span className="flex items-center gap-1.5 px-2.5 py-1.5">
{Icon ? <Icon className="size-4 shrink-0" aria-hidden="true" /> : null}
{link.label}
</span>
{stars != null ? (
<span
title={title}
className="flex items-center gap-1.5 border-l px-2.5 py-1.5 tabular-nums"
>
<StarCount stars={stars} />
</span>
) : null}
</a>
);
}

export function SiteNav({ stars }: { stars: number | null }) {
const [open, setOpen] = useState(false);
const triggerRef = useRef<HTMLButtonElement>(null);
const menuRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -92,41 +203,34 @@ export function SiteNav() {
return (
<header className="sticky top-0 z-50 bg-fd-background/80 backdrop-blur supports-backdrop-filter:bg-fd-background/70">
<div className="container mx-auto flex h-16 items-center justify-between px-6">
<Link href="/" aria-label="OpenKnowledge home" className="inline-flex items-center">
<OkWordmark aria-label="OpenKnowledge" className="h-8 w-auto text-slide-text" />
</Link>

<nav className="hidden items-center gap-6 text-sm text-slide-muted md:flex uppercase font-mono">
{navLinks.map((link) => {
const Icon = link.icon;
const content = (
<>
{Icon ? <Icon className="size-4 shrink-0" aria-hidden="true" /> : null}
{link.iconOnly ? null : link.label}
</>
);
return link.external ? (
<a
key={link.href}
href={link.href}
target="_blank"
rel="noreferrer"
aria-label={link.iconOnly ? link.label : undefined}
className="inline-flex items-center gap-1.5 transition-colors hover:text-slide-text"
>
{content}
</a>
) : (
<Link
key={link.href}
href={link.href}
aria-label={link.iconOnly ? link.label : undefined}
className="inline-flex items-center gap-1.5 transition-colors hover:text-slide-text"
>
{content}
</Link>
);
})}
<div className="flex items-center gap-6">
<Link href="/" aria-label="OpenKnowledge home" className="inline-flex items-center">
<OkWordmark aria-label="OpenKnowledge" className="h-8 w-auto text-slide-text" />
</Link>
<nav
aria-label="Primary"
className="hidden items-center gap-6 text-sm text-slide-muted md:flex uppercase font-mono"
>
<NavItem
link={docsLink}
className="inline-flex items-center gap-1.5 transition-colors hover:text-slide-text"
/>
</nav>
</div>

<nav
aria-label="Secondary"
className="hidden items-center gap-6 text-sm text-slide-muted md:flex uppercase font-mono"
>
{socialLinks.map((link) => (
<NavItem
key={link.href}
link={link}
className="inline-flex items-center gap-1.5 transition-colors hover:text-slide-text"
/>
))}
<span aria-hidden="true" className="h-5 w-px bg-slide-border" />
<GitHubStarButton link={githubLink} stars={stars} variant="pill" />
<MarketingButton href={DOWNLOAD_ROUTE} size="sm">
Download
</MarketingButton>
Expand Down Expand Up @@ -155,37 +259,21 @@ export function SiteNav() {
hidden={!open}
className="border-t bg-fd-background md:hidden"
>
<nav className="container mx-auto flex flex-col gap-1 px-6 py-4 text-base uppercase font-mono">
{navLinks.map((link) => {
const Icon = link.icon;
const content = (
<>
{Icon ? <Icon className="size-4 shrink-0" aria-hidden="true" /> : null}
{link.iconOnly ? null : link.label}
</>
);
return link.external ? (
<a
key={link.href}
href={link.href}
target="_blank"
rel="noreferrer"
aria-label={link.iconOnly ? link.label : undefined}
className="flex items-center gap-2 rounded-md px-3 py-2 text-slide-text transition-colors hover:bg-slide-bg-elevated"
>
{content}
</a>
<nav
aria-label="Mobile"
className="container mx-auto flex flex-col gap-1 px-6 py-4 text-base uppercase font-mono"
>
{mobileLinks.map((link) =>
link.showStars ? (
<GitHubStarButton key={link.href} link={link} stars={stars} variant="row" />
) : (
<Link
<NavItem
key={link.href}
href={link.href}
aria-label={link.iconOnly ? link.label : undefined}
link={link}
className="flex items-center gap-2 rounded-md px-3 py-2 text-slide-text transition-colors hover:bg-slide-bg-elevated"
>
{content}
</Link>
);
})}
/>
),
)}
<MarketingButton href={DOWNLOAD_ROUTE} size="md" className="text-base" showIcon>
Download
</MarketingButton>
Expand Down
1 change: 1 addition & 0 deletions docs/src/app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
--color-gray-light: #5f5c62;
--color-golden-sun-100: #ffefd9;
--color-golden-sun-200: #fcdfb6;
--color-golden-sun-300: #f8d393;
--color-purple-light: #e1dbff;

/* based on azure blue (#3784ff) */
Expand Down
25 changes: 25 additions & 0 deletions docs/src/lib/github-stars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const REPO_API_URL = 'https://api.github.com/repos/inkeep/open-knowledge';

export async function getGitHubStars(): Promise<number | null> {
try {
const res = await fetch(REPO_API_URL, {
signal: AbortSignal.timeout(5_000),
next: { revalidate: 3600 },
headers: {
accept: 'application/vnd.github+json',
'user-agent': 'openknowledge.ai site nav',
},
});
if (!res.ok) {
console.warn(`[github-stars] GitHub API responded ${res.status}`);
return null;
}
const json = (await res.json()) as { stargazers_count?: unknown };
return typeof json.stargazers_count === 'number' ? json.stargazers_count : null;
} catch (err) {
console.warn(
`[github-stars] fetch failed: ${err instanceof Error ? err.message : String(err)}`,
);
return null;
}
}