v0.2.0: vim bindings, Shift-H/L focus nav, hot-model default #3

Merged
jpnadas merged 8 commits from feature/vim-bindings into main 2026-06-09 15:22:41 +00:00
2 changed files with 64 additions and 13 deletions
Showing only changes of commit 782c7182f2 - Show all commits

View File

@@ -7,6 +7,9 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
### 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.
## [0.1.0] - 2026-06-09 ## [0.1.0] - 2026-06-09
First release. Stateless translator UI that proxies streaming requests to First release. Stateless translator UI that proxies streaming requests to

View File

@@ -155,6 +155,28 @@
background: var(--border); background: var(--border);
box-shadow: none; box-shadow: none;
} }
/* CodeMirror editor host — matches .output styling so input & output align */
.cm-editor {
min-height: 180px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--panel);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.cm-editor.cm-focused {
outline: none;
border-color: var(--accent);
}
.cm-scroller { padding: 0.6rem 0.75rem; }
.cm-content { caret-color: var(--accent); }
/* vim's command-line / status hint inherits readable colors */
.cm-vim-panel {
background: var(--panel);
color: var(--fg);
border-top: 1px solid var(--border);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
</style> </style>
</head> </head>
<body> <body>
@@ -185,7 +207,7 @@
<div class="panes"> <div class="panes">
<div> <div>
<label for="input">Input</label> <label for="input">Input</label>
<textarea id="input" placeholder="Type or paste text to translate…"></textarea> <div id="input"></div>
</div> </div>
<div> <div>
<label for="output">Translation</label> <label for="output">Translation</label>
@@ -197,7 +219,12 @@
<div id="meta" class="meta"></div> <div id="meta" class="meta"></div>
</div> </div>
<script> <script type="module">
import { EditorView, keymap, placeholder } from "https://esm.sh/@codemirror/view@6";
import { EditorState } from "https://esm.sh/@codemirror/state@6";
import { history, defaultKeymap, historyKeymap } from "https://esm.sh/@codemirror/commands@6";
import { vim } from "https://esm.sh/@replit/codemirror-vim@6";
const LANGUAGES = [ const LANGUAGES = [
"English", "Portuguese (Brazil)", "Portuguese (Portugal)", "Spanish", "English", "Portuguese (Brazil)", "Portuguese (Portugal)", "Spanish",
"French", "German", "Italian", "Dutch", "Polish", "Russian", "French", "German", "Italian", "Dutch", "Polish", "Russian",
@@ -210,13 +237,43 @@
const sourceSel = document.getElementById("source"); const sourceSel = document.getElementById("source");
const targetSel = document.getElementById("target"); const targetSel = document.getElementById("target");
const modelSel = document.getElementById("model"); const modelSel = document.getElementById("model");
const input = document.getElementById("input");
const output = document.getElementById("output"); const output = document.getElementById("output");
const goBtn = document.getElementById("go"); const goBtn = document.getElementById("go");
const meta = document.getElementById("meta"); const meta = document.getElementById("meta");
const hotStatus = document.getElementById("hot-status"); const hotStatus = document.getElementById("hot-status");
const hotStatusText = document.getElementById("hot-status-text"); const hotStatusText = document.getElementById("hot-status-text");
// --- CodeMirror editor (vim mode on real keyboards only) ----------------
const isCoarsePointer = window.matchMedia("(pointer: coarse)").matches;
let typingTimer;
const editor = new EditorView({
parent: document.getElementById("input"),
state: EditorState.create({
extensions: [
// vim() MUST be first per @replit/codemirror-vim docs so its keymap
// takes precedence. Skipped on touch devices — modal editing without
// a hardware Esc key on a virtual keyboard is unusable, and IME
// composition fights the vim keymap.
...(isCoarsePointer ? [] : [vim()]),
history(),
keymap.of([
{ key: "Mod-Enter", run: () => { translate(); return true; } },
...defaultKeymap,
...historyKeymap,
]),
EditorView.lineWrapping,
placeholder("Type or paste text to translate…"),
EditorView.updateListener.of(u => {
if (u.docChanged) {
clearTimeout(typingTimer);
typingTimer = setTimeout(refreshLoaded, 500);
}
}),
],
}),
});
const inputText = () => editor.state.doc.toString();
function fillLangs() { function fillLangs() {
const auto = document.createElement("option"); const auto = document.createElement("option");
auto.value = "auto"; auto.value = "auto";
@@ -276,7 +333,7 @@
} }
async function translate() { async function translate() {
const text = input.value.trim(); const text = inputText().trim();
if (!text) return; if (!text) return;
goBtn.disabled = true; goBtn.disabled = true;
goBtn.textContent = "Translating…"; goBtn.textContent = "Translating…";
@@ -409,16 +466,7 @@
hotStatusText.textContent = `Hot: ${parts.join(", ")}`; hotStatusText.textContent = `Hot: ${parts.join(", ")}`;
} }
let typingTimer;
input.addEventListener("input", () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(refreshLoaded, 500);
});
goBtn.addEventListener("click", translate); goBtn.addEventListener("click", translate);
input.addEventListener("keydown", (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") translate();
});
fillLangs(); fillLangs();
loadModels().then(refreshLoaded); loadModels().then(refreshLoaded);