Phase 2C WIP: PreviewOverlay modal + root integration (reel centering bug)

Working:
- PreviewOverlay.svelte mounted at app root; opens over a fixture manifest
  (window.__previewFixture(n) in dev), inert background, player bar hidden,
  focus trap, keyboard nav, F3 rail, T3 swaps, R1b capsule, accent extraction.
- previewSession.svelte.js: store + service wiring (production + fixture).
- Root integration in App.svelte; revision-gateway bumps + notePlaybackGeneration
  threaded through state.svelte.js player entry points; api.win guarded so plain
  vite browser mounts.
- Launcher on the artist page builds a real artist manifest and opens the modal
  with a play-full starter.
- 70 JS tests still green.

Two bugs found and fixed during bring-up:
- effect_update_depth_exceeded: bleed-crossfade and bump effects read AND wrote
  the same $state; wrapped writes in untrack(). This loop was breaking Svelte's
  scheduler globally (even ui.* stopped reacting).
- accentFromImage now sets crossOrigin for remote covers.

KNOWN BROKEN (handoff): reel vertical centering. The active card stays centred
at index 0 but drifts by one stride per step. Root cause isolated — see
TODO-Feature-Docs/scrollable-track-previews/HANDOFF-PHASE2C.md.
This commit is contained in:
KeelrLeek committed 2026-07-22 19:26:41 -05:00
1 parent 8cb3cd593b
commit d051f0db28
7 files changed
+1492 -16

No files matched your search

+27 -3
View File
@@ -33,6 +33,8 @@
} from "../lib/state.svelte.js";
import * as api from "../lib/api.js";
import { accentFromImage } from "../lib/color.js";
import { buildArtistManifest } from "../lib/preview/manifest.js";
import { openPreview } from "../lib/preview/previewSession.svelte.js";
import { traceToSvg, svgDataUrl } from "../lib/trace.js";
import Icon from "../components/Icon.svelte";
import HeroGpuCanvas from "../components/HeroGpuCanvas.svelte";
@@ -537,8 +539,6 @@
// state cannot occur here: the whole action row is gated on
// popularTracks.length, so by the time this control exists the tracks (and
// therefore their preview fields) have landed.
// TODO(previews): openPreviews() opens the scrollable preview modal —
// TODO-Feature-Docs/scrollable-track-previews. Placement-only for now.
const previewableCount = $derived(allPopular.filter((track) => track.preview).length);
const previewState = $derived(
previewableCount === 0
@@ -554,7 +554,31 @@
? `Preview tracks — ${previewableCount} of ${allPopular.length} available`
: "Preview tracks",
);
function openPreviews() {}
/** Open the preview modal over the COMPLETE ranked top-track payload —
* never just the rendered rows (design §13, artist adapter). The manifest
* is a snapshot: later staged-page rebuilds cannot rewrite an open
* session. Play-full hands the active item to the ordinary resolver
* pipeline via the same path the Popular rows use. */
function openPreviews() {
if (!page?.popular_tracks?.length) return;
const snapshotTracks = page.popular_tracks.slice(0, 10);
const byProviderId = new Map(snapshotTracks.map((t) => [String(t.id), t]));
const manifest = buildArtistManifest({
artist: {
id: page.artist.id,
name: page.artist.name,
picture: page.artist.picture ?? null,
},
popularTracks: snapshotTracks,
createdAtMs: Date.now(),
});
void openPreview(manifest, {
playFull: (item) => {
const track = byProviderId.get(item.sources[0]?.providerTrackId);
if (track) playPopular(track);
},
});
}
// Instant-shell seed: the search portrait the user clicked, shown as a
// heavily-blurred colour field behind the hero until the real photo