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;
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">