/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ /** * @backward-compat { version 153 } * Everything below tagged with the same marker — the logo-variation * registry, `pickVariant`, the hook, and the variation-selection block * inside `Logo()` — can be removed after Firefox 153 hits Release, when * the 2026 World Cup is over. After cleanup, `Logo()` reverts to its * original shape: just the wrapper + `.logo` div + `.wordmark`. */ import React, { useEffect, useMemo, useState } from "react"; import { useSelector } from "react-redux"; import { FootballBounce } from "./variants/FootballBounce"; import { RotatingBall } from "./variants/RotatingBall"; import { SpinBallSmall } from "./variants/SpinBallSmall"; import { SpinSmooth } from "./variants/SpinSmooth"; import { WIDGET_REGISTRY, isWidgetEnabled } from "common/WidgetsRegistry.mjs"; /** * @backward-compat { version 153 } * Pref consulted (after `trainhopConfig.logo.variation`) to choose a logo * variation. Empty string disables. Useful for local QA — set it via * `about:config` to preview a variation without an experiment. */ export const PREF_LOGO_VARIATION = "logo.variation"; const PREF_WIDGETS_ENABLED = "widgets.enabled"; /** * @backward-compat { version 153 } * Registry of all available logo variations. * * The key is the variant's string ID — the value that * `trainhopConfig.logo.variation` or the pref must equal for this variant * to be selected. Adding a new variant means: * 1. Implementing a `` component under `./variants/`. * 2. Adding an entry here with its constraints and fallback target. * * Each entry has: * - `component`: the React component to render. * - `minViewportWidth`: minimum viewport width in CSS pixels for this * variant to be considered usable. `0` means no width restriction. * - `requiresLTR`: when `true`, this variant is skipped in RTL locales. * - `fallback`: another variant ID to try when this variant's constraints * aren't met, or `null` to fall through to the default newtab logo. * * Universal constraints that apply to every variant (e.g. * `prefers-reduced-motion: reduce` handling) are NOT encoded here; they * are handled at the call site or inside the variation component instead. */ export const LOGO_VARIATIONS = { "spin-ball-small": { component: SpinBallSmall, minViewportWidth: 0, requiresLTR: false, fallback: null, }, "spin-smooth": { component: SpinSmooth, minViewportWidth: 0, requiresLTR: false, fallback: null, }, "rotating-ball": { component: RotatingBall, minViewportWidth: 0, requiresLTR: false, fallback: null, }, "football-bounce": { component: FootballBounce, minViewportWidth: 0, requiresLTR: true, fallback: "spin-smooth", }, }; const VARIANT_THRESHOLDS = Object.values(LOGO_VARIATIONS).map( v => v.minViewportWidth ); /** * @backward-compat { version 153 } * Walk the fallback chain starting at `variantId`, returning the first * variant whose per-variant constraints are satisfied by the supplied * environment, or `null` if none are. * * Cycle-safe: a fallback chain that loops back on itself terminates as soon * as a previously-seen ID is encountered. * * @param {string|null|undefined} variantId * The variant ID to start walking from (typically the value of the * trainhopConfig or pref). Falsy values short-circuit to `null`. * @param {object} env * The current rendering environment. * @param {number} env.viewportWidth * The largest `min-width` breakpoint the viewport currently satisfies, in * CSS pixels. A variant passes the width gate when its `minViewportWidth` * is at or below this number. * @param {boolean} env.isLTR * `true` if the document direction is LTR. A variant whose `requiresLTR` * is `true` is skipped when this is `false`. * @returns {object|null} * The selected variant entry from `LOGO_VARIATIONS`, or `null` when no * variant in the chain is usable (callers should render the default logo). */ export function pickVariant(variantId, { viewportWidth, isLTR }) { let id = variantId; const seen = new Set(); while (id && !seen.has(id)) { seen.add(id); const v = LOGO_VARIATIONS[id]; if (!v) { return null; } const widthOk = viewportWidth >= v.minViewportWidth; const dirOk = !v.requiresLTR || isLTR; if (widthOk && dirOk) { return v; } id = v.fallback; } return null; } /** * @backward-compat { version 153 } * Subscribe to a set of `(min-width: Npx)` media queries and return the * largest threshold currently matched. Useful for picking a behaviour based * on the current viewport size while only re-rendering on breakpoint * crossings (not on every `resize` tick). * * @param {number[]} thresholds * The breakpoints to observe, in CSS pixels. Duplicates are deduplicated. * Pass a stable array reference (e.g. a module-level constant) so the * underlying `MediaQueryList` instances aren't recreated on every render. * @returns {number} * The largest threshold in `thresholds` whose query currently matches, or * `0` if none of them do. */ function useMaxMatchedMinWidth(thresholds) { const queries = useMemo(() => { const unique = [...new Set(thresholds)].sort((a, b) => a - b); return unique.map(px => ({ px, mql: window.matchMedia(`(min-width: ${px}px)`), })); }, [thresholds]); const computeMax = () => { let max = 0; for (const { px, mql } of queries) { if (mql.matches) { max = px; } } return max; }; const [max, setMax] = useState(computeMax); useEffect(() => { const onChange = () => setMax(computeMax()); for (const { mql } of queries) { mql.addEventListener("change", onChange); } setMax(computeMax()); return () => { for (const { mql } of queries) { mql.removeEventListener("change", onChange); } }; // computeMax is recreated each render but closes over the stable // `queries` array, so depending on `queries` alone is correct. // eslint-disable-next-line react-hooks/exhaustive-deps }, [queries]); return max; } /** * The newtab logo. Renders either the default Firefox logo + wordmark, or a * registered logo variation when one is selected and its environmental * constraints are met. * * Variant selection priority (first non-empty wins): * 1. `prefs.trainhopConfig.logo.variation` (experiment-driven). * 2. `prefs[PREF_LOGO_VARIATION]` (user pref — for local testing). * 3. None → default logo. * * Reduced-motion users still get the variant rendered (statically, at its * frame-0 keyframe state); the variant's click handler is responsible for * not invoking `play()` when motion is suppressed. This keeps the visual * presence consistent across users without forcing animation on anyone. */ function Logo() { // @backward-compat { version 153 } // The four lines below (useSelector + useMaxMatchedMinWidth + isLTR + // the pickVariant/VariantComponent block) can be removed after Firefox // 153 hits Release. Logo() reverts to a plain render of the default // logo + wordmark. const prefs = useSelector(state => state.Prefs.values); const viewportWidth = useMaxMatchedMinWidth(VARIANT_THRESHOLDS); const isLTR = document.dir === "ltr"; const trainhopVariant = prefs.trainhopConfig?.logo?.variation; const prefVariant = prefs[PREF_LOGO_VARIATION]; const variantId = trainhopVariant || prefVariant || null; // All logo variations are gated on the Sports Widget being enabled — // when the widget is off, the variations are conceptually // inapplicable and the standard logo is shown regardless of any // trainhopConfig/pref selection. const widgetsEnabled = prefs[PREF_WIDGETS_ENABLED]; const sportsWidget = WIDGET_REGISTRY.find(w => w.id === "sportsWidget"); const sportsWidgetEnabled = isWidgetEnabled( sportsWidget, prefs, widgetsEnabled ); const variant = sportsWidgetEnabled && variantId ? pickVariant(variantId, { viewportWidth, isLTR }) : null; const VariantComponent = variant?.component; return (

{/** @backward-compat { version 153 } collapse to
after Fx 153 hits Release. */} {VariantComponent ? :
}

); } export { Logo };