diff --git a/CHANGELOG.md b/CHANGELOG.md index b0516d95..68c5b7bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.581] — 2026-06-22 — Release UN (full-screen PWA mobile sidebar drawer) + +### Changed + +- **Improved the mobile/PWA sidebar drawer.** On phones the sidebar is now a full-screen transform drawer that behaves like a dedicated navigation surface: the hamburger and left-edge swipe open the list for the *currently active* module (instead of always jumping back to Sessions), rail taps switch lists within the drawer rather than dismissing it, and the drawer closes automatically after a selection that drives a detail page so you see the result immediately. Inline sidebar controls stay usable without closing the drawer, and the dimming overlay is dropped for this full-screen drawer so the top safe-area/titlebar no longer looks disabled. Thanks @franksong2702. (#4660) + ## [v0.51.580] — 2026-06-22 — Release UM (wrap markdown source previews) ### Fixed diff --git a/static/boot.js b/static/boot.js index fd910685..9e263160 100644 --- a/static/boot.js +++ b/static/boot.js @@ -253,20 +253,23 @@ function syncWorkspacePanelUI(){ function toggleMobileSidebar(){ const sidebar=document.querySelector('.sidebar'); - const overlay=$('mobileOverlay'); if(!sidebar)return; const isOpen=sidebar.classList.contains('mobile-open'); if(isOpen){closeMobileSidebar();} - else{sidebar.classList.add('mobile-open');if(overlay)overlay.classList.add('visible');} + else{ + try{if(typeof _syncMobileSidebarPanelFromMainView==='function')_syncMobileSidebarPanelFromMainView();}catch(_){} + sidebar.classList.remove('mobile-session-page');sidebar.classList.add('mobile-panel-drawer','mobile-open'); + } } function closeMobileSidebar(){ const sidebar=document.querySelector('.sidebar'); const overlay=$('mobileOverlay'); - if(sidebar)sidebar.classList.remove('mobile-open'); + if(sidebar)sidebar.classList.remove('mobile-open','mobile-session-page','mobile-panel-drawer'); if(overlay)overlay.classList.remove('visible'); } const _PWA_SIDEBAR_SWIPE_EDGE=80; +const _PWA_SIDEBAR_SWIPE_CLAIM=10; const _PWA_SIDEBAR_SWIPE_TRIGGER=64; const _PWA_SIDEBAR_SWIPE_MAX_VERTICAL=56; let _pwaSidebarSwipe=null; @@ -284,35 +287,55 @@ function _isInteractiveSwipeTarget(target){ catch(_){return false;} } +function _pwaSidebarSwipePoint(e){ + const touch=e&&e.touches&&e.touches[0]||e&&e.changedTouches&&e.changedTouches[0]; + const src=touch||e; + if(!src)return null; + return {clientX:Number(src.clientX)||0,clientY:Number(src.clientY)||0}; +} + +function _isTouchPointerEvent(e){ + return !!(e&&e.pointerType==='touch'); +} + function _openMobileSidebarFromGesture(){ if(_isDesktopWidth())return; const sidebar=document.querySelector('.sidebar'); - const overlay=$('mobileOverlay'); if(!sidebar)return; + try{if(typeof _syncMobileSidebarPanelFromMainView==='function')_syncMobileSidebarPanelFromMainView();}catch(_){} 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.remove('mobile-session-page'); + sidebar.classList.add('mobile-panel-drawer'); sidebar.classList.add('mobile-open'); - if(overlay)overlay.classList.add('visible'); } function _onPwaSidebarSwipeStart(e){ if(_isDesktopWidth())return; + if(_isTouchPointerEvent(e))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; + const point=_pwaSidebarSwipePoint(e); + if(!point)return; + if(point.clientX>_PWA_SIDEBAR_SWIPE_EDGE)return; if(_isInteractiveSwipeTarget(e.target))return; - _pwaSidebarSwipe={startX:clientX,startY:Number(e.clientY)||0,active:true,opened:false}; + _pwaSidebarSwipe={startX:point.clientX,startY:point.clientY,active:true,opened:false}; } function _onPwaSidebarSwipeMove(e){ + if(_isTouchPointerEvent(e))return; 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; + const point=_pwaSidebarSwipePoint(e); + if(!point)return; + const dx=point.clientX-swipe.startX; + const dy=point.clientY-swipe.startY; if(dx<0||Math.abs(dy)>_PWA_SIDEBAR_SWIPE_MAX_VERTICAL*1.5){_pwaSidebarSwipe=null;return;} + if(dx>=_PWA_SIDEBAR_SWIPE_CLAIM&&dx>Math.abs(dy)*1.2){ + if(e.cancelable)e.preventDefault(); + } 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; @@ -320,10 +343,21 @@ function _onPwaSidebarSwipeMove(e){ } } -function _onPwaSidebarSwipeEnd(){_pwaSidebarSwipe=null;} -function _onPwaSidebarSwipeCancel(){_pwaSidebarSwipe=null;} +function _onPwaSidebarSwipeEnd(e){if(_isTouchPointerEvent(e))return;_pwaSidebarSwipe=null;} +function _onPwaSidebarSwipeCancel(e){if(_isTouchPointerEvent(e))return;_pwaSidebarSwipe=null;} function _installPwaSidebarSwipeGesture(){ + // #4660 review (Codex CORE): the #pwaSidebarEdgeGuard element is now + // pointer-events:none (CSS), so it can no longer intercept hit-testing for + // taps / vertical scrolls that merely start in the left edge strip — those + // pass through to the underlying .messages scroller. The edge-swipe-to-open + // gesture is handled entirely by the window-level CAPTURE touch/pointer + // listeners below (which see the event regardless of the guard), so no + // dedicated guard-element listener is needed. + window.addEventListener('touchstart', _onPwaSidebarSwipeStart, {capture:true,passive:true}); + window.addEventListener('touchmove', _onPwaSidebarSwipeMove, {capture:true,passive:false}); + window.addEventListener('touchend', _onPwaSidebarSwipeEnd, {capture:true,passive:true}); + window.addEventListener('touchcancel', _onPwaSidebarSwipeCancel, {capture:true,passive:true}); window.addEventListener('pointerdown', _onPwaSidebarSwipeStart, {passive:true}); window.addEventListener('pointermove', _onPwaSidebarSwipeMove, {passive:false}); window.addEventListener('pointerup', _onPwaSidebarSwipeEnd, {passive:true}); @@ -436,10 +470,9 @@ function mobileSwitchPanel(name){ closeMobileSidebar(); } else { const sidebar=document.querySelector('.sidebar'); - const overlay=$('mobileOverlay'); if(sidebar){ - sidebar.classList.add('mobile-open'); - if(overlay)overlay.classList.add('visible'); + sidebar.classList.remove('mobile-session-page'); + sidebar.classList.add('mobile-panel-drawer','mobile-open'); } } } diff --git a/static/i18n.js b/static/i18n.js index d78c52e7..bbe6ec5d 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -925,6 +925,7 @@ const LOCALES = { dashboard_loopback_warning: 'Dashboard is loopback-only on the server. Either browse from the server itself or restart it with --host 0.0.0.0 (insecure).', tab_logs: 'Logs', tab_settings: 'Settings', + close_menu: 'Close menu', new_conversation: 'New conversation', filter_conversations: 'Filter conversations...', markdown_table_filter: 'Filter table', @@ -2497,6 +2498,7 @@ const LOCALES = { dashboard_loopback_warning: 'La dashboard è solo loopback sul server. Naviga dal server stesso o riavvialo con --host 0.0.0.0 (non sicuro).', tab_logs: 'Log', tab_settings: 'Impostazioni', + close_menu: 'Close menu', // TODO: translate new_conversation: 'Nuova conversazione', filter_conversations: 'Filtra conversazioni...', markdown_table_filter: 'Filtra tabella', @@ -4060,6 +4062,7 @@ const LOCALES = { dashboard_loopback_warning: 'ダッシュボードはサーバー上のループバック専用です。サーバー上で閲覧するか、--host 0.0.0.0(安全ではありません)で再起動してください。', tab_logs: 'ログ', tab_settings: '設定', + close_menu: 'Close menu', // TODO: translate new_conversation: '新しい会話', filter_conversations: '会話を絞り込み...', markdown_table_filter: 'テーブルをフィルター', @@ -5363,6 +5366,7 @@ const LOCALES = { dashboard_loopback_warning: 'Панель доступна только через loopback на сервере. Откройте её с самого сервера или перезапустите с --host 0.0.0.0 (небезопасно).', tab_logs: 'Logs', tab_settings: 'Настройки', + close_menu: 'Close menu', // TODO: translate logs_title: 'Logs', // TODO: translate logs_file: 'File', // TODO: translate @@ -6863,6 +6867,7 @@ const LOCALES = { dashboard_loopback_warning: 'El panel solo usa loopback en el servidor. Navega desde el propio servidor o reinícialo con --host 0.0.0.0 (inseguro).', tab_logs: 'Logs', tab_settings: 'Ajustes', + close_menu: 'Close menu', // TODO: translate logs_title: 'Logs', // TODO: translate logs_file: 'File', // TODO: translate @@ -8341,6 +8346,7 @@ const LOCALES = { dashboard_loopback_warning: 'Das Dashboard ist auf dem Server nur per Loopback erreichbar. Öffne es direkt auf dem Server oder starte es mit --host 0.0.0.0 neu (unsicher).', tab_logs: 'Logs', tab_settings: 'Einstellungen', + close_menu: 'Close menu', // TODO: translate logs_title: 'Logs', // TODO: translate logs_file: 'File', // TODO: translate @@ -9876,6 +9882,7 @@ const LOCALES = { tab_profiles: '配置', tab_logs: '日志', tab_settings: '设置', + close_menu: 'Close menu', // TODO: translate logs_title: '日志', logs_file: '文件', @@ -11626,6 +11633,7 @@ const LOCALES = { dashboard_loopback_warning: '儀表板在伺服器上僅限 loopback 存取。請從伺服器本機瀏覽,或使用 --host 0.0.0.0 重新啟動(不安全)。', tab_logs: '記錄', tab_settings: '設定', + close_menu: '關閉選單', new_conversation: '新增對話', filter_conversations: '篩選對話……', markdown_table_filter: '篩選表格', @@ -13076,6 +13084,7 @@ const LOCALES = { dashboard_loopback_warning: 'O painel é somente loopback no servidor. Navegue pelo próprio servidor ou reinicie com --host 0.0.0.0 (inseguro).', tab_logs: 'Logs', tab_settings: 'Configurações', + close_menu: 'Close menu', // TODO: translate logs_title: 'Logs', // TODO: translate logs_file: 'File', // TODO: translate @@ -14540,6 +14549,7 @@ const LOCALES = { dashboard_loopback_warning: '대시보드는 서버에서 loopback 전용입니다. 서버 자체에서 접속하거나 --host 0.0.0.0(안전하지 않음)으로 다시 시작하세요.', tab_logs: 'Logs', tab_settings: '설정', + close_menu: 'Close menu', // TODO: translate logs_title: 'Logs', // TODO: translate logs_file: 'File', // TODO: translate @@ -16009,6 +16019,7 @@ const LOCALES = { dashboard_loopback_warning: 'Le tableau de bord est en boucle uniquement sur le serveur. Naviguez depuis le serveur lui-même ou redémarrez-le avec --host 0.0.0.0 (non sécurisé).', tab_logs: 'Journaux', tab_settings: 'Paramètres', + close_menu: 'Close menu', // TODO: translate new_conversation: 'Nouvelle conversation', filter_conversations: 'Filtrer les conversations...', markdown_table_filter: 'Filtrer le tableau', @@ -17607,6 +17618,7 @@ const LOCALES = { dashboard_loopback_warning: 'Kontrol Paneli sunucuda yalnızca geri döngüye sahiptir. Ya sunucunun kendisinden göz atın ya da --host 0.0.0.0 (güvenli değil) ile yeniden başlatın.', tab_logs: 'Günlükler', tab_settings: 'Ayarlar', + close_menu: 'Close menu', // TODO: translate logs_title: 'Günlükler', logs_file: 'Dosya', @@ -19185,6 +19197,7 @@ const LOCALES = { dashboard_loopback_warning: 'Pulpit nawigacyjny na serwerze jest dostępny wyłącznie lokalnie (loopback). Przeglądaj bezpośrednio z serwera lub uruchom go ponownie z flagą --host 0.0.0.0 (niebezpieczne).', tab_logs: 'Logi', tab_settings: 'Ustawienia', + close_menu: 'Close menu', // TODO: translate new_conversation: 'Nowa konwersacja', filter_conversations: 'Filtruj konwersacje...', markdown_table_filter: 'Filtruj tabelę', diff --git a/static/index.html b/static/index.html index 619744c0..a213d508 100644 --- a/static/index.html +++ b/static/index.html @@ -177,7 +177,9 @@