Skip to content

fix(ui): refresh the open help panel when the language changes - #288

Merged
argszero merged 1 commit into
mainfrom
fix/help-panel-follows-the-language
Sep 22, 2026
Merged

argszero merged 1 commit into
mainfrom
fix/help-panel-follows-the-language

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

The shortcut help panel (#help-panel) is a non-modal floating panel: it does not cover the
Settings language select, so the user can change the language while it is open. Its heading and
close button carry [data-i18n] and are rewritten by applyStatic(), but the four shortcut rows
and the #help-context line are built by renderHelp() — whose only call site is toggleHelp's
open 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:langchange already re-renders the identically situated tour popup (renderTourStep()); this
adds the help panel next to it. The guard reads classList.contains("hidden") — the mechanism
toggleHelp hides the panel with. The property form (!$("#help-panel").hidden) is always false
here, 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: the atp:langchange handler re-renders #help-panel when it is open
  • src/state_gate.rs: new rule the_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 #app carrying the hidden
    class), modal-vs-non-modal from ui/css/style.css (a class rule carrying inset: 0 — a modal
    overlay 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 calls T().
    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 happens
    to the competing fixes.
  • ui/README.md: the refresh list gains the help panel, plus a bullet recording the invariant
    and the gate's scope
  • ui/index.html: cache-bust js/app.js?v=20260922-10 → -11
  • no config/data-structure change

Tests

  • cargo test — 361 passed / 0 failed (baseline before this branch: 356)
  • cargo fmt --check — clean (rc=0)
  • new unit tests added (5: the rule, its roster, its teeth, the variant relations, the edit-sheet
    sync)

Checklist

  • branch name follows the convention (fix/…)
  • commit message uses Conventional Commits
  • single responsibility, minimal diff

Evidence

Two instruments, one for the shape and one for the fact.

jsdom probe (r94_probe.js, real app.js in real index.html, real ? / language-change
events; the language-discriminating values are read out of window.I18N, not hardcoded):

tree variant axis-fail legs
pre-fix (6036d759…) base A1, A2, Z1 — the panel keeps the old language
pre-fix fix none
pre-fix m_close (competing fix: close the panel) A4 — the switch steals the panel the user opened
pre-fix m_datai18n (competing fix: hang data-i18n on the JS rows) A2, Z1 — a template with {view} cannot be maintained that way
shipped bytes (c6ca9310…) landed none — 8/8 legs

shape gate (r94_langchange_roster_scan.py, rosters derived from ui/index.html +
ui/js/app.js + ui/css/style.css): pre-fix RED: R1:help-panel; with the fix ALL GREEN;
m_close RED: R1:help-panel (closing the panel is not refreshing it); shipped bytes ALL GREEN.

compiled gate (r94_compile_gate.py, splices the rule into the real src/state_gate.rs,
rustc + clippy + tests): 3 legs (base / fix / live), 30/30 as declared — base red on
exactly the_language_switch_refreshes_every_overlay_it_can_show, fix and live green; the
live leg's app.js md5 is the shipped c6ca9310…, 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 red
because 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 A4 leg rejects that one. Recorded in ui/README.md.

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.
@argszero

Copy link
Copy Markdown
Owner Author

Self-review (the PR author is this account; --approve is rejected on one's own PR, so this is a plain comment, not a formal review).

What I checked before merging

  • The guard's mechanism is the one the element itself uses: toggleHelp(false) hides #help-panel with classList.add("hidden"), so the branch reads classList.contains("hidden"). The sibling-property form would be a constant-true guard and an unconditional repaint — the probe's D2/E2 legs reject it.
  • The insertion point is the handler that already refreshes the identically situated tour popup, not a new listener: the file still has exactly one atp:langchange listener (counted before and after).
  • renderHelp gained exactly one call site (derived from the live count, not asserted). No new i18n key: T("help.* count is unchanged, and the fix uses no literal text.
  • The gate is derivation-based, not a snapshot: the overlay roster, the modal/non-modal split and the writer roster are all computed from ui/index.html / ui/css/style.css / ui/js/app.js. There is no exemption list — #chat-modal is excluded because its class rule carries inset: 0, a fact read out of the stylesheet.
  • The declared-vs-actual tables are printed side by side, and the m_close boundary is measured rather than hidden: both instruments reject "close the panel on a language switch", but for different reasons — the gate because renderHelp was not called (R1 asks whether the writer ran), the probe because the user's panel was taken away (A4). That relation is asserted in the_r94_rules_separate_the_variants.
  • The merged tree is the tested tree: the live leg's app.js md5 (c6ca931073951cc400245036850f3133) is the one in this branch, and the gate splices the rule into the real src/state_gate.rs, so rustc + clippy -D warnings + the test run all measure the shipped text.

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 ui/README.md.

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 old x1 in both states, so the state has to be read off new).

@argszero
argszero merged commit 5e67055 into main Sep 22, 2026
1 check passed
@argszero
argszero deleted the fix/help-panel-follows-the-language branch September 22, 2026 12:18
@argszero argszero mentioned this pull request Sep 24, 2026
10 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant