From 84f3b1646176222df82781c7a198057ecae7a051 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 10:15:52 +0000 Subject: [PATCH 1/4] Fix DraggableGridLayout initial height collapse due to nullish coalescing bug availableWidth starts at 0 (not null/undefined) before ResizeObserver fires. The ?? operator passes 0 through instead of falling back to window.innerWidth, making cellHeight negative and causing react-grid-layout to render a ~10px container. The overflow-x-hidden div then becomes an implicit scroll container, producing the 'cards squeezed in a small rectangle' symptom. Changing ?? to || makes 0 trigger the window.innerWidth fallback, giving a reasonable initial rowHeight until the real container width is measured. https://claude.ai/code/session_01H1sqbcFmtwwsdNTJcJHJWd --- web/src/views/live/DraggableGridLayout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/views/live/DraggableGridLayout.tsx b/web/src/views/live/DraggableGridLayout.tsx index a37a606f0..cfa3ff918 100644 --- a/web/src/views/live/DraggableGridLayout.tsx +++ b/web/src/views/live/DraggableGridLayout.tsx @@ -359,7 +359,7 @@ export default function DraggableGridLayout({ // subtract container margin, 1 camera takes up at least 4 rows // account for additional margin on bottom of each row return ( - ((availableWidth ?? window.innerWidth) - 2 * marginValue) / + ((availableWidth || window.innerWidth) - 2 * marginValue) / 12 / aspectRatio - marginValue + @@ -712,7 +712,7 @@ export default function DraggableGridLayout({ /> Date: Sun, 15 Mar 2026 11:03:26 +0000 Subject: [PATCH 2/4] fix: prevent grid right-edge overflow by gating Responsive on containerWidth Gate rendering on containerWidth > 0 so it only mounts after ResizeObserver has measured the container. Use availableWidth directly as the width prop (no window.innerWidth fallback) since the component now only renders when containerWidth is known. This prevents the grid from rendering wider than its container (which caused the rightmost column to overflow the right edge). https://claude.ai/code/session_01H1sqbcFmtwwsdNTJcJHJWd --- web/src/views/live/DraggableGridLayout.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/views/live/DraggableGridLayout.tsx b/web/src/views/live/DraggableGridLayout.tsx index cfa3ff918..1f75c2fc4 100644 --- a/web/src/views/live/DraggableGridLayout.tsx +++ b/web/src/views/live/DraggableGridLayout.tsx @@ -710,9 +710,9 @@ export default function DraggableGridLayout({ currentGroups={groups} activeGroup={group} /> - 0 && ); })} - + } {isDesktop && (
Date: Sun, 15 Mar 2026 11:32:13 +0000 Subject: [PATCH 3/4] fix: reliably init grid width on page refresh using useLayoutEffect useResizeObserver reads ref.current at render time; on page refresh with fast SWR cache, no re-render occurs after mount so ref.current remains null in the effect, observation never starts, and containerWidth stays 0 forever. Add a useLayoutEffect that measures offsetWidth synchronously before paint as a seed value (effectiveWidth = containerWidth || initialWidth). Once ResizeObserver fires normally, containerWidth takes over. The Responsive grid is gated on effectiveWidth > 0 so it always renders correctly on both first load and refresh. https://claude.ai/code/session_01H1sqbcFmtwwsdNTJcJHJWd --- web/src/views/live/DraggableGridLayout.tsx | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/web/src/views/live/DraggableGridLayout.tsx b/web/src/views/live/DraggableGridLayout.tsx index 1f75c2fc4..c35bacbf3 100644 --- a/web/src/views/live/DraggableGridLayout.tsx +++ b/web/src/views/live/DraggableGridLayout.tsx @@ -332,6 +332,20 @@ export default function DraggableGridLayout({ const [{ width: containerWidth, height: containerHeight }] = useResizeObserver(gridContainerRef); + // useResizeObserver reads ref.current at render time, so it may miss the + // initial mount when ref.current is null (e.g. on page refresh with cached + // SWR data). Measure the container synchronously in useLayoutEffect as a + // reliable seed value; containerWidth from ResizeObserver takes over once + // it fires. + const [initialWidth, setInitialWidth] = useState(0); + useLayoutEffect(() => { + if (gridContainerRef.current) { + setInitialWidth(gridContainerRef.current.offsetWidth); + } + }, []); + + const effectiveWidth = containerWidth || initialWidth; + const scrollBarWidth = useMemo(() => { if (containerWidth && containerHeight && containerRef.current) { return ( @@ -342,8 +356,8 @@ export default function DraggableGridLayout({ }, [containerRef, containerHeight, containerWidth]); const availableWidth = useMemo( - () => (scrollBarWidth ? containerWidth + scrollBarWidth : containerWidth), - [containerWidth, scrollBarWidth], + () => (scrollBarWidth ? effectiveWidth + scrollBarWidth : effectiveWidth), + [effectiveWidth, scrollBarWidth], ); const hasScrollbar = useMemo(() => { @@ -710,7 +724,7 @@ export default function DraggableGridLayout({ currentGroups={groups} activeGroup={group} /> - {containerWidth > 0 && 0 && Date: Sun, 15 Mar 2026 12:03:54 +0000 Subject: [PATCH 4/4] fix: replace useResizeObserver with useLayoutEffect for reliable container width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useResizeObserver reads ref.current during render (before commit), so on first render ref.current is null, no observation starts, and containerWidth stays 0 if no subsequent re-render happens (e.g. page refresh with cached SWR data). useLayoutEffect runs after refs are committed, so ref.current is always the real DOM element. This fixes both the right-column overflow (no window.innerWidth fallback needed — width is always the actual container width) and the black screen on refresh (containerWidth is reliable before the first paint). https://claude.ai/code/session_01H1sqbcFmtwwsdNTJcJHJWd --- web/src/views/live/DraggableGridLayout.tsx | 38 ++++++++++++---------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/web/src/views/live/DraggableGridLayout.tsx b/web/src/views/live/DraggableGridLayout.tsx index c35bacbf3..1f62943e0 100644 --- a/web/src/views/live/DraggableGridLayout.tsx +++ b/web/src/views/live/DraggableGridLayout.tsx @@ -28,7 +28,7 @@ import { VolumeState, } from "@/types/live"; import { Skeleton } from "@/components/ui/skeleton"; -import { useResizeObserver } from "@/hooks/resize-observer"; + import { isEqual } from "lodash"; import useSWR from "swr"; import { isDesktop, isMobile } from "react-device-detect"; @@ -329,23 +329,25 @@ export default function DraggableGridLayout({ const gridContainerRef = useRef(null); - const [{ width: containerWidth, height: containerHeight }] = - useResizeObserver(gridContainerRef); + const [containerWidth, setContainerWidth] = useState(0); + const [containerHeight, setContainerHeight] = useState(0); - // useResizeObserver reads ref.current at render time, so it may miss the - // initial mount when ref.current is null (e.g. on page refresh with cached - // SWR data). Measure the container synchronously in useLayoutEffect as a - // reliable seed value; containerWidth from ResizeObserver takes over once - // it fires. - const [initialWidth, setInitialWidth] = useState(0); + // useLayoutEffect reads ref.current after commit (refs are set before layout + // effects run), so this reliably fires before the first paint regardless of + // whether SWR triggers subsequent re-renders or not. useLayoutEffect(() => { - if (gridContainerRef.current) { - setInitialWidth(gridContainerRef.current.offsetWidth); - } + const el = gridContainerRef.current; + if (!el) return; + setContainerWidth(el.clientWidth); + setContainerHeight(el.clientHeight); + const ro = new ResizeObserver(([entry]) => { + setContainerWidth(entry.contentRect.width); + setContainerHeight(entry.contentRect.height); + }); + ro.observe(el); + return () => ro.disconnect(); }, []); - const effectiveWidth = containerWidth || initialWidth; - const scrollBarWidth = useMemo(() => { if (containerWidth && containerHeight && containerRef.current) { return ( @@ -356,8 +358,8 @@ export default function DraggableGridLayout({ }, [containerRef, containerHeight, containerWidth]); const availableWidth = useMemo( - () => (scrollBarWidth ? effectiveWidth + scrollBarWidth : effectiveWidth), - [effectiveWidth, scrollBarWidth], + () => (scrollBarWidth ? containerWidth + scrollBarWidth : containerWidth), + [containerWidth, scrollBarWidth], ); const hasScrollbar = useMemo(() => { @@ -373,7 +375,7 @@ export default function DraggableGridLayout({ // subtract container margin, 1 camera takes up at least 4 rows // account for additional margin on bottom of each row return ( - ((availableWidth || window.innerWidth) - 2 * marginValue) / + (availableWidth - 2 * marginValue) / 12 / aspectRatio - marginValue + @@ -724,7 +726,7 @@ export default function DraggableGridLayout({ currentGroups={groups} activeGroup={group} /> - {effectiveWidth > 0 && 0 &&