aboutsummaryrefslogtreecommitdiffstats
path: root/frontend-vanilla/src
diff options
context:
space:
mode:
authorAdam Mathes <adam@adammathes.com>2026-02-16 10:53:59 -0800
committerAdam Mathes <adam@trenchant.org>2026-02-16 10:57:38 -0800
commit5cf8275540d7162cd4936a7c0e76dbfe7f66b62c (patch)
treefdb3c47560f1b1556f7c203f72d50d13f171c95a /frontend-vanilla/src
parent96e78c5fdfada73d37644083c7580a1d444ed748 (diff)
downloadneko-5cf8275540d7162cd4936a7c0e76dbfe7f66b62c.tar.gz
neko-5cf8275540d7162cd4936a7c0e76dbfe7f66b62c.tar.bz2
neko-5cf8275540d7162cd4936a7c0e76dbfe7f66b62c.zip
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
Diffstat (limited to 'frontend-vanilla/src')
-rw-r--r--frontend-vanilla/src/components/FeedItem.ts4
-rw-r--r--frontend-vanilla/src/main.test.ts21
-rw-r--r--frontend-vanilla/src/main.ts43
-rw-r--r--frontend-vanilla/src/style.css17
4 files changed, 67 insertions, 18 deletions
diff --git a/frontend-vanilla/src/components/FeedItem.ts b/frontend-vanilla/src/components/FeedItem.ts
index e58aac8..212e9dd 100644
--- a/frontend-vanilla/src/components/FeedItem.ts
+++ b/frontend-vanilla/src/components/FeedItem.ts
@@ -1,9 +1,9 @@
import type { Item } from '../types';
-export function createFeedItem(item: Item): string {
+export function createFeedItem(item: Item, isSelected: boolean = false): string {
const date = new Date(item.publish_date).toLocaleDateString();
return `
- <li class="feed-item ${item.read ? 'read' : 'unread'}" data-id="${item._id}">
+ <li class="feed-item ${item.read ? 'read' : 'unread'} ${isSelected ? 'selected' : ''}" data-id="${item._id}">
<div class="item-header">
<a href="${item.url}" target="_blank" rel="noopener noreferrer" class="item-title" data-action="open">
${item.title || '(No Title)'}
diff --git a/frontend-vanilla/src/main.test.ts b/frontend-vanilla/src/main.test.ts
index c9d0e0c..5bf9fe0 100644
--- a/frontend-vanilla/src/main.test.ts
+++ b/frontend-vanilla/src/main.test.ts
@@ -347,4 +347,25 @@ describe('main application logic', () => {
expect(manageSection?.innerHTML).toContain('My Feed');
expect(document.querySelector('.feed-tag-input')).not.toBeNull();
});
+
+ it('should navigate items with j/k keys', () => {
+ store.setItems([
+ { _id: 101, title: 'Item 1', publish_date: '2023-01-01', read: false } as any,
+ { _id: 102, title: 'Item 2', publish_date: '2023-01-02', read: false } as any
+ ]);
+ renderLayout();
+ renderItems();
+
+ // 1st press 'j' -> index 0
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'j' }));
+ expect(document.querySelector('.feed-item[data-id="101"]')?.classList.contains('selected')).toBe(true);
+
+ // 2nd press 'j' -> index 1
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'j' }));
+ expect(document.querySelector('.feed-item[data-id="102"]')?.classList.contains('selected')).toBe(true);
+
+ // Press 'k' -> back to index 0
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'k' }));
+ expect(document.querySelector('.feed-item[data-id="101"]')?.classList.contains('selected')).toBe(true);
+ });
});
diff --git a/frontend-vanilla/src/main.ts b/frontend-vanilla/src/main.ts
index 3f53fc2..7b55c48 100644
--- a/frontend-vanilla/src/main.ts
+++ b/frontend-vanilla/src/main.ts
@@ -185,6 +185,14 @@ export function attachLayoutListeners() {
const itemRow = target.closest('.feed-item');
if (itemRow && !itemTitle) { // Clicking the row itself (but not the link)
const id = parseInt(itemRow.getAttribute('data-id')!);
+ activeItemId = id;
+
+ // Update visual selection
+ document.querySelectorAll('.feed-item').forEach(el => {
+ const itemId = parseInt(el.getAttribute('data-id') || '0');
+ el.classList.toggle('selected', itemId === activeItemId);
+ });
+
const item = store.items.find(i => i._id === id);
if (item && !item.read) {
updateItem(id, { read: true });
@@ -252,7 +260,7 @@ export function renderItems() {
contentArea.innerHTML = `
<ul class="item-list">
- ${items.map((item: Item) => createFeedItem(item)).join('')}
+ ${items.map((item: Item) => createFeedItem(item, item._id === activeItemId)).join('')}
</ul>
${store.hasMore ? '<div id="load-more-sentinel" class="loading-more">Loading more...</div>' : ''}
`;
@@ -682,19 +690,28 @@ window.addEventListener('keydown', (e) => {
function navigateItems(direction: number) {
if (store.items.length === 0) return;
- let index = store.items.findIndex(i => i._id === activeItemId);
- index += direction;
- if (index >= 0 && index < store.items.length) {
- activeItemId = store.items[index]._id;
- const el = document.querySelector(`.feed-item[data-id="${activeItemId}"]`);
- if (el) el.scrollIntoView({ block: 'nearest' });
- // Optional: mark as read when keyboard navigating
- if (!store.items[index].read) updateItem(activeItemId, { read: true });
- // Since we are in 2-pane, we just scroll to it.
- } else if (index === -1) {
- activeItemId = store.items[0]._id;
+ const currentIndex = store.items.findIndex(i => i._id === activeItemId);
+ let nextIndex;
+
+ if (currentIndex === -1) {
+ nextIndex = direction > 0 ? 0 : store.items.length - 1;
+ } else {
+ nextIndex = currentIndex + direction;
+ }
+
+ if (nextIndex >= 0 && nextIndex < store.items.length) {
+ activeItemId = store.items[nextIndex]._id;
+
+ // Update visual selection without full re-render for speed
+ document.querySelectorAll('.feed-item').forEach(el => {
+ const id = parseInt(el.getAttribute('data-id') || '0');
+ el.classList.toggle('selected', id === activeItemId);
+ });
+
const el = document.querySelector(`.feed-item[data-id="${activeItemId}"]`);
- if (el) el.scrollIntoView({ block: 'nearest' });
+ if (el) el.scrollIntoView({ block: 'start', behavior: 'smooth' });
+
+ if (!store.items[nextIndex].read) updateItem(activeItemId, { read: true });
}
}
diff --git a/frontend-vanilla/src/style.css b/frontend-vanilla/src/style.css
index 0076e58..37ba761 100644
--- a/frontend-vanilla/src/style.css
+++ b/frontend-vanilla/src/style.css
@@ -348,9 +348,20 @@ select:focus {
}
.feed-item {
- padding: 1rem 0;
- margin-top: 5rem;
+ padding: 1rem 0.5rem;
+ margin-top: 2rem;
border-bottom: none;
+ border-radius: 8px;
+ transition: background-color 0.2s ease;
+}
+
+.feed-item.selected {
+ background-color: rgba(0, 123, 255, 0.05);
+ box-shadow: inset 4px 0 0 var(--accent-color);
+}
+
+.theme-dark .feed-item.selected {
+ background-color: rgba(33, 136, 255, 0.1);
}
.item-header {
@@ -452,7 +463,7 @@ select:focus {
--bg-color: #24292e;
--text-color: #ffffff;
--sidebar-bg: #1b1f23;
- --link-color: rgb(90, 200, 250);
+ --link-color: #5ac8fa;
--border-color: #444d56;
--accent-color: #2188ff;
}