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;
|
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">
|
||||||
|
|||||||
Reference in New Issue
Block a user