v0.2.0: vim bindings, Shift-H/L focus nav, hot-model default #3
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user