Show the build id, tame long table cells, and add purposeful motion

The build identifier now sits in the sidebar footer. "Is my deploy actually
live?" was only answerable by opening Session & Security, which is the wrong
place for the first question asked after every deploy.

Error-log rows carry stack traces and full URLs. They stretched their row far
past the viewport and pushed the other columns out of view, which is exactly
where the console is least usable. Values over 70 characters truncate and
expand on click.

Motion is limited to two things that mean something: content arriving
(staggered, so a grid resolves instead of snapping), and a fetch being in
flight — the refresh control spins and the status pill pulses, so a section
that legitimately returns nothing still shows that it tried. All of it is
disabled under prefers-reduced-motion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-25 17:49:48 +03:00
co-authored by Claude Opus 5
parent f9110a7d92
commit 3fb7bc5190
3 changed files with 117 additions and 1 deletions
+83
View File
@@ -1508,3 +1508,86 @@ textarea.form-input { text-align: start; }
direction: ltr;
text-align: start;
}
/* Build identifier, always visible in the sidebar footer */
.build-stamp {
padding: 0.45rem 1.25rem 0.75rem;
font-size: 0.68rem;
letter-spacing: 0.04em;
color: var(--text-subtle);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
direction: ltr;
text-align: center;
border-top: 1px solid var(--border-color);
}
/* Long values in generated tables */
.cell-long {
cursor: pointer;
border-bottom: 1px dotted var(--text-subtle);
}
.cell-long:hover { color: var(--primary); }
.cell-full {
display: block;
max-width: 460px;
white-space: pre-wrap;
word-break: break-word;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.74rem;
color: var(--text-muted);
}
.data-table td { max-width: 420px; }
/* ==========================================================================
Motion — used sparingly, and only where it carries meaning:
arrival of content, and the moment data is being fetched.
========================================================================== */
@keyframes riseIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.page-view.active .stat-card,
.page-view.active > .card,
.module-panels > .card {
animation: riseIn 0.34s cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
/* Stagger so a grid resolves left-to-right instead of snapping in at once */
.page-view.active .stats-grid .stat-card:nth-child(1) { animation-delay: 0.00s; }
.page-view.active .stats-grid .stat-card:nth-child(2) { animation-delay: 0.05s; }
.page-view.active .stats-grid .stat-card:nth-child(3) { animation-delay: 0.10s; }
.page-view.active .stats-grid .stat-card:nth-child(4) { animation-delay: 0.15s; }
.module-panels > .card:nth-child(2) { animation-delay: 0.06s; }
.module-panels > .card:nth-child(3) { animation-delay: 0.12s; }
.stat-card, .card { transition: transform 0.25s ease, border-color 0.25s ease; }
.stat-card:hover { transform: translateY(-3px); }
/* Rows settle in rather than appearing fully formed */
.data-table tbody tr { animation: riseIn 0.25s ease backwards; }
.data-table tbody tr:nth-child(1) { animation-delay: 0.02s; }
.data-table tbody tr:nth-child(2) { animation-delay: 0.04s; }
.data-table tbody tr:nth-child(3) { animation-delay: 0.06s; }
.data-table tbody tr:nth-child(4) { animation-delay: 0.08s; }
.data-table tbody tr:nth-child(n+5) { animation-delay: 0.10s; }
/* The refresh control spins while a request is in flight, so a section that
returns nothing still visibly did something. */
@keyframes spin { to { transform: rotate(360deg); } }
.btn-icon.is-busy i { animation: spin 0.8s linear infinite; }
.live-indicator[data-state="loading"] { animation: pulseSoft 1.4s ease-in-out infinite; }
@keyframes pulseSoft { 50% { opacity: 0.55; } }
/* Respect users who ask the system for less motion */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
+4
View File
@@ -134,6 +134,10 @@
<i class="ph ph-sign-out"></i>
</button>
</div>
<!-- Always-visible build id: "is my deploy live?" must be answerable at
a glance, without opening a settings page. -->
<div class="build-stamp" id="buildStamp" title="Console build">—
</div>
</aside>
<!-- Main Content Body -->
+30 -1
View File
@@ -560,6 +560,8 @@
applyLanguage();
deviceFingerprint = await resolveFingerprint();
if (el.fpPreview) el.fpPreview.textContent = deviceFingerprint.slice(0, 12) + '…';
const stamp = $('buildStamp');
if (stamp) stamp.textContent = `build ${BUILD}`;
buildModules();
setupNavigation();
@@ -3149,13 +3151,23 @@
return wrap;
}
const LONG_CELL = 70;
function cellHtml(column, value) {
if (value === null || value === undefined || value === '') return '<span class="stamp">—</span>';
if (typeof value === 'object') return `<span class="stamp">${esc(JSON.stringify(value).slice(0, 60))}</span>`;
if (/status|type|state|result/i.test(column)) {
return `<span class="badge ${badgeClass(value)}">${esc(labelStatus(value))}</span>`;
}
return esc(formatValue(value, column));
const text = formatValue(value, column);
// Stack traces and URLs would otherwise stretch the row far past the
// viewport and push every other column out of view.
if (String(text).length > LONG_CELL) {
return `<span class="cell-long" title="click to expand">${esc(String(text).slice(0, LONG_CELL))}…</span>
<span class="cell-full" hidden>${esc(String(text))}</span>`;
}
return esc(text);
}
function formatValue(value, column = '') {
@@ -3667,6 +3679,11 @@
};
function refreshView(viewId, moduleId) {
setConnection('loading', t('Loading live data…'));
setTimeout(() => {
if (el.connectionPill?.dataset.state === 'loading') setConnection('live', t('Live database'));
}, 900);
const mod = MODULES.find((m) => m.id === moduleId);
if (mod) {
loadModule(mod, true); // force: bypass the loaded-once cache
@@ -3711,6 +3728,15 @@
});
});
document.addEventListener('click', (e) => {
const cell = e.target.closest('.cell-long');
if (!cell) return;
const full = cell.nextElementSibling;
if (!full?.classList.contains('cell-full')) return;
cell.hidden = true;
full.hidden = false;
});
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
@@ -3748,6 +3774,9 @@
if (!el.connectionPill) return;
el.connectionPill.dataset.state = state;
el.connectionText.textContent = text;
// Visible feedback that a fetch is running: a section with no data still
// needs to show that it tried.
el.refreshBtn?.classList.toggle('is-busy', state === 'loading');
}
function busy(btn, isBusy, label) {