feat(bundle): mirror source changes in embedded userscript

This commit is contained in:
dev
2026-06-07 12:17:29 -05:00
parent ca83996c6e
commit c6de810417
+38 -18
View File
@@ -276,6 +276,7 @@
// ===== ui.js (embedded) ===== // ===== ui.js (embedded) =====
const TAT_STYLE = ` const TAT_STYLE = `
.tat-root { position: fixed; z-index: 99999; min-width: 320px; max-width: 420px; background: #2b2b2b; color: #ddd; border: 1px solid #444; border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.4); font: 13px/1.4 Tahoma, Verdana, sans-serif; padding: 12px 14px; } .tat-root { position: fixed; z-index: 99999; min-width: 320px; max-width: 420px; background: #2b2b2b; color: #ddd; border: 1px solid #444; border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.4); font: 13px/1.4 Tahoma, Verdana, sans-serif; padding: 12px 14px; }
.tat-root.tat-anchored { position: static; margin: 0 auto 12px; max-width: 720px; box-shadow: none; }
.tat-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 8px; margin-bottom: 10px; border-bottom: 1px solid #444; cursor: move; user-select: none; } .tat-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 8px; margin-bottom: 10px; border-bottom: 1px solid #444; cursor: move; user-select: none; }
.tat-header strong { color: #fff; } .tat-header strong { color: #fff; }
.tat-close { cursor: pointer; opacity: 0.6; padding: 0 4px; } .tat-close { cursor: pointer; opacity: 0.6; padding: 0 4px; }
@@ -350,10 +351,36 @@
setMode(mode, anchorInfo) { setMode(mode, anchorInfo) {
this.mode = mode; this.mode = mode;
if (!this.root) return; if (!this.root) return;
// Clear all position styles
this.root.style.transform = ''; this.root.style.top = ''; this.root.style.bottom = ''; this.root.style.left = ''; this.root.style.right = ''; this.root.style.transform = ''; this.root.style.top = ''; this.root.style.bottom = ''; this.root.style.left = ''; this.root.style.right = '';
if (mode === 'free') { this.root.style.bottom = '20px'; this.root.style.left = '20px'; } this.root.classList.remove('tat-anchored');
else if (anchorInfo && anchorInfo.canAnchor) { this._positionAnchored(anchorInfo.rect); } if (mode === 'free') {
else { this.root.style.top = '20px'; this.root.style.left = '50%'; this.root.style.transform = 'translateX(-50%)'; } // Floating mode: ensure dialog is in body and position at bottom-left.
if (this.root.parentNode !== document.body) {
document.body.appendChild(this.root);
}
this.root.style.bottom = '20px';
this.root.style.left = '20px';
} else if (anchorInfo && anchorInfo.canAnchor) {
if (anchorInfo.insertBefore) {
// Docked mode: insert the dialog into the page flow before the
// given element, and add the tat-anchored class to switch to
// static positioning.
anchorInfo.insertBefore.parentNode.insertBefore(this.root, anchorInfo.insertBefore);
this.root.classList.add('tat-anchored');
} else if (anchorInfo.rect) {
// Fallback: position fixed above the rect (old behavior, used when
// no insertion point is available but a rect was given).
this._positionAnchored(anchorInfo.rect);
}
// If neither insertBefore nor rect, leave the dialog where it is
// (the caller will show an anchorError note).
} else {
// Top-center fallback (used when mode is anchored but no anchor info).
this.root.style.top = '20px';
this.root.style.left = '50%';
this.root.style.transform = 'translateX(-50%)';
}
} }
_positionAnchored(rect) { _positionAnchored(rect) {
if (!rect) return; // defensive: setMode may be called without a rect if (!rect) return; // defensive: setMode may be called without a rect
@@ -439,20 +466,20 @@
// ===== main.js (embedded) ===== // ===== main.js (embedded) =====
function findAnchorElement() { function findAnchorElement() {
// Try several selectors in priority order. Torn's gym page renders the // Return the element to insert the dialog BEFORE in the DOM.
// training UI as <ul class="properties___HASH"> (the list of attribute // The user wants the dialog between the notification wrapper and the
// rows). Anchor the dialog above that list. // gym content wrapper; we insert before gymContentWrapper.
const candidates = [ const candidates = [
'ul[class*="properties"]',
'[class*="gymContent"]',
'[class*="gymContentWrapper"]', '[class*="gymContentWrapper"]',
'[class*="gymContent"]',
'ul[class*="properties"]',
// Legacy fallbacks (kept in case Torn ever wraps the list in a form): // Legacy fallbacks (kept in case Torn ever wraps the list in a form):
'form[action*="train"]', 'form[action*="train"]',
'form.train-form', 'form.train-form',
'form[class*="train"]', 'form[class*="train"]',
'[class*="train-button"]', '[class*="train-button"]',
'button[class*="train"]', 'button[class*="train"]',
'a[class*="train"]', 'a[href*="train"]',
'button[name="train"]', 'button[name="train"]',
'a[href*="train"]', 'a[href*="train"]',
]; ];
@@ -516,20 +543,13 @@
if (prefs.mode === 'anchored') { if (prefs.mode === 'anchored') {
const el = findAnchorElement(); const el = findAnchorElement();
if (el) { if (el) {
const rect = el.getBoundingClientRect(); dialog.setMode('anchored', { canAnchor: true, insertBefore: el });
dialog.setMode('anchored', { canAnchor: true, rect: rect });
if (typeof ResizeObserver !== 'undefined') {
const ro = new ResizeObserver(function () {
if (prefs.mode === 'anchored') dialog._positionAnchored(el.getBoundingClientRect());
});
ro.observe(el);
}
anchorError = null; anchorError = null;
return; return;
} }
// Anchor selector missed — don't snap to default, just keep current // Anchor selector missed — don't snap to default, just keep current
// position and show a note. // position and show a note.
anchorError = "Couldn't find the training form on this page."; anchorError = "Couldn't find the training UI on this page.";
render(); render();
return; return;
} }