feat(detail): re-land thumbnail strip + close transition (#518) and add the WebGL-context LRU#520
Merged
Merged
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This was referenced Jun 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 → mainlanded 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 oforigin/main.So this PR against
maincontains 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):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.
preview-imagekeeps a cap-3 LRU of recently-zoomed photo ids; opening zoom bumps to front, the tail is dropped. The current photo is always front → never evicted.ProgressiveImagegetskeepViewerMounted= in-LRU. Its kept-mounted viewer is gated onhasOpenedFullScreen && keepViewerMounted !== false. Eviction unmounts the viewer → runsWebGLImageViewer's existing destroy-on-unmount (loseContext) → resets internal state. This reuses the verified fix(viewer): keep zoom viewer mounted + CSS-toggle (restore context reuse/state, follow-up to #509) #510 lifecycle (full unmount → destroy → remount-fresh) instead of imperatively destroying a still-mounted engine (which would strandisInitializedover a dead context and re-trigger fix(viewer): keep zoom viewer mounted + CSS-toggle (restore context reuse/state, follow-up to #509) #510 — the approach Review rejected). The slide's preview variant<img>is untouched; only the GL context is freed.isDestroyedguard, now in main): eviction-unmount and ±loadRadius-unmount can each destroy a viewer; the guard makes the double / StrictMode destroy a safe no-op.Safety / verification
visibilityinternals untouched (one added gate).tscclean. Static-only here (no GPU/dev). Device round: zoom N photos → liveWebGLRenderingContext≤3, created == destroyed across swipe/open-close, re-zoom of an evicted photo doesn't crash.🤖 Generated with Claude Code