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).
This commit is contained in:
+47
-68
@@ -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 = '<div class="empty-state"><div class="es-icon">📦</div>No assets found</div>';
|
||||
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 @@
|
||||
➕
|
||||
</button>
|
||||
|
||||
<script>
|
||||
// ── FAB initial visibility ─────────────────────────────────────────────
|
||||
(function() {
|
||||
const fab = document.getElementById('fabAdd');
|
||||
if (fab) {
|
||||
const activePanel = document.querySelector('.tab-panel.active');
|
||||
if (activePanel && activePanel.id === 'tabAssets') {
|
||||
fab.classList.remove('hidden-fab');
|
||||
} else {
|
||||
fab.classList.add('hidden-fab');
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
<!-- Undo toast -->
|
||||
<div class="undo-toast" id="undoToast">
|
||||
|
||||
Reference in New Issue
Block a user