style(ui): refactor reader layout grid, fix focus mode layout collapse, fix SVG rendering dots, reorganize intelligence toolbar #69

Merged
mjasin merged 10 commits from feature/reader-visual-refactor into develop 2026-06-05 09:51:29 +00:00
7 changed files with 71 additions and 65 deletions
Showing only changes of commit 46cc119c81 - Show all commits
+10
View File
@@ -36,3 +36,13 @@ Run test suite:
```bash
dotnet test --no-restore
```
## 🗄️ Database Migrations
Automatic database migrations at startup (`MigrateAsync()`) have been disabled to ensure compatibility with Native AOT compilation and prevent locking issues in multi-instance environments.
To apply database migrations locally, run the EF Core migration command from the solution root:
```bash
dotnet ef database update --project src/NexusReader.Infrastructure --startup-project src/NexusReader.Web
```
@@ -61,7 +61,7 @@
private Task HandleUpdate() => InvokeAsync(StateHasChanged);
private async Task HandleThemeChangedAsync() => await InvokeAsync(StateHasChanged);
private Task HandleThemeChangedAsync() => InvokeAsync(StateHasChanged);
public void Dispose()
{
@@ -82,41 +82,39 @@
/* Light mode overrides */
.theme-light .intelligence-toolbar {
background: #ffffff;
background: #f5f5f4;
border-right: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: inset -1px 0 0 rgba(0,0,0,0.02);
box-shadow: inset -2px 0 10px rgba(0, 0, 0, 0.02);
}
.theme-light .toolbar-item {
color: #71717a;
color: #78716c;
}
.theme-light .toolbar-item:hover {
color: #10b981;
background: rgba(16, 185, 129, 0.05);
box-shadow: 0 0 15px rgba(16, 185, 129, 0.15);
filter: drop-shadow(0 0 5px rgba(16, 185, 129, 0.2));
box-shadow: 0 0 10px rgba(16, 185, 129, 0.1);
filter: none;
}
.theme-light .toolbar-item.active {
color: #10b981;
background: rgba(16, 185, 129, 0.08);
box-shadow: 0 0 20px rgba(16, 185, 129, 0.25);
filter: drop-shadow(0 0 8px rgba(16, 185, 129, 0.3));
box-shadow: 0 0 15px rgba(16, 185, 129, 0.15);
filter: none;
}
.theme-light .toolbar-item.active::after {
background: #10b981;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.5);
box-shadow: none;
}
.theme-light .toolbar-item.focus-active {
color: #10b981;
filter: drop-shadow(0 0 8px rgba(16, 185, 129, 0.3));
filter: none;
}
.theme-light .toolbar-separator {
background: rgba(0, 0, 0, 0.08);
}
@@ -99,6 +99,7 @@
private bool _isMobile = false;
private DotNetObjectReference<ReaderCanvas>? _selfReference;
private IJSObjectReference? _viewportModule;
private IJSObjectReference? _selectionModule;
protected override async Task OnInitializedAsync()
{
@@ -201,10 +202,13 @@
{
try
{
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js");
if (_selectionModule == null)
{
_selectionModule = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js");
}
if (_selfReference != null)
{
await module.InvokeVoidAsync("initSelectionListener", _selfReference, _containerRef);
await _selectionModule.InvokeVoidAsync("initSelectionListener", _selfReference, _containerRef);
}
}
catch (Exception ex)
1
@@ -440,6 +444,19 @@
InteractionService.OnTextSelected -= HandleTextSelected;
SyncService.OnProgressReceived -= HandleSyncProgressReceived;
try
{
if (_selectionModule != null)
{
await _selectionModule.InvokeVoidAsync("destroySelectionListener");
await _selectionModule.DisposeAsync();
}
}
catch (Exception ex)
{
Logger.LogDebug(ex, "Failed to destroy JS selection listener.");
}
try
{
if (_viewportModule != null)
1
@@ -233,6 +233,10 @@ public sealed partial class KnowledgeCoordinator : IDisposable, IAsyncDisposable
{
SelectionSummary = result.Value.Summary;
}
else
{
_logger.LogWarning("[KnowledgeCoordinator] Selection summary request failed: {Errors}", string.Join(", ", result.Errors.Select(e => e.Message)));
}
}
finally
{
+2 -48
View File
@@ -176,52 +176,6 @@
--nexus-accent: #10b981;
}
/* Scoped Component overrides for Light Mode (Bypassing Blazor CSS isolation) */
.theme-light .intelligence-toolbar {
background: #f5f5f4 !important;
border-right: 1px solid rgba(0, 0, 0, 0.08) !important;
box-shadow: inset -2px 0 10px rgba(0, 0, 0, 0.02) !important;
}
.theme-light .intelligence-toolbar .toolbar-item {
color: #78716c !important;
}
.theme-light .intelligence-toolbar .toolbar-item:hover {
color: #10b981 !important;
background: rgba(16, 185, 129, 0.05) !important;
box-shadow: 0 0 10px rgba(16, 185, 129, 0.1) !important;
filter: none !important;
}
.theme-light .intelligence-toolbar .toolbar-item.active {
color: #10b981 !important;
background: rgba(16, 185, 129, 0.08) !important;
box-shadow: 0 0 15px rgba(16, 185, 129, 0.15) !important;
filter: none !important;
}
.theme-light .intelligence-toolbar .toolbar-item.active::after {
background: #10b981 !important;
box-shadow: none !important;
}
.theme-light .intelligence-toolbar .toolbar-item.focus-active {
color: #10b981 !important;
filter: none !important;
}
.theme-light .intelligence-toolbar .toolbar-item.logout-item {
border-top: 1px solid rgba(0, 0, 0, 0.08) !important;
color: #a8a29e !important;
}
.theme-light .intelligence-toolbar .toolbar-item.logout-item:hover {
color: #ef4444 !important;
filter: none !important;
}
.theme-light .knowledge-graph-container svg {
background: radial-gradient(circle, #ffffff 0%, #e8e4da 100%) !important;
}
mjasin marked this conversation as resolved
Review

🟡 Design/Architecture: Global CSS overrides for light-mode component colors (like .theme-light .intelligence-toolbar) violate the architectural separation of concerns.

Component-specific styling should live inside their respective scoped stylesheets (e.g., IntelligenceToolbar.razor.css) rather than the global app.css. Using !important here also creates styling conflicts (e.g., the scoped stylesheet has a background of #ffffff, but this global rule overrides it with #f5f5f4).

Please move these overrides into IntelligenceToolbar.razor.css.

🟡 Design/Architecture: Global CSS overrides for light-mode component colors (like `.theme-light .intelligence-toolbar`) violate the architectural separation of concerns. Component-specific styling should live inside their respective scoped stylesheets (e.g., `IntelligenceToolbar.razor.css`) rather than the global `app.css`. Using `!important` here also creates styling conflicts (e.g., the scoped stylesheet has a background of `#ffffff`, but this global rule overrides it with `#f5f5f4`). Please move these overrides into `IntelligenceToolbar.razor.css`.
@@ -350,14 +304,14 @@ body {
/* Platform Specific Tweaks */
.platform-mobile .nexus-button {
.platform-mobile .nexus-btn {
min-height: var(--touch-target-size);
min-width: var(--touch-target-size);
font-size: 1.1rem;
padding: 12px 24px;
}
.platform-desktop .nexus-button {
.platform-desktop .nexus-btn {
min-height: 36px;
font-size: 0.9rem;
padding: 8px 16px;
@@ -54,11 +54,17 @@ export function positionToolbar() {
below: below
};
}
let currentHandleSelection = null;
let currentMouseUpHandler = null;
let currentContainer = null;
export function initSelectionListener(dotNetHelper, container) {
if (!container) return;
console.log("[SelectionHandler] Initializing...");
// Clean up any existing listeners first
destroySelectionListener();
const handleSelection = () => {
const selection = window.getSelection();
1
@@ -104,9 +110,26 @@ export function initSelectionListener(dotNetHelper, container) {
}
};
// Use multiple triggers for maximum reliability
document.addEventListener('selectionchange', handleSelection);
container.addEventListener('mouseup', () => setTimeout(handleSelection, 10));
const mouseUpHandler = () => setTimeout(handleSelection, 10);
currentHandleSelection = handleSelection;
currentMouseUpHandler = mouseUpHandler;
currentContainer = container;
document.addEventListener('selectionchange', currentHandleSelection);
currentContainer.addEventListener('mouseup', currentMouseUpHandler);
}
export function destroySelectionListener() {
if (currentHandleSelection) {
document.removeEventListener('selectionchange', currentHandleSelection);
currentHandleSelection = null;
}
if (currentMouseUpHandler && currentContainer) {
currentContainer.removeEventListener('mouseup', currentMouseUpHandler);
currentMouseUpHandler = null;
currentContainer = null;
}
}
export function getSelectionText() {