feat(timeline): focus follows archive, snaps to first on view load

- New selection.focusAfter(excluded) walks selection.order forward past
  the archived/restored set so X-ing through the timeline keeps the
  cursor on the next live photo instead of falling back to photo[0]
  via the auto-anchor effect. Wired into gridKeyNav.toggleArchive (X
  key) and BulkActionBar.onArchive.
- Auto-focus effect on the timeline always re-anchors to photos[0] on
  view load (pageCount → 1), instead of preserving a stale uid from
  the previous filter.
- PhotoGrid re-anchors focus when the previously focused uid isn't in
  the new photo set, so drilling into a /tags category drops the
  cursor on its first tile instead of carrying a stale selection from
  whatever view the user came from.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 21:56:54 +02:00
parent 84e433ff63
commit a72619e3d1
9 changed files with 222 additions and 28 deletions

View File

@@ -113,3 +113,51 @@ export function setFocused(uid: string | null): void {
export function setAnchor(uid: string | null): void {
selection.anchor = uid;
}
/**
* Advance focus to the photo immediately after `excluded` in the current
* order, skipping any uid that's in `excluded`. Falls back to the closest
* non-excluded uid *before* the excluded set when the user is already at
* the tail. Returns `null` when nothing else is left.
*
* Used right after a mutation that removes the focused photo from the
* current view (archive / restore / approve / delete) — calling this
* *before* the photo cache refetches keeps focus stable instead of the
* effect-driven anchor falling back to photo[0].
*/
export function focusAfter(excluded: Iterable<string>): string | null {
const excludedSet = excluded instanceof Set ? excluded : new Set(excluded);
const order = selection.order;
if (order.length === 0) {
setFocused(null);
return null;
}
// Anchor index: prefer current focus, else the first excluded uid we
// can find (covers the case where focus was already null).
let anchorIdx = indexOf(selection.focused);
if (anchorIdx === -1) {
for (let i = 0; i < order.length; i++) {
if (excludedSet.has(order[i])) {
anchorIdx = i;
break;
}
}
}
if (anchorIdx === -1) return null;
for (let i = anchorIdx + 1; i < order.length; i++) {
if (!excludedSet.has(order[i])) {
setFocused(order[i]);
setAnchor(order[i]);
return order[i];
}
}
for (let i = anchorIdx - 1; i >= 0; i--) {
if (!excludedSet.has(order[i])) {
setFocused(order[i]);
setAnchor(order[i]);
return order[i];
}
}
setFocused(null);
return null;
}