diff --git a/apps/web/src/domains/home/home-feed-filter-bar.tsx b/apps/web/src/domains/home/home-feed-filter-bar.tsx index 9ae97f74caa..158984b19d6 100644 --- a/apps/web/src/domains/home/home-feed-filter-bar.tsx +++ b/apps/web/src/domains/home/home-feed-filter-bar.tsx @@ -111,7 +111,7 @@ export function HomeFeedFilterBar({ categories.includes(c), ); - if (presentCategories.length === 0) return null; + if (presentCategories.length <= 1) return null; return (
diff --git a/apps/web/src/domains/home/home-feed-list.tsx b/apps/web/src/domains/home/home-feed-list.tsx index 1f81d51dc07..c9088026dfa 100644 --- a/apps/web/src/domains/home/home-feed-list.tsx +++ b/apps/web/src/domains/home/home-feed-list.tsx @@ -36,15 +36,31 @@ export function HomeFeedList({ const visible = items.filter((item) => item.status !== "dismissed"); const eligible = excludeHighUrgency(visible); const presentCategories = getPresentCategories(eligible); - const filtered = filterByCategory(eligible, activeFilter); + const effectiveFilter = + activeFilter && presentCategories.includes(activeFilter) + ? activeFilter + : null; + + // Reset stale activeFilter during render when its category disappears + // from the feed. Without this, the previously-selected filter would + // silently re-activate if the category later reappears (e.g. a new + // notification of that category arrives). React bails out when the + // next state equals the current, so this is safe and preferable to a + // synchronization Effect. + // https://react.dev/reference/react/useState#storing-information-from-previous-renders + if (activeFilter !== effectiveFilter) { + setActiveFilter(effectiveFilter); + } + + const filtered = filterByCategory(eligible, effectiveFilter); const sorted = sortFeedItems(filtered); const grouped = groupByTime(sorted); return ( -
+
@@ -53,7 +69,7 @@ export function HomeFeedList({ variant="body-medium-lighter" className="py-[var(--app-spacing-xl)] text-center text-[var(--content-disabled)]" > - {activeFilter + {effectiveFilter ? "No items match the selected filter." : "No items to show."} diff --git a/apps/web/src/domains/home/home-page.tsx b/apps/web/src/domains/home/home-page.tsx index 532edffb282..ad09693d705 100644 --- a/apps/web/src/domains/home/home-page.tsx +++ b/apps/web/src/domains/home/home-page.tsx @@ -150,7 +150,7 @@ export function HomePage({ return ( {isUnread && ( - + )} diff --git a/apps/web/src/domains/home/home-suggestion-pill-bar.tsx b/apps/web/src/domains/home/home-suggestion-pill-bar.tsx index 290a4707e21..519a14ce773 100644 --- a/apps/web/src/domains/home/home-suggestion-pill-bar.tsx +++ b/apps/web/src/domains/home/home-suggestion-pill-bar.tsx @@ -80,7 +80,7 @@ export function HomeSuggestionPillBar({ style={{ width: 26, height: 26 }} aria-hidden="true" > - + {suggestion.label} diff --git a/packages/design-library/src/components/resizable-panel.tsx b/packages/design-library/src/components/resizable-panel.tsx index dd115af9ca1..9c233d27cd8 100644 --- a/packages/design-library/src/components/resizable-panel.tsx +++ b/packages/design-library/src/components/resizable-panel.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, + useLayoutEffect, useRef, useState, type ComponentProps, @@ -9,6 +10,27 @@ import { import { cn } from "../utils/cn.js"; +/** + * Read a persisted pixel width from localStorage, validating both shape + * and finiteness. Returns `null` for unset/malformed entries or when + * storage access throws (strict-privacy contexts, quota errors, SSR). + */ +function readStoredWidth( + storageKey: string | undefined, + minLeftWidth: number, +): number | null { + if (!storageKey || typeof window === "undefined") return null; + try { + const stored = localStorage.getItem(storageKey); + if (stored == null) return null; + const parsed = Number(stored); + if (!Number.isFinite(parsed)) return null; + return Math.max(minLeftWidth, parsed); + } catch { + return null; + } +} + export interface ResizablePanelProps extends Omit, "children"> { /** Content for the left pane. */ left: ReactNode; @@ -16,6 +38,8 @@ export interface ResizablePanelProps extends Omit, "childr right: ReactNode; /** Initial width of the left pane in px (default 400). */ defaultLeftWidth?: number; + /** Initial width of the left pane as a percentage of the container (0–100). Resolved via useLayoutEffect on mount. */ + defaultLeftPercent?: number; /** Minimum left pane width in px (default 300). */ minLeftWidth?: number; /** Minimum right pane width in px (default 300). */ @@ -36,6 +60,7 @@ export function ResizablePanel({ left, right, defaultLeftWidth = 400, + defaultLeftPercent, minLeftWidth = 300, minRightWidth = 300, onWidthChange, @@ -45,20 +70,9 @@ export function ResizablePanel({ }: ResizablePanelProps) { const containerRef = useRef(null); - const [leftWidth, setLeftWidth] = useState(() => { - if (storageKey) { - try { - const stored = localStorage.getItem(storageKey); - if (stored != null) { - const parsed = Number(stored); - if (Number.isFinite(parsed)) return Math.max(minLeftWidth, parsed); - } - } catch { - // localStorage access can throw under strict-privacy contexts. - } - } - return defaultLeftWidth; - }); + const [leftWidth, setLeftWidth] = useState( + () => readStoredWidth(storageKey, minLeftWidth) ?? defaultLeftWidth, + ); const dragRef = useRef<{ startX: number; startWidth: number } | null>(null); const [isDragging, setIsDragging] = useState(false); @@ -125,6 +139,19 @@ export function ResizablePanel({ return () => window.removeEventListener("resize", onResize); }, [clamp]); + // Resolve percentage-based default on mount (before paint) when no valid + // persisted preference exists. Runs in useLayoutEffect so the resolved + // width is committed before the browser paints, preventing a single-frame + // flash of the `defaultLeftWidth` pixel fallback. + useLayoutEffect(() => { + if (defaultLeftPercent == null) return; + if (readStoredWidth(storageKey, minLeftWidth) !== null) return; + const container = containerRef.current; + if (!container) return; + const target = (container.offsetWidth * defaultLeftPercent) / 100; + setLeftWidth(clamp(target)); + }, [defaultLeftPercent, storageKey, minLeftWidth, clamp]); + return (