feat: implement draggable sidebar resizer with persistent state and dynamic UI updates
This commit is contained in:
@@ -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'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user