From 95649009090fc7a16f85ef05ae7b7f1fdb6393e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Battistella=20Nadas?= Date: Tue, 9 Jun 2026 16:22:20 +0200 Subject: [PATCH] feat: editor font-size match, hot model default, Shift-H/L nav MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .cm-editor pinned to font-size 1rem so the input matches the output pane (CodeMirror 6 defaults smaller) - refreshLoaded(isInitial=true) on first load picks the hot model as the dropdown default — avoids the 5-15s disk reload on the first translation. Subsequent refreshes don't override the user's manual selection. - document-level Shift-H / Shift-L cycle focus across controls (source → target → model → editor → button), suppressed when focus is inside CodeMirror so vim's viewport H/L still work. --- CHANGELOG.md | 5 +++++ static/index.html | 41 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19c8ef4..185dfab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Added - Vim bindings in the input via CodeMirror 6 + `@replit/codemirror-vim` (loaded from esm.sh, no build step). Supports `i`/`Esc`, `hjkl`, word/line motions, `dd`/`yy`/`p`, undo/redo, `/` search, `:` command line. Auto-disabled on touch devices (detected via `(pointer: coarse)`) where modal editing fights the virtual keyboard. +- `Shift-H` / `Shift-L` cycle focus across the page controls (source → target → model → editor → button) like `Shift-Tab` / `Tab`. Suppressed inside the editor where vim already owns those motions. +- On page load, the model dropdown defaults to whichever model Ollama already has hot — skips the 5-15 s disk-reload pain on the first translation. After load it follows the user's manual selection. + +### Changed +- Editor font-size pinned to `1rem` to match the output pane (CodeMirror 6 ships smaller by default). ## [0.1.0] - 2026-06-09 diff --git a/static/index.html b/static/index.html index ae5f470..c4a9587 100644 --- a/static/index.html +++ b/static/index.html @@ -163,6 +163,7 @@ border-radius: 6px; background: var(--panel); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 1rem; /* match .output's inherited body size — CM6 defaults are smaller */ } .cm-editor.cm-focused { outline: none; @@ -428,7 +429,7 @@ return `${secs}s`; } - async function refreshLoaded() { + async function refreshLoaded(isInitial = false) { let models = []; try { const r = await fetch("/api/loaded"); @@ -444,6 +445,7 @@ const loadedMap = new Map(models.map(m => [m.name, m.expires_at])); // Update dropdown options. Use textContent (option doesn't render HTML). + const optionValues = Array.from(modelSel.options, o => o.value); for (const opt of modelSel.options) { if (loadedMap.has(opt.value)) { opt.textContent = `● ${opt.value}`; @@ -452,6 +454,14 @@ } } + // On the very first refresh after page load, prefer a hot model as the + // dropdown default — skipping disk reload pain. Only on initial load, + // so the user's manual selection isn't yanked away while they type. + if (isInitial) { + const hot = models.find(m => optionValues.includes(m.name)); + if (hot) modelSel.value = hot.name; + } + // Update status line. if (models.length === 0) { hotStatus.classList.add("idle"); @@ -468,8 +478,35 @@ goBtn.addEventListener("click", translate); + // Shift-H / Shift-L navigate focus across controls (vim-style Shift-Tab / + // Tab). Skipped when focus is inside the CodeMirror editor — there H/L + // are vim's viewport motions and must reach the editor untouched. + const focusables = [ + { el: sourceSel, focus: () => sourceSel.focus() }, + { el: targetSel, focus: () => targetSel.focus() }, + { el: modelSel, focus: () => modelSel.focus() }, + { el: editor.contentDOM, focus: () => editor.focus() }, + { el: goBtn, focus: () => goBtn.focus() }, + ]; + document.addEventListener("keydown", (e) => { + if (!e.shiftKey) return; + const k = e.key.toLowerCase(); + if (k !== "h" && k !== "l") return; + if (editor.dom.contains(document.activeElement)) return; // vim owns H/L in the editor + e.preventDefault(); + const ae = document.activeElement; + let idx = focusables.findIndex(f => f.el === ae); + if (idx === -1) { + focusables[k === "l" ? 0 : focusables.length - 1].focus(); + return; + } + const delta = k === "l" ? 1 : -1; + const next = focusables[(idx + delta + focusables.length) % focusables.length]; + next.focus(); + }); + fillLangs(); - loadModels().then(refreshLoaded); + loadModels().then(() => refreshLoaded(true));