diff --git a/apps/web/src/components/ui/sidebar.tsx b/apps/web/src/components/ui/sidebar.tsx index d27412c9599..0cb290b2fae 100644 --- a/apps/web/src/components/ui/sidebar.tsx +++ b/apps/web/src/components/ui/sidebar.tsx @@ -53,6 +53,16 @@ type SidebarResizableOptions = { storageKey?: string; }; +const SidebarStoredWidthSchema = Schema.Union([ + Schema.Finite, + Schema.Struct({ + unit: Schema.Literal("fraction"), + value: Schema.Finite, + }), +]); + +type SidebarStoredWidth = typeof SidebarStoredWidthSchema.Type; + type SidebarResolvedResizableOptions = { maxWidth: number; minWidth: number; @@ -335,6 +345,26 @@ function clampSidebarWidth(width: number, options: SidebarResolvedResizableOptio return Math.max(options.minWidth, Math.min(width, options.maxWidth)); } +function getSidebarWidthStorageBasis(wrapper: HTMLElement): number { + return wrapper.parentElement?.clientWidth || window.innerWidth; +} + +function getStoredSidebarWidth( + storedWidth: SidebarStoredWidth, + wrapper: HTMLElement, +): number | null { + if (typeof storedWidth === "number") { + return null; + } + + return storedWidth.value * getSidebarWidthStorageBasis(wrapper); +} + +function createStoredSidebarWidth(width: number, wrapper: HTMLElement): SidebarStoredWidth { + const basis = getSidebarWidthStorageBasis(wrapper); + return { unit: "fraction", value: basis > 0 ? width / basis : 0 }; +} + function SidebarRail({ className, onClick, @@ -380,7 +410,11 @@ function SidebarRail({ element.style.removeProperty("transition-duration"); }); if (resolvedResizable?.storageKey && typeof window !== "undefined") { - setLocalStorageItem(resolvedResizable.storageKey, resizeState.width, Schema.Finite); + setLocalStorageItem( + resolvedResizable.storageKey, + createStoredSidebarWidth(resizeState.width, resizeState.wrapper), + SidebarStoredWidthSchema, + ); } resolvedResizable?.onResize?.(resizeState.width); resizeStateRef.current = null; @@ -545,16 +579,34 @@ function SidebarRail({ React.useEffect(() => { if (!resolvedResizable?.storageKey || typeof window === "undefined") return; + const storageKey = resolvedResizable.storageKey; const rail = railRef.current; if (!rail) return; const wrapper = rail.closest("[data-slot='sidebar-wrapper']"); if (!wrapper) return; + const storageBasisElement = wrapper.parentElement; + + const applyStoredWidth = () => { + const storedWidth = getLocalStorageItem(storageKey, SidebarStoredWidthSchema); + if (storedWidth === null) return; + const width = getStoredSidebarWidth(storedWidth, wrapper); + if (width === null) return; + const clampedWidth = clampSidebarWidth(width, resolvedResizable); + wrapper.style.setProperty("--sidebar-width", `${clampedWidth}px`); + resolvedResizable.onResize?.(clampedWidth); + }; - const storedWidth = getLocalStorageItem(resolvedResizable.storageKey, Schema.Finite); - if (storedWidth === null) return; - const clampedWidth = clampSidebarWidth(storedWidth, resolvedResizable); - wrapper.style.setProperty("--sidebar-width", `${clampedWidth}px`); - resolvedResizable.onResize?.(clampedWidth); + applyStoredWidth(); + window.addEventListener("resize", applyStoredWidth); + const resizeObserver = new ResizeObserver(applyStoredWidth); + if (storageBasisElement) { + resizeObserver.observe(storageBasisElement); + } + + return () => { + window.removeEventListener("resize", applyStoredWidth); + resizeObserver?.disconnect(); + }; }, [resolvedResizable]); React.useEffect(() => {