mostly fixed layouts
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user