Polish mobile session swipe feedback
This commit is contained in:
1 parent
b13ed26f5b
commit
4d7fa305fa
3 files changed
+210
-35
No files matched your search
+101
-25
@@ -1524,7 +1524,7 @@ function closeSessionActionMenu(){
|
||||
_sessionActionAnchor.classList.remove('active');
|
||||
}
|
||||
const row=_sessionActionAnchor.closest('.session-item');
|
||||
if(row) row.classList.remove('menu-open');
|
||||
if(row) row.classList.remove('menu-open','long-pressing');
|
||||
_sessionActionAnchor = null;
|
||||
}
|
||||
_sessionActionSessionId = null;
|
||||
@@ -1598,7 +1598,7 @@ function _playSessionActionMenuEntrance(menu){
|
||||
{opacity:0, transform:'translate3d(0,-4px,0) scale(.985)'},
|
||||
{opacity:1, transform:'translate3d(0,0,0) scale(1)'}
|
||||
],
|
||||
{duration:500, easing:'cubic-bezier(.2,.8,.2,1)'}
|
||||
{duration:450, easing:'cubic-bezier(.2,.8,.2,1)'}
|
||||
);
|
||||
if(anim&&anim.finished) anim.finished.catch(()=>{});
|
||||
return;
|
||||
@@ -1608,14 +1608,15 @@ function _playSessionActionMenuEntrance(menu){
|
||||
}
|
||||
|
||||
async function _archiveSession(session, archived=true){
|
||||
if(_isReadOnlySession(session)){ if(typeof showToast==='function') showToast('Read-only imported sessions cannot be modified.',3000); return; }
|
||||
if(_isReadOnlySession(session)){ if(typeof showToast==='function') showToast('Read-only imported sessions cannot be modified.',3000); return false; }
|
||||
try{
|
||||
const response=await api('/api/session/archive',{method:'POST',body:JSON.stringify({session_id:session.session_id,archived})});
|
||||
session.archived=archived;
|
||||
if(S.session&&S.session.session_id===session.session_id) S.session.archived=archived;
|
||||
await renderSessionList();
|
||||
showToast(session.archived?_sessionArchiveToast(response,session):t('session_restored'));
|
||||
}catch(err){showToast(t('session_archive_failed')+err.message);}
|
||||
return true;
|
||||
}catch(err){showToast(t('session_archive_failed')+err.message);return false;}
|
||||
}
|
||||
|
||||
function _openSessionActionMenu(session, anchorEl){
|
||||
@@ -3191,24 +3192,35 @@ function renderSessionListFromCache(){
|
||||
let _longPressTimer=null;
|
||||
let _longPressMenuOpened=false;
|
||||
let _swipeHandled=false;
|
||||
const _longPressDelay=560;
|
||||
const _swipeActionThreshold=96;
|
||||
let _swipeTracking=false;
|
||||
let _pointerX=0;
|
||||
let _pointerY=0;
|
||||
let _gesturePointerType='';
|
||||
const _longPressDelay=400;
|
||||
const _swipeActionThreshold=144;
|
||||
const _swipeCancelRatio=0.75;
|
||||
const _committedSwipeDuration=420;
|
||||
const _clearLongPressTimer=()=>{
|
||||
if(_longPressTimer){clearTimeout(_longPressTimer);_longPressTimer=null;}
|
||||
if(!_longPressMenuOpened) el.classList.remove('long-pressing');
|
||||
};
|
||||
const _beginSessionTouchGesture=(clientX,clientY)=>{
|
||||
const _beginSessionGesture=(clientX,clientY)=>{
|
||||
_pointerActive=true;
|
||||
_gesturePointerType='';
|
||||
_pointerDownX=clientX;
|
||||
_pointerDownY=clientY;
|
||||
_pointerX=clientX;
|
||||
_pointerY=clientY;
|
||||
_isDragging=false;
|
||||
_swipeTracking=false;
|
||||
_longPressMenuOpened=false;
|
||||
_swipeHandled=false;
|
||||
if(_clearDragTimer){clearTimeout(_clearDragTimer);_clearDragTimer=null;}
|
||||
el.classList.remove('dragging');
|
||||
el.classList.remove('dragging','swipe-committed');
|
||||
};
|
||||
const _scheduleSessionLongPressMenu=()=>{
|
||||
_clearLongPressTimer();
|
||||
el.classList.add('long-pressing');
|
||||
_longPressTimer=setTimeout(()=>{
|
||||
if(!_pointerActive||_isDragging||_renamingSid||_sessionSelectMode||readOnly) return;
|
||||
_longPressMenuOpened=true;
|
||||
@@ -3219,11 +3231,37 @@ function renderSessionListFromCache(){
|
||||
},_longPressDelay);
|
||||
};
|
||||
const _isSessionSwipeTarget=()=>{
|
||||
return !readOnly&&!_renamingSid&&!_sessionSelectMode;
|
||||
return _gesturePointerType!=='mouse'&&!readOnly&&!_renamingSid&&!_sessionSelectMode;
|
||||
};
|
||||
const _trackHorizontalSwipe=(dx,dy)=>{
|
||||
if(dx>16&&dx>dy*1.2) _swipeTracking=true;
|
||||
};
|
||||
const _canSwipeDeleteSession=()=>{
|
||||
return _isSessionSwipeTarget()&&!_isMessagingSession(s)&&!_isCliSession(s);
|
||||
};
|
||||
const _paintSessionSwipe=(signedDx)=>{
|
||||
const offset=Math.max(-72,Math.min(72,signedDx*.55));
|
||||
const progress=Math.min(1,Math.abs(offset)/72);
|
||||
el.style.setProperty('--session-swipe-offset',offset+'px');
|
||||
el.style.setProperty('--session-swipe-progress',Math.pow(progress,1.5));
|
||||
el.classList.toggle('swiping-right',offset>0);
|
||||
el.classList.toggle('swiping-left',offset<0);
|
||||
};
|
||||
const _clearSessionSwipePaint=()=>{
|
||||
el.style.removeProperty('--session-swipe-offset');
|
||||
el.style.removeProperty('--session-swipe-progress');
|
||||
el.classList.remove('swiping-right','swiping-left','swipe-committed');
|
||||
};
|
||||
const _settleSessionSwipePaint=()=>{
|
||||
el.classList.remove('dragging');
|
||||
requestAnimationFrame(()=>requestAnimationFrame(_clearSessionSwipePaint));
|
||||
};
|
||||
const _completeSessionSwipePaint=(signedDx)=>{
|
||||
el.classList.remove('dragging');
|
||||
el.classList.add('swipe-committed');
|
||||
el.style.setProperty('--session-swipe-progress','0');
|
||||
el.style.setProperty('--session-swipe-offset',(signedDx>0?1:-1)*window.innerWidth+'px');
|
||||
};
|
||||
const _handleSessionSwipe=(signedDx,signedDy)=>{
|
||||
if(_swipeHandled||!_isSessionSwipeTarget()) return false;
|
||||
if(Math.abs(signedDx)<_swipeActionThreshold) return false;
|
||||
@@ -3234,26 +3272,42 @@ function renderSessionListFromCache(){
|
||||
_tapTimer=null;
|
||||
_lastTapTime=0;
|
||||
if(signedDx>0){
|
||||
_archiveSession(s,true);
|
||||
_completeSessionSwipePaint(signedDx);
|
||||
setTimeout(async()=>{
|
||||
const archived=await _archiveSession(s,!s.archived);
|
||||
if(!archived) _settleSessionSwipePaint();
|
||||
},_committedSwipeDuration);
|
||||
}else if(_canSwipeDeleteSession()){
|
||||
deleteSession(s.session_id);
|
||||
el.classList.remove('dragging');
|
||||
deleteSession(s.session_id,async()=>{
|
||||
_completeSessionSwipePaint(signedDx);
|
||||
await new Promise(resolve=>setTimeout(resolve,_committedSwipeDuration));
|
||||
}).then((deleted)=>{
|
||||
if(!deleted) _settleSessionSwipePaint();
|
||||
});
|
||||
}else if(typeof showToast==='function'){
|
||||
showToast('Imported sessions cannot be deleted here.',3000);
|
||||
_swipeHandled=false;
|
||||
_settleSessionSwipePaint();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const _commitSessionSwipe=()=>{
|
||||
return _handleSessionSwipe(_pointerX-_pointerDownX,_pointerY-_pointerDownY);
|
||||
};
|
||||
const _clearPointerDragState=()=>{
|
||||
_pointerActive=false;
|
||||
_clearLongPressTimer();
|
||||
if(_isDragging){
|
||||
_isDragging=false;
|
||||
if(_clearDragTimer){clearTimeout(_clearDragTimer);_clearDragTimer=null;}
|
||||
_clearDragTimer=setTimeout(()=>{el.classList.remove('dragging');_clearDragTimer=null;},50);
|
||||
_clearDragTimer=setTimeout(()=>{_settleSessionSwipePaint();_clearDragTimer=null;},50);
|
||||
}
|
||||
};
|
||||
el.onpointerdown=(e)=>{
|
||||
if(e.pointerType==='mouse' && e.button!==0) return;
|
||||
_beginSessionTouchGesture(e.clientX,e.clientY);
|
||||
_beginSessionGesture(e.clientX,e.clientY);
|
||||
_gesturePointerType=e.pointerType||'';
|
||||
if(e.pointerType==='touch'||e.pointerType==='pen'){
|
||||
_scheduleSessionLongPressMenu();
|
||||
}
|
||||
@@ -3263,6 +3317,8 @@ function renderSessionListFromCache(){
|
||||
// after an actual press starts on this row; otherwise hovered rows stay
|
||||
// faded until the next sidebar rerender clears their DOM nodes.
|
||||
if(!_pointerActive) return;
|
||||
_pointerX=e.clientX;
|
||||
_pointerY=e.clientY;
|
||||
const dx=Math.abs(e.clientX-_pointerDownX);
|
||||
const dy=Math.abs(e.clientY-_pointerDownY);
|
||||
if(!_isDragging&&(dx>5||dy>5)){
|
||||
@@ -3274,7 +3330,8 @@ function renderSessionListFromCache(){
|
||||
}
|
||||
const signedDx=e.clientX-_pointerDownX;
|
||||
const signedDy=e.clientY-_pointerDownY;
|
||||
_handleSessionSwipe(signedDx,signedDy);
|
||||
_trackHorizontalSwipe(Math.abs(signedDx),Math.abs(signedDy));
|
||||
if(_isSessionSwipeTarget()&&(_swipeTracking||Math.abs(signedDx)>Math.abs(signedDy))) _paintSessionSwipe(signedDx);
|
||||
};
|
||||
el.onpointercancel=_clearPointerDragState;
|
||||
el.onpointerleave=()=>{ if(_pointerActive) _clearPointerDragState(); };
|
||||
@@ -3284,6 +3341,9 @@ function renderSessionListFromCache(){
|
||||
_clearLongPressTimer();
|
||||
if(_renamingSid) return;
|
||||
if(actions&&actions.contains(e.target)) return;
|
||||
_pointerX=e.clientX;
|
||||
_pointerY=e.clientY;
|
||||
_commitSessionSwipe();
|
||||
if(_longPressMenuOpened||_swipeHandled){e.stopPropagation();return;}
|
||||
if(_sessionActionMenu&&!_sessionActionMenu.contains(e.target)){
|
||||
closeSessionActionMenu();
|
||||
@@ -3293,7 +3353,7 @@ function renderSessionListFromCache(){
|
||||
if(e.target&&e.target.closest&&e.target.closest('.session-child-count,.session-child-sessions,.session-child-session,.session-lineage-count,.session-lineage-segments,.session-lineage-segment')) return;
|
||||
if(_sessionSelectMode){e.stopPropagation();if(!readOnly)toggleSessionSelect(s.session_id);return;}
|
||||
// If the pointer moved enough to be a drag, cancel any pending tap
|
||||
if(_isDragging){clearTimeout(_tapTimer);_tapTimer=null;_lastTapTime=0;_clearDragTimer=setTimeout(()=>{el.classList.remove('dragging');_clearDragTimer=null;},50);return;}
|
||||
if(_isDragging){clearTimeout(_tapTimer);_tapTimer=null;_lastTapTime=0;_clearDragTimer=setTimeout(()=>{_settleSessionSwipePaint();_clearDragTimer=null;},50);return;}
|
||||
const now=Date.now();
|
||||
if(now-_lastTapTime<350){
|
||||
// Double-tap: rename
|
||||
@@ -3321,8 +3381,12 @@ function renderSessionListFromCache(){
|
||||
await api('/api/session/import_cli',{method:'POST',body:JSON.stringify({session_id:s.session_id})});
|
||||
}catch(e){ /* import failed -- fall through to read-only view */ }
|
||||
}
|
||||
await loadSession(s.session_id);renderSessionListFromCache();
|
||||
if(typeof closeMobileSidebar==='function')closeMobileSidebar();
|
||||
try{
|
||||
await loadSession(s.session_id);renderSessionListFromCache();
|
||||
if(typeof closeMobileSidebar==='function')closeMobileSidebar();
|
||||
}finally{
|
||||
el.classList.remove('loading');
|
||||
}
|
||||
}, delay);
|
||||
};
|
||||
// Add ondblclick for more reliable double-click detection
|
||||
@@ -3343,13 +3407,16 @@ function renderSessionListFromCache(){
|
||||
el.addEventListener('touchstart',(e)=>{
|
||||
const touch=e.changedTouches&&e.changedTouches[0];
|
||||
if(!touch) return;
|
||||
_beginSessionTouchGesture(touch.clientX,touch.clientY);
|
||||
_beginSessionGesture(touch.clientX,touch.clientY);
|
||||
_gesturePointerType='touch';
|
||||
_scheduleSessionLongPressMenu();
|
||||
},{passive:true});
|
||||
el.addEventListener('touchmove',(e)=>{
|
||||
if(!_pointerActive) return;
|
||||
const touch=e.changedTouches&&e.changedTouches[0];
|
||||
if(!touch) return;
|
||||
_pointerX=touch.clientX;
|
||||
_pointerY=touch.clientY;
|
||||
const signedDx=touch.clientX-_pointerDownX;
|
||||
const signedDy=touch.clientY-_pointerDownY;
|
||||
const dx=Math.abs(signedDx);
|
||||
@@ -3360,15 +3427,22 @@ function renderSessionListFromCache(){
|
||||
el.classList.add('dragging');
|
||||
if(_clearDragTimer){clearTimeout(_clearDragTimer);_clearDragTimer=null;}
|
||||
}
|
||||
if(dx>16&&dx>dy*1.2) e.preventDefault();
|
||||
_handleSessionSwipe(signedDx,signedDy);
|
||||
_trackHorizontalSwipe(dx,dy);
|
||||
if(_isSessionSwipeTarget()&&(_swipeTracking||dx>dy)) _paintSessionSwipe(signedDx);
|
||||
if(_swipeTracking) e.preventDefault();
|
||||
},{passive:false});
|
||||
el.addEventListener('touchend',()=>{
|
||||
el.addEventListener('touchend',(e)=>{
|
||||
_pointerActive=false;
|
||||
_clearLongPressTimer();
|
||||
const touch=e.changedTouches&&e.changedTouches[0];
|
||||
if(touch){
|
||||
_pointerX=touch.clientX;
|
||||
_pointerY=touch.clientY;
|
||||
}
|
||||
_commitSessionSwipe();
|
||||
if(_isDragging){
|
||||
_isDragging=false;
|
||||
_clearDragTimer=setTimeout(()=>{el.classList.remove('dragging');_clearDragTimer=null;},50);
|
||||
if(!_swipeHandled) _clearDragTimer=setTimeout(()=>{_settleSessionSwipePaint();_clearDragTimer=null;},50);
|
||||
}
|
||||
},{passive:true});
|
||||
return el;
|
||||
@@ -3474,19 +3548,20 @@ async function removeWorktree(session){
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSession(sid){
|
||||
async function deleteSession(sid, beforeDelete=null){
|
||||
const session=_sessionSnapshotById(sid);
|
||||
const ok=await showConfirmDialog({
|
||||
message:session&&session.worktree_path?t('session_delete_worktree_confirm',session.worktree_path):t('session_delete_confirm'),
|
||||
confirmLabel:t('delete_title'),
|
||||
danger:true
|
||||
});
|
||||
if(!ok)return;
|
||||
if(!ok)return false;
|
||||
if(beforeDelete) await beforeDelete();
|
||||
let response=null;
|
||||
try{
|
||||
response=await api('/api/session/delete',{method:'POST',body:JSON.stringify({session_id:sid})});
|
||||
_clearHandoffStorageForSession(sid);
|
||||
}catch(e){setStatus(`Delete failed: ${e.message}`);return;}
|
||||
}catch(e){setStatus(`Delete failed: ${e.message}`);return false;}
|
||||
if(S.session&&S.session.session_id===sid){
|
||||
S.session=null;S.messages=[];S.entries=[];
|
||||
localStorage.removeItem('hermes-webui-session');
|
||||
@@ -3506,6 +3581,7 @@ async function deleteSession(sid){
|
||||
}
|
||||
showToast(_sessionResponseRetainsWorktree(response,session)?t('session_deleted_worktree'):t('session_deleted'));
|
||||
await renderSessionList();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Project helpers ─────────────────────────────────────────────────────
|
||||
|
||||
+50
-2
@@ -429,18 +429,65 @@
|
||||
and attention indicator (26x26 at right:6px) still need 40px reserved
|
||||
when they're visible — covered by the hover / streaming / unread /
|
||||
menu-open / focus-within rule below. */
|
||||
.session-item{padding:8px 8px;margin-bottom:2px;border-radius:8px;cursor:pointer;font-size:13px;color:var(--muted);transition:background .15s,color .15s;display:flex;align-items:flex-start;gap:8px;min-width:0;position:relative;touch-action:pan-y;-webkit-tap-highlight-color:transparent;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none;}
|
||||
.session-item{padding:8px 8px;margin-bottom:2px;border-radius:8px;cursor:pointer;font-size:13px;color:var(--muted);transition:background .15s,color .15s,transform .5s cubic-bezier(.2,.8,.2,1),box-shadow .15s ease;display:flex;align-items:flex-start;gap:8px;min-width:0;position:relative;touch-action:pan-y;-webkit-tap-highlight-color:transparent;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none;transform:translateX(var(--session-swipe-offset,0));}
|
||||
.session-item.streaming,.session-item.unread,.session-item:focus-within,.session-item.menu-open{padding-right:40px;}
|
||||
@media (hover:hover){.session-item:hover{background:var(--hover-bg);color:var(--text);}}
|
||||
.session-item.loading{background:var(--hover-bg);color:var(--text);}
|
||||
.session-item.long-pressing{
|
||||
background:var(--hover-bg);
|
||||
color:var(--text);
|
||||
box-shadow:0 0 0 1px color-mix(in srgb,var(--accent) 38%,transparent);
|
||||
animation:session-long-press .4s cubic-bezier(.2,.8,.2,1) both;
|
||||
}
|
||||
.session-item.swiping-right{background:color-mix(in srgb,var(--warning) 18%,var(--surface));box-shadow:0 0 0 1px color-mix(in srgb,var(--warning) 60%,transparent);}
|
||||
.session-item.swiping-left{background:color-mix(in srgb,var(--error) 16%,var(--surface));box-shadow:0 0 0 1px color-mix(in srgb,var(--error) 60%,transparent);}
|
||||
.session-item.swiping-right::after,.session-item.swiping-left::after{
|
||||
position:absolute;
|
||||
top:0;
|
||||
bottom:0;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
width:72px;
|
||||
padding:0 10px;
|
||||
box-sizing:border-box;
|
||||
opacity:var(--session-swipe-progress,0);
|
||||
font-size:10px;
|
||||
font-weight:700;
|
||||
letter-spacing:.06em;
|
||||
text-transform:uppercase;
|
||||
pointer-events:none;
|
||||
}
|
||||
.session-item.swiping-right::after{
|
||||
content:"Archive";
|
||||
left:0;
|
||||
justify-content:flex-start;
|
||||
transform:translateX(calc(-1 * var(--session-swipe-offset,0px)));
|
||||
color:var(--warning);
|
||||
background:transparent;
|
||||
}
|
||||
.session-item.swiping-left::after{
|
||||
content:"Delete";
|
||||
right:0;
|
||||
justify-content:flex-end;
|
||||
transform:translateX(calc(-1 * var(--session-swipe-offset,0px)));
|
||||
color:var(--error);
|
||||
background:transparent;
|
||||
}
|
||||
.session-item.dragging{transition:background .15s,color .15s,box-shadow .15s ease;}
|
||||
.session-item.swipe-committed{transition:background .15s,color .15s,transform .42s cubic-bezier(.2,.8,.2,1),box-shadow .15s ease;}
|
||||
.session-item.swipe-committed::after{transition:opacity .18s ease;}
|
||||
/* Suppress hover highlight during drag to avoid visual noise mid-scroll */
|
||||
.session-item.dragging:hover{background:transparent;color:var(--muted);}
|
||||
.session-item.dragging.swiping-right{background:color-mix(in srgb,var(--warning) 18%,var(--surface));}
|
||||
.session-item.dragging.swiping-left{background:color-mix(in srgb,var(--error) 16%,var(--surface));}
|
||||
/* Restore hover padding-right only for mouse (hover:hover) devices.
|
||||
Touch/iPad (hover:none) must NOT expand padding-right on :hover —
|
||||
the expansion causes a layout-reflow mid-tap that moves session-actions
|
||||
under the finger, triggering stopPropagation and swallowing navigation. */
|
||||
@media (hover:hover){:root:not(.dark) .session-item:hover{background:var(--hover-bg);color:var(--text);}.session-item:hover{padding-right:40px;}}
|
||||
.session-item.active{background:var(--accent-bg);color:var(--accent);}
|
||||
.session-item.active.swiping-right{background:color-mix(in srgb,var(--warning) 22%,var(--accent-bg));}
|
||||
.session-item.active.swiping-left{background:color-mix(in srgb,var(--error) 20%,var(--accent-bg));}
|
||||
.session-item.streaming .session-title{color:var(--accent);}
|
||||
.session-item.streaming .session-title-row{color:var(--text);}
|
||||
.session-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;overflow:hidden;}
|
||||
@@ -500,7 +547,7 @@
|
||||
.session-actions-trigger.active{background:var(--accent-bg);border-color:var(--accent-bg-strong);color:var(--text);}
|
||||
.session-actions-trigger svg{display:block;}
|
||||
.session-action-menu{display:block;position:fixed;left:0;top:0;right:auto;bottom:auto;min-width:220px;max-width:min(280px,calc(100vw - 16px));background:var(--surface);border:1px solid var(--border2);border-radius:10px;box-shadow:0 -4px 24px rgba(0,0,0,.4);z-index:999;overflow:hidden;max-height:calc(100vh - 16px);overflow-y:auto;transform-origin:top right;will-change:opacity,transform;}
|
||||
.session-action-menu.open-animated{animation:session-menu-in .5s cubic-bezier(.2,.8,.2,1);}
|
||||
.session-action-menu.open-animated{animation:session-menu-in .45s cubic-bezier(.2,.8,.2,1);}
|
||||
.session-action-opt{width:100%;background:none;border:none;text-align:left;font:inherit;color:var(--text);flex-direction:row!important;gap:0!important;padding:0!important;}
|
||||
.session-action-opt .ws-opt-action{display:flex;flex-direction:row;align-items:center;gap:10px;width:100%;padding:10px 14px;}
|
||||
.session-action-opt .ws-opt-icon{color:var(--muted);transition:color .12s,opacity .12s;flex-shrink:0;display:flex;align-items:center;width:16px;}
|
||||
@@ -516,6 +563,7 @@
|
||||
/* Hide overlay during inline rename */
|
||||
.session-item:has(.session-title-input) .session-actions{display:none;}
|
||||
@keyframes session-menu-in{from{opacity:0;transform:translate3d(0,-4px,0) scale(.985);}to{opacity:1;transform:translate3d(0,0,0) scale(1);}}
|
||||
@keyframes session-long-press{from{filter:brightness(1);}to{filter:brightness(1.08);}}
|
||||
@keyframes newflash{0%{background:var(--accent-bg-strong);color:var(--accent);}100%{background:transparent;color:var(--muted);}}
|
||||
@keyframes spin{to{transform:rotate(360deg);}}
|
||||
.session-item.new-flash{animation:newflash 1.4s ease-out forwards;}
|
||||
|
||||
@@ -19,20 +19,23 @@ def test_session_menu_has_subtle_open_animation():
|
||||
assert "function _playSessionActionMenuEntrance(menu){" in SESSIONS_JS
|
||||
assert "typeof menu.animate==='function'" in SESSIONS_JS
|
||||
assert "{opacity:0, transform:'translate3d(0,-4px,0) scale(.985)'}" in SESSIONS_JS
|
||||
assert "{duration:500, easing:'cubic-bezier(.2,.8,.2,1)'}" in SESSIONS_JS
|
||||
assert "{duration:450, easing:'cubic-bezier(.2,.8,.2,1)'}" in SESSIONS_JS
|
||||
assert "menu.classList.add('open-animated')" in SESSIONS_JS
|
||||
assert ".session-action-menu.open-animated{animation:session-menu-in .5s cubic-bezier(.2,.8,.2,1);}" in STYLE_CSS
|
||||
assert ".session-action-menu.open-animated{animation:session-menu-in .45s cubic-bezier(.2,.8,.2,1);}" in STYLE_CSS
|
||||
assert "@keyframes session-menu-in" in STYLE_CSS
|
||||
assert "@media (prefers-reduced-motion:reduce)" in STYLE_CSS
|
||||
assert ".session-action-menu{animation:none;will-change:auto;}" in STYLE_CSS
|
||||
|
||||
|
||||
def test_mobile_session_menu_opens_from_long_press_and_hides_dots():
|
||||
assert "_longPressDelay=560" in SESSIONS_JS
|
||||
assert "_longPressDelay=400" in SESSIONS_JS
|
||||
assert "el.classList.add('long-pressing')" in SESSIONS_JS
|
||||
assert "if(!_longPressMenuOpened) el.classList.remove('long-pressing')" in SESSIONS_JS
|
||||
assert "row.classList.remove('menu-open','long-pressing')" in SESSIONS_JS
|
||||
assert "_openSessionActionMenu(s, el)" in SESSIONS_JS
|
||||
assert "@media (hover:none) and (pointer:coarse)" in STYLE_CSS
|
||||
assert ".session-actions{display:none;}" in STYLE_CSS
|
||||
assert "const _beginSessionTouchGesture=(clientX,clientY)=>{" in SESSIONS_JS
|
||||
assert "const _beginSessionGesture=(clientX,clientY)=>{" in SESSIONS_JS
|
||||
assert "const _scheduleSessionLongPressMenu=()=>{" in SESSIONS_JS
|
||||
mobile_touch = STYLE_CSS[STYLE_CSS.find("@media (hover:none) and (pointer:coarse)"):STYLE_CSS.find("@media (max-width: 340px)")]
|
||||
assert ".session-item{padding-right:6px;}" in mobile_touch
|
||||
@@ -52,13 +55,61 @@ def test_open_session_menu_consumes_next_row_activation():
|
||||
|
||||
|
||||
def test_session_swipes_archive_right_and_delete_left():
|
||||
assert "_swipeActionThreshold=96" in SESSIONS_JS
|
||||
assert "_gesturePointerType!=='mouse'" in SESSIONS_JS
|
||||
assert "_swipeTracking=true" in SESSIONS_JS
|
||||
assert "const _trackHorizontalSwipe=(dx,dy)=>{" in SESSIONS_JS
|
||||
assert "_swipeActionThreshold=144" in SESSIONS_JS
|
||||
assert "_committedSwipeDuration=420" in SESSIONS_JS
|
||||
assert "const _handleSessionSwipe=(signedDx,signedDy)=>{" in SESSIONS_JS
|
||||
assert "if(_isSessionSwipeTarget()&&(_swipeTracking||Math.abs(signedDx)>Math.abs(signedDy))) _paintSessionSwipe(signedDx)" in SESSIONS_JS
|
||||
assert "if(_isSessionSwipeTarget()&&(_swipeTracking||dx>dy)) _paintSessionSwipe(signedDx)" in SESSIONS_JS
|
||||
assert "if(signedDx>0){" in SESSIONS_JS
|
||||
assert "_archiveSession(s,true)" in SESSIONS_JS
|
||||
assert "deleteSession(s.session_id)" in SESSIONS_JS
|
||||
assert "_archiveSession(s,!s.archived)" in SESSIONS_JS
|
||||
assert "deleteSession(s.session_id,async()=>{" in SESSIONS_JS
|
||||
assert "showToast('Imported sessions cannot be deleted here.',3000);" in SESSIONS_JS
|
||||
assert "_swipeHandled=false;" in SESSIONS_JS
|
||||
assert "if(!_isDragging&&(dx>5||dy>5))" in SESSIONS_JS
|
||||
assert "_handleSessionSwipe(signedDx,signedDy)" in SESSIONS_JS
|
||||
assert "const _commitSessionSwipe=()=>{" in SESSIONS_JS
|
||||
assert "_commitSessionSwipe();" in SESSIONS_JS
|
||||
|
||||
|
||||
def test_session_swipes_show_visual_feedback_and_touch_load_clears():
|
||||
assert "const _paintSessionSwipe=(signedDx)=>{" in SESSIONS_JS
|
||||
assert "el.style.setProperty('--session-swipe-offset',offset+'px')" in SESSIONS_JS
|
||||
assert "const progress=Math.min(1,Math.abs(offset)/72)" in SESSIONS_JS
|
||||
assert "el.style.setProperty('--session-swipe-progress',Math.pow(progress,1.5))" in SESSIONS_JS
|
||||
assert "const _clearSessionSwipePaint=()=>{" in SESSIONS_JS
|
||||
assert "const _settleSessionSwipePaint=()=>{" in SESSIONS_JS
|
||||
assert "const _completeSessionSwipePaint=(signedDx)=>{" in SESSIONS_JS
|
||||
assert "el.classList.add('swipe-committed')" in SESSIONS_JS
|
||||
assert "el.style.setProperty('--session-swipe-progress','0')" in SESSIONS_JS
|
||||
assert "deleteSession(s.session_id,async()=>{" in SESSIONS_JS
|
||||
assert "const archived=await _archiveSession(s,!s.archived);" in SESSIONS_JS
|
||||
assert "if(!archived) _settleSessionSwipePaint();" in SESSIONS_JS
|
||||
assert "await new Promise(resolve=>setTimeout(resolve,_committedSwipeDuration));" in SESSIONS_JS
|
||||
assert "async function deleteSession(sid, beforeDelete=null){" in SESSIONS_JS
|
||||
assert "requestAnimationFrame(()=>requestAnimationFrame(_clearSessionSwipePaint))" in SESSIONS_JS
|
||||
assert ".session-item.swiping-right" in STYLE_CSS
|
||||
assert ".session-item.swiping-left" in STYLE_CSS
|
||||
assert "opacity:var(--session-swipe-progress,0)" in STYLE_CSS
|
||||
assert "transform:translateX(calc(-1 * var(--session-swipe-offset,0px)))" in STYLE_CSS
|
||||
assert STYLE_CSS.count("background:transparent;") >= 3
|
||||
assert "transform .5s cubic-bezier(.2,.8,.2,1)" in STYLE_CSS
|
||||
assert ".session-item.dragging.swiping-right" in STYLE_CSS
|
||||
assert ".session-item.dragging.swiping-left" in STYLE_CSS
|
||||
assert ".session-item.active.swiping-right" in STYLE_CSS
|
||||
assert ".session-item.active.swiping-left" in STYLE_CSS
|
||||
assert 'content:"Archive"' in STYLE_CSS
|
||||
assert 'content:"Delete"' in STYLE_CSS
|
||||
assert ".session-item.dragging{transition:background .15s,color .15s,box-shadow .15s ease;}" in STYLE_CSS
|
||||
assert ".session-item.swipe-committed" in STYLE_CSS
|
||||
assert "transform .42s cubic-bezier(.2,.8,.2,1)" in STYLE_CSS
|
||||
assert ".session-item.swipe-committed::after{transition:opacity .18s ease;}" in STYLE_CSS
|
||||
assert ".session-item.long-pressing" in STYLE_CSS
|
||||
assert "@keyframes session-long-press" in STYLE_CSS
|
||||
assert "transform:translateX(var(--session-swipe-offset,0))" in STYLE_CSS
|
||||
assert "finally{" in SESSIONS_JS
|
||||
assert "el.classList.remove('loading');" in SESSIONS_JS
|
||||
|
||||
|
||||
def test_ios_touch_events_drive_session_swipes():
|
||||
|
||||
Reference in new issue
Block a user