diff options
| author | Fuwn <[email protected]> | 2026-02-07 05:25:30 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-07 05:25:30 -0800 |
| commit | e30e7b30850de620b3274d204df1f2b6090389b2 (patch) | |
| tree | d01eacd413029c885f6b11709efbbd193be048f7 /apps | |
| parent | fix: reset sidebar to midpoint of min/max, detail to 50/50 split (diff) | |
| download | asa.news-e30e7b30850de620b3274d204df1f2b6090389b2.tar.xz asa.news-e30e7b30850de620b3274d204df1f2b6090389b2.zip | |
fix: reset panel sizes without page reload, prevent partial-data max width clamping
Use imperative groupRef API to resize panels instantly instead of
writing to localStorage and calling window.location.reload(). Register
reset callbacks in Zustand store from layout components.
Change sidebarMaxWidth early return from && to || so the generous 35%
fallback is used until both subscriptions and custom feeds have loaded,
preventing intermittent clamping to minimum size.
Diffstat (limited to 'apps')
| -rw-r--r-- | apps/web/app/reader/_components/command-palette.tsx | 36 | ||||
| -rw-r--r-- | apps/web/app/reader/_components/reader-layout-shell.tsx | 42 | ||||
| -rw-r--r-- | apps/web/app/reader/_components/reader-shell.tsx | 21 | ||||
| -rw-r--r-- | apps/web/lib/stores/user-interface-store.ts | 12 |
4 files changed, 74 insertions, 37 deletions
diff --git a/apps/web/app/reader/_components/command-palette.tsx b/apps/web/app/reader/_components/command-palette.tsx index e9b7b04..cca04b6 100644 --- a/apps/web/app/reader/_components/command-palette.tsx +++ b/apps/web/app/reader/_components/command-palette.tsx @@ -195,39 +195,9 @@ export function CommandPalette() { <Command.Item onSelect={() => actionAndClose(() => { - const sidebarPanel = document.querySelector('[data-panel-id="sidebar"]') as HTMLElement | null - const mainContentPanel = document.querySelector('[data-panel-id="main-content"]') as HTMLElement | null - if (sidebarPanel && mainContentPanel) { - const totalPixels = sidebarPanel.offsetWidth + mainContentPanel.offsetWidth - const minPixels = 192 - const maxPixels = parseInt(sidebarPanel.style.maxWidth, 10) || Math.round(totalPixels * 0.35) - const midpoint = Math.round((minPixels + maxPixels) / 2) - localStorage.setItem( - "react-resizable-panels:asa-sidebar-layout:sidebar:main-content", - JSON.stringify({ sidebar: midpoint, "main-content": totalPixels - midpoint }) - ) - } else { - localStorage.removeItem( - "react-resizable-panels:asa-sidebar-layout:sidebar:main-content" - ) - } - - const entryListPanel = document.querySelector('[data-panel-id="entry-list"]') as HTMLElement | null - const detailPanel = document.querySelector('[data-panel-id="detail-panel"]') as HTMLElement | null - if (entryListPanel && detailPanel) { - const totalPixels = entryListPanel.offsetWidth + detailPanel.offsetWidth - const halfPixels = Math.round(totalPixels / 2) - localStorage.setItem( - "react-resizable-panels:asa-detail-layout:entry-list:detail-panel", - JSON.stringify({ "entry-list": halfPixels, "detail-panel": totalPixels - halfPixels }) - ) - } else { - localStorage.removeItem( - "react-resizable-panels:asa-detail-layout:entry-list:detail-panel" - ) - } - - window.location.reload() + const store = useUserInterfaceStore.getState() + store.resetSidebarLayout?.() + store.resetDetailLayout?.() }) } className="cursor-pointer px-2 py-1 text-text-secondary aria-selected:bg-background-tertiary aria-selected:text-text-primary" diff --git a/apps/web/app/reader/_components/reader-layout-shell.tsx b/apps/web/app/reader/_components/reader-layout-shell.tsx index 3ff2fa8..9d2eb24 100644 --- a/apps/web/app/reader/_components/reader-layout-shell.tsx +++ b/apps/web/app/reader/_components/reader-layout-shell.tsx @@ -1,7 +1,7 @@ "use client" -import { Suspense, useEffect, useMemo, useState } from "react" -import { Group, Panel, Separator, useDefaultLayout } from "react-resizable-panels" +import { Suspense, useEffect, useMemo, useRef, useState } from "react" +import { Group, Panel, Separator, useDefaultLayout, useGroupRef } from "react-resizable-panels" import { useUserInterfaceStore } from "@/lib/stores/user-interface-store" import { classNames } from "@/lib/utilities" import { useIsMobile } from "@/lib/hooks/use-is-mobile" @@ -60,7 +60,7 @@ export function ReaderLayoutShell({ const sidebarMaxWidth = useMemo(() => { if (typeof window === "undefined") return "35%" - if (!subscriptionsData && !customFeedsData) return "35%" + if (!subscriptionsData || !customFeedsData) return "35%" const canvas = document.createElement("canvas") const maybeContext = canvas.getContext("2d") @@ -159,6 +159,41 @@ export function ReaderLayoutShell({ storage: typeof window !== "undefined" ? localStorage : { getItem: () => null, setItem: () => {} }, }) + const sidebarGroupRef = useGroupRef() + const sidebarMaxWidthRef = useRef(sidebarMaxWidth) + sidebarMaxWidthRef.current = sidebarMaxWidth + const sidebarOnLayoutChangedRef = useRef(sidebarLayout.onLayoutChanged) + sidebarOnLayoutChangedRef.current = sidebarLayout.onLayoutChanged + + useEffect(() => { + useUserInterfaceStore.getState().setResetSidebarLayout(() => { + const group = sidebarGroupRef.current + if (!group) return + const groupElement = document.querySelector("[data-group]") as HTMLElement | null + if (!groupElement) return + const totalPixels = groupElement.offsetWidth + if (totalPixels === 0) return + const minPixels = 192 + const maxWidthValue = sidebarMaxWidthRef.current + let maxPixels: number + if (maxWidthValue.endsWith("px")) { + maxPixels = parseFloat(maxWidthValue) + } else if (maxWidthValue.endsWith("%")) { + maxPixels = totalPixels * parseFloat(maxWidthValue) / 100 + } else { + maxPixels = totalPixels * 0.35 + } + const midpointPixels = Math.round((minPixels + maxPixels) / 2) + const midpointPercent = (midpointPixels / totalPixels) * 100 + const appliedLayout = group.setLayout({ + sidebar: midpointPercent, + "main-content": 100 - midpointPercent, + }) + sidebarOnLayoutChangedRef.current?.(appliedLayout) + }) + return () => useUserInterfaceStore.getState().setResetSidebarLayout(null) + }, [sidebarGroupRef]) + useKeyboardNavigation() useEffect(() => { @@ -308,6 +343,7 @@ export function ReaderLayoutShell({ orientation="horizontal" defaultLayout={sidebarLayout.defaultLayout} onLayoutChanged={isSidebarCollapsed ? undefined : sidebarLayout.onLayoutChanged} + groupRef={sidebarGroupRef} > {!isSidebarCollapsed && ( <> diff --git a/apps/web/app/reader/_components/reader-shell.tsx b/apps/web/app/reader/_components/reader-shell.tsx index 09278c3..247ec8d 100644 --- a/apps/web/app/reader/_components/reader-shell.tsx +++ b/apps/web/app/reader/_components/reader-shell.tsx @@ -1,6 +1,7 @@ "use client" -import { Group, Panel, Separator, useDefaultLayout } from "react-resizable-panels" +import { useEffect, useRef } from "react" +import { Group, Panel, Separator, useDefaultLayout, useGroupRef } from "react-resizable-panels" import { useUserInterfaceStore } from "@/lib/stores/user-interface-store" import { useMarkAllAsRead } from "@/lib/queries/use-mark-all-as-read" import { useSubscriptions } from "@/lib/queries/use-subscriptions" @@ -58,6 +59,23 @@ export function ReaderShell({ storage: typeof window !== "undefined" ? localStorage : { getItem: () => null, setItem: () => {} }, }) + const detailGroupRef = useGroupRef() + const detailOnLayoutChangedRef = useRef(detailLayout.onLayoutChanged) + detailOnLayoutChangedRef.current = detailLayout.onLayoutChanged + + useEffect(() => { + useUserInterfaceStore.getState().setResetDetailLayout(() => { + const group = detailGroupRef.current + if (!group) return + const appliedLayout = group.setLayout({ + "entry-list": 50, + "detail-panel": 50, + }) + detailOnLayoutChangedRef.current?.(appliedLayout) + }) + return () => useUserInterfaceStore.getState().setResetDetailLayout(null) + }, [detailGroupRef]) + useRealtimeEntries() let pageTitle = feedFilter === "saved" ? "saved" : "all entries" @@ -192,6 +210,7 @@ export function ReaderShell({ className="min-h-0 flex-1" defaultLayout={detailLayout.defaultLayout} onLayoutChanged={detailLayout.onLayoutChanged} + groupRef={detailGroupRef} > <Panel id="entry-list" defaultSize={selectedEntryIdentifier ? "40%" : "100%"} minSize="25%"> <div data-panel-zone="entryList" className={classNames( diff --git a/apps/web/lib/stores/user-interface-store.ts b/apps/web/lib/stores/user-interface-store.ts index 5890167..01dceba 100644 --- a/apps/web/lib/stores/user-interface-store.ts +++ b/apps/web/lib/stores/user-interface-store.ts @@ -44,6 +44,8 @@ interface UserInterfaceState { isShortcutsDialogOpen: boolean expandedFolderIdentifiers: string[] navigableEntryIdentifiers: string[] + resetSidebarLayout: (() => void) | null + resetDetailLayout: (() => void) | null toggleSidebar: () => void setSidebarCollapsed: (isCollapsed: boolean) => void @@ -67,6 +69,8 @@ interface UserInterfaceState { toggleShortcutsDialog: () => void toggleFolderExpansion: (folderIdentifier: string) => void setNavigableEntryIdentifiers: (identifiers: string[]) => void + setResetSidebarLayout: (callback: (() => void) | null) => void + setResetDetailLayout: (callback: (() => void) | null) => void } export const useUserInterfaceStore = create<UserInterfaceState>()( @@ -92,6 +96,8 @@ export const useUserInterfaceStore = create<UserInterfaceState>()( isShortcutsDialogOpen: false, expandedFolderIdentifiers: [], navigableEntryIdentifiers: [], + resetSidebarLayout: null, + resetDetailLayout: null, toggleSidebar: () => set((state) => ({ isSidebarCollapsed: !state.isSidebarCollapsed })), @@ -155,6 +161,12 @@ export const useUserInterfaceStore = create<UserInterfaceState>()( setNavigableEntryIdentifiers: (identifiers) => set({ navigableEntryIdentifiers: identifiers }), + + setResetSidebarLayout: (callback) => + set({ resetSidebarLayout: callback }), + + setResetDetailLayout: (callback) => + set({ resetDetailLayout: callback }), }), { name: "asa-news-ui-preferences", |