Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/src/domains/home/home-feed-filter-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export function HomeFeedFilterBar({
categories.includes(c),
);

if (presentCategories.length === 0) return null;
if (presentCategories.length <= 1) return null;

return (
<div className="flex items-center gap-[var(--app-spacing-sm)] overflow-x-auto">
Expand Down
24 changes: 20 additions & 4 deletions apps/web/src/domains/home/home-feed-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines +39 to +42
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Stale activeFilter state can silently re-activate when categories reappear

The effectiveFilter derived state at apps/web/src/domains/home/home-feed-list.tsx:39-42 gracefully falls back to null when the selected category disappears from presentCategories. However, the underlying activeFilter React state is never reset — it retains the old category. If that category later reappears (e.g., new items arrive), effectiveFilter will snap back to the previously selected filter without user interaction. This is an edge case (requires dismissing all items of a category, then new items arriving in the same category), and could be seen as a feature ("sticky filter memory"), but it may surprise users. A useEffect that calls setActiveFilter(null) when activeFilter is not in presentCategories would make the reset explicit.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


// 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 (
<div className="flex flex-col gap-[var(--app-spacing-lg)]">
<div className="flex flex-col gap-[var(--app-spacing-sm)]">
<HomeFeedFilterBar
categories={presentCategories}
activeFilter={activeFilter}
activeFilter={effectiveFilter}
onFilterChange={setActiveFilter}
/>

Expand All @@ -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."}
</Typography>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/domains/home/home-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export function HomePage({
return (
<ResizablePanel
storageKey="homeDetailPanelWidth"
defaultLeftWidth={600}
defaultLeftPercent={50}
minLeftWidth={400}
minRightWidth={320}
left={
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/domains/home/home-recap-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export function HomeRecapRow({ item, onSelect, onDismiss }: HomeRecapRowProps) {
<Icon width={12} height={12} style={{ color: style.strong }} />
</span>
{isUnread && (
<span className="absolute -right-0.5 -top-0.5 h-2 w-2 rounded-full bg-[var(--system-mid-strong)]" />
<span className="absolute -left-0.5 -top-0.5 h-2 w-2 rounded-full bg-[var(--system-mid-strong)]" />
)}
</span>

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/domains/home/home-suggestion-pill-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function HomeSuggestionPillBar({
style={{ width: 26, height: 26 }}
aria-hidden="true"
>
<Icon className="size-[9px]" />
<Icon className="size-[18px]" />
</span>
<span className="text-body-small-default">
{suggestion.label}
Expand Down
55 changes: 41 additions & 14 deletions packages/design-library/src/components/resizable-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
type ComponentProps,
Expand All @@ -9,13 +10,36 @@ 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<ComponentProps<"div">, "children"> {
/** Content for the left pane. */
left: ReactNode;
/** Content for the right pane. */
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). */
Expand All @@ -36,6 +60,7 @@ export function ResizablePanel({
left,
right,
defaultLeftWidth = 400,
defaultLeftPercent,
minLeftWidth = 300,
minRightWidth = 300,
onWidthChange,
Expand All @@ -45,20 +70,9 @@ export function ResizablePanel({
}: ResizablePanelProps) {
const containerRef = useRef<HTMLDivElement>(null);

const [leftWidth, setLeftWidth] = useState<number>(() => {
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<number>(
() => readStoredWidth(storageKey, minLeftWidth) ?? defaultLeftWidth,
);

const dragRef = useRef<{ startX: number; startWidth: number } | null>(null);
const [isDragging, setIsDragging] = useState(false);
Expand Down Expand Up @@ -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 (
<div
{...rest}
Expand Down
Loading