mostly fixed layouts

This commit is contained in:
2026-03-13 18:31:21 -05:00
parent dfc4cccb56
commit c72da2fad5
5 changed files with 391 additions and 263 deletions
+95
View File
@@ -0,0 +1,95 @@
/**
* ScopeScaleWrapper
*
* Wraps a scope's grid area (header + rows) in a container that:
* 1. Scales the inner content DOWN to fit the available width on
* small screens, so there is no excess horizontal scroll.
* 2. If content is even too wide at minScale, falls back to
* horizontal scroll only at that point.
*
* Key behaviour:
* - When content fits (scale = 1): inner is width:100%, outer has
* overflow-x:hidden — zero unused scroll space.
* - When too wide to fit: inner is width:max-content so it renders
* at its natural size, scale transform shrinks it visually, and
* outer height is corrected for the scaled layout height.
*/
import type { Component, JSX } from 'solid-js';
import { onMount, onCleanup, createSignal } from 'solid-js';
interface ScopeScaleWrapperProps {
children: JSX.Element;
class?: string;
/** minimum scale — won't zoom below this (default 0.65) */
minScale?: number;
}
const ScopeScaleWrapper: Component<ScopeScaleWrapperProps> = (props) => {
let outerEl: HTMLDivElement | undefined;
let innerEl: HTMLDivElement | undefined;
const [scale, setScale] = createSignal(1);
const [isZooming, setIsZooming] = createSignal(false);
const recalculate = () => {
if (!outerEl || !innerEl) return;
// Temporarily switch inner to max-content so we can measure
// its natural width without being constrained by the parent.
innerEl.style.width = 'max-content';
const naturalWidth = innerEl.scrollWidth;
const outerWidth = outerEl.clientWidth;
if (naturalWidth <= outerWidth) {
// Content fits — no zoom, no excess scroll
setScale(1);
setIsZooming(false);
innerEl.style.width = '100%';
innerEl.style.height = '';
innerEl.style.removeProperty('--scale-factor');
} else {
const min = props.minScale ?? 0.65;
const newScale = Math.max(min, outerWidth / naturalWidth);
setScale(newScale);
setIsZooming(true);
// Keep width:max-content so content renders at natural size
innerEl.style.width = 'max-content';
// Correct height so the card shrinks with the scaled content
const naturalHeight = innerEl.scrollHeight;
innerEl.style.height = (naturalHeight * newScale) + 'px';
innerEl.style.setProperty('--scale-factor', String(newScale));
}
};
let ro: ResizeObserver;
onMount(() => {
ro = new ResizeObserver(recalculate);
if (outerEl) ro.observe(outerEl);
recalculate();
});
onCleanup(() => ro?.disconnect());
return (
<div
ref={outerEl}
class={`relative ${isZooming() ? 'overflow-x-auto' : 'overflow-x-hidden'} overflow-y-visible ${props.class ?? ''}`}
>
<div
ref={innerEl}
style={{
'transform-origin': 'top left',
'transform': `scale(${scale()})`,
'transition': 'transform 0.2s ease',
'will-change': 'transform',
}}
>
{props.children}
</div>
</div>
);
};
export default ScopeScaleWrapper;