Initial commit: NexusArchitect Professional Workstation Overhaul

This commit is contained in:
Debian
2026-04-24 20:27:22 +02:00
commit f3e94c4f42
193 changed files with 5809 additions and 0 deletions
@@ -0,0 +1,11 @@
<button class="nexus-btn @Class" @onclick="OnClick" disabled="@Disabled" @attributes="AdditionalAttributes">
@ChildContent
</button>
@code {
[Parameter] public RenderFragment? ChildContent { get; set; }
[Parameter] public string Class { get; set; } = string.Empty;
[Parameter] public EventCallback<Microsoft.AspNetCore.Components.Web.MouseEventArgs> OnClick { get; set; }
[Parameter] public bool Disabled { get; set; }
[Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }
}
@@ -0,0 +1,35 @@
.nexus-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 44px;
min-height: 44px;
padding: 0.5rem 1rem;
background-color: var(--nexus-card);
color: var(--nexus-neon);
border: 1px solid var(--nexus-neon);
font-family: var(--nexus-font-sans);
font-weight: 500;
font-size: 1rem;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 0 5px rgba(0, 255, 153, 0.1);
}
.nexus-btn:hover:not(:disabled) {
background-color: rgba(0, 255, 153, 0.1);
box-shadow: 0 0 15px rgba(0, 255, 153, 0.3);
}
.nexus-btn:active:not(:disabled) {
transform: scale(0.98);
}
.nexus-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
border-color: #555;
color: #555;
box-shadow: none;
}
@@ -0,0 +1,40 @@
<svg class="nexus-icon @Class" viewBox="0 0 24 24" fill="currentColor" width="@Size" height="@Size" @attributes="AdditionalAttributes">
@switch (Name.ToLowerInvariant())
{
case "robot":
<path d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h5a2 2 0 0 1 2 2v2a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2a2 2 0 0 1-2-2v-2a2 2 0 0 1 2-2V9c0-1.1.9-2 2-2h5V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2zM8 11v4h8v-4H8zm-2 0H4v4h2v-4zm14 0h-2v4h2v-4z" />
break;
case "play":
<path d="M8 5v14l11-7z" />
break;
case "check":
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
break;
case "search":
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" />
break;
case "message-square":
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
break;
case "settings":
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" /><circle cx="12" cy="12" r="3" />
break;
case "bookmark":
<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" />
break;
case "target":
<circle cx="12" cy="12" r="10" /><circle cx="12" cy="12" r="6" /><circle cx="12" cy="12" r="2" />
break;
default:
<!-- Fallback circle -->
<circle cx="12" cy="12" r="10" />
break;
}
</svg>
@code {
[Parameter] public string Name { get; set; } = string.Empty;
[Parameter] public string Size { get; set; } = "24";
[Parameter] public string Class { get; set; } = string.Empty;
[Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }
}

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,10 @@
.nexus-icon {
display: inline-block;
vertical-align: middle;
transition: fill 0.2s ease, filter 0.2s ease;
}
.neon-glow {
fill: var(--nexus-neon);
filter: drop-shadow(0 0 4px var(--nexus-neon));
}
@@ -0,0 +1,25 @@
<div class="nexus-typography @VariantCssClass @Class" @attributes="AdditionalAttributes">
@ChildContent
</div>
@code {
[Parameter] public RenderFragment? ChildContent { get; set; }
[Parameter] public string Class { get; set; } = string.Empty;
[Parameter] public TypographyVariant Variant { get; set; } = TypographyVariant.UI;
[Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }
private string VariantCssClass => Variant switch
{
TypographyVariant.Heading => "nexus-heading",
TypographyVariant.Ebook => "nexus-ebook",
TypographyVariant.UI => "nexus-ui",
_ => "nexus-ui"
};
public enum TypographyVariant
{
Heading,
Ebook,
UI
}
}
@@ -0,0 +1,27 @@
.nexus-typography {
margin: 0;
}
.nexus-heading {
font-family: var(--nexus-font-sans);
font-size: 2rem;
font-weight: 600;
color: white;
margin-bottom: 1rem;
}
.nexus-ebook {
font-family: var(--nexus-font-serif);
font-size: 1.125rem;
line-height: 1.65;
color: #1a1a1a;
margin-bottom: 1.5rem;
font-weight: 300;
}
.nexus-ui {
font-family: var(--nexus-font-sans);
font-size: 1rem;
color: #cccccc;
}
@@ -0,0 +1,49 @@
@using NexusReader.UI.Shared.Services
@inject IQuizStateService QuizState
<div class="ai-bubble-container">
<div class="ai-bubble">
<div class="ai-avatar">
<div class="avatar-ring"></div>
<NexusIcon Name="robot" Size="48" Class="neon-pulse" />
<div class="avatar-label">
<span class="name">E-Czytnik</span>
<span class="role">Asystent AI</span>
</div>
</div>
<div class="ai-content">
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">@Dialogue</NexusTypography>
<div class="ai-actions">
<button class="action-btn ghost" @onclick='() => HandleActionClick("more")'>Pokaż więcej informacji</button>
<button class="action-btn neon-border" @onclick='() => HandleActionClick("quiz")'>Rozwiąż quiz</button>
</div>
</div>
<div class="bubble-pointer"></div>
</div>
</div>
@code {
[Parameter] public string ContextBlockId { get; set; } = string.Empty;
[Parameter] public string Dialogue { get; set; } = string.Empty;
[Parameter] public List<string> Actions { get; set; } = new();
[Parameter] public EventCallback<string> OnActionTriggered { get; set; }
private bool _isQuizMode = false;
private async Task HandleActionClick(string action)
{
if (action.Contains("quiz", StringComparison.OrdinalIgnoreCase))
{
_isQuizMode = true;
QuizState.RequestQuiz(ContextBlockId);
}
if (OnActionTriggered.HasDelegate)
{
await OnActionTriggered.InvokeAsync(action);
}
}
}
@@ -0,0 +1,108 @@
.ai-bubble-container {
margin: 2rem 0;
display: flex;
justify-content: center;
}
.ai-bubble {
position: relative;
display: flex;
flex-direction: row;
gap: 1.5rem;
padding: 1.5rem;
background: rgba(18, 18, 18, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
max-width: 600px;
color: #fff;
}
.ai-avatar {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
min-width: 100px;
}
.avatar-label {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.avatar-label .name {
font-size: 0.8rem;
font-weight: 600;
color: #fff;
}
.avatar-label .role {
font-size: 0.7rem;
opacity: 0.6;
}
.neon-pulse {
color: var(--nexus-neon);
filter: drop-shadow(0 0 8px var(--nexus-neon));
animation: pulse 2s infinite ease-in-out;
}
@keyframes pulse {
0% { transform: scale(1); filter: drop-shadow(0 0 8px var(--nexus-neon)); }
50% { transform: scale(1.05); filter: drop-shadow(0 0 15px var(--nexus-neon)); }
100% { transform: scale(1); filter: drop-shadow(0 0 8px var(--nexus-neon)); }
}
.ai-content {
display: flex;
flex-direction: column;
gap: 1rem;
justify-content: center;
}
.ai-actions {
display: flex;
gap: 1rem;
}
.action-btn {
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s ease;
font-family: var(--nexus-font-sans);
}
.action-btn.ghost {
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.2);
color: #aaa;
}
.action-btn.neon-border {
background: rgba(0, 255, 153, 0.1);
border: 1px solid var(--nexus-neon);
color: var(--nexus-neon);
}
.action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 255, 153, 0.2);
}
.bubble-pointer {
position: absolute;
right: -10px;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-left: 10px solid rgba(18, 18, 18, 0.95);
}
@@ -0,0 +1,47 @@
@using NexusReader.UI.Shared.Services
@inject IFocusModeService FocusMode
<aside class="intelligence-toolbar">
<div class="toolbar-top">
<button class="toolbar-item" title="Back">
<NexusIcon Name="play" Size="20" Class="rotate-180" />
</button>
<button class="toolbar-item active" title="Chat">
<NexusIcon Name="message-square" Size="20" />
</button>
</div>
<div class="toolbar-middle">
<button class="toolbar-item" title="Settings">
<NexusIcon Name="settings" Size="20" />
</button>
<button class="toolbar-item" title="Bookmarks">
<NexusIcon Name="bookmark" Size="20" />
</button>
<button class="toolbar-item" title="Search">
<NexusIcon Name="search" Size="20" />
</button>
</div>
<div class="toolbar-bottom">
<button class="toolbar-item @(FocusMode.IsFocusModeActive ? "active focus-active" : "")"
@onclick="FocusMode.ToggleAsync" title="Focus Mode (F)">
<NexusIcon Name="target" Size="20" />
</button>
<button class="toolbar-item" title="Global Settings">
<NexusIcon Name="settings" Size="20" />
</button>
</div>
</aside>
@code {
protected override void OnInitialized()
{
FocusMode.OnFocusModeChanged += StateHasChanged;
}
public void Dispose()
{
FocusMode.OnFocusModeChanged -= StateHasChanged;
}
}
@@ -0,0 +1,48 @@
.intelligence-toolbar {
width: 50px;
height: 100%;
background: #1a1a1a;
border-right: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 1rem 0;
align-items: center;
z-index: 20;
}
.toolbar-top, .toolbar-middle, .toolbar-bottom {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.toolbar-item {
background: none;
border: none;
color: #666;
cursor: pointer;
padding: 8px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
border-radius: 4px;
}
.toolbar-item:hover {
color: #fff;
background: rgba(255, 255, 255, 0.05);
}
.toolbar-item.active {
color: var(--nexus-neon);
}
.toolbar-item.focus-active {
filter: drop-shadow(0 0 5px var(--nexus-neon));
}
.rotate-180 {
transform: rotate(180deg);
}
@@ -0,0 +1,114 @@
@using MediatR
@using NexusReader.Application.Queries.Quiz
@using NexusReader.Application.Commands.Quiz
@using NexusReader.Application.Abstractions.Services
@inject IMediator Mediator
@inject IPlatformService PlatformService
<div class="knowledge-check">
<div class="quiz-header">
<span class="header-title">Sprawdzian Wiedzy</span>
<button class="expand-btn">⌵</button>
</div>
@if (_isLoading)
{
<div class="loading-state">Pobieranie pytań...</div>
}
else if (_quiz != null)
{
<div class="quiz-body">
@foreach (var question in _quiz.Questions)
{
<div class="question-container">
<p class="question-text">@question.Question</p>
<div class="options-list">
@for (int i = 0; i < question.Options.Count; i++)
{
var index = i;
var letter = (char)('A' + i);
<button class="option-item @GetOptionClass(question, index)"
@onclick="() => SelectOptionAsync(question, index)"
disabled="@_states.ContainsKey(question)">
<span class="option-letter">@letter)</span>
<span class="option-text">@question.Options[index]</span>
</button>
}
</div>
</div>
}
<div class="quiz-footer">
<button class="submit-btn" disabled="@(!AllQuestionsAnswered())">Wyślij</button>
</div>
</div>
}
</div>
@code {
[Parameter] public string ContextBlockId { get; set; } = string.Empty;
private bool _isLoading = true;
private QuizDto? _quiz;
private Dictionary<QuizQuestionDto, (int SelectedIndex, bool IsCorrect)> _states = new();
protected override async Task OnInitializedAsync()
{
_isLoading = true;
var query = new GetQuizQuestionsQuery(ContextBlockId);
var result = await Mediator.Send(query);
if (result.IsSuccess)
_quiz = result.Value;
_isLoading = false;
}
private async Task SelectOptionAsync(QuizQuestionDto question, int index)
{
if (_states.ContainsKey(question)) return;
// Haptic feedback
await PlatformService.VibrateAsync(40);
var cmd = new SubmitAnswerCommand(index, question.CorrectIndex);
var res = await Mediator.Send(cmd);
_states[question] = (index, res.IsSuccess);
if (res.IsSuccess)
await PlatformService.VibrateSuccessAsync();
else
await PlatformService.VibrateErrorAsync();
StateHasChanged();
}
private bool AllQuestionsAnswered()
{
return _quiz != null && _states.Count == _quiz.Questions.Count;
}
private string GetBlockClass(QuizQuestionDto question)
{
if (!_states.TryGetValue(question, out var state)) return "";
return state.IsCorrect ? "state-correct" : "state-incorrect";
}
private string GetOptionClass(QuizQuestionDto question, int index)
{
if (!_states.TryGetValue(question, out var state)) return "";
if (state.SelectedIndex == index)
return state.IsCorrect ? "option-correct" : "option-incorrect";
if (state.IsCorrect == false && question.CorrectIndex == index)
return "option-revealed-correct";
return "option-faded";
}
}
@@ -0,0 +1,108 @@
.knowledge-check {
padding: 1.5rem;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12px;
margin: 1rem;
}
.quiz-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
font-family: var(--nexus-font-sans);
}
.header-title {
font-size: 1.1rem;
font-weight: 600;
color: #fff;
}
.expand-btn {
background: none;
border: none;
color: #666;
font-size: 1.2rem;
cursor: pointer;
}
.question-text {
font-size: 0.95rem;
color: #ccc;
margin-bottom: 1rem;
}
.options-list {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.option-item {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 0.8rem 1rem;
display: flex;
align-items: center;
gap: 1rem;
cursor: pointer;
transition: all 0.2s ease;
text-align: left;
width: 100%;
}
.option-item:hover {
background: rgba(255, 255, 255, 0.06);
}
.option-item.selected {
border-color: var(--nexus-neon);
background: rgba(0, 255, 153, 0.05);
}
.option-letter {
font-weight: 600;
color: var(--nexus-neon);
min-width: 25px;
}
.option-text {
font-size: 0.9rem;
color: #fff;
}
.quiz-footer {
margin-top: 1.5rem;
display: flex;
justify-content: center;
}
.submit-btn {
padding: 0.6rem 2.5rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
color: #888;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.3s ease;
}
.submit-btn:not(:disabled) {
background: var(--nexus-neon);
color: #000;
border-color: var(--nexus-neon);
}
.option-correct {
border-color: #00ff99 !important;
background: rgba(0, 255, 153, 0.1) !important;
}
.option-incorrect {
border-color: #ff4444 !important;
background: rgba(255, 68, 68, 0.1) !important;
}
@@ -0,0 +1,112 @@
@using MediatR
@using NexusReader.Application.Queries.Graph
@using Microsoft.JSInterop
@using NexusReader.UI.Shared.Services
@implements IAsyncDisposable
@inject IMediator Mediator
@inject IJSRuntime JS
@inject IFocusModeService FocusMode
<div class="knowledge-graph-container" id="@ContainerId">
@if (GraphData == null)
{
<div class="loading-state">
<NexusIcon Name="robot" Size="48" Class="neon-glow" />
<NexusTypography>Analyzing Chapter Nodes...</NexusTypography>
</div>
}
else
{
<div class="graph-controls">
<button class="zoom-btn" @onclick="ZoomIn" title="Zoom In">+</button>
<button class="zoom-btn" @onclick="ZoomOut" title="Zoom Out"></button>
<button class="zoom-btn reset" @onclick="ZoomReset" title="Reset">⟲</button>
</div>
}
</div>
@code {
[Parameter] public EventCallback<string> OnNodeSelected { get; set; }
private string ContainerId = "d3-graph-container";
private GraphDataDto? GraphData;
private IJSObjectReference? _module;
private DotNetObjectReference<KnowledgeGraph>? _dotNetHelper;
protected override void OnInitialized()
{
FocusMode.OnFocusModeChanged += HandleFocusSimulation;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var result = await Mediator.Send(new GetKnowledgeGraphQuery());
if (result.IsSuccess)
{
GraphData = result.Value;
StateHasChanged();
await InitializeGraphAsync();
}
}
}
private async Task InitializeGraphAsync()
{
_module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/knowledgeGraph.js");
_dotNetHelper = DotNetObjectReference.Create(this);
await _module.InvokeVoidAsync("mount", ContainerId, GraphData, _dotNetHelper);
}
private async Task ZoomIn() => await (_module?.InvokeVoidAsync("zoomIn") ?? ValueTask.CompletedTask);
private async Task ZoomOut() => await (_module?.InvokeVoidAsync("zoomOut") ?? ValueTask.CompletedTask);
private async Task ZoomReset() => await (_module?.InvokeVoidAsync("zoomReset") ?? ValueTask.CompletedTask);
[JSInvokable]
public async Task OnNodeClicked(string nodeId)
{
if (OnNodeSelected.HasDelegate)
{
await OnNodeSelected.InvokeAsync(nodeId);
}
}
private async void HandleFocusSimulation()
{
if (_module == null) return;
try
{
if (FocusMode.IsFocusModeActive)
await _module.InvokeVoidAsync("pause");
else
await _module.InvokeVoidAsync("resume");
}
catch { }
}
public async ValueTask DisposeAsync()
{
FocusMode.OnFocusModeChanged -= HandleFocusSimulation;
try
{
if (_module is not null)
{
await _module.InvokeVoidAsync("unmount", ContainerId);
await _module.DisposeAsync();
}
}
catch (JSDisconnectedException)
{
// Ignored, the circuit is already closed
}
catch (TaskCanceledException)
{
// Ignored, the circuit is already closed
}
_dotNetHelper?.Dispose();
}
}
@@ -0,0 +1,75 @@
.knowledge-graph-container {
width: 100%;
height: 50vh;
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
overflow: hidden;
position: relative;
}
.graph-controls {
position: absolute;
bottom: 1rem;
right: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
z-index: 10;
}
.zoom-btn {
width: 28px;
height: 28px;
background: rgba(18, 18, 18, 0.8);
backdrop-filter: blur(4px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 4px;
color: #888;
font-size: 1rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.zoom-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--nexus-neon);
border-color: var(--nexus-neon);
}
.zoom-btn.reset {
font-size: 0.8rem;
}
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
animation: pulse 2s infinite ease-in-out;
}
@keyframes pulse {
0% { opacity: 0.6; }
50% { opacity: 1; }
100% { opacity: 0.6; }
}
::deep .nexus-node-active {
stroke: var(--nexus-neon) !important;
stroke-width: 2px !important;
filter: drop-shadow(0 0 12px var(--nexus-neon));
transition: all 0.3s ease;
}
.neon-glow {
color: var(--nexus-neon);
filter: drop-shadow(0 0 5px var(--nexus-neon));
}
@@ -0,0 +1,79 @@
@using MediatR
@using NexusReader.Application.Queries.Reader
@using Microsoft.JSInterop
@using NexusReader.UI.Shared.Services
@implements IDisposable
@inject IMediator Mediator
@inject IJSRuntime JS
@inject IThemeService ThemeService
@inject IFocusModeService FocusMode
<div class="reader-canvas theme-light">
@if (ViewModel == null)
{
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">@StatusMessage</NexusTypography>
}
else
{
<div class="reader-flow-container">
@foreach (var block in ViewModel.Blocks)
{
<div id="@block.Id" class="block-wrapper">
@if (block is TextSegmentBlock textSegment)
{
<NexusTypography Variant="NexusTypography.TypographyVariant.Ebook">@textSegment.Content</NexusTypography>
}
else if (block is AiActionTriggerBlock aiTrigger)
{
<AiAssistantBubble
ContextBlockId="@block.Id"
Dialogue="@aiTrigger.Dialogue"
Actions="@aiTrigger.ActionOptions"
OnActionTriggered="HandleAiAction" />
}
</div>
}
</div>
}
</div>
@code {
private ReaderPageViewModel? ViewModel;
private string StatusMessage = "Loading chapter...";
protected override async Task OnInitializedAsync()
{
ThemeService.OnThemeChanged += StateHasChanged;
var result = await Mediator.Send(new GetReaderPageQuery());
if (result.IsSuccess)
{
ViewModel = result.Value;
}
else
{
StatusMessage = "Failed to load chapter content.";
}
}
private void HandleAiAction(string action)
{
Console.WriteLine($"Action Triggered from Bubble: {action}");
}
public async Task ScrollToNodeAsync(string id)
{
try
{
await JS.InvokeVoidAsync("eval", $"document.getElementById('{id}')?.scrollIntoView({{ behavior: 'smooth', block: 'center' }});");
}
catch { }
}
public void Dispose()
{
ThemeService.OnThemeChanged -= StateHasChanged;
}
}
@@ -0,0 +1,12 @@
.reader-canvas {
max-width: 800px;
margin: 0 auto;
padding: 2rem 1rem;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.reader-flow-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
@@ -0,0 +1,21 @@
<footer class="reader-footer">
<div class="footer-content">
<div class="page-info">
<span class="label">Postęp:</span>
<span class="value">@Progress%</span>
</div>
<div class="progress-container">
<div class="progress-bar" style="width: @Progress%"></div>
</div>
<div class="meta-info">
<span class="time">1:30</span>
<span class="battery">45% 🔋</span>
</div>
</div>
</footer>
@code {
[Parameter] public int Progress { get; set; } = 45;
}
@@ -0,0 +1,53 @@
.reader-footer {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 40px;
background: #F9F9F9;
border-top: 1px solid rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
padding: 0 1.5rem;
z-index: 10;
}
.footer-content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
font-family: var(--nexus-font-sans);
font-size: 0.75rem;
color: #666;
}
.progress-container {
flex: 1;
height: 4px;
background: rgba(0, 0, 0, 0.1);
margin: 0 2rem;
border-radius: 2px;
overflow: hidden;
max-width: 400px;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #00ff99 0%, #00d4ff 100%);
border-radius: 2px;
}
.page-info, .meta-info {
display: flex;
gap: 0.5rem;
align-items: center;
}
.label {
opacity: 0.7;
}
.value {
font-weight: 600;
}