feat(ui): implement premium mobile-first reader toolbar, bottom navigation, and auth ux stabilization (#61)
# Description This Pull Request integrates the premium mobile-first layout enhancements, a responsive, full-bleed Reader Toolbar, and critical authorization flow stabilizations for the NexusReader Blazor application (targeting .NET 10 with Native AOT compatibility). Resolves #62 Resolves #63 Resolves #15 ## Key Changes ### 📱 1. Mobile-First Reader Layout & Toolbar - **Full-Bleed Responsive Layout**: Redesigned `ReaderLayout` to feature a premium mobile-first three-tab bottom navigation system (Chapters, Graph, Assistant) and a glassmorphic floating action button (FAB) for the AI assistant. - **Header & Escaping Routes**: Built `MobileReaderToolbar` with seamless exit paths back to the "Pulpit" (dashboard) and smooth transitions. - **Custom Iconography**: Added the custom `NexusIcon` component supporting dynamic theme styling and responsive layouts without relying on external CSS frameworks. ### 🔐 2. Authentication Flow UX Stabilization - **WASM Transition Hydration**: Implemented `AuthenticationStatePersister` and loading preloaders to eliminate authorization race conditions during Blazor WASM interactive hydration. - **AOT-Compatible JWT Validation**: Integrated a robust, AOT-compatible `JwtTokenValidator` with unit tests (`JwtTokenValidatorTests.cs`) to cleanly parse claims without throwing performance-heavy runtime exceptions. - **Secure Header Propagation**: Standardized token transmission in WASM (`AuthenticationHeaderHandler.cs`) and MAUI Hybrid client layers (`MobileAuthenticationHeaderHandler.cs`), ensuring cookies are correctly propagated. ### 📊 3. D3.js Knowledge Graph & Interaction - **Dynamic Viewport Synchronization**: Refactored `knowledgeGraph.js` to ensure the SVG graph behaves correctly under flexbox containment, handles panel expansion/collapse gracefully, and avoids infinite loop redraws. - **Interaction Hook**: Connected graph node clicks directly to chapter jumps via a new `IReaderInteractionService` abstraction. ### 🏗️ 4. Infrastructure & Central Package Management (CPM) - **Beta Deployment Configuration**: Added `.env.test.template`, `docker-compose.test.yml`, and `appsettings.Test.json` with hardened environment security guards. - **Docker-Compose Cache Optimization**: Maintained CPM consistency during multi-stage Docker builds. ## Verification & Build Results - Run a successful local build check: ```bash dotnet build NexusReader.slnx --no-restore ``` **Status**: Successfully completed with `0` compilation errors. --------- Co-authored-by: Marek Jasiński <jasins.marek@gmail.com> Reviewed-on: #61 Co-authored-by: Antigravity <antigravity@google.com> Co-committed-by: Antigravity <antigravity@google.com>
This commit was merged in pull request #61.
This commit is contained in:
@@ -200,6 +200,9 @@ export function mount(containerId, data, dotNetHelper) {
|
||||
width = container.clientWidth || 400;
|
||||
height = container.clientHeight || 400;
|
||||
|
||||
// Clean up any existing SVG to prevent duplicates
|
||||
container.querySelectorAll("svg").forEach(el => el.remove());
|
||||
|
||||
// Create SVG
|
||||
svgElement = d3.select(container).append("svg")
|
||||
.attr("viewBox", [0, 0, width, height])
|
||||
|
||||
@@ -20,3 +20,43 @@ export function initObserver(dotNetHelper, containerSelector, itemSelector) {
|
||||
|
||||
return observer;
|
||||
}
|
||||
|
||||
export function initScrollListener(dotNetHelper, scrollContainerSelector) {
|
||||
const container = document.querySelector(scrollContainerSelector);
|
||||
if (!container) return null;
|
||||
|
||||
let isThrottled = false;
|
||||
|
||||
const onScroll = () => {
|
||||
if (isThrottled) return;
|
||||
isThrottled = true;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const scrollTop = container.scrollTop;
|
||||
const scrollHeight = container.scrollHeight;
|
||||
const clientHeight = container.clientHeight;
|
||||
|
||||
let percentage = 0;
|
||||
if (scrollHeight > clientHeight) {
|
||||
percentage = Math.round((scrollTop / (scrollHeight - clientHeight)) * 100);
|
||||
}
|
||||
|
||||
// Ensure bounds
|
||||
percentage = Math.max(0, Math.min(100, percentage));
|
||||
|
||||
dotNetHelper.invokeMethodAsync('HandleScrollPercentChanged', percentage);
|
||||
isThrottled = false;
|
||||
});
|
||||
};
|
||||
|
||||
container.addEventListener('scroll', onScroll, { passive: true });
|
||||
|
||||
// Initial calculation after a brief layout delay
|
||||
setTimeout(onScroll, 100);
|
||||
|
||||
return {
|
||||
dispose: () => {
|
||||
container.removeEventListener('scroll', onScroll);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Viewport and scrolling utilities for NexusReader.
|
||||
* Avoids eval() usage, supports CSP, AOT-safety, and prevents memory leaks.
|
||||
*/
|
||||
|
||||
export function isMobileViewport() {
|
||||
return window.innerWidth < 768;
|
||||
}
|
||||
|
||||
export function registerViewportObserver(dotNetHelper) {
|
||||
let currentIsMobile = window.innerWidth < 768;
|
||||
|
||||
const listener = () => {
|
||||
const isMobile = window.innerWidth < 768;
|
||||
if (isMobile !== currentIsMobile) {
|
||||
currentIsMobile = isMobile;
|
||||
dotNetHelper.invokeMethodAsync('OnViewportChanged', isMobile);
|
||||
}
|
||||
};
|
||||
|
||||
// Store listener directly on the JS object wrapper of the DotNetObjectReference for elegant cleanup
|
||||
dotNetHelper._viewportListener = listener;
|
||||
window.addEventListener('resize', listener);
|
||||
}
|
||||
|
||||
export function unregisterViewportObserver(dotNetHelper) {
|
||||
if (dotNetHelper && dotNetHelper._viewportListener) {
|
||||
window.removeEventListener('resize', dotNetHelper._viewportListener);
|
||||
delete dotNetHelper._viewportListener;
|
||||
}
|
||||
}
|
||||
|
||||
export function scrollIntoView(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user