From 131981992c0b2869196837eaf497e0cf5b88fae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Jasi=C5=84ski?= Date: Mon, 27 Apr 2026 18:33:32 +0200 Subject: [PATCH] feat: implement draggable sidebar resizer with persistent state and dynamic UI updates --- .../Layout/MainLayout.razor | 16 +++++ .../Layout/MainLayout.razor.css | 32 ++++++++-- .../wwwroot/js/knowledgeGraph.js | 20 ++++++- .../wwwroot/js/layoutResizer.js | 59 +++++++++++++++++++ 4 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 src/NexusReader.UI.Shared/wwwroot/js/layoutResizer.js diff --git a/src/NexusReader.UI.Shared/Layout/MainLayout.razor b/src/NexusReader.UI.Shared/Layout/MainLayout.razor index d54642b..8fe2dd1 100644 --- a/src/NexusReader.UI.Shared/Layout/MainLayout.razor +++ b/src/NexusReader.UI.Shared/Layout/MainLayout.razor @@ -6,6 +6,7 @@ @inject IPlatformService PlatformService @inject IFocusModeService FocusMode @inject IQuizStateService QuizService +@inject IJSRuntime JS @implements IDisposable
@@ -16,6 +17,8 @@
+ +
@@ -58,6 +61,19 @@ } } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + try + { + var module = await JS.InvokeAsync("import", "./_content/NexusReader.UI.Shared/js/layoutResizer.js"); + await module.InvokeVoidAsync("initResizer", ".app-container", "#sidebar-resizer", "--sidebar-width"); + } + catch { } + } + } + public void Dispose() { FocusMode.OnFocusModeChanged -= StateHasChanged; diff --git a/src/NexusReader.UI.Shared/Layout/MainLayout.razor.css b/src/NexusReader.UI.Shared/Layout/MainLayout.razor.css index 36c8967..b876b85 100644 --- a/src/NexusReader.UI.Shared/Layout/MainLayout.razor.css +++ b/src/NexusReader.UI.Shared/Layout/MainLayout.razor.css @@ -1,11 +1,10 @@ .app-container { display: grid; - grid-template-columns: 1fr 450px; + grid-template-columns: 1fr auto var(--sidebar-width, 450px); width: 100vw; height: 100vh; overflow: hidden; background: #121212; - transition: grid-template-columns 0.4s cubic-bezier(0.4, 0, 0.2, 1); } @@ -29,26 +28,47 @@ main { .intelligence-sidebar { display: grid; grid-template-columns: 50px 1fr; - width: 450px; + width: 100%; /* controlled by grid */ height: 100%; background: #0d0d0d; box-shadow: -10px 0 30px rgba(0, 0, 0, 0.3); border-left: 1px solid rgba(255, 255, 255, 0.05); - transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); overflow: hidden; 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 { - grid-template-columns: 1fr 50px; + grid-template-columns: 1fr 0px 50px; } .app-container.focus-mode-active .intelligence-sidebar { - width: 50px; grid-template-columns: 50px 0px; } +.app-container.focus-mode-active .resizer { + display: none; +} + .app-container.focus-mode-active .intelligence-content { opacity: 0; pointer-events: none; diff --git a/src/NexusReader.UI.Shared/wwwroot/js/knowledgeGraph.js b/src/NexusReader.UI.Shared/wwwroot/js/knowledgeGraph.js index eb46cd1..4b9046b 100644 --- a/src/NexusReader.UI.Shared/wwwroot/js/knowledgeGraph.js +++ b/src/NexusReader.UI.Shared/wwwroot/js/knowledgeGraph.js @@ -4,7 +4,7 @@ let simulation; let zoomBehavior; 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) { const container = document.getElementById(containerId); @@ -66,6 +66,9 @@ export function mount(containerId, data, dotNetHelper) { svgElement.call(zoomBehavior).on("wheel.zoom", null); + resizeHandler = () => handleResize(containerId); + window.addEventListener('resize', resizeHandler); + simulation = d3.forceSimulation() .force("link", d3.forceLink().id(d => d.id).distance(120)) .force("charge", d3.forceManyBody().strength(-400)) @@ -228,12 +231,27 @@ export function unmount(containerId) { if (simulation) { simulation.stop(); } + if (resizeHandler) { + window.removeEventListener('resize', resizeHandler); + } const container = document.getElementById(containerId); if (container) { 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) { const el = document.getElementById(id); if (el) { diff --git a/src/NexusReader.UI.Shared/wwwroot/js/layoutResizer.js b/src/NexusReader.UI.Shared/wwwroot/js/layoutResizer.js new file mode 100644 index 0000000..79f862a --- /dev/null +++ b/src/NexusReader.UI.Shared/wwwroot/js/layoutResizer.js @@ -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')); + } + }); +}