From 5cf8275540d7162cd4936a7c0e76dbfe7f66b62c Mon Sep 17 00:00:00 2001 From: Adam Mathes Date: Mon, 16 Feb 2026 10:53:59 -0800 Subject: V3 UI Polish: Improved keyboard navigation, fixed logo position, and updated branding - Fix V3 keyboard navigation delay (resolved NK-wjats7) - Update V3 document title to 'neko' (resolved NK-4p3s91) - Fix V3 neko logo/button position to be top-left fixed (resolved NK-89za3s) - Improve FeedItems (React) stability with ref-based index tracking and robust tests - Sync V3 styling and selection feedback with V2 patterns - Rebuild production assets --- frontend/src/components/FeedItems.test.tsx | 5 +++++ frontend/src/components/FeedItems.tsx | 21 ++++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) (limited to 'frontend/src') diff --git a/frontend/src/components/FeedItems.test.tsx b/frontend/src/components/FeedItems.test.tsx index 89c591c..fc95948 100644 --- a/frontend/src/components/FeedItems.test.tsx +++ b/frontend/src/components/FeedItems.test.tsx @@ -265,8 +265,13 @@ describe('FeedItems Component', () => { }); fireEvent.keyDown(window, { key: 'j' }); // index 0 + await waitFor(() => expect(document.getElementById('item-0')).toHaveAttribute('data-selected', 'true')); + fireEvent.keyDown(window, { key: 'j' }); // index 1 + await waitFor(() => expect(document.getElementById('item-1')).toHaveAttribute('data-selected', 'true')); + fireEvent.keyDown(window, { key: 'j' }); // index 2 (last item) + await waitFor(() => expect(document.getElementById('item-2')).toHaveAttribute('data-selected', 'true')); await waitFor(() => { expect(screen.getByText('Item 0')).toBeInTheDocument(); diff --git a/frontend/src/components/FeedItems.tsx b/frontend/src/components/FeedItems.tsx index 2c3253b..ea5d8fd 100644 --- a/frontend/src/components/FeedItems.tsx +++ b/frontend/src/components/FeedItems.tsx @@ -19,6 +19,7 @@ export default function FeedItems() { const hasMoreRef = useRef(hasMore); const [error, setError] = useState(''); const [selectedIndex, setSelectedIndex] = useState(-1); + const selectedIndexRef = useRef(selectedIndex); // Sync refs useEffect(() => { @@ -33,6 +34,10 @@ export default function FeedItems() { hasMoreRef.current = hasMore; }, [hasMore]); + useEffect(() => { + selectedIndexRef.current = selectedIndex; + }, [selectedIndex]); + const fetchItems = useCallback((maxId?: string) => { if (maxId) { setLoadingMore(true); @@ -156,8 +161,9 @@ export default function FeedItems() { if (currentItems.length === 0) return; if (e.key === 'j') { - const nextIndex = Math.min(selectedIndex + 1, currentItems.length - 1); - if (nextIndex !== selectedIndex) { + const nextIndex = Math.min(selectedIndexRef.current + 1, currentItems.length - 1); + if (nextIndex !== selectedIndexRef.current) { + selectedIndexRef.current = nextIndex; setSelectedIndex(nextIndex); const item = currentItems[nextIndex]; if (!item.read) { @@ -174,21 +180,22 @@ export default function FeedItems() { fetchItems(String(currentItems[currentItems.length - 1]._id)); } } else if (e.key === 'k') { - const nextIndex = Math.max(selectedIndex - 1, 0); - if (nextIndex !== selectedIndex) { + const nextIndex = Math.max(selectedIndexRef.current - 1, 0); + if (nextIndex !== selectedIndexRef.current) { + selectedIndexRef.current = nextIndex; setSelectedIndex(nextIndex); scrollToItem(nextIndex); } } else if (e.key === 's') { - if (selectedIndex >= 0 && selectedIndex < currentItems.length) { - toggleStar(currentItems[selectedIndex]); + if (selectedIndexRef.current >= 0 && selectedIndexRef.current < currentItems.length) { + toggleStar(currentItems[selectedIndexRef.current]); } } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); - }, [markAsRead, scrollToItem, toggleStar, fetchItems, selectedIndex]); + }, [markAsRead, scrollToItem, toggleStar, fetchItems]); // Stable Observer -- cgit v1.2.3