diff --git a/static/index.html b/static/index.html index be5e23b..3a7150a 100644 --- a/static/index.html +++ b/static/index.html @@ -412,35 +412,6 @@ border-top-color: var(--accent); border-radius: 50%; animation: spin 0.7s linear infinite; } - /* ========================================================================= - FAB - Floating Action Button (pulse animation) - ========================================================================= */ - .fab { - position: fixed; bottom: calc(var(--tab-height) + 20px); right: 20px; - width: 56px; height: 56px; border-radius: 50%; - background: var(--accent); color: #fff; - border: none; font-size: 28px; font-weight: 300; cursor: pointer; - display: flex; align-items: center; justify-content: center; - box-shadow: 0 4px 16px rgba(91,110,247,0.35); - z-index: 90; - transition: transform 0.18s, box-shadow 0.18s; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - animation: fabPulse 3s cubic-bezier(0.4, 0, 0.2, 1) infinite; - } - .fab:hover { - transform: scale(1.08); - box-shadow: 0 6px 24px rgba(91,110,247,0.45); - } - .fab:active { - transform: scale(0.94); - animation: none; - } - @keyframes fabPulse { - 0%, 100% { box-shadow: 0 4px 16px rgba(91,110,247,0.35); } - 50% { box-shadow: 0 4px 28px rgba(91,110,247,0.55); } - } - - /* ═══════════════════════════════════════════════════════════════════════ SKELETON LOADING (shimmer) ═══════════════════════════════════════════════════════════════════════ */ @@ -1538,46 +1509,7 @@ /* ═══════════════════════════════════════════════════════════════════════ PULL-TO-REFRESH ═══════════════════════════════════════════════════════════════════════ */ - .ptr-indicator { - text-align: center; padding: 10px; color: var(--text2); - font-size: 12px; display: flex; align-items: center; justify-content: center; - gap: 8px; max-height: 0; overflow: hidden; - transition: max-height 0.25s ease; pointer-events: none; - } - .ptr-indicator.pulling { max-height: 44px; color: var(--accent2); } - .ptr-indicator.refreshing { max-height: 44px; } - .ptr-spinner { - display: inline-block; width: 18px; height: 18px; - border: 2px solid var(--border); border-top-color: var(--accent); - border-radius: 50%; animation: spin 0.7s linear infinite; - } - .ptr-arrow { - font-size: 16px; transition: transform 0.2s; - } - .ptr-indicator.pulling .ptr-arrow { transform: rotate(180deg); } - /* ═══════════════════════════════════════════════════════════════════════ - SWIPE-TO-DELETE - ═══════════════════════════════════════════════════════════════════════ */ - .swipe-item-wrap { - position: relative; overflow: hidden; - } - .swipe-bg { - position: absolute; right: 0; top: 0; bottom: 0; - background: var(--red); color: #fff; - display: flex; align-items: center; justify-content: center; - font-size: 16px; font-weight: 600; padding: 0 20px; - transition: width 0.2s; pointer-events: none; z-index: 0; - width: 0; overflow: hidden; white-space: nowrap; - } - .swipe-item-wrap.swiped .swipe-bg { width: 80px; } - .asset-item.swipe-dragging { - transition: none !important; - } - - /* ═══════════════════════════════════════════════════════════════════════ - UNDO TOAST (for swipe-delete) - ═══════════════════════════════════════════════════════════════════════ */ .undo-toast { position: fixed; bottom: calc(var(--tab-height) + 12px); left: 50%; transform: translateX(-50%); @@ -1609,6 +1541,7 @@ -webkit-tap-highlight-color: transparent; } .fab:active { transform: scale(0.88); box-shadow: 0 2px 8px rgba(91,110,247,0.3); } + .fab:hover { transform: scale(1.08); box-shadow: 0 6px 24px rgba(91,110,247,0.55); } @keyframes fabPulse { 0%, 100% { box-shadow: 0 4px 18px rgba(91,110,247,0.45); } 50% { box-shadow: 0 4px 28px rgba(91,110,247,0.65); } @@ -2875,11 +2808,6 @@ ═══════════════════════════════════════════════════════════════════════ -->
- - - @@ -3349,10 +3277,10 @@ let undoToastTimer; let undoToastCallback = null; function showUndoToast(msg, onUndo) { - const t = document.getElementById('toast'); + const t = document.getElementById('undoToast'); + if (!t) return; undoToastCallback = onUndo; - t.innerHTML = '' + msg + ''; - t.className = 'toast'; + t.querySelector('span').textContent = '🗑️ ' + msg; t.classList.add('show'); clearTimeout(undoToastTimer); clearTimeout(toastTimer); @@ -3361,12 +3289,18 @@ if (undoToastCallback) { undoToastCallback = null; } }, 5000); } - function handleUndoToast() { + function undoDelete() { clearTimeout(undoToastTimer); - const t = document.getElementById('toast'); - t.classList.remove('show'); + const t = document.getElementById('undoToast'); + if (t) t.classList.remove('show'); if (undoToastCallback) { const cb = undoToastCallback; undoToastCallback = null; cb(); } } + function dismissUndo() { + clearTimeout(undoToastTimer); + const t = document.getElementById('undoToast'); + if (t) t.classList.remove('show'); + undoToastCallback = null; + } // ========================================================================= // PULL-TO-REFRESH @@ -3375,6 +3309,7 @@ let ptrStartY = 0; let ptrCurrentY = 0; let ptrTouchActive = false; + let _ptrInited = false; const PTR_THRESHOLD = 60; const PTR_MAX_PULL = 120; @@ -3854,6 +3789,9 @@ // Close more popover if open closeMorePopover(); + // Detect reverse direction: going back to where we just came from + const isReverse = (tabId === AppState._prevTab); + // Track previous tab for back navigation if (AppState.currentTab !== tabId && AppState.currentTab !== 'tabNavigate') { AppState._prevTab = AppState.currentTab; @@ -3871,18 +3809,18 @@ if (oldPanel && oldPanel !== newPanel) { // Start entrance animation on new panel immediately if (newPanel) { - newPanel.classList.add('active', 'anim-enter'); + newPanel.classList.add('active', isReverse ? 'anim-enter-reverse' : 'anim-enter'); newPanel.addEventListener('animationend', function handler() { newPanel.removeEventListener('animationend', handler); - newPanel.classList.remove('anim-enter'); + newPanel.classList.remove(isReverse ? 'anim-enter-reverse' : 'anim-enter'); }, { once: true }); } // Animate old panel out while new slides in - oldPanel.classList.add('anim-exit'); + oldPanel.classList.add(isReverse ? 'anim-exit-reverse' : 'anim-exit'); oldPanel.classList.remove('active'); oldPanel.addEventListener('animationend', function handler() { oldPanel.removeEventListener('animationend', handler); - oldPanel.classList.remove('anim-exit'); + oldPanel.classList.remove(isReverse ? 'anim-exit-reverse' : 'anim-exit'); }, { once: true }); } else if (newPanel) { newPanel.classList.add('active'); @@ -3929,12 +3867,6 @@ if (tabId === 'tabReports') loadReports(); if (tabId === 'tabNavigate') renderNavPage(); - // FAB visibility — show only on Assets tab - const fab = document.getElementById('fabAdd'); - if (fab) { - fab.style.display = (tabId === 'tabAssets') ? '' : 'none'; - } - // Dispatch event so child tab implementations can hook in document.dispatchEvent(new CustomEvent('tabChange', { detail: { tabId } })); } @@ -5353,87 +5285,6 @@ } - // ── Pull-to-refresh ─────────────────────────────────────────────────── - let ptrState = { startY: 0, pulling: false, refreshing: false, dist: 0 }; - - function initPullToRefresh() { - const wrap = document.getElementById('assetListWrap'); - if (!wrap) return; - - // Create indicator - let indicator = document.getElementById('ptrIndicator'); - if (!indicator) { - indicator = document.createElement('div'); - indicator.id = 'ptrIndicator'; - indicator.className = 'ptr-indicator'; - indicator.innerHTML = '⇣ Pull to refresh'; - wrap.parentNode.insertBefore(indicator, wrap); - } - - wrap.addEventListener('touchstart', function(e) { - if (ptrState.refreshing) return; - // Only trigger when scrolled to top - if (wrap.scrollTop <= 0 && document.getElementById('assetSearch') !== document.activeElement) { - ptrState.startY = e.touches[0].clientY; - ptrState.pulling = true; - } - }, { passive: true }); - - wrap.addEventListener('touchmove', function(e) { - if (!ptrState.pulling || ptrState.refreshing) return; - const dist = e.touches[0].clientY - ptrState.startY; - if (dist > 20) { - ptrState.dist = dist; - const indicator = document.getElementById('ptrIndicator'); - if (indicator) { - indicator.className = 'ptr-indicator pulling'; - indicator.innerHTML = dist > 60 - ? '⇡ Release to refresh' - : '⇣ Pull to refresh'; - } - e.preventDefault(); - } - }, { passive: false }); - - wrap.addEventListener('touchend', function(e) { - if (!ptrState.pulling || ptrState.refreshing) return; - ptrState.pulling = false; - const indicator = document.getElementById('ptrIndicator'); - - if (ptrState.dist > 60) { - // Trigger refresh - ptrState.refreshing = true; - if (indicator) { - indicator.className = 'ptr-indicator refreshing'; - indicator.innerHTML = ' Refreshing...'; - } - // Reset cache and reload - totalAssetCount = 0; - masterCategories = []; - loadAssets().then(() => { - ptrState.refreshing = false; - if (indicator) { - indicator.className = 'ptr-indicator'; - indicator.innerHTML = '⇣ Pull to refresh'; - } - showToast('List refreshed'); - }).catch(() => { - ptrState.refreshing = false; - if (indicator) { - indicator.className = 'ptr-indicator'; - indicator.innerHTML = '⇣ Pull to refresh'; - } - }); - } else { - if (indicator) { - indicator.className = 'ptr-indicator'; - indicator.innerHTML = '⇣ Pull to refresh'; - } - } - ptrState.dist = 0; - }, { passive: true }); - } - // Initialize PTR when assets tab loads document.addEventListener('tabChange', function(e) { if (e.detail.tabId === 'tabAssets') { @@ -5458,136 +5309,6 @@ } } - // ── Swipe-to-delete ─────────────────────────────────────────────────── - let swipeState = { startX: 0, startY: 0, swiping: false, item: null, assetId: null, assetName: '' }; - let undoData = null; - let undoTimer = null; - - function initSwipeToDelete() { - const list = document.getElementById('assetList'); - if (!list) return; - - list.addEventListener('touchstart', function(e) { - const item = e.target.closest('.asset-item'); - if (!item) return; - swipeState.item = item; - swipeState.startX = e.touches[0].clientX; - swipeState.startY = e.touches[0].clientY; - swipeState.swiping = false; - - // Get asset id from onclick - const onclick = item.getAttribute('onclick') || ''; - const match = onclick.match(/viewAsset\((\d+)\)/); - swipeState.assetId = match ? parseInt(match[1]) : null; - }, { passive: true }); - - list.addEventListener('touchmove', function(e) { - if (!swipeState.item || !swipeState.assetId) return; - const dx = e.touches[0].clientX - swipeState.startX; - const dy = e.touches[0].clientY - swipeState.startY; - - // Only horizontal swipe (reject vertical scrolling) - if (Math.abs(dx) > 15 && Math.abs(dx) > Math.abs(dy) * 1.5 && dx < 0) { - swipeState.swiping = true; - swipeState.item.classList.add('swipe-dragging'); - const tx = Math.max(dx, -100); - swipeState.item.style.transform = 'translateX(' + tx + 'px)'; - swipeState.item.style.opacity = (1 + tx / 200); - e.preventDefault(); - } - }, { passive: false }); - - list.addEventListener('touchend', function(e) { - if (!swipeState.item) return; - swipeState.item.classList.remove('swipe-dragging'); - - if (swipeState.swiping) { - // Check if swiped far enough - const dx = parseFloat(swipeState.item.style.transform?.replace('translateX(', '').replace('px)', '') || '0'); - if (dx < -50 && swipeState.assetId) { - // Delete with undo - swipeDeleteAsset(swipeState.assetId); - } else { - // Snap back - swipeState.item.style.transform = ''; - swipeState.item.style.opacity = ''; - } - } - swipeState.item = null; - swipeState.swiping = false; - }, { passive: true }); - } - - async function swipeDeleteAsset(id) { - try { - // Get asset name for undo toast - const asset = cachedAssets.find(a => a.id === id); - const name = asset ? asset.name : 'Asset'; - - // Store for undo - undoData = { id: id, name: name }; - - // Delete via API - await api('/api/assets/' + id, 'DELETE'); - - // Remove from local cache - cachedAssets = cachedAssets.filter(a => a.id !== id); - totalAssetCount = Math.max(0, totalAssetCount - 1); - renderAssetList(cachedAssets); - updateCountBar(cachedAssets.length); - - // Show undo toast - showUndoToast(name); - - // Clear undo after timeout - clearTimeout(undoTimer); - undoTimer = setTimeout(() => { undoData = null; }, 6000); - - } catch (e) { - showToast('Failed to delete: ' + e.message, true); - } - } - - function showUndoToast(name) { - const toast = document.getElementById('undoToast'); - if (!toast) return; - toast.querySelector('span').textContent = '🗑️ Deleted ' + name; - toast.classList.add('show'); - } - - function undoDelete() { - if (!undoData) return; - const { id, name } = undoData; - - // Remove from current list view temporarily (will be restored on next load) - // Actually, just reload to restore proper state - const toast = document.getElementById('undoToast'); - if (toast) toast.classList.remove('show'); - - // We can't easily undo a DELETE without the original data, - // so just show a note and flag to reload - showToast('Reload the list to restore — undo requires re-adding.', true); - undoData = null; - clearTimeout(undoTimer); - - // Reload assets to get accurate state - totalAssetCount = 0; - loadAssets(); - } - - function dismissUndo() { - const toast = document.getElementById('undoToast'); - if (toast) toast.classList.remove('show'); - undoData = null; - clearTimeout(undoTimer); - } - - // Init swipe when assets tab loads - document.addEventListener('tabChange', function(e) { - if (e.detail.tabId === 'tabAssets') { - setTimeout(initSwipeToDelete, 150); - } - }); // ── Skeleton loading helpers ──────────────────────────────────────────