components/Stage/Stage.jsxjavascript
import PropTypes from "prop-types";
import { useLocation } from "react-router";
import { useConfig } from "../../services/config/config";
import { useElementSize } from "../../hooks/elementSize";
import { resolveModeForPath } from "../../services/presentationModes";
import "./Stage.scss";

/**
 * Whole-app scale wrapper. Renders `children` inside a fixed-size box and
 * scales that box down to fit whatever real screen it ends up on via CSS
 * `transform: scale()`.
 *
 * Replaces the previous per-page `calc(1rem / X)` + html-root-font-size
 * scaling scheme: instead of keeping dozens of components' em/rem units in
 * sync with two independent, uncoordinated breakpoint systems (the app's
 * own root font-size AND @ui-marcom/shared's internal viewport-width
 * breakpoints), every pixel authored anywhere in `children` scales together
 * as one image - which system decided a given size no longer matters once
 * it's inside this box.
 *
 * Each sales app has one fixed, content-driven orientation (confirmed with
 * the content team: everything is landscape except Rad Enablement, which is
 * portrait-only - see PRESENTATION_MODES' `orientation` field, matched
 * against the current route via resolveModeForPath()). "/" (app selection)
 * matches no mode and defaults to landscape.
 *
 * The reference box has two independent axes, not one:
 * - WHICH physical kiosk this is (`stageDevice` config value: "4k" or
 *   "ipad") picks that device's own landscape width/height pair. A 4K panel
 *   and an iPad need separate pairs (not a shared one) because an iPad is a
 *   2x Retina display - its CSS point resolution is roughly half its
 *   physical pixel count, very different from a 4K panel's 1:1 mapping.
 * - Orientation then swaps THAT device's own pair for portrait routes - "a
 *   physically rotated panel" is only true *within* one device's pair, not
 *   across devices. Treating portrait as a single cross-device resolution
 *   was tried first and broke both ends: Rad Enablement letterboxed on the
 *   iPad (wrong aspect ratio), and would have upscaled/blurred on a
 *   rotated 4K panel (wrong magnitude) - see README.md's "Scaling (Stage)"
 *   section.
 *
 * The scale factor is never allowed above 1 in practice: the reference
 * resolution is deliberately the *largest* target device in its
 * orientation, so real screens only ever scale down. Upscaling would blur
 * the WebGL globe (InteractiveGlobe.jsx), whose canvas render resolution
 * three.js derives from layout size, not from a CSS transform applied to an
 * ancestor.
 *
 * `stage-inner` also carries `data-stage-device` so per-device CSS overrides
 * (e.g. RadEnablement.scss's iPad content-density correction, since the
 * same em-authored content necessarily renders larger on the iPad's
 * ~2x-smaller reference box) can target a device without their own JS.
 *
 * Must be mounted inside the router (see HashRouter.jsx), not in App.jsx -
 * it reads its config (stageDevice and the stageRefWidth/Height /
 * stageIpadRefWidth/Height pairs) via useConfig() and the current route via
 * useLocation(), both of which need router context and would silently fail
 * (or throw) outside a <HashRouter>.
 */
export const Stage = ({ children }) => {
  const { stageDevice, stageRefWidth, stageRefHeight, stageIpadRefWidth, stageIpadRefHeight } =
    useConfig();
  const { pathname } = useLocation();
  const [outerRef, { width, height }] = useElementSize({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  const [deviceLandscapeWidth, deviceLandscapeHeight] =
    stageDevice === "ipad"
      ? [stageIpadRefWidth, stageIpadRefHeight]
      : [stageRefWidth, stageRefHeight];

  const isPortrait = resolveModeForPath(pathname)?.orientation === "portrait";
  const refWidth = isPortrait ? deviceLandscapeHeight : deviceLandscapeWidth;
  const refHeight = isPortrait ? deviceLandscapeWidth : deviceLandscapeHeight;

  // "contain" scaling: shrink just enough that both dimensions fit, so the
  // reference box never gets cropped - whichever dimension is relatively
  // narrower on the real screen determines the factor, the other dimension
  // letterboxes.
  const scale = Math.min(width / refWidth, height / refHeight) || 1;

  return (
    <div className="stage-outer" ref={outerRef}>
      <div
        className="stage-inner"
        data-stage-device={stageDevice}
        style={{
          width: refWidth,
          height: refHeight,
          transform: `scale(${scale})`,
        }}>
        {children}
      </div>
    </div>
  );
};

Stage.propTypes = {
  children: PropTypes.node.isRequired,
};