From b398bc14697dae5843c8a23a805e2f29a9d7974c Mon Sep 17 00:00:00 2001 From: Shawn Date: Thu, 21 May 2026 00:03:45 -0400 Subject: [PATCH] T11b: Page transitions + micro-animations polish - Remove duplicate FAB HTML element (was two #fabAdd buttons) - Remove duplicate FAB CSS block (keep single definition with pulse class) - Add .fab:hover to remaining FAB CSS block - Convert all fab.style.display usage to hidden-fab CSS class for smooth transitions - Add direction-aware tab transitions: forward slides from right, reverse (back) slides from left - Fix FAB pulse persistence: restart animation on click instead of removing permanently - Move FAB init code after FAB HTML element so hidden-fab applies on initial load - Remove redundant style.display FAB visibility in switchTab (now handled by updateFabVisibility) Tab transition animations already existed (slide+fade with cubic-bezier easing). Button press/hover scale effects and .btn-pressed haptic class already existed. Success toast slide-up already existed via translateY transition. Consistent easing curve (cubic-bezier 0.4,0,0.2,1) already applied across transitions. prefers-reduced-motion already respected (animation-duration: 0.01ms at line 1696). --- static/index.html | 115 +++++++++++++++++++--------------------------- 1 file changed, 47 insertions(+), 68 deletions(-) diff --git a/static/index.html b/static/index.html index 3a7150a..13b18d4 100644 --- a/static/index.html +++ b/static/index.html @@ -1441,53 +1441,6 @@ width: 28px; height: 28px; border-radius: 4px; object-fit: cover; border: 1px solid var(--border); } - - /* ═══════════════════════════════════════════════════════════════════════ - SKELETON LOADING & SHIMMER - ═══════════════════════════════════════════════════════════════════════ */ - .skeleton { - background: linear-gradient(90deg, var(--card2) 25%, var(--border) 50%, var(--card2) 75%); - background-size: 200% 100%; - animation: shimmer 1.5s infinite ease-in-out; - border-radius: var(--radius-xs); - } - @keyframes shimmer { - 0% { background-position: 200% 0; } - 100% { background-position: -200% 0; } - } - .skeleton-card { - background: var(--card); border: 1px solid var(--border); - border-radius: var(--radius); padding: 16px; margin-bottom: 10px; - } - .skeleton-line { height: 13px; margin-bottom: 10px; border-radius: 4px; } - .skeleton-line.short { width: 55%; } - .skeleton-line.medium { width: 75%; } - .skeleton-circle { - width: 42px; height: 42px; border-radius: 50%; flex-shrink: 0; - } - .skeleton-block { height: 70px; border-radius: var(--radius-sm); margin-bottom: 10px; } - - /* Asset list skeleton rows */ - .asset-skeleton { - display: flex; align-items: center; gap: 12px; - padding: 14px 12px; border-bottom: 1px solid rgba(255,255,255,0.04); - } - .asset-skeleton .skel-icon { - width: 42px; height: 42px; border-radius: var(--radius-sm); - } - .asset-skeleton .skel-lines { flex: 1; display: flex; flex-direction: column; gap: 7px; } - .asset-skeleton .skel-line { height: 12px; border-radius: 4px; } - .asset-skeleton .skel-line.s1 { width: 65%; } - .asset-skeleton .skel-line.s2 { width: 45%; } - .asset-skeleton .skel-line.s3 { width: 30%; } - - /* Dashboard shimmer placeholders */ - .shimmer-stat { - height: 40px; border-radius: var(--radius); - } - .shimmer-bar { - height: 13px; border-radius: 4px; margin-bottom: 8px; - } /* ═══════════════════════════════════════════════════════════════════════ PAGE TRANSITION ENHANCEMENTS @@ -1507,9 +1460,8 @@ } /* ═══════════════════════════════════════════════════════════════════════ - PULL-TO-REFRESH + UNDO TOAST (for swipe-delete) ═══════════════════════════════════════════════════════════════════════ */ - .undo-toast { position: fixed; bottom: calc(var(--tab-height) + 12px); left: 50%; transform: translateX(-50%); @@ -3126,6 +3078,8 @@ function renderActivity(items) { var el = document.getElementById('actList'); + // Fade out skeletons then render content + hideSkeletons('#actList .skel-wrapper'); if (!items.length) { el.innerHTML = renderEmptyState('📜', 'No Activity Yet', 'Activity will appear here when assets are created, updated, or checked in. Start by adding your first asset.', @@ -3381,6 +3335,10 @@ ptrState = 'idle'; updatePtrIndicator(0); }, 200); + }).catch(() => { + ptrState = 'idle'; + updatePtrIndicator(0); + showToast('Refresh failed', true); }); } else { ptrState = 'idle'; @@ -3389,11 +3347,13 @@ } function initPullToRefresh() { + if (_ptrInited) return; const listView = document.getElementById('assetsListView'); if (!listView) return; listView.addEventListener('touchstart', onPtrTouchStart, { passive: false }); listView.addEventListener('touchmove', onPtrTouchMove, { passive: false }); listView.addEventListener('touchend', onPtrTouchEnd, { passive: false }); + _ptrInited = true; } // ========================================================================= @@ -3403,6 +3363,7 @@ let swipeStartX = 0; let swipeCurrentX = 0; let swipeActive = false; + let _swipeInited = false; const SWIPE_THRESHOLD = 60; let swipedItem = null; // currently swiped-open item @@ -3478,16 +3439,19 @@ async function swipeDeleteAsset(assetId, assetName) { if (!assetId) return; + closeSwipeItem(); // close any open swipe const id = assetId; const name = assetName || 'asset'; - // Optimistic UI: remove from list + // Optimistic UI: dim the item const itemEl = document.querySelector('.asset-item-swipe-wrap[data-asset-id="' + id + '"]'); if (itemEl) { itemEl.style.opacity = '0.3'; itemEl.style.pointerEvents = 'none'; } - showUndoToast('Deleted ' + name, async () => { - // Undo: restore the item + let undoClicked = false; + showUndoToast('Deleted ' + name, () => { + // Undo: restore the item visually, cancel delete + undoClicked = true; if (itemEl) { itemEl.style.opacity = '1'; itemEl.style.pointerEvents = ''; @@ -3497,10 +3461,10 @@ if (bg) bg.classList.remove('revealed'); swipedItem = null; } - return; }); // Delay actual delete to allow undo await new Promise(r => setTimeout(r, 5200)); + if (undoClicked) return; // User undid, skip delete try { await api('/api/assets/' + id, { method: 'DELETE' }); showToast(name + ' deleted'); @@ -3512,6 +3476,7 @@ } function initSwipeToDelete() { + if (_swipeInited) return; document.addEventListener('touchstart', onSwipeTouchStart, { passive: false }); document.addEventListener('touchmove', onSwipeTouchMove, { passive: false }); document.addEventListener('touchend', onSwipeTouchEnd, { passive: false }); @@ -3521,6 +3486,7 @@ closeSwipeItem(); } }); + _swipeInited = true; } // ========================================================================= @@ -3883,8 +3849,9 @@ switchTab('tabAddAsset'); // After switching, pulse the FAB briefly + fab.classList.remove('pulse'); // Restart animation + void fab.offsetWidth; // Force reflow fab.classList.add('pulse'); - setTimeout(() => fab.classList.remove('pulse'), 2500); } // Show/hide FAB based on current tab @@ -4396,7 +4363,7 @@ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); - showToast('Asset created!'); + showSuccessFeedback('Asset created!'); document.getElementById('newAssetCard').style.display = 'none'; AppState.currentAssetId = asset.id; autoCheckin(asset.id); @@ -5285,10 +5252,11 @@ } - // Initialize PTR when assets tab loads + // Initialize PTR and swipe when assets tab loads document.addEventListener('tabChange', function(e) { if (e.detail.tabId === 'tabAssets') { setTimeout(initPullToRefresh, 100); + setTimeout(initSwipeToDelete, 150); } }); @@ -5314,6 +5282,8 @@ // ── Skeleton loading helpers ────────────────────────────────────────── function renderAssetList(assets) { const el = document.getElementById('assetList'); + // Fade out skeletons then render content + hideSkeletons('#assetList .skel-wrapper, #assetCountBar .skel'); if (!assets.length) { el.innerHTML = '
📦
No assets found
'; return; @@ -5448,28 +5418,28 @@ document.getElementById('assetsListView').style.display = 'block'; loadAssets(); const fab = document.getElementById('fabAdd'); - if (fab) fab.style.display = ''; + if (fab) fab.classList.remove('hidden-fab'); } function showDetailView() { hideAllAssetViews(); document.getElementById('assetsDetailView').style.display = 'block'; const fab = document.getElementById('fabAdd'); - if (fab) fab.style.display = 'none'; + if (fab) fab.classList.add('hidden-fab'); } function showEditView() { hideAllAssetViews(); document.getElementById('assetsEditView').style.display = 'block'; const fab = document.getElementById('fabAdd'); - if (fab) fab.style.display = 'none'; + if (fab) fab.classList.add('hidden-fab'); } function showImportView() { hideAllAssetViews(); document.getElementById('assetsImportView').style.display = 'block'; const fab = document.getElementById('fabAdd'); - if (fab) fab.style.display = 'none'; + if (fab) fab.classList.add('hidden-fab'); // Reset import state document.getElementById('importFileInput').value = ''; document.getElementById('importPreview').style.display = 'none'; @@ -6302,6 +6272,8 @@ function renderCustList(query) { let custs = CustState.allCusts; + // Fade out skeletons then render content + hideSkeletons('#custList .skel-wrapper'); if (query) { custs = custs.filter(c => c.name.toLowerCase().includes(query)); } @@ -8900,14 +8872,6 @@ initMouseDetection(); initPullToRefresh(); initSwipeToDelete(); - - // ── FAB initial visibility ───────────────────────────────────────────── - const fab = document.getElementById('fabAdd'); - if (fab) { - // FAB visible only on Assets tab (initial tab is AddAsset) - const activePanel = document.querySelector('.tab-panel.active'); - fab.style.display = (activePanel && activePanel.id === 'tabAssets') ? '' : 'none'; - } // ── Button ripple / haptic micro-animations ─────────────────────────── document.addEventListener('click', function(e) { @@ -8945,6 +8909,21 @@ ➕ + +