Add tablet navigation drawer to shell
This commit is contained in:
@@ -46,6 +46,7 @@ It is intentionally implementation-focused:
|
|||||||
| 2026-03-21 | P1.6 | Completed | Added explicit shell slots for nav, omnibox, shortcuts, and utilities; switched shell navigation to `Play`, `Tables`, `Curation`, and `Tools`; and wired the first live theme control into the shell. |
|
| 2026-03-21 | P1.6 | Completed | Added explicit shell slots for nav, omnibox, shortcuts, and utilities; switched shell navigation to `Play`, `Tables`, `Curation`, and `Tools`; and wired the first live theme control into the shell. |
|
||||||
| 2026-03-21 | P1.7 | Completed | Added a shell-level skip link and tightened the top-level header, navigation, and main landmarks around the new shell structure. |
|
| 2026-03-21 | P1.7 | Completed | Added a shell-level skip link and tightened the top-level header, navigation, and main landmarks around the new shell structure. |
|
||||||
| 2026-03-21 | P1.8 | Completed | Introduced a cooler tooling emphasis for `Tools`, diagnostics, and API surfaces, and styled the `Tools` destination as distinct without splitting the shell. |
|
| 2026-03-21 | P1.8 | Completed | Introduced a cooler tooling emphasis for `Tools`, diagnostics, and API surfaces, and styled the `Tools` destination as distinct without splitting the shell. |
|
||||||
|
| 2026-03-21 | Post-P1 fix 1 | Completed | Closed the 768px-1023px navigation gap by adding a shell hamburger menu and drawer so primary navigation never disappears at tablet widths. |
|
||||||
|
|
||||||
### Lessons Learned
|
### Lessons Learned
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ It is intentionally implementation-focused:
|
|||||||
- Once a Razor component exposes multiple named `RenderFragment` parameters, the page body must be passed explicitly through `<ChildContent>`. That pattern is now the baseline for shell composition here.
|
- Once a Razor component exposes multiple named `RenderFragment` parameters, the page body must be passed explicitly through `<ChildContent>`. That pattern is now the baseline for shell composition here.
|
||||||
- Accessibility work is cheaper when the shell owns the landmarks. Adding skip links and nav/main structure at the shell layer avoids repeating that work page by page.
|
- Accessibility work is cheaper when the shell owns the landmarks. Adding skip links and nav/main structure at the shell layer avoids repeating that work page by page.
|
||||||
- Tooling can feel distinct through cooler surfaces and labeling alone. A separate app shell is unnecessary and would undermine the shared-product goal.
|
- Tooling can feel distinct through cooler surfaces and labeling alone. A separate app shell is unnecessary and would undermine the shared-product goal.
|
||||||
|
- Responsive shell design needs an explicit tablet state, not just desktop and phone states. The original breakpoints left a navigation dead zone between the top nav and bottom nav layouts.
|
||||||
|
|
||||||
## Target Outcomes
|
## Target Outcomes
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
@implements IDisposable
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
|
||||||
<div class="app-shell">
|
<div class="app-shell">
|
||||||
<a class="skip-link" href="#app-main">Skip to main content</a>
|
<a class="skip-link" href="#app-main">Skip to main content</a>
|
||||||
|
|
||||||
@@ -39,6 +42,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="app-shell-header-actions">
|
<div class="app-shell-header-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="app-shell-menu-toggle"
|
||||||
|
aria-expanded="@isNavMenuOpen"
|
||||||
|
aria-controls="app-shell-drawer"
|
||||||
|
@onclick="ToggleNavMenu">
|
||||||
|
<span class="app-shell-menu-toggle-bar"></span>
|
||||||
|
<span class="app-shell-menu-toggle-bar"></span>
|
||||||
|
<span class="app-shell-menu-toggle-bar"></span>
|
||||||
|
<span class="visually-hidden">Toggle navigation</span>
|
||||||
|
</button>
|
||||||
@UtilityContent
|
@UtilityContent
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,6 +71,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
@if (isNavMenuOpen)
|
||||||
|
{
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="app-shell-drawer-backdrop"
|
||||||
|
aria-label="Close navigation"
|
||||||
|
@onclick="CloseNavMenu"></button>
|
||||||
|
|
||||||
|
<aside id="app-shell-drawer" class="app-shell-drawer" aria-label="Primary navigation">
|
||||||
|
<div class="app-shell-drawer-header">
|
||||||
|
<strong>Navigate</strong>
|
||||||
|
<button type="button" class="app-shell-drawer-close" @onclick="CloseNavMenu">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ShellPrimaryNav />
|
||||||
|
</aside>
|
||||||
|
}
|
||||||
|
|
||||||
<nav class="app-shell-mobile-nav" aria-label="Primary">
|
<nav class="app-shell-mobile-nav" aria-label="Primary">
|
||||||
<ShellPrimaryNav IsBottomNav="true" />
|
<ShellPrimaryNav IsBottomNav="true" />
|
||||||
</nav>
|
</nav>
|
||||||
@@ -80,4 +114,32 @@
|
|||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public RenderFragment? UtilityContent { get; set; }
|
public RenderFragment? UtilityContent { get; set; }
|
||||||
|
|
||||||
|
private bool isNavMenuOpen;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
NavigationManager.LocationChanged += HandleLocationChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleNavMenu()
|
||||||
|
{
|
||||||
|
isNavMenuOpen = !isNavMenuOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CloseNavMenu()
|
||||||
|
{
|
||||||
|
isNavMenuOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
|
||||||
|
{
|
||||||
|
isNavMenuOpen = false;
|
||||||
|
_ = InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
NavigationManager.LocationChanged -= HandleLocationChanged;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,28 @@
|
|||||||
min-height: 2.75rem;
|
min-height: 2.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-shell-menu-toggle {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.24rem;
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: color-mix(in srgb, var(--surface-2) 84%, transparent);
|
||||||
|
color: var(--text-primary);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell-menu-toggle-bar {
|
||||||
|
width: 1rem;
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
.app-shell-shortcuts {
|
.app-shell-shortcuts {
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
}
|
}
|
||||||
@@ -128,6 +150,55 @@
|
|||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-shell-drawer-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 45;
|
||||||
|
border: none;
|
||||||
|
background: var(--bg-overlay);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell-drawer {
|
||||||
|
position: fixed;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
z-index: 50;
|
||||||
|
width: min(24rem, calc(100vw - 2rem));
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: 24px;
|
||||||
|
background: color-mix(in srgb, var(--bg-elevated) 94%, transparent);
|
||||||
|
box-shadow: var(--shadow-2);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell-drawer .shell-primary-nav {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell-drawer .shell-primary-nav-link {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell-drawer-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell-drawer-close {
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: color-mix(in srgb, var(--surface-2) 84%, transparent);
|
||||||
|
color: var(--text-primary);
|
||||||
|
min-height: 2.5rem;
|
||||||
|
padding: 0.45rem 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
.app-shell-header {
|
.app-shell-header {
|
||||||
padding: 0.65rem 0.75rem 0;
|
padding: 0.65rem 0.75rem 0;
|
||||||
@@ -170,4 +241,15 @@
|
|||||||
.app-shell-header-nav {
|
.app-shell-header-nav {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-shell-menu-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.app-shell-drawer,
|
||||||
|
.app-shell-drawer-backdrop {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user