From aeaf3693add503f0512c4529eed7b2e27b10e633 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sun, 11 Oct 2026 21:33:31 +0200 Subject: [PATCH] fix(mascot): typecheck the clip metadata and keep the client ratchets green The tuple cast on clips.meta.json failed tsc. The clip lookup and the 3D opt-out move into mascotClips.ts so the component file only exports the component, the crop offset is allowed as a deliberate physical side, and MDancingTrek leaves the format baseline. --- client/scripts/format-baseline.json | 1 - client/scripts/rtl-disable-baseline.json | 3 +- client/src/mobile/components/MDancingTrek.tsx | 3 +- .../mobile/components/trek3d/MascotClip.tsx | 39 +------------------ .../mobile/components/trek3d/mascotClips.ts | 37 ++++++++++++++++++ 5 files changed, 43 insertions(+), 40 deletions(-) create mode 100644 client/src/mobile/components/trek3d/mascotClips.ts diff --git a/client/scripts/format-baseline.json b/client/scripts/format-baseline.json index 4738afed2..8edbecb2b 100644 --- a/client/scripts/format-baseline.json +++ b/client/scripts/format-baseline.json @@ -1277,7 +1277,6 @@ "src/mobile/components/MBottomNav.test.tsx", "src/mobile/components/MBottomNav.tsx", "src/mobile/components/MChip.tsx", - "src/mobile/components/MDancingTrek.tsx", "src/mobile/components/MDayImpactList.tsx", "src/mobile/components/MDropdownPanel.tsx", "src/mobile/components/MFab.tsx", diff --git a/client/scripts/rtl-disable-baseline.json b/client/scripts/rtl-disable-baseline.json index f26793f81..c8cee7a9c 100644 --- a/client/scripts/rtl-disable-baseline.json +++ b/client/scripts/rtl-disable-baseline.json @@ -1,3 +1,4 @@ { - "pages/tripPlanner/PlanMapStage.tsx": 4 + "pages/tripPlanner/PlanMapStage.tsx": 4, + "mobile/components/trek3d/MascotClip.tsx": 1 } diff --git a/client/src/mobile/components/MDancingTrek.tsx b/client/src/mobile/components/MDancingTrek.tsx index a023adb75..ab50c2ec4 100644 --- a/client/src/mobile/components/MDancingTrek.tsx +++ b/client/src/mobile/components/MDancingTrek.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; -import MascotClip, { findClip, mascot3dEnabled } from './trek3d/MascotClip'; +import MascotClip from './trek3d/MascotClip'; +import { findClip, mascot3dEnabled } from './trek3d/mascotClips'; /** * The TREK mascot — an empty-state character that IS the TREK mark: the logo is diff --git a/client/src/mobile/components/trek3d/MascotClip.tsx b/client/src/mobile/components/trek3d/MascotClip.tsx index e08bbd2ea..d84e4c54e 100644 --- a/client/src/mobile/components/trek3d/MascotClip.tsx +++ b/client/src/mobile/components/trek3d/MascotClip.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState, type ReactNode } from 'react'; import meta from './clips.meta.json'; +import { CLIPS } from './mascotClips'; /** * The 3D TREK mascot as pre-rendered clips. Each clip is a plain H.264 MP4 with @@ -17,48 +18,12 @@ import meta from './clips.meta.json'; * inside the full 88:96 box so every scene keeps the same scale. Only URLs are * bundled; the files load when a mascot actually renders. */ -const CLIPS = import.meta.glob('./clips/*.mp4', { query: '?url', import: 'default', eager: true }) as Record< - string, - string ->; -const META = meta as Record; +const META: Record = meta; const FADE_MS = 140; // The clips frame the figure with more air than the SVG, so they render at 1.6 times the requested size const CLIP_SCALE = 1.6; -function clipKey(scene: string, mood: string, sceneMood: string) { - return mood === sceneMood ? scene : `${scene}--${mood}`; -} - -export function findClip(scene: string, mood: string, sceneMood: string): string | null { - const key = clipKey(scene, mood, sceneMood); - return CLIPS[`./clips/${key}-light-loop.mp4`] ? key : null; -} - -let webgl: boolean | null = null; -function hasWebgl(): boolean { - if (webgl === null) { - try { - webgl = !!document.createElement('canvas').getContext('webgl'); - } catch { - webgl = false; - } - } - return webgl; -} - -/** Opt-out for comparing against the SVG: localStorage `trek-mascot-3d` = `0`. */ -export function mascot3dEnabled(): boolean { - try { - if (localStorage.getItem('trek-mascot-3d') === '0') return false; - } catch { - /* storage blocked: keep the default */ - } - if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) return false; - return hasWebgl(); -} - function useIsDark() { const read = () => document.documentElement.classList.contains('dark'); const [dark, setDark] = useState(read); diff --git a/client/src/mobile/components/trek3d/mascotClips.ts b/client/src/mobile/components/trek3d/mascotClips.ts new file mode 100644 index 000000000..fe945ad70 --- /dev/null +++ b/client/src/mobile/components/trek3d/mascotClips.ts @@ -0,0 +1,37 @@ +/** Clip lookup and the 3D opt-out, kept apart from the component so Fast Refresh keeps working. */ +export const CLIPS = import.meta.glob('./clips/*.mp4', { query: '?url', import: 'default', eager: true }) as Record< + string, + string +>; + +function clipKey(scene: string, mood: string, sceneMood: string) { + return mood === sceneMood ? scene : `${scene}--${mood}`; +} + +export function findClip(scene: string, mood: string, sceneMood: string): string | null { + const key = clipKey(scene, mood, sceneMood); + return CLIPS[`./clips/${key}-light-loop.mp4`] ? key : null; +} + +let webgl: boolean | null = null; +function hasWebgl(): boolean { + if (webgl === null) { + try { + webgl = !!document.createElement('canvas').getContext('webgl'); + } catch { + webgl = false; + } + } + return webgl; +} + +/** Opt-out for comparing against the SVG: localStorage `trek-mascot-3d` = `0`. */ +export function mascot3dEnabled(): boolean { + try { + if (localStorage.getItem('trek-mascot-3d') === '0') return false; + } catch { + /* storage blocked: keep the default */ + } + if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) return false; + return hasWebgl(); +}