feat(ui): align skeleton height to real KPI cards + add page/tab/card entry animations

- KPI skeleton (.ksk) min-height now matches the real card (186px) with a title/value/chart bar layout so first paint doesn't jump
- global entry animations: cards fade+slide-up (fadeUp), tab panes fade+rise (tabIn), chat empty state fades in
- goTab restarts the tabIn animation on switch (reset animation + forced reflow)
- table-row animation intentionally NOT added (those re-render every 3s poll and would flicker)
- deployed (bak .bak.20260811v), server active
This commit is contained in:
root
2026-08-11 17:55:24 +08:00
parent 12c61317fb
commit dbee5188f8

View File

@ -102,10 +102,17 @@ pre.configbox { background:var(--card2); border:1px solid var(--line); border-ra
/* ---------- dashboard / usage metrics ---------- */
.kpi-skeletons { grid-column:1 / -1; display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:12px; }
.ksk { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:14px 16px; min-height:150px; display:flex; flex-direction:column; gap:12px; }
.ksk i { display:block; height:12px; border-radius:6px; background:linear-gradient(90deg,var(--card2) 25%,var(--line) 37%,var(--card2) 63%); background-size:400% 100%; animation:shimmer 1.2s infinite; }
.ksk i:nth-child(1){ width:40%; } .ksk i:nth-child(2){ width:70%; height:22px; } .ksk i:nth-child(3){ width:100%; height:64px; }
.ksk { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:14px 16px; min-height:186px; display:flex; flex-direction:column; gap:10px; }
.ksk i { display:block; height:11px; border-radius:6px; background:linear-gradient(90deg,var(--card2) 25%,var(--line) 37%,var(--card2) 63%); background-size:400% 100%; animation:shimmer 1.2s infinite; }
.ksk i:nth-child(1){ width:38%; align-self:flex-start; } .ksk i:nth-child(2){ width:68%; height:24px; } .ksk i:nth-child(3){ width:100%; height:86px; margin-top:4px; }
@keyframes shimmer { 0%{background-position:100% 0} 100%{background-position:0 0} }
/* entry / transition animations */
@keyframes fadeUp { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:none} }
@keyframes fadeIn { from{opacity:0} to{opacity:1} }
@keyframes tabIn { from{opacity:0;transform:translateY(6px) scale(.995)} to{opacity:1;transform:none} }
.card { animation:fadeUp .28s ease both; }
.tab-pane { animation:tabIn .25s ease; }
.chat-log .chat-empty { animation:fadeIn .3s ease; }
.kpis { display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:12px; margin-bottom:18px; }
.kpi { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:14px 16px; box-shadow:var(--shadow); }
.kpi .k-hdr { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:6px; }
@ -456,12 +463,12 @@ html[data-theme="dark"] .dropzone.dragover, html[data-theme="dark"] .dropzone:ho
</div>
</header>
<main>
<div id="tab-status"></div>
<div id="tab-chat" class="hidden"></div>
<div id="tab-keys" class="hidden"></div>
<div id="tab-sort" class="hidden"></div>
<div id="tab-sources" class="hidden"></div>
<div id="tab-adapters" class="hidden"></div>
<div id="tab-status" class="tab-pane"></div>
<div id="tab-chat" class="tab-pane hidden"></div>
<div id="tab-keys" class="tab-pane hidden"></div>
<div id="tab-sort" class="tab-pane hidden"></div>
<div id="tab-sources" class="tab-pane hidden"></div>
<div id="tab-adapters" class="tab-pane hidden"></div>
</main>
<div id="toast"></div>
<script>
@ -616,9 +623,14 @@ document.querySelectorAll('nav button').forEach(b => {
function goTab(name) {
document.querySelectorAll('nav button').forEach(x => x.classList.toggle('active', x.dataset.tab === name));
['status','chat','keys','sort','sources','adapters'].forEach(tn => $('#tab-' + tn).classList.toggle('hidden', tn !== name));
lastTab = name; refresh(lastTab);
const pane = $('#tab-' + name);
if (pane) {
pane.style.animation = 'none';
void pane.offsetHeight; // reflow to restart the CSS transition
pane.style.animation = '';
}
applyI18n();
lastTab = name; refresh(lastTab);
}applyI18n();
/* ---------- helpers ---------- */
const $ = s => document.querySelector(s);