Animate session list reflow on removal
This commit is contained in:
parent
22aae48672
commit
587101fb97
3 changed files with 69 additions and 2 deletions
|
|
@ -1303,6 +1303,38 @@ const _lineageReportCache = new Map();
|
|||
const _lineageReportInflight = new Map();
|
||||
let _lineageReportCacheGeneration = 0;
|
||||
let _sessionVisibleSidebarIds = [];
|
||||
let _pendingSessionReflowPositions = null;
|
||||
|
||||
function _captureSessionReflowPositions(){
|
||||
const list=$('sessionList');
|
||||
if(!list) return null;
|
||||
const positions=new Map();
|
||||
list.querySelectorAll('.session-item[data-sid]').forEach(row=>{
|
||||
positions.set(row.dataset.sid,row.getBoundingClientRect().top);
|
||||
});
|
||||
return positions;
|
||||
}
|
||||
|
||||
function _playQueuedSessionReflowAnimation(){
|
||||
const before=_pendingSessionReflowPositions;
|
||||
_pendingSessionReflowPositions=null;
|
||||
if(!before||!before.size) return;
|
||||
const reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
if(reduce) return;
|
||||
const list=$('sessionList');
|
||||
if(!list) return;
|
||||
list.querySelectorAll('.session-item[data-sid]').forEach(row=>{
|
||||
const oldTop=before.get(row.dataset.sid);
|
||||
if(oldTop===undefined) return;
|
||||
const delta=oldTop-row.getBoundingClientRect().top;
|
||||
if(Math.abs(delta)<1||typeof row.animate!=='function') return;
|
||||
const anim=row.animate(
|
||||
[{transform:`translateY(${delta}px)`},{transform:'translateY(0)'}],
|
||||
{duration:360,easing:'cubic-bezier(.2,.8,.2,1)'}
|
||||
);
|
||||
if(anim&&anim.finished) anim.finished.catch(()=>{});
|
||||
});
|
||||
}
|
||||
const SESSION_VIRTUAL_ROW_HEIGHT = 52;
|
||||
const SESSION_VIRTUAL_BUFFER_ROWS = 12;
|
||||
const SESSION_VIRTUAL_THRESHOLD_ROWS = 80;
|
||||
|
|
@ -1609,14 +1641,16 @@ 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 false; }
|
||||
const reflowPositions=_captureSessionReflowPositions();
|
||||
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;
|
||||
_pendingSessionReflowPositions=reflowPositions;
|
||||
await renderSessionList();
|
||||
showToast(session.archived?_sessionArchiveToast(response,session):t('session_restored'));
|
||||
return true;
|
||||
}catch(err){showToast(t('session_archive_failed')+err.message);return false;}
|
||||
}catch(err){_pendingSessionReflowPositions=null;showToast(t('session_archive_failed')+err.message);return false;}
|
||||
}
|
||||
|
||||
function _openSessionActionMenu(session, anchorEl){
|
||||
|
|
@ -2841,6 +2875,7 @@ function renderSessionListFromCache(){
|
|||
toggleBtn.onclick=(e)=>{e.stopPropagation();toggleSessionSelectMode();};
|
||||
list.appendChild(toggleBtn);
|
||||
}
|
||||
_playQueuedSessionReflowAnimation();
|
||||
// Note: declared after the groups loop but available via function hoisting.
|
||||
function _renderOneSession(s, isPinnedGroup=false){
|
||||
const el=document.createElement('div');
|
||||
|
|
@ -3283,6 +3318,10 @@ function renderSessionListFromCache(){
|
|||
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 rect=el.getBoundingClientRect();
|
||||
el.style.height=rect.height+'px';
|
||||
el.style.minHeight=rect.height+'px';
|
||||
requestAnimationFrame(()=>el.classList.add('swipe-removing'));
|
||||
};
|
||||
const _handleSessionSwipe=(signedDx,signedDy)=>{
|
||||
if(_gestureState==='committed'||!_isSessionSwipeTarget()) return false;
|
||||
|
|
@ -3575,12 +3614,13 @@ async function deleteSession(sid, beforeDelete=null){
|
|||
danger:true
|
||||
});
|
||||
if(!ok)return false;
|
||||
const reflowPositions=_captureSessionReflowPositions();
|
||||
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 false;}
|
||||
}catch(e){_pendingSessionReflowPositions=null;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');
|
||||
|
|
@ -3599,6 +3639,7 @@ async function deleteSession(sid, beforeDelete=null){
|
|||
}
|
||||
}
|
||||
showToast(_sessionResponseRetainsWorktree(response,session)?t('session_deleted_worktree'):t('session_deleted'));
|
||||
_pendingSessionReflowPositions=reflowPositions;
|
||||
await renderSessionList();
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -492,6 +492,15 @@
|
|||
}
|
||||
.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-removing{
|
||||
min-height:0!important;
|
||||
height:0!important;
|
||||
margin-bottom:0;
|
||||
padding-top:0;
|
||||
padding-bottom:0;
|
||||
overflow:hidden;
|
||||
transition:background .15s,color .15s,transform .42s cubic-bezier(.2,.8,.2,1),box-shadow .15s ease,height .36s cubic-bezier(.2,.8,.2,1),min-height .36s cubic-bezier(.2,.8,.2,1),margin-bottom .36s cubic-bezier(.2,.8,.2,1),padding-top .36s cubic-bezier(.2,.8,.2,1),padding-bottom .36s cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.session-item.swipe-committed .session-swipe-affordance{transition:opacity .18s ease,transform .18s ease;}
|
||||
/* Suppress hover highlight during drag to avoid visual noise mid-scroll */
|
||||
.session-item.dragging:hover{background:transparent;color:var(--muted);}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,8 @@ def test_session_swipes_show_visual_feedback_and_touch_load_clears():
|
|||
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.height=rect.height+'px'" in SESSIONS_JS
|
||||
assert "requestAnimationFrame(()=>el.classList.add('swipe-removing'))" 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
|
||||
|
|
@ -110,6 +112,8 @@ def test_session_swipes_show_visual_feedback_and_touch_load_clears():
|
|||
assert ".session-item.active.swiping-left" 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 ".session-item.swipe-removing{" in STYLE_CSS
|
||||
assert "height .36s cubic-bezier(.2,.8,.2,1)" in STYLE_CSS
|
||||
assert "transform .42s cubic-bezier(.2,.8,.2,1)" in STYLE_CSS
|
||||
assert ".session-item.swipe-committed .session-swipe-affordance{transition:opacity .18s ease,transform .18s ease;}" in STYLE_CSS
|
||||
assert ".session-item.long-pressing" in STYLE_CSS
|
||||
|
|
@ -119,6 +123,19 @@ def test_session_swipes_show_visual_feedback_and_touch_load_clears():
|
|||
assert "el.classList.remove('loading');" in SESSIONS_JS
|
||||
|
||||
|
||||
def test_session_removal_reflows_surviving_rows_smoothly():
|
||||
assert "let _pendingSessionReflowPositions = null;" in SESSIONS_JS
|
||||
assert "function _captureSessionReflowPositions(){" in SESSIONS_JS
|
||||
assert "positions.set(row.dataset.sid,row.getBoundingClientRect().top);" in SESSIONS_JS
|
||||
assert "function _playQueuedSessionReflowAnimation(){" in SESSIONS_JS
|
||||
assert "window.matchMedia('(prefers-reduced-motion: reduce)').matches" in SESSIONS_JS
|
||||
assert "const delta=oldTop-row.getBoundingClientRect().top;" in SESSIONS_JS
|
||||
assert "{duration:360,easing:'cubic-bezier(.2,.8,.2,1)'}" in SESSIONS_JS
|
||||
assert SESSIONS_JS.count("const reflowPositions=_captureSessionReflowPositions();") >= 2
|
||||
assert SESSIONS_JS.count("_pendingSessionReflowPositions=reflowPositions;") >= 2
|
||||
assert "_playQueuedSessionReflowAnimation();" in SESSIONS_JS
|
||||
|
||||
|
||||
def test_ios_touch_events_drive_session_swipes():
|
||||
assert "el.addEventListener('touchstart'" in SESSIONS_JS
|
||||
assert "el.addEventListener('touchmove'" in SESSIONS_JS
|
||||
|
|
|
|||
Loading…
Reference in a new issue