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:
2026-05-21 00:03:45 -04:00
parent 111a412c3f
commit b398bc1469
+47 -68
View File
@@ -1441,53 +1441,6 @@
width: 28px; height: 28px; border-radius: 4px; object-fit: cover; width: 28px; height: 28px; border-radius: 4px; object-fit: cover;
border: 1px solid var(--border); 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 PAGE TRANSITION ENHANCEMENTS
@@ -1507,9 +1460,8 @@
} }
/* ═══════════════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════════════
PULL-TO-REFRESH UNDO TOAST (for swipe-delete)
═══════════════════════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════════════════════ */
.undo-toast { .undo-toast {
position: fixed; bottom: calc(var(--tab-height) + 12px); position: fixed; bottom: calc(var(--tab-height) + 12px);
left: 50%; transform: translateX(-50%); left: 50%; transform: translateX(-50%);
@@ -3126,6 +3078,8 @@
function renderActivity(items) { function renderActivity(items) {
var el = document.getElementById('actList'); var el = document.getElementById('actList');
// Fade out skeletons then render content
hideSkeletons('#actList .skel-wrapper');
if (!items.length) { if (!items.length) {
el.innerHTML = renderEmptyState('📜', 'No Activity Yet', el.innerHTML = renderEmptyState('📜', 'No Activity Yet',
'Activity will appear here when assets are created, updated, or checked in. Start by adding your first asset.', 'Activity will appear here when assets are created, updated, or checked in. Start by adding your first asset.',
@@ -3381,6 +3335,10 @@
ptrState = 'idle'; ptrState = 'idle';
updatePtrIndicator(0); updatePtrIndicator(0);
}, 200); }, 200);
}).catch(() => {
ptrState = 'idle';
updatePtrIndicator(0);
showToast('Refresh failed', true);
}); });
} else { } else {
ptrState = 'idle'; ptrState = 'idle';
@@ -3389,11 +3347,13 @@
} }
function initPullToRefresh() { function initPullToRefresh() {
if (_ptrInited) return;
const listView = document.getElementById('assetsListView'); const listView = document.getElementById('assetsListView');
if (!listView) return; if (!listView) return;
listView.addEventListener('touchstart', onPtrTouchStart, { passive: false }); listView.addEventListener('touchstart', onPtrTouchStart, { passive: false });
listView.addEventListener('touchmove', onPtrTouchMove, { passive: false }); listView.addEventListener('touchmove', onPtrTouchMove, { passive: false });
listView.addEventListener('touchend', onPtrTouchEnd, { passive: false }); listView.addEventListener('touchend', onPtrTouchEnd, { passive: false });
_ptrInited = true;
} }
// ========================================================================= // =========================================================================
@@ -3403,6 +3363,7 @@
let swipeStartX = 0; let swipeStartX = 0;
let swipeCurrentX = 0; let swipeCurrentX = 0;
let swipeActive = false; let swipeActive = false;
let _swipeInited = false;
const SWIPE_THRESHOLD = 60; const SWIPE_THRESHOLD = 60;
let swipedItem = null; // currently swiped-open item let swipedItem = null; // currently swiped-open item
@@ -3478,16 +3439,19 @@
async function swipeDeleteAsset(assetId, assetName) { async function swipeDeleteAsset(assetId, assetName) {
if (!assetId) return; if (!assetId) return;
closeSwipeItem(); // close any open swipe
const id = assetId; const id = assetId;
const name = assetName || 'asset'; 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 + '"]'); const itemEl = document.querySelector('.asset-item-swipe-wrap[data-asset-id="' + id + '"]');
if (itemEl) { if (itemEl) {
itemEl.style.opacity = '0.3'; itemEl.style.opacity = '0.3';
itemEl.style.pointerEvents = 'none'; itemEl.style.pointerEvents = 'none';
} }
showUndoToast('Deleted ' + name, async () => { let undoClicked = false;
// Undo: restore the item showUndoToast('Deleted ' + name, () => {
// Undo: restore the item visually, cancel delete
undoClicked = true;
if (itemEl) { if (itemEl) {
itemEl.style.opacity = '1'; itemEl.style.opacity = '1';
itemEl.style.pointerEvents = ''; itemEl.style.pointerEvents = '';
@@ -3497,10 +3461,10 @@
if (bg) bg.classList.remove('revealed'); if (bg) bg.classList.remove('revealed');
swipedItem = null; swipedItem = null;
} }
return;
}); });
// Delay actual delete to allow undo // Delay actual delete to allow undo
await new Promise(r => setTimeout(r, 5200)); await new Promise(r => setTimeout(r, 5200));
if (undoClicked) return; // User undid, skip delete
try { try {
await api('/api/assets/' + id, { method: 'DELETE' }); await api('/api/assets/' + id, { method: 'DELETE' });
showToast(name + ' deleted'); showToast(name + ' deleted');
@@ -3512,6 +3476,7 @@
} }
function initSwipeToDelete() { function initSwipeToDelete() {
if (_swipeInited) return;
document.addEventListener('touchstart', onSwipeTouchStart, { passive: false }); document.addEventListener('touchstart', onSwipeTouchStart, { passive: false });
document.addEventListener('touchmove', onSwipeTouchMove, { passive: false }); document.addEventListener('touchmove', onSwipeTouchMove, { passive: false });
document.addEventListener('touchend', onSwipeTouchEnd, { passive: false }); document.addEventListener('touchend', onSwipeTouchEnd, { passive: false });
@@ -3521,6 +3486,7 @@
closeSwipeItem(); closeSwipeItem();
} }
}); });
_swipeInited = true;
} }
// ========================================================================= // =========================================================================
@@ -3883,8 +3849,9 @@
switchTab('tabAddAsset'); switchTab('tabAddAsset');
// After switching, pulse the FAB briefly // After switching, pulse the FAB briefly
fab.classList.remove('pulse'); // Restart animation
void fab.offsetWidth; // Force reflow
fab.classList.add('pulse'); fab.classList.add('pulse');
setTimeout(() => fab.classList.remove('pulse'), 2500);
} }
// Show/hide FAB based on current tab // Show/hide FAB based on current tab
@@ -4396,7 +4363,7 @@
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
showToast('Asset created!'); showSuccessFeedback('Asset created!');
document.getElementById('newAssetCard').style.display = 'none'; document.getElementById('newAssetCard').style.display = 'none';
AppState.currentAssetId = asset.id; AppState.currentAssetId = asset.id;
autoCheckin(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) { document.addEventListener('tabChange', function(e) {
if (e.detail.tabId === 'tabAssets') { if (e.detail.tabId === 'tabAssets') {
setTimeout(initPullToRefresh, 100); setTimeout(initPullToRefresh, 100);
setTimeout(initSwipeToDelete, 150);
} }
}); });
@@ -5314,6 +5282,8 @@
// ── Skeleton loading helpers ────────────────────────────────────────── // ── Skeleton loading helpers ──────────────────────────────────────────
function renderAssetList(assets) { function renderAssetList(assets) {
const el = document.getElementById('assetList'); const el = document.getElementById('assetList');
// Fade out skeletons then render content
hideSkeletons('#assetList .skel-wrapper, #assetCountBar .skel');
if (!assets.length) { if (!assets.length) {
el.innerHTML = '<div class="empty-state"><div class="es-icon">📦</div>No assets found</div>'; el.innerHTML = '<div class="empty-state"><div class="es-icon">📦</div>No assets found</div>';
return; return;
@@ -5448,28 +5418,28 @@
document.getElementById('assetsListView').style.display = 'block'; document.getElementById('assetsListView').style.display = 'block';
loadAssets(); loadAssets();
const fab = document.getElementById('fabAdd'); const fab = document.getElementById('fabAdd');
if (fab) fab.style.display = ''; if (fab) fab.classList.remove('hidden-fab');
} }
function showDetailView() { function showDetailView() {
hideAllAssetViews(); hideAllAssetViews();
document.getElementById('assetsDetailView').style.display = 'block'; document.getElementById('assetsDetailView').style.display = 'block';
const fab = document.getElementById('fabAdd'); const fab = document.getElementById('fabAdd');
if (fab) fab.style.display = 'none'; if (fab) fab.classList.add('hidden-fab');
} }
function showEditView() { function showEditView() {
hideAllAssetViews(); hideAllAssetViews();
document.getElementById('assetsEditView').style.display = 'block'; document.getElementById('assetsEditView').style.display = 'block';
const fab = document.getElementById('fabAdd'); const fab = document.getElementById('fabAdd');
if (fab) fab.style.display = 'none'; if (fab) fab.classList.add('hidden-fab');
} }
function showImportView() { function showImportView() {
hideAllAssetViews(); hideAllAssetViews();
document.getElementById('assetsImportView').style.display = 'block'; document.getElementById('assetsImportView').style.display = 'block';
const fab = document.getElementById('fabAdd'); const fab = document.getElementById('fabAdd');
if (fab) fab.style.display = 'none'; if (fab) fab.classList.add('hidden-fab');
// Reset import state // Reset import state
document.getElementById('importFileInput').value = ''; document.getElementById('importFileInput').value = '';
document.getElementById('importPreview').style.display = 'none'; document.getElementById('importPreview').style.display = 'none';
@@ -6302,6 +6272,8 @@
function renderCustList(query) { function renderCustList(query) {
let custs = CustState.allCusts; let custs = CustState.allCusts;
// Fade out skeletons then render content
hideSkeletons('#custList .skel-wrapper');
if (query) { if (query) {
custs = custs.filter(c => c.name.toLowerCase().includes(query)); custs = custs.filter(c => c.name.toLowerCase().includes(query));
} }
@@ -8900,14 +8872,6 @@
initMouseDetection(); initMouseDetection();
initPullToRefresh(); initPullToRefresh();
initSwipeToDelete(); 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 ─────────────────────────── // ── Button ripple / haptic micro-animations ───────────────────────────
document.addEventListener('click', function(e) { document.addEventListener('click', function(e) {
@@ -8945,6 +8909,21 @@
</button> </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 --> <!-- Undo toast -->
<div class="undo-toast" id="undoToast"> <div class="undo-toast" id="undoToast">