1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-22 03:05:32 +02:00

feat(frontend): j/k keyboard navigation for entries; test avatar cached_bytes

App.jsx — j/k handler:
- Fires on keydown when not focused on INPUT/TEXTAREA/SELECT/contenteditable
- Ignores meta/ctrl/alt modifier combos
- Uses DOM order (#entries-body [data-entry-uid]) so expanded child rows
  participate, matching the shift-range selection logic
- Resolves target entry via entryCacheRef → root entries array →
  fetchEntryDetail(..).summary on cache miss
- Scrolls target into view (block: nearest); updates lastAnchorIndexRef
  so subsequent shift-click ranges start from the keyboard-navigated row

archive.rs — cached_bytes_excludes_avatar_blobs test:
- Two tweet entries sharing an avatar blob (100B) and a media blob (900B)
- Asserts refresh_entry_cached_bytes sets cached_bytes = 900 (not 1000)
- Asserts list_root_entries summary: cached_bytes=900, cacheable_bytes=900,
  total_artifact_bytes=1000 — SIZE includes avatar, % cached denominator
  and numerator both exclude it
This commit is contained in:
TheGeneralist 2026-07-21 15:49:05 +02:00
parent b8cec96256
commit dea8b27fba
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4
4 changed files with 211 additions and 14 deletions

View file

@ -450,6 +450,56 @@ export default function App() {
return () => document.removeEventListener('keydown', handler)
}, [view])
// j/k: navigate entries down/up when not focused on an input element.
useEffect(() => {
const handler = (e) => {
// Ignore when a modifier is held (don't steal browser/app shortcuts)
if (e.metaKey || e.ctrlKey || e.altKey) return
if (e.key !== 'j' && e.key !== 'k') return
// Ignore when focus is on any editable target
const tag = document.activeElement?.tagName
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return
if (document.activeElement?.isContentEditable) return
const allNodes = [...document.querySelectorAll('#entries-body [data-entry-uid]')]
if (allNodes.length === 0) return
const [currentUid] = selectedUids.size === 1 ? selectedUids : [null]
const currentIdx = currentUid
? allNodes.findIndex(n => n.dataset.entryUid === currentUid)
: -1
const nextIdx = e.key === 'j'
? Math.min(currentIdx + 1, allNodes.length - 1)
: Math.max(currentIdx - 1, 0)
if (nextIdx === currentIdx && currentIdx !== -1) return
const nextNode = allNodes[nextIdx < 0 ? 0 : nextIdx]
const nextUid = nextNode.dataset.entryUid
lastAnchorIndexRef.current = nextUid
setSelectedUids(new Set([nextUid]))
// Resolve entry object: cache root entries array server fetch
const cached = entryCacheRef.current.get(nextUid)
?? entries.find(x => x.entry_uid === nextUid)
?? null
if (cached) {
selectEntry(cached)
} else {
fetchEntryDetail(archiveId, nextUid)
.then(det => { if (det?.summary) selectEntry(det.summary) })
.catch(() => {})
}
nextNode.scrollIntoView({ block: 'nearest' })
e.preventDefault()
}
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [selectedUids, entries, selectEntry, archiveId])
// After switching to archive view via K, focus the search input once rendered.
useEffect(() => {
if (view === 'archive' && pendingSearchFocus.current) {