From 78ddbe8d0b811e89667e0941198572b9fbc22fea Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Mon, 5 Jan 2026 19:54:49 +0000 Subject: [PATCH 1/6] Use spline-viewer web component and load it dynamically --- apps/webapp/app/components/ErrorDisplay.tsx | 33 +++++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/ErrorDisplay.tsx b/apps/webapp/app/components/ErrorDisplay.tsx index 1a8f4b2ad9..6088fbe8c7 100644 --- a/apps/webapp/app/components/ErrorDisplay.tsx +++ b/apps/webapp/app/components/ErrorDisplay.tsx @@ -5,8 +5,21 @@ import { friendlyErrorDisplay } from "~/utils/httpErrors"; import { LinkButton } from "./primitives/Buttons"; import { Header1 } from "./primitives/Headers"; import { Paragraph } from "./primitives/Paragraph"; -import Spline from "@splinetool/react-spline"; -import { type ReactNode } from "react"; +import { type ReactNode, useEffect } from "react"; + +declare global { + namespace JSX { + interface IntrinsicElements { + "spline-viewer": React.DetailedHTMLProps< + React.HTMLAttributes & { + url?: string; + "loading-anim-type"?: string; + }, + HTMLElement + >; + } + } +} type ErrorDisplayOptions = { button?: { @@ -43,6 +56,16 @@ type DisplayOptionsProps = { } & ErrorDisplayOptions; export function ErrorDisplay({ title, message, button }: DisplayOptionsProps) { + useEffect(() => { + // Dynamically load the Spline viewer script + if (!customElements.get("spline-viewer")) { + const script = document.createElement("script"); + script.type = "module"; + script.src = "https://unpkg.com/@splinetool/viewer@1.12.29/build/spline-viewer.js"; + document.head.appendChild(script); + } + }, []); + return (
@@ -63,7 +86,11 @@ export function ErrorDisplay({ title, message, button }: DisplayOptionsProps) { animate={{ opacity: 1 }} transition={{ delay: 0.5, duration: 2, ease: "easeOut" }} > - +
); From 18351174c8c94dfc4730f2454bf8e2b904adcd31 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Mon, 5 Jan 2026 19:55:09 +0000 Subject: [PATCH 2/6] Remove spline react bundle dependency --- apps/webapp/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 45de003c8d..20023975b7 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -109,7 +109,6 @@ "@sentry/remix": "9.46.0", "@slack/web-api": "7.9.1", "@socket.io/redis-adapter": "^8.3.0", - "@splinetool/react-spline": "^2.2.6", "@tabler/icons-react": "^2.39.0", "@tailwindcss/container-queries": "^0.1.1", "@tanstack/react-virtual": "^3.0.4", From fdfbb07d8e1f870e0dfcb484704eef0043c3f9d2 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 7 Jan 2026 14:14:51 +0000 Subject: [PATCH 3/6] Fix to prevent possible race condition --- apps/webapp/app/components/ErrorDisplay.tsx | 47 ++++++++++++--------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/apps/webapp/app/components/ErrorDisplay.tsx b/apps/webapp/app/components/ErrorDisplay.tsx index 6088fbe8c7..961772a2dd 100644 --- a/apps/webapp/app/components/ErrorDisplay.tsx +++ b/apps/webapp/app/components/ErrorDisplay.tsx @@ -5,7 +5,7 @@ import { friendlyErrorDisplay } from "~/utils/httpErrors"; import { LinkButton } from "./primitives/Buttons"; import { Header1 } from "./primitives/Headers"; import { Paragraph } from "./primitives/Paragraph"; -import { type ReactNode, useEffect } from "react"; +import { type ReactNode, useEffect, useState } from "react"; declare global { namespace JSX { @@ -56,14 +56,21 @@ type DisplayOptionsProps = { } & ErrorDisplayOptions; export function ErrorDisplay({ title, message, button }: DisplayOptionsProps) { + const [isSplineReady, setIsSplineReady] = useState(false); + useEffect(() => { - // Dynamically load the Spline viewer script - if (!customElements.get("spline-viewer")) { - const script = document.createElement("script"); - script.type = "module"; - script.src = "https://unpkg.com/@splinetool/viewer@1.12.29/build/spline-viewer.js"; - document.head.appendChild(script); + // Already registered from a previous render + if (customElements.get("spline-viewer")) { + setIsSplineReady(true); + return; } + + const script = document.createElement("script"); + script.type = "module"; + script.src = "https://unpkg.com/@splinetool/viewer@1.12.29/build/spline-viewer.js"; + script.onload = () => setIsSplineReady(true); + // On error, we simply don't show the decorative viewer - no action needed + document.head.appendChild(script); }, []); return ( @@ -80,18 +87,20 @@ export function ErrorDisplay({ title, message, button }: DisplayOptionsProps) { {button ? button.title : "Go to homepage"}
- - - + {isSplineReady && ( + + + + )} ); } From 42b385bdd398e200674576cbd0a8798ee9996d45 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 7 Jan 2026 14:22:58 +0000 Subject: [PATCH 4/6] Move the rotating spline logo into its own component --- apps/webapp/app/components/ErrorDisplay.tsx | 50 ++--------------- .../app/components/TriggerRotatingLogo.tsx | 54 +++++++++++++++++++ 2 files changed, 57 insertions(+), 47 deletions(-) create mode 100644 apps/webapp/app/components/TriggerRotatingLogo.tsx diff --git a/apps/webapp/app/components/ErrorDisplay.tsx b/apps/webapp/app/components/ErrorDisplay.tsx index 961772a2dd..5787a2edba 100644 --- a/apps/webapp/app/components/ErrorDisplay.tsx +++ b/apps/webapp/app/components/ErrorDisplay.tsx @@ -1,25 +1,11 @@ import { HomeIcon } from "@heroicons/react/20/solid"; import { isRouteErrorResponse, useRouteError } from "@remix-run/react"; -import { motion } from "framer-motion"; import { friendlyErrorDisplay } from "~/utils/httpErrors"; import { LinkButton } from "./primitives/Buttons"; import { Header1 } from "./primitives/Headers"; import { Paragraph } from "./primitives/Paragraph"; -import { type ReactNode, useEffect, useState } from "react"; - -declare global { - namespace JSX { - interface IntrinsicElements { - "spline-viewer": React.DetailedHTMLProps< - React.HTMLAttributes & { - url?: string; - "loading-anim-type"?: string; - }, - HTMLElement - >; - } - } -} +import { TriggerRotatingLogo } from "./TriggerRotatingLogo"; +import { type ReactNode } from "react"; type ErrorDisplayOptions = { button?: { @@ -56,23 +42,6 @@ type DisplayOptionsProps = { } & ErrorDisplayOptions; export function ErrorDisplay({ title, message, button }: DisplayOptionsProps) { - const [isSplineReady, setIsSplineReady] = useState(false); - - useEffect(() => { - // Already registered from a previous render - if (customElements.get("spline-viewer")) { - setIsSplineReady(true); - return; - } - - const script = document.createElement("script"); - script.type = "module"; - script.src = "https://unpkg.com/@splinetool/viewer@1.12.29/build/spline-viewer.js"; - script.onload = () => setIsSplineReady(true); - // On error, we simply don't show the decorative viewer - no action needed - document.head.appendChild(script); - }, []); - return (
@@ -87,20 +56,7 @@ export function ErrorDisplay({ title, message, button }: DisplayOptionsProps) { {button ? button.title : "Go to homepage"}
- {isSplineReady && ( - - - - )} +
); } diff --git a/apps/webapp/app/components/TriggerRotatingLogo.tsx b/apps/webapp/app/components/TriggerRotatingLogo.tsx new file mode 100644 index 0000000000..cb20bc6f77 --- /dev/null +++ b/apps/webapp/app/components/TriggerRotatingLogo.tsx @@ -0,0 +1,54 @@ +import { motion } from "framer-motion"; +import { useEffect, useState } from "react"; + +declare global { + namespace JSX { + interface IntrinsicElements { + "spline-viewer": React.DetailedHTMLProps< + React.HTMLAttributes & { + url?: string; + "loading-anim-type"?: string; + }, + HTMLElement + >; + } + } +} + +export function TriggerRotatingLogo() { + const [isSplineReady, setIsSplineReady] = useState(false); + + useEffect(() => { + // Already registered from a previous render + if (customElements.get("spline-viewer")) { + setIsSplineReady(true); + return; + } + + const script = document.createElement("script"); + script.type = "module"; + script.src = "https://unpkg.com/@splinetool/viewer@1.12.29/build/spline-viewer.js"; + script.onload = () => setIsSplineReady(true); + // On error, we simply don't show the decorative viewer - no action needed + document.head.appendChild(script); + }, []); + + if (!isSplineReady) { + return null; + } + + return ( + + + + ); +} From c954411163c3a545f19d252667d7026f5094fca8 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 7 Jan 2026 14:34:26 +0000 Subject: [PATCH 5/6] pnpm lockfile to remove spline --- pnpm-lock.yaml | 42 ------------------------------------------ 1 file changed, 42 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d83f4981f..c51657b95f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -453,9 +453,6 @@ importers: '@socket.io/redis-adapter': specifier: ^8.3.0 version: 8.3.0(socket.io-adapter@2.5.4(bufferutil@4.0.9)) - '@splinetool/react-spline': - specifier: ^2.2.6 - version: 2.2.6(@splinetool/runtime@1.11.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@tabler/icons-react': specifier: ^2.39.0 version: 2.47.0(react@18.2.0) @@ -9942,16 +9939,6 @@ packages: '@sodaru/yup-to-json-schema@2.0.1': resolution: {integrity: sha512-lWb0Wiz8KZ9ip/dY1eUqt7fhTPmL24p6Hmv5Fd9pzlzAdw/YNcWZr+tiCT4oZ4Zyxzi9+1X4zv82o7jYvcFxYA==} - '@splinetool/react-spline@2.2.6': - resolution: {integrity: sha512-y9L2VEbnC6FNZZu8XMmWM9YTTTWal6kJVfP05Amf0QqDNzCSumKsJxZyGUODvuCmiAvy0PfIfEsiVKnSxvhsDw==} - peerDependencies: - '@splinetool/runtime': '*' - react: '>=17.0.0' - react-dom: '>=17.0.0' - - '@splinetool/runtime@1.11.2': - resolution: {integrity: sha512-rFz3KOQQRHQGzWBvPKRZcI7fZe5qxNYX1FmmCqzsbJkAU/hJdifaxpyN4xESpbkdta6s7riSmoz5lmPGIpZRRQ==} - '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -16288,10 +16275,6 @@ packages: resolution: {integrity: sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==} engines: {node: ^10.13.0 || >=12.0.0} - on-change@4.0.2: - resolution: {integrity: sha512-cMtCyuJmTx/bg2HCpHo3ZLeF7FZnBOapLqZHr2AlLeJ5Ul0Zu2mUJJz051Fdwu/Et2YW04ZD+TtU+gVy0ACNCA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -17419,9 +17402,6 @@ packages: '@types/react': '>=18' react: '>=18' - react-merge-refs@2.1.1: - resolution: {integrity: sha512-jLQXJ/URln51zskhgppGJ2ub7b2WFKGq3cl3NYKtlHoTG+dN2q7EzWrn3hN3EgPsTMvpR9tpq5ijdp7YwFZkag==} - react-popper@2.3.0: resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==} peerDependencies: @@ -18006,9 +17986,6 @@ packages: sembear@0.5.2: resolution: {integrity: sha512-Ij1vCAdFgWABd7zTg50Xw1/p0JgESNxuLlneEAsmBrKishA06ulTTL/SHGmNy2Zud7+rKrHTKNI6moJsn1ppAQ==} - semver-compare@1.0.0: - resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} - semver@5.7.1: resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} hasBin: true @@ -30136,19 +30113,6 @@ snapshots: '@sodaru/yup-to-json-schema@2.0.1': {} - '@splinetool/react-spline@2.2.6(@splinetool/runtime@1.11.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': - dependencies: - '@splinetool/runtime': 1.11.2 - lodash.debounce: 4.0.8 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-merge-refs: 2.1.1 - - '@splinetool/runtime@1.11.2': - dependencies: - on-change: 4.0.2 - semver-compare: 1.0.0 - '@standard-schema/spec@1.0.0': {} '@stricli/auto-complete@1.2.0': @@ -37633,8 +37597,6 @@ snapshots: oidc-token-hash@5.0.3: optional: true - on-change@4.0.2: {} - on-exit-leak-free@2.1.2: {} on-finished@2.3.0: @@ -38981,8 +38943,6 @@ snapshots: transitivePeerDependencies: - supports-color - react-merge-refs@2.1.1: {} - react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@popperjs/core': 2.11.8 @@ -39734,8 +39694,6 @@ snapshots: '@types/semver': 6.2.3 semver: 6.3.1 - semver-compare@1.0.0: {} - semver@5.7.1: {} semver@6.3.1: {} From 8997f7c14979097fd7e9c4bd21ace655c8c1254a Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 7 Jan 2026 14:51:08 +0000 Subject: [PATCH 6/6] Another fix for race condition --- .../app/components/TriggerRotatingLogo.tsx | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/components/TriggerRotatingLogo.tsx b/apps/webapp/app/components/TriggerRotatingLogo.tsx index cb20bc6f77..878c203a3c 100644 --- a/apps/webapp/app/components/TriggerRotatingLogo.tsx +++ b/apps/webapp/app/components/TriggerRotatingLogo.tsx @@ -13,6 +13,10 @@ declare global { >; } } + + interface Window { + __splineLoader?: Promise; + } } export function TriggerRotatingLogo() { @@ -25,12 +29,29 @@ export function TriggerRotatingLogo() { return; } + // Another mount already started loading - share the same promise + if (window.__splineLoader) { + window.__splineLoader.then(() => setIsSplineReady(true)).catch(() => setIsSplineReady(false)); + return; + } + + // First mount: create script and shared loader promise const script = document.createElement("script"); script.type = "module"; + // Version pinned; SRI hash omitted as unpkg doesn't guarantee hash stability across deploys script.src = "https://unpkg.com/@splinetool/viewer@1.12.29/build/spline-viewer.js"; - script.onload = () => setIsSplineReady(true); - // On error, we simply don't show the decorative viewer - no action needed + + window.__splineLoader = new Promise((resolve, reject) => { + script.onload = () => resolve(); + script.onerror = () => reject(); + }); + + window.__splineLoader.then(() => setIsSplineReady(true)).catch(() => setIsSplineReady(false)); + document.head.appendChild(script); + + // Intentionally no cleanup: once the custom element is registered globally, + // removing the script would break re-mounts while providing no benefit }, []); if (!isSplineReady) {