diff --git a/static/sessions.js b/static/sessions.js index 9ecaa6f5..223dd981 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -1908,12 +1908,15 @@ async function _archiveSession(session, archived=true, beforeListRender=null){ try{ const response=await api('/api/session/archive',{method:'POST',body:JSON.stringify({session_id:session.session_id,archived})}); session.archived=archived; + const cached=(_allSessions||[]).find(s=>s&&s.session_id===session.session_id); + if(cached) cached.archived=archived; if(S.session&&S.session.session_id===session.session_id) S.session.archived=archived; + showToast(session.archived?_sessionArchiveToast(response,session):t('session_restored')); 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')); + renderSessionListFromCache(); + void renderSessionList(); return true; }catch(err){if(renderHold) await renderHold.catch(()=>{});_pendingSessionReflowPositions=null;showToast(t('session_archive_failed')+err.message);return false;} } @@ -3767,7 +3770,7 @@ function renderSessionListFromCache(){ if(!readOnly){ el.append( - _makeSessionSwipeAffordance('right',s.archived?'undo':'archive',s.archived?t('session_restore'):t('session_batch_archive')), + _makeSessionSwipeAffordance('right',s.archived?'undo':'archive',s.archived?'Restore':t('session_batch_archive')), _makeSessionSwipeAffordance('left','trash-2',t('session_batch_delete')), ); } @@ -3860,10 +3863,12 @@ function renderSessionListFromCache(){ const offset=Math.sign(rawOffset)*(Math.abs(revealedOffset)+Math.sqrt(overshoot)*5); const progress=Math.min(1,Math.abs(revealedOffset)/72); const reveal=Math.abs(offset); - const iconScale=1+Math.min(.45,Math.max(0,Math.abs(rawOffset)-52)/130); + const iconScale=Math.min(1,Math.max(.01,progress*1.12)); + const badgeStretch=Math.min(Math.max(0,reveal-34),overshoot*1.15); el.style.setProperty('--session-swipe-offset',offset+'px'); el.style.setProperty('--session-swipe-reveal',reveal+'px'); el.style.setProperty('--session-swipe-icon-scale',iconScale); + el.style.setProperty('--session-swipe-badge-stretch',badgeStretch+'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); @@ -3872,6 +3877,7 @@ function renderSessionListFromCache(){ el.style.removeProperty('--session-swipe-offset'); el.style.removeProperty('--session-swipe-reveal'); el.style.removeProperty('--session-swipe-icon-scale'); + el.style.removeProperty('--session-swipe-badge-stretch'); el.style.removeProperty('--session-swipe-progress'); el.style.removeProperty('height'); el.style.removeProperty('min-height'); diff --git a/static/style.css b/static/style.css index ab230249..51a95324 100644 --- a/static/style.css +++ b/static/style.css @@ -735,7 +735,7 @@ justify-content:center; gap:3px; width:var(--session-swipe-reveal,0px); - padding:0 8px; + padding:0; box-sizing:border-box; opacity:0; overflow:hidden; @@ -746,16 +746,18 @@ .session-swipe-affordance-right{ left:0; color:#fff; - background:var(--warning); - border-radius:8px 6px 6px 8px; + background:transparent; + border-radius:0; transform-origin:left center; + --session-swipe-action-color:var(--warning); } .session-swipe-affordance-left{ right:0; color:#fff; - background:var(--error); - border-radius:6px 8px 8px 6px; + background:transparent; + border-radius:0; transform-origin:right center; + --session-swipe-action-color:var(--error); } .session-item.swiping-right .session-swipe-affordance-right, .session-item.swiping-left .session-swipe-affordance-left{ @@ -763,13 +765,15 @@ } .session-text,.session-state-indicator,.session-actions{z-index:1;} .session-swipe-badge{ - width:28px; - height:28px; - border-radius:0; + width:calc(34px + var(--session-swipe-badge-stretch,0px)); + height:34px; + border-radius:999px; + background:var(--session-swipe-action-color); display:flex; align-items:center; justify-content:center; color:#fff; + flex:0 0 auto; transform:scale(var(--session-swipe-icon-scale,1)); transform-origin:center; } @@ -804,7 +808,7 @@ .session-item.active{background:var(--accent-bg);color:var(--accent);} .session-item.active.swiping-right{background:color-mix(in srgb,var(--warning) 20%,var(--accent-bg));} .session-item.active.swiping-left{background:color-mix(in srgb,var(--error) 18%,var(--accent-bg));} - .session-item.archived .session-swipe-affordance-right{background:var(--success);} + .session-item.archived .session-swipe-affordance-right{--session-swipe-action-color:var(--success);} .session-item.archived.swiping-right, .session-item.archived.dragging.swiping-right{background:color-mix(in srgb,var(--success) 16%,var(--surface));box-shadow:0 0 0 1px color-mix(in srgb,var(--success) 48%,transparent);} .session-item.active.archived.swiping-right{background:color-mix(in srgb,var(--success) 20%,var(--accent-bg));} diff --git a/tests/test_session_touch_actions.py b/tests/test_session_touch_actions.py index 6f896ef3..8d6fada3 100644 --- a/tests/test_session_touch_actions.py +++ b/tests/test_session_touch_actions.py @@ -121,12 +121,16 @@ def test_session_swipe_paint_uses_transform_only_exit(): assert "--session-swipe-offset" in paint assert "--session-swipe-reveal" in paint assert "--session-swipe-progress" in paint + assert "--session-swipe-badge-stretch" in paint assert "const reveal=Math.abs(offset);" in paint + assert "const iconScale=Math.min(1,Math.max(.01,progress*1.12));" in paint + assert "const badgeStretch=Math.min(Math.max(0,reveal-34),overshoot*1.15);" in paint assert "window.innerWidth+'px'" in complete assert "el.style.height=rect.height+'px'" in complete assert "requestAnimationFrame(()=>el.classList.add('swipe-removing'))" in complete assert "requestAnimationFrame(()=>requestAnimationFrame(_clearSessionSwipePaint))" in SESSIONS_JS assert "el.classList.remove('swiping-right','swiping-left','swipe-committed','swipe-removing')" in clear + assert "el.style.removeProperty('--session-swipe-badge-stretch');" in clear assert ".session-item.swipe-committed,\n .session-item.swipe-removing{transition:" in STYLE_CSS assert "transform:translate3d(calc(-1 * var(--session-swipe-offset,0px)),0,0)" in STYLE_CSS assert "transform:scale(var(--session-swipe-icon-scale,1))" in STYLE_CSS @@ -140,6 +144,29 @@ def test_session_swipe_paint_uses_transform_only_exit(): assert "transform:translate3d(var(--session-swipe-offset,0),var(--session-reflow-offset,0),0)" in STYLE_CSS +def test_session_swipe_actions_use_circular_icon_badges(): + right_start = STYLE_CSS.find(".session-swipe-affordance-right{") + left_start = STYLE_CSS.find(".session-swipe-affordance-left{") + badge_start = STYLE_CSS.find(".session-swipe-badge{") + right = STYLE_CSS[right_start:left_start] + left = STYLE_CSS[left_start:STYLE_CSS.find(".session-item.swiping-right", left_start)] + badge = STYLE_CSS[badge_start:STYLE_CSS.find(".session-swipe-badge svg", badge_start)] + assert "background:transparent" in right + assert "background:transparent" in left + assert "--session-swipe-action-color:var(--warning)" in right + assert "--session-swipe-action-color:var(--error)" in left + assert "width:calc(34px + var(--session-swipe-badge-stretch,0px))" in badge + assert "height:34px" in badge + assert "border-radius:999px" in badge + assert "background:var(--session-swipe-action-color)" in badge + assert ".session-item.archived .session-swipe-affordance-right{--session-swipe-action-color:var(--success);}" in STYLE_CSS + assert "_makeSessionSwipeAffordance('right',s.archived?'undo':'archive',s.archived?'Restore':t('session_batch_archive'))" in SESSIONS_JS + label = STYLE_CSS[STYLE_CSS.find(".session-swipe-label{"):STYLE_CSS.find(".session-item.dragging")] + assert "session-swipe-label" in SESSIONS_JS + assert "max-width:58px" in label + assert "font-size:10px" in label + + def test_session_removal_reflows_surviving_rows_smoothly(): assert "let _pendingSessionReflowPositions = null;" in SESSIONS_JS assert "const _optimisticallyRemovedSessionIds = new Set();" in SESSIONS_JS @@ -159,6 +186,16 @@ def test_session_removal_reflows_surviving_rows_smoothly(): assert "_playSessionRowsReflowFromPositions(reflowBefore,reflowTimeout,_sessionPrefersReducedMotion);" in SESSIONS_JS assert "async function _archiveSession(session, archived=true, beforeListRender=null){" in SESSIONS_JS assert "const renderHold=beforeListRender?Promise.resolve().then(beforeListRender):null;" in SESSIONS_JS + assert "const cached=(_allSessions||[]).find(s=>s&&s.session_id===session.session_id);" in SESSIONS_JS + assert "if(cached) cached.archived=archived;" in SESSIONS_JS + archive_start = SESSIONS_JS.find("async function _archiveSession(session, archived=true, beforeListRender=null){") + archive_end = SESSIONS_JS.find("function _openSessionActionMenu", archive_start) + archive_body = SESSIONS_JS[archive_start:archive_end] + toast_idx = archive_body.find("showToast(session.archived?_sessionArchiveToast(response,session):t('session_restored'));") + hold_idx = archive_body.find("if(renderHold) await renderHold;") + cache_render_idx = archive_body.find("renderSessionListFromCache();") + reconcile_idx = archive_body.find("void renderSessionList();") + assert 0 <= toast_idx < hold_idx < cache_render_idx < reconcile_idx assert "if(renderHold) await renderHold;" in SESSIONS_JS assert "const serverSessions=_optimisticallyRemovedSessionIds.size" in SESSIONS_JS assert "? (sessData.sessions||[]).filter(s=>s&&!_optimisticallyRemovedSessionIds.has(s.session_id))" in SESSIONS_JS