Files
voculinux/web/src/components/site-chrome.tsx
T
xavierk c9be45d3c3 fix(web): link the fork's own source host and drop the upstream star count
The header badge fetched the star count of VocaHQ/vocalinux at build time
and labelled it as Voculinux's. The fetch and the count are gone; the
header, footer and social "source" links now go to the fork repository at
git.bongbetic.com/xavierk/voculinux, as do the issue links, the docs
anchors, the home page JSON-LD and the xbps release card.

Upstream GitHub stays only where it is the real artefact or provenance and
is labelled as such: the install script, the AppImage release card, the
upstream card and the changelog release links. The changelog now says its
entries are upstream history, the open-source page no longer claims
VocaHQ membership or an active GitHub community, and the organization
JSON-LD drops the upstream author as founder. DESIGN.md and PRODUCT.md
follow.
2026-11-06 18:22:17 +05:30

286 lines
8.0 KiB
TypeScript

"use client";
import React, { useEffect, useId, useState } from "react";
import Link from "next/link";
import { Code } from "lucide-react";
import { VoculinuxLogo } from "@/components/optimized-image";
import { SOURCE_REPO_URL } from "@/lib/seo";
import {
footerGroups,
homeNav,
mobileExtraNav,
pageNav,
socialLinks,
type NavLink,
} from "@/lib/site-nav";
function SmartLink({
href,
className,
children,
onClick,
}: {
href: string;
className?: string;
children: React.ReactNode;
onClick?: () => void;
}) {
const external = href.startsWith("http");
if (external) {
return (
<a
href={href}
className={className}
onClick={onClick}
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
);
}
if (href.startsWith("#")) {
return (
<a href={href} className={className} onClick={onClick}>
{children}
</a>
);
}
return (
<Link href={href} className={className} onClick={onClick}>
{children}
</Link>
);
}
export function SiteChrome({
children,
variant = "page",
}: {
children: React.ReactNode;
variant?: "home" | "page";
}) {
const [open, setOpen] = useState(false);
const navId = useId();
const nav = variant === "home" ? homeNav : pageNav;
const ctaHref = variant === "home" ? "#install" : "/#install";
const extras: NavLink[] =
variant === "home"
? [
{ href: "/screenshots/", label: "Screenshots" },
{ href: "/install/", label: "Distro guides" },
...mobileExtraNav,
]
: mobileExtraNav;
useEffect(() => {
const onKey = (event: KeyboardEvent) => {
if (event.key === "Escape") setOpen(false);
};
const onResize = () => {
if (window.innerWidth >= 921) setOpen(false);
};
window.addEventListener("keydown", onKey);
window.addEventListener("resize", onResize);
return () => {
window.removeEventListener("keydown", onKey);
window.removeEventListener("resize", onResize);
};
}, []);
return (
<div className="min-h-[100dvh] max-w-[100vw] overflow-x-clip">
<a className="skip-link" href="#main">
Skip to content
</a>
<header className="site-header">
<div className="shell header-inner">
<Link href="/" className="brand" aria-label="Voculinux home">
<VoculinuxLogo
width={28}
height={28}
className="h-7 w-7"
priority
decorative
/>
<span>Voculinux</span>
</Link>
<a
className="header-attribution"
href="https://bongbetic.com"
rel="noopener noreferrer"
aria-label="By Bongbetic"
title="By Bongbetic"
>
{/* Hosted brand asset: never vendored into the repository. */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="https://bongbetic.com/branding/bongbetic-logo.png"
alt=""
width={18}
height={18}
loading="lazy"
decoding="async"
/>
<span>by Bongbetic</span>
</a>
<nav className="nav-links" aria-label="Primary">
{nav.map((item) => (
<SmartLink key={item.href + item.label} href={item.href}>
{item.label}
</SmartLink>
))}
</nav>
<div className="header-actions">
<a
className="header-source"
href={SOURCE_REPO_URL}
rel="noopener noreferrer"
aria-label="Voculinux source repository"
>
<Code
className="h-4 w-4"
strokeWidth={1.75}
aria-hidden="true"
/>
<span>Source</span>
</a>
<button
className="nav-toggle"
type="button"
aria-expanded={open}
aria-controls={navId}
aria-label="Toggle menu"
onClick={() => setOpen((value) => !value)}
>
<span className="sr-only">
{open ? "Close navigation" : "Open navigation"}
</span>
<span className="nav-toggle-bars" aria-hidden="true">
<i></i>
<i></i>
<i></i>
</span>
</button>
</div>
</div>
<nav
id={navId}
className="mobile-nav shell"
aria-label="Site"
hidden={!open}
>
{[...nav, ...extras].map((item) => (
<SmartLink
key={`m-${item.href}-${item.label}`}
href={item.href}
onClick={() => setOpen(false)}
>
{item.label}
</SmartLink>
))}
<a
className="btn btn-primary"
href={ctaHref}
onClick={() => setOpen(false)}
>
Install Voculinux
</a>
</nav>
</header>
<main id="main">{children}</main>
<footer className="site-footer">
<div className="shell">
<div className="footer-inner">
<div className="footer-brand">
<Link href="/" className="brand">
<VoculinuxLogo
width={28}
height={28}
className="h-7 w-7"
decorative
/>
<span>Voculinux</span>
</Link>
<p>
Offline voice typing for Linux. Local engines by default, no
required account.
</p>
<ul className="footer-social">
{socialLinks.map((item) => (
<li key={item.href}>
<a
href={item.href}
{...(item.href.startsWith("http")
? { target: "_blank", rel: "noopener noreferrer" }
: {})}
aria-label={item.label}
title={item.label}
>
<span
className="footer-social-icon"
style={{
maskImage: `url(${item.icon})`,
WebkitMaskImage: `url(${item.icon})`,
}}
/>
</a>
</li>
))}
</ul>
</div>
{footerGroups.map((group) => (
<div className="footer-col" key={group.title}>
<h2>{group.title}</h2>
<ul>
{group.links.map((link) => (
<li key={link.href + link.label}>
<SmartLink href={link.href}>{link.label}</SmartLink>
</li>
))}
</ul>
</div>
))}
</div>
<div className="footer-base">
<span>
AGPL-3.0. Built in public by{" "}
<a
href="https://github.com/jatinkrmalik"
rel="noopener noreferrer"
>
Jatin K Malik
</a>
.
</span>
<span className="footer-attribution">
{/* Hosted brand assets: linked, never vendored. */}
<a
href="https://bongbetic.com"
rel="noopener noreferrer"
aria-label="Bongbetic"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="https://bongbetic.com/branding/bongbetic-logo-lockup.png"
alt="Bongbetic"
width={96}
height={24}
loading="lazy"
decoding="async"
/>
</a>
</span>
</div>
</div>
</footer>
</div>
);
}