feat(ui): implement premium mobile-first reader layout with three-tab bottom navigation and assistant FAB
This commit is contained in:
@@ -4,9 +4,31 @@
|
||||
@using NexusReader.Application.Abstractions.Services
|
||||
@using NexusReader.UI.Shared.Services
|
||||
|
||||
<div class="hub-container">
|
||||
<div class="hub-container @(_isMobileMenuOpen ? "mobile-menu-open" : "")">
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<!-- Mobile Sticky Top-bar -->
|
||||
<div class="nexus-mobile-topbar">
|
||||
<button class="hamburger-btn" @onclick="ToggleMobileMenu" aria-label="Toggle Menu">
|
||||
<NexusIcon Name="menu" Size="24" />
|
||||
</button>
|
||||
<div class="mobile-logo">
|
||||
<NexusIcon Name="diamond" Size="20" Class="logo-icon pulsing-logo" />
|
||||
<span class="logo-text">Nexus</span>
|
||||
</div>
|
||||
<div class="mobile-user-pill">
|
||||
<div class="user-avatar-mini">
|
||||
@context.User.Identity?.Name?[0].ToString().ToUpper()
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Backdrop overlay -->
|
||||
@if (_isMobileMenuOpen)
|
||||
{
|
||||
<div class="mobile-sidebar-backdrop" @onclick="CloseMobileMenu"></div>
|
||||
}
|
||||
|
||||
<aside class="hub-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="logo">
|
||||
@@ -16,43 +38,43 @@
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<NavLink class="nav-item" href="/" Match="NavLinkMatch.All">
|
||||
<NavLink class="nav-item" href="/" Match="NavLinkMatch.All" @onclick="CloseMobileMenu">
|
||||
<div class="nav-icon">
|
||||
<NexusIcon Name="home" Size="18" />
|
||||
</div>
|
||||
<span class="nav-text">Dashboard</span>
|
||||
</NavLink>
|
||||
<NavLink class="nav-item" href="/library">
|
||||
<NavLink class="nav-item" href="/library" @onclick="CloseMobileMenu">
|
||||
<div class="nav-icon">
|
||||
<NexusIcon Name="book-open" Size="18" />
|
||||
</div>
|
||||
<span class="nav-text">Library</span>
|
||||
</NavLink>
|
||||
<NavLink class="nav-item" href="/concepts-map">
|
||||
<NavLink class="nav-item" href="/concepts-map" @onclick="CloseMobileMenu">
|
||||
<div class="nav-icon">
|
||||
<NexusIcon Name="map" Size="18" />
|
||||
</div>
|
||||
<span class="nav-text">Concepts Map</span>
|
||||
</NavLink>
|
||||
<NavLink class="nav-item" href="/intelligence">
|
||||
<NavLink class="nav-item" href="/intelligence" @onclick="CloseMobileMenu">
|
||||
<div class="nav-icon">
|
||||
<NexusIcon Name="cpu" Size="18" />
|
||||
</div>
|
||||
<span class="nav-text">Global AI Q&A</span>
|
||||
</NavLink>
|
||||
<NavLink class="nav-item" href="/profile">
|
||||
<NavLink class="nav-item" href="/profile" @onclick="CloseMobileMenu">
|
||||
<div class="nav-icon">
|
||||
<NexusIcon Name="message-square" Size="18" />
|
||||
</div>
|
||||
<span class="nav-text">Profile</span>
|
||||
</NavLink>
|
||||
<NavLink class="nav-item" href="/settings">
|
||||
<NavLink class="nav-item" href="/settings" @onclick="CloseMobileMenu">
|
||||
<div class="nav-icon">
|
||||
<NexusIcon Name="settings" Size="18" />
|
||||
</div>
|
||||
<span class="nav-text">Settings</span>
|
||||
</NavLink>
|
||||
<NavLink class="nav-item" href="/concenters">
|
||||
<NavLink class="nav-item" href="/concenters" @onclick="CloseMobileMenu">
|
||||
<div class="nav-icon">
|
||||
<NexusIcon Name="target" Size="18" />
|
||||
</div>
|
||||
@@ -90,6 +112,7 @@
|
||||
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
|
||||
|
||||
private bool _isSyncing = false;
|
||||
private bool _isMobileMenuOpen = false;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
@@ -104,8 +127,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleMobileMenu()
|
||||
{
|
||||
_isMobileMenuOpen = !_isMobileMenuOpen;
|
||||
}
|
||||
|
||||
private void CloseMobileMenu()
|
||||
{
|
||||
_isMobileMenuOpen = false;
|
||||
}
|
||||
|
||||
private async Task HandleLogout()
|
||||
{
|
||||
CloseMobileMenu();
|
||||
await IdentityService.LogoutAsync();
|
||||
NavigationManager.NavigateTo("/account/logout-form", true);
|
||||
}
|
||||
|
||||
@@ -190,4 +190,157 @@
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Mobile Styles */
|
||||
.nexus-mobile-topbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.nexus-mobile-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 60px;
|
||||
background: rgba(18, 18, 18, 0.85);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
padding: 0 1.25rem;
|
||||
z-index: 150;
|
||||
}
|
||||
|
||||
.hamburger-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #e0e0e0;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.2s;
|
||||
min-height: 48px;
|
||||
min-width: 48px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.hamburger-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.mobile-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.pulsing-logo {
|
||||
animation: pulse-glow 2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% {
|
||||
filter: drop-shadow(0 0 5px rgba(0, 255, 153, 0.4));
|
||||
opacity: 0.8;
|
||||
}
|
||||
50% {
|
||||
filter: drop-shadow(0 0 12px rgba(0, 255, 153, 0.8));
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-user-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-avatar-mini {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: linear-gradient(135deg, var(--nexus-neon) 0%, #0099ff 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: #121212;
|
||||
box-shadow: 0 0 10px rgba(0, 255, 153, 0.2);
|
||||
}
|
||||
|
||||
.mobile-sidebar-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
z-index: 190;
|
||||
animation: fade-in 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
::deep .hub-sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 280px;
|
||||
height: 100%;
|
||||
background: #141414;
|
||||
z-index: 200;
|
||||
transform: translateX(-100%);
|
||||
will-change: transform;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.mobile-menu-open ::deep .hub-sidebar {
|
||||
transform: translateX(0);
|
||||
box-shadow: 10px 0 30px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.hub-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hub-main {
|
||||
margin-top: 60px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
.hub-content {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
::deep .sidebar-header {
|
||||
padding: 1.5rem 1.25rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
::deep .sidebar-nav {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
::deep .nav-item {
|
||||
padding: 0.9rem 1.25rem;
|
||||
font-size: 0.95rem;
|
||||
min-height: 48px; /* Touch target */
|
||||
}
|
||||
|
||||
::deep .sidebar-footer {
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,4 +467,286 @@ main {
|
||||
.back-to-graph-btn:hover {
|
||||
color: var(--nexus-neon, #00f0ff);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
/* Mobile-First Platform Customization */
|
||||
.platform-mobile {
|
||||
grid-template-columns: 1fr !important;
|
||||
height: 100vh !important;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.platform-mobile .reader-pane {
|
||||
width: 100vw !important;
|
||||
height: calc(100vh - 60px) !important; /* reserve bottom nav height */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Three-tab mobile views depending on the active mobile tab class */
|
||||
.app-container.platform-mobile.active-mobile-tab-reader .reader-pane {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.app-container.platform-mobile.active-mobile-tab-graph .nexus-mobile-reader-tabs .graph-tab {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.app-container.platform-mobile.active-mobile-tab-insight .nexus-mobile-reader-tabs .insight-tab {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Mobile full-bleed tabs container */
|
||||
.nexus-mobile-reader-tabs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.platform-mobile .nexus-mobile-reader-tabs {
|
||||
display: block;
|
||||
width: 100vw;
|
||||
height: calc(100vh - 60px);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: #0d0d0d;
|
||||
overflow: hidden;
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
.nexus-mobile-tab-content {
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Active tab display with smooth slide-up / fade-in transition */
|
||||
.nexus-mobile-tab-content.active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: tab-transition 0.25s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes tab-transition {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Inside Mobile Graph Tab: full bleed and responsive */
|
||||
.nexus-mobile-tab-content.graph-tab {
|
||||
background: #09090b;
|
||||
}
|
||||
|
||||
.nexus-mobile-tab-content.graph-tab :deep(svg) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* Mobile Insight container & tabs */
|
||||
.mobile-insight-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mobile-insight-header {
|
||||
background: rgba(13, 13, 13, 0.95);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
padding: 0.75rem 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mobile-insight-nav {
|
||||
display: flex;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.mobile-insight-nav-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6rem 0.75rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-family: var(--nexus-font-sans, "Outfit", sans-serif);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.mobile-insight-nav-btn.active {
|
||||
background: rgba(0, 240, 255, 0.1);
|
||||
color: var(--nexus-neon, #00f0ff);
|
||||
box-shadow: 0 0 10px rgba(0, 240, 255, 0.15);
|
||||
}
|
||||
|
||||
.mobile-insight-nav-btn.quiz-btn.quiz-pulse {
|
||||
animation: quiz-pulse-btn-anim 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes quiz-pulse-btn-anim {
|
||||
0% { color: rgba(255, 255, 255, 0.5); }
|
||||
50% { color: #f43f5e; text-shadow: 0 0 8px rgba(244, 63, 94, 0.6); }
|
||||
100% { color: rgba(255, 255, 255, 0.5); }
|
||||
}
|
||||
|
||||
.mobile-insight-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: #09090b;
|
||||
}
|
||||
|
||||
.mobile-insight-body .contextual-intelligence-panel {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.mobile-insight-body .contextual-intelligence-panel .panel-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.mobile-quiz-wrapper {
|
||||
padding: 1.25rem;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Three-Tab Bottom Navigation Bar styling */
|
||||
.nexus-mobile-bottom-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.platform-mobile .nexus-mobile-bottom-nav {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 60px;
|
||||
background: rgba(13, 13, 13, 0.95);
|
||||
backdrop-filter: blur(16px);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.bottom-nav-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
height: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-family: var(--nexus-font-sans, "Outfit", sans-serif);
|
||||
font-size: 0.65rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.bottom-nav-item.active {
|
||||
color: var(--nexus-neon, #00f0ff);
|
||||
text-shadow: 0 0 10px rgba(0, 240, 255, 0.2);
|
||||
}
|
||||
|
||||
.bottom-nav-item.active :deep(svg) {
|
||||
filter: drop-shadow(0 0 5px var(--nexus-neon, #00f0ff));
|
||||
}
|
||||
|
||||
.insight-icon-wrapper {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-quiz-indicator {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #f43f5e;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 8px #f43f5e;
|
||||
animation: indicator-flash 1.5s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes indicator-flash {
|
||||
0% { transform: scale(0.8); opacity: 0.6; }
|
||||
50% { transform: scale(1.2); opacity: 1; }
|
||||
100% { transform: scale(0.8); opacity: 0.6; }
|
||||
}
|
||||
|
||||
/* Assistant FAB styling inside ReaderCanvas */
|
||||
:global(.nexus-mobile-assistant-fab) {
|
||||
position: fixed;
|
||||
bottom: 75px;
|
||||
right: 20px;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, rgba(0, 240, 255, 0.15) 0%, rgba(0, 100, 255, 0.15) 100%);
|
||||
border: 1px solid rgba(0, 240, 255, 0.4);
|
||||
box-shadow: 0 4px 20px rgba(0, 240, 255, 0.25);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 99;
|
||||
backdrop-filter: blur(8px);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
:global(.nexus-mobile-assistant-fab:hover) {
|
||||
transform: scale(1.1) translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 240, 255, 0.4);
|
||||
border-color: var(--nexus-neon, #00f0ff);
|
||||
}
|
||||
|
||||
:global(.nexus-mobile-assistant-fab:active) {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
:global(.nexus-mobile-assistant-fab.has-new-quiz) {
|
||||
border-color: #f43f5e;
|
||||
box-shadow: 0 4px 20px rgba(244, 63, 94, 0.3);
|
||||
}
|
||||
|
||||
:global(.nexus-mobile-assistant-fab .fab-badge) {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: #f43f5e;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 10px #f43f5e;
|
||||
animation: indicator-flash 1.5s infinite ease-in-out;
|
||||
}
|
||||
Reference in New Issue
Block a user