fix: drop vim mode, pin CodeMirror to dodge esm.sh outage

esm.sh's build backend was returning HTTP 500 for unbuilt @codemirror/*@6
version ranges (out of disk). A failed top-level module import aborts the
whole <script type=module>, so fillLangs()/loadModels() never ran and the
language/model dropdowns came up empty despite a healthy backend.

- Pin CodeMirror imports to already-built versions (view@6.36.0,
  state@6.7.0, commands@6.10.4), which esm.sh serves fine during the outage.
- Remove vim editing mode entirely (@replit/codemirror-vim import, vim()
  extension, Vim.mapCommand H/L bindings, .cm-vim-panel CSS). The
  document-level Shift-H/L focus cycling is retained.
- Bump to 0.4.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
João Pedro Battistella Nadas
2026-07-05 11:15:07 +02:00
parent fd8f0b74aa
commit 33fc5e0c7d
3 changed files with 16 additions and 34 deletions

View File

@@ -7,6 +7,14 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
## [0.4.0] - 2026-07-05
### Removed
- Vim editing mode. Dropped the `@replit/codemirror-vim` import and its `Vim.mapCommand` H/L focus bindings. The document-level Shift-H/L focus cycling across the dropdowns/buttons is retained (it never depended on vim). One fewer esm.sh dependency.
### Fixed
- Empty language and model dropdowns when esm.sh cannot serve an unpinned `@codemirror/*@6` range (their build backend was returning HTTP 500 on unbuilt versions). A failed top-level module import aborts the whole `<script type="module">`, so `fillLangs()`/`loadModels()` never ran despite a healthy backend. CodeMirror imports are now pinned to already-built versions (`view@6.36.0`, `state@6.7.0`, `commands@6.10.4`).
## [0.3.0] - 2026-06-09 ## [0.3.0] - 2026-06-09
### Added ### Added

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "finrod" name = "finrod"
version = "0.3.0" version = "0.4.0"
description = "Translator UI backed by Ollama" description = "Translator UI backed by Ollama"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [

View File

@@ -474,13 +474,6 @@
} }
.cm-scroller { padding: 0.6rem 0.75rem; } .cm-scroller { padding: 0.6rem 0.75rem; }
.cm-content { caret-color: var(--accent); } .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>
@@ -550,10 +543,9 @@
</button> </button>
<script type="module"> <script type="module">
import { EditorView, keymap, placeholder } from "https://esm.sh/@codemirror/view@6"; import { EditorView, keymap, placeholder } from "https://esm.sh/@codemirror/view@6.36.0";
import { EditorState } from "https://esm.sh/@codemirror/state@6"; import { EditorState } from "https://esm.sh/@codemirror/state@6.7.0";
import { history, defaultKeymap, historyKeymap } from "https://esm.sh/@codemirror/commands@6"; import { history, defaultKeymap, historyKeymap } from "https://esm.sh/@codemirror/commands@6.10.4";
import { vim, 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",
@@ -975,18 +967,13 @@
if (file) runOCR(file); if (file) runOCR(file);
}); });
// --- CodeMirror editor (vim mode on real keyboards only) ---------------- // --- CodeMirror editor --------------------------------------------------
const isCoarsePointer = window.matchMedia("(pointer: coarse)").matches; const isCoarsePointer = window.matchMedia("(pointer: coarse)").matches;
let typingTimer; let typingTimer;
const editor = new EditorView({ const editor = new EditorView({
parent: document.getElementById("input"), parent: document.getElementById("input"),
state: EditorState.create({ state: EditorState.create({
extensions: [ 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(), history(),
keymap.of([ keymap.of([
{ key: "Mod-Enter", run: () => { translate(); return true; } }, { key: "Mod-Enter", run: () => { translate(); return true; } },
@@ -1214,10 +1201,8 @@
goBtn.addEventListener("click", translate); goBtn.addEventListener("click", translate);
// Shift-H / Shift-L cycle focus across controls (vim-style Shift-Tab / // Shift-H / Shift-L cycle focus across controls (vim-style Shift-Tab /
// Tab). Two paths kept in sync: // Tab), for focus on a select / button. Inside the editor they type
// 1. Document keydown listener — fires when focus is on a select / button // normally.
// 2. Vim normal-mode mapping (below) — fires when focus is in the editor
// Together they make Shift-H/L work everywhere, including escaping the editor.
const focusables = [ const focusables = [
{ el: sourceSel, focus: () => sourceSel.focus() }, { el: sourceSel, focus: () => sourceSel.focus() },
{ el: targetSel, focus: () => targetSel.focus() }, { el: targetSel, focus: () => targetSel.focus() },
@@ -1239,23 +1224,12 @@
if (!e.shiftKey) return; if (!e.shiftKey) return;
const k = e.key.toLowerCase(); const k = e.key.toLowerCase();
if (k !== "h" && k !== "l") return; if (k !== "h" && k !== "l") return;
// Inside the editor, vim's mapCommand handles H/L (see below). Letting // Inside the editor, let H/L type normally.
// the event bubble here would double-jump.
if (editor.dom.contains(document.activeElement)) return; if (editor.dom.contains(document.activeElement)) return;
e.preventDefault(); e.preventDefault();
moveFocus(k === "l" ? 1 : -1); moveFocus(k === "l" ? 1 : -1);
}); });
// Override vim's H/L (normally "viewport top/bottom") so they exit the
// editor to the previous/next control, matching the Shift-H/L contract
// the document handler uses elsewhere.
if (!isCoarsePointer) {
Vim.defineAction("finrodFocusPrev", () => moveFocus(-1));
Vim.defineAction("finrodFocusNext", () => moveFocus(1));
Vim.mapCommand("H", "action", "finrodFocusPrev", {}, { context: "normal" });
Vim.mapCommand("L", "action", "finrodFocusNext", {}, { context: "normal" });
}
// Side drawer (Settings) wiring — opens from the top-left hamburger. // Side drawer (Settings) wiring — opens from the top-left hamburger.
const drawerEl = document.getElementById("drawer"); const drawerEl = document.getElementById("drawer");
const drawerBackdrop = document.getElementById("drawer-backdrop"); const drawerBackdrop = document.getElementById("drawer-backdrop");