T6 cleanup: remove scope-creep FAB, pull-to-refresh, skeleton loading — map features preserved
This commit is contained in:
+21
-300
@@ -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 @@
|
||||
═══════════════════════════════════════════════════════════════════════ -->
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<!-- =========================================================================
|
||||
FAB — Floating Action Button (quick add asset)
|
||||
========================================================================= -->
|
||||
<button class="fab" id="fabAdd" onclick="switchTab('tabAddAsset')" title="Add Asset" aria-label="Add Asset">+</button>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||
MODAL
|
||||
═══════════════════════════════════════════════════════════════════════ -->
|
||||
@@ -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 = '<span>' + msg + '</span><button class="toast-undo-btn" onclick="handleUndoToast()">Undo</button>';
|
||||
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 = '<span class="ptr-arrow">⇣</span> 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
|
||||
? '<span class="ptr-arrow">⇡</span> Release to refresh'
|
||||
: '<span class="ptr-arrow">⇣</span> 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 = '<div class="ptr-spinner"></div> Refreshing...';
|
||||
}
|
||||
// Reset cache and reload
|
||||
totalAssetCount = 0;
|
||||
masterCategories = [];
|
||||
loadAssets().then(() => {
|
||||
ptrState.refreshing = false;
|
||||
if (indicator) {
|
||||
indicator.className = 'ptr-indicator';
|
||||
indicator.innerHTML = '<span class="ptr-arrow">⇣</span> Pull to refresh';
|
||||
}
|
||||
showToast('List refreshed');
|
||||
}).catch(() => {
|
||||
ptrState.refreshing = false;
|
||||
if (indicator) {
|
||||
indicator.className = 'ptr-indicator';
|
||||
indicator.innerHTML = '<span class="ptr-arrow">⇣</span> Pull to refresh';
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (indicator) {
|
||||
indicator.className = 'ptr-indicator';
|
||||
indicator.innerHTML = '<span class="ptr-arrow">⇣</span> 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 ──────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user