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.
286 lines
8.0 KiB
TypeScript
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>
|
|
);
|
|
}
|