feat(web): animate structured session interactions
This commit is contained in:
@@ -325,6 +325,7 @@ describe("structured connection: data flow", () => {
|
||||
transcript: { type: "append", row: row("agent:1", "agent_text", { content: { type: "text", text: "partial" } }) },
|
||||
state: idleState,
|
||||
});
|
||||
expect(conn.el.querySelector('[data-row-id="agent:1"]')?.classList).toContain("st-row-enter");
|
||||
ws.emit({
|
||||
type: "projection_update",
|
||||
transcript: { type: "patch", row: row("agent:1", "agent_text", { content: { type: "text", text: "partial done" } }) },
|
||||
@@ -332,6 +333,7 @@ describe("structured connection: data flow", () => {
|
||||
});
|
||||
const rows = conn.el.querySelectorAll(".st-row-agent");
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].classList).not.toContain("st-row-enter");
|
||||
expect(conn.el.textContent).toContain("partial done");
|
||||
expect(conn.el.textContent).not.toContain("partial\n"); // the un-patched text is gone, not appended beside it
|
||||
conn.dispose();
|
||||
@@ -357,7 +359,7 @@ describe("structured connection: data flow", () => {
|
||||
const groups = conn.el.querySelectorAll<HTMLElement>(".st-tool-group");
|
||||
expect(groups).toHaveLength(1);
|
||||
expect(groups[0].querySelector(".st-tool-group-head")!.textContent).toContain("2 tool calls");
|
||||
expect(groups[0].querySelector<HTMLElement>(".st-tool-group-body")!.hidden).toBe(true);
|
||||
expect(groups[0].querySelector<HTMLElement>(".st-tool-group-body")!.getAttribute("aria-hidden")).toBe("true");
|
||||
expect(conn.el.querySelector('[data-row-id="tool:3"]')?.closest(".st-tool-group")).toBeNull();
|
||||
conn.dispose();
|
||||
});
|
||||
@@ -1289,7 +1291,7 @@ describe("structured connection: transcript scrolling", () => {
|
||||
it("keeps pinned appends and patches at the bottom after layout", () => {
|
||||
const ws = new FakeSocket();
|
||||
const conn = mount({ sockets: [ws] });
|
||||
const { values } = metrics(conn, 600, 200);
|
||||
const { scroll, values } = metrics(conn, 600, 200);
|
||||
ws.open();
|
||||
ws.emit({ type: "transcript_snapshot", snapshot: { cursor: 0, rows: [] } });
|
||||
runFrames();
|
||||
@@ -1305,6 +1307,7 @@ describe("structured connection: transcript scrolling", () => {
|
||||
});
|
||||
runFrames();
|
||||
expect(values.top).toBe(520);
|
||||
expect(scroll.style.scrollBehavior).toBe("smooth");
|
||||
|
||||
values.height = 780;
|
||||
ws.emit({
|
||||
@@ -1319,6 +1322,32 @@ describe("structured connection: transcript scrolling", () => {
|
||||
});
|
||||
runFrames();
|
||||
expect(values.top).toBe(580);
|
||||
expect(scroll.style.scrollBehavior).toBe("smooth");
|
||||
conn.dispose();
|
||||
});
|
||||
|
||||
it("keeps follow-to-bottom immediate when reduced motion is requested", () => {
|
||||
vi.stubGlobal("matchMedia", () => ({ matches: true }));
|
||||
const ws = new FakeSocket();
|
||||
const conn = mount({ sockets: [ws] });
|
||||
const { scroll, values } = metrics(conn, 600, 200);
|
||||
ws.open();
|
||||
ws.emit({ type: "transcript_snapshot", snapshot: { cursor: 0, rows: [] } });
|
||||
runFrames();
|
||||
|
||||
values.height = 720;
|
||||
ws.emit({
|
||||
type: "projection_update",
|
||||
transcript: {
|
||||
type: "append",
|
||||
row: row("agent:1", "agent_text", { content: { type: "text", text: "quietly" } }),
|
||||
},
|
||||
state: idleState,
|
||||
});
|
||||
runFrames();
|
||||
|
||||
expect(values.top).toBe(520);
|
||||
expect(scroll.style.scrollBehavior).toBe("");
|
||||
conn.dispose();
|
||||
});
|
||||
|
||||
|
||||
+55
-10
@@ -148,6 +148,7 @@ export function createStructuredConnection(
|
||||
const rowEls = new Map<string, HTMLElement>();
|
||||
const expandedRowIds = new Set<string>();
|
||||
const expandedGroupIds = new Set<string>();
|
||||
const enteringRowIds = new Set<string>();
|
||||
const groupEls = new Map<string, HTMLElement>();
|
||||
let state: ReducedState = { turn: idleTurn() };
|
||||
let pendingApprovalRowId: string | null = null;
|
||||
@@ -171,7 +172,20 @@ export function createStructuredConnection(
|
||||
let scrollWorkActive = true;
|
||||
let parkedScrollTop: number | null = null;
|
||||
let scrollFrame: number | null = null;
|
||||
let pendingScroll: { top: number; follow: boolean } | null = null;
|
||||
let pendingScroll: { top: number; follow: boolean; smooth: boolean } | null = null;
|
||||
let smoothScrollTimer: number | null = null;
|
||||
let smoothFollowing = false;
|
||||
|
||||
function reduceMotion(): boolean {
|
||||
return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
|
||||
}
|
||||
|
||||
function stopSmoothFollow(): void {
|
||||
if (smoothScrollTimer !== null) window.clearTimeout(smoothScrollTimer);
|
||||
smoothScrollTimer = null;
|
||||
smoothFollowing = false;
|
||||
scrollEl.style.removeProperty("scroll-behavior");
|
||||
}
|
||||
|
||||
function cancelScrollFrame(): void {
|
||||
if (scrollFrame !== null) window.cancelAnimationFrame(scrollFrame);
|
||||
@@ -179,15 +193,17 @@ export function createStructuredConnection(
|
||||
pendingScroll = null;
|
||||
}
|
||||
|
||||
function beginTranscriptLayout(forceBottom = false): void {
|
||||
function beginTranscriptLayout(forceBottom = false, smooth = false): void {
|
||||
if (!scrollWorkActive || disposed) return;
|
||||
if (pendingScroll) {
|
||||
if (forceBottom) pendingScroll.follow = true;
|
||||
if (smooth) pendingScroll.smooth = true;
|
||||
return;
|
||||
}
|
||||
pendingScroll = {
|
||||
top: scrollEl.scrollTop,
|
||||
follow: forceBottom || followingBottom,
|
||||
smooth,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -198,19 +214,30 @@ export function createStructuredConnection(
|
||||
const pending = pendingScroll;
|
||||
pendingScroll = null;
|
||||
if (!pending || disposed || !scrollWorkActive) return;
|
||||
scrollEl.scrollTop = pending.follow
|
||||
const target = pending.follow
|
||||
? Math.max(0, scrollEl.scrollHeight - scrollEl.clientHeight)
|
||||
: pending.top;
|
||||
if (pending.follow && pending.smooth && !reduceMotion()) {
|
||||
smoothFollowing = true;
|
||||
scrollEl.style.scrollBehavior = "smooth";
|
||||
scrollEl.scrollTop = target;
|
||||
if (smoothScrollTimer !== null) window.clearTimeout(smoothScrollTimer);
|
||||
smoothScrollTimer = window.setTimeout(() => stopSmoothFollow(), 220);
|
||||
} else {
|
||||
stopSmoothFollow();
|
||||
scrollEl.scrollTop = target;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateTranscriptLayout(update: () => void, forceBottom = false): void {
|
||||
beginTranscriptLayout(forceBottom);
|
||||
function updateTranscriptLayout(update: () => void, forceBottom = false, smooth = false): void {
|
||||
beginTranscriptLayout(forceBottom, smooth);
|
||||
update();
|
||||
finishTranscriptLayout();
|
||||
}
|
||||
|
||||
function onTranscriptScroll(): void {
|
||||
if (smoothFollowing) return;
|
||||
const distance = scrollEl.scrollHeight - scrollEl.clientHeight - scrollEl.scrollTop;
|
||||
followingBottom = distance <= FOLLOW_BOTTOM_THRESHOLD_PX;
|
||||
cancelScrollFrame();
|
||||
@@ -218,12 +245,19 @@ export function createStructuredConnection(
|
||||
|
||||
function onTranscriptClick(event: Event): void {
|
||||
if ((event.target as Element | null)?.closest(".st-tool-head, .st-tool-group-head")) {
|
||||
beginTranscriptLayout();
|
||||
beginTranscriptLayout(false, true);
|
||||
}
|
||||
}
|
||||
|
||||
function settleDisclosureLayout(): void {
|
||||
finishTranscriptLayout();
|
||||
window.setTimeout(() => updateTranscriptLayout(() => {}, false, true), 190);
|
||||
}
|
||||
|
||||
scrollEl.addEventListener("scroll", onTranscriptScroll);
|
||||
scrollEl.addEventListener("click", onTranscriptClick, true);
|
||||
scrollEl.addEventListener("wheel", stopSmoothFollow, { passive: true });
|
||||
scrollEl.addEventListener("touchstart", stopSmoothFollow, { passive: true });
|
||||
|
||||
function lastRowIdOfKind(kind: TranscriptRowKind): string | null {
|
||||
for (let i = rows.length - 1; i >= 0; i--) if (rows[i].kind === kind) return rows[i].id;
|
||||
@@ -268,10 +302,11 @@ export function createStructuredConnection(
|
||||
(expanded) => {
|
||||
if (expanded) expandedRowIds.add(row.id);
|
||||
else expandedRowIds.delete(row.id);
|
||||
finishTranscriptLayout();
|
||||
settleDisclosureLayout();
|
||||
},
|
||||
);
|
||||
if (!el) return null;
|
||||
if (enteringRowIds.has(row.id)) el.classList.add("st-row-enter");
|
||||
if (el.querySelector(".st-tool-head")?.getAttribute("aria-expanded") !== "true") {
|
||||
expandedRowIds.delete(row.id);
|
||||
}
|
||||
@@ -328,9 +363,12 @@ export function createStructuredConnection(
|
||||
(expanded) => {
|
||||
if (expanded) expandedGroupIds.add(groupId);
|
||||
else expandedGroupIds.delete(groupId);
|
||||
finishTranscriptLayout();
|
||||
settleDisclosureLayout();
|
||||
},
|
||||
);
|
||||
if (groupRows.some((toolRow) => enteringRowIds.has(toolRow.id))) {
|
||||
groupEl.classList.add("st-row-enter");
|
||||
}
|
||||
groupEls.set(groupId, groupEl);
|
||||
children.push(groupEl);
|
||||
continue;
|
||||
@@ -348,6 +386,7 @@ export function createStructuredConnection(
|
||||
(child, index) => !child.classList.contains("st-turn-meta") || index === children.length - 1,
|
||||
);
|
||||
rowsEl.replaceChildren(...visibleChildren);
|
||||
enteringRowIds.clear();
|
||||
if (restoreToolFocus && focusedRowId) {
|
||||
rowEls.get(focusedRowId)?.querySelector<HTMLButtonElement>(".st-tool-head")?.focus();
|
||||
} else if (restoreGroupFocus && focusedGroupId) {
|
||||
@@ -375,6 +414,7 @@ export function createStructuredConnection(
|
||||
if (!delta) return;
|
||||
updateTranscriptLayout(() => {
|
||||
if (delta.type === "append") {
|
||||
if (receivedSnapshot) enteringRowIds.add(delta.row.id);
|
||||
rowIndex.set(delta.row.id, rows.length);
|
||||
rows.push(delta.row);
|
||||
} else if (delta.type === "patch") {
|
||||
@@ -395,7 +435,7 @@ export function createStructuredConnection(
|
||||
}
|
||||
rebuildTranscript();
|
||||
syncAvailableCommands();
|
||||
});
|
||||
}, false, true);
|
||||
}
|
||||
|
||||
function sessionConfigOptions(): AcpSessionConfigOption[] {
|
||||
@@ -710,7 +750,7 @@ export function createStructuredConnection(
|
||||
input.style.overflowY = "hidden";
|
||||
input.style.height = `${Math.min(input.scrollHeight, 160)}px`;
|
||||
if (input.scrollHeight > 160) input.style.overflowY = "auto";
|
||||
});
|
||||
}, false, true);
|
||||
}
|
||||
input.addEventListener("input", () => {
|
||||
autoResize();
|
||||
@@ -919,6 +959,7 @@ export function createStructuredConnection(
|
||||
pendingScroll = {
|
||||
top: parkedScrollTop ?? scrollEl.scrollTop,
|
||||
follow: followingBottom,
|
||||
smooth: false,
|
||||
};
|
||||
parkedScrollTop = null;
|
||||
finishTranscriptLayout();
|
||||
@@ -934,6 +975,7 @@ export function createStructuredConnection(
|
||||
parkedScrollTop = pendingScroll?.top ?? scrollEl.scrollTop;
|
||||
scrollWorkActive = false;
|
||||
cancelScrollFrame();
|
||||
stopSmoothFollow();
|
||||
cancelFocusFrame();
|
||||
},
|
||||
focusTerminal() {
|
||||
@@ -950,6 +992,7 @@ export function createStructuredConnection(
|
||||
mountToken++;
|
||||
scrollWorkActive = false;
|
||||
cancelScrollFrame();
|
||||
stopSmoothFollow();
|
||||
cancelFocusFrame();
|
||||
cancelReconnect();
|
||||
closeCurrentSocket();
|
||||
@@ -957,6 +1000,8 @@ export function createStructuredConnection(
|
||||
expandedGroupIds.clear();
|
||||
scrollEl.removeEventListener("scroll", onTranscriptScroll);
|
||||
scrollEl.removeEventListener("click", onTranscriptClick, true);
|
||||
scrollEl.removeEventListener("wheel", stopSmoothFollow);
|
||||
scrollEl.removeEventListener("touchstart", stopSmoothFollow);
|
||||
document.removeEventListener("pointerdown", closeConfigOnOutsidePress);
|
||||
el.remove();
|
||||
},
|
||||
|
||||
@@ -158,7 +158,10 @@ describe("buildTranscriptRow: tool calls", () => {
|
||||
expect(head.getAttribute("aria-expanded")).toBe("true");
|
||||
expect(out.textContent).toContain("match found");
|
||||
head.click();
|
||||
expect(out.hidden).toBe(true);
|
||||
expect(out.hidden).toBe(false);
|
||||
expect(out.getAttribute("aria-hidden")).toBe("true");
|
||||
expect(out.inert).toBe(true);
|
||||
expect(out.classList.contains("is-expanded")).toBe(false);
|
||||
});
|
||||
|
||||
it("does not expand when there is nothing to show", () => {
|
||||
@@ -196,9 +199,11 @@ describe("buildToolGroup", () => {
|
||||
const head = group.querySelector<HTMLButtonElement>(".st-tool-group-head")!;
|
||||
const body = group.querySelector<HTMLElement>(".st-tool-group-body")!;
|
||||
expect(head.textContent).toContain("2 tool calls");
|
||||
expect(body.hidden).toBe(true);
|
||||
expect(body.getAttribute("aria-hidden")).toBe("true");
|
||||
expect(body.inert).toBe(true);
|
||||
head.click();
|
||||
expect(body.hidden).toBe(false);
|
||||
expect(body.getAttribute("aria-hidden")).toBe("false");
|
||||
expect(body.classList.contains("is-expanded")).toBe(true);
|
||||
expect(onExpanded).toHaveBeenCalledWith(true);
|
||||
const toolHeads = group.querySelectorAll<HTMLButtonElement>(".st-tool-head");
|
||||
toolHeads[1].click();
|
||||
|
||||
+40
-16
@@ -269,6 +269,19 @@ function appendToolDetail(container: HTMLElement, label: string, text: string):
|
||||
container.appendChild(detail);
|
||||
}
|
||||
|
||||
function setDisclosure(element: HTMLElement, expanded: boolean, animate = false): void {
|
||||
element.hidden = false;
|
||||
if (animate && expanded && !element.classList.contains("is-expanded")) {
|
||||
// Establish the collapsed grid track before opening it in the same event
|
||||
// turn. Without this read, a freshly lazy-built tool body has no painted
|
||||
// starting state and the browser quite reasonably skips the transition.
|
||||
void element.offsetHeight;
|
||||
}
|
||||
element.classList.toggle("is-expanded", expanded);
|
||||
element.setAttribute("aria-hidden", String(!expanded));
|
||||
element.inert = !expanded;
|
||||
}
|
||||
|
||||
/** `+added -removed` across every diff block in a tool call's content —
|
||||
* real derived data, never a fabricated duration. */
|
||||
function toolDiffMeta(content: AcpToolCallContent[]): string {
|
||||
@@ -331,17 +344,21 @@ function buildToolRow(
|
||||
}
|
||||
card.appendChild(head);
|
||||
|
||||
const out = el("div", "st-tool-out");
|
||||
const out = el("div", "st-tool-out st-disclosure");
|
||||
const outClip = el("div", "st-disclosure-clip");
|
||||
const outInner = el("div", "st-disclosure-inner");
|
||||
outClip.appendChild(outInner);
|
||||
out.appendChild(outClip);
|
||||
out.hidden = true;
|
||||
let outBuilt = false;
|
||||
const buildOut = () => {
|
||||
if (outBuilt) return;
|
||||
outBuilt = true;
|
||||
const { command, input } = toolInput(tool);
|
||||
if (command) appendToolDetail(out, "Command", command);
|
||||
if (input !== null) appendToolDetail(out, "Input", input);
|
||||
if (command) appendToolDetail(outInner, "Command", command);
|
||||
if (input !== null) appendToolDetail(outInner, "Input", input);
|
||||
const rawOutput = printable(tool.rawOutput);
|
||||
if (rawOutput !== null) appendToolDetail(out, "Output", rawOutput);
|
||||
if (rawOutput !== null) appendToolDetail(outInner, "Output", rawOutput);
|
||||
const texts = content
|
||||
.filter((c): c is Extract<AcpToolCallContent, { type: "content" }> => c.type === "content")
|
||||
.map((c) => contentBlockText(c.content))
|
||||
@@ -351,28 +368,31 @@ function buildToolRow(
|
||||
const textPanel = el("div", "st-tool-text");
|
||||
textPanel.textContent = contentText;
|
||||
makeCopyable(textPanel, contentText, "output");
|
||||
out.appendChild(textPanel);
|
||||
outInner.appendChild(textPanel);
|
||||
}
|
||||
for (const block of content) {
|
||||
if (block.type === "diff") out.appendChild(buildDiffBlock(block));
|
||||
if (block.type === "diff") outInner.appendChild(buildDiffBlock(block));
|
||||
if (block.type === "terminal") {
|
||||
const note = el("div", "st-tool-text");
|
||||
note.textContent = "[embedded terminal]";
|
||||
out.appendChild(note);
|
||||
outInner.appendChild(note);
|
||||
}
|
||||
}
|
||||
if (out.children.length === 0) out.hidden = true;
|
||||
};
|
||||
card.appendChild(out);
|
||||
|
||||
const setExpanded = (nextExpanded: boolean) => {
|
||||
const setExpanded = (nextExpanded: boolean, animate = false) => {
|
||||
if (nextExpanded) buildOut();
|
||||
if (out.children.length === 0) nextExpanded = false;
|
||||
if (outInner.children.length === 0) nextExpanded = false;
|
||||
head.setAttribute("aria-expanded", String(nextExpanded));
|
||||
out.hidden = !nextExpanded;
|
||||
if (outInner.children.length === 0) {
|
||||
out.hidden = true;
|
||||
} else {
|
||||
setDisclosure(out, nextExpanded, animate);
|
||||
}
|
||||
};
|
||||
head.addEventListener("click", () => {
|
||||
setExpanded(head.getAttribute("aria-expanded") !== "true");
|
||||
setExpanded(head.getAttribute("aria-expanded") !== "true", true);
|
||||
onExpandedChange?.(head.getAttribute("aria-expanded") === "true");
|
||||
});
|
||||
setExpanded(expanded);
|
||||
@@ -408,13 +428,17 @@ export function buildToolGroup(
|
||||
.join(" · ");
|
||||
head.append(glyph, title, names);
|
||||
|
||||
const body = el("div", "st-tool-group-body");
|
||||
body.hidden = !expanded;
|
||||
body.append(...children);
|
||||
const body = el("div", "st-tool-group-body st-disclosure");
|
||||
const bodyClip = el("div", "st-disclosure-clip");
|
||||
const bodyInner = el("div", "st-disclosure-inner");
|
||||
bodyInner.append(...children);
|
||||
bodyClip.appendChild(bodyInner);
|
||||
body.appendChild(bodyClip);
|
||||
setDisclosure(body, expanded);
|
||||
head.addEventListener("click", () => {
|
||||
expanded = !expanded;
|
||||
head.setAttribute("aria-expanded", String(expanded));
|
||||
body.hidden = !expanded;
|
||||
setDisclosure(body, expanded, true);
|
||||
onExpandedChange?.(expanded);
|
||||
});
|
||||
wrap.append(head, body);
|
||||
|
||||
+60
-6
@@ -2601,6 +2601,7 @@ input.leaf-rename:focus {
|
||||
gap: var(--sp-2);
|
||||
padding: var(--sp-2) var(--sp-3);
|
||||
min-width: 0;
|
||||
transition: background var(--dur-fast) var(--ease-out);
|
||||
}
|
||||
|
||||
.st-tool-head:hover {
|
||||
@@ -2650,11 +2651,38 @@ input.leaf-rename:focus {
|
||||
}
|
||||
|
||||
.st-tool-out {
|
||||
border-top: 1px solid var(--border);
|
||||
padding: var(--sp-2) var(--sp-3);
|
||||
border-top: 1px solid transparent;
|
||||
}
|
||||
|
||||
.st-disclosure {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
opacity: 0;
|
||||
transition:
|
||||
grid-template-rows var(--dur) var(--ease-out),
|
||||
opacity var(--dur-fast) var(--ease-out),
|
||||
border-color var(--dur-fast) var(--ease-out);
|
||||
}
|
||||
|
||||
.st-disclosure.is-expanded {
|
||||
grid-template-rows: 1fr;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.st-disclosure-clip {
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.st-tool-out.is-expanded {
|
||||
border-top-color: var(--border);
|
||||
}
|
||||
|
||||
.st-tool-out .st-disclosure-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-2);
|
||||
padding: var(--sp-2) var(--sp-3);
|
||||
}
|
||||
|
||||
/* `display: flex` above outranks the UA sheet's `[hidden]` (same pitfall as
|
||||
@@ -2723,6 +2751,7 @@ input.leaf-rename:focus {
|
||||
gap: var(--sp-2);
|
||||
padding: 0;
|
||||
min-width: 0;
|
||||
transition: color var(--dur-fast) var(--ease-out);
|
||||
}
|
||||
|
||||
.st-tool-group-head:hover {
|
||||
@@ -2753,12 +2782,16 @@ input.leaf-rename:focus {
|
||||
}
|
||||
|
||||
.st-tool-group-body {
|
||||
padding-top: var(--sp-2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.st-tool-group-body > .st-row-tool {
|
||||
.st-tool-group-body .st-disclosure-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: var(--sp-2);
|
||||
}
|
||||
|
||||
.st-tool-group-body .st-disclosure-inner > .st-row-tool {
|
||||
padding: var(--sp-1) 0;
|
||||
}
|
||||
|
||||
@@ -2766,6 +2799,21 @@ input.leaf-rename:focus {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Live transcript rows resolve into the stream through a short occlusion,
|
||||
matching data becoming concrete without replaying on token patches. */
|
||||
.st-row-enter {
|
||||
animation: structured-row-enter var(--dur) var(--ease-out) both;
|
||||
}
|
||||
|
||||
@keyframes structured-row-enter {
|
||||
from {
|
||||
opacity: 0.2;
|
||||
transform: translateY(3px);
|
||||
clip-path: inset(0 0 18% 0);
|
||||
filter: blur(1px);
|
||||
}
|
||||
}
|
||||
|
||||
.st-diff-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -3134,10 +3182,16 @@ input.leaf-rename:focus {
|
||||
.act,
|
||||
.st-copyable,
|
||||
.st-copyable::after,
|
||||
.st-disclosure,
|
||||
.st-tool-head,
|
||||
.st-tool-group-glyph {
|
||||
transition-duration: 0.01ms;
|
||||
}
|
||||
|
||||
.st-row-enter {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.structured-activity-pulse {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user