summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-07 05:25:30 -0800
committerFuwn <[email protected]>2026-02-07 05:25:30 -0800
commite30e7b30850de620b3274d204df1f2b6090389b2 (patch)
treed01eacd413029c885f6b11709efbbd193be048f7
parentfix: reset sidebar to midpoint of min/max, detail to 50/50 split (diff)
downloadasa.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.
-rw-r--r--apps/web/app/reader/_components/command-palette.tsx36
-rw-r--r--apps/web/app/reader/_components/reader-layout-shell.tsx42
-rw-r--r--apps/web/app/reader/_components/reader-shell.tsx21
-rw-r--r--apps/web/lib/stores/user-interface-store.ts12
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",