Compare commits

...

3 Commits

Author SHA1 Message Date
Claude a061410f16 fix(bundle): mirror source fixes in embedded userscript
Mirror the three source-level fixes in the embedded copies inside
torn-attribute-tracker.user.js:

1. _wireHeaderDrag: add the .tat-header closest() guard so the bundle
   no longer steals focus from inputs/selects.
2. findAnchorElement: replace the narrow 'button[name="train"]' query
   with the priority-ordered candidate list and the gym-panel last-
   ditch fallback.
3. Inline anchor-error UX: add the anchorError state, include it in
   the snapshot, surface it via applyMode, render the note with the
   .tat-anchor-err class, and add the corresponding CSS rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 18:47:39 -05:00
Claude bb33bcbb61 fix(main): broaden anchor selector and show inline "can't anchor" note
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 <noreply@anthropic.com>
2026-06-01 18:45:40 -05:00
Claude e200faf8c4 fix(ui): scope mousedown handler to .tat-header to restore input/select focus
The dialog's mousedown listener was attached to the whole root and
unconditionally called preventDefault(), which blocked the target
element from receiving focus. As a result, the custom target <input>
and the milestone <select> could never be focused.

Only initiate drag (and only preventDefault) when the mousedown is on
the .tat-header bar. This lets the user click into inputs, selects, and
buttons inside the dialog body, while still allowing the dialog to be
dragged from the title bar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 18:42:16 -05:00
3 changed files with 85 additions and 9 deletions
+37 -5
View File
@@ -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');
}
+5
View File
@@ -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; }
`;
@@ -148,6 +149,9 @@ export class Dialog {
const self = this;
this.root.addEventListener('mousedown', (e) => {
if (self.mode !== 'free') return;
// Only initiate drag from the header bar. This prevents stealing focus
// from inputs, selects, and buttons inside the dialog body.
if (!e.target.closest('.tat-header')) return;
if (e.target.classList.contains('tat-close')) return;
const rect = self.root.getBoundingClientRect();
self.dragState = { dx: e.clientX - rect.left, dy: e.clientY - rect.top };
@@ -224,6 +228,7 @@ export class Dialog {
<div class="tat-row"><span>ETA</span><span>${est.days > 0 ? `~ ${fmtFull(est.days)} days (${fmtDate(est.eta)})` : '—'}</span></div>
<div class="tat-modes">${modes}</div>
${warn ? `<div class="tat-warn">⚠ ${esc(warn)}</div>` : ''}
${state.anchorError ? `<div class="tat-anchor-err">⚠ ${esc(state.anchorError)}</div>` : ''}
`;
this.root.querySelector('.tat-close').onclick = () => this.onClose && this.onClose();
+43 -4
View File
@@ -263,6 +263,7 @@
.tat-modes button { flex: 1; padding: 4px; background: #2b2b2b; color: #ddd; border: 1px solid #555; font: inherit; font-size: 11px; cursor: pointer; }
.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; }
`;
@@ -343,6 +344,9 @@
const self = this;
this.root.addEventListener('mousedown', function (e) {
if (self.mode !== 'free') return;
// Only initiate drag from the header bar. This prevents stealing focus
// from inputs, selects, and buttons inside the dialog body.
if (!e.target.closest('.tat-header')) return;
if (e.target.classList.contains('tat-close')) return;
const rect = self.root.getBoundingClientRect();
self.dragState = { dx: e.clientX - rect.left, dy: e.clientY - rect.top };
@@ -399,7 +403,8 @@
+ '<div class="tat-row"><span>Trains to go</span><span>≈ ' + tatFmtFull(est.trainsToGo) + '</span></div>'
+ '<div class="tat-row"><span>ETA</span><span>' + (est.days > 0 ? '~ ' + tatFmtFull(est.days) + ' days (' + tatFmtDate(est.eta) + ')' : '—') + '</span></div>'
+ '<div class="tat-modes">' + modes + '</div>'
+ (s.warn ? '<div class="tat-warn">⚠ ' + tatEsc(s.warn) + '</div>' : '');
+ (s.warn ? '<div class="tat-warn">⚠ ' + tatEsc(s.warn) + '</div>' : '')
+ (s.anchorError ? '<div class="tat-anchor-err">⚠ ' + tatEsc(s.anchorError) + '</div>' : '');
this.root.querySelector('.tat-close').onclick = function () { self.onClose && self.onClose(); };
this.root.querySelector('[data-role="target"]').onchange = function (e) { self.onTargetChange && self.onTargetChange(e.target.value); };
this.root.querySelector('[data-role="milestone"]').onchange = function (e) { const v = e.target.value; if (v !== '') self.onTargetChange && self.onTargetChange(Number(v)); };
@@ -409,9 +414,34 @@
// ===== main.js (embedded) =====
function findAnchorElement() {
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() {
@@ -434,6 +464,7 @@
let lastSnapshot = null;
let lastAttr = null;
let lastDelta = 0;
let anchorError = null;
function snapshot() {
const a = currentAttribute();
@@ -444,6 +475,7 @@
attr: a.attr, gym: a.gym, current: a.current,
target: store.getTarget(a.attr), perTrain: lastDelta, summary: summary,
warn: store._saveDisabled ? 'saving disabled this session' : null,
anchorError: anchorError,
};
}
@@ -462,9 +494,16 @@
});
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');
}