Compare commits

..

7 Commits

Author SHA1 Message Date
dev e44bf2b3c9 style(ui): make docked dialog look more like a Torn panel (full-width, accent border)
Previously the .tat-root.tat-anchored rule centered the dialog with a
720px max-width and no extra border, which made it look like a floating
widget squeezed into the page rather than an embedded panel. The
Torn gym page reference is a full-width panel with a thin dark border
and a red top accent line.

Changes:
- margin: 0 0 12px 0 (full-width, no centering)
- max-width: none (span the content area)
- border-radius: 0 (Torn panels are flat, not rounded)
- border: 1px solid #444 with border-top: 2px solid #c00 (red accent)
- padding: 16px 20px (more breathing room, matching Torn panels)
- .tat-root.tat-anchored .tat-header { cursor: default } (drag is
  disabled in anchored mode, so the move cursor was misleading)

Free-floating mode is unchanged: the .tat-root base rule keeps its
rounded shadowed look and .tat-header keeps cursor: move.

Mirrored into the embedded TAT_STYLE in the bundle to keep the source
and bundle in lockstep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:02:11 -05:00
dev b626fb7d41 fix(bundle): stop pre-escaping gym name in embedded dom.js (apostrophe fix)
The embedded currentAttribute() in the bundle was returning gym:
tatEsc(gym), but the render template in Dialog.render also escapes with
tatEsc(s.gym). The double-escape turned 'George's' into 'George&amp;#39;s',
which the browser decoded to the visible text 'George&#39;s'.

The src/dom.js source does NOT pre-escape (returns the raw gym string and
lets the render template handle escaping once). This commit restores the
mirror in the embedded dom.js so the bundle matches the source.

No changes to src/dom.js (it was already correct).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:00:40 -05:00
dev c6de810417 feat(bundle): mirror source changes in embedded userscript 2026-06-07 12:17:29 -05:00
dev ca83996c6e feat(main): anchor dialog by inserting before gymContentWrapper (dock in page flow) 2026-06-07 12:15:09 -05:00
dev a1e79ac801 feat(ui): add tat-anchored CSS class and rework setMode to dock the dialog in the page flow 2026-06-07 12:14:05 -05:00
dev 7fddd84b6a fix(bundle): mirror source fixes in embedded userscript 2026-06-07 09:00:40 -05:00
dev 2ed25c14de fix(main): pass rect to setMode and remove redundant _positionAnchored call (anchor crash fix) 2026-06-07 09:00:04 -05:00
3 changed files with 84 additions and 38 deletions
+10 -17
View File
@@ -4,20 +4,20 @@ import { currentAttribute } from './dom.js';
import { startRequestInterceptor } from './interceptor.js';
function findAnchorElement() {
// Try several selectors in priority order. Torn's gym page renders the
// training UI as <ul class="properties___HASH"> (the list of attribute
// rows). Anchor the dialog above that list.
// Return the element to insert the dialog BEFORE in the DOM.
// The user wants the dialog between the notification wrapper and the
// gym content wrapper; we insert before gymContentWrapper.
const candidates = [
'ul[class*="properties"]',
'[class*="gymContent"]',
'[class*="gymContentWrapper"]',
// Legacy fallbacks (kept in case Torn ever wraps the list in a form):
'[class*="gymContentWrapper"]', // outer wrapper — best insertion point
'[class*="gymContent"]', // inner wrapper (fallback)
'ul[class*="properties"]', // the list itself (last resort)
// Legacy fallbacks (kept for robustness):
'form[action*="train"]',
'form.train-form',
'form[class*="train"]',
'[class*="train-button"]',
'button[class*="train"]',
'a[class*="train"]',
'a[href*="train"]',
'button[name="train"]',
'a[href*="train"]',
];
@@ -99,20 +99,13 @@ function start() {
if (prefs.mode === 'anchored') {
const el = findAnchorElement();
if (el) {
const rect = el.getBoundingClientRect();
dialog.setMode('anchored', { canAnchor: true });
dialog._positionAnchored(rect);
// observe
const ro = new ResizeObserver(() => {
if (prefs.mode === 'anchored') dialog._positionAnchored(el.getBoundingClientRect());
});
ro.observe(el);
dialog.setMode('anchored', { canAnchor: true, insertBefore: 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.";
anchorError = "Couldn't find the training UI on this page.";
render();
return;
}
+33 -1
View File
@@ -8,6 +8,17 @@ const STYLE = `
font: 13px/1.4 Tahoma, Verdana, sans-serif;
padding: 12px 14px;
}
.tat-root.tat-anchored {
position: static;
margin: 0 0 12px 0;
max-width: none;
box-shadow: none;
border-radius: 0;
border: 1px solid #444;
border-top: 2px solid #c00;
padding: 16px 20px;
}
.tat-root.tat-anchored .tat-header { cursor: default; }
.tat-header {
display: flex; justify-content: space-between; align-items: center;
padding-bottom: 8px; margin-bottom: 10px;
@@ -117,17 +128,37 @@ export class Dialog {
setMode(mode, anchorInfo) {
this.mode = mode;
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.classList.remove('tat-anchored');
if (mode === 'free') {
// 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) {
this._positionAnchored(anchorInfo.rect);
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%)';
@@ -135,6 +166,7 @@ export class Dialog {
}
_positionAnchored(rect) {
if (!rect) return; // defensive: setMode may be called without a rect
const dialogRect = this.root.getBoundingClientRect();
let top = rect.top - dialogRect.height - 8;
if (top < 8) top = 20;
+41 -20
View File
@@ -131,7 +131,7 @@
const current = tatExtractValueFromLi(li);
if (current == null) return null;
const gym = tatFindGymName() || 'Unknown gym';
return { attr: attr, current: current, gym: tatEsc(gym) };
return { attr: attr, current: current, gym: gym };
}
function tatFindActiveAttributeLi() {
// Priority 1: the <li> with the "success" class (just trained).
@@ -276,6 +276,8 @@
// ===== ui.js (embedded) =====
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.tat-anchored { position: static; margin: 0 0 12px 0; max-width: none; box-shadow: none; border-radius: 0; border: 1px solid #444; border-top: 2px solid #c00; padding: 16px 20px; }
.tat-root.tat-anchored .tat-header { cursor: default; }
.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-close { cursor: pointer; opacity: 0.6; padding: 0 4px; }
@@ -350,12 +352,39 @@
setMode(mode, anchorInfo) {
this.mode = mode;
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 = '';
if (mode === 'free') { this.root.style.bottom = '20px'; this.root.style.left = '20px'; }
else if (anchorInfo && anchorInfo.canAnchor) { this._positionAnchored(anchorInfo.rect); }
else { this.root.style.top = '20px'; this.root.style.left = '50%'; this.root.style.transform = 'translateX(-50%)'; }
this.root.classList.remove('tat-anchored');
if (mode === 'free') {
// 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) {
if (!rect) return; // defensive: setMode may be called without a rect
const dialogRect = this.root.getBoundingClientRect();
let top = rect.top - dialogRect.height - 8;
if (top < 8) top = 20;
@@ -438,20 +467,20 @@
// ===== main.js (embedded) =====
function findAnchorElement() {
// Try several selectors in priority order. Torn's gym page renders the
// training UI as <ul class="properties___HASH"> (the list of attribute
// rows). Anchor the dialog above that list.
// Return the element to insert the dialog BEFORE in the DOM.
// The user wants the dialog between the notification wrapper and the
// gym content wrapper; we insert before gymContentWrapper.
const candidates = [
'ul[class*="properties"]',
'[class*="gymContent"]',
'[class*="gymContentWrapper"]',
'[class*="gymContent"]',
'ul[class*="properties"]',
// Legacy fallbacks (kept in case Torn ever wraps the list in a form):
'form[action*="train"]',
'form.train-form',
'form[class*="train"]',
'[class*="train-button"]',
'button[class*="train"]',
'a[class*="train"]',
'a[href*="train"]',
'button[name="train"]',
'a[href*="train"]',
];
@@ -515,21 +544,13 @@
if (prefs.mode === 'anchored') {
const el = findAnchorElement();
if (el) {
const rect = el.getBoundingClientRect();
dialog.setMode('anchored', { canAnchor: true });
dialog._positionAnchored(rect);
if (typeof ResizeObserver !== 'undefined') {
const ro = new ResizeObserver(function () {
if (prefs.mode === 'anchored') dialog._positionAnchored(el.getBoundingClientRect());
});
ro.observe(el);
}
dialog.setMode('anchored', { canAnchor: true, insertBefore: 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.";
anchorError = "Couldn't find the training UI on this page.";
render();
return;
}