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
3 changed files with 129 additions and 36 deletions
Showing only changes of commit 2ef8dd4066 - Show all commits
@@ -4,10 +4,11 @@
@inject KnowledgeCoordinator Coordinator
@inject IReaderInteractionService InteractionService
@inject IQuizStateService QuizService
@inject IJSRuntime JS
@if (IsVisible)
{
<div class="selection-ai-panel @(PositionBelow ? "below" : "")" style="@PanelStyle">
<div class="selection-ai-panel @(_positionBelow ? "below" : "")" style="@_style">
<button class="toolbar-btn primary @(IsLoadingSummary ? "loading" : "") @(IsAnyLoading ? "disabled" : "")"
disabled="@IsAnyLoading"
@onclick="RequestSummaryAsync">
@@ -50,22 +51,53 @@
private bool IsLoadingSummary = false;
private bool IsLoadingQuiz = false;
private bool IsAnyLoading => IsLoadingSummary || IsLoadingQuiz;
private bool PositionBelow => Coordinates != null && Coordinates.Top < 250;
private string _style = "visibility: hidden; opacity: 0; pointer-events: none;";
private bool _positionBelow = false;
private SelectionCoordinates? _lastCoordinates;
protected override void OnParametersSet()
{
Console.WriteLine($"[SelectionAiPanel] Parameters set. SelectedText: {SelectedText.Length} chars, Coordinates: {Coordinates?.Top}, PositionBelow: {PositionBelow}");
Console.WriteLine($"[SelectionAiPanel] Parameters set. SelectedText: {SelectedText.Length} chars, Coordinates: {Coordinates?.Top}");
if (Coordinates != _lastCoordinates)
{
_lastCoordinates = Coordinates;
_style = "visibility: hidden; opacity: 0; pointer-events: none;";
_positionBelow = false;
}
// Reset loading states when parameters change
IsLoadingSummary = false;
IsLoadingQuiz = false;
}
private string PanelStyle => Coordinates != null
? string.Create(System.Globalization.CultureInfo.InvariantCulture,
$"top: {(PositionBelow ? Coordinates.Bottom + 16 : Coordinates.Top - 16):F1}px !important; " +
$"left: {Math.Max(140.0, Math.Min(Coordinates.ViewportWidth - 140.0, Coordinates.Left + Coordinates.Width / 2.0)):F1}px !important; " +
$"transform: translate(-50%, {(PositionBelow ? "0" : "-100%")}) !important;")
: "";
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (IsVisible && _style.Contains("visibility: hidden"))
{
try
{
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js");
var result = await module.InvokeAsync<PositionResult>("positionToolbar");
if (result != null)
{
_style = string.Create(System.Globalization.CultureInfo.InvariantCulture,
$"left: {result.Left:F1}px !important; " +
$"top: {result.Top:F1}px !important; " +
$"visibility: visible !important; " +
$"opacity: 1 !important; " +
$"pointer-events: auto !important;");
_positionBelow = result.Below;
StateHasChanged();
}
}
catch (Exception ex)
{
Console.WriteLine($"[SelectionAiPanel] Error positioning toolbar: {ex.Message}");
}
}
}
private async Task RequestSummaryAsync()
{
@@ -131,5 +163,13 @@
{
await InteractionService.NotifyTextSelected(string.Empty, string.Empty, null!);
}
private class PositionResult
{
public double Left { get; set; }
public double Top { get; set; }
public bool Below { get; set; }
}
}
@@ -1,6 +1,6 @@
.selection-ai-panel {
position: fixed;
z-index: 9999;
position: absolute;
z-index: 10000;
display: flex;
align-items: center;
background: rgba(24, 24, 28, 0.85);
@@ -11,7 +11,7 @@
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5), 0 12px 28px rgba(0, 0, 0, 0.4);
padding: 4px 6px;
gap: 4px;
pointer-events: auto;
pointer-events: none; /* Controlled by inline styles */
user-select: none;
animation: fadeInScale 0.18s cubic-bezier(0.16, 1, 0.3, 1);
}
@@ -19,11 +19,11 @@
@keyframes fadeInScale {
from {
opacity: 0;
transform: translate(-50%, -80%) scale(0.96);
transform: scale(0.96);
}
to {
opacity: 1;
transform: translate(-50%, -100%) scale(1);
transform: scale(1);
}
}
@@ -34,11 +34,11 @@
@keyframes fadeInScaleBelow {
from {
opacity: 0;
transform: translate(-50%, -20%) scale(0.96);
transform: scale(0.96);
}
to {
opacity: 1;
transform: translate(-50%, 0) scale(1);
transform: scale(1);
}
}
@@ -1,3 +1,52 @@
export function positionToolbar() {
const toolbarElement = document.querySelector('.selection-ai-panel');
if (!toolbarElement) return;
const selection = window.getSelection();
if (selection.isCollapsed) return;
const range = selection.getRangeAt(0);
const rects = range.getClientRects();
if (!rects || rects.length === 0) return;
const firstRect = rects[0];
const combinedRect = range.getBoundingClientRect();
// Find the canvas container (which is the positioned parent)
const canvasElement = document.querySelector('.reader-canvas');
let canvasRect = { top: 0, left: 0 };
let scrollTop = 0;
let scrollLeft = 0;
if (canvasElement) {
canvasRect = canvasElement.getBoundingClientRect();
scrollTop = canvasElement.scrollTop;
scrollLeft = canvasElement.scrollLeft;
}
const toolbarWidth = toolbarElement.offsetWidth;
const toolbarHeight = toolbarElement.offsetHeight;
// Oblicz środek zaznaczenia w poziomie
const left = (combinedRect.left - canvasRect.left) + scrollLeft + (combinedRect.width / 2) - (toolbarWidth / 2);
// Warunek brzegowy (Top Screen Fallback)
const relativeTop = firstRect.top - toolbarHeight - 14;
let top;
if (relativeTop < 0) {
// Pozwól wskoczyć POD zaznaczony tekst
top = (combinedRect.bottom - canvasRect.top) + scrollTop + 12;
toolbarElement.classList.add('below');
} else {
top = (firstRect.top - canvasRect.top) + scrollTop - toolbarHeight - 14;
toolbarElement.classList.remove('below');
}
toolbarElement.style.left = `${left}px`;
toolbarElement.style.top = `${top}px`;
}
export function initSelectionListener(dotNetHelper, container) {
if (!container) return;
@@ -16,29 +65,32 @@ export function initSelectionListener(dotNetHelper, container) {
const blockNode = node.closest('[id]');
if (blockNode) {
const rects = range.getClientRects();
const firstRect = rects && rects.length > 0 ? rects[0] : null;
const lastRect = rects && rects.length > 0 ? rects[rects.length - 1] : null;
const combinedRect = range.getBoundingClientRect();
if (blockNode) {
const rects = range.getClientRects();
const firstRect = rects && rects.length > 0 ? rects[0] : null;
const lastRect = rects && rects.length > 0 ? rects[rects.length - 1] : null;
const combinedRect = range.getBoundingClientRect();
const topVal = firstRect ? firstRect.top : combinedRect.top;
const bottomVal = lastRect ? lastRect.bottom : combinedRect.bottom;
const topVal = firstRect ? firstRect.top : combinedRect.top;
const bottomVal = lastRect ? lastRect.bottom : combinedRect.bottom;
console.log("[SelectionHandler] Selection coords (first/last/combined):", topVal, bottomVal, combinedRect.left);
console.log("[SelectionHandler] Selection coords (first/last/combined):", topVal, bottomVal, combinedRect.left);
dotNetHelper.invokeMethodAsync('HandleTextSelected',
text,
blockNode.id,
{
Top: topVal,
Left: combinedRect.left,
Width: combinedRect.width,
Height: combinedRect.height,
Bottom: bottomVal,
ViewportWidth: window.innerWidth
});
}
dotNetHelper.invokeMethodAsync('HandleTextSelected',
text,
blockNode.id,
{
Top: topVal,
Left: combinedRect.left,
Width: combinedRect.width,
Height: combinedRect.height,
Bottom: bottomVal,
ViewportWidth: window.innerWidth
});
// Reposition the toolbar if already present
setTimeout(positionToolbar, 0);
}
} else {
dotNetHelper.invokeMethodAsync('HandleSelectionCleared');
}
@@ -48,3 +100,4 @@ export function initSelectionListener(dotNetHelper, container) {
document.addEventListener('selectionchange', handleSelection);
container.addEventListener('mouseup', () => setTimeout(handleSelection, 10));
}