While adding test coverage for src/system/platform.ts, audited the platform-detection constants. They're functional but the underlying user-agent regex approach has known gaps in 2026, the biggest being that modern iPads identify as desktop Macs.
Current implementation
// src/system/platform.ts
export const iOS = /iPhone|iPad|iPod/i.test(ua);
export const android = /Android/i.test(ua);
export const wp = /Windows Phone/i.test(ua);
export const BlackBerry = /BlackBerry/i.test(ua);
export const Kindle = /Kindle|Silk.*Mobile Safari/i.test(ua);
export const isMobile =
/Mobi/i.test(ua) || iOS || android || wp || BlackBerry || Kindle;
What's outdated / wrong
1. iPadOS 13+ ships as Macintosh (the big one)
Since iPadOS 13 (2019), Safari's default UA on iPad is Mozilla/5.0 (Macintosh; Intel Mac OS X ...) — no iPad token. The iOS regex misses every modern iPad, and they fall through isMobile as desktop. This is a real, observable gap for our keyboard/input branches (src/input/keyboard.ts consumes isMobile).
Standard fix: also check
navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1
(a Mac with multi-touch is, in practice, always an iPad — Macs don't have touchscreens).
2. Dead platforms still scanned
wp (Windows Phone) — Microsoft EOL'd the platform in 2017. The regex matches a UA string nothing ships anymore.
BlackBerry — RIM stopped making BB10 devices in 2016; UA appears in <0.01% of traffic globally.
android2 — exported but unused; Android 2.x is from 2009-2011.
These can probably be deleted (or at least dropped from isMobile's OR chain).
3. UA sniffing is the wrong primitive
The web platform has caught up with this since the original detection was written:
navigator.maxTouchPoints — > 0 means the device has a touch digitizer
matchMedia(\"(pointer: coarse)\") — touch is the primary pointer
navigator.userAgentData — Client Hints API, gives structured browser/platform/mobility info on Chromium-based browsers (matches Sec-CH-UA-Mobile). Limited Safari/Firefox support today but the right long-term direction.
For our use cases (skip on-screen keyboard branch, pick gamepad-vs-touch input, set viewport scaling defaults), feature detection is more correct than UA detection — a touchscreen laptop should be treated as having touch capability without being "mobile".
Suggested approach
Roughly in tiers (don't have to do all at once):
-
Quick win: patch iOS to also catch the iPadOS-13-as-Mac case via navigator.platform === \"MacIntel\" && navigator.maxTouchPoints > 1. One-line fix, closes the biggest gap.
-
Add an isTouch flag (separate from isMobile) — matchMedia(\"(pointer: coarse)\").matches || (navigator.maxTouchPoints || 0) > 0. This is what most internal call sites probably actually want.
-
Deprecate the dead platforms — wp, BlackBerry, Kindle, android2 — leave the exports for backwards-compat through 19.x, mark deprecated, remove in 20.x.
-
Keep isMobile as a UA-based heuristic but document that it's best-effort and recommend isTouch / pointer: coarse for new code.
-
Long-term: migrate to navigator.userAgentData where available, fall back to UA. Out of scope for a single PR.
Test coverage
The new tests/platform.spec.ts (added in the same audit pass) pins the current behavior under Playwright + chromium. Any modernization PR should update those expectations and add cases for the iPad-on-iPadOS scenario (which the chromium test runner can simulate by overriding navigator.platform + maxTouchPoints).
🤖 Filed via Claude Code during the 19.7 / Camera3d cleanup pass
While adding test coverage for
src/system/platform.ts, audited the platform-detection constants. They're functional but the underlying user-agent regex approach has known gaps in 2026, the biggest being that modern iPads identify as desktop Macs.Current implementation
What's outdated / wrong
1. iPadOS 13+ ships as
Macintosh(the big one)Since iPadOS 13 (2019), Safari's default UA on iPad is
Mozilla/5.0 (Macintosh; Intel Mac OS X ...)— noiPadtoken. TheiOSregex misses every modern iPad, and they fall throughisMobileas desktop. This is a real, observable gap for our keyboard/input branches (src/input/keyboard.tsconsumesisMobile).Standard fix: also check
(a Mac with multi-touch is, in practice, always an iPad — Macs don't have touchscreens).
2. Dead platforms still scanned
wp(Windows Phone) — Microsoft EOL'd the platform in 2017. The regex matches a UA string nothing ships anymore.BlackBerry— RIM stopped making BB10 devices in 2016; UA appears in <0.01% of traffic globally.android2— exported but unused; Android 2.x is from 2009-2011.These can probably be deleted (or at least dropped from
isMobile's OR chain).3. UA sniffing is the wrong primitive
The web platform has caught up with this since the original detection was written:
navigator.maxTouchPoints—> 0means the device has a touch digitizermatchMedia(\"(pointer: coarse)\")— touch is the primary pointernavigator.userAgentData— Client Hints API, gives structured browser/platform/mobility info on Chromium-based browsers (matchesSec-CH-UA-Mobile). Limited Safari/Firefox support today but the right long-term direction.For our use cases (skip on-screen keyboard branch, pick gamepad-vs-touch input, set viewport scaling defaults), feature detection is more correct than UA detection — a touchscreen laptop should be treated as having touch capability without being "mobile".
Suggested approach
Roughly in tiers (don't have to do all at once):
Quick win: patch
iOSto also catch the iPadOS-13-as-Mac case vianavigator.platform === \"MacIntel\" && navigator.maxTouchPoints > 1. One-line fix, closes the biggest gap.Add an
isTouchflag (separate fromisMobile) —matchMedia(\"(pointer: coarse)\").matches || (navigator.maxTouchPoints || 0) > 0. This is what most internal call sites probably actually want.Deprecate the dead platforms —
wp,BlackBerry,Kindle,android2— leave the exports for backwards-compat through 19.x, mark deprecated, remove in 20.x.Keep
isMobileas a UA-based heuristic but document that it's best-effort and recommendisTouch/pointer: coarsefor new code.Long-term: migrate to
navigator.userAgentDatawhere available, fall back to UA. Out of scope for a single PR.Test coverage
The new
tests/platform.spec.ts(added in the same audit pass) pins the current behavior under Playwright + chromium. Any modernization PR should update those expectations and add cases for the iPad-on-iPadOS scenario (which the chromium test runner can simulate by overridingnavigator.platform+maxTouchPoints).🤖 Filed via Claude Code during the 19.7 / Camera3d cleanup pass