From c99d66dc03248b957e547ce784d875196f120ee9 Mon Sep 17 00:00:00 2001 From: Drew Smirnoff Date: Thu, 23 Apr 2026 22:46:50 +0400 Subject: [PATCH] chore: update website (#858) --- .gitattributes | 4 + public/.nojekyll | 0 public/README.md | 30 - public/assets/floatpane.png | Bin 0 -> 3854 bytes public/components/site.jsx | 630 +++++++++++++ public/components/tui.jsx | 940 +++++++++++++++++++ public/contact/index.html | 11 - public/features/index.html | 11 - public/get-started/index.html | 12 - public/index.html | 1668 +-------------------------------- public/styles.css | 1126 ++++++++++++++++++++++ 11 files changed, 2740 insertions(+), 1692 deletions(-) create mode 100644 public/.nojekyll delete mode 100644 public/README.md create mode 100644 public/assets/floatpane.png create mode 100644 public/components/site.jsx create mode 100644 public/components/tui.jsx delete mode 100644 public/contact/index.html delete mode 100644 public/features/index.html delete mode 100644 public/get-started/index.html create mode 100644 public/styles.css diff --git a/.gitattributes b/.gitattributes index 8441b122089e071f1635a0bd62d22fa7b0694086..7bfd22535a87fbe320445a05dfc92406f93aa4e9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,7 @@ clib/stb_image.h linguist-vendored +*.jsx linguist-detectable=false +*.css linguist-detectable=false +*.js linguist-detectable=false +*.json linguist-detectable=false *.html linguist-detectable=false public/** linguist-documentation diff --git a/public/.nojekyll b/public/.nojekyll new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/public/README.md b/public/README.md deleted file mode 100644 index dd164c5ba071c26ebb7ed3215ee59452f5d634f5..0000000000000000000000000000000000000000 --- a/public/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# public - -Static website files for the Matcha project landing page. This site is deployed via GitHub Pages using the `.github/workflows/static.yml` workflow. - -## Architecture - -A simple static HTML/CSS website that serves as the public-facing project page with information about features, installation, and contact details. - -## Files - -| File | Description | -|------|-------------| -| `index.html` | Main landing page. | -| `style.css` | Stylesheet for all pages. | -| `matcha.flatpakref` | Flatpak package reference for Linux distribution. | -| `features/index.html` | Features overview page. | -| `get-started/index.html` | Getting started / installation guide page. | -| `contact/index.html` | Contact information page. | - -### assets/ - -| File | Description | -|------|-------------| -| `demo.gif` | Animated terminal demo. | -| `logo.png` | Project logo. | -| `logo-transparent.png` | Logo with transparent background. | -| `logo.txt` | ASCII text version of the logo. | -| `favicon.ico` | Browser favicon. | -| `preview.png` | Social media / link preview image. | -| `screenshots/` | Directory for generated UI screenshots (populated by CI). | diff --git a/public/assets/floatpane.png b/public/assets/floatpane.png new file mode 100644 index 0000000000000000000000000000000000000000..b9de158ef18c2fc9be8a4314a194f8d7fd49b40b GIT binary patch literal 3854 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 Jmw>c+_CIckDZu~$ literal 0 HcmV?d00001 diff --git a/public/components/site.jsx b/public/components/site.jsx new file mode 100644 index 0000000000000000000000000000000000000000..6191c21e74ae5e7bd279f5243e7ac6074e6252ef --- /dev/null +++ b/public/components/site.jsx @@ -0,0 +1,630 @@ +const { useState, useEffect } = React; + +function FloatpaneMark({ size = 20 }) { + return ( + Floatpane logo + ); +} + +function MatchaWordmark() { + const [version, setVersion] = useState("v0.8.2"); + useEffect(() => { + // floatpane/matcha repo — fetch latest release tag + fetch("https://api.github.com/repos/floatpane/matcha/releases/latest") + .then((r) => (r.ok ? r.json() : null)) + .then((d) => { + if (d && d.tag_name) + setVersion( + d.tag_name.startsWith("v") ? d.tag_name : "v" + d.tag_name, + ); + }) + .catch(() => {}); + }, []); + return ( +
+ Matcha logo + matcha + — {version} +
+ ); +} + +function TopNav() { + const [stars, setStars] = useState(null); + useEffect(() => { + fetch("https://api.github.com/repos/floatpane/matcha") + .then((r) => (r.ok ? r.json() : null)) + .then((d) => { + if (d && typeof d.stargazers_count === "number") { + const n = d.stargazers_count; + setStars(n >= 1000 ? (n / 1000).toFixed(1) + "k" : String(n)); + } + }) + .catch(() => {}); + }, []); + return ( +
+ + + + + +
+ ); +} + +function Hero({ datasetKey, setDatasetKey }) { + const [pressed, setPressed] = useState(false); + const TUI = window.MatchaTUI; + return ( +
+
+
+ + by floatpane · local-first · secure · no telemetry +
+

+ A powerful, feature-rich +
+ email client for your terminal. +

+

+ Matcha is a modern TUI email client for people who live in the shell. + Vim keybindings, PGP, IMAP multi-account, markdown composing, + visual-mode batch ops, and a CLI that speaks your language. +

+ +
+
+ license MIT +
+
+ runtime single static binary +
+
+ platforms macOS · Linux · Windows +
+
+
+ +
+
+
+ demo · + + + +
+
+ {pressed ? ( + ✓ keyboard live + ) : ( + + click · j + k · tab · + + )} +
+
+ {TUI && ( + setPressed(true)} /> + )} +
+
+ ); +} + +const FEATURES = [ + { + k: "01", + title: "Email, the way you move", + body: "Read, reply, delete, archive — all from the home row. j/k to move, h/l between accounts, tab between folders. No mouse, no modals.", + mono: "j k h l r d a ↵", + }, + { + k: "02", + title: "Visual mode, for real", + body: "Press v to enter Vim-style multi-select. Expand with j/k, then d, a, or m to run batch ops as a single IMAP command.", + mono: "v j j j d\n→ deleted 4 messages", + }, + { + k: "03", + title: "Compose in markdown", + body: "Write in the syntax you already know. Headings, lists, fenced code, and tables render cleanly on the other side.", + mono: "# subject\n- bullet\n`inline`", + }, + { + k: "04", + title: "Multi-account, tabbed", + body: "IMAP, Gmail, Fastmail, Proton Bridge — all tabbed in one window. h and l switch between them so you never reply from the wrong address.", + mono: "← me@andrinoff\n→ drew@floatpane", + }, + { + k: "05", + title: "Fuzzy filter", + body: "Press / to fuzzy-filter across senders, subjects, and bodies in the active view. Results stream in as you type.", + mono: "/lena → 3 hits", + }, + { + k: "06", + title: "Local-first drafts", + body: "Every keystroke hits disk before it hits the wire. Close the laptop, open it anywhere, pick up mid-sentence. Esc saves.", + mono: "~/.cache/matcha/drafts", + }, + { + k: "07", + title: "CLI that composes", + body: "Pipe errors into apologies. Send from scripts, CI, or cron. `matcha send` does one thing well.", + mono: "$ matcha send --to …", + }, + { + k: "08", + title: "AI, on your terms", + body: "Rewrite drafts with the model of your choice. Let agents send on your behalf — with strict scopes and an audit log.", + mono: "alt + r: make it more formal", + }, + { + k: "09", + title: "Smart image rendering", + body: "Images render inline via iterm2 or kitty-graphics where supported. Toggle with i. Off by default, always.", + mono: "i → ◧ images on", + }, +]; + +function Features() { + return ( +
+
+
+
§ features
+

+ Everything you'd expect. +
+ And nothing you wouldn't. +

+
+

+ Matcha is opinionated. It won't follow you around the web, won't + upsell you on AI credits, and won't sync your signatures to a SaaS. It + reads mail. It writes mail. It stays out of the way. +

+
+
+ {FEATURES.map((f) => ( +
+
+ {f.k} + —— +
+

{f.title}

+

{f.body}

+
{f.mono}
+
+ ))} +
+
+ ); +} + +function Keybinds() { + const rows = [ + { + g: "motion", + items: [ + ["j / k", "next / prev message"], + ["↑ / ↓", "next / prev message"], + ["h / l", "prev / next account"], + ["← / →", "prev / next account"], + ["tab", "next folder"], + ["shift-tab", "prev folder"], + ], + }, + { + g: "inbox", + items: [ + ["↵", "open email"], + ["r", "refresh"], + ["d", "delete"], + ["a", "archive"], + ["/", "filter"], + ["v", "visual mode"], + ["esc", "back / main menu"], + ], + }, + { + g: "visual mode", + items: [ + ["v", "enter visual mode"], + ["j / k", "expand selection"], + ["d", "delete all selected"], + ["a", "archive all selected"], + ["m", "move to folder"], + ["v / esc", "exit visual mode"], + ], + }, + { + g: "email view", + items: [ + ["j / k", "scroll body"], + ["r", "reply"], + ["d", "delete"], + ["a", "archive"], + ["tab", "focus attachments"], + ["i", "toggle images"], + ["esc", "back to inbox"], + ], + }, + { + g: "attachments", + items: [ + ["j / k", "navigate"], + ["↵", "download & open"], + ["tab / esc", "back to body"], + ], + }, + { + g: "composer", + items: [ + ["tab / shift-tab", "navigate fields"], + ["↵ on From", "select account"], + ["↵ on Attachment", "open file picker"], + ["↵ on Send", "send email"], + ["↑ / ↓", "contact suggestions"], + ["esc", "save draft & exit"], + ], + }, + ]; + return ( +
+
+
+
§ keybinds
+

+ Vim-native. +
+ Home row to inbox zero. +

+
+

+ Every binding is documented at{" "} + + docs.matcha.floatpane.com + + . Muscle-memory for vimmers, learnable for everyone else. +

+
+
+ {rows.map((row) => ( +
+
── {row.g} ──
+ {row.items.map(([k, label]) => ( +
+ + {k} + + {"·".repeat(26)} + {label} +
+ ))} +
+ ))} +
+
+ ); +} + +const INSTALL_TABS = { + brew: { + plat: "macOS · Linux", + cmd: "$ brew install floatpane/matcha/matcha\n$ matcha", + }, + winget: { + plat: "Windows 10 / 11", + cmd: "$ winget install --id=floatpane.matcha\n$ matcha", + }, + snap: { plat: "Ubuntu · Linux", cmd: "$ sudo snap install matcha\n$ matcha" }, + flatpak: { + plat: "Linux", + cmd: "$ flatpak install https://matcha.floatpane.com/matcha.flatpakref\n$ matcha", + }, + aur: { plat: "Arch Linux", cmd: "$ yay -S matcha-client-bin\n$ matcha" }, + nix: { + plat: "NixOS · any Nix", + cmd: "$ nix profile install github:floatpane/matcha\n$ matcha", + }, +}; + +function Install() { + const [tab, setTab] = useState("brew"); + const [meta, setMeta] = useState({ + version: "0.8.2", + date: "apr 23, 2026", + size: null, + }); + useEffect(() => { + fetch("https://api.github.com/repos/floatpane/matcha/releases/latest") + .then((r) => (r.ok ? r.json() : null)) + .then((d) => { + if (!d) return; + const version = (d.tag_name || "").replace(/^v/, "") || "0.8.2"; + const date = d.published_at + ? new Date(d.published_at) + .toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }) + .toLowerCase() + : "apr 23, 2026"; + const asset = (d.assets || []).find((a) => a.size) || null; + const size = asset + ? (asset.size / (1024 * 1024)).toFixed(1) + " MB" + : null; + setMeta({ version, date, size }); + }) + .catch(() => {}); + }, []); + const t = INSTALL_TABS[tab]; + return ( +
+
+
+
§ install
+

+ One binary. +
+ Pick your package manager. +

+
+

+ No runtime. Ships natively for macOS, Linux, Windows. Source and + issues at{" "} + + github.com/floatpane/matcha + + . +

+
+
+
+ {Object.keys(INSTALL_TABS).map((k) => ( + + ))} +
+ {t.plat} +
+
{t.cmd}
+
+
+ latest {meta.version} · {meta.date} +
+
+ source github.com/floatpane/matcha +
+
+
+
+ ); +} + +function CTA() { + return ( +
+
+
$ _
+

+ Your inbox is waiting +
+ in the terminal. +

+ +
+
+ ); +} + +function Footer() { + return ( + + ); +} + +// ---------- Tweaks ---------- +function Tweaks({ datasetKey, setDatasetKey, visible }) { + if (!visible) return null; + return ( +
+
Tweaks
+
Demo content
+ {Object.entries({ + default: "drew's inbox", + dev: "floatpane dev", + personal: "personal", + }).map(([k, label]) => ( + + ))} +
+ ); +} + +function App() { + const [datasetKey, setDatasetKey] = useState(() => { + try { + return localStorage.getItem("matcha-dataset") || "default"; + } catch { + return "default"; + } + }); + const [tweaksVisible, setTweaksVisible] = useState(false); + + useEffect(() => { + try { + localStorage.setItem("matcha-dataset", datasetKey); + } catch {} + }, [datasetKey]); + + useEffect(() => { + const onMsg = (e) => { + const d = e.data || {}; + if (d.type === "__activate_edit_mode") setTweaksVisible(true); + if (d.type === "__deactivate_edit_mode") setTweaksVisible(false); + }; + window.addEventListener("message", onMsg); + window.parent.postMessage({ type: "__edit_mode_available" }, "*"); + return () => window.removeEventListener("message", onMsg); + }, []); + + useEffect(() => { + window.parent.postMessage( + { type: "__edit_mode_set_keys", edits: { datasetKey } }, + "*", + ); + }, [datasetKey]); + + return ( +
+ + + + + + +
+ +
+ ); +} + +window.MatchaApp = App; diff --git a/public/components/tui.jsx b/public/components/tui.jsx new file mode 100644 index 0000000000000000000000000000000000000000..725f40e6ed9d530db05122c3b7030bdaba89f695 --- /dev/null +++ b/public/components/tui.jsx @@ -0,0 +1,940 @@ +// Interactive TUI email client demo — matches real Matcha layout +const { useState, useEffect, useRef, useCallback, useMemo } = React; + +// ---------- Data: folders, accounts, messages ---------- +const FOLDERS_LIST = [ + "INBOX", + "Archive", + "Deleted Messages", + "Drafts", + "Junk", + "Notes", + "Sent Messages", + "Work", + "Receipts", +]; + +const ACCOUNTS_DEFAULT = [ + { id: "all", label: "ALL", isAll: true }, + { id: "home", label: "sam@proton.me" }, + { id: "work", label: "s.park@kestrel.works" }, + { id: "side", label: "hello@driftnotes.xyz" }, + { id: "news", label: "inbox@fastmail.sam" }, +]; + +const MSGS_DEFAULT = [ + { + acct: "work", + from: "GitHub", + subject: + "[kestrel/hedge] PR #184 ready for review — refactor auth middleware", + date: "2 min ago", + }, + { + acct: "home", + from: "Linear", + subject: 'You were assigned KST-219 · "Fix timezone drift in daily digest"', + date: "14 min ago", + }, + { + acct: "home", + from: "Margo Tran", + subject: "re: pickup pottery on saturday?", + date: "1 hour ago", + }, + { + acct: "side", + from: "Stripe", + subject: "Your payout of $642.11 is on its way", + date: "3 hours ago", + }, + { + acct: "news", + from: "Hacker News Digest", + subject: 'Top 10: "Ask HN: what are you running on your homelab in 2026?"', + date: "5 hours ago", + }, + { + acct: "home", + from: "DMV", + subject: "Appointment confirmed — Tue May 12, 10:45 AM", + date: "8 hours ago", + }, + { + acct: "work", + from: "Figma", + subject: 'Ines shared a file with you: "Hedge · onboarding v3"', + date: "Yesterday", + }, + { + acct: "home", + from: "Alaska Airlines", + subject: "Check in for flight AS 1312 — SEA → SFO", + date: "Yesterday", + }, + { + acct: "news", + from: "The Browser", + subject: "Five long reads for a slow sunday", + date: "Yesterday", + }, + { + acct: "work", + from: "Vercel", + subject: "Deploy failed — hedge-app (main) · missing env DATABASE_URL", + date: "21/04/2026 17:12", + }, + { + acct: "side", + from: "Plausible Analytics", + subject: "driftnotes.xyz · 1,204 visitors this week (+18%)", + date: "21/04/2026 09:02", + }, + { + acct: "home", + from: "Spotify", + subject: "Your Discover Weekly is ready", + date: "20/04/2026 08:00", + }, + { + acct: "news", + from: "Metafilter", + subject: 'MeFi Digest — "I finally fixed my kitchen sink (a story)"', + date: "19/04/2026 22:44", + }, + { + acct: "work", + from: "Sentry", + subject: + "[hedge-prod] New issue: TimeoutError in worker.py:128 (×42 events)", + date: "18/04/2026 13:27", + }, + { + acct: "home", + from: "Oliver Kim", + subject: "we are going to regret this tattoo idea (attached)", + date: "17/04/2026 23:10", + }, + { + acct: "home", + from: "Patagonia", + subject: "Your Nano Puff has shipped — arriving Thu Apr 25", + date: "16/04/2026 11:03", + }, + { + acct: "side", + from: "Cloudflare", + subject: "Reminder: driftnotes.xyz renews in 9 days", + date: "15/04/2026 04:18", + }, + { + acct: "work", + from: "Notion", + subject: 'Weekly digest: 4 pages edited in "Kestrel / Engineering"', + date: "14/04/2026 17:55", + }, + { + acct: "news", + from: "arXiv daily", + subject: "cs.DC — 6 new submissions (2026-04-13)", + date: "13/04/2026 06:00", + }, + { + acct: "home", + from: "mom", + subject: "did the package get there ok??", + date: "12/04/2026 19:44", + }, +]; + +const MSGS_DEV = [ + { + acct: "work", + from: "GitHub", + subject: "[kestrel/hedge] PR #184 approved by ines-w", + date: "09:42", + }, + { + acct: "work", + from: "Vercel", + subject: "Deploy succeeded — hedge-app (main) · commit 9f2c31a", + date: "09:18", + }, + { + acct: "work", + from: "Linear", + subject: "KST-219 moved to In Review · assigned to you", + date: "08:55", + }, + { + acct: "work", + from: "Sentry", + subject: "[hedge-prod] Regression: 3 new issues in last 24h", + date: "Yesterday", + }, + { + acct: "work", + from: "Cloudflare", + subject: "Worker deployed: hedge-edge@v4.1.0", + date: "Yesterday", + }, + { + acct: "side", + from: "npm", + subject: "Your package 'haze-schedule' received 1,043 downloads", + date: "Mon", + }, + { + acct: "news", + from: "Hacker News", + subject: "Show HN: A tiny CRDT you can read in one afternoon", + date: "Mon", + }, + { + acct: "home", + from: "Stripe", + subject: "New payout: $428.00 USD to Mercury ••4411", + date: "Mon", + }, + { + acct: "work", + from: "GitHub", + subject: "[kestrel/hedge] Issue #412: wrap long headers in digest", + date: "Sun", + }, + { + acct: "work", + from: "AWS Billing", + subject: "April forecast: $387.22 (-12% vs Mar)", + date: "Sun", + }, + { + acct: "side", + from: "Fly.io", + subject: "Machine restarted in ord: driftnotes-web (oom)", + date: "Sat", + }, + { + acct: "news", + from: "arXiv daily", + subject: "cs.PL — 4 new submissions (2026-04-20)", + date: "Sat", + }, +]; + +const MSGS_PERSONAL = [ + { acct: "home", from: "mom", subject: "did you eat?", date: "12:14" }, + { acct: "home", from: "Margo", subject: "pottery saturday??", date: "09:33" }, + { + acct: "home", + from: "REI", + subject: "Your order has shipped", + date: "10:01", + }, + { + acct: "home", + from: "Strava", + subject: "Your week: 38km · 3 runs", + date: "Yesterday", + }, + { + acct: "home", + from: "Ava (landlord)", + subject: "building — water shutoff Sat 9-11am", + date: "Yesterday", + }, + { + acct: "home", + from: "Goodreads", + subject: "Nadia finished 'The MANIAC'", + date: "Mon", + }, + { + acct: "home", + from: "Calendly", + subject: "New booking: Coffee w/ Oliver — Fri 3pm", + date: "Mon", + }, + { + acct: "home", + from: "your past self", + subject: "rotate the sourdough starter today", + date: "Sun", + }, + { + acct: "home", + from: "Spotify", + subject: "Your Spring Mix is ready", + date: "Sun", + }, + { + acct: "news", + from: "The New Yorker", + subject: "The daily — a long weekend read", + date: "Sat", + }, +]; + +const DATASETS = { + default: { + accounts: ACCOUNTS_DEFAULT, + messages: MSGS_DEFAULT, + label: "inbox", + }, + dev: { accounts: ACCOUNTS_DEFAULT, messages: MSGS_DEV, label: "work" }, + personal: { + accounts: ACCOUNTS_DEFAULT, + messages: MSGS_PERSONAL, + label: "personal", + }, +}; + +const EMAIL_BODIES = { + "re: pickup pottery on saturday?": `yes! saturday works — 11am at the studio? +they said the bowls finally came out of the kiln. + +also margo owes you coffee. i'm bringing cash. + +— m`, + "Hello world": `Hello world! + +this is a simple greeting from nowhere in particular. +just confirming the new inbox is working as expected. + +cheers, +m`, +}; + +function bodyFor(msg) { + if (EMAIL_BODIES[msg.subject]) return EMAIL_BODIES[msg.subject]; + return `[no plain-text part] + +This message has no rendered body in the demo. +Press esc to return to the inbox.`; +} + +// ---------- Watermark (original — desert + running figure, NOT copyrighted) ---------- +function Watermark() { + return ( + + ); +} + +// ---------- Selection indicator char ---------- +function cursor(i, cur, visual, vStart) { + if (visual && vStart != null) { + const lo = Math.min(cur, vStart), + hi = Math.max(cur, vStart); + const inRange = i >= lo && i <= hi; + if (inRange && i === cur) return ">*"; + if (inRange) return " *"; + if (i === cur) return "> "; + return " "; + } + return i === cur ? "> " : " "; +} + +// ---------- TUI component ---------- +function TUI({ datasetKey = "default", onKeyPressed }) { + const dataset = DATASETS[datasetKey] || DATASETS.default; + const [folderIdx, setFolderIdx] = useState(0); + const [acctIdx, setAcctIdx] = useState(0); + const [cur, setCur] = useState(0); + const [mode, setMode] = useState("list"); // "list" | "email" | "filter" | "visual" + const [filter, setFilter] = useState(""); + const [visualStart, setVisualStart] = useState(null); + const [flash, setFlash] = useState(""); + const [deleted, setDeleted] = useState(new Set()); // indices into dataset.messages + const containerRef = useRef(null); + const [focused, setFocused] = useState(false); + + const messages = dataset.messages; + const accounts = dataset.accounts; + const activeAcct = accounts[acctIdx]; + + // Reset on dataset change + useEffect(() => { + setFolderIdx(0); + setAcctIdx(0); + setCur(0); + setMode("list"); + setFilter(""); + setVisualStart(null); + setDeleted(new Set()); + setFlash(`loaded: ${dataset.label}`); + const t = setTimeout(() => setFlash(""), 1400); + return () => clearTimeout(t); + }, [datasetKey]); + + // Visible messages + const visible = useMemo(() => { + let list = messages + .map((m, origIdx) => ({ ...m, origIdx })) + .filter((m) => !deleted.has(m.origIdx)); + if (folderIdx === 0) { + if (!activeAcct.isAll) + list = list.filter((m) => m.acct === activeAcct.id); + } else { + // Other folders are empty in the demo + list = []; + } + if (filter.trim()) { + const q = filter.toLowerCase(); + list = list.filter( + (m) => + m.subject.toLowerCase().includes(q) || + m.from.toLowerCase().includes(q), + ); + } + return list; + }, [messages, deleted, folderIdx, activeAcct, filter]); + + const selected = visible[cur] || null; + + const focusMe = useCallback(() => { + if (containerRef.current) containerRef.current.focus(); + }, []); + + const flashFor = (msg, ms = 1200) => { + setFlash(msg); + setTimeout(() => setFlash((f) => (f === msg ? "" : f)), ms); + }; + + const doDelete = (indices) => { + setDeleted((prev) => { + const next = new Set(prev); + indices.forEach((i) => next.add(i)); + return next; + }); + flashFor( + `✓ deleted ${indices.length} message${indices.length === 1 ? "" : "s"}`, + ); + }; + + // Key handling + const handleKey = (e) => { + if (onKeyPressed) onKeyPressed(); + const k = e.key; + + if (mode === "filter") { + if (k === "Enter" || k === "Escape") { + e.preventDefault(); + setMode("list"); + flashFor(filter ? `filter: "${filter}"` : "filter cleared"); + return; + } + if (k === "Backspace") { + e.preventDefault(); + setFilter((f) => f.slice(0, -1)); + return; + } + if (k.length === 1) { + e.preventDefault(); + setFilter((f) => f + k); + return; + } + return; + } + + if (mode === "email") { + if (k === "Escape") { + e.preventDefault(); + setMode("list"); + return; + } + if (k === "r") { + e.preventDefault(); + flashFor("⇢ reply (composer)"); + return; + } + if (k === "f") { + e.preventDefault(); + flashFor("⇢ forward"); + return; + } + if (k === "d") { + e.preventDefault(); + if (selected) { + doDelete([selected.origIdx]); + setMode("list"); + setCur((c) => Math.max(0, Math.min(c, visible.length - 2))); + } + return; + } + if (k === "a") { + e.preventDefault(); + flashFor("▤ archived"); + setMode("list"); + return; + } + if (k === "i") { + e.preventDefault(); + flashFor("◧ images toggled"); + return; + } + return; + } + + if (mode === "visual") { + if (k === "v" || k === "Escape") { + e.preventDefault(); + setMode("list"); + setVisualStart(null); + return; + } + if (k === "j" || k === "ArrowDown") { + e.preventDefault(); + setCur((c) => Math.min(c + 1, visible.length - 1)); + return; + } + if (k === "k" || k === "ArrowUp") { + e.preventDefault(); + setCur((c) => Math.max(c - 1, 0)); + return; + } + if (k === "d") { + e.preventDefault(); + const lo = Math.min(cur, visualStart), + hi = Math.max(cur, visualStart); + const indices = visible.slice(lo, hi + 1).map((m) => m.origIdx); + doDelete(indices); + setMode("list"); + setVisualStart(null); + setCur((c) => + Math.max(0, Math.min(lo, visible.length - indices.length - 1)), + ); + return; + } + if (k === "a") { + e.preventDefault(); + flashFor("▤ archived batch"); + setMode("list"); + setVisualStart(null); + return; + } + if (k === "m") { + e.preventDefault(); + flashFor("⇢ move to folder…"); + setMode("list"); + setVisualStart(null); + return; + } + return; + } + + // list mode + if (k === "Escape") { + e.preventDefault(); + flashFor("← main menu"); + return; + } + if (k === "/") { + e.preventDefault(); + setMode("filter"); + setFilter(""); + return; + } + if (k === "j" || k === "ArrowDown") { + e.preventDefault(); + setCur((c) => Math.min(c + 1, visible.length - 1)); + return; + } + if (k === "k" || k === "ArrowUp") { + e.preventDefault(); + setCur((c) => Math.max(c - 1, 0)); + return; + } + if (k === "h" || k === "ArrowLeft") { + e.preventDefault(); + setAcctIdx((i) => (i - 1 + accounts.length) % accounts.length); + setCur(0); + return; + } + if (k === "l" || k === "ArrowRight") { + e.preventDefault(); + setAcctIdx((i) => (i + 1) % accounts.length); + setCur(0); + return; + } + if (k === "Tab" && !e.shiftKey) { + e.preventDefault(); + setFolderIdx((i) => (i + 1) % FOLDERS_LIST.length); + setCur(0); + return; + } + if (k === "Tab" && e.shiftKey) { + e.preventDefault(); + setFolderIdx((i) => (i - 1 + FOLDERS_LIST.length) % FOLDERS_LIST.length); + setCur(0); + return; + } + if (k === "Enter") { + e.preventDefault(); + if (selected) setMode("email"); + return; + } + if (k === "v") { + e.preventDefault(); + if (visible.length) { + setMode("visual"); + setVisualStart(cur); + } + return; + } + if (k === "d") { + e.preventDefault(); + if (selected) { + doDelete([selected.origIdx]); + setCur((c) => Math.max(0, Math.min(c, visible.length - 2))); + } + return; + } + if (k === "a") { + e.preventDefault(); + flashFor("▤ archived"); + return; + } + if (k === "r") { + e.preventDefault(); + flashFor("↻ inbox refreshed"); + return; + } + if (k === "q") { + e.preventDefault(); + flashFor("quit"); + return; + } + }; + + const titleSuffix = + mode === "visual" + ? ` - VISUAL (${Math.abs(cur - (visualStart ?? cur)) + 1} selected)` + : ""; + const folderLabel = FOLDERS_LIST[folderIdx]; + const isInbox = folderIdx === 0; + const acctLabel = activeAcct.isAll ? "All Accounts" : activeAcct.label; + + return ( +
setFocused(true)} + onBlur={() => setFocused(false)} + onClick={focusMe} + > + {/* Title bar */} +
+
+ + + + + + matcha — kai@floatpane +
+
+ · + 80×24 +
+
+ + {/* Body: sidebar + main pane */} +
+ {/* Sidebar */} +
+
Drew Smirnoff
+ {FOLDERS_LIST.map((f, i) => ( +
{ + setFolderIdx(i); + setCur(0); + focusMe(); + }} + > + {f} +
+ ))} +
+ + {/* Main */} +
+ {mode !== "email" ? ( + <> + {/* Account tabs */} +
+ {accounts.map((a, i) => ( +
{ + setAcctIdx(i); + setCur(0); + focusMe(); + }} + > + {a.label} +
+ ))} +
+ + {/* Header line */} +
+ {isInbox ? ( + <> + INBOX — {acctLabel} + {titleSuffix} + + ) : ( + <> + {folderLabel} + {titleSuffix} + + )} +
+
+ {isInbox + ? `${visible.length} ${visible.length === 1 ? "email" : "emails"}` + : "folder empty"} + {filter && /{filter}} +
+ + {/* Message rows */} +
+ {visible.length === 0 && ( +
— no messages —
+ )} + {visible.map((m, i) => { + const acct = accounts.find((a) => a.id === m.acct); + return ( +
{ + setCur(i); + focusMe(); + }} + onDoubleClick={() => { + setCur(i); + setMode("email"); + focusMe(); + }} + > + + {cursor(i, cur, mode === "visual", visualStart)} + + + {String(i + 1).padStart(2, " ")}. + + + [{acct ? acct.label : m.acct}] + + + {m.from} + + {m.subject} + {m.date} +
+ ); + })} +
• • • •
+
+ + + + ) : ( + a.id === selected?.acct)} + /> + )} +
+
+ + {/* Filter input */} + {mode === "filter" && ( +
+ / + {filter} + +
+ )} + + {/* Flash */} + {flash &&
{flash}
} + + {/* Status line */} +
+ {mode === "email" ? ( + <> + ↑↓ r: reply + ↵ f: forward + ● d: delete + □ a: archive + ⇥ tab: focus attachments + ⎋ esc: back to inbox + ⚫ i: toggle images + + ) : mode === "visual" ? ( + <> + -- VISUAL -- + j/k expand + d delete + a archive + m move + v/esc exit + + ) : mode === "filter" ? ( + <> + -- FILTER -- + enter apply + esc cancel + + ) : ( + <> + ↑/k up + ↓/j down + / filter + v visual mode + ● d delete + □ a archive + ◇ r refresh + ←/h prev tab + →/l next tab + tab next folder + shift+tab prev folder + m move + q quit + + )} +
+ + {/* Focus hint */} + {!focused && ( +
+ click · try j + k tab / +
+ )} +
+ ); +} + +function EmailView({ msg, acct }) { + if (!msg) return null; + const body = bodyFor(msg); + const from = acct ? `<${acct.label}>` : ""; + return ( +
+
+ To: {acct ? acct.label : "you"} | From: {msg.from} {from} | Subject:{" "} + {msg.subject} +
+
+ {body.split("\n").map((line, i) => { + // highlight 'this guy' style blue token if present + const parts = line.split(/(this guy)/); + return ( +
+ {parts.map((p, j) => + p === "this guy" ? ( + + {p} + + ) : ( + {p || "\u00a0"} + ), + )} +
+ ); + })} +
+ +
+ ); +} + +window.MatchaTUI = TUI; diff --git a/public/contact/index.html b/public/contact/index.html deleted file mode 100644 index a597f3207ebbb83a192e0d631df6cd661a407a28..0000000000000000000000000000000000000000 --- a/public/contact/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - Redirecting... - - -

Redirecting to contact...

- - diff --git a/public/features/index.html b/public/features/index.html deleted file mode 100644 index d532a4083b087850e109f758cf62549670762047..0000000000000000000000000000000000000000 --- a/public/features/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - Redirecting to Features... - - -

Redirecting to features...

- - diff --git a/public/get-started/index.html b/public/get-started/index.html deleted file mode 100644 index 231efa245813e697dcc5029732f31a26c83f2bfe..0000000000000000000000000000000000000000 --- a/public/get-started/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Redirecting... - - - -

Redirecting to installation...

- - diff --git a/public/index.html b/public/index.html index f13d0b7308fc4af95f92892e02b819b8af04484e..451adb87a03511a594cd0f5da9166e9f7831b76f 100644 --- a/public/index.html +++ b/public/index.html @@ -1,1644 +1,56 @@ - - - Matcha - Terminal Email Client + + + Matcha — a TUI email client, by Floatpane - + - - - - - -
-
-
- -
- -
- -
-
-
- - Open Source · MIT Licensed -
- -

- Email belongs in
- your terminal. -

- -

- Matcha is a fast, keyboard-driven email client that lives in - your terminal. Built with Go and designed for developers who - never want to leave the command line. -

- - - -
-
-
- -
- matcha - -
-
-
- ~ - $ - matcha - | -
-
-
-
-
- -
-
-
- - - - - Gmail -
-
-
- - - - iCloud -
-
-
- - - - - Custom IMAP -
-
-
- Go - Written in Go -
-
-
- MIT - Open Source -
-
-
- -
-
- -

- Everything you need.
Nothing you don't. -

-
- -
-
-
- - - - -
-

Full Inbox Management

-

- Read, compose, reply, and manage your emails - entirely from the terminal. Beautiful formatting, - attachment support, and lightning-fast navigation. -

-
- -
-
- - - - -
-

Compose & Send

-

- Intuitive compose interface with tab-based field - navigation. Write and send emails without touching a - browser. -

-
- -
-
- - - - - - -
-

Multiple Accounts

-

- Manage all your email accounts in one place. Switch - between them seamlessly. -

-
- -
-
- - - - - -
-

Keyboard-First

-

- Vim-inspired keybindings. Navigate, select, and - manage everything without ever touching the mouse. -

-
- -
-
- - - -
-

Blazing Fast

-

- Local email caching means instant load times. No - Electron, no bloat -- just pure terminal speed. -

-
- -
-
- - - - -
-

Private & Secure

-

- Your credentials stay on your machine. No telemetry, - no tracking, no cloud middleman. -

-
- -
-
- - - - - - -
-

Contacts

-

- Save and manage contacts locally with auto-complete - support when composing. -

-
- -
-
- - - - - -
-

Beautiful TUI

-

- Built with Bubble Tea. A clean, modern interface - that makes terminal email a pleasure. -

-
-
-
- -
-
- -

- Up and running
in seconds. -

-
- -
-
-
-

Homebrew

- Recommended -
-
-
- Terminal - -
-
-
- $brew install - floatpane/matcha/matcha -
-
- $matcha -
-
-
-
- -
-

Build from Source

-
-
- Terminal - -
-
-
- $git clone - https://github.com/floatpane/matcha.git -
-
- $cd matcha && make build -
-
- $matcha -
-
-
-
-
- -

- Need help setting up? Check the - documentation - for detailed guides. -

-
- -
-
-

Ready to ditch the browser
for your email?

-

- Join developers who manage their inbox from the - terminal. Star the project to show your support and stay - updated. -

- - -
-
- -
- -
+ + + + +
+ + + + + + + + + diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000000000000000000000000000000000000..da09bd383bf0aff3375f0a22573a211bfd73c725 --- /dev/null +++ b/public/styles.css @@ -0,0 +1,1126 @@ +/* ============ Matcha site styles ============ */ +:root { + /* Dark blue-green terminal, like the real Matcha screenshots */ + --bg: #05080a; + --bg-2: #060a0d; + --panel: #0a1013; + --panel-2: #0e1619; + --line: #14222b; + --line-2: #1d3441; + --ink: #c8d6d8; + --ink-dim: #7d9099; + --ink-dim2: #52666e; + + /* green accent — slightly darker, per request */ + --accent: #6aa84f; + --accent-2: #85c26a; + --accent-deep: #3b6b28; + --green-soft: #8fb88b; + + --blue: #5aa3d8; + --yellow: #d1b85a; + + --mono: "IBM Plex Mono", ui-monospace, "JetBrains Mono", Menlo, monospace; + --sans: + "IBM Plex Sans", ui-sans-serif, -apple-system, system-ui, sans-serif; + --serif: "IBM Plex Serif", ui-serif, Georgia, serif; +} + +* { + box-sizing: border-box; +} +html, +body { + margin: 0; + padding: 0; + background: var(--bg); + color: var(--ink); + font-family: var(--sans); + font-size: 15px; + line-height: 1.55; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} +body::before { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 0; + background: + radial-gradient( + 1000px 500px at 85% -10%, + rgba(106, 168, 79, 0.06), + transparent 60% + ), + radial-gradient( + 800px 400px at -10% 30%, + rgba(106, 168, 79, 0.03), + transparent 60% + ); +} +a { + color: inherit; + text-decoration: none; +} +code, +kbd, +pre { + font-family: var(--mono); +} +em { + font-style: italic; + color: var(--accent); + font-family: var(--serif); + font-weight: 500; +} +.dim { + color: var(--ink-dim); +} +.underline-link { + color: var(--accent); + border-bottom: 1px dashed var(--accent-deep); + padding-bottom: 1px; +} +.underline-link:hover { + color: var(--accent-2); +} + +.site { + position: relative; + z-index: 1; + max-width: 1280px; + margin: 0 auto; + padding: 0 32px; +} + +/* ---------- Nav ---------- */ +.nav { + display: flex; + align-items: center; + justify-content: space-between; + padding: 22px 0; + border-bottom: 1px solid var(--line); + position: sticky; + top: 0; + background: rgba(5, 8, 10, 0.82); + backdrop-filter: blur(12px); + z-index: 50; +} +.nav-brand { + display: flex; + align-items: center; +} +.wordmark { + display: inline-flex; + align-items: center; + gap: 8px; + font-family: var(--mono); + font-size: 15px; + letter-spacing: -0.01em; +} +.wm-bracket { + color: var(--accent); + font-size: 12px; + transform: translateY(-1px); +} +.wm-name { + color: var(--ink); + font-weight: 500; +} +.wm-dim { + color: var(--ink-dim2); + font-size: 12px; +} + +.wm-logo { + display: flex; + align-items: center; + align-self: center; +} + +.nav-links { + display: flex; + gap: 28px; + align-items: center; + font-family: var(--mono); + font-size: 13px; +} +.nav-links a { + color: var(--ink-dim); + transition: color 0.12s; +} +.nav-links a:hover { + color: var(--ink); +} +.nav-github { + display: inline-flex; + align-items: center; + gap: 10px; +} +.nav-star { + color: var(--accent); + font-size: 12px; +} +.nav-right .btn { + font-family: var(--mono); + font-size: 13px; +} + +/* ---------- Buttons ---------- */ +.btn { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 10px 16px; + border: 1px solid var(--line-2); + border-radius: 2px; + font-family: var(--mono); + font-size: 13px; + color: var(--ink); + background: transparent; + cursor: pointer; + transition: all 0.12s; +} +.btn:hover { + border-color: var(--accent); + color: var(--accent); +} +.btn-k { + font-size: 11px; + color: var(--ink-dim); +} +.btn:hover .btn-k { + color: var(--accent); +} +.btn-primary { + background: var(--accent); + color: #05080a; + border-color: var(--accent); + font-weight: 500; +} +.btn-primary:hover { + background: var(--accent-2); + border-color: var(--accent-2); + color: #05080a; +} +.btn-primary .btn-k { + color: #05080a; + opacity: 0.6; +} +.btn-ghost { + background: transparent; +} +.btn-lg { + padding: 14px 22px; + font-size: 14px; +} + +/* ---------- Hero ---------- */ +.hero { + display: grid; + grid-template-columns: 1fr 1.35fr; + gap: 56px; + padding: 72px 0 80px; + align-items: start; +} +.hero-copy { + padding-top: 24px; +} +.hero-eyebrow { + display: inline-flex; + align-items: center; + gap: 10px; + font-family: var(--mono); + font-size: 12px; + color: var(--ink-dim); + padding: 6px 12px; + border: 1px solid var(--line); + border-radius: 2px; + margin-bottom: 28px; +} +.dot-live { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--accent); + box-shadow: 0 0 0 3px rgba(106, 168, 79, 0.2); + animation: pulse 2.4s ease-in-out infinite; +} +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.4; + } +} +.hero-h1 { + font-family: var(--sans); + font-size: 56px; + font-weight: 500; + letter-spacing: -0.025em; + line-height: 1.04; + margin: 0 0 24px; + text-wrap: pretty; +} +.hero-sub { + font-size: 17px; + color: var(--ink-dim); + max-width: 46ch; + margin: 0 0 36px; + line-height: 1.55; +} +.hero-cta { + display: flex; + gap: 12px; + margin-bottom: 44px; +} +.hero-meta { + display: grid; + grid-template-columns: repeat(3, auto); + gap: 32px; + font-family: var(--mono); + font-size: 12px; + color: var(--ink); +} +.hero-meta .dim { + display: block; + color: var(--ink-dim2); + font-size: 11px; + margin-bottom: 2px; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.hero-demo { + position: relative; +} +.hero-demo-chrome { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 0 10px; + font-family: var(--mono); + font-size: 12px; +} +.hero-demo-label { + display: flex; + align-items: center; + gap: 8px; + color: var(--ink-dim); +} +.demo-swap { + background: transparent; + border: 1px solid var(--line); + color: var(--ink-dim); + padding: 3px 8px; + font-family: var(--mono); + font-size: 11px; + border-radius: 2px; + cursor: pointer; + transition: all 0.12s; +} +.demo-swap:hover { + color: var(--ink); + border-color: var(--line-2); +} +.demo-swap.on { + color: var(--accent); + border-color: var(--accent); +} +.hero-demo-hint { + color: var(--ink-dim); +} +.hero-demo-hint kbd { + display: inline-block; + margin: 0 2px; + padding: 1px 5px; + border: 1px solid var(--line-2); + border-radius: 2px; + font-size: 10px; + color: var(--ink); +} + +@media (max-width: 960px) { + .hero { + grid-template-columns: 1fr; + } + .hero-h1 { + font-size: 40px; + } +} + +/* ---------- TUI — matches real Matcha layout ---------- */ +.tui-root { + position: relative; + background: var(--bg); + border: 1px solid var(--line-2); + border-radius: 6px; + overflow: hidden; + font-family: var(--mono); + font-size: 12px; + line-height: 1.5; + color: var(--ink); + outline: none; + box-shadow: + 0 1px 0 rgba(255, 255, 255, 0.02) inset, + 0 30px 80px -20px rgba(0, 0, 0, 0.7), + 0 10px 30px -10px rgba(106, 168, 79, 0.08); + user-select: none; + transition: + box-shadow 0.2s, + border-color 0.2s; + min-height: 520px; +} +.tui-root.is-focused { + border-color: var(--accent); + box-shadow: + 0 0 0 1px var(--accent), + 0 30px 80px -20px rgba(0, 0, 0, 0.7), + 0 10px 30px -10px rgba(106, 168, 79, 0.2); +} + +.tui-titlebar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + background: #02050a; + border-bottom: 1px solid var(--line); + font-size: 11px; + color: var(--ink-dim); +} +.tui-titlebar-left { + display: flex; + align-items: center; + gap: 10px; +} +.tui-traffic { + display: inline-flex; + gap: 6px; +} +.tl { + width: 11px; + height: 11px; + border-radius: 50%; + display: inline-block; +} +.tl-r { + background: #2a1414; +} +.tl-y { + background: #2a2514; +} +.tl-g { + background: #0f2a16; +} +.tui-title-text { + color: var(--ink-dim); +} +.tui-titlebar-right { + display: flex; + gap: 8px; +} +.tui-titlebar-right .sep { + color: var(--ink-dim2); +} + +.tui-shell { + display: grid; + grid-template-columns: 160px 1fr; + min-height: 460px; +} + +/* Sidebar — folders */ +.tui-sidebar { + background: var(--bg-2); + padding: 10px 0; + border-right: 1px solid var(--line); + font-size: 12px; +} +.tui-sidebar-head { + padding: 2px 14px 10px; + color: var(--accent); + font-weight: 500; +} +.tui-folder { + padding: 2px 14px; + color: var(--ink-dim); + cursor: pointer; + white-space: nowrap; +} +.tui-folder:hover { + color: var(--ink); +} +.tui-folder.active { + background: var(--accent); + color: #05080a; + font-weight: 500; +} + +/* Main pane */ +.tui-main { + position: relative; + padding: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* Account tabs row */ +.tui-acct-row { + display: flex; + gap: 22px; + padding: 8px 16px; + border-bottom: 1px solid var(--line); + font-size: 12px; +} +.tui-acct { + color: var(--ink-dim); + cursor: pointer; + padding: 2px 0; +} +.tui-acct:hover { + color: var(--ink); +} +.tui-acct.active { + color: var(--accent); + border-bottom: 1px solid var(--accent); + padding-bottom: 1px; +} + +.tui-heading { + padding: 14px 16px 2px; + color: var(--accent); + font-weight: 500; + font-size: 12.5px; +} +.tui-subhead { + padding: 0 16px 8px; + color: var(--ink-dim); + font-size: 11.5px; +} +.tui-filter-chip { + color: var(--yellow); + margin-left: 10px; +} + +.tui-list { + flex: 1; + padding: 2px 16px 40px; + position: relative; + z-index: 2; +} + +.tui-row { + display: grid; + grid-template-columns: 18px 26px auto 14px auto 14px 1fr auto; + gap: 6px; + align-items: baseline; + padding: 1px 0; + cursor: pointer; + color: var(--ink-dim); + white-space: nowrap; + font-size: 12px; +} +.tui-row:hover { + color: var(--ink); +} +.tui-row.cur { + color: var(--accent); + background: rgba(106, 168, 79, 0.04); +} +.tui-row.visual.cur { + color: var(--yellow); +} +.tui-row-cursor { + color: var(--accent); + white-space: pre; + font-weight: 600; +} +.tui-row.visual .tui-row-cursor { + color: var(--yellow); +} +.tui-row-num { + color: var(--ink-dim2); +} +.tui-row-acct { + color: var(--blue); +} +.tui-row.cur .tui-row-acct { + color: var(--blue); +} +.tui-row-sep { + color: var(--ink-dim2); +} +.tui-row-from { + color: var(--ink); +} +.tui-row.cur .tui-row-from { + color: var(--accent); +} +.tui-row-subj { + overflow: hidden; + text-overflow: ellipsis; + color: var(--ink-dim); +} +.tui-row.cur .tui-row-subj { + color: var(--accent-2); +} +.tui-row-date { + color: var(--ink-dim2); + padding-left: 12px; +} +.tui-row.cur .tui-row-date { + color: var(--green-soft); +} +.tui-row-dots { + color: var(--ink-dim2); + padding-top: 12px; + letter-spacing: 4px; +} +.tui-row-empty { + color: var(--ink-dim2); + padding: 32px 0; + text-align: center; +} + +/* Email view */ +.tui-email { + position: relative; + flex: 1; + padding: 0; + display: flex; + flex-direction: column; +} +.tui-email-head { + padding: 10px 16px; + color: var(--ink); + border-bottom: 1px solid var(--line); + font-size: 12px; +} +.tui-email-body { + padding: 16px; + flex: 1; + color: var(--ink); + position: relative; + z-index: 2; +} +.tui-email-line { + white-space: pre-wrap; +} +.tui-link { + color: var(--blue); + border-bottom: 1px solid var(--blue); + cursor: pointer; +} + +/* Watermark */ +.tui-watermark { + position: absolute; + left: 0; + right: 0; + bottom: 30px; + width: 100%; + height: 60%; + color: var(--ink-dim2); + pointer-events: none; + z-index: 1; + opacity: 0.55; +} + +/* Flash */ +.tui-flash { + position: absolute; + top: 52px; + right: 16px; + background: rgba(5, 8, 10, 0.94); + border: 1px solid var(--accent); + color: var(--accent); + padding: 4px 10px; + font-size: 11px; + border-radius: 2px; + z-index: 6; +} + +/* Status / legend line — dim like the real TUI */ +.tui-status { + display: flex; + flex-wrap: wrap; + gap: 0 14px; + padding: 6px 16px; + border-top: 1px solid var(--line); + background: var(--bg-2); + color: var(--ink-dim2); + font-size: 11px; + min-height: 26px; +} +.tui-status .seg { + color: var(--ink-dim); + white-space: nowrap; +} +.tui-status .seg.hi { + color: var(--yellow); +} + +/* Filter command line */ +.tui-cmdline { + position: absolute; + bottom: 26px; + left: 0; + right: 0; + padding: 5px 12px; + background: #02050a; + color: var(--accent); + border-top: 1px solid var(--accent); + font-size: 12px; + display: flex; + align-items: center; + gap: 4px; + z-index: 5; +} +.tui-cmdline-prompt { + color: var(--accent); +} +.tui-cmdline-buf { + color: var(--ink); +} +.tui-caret { + display: inline-block; + width: 7px; + height: 13px; + background: var(--accent); + margin-left: 2px; + transform: translateY(2px); + animation: blink 1s step-end infinite; +} +@keyframes blink { + 50% { + opacity: 0; + } +} + +.tui-focus-hint { + position: absolute; + top: 48px; + right: 16px; + padding: 5px 10px; + background: rgba(5, 8, 10, 0.9); + border: 1px solid var(--line-2); + border-radius: 2px; + font-size: 11px; + color: var(--ink-dim); + pointer-events: none; + z-index: 3; +} +.tui-focus-hint kbd { + display: inline-block; + margin: 0 1px; + padding: 0 4px; + border: 1px solid var(--line-2); + background: var(--panel); + border-radius: 2px; + color: var(--ink); +} + +/* ---------- Sections ---------- */ +.section-head { + display: grid; + grid-template-columns: 1.2fr 1fr; + gap: 48px; + padding: 80px 0 40px; + border-top: 1px solid var(--line); + align-items: end; +} +.section-eyebrow { + font-family: var(--mono); + font-size: 12px; + color: var(--accent); + margin-bottom: 16px; + letter-spacing: 0.04em; +} +.section-h2 { + font-size: 44px; + font-weight: 500; + letter-spacing: -0.025em; + line-height: 1.08; + margin: 0; + text-wrap: pretty; +} +.section-h2 .dim { + color: var(--ink-dim); +} +.section-head-r { + color: var(--ink-dim); + font-size: 15px; + max-width: 44ch; + margin: 0; +} +.section-head-r code { + font-size: 13px; + color: var(--accent); + background: var(--panel); + padding: 1px 5px; + border-radius: 2px; +} + +/* Features grid */ +.feature-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1px; + background: var(--line); + border: 1px solid var(--line); + margin-top: 24px; +} +.feature { + background: var(--bg); + padding: 32px 28px; + min-height: 240px; + display: flex; + flex-direction: column; + transition: background 0.2s; +} +.feature:hover { + background: var(--bg-2); +} +.feature-head { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 24px; + font-family: var(--mono); + font-size: 12px; + color: var(--accent); +} +.feature-dash { + color: var(--ink-dim2); +} +.feature-title { + font-family: var(--sans); + font-size: 20px; + font-weight: 500; + letter-spacing: -0.01em; + margin: 0 0 10px; + line-height: 1.2; +} +.feature-body { + color: var(--ink-dim); + font-size: 14px; + margin: 0 0 20px; + flex: 1; + line-height: 1.55; +} +.feature-mono { + margin: 0; + background: var(--panel-2); + border: 1px solid var(--line-2); + padding: 10px 12px; + font-size: 11.5px; + color: var(--accent); + border-radius: 2px; + white-space: pre-wrap; +} +@media (max-width: 960px) { + .feature-grid { + grid-template-columns: 1fr; + } + .section-head { + grid-template-columns: 1fr; + gap: 20px; + } + .section-h2 { + font-size: 32px; + } +} + +/* Keybinds */ +.keybinds-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 40px 32px; + margin-top: 24px; + padding: 32px 0 80px; +} +.keybinds-col-head { + font-family: var(--mono); + font-size: 12px; + color: var(--accent); + margin-bottom: 16px; + letter-spacing: 0.04em; +} +.keybind-row { + display: flex; + align-items: baseline; + gap: 8px; + margin-bottom: 8px; + font-family: var(--mono); + font-size: 12.5px; +} +.keybind-k kbd { + display: inline-block; + padding: 1px 8px; + border: 1px solid var(--line-2); + background: var(--panel); + color: var(--ink); + border-radius: 2px; + font-size: 11.5px; + white-space: nowrap; +} +.keybind-dots { + color: var(--line-2); + flex: 1; + overflow: hidden; + white-space: nowrap; +} +.keybind-label { + color: var(--ink-dim); +} +@media (max-width: 960px) { + .keybinds-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +/* Install */ +.install-card { + background: var(--bg-2); + border: 1px solid var(--line-2); + border-radius: 4px; + overflow: hidden; + margin-bottom: 80px; +} +.install-tabs { + display: flex; + align-items: center; + border-bottom: 1px solid var(--line); + background: var(--bg); + padding: 0 8px; + overflow-x: auto; +} +.install-tab { + background: transparent; + border: none; + padding: 12px 16px; + color: var(--ink-dim); + font-family: var(--mono); + font-size: 13px; + cursor: pointer; + border-bottom: 2px solid transparent; + transition: all 0.12s; + white-space: nowrap; +} +.install-tab:hover { + color: var(--ink); +} +.install-tab.active { + color: var(--accent); + border-bottom-color: var(--accent); +} +.install-tabs-spacer { + flex: 1; +} +.install-plat { + font-family: var(--mono); + font-size: 11px; + color: var(--ink-dim2); + padding-right: 12px; + white-space: nowrap; +} +.install-code { + margin: 0; + padding: 36px 28px; + font-size: 15px; + color: var(--accent); + background: var(--bg-2); + white-space: pre-wrap; + line-height: 1.7; + overflow-x: auto; +} +.install-foot { + display: flex; + gap: 32px; + flex-wrap: wrap; + padding: 12px 28px; + border-top: 1px solid var(--line); + background: var(--bg); + font-family: var(--mono); + font-size: 12px; + color: var(--ink); +} +.install-foot .dim { + color: var(--ink-dim2); + text-transform: uppercase; + letter-spacing: 0.06em; + font-size: 10px; + margin-right: 6px; +} + +/* CTA */ +.cta { + border-top: 1px solid var(--line); + border-bottom: 1px solid var(--line); + margin-top: 20px; + padding: 100px 0; + text-align: center; + background: repeating-linear-gradient( + 0deg, + transparent 0 26px, + rgba(106, 168, 79, 0.02) 26px 27px + ); +} +.cta-pre { + font-family: var(--mono); + color: var(--accent); + font-size: 14px; + margin-bottom: 16px; +} +.cta-h2 { + font-size: 60px; + font-weight: 500; + letter-spacing: -0.03em; + line-height: 1.04; + margin: 0 0 36px; +} +.cta-row { + display: flex; + gap: 14px; + justify-content: center; + flex-wrap: wrap; +} +@media (max-width: 960px) { + .cta-h2 { + font-size: 36px; + } +} + +/* Footer */ +.footer { + padding: 64px 0 32px; +} +.footer-top { + display: grid; + grid-template-columns: 1.2fr 3fr; + gap: 48px; + padding-bottom: 48px; + border-bottom: 1px solid var(--line); +} +.footer-tag { + color: var(--ink-dim); + font-size: 13px; + margin: 16px 0 0; +} +.footer-cols { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 32px; +} +.footer-h { + font-family: var(--mono); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--ink-dim2); + margin-bottom: 12px; +} +.footer-cols a { + display: block; + color: var(--ink-dim); + font-size: 13px; + padding: 4px 0; + cursor: pointer; +} +.footer-cols a:hover { + color: var(--accent); +} +.footer-bot { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 24px; + font-family: var(--mono); + font-size: 11px; + color: var(--ink-dim2); +} +.footer-copy { + display: flex; + align-items: center; + gap: 10px; + color: var(--ink-dim); +} +.footer-meta { + display: flex; + gap: 8px; +} +.footer-meta .sep { + color: var(--line-2); +} +@media (max-width: 960px) { + .footer-top { + grid-template-columns: 1fr; + } + .footer-cols { + grid-template-columns: repeat(2, 1fr); + } +} + +/* Tweaks */ +.tweaks { + position: fixed; + bottom: 20px; + right: 20px; + background: var(--bg-2); + border: 1px solid var(--accent); + padding: 14px; + border-radius: 4px; + min-width: 220px; + font-family: var(--mono); + box-shadow: + 0 20px 40px -10px rgba(0, 0, 0, 0.6), + 0 0 0 4px rgba(106, 168, 79, 0.06); + z-index: 100; +} +.tweaks-head { + font-size: 11px; + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.12em; + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px dashed var(--line-2); +} +.tweaks-sub { + font-size: 10px; + color: var(--ink-dim2); + text-transform: uppercase; + letter-spacing: 0.08em; + margin-bottom: 8px; +} +.tweaks-opt { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 6px 8px; + background: transparent; + border: none; + color: var(--ink-dim); + font-family: var(--mono); + font-size: 12px; + text-align: left; + cursor: pointer; + border-radius: 2px; +} +.tweaks-opt:hover { + background: var(--panel); + color: var(--ink); +} +.tweaks-opt.on { + color: var(--accent); +} +.tweaks-dot { + color: var(--accent); + width: 10px; +}