refine archive/restore animations
This commit is contained in:
parent
a277bbd42c
commit
c4fffff205
3 changed files with 43 additions and 11 deletions
|
|
@ -107,8 +107,8 @@ let _sessionListLastScrollAt = 0;
|
|||
let _pendingSessionListPayload = null;
|
||||
let _pendingSessionListApplyTimer = 0;
|
||||
const SESSION_LIST_INTERACTION_IDLE_MS = 700;
|
||||
const SESSION_SWIPE_DURATION_MS = 420;
|
||||
const SESSION_SWIPE_REFLOW_LEAD_MS = 180;
|
||||
const SESSION_SWIPE_DURATION_MS = 500;
|
||||
const SESSION_SWIPE_REFLOW_LEAD_MS = 220;
|
||||
const SESSION_REFLOW_TIMEOUT_MS = 420;
|
||||
const SESSION_LIST_FLIP_TIMEOUT_MS = 460;
|
||||
|
||||
|
|
@ -1459,6 +1459,7 @@ let _lineageReportCacheGeneration = 0;
|
|||
let _sessionVisibleSidebarIds = [];
|
||||
let _pendingSessionReflowPositions = null;
|
||||
const _optimisticallyRemovedSessionIds = new Set();
|
||||
const _sessionSwipeReturnOffsets = new Map();
|
||||
|
||||
function _captureSessionReflowPositions(){
|
||||
const list=$('sessionList');
|
||||
|
|
@ -1834,6 +1835,7 @@ async function _archiveSession(session, archived=true, beforeListRender=null){
|
|||
session.archived=archived;
|
||||
if(S.session&&S.session.session_id===session.session_id) S.session.archived=archived;
|
||||
if(renderHold) await renderHold;
|
||||
if(_showArchived&&!_sessionPrefersReducedMotion()) _sessionSwipeReturnOffsets.set(session.session_id,'0px');
|
||||
_pendingSessionReflowPositions=reflowPositions;
|
||||
await renderSessionList();
|
||||
showToast(session.archived?_sessionArchiveToast(response,session):t('session_restored'));
|
||||
|
|
@ -3272,6 +3274,16 @@ function renderSessionListFromCache(){
|
|||
const hasUnread=_hasUnreadForSession(s)&&!isActive;
|
||||
const readOnly=_isReadOnlySession(s);
|
||||
el.className='session-item'+(isActive?' active':'')+(isActive&&S.session&&S.session._flash?' new-flash':'')+(s.archived?' archived':'')+(isStreaming?' streaming':'')+(hasUnread?' unread':'');
|
||||
const swipeReturnOffset=_sessionSwipeReturnOffsets.get(s.session_id);
|
||||
if(swipeReturnOffset!==undefined){
|
||||
_sessionSwipeReturnOffsets.delete(s.session_id);
|
||||
el.style.setProperty('--session-swipe-return-offset',swipeReturnOffset);
|
||||
el.classList.add('session-swipe-returning');
|
||||
el.addEventListener('animationend',()=>{
|
||||
el.classList.remove('session-swipe-returning');
|
||||
el.style.removeProperty('--session-swipe-return-offset');
|
||||
},{once:true});
|
||||
}
|
||||
if(animateRefresh&&(enterAllAnimatedRows||!(flipBefore&&flipBefore.has(s.session_id)))){
|
||||
el.classList.add('session-list-flip-enter');
|
||||
}
|
||||
|
|
@ -3771,6 +3783,11 @@ function renderSessionListFromCache(){
|
|||
_archiveSession(s,false,()=>_waitForSessionMotion(committedSwipeDuration)).then((restored)=>{
|
||||
if(!restored) _settleSessionSwipePaint();
|
||||
});
|
||||
}else if(_showArchived){
|
||||
_settleSessionSwipePaint();
|
||||
_archiveSession(s,true,()=>_waitForSessionMotion(committedSwipeDuration)).then((archived)=>{
|
||||
if(!archived) _settleSessionSwipePaint();
|
||||
});
|
||||
}else{
|
||||
_completeSessionSwipePaint(signedDx);
|
||||
_archiveSession(s,true,()=>_waitForSessionMotion(committedSwipeReflowDelay)).then((archived)=>{
|
||||
|
|
|
|||
|
|
@ -654,7 +654,7 @@
|
|||
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,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)) translateY(var(--session-reflow-offset,0));}
|
||||
.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(.22,.61,.36,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:translate3d(var(--session-swipe-offset,0),var(--session-reflow-offset,0),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);}
|
||||
|
|
@ -681,7 +681,7 @@
|
|||
box-sizing:border-box;
|
||||
opacity:0;
|
||||
overflow:hidden;
|
||||
transform:translateX(calc(-1 * var(--session-swipe-offset,0px))) scale(calc(.82 + var(--session-swipe-progress,0) * .18));
|
||||
transform:translate3d(calc(-1 * var(--session-swipe-offset,0px)),0,0) scale(calc(.82 + var(--session-swipe-progress,0) * .18));
|
||||
pointer-events:none;
|
||||
z-index:0;
|
||||
}
|
||||
|
|
@ -728,10 +728,11 @@
|
|||
.session-item.dragging{transition:background .15s,color .15s,box-shadow .15s ease;will-change:transform;}
|
||||
.session-item.session-reflowing{transition:background .15s,color .15s,transform .36s cubic-bezier(.2,.8,.2,1),box-shadow .15s ease;will-change:transform;}
|
||||
.session-item.swipe-committed,
|
||||
.session-item.swipe-removing{transition:background .15s,color .15s,transform .42s cubic-bezier(.2,.8,.2,1),box-shadow .15s ease;will-change:transform;}
|
||||
.session-item.swipe-removing{transition:background .15s,color .15s,transform .5s cubic-bezier(.22,.61,.36,1),box-shadow .15s ease;will-change:transform;}
|
||||
.session-item.swipe-removing{
|
||||
overflow:hidden;
|
||||
}
|
||||
.session-item.session-swipe-returning{animation:sessionSwipeReturn .5s cubic-bezier(.22,.61,.36,1) both;will-change:transform,opacity;}
|
||||
.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);}
|
||||
|
|
@ -823,13 +824,14 @@
|
|||
@media (prefers-reduced-motion:reduce){
|
||||
.session-action-menu{animation:none;will-change:auto;}
|
||||
.session-item,.session-item.dragging,.session-item.session-reflowing,.session-item.swipe-committed,.session-item.swipe-removing{transition:none;}
|
||||
.session-item.long-pressing{animation:none;}
|
||||
.session-item.long-pressing,.session-item.session-swipe-returning{animation:none;}
|
||||
.session-item.session-list-flip-enter{animation:none;}
|
||||
}
|
||||
/* 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 sessionSwipeReturn{0%{opacity:.62;transform:translate3d(var(--session-swipe-return-offset,32px),var(--session-reflow-offset,0),0) scale(.985);}100%{opacity:1;transform:translate3d(0,var(--session-reflow-offset,0),0) scale(1);}}
|
||||
@keyframes newflash{0%{background:var(--accent-bg-strong);color:var(--accent);}100%{background:transparent;color:var(--muted);}}
|
||||
@keyframes sessionListFlipIn{0%{opacity:0;transform:perspective(700px) rotateX(-8deg) translateY(10px) scale(.985);}100%{opacity:1;transform:perspective(700px) rotateX(0) translateY(0) scale(1);}}
|
||||
@keyframes spin{to{transform:rotate(360deg);}}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ def test_session_menu_has_subtle_open_animation():
|
|||
assert "@media (prefers-reduced-motion:reduce)" in STYLE_CSS
|
||||
assert ".session-action-menu{animation:none;will-change:auto;}" in STYLE_CSS
|
||||
assert ".session-item,.session-item.dragging,.session-item.session-reflowing,.session-item.swipe-committed,.session-item.swipe-removing{transition:none;}" in STYLE_CSS
|
||||
assert ".session-item.long-pressing{animation:none;}" in STYLE_CSS
|
||||
assert ".session-item.long-pressing,.session-item.session-swipe-returning{animation:none;}" in STYLE_CSS
|
||||
|
||||
|
||||
def test_mobile_session_menu_opens_from_long_press_and_hides_dots():
|
||||
|
|
@ -86,8 +86,8 @@ def test_session_swipes_route_archive_restore_and_delete():
|
|||
assert "_swipeTracking=true" in SESSIONS_JS
|
||||
assert "_archiveSwipeActionThreshold=128" in SESSIONS_JS
|
||||
assert "_deleteSwipeActionThreshold=128" in SESSIONS_JS
|
||||
assert "const SESSION_SWIPE_DURATION_MS = 420;" in SESSIONS_JS
|
||||
assert "const SESSION_SWIPE_REFLOW_LEAD_MS = 180;" in SESSIONS_JS
|
||||
assert "const SESSION_SWIPE_DURATION_MS = 500;" in SESSIONS_JS
|
||||
assert "const SESSION_SWIPE_REFLOW_LEAD_MS = 220;" in SESSIONS_JS
|
||||
assert "const committedSwipeDuration=_sessionPrefersReducedMotion()?0:SESSION_SWIPE_DURATION_MS;" in SESSIONS_JS
|
||||
assert "const committedSwipeReflowDelay=Math.max(0,committedSwipeDuration-SESSION_SWIPE_REFLOW_LEAD_MS);" in SESSIONS_JS
|
||||
swipe_block = _sessions_block("const _handleSessionSwipe=(signedDx,signedDy)=>{", "const _commitSessionSwipe=()=>{")
|
||||
|
|
@ -106,6 +106,12 @@ def test_session_swipes_route_archive_restore_and_delete():
|
|||
assert "_settleSessionSwipePaint();" in restore_branch
|
||||
assert "_completeSessionSwipePaint(signedDx);" not in restore_branch
|
||||
assert "_archiveSession(s,false,()=>_waitForSessionMotion(committedSwipeDuration))" in restore_branch
|
||||
archived_visible_start = swipe_block.find("}else if(_showArchived){")
|
||||
assert archived_visible_start > restore_start
|
||||
archived_visible_branch = swipe_block[archived_visible_start:archive_start]
|
||||
assert "_settleSessionSwipePaint();" in archived_visible_branch
|
||||
assert "_completeSessionSwipePaint(signedDx);" not in archived_visible_branch
|
||||
assert "_archiveSession(s,true,()=>_waitForSessionMotion(committedSwipeDuration))" in archived_visible_branch
|
||||
assert archive_branch.find("_completeSessionSwipePaint(signedDx);") < archive_branch.find("_archiveSession(s,true,()=>_waitForSessionMotion(committedSwipeReflowDelay))")
|
||||
assert delete_branch.find("deleteSession(s.session_id,async()=>{") < delete_branch.find("_completeSessionSwipePaint(signedDx);")
|
||||
assert "showToast('Imported sessions cannot be deleted here.',3000);" in SESSIONS_JS
|
||||
|
|
@ -129,7 +135,8 @@ def test_session_swipe_paint_uses_transform_only_exit():
|
|||
assert "requestAnimationFrame(()=>requestAnimationFrame(_clearSessionSwipePaint))" in SESSIONS_JS
|
||||
assert "el.classList.remove('swiping-right','swiping-left','swipe-committed','swipe-removing')" in clear
|
||||
assert ".session-item.swipe-committed,\n .session-item.swipe-removing{transition:" in STYLE_CSS
|
||||
assert "transform:translateX(calc(-1 * var(--session-swipe-offset,0px))) scale(calc(.82 + var(--session-swipe-progress,0) * .18))" in STYLE_CSS
|
||||
assert "transition:background .15s,color .15s,transform .5s cubic-bezier(.22,.61,.36,1),box-shadow .15s ease" in STYLE_CSS
|
||||
assert "transform:translate3d(calc(-1 * var(--session-swipe-offset,0px)),0,0) scale(calc(.82 + var(--session-swipe-progress,0) * .18))" in STYLE_CSS
|
||||
swipe_start = STYLE_CSS.find(".session-item.swipe-removing{")
|
||||
swipe_end = STYLE_CSS.find("}", swipe_start)
|
||||
assert swipe_start >= 0 and swipe_end > swipe_start
|
||||
|
|
@ -137,12 +144,13 @@ def test_session_swipe_paint_uses_transform_only_exit():
|
|||
assert "height:0" not in swipe_removing
|
||||
assert "padding-top:0" not in swipe_removing
|
||||
assert "margin-bottom:0" not in swipe_removing
|
||||
assert "transform:translateX(var(--session-swipe-offset,0))" in STYLE_CSS
|
||||
assert "transform:translate3d(var(--session-swipe-offset,0),var(--session-reflow-offset,0),0)" in STYLE_CSS
|
||||
|
||||
|
||||
def test_session_removal_reflows_surviving_rows_smoothly():
|
||||
assert "let _pendingSessionReflowPositions = null;" in SESSIONS_JS
|
||||
assert "const _optimisticallyRemovedSessionIds = new Set();" in SESSIONS_JS
|
||||
assert "const _sessionSwipeReturnOffsets = new Map();" in SESSIONS_JS
|
||||
capture = _sessions_block("function _captureSessionReflowPositions(){", "function _waitForSessionMotion")
|
||||
helper = _sessions_block("function _playSessionRowsReflowFromPositions", "function _sessionPrefersReducedMotion")
|
||||
assert "positions.set(row.dataset.sid,row.getBoundingClientRect().top);" in capture
|
||||
|
|
@ -163,6 +171,11 @@ def test_session_removal_reflows_surviving_rows_smoothly():
|
|||
assert "const serverSessions=_optimisticallyRemovedSessionIds.size" in SESSIONS_JS
|
||||
assert "? (sessData.sessions||[]).filter(s=>s&&!_optimisticallyRemovedSessionIds.has(s.session_id))" in SESSIONS_JS
|
||||
assert ".session-item.session-reflowing{transition:background .15s,color .15s,transform .36s cubic-bezier(.2,.8,.2,1),box-shadow .15s ease;will-change:transform;}" in STYLE_CSS
|
||||
assert "if(_showArchived&&!_sessionPrefersReducedMotion()) _sessionSwipeReturnOffsets.set(session.session_id,'0px');" in SESSIONS_JS
|
||||
assert "const swipeReturnOffset=_sessionSwipeReturnOffsets.get(s.session_id);" in SESSIONS_JS
|
||||
assert "el.classList.add('session-swipe-returning');" in SESSIONS_JS
|
||||
assert "@keyframes sessionSwipeReturn" in STYLE_CSS
|
||||
assert ".session-item.session-swipe-returning{animation:sessionSwipeReturn .5s cubic-bezier(.22,.61,.36,1) both;will-change:transform,opacity;}" in STYLE_CSS
|
||||
delete_start = SESSIONS_JS.find("async function deleteSession(sid, beforeDelete=null){")
|
||||
delete_end = SESSIONS_JS.find("// ── Project helpers", delete_start)
|
||||
assert delete_start >= 0 and delete_end > delete_start
|
||||
|
|
|
|||
Loading…
Reference in a new issue