nuts / files / src / compat.ts
compat.ts
f1262fd9 2026-04-12 Pavel Safronov | 1 | export type CompatResult = |
| 2 | | { ok: true } | |
4ea6baf7 2026-04-12 Pavel Safronov | 3 | | { ok: false; reason: 'no-touch' | 'no-motion' } |
| 4 | ||
| 5 | /** True if the Vibration API is available (Chrome Android). */ | |
| 6 | export function hasVibration(): boolean { | |
| 7 | return 'vibrate' in navigator && typeof navigator.vibrate === 'function' | |
| 8 | } | |
f1262fd9 2026-04-12 Pavel Safronov | 9 | |
| 10 | /** | |
| 11 | * Check whether this platform can run the full experience. | |
| 12 | * Returns immediately (no async, no permissions needed). | |
| 13 | */ | |
| 14 | export function checkCompat(): CompatResult { | |
| 15 | // Must be a touch device | |
| 16 | if (!('ontouchstart' in window) && !navigator.maxTouchPoints) { | |
| 17 | return { ok: false, reason: 'no-touch' } | |
| 18 | } | |
| 19 | ||
| 20 | // Must have DeviceMotion (accelerometer / tilt gravity) | |
| 21 | if (typeof DeviceMotionEvent === 'undefined') { | |
| 22 | return { ok: false, reason: 'no-motion' } | |
| 23 | } | |
| 24 | ||
| 25 | return { ok: true } | |
| 26 | } | |
| 27 | ||
| 28 | const MESSAGES: Record<string, { headline: string; body: string }> = { | |
| 29 | 'no-touch': { | |
| 30 | headline: 'Phone only', | |
4ea6baf7 2026-04-12 Pavel Safronov | 31 | body: 'This app needs a touchscreen to work.\nOpen it on your phone in Chrome.', |
f1262fd9 2026-04-12 Pavel Safronov | 32 | }, |
| 33 | 'no-motion': { | |
| 34 | headline: 'No accelerometer', | |
4ea6baf7 2026-04-12 Pavel Safronov | 35 | body: 'This app needs a motion sensor to work.\nOpen it on your phone in Chrome.', |
f1262fd9 2026-04-12 Pavel Safronov | 36 | }, |
| 37 | } | |
| 38 | ||
| 39 | /** Replace the page body with a full-screen "not supported" wall. */ | |
| 40 | export function showUnsupportedWall(reason: string): void { | |
| 41 | const { headline, body } = MESSAGES[reason] ?? { | |
| 42 | headline: 'Not supported', | |
| 43 | body: 'Open this on your Android phone in Chrome.', | |
| 44 | } | |
| 45 | ||
| 46 | document.body.innerHTML = ` | |
| 47 | <div style=" | |
| 48 | position:fixed;inset:0; | |
| 49 | background:#111; | |
| 50 | display:flex;flex-direction:column;align-items:center;justify-content:center; | |
| 51 | padding:32px;gap:20px; | |
| 52 | font-family:system-ui,-apple-system,sans-serif; | |
| 53 | text-align:center; | |
| 54 | "> | |
| 55 | <div style="font-size:72px;line-height:1;filter:grayscale(1) opacity(0.4)">🔩</div> | |
| 56 | <h1 style="color:#fff;font-size:28px;font-weight:700;margin:0">${headline}</h1> | |
| 57 | <p style="color:#666;font-size:16px;line-height:1.6;margin:0;white-space:pre-line">${body}</p> | |
cf658d5b 2026-10-27 Pavel Safronov | 58 | <a href="https://apps.fuzzyworld.net/nuts/" |
f1262fd9 2026-04-12 Pavel Safronov | 59 | style="margin-top:8px;color:#c8a84b;font-size:13px;opacity:0.6;text-decoration:none"> |
cf658d5b 2026-10-27 Pavel Safronov | 60 | apps.fuzzyworld.net/nuts |
f1262fd9 2026-04-12 Pavel Safronov | 61 | </a> |
| 62 | </div>` | |
| 63 | } |