diff --git a/src/content-scripts/adapters/axiom.ts b/src/content-scripts/adapters/axiom.ts index a6bdd7b..12c2316 100644 --- a/src/content-scripts/adapters/axiom.ts +++ b/src/content-scripts/adapters/axiom.ts @@ -3,15 +3,33 @@ import type { SiteAdapter } from './adapter-interface'; /** Matches "Buy", "Buy pixelcat", etc. — never a hardcoded token name. */ const NOOB_MODE_BUY_TEXT = /^Buy\s/i; +/** Axiom's "increase"/"decrease" Tailwind color tokens are its buy(green)/sell(red) convention. */ +const INCREASE_MARKER_SELECTOR = '[class*="increase"]'; + +function looksLikeBuyPill(el: HTMLElement): boolean { + return el.matches(INCREASE_MARKER_SELECTOR) || el.querySelector(INCREASE_MARKER_SELECTOR) !== null; +} + function findQuickBuyContainers(root: ParentNode): HTMLElement[] { - // The reliable anchor is `.buy-click-container` itself, not a fixed - // ancestor depth from the inner pill — DOM depth here isn't stable. - return Array.from(root.querySelectorAll('.buy-click-container')); + // `.buy-click-container` is a *shared* wrapper around both the buy and + // sell quick-amount pills as siblings — it is NOT buy-only. The buy pill + // is always its first child element; the sell pill is another sibling. + // Never treat the wrapper itself as the buy element/container. + const buyPills: HTMLElement[] = []; + for (const wrapper of root.querySelectorAll('.buy-click-container')) { + const firstChild = wrapper.firstElementChild; + if (firstChild instanceof HTMLElement && looksLikeBuyPill(firstChild)) { + buyPills.push(firstChild); + } + } + return buyPills; } function findNoobModeBuyButtons(root: ParentNode): HTMLElement[] { + // Buy and Sell noob-mode buttons are siblings too; the buy one is + // identified by its own text starting with "Buy", never by position. return Array.from(root.querySelectorAll('button.bg-increase')).filter((button) => - NOOB_MODE_BUY_TEXT.test(button.textContent?.trim() ?? ''), + {console.log(button.textContent); return NOOB_MODE_BUY_TEXT.test(button.textContent?.trim() ?? '')}, ); } @@ -31,9 +49,12 @@ export const axiomAdapter: SiteAdapter = { }, getContainer(el) { - // Quick Buy pills are already `.buy-click-container` (closest() matches - // the element itself). Noob Mode buttons have no such ancestor, so the - // button itself is the stable container to style. - return el.closest('.buy-click-container') ?? el; + // findBuyElements() above already resolves to exactly the buy-side + // element in both cases — the quick-buy pill (first child of the shared + // buy/sell wrapper) or the noob-mode button whose own text is "Buy …". + // Do NOT climb to any ancestor (e.g. via closest('.buy-click-container')) + // — that wrapper holds the sell pill too, so treating it as the + // container would blur/disable sell right along with buy. + return el; }, }; diff --git a/src/content-scripts/interventions/blur-disable.ts b/src/content-scripts/interventions/blur-disable.ts index 7e05369..cbb7716 100644 --- a/src/content-scripts/interventions/blur-disable.ts +++ b/src/content-scripts/interventions/blur-disable.ts @@ -3,13 +3,20 @@ import type { Intervention } from './intervention-interface'; interface LockedRecord { originalStyleAttr: string | null; overlay: HTMLElement; - onOverlayClick: (event: MouseEvent) => void; + onOverlayEvent: (event: Event) => void; } // Tracked outside the DOM so remove() can restore each container exactly, // without relying on any marker class/attribute the site itself might touch. const locked = new WeakMap(); +// Fast trading UIs (Axiom included) often fire the actual buy on +// `pointerdown`/`mousedown` rather than waiting for `click`, to shave off +// the mouseup round-trip. Intercepting only `click` lets those through — +// so every stage of a click gesture is captured and swallowed here, and the +// reason toast is surfaced on `click` once the gesture completes. +const INTERCEPTED_EVENT_TYPES = ['pointerdown', 'mousedown', 'mouseup', 'click'] as const; + const LOCK_ICON_SVG = ''; @@ -58,23 +65,29 @@ export const blurDisable: Intervention = { overlay.style.cssText = OVERLAY_STYLE; overlay.innerHTML = LOCK_ICON_SVG; - const onOverlayClick = (event: MouseEvent) => { + const onOverlayEvent = (event: Event) => { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); - onLockedInteraction(reason); + if (event.type === 'click') { + onLockedInteraction(reason); + } }; - overlay.addEventListener('click', onOverlayClick, true); + for (const type of INTERCEPTED_EVENT_TYPES) { + overlay.addEventListener(type, onOverlayEvent, true); + } container.appendChild(overlay); - locked.set(container, { originalStyleAttr, overlay, onOverlayClick }); + locked.set(container, { originalStyleAttr, overlay, onOverlayEvent }); }, remove(container) { const record = locked.get(container); if (!record) return; - record.overlay.removeEventListener('click', record.onOverlayClick, true); + for (const type of INTERCEPTED_EVENT_TYPES) { + record.overlay.removeEventListener(type, record.onOverlayEvent, true); + } record.overlay.remove(); if (record.originalStyleAttr === null) { diff --git a/src/notifications/toast.ts b/src/notifications/toast.ts index d925ca1..03a3a94 100644 --- a/src/notifications/toast.ts +++ b/src/notifications/toast.ts @@ -3,6 +3,9 @@ import type { NexaMessage } from '@/shared/messaging'; const TOAST_ID = 'nexa-lock-toast'; const STYLE_ID = 'nexa-lock-toast-styles'; +const AUTO_HIDE_MS = 5000; + +let hideTimer: ReturnType | undefined; function ensureStyles(): void { if (document.getElementById(STYLE_ID)) return; @@ -59,8 +62,9 @@ function ensureToast(): HTMLElement {
View details
`; - // Deliberately no close/dismiss control — unlocking is the only way this - // toast goes away. See hideLockToast(), called only on an actual unlock. + // No manual close button — the toast only goes away on its own (auto-hide + // timer below) or when the lock state actually changes; clicking it never + // unlocks anything. el.querySelector('.nexa-toast-action')?.addEventListener('click', () => { const message: NexaMessage = { type: 'nexa:open-popup' }; browser.runtime.sendMessage(message).catch(() => undefined); @@ -76,8 +80,18 @@ export function showLockToast(reason: string | undefined): void { if (reasonEl) { reasonEl.textContent = reason ?? 'Buy actions are temporarily locked.'; } + + if (hideTimer) clearTimeout(hideTimer); + hideTimer = setTimeout(() => { + hideTimer = undefined; + hideLockToast(); + }, AUTO_HIDE_MS); } export function hideLockToast(): void { + if (hideTimer) { + clearTimeout(hideTimer); + hideTimer = undefined; + } document.getElementById(TOAST_ID)?.remove(); }