docs: move PLAN 2.4 to COMPLETED — interactivity-gap loading guards landed
This commit is contained in:
+44
-10
@@ -6,6 +6,50 @@ Newest entries at the top. Group by phase/wave header (mirroring `PLAN.md` / `CM
|
||||
|
||||
---
|
||||
|
||||
## Phase 2.4 — Interactivity-gap loading guard on dead-during-prerender controls
|
||||
|
||||
**Status:** Fully landed on 2026-06-08 (implementation complete, reviewed and merged to dev).
|
||||
|
||||
Guard controls that are dead during the SSR→interactive handoff window (1–2s on fast loads, 5s+ on cold WASM cache) so they *look* inactive until the Blazor runtime attaches, then re-render into their live form. The listener reaches for **play** first — a play button that looks armed but eats the click reads as "the site is broken," not "the site is loading." This is a credibility/perceived-quality fix on the primary action.
|
||||
|
||||
**Implementation approach:** Extend the existing `RendererInfo.IsInteractive` pattern already established in `PlayStateIcon.razor` and `DeepDrftHero.razor`. Add `Disabled="@(!RendererInfo.IsInteractive)"` (or the HTML equivalent) to unguarded controls during the SSR phase. No global overlay/scrim (rejected — it fights the prerender's purpose and risks colliding with Blazor's `#components-reconnect-modal`); per-control guarding leaves the working parts (plain `<a>` links, idle UI) live. Each control carries its own inline gate — mild duplication over a shared `<InteractivityGate>` wrapper is deliberately accepted (over-engineering for ~4 call sites; would obscure the per-control rendering differences). Consistent with existing patterns.
|
||||
|
||||
**Guarded controls (as implemented):**
|
||||
- **`TrackCard.razor` play `MudFab` (grid + list mode) — HIGHEST PRIORITY.** Disabled during the gap (greyed, non-interactive via MudBlazor's built-in disabled state). Card looks *composed but not-yet-armed*, not alarmed. Re-enables once `RendererInfo.IsInteractive` flips. Note: `/tracks` bridges *data* across the seam via `PersistentComponentState` — but bridging data ≠ wiring handlers; the gap still exists on a cold WASM cache load.
|
||||
- **`TracksView.razor` `MudToggleGroup` (grid/list switch) + `MudPagination`.** Both gated to `Disabled="true"` during the gap. Lower priority than play, but cheap to include in the same pass and visually consistent.
|
||||
- **`SharePopover.razor` (on `TrackDetail`).** The Share `MudIconButton` trigger gated to `Disabled="true"` until interactive; the in-popover copy buttons are moot while the trigger is disabled, so the single guard on the trigger suffices.
|
||||
- **`DeepDrftMenu.razor` "Stream Now" CTA.** Folded `!RendererInfo.IsInteractive` into the existing `disabled="@(...)"` expression (e.g. `disabled="@(_streamLoading || !RendererInfo.IsInteractive)"`) on both desktop and mobile buttons. The label-swap precedent here ("Finding a track…") is the house voice — disabling is the floor.
|
||||
|
||||
**What was deliberately left untouched (mirrors `WASM_SEAMS.md` §2 discipline):**
|
||||
- **Minimized `AudioPlayerBar` dock** — default state shows only `LevelMeterFab`, which is idle (untinted, no animation) until audio plays. Reads correctly during the gap; nothing to guard.
|
||||
- **Expanded `AudioPlayerBar` transport zone** — already routes its play/pause glyph through the guarded `PlayStateIcon`. Already covered by the existing pattern.
|
||||
- **`NowPlaying` / `NowPlayingCard`** — reflect live player state; show "Nothing playing" on both passes on a cold load. No dead control; the player is gesture-gated and intentionally non-persisted.
|
||||
- **Plain `<a href>` links** (track titles → `/track/{key}`, nav links, hero CTAs) — work in static SSR. Out of scope by construction.
|
||||
|
||||
**Coexistence constraint:** This guard targets the *initial* SSR→interactive handoff. It does not duplicate or interfere with Blazor's built-in `#components-reconnect-modal` (dropped-circuit recovery, a different lifecycle event). The two are orthogonal — `RendererInfo.IsInteractive` does not flip back to `false` on a *reconnect*, so the guards correctly stay inactive during a reconnect.
|
||||
|
||||
**Prerequisite:** None. Pure client-side rendering work in `DeepDrftPublic.Client`; no API or data-layer change.
|
||||
|
||||
---
|
||||
|
||||
## LevelMeterFab — Continuous vertical fill animation
|
||||
|
||||
**Status:** Fully landed on 2026-06-08 (feature complete, component + CSS animation, merged to dev).
|
||||
|
||||
Replaced the discrete three-band tint model with a **continuous vertical fill** inside the music-note SVG silhouette. The fill height tracks live audio level bottom-up (0–100%); a fixed three-zone gradient (`linearGradient` with `gradientUnits="userSpaceOnUse"`) renders green (0–60% of note height), yellow (60–85%), and orange (85–100%) zones. The color at the fill line therefore changes naturally as the level rises. The note shape remains always visible as a dim silhouette at 25% opacity; idle (paused/stopped) shows the silhouette alone.
|
||||
|
||||
**Implementation details:**
|
||||
- **C# side (`LevelMeterFab.razor.cs`)**: Removed discrete `_bandClass` field; replaced with continuous `_fillPercent` (0–100). dB → fill % uses a linear map over a −30 to 0 dB window (−30 dB = 0% fill, 0 dB = 100%, −12 dB = 60% / yellow boundary, −4.5 dB = 85% / orange boundary). Smoothing envelope operates on the continuous value (attack-fast / release-slow on dB, then map). Computed properties `FillY` and `FillH` expose the rect geometry to the SVG template.
|
||||
- **SVG (`LevelMeterFab.razor`)**: Two layers — always-on dim silhouette (note path at 25% white) and a clipped fill group (rectangle revealed through the note via `clipPath`, painted with the zone gradient). No color cascade; explicit rgba on silhouette, explicit colors in gradient stops.
|
||||
- **Gradient anchoring**: `linearGradient` with `gradientUnits="userSpaceOnUse"` (not `objectBoundingBox`) — x1="0" y1="24" x2="0" y2="0" (bottom to top in viewBox coordinates). This pins the zones to fixed heights so the fill line always crosses the same colors at the same levels.
|
||||
- **CSS (`LevelMeterFab.razor.css`)**: Removed band-tint color transition (no longer applicable). Geometry attributes `y` and `height` are not CSS-animatable in a reliable way; animation is purely the 30fps C# value updates driven by smoothing envelope. Silhouette remains always-on idle visual when `_fillPercent = 0`.
|
||||
- **Re-render gate**: 0.5% change threshold prevents churn on sub-pixel deltas; renders only on meaningful level swings.
|
||||
- **Idle behavior**: `StopAnimation` resets `_fillPercent = 0` and `_smoothedDb = SilenceFloorDb`, dropping the column and leaving only the dim silhouette.
|
||||
|
||||
Supersedes the earlier discrete-tint `LevelMeterFab` entry from the same component. The new model is load-bearing for real-time level feedback on a commercial dance-music master (−8 to −3 dBFS); the meter "breathes" through the green/yellow zones with peaks reaching orange, rather than holding in one band.
|
||||
|
||||
---
|
||||
|
||||
## Track Gallery View Toggle
|
||||
|
||||
**Status:** Fully landed on 2026-06-08 (feature complete, component + layout + CSS, merged to dev).
|
||||
@@ -71,16 +115,6 @@ Give the track gallery two switchable view modes behind a page-level toggle: **M
|
||||
|
||||
---
|
||||
|
||||
## LevelMeterFab — Reactive audio-level-tint FAB
|
||||
|
||||
**Status:** Fully landed on 2026-06-08 (feature complete, component + integration, merged to dev).
|
||||
|
||||
- **What:** A new `LevelMeterFab` Blazor component that replaces the static music-note FAB in the minimized player dock (`AudioPlayerBar.razor`). Reactively tints the icon based on live audio output level: green (−∞ to −18 dB), yellow (−18 to −6 dB), orange (above −6 dB). When idle (no track, paused, stopped), reverts to static untinted state.
|
||||
- **Why it matters:** The minimized dock is always visible in the UI; adding a live level indicator gives real-time visual feedback on the audio stream's loudness, and the three-band color coding immediately communicates whether the output is quiet, normal, or hot.
|
||||
- **Implementation:** No TypeScript or `AudioInteropService` changes — reuses the existing spectrum callback infrastructure (`StartSpectrumAnimationAsync` / `StopSpectrumAnimationAsync`). The component subscribes to live spectrum buckets at ~30fps, reduces the peak bucket to a reconstructed dB value via the inverse of the spectrum normalization formula, applies attack-fast/release-slow smoothing, and updates the icon color class. CSS transitions on the color (120ms ease-out) smooth the band changes. Follows the identical state-subscription pattern as `SpectrumVisualizer` — observes `IPlayerService.StateChanged` to toggle animation on play and off on pause/stop/track-end.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2.5 — "Stream Now" — random-track instant play
|
||||
|
||||
**Status:** Fully landed on 2026-06-07 (feature complete, endpoints + service methods + menu wiring, merged to dev).
|
||||
|
||||
Reference in New Issue
Block a user