Skip to content

feat(detail): re-land thumbnail strip + close transition (#518) and add the WebGL-context LRU#520

Merged
Zheaoli merged 3 commits into
mainfrom
feat/detail-viewer-lru
Jun 10, 2026
Merged

feat(detail): re-land thumbnail strip + close transition (#518) and add the WebGL-context LRU#520
Zheaoli merged 3 commits into
mainfrom
feat/detail-viewer-lru

Conversation

@Zheaoli

@Zheaoli Zheaoli commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

⚠️ Also re-lands #518 — its content is missing from main

PR #518 (thumbnail strip + fade-out close transition) shows as merged, but its content is not in main: it was a stacked PR whose base was feat/detail-transition-phase2, and #517 → main landed before #518 reached main, so #518 merged into the (already-merged, since-deleted) phase-2 branch and got orphaned. Confirmed: the strip/close commits are not ancestors of origin/main.

So this PR against main contains both the orphaned #518 work and the new LRU, to get everything onto the main line in one clean merge (no re-stacking, which is what caused the orphaning):

  1. Bottom thumbnail strip (was feat(detail): bottom thumbnail strip + fade-out close transition #518) — Review-approved.
  2. Fade-out close transition (was feat(detail): bottom thumbnail strip + fade-out close transition #518) — Review-approved.
  3. WebGL-context LRU (new) — the cap-3 zoomed-viewer LRU.

If you'd rather split, I can land 1+2 separately first; but a single PR to main avoids another stacked-merge ordering trap.

The LRU (the new part)

Hard-caps live WebGL zoom contexts at 3, on top of the ±loadRadius unmount that already bounds them.

Safety / verification

  • No new WebGL-context path; only tightens the bound (≤5 → ≤3). ProgressiveImage visibility internals untouched (one added gate).
  • tsc clean. Static-only here (no GPU/dev). Device round: zoom N photos → live WebGLRenderingContext ≤3, created == destroyed across swipe/open-close, re-zoom of an evicted photo doesn't crash.

🤖 Generated with Claude Code

Zheaoli and others added 3 commits June 10, 2026 08:30
A horizontally scrollable strip of the album window sits at the bottom of the
detail view: the active photo is ringed and auto-scrolled to center, and
clicking a thumbnail jumps the carousel to it (via embla scrollTo, so it shares
the same settle → index/URL sync). Hidden for single-photo views and while the
zoom viewer is open.

Reuses the windowed `photos` the carousel already holds and renders only plain
<img>/blurhash thumbnails — no extra fetches and no WebGL, so the bounded
single-viewer model is untouched.

Phase 3 (1/2): thumbnail strip. Mobile swipe-down dismiss + close transition next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closing the detail view now fades out over 0.2s and then navigates, instead of
hard-cutting back to the grid. handleClose sets a `closing` flag that drives the
root's opacity to 0; navigation runs in onAnimationComplete.

Opacity-only (no transform) so it can't create a containing block for the
fixed-position zoom viewer, and the fade only defers the unmount — once it
navigates, the modal unmounts and ProgressiveImage's destroy/loseContext fires
normally, so the viewer teardown is never stalled.

Phase 3 (2/2): close transition.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-gate)

Hard-caps the number of live WebGL zoom contexts at 3, on top of the
±loadRadius unmount that already bounds them, so a zoom-heavy session can never
approach the browser's ~16-context limit.

- preview-image keeps a small LRU (cap 3) of recently-zoomed photo ids. Opening
  zoom bumps the photo to the front; one that falls off the tail is dropped.
- Each carousel slide's ProgressiveImage gets keepViewerMounted = the photo is
  in the LRU. The current photo is always the most-recently-opened (front), so
  it is never evicted.
- ProgressiveImage gates its kept-mounted viewer on
  `hasOpenedFullScreen && keepViewerMounted !== false`. Eviction simply
  *unmounts* the viewer, which runs WebGLImageViewer's existing
  destroy-on-unmount (loseContext) and resets its internal state — reusing the
  verified #510 lifecycle (full unmount → destroy → remount-fresh) rather than
  imperatively destroying a still-mounted engine (which would strand
  isInitialized over a dead context and re-trigger the #510 crash). The slide's
  preview variant <img> is untouched; only the GL context is released.

Composes with the engine's isDestroyed re-entry guard (#519): a viewer can be
torn down by either LRU eviction or the ±loadRadius unmount, and the guard makes
the resulting double/StrictMode destroy a safe no-op.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
picimpact Ready Ready Preview, Comment Jun 10, 2026 12:32am

@Zheaoli Zheaoli merged commit 850c57b into main Jun 10, 2026
6 checks passed
@Zheaoli Zheaoli deleted the feat/detail-viewer-lru branch June 10, 2026 00:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant