fix(compat): improve cross-browser support for Firefox, Safari, and older browsers

Add global polyfills for AbortSignal.timeout() (Safari <17, Firefox <122)
and crypto.randomUUID() (Safari <15.4, Firefox <95) in shell HTML templates.
Add -webkit-backdrop-filter prefix across 13 files for older Safari support.
Add Firefox scrollbar (scrollbar-width/scrollbar-color), range input
(::-moz-range-thumb/track), and color-mix() rgba fallbacks. Create shared
compat.ts utility module. Lowers browser floor from Safari 17/Firefox 122
to Safari 15.4/Firefox 95.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-20 10:43:38 -07:00
parent a736321189
commit 39ec09bb3b
15 changed files with 100 additions and 13 deletions

View File

@ -137,6 +137,19 @@ const styles = css`
background: #059669;
cursor: pointer;
}
.slider-input::-moz-range-thumb {
width: 14px;
height: 14px;
border: none;
border-radius: 50%;
background: #059669;
cursor: pointer;
}
.slider-input::-moz-range-track {
background: #e2e8f0;
border-radius: 2px;
height: 4px;
}
.slider-val {
font-size: 12px;
@ -300,6 +313,8 @@ const styles = css`
.settings-item input[type="text"] { flex: 1; border: 1px solid #e2e8f0; border-radius: 4px; padding: 4px 6px; font-size: 11px; outline: none; min-width: 0; }
.settings-item input[type="range"] { width: 60px; height: 4px; -webkit-appearance: none; appearance: none; background: #e2e8f0; border-radius: 2px; outline: none; cursor: pointer; }
.settings-item input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: #059669; cursor: pointer; }
.settings-item input[type="range"]::-moz-range-thumb { width: 12px; height: 12px; border: none; border-radius: 50%; background: #059669; cursor: pointer; }
.settings-item input[type="range"]::-moz-range-track { background: #e2e8f0; border-radius: 2px; height: 4px; }
.settings-item .weight-val { font-size: 10px; font-weight: 600; color: #059669; min-width: 12px; text-align: center; }
.settings-item .remove-btn { background: none; border: none; color: #94a3b8; cursor: pointer; font-size: 14px; padding: 2px 4px; border-radius: 4px; flex-shrink: 0; }
.settings-item .remove-btn:hover { color: #ef4444; background: #fef2f2; }

View File

@ -335,6 +335,7 @@ const HEADER_STYLES = `
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
display: flex;
align-items: center;

View File

@ -77,6 +77,7 @@
}
/* Thin scrollbars (rApp convention) */
.flows-detail, .flows-landing { scrollbar-width: thin; scrollbar-color: var(--rs-bg-surface-raised) transparent; }
.flows-detail ::-webkit-scrollbar,
.flows-landing ::-webkit-scrollbar { width: 6px; height: 6px; }
.flows-detail ::-webkit-scrollbar-track,
@ -547,6 +548,7 @@
animation: modalSlideIn 0.2s ease-out;
}
@keyframes modalSlideIn { from { transform: translateY(12px); opacity: 0; } to { transform: none; opacity: 1; } }
.flows-modal { scrollbar-width: thin; scrollbar-color: var(--rs-bg-surface-raised) transparent; }
.flows-modal::-webkit-scrollbar { width: 6px; }
.flows-modal::-webkit-scrollbar-track { background: transparent; }
.flows-modal::-webkit-scrollbar-thumb { background: var(--rs-bg-surface-raised); border-radius: 3px; }
@ -709,6 +711,7 @@
flex: 1; overflow-y: auto; padding: 8px 10px;
max-height: 180px; min-height: 60px;
}
.icp-body { scrollbar-width: thin; scrollbar-color: var(--rs-border-strong) transparent; }
.icp-body::-webkit-scrollbar { width: 4px; }
.icp-body::-webkit-scrollbar-thumb { background: var(--rs-border-strong); border-radius: 2px; }
.icp-toolbar {
@ -758,6 +761,13 @@
-webkit-appearance: none; width: 12px; height: 12px;
border-radius: 50%; background: var(--rs-primary); cursor: pointer;
}
.icp-range::-moz-range-thumb {
width: 12px; height: 12px; border: none;
border-radius: 50%; background: var(--rs-primary); cursor: pointer;
}
.icp-range::-moz-range-track {
background: var(--rs-border-strong); border-radius: 2px; height: 4px;
}
.icp-range-value {
font-size: 11px; font-weight: 600; color: var(--rs-text-primary);
min-width: 40px; text-align: right;
@ -1094,6 +1104,7 @@
flex: 1; overflow-y: auto; padding: 8px 0;
max-height: 400px;
}
.flows-mgmt__body { scrollbar-width: thin; scrollbar-color: var(--rs-bg-surface-raised) transparent; }
.flows-mgmt__body::-webkit-scrollbar { width: 6px; }
.flows-mgmt__body::-webkit-scrollbar-thumb { background: var(--rs-bg-surface-raised); border-radius: 3px; }
.flows-mgmt__row {
@ -1166,7 +1177,7 @@
}
.spm-backdrop {
position: absolute; inset: 0;
background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(2px);
background: rgba(0, 0, 0, 0.7); -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
}
.spm-card {
position: relative; z-index: 1;
@ -1319,6 +1330,7 @@
border-radius: 50%;
border: 2px solid rgba(6, 182, 212, 0.5);
background: linear-gradient(135deg, rgba(6, 182, 212, 0.2), rgba(139, 92, 246, 0.2));
-webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px);
cursor: pointer;
display: flex;

View File

@ -20,7 +20,7 @@ class MapImportModal extends HTMLElement {
}
private render() {
this.style.cssText = `position:fixed;inset:0;z-index:100;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.6);backdrop-filter:blur(4px);`;
this.style.cssText = `position:fixed;inset:0;z-index:100;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.6);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);`;
if (this._step === "upload") this.renderUpload();
else if (this._step === "preview") this.renderPreview();

View File

@ -4,7 +4,7 @@
* Dispatches 'modal-close' on dismiss.
*/
const MODAL_STYLE = `position:fixed;inset:0;z-index:100;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.6);backdrop-filter:blur(4px);`;
const MODAL_STYLE = `position:fixed;inset:0;z-index:100;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.6);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);`;
const MEETING_EMOJIS = ["\u{1F4CD}", "\u{2B50}", "\u{1F3E0}", "\u{1F37D}", "\u{26FA}", "\u{1F3AF}", "\u{1F680}", "\u{1F33F}", "\u{26A1}", "\u{1F48E}"];
class MapMeetingModal extends HTMLElement {

View File

@ -21,7 +21,7 @@ class MapShareModal extends HTMLElement {
}
private async render() {
this.style.cssText = `position:fixed;inset:0;z-index:100;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.6);backdrop-filter:blur(4px);`;
this.style.cssText = `position:fixed;inset:0;z-index:100;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.6);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);`;
this.innerHTML = `
<div style="background:var(--rs-bg-surface);border:1px solid var(--rs-border-strong);border-radius:14px;padding:24px;max-width:360px;width:90%;text-align:center;">

View File

@ -2855,6 +2855,7 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
.note-action-badge {
display: inline-flex; align-items: center; gap: 4px;
padding: 3px 10px; border-radius: 12px;
background: rgba(34, 197, 94, 0.15);
background: color-mix(in srgb, var(--rs-success, #22c55e) 15%, transparent);
color: var(--rs-success, #22c55e); font-size: 11px; font-weight: 600;
}
@ -2889,7 +2890,7 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
}
@keyframes dictation-pulse {
0%, 100% { background: var(--rs-bg-surface-raised); }
50% { background: color-mix(in srgb, var(--rs-error, #ef4444) 8%, var(--rs-bg-surface-raised)); }
50% { background: rgba(239, 68, 68, 0.08); background: color-mix(in srgb, var(--rs-error, #ef4444) 8%, var(--rs-bg-surface-raised)); }
}
.toolbar-select {
padding: 2px 4px; border-radius: 4px; border: 1px solid var(--rs-toolbar-panel-border);

View File

@ -550,7 +550,7 @@ class ImportExportDialog extends HTMLElement {
.dialog-overlay {
display: none;
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5); backdrop-filter: blur(4px);
background: rgba(0,0,0,0.5); -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
z-index: 10000; justify-content: center; align-items: center;
}
.dialog-overlay.open { display: flex; }

View File

@ -52,6 +52,7 @@
transition: transform 0.2s, border-color 0.2s, box-shadow 0.2s;
display: flex;
flex-direction: column;
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
}
@ -164,6 +165,7 @@
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
gap: 0.5rem;
z-index: 2;
@ -401,6 +403,7 @@ button.splat-viewer__save {
padding: 0.5rem 0.75rem;
border-radius: 8px;
background: var(--splat-accent);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
color: white;
text-decoration: none;
@ -452,6 +455,7 @@ button.splat-viewer__save {
padding: 0.5rem 0.75rem;
border-radius: 8px;
background: var(--rs-glass-bg, rgba(30, 41, 59, 0.85));
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
color: var(--splat-text);
text-decoration: none;
@ -473,6 +477,7 @@ button.splat-viewer__save {
padding: 0.75rem 1rem;
border-radius: 8px;
background: var(--rs-glass-bg, rgba(30, 41, 59, 0.85));
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
border: 1px solid var(--splat-border);
color: var(--splat-text);
@ -601,6 +606,7 @@ button.splat-viewer__save {
padding: 0.5rem 0.75rem;
border-radius: 8px;
background: var(--rs-glass-bg, rgba(30, 41, 59, 0.85));
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
color: var(--splat-text);
font-size: 0.8rem;

View File

@ -9,6 +9,9 @@ import { resolve } from "node:path";
import type { ModuleInfo, SubPageInfo } from "../shared/module";
import { getDocumentData } from "./community-store";
// ── Browser compatibility polyfills (inline, runs before ES modules) ──
const COMPAT_POLYFILLS = `<script>(function(){if(typeof AbortSignal.timeout!=="function"){AbortSignal.timeout=function(ms){var c=new AbortController();setTimeout(function(){c.abort(new DOMException("The operation was aborted due to timeout","TimeoutError"))},ms);return c.signal}}if(typeof crypto!=="undefined"&&typeof crypto.randomUUID!=="function"){crypto.randomUUID=function(){var b=crypto.getRandomValues(new Uint8Array(16));b[6]=(b[6]&0x0f)|0x40;b[8]=(b[8]&0x3f)|0x80;var h=Array.from(b,function(x){return x.toString(16).padStart(2,"0")}).join("");return h.slice(0,8)+"-"+h.slice(8,12)+"-"+h.slice(12,16)+"-"+h.slice(16,20)+"-"+h.slice(20)}}})()</script>`;
// ── Content-hash cache busting ──
let moduleHashes: Record<string, string> = {};
try {
@ -150,6 +153,7 @@ export function renderShell(opts: ShellOptions): string {
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://rspace.online/og-image.png">
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t);var b=localStorage.getItem('canvas-bg')||'grid';document.documentElement.setAttribute('data-canvas-bg',b)})()</script>
${COMPAT_POLYFILLS}
<link rel="stylesheet" href="/theme.css">
<link rel="stylesheet" href="/shell.css">
<style>
@ -1009,6 +1013,7 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
<link rel="icon" type="image/png" href="/favicon.png">
<title>${escapeHtml(title)}</title>
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t);var b=localStorage.getItem('canvas-bg')||'grid';document.documentElement.setAttribute('data-canvas-bg',b)})()</script>
${COMPAT_POLYFILLS}
<link rel="stylesheet" href="/theme.css">
<link rel="stylesheet" href="/shell.css">
<style>
@ -1125,7 +1130,7 @@ const ACCESS_GATE_CSS = `
#rspace-access-gate {
position: fixed; inset: 0; z-index: 9999;
display: flex; align-items: center; justify-content: center;
background: var(--rs-bg-overlay); backdrop-filter: blur(8px);
background: var(--rs-bg-overlay); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
}
.access-gate__card {
text-align: center; color: var(--rs-text-primary); max-width: 400px; padding: 2rem;
@ -1398,6 +1403,7 @@ const INFO_PANEL_CSS = `
.rapp-info-panel__body .rl-back a { font-size: 0.82rem; }
/* Scrollbar styling */
.rapp-info-panel__body { scrollbar-width: thin; scrollbar-color: rgba(20,184,166,0.2) transparent; }
.rapp-info-panel__body::-webkit-scrollbar { width: 5px; }
.rapp-info-panel__body::-webkit-scrollbar-track { background: transparent; }
.rapp-info-panel__body::-webkit-scrollbar-thumb { background: rgba(20,184,166,0.2); border-radius: 9999px; }
@ -1615,6 +1621,7 @@ export function renderModuleLanding(opts: ModuleLandingOptions): string {
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>${escapeHtml(mod.name)} rSpace</title>
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t);var b=localStorage.getItem('canvas-bg')||'grid';document.documentElement.setAttribute('data-canvas-bg',b)})()</script>
${COMPAT_POLYFILLS}
<link rel="stylesheet" href="/theme.css">
<link rel="stylesheet" href="/shell.css">
${cssBlock}
@ -1959,6 +1966,7 @@ export function renderSubPageInfo(opts: SubPageInfoOptions): string {
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>${escapeHtml(subPage.title)} ${escapeHtml(mod.name)} | rSpace</title>
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t);var b=localStorage.getItem('canvas-bg')||'grid';document.documentElement.setAttribute('data-canvas-bg',b)})()</script>
${COMPAT_POLYFILLS}
<link rel="stylesheet" href="/theme.css">
<link rel="stylesheet" href="/shell.css">
<style>${MODULE_LANDING_CSS}</style>

35
shared/compat.ts Normal file
View File

@ -0,0 +1,35 @@
/**
* Browser compatibility utilities
*
* Polyfills and wrappers for APIs that aren't available in all browsers.
* Import individual helpers as needed.
*/
/**
* crypto.randomUUID() polyfill for Safari <15.4, Firefox <95
*/
export function randomUUID(): string {
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
}
// RFC 4122 v4 UUID fallback using crypto.getRandomValues
const bytes = crypto.getRandomValues(new Uint8Array(16));
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 10
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
/**
* AbortSignal.timeout() polyfill for Safari <17, Firefox <122
*
* Returns an AbortSignal that aborts after the given milliseconds.
*/
export function timeoutSignal(ms: number): AbortSignal {
if (typeof AbortSignal.timeout === "function") {
return AbortSignal.timeout(ms);
}
const controller = new AbortController();
setTimeout(() => controller.abort(new DOMException("The operation was aborted due to timeout", "TimeoutError")), ms);
return controller.signal;
}

View File

@ -428,6 +428,7 @@ const OVERLAY_CSS = `
padding: 4px 10px 4px 6px;
border-radius: 16px;
background: var(--rs-bg-secondary, rgba(30, 30, 30, 0.85));
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
font-size: 11px;
color: var(--rs-text-secondary, #ccc);

View File

@ -1660,7 +1660,7 @@ const STYLES = `
const MODAL_STYLES = `
.rstack-auth-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.6);
backdrop-filter: blur(4px); display: flex; align-items: center;
-webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); display: flex; align-items: center;
justify-content: center; z-index: 10000; animation: fadeIn 0.2s;
}
.auth-modal {
@ -1759,7 +1759,7 @@ const SETTINGS_STYLES = `
const ACCOUNT_MODAL_STYLES = `
.rstack-account-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.6);
backdrop-filter: blur(4px); display: flex; align-items: center;
-webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); display: flex; align-items: center;
justify-content: center; z-index: 10000; animation: fadeIn 0.2s;
}
.account-modal {
@ -1838,7 +1838,7 @@ const ACCOUNT_MODAL_STYLES = `
const SPACES_STYLES = `
.rstack-spaces-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.6);
backdrop-filter: blur(4px); display: flex; align-items: center;
-webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); display: flex; align-items: center;
justify-content: center; z-index: 10000; animation: fadeIn 0.2s;
}
.spaces-modal {
@ -1912,7 +1912,7 @@ const SPACES_STYLES = `
const WALLETS_STYLES = `
.rstack-wallets-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.6);
backdrop-filter: blur(4px); display: flex; align-items: center;
-webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); display: flex; align-items: center;
justify-content: center; z-index: 10000; animation: fadeIn 0.2s;
}
.wallets-modal {

View File

@ -1262,7 +1262,7 @@ const STYLES = `
const REQUEST_MODAL_STYLES = `
.rstack-auth-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.6);
backdrop-filter: blur(4px); display: flex; align-items: center;
-webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); display: flex; align-items: center;
justify-content: center; z-index: 10000; animation: fadeIn 0.2s;
}
.auth-modal {
@ -1316,7 +1316,7 @@ const REQUEST_MODAL_STYLES = `
const EDIT_SPACE_MODAL_STYLES = `
.rstack-auth-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.6);
backdrop-filter: blur(4px); display: flex; align-items: center;
-webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); display: flex; align-items: center;
justify-content: center; z-index: 10000; animation: fadeIn 0.2s;
}
.edit-modal {

View File

@ -1879,6 +1879,7 @@ const STYLES = `
max-height: 80vh;
transition: max-height 0.3s ease;
position: relative;
background: var(--rs-bg-surface);
background: color-mix(in srgb, var(--rs-bg-surface) 50%, transparent);
border-bottom: 1px solid var(--rs-border-subtle);
}
@ -1914,6 +1915,7 @@ const STYLES = `
cursor: pointer;
transition: border-color 0.2s, box-shadow 0.2s;
transform-style: preserve-3d;
background: var(--rs-bg-surface);
background: color-mix(in srgb, var(--rs-bg-surface) 65%, transparent);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
@ -1928,6 +1930,7 @@ const STYLES = `
.layer-plane--active {
border-width: 2px;
box-shadow: 0 0 24px color-mix(in srgb, var(--layer-color) 40%, transparent);
background: var(--rs-bg-surface);
background: color-mix(in srgb, var(--rs-bg-surface) 85%, transparent);
}
@ -2006,14 +2009,18 @@ const STYLES = `
}
.io-chip--out {
background: rgba(148,163,184,0.18);
background: color-mix(in srgb, var(--chip-color) 18%, transparent);
border: 1px solid rgba(148,163,184,0.4);
border: 1px solid color-mix(in srgb, var(--chip-color) 40%, transparent);
color: var(--chip-color);
}
.io-chip--in {
background: transparent;
border: 1px dashed rgba(148,163,184,0.35);
border: 1px dashed color-mix(in srgb, var(--chip-color) 35%, transparent);
color: var(--rs-text-secondary);
color: color-mix(in srgb, var(--chip-color) 70%, var(--rs-text-primary));
}
@ -2307,6 +2314,7 @@ const STYLES = `
.flow-kind-btn:hover { background: var(--rs-bg-hover); }
.flow-kind-btn.selected {
border-color: var(--kind-color);
background: rgba(148,163,184,0.1);
background: color-mix(in srgb, var(--kind-color) 10%, transparent);
}