diff --git a/AGENTS.md b/AGENTS.md index 690961a..bc58434 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,6 +70,12 @@ vocalinux --debug ### Website (Next.js) +Website-specific agent notes, product truth, and design system live under `web/`: + +- `web/AGENTS.md` — commands and layout map +- `web/PRODUCT.md` — product claims / audience for the site +- `web/DESIGN.md` — visual system for marketing UI + ```bash cd web npm install diff --git a/web/AGENTS.md b/web/AGENTS.md new file mode 100644 index 0000000..576a18c --- /dev/null +++ b/web/AGENTS.md @@ -0,0 +1,49 @@ +# AGENTS.md — vocalinux.com (`web/`) + +Guidelines for agents working on the marketing site only. For the Python desktop app, see the repo root `AGENTS.md`. + +## What this package is + +Next.js (App Router) static marketing + SEO guides for Vocalinux. Source lives here; production deploys from `web/` (static export to `out/`). + +## Product and design authority + +Read these before changing marketing UI or copy: + +| File | Holds | +|---|---| +| [`PRODUCT.md`](./PRODUCT.md) | Who the site is for, product truth, claims we can make | +| [`DESIGN.md`](./DESIGN.md) | Visual system: color, type, layout, components, don'ts | + +Do not invent features, benchmarks, user counts, or privacy claims that contradict `PRODUCT.md`. Prefer screenshots and install commands over abstract feature theater. + +## Commands + +```bash +cd web +npm install +npm run dev # local (prefer -H 0.0.0.0 -p 3456 when sharing) +npm run build # static export +npm test +npm run typecheck +``` + +Viewport layout checks (needs a running server for the browser leg): + +```bash +# with http://127.0.0.1:3456 up +BASE_URL=http://127.0.0.1:3456 npm test -- --testPathPatterns=viewport-layout +# or +npm run test:viewport +``` + +## Layout map + +- Home: `src/app/page.tsx` +- Shared SEO shell: `src/components/seo-subpage-shell.tsx` +- Tokens / utilities: `src/styles/globals.css`, `tailwind.config.ts` +- Fonts: Bricolage Grotesque + Source Sans 3 in `src/app/layout.tsx` + +## Maintaining this file + +Update when website build, structure, or design authority paths change. Keep product claims and visual tokens in `PRODUCT.md` / `DESIGN.md`, not duplicated here. diff --git a/web/DESIGN.md b/web/DESIGN.md new file mode 100644 index 0000000..c102ef6 --- /dev/null +++ b/web/DESIGN.md @@ -0,0 +1,166 @@ +--- +name: Vocalinux website +description: Offline Linux voice dictation marketing site — workstation craft, not SaaS chrome +colors: + ink: "#09090b" + paper: "#ffffff" + paper-raised: "#f4f4f5" + surface-muted: "#f4f4f5" + primary: "#1a7f4e" + primary-deep: "#14663e" + primary-soft: "#ecfdf3" + terminal: "#0a0a0c" + terminal-fg: "#6ee7a8" + muted: "#52525b" + border: "#e4e4e7" + status-no: "#c45c5c" + status-no-dark: "#e08b8b" + dark-bg: "#0c0c0e" + dark-raised: "#16161a" + dark-muted: "#a1a1aa" +typography: + display: + fontFamily: "var(--font-display), ui-sans-serif, system-ui, sans-serif" + fontSize: "clamp(2.25rem, 5vw, 3.75rem)" + fontWeight: 600 + lineHeight: 1.08 + letterSpacing: "-0.03em" + headline: + fontFamily: "var(--font-display), ui-sans-serif, system-ui, sans-serif" + fontSize: "clamp(1.75rem, 3vw, 2.5rem)" + fontWeight: 600 + lineHeight: 1.15 + letterSpacing: "-0.025em" + body: + fontFamily: "var(--font-body), ui-sans-serif, system-ui, sans-serif" + fontSize: "1.0625rem" + fontWeight: 400 + lineHeight: 1.65 + label: + fontFamily: "var(--font-body), ui-sans-serif, system-ui, sans-serif" + fontSize: "0.8125rem" + fontWeight: 600 + lineHeight: 1.4 + letterSpacing: "0.02em" + mono: + fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" + fontSize: "0.875rem" +rounded: + sm: "6px" + md: "10px" + lg: "14px" +spacing: + section-y: "5rem" + gutter: "1.5rem" + content-max: "72rem" +--- + +# Design system: vocalinux.com + +## Overview + +**Creative north star: "The Linux desk utility"** + +Pure white / near-black iron surfaces, committed emerald, real app screenshots as proof, full-bleed install strip, asymmetric home hero. Not generic AI SaaS (purple mesh, gradient text, icon-card grids, browser-fake demos). + +Home is a persuade surface: first viewport shows product + screenshot; conversion lives in the install strip; then desktop proof. Child pages share the same shell and tokens via `SeoSubpageShell`. + +**Key characteristics** +- Pure white / zinc iron surfaces (not cream, not purple) +- Display (Bricolage Grotesque) + body (Source Sans 3); mono for install only +- Real `public/screenshots/` on the home feature section +- Full-bleed dark install strip as primary conversion +- Feature rail + dense engine list instead of rainbow icon cards + +## Colors + +Committed emerald on pure iron neutrals. + +### Primary +- **Signal green** (`#1a7f4e` light / `#50c878` dark): buttons, focus, success marks, key links +- **Deep canopy** (`#14663e`): hover/pressed primary + +### Neutral +- **Ink** (`#09090b` / `#fafafa` dark): primary text +- **Iron white** (`#ffffff`): page background (light) +- **Surface** (`#f4f4f5` / `#16161a`): elevated panels +- **Muted** (`#52525b` / `#a1a1aa`): secondary text +- **Seam** (`#e4e4e7` / `#27272a`): borders +- **Terminal night** (`#0a0a0c`) / **Terminal leaf** (`#6ee7a8`): install command surfaces + +### Status (comparison tables) +- **Yes:** primary emerald +- **No:** soft rose `#c45c5c` light / `#e08b8b` dark (distinct without form-error noise) + +### Named rules +**The one signal rule.** Emerald is the only chromatic accent on marketing pages (except status-no rose on comparison marks). No purple, cyan glow pairs, multi-colored metric chips, or rainbow icon tiles. + +**The iron surface rule.** Backgrounds stay pure white or near-black iron. Never warm cream/beige as the default, and never purple-to-blue gradients. + +## Typography + +**Display:** Bricolage Grotesque +**Body:** Source Sans 3 +**Mono:** system UI monospace for install commands only + +No Geist/Inter as the whole-page face. + +### Hierarchy +- **Display** (600, clamp 2.25–3.75rem): home hero +- **Headline** (600, clamp 1.75–2.5rem): section titles +- **Title** (600, ~1.125rem): card/row titles +- **Body** (400, ~17px, 1.65 lh, max ~70ch) +- **Label** (600, ~0.8125rem): nav, meta; mono uppercase only for short kickers + +### Named rules +**The no costume mono rule.** Monospace is for shell commands, paths, and config keys, not marketing labels. + +## Layout + +- Content max width ~72rem +- Section vertical rhythm ~4–6rem; more space above a heading than below it +- Home first viewport: asymmetric offer + screenshot when wide; stacked on small screens +- Install strip full-bleed dark band +- Features: settings shot paired with a stretched feature rail (matched heights) +- Child pages: breadcrumbs + content in `max-w-6xl`, shared shell chrome + +## Elevation and shapes + +Flat by default. Soft shadows only on terminal / primary elevated panels, not hairline + wide glow together. + +- Panels and cards: 6–14px radius (use 12px as the common surface radius) +- Pills only for small controls (Install nav chip, default engine badge) + +## Components + +### Buttons +- Primary: solid signal green, white text, full-pill on marketing CTAs +- Secondary/ghost: border + background, no scale-bounce hover +- Focus: 2px ring in primary + +### Feature rail +- Single bordered panel, divided rows, link affordance on the row +- No oversized icon tiles stacked above headings + +### Terminal panel +- Near-black surface, leaf green command text, copy in chrome +- Long URLs wrap (`break-all` / `overflow-wrap`) so mobile never overflows + +### Navigation +- Fixed translucent bar, seam border +- Home: simple links + Install pill +- Child: category dropdowns + Install pill + theme toggle + +## Do's and don'ts + +### Do +- Lead with install and real product proof (screenshots) +- Keep product claims accurate (offline local engines; remote only when configured) +- Keep marketing copy short and specific + +### Don't +- Gradient text, animated mesh/orb backgrounds, or cyan+green dual glows +- Repeated icon-in-rounded-square feature cards as the page structure +- Tracked uppercase eyebrows on every section +- Scatter identical fade-up motion on every block +- Invent metrics, testimonials, or "blazing" claims diff --git a/web/PRODUCT.md b/web/PRODUCT.md new file mode 100644 index 0000000..ed6018b --- /dev/null +++ b/web/PRODUCT.md @@ -0,0 +1,62 @@ +# Product (vocalinux.com) + +## Platform + +web (Next.js marketing site under `web/`) + +## Users + +Linux desktop users who want voice dictation that stays on their machine: developers, writers, and people reducing keyboard strain. They already live in terminals, browsers, IDEs, and GTK/Qt apps. + +## Product purpose + +Vocalinux is free, open-source offline voice dictation for Linux. It turns speech into typed text in whatever app has focus, using local engines (whisper.cpp default, Whisper, VOSK) or an optional user-configured remote API. Success is: install once, dictate in any app, without cloud transcription or telemetry. + +## Positioning + +Offline-first Linux voice typing with real desktop integration (system tray, X11 and Wayland text injection, toggle and push-to-talk, Silero VAD, suspend recovery). A cloud SaaS voice product cannot truthfully claim local-only models plus no usage telemetry from the installed app. + +## Operating context + +- Install via a one-line shell installer, then run from PATH or app menu +- System tray indicator and settings GUI (GTK) +- Dictation into terminals, browsers, IDEs, office apps +- Engines and models chosen for hardware (CPU, optional Vulkan GPU) +- Guides and comparison pages on vocalinux.com; source on GitHub + +## Capabilities and constraints + +- Engines: whisper.cpp (default), OpenAI Whisper, VOSK, optional Remote API +- Display servers: X11 and Wayland +- Shortcut modes: toggle and push-to-talk; left/right modifier distinction +- Optional voice commands; Silero neural VAD with amplitude fallback +- No usage telemetry in the installed app +- GPL-3.0; marketing version string is tracked in site package/version surfaces +- Website is Next.js marketing + SEO guides (static export) + +## Brand commitments + +- Name: Vocalinux (also part of the "Voca" family with VocaMac / VocaWin) +- Mark: microphone logo assets under `public/` +- Emerald accent in the project green family (not purple SaaS defaults) +- Voice: practical, specific, Linux-native; not hype-first SaaS copy + +## Evidence on hand + +- Real app screenshots in `public/screenshots/` +- Install/uninstall commands pointing at GitHub raw install scripts +- JSON-LD on the home page (software application, FAQ, how-to) +- Do not invent user counts, testimonials, or benchmarks + +## Product principles + +1. Privacy is default: local engines process audio on-device; remote only when the user opts in. +2. Ship the install path and real desktop proof, not abstract feature theater. +3. Stay honest about engines, hardware, and what runs offline vs remote. +4. Prefer Linux craft and clarity over generic AI-startup marketing patterns. + +## Accessibility + +- Respect `prefers-reduced-motion` +- Body text contrast at least 4.5:1; large text at least 3:1 +- Keyboard-reachable install copy controls and navigation diff --git a/web/next-env.d.ts b/web/next-env.d.ts index 830fb59..d39ca30 100644 --- a/web/next-env.d.ts +++ b/web/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -/// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/web/package-lock.json b/web/package-lock.json index 4017640..0e97f98 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -14,13 +14,11 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.9.4", - "geist": "^1.4.1", "lucide-react": "^0.507.0", "next": "^15.5.18", "next-themes": "^0.4.6", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-intersection-observer": "^9.16.0", "react-syntax-highlighter": "^15.6.1", "tailwind-merge": "^3.2.0", "tailwindcss": "^3.4.3", @@ -39,6 +37,7 @@ "jest": "^30.2.0", "jest-environment-jsdom": "^30.2.0", "jest-extended": "^7.0.0", + "playwright-core": "^1.61.1", "postcss": "^8.5.12", "prettier": "^3.3.2", "prettier-plugin-tailwindcss": "^0.6.5", @@ -5651,14 +5650,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/geist": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/geist/-/geist-1.7.2.tgz", - "integrity": "sha512-Gu5lDFa3pLRyoBlBPf0QIFHVdWAnpco7fS1bJm41jyLPFoguBgiubseUN2oLXMgqZ7uxAxDoXcHMhCY/fOTTgg==", - "peerDependencies": { - "next": ">=13.2.0" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -8880,6 +8871,18 @@ "node": ">=8" } }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -9245,21 +9248,6 @@ "react": "^18.3.1" } }, - "node_modules/react-intersection-observer": { - "version": "9.16.0", - "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz", - "integrity": "sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==", - "license": "MIT", - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/web/package.json b/web/package.json index 14c8018..f7912d2 100644 --- a/web/package.json +++ b/web/package.json @@ -18,6 +18,7 @@ "test": "jest", "test:types": "tsc --noEmit --pretty", "test:all": "npm run test && npm run test:types", + "test:viewport": "node scripts/viewport-qa.mjs http://127.0.0.1:3456", "deploy": "next build && touch out/.nojekyll && echo 'vocalinux.com' > out/CNAME" }, "dependencies": { @@ -27,13 +28,11 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.9.4", - "geist": "^1.4.1", "lucide-react": "^0.507.0", "next": "^15.5.18", "next-themes": "^0.4.6", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-intersection-observer": "^9.16.0", "react-syntax-highlighter": "^15.6.1", "tailwind-merge": "^3.2.0", "tailwindcss": "^3.4.3", @@ -52,6 +51,7 @@ "jest": "^30.2.0", "jest-environment-jsdom": "^30.2.0", "jest-extended": "^7.0.0", + "playwright-core": "^1.61.1", "postcss": "^8.5.12", "prettier": "^3.3.2", "prettier-plugin-tailwindcss": "^0.6.5", diff --git a/web/scripts/viewport-qa.mjs b/web/scripts/viewport-qa.mjs new file mode 100644 index 0000000..4068d7b --- /dev/null +++ b/web/scripts/viewport-qa.mjs @@ -0,0 +1,175 @@ +#!/usr/bin/env node +/** + * Viewport QA for the Vocalinux marketing home page. + * Uses system Google Chrome via playwright-core (no browser download). + * + * Usage: + * node scripts/viewport-qa.mjs [baseUrl] [outDir] [passLabel] + * + * Exit 0 only when every configured viewport has no horizontal overflow + * and the primary hero + install terminal are present and in-bounds. + */ +import { chromium } from "playwright-core"; +import fs from "node:fs"; +import path from "node:path"; + +const baseUrl = process.argv[2] || "http://127.0.0.1:3456"; +const outDir = process.argv[3] || path.join(process.cwd(), "viewport-qa-out"); +const passLabel = process.argv[4] || "pass"; + +const VIEWPORTS = [ + { name: "desktop", width: 1280, height: 800 }, + { name: "tablet", width: 768, height: 1024 }, + { name: "mobile", width: 390, height: 844 }, +]; + +const CHROME = + process.env.CHROME_PATH || + ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium"].find( + (p) => fs.existsSync(p), + ); + +if (!CHROME) { + console.error("No Chrome binary found. Set CHROME_PATH."); + process.exit(2); +} + +fs.mkdirSync(outDir, { recursive: true }); + +const results = []; + +const browser = await chromium.launch({ + executablePath: CHROME, + headless: true, + args: ["--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu"], +}); + +try { + for (const vp of VIEWPORTS) { + const context = await browser.newContext({ + viewport: { width: vp.width, height: vp.height }, + deviceScaleFactor: 1, + }); + const page = await context.newPage(); + const pageErrors = []; + page.on("pageerror", (err) => pageErrors.push(String(err))); + + await page.goto(baseUrl + "/", { waitUntil: "networkidle", timeout: 60000 }); + await page.waitForSelector("h1", { timeout: 15000 }); + + const metrics = await page.evaluate(() => { + const de = document.documentElement; + const hero = document.querySelector("h1"); + const terminal = document.querySelector(".terminal-panel"); + const install = document.querySelector("#install"); + const nav = document.querySelector("header nav"); + const overflowX = de.scrollWidth > de.clientWidth + 1; + const bodyText = document.body.innerText; + const heroRect = hero ? hero.getBoundingClientRect() : null; + const terminalRect = terminal ? terminal.getBoundingClientRect() : null; + const inBounds = (r) => + !!r && r.width > 0 && r.height > 0 && r.left >= -1 && r.right <= de.clientWidth + 1; + + return { + scrollWidth: de.scrollWidth, + clientWidth: de.clientWidth, + overflowX, + hasVocalinux: bodyText.includes("Vocalinux"), + hasInstallCta: !!document.querySelector('a[href="#install"]'), + hasGradientText: !!document.querySelector('[class*="bg-clip-text"]'), + h1: hero ? hero.innerText.trim() : null, + heroInBounds: inBounds(heroRect), + terminalInBounds: terminal ? inBounds(terminalRect) : false, + installPresent: !!install, + navPresent: !!nav, + heroWidth: heroRect ? heroRect.width : null, + terminalWidth: terminalRect ? terminalRect.width : null, + }; + }); + + const shotName = + passLabel === "pass1" || passLabel === "1" + ? `${vp.name}.png` + : passLabel === "pass2" || passLabel === "2" || passLabel === "after" + ? `${vp.name}-after.png` + : `${vp.name}-${passLabel}.png`; + + await page.screenshot({ + path: path.join(outDir, shotName), + fullPage: false, + }); + + // Install section shot for mobile proof + if (vp.name === "mobile") { + await page.locator("#install").scrollIntoViewIfNeeded(); + await page.waitForTimeout(200); + await page.screenshot({ + path: path.join(outDir, shotName.replace(".png", "-install.png")), + fullPage: false, + }); + } + + // Mobile menu path check (header only; footer also has Features) + let mobileMenuOk = true; + if (vp.width < 768) { + const toggle = page.getByRole("button", { name: /toggle menu/i }); + if (await toggle.count()) { + await toggle.click(); + await page.waitForTimeout(150); + mobileMenuOk = await page + .locator("header") + .getByRole("link", { name: "Features" }) + .isVisible(); + await toggle.click(); + } else { + mobileMenuOk = false; + } + } + + const ok = + !metrics.overflowX && + metrics.hasVocalinux && + metrics.hasInstallCta && + !metrics.hasGradientText && + metrics.heroInBounds && + metrics.terminalInBounds && + metrics.navPresent && + mobileMenuOk && + pageErrors.length === 0; + + results.push({ + viewport: vp, + ok, + metrics, + mobileMenuOk, + pageErrors, + screenshot: shotName, + }); + + await context.close(); + } +} finally { + await browser.close(); +} + +const summary = { + baseUrl, + chrome: CHROME, + passLabel, + allOk: results.every((r) => r.ok), + results, +}; + +const jsonName = + passLabel === "pass1" || passLabel === "1" + ? "viewport-pass1.json" + : passLabel === "pass2" || passLabel === "2" || passLabel === "after" + ? "viewport-pass2.json" + : `viewport-${passLabel}.json`; + +fs.writeFileSync(path.join(outDir, jsonName), JSON.stringify(summary, null, 2)); +console.log(JSON.stringify(summary, null, 2)); + +if (!summary.allOk) { + process.exit(1); +} diff --git a/web/src/app/__tests__/screenshots-page.test.ts b/web/src/app/__tests__/screenshots-page.test.ts index e53f4a8..2dbc650 100644 --- a/web/src/app/__tests__/screenshots-page.test.ts +++ b/web/src/app/__tests__/screenshots-page.test.ts @@ -62,12 +62,16 @@ describe("Screenshots page and assets", () => { } }); - it("does not embed a full homepage gallery (link-only on home)", () => { - // Homepage should link to the gallery, not list every settings shot. + 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).not.toContain("/screenshots/settings-speech-engine.png"); + expect(homeSource).toContain("/screenshots/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"); + expect(homeSource).not.toContain("/screenshots/settings-shortcuts.png"); + expect(homeSource).not.toContain("/screenshots/settings-advanced.png"); }); it("exposes Screenshots in the shared subpage shell nav", () => { diff --git a/web/src/app/__tests__/viewport-layout.test.ts b/web/src/app/__tests__/viewport-layout.test.ts new file mode 100644 index 0000000..9b78049 --- /dev/null +++ b/web/src/app/__tests__/viewport-layout.test.ts @@ -0,0 +1,143 @@ +/** + * @jest-environment node + * + * Structural + real-browser layout checks for the marketing home page. + * + * 1. Structural: shipped CSS/markup include overflow guards (break-all, min-w-0). + * 2. Browser: when BASE_URL is set (or local :3456 is up), playwright-core + system + * Chrome measures scrollWidth vs clientWidth at three viewports. + * + * Run browser leg: + * BASE_URL=http://127.0.0.1:3456 npm test -- --testPathPatterns=viewport-layout + */ +import fs from "node:fs"; +import path from "node:path"; + +const root = path.resolve(__dirname, "../../.."); +const pageTsx = fs.readFileSync(path.join(root, "src/app/page.tsx"), "utf8"); +const globalsCss = fs.readFileSync( + path.join(root, "src/styles/globals.css"), + "utf8", +); + +const CHROME = + process.env.CHROME_PATH || + ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium"].find( + (p) => fs.existsSync(p), + ); + +async function resolveBaseUrl(): Promise { + if (process.env.BASE_URL) return process.env.BASE_URL.replace(/\/$/, ""); + try { + const res = await fetch("http://127.0.0.1:3456/", { signal: AbortSignal.timeout(2000) }); + if (res.ok) return "http://127.0.0.1:3456"; + } catch { + /* offline */ + } + return null; +} + +describe("marketing home layout guards (shipped source)", () => { + it("terminal and main constrain long monospace without page-wide overflow", () => { + 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/); + }); + + it("hero uses solid display type (no gradient text slop)", () => { + expect(pageTsx).not.toMatch(/bg-clip-text/); + expect(pageTsx).not.toMatch(/bg-gradient-to-r from-primary via-teal/); + expect(pageTsx).toMatch(/font-display/); + expect(pageTsx).toMatch(/TerminalBlock|terminal-panel/); + }); +}); + +describe("marketing home three-viewport overflow (live browser)", () => { + const viewports = [ + { name: "desktop", width: 1280, height: 800 }, + { name: "tablet", width: 768, height: 1024 }, + { name: "mobile", width: 390, height: 844 }, + ]; + + it("has no horizontal overflow and keeps hero + install terminal in bounds", async () => { + const baseUrl = await resolveBaseUrl(); + if (!baseUrl) { + console.warn("Skipping live browser leg: no BASE_URL and :3456 not up"); + return; + } + if (!CHROME) { + console.warn("Skipping live browser leg: no Chrome binary"); + return; + } + + const { chromium } = await import("playwright-core"); + const browser = await chromium.launch({ + executablePath: CHROME, + headless: true, + args: ["--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu"], + }); + + try { + for (const vp of viewports) { + const context = await browser.newContext({ + viewport: { width: vp.width, height: vp.height }, + }); + const page = await context.newPage(); + await page.goto(`${baseUrl}/`, { waitUntil: "networkidle", timeout: 60000 }); + await page.waitForSelector("h1"); + + const metrics = await page.evaluate(() => { + const de = document.documentElement; + const hero = document.querySelector("h1"); + const terminal = document.querySelector(".terminal-panel"); + const heroRect = hero?.getBoundingClientRect(); + const termRect = terminal?.getBoundingClientRect(); + const inBounds = (r?: DOMRect) => + !!r && + r.width > 0 && + r.height > 0 && + r.left >= -1 && + r.right <= de.clientWidth + 1; + return { + overflowX: de.scrollWidth > de.clientWidth + 1, + scrollWidth: de.scrollWidth, + clientWidth: de.clientWidth, + heroInBounds: inBounds(heroRect), + terminalInBounds: inBounds(termRect), + hasGradientText: !!document.querySelector('[class*="bg-clip-text"]'), + hasVocalinux: document.body.innerText.includes("Vocalinux"), + }; + }); + + expect({ + viewport: vp.name, + ...metrics, + }).toEqual( + expect.objectContaining({ + overflowX: false, + heroInBounds: true, + terminalInBounds: true, + hasGradientText: false, + hasVocalinux: true, + }), + ); + + if (vp.width < 768) { + await page.getByRole("button", { name: /toggle menu/i }).click(); + const featuresVisible = await page + .locator("header") + .getByRole("link", { name: "Features" }) + .isVisible(); + expect(featuresVisible).toBe(true); + } + + await context.close(); + } + } finally { + await browser.close(); + } + }, 120000); +}); diff --git a/web/src/app/advanced-settings/page.tsx b/web/src/app/advanced-settings/page.tsx index 9e87024..9d8e7d9 100644 --- a/web/src/app/advanced-settings/page.tsx +++ b/web/src/app/advanced-settings/page.tsx @@ -110,7 +110,7 @@ export default function AdvancedSettingsPage() { v0.11.0 Advanced Tab

-

+

Advanced Whisper.cpp Settings for Linux Dictation

@@ -145,9 +145,9 @@ export default function AdvancedSettingsPage() { return (

- +

{item.title}

@@ -159,8 +159,8 @@ export default function AdvancedSettingsPage() { })} -
-

+
+

Whisper.cpp Decoding Controls

@@ -168,7 +168,7 @@ export default function AdvancedSettingsPage() { {whisperControls.map((control) => (

{control.name}

{control.detail}

@@ -178,24 +178,24 @@ export default function AdvancedSettingsPage() {
-
-

- +
+

+ When to Use Advanced Settings

    {useCases.map((useCase) => (
  • - + {useCase}
  • ))}
-
-

- +
+

+ Keep Defaults Unless You Need Them

@@ -207,8 +207,8 @@ export default function AdvancedSettingsPage() {

-
-

Related Guides

+
+

Related Guides

Silero VAD

diff --git a/web/src/app/alternatives/page.tsx b/web/src/app/alternatives/page.tsx index 94ec314..3cb649a 100644 --- a/web/src/app/alternatives/page.tsx +++ b/web/src/app/alternatives/page.tsx @@ -101,11 +101,11 @@ export default function AlternativesPage() { />
-

+

Comparison

-

+

Vocalinux vs The Alternatives

@@ -114,8 +114,8 @@ export default function AlternativesPage() {

-
-

Why Vocalinux for Linux?

+
+

Why Vocalinux for Linux?

@@ -136,11 +136,11 @@ export default function AlternativesPage() {
-
-

Feature Comparison

+
+

Feature Comparison

- + @@ -150,34 +150,46 @@ export default function AlternativesPage() { {comparisonTable.map((row) => ( - + @@ -187,41 +199,49 @@ export default function AlternativesPage() {
-

Detailed Comparison

+

Detailed Comparison

{alternatives.map((alt) => (
-

{alt.name}

+

{alt.name}

{alt.type}

- + {alt.pricing}
-
-

Pros

-
    +
    +

    Pros

    +
      {alt.pros.map((pro) => ( -
    • - +
    • + {pro}
    • ))}
    -
    -

    Cons

    -
      +
      +

      + Cons +

      +
        {alt.cons.map((con) => ( -
      • - +
      • + {con}
      • ))} @@ -232,8 +252,8 @@ export default function AlternativesPage() { ))}
-
-

Read the Nerd Dictation comparison

+
+

Read the Nerd Dictation comparison

Want a direct Linux-focused breakdown of CLI vs GUI workflows, Wayland behavior, and GPU support? Read our dedicated page. @@ -241,22 +261,22 @@ export default function AlternativesPage() {

Read the Nerd Dictation comparison Engine comparison
-
-

The Bottom Line

+
+

The Bottom Line

If you're on Linux and need voice dictation, Vocalinux is the only option that offers offline processing, privacy, and full desktop integration at no cost. @@ -264,14 +284,14 @@ export default function AlternativesPage() {

Install Vocalinux Our Privacy Commitment diff --git a/web/src/app/autostart/page.tsx b/web/src/app/autostart/page.tsx index 999a82e..284f442 100644 --- a/web/src/app/autostart/page.tsx +++ b/web/src/app/autostart/page.tsx @@ -142,11 +142,11 @@ export default function AutostartPage() { />
-

+

New in v0.7.0

-

+

Autostart Voice Dictation on Login

@@ -161,7 +161,7 @@ export default function AutostartPage() { return (

{method.method}

@@ -172,23 +172,23 @@ export default function AutostartPage() {
-

How It Works

+

How It Works

{howItWorks.map((step) => (
- + {step.step}

{step.title}

{step.description}

{step.code && ( -
- {step.code} +
+ {step.code}
)}
@@ -196,8 +196,8 @@ export default function AutostartPage() {
-
-

Desktop Environment Compatibility

+
+

Desktop Environment Compatibility

Vocalinux uses the XDG Autostart specification, which is supported by virtually all Linux desktop environments: @@ -205,7 +205,7 @@ export default function AutostartPage() {

Feature Vocalinux Dragon
{row.feature} {row.vocalinux ? ( - + ) : ( - + )} {row.dragon ? ( - + ) : ( - + )} {row.otter ? ( - + ) : ( - + )} {row.google ? ( - + ) : ( - + )}
- + @@ -214,12 +214,12 @@ export default function AutostartPage() { {desktopEnvironments.map((de) => ( - +
Desktop Method Supported
{de.name} {de.autostartMethod} {de.supported ? ( - + Yes @@ -235,27 +235,27 @@ export default function AutostartPage() { -
-

+
+

Not a systemd Service

-

+

Vocalinux autostart uses XDG desktop entries, not systemd services. This means it starts as a regular desktop application in your graphical session - which is the correct way to launch GUI apps. No background services, no root required.

-
-

Manual Setup (Advanced)

+
+

Manual Setup (Advanced)

If you prefer to manage autostart manually or need to customize the startup behavior:

Enable autostart:

-
- +
+ mkdir -p ~/.config/autostart && cp /usr/share/applications/vocalinux.desktop ~/.config/autostart/ @@ -263,14 +263,14 @@ export default function AutostartPage() {

Disable autostart:

-
- rm ~/.config/autostart/vocalinux.desktop +
+ rm ~/.config/autostart/vocalinux.desktop

Start minimized (skip welcome dialog):

-
- +
+ vocalinux --start-minimized
@@ -278,8 +278,8 @@ export default function AutostartPage() {
-
-

Ready to Automate?

+
+

Ready to Automate?

Install Vocalinux and enable autostart. Your voice dictation will be ready the moment you log in. @@ -287,14 +287,14 @@ export default function AutostartPage() {

Install Now Common Questions diff --git a/web/src/app/changelog/page.tsx b/web/src/app/changelog/page.tsx index dff5bf2..e234f2c 100644 --- a/web/src/app/changelog/page.tsx +++ b/web/src/app/changelog/page.tsx @@ -291,24 +291,24 @@ const getTypeStyles = (type: string) => { case "stable": return { badge: - "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400", + "bg-primary/10 text-primary dark:text-[color:var(--terminal-fg)]", label: "Stable", }; case "beta": return { badge: - "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400", + "bg-primary/10 text-primary dark:text-primary", label: "Beta", }; case "alpha": return { badge: - "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400", + "bg-muted text-muted-foreground dark:text-muted-foreground", label: "Alpha", }; default: return { - badge: "bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-400", + badge: "bg-muted text-zinc-700", label: type, }; } @@ -363,7 +363,7 @@ export default function ChangelogPage() { Release History

-

+

Vocalinux Changelog

@@ -378,12 +378,12 @@ export default function ChangelogPage() { return (

- {release.version} + {release.version}
- + {highlight} ))} @@ -438,7 +438,7 @@ export default function ChangelogPage() {
)} -
+
-
-

Stay Updated

+
+

Stay Updated

  • diff --git a/web/src/app/compare/page.tsx b/web/src/app/compare/page.tsx index e55cc67..cfe3da4 100644 --- a/web/src/app/compare/page.tsx +++ b/web/src/app/compare/page.tsx @@ -20,8 +20,8 @@ const engineTable = [ footprint: "Small models available (~74MB tiny)", bestFor: "Most users who want strong speed + quality", icon: Zap, - iconColor: "text-amber-500", - iconBg: "bg-amber-500/10", + iconColor: "text-primary", + iconBg: "bg-muted", }, { engine: "Whisper (OpenAI)", @@ -31,8 +31,8 @@ const engineTable = [ footprint: "Large dependency footprint (~2.3GB)", bestFor: "Users already standardized on PyTorch stack", icon: Sparkles, - iconColor: "text-violet-500", - iconBg: "bg-violet-500/10", + iconColor: "text-primary", + iconBg: "bg-primary/10", }, { engine: "VOSK", @@ -42,8 +42,8 @@ const engineTable = [ footprint: "Very lightweight (~40MB model)", bestFor: "Older hardware and minimal-resource environments", icon: Cpu, - iconColor: "text-cyan-500", - iconBg: "bg-cyan-500/10", + iconColor: "text-primary", + iconBg: "bg-primary/10", }, { engine: "Remote API", @@ -53,8 +53,8 @@ const engineTable = [ footprint: "No local model required", bestFor: "Powerful LAN servers or shared transcription backends", icon: Server, - iconColor: "text-green-500", - iconBg: "bg-green-500/10", + iconColor: "text-primary", + iconBg: "bg-primary/10", }, ]; @@ -109,7 +109,7 @@ export default function CompareEnginesPage() { Speech Engine Comparison

    -

    +

    whisper.cpp vs Whisper vs VOSK vs Remote API on Linux

    @@ -125,7 +125,7 @@ export default function CompareEnginesPage() { return (

    @@ -154,10 +154,10 @@ export default function CompareEnginesPage() { })}
-
+
- + @@ -172,7 +172,7 @@ export default function CompareEnginesPage() { return (
Engine Speed Hardware
@@ -206,7 +206,7 @@ export default function CompareEnginesPage() {
-
+

Switching Between Engines

@@ -219,7 +219,7 @@ export default function CompareEnginesPage() {
-
+

When to pick whisper.cpp

@@ -231,7 +231,7 @@ export default function CompareEnginesPage() {

-
+

When to pick Whisper

Choose OpenAI Whisper if your environment already depends on @@ -239,7 +239,7 @@ export default function CompareEnginesPage() {

-
+

When to pick VOSK

Choose VOSK on older laptops, low-RAM systems, or lightweight VMs @@ -247,7 +247,7 @@ export default function CompareEnginesPage() {

-
+

When to pick Remote API

@@ -265,11 +265,11 @@ export default function CompareEnginesPage() {
-
-

Next steps

+
+

Next steps

  • - + Install by distro:
  • - + Use interactive install to detect your hardware and pick the best engine defaults.
  • - + After install, tune model size, VAD sensitivity, or Remote API settings for your preferred latency and accuracy level. diff --git a/web/src/app/desktop-reliability/page.tsx b/web/src/app/desktop-reliability/page.tsx index 1cba7b7..57329e4 100644 --- a/web/src/app/desktop-reliability/page.tsx +++ b/web/src/app/desktop-reliability/page.tsx @@ -125,7 +125,7 @@ export default function DesktopReliabilityPage() { Desktop Reliability

    -

    +

    Reliable Voice Dictation on Real Linux Desktops

    @@ -142,9 +142,9 @@ export default function DesktopReliabilityPage() { return (

    - +

    {feature.title}

    @@ -156,13 +156,13 @@ export default function DesktopReliabilityPage() { })}
-
-

Reliability Work by Release

+
+

Reliability Work by Release

{releaseMap.map((release) => (

{release.version}

@@ -174,8 +174,8 @@ export default function DesktopReliabilityPage() {

-
-

Why This Matters

+
+

Why This Matters

    {[ "Dictation should survive laptop lid closes, docking changes, and USB re-enumeration.", @@ -184,15 +184,15 @@ export default function DesktopReliabilityPage() { "Settings should fit smaller displays and window managers with different decoration behavior.", ].map((item) => (
  • - + {item}
  • ))}
-
-

Where to Debug Issues

+
+

Where to Debug Issues

If dictation works in one app but not another, start with the display server and input method path. Wayland sessions often depend @@ -208,8 +208,8 @@ export default function DesktopReliabilityPage() {

-
-

Related Guides

+
+

Related Guides

Wayland support

diff --git a/web/src/app/faq/page.tsx b/web/src/app/faq/page.tsx index b40d9f6..15c597d 100644 --- a/web/src/app/faq/page.tsx +++ b/web/src/app/faq/page.tsx @@ -18,7 +18,7 @@ const faqCategories = [ { category: "Getting Started", icon: Zap, - iconColor: "text-amber-500", + iconColor: "text-primary", questions: [ { q: "How do I install Vocalinux?", @@ -37,7 +37,7 @@ const faqCategories = [ { category: "Privacy & Security", icon: Lock, - iconColor: "text-green-500", + iconColor: "text-primary", questions: [ { q: "Is Vocalinux really 100% offline?", @@ -56,7 +56,7 @@ const faqCategories = [ { category: "Speech Recognition", icon: Mic, - iconColor: "text-blue-500", + iconColor: "text-primary", questions: [ { q: "Which speech recognition engines are available?", @@ -79,7 +79,7 @@ const faqCategories = [ { category: "Usage & Features", icon: Volume2, - iconColor: "text-violet-500", + iconColor: "text-primary", questions: [ { q: "How do I start and stop dictation?", @@ -106,7 +106,7 @@ const faqCategories = [ { category: "Performance", icon: Cpu, - iconColor: "text-cyan-500", + iconColor: "text-primary", questions: [ { q: "How much RAM does Vocalinux need?", @@ -166,7 +166,7 @@ export default function FaqPage() { Help Center

-

+

Frequently Asked Questions

@@ -181,10 +181,10 @@ export default function FaqPage() { return (

-

- +

+ {category.category} @@ -194,7 +194,7 @@ export default function FaqPage() { {category.questions.map((item, index) => (

{item.q}

{item.a}

@@ -206,8 +206,8 @@ export default function FaqPage() { })}

-
-

Still have questions?

+
+

Still have questions?

  • diff --git a/web/src/app/for-developers/page.tsx b/web/src/app/for-developers/page.tsx index ac3d689..4a6958a 100644 --- a/web/src/app/for-developers/page.tsx +++ b/web/src/app/for-developers/page.tsx @@ -21,8 +21,8 @@ const useCases = [ "Dictate inline comments, JSDoc, docstrings, and README sections without leaving your editor.", example: "This function validates user input and returns true if valid", icon: FileCode2, - iconColor: "text-blue-500", - iconBg: "bg-blue-100 dark:bg-blue-900/30", + iconColor: "text-primary", + iconBg: "bg-primary/10", }, { title: "Git Commit Messages", @@ -30,8 +30,8 @@ const useCases = [ "Speak your commit messages naturally - no more brief, unhelpful commits because typing is slow.", example: "fix authentication timeout when server is under heavy load", icon: Terminal, - iconColor: "text-orange-500", - iconBg: "bg-orange-100 dark:bg-orange-900/30", + iconColor: "text-primary", + iconBg: "bg-primary/10", }, { title: "Pull Request Descriptions", @@ -39,8 +39,8 @@ const useCases = [ "Write detailed PR descriptions explaining your changes, rationale, and testing approach.", example: "This PR refactors the user service to improve testability and adds error handling", icon: FileText, - iconColor: "text-purple-500", - iconBg: "bg-purple-100 dark:bg-purple-900/30", + iconColor: "text-primary", + iconBg: "bg-primary/10", }, { title: "Technical Documentation", @@ -48,8 +48,8 @@ const useCases = [ "Create API docs, architecture guides, and technical specifications at speaking speed.", example: "The API accepts a JSON payload with the following fields", icon: FileText, - iconColor: "text-emerald-500", - iconBg: "bg-emerald-100 dark:bg-emerald-900/30", + iconColor: "text-primary", + iconBg: "bg-primary/10", }, { title: "Code Review Comments", @@ -57,8 +57,8 @@ const useCases = [ "Write thorough code review feedback without the fatigue of typing detailed explanations.", example: "Consider extracting this logic into a separate function for reusability", icon: MessageSquare, - iconColor: "text-rose-500", - iconBg: "bg-rose-100 dark:bg-rose-900/30", + iconColor: "text-primary", + iconBg: "bg-primary/10", }, { title: "Pair Programming Notes", @@ -66,8 +66,8 @@ const useCases = [ "Dictate notes while pairing - keep your hands on the keyboard for code, use voice for meta.", example: "Remember to add unit tests for the edge case we just discussed", icon: Zap, - iconColor: "text-amber-500", - iconBg: "bg-amber-100 dark:bg-amber-900/30", + iconColor: "text-primary", + iconBg: "bg-primary/10", }, ]; @@ -160,11 +160,11 @@ export default function ForDevelopersPage() { />
    -

    +

    For Developers

    -

    +

    Voice Dictation for Developers

    @@ -177,7 +177,7 @@ export default function ForDevelopersPage() { {benefits.map((benefit) => (

    {benefit.title}

    {benefit.description}

    @@ -186,14 +186,14 @@ export default function ForDevelopersPage() {
    -

    Developer Use Cases

    +

    Developer Use Cases

    {useCases.map((useCase) => { const Icon = useCase.icon; return (
    @@ -202,8 +202,8 @@ export default function ForDevelopersPage() {

    {useCase.title}

    {useCase.description}

    -
    - "{useCase.example}" +
    + "{useCase.example}"
    ); @@ -211,8 +211,8 @@ export default function ForDevelopersPage() {
    -
    -

    Helpful Voice Commands for Coding

    +
    +

    Helpful Voice Commands for Coding

    Use these voice commands for punctuation and symbols commonly used in code:

    @@ -220,9 +220,9 @@ export default function ForDevelopersPage() { {voiceCommands.map((cmd) => (
    - + "{cmd.command}" {cmd.action} @@ -234,19 +234,19 @@ export default function ForDevelopersPage() {

    -
    -

    +
    +

    Not Voice Coding - Voice Documentation

    -

    +

    Vocalinux is designed for dictating text, not writing code syntax. For actual code, your keyboard is still faster. But for comments, docs, messages, and notes - voice is a game-changer. Use both together for maximum productivity.

    -
    -

    Works in Every IDE

    +
    +

    Works in Every IDE

    {[ "VS Code", @@ -261,9 +261,9 @@ export default function ForDevelopersPage() { ].map((ide) => (
    - + {ide}
    ))} @@ -273,8 +273,8 @@ export default function ForDevelopersPage() {

    -
    -

    Start Dictating Your Docs

    +
    +

    Start Dictating Your Docs

    Install Vocalinux and start writing better documentation with less effort. Your future self (and your team) will thank you. @@ -282,14 +282,14 @@ export default function ForDevelopersPage() {

    Install Now RSI Prevention diff --git a/web/src/app/gnome-kde/page.tsx b/web/src/app/gnome-kde/page.tsx index e1e5234..c041e35 100644 --- a/web/src/app/gnome-kde/page.tsx +++ b/web/src/app/gnome-kde/page.tsx @@ -90,11 +90,11 @@ export default function GnomeVsKdePage() { />
    -

    +

    Linux Desktop Comparison

    -

    +

    Voice Typing on GNOME vs KDE Plasma: Linux Desktop Comparison

    @@ -104,8 +104,8 @@ export default function GnomeVsKdePage() {

    -
    -

    Why desktop environment matters for voice dictation

    +
    +

    Why desktop environment matters for voice dictation

    Voice typing quality is not just about the speech engine. Your desktop environment determines session type defaults, tray behavior, and input method routing. That affects whether dictation @@ -113,32 +113,32 @@ export default function GnomeVsKdePage() {

    -
    -

    GNOME: what to expect

    +
    +

    GNOME: what to expect

    • - + Wayland is the default on modern GNOME sessions, so IBus-backed text injection is usually the primary path.
    • - + IBus integration is central for reliable cross-app dictation behavior on Wayland.
    • - + Tray icon often needs AppIndicator support via GNOME extension, because legacy tray behavior differs from KDE.
    • - + Known issues and workarounds: if the tray icon is missing, install/enable AppIndicator extension; if text does not appear in apps, verify IBus daemon/engine state. @@ -147,25 +147,25 @@ export default function GnomeVsKdePage() {
    -
    -

    KDE Plasma: what to expect

    +
    +

    KDE Plasma: what to expect

    • - + X11 vs Wayland matters: Plasma supports both widely, so behavior can differ between sessions.
    • - + IBus setup may be more explicit compared with GNOME defaults, depending on distro and prior input method configuration.
    • - + System Settings integration is strong, making it easier to inspect session type, input method behavior, and tray settings from one place. @@ -174,11 +174,11 @@ export default function GnomeVsKdePage() {
    -
    -

    GNOME vs KDE Plasma comparison

    +
    +

    GNOME vs KDE Plasma comparison

    - + @@ -188,7 +188,7 @@ export default function GnomeVsKdePage() { {comparisonRows.map((row) => { const Icon = row.icon; return ( - +
    Feature GNOME KDE Plasma
    @@ -207,7 +207,7 @@ export default function GnomeVsKdePage() {
    -
    +

    When GNOME is a better fit

    Choose GNOME when you want a Wayland-first environment with familiar IBus defaults and a @@ -216,7 +216,7 @@ export default function GnomeVsKdePage() {

    -
    +

    When KDE Plasma is a better fit

    Choose KDE Plasma when you want deeper system configurability, flexible X11/Wayland session @@ -225,8 +225,8 @@ export default function GnomeVsKdePage() {

    -
    -

    Helpful guides

    +
    +

    Helpful guides

    Wayland setup and architecture @@ -240,15 +240,15 @@ export default function GnomeVsKdePage() {
    -
    -

    Ready to set up voice typing on your desktop?

    +
    +

    Ready to set up voice typing on your desktop?

    Install Vocalinux and get reliable offline voice dictation on GNOME or KDE Plasma, with the right text injection path for your session type.

    Install Vocalinux diff --git a/web/src/app/gpu-acceleration/page.tsx b/web/src/app/gpu-acceleration/page.tsx index c52d8d8..e261d46 100644 --- a/web/src/app/gpu-acceleration/page.tsx +++ b/web/src/app/gpu-acceleration/page.tsx @@ -25,8 +25,8 @@ const gpuBackends = [ "Best option for AMD and Intel GPUs", ], icon: Monitor, - iconColor: "text-violet-500", - iconBg: "bg-violet-100 dark:bg-violet-900/30", + iconColor: "text-primary", + iconBg: "bg-primary/10", recommended: true, }, { @@ -40,8 +40,8 @@ const gpuBackends = [ "Works with PyTorch-based Whisper engine", ], icon: Cpu, - iconColor: "text-green-500", - iconBg: "bg-green-100 dark:bg-green-900/30", + iconColor: "text-primary", + iconBg: "bg-primary/10", recommended: false, }, { @@ -56,8 +56,8 @@ const gpuBackends = [ "Lowest power consumption option", ], icon: MemoryStick, - iconColor: "text-blue-500", - iconBg: "bg-blue-100 dark:bg-blue-900/30", + iconColor: "text-primary", + iconBg: "bg-primary/10", recommended: false, }, ]; @@ -178,11 +178,11 @@ export default function GpuAccelerationPage() { />
    -

    +

    Performance

    -

    +

    GPU Accelerated Voice Dictation

    @@ -191,13 +191,13 @@ export default function GpuAccelerationPage() {

    -
    -

    Quick Check: Do You Have GPU Support?

    +
    +

    Quick Check: Do You Have GPU Support?

    Run this command to check if your system supports Vulkan GPU acceleration:

    -
    - vulkaninfo --summary | grep deviceName +
    + vulkaninfo --summary | grep deviceName

    If you see your GPU name, you're ready for accelerated dictation! @@ -205,17 +205,17 @@ export default function GpuAccelerationPage() {

    -

    Supported GPU Backends

    +

    Supported GPU Backends

    {gpuBackends.map((backend) => { const Icon = backend.icon; return (
    @@ -238,7 +238,7 @@ export default function GpuAccelerationPage() {
      {backend.advantages.map((advantage) => (
    • - + {advantage}
    • ))} @@ -249,8 +249,8 @@ export default function GpuAccelerationPage() {
    -
    -

    Model Performance Comparison

    +
    +

    Model Performance Comparison

    Approximate transcription times for 5 seconds of audio. GPU times assume a mid-range Vulkan-compatible GPU. @@ -258,7 +258,7 @@ export default function GpuAccelerationPage() {

    - + @@ -269,7 +269,7 @@ export default function GpuAccelerationPage() { {modelPerformance.map((row) => ( - + @@ -284,12 +284,12 @@ export default function GpuAccelerationPage() {
    -

    Hardware Requirements

    +

    Hardware Requirements

    {hardwareRequirements.map((tier) => (

    {tier.category}

    @@ -312,14 +312,14 @@ export default function GpuAccelerationPage() {
    -
    +
    - +
    -

    +

    Intel GPU Note

    -

    +

    Some Intel integrated GPUs don't fully support the compute features needed for whisper.cpp GPU acceleration. Vocalinux v0.7.0+ automatically detects incompatible Intel GPUs and falls back to CPU processing for reliability. @@ -328,8 +328,8 @@ export default function GpuAccelerationPage() {

    -
    -

    Get Started with GPU Acceleration

    +
    +

    Get Started with GPU Acceleration

    The installer automatically detects your GPU and configures the best backend. Just run and go. @@ -337,14 +337,14 @@ export default function GpuAccelerationPage() {

    Install Now Compare Engines diff --git a/web/src/app/install/[distro]/page.tsx b/web/src/app/install/[distro]/page.tsx index ea70e7c..948964e 100644 --- a/web/src/app/install/[distro]/page.tsx +++ b/web/src/app/install/[distro]/page.tsx @@ -157,26 +157,26 @@ const getDistroStyles = (distro: DistroSlug) => { if (distro === "ubuntu") { return { icon: Laptop, - iconClass: "text-orange-500", - chipClass: "border-orange-500/30 bg-orange-500/10 text-orange-600 dark:text-orange-400", - panelClass: "from-orange-500/15 to-amber-500/10", + iconClass: "text-primary", + chipClass: "border-border bg-muted text-muted-foreground", + panelClass: "bg-primary/10", }; } if (distro === "fedora") { return { icon: Sparkles, - iconClass: "text-blue-500", - chipClass: "border-blue-500/30 bg-blue-500/10 text-blue-600 dark:text-blue-400", - panelClass: "from-blue-500/15 to-cyan-500/10", + iconClass: "text-primary", + chipClass: "border-border bg-primary/10 text-primary", + panelClass: "bg-primary/10", }; } return { icon: Cpu, - iconClass: "text-emerald-500", - chipClass: "border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400", - panelClass: "from-emerald-500/15 to-teal-500/10", + iconClass: "text-primary", + chipClass: "border-border bg-primary/10 text-muted-foreground", + panelClass: "bg-primary/10", }; }; @@ -297,18 +297,18 @@ export default async function DistroInstallPage({ {config.name} Setup Guide

    -

    {config.title}

    +

    {config.title}

    {config.intro}

    - + Install command

    -
    - +
    + {config.installCommand}
    @@ -316,30 +316,30 @@ export default async function DistroInstallPage({
    -
    +

    - + Prerequisites

      {config.prerequisites.map((item) => (
    • - + {item}
    • ))}
    -
    +

    - + Tested environments

      {config.testedOn.map((item) => (
    • - + {item}
    • ))} @@ -347,30 +347,30 @@ export default async function DistroInstallPage({
    -
    +

    - + Setup notes for {config.name}

      {config.setupNotes.map((item) => (
    1. - + {item}
    2. ))}
    -
    +

    - + Post-install checklist

      {config.postInstallChecks.map((item) => (
    • - + {item}
    • ))} diff --git a/web/src/app/install/page.tsx b/web/src/app/install/page.tsx index 81ba509..2240fa0 100644 --- a/web/src/app/install/page.tsx +++ b/web/src/app/install/page.tsx @@ -1,6 +1,6 @@ import Link from "next/link"; import { type Metadata } from "next"; -import { ChevronRight, Cpu, Laptop, Sparkles, Terminal } from "lucide-react"; +import { ChevronRight, Cpu, Laptop, Terminal } from "lucide-react"; import { SeoSubpageShell } from "@/components/seo-subpage-shell"; import { buildPageMetadata } from "@/lib/seo"; @@ -14,17 +14,13 @@ const distroGuides = [ description: "Install offline voice dictation on Ubuntu 22.04+ with GNOME, KDE, X11, or Wayland.", icon: Laptop, - accent: "from-orange-500/15 to-amber-500/10", - iconColor: "text-orange-500", }, { name: "Fedora", href: "/install/fedora/", description: "Set up speech-to-text on Fedora Workstation and Silverblue with distro-specific notes.", - icon: Sparkles, - accent: "from-blue-500/15 to-cyan-500/10", - iconColor: "text-blue-500", + icon: Terminal, }, { name: "Arch Linux", @@ -32,8 +28,6 @@ const distroGuides = [ description: "Install Vocalinux on Arch, Manjaro, and EndeavourOS with package prerequisites and tips.", icon: Cpu, - accent: "from-emerald-500/15 to-teal-500/10", - iconColor: "text-emerald-500", }, ]; @@ -54,71 +48,75 @@ export const metadata: Metadata = buildPageMetadata({ export default function InstallGuidesPage() { return ( -
      -

      - - Linux Installation Guides -

      -

      - Install Voice Dictation on Linux -

      -

      - Pick your distro guide and follow a proven setup flow for offline speech-to-text on Linux. - Every guide is optimized for real desktop usage, not just terminal demos. +

      +

      Linux installation

      +

      Install Voice Dictation on Linux

      +

      + Pick your distro guide and follow a proven setup flow for offline + speech-to-text on Linux. Every guide is written for real desktop usage.

      -
      -

      - - Recommended interactive install +

      +

      + + install.sh --interactive

      - + {installCommand}
      -
      +
      {distroGuides.map((guide) => { const Icon = guide.icon; return ( -
      - +
      +
      -

      {guide.name}

      -

      {guide.description}

      - +

      {guide.name}

      +

      + {guide.description} +

      + Read {guide.name} guide - + ); })}
      -
      -

      After Installation

      +
      +

      After installation

      • - Launch with - vocalinux. + Launch with{" "} + + vocalinux + + .
      • -
      • Use toggle mode (double-tap Ctrl) or push-to-talk mode to control dictation in any text field.
      • - Choose whisper.cpp for speed, Whisper for maximum model compatibility, or VOSK for - low-RAM systems. + Use toggle mode (double-tap Ctrl) or push-to-talk to control + dictation in any text field. +
      • +
      • + Choose whisper.cpp for speed, Whisper for maximum model + compatibility, or VOSK for low-RAM systems.

      - Need help choosing an engine? See the - + Need help choosing an engine? See the{" "} + speech engine comparison . diff --git a/web/src/app/languages/page.tsx b/web/src/app/languages/page.tsx index e7e65b4..e52ee71 100644 --- a/web/src/app/languages/page.tsx +++ b/web/src/app/languages/page.tsx @@ -160,11 +160,11 @@ export default function LanguagesPage() { />

      -

      +

      9+ Languages

      -

      +

      Multilingual Voice Dictation for Linux

      @@ -179,7 +179,7 @@ export default function LanguagesPage() { return (

      {feature.title}

      @@ -190,12 +190,12 @@ export default function LanguagesPage() {
      -

      Supported Languages

      +

      Supported Languages

      {supportedLanguages.map((lang) => (
      {lang.flag} @@ -209,7 +209,7 @@ export default function LanguagesPage() { {lang.engines.map((engine) => ( {engine} @@ -220,22 +220,22 @@ export default function LanguagesPage() {
      -
      -

      How Language Support Works

      +
      +

      How Language Support Works

      Language Selection

      • - + Select your language in Settings → Speech Engine → Language
      • - + Use "Auto" for automatic language detection (whisper.cpp/Whisper)
      • - + Switch languages on-the-fly without restarting
      @@ -244,15 +244,15 @@ export default function LanguagesPage() {

      Model Selection

      • - + Larger models provide better accuracy for all languages
      • - + "tiny" model works for all supported languages
      • - + Models are downloaded automatically on first use
      @@ -260,8 +260,8 @@ export default function LanguagesPage() {
      -
      -

      Ready to Dictate in Your Language?

      +
      +

      Ready to Dictate in Your Language?

      Install Vocalinux and start dictating in your preferred language. All processing happens locally on your Linux machine. @@ -269,14 +269,14 @@ export default function LanguagesPage() {

      Install Now Compare Engines diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index 9114f34..9945c4a 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -1,6 +1,6 @@ import "@/styles/globals.css"; import React from "react"; -import { GeistSans } from "geist/font/sans"; +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"; @@ -13,13 +13,25 @@ import { TWITTER_HANDLE, } 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: "#18181b" }, + { media: "(prefers-color-scheme: dark)", color: "#0c0c0e" }, ], }; @@ -176,7 +188,7 @@ export default function RootLayout({ return ( diff --git a/web/src/app/offline/page.tsx b/web/src/app/offline/page.tsx index 2f2311e..0736556 100644 --- a/web/src/app/offline/page.tsx +++ b/web/src/app/offline/page.tsx @@ -19,32 +19,32 @@ const benefits = [ description: "Works on airplanes, in tunnels, on remote farms. Your voice never needs to leave your machine.", icon: WifiOff, - iconColor: "text-blue-500", - iconBg: "bg-blue-100 dark:bg-blue-900/30", + iconColor: "text-primary", + iconBg: "bg-primary/10", }, { title: "Zero Data Collection", description: "No telemetry, no analytics, no usage tracking. There's no server to send data to even if we wanted to.", icon: Database, - iconColor: "text-emerald-500", - iconBg: "bg-emerald-100 dark:bg-emerald-900/30", + iconColor: "text-primary", + iconBg: "bg-primary/10", }, { title: "No Account Required", description: "No signup, no login, no password to forget. Download and run - that's it.", icon: Lock, - iconColor: "text-violet-500", - iconBg: "bg-violet-100 dark:bg-violet-900/30", + iconColor: "text-primary", + iconBg: "bg-primary/10", }, { title: "Your Voice Stays Yours", description: "Voice data is biometric. Your voice patterns, accent, and speech habits are yours alone.", icon: Shield, - iconColor: "text-rose-500", - iconBg: "bg-rose-100 dark:bg-rose-900/30", + iconColor: "text-primary", + iconBg: "bg-primary/10", }, ]; @@ -135,11 +135,11 @@ export default function OfflinePage() { />
      -

      +

      100% Offline

      -

      +

      Offline Voice Dictation

      @@ -155,7 +155,7 @@ export default function OfflinePage() { return (

      @@ -167,12 +167,12 @@ export default function OfflinePage() { })}
      -
      -

      Offline vs Cloud Dictation

      +
      +

      Offline vs Cloud Dictation

    Model Size CPU Time
    {row.model} {row.size} {row.cpuSpeed}
    - + @@ -180,20 +180,20 @@ export default function OfflinePage() { {vsCloud.map((row) => ( - + @@ -204,12 +204,12 @@ export default function OfflinePage() {
    -

    When Offline Matters Most

    +

    When Offline Matters Most

    {sensitiveUseCases.map((useCase) => (

    {useCase.title}

    {useCase.description}

    @@ -218,14 +218,14 @@ export default function OfflinePage() {
    -
    -

    +
    +

    How Offline Processing Works

    -

    +

    Vocalinux uses speech recognition models that run entirely on your CPU or GPU:

    -
      +
      • whisper.cpp - C++ port of OpenAI Whisper, runs locally with GPU @@ -240,20 +240,20 @@ export default function OfflinePage() { OpenAI Whisper - PyTorch-based, runs on NVIDIA GPU or CPU
      -

      +

      Models are downloaded once (74MB-3.0GB depending on size), then all processing happens locally. No network requests during dictation.

    -
    -

    +
    +

    Verify It Yourself

    -

    +

    Don't trust - verify. Vocalinux is open source. You can:

    -
      +
      • Read every line of code on{" "} @@ -268,12 +268,12 @@ export default function OfflinePage() {
      • - Run with --debug to see all network activity (there is none) + Run with --debug to see all network activity (there is none)
      • - Monitor with tcpdump or{" "} - wireshark - you'll see silence + Monitor with tcpdump or{" "} + wireshark - you'll see silence
      • @@ -282,8 +282,8 @@ export default function OfflinePage() {
    -
    -

    Truly Private Voice Dictation

    +
    +

    Truly Private Voice Dictation

    Install Vocalinux and take control of your voice data. No cloud, no tracking, no compromises. @@ -291,14 +291,14 @@ export default function OfflinePage() {

    Install Now View Source Code diff --git a/web/src/app/open-source/page.tsx b/web/src/app/open-source/page.tsx index 7167bd2..e9d31a9 100644 --- a/web/src/app/open-source/page.tsx +++ b/web/src/app/open-source/page.tsx @@ -22,32 +22,32 @@ const benefits = [ description: "Your voice data stays with you. No subscription to cancel, no service to shut down, no company to go out of business.", icon: Lock, - iconColor: "text-blue-500", - iconBg: "bg-blue-100 dark:bg-blue-900/30", + iconColor: "text-primary", + iconBg: "bg-primary/10", }, { title: "Transparent & Auditable", description: "Every line of code is visible. Security researchers and users can verify exactly what the software does.", icon: Eye, - iconColor: "text-violet-500", - iconBg: "bg-violet-100 dark:bg-violet-900/30", + iconColor: "text-primary", + iconBg: "bg-primary/10", }, { title: "Community Driven", description: "Built by Linux users, for Linux users. Features and fixes come from real users, not corporate roadmaps.", icon: Users, - iconColor: "text-emerald-500", - iconBg: "bg-emerald-100 dark:bg-emerald-900/30", + iconColor: "text-primary", + iconBg: "bg-primary/10", }, { title: "Free Forever", description: "GPL v3 licensed. Free to use, modify, and share. No freemium, no premium tier, no hidden costs.", icon: Heart, - iconColor: "text-rose-500", - iconBg: "bg-rose-100 dark:bg-rose-900/30", + iconColor: "text-primary", + iconBg: "bg-primary/10", }, ]; @@ -133,11 +133,11 @@ export default function OpenSourcePage() { />
    -

    +

    Open Source

    -

    +

    Free & Open Source Voice Dictation

    @@ -146,14 +146,14 @@ export default function OpenSourcePage() {

    -
    +
    - -

    + +

    GPL v3 Licensed

    -

    +

    Vocalinux is released under the GNU General Public License v3.0, which means:

      @@ -163,7 +163,7 @@ export default function OpenSourcePage() { "Free to share with others", "Modifications must remain open", ].map((item) => ( -
    • +
    • {item}
    • @@ -172,14 +172,14 @@ export default function OpenSourcePage() {
    -

    Why Open Source Matters

    +

    Why Open Source Matters

    {benefits.map((benefit) => { const Icon = benefit.icon; return (
    @@ -194,12 +194,12 @@ export default function OpenSourcePage() {
    -
    -

    Open Source vs Proprietary

    +
    +

    Open Source vs Proprietary

    Feature Vocalinux (Offline) Cloud Dictation
    {row.feature} {row.offline ? ( - + ) : ( - + )} {row.cloud ? ( - + ) : ( - + )}
    - + @@ -207,9 +207,9 @@ export default function OpenSourcePage() { {vsProprietary.map((row) => ( - + - + ))} @@ -219,7 +219,7 @@ export default function OpenSourcePage() {
    -

    Built on Open Source

    +

    Built on Open Source

    Vocalinux stands on the shoulders of these excellent open source projects:

    @@ -227,11 +227,11 @@ export default function OpenSourcePage() { {techStack.map((tech) => (

    {tech.name}

    - + {tech.license}
    @@ -241,11 +241,11 @@ export default function OpenSourcePage() {
    -
    +
    -

    Privacy by Design

    +

    Privacy by Design

    Because Vocalinux is open source and runs entirely offline, you can verify that your voice data never leaves your computer. No telemetry, no analytics, no cloud @@ -256,7 +256,7 @@ export default function OpenSourcePage() { (item) => ( {item} @@ -267,14 +267,14 @@ export default function OpenSourcePage() {

    -
    +
    - +
    -

    +

    Contribute to Vocalinux

    -

    +

    We welcome contributions! Whether it's bug fixes, features, documentation, or translations - join the community.

    @@ -282,7 +282,7 @@ export default function OpenSourcePage() { href="https://github.com/jatinkrmalik/vocalinux" target="_blank" rel="noopener noreferrer" - className="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700" + className="inline-flex items-center gap-2 rounded-full border border-border bg-background px-4 py-2 text-sm font-semibold text-foreground hover:bg-muted" > View on GitHub @@ -291,22 +291,22 @@ export default function OpenSourcePage() {
    -
    -

    Join the Open Source Revolution

    +
    +

    Join the Open Source Revolution

    Install Vocalinux and experience voice dictation that respects your freedom and privacy.

    Install Free Compare Alternatives diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index e6c39c6..efd2b73 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -1,41 +1,22 @@ "use client"; import React, { useState, useEffect, useCallback } from "react"; -import { motion } from "framer-motion"; +import Image from "next/image"; import Link from "next/link"; -import { LiveDemo } from "@/components/live-demo"; import { - Terminal, - Activity, - Lock, - Zap, - Laptop, - Keyboard, - Settings, - Github, - Star, - Download, - ChevronRight, - CheckCircle2, - Menu, - X, - Copy, Check, - Play, - Shield, - ShieldCheck, - Server, - Globe, - Cpu, - Volume2, - Sparkles, + ChevronRight, + Copy, + Github, + Menu, + Star, + Terminal, + X, Heart, } from "lucide-react"; import { ThemeToggle } from "@/components/theme-toggle"; import { VocalinuxLogo } from "@/components/optimized-image"; -import { useInView } from "react-intersection-observer"; -// The one-liner install command (split into three lines for display) // Always uses main/install.sh. The installer dynamically resolves the latest release tag via GitHub API const installCommands = { interactiveInstallCommand: `curl -fsSL https://raw.githubusercontent.com/jatinkrmalik/vocalinux/main/install.sh -o /tmp/vl.sh && bash /tmp/vl.sh --interactive`, @@ -43,7 +24,6 @@ const installCommands = { https://raw.githubusercontent.com/jatinkrmalik/vocalinux/main/install.sh \\ -o /tmp/vl.sh && \\ bash /tmp/vl.sh --interactive`, - uninstallCommand: `curl -fsSL https://raw.githubusercontent.com/jatinkrmalik/vocalinux/main/uninstall.sh -o /tmp/vul.sh && bash /tmp/vul.sh`, uninstallDisplayCommand: `curl -fsSL \\ https://raw.githubusercontent.com/jatinkrmalik/vocalinux/main/uninstall.sh \\ @@ -147,7 +127,7 @@ const homeJsonLd = [ name: "Does Vocalinux preserve my keyboard layout?", acceptedAnswer: { "@type": "Answer", - text: "Yes! v0.10.1+ preserves your XKB keyboard layout when activating IBus, so you won't unexpectedly switch to US layout mid-dictation.", + text: "Yes. v0.10.1+ preserves your XKB keyboard layout when activating IBus, so you won't unexpectedly switch to US layout mid-dictation.", }, }, ], @@ -181,51 +161,84 @@ const homeJsonLd = [ }, ]; -const FeatureCard = ({ - icon, - title, - description, - href, -}: { - icon: React.ReactNode; - title: string; - description: string; - href?: string; -}) => { - const [ref, inView] = useInView({ - triggerOnce: true, - threshold: 0.1, - }); +const featureRows = [ + { + title: "Stays on your machine", + body: "Local engines process audio on-device. No cloud upload, no telemetry, no account.", + href: "/offline/", + link: "Offline privacy", + }, + { + title: "Works where you type", + body: "Terminals, browsers, IDEs, office apps, and any focused text field on X11 or Wayland.", + href: "/use-cases/", + link: "Use cases", + }, + { + title: "Fast by default", + body: "whisper.cpp with Vulkan on AMD, Intel, and NVIDIA. Tiny model is about 74MB.", + href: "/gpu-acceleration/", + link: "GPU acceleration", + }, + { + title: "Your shortcuts", + body: "Toggle or push-to-talk. Bind modifiers the way your hands already work.", + href: "/shortcuts/", + link: "Shortcuts", + }, + { + title: "Desktop reliability", + body: "Suspend recovery, IBus hardening, layout preservation, non-ASCII injection fallbacks.", + href: "/desktop-reliability/", + link: "Reliability notes", + }, + { + title: "Neural silence filter", + body: "Silero VAD drops empty buffers before recognition, with amplitude fallback if needed.", + href: "/voice-activity-detection/", + link: "Voice activity detection", + }, +]; - const card = ( - -
    -
    {icon}
    -

    {title}

    -
    -

    - {description} -

    - {href && ( -
    - Learn more{" "} - -
    - )} -
    - ); - - if (href) { - return {card}; - } - return card; -}; +const engines = [ + { + name: "whisper.cpp", + badge: "Default", + summary: "C++ Whisper with Vulkan. Fast install, multi-vendor GPU.", + points: [ + "About 1–2 min default setup", + "AMD / Intel / NVIDIA via Vulkan", + "Tiny model ~74MB", + ], + }, + { + name: "Whisper", + summary: "Original OpenAI PyTorch path for NVIDIA CUDA workflows.", + points: [ + "Same model family accuracy", + "CUDA when you already live in PyTorch", + "Larger install footprint", + ], + }, + { + name: "VOSK", + summary: "Small footprint for older machines and tight RAM budgets.", + points: [ + "CPU-friendly streaming", + "Models around ~40MB", + "Great on modest hardware", + ], + }, + { + name: "Remote API", + summary: "Offload to a server you trust while keeping desktop injection local.", + points: [ + "OpenAI-compatible endpoints", + "whisper.cpp server support", + "Local VAD still applies", + ], + }, +]; const CopyButton = ({ text, @@ -235,7 +248,6 @@ const CopyButton = ({ className?: string; }) => { const [copied, setCopied] = useState(false); - const handleCopy = useCallback(() => { navigator.clipboard.writeText(text); setCopied(true); @@ -244,49 +256,56 @@ const CopyButton = ({ return ( ); }; -const FadeInSection = ({ - children, - delay = 0, +function TerminalBlock({ + command, + displayCommand, + label = "terminal", }: { - children: React.ReactNode; - delay?: number; -}) => { - const [ref, inView] = useInView({ - triggerOnce: true, - threshold: 0.1, - rootMargin: "0px 0px -50px 0px", - }); + command: string; + displayCommand: string; + label?: string; +}) { return ( - - {children} - +
    +
    +
    +
    + + + +
    + + {label} + +
    + +
    +
    +
    +          $ 
    +          {displayCommand}
    +        
    +
    +
    ); -}; +} export default function HomePage() { const [stars, setStars] = useState(null); @@ -296,18 +315,12 @@ export default function HomePage() { fetch("https://api.github.com/repos/jatinkrmalik/vocalinux") .then((res) => res.json()) .then((data) => { - if (data.stargazers_count) { - setStars(data.stargazers_count); - } + if (data.stargazers_count) setStars(data.stargazers_count); }) - .catch(() => { - // Fallback if API fails - setStars(null); - }); + .catch(() => setStars(null)); }, []); const navLinks = [ - { href: "#demo", label: "Demo" }, { href: "#features", label: "Features" }, { href: "/screenshots/", label: "Screenshots" }, { href: "#install", label: "Install" }, @@ -323,36 +336,28 @@ export default function HomePage() { } = installCommands; return ( -
    +
    Feature Vocalinux Proprietary Alternatives
    {row.feature}{row.vocalinux}{row.vocalinux} {row.proprietary}