Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 13 additions & 16 deletions src/web-ui/src/flow_chat/components/ChatInput.scss
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,6 @@
// Match the 7px box padding so the round Plus button keeps equal
// clearance on all four sides (top/bottom/left/right)
margin-right: 7px;
animation: none;

// Enlarge the Plus button to match the capsule height
.bitfun-chat-input__agent-boost-add {
Expand Down Expand Up @@ -418,7 +417,6 @@
align-items: center;
gap: $size-gap-1;
padding: 0;
animation: none;

// Enlarge send/stop button to match the capsule height
.bitfun-chat-input__send-button {
Expand Down Expand Up @@ -631,11 +629,16 @@
background: var(--color-bg-scene);
backdrop-filter: blur(16px) saturate(1.2);
-webkit-backdrop-filter: blur(16px) saturate(1.2);
// `max-height` must never animate: the box height is `auto`, so an
// interpolating `max-height` clamps the border box below its own content
// while `overflow` stays visible (popovers need it) — the text and the
// action row then paint *outside* the rounded border for the length of the
// transition. Only `min-height` is animated: it can never fall below the
// content, so it smooths the collapse without ever clipping the card.
transition:
border-radius 0.32s cubic-bezier(0.4, 0, 0.2, 1),
padding 0.32s cubic-bezier(0.4, 0, 0.2, 1),
min-height 0.32s cubic-bezier(0.4, 0, 0.2, 1),
max-height 0.32s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.32s cubic-bezier(0.4, 0, 0.2, 1),
border-color 0.32s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow:
Expand Down Expand Up @@ -1721,12 +1724,18 @@
padding: 0 0 $size-gap-2;
}

// Keep the action groups free of entrance animations. The composer mounts in
// capsule mode, which used to null the animation out, so a reveal here never
// played on mount — it only restarted on every capsule → multi-line flip, and
// its `both` fill held the group at `opacity: 0` through the delay. That left
// the plus button and the model/mic/send cluster blank for ~0.2s and still
// sliding ~0.45s after the box had already resized, which reads as the card
// and its controls desyncing on a single keystroke at the wrap boundary.
&__actions-left {
display: flex;
align-items: center;
gap: $size-gap-1;
min-width: 0;
animation: bitfun-stacked-reveal 0.28s cubic-bezier(0.4, 0, 0.2, 1) 0.17s both;
}

&__model-usage-group {
Expand All @@ -1741,7 +1750,6 @@
align-items: center;
gap: $size-gap-1;
height: 100%;
animation: bitfun-stacked-reveal 0.24s cubic-bezier(0.4, 0, 0.2, 1) 0.22s both;
}

&__split-actions {
Expand Down Expand Up @@ -2119,14 +2127,3 @@
transform: translateY(0);
}
}

@keyframes bitfun-stacked-reveal {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
20 changes: 19 additions & 1 deletion src/web-ui/src/flow_chat/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,25 @@ export const ChatInput: React.FC<ChatInputProps> = ({
}

let rafId: number | null = null;
const observer = new ResizeObserver(() => {
// Only width feeds the capsule measurement, and re-measuring clones the whole
// composer into the document (two forced layouts). The box height animates on
// every capsule ↔ multi-line flip, so reacting to height would run that clone
// once per frame of the transition — exactly while the user is typing at the
// wrap boundary. Ignore entries whose width is unchanged.
const lastObservedWidths = new WeakMap<Element, number>();
const observer = new ResizeObserver(entries => {
let widthChanged = false;
for (const entry of entries) {
const width = entry.contentRect.width;
const previousWidth = lastObservedWidths.get(entry.target);
if (previousWidth === undefined || Math.abs(previousWidth - width) >= 0.5) {
widthChanged = true;
}
lastObservedWidths.set(entry.target, width);
}
if (!widthChanged) {
return;
}
if (rafId !== null) {
cancelAnimationFrame(rafId);
}
Expand Down