feat: implement draggable sidebar resizer with persistent state and dynamic UI updates

This commit is contained in:
2026-04-27 18:33:32 +02:00
parent 39a9ca5706
commit 131981992c
4 changed files with 120 additions and 7 deletions
@@ -6,6 +6,7 @@
@inject IPlatformService PlatformService @inject IPlatformService PlatformService
@inject IFocusModeService FocusMode @inject IFocusModeService FocusMode
@inject IQuizStateService QuizService @inject IQuizStateService QuizService
@inject IJSRuntime JS
@implements IDisposable @implements IDisposable
<div class="app-container @_platformClass @(FocusMode.IsFocusModeActive ? "focus-mode-active" : "")"> <div class="app-container @_platformClass @(FocusMode.IsFocusModeActive ? "focus-mode-active" : "")">
@@ -16,6 +17,8 @@
<ReaderFooter /> <ReaderFooter />
</div> </div>
<div class="resizer" id="sidebar-resizer"></div>
<div class="intelligence-sidebar"> <div class="intelligence-sidebar">
<IntelligenceToolbar /> <IntelligenceToolbar />
<div class="intelligence-content"> <div class="intelligence-content">
@@ -58,6 +61,19 @@
} }
} }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
try
{
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/layoutResizer.js");
await module.InvokeVoidAsync("initResizer", ".app-container", "#sidebar-resizer", "--sidebar-width");
}
catch { }
}
}
public void Dispose() public void Dispose()
{ {
FocusMode.OnFocusModeChanged -= StateHasChanged; FocusMode.OnFocusModeChanged -= StateHasChanged;
@@ -1,11 +1,10 @@
.app-container { .app-container {
display: grid; display: grid;
grid-template-columns: 1fr 450px; grid-template-columns: 1fr auto var(--sidebar-width, 450px);
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
background: #121212; background: #121212;
transition: grid-template-columns 0.4s cubic-bezier(0.4, 0, 0.2, 1);
} }
@@ -29,26 +28,47 @@ main {
.intelligence-sidebar { .intelligence-sidebar {
display: grid; display: grid;
grid-template-columns: 50px 1fr; grid-template-columns: 50px 1fr;
width: 450px; width: 100%; /* controlled by grid */
height: 100%; height: 100%;
background: #0d0d0d; background: #0d0d0d;
box-shadow: -10px 0 30px rgba(0, 0, 0, 0.3); box-shadow: -10px 0 30px rgba(0, 0, 0, 0.3);
border-left: 1px solid rgba(255, 255, 255, 0.05); border-left: 1px solid rgba(255, 255, 255, 0.05);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden; overflow: hidden;
z-index: 10; z-index: 10;
} }
.resizer {
width: 4px;
cursor: col-resize;
background: rgba(255, 255, 255, 0.02);
transition: background 0.2s, width 0.2s;
z-index: 20;
border-left: 1px solid rgba(255, 255, 255, 0.05);
}
.resizer:hover, .app-container.is-resizing .resizer {
background: var(--nexus-neon);
width: 6px;
box-shadow: 0 0 10px var(--nexus-neon);
}
.app-container.is-resizing {
user-select: none;
}
.app-container.focus-mode-active { .app-container.focus-mode-active {
grid-template-columns: 1fr 50px; grid-template-columns: 1fr 0px 50px;
} }
.app-container.focus-mode-active .intelligence-sidebar { .app-container.focus-mode-active .intelligence-sidebar {
width: 50px;
grid-template-columns: 50px 0px; grid-template-columns: 50px 0px;
} }
.app-container.focus-mode-active .resizer {
display: none;
}
.app-container.focus-mode-active .intelligence-content { .app-container.focus-mode-active .intelligence-content {
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
@@ -4,7 +4,7 @@ let simulation;
let zoomBehavior; let zoomBehavior;
let svgElement; let svgElement;
let node, link, rootGroup, badge, width, height, currentDotNetHelper; let node, link, rootGroup, badge, width, height, currentDotNetHelper, resizeHandler;
export function mount(containerId, data, dotNetHelper) { export function mount(containerId, data, dotNetHelper) {
const container = document.getElementById(containerId); const container = document.getElementById(containerId);
@@ -66,6 +66,9 @@ export function mount(containerId, data, dotNetHelper) {
svgElement.call(zoomBehavior).on("wheel.zoom", null); svgElement.call(zoomBehavior).on("wheel.zoom", null);
resizeHandler = () => handleResize(containerId);
window.addEventListener('resize', resizeHandler);
simulation = d3.forceSimulation() simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(d => d.id).distance(120)) .force("link", d3.forceLink().id(d => d.id).distance(120))
.force("charge", d3.forceManyBody().strength(-400)) .force("charge", d3.forceManyBody().strength(-400))
@@ -228,12 +231,27 @@ export function unmount(containerId) {
if (simulation) { if (simulation) {
simulation.stop(); simulation.stop();
} }
if (resizeHandler) {
window.removeEventListener('resize', resizeHandler);
}
const container = document.getElementById(containerId); const container = document.getElementById(containerId);
if (container) { if (container) {
container.innerHTML = ''; // clear svg container.innerHTML = ''; // clear svg
} }
} }
export function handleResize(containerId) {
const container = document.getElementById(containerId);
if (!container || !svgElement || !simulation) return;
width = container.clientWidth;
height = container.clientHeight;
svgElement.attr("viewBox", [0, 0, width, height]);
simulation.force("center", d3.forceCenter(width / 2, height / 2));
simulation.alpha(0.3).restart();
}
export function scrollToNode(id) { export function scrollToNode(id) {
const el = document.getElementById(id); const el = document.getElementById(id);
if (el) { if (el) {
@@ -0,0 +1,59 @@
/**
* Handles resizing of the main application layout panes.
*/
export function initResizer(containerSelector, resizerSelector, variableName) {
const container = document.querySelector(containerSelector);
const resizer = document.querySelector(resizerSelector);
if (!container || !resizer) return;
const storageKey = 'nexus-sidebar-width';
let isResizing = false;
// Load initial width
const savedWidth = localStorage.getItem(storageKey);
if (savedWidth) {
container.style.setProperty(variableName, savedWidth);
// Delay a bit to ensure components are mounted before triggering resize
setTimeout(() => window.dispatchEvent(new Event('resize')), 100);
}
resizer.addEventListener('mousedown', (e) => {
isResizing = true;
document.body.style.cursor = 'col-resize';
container.classList.add('is-resizing');
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
// Calculate new width for the right panel
// container width - mouse X position
const containerRect = container.getBoundingClientRect();
const newWidth = containerRect.right - e.clientX;
// Constraints
const minWidth = 300;
const maxWidth = containerRect.width * 0.7;
if (newWidth >= minWidth && newWidth <= maxWidth) {
container.style.setProperty(variableName, `${newWidth}px`);
}
});
document.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
document.body.style.cursor = '';
container.classList.remove('is-resizing');
// Save width
const currentWidth = container.style.getPropertyValue(variableName);
localStorage.setItem(storageKey, currentWidth);
// Dispatch a window resize event to notify components like D3
window.dispatchEvent(new Event('resize'));
}
});
}