feat(chat): soften the committed-block reveal into a materialize cascade

The 140ms fade read as a pop next to the scroll glide. The reveal now
runs 320ms on an ease-out-quint curve with a 10px rise and a touch of
blur resolving — paint-only properties throughout, so row measurement is
untouched — and blocks committed in the same tick cascade with a 55ms
stagger instead of appearing together. Reduced motion still disables the
whole thing.
This commit is contained in:
Bohdan Triapitsyn
2026-08-25 23:08:36 +03:00
parent d71bcb5025
commit 5e37a436c1
2 changed files with 21 additions and 5 deletions
@@ -843,6 +843,7 @@ const useMorphdomMarkdown = ({
// Reconcile per block: only re-morph blocks whose content changed, leaving
// stable leading blocks untouched. Keeps per-stream-step DOM work bounded
// to the trailing (growing) block instead of the whole message.
let enteredThisPass = 0;
blocks.forEach((block, index) => {
let el = existing[index];
let isNewBlock = false;
@@ -863,9 +864,16 @@ const useMorphdomMarkdown = ({
// goes on the block's children — the wrapper is display:contents
// and cannot animate — and the transform never changes layout, so
// row measurement stays exact. Skipped for the first block so a
// full initial render does not shimmer.
// full initial render does not shimmer. Several blocks committed
// in one tick cascade with a small stagger instead of popping in
// together.
const delayMs = Math.min(enteredThisPass, 4) * 55;
enteredThisPass += 1;
for (const child of Array.from(temp.children)) {
child.classList.add('oc-md-block-enter');
if (delayMs > 0 && child instanceof HTMLElement) {
child.style.setProperty('--oc-md-enter-delay', `${delayMs}ms`);
}
}
}
const hadMermaidBlock = shouldRefreshMermaidViewers(el);
+12 -4
View File
@@ -1378,21 +1378,29 @@ html:not(.dark) .chat-scroll {
}
}
/* A block committed mid-stream enters with a short fade-and-rise; the
transform is paint-only, so virtualized row measurement is unaffected. */
/* A block committed mid-stream materializes: fade, a gentle rise, and a
touch of blur resolving. Transform and filter are paint-only, so
virtualized row measurement is unaffected. Blocks committed in the same
tick cascade via --oc-md-enter-delay set inline by the renderer. */
@keyframes oc-md-block-enter {
from {
opacity: 0;
transform: translateY(4px);
transform: translateY(10px);
filter: blur(2px);
}
60% {
filter: blur(0);
}
to {
opacity: 1;
transform: none;
filter: none;
}
}
.oc-md-block-enter {
animation: oc-md-block-enter 140ms ease-out both;
animation: oc-md-block-enter 320ms cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: var(--oc-md-enter-delay, 0ms);
}
@media (prefers-reduced-motion: reduce) {