Files
Zebratic 02a90e839c Initial scaffold: Beat Pocket MV3 extension
- manifest.json with MV3 config, host permissions for beat-battle.net
- content/detector.ts: route + overlay observer, emits bbp:phase events
- content/layout.ts: defensive mobile-first layout reflow
- content/chat-drawer.ts: bottom-sheet drawer, dispatches synthetic events
  through site's own React-managed textarea + button (preserves auth,
  rate-limit, mute, profanity guards)
- content/util/selectors.ts: text- and role-based fallback chain
- content/util/ws-proxy.ts: lightweight WebSocket instrumentation, no
  payload capture
- content/inject.css: mobile overrides using site's existing --bb-* vars
- background.js: MV3 service worker stub
- package.json + tsconfig.json + esbuild.config.mjs: build pipeline
- icons/: 16/32/48/128 placeholder PNGs (chili-red disc)
- docs/PLAN.md: full V1 architecture, risks, scope
- README.md: scope, dev workflow (load unpacked), tech
2026-07-11 02:24:37 +00:00

96 lines
2.7 KiB
TypeScript

/**
* Selector helpers. Prefer semantic names and text content over Tailwind
* utility classes — utility classes change every deploy.
*/
export const SITE = {
overlay: '.bb-room-overlay',
inGameRoot: '.bb-room-overlay',
homeRoot: '.bb-scanlines',
loginChip: '.bb-login-chip',
footerNav: '.bb-footer-nav-main-fade',
chatInputCandidates: [
'textarea.bb-chat-input',
'[data-bb-chat-input]',
'textarea[placeholder*="chat" i]',
'textarea[placeholder*="message" i]',
'textarea[aria-label*="message" i]',
'textarea[aria-label*="chat" i]',
],
chatScrollbackCandidates: [
'.bb-chat-messages',
'[data-bb-chat-messages]',
'[role="log"]',
'.bb-messages',
],
sendButtonCandidates: [
'button[data-bb-chat-send]',
'button[aria-label*="send" i]',
'button[type="submit"]',
],
menuItems: '.bb-menu-item',
inGameTimer: '.bb-timer, [data-bb-timer]',
inGamePlayers: '[data-bb-players], .bb-players',
inGameRankPanel: '.bb-rank-panel, [data-bb-rank]',
};
/**
* First matching element from a candidate selector list, OR a fallback
* function-based search.
*/
export function pick<T extends Element>(
candidates: string[],
root: ParentNode = document,
): T | null {
for (const sel of candidates) {
const el = root.querySelector(sel);
if (el) return el as T;
}
return null;
}
/**
* Text-based finder: returns the deepest element whose own text exactly
* matches one of the given strings (case-insensitive, trimmed).
*/
export function findByText(
root: ParentNode,
needles: string[],
options: { tag?: string } = {},
): Element | null {
const tag = options.tag?.toLowerCase() ?? '';
const norm = needles.map((s) => s.trim().toLowerCase());
const walker = document.createTreeWalker(root as Node, NodeFilter.SHOW_ELEMENT);
let found: Element | null = null;
let node: Node | null = walker.currentNode as Node;
while (node) {
const el = node as Element;
if (el.children.length === 0 && (!tag || el.tagName.toLowerCase() === tag)) {
const txt = (el.textContent ?? '').trim().toLowerCase();
if (txt.length > 0 && norm.includes(txt)) {
found = el;
break;
}
}
node = walker.nextNode();
}
return found;
}
export function isHome(): boolean {
return location.pathname === '/' || location.pathname === '';
}
export function isLogin(): boolean {
return location.pathname.startsWith('/login');
}
export function isInGame(): boolean {
return !!document.querySelector(SITE.overlay);
}
export function isLobbyList(): boolean {
// Heuristic: home rendered in browse-mode shows lobby list
return isHome() && !isInGame() && !!document.querySelector('.bb-lobby-card, [data-bb-lobby]');
}