/** * ScopeScaleWrapper * * Wraps a scope's items/header block so that on narrow screens the * whole thing scales down as a unit rather than overflowing. * * Critical CSS behaviour used here: * * overflow: clip * Clips the inner element at the outer's edges — identical to * `overflow: hidden` visually, BUT does NOT create a scroll * container. This matters because `position: sticky` elements * inside reference the nearest scroll container ancestor. With * `overflow: hidden` that would be this element (which doesn't * scroll → sticky breaks). With `overflow: clip` there is no * new scroll container → sticky still references the page. * * transform: scale() + transform-origin: top left * Visually shrinks the inner block. Layout size is unaffected * (transform never changes layout). We compensate by setting * the outer div's explicit height = naturalHeight × scale, and * clipping with overflow: clip. */ import type { Component, JSX } from 'solid-js'; import { onMount, onCleanup } from 'solid-js'; interface ScopeScaleWrapperProps { children: JSX.Element; class?: string; /** Minimum scale factor — won't zoom below this. Default: 0.65 */ minScale?: number; } const ScopeScaleWrapper: Component = (props) => { let outerEl: HTMLDivElement | undefined; let innerEl: HTMLDivElement | undefined; const recalculate = () => { if (!outerEl || !innerEl) return; const minScale = props.minScale ?? 0.65; // ── Step 1: measure the inner's TRUE natural width ─────────────── // Must set max-content BEFORE measuring — if width is unset the // element stretches to fill the parent, making scrollWidth == outerWidth // and we'd never think scaling is needed. innerEl.style.width = 'max-content'; innerEl.style.transform = 'none'; outerEl.style.height = ''; outerEl.style.overflow = ''; void outerEl.offsetWidth; // force reflow const naturalWidth = innerEl.scrollWidth; const naturalHeight = innerEl.scrollHeight; const outerWidth = outerEl.clientWidth; if (naturalWidth <= outerWidth) { // ── Fits naturally — fill available width, no clip/scale ────── innerEl.style.width = '100%'; innerEl.style.transform = ''; outerEl.style.height = ''; outerEl.style.overflow = ''; } else { // ── Too wide — scale down ───────────────────────────────────── const rawScale = outerWidth / naturalWidth; const newScale = Math.max(minScale, rawScale); // Fix inner to its natural pixel width so transform-origin math // is reliable, then scale from the top-left corner innerEl.style.width = `${naturalWidth}px`; innerEl.style.transformOrigin = 'top left'; innerEl.style.transform = `scale(${newScale})`; // Set outer height to visual (post-scale) height so no dead space // appears below the scaled block outerEl.style.height = `${naturalHeight * newScale}px`; // overflow: clip — clips layout overflow WITHOUT creating a scroll // container, so position:sticky inside still works against the page outerEl.style.overflow = 'clip'; } }; let ro: ResizeObserver; onMount(() => { ro = new ResizeObserver(recalculate); if (outerEl) ro.observe(outerEl); if (innerEl) ro.observe(innerEl); recalculate(); }); onCleanup(() => ro?.disconnect()); return (
{props.children}
); }; export default ScopeScaleWrapper;