feat: editor font-size match, hot model default, Shift-H/L nav
- .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.
This commit is contained in:
@@ -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));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user