From bb33bcbb61c94b0aa68b57bb6530e3c07ac0fa63 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 1 Jun 2026 18:45:40 -0500 Subject: [PATCH] fix(main): broaden anchor selector and show inline "can't anchor" note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit findAnchorElement used a narrow selector ('button[name="train"], a[href*="train"]') that often missed Torn's actual gym page DOM. When it missed, applyMode fell through to dialog.setMode('free'), snapping the dialog to the default bottom-right position — which the user perceived as a 'bounce' when clicking 'Above training UI'. Widen the selector to a priority-ordered candidate list and prefer the form ancestor of any matched element. As a last-ditch, look for a form inside the gym panel. This covers more of Torn's gym-page variations. When the anchor selector still misses, do NOT snap to the default free position. Instead, keep the dialog where it is, set anchorError on the state, and let the dialog render an inline note so the user gets visible feedback explaining what happened. Co-Authored-By: Claude Opus 4.8 --- src/main.js | 42 +++++++++++++++++++++++++++++++++++++----- src/ui.js | 2 ++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/main.js b/src/main.js index c868bf2..6f88918 100644 --- a/src/main.js +++ b/src/main.js @@ -4,11 +4,34 @@ import { currentAttribute } from './dom.js'; import { startRequestInterceptor } from './interceptor.js'; function findAnchorElement() { - // Torn's training form is the element containing the Train button. - // Selector is best-effort; the Dialog will fall back if missing. - const btn = document.querySelector('button[name="train"], a[href*="train"]'); - if (!btn) return null; - return btn.closest('form') || btn.parentElement; + // Try several selectors in priority order. Torn's gym page structure + // varies; we cast a wide net and return the first match. + const candidates = [ + 'form[action*="train"]', + 'form.train-form', + 'form[class*="train"]', + '[class*="train-button"]', + 'button[class*="train"]', + 'a[class*="train"]', + 'button[name="train"]', + 'a[href*="train"]', + ]; + for (const sel of candidates) { + const el = document.querySelector(sel); + if (el) { + // Prefer the form ancestor if the match is a button/link, since we + // want to anchor above the whole form, not just the button. + return el.closest('form') || el; + } + } + // Last-ditch: any element inside the gym panel that looks like the + // training form. + const panel = document.querySelector('.gym, #gym, [class*="gym-"], [class*="Gym"]'); + if (panel) { + const form = panel.querySelector('form'); + if (form) return form; + } + return null; } function start() { @@ -40,6 +63,7 @@ function start() { let lastSnapshot = null; let lastAttr = null; let lastDelta = 0; + let anchorError = null; function snapshot() { const a = currentAttribute(); @@ -56,6 +80,7 @@ function start() { perTrain: lastDelta, summary, warn: store._saveDisabled ? 'saving disabled this session' : null, + anchorError: anchorError, }; } @@ -76,9 +101,16 @@ function start() { if (prefs.mode === 'anchored') dialog._positionAnchored(el.getBoundingClientRect()); }); ro.observe(el); + anchorError = null; return; } + // Anchor selector missed — don't snap to default, just keep current + // position and show a note. + anchorError = "Couldn't find the training form on this page."; + render(); + return; } + anchorError = null; dialog.setMode('free'); } diff --git a/src/ui.js b/src/ui.js index 610d691..d5b6599 100644 --- a/src/ui.js +++ b/src/ui.js @@ -30,6 +30,7 @@ const STYLE = ` } .tat-modes button.active { background: #444; border-color: #888; } .tat-warn { color: #c90; margin-top: 6px; font-size: 11px; } +.tat-anchor-err { color: #c90; margin-top: 6px; font-size: 11px; } .tat-error { padding: 8px 0; color: #f88; } .tat-error button { margin-left: 8px; } `; @@ -227,6 +228,7 @@ export class Dialog {
ETA${est.days > 0 ? `~ ${fmtFull(est.days)} days (${fmtDate(est.eta)})` : '—'}
${modes}
${warn ? `
⚠ ${esc(warn)}
` : ''} + ${state.anchorError ? `
⚠ ${esc(state.anchorError)}
` : ''} `; this.root.querySelector('.tat-close').onclick = () => this.onClose && this.onClose();