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:
@@ -843,6 +843,7 @@ const useMorphdomMarkdown = ({
|
|||||||
// Reconcile per block: only re-morph blocks whose content changed, leaving
|
// Reconcile per block: only re-morph blocks whose content changed, leaving
|
||||||
// stable leading blocks untouched. Keeps per-stream-step DOM work bounded
|
// stable leading blocks untouched. Keeps per-stream-step DOM work bounded
|
||||||
// to the trailing (growing) block instead of the whole message.
|
// to the trailing (growing) block instead of the whole message.
|
||||||
|
let enteredThisPass = 0;
|
||||||
blocks.forEach((block, index) => {
|
blocks.forEach((block, index) => {
|
||||||
let el = existing[index];
|
let el = existing[index];
|
||||||
let isNewBlock = false;
|
let isNewBlock = false;
|
||||||
@@ -863,9 +864,16 @@ const useMorphdomMarkdown = ({
|
|||||||
// goes on the block's children — the wrapper is display:contents
|
// goes on the block's children — the wrapper is display:contents
|
||||||
// and cannot animate — and the transform never changes layout, so
|
// and cannot animate — and the transform never changes layout, so
|
||||||
// row measurement stays exact. Skipped for the first block so a
|
// 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)) {
|
for (const child of Array.from(temp.children)) {
|
||||||
child.classList.add('oc-md-block-enter');
|
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);
|
const hadMermaidBlock = shouldRefreshMermaidViewers(el);
|
||||||
|
|||||||
@@ -1378,21 +1378,29 @@ html:not(.dark) .chat-scroll {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* A block committed mid-stream enters with a short fade-and-rise; the
|
/* A block committed mid-stream materializes: fade, a gentle rise, and a
|
||||||
transform is paint-only, so virtualized row measurement is unaffected. */
|
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 {
|
@keyframes oc-md-block-enter {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(4px);
|
transform: translateY(10px);
|
||||||
|
filter: blur(2px);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
filter: blur(0);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: none;
|
transform: none;
|
||||||
|
filter: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.oc-md-block-enter {
|
.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) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
|||||||
Reference in New Issue
Block a user