From 595052460734180147e944c09ba8d9a49acef658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Sun, 16 Aug 2026 12:14:38 +0200 Subject: [PATCH 1/2] feat(website): show error when trying to run the demo on an unsupported browser --- website/package.json | 3 +- website/scripts/build-demos.sh | 50 +++++++++++++------ website/src/pages/demos.module.css | 28 ++++++++--- website/src/pages/demos.tsx | 78 ++++++++++++++++++++++-------- 4 files changed, 117 insertions(+), 42 deletions(-) diff --git a/website/package.json b/website/package.json index 4dfb39de..0b1453f1 100644 --- a/website/package.json +++ b/website/package.json @@ -46,5 +46,6 @@ }, "engines": { "node": ">=20.0" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/website/scripts/build-demos.sh b/website/scripts/build-demos.sh index 4f1f2b03..db5e9087 100755 --- a/website/scripts/build-demos.sh +++ b/website/scripts/build-demos.sh @@ -173,35 +173,55 @@ write_index_html() { } .error { color: #ff6b6b; + line-height: 1.5; } .error::after { display: none; } + .error a { + color: #ff6b6b; + } @keyframes spin { to { transform: rotate(360deg); } }
Loading WebAssembly...
diff --git a/website/src/pages/demos.module.css b/website/src/pages/demos.module.css index 9761c1b7..fe51bfc0 100644 --- a/website/src/pages/demos.module.css +++ b/website/src/pages/demos.module.css @@ -57,13 +57,27 @@ } } -/* WebGPU warning */ -.webgpuWarning { - padding: 0.75rem 1rem; - background: var(--ifm-color-warning-contrast-background, #fff8e6); - color: var(--ifm-color-warning-contrast-foreground, #4d3800); - border-bottom: 1px solid var(--ifm-color-emphasis-300); - font-size: 0.9rem; +/* Shown instead of the viewer when the browser can't run the demos */ +.unsupported { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + gap: 0.75rem; + padding: 2rem; + text-align: center; +} + +.unsupported h2 { + margin: 0; + font-size: 1.5rem; +} + +.unsupported p { + margin: 0; + max-width: 42rem; + color: var(--ifm-color-emphasis-700); } /* Viewer */ diff --git a/website/src/pages/demos.tsx b/website/src/pages/demos.tsx index 1d055d97..0ae69b46 100644 --- a/website/src/pages/demos.tsx +++ b/website/src/pages/demos.tsx @@ -20,16 +20,31 @@ const demos = [ }, ]; +// What prevents the demo from running here, if anything. Resolved on the +// client only (`navigator` doesn't exist while the site is pre-rendered), so +// `undefined` means "not determined yet". +type Blocker = 'safari' | 'webgpu' | null; + +function detectBlocker(): Blocker { + // Safari matches every WebKit-based UA except the Chromium/Firefox ones, + // which advertise themselves as Safari too. + const isSafari = /^((?!chromium|chrome|crios|fxios|edgios|android).)*safari/i + .test(navigator.userAgent); + if (isSafari) return 'safari'; + // Nexus runs its physics as WebGPU compute shaders: no WebGPU, no demo. + if (!(navigator as any).gpu) return 'webgpu'; + return null; +} + export default function Demos(): ReactNode { const [selected, setSelected] = useState(null); const [activeDemo, setActiveDemo] = useState(null); const [isLoading, setIsLoading] = useState(false); - const [webgpuSupported, setWebgpuSupported] = useState(true); + const [blocker, setBlocker] = useState(undefined); const iframeRef = useRef(null); - // Nexus runs its physics as WebGPU compute shaders: no WebGPU, no demo. useEffect(() => { - setWebgpuSupported(typeof navigator !== 'undefined' && !!(navigator as any).gpu); + setBlocker(detectBlocker()); }, []); // Handle URL hash for deep linking @@ -52,6 +67,9 @@ export default function Demos(): ReactNode { // Handle demo transitions - clear iframe first to release WebGPU context useEffect(() => { + // Nothing is loaded until the browser is known to support the demos + // (`undefined` = still unknown, non-null = unsupported). + if (blocker !== null) return; if (selected === activeDemo) return; setIsLoading(true); @@ -69,7 +87,7 @@ export default function Demos(): ReactNode { }, 500); return () => clearTimeout(timer); - }, [selected]); + }, [selected, blocker]); const handleSelect = (name: string) => { setSelected(name); @@ -97,24 +115,46 @@ export default function Demos(): ReactNode { ))} - - Pick individual demos from the panel inside the viewer. First load - may take a while (the physics engine ships as a large WASM module). - + {!blocker && ( + + Pick individual demos from the panel inside the viewer. First load + may take a while (the physics engine ships as a large WASM module). + + )} - {!webgpuSupported && ( -
- WebGPU is not available in this browser. Nexus - runs its physics as WebGPU compute shaders. On Firefox, enable{' '} - dom.webgpu.enabled in about:config; on - Chromium, enable Unsafe WebGPU Support in{' '} - chrome://flags. Safari is currently not supported. -
- )} -
- {activeDemo ? ( + {blocker === 'safari' ? ( +
+

Safari is not supported

+

+ Nexus does not currently run in Safari. Please open this page + in a recent Chromium-based browser or in Firefox. +

+
+ ) : blocker === 'webgpu' ? ( +
+

WebGPU is required

+

+ Nexus runs its physics as WebGPU compute shaders, and WebGPU is + not available in this browser. See{' '} + + caniuse.com/webgpu + {' '} + for browser support. +

+

+ On Firefox, enable dom.webgpu.enabled in{' '} + about:config; on Chromium, enable{' '} + Unsafe WebGPU Support in{' '} + chrome://flags. +

+
+ ) : activeDemo ? ( <>