Files
cinny/src/playground/ResizableStage.tsx
litruv 0da4f0d6cb Add emoji confetti bursts and component playground.
Support app.relay.emoji_confetti on jumbo emoji clicks with canvas physics,
per-emoji particle profiles, and a fixed overlay that does not affect scroll.
Also add a Vite playground for live component previews and theme hover preview.
2026-07-29 20:40:43 +10:00

87 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useRef, useState, type CSSProperties } from 'react';
type Props = {
children: React.ReactNode;
};
const MIN = 160;
const MAX = 1200;
export function ResizableStage({ children }: Props) {
const [size, setSize] = useState({ width: 520, height: 480 });
const dragRef = useRef<{
edge: 'e' | 's' | 'se';
startX: number;
startY: number;
startW: number;
startH: number;
} | null>(null);
const onPointerMove = useCallback((e: PointerEvent) => {
const drag = dragRef.current;
if (!drag) return;
const dx = e.clientX - drag.startX;
const dy = e.clientY - drag.startY;
setSize((prev) => {
let width = prev.width;
let height = prev.height;
if (drag.edge === 'e' || drag.edge === 'se') {
width = clamp(drag.startW + dx, MIN, MAX);
}
if (drag.edge === 's' || drag.edge === 'se') {
height = clamp(drag.startH + dy, MIN, MAX);
}
return { width, height };
});
}, []);
const onPointerUp = useCallback(() => {
dragRef.current = null;
window.removeEventListener('pointermove', onPointerMove);
window.removeEventListener('pointerup', onPointerUp);
}, [onPointerMove]);
const startDrag = (edge: 'e' | 's' | 'se') => (e: React.PointerEvent) => {
e.preventDefault();
dragRef.current = {
edge,
startX: e.clientX,
startY: e.clientY,
startW: size.width,
startH: size.height,
};
window.addEventListener('pointermove', onPointerMove);
window.addEventListener('pointerup', onPointerUp);
};
useEffect(() => () => onPointerUp(), [onPointerUp]);
const stageStyle: CSSProperties = {
width: size.width,
height: size.height,
};
return (
<div className="stage-wrap">
<div className="stage-meta">
{size.width} × {size.height}
</div>
<div className="stage" style={stageStyle}>
<div className="stage-canvas">{children}</div>
<button type="button" className="handle handle-e" aria-label="Resize width" onPointerDown={startDrag('e')} />
<button type="button" className="handle handle-s" aria-label="Resize height" onPointerDown={startDrag('s')} />
<button
type="button"
className="handle handle-se"
aria-label="Resize both"
onPointerDown={startDrag('se')}
/>
</div>
</div>
);
}
function clamp(n: number, min: number, max: number) {
return Math.min(max, Math.max(min, n));
}