diff --git a/DeepDrftWeb.Client/Layout/DeepDrftMenu.razor b/DeepDrftWeb.Client/Layout/DeepDrftMenu.razor index 93381fb..f5310a2 100644 --- a/DeepDrftWeb.Client/Layout/DeepDrftMenu.razor +++ b/DeepDrftWeb.Client/Layout/DeepDrftMenu.razor @@ -1,78 +1,80 @@ -@using DeepDrftWeb.Client.Controls +@using DeepDrftWeb.Client.Common @using DeepDrftWeb.Client.Services +@implements IAsyncDisposable - -
- @if (_isDesktop) - { -
- - - - - - -
Deep DRFT
-
-
-
-
- -
- - @foreach (PageRoute thePage in Pages.AllPages) - { - @thePage.Name - } - -
- } - else - { -
- - - - - - - - -
Deep DRFT
-
-
- - @foreach (PageRoute route in Pages.AllPages) - { - - - - - @route.Name - - - - } - -
-
-
- } - -
- +
- + } + else + { +
+ + +
+ + @if (_mobileMenuOpen) + { + + } + } + @code { [Inject] public required IBrowserViewportService BrowserViewportService { get; set; } [Inject] public required DarkModeCookieService DarkModeCookieService { get; set; } + + // Elevation is vestigial under the frosted-glass design but kept on the parameter + // surface so MainLayout's call site stays intact. [Parameter] public int Elevation { get; set; } [Parameter] public required bool IsDarkMode { get; set; } [Parameter] public required EventCallback IsDarkModeChanged { get; set; } - + private bool _isDesktop = true; + private bool _mobileMenuOpen; private Guid _viewportSubscriptionId; protected override async Task OnInitializedAsync() @@ -85,34 +87,41 @@ { if (firstRender) { - var breakpoint = await BrowserViewportService.GetCurrentBreakpointAsync(); - _isDesktop = breakpoint >= Breakpoint.Sm; - _viewportSubscriptionId = Guid.NewGuid(); await BrowserViewportService.SubscribeAsync( _viewportSubscriptionId, args => { _isDesktop = args.Breakpoint >= Breakpoint.Sm; + if (_isDesktop) + { + _mobileMenuOpen = false; + } InvokeAsync(StateHasChanged); }, new ResizeOptions { NotifyOnBreakpointOnly = true }, fireImmediately: true); - - StateHasChanged(); } } - - private string DarkLightModeButtonIcon => IsDarkMode switch + + public async ValueTask DisposeAsync() { - true => DDIcons.GasLampLit, - false => DDIcons.GasLamp, - }; - + if (_viewportSubscriptionId != Guid.Empty) + await BrowserViewportService.UnsubscribeAsync(_viewportSubscriptionId); + } + + private string NavClass => IsDarkMode ? "dd-nav dd-nav-dark" : "dd-nav dd-nav-light"; + + private string DarkLightModeIconSvg => IsDarkMode ? DDIcons.GasLampLit : DDIcons.GasLamp; + private async Task DarkModeToggle() { IsDarkMode = !IsDarkMode; await DarkModeCookieService.SetDarkModeAsync(IsDarkMode); await IsDarkModeChanged.InvokeAsync(IsDarkMode); } + + private void ToggleMobileMenu() => _mobileMenuOpen = !_mobileMenuOpen; + + private void CloseMobileMenu() => _mobileMenuOpen = false; } diff --git a/DeepDrftWeb.Client/Layout/DeepDrftMenu.razor.css b/DeepDrftWeb.Client/Layout/DeepDrftMenu.razor.css new file mode 100644 index 0000000..74b80fd --- /dev/null +++ b/DeepDrftWeb.Client/Layout/DeepDrftMenu.razor.css @@ -0,0 +1,220 @@ +/* Frosted-glass top navigation. + Scoped to DeepDrftMenu.razor only — global styles live in deepdrft-styles.css. + The CSS variables consumed here (--deepdrft-navy etc.) are theme-aware and + defined globally; the dd-nav-dark modifier overrides the backdrop RGBA since + it must be hard-coded for the blur fallback. */ + +.dd-nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; + + display: flex; + align-items: center; + justify-content: space-between; + gap: 2rem; + + padding: 1.5rem 3rem; + + border-bottom: 1px solid var(--deepdrft-border); + box-shadow: none; + + -webkit-backdrop-filter: blur(18px); + backdrop-filter: blur(18px); +} + +.dd-nav-light { + background: rgba(250, 250, 248, 0.88); +} + +.dd-nav-dark { + background: rgba(13, 13, 18, 0.85); +} + +/* Brand */ +.dd-nav-brand { + font-family: var(--deepdrft-font-mono); + font-size: 0.75rem; + letter-spacing: 0.22em; + text-transform: uppercase; + color: var(--deepdrft-navy); + text-decoration: none; + white-space: nowrap; +} + +.dd-nav-dark .dd-nav-brand { + color: var(--deepdrft-white); +} + +/* Centred link list */ +.dd-nav-links { + display: flex; + align-items: center; + gap: 2.5rem; + + flex: 1; + justify-content: center; + + list-style: none; + margin: 0; + padding: 0; +} + +.dd-nav-link { + font-family: var(--deepdrft-font-mono); + font-size: 0.68rem; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--deepdrft-muted); + text-decoration: none; + transition: color 0.25s ease; +} + +.dd-nav-link:hover, +.dd-nav-link:focus-visible { + color: var(--deepdrft-navy); +} + +.dd-nav-dark .dd-nav-link:hover, +.dd-nav-dark .dd-nav-link:focus-visible { + color: var(--deepdrft-white); +} + +/* Right-side cluster */ +.dd-nav-actions { + display: flex; + align-items: center; + gap: 1rem; +} + +/* Stream Now CTA — square pill, navy on warm white */ +.dd-nav-cta { + display: inline-block; + font-family: var(--deepdrft-font-mono); + font-size: 0.68rem; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--deepdrft-white); + background: var(--deepdrft-navy); + padding: 0.6rem 1.4rem; + border: none; + border-radius: 0; + text-decoration: none; + cursor: pointer; + transition: background 0.25s ease; +} + +.dd-nav-cta:hover, +.dd-nav-cta:focus-visible { + background: var(--deepdrft-green); +} + +.dd-nav-dark .dd-nav-cta { + color: var(--deepdrft-white); + background: var(--deepdrft-primary); +} + +.dd-nav-dark .dd-nav-cta:hover, +.dd-nav-dark .dd-nav-cta:focus-visible { + background: var(--deepdrft-senary); +} + +/* Dark-mode toggle (gas lamp) */ +.dd-nav-toggle { + width: 2rem; + height: 2rem; + padding: 0; + background: transparent; + border: none; + cursor: pointer; + color: var(--deepdrft-navy); + display: inline-flex; + align-items: center; + justify-content: center; + transition: color 0.25s ease; +} + +.dd-nav-toggle:hover, +.dd-nav-toggle:focus-visible { + color: var(--deepdrft-green-accent); +} + +.dd-nav-toggle::deep svg { + width: 1.25rem; + height: 1.25rem; +} + +.dd-nav-dark .dd-nav-toggle { + color: var(--deepdrft-white); +} + +.dd-nav-dark .dd-nav-toggle:hover, +.dd-nav-dark .dd-nav-toggle:focus-visible { + color: var(--deepdrft-tertiary); +} + +/* Mobile hamburger */ +.dd-nav-hamburger { + width: 2rem; + height: 2rem; + padding: 0; + background: transparent; + border: none; + cursor: pointer; + display: inline-flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 4px; +} + +.dd-nav-hamburger span { + display: block; + width: 1.25rem; + height: 1.5px; + background: var(--deepdrft-navy); +} + +.dd-nav-dark .dd-nav-hamburger span { + background: var(--deepdrft-white); +} + +/* Mobile dropdown panel — absolute-positioned beneath the bar */ +.dd-nav-links-mobile { + position: absolute; + top: 100%; + left: 0; + right: 0; + + flex-direction: column; + align-items: stretch; + gap: 0; + padding: 0.75rem 1.5rem 1.25rem; + + background: rgba(250, 250, 248, 0.96); + border-bottom: 1px solid var(--deepdrft-border); + -webkit-backdrop-filter: blur(18px); + backdrop-filter: blur(18px); +} + +.dd-nav-dark .dd-nav-links-mobile { + background: rgba(13, 13, 18, 0.95); +} + +.dd-nav-links-mobile li { + padding: 0.6rem 0; +} + +.dd-nav-links-mobile .dd-nav-cta { + margin-top: 0.5rem; + text-align: center; +} + +/* Mobile padding — give the nav room to breathe without crowding */ +@media (max-width: 599px) { + .dd-nav { + padding: 1rem 1.25rem; + } +} diff --git a/DeepDrftWeb.Client/Layout/Pages.cs b/DeepDrftWeb.Client/Layout/Pages.cs index 81f2a5d..c56a34f 100644 --- a/DeepDrftWeb.Client/Layout/Pages.cs +++ b/DeepDrftWeb.Client/Layout/Pages.cs @@ -13,7 +13,10 @@ public static class Pages { public static readonly List MenuPages = [ - new() { Name = "Track Gallery", Route = "/tracks", Icon = Icons.Material.Filled.LibraryMusic } + new() { Name = "Listen", Route = "/tracks", Icon = Icons.Material.Filled.LibraryMusic }, + new() { Name = "Sessions", Route = "#", Icon = Icons.Material.Filled.Album }, // TODO: placeholder until Sessions ships + new() { Name = "Archive", Route = "#", Icon = Icons.Material.Filled.FolderOpen }, // TODO: placeholder until Archive ships + new() { Name = "About", Route = "#", Icon = Icons.Material.Filled.Info }, // TODO: placeholder until About ships ]; public static readonly List AllPages =