feat(web): redesign marketing site with workstation craft (#582)
* feat(web): deslop marketing site with workstation craft identity Replace generic AI SaaS landing patterns with a deliberate Linux desk utility look: cool paper palette, Bricolage + Source Sans, solid type, editorial feature rows, and terminal install as first-class proof. Adds PRODUCT.md, DESIGN.md, and WEB-DESLOP-NOTES.md. Drops unused geist and react-intersection-observer. No product claim changes. * chore(web): drop local deslop working notes from PR WEB-DESLOP-NOTES.md was a firstmate/session artifact, not product docs. * fix(web): stop mobile horizontal overflow and harden viewport layout Long install URLs expanded the page past the phone width. Constrain terminal pre with break-all, min-w-0 columns, and overflow-x clip. Tighten mobile hero type and full-width CTAs. Add Playwright viewport QA script and jest checks that assert no overflow at desktop/tablet/mobile. * refactor(web): drop browser speech preview from marketing home Remove the cloud SpeechRecognition demo section and nav Demo link. Keep product truth: hero install, features, screenshots, engines. Fold shortcut/engine/audio links into the features intro. Delete unused LiveDemo component and its unit test. * chore(web): remove orphaned demo overlay and double-press hook Only LiveDemo used these; gone with the browser preview section. * feat(web): borrow PR #546 craft language into deslop home Merge closed redesign language onto feat/web-deslop-craft: pure white and near-black iron surfaces, emerald-committed palette, asymmetric hero with real app screenshots, full-bleed install strip, feature bento with tray/settings shots, and install-to-dictation steps. Keep Bricolage + Source Sans (not Geist), no browser speech demo, and mobile overflow guards. Update screenshots homepage expectations. * docs(design): align DESIGN.md tokens with PR #546 iron/emerald palette * fix(web): drop settings screenshot from installer details Uninstall sat too far below the list once the general-settings shot was in that column. Keep checklist, requirements, and uninstall only. * fix(web): raise light-mode primary and footer contrast Darken light emerald so white button text hits ~5:1 (was 3.4). Brighten install-strip and footer secondary text for legibility. Verified light/dark × desktop/tablet/mobile with no overflow. * fix(web): compact feature cards to kill empty vertical space Right-column tiles were h-full + justify-between next to a tall screenshot, so short copy left large voids. Use content-height cards with denser padding and inline link rows instead. * fix(web): pair settings shot with full feature rail Replace three orphan cards beside a tall dialog with a six-row feature panel that stretches with the screenshot frame. Crop the settings image with object-cover/object-top so heights match and no void sits next to the UI shot. * feat(web): apply homepage craft language to SEO child pages Rewrite SeoSubpageShell chrome (fixed nav, install pill, iron footer) to match the deslop home. Add subpage utilities and retoken all templated child routes: borders, radii, mono kickers, single emerald accent, terminal panels, no rainbow icon tiles or gradient cards. * fix(web): avoid invalid @apply hover:border-primary/40 in subpage-card * fix(web): rewrite subpage CSS without invalid Tailwind @apply * fix(web): color comparison X icons rose-red on alternatives Use soft rose (#c45c5c / #e08b8b) for no/cons marks so they read as distinct from emerald yes/checks without looking like form errors. * docs(web): move product/design docs under web/ and drop impeccable markers Keep PRODUCT.md and DESIGN.md next to the Next app (not repo root), add web/AGENTS.md as the entry point, and point the root AGENTS website section at those files. Strip tool-specific schema comments. * fix(web): use icon-only copy control on install terminal Replace the chunky Copy/Copied chip with a quiet clipboard icon that turns into a check on success. * fix(web): restyle system requirements as a spec rail Replace the nested bullet card with mono-label / value pairs under the installer section so it matches the craft language used elsewhere. * fix(web): center engine rows and quiet the Default badge Align name/badge with multi-line engine details on sm+; replace the filled green pill with a small mono outline chip.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
+166
@@ -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
|
||||
@@ -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
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
/// <reference path="./out/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
Generated
+13
-25
@@ -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",
|
||||
|
||||
+2
-2
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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<string | null> {
|
||||
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);
|
||||
});
|
||||
@@ -110,7 +110,7 @@ export default function AdvancedSettingsPage() {
|
||||
<SlidersHorizontal className="h-4 w-4" />
|
||||
v0.11.0 Advanced Tab
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Advanced Whisper.cpp Settings for Linux Dictation
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -145,9 +145,9 @@ export default function AdvancedSettingsPage() {
|
||||
return (
|
||||
<article
|
||||
key={item.title}
|
||||
className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<span className="bg-primary/10 mb-4 inline-flex rounded-xl p-3">
|
||||
<span className="bg-primary/10 mb-4 inline-flex rounded-[12px] p-3">
|
||||
<Icon className="h-6 w-6 text-primary" />
|
||||
</span>
|
||||
<h2 className="mb-2 text-xl font-semibold">{item.title}</h2>
|
||||
@@ -159,8 +159,8 @@ export default function AdvancedSettingsPage() {
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-5 flex items-center gap-2 text-2xl font-bold">
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-5 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Cpu className="h-5 w-5 text-primary" />
|
||||
Whisper.cpp Decoding Controls
|
||||
</h2>
|
||||
@@ -168,7 +168,7 @@ export default function AdvancedSettingsPage() {
|
||||
{whisperControls.map((control) => (
|
||||
<article
|
||||
key={control.name}
|
||||
className="rounded-xl border border-zinc-200 bg-zinc-50 p-5 dark:border-zinc-700 dark:bg-zinc-900/60"
|
||||
className="rounded-[12px] border border-border bg-muted p-5"
|
||||
>
|
||||
<h3 className="mb-2 text-lg font-semibold">{control.name}</h3>
|
||||
<p className="text-sm text-muted-foreground">{control.detail}</p>
|
||||
@@ -178,24 +178,24 @@ export default function AdvancedSettingsPage() {
|
||||
</section>
|
||||
|
||||
<section className="mb-12 grid gap-6 lg:grid-cols-[1fr_1.1fr]">
|
||||
<div className="rounded-2xl border border-zinc-200 bg-zinc-50 p-6 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 flex items-center gap-2 text-2xl font-bold">
|
||||
<Settings className="h-5 w-5 text-blue-500" />
|
||||
<div className="rounded-[12px] border border-border bg-muted p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Settings className="h-5 w-5 text-primary" />
|
||||
When to Use Advanced Settings
|
||||
</h2>
|
||||
<ul className="space-y-3 text-sm text-muted-foreground">
|
||||
{useCases.map((useCase) => (
|
||||
<li key={useCase} className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>{useCase}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-amber-500/30 bg-amber-500/10 p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 text-2xl font-bold">
|
||||
<AlertTriangle className="h-5 w-5 text-amber-600 dark:text-amber-400" />
|
||||
<div className="rounded-[12px] border border-border bg-muted p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<AlertTriangle className="h-5 w-5 text-muted-foreground" />
|
||||
Keep Defaults Unless You Need Them
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@@ -207,8 +207,8 @@ export default function AdvancedSettingsPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border-primary/20 bg-primary/5 rounded-2xl border p-8">
|
||||
<h2 className="mb-4 text-2xl font-bold">Related Guides</h2>
|
||||
<section className="border-primary/20 bg-primary/5 rounded-[12px] border p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Related Guides</h2>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Link href="/voice-activity-detection/" className="group">
|
||||
<h3 className="font-semibold">Silero VAD</h3>
|
||||
|
||||
@@ -101,11 +101,11 @@ export default function AlternativesPage() {
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-primary/30 bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<p className="subpage-kicker">
|
||||
<Zap className="h-4 w-4" />
|
||||
Comparison
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Vocalinux vs The Alternatives
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -114,8 +114,8 @@ export default function AlternativesPage() {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-primary/20 bg-primary/5 p-6">
|
||||
<h2 className="mb-4 text-xl font-bold">Why Vocalinux for Linux?</h2>
|
||||
<section className="mb-12 rounded-[12px] border border-primary/20 bg-primary/5 p-6">
|
||||
<h2 className="mb-4 text-xl font-semibold">Why Vocalinux for Linux?</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Lock className="h-5 w-5 text-primary" />
|
||||
@@ -136,11 +136,11 @@ export default function AlternativesPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-white p-6 overflow-x-auto dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-6 text-2xl font-bold">Feature Comparison</h2>
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6 overflow-x-auto">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Feature Comparison</h2>
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-200 dark:border-zinc-700">
|
||||
<tr className="border-b border-border">
|
||||
<th className="pb-3 text-sm font-semibold">Feature</th>
|
||||
<th className="pb-3 text-sm font-semibold">Vocalinux</th>
|
||||
<th className="pb-3 text-sm font-semibold">Dragon</th>
|
||||
@@ -150,34 +150,46 @@ export default function AlternativesPage() {
|
||||
</thead>
|
||||
<tbody className="text-sm">
|
||||
{comparisonTable.map((row) => (
|
||||
<tr key={row.feature} className="border-b border-zinc-100 dark:border-zinc-700/70">
|
||||
<tr key={row.feature} className="border-b border-border">
|
||||
<td className="py-3 font-medium">{row.feature}</td>
|
||||
<td className="py-3">
|
||||
{row.vocalinux ? (
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
<CheckCircle2 className="h-5 w-5 text-primary" aria-label="Yes" />
|
||||
) : (
|
||||
<XCircle className="h-5 w-5 text-red-500" />
|
||||
<XCircle
|
||||
className="h-5 w-5 text-[#c45c5c] dark:text-[#e08b8b]"
|
||||
aria-label="No"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-3">
|
||||
{row.dragon ? (
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
<CheckCircle2 className="h-5 w-5 text-primary" aria-label="Yes" />
|
||||
) : (
|
||||
<XCircle className="h-5 w-5 text-red-500" />
|
||||
<XCircle
|
||||
className="h-5 w-5 text-[#c45c5c] dark:text-[#e08b8b]"
|
||||
aria-label="No"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-3">
|
||||
{row.otter ? (
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
<CheckCircle2 className="h-5 w-5 text-primary" aria-label="Yes" />
|
||||
) : (
|
||||
<XCircle className="h-5 w-5 text-red-500" />
|
||||
<XCircle
|
||||
className="h-5 w-5 text-[#c45c5c] dark:text-[#e08b8b]"
|
||||
aria-label="No"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-3">
|
||||
{row.google ? (
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
<CheckCircle2 className="h-5 w-5 text-primary" aria-label="Yes" />
|
||||
) : (
|
||||
<XCircle className="h-5 w-5 text-red-500" />
|
||||
<XCircle
|
||||
className="h-5 w-5 text-[#c45c5c] dark:text-[#e08b8b]"
|
||||
aria-label="No"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -187,41 +199,49 @@ export default function AlternativesPage() {
|
||||
</section>
|
||||
|
||||
<section className="space-y-6">
|
||||
<h2 className="text-2xl font-bold">Detailed Comparison</h2>
|
||||
<h2 className="font-display text-2xl font-semibold">Detailed Comparison</h2>
|
||||
{alternatives.map((alt) => (
|
||||
<article
|
||||
key={alt.name}
|
||||
className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold">{alt.name}</h3>
|
||||
<h3 className="text-xl font-semibold">{alt.name}</h3>
|
||||
<p className="text-sm text-muted-foreground">{alt.type}</p>
|
||||
</div>
|
||||
<span className="rounded-full bg-zinc-100 px-3 py-1 text-sm font-medium dark:bg-zinc-700">
|
||||
<span className="rounded-full bg-muted px-3 py-1 text-sm font-medium bg-muted">
|
||||
{alt.pricing}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="rounded-lg bg-green-50 p-4 dark:bg-green-900/20">
|
||||
<p className="mb-2 text-sm font-semibold text-green-700 dark:text-green-400">Pros</p>
|
||||
<ul className="space-y-1">
|
||||
<div className="rounded-[12px] border border-border bg-muted/40 p-4">
|
||||
<p className="mb-2 text-sm font-semibold text-primary">Pros</p>
|
||||
<ul className="space-y-1.5">
|
||||
{alt.pros.map((pro) => (
|
||||
<li key={pro} className="flex items-center gap-2 text-sm text-green-600 dark:text-green-300">
|
||||
<CheckCircle2 className="h-3.5 w-3.5" />
|
||||
<li
|
||||
key={pro}
|
||||
className="flex items-start gap-2 text-sm text-muted-foreground"
|
||||
>
|
||||
<CheckCircle2 className="mt-0.5 h-3.5 w-3.5 shrink-0 text-primary" />
|
||||
{pro}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-red-50 p-4 dark:bg-red-900/20">
|
||||
<p className="mb-2 text-sm font-semibold text-red-700 dark:text-red-400">Cons</p>
|
||||
<ul className="space-y-1">
|
||||
<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]">
|
||||
Cons
|
||||
</p>
|
||||
<ul className="space-y-1.5">
|
||||
{alt.cons.map((con) => (
|
||||
<li key={con} className="flex items-center gap-2 text-sm text-red-600 dark:text-red-300">
|
||||
<XCircle className="h-3.5 w-3.5" />
|
||||
<li
|
||||
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]" />
|
||||
{con}
|
||||
</li>
|
||||
))}
|
||||
@@ -232,8 +252,8 @@ export default function AlternativesPage() {
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="mt-12 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-3 text-2xl font-bold">Read the Nerd Dictation comparison</h2>
|
||||
<section className="mt-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 font-display text-2xl font-semibold">Read the Nerd Dictation comparison</h2>
|
||||
<p className="mb-5 text-muted-foreground">
|
||||
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() {
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link
|
||||
href="/vs-nerd-dictation/"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground dark:text-black hover:bg-primary/90"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||
>
|
||||
Read the Nerd Dictation comparison
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/compare/"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-zinc-300 bg-white px-4 py-2 text-sm font-semibold hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-800 dark:hover:bg-zinc-700"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted hover:bg-muted"
|
||||
>
|
||||
Engine comparison
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-12 rounded-2xl border border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 text-2xl font-bold">The Bottom Line</h2>
|
||||
<section className="mt-12 rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">The Bottom Line</h2>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
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() {
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Link
|
||||
href="/install/"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground dark:text-black hover:bg-primary/90"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||
>
|
||||
Install Vocalinux
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/privacy/"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-zinc-300 bg-white px-4 py-2 text-sm font-semibold hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-800 dark:hover:bg-zinc-700"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted hover:bg-muted"
|
||||
>
|
||||
<Lock className="h-4 w-4" />
|
||||
Our Privacy Commitment
|
||||
|
||||
@@ -142,11 +142,11 @@ export default function AutostartPage() {
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-primary/30 bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<p className="subpage-kicker">
|
||||
<Power className="h-4 w-4" />
|
||||
New in v0.7.0
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Autostart Voice Dictation on Login
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -161,7 +161,7 @@ export default function AutostartPage() {
|
||||
return (
|
||||
<div
|
||||
key={method.method}
|
||||
className="rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<Icon className="mb-3 h-6 w-6 text-primary" />
|
||||
<h3 className="mb-2 font-semibold">{method.method}</h3>
|
||||
@@ -172,23 +172,23 @@ export default function AutostartPage() {
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 text-2xl font-bold">How It Works</h2>
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">How It Works</h2>
|
||||
<div className="space-y-4">
|
||||
{howItWorks.map((step) => (
|
||||
<div
|
||||
key={step.step}
|
||||
className="rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<div className="mb-2 flex items-center gap-3">
|
||||
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-primary text-sm font-bold text-primary-foreground">
|
||||
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground">
|
||||
{step.step}
|
||||
</span>
|
||||
<h3 className="font-semibold">{step.title}</h3>
|
||||
</div>
|
||||
<p className="ml-10 text-sm text-muted-foreground">{step.description}</p>
|
||||
{step.code && (
|
||||
<div className="ml-10 mt-2 rounded-lg bg-zinc-950 p-2">
|
||||
<code className="text-sm text-green-400">{step.code}</code>
|
||||
<div className="ml-10 mt-2 rounded-lg bg-[color:var(--terminal)] p-2">
|
||||
<code className="text-sm text-[color:var(--terminal-fg)]">{step.code}</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -196,8 +196,8 @@ export default function AutostartPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-6 text-2xl font-bold">Desktop Environment Compatibility</h2>
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Desktop Environment Compatibility</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Vocalinux uses the XDG Autostart specification, which is supported by virtually all Linux
|
||||
desktop environments:
|
||||
@@ -205,7 +205,7 @@ export default function AutostartPage() {
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-200 dark:border-zinc-700">
|
||||
<tr className="border-b border-border">
|
||||
<th className="pb-3 pr-4 font-semibold">Desktop</th>
|
||||
<th className="pb-3 pr-4 font-semibold">Method</th>
|
||||
<th className="pb-3 pr-4 font-semibold">Supported</th>
|
||||
@@ -214,12 +214,12 @@ export default function AutostartPage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{desktopEnvironments.map((de) => (
|
||||
<tr key={de.name} className="border-b border-zinc-100 dark:border-zinc-700/70">
|
||||
<tr key={de.name} className="border-b border-border">
|
||||
<td className="py-3 pr-4 font-medium">{de.name}</td>
|
||||
<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-green-100 px-2 py-0.5 text-xs font-medium text-green-700 dark:bg-green-900/30 dark:text-green-400">
|
||||
<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)]">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
Yes
|
||||
</span>
|
||||
@@ -235,27 +235,27 @@ export default function AutostartPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-blue-200 bg-blue-50 p-6 dark:border-blue-800 dark:bg-blue-900/20">
|
||||
<h2 className="mb-3 text-xl font-bold text-blue-800 dark:text-blue-300">
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-primary/5 p-6">
|
||||
<h2 className="mb-3 text-xl font-semibold text-foreground">
|
||||
Not a systemd Service
|
||||
</h2>
|
||||
<p className="text-sm text-blue-700 dark:text-blue-400">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Vocalinux autostart uses <strong>XDG desktop entries</strong>, 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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-4 text-2xl font-bold">Manual Setup (Advanced)</h2>
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Manual Setup (Advanced)</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
If you prefer to manage autostart manually or need to customize the startup behavior:
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="mb-1 text-sm font-medium">Enable autostart:</p>
|
||||
<div className="rounded-lg bg-zinc-950 p-3">
|
||||
<code className="text-sm text-green-400">
|
||||
<div className="rounded-lg bg-[color:var(--terminal)] p-3">
|
||||
<code className="text-sm text-[color:var(--terminal-fg)]">
|
||||
mkdir -p ~/.config/autostart && cp /usr/share/applications/vocalinux.desktop
|
||||
~/.config/autostart/
|
||||
</code>
|
||||
@@ -263,14 +263,14 @@ export default function AutostartPage() {
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-1 text-sm font-medium">Disable autostart:</p>
|
||||
<div className="rounded-lg bg-zinc-950 p-3">
|
||||
<code className="text-sm text-green-400">rm ~/.config/autostart/vocalinux.desktop</code>
|
||||
<div className="rounded-lg bg-[color:var(--terminal)] p-3">
|
||||
<code className="text-sm text-[color:var(--terminal-fg)]">rm ~/.config/autostart/vocalinux.desktop</code>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-1 text-sm font-medium">Start minimized (skip welcome dialog):</p>
|
||||
<div className="rounded-lg bg-zinc-950 p-3">
|
||||
<code className="text-sm text-green-400">
|
||||
<div className="rounded-lg bg-[color:var(--terminal)] p-3">
|
||||
<code className="text-sm text-[color:var(--terminal-fg)]">
|
||||
vocalinux --start-minimized
|
||||
</code>
|
||||
</div>
|
||||
@@ -278,8 +278,8 @@ export default function AutostartPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 text-2xl font-bold">Ready to Automate?</h2>
|
||||
<section className="rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Ready to Automate?</h2>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
Install Vocalinux and enable autostart. Your voice dictation will be ready the moment you
|
||||
log in.
|
||||
@@ -287,14 +287,14 @@ export default function AutostartPage() {
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Link
|
||||
href="/install/"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground dark:text-black hover:bg-primary/90"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||
>
|
||||
Install Now
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/faq/"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-zinc-300 bg-white px-4 py-2 text-sm font-semibold hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-800 dark:hover:bg-zinc-700"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted hover:bg-muted"
|
||||
>
|
||||
Common Questions
|
||||
</Link>
|
||||
|
||||
@@ -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() {
|
||||
<Clock className="h-4 w-4" />
|
||||
Release History
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Vocalinux Changelog
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -378,12 +378,12 @@ export default function ChangelogPage() {
|
||||
return (
|
||||
<article
|
||||
key={release.version}
|
||||
className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<div className="mb-4 flex flex-wrap items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tag className="h-5 w-5 text-primary" />
|
||||
<span className="text-2xl font-bold">{release.version}</span>
|
||||
<span className="font-display text-2xl font-semibold">{release.version}</span>
|
||||
</div>
|
||||
<span
|
||||
className={`rounded-full px-3 py-1 text-xs font-semibold ${styles.badge}`}
|
||||
@@ -406,7 +406,7 @@ export default function ChangelogPage() {
|
||||
key={highlight}
|
||||
className="flex items-start gap-2 text-muted-foreground"
|
||||
>
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
{highlight}
|
||||
</li>
|
||||
))}
|
||||
@@ -438,7 +438,7 @@ export default function ChangelogPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 border-t border-zinc-100 pt-4 dark:border-zinc-700">
|
||||
<div className="mt-4 border-t border-border pt-4">
|
||||
<a
|
||||
href={`https://github.com/jatinkrmalik/vocalinux/releases/tag/${release.version}`}
|
||||
target="_blank"
|
||||
@@ -455,8 +455,8 @@ export default function ChangelogPage() {
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="mt-12 rounded-2xl border border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 text-2xl font-bold">Stay Updated</h2>
|
||||
<section className="mt-12 rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Stay Updated</h2>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
<li className="flex items-center gap-2">
|
||||
<Sparkles className="h-4 w-4 text-primary" />
|
||||
|
||||
@@ -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() {
|
||||
<Sparkles className="h-4 w-4" />
|
||||
Speech Engine Comparison
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
whisper.cpp vs Whisper vs VOSK vs Remote API on Linux
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -125,7 +125,7 @@ export default function CompareEnginesPage() {
|
||||
return (
|
||||
<article
|
||||
key={row.engine}
|
||||
className="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<span className={`inline-flex rounded-md p-2 ${row.iconBg}`}>
|
||||
@@ -154,10 +154,10 @@ export default function CompareEnginesPage() {
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="hidden overflow-x-auto rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-800 md:block">
|
||||
<section className="hidden overflow-x-auto rounded-[12px] border border-border bg-background md:block">
|
||||
<table className="w-full border-collapse text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-200 dark:border-zinc-700">
|
||||
<tr className="border-b border-border">
|
||||
<th className="px-4 py-3 text-sm font-semibold">Engine</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">Speed</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">Hardware</th>
|
||||
@@ -172,7 +172,7 @@ export default function CompareEnginesPage() {
|
||||
return (
|
||||
<tr
|
||||
key={row.engine}
|
||||
className="border-b border-zinc-100 align-top dark:border-zinc-700/70"
|
||||
className="border-b border-border align-top"
|
||||
>
|
||||
<td className="px-4 py-4 font-semibold">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
@@ -206,7 +206,7 @@ export default function CompareEnginesPage() {
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section className="mt-8 rounded-2xl border border-zinc-200 bg-zinc-50 p-6 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<section className="mt-8 rounded-[12px] border border-border bg-muted p-6">
|
||||
<h2 className="mb-3 text-2xl font-semibold">
|
||||
Switching Between Engines
|
||||
</h2>
|
||||
@@ -219,7 +219,7 @@ export default function CompareEnginesPage() {
|
||||
</section>
|
||||
|
||||
<section className="mt-12 grid gap-6 md:grid-cols-2 xl:grid-cols-4">
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 text-2xl font-semibold">
|
||||
When to pick whisper.cpp
|
||||
</h2>
|
||||
@@ -231,7 +231,7 @@ export default function CompareEnginesPage() {
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 text-2xl font-semibold">When to pick Whisper</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose OpenAI Whisper if your environment already depends on
|
||||
@@ -239,7 +239,7 @@ export default function CompareEnginesPage() {
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 text-2xl font-semibold">When to pick VOSK</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose VOSK on older laptops, low-RAM systems, or lightweight VMs
|
||||
@@ -247,7 +247,7 @@ export default function CompareEnginesPage() {
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 text-2xl font-semibold">
|
||||
When to pick Remote API
|
||||
</h2>
|
||||
@@ -265,11 +265,11 @@ export default function CompareEnginesPage() {
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="mt-12 rounded-2xl border border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 text-2xl font-bold">Next steps</h2>
|
||||
<section className="mt-12 rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Next steps</h2>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
<li className="flex gap-2">
|
||||
<CheckCircle2 className="mt-1 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-1 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
Install by distro:
|
||||
<Link
|
||||
@@ -296,14 +296,14 @@ export default function CompareEnginesPage() {
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex gap-2">
|
||||
<CheckCircle2 className="mt-1 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-1 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
Use interactive install to detect your hardware and pick the best
|
||||
engine defaults.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex gap-2">
|
||||
<CheckCircle2 className="mt-1 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-1 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
After install, tune model size, VAD sensitivity, or Remote API
|
||||
settings for your preferred latency and accuracy level.
|
||||
|
||||
@@ -125,7 +125,7 @@ export default function DesktopReliabilityPage() {
|
||||
<ShieldCheck className="h-4 w-4" />
|
||||
Desktop Reliability
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Reliable Voice Dictation on Real Linux Desktops
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -142,9 +142,9 @@ export default function DesktopReliabilityPage() {
|
||||
return (
|
||||
<article
|
||||
key={feature.title}
|
||||
className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<span className="bg-primary/10 mb-4 inline-flex rounded-xl p-3">
|
||||
<span className="bg-primary/10 mb-4 inline-flex rounded-[12px] p-3">
|
||||
<Icon className="h-6 w-6 text-primary" />
|
||||
</span>
|
||||
<h2 className="mb-2 text-xl font-semibold">{feature.title}</h2>
|
||||
@@ -156,13 +156,13 @@ export default function DesktopReliabilityPage() {
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-zinc-50 p-6 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-5 text-2xl font-bold">Reliability Work by Release</h2>
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-muted p-6">
|
||||
<h2 className="mb-5 font-display text-2xl font-semibold">Reliability Work by Release</h2>
|
||||
<div className="space-y-4">
|
||||
{releaseMap.map((release) => (
|
||||
<article
|
||||
key={release.version}
|
||||
className="rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<h3 className="mb-2 text-lg font-semibold">{release.version}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@@ -174,8 +174,8 @@ export default function DesktopReliabilityPage() {
|
||||
</section>
|
||||
|
||||
<section className="mb-12 grid gap-6 lg:grid-cols-[1fr_1fr]">
|
||||
<div className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-4 text-2xl font-bold">Why This Matters</h2>
|
||||
<div className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Why This Matters</h2>
|
||||
<ul className="space-y-3 text-sm text-muted-foreground">
|
||||
{[
|
||||
"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) => (
|
||||
<li key={item} className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="border-primary/20 bg-primary/5 rounded-2xl border p-6">
|
||||
<h2 className="mb-4 text-2xl font-bold">Where to Debug Issues</h2>
|
||||
<div className="border-primary/20 bg-primary/5 rounded-[12px] border p-6">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Where to Debug Issues</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
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() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border-primary/20 bg-primary/5 rounded-2xl border p-8">
|
||||
<h2 className="mb-4 text-2xl font-bold">Related Guides</h2>
|
||||
<section className="border-primary/20 bg-primary/5 rounded-[12px] border p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Related Guides</h2>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Link href="/wayland/" className="group">
|
||||
<h3 className="font-semibold">Wayland support</h3>
|
||||
|
||||
+12
-12
@@ -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() {
|
||||
<CircleHelp className="h-4 w-4" />
|
||||
Help Center
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Frequently Asked Questions
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -181,10 +181,10 @@ export default function FaqPage() {
|
||||
return (
|
||||
<article
|
||||
key={category.category}
|
||||
className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<h2 className="mb-6 flex items-center gap-3 text-2xl font-bold">
|
||||
<span className={`rounded-xl bg-zinc-100 p-2 dark:bg-zinc-700`}>
|
||||
<h2 className="mb-6 flex items-center gap-3 font-display text-2xl font-semibold">
|
||||
<span className={`rounded-[12px] bg-muted p-2 bg-muted`}>
|
||||
<Icon className={`h-5 w-5 ${category.iconColor}`} />
|
||||
</span>
|
||||
{category.category}
|
||||
@@ -194,7 +194,7 @@ export default function FaqPage() {
|
||||
{category.questions.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border-b border-zinc-100 pb-6 last:border-0 last:pb-0 dark:border-zinc-700"
|
||||
className="border-b border-border pb-6 last:border-0 last:pb-0"
|
||||
>
|
||||
<h3 className="mb-2 text-lg font-semibold">{item.q}</h3>
|
||||
<p className="text-muted-foreground">{item.a}</p>
|
||||
@@ -206,8 +206,8 @@ export default function FaqPage() {
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="mt-12 rounded-2xl border border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 text-2xl font-bold">Still have questions?</h2>
|
||||
<section className="mt-12 rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Still have questions?</h2>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
<li>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
|
||||
@@ -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() {
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-primary/30 bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<p className="subpage-kicker">
|
||||
<Code2 className="h-4 w-4" />
|
||||
For Developers
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Voice Dictation for Developers
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -177,7 +177,7 @@ export default function ForDevelopersPage() {
|
||||
{benefits.map((benefit) => (
|
||||
<div
|
||||
key={benefit.title}
|
||||
className="rounded-xl border border-zinc-200 bg-white p-5 text-center dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-5 text-center"
|
||||
>
|
||||
<h3 className="mb-2 font-semibold">{benefit.title}</h3>
|
||||
<p className="text-sm text-muted-foreground">{benefit.description}</p>
|
||||
@@ -186,14 +186,14 @@ export default function ForDevelopersPage() {
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 text-2xl font-bold">Developer Use Cases</h2>
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Developer Use Cases</h2>
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{useCases.map((useCase) => {
|
||||
const Icon = useCase.icon;
|
||||
return (
|
||||
<article
|
||||
key={useCase.title}
|
||||
className="rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<div className="mb-3 flex items-center gap-3">
|
||||
<div className={`rounded-lg p-2 ${useCase.iconBg}`}>
|
||||
@@ -202,8 +202,8 @@ export default function ForDevelopersPage() {
|
||||
<h3 className="font-semibold">{useCase.title}</h3>
|
||||
</div>
|
||||
<p className="mb-3 text-sm text-muted-foreground">{useCase.description}</p>
|
||||
<div className="rounded-lg bg-zinc-950 p-3">
|
||||
<code className="text-xs text-green-400">"{useCase.example}"</code>
|
||||
<div className="rounded-lg bg-[color:var(--terminal)] p-3">
|
||||
<code className="text-xs text-[color:var(--terminal-fg)]">"{useCase.example}"</code>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
@@ -211,8 +211,8 @@ export default function ForDevelopersPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-4 text-2xl font-bold">Helpful Voice Commands for Coding</h2>
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Helpful Voice Commands for Coding</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Use these voice commands for punctuation and symbols commonly used in code:
|
||||
</p>
|
||||
@@ -220,9 +220,9 @@ export default function ForDevelopersPage() {
|
||||
{voiceCommands.map((cmd) => (
|
||||
<div
|
||||
key={cmd.command}
|
||||
className="flex items-center justify-between rounded-lg border border-zinc-100 bg-zinc-50 px-3 py-2 dark:border-zinc-700 dark:bg-zinc-900"
|
||||
className="flex items-center justify-between rounded-lg border border-border bg-muted px-3 py-2"
|
||||
>
|
||||
<kbd className="rounded bg-zinc-200 px-2 py-0.5 text-xs font-mono dark:bg-zinc-700">
|
||||
<kbd className="rounded bg-muted px-2 py-0.5 text-xs font-mono bg-muted">
|
||||
"{cmd.command}"
|
||||
</kbd>
|
||||
<span className="text-xs text-muted-foreground">{cmd.action}</span>
|
||||
@@ -234,19 +234,19 @@ export default function ForDevelopersPage() {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-blue-200 bg-blue-50 p-6 dark:border-blue-800 dark:bg-blue-900/20">
|
||||
<h2 className="mb-3 text-xl font-bold text-blue-800 dark:text-blue-300">
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-primary/5 p-6">
|
||||
<h2 className="mb-3 text-xl font-semibold text-foreground">
|
||||
Not Voice Coding - Voice Documentation
|
||||
</h2>
|
||||
<p className="text-sm text-blue-700 dark:text-blue-400">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Vocalinux is designed for <strong>dictating text</strong>, 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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-4 text-2xl font-bold">Works in Every IDE</h2>
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Works in Every IDE</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
{[
|
||||
"VS Code",
|
||||
@@ -261,9 +261,9 @@ export default function ForDevelopersPage() {
|
||||
].map((ide) => (
|
||||
<div
|
||||
key={ide}
|
||||
className="flex items-center gap-2 rounded-lg border border-zinc-100 bg-zinc-50 px-3 py-2 dark:border-zinc-700 dark:bg-zinc-900"
|
||||
className="flex items-center gap-2 rounded-lg border border-border bg-muted px-3 py-2"
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<CheckCircle2 className="h-4 w-4 text-primary" />
|
||||
<span className="text-sm">{ide}</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -273,8 +273,8 @@ export default function ForDevelopersPage() {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 text-2xl font-bold">Start Dictating Your Docs</h2>
|
||||
<section className="rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Start Dictating Your Docs</h2>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
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() {
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Link
|
||||
href="/install/"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground dark:text-black hover:bg-primary/90"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||
>
|
||||
Install Now
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/rsi-prevention/"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-zinc-300 bg-white px-4 py-2 text-sm font-semibold hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-800 dark:hover:bg-zinc-700"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted hover:bg-muted"
|
||||
>
|
||||
RSI Prevention
|
||||
</Link>
|
||||
|
||||
@@ -90,11 +90,11 @@ export default function GnomeVsKdePage() {
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-primary/30 bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<p className="subpage-kicker">
|
||||
<Monitor className="h-4 w-4" />
|
||||
Linux Desktop Comparison
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Voice Typing on GNOME vs KDE Plasma: Linux Desktop Comparison
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -104,8 +104,8 @@ export default function GnomeVsKdePage() {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-4 text-2xl font-bold">Why desktop environment matters for voice dictation</h2>
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Why desktop environment matters for voice dictation</h2>
|
||||
<p className="text-muted-foreground">
|
||||
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() {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-4 text-2xl font-bold">GNOME: what to expect</h2>
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">GNOME: what to expect</h2>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
<strong>Wayland is the default</strong> on modern GNOME sessions, so IBus-backed text
|
||||
injection is usually the primary path.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
<strong>IBus integration is central</strong> for reliable cross-app dictation behavior on
|
||||
Wayland.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
<strong>Tray icon often needs AppIndicator support</strong> via GNOME extension, because
|
||||
legacy tray behavior differs from KDE.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
<strong>Known issues and workarounds:</strong> 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() {
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-4 text-2xl font-bold">KDE Plasma: what to expect</h2>
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">KDE Plasma: what to expect</h2>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
<strong>X11 vs Wayland matters</strong>: Plasma supports both widely, so behavior can differ
|
||||
between sessions.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
<strong>IBus setup may be more explicit</strong> compared with GNOME defaults, depending on
|
||||
distro and prior input method configuration.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<span>
|
||||
<strong>System Settings integration is strong</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() {
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 overflow-x-auto rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="px-4 pb-2 pt-5 text-2xl font-bold">GNOME vs KDE Plasma comparison</h2>
|
||||
<section className="mb-12 overflow-x-auto rounded-[12px] border border-border bg-background">
|
||||
<h2 className="px-4 pb-2 pt-5 font-display text-2xl font-semibold">GNOME vs KDE Plasma comparison</h2>
|
||||
<table className="w-full border-collapse text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-200 dark:border-zinc-700">
|
||||
<tr className="border-b border-border">
|
||||
<th className="px-4 py-3 text-sm font-semibold">Feature</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">GNOME</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">KDE Plasma</th>
|
||||
@@ -188,7 +188,7 @@ export default function GnomeVsKdePage() {
|
||||
{comparisonRows.map((row) => {
|
||||
const Icon = row.icon;
|
||||
return (
|
||||
<tr key={row.feature} className="border-b border-zinc-100 align-top dark:border-zinc-700/70">
|
||||
<tr key={row.feature} className="border-b border-border align-top">
|
||||
<td className="px-4 py-4 font-semibold">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<span className="rounded-md bg-primary/10 p-1.5">
|
||||
@@ -207,7 +207,7 @@ export default function GnomeVsKdePage() {
|
||||
</section>
|
||||
|
||||
<section className="mb-12 grid gap-6 md:grid-cols-2">
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 text-2xl font-semibold">When GNOME is a better fit</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose GNOME when you want a Wayland-first environment with familiar IBus defaults and a
|
||||
@@ -216,7 +216,7 @@ export default function GnomeVsKdePage() {
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 text-2xl font-semibold">When KDE Plasma is a better fit</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose KDE Plasma when you want deeper system configurability, flexible X11/Wayland session
|
||||
@@ -225,8 +225,8 @@ export default function GnomeVsKdePage() {
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-zinc-50 p-6 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 text-2xl font-bold">Helpful guides</h2>
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-muted p-6">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Helpful guides</h2>
|
||||
<div className="grid gap-3 text-sm text-muted-foreground sm:grid-cols-3">
|
||||
<Link href="/wayland/" className="font-semibold text-primary hover:underline">
|
||||
Wayland setup and architecture
|
||||
@@ -240,15 +240,15 @@ export default function GnomeVsKdePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-primary/20 bg-primary/5 p-8">
|
||||
<h2 className="mb-4 text-2xl font-bold">Ready to set up voice typing on your desktop?</h2>
|
||||
<section className="rounded-[12px] border border-primary/20 bg-primary/5 p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Ready to set up voice typing on your desktop?</h2>
|
||||
<p className="mb-6 max-w-3xl text-muted-foreground">
|
||||
Install Vocalinux and get reliable offline voice dictation on GNOME or KDE Plasma, with the
|
||||
right text injection path for your session type.
|
||||
</p>
|
||||
<Link
|
||||
href="/install/"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground dark:text-black hover:bg-primary/90"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||
>
|
||||
Install Vocalinux
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
|
||||
@@ -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() {
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-primary/30 bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<p className="subpage-kicker">
|
||||
<Zap className="h-4 w-4" />
|
||||
Performance
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
GPU Accelerated Voice Dictation
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -191,13 +191,13 @@ export default function GpuAccelerationPage() {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-primary/20 bg-primary/5 p-6">
|
||||
<h2 className="mb-4 text-xl font-bold">Quick Check: Do You Have GPU Support?</h2>
|
||||
<section className="mb-12 rounded-[12px] border border-primary/20 bg-primary/5 p-6">
|
||||
<h2 className="mb-4 text-xl font-semibold">Quick Check: Do You Have GPU Support?</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Run this command to check if your system supports Vulkan GPU acceleration:
|
||||
</p>
|
||||
<div className="rounded-lg bg-zinc-950 p-4">
|
||||
<code className="text-sm text-green-400">vulkaninfo --summary | grep deviceName</code>
|
||||
<div className="rounded-lg bg-[color:var(--terminal)] p-4">
|
||||
<code className="text-sm text-[color:var(--terminal-fg)]">vulkaninfo --summary | grep deviceName</code>
|
||||
</div>
|
||||
<p className="mt-3 text-sm text-muted-foreground">
|
||||
If you see your GPU name, you're ready for accelerated dictation!
|
||||
@@ -205,17 +205,17 @@ export default function GpuAccelerationPage() {
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 text-2xl font-bold">Supported GPU Backends</h2>
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Supported GPU Backends</h2>
|
||||
<div className="space-y-6">
|
||||
{gpuBackends.map((backend) => {
|
||||
const Icon = backend.icon;
|
||||
return (
|
||||
<article
|
||||
key={backend.name}
|
||||
className={`rounded-2xl border p-6 ${
|
||||
className={`rounded-[12px] border p-6 ${
|
||||
backend.recommended
|
||||
? "border-primary/50 bg-primary/5"
|
||||
: "border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-800"
|
||||
: "border-border bg-background "
|
||||
}`}
|
||||
>
|
||||
<div className="mb-4 flex flex-wrap items-start justify-between gap-4">
|
||||
@@ -238,7 +238,7 @@ export default function GpuAccelerationPage() {
|
||||
<ul className="space-y-1.5">
|
||||
{backend.advantages.map((advantage) => (
|
||||
<li key={advantage} className="flex items-start gap-2 text-sm">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
{advantage}
|
||||
</li>
|
||||
))}
|
||||
@@ -249,8 +249,8 @@ export default function GpuAccelerationPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-6 text-2xl font-bold">Model Performance Comparison</h2>
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Model Performance Comparison</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
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() {
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-200 dark:border-zinc-700">
|
||||
<tr className="border-b border-border">
|
||||
<th className="pb-3 pr-4 font-semibold">Model</th>
|
||||
<th className="pb-3 pr-4 font-semibold">Size</th>
|
||||
<th className="pb-3 pr-4 font-semibold">CPU Time</th>
|
||||
@@ -269,7 +269,7 @@ export default function GpuAccelerationPage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{modelPerformance.map((row) => (
|
||||
<tr key={row.model} className="border-b border-zinc-100 dark:border-zinc-700/70">
|
||||
<tr key={row.model} className="border-b border-border">
|
||||
<td className="py-3 pr-4 font-medium">{row.model}</td>
|
||||
<td className="py-3 pr-4 text-muted-foreground">{row.size}</td>
|
||||
<td className="py-3 pr-4 text-muted-foreground">{row.cpuSpeed}</td>
|
||||
@@ -284,12 +284,12 @@ export default function GpuAccelerationPage() {
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 text-2xl font-bold">Hardware Requirements</h2>
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Hardware Requirements</h2>
|
||||
<div className="grid gap-6 sm:grid-cols-3">
|
||||
{hardwareRequirements.map((tier) => (
|
||||
<article
|
||||
key={tier.category}
|
||||
className="rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<h3 className="mb-3 font-semibold">{tier.category}</h3>
|
||||
<dl className="space-y-2 text-sm">
|
||||
@@ -312,14 +312,14 @@ export default function GpuAccelerationPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-amber-200 bg-amber-50 p-6 dark:border-amber-800 dark:bg-amber-900/20">
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-muted p-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<Gauge className="mt-0.5 h-5 w-5 flex-shrink-0 text-amber-600 dark:text-amber-400" />
|
||||
<Gauge className="mt-0.5 h-5 w-5 flex-shrink-0 text-muted-foreground" />
|
||||
<div>
|
||||
<h3 className="mb-2 font-semibold text-amber-800 dark:text-amber-300">
|
||||
<h3 className="mb-2 font-semibold text-foreground">
|
||||
Intel GPU Note
|
||||
</h3>
|
||||
<p className="text-sm text-amber-700 dark:text-amber-400">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
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() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 text-2xl font-bold">Get Started with GPU Acceleration</h2>
|
||||
<section className="rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Get Started with GPU Acceleration</h2>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
The installer automatically detects your GPU and configures the best backend. Just run
|
||||
and go.
|
||||
@@ -337,14 +337,14 @@ export default function GpuAccelerationPage() {
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Link
|
||||
href="/install/"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground dark:text-black hover:bg-primary/90"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||
>
|
||||
Install Now
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/compare/"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-zinc-300 bg-white px-4 py-2 text-sm font-semibold hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-800 dark:hover:bg-zinc-700"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted hover:bg-muted"
|
||||
>
|
||||
Compare Engines
|
||||
</Link>
|
||||
|
||||
@@ -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
|
||||
</p>
|
||||
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl">{config.title}</h1>
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">{config.title}</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">{config.intro}</p>
|
||||
|
||||
<div
|
||||
className={`rounded-2xl border border-zinc-200 bg-gradient-to-br p-6 dark:border-zinc-700 ${styles.panelClass}`}
|
||||
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-green-600 dark:text-green-400" />
|
||||
<Terminal className="h-5 w-5 text-primary dark:text-[color:var(--terminal-fg)]" />
|
||||
Install command
|
||||
</h2>
|
||||
<div className="rounded-xl bg-zinc-950 p-4">
|
||||
<code className="block overflow-x-auto whitespace-pre-wrap break-all text-sm text-green-400 sm:text-base">
|
||||
<div className="rounded-[12px] bg-[color:var(--terminal)] p-4">
|
||||
<code className="block overflow-x-auto whitespace-pre-wrap break-all text-sm text-[color:var(--terminal-fg)] sm:text-base">
|
||||
{config.installCommand}
|
||||
</code>
|
||||
</div>
|
||||
@@ -316,30 +316,30 @@ export default async function DistroInstallPage({
|
||||
</section>
|
||||
|
||||
<section className="mt-10 grid gap-6 md:grid-cols-2">
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 inline-flex items-center gap-2 text-2xl font-semibold">
|
||||
<Shield className="h-5 w-5 text-blue-500" />
|
||||
<Shield className="h-5 w-5 text-primary" />
|
||||
Prerequisites
|
||||
</h2>
|
||||
<ul className="space-y-2 text-muted-foreground">
|
||||
{config.prerequisites.map((item) => (
|
||||
<li key={item} className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 inline-flex items-center gap-2 text-2xl font-semibold">
|
||||
<Sparkles className="h-5 w-5 text-violet-500" />
|
||||
<Sparkles className="h-5 w-5 text-primary" />
|
||||
Tested environments
|
||||
</h2>
|
||||
<ul className="space-y-2 text-muted-foreground">
|
||||
{config.testedOn.map((item) => (
|
||||
<li key={item} className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
@@ -347,30 +347,30 @@ export default async function DistroInstallPage({
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="mt-10 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<section className="mt-10 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 inline-flex items-center gap-2 text-2xl font-semibold">
|
||||
<Cpu className="h-5 w-5 text-cyan-500" />
|
||||
<Cpu className="h-5 w-5 text-primary" />
|
||||
Setup notes for {config.name}
|
||||
</h2>
|
||||
<ol className="space-y-3 text-muted-foreground">
|
||||
{config.setupNotes.map((item) => (
|
||||
<li key={item} className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section className="mt-10 rounded-2xl border border-zinc-200 bg-zinc-50 p-6 dark:border-zinc-700 dark:bg-zinc-900/70">
|
||||
<section className="mt-10 rounded-[12px] border border-border bg-muted p-6 /70">
|
||||
<h2 className="mb-4 inline-flex items-center gap-2 text-2xl font-semibold">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
<CheckCircle2 className="h-5 w-5 text-primary" />
|
||||
Post-install checklist
|
||||
</h2>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
{config.postInstallChecks.map((item) => (
|
||||
<li key={item} className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -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 (
|
||||
<SeoSubpageShell>
|
||||
<section className="text-center">
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-primary/30 bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
Linux Installation Guides
|
||||
</p>
|
||||
<h1 className="mb-6 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
Install Voice Dictation on Linux
|
||||
</h1>
|
||||
<p className="mx-auto mb-8 max-w-3xl text-lg text-muted-foreground">
|
||||
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.
|
||||
<section>
|
||||
<p className="subpage-kicker">Linux installation</p>
|
||||
<h1>Install Voice Dictation on Linux</h1>
|
||||
<p className="subpage-lede">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div className="rounded-2xl border border-zinc-200 bg-zinc-950 p-6 text-left shadow-lg dark:border-zinc-700">
|
||||
<p className="mb-3 inline-flex items-center gap-2 text-sm font-semibold uppercase tracking-wide text-zinc-400">
|
||||
<Terminal className="h-4 w-4 text-green-400" />
|
||||
Recommended interactive install
|
||||
<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 whitespace-pre-wrap break-all text-sm text-green-400 sm:text-base">
|
||||
<code className="block overflow-x-auto break-all text-sm sm:text-base">
|
||||
{installCommand}
|
||||
</code>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-14 grid gap-6 md:grid-cols-3">
|
||||
<section className="mt-12 grid gap-4 md:grid-cols-3">
|
||||
{distroGuides.map((guide) => {
|
||||
const Icon = guide.icon;
|
||||
return (
|
||||
<Link
|
||||
key={guide.href}
|
||||
href={guide.href}
|
||||
className="group rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm transition-all hover:-translate-y-1 hover:shadow-lg dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="subpage-card group p-5"
|
||||
>
|
||||
<div
|
||||
className={`mb-4 inline-flex rounded-xl bg-gradient-to-br p-3 ${guide.accent}`}
|
||||
>
|
||||
<Icon className={`h-6 w-6 ${guide.iconColor}`} />
|
||||
<div className="mb-3 inline-flex rounded-[10px] bg-primary/10 p-2.5">
|
||||
<Icon className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<h2 className="mb-3 text-2xl font-semibold">{guide.name}</h2>
|
||||
<p className="mb-5 text-sm text-muted-foreground">{guide.description}</p>
|
||||
<span className="inline-flex items-center gap-1 text-sm font-semibold text-primary group-hover:underline">
|
||||
<h2 className="mb-2 text-xl font-semibold">{guide.name}</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
{guide.description}
|
||||
</p>
|
||||
<span className="inline-flex items-center gap-1 text-sm font-semibold text-primary">
|
||||
Read {guide.name} guide
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
<ChevronRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="mt-16 rounded-2xl border border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 text-2xl font-bold">After Installation</h2>
|
||||
<section className="subpage-panel mt-12 p-6 sm:p-8">
|
||||
<h2 className="mb-4">After installation</h2>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
<li>
|
||||
Launch with
|
||||
<code className="ml-1.5 rounded bg-zinc-200 px-1.5 py-0.5 dark:bg-zinc-800">vocalinux</code>.
|
||||
Launch with{" "}
|
||||
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-sm text-foreground">
|
||||
vocalinux
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li>Use toggle mode (double-tap Ctrl) or push-to-talk mode to control dictation in any text field.</li>
|
||||
<li>
|
||||
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.
|
||||
</li>
|
||||
<li>
|
||||
Choose whisper.cpp for speed, Whisper for maximum model
|
||||
compatibility, or VOSK for low-RAM systems.
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mt-6 text-sm text-muted-foreground">
|
||||
Need help choosing an engine? See the
|
||||
<Link href="/compare/" className="ml-1 font-semibold text-primary hover:underline">
|
||||
Need help choosing an engine? See the{" "}
|
||||
<Link
|
||||
href="/compare/"
|
||||
className="font-semibold text-primary hover:underline"
|
||||
>
|
||||
speech engine comparison
|
||||
</Link>
|
||||
.
|
||||
|
||||
@@ -160,11 +160,11 @@ export default function LanguagesPage() {
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-primary/30 bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<p className="subpage-kicker">
|
||||
<Languages className="h-4 w-4" />
|
||||
9+ Languages
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Multilingual Voice Dictation for Linux
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -179,7 +179,7 @@ export default function LanguagesPage() {
|
||||
return (
|
||||
<div
|
||||
key={feature.title}
|
||||
className="rounded-xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<Icon className="mb-3 h-8 w-8 text-primary" />
|
||||
<h3 className="mb-2 font-semibold">{feature.title}</h3>
|
||||
@@ -190,12 +190,12 @@ export default function LanguagesPage() {
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 text-2xl font-bold">Supported Languages</h2>
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Supported Languages</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{supportedLanguages.map((lang) => (
|
||||
<article
|
||||
key={lang.code}
|
||||
className="rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<div className="mb-3 flex items-center gap-3">
|
||||
<span className="text-3xl">{lang.flag}</span>
|
||||
@@ -209,7 +209,7 @@ export default function LanguagesPage() {
|
||||
{lang.engines.map((engine) => (
|
||||
<span
|
||||
key={engine}
|
||||
className="rounded-full bg-zinc-100 px-2 py-0.5 text-xs font-medium dark:bg-zinc-700"
|
||||
className="rounded-full bg-muted px-2 py-0.5 text-xs font-medium bg-muted"
|
||||
>
|
||||
{engine}
|
||||
</span>
|
||||
@@ -220,22 +220,22 @@ export default function LanguagesPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-4 text-2xl font-bold">How Language Support Works</h2>
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">How Language Support Works</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<h3 className="mb-3 font-semibold">Language Selection</h3>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Select your language in Settings → Speech Engine → Language
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Use "Auto" for automatic language detection (whisper.cpp/Whisper)
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Switch languages on-the-fly without restarting
|
||||
</li>
|
||||
</ul>
|
||||
@@ -244,15 +244,15 @@ export default function LanguagesPage() {
|
||||
<h3 className="mb-3 font-semibold">Model Selection</h3>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Larger models provide better accuracy for all languages
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
"tiny" model works for all supported languages
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Models are downloaded automatically on first use
|
||||
</li>
|
||||
</ul>
|
||||
@@ -260,8 +260,8 @@ export default function LanguagesPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 text-2xl font-bold">Ready to Dictate in Your Language?</h2>
|
||||
<section className="rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Ready to Dictate in Your Language?</h2>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
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() {
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Link
|
||||
href="/install/"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground dark:text-black hover:bg-primary/90"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||
>
|
||||
Install Now
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/compare/"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-zinc-300 bg-white px-4 py-2 text-sm font-semibold hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-800 dark:hover:bg-zinc-700"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted hover:bg-muted"
|
||||
>
|
||||
Compare Engines
|
||||
</Link>
|
||||
|
||||
+15
-3
@@ -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 (
|
||||
<html
|
||||
lang="en"
|
||||
className={`${GeistSans.variable} scroll-smooth`}
|
||||
className={`${display.variable} ${body.variable} scroll-smooth`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<head>
|
||||
|
||||
@@ -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() {
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-primary/30 bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<p className="subpage-kicker">
|
||||
<CloudOff className="h-4 w-4" />
|
||||
100% Offline
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Offline Voice Dictation
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -155,7 +155,7 @@ export default function OfflinePage() {
|
||||
return (
|
||||
<article
|
||||
key={benefit.title}
|
||||
className="rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<div className={`mb-3 inline-flex rounded-lg p-2 ${benefit.iconBg}`}>
|
||||
<Icon className={`h-5 w-5 ${benefit.iconColor}`} />
|
||||
@@ -167,12 +167,12 @@ export default function OfflinePage() {
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-6 text-2xl font-bold">Offline vs Cloud Dictation</h2>
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Offline vs Cloud Dictation</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-200 dark:border-zinc-700">
|
||||
<tr className="border-b border-border">
|
||||
<th className="pb-3 pr-4 font-semibold">Feature</th>
|
||||
<th className="pb-3 pr-4 font-semibold text-center">Vocalinux (Offline)</th>
|
||||
<th className="pb-3 font-semibold text-center">Cloud Dictation</th>
|
||||
@@ -180,20 +180,20 @@ export default function OfflinePage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{vsCloud.map((row) => (
|
||||
<tr key={row.feature} className="border-b border-zinc-100 dark:border-zinc-700/70">
|
||||
<tr key={row.feature} className="border-b border-border">
|
||||
<td className="py-3 pr-4">{row.feature}</td>
|
||||
<td className="py-3 pr-4 text-center">
|
||||
{row.offline ? (
|
||||
<CheckCircle2 className="mx-auto h-5 w-5 text-green-500" />
|
||||
<CheckCircle2 className="mx-auto h-5 w-5 text-primary" />
|
||||
) : (
|
||||
<XCircle className="mx-auto h-5 w-5 text-red-500" />
|
||||
<XCircle className="mx-auto h-5 w-5 text-muted-foreground/50" />
|
||||
)}
|
||||
</td>
|
||||
<td className="py-3 text-center">
|
||||
{row.cloud ? (
|
||||
<CheckCircle2 className="mx-auto h-5 w-5 text-green-500" />
|
||||
<CheckCircle2 className="mx-auto h-5 w-5 text-primary" />
|
||||
) : (
|
||||
<XCircle className="mx-auto h-5 w-5 text-red-500" />
|
||||
<XCircle className="mx-auto h-5 w-5 text-muted-foreground/50" />
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -204,12 +204,12 @@ export default function OfflinePage() {
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 text-2xl font-bold">When Offline Matters Most</h2>
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">When Offline Matters Most</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{sensitiveUseCases.map((useCase) => (
|
||||
<div
|
||||
key={useCase.title}
|
||||
className="rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<h3 className="mb-2 font-semibold">{useCase.title}</h3>
|
||||
<p className="text-sm text-muted-foreground">{useCase.description}</p>
|
||||
@@ -218,14 +218,14 @@ export default function OfflinePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-emerald-200 bg-emerald-50 p-6 dark:border-emerald-800 dark:bg-emerald-900/20">
|
||||
<h2 className="mb-3 text-xl font-bold text-emerald-800 dark:text-emerald-300">
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-primary/5 p-6">
|
||||
<h2 className="mb-3 text-xl font-semibold text-foreground">
|
||||
How Offline Processing Works
|
||||
</h2>
|
||||
<p className="mb-4 text-sm text-emerald-700 dark:text-emerald-400">
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Vocalinux uses speech recognition models that run entirely on your CPU or GPU:
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-emerald-700 dark:text-emerald-400">
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
<strong>whisper.cpp</strong> - C++ port of OpenAI Whisper, runs locally with GPU
|
||||
@@ -240,20 +240,20 @@ export default function OfflinePage() {
|
||||
<strong>OpenAI Whisper</strong> - PyTorch-based, runs on NVIDIA GPU or CPU
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mt-4 text-sm text-emerald-700 dark:text-emerald-400">
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
Models are downloaded once (74MB-3.0GB depending on size), then all processing happens
|
||||
locally. No network requests during dictation.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-amber-200 bg-amber-50 p-6 dark:border-amber-800 dark:bg-amber-900/20">
|
||||
<h2 className="mb-3 text-xl font-bold text-amber-800 dark:text-amber-300">
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-muted p-6">
|
||||
<h2 className="mb-3 text-xl font-semibold text-foreground">
|
||||
Verify It Yourself
|
||||
</h2>
|
||||
<p className="mb-3 text-sm text-amber-700 dark:text-amber-400">
|
||||
<p className="mb-3 text-sm text-muted-foreground">
|
||||
Don't trust - verify. Vocalinux is open source. You can:
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-amber-700 dark:text-amber-400">
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
Read every line of code on{" "}
|
||||
@@ -268,12 +268,12 @@ 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-amber-200 px-1 dark:bg-amber-900">--debug</code> to see all network activity (there is none)
|
||||
Run with <code className="rounded bg-muted px-1 dark:bg-muted">--debug</code> to see all network activity (there is none)
|
||||
</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-amber-200 px-1 dark:bg-amber-900">tcpdump</code> or{" "}
|
||||
<code className="rounded bg-amber-200 px-1 dark:bg-amber-900">wireshark</code> - you'll see silence
|
||||
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> - you'll see silence
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
@@ -282,8 +282,8 @@ export default function OfflinePage() {
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 text-2xl font-bold">Truly Private Voice Dictation</h2>
|
||||
<section className="rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Truly Private Voice Dictation</h2>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
Install Vocalinux and take control of your voice data. No cloud, no tracking, no
|
||||
compromises.
|
||||
@@ -291,14 +291,14 @@ export default function OfflinePage() {
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Link
|
||||
href="/install/"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground dark:text-black hover:bg-primary/90"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||
>
|
||||
Install Now
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/open-source/"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-zinc-300 bg-white px-4 py-2 text-sm font-semibold hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-800 dark:hover:bg-zinc-700"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted hover:bg-muted"
|
||||
>
|
||||
View Source Code
|
||||
</Link>
|
||||
|
||||
@@ -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() {
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-emerald-500/30 bg-emerald-500/10 px-4 py-1.5 text-sm font-medium text-emerald-600 dark:text-emerald-400">
|
||||
<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-muted-foreground">
|
||||
<Code className="h-4 w-4" />
|
||||
Open Source
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Free & Open Source Voice Dictation
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -146,14 +146,14 @@ export default function OpenSourcePage() {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-emerald-200 bg-emerald-50 p-6 dark:border-emerald-800 dark:bg-emerald-900/20">
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-primary/5 p-6">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<Scale className="h-6 w-6 text-emerald-600 dark:text-emerald-400" />
|
||||
<h2 className="text-xl font-bold text-emerald-800 dark:text-emerald-300">
|
||||
<Scale className="h-6 w-6 text-muted-foreground" />
|
||||
<h2 className="text-xl font-semibold text-foreground">
|
||||
GPL v3 Licensed
|
||||
</h2>
|
||||
</div>
|
||||
<p className="mb-4 text-emerald-700 dark:text-emerald-400">
|
||||
<p className="mb-4 text-muted-foreground">
|
||||
Vocalinux is released under the GNU General Public License v3.0, which means:
|
||||
</p>
|
||||
<ul className="grid gap-2 sm:grid-cols-2">
|
||||
@@ -163,7 +163,7 @@ export default function OpenSourcePage() {
|
||||
"Free to share with others",
|
||||
"Modifications must remain open",
|
||||
].map((item) => (
|
||||
<li key={item} className="flex items-center gap-2 text-sm text-emerald-700 dark:text-emerald-400">
|
||||
<li key={item} className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
{item}
|
||||
</li>
|
||||
@@ -172,14 +172,14 @@ export default function OpenSourcePage() {
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 text-2xl font-bold">Why Open Source Matters</h2>
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Why Open Source Matters</h2>
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
{benefits.map((benefit) => {
|
||||
const Icon = benefit.icon;
|
||||
return (
|
||||
<article
|
||||
key={benefit.title}
|
||||
className="rounded-xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className={`rounded-lg p-2.5 ${benefit.iconBg}`}>
|
||||
@@ -194,12 +194,12 @@ export default function OpenSourcePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-6 text-2xl font-bold">Open Source vs Proprietary</h2>
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Open Source vs Proprietary</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-200 dark:border-zinc-700">
|
||||
<tr className="border-b border-border">
|
||||
<th className="pb-3 pr-4 font-semibold">Feature</th>
|
||||
<th className="pb-3 pr-4 font-semibold">Vocalinux</th>
|
||||
<th className="pb-3 font-semibold">Proprietary Alternatives</th>
|
||||
@@ -207,9 +207,9 @@ export default function OpenSourcePage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{vsProprietary.map((row) => (
|
||||
<tr key={row.feature} className="border-b border-zinc-100 dark:border-zinc-700/70">
|
||||
<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-green-600 dark:text-green-400">{row.vocalinux}</td>
|
||||
<td className="py-3 pr-4 text-primary dark:text-[color:var(--terminal-fg)]">{row.vocalinux}</td>
|
||||
<td className="py-3 text-muted-foreground">{row.proprietary}</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -219,7 +219,7 @@ export default function OpenSourcePage() {
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 text-2xl font-bold">Built on Open Source</h2>
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Built on Open Source</h2>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
Vocalinux stands on the shoulders of these excellent open source projects:
|
||||
</p>
|
||||
@@ -227,11 +227,11 @@ export default function OpenSourcePage() {
|
||||
{techStack.map((tech) => (
|
||||
<div
|
||||
key={tech.name}
|
||||
className="rounded-xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-4"
|
||||
>
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<h3 className="font-semibold">{tech.name}</h3>
|
||||
<span className="rounded-full bg-zinc-100 px-2 py-0.5 text-xs font-medium dark:bg-zinc-700">
|
||||
<span className="rounded-full bg-muted px-2 py-0.5 text-xs font-medium bg-muted">
|
||||
{tech.license}
|
||||
</span>
|
||||
</div>
|
||||
@@ -241,11 +241,11 @@ export default function OpenSourcePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<Shield className="mt-1 h-8 w-8 flex-shrink-0 text-primary" />
|
||||
<div>
|
||||
<h2 className="mb-3 text-xl font-bold">Privacy by Design</h2>
|
||||
<h2 className="mb-3 text-xl font-semibold">Privacy by Design</h2>
|
||||
<p className="mb-4 text-muted-foreground">
|
||||
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) => (
|
||||
<span
|
||||
key={item}
|
||||
className="rounded-full bg-zinc-100 px-3 py-1 text-xs font-medium dark:bg-zinc-700"
|
||||
className="rounded-full bg-muted px-3 py-1 text-xs font-medium bg-muted"
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
@@ -267,14 +267,14 @@ export default function OpenSourcePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-blue-200 bg-blue-50 p-6 dark:border-blue-800 dark:bg-blue-900/20">
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-primary/5 p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<GitBranch className="mt-1 h-6 w-6 flex-shrink-0 text-blue-600 dark:text-blue-400" />
|
||||
<GitBranch className="mt-1 h-6 w-6 flex-shrink-0 text-primary" />
|
||||
<div>
|
||||
<h3 className="mb-2 font-semibold text-blue-800 dark:text-blue-300">
|
||||
<h3 className="mb-2 font-semibold text-foreground">
|
||||
Contribute to Vocalinux
|
||||
</h3>
|
||||
<p className="mb-4 text-sm text-blue-700 dark:text-blue-400">
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
We welcome contributions! Whether it's bug fixes, features, documentation, or
|
||||
translations - join the community.
|
||||
</p>
|
||||
@@ -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"
|
||||
>
|
||||
<Zap className="h-4 w-4" />
|
||||
View on GitHub
|
||||
@@ -291,22 +291,22 @@ export default function OpenSourcePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 text-2xl font-bold">Join the Open Source Revolution</h2>
|
||||
<section className="rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Join the Open Source Revolution</h2>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
Install Vocalinux and experience voice dictation that respects your freedom and privacy.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Link
|
||||
href="/install/"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground dark:text-black hover:bg-primary/90"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||
>
|
||||
Install Free
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/alternatives/"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-zinc-300 bg-white px-4 py-2 text-sm font-semibold hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-800 dark:hover:bg-zinc-700"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted hover:bg-muted"
|
||||
>
|
||||
Compare Alternatives
|
||||
</Link>
|
||||
|
||||
+807
-1685
File diff suppressed because it is too large
Load Diff
@@ -49,11 +49,11 @@ export default function PrivacyPage() {
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-green-500/30 bg-green-500/10 px-4 py-1.5 text-sm font-medium text-green-600 dark:text-green-400">
|
||||
<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)]">
|
||||
<Shield className="h-4 w-4" />
|
||||
Privacy First
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Your Voice Stays on Your Computer
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -64,9 +64,9 @@ 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-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<div className="mb-4 inline-flex rounded-xl bg-green-100 p-3 dark:bg-green-900/30">
|
||||
<Lock className="h-6 w-6 text-green-600 dark:text-green-400" />
|
||||
<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>
|
||||
<h3 className="mb-2 text-xl font-semibold">100% Offline</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@@ -75,9 +75,9 @@ export default function PrivacyPage() {
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<div className="mb-4 inline-flex rounded-xl bg-blue-100 p-3 dark:bg-blue-900/30">
|
||||
<Server className="h-6 w-6 text-blue-600 dark:text-blue-400" />
|
||||
<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">
|
||||
<Server className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-semibold">No Cloud Processing</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@@ -86,9 +86,9 @@ export default function PrivacyPage() {
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<div className="mb-4 inline-flex rounded-xl bg-violet-100 p-3 dark:bg-violet-900/30">
|
||||
<Eye className="h-6 w-6 text-violet-600 dark:text-violet-400" />
|
||||
<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">
|
||||
<Eye className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-semibold">Open Source</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@@ -100,8 +100,8 @@ export default function PrivacyPage() {
|
||||
|
||||
{/* Detailed Privacy Sections */}
|
||||
<section className="space-y-8">
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-4 flex items-center gap-2 text-2xl font-bold">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Database className="h-5 w-5 text-primary" />
|
||||
Data Collection
|
||||
</h2>
|
||||
@@ -112,31 +112,31 @@ export default function PrivacyPage() {
|
||||
</p>
|
||||
<ul className="space-y-2">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
No voice recordings or audio data
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
No transcribed text
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
No usage analytics or telemetry
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
No device fingerprints
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
No IP addresses or location data
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-4 flex items-center gap-2 text-2xl font-bold">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Shield className="h-5 w-5 text-primary" />
|
||||
How We Process Your Voice
|
||||
</h2>
|
||||
@@ -160,33 +160,33 @@ export default function PrivacyPage() {
|
||||
discarded immediately after processing
|
||||
</li>
|
||||
</ol>
|
||||
<p className="mt-4 rounded-lg bg-amber-50 p-4 text-sm dark:bg-amber-900/20">
|
||||
<p className="mt-4 rounded-lg bg-muted p-4 text-sm dark:bg-muted">
|
||||
<strong>Note:</strong> No audio is ever written to disk or transmitted over the
|
||||
network.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-4 flex items-center gap-2 text-2xl font-bold">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Lock className="h-5 w-5 text-primary" />
|
||||
Configuration Data
|
||||
</h2>
|
||||
<div className="space-y-4 text-muted-foreground">
|
||||
<p>
|
||||
Vocalinux stores configuration locally at <code className="rounded bg-zinc-100 px-1.5 py-0.5 dark:bg-zinc-700">~/.config/vocalinux/</code>:
|
||||
Vocalinux stores configuration locally at <code className="rounded bg-muted px-1.5 py-0.5 bg-muted">~/.config/vocalinux/</code>:
|
||||
</p>
|
||||
<ul className="space-y-2">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Settings like preferred engine, model size, and language
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Custom keyboard shortcuts
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Window and tray preferences
|
||||
</li>
|
||||
</ul>
|
||||
@@ -199,41 +199,41 @@ export default function PrivacyPage() {
|
||||
</section>
|
||||
|
||||
{/* Comparison with Cloud Services */}
|
||||
<section className="mt-12 rounded-2xl border border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-6 text-2xl font-bold">Vocalinux vs Cloud Dictation Services</h2>
|
||||
<section className="mt-12 rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Vocalinux vs Cloud Dictation Services</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-200 dark:border-zinc-700">
|
||||
<tr className="border-b border-border">
|
||||
<th className="pb-3 text-sm font-semibold">Feature</th>
|
||||
<th className="pb-3 text-sm font-semibold">Vocalinux</th>
|
||||
<th className="pb-3 text-sm font-semibold">Cloud Services</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-sm text-muted-foreground">
|
||||
<tr className="border-b border-zinc-100 dark:border-zinc-700/70">
|
||||
<tr className="border-b border-border">
|
||||
<td className="py-3 font-medium text-foreground">Data leaves device</td>
|
||||
<td className="py-3 text-green-600 dark:text-green-400">Never</td>
|
||||
<td className="py-3 text-red-600 dark:text-red-400">Always</td>
|
||||
<td className="py-3 text-primary dark:text-[color:var(--terminal-fg)]">Never</td>
|
||||
<td className="py-3 text-primary">Always</td>
|
||||
</tr>
|
||||
<tr className="border-b border-zinc-100 dark:border-zinc-700/70">
|
||||
<tr className="border-b border-border">
|
||||
<td className="py-3 font-medium text-foreground">Internet required</td>
|
||||
<td className="py-3 text-green-600 dark:text-green-400">No</td>
|
||||
<td className="py-3 text-red-600 dark:text-red-400">Yes</td>
|
||||
<td className="py-3 text-primary dark:text-[color:var(--terminal-fg)]">No</td>
|
||||
<td className="py-3 text-primary">Yes</td>
|
||||
</tr>
|
||||
<tr className="border-b border-zinc-100 dark:border-zinc-700/70">
|
||||
<tr className="border-b border-border">
|
||||
<td className="py-3 font-medium text-foreground">Source code available</td>
|
||||
<td className="py-3 text-green-600 dark:text-green-400">Yes (GPL-3.0)</td>
|
||||
<td className="py-3 text-red-600 dark:text-red-400">No</td>
|
||||
<td className="py-3 text-primary dark:text-[color:var(--terminal-fg)]">Yes (GPL-3.0)</td>
|
||||
<td className="py-3 text-primary">No</td>
|
||||
</tr>
|
||||
<tr className="border-b border-zinc-100 dark:border-zinc-700/70">
|
||||
<tr className="border-b border-border">
|
||||
<td className="py-3 font-medium text-foreground">Voice recordings stored</td>
|
||||
<td className="py-3 text-green-600 dark:text-green-400">No</td>
|
||||
<td className="py-3 text-primary dark:text-[color:var(--terminal-fg)]">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-green-600 dark:text-green-400">Free forever</td>
|
||||
<td className="py-3 text-primary dark:text-[color:var(--terminal-fg)]">Free forever</td>
|
||||
<td className="py-3">Subscription</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -242,8 +242,8 @@ export default function PrivacyPage() {
|
||||
</section>
|
||||
|
||||
{/* Contact */}
|
||||
<section className="mt-12 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-4 text-xl font-bold">Questions About Privacy?</h2>
|
||||
<section className="mt-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 text-xl font-semibold">Questions About Privacy?</h2>
|
||||
<p className="mb-4 text-muted-foreground">
|
||||
If you have any questions about our privacy practices or want to verify our claims,
|
||||
please reach out:
|
||||
|
||||
@@ -109,7 +109,7 @@ export default function RemoteApiPage() {
|
||||
<Server className="h-4 w-4" />
|
||||
v0.12.0 Remote Engine
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Remote API Speech Recognition for Linux
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -144,9 +144,9 @@ export default function RemoteApiPage() {
|
||||
return (
|
||||
<article
|
||||
key={item.title}
|
||||
className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<span className="bg-primary/10 mb-4 inline-flex rounded-xl p-3">
|
||||
<span className="bg-primary/10 mb-4 inline-flex rounded-[12px] p-3">
|
||||
<Icon className="h-6 w-6 text-primary" />
|
||||
</span>
|
||||
<h2 className="mb-2 text-xl font-semibold">{item.title}</h2>
|
||||
@@ -158,21 +158,21 @@ export default function RemoteApiPage() {
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-5 text-2xl font-bold">
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-5 font-display text-2xl font-semibold">
|
||||
Supported Remote API Formats
|
||||
</h2>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{protocolOptions.map((option) => (
|
||||
<article
|
||||
key={option.endpoint}
|
||||
className="rounded-xl border border-zinc-200 bg-zinc-50 p-5 dark:border-zinc-700 dark:bg-zinc-900/60"
|
||||
className="rounded-[12px] border border-border bg-muted p-5"
|
||||
>
|
||||
<h3 className="mb-2 text-lg font-semibold">{option.name}</h3>
|
||||
<p className="mb-3 text-sm text-muted-foreground">
|
||||
{option.description}
|
||||
</p>
|
||||
<code className="rounded-md bg-zinc-950 px-2.5 py-1.5 text-sm text-green-400">
|
||||
<code className="rounded-md bg-[color:var(--terminal)] px-2.5 py-1.5 text-sm text-[color:var(--terminal-fg)]">
|
||||
{option.endpoint}
|
||||
</code>
|
||||
</article>
|
||||
@@ -181,15 +181,15 @@ export default function RemoteApiPage() {
|
||||
</section>
|
||||
|
||||
<section className="mb-12 grid gap-6 lg:grid-cols-[1fr_1.1fr]">
|
||||
<div className="rounded-2xl border border-zinc-200 bg-zinc-50 p-6 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 flex items-center gap-2 text-2xl font-bold">
|
||||
<Terminal className="h-5 w-5 text-green-500" />
|
||||
<div className="rounded-[12px] border border-border bg-muted p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Terminal className="h-5 w-5 text-primary" />
|
||||
How the Request Flows
|
||||
</h2>
|
||||
<ol className="space-y-3 text-sm text-muted-foreground">
|
||||
{workflowSteps.map((step, index) => (
|
||||
<li key={step} className="flex gap-3">
|
||||
<span className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground dark:text-black">
|
||||
<span className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-primary text-xs font-semibold text-primary-foreground">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span>{step}</span>
|
||||
@@ -198,22 +198,22 @@ export default function RemoteApiPage() {
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-4 flex items-center gap-2 text-2xl font-bold">
|
||||
<Lock className="h-5 w-5 text-blue-500" />
|
||||
<div className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Lock className="h-5 w-5 text-primary" />
|
||||
Privacy and Security Notes
|
||||
</h2>
|
||||
<ul className="space-y-3 text-sm text-muted-foreground">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Audio is not written to disk before upload.
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Use HTTPS and an API key outside a trusted local network.
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 flex-shrink-0 text-amber-500" />
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Remote API is not the same as fully offline mode because audio is
|
||||
sent to the server you configure.
|
||||
</li>
|
||||
@@ -222,14 +222,14 @@ export default function RemoteApiPage() {
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-5 text-2xl font-bold">Remote API Setup Path</h2>
|
||||
<h2 className="mb-5 font-display text-2xl font-semibold">Remote API Setup Path</h2>
|
||||
<div className="grid gap-5 md:grid-cols-2">
|
||||
{setupSteps.map((step, index) => (
|
||||
<article
|
||||
key={step.title}
|
||||
className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<span className="mb-4 inline-flex h-8 w-8 items-center justify-center rounded-full bg-primary text-sm font-bold text-primary-foreground dark:text-black">
|
||||
<span className="mb-4 inline-flex h-8 w-8 items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground">
|
||||
{index + 1}
|
||||
</span>
|
||||
<h3 className="mb-2 text-xl font-semibold">{step.title}</h3>
|
||||
@@ -241,8 +241,8 @@ export default function RemoteApiPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border-primary/20 bg-primary/5 rounded-2xl border p-8">
|
||||
<h2 className="mb-4 text-2xl font-bold">Next Steps</h2>
|
||||
<section className="border-primary/20 bg-primary/5 rounded-[12px] border p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Next Steps</h2>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Link href="/advanced-settings/" className="group">
|
||||
<h3 className="font-semibold">Open advanced settings</h3>
|
||||
|
||||
@@ -10,32 +10,32 @@ const benefits = [
|
||||
description:
|
||||
"Give your hands, wrists, and fingers a break. Voice dictation lets you type without the physical toll of keyboard use.",
|
||||
icon: Heart,
|
||||
iconColor: "text-rose-500",
|
||||
iconBg: "bg-rose-100 dark:bg-rose-900/30",
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "Prevent Repetitive Stress",
|
||||
description:
|
||||
"RSI develops from thousands of repeated motions. Dictating breaks the cycle and allows recovery.",
|
||||
icon: Keyboard,
|
||||
iconColor: "text-amber-500",
|
||||
iconBg: "bg-amber-100 dark:bg-amber-900/30",
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "Privacy for Health",
|
||||
description:
|
||||
"Your health data stays private. All voice processing happens locally - no cloud uploads, no third parties.",
|
||||
icon: Shield,
|
||||
iconColor: "text-blue-500",
|
||||
iconBg: "bg-blue-100 dark:bg-blue-900/30",
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "Independence",
|
||||
description:
|
||||
"For those with mobility limitations, voice dictation provides computer independence without relying on others.",
|
||||
icon: User,
|
||||
iconColor: "text-emerald-500",
|
||||
iconBg: "bg-emerald-100 dark:bg-emerald-900/30",
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -137,11 +137,11 @@ export default function RsiPreventionPage() {
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-rose-500/30 bg-rose-500/10 px-4 py-1.5 text-sm font-medium text-rose-600 dark:text-rose-400">
|
||||
<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">
|
||||
<Heart className="h-4 w-4" />
|
||||
Health & Accessibility
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Voice Dictation for RSI Prevention
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -155,23 +155,23 @@ export default function RsiPreventionPage() {
|
||||
{statistics.map((stat) => (
|
||||
<div
|
||||
key={stat.label}
|
||||
className="rounded-xl border border-zinc-200 bg-white p-5 text-center dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-5 text-center"
|
||||
>
|
||||
<p className="mb-1 text-3xl font-bold text-primary">{stat.value}</p>
|
||||
<p className="mb-1 font-display text-3xl font-semibold text-primary">{stat.value}</p>
|
||||
<p className="text-sm text-muted-foreground">{stat.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 text-2xl font-bold">Why Voice Dictation Helps</h2>
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Why Voice Dictation Helps</h2>
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
{benefits.map((benefit) => {
|
||||
const Icon = benefit.icon;
|
||||
return (
|
||||
<article
|
||||
key={benefit.title}
|
||||
className="rounded-xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className={`rounded-lg p-2.5 ${benefit.iconBg}`}>
|
||||
@@ -187,17 +187,17 @@ export default function RsiPreventionPage() {
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 text-2xl font-bold">Conditions Helped by Voice Dictation</h2>
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Conditions Helped by Voice Dictation</h2>
|
||||
<div className="space-y-4">
|
||||
{conditions.map((condition) => (
|
||||
<article
|
||||
key={condition.name}
|
||||
className="rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<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-green-50 p-3 dark:bg-green-900/20">
|
||||
<p className="text-sm text-green-700 dark:text-green-400">
|
||||
<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)]">
|
||||
<strong>How dictation helps:</strong> {condition.howDictationHelps}
|
||||
</p>
|
||||
</div>
|
||||
@@ -206,26 +206,26 @@ export default function RsiPreventionPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-4 text-2xl font-bold">Getting Started with Hands-Free Typing</h2>
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Getting Started with Hands-Free Typing</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<h3 className="mb-3 font-semibold">Tips for Transitioning</h3>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Start by dictating emails and documents, then expand to other tasks
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Use voice commands for punctuation to maintain natural speech flow
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Take breaks - voice dictation is still work, and vocal rest matters too
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Combine with ergonomic equipment for maximum benefit
|
||||
</li>
|
||||
</ul>
|
||||
@@ -234,19 +234,19 @@ export default function RsiPreventionPage() {
|
||||
<h3 className="mb-3 font-semibold">Vocalinux Advantages</h3>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Toggle mode or push-to-talk control - no mouse required
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
System tray indicator shows recording status
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Pleasant audio feedback confirms start/stop
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Works in ALL Linux applications - universal compatibility
|
||||
</li>
|
||||
</ul>
|
||||
@@ -254,11 +254,11 @@ export default function RsiPreventionPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-rose-200 bg-rose-50 p-8 dark:border-rose-800 dark:bg-rose-900/20">
|
||||
<h2 className="mb-4 text-2xl font-bold text-rose-800 dark:text-rose-300">
|
||||
<section className="rounded-[12px] border border-border bg-primary/10 p-8 dark:border-border">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold text-primary">
|
||||
Take Care of Your Hands
|
||||
</h2>
|
||||
<p className="mb-6 text-rose-700 dark:text-rose-400">
|
||||
<p className="mb-6 text-primary">
|
||||
Your health matters. If you're experiencing pain, numbness, or tingling, consult a
|
||||
healthcare professional. Voice dictation is a tool to help, not a substitute for proper
|
||||
medical care.
|
||||
@@ -266,14 +266,14 @@ export default function RsiPreventionPage() {
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Link
|
||||
href="/install/"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground dark:text-black hover:bg-primary/90"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||
>
|
||||
Install Vocalinux
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/use-cases/"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-rose-300 bg-white px-4 py-2 text-sm font-semibold text-rose-700 hover:bg-rose-50 dark:border-rose-700 dark:bg-rose-900/50 dark:text-rose-300 dark:hover:bg-rose-900/70"
|
||||
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"
|
||||
>
|
||||
See Other Use Cases
|
||||
</Link>
|
||||
|
||||
@@ -145,7 +145,7 @@ export default function ScreenshotsPage() {
|
||||
<Camera className="h-4 w-4" />
|
||||
v0.14.0 Beta UI
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Vocalinux Screenshots
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -160,8 +160,8 @@ export default function ScreenshotsPage() {
|
||||
settingsShots={settingsShots}
|
||||
/>
|
||||
|
||||
<section className="border-primary/20 bg-primary/5 rounded-2xl border p-8">
|
||||
<h2 className="mb-3 text-2xl font-bold">Try it yourself</h2>
|
||||
<section className="border-primary/20 bg-primary/5 rounded-[12px] border p-8">
|
||||
<h2 className="mb-3 font-display text-2xl font-semibold">Try it yourself</h2>
|
||||
<p className="mb-6 max-w-2xl text-muted-foreground">
|
||||
Install Vocalinux and open Settings from the system tray to explore
|
||||
the same screens on your desktop.
|
||||
@@ -169,7 +169,7 @@ export default function ScreenshotsPage() {
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<Link
|
||||
href="/install/"
|
||||
className="hover:bg-primary/90 inline-flex items-center justify-center gap-2 rounded-xl bg-primary px-5 py-3 text-sm font-semibold text-primary-foreground dark:text-black"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-[12px] bg-primary px-5 py-3 text-sm font-semibold text-primary-foreground"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Install guide
|
||||
@@ -177,7 +177,7 @@ export default function ScreenshotsPage() {
|
||||
</Link>
|
||||
<Link
|
||||
href="/advanced-settings/"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-xl border border-zinc-300 bg-white px-5 py-3 text-sm font-semibold transition-colors hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-800 dark:hover:bg-zinc-700"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-[12px] border border-border bg-background px-5 py-3 text-sm font-semibold transition-colors hover:bg-muted hover:bg-muted"
|
||||
>
|
||||
Advanced settings guide
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
|
||||
@@ -8,7 +8,7 @@ const shortcutCategories = [
|
||||
{
|
||||
category: "Dictation Controls",
|
||||
icon: Mic,
|
||||
iconColor: "text-blue-500",
|
||||
iconColor: "text-primary",
|
||||
shortcuts: [
|
||||
{ keys: ["Ctrl", "Ctrl"], action: "Toggle mode: start/stop dictation", note: "Default mode" },
|
||||
{ keys: ["Hold Ctrl"], action: "Push-to-talk mode: speak while held", note: "Release to stop" },
|
||||
@@ -27,7 +27,7 @@ const shortcutCategories = [
|
||||
{
|
||||
category: "Voice Commands",
|
||||
icon: Zap,
|
||||
iconColor: "text-amber-500",
|
||||
iconColor: "text-primary",
|
||||
shortcuts: [
|
||||
{ keys: ["\"new line\""], action: "Insert a new line", note: "Say this phrase" },
|
||||
{ keys: ["\"new paragraph\""], action: "Insert a new paragraph", note: "Say this phrase" },
|
||||
@@ -48,7 +48,7 @@ const shortcutCategories = [
|
||||
{
|
||||
category: "Audio Feedback",
|
||||
icon: Volume2,
|
||||
iconColor: "text-sky-500",
|
||||
iconColor: "text-primary",
|
||||
shortcuts: [
|
||||
{
|
||||
keys: ["Settings Toggle"],
|
||||
@@ -65,7 +65,7 @@ const shortcutCategories = [
|
||||
{
|
||||
category: "Tray Icon Actions",
|
||||
icon: Mouse,
|
||||
iconColor: "text-green-500",
|
||||
iconColor: "text-primary",
|
||||
shortcuts: [
|
||||
{ keys: ["Left Click"], action: "Toggle dictation", note: "On tray icon" },
|
||||
{ keys: ["Right Click"], action: "Open context menu", note: "Settings, quit, etc." },
|
||||
@@ -74,7 +74,7 @@ const shortcutCategories = [
|
||||
{
|
||||
category: "Configuration",
|
||||
icon: Settings,
|
||||
iconColor: "text-violet-500",
|
||||
iconColor: "text-primary",
|
||||
shortcuts: [
|
||||
{ keys: ["Config File"], action: "~/.config/vocalinux/config.json", note: "Manual editing" },
|
||||
],
|
||||
@@ -136,11 +136,11 @@ export default function ShortcutsPage() {
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-primary/30 bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<p className="subpage-kicker">
|
||||
<Keyboard className="h-4 w-4" />
|
||||
Quick Reference
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Keyboard Shortcuts & Voice Commands
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -150,15 +150,15 @@ export default function ShortcutsPage() {
|
||||
</section>
|
||||
|
||||
{/* Quick Start */}
|
||||
<section className="mb-12 rounded-2xl border border-primary/20 bg-primary/5 p-6">
|
||||
<h2 className="mb-4 text-xl font-bold">Quick Start</h2>
|
||||
<section className="mb-12 rounded-[12px] border border-primary/20 bg-primary/5 p-6">
|
||||
<h2 className="mb-4 text-xl font-semibold">Quick Start</h2>
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<kbd className="rounded-lg border border-zinc-300 bg-zinc-100 px-3 py-1.5 font-mono text-sm font-semibold dark:border-zinc-600 dark:bg-zinc-800">
|
||||
<kbd className="rounded-lg border border-border bg-muted px-3 py-1.5 font-mono text-sm font-semibold">
|
||||
Ctrl
|
||||
</kbd>
|
||||
<span className="text-muted-foreground">+</span>
|
||||
<kbd className="rounded-lg border border-zinc-300 bg-zinc-100 px-3 py-1.5 font-mono text-sm font-semibold dark:border-zinc-600 dark:bg-zinc-800">
|
||||
<kbd className="rounded-lg border border-border bg-muted px-3 py-1.5 font-mono text-sm font-semibold">
|
||||
Ctrl
|
||||
</kbd>
|
||||
</div>
|
||||
@@ -181,10 +181,10 @@ export default function ShortcutsPage() {
|
||||
return (
|
||||
<article
|
||||
key={category.category}
|
||||
className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<h2 className="mb-6 flex items-center gap-3 text-2xl font-bold">
|
||||
<span className={`rounded-xl bg-zinc-100 p-2 dark:bg-zinc-700`}>
|
||||
<h2 className="mb-6 flex items-center gap-3 font-display text-2xl font-semibold">
|
||||
<span className={`rounded-[12px] bg-muted p-2 bg-muted`}>
|
||||
<Icon className={`h-5 w-5 ${category.iconColor}`} />
|
||||
</span>
|
||||
{category.category}
|
||||
@@ -194,12 +194,12 @@ export default function ShortcutsPage() {
|
||||
{category.shortcuts.map((shortcut, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-wrap items-center justify-between gap-4 rounded-xl bg-zinc-50 p-4 dark:bg-zinc-900/50"
|
||||
className="flex flex-wrap items-center justify-between gap-4 rounded-[12px] bg-muted p-4"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{shortcut.keys.map((key, keyIndex) => (
|
||||
<span key={keyIndex} className="flex items-center gap-2">
|
||||
<kbd className="rounded-lg border border-zinc-300 bg-zinc-100 px-3 py-1.5 font-mono text-sm font-semibold shadow-sm dark:border-zinc-600 dark:bg-zinc-800">
|
||||
<kbd className="rounded-lg border border-border bg-muted px-3 py-1.5 font-mono text-sm font-semibold">
|
||||
{key}
|
||||
</kbd>
|
||||
{keyIndex < shortcut.keys.length - 1 && (
|
||||
@@ -221,14 +221,14 @@ export default function ShortcutsPage() {
|
||||
</section>
|
||||
|
||||
{/* Customization */}
|
||||
<section className="mt-12 rounded-2xl border border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 text-2xl font-bold">Customizing Shortcuts</h2>
|
||||
<section className="mt-12 rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Customizing Shortcuts</h2>
|
||||
<p className="mb-4 text-muted-foreground">
|
||||
You can customize the activation shortcut in the settings panel or by editing the
|
||||
configuration file directly:
|
||||
</p>
|
||||
<div className="rounded-lg bg-zinc-950 p-4">
|
||||
<code className="text-sm text-green-400">~/.config/vocalinux/config.json</code>
|
||||
<div className="rounded-lg bg-[color:var(--terminal)] p-4">
|
||||
<code className="text-sm text-[color:var(--terminal-fg)]">~/.config/vocalinux/config.json</code>
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
Open Vocalinux and access Settings from the tray icon to change keyboard shortcuts
|
||||
@@ -240,7 +240,7 @@ export default function ShortcutsPage() {
|
||||
<section className="mt-12 grid gap-6 md:grid-cols-2">
|
||||
<Link
|
||||
href="/faq/"
|
||||
className="group rounded-2xl border border-zinc-200 bg-white p-6 transition-all hover:-translate-y-1 hover:shadow-lg dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="group rounded-[12px] border border-border bg-background p-6 transition-all"
|
||||
>
|
||||
<h3 className="mb-2 text-xl font-semibold">Frequently Asked Questions</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@@ -254,7 +254,7 @@ export default function ShortcutsPage() {
|
||||
|
||||
<Link
|
||||
href="/troubleshooting/"
|
||||
className="group rounded-2xl border border-zinc-200 bg-white p-6 transition-all hover:-translate-y-1 hover:shadow-lg dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="group rounded-[12px] border border-border bg-background p-6 transition-all"
|
||||
>
|
||||
<h3 className="mb-2 text-xl font-semibold">Troubleshooting</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
|
||||
@@ -212,11 +212,11 @@ export default function TroubleshootingPage() {
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-amber-500/30 bg-amber-500/10 px-4 py-1.5 text-sm font-medium text-amber-600 dark:text-amber-400">
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-border bg-muted px-4 py-1.5 text-sm font-medium text-muted-foreground">
|
||||
<Wrench className="h-4 w-4" />
|
||||
Support Guide
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Troubleshooting Guide
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -225,16 +225,16 @@ export default function TroubleshootingPage() {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-4 flex items-center gap-2 text-xl font-bold">
|
||||
<Terminal className="h-5 w-5 text-green-500" />
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 text-xl font-semibold">
|
||||
<Terminal className="h-5 w-5 text-primary" />
|
||||
Quick Debug Command
|
||||
</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Run Vocalinux with debug output to see detailed logs:
|
||||
</p>
|
||||
<div className="rounded-lg bg-zinc-950 p-4">
|
||||
<code className="text-sm text-green-400">vocalinux --debug</code>
|
||||
<div className="rounded-lg bg-[color:var(--terminal)] p-4">
|
||||
<code className="text-sm text-[color:var(--terminal-fg)]">vocalinux --debug</code>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -242,16 +242,16 @@ export default function TroubleshootingPage() {
|
||||
{troubleshootingItems.map((item) => (
|
||||
<article
|
||||
key={item.title}
|
||||
className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<h2 className="mb-4 text-xl font-bold">{item.title}</h2>
|
||||
<h2 className="mb-4 text-xl font-semibold">{item.title}</h2>
|
||||
|
||||
<div className="mb-4 rounded-lg bg-amber-50 p-4 dark:bg-amber-900/20">
|
||||
<p className="mb-2 flex items-center gap-2 text-sm font-semibold text-amber-700 dark:text-amber-400">
|
||||
<div className="mb-4 rounded-lg bg-muted p-4 dark:bg-muted">
|
||||
<p className="mb-2 flex items-center gap-2 text-sm font-semibold text-muted-foreground">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
Symptoms
|
||||
</p>
|
||||
<ul className="ml-6 list-disc text-sm text-amber-600 dark:text-amber-300">
|
||||
<ul className="ml-6 list-disc text-sm text-muted-foreground">
|
||||
{item.symptoms.map((symptom) => (
|
||||
<li key={symptom} className="break-words">
|
||||
{symptom}
|
||||
@@ -260,12 +260,12 @@ export default function TroubleshootingPage() {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-green-50 p-4 dark:bg-green-900/20">
|
||||
<p className="mb-2 flex items-center gap-2 text-sm font-semibold text-green-700 dark:text-green-400">
|
||||
<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)]">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
Solutions
|
||||
</p>
|
||||
<ul className="ml-6 list-disc text-sm text-green-600 dark:text-green-300">
|
||||
<ul className="ml-6 list-disc text-sm text-primary">
|
||||
{item.solutions.map((solution) => (
|
||||
<li key={solution} className="break-words">
|
||||
{solution}
|
||||
@@ -277,9 +277,9 @@ export default function TroubleshootingPage() {
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="mt-12 rounded-2xl border border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 flex items-center gap-2 text-2xl font-bold">
|
||||
<Lightbulb className="h-5 w-5 text-amber-500" />
|
||||
<section className="mt-12 rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Lightbulb className="h-5 w-5 text-primary" />
|
||||
Still having issues?
|
||||
</h2>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
|
||||
@@ -8,8 +8,8 @@ const useCases = [
|
||||
{
|
||||
title: "Developers",
|
||||
icon: Code,
|
||||
iconColor: "text-blue-500",
|
||||
iconBg: "bg-blue-100 dark:bg-blue-900/30",
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
description: "Code faster with voice. Dictate comments, documentation, and even code snippets hands-free.",
|
||||
benefits: [
|
||||
"Dictate code comments while keeping hands on the keyboard",
|
||||
@@ -22,8 +22,8 @@ const useCases = [
|
||||
{
|
||||
title: "Writers & Authors",
|
||||
icon: Feather,
|
||||
iconColor: "text-violet-500",
|
||||
iconBg: "bg-violet-100 dark:bg-violet-900/30",
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
description: "Capture ideas as fast as you can think them. Perfect for drafts, articles, and creative writing.",
|
||||
benefits: [
|
||||
"Write at natural speaking speed (100+ WPM)",
|
||||
@@ -36,8 +36,8 @@ const useCases = [
|
||||
{
|
||||
title: "Students",
|
||||
icon: GraduationCap,
|
||||
iconColor: "text-emerald-500",
|
||||
iconBg: "bg-emerald-100 dark:bg-emerald-900/30",
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
description: "Take notes during lectures, transcribe recordings, and write essays faster.",
|
||||
benefits: [
|
||||
"Take lecture notes at speaking speed",
|
||||
@@ -50,8 +50,8 @@ const useCases = [
|
||||
{
|
||||
title: "Accessibility Users",
|
||||
icon: Accessibility,
|
||||
iconColor: "text-rose-500",
|
||||
iconBg: "bg-rose-100 dark:bg-rose-900/30",
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
description: "Full voice control for users with mobility impairments, RSI, or carpal tunnel.",
|
||||
benefits: [
|
||||
"Type without using hands",
|
||||
@@ -64,8 +64,8 @@ const useCases = [
|
||||
{
|
||||
title: "Professionals",
|
||||
icon: Briefcase,
|
||||
iconColor: "text-amber-500",
|
||||
iconBg: "bg-amber-100 dark:bg-amber-900/30",
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
description: "Dictate emails, reports, and meeting notes. Boost productivity in any profession.",
|
||||
benefits: [
|
||||
"Draft emails quickly with natural speech",
|
||||
@@ -78,8 +78,8 @@ const useCases = [
|
||||
{
|
||||
title: "Content Creators",
|
||||
icon: Heart,
|
||||
iconColor: "text-pink-500",
|
||||
iconBg: "bg-pink-100 dark:bg-pink-900/30",
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
description: "Create YouTube scripts, podcast notes, and social media content at speaking speed.",
|
||||
benefits: [
|
||||
"Write scripts as fast as you can speak",
|
||||
@@ -136,11 +136,11 @@ export default function UseCasesPage() {
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-primary/30 bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<p className="subpage-kicker">
|
||||
<Users className="h-4 w-4" />
|
||||
For Everyone
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Voice Dictation for Every Workflow
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -155,14 +155,14 @@ export default function UseCasesPage() {
|
||||
return (
|
||||
<article
|
||||
key={useCase.title}
|
||||
className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<div className="mb-6 flex flex-wrap items-start gap-4">
|
||||
<div className={`rounded-xl p-3 ${useCase.iconBg}`}>
|
||||
<div className={`rounded-[12px] p-3 ${useCase.iconBg}`}>
|
||||
<Icon className={`h-6 w-6 ${useCase.iconColor}`} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="mb-2 text-2xl font-bold">{useCase.title}</h2>
|
||||
<h2 className="mb-2 font-display text-2xl font-semibold">{useCase.title}</h2>
|
||||
<p className="text-muted-foreground">{useCase.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -186,7 +186,7 @@ export default function UseCasesPage() {
|
||||
{useCase.commands.map((cmd) => (
|
||||
<kbd
|
||||
key={cmd}
|
||||
className="rounded-lg border border-zinc-300 bg-zinc-100 px-2 py-1 text-xs font-mono dark:border-zinc-600 dark:bg-zinc-900"
|
||||
className="rounded-lg border border-border bg-muted px-2 py-1 text-xs font-mono"
|
||||
>
|
||||
"{cmd}"
|
||||
</kbd>
|
||||
@@ -199,22 +199,22 @@ export default function UseCasesPage() {
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="mt-12 rounded-2xl border border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 text-2xl font-bold">Ready to Get Started?</h2>
|
||||
<section className="mt-12 rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Ready to Get Started?</h2>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
Install Vocalinux in minutes and start dictating in any application on Linux.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Link
|
||||
href="/install/"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground dark:text-black hover:bg-primary/90"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||
>
|
||||
Install Now
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/shortcuts/"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-zinc-300 bg-white px-4 py-2 text-sm font-semibold hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-800 dark:hover:bg-zinc-700"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted hover:bg-muted"
|
||||
>
|
||||
View Voice Commands
|
||||
</Link>
|
||||
|
||||
@@ -88,7 +88,7 @@ export default function VoiceActivityDetectionPage() {
|
||||
<Activity className="h-4 w-4" />
|
||||
v0.12.0 Silero VAD
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Silero VAD for Cleaner Linux Voice Dictation
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -103,10 +103,10 @@ export default function VoiceActivityDetectionPage() {
|
||||
{vadBenefits.map((benefit) => (
|
||||
<article
|
||||
key={benefit.title}
|
||||
className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<span className="mb-4 inline-flex rounded-xl bg-green-500/10 p-3">
|
||||
<CheckCircle2 className="h-6 w-6 text-green-500" />
|
||||
<span className="mb-4 inline-flex rounded-[12px] bg-primary/10 p-3">
|
||||
<CheckCircle2 className="h-6 w-6 text-primary" />
|
||||
</span>
|
||||
<h2 className="mb-2 text-xl font-semibold">{benefit.title}</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@@ -117,15 +117,15 @@ export default function VoiceActivityDetectionPage() {
|
||||
</section>
|
||||
|
||||
<section className="mb-12 grid gap-6 lg:grid-cols-[1.05fr_1fr]">
|
||||
<div className="rounded-2xl border border-zinc-200 bg-zinc-50 p-6 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 flex items-center gap-2 text-2xl font-bold">
|
||||
<div className="rounded-[12px] border border-border bg-muted p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Volume2 className="h-5 w-5 text-primary" />
|
||||
How Speech Detection Fits In
|
||||
</h2>
|
||||
<ol className="space-y-3 text-sm text-muted-foreground">
|
||||
{signalFlow.map((step, index) => (
|
||||
<li key={step} className="flex gap-3">
|
||||
<span className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground dark:text-black">
|
||||
<span className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-primary text-xs font-semibold text-primary-foreground">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span>{step}</span>
|
||||
@@ -134,9 +134,9 @@ export default function VoiceActivityDetectionPage() {
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-4 flex items-center gap-2 text-2xl font-bold">
|
||||
<Settings className="h-5 w-5 text-blue-500" />
|
||||
<div className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Settings className="h-5 w-5 text-primary" />
|
||||
Settings and Installation
|
||||
</h2>
|
||||
<div className="space-y-4 text-sm text-muted-foreground">
|
||||
@@ -145,8 +145,8 @@ export default function VoiceActivityDetectionPage() {
|
||||
ONNX Runtime is not available, Vocalinux logs the fallback and
|
||||
continues with amplitude-based VAD.
|
||||
</p>
|
||||
<div className="rounded-lg bg-zinc-950 p-4">
|
||||
<code className="text-green-400">
|
||||
<div className="rounded-lg bg-[color:var(--terminal)] p-4">
|
||||
<code className="text-[color:var(--terminal-fg)]">
|
||||
pip install "vocalinux[vad]"
|
||||
</code>
|
||||
</div>
|
||||
@@ -184,9 +184,9 @@ export default function VoiceActivityDetectionPage() {
|
||||
return (
|
||||
<article
|
||||
key={item.title}
|
||||
className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<span className="bg-primary/10 mb-4 inline-flex rounded-xl p-3">
|
||||
<span className="bg-primary/10 mb-4 inline-flex rounded-[12px] p-3">
|
||||
<Icon className="h-6 w-6 text-primary" />
|
||||
</span>
|
||||
<h2 className="mb-2 text-xl font-semibold">{item.title}</h2>
|
||||
@@ -198,8 +198,8 @@ export default function VoiceActivityDetectionPage() {
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="border-primary/20 bg-primary/5 rounded-2xl border p-8">
|
||||
<h2 className="mb-4 text-2xl font-bold">Related Guides</h2>
|
||||
<section className="border-primary/20 bg-primary/5 rounded-[12px] border p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Related Guides</h2>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Link href="/shortcuts/" className="group">
|
||||
<h3 className="font-semibold">Shortcut modes</h3>
|
||||
|
||||
@@ -44,8 +44,8 @@ const workflows = [
|
||||
"Great when context-switching between keyboard and voice.",
|
||||
],
|
||||
icon: ToggleLeft,
|
||||
iconColor: "text-blue-500",
|
||||
iconBg: "bg-blue-100 dark:bg-blue-900/30",
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "Push-to-talk for continuous coding (recommended)",
|
||||
@@ -57,8 +57,8 @@ const workflows = [
|
||||
"Preferred by many developers for precise control.",
|
||||
],
|
||||
icon: Mic,
|
||||
iconColor: "text-emerald-500",
|
||||
iconBg: "bg-emerald-100 dark:bg-emerald-900/30",
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "Voice commands for code editing",
|
||||
@@ -70,8 +70,8 @@ const workflows = [
|
||||
"Use keyboard for dense syntax-heavy sections.",
|
||||
],
|
||||
icon: MessageSquare,
|
||||
iconColor: "text-purple-500",
|
||||
iconBg: "bg-purple-100 dark:bg-purple-900/30",
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -135,11 +135,11 @@ export default function VoiceTypingVsCodePage() {
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-primary/30 bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<p className="subpage-kicker">
|
||||
<Code2 className="h-4 w-4" />
|
||||
VS Code Workflow
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
How to Dictate in VS Code on Linux (Voice Coding Workflow)
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -148,9 +148,9 @@ export default function VoiceTypingVsCodePage() {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-3 flex items-center gap-2 text-2xl font-bold">
|
||||
<Lightbulb className="h-5 w-5 text-amber-500" />
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 flex items-center gap-2 font-display text-2xl font-semibold">
|
||||
<Lightbulb className="h-5 w-5 text-primary" />
|
||||
Why use voice coding in VS Code?
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
@@ -161,14 +161,14 @@ export default function VoiceTypingVsCodePage() {
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 text-2xl font-bold">Setup in 2 steps</h2>
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Setup in 2 steps</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{setupSteps.map((step) => {
|
||||
const Icon = step.icon;
|
||||
return (
|
||||
<article
|
||||
key={step.title}
|
||||
className="rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<div className="mb-3 inline-flex rounded-lg bg-primary/10 p-2">
|
||||
<Icon className="h-4 w-4 text-primary" />
|
||||
@@ -185,7 +185,7 @@ export default function VoiceTypingVsCodePage() {
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 text-2xl font-bold">Recommended workflows</h2>
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Recommended workflows</h2>
|
||||
<div className="space-y-4">
|
||||
{workflows.map((workflow) => {
|
||||
const Icon = workflow.icon;
|
||||
@@ -193,7 +193,7 @@ export default function VoiceTypingVsCodePage() {
|
||||
return (
|
||||
<article
|
||||
key={workflow.title}
|
||||
className="rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<h3 className="mb-2 flex items-center gap-3 text-lg font-semibold">
|
||||
<span className={`rounded-lg p-2 ${workflow.iconBg}`}>
|
||||
@@ -216,8 +216,8 @@ export default function VoiceTypingVsCodePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-zinc-50 p-6 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 text-2xl font-bold">VS Code-specific tips</h2>
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-muted p-6">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">VS Code-specific tips</h2>
|
||||
<ul className="space-y-3 text-sm text-muted-foreground">
|
||||
{vscodeTips.map((tip) => (
|
||||
<li key={tip} className="flex items-start gap-2">
|
||||
@@ -228,12 +228,12 @@ export default function VoiceTypingVsCodePage() {
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-orange-200 bg-orange-50 p-6 dark:border-orange-800 dark:bg-orange-900/20">
|
||||
<h2 className="mb-4 flex items-center gap-2 text-2xl font-bold text-orange-800 dark:text-orange-300">
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-muted p-6 dark:border-border">
|
||||
<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
|
||||
</h2>
|
||||
<ul className="space-y-3 text-sm text-orange-700 dark:text-orange-300">
|
||||
<ul className="space-y-3 text-sm text-muted-foreground">
|
||||
{troubleshootingTips.map((tip) => (
|
||||
<li key={tip} className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
@@ -254,15 +254,15 @@ export default function VoiceTypingVsCodePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-primary/20 bg-primary/5 p-8">
|
||||
<h2 className="mb-4 text-2xl font-bold">Ready to voice-type in VS Code?</h2>
|
||||
<section className="rounded-[12px] border border-primary/20 bg-primary/5 p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Ready to voice-type in VS Code?</h2>
|
||||
<p className="mb-6 max-w-3xl text-muted-foreground">
|
||||
Install Vocalinux and use a hybrid keyboard + voice workflow that fits real-world Linux
|
||||
development.
|
||||
</p>
|
||||
<Link
|
||||
href="/install/"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground dark:text-black hover:bg-primary/90"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||
>
|
||||
Install Vocalinux
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
|
||||
@@ -110,11 +110,11 @@ export default function VsNerdDictationPage() {
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-primary/30 bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<p className="subpage-kicker">
|
||||
<Trophy className="h-4 w-4" />
|
||||
Vocalinux Comparison
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Vocalinux vs Nerd Dictation
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -124,11 +124,11 @@ export default function VsNerdDictationPage() {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="overflow-x-auto rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="px-4 pb-2 pt-5 text-2xl font-bold">Feature-by-feature comparison</h2>
|
||||
<section className="overflow-x-auto rounded-[12px] border border-border bg-background">
|
||||
<h2 className="px-4 pb-2 pt-5 font-display text-2xl font-semibold">Feature-by-feature comparison</h2>
|
||||
<table className="w-full border-collapse text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-200 dark:border-zinc-700">
|
||||
<tr className="border-b border-border">
|
||||
<th className="px-4 py-3 text-sm font-semibold">Feature</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">Vocalinux</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">Nerd Dictation</th>
|
||||
@@ -142,7 +142,7 @@ export default function VsNerdDictationPage() {
|
||||
return (
|
||||
<tr
|
||||
key={row.feature}
|
||||
className="border-b border-zinc-100 align-top dark:border-zinc-700/70"
|
||||
className="border-b border-border align-top"
|
||||
>
|
||||
<td className="px-4 py-4 font-semibold">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
@@ -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-green-600 dark:text-green-400">
|
||||
<td className="px-4 py-4 text-sm font-semibold text-primary dark:text-[color:var(--terminal-fg)]">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
{row.winner}
|
||||
@@ -167,8 +167,8 @@ export default function VsNerdDictationPage() {
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section className="mt-10 rounded-2xl border border-zinc-200 bg-zinc-50 p-6 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 text-2xl font-bold">Go deeper</h2>
|
||||
<section className="mt-10 rounded-[12px] border border-border bg-muted p-6">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Go deeper</h2>
|
||||
<div className="grid gap-3 text-sm text-muted-foreground sm:grid-cols-2 lg:grid-cols-3">
|
||||
<Link href="/offline/" className="font-semibold text-primary hover:underline">
|
||||
Why offline dictation matters
|
||||
@@ -191,8 +191,8 @@ export default function VsNerdDictationPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-10 rounded-2xl border border-primary/20 bg-primary/5 p-8">
|
||||
<h2 className="mb-4 text-2xl font-bold">Ready to switch from terminal-only dictation?</h2>
|
||||
<section className="mt-10 rounded-[12px] border border-primary/20 bg-primary/5 p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Ready to switch from terminal-only dictation?</h2>
|
||||
<p className="mb-6 max-w-3xl text-muted-foreground">
|
||||
Install Vocalinux for a native Linux voice dictation workflow with offline privacy, better
|
||||
usability, and stronger performance options.
|
||||
@@ -200,14 +200,14 @@ export default function VsNerdDictationPage() {
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Link
|
||||
href="/install/"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground dark:text-black hover:bg-primary/90"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||
>
|
||||
Install Vocalinux
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/compare/"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-zinc-300 bg-white px-4 py-2 text-sm font-semibold hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-800 dark:hover:bg-zinc-700"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted hover:bg-muted"
|
||||
>
|
||||
Engine comparison
|
||||
</Link>
|
||||
|
||||
@@ -158,7 +158,7 @@ export default function WaylandPage() {
|
||||
<Monitor className="h-4 w-4" />
|
||||
Wayland Support
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Voice Dictation on Wayland
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -173,7 +173,7 @@ export default function WaylandPage() {
|
||||
return (
|
||||
<div
|
||||
key={feature.title}
|
||||
className="rounded-xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<Icon className="mb-3 h-8 w-8 text-primary" />
|
||||
<h3 className="mb-2 font-semibold">{feature.title}</h3>
|
||||
@@ -185,14 +185,14 @@ export default function WaylandPage() {
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-6 text-2xl font-bold">
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">
|
||||
Supported Wayland Compositors
|
||||
</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-200 dark:border-zinc-700">
|
||||
<tr className="border-b border-border">
|
||||
<th className="pb-3 pr-4 text-sm font-semibold">Compositor</th>
|
||||
<th className="pb-3 pr-4 text-sm font-semibold">Method</th>
|
||||
<th className="pb-3 pr-4 text-sm font-semibold">Status</th>
|
||||
@@ -203,14 +203,14 @@ export default function WaylandPage() {
|
||||
{compositorSupport.map((item) => (
|
||||
<tr
|
||||
key={item.name}
|
||||
className="border-b border-zinc-100 dark:border-zinc-700/70"
|
||||
className="border-b border-border"
|
||||
>
|
||||
<td className="py-3 pr-4 font-medium">{item.name}</td>
|
||||
<td className="py-3 pr-4 text-muted-foreground">
|
||||
{item.method}
|
||||
</td>
|
||||
<td className="py-3 pr-4">
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700 dark:bg-green-900/30 dark:text-green-400">
|
||||
<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)]">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
{item.status}
|
||||
</span>
|
||||
@@ -224,15 +224,15 @@ export default function WaylandPage() {
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 text-2xl font-bold">Quick Setup Guide</h2>
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Quick Setup Guide</h2>
|
||||
<div className="space-y-4">
|
||||
{setupSteps.map((step) => (
|
||||
<div
|
||||
key={step.step}
|
||||
className="rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<div className="mb-2 flex items-center gap-3">
|
||||
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-primary text-sm font-bold text-primary-foreground">
|
||||
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground">
|
||||
{step.step}
|
||||
</span>
|
||||
<h3 className="font-semibold">{step.title}</h3>
|
||||
@@ -240,8 +240,8 @@ export default function WaylandPage() {
|
||||
<p className="mb-3 ml-10 text-sm text-muted-foreground">
|
||||
{step.description}
|
||||
</p>
|
||||
<div className="ml-10 rounded-lg bg-zinc-950 p-3">
|
||||
<code className="text-sm text-green-400">{step.command}</code>
|
||||
<div className="ml-10 rounded-lg bg-[color:var(--terminal)] p-3">
|
||||
<code className="text-sm text-[color:var(--terminal-fg)]">{step.command}</code>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -252,16 +252,16 @@ export default function WaylandPage() {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-blue-200 bg-blue-50 p-6 dark:border-blue-800 dark:bg-blue-900/20">
|
||||
<h2 className="mb-3 text-xl font-bold text-blue-800 dark:text-blue-300">
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-primary/5 p-6">
|
||||
<h2 className="mb-3 text-xl font-semibold text-foreground">
|
||||
How Wayland Text Injection Works
|
||||
</h2>
|
||||
<p className="mb-4 text-sm text-blue-700 dark:text-blue-400">
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Unlike X11 where applications can simulate keyboard input globally,
|
||||
Wayland's security model requires a different approach. Vocalinux
|
||||
uses:
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-blue-700 dark:text-blue-400">
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
<strong>IBus Engine</strong> - A custom IBus input method that
|
||||
@@ -278,20 +278,20 @@ export default function WaylandPage() {
|
||||
<strong>X11 Fallback</strong> - xdotool for XWayland applications
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mt-4 text-sm text-blue-700 dark:text-blue-400">
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
v0.9.0+ includes clipboard fallback for Wayland compositors without
|
||||
virtual keyboard support. Copy-to-clipboard is now disabled by default
|
||||
for privacy - enable in Settings if needed.
|
||||
</p>
|
||||
<p className="mt-3 text-sm text-blue-700 dark:text-blue-400">
|
||||
<p className="mt-3 text-sm text-muted-foreground">
|
||||
v0.10.2+ also improves IBus detection without legacy environment
|
||||
variables and handles non-ASCII ydotool paths with clipboard paste
|
||||
fallback.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-700 dark:bg-zinc-900/60">
|
||||
<h2 className="mb-4 text-2xl font-bold">Start Dictating on Wayland</h2>
|
||||
<section className="rounded-[12px] border border-border bg-muted p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Start Dictating on Wayland</h2>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
Vocalinux automatically detects your display server and configures the
|
||||
appropriate text injection method. Just install and run.
|
||||
@@ -299,20 +299,20 @@ export default function WaylandPage() {
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Link
|
||||
href="/install/"
|
||||
className="hover:bg-primary/90 inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground dark:text-black"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||
>
|
||||
Install Now
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/troubleshooting/"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-zinc-300 bg-white px-4 py-2 text-sm font-semibold hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-800 dark:hover:bg-zinc-700"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted hover:bg-muted"
|
||||
>
|
||||
Troubleshooting Guide
|
||||
</Link>
|
||||
<Link
|
||||
href="/desktop-reliability/"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-zinc-300 bg-white px-4 py-2 text-sm font-semibold hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-800 dark:hover:bg-zinc-700"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-semibold hover:bg-muted hover:bg-muted"
|
||||
>
|
||||
Reliability Improvements
|
||||
</Link>
|
||||
|
||||
@@ -106,11 +106,11 @@ export default function WhisperModelGuidePage() {
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-primary/30 bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<p className="subpage-kicker">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
Whisper Model Guide
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Whisper Model Sizes Explained: Tiny, Base, Small, Medium, Large for Linux
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -120,10 +120,10 @@ export default function WhisperModelGuidePage() {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="overflow-x-auto rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<section className="overflow-x-auto rounded-[12px] border border-border bg-background">
|
||||
<table className="w-full border-collapse text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-200 dark:border-zinc-700">
|
||||
<tr className="border-b border-border">
|
||||
<th className="px-4 py-3 text-sm font-semibold">Model</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">Size</th>
|
||||
<th className="px-4 py-3 text-sm font-semibold">RAM (CPU)</th>
|
||||
@@ -137,7 +137,7 @@ export default function WhisperModelGuidePage() {
|
||||
{modelComparison.map((row) => (
|
||||
<tr
|
||||
key={row.model}
|
||||
className="border-b border-zinc-100 align-top dark:border-zinc-700/70"
|
||||
className="border-b border-border align-top"
|
||||
>
|
||||
<td className="px-4 py-4 font-semibold">{row.model}</td>
|
||||
<td className="px-4 py-4 text-sm text-muted-foreground">{row.size}</td>
|
||||
@@ -153,71 +153,71 @@ export default function WhisperModelGuidePage() {
|
||||
</section>
|
||||
|
||||
<section className="mt-12">
|
||||
<h2 className="mb-6 text-2xl font-bold">Which model should you choose?</h2>
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Which model should you choose?</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h3 className="mb-3 inline-flex items-center gap-2 text-xl font-semibold">
|
||||
<Zap className="h-5 w-5 text-amber-500" />
|
||||
<Zap className="h-5 w-5 text-primary" />
|
||||
Tiny or Base
|
||||
</h3>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Best if you care about low latency over absolute transcription quality.
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Great for older CPUs, battery-conscious laptops, and quick chat replies.
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h3 className="mb-3 inline-flex items-center gap-2 text-xl font-semibold">
|
||||
<Gauge className="h-5 w-5 text-blue-500" />
|
||||
<Gauge className="h-5 w-5 text-primary" />
|
||||
Small (recommended default)
|
||||
</h3>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Best balance for most Linux dictation workflows.
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Better punctuation and proper nouns while staying near real-time.
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h3 className="mb-3 inline-flex items-center gap-2 text-xl font-semibold">
|
||||
<Cpu className="h-5 w-5 text-violet-500" />
|
||||
<Cpu className="h-5 w-5 text-primary" />
|
||||
Medium
|
||||
</h3>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Use when accuracy is more important than response time.
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Ideal for technical dictation, multilingual speech, and noisy environments.
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h3 className="mb-3 inline-flex items-center gap-2 text-xl font-semibold">
|
||||
<Rocket className="h-5 w-5 text-rose-500" />
|
||||
<Rocket className="h-5 w-5 text-primary" />
|
||||
Large
|
||||
</h3>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Choose only if you have strong GPU hardware and want maximum quality.
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
Best for polished long-form writing where errors are costly.
|
||||
</li>
|
||||
</ul>
|
||||
@@ -226,7 +226,7 @@ export default function WhisperModelGuidePage() {
|
||||
</section>
|
||||
|
||||
<section className="mt-12 grid gap-6 md:grid-cols-3">
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 text-xl font-semibold">Need more speed?</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Enable GPU acceleration to run larger models faster, especially medium and large.
|
||||
@@ -236,7 +236,7 @@ export default function WhisperModelGuidePage() {
|
||||
</Link>
|
||||
</article>
|
||||
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 text-xl font-semibold">Comparing engines?</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
See whisper.cpp vs Whisper vs VOSK before deciding your full dictation stack.
|
||||
@@ -246,7 +246,7 @@ export default function WhisperModelGuidePage() {
|
||||
</Link>
|
||||
</article>
|
||||
|
||||
<article className="rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<article className="rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-3 text-xl font-semibold">Ready to install?</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Use the install guide to get Vocalinux running with the model size that fits your hardware.
|
||||
@@ -257,8 +257,8 @@ export default function WhisperModelGuidePage() {
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="mt-12 rounded-2xl border border-primary/30 bg-primary/10 p-8">
|
||||
<h2 className="mb-4 text-2xl font-bold">Install Vocalinux and start dictating today</h2>
|
||||
<section className="mt-12 rounded-[12px] border border-primary/30 bg-primary/10 p-8">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Install Vocalinux and start dictating today</h2>
|
||||
<p className="mb-6 max-w-3xl text-muted-foreground">
|
||||
Start with <strong>small</strong> if unsure, then move up or down after a day of use.
|
||||
Vocalinux makes model changes simple, so you can tune for your exact speed/accuracy
|
||||
@@ -266,7 +266,7 @@ export default function WhisperModelGuidePage() {
|
||||
</p>
|
||||
<Link
|
||||
href="/install/"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground dark:text-black hover:bg-primary/90"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||
>
|
||||
Install Vocalinux
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
|
||||
@@ -20,16 +20,16 @@ const writingModes = [
|
||||
description:
|
||||
"Use toggle mode for long-form drafting. Start dictation, speak continuously, and stay in creative flow while writing chapters, essays, and long notes.",
|
||||
icon: ToggleLeft,
|
||||
iconColor: "text-violet-500",
|
||||
iconBg: "bg-violet-100 dark:bg-violet-900/30",
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
title: "Push-to-talk for structured writing",
|
||||
description:
|
||||
"Use push-to-talk when precision matters: outlines, headlines, bullet lists, or source notes. Hold to speak, release to review and edit.",
|
||||
icon: Mic,
|
||||
iconColor: "text-blue-500",
|
||||
iconBg: "bg-blue-100 dark:bg-blue-900/30",
|
||||
iconColor: "text-primary",
|
||||
iconBg: "bg-primary/10",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -115,11 +115,11 @@ export default function WritersPage() {
|
||||
/>
|
||||
|
||||
<section>
|
||||
<p className="mb-4 inline-flex items-center gap-2 rounded-full border border-primary/30 bg-primary/10 px-4 py-1.5 text-sm font-medium text-primary">
|
||||
<p className="subpage-kicker">
|
||||
<PenLine className="h-4 w-4" />
|
||||
For Writers
|
||||
</p>
|
||||
<h1 className="mb-5 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
||||
<h1 className="mb-5 font-display text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Voice Dictation for Writers on Linux: Dictate Novels, Articles & Notes
|
||||
</h1>
|
||||
<p className="mb-8 max-w-4xl text-lg text-muted-foreground">
|
||||
@@ -128,12 +128,12 @@ export default function WritersPage() {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-4 text-2xl font-bold">Why Writers Should Use Voice Dictation</h2>
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Why Writers Should Use Voice Dictation</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<h3 className="mb-3 inline-flex items-center gap-2 font-semibold">
|
||||
<Heart className="h-5 w-5 text-rose-500" /> RSI prevention and comfort
|
||||
<Heart className="h-5 w-5 text-primary" /> RSI prevention and comfort
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Long writing sessions can strain wrists and fingers. Dictating reduces repetitive
|
||||
@@ -142,7 +142,7 @@ export default function WritersPage() {
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-3 inline-flex items-center gap-2 font-semibold">
|
||||
<FileText className="h-5 w-5 text-emerald-500" /> More words, less friction
|
||||
<FileText className="h-5 w-5 text-primary" /> More words, less friction
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Most people speak faster than they type. Voice dictation helps produce first drafts,
|
||||
@@ -153,14 +153,14 @@ export default function WritersPage() {
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 text-2xl font-bold">Recommended Workflow for Writers</h2>
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">Recommended Workflow for Writers</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{writingModes.map((mode) => {
|
||||
const Icon = mode.icon;
|
||||
return (
|
||||
<article
|
||||
key={mode.title}
|
||||
className="rounded-xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-6"
|
||||
>
|
||||
<div className={`mb-3 inline-flex rounded-lg p-2.5 ${mode.iconBg}`}>
|
||||
<Icon className={`h-5 w-5 ${mode.iconColor}`} />
|
||||
@@ -173,8 +173,8 @@ export default function WritersPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12 rounded-2xl border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 className="mb-4 text-2xl font-bold">Voice Commands Useful for Writing</h2>
|
||||
<section className="mb-12 rounded-[12px] border border-border bg-background p-6">
|
||||
<h2 className="mb-4 font-display text-2xl font-semibold">Voice Commands Useful for Writing</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
These commands help maintain writing rhythm while dictating punctuation and formatting.
|
||||
</p>
|
||||
@@ -182,7 +182,7 @@ export default function WritersPage() {
|
||||
{writingCommands.map((cmd) => (
|
||||
<kbd
|
||||
key={cmd}
|
||||
className="rounded-lg border border-zinc-300 bg-zinc-100 px-2 py-1 text-xs font-mono dark:border-zinc-600 dark:bg-zinc-900"
|
||||
className="rounded-lg border border-border bg-muted px-2 py-1 text-xs font-mono"
|
||||
>
|
||||
"{cmd}"
|
||||
</kbd>
|
||||
@@ -191,12 +191,12 @@ export default function WritersPage() {
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="mb-6 text-2xl font-bold">App Integrations for Writing Workflows</h2>
|
||||
<h2 className="mb-6 font-display text-2xl font-semibold">App Integrations for Writing Workflows</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{integrations.map((integration) => (
|
||||
<article
|
||||
key={integration.app}
|
||||
className="rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
className="rounded-[12px] border border-border bg-background p-5"
|
||||
>
|
||||
<h3 className="mb-2 font-semibold">{integration.app}</h3>
|
||||
<p className="text-sm text-muted-foreground">{integration.description}</p>
|
||||
@@ -208,50 +208,50 @@ export default function WritersPage() {
|
||||
<section className="mb-12 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<Link
|
||||
href="/languages/"
|
||||
className="rounded-xl border border-zinc-200 bg-white p-5 transition-colors hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800 dark:hover:bg-zinc-700"
|
||||
className="rounded-[12px] border border-border bg-background p-5 transition-colors hover:bg-muted hover:bg-muted"
|
||||
>
|
||||
<Globe className="mb-3 h-6 w-6 text-blue-500" />
|
||||
<Globe className="mb-3 h-6 w-6 text-primary" />
|
||||
<h3 className="mb-1 font-semibold">Multilingual Writing</h3>
|
||||
<p className="text-sm text-muted-foreground">See supported languages for dictation.</p>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/rsi-prevention/"
|
||||
className="rounded-xl border border-zinc-200 bg-white p-5 transition-colors hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800 dark:hover:bg-zinc-700"
|
||||
className="rounded-[12px] border border-border bg-background p-5 transition-colors hover:bg-muted hover:bg-muted"
|
||||
>
|
||||
<Heart className="mb-3 h-6 w-6 text-rose-500" />
|
||||
<Heart className="mb-3 h-6 w-6 text-primary" />
|
||||
<h3 className="mb-1 font-semibold">Health Benefits</h3>
|
||||
<p className="text-sm text-muted-foreground">Learn how dictation helps prevent RSI.</p>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/offline/"
|
||||
className="rounded-xl border border-zinc-200 bg-white p-5 transition-colors hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800 dark:hover:bg-zinc-700"
|
||||
className="rounded-[12px] border border-border bg-background p-5 transition-colors hover:bg-muted hover:bg-muted"
|
||||
>
|
||||
<Lock className="mb-3 h-6 w-6 text-emerald-500" />
|
||||
<Lock className="mb-3 h-6 w-6 text-primary" />
|
||||
<h3 className="mb-1 font-semibold">Privacy Benefits</h3>
|
||||
<p className="text-sm text-muted-foreground">Keep drafts private with offline dictation.</p>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/use-cases/"
|
||||
className="rounded-xl border border-zinc-200 bg-white p-5 transition-colors hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800 dark:hover:bg-zinc-700"
|
||||
className="rounded-[12px] border border-border bg-background p-5 transition-colors hover:bg-muted hover:bg-muted"
|
||||
>
|
||||
<BookOpen className="mb-3 h-6 w-6 text-violet-500" />
|
||||
<BookOpen className="mb-3 h-6 w-6 text-primary" />
|
||||
<h3 className="mb-1 font-semibold">More Use Cases</h3>
|
||||
<p className="text-sm text-muted-foreground">Explore all workflows powered by Vocalinux.</p>
|
||||
</Link>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-primary/30 bg-primary/10 p-8">
|
||||
<h2 className="mb-3 text-2xl font-bold">Start Writing with Your Voice</h2>
|
||||
<section className="rounded-[12px] border border-primary/30 bg-primary/10 p-8">
|
||||
<h2 className="mb-3 font-display text-2xl font-semibold">Start Writing with Your Voice</h2>
|
||||
<p className="mb-6 max-w-3xl text-muted-foreground">
|
||||
Install Vocalinux and start dictating novels, blog posts, academic notes, and newsroom
|
||||
drafts directly on Linux - without cloud lock-in.
|
||||
</p>
|
||||
<Link
|
||||
href="/install/"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground dark:text-black hover:bg-primary/90"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||
>
|
||||
Install Vocalinux
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { LiveDemo } from "../live-demo";
|
||||
|
||||
describe("LiveDemo unsupported browser state", () => {
|
||||
const originalSpeechRecognition = window.SpeechRecognition;
|
||||
const originalWebkitSpeechRecognition = window.webkitSpeechRecognition;
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(window, "SpeechRecognition", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: undefined,
|
||||
});
|
||||
Object.defineProperty(window, "webkitSpeechRecognition", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(window, "SpeechRecognition", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: originalSpeechRecognition,
|
||||
});
|
||||
Object.defineProperty(window, "webkitSpeechRecognition", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: originalWebkitSpeechRecognition,
|
||||
});
|
||||
});
|
||||
|
||||
it("explains missing speech recognition support and links to compatibility references", async () => {
|
||||
render(<LiveDemo />);
|
||||
|
||||
expect(
|
||||
await screen.findByText("Browser Speech Demo Not Available"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/does not support the SpeechRecognition API/i),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Chrome")).toBeInTheDocument();
|
||||
expect(screen.getByText("Edge")).toBeInTheDocument();
|
||||
expect(screen.getByText("Safari")).toBeInTheDocument();
|
||||
expect(screen.getByRole("link", { name: "MDN" })).toHaveAttribute(
|
||||
"href",
|
||||
"https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,109 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Mic } from "lucide-react";
|
||||
interface DictationOverlayProps {
|
||||
isActive: boolean;
|
||||
onAnimationComplete?: () => void;
|
||||
}
|
||||
export function DictationOverlay({
|
||||
isActive,
|
||||
onAnimationComplete
|
||||
}: DictationOverlayProps) {
|
||||
const [text, setText] = useState("");
|
||||
const fullText = "Hello world";
|
||||
|
||||
// Handle text typing animation
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
setText("");
|
||||
return;
|
||||
}
|
||||
let currentIndex = 0;
|
||||
const interval = setInterval(() => {
|
||||
if (currentIndex < fullText.length) {
|
||||
setText(fullText.substring(0, currentIndex + 1));
|
||||
currentIndex++;
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
// Wait a bit before triggering the fade out
|
||||
setTimeout(() => {
|
||||
if (onAnimationComplete) onAnimationComplete();
|
||||
}, 800);
|
||||
}
|
||||
}, 100); // Speed of typing
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isActive, onAnimationComplete]);
|
||||
return <AnimatePresence>
|
||||
{isActive && <div className="fixed inset-0 flex items-center justify-center z-50 bg-background/40 backdrop-blur-sm">
|
||||
<div className="flex flex-col items-center">
|
||||
{/* Microphone Icon */}
|
||||
<motion.div initial={{
|
||||
opacity: 0,
|
||||
scale: 0.5
|
||||
}} animate={{
|
||||
opacity: 1,
|
||||
scale: 1
|
||||
}} exit={{
|
||||
opacity: 0,
|
||||
scale: 0.5
|
||||
}} transition={{
|
||||
duration: 0.5
|
||||
}} className="relative mb-6">
|
||||
<motion.div animate={{
|
||||
scale: [1, 1.2, 1]
|
||||
}} transition={{
|
||||
repeat: Infinity,
|
||||
duration: 1.5
|
||||
}} className="bg-primary/10 p-6 rounded-full">
|
||||
<Mic className="h-12 w-12 text-primary" />
|
||||
</motion.div>
|
||||
|
||||
{/* Ripple effect */}
|
||||
<motion.div className="absolute inset-0 rounded-full border-2 border-primary" initial={{
|
||||
opacity: 0.6,
|
||||
scale: 1
|
||||
}} animate={{
|
||||
opacity: 0,
|
||||
scale: 1.8
|
||||
}} transition={{
|
||||
repeat: Infinity,
|
||||
duration: 1.5,
|
||||
ease: "easeOut"
|
||||
}} />
|
||||
</motion.div>
|
||||
|
||||
{/* Text Box */}
|
||||
<motion.div initial={{
|
||||
opacity: 0,
|
||||
y: 10
|
||||
}} animate={{
|
||||
opacity: 1,
|
||||
y: 0
|
||||
}} exit={{
|
||||
opacity: 0,
|
||||
y: 10
|
||||
}} transition={{
|
||||
delay: 0.3,
|
||||
duration: 0.5
|
||||
}} className="bg-white dark:bg-zinc-800 p-4 rounded-lg shadow-lg min-w-[300px] border border-border">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="h-2 w-2 rounded-full bg-green-500 animate-pulse"></div>
|
||||
<span className="text-sm font-medium">Transcribing...</span>
|
||||
</div>
|
||||
<div className="min-h-[24px]">
|
||||
{text}
|
||||
<motion.span animate={{
|
||||
opacity: [1, 0]
|
||||
}} transition={{
|
||||
repeat: Infinity,
|
||||
duration: 0.8
|
||||
}} className="inline-block w-2 h-4 bg-primary ml-0.5" />
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>}
|
||||
</AnimatePresence>;
|
||||
}
|
||||
@@ -1,543 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Mic, MicOff, AlertCircle, Volume2, Sparkles, Keyboard } from "lucide-react";
|
||||
|
||||
// Define types for Web Speech API
|
||||
interface SpeechRecognitionEvent {
|
||||
results: SpeechRecognitionResultList;
|
||||
resultIndex: number;
|
||||
}
|
||||
|
||||
interface SpeechRecognitionErrorEvent {
|
||||
error: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface SpeechRecognition extends EventTarget {
|
||||
continuous: boolean;
|
||||
interimResults: boolean;
|
||||
lang: string;
|
||||
start(): void;
|
||||
stop(): void;
|
||||
abort(): void;
|
||||
onstart: ((this: SpeechRecognition, ev: Event) => void) | null;
|
||||
onend: ((this: SpeechRecognition, ev: Event) => void) | null;
|
||||
onresult: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => void) | null;
|
||||
onerror: ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => void) | null;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
SpeechRecognition?: new () => SpeechRecognition;
|
||||
webkitSpeechRecognition?: new () => SpeechRecognition;
|
||||
}
|
||||
}
|
||||
|
||||
// Language code to display name and flag mapping
|
||||
const LANGUAGE_MAP: Record<string, { name: string; flag: string }> = {
|
||||
"en": { name: "English", flag: "🇬🇧" },
|
||||
"en-US": { name: "English (US)", flag: "🇺🇸" },
|
||||
"en-GB": { name: "English (UK)", flag: "🇬🇧" },
|
||||
"en-CA": { name: "English (Canada)", flag: "🇨🇦" },
|
||||
"en-AU": { name: "English (Australia)", flag: "🇦🇺" },
|
||||
"fr": { name: "French", flag: "🇫🇷" },
|
||||
"fr-FR": { name: "French (France)", flag: "🇫🇷" },
|
||||
"fr-CA": { name: "French (Canada)", flag: "🇨🇦" },
|
||||
"de": { name: "German", flag: "🇩🇪" },
|
||||
"de-DE": { name: "German (Germany)", flag: "🇩🇪" },
|
||||
"es": { name: "Spanish", flag: "🇪🇸" },
|
||||
"es-ES": { name: "Spanish (Spain)", flag: "🇪🇸" },
|
||||
"es-MX": { name: "Spanish (Mexico)", flag: "🇲🇽" },
|
||||
"es-AR": { name: "Spanish (Argentina)", flag: "🇦🇷" },
|
||||
"it": { name: "Italian", flag: "🇮🇹" },
|
||||
"it-IT": { name: "Italian (Italy)", flag: "🇮🇹" },
|
||||
"pt": { name: "Portuguese", flag: "🇵🇹" },
|
||||
"pt-BR": { name: "Portuguese (Brazil)", flag: "🇧🇷" },
|
||||
"pt-PT": { name: "Portuguese (Portugal)", flag: "🇵🇹" },
|
||||
"nl": { name: "Dutch", flag: "🇳🇱" },
|
||||
"nl-NL": { name: "Dutch (Netherlands)", flag: "🇳🇱" },
|
||||
"ja": { name: "Japanese", flag: "🇯🇵" },
|
||||
"ja-JP": { name: "Japanese (Japan)", flag: "🇯🇵" },
|
||||
"ko": { name: "Korean", flag: "🇰🇷" },
|
||||
"ko-KR": { name: "Korean (Korea)", flag: "🇰🇷" },
|
||||
"zh": { name: "Chinese", flag: "🇨🇳" },
|
||||
"zh-CN": { name: "Chinese (Simplified)", flag: "🇨🇳" },
|
||||
"zh-TW": { name: "Chinese (Traditional)", flag: "🇹🇼" },
|
||||
"ru": { name: "Russian", flag: "🇷🇺" },
|
||||
"ru-RU": { name: "Russian (Russia)", flag: "🇷🇺" },
|
||||
"ar": { name: "Arabic", flag: "🇸🇦" },
|
||||
"ar-SA": { name: "Arabic (Saudi Arabia)", flag: "🇸🇦" },
|
||||
"hi": { name: "Hindi", flag: "🇮🇳" },
|
||||
"hi-IN": { name: "Hindi (India)", flag: "🇮🇳" },
|
||||
"pl": { name: "Polish", flag: "🇵🇱" },
|
||||
"pl-PL": { name: "Polish (Poland)", flag: "🇵🇱" },
|
||||
"tr": { name: "Turkish", flag: "🇹🇷" },
|
||||
"tr-TR": { name: "Turkish (Turkey)", flag: "🇹🇷" },
|
||||
"sv": { name: "Swedish", flag: "🇸🇪" },
|
||||
"sv-SE": { name: "Swedish (Sweden)", flag: "🇸🇪" },
|
||||
"da": { name: "Danish", flag: "🇩🇰" },
|
||||
"da-DK": { name: "Danish (Denmark)", flag: "🇩🇰" },
|
||||
"fi": { name: "Finnish", flag: "🇫🇮" },
|
||||
"fi-FI": { name: "Finnish (Finland)", flag: "🇫🇮" },
|
||||
"no": { name: "Norwegian", flag: "🇳🇴" },
|
||||
"no-NO": { name: "Norwegian (Norway)", flag: "🇳🇴" },
|
||||
"cs": { name: "Czech", flag: "🇨🇿" },
|
||||
"cs-CZ": { name: "Czech (Czech Republic)", flag: "🇨🇿" },
|
||||
"el": { name: "Greek", flag: "🇬🇷" },
|
||||
"el-GR": { name: "Greek (Greece)", flag: "🇬🇷" },
|
||||
"he": { name: "Hebrew", flag: "🇮🇱" },
|
||||
"he-IL": { name: "Hebrew (Israel)", flag: "🇮🇱" },
|
||||
"th": { name: "Thai", flag: "🇹🇭" },
|
||||
"th-TH": { name: "Thai (Thailand)", flag: "🇹🇭" },
|
||||
"vi": { name: "Vietnamese", flag: "🇻🇳" },
|
||||
"vi-VN": { name: "Vietnamese (Vietnam)", flag: "🇻🇳" },
|
||||
"id": { name: "Indonesian", flag: "🇮🇩" },
|
||||
"id-ID": { name: "Indonesian (Indonesia)", flag: "🇮🇩" },
|
||||
"ms": { name: "Malay", flag: "🇲🇾" },
|
||||
"ms-MY": { name: "Malay (Malaysia)", flag: "🇲🇾" },
|
||||
};
|
||||
|
||||
// Helper function to get language display info
|
||||
function getLanguageInfo(locale: string): { name: string; flag: string } {
|
||||
// Try exact match first
|
||||
if (LANGUAGE_MAP[locale]) {
|
||||
return LANGUAGE_MAP[locale];
|
||||
}
|
||||
// Try base language code (e.g., "en-US" -> "en")
|
||||
const baseCode = locale.split("-")[0];
|
||||
if (LANGUAGE_MAP[baseCode]) {
|
||||
return LANGUAGE_MAP[baseCode];
|
||||
}
|
||||
// Fallback to locale code itself
|
||||
return { name: locale, flag: "🌐" };
|
||||
}
|
||||
|
||||
export function LiveDemo() {
|
||||
const [isSupported, setIsSupported] = useState<boolean | null>(null);
|
||||
const [isListening, setIsListening] = useState(false);
|
||||
const [transcript, setTranscript] = useState("");
|
||||
const [interimTranscript, setInterimTranscript] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
|
||||
const [lastCtrlPress, setLastCtrlPress] = useState<number | null>(null);
|
||||
const [showCtrlHint, setShowCtrlHint] = useState(false);
|
||||
const [detectedLanguage, setDetectedLanguage] = useState<string>("en-US");
|
||||
const recognitionRef = useRef<SpeechRecognition | null>(null);
|
||||
const isListeningRef = useRef(false);
|
||||
const processedResultsRef = useRef<Set<string>>(new Set());
|
||||
const lastTranscriptRef = useRef<string>("");
|
||||
|
||||
// Keep ref in sync with state for use in event handlers
|
||||
useEffect(() => {
|
||||
isListeningRef.current = isListening;
|
||||
}, [isListening]);
|
||||
|
||||
// Check for browser support and detect language
|
||||
useEffect(() => {
|
||||
const SpeechRecognitionAPI = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
setIsSupported(!!SpeechRecognitionAPI);
|
||||
|
||||
// Detect browser language
|
||||
const browserLang = navigator.language || "en-US";
|
||||
setDetectedLanguage(browserLang);
|
||||
}, []);
|
||||
|
||||
// Initialize recognition
|
||||
const initRecognition = useCallback(() => {
|
||||
const SpeechRecognitionAPI = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
if (!SpeechRecognitionAPI) return null;
|
||||
|
||||
const recognition = new SpeechRecognitionAPI();
|
||||
recognition.continuous = true;
|
||||
recognition.interimResults = true;
|
||||
recognition.lang = navigator.language || "en-US";
|
||||
|
||||
recognition.onstart = () => {
|
||||
setIsListening(true);
|
||||
setError(null);
|
||||
setHasPermission(true);
|
||||
};
|
||||
|
||||
recognition.onend = () => {
|
||||
setIsListening(false);
|
||||
};
|
||||
|
||||
recognition.onresult = (event: SpeechRecognitionEvent) => {
|
||||
// On mobile Chrome, the Web Speech API has specific quirks:
|
||||
// 1. Interim results may be incorrectly marked as isFinal with confidence=0
|
||||
// 2. Final results may be emitted multiple times
|
||||
// 3. resultIndex may stay at 0
|
||||
// Solution: Check isFinal AND confidence > 0, plus debounce duplicates
|
||||
let interim = "";
|
||||
let final = "";
|
||||
|
||||
for (let i = event.resultIndex; i < event.results.length; i++) {
|
||||
const result = event.results[i];
|
||||
const transcript = result[0].transcript;
|
||||
const confidence = result[0].confidence || 0;
|
||||
|
||||
// On mobile Chrome, interim results can have isFinal=true but confidence=0
|
||||
// Real final results have confidence > 0
|
||||
const isReallyFinal = result.isFinal && confidence > 0;
|
||||
|
||||
// Create a unique key for this result to prevent duplication
|
||||
const resultKey = `${i}-${transcript}`;
|
||||
|
||||
// Skip if we've already processed this exact result
|
||||
if (processedResultsRef.current.has(resultKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mark this result as processed
|
||||
processedResultsRef.current.add(resultKey);
|
||||
|
||||
if (isReallyFinal) {
|
||||
// Debounce: skip if this is the same as the last final transcript
|
||||
// Mobile Chrome sometimes emits the same final result twice
|
||||
if (transcript !== lastTranscriptRef.current) {
|
||||
final += transcript;
|
||||
lastTranscriptRef.current = transcript;
|
||||
}
|
||||
} else {
|
||||
interim += transcript;
|
||||
}
|
||||
}
|
||||
|
||||
if (final) {
|
||||
setTranscript((prev) => prev + final + " ");
|
||||
setInterimTranscript("");
|
||||
} else {
|
||||
setInterimTranscript(interim);
|
||||
}
|
||||
};
|
||||
|
||||
recognition.onerror = (event: SpeechRecognitionErrorEvent) => {
|
||||
if (event.error === "not-allowed") {
|
||||
setHasPermission(false);
|
||||
setError("Microphone access denied. Please allow microphone permissions.");
|
||||
} else if (event.error === "no-speech") {
|
||||
// Ignore no-speech errors
|
||||
} else {
|
||||
setError(`Error: ${event.error}`);
|
||||
}
|
||||
setIsListening(false);
|
||||
};
|
||||
|
||||
return recognition;
|
||||
}, []);
|
||||
|
||||
const startListening = useCallback(() => {
|
||||
if (recognitionRef.current) {
|
||||
recognitionRef.current.abort();
|
||||
}
|
||||
recognitionRef.current = initRecognition();
|
||||
if (recognitionRef.current) {
|
||||
setTranscript("");
|
||||
setInterimTranscript("");
|
||||
// Clear processed results tracking for new session
|
||||
processedResultsRef.current.clear();
|
||||
lastTranscriptRef.current = "";
|
||||
try {
|
||||
recognitionRef.current.start();
|
||||
} catch (e) {
|
||||
setError("Could not start recognition. Please try again.");
|
||||
}
|
||||
}
|
||||
}, [initRecognition]);
|
||||
|
||||
const stopListening = useCallback(() => {
|
||||
if (recognitionRef.current) {
|
||||
recognitionRef.current.stop();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Toggle listening state
|
||||
const toggleListening = useCallback(() => {
|
||||
if (isListeningRef.current) {
|
||||
stopListening();
|
||||
} else {
|
||||
startListening();
|
||||
}
|
||||
}, [startListening, stopListening]);
|
||||
|
||||
const clearTranscript = useCallback(() => {
|
||||
setTranscript("");
|
||||
setInterimTranscript("");
|
||||
}, []);
|
||||
|
||||
// Double-tap Ctrl detection
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key !== "Control") return;
|
||||
|
||||
const now = Date.now();
|
||||
const DOUBLE_TAP_DELAY = 400;
|
||||
|
||||
// Show hint on first Ctrl press
|
||||
setShowCtrlHint(true);
|
||||
setTimeout(() => setShowCtrlHint(false), 600);
|
||||
|
||||
if (lastCtrlPress && now - lastCtrlPress <= DOUBLE_TAP_DELAY) {
|
||||
// Double-tap detected!
|
||||
toggleListening();
|
||||
setLastCtrlPress(null);
|
||||
setShowCtrlHint(false);
|
||||
} else {
|
||||
setLastCtrlPress(now);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [lastCtrlPress, toggleListening]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (recognitionRef.current) {
|
||||
recognitionRef.current.abort();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Not supported fallback
|
||||
if (isSupported === false) {
|
||||
return (
|
||||
<div className="bg-zinc-900 rounded-2xl p-6 sm:p-8 border border-zinc-800">
|
||||
<div className="text-center max-w-3xl mx-auto">
|
||||
<div className="inline-flex items-center justify-center h-16 w-16 rounded-full bg-yellow-500/10 mb-4">
|
||||
<AlertCircle className="h-8 w-8 text-yellow-500" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-white mb-2">
|
||||
Browser Speech Demo Not Available
|
||||
</h3>
|
||||
<p className="text-zinc-400 mb-4 max-w-2xl mx-auto leading-relaxed">
|
||||
Your browser does not support the SpeechRecognition API used by this
|
||||
live demo. Firefox supports parts of Web Speech, but speech recognition
|
||||
is still disabled for regular pages.
|
||||
</p>
|
||||
<p className="text-sm text-zinc-500 mb-3">
|
||||
Try a browser listed by MDN with SpeechRecognition support:
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-2 mb-4 text-sm max-w-2xl mx-auto">
|
||||
{["Chrome", "Edge", "Safari", "Chrome Android", "Safari iOS", "Samsung Internet"].map((browser) => (
|
||||
<span key={browser} className="px-3 py-1 rounded-full bg-zinc-800 text-zinc-300 border border-zinc-700">
|
||||
{browser}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm text-zinc-500 mb-6">
|
||||
Check compatibility on{" "}
|
||||
<a
|
||||
href="https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition"
|
||||
className="text-primary hover:underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
MDN
|
||||
</a>.
|
||||
</p>
|
||||
<p className="text-sm text-zinc-500">
|
||||
You can still <a href="#install" className="text-primary hover:underline">install Vocalinux</a> for
|
||||
the full offline experience with Whisper AI!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Loading state
|
||||
if (isSupported === null) {
|
||||
return (
|
||||
<div className="bg-zinc-900 rounded-2xl p-8 border border-zinc-800 animate-pulse">
|
||||
<div className="h-48 flex items-center justify-center">
|
||||
<div className="text-zinc-500">Checking browser support...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-br from-zinc-900 to-zinc-950 rounded-2xl overflow-hidden border border-zinc-800">
|
||||
{/* Header */}
|
||||
<div className="px-4 sm:px-6 py-3 bg-zinc-800/50 border-b border-zinc-800 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex gap-1.5">
|
||||
<div className="h-3 w-3 rounded-full bg-red-500/70" />
|
||||
<div className="h-3 w-3 rounded-full bg-yellow-500/70" />
|
||||
<div className="h-3 w-3 rounded-full bg-green-500/70" />
|
||||
</div>
|
||||
<span className="text-sm text-zinc-400 font-medium">
|
||||
Live Browser Demo
|
||||
</span>
|
||||
<span className="text-xs bg-primary/20 text-primary px-2 py-0.5 rounded-full">
|
||||
SpeechRecognition API
|
||||
</span>
|
||||
{(() => {
|
||||
const langInfo = getLanguageInfo(detectedLanguage);
|
||||
return (
|
||||
<span
|
||||
className="text-xs bg-zinc-700/50 text-zinc-300 px-2 py-0.5 rounded-full flex items-center gap-1"
|
||||
title={`Detected language: ${langInfo.name} (${detectedLanguage})`}
|
||||
>
|
||||
<span>{langInfo.flag}</span>
|
||||
<span className="hidden sm:inline">{langInfo.name}</span>
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
{transcript && (
|
||||
<button
|
||||
onClick={clearTranscript}
|
||||
className="text-xs text-zinc-500 hover:text-zinc-300 transition-colors"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="p-6 sm:p-8">
|
||||
{/* Error message */}
|
||||
<AnimatePresence>
|
||||
{error && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
className="mb-6 p-4 bg-red-500/10 border border-red-500/20 rounded-lg flex items-start gap-3"
|
||||
>
|
||||
<AlertCircle className="h-5 w-5 text-red-400 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm text-red-400">{error}</p>
|
||||
{hasPermission === false && (
|
||||
<p className="text-xs text-red-400/70 mt-1">
|
||||
Click the lock icon in your browser's address bar to enable microphone access.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Microphone button */}
|
||||
<div className="flex flex-col items-center mb-6">
|
||||
{/* Double-tap Ctrl hint */}
|
||||
<AnimatePresence>
|
||||
{showCtrlHint && !isListening && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
className="mb-4 px-4 py-2 bg-primary/20 rounded-full text-sm text-primary flex items-center gap-2"
|
||||
>
|
||||
<Keyboard className="h-4 w-4" />
|
||||
Tap Ctrl again to start!
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<motion.button
|
||||
onClick={toggleListening}
|
||||
className={`relative h-20 w-20 sm:h-24 sm:w-24 rounded-full flex items-center justify-center transition-all ${isListening
|
||||
? "bg-red-500 hover:bg-red-600 shadow-lg shadow-red-500/30"
|
||||
: "bg-primary hover:bg-primary/90 shadow-lg shadow-primary/30"
|
||||
}`}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
aria-label={isListening ? "Stop listening" : "Start listening"}
|
||||
>
|
||||
{/* Pulse animation when listening */}
|
||||
{isListening && (
|
||||
<>
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-full bg-red-500"
|
||||
animate={{ scale: [1, 1.2], opacity: [0.5, 0] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity }}
|
||||
/>
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-full bg-red-500"
|
||||
animate={{ scale: [1, 1.4], opacity: [0.3, 0] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity, delay: 0.5 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{isListening ? (
|
||||
<MicOff className="h-8 w-8 sm:h-10 sm:w-10 text-zinc-900 relative z-10" />
|
||||
) : (
|
||||
<Mic className="h-8 w-8 sm:h-10 sm:w-10 text-zinc-900 relative z-10" />
|
||||
)}
|
||||
</motion.button>
|
||||
|
||||
<div className="text-center mt-4">
|
||||
{isListening ? (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-zinc-200 flex items-center justify-center gap-2">
|
||||
{(() => {
|
||||
const langInfo = getLanguageInfo(detectedLanguage);
|
||||
return (
|
||||
<>
|
||||
<span>{langInfo.flag}</span>
|
||||
<span>Listening in {langInfo.name}</span>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</p>
|
||||
<p className="text-xs text-zinc-400">
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
|
||||
</span>
|
||||
Double-tap <kbd className="px-2 py-1 bg-zinc-800 border border-zinc-600 rounded text-xs mx-1 text-zinc-200 font-mono">Ctrl</kbd> or click to stop
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-zinc-200">
|
||||
Double-tap <kbd className="px-2 py-1 bg-zinc-800 border border-zinc-600 rounded text-xs mx-1 text-zinc-200 font-mono">Ctrl</kbd> to start
|
||||
</p>
|
||||
<p className="text-xs text-zinc-400">or click the microphone</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Transcript area */}
|
||||
<div className="bg-zinc-950/50 rounded-xl p-4 sm:p-6 min-h-[120px] border border-zinc-800/50">
|
||||
{transcript || interimTranscript ? (
|
||||
<div className="text-zinc-200 leading-relaxed">
|
||||
<span>{transcript}</span>
|
||||
{interimTranscript && (
|
||||
<span className="text-zinc-500 italic">{interimTranscript}</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-zinc-500 text-center py-4">
|
||||
<Volume2 className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
||||
<p>Your transcribed speech will appear here</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info banner */}
|
||||
<div className="mt-6 flex items-start gap-3 p-4 bg-primary/5 rounded-lg border border-primary/10">
|
||||
<Sparkles className="h-5 w-5 text-primary flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm">
|
||||
<p className="text-zinc-300">
|
||||
<strong>This is just a preview!</strong> The browser demo uses browser
|
||||
SpeechRecognition, which is cloud-based in many browsers.
|
||||
</p>
|
||||
<p className="text-zinc-500 mt-1">
|
||||
Vocalinux runs <strong>100% offline</strong> using Whisper AI - no cloud, no data sharing,
|
||||
complete privacy. <a href="#install" className="text-primary hover:underline">Install it</a> to
|
||||
experience the real deal!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -22,13 +22,12 @@ import {
|
||||
ShieldCheck,
|
||||
SlidersHorizontal,
|
||||
Shield,
|
||||
Sun,
|
||||
X,
|
||||
Zap,
|
||||
Moon,
|
||||
Github,
|
||||
} from "lucide-react";
|
||||
import { VocalinuxLogo } from "@/components/optimized-image";
|
||||
import { useTheme } from "next-themes";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
|
||||
interface SeoSubpageShellProps {
|
||||
children: React.ReactNode;
|
||||
@@ -116,22 +115,22 @@ function DropdownMenu({ category }: { category: (typeof navCategories)[0] }) {
|
||||
<div className="group relative">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-zinc-100 hover:text-foreground focus:bg-zinc-100 focus:text-foreground focus:outline-none dark:hover:bg-zinc-800 dark:focus:bg-zinc-800"
|
||||
className="inline-flex items-center gap-1 rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:bg-muted focus:text-foreground focus:outline-none"
|
||||
>
|
||||
{category.label}
|
||||
<ChevronDown className="h-3.5 w-3.5 transition-transform group-focus-within:rotate-180 group-hover:rotate-180" />
|
||||
</button>
|
||||
|
||||
<div className="pointer-events-none invisible absolute left-0 top-full z-50 mt-0 w-56 rounded-xl border border-zinc-200 bg-white p-2 opacity-0 shadow-lg transition-all duration-150 group-focus-within:pointer-events-auto group-focus-within:visible group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:visible group-hover:opacity-100 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<div className="pointer-events-none invisible absolute left-0 top-full z-50 mt-0 w-56 rounded-[12px] border border-border bg-background p-1.5 opacity-0 shadow-[0_12px_32px_-12px_rgba(0,0,0,0.35)] transition-all duration-150 group-focus-within:pointer-events-auto group-focus-within:visible group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:visible group-hover:opacity-100">
|
||||
{category.items.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-zinc-100 hover:text-foreground dark:hover:bg-zinc-700"
|
||||
className="flex items-center gap-3 rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<Icon className="h-4 w-4 text-primary" />
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
@@ -173,13 +172,13 @@ function Breadcrumbs() {
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="mb-6 flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<nav className="mb-8 flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
|
||||
<Link href="/" className="transition-colors hover:text-foreground">
|
||||
Home
|
||||
</Link>
|
||||
{breadcrumbs.map((crumb, index) => (
|
||||
<React.Fragment key={crumb.href}>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
<ChevronRight className="h-4 w-4 shrink-0" />
|
||||
{index === breadcrumbs.length - 1 ? (
|
||||
<span className="font-medium text-foreground">
|
||||
{getPageTitle(crumb.href)}
|
||||
@@ -200,11 +199,13 @@ function Breadcrumbs() {
|
||||
|
||||
export function SeoSubpageShell({ children }: SeoSubpageShellProps) {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
const onResize = () => {
|
||||
if (window.innerWidth >= 1024) setMobileMenuOpen(false);
|
||||
};
|
||||
window.addEventListener("resize", onResize);
|
||||
return () => window.removeEventListener("resize", onResize);
|
||||
}, []);
|
||||
|
||||
const mainNavCategories = [
|
||||
@@ -277,67 +278,58 @@ export function SeoSubpageShell({ children }: SeoSubpageShellProps) {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-zinc-50 to-white dark:from-zinc-900 dark:to-zinc-950">
|
||||
<header className="bg-background/95 sticky top-0 z-50 border-b border-zinc-200/80 backdrop-blur-md dark:border-zinc-800/80">
|
||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-3 sm:px-6">
|
||||
<Link href="/" className="group flex items-center gap-2">
|
||||
<VocalinuxLogo
|
||||
width={30}
|
||||
height={30}
|
||||
className="h-7 w-7 transition-transform group-hover:scale-110"
|
||||
/>
|
||||
<span className="text-base font-bold sm:text-lg">Vocalinux</span>
|
||||
<div className="min-h-screen max-w-[100vw] overflow-x-clip bg-background text-foreground">
|
||||
<header className="fixed inset-x-0 top-0 z-50 border-b border-border/80 bg-background/85 backdrop-blur-md">
|
||||
<div className="mx-auto flex h-14 max-w-6xl items-center justify-between px-4 sm:h-16 sm:px-6">
|
||||
<Link href="/" className="flex items-center gap-2.5">
|
||||
<VocalinuxLogo width={28} height={28} className="h-7 w-7" />
|
||||
<span className="font-display text-[15px] font-semibold tracking-tight">
|
||||
Vocalinux
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop nav with dropdowns */}
|
||||
<nav className="hidden items-center gap-1 lg:flex">
|
||||
{mainNavCategories.map((category) => (
|
||||
<DropdownMenu key={category.label} category={category} />
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Theme toggle and mobile menu */}
|
||||
<div className="flex items-center gap-2">
|
||||
{mounted && (
|
||||
<button
|
||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||
className="hover:bg-muted/80 flex h-8 w-8 items-center justify-center rounded-md bg-muted transition-all sm:h-9 sm:w-9"
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<Sun className="h-4 w-4 text-yellow-400" />
|
||||
) : (
|
||||
<Moon className="h-4 w-4 text-slate-700" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<ThemeToggle />
|
||||
<a
|
||||
href="/#install"
|
||||
className="hidden h-9 items-center rounded-full bg-primary px-4 text-sm font-medium text-primary-foreground sm:inline-flex"
|
||||
>
|
||||
Install
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setMobileMenuOpen(!mobileMenuOpen);
|
||||
}}
|
||||
className="flex items-center justify-center rounded-lg p-2 text-muted-foreground transition-colors hover:bg-zinc-100 hover:text-foreground dark:hover:bg-zinc-800 lg:hidden"
|
||||
className="inline-flex h-9 w-9 items-center justify-center rounded-md border border-border text-muted-foreground transition-colors hover:bg-muted hover:text-foreground lg:hidden"
|
||||
aria-label={mobileMenuOpen ? "Close menu" : "Open menu"}
|
||||
aria-expanded={mobileMenuOpen}
|
||||
>
|
||||
{mobileMenuOpen ? (
|
||||
<X className="h-6 w-6" />
|
||||
<X className="h-5 w-5" />
|
||||
) : (
|
||||
<Menu className="h-6 w-6" />
|
||||
<Menu className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="border-t border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900 lg:hidden">
|
||||
{mobileMenuOpen ? (
|
||||
<div className="border-t border-border bg-background lg:hidden">
|
||||
<nav className="max-h-[calc(100vh-4rem)] overflow-y-auto px-4 py-4">
|
||||
{mainNavCategories.map((category) => (
|
||||
<div key={category.label} className="mb-4">
|
||||
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
<h3 className="mb-2 font-mono text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{category.label}
|
||||
</h3>
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-0.5">
|
||||
{category.items.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
@@ -345,9 +337,9 @@ export function SeoSubpageShell({ children }: SeoSubpageShellProps) {
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-zinc-100 hover:text-foreground dark:hover:bg-zinc-800"
|
||||
className="flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<Icon className="h-4 w-4 text-primary" />
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
@@ -355,41 +347,65 @@ export function SeoSubpageShell({ children }: SeoSubpageShellProps) {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<a
|
||||
href="/#install"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="mt-2 flex items-center justify-center rounded-full bg-primary px-4 py-2.5 text-sm font-semibold text-primary-foreground"
|
||||
>
|
||||
Install Vocalinux
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
<div className="mx-auto w-full max-w-6xl px-4 pb-16 pt-6 sm:px-6 sm:pt-10">
|
||||
<div className="mx-auto w-full max-w-6xl px-4 pb-16 pt-24 sm:px-6 sm:pt-28">
|
||||
<Breadcrumbs />
|
||||
{children}
|
||||
<div className="subpage-content min-w-0">{children}</div>
|
||||
</div>
|
||||
|
||||
<footer className="border-t border-zinc-200 bg-white/70 dark:border-zinc-800 dark:bg-zinc-900/60">
|
||||
<div className="mx-auto max-w-6xl px-4 py-8 sm:px-6">
|
||||
<div className="mb-6 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
|
||||
<footer className="border-t border-border bg-[#0a0a0c] text-zinc-300">
|
||||
<div className="mx-auto max-w-6xl px-4 py-10 sm:px-6">
|
||||
<div className="mb-8 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
|
||||
<div>
|
||||
<p className="text-sm font-semibold">
|
||||
<p className="text-sm font-semibold text-zinc-100">
|
||||
Want the full product overview?
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Go back to the homepage for the live demo and one-command
|
||||
installer.
|
||||
<p className="mt-1 text-sm text-zinc-400">
|
||||
Homepage has the install command, app screenshots, and engine
|
||||
guide.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/"
|
||||
className="hover:bg-primary/90 inline-flex items-center gap-1.5 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground dark:text-black"
|
||||
>
|
||||
Home Page
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-1.5 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
||||
>
|
||||
Home
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<a
|
||||
href="/#install"
|
||||
className="inline-flex items-center gap-1.5 rounded-full border border-zinc-700 px-4 py-2 text-sm font-medium text-zinc-100 transition-colors hover:border-zinc-500"
|
||||
>
|
||||
Install
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/jatinkrmalik/vocalinux"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 rounded-full border border-zinc-700 px-4 py-2 text-sm font-medium text-zinc-100 transition-colors hover:border-zinc-500"
|
||||
>
|
||||
<Github className="h-4 w-4" />
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 border-t border-zinc-200 pt-6 dark:border-zinc-700 sm:grid-cols-2 lg:grid-cols-5">
|
||||
<div className="grid gap-6 border-t border-zinc-800 pt-8 sm:grid-cols-2 lg:grid-cols-5">
|
||||
{mainNavCategories.map((category) => (
|
||||
<div key={category.label}>
|
||||
<h4 className="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
<h4 className="mb-2 font-mono text-[11px] font-semibold uppercase tracking-wider text-zinc-500">
|
||||
{category.label}
|
||||
</h4>
|
||||
<ul className="space-y-1.5">
|
||||
@@ -397,7 +413,7 @@ export function SeoSubpageShell({ children }: SeoSubpageShellProps) {
|
||||
<li key={item.href}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className="text-sm text-muted-foreground transition-colors hover:text-foreground"
|
||||
className="text-sm text-zinc-400 transition-colors hover:text-white"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
interface UseDoublePressOptions {
|
||||
key: string;
|
||||
delay?: number;
|
||||
onDoublePress?: () => void;
|
||||
}
|
||||
|
||||
export function useDoublePress({
|
||||
key,
|
||||
delay = 400,
|
||||
onDoublePress,
|
||||
}: UseDoublePressOptions) {
|
||||
const [lastPressed, setLastPressed] = useState<number | null>(null);
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
// Only respond to the specified key
|
||||
if (event.key.toLowerCase() !== key.toLowerCase()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// First press or too much time has passed
|
||||
if (lastPressed === null || now - lastPressed > delay) {
|
||||
setLastPressed(now);
|
||||
return;
|
||||
}
|
||||
|
||||
// Double press detected within the delay timeframe
|
||||
if (now - lastPressed <= delay) {
|
||||
setIsActive(true);
|
||||
if (onDoublePress) onDoublePress();
|
||||
setLastPressed(null);
|
||||
}
|
||||
},
|
||||
[key, delay, lastPressed, onDoublePress]
|
||||
);
|
||||
|
||||
// Reset the active state
|
||||
const resetActive = useCallback(() => {
|
||||
setIsActive(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Add event listener for key press
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [handleKeyDown]);
|
||||
|
||||
return { isActive, resetActive };
|
||||
}
|
||||
+260
-69
@@ -4,76 +4,91 @@
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: #FFFFFF;
|
||||
--foreground: #09090B;
|
||||
--card: #FFFFFF;
|
||||
--card-foreground: #09090B;
|
||||
--popover: #FFFFFF;
|
||||
--popover-foreground: #09090B;
|
||||
--primary: #3CB371;
|
||||
--primary-foreground: #FFFFFF;
|
||||
--secondary: #F4F4F5;
|
||||
--secondary-foreground: #18181B;
|
||||
--muted: #F4F4F5;
|
||||
--muted-foreground: #71717A;
|
||||
--accent: #F4F4F5;
|
||||
--accent-foreground: #18181B;
|
||||
--destructive: #EF4444;
|
||||
--destructive-foreground: #FAFAFA;
|
||||
--border: #E4E4E7;
|
||||
--input: #E4E4E7;
|
||||
--ring: #3CB371;
|
||||
--chart-1: #3CB371;
|
||||
--chart-2: #2DD4BF;
|
||||
--chart-3: #2F3F4A;
|
||||
--chart-4: #D9B64E;
|
||||
--chart-5: #E67E33;
|
||||
--radius: 0.5rem;
|
||||
/* PR #546 craft language: pure iron surfaces + committed emerald */
|
||||
--background: #ffffff;
|
||||
--foreground: #09090b;
|
||||
--card: #f4f4f5;
|
||||
--card-foreground: #09090b;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #09090b;
|
||||
/* Darker emerald so white button text clears ~4.5:1 (was 3.4 on #2f9e5f) */
|
||||
--primary: #1a7f4e;
|
||||
--primary-deep: #14663e;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #f4f4f5;
|
||||
--secondary-foreground: #18181b;
|
||||
--muted: #f4f4f5;
|
||||
--muted-foreground: #52525b;
|
||||
--accent: #ecfdf3;
|
||||
--accent-foreground: #14532d;
|
||||
--destructive: #ef4444;
|
||||
--destructive-foreground: #fafafa;
|
||||
--border: #e4e4e7;
|
||||
--input: #e4e4e7;
|
||||
--ring: #2f9e5f;
|
||||
--chart-1: #2f9e5f;
|
||||
--chart-2: #3f3f46;
|
||||
--chart-3: #71717a;
|
||||
--chart-4: #a1a1aa;
|
||||
--chart-5: #d4d4d8;
|
||||
--radius: 0.625rem;
|
||||
|
||||
--sidebar-background: #FAFAFA;
|
||||
--sidebar-foreground: #3F3F46;
|
||||
--sidebar-primary: #3CB371;
|
||||
--sidebar-primary-foreground: #FAFAFA;
|
||||
--sidebar-accent: #F4F4F5;
|
||||
--sidebar-accent-foreground: #18181B;
|
||||
--sidebar-border: #E5E7EB;
|
||||
--sidebar-ring: #3CB371;
|
||||
--sidebar-background: #fafafa;
|
||||
--sidebar-foreground: #3f3f46;
|
||||
--sidebar-primary: #2f9e5f;
|
||||
--sidebar-primary-foreground: #fafafa;
|
||||
--sidebar-accent: #f4f4f5;
|
||||
--sidebar-accent-foreground: #18181b;
|
||||
--sidebar-border: #e5e7eb;
|
||||
--sidebar-ring: #2f9e5f;
|
||||
|
||||
--terminal: #0a0a0c;
|
||||
--terminal-fg: #6ee7a8;
|
||||
--ink: #09090b;
|
||||
--paper: #ffffff;
|
||||
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #09090B;
|
||||
--foreground: #FAFAFA;
|
||||
--card: #09090B;
|
||||
--card-foreground: #FAFAFA;
|
||||
--popover: #09090B;
|
||||
--popover-foreground: #FAFAFA;
|
||||
--primary: #50C878;
|
||||
--primary-foreground: #FFFFFF;
|
||||
--secondary: #27272A;
|
||||
--secondary-foreground: #FAFAFA;
|
||||
--muted: #27272A;
|
||||
--muted-foreground: #A1A1AA;
|
||||
--accent: #27272A;
|
||||
--accent-foreground: #FAFAFA;
|
||||
--destructive: #7F1D1D;
|
||||
--destructive-foreground: #FAFAFA;
|
||||
--border: #27272A;
|
||||
--input: #27272A;
|
||||
--ring: #50C878;
|
||||
--chart-1: #50C878;
|
||||
--chart-2: #2DD4BF;
|
||||
--chart-3: #FB923C;
|
||||
--chart-4: #C084FC;
|
||||
--chart-5: #F87171;
|
||||
--background: #0c0c0e;
|
||||
--foreground: #fafafa;
|
||||
--card: #16161a;
|
||||
--card-foreground: #fafafa;
|
||||
--popover: #16161a;
|
||||
--popover-foreground: #fafafa;
|
||||
--primary: #50c878;
|
||||
--primary-deep: #3cb371;
|
||||
--primary-foreground: #052e16;
|
||||
--secondary: #1c1c21;
|
||||
--secondary-foreground: #fafafa;
|
||||
--muted: #1c1c21;
|
||||
--muted-foreground: #a1a1aa;
|
||||
--accent: #0f291a;
|
||||
--accent-foreground: #86efac;
|
||||
--destructive: #7f1d1d;
|
||||
--destructive-foreground: #fafafa;
|
||||
--border: #27272a;
|
||||
--input: #27272a;
|
||||
--ring: #50c878;
|
||||
--chart-1: #50c878;
|
||||
--chart-2: #a1a1aa;
|
||||
--chart-3: #71717a;
|
||||
--chart-4: #52525b;
|
||||
--chart-5: #3f3f46;
|
||||
|
||||
--sidebar-background: #18181B;
|
||||
--sidebar-foreground: #F4F4F5;
|
||||
--sidebar-primary: #50C878;
|
||||
--sidebar-primary-foreground: #FFFFFF;
|
||||
--sidebar-accent: #27272A;
|
||||
--sidebar-accent-foreground: #F4F4F5;
|
||||
--sidebar-border: #27272A;
|
||||
--sidebar-ring: #50C878;
|
||||
--sidebar-background: #16161a;
|
||||
--sidebar-foreground: #f4f4f5;
|
||||
--sidebar-primary: #50c878;
|
||||
--sidebar-primary-foreground: #052e16;
|
||||
--sidebar-accent: #1c1c21;
|
||||
--sidebar-accent-foreground: #f4f4f5;
|
||||
--sidebar-border: #27272a;
|
||||
--sidebar-ring: #50c878;
|
||||
|
||||
--terminal: #060608;
|
||||
--terminal-fg: #6ee7a8;
|
||||
--ink: #fafafa;
|
||||
--paper: #0c0c0e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,12 +97,182 @@
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
html {
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
max-width: 100%;
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground antialiased;
|
||||
font-family: var(--font-body), ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
.font-display {
|
||||
font-family: var(--font-display), ui-sans-serif, system-ui, sans-serif;
|
||||
letter-spacing: -0.03em;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
p {
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
@apply outline-2 outline-offset-2 outline-primary;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.btn-primary {
|
||||
@apply inline-flex h-11 items-center justify-center gap-2 rounded-full bg-primary px-6 text-sm font-semibold text-primary-foreground transition-transform active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply inline-flex h-11 items-center justify-center gap-2 rounded-full border border-border bg-background px-5 text-sm font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
@apply inline-flex h-11 items-center justify-center gap-2 rounded-full border border-border bg-transparent px-5 text-sm font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring;
|
||||
}
|
||||
|
||||
.terminal-panel {
|
||||
@apply w-full max-w-full min-w-0 overflow-hidden rounded-[12px] border border-zinc-800 bg-[color:var(--terminal)] shadow-[0_24px_60px_-28px_rgba(0,0,0,0.55)];
|
||||
}
|
||||
|
||||
.terminal-panel pre {
|
||||
@apply max-w-full whitespace-pre-wrap break-all font-mono;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
@apply font-display text-3xl font-semibold tracking-tight text-foreground sm:text-4xl;
|
||||
}
|
||||
|
||||
.section-lede {
|
||||
@apply mt-3 max-w-xl text-base text-muted-foreground sm:text-lg;
|
||||
}
|
||||
|
||||
.shot-frame {
|
||||
@apply relative overflow-hidden rounded-[12px] border border-border bg-card shadow-[0_24px_60px_-28px_rgba(0,0,0,0.35)];
|
||||
}
|
||||
|
||||
/* Shared craft language for SEO / child pages (plain CSS: @apply rejects opacity modifiers) */
|
||||
.subpage-content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.subpage-content h1 {
|
||||
font-family: var(--font-display), ui-sans-serif, system-ui, sans-serif;
|
||||
font-size: 2.25rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1.1;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.subpage-content h1 {
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.subpage-content h2 {
|
||||
font-family: var(--font-display), ui-sans-serif, system-ui, sans-serif;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.025em;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.subpage-content h2 {
|
||||
font-size: 1.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.subpage-content h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.subpage-kicker {
|
||||
margin-bottom: 1rem;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.subpage-lede {
|
||||
margin-top: 1rem;
|
||||
max-width: 42rem;
|
||||
font-size: 1.0625rem;
|
||||
line-height: 1.65;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.subpage-lede {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
.subpage-card {
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--background);
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.subpage-card:hover {
|
||||
border-color: color-mix(in oklab, var(--primary) 40%, var(--border));
|
||||
}
|
||||
|
||||
.subpage-panel {
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: color-mix(in oklab, var(--card) 40%, transparent);
|
||||
}
|
||||
|
||||
.subpage-terminal {
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #27272a;
|
||||
background: var(--terminal);
|
||||
box-shadow: 0 24px 60px -28px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.subpage-terminal code,
|
||||
.subpage-terminal pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 0.875rem;
|
||||
color: var(--terminal-fg);
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.subpage-terminal code,
|
||||
.subpage-terminal pre {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide scrollbars but keep scroll functionality */
|
||||
@layer utilities {
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
@@ -96,9 +281,16 @@
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.text-measure {
|
||||
max-width: 65ch;
|
||||
}
|
||||
|
||||
.ease-out-expo {
|
||||
transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide scrollbars on syntax highlighter code blocks */
|
||||
pre[class*="language-"],
|
||||
code {
|
||||
-ms-overflow-style: none;
|
||||
@@ -113,7 +305,6 @@ nextjs-portal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Respect user's motion preferences for accessibility */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
|
||||
+12
-1
@@ -10,8 +10,19 @@ export default {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: [
|
||||
'var(--font-geist-sans)',
|
||||
'var(--font-body)',
|
||||
...defaultTheme.fontFamily.sans
|
||||
],
|
||||
display: [
|
||||
'var(--font-display)',
|
||||
...defaultTheme.fontFamily.sans
|
||||
],
|
||||
mono: [
|
||||
'ui-monospace',
|
||||
'SFMono-Regular',
|
||||
'Menlo',
|
||||
'Consolas',
|
||||
...defaultTheme.fontFamily.mono
|
||||
]
|
||||
},
|
||||
borderRadius: {
|
||||
|
||||
Reference in New Issue
Block a user