feat: add PWA sidebar edge swipe

This commit is contained in:
AJV20 committed 2026-05-24 15:14:28 -04:00
1 parent 3a98700a6b
commit b0f7a7bdff
3 files changed
+112

No files matched your search

+4
View File
@@ -3,6 +3,10 @@
## [Unreleased]
### Added
- Installed/mobile PWA sessions now support an edge swipe from the left side of the screen to open the mobile sidebar drawer, while preserving the existing hamburger and overlay controls.
## [v0.51.129] — 2026-05-24 — Release DA (stage-batch11 — 4-PR feature + perf batch)
### Performance
+65
View File
@@ -228,6 +228,71 @@ function closeMobileSidebar(){
if(overlay)overlay.classList.remove('visible');
}
const _PWA_SIDEBAR_SWIPE_EDGE=28;
const _PWA_SIDEBAR_SWIPE_TRIGGER=72;
const _PWA_SIDEBAR_SWIPE_MAX_VERTICAL=48;
let _pwaSidebarSwipe=null;
function _isPwaStandalone(){
try{
return document.documentElement.classList.contains('pwa-standalone')
|| window.matchMedia('(display-mode: standalone)').matches
|| window.navigator.standalone===true;
}catch(_){return false;}
}
function _isInteractiveSwipeTarget(target){
try{return !!(target&&target.closest&&target.closest('input,textarea,select,button,a,[contenteditable="true"],.topbar-chips,.composer-left,.sidebar,.rightpanel'));}
catch(_){return false;}
}
function _openMobileSidebarFromGesture(){
if(_isDesktopWidth())return;
const sidebar=document.querySelector('.sidebar');
const overlay=$('mobileOverlay');
if(!sidebar)return;
const layout=document.querySelector('.layout');
if(layout)layout.classList.remove('sidebar-collapsed');
sidebar.classList.remove('sidebar-collapsed');
try{document.documentElement.removeAttribute('data-sidebar-collapsed');}catch(_){}
sidebar.classList.add('mobile-open');
if(overlay)overlay.classList.add('visible');
}
function _onPwaSidebarSwipeStart(e){
if(!_isPwaStandalone()||_isDesktopWidth())return;
if(e.pointerType==='mouse'||(e.pointerType&&e.pointerType!=='touch'&&e.pointerType!=='pen'))return;
if(document.querySelector('.sidebar')?.classList.contains('mobile-open'))return;
const clientX=Number(e.clientX)||0;
if(clientX>_PWA_SIDEBAR_SWIPE_EDGE)return;
if(_isInteractiveSwipeTarget(e.target))return;
_pwaSidebarSwipe={startX:clientX,startY:Number(e.clientY)||0,active:true,opened:false};
}
function _onPwaSidebarSwipeMove(e){
const swipe=_pwaSidebarSwipe;
if(!swipe||!swipe.active||swipe.opened)return;
const dx=(Number(e.clientX)||0)-swipe.startX;
const dy=(Number(e.clientY)||0)-swipe.startY;
if(dx<0||Math.abs(dy)>_PWA_SIDEBAR_SWIPE_MAX_VERTICAL*1.5){_pwaSidebarSwipe=null;return;}
if(dx>=_PWA_SIDEBAR_SWIPE_TRIGGER&&Math.abs(dy)<=_PWA_SIDEBAR_SWIPE_MAX_VERTICAL&&dx>Math.abs(dy)*1.5){
if(e.cancelable)e.preventDefault();
swipe.opened=true;
_openMobileSidebarFromGesture();
}
}
function _onPwaSidebarSwipeEnd(){_pwaSidebarSwipe=null;}
function _onPwaSidebarSwipeCancel(){_pwaSidebarSwipe=null;}
function _installPwaSidebarSwipeGesture(){
window.addEventListener('pointerdown', _onPwaSidebarSwipeStart, {passive:true});
window.addEventListener('pointermove', _onPwaSidebarSwipeMove, {passive:false});
window.addEventListener('pointerup', _onPwaSidebarSwipeEnd, {passive:true});
window.addEventListener('pointercancel', _onPwaSidebarSwipeCancel, {passive:true});
}
_installPwaSidebarSwipeGesture();
// ── Desktop sidebar collapse toggle ────────────────────────────────────────
// Two discoverability paths into the same state:
// (1) Click the already-active rail icon → collapse / expand the sidebar.
+43
View File
@@ -0,0 +1,43 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
BOOT_JS = (ROOT / "static" / "boot.js").read_text(encoding="utf-8")
STYLE_CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
def test_pwa_edge_swipe_gesture_is_registered_for_mobile_sidebar():
assert "function _installPwaSidebarSwipeGesture" in BOOT_JS
assert "window.addEventListener('pointerdown', _onPwaSidebarSwipeStart" in BOOT_JS
assert "window.addEventListener('pointermove', _onPwaSidebarSwipeMove" in BOOT_JS
assert "window.addEventListener('pointerup', _onPwaSidebarSwipeEnd" in BOOT_JS
assert "window.addEventListener('pointercancel', _onPwaSidebarSwipeCancel" in BOOT_JS
def test_pwa_sidebar_swipe_is_edge_gated_standalone_and_horizontal():
assert "_isPwaStandalone()" in BOOT_JS
assert "_PWA_SIDEBAR_SWIPE_EDGE" in BOOT_JS
assert "_PWA_SIDEBAR_SWIPE_TRIGGER" in BOOT_JS
assert "_PWA_SIDEBAR_SWIPE_MAX_VERTICAL" in BOOT_JS
assert "clientX>_PWA_SIDEBAR_SWIPE_EDGE" in BOOT_JS.replace(" ", "")
assert "dx>=_PWA_SIDEBAR_SWIPE_TRIGGER" in BOOT_JS.replace(" ", "")
assert "Math.abs(dy)<=_PWA_SIDEBAR_SWIPE_MAX_VERTICAL" in BOOT_JS.replace(" ", "")
assert "dx>Math.abs(dy)*1.5" in BOOT_JS.replace(" ", "")
assert "input,textarea,select,button,a,[contenteditable=\"true\"],.topbar-chips,.composer-left,.sidebar,.rightpanel" in BOOT_JS
assert ".messages" not in BOOT_JS[BOOT_JS.find("function _isInteractiveSwipeTarget"):BOOT_JS.find("function _openMobileSidebarFromGesture")]
def test_pwa_sidebar_swipe_opens_existing_mobile_drawer_without_desktop_collapse():
assert "_openMobileSidebarFromGesture" in BOOT_JS
assert "sidebar.classList.remove('sidebar-collapsed')" in BOOT_JS
assert "sidebar.classList.add('mobile-open')" in BOOT_JS
assert "overlay.classList.add('visible')" in BOOT_JS
assert "toggleSidebar(" not in BOOT_JS[BOOT_JS.find("function _openMobileSidebarFromGesture"):BOOT_JS.find("function _installPwaSidebarSwipeGesture")]
def test_pwa_sidebar_swipe_does_not_disable_horizontal_scrollers_globally():
compact = STYLE_CSS.replace(" ", "")
assert "html{touch-action" not in compact
assert "body{touch-action" not in compact
assert ".layout{touch-action" not in compact