web: restyle vocalinux.com to the Voca family workbench (#728)
* 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.
This commit is contained in:
@@ -62,24 +62,17 @@ describe('Lazy Loading Behavior', () => {
|
||||
});
|
||||
|
||||
describe('VocalinuxLogo Component', () => {
|
||||
it('lazy loads logo by default', () => {
|
||||
it('lazy loads the circular mark by default', () => {
|
||||
render(<VocalinuxLogo />);
|
||||
|
||||
expect(mockImage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
priority: false,
|
||||
})
|
||||
);
|
||||
const img = screen.getByAltText('Vocalinux');
|
||||
expect(img).toHaveAttribute('src', '/brand/vocalinux-mark-circle.svg');
|
||||
expect(img).toHaveAttribute('loading', 'lazy');
|
||||
});
|
||||
|
||||
it('eager loads logo when priority is set', () => {
|
||||
it('eager loads the mark when priority is set', () => {
|
||||
render(<VocalinuxLogo priority />);
|
||||
|
||||
expect(mockImage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
priority: true,
|
||||
})
|
||||
);
|
||||
const img = screen.getByAltText('Vocalinux');
|
||||
expect(img).toHaveAttribute('loading', 'eager');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,14 +86,7 @@ describe('Lazy Loading Behavior', () => {
|
||||
});
|
||||
|
||||
it('prevents layout shift with explicit dimensions', () => {
|
||||
const { container } = render(
|
||||
<VocalinuxLogo width={64} height={64} />
|
||||
);
|
||||
|
||||
const picture = container.querySelector('picture');
|
||||
expect(picture).toBeInTheDocument();
|
||||
|
||||
// Picture element helps with responsive images and prevents layout shift
|
||||
render(<VocalinuxLogo width={64} height={64} />);
|
||||
const img = screen.getByAltText('Vocalinux');
|
||||
expect(img).toHaveAttribute('width', '64');
|
||||
expect(img).toHaveAttribute('height', '64');
|
||||
@@ -119,20 +105,10 @@ describe('Lazy Loading Behavior', () => {
|
||||
expect(source).toHaveAttribute('type', 'image/webp');
|
||||
});
|
||||
|
||||
it('browser selects WebP source before loading image', () => {
|
||||
const { container } = render(
|
||||
<VocalinuxLogo width={32} height={32} />
|
||||
);
|
||||
|
||||
const picture = container.querySelector('picture');
|
||||
const source = picture?.querySelector('source');
|
||||
const img = picture?.querySelector('img');
|
||||
|
||||
// picture > source should come before img in DOM order
|
||||
if (source && img) {
|
||||
const children = Array.from(picture?.children || []);
|
||||
expect(children.indexOf(source)).toBeLessThan(children.indexOf(img));
|
||||
}
|
||||
it('ships the circular mark as an SVG', () => {
|
||||
render(<VocalinuxLogo width={32} height={32} />);
|
||||
const img = screen.getByAltText('Vocalinux');
|
||||
expect(img).toHaveAttribute('src', '/brand/vocalinux-mark-circle.svg');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,7 +52,6 @@ describe("Screenshots page and assets", () => {
|
||||
expect(gallerySource).toContain('role="dialog"');
|
||||
expect(gallerySource).toContain("aria-modal");
|
||||
expect(gallerySource).toContain("View larger");
|
||||
expect(gallerySource).toContain("ThemeScreenshotImg");
|
||||
});
|
||||
|
||||
it("references every public screenshot asset from the page source", () => {
|
||||
@@ -74,10 +73,8 @@ describe("Screenshots page and assets", () => {
|
||||
|
||||
it("links to the gallery and only embeds a few product shots on home", () => {
|
||||
// Craft home shows a handful of real app shots, then /screenshots/ for the rest.
|
||||
expect(homeSource).toContain('href: "/screenshots/"');
|
||||
expect(homeSource).toContain('href="/screenshots/"');
|
||||
expect(homeSource).toContain("/screenshots/00-transcription.png");
|
||||
expect(homeSource).toContain("/screenshots/dark/00-transcription.png");
|
||||
expect(homeSource).not.toContain("/screenshots/05-about-view.png");
|
||||
expect(homeSource).not.toContain("/screenshots/settings-recognition.png");
|
||||
expect(homeSource).not.toContain("/screenshots/settings-audio.png");
|
||||
@@ -85,9 +82,14 @@ describe("Screenshots page and assets", () => {
|
||||
expect(homeSource).not.toContain("/screenshots/settings-advanced.png");
|
||||
});
|
||||
|
||||
it("exposes Screenshots in the shared subpage shell nav", () => {
|
||||
expect(shellSource).toContain('href: "/screenshots/"');
|
||||
expect(shellSource).toContain('label: "Screenshots"');
|
||||
it("exposes Screenshots in the shared site nav", () => {
|
||||
const navSource = fs.readFileSync(
|
||||
path.join(webRoot, "src/lib/site-nav.ts"),
|
||||
"utf-8",
|
||||
);
|
||||
expect(navSource).toContain('href: "/screenshots/"');
|
||||
expect(navSource).toContain('label: "Screenshots"');
|
||||
expect(shellSource).toContain("SiteChrome");
|
||||
});
|
||||
|
||||
it("includes captioned settings and product sections", () => {
|
||||
|
||||
@@ -15,10 +15,19 @@ import path from "node:path";
|
||||
|
||||
const root = path.resolve(__dirname, "../../..");
|
||||
const pageTsx = fs.readFileSync(path.join(root, "src/app/page.tsx"), "utf8");
|
||||
const terminalTsx = fs.readFileSync(
|
||||
path.join(root, "src/components/terminal-block.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
const chromeTsx = fs.readFileSync(
|
||||
path.join(root, "src/components/site-chrome.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
const globalsCss = fs.readFileSync(
|
||||
path.join(root, "src/styles/globals.css"),
|
||||
"utf8",
|
||||
);
|
||||
const homeSource = `${pageTsx}\n${terminalTsx}\n${chromeTsx}`;
|
||||
|
||||
const CHROME =
|
||||
process.env.CHROME_PATH ||
|
||||
@@ -42,9 +51,9 @@ describe("marketing home layout guards (shipped source)", () => {
|
||||
expect(globalsCss).toMatch(/overflow-x:\s*clip/);
|
||||
expect(globalsCss).toMatch(/\.terminal-panel pre/);
|
||||
expect(globalsCss).toMatch(/overflow-wrap:\s*anywhere|break-all|word-break/);
|
||||
expect(pageTsx).toMatch(/break-all/);
|
||||
expect(pageTsx).toMatch(/min-w-0/);
|
||||
expect(pageTsx).toMatch(/overflow-x-clip|overflow-x-auto/);
|
||||
expect(homeSource).toMatch(/break-all/);
|
||||
expect(homeSource).toMatch(/min-w-0/);
|
||||
expect(homeSource).toMatch(/overflow-x-clip|overflow-x-auto/);
|
||||
});
|
||||
|
||||
it("hero uses solid display type (no gradient text slop)", () => {
|
||||
@@ -53,6 +62,13 @@ describe("marketing home layout guards (shipped source)", () => {
|
||||
expect(pageTsx).toMatch(/font-display/);
|
||||
expect(pageTsx).toMatch(/TerminalBlock|terminal-panel/);
|
||||
});
|
||||
|
||||
it("uses the Voca family paper workbench tokens", () => {
|
||||
expect(globalsCss).toMatch(/--paper:\s*#f4f1e8/);
|
||||
expect(globalsCss).toMatch(/--brand:\s*#0f6b57/);
|
||||
expect(globalsCss).not.toMatch(/linear-gradient|radial-gradient|conic-gradient/);
|
||||
expect(pageTsx).not.toMatch(/linear-gradient|radial-gradient|conic-gradient/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("marketing home three-viewport overflow (live browser)", () => {
|
||||
@@ -127,11 +143,11 @@ describe("marketing home three-viewport overflow (live browser)", () => {
|
||||
|
||||
if (vp.width < 768) {
|
||||
await page.getByRole("button", { name: /toggle menu/i }).click();
|
||||
const featuresVisible = await page
|
||||
const howVisible = await page
|
||||
.locator("header")
|
||||
.getByRole("link", { name: "Features" })
|
||||
.getByRole("link", { name: "How it works" })
|
||||
.isVisible();
|
||||
expect(featuresVisible).toBe(true);
|
||||
expect(howVisible).toBe(true);
|
||||
}
|
||||
|
||||
await context.close();
|
||||
|
||||
@@ -157,7 +157,7 @@ export default function AlternativesPage() {
|
||||
<CheckCircle2 className="h-5 w-5 text-primary" aria-label="Yes" />
|
||||
) : (
|
||||
<XCircle
|
||||
className="h-5 w-5 text-[#c45c5c] dark:text-[#e08b8b]"
|
||||
className="h-5 w-5 text-[#c45c5c]"
|
||||
aria-label="No"
|
||||
/>
|
||||
)}
|
||||
@@ -167,7 +167,7 @@ export default function AlternativesPage() {
|
||||
<CheckCircle2 className="h-5 w-5 text-primary" aria-label="Yes" />
|
||||
) : (
|
||||
<XCircle
|
||||
className="h-5 w-5 text-[#c45c5c] dark:text-[#e08b8b]"
|
||||
className="h-5 w-5 text-[#c45c5c]"
|
||||
aria-label="No"
|
||||
/>
|
||||
)}
|
||||
@@ -177,7 +177,7 @@ export default function AlternativesPage() {
|
||||
<CheckCircle2 className="h-5 w-5 text-primary" aria-label="Yes" />
|
||||
) : (
|
||||
<XCircle
|
||||
className="h-5 w-5 text-[#c45c5c] dark:text-[#e08b8b]"
|
||||
className="h-5 w-5 text-[#c45c5c]"
|
||||
aria-label="No"
|
||||
/>
|
||||
)}
|
||||
@@ -187,7 +187,7 @@ export default function AlternativesPage() {
|
||||
<CheckCircle2 className="h-5 w-5 text-primary" aria-label="Yes" />
|
||||
) : (
|
||||
<XCircle
|
||||
className="h-5 w-5 text-[#c45c5c] dark:text-[#e08b8b]"
|
||||
className="h-5 w-5 text-[#c45c5c]"
|
||||
aria-label="No"
|
||||
/>
|
||||
)}
|
||||
@@ -232,7 +232,7 @@ export default function AlternativesPage() {
|
||||
</div>
|
||||
|
||||
<div className="rounded-[12px] border border-border bg-muted/40 p-4">
|
||||
<p className="mb-2 text-sm font-semibold text-[#c45c5c] dark:text-[#e08b8b]">
|
||||
<p className="mb-2 text-sm font-semibold text-[#c45c5c]">
|
||||
Cons
|
||||
</p>
|
||||
<ul className="space-y-1.5">
|
||||
@@ -241,7 +241,7 @@ export default function AlternativesPage() {
|
||||
key={con}
|
||||
className="flex items-start gap-2 text-sm text-muted-foreground"
|
||||
>
|
||||
<XCircle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-[#c45c5c] dark:text-[#e08b8b]" />
|
||||
<XCircle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-[#c45c5c]" />
|
||||
{con}
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -219,7 +219,7 @@ export default function AutostartPage() {
|
||||
<td className="py-3 pr-4 text-muted-foreground">{de.autostartMethod}</td>
|
||||
<td className="py-3 pr-4">
|
||||
{de.supported ? (
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary dark:text-[color:var(--terminal-fg)]">
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
Yes
|
||||
</span>
|
||||
|
||||
@@ -312,24 +312,24 @@ const getTypeStyles = (type: string) => {
|
||||
case "stable":
|
||||
return {
|
||||
badge:
|
||||
"bg-primary/10 text-primary dark:text-[color:var(--terminal-fg)]",
|
||||
"bg-primary/10 text-primary",
|
||||
label: "Stable",
|
||||
};
|
||||
case "beta":
|
||||
return {
|
||||
badge:
|
||||
"bg-primary/10 text-primary dark:text-primary",
|
||||
"bg-primary/10 text-primary",
|
||||
label: "Beta",
|
||||
};
|
||||
case "alpha":
|
||||
return {
|
||||
badge:
|
||||
"bg-muted text-muted-foreground dark:text-muted-foreground",
|
||||
"bg-muted text-muted-foreground",
|
||||
label: "Alpha",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
badge: "bg-muted text-zinc-700",
|
||||
badge: "bg-muted text-muted-foreground",
|
||||
label: type,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -304,7 +304,7 @@ export default async function DistroInstallPage({
|
||||
className={`rounded-[12px] border border-border p-6 ${styles.panelClass}`}
|
||||
>
|
||||
<h2 className="mb-3 inline-flex items-center gap-2 text-xl font-semibold">
|
||||
<Terminal className="h-5 w-5 text-primary dark:text-[color:var(--terminal-fg)]" />
|
||||
<Terminal className="h-5 w-5 text-primary" />
|
||||
Install command
|
||||
</h2>
|
||||
<div className="rounded-[12px] bg-[color:var(--terminal)] p-4">
|
||||
|
||||
@@ -2,10 +2,15 @@ import Link from "next/link";
|
||||
import { type Metadata } from "next";
|
||||
import { ChevronRight, Cpu, Laptop, Terminal } from "lucide-react";
|
||||
import { SeoSubpageShell } from "@/components/seo-subpage-shell";
|
||||
import { TerminalBlock } from "@/components/terminal-block";
|
||||
import { buildPageMetadata } from "@/lib/seo";
|
||||
|
||||
const installCommand =
|
||||
"curl -fsSL https://raw.githubusercontent.com/VocaHQ/vocalinux/main/install.sh -o /tmp/vl.sh && bash /tmp/vl.sh --interactive";
|
||||
const installDisplayCommand = `curl -fsSL \\
|
||||
https://raw.githubusercontent.com/VocaHQ/vocalinux/main/install.sh \\
|
||||
-o /tmp/vl.sh && \\
|
||||
bash /tmp/vl.sh --interactive`;
|
||||
|
||||
const distroGuides = [
|
||||
{
|
||||
@@ -56,26 +61,42 @@ export default function InstallGuidesPage() {
|
||||
speech-to-text on Linux. Every guide is written for real desktop usage.
|
||||
</p>
|
||||
|
||||
<div className="subpage-terminal mt-8 p-5 sm:p-6">
|
||||
<p className="mb-3 inline-flex items-center gap-2 font-mono text-[11px] uppercase tracking-wider text-zinc-500">
|
||||
<Terminal className="h-3.5 w-3.5 text-[color:var(--terminal-fg)]" />
|
||||
install.sh --interactive
|
||||
</p>
|
||||
<code className="block overflow-x-auto break-all text-sm sm:text-base">
|
||||
{installCommand}
|
||||
</code>
|
||||
<div className="mt-8">
|
||||
<TerminalBlock
|
||||
command={installCommand}
|
||||
displayCommand={installDisplayCommand}
|
||||
label="install.sh --interactive"
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
Prefer no installer? Download an{" "}
|
||||
<a
|
||||
href="https://github.com/VocaHQ/vocalinux/releases"
|
||||
className="font-semibold text-primary hover:underline"
|
||||
>
|
||||
AppImage
|
||||
</a>{" "}
|
||||
for x86_64 or aarch64 from GitHub Releases (host text-injection tools
|
||||
still required).
|
||||
Then launch <code>vocalinux</code>.
|
||||
</p>
|
||||
<div className="install-alts">
|
||||
<a
|
||||
className="install-alt"
|
||||
href="https://github.com/VocaHQ/vocalinux/releases"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<p className="install-alt-kicker">No installer</p>
|
||||
<h3>AppImage</h3>
|
||||
<p>
|
||||
x86_64 or aarch64 from GitHub Releases. No root. Host
|
||||
text-injection tools are still required.
|
||||
</p>
|
||||
</a>
|
||||
<a
|
||||
className="install-alt"
|
||||
href="https://github.com/VocaHQ/vocalinux/blob/main/docs/INSTALL.md#from-source"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<p className="install-alt-kicker">From source</p>
|
||||
<h3>Build it yourself</h3>
|
||||
<p>
|
||||
Clone the repo and run <code>./install.sh</code>. The install
|
||||
guide has the full source path.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-12 grid gap-4 md:grid-cols-3">
|
||||
|
||||
+7
-30
@@ -1,9 +1,7 @@
|
||||
import "@/styles/globals.css";
|
||||
import React from "react";
|
||||
import { Bricolage_Grotesque, Source_Sans_3 } from "next/font/google";
|
||||
import { type Metadata } from "next";
|
||||
import Script from "next/script";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import {
|
||||
absoluteUrl,
|
||||
DEFAULT_OG_IMAGE_ALT,
|
||||
@@ -15,26 +13,11 @@ import {
|
||||
VOCAHQ_URL,
|
||||
} from "@/lib/seo";
|
||||
|
||||
const display = Bricolage_Grotesque({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-display",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const body = Source_Sans_3({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-body",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const viewport = {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
maximumScale: 5,
|
||||
themeColor: [
|
||||
{ media: "(prefers-color-scheme: light)", color: "#ffffff" },
|
||||
{ media: "(prefers-color-scheme: dark)", color: "#0c0c0e" },
|
||||
],
|
||||
themeColor: "#f4f1e8",
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -69,15 +52,16 @@ export const metadata: Metadata = {
|
||||
publisher: SITE_NAME,
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: "/vocalinux.png", type: "image/png" },
|
||||
{ url: "/brand/vocalinux-mark-circle.svg", type: "image/svg+xml" },
|
||||
{ url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" },
|
||||
{ url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" },
|
||||
{ url: "/favicon-96x96.png", sizes: "96x96", type: "image/png" },
|
||||
{ url: "/vocalinux.png", type: "image/png", sizes: "512x512" },
|
||||
],
|
||||
apple: [
|
||||
{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" },
|
||||
],
|
||||
shortcut: "/vocalinux.png",
|
||||
shortcut: "/favicon-32x32.png",
|
||||
},
|
||||
manifest: "/site.webmanifest",
|
||||
appleWebApp: {
|
||||
@@ -174,8 +158,7 @@ const webSiteJsonLd = {
|
||||
"@type": "SearchAction",
|
||||
target: {
|
||||
"@type": "EntryPoint",
|
||||
urlTemplate:
|
||||
`${GITHUB_REPO_URL}/issues?q={search_term_string}`,
|
||||
urlTemplate: `${GITHUB_REPO_URL}/issues?q={search_term_string}`,
|
||||
},
|
||||
"query-input": {
|
||||
"@type": "PropertyValueSpecification",
|
||||
@@ -193,11 +176,7 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
className={`${display.variable} ${body.variable} scroll-smooth`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<html lang="en" className="scroll-smooth" suppressHydrationWarning>
|
||||
<head>
|
||||
<link rel="preconnect" href="https://api.github.com" />
|
||||
<script
|
||||
@@ -218,9 +197,7 @@ export default function RootLayout({
|
||||
gtag('config', 'G-7NBBNJNNQ7');
|
||||
`}
|
||||
</Script>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -264,13 +264,13 @@ export default function OfflinePage() {
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
Run with <code className="rounded bg-muted px-1 dark:bg-muted">--debug</code> and
|
||||
Run with <code className="rounded bg-muted px-1">--debug</code> and
|
||||
watch network activity. Local engines should stay quiet during dictation.
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
Monitor with <code className="rounded bg-muted px-1 dark:bg-muted">tcpdump</code> or{" "}
|
||||
<code className="rounded bg-muted px-1 dark:bg-muted">wireshark</code>. Local engines
|
||||
Monitor with <code className="rounded bg-muted px-1">tcpdump</code> or{" "}
|
||||
<code className="rounded bg-muted px-1">wireshark</code>. Local engines
|
||||
should stay quiet during dictation.
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
|
||||
@@ -219,7 +219,7 @@ export default function OpenSourcePage() {
|
||||
{vsProprietary.map((row) => (
|
||||
<tr key={row.feature} className="border-b border-border">
|
||||
<td className="py-3 pr-4 font-medium">{row.feature}</td>
|
||||
<td className="py-3 pr-4 text-primary dark:text-[color:var(--terminal-fg)]">{row.vocalinux}</td>
|
||||
<td className="py-3 pr-4 text-primary">{row.vocalinux}</td>
|
||||
<td className="py-3 text-muted-foreground">{row.proprietary}</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
+714
-962
File diff suppressed because it is too large
Load Diff
@@ -49,7 +49,7 @@ export default function PrivacyPage() {
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-border bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary dark:text-[color:var(--terminal-fg)]">
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-border bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<Shield className="h-4 w-4" />
|
||||
Privacy First
|
||||
</p>
|
||||
@@ -65,8 +65,8 @@ export default function PrivacyPage() {
|
||||
{/* Key Privacy Points */}
|
||||
<section className="mb-12 grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<div className="mb-4 inline-flex rounded-[12px] bg-primary/10 p-3 dark:bg-primary/10">
|
||||
<Lock className="h-6 w-6 text-primary dark:text-[color:var(--terminal-fg)]" />
|
||||
<div className="mb-4 inline-flex rounded-[12px] bg-primary/10 p-3">
|
||||
<Lock className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-semibold">Local engines</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@@ -76,7 +76,7 @@ export default function PrivacyPage() {
|
||||
</article>
|
||||
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<div className="mb-4 inline-flex rounded-[12px] bg-primary/10 p-3 dark:bg-primary/10">
|
||||
<div className="mb-4 inline-flex rounded-[12px] bg-primary/10 p-3">
|
||||
<Server className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-semibold">No Cloud Processing</h3>
|
||||
@@ -87,7 +87,7 @@ export default function PrivacyPage() {
|
||||
</article>
|
||||
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<div className="mb-4 inline-flex rounded-[12px] bg-primary/10 p-3 dark:bg-primary/10">
|
||||
<div className="mb-4 inline-flex rounded-[12px] bg-primary/10 p-3">
|
||||
<Eye className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-semibold">Open Source</h3>
|
||||
@@ -160,7 +160,7 @@ export default function PrivacyPage() {
|
||||
discarded immediately after processing
|
||||
</li>
|
||||
</ol>
|
||||
<p className="mt-4 rounded-lg bg-muted p-4 text-sm dark:bg-muted">
|
||||
<p className="mt-4 rounded-lg bg-muted p-4 text-sm">
|
||||
<strong>Note:</strong> Local engines do not write audio to disk or send it over the
|
||||
network. Remote API sends audio to the server you configure.
|
||||
</p>
|
||||
@@ -213,31 +213,31 @@ export default function PrivacyPage() {
|
||||
<tbody className="text-sm text-muted-foreground">
|
||||
<tr className="border-b border-border">
|
||||
<td className="py-3 font-medium text-foreground">Data leaves device</td>
|
||||
<td className="py-3 text-primary dark:text-[color:var(--terminal-fg)]">
|
||||
<td className="py-3 text-primary">
|
||||
Only with Remote API
|
||||
</td>
|
||||
<td className="py-3 text-primary">Always</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border">
|
||||
<td className="py-3 font-medium text-foreground">Internet required</td>
|
||||
<td className="py-3 text-primary dark:text-[color:var(--terminal-fg)]">
|
||||
<td className="py-3 text-primary">
|
||||
Only for Remote API or a model download
|
||||
</td>
|
||||
<td className="py-3 text-primary">Yes</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border">
|
||||
<td className="py-3 font-medium text-foreground">Source code available</td>
|
||||
<td className="py-3 text-primary dark:text-[color:var(--terminal-fg)]">Yes (AGPL-3.0)</td>
|
||||
<td className="py-3 text-primary">Yes (AGPL-3.0)</td>
|
||||
<td className="py-3 text-primary">No</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border">
|
||||
<td className="py-3 font-medium text-foreground">Voice recordings stored</td>
|
||||
<td className="py-3 text-primary dark:text-[color:var(--terminal-fg)]">No</td>
|
||||
<td className="py-3 text-primary">No</td>
|
||||
<td className="py-3">Varies by service</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-3 font-medium text-foreground">Cost</td>
|
||||
<td className="py-3 text-primary dark:text-[color:var(--terminal-fg)]">Free forever</td>
|
||||
<td className="py-3 text-primary">Free forever</td>
|
||||
<td className="py-3">Subscription</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -196,8 +196,8 @@ export default function RsiPreventionPage() {
|
||||
>
|
||||
<h3 className="mb-2 font-semibold">{condition.name}</h3>
|
||||
<p className="mb-3 text-sm text-muted-foreground">{condition.description}</p>
|
||||
<div className="rounded-lg bg-primary/10 p-3 dark:bg-primary/10">
|
||||
<p className="text-sm text-primary dark:text-[color:var(--terminal-fg)]">
|
||||
<div className="rounded-lg bg-primary/10 p-3">
|
||||
<p className="text-sm text-primary">
|
||||
<strong>How dictation helps:</strong> {condition.howDictationHelps}
|
||||
</p>
|
||||
</div>
|
||||
@@ -254,7 +254,7 @@ export default function RsiPreventionPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[12px] border border-border bg-primary/10 p-8 dark:border-border">
|
||||
<section className="rounded-[12px] border border-border bg-primary/10 p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold text-primary">
|
||||
Take Care of Your Hands
|
||||
</h2>
|
||||
@@ -273,7 +273,7 @@ export default function RsiPreventionPage() {
|
||||
</Link>
|
||||
<Link
|
||||
href="/use-cases/"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold text-primary hover:bg-primary/10 dark:bg-primary/10 dark:hover:bg-primary/10"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold text-primary hover:bg-primary/10"
|
||||
>
|
||||
See Other Use Cases
|
||||
</Link>
|
||||
|
||||
@@ -105,7 +105,7 @@ const settingsShots: Screenshot[] = [
|
||||
export const metadata: Metadata = buildPageMetadata({
|
||||
title: "Vocalinux Screenshots | Linux Voice Dictation UI",
|
||||
description:
|
||||
"See Vocalinux in action: system tray, log viewer, and settings pages for speech model, dictation, audio, performance, application, advanced options, and about/updates. Light and dark themes included.",
|
||||
"See Vocalinux in action: system tray, log viewer, and settings pages for speech model, dictation, audio, performance, application, advanced options, and about/updates. Flip between light and dark app shots.",
|
||||
path: "/screenshots",
|
||||
keywords: [
|
||||
"Vocalinux screenshots",
|
||||
@@ -157,9 +157,8 @@ export default function ScreenshotsPage() {
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
A look at offline voice dictation on Linux: tray controls, debugging
|
||||
tools, and the searchable sidebar settings UI from v0.15. Toggle the
|
||||
site theme to switch between light and dark app shots. Click any image
|
||||
to expand it and browse the gallery.
|
||||
tools, and the searchable sidebar settings UI from v0.15. Flip light
|
||||
and dark app shots. Click any image to expand it.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -246,7 +246,7 @@ export default function TroubleshootingPage() {
|
||||
>
|
||||
<h2 className="mb-4 text-xl font-semibold">{item.title}</h2>
|
||||
|
||||
<div className="mb-4 rounded-lg bg-muted p-4 dark:bg-muted">
|
||||
<div className="mb-4 rounded-lg bg-muted p-4">
|
||||
<p className="mb-2 flex items-center gap-2 text-sm font-semibold text-muted-foreground">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
Symptoms
|
||||
@@ -260,8 +260,8 @@ export default function TroubleshootingPage() {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-primary/10 p-4 dark:bg-primary/10">
|
||||
<p className="mb-2 flex items-center gap-2 text-sm font-semibold text-primary dark:text-[color:var(--terminal-fg)]">
|
||||
<div className="rounded-lg bg-primary/10 p-4">
|
||||
<p className="mb-2 flex items-center gap-2 text-sm font-semibold text-primary">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
Solutions
|
||||
</p>
|
||||
|
||||
@@ -228,7 +228,7 @@ export default function VoiceTypingVsCodePage() {
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-muted p-6 dark:border-border">
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-muted p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold text-muted-foreground">
|
||||
<Bug className="h-5 w-5" />
|
||||
Troubleshooting
|
||||
|
||||
@@ -154,7 +154,7 @@ export default function VsNerdDictationPage() {
|
||||
</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">{row.vocalinux}</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">{row.nerdDictation}</td>
|
||||
<td className="px-4 py-4 text-sm font-semibold text-primary dark:text-[color:var(--terminal-fg)]">
|
||||
<td className="px-4 py-4 text-sm font-semibold text-primary">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
{row.winner}
|
||||
|
||||
@@ -210,7 +210,7 @@ export default function WaylandPage() {
|
||||
{item.method}
|
||||
</td>
|
||||
<td className="py-3 pr-4">
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary dark:text-[color:var(--terminal-fg)]">
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
{item.status}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user