Phase 9 Wave 4: ARCHIVE nav + Cuts/Sessions/Mixes pages + MixWaveformVisualizer

Replaces flat RELEASES/SESSIONS/MIXES nav with ARCHIVE dropdown (PageRoute.Children,
one-level cap, dual-role node). Adds /archive overview, /cuts (AlbumsView + medium
filter; /albums redirects), /sessions + /sessions/{id} (hero-dominant), /mixes +
/mixes/{id} (MixWaveformVisualizer full-page background). Extracts ReleaseDetailScaffold
from TrackDetail (invariant trio). PersistentComponentState bridge on all new pages.
Click-to-seek seam designed on MixWaveformVisualizer (inert until wired).
This commit is contained in:
daniel-c-harvey
2026-06-12 23:05:25 -04:00
parent 5f7eaed112
commit af724ce570
31 changed files with 1334 additions and 122 deletions
+29 -17
View File
@@ -10,21 +10,33 @@
<ul class="dd-nav-links">
@foreach (var navPage in Pages.MenuPages)
{
<li>
<a href="@navPage.Route" class="dd-nav-link">@navPage.Name</a>
</li>
@if (navPage.HasChildren)
{
@* Dual-role node: the parent anchor navigates to its own route on click,
while hover/focus reveals the child dropdown (pure CSS, no JS). *@
<li class="dd-nav-item-parent">
<a href="@navPage.Route" class="dd-nav-link">@navPage.Name</a>
<ul class="dd-nav-dropdown">
@foreach (var child in navPage.Children)
{
<li>
<a href="@child.Route" class="dd-nav-link dd-nav-dropdown-link">@child.Name</a>
</li>
}
</ul>
</li>
}
else
{
<li>
<a href="@navPage.Route" class="dd-nav-link">@navPage.Name</a>
</li>
}
}
</ul>
<div class="dd-nav-actions">
<StreamNowButton ButtonClass="dd-nav-cta" ButtonLabel="Stream Now &#9654;" />
@* <button type="button" *@
@* class="dd-nav-toggle" *@
@* aria-label="Toggle dark mode" *@
@* aria-pressed="@IsDarkMode.ToString().ToLowerInvariant()" *@
@* @onclick="DarkModeToggle"> *@
@* @((MarkupString)DarkLightModeIconSvg) *@
@* </button> *@
</div>
</nav>
</div>
@@ -35,13 +47,6 @@
<a class="dd-nav-brand" href="/">Deep DRFT</a>
<div class="dd-nav-actions">
@* <button type="button" *@
@* class="dd-nav-toggle" *@
@* aria-label="Toggle dark mode" *@
@* aria-pressed="@IsDarkMode.ToString().ToLowerInvariant()" *@
@* @onclick="DarkModeToggle"> *@
@* @((MarkupString)DarkLightModeIconSvg) *@
@* </button> *@
<button type="button"
class="dd-nav-hamburger"
aria-label="Toggle navigation"
@@ -59,6 +64,13 @@
<li>
<a href="@navPage.Route" class="dd-nav-link" @onclick="CloseMobileMenu">@navPage.Name</a>
</li>
@* One-level fan-out: render children as an indented sub-list under the parent link. *@
@foreach (var child in navPage.Children)
{
<li class="dd-nav-mobile-child">
<a href="@child.Route" class="dd-nav-link" @onclick="CloseMobileMenu">@child.Name</a>
</li>
}
}
<li>
<StreamNowButton ButtonClass="dd-nav-cta" ButtonLabel="Stream Now &#9654;" OnStreamStarted="CloseMobileMenu" />
@@ -82,6 +82,57 @@
color: var(--deepdrft-white);
}
/* Dual-role parent node: anchor for the parent route, positioning context for the dropdown. */
.dd-nav-item-parent {
position: relative;
}
/* Hover-revealed child dropdown. Pure CSS — hidden by default, shown when the parent item is
hovered or contains keyboard focus. Sits directly beneath the parent link, frosted to match
the bar. */
.dd-nav-dropdown {
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%) translateY(0.25rem);
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0.75rem;
list-style: none;
margin: 0;
padding: 1rem 1.5rem;
min-width: 9rem;
background: rgba(250, 250, 248, 0.96);
border: 1px solid var(--deepdrft-border);
-webkit-backdrop-filter: blur(18px);
backdrop-filter: blur(18px);
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.18s ease, visibility 0.18s ease;
z-index: 101;
}
.dd-nav-dark .dd-nav-dropdown {
background: rgba(13, 13, 18, 0.95);
}
.dd-nav-item-parent:hover .dd-nav-dropdown,
.dd-nav-item-parent:focus-within .dd-nav-dropdown {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
.dd-nav-dropdown-link {
white-space: nowrap;
}
/* Right-side cluster */
.dd-nav-actions {
display: flex;
@@ -207,6 +258,11 @@
padding: 0.6rem 0;
}
/* Indented child rows under their parent link in the mobile drawer. */
.dd-nav-mobile-child {
padding-left: 1.25rem;
}
.dd-nav-links-mobile ::deep .dd-nav-cta {
margin-top: 0.5rem;
text-align: center;
+20 -6
View File
@@ -7,20 +7,34 @@ public class PageRoute
public string Name { get; set; } = string.Empty;
public string Route { get; set; } = string.Empty;
public string? Icon { get; set; } = null;
// Optional one-level fan-out. A node with children is a dual-role node: its own Route is a
// real destination (desktop click / mobile tap navigate to it), and Children render as a
// hover dropdown on desktop and an indented sub-list on mobile. Depth is capped at one level
// by convention — children are not themselves expected to carry children.
public IReadOnlyList<PageRoute> Children { get; set; } = [];
public bool HasChildren => Children.Count > 0;
}
public static class Pages
{
public static readonly List<PageRoute> MenuPages =
[
new() { Name = "Releases", Route = "/tracks", Icon = Icons.Material.Filled.LibraryMusic },
new() { Name = "Albums", Route = "/albums", Icon = Icons.Material.Filled.Album },
new() { Name = "Genres", Route = "/genres", Icon = Icons.Material.Filled.Category },
new() { Name = "Sessions", Route = "#", Icon = Icons.Material.Filled.Piano }, // TODO: placeholder until Sessions ships
new() { Name = "Mixes", Route = "#", Icon = Icons.Material.Filled.Album }, // TODO: placeholder until Mixes ships
new()
{
Name = "Archive", Route = "/archive", Icon = Icons.Material.Filled.Inventory2,
Children =
[
new() { Name = "Cuts", Route = "/cuts", Icon = Icons.Material.Filled.Album },
new() { Name = "Sessions", Route = "/sessions", Icon = Icons.Material.Filled.Piano },
new() { Name = "Mixes", Route = "/mixes", Icon = Icons.Material.Filled.GraphicEq },
],
},
new() { Name = "Genres", Route = "/genres", Icon = Icons.Material.Filled.Category },
];
public static readonly List<PageRoute> AllPages =
public static readonly List<PageRoute> AllPages =
new List<PageRoute>
{
new() { Name = "Home", Route = "/", Icon = Icons.Material.Filled.Home }