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:
parent
b8cec96256
commit
dea8b27fba
4 changed files with 211 additions and 14 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue