style(ui): refactor reader layout grid, fix focus mode layout collapse, fix SVG rendering dots, reorganize intelligence toolbar #69
@@ -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;
|
||||
|
||||
@@ -38,6 +87,9 @@ export function initSelectionListener(dotNetHelper, container) {
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user