* web: restyle vocalinux.com to the Voca family workbench Bring the Linux marketing site onto the same paper, ink, and teal language as VocaHQ, VocaMac, VocaPhone, and VocaGateway. Drop the site-wide dark mode so the canvas stays paper-only, matching the other product sites. * web: loop the home ribbon without a gap or jump The ticker used a stretched full-width track, so a -50% slide left an empty right edge before snapping back. Two equal groups, each wider than the viewport, scroll by exactly one group width. * web: match vocahq.com ribbon speed and teal band The looped ticker was covering six item cycles in 32s, so it raced. It now spends 28s per cycle like vocahq.com, uses the same dark-teal band, and pauses when the tab is hidden. * web: drive the hero panel clock from the visitor's local time Replace the static 11:24 mock with a 24-hour HH:MM clock in the client timezone, updated on the minute. * web: add official platform marks on the family cards Use the .github brand platform SVGs (Linux, Apple, Android, Windows) plus the HQ server mark for VocaGateway, matching vocahq.com's product row. * web: restore a Light/Dark flip on the screenshots page The captured GTK shots exist in both appearances. A page-local switch defaults to prefers-color-scheme and stores a tab override. The rest of the site stays paper-only. * web: make the homepage CTA say Install, not Get The button already jumps to the one-line installer. Name that action, note that there is no account, and keep the same copy in the closer and the mobile menu. * web: style install commands as GTK terminals Command boxes use a headerbar with copy, a centered title, and window controls on the right. The prompt is user@linux:~$, not Mac traffic-light chrome. * web: stop repeating the engine list in the hero The proof line, the notes window, and the tray terminal all said the same whisper.cpp / Whisper / VOSK / Remote API line. Engines stay on the window chips. Proof is now desktop and license facts. The terminal shows launching into the tray. * web: reflow copy, color prompts, and add install paths Drop pretty/balance wrapping so titles and ledes follow the shell width. Color the user@linux prompt. Hero terminal now says audio stays on this machine. Ribbon adds project words. AppImage and source-build sit under the install command. * web: put X, Discord, and GitHub by the footer mark Official family social marks sit next to the Vocalinux logo. Footer columns are Product, Install, Guides, and Project. The bottom line is Part of VocaHQ, without X11 or whisper.cpp. * web: add hello@vocahq.com to the footer social row The official mail mark sits with X, Discord, and GitHub and opens the family hello@vocahq.com address. * web: use the circular mark in the header and footer Header and footer now use the official teal circle lockup instead of the square app icon. * web: ship a circular favicon and a family OG card Favicons use the teal circle lockup. The Open Graph image is redone in the paper workbench language with the current homepage line. * web: set the hero to Dictate locally / In any app Shorter couplet in the VocaHQ display size, with a matching lede and Open Graph card. * web: gray the hero terminal note as a comment The second line was reading as part of the vocalinux command. It is now a dim # comment under the prompt.
261 lines
7.3 KiB
TypeScript
261 lines
7.3 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useId, useState } from "react";
|
|
import Link from "next/link";
|
|
import { Star } from "lucide-react";
|
|
import { VocalinuxLogo } from "@/components/optimized-image";
|
|
import { GITHUB_REPO_URL, VOCAHQ_URL } from "@/lib/seo";
|
|
import {
|
|
footerGroups,
|
|
homeNav,
|
|
mobileExtraNav,
|
|
pageNav,
|
|
socialLinks,
|
|
type NavLink,
|
|
} from "@/lib/site-nav";
|
|
|
|
const GITHUB_API_REPO_URL = `https://api.github.com/repos/${GITHUB_REPO_URL.replace("https://github.com/", "")}`;
|
|
|
|
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 [stars, setStars] = useState<number | null>(null);
|
|
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);
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
fetch(GITHUB_API_REPO_URL)
|
|
.then((res) => res.json())
|
|
.then((data: { stargazers_count?: number }) => {
|
|
if (data.stargazers_count) setStars(data.stargazers_count);
|
|
})
|
|
.catch(() => setStars(null));
|
|
}, []);
|
|
|
|
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="Vocalinux home">
|
|
<VocalinuxLogo width={28} height={28} className="h-7 w-7" priority />
|
|
<span>Vocalinux</span>
|
|
</Link>
|
|
|
|
<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-stars"
|
|
href={GITHUB_REPO_URL}
|
|
rel="noopener noreferrer"
|
|
aria-label={
|
|
stars !== null
|
|
? `Vocalinux on GitHub, ${stars.toLocaleString()} stars`
|
|
: "Vocalinux on GitHub"
|
|
}
|
|
>
|
|
<Star
|
|
className="h-4 w-4"
|
|
strokeWidth={1.75}
|
|
fill="currentColor"
|
|
aria-hidden="true"
|
|
/>
|
|
<span>{stars !== null ? stars.toLocaleString() : "GitHub"}</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 Vocalinux
|
|
</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">
|
|
<VocalinuxLogo width={28} height={28} className="h-7 w-7" />
|
|
<span>Vocalinux</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>
|
|
Part of{" "}
|
|
<a href={VOCAHQ_URL} rel="noopener noreferrer">
|
|
VocaHQ
|
|
</a>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|