feat(ui): implement premium mobile-first reader layout with three-tab bottom navigation and assistant FAB (#57)
This pull request delivers a comprehensive mobile-first user experience overhaul for the NexusReader SaaS platform, specifically optimizing the Reader Canvas, D3.js Knowledge Graph representation, Dashboard card grid layout, and the application-wide navigation shell on mobile viewports (< 768px). ### Key Enhancements: 1. **Interactive Three-Tab Bottom Navigation**: Added premium, frosted glassy bottom-bar for mobile viewports to switch between standard reading, D3.js graph visual workspace, and structural concept reviews/quizzes. 2. **Contextual Floating Action Button (FAB)**: Introduced the AI Assistant FAB on mobile canvas layout with responsive animation, state-synchronization to trigger corresponding quiz views, and pulsing badge notification when new quizzes are dynamically generated. 3. **Adaptive D3.js Simulation & Rendering**: Integrated `setMobileMode(isMobile)` logic inside the D3 simulation engine, optimizing forces, rendering compact glyph pills, and installing auto-resize observers. 4. **Architectural & Native AOT Cleanliness**: Clean separation of layouts, fully scoped CSS configurations, functional-safe event orchestration inside `IReaderInteractionService`, and zero compiler errors. --------- Co-authored-by: Marek Jasiński <jasins.marek@gmail.com> Reviewed-on: #57 Co-authored-by: Antigravity <antigravity@google.com> Co-committed-by: Antigravity <antigravity@google.com>
This commit was merged in pull request #57.
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
@inject Microsoft.Extensions.Logging.ILogger<ReaderLayout> Logger
|
||||
@implements IDisposable
|
||||
|
||||
<div class="app-container @_platformClass @(FocusMode.IsFocusModeActive ? "focus-mode-active" : "")">
|
||||
<div class="app-container @_platformClass @(FocusMode.IsFocusModeActive ? "focus-mode-active" : "") @($"active-mobile-tab-{_activeMobileTab.ToString().ToLower()}")">
|
||||
<div class="reader-pane">
|
||||
<main>
|
||||
@Body
|
||||
@@ -30,108 +30,221 @@
|
||||
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<div class="resizer" id="sidebar-resizer"></div>
|
||||
@if (!_isMobile)
|
||||
{
|
||||
<div class="resizer" id="sidebar-resizer"></div>
|
||||
|
||||
<div class="intelligence-sidebar">
|
||||
<IntelligenceToolbar />
|
||||
<div class="intelligence-content">
|
||||
<div class="intelligence-header">
|
||||
<div class="ai-title">
|
||||
<NexusIcon Name="robot" Size="20"
|
||||
Class="@($"neon-glow {(QuizService.HasNewQuiz ? "quiz-available" : "")}")" />
|
||||
<span>Asystent AI</span>
|
||||
<div class="intelligence-sidebar">
|
||||
<IntelligenceToolbar />
|
||||
<div class="intelligence-content">
|
||||
<div class="intelligence-header">
|
||||
<div class="ai-title">
|
||||
<NexusIcon Name="robot" Size="20"
|
||||
Class="@($"neon-glow {(QuizService.HasNewQuiz ? "quiz-available" : "")}")" />
|
||||
<span>Asystent AI</span>
|
||||
</div>
|
||||
<button class="close-btn">×</button>
|
||||
</div>
|
||||
<button class="close-btn">×</button>
|
||||
</div>
|
||||
|
||||
@if (_activeTab == SidebarTab.Knowledge)
|
||||
{
|
||||
<div class="intelligence-scroll-area stacked-layout">
|
||||
@if (!_isMobile)
|
||||
{
|
||||
@if (_activeTab == SidebarTab.Knowledge)
|
||||
{
|
||||
<div class="intelligence-scroll-area stacked-layout">
|
||||
<div class="visual-workspace">
|
||||
<KnowledgeGraph />
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="contextual-intelligence-panel">
|
||||
<div class="panel-header">
|
||||
<NexusIcon Name="brain" Size="18" Class="neon-accent-icon" />
|
||||
<span class="panel-title">Contextual Intelligence Panel</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
@if (_selectedNode != null)
|
||||
{
|
||||
<div class="node-details">
|
||||
<div class="node-header-section">
|
||||
<span class="node-group-badge @(_selectedNode.Group.ToLower())">@(_selectedNode.Group.ToUpper())</span>
|
||||
<h3 class="node-label">@_selectedNode.Label</h3>
|
||||
|
||||
<div class="contextual-intelligence-panel">
|
||||
<div class="panel-header">
|
||||
<NexusIcon Name="brain" Size="18" Class="neon-accent-icon" />
|
||||
<span class="panel-title">Contextual Intelligence Panel</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
@if (_selectedNode != null)
|
||||
{
|
||||
<div class="node-details">
|
||||
<div class="node-header-section">
|
||||
<span class="node-group-badge @(_selectedNode.Group.ToLower())">@(_selectedNode.Group.ToUpper())</span>
|
||||
<h3 class="node-label">@_selectedNode.Label</h3>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(_selectedNode.Description))
|
||||
{
|
||||
<div class="detail-section">
|
||||
<p class="node-description">@_selectedNode.Description</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(_selectedNode.Summary))
|
||||
{
|
||||
<div class="detail-section summary-section">
|
||||
<h4 class="section-title neon-sub-header">Podsumowanie</h4>
|
||||
<p class="node-summary">@_selectedNode.Summary</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (_selectedNode.KeyTerms != null && _selectedNode.KeyTerms.Any())
|
||||
{
|
||||
<div class="detail-section key-terms-section">
|
||||
<h4 class="section-title neon-sub-header">Kluczowe Pojęcia</h4>
|
||||
<ul class="key-terms-list">
|
||||
@foreach (var term in _selectedNode.KeyTerms)
|
||||
{
|
||||
<li class="key-term-item">
|
||||
<span class="term-bullet">•</span>
|
||||
<span class="term-text">@term</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(_selectedNode.Description))
|
||||
{
|
||||
<div class="detail-section">
|
||||
<p class="node-description">@_selectedNode.Description</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(_selectedNode.Summary))
|
||||
{
|
||||
<div class="detail-section summary-section">
|
||||
<h4 class="section-title neon-sub-header">Podsumowanie</h4>
|
||||
<p class="node-summary">@_selectedNode.Summary</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (_selectedNode.KeyTerms != null && _selectedNode.KeyTerms.Any())
|
||||
{
|
||||
<div class="detail-section key-terms-section">
|
||||
<h4 class="section-title neon-sub-header">Kluczowe Pojęcia</h4>
|
||||
<ul class="key-terms-list">
|
||||
@foreach (var term in _selectedNode.KeyTerms)
|
||||
{
|
||||
<li class="key-term-item">
|
||||
<span class="term-bullet">•</span>
|
||||
<span class="term-text">@term</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="no-node-selected">
|
||||
<div class="placeholder-glow"></div>
|
||||
<p class="placeholder-text">Wybierz węzeł na wykresie, aby wyświetlić szczegóły architektoniczne.</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="no-node-selected">
|
||||
<div class="placeholder-glow"></div>
|
||||
<p class="placeholder-text">Wybierz węzeł na wykresie, aby wyświetlić szczegóły architektoniczne.</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<button class="open-quiz-btn neon-glow-btn @(QuizService.HasNewQuiz ? "quiz-pulse-btn" : "")" @onclick="() => SetActiveTab(SidebarTab.Quiz)">
|
||||
<NexusIcon Name="quiz" Size="18" />
|
||||
<span>OPEN KNOWLEDGE QUIZ</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="intelligence-scroll-area quiz-layout">
|
||||
<div class="quiz-nav">
|
||||
<button class="back-to-graph-btn" @onclick="() => SetActiveTab(SidebarTab.Knowledge)">
|
||||
<NexusIcon Name="arrow-left" Size="16" />
|
||||
<span>← Powrót do wykresu</span>
|
||||
<div class="sidebar-footer">
|
||||
<button class="open-quiz-btn neon-glow-btn @(QuizService.HasNewQuiz ? "quiz-pulse-btn" : "")" @onclick="() => SetActiveTab(SidebarTab.Quiz)">
|
||||
<NexusIcon Name="quiz" Size="18" />
|
||||
<span>OPEN KNOWLEDGE QUIZ</span>
|
||||
</button>
|
||||
</div>
|
||||
<KnowledgeCheck />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="intelligence-scroll-area quiz-layout">
|
||||
<div class="quiz-nav">
|
||||
<button class="back-to-graph-btn" @onclick="() => SetActiveTab(SidebarTab.Knowledge)">
|
||||
<NexusIcon Name="arrow-left" Size="16" />
|
||||
<span>← Powrót do wykresu</span>
|
||||
</button>
|
||||
</div>
|
||||
<KnowledgeCheck />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Mobile full-bleed containers mapped to bottom tab navigation -->
|
||||
<div class="nexus-mobile-reader-tabs">
|
||||
<!-- Tab 2: Graph -->
|
||||
<div class="nexus-mobile-tab-content graph-tab @(_activeMobileTab == MobileReaderTab.Graph ? "active" : "")">
|
||||
<KnowledgeGraph />
|
||||
</div>
|
||||
|
||||
<!-- Tab 3: Insight (Contextual Intelligence AND Knowledge Quiz) -->
|
||||
<div class="nexus-mobile-tab-content insight-tab @(_activeMobileTab == MobileReaderTab.Insight ? "active" : "")">
|
||||
<div class="mobile-insight-container">
|
||||
<div class="mobile-insight-header">
|
||||
<div class="mobile-insight-nav">
|
||||
<button class="mobile-insight-nav-btn @(_activeTab == SidebarTab.Knowledge ? "active" : "")" @onclick="() => SetActiveTab(SidebarTab.Knowledge)">
|
||||
<NexusIcon Name="brain" Size="16" />
|
||||
<span>Podgląd pojęcia</span>
|
||||
</button>
|
||||
<button class="mobile-insight-nav-btn quiz-btn @(_activeTab == SidebarTab.Quiz ? "active" : "") @(QuizService.HasNewQuiz ? "quiz-pulse" : "")" @onclick="() => SetActiveTab(SidebarTab.Quiz)">
|
||||
<NexusIcon Name="quiz" Size="16" />
|
||||
<span>Quiz wiedzy</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mobile-insight-body">
|
||||
@if (_activeTab == SidebarTab.Knowledge)
|
||||
{
|
||||
<div class="contextual-intelligence-panel">
|
||||
<div class="panel-body">
|
||||
@if (_selectedNode != null)
|
||||
{
|
||||
<div class="node-details">
|
||||
<div class="node-header-section">
|
||||
<span class="node-group-badge @(_selectedNode.Group.ToLower())">@(_selectedNode.Group.ToUpper())</span>
|
||||
<h3 class="node-label">@_selectedNode.Label</h3>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(_selectedNode.Description))
|
||||
{
|
||||
<div class="detail-section">
|
||||
<p class="node-description">@_selectedNode.Description</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(_selectedNode.Summary))
|
||||
{
|
||||
<div class="detail-section summary-section">
|
||||
<h4 class="section-title neon-sub-header">Podsumowanie</h4>
|
||||
<p class="node-summary">@_selectedNode.Summary</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (_selectedNode.KeyTerms != null && _selectedNode.KeyTerms.Any())
|
||||
{
|
||||
<div class="detail-section key-terms-section">
|
||||
<h4 class="section-title neon-sub-header">Kluczowe Pojęcia</h4>
|
||||
<ul class="key-terms-list">
|
||||
@foreach (var term in _selectedNode.KeyTerms)
|
||||
{
|
||||
<li class="key-term-item">
|
||||
<span class="term-bullet">•</span>
|
||||
<span class="term-text">@term</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="no-node-selected">
|
||||
<div class="placeholder-glow"></div>
|
||||
<p class="placeholder-text">Wybierz pojęcie na wykresie, aby wyświetlić jego podsumowanie.</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="mobile-quiz-wrapper">
|
||||
<KnowledgeCheck />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Three-Tab Fixed Bottom Navigation Bar -->
|
||||
<div class="nexus-mobile-bottom-nav">
|
||||
<button class="bottom-nav-item @(_activeMobileTab == MobileReaderTab.Reader ? "active" : "")" @onclick="() => SetMobileTab(MobileReaderTab.Reader)">
|
||||
<NexusIcon Name="book-open" Size="20" />
|
||||
<span>Czytnik</span>
|
||||
</button>
|
||||
<button class="bottom-nav-item @(_activeMobileTab == MobileReaderTab.Graph ? "active" : "")" @onclick="() => SetMobileTab(MobileReaderTab.Graph)">
|
||||
<NexusIcon Name="network" Size="20" />
|
||||
<span>Wykres</span>
|
||||
</button>
|
||||
<button class="bottom-nav-item @(_activeMobileTab == MobileReaderTab.Insight ? "active" : "")" @onclick="() => SetMobileTab(MobileReaderTab.Insight)">
|
||||
<span class="insight-icon-wrapper">
|
||||
<NexusIcon Name="brain" Size="20" />
|
||||
@if (QuizService.HasNewQuiz)
|
||||
{
|
||||
<span class="nav-quiz-indicator"></span>
|
||||
}
|
||||
</span>
|
||||
<span>Analiza</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</Authorized>
|
||||
<Authorizing>
|
||||
<div class="app-preloader">
|
||||
@@ -155,7 +268,15 @@
|
||||
Quiz
|
||||
}
|
||||
|
||||
private enum MobileReaderTab
|
||||
{
|
||||
Reader,
|
||||
Graph,
|
||||
Insight
|
||||
}
|
||||
|
||||
private SidebarTab _activeTab = SidebarTab.Knowledge;
|
||||
private MobileReaderTab _activeMobileTab = MobileReaderTab.Reader;
|
||||
private string? _selectedNodeId;
|
||||
private GraphNodeDto? _selectedNode;
|
||||
|
||||
@@ -169,6 +290,7 @@
|
||||
QuizService.OnQuizRequested += HandleQuizRequestedAsync;
|
||||
|
||||
InteractionService.OnNodeSelected += HandleNodeSelectedAsync;
|
||||
InteractionService.OnAssistantRequested += HandleAssistantRequestedAsync;
|
||||
GraphService.OnGraphUpdated += HandleGraphUpdatedAsync;
|
||||
|
||||
var context = PlatformService.GetDeviceContext();
|
||||
@@ -190,8 +312,25 @@
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void SetMobileTab(MobileReaderTab tab)
|
||||
{
|
||||
_activeMobileTab = tab;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task HandleQuizRequestedAsync(string blockId)
|
||||
{
|
||||
_activeTab = SidebarTab.Quiz;
|
||||
if (_isMobile)
|
||||
{
|
||||
_activeMobileTab = MobileReaderTab.Insight;
|
||||
}
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task HandleAssistantRequestedAsync()
|
||||
{
|
||||
_activeMobileTab = MobileReaderTab.Insight;
|
||||
_activeTab = SidebarTab.Quiz;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
@@ -203,6 +342,11 @@
|
||||
{
|
||||
_selectedNode = GraphService.CurrentGraphData.Nodes.FirstOrDefault(n => n.Id == nodeId);
|
||||
}
|
||||
if (_isMobile)
|
||||
{
|
||||
_activeMobileTab = MobileReaderTab.Insight;
|
||||
_activeTab = SidebarTab.Knowledge;
|
||||
}
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
@@ -237,6 +381,7 @@
|
||||
QuizService.OnQuizUpdated -= HandleUpdate;
|
||||
QuizService.OnQuizRequested -= HandleQuizRequestedAsync;
|
||||
InteractionService.OnNodeSelected -= HandleNodeSelectedAsync;
|
||||
InteractionService.OnAssistantRequested -= HandleAssistantRequestedAsync;
|
||||
GraphService.OnGraphUpdated -= HandleGraphUpdatedAsync;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user