fix(ui): refresh the open help panel when the language changes - #288
Conversation
The shortcuts panel (`#help-panel`) is a non-modal floating panel — it does
not cover the Settings language select — so the user can switch the language
while it is open. Its heading and close button carry `[data-i18n]` and follow
`applyStatic()`, but the four shortcut rows and the context line are built by
`renderHelp()`, whose only call site is `toggleHelp`'s *open* branch. So after
a language switch the same panel shows two languages, and nothing short of
closing and re-opening it redraws it.
`atp:langchange` already re-renders the identically-situated tour popup
(`renderTourStep()`); add the help panel next to it, guarded by
`classList.contains("hidden")` — the mechanism `toggleHelp` hides it with
(the property form is always false here, which would make the guard always
true).
The fix ships with a rule that derives the roster instead of restating it
(`state_gate::the_language_switch_refreshes_every_overlay_it_can_show`): the
overlay set comes from `ui/index.html` (top-level elements after `#app`
carrying the `hidden` class), modal-vs-non-modal from `ui/css/style.css` (a
class rule carrying `inset: 0`), and the writers from `ui/js/app.js` (a
function that writes one of the overlay's ids and calls `T(`). A future
overlay or writer that nobody refreshes turns the gate red. `ui/README.md`
records the convention and the gate's scope.
|
Self-review (the PR author is this account; What I checked before merging
Honest limits The rule is lexical. It shows the refresh roster covers every writer of every derived non-modal overlay; it does not show that the pixels are in the current language at that moment — the jsdom probe owns that half — and it cannot see a fix that removes the panel instead of repainting it. Both limits are recorded in While landing I found two bugs in my own instruments, both of the "trip wire was wired to the wrong fact" kind, and fixed them here rather than papering over the readings: the applier's read-back leg asked for a marker comment the splice intentionally drops, and the gate's E1-direction leg demanded the pre-landing XOR, which reports a false red on an already-landed tree (an "insert after an anchor" edit keeps |
Summary
The shortcut help panel (
#help-panel) is a non-modal floating panel: it does not cover theSettings language select, so the user can change the language while it is open. Its heading and
close button carry
[data-i18n]and are rewritten byapplyStatic(), but the four shortcut rowsand the
#help-contextline are built byrenderHelp()— whose only call site istoggleHelp'sopen branch. After a language switch the same panel therefore shows two languages at once, and
nothing short of closing and re-opening it redraws it.
atp:langchangealready re-renders the identically situated tour popup (renderTourStep()); thisadds the help panel next to it. The guard reads
classList.contains("hidden")— the mechanismtoggleHelphides the panel with. The property form (!$("#help-panel").hidden) is always falsehere, so it would turn the branch into an unconditional repaint.
Related Issue
No linked issue: the repository has no open issue for this. Reported here with reproduction.
Changes
ui/js/app.js: theatp:langchangehandler re-renders#help-panelwhen it is opensrc/state_gate.rs: new rulethe_language_switch_refreshes_every_overlay_it_can_show,which derives what must follow the language switch instead of restating it:
the overlay set from
ui/index.html(top-level elements after#appcarrying thehiddenclass), modal-vs-non-modal from
ui/css/style.css(a class rule carryinginset: 0— a modaloverlay blocks the language select, so it cannot be open when the user switches), and the
writers from
ui/js/app.js(a function that writes one of the overlay's ids and callsT().A future overlay or a future writer nobody refreshes turns it red. The four companion tests
(
_roster_is_real,_rules_have_teeth,_rules_separate_the_variants,_fixed_guard_is_the_edit_sheet_text) give each rule its own teeth and declare what happensto the competing fixes.
ui/README.md: the refresh list gains the help panel, plus a bullet recording the invariantand the gate's scope
ui/index.html: cache-bustjs/app.js?v=20260922-10→-11Tests
cargo test— 361 passed / 0 failed (baseline before this branch: 356)cargo fmt --check— clean (rc=0)sync)
Checklist
fix/…)Evidence
Two instruments, one for the shape and one for the fact.
jsdom probe (
r94_probe.js, realapp.jsin realindex.html, real?/ language-changeevents; the language-discriminating values are read out of
window.I18N, not hardcoded):6036d759…)baseA1, A2, Z1— the panel keeps the old languagefixm_close(competing fix: close the panel)A4— the switch steals the panel the user openedm_datai18n(competing fix: hangdata-i18non the JS rows)A2, Z1— a template with{view}cannot be maintained that wayc6ca9310…)landedshape gate (
r94_langchange_roster_scan.py, rosters derived fromui/index.html+ui/js/app.js+ui/css/style.css): pre-fixRED: R1:help-panel; with the fixALL GREEN;m_closeRED: R1:help-panel(closing the panel is not refreshing it); shipped bytesALL GREEN.compiled gate (
r94_compile_gate.py, splices the rule into the realsrc/state_gate.rs,rustc + clippy + tests): 3 legs (
base/fix/live), 30/30 as declared —basered onexactly
the_language_switch_refreshes_every_overlay_it_can_show,fixandlivegreen; theliveleg'sapp.jsmd5 is the shippedc6ca9310…, so the tree under test is the tree merged.Negative control (rule 1 disarmed): every leg red on exactly
{_roster_is_real, _rules_have_teeth, _rules_separate_the_variants}, and the unfixed tree's axis test turns green — it was redbecause of rule 1, which is the direction that is easy to get backwards.
Scope, stated honestly: the rule is lexical. It proves the refresh roster covers every writer of
every derived non-modal overlay; it does not prove that what is on screen is in the current
language at that instant (that half belongs to the jsdom probe), and it cannot see the competing
"just close the panel" fix — the probe's
A4leg rejects that one. Recorded inui/README.md.