style(ui): refactor reader layout grid, fix focus mode layout collapse, fix SVG rendering dots, reorganize intelligence toolbar #69
@@ -36,3 +36,13 @@ Run test suite:
|
|||||||
```bash
|
```bash
|
||||||
dotnet test --no-restore
|
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 Task HandleUpdate() => InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
private async Task HandleThemeChangedAsync() => await InvokeAsync(StateHasChanged);
|
private Task HandleThemeChangedAsync() => InvokeAsync(StateHasChanged);
|
||||||
|
mjasin marked this conversation as resolved
Outdated
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -82,41 +82,39 @@
|
|||||||
|
|
||||||
/* Light mode overrides */
|
/* Light mode overrides */
|
||||||
.theme-light .intelligence-toolbar {
|
.theme-light .intelligence-toolbar {
|
||||||
background: #ffffff;
|
background: #f5f5f4;
|
||||||
border-right: 1px solid rgba(0, 0, 0, 0.08);
|
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 {
|
.theme-light .toolbar-item {
|
||||||
color: #71717a;
|
color: #78716c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-light .toolbar-item:hover {
|
.theme-light .toolbar-item:hover {
|
||||||
color: #10b981;
|
color: #10b981;
|
||||||
background: rgba(16, 185, 129, 0.05);
|
background: rgba(16, 185, 129, 0.05);
|
||||||
box-shadow: 0 0 15px rgba(16, 185, 129, 0.15);
|
box-shadow: 0 0 10px rgba(16, 185, 129, 0.1);
|
||||||
filter: drop-shadow(0 0 5px rgba(16, 185, 129, 0.2));
|
filter: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-light .toolbar-item.active {
|
.theme-light .toolbar-item.active {
|
||||||
color: #10b981;
|
color: #10b981;
|
||||||
background: rgba(16, 185, 129, 0.08);
|
background: rgba(16, 185, 129, 0.08);
|
||||||
box-shadow: 0 0 20px rgba(16, 185, 129, 0.25);
|
box-shadow: 0 0 15px rgba(16, 185, 129, 0.15);
|
||||||
filter: drop-shadow(0 0 8px rgba(16, 185, 129, 0.3));
|
filter: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-light .toolbar-item.active::after {
|
.theme-light .toolbar-item.active::after {
|
||||||
background: #10b981;
|
background: #10b981;
|
||||||
box-shadow: 0 0 8px rgba(16, 185, 129, 0.5);
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-light .toolbar-item.focus-active {
|
.theme-light .toolbar-item.focus-active {
|
||||||
color: #10b981;
|
color: #10b981;
|
||||||
filter: drop-shadow(0 0 8px rgba(16, 185, 129, 0.3));
|
filter: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.theme-light .toolbar-separator {
|
.theme-light .toolbar-separator {
|
||||||
background: rgba(0, 0, 0, 0.08);
|
background: rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,7 @@
|
|||||||
private bool _isMobile = false;
|
private bool _isMobile = false;
|
||||||
private DotNetObjectReference<ReaderCanvas>? _selfReference;
|
private DotNetObjectReference<ReaderCanvas>? _selfReference;
|
||||||
private IJSObjectReference? _viewportModule;
|
private IJSObjectReference? _viewportModule;
|
||||||
|
private IJSObjectReference? _selectionModule;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
@@ -201,10 +202,13 @@
|
|||||||
{
|
{
|
||||||
try
|
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)
|
if (_selfReference != null)
|
||||||
{
|
{
|
||||||
await module.InvokeVoidAsync("initSelectionListener", _selfReference, _containerRef);
|
await _selectionModule.InvokeVoidAsync("initSelectionListener", _selfReference, _containerRef);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -440,6 +444,19 @@
|
|||||||
InteractionService.OnTextSelected -= HandleTextSelected;
|
InteractionService.OnTextSelected -= HandleTextSelected;
|
||||||
SyncService.OnProgressReceived -= HandleSyncProgressReceived;
|
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
|
try
|
||||||
{
|
{
|
||||||
if (_viewportModule != null)
|
if (_viewportModule != null)
|
||||||
|
|||||||
@@ -233,6 +233,10 @@ public sealed partial class KnowledgeCoordinator : IDisposable, IAsyncDisposable
|
|||||||
{
|
{
|
||||||
SelectionSummary = result.Value.Summary;
|
SelectionSummary = result.Value.Summary;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("[KnowledgeCoordinator] Selection summary request failed: {Errors}", string.Join(", ", result.Errors.Select(e => e.Message)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -176,52 +176,6 @@
|
|||||||
|
|
||||||
--nexus-accent: #10b981;
|
--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 {
|
.theme-light .knowledge-graph-container svg {
|
||||||
background: radial-gradient(circle, #ffffff 0%, #e8e4da 100%) !important;
|
background: radial-gradient(circle, #ffffff 0%, #e8e4da 100%) !important;
|
||||||
}
|
}
|
||||||
|
mjasin marked this conversation as resolved
Antigravity
commented
🟡 Design/Architecture: Global CSS overrides for light-mode component colors (like Component-specific styling should live inside their respective scoped stylesheets (e.g., Please move these overrides into 🟡 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 Specific Tweaks */
|
||||||
.platform-mobile .nexus-button {
|
.platform-mobile .nexus-btn {
|
||||||
min-height: var(--touch-target-size);
|
min-height: var(--touch-target-size);
|
||||||
min-width: var(--touch-target-size);
|
min-width: var(--touch-target-size);
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
padding: 12px 24px;
|
padding: 12px 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-desktop .nexus-button {
|
.platform-desktop .nexus-btn {
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
|
|||||||
@@ -54,11 +54,17 @@ export function positionToolbar() {
|
|||||||
below: below
|
below: below
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
let currentHandleSelection = null;
|
||||||
|
let currentMouseUpHandler = null;
|
||||||
|
let currentContainer = null;
|
||||||
|
|
||||||
export function initSelectionListener(dotNetHelper, container) {
|
export function initSelectionListener(dotNetHelper, container) {
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
console.log("[SelectionHandler] Initializing...");
|
console.log("[SelectionHandler] Initializing...");
|
||||||
|
|
||||||
|
// Clean up any existing listeners first
|
||||||
|
destroySelectionListener();
|
||||||
|
|
||||||
const handleSelection = () => {
|
const handleSelection = () => {
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
@@ -104,9 +110,26 @@ export function initSelectionListener(dotNetHelper, container) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use multiple triggers for maximum reliability
|
const mouseUpHandler = () => setTimeout(handleSelection, 10);
|
||||||
document.addEventListener('selectionchange', handleSelection);
|
|
||||||
container.addEventListener('mouseup', () => 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() {
|
export function getSelectionText() {
|
||||||
|
|||||||
Reference in New Issue
Block a user
🟢 Minor/Suggestion: The method
HandleThemeChangedAsyncdoes not need to be marked asasync Tasksince it only awaits a single task and does not contain other logic.You can simplify it to avoid state machine generation overhead: