diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx
index 8f91f08c7093..76f17801d5af 100644
--- a/apps/web/src/components/chat/ChatComposer.tsx
+++ b/apps/web/src/components/chat/ChatComposer.tsx
@@ -53,7 +53,6 @@ import { folderDropTarget, resolveDroppedFolderPath } from "./folderDrop";
import { createModelSelection, normalizeModelSlug } from "@t3tools/shared/model";
import { USAGE_LIMITS_COMMAND } from "@t3tools/shared/usageLimits";
import {
- Fragment,
memo,
type ComponentProps,
type ReactNode,
@@ -185,17 +184,14 @@ import {
import { useComposerPathSearch } from "../../lib/composerPathSearchState";
import { replaceComposerContextReferences } from "@t3tools/shared/composerContextReferences";
import {
- COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX,
- COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX,
getRestingComposerImagePreviewCounts,
resolveRestingComposerControlsLayout,
shouldAnimateComposerRestingTransition,
shouldUseCompactComposerPrimaryActions,
- shouldUseCompactComposerFooter,
+ resolveRestingComposerControlsNaturalWidth,
shouldUseRestingComposerLayout,
} from "../composerFooterLayout";
import { measureRestingComposerControls } from "./restingComposerControlsMeasurement";
-import { observeResponsiveBreakpointFade, usePanelAnimationSettings } from "../../panelAnimations";
import { type ComposerPromptEditorHandle, ComposerPromptEditor } from "../ComposerPromptEditor";
import {
ComposerContextActionsContext,
@@ -2096,8 +2092,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
key: 0,
active: false,
});
- const { active: panelAnimationsActive, durationMs: panelAnimationDurationMs } =
- usePanelAnimationSettings();
const isComposerCollapsedMobile =
isMobileViewport && !forceExpandedOnMobile && !isComposerFocused && !hasMultilinePrompt;
@@ -3133,71 +3127,48 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
// ------------------------------------------------------------------
// Footer compact layout observation
// ------------------------------------------------------------------
- useLayoutEffect(() => {
+ const measureFooterCompactness = useCallback(() => {
const composerForm = composerFormRef.current;
if (!composerForm) return;
- const measureComposerFormWidth = () => composerForm.clientWidth;
- const measureFooterCompactness = () => {
- const composerFormWidth = measureComposerFormWidth();
- const footerCompact = shouldUseCompactComposerFooter(composerFormWidth, {
+ setIsComposerPrimaryActionsCompact(
+ shouldUseCompactComposerPrimaryActions(composerForm.clientWidth, {
hasWideActions: composerFooterHasWideActions,
- });
- const primaryActionsCompact =
- footerCompact &&
- shouldUseCompactComposerPrimaryActions(composerFormWidth, {
- hasWideActions: composerFooterHasWideActions,
- });
- return {
- primaryActionsCompact,
- footerCompact,
- };
- };
-
- const initialCompactness = measureFooterCompactness();
- setIsComposerPrimaryActionsCompact(initialCompactness.primaryActionsCompact);
- setIsComposerFooterCompact(initialCompactness.footerCompact);
- if (typeof ResizeObserver === "undefined") return;
- const footerControls = composerFooterControlsRef.current;
- const stopFooterControlsFade = footerControls
- ? observeResponsiveBreakpointFade({
- target: footerControls,
- container: composerForm,
- active: panelAnimationsActive,
- durationMs: panelAnimationDurationMs,
- breakpoint: {
- value: composerFooterHasWideActions
- ? COMPOSER_FOOTER_WIDE_ACTIONS_COMPACT_BREAKPOINT_PX
- : COMPOSER_FOOTER_COMPACT_BREAKPOINT_PX,
- unit: "px",
- },
- })
- : undefined;
-
- const observer = new ResizeObserver(() => {
- const nextCompactness = measureFooterCompactness();
- setIsComposerPrimaryActionsCompact((previous) =>
- previous === nextCompactness.primaryActionsCompact
- ? previous
- : nextCompactness.primaryActionsCompact,
- );
- setIsComposerFooterCompact((previous) =>
- previous === nextCompactness.footerCompact ? previous : nextCompactness.footerCompact,
- );
- });
-
+ }),
+ );
+ const controls = composerFooterControlsRef.current;
+ if (!controls || controls.clientWidth === 0) return;
+ const measurement = measureRestingComposerControls(controls);
+ if (!measurement) return;
+ const style = getComputedStyle(controls);
+ const availableWidth =
+ controls.clientWidth -
+ (Number.parseFloat(style.paddingInlineStart) || 0) -
+ (Number.parseFloat(style.paddingInlineEnd) || 0);
+ const naturalWidth = resolveRestingComposerControlsNaturalWidth(measurement);
+ // Hidden controls keep their natural width, so reopening the footer does
+ // not depend on the smaller overflow menu. Leave a pixel for rounding.
+ setIsComposerFooterCompact((previous) => naturalWidth > availableWidth - (previous ? 1 : 0));
+ }, [composerFooterHasWideActions]);
+
+ useLayoutEffect(measureFooterCompactness);
+ useEffect(() => {
+ const composerForm = composerFormRef.current;
+ if (!composerForm || typeof ResizeObserver === "undefined") return;
+ const observer = new ResizeObserver(measureFooterCompactness);
observer.observe(composerForm);
+ const controls = composerFooterControlsRef.current;
+ if (controls) observer.observe(controls);
+ document.fonts.addEventListener("loadingdone", measureFooterCompactness);
return () => {
observer.disconnect();
- stopFooterControlsFade?.();
+ document.fonts.removeEventListener("loadingdone", measureFooterCompactness);
};
}, [
activeThreadId,
composerFooterActionLayoutKey,
- composerFooterHasWideActions,
isComposerApprovalState,
isComposerCollapsedMobile,
- panelAnimationDurationMs,
- panelAnimationsActive,
+ measureFooterCompactness,
]);
// ------------------------------------------------------------------
@@ -4909,7 +4880,12 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
content: (
<>