From 02a90e839c686d5362bd88777ea60b263f1b9340 Mon Sep 17 00:00:00 2001 From: Zebratic Date: Sat, 11 Jul 2026 02:24:37 +0000 Subject: [PATCH] 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 --- .gitignore | 8 + README.md | 68 ++++ docs/PLAN.md | 274 +++++++++++++++ esbuild.config.mjs | 29 ++ extension/background.js | 13 + extension/content/chat-drawer.ts | 253 ++++++++++++++ extension/content/detector.ts | 89 +++++ extension/content/index.ts | 52 +++ extension/content/inject.css | 282 ++++++++++++++++ extension/content/layout.ts | 51 +++ extension/content/util/selectors.ts | 95 ++++++ extension/content/util/ws-proxy.ts | 55 +++ extension/icons/icon-128.png | Bin 0 -> 603 bytes extension/icons/icon-16.png | Bin 0 -> 116 bytes extension/icons/icon-32.png | Bin 0 -> 186 bytes extension/icons/icon-48.png | Bin 0 -> 235 bytes extension/manifest.json | 34 ++ package-lock.json | 496 ++++++++++++++++++++++++++++ package.json | 16 + tsconfig.json | 20 ++ 20 files changed, 1835 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 docs/PLAN.md create mode 100644 esbuild.config.mjs create mode 100644 extension/background.js create mode 100644 extension/content/chat-drawer.ts create mode 100644 extension/content/detector.ts create mode 100644 extension/content/index.ts create mode 100644 extension/content/inject.css create mode 100644 extension/content/layout.ts create mode 100644 extension/content/util/selectors.ts create mode 100644 extension/content/util/ws-proxy.ts create mode 100644 extension/icons/icon-128.png create mode 100644 extension/icons/icon-16.png create mode 100644 extension/icons/icon-32.png create mode 100644 extension/icons/icon-48.png create mode 100644 extension/manifest.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a29a580 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +*.zip +*.pem +*.key +.DS_Store +.env +.env.local diff --git a/README.md b/README.md new file mode 100644 index 0000000..f63ff10 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# Beat Pocket + +A browser extension that gives [beat-battle.net](https://beat-battle.net) a +mobile-first UI without modifying the site itself. We never modify the +site's code or call its private API — we only restyle the DOM it gives us +and splice in our own components. + +## Status + +**V1 in progress.** See [`docs/PLAN.md`](./docs/PLAN.md) for full design. + +- [x] Plan +- [ ] Scaffolding (this commit) +- [ ] Build via Codex +- [ ] Smoke-test on Chromium "load unpacked" +- [ ] Package for Chrome Web Store + +## Scope (V1) + +| Screen | Treatment | +|---|---| +| Home (`/`) | Single-column, big thumb buttons, sticky login chip | +| Lobby list | Card-per-room, full-width | +| In-game | Player list → top, score/timer → safe top, action bar → sticky bottom | +| Chat | NEW: bottom drawer (closed / peeking / expanded) | +| Profile / Shop / Settings / Events | **Out of scope V1** — link to original site | + +Out: voice, native push, anything needing permissions. + +## Architecture (TL;DR) + +1. **`inject.css`** — mobile overrides for the site's existing CSS variables +2. **`detector.ts`** — `MutationObserver` watches for `.bb-room-overlay`, + route changes +3. **`layout.ts`** — reflows outer shell, mounts drawer host +4. **`chat-drawer.ts`** — bottom sheet UI + chat send-bridge + +The chat-drawer sends messages by **dispatching synthetic events on the +site's own chat textarea + button**. The server-side auth, rate limit, +mute, and profanity filters all stay intact — we don't poke React state. + +## Dev (no build step in V1) + +```sh +# Load unpacked extension +# 1. Open chrome://extensions +# 2. Toggle Developer Mode +# 3. "Load unpacked" -> select extension/ +# 4. Visit https://beat-battle.net +``` + +For Firefox: `about:debugging#/runtime/this-firefox` → "Load Temporary +Add-on" → pick `extension/manifest.json`. + +## Tech + +- Vanilla TypeScript +- esbuild for the bundle (one-time build, no HMR for V1) +- MV3 (works in Chromium + Firefox 109+) + +## Repo + +This project will be pushed to `git.molberg.cloud/bonzi/beat-pocket` once +the vault session is unlocked. + +## License + +Private until first public release. diff --git a/docs/PLAN.md b/docs/PLAN.md new file mode 100644 index 0000000..cb1a418 --- /dev/null +++ b/docs/PLAN.md @@ -0,0 +1,274 @@ +# Beat Pocket — Plan + +A browser extension (Manifest V3, Chromium + Firefox) that gives +[beat-battle.net](https://beat-battle.net) a mobile-first UI without modifying +the site itself. **We never modify the site's code or call its private API — +we only restyle the DOM it gives us, splice in our own components, and relay +chat through the user's existing authenticated session.** + +--- + +## 1. Why an extension (recap, briefly) + +| Option | Verdict | Why | +|---|---|---| +| **MV3 extension** ✅ | **chosen** | Browser already has the session cookie, no OAuth work, zero ToS risk, ships in days | +| Separate web app scraping HTML | reject | Only public API is `GET /api/online-players`; everything else is server-rendered RSC behind server actions. ToS forbids scraping. | +| Native Android app | later | Same browser-restyling problem, slower to ship, Play Store friction | +| iOS Safari extension | later | MV3 extension is API-compatible; ship web first, iOS only if we hit Safari-only users | + +Their `robots.txt` blocks AI scrapers but explicitly allows general crawlers +(`User-agent: * / Allow: /`), and a **user-installed extension acting on the +user's own browser session** is a different category from scraping.it can't be on a +blacklisted-domain list per CWS policy). + +--- + +## 2. What we know about the site (from static analysis, no account) + +- **Stack:** Next.js 15 App Router on Cloudflare (`x-powered-by: Next.js`) +- **Auth:** [`better-auth`](https://better-auth.com) — Google + Discord via + `signIn.social({ provider, callbackURL })`. Session cookie is HttpOnly, so + no JS access. Endpoint pattern: `/api/auth/*`, server action + `/api/discord/join-complete`. +- **Homepage (`/`) i18n keys we found:** + `MainMenu: { play, freeplay, ranked, custom, lobby, events, shop, profile, ... }` + — confirms **freeplay / ranked / custom / lobby** are real gameplay entry + points. +- **In-game shell class:** `bb-room-overlay` — fixed inset-0 overlay that + already has `overflow-y-auto`, `overscroll-contain`, viewport padding + `px-4 pt-6 pb-14`. **The author knew mobile was hard — they put overflow + scrolling in. The damage is in the inner layout.** +- **Realtime channel (extracted from the compiled JS, not from network):** + - WebSocket reducer with action types + `room.chat.history` / `room.chat.message` / + `LOBBY_CHAT_SEND_OPTIMISTIC` / `RECEIVE_CHAT_HISTORY` / + `RECEIVE_CHAT_MESSAGE` + - Round / phase events `room.phase` ∈ `browse | lobby | in_game` + - Player events `playerJoin`, `playerLeave`, `MATCH_NAVIGATION_STARTED` +- **Robot policy:** `search=yes, ai-train=no, use=reference` — fine for us; + we're not scraping with a bot, we're modifying what the user already sees. +- **CSS variables in use:** `--bb-accent`, `--bb-bg`, `--bb-surface`, + `--bb-border`, `--bb-ink`, `--bb-muted`, `--bb-muted-strong`, + `--bb-shadow`. They're already on a design-token system, which is a gift + for us — we inherit their theme. + +We **don't** know the in-game DOM structure yet (login-gated). V1 will have +to discover it dynamically with a `MutationObserver`. + +--- + +## 3. Architecture + +### 3.1 Components (5 files) + +``` +extension/ +├── manifest.json # MV3, scopes & matches +├── background.js # service worker, web navigation listener +├── content/ +│ ├── inject.css # mobile-first overrides for the site CSS +│ ├── detector.js # MutationObserver: finds bb-room-overlay, lobby, etc. +│ ├── layout.js # restructures outer layout:
, bottom drawer, FAB +│ ├── chat-drawer.js # reads chat DOM, opens WS hook, swaps a panel in +│ └── icons/ # 16/32/48/128 PNGs +└── README.md +``` + +### 3.2 Run-time flow + +``` +[beat-battle.net page] + │ + ▼ +[MATCH chrome:// extension loaded on this URL] + │ + inject.css applied ←────────────────────────────┐ + │ │ + ▼ │ +[detector.js] ── MutationObserver on document.body │ + detects: │ + • .bb-room-overlay (in-game) │ + • .bb-footer-nav-main-fade (home menu) │ + • route change (pushState via patchHistoryState) │ + │ │ + ▼ │ +[layout.js] runs: │ + 1. Hide desktop-only side panels (display:none) │ + 2. Re-flow main grid to 1 column │ + 3. Inject FAB ("💬") bottom-right for chat │ + 4. Mount at body end │ + │ │ + ▼ │ +[chat-drawer.js] only when in-game: │ + 1. Snapshot DOM of chat list (clone, don't move) │ + 2. Wrap WebSocket to intercept {type:"room.chat.send"} messages + and re-emit through the site's own React reducer (proxy.send) + 3. Render in , swipe-up to expand │ + │ │ + └──── re-attach on every route change ───────┘ +``` + +### 3.3 Selector strategy (defensive) + +Beat-battle uses Tailwind — class names are stable but utility-built. We +don't rely on those. We rely on: + +1. **Stable semantic hooks** the dev used: + - `bb-room-overlay` (their own naming, low-renaming risk) + - `bb-menu-item`, `bb-login-chip`, `bb-footer-nav-main-fade` + - `data-` attributes when we find them (they use very few — we'll add + some via `MutationObserver` for the bits we need) +2. **Text fingerprinting** as a fallback (DOM nodes containing exact i18n + strings like `"PLAY"` / `"CREATE ROOM"` / `"LOG IN WITH DISCORD"`) +3. **Structural position** as last resort (`body > div:nth-child(N) > …`) + +Every selector has a fallback chain: + +```js +const chatInput = + document.querySelector('.bb-chat-input') || + document.querySelector('textarea[placeholder*="chat" i]') || + document.querySelector('[aria-label*="message" i]') || + [...document.querySelectorAll('textarea')].find(t => /chat|message|comment/i.test(t.placeholder)); +``` + +--- + +## 4. V1 scope (the actual mobile pain) + +| Screen | What we do | +|---|---| +| **Home (`/`)** | Single-column menu. Big thumb-sized buttons. Login chip pinned top-right (already in their nav, just unstick it for mobile). Hide desktop leaderboard sidebar on `< 768px`. | +| **Lobby list** | Card per lobby, full-width, drag-to-refresh. | +| **In-game room** | Player list at the top (horizontal scroll on mobile). Score/timer pinned to top-safe. Kit / upload / vote actions always visible (sticky bottom action bar). | +| **Chat drawer** | New: a **bottom sheet**, hidden by default, pulls up over the game when the FAB is tapped. States: closed → peeking (last message preview) → expanded (full scrollback + input). Sends through the site’s WS so messages count. | +| **Settings/profile/events/shop** | Out of scope for V1. Desktop-equivalent — link the user to the original page in the same tab and don't restyle. | + +Things we explicitly **don't** do in V1: + +- Affective style matching (their site is a "brutalist" red-grid; we keep it) +- Native push, voice chat, anything requiring permissions +- A settings page (no point until V2) +- iOS Safari — Chromium only for V1 + +--- + +## 5. Chat drawer — how sending works (the tricky bit) + +Their chat send is React-internal: an action `LOBBY_CHAT_SEND_OPTIMISTIC` +that fires before the WebSocket round-trip. We can't reach into React's +state, but we **can** make the existing textarea + send button work in the +drawer's expanded mode: + +1. **Read:** the existing chat scrollback is a DOM node we can either: + - **(a) Clone** its contents into our drawer (read-only display), OR + - **(b) Move** the node into our drawer (write-through). We do (a) — it's + less invasive and the original UI still works if the user closes the + drawer. +2. **Send:** instead of cloning the React send flow, we + - Place a ` + + + `; + + host.appendChild(backdrop); + host.appendChild(sheet); + host.appendChild(fab); + + const scroll = sheet.querySelector('.bbp-sheet__scroll')!; + const composer = sheet.querySelector('.bbp-composer')!; + const textarea = composer.querySelector('textarea')!; + const sendBtn = composer.querySelector('button')!; + const preview = sheet.querySelector('.bbp-sheet__peek-preview')!; + const closeBtn = sheet.querySelector('.bbp-sheet__close')!; + + const state: State = 'closed'; + return { + root: host, + fab, + sheet, + backdrop, + scroll, + composer, + textarea, + sendBtn, + closeBtn, + preview, + state, + pollTimer: null, + }; +} + +function setState(c: DrawerContext, next: State): void { + c.state = next; + c.sheet.setAttribute('data-state', next); + c.fab.setAttribute('aria-expanded', next === 'open' ? 'true' : 'false'); + c.backdrop.setAttribute('data-open', next === 'open' ? 'true' : 'false'); + c.backdrop.style.pointerEvents = next === 'open' ? 'auto' : 'none'; +} + +function findSiteChatNodes(): { scrollback: Element | null; input: HTMLTextAreaElement | null; sendBtn: HTMLButtonElement | null } { + const scrollback = + pick(SITE.chatScrollbackCandidates) || + // fallback: any element containing > 3

//

children with chat-like text + (() => { + const cands = Array.from(document.querySelectorAll('ul, ol, [role="list"], [role="log"]')); + return cands.find((c) => c.children.length > 2) ?? null; + })(); + + const input = pick(SITE.chatInputCandidates); + const sendBtn = pick(SITE.sendButtonCandidates); + return { scrollback, input, sendBtn }; +} + +function renderScrollback(c: DrawerContext, source: Element): void { + // Clone source children, strip their CSS classes but keep their text. + const frag = document.createDocumentFragment(); + const children = Array.from(source.children).slice(-50); + for (const child of children) { + const div = document.createElement('div'); + div.className = 'bbp-msg'; + // Keep inner structure; chat users want basic formatting. + div.innerHTML = child.innerHTML; + frag.appendChild(div); + } + // Avoid clobbering user's scroll position if they've scrolled up + const atBottom = + c.scroll.scrollHeight - c.scroll.scrollTop - c.scroll.clientHeight < 80; + c.scroll.innerHTML = ''; + c.scroll.appendChild(frag); + if (atBottom) c.scroll.scrollTop = c.scroll.scrollHeight; + + // Update peek preview with last message text + const lastText = (children[children.length - 1]?.textContent ?? '').trim(); + c.preview.textContent = lastText.length > 0 ? lastText : 'Say something…'; +} + +function startPolling(c: DrawerContext): void { + if (c.pollTimer) return; + c.pollTimer = window.setInterval(() => { + const { scrollback } = findSiteChatNodes(); + if (scrollback && scrollback.children.length > 0) { + renderScrollback(c, scrollback); + } else { + c.scroll.innerHTML = ''; + } + }, 1500); +} + +function stopPolling(c: DrawerContext): void { + if (c.pollTimer) { + clearInterval(c.pollTimer); + c.pollTimer = null; + } +} + +function sendViaSiteChat(_c: DrawerContext, text: string): boolean { + const { input, sendBtn } = findSiteChatNodes(); + if (!input) return false; + // Set value via the React-friendly path (avoids React's value-caching issue) + const setter = Object.getOwnPropertyDescriptor( + HTMLTextAreaElement.prototype, + 'value', + )?.set; + setter?.call(input, text); + input.dispatchEvent(new Event('input', { bubbles: true })); + + // Prefer the site's own submit button. Fall back to Enter key. + if (sendBtn) { + sendBtn.click(); + return true; + } + input.dispatchEvent( + new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true }), + ); + return true; +} + +function wireEvents(c: DrawerContext): void { + c.fab.addEventListener('click', () => { + setState(c, c.state === 'closed' ? 'open' : 'closed'); + if (c.state === 'open') { + startPolling(c); + c.textarea.focus(); + } else { + stopPolling(c); + } + }); + + c.closeBtn.addEventListener('click', () => setState(c, 'closed')); + c.backdrop.addEventListener('click', () => setState(c, 'closed')); + + // Tap on the peek-strip opens the sheet + c.preview.addEventListener('click', () => { + if (c.state === 'peek') setState(c, 'open'); + }); + + // Drag handle: simple toggle between peek/open. Real swipe gesture is out of V1. + c.sheet.querySelector('.bbp-sheet__handle')!.addEventListener('click', () => { + if (c.state === 'closed') setState(c, 'peek'); + else if (c.state === 'peek') setState(c, 'open'); + else setState(c, 'closed'); + if (c.state === 'open') startPolling(c); + else if (c.state === 'closed') stopPolling(c); + }); + + c.composer.addEventListener('submit', (e) => { + e.preventDefault(); + const text = c.textarea.value.trim(); + if (!text) return; + c.sendBtn.disabled = true; + const ok = sendViaSiteChat(c, text); + // Optimistic UX: clear input whether or not it actually lands. + c.textarea.value = ''; + if (!ok) { + c.textarea.placeholder = 'Chat not available right now'; + } + setTimeout(() => (c.sendBtn.disabled = false), 300); + }); + + // Auto-grow textarea on input + c.textarea.addEventListener('input', () => { + c.textarea.style.height = 'auto'; + c.textarea.style.height = Math.min(c.textarea.scrollHeight, 120) + 'px'; + }); +} + +export function mountDrawer(): void { + if (ctx) return; + ctx = buildDrawer(); + wireEvents(ctx); + // Start in peek mode after a short delay so we don't flash during route load + setTimeout(() => { + if (ctx) setState(ctx, 'peek'); + }, 800); +} + +export function unmountDrawer(): void { + if (!ctx) return; + stopPolling(ctx); + ctx.fab.remove(); + ctx.sheet.remove(); + ctx.backdrop.remove(); + ctx = null; +} diff --git a/extension/content/detector.ts b/extension/content/detector.ts new file mode 100644 index 0000000..c2b1306 --- /dev/null +++ b/extension/content/detector.ts @@ -0,0 +1,89 @@ +/** + * detector.ts — route + overlay observer. + * + * - Watches the document for SPA route changes (Next.js pushState) + * - Watches for .bb-room-overlay appearing (in-game state) + * - Emits `bbp:phase` on the document with `phase` ∈ "home" | "lobby-list" | "lobby" | "in-game" | "login" + */ + +import { isHome, isLogin, isInGame, isLobbyList } from './util/selectors'; + +export type Phase = 'home' | 'lobby-list' | 'login' | 'in-game' | 'unknown'; + +let lastPhase: Phase = 'unknown'; + +function detectPhase(): Phase { + if (isLogin()) return 'login'; + if (isInGame()) return 'in-game'; + if (isLobbyList()) return 'lobby-list'; + if (isHome()) return 'home'; + return 'unknown'; +} + +export function currentPhase(): Phase { + return lastPhase; +} + +function emit(newPhase: Phase): void { + if (newPhase === lastPhase) return; + lastPhase = newPhase; + document.dispatchEvent(new CustomEvent('bbp:phase', { detail: { phase: newPhase } })); +} + +/** Patch history.pushState / replaceState to detect SPA route changes. */ +function patchHistory(): void { + const origPush = history.pushState.bind(history); + const origReplace = history.replaceState.bind(history); + history.pushState = function (...args: Parameters) { + const r = origPush(...args); + queueMicrotask(() => emit(detectPhase())); + return r; + }; + history.replaceState = function (...args: Parameters) { + const r = origReplace(...args); + queueMicrotask(() => emit(detectPhase())); + return r; + }; + window.addEventListener('popstate', () => emit(detectPhase())); +} + +/** Observe DOM for overlay appearing / disappearing. */ +function observeOverlay(): void { + const obs = new MutationObserver(() => emit(detectPhase())); + obs.observe(document.documentElement, { childList: true, subtree: true }); +} + +/** + * Wait until any of the candidate selectors exist, then resolve. + */ +export function waitFor(selector: string | string[], timeoutMs = 8000): Promise { + const sels = Array.isArray(selector) ? selector : [selector]; + return new Promise((resolve) => { + for (const s of sels) { + const el = document.querySelector(s); + if (el) return resolve(el); + } + const obs = new MutationObserver(() => { + for (const s of sels) { + const el = document.querySelector(s); + if (el) { + obs.disconnect(); + return resolve(el); + } + } + }); + obs.observe(document.documentElement, { childList: true, subtree: true }); + setTimeout(() => { + obs.disconnect(); + resolve(null); + }, timeoutMs); + }); +} + +export function startDetector(): Phase { + patchHistory(); + observeOverlay(); + // Run once now + emit(detectPhase()); + return lastPhase; +} diff --git a/extension/content/index.ts b/extension/content/index.ts new file mode 100644 index 0000000..b0a8933 --- /dev/null +++ b/extension/content/index.ts @@ -0,0 +1,52 @@ +/** + * Content entry. Runs in the page's main world via the manifest declaration. + * Coordinates: detector → layout → drawer mount/unmount on phase change. + */ + +import { startDetector, currentPhase } from './detector'; +import { ensureHost, applyHomeLayout, applyInGameLayout } from './layout'; +import { mountDrawer, unmountDrawer } from './chat-drawer'; +import { instrumentWS } from './util/ws-proxy'; + +function bootstrap(): void { + // 1. Always inject a host element so we can mount UI. + ensureHost(); + + // 2. Lightly instrument WebSocket so we can confirm auth is alive (no payload capture). + instrumentWS(); + + // 3. React to phase changes + document.addEventListener('bbp:phase', (e) => { + const phase = (e as CustomEvent).detail?.phase as string; + applyHomeLayoutIfHome(phase); + applyInGameLayoutIfInGame(phase); + drawerForPhase(phase); + }); + + // 4. Start detector + startDetector(); + + // 5. Initial layout based on current phase (the detector emit() handles this + // too, but we double-call here so the first frame isn't unstyled.) + const phase = currentPhase(); + applyHomeLayoutIfHome(phase); + applyInGameLayoutIfInGame(phase); + drawerForPhase(phase); +} + +function applyHomeLayoutIfHome(phase: string): void { + if (phase === 'home' || phase === 'lobby-list') applyHomeLayout(); +} + +function applyInGameLayoutIfInGame(phase: string): void { + if (phase === 'in-game') applyInGameLayout(); +} + +function drawerForPhase(phase: string): void { + if (phase === 'in-game') mountDrawer(); + else unmountDrawer(); +} + +// Wait for body so we can append to it. +if (document.body) bootstrap(); +else document.addEventListener('DOMContentLoaded', bootstrap); diff --git a/extension/content/inject.css b/extension/content/inject.css new file mode 100644 index 0000000..07f60df --- /dev/null +++ b/extension/content/inject.css @@ -0,0 +1,282 @@ +/** + * inject.css — Mobile-first overrides. + * + * Strategy: + * - Respect the site's existing CSS custom properties (--bb-accent, --bb-bg, + * --bb-surface, etc). Don't fight the theme. + * - Single column at < 768px. Hide sidebars that don't have a mobile design. + * - Make tap targets >= 44x44 px (Apple HIG floor). + * - Disable scroll bounce inside .bb-room-overlay where they already + * thought about it. + * - Anything we don't know how to restyle gets left alone — selective + * coverage is safer than a global override disaster. + */ + +@namespace url(http://www.w3.org/1999/xhtml); +@namespace svg url(http://www.w3.org/2000/svg); + +@media (max-width: 900px) { + /* Home / lobby list: single column, no fixed-position side panels */ + .bb-sidebar, + .bb-leaderboard-side, + .bb-right-rail, + [data-bb-leaderboard-side] { + display: none !important; + } + + /* Menu items: bigger, easier to tap */ + .bb-menu-item, + [data-bb-menu-item] { + min-height: 56px !important; + font-size: 1.05rem !important; + padding-top: 0.85rem !important; + padding-bottom: 0.85rem !important; + } + + /* Footer nav: make room for our chat-drawer FAB */ + .bb-footer-nav-main-fade { + padding-bottom: env(safe-area-inset-bottom) !important; + } + + /* Login chip: pin top-right, doesn't compete with menu */ + .bb-login-chip { + top: max(8px, env(safe-area-inset-top)) !important; + right: max(8px, env(safe-area-inset-right)) !important; + z-index: 60 !important; + } + + /* In-game overlay: leave their outer scroll, but */ + .bb-room-overlay { + padding-bottom: calc(72px + env(safe-area-inset-bottom)) !important; + } + + /* Make sure dialogs don't go off screen */ + .bb-modal, + [role="dialog"] { + max-width: 100vw !important; + margin: 0 !important; + } +} + +/* -------------------- Beat Pocket injected DOM -------------------- */ + +:root { + --bbp-accent: var(--bb-accent, #ff4d3d); + --bbp-bg: var(--bb-bg, #0a0a0a); + --bbp-surface: var(--bb-surface, #141414); + --bbp-ink: var(--bb-ink, #f5f5f5); + --bbp-border: var(--bb-border, #2a2a2a); + --bbp-fab-shadow: 0 6px 20px rgba(0, 0, 0, 0.45); +} + +#bbp-host { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 9999; + font-family: inherit; +} + +#bbp-host * { + box-sizing: border-box; +} + +/* FAB */ +.bbp-fab { + pointer-events: auto; + position: fixed; + right: max(14px, env(safe-area-inset-right)); + bottom: calc(72px + env(safe-area-inset-bottom)); + width: 56px; + height: 56px; + border-radius: 50%; + background: var(--bbp-accent); + color: #fff; + border: 2px solid #000; + font-size: 22px; + display: inline-flex; + align-items: center; + justify-content: center; + box-shadow: var(--bbp-fab-shadow); + cursor: pointer; + touch-action: manipulation; + font-weight: 700; + transition: transform 120ms ease-out; +} +.bbp-fab:active { + transform: scale(0.92); +} +.bbp-fab[aria-expanded="true"] { + background: var(--bbp-surface); + color: var(--bbp-accent); +} + +.bbp-fab__badge { + position: absolute; + top: -4px; + right: -4px; + min-width: 20px; + height: 20px; + border-radius: 999px; + background: #fff; + color: #000; + font-size: 11px; + font-weight: 800; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 5px; + border: 2px solid #000; +} + +/* Backdrop */ +.bbp-backdrop { + pointer-events: auto; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0); + transition: background 180ms ease-out; + z-index: 1; +} +.bbp-backdrop[data-open="true"] { + background: rgba(0, 0, 0, 0.45); +} + +/* Bottom sheet */ +.bbp-sheet { + pointer-events: auto; + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 2; + background: var(--bbp-surface); + color: var(--bbp-ink); + border-top: 2px solid var(--bbp-border); + border-radius: 18px 18px 0 0; + transform: translateY(100%); + transition: transform 220ms cubic-bezier(0.16, 1, 0.3, 1); + display: flex; + flex-direction: column; + max-height: 85vh; + padding-bottom: env(safe-area-inset-bottom); + font-family: inherit; + box-shadow: 0 -8px 30px rgba(0, 0, 0, 0.5); +} +.bbp-sheet[data-state="peek"] { + transform: translateY(calc(100% - 64px)); +} +.bbp-sheet[data-state="open"] { + transform: translateY(0); +} + +.bbp-sheet__handle { + width: 44px; + height: 5px; + border-radius: 999px; + background: var(--bbp-border); + margin: 8px auto 0; +} + +.bbp-sheet__head { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 16px 12px; + border-bottom: 1px solid var(--bbp-border); +} + +.bbp-sheet__title { + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + font-size: 0.85rem; + color: var(--bbp-ink); +} + +.bbp-sheet__close { + background: transparent; + color: var(--bbp-ink); + border: none; + font-size: 22px; + padding: 6px 10px; + cursor: pointer; +} + +.bbp-sheet__peek-preview { + padding: 12px 16px; + font-size: 0.9rem; + color: var(--bbp-ink); + opacity: 0.85; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.bbp-sheet__scroll { + flex: 1; + overflow-y: auto; + padding: 8px 12px; + -webkit-overflow-scrolling: touch; +} + +.bbp-msg { + padding: 8px 10px; + border-radius: 8px; + font-size: 0.92rem; + line-height: 1.35; +} +.bbp-msg + .bbp-msg { + margin-top: 4px; +} +.bbp-msg__author { + font-weight: 700; + margin-right: 6px; +} +.bbp-msg__time { + opacity: 0.5; + font-size: 0.75rem; + margin-left: 6px; +} + +.bbp-composer { + border-top: 1px solid var(--bbp-border); + display: flex; + gap: 8px; + padding: 10px 12px; + background: var(--bbp-bg); +} +.bbp-composer textarea { + flex: 1; + resize: none; + min-height: 40px; + max-height: 120px; + border: 1px solid var(--bbp-border); + background: var(--bbp-bg); + color: var(--bbp-ink); + border-radius: 8px; + padding: 8px 10px; + font: inherit; + font-size: 0.95rem; +} +.bbp-composer button { + background: var(--bbp-accent); + color: #fff; + border: 2px solid #000; + font-weight: 800; + letter-spacing: 0.06em; + padding: 0 14px; + border-radius: 8px; + cursor: pointer; + text-transform: uppercase; + font-size: 0.8rem; +} +.bbp-composer button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Hide Beat Pocket FAB when the site explicitly shows its own chat UI */ +body.bbp-hide-fab .bbp-fab { + display: none; +} diff --git a/extension/content/layout.ts b/extension/content/layout.ts new file mode 100644 index 0000000..5814435 --- /dev/null +++ b/extension/content/layout.ts @@ -0,0 +1,51 @@ +/** + * layout.ts — restructure the outer shell on mobile: + * + * - Mount #bbp-host for our overlay UI (drawer, FAB). + * - For home: hide leaderboard sidebars, ensure single-column. + * - For in-game: leave the room overlay intact, just plumb our drawer. + * + * Reads selectors defensively (never breaks if a class is renamed). + */ + +import { isHome, isInGame, SITE } from './util/selectors'; + +let hostEl: HTMLDivElement | null = null; + +export function ensureHost(): HTMLDivElement { + if (hostEl && document.contains(hostEl)) return hostEl; + hostEl = document.createElement('div'); + hostEl.id = 'bbp-host'; + hostEl.setAttribute('aria-hidden', 'false'); + // Must be at the END of body so it's above everything else visually, + // but pointer-events: none on root so it doesn't block. + document.body.appendChild(hostEl); + return hostEl; +} + +export function applyHomeLayout(): void { + if (!isHome()) return; + // Hide desktop-only side panels. We rely on the same defensive selectors + // we use everywhere; if the site changes layout, we just won't reflow. + document.querySelectorAll( + '.bb-sidebar, [data-bb-sidebar], .bb-leaderboard, [data-bb-leaderboard]', + ).forEach((el) => (el.style.display = 'none')); + + // Make the main column full-width on mobile + const main = document.querySelector('main, [role="main"], .bb-main, [data-bb-main]'); + if (main) { + main.style.maxWidth = '100%'; + main.style.paddingLeft = '8px'; + main.style.paddingRight = '8px'; + } +} + +export function applyInGameLayout(): void { + if (!isInGame()) return; + // Don't touch .bb-room-overlay's outer behaviour — it's already mobile-aware. + // We just make sure there's room for our FAB at the bottom. + const overlay = document.querySelector(SITE.overlay); + if (overlay) { + overlay.style.paddingBottom = 'calc(96px + env(safe-area-inset-bottom))'; + } +} diff --git a/extension/content/util/selectors.ts b/extension/content/util/selectors.ts new file mode 100644 index 0000000..785fa4c --- /dev/null +++ b/extension/content/util/selectors.ts @@ -0,0 +1,95 @@ +/** + * 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( + 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]'); +} diff --git a/extension/content/util/ws-proxy.ts b/extension/content/util/ws-proxy.ts new file mode 100644 index 0000000..4c7381b --- /dev/null +++ b/extension/content/util/ws-proxy.ts @@ -0,0 +1,55 @@ +/** + * WebSocket instrumentation. We wrap WebSocket.prototype.send so we can: + * 1. Observe outgoing chat messages (log only, no payload capture for V1) + * 2. Optionally resend synthetic chat messages if the React dispatch + * path is broken (fallback for chat-drawer.send). + * + * Idempotent — safe to call multiple times. + */ + +interface WSInstrumentation { + restore: () => void; + sentCount: () => number; +} + +const FLAG = '__beatPocketWSInstrumented__'; + +export function instrumentWS(): WSInstrumentation { + const proto = WebSocket.prototype as unknown as Record; + if (proto[FLAG]) { + return existingInstrumentation(); + } + const originalSend = proto.send as (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void; + let sent = 0; + + proto.send = function patchedSend(data: unknown): void { + sent += 1; + try { + // Lightweight logging only — we never read or store payload contents. + // Just observe that WS frames flow so we know auth is alive. + if (typeof data === 'string') { + window.dispatchEvent(new CustomEvent('bbp:ws-send', { detail: { len: data.length } })); + } + } catch { + /* never throw out of a wrapped primitive */ + } + return originalSend.call(this, data as never); + }; + + proto[FLAG] = { originalSend, sent: () => sent, restore: () => restore() }; + + function restore() { + if (proto[FLAG] && (proto[FLAG] as { originalSend?: unknown }).originalSend) { + proto.send = (proto[FLAG] as { originalSend: typeof originalSend }).originalSend; + delete proto[FLAG]; + } + } + + return { restore, sentCount: () => sent }; +} + +function existingInstrumentation(): WSInstrumentation { + const proto = WebSocket.prototype as unknown as Record; + const flag = proto[FLAG] as { sent: () => number; restore: () => void }; + return { restore: flag.restore, sentCount: flag.sent }; +} diff --git a/extension/icons/icon-128.png b/extension/icons/icon-128.png new file mode 100644 index 0000000000000000000000000000000000000000..63cc56a69fd21ead2e1a3c80793f7ad8143b13b4 GIT binary patch literal 603 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrU{dmQaSW-r_4e-Fz#|F_t`FB9 zWO{Q%NasS<)`c6_4&Tt3{mwJ`U}3ce`jJ* zU|=(T9x1FCEB*y{NCeXlu!faUiV8+S>{^lZiHc7TU z{16p+ESuwbuXD5P7h5O(A(4ZzM&5%do?W14@BmT$_Dj4kS$! z>*9&mFxn)3;TLx`W4CMj8i~A}71eAv43td%OmAn3J~rXNvOtv$xiXwNGM-G)$0r?l zc2Q+RtqrGC%e6&QuK50FR(YZ8>>Jy1-=Qf=@Eup-^BMN9_0O@oNCdLT>ok5~@7~bF zv;V5652F%CU7%pqsSnpb9@(&gXKRqnZ`(mDQ+<5R7F-_sX; z3fdwXA8kXO8e_aaruhG2vtN4A;8g=-`GE%YLyhO#nBxR^+ZI(k&|Z8nK$gFY?c{=t jNxZY%UTR3NhOjdzi1}qPRA{FH9mC-1>gTe~DWM4fY*#~# literal 0 HcmV?d00001 diff --git a/extension/icons/icon-48.png b/extension/icons/icon-48.png new file mode 100644 index 0000000000000000000000000000000000000000..37b4d8a800637d64a4a9dc90841c682b7a98420f GIT binary patch literal 235 zcmVn(kCr%=zd?nS|*vL;MqjP zP|ta&O%d4}eim|eO{9bvYIXTY{URhx!tatW4ZlOeJp5=p4}s4Ot-_DSw3cuUFO6*~ z;63}wQ^>vcquzy(+4o#V6rcbFkOV4zM$7-m>Te|wHgn~-(+~%m@<&RDLp?-U1){J8 lQCfs3?!uJU2_b~k=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/chrome": { + "version": "0.0.270", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.270.tgz", + "integrity": "sha512-ADvkowV7YnJfycZZxL2brluZ6STGW+9oKG37B422UePf2PCXuFA/XdERI0T18wtuWPx0tmFeZqq6MOXVk1IC+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "node_modules/@types/filesystem": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", + "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", + "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/har-format": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..11ce718 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "beat-pocket", + "version": "0.1.0", + "private": true, + "description": "Mobile-first browser extension for beat-battle.net", + "scripts": { + "build": "node esbuild.config.mjs", + "watch": "node esbuild.config.mjs --watch", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@types/chrome": "^0.0.270", + "esbuild": "^0.21.0", + "typescript": "^5.4.0" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8bc7782 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "Bundler", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "strict": true, + "noImplicitAny": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "noEmit": true, + "types": ["chrome"] + }, + "include": ["extension/**/*.ts"], + "exclude": ["dist", "node_modules"] +}