Polish mobile dashboard sidebar drawer UX
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s

Refactors the dashboard’s mobile sidebar into a modal-style drawer with a coordinated state helper, scrim overlay, and dedicated close button. Adds accessibility improvements (`aria-label`, `aria-expanded`) and stronger dismissal behavior (scrim tap, close button, Escape, and swipe-left with velocity/threshold handling). Updates mobile CSS for safe-area-aware positioning, improved touch targets/pressed states, frosted toggle styling, drawer shadows/transitions, and drag-state handling for smoother interactions.
This commit is contained in:
jelveh
2026-07-23 18:56:43 -07:00
parent 2982e894b1
commit 9877bb79d0
2 changed files with 253 additions and 40 deletions
+99 -9
View File
@@ -92,10 +92,13 @@ async function UIDashboard (options) {
h += '<div class="dashboard">';
// Mobile sidebar toggle
h += '<button class="dashboard-sidebar-toggle">';
h += '<button class="dashboard-sidebar-toggle" aria-label="Open menu" aria-expanded="false">';
h += '<span></span><span></span><span></span>';
h += '</button>';
// Scrim behind the mobile drawer; tap closes it
h += '<div class="dashboard-sidebar-scrim"></div>';
// Sidebar
h += '<div class="dashboard-sidebar hide-scrollbar">';
// Sidebar header with logo and collapse toggle
@@ -105,6 +108,10 @@ async function UIDashboard (options) {
h += `<img class="sidebar-toggle-close" src="${window.icons['sidebar-close.svg']}">`;
h += `<img class="sidebar-toggle-open" src="${window.icons['sidebar-open.svg']}">`;
h += '</button>';
// Mobile-only close button (the collapse toggle is desktop-only)
h += '<button class="dashboard-sidebar-close" aria-label="Close menu">';
h += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>';
h += '</button>';
h += '</div>';
// Navigation items container
h += '<div class="dashboard-sidebar-nav">';
@@ -491,16 +498,100 @@ async function UIDashboard (options) {
$el_window.find('.dashboard-content').scrollTop(0);
// Close sidebar on mobile after selection
$el_window.find('.dashboard-sidebar').removeClass('open');
$el_window.find('.dashboard-sidebar-toggle').removeClass('open');
setMobileSidebar(false);
});
// Mobile toggle handler
// =========================================================================
// Mobile drawer
// The sidebar becomes a modal drawer below 768px: scrim behind it, close
// button in its header, Escape and swipe-left dismiss it. One helper keeps
// the drawer, scrim, and toggle (incl. aria-expanded) in sync.
// =========================================================================
const $sidebar = $el_window.find('.dashboard-sidebar');
const $scrim = $el_window.find('.dashboard-sidebar-scrim');
const $sidebar_toggle = $el_window.find('.dashboard-sidebar-toggle');
const setMobileSidebar = (open) => {
$sidebar.toggleClass('open', open);
$scrim.toggleClass('open', open);
$sidebar_toggle.toggleClass('open', open).attr('aria-expanded', open ? 'true' : 'false');
};
$el_window.on('click', '.dashboard-sidebar-toggle', function () {
$(this).toggleClass('open');
$el_window.find('.dashboard-sidebar').toggleClass('open');
setMobileSidebar(!$sidebar.hasClass('open'));
});
$el_window.on('click', '.dashboard-sidebar-close, .dashboard-sidebar-scrim', function () {
setMobileSidebar(false);
});
$(document).on('keydown.dashboard-mobile-sidebar', function (e) {
if ( e.key === 'Escape' && $sidebar.hasClass('open') ) {
setMobileSidebar(false);
}
});
// Swipe-to-close: the drawer tracks a leftward drag 1:1 (scrim fading in
// step), then commits past 35% width or a quick flick, else snaps back.
// Transitions are suspended during the drag (.dragging) so releasing
// animates from the finger's last position, not from fully-open.
{
const sidebar = $sidebar[0];
const scrim = $scrim[0];
let startX = 0, startY = 0, dx = 0, lastX = 0, lastT = 0, velocity = 0;
let tracking = false, axis = null;
sidebar.addEventListener('touchstart', (e) => {
if ( ! sidebar.classList.contains('open') ) return;
const t = e.touches[0];
startX = lastX = t.clientX;
startY = t.clientY;
lastT = e.timeStamp;
dx = 0; velocity = 0; axis = null;
tracking = true;
}, { passive: true });
sidebar.addEventListener('touchmove', (e) => {
if ( ! tracking ) return;
const t = e.touches[0];
const moveX = t.clientX - startX;
const moveY = t.clientY - startY;
// Lock to an axis on the first meaningful movement so vertical
// scrolling inside the drawer never drags it sideways.
if ( axis === null && (Math.abs(moveX) > 8 || Math.abs(moveY) > 8) ) {
axis = Math.abs(moveX) > Math.abs(moveY) ? 'x' : 'y';
if ( axis === 'x' ) {
sidebar.classList.add('dragging');
scrim.classList.add('dragging');
}
}
if ( axis !== 'x' ) return;
dx = Math.min(0, moveX);
const dt = e.timeStamp - lastT;
if ( dt > 0 ) velocity = (t.clientX - lastX) / dt;
lastX = t.clientX;
lastT = e.timeStamp;
sidebar.style.transform = `translateX(${dx}px)`;
scrim.style.opacity = Math.max(0, 1 + dx / sidebar.offsetWidth);
}, { passive: true });
const endDrag = () => {
if ( ! tracking ) return;
tracking = false;
const shouldClose = axis === 'x'
&& (dx < -sidebar.offsetWidth * 0.35 || velocity < -0.4);
// Re-enable transitions and drop inline styles in the same frame
// as the class change so the drawer animates from where it is.
sidebar.classList.remove('dragging');
scrim.classList.remove('dragging');
sidebar.style.transform = '';
scrim.style.opacity = '';
if ( shouldClose ) setMobileSidebar(false);
axis = null; dx = 0;
};
sidebar.addEventListener('touchend', endDrag);
sidebar.addEventListener('touchcancel', endDrag);
}
// Desktop sidebar collapse toggle
$el_window.on('click', '.dashboard-sidebar-collapse-toggle', function () {
const $sidebar = $el_window.find('.dashboard-sidebar');
@@ -528,9 +619,8 @@ async function UIDashboard (options) {
$el_window.on('mousedown touchstart', function (e) {
if ( !$(e.target).closest('.dashboard-sidebar').length
&& !$(e.target).closest('.dashboard-sidebar-toggle').length
&& $el_window.find('.dashboard-sidebar').hasClass('open') ) {
$el_window.find('.dashboard-sidebar').removeClass('open');
$el_window.find('.dashboard-sidebar-toggle').removeClass('open');
&& $sidebar.hasClass('open') ) {
setMobileSidebar(false);
}
});
+154 -31
View File
@@ -85,6 +85,8 @@
--dashboard-shadow-light: rgba(0, 0, 0, 0.06);
--dashboard-shadow-medium: rgba(0, 0, 0, 0.1);
--dashboard-shadow-overlay: rgba(0, 0, 0, 0.5);
--dashboard-scrim: rgba(15, 18, 24, 0.42);
--dashboard-drawer-shadow: 0 0 0 1px rgba(0, 0, 0, 0.04), 24px 0 64px rgba(0, 0, 0, 0.24);
--dashboard-gradient-indigo: rgba(99, 102, 241, 0.08);
--dashboard-gradient-purple: rgba(168, 85, 247, 0.06);
@@ -180,6 +182,8 @@ body {
--dashboard-shadow-light: rgba(0, 0, 0, 0.3);
--dashboard-shadow-medium: rgba(0, 0, 0, 0.4);
--dashboard-shadow-overlay: rgba(0, 0, 0, 0.7);
--dashboard-scrim: rgba(0, 0, 0, 0.6);
--dashboard-drawer-shadow: 0 0 0 1px rgba(255, 255, 255, 0.06), 24px 0 64px rgba(0, 0, 0, 0.55);
--dashboard-gradient-indigo: rgba(99, 102, 241, 0.15);
--dashboard-gradient-purple: rgba(168, 85, 247, 0.12);
@@ -365,11 +369,43 @@ body {
opacity: 1;
}
/* Hide desktop collapse toggle on mobile */
/* Mobile drawer close button — hidden on desktop, where the collapse toggle
occupies the same header slot */
.dashboard-sidebar-close {
display: none;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
background: none;
border-radius: 8px;
color: var(--dashboard-text-muted);
cursor: pointer;
flex-shrink: 0;
-webkit-tap-highlight-color: transparent;
transition: background-color 0.15s, color 0.15s;
}
.dashboard-sidebar-close svg {
width: 20px;
height: 20px;
}
.dashboard-sidebar-close:active {
background: var(--dashboard-hover);
color: var(--dashboard-text);
}
/* Hide desktop collapse toggle on mobile; show the drawer close button */
@media (max-width: 768px) {
.dashboard-sidebar-collapse-toggle {
display: none;
}
.dashboard-sidebar-close {
display: inline-flex;
}
}
.dashboard-sidebar-nav {
@@ -1366,6 +1402,12 @@ body.myapps-reordering .myapps-tile {
.myapps-drag-ghost {
transition: none;
}
.dashboard-sidebar,
.dashboard-sidebar-scrim,
.dashboard-sidebar-toggle {
transition: none;
}
}
/* Dashboard files */
@@ -2518,76 +2560,157 @@ body.myapps-reordering .myapps-tile {
margin: 4px 0;
}
/* Mobile sidebar toggle */
/* Mobile sidebar toggle — frosted so scrolling content reads through it,
aligned to the 16px content gutter */
.dashboard-sidebar-toggle {
display: none;
position: fixed;
top: 12px;
left: 12px;
top: max(12px, env(safe-area-inset-top));
left: max(16px, env(safe-area-inset-left));
z-index: 100;
width: 35px;
height: 35px;
background: var(--dashboard-background);
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.8);
-webkit-backdrop-filter: blur(16px) saturate(180%);
backdrop-filter: blur(16px) saturate(180%);
border: 1px solid var(--dashboard-border);
border-radius: 6px;
border-radius: 10px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 4px 16px rgba(0, 0, 0, 0.06);
cursor: pointer;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
-webkit-tap-highlight-color: transparent;
transition: opacity 0.2s ease, transform 0.2s ease;
}
/* Extend the tap target to ~52px without growing the button visually */
.dashboard-sidebar-toggle::after {
content: '';
position: absolute;
inset: -6px;
border-radius: inherit;
}
.dashboard-sidebar-toggle:active {
transform: scale(0.94);
}
/* The open drawer covers the toggle; fade it so it can't peek out from
behind the drawer's rounded corner or catch stray taps */
.dashboard-sidebar-toggle.open {
opacity: 0;
transform: scale(0.9);
pointer-events: none;
}
.dashboard-sidebar-toggle span {
display: block;
width: 18px;
width: 16px;
height: 2px;
background: var(--dashboard-text);
border-radius: 1px;
transition: transform 0.2s, opacity 0.2s;
}
.dashboard-sidebar-toggle.open span:nth-child(1) {
transform: rotate(45deg) translate(4px, 4px);
@media (prefers-color-scheme: dark) {
.dashboard-sidebar-toggle {
background: rgba(37, 37, 37, 0.8);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 4px 16px rgba(0, 0, 0, 0.3);
}
}
.dashboard-sidebar-toggle.open span:nth-child(2) {
opacity: 0;
/* Scrim behind the mobile drawer */
.dashboard-sidebar-scrim {
display: none;
}
.dashboard-sidebar-toggle.open span:nth-child(3) {
transform: rotate(-45deg) translate(4px, -4px);
}
/* Responsive: tablet and below */
/* Responsive: tablet and below — the sidebar becomes a modal drawer */
@media (max-width: 768px) {
.dashboard-sidebar-header {
padding-left: 45px;
border-bottom: 0;
margin-bottom: 2px;
margin-top: 3px;
}
.dashboard-sidebar-header .dashboard-sidebar-logo {
margin-left: auto;
padding: 2px 2px 12px 6px;
}
.dashboard-sidebar-toggle {
display: flex;
}
.dashboard-sidebar-scrim {
display: block;
position: fixed;
inset: 0;
z-index: 98;
background: var(--dashboard-scrim);
opacity: 0;
pointer-events: none;
touch-action: none;
transition: opacity 0.32s cubic-bezier(0.32, 0.72, 0, 1);
}
.dashboard-sidebar-scrim.open {
opacity: 1;
pointer-events: auto;
}
.dashboard-sidebar {
position: fixed;
left: 0;
top: 0;
height: 100%;
bottom: 0;
height: auto;
width: min(304px, calc(100vw - 56px));
min-width: 0;
z-index: 99;
padding: max(16px, env(safe-area-inset-top)) 14px max(14px, env(safe-area-inset-bottom)) calc(14px + env(safe-area-inset-left));
border-right: none;
border-radius: 0 16px 16px 0;
transform: translateX(-110%);
transition: transform 0.2s ease;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
visibility: hidden;
box-shadow: var(--dashboard-drawer-shadow);
overscroll-behavior: contain;
/* visibility flips only after the slide-out finishes, keeping the
closed drawer out of the tab order */
transition: transform 0.32s cubic-bezier(0.32, 0.72, 0, 1), visibility 0s linear 0.32s;
}
.dashboard-sidebar.open {
transform: translateX(0);
visibility: visible;
transition: transform 0.32s cubic-bezier(0.32, 0.72, 0, 1);
}
/* Mid-swipe: the drawer follows the finger, so no transitions */
.dashboard-sidebar.dragging,
.dashboard-sidebar-scrim.dragging {
transition: none;
}
/* Roomier touch targets inside the drawer, with pressed-state feedback
in place of hover */
.dashboard-sidebar-item {
padding: 12px;
gap: 12px;
font-size: 15px;
margin-bottom: 2px;
border-radius: 10px;
-webkit-tap-highlight-color: transparent;
}
.dashboard-sidebar-item svg {
width: 20px;
height: 20px;
}
.dashboard-sidebar-item:active {
background: var(--dashboard-hover);
}
.dashboard-user-btn {
-webkit-tap-highlight-color: transparent;
}
.dashboard-user-btn:active {
background: var(--dashboard-hover);
}
.dashboard-content {
@@ -2600,7 +2723,7 @@ body.myapps-reordering .myapps-tile {
the first folder row (the column is hidden below 480px, so this only
affects the tablet range). */
.dashboard-section-files .directories {
padding-top: 52px;
padding-top: 60px;
}
.myapps-search-wrap {