diff --git a/src/supported.css b/src/supported.css new file mode 100644 index 0000000..ef50e58 --- /dev/null +++ b/src/supported.css @@ -0,0 +1,238 @@ +/* ── Supported Devices Page ────────────────────────────────────────────── */ + +.devices-hero { + max-width: 720px; + margin: 0 auto 3rem; + padding: 4rem 1.5rem 0; + text-align: center; +} + +.devices-hero .eyebrow { + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.12em; + color: var(--color-muted, #888); + text-transform: uppercase; + margin: 0 0 0.75rem; +} + +.devices-hero h1 { + font-size: clamp(2.4rem, 6vw, 3.6rem); + font-weight: 800; + line-height: 1.08; + margin: 0 0 1.25rem; + letter-spacing: -0.03em; +} + +.devices-hero h1 em { + font-style: normal; + color: var(--color-accent, #6366f1); +} + +.devices-hero .lead { + font-size: 1.05rem; + color: var(--color-muted, #888); + margin: 0; + line-height: 1.6; +} + +/* ── Filter bar ─────────────────────────────────────────────────────────── */ +.filter-bar { + max-width: 900px; + margin: 0 auto 1.5rem; + padding: 0 1.5rem; + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + align-items: center; +} + +.search-input { + flex: 1 1 180px; + min-width: 0; + padding: 0.45rem 0.85rem; + border-radius: 8px; + border: 1.5px solid var(--color-border, #e4e4e7); + background: var(--color-surface, #fff); + color: inherit; + font-size: 0.9rem; + outline: none; + transition: border-color 0.15s; +} + +.search-input:focus { + border-color: var(--color-accent, #6366f1); +} + +.ftabs { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} + +.ftab { + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.35rem 0.75rem; + border-radius: 20px; + border: 1.5px solid var(--color-border, #e4e4e7); + background: transparent; + color: var(--color-muted, #888); + font-size: 0.8rem; + font-weight: 500; + cursor: pointer; + transition: background 0.12s, color 0.12s, border-color 0.12s; +} + +.ftab:hover { background: var(--color-hover, #f4f4f5); color: inherit; } +.ftab.on { background: var(--color-accent, #6366f1); color: #fff; border-color: var(--color-accent, #6366f1); } + +.ftab-n { + background: rgba(0,0,0,.08); + border-radius: 10px; + padding: 0 5px; + font-size: 0.72rem; +} + +.ftab.on .ftab-n { background: rgba(255,255,255,.25); } + +/* ── Stat row ────────────────────────────────────────────────────────────── */ +.stat-row { + max-width: 900px; + margin: 0 auto 2rem; + padding: 0 1.5rem; + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.stat-chip { + display: inline-flex; + align-items: center; + gap: 0.35rem; + font-size: 0.8rem; + color: var(--color-muted, #888); +} + +.sdot { + width: 8px; height: 8px; + border-radius: 50%; + display: inline-block; + flex-shrink: 0; +} + +.sdot-supported { background: #22c55e; } +.sdot-pr { background: #a855f7; } +.sdot-quickwin { background: #3b82f6; } +.sdot-likely { background: #f59e0b; } +.sdot-driver { background: #f97316; } +.sdot-unknown { background: #94a3b8; } + +/* ── Device list ─────────────────────────────────────────────────────────── */ +#device-list { + max-width: 900px; + margin: 0 auto; + padding: 0 1.5rem; +} + +.brand-grp { margin-bottom: 1.75rem; } + +.brand-lbl { + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--color-muted, #888); + margin-bottom: 0.5rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.brand-reqs { + font-weight: 400; + text-transform: none; + letter-spacing: 0; + font-size: 0.72rem; +} + +.device-card { + border: 1.5px solid var(--color-border, #e4e4e7); + border-radius: 10px; + overflow: hidden; +} + +.device-row { + display: grid; + grid-template-columns: 1fr auto auto; + gap: 0.75rem; + align-items: center; + padding: 0.65rem 1rem; + border-bottom: 1px solid var(--color-border, #e4e4e7); +} + +.device-row:last-child { border-bottom: none; } + +.device-name { + font-size: 0.88rem; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.device-note { + font-size: 0.75rem; + color: var(--color-muted, #888); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.spill { + font-size: 0.72rem; + font-weight: 600; + padding: 0.2rem 0.55rem; + border-radius: 20px; + white-space: nowrap; +} + +.spill-supported { background: #dcfce7; color: #15803d; } +.spill-pr { background: #f3e8ff; color: #7e22ce; } +.spill-quickwin { background: #dbeafe; color: #1d4ed8; } +.spill-likely { background: #fef3c7; color: #b45309; } +.spill-driver { background: #ffedd5; color: #c2410c; } +.spill-unknown { background: #f1f5f9; color: #475569; } + +[data-theme="dark"] .spill-supported { background: #14532d; color: #86efac; } +[data-theme="dark"] .spill-pr { background: #3b0764; color: #d8b4fe; } +[data-theme="dark"] .spill-quickwin { background: #1e3a5f; color: #93c5fd; } +[data-theme="dark"] .spill-likely { background: #451a03; color: #fcd34d; } +[data-theme="dark"] .spill-driver { background: #431407; color: #fdba74; } +[data-theme="dark"] .spill-unknown { background: #1e293b; color: #94a3b8; } + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .spill-supported { background: #14532d; color: #86efac; } + :root:not([data-theme="light"]) .spill-pr { background: #3b0764; color: #d8b4fe; } + :root:not([data-theme="light"]) .spill-quickwin { background: #1e3a5f; color: #93c5fd; } + :root:not([data-theme="light"]) .spill-likely { background: #451a03; color: #fcd34d; } + :root:not([data-theme="light"]) .spill-driver { background: #431407; color: #fdba74; } + :root:not([data-theme="light"]) .spill-unknown { background: #1e293b; color: #94a3b8; } +} + +.req-n { + font-size: 0.72rem; + color: var(--color-muted, #888); + white-space: nowrap; + min-width: 38px; + text-align: right; +} + +.req-n.hot { color: #ef4444; font-weight: 700; } + +.no-results { + text-align: center; + color: var(--color-muted, #888); + padding: 3rem 0; +} diff --git a/src/supported.ts b/src/supported.ts new file mode 100644 index 0000000..641c06f --- /dev/null +++ b/src/supported.ts @@ -0,0 +1,647 @@ +import "./styles.css"; +import "./supported.css"; + +// ── Theme ───────────────────────────────────────────────────────────────── +type Theme = "light" | "dark"; +type ThemePreference = Theme | "system"; + +const colorSchemeQuery = window.matchMedia("(prefers-color-scheme: dark)"); + +function preferredTheme(): ThemePreference { + try { + const v = localStorage.getItem("openmouse-theme-preference"); + if (v === "system" || v === "light" || v === "dark") return v; + } catch { /* storage unavailable */ } + return "system"; +} + +function resolvedTheme(pref: ThemePreference): Theme { + return pref === "system" ? (colorSchemeQuery.matches ? "dark" : "light") : pref; +} + +let themePreference = preferredTheme(); + +function applyTheme(): void { + const theme = resolvedTheme(themePreference); + document.documentElement.dataset.theme = theme; + document.querySelector('meta[name="theme-color"]') + ?.setAttribute("content", theme === "dark" ? "#09090b" : "#ffffff"); + try { localStorage.setItem("openmouse-theme-preference", themePreference); } catch { /* ok */ } +} + +function updateThemeToggle(): void { + const toggle = document.querySelector("#theme-toggle"); + if (!toggle) return; + const label = themePreference[0].toUpperCase() + themePreference.slice(1); + toggle.textContent = `Theme: ${label}`; + const next = themePreference === "system" ? "light" : themePreference === "light" ? "dark" : "system"; + toggle.setAttribute("aria-label", `Color theme: ${label}. Click to switch to ${next}`); +} + +applyTheme(); +colorSchemeQuery.addEventListener("change", () => { + if (themePreference === "system") applyTheme(); +}); + +// ── Data ────────────────────────────────────────────────────────────────── +type Status = "supported" | "pr" | "quickwin" | "likely" | "driver" | "unknown"; + +interface Mouse { + brand: string; + model: string; + status: Status; + req: number; + note: string; +} + +const STATUS: Record = { + supported: { label: "Supported", order: 0 }, + pr: { label: "PR pending", order: 1 }, + quickwin: { label: "Quick Win", order: 2 }, + likely: { label: "Test Needed", order: 3 }, + driver: { label: "Driver Needed", order: 4 }, + unknown: { label: "Unknown", order: 5 }, +}; + +const TABS: Array<{ key: Status | "all"; label: string }> = [ + { key: "all", label: "All" }, + { key: "supported", label: "Supported" }, + { key: "pr", label: "PR pending" }, + { key: "quickwin", label: "Quick Win" }, + { key: "likely", label: "Test Needed" }, + { key: "driver", label: "Driver Needed" }, + { key: "unknown", label: "Unknown" }, +]; + +const MICE: Mouse[] = [ + // LOGITECH ───────────────────────────────────────────────────────────── + { brand: "Logitech", model: "G502 (all variants)", status: "likely", req: 42, + note: "Multiple G502 generations requested — G502 HERO wired supported; Lightspeed via dongle likely working" }, + { brand: "Logitech", model: "G203 LIGHTSYNC", status: "supported", req: 3, + note: "PID 0xc092 — HID++ direct, legacy DPI feature 0x2201" }, + { brand: "Logitech", model: "G203 PRODIGY", status: "supported", req: 2, + note: "PID 0xc084 — HID++ direct, legacy DPI feature 0x2201" }, + { brand: "Logitech", model: "G102 LIGHTSYNC", status: "supported", req: 1, + note: "PID 0xc089 — same family as G203, HID++ direct" }, + { brand: "Logitech", model: "G402 Hyperion Fury", status: "supported", req: 3, + note: "PID 0xc07e — HID++ direct, legacy DPI feature 0x2201" }, + { brand: "Logitech", model: "G303 Daedalus Apex", status: "supported", req: 1, + note: "PID 0xc080 — HID++ direct" }, + { brand: "Logitech", model: "G Pro (2017)", status: "supported", req: 2, + note: "PID 0xc085 — HID++ direct, DPI feature 0x2202" }, + { brand: "Logitech", model: "G502 HERO (wired)", status: "supported", req: 3, + note: "PID 0xc08b — HID++ direct, DPI feature 0x2202" }, + { brand: "Logitech", model: "G Pro Hero", status: "supported", req: 2, + note: "PID 0xc08c — HID++ direct" }, + { brand: "Logitech", model: "G903 HERO (wired)", status: "supported", req: 2, + note: "PID 0xc08e — HID++ direct" }, + { brand: "Logitech", model: "G403 HERO (wired)", status: "supported", req: 1, + note: "PID 0xc08f — HID++ direct, DPI feature 0x2202" }, + { brand: "Logitech", model: "G Pro X Superlight", status: "supported", req: 4, + note: "PID 0xc094 — HID++ direct" }, + { brand: "Logitech", model: "G305", status: "supported", req: 8, + note: "Via Lightspeed USB receiver — confirmed ✓" }, + { brand: "Logitech", model: "MX Master 3S", status: "supported", req: 8, + note: "Logi Bolt receiver already in driver" }, + { brand: "Logitech", model: "G502 X (wired)", status: "quickwin", req: 31, + note: "HID++ direct — PID not yet confirmed, needs hardware test" }, + { brand: "Logitech", model: "G502 X PLUS (wireless)", status: "quickwin", req: 27, + note: "Via Lightspeed dongle — PID not yet confirmed" }, + { brand: "Logitech", model: "G502 X LIGHTSPEED", status: "quickwin", req: 2, + note: "Wireless Lightspeed variant — PID needs confirmation" }, + { brand: "Logitech", model: "MX Vertical", status: "driver", req: 9, + note: "Bluetooth primary — WebHID Bluetooth support limited in Chrome" }, + { brand: "Logitech", model: "MX Master 4", status: "driver", req: 8, + note: "Bluetooth primary — WebHID Bluetooth support limited in Chrome" }, + { brand: "Logitech", model: "G304", status: "likely", req: 7, + note: "Via Lightspeed receiver — test at dev.openmouse.app" }, + { brand: "Logitech", model: "G Pro Wireless", status: "likely", req: 6, + note: "Via Lightspeed dongle — likely working" }, + { brand: "Logitech", model: "G300s", status: "driver", req: 4, + note: "Older HID++ protocol variant — needs investigation" }, + { brand: "Logitech", model: "G600", status: "driver", req: 4, + note: "12-button side panel — HID++ protocol variant not yet implemented" }, + { brand: "Logitech", model: "G Pro 2 Lightspeed", status: "likely", req: 3, + note: "Via Lightspeed dongle — needs PID check in registry" }, + { brand: "Logitech", model: "G502 Lightspeed", status: "likely", req: 2, + note: "Via Lightspeed USB dongle — likely working" }, + { brand: "Logitech", model: "MX Master 2S", status: "likely", req: 2, + note: "Via Logi Bolt or Lightspeed dongle — likely working" }, + { brand: "Logitech", model: "MX Anywhere 3", status: "likely", req: 2, + note: "Via Logi Bolt dongle — likely working" }, + { brand: "Logitech", model: "G309 Lightspeed", status: "likely", req: 1, + note: "Via Lightspeed USB dongle — likely working" }, + { brand: "Logitech", model: "MX Ergo S", status: "likely", req: 1, + note: "Via Logi Bolt dongle — likely working" }, + { brand: "Logitech", model: "MX Anywhere 2", status: "likely", req: 1, + note: "Via Lightspeed dongle — likely working" }, + { brand: "Logitech", model: "Compact Wireless", status: "unknown", req: 1, + note: "Protocol unknown — receiver type needs identification" }, + { brand: "Logitech", model: "G700s", status: "unknown", req: 1, + note: "HID++ 1.0 (legacy protocol) — not yet implemented" }, + { brand: "Logitech", model: "M171 Wireless", status: "driver", req: 1, + note: "Basic optical — no HID++ control channel exposed" }, + + // ROCCAT ───────────────────────────────────────────────────────────── + { brand: "Roccat", model: "Kone Pro Air", status: "driver", req: 39, + note: "Roccat Swarm / AIMO protocol — not yet implemented" }, + { brand: "Roccat", model: "Kone XP Air", status: "driver", req: 5, + note: "Roccat AIMO protocol — not yet implemented" }, + { brand: "Roccat", model: "Tyon", status: "driver", req: 2, + note: "Roccat protocol — not implemented" }, + { brand: "Roccat", model: "Nyth", status: "driver", req: 2, + note: "Roccat protocol — not implemented" }, + { brand: "Roccat", model: "Kain 100 AIMO", status: "driver", req: 2, + note: "Roccat AIMO protocol — not implemented" }, + { brand: "Roccat", model: "Kone Pro", status: "driver", req: 1, + note: "Roccat protocol — not implemented" }, + + // REDRAGON ───────────────────────────────────────────────────────────── + { brand: "Redragon", model: "M725 Honeycomb", status: "driver", req: 32, + note: "Redragon HID protocol — not implemented" }, + { brand: "Redragon", model: "Impact Elite M913", status: "driver", req: 18, + note: "Redragon HID protocol — not implemented" }, + { brand: "Redragon", model: "PREDATOR M612", status: "driver", req: 2, + note: "Redragon HID protocol — not implemented" }, + { brand: "Redragon", model: "K1ng M916 Pro 4K", status: "driver", req: 2, + note: "Redragon HID protocol — not implemented" }, + { brand: "Redragon", model: "M711 Cobra", status: "driver", req: 2, + note: "Redragon HID protocol — not implemented" }, + { brand: "Redragon", model: "M917 Pro", status: "driver", req: 2, + note: "Redragon HID protocol — not implemented" }, + { brand: "Redragon", model: "M991", status: "driver", req: 2, + note: "Redragon HID protocol — not implemented" }, + { brand: "Redragon", model: "M703", status: "driver", req: 2, + note: "Redragon HID protocol — not implemented" }, + { brand: "Redragon", model: "TAIPAN PRO M810", status: "driver", req: 2, + note: "Redragon HID protocol — not implemented" }, + { brand: "Redragon", model: "Ranger Lite", status: "driver", req: 1, + note: "Redragon HID protocol — not implemented" }, + { brand: "Redragon", model: "Infernal Dragon", status: "driver", req: 1, + note: "Redragon HID protocol — not implemented" }, + { brand: "Redragon", model: "M696 Pro", status: "driver", req: 1, + note: "Redragon HID protocol — not implemented" }, + { brand: "Redragon", model: "M721 Pro", status: "driver", req: 1, + note: "Redragon HID protocol — not implemented" }, + + // ATTACK SHARK ──────────────────────────────────────────────────────── + { brand: "Attack Shark", model: "X3", status: "driver", req: 15, + note: "VID 0x25a7 — driver skeleton added, protocol WIP" }, + { brand: "Attack Shark", model: "X11", status: "driver", req: 10, + note: "VID 0x25a7 — custom HID protocol, driver not yet implemented" }, + { brand: "Attack Shark", model: "R5 Ultra", status: "driver", req: 7, + note: "VID 0x25a7 — custom HID protocol, driver not yet implemented" }, + { brand: "Attack Shark", model: "X6", status: "driver", req: 5, + note: "VID 0x25a7 — custom HID protocol, driver not yet implemented" }, + { brand: "Attack Shark", model: "R3", status: "driver", req: 4, + note: "VID 0x25a7 — custom HID protocol, driver not yet implemented" }, + { brand: "Attack Shark", model: "R1", status: "driver", req: 3, + note: "VID 0x25a7 — custom HID protocol, driver not yet implemented" }, + { brand: "Attack Shark", model: "X8 Ultra", status: "driver", req: 3, + note: "VID 0x25a7 — custom HID protocol, driver not yet implemented" }, + { brand: "Attack Shark", model: "X8 SE", status: "driver", req: 3, + note: "VID 0x25a7 — custom HID protocol, driver not yet implemented" }, + { brand: "Attack Shark", model: "X1", status: "driver", req: 2, + note: "VID 0x25a7 — custom HID protocol, driver not yet implemented" }, + { brand: "Attack Shark", model: "V3 Pro", status: "driver", req: 1, + note: "VID 0x25a7 — custom HID protocol, driver not yet implemented" }, + { brand: "Attack Shark", model: "X3 Pro", status: "driver", req: 1, + note: "VID 0x25a7 — custom HID protocol, driver not yet implemented" }, + { brand: "Attack Shark", model: "X11 SE", status: "driver", req: 1, + note: "VID 0x25a7 — custom HID protocol, driver not yet implemented" }, + { brand: "Attack Shark", model: "X8 PLUS", status: "driver", req: 1, + note: "VID 0x25a7 — custom HID protocol, driver not yet implemented" }, + { brand: "Attack Shark", model: "X12", status: "driver", req: 1, + note: "VID 0x25a7 — custom HID protocol, driver not yet implemented" }, + { brand: "Attack Shark", model: "R6", status: "driver", req: 1, + note: "VID 0x25a7 — custom HID protocol, driver not yet implemented" }, + { brand: "Attack Shark", model: "G3", status: "driver", req: 1, + note: "VID 0x25a7 — custom HID protocol, driver not yet implemented" }, + { brand: "Attack Shark", model: "R2", status: "driver", req: 1, + note: "VID 0x25a7 — custom HID protocol, driver not yet implemented" }, + + // RAZER ─────────────────────────────────────────────────────────────── + { brand: "Razer", model: "DeathAdder V3 (wired)", status: "supported", req: 9, + note: "Confirmed working ✓" }, + { brand: "Razer", model: "DeathAdder V2 Pro", status: "supported", req: 3, + note: "0x0084 — specific driver filter" }, + { brand: "Razer", model: "Cobra", status: "supported", req: 2, + note: "0x00a3 — specific driver filter" }, + { brand: "Razer", model: "Viper V3 Pro", status: "supported", req: 1, + note: "0x00c0 / 0x00c1 — specific driver filter" }, + { brand: "Razer", model: "DeathAdder Essential", status: "supported", req: 1, + note: "0x006e / 0x0071 / 0x0098 — specific driver filters" }, + { brand: "Razer", model: "Basilisk V3 (wired)", status: "likely", req: 13, + note: "In Razer device registry — test at dev.openmouse.app" }, + { brand: "Razer", model: "Basilisk V3 Pro", status: "likely", req: 13, + note: "Wireless — in Razer registry, needs HyperSpeed dongle" }, + { brand: "Razer", model: "Viper Ultimate", status: "likely", req: 5, + note: "Wireless — in Razer registry" }, + { brand: "Razer", model: "Basilisk V3 X HyperSpeed", status: "likely", req: 5, + note: "Wireless — in Razer registry" }, + { brand: "Razer", model: "DeathAdder V2 (wired)", status: "likely", req: 3, + note: "Wired variant — in Razer registry" }, + { brand: "Razer", model: "Viper mini", status: "likely", req: 3, + note: "In Razer registry — control interface not fully pinned down" }, + { brand: "Razer", model: "Naga V2 Pro", status: "likely", req: 3, + note: "In Razer registry" }, + { brand: "Razer", model: "DeathAdder V3 Pro", status: "likely", req: 2, + note: "Wireless — in Razer registry" }, + { brand: "Razer", model: "DeathAdder V4 Pro", status: "likely", req: 2, + note: "In Razer registry" }, + { brand: "Razer", model: "Naga Trinity", status: "driver", req: 2, + note: "Older HID layout — control channel not yet mapped" }, + { brand: "Razer", model: "Naga X", status: "likely", req: 2, + note: "In Razer registry" }, + { brand: "Razer", model: "Basilisk Essential", status: "likely", req: 1, + note: "In Razer device registry" }, + { brand: "Razer", model: "Basilisk V3 HyperSpeed", status: "likely", req: 1, + note: "Wireless — in registry, needs dongle connected" }, + { brand: "Razer", model: "Basilisk", status: "likely", req: 1, + note: "In Razer device registry" }, + { brand: "Razer", model: "Orochi V2", status: "likely", req: 1, + note: "Wireless — in Razer registry" }, + { brand: "Razer", model: "Cobra HyperSpeed", status: "likely", req: 1, + note: "Wireless — in Razer registry" }, + { brand: "Razer", model: "Viper 8KHz", status: "likely", req: 1, + note: "In Razer registry" }, + { brand: "Razer", model: "Viper V3 HyperSpeed", status: "likely", req: 1, + note: "Wireless — in Razer registry" }, + { brand: "Razer", model: "Basilisk X HyperSpeed", status: "likely", req: 1, + note: "Wireless — in Razer registry" }, + { brand: "Razer", model: "Naga V2 HyperSpeed", status: "likely", req: 1, + note: "Wireless — in Razer registry" }, + { brand: "Razer", model: "Basilisk Ultimate", status: "likely", req: 1, + note: "Wireless — in Razer registry" }, + { brand: "Razer", model: "DeathAdder V2 X HyperSpeed",status: "likely", req: 1, + note: "Wireless — in Razer registry" }, + { brand: "Razer", model: "DeathAdder Elite", status: "driver", req: 1, + note: "Older protocol variant — not yet implemented" }, + { brand: "Razer", model: "Viper V3 Pro SE", status: "quickwin", req: 1, + note: "SE variant PID likely differs — verify & add to registry" }, + + // HYPERX ───────────────────────────────────────────────────────────── + { brand: "HyperX", model: "Pulsefire Haste 2", status: "driver", req: 10, + note: "Kingston NGENUITY protocol — not implemented" }, + { brand: "HyperX", model: "Pulsefire Haste 2 Wireless", status: "driver", req: 4, + note: "Kingston NGENUITY protocol — not implemented" }, + { brand: "HyperX", model: "Pulsefire FPS Pro", status: "driver", req: 2, + note: "Kingston NGENUITY protocol — not implemented" }, + { brand: "HyperX", model: "Pulsefire Fuse", status: "driver", req: 1, + note: "Kingston NGENUITY protocol — not implemented" }, + + // STEELSERIES ───────────────────────────────────────────────────────── + { brand: "SteelSeries", model: "Aerox 9 Wireless", status: "driver", req: 5, + note: "SteelSeries GG protocol — not implemented" }, + { brand: "SteelSeries", model: "Sensei Ten", status: "driver", req: 4, + note: "SteelSeries GG protocol — not implemented" }, + { brand: "SteelSeries", model: "Aerox 5", status: "driver", req: 2, + note: "SteelSeries GG protocol — not implemented" }, + { brand: "SteelSeries", model: "Rival 3 Wireless", status: "driver", req: 2, + note: "SteelSeries GG protocol — not implemented" }, + { brand: "SteelSeries", model: "Rival 650 Wireless", status: "driver", req: 2, + note: "SteelSeries GG protocol — not implemented" }, + { brand: "SteelSeries", model: "Aerox 3", status: "driver", req: 1, + note: "SteelSeries GG protocol — not implemented" }, + { brand: "SteelSeries", model: "Prime+", status: "driver", req: 1, + note: "SteelSeries GG protocol — not implemented" }, + { brand: "SteelSeries", model: "Rival 310", status: "driver", req: 1, + note: "SteelSeries GG protocol — not implemented" }, + { brand: "SteelSeries", model: "Prime Mini Wireless", status: "driver", req: 1, + note: "SteelSeries GG protocol — not implemented" }, + + // CORSAIR ───────────────────────────────────────────────────────────── + { brand: "Corsair", model: "Harpoon RGB Pro", status: "driver", req: 5, + note: "iCUE protocol — not implemented" }, + { brand: "Corsair", model: "Katar Pro", status: "driver", req: 4, + note: "iCUE protocol — not implemented" }, + { brand: "Corsair", model: "M65 Pro RGB", status: "driver", req: 3, + note: "iCUE protocol — not implemented" }, + { brand: "Corsair", model: "Sabre V2 Pro Ultralight", status: "driver", req: 3, + note: "iCUE protocol — not implemented" }, + { brand: "Corsair", model: "Scimitar Elite Wireless SE",status: "driver", req: 3, + note: "iCUE protocol — not implemented" }, + { brand: "Corsair", model: "M65 RGB Elite", status: "driver", req: 1, + note: "iCUE protocol — not implemented" }, + { brand: "Corsair", model: "Scimitar RGB Elite", status: "driver", req: 1, + note: "iCUE protocol — not implemented" }, + { brand: "Corsair", model: "Ironclaw RGB", status: "driver", req: 1, + note: "iCUE protocol — not implemented" }, + + // GLORIOUS ──────────────────────────────────────────────────────────── + { brand: "Glorious", model: "Model O 2 Pro Wireless", status: "driver", req: 5, + note: "Glorious CORE protocol — not implemented" }, + { brand: "Glorious", model: "Model O", status: "driver", req: 3, + note: "Glorious CORE protocol — not implemented" }, + { brand: "Glorious", model: "Model D Wireless", status: "driver", req: 2, + note: "Glorious CORE protocol — not implemented" }, + { brand: "Glorious", model: "Model D", status: "driver", req: 2, + note: "Glorious CORE protocol — not implemented" }, + { brand: "Glorious", model: "Model O2 Pro 4K/8K", status: "driver", req: 2, + note: "Glorious CORE protocol — not implemented" }, + { brand: "Glorious", model: "Model O Pro", status: "driver", req: 1, + note: "Glorious CORE protocol — not implemented" }, + { brand: "Glorious", model: "Model D2 Pro Wireless 8K",status: "driver", req: 1, + note: "Glorious CORE protocol — not implemented" }, + + // ENDGAME GEAR ──────────────────────────────────────────────────────── + { brand: "Endgame Gear", model: "XM2w 4K", status: "supported", req: 3, + note: "EGG vendor (0x3367) covered by vendor filter" }, + { brand: "Endgame Gear", model: "OP1w 4K v2", status: "likely", req: 7, + note: "EGG driver likely covers this model — test at dev.openmouse.app" }, + + // ATK ───────────────────────────────────────────────────────────────── + { brand: "ATK", model: "F1 V2 Ultra Max", status: "supported", req: 1, + note: "ATK vendor (0x373b, usagePage 0xff02) covered" }, + { brand: "ATK", model: "VXE Dragonfly R1 Pro", status: "likely", req: 3, + note: "ATK driver (0x373b) likely covers this — needs hardware test" }, + { brand: "ATK", model: "X1 V2 Ultimate", status: "likely", req: 1, + note: "ATK driver likely covers this — needs hardware test" }, + { brand: "ATK", model: "A9 Air", status: "likely", req: 1, + note: "ATK driver likely covers this — needs hardware test" }, + { brand: "ATK", model: "F1 Ultimate", status: "likely", req: 1, + note: "ATK driver likely covers this — needs hardware test" }, + { brand: "ATK", model: "VXE Mad R Major", status: "likely", req: 1, + note: "ATK driver likely covers this — needs hardware test" }, + { brand: "ATK", model: "Zero Normal", status: "likely", req: 1, + note: "ATK driver likely covers this — needs hardware test" }, + + // WLMOUSE ───────────────────────────────────────────────────────────── + { brand: "WLMouse", model: "Beast X", status: "supported", req: 1, + note: "PID 0xa883 / 0xa884 already in WLMouse driver" }, + + // PULSAR ────────────────────────────────────────────────────────────── + { brand: "Pulsar", model: "Tenz Signature", status: "likely", req: 6, + note: "Pulsar driver exists — PID not yet confirmed" }, + { brand: "Pulsar", model: "X2h", status: "likely", req: 3, + note: "Pulsar driver exists — PID not yet confirmed" }, + { brand: "Pulsar", model: "X2 V3 ES Mini", status: "likely", req: 2, + note: "Pulsar driver exists — PID not yet confirmed" }, + { brand: "Pulsar", model: "X3 Medium", status: "likely", req: 1, + note: "Pulsar driver exists — PID not yet confirmed" }, + { brand: "Pulsar", model: "X2F", status: "likely", req: 1, + note: "Pulsar driver exists — PID not yet confirmed" }, + + // LAMZU ─────────────────────────────────────────────────────────────── + { brand: "Lamzu", model: "Inca", status: "likely", req: 1, + note: "Lamzu driver exists — PID not yet confirmed" }, + { brand: "Lamzu", model: "Thorn", status: "likely", req: 1, + note: "Lamzu driver exists — PID not yet confirmed" }, + + // NINJUTSO ──────────────────────────────────────────────────────────── + { brand: "Ninjutso", model: "Sora", status: "likely", req: 1, + note: "Ninjutso driver exists — PID not yet confirmed" }, + + // ASUS ROG ──────────────────────────────────────────────────────────── + { brand: "ASUS ROG", model: "Harpe Ace Aim Lab", status: "driver", req: 5, + note: "Armoury Crate protocol — not implemented" }, + { brand: "ASUS ROG", model: "TUF Gaming M5", status: "driver", req: 2, + note: "Armoury Crate protocol — not implemented" }, + { brand: "ASUS ROG", model: "Gladius II Core", status: "driver", req: 1, + note: "Armoury Crate protocol — not implemented" }, + { brand: "ASUS ROG", model: "Strix Impact III", status: "driver", req: 1, + note: "Armoury Crate protocol — not implemented" }, + { brand: "ASUS ROG", model: "TUF Gaming M3 Gen 1", status: "driver", req: 1, + note: "Armoury Crate protocol — not implemented" }, + + // OTHER DRIVER-NEEDED ───────────────────────────────────────────────── + { brand: "Keychron", model: "M6", status: "quickwin", req: 1, + note: "Keychron vendor covered; M6 PID not yet in KEYCHRON_PRODUCT_IDS" }, + { brand: "Bloody", model: "Max W95", status: "driver", req: 1, + note: "A4tech / Bloody protocol — not implemented" }, + { brand: "Fantech", model: "THOR II X16", status: "driver", req: 1, + note: "Fantech protocol — not implemented" }, + { brand: "Cooler Master", model: "MasterMouse MM720", status: "driver", req: 1, + note: "CM protocol — not implemented" }, + { brand: "Cooler Master", model: "MM711", status: "driver", req: 1, + note: "CM protocol — not implemented" }, + + // UNKNOWNS ──────────────────────────────────────────────────────────── + { brand: "Hitscan", model: "Hyperlight", status: "unknown", req: 5, + note: "Protocol unknown — HID capture needed" }, + { brand: "RAWM", model: "Leviathan V4", status: "unknown", req: 5, + note: "Protocol unknown" }, + { brand: "AJAZZ", model: "AJ159 Apex", status: "unknown", req: 5, + note: "Protocol unknown" }, + { brand: "Furycube", model: "G11", status: "unknown", req: 5, + note: "Protocol unknown" }, + { brand: "Mchose", model: "K7 Ultra", status: "unknown", req: 5, + note: "Protocol unknown" }, + { brand: "Mchose", model: "L7 Ultra+", status: "unknown", req: 3, + note: "Protocol unknown" }, + { brand: "G-Wolves", model: "HTX Mini 8K", status: "unknown", req: 3, + note: "Protocol unknown" }, + { brand: "Angry Miao", model: "Infinity .97", status: "unknown", req: 3, + note: "Protocol unknown" }, + { brand: "Dark Project", model: "Novus SE", status: "unknown", req: 2, + note: "Protocol unknown — HID capture needed" }, + { brand: "Scyrox", model: "V8", status: "unknown", req: 2, + note: "Protocol unknown" }, + { brand: "Incott", model: "G23 V2 Pro", status: "unknown", req: 2, + note: "Protocol unknown" }, + { brand: "Mchose", model: "AX5", status: "unknown", req: 2, + note: "Protocol unknown" }, + { brand: "AJAZZ", model: "AJ139 Pro", status: "unknown", req: 1, + note: "Protocol unknown" }, + { brand: "AJAZZ", model: "AJ199", status: "unknown", req: 1, + note: "Protocol unknown" }, + { brand: "Gravastar", model: "Mercury X Pro", status: "unknown", req: 1, + note: "Protocol unknown" }, + { brand: "Gravastar", model: "M1", status: "unknown", req: 1, + note: "Protocol unknown" }, + { brand: "Gravastar", model: "M2", status: "unknown", req: 1, + note: "Protocol unknown" }, + { brand: "G-Wolves", model: "HT-M-H ACE Wireless",status: "unknown", req: 1, + note: "Protocol unknown" }, + { brand: "Angry Miao", model: "Infinity Mouse", status: "unknown", req: 1, + note: "Protocol unknown" }, + { brand: "VortexSeries", model: "IGNIX F3", status: "unknown", req: 1, + note: "Protocol unknown" }, + { brand: "Darmoshark", model: "M3S", status: "unknown", req: 1, + note: "Protocol unknown" }, + { brand: "RAWM", model: "MH01 Pro", status: "unknown", req: 1, + note: "Protocol unknown" }, + { brand: "Swiftpoint", model: "Z2", status: "unknown", req: 1, + note: "Has own software — complex macro/haptics protocol" }, + { brand: "Swiftpoint", model: "Z3", status: "unknown", req: 1, + note: "Has own software — complex macro/haptics protocol" }, + { brand: "Kreo", model: "Chimera V2 Wireless",status: "unknown", req: 1, + note: "Protocol unknown" }, + { brand: "Kreo", model: "Harpy", status: "unknown", req: 1, + note: "Protocol unknown" }, + { brand: "ipi", model: "Float 3950", status: "unknown", req: 1, + note: "Protocol unknown" }, + { brand: "Solakaka", model: "SM802 Pro", status: "unknown", req: 1, + note: "Has its own web configurator" }, + { brand: "Eyooso", model: "X-44 Lite", status: "unknown", req: 1, + note: "Protocol unknown" }, + { brand: "HAVIT", model: "Gamenote", status: "unknown", req: 1, + note: "Protocol unknown" }, + { brand: "Empire Gaming", model: "RF906 Wireless", status: "unknown", req: 1, + note: "Protocol unknown" }, + { brand: "DeadSkull", model: "Orbit Wired", status: "unknown", req: 1, + note: "Protocol unknown" }, + { brand: "IGM", model: "IGM-6000", status: "unknown", req: 1, + note: "Protocol unknown" }, +]; + +// ── State ───────────────────────────────────────────────────────────────── +let activeFilter: Status | "all" = "all"; +let searchQuery = ""; + +// ── Helpers ─────────────────────────────────────────────────────────────── +function counts(): Record { + const c: Record = { all: 0 }; + for (const k of Object.keys(STATUS)) c[k] = 0; + for (const m of MICE) { c[m.status]++; c.all++; } + return c; +} + +function visibleMice(): Mouse[] { + return MICE.filter(m => { + const fOk = activeFilter === "all" || m.status === activeFilter; + const q = searchQuery.toLowerCase(); + const sOk = !q || m.model.toLowerCase().includes(q) || m.brand.toLowerCase().includes(q) || m.note.toLowerCase().includes(q); + return fOk && sOk; + }); +} + +// ── Render ──────────────────────────────────────────────────────────────── +const app = document.querySelector("#app"); +if (!app) throw new Error("Root element missing"); + +const GH_SVG = ``; + +const supportedCount = MICE.filter(m => m.status === "supported").length; +const totalBrands = new Set(MICE.filter(m => m.status === "supported").map(m => m.brand)).size; + +app.innerHTML = ` + + + +
+
+

DEVICE SUPPORT

+

Community
requests.

+

+ ${supportedCount} models confirmed working across ${totalBrands} brands. + Community-requested devices are tracked below — filter by status or search by name. +

+
+ +
+ +
+
+ +
+
+
+ + +`; + +const themeToggleEl = document.querySelector("#theme-toggle"); +if (themeToggleEl) { + updateThemeToggle(); + themeToggleEl.addEventListener("click", () => { + const cycle: ThemePreference[] = ["system", "light", "dark"]; + themePreference = cycle[(cycle.indexOf(themePreference) + 1) % cycle.length]; + applyTheme(); + updateThemeToggle(); + }); +} + +document.getElementById("s-input")?.addEventListener("input", e => { + searchQuery = (e.target as HTMLInputElement).value; + renderList(); +}); + +function renderTabs(): void { + const c = counts(); + document.getElementById("ftabs")!.innerHTML = TABS.map(t => + `` + ).join(""); + document.getElementById("ftabs")!.querySelectorAll(".ftab").forEach(btn => { + btn.addEventListener("click", () => { + activeFilter = btn.dataset.key as typeof activeFilter; + renderTabs(); + renderList(); + }); + }); +} + +function renderStats(): void { + const c = counts(); + document.getElementById("stat-row")!.innerHTML = (Object.keys(STATUS) as Status[]).map(k => + ` + + ${STATUS[k].label}: ${c[k]} + ` + ).join(""); +} + +function renderList(): void { + const data = visibleMice(); + const el = document.getElementById("device-list")!; + + if (!data.length) { + el.innerHTML = `

No devices match your search.

`; + return; + } + + const groups: Record = {}; + for (const m of data) (groups[m.brand] = groups[m.brand] || []).push(m); + + const sortedBrands = Object.keys(groups).sort((a, b) => { + const ra = groups[a].reduce((s, m) => s + m.req, 0); + const rb = groups[b].reduce((s, m) => s + m.req, 0); + return rb - ra || a.localeCompare(b); + }); + + el.innerHTML = sortedBrands.map(brand => { + const items = groups[brand].sort((a, b) => + STATUS[a.status].order - STATUS[b.status].order || b.req - a.req, + ); + const totalReq = items.reduce((s, m) => s + m.req, 0); + + const rows = items.map(m => + `
+
+
${m.model}
+
${m.note}
+
+ ${STATUS[m.status].label} + ${m.req} req +
`, + ).join(""); + + return `
+
${brand} ${totalReq} request${totalReq === 1 ? "" : "s"}
+
${rows}
+
`; + }).join(""); +} + +renderTabs(); +renderStats(); +renderList(); diff --git a/src/vendors.ts b/src/vendors.ts index b611b36..420afdf 100644 --- a/src/vendors.ts +++ b/src/vendors.ts @@ -5,6 +5,11 @@ export const VENDOR_ID = { endgameGear: 0x3367, wlmouse: 0x36a7, logitech: 0x046d, + attackShark: 0x25a7, + // R5 Ultra / R3 and similar OEM variants share the Lamzu VID + attackSharkOem: 0x373e, + // R1 / X11 family — PIDs change per firmware so detection is collection-based + attackSharkX: 0x1d57, } as const; export const LOGITECH_RECEIVER_FILTER: HIDDeviceFilter = { @@ -48,6 +53,9 @@ export const SUPPORTED_HID_FILTERS: HIDDeviceFilter[] = [ { vendorId: VENDOR_ID.pulsar }, { vendorId: VENDOR_ID.endgameGear }, { vendorId: VENDOR_ID.wlmouse }, + { vendorId: VENDOR_ID.attackShark }, + { vendorId: VENDOR_ID.attackSharkOem }, + { vendorId: VENDOR_ID.attackSharkX }, ...EGG_WE_HID_FILTERS, LOGITECH_RECEIVER_FILTER, ]; diff --git a/supported.html b/supported.html new file mode 100644 index 0000000..793add0 --- /dev/null +++ b/supported.html @@ -0,0 +1,14 @@ + + + + + + + Supported Devices — OpenMouse + + + +
+ + + diff --git a/vite.config.ts b/vite.config.ts index 98bea69..234a4ef 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -20,6 +20,7 @@ export default defineConfig({ input: { main: resolve(__dirname, "index.html"), demo: resolve(__dirname, "demo.html"), + supported: resolve(__dirname, "supported.html"), control: resolve(__dirname, "control.html"), controlApp: resolve(__dirname, "control-app.html"), },