Improve splitter UX: visual feedback & smoother resizing

Enhanced the editor's vertical splitter with immediate visual feedback during dragging, including dynamic width, shadow, and handle size. Updated logic to re-render UI and save split ratio in real time. Refined save status updates to reduce unnecessary renders. Improved mobile responsiveness and overall user experience.
This commit is contained in:
Michal Vrba
2026-01-31 13:23:29 +01:00
parent 5a06a307fb
commit efc31f643d
3 changed files with 61 additions and 20 deletions
+31 -17
View File
@@ -82,7 +82,7 @@
<!-- Simple Splitter --> <!-- Simple Splitter -->
@if (showPreview) @if (showPreview)
{ {
<div class="splitter" @onmousedown="OnSplitterMouseDown" @onmousedown:preventDefault="true"> <div class="splitter @(isDragging ? "dragging" : "")" @onmousedown="OnSplitterMouseDown" @onmousedown:preventDefault="true">
<div class="splitter-handle"></div> <div class="splitter-handle"></div>
</div> </div>
} }
@@ -156,11 +156,11 @@
await InvokeAsync(async () => await AutoSave()); await InvokeAsync(async () => await AutoSave());
}, null, 2000, Timeout.Infinite); // Increased to 2 seconds for production }, null, 2000, Timeout.Infinite); // Increased to 2 seconds for production
// Don't show "Saving..." immediately, wait for actual save // Only update status if it was previously "Saved"
if (saveStatus == "Saved") if (saveStatus == "Saved")
{ {
saveStatus = "Modified"; saveStatus = "Modified";
await InvokeAsync(StateHasChanged); StateHasChanged();
} }
} }
@@ -177,9 +177,11 @@
if (string.IsNullOrEmpty(sessionId) || !hasUnsavedChanges) if (string.IsNullOrEmpty(sessionId) || !hasUnsavedChanges)
return; return;
var previousStatus = saveStatus;
// Set saving status only when actually starting to save // Set saving status only when actually starting to save
saveStatus = "Saving..."; saveStatus = "Saving...";
await InvokeAsync(StateHasChanged); StateHasChanged(); // Use StateHasChanged instead of InvokeAsync
try try
{ {
@@ -194,7 +196,7 @@
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
saveStatus = "Timeout"; saveStatus = "Timeout";
// Retry after 5 seconds // Simplified retry - no immediate re-rendering
_ = Task.Delay(5000).ContinueWith(async _ => _ = Task.Delay(5000).ContinueWith(async _ =>
{ {
if (hasUnsavedChanges) if (hasUnsavedChanges)
@@ -206,7 +208,7 @@
catch (Exception) catch (Exception)
{ {
saveStatus = "Error"; saveStatus = "Error";
// Retry after 3 seconds // Simplified retry - no immediate re-rendering
_ = Task.Delay(3000).ContinueWith(async _ => _ = Task.Delay(3000).ContinueWith(async _ =>
{ {
if (hasUnsavedChanges) if (hasUnsavedChanges)
@@ -216,7 +218,11 @@
}); });
} }
await InvokeAsync(StateHasChanged); // Only trigger re-render if status actually changed
if (saveStatus != previousStatus)
{
StateHasChanged();
}
} }
private void TogglePreview() private void TogglePreview()
@@ -396,25 +402,33 @@
{ {
if (!isDragging || containerWidth <= 0) return; if (!isDragging || containerWidth <= 0) return;
var newRatio = clientX / containerWidth; var newRatio = Math.Max(0.2, Math.Min(0.8, clientX / containerWidth)); // Clamp between 20% and 80%
splitRatio = Math.Max(0.2, Math.Min(0.8, newRatio)); // Clamp between 20% and 80%
// Save to localStorage // Update immediately for smooth dragging
splitRatio = newRatio;
// Use StateHasChanged for immediate visual feedback
StateHasChanged();
}
[JSInvokable]
public async Task StopDragging()
{
if (!isDragging) return;
isDragging = false;
// Save split ratio to localStorage when dragging stops
try try
{ {
await JSRuntime.InvokeVoidAsync("localStorage.setItem", "notemark-split-ratio", splitRatio.ToString()); await JSRuntime.InvokeVoidAsync("localStorage.setItem", "notemark-split-ratio", splitRatio.ToString("F2"));
} }
catch catch
{ {
// Ignore localStorage errors // Ignore localStorage errors
} }
// Single StateHasChanged at the end
StateHasChanged(); StateHasChanged();
} }
[JSInvokable]
public void StopDragging()
{
isDragging = false;
}
} }
+17 -2
View File
@@ -438,11 +438,18 @@
justify-content: center; justify-content: center;
position: relative; position: relative;
flex-shrink: 0; flex-shrink: 0;
transition: background-color 0.2s ease; transition: all 0.15s ease;
} }
.splitter:hover { .splitter:hover {
background-color: var(--accent); background-color: var(--accent);
width: 10px;
}
.splitter.dragging {
background-color: var(--accent);
width: 12px;
box-shadow: 0 0 8px rgba(0, 122, 204, 0.3);
} }
.splitter-handle { .splitter-handle {
@@ -450,11 +457,19 @@
height: 40px; height: 40px;
background-color: var(--secondary-text); background-color: var(--secondary-text);
border-radius: 1px; border-radius: 1px;
transition: background-color 0.2s ease; transition: all 0.15s ease;
} }
.splitter:hover .splitter-handle { .splitter:hover .splitter-handle {
background-color: white; background-color: white;
width: 3px;
height: 50px;
}
.splitter.dragging .splitter-handle {
background-color: white;
width: 4px;
height: 60px;
} }
/* Mobile Responsive - Place at end to override desktop styles */ /* Mobile Responsive - Place at end to override desktop styles */
+13 -1
View File
@@ -59,9 +59,15 @@ window.startSplitterDrag = function(blazorInstance) {
document.addEventListener('mousemove', handleSplitterMove); document.addEventListener('mousemove', handleSplitterMove);
document.addEventListener('mouseup', handleSplitterUp); document.addEventListener('mouseup', handleSplitterUp);
// Prevent text selection // Improve visual feedback during drag
document.body.style.userSelect = 'none'; document.body.style.userSelect = 'none';
document.body.style.cursor = 'col-resize'; document.body.style.cursor = 'col-resize';
// Add dragging class for visual feedback
const splitter = document.querySelector('.splitter');
if (splitter) {
splitter.classList.add('dragging');
}
}; };
function handleSplitterMove(e) { function handleSplitterMove(e) {
@@ -88,6 +94,12 @@ function handleSplitterUp(e) {
document.body.style.cursor = ''; document.body.style.cursor = '';
document.body.style.userSelect = ''; document.body.style.userSelect = '';
// Remove dragging visual state
const splitter = document.querySelector('.splitter');
if (splitter) {
splitter.classList.remove('dragging');
}
// Notify Blazor that dragging stopped // Notify Blazor that dragging stopped
if (splitterState.blazorInstance) { if (splitterState.blazorInstance) {
splitterState.blazorInstance.invokeMethodAsync('StopDragging'); splitterState.blazorInstance.invokeMethodAsync('StopDragging');